From ee9ac1ab5378295db9775f5953a3e3565d8c1a56 Mon Sep 17 00:00:00 2001 From: kayjaydee Date: Mon, 27 Apr 2026 19:08:39 +0200 Subject: [PATCH] =?UTF-8?q?feat(phase-4):=20VFX=20via=20EntityEffect=20bri?= =?UTF-8?q?dge=20=E2=80=94=20chain=20hits=20glow=20blue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .gitignore | 4 + .../chainlightning/chain/ParticleTrail.java | 6 +- .../ChainLightningSceptreInteraction.java | 11 +++ .../sceptre/HytaleVfxEmitter.java | 93 ++++++++++++++++++ .../Common/Particles/Chain_Spark.png | Bin 86 -> 0 bytes .../Entity/Effects/Chain_Hit_Effect.json | 19 ++++ .../Particles/Chain_Spark.particlespawner | 32 ------ .../Particles/Chain_Spark.particlesystem | 10 -- 8 files changed, 131 insertions(+), 44 deletions(-) create mode 100644 src/main/java/com/mythlane/chainlightning/sceptre/HytaleVfxEmitter.java delete mode 100644 src/main/resources/Common/Particles/Chain_Spark.png create mode 100644 src/main/resources/Server/Entity/Effects/Chain_Hit_Effect.json delete mode 100644 src/main/resources/Server/Particles/Chain_Spark.particlespawner delete mode 100644 src/main/resources/Server/Particles/Chain_Spark.particlesystem diff --git a/.gitignore b/.gitignore index d34d2a2..11d83bd 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,7 @@ logs/ .env .env.local *.local + +# Phase 4 — temporary asset sources (not committed) +*.zip +note diff --git a/src/main/java/com/mythlane/chainlightning/chain/ParticleTrail.java b/src/main/java/com/mythlane/chainlightning/chain/ParticleTrail.java index 99944a0..70815fa 100644 --- a/src/main/java/com/mythlane/chainlightning/chain/ParticleTrail.java +++ b/src/main/java/com/mythlane/chainlightning/chain/ParticleTrail.java @@ -18,8 +18,10 @@ import java.util.List; */ public final class ParticleTrail { - /** Default trail density in particles per block. */ - public static final double TRAIL_DENSITY = 4.0; + /** Default trail density in particles per block. Reduced from 4.0 (post 14:37 UAT) to keep + * total per-click emit count low enough that the client doesn't throttle at the particle + * budget. With 5-hop chain at 5 blocks each: 1.0 = ~25 arcs total instead of ~100. */ + public static final double TRAIL_DENSITY = 1.0; private ParticleTrail() { // utility class — no instances diff --git a/src/main/java/com/mythlane/chainlightning/sceptre/ChainLightningSceptreInteraction.java b/src/main/java/com/mythlane/chainlightning/sceptre/ChainLightningSceptreInteraction.java index e621c4c..f491b6c 100644 --- a/src/main/java/com/mythlane/chainlightning/sceptre/ChainLightningSceptreInteraction.java +++ b/src/main/java/com/mythlane/chainlightning/sceptre/ChainLightningSceptreInteraction.java @@ -136,6 +136,17 @@ public final class ChainLightningSceptreInteraction extends SimpleInstantInterac ChainDamageApplier.apply(hits, playerRef, commandBuffer, commandBuffer); LOGGER.info("[ChainLightning][7/9] damage application returned"); + // --- Étape 7.5 : émettre VFX/SFX (best-effort) --- + // CONTEXT failure-mode decision : damage déjà appliqué — si l'emit échoue, log + continue + // vers le cooldown. Pas de propagation : le tick serveur ne doit pas crash sur un bug VFX. + try { + LOGGER.info(String.format("[ChainLightning][7.5/9] vfx emit START hits=%d", hits.size())); + HytaleVfxEmitter.playChainEffects(hits, playerRef, commandBuffer); + LOGGER.info("[ChainLightning][7.5/9] vfx emit DONE"); + } catch (Throwable t) { + LOGGER.log(Level.WARNING, "[ChainLightning][7.5/9] vfx emit failed (damage already applied)", t); + } + // --- Étape 8 : démarrer le cooldown APRÈS succès --- cooldown.deductCharge(); LOGGER.info(String.format("[ChainLightning][8/9] cooldown deducted (next available in %.1fs)", COOLDOWN_TIME)); diff --git a/src/main/java/com/mythlane/chainlightning/sceptre/HytaleVfxEmitter.java b/src/main/java/com/mythlane/chainlightning/sceptre/HytaleVfxEmitter.java new file mode 100644 index 0000000..70cef6b --- /dev/null +++ b/src/main/java/com/mythlane/chainlightning/sceptre/HytaleVfxEmitter.java @@ -0,0 +1,93 @@ +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.asset.type.entityeffect.config.EntityEffect; +import com.hypixel.hytale.server.core.entity.effect.EffectControllerComponent; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.mythlane.chainlightning.chain.ChainHit; + +import javax.annotation.Nonnull; +import java.util.List; +import java.util.logging.Logger; + +/** + * Phase 4 — Adapter VFX/SFX qui applique un EntityEffect custom à chaque cible touchée. + * + *

