Passed
Push — master ( f07afa...95c2ad )
by Vincent
05:58 queued 12s
created

actionPointsCost()   A

Complexity

Conditions 1

Size

Total Lines 2
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
c 0
b 0
f 0
dl 0
loc 2
ccs 1
cts 1
cp 1
crap 1
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
     * Add a damage on the target
125
     *
126
     * @param value The damage value
127
     * @param target The target fighter
128
     */
129
    public void addDamage(final Interval value, final PassiveFighter target) {
130 1
        final int targetLife = target.life().current();
131 1
        final double killProbability = cappedProbability(value, targetLife);
132
133 1
        apply(new EffectValueComputer() {
134
            @Override
135
            public double killProbability() {
136 1
                return killProbability;
137
            }
138
139
            @Override
140
            public double lifeChange() {
141 1
                return -computeCappedEffect(value, targetLife, killProbability);
142
            }
143
        }, target);
144 1
    }
145
146
    /**
147
     * Add a poison (damage on multiple turns) on the target
148
     *
149
     * @param value The damage value. Should be positive
150
     * @param duration The poison duration in turns
151
     * @param target The target fighter
152
     */
153
    public void addPoison(final Interval value, final @Positive int duration, final PassiveFighter target) {
154 1
        apply(new EffectValueComputer() {
155
            @Override
156
            public double lifeChange() {
157 1
                return -computeCappedEffect(
158 1
                    value.map(v -> v * duration),
159 1
                    target.life().current()
160
                ) * POISON_RATE;
161
            }
162
        }, target);
163 1
    }
164
165
    /**
166
     * Action point alternation for the current fighter
167
     * A positive value means that the current spell will add action points on the current turn of the fighter
168
     *
169
     * This value will be removed from spell action point cost for compute actual action point cost.
170
     */
171
    public void alterActionPoints(double value) {
172 1
        actionPointsModifier += value;
173 1
    }
174
175
    /**
176
     * Apply the effect values to a target
177
     *
178
     * @param values Computed effect values
179
     * @param target The target
180
     */
181
    public void apply(EffectValueComputer values, PassiveFighter target) {
182 1
        if (target.equals(caster)) {
183 1
            selfLife += values.lifeChange();
184 1
            suicide += values.killProbability();
185 1
            selfBoost += values.boost();
186 1
        } else if (target.team().equals(caster.team())) {
187 1
            alliesLife += values.lifeChange();
188 1
            killedAllies += values.killProbability();
189 1
            alliesBoost += values.boost();
190
        } else {
191 1
            enemiesLife += values.lifeChange();
192 1
            killedEnemies += values.killProbability();
193 1
            enemiesBoost += values.boost();
194
        }
195 1
    }
196
197
    /**
198
     * The enemy boost value.
199
     * Negative value for malus, and positive for bonus
200
     */
201
    public double enemiesBoost() {
202 1
        return enemiesBoost;
203
    }
204
205
    /**
206
     * The allies boost value (without self).
207
     * Negative value for malus, and positive for bonus
208
     */
209
    public double alliesBoost() {
210 1
        return alliesBoost;
211
    }
212
213
    /**
214
     * The self boost value.
215
     * Negative value for malus, and positive for bonus
216
     */
217
    public double selfBoost() {
218 1
        return selfBoost;
219
    }
220
221
    /**
222
     * Add a boost to the target
223
     *
224
     * @param value The boost value. Can be negative for a malus
225
     * @param target The target fighter
226
     */
227
    public void addBoost(double value, PassiveFighter target) {
228 1
        apply(new EffectValueComputer() {
229
            @Override
230
            public double boost() {
231 1
                return value;
232
            }
233
        }, target);
234 1
    }
235
236
    /**
237
     * Get the simulated spell caster
238
     */
239
    public ActiveFighter caster() {
240 1
        return caster;
241
    }
242
243
    /**
244
     * Get the simulated spell
245
     */
246
    public Spell spell() {
247 1
        return spell;
248
    }
249
250
    /**
251
     * Get the target cell
252
     */
253
    public FightCell target() {
254 1
        return target;
255
    }
256
257
    /**
258
     * Get the actual action points cost of the current action
259
     * Actions points change on the current fighter will be taken in account
260
     *
261
     * ex: if the spell cost 4 AP, but give 1 AP, the cost will be 3 AP
262
     *
263
     * The minimal value is bounded to 0.1
264
     */
265
    public double actionPointsCost() {
266 1
        return Math.max(spell.apCost() - actionPointsModifier, 0.1);
267
    }
268
269
    /**
270
     * Merge the simulation result into the current simulation
271
     *
272
     * All results will be added considering the percent,
273
     * which represents the probability of the simulation
274
     *
275
     * @param simulation The simulation to merge
276
     * @param percent The simulation chance int percent. This value as interval of [0, 100]
277
     */
