diff --git a/src/main/java/com/mythlane/chainlightning/chain/ChainResolver.java b/src/main/java/com/mythlane/chainlightning/chain/ChainResolver.java index 9599fb0..f3cdaab 100644 --- a/src/main/java/com/mythlane/chainlightning/chain/ChainResolver.java +++ b/src/main/java/com/mythlane/chainlightning/chain/ChainResolver.java @@ -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 resolve( - Vec3 shooterOrigin, - Vec3 shooterDirection, - double rayMaxBlocks, - RayCaster ray, - EntitySource neighbors, - ChainParameters params) { + public static List resolve(double rayMaxBlocks, + RayCaster ray, + EntitySource neighbors, + ChainParameters params) { - Optional primaryOpt = ray.firstHit(shooterOrigin, shooterDirection, rayMaxBlocks); + Optional primaryOpt = ray.firstHit(rayMaxBlocks); if (primaryOpt.isEmpty()) { return List.of(); } diff --git a/src/main/java/com/mythlane/chainlightning/chain/RayCaster.java b/src/main/java/com/mythlane/chainlightning/chain/RayCaster.java index 060e572..d5b90ac 100644 --- a/src/main/java/com/mythlane/chainlightning/chain/RayCaster.java +++ b/src/main/java/com/mythlane/chainlightning/chain/RayCaster.java @@ -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 firstHit(Vec3 origin, Vec3 direction, double maxBlocks); + Optional firstHit(double maxBlocks); } diff --git a/src/main/java/com/mythlane/chainlightning/sceptre/ChainDamageApplier.java b/src/main/java/com/mythlane/chainlightning/sceptre/ChainDamageApplier.java index 4462fe1..5ab56d9 100644 --- a/src/main/java/com/mythlane/chainlightning/sceptre/ChainDamageApplier.java +++ b/src/main/java/com/mythlane/chainlightning/sceptre/ChainDamageApplier.java @@ -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 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 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, diff --git a/src/main/java/com/mythlane/chainlightning/sceptre/ChainLightningSceptreInteraction.java b/src/main/java/com/mythlane/chainlightning/sceptre/ChainLightningSceptreInteraction.java index a682490..4adadf2 100644 --- a/src/main/java/com/mythlane/chainlightning/sceptre/ChainLightningSceptreInteraction.java +++ b/src/main/java/com/mythlane/chainlightning/sceptre/ChainLightningSceptreInteraction.java @@ -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 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. */ diff --git a/src/main/java/com/mythlane/chainlightning/sceptre/HytaleEntityAdapter.java b/src/main/java/com/mythlane/chainlightning/sceptre/HytaleEntityAdapter.java index 135ad76..7a7dc79 100644 --- a/src/main/java/com/mythlane/chainlightning/sceptre/HytaleEntityAdapter.java +++ b/src/main/java/com/mythlane/chainlightning/sceptre/HytaleEntityAdapter.java @@ -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 ref() { diff --git a/src/main/java/com/mythlane/chainlightning/sceptre/HytalePlayerRayCaster.java b/src/main/java/com/mythlane/chainlightning/sceptre/HytalePlayerRayCaster.java index 1566996..7d2d9e2 100644 --- a/src/main/java/com/mythlane/chainlightning/sceptre/HytalePlayerRayCaster.java +++ b/src/main/java/com/mythlane/chainlightning/sceptre/HytalePlayerRayCaster.java @@ -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 playerRef; @@ -24,7 +23,7 @@ public final class HytalePlayerRayCaster implements RayCaster { } @Override - public Optional firstHit(Vec3 originIgnored, Vec3 directionIgnored, double maxBlocks) { + public Optional firstHit(double maxBlocks) { Ref target = TargetUtil.getTargetEntity(playerRef, (float) maxBlocks, accessor); if (target == null) { return Optional.empty(); diff --git a/src/main/java/com/mythlane/chainlightning/sceptre/HytaleVfxEmitter.java b/src/main/java/com/mythlane/chainlightning/sceptre/HytaleVfxEmitter.java index e1ec7d6..982e208 100644 --- a/src/main/java/com/mythlane/chainlightning/sceptre/HytaleVfxEmitter.java +++ b/src/main/java/com/mythlane/chainlightning/sceptre/HytaleVfxEmitter.java @@ -37,28 +37,17 @@ public final class HytaleVfxEmitter { ComponentAccessor accessor = commandBuffer; int applied = 0; - int skipped = 0; - for (int i = 0; i < hits.size(); i++) { - Ref targetRef = ((HytaleEntityAdapter) hits.get(i).target()).ref(); - if (targetRef == null || !targetRef.isValid()) { - skipped++; - continue; - } + for (ChainHit hit : hits) { + Ref 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)); } } diff --git a/src/test/java/com/mythlane/chainlightning/chain/ChainResolverTest.java b/src/test/java/com/mythlane/chainlightning/chain/ChainResolverTest.java index e12a7f7..c89da0c 100644 --- a/src/test/java/com/mythlane/chainlightning/chain/ChainResolverTest.java +++ b/src/test/java/com/mythlane/chainlightning/chain/ChainResolverTest.java @@ -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 candidates) { @@ -35,16 +35,14 @@ final class ChainResolverTest { @Test void resolve_noPrimaryHit_returnsEmpty() { - List hits = ChainResolver.resolve( - ORIGIN, DIR, RAY_MAX, rayMisses(), neighborsEmpty(), ChainParameters.DEFAULT); + List 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 hits = ChainResolver.resolve( - ORIGIN, DIR, RAY_MAX, rayHits(primary), neighborsEmpty(), ChainParameters.DEFAULT); + List 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 all = List.of(e0, e1, e2, e3, e4); - List hits = ChainResolver.resolve( - ORIGIN, DIR, RAY_MAX, rayHits(e0), neighborsAlways(all), ChainParameters.DEFAULT); + List 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 all = List.of(primary, e1, e2, e3, e4, e5, e6, e7, e8, e9); + List 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 hits = ChainResolver.resolve( - ORIGIN, DIR, RAY_MAX, rayHits(primary), neighborsAlways(all), ChainParameters.DEFAULT); + List 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 far = List.of( + entity("f1", 9, 0, 0), + entity("f2", 0, 9, 0), + entity("f3", 0, 0, 9)); - List hits = ChainResolver.resolve( - ORIGIN, DIR, RAY_MAX, rayHits(primary), - neighborsAlways(List.of(far1, far2, far3)), - ChainParameters.DEFAULT); + List 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 mutual = List.of(a, b); - ChainParameters p = new ChainParameters(5, 8.0, new int[]{8, 6, 4, 3, 2}); - List hits = ChainResolver.resolve( - ORIGIN, DIR, RAY_MAX, rayHits(a), neighborsAlways(mutual), p); + List 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 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 order1 = List.of(zebra, alpha); - List order2 = List.of(alpha, zebra); List 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 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 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 all = List.of(e0, e1, e2, e3, e4); + List 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 hits = ChainResolver.resolve( - ORIGIN, DIR, RAY_MAX, rayHits(e0), neighborsAlways(all), p); + List hits = ChainResolver.resolve(RAY_MAX, rayHits(e0), neighborsAlways(all), p); assertEquals(3, hits.size()); assertEquals(10, hits.get(0).damageHp()); diff --git a/src/test/java/com/mythlane/chainlightning/sceptre/ChainDamageApplierTest.java b/src/test/java/com/mythlane/chainlightning/sceptre/ChainDamageApplierTest.java index fb40b67..d5c6c69 100644 --- a/src/test/java/com/mythlane/chainlightning/sceptre/ChainDamageApplierTest.java +++ b/src/test/java/com/mythlane/chainlightning/sceptre/ChainDamageApplierTest.java @@ -23,12 +23,7 @@ final class ChainDamageApplierTest { @Test void apply_invokes_executeDamage_per_hit() { - @SuppressWarnings("unchecked") - Ref attacker = (Ref) mock(Ref.class); - @SuppressWarnings("unchecked") - CommandBuffer buf = (CommandBuffer) mock(CommandBuffer.class); - ChainDamageApplier.DamageExecutor executor = mock(ChainDamageApplier.DamageExecutor.class); - + Fixture f = new Fixture(); List 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 attacker = (Ref) mock(Ref.class); - @SuppressWarnings("unchecked") - CommandBuffer buf = (CommandBuffer) mock(CommandBuffer.class); - ChainDamageApplier.DamageExecutor executor = mock(ChainDamageApplier.DamageExecutor.class); - + Fixture f = new Fixture(); List hits = List.of( hit("a", 8, 0), hit("b", 6, 1), hit("c", 4, 2) ); - ArgumentCaptor 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 attacker = (Ref) mock(Ref.class); - @SuppressWarnings("unchecked") - CommandBuffer buf = (CommandBuffer) 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 attacker = (Ref) mock(Ref.class, "attacker"); + Fixture f = new Fixture(); @SuppressWarnings("unchecked") Ref targetRef = (Ref) mock(Ref.class, "target"); - @SuppressWarnings("unchecked") - CommandBuffer buf = (CommandBuffer) 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> 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 attacker = (Ref) mock(Ref.class, "attacker"); + @SuppressWarnings("unchecked") + final CommandBuffer buf = (CommandBuffer) mock(CommandBuffer.class); + final ChainDamageApplier.DamageExecutor executor = mock(ChainDamageApplier.DamageExecutor.class); + } }