Server WARN: "Asset key 'chain_spark' has incorrect format! Expected: 'Chain_Spark'".
Hytale validator derives expected asset key from filename via PascalCase
normalization. Renamed all three files + internal SpawnerId + Texture refs.
This contradicts the GravityFlip snake_case rule for Particles specifically —
filename casing IS the asset key.
- Author chain_spark.particlesystem (Spawners[1] -> SpawnerId=chain_spark)
- Author chain_spark.particlespawner (required co-asset, Sphere/BlendAdd, embedded Particle with Animation[0,100])
- Bundle 4x4 placeholder chain_spark.png in Particles/ to satisfy CommonAssetValidator.TEXTURE_PARTICLES
- ShadowJar packages all three at exact case-sensitive path Server/Particles/
Deviation from plan: research assumed only one .particlesystem file needed. Decompiled codec
shows ParticleSpawner is a SEPARATE asset (.particlespawner) referenced by SpawnerId, plus
Particle.Texture is validated against CommonAssetRegistry (must exist as Particles/<name>.png).
Both required for the spike to validate end-to-end. Documented in SPIKE-FINDINGS.
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.