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 @@ -2,12 +2,14 @@

import java.util.ArrayList;
import java.util.List;
import org.bukkit.FluidCollisionMode;
import org.bukkit.Location;
import org.bukkit.Particle;
import org.bukkit.World;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.util.RayTraceResult;
import org.bukkit.util.Vector;

import net.tfminecraft.vehicleframework.VehicleFramework;
Expand All @@ -23,6 +25,13 @@
public class DefaultShooter implements Shooter {
//Regular shooting method for things like cannonballs, explode on impact

// Vanilla armour stand physics per tick, so bomblets fall as they did under vanilla movement.
private static final double CLUSTER_GRAVITY = 0.08;
private static final double CLUSTER_VERTICAL_DRAG = 0.98;
private static final double CLUSTER_HORIZONTAL_DRAG = 0.91;
// Safety stop for a bomblet that never reaches anything.
private static final int CLUSTER_MAX_TICKS = 200;

private HitChecker checker = new HitChecker();
private ProjectileShooter shooter;

Expand Down Expand Up @@ -108,29 +117,60 @@ private void sendCluster(Location original, ClusterBomb a, List<Player> players,
Entity armorStand = a.getClusterData().spawn(original);
projectiles.add(armorStand);
Cache.projectiles.add(armorStand);
// The task below moves it; vanilla gravity would move it a second time near players.
armorStand.setGravity(false);

Vector velocity = new Vector(
(Math.random() - 0.5) * a.getSpread(),
-3.5, // downward velocity
(Math.random() - 0.5) * a.getSpread()
);
armorStand.setVelocity(velocity);

// Handle explosion on ground impact
new BukkitRunnable() {
int i = 0;
boolean landed = false;
Location current = armorStand.getLocation();
public void run() {
if (i > 5 && (checker.hasHit(armorStand, projectiles) || armorStand.isOnGround() || armorStand.isDead())) {
if (i > 5 && (landed || checker.hasHit(armorStand, projectiles) || armorStand.isDead())) {
shooter.triggerExplosion(players, armorStand.getLocation(), a.getClusterData(), w);
armorStand.remove();
projectiles.remove(armorStand);
Cache.projectiles.remove(armorStand);
cancel();
removeBomblet();
return;
}
if (i >= CLUSTER_MAX_TICKS) {
removeBomblet();
return;
}
if (!landed) {
landed = move();
}
a.getData().fx(players, armorStand.getLocation(), 1f, i);
i++;
}

// Not left to vanilla physics: entities stop moving outside simulation distance.
private boolean move() {
RayTraceResult hit = world.rayTraceBlocks(current, velocity.clone().normalize(), velocity.length(), FluidCollisionMode.ALWAYS, false);
if (hit != null) {
// Stop on the surface instead of passing through thin roofs or into the ground.
current = hit.getHitPosition().toLocation(world, current.getYaw(), current.getPitch());
armorStand.teleport(current);
return true;
}
current.add(velocity);
armorStand.teleport(current);
velocity.setX(velocity.getX() * CLUSTER_HORIZONTAL_DRAG);
velocity.setY((velocity.getY() - CLUSTER_GRAVITY) * CLUSTER_VERTICAL_DRAG);
velocity.setZ(velocity.getZ() * CLUSTER_HORIZONTAL_DRAG);
return false;
}

private void removeBomblet() {
armorStand.remove();
projectiles.remove(armorStand);
Cache.projectiles.remove(armorStand);
cancel();
}
}.runTaskTimer(VehicleFramework.plugin, 0L, 1L);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -971,13 +971,31 @@ void clusterFuseCreatesConfiguredChildrenAndCleansThemOnImpact() throws Exceptio
verify(shell).remove();
assertEquals(3, r.spawned.size());
assertEquals(2, Cache.projectiles.size());
for (Entity child : r.spawned.subList(1, 3)) {
assertEquals(new Vector(0, -3.5, 0), child.getVelocity());
when(child.isOnGround()).thenReturn(true);
List<Entity> children = r.spawned.subList(1, 3);
Map<Entity, Double> startY = new HashMap<>();
for (Entity child : children) {
verify(child).setGravity(false);
verify(child, never()).setVelocity(any());
startY.put(child, child.getLocation().getY());
}
r.ticks(7);
r.tick();
for (Entity child : children)
assertEquals(startY.get(child) - 3.5, child.getLocation().getY(), 1e-9);
when(r.world.rayTraceBlocks(
any(Location.class),
any(Vector.class),
anyDouble(),
eq(FluidCollisionMode.ALWAYS),
eq(false)))
.thenReturn(new RayTraceResult(new Vector(0, 40, 0)));
r.ticks(5);
assertEquals(2, Cache.projectiles.size(), "Landed bomblets still wait out the arming delay");
r.tick();
assertTrue(Cache.projectiles.isEmpty());
for (Entity child : r.spawned.subList(1, 3)) verify(child).remove();
for (Entity child : children) {
verify(child).remove();
assertEquals(40, child.getLocation().getY(), 1e-9);
}
verify(player)
.spawnParticle(
eq(Particle.EXPLOSION_EMITTER),
Expand Down Expand Up @@ -1016,6 +1034,82 @@ void clusterImpactAtFuseBoundaryDetonatesOnlyOnce() throws Exception {
}
}

@Test
void clusterBombletsFallWithArmourStandDragWithoutVanillaTicks() throws Exception {
Entity child = fireCluster(7.5);
Location start = child.getLocation();
r.tick();
Location first = child.getLocation();
r.tick();
Location second = child.getLocation();
assertEquals(start.getY() - 3.5, first.getY(), 1e-9);
assertEquals(first.getY() - 3.58 * .98, second.getY(), 1e-9);
double firstStepX = first.getX() - start.getX();
double secondStepX = second.getX() - first.getX();
double firstStepZ = first.getZ() - start.getZ();
double secondStepZ = second.getZ() - first.getZ();
assertEquals(firstStepX * .91, secondStepX, 1e-9);
assertEquals(firstStepZ * .91, secondStepZ, 1e-9);
verify(child, never()).isOnGround();
}

@Test
void clusterBombletsDetonateNextToBlocksTheyDidNotRayTrace() throws Exception {
try (var explosions = mockStatic(ExplosionCreator.class)) {
Entity child = fireCluster(0);
r.ticks(6);
verify(child, never()).remove();
Location at = child.getLocation();
r.block(at.getBlockX(), at.getBlockY() - 1, at.getBlockZ(), Material.STONE);
r.tick();
verify(child).remove();
assertTrue(Cache.projectiles.isEmpty());
explosions.verify(
() ->
ExplosionCreator.triggerExplosion(
eq(at), anyDouble(), anyDouble(), anyDouble(), anyString(), eq(false)));
}
}

@Test
void clusterBombletsThatNeverLandAreRemovedWithoutExploding() throws Exception {
try (var explosions = mockStatic(ExplosionCreator.class)) {
Entity child = fireCluster(0);
r.ticks(200);
verify(child, never()).remove();
r.tick();
verify(child).remove();
assertTrue(Cache.projectiles.isEmpty());
assertEquals(0, r.pending());
explosions.verifyNoInteractions();
}
}

private Entity fireCluster(double spread) throws Exception {
ClusterBomb ammo =
(ClusterBomb)
ammo(
"CLUSTER",
"fuse: 1\n"
+ "amount: 1\n"
+ "spread: " + spread + "\n"
+ "explosive: false\n"
+ "cluster:\n"
+ " explosive: true\n"
+ " model: {type: item, material: STONE}\n");
new ProjectileShooter()
.shoot(
List.of(),
vehicle.getEntity(),
new Location(r.world, 0, 1000, 0),
new Vector(0, 0, 1),
ammo,
null);
r.tick();
assertEquals(2, r.spawned.size());
return r.spawned.getLast();
}

@Test
void torpedoesLevelAccelerateInWaterAndExpireAfterTheirFuse() throws Exception {
for (boolean waterlogged : List.of(false, true)) {
Expand Down
Loading