Skip to content

Commit

Permalink
Merge pull request #263 from vincent4vx/spell-effect-heal-on-attack
Browse files Browse the repository at this point in the history
#27 Add spell effect heal on attack
  • Loading branch information
vincent4vx authored May 15, 2022
2 parents d8c2ab0 + c2f0f2c commit 8177319
Show file tree
Hide file tree
Showing 5 changed files with 310 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*
* 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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', '')",
}, ",") + ";"
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Fighter> configureFight(Consumer<FightBuilder> configurator) {
fight.cancel(true);

Expand Down
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*
* 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<Buff> 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;
}
}

0 comments on commit 8177319

Please sign in to comment.