Pivot 14:52 (POC EntityEffect bridge) : les tentatives précédentes via + * {@code ParticleUtil.spawnParticleEffect("Splash", ...)} ne rendaient pas les particles côté + * client (asset sync plugin custom + budget client → 0/5 visible). On utilise maintenant le + * pattern canonique Cleric-Rod : on déclare un {@code EntityEffect} JSON (Server/Entity/Effects/ + * Chain_Hit_Effect.json) qui contient des particles vanilla inline + EntityTopTint/BottomTint, + * et on applique cet effect à chaque target via {@link EffectControllerComponent#addEffect}. + * + *

Le rendu passe par la réplication ECS (le ECS state du target propage l'effet aux clients + * automatiquement), pas par un packet SpawnParticleSystem standalone. C'est le path éprouvé. + * + *

Fallback : si le lookup de l'EntityEffect échoue (asset pas chargé), on log et skip. + * Pas de propagation : damage déjà appliqué côté caller, l'emit failure ne doit pas crash. + * + *

Hop-index ignoré pour POC : l'EntityEffect est uniforme sur les 5 cibles. La courbe + * de volume (VFX-02) reste implémentée dans {@code VolumeCurve} mais n'est pas utilisée ici -- + * une variante future pourrait définir 5 EntityEffects {@code Chain_Hit_Effect_0..4} avec des + * intensités décroissantes, ou patcher l'EntityEffect existant runtime (non supporté par l'API). + */ +public final class HytaleVfxEmitter { + + private static final Logger LOGGER = Logger.getLogger(HytaleVfxEmitter.class.getName()); + + /** Asset id du EntityEffect appliqué à chaque target touchée par la chaîne. */ + private static final String EFFECT_ID = "Chain_Hit_Effect"; + + private HytaleVfxEmitter() {} + + /** + * Applique l'EntityEffect {@code Chain_Hit_Effect} à chaque hit de la chaîne. + * + * @param hits liste résolue par ChainResolver, ordre BFS + * @param playerRef ref du caster (présent pour symétrie d'API future, non utilisé ici -- + * l'EntityEffect rend automatiquement à tous les viewers en range) + * @param commandBuffer le CommandBuffer du tick courant (sert pour {@code getComponent} et + * pour propager les changes d'état ECS au tick end) + */ + public static void playChainEffects(@Nonnull List hits, + @Nonnull Ref playerRef, + @Nonnull CommandBuffer commandBuffer) { + if (hits.isEmpty()) return; + + EntityEffect entityEffect = EntityEffect.getAssetMap().getAsset(EFFECT_ID); + if (entityEffect == null) { + LOGGER.warning(String.format( + "[ChainLightning][Vfx] EntityEffect '%s' not registered -- VFX skipped (damage already applied)", + EFFECT_ID)); + return; + } + + ComponentAccessor accessor = commandBuffer; + int applied = 0; + int skipped = 0; + for (int i = 0; i < hits.size(); i++) { + Ref targetRef = ((HytaleEntityAdapter) hits.get(i).target()).ref(); + if (targetRef == null || !targetRef.isValid()) { + skipped++; + continue; + } + EffectControllerComponent ecc = accessor.getComponent(targetRef, EffectControllerComponent.getComponentType()); + if (ecc == null) { + skipped++; + continue; + } + boolean ok = ecc.addEffect(targetRef, entityEffect, accessor); + if (ok) { + applied++; + } else { + skipped++; + } + } + + LOGGER.info(String.format( + "[ChainLightning][Vfx] EntityEffect '%s' applied to %d/%d targets (skipped=%d, caster=%s)", + EFFECT_ID, applied, hits.size(), skipped, playerRef)); + } +} diff --git a/src/main/resources/Common/Particles/Chain_Spark.png b/src/main/resources/Common/Particles/Chain_Spark.png deleted file mode 100644 index 28e5c8a0ecbdc7c7c1f95b94e5bce3c313039691..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 86 zcmeAS@N?(olHy`uVBq!ia0vp^EFjFm1|(O0oL2{=WIbIRLn`JZ-za*%-%LQ=X`>nQ jl#Rk&!W+33WH2&>P15h2-Ya|%sF1