diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/heal/HealOnAttackHandler.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/heal/HealOnAttackHandler.java new file mode 100644 index 000000000..e6b39231d --- /dev/null +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/heal/HealOnAttackHandler.java @@ -0,0 +1,59 @@ +/* + * This file is part of Araknemu. + * + * Araknemu is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Araknemu is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Araknemu. If not, see . + * + * Copyright (c) 2017-2022 Vincent Quatrevieux + */ + +package fr.quatrevieux.araknemu.game.fight.castable.effect.handler.heal; + +import fr.quatrevieux.araknemu.game.fight.castable.CastScope; +import fr.quatrevieux.araknemu.game.fight.castable.effect.buff.Buff; +import fr.quatrevieux.araknemu.game.fight.castable.effect.buff.BuffHook; +import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.EffectHandler; +import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.damage.Damage; +import fr.quatrevieux.araknemu.game.fight.fighter.ActiveFighter; +import fr.quatrevieux.araknemu.game.fight.fighter.PassiveFighter; +import fr.quatrevieux.araknemu.util.Asserter; + +/** + * Heal the attacker by returning suffered damage as heal + * The return factor is configured by the "special" effect parameter (i.e. third parameter) in percent + * + * Note: only direct damage are taken in account + */ +public final class HealOnAttackHandler implements EffectHandler, BuffHook { + @Override + public void handle(CastScope cast, CastScope.EffectScope effect) { + throw new UnsupportedOperationException("Heal on attack effect must be used as a buff"); + } + + @Override + public void buff(CastScope cast, CastScope.EffectScope effect) { + for (PassiveFighter target : effect.targets()) { + target.buffs().add(new Buff(effect.effect(), cast.action(), cast.caster(), target, this)); + } + } + + @Override + public void onDirectDamage(Buff buff, ActiveFighter caster, Damage value) { + final int percent = Asserter.assertPercent(buff.effect().special()); + final int heal = value.value() * percent / 100; + + if (heal > 0) { + caster.life().alter(buff.target(), heal); + } + } +} diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/module/CommonEffectsModule.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/module/CommonEffectsModule.java index 670f17ef3..5e485c3b1 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/module/CommonEffectsModule.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/module/CommonEffectsModule.java @@ -59,6 +59,7 @@ import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.heal.FixedHealHandler; import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.heal.GivePercentLifeHandler; import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.heal.HealHandler; +import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.heal.HealOnAttackHandler; import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.heal.HealOnDamageHandler; import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.misc.ChangeAppearanceHandler; import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.misc.DispelHandler; @@ -135,6 +136,7 @@ public void effects(EffectsHandler handler) { handler.register(90, new GivePercentLifeHandler()); handler.register(108, new HealHandler()); handler.register(143, new FixedHealHandler()); + handler.register(786, new HealOnAttackHandler()); handler.register(140, new SkipTurnHandler(fight)); handler.register(149, new ChangeAppearanceHandler(fight)); diff --git a/src/test/java/fr/quatrevieux/araknemu/game/GameDataSet.java b/src/test/java/fr/quatrevieux/araknemu/game/GameDataSet.java index 59ee7c114..258a29874 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/GameDataSet.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/GameDataSet.java @@ -518,6 +518,7 @@ public GameDataSet pushFunctionalSpells() throws SQLException, ContainerExceptio "(155, 'Vitalité ', 801, '10,1,1', '125,81,90,,20,0,1d10+80|125,110,,,20,0,0d0+110|4|0|1|45|100|true|false|false|false|0|0|0|5|PaPa||18;19;3;1;41|36|false', '125,91,110,,20,0,1d20+90|125,130,,,20,0,0d0+130|4|0|1|45|100|true|false|false|false|0|0|0|5|PaPa||18;19;3;1;41|36|false', '125,101,130,,20,0,1d30+100|125,150,,,20,0,0d0+150|4|0|1|45|100|true|false|false|false|0|0|0|5|PaPa||18;19;3;1;41|36|false', '125,121,150,,20,0,1d30+120|125,175,,,20,0,0d0+175|4|0|1|45|100|true|false|false|false|0|0|0|5|PaPa||18;19;3;1;41|36|false', '125,151,180,,20,0,1d30+150|125,200,,,20,0,0d0+200|4|0|1|45|100|true|false|false|false|0|0|0|5|PaPa||18;19;3;1;41|36|false', '125,251,300,,20,0,1d50+250|125,350,,,20,0,0d0+350|3|0|1|45|100|true|false|false|false|0|0|0|5|PaPa||18;19;3;1;41|136|false', '')", "(1723, 'Spajuste', -1, '0,0,0', '268,8,12,,4,0,1d5+7|268,14,,,4,0,0d0+14|2|1|3|40|100|false|true|false|true|0|0|1|0|PaPa||18;19;3;1;41|30|false', '268,10,14,,4,0,1d5+9|268,16,,,4,0,0d0+16|2|1|3|40|100|false|true|false|true|0|0|1|0|PaPa||18;19;3;1;41|30|false', '268,12,16,,4,0,1d5+11|268,18,,,4,0,0d0+18|2|1|4|40|100|false|true|false|true|0|0|1|0|PaPa||18;19;3;1;41|30|false', '268,14,18,,4,0,1d5+13|268,20,,,4,0,0d0+20|2|1|4|40|100|false|true|false|true|0|0|1|0|PaPa||18;19;3;1;41|30|false', '268,16,20,,4,0,1d5+15|268,22,,,4,0,0d0+22|2|1|5|40|100|false|true|false|true|0|0|1|0|PaPa||18;19;3;1;41|30|false', '268,21,25,,4,0,1d5+20|268,30,,,4,0,0d0+30|2|1|6|35|100|false|true|false|true|0|0|1|0|PaPa||18;19;3;1;41|30|false', '')", "(171, 'Flèche Punitive', 912, '51,2,1', '97,15,17,,0,0,1d3+14;293,171,,16,3,0|97,20,22,,0,0,1d3+19;293,171,,21,3,0|4|6|8|30|100|false|true|false|true|0|0|0|2|PaPaPaPa||18;19;3;1;41|31|false', '97,17,19,,0,0,1d3+16;293,171,,18,2,0|97,22,24,,0,0,1d3+21;293,171,,23,2,0|4|6|8|30|100|false|true|false|true|0|0|0|2|PaPaPaPa||18;19;3;1;41|31|false', '97,19,21,,0,0,1d3+18;293,171,,20,2,0|97,23,25,,0,0,1d3+22;293,171,,24,2,0|4|6|8|30|100|false|true|false|true|0|0|0|2|PaPaPaPa||18;19;3;1;41|31|false', '97,23,25,,0,0,1d3+22;293,171,,24,2,0|97,29,31,,0,0,1d3+28;293,171,,30,2,0|4|6|8|30|100|false|true|false|true|0|0|0|2|PaPaPaPa||18;19;3;1;41|31|false', '97,25,27,,0,0,1d3+24;293,171,,26,2,0|97,30,32,,0,0,1d3+29;293,171,,31,2,0|4|6|8|30|100|false|true|false|true|0|0|0|2|PaPaPaPa||18;19;3;1;41|31|false', '97,31,33,,0,0,1d3+30;293,171,,32,2,0|97,37,39,,0,0,1d3+36;293,171,,38,2,0|4|6|8|30|100|false|true|false|true|0|0|0|2|PaPaPaPa||18;19;3;1;41|131|false', '0;32')", + "(1687, 'Soin Sylvestre', 0, '0,-1,0', '786,,,100,1,0||6|0|0|0|0|false|true|false|false|0|1|0|0|Pa||18;19;3;1;41|0|false', '786,,,100,1,0||6|0|0|0|0|false|true|false|false|0|1|0|0|Pa||18;19;3;1;41|0|false', '786,,,100,1,0||6|0|0|0|0|false|true|false|false|0|1|0|0|Pa||18;19;3;1;41|0|false', '786,,,100,1,0||6|0|0|0|0|false|true|false|false|0|1|0|0|Pa||18;19;3;1;41|0|false', '786,,,100,1,0||6|0|0|0|0|false|true|false|false|0|1|0|0|Pa||18;19;3;1;41|0|false', '786,,,100,1,0||6|0|0|0|0|false|true|false|false|0|1|0|0|Pa||18;19;3;1;41|0|false', '')", }, ",") + ";" ); diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/FunctionalTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/FunctionalTest.java index 5943f9596..fa21da9d9 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/FunctionalTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/FunctionalTest.java @@ -1031,6 +1031,22 @@ void boostSpellDamage() { assertBetween(102, 106, damage); } + @Test + void healOnAttack() { + castNormal(1687, fighter1.cell()); // Soin Sylvestre + fighter2.life().alter(fighter2, -20); + fighter1.turn().stop(); + + int lastLife = fighter2.life().current(); + + castNormal(183, fighter1.cell()); // Simple attack + + int damage = fighter1.life().max() - fighter1.life().current(); + + assertEquals(damage, fighter2.life().current() - lastLife); + requestStack.assertOne(ActionEffect.alterLifePoints(fighter1, fighter2, damage)); + } + private List configureFight(Consumer configurator) { fight.cancel(true); diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/heal/HealOnAttackHandlerTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/heal/HealOnAttackHandlerTest.java new file mode 100644 index 000000000..0ffb1d3cf --- /dev/null +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/heal/HealOnAttackHandlerTest.java @@ -0,0 +1,232 @@ +/* + * This file is part of Araknemu. + * + * Araknemu is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Araknemu is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Araknemu. If not, see . + * + * Copyright (c) 2017-2022 Vincent Quatrevieux + */ + +package fr.quatrevieux.araknemu.game.fight.castable.effect.handler.heal; + +import fr.quatrevieux.araknemu.data.constant.Characteristic; +import fr.quatrevieux.araknemu.data.value.EffectArea; +import fr.quatrevieux.araknemu.game.fight.Fight; +import fr.quatrevieux.araknemu.game.fight.FightBaseCase; +import fr.quatrevieux.araknemu.game.fight.castable.CastScope; +import fr.quatrevieux.araknemu.game.fight.castable.effect.Element; +import fr.quatrevieux.araknemu.game.fight.castable.effect.buff.Buff; +import fr.quatrevieux.araknemu.game.fight.castable.effect.buff.BuffHook; +import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.damage.Damage; +import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.damage.DamageApplier; +import fr.quatrevieux.araknemu.game.fight.fighter.player.PlayerFighter; +import fr.quatrevieux.araknemu.game.spell.Spell; +import fr.quatrevieux.araknemu.game.spell.SpellConstraints; +import fr.quatrevieux.araknemu.game.spell.effect.SpellEffect; +import fr.quatrevieux.araknemu.game.spell.effect.area.CellArea; +import fr.quatrevieux.araknemu.game.spell.effect.area.CircleArea; +import fr.quatrevieux.araknemu.game.spell.effect.target.SpellEffectTarget; +import fr.quatrevieux.araknemu.network.game.out.fight.action.ActionEffect; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +class HealOnAttackHandlerTest extends FightBaseCase { + private Fight fight; + private PlayerFighter caster; + private PlayerFighter target; + private HealOnAttackHandler handler; + private int lastCasterLife; + + @Override + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + + fight = createFight(); + fight.nextState(); + + caster = player.fighter(); + target = other.fighter(); + + target.move(fight.map().get(123)); + caster.life().alter(caster, -30); + lastCasterLife = caster.life().current(); + + handler = new HealOnAttackHandler(); + + player.properties().characteristics().base().set(Characteristic.INTELLIGENCE, 0); + + requestStack.clear(); + } + + @Test + void handle() { + SpellEffect effect = Mockito.mock(SpellEffect.class); + Spell spell = Mockito.mock(Spell.class); + SpellConstraints constraints = Mockito.mock(SpellConstraints.class); + + Mockito.when(effect.area()).thenReturn(new CellArea()); + Mockito.when(effect.target()).thenReturn(SpellEffectTarget.DEFAULT); + Mockito.when(spell.constraints()).thenReturn(constraints); + Mockito.when(constraints.freeCell()).thenReturn(false); + + CastScope scope = makeCastScope(caster, spell, effect, caster.cell()); + assertThrows(UnsupportedOperationException.class, () -> handler.handle(scope, scope.effects().get(0))); + } + + @Test + void buffWillAddBuffToList() { + SpellEffect effect = Mockito.mock(SpellEffect.class); + Spell spell = Mockito.mock(Spell.class); + SpellConstraints constraints = Mockito.mock(SpellConstraints.class); + + Mockito.when(effect.min()).thenReturn(10); + Mockito.when(effect.area()).thenReturn(new CellArea()); + Mockito.when(effect.target()).thenReturn(SpellEffectTarget.DEFAULT); + Mockito.when(effect.duration()).thenReturn(5); + Mockito.when(spell.constraints()).thenReturn(constraints); + Mockito.when(constraints.freeCell()).thenReturn(false); + + CastScope scope = makeCastScope(caster, spell, effect, target.cell()); + handler.buff(scope, scope.effects().get(0)); + + Optional found = target.buffs().stream().filter(buff -> buff.effect().equals(effect)).findFirst(); + + assertTrue(found.isPresent()); + assertEquals(caster, found.get().caster()); + assertEquals(target, found.get().target()); + assertEquals(effect, found.get().effect()); + assertEquals(spell, found.get().action()); + assertEquals(handler, found.get().hook()); + assertEquals(5, found.get().remainingTurns()); + } + + @Test + void buffWithAreaMultipleFighters() { + SpellEffect effect = Mockito.mock(SpellEffect.class); + Spell spell = Mockito.mock(Spell.class); + SpellConstraints constraints = Mockito.mock(SpellConstraints.class); + + Mockito.when(effect.min()).thenReturn(10); + Mockito.when(effect.area()).thenReturn(new CircleArea(new EffectArea(EffectArea.Type.CIRCLE, 20))); + Mockito.when(effect.target()).thenReturn(SpellEffectTarget.DEFAULT); + Mockito.when(spell.constraints()).thenReturn(constraints); + Mockito.when(constraints.freeCell()).thenReturn(false); + + CastScope scope = makeCastScope(caster, spell, effect, fight.map().get(122)); + handler.buff(scope, scope.effects().get(0)); + + assertTrue(caster.buffs().stream().anyMatch(buff -> buff.effect().equals(effect))); + assertTrue(target.buffs().stream().anyMatch(buff -> buff.effect().equals(effect))); + } + + @Test + void onDirectDamage() { + SpellEffect effect = Mockito.mock(SpellEffect.class); + Spell spell = Mockito.mock(Spell.class); + SpellConstraints constraints = Mockito.mock(SpellConstraints.class); + + Mockito.when(effect.special()).thenReturn(100); + Mockito.when(effect.area()).thenReturn(new CellArea()); + Mockito.when(effect.target()).thenReturn(SpellEffectTarget.DEFAULT); + Mockito.when(spell.constraints()).thenReturn(constraints); + Mockito.when(constraints.freeCell()).thenReturn(false); + + CastScope scope = makeCastScope(caster, spell, effect, target.cell()); + handler.buff(scope, scope.effects().get(0)); + + new DamageApplier(Element.WATER, fight).applyFixed(caster, 10, target); + + assertEquals(10, computeHeal()); + + requestStack.assertOne(ActionEffect.alterLifePoints(target, caster, computeHeal())); + } + + @Test + void onDirectDamageTransformedToHealShouldBeIgnored() { + SpellEffect effect = Mockito.mock(SpellEffect.class); + Spell spell = Mockito.mock(Spell.class); + SpellConstraints constraints = Mockito.mock(SpellConstraints.class); + + Mockito.when(effect.special()).thenReturn(100); + Mockito.when(effect.area()).thenReturn(new CellArea()); + Mockito.when(effect.target()).thenReturn(SpellEffectTarget.DEFAULT); + Mockito.when(spell.constraints()).thenReturn(constraints); + Mockito.when(constraints.freeCell()).thenReturn(false); + + target.buffs().add(new Buff(effect, spell, target, target, new BuffHook() { + @Override + public void onDamage(Buff buff, Damage value) { + value.multiply(-1); // Transform damage to heal + } + })); + + CastScope scope = makeCastScope(caster, spell, effect, target.cell()); + handler.buff(scope, scope.effects().get(0)); + + new DamageApplier(Element.WATER, fight).applyFixed(caster, 10, target); + + assertEquals(0, computeHeal()); + } + + @Test + void onDirectDamageNot100Percent() { + SpellEffect effect = Mockito.mock(SpellEffect.class); + Spell spell = Mockito.mock(Spell.class); + SpellConstraints constraints = Mockito.mock(SpellConstraints.class); + + Mockito.when(effect.special()).thenReturn(50); + Mockito.when(effect.area()).thenReturn(new CellArea()); + Mockito.when(effect.target()).thenReturn(SpellEffectTarget.DEFAULT); + Mockito.when(spell.constraints()).thenReturn(constraints); + Mockito.when(constraints.freeCell()).thenReturn(false); + + CastScope scope = makeCastScope(caster, spell, effect, target.cell()); + handler.buff(scope, scope.effects().get(0)); + + new DamageApplier(Element.WATER, fight).applyFixed(caster, 10, target); + + assertEquals(5, computeHeal()); + + requestStack.assertOne(ActionEffect.alterLifePoints(target, caster, computeHeal())); + } + + @Test + void onIndirectDamageShouldBeIgnored() { + SpellEffect effect = Mockito.mock(SpellEffect.class); + Spell spell = Mockito.mock(Spell.class); + SpellConstraints constraints = Mockito.mock(SpellConstraints.class); + + Mockito.when(effect.special()).thenReturn(100); + Mockito.when(effect.area()).thenReturn(new CellArea()); + Mockito.when(effect.target()).thenReturn(SpellEffectTarget.DEFAULT); + Mockito.when(spell.constraints()).thenReturn(constraints); + Mockito.when(constraints.freeCell()).thenReturn(false); + + CastScope scope = makeCastScope(caster, spell, effect, target.cell()); + handler.buff(scope, scope.effects().get(0)); + + new DamageApplier(Element.WATER, fight).applyIndirectFixed(caster, 10, target); + + assertEquals(0, computeHeal()); + } + + private int computeHeal() { + return caster.life().current() - lastCasterLife; + } +}