Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@
public abstract class Ball extends ThrowableProjectile {
public static final String REBOUNDS_KEY = "rebounds";

/** Trail particles spawned per tick, strung along the ground the ball covers during it. */
private static final int TRAIL_PARTICLES_PER_TICK = 1;
/** Distance travelled in a tick under which a ball counts as standing still and trails nothing. */
private static final double MIN_TRAIL_DISTANCE = 0.01D;
/** Share of the speed of the ball a trail particle is pushed back by, the rest of it being shed. */
private static final double TRAIL_DRIFT = 0.05D;
/** Upwards drift of a trail particle, so that it lingers behind rather than sinking with the ball. */
private static final double TRAIL_RISE = 0.02D;

protected int rebounds = 3;
private boolean rotateClockwards = false;

Expand Down Expand Up @@ -57,13 +66,25 @@ protected void defineSynchedData(SynchedEntityData.Builder builder) {}
@Override
public void tick() {
super.tick();
if (this.level().isClientSide()) {
this.spawnTrailParticles();
}
}

@Nullable
protected abstract SoundEvent getDeathSound();

protected abstract ParticleOptions getDeathParticle();

/**
* @return the particle the ball trails behind it while it moves, or {@code null} for a ball that
* leaves no trail at all
*/
@Nullable
protected ParticleOptions getTrailParticle() {
return null;
}

@Override
protected double getDefaultGravity() {
return 0.08;
Expand Down Expand Up @@ -127,6 +148,43 @@ public void handleEntityEvent(byte state) {
}
}

/**
* Spawns the trail a moving ball leaves behind, in the manner of the one vanilla arrows leave:
* particles strung along the movement of the tick and pushed back the way the ball came, so that
* they fall behind it instead of riding along with it. Arrows hand the particles their whole speed,
* which is far too brisk for a ball, so only a fraction of it is passed on here.
* <p>
* A ball only trails while it actually moves: one that has come to a halt emits nothing.
*/
protected void spawnTrailParticles() {
ParticleOptions particle = this.getTrailParticle();
if (particle == null || this.isRemoved()) {
return;
}
Vec3 movement = this.getDeltaMovement();
int count = trailParticleCount(movement.length());
// The model of a ball is centred on its position rather than standing on it, so the particles
// need no offset of their own to sit in the middle of the sprite.
Vec3 from = this.position();
Vec3 drift = movement.scale(-TRAIL_DRIFT).add(0.0D, TRAIL_RISE, 0.0D);

for (int i = 0; i < count; i++) {
Vec3 pos = from.add(movement.scale((i + 0.5D) / count));
this.level().addParticle(particle, pos.x, pos.y, pos.z, drift.x, drift.y, drift.z);
}
}

/**
* The trail is worth as many particles whatever the speed: a fast ball spaces them out further
* instead of spawning more of them.
*
* @param distance how far the ball travelled during the tick, in blocks
* @return how many particles to spawn, none at all for a ball that barely moved
*/
public static int trailParticleCount(double distance) {
return distance < MIN_TRAIL_DISTANCE ? 0 : TRAIL_PARTICLES_PER_TICK;
}

protected void spawnDeathParticles() {
for (int i = 0; i < 8; ++i) {
float s1 = random.nextFloat() * 0.2F - 0.1F;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ protected ParticleOptions getDeathParticle() {
return ParticleTypes.FLAME;
}

@Override
protected ParticleOptions getTrailParticle() {
// Smaller than the flames of the burst, so that the trail reads as embers rather than as fire.
return ParticleTypes.SMALL_FLAME;
}

@Override
protected void onHitEntity(EntityHitResult result) {
super.onHitEntity(result);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ protected ParticleOptions getDeathParticle() {
return SuperMarioParticleTypes.COIN_SPARKLE; //TODO change
}

@Override
protected ParticleOptions getTrailParticle() {
return SuperMarioParticleTypes.COIN_SPARKLE;
}

@Override
protected void onHitEntity(EntityHitResult hitResult) {
super.onHitEntity(hitResult);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ protected ParticleOptions getDeathParticle() {
return ParticleTypes.CLOUD;
}

@Override
protected ParticleOptions getTrailParticle() {
return ParticleTypes.SNOWFLAKE;
}

@Override
protected void onHitEntity(EntityHitResult result) {
super.onHitEntity(result);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package fr.hugman.mubble.test.unit;

import fr.hugman.mubble.test.unit.support.TestBootstrap;
import fr.hugman.mubble.world.entity.projectile.Ball;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
* Whether a {@link Ball} trails anything at all depends on the ground it covers during the tick. The
* particles themselves only exist on a client, but the count behind them is plain maths and belongs
* here rather than in a game test.
*/
public class BallTrailTest {
/** Roughly the distance a ball thrown by a player covers in one tick. */
private static final double THROWN_BALL_DISTANCE = 1.5D;

@BeforeAll
static void bootstrapMinecraft() {
TestBootstrap.bootstrap();
}

@Test
@DisplayName("a ball that has come to a halt trails nothing")
void motionlessBallTrailsNothing() {
assertEquals(0, Ball.trailParticleCount(0.0D), "a ball standing still should not trail anything");
// A ball resting on the ground still jitters by a fraction of a block, which is no movement to speak of.
assertEquals(0, Ball.trailParticleCount(0.001D), "a barely moving ball should not trail anything");
}

@Test
@DisplayName("a moving ball always trails at least one particle")
void movingBallAlwaysTrailsSomething() {
assertTrue(Ball.trailParticleCount(0.05D) >= 1, "a slowly moving ball should still trail something");
assertTrue(Ball.trailParticleCount(THROWN_BALL_DISTANCE) >= 1, "a thrown ball should trail something");
}

@Test
@DisplayName("going faster spaces the trail out rather than thickening it")
void speedDoesNotThickenTheTrail() {
int slow = Ball.trailParticleCount(0.05D);
int fast = Ball.trailParticleCount(THROWN_BALL_DISTANCE);
int absurd = Ball.trailParticleCount(1000.0D);

assertEquals(slow, fast, "a faster ball should trail no more particles than a slow one");
assertEquals(slow, absurd, "however fast a ball goes, a single tick should never spawn a screenful of particles");
}
}
Loading