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:
+92
-27
@@ -7,22 +7,53 @@ import com.hypixel.hytale.protocol.InteractionType;
|
||||
import com.hypixel.hytale.server.core.entity.InteractionContext;
|
||||
import com.hypixel.hytale.server.core.modules.interaction.interaction.CooldownHandler;
|
||||
import com.hypixel.hytale.server.core.modules.interaction.interaction.config.SimpleInstantInteraction;
|
||||
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||
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.UUID;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Phase 1 stub interaction handler for the Chain Lightning Sceptre item.
|
||||
* Logs the click + applies a 4s cooldown smoke-test. Full chain logic lands in Phase 3.
|
||||
* Phase 3 — Orchestrateur runtime du sceptre Chain Lightning.
|
||||
*
|
||||
* <p>Pipeline (per CONTEXT.md "Pipeline firstRun — séquence exacte") :
|
||||
* <ol>
|
||||
* <li>Récupérer (ou créer) le cooldown "chain_lightning_sceptre" (4.0s, 1 charge).</li>
|
||||
* <li>Si {@code hasCooldown(false)} → return silencieux (refus UX).</li>
|
||||
* <li>Extraire playerRef + commandBuffer depuis InteractionContext.</li>
|
||||
* <li>Construire HytalePlayerRayCaster + HytaleEntitySource (frontière vers Phase 2).</li>
|
||||
* <li>Appeler ChainResolver.resolve avec Vec3.ZERO placeholders pour origin/direction
|
||||
* (la lambda ray-cast les ignore — TargetUtil reconstruit eye-origin en interne).</li>
|
||||
* <li>Si hits.isEmpty() → log fine + return SANS consommer le cooldown (rater = re-cliquer
|
||||
* immédiatement permis, decision CONTEXT).</li>
|
||||
* <li>ChainDamageApplier.apply(hits, playerRef, commandBuffer, commandBuffer).</li>
|
||||
* <li>cooldown.deductCharge() APRÈS succès (decision CONTEXT : pas de cooldown si rate).</li>
|
||||
* <li>Log info structuré avec count + ids.</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>Try/catch global wrappe les étapes 3-9 — toute exception est loggée mais non propagée
|
||||
* (éviter de crash le tick serveur, decision CONTEXT "Pas de try/catch défensif partout").
|
||||
*/
|
||||
public final class ChainLightningSceptreInteraction extends SimpleInstantInteraction {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(ChainLightningSceptreInteraction.class.getName());
|
||||
|
||||
// --- Constantes cooldown (per RESEARCH Q3) ---
|
||||
private static final String COOLDOWN_ID = "chain_lightning_sceptre";
|
||||
private static final float COOLDOWN_TIME = 4.0f;
|
||||
private static final float[] CHARGE_TIMES = new float[]{4.0f};
|
||||
private static final boolean FORCE_CREATE = true;
|
||||
private static final boolean INTERRUPT_RECHARGE = false;
|
||||
|
||||
// --- Constantes chaîne (per ROADMAP CHAIN-02 + CHAIN-03) ---
|
||||
private static final double RAY_MAX_BLOCKS = 25.0;
|
||||
|
||||
// --- BuilderCodec préservé tel quel depuis Phase 1 ---
|
||||
@Nonnull
|
||||
public static final BuilderCodec<ChainLightningSceptreInteraction> CODEC =
|
||||
((BuilderCodec.Builder) BuilderCodec
|
||||
@@ -40,31 +71,65 @@ public final class ChainLightningSceptreInteraction extends SimpleInstantInterac
|
||||
protected void firstRun(@Nonnull InteractionType type,
|
||||
@Nonnull InteractionContext context,
|
||||
@Nonnull CooldownHandler cooldownHandler) {
|
||||
// Phase 1: stub only. Identify the player for traceability, log, then arm the cooldown
|
||||
// (smoke test of the CooldownHandler API — actual chain resolution lands in Phase 3).
|
||||
UUID playerUuid = null;
|
||||
try {
|
||||
Ref<EntityStore> entityRef = context.getEntity();
|
||||
CommandBuffer<EntityStore> commandBuffer = context.getCommandBuffer();
|
||||
if (entityRef != null && commandBuffer != null) {
|
||||
PlayerRef playerRef = commandBuffer.getComponent(entityRef, PlayerRef.getComponentType());
|
||||
if (playerRef != null) {
|
||||
playerUuid = playerRef.getUuid();
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
// Defensive: never fail the click because of telemetry.
|
||||
// --- Étape 1 : récupérer le cooldown ---
|
||||
CooldownHandler.Cooldown cooldown = cooldownHandler.getCooldown(
|
||||
COOLDOWN_ID, COOLDOWN_TIME, CHARGE_TIMES, FORCE_CREATE, INTERRUPT_RECHARGE);
|
||||
if (cooldown == null) {
|
||||
LOGGER.log(Level.WARNING, "[ChainLightning] cooldown handler returned null — aborting");
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.log(Level.INFO,
|
||||
"[ChainLightning] sceptre clicked by player {0} (type={1})",
|
||||
new Object[]{playerUuid, type});
|
||||
// --- Étape 2 : check cooldown sans décompter ---
|
||||
if (cooldown.hasCooldown(false)) {
|
||||
return; // refus silencieux
|
||||
}
|
||||
|
||||
// TODO Phase 3: wire cooldown via CooldownHandler.
|
||||
// Confirmed API (javap on Server-2026.03.26-89796e57b.jar):
|
||||
// CooldownHandler has NO cooldown(Duration) method.
|
||||
// Use: cooldownHandler.getCooldown("primary").setCooldownMax(4f)
|
||||
// then cooldownHandler.resetCooldown("primary", ...) on next tick.
|
||||
// Full cooldown integration deferred to Phase 3 to keep Phase 1 build green.
|
||||
try {
|
||||
// --- Étape 3 : extraire player + commandBuffer ---
|
||||
Ref<EntityStore> playerRef = context.getEntity();
|
||||
CommandBuffer<EntityStore> commandBuffer = context.getCommandBuffer();
|
||||
if (playerRef == null || commandBuffer == null) {
|
||||
LOGGER.log(Level.FINE, "[ChainLightning] missing playerRef or commandBuffer — abort");
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Étape 4 : construire les adapters ---
|
||||
// CommandBuffer<EntityStore> implémente ComponentAccessor<EntityStore> — passé directement
|
||||
HytalePlayerRayCaster ray = new HytalePlayerRayCaster(playerRef, commandBuffer);
|
||||
HytaleEntitySource neighbors = new HytaleEntitySource(commandBuffer);
|
||||
|
||||
// --- Étape 5 : résolution BFS (origin/direction = placeholders ignorés par le wrapper) ---
|
||||
List<ChainHit> hits = ChainResolver.resolve(
|
||||
Vec3.ZERO, Vec3.ZERO, RAY_MAX_BLOCKS,
|
||||
ray, neighbors, ChainParameters.DEFAULT
|
||||
);
|
||||
|
||||
// --- Étape 6 : pas de cible → return SANS cooldown ---
|
||||
if (hits.isEmpty()) {
|
||||
LOGGER.log(Level.FINE, "[ChainLightning] no target");
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Étape 7 : appliquer les dégâts ---
|
||||
ChainDamageApplier.apply(hits, playerRef, commandBuffer, commandBuffer);
|
||||
|
||||
// --- Étape 8 : démarrer le cooldown APRÈS succès ---
|
||||
cooldown.deductCharge();
|
||||
|
||||
// --- Étape 9 : log structuré ---
|
||||
if (LOGGER.isLoggable(Level.INFO)) {
|
||||
StringBuilder ids = new StringBuilder();
|
||||
for (int i = 0; i < hits.size(); i++) {
|
||||
if (i > 0) ids.append(',');
|
||||
ids.append(hits.get(i).target().id());
|
||||
}
|
||||
LOGGER.log(Level.INFO,
|
||||
"[ChainLightning] ref:{0} chained {1} targets [{2}]",
|
||||
new Object[]{"ref:" + playerRef.getIndex(), hits.size(), ids.toString()});
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
// CONTEXT decision : try/catch global pour éviter crash tick serveur
|
||||
LOGGER.log(Level.WARNING, "[ChainLightning] chain resolution failed", t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user