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:
2026-04-27 12:14:58 +02:00
parent cd5d0bedd3
commit 8725b8a1c7
8 changed files with 524 additions and 27 deletions
@@ -0,0 +1,96 @@
package com.mythlane.chainlightning.sceptre;
import com.hypixel.hytale.component.ComponentAccessor;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.math.vector.Vector3d;
import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent;
import com.hypixel.hytale.server.core.modules.entitystats.EntityStatMap;
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.Vec3;
import javax.annotation.Nonnull;
/**
* Adapter immuable Ref&lt;EntityStore&gt; -&gt; ChainEntity (Phase 2 SAM).
*
* <p>Snapshot eager : la position et l'etat alive sont lus AU MOMENT de la creation.
* La resolution BFS de ChainResolver lit ces valeurs figees -- robuste face a un mob
* qui bouge ou meurt pendant la resolution.
*
* <p>Si TransformComponent est null (entite hors monde), l'adapter retourne un snapshot
* "mort" (alive=false, position=Vec3.ZERO) qui sera filtre par ChainResolver.
*/
public final class HytaleEntityAdapter implements ChainEntity {
private final Ref<EntityStore> ref;
private final String id;
private final Vec3 position;
private final boolean alive;
private HytaleEntityAdapter(Ref<EntityStore> ref, String id, Vec3 position, boolean alive) {
this.ref = ref;
this.id = id;
this.position = position;
this.alive = alive;
}
/**
* Constructeur package-private pour les tests : permet de construire un adapter
* sans passer par snapshot() (qui initialise TransformComponent.getComponentType()
* et donc le runtime Hytale complet).
*/
static HytaleEntityAdapter forTest(@Nonnull Ref<EntityStore> ref, @Nonnull String id,
@Nonnull Vec3 position, boolean alive) {
return new HytaleEntityAdapter(ref, id, position, alive);
}
/**
* Projette un Ref&lt;EntityStore&gt; vers un ChainEntity en lisant TransformComponent + EntityStatMap.
*
* @param ref reference entite Hytale
* @param accessor ComponentAccessor (CommandBuffer implemente ComponentAccessor)
* @return adapter snapshot. Jamais null.
*/
@Nonnull
public static HytaleEntityAdapter snapshot(@Nonnull Ref<EntityStore> ref,
@Nonnull ComponentAccessor<EntityStore> accessor) {
String id = "ref:" + ref.getIndex();
TransformComponent tc = accessor.getComponent(ref, TransformComponent.getComponentType());
if (tc == null) {
return new HytaleEntityAdapter(ref, id, Vec3.ZERO, false);
}
Vector3d pos = tc.getPosition();
Vec3 vec = new Vec3(pos.x, pos.y, pos.z);
EntityStatMap statMap = accessor.getComponent(ref, EntityStatMap.getComponentType());
EntityStatValue health = statMap != null ? statMap.get(DefaultEntityStatTypes.getHealth()) : null;
boolean alive = health != null && health.get() > 0.0f;
return new HytaleEntityAdapter(ref, id, vec, alive);
}
/** Reference Hytale sous-jacente, exposee pour DamageSystems.executeDamage. */
@Nonnull
public Ref<EntityStore> ref() {
return ref;
}
@Override
public String id() {
return id;
}
@Override
public Vec3 position() {
return position;
}
@Override
public boolean isAlive() {
return alive;
}
}