fr.quatrevieux.araknemu.game.fight.ai.simulation.CastSimulation   A
last analyzed

Complexity

Total Complexity 37

Size/Duplication

Total Lines 386
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 130
c 1
b 0
f 0
dl 0
loc 386
ccs 81
cts 81
cp 1
rs 9.44
wmc 37

31 Methods

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