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) : - *

    - *
  1. Ray-cast → cible primaire ou empty.
  2. - *
  3. Boucle BFS plus-proche-voisin jusqu'à maxTargets ou plus aucun candidat.
  4. - *
  5. Tie-breaker déterministe sur id() lexicographique.
  6. - *
  7. Anti-double-hit via Set<String> visited.
  8. - *
- * - *

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. - *

- * - * @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") : - *

    - *
  1. Récupérer (ou créer) le cooldown "chain_lightning_sceptre" (4.0s, 1 charge).
  2. - *
  3. Si {@code hasCooldown(false)} → return silencieux (refus UX).
  4. - *
  5. Extraire playerRef + commandBuffer depuis InteractionContext.
  6. - *
  7. Construire HytalePlayerRayCaster + HytaleEntitySource (frontière vers Phase 2).
  8. - *
  9. Appeler ChainResolver.resolve avec Vec3.ZERO placeholders pour origin/direction - * (la lambda ray-cast les ignore — TargetUtil reconstruit eye-origin en interne).
  10. - *
  11. Si hits.isEmpty() → log fine + return SANS consommer le cooldown (rater = re-cliquer - * immédiatement permis, decision CONTEXT).
  12. - *
  13. ChainDamageApplier.apply(hits, playerRef, commandBuffer, commandBuffer).
  14. - *
  15. cooldown.deductCharge() APRÈS succès (decision CONTEXT : pas de cooldown si rate).
  16. - *
  17. Log info structuré avec count + ids.
  18. - *
- * - *

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