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)); } }