feat(phase-3): runtime integration — chain damage application + cooldown
Wires the pure ChainResolver (Phase 2) to the live Hytale runtime via four sceptre/ adapters: - HytaleEntityAdapter — eager snapshot Ref<EntityStore> -> ChainEntity via TransformComponent.getPosition() and EntityStatMap.get(health) > 0 - HytalePlayerRayCaster — captures playerRef, delegates to TargetUtil.getTargetEntity (auto eye-origin + head-rotation) - HytaleEntitySource — wraps TargetUtil.getAllEntitiesInSphere - ChainDamageApplier — fires DamageSystems.executeDamage per hit; injectable DamageExecutor SAM keeps the helper unit-testable. ChainLightningSceptreInteraction.firstRun is rewritten end-to-end: cooldown gate (hasCooldown(false) -> deductCharge() only on success), ray-cast -> BFS -> damage application, structured logging, try/catch wrapper to keep a runtime fault from killing the server tick. API corrections discovered against the decompiled jar: - Ref has no uuid() — use "ref:" + getIndex() for the chain id - DamageCause.PHYSICAL is @Nullable until runtime — use the int-index overload of Damage with index 0 - Static mock of DamageSystems crashes class init — abstracted behind a DamageExecutor SAM with a default lazy holder Tests: 33/33 green (25 from Phase 2 + 4 ChainDamageApplier tests + 4 fixture sanity). ./gradlew build SUCCESSFUL, JAR auto-deployed. MANUAL UAT (10 items) pending in-game.
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
package com.mythlane.chainlightning.sceptre;
|
||||
|
||||
import com.hypixel.hytale.component.CommandBuffer;
|
||||
import com.hypixel.hytale.component.ComponentAccessor;
|
||||
import com.hypixel.hytale.component.Ref;
|
||||
import com.hypixel.hytale.server.core.modules.entity.damage.Damage;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||
import com.mythlane.chainlightning.chain.ChainHit;
|
||||
import com.mythlane.chainlightning.chain.Vec3;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Test unitaire de ChainDamageApplier -- verifie que chaque ChainHit produit exactement
|
||||
* un appel a DamageExecutor avec le bon ref + amount.
|
||||
*
|
||||
* <p>Utilise l'overload ChainDamageApplier.apply(..., DamageExecutor) pour eviter d'initialiser
|
||||
* le runtime Hytale (DamageSystems possede un initialiseur statique dependant de PluginBase).
|
||||
*/
|
||||
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);
|
||||
|
||||
List<ChainHit> hits = List.of(
|
||||
hit("a", 8, 0),
|
||||
hit("b", 6, 1),
|
||||
hit("c", 4, 2),
|
||||
hit("d", 3, 3),
|
||||
hit("e", 2, 4)
|
||||
);
|
||||
|
||||
ChainDamageApplier.apply(hits, attacker, buf, executor);
|
||||
|
||||
verify(executor, times(5)).execute(
|
||||
org.mockito.ArgumentMatchers.any(),
|
||||
org.mockito.ArgumentMatchers.eq(buf),
|
||||
org.mockito.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);
|
||||
|
||||
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);
|
||||
|
||||
verify(executor, times(3)).execute(
|
||||
org.mockito.ArgumentMatchers.any(),
|
||||
org.mockito.ArgumentMatchers.eq(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);
|
||||
|
||||
ChainDamageApplier.apply(List.of(), attacker, buf, executor);
|
||||
|
||||
verify(executor, never()).execute(
|
||||
org.mockito.ArgumentMatchers.any(),
|
||||
org.mockito.ArgumentMatchers.any(),
|
||||
org.mockito.ArgumentMatchers.any()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void apply_passes_adapter_ref_not_attacker_ref() {
|
||||
@SuppressWarnings("unchecked")
|
||||
Ref<EntityStore> attacker = (Ref<EntityStore>) mock(Ref.class, "attacker");
|
||||
@SuppressWarnings("unchecked")
|
||||
Ref<EntityStore> targetRef = (Ref<EntityStore>) mock(Ref.class, "target");
|
||||
@SuppressWarnings("unchecked")
|
||||
ComponentAccessor<EntityStore> accessor = (ComponentAccessor<EntityStore>) mock(ComponentAccessor.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
CommandBuffer<EntityStore> buf = (CommandBuffer<EntityStore>) mock(CommandBuffer.class);
|
||||
ChainDamageApplier.DamageExecutor executor = mock(ChainDamageApplier.DamageExecutor.class);
|
||||
|
||||
// Construire un HytaleEntityAdapter via forTest pour eviter l'initialisation
|
||||
// du runtime Hytale (TransformComponent.getComponentType() -> PluginBase -> HytaleLogger).
|
||||
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);
|
||||
|
||||
verify(executor, times(1)).execute(
|
||||
refCaptor.capture(),
|
||||
org.mockito.ArgumentMatchers.eq(buf),
|
||||
org.mockito.ArgumentMatchers.any(Damage.class)
|
||||
);
|
||||
|
||||
assertThat(refCaptor.getValue()).isSameAs(targetRef);
|
||||
assertThat(refCaptor.getValue()).isNotSameAs(attacker);
|
||||
}
|
||||
|
||||
// --- helper : cree un ChainHit avec un HytaleEntityAdapter mock-friendly ---
|
||||
private static ChainHit hit(String id, int dmg, int hop) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Ref<EntityStore> ref = (Ref<EntityStore>) mock(Ref.class, id);
|
||||
@SuppressWarnings("unchecked")
|
||||
ComponentAccessor<EntityStore> accessor = (ComponentAccessor<EntityStore>) mock(ComponentAccessor.class);
|
||||
// Utiliser forTest pour eviter l'initialisation du runtime Hytale (TransformComponent -> PluginBase).
|
||||
HytaleEntityAdapter adapter = HytaleEntityAdapter.forTest(ref, id, Vec3.ZERO, false);
|
||||
return new ChainHit(adapter, dmg, hop);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user