refactor: simplify ChainResolver and RayCaster interfaces, enhance ChainDamageApplier logic
- Updated ChainResolver to remove unused shooterOrigin and shooterDirection parameters, streamlining the resolve method. - Modified RayCaster interface to reflect changes in method signature, focusing on maxBlocks only. - Enhanced ChainDamageApplier to utilize a new CauseIndexHolder for resolving the PHYSICAL damage cause index, improving clarity and performance. - Refactored ChainDamageApplier.apply method to use HytaleEntityAdapter for target references, ensuring correct damage application. - Adjusted tests in ChainResolverTest and ChainDamageApplierTest to align with the new method signatures and logic. Tests: All tests passing. Build: ./gradlew clean build successful.
This commit is contained in:
@@ -12,15 +12,12 @@ public final class ChainResolver {
|
||||
private ChainResolver() {}
|
||||
|
||||
/** Resolves the full chain; returns the primary plus up to maxTargets-1 nearest unique neighbors. */
|
||||
public static List<ChainHit> resolve(
|
||||
Vec3 shooterOrigin,
|
||||
Vec3 shooterDirection,
|
||||
double rayMaxBlocks,
|
||||
public static List<ChainHit> resolve(double rayMaxBlocks,
|
||||
RayCaster ray,
|
||||
EntitySource neighbors,
|
||||
ChainParameters params) {
|
||||
|
||||
Optional<ChainEntity> primaryOpt = ray.firstHit(shooterOrigin, shooterDirection, rayMaxBlocks);
|
||||
Optional<ChainEntity> primaryOpt = ray.firstHit(rayMaxBlocks);
|
||||
if (primaryOpt.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ package com.mythlane.chainlightning.chain;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/** SAM that returns the first entity hit along a ray, or empty. */
|
||||
/** SAM that returns the first entity along the caster's look ray, or empty. */
|
||||
@FunctionalInterface
|
||||
public interface RayCaster {
|
||||
Optional<ChainEntity> firstHit(Vec3 origin, Vec3 direction, double maxBlocks);
|
||||
Optional<ChainEntity> firstHit(double maxBlocks);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/** Applies DamageSystems.executeDamage to each chain hit; injectable executor keeps unit tests off the Hytale runtime. */
|
||||
@SuppressWarnings("deprecation")
|
||||
public final class ChainDamageApplier {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(ChainDamageApplier.class.getName());
|
||||
@@ -28,24 +29,28 @@ public final class ChainDamageApplier {
|
||||
}
|
||||
|
||||
/** Lazy holder keeps DamageSystems.<clinit> out of the test classpath. */
|
||||
@SuppressWarnings("deprecation")
|
||||
private static final class DefaultHolder {
|
||||
static final DamageExecutor INSTANCE = DamageSystems::executeDamage;
|
||||
}
|
||||
|
||||
/** Lazy holder caches the asset-map index of PHYSICAL once the runtime is initialized. */
|
||||
private static final class CauseIndexHolder {
|
||||
static final int VALUE = computeIndex();
|
||||
|
||||
private static int computeIndex() {
|
||||
DamageCause physical = DamageCause.PHYSICAL;
|
||||
return physical == null ? 0 : DamageCause.getAssetMap().getIndex(physical.getId());
|
||||
}
|
||||
}
|
||||
|
||||
/** Production executor that delegates to DamageSystems.executeDamage. */
|
||||
public static DamageExecutor defaultExecutor() {
|
||||
return DefaultHolder.INSTANCE;
|
||||
}
|
||||
|
||||
/** Resolves the PHYSICAL cause index at call time so the runtime asset map ordering is honored. */
|
||||
@SuppressWarnings("deprecation")
|
||||
/** PHYSICAL cause index resolved from the asset map; falls back to 0 in unit tests where the runtime is not booted. */
|
||||
static int physicalDamageCauseIndex() {
|
||||
DamageCause physical = DamageCause.PHYSICAL;
|
||||
if (physical == null) {
|
||||
return 0;
|
||||
}
|
||||
return DamageCause.getAssetMap().getIndex(physical.getId());
|
||||
return CauseIndexHolder.VALUE;
|
||||
}
|
||||
|
||||
private ChainDamageApplier() {}
|
||||
@@ -64,17 +69,13 @@ public final class ChainDamageApplier {
|
||||
@Nonnull CommandBuffer<EntityStore> commandBuffer,
|
||||
@Nonnull DamageExecutor executor) {
|
||||
int causeIndex = physicalDamageCauseIndex();
|
||||
Damage.EntitySource source = new Damage.EntitySource(attacker);
|
||||
int succeeded = 0;
|
||||
for (ChainHit hit : hits) {
|
||||
HytaleEntityAdapter adapter = (HytaleEntityAdapter) hit.target();
|
||||
Ref<EntityStore> targetRef = adapter.ref();
|
||||
Damage damage = new Damage(
|
||||
new Damage.EntitySource(attacker),
|
||||
causeIndex,
|
||||
(float) hit.damageHp()
|
||||
);
|
||||
HytaleEntityAdapter adapter = HytaleEntityAdapter.from(hit);
|
||||
Damage damage = new Damage(source, causeIndex, (float) hit.damageHp());
|
||||
try {
|
||||
executor.execute(targetRef, commandBuffer, damage);
|
||||
executor.execute(adapter.ref(), commandBuffer, damage);
|
||||
succeeded++;
|
||||
} catch (Throwable t) {
|
||||
LOGGER.log(Level.WARNING,
|
||||
|
||||
+1
-5
@@ -11,7 +11,6 @@ import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||
import com.mythlane.chainlightning.chain.ChainHit;
|
||||
import com.mythlane.chainlightning.chain.ChainParameters;
|
||||
import com.mythlane.chainlightning.chain.ChainResolver;
|
||||
import com.mythlane.chainlightning.chain.Vec3;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.List;
|
||||
@@ -81,10 +80,7 @@ public final class ChainLightningSceptreInteraction extends SimpleInstantInterac
|
||||
@Nonnull CommandBuffer<EntityStore> commandBuffer) {
|
||||
HytalePlayerRayCaster ray = new HytalePlayerRayCaster(playerRef, commandBuffer);
|
||||
HytaleEntitySource neighbors = new HytaleEntitySource(commandBuffer);
|
||||
return ChainResolver.resolve(
|
||||
Vec3.ZERO, Vec3.ZERO, RAY_MAX_BLOCKS,
|
||||
ray, neighbors, ChainParameters.DEFAULT
|
||||
);
|
||||
return ChainResolver.resolve(RAY_MAX_BLOCKS, ray, neighbors, ChainParameters.DEFAULT);
|
||||
}
|
||||
|
||||
/** VFX emit is best-effort: damage is already applied so a failure must not abort the cooldown step. */
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.hypixel.hytale.server.core.modules.entitystats.EntityStatValue;
|
||||
import com.hypixel.hytale.server.core.modules.entitystats.asset.DefaultEntityStatTypes;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||
import com.mythlane.chainlightning.chain.ChainEntity;
|
||||
import com.mythlane.chainlightning.chain.ChainHit;
|
||||
import com.mythlane.chainlightning.chain.Vec3;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
@@ -54,6 +55,12 @@ public final class HytaleEntityAdapter implements ChainEntity {
|
||||
return new HytaleEntityAdapter(ref, id, vec, alive);
|
||||
}
|
||||
|
||||
/** Single cast point so callers stop reaching into ChainEntity to recover the runtime adapter. */
|
||||
@Nonnull
|
||||
public static HytaleEntityAdapter from(@Nonnull ChainHit hit) {
|
||||
return (HytaleEntityAdapter) hit.target();
|
||||
}
|
||||
|
||||
/** Underlying Hytale ref, exposed so DamageSystems and EffectControllerComponent can address the entity. */
|
||||
@Nonnull
|
||||
public Ref<EntityStore> ref() {
|
||||
|
||||
@@ -6,12 +6,11 @@ import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||
import com.hypixel.hytale.server.core.util.TargetUtil;
|
||||
import com.mythlane.chainlightning.chain.ChainEntity;
|
||||
import com.mythlane.chainlightning.chain.RayCaster;
|
||||
import com.mythlane.chainlightning.chain.Vec3;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.Optional;
|
||||
|
||||
/** Phase 3 RayCaster delegating to TargetUtil; origin/direction args are ignored since TargetUtil derives them from playerRef. */
|
||||
/** Phase 3 RayCaster delegating to TargetUtil; eye origin and look direction are derived from the playerRef inside Hytale. */
|
||||
public final class HytalePlayerRayCaster implements RayCaster {
|
||||
|
||||
private final Ref<EntityStore> playerRef;
|
||||
@@ -24,7 +23,7 @@ public final class HytalePlayerRayCaster implements RayCaster {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ChainEntity> firstHit(Vec3 originIgnored, Vec3 directionIgnored, double maxBlocks) {
|
||||
public Optional<ChainEntity> firstHit(double maxBlocks) {
|
||||
Ref<EntityStore> target = TargetUtil.getTargetEntity(playerRef, (float) maxBlocks, accessor);
|
||||
if (target == null) {
|
||||
return Optional.empty();
|
||||
|
||||
@@ -37,28 +37,17 @@ public final class HytaleVfxEmitter {
|
||||
|
||||
ComponentAccessor<EntityStore> accessor = commandBuffer;
|
||||
int applied = 0;
|
||||
int skipped = 0;
|
||||
for (int i = 0; i < hits.size(); i++) {
|
||||
Ref<EntityStore> targetRef = ((HytaleEntityAdapter) hits.get(i).target()).ref();
|
||||
if (targetRef == null || !targetRef.isValid()) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
for (ChainHit hit : hits) {
|
||||
Ref<EntityStore> targetRef = HytaleEntityAdapter.from(hit).ref();
|
||||
if (!targetRef.isValid()) continue;
|
||||
EffectControllerComponent ecc = accessor.getComponent(targetRef, EffectControllerComponent.getComponentType());
|
||||
if (ecc == null) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
boolean ok = ecc.addEffect(targetRef, entityEffect, accessor);
|
||||
if (ok) {
|
||||
if (ecc != null && ecc.addEffect(targetRef, entityEffect, accessor)) {
|
||||
applied++;
|
||||
} else {
|
||||
skipped++;
|
||||
}
|
||||
}
|
||||
|
||||
LOGGER.info(String.format(
|
||||
"[ChainLightning][Vfx] EntityEffect '%s' applied to %d/%d targets (skipped=%d, caster=%s)",
|
||||
EFFECT_ID, applied, hits.size(), skipped, playerRef));
|
||||
"[ChainLightning][Vfx] EntityEffect '%s' applied to %d/%d targets (caster=%s)",
|
||||
EFFECT_ID, applied, hits.size(), playerRef));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,16 +13,16 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
final class ChainResolverTest {
|
||||
|
||||
private static final Vec3 ORIGIN = new Vec3(0, 0, 0);
|
||||
private static final Vec3 DIR = new Vec3(1, 0, 0);
|
||||
private static final double RAY_MAX = 25.0;
|
||||
private static final ChainParameters DEFAULT = ChainParameters.DEFAULT;
|
||||
private static final ChainParameters TWO_HOPS = new ChainParameters(2, 8.0, new int[]{8, 6});
|
||||
|
||||
private static RayCaster rayMisses() {
|
||||
return (o, d, max) -> Optional.empty();
|
||||
return max -> Optional.empty();
|
||||
}
|
||||
|
||||
private static RayCaster rayHits(ChainEntity e) {
|
||||
return (o, d, max) -> Optional.of(e);
|
||||
return max -> Optional.of(e);
|
||||
}
|
||||
|
||||
private static EntitySource neighborsAlways(List<ChainEntity> candidates) {
|
||||
@@ -35,16 +35,14 @@ final class ChainResolverTest {
|
||||
|
||||
@Test
|
||||
void resolve_noPrimaryHit_returnsEmpty() {
|
||||
List<ChainHit> hits = ChainResolver.resolve(
|
||||
ORIGIN, DIR, RAY_MAX, rayMisses(), neighborsEmpty(), ChainParameters.DEFAULT);
|
||||
List<ChainHit> hits = ChainResolver.resolve(RAY_MAX, rayMisses(), neighborsEmpty(), DEFAULT);
|
||||
assertTrue(hits.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolve_primaryOnly_noNeighbors_returnsSingleHit() {
|
||||
ChainEntity primary = entity("p", 10, 0, 0);
|
||||
List<ChainHit> hits = ChainResolver.resolve(
|
||||
ORIGIN, DIR, RAY_MAX, rayHits(primary), neighborsEmpty(), ChainParameters.DEFAULT);
|
||||
List<ChainHit> hits = ChainResolver.resolve(RAY_MAX, rayHits(primary), neighborsEmpty(), DEFAULT);
|
||||
assertEquals(1, hits.size());
|
||||
assertSame(primary, hits.get(0).target());
|
||||
assertEquals(8, hits.get(0).damageHp());
|
||||
@@ -60,8 +58,7 @@ final class ChainResolverTest {
|
||||
ChainEntity e4 = entity("e4", 18, 0, 0);
|
||||
List<ChainEntity> all = List.of(e0, e1, e2, e3, e4);
|
||||
|
||||
List<ChainHit> hits = ChainResolver.resolve(
|
||||
ORIGIN, DIR, RAY_MAX, rayHits(e0), neighborsAlways(all), ChainParameters.DEFAULT);
|
||||
List<ChainHit> hits = ChainResolver.resolve(RAY_MAX, rayHits(e0), neighborsAlways(all), DEFAULT);
|
||||
|
||||
assertEquals(5, hits.size());
|
||||
assertSame(e0, hits.get(0).target()); assertEquals(8, hits.get(0).damageHp()); assertEquals(0, hits.get(0).hopIndex());
|
||||
@@ -74,19 +71,19 @@ final class ChainResolverTest {
|
||||
@Test
|
||||
void resolve_moreThanFiveCandidates_stopsAtMaxTargets() {
|
||||
ChainEntity primary = entity("e0", 10, 0, 0);
|
||||
ChainEntity e1 = entity("e1", 11, 0, 0);
|
||||
ChainEntity e2 = entity("e2", 12, 0, 0);
|
||||
ChainEntity e3 = entity("e3", 13, 0, 0);
|
||||
ChainEntity e4 = entity("e4", 14, 0, 0);
|
||||
ChainEntity e5 = entity("e5", 15, 0, 0);
|
||||
ChainEntity e6 = entity("e6", 16, 0, 0);
|
||||
ChainEntity e7 = entity("e7", 17, 0, 0);
|
||||
ChainEntity e8 = entity("e8", 18, 0, 0);
|
||||
ChainEntity e9 = entity("e9", 19, 0, 0);
|
||||
List<ChainEntity> all = List.of(primary, e1, e2, e3, e4, e5, e6, e7, e8, e9);
|
||||
List<ChainEntity> all = List.of(
|
||||
primary,
|
||||
entity("e1", 11, 0, 0),
|
||||
entity("e2", 12, 0, 0),
|
||||
entity("e3", 13, 0, 0),
|
||||
entity("e4", 14, 0, 0),
|
||||
entity("e5", 15, 0, 0),
|
||||
entity("e6", 16, 0, 0),
|
||||
entity("e7", 17, 0, 0),
|
||||
entity("e8", 18, 0, 0),
|
||||
entity("e9", 19, 0, 0));
|
||||
|
||||
List<ChainHit> hits = ChainResolver.resolve(
|
||||
ORIGIN, DIR, RAY_MAX, rayHits(primary), neighborsAlways(all), ChainParameters.DEFAULT);
|
||||
List<ChainHit> hits = ChainResolver.resolve(RAY_MAX, rayHits(primary), neighborsAlways(all), DEFAULT);
|
||||
|
||||
assertEquals(5, hits.size());
|
||||
}
|
||||
@@ -94,14 +91,12 @@ final class ChainResolverTest {
|
||||
@Test
|
||||
void resolve_candidatesOutsideRadius_excluded() {
|
||||
ChainEntity primary = entity("p", 0, 0, 0);
|
||||
ChainEntity far1 = entity("f1", 9, 0, 0);
|
||||
ChainEntity far2 = entity("f2", 0, 9, 0);
|
||||
ChainEntity far3 = entity("f3", 0, 0, 9);
|
||||
List<ChainEntity> far = List.of(
|
||||
entity("f1", 9, 0, 0),
|
||||
entity("f2", 0, 9, 0),
|
||||
entity("f3", 0, 0, 9));
|
||||
|
||||
List<ChainHit> hits = ChainResolver.resolve(
|
||||
ORIGIN, DIR, RAY_MAX, rayHits(primary),
|
||||
neighborsAlways(List.of(far1, far2, far3)),
|
||||
ChainParameters.DEFAULT);
|
||||
List<ChainHit> hits = ChainResolver.resolve(RAY_MAX, rayHits(primary), neighborsAlways(far), DEFAULT);
|
||||
|
||||
assertEquals(1, hits.size());
|
||||
assertSame(primary, hits.get(0).target());
|
||||
@@ -111,11 +106,8 @@ final class ChainResolverTest {
|
||||
void resolve_noDoubleHit_visitedExcluded() {
|
||||
ChainEntity a = entity("a", 0, 0, 0);
|
||||
ChainEntity b = entity("b", 2, 0, 0);
|
||||
List<ChainEntity> mutual = List.of(a, b);
|
||||
|
||||
ChainParameters p = new ChainParameters(5, 8.0, new int[]{8, 6, 4, 3, 2});
|
||||
List<ChainHit> hits = ChainResolver.resolve(
|
||||
ORIGIN, DIR, RAY_MAX, rayHits(a), neighborsAlways(mutual), p);
|
||||
List<ChainHit> hits = ChainResolver.resolve(RAY_MAX, rayHits(a), neighborsAlways(List.of(a, b)), DEFAULT);
|
||||
|
||||
assertEquals(2, hits.size());
|
||||
assertSame(a, hits.get(0).target());
|
||||
@@ -129,9 +121,7 @@ final class ChainResolverTest {
|
||||
ChainEntity far = entity("far", 7, 0, 0);
|
||||
|
||||
List<ChainHit> hits = ChainResolver.resolve(
|
||||
ORIGIN, DIR, RAY_MAX, rayHits(primary),
|
||||
neighborsAlways(List.of(near, far)),
|
||||
new ChainParameters(2, 8.0, new int[]{8, 6}));
|
||||
RAY_MAX, rayHits(primary), neighborsAlways(List.of(near, far)), TWO_HOPS);
|
||||
|
||||
assertEquals(2, hits.size());
|
||||
assertSame(near, hits.get(1).target());
|
||||
@@ -144,14 +134,10 @@ final class ChainResolverTest {
|
||||
ChainEntity alpha = entity("alpha", 0, 5, 0);
|
||||
|
||||
for (int i = 0; i < 100; i++) {
|
||||
List<ChainEntity> order1 = List.of(zebra, alpha);
|
||||
List<ChainEntity> order2 = List.of(alpha, zebra);
|
||||
List<ChainHit> h1 = ChainResolver.resolve(
|
||||
ORIGIN, DIR, RAY_MAX, rayHits(primary), neighborsAlways(order1),
|
||||
new ChainParameters(2, 8.0, new int[]{8, 6}));
|
||||
RAY_MAX, rayHits(primary), neighborsAlways(List.of(zebra, alpha)), TWO_HOPS);
|
||||
List<ChainHit> h2 = ChainResolver.resolve(
|
||||
ORIGIN, DIR, RAY_MAX, rayHits(primary), neighborsAlways(order2),
|
||||
new ChainParameters(2, 8.0, new int[]{8, 6}));
|
||||
RAY_MAX, rayHits(primary), neighborsAlways(List.of(alpha, zebra)), TWO_HOPS);
|
||||
assertSame(alpha, h1.get(1).target(), "run " + i + " order1");
|
||||
assertSame(alpha, h2.get(1).target(), "run " + i + " order2");
|
||||
}
|
||||
@@ -164,9 +150,7 @@ final class ChainResolverTest {
|
||||
ChainEntity alive = entity("alive", 4, 0, 0);
|
||||
|
||||
List<ChainHit> hits = ChainResolver.resolve(
|
||||
ORIGIN, DIR, RAY_MAX, rayHits(primary),
|
||||
neighborsAlways(List.of(deadOne, alive)),
|
||||
new ChainParameters(2, 8.0, new int[]{8, 6}));
|
||||
RAY_MAX, rayHits(primary), neighborsAlways(List.of(deadOne, alive)), TWO_HOPS);
|
||||
|
||||
assertEquals(2, hits.size());
|
||||
assertSame(alive, hits.get(1).target());
|
||||
@@ -175,15 +159,15 @@ final class ChainResolverTest {
|
||||
@Test
|
||||
void resolve_customMaxTargets_truncatesEarly() {
|
||||
ChainEntity e0 = entity("e0", 0, 0, 0);
|
||||
ChainEntity e1 = entity("e1", 1, 0, 0);
|
||||
ChainEntity e2 = entity("e2", 2, 0, 0);
|
||||
ChainEntity e3 = entity("e3", 3, 0, 0);
|
||||
ChainEntity e4 = entity("e4", 4, 0, 0);
|
||||
List<ChainEntity> all = List.of(e0, e1, e2, e3, e4);
|
||||
List<ChainEntity> all = List.of(
|
||||
e0,
|
||||
entity("e1", 1, 0, 0),
|
||||
entity("e2", 2, 0, 0),
|
||||
entity("e3", 3, 0, 0),
|
||||
entity("e4", 4, 0, 0));
|
||||
|
||||
ChainParameters p = new ChainParameters(3, 8.0, new int[]{10, 5, 1});
|
||||
List<ChainHit> hits = ChainResolver.resolve(
|
||||
ORIGIN, DIR, RAY_MAX, rayHits(e0), neighborsAlways(all), p);
|
||||
List<ChainHit> hits = ChainResolver.resolve(RAY_MAX, rayHits(e0), neighborsAlways(all), p);
|
||||
|
||||
assertEquals(3, hits.size());
|
||||
assertEquals(10, hits.get(0).damageHp());
|
||||
|
||||
@@ -23,12 +23,7 @@ final class ChainDamageApplierTest {
|
||||
|
||||
@Test
|
||||
void apply_invokes_executeDamage_per_hit() {
|
||||
@SuppressWarnings("unchecked")
|
||||
Ref<EntityStore> attacker = (Ref<EntityStore>) mock(Ref.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
CommandBuffer<EntityStore> buf = (CommandBuffer<EntityStore>) mock(CommandBuffer.class);
|
||||
ChainDamageApplier.DamageExecutor executor = mock(ChainDamageApplier.DamageExecutor.class);
|
||||
|
||||
Fixture f = new Fixture();
|
||||
List<ChainHit> hits = List.of(
|
||||
hit("a", 8, 0),
|
||||
hit("b", 6, 1),
|
||||
@@ -37,53 +32,42 @@ final class ChainDamageApplierTest {
|
||||
hit("e", 2, 4)
|
||||
);
|
||||
|
||||
ChainDamageApplier.apply(hits, attacker, buf, executor);
|
||||
ChainDamageApplier.apply(hits, f.attacker, f.buf, f.executor);
|
||||
|
||||
verify(executor, times(5)).execute(
|
||||
verify(f.executor, times(5)).execute(
|
||||
ArgumentMatchers.any(),
|
||||
ArgumentMatchers.eq(buf),
|
||||
ArgumentMatchers.eq(f.buf),
|
||||
ArgumentMatchers.any(Damage.class)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void apply_passes_correct_damage_amounts_in_order() {
|
||||
@SuppressWarnings("unchecked")
|
||||
Ref<EntityStore> attacker = (Ref<EntityStore>) mock(Ref.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
CommandBuffer<EntityStore> buf = (CommandBuffer<EntityStore>) mock(CommandBuffer.class);
|
||||
ChainDamageApplier.DamageExecutor executor = mock(ChainDamageApplier.DamageExecutor.class);
|
||||
|
||||
Fixture f = new Fixture();
|
||||
List<ChainHit> hits = List.of(
|
||||
hit("a", 8, 0),
|
||||
hit("b", 6, 1),
|
||||
hit("c", 4, 2)
|
||||
);
|
||||
|
||||
ArgumentCaptor<Damage> damageCaptor = ArgumentCaptor.forClass(Damage.class);
|
||||
|
||||
ChainDamageApplier.apply(hits, attacker, buf, executor);
|
||||
ChainDamageApplier.apply(hits, f.attacker, f.buf, f.executor);
|
||||
|
||||
verify(executor, times(3)).execute(
|
||||
verify(f.executor, times(3)).execute(
|
||||
ArgumentMatchers.any(),
|
||||
ArgumentMatchers.eq(buf),
|
||||
ArgumentMatchers.eq(f.buf),
|
||||
damageCaptor.capture()
|
||||
);
|
||||
|
||||
assertThat(damageCaptor.getAllValues()).hasSize(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void apply_with_empty_list_invokes_nothing() {
|
||||
@SuppressWarnings("unchecked")
|
||||
Ref<EntityStore> attacker = (Ref<EntityStore>) mock(Ref.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
CommandBuffer<EntityStore> buf = (CommandBuffer<EntityStore>) mock(CommandBuffer.class);
|
||||
ChainDamageApplier.DamageExecutor executor = mock(ChainDamageApplier.DamageExecutor.class);
|
||||
Fixture f = new Fixture();
|
||||
|
||||
ChainDamageApplier.apply(List.of(), attacker, buf, executor);
|
||||
ChainDamageApplier.apply(List.of(), f.attacker, f.buf, f.executor);
|
||||
|
||||
verify(executor, never()).execute(
|
||||
verify(f.executor, never()).execute(
|
||||
ArgumentMatchers.any(),
|
||||
ArgumentMatchers.any(),
|
||||
ArgumentMatchers.any()
|
||||
@@ -92,30 +76,22 @@ final class ChainDamageApplierTest {
|
||||
|
||||
@Test
|
||||
void apply_passes_adapter_ref_not_attacker_ref() {
|
||||
@SuppressWarnings("unchecked")
|
||||
Ref<EntityStore> attacker = (Ref<EntityStore>) mock(Ref.class, "attacker");
|
||||
Fixture f = new Fixture();
|
||||
@SuppressWarnings("unchecked")
|
||||
Ref<EntityStore> targetRef = (Ref<EntityStore>) mock(Ref.class, "target");
|
||||
@SuppressWarnings("unchecked")
|
||||
CommandBuffer<EntityStore> buf = (CommandBuffer<EntityStore>) mock(CommandBuffer.class);
|
||||
ChainDamageApplier.DamageExecutor executor = mock(ChainDamageApplier.DamageExecutor.class);
|
||||
|
||||
HytaleEntityAdapter adapter = HytaleEntityAdapter.forTest(targetRef, "target", Vec3.ZERO, false);
|
||||
ChainHit hit = new ChainHit(adapter, 8, 0);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
ArgumentCaptor<Ref<EntityStore>> refCaptor = ArgumentCaptor.forClass(Ref.class);
|
||||
|
||||
ChainDamageApplier.apply(List.of(hit), attacker, buf, executor);
|
||||
ChainDamageApplier.apply(List.of(new ChainHit(adapter, 8, 0)), f.attacker, f.buf, f.executor);
|
||||
|
||||
verify(executor, times(1)).execute(
|
||||
verify(f.executor, times(1)).execute(
|
||||
refCaptor.capture(),
|
||||
ArgumentMatchers.eq(buf),
|
||||
ArgumentMatchers.eq(f.buf),
|
||||
ArgumentMatchers.any(Damage.class)
|
||||
);
|
||||
|
||||
assertThat(refCaptor.getValue()).isSameAs(targetRef);
|
||||
assertThat(refCaptor.getValue()).isNotSameAs(attacker);
|
||||
assertThat(refCaptor.getValue()).isNotSameAs(f.attacker);
|
||||
}
|
||||
|
||||
/** Builds a ChainHit backed by a HytaleEntityAdapter created via forTest to skip Hytale runtime init. */
|
||||
@@ -125,4 +101,13 @@ final class ChainDamageApplierTest {
|
||||
HytaleEntityAdapter adapter = HytaleEntityAdapter.forTest(ref, id, Vec3.ZERO, false);
|
||||
return new ChainHit(adapter, dmg, hop);
|
||||
}
|
||||
|
||||
/** Bundles the three mocks every test needs to keep test bodies focused on assertions. */
|
||||
private static final class Fixture {
|
||||
@SuppressWarnings("unchecked")
|
||||
final Ref<EntityStore> attacker = (Ref<EntityStore>) mock(Ref.class, "attacker");
|
||||
@SuppressWarnings("unchecked")
|
||||
final CommandBuffer<EntityStore> buf = (CommandBuffer<EntityStore>) mock(CommandBuffer.class);
|
||||
final ChainDamageApplier.DamageExecutor executor = mock(ChainDamageApplier.DamageExecutor.class);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user