Passed
Push — master ( 95c2ad...b1bd2c )
by Vincent
06:21 queued 12s
created

addHealBuff(Interval,int,PassiveFighter)   A

Complexity

Conditions 2

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 4
c 0
b 0
f 0
dl 0
loc 5
ccs 3
cts 3
cp 1
crap 2
rs 10
1
/*
2
 * This file is part of Araknemu.
3
 *
4
 * Araknemu is free software: you can redistribute it and/or modify
5
 * it under the terms of the GNU Lesser General Public License as published by
6
 * the Free Software Foundation, either version 3 of the License, or
7
 * (at your option) any later version.
8
 *
9
 * Araknemu is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 * GNU Lesser General Public License for more details.
13
 *
14
 * You should have received a copy of the GNU Lesser General Public License
15
 * along with Araknemu.  If not, see <https://www.gnu.org/licenses/>.
16
 *
17
 * Copyright (c) 2017-2021 Vincent Quatrevieux
18
 */
19
20
package fr.quatrevieux.araknemu.game.fight.ai.simulation;
21
22
import fr.arakne.utils.value.Interval;
23
import fr.quatrevieux.araknemu.game.fight.fighter.ActiveFighter;
24
import fr.quatrevieux.araknemu.game.fight.fighter.PassiveFighter;
25
import fr.quatrevieux.araknemu.game.fight.map.FightCell;
26
import fr.quatrevieux.araknemu.game.spell.Spell;
27
import org.checkerframework.checker.index.qual.Positive;
28
29
/**
30
 * The simulation result of a cast
31
 */
