feat: LivingEntity fire overhaul (#2122)

* feat: LivingEntity fire overhaul

* chore: missed method change
This commit is contained in:
DeidaraMC 2024-05-27 11:59:48 -04:00 committed by Matt Worzala
parent 1ce17664c0
commit da0950329f
7 changed files with 239 additions and 135 deletions

View File

@ -1062,18 +1062,6 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev
return this.entityMeta.isOnFire();
}
/**
* Sets the entity in fire visually.
* <p>
* WARNING: if you want to apply damage or specify a duration,
* see {@link LivingEntity#setFireForDuration(int, TemporalUnit)}.
*
* @param fire should the entity be set in fire
*/
public void setOnFire(boolean fire) {
this.entityMeta.setOnFire(fire);
}
/**
* Gets if the entity is sneaking.
* <p>

View File

@ -12,7 +12,8 @@ import net.minestom.server.entity.metadata.LivingEntityMeta;
import net.minestom.server.event.EventDispatcher;
import net.minestom.server.event.entity.EntityDamageEvent;
import net.minestom.server.event.entity.EntityDeathEvent;
import net.minestom.server.event.entity.EntityFireEvent;
import net.minestom.server.event.entity.EntityFireExtinguishEvent;
import net.minestom.server.event.entity.EntitySetFireEvent;
import net.minestom.server.event.item.EntityEquipEvent;
import net.minestom.server.event.item.PickupItemEvent;
import net.minestom.server.instance.EntityTracker;
@ -61,14 +62,9 @@ public class LivingEntity extends Entity implements EquipmentHandler {
protected boolean invulnerable;
/**
* Time at which this entity must be extinguished
* Ticks until this entity must be extinguished
*/
private long fireExtinguishTime;
/**
* Period, in ms, between two fire damage applications
*/
private long fireDamagePeriod = 1000L;
private int remainingFireTicks;
private Team team;
@ -186,8 +182,9 @@ public class LivingEntity extends Entity implements EquipmentHandler {
@Override
public void update(long time) {
if (isOnFire() && time > fireExtinguishTime) {
setOnFire(false);
// Fire
if (remainingFireTicks > 0 && --remainingFireTicks == 0) {
EventDispatcher.callCancellable(new EntityFireExtinguishEvent(this, true), () -> entityMeta.setOnFire(false));
}
// Items picking
@ -272,44 +269,40 @@ public class LivingEntity extends Entity implements EquipmentHandler {
}
/**
* Sets fire to this entity for a given duration.
* Gets the amount of ticks this entity is on fire for.
*
* @param duration duration in ticks of the effect
* @return the remaining duration of fire in ticks, 0 if not on fire
*/
public void setFireForDuration(int duration) {
setFireForDuration(duration, TimeUnit.SERVER_TICK);
public int getFireTicks() {
return remainingFireTicks;
}
/**
* Sets fire to this entity for a given duration.
* Sets this entity on fire for the given ticks.
*
* @param duration duration of the effect
* @param temporalUnit unit used to express the duration
* @see #setOnFire(boolean) if you want it to be permanent without any event callback
* @param ticks duration of fire in ticks
*/
public void setFireForDuration(int duration, TemporalUnit temporalUnit) {
setFireForDuration(Duration.of(duration, temporalUnit));
}
public void setFireTicks(int ticks) {
int fireTicks = Math.max(0, ticks);
if (fireTicks > 0) {
EntitySetFireEvent entitySetFireEvent = new EntitySetFireEvent(this, ticks);
EventDispatcher.call(entitySetFireEvent);
if (entitySetFireEvent.isCancelled()) return;
/**
* Sets fire to this entity for a given duration.
*
* @param duration duration of the effect
* @see #setOnFire(boolean) if you want it to be permanent without any event callback
*/
public void setFireForDuration(Duration duration) {
EntityFireEvent entityFireEvent = new EntityFireEvent(this, duration);
// Do not start fire event if the fire needs to be removed (< 0 duration)
if (duration.toMillis() > 0) {
EventDispatcher.callCancellable(entityFireEvent, () -> {
final long fireTime = entityFireEvent.getFireTime(TimeUnit.MILLISECOND);
setOnFire(true);
fireExtinguishTime = System.currentTimeMillis() + fireTime;
});
} else {
fireExtinguishTime = System.currentTimeMillis();
fireTicks = Math.max(0, entitySetFireEvent.getFireTicks());
if (fireTicks > 0) {
remainingFireTicks = fireTicks;
entityMeta.setOnFire(true);
return;
}
}
if (remainingFireTicks != 0) {
EntityFireExtinguishEvent entityFireExtinguishEvent = new EntityFireExtinguishEvent(this, false);
EventDispatcher.callCancellable(entityFireExtinguishEvent, () -> entityMeta.setOnFire(false));
}
remainingFireTicks = fireTicks;
}
public boolean damage(@NotNull DynamicRegistry.Key<DamageType> type, float amount) {
@ -568,35 +561,6 @@ public class LivingEntity extends Entity implements EquipmentHandler {
return new EntityAttributesPacket(getEntityId(), List.copyOf(attributeModifiers.values()));
}
/**
* Gets the time in ms between two fire damage applications.
*
* @return the time in ms
* @see #setFireDamagePeriod(Duration)
*/
public long getFireDamagePeriod() {
return fireDamagePeriod;
}
/**
* Changes the delay between two fire damage applications.
*
* @param fireDamagePeriod the delay
* @param temporalUnit the time unit
*/
public void setFireDamagePeriod(long fireDamagePeriod, @NotNull TemporalUnit temporalUnit) {
setFireDamagePeriod(Duration.of(fireDamagePeriod, temporalUnit));
}
/**
* Changes the delay between two fire damage applications.
*
* @param fireDamagePeriod the delay
*/
public void setFireDamagePeriod(Duration fireDamagePeriod) {
this.fireDamagePeriod = fireDamagePeriod.toMillis();
}
/**
* Changes the {@link Team} for the entity.
*

View File

@ -519,8 +519,8 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
if (!isDead())
return;
setFireForDuration(0);
setOnFire(false);
setFireTicks(0);
entityMeta.setOnFire(false);
refreshHealth();
sendPacket(new RespawnPacket(DIMENSION_TYPE_REGISTRY.getId(getDimensionType().namespace()), instance.getDimensionName(),

View File

@ -1,53 +0,0 @@
package net.minestom.server.event.entity;
import net.minestom.server.entity.Entity;
import net.minestom.server.event.trait.CancellableEvent;
import net.minestom.server.event.trait.EntityInstanceEvent;
import org.jetbrains.annotations.NotNull;
import java.time.Duration;
import java.time.temporal.TemporalUnit;
public class EntityFireEvent implements EntityInstanceEvent, CancellableEvent {
private final Entity entity;
private Duration duration;
private boolean cancelled;
public EntityFireEvent(Entity entity, int duration, TemporalUnit temporalUnit) {
this(entity, Duration.of(duration, temporalUnit));
}
public EntityFireEvent(Entity entity, Duration duration) {
this.entity = entity;
setFireTime(duration);
}
public long getFireTime(TemporalUnit temporalUnit) {
return duration.toNanos() / temporalUnit.getDuration().toNanos();
}
public void setFireTime(int duration, TemporalUnit temporalUnit) {
setFireTime(Duration.of(duration, temporalUnit));
}
public void setFireTime(Duration duration) {
this.duration = duration;
}
@Override
public boolean isCancelled() {
return cancelled;
}
@Override
public void setCancelled(boolean cancel) {
this.cancelled = cancel;
}
@Override
public @NotNull Entity getEntity() {
return entity;
}
}

View File

@ -0,0 +1,38 @@
package net.minestom.server.event.entity;
import net.minestom.server.entity.Entity;
import net.minestom.server.event.trait.CancellableEvent;
import net.minestom.server.event.trait.EntityInstanceEvent;
import org.jetbrains.annotations.NotNull;
public class EntityFireExtinguishEvent implements EntityInstanceEvent, CancellableEvent {
private final Entity entity;
private boolean natural;
private boolean cancelled;
public EntityFireExtinguishEvent(Entity entity, boolean natural) {
this.entity = entity;
this.natural = natural;
}
public boolean isNatural() {
return natural;
}
@Override
public boolean isCancelled() {
return cancelled;
}
@Override
public void setCancelled(boolean cancel) {
this.cancelled = cancel;
}
@Override
public @NotNull Entity getEntity() {
return entity;
}
}

View File

@ -0,0 +1,42 @@
package net.minestom.server.event.entity;
import net.minestom.server.entity.Entity;
import net.minestom.server.event.trait.CancellableEvent;
import net.minestom.server.event.trait.EntityInstanceEvent;
import org.jetbrains.annotations.NotNull;
public class EntitySetFireEvent implements EntityInstanceEvent, CancellableEvent {
private final Entity entity;
private int ticks;
private boolean cancelled;
public EntitySetFireEvent(Entity entity, int ticks) {
this.entity = entity;
this.ticks = ticks;
}
public int getFireTicks() {
return ticks;
}
public void setFireTicks(int ticks) {
this.ticks = ticks;
}
@Override
public boolean isCancelled() {
return cancelled;
}
@Override
public void setCancelled(boolean cancel) {
this.cancelled = cancel;
}
@Override
public @NotNull Entity getEntity() {
return entity;
}
}

View File

@ -0,0 +1,125 @@
package net.minestom.server.entity;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.event.entity.EntityFireExtinguishEvent;
import net.minestom.server.event.entity.EntitySetFireEvent;
import net.minestom.testing.Env;
import net.minestom.testing.EnvTest;
import org.junit.jupiter.api.Test;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.*;
@EnvTest
public class EntityFireTest
{
@Test
public void duration(Env env) {
var instance = env.createFlatInstance();
instance.loadChunk(0, 0).join();
final int fireTicks = 10;
LivingEntity entity = new LivingEntity(EntityType.ZOMBIE);
entity.setInstance(instance, new Vec(0, 0, 0));
entity.setFireTicks(fireTicks);
assertTrue(entity.getEntityMeta().isOnFire());
for (int i = 0; i < fireTicks; i++) {
assertTrue(entity.getEntityMeta().isOnFire());
assertEquals(fireTicks - i, entity.getFireTicks());
entity.tick(0);
}
assertFalse(entity.getEntityMeta().isOnFire());
assertEquals(entity.getFireTicks(), 0);
}
@Test
public void nonNegativeFireDuration(Env env) {
var instance = env.createFlatInstance();
instance.loadChunk(0, 0).join();
LivingEntity entity = new LivingEntity(EntityType.ZOMBIE);
entity.setInstance(instance, new Vec(0, 0, 0));
// Natural fire decay
entity.setFireTicks(5);
for (int i = 0; i < 20; i++) {
assertTrue(entity.getFireTicks() >= 0);
}
// Explicit negative
entity.setFireTicks(-1);
assertEquals(0, entity.getFireTicks());
// Explicit negative in event
env.listen(EntitySetFireEvent.class).followup(e -> {
e.setFireTicks(-1);
});
entity.setFireTicks(1);
assertEquals(entity.getFireTicks(), 0);
}
@Test
public void setFireMetadata(Env env) {
var instance = env.createFlatInstance();
instance.loadChunk(0, 0).join();
LivingEntity entity = new LivingEntity(EntityType.ZOMBIE);
entity.setInstance(instance, new Vec(0, 0, 0));
// Do not extinguish an entity when they're set on fire explicitly
entity.getEntityMeta().setOnFire(true);
for (int i = 0; i < 40; i++) {
entity.tick(0);
assertTrue(entity.getEntityMeta().isOnFire());
}
// Unless setFireTicks has been called to activate the internal remainingFireTicks timer
entity.setFireTicks(1);
entity.tick(0);
assertFalse(entity.isOnFire());
}
@Test
public void extinguishEvent(Env env) {
var instance = env.createFlatInstance();
instance.loadChunk(0, 0).join();
LivingEntity entity = new LivingEntity(EntityType.ZOMBIE);
entity.setInstance(instance, new Vec(0, 0, 0));
AtomicInteger callCount = new AtomicInteger();
env.listen(EntityFireExtinguishEvent.class).followup(e -> {
callCount.getAndIncrement();
if (callCount.get() == 2) assertTrue(e.isNatural());
else assertFalse(e.isNatural());
});
// Don't call when the entity is already on fire
entity.setFireTicks(0);
assertEquals(0, callCount.get());
// Call now, the entity is set on fire
entity.setFireTicks(1);
entity.setFireTicks(-1);
assertEquals(1, callCount.get());
// Call naturally
entity.setFireTicks(3);
for (int i = 0; i < 3; i++) {
entity.tick(0);
}
assertEquals(2, callCount.get());
// Don't call if cancelled EntitySetFireEvent
env.listen(EntitySetFireEvent.class).followup(e -> {
e.setCancelled(true);
});
entity.setFireTicks(5);
assertEquals(2, callCount.get());
}
}