refactor: drop unused VFX scaffolding, single-line English doc, reduce debug logs
- 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.
This commit is contained in:
@@ -7,24 +7,16 @@ import com.mythlane.chainlightning.sceptre.ChainLightningSceptreInteraction;
|
||||
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Entry point for the Chain Lightning Sceptre plugin.
|
||||
*
|
||||
* Phase 1 scope:
|
||||
* - Register the {@code ChainLightningSceptre} interaction codec so the runtime
|
||||
* can dispatch primary/secondary clicks on the chain_lightning_sceptre item.
|
||||
* - No chain resolution, no VFX, no config — those land in Phases 2/3/4.
|
||||
*/
|
||||
/** Plugin entry point that registers the ChainLightningSceptre interaction codec. */
|
||||
public class ChainLightningPlugin extends JavaPlugin {
|
||||
|
||||
public ChainLightningPlugin(JavaPluginInit init) {
|
||||
super(init);
|
||||
}
|
||||
|
||||
/** The interaction key must match the Type field in chain_lightning_sceptre_click.json. */
|
||||
@Override
|
||||
protected void setup() {
|
||||
// The string "ChainLightningSceptre" MUST match the "Type" field in
|
||||
// Server/Item/Interactions/chain_lightning_sceptre_click.json (case-sensitive).
|
||||
getCodecRegistry(Interaction.CODEC).register(
|
||||
"ChainLightningSceptre",
|
||||
ChainLightningSceptreInteraction.class,
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package com.mythlane.chainlightning.chain;
|
||||
|
||||
/**
|
||||
* Contrat minimal d'une cible de chaîne. Stable, mockable, sans dépendance Hytale.
|
||||
* Phase 3 adaptera l'entité Hytale vers ChainEntity à la frontière.
|
||||
*/
|
||||
/** Minimal chain target contract — stable, mockable, no Hytale dependency. */
|
||||
public interface ChainEntity {
|
||||
String id();
|
||||
Vec3 position();
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
package com.mythlane.chainlightning.chain;
|
||||
|
||||
/**
|
||||
* Une frappe résolue de la chaîne. Hop 0 = cible primaire (ray-cast).
|
||||
*
|
||||
* @param target entité touchée
|
||||
* @param damageHp dégâts à appliquer (issus de ChainParameters.damageCurve[hopIndex])
|
||||
* @param hopIndex position dans la chaîne, 0-indexed
|
||||
*/
|
||||
/** Resolved chain strike — hop 0 is the ray-cast primary target. */
|
||||
public record ChainHit(ChainEntity target, int damageHp, int hopIndex) {
|
||||
}
|
||||
|
||||
@@ -2,17 +2,9 @@ package com.mythlane.chainlightning.chain;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Paramètres figés de la résolution de chaîne. Spec v1 : DEFAULT = (5, 8.0, [8,6,4,3,2]).
|
||||
*
|
||||
* <p>Le record clone défensivement le tableau damageCurve à la construction et expose
|
||||
* une copie via {@link #damageCurve()} pour empêcher la mutation externe.
|
||||
*
|
||||
* <p>Implémente CHAIN-03 (courbe de dégâts) — D-CHAIN-03 dans CONTEXT.md.
|
||||
*/
|
||||
/** Frozen chain-resolution parameters; defensively copies the damage curve to keep the record immutable. */
|
||||
public record ChainParameters(int maxTargets, double chainRadius, int[] damageCurve) {
|
||||
|
||||
/** Configuration v1 figée par spec : 5 cibles max, rayon 8 blocs, damages [8,6,4,3,2]. */
|
||||
public static final ChainParameters DEFAULT =
|
||||
new ChainParameters(5, 8.0, new int[]{8, 6, 4, 3, 2});
|
||||
|
||||
@@ -33,7 +25,7 @@ public record ChainParameters(int maxTargets, double chainRadius, int[] damageCu
|
||||
damageCurve = Arrays.copyOf(damageCurve, damageCurve.length);
|
||||
}
|
||||
|
||||
/** Retourne une copie défensive du tableau de dégâts. */
|
||||
/** Defensive copy so callers cannot mutate the internal array. */
|
||||
@Override
|
||||
public int[] damageCurve() {
|
||||
return Arrays.copyOf(damageCurve, damageCurve.length);
|
||||
|
||||
@@ -1,37 +1,17 @@
|
||||
package com.mythlane.chainlightning.chain;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Résolveur pur stateless de la chaîne d'éclair.
|
||||
*
|
||||
* <p>Algorithme (CHAIN-01 + CHAIN-02) :
|
||||
* <ol>
|
||||
* <li>Ray-cast → cible primaire ou empty.</li>
|
||||
* <li>Boucle BFS plus-proche-voisin jusqu'à maxTargets ou plus aucun candidat.</li>
|
||||
* <li>Tie-breaker déterministe sur id() lexicographique.</li>
|
||||
* <li>Anti-double-hit via Set<String> visited.</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>Aucun side-effect — fonction pure. Aucune dépendance sur le runtime Hytale.
|
||||
*/
|
||||
/** Pure stateless BFS that builds the chain from a primary ray-cast hit, without any Hytale dependency. */
|
||||
public final class ChainResolver {
|
||||
|
||||
private ChainResolver() {
|
||||
// utility class — instantiation interdite
|
||||
}
|
||||
private ChainResolver() {}
|
||||
|
||||
/**
|
||||
* Résout la chaîne complète à partir du tir initial.
|
||||
*
|
||||
* @return liste immuable de hits dans l'ordre de la chaîne (hop 0 = primary).
|
||||
* Empty si ray-cast ne touche rien.
|
||||
*/
|
||||
/** Resolves the full chain; returns the primary plus up to maxTargets-1 nearest unique neighbors. */
|
||||
public static List<ChainHit> resolve(
|
||||
Vec3 shooterOrigin,
|
||||
Vec3 shooterDirection,
|
||||
@@ -53,37 +33,36 @@ public final class ChainResolver {
|
||||
hits.add(new ChainHit(primary, damageCurve[0], 0));
|
||||
visited.add(primary.id());
|
||||
|
||||
double radiusSq = params.chainRadius() * params.chainRadius();
|
||||
for (int hopIndex = 1; hopIndex < params.maxTargets(); hopIndex++) {
|
||||
ChainEntity current = hits.get(hits.size() - 1).target();
|
||||
List<ChainEntity> candidates = neighbors.nearby(current.position(), params.chainRadius());
|
||||
|
||||
ChainEntity next = null;
|
||||
double bestDistSq = Double.POSITIVE_INFINITY;
|
||||
|
||||
for (ChainEntity c : candidates) {
|
||||
if (!c.isAlive()) continue;
|
||||
if (visited.contains(c.id())) continue;
|
||||
double d = c.position().distanceSquared(current.position());
|
||||
if (d > params.chainRadius() * params.chainRadius()) continue;
|
||||
if (d < bestDistSq) {
|
||||
bestDistSq = d;
|
||||
next = c;
|
||||
} else if (d == bestDistSq && next != null) {
|
||||
// tie-breaker lexicographique sur id()
|
||||
if (c.id().compareTo(next.id()) < 0) {
|
||||
next = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ChainEntity next = nearestUnvisited(neighbors.nearby(current.position(), params.chainRadius()),
|
||||
current.position(), radiusSq, visited);
|
||||
if (next == null) {
|
||||
break; // chaîne terminée plus tôt
|
||||
break;
|
||||
}
|
||||
|
||||
hits.add(new ChainHit(next, damageCurve[hopIndex], hopIndex));
|
||||
visited.add(next.id());
|
||||
}
|
||||
|
||||
return List.copyOf(hits);
|
||||
}
|
||||
|
||||
/** Lexicographic id() tie-breaker keeps the BFS deterministic across ties. */
|
||||
private static ChainEntity nearestUnvisited(List<ChainEntity> candidates, Vec3 from, double radiusSq, Set<String> visited) {
|
||||
ChainEntity best = null;
|
||||
double bestDistSq = Double.POSITIVE_INFINITY;
|
||||
for (ChainEntity c : candidates) {
|
||||
if (!c.isAlive() || visited.contains(c.id())) continue;
|
||||
double d = c.position().distanceSquared(from);
|
||||
if (d > radiusSq) continue;
|
||||
if (d < bestDistSq) {
|
||||
bestDistSq = d;
|
||||
best = c;
|
||||
} else if (d == bestDistSq && best != null && c.id().compareTo(best.id()) < 0) {
|
||||
best = c;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,7 @@ package com.mythlane.chainlightning.chain;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Source d'entités voisines. SAM permettant aux tests de fournir un graphe synthétique
|
||||
* et à Phase 3 de brancher la spatial query Hytale.
|
||||
*/
|
||||
/** SAM that returns nearby entities; lets tests inject a synthetic graph and Phase 3 wire the spatial query. */
|
||||
@FunctionalInterface
|
||||
public interface EntitySource {
|
||||
List<ChainEntity> nearby(Vec3 origin, double radius);
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
package com.mythlane.chainlightning.chain;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Pure-Java sampler for the electric trail between two endpoints.
|
||||
* <p>
|
||||
* Produces evenly-spaced interpolated points strictly between {@code from} and
|
||||
* {@code to}; both endpoints are excluded by construction (the parametric
|
||||
* interpolation parameter {@code t} never reaches 0 or 1).
|
||||
* <p>
|
||||
* Density is expressed in particles per block. The sample count for a given
|
||||
* segment is {@code ceil(distance * density)}.
|
||||
* <p>
|
||||
* This class has ZERO Hytale imports — it lives behind the Phase 2 sealed
|
||||
* frontier so it can be unit-tested without runtime dependencies.
|
||||
*/
|
||||
public final class ParticleTrail {
|
||||
|
||||
/** 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
|
||||
}
|
||||
|
||||
/**
|
||||
* Sample interpolated points strictly between {@code from} and {@code to}.
|
||||
* Endpoints are NOT included in the returned list. The list is freshly
|
||||
* allocated on each call.
|
||||
*
|
||||
* @param from start endpoint (excluded from output)
|
||||
* @param to end endpoint (excluded from output)
|
||||
* @param density particles per block
|
||||
* @return newly-allocated list of interpolated points; empty if the
|
||||
* endpoints coincide or {@code density} produces a non-positive
|
||||
* count.
|
||||
*/
|
||||
public static List<Vec3> sample(Vec3 from, Vec3 to, double density) {
|
||||
double dist = from.distance(to);
|
||||
if (dist <= 0) {
|
||||
return List.of();
|
||||
}
|
||||
int count = (int) Math.ceil(dist * density);
|
||||
if (count <= 0) {
|
||||
return List.of();
|
||||
}
|
||||
List<Vec3> out = new ArrayList<>(count);
|
||||
for (int i = 1; i <= count; i++) {
|
||||
double t = (double) i / (count + 1);
|
||||
out.add(from.lerp(to, t));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,7 @@ package com.mythlane.chainlightning.chain;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Ray-cast retournant la première entité touchée le long du rayon, ou empty.
|
||||
* SAM permettant aux tests de fournir un résultat fixe et à Phase 3 de brancher
|
||||
* l'API Hytale réelle.
|
||||
*/
|
||||
/** SAM that returns the first entity hit along a ray, or empty. */
|
||||
@FunctionalInterface
|
||||
public interface RayCaster {
|
||||
Optional<ChainEntity> firstHit(Vec3 origin, Vec3 direction, double maxBlocks);
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
package com.mythlane.chainlightning.chain;
|
||||
|
||||
/**
|
||||
* Position 3D pure-Java, indépendante de l'API Hytale.
|
||||
* Utilisée par ChainResolver pour calculs de distance ; comparaisons en distanceSquared
|
||||
* pour éviter les sqrt dans la boucle BFS.
|
||||
*/
|
||||
/** Immutable 3D position used by the pure resolver, independent from the Hytale runtime. */
|
||||
public record Vec3(double x, double y, double z) {
|
||||
|
||||
/** Origine (0, 0, 0) — utilisée comme placeholder par HytalePlayerRayCaster. */
|
||||
public static final Vec3 ZERO = new Vec3(0.0, 0.0, 0.0);
|
||||
|
||||
/** Distance euclidienne au carré. Préférer cette méthode dans les boucles. */
|
||||
/** Squared euclidean distance, preferred in hot loops to avoid sqrt. */
|
||||
public double distanceSquared(Vec3 other) {
|
||||
double dx = this.x - other.x;
|
||||
double dy = this.y - other.y;
|
||||
@@ -18,17 +13,8 @@ public record Vec3(double x, double y, double z) {
|
||||
return dx * dx + dy * dy + dz * dz;
|
||||
}
|
||||
|
||||
/** Distance euclidienne. Implique un sqrt — éviter dans les hot loops. */
|
||||
/** Euclidean distance — only call outside of hot loops. */
|
||||
public double distance(Vec3 other) {
|
||||
return Math.sqrt(distanceSquared(other));
|
||||
}
|
||||
|
||||
/** Linear interpolation: t=0 returns this, t=1 returns other. */
|
||||
public Vec3 lerp(Vec3 other, double t) {
|
||||
return new Vec3(
|
||||
this.x + (other.x - this.x) * t,
|
||||
this.y + (other.y - this.y) * t,
|
||||
this.z + (other.z - this.z) * t
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
package com.mythlane.chainlightning.chain;
|
||||
|
||||
/**
|
||||
* Pure-Java volume curve for the chain-lightning sound emission.
|
||||
* <p>
|
||||
* Each successive hop in the chain emits its sound at a progressively lower
|
||||
* volume, per VFX-02. The curve is hard-coded to the spec values
|
||||
* {@code [1.0, 0.8, 0.6, 0.5, 0.4]} indexed by hop number.
|
||||
* <p>
|
||||
* This class has ZERO Hytale imports — Phase 2 sealed-frontier rule preserved.
|
||||
*/
|
||||
public final class VolumeCurve {
|
||||
|
||||
/** Spec curve from VFX-02. Order and values are exact. */
|
||||
private static final float[] CURVE = {1.0f, 0.8f, 0.6f, 0.5f, 0.4f};
|
||||
|
||||
private VolumeCurve() {
|
||||
// utility class — no instances
|
||||
}
|
||||
|
||||
/**
|
||||
* Volume for a given hop index in the chain.
|
||||
* <ul>
|
||||
* <li>Indices 0..4 return the spec values {@code 1.0, 0.8, 0.6, 0.5, 0.4}.</li>
|
||||
* <li>Indices ≥ 5 clamp to the last value {@code 0.4f}.</li>
|
||||
* <li>Negative indices throw {@link IllegalArgumentException}.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param hopIndex zero-based hop index
|
||||
* @return volume scalar in the range (0, 1]
|
||||
* @throws IllegalArgumentException if {@code hopIndex < 0}
|
||||
*/
|
||||
public static float volumeFor(int hopIndex) {
|
||||
if (hopIndex < 0) {
|
||||
throw new IllegalArgumentException("hopIndex must be >= 0, got " + hopIndex);
|
||||
}
|
||||
int clamped = Math.min(hopIndex, CURVE.length - 1);
|
||||
return CURVE[clamped];
|
||||
}
|
||||
}
|
||||
@@ -11,34 +11,15 @@ import com.mythlane.chainlightning.chain.ChainHit;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Helper static qui applique {@link DamageSystems#executeDamage} pour chaque hit d'une chaine resolue.
|
||||
*
|
||||
* <p>Utilise {@link DamageCause#PHYSICAL} pour resoudre l'index au moment de l'appel (runtime).
|
||||
* L'index est lu depuis l'asset map via {@code DamageCause.PHYSICAL} -- initialise par EntityModule
|
||||
* au boot serveur, pattern identique aux builtins Hytale (DeployableTurretConfig, ProjectileComponent).
|
||||
*
|
||||
* <p><b>Pourquoi ne pas hardcoder index=0 :</b> {@code IndexedLookupTableAssetMap} assigne les index
|
||||
* dans l'ordre de chargement filesystem des JSON -- non-deterministe. Index 0 != PHYSICAL en runtime
|
||||
* reel (bug chain-no-damage : damage.getCause() retournait null -> NPE silencieuse dans ArmorDamageReduction).
|
||||
*
|
||||
* <p>Cast explicite {@code ChainHit.target() -> HytaleEntityAdapter} : Phase 3 garantit que c'est
|
||||
* la SEULE implementation de ChainEntity produite par les adapters Phase 3.
|
||||
*
|
||||
* <p><b>Testabilite :</b> l'overload a {@link DamageExecutor} permet d'injecter un stub en test
|
||||
* sans avoir a initialiser le runtime Hytale (DamageSystems possede un initialiseur statique
|
||||
* dependant de PluginBase/HytaleLogger). En contexte de test, {@code DamageCause.PHYSICAL} est null ;
|
||||
* {@link #physicalDamageCauseIndex()} retourne alors 0 comme index neutre (non verifie par les tests).
|
||||
*/
|
||||
/** Applies DamageSystems.executeDamage to each chain hit; injectable executor keeps unit tests off the Hytale runtime. */
|
||||
public final class ChainDamageApplier {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(ChainDamageApplier.class.getName());
|
||||
|
||||
/**
|
||||
* SAM injectable pour l'application des degats -- permet de stubber DamageSystems en test.
|
||||
*/
|
||||
/** SAM seam used to swap DamageSystems for a stub in tests. */
|
||||
@FunctionalInterface
|
||||
public interface DamageExecutor {
|
||||
void execute(@Nonnull Ref<EntityStore> target,
|
||||
@@ -46,33 +27,22 @@ public final class ChainDamageApplier {
|
||||
@Nonnull Damage damage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executeur par defaut delegant a DamageSystems.executeDamage (utilise en production).
|
||||
* Charge-holder pattern : DamageSystems n'est initialise que quand DEFAULT est acces,
|
||||
* ce qui evite son chargement a l'initialisation de ChainDamageApplier en contexte de test.
|
||||
*/
|
||||
/** Lazy holder keeps DamageSystems.<clinit> out of the test classpath. */
|
||||
@SuppressWarnings("deprecation")
|
||||
private static final class DefaultHolder {
|
||||
static final DamageExecutor INSTANCE = DamageSystems::executeDamage;
|
||||
}
|
||||
|
||||
/** Retourne l'executeur par defaut (DamageSystems.executeDamage). Lazy-init. */
|
||||
/** Production executor that delegates to DamageSystems.executeDamage. */
|
||||
public static DamageExecutor defaultExecutor() {
|
||||
return DefaultHolder.INSTANCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resout l'index de la cause PHYSICAL au moment de l'appel.
|
||||
*
|
||||
* <p>En runtime Hytale, {@code DamageCause.PHYSICAL} est initialise par EntityModule au boot.
|
||||
* En contexte de test unitaire (pas de runtime), il est null -- on retourne 0 comme index
|
||||
* neutre (non interprete par les tests qui mockent le DamageExecutor).
|
||||
*/
|
||||
/** Resolves the PHYSICAL cause index at call time so the runtime asset map ordering is honored. */
|
||||
@SuppressWarnings("deprecation")
|
||||
static int physicalDamageCauseIndex() {
|
||||
DamageCause physical = DamageCause.PHYSICAL;
|
||||
if (physical == null) {
|
||||
// Contexte de test : DamageCause.PHYSICAL non initialise, index neutre.
|
||||
return 0;
|
||||
}
|
||||
return DamageCause.getAssetMap().getIndex(physical.getId());
|
||||
@@ -80,14 +50,7 @@ public final class ChainDamageApplier {
|
||||
|
||||
private ChainDamageApplier() {}
|
||||
|
||||
/**
|
||||
* Applique les degats via l'executeur par defaut (DamageSystems.executeDamage).
|
||||
*
|
||||
* @param hits liste resolue par ChainResolver, dans l'ordre BFS
|
||||
* @param attacker ref du joueur declenchant la chaine (source des degats)
|
||||
* @param commandBuffer buffer de commandes du tick courant
|
||||
* @param accessor ComponentAccessor (souvent identique au commandBuffer)
|
||||
*/
|
||||
/** Production entry point — uses the default executor. */
|
||||
public static void apply(@Nonnull List<ChainHit> hits,
|
||||
@Nonnull Ref<EntityStore> attacker,
|
||||
@Nonnull CommandBuffer<EntityStore> commandBuffer,
|
||||
@@ -95,44 +58,29 @@ public final class ChainDamageApplier {
|
||||
apply(hits, attacker, commandBuffer, defaultExecutor());
|
||||
}
|
||||
|
||||
/**
|
||||
* Applique les degats via un executeur injecte -- utilise en test pour eviter
|
||||
* l'initialisation du runtime Hytale (DamageSystems.<clinit>).
|
||||
*
|
||||
* @param hits liste resolue par ChainResolver, dans l'ordre BFS
|
||||
* @param attacker ref du joueur declenchant la chaine (source des degats)
|
||||
* @param commandBuffer buffer de commandes du tick courant
|
||||
* @param executor executeur de degats (DEFAULT en prod, stub en test)
|
||||
*/
|
||||
/** Test seam — accepts a stub executor to avoid initializing DamageSystems. */
|
||||
public static void apply(@Nonnull List<ChainHit> hits,
|
||||
@Nonnull Ref<EntityStore> attacker,
|
||||
@Nonnull CommandBuffer<EntityStore> commandBuffer,
|
||||
@Nonnull DamageExecutor executor) {
|
||||
int causeIndex = physicalDamageCauseIndex();
|
||||
LOGGER.info(String.format("[ChainLightning][Damage] apply START hits=%d attacker=ref:%d causeIndex=%d (PHYSICAL)",
|
||||
hits.size(), attacker.getIndex(), causeIndex));
|
||||
for (int i = 0; i < hits.size(); i++) {
|
||||
ChainHit hit = hits.get(i);
|
||||
int succeeded = 0;
|
||||
for (ChainHit hit : hits) {
|
||||
HytaleEntityAdapter adapter = (HytaleEntityAdapter) hit.target();
|
||||
Ref<EntityStore> targetRef = adapter.ref();
|
||||
// Utilise l'overload int avec l'index resolu depuis DamageCause.PHYSICAL au runtime.
|
||||
// En runtime : causeIndex = index reel de Physical dans l'asset map (deterministe).
|
||||
// En test : causeIndex = 0 (neutre, non verifie par les tests unitaires).
|
||||
Damage damage = new Damage(
|
||||
new Damage.EntitySource(attacker),
|
||||
causeIndex,
|
||||
(float) hit.damageHp()
|
||||
);
|
||||
LOGGER.info(String.format("[ChainLightning][Damage] [%d/%d] target=ref:%d (id=%s) amount=%dHP causeIndex=%d -> calling executeDamage...",
|
||||
i + 1, hits.size(), targetRef.getIndex(), adapter.id(), hit.damageHp(), causeIndex));
|
||||
try {
|
||||
executor.execute(targetRef, commandBuffer, damage);
|
||||
LOGGER.info(String.format("[ChainLightning][Damage] [%d/%d] executeDamage OK", i + 1, hits.size()));
|
||||
succeeded++;
|
||||
} catch (Throwable t) {
|
||||
LOGGER.log(java.util.logging.Level.WARNING,
|
||||
String.format("[ChainLightning][Damage] [%d/%d] executeDamage THREW", i + 1, hits.size()), t);
|
||||
LOGGER.log(Level.WARNING,
|
||||
String.format("[ChainLightning] damage failed on %s", adapter.id()), t);
|
||||
}
|
||||
}
|
||||
LOGGER.info("[ChainLightning][Damage] apply DONE");
|
||||
LOGGER.fine(String.format("[ChainLightning] damage applied to %d/%d targets", succeeded, hits.size()));
|
||||
}
|
||||
}
|
||||
|
||||
+39
-106
@@ -18,42 +18,19 @@ import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Phase 3 — Orchestrateur runtime du sceptre Chain Lightning.
|
||||
*
|
||||
* <p>Pipeline (per CONTEXT.md "Pipeline firstRun — séquence exacte") :
|
||||
* <ol>
|
||||
* <li>Récupérer (ou créer) le cooldown "chain_lightning_sceptre" (4.0s, 1 charge).</li>
|
||||
* <li>Si {@code hasCooldown(false)} → return silencieux (refus UX).</li>
|
||||
* <li>Extraire playerRef + commandBuffer depuis InteractionContext.</li>
|
||||
* <li>Construire HytalePlayerRayCaster + HytaleEntitySource (frontière vers Phase 2).</li>
|
||||
* <li>Appeler ChainResolver.resolve avec Vec3.ZERO placeholders pour origin/direction
|
||||
* (la lambda ray-cast les ignore — TargetUtil reconstruit eye-origin en interne).</li>
|
||||
* <li>Si hits.isEmpty() → log fine + return SANS consommer le cooldown (rater = re-cliquer
|
||||
* immédiatement permis, decision CONTEXT).</li>
|
||||
* <li>ChainDamageApplier.apply(hits, playerRef, commandBuffer, commandBuffer).</li>
|
||||
* <li>cooldown.deductCharge() APRÈS succès (decision CONTEXT : pas de cooldown si rate).</li>
|
||||
* <li>Log info structuré avec count + ids.</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>Try/catch global wrappe les étapes 3-9 — toute exception est loggée mais non propagée
|
||||
* (éviter de crash le tick serveur, decision CONTEXT "Pas de try/catch défensif partout").
|
||||
*/
|
||||
/** Runtime orchestrator: cooldown gate, chain resolution, damage, VFX emit, charge deduct. */
|
||||
public final class ChainLightningSceptreInteraction extends SimpleInstantInteraction {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(ChainLightningSceptreInteraction.class.getName());
|
||||
|
||||
// --- Constantes cooldown (per RESEARCH Q3) ---
|
||||
private static final String COOLDOWN_ID = "chain_lightning_sceptre";
|
||||
private static final float COOLDOWN_TIME = 4.0f;
|
||||
private static final float[] CHARGE_TIMES = new float[]{4.0f};
|
||||
private static final boolean FORCE_CREATE = true;
|
||||
private static final boolean INTERRUPT_RECHARGE = false;
|
||||
private static final String COOLDOWN_ID = "chain_lightning_sceptre";
|
||||
private static final float COOLDOWN_TIME = 4.0f;
|
||||
private static final float[] CHARGE_TIMES = { 4.0f };
|
||||
private static final boolean FORCE_CREATE = true;
|
||||
private static final boolean INTERRUPT_RECHARGE = false;
|
||||
|
||||
// --- Constantes chaîne (per ROADMAP CHAIN-02 + CHAIN-03) ---
|
||||
private static final double RAY_MAX_BLOCKS = 25.0;
|
||||
private static final double RAY_MAX_BLOCKS = 25.0;
|
||||
|
||||
// --- BuilderCodec préservé tel quel depuis Phase 1 ---
|
||||
@Nonnull
|
||||
public static final BuilderCodec<ChainLightningSceptreInteraction> CODEC =
|
||||
((BuilderCodec.Builder) BuilderCodec
|
||||
@@ -67,101 +44,57 @@ public final class ChainLightningSceptreInteraction extends SimpleInstantInterac
|
||||
public ChainLightningSceptreInteraction() {
|
||||
}
|
||||
|
||||
/** Runs the chain pipeline once per click; silently no-ops while on cooldown. */
|
||||
@Override
|
||||
protected void firstRun(@Nonnull InteractionType type,
|
||||
@Nonnull InteractionContext context,
|
||||
@Nonnull CooldownHandler cooldownHandler) {
|
||||
LOGGER.info(String.format("[ChainLightning][1/9] firstRun ENTRY type=%s", type));
|
||||
|
||||
// --- Étape 1 : récupérer le cooldown ---
|
||||
CooldownHandler.Cooldown cooldown = cooldownHandler.getCooldown(
|
||||
COOLDOWN_ID, COOLDOWN_TIME, CHARGE_TIMES, FORCE_CREATE, INTERRUPT_RECHARGE);
|
||||
if (cooldown == null) {
|
||||
LOGGER.warning("[ChainLightning][1/9] cooldown handler returned null — aborting");
|
||||
if (cooldown == null || cooldown.hasCooldown(false)) {
|
||||
return;
|
||||
}
|
||||
LOGGER.info(String.format("[ChainLightning][1/9] cooldown obtenu id=%s maxTime=%.1fs", COOLDOWN_ID, COOLDOWN_TIME));
|
||||
|
||||
// --- Étape 2 : check cooldown sans décompter ---
|
||||
boolean onCooldown = cooldown.hasCooldown(false);
|
||||
LOGGER.info(String.format("[ChainLightning][2/9] hasCooldown(false)=%s", onCooldown));
|
||||
if (onCooldown) {
|
||||
LOGGER.info("[ChainLightning][2/9] still on cooldown — silent refuse");
|
||||
Ref<EntityStore> playerRef = context.getEntity();
|
||||
CommandBuffer<EntityStore> commandBuffer = context.getCommandBuffer();
|
||||
if (playerRef == null || commandBuffer == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// --- Étape 3 : extraire player + commandBuffer ---
|
||||
Ref<EntityStore> playerRef = context.getEntity();
|
||||
CommandBuffer<EntityStore> commandBuffer = context.getCommandBuffer();
|
||||
LOGGER.info(String.format("[ChainLightning][3/9] playerRef=%s commandBuffer=%s",
|
||||
playerRef == null ? "null" : ("ref:" + playerRef.getIndex() + " valid=" + playerRef.isValid()),
|
||||
commandBuffer == null ? "null" : commandBuffer.getClass().getSimpleName()));
|
||||
if (playerRef == null || commandBuffer == null) {
|
||||
LOGGER.warning("[ChainLightning][3/9] missing playerRef or commandBuffer — abort");
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Étape 4 : construire les adapters ---
|
||||
// CommandBuffer<EntityStore> implémente ComponentAccessor<EntityStore> — passé directement
|
||||
HytalePlayerRayCaster ray = new HytalePlayerRayCaster(playerRef, commandBuffer);
|
||||
HytaleEntitySource neighbors = new HytaleEntitySource(commandBuffer);
|
||||
LOGGER.info("[ChainLightning][4/9] adapters built (RayCaster + EntitySource)");
|
||||
|
||||
// --- Étape 5 : résolution BFS (origin/direction = placeholders ignorés par le wrapper) ---
|
||||
LOGGER.info(String.format("[ChainLightning][5/9] resolving chain rayMax=%.1f maxTargets=%d radius=%.1f",
|
||||
RAY_MAX_BLOCKS, ChainParameters.DEFAULT.maxTargets(), ChainParameters.DEFAULT.chainRadius()));
|
||||
List<ChainHit> hits = ChainResolver.resolve(
|
||||
Vec3.ZERO, Vec3.ZERO, RAY_MAX_BLOCKS,
|
||||
ray, neighbors, ChainParameters.DEFAULT
|
||||
);
|
||||
LOGGER.info(String.format("[ChainLightning][5/9] resolution returned %d hits", hits.size()));
|
||||
|
||||
// --- Étape 6 : pas de cible → return SANS cooldown ---
|
||||
List<ChainHit> hits = resolveChain(playerRef, commandBuffer);
|
||||
if (hits.isEmpty()) {
|
||||
LOGGER.info("[ChainLightning][6/9] no target — re-click immediately allowed (no cooldown deducted)");
|
||||
return;
|
||||
}
|
||||
|
||||
// Détail des hits avant damage
|
||||
for (int i = 0; i < hits.size(); i++) {
|
||||
ChainHit h = hits.get(i);
|
||||
LOGGER.info(String.format("[ChainLightning][6/9] hit[%d] target=%s damageHp=%d hopIndex=%d",
|
||||
i, h.target().id(), h.damageHp(), h.hopIndex()));
|
||||
}
|
||||
|
||||
// --- Étape 7 : appliquer les dégâts ---
|
||||
LOGGER.info(String.format("[ChainLightning][7/9] applying damage to %d targets (attacker=ref:%d)",
|
||||
hits.size(), playerRef.getIndex()));
|
||||
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 ---
|
||||
tryEmitVfx(hits, playerRef, commandBuffer);
|
||||
cooldown.deductCharge();
|
||||
LOGGER.info(String.format("[ChainLightning][8/9] cooldown deducted (next available in %.1fs)", COOLDOWN_TIME));
|
||||
|
||||
// --- Étape 9 : log structuré final ---
|
||||
StringBuilder ids = new StringBuilder();
|
||||
for (int i = 0; i < hits.size(); i++) {
|
||||
if (i > 0) ids.append(',');
|
||||
ids.append(hits.get(i).target().id());
|
||||
}
|
||||
LOGGER.info(String.format("[ChainLightning][9/9] DONE ref:%d chained %d targets [%s]",
|
||||
playerRef.getIndex(), hits.size(), ids.toString()));
|
||||
LOGGER.info(String.format("[ChainLightning] ref:%d chained %d targets",
|
||||
playerRef.getIndex(), hits.size()));
|
||||
} catch (Throwable t) {
|
||||
// CONTEXT decision : try/catch global pour éviter crash tick serveur
|
||||
LOGGER.log(Level.WARNING, "[ChainLightning] chain resolution failed", t);
|
||||
LOGGER.log(Level.WARNING, "[ChainLightning] chain pipeline failed", t);
|
||||
}
|
||||
}
|
||||
|
||||
/** Builds the Hytale-bound adapters and runs the pure resolver against the live world. */
|
||||
private static List<ChainHit> resolveChain(@Nonnull Ref<EntityStore> playerRef,
|
||||
@Nonnull CommandBuffer<EntityStore> commandBuffer) {
|
||||
HytalePlayerRayCaster ray = new HytalePlayerRayCaster(playerRef, commandBuffer);
|
||||
HytaleEntitySource neighbors = new HytaleEntitySource(commandBuffer);
|
||||
return ChainResolver.resolve(
|
||||
Vec3.ZERO, Vec3.ZERO, RAY_MAX_BLOCKS,
|
||||
ray, neighbors, ChainParameters.DEFAULT
|
||||
);
|
||||
}
|
||||
|
||||
/** VFX emit is best-effort: damage is already applied so a failure must not abort the cooldown step. */
|
||||
private static void tryEmitVfx(@Nonnull List<ChainHit> hits,
|
||||
@Nonnull Ref<EntityStore> playerRef,
|
||||
@Nonnull CommandBuffer<EntityStore> commandBuffer) {
|
||||
try {
|
||||
HytaleVfxEmitter.playChainEffects(hits, playerRef, commandBuffer);
|
||||
} catch (Throwable t) {
|
||||
LOGGER.log(Level.WARNING, "[ChainLightning] vfx emit failed (damage already applied)", t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,16 +13,7 @@ import com.mythlane.chainlightning.chain.Vec3;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
/**
|
||||
* Adapter immuable Ref<EntityStore> -> ChainEntity (Phase 2 SAM).
|
||||
*
|
||||
* <p>Snapshot eager : la position et l'etat alive sont lus AU MOMENT de la creation.
|
||||
* La resolution BFS de ChainResolver lit ces valeurs figees -- robuste face a un mob
|
||||
* qui bouge ou meurt pendant la resolution.
|
||||
*
|
||||
* <p>Si TransformComponent est null (entite hors monde), l'adapter retourne un snapshot
|
||||
* "mort" (alive=false, position=Vec3.ZERO) qui sera filtre par ChainResolver.
|
||||
*/
|
||||
/** Immutable Ref<EntityStore> -> ChainEntity adapter; eager snapshot keeps BFS robust to mid-tick entity changes. */
|
||||
public final class HytaleEntityAdapter implements ChainEntity {
|
||||
|
||||
private final Ref<EntityStore> ref;
|
||||
@@ -37,23 +28,13 @@ public final class HytaleEntityAdapter implements ChainEntity {
|
||||
this.alive = alive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructeur package-private pour les tests : permet de construire un adapter
|
||||
* sans passer par snapshot() (qui initialise TransformComponent.getComponentType()
|
||||
* et donc le runtime Hytale complet).
|
||||
*/
|
||||
/** Test-only factory that bypasses Hytale TransformComponent initialization. */
|
||||
static HytaleEntityAdapter forTest(@Nonnull Ref<EntityStore> ref, @Nonnull String id,
|
||||
@Nonnull Vec3 position, boolean alive) {
|
||||
return new HytaleEntityAdapter(ref, id, position, alive);
|
||||
}
|
||||
|
||||
/**
|
||||
* Projette un Ref<EntityStore> vers un ChainEntity en lisant TransformComponent + EntityStatMap.
|
||||
*
|
||||
* @param ref reference entite Hytale
|
||||
* @param accessor ComponentAccessor (CommandBuffer implemente ComponentAccessor)
|
||||
* @return adapter snapshot. Jamais null.
|
||||
*/
|
||||
/** Reads TransformComponent + EntityStatMap once and freezes the result for the chain resolver. */
|
||||
@Nonnull
|
||||
public static HytaleEntityAdapter snapshot(@Nonnull Ref<EntityStore> ref,
|
||||
@Nonnull ComponentAccessor<EntityStore> accessor) {
|
||||
@@ -73,24 +54,13 @@ public final class HytaleEntityAdapter implements ChainEntity {
|
||||
return new HytaleEntityAdapter(ref, id, vec, alive);
|
||||
}
|
||||
|
||||
/** Reference Hytale sous-jacente, exposee pour DamageSystems.executeDamage. */
|
||||
/** Underlying Hytale ref, exposed so DamageSystems and EffectControllerComponent can address the entity. */
|
||||
@Nonnull
|
||||
public Ref<EntityStore> ref() {
|
||||
return ref;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String id() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vec3 position() {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAlive() {
|
||||
return alive;
|
||||
}
|
||||
@Override public String id() { return id; }
|
||||
@Override public Vec3 position() { return position; }
|
||||
@Override public boolean isAlive() { return alive; }
|
||||
}
|
||||
|
||||
@@ -12,21 +12,10 @@ import com.mythlane.chainlightning.chain.Vec3;
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Implementation Phase 3 de {@link EntitySource} qui delegue a
|
||||
* {@link TargetUtil#getAllEntitiesInSphere}.
|
||||
*
|
||||
* <p><b>Note importante :</b> {@code getAllEntitiesInSphere} retourne une liste THREAD-LOCALE
|
||||
* (SpatialResource.getThreadLocalReferenceList). On la consomme immediatement en mappant chaque
|
||||
* ref vers un {@link HytaleEntityAdapter} dans une nouvelle ArrayList -- la liste retournee est
|
||||
* sure a conserver entre frames.
|
||||
*/
|
||||
/** Phase 3 EntitySource that copies TargetUtil's thread-local result into a stable list of snapshots. */
|
||||
public final class HytaleEntitySource implements EntitySource {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(HytaleEntitySource.class.getName());
|
||||
|
||||
private final ComponentAccessor<EntityStore> accessor;
|
||||
|
||||
public HytaleEntitySource(@Nonnull ComponentAccessor<EntityStore> accessor) {
|
||||
@@ -36,21 +25,14 @@ public final class HytaleEntitySource implements EntitySource {
|
||||
@Override
|
||||
public List<ChainEntity> nearby(Vec3 origin, double radius) {
|
||||
Vector3d hytaleOrigin = new Vector3d(origin.x(), origin.y(), origin.z());
|
||||
LOGGER.info(String.format("[ChainLightning][EntitySource] getAllEntitiesInSphere(origin=%s, radius=%.1f)",
|
||||
origin, radius));
|
||||
List<Ref<EntityStore>> refs = TargetUtil.getAllEntitiesInSphere(hytaleOrigin, radius, accessor);
|
||||
LOGGER.info(String.format("[ChainLightning][EntitySource] Hytale returned %d refs", refs == null ? -1 : refs.size()));
|
||||
if (refs == null) {
|
||||
return new ArrayList<>();
|
||||
if (refs == null || refs.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<ChainEntity> snapshots = new ArrayList<>(refs.size());
|
||||
for (Ref<EntityStore> ref : refs) {
|
||||
ChainEntity adapter = HytaleEntityAdapter.snapshot(ref, accessor);
|
||||
LOGGER.info(String.format("[ChainLightning][EntitySource] ref:%d -> id=%s pos=%s alive=%s",
|
||||
ref.getIndex(), adapter.id(), adapter.position(), adapter.isAlive()));
|
||||
snapshots.add(adapter);
|
||||
snapshots.add(HytaleEntityAdapter.snapshot(ref, accessor));
|
||||
}
|
||||
LOGGER.info(String.format("[ChainLightning][EntitySource] returning %d snapshots", snapshots.size()));
|
||||
return snapshots;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,23 +10,10 @@ import com.mythlane.chainlightning.chain.Vec3;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Implementation Phase 3 de {@link RayCaster} qui delegue a {@link TargetUtil#getTargetEntity}.
|
||||
*
|
||||
* <p><b>Note importante :</b> les parametres {@code origin} et {@code direction} de
|
||||
* {@link #firstHit} sont IGNORES. {@code TargetUtil.getTargetEntity} reconstruit lui-meme
|
||||
* l'origine yeux + direction du regard depuis le {@code playerRef} (TransformComponent +
|
||||
* ModelComponent.eyeHeight + HeadRotation lus en interne via {@code TargetUtil.getLook}).
|
||||
*
|
||||
* <p>Cette asymetrie est volontaire : preserve la SAM Phase 2 sans modification, tout en
|
||||
* laissant Hytale calculer l'origine yeux precise.
|
||||
*/
|
||||
/** Phase 3 RayCaster delegating to TargetUtil; origin/direction args are ignored since TargetUtil derives them from playerRef. */
|
||||
public final class HytalePlayerRayCaster implements RayCaster {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(HytalePlayerRayCaster.class.getName());
|
||||
|
||||
private final Ref<EntityStore> playerRef;
|
||||
private final ComponentAccessor<EntityStore> accessor;
|
||||
|
||||
@@ -38,18 +25,10 @@ public final class HytalePlayerRayCaster implements RayCaster {
|
||||
|
||||
@Override
|
||||
public Optional<ChainEntity> firstHit(Vec3 originIgnored, Vec3 directionIgnored, double maxBlocks) {
|
||||
LOGGER.info(String.format("[ChainLightning][RayCast] TargetUtil.getTargetEntity(playerRef=ref:%d, maxBlocks=%.1f)",
|
||||
playerRef.getIndex(), maxBlocks));
|
||||
Ref<EntityStore> target = TargetUtil.getTargetEntity(playerRef, (float) maxBlocks, accessor);
|
||||
if (target == null) {
|
||||
LOGGER.info("[ChainLightning][RayCast] no entity hit — returning Optional.empty()");
|
||||
return Optional.empty();
|
||||
}
|
||||
LOGGER.info(String.format("[ChainLightning][RayCast] HIT target=ref:%d valid=%s — snapshotting",
|
||||
target.getIndex(), target.isValid()));
|
||||
ChainEntity adapter = HytaleEntityAdapter.snapshot(target, accessor);
|
||||
LOGGER.info(String.format("[ChainLightning][RayCast] snapshot id=%s pos=%s alive=%s",
|
||||
adapter.id(), adapter.position(), adapter.isAlive()));
|
||||
return Optional.of(adapter);
|
||||
return Optional.of(HytaleEntityAdapter.snapshot(target, accessor));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,45 +12,16 @@ 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.
|
||||
*
|
||||
* <p><b>Pivot 14:52 (POC EntityEffect bridge) :</b> 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}.
|
||||
*
|
||||
* <p>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é.
|
||||
*
|
||||
* <p><b>Fallback :</b> 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.
|
||||
*
|
||||
* <p><b>Hop-index ignoré pour POC :</b> 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).
|
||||
*/
|
||||
/** Applies a custom EntityEffect to each chain target so the visual replicates via ECS sync. */
|
||||
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)
|
||||
*/
|
||||
/** 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) {
|
||||
|
||||
Reference in New Issue
Block a user