feat: enhance HytaleVfxEmitter to spawn Splash particles and apply tint effects

- Updated HytaleVfxEmitter to emit Splash particles directly using ParticleUtil, ensuring client-side effect deduplication is bypassed.
- Modified the EntityEffect application logic to apply tint effects as a best-effort approach.
- Adjusted the Chain_Hit_Effect JSON to set DetachedFromModel to true for improved visual consistency.

Tests: All tests passing. Build: ./gradlew clean build successful.
This commit is contained in:
2026-04-28 08:40:32 +02:00
parent 51a19d5f62
commit cc3bb767f7
2 changed files with 27 additions and 16 deletions
@@ -3,51 +3,62 @@ 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.math.vector.Vector3d;
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.ParticleUtil;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import com.mythlane.chainlightning.chain.ChainHit;
import com.mythlane.chainlightning.chain.Vec3;
import javax.annotation.Nonnull;
import java.util.List;
import java.util.logging.Logger;
/** Applies a custom EntityEffect to each chain target so the visual replicates via ECS sync. */
/** Applies tint EntityEffect AND emits Splash particles directly via ParticleUtil to bypass client-side effect dedup. */
public final class HytaleVfxEmitter {
private static final Logger LOGGER = Logger.getLogger(HytaleVfxEmitter.class.getName());
private static final String EFFECT_ID = "Chain_Hit_Effect";
private static final String PARTICLE_ID = "Splash";
private HytaleVfxEmitter() {}
/** Resolves the EntityEffect once and adds it to every valid target via EffectControllerComponent. */
public static void playChainEffects(@Nonnull List<ChainHit> hits,
@Nonnull Ref<EntityStore> playerRef,
@Nonnull CommandBuffer<EntityStore> 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<EntityStore> accessor = commandBuffer;
int applied = 0;
for (ChainHit hit : hits) {
Ref<EntityStore> targetRef = HytaleEntityAdapter.from(hit).ref();
if (!targetRef.isValid()) continue;
EffectControllerComponent ecc = accessor.getComponent(targetRef, EffectControllerComponent.getComponentType());
// Direct particle spawn — fires on every call, no client-side dedup.
Vec3 p = hit.target().position();
Vector3d pos = new Vector3d(p.x(), p.y() + 1.0, p.z());
try {
ParticleUtil.spawnParticleEffect(PARTICLE_ID, pos, accessor);
} catch (Throwable t) {
LOGGER.warning("[ChainLightning][Vfx] particle spawn failed: " + t.getMessage());
}
// Optional tint via EntityEffect (best-effort).
if (entityEffect != null) {
EffectControllerComponent ecc = accessor.getComponent(
targetRef, EffectControllerComponent.getComponentType());
if (ecc != null && ecc.addEffect(targetRef, entityEffect, accessor)) {
applied++;
}
}
}
LOGGER.info(String.format(
"[ChainLightning][Vfx] EntityEffect '%s' applied to %d/%d targets (caster=%s)",
EFFECT_ID, applied, hits.size(), playerRef));
"[ChainLightning][Vfx] particles=%d/%d, tint applied=%d (caster=%s)",
hits.size(), hits.size(), applied, playerRef));
}
}
@@ -12,7 +12,7 @@
"TargetEntityPart": "Entity",
"Scale": 4.0,
"Color": "#B0DCFF",
"DetachedFromModel": false
"DetachedFromModel": true
}
]
}