diff --git a/src/main/java/com/mythlane/chainlightning/ChainLightningPlugin.java b/src/main/java/com/mythlane/chainlightning/ChainLightningPlugin.java
index 8a7b688..7db0fbb 100644
--- a/src/main/java/com/mythlane/chainlightning/ChainLightningPlugin.java
+++ b/src/main/java/com/mythlane/chainlightning/ChainLightningPlugin.java
@@ -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,
diff --git a/src/main/java/com/mythlane/chainlightning/chain/ChainEntity.java b/src/main/java/com/mythlane/chainlightning/chain/ChainEntity.java
index 765c7e6..de4a86a 100644
--- a/src/main/java/com/mythlane/chainlightning/chain/ChainEntity.java
+++ b/src/main/java/com/mythlane/chainlightning/chain/ChainEntity.java
@@ -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();
diff --git a/src/main/java/com/mythlane/chainlightning/chain/ChainHit.java b/src/main/java/com/mythlane/chainlightning/chain/ChainHit.java
index e019a0a..2c9eff5 100644
--- a/src/main/java/com/mythlane/chainlightning/chain/ChainHit.java
+++ b/src/main/java/com/mythlane/chainlightning/chain/ChainHit.java
@@ -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) {
}
diff --git a/src/main/java/com/mythlane/chainlightning/chain/ChainParameters.java b/src/main/java/com/mythlane/chainlightning/chain/ChainParameters.java
index 5869794..86fcb51 100644
--- a/src/main/java/com/mythlane/chainlightning/chain/ChainParameters.java
+++ b/src/main/java/com/mythlane/chainlightning/chain/ChainParameters.java
@@ -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]).
- *
- *
Le record clone défensivement le tableau damageCurve à la construction et expose
- * une copie via {@link #damageCurve()} pour empêcher la mutation externe.
- *
- *
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);
diff --git a/src/main/java/com/mythlane/chainlightning/chain/ChainResolver.java b/src/main/java/com/mythlane/chainlightning/chain/ChainResolver.java
index 6fab78b..9599fb0 100644
--- a/src/main/java/com/mythlane/chainlightning/chain/ChainResolver.java
+++ b/src/main/java/com/mythlane/chainlightning/chain/ChainResolver.java
@@ -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.
- *
- *
Algorithme (CHAIN-01 + CHAIN-02) :
- *
- * - Ray-cast → cible primaire ou empty.
- * - Boucle BFS plus-proche-voisin jusqu'à maxTargets ou plus aucun candidat.
- * - Tie-breaker déterministe sur id() lexicographique.
- * - Anti-double-hit via Set<String> visited.
- *
- *
- * 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 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 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 candidates, Vec3 from, double radiusSq, Set 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;
+ }
}
diff --git a/src/main/java/com/mythlane/chainlightning/chain/EntitySource.java b/src/main/java/com/mythlane/chainlightning/chain/EntitySource.java
index 420ae2e..c21f8cc 100644
--- a/src/main/java/com/mythlane/chainlightning/chain/EntitySource.java
+++ b/src/main/java/com/mythlane/chainlightning/chain/EntitySource.java
@@ -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 nearby(Vec3 origin, double radius);
diff --git a/src/main/java/com/mythlane/chainlightning/chain/ParticleTrail.java b/src/main/java/com/mythlane/chainlightning/chain/ParticleTrail.java
deleted file mode 100644
index 70815fa..0000000
--- a/src/main/java/com/mythlane/chainlightning/chain/ParticleTrail.java
+++ /dev/null
@@ -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.
- *
- * 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).
- *
- * Density is expressed in particles per block. The sample count for a given
- * segment is {@code ceil(distance * density)}.
- *
- * 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 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 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;
- }
-}
diff --git a/src/main/java/com/mythlane/chainlightning/chain/RayCaster.java b/src/main/java/com/mythlane/chainlightning/chain/RayCaster.java
index db2b4e8..060e572 100644
--- a/src/main/java/com/mythlane/chainlightning/chain/RayCaster.java
+++ b/src/main/java/com/mythlane/chainlightning/chain/RayCaster.java
@@ -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 firstHit(Vec3 origin, Vec3 direction, double maxBlocks);
diff --git a/src/main/java/com/mythlane/chainlightning/chain/Vec3.java b/src/main/java/com/mythlane/chainlightning/chain/Vec3.java
index 4b91bcc..d3d0c0f 100644
--- a/src/main/java/com/mythlane/chainlightning/chain/Vec3.java
+++ b/src/main/java/com/mythlane/chainlightning/chain/Vec3.java
@@ -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
- );
- }
}
diff --git a/src/main/java/com/mythlane/chainlightning/chain/VolumeCurve.java b/src/main/java/com/mythlane/chainlightning/chain/VolumeCurve.java
deleted file mode 100644
index 016eb56..0000000
--- a/src/main/java/com/mythlane/chainlightning/chain/VolumeCurve.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package com.mythlane.chainlightning.chain;
-
-/**
- * Pure-Java volume curve for the chain-lightning sound emission.
- *
- * 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.
- *
- * 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.
- *
- * - Indices 0..4 return the spec values {@code 1.0, 0.8, 0.6, 0.5, 0.4}.
- * - Indices ≥ 5 clamp to the last value {@code 0.4f}.
- * - Negative indices throw {@link IllegalArgumentException}.
- *
- *
- * @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];
- }
-}
diff --git a/src/main/java/com/mythlane/chainlightning/sceptre/ChainDamageApplier.java b/src/main/java/com/mythlane/chainlightning/sceptre/ChainDamageApplier.java
index 9482dda..4462fe1 100644
--- a/src/main/java/com/mythlane/chainlightning/sceptre/ChainDamageApplier.java
+++ b/src/main/java/com/mythlane/chainlightning/sceptre/ChainDamageApplier.java
@@ -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.
- *
- * 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).
- *
- *
Pourquoi ne pas hardcoder index=0 : {@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).
- *
- *
Cast explicite {@code ChainHit.target() -> HytaleEntityAdapter} : Phase 3 garantit que c'est
- * la SEULE implementation de ChainEntity produite par les adapters Phase 3.
- *
- *
Testabilite : 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 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.
- *
- * 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 hits,
@Nonnull Ref attacker,
@Nonnull CommandBuffer 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 hits,
@Nonnull Ref attacker,
@Nonnull CommandBuffer 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 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()));
}
}
diff --git a/src/main/java/com/mythlane/chainlightning/sceptre/ChainLightningSceptreInteraction.java b/src/main/java/com/mythlane/chainlightning/sceptre/ChainLightningSceptreInteraction.java
index f491b6c..a682490 100644
--- a/src/main/java/com/mythlane/chainlightning/sceptre/ChainLightningSceptreInteraction.java
+++ b/src/main/java/com/mythlane/chainlightning/sceptre/ChainLightningSceptreInteraction.java
@@ -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.
- *
- * Pipeline (per CONTEXT.md "Pipeline firstRun — séquence exacte") :
- *
- * - Récupérer (ou créer) le cooldown "chain_lightning_sceptre" (4.0s, 1 charge).
- * - Si {@code hasCooldown(false)} → return silencieux (refus UX).
- * - Extraire playerRef + commandBuffer depuis InteractionContext.
- * - Construire HytalePlayerRayCaster + HytaleEntitySource (frontière vers Phase 2).
- * - Appeler ChainResolver.resolve avec Vec3.ZERO placeholders pour origin/direction
- * (la lambda ray-cast les ignore — TargetUtil reconstruit eye-origin en interne).
- * - Si hits.isEmpty() → log fine + return SANS consommer le cooldown (rater = re-cliquer
- * immédiatement permis, decision CONTEXT).
- * - ChainDamageApplier.apply(hits, playerRef, commandBuffer, commandBuffer).
- * - cooldown.deductCharge() APRÈS succès (decision CONTEXT : pas de cooldown si rate).
- * - Log info structuré avec count + ids.
- *
- *
- * 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 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 playerRef = context.getEntity();
+ CommandBuffer commandBuffer = context.getCommandBuffer();
+ if (playerRef == null || commandBuffer == null) {
return;
}
try {
- // --- Étape 3 : extraire player + commandBuffer ---
- Ref playerRef = context.getEntity();
- CommandBuffer 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 implémente ComponentAccessor — 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 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 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 resolveChain(@Nonnull Ref playerRef,
+ @Nonnull CommandBuffer 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 hits,
+ @Nonnull Ref playerRef,
+ @Nonnull CommandBuffer commandBuffer) {
+ try {
+ HytaleVfxEmitter.playChainEffects(hits, playerRef, commandBuffer);
+ } catch (Throwable t) {
+ LOGGER.log(Level.WARNING, "[ChainLightning] vfx emit failed (damage already applied)", t);
}
}
}
diff --git a/src/main/java/com/mythlane/chainlightning/sceptre/HytaleEntityAdapter.java b/src/main/java/com/mythlane/chainlightning/sceptre/HytaleEntityAdapter.java
index 8615044..135ad76 100644
--- a/src/main/java/com/mythlane/chainlightning/sceptre/HytaleEntityAdapter.java
+++ b/src/main/java/com/mythlane/chainlightning/sceptre/HytaleEntityAdapter.java
@@ -13,16 +13,7 @@ import com.mythlane.chainlightning.chain.Vec3;
import javax.annotation.Nonnull;
-/**
- * Adapter immuable Ref<EntityStore> -> ChainEntity (Phase 2 SAM).
- *
- * 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.
- *
- *
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 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 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 ref,
@Nonnull ComponentAccessor 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 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; }
}
diff --git a/src/main/java/com/mythlane/chainlightning/sceptre/HytaleEntitySource.java b/src/main/java/com/mythlane/chainlightning/sceptre/HytaleEntitySource.java
index 4e0f119..dcf2d51 100644
--- a/src/main/java/com/mythlane/chainlightning/sceptre/HytaleEntitySource.java
+++ b/src/main/java/com/mythlane/chainlightning/sceptre/HytaleEntitySource.java
@@ -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}.
- *
- * Note importante : {@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 accessor;
public HytaleEntitySource(@Nonnull ComponentAccessor accessor) {
@@ -36,21 +25,14 @@ public final class HytaleEntitySource implements EntitySource {
@Override
public List 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[> 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 snapshots = new ArrayList<>(refs.size());
for (Ref 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;
}
}
diff --git a/src/main/java/com/mythlane/chainlightning/sceptre/HytalePlayerRayCaster.java b/src/main/java/com/mythlane/chainlightning/sceptre/HytalePlayerRayCaster.java
index 99b7860..1566996 100644
--- a/src/main/java/com/mythlane/chainlightning/sceptre/HytalePlayerRayCaster.java
+++ b/src/main/java/com/mythlane/chainlightning/sceptre/HytalePlayerRayCaster.java
@@ -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}.
- *
- * ]Note importante : 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}).
- *
- *
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 playerRef;
private final ComponentAccessor accessor;
@@ -38,18 +25,10 @@ public final class HytalePlayerRayCaster implements RayCaster {
@Override
public Optional firstHit(Vec3 originIgnored, Vec3 directionIgnored, double maxBlocks) {
- LOGGER.info(String.format("[ChainLightning][RayCast] TargetUtil.getTargetEntity(playerRef=ref:%d, maxBlocks=%.1f)",
- playerRef.getIndex(), maxBlocks));
Ref 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));
}
}
diff --git a/src/main/java/com/mythlane/chainlightning/sceptre/HytaleVfxEmitter.java b/src/main/java/com/mythlane/chainlightning/sceptre/HytaleVfxEmitter.java
index 70cef6b..e1ec7d6 100644
--- a/src/main/java/com/mythlane/chainlightning/sceptre/HytaleVfxEmitter.java
+++ b/src/main/java/com/mythlane/chainlightning/sceptre/HytaleVfxEmitter.java
@@ -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.
- *
- * 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).
- */
+/** 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 hits,
@Nonnull Ref playerRef,
@Nonnull CommandBuffer commandBuffer) {
diff --git a/src/test/java/com/mythlane/chainlightning/chain/ChainResolverTest.java b/src/test/java/com/mythlane/chainlightning/chain/ChainResolverTest.java
index 9ef1bcd..e12a7f7 100644
--- a/src/test/java/com/mythlane/chainlightning/chain/ChainResolverTest.java
+++ b/src/test/java/com/mythlane/chainlightning/chain/ChainResolverTest.java
@@ -17,27 +17,22 @@ final class ChainResolverTest {
private static final Vec3 DIR = new Vec3(1, 0, 0);
private static final double RAY_MAX = 25.0;
- /** RayCaster qui retourne toujours empty. */
private static RayCaster rayMisses() {
return (o, d, max) -> Optional.empty();
}
- /** RayCaster qui retourne toujours la même entité. */
private static RayCaster rayHits(ChainEntity e) {
return (o, d, max) -> Optional.of(e);
}
- /** EntitySource qui retourne toujours la même liste de candidats (filtre par radius côté resolver). */
private static EntitySource neighborsAlways(List candidates) {
return (origin, radius) -> candidates;
}
- /** EntitySource vide. */
private static EntitySource neighborsEmpty() {
return (origin, radius) -> List.of();
}
- // 1
@Test
void resolve_noPrimaryHit_returnsEmpty() {
List hits = ChainResolver.resolve(
@@ -45,7 +40,6 @@ final class ChainResolverTest {
assertTrue(hits.isEmpty());
}
- // 2
@Test
void resolve_primaryOnly_noNeighbors_returnsSingleHit() {
ChainEntity primary = entity("p", 10, 0, 0);
@@ -57,11 +51,8 @@ final class ChainResolverTest {
assertEquals(0, hits.get(0).hopIndex());
}
- // 3
@Test
void resolve_fullChainOfFive_appliesDamageCurveAndOrder() {
- // Chaîne linéaire e0→e1→e2→e3→e4 espacés de 2 blocs sur l'axe X. Chaque entité a comme
- // voisins TOUS les autres ; le resolver choisit toujours le plus proche non visité.
ChainEntity e0 = entity("e0", 10, 0, 0);
ChainEntity e1 = entity("e1", 12, 0, 0);
ChainEntity e2 = entity("e2", 14, 0, 0);
@@ -80,10 +71,8 @@ final class ChainResolverTest {
assertSame(e4, hits.get(4).target()); assertEquals(2, hits.get(4).damageHp()); assertEquals(4, hits.get(4).hopIndex());
}
- // 4
@Test
void resolve_moreThanFiveCandidates_stopsAtMaxTargets() {
- // 10 candidats alignés à 1 bloc d'écart — on doit s'arrêter à 5
ChainEntity primary = entity("e0", 10, 0, 0);
ChainEntity e1 = entity("e1", 11, 0, 0);
ChainEntity e2 = entity("e2", 12, 0, 0);
@@ -102,10 +91,8 @@ final class ChainResolverTest {
assertEquals(5, hits.size());
}
- // 5
@Test
void resolve_candidatesOutsideRadius_excluded() {
- // Primary à origine, 3 candidats à 9 blocs (> radius 8) → aucun hop possible
ChainEntity primary = entity("p", 0, 0, 0);
ChainEntity far1 = entity("f1", 9, 0, 0);
ChainEntity far2 = entity("f2", 0, 9, 0);
@@ -120,10 +107,8 @@ final class ChainResolverTest {
assertSame(primary, hits.get(0).target());
}
- // 6
@Test
void resolve_noDoubleHit_visitedExcluded() {
- // A et B mutuellement voisins ; chaîne A→B et ne doit PAS revenir à A
ChainEntity a = entity("a", 0, 0, 0);
ChainEntity b = entity("b", 2, 0, 0);
List mutual = List.of(a, b);
@@ -137,10 +122,8 @@ final class ChainResolverTest {
assertSame(b, hits.get(1).target());
}
- // 7
@Test
void resolve_picksClosestCandidate() {
- // Primary à origine ; un candidat à 3 blocs et un à 7 blocs → le plus proche choisi
ChainEntity primary = entity("p", 0, 0, 0);
ChainEntity near = entity("near", 3, 0, 0);
ChainEntity far = entity("far", 7, 0, 0);
@@ -154,15 +137,12 @@ final class ChainResolverTest {
assertSame(near, hits.get(1).target());
}
- // 8
@Test
void resolve_tieBreaker_deterministicByEntityId() {
- // Deux candidats EXACTEMENT à la même distance (5) du primary
ChainEntity primary = entity("p", 0, 0, 0);
ChainEntity zebra = entity("zebra", 5, 0, 0);
ChainEntity alpha = entity("alpha", 0, 5, 0);
- // 100 runs avec ordre d'insertion variable → toujours alpha (id lexico < zebra)
for (int i = 0; i < 100; i++) {
List order1 = List.of(zebra, alpha);
List order2 = List.of(alpha, zebra);
@@ -177,10 +157,8 @@ final class ChainResolverTest {
}
}
- // 9
@Test
void resolve_deadEntity_excluded() {
- // Primary à origine ; voisin mort à 2 blocs, voisin vivant à 4 blocs → choisit le vivant
ChainEntity primary = entity("p", 0, 0, 0);
ChainEntity deadOne = dead("dead", 2, 0, 0);
ChainEntity alive = entity("alive", 4, 0, 0);
@@ -194,10 +172,8 @@ final class ChainResolverTest {
assertSame(alive, hits.get(1).target());
}
- // 10
@Test
void resolve_customMaxTargets_truncatesEarly() {
- // 5 candidats disponibles mais maxTargets=3 → 3 hits
ChainEntity e0 = entity("e0", 0, 0, 0);
ChainEntity e1 = entity("e1", 1, 0, 0);
ChainEntity e2 = entity("e2", 2, 0, 0);
diff --git a/src/test/java/com/mythlane/chainlightning/chain/ParticleTrailTest.java b/src/test/java/com/mythlane/chainlightning/chain/ParticleTrailTest.java
deleted file mode 100644
index 852bc93..0000000
--- a/src/test/java/com/mythlane/chainlightning/chain/ParticleTrailTest.java
+++ /dev/null
@@ -1,82 +0,0 @@
-package com.mythlane.chainlightning.chain;
-
-import java.util.List;
-
-import org.junit.jupiter.api.Test;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-final class ParticleTrailTest {
-
- private static final double EPS = 1e-9;
-
- @Test
- void zeroDistance() {
- Vec3 p = new Vec3(2, 3, 4);
- List out = ParticleTrail.sample(p, p, 4.0);
- assertTrue(out.isEmpty());
- }
-
- @Test
- void subBlockDistance() {
- // distance 0.5 along X with density 4 → ceil(2.0) = 2
- Vec3 from = new Vec3(0, 0, 0);
- Vec3 to = new Vec3(0.5, 0, 0);
- List out = ParticleTrail.sample(from, to, 4.0);
- assertEquals(2, out.size());
- // Both points strictly between endpoints
- for (Vec3 v : out) {
- assertTrue(from.distanceSquared(v) > EPS, "sample equals from");
- assertTrue(to.distanceSquared(v) > EPS, "sample equals to");
- }
- }
-
- @Test
- void integerDistanceCount() {
- Vec3 from = new Vec3(0, 0, 0);
- Vec3 to = new Vec3(5, 0, 0);
- List out = ParticleTrail.sample(from, to, 4.0);
- assertEquals(20, out.size());
- }
-
- @Test
- void fractionalDistanceCount() {
- Vec3 from = new Vec3(0, 0, 0);
- Vec3 to = new Vec3(3.7, 0, 0);
- List out = ParticleTrail.sample(from, to, 4.0);
- // ceil(3.7 * 4) = ceil(14.8) = 15
- assertEquals(15, out.size());
- }
-
- @Test
- void endpointsExcluded() {
- Vec3 from = new Vec3(1, 2, 3);
- Vec3 to = new Vec3(7, 8, 9);
- List out = ParticleTrail.sample(from, to, 4.0);
- assertTrue(out.size() > 0);
- for (Vec3 v : out) {
- assertTrue(from.distanceSquared(v) > EPS, "sample equals from endpoint");
- assertTrue(to.distanceSquared(v) > EPS, "sample equals to endpoint");
- }
- }
-
- @Test
- void uniformSpacing() {
- Vec3 from = new Vec3(0, 0, 0);
- Vec3 to = new Vec3(5, 0, 0);
- List out = ParticleTrail.sample(from, to, 4.0);
- assertEquals(20, out.size());
- double total = from.distance(to);
- double expectedSpacing = total / (out.size() + 1);
-
- // from -> first
- assertEquals(expectedSpacing, from.distance(out.get(0)), EPS);
- // last -> to
- assertEquals(expectedSpacing, out.get(out.size() - 1).distance(to), EPS);
- // consecutive
- for (int i = 1; i < out.size(); i++) {
- assertEquals(expectedSpacing, out.get(i - 1).distance(out.get(i)), EPS);
- }
- }
-}
diff --git a/src/test/java/com/mythlane/chainlightning/chain/TestChainEntity.java b/src/test/java/com/mythlane/chainlightning/chain/TestChainEntity.java
index 7da16e3..9c8c48e 100644
--- a/src/test/java/com/mythlane/chainlightning/chain/TestChainEntity.java
+++ b/src/test/java/com/mythlane/chainlightning/chain/TestChainEntity.java
@@ -1,9 +1,6 @@
package com.mythlane.chainlightning.chain;
-/**
- * Helper test record implémentant ChainEntity. Permet construction concise dans
- * les tests : entity("e1", 0, 0, 0) pour un vivant, dead("e2", 5, 0, 0) pour un mort.
- */
+/** Test-only ChainEntity record with concise factories for alive and dead entities. */
record TestChainEntity(String id, Vec3 position, boolean isAlive) implements ChainEntity {
static TestChainEntity entity(String id, double x, double y, double z) {
diff --git a/src/test/java/com/mythlane/chainlightning/chain/Vec3Test.java b/src/test/java/com/mythlane/chainlightning/chain/Vec3Test.java
index b3f18d1..85542f1 100644
--- a/src/test/java/com/mythlane/chainlightning/chain/Vec3Test.java
+++ b/src/test/java/com/mythlane/chainlightning/chain/Vec3Test.java
@@ -41,34 +41,4 @@ final class Vec3Test {
assertEquals(12.0, a.distanceSquared(b), 1e-9);
assertTrue(a.distance(b) > 3.4 && a.distance(b) < 3.5);
}
-
- @Test
- void lerpAtZeroReturnsFrom() {
- Vec3 from = Vec3.ZERO;
- Vec3 to = new Vec3(10, 10, 10);
- Vec3 result = from.lerp(to, 0.0);
- assertEquals(from.x(), result.x(), 1e-9);
- assertEquals(from.y(), result.y(), 1e-9);
- assertEquals(from.z(), result.z(), 1e-9);
- }
-
- @Test
- void lerpAtOneReturnsTo() {
- Vec3 from = Vec3.ZERO;
- Vec3 to = new Vec3(10, 10, 10);
- Vec3 result = from.lerp(to, 1.0);
- assertEquals(to.x(), result.x(), 1e-9);
- assertEquals(to.y(), result.y(), 1e-9);
- assertEquals(to.z(), result.z(), 1e-9);
- }
-
- @Test
- void lerpAtHalfReturnsMidpoint() {
- Vec3 from = new Vec3(0, 0, 0);
- Vec3 to = new Vec3(10, 0, 0);
- Vec3 result = from.lerp(to, 0.5);
- assertEquals(5.0, result.x(), 1e-9);
- assertEquals(0.0, result.y(), 1e-9);
- assertEquals(0.0, result.z(), 1e-9);
- }
}
diff --git a/src/test/java/com/mythlane/chainlightning/chain/VolumeCurveTest.java b/src/test/java/com/mythlane/chainlightning/chain/VolumeCurveTest.java
deleted file mode 100644
index 81fc126..0000000
--- a/src/test/java/com/mythlane/chainlightning/chain/VolumeCurveTest.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package com.mythlane.chainlightning.chain;
-
-import org.junit.jupiter.api.Test;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-
-final class VolumeCurveTest {
-
- @Test
- void specValues() {
- assertEquals(1.0f, VolumeCurve.volumeFor(0), 1e-6);
- assertEquals(0.8f, VolumeCurve.volumeFor(1), 1e-6);
- assertEquals(0.6f, VolumeCurve.volumeFor(2), 1e-6);
- assertEquals(0.5f, VolumeCurve.volumeFor(3), 1e-6);
- assertEquals(0.4f, VolumeCurve.volumeFor(4), 1e-6);
- }
-
- @Test
- void indexClamp() {
- assertEquals(0.4f, VolumeCurve.volumeFor(5), 1e-6);
- assertEquals(0.4f, VolumeCurve.volumeFor(99), 1e-6);
- assertEquals(0.4f, VolumeCurve.volumeFor(Integer.MAX_VALUE), 1e-6);
- }
-
- @Test
- void negativeIndexThrows() {
- assertThrows(IllegalArgumentException.class, () -> VolumeCurve.volumeFor(-1));
- assertThrows(IllegalArgumentException.class, () -> VolumeCurve.volumeFor(Integer.MIN_VALUE));
- }
-}
diff --git a/src/test/java/com/mythlane/chainlightning/sceptre/ChainDamageApplierTest.java b/src/test/java/com/mythlane/chainlightning/sceptre/ChainDamageApplierTest.java
index bc86913..fb40b67 100644
--- a/src/test/java/com/mythlane/chainlightning/sceptre/ChainDamageApplierTest.java
+++ b/src/test/java/com/mythlane/chainlightning/sceptre/ChainDamageApplierTest.java
@@ -1,7 +1,6 @@
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.modules.entity.damage.Damage;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
@@ -9,6 +8,7 @@ import com.mythlane.chainlightning.chain.ChainHit;
import com.mythlane.chainlightning.chain.Vec3;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatchers;
import java.util.List;
@@ -18,13 +18,7 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
-/**
- * Test unitaire de ChainDamageApplier -- verifie que chaque ChainHit produit exactement
- * un appel a DamageExecutor avec le bon ref + amount.
- *
- * Utilise l'overload ChainDamageApplier.apply(..., DamageExecutor) pour eviter d'initialiser
- * le runtime Hytale (DamageSystems possede un initialiseur statique dependant de PluginBase).
- */
+/** Verifies ChainDamageApplier emits one DamageExecutor call per hit; uses the executor seam to skip Hytale runtime init. */
final class ChainDamageApplierTest {
@Test
@@ -46,9 +40,9 @@ final class ChainDamageApplierTest {
ChainDamageApplier.apply(hits, attacker, buf, executor);
verify(executor, times(5)).execute(
- org.mockito.ArgumentMatchers.any(),
- org.mockito.ArgumentMatchers.eq(buf),
- org.mockito.ArgumentMatchers.any(Damage.class)
+ ArgumentMatchers.any(),
+ ArgumentMatchers.eq(buf),
+ ArgumentMatchers.any(Damage.class)
);
}
@@ -71,8 +65,8 @@ final class ChainDamageApplierTest {
ChainDamageApplier.apply(hits, attacker, buf, executor);
verify(executor, times(3)).execute(
- org.mockito.ArgumentMatchers.any(),
- org.mockito.ArgumentMatchers.eq(buf),
+ ArgumentMatchers.any(),
+ ArgumentMatchers.eq(buf),
damageCaptor.capture()
);
@@ -90,9 +84,9 @@ final class ChainDamageApplierTest {
ChainDamageApplier.apply(List.of(), attacker, buf, executor);
verify(executor, never()).execute(
- org.mockito.ArgumentMatchers.any(),
- org.mockito.ArgumentMatchers.any(),
- org.mockito.ArgumentMatchers.any()
+ ArgumentMatchers.any(),
+ ArgumentMatchers.any(),
+ ArgumentMatchers.any()
);
}
@@ -103,13 +97,9 @@ final class ChainDamageApplierTest {
@SuppressWarnings("unchecked")
Ref targetRef = (Ref) mock(Ref.class, "target");
@SuppressWarnings("unchecked")
- ComponentAccessor accessor = (ComponentAccessor) mock(ComponentAccessor.class);
- @SuppressWarnings("unchecked")
CommandBuffer buf = (CommandBuffer) mock(CommandBuffer.class);
ChainDamageApplier.DamageExecutor executor = mock(ChainDamageApplier.DamageExecutor.class);
- // Construire un HytaleEntityAdapter via forTest pour eviter l'initialisation
- // du runtime Hytale (TransformComponent.getComponentType() -> PluginBase -> HytaleLogger).
HytaleEntityAdapter adapter = HytaleEntityAdapter.forTest(targetRef, "target", Vec3.ZERO, false);
ChainHit hit = new ChainHit(adapter, 8, 0);
@@ -120,21 +110,18 @@ final class ChainDamageApplierTest {
verify(executor, times(1)).execute(
refCaptor.capture(),
- org.mockito.ArgumentMatchers.eq(buf),
- org.mockito.ArgumentMatchers.any(Damage.class)
+ ArgumentMatchers.eq(buf),
+ ArgumentMatchers.any(Damage.class)
);
assertThat(refCaptor.getValue()).isSameAs(targetRef);
assertThat(refCaptor.getValue()).isNotSameAs(attacker);
}
- // --- helper : cree un ChainHit avec un HytaleEntityAdapter mock-friendly ---
+ /** Builds a ChainHit backed by a HytaleEntityAdapter created via forTest to skip Hytale runtime init. */
private static ChainHit hit(String id, int dmg, int hop) {
@SuppressWarnings("unchecked")
Ref ref = (Ref) mock(Ref.class, id);
- @SuppressWarnings("unchecked")
- ComponentAccessor accessor = (ComponentAccessor) mock(ComponentAccessor.class);
- // Utiliser forTest pour eviter l'initialisation du runtime Hytale (TransformComponent -> PluginBase).
HytaleEntityAdapter adapter = HytaleEntityAdapter.forTest(ref, id, Vec3.ZERO, false);
return new ChainHit(adapter, dmg, hop);
}