- Remove unused chain/ParticleTrail and chain/VolumeCurve (dead since the EntityEffect
pivot replaced the per-particle emit path) plus their test suites.
- Drop Vec3.lerp (only consumer was ParticleTrail).
- Strip step-by-step "[N/9]" debug logs from the orchestrator and per-entity logs
from HytaleEntitySource / HytalePlayerRayCaster / ChainDamageApplier; keep one
summary log per click and warnings on failure.
- Extract resolveChain and tryEmitVfx helpers in ChainLightningSceptreInteraction
so firstRun reads top-down (cooldown gate -> resolve -> damage -> vfx -> deduct).
- Translate every Java doc/comment to single-line English.
Tests: 30/30 green (29 baseline kept + 1 chain damage adapter test).
Build: ./gradlew shadowJar clean.
Working POC visual : chaque cible touchée par la chaîne reçoit l'EntityEffect
`Chain_Hit_Effect` (Server/Entity/Effects/) appliqué via
`EffectControllerComponent.addEffect`. L'effet contient EntityTopTint/BottomTint
bleu + un Splash particle scale 4 tinted, duration 0.6s.
## Pourquoi pas ParticleUtil.spawnParticleEffect
Tentatives extensives via le path standalone SpawnParticleSystem packet (3-arg
auto-broadcast et 7-arg explicit playerRef) ont échoué — particles invisibles
côté client malgré delivery confirmée serveur. Pattern Java→custom particle
non supporté en l'état Hytale 2026.03.26-89796e57b (asset sync plugin custom
pas wired). Vanilla "Splash" via le path standalone : 0/5 visible.
## EntityEffect = path ECS replication
`EffectControllerComponent.addEffect` ajoute un `ActiveEntityEffect` au target,
propagé aux clients via la sync ECS automatique (path Cleric-Rod / canonique).
Le client lookup l'EntityEffect par index dans son map (broadcast au connect)
et applique ApplicationEffects (tints + particles inline) sur le modèle de
l'entity. C'est ce path qui rend.
## Limitations POC
- VFX-02 (atténuation sonore) non livré : SoundEvent custom subit le même
problème de sync que ParticleSystem custom. Path standalone SoundUtil
également cassé pour assets plugin. EntityEffect a un WorldSoundEventId
field qu'on pourrait remplir avec un vanilla, deferred.
- VolumeCurve.java + ParticleTrail.java conservés (33 tests JUnit verts) mais
inutilisés runtime — la courbe de volume était hop-indexed et l'EntityEffect
est uniforme. Garder pour usage futur si Hytale fix le sync.
- TRAIL_DENSITY abaissé 4.0 → 1.0 (réduction du spam packet pendant les
itérations diagnostic, pas critique vu qu'inutilisé).
- Phase 1 items en snake_case toujours warns au boot — pas bloquant, fix
cosmétique reporté.
## Files
NEW : `src/main/resources/Server/Entity/Effects/Chain_Hit_Effect.json`
NEW : `src/main/java/com/mythlane/chainlightning/sceptre/HytaleVfxEmitter.java`
(réécrit pour utiliser EffectControllerComponent.addEffect)
MOD : `ChainLightningSceptreInteraction.java` étape 7.5 — passe playerRef
MOD : `ParticleTrail.TRAIL_DENSITY` 4.0 → 1.0
DEL : `Server/Particles/Chain_Spark.{particlesystem,particlespawner}` +
`Common/Particles/Chain_Spark.png` (dropped — inutilisés par EntityEffect
qui référence vanilla Splash)
.gitignore : ignore *.zip + note (asset source temporaires)
Tests : 41/41 verts (29 baseline + 12 Phase 4 pure-Java).
Build : `./gradlew shadowJar` clean.
UAT 17:05 : confirmé visuel — mobs glow bleu sur hit chain.
Bug: ChainDamageApplier hardcoded causeIndex=0 assuming Physical was
loaded first. IndexedLookupTableAssetMap assigns indexes by filesystem
load order — non-deterministic. Index 0 was not Physical at runtime,
damage.getCause() returned null, NPE silently swallowed in
ArmorDamageReduction before any HP was deducted.
Fix: physicalDamageCauseIndex() resolves the real index at call time
via DamageCause.PHYSICAL.getId() + getAssetMap().getIndex(...). Test
contexts where PHYSICAL is null fall back to 0 (neutral, not asserted).
Also: replace getLogger().log(Level, fmt, args) with String.format
upstream — the Hytale log handler does not interpolate {0}/{1}/{2}
placeholders. Add per-step verbose logs throughout the pipeline
(firstRun 1/9..9/9, RayCast hit/miss + snapshot, EntitySource per-ref,
ChainDamageApplier per-hit before/after) to make future runtime
diagnostics trivial.
UAT confirmed in-game: 5 sheep chained at 8/6/4/3/2 HP, cooldown gates
the second click within 4s, no exceptions, causeIndex resolves to 6
(stable across restarts).
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.