32
public final class CastSimulation {
33
    /**
34
     * The rate to apply on a poison damage value
35
     */
36
    public static final double POISON_RATE = 0.75;
37
38
    private final Spell spell;
39
    private final ActiveFighter caster;
40
    private final FightCell target;
41
42
    private double enemiesLife;
43
    private double alliesLife;
44
    private double selfLife;
45
46
    private double enemiesBoost;
47
    private double alliesBoost;
48
    private double selfBoost;
49
50
    private double killedAllies;
51
    private double killedEnemies;
52
    private double suicide;
53
54 1
    private double actionPointsModifier = 0;
55
56 1
    public CastSimulation(Spell spell, ActiveFighter caster, FightCell target) {
57 1
        this.spell = spell;
58 1
        this.caster = caster;
59 1
        this.target = target;
60 1
    }
61
62
    /**
63
     * The enemies life diff (negative value for damage, positive for heal)
64
     */
65
    public double enemiesLife() {
66 1
        return enemiesLife;
67
    }
68
69
    /**
70
     * The allies (without self) life diff (negative value for damage, positive for heal)
71
     */
72
    public double alliesLife() {
73 1
        return alliesLife;
74
    }
75
76
    /**
77
     * The self (caster) life diff (negative value for damage, positive for heal)
78
     */
79
    public double selfLife() {
80 1
        return selfLife;
81
    }
82
83
    /**
84
     * Number of killed allies
85
     */
86
    public double killedAllies() {
87 1
        return killedAllies;
88
    }
89
90
    /**
91
     * Number of killed enemies
92
     */
93
    public double killedEnemies() {
94 1
        return killedEnemies;
95
    }
96
97
    /**
98
     * The suicide (self kill) probability
99
     *
100
     * @return The probability between 0 and 1
101
     */
102
    public double suicideProbability() {
103 1
        return Math.min(suicide, 1);
104
    }
105
106
    /**
107
     * Heal a target
108
     *
109
     * @param value The heal value
110
     * @param target The target fighter
111
     */
112
    public void addHeal(final Interval value, final PassiveFighter target) {
113 1
        final int targetLostLife = target.life().max() - target.life().current();
114
115 1
        apply(new EffectValueComputer() {
116
            @Override
117
            public double lifeChange() {
118 1
                return computeCappedEffect(value, targetLostLife);
119
            }
120
        }, target);
121 1
    }
122
123
    /**
124
     * Heal a target using a buff
125
     *
126
     * @param value The heal value
127
     * @param target The target fighter
128
     */
129
    public void addHealBuff(final Interval value, final @Positive int duration, final PassiveFighter target) {
130 1
        addHeal(value, target);
131
132 1
        if (duration > 1) {
133 1
            addBoost(value.average() * POISON_RATE * (duration - 1), target);
134
        }
135 1
    }
136
137
    /**
138
     * Add a damage on the target
139
     *
140
     * @param value The damage value
141
     * @param target The target fighter
142
     */
143
    public void addDamage(final Interval value, final PassiveFighter target) {
144 1
        final int targetLife = target.life().current();
145 1
        final double killProbability = cappedProbability(value, targetLife);
146
147 1
        apply(new EffectValueComputer() {
148
            @Override
149
            public double killProbability() {
150 1
                return killProbability;
151
            }
152
153
            @Override
154
            public double lifeChange() {
155 1
                return -computeCappedEffect(value, targetLife, killProbability);
156
            }
157
        }, target);
158 1
    }
159
160
    /**
161
     * Add a poison (damage on multiple turns) on the target
162
     *
163
     * @param value The damage value. Should be positive
164
     * @param duration The poison duration in turns
165
     * @param target The target fighter
166
     */
167
    public void addPoison(final Interval value, final @Positive int duration, final PassiveFighter target) {
168 1
        apply(new EffectValueComputer() {
169
            @Override
170
            public double lifeChange() {
171 1
                return -computeCappedEffect(
172 1
                    value.map(v -> v * duration),
173 1
                    target.life().current()
174
                ) * POISON_RATE;
175
            }
176
        }, target);
177 1
    }
178
179
    /**
180
     * Action point alternation for the current fighter
181
     * A positive value means that the current spell will add action points on the current turn of the fighter
182
     *
183
     * This value will be removed from spell action point cost for compute actual action point cost.
184
     */
185
    public void alterActionPoints(double value) {
186 1
        actionPointsModifier += value;
187 1
    }
188
189
    /**
190
     * Apply the effect values to a target
191
     *
192
     * @param values Computed effect values
193
     * @param target The target
194
     */
195
    public void apply(EffectValueComputer values, PassiveFighter target) {
196 1
        if (target.equals(caster)) {
197 1
            selfLife += values.lifeChange();
198 1
            suicide += values.killProbability();
199 1
            selfBoost += values.boost();
200 1
        } else if (target.team().equals(caster.team())) {
201 1
            alliesLife += values.lifeChange();
202 1
            killedAllies += values.killProbability();
203 1
            alliesBoost += values.boost();
204
        } else {
205 1
            enemiesLife += values.lifeChange();
206 1
            killedEnemies += values.killProbability();
207 1
            enemiesBoost += values.boost();
208
        }
209 1
    }
210
211
    /**
212
     * The enemy boost value.
213
     * Negative value for malus, and positive for bonus
214
     */
215
    public double enemiesBoost() {
216 1
        return enemiesBoost;
217
    }
218
219
    /**
220
     * The allies boost value (without self).
221
     * Negative value for malus, and positive for bonus
222
     */
223
    public double alliesBoost() {
224 1
        return alliesBoost;
225
    }
226
227
    /**
228
     * The self boost value.
229
     * Negative value for malus, and positive for bonus
230
     */
231
    public double selfBoost() {
232 1
        return selfBoost;
233
    }
234
235
    /**
236
     * Add a boost to the target
237
     *
238
     * @param value The boost value. Can be negative for a malus
239
     * @param target The target fighter
240
     */
241
    public void addBoost(double value, PassiveFighter target) {
242 1
        apply(new EffectValueComputer() {
243
            @Override
244
            public double boost() {
245 1
                return value;
246
            }
247
        }, target);
248 1
    }
249
250
    /**
251
     * Get the simulated spell caster
252
     */
253
    public ActiveFighter caster() {
254 1
        return caster;
255
    }
256
257
    /**
258
     * Get the simulated spell
259
     */
260
    public Spell spell() {
261 1
        return spell;
262
    }
263
264
    /**
265
     * Get the target cell
266
     */
267
    public FightCell target() {
268 1
        return target;
269
    }
270
271
    /**
272
     * Get the actual action points cost of the current action
273
     * Actions points change on the current fighter will be taken in account
274
     *
275
     * ex: if the spell cost 4 AP, but give 1 AP, the cost will be 3 AP
276
     *
277
     * The minimal value is bounded to 0.1
278
     */
279
    public double actionPointsCost() {
280 1
        return Math.max(spell.apCost() - actionPointsModifier, 0.1);
281
    }
282
283
    /**
284
     * Merge the simulation result into the current simulation
285
     *
286
     * All results will be added considering the percent,
287
     * which represents the probability of the simulation
288
     *
289
     * @param simulation The simulation to merge
290
     * @param percent The simulation chance int percent. This value as interval of [0, 100]
291
     */
292
    public void merge(CastSimulation simulation, double percent) {
293 1
        enemiesLife += simulation.enemiesLife * percent / 100d;
294 1
        alliesLife += simulation.alliesLife * percent / 100d;
295 1
        selfLife += simulation.selfLife * percent / 100d;
296
297 1
        enemiesBoost += simulation.enemiesBoost * percent / 100d;
298 1
        alliesBoost += simulation.alliesBoost * percent / 100d;
299 1
        selfBoost += simulation.selfBoost * percent / 100d;
300
301 1
        killedAllies += simulation.killedAllies * percent / 100d;
302 1
        killedEnemies += simulation.killedEnemies * percent / 100d;
303 1
        suicide += simulation.suicide * percent / 100d;
304
305 1
        actionPointsModifier += simulation.actionPointsModifier * percent / 100d;
306 1
    }
307
308
    /**
309
     * Compute the chance to rise max value of an effect
310
     *
311
     * Ex:
312
     * - Enemy has 50 life points
313
     * - The spell can inflict 25 to 75 damage
314
     * - So the spell has 50% chance of kill the enemy (25 -> 49: enemy is alive, 50 -> 75 enemy is dead)
315
     *
316
     * @param value The effect value interval
317
     * @param maxValue The maximum allowed value (capped value)
318
     *
319
     * @return The probability to rise the max value of the effect. 0 if max less than maxValue, 1 if min higher than maxValue, any value between 0 and 1 in other cases
320
     */
321
    private double cappedProbability(Interval value, double maxValue) {
322 1
        if (value.min() >= maxValue) {
323 1
            return 1;
324
        }
325
326 1
        if (value.max() < maxValue) {
327 1
            return 0;
328
        }
329
330 1
        return (value.max() - maxValue) / value.amplitude();
331
    }
332
333
    /**
334
     * Compute value of a capped effect
335
     *
336
     * Ex:
337
     * - Enemy has 50 life points
338
     * - The spell can inflict 25 to 75 damage
339
     * - If spell damage is higher than 50 (50 -> 75, 50% of chance), it will be capped to 50
340
     * - If spell damage is less than 50 (25 -> 49, 50% of chance), any value in the interval can happen, so average value is (25 + 49) / 2 ~= 37
341
     * - So the real average damage is : 50% * 50 + 50% * 37 = 43.5
342
     *
343
     * @param value The effect value interval
344
     * @param maxValue The maximum allowed value (capped value)
345
     * @param maxProbability The probability to rise the max value. Use {@link CastSimulation#cappedProbability(Interval, double)} to compute this value
346
     *
347
     * @return The real effect value.
348
     *     - If min is higher than maxValue return the maxValue
349
     *     - If max is less than maxValue return the average value of the interval
350
     *     - Else, takes the capped probability in account to compute the value
351
     */
352
    private double computeCappedEffect(Interval value, double maxValue, double maxProbability) {
353 1
        if (maxProbability == 1) {
354 1
            return maxValue;
355
        }
356
357 1
        if (maxProbability == 0) {
358 1
            return value.average();
359
        }
360
361 1
        final double cappedAvgValue = ((double) value.min() + maxValue) / 2d;
362
363 1
        return cappedAvgValue * (1d - maxProbability) + maxValue * maxProbability;
364
    }
365
366
    /**
367
     * Compute value of a capped effect
368
     *
369
     * Ex:
370
     * - Enemy has 50 life points
371
     * - The spell can inflict 25 to 75 damage
372
     * - If spell damage is higher than 50 (50 -> 75, 50% of chance), it will be capped to 50
373
     * - If spell damage is less than 50 (25 -> 49, 50% of chance), any value in the interval can happen, so average value is (25 + 49) / 2 ~= 37
374
     * - So the real average damage is : 50% * 50 + 50% * 37 = 43.5
375
     *
376
     * @param value The effect value interval
377
     * @param maxValue The maximum allowed value (capped value)
378
     *
379
     * @return The real effect value.
380
     *     - If min is higher than maxValue return the maxValue
381
     *     - If max is less than maxValue return the average value of the interval
382
     *     - Else, takes the capped probability in account to compute the value
383
     */
384
    private double computeCappedEffect(Interval value, double maxValue) {
385 1
        return computeCappedEffect(value, maxValue, cappedProbability(value, maxValue));
386
    }
387
388
    /**
389
     * Structure for compute applied effects values
390
     */
391
    public interface EffectValueComputer {
392
        /**
393
         * The kill probability
394
         *
395
         * @return a double value between 0 and 1
396
         */
397
        public default double killProbability() {
398 1
            return 0;
399
        }
400
401
        /**
402
         * The changed life of the target
403
         * Return a negative value for damage, or a positive for heal
404
         * Do nothing is return 0
405
         *
406
         * Note: the computed value must take in account the target current life
407
         */
408
        public default double lifeChange() {
409 1
            return 0;
410
        }
411
412
        /**
413
         * The boost value of the effect
414
         * Negative value for debuff, and positive for buff
415
         */
416
        public default double boost() {
417 1
            return 0;
418
        }
419
    }
420
}
421