Skip to content

Commit

Permalink
Add projectile damage prediction
Browse files Browse the repository at this point in the history
  • Loading branch information
ZakShearman committed Jul 9, 2023
1 parent e7d045d commit c984aa4
Show file tree
Hide file tree
Showing 10 changed files with 124 additions and 11 deletions.
2 changes: 1 addition & 1 deletion src/main/java/pink/zak/minestom/towerdefence/Tags.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import org.jetbrains.annotations.NotNull;

public class Tags {
// Real time in ms of when a mob can be stunned again
// Current time in ms of when a mob can be stunned again
public static final Tag<Long> NEXT_STUNNABLE_TIME = Tag.Long("towerdefence:NEXT_STUNNABLE_TIME");

public static <T> T getOrDefault(@NotNull Taggable taggable, Tag<T> tag, T defaultValue) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,11 @@
import dev.emortal.api.modules.ModuleEnvironment;
import dev.emortal.minestom.core.module.MinestomModule;
import dev.emortal.minestom.core.module.kubernetes.KubernetesModule;
import net.kyori.adventure.text.Component;
import net.minestom.server.MinecraftServer;
import net.minestom.server.adventure.audience.Audiences;
import net.minestom.server.command.CommandManager;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.event.Event;
import net.minestom.server.event.EventNode;
import net.minestom.server.event.entity.projectile.ProjectileCollideWithBlockEvent;
import net.minestom.server.event.player.PlayerLoginEvent;
import org.jetbrains.annotations.NotNull;
import pink.zak.minestom.towerdefence.agones.AgonesManager;
Expand Down Expand Up @@ -46,10 +43,6 @@ public class TowerDefenceModule extends MinestomModule {
protected TowerDefenceModule(@NotNull ModuleEnvironment environment) {
super(environment);

MinecraftServer.getGlobalEventHandler().addListener(ProjectileCollideWithBlockEvent.class, event -> {
Audiences.all().sendMessage(Component.text("Projectile %s collided with block at %s".formatted(event.getEntity().getEntityId(), event.getCollisionPosition().asVec())));
});

this.kubernetesModule = super.getModule(KubernetesModule.class);
this.agonesManager = new AgonesManager(this.kubernetesModule);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ public static void main(String[] args) {
.commonModules()
.module(MonitoringModule.class, MonitoringModule::new)
.module(TowerDefenceModule.class, TowerDefenceModule::new)
.build();
.buildAndStart();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import pink.zak.minestom.towerdefence.model.map.TowerMap;
import pink.zak.minestom.towerdefence.model.mob.QueuedEnemyMob;
import pink.zak.minestom.towerdefence.model.mob.living.LivingTDEnemyMob;
import pink.zak.minestom.towerdefence.model.prediction.DamagePredictable;
import pink.zak.minestom.towerdefence.model.tower.placed.PlacedAttackingTower;
import pink.zak.minestom.towerdefence.model.user.GameUser;
import pink.zak.minestom.towerdefence.utils.TDEnvUtils;
Expand Down Expand Up @@ -75,6 +76,7 @@ private void updateAttackingTowers() {
// todo is there a way other than recalculating every time? Sure this is easy, but not great on performance
private void updateAttackingTowers(@NotNull Team team) {
List<LivingTDEnemyMob> distanceSortedMobs = new ArrayList<>(this.getMobs(team));
distanceSortedMobs.removeIf(DamagePredictable::isPredictedDead);
distanceSortedMobs.sort(Comparator.comparingDouble(LivingTDEnemyMob::getTotalDistanceMoved).reversed());

for (PlacedAttackingTower<?> tower : this.towerHandler.getTowers(team)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import pink.zak.minestom.towerdefence.model.mob.modifier.SpeedModifier;
import pink.zak.minestom.towerdefence.model.mob.statuseffect.StatusEffect;
import pink.zak.minestom.towerdefence.model.mob.statuseffect.StatusEffectType;
import pink.zak.minestom.towerdefence.model.prediction.DamagePredictable;
import pink.zak.minestom.towerdefence.model.tower.config.towers.level.NecromancerTowerLevel;
import pink.zak.minestom.towerdefence.model.tower.placed.PlacedAttackingTower;
import pink.zak.minestom.towerdefence.model.tower.placed.types.NecromancerTower;
Expand All @@ -36,7 +37,7 @@
import java.util.Map;
import java.util.Set;

public interface LivingTDEnemyMob extends LivingTDMob, Taggable {
public interface LivingTDEnemyMob extends LivingTDMob, Taggable, DamagePredictable {

static LivingTDEnemyMob create(GameHandler gameHandler, EnemyMob enemyMob, int level, Instance instance, TowerMap map, GameUser gameUser) {
EntityType entityType = enemyMob.getLevel(level).getEntityType();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import pink.zak.minestom.towerdefence.model.mob.modifier.SpeedModifier;
import pink.zak.minestom.towerdefence.model.mob.statuseffect.StatusEffect;
import pink.zak.minestom.towerdefence.model.mob.statuseffect.StatusEffectType;
import pink.zak.minestom.towerdefence.model.prediction.DamagePredictionHandler;
import pink.zak.minestom.towerdefence.model.tower.placed.PlacedAttackingTower;
import pink.zak.minestom.towerdefence.model.tower.placed.PlacedTower;
import pink.zak.minestom.towerdefence.model.tower.placed.types.CharityTower;
Expand All @@ -57,6 +58,8 @@ public class SingleEnemyTDMob extends SingleTDMob implements LivingTDEnemyMob {
protected final Team team;
protected final GameUser sender;

private final DamagePredictionHandler damagePredictionHandler = new DamagePredictionHandler(() -> this.health);

protected final int positionModifier;
protected final List<PathCorner> corners;

Expand Down Expand Up @@ -345,4 +348,9 @@ public double getTotalDistanceMoved() {
public @NotNull MobHandler getMobHandler() {
return mobHandler;
}

@Override
public @NotNull DamagePredictionHandler damagePredictionHandler() {
return this.damagePredictionHandler;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package pink.zak.minestom.towerdefence.model.prediction;

import org.jetbrains.annotations.NotNull;

public interface DamagePredictable {

@NotNull DamagePredictionHandler damagePredictionHandler();

default @NotNull Prediction addDamagePrediction(float damage) {
return this.damagePredictionHandler().addPrediction(damage);
}

default @NotNull Prediction addDamagePrediction(long duration, float damage) {
return this.damagePredictionHandler().addPrediction(duration, damage);
}

/**
* Whether it is predicted this mob will die soon due to a damage hold from a tower.
*
* @return whether this mob is predicted to die soon.
*/
default boolean isPredictedDead() {
return this.damagePredictionHandler().isPredictedDead();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package pink.zak.minestom.towerdefence.model.prediction;

import com.google.common.util.concurrent.AtomicDouble;
import net.minestom.server.MinecraftServer;
import net.minestom.server.timer.SchedulerManager;
import net.minestom.server.timer.Task;
import net.minestom.server.utils.time.TimeUnit;
import org.jetbrains.annotations.NotNull;

import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;

// NOTE: These preds will unnecessarily stack up. We need to remove them when the projectile hits.
public final class DamagePredictionHandler {
private static final SchedulerManager SCHEDULER_MANAGER = MinecraftServer.getSchedulerManager();

private final @NotNull Set<Task> cleanupTasks = ConcurrentHashMap.newKeySet();
private final @NotNull Supplier<Float> healthSupplier;

private final AtomicDouble counter = new AtomicDouble(0);

public DamagePredictionHandler(@NotNull Supplier<Float> healthSupplier) {
this.healthSupplier = healthSupplier;
}

public Prediction addPrediction(float damage) {
this.counter.addAndGet(damage);
return new Prediction(damage, null, this);
}

public Prediction addPrediction(long duration, float damage) {
this.counter.addAndGet(damage);
Task task = SCHEDULER_MANAGER.buildTask(() -> this.counter.addAndGet(-damage))
.delay(duration, TimeUnit.MILLISECOND).schedule();
this.cleanupTasks.add(task);

return new Prediction(damage, task, this);
}

void removePrediction(@NotNull Prediction prediction) {
Task task = prediction.task();
if (task != null) {
task.cancel();
this.cleanupTasks.remove(task);
}

this.counter.addAndGet(-prediction.damage());
}

public boolean isPredictedDead() {
return this.counter.get() >= this.healthSupplier.get();
}

public void destroy() {
this.cleanupTasks.forEach(Task::cancel);
this.cleanupTasks.clear();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package pink.zak.minestom.towerdefence.model.prediction;

import net.minestom.server.timer.Task;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public record Prediction(double damage, @Nullable Task task, @NotNull DamagePredictionHandler damagePredictable) {

public void destroy() {
this.damagePredictable.removePrediction(this);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@
import net.minestom.server.entity.LivingEntity;
import net.minestom.server.event.EventFilter;
import net.minestom.server.event.EventNode;
import net.minestom.server.event.entity.projectile.ProjectileCollideWithBlockEvent;
import net.minestom.server.event.entity.projectile.ProjectileCollideWithEntityEvent;
import net.minestom.server.event.trait.InstanceEvent;
import net.minestom.server.instance.Instance;
import net.minestom.server.item.Material;
import net.minestom.server.utils.Direction;
import org.jetbrains.annotations.NotNull;
import pink.zak.minestom.towerdefence.model.mob.living.LivingTDEnemyMob;
import pink.zak.minestom.towerdefence.model.prediction.Prediction;
import pink.zak.minestom.towerdefence.model.tower.config.AttackingTower;
import pink.zak.minestom.towerdefence.model.tower.config.AttackingTowerLevel;
import pink.zak.minestom.towerdefence.model.tower.config.towers.ArcherTowerConfig;
Expand Down Expand Up @@ -53,6 +55,9 @@ public ArcherTower(Instance instance, AttackingTower tower, Material towerBaseMa
// todo predict that we will kill the entity and don't target that entity any more.
private void betterFire() {
LivingTDEnemyMob target = this.targets.get(0);
float damage = this.level.getDamage();
Prediction prediction = target.addDamagePrediction(damage);

Pos targetPos = target.getPosition().add(0, target.getEyeHeight() / 2, 0);

Point fPoint = this.getFiringPoint(targetPos);
Expand All @@ -62,7 +67,7 @@ private void betterFire() {

// NOTE: We can't expand the bounding box of the arrow MORE, or it will collide with the tower.
this.fakeShooter.lookAt(targetPos);
ArcGuidedProjectile projectile = new ArcGuidedProjectile(EntityType.ARROW, this.fakeShooter, speed, 1.10);
ArcGuidedProjectile projectile = new ArcGuidedProjectile(EntityType.ARROW, this.fakeShooter, speed, 1.15);
projectile.setBoundingBox(projectile.getBoundingBox().expand(0.2, 0.2, 0.2));
projectile.shoot(this.instance, fPoint, targetPos);

Expand All @@ -82,8 +87,16 @@ private void betterFire() {
}

// livingEntity.setArrowCount(livingEntity.getArrowCount() + 1); // todo
prediction.destroy();
eventEntity.remove();
tdEnemyMob.damage(this, this.getLevel().getDamage());
}).addListener(ProjectileCollideWithBlockEvent.class, event -> {
Entity eventEntity = event.getEntity();
if (eventEntity != projectile) return;

prediction.destroy();
eventEntity.remove();
Audiences.all().sendMessage(Component.text("Arrow %s missed".formatted(eventEntity.getEntityId())));
});
}

Expand Down

0 comments on commit c984aa4

Please sign in to comment.