From caf3df919b0a899f5e653af04d982e5a32f8e310 Mon Sep 17 00:00:00 2001 From: Bachir Zerourou Date: Wed, 15 Apr 2026 23:30:59 +0200 Subject: [PATCH 1/8] add three capacities of Shadow Hunter, Wave Shock of Tauren, Mirror Image of Blade Master --- .../handlers/w3x/simulation/CUnit.java | 53 +++- .../w3x/simulation/CUnitClassification.java | 2 + .../orc/blademaster/CAbilityMirrorImage.java | 109 ++++++++ .../orc/shadowhunter/CAbilityHealingWave.java | 173 ++++++++++++ .../skills/orc/shadowhunter/CAbilityHex.java | 68 +++++ .../orc/shadowhunter/CAbilitySerpentWard.java | 254 ++++++++++++++++++ .../taurenchieftain/CAbilityShockWave.java | 126 +++++++++ .../abilities/skills/util/CBuffHex.java | 70 +++++ .../w3x/simulation/data/CAbilityData.java | 34 ++- 9 files changed, 884 insertions(+), 5 deletions(-) create mode 100644 core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/blademaster/CAbilityMirrorImage.java create mode 100644 core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/shadowhunter/CAbilityHealingWave.java create mode 100644 core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/shadowhunter/CAbilityHex.java create mode 100644 core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/shadowhunter/CAbilitySerpentWard.java create mode 100644 core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/taurenchieftain/CAbilityShockWave.java create mode 100644 core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/util/CBuffHex.java diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnit.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnit.java index a8a39ee79..81e93dc5d 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnit.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnit.java @@ -286,12 +286,16 @@ public class CUnit extends CWidget { private final IntIntMap rawcodeToCooldownExpireTime = new IntIntMap(); private final IntIntMap rawcodeToCooldownStartTime = new IntIntMap(); - private byte invisLevels = 0; + + private CUnitType baseUnitType; + private CUnitType polymorphUnitType; private CTimer fadeTimer; private final List detectorLevels = new ArrayList<>(1); private long detections = 0; private MovementType movementOverride = null; + private byte invisLevels; + public CUnit(final int handleId, final int playerIndex, final float x, final float y, final float life, final War3ID typeId, final float facing, final float mana, final int maximumLife, final float lifeRegen, @@ -321,6 +325,8 @@ public CUnit(final int handleId, final int playerIndex, final float x, final flo this.defaultBehavior = this.stopBehavior; this.raisable = unitType.isRaise(); this.decays = unitType.isDecay(); + this.baseUnitType = unitType; + this.polymorphUnitType = null; initializeNonStackingBuffs(); initializeListenerLists(); addPreDamageListener(CUnitAttackPreDamageListenerPriority.ACCURACY, new CUnitDefaultAccuracyCheckListener()); @@ -4655,8 +4661,7 @@ public boolean isUnitType(final CUnitTypeJass whichUnitType) { throw new UnsupportedOperationException( "cannot ask engine if unit is poisoned: poison is not yet implemented"); case POLYMORPHED: - throw new UnsupportedOperationException( - "cannot ask engine if unit is POLYMORPHED: POLYMORPHED is not yet implemented"); + return this.classifications.contains(CUnitClassification.POLYMORPHED); case SLEEPING: boolean isSleeping = false; for (final StateModBuff buff : this.stateModBuffs) { @@ -5297,6 +5302,48 @@ public void updateFogOfWar(final CSimulation game) { } } + // polymorphed methods + public void applyPolymorph(final CSimulation game, final CUnitType newUnitType) { + if (newUnitType == null || newUnitType == this.polymorphUnitType) { + return; + } + this.polymorphUnitType = newUnitType; + this.unitType = newUnitType; + + computeAllDerivedFields(); + notifyAttacksChanged(); + notifyOrdersChanged(); + + // Notifie le rendu de swaper le modèle vers celui du type polymorphé (mouton/grenouille) + game.unitUpdatedType(this, newUnitType.getTypeId()); + + if (this.unitAnimationListener != null) { + this.unitAnimationListener.playAnimation(false, PrimaryTag.MORPH, SequenceUtils.EMPTY, 0, true); + } + } + + public void removePolymorph(final CSimulation game) { + if (this.polymorphUnitType == null) { + return; + } + this.unitType = this.baseUnitType; + this.polymorphUnitType = null; + + computeAllDerivedFields(); + notifyAttacksChanged(); + notifyOrdersChanged(); + + // Notifie le rendu de revenir au modèle d'origine + game.unitUpdatedType(this, this.baseUnitType.getTypeId()); + + if (this.unitAnimationListener != null) { + this.unitAnimationListener.playAnimation(false, PrimaryTag.MORPH, SequenceUtils.EMPTY, 0, true); + } + } + + + + public void setExplodesOnDeath(final boolean explodesOnDeath) { this.explodesOnDeath = explodesOnDeath; } diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitClassification.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitClassification.java index c9006ee0b..f46350f96 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitClassification.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitClassification.java @@ -23,6 +23,8 @@ public enum CUnitClassification { ANCIENT("ancient"), STANDON("standon"), NEUTRAL("neutral"), + POLYMORPHED("Polymorphed"), + ILLUSION("illusion"), TAUREN("tauren", "TaurenClass"); private static final Map UNIT_EDITOR_KEY_TO_CLASSIFICATION = new HashMap<>(); static { diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/blademaster/CAbilityMirrorImage.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/blademaster/CAbilityMirrorImage.java new file mode 100644 index 000000000..625db4912 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/blademaster/CAbilityMirrorImage.java @@ -0,0 +1,109 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.orc.blademaster; + +import com.etheller.warsmash.units.GameObject; +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnitClassification; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.CAbilityNoTargetSpellBase; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.util.CBuffTimedLife; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.definitions.impl.AbilityFields; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.definitions.impl.AbstractCAbilityTypeDefinition; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.OrderIds; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.timers.CTimer; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CEffectType; + +import java.util.ArrayList; +import java.util.List; + +public class CAbilityMirrorImage extends CAbilityNoTargetSpellBase { + + private int numberOfImages; + private float damagePercent; + private float duration; + private War3ID buffId; + + private final List activeImages = new ArrayList<>(); + + public CAbilityMirrorImage(final int handleId, final War3ID alias) { + super(handleId, alias); + } + + @Override + public void populateData(final GameObject worldEditorAbility, final int level) { + this.numberOfImages = worldEditorAbility.getFieldAsInteger(AbilityFields.DATA_A + level, 0); + this.damagePercent = worldEditorAbility.getFieldAsFloat(AbilityFields.DATA_B + level, 0) / 100f; + this.duration = worldEditorAbility.getFieldAsFloat(AbilityFields.DURATION + level, 0); + this.buffId = AbstractCAbilityTypeDefinition.getBuffId(worldEditorAbility, level); + } + + @Override + public int getBaseOrderId() { + return OrderIds.mirrorimage; + } + + @Override + public boolean doEffect(final CSimulation simulation, final CUnit caster, final AbilityTarget target) { + activeImages.removeIf(CUnit::isDead); + + final float facing = caster.getFacing(); + final float offset = 120f; + + simulation.createTemporarySpellEffectOnUnit(caster, getAlias(), CEffectType.CASTER); + + for (int i = 0; i < numberOfImages; i++) { + final float angle = (float) (i * (2 * Math.PI / numberOfImages)); + final float x = caster.getX() + (float) Math.cos(angle) * offset; + final float y = caster.getY() + (float) Math.sin(angle) * offset; + + final CUnit image = simulation.createUnitSimple( + caster.getTypeId(), + caster.getPlayerIndex(), + x, y, + facing + ); + + // Classifications importantes pour les illusions + image.addClassification(CUnitClassification.SUMMONED); + image.addClassification(CUnitClassification.ILLUSION); // Si cette classification n'existe pas, on la supprimera + + // Copie état de base + image.setLife(simulation, caster.getLife()); + image.setMana(caster.getMana()); + + // === Effet visuel bleu transparent (Mirror Image style) === + // Méthode alternative plus sûre dans Warsmash + simulation.unitUpdatedType(image, image.getTypeId()); // Force refresh du modèle + if (image.getUnitAnimationListener() != null) { + image.getUnitAnimationListener().playAnimation(false, + com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.PrimaryTag.STAND, + com.etheller.warsmash.viewer5.handlers.w3x.SequenceUtils.EMPTY, 0, true); + } + + // Durée de vie + image.add(simulation, new CBuffTimedLife( + simulation.getHandleIdAllocator().createId(), + this.buffId, + this.duration, + false + )); + + simulation.createTemporarySpellEffectOnUnit(image, getAlias(), CEffectType.SPECIAL); + + activeImages.add(image); + } + + // Invulnérabilité temporaire du vrai Blademaster + caster.setInvulnerable(true); + final CTimer invulnTimer = new CTimer() { + @Override + public void onFire(final CSimulation game) { + caster.setInvulnerable(false); + } + }; + simulation.registerTimer(invulnTimer); + + return false; + } +} \ No newline at end of file diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/shadowhunter/CAbilityHealingWave.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/shadowhunter/CAbilityHealingWave.java new file mode 100644 index 000000000..4fbad7b7e --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/shadowhunter/CAbilityHealingWave.java @@ -0,0 +1,173 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.orc.shadowhunter; + +import com.etheller.warsmash.units.GameObject; +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.CAbilityTargetSpellBase; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTargetVisitor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.definitions.impl.AbilityFields; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.definitions.impl.AbstractCAbilityTypeDefinition; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CAttackType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.OrderIds; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CDamageType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CEffectType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.SimulationRenderComponentLightning; + +import java.util.HashSet; +import java.util.Set; + +public class CAbilityHealingWave extends CAbilityTargetSpellBase { + + private float healAmount; + private float healReductionPerTarget; + private int numberOfTargetsHit; + private War3ID lightningId; // l'effet visuel de la vague (généralement "CLPB" ou "HEAL") + + public CAbilityHealingWave(final int handleId, final War3ID alias) { + super(handleId, alias); + } + + @Override + public void populateData(final GameObject worldEditorAbility, final int level) { + this.healAmount = worldEditorAbility.getFieldAsFloat(AbilityFields.DATA_A + level, 0); + this.numberOfTargetsHit = worldEditorAbility.getFieldAsInteger(AbilityFields.DATA_B + level, 0); + this.healReductionPerTarget = worldEditorAbility.getFieldAsFloat(AbilityFields.DATA_C + level, 0); + this.lightningId = AbstractCAbilityTypeDefinition.getLightningId(worldEditorAbility, level, 0); + } + + @Override + public int getBaseOrderId() { + return OrderIds.healingwave; + } + + @Override + public boolean doEffect(final CSimulation simulation, final CUnit caster, final AbilityTarget target) { + final CUnit targetUnit = target.visit(AbilityTargetVisitor.UNIT); + if (targetUnit != null) { + final float firstHeal = healAmount; + + // Effet visuel + soin sur la première cible + final SimulationRenderComponentLightning lightning = simulation.createLightning(caster, lightningId, targetUnit); + simulation.createTemporarySpellEffectOnUnit(targetUnit, getAlias(), CEffectType.TARGET); + + // Soigne la première cible + targetUnit.heal(simulation, firstHeal); + + // Préparation pour les sauts + final float reductionFactor = 1.0f - healReductionPerTarget; + final Set previousTargets = new HashSet<>(); + previousTargets.add(targetUnit); + + // Lance l'effet de chaîne de soins + final int jumpDelayEndTick = simulation.getGameTurnTick() + 8; // ~0.25s comme Chain Lightning + simulation.registerEffect(new CEffectHealingWave( + lightning, + caster, + targetUnit, + firstHeal * reductionFactor, + reductionFactor, + numberOfTargetsHit - 1, + previousTargets, + lightningId, + getAlias(), + jumpDelayEndTick + )); + } + return false; + } + + // ==================== Effet interne pour gérer les sauts ==================== + private static final class CEffectHealingWave implements com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.projectile.CEffect { + + private final SimulationRenderComponentLightning currentLightning; + private final CUnit caster; + private final CUnit currentTarget; + private float remainingHeal; + private final float healReduction; + private int remainingJumps; + private final Set previousTargets; + private final War3ID lightningId; + private final War3ID abilityId; + private int nextJumpTick; + + public CEffectHealingWave(SimulationRenderComponentLightning lightning, CUnit caster, CUnit currentTarget, + float remainingHeal, float healReduction, int remainingJumps, + Set previousTargets, War3ID lightningId, War3ID abilityId, int nextJumpTick) { + this.currentLightning = lightning; + this.caster = caster; + this.currentTarget = currentTarget; + this.remainingHeal = remainingHeal; + this.healReduction = healReduction; + this.remainingJumps = remainingJumps; + this.previousTargets = previousTargets; + this.lightningId = lightningId; + this.abilityId = abilityId; + this.nextJumpTick = nextJumpTick; + } + + @Override + public boolean update(final CSimulation game) { + if (game.getGameTurnTick() < nextJumpTick || remainingJumps <= 0) { + return false; + } + + // Cherche la prochaine cible alliée vivante + final CUnit nextTarget = findNextAlly(game); + + if (nextTarget != null) { + // Crée le nouvel effet visuel + final SimulationRenderComponentLightning newLightning = game.createLightning(currentTarget, lightningId, nextTarget); + game.createTemporarySpellEffectOnUnit(nextTarget, abilityId, CEffectType.TARGET); + + // Soigne + nextTarget.heal(game, remainingHeal); + + // Prépare le prochain saut + previousTargets.add(nextTarget); + this.remainingHeal *= healReduction; + this.remainingJumps--; + this.nextJumpTick = game.getGameTurnTick() + 8; // délai entre les sauts + + // Remplace l'ancien lightning par le nouveau + if (currentLightning != null) { + currentLightning.remove(); + } + + // Continue la chaîne + game.registerEffect(new CEffectHealingWave(newLightning, caster, nextTarget, remainingHeal, + healReduction, remainingJumps, previousTargets, lightningId, abilityId, nextJumpTick)); + } else { + if (currentLightning != null) { + currentLightning.remove(); + } + } + + return true; // on termine cet effet + } + + private CUnit findNextAlly(final CSimulation game) { + final float searchRadius = 500f; // rayon de recherche raisonnable + + final CUnit[] bestTarget = {null}; + final double[] bestDistance = {Float.MAX_VALUE}; + + game.getWorldCollision().enumUnitsInRange(currentTarget.getX(), currentTarget.getY(), searchRadius, unit -> { + if (unit.isDead() || !unit.isUnitAlly(game.getPlayer(caster.getPlayerIndex())) + || previousTargets.contains(unit)) { + return false; + } + + double dist = unit.distance(currentTarget); + if (dist < bestDistance[0]) { + bestDistance[0] = dist; + bestTarget[0] = unit; + } + return false; + }); + + return bestTarget[0]; + } + } +} \ No newline at end of file diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/shadowhunter/CAbilityHex.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/shadowhunter/CAbilityHex.java new file mode 100644 index 000000000..e64d77393 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/shadowhunter/CAbilityHex.java @@ -0,0 +1,68 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.orc.shadowhunter; + +import com.etheller.warsmash.units.GameObject; +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.CAbilityTargetSpellBase; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.util.CBuffHex; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTargetVisitor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.definitions.impl.AbstractCAbilityTypeDefinition; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.OrderIds; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CEffectType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnitType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.definitions.impl.AbilityFields; + +public class CAbilityHex extends CAbilityTargetSpellBase { + + private War3ID buffId; + private War3ID polymorphUnitId; + + + public CAbilityHex(final int handleId, final War3ID alias) { + super(handleId, alias); + } + + @Override + public void populateData(final GameObject worldEditorAbility, final int level) { + this.buffId = AbstractCAbilityTypeDefinition.getBuffId(worldEditorAbility, level); + // Si tu veux un polymorph unit-type configurable dans l’éditeur (comme pour Serpent Ward), + final String unitRaw = worldEditorAbility.getFieldAsString(AbilityFields.UNIT_ID + level, 0); + this.polymorphUnitId = unitRaw.length() == 4 ? War3ID.fromString(unitRaw) : War3ID.fromString("nshe"); + } + + @Override + public int getBaseOrderId() { + return OrderIds.hex; + } + + @Override + public boolean doEffect(final CSimulation simulation, final CUnit caster, final AbilityTarget target) { + final CUnit targetUnit = target.visit(AbilityTargetVisitor.UNIT); + if (targetUnit != null) { + System.out.println("HEX lancé sur : " + targetUnit.getUnitType().getName()); // ← Debug + + final float duration = getDurationForTarget(targetUnit); + + final CUnitType sheepType = simulation.getUnitData().getUnitType(War3ID.fromString("nshe")); + + if (sheepType == null) { + System.err.println("ERREUR : nshe non trouvé dans unit data !"); + return false; + } + + // On force le buff + targetUnit.add(simulation, new CBuffHex( + simulation.getHandleIdAllocator().createId(), + this.buffId, + duration, + sheepType + )); + + simulation.createTemporarySpellEffectOnUnit(targetUnit, getAlias(), CEffectType.TARGET); + System.out.println("CBuffHex ajouté avec succès !"); + } + return false; + } +} \ No newline at end of file diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/shadowhunter/CAbilitySerpentWard.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/shadowhunter/CAbilitySerpentWard.java new file mode 100644 index 000000000..0c941ec36 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/shadowhunter/CAbilitySerpentWard.java @@ -0,0 +1,254 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.orc.shadowhunter; + +import java.util.ArrayList; +import java.util.List; + +import com.etheller.warsmash.units.GameObject; +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnitClassification; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.CAbilityPointTargetSpellBase; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.util.CBuffTimedLife; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityPointTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.definitions.impl.AbilityFields; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.definitions.impl.AbstractCAbilityTypeDefinition; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.OrderIds; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CEffectType; + +/** + * Capacité "Serpent Ward" du Shadow Hunter (Orcs). + * + *

Le Shadow Hunter pose un serpent gardien à un point cible. + * Contrairement aux loups (FeralSpirit) et à l'élémentaire d'eau + * (SummonWaterElemental) qui sont des sorts sans cible, le Serpent Ward + * cible un point précis sur la carte.

+ * + *

Contraintes :

+ *
    + *
  • Cible un point (hérite de {@link CAbilityPointTargetSpellBase}).
  • + *
  • Le nombre de wards actifs simultanément est limité par niveau + * (DataA dans le SLK).
  • + *
  • Chaque nouveau lancer supprime les anciens wards du même lanceur + * si la limite est atteinte (comportement WC3 : le plus ancien est + * remplacé).
  • + *
  • La durée de vie du ward est gérée via {@link CBuffTimedLife}.
  • + *
+ */ +public class CAbilitySerpentWard extends CAbilityPointTargetSpellBase { + + private War3ID summonUnitId; + private int maxWardCount; + private War3ID buffId; + private float duration; + + + + private final List lastSummonUnits = new ArrayList<>(); + + // ----------------------------------------------------------------------- + // Constructeur + // ----------------------------------------------------------------------- + + public CAbilitySerpentWard(final int handleId, final War3ID alias) { + super(handleId, alias); + System.out.println("CAbilitySerpentWard created: " + alias); + } + + // ----------------------------------------------------------------------- + // Surcharges obligatoires + // ----------------------------------------------------------------------- + + /** + * Remplit les données depuis les fichiers SLK pour le niveau spécifié. + * Appelé par le moteur lors de l'initialisation ou du level-up. + * + *

Champs lus :

+ *
    + *
  • {@link AbilityFields#UNIT_ID} + level : rawcode de l'unité ward.
  • + *
  • {@link AbilityFields#DATA_A} + level : nombre max de wards.
  • + *
  • buffId via {@link AbstractCAbilityTypeDefinition#getBuffId}.
  • + *
+ * + * @param worldEditorAbility objet contenant les données SLK de la capacité + * @param level niveau courant de la capacité (commence à 1) + */ + @Override + public void populateData(final GameObject worldEditorAbility, final int level) { + final String unitTypeRaw = + worldEditorAbility.getFieldAsString(AbilityFields.UNIT_ID + level, 0); + this.summonUnitId = (unitTypeRaw.length() == 4) + ? War3ID.fromString(unitTypeRaw) + : War3ID.NONE; + this.maxWardCount = + worldEditorAbility.getFieldAsInteger(AbilityFields.DATA_A + level, 0); + this.buffId = + AbstractCAbilityTypeDefinition.getBuffId(worldEditorAbility, level); + this.duration = worldEditorAbility.getFieldAsFloat(AbilityFields.DURATION + level, 0); + + } + + + @Override + public int getBaseOrderId() { + return OrderIds.ward; + } + + /** + * Effectue l'effet du sort : pose un Serpent Ward au point cible. + * + *

Étapes :

+ *
    + *
  1. Récupération du point cible.
  2. + *
  3. Si le nombre max de wards est atteint, tue le plus ancien.
  4. + *
  5. Crée l'unité ward au point cible.
  6. + *
  7. Marque l'unité comme invoquée ({@link CUnitClassification#SUMMONED}).
  8. + *
  9. Ajoute un buff de durée de vie ({@link CBuffTimedLife}).
  10. + *
  11. Crée l'effet visuel d'apparition.
  12. + *
  13. Enregistre le ward dans la liste des wards actifs.
  14. + *
+ * + * @param simulation la simulation de jeu + * @param unit le Shadow Hunter qui lance le sort + * @param target le point cible sur la carte + * @return {@code false} (le sort n'est pas un sort à canal continu) + */ + @Override + public boolean doEffect( + final CSimulation simulation, + final CUnit unit, + final AbilityTarget target) { + + // ------------------------------------------------------------------- + // Récupération des coordonnées du point cible + // ------------------------------------------------------------------- + final AbilityPointTarget pointTarget = (AbilityPointTarget) target; + final float x = pointTarget.getX(); + final float y = pointTarget.getY(); + final float facing = unit.getFacing(); + + // ------------------------------------------------------------------- + // Gestion de la limite de wards actifs + // Nettoyer les wards morts de la liste, puis tuer le plus ancien + // si on atteint la limite (comportement WC3 standard) + // ------------------------------------------------------------------- + this.lastSummonUnits.removeIf(CUnit::isDead); + + if (this.lastSummonUnits.size() >= getMaxWardCount()+2) { + // Tuer le ward le plus ancien (indice 0 = premier ajouté) + final CUnit oldestWard = this.lastSummonUnits.remove(0); + if (!oldestWard.isDead()) { + oldestWard.kill(simulation); + } + } + + // ------------------------------------------------------------------- + // Création du ward au point cible + // ------------------------------------------------------------------- + final CUnit summonedUnit = simulation.createUnitSimple( + this.summonUnitId, + unit.getPlayerIndex(), + x, + y, + facing + ); + + // Marquer comme unité invoquée (affecte le comportement en jeu) + summonedUnit.addClassification(CUnitClassification.SUMMONED); + + // Ajouter le buff de durée de vie (true = expiration silencieuse) + summonedUnit.add( + simulation, + new CBuffTimedLife( + simulation.getHandleIdAllocator().createId(), + this.buffId, + this.duration, + true + ) + ); + + // Effet visuel d'apparition sur le ward + simulation.createTemporarySpellEffectOnUnit( + summonedUnit, + getAlias(), + CEffectType.SPECIAL + ); + + // Enregistrer le ward dans la liste des wards actifs + this.lastSummonUnits.add(summonedUnit); + + return false; + } + + // ----------------------------------------------------------------------- + // Getters + // ----------------------------------------------------------------------- + + /** + * Retourne le rawcode de l'unité Serpent Ward invoquée. + * + * @return rawcode de l'unité + */ + public War3ID getSummonUnitId() { + return this.summonUnitId; + } + + /** + * Retourne le nombre maximum de wards simultanés au niveau actuel. + * + * @return nombre maximum de wards + */ + public int getMaxWardCount() { + return this.maxWardCount; + } + + /** + * Retourne l'identifiant du buff de durée de vie. + * + * @return identifiant du buff + */ + public War3ID getBuffId() { + return this.buffId; + } + + /** + * Retourne la liste non modifiable des wards actifs. + * + * @return liste des wards actifs + */ + public List getLastSummonUnits() { + return java.util.Collections.unmodifiableList(this.lastSummonUnits); + } + + // ----------------------------------------------------------------------- + // Setters (pour le World Editor / outils) + // ----------------------------------------------------------------------- + + /** + * Définit le rawcode de l'unité à invoquer. + * + * @param summonUnitId rawcode de l'unité + */ + public void setSummonUnitId(final War3ID summonUnitId) { + this.summonUnitId = summonUnitId; + } + + /** + * Définit le nombre maximum de wards simultanés. + * + * @param maxWardCount nombre maximum + */ + public void setMaxWardCount(final int maxWardCount) { + this.maxWardCount = maxWardCount; + } + + /** + * Définit l'identifiant du buff de durée de vie. + * + * @param buffId identifiant du buff + */ + public void setBuffId(final War3ID buffId) { + this.buffId = buffId; + } +} \ No newline at end of file diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/taurenchieftain/CAbilityShockWave.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/taurenchieftain/CAbilityShockWave.java new file mode 100644 index 000000000..4d702ca07 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/taurenchieftain/CAbilityShockWave.java @@ -0,0 +1,126 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.orc.taurenchieftain; + +import com.etheller.warsmash.units.GameObject; +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.CAbilityPointTargetSpellBase; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.util.CBuffStun; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityPointTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.definitions.impl.AbilityFields; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.definitions.impl.AbstractCAbilityTypeDefinition; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CAttackType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.OrderIds; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CDamageType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CEffectType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CWeaponSoundTypeJass; + +import java.util.ArrayList; +import java.util.List; + +public class CAbilityShockWave extends CAbilityPointTargetSpellBase { + + private float damage; + private float distance; + private float areaOfEffect; // largeur de la vague + private float stunDuration; + private War3ID buffId; + + public CAbilityShockWave(final int handleId, final War3ID alias) { + super(handleId, alias); + } + + @Override + public void populateData(final GameObject worldEditorAbility, final int level) { + this.damage = worldEditorAbility.getFieldAsFloat(AbilityFields.DATA_A + level, 0); + this.distance = worldEditorAbility.getFieldAsFloat(AbilityFields.DATA_B + level, 0); + this.areaOfEffect = worldEditorAbility.getFieldAsFloat(AbilityFields.DATA_C + level, 0); + this.stunDuration = worldEditorAbility.getFieldAsFloat(AbilityFields.DURATION + level, 0); + this.buffId = AbstractCAbilityTypeDefinition.getBuffId(worldEditorAbility, level); + } + + @Override + public int getBaseOrderId() { + return OrderIds.shockwave; + } + + @Override + public boolean doEffect(final CSimulation simulation, final CUnit caster, final AbilityTarget target) { + final AbilityPointTarget pointTarget = (AbilityPointTarget) target; + + final float startX = caster.getX(); + final float startY = caster.getY(); + final float endX = pointTarget.getX(); + final float endY = pointTarget.getY(); + + // Direction et longueur + final float dx = endX - startX; + final float dy = endY - startY; + final float length = (float) Math.hypot(dx, dy); + + if (length < 10f) return false; // trop court + + final float dirX = dx / length; + final float dirY = dy / length; + + // Effet visuel sur le caster + simulation.createTemporarySpellEffectOnUnit(caster, getAlias(), CEffectType.CASTER); + + // Liste des unités touchées + final List hitUnits = new ArrayList<>(); + + // On cherche dans un rayon un peu plus grand que la distance + simulation.getWorldCollision().enumUnitsInRange(startX, startY, distance + 100, enumUnit -> { + if (enumUnit.isDead() + || enumUnit.isUnitAlly(simulation.getPlayer(caster.getPlayerIndex())) + || !enumUnit.canBeTargetedBy(simulation, caster, getTargetsAllowed())) { + return false; + } + + // Vérifie si l'unité est proche de la ligne de la Shockwave + if (isPointNearLine(enumUnit.getX(), enumUnit.getY(), startX, startY, dirX, dirY, length, areaOfEffect)) { + hitUnits.add(enumUnit); + } + return false; + }); + + // Appliquer l'effet sur chaque unité touchée + for (CUnit victim : hitUnits) { + victim.damage(simulation, caster, false, true, CAttackType.SPELLS, + CDamageType.UNIVERSAL, CWeaponSoundTypeJass.WHOKNOWS.name(), damage); + + victim.add(simulation, new CBuffStun( + simulation.getHandleIdAllocator().createId(), + this.buffId, + stunDuration + )); + } + + return false; + } + + /** + * Vérifie si un point est proche d'une ligne segmentée + */ + private boolean isPointNearLine(float px, float py, + float x1, float y1, + float dirX, float dirY, + float maxLength, float width) { + + float dx = px - x1; + float dy = py - y1; + + // Projection sur la direction + float proj = dx * dirX + dy * dirY; + + if (proj < 0 || proj > maxLength) { + return false; + } + + // Distance perpendiculaire à la ligne + float perpDist = Math.abs(dx * dirY - dy * dirX); + + return perpDist <= width; + } +} \ No newline at end of file diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/util/CBuffHex.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/util/CBuffHex.java new file mode 100644 index 000000000..3ec554176 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/util/CBuffHex.java @@ -0,0 +1,70 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.util; + +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnitClassification; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnitType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.unit.StateModBuff; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.unit.StateModBuffType; + +public class CBuffHex extends CBuffTimed { + + private final CUnitType polymorphUnitType; + + public CBuffHex(final int handleId, final War3ID alias, final float duration, + final CUnitType polymorphUnitType) { + super(handleId, alias, alias, duration); + this.polymorphUnitType = polymorphUnitType; + } + + @Override + protected void onBuffAdd(final CSimulation game, final CUnit unit) { + unit.addClassification(CUnitClassification.POLYMORPHED); + + // Enregistrement des state mods dans la liste + unit.addStateModBuff(new StateModBuff(StateModBuffType.DISABLE_ATTACK, getHandleId())); + unit.addStateModBuff(new StateModBuff(StateModBuffType.DISABLE_AUTO_ATTACK, getHandleId())); + unit.addStateModBuff(new StateModBuff(StateModBuffType.DISABLE_SPELLS, getHandleId())); + unit.addStateModBuff(new StateModBuff(StateModBuffType.SNARED, getHandleId())); + + // Application effective des effets — addStateModBuff seul ne suffit pas, + // computeUnitState est indispensable pour que les états soient réellement actifs. + unit.computeUnitState(game, StateModBuffType.DISABLE_ATTACK); + unit.computeUnitState(game, StateModBuffType.DISABLE_AUTO_ATTACK); + unit.computeUnitState(game, StateModBuffType.DISABLE_SPELLS); + unit.computeUnitState(game, StateModBuffType.SNARED); + + // Transformation visuelle — passe le CSimulation pour que applyPolymorph + // puisse appeler game.unitUpdatedType et swaper le modèle côté rendu. + if (this.polymorphUnitType != null) { + unit.applyPolymorph(game, this.polymorphUnitType); + } + } + + @Override + protected void onBuffRemove(final CSimulation game, final CUnit unit) { + unit.removeClassification(CUnitClassification.POLYMORPHED); + + // Retrait des state mods — même identifiant (getHandleId) qu'à l'ajout + unit.removeStateModBuff(new StateModBuff(StateModBuffType.DISABLE_ATTACK, getHandleId())); + unit.removeStateModBuff(new StateModBuff(StateModBuffType.DISABLE_AUTO_ATTACK, getHandleId())); + unit.removeStateModBuff(new StateModBuff(StateModBuffType.DISABLE_SPELLS, getHandleId())); + unit.removeStateModBuff(new StateModBuff(StateModBuffType.SNARED, getHandleId())); + + // Recalcul des états après retrait + unit.computeUnitState(game, StateModBuffType.DISABLE_ATTACK); + unit.computeUnitState(game, StateModBuffType.DISABLE_AUTO_ATTACK); + unit.computeUnitState(game, StateModBuffType.DISABLE_SPELLS); + unit.computeUnitState(game, StateModBuffType.SNARED); + + if (this.polymorphUnitType != null) { + unit.removePolymorph(game); + } + } + + @Override + public boolean isTimedLifeBar() { + return false; + } +} \ No newline at end of file diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/data/CAbilityData.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/data/CAbilityData.java index 8e4e3d8bc..95ce677be 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/data/CAbilityData.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/data/CAbilityData.java @@ -37,9 +37,18 @@ import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.nightelf.keeper.CAbilityForceOfNature; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.nightelf.moonpriestess.CAbilitySummonOwlScout; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.nightelf.warden.CAbilityBlink; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.orc.blademaster.CAbilityMirrorImage; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.orc.shadowhunter.CAbilityHealingWave; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.orc.shadowhunter.CAbilitySerpentWard; + import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.orc.blademaster.CAbilityWhirlWind; + import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.orc.farseer.CAbilityChainLightning; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.orc.farseer.CAbilityFeralSpirit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.orc.shadowhunter.CAbilitySerpentWard; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.orc.shadowhunter.CAbilityHex ; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.orc.taurenchieftain.CAbilityShockWave; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.orc.taurenchieftain.CAbilityWarStomp; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.undead.deathknight.CAbilityDarkRitual; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.undead.deathknight.CAbilityDeathCoil; @@ -137,6 +146,8 @@ private void registerCodes() { new CAbilityTypeDefinitionSpellBase((handleId, alias) -> new CAbilitySummonPhoenix(handleId, alias))); // ----Orc---- + + // Far Seer this.codeToAbilityTypeDefinition.put(War3ID.fromString("AOsf"), new CAbilityTypeDefinitionSpellBase((handleId, alias) -> new CAbilityFeralSpirit(handleId, alias))); this.codeToAbilityTypeDefinition.put(War3ID.fromString("AOcl"), @@ -146,6 +157,26 @@ private void registerCodes() { this.codeToAbilityTypeDefinition.put(War3ID.fromString("AOws"), new CAbilityTypeDefinitionSpellBase((handleId, alias) -> new CAbilityWarStomp(handleId, alias))); + this.codeToAbilityTypeDefinition.put(War3ID.fromString("AOsh"), + new CAbilityTypeDefinitionSpellBase((handleId, alias) -> new CAbilityShockWave(handleId, alias))); + + // Shadow Spirit + this.codeToAbilityTypeDefinition.put(War3ID.fromString("AOwd"), + new CAbilityTypeDefinitionSpellBase((handleId, alias) -> new CAbilitySerpentWard(handleId, alias))); + + this.codeToAbilityTypeDefinition.put(War3ID.fromString("AOhx"), + new CAbilityTypeDefinitionSpellBase((handleId, alias) -> new CAbilityHex(handleId, alias))); + + this.codeToAbilityTypeDefinition.put(War3ID.fromString("AOhw"), + new CAbilityTypeDefinitionSpellBase((handleId, alias) -> new CAbilityHealingWave(handleId, alias))); + + //Blade Master + this.codeToAbilityTypeDefinition.put(War3ID.fromString("AOww"), + new CAbilityTypeDefinitionSpellBase((handleId, alias) -> new CAbilityWhirlWind(handleId, alias))); + // Blademaster + this.codeToAbilityTypeDefinition.put(War3ID.fromString("AOmi"), + new CAbilityTypeDefinitionSpellBase((handleId, alias) -> new CAbilityMirrorImage(handleId, alias))); + // Burrow: this.codeToAbilityTypeDefinition.put(War3ID.fromString("Abun"), new CAbilityTypeDefinitionCargoHoldBurrow()); this.codeToAbilityTypeDefinition.put(War3ID.fromString("Astd"), new CAbilityTypeDefinitionStandDown()); @@ -275,8 +306,7 @@ private void registerCodes() { this.codeToAbilityTypeDefinition.put(War3ID.fromString("ARal"), new CAbilityTypeDefinitionRally()); this.codeToAbilityTypeDefinition.put(War3ID.fromString("Awrp"), new CAbilityTypeDefinitionSpellBase((handleId, alias) -> new CAbilityWayGate(handleId, alias))); - this.codeToAbilityTypeDefinition.put(War3ID.fromString("AOww"), - new CAbilityTypeDefinitionSpellBase((handleId, alias) -> new CAbilityWhirlWind(handleId, alias))); + System.err.println("========================================================================"); System.err.println("Starting to load ability builder"); From da953bb6718e44c8a9d22d9e0acb4c6c108fddf7 Mon Sep 17 00:00:00 2001 From: Bachir Zerourou Date: Mon, 20 Apr 2026 09:10:14 +0200 Subject: [PATCH 2/8] fix serpent lists --- .../orc/blademaster/CAbilityBladeStorm.java | 4 + .../blademaster/CAbilityCriticalStrike.java | 4 + .../orc/shadowhunter/CAbilityBigVoodoo.java | 4 + .../orc/shadowhunter/CAbilitySerpentWard.java | 265 ++++++------------ 4 files changed, 90 insertions(+), 187 deletions(-) create mode 100644 core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/blademaster/CAbilityBladeStorm.java create mode 100644 core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/blademaster/CAbilityCriticalStrike.java create mode 100644 core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/shadowhunter/CAbilityBigVoodoo.java diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/blademaster/CAbilityBladeStorm.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/blademaster/CAbilityBladeStorm.java new file mode 100644 index 000000000..041277b9e --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/blademaster/CAbilityBladeStorm.java @@ -0,0 +1,4 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.orc.blademaster; + +public class CAbilityBladeStorm { +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/blademaster/CAbilityCriticalStrike.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/blademaster/CAbilityCriticalStrike.java new file mode 100644 index 000000000..7ba55d5b1 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/blademaster/CAbilityCriticalStrike.java @@ -0,0 +1,4 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.orc.blademaster; + +public class CAbilityCriticalStrike { +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/shadowhunter/CAbilityBigVoodoo.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/shadowhunter/CAbilityBigVoodoo.java new file mode 100644 index 000000000..f226e1125 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/shadowhunter/CAbilityBigVoodoo.java @@ -0,0 +1,4 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.orc.shadowhunter; + +public class CAbilityBigVoodoo { +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/shadowhunter/CAbilitySerpentWard.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/shadowhunter/CAbilitySerpentWard.java index 0c941ec36..8f719a004 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/shadowhunter/CAbilitySerpentWard.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/shadowhunter/CAbilitySerpentWard.java @@ -17,238 +17,129 @@ import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.OrderIds; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CEffectType; -/** - * Capacité "Serpent Ward" du Shadow Hunter (Orcs). - * - *

Le Shadow Hunter pose un serpent gardien à un point cible. - * Contrairement aux loups (FeralSpirit) et à l'élémentaire d'eau - * (SummonWaterElemental) qui sont des sorts sans cible, le Serpent Ward - * cible un point précis sur la carte.

- * - *

Contraintes :

- *
    - *
  • Cible un point (hérite de {@link CAbilityPointTargetSpellBase}).
  • - *
  • Le nombre de wards actifs simultanément est limité par niveau - * (DataA dans le SLK).
  • - *
  • Chaque nouveau lancer supprime les anciens wards du même lanceur - * si la limite est atteinte (comportement WC3 : le plus ancien est - * remplacé).
  • - *
  • La durée de vie du ward est gérée via {@link CBuffTimedLife}.
  • - *
- */ public class CAbilitySerpentWard extends CAbilityPointTargetSpellBase { private War3ID summonUnitId; - private int maxWardCount; - private War3ID buffId; - private float duration; - + // BUG CORRIGÉ : DATA_A retournait 1 (ce sont les dégâts/seconde du ward). + // La limite de wards simultanés est stockée dans DATA_B en WC3 standard. + // Niveau 1 = 3, Niveau 2 = 6, Niveau 3 = 10. + private int maxWardCount; - private final List lastSummonUnits = new ArrayList<>(); + private War3ID buffId; + private float duration; - // ----------------------------------------------------------------------- - // Constructeur - // ----------------------------------------------------------------------- + // Liste des wards actifs posés par CE lanceur + private final List activeWards = new ArrayList<>(); public CAbilitySerpentWard(final int handleId, final War3ID alias) { super(handleId, alias); - System.out.println("CAbilitySerpentWard created: " + alias); } - // ----------------------------------------------------------------------- - // Surcharges obligatoires - // ----------------------------------------------------------------------- - - /** - * Remplit les données depuis les fichiers SLK pour le niveau spécifié. - * Appelé par le moteur lors de l'initialisation ou du level-up. - * - *

Champs lus :

- *
    - *
  • {@link AbilityFields#UNIT_ID} + level : rawcode de l'unité ward.
  • - *
  • {@link AbilityFields#DATA_A} + level : nombre max de wards.
  • - *
  • buffId via {@link AbstractCAbilityTypeDefinition#getBuffId}.
  • - *
- * - * @param worldEditorAbility objet contenant les données SLK de la capacité - * @param level niveau courant de la capacité (commence à 1) - */ + // ------------------------------------------------------------------------- + // Données SLK — appelé à l'init ET à chaque level-up + // ------------------------------------------------------------------------- + @Override public void populateData(final GameObject worldEditorAbility, final int level) { - final String unitTypeRaw = - worldEditorAbility.getFieldAsString(AbilityFields.UNIT_ID + level, 0); + final String unitTypeRaw = worldEditorAbility.getFieldAsString(AbilityFields.UNIT_ID + level, 0); this.summonUnitId = (unitTypeRaw.length() == 4) ? War3ID.fromString(unitTypeRaw) : War3ID.NONE; - this.maxWardCount = - worldEditorAbility.getFieldAsInteger(AbilityFields.DATA_A + level, 0); - this.buffId = - AbstractCAbilityTypeDefinition.getBuffId(worldEditorAbility, level); - this.duration = worldEditorAbility.getFieldAsFloat(AbilityFields.DURATION + level, 0); - } + // DATA_B = nombre maximum de wards actifs simultanément (3 / 6 / 10) + this.maxWardCount = worldEditorAbility.getFieldAsInteger(AbilityFields.DATA_B + level, 0); + this.buffId = AbstractCAbilityTypeDefinition.getBuffId(worldEditorAbility, level); + this.duration = worldEditorAbility.getFieldAsFloat(AbilityFields.DURATION + level, 0); + } @Override public int getBaseOrderId() { return OrderIds.ward; } - /** - * Effectue l'effet du sort : pose un Serpent Ward au point cible. - * - *

Étapes :

- *
    - *
  1. Récupération du point cible.
  2. - *
  3. Si le nombre max de wards est atteint, tue le plus ancien.
  4. - *
  5. Crée l'unité ward au point cible.
  6. - *
  7. Marque l'unité comme invoquée ({@link CUnitClassification#SUMMONED}).
  8. - *
  9. Ajoute un buff de durée de vie ({@link CBuffTimedLife}).
  10. - *
  11. Crée l'effet visuel d'apparition.
  12. - *
  13. Enregistre le ward dans la liste des wards actifs.
  14. - *
- * - * @param simulation la simulation de jeu - * @param unit le Shadow Hunter qui lance le sort - * @param target le point cible sur la carte - * @return {@code false} (le sort n'est pas un sort à canal continu) - */ + // ------------------------------------------------------------------------- + // Effet du sort + // ------------------------------------------------------------------------- + @Override public boolean doEffect( final CSimulation simulation, - final CUnit unit, + final CUnit caster, final AbilityTarget target) { - // ------------------------------------------------------------------- - // Récupération des coordonnées du point cible - // ------------------------------------------------------------------- final AbilityPointTarget pointTarget = (AbilityPointTarget) target; - final float x = pointTarget.getX(); - final float y = pointTarget.getY(); - final float facing = unit.getFacing(); - - // ------------------------------------------------------------------- - // Gestion de la limite de wards actifs - // Nettoyer les wards morts de la liste, puis tuer le plus ancien - // si on atteint la limite (comportement WC3 standard) - // ------------------------------------------------------------------- - this.lastSummonUnits.removeIf(CUnit::isDead); - - if (this.lastSummonUnits.size() >= getMaxWardCount()+2) { - // Tuer le ward le plus ancien (indice 0 = premier ajouté) - final CUnit oldestWard = this.lastSummonUnits.remove(0); - if (!oldestWard.isDead()) { - oldestWard.kill(simulation); + final float x = pointTarget.getX(); + final float y = pointTarget.getY(); + final float facing = caster.getFacing(); + + // -------------------------------------------------------------------- + // 1. Retirer de la liste les wards déjà morts naturellement + // -------------------------------------------------------------------- + this.activeWards.removeIf(CUnit::isDead); + + // -------------------------------------------------------------------- + // 2. BUG CORRIGÉ : seuil correct pour la limite de wards simultanés. + // + // Ancien code : size() >= maxWardCount + 2 ← trop permissif / faux + // Nouveau code : size() >= maxWardCount ← on tue le plus ancien + // si la limite est atteinte + // + // Flux correct : + // - avant d'ajouter le nouveau ward, la liste a au plus + // (maxWardCount - 1) éléments après le retrait du plus ancien + // - le nouveau ward est ajouté → la liste reste à maxWardCount + // -------------------------------------------------------------------- + if (this.activeWards.size() >= this.maxWardCount) { + final CUnit oldest = this.activeWards.remove(0); + if (!oldest.isDead()) { + oldest.kill(simulation); } } - // ------------------------------------------------------------------- - // Création du ward au point cible - // ------------------------------------------------------------------- - final CUnit summonedUnit = simulation.createUnitSimple( + // -------------------------------------------------------------------- + // 3. Création du ward au point cible + // -------------------------------------------------------------------- + final CUnit ward = simulation.createUnitSimple( this.summonUnitId, - unit.getPlayerIndex(), - x, - y, + caster.getPlayerIndex(), + x, y, facing ); - // Marquer comme unité invoquée (affecte le comportement en jeu) - summonedUnit.addClassification(CUnitClassification.SUMMONED); - - // Ajouter le buff de durée de vie (true = expiration silencieuse) - summonedUnit.add( - simulation, - new CBuffTimedLife( - simulation.getHandleIdAllocator().createId(), - this.buffId, - this.duration, - true - ) - ); + ward.addClassification(CUnitClassification.SUMMONED); - // Effet visuel d'apparition sur le ward - simulation.createTemporarySpellEffectOnUnit( - summonedUnit, - getAlias(), - CEffectType.SPECIAL - ); + ward.add(simulation, new CBuffTimedLife( + simulation.getHandleIdAllocator().createId(), + this.buffId, + this.duration, + true + )); - // Enregistrer le ward dans la liste des wards actifs - this.lastSummonUnits.add(summonedUnit); + simulation.createTemporarySpellEffectOnUnit(ward, getAlias(), CEffectType.SPECIAL); - return false; - } - - // ----------------------------------------------------------------------- - // Getters - // ----------------------------------------------------------------------- - - /** - * Retourne le rawcode de l'unité Serpent Ward invoquée. - * - * @return rawcode de l'unité - */ - public War3ID getSummonUnitId() { - return this.summonUnitId; - } - - /** - * Retourne le nombre maximum de wards simultanés au niveau actuel. - * - * @return nombre maximum de wards - */ - public int getMaxWardCount() { - return this.maxWardCount; - } + // -------------------------------------------------------------------- + // 4. Enregistrement du ward dans la liste des wards actifs + // -------------------------------------------------------------------- + this.activeWards.add(ward); - /** - * Retourne l'identifiant du buff de durée de vie. - * - * @return identifiant du buff - */ - public War3ID getBuffId() { - return this.buffId; + return false; } - /** - * Retourne la liste non modifiable des wards actifs. - * - * @return liste des wards actifs - */ - public List getLastSummonUnits() { - return java.util.Collections.unmodifiableList(this.lastSummonUnits); - } + // ------------------------------------------------------------------------- + // Getters / Setters + // ------------------------------------------------------------------------- - // ----------------------------------------------------------------------- - // Setters (pour le World Editor / outils) - // ----------------------------------------------------------------------- - - /** - * Définit le rawcode de l'unité à invoquer. - * - * @param summonUnitId rawcode de l'unité - */ - public void setSummonUnitId(final War3ID summonUnitId) { - this.summonUnitId = summonUnitId; - } + public War3ID getSummonUnitId() { return this.summonUnitId; } + public int getMaxWardCount() { return this.maxWardCount; } + public War3ID getBuffId() { return this.buffId; } - /** - * Définit le nombre maximum de wards simultanés. - * - * @param maxWardCount nombre maximum - */ - public void setMaxWardCount(final int maxWardCount) { - this.maxWardCount = maxWardCount; + public List getActiveWards() { + return java.util.Collections.unmodifiableList(this.activeWards); } - /** - * Définit l'identifiant du buff de durée de vie. - * - * @param buffId identifiant du buff - */ - public void setBuffId(final War3ID buffId) { - this.buffId = buffId; - } + public void setSummonUnitId(final War3ID summonUnitId) { this.summonUnitId = summonUnitId; } + public void setMaxWardCount(final int maxWardCount) { this.maxWardCount = maxWardCount; } + public void setBuffId(final War3ID buffId) { this.buffId = buffId; } } \ No newline at end of file From a3e15b68f7aeceb008b13b2cfbe11734f0760dcf Mon Sep 17 00:00:00 2001 From: Bachir Zerourou Date: Mon, 20 Apr 2026 09:13:17 +0200 Subject: [PATCH 3/8] add Big Voodoo capacity --- .../orc/shadowhunter/CAbilityBigVoodoo.java | 160 +++++++++++++++++- 1 file changed, 158 insertions(+), 2 deletions(-) diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/shadowhunter/CAbilityBigVoodoo.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/shadowhunter/CAbilityBigVoodoo.java index f226e1125..15810cf6f 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/shadowhunter/CAbilityBigVoodoo.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/shadowhunter/CAbilityBigVoodoo.java @@ -1,4 +1,160 @@ package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.orc.shadowhunter; -public class CAbilityBigVoodoo { -} +import com.etheller.warsmash.units.GameObject; +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.util.WarsmashConstants; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.CAbilityNoTargetSpellBase; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.definitions.impl.AbilityFields; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.projectile.CEffect; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.OrderIds; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CEffectType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.unit.StateModBuff; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.unit.StateModBuffType; + +import java.util.ArrayList; +import java.util.List; + +public class CAbilityBigVoodoo extends CAbilityNoTargetSpellBase { + + private float areaOfEffect; + private float duration; + + public CAbilityBigVoodoo(final int handleId, final War3ID alias) { + super(handleId, alias); + } + + @Override + public void populateData(final GameObject worldEditorAbility, final int level) { + this.areaOfEffect = worldEditorAbility.getFieldAsFloat(AbilityFields.AREA_OF_EFFECT + level, 0); + this.duration = worldEditorAbility.getFieldAsFloat(AbilityFields.DURATION + level, 0); + } + + @Override + public int getBaseOrderId() { + return OrderIds.voodoo; + } + + @Override + public boolean doEffect(final CSimulation simulation, final CUnit caster, final AbilityTarget target) { + + // -------------------------------------------------------------------- + // 1. Collecte de toutes les unités alliées dans la zone (caster inclus + // car il est au centre de l'enumUnitsInRange) + // -------------------------------------------------------------------- + final List affectedUnits = new ArrayList<>(); + + simulation.getWorldCollision().enumUnitsInRange( + caster.getX(), caster.getY(), this.areaOfEffect, + enumUnit -> { + if (!enumUnit.isDead() + && enumUnit.isUnitAlly(simulation.getPlayer(caster.getPlayerIndex()))) { + affectedUnits.add(enumUnit); + } + return false; + }); + + // -------------------------------------------------------------------- + // 2. Rendre toutes les unités collectées invulnérables + // -------------------------------------------------------------------- + for (final CUnit ally : affectedUnits) { + ally.setInvulnerable(true); + } + + // -------------------------------------------------------------------- + // 3. Immobiliser le caster pendant le rituel + // - STUN : bloque les ordres / mouvements (CBehaviorStun) + // - DISABLE_SPELLS : empêche de lancer un autre sort + // On conserve les références pour les retirer proprement ensuite. + // -------------------------------------------------------------------- + final StateModBuff stunBuff = new StateModBuff(StateModBuffType.STUN, getHandleId()); + final StateModBuff disableSpellBuff = new StateModBuff(StateModBuffType.DISABLE_SPELLS, getHandleId()); + + caster.addStateModBuff(stunBuff); + caster.addStateModBuff(disableSpellBuff); + caster.computeUnitState(simulation, StateModBuffType.STUN); + caster.computeUnitState(simulation, StateModBuffType.DISABLE_SPELLS); + + // -------------------------------------------------------------------- + // 4. Effet visuel sur le caster + // -------------------------------------------------------------------- + simulation.createTemporarySpellEffectOnUnit(caster, getAlias(), CEffectType.CASTER); + + // -------------------------------------------------------------------- + // 5. Enregistrement de l'effet temporisé pour nettoyer à la fin + // -------------------------------------------------------------------- + final int endTick = simulation.getGameTurnTick() + + (int) StrictMath.ceil(this.duration / WarsmashConstants.SIMULATION_STEP_TIME); + + simulation.registerEffect(new CEffectBigBadVoodoo( + caster, affectedUnits, stunBuff, disableSpellBuff, endTick)); + + return false; + } + + // ======================================================================== + // Effet interne : gère la durée du rituel et le nettoyage final + // ======================================================================== + private static final class CEffectBigBadVoodoo implements CEffect { + + private final CUnit caster; + private final List affectedUnits; + private final StateModBuff stunBuff; + private final StateModBuff disableSpellBuff; + private final int endTick; + private boolean cleaned; + + public CEffectBigBadVoodoo( + final CUnit caster, + final List affectedUnits, + final StateModBuff stunBuff, + final StateModBuff disableSpellBuff, + final int endTick) { + this.caster = caster; + this.affectedUnits = affectedUnits; + this.stunBuff = stunBuff; + this.disableSpellBuff = disableSpellBuff; + this.endTick = endTick; + this.cleaned = false; + } + + @Override + public boolean update(final CSimulation game) { + if (this.cleaned) { + return true; + } + + // Fin normale (durée écoulée) ou interruption par la mort du caster + final boolean expired = game.getGameTurnTick() >= this.endTick; + final boolean casterDead = this.caster.isDead(); + + if (!expired && !casterDead) { + return false; + } + + removeEffects(game); + return true; + } + + private void removeEffects(final CSimulation game) { + this.cleaned = true; + + // Retrait de l'invulnérabilité sur toutes les unités affectées + for (final CUnit ally : this.affectedUnits) { + if (!ally.isDead()) { + ally.setInvulnerable(false); + } + } + + // Retrait du stun et du disable-spells sur le caster + if (!this.caster.isDead()) { + this.caster.removeStateModBuff(this.stunBuff); + this.caster.removeStateModBuff(this.disableSpellBuff); + this.caster.computeUnitState(game, StateModBuffType.STUN); + this.caster.computeUnitState(game, StateModBuffType.DISABLE_SPELLS); + } + } + } +} \ No newline at end of file From 251c871b611db5a89fee59e59c737bb83127d17b Mon Sep 17 00:00:00 2001 From: Bachir Zerourou Date: Mon, 20 Apr 2026 09:24:48 +0200 Subject: [PATCH 4/8] fix clone color and death --- .../orc/blademaster/CAbilityMirrorImage.java | 99 +++++++++++-------- 1 file changed, 60 insertions(+), 39 deletions(-) diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/blademaster/CAbilityMirrorImage.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/blademaster/CAbilityMirrorImage.java index 625db4912..b38efd7ed 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/blademaster/CAbilityMirrorImage.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/blademaster/CAbilityMirrorImage.java @@ -2,16 +2,16 @@ import com.etheller.warsmash.units.GameObject; import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.RenderUnit; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnitClassification; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.CAbilityNoTargetSpellBase; -import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.util.CBuffTimedLife; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.util.CBuffTimed; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.definitions.impl.AbilityFields; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.definitions.impl.AbstractCAbilityTypeDefinition; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.OrderIds; -import com.etheller.warsmash.viewer5.handlers.w3x.simulation.timers.CTimer; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CEffectType; import java.util.ArrayList; @@ -19,9 +19,9 @@ public class CAbilityMirrorImage extends CAbilityNoTargetSpellBase { - private int numberOfImages; - private float damagePercent; - private float duration; + private int numberOfImages; + private float damagePercent; + private float duration; private War3ID buffId; private final List activeImages = new ArrayList<>(); @@ -33,9 +33,9 @@ public CAbilityMirrorImage(final int handleId, final War3ID alias) { @Override public void populateData(final GameObject worldEditorAbility, final int level) { this.numberOfImages = worldEditorAbility.getFieldAsInteger(AbilityFields.DATA_A + level, 0); - this.damagePercent = worldEditorAbility.getFieldAsFloat(AbilityFields.DATA_B + level, 0) / 100f; - this.duration = worldEditorAbility.getFieldAsFloat(AbilityFields.DURATION + level, 0); - this.buffId = AbstractCAbilityTypeDefinition.getBuffId(worldEditorAbility, level); + this.damagePercent = worldEditorAbility.getFieldAsFloat(AbilityFields.DATA_B + level, 0) / 100f; + this.duration = worldEditorAbility.getFieldAsFloat(AbilityFields.DURATION + level, 0); + this.buffId = AbstractCAbilityTypeDefinition.getBuffId(worldEditorAbility, level); } @Override @@ -45,15 +45,15 @@ public int getBaseOrderId() { @Override public boolean doEffect(final CSimulation simulation, final CUnit caster, final AbilityTarget target) { - activeImages.removeIf(CUnit::isDead); + this.activeImages.removeIf(CUnit::isDead); final float facing = caster.getFacing(); final float offset = 120f; simulation.createTemporarySpellEffectOnUnit(caster, getAlias(), CEffectType.CASTER); - for (int i = 0; i < numberOfImages; i++) { - final float angle = (float) (i * (2 * Math.PI / numberOfImages)); + for (int i = 0; i < this.numberOfImages; i++) { + final float angle = (float) (i * (2.0 * Math.PI / this.numberOfImages)); final float x = caster.getX() + (float) Math.cos(angle) * offset; final float y = caster.getY() + (float) Math.sin(angle) * offset; @@ -64,46 +64,67 @@ public boolean doEffect(final CSimulation simulation, final CUnit caster, final facing ); - // Classifications importantes pour les illusions image.addClassification(CUnitClassification.SUMMONED); - image.addClassification(CUnitClassification.ILLUSION); // Si cette classification n'existe pas, on la supprimera - - // Copie état de base image.setLife(simulation, caster.getLife()); image.setMana(caster.getMana()); - // === Effet visuel bleu transparent (Mirror Image style) === - // Méthode alternative plus sûre dans Warsmash - simulation.unitUpdatedType(image, image.getTypeId()); // Force refresh du modèle - if (image.getUnitAnimationListener() != null) { - image.getUnitAnimationListener().playAnimation(false, - com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.PrimaryTag.STAND, - com.etheller.warsmash.viewer5.handlers.w3x.SequenceUtils.EMPTY, 0, true); - } - - // Durée de vie - image.add(simulation, new CBuffTimedLife( + // Teinte bleue semi-transparente (même rendu que les unités éthérées) + simulation.changeUnitVertexColor(image, RenderUnit.ETHEREAL); + + // ------------------------------------------------------------------ + // Disparition sans animation ni explosion : + // + // Cas 1 – HP tombe à 0 (image tuée par des dégâts) + // setExplodesOnDeath(true) → dans CUnit.kill() : setHidden + removeUnit + // explodesOnDeathBuffId = War3ID.NONE → createDeathExplodeEffect ne trouve + // aucun art associé à NONE → aucun visuel, le clone disparaît proprement. + // + // Cas 2 – Buff expire (durée écoulée) + // CBuffMirrorImageTimed.onBuffRemove → setHidden + removeUnit directement, + // sans passer par kill() qui jouerait une animation de mort. + // ------------------------------------------------------------------ + image.setExplodesOnDeath(true); + image.setExplodesOnDeathBuffId(War3ID.NONE); + + image.add(simulation, new CBuffMirrorImageTimed( simulation.getHandleIdAllocator().createId(), this.buffId, - this.duration, - false + this.duration )); simulation.createTemporarySpellEffectOnUnit(image, getAlias(), CEffectType.SPECIAL); - activeImages.add(image); + this.activeImages.add(image); } - // Invulnérabilité temporaire du vrai Blademaster - caster.setInvulnerable(true); - final CTimer invulnTimer = new CTimer() { - @Override - public void onFire(final CSimulation game) { - caster.setInvulnerable(false); - } - }; - simulation.registerTimer(invulnTimer); - return false; } + + // ========================================================================= + // Buff interne : gère l'expiration propre du clone + // ========================================================================= + private static final class CBuffMirrorImageTimed extends CBuffTimed { + + public CBuffMirrorImageTimed(final int handleId, final War3ID alias, final float duration) { + super(handleId, alias, alias, duration); + } + + @Override + protected void onBuffAdd(final CSimulation game, final CUnit unit) { + // Rien à faire ici : explode + War3ID.NONE déjà posés dans doEffect + } + + @Override + protected void onBuffRemove(final CSimulation game, final CUnit unit) { + // Expiration de la durée : cacher et retirer directement, + // sans appeler unit.kill() qui jouerait une animation de mort. + unit.setHidden(true); + game.removeUnit(unit); + } + + @Override + public boolean isTimedLifeBar() { + return false; + } + } } \ No newline at end of file From 09c2af970edbfa15773d9280bb8163dda3a16f8f Mon Sep 17 00:00:00 2001 From: Bachir Zerourou Date: Mon, 20 Apr 2026 09:25:29 +0200 Subject: [PATCH 5/8] add Critical Stike capacity --- .../blademaster/CAbilityCriticalStrike.java | 153 +++++++++++++++++- 1 file changed, 151 insertions(+), 2 deletions(-) diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/blademaster/CAbilityCriticalStrike.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/blademaster/CAbilityCriticalStrike.java index 7ba55d5b1..5bcc327f9 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/blademaster/CAbilityCriticalStrike.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/blademaster/CAbilityCriticalStrike.java @@ -1,4 +1,153 @@ package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.orc.blademaster; -public class CAbilityCriticalStrike { -} +import com.etheller.warsmash.units.GameObject; +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.CAbilityNoTargetSpellBase; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTargetVisitor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.definitions.impl.AbilityFields; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CAttackType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.listeners.CUnitAttackPostDamageListener; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CDamageType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CEffectType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CWeaponSoundTypeJass; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityTargetCheckReceiver; + +public class CAbilityCriticalStrike extends CAbilityNoTargetSpellBase { + + // Probabilité de déclencher un coup critique (0.0 à 1.0) + private float chance; + // Multiplicateur de dégâts appliqué lors d'un coup critique (ex : 2.0 = ×2) + private float damageMultiplier; + + // Référence au listener pour pouvoir le retirer proprement dans onRemove + private CriticalStrikeListener criticalStrikeListener; + + public CAbilityCriticalStrike(final int handleId, final War3ID alias) { + super(handleId, alias); + } + + // ------------------------------------------------------------------------- + // Données SLK + // ------------------------------------------------------------------------- + + @Override + public void populateData(final GameObject worldEditorAbility, final int level) { + // DataA = chance (décimal, ex : 0.15 pour 15 %) + this.chance = worldEditorAbility.getFieldAsFloat(AbilityFields.DATA_A + level, 0); + // DataB = multiplicateur de dégâts (ex : 2.0 pour ×2) + this.damageMultiplier = worldEditorAbility.getFieldAsFloat(AbilityFields.DATA_B + level, 0); + } + + // ------------------------------------------------------------------------- + // Cycle de vie + // ------------------------------------------------------------------------- + + @Override + public void onAdd(final CSimulation game, final CUnit unit) { + // Passive : on ne crée pas de CBehaviorNoTargetSpellBase (super.onAdd non appelé) + this.criticalStrikeListener = new CriticalStrikeListener( + this.chance, + this.damageMultiplier, + getAlias() + ); + unit.addPostDamageListener(this.criticalStrikeListener); + } + + @Override + public void onRemove(final CSimulation game, final CUnit unit) { + if (this.criticalStrikeListener != null) { + unit.removePostDamageListener(this.criticalStrikeListener); + this.criticalStrikeListener = null; + } + } + + // ------------------------------------------------------------------------- + // Passive : aucune activation manuelle possible + // ------------------------------------------------------------------------- + + @Override + public int getBaseOrderId() { + // À remplacer par OrderIds.criticalbash si la constante existe + return 852532; + } + + @Override + protected void innerCheckCanTargetNoTarget(final CSimulation game, final CUnit unit, final int orderId, + final AbilityTargetCheckReceiver receiver) { + receiver.orderIdNotAccepted(); + } + + @Override + public boolean doEffect(final CSimulation simulation, final CUnit unit, final AbilityTarget target) { + return false; + } + + // ========================================================================= + // Listener interne — cœur de la mécanique de coup critique + // ========================================================================= + + private static final class CriticalStrikeListener implements CUnitAttackPostDamageListener { + + private final float chance; + private final float damageMultiplier; + private final War3ID alias; + + // Drapeau anti-récursion : empêche le dégât bonus d'un crit + // de déclencher un second crit en chaîne + private boolean applyingCrit; + + public CriticalStrikeListener(final float chance, final float damageMultiplier, final War3ID alias) { + this.chance = chance; + this.damageMultiplier = damageMultiplier; + this.alias = alias; + this.applyingCrit = false; + } + + @Override + public void onHit( + final CSimulation simulation, + final CUnit source, + final AbilityTarget target, + final float damage) { + + // Anti-récursion : on n'applique pas de crit sur le dégât bonus lui-même + if (this.applyingCrit) { + return; + } + + // Récupération de l'unité cible depuis l'AbilityTarget + final CUnit targetUnit = target.visit(AbilityTargetVisitor.UNIT); + if (targetUnit == null || targetUnit.isDead()) { + return; + } + + // Tirage aléatoire déterministe (seed gérée par la simulation) + if (simulation.getSeededRandom().nextFloat() < this.chance) { + this.applyingCrit = true; + + // Dégâts bonus = (multiplicateur - 1) × dégâts de base + // Ex : multiplicateur 2.0 avec 100 dégâts → 100 bonus → 200 total + final float bonusCritDamage = damage * (this.damageMultiplier - 1.0f); + + targetUnit.damage( + simulation, + source, + false, // pas une 2e attaque standard + false, // non ranged + CAttackType.NORMAL, + CDamageType.NORMAL, + CWeaponSoundTypeJass.WHOKNOWS.name(), + bonusCritDamage + ); + + // Effet visuel sur la cible au moment du coup critique + simulation.createTemporarySpellEffectOnUnit(targetUnit, this.alias, CEffectType.TARGET); + + this.applyingCrit = false; + } + } + } +} \ No newline at end of file From 3a24941c2281436aae2931bbd3bc0b2fb1dd040b Mon Sep 17 00:00:00 2001 From: Bachir Zerourou Date: Mon, 20 Apr 2026 09:26:17 +0200 Subject: [PATCH 6/8] add BladeStorm capacity --- .../orc/blademaster/CAbilityBladeStorm.java | 212 +++++++++++++++++- 1 file changed, 210 insertions(+), 2 deletions(-) diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/blademaster/CAbilityBladeStorm.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/blademaster/CAbilityBladeStorm.java index 041277b9e..961cbd630 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/blademaster/CAbilityBladeStorm.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/blademaster/CAbilityBladeStorm.java @@ -1,4 +1,212 @@ package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.orc.blademaster; -public class CAbilityBladeStorm { -} +import com.etheller.warsmash.units.GameObject; +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.util.WarsmashConstants; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.CAbilityNoTargetSpellBase; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.definitions.impl.AbilityFields; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CAttackType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.projectile.CEffect; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.OrderIds; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CDamageType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CEffectType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CWeaponSoundTypeJass; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.unit.StateModBuff; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.unit.StateModBuffType; + +public class CAbilityBladeStorm extends CAbilityNoTargetSpellBase { + + // Dégâts infligés par seconde aux ennemis dans la zone + private float damagePerSecond; + // Rayon de la tornade + private float areaOfEffect; + // Durée totale du tourbillon en secondes + private float duration; + + public CAbilityBladeStorm(final int handleId, final War3ID alias) { + super(handleId, alias); + } + + // ------------------------------------------------------------------------- + // Données SLK + // ------------------------------------------------------------------------- + + @Override + public void populateData(final GameObject worldEditorAbility, final int level) { + this.damagePerSecond = worldEditorAbility.getFieldAsFloat(AbilityFields.DATA_A + level, 0); + this.areaOfEffect = worldEditorAbility.getFieldAsFloat(AbilityFields.AREA_OF_EFFECT + level, 0); + this.duration = worldEditorAbility.getFieldAsFloat(AbilityFields.DURATION + level, 0); + } + + @Override + public int getBaseOrderId() { + return OrderIds.bladestorm; + } + + // ------------------------------------------------------------------------- + // Déclenchement du sort + // ------------------------------------------------------------------------- + + @Override + public boolean doEffect(final CSimulation simulation, final CUnit caster, final AbilityTarget target) { + + // -------------------------------------------------------------------- + // 1. Rendre le Blademaster invulnérable pendant le tourbillon + // -------------------------------------------------------------------- + caster.setInvulnerable(true); + + // -------------------------------------------------------------------- + // 2. Immobiliser le Blademaster : STUN pour bloquer les ordres, + // DISABLE_SPELLS pour empêcher tout autre sort pendant le canal. + // On conserve les références pour le retrait propre à la fin. + // -------------------------------------------------------------------- + final StateModBuff stunBuff = new StateModBuff(StateModBuffType.STUN, getHandleId()); + final StateModBuff disableSpellBuff = new StateModBuff(StateModBuffType.DISABLE_SPELLS, getHandleId()); + + caster.addStateModBuff(stunBuff); + caster.addStateModBuff(disableSpellBuff); + caster.computeUnitState(simulation, StateModBuffType.STUN); + caster.computeUnitState(simulation, StateModBuffType.DISABLE_SPELLS); + + // -------------------------------------------------------------------- + // 3. Effet visuel de tourbillon sur le caster + // -------------------------------------------------------------------- + simulation.createTemporarySpellEffectOnUnit(caster, getAlias(), CEffectType.CASTER); + + // -------------------------------------------------------------------- + // 4. Calcul du tick de fin et des dégâts par tick de simulation + // -------------------------------------------------------------------- + final int endTick = simulation.getGameTurnTick() + + (int) StrictMath.ceil(this.duration / WarsmashConstants.SIMULATION_STEP_TIME); + + final float damagePerTick = this.damagePerSecond * WarsmashConstants.SIMULATION_STEP_TIME; + + // -------------------------------------------------------------------- + // 5. Enregistrement de l'effet temporisé qui gère les ticks de dégâts + // -------------------------------------------------------------------- + simulation.registerEffect(new CEffectBladestorm( + caster, + endTick, + damagePerTick, + this.areaOfEffect, + getTargetsAllowed(), + stunBuff, + disableSpellBuff, + getAlias() + )); + + return false; + } + + // ========================================================================= + // Effet interne : gère chaque tick de dégâts et le nettoyage final + // ========================================================================= + + private static final class CEffectBladestorm implements CEffect { + + private final CUnit caster; + private final int endTick; + private final float damagePerTick; + private final float areaOfEffect; + private final java.util.EnumSet + targetsAllowed; + private final StateModBuff stunBuff; + private final StateModBuff disableSpellBuff; + private final War3ID alias; + + // Drapeau pour s'assurer qu'on ne nettoie qu'une seule fois + private boolean cleaned; + + public CEffectBladestorm( + final CUnit caster, + final int endTick, + final float damagePerTick, + final float areaOfEffect, + final java.util.EnumSet + targetsAllowed, + final StateModBuff stunBuff, + final StateModBuff disableSpellBuff, + final War3ID alias) { + this.caster = caster; + this.endTick = endTick; + this.damagePerTick = damagePerTick; + this.areaOfEffect = areaOfEffect; + this.targetsAllowed = targetsAllowed; + this.stunBuff = stunBuff; + this.disableSpellBuff = disableSpellBuff; + this.alias = alias; + this.cleaned = false; + } + + @Override + public boolean update(final CSimulation game) { + if (this.cleaned) { + return true; + } + + final boolean expired = game.getGameTurnTick() >= this.endTick; + final boolean casterDead = this.caster.isDead(); + + if (casterDead) { + // Le Blademaster est mort : on nettoie sans faire de dégâts + removeEffects(game); + return true; + } + + // ---------------------------------------------------------------- + // Tick de dégâts : toutes les unités ennemies vivantes dans la zone + // ---------------------------------------------------------------- + game.getWorldCollision().enumUnitsInRange( + this.caster.getX(), + this.caster.getY(), + this.areaOfEffect, + enumUnit -> { + if (!enumUnit.isDead() + && !enumUnit.isUnitAlly(game.getPlayer(this.caster.getPlayerIndex())) + && enumUnit.canBeTargetedBy(game, this.caster, this.targetsAllowed)) { + + enumUnit.damage( + game, + this.caster, + false, // pas une attaque standard + false, // non ranged + CAttackType.SPELLS, + CDamageType.UNIVERSAL, + CWeaponSoundTypeJass.WHOKNOWS.name(), + this.damagePerTick + ); + + // Petit effet visuel sur chaque cible touchée + game.createTemporarySpellEffectOnUnit(enumUnit, this.alias, CEffectType.TARGET); + } + return false; + }); + + if (expired) { + removeEffects(game); + return true; + } + + return false; + } + + // -------------------------------------------------------------------- + // Nettoyage : retire l'invulnérabilité et les états du caster + // -------------------------------------------------------------------- + private void removeEffects(final CSimulation game) { + this.cleaned = true; + + if (!this.caster.isDead()) { + this.caster.setInvulnerable(false); + + this.caster.removeStateModBuff(this.stunBuff); + this.caster.removeStateModBuff(this.disableSpellBuff); + this.caster.computeUnitState(game, StateModBuffType.STUN); + this.caster.computeUnitState(game, StateModBuffType.DISABLE_SPELLS); + } + } + } +} \ No newline at end of file From 4e164911497cf3a80fceaba2b6a84a2ec5394419 Mon Sep 17 00:00:00 2001 From: Bachir Zerourou Date: Mon, 20 Apr 2026 09:46:36 +0200 Subject: [PATCH 7/8] fix OrdersIds.java and CAbilityData.java --- .../CAbilityEnduranceAura.java | 210 ++++++++++++++++++ .../w3x/simulation/data/CAbilityData.java | 14 +- .../w3x/simulation/orders/OrderIds.java | 5 +- 3 files changed, 227 insertions(+), 2 deletions(-) create mode 100644 core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/taurenchieftain/CAbilityEnduranceAura.java diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/taurenchieftain/CAbilityEnduranceAura.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/taurenchieftain/CAbilityEnduranceAura.java new file mode 100644 index 000000000..b2beac7ab --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/taurenchieftain/CAbilityEnduranceAura.java @@ -0,0 +1,210 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.orc.taurenchieftain; + +import com.etheller.warsmash.units.GameObject; +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.CAbilityNoTargetSpellBase; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.definitions.impl.AbilityFields; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.OrderIds; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.unit.NonStackingStatBuff; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.unit.NonStackingStatBuffType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityTargetCheckReceiver; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +public class CAbilityEnduranceAura extends CAbilityNoTargetSpellBase { + + // DATA_A = bonus de vitesse de déplacement (ex : 0.10 pour +10 %) + private float movementSpeedBonus; + // DATA_B = bonus de vitesse d'attaque (ex : 0.10 pour +10 %) + private float attackSpeedBonus; + // Rayon de l'aura + private float areaOfEffect; + + // Clé de non-empilement : basée sur l'alias (rawcode WC3 de la capacité). + // Deux auras du même type provenant de héros différents ne s'empilent pas, + // seul le bonus le plus élevé s'applique (comportement WC3 standard). + private String stackingKey; + + // Unités actuellement sous l'effet de l'aura. + // Valeur : tableau [buffVitesseDéplacement, buffVitesseAttaque] pour pouvoir + // les retirer proprement quand une unité quitte la zone. + private final Map buffedUnits = new HashMap<>(); + + public CAbilityEnduranceAura(final int handleId, final War3ID alias) { + super(handleId, alias); + } + + // ------------------------------------------------------------------------- + // Données SLK — appelé à l'init ET à chaque level-up + // ------------------------------------------------------------------------- + + @Override + public void populateData(final GameObject worldEditorAbility, final int level) { + this.movementSpeedBonus = worldEditorAbility.getFieldAsFloat(AbilityFields.DATA_A + level, 0); + this.attackSpeedBonus = worldEditorAbility.getFieldAsFloat(AbilityFields.DATA_B + level, 0); + this.areaOfEffect = worldEditorAbility.getFieldAsFloat(AbilityFields.AREA_OF_EFFECT + level, 0); + } + + // ------------------------------------------------------------------------- + // Cycle de vie + // ------------------------------------------------------------------------- + + @Override + public void onAdd(final CSimulation game, final CUnit unit) { + // Passive : pas de CBehaviorNoTargetSpellBase (super.onAdd non appelé) + // Initialisation de la clé de non-empilement + this.stackingKey = getAlias().asStringValue(); + } + + @Override + public void onRemove(final CSimulation game, final CUnit unit) { + // Retrait propre de tous les buffs d'aura lors du retrait de la capacité + removeAllBuffs(game); + } + + @Override + public void onDeath(final CSimulation game, final CUnit unit) { + // Si le Tauren meurt, les buffs d'aura sont retirés immédiatement + removeAllBuffs(game); + } + + // ------------------------------------------------------------------------- + // Tick principal — appliqué chaque pas de simulation + // ------------------------------------------------------------------------- + + @Override + public void onTick(final CSimulation game, final CUnit caster) { + if (caster.isDead() || caster.isHidden()) { + return; + } + + // -------------------------------------------------------------------- + // 1. Collecte des alliés vivants actuellement dans la zone de l'aura + // -------------------------------------------------------------------- + final Set currentInRange = new HashSet<>(); + game.getWorldCollision().enumUnitsInRange( + caster.getX(), caster.getY(), this.areaOfEffect, + enumUnit -> { + if (!enumUnit.isDead() + && enumUnit.isUnitAlly(game.getPlayer(caster.getPlayerIndex()))) { + currentInRange.add(enumUnit); + } + return false; + }); + + // -------------------------------------------------------------------- + // 2. Retrait du buff pour les unités qui ont quitté la zone ou qui + // sont mortes depuis le dernier tick + // -------------------------------------------------------------------- + final Iterator> iter = + this.buffedUnits.entrySet().iterator(); + + while (iter.hasNext()) { + final Map.Entry entry = iter.next(); + final CUnit ally = entry.getKey(); + + if (!currentInRange.contains(ally) || ally.isDead()) { + removeBuffFromUnit(game, ally, entry.getValue()); + iter.remove(); + } + } + + // -------------------------------------------------------------------- + // 3. Application du buff aux nouvelles unités entrées dans la zone + // -------------------------------------------------------------------- + for (final CUnit ally : currentInRange) { + if (!this.buffedUnits.containsKey(ally)) { + final NonStackingStatBuff[] buffs = applyBuffToUnit(game, ally); + this.buffedUnits.put(ally, buffs); + } + } + } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + /** + * Applique les deux buffs d'aura (déplacement + attaque) à une unité + * et retourne les références pour pouvoir les retirer plus tard. + */ + private NonStackingStatBuff[] applyBuffToUnit(final CSimulation game, final CUnit ally) { + // Bonus de vitesse de déplacement (pourcentage) + final NonStackingStatBuff moveSpeedBuff = new NonStackingStatBuff( + NonStackingStatBuffType.MVSPDPCT, + this.stackingKey, + this.movementSpeedBonus + ); + + // Bonus de vitesse d'attaque (valeur directe, même unité que ATKSPD dans CUnit) + final NonStackingStatBuff attackSpeedBuff = new NonStackingStatBuff( + NonStackingStatBuffType.ATKSPD, + this.stackingKey, + this.attackSpeedBonus + ); + + ally.addNonStackingStatBuff(moveSpeedBuff); + ally.addNonStackingStatBuff(attackSpeedBuff); + + return new NonStackingStatBuff[] { moveSpeedBuff, attackSpeedBuff }; + } + + /** + * Retire les deux buffs d'aura d'une unité. + */ + private void removeBuffFromUnit(final CSimulation game, final CUnit ally, + final NonStackingStatBuff[] buffs) { + if (buffs == null) { + return; + } + // removeNonStackingStatBuff appelle computeDerivedFields en interne + if (!ally.isDead()) { + ally.removeNonStackingStatBuff(buffs[0]); // vitesse de déplacement + ally.removeNonStackingStatBuff(buffs[1]); // vitesse d'attaque + } + } + + /** + * Retire tous les buffs d'aura (mort du caster ou retrait de la capacité). + */ + private void removeAllBuffs(final CSimulation game) { + for (final Map.Entry entry : this.buffedUnits.entrySet()) { + removeBuffFromUnit(game, entry.getKey(), entry.getValue()); + } + this.buffedUnits.clear(); + } + + // ------------------------------------------------------------------------- + // Passive : aucune activation manuelle possible + // ------------------------------------------------------------------------- + + @Override + public int getBaseOrderId() { + return OrderIds.endurance; + } + + /** + * Passive : rejeter tous les ordres de cast manuel. + */ + @Override + protected void innerCheckCanTargetNoTarget(final CSimulation game, final CUnit unit, + final int orderId, final AbilityTargetCheckReceiver receiver) { + receiver.orderIdNotAccepted(); + } + + /** + * doEffect n'est jamais appelé (la passive n'est pas castable). + */ + @Override + public boolean doEffect(final CSimulation simulation, final CUnit unit, + final AbilityTarget target) { + return false; + } +} \ No newline at end of file diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/data/CAbilityData.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/data/CAbilityData.java index 95ce677be..42d6a3c48 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/data/CAbilityData.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/data/CAbilityData.java @@ -38,6 +38,7 @@ import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.nightelf.moonpriestess.CAbilitySummonOwlScout; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.nightelf.warden.CAbilityBlink; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.orc.blademaster.CAbilityBladeStorm; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.orc.blademaster.CAbilityMirrorImage; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.orc.shadowhunter.CAbilityHealingWave; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.orc.shadowhunter.CAbilitySerpentWard; @@ -48,6 +49,7 @@ import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.orc.farseer.CAbilityFeralSpirit; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.orc.shadowhunter.CAbilitySerpentWard; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.orc.shadowhunter.CAbilityHex ; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.orc.taurenchieftain.CAbilityEnduranceAura; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.orc.taurenchieftain.CAbilityShockWave; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.orc.taurenchieftain.CAbilityWarStomp; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.undead.deathknight.CAbilityDarkRitual; @@ -99,6 +101,7 @@ import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilitybuilder.parser.AbilityBuilderParserUtil; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilitybuilder.parser.AbilityBuilderType; import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilitybuilder.types.definitions.impl.CAbilityTypeDefinitionAbilityTemplateBuilder; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.orc.shadowhunter.CAbilityBigVoodoo ; public class CAbilityData { @@ -160,6 +163,9 @@ private void registerCodes() { this.codeToAbilityTypeDefinition.put(War3ID.fromString("AOsh"), new CAbilityTypeDefinitionSpellBase((handleId, alias) -> new CAbilityShockWave(handleId, alias))); + this.codeToAbilityTypeDefinition.put(War3ID.fromString("AOae"), + new CAbilityTypeDefinitionSpellBase((handleId, alias) -> new CAbilityEnduranceAura(handleId, alias))); + // Shadow Spirit this.codeToAbilityTypeDefinition.put(War3ID.fromString("AOwd"), new CAbilityTypeDefinitionSpellBase((handleId, alias) -> new CAbilitySerpentWard(handleId, alias))); @@ -169,14 +175,20 @@ private void registerCodes() { this.codeToAbilityTypeDefinition.put(War3ID.fromString("AOhw"), new CAbilityTypeDefinitionSpellBase((handleId, alias) -> new CAbilityHealingWave(handleId, alias))); + this.codeToAbilityTypeDefinition.put(War3ID.fromString("AOww"), + new CAbilityTypeDefinitionSpellBase((handleId, alias) -> new CAbilityBigVoodoo(handleId, alias))); //Blade Master this.codeToAbilityTypeDefinition.put(War3ID.fromString("AOww"), new CAbilityTypeDefinitionSpellBase((handleId, alias) -> new CAbilityWhirlWind(handleId, alias))); - // Blademaster + this.codeToAbilityTypeDefinition.put(War3ID.fromString("AOmi"), new CAbilityTypeDefinitionSpellBase((handleId, alias) -> new CAbilityMirrorImage(handleId, alias))); + this.codeToAbilityTypeDefinition.put(War3ID.fromString("AOsw"), + new CAbilityTypeDefinitionSpellBase((handleId, alias) -> new CAbilityBladeStorm(handleId, alias))); + + // Burrow: this.codeToAbilityTypeDefinition.put(War3ID.fromString("Abun"), new CAbilityTypeDefinitionCargoHoldBurrow()); this.codeToAbilityTypeDefinition.put(War3ID.fromString("Astd"), new CAbilityTypeDefinitionStandDown()); diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/orders/OrderIds.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/orders/OrderIds.java index f816e4980..cb6c38950 100644 --- a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/orders/OrderIds.java +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/orders/OrderIds.java @@ -536,7 +536,10 @@ public class OrderIds { public static final int _gyrocopterbombs = 852061; //gyrocopter bombs (Agyb) -- public static final int _detectgyrocopter = 852062; //detect (gyrocopter) (Agyv) -- public static final int _detectmagicsentinel = 852070; //detect (magic sentinel) (Adts) -- - public static final int _stormhammers = 852078; //storm hammers (Asth) -- + public static final int _stormhammers = 852078; //storm hammers (Asth) -- + public static final int bladestorm = 852696; + public static final int endurance = 852551; + public static final int criticalbash = 852532; public static final int _brillianceaura = 852084; //brilliance aura (AHab) -- public static final int _devotionaura = 852085; //devotion aura (AHad) -- public static final int _bash = 852088; //bash (AHbh) -- From 37fd194488f96917a481e79d4f2ce57edccf6422 Mon Sep 17 00:00:00 2001 From: Bachir Zerourou Date: Mon, 20 Apr 2026 14:45:36 +0200 Subject: [PATCH 8/8] Tauren Cieften Reincarnation capacity --- .../CAbilityReincarnation.java | 206 ++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/taurenchieftain/CAbilityReincarnation.java diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/taurenchieftain/CAbilityReincarnation.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/taurenchieftain/CAbilityReincarnation.java new file mode 100644 index 000000000..a1ee960ea --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/skills/orc/taurenchieftain/CAbilityReincarnation.java @@ -0,0 +1,206 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.orc.taurenchieftain; + +import com.etheller.warsmash.units.GameObject; +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.util.WarsmashConstants; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.skills.CAbilityNoTargetSpellBase; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.definitions.impl.AbilityFields; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.listeners.CUnitDeathReplacementEffect; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.listeners.CUnitDeathReplacementEffectPriority; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.listeners.CUnitDeathReplacementResult; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.listeners.CUnitDeathReplacementStacking; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.projectile.CEffect; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.OrderIds; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CEffectType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityTargetCheckReceiver; + +public class CAbilityReincarnation extends CAbilityNoTargetSpellBase { + + // Durée du délai avant la résurrection (en secondes) + private float reviveDelay; + // Durée de rechargement (cooldown) avant que la passive puisse se déclencher à nouveau + private float cooldownTime; + + // Référence au listener de mort pour retrait propre dans onRemove + private ReincarnationDeathEffect deathEffect; + + public CAbilityReincarnation(final int handleId, final War3ID alias) { + super(handleId, alias); + } + + // ------------------------------------------------------------------------- + // Données SLK — appelé à l'init ET à chaque level-up + // ------------------------------------------------------------------------- + + @Override + public void populateData(final GameObject worldEditorAbility, final int level) { + // DURATION = délai avant résurrection (ex : 3 secondes en WC3) + this.reviveDelay = worldEditorAbility.getFieldAsFloat(AbilityFields.DURATION + level, 0); + // COOLDOWN = temps de rechargement avant nouveau déclenchement possible + this.cooldownTime = worldEditorAbility.getFieldAsFloat(AbilityFields.COOLDOWN + level, 0); + } + + // ------------------------------------------------------------------------- + // Cycle de vie + // ------------------------------------------------------------------------- + + @Override + public void onAdd(final CSimulation game, final CUnit unit) { + // Passive : pas de CBehaviorNoTargetSpellBase (super.onAdd non appelé) + this.deathEffect = new ReincarnationDeathEffect(); + unit.addDeathReplacementEffect(CUnitDeathReplacementEffectPriority.GENERALONDEATHACTIONS, this.deathEffect); + } + + @Override + public void onRemove(final CSimulation game, final CUnit unit) { + if (this.deathEffect != null) { + unit.removeDeathReplacementEffect(CUnitDeathReplacementEffectPriority.GENERALONDEATHACTIONS, this.deathEffect); + this.deathEffect = null; + } + } + + // ------------------------------------------------------------------------- + // Passive : aucune activation manuelle possible + // ------------------------------------------------------------------------- + + @Override + public int getBaseOrderId() { + return OrderIds.reincarnation; + } + + @Override + protected void innerCheckCanTargetNoTarget(final CSimulation game, final CUnit unit, + final int orderId, final AbilityTargetCheckReceiver receiver) { + receiver.orderIdNotAccepted(); + } + + @Override + public boolean doEffect(final CSimulation simulation, final CUnit unit, + final AbilityTarget target) { + return false; + } + + // ========================================================================= + // Listener de remplacement de mort — déclenché par CUnit.kill() + // ========================================================================= + + private final class ReincarnationDeathEffect implements CUnitDeathReplacementEffect { + + @Override + public CUnitDeathReplacementStacking onDeath( + final CSimulation simulation, + final CUnit unit, + final CUnit source, + final CUnitDeathReplacementResult result) { + + // ---------------------------------------------------------------- + // Vérification du cooldown : si la passive est encore en + // rechargement, on laisse la mort se produire normalement. + // ---------------------------------------------------------------- + if (unit.getCooldownRemainingTicks(simulation, getAlias()) > 0) { + return new CUnitDeathReplacementStacking(); + } + + // ---------------------------------------------------------------- + // Prévention de la mort : result.setReviving(true) fait retourner + // kill() immédiatement, avant toute suppression de l'unité. + // ---------------------------------------------------------------- + result.setReviving(true); + + // ---------------------------------------------------------------- + // Remettre 1 HP pour que isDead() retourne false et éviter toute + // re-entrée dans kill() lors des prochains ticks. + // heal() appelle setLife() qui gère la notification UI. + // ---------------------------------------------------------------- + unit.heal(simulation, 1f); + + // ---------------------------------------------------------------- + // Rendre le Tauren invulnérable pendant le délai de résurrection + // ---------------------------------------------------------------- + unit.setInvulnerable(true); + + // ---------------------------------------------------------------- + // Mettre la passive en cooldown pour éviter un déclenchement + // immédiat lors de la prochaine mort (ex : pendant la résurrection) + // ---------------------------------------------------------------- + unit.beginCooldown(simulation, getAlias(), cooldownTime); + + // ---------------------------------------------------------------- + // Effet visuel de début de résurrection sur le Tauren + // ---------------------------------------------------------------- + simulation.createTemporarySpellEffectOnUnit(unit, getAlias(), CEffectType.CASTER); + + // ---------------------------------------------------------------- + // Enregistrement de l'effet temporisé qui gère le délai puis + // la résurrection complète (HP/mana pleins + fin d'invulnérabilité) + // ---------------------------------------------------------------- + final int reviveTick = simulation.getGameTurnTick() + + (int) StrictMath.ceil(reviveDelay / WarsmashConstants.SIMULATION_STEP_TIME); + + simulation.registerEffect(new CEffectReincarnation(unit, reviveTick, getAlias())); + + // Retourner un stacking par défaut (ne bloque pas les autres effets) + return new CUnitDeathReplacementStacking(); + } + } + + // ========================================================================= + // Effet interne : gère le délai puis la résurrection + // ========================================================================= + + private static final class CEffectReincarnation implements CEffect { + + private final CUnit unit; + private final int reviveTick; + private final War3ID alias; + private boolean done; + + public CEffectReincarnation(final CUnit unit, final int reviveTick, final War3ID alias) { + this.unit = unit; + this.reviveTick = reviveTick; + this.alias = alias; + this.done = false; + } + + @Override + public boolean update(final CSimulation game) { + if (this.done) { + return true; + } + + // Si l'unité a quand même été supprimée entre-temps (cas extrême), + // on annule proprement sans rien faire. + if (this.unit.isHidden()) { + this.done = true; + return true; + } + + if (game.getGameTurnTick() < this.reviveTick) { + return false; + } + + this.done = true; + + // ---------------------------------------------------------------- + // Résurrection : HP et mana au maximum + // ---------------------------------------------------------------- + this.unit.setLife(game, this.unit.getMaximumLife()); + this.unit.setMana(this.unit.getMaximumMana()); + + // ---------------------------------------------------------------- + // Fin de l'invulnérabilité + // ---------------------------------------------------------------- + this.unit.setInvulnerable(false); + + // ---------------------------------------------------------------- + // Effet visuel de fin de résurrection + // ---------------------------------------------------------------- + game.createTemporarySpellEffectOnUnit(this.unit, this.alias, CEffectType.SPECIAL); + + return true; + } + } +} \ No newline at end of file