278
    public void merge(CastSimulation simulation, double percent) {
279 1
        enemiesLife += simulation.enemiesLife * percent / 100d;
280 1
        alliesLife += simulation.alliesLife * percent / 100d;
281 1
        selfLife += simulation.selfLife * percent / 100d;
282
283 1
        enemiesBoost += simulation.enemiesBoost * percent / 100d;
284 1
        alliesBoost += simulation.alliesBoost * percent / 100d;
285 1
        selfBoost += simulation.selfBoost * percent / 100d;
286
287 1
        killedAllies += simulation.killedAllies * percent / 100d;
288 1
        killedEnemies += simulation.killedEnemies * percent / 100d;
289 1
        suicide += simulation.suicide * percent / 100d;
290
291 1
        actionPointsModifier += simulation.actionPointsModifier * percent / 100d;
292 1
    }
293
294
    /**
295
     * Compute the chance to rise max value of an effect
296
     *
297
     * Ex:
298
     * - Enemy has 50 life points
299
     * - The spell can inflict 25 to 75 damage
300
     * - So the spell has 50% chance of kill the enemy (25 -> 49: enemy is alive, 50 -> 75 enemy is dead)
301
     *
302
     * @param value The effect value interval
303
     * @param maxValue The maximum allowed value (capped value)
304
     *
305
     * @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
306
     */
307
    private double cappedProbability(Interval value, double maxValue) {
308 1
        if (value.min() >= maxValue) {
309 1
            return 1;
310
        }
311
312 1
        if (value.max() < maxValue) {
313 1
            return 0;
314
        }
315
316 1
        return (value.max() - maxValue) / value.amplitude();
317
    }
318
319
    /**
320
     * Compute value of a capped effect
321
     *
322
     * Ex:
323
     * - Enemy has 50 life points
324
     * - The spell can inflict 25 to 75 damage
325
     * - If spell damage is higher than 50 (50 -> 75, 50% of chance), it will be capped to 50
326
     * - 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
327
     * - So the real average damage is : 50% * 50 + 50% * 37 = 43.5
328
     *
329
     * @param value The effect value interval
330
     * @param maxValue The maximum allowed value (capped value)
331
     * @param maxProbability The probability to rise the max value. Use {@link CastSimulation#cappedProbability(Interval, double)} to compute this value
332
     *
333
     * @return The real effect value.
334
     *     - If min is higher than maxValue return the maxValue
335
     *     - If max is less than maxValue return the average value of the interval
336
     *     - Else, takes the capped probability in account to compute the value
337
     */
338
    private double computeCappedEffect(Interval value, double maxValue, double maxProbability) {
339 1
        if (maxProbability == 1) {
340 1
            return maxValue;
341
        }
342
343 1
        if (maxProbability == 0) {
344 1
            return value.average();
345
        }
346
347 1
        final double cappedAvgValue = ((double) value.min() + maxValue) / 2d;
348
349 1
        return cappedAvgValue * (1d - maxProbability) + maxValue * maxProbability;
350
    }
351
352
    /**
353
     * Compute value of a capped effect
354
     *
355
     * Ex:
356
     * - Enemy has 50 life points
357
     * - The spell can inflict 25 to 75 damage
358
     * - If spell damage is higher than 50 (50 -> 75, 50% of chance), it will be capped to 50
359
     * - 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
360
     * - So the real average damage is : 50% * 50 + 50% * 37 = 43.5
361
     *
362
     * @param value The effect value interval
363
     * @param maxValue The maximum allowed value (capped value)
364
     *
365
     * @return The real effect value.
366
     *     - If min is higher than maxValue return the maxValue
367
     *     - If max is less than maxValue return the average value of the interval
368
     *     - Else, takes the capped probability in account to compute the value
369
     */
370
    private double computeCappedEffect(Interval value, double maxValue) {
371 1
        return computeCappedEffect(value, maxValue, cappedProbability(value, maxValue));
372
    }
373
374
    /**
375
     * Structure for compute applied effects values
376
     */
377
    public interface EffectValueComputer {
378
        /**
379
         * The kill probability
380
         *
381
         * @return a double value between 0 and 1
382
         */
383
        public default double killProbability() {
384 1
            return 0;
385
        }
386
387
        /**
388
         * The changed life of the target
389
         * Return a negative value for damage, or a positive for heal
390
         * Do nothing is return 0
391
         *
392
         * Note: the computed value must take in account the target current life
393
         */
394
        public default double lifeChange() {
395 1
            return 0;
396
        }
397
398
        /**
399
         * The boost value of the effect
400
         * Negative value for debuff, and positive for buff
401
         */
402
        public default double boost() {
403 1
            return 0;
404
        }
405
    }
406
}
407