Passed
Pull Request — master (#206)
by Vincent
12:05
created

resolveTarget(PassiveFighter)   B

Complexity

Conditions 6

Size

Total Lines 24
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 6

Importance

Changes 0
Metric Value
cc 6
eloc 12
c 0
b 0
f 0
dl 0
loc 24
ccs 10
cts 10
cp 1
crap 6
rs 8.6666
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-2020 Vincent Quatrevieux
18
 */
19
20
package fr.quatrevieux.araknemu.game.fight.castable;
21
22
import fr.arakne.utils.value.helper.RandomUtil;
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 fr.quatrevieux.araknemu.game.spell.effect.SpellEffect;
28
29
import java.util.ArrayList;
30
import java.util.Collection;
31
import java.util.Collections;
32
import java.util.HashMap;
33
import java.util.HashSet;
34
import java.util.List;
35
import java.util.Map;
36
import java.util.Optional;
37
import java.util.Set;
38
import java.util.stream.Collectors;
39
40
/**
41
 * Wrap casting arguments
42
 */
43
public final class CastScope {
44
    /**
45
     * Cast scope is a temporary object, and the random is rarely used (only for "probable effects")
46
     */
47 1
    private static final RandomUtil RANDOM = RandomUtil.createShared();
48
49
    private final Castable action;
50
    private final ActiveFighter caster;
51
    private final FightCell target;
52
53
    private List<EffectScope> effects;
54
    private Map<PassiveFighter, PassiveFighter> targetMapping;
55
56 1
    public CastScope(Castable action, ActiveFighter caster, FightCell target) {
57 1
        this.action = action;
58 1
        this.caster = caster;
59 1
        this.target = target;
60 1
    }
61
62
    /**
63
     * Get the casted action
64
     */
65
    public Castable action() {
66 1
        return action;
67
    }
68
69
    /**
70
     * Get the spell, if and only if the action is a spell
71
     */
72
    public Optional<Spell> spell() {
73 1
        if (action instanceof Spell) {
74 1
            return Optional.of((Spell) action);
75
        } else {
76 1
            return Optional.empty();
77
        }
78
    }
79
80
    /**
81
     * Get the caster
82
     */
83
    public ActiveFighter caster() {
84 1
        return caster;
85
    }
86
87
    /**
88
     * Get the targeted cell
89
     */
90
    public FightCell target() {
91 1
        return target;
92
    }
93
94
    /**
95
     * Get the cast targets
96
     *
97
     * This method will not resolve target mapping, nor effect target mapping
98
     * It will return all targets, before the mapping is resolved
99
     * So if {@link CastScope#replaceTarget(PassiveFighter, PassiveFighter)} is called,
100
     * the new target will be added on this set
101
     *
102
     * Note: a new instance is returned to ensure that concurrent modification will not occur
103
     */
104
    public Set<PassiveFighter> targets() {
105 1
        return new HashSet<>(targetMapping.keySet());
106
    }
107
108
    /**
109
     * Replace a target of the cast
110
     *
111
     * @param originalTarget The base target fighter
112
     * @param newTarget The new target fighter
113
     */
114
    public void replaceTarget(PassiveFighter originalTarget, PassiveFighter newTarget) {
115 1
        targetMapping.put(originalTarget, newTarget);
116
117
        // Add new target as target if not yet defined
118 1
        if (!targetMapping.containsKey(newTarget)) {
119 1
            targetMapping.put(newTarget, newTarget);
120
        }
121 1
    }
122
123
    /**
124
     * Remove a target of the cast
125
     *
126
     * Note: this method will definitively remove the target,
127
     * even if {@link CastScope#replaceTarget(PassiveFighter, PassiveFighter)} is called
128
     */
129
    public void removeTarget(PassiveFighter target) {
130
        // Set target to null without remove the key to ensure that it will effectively remove
131
        // even if a replaceTarget() point to it
132 1
        targetMapping.put(target, null);
133 1
    }
134
135
    /**
136
     * Get list of effects to apply
137
     */
138
    public List<EffectScope> effects() {
139 1
        return effects;
140
    }
141
142
    /**
143
     * Add effects to the cast scope
144
     *
145
     * @param effects Effects to add
146
     */
147
    public CastScope withEffects(List<SpellEffect> effects) {
148 1
        this.effects = effects.stream()
149 1
            .map(effect -> new EffectScope(effect, resolveTargets(effect)))
150 1
            .collect(Collectors.toList())
151
        ;
152
153 1
        targetMapping = new HashMap<>();
154
155 1
        for (EffectScope effect : this.effects) {
156 1
            for (PassiveFighter fighter : effect.targets) {
157 1
                targetMapping.put(fighter, fighter);
158 1
            }
159 1
        }
160
161 1
        return this;
162
    }
163
164
    /**
165
     * Add effects which can have a probability to occurs (ex: Bluff)
166
     *
167
     * @param effects Effects to resolve and add
168
     */
169
    public CastScope withRandomEffects(List<SpellEffect> effects) {
170 1
        final List<SpellEffect> selectedEffects = new ArrayList<>(effects.size());
171 1
        final List<SpellEffect> probableEffects = new ArrayList<>();
172
173 1
        for (SpellEffect effect : effects) {
174 1
            if (effect.probability() == 0) {
175 1
                selectedEffects.add(effect);
176
            }  else {
177 1
                probableEffects.add(effect);
178
            }
179 1
        }
180
181
        // No probable effects
182 1
        if (probableEffects.isEmpty()) {
183 1
            return withEffects(effects);
184
        }
185
186 1
        int dice = RANDOM.nextInt(100);
187
188 1
        for (SpellEffect effect : probableEffects) {
189 1
            dice -= effect.probability();
190
191 1
            if (dice <= 0) {
192 1
                selectedEffects.add(effect);
193 1
                break;
194
            }
195 1
        }
196
197 1
        return withEffects(selectedEffects);
198
    }
199
200
    /**
201
     * Resolve the targets of the effect
202
     */
203
    private Collection<PassiveFighter> resolveTargets(SpellEffect effect) {
204 1
        if (effect.target().onlyCaster()) {
205 1
            return Collections.singleton(caster);
206
        }
207
208 1
        if (action.constraints().freeCell()) {
209 1
            return Collections.emptyList();
210
        }
211
212 1
        return effect.area()
213 1
            .resolve(target, caster.cell())
214 1
            .stream()
215 1
            .map(FightCell::fighter)
216 1
            .filter(Optional::isPresent)
217 1
            .map(Optional::get)
218 1
            .filter(fighter -> effect.target().test(caster, fighter))
219 1
            .collect(Collectors.toList())
220
        ;
221
    }
222
223
    /**
224
     * Resolve the target mapping
225
     *
226
     * @param baseTarget The base target of the effect
227
     *
228
     * @return Resolved target. Null if the target is removed
229
     */
230
    private PassiveFighter resolveTarget(PassiveFighter baseTarget) {
231 1
        PassiveFighter target = targetMapping.get(baseTarget);
0 ignored issues
show
Comprehensibility introduced by
The variable targetshadows a field with the same name declared in line 51. Consider renaming it.
Loading history...
232
233
        // Target is removed, or it's the original one : do not resolve chaining
234 1
        if (target == null || target.equals(baseTarget)) {
235 1
            return target;
236
        }
237
238
        // Keep list of visited mapping to break recursion
239 1
        final Set<PassiveFighter> resolved = new HashSet<>();
240
241 1
        resolved.add(baseTarget);
242 1
        resolved.add(target);
243
244
        // Resolve chaining
245
        for (;;) {
246 1
            target = targetMapping.get(target);
247
248
            // The target is removed, or already visited (can be itself)
249 1
            if (target == null || resolved.contains(target)) {
250 1
                return target;
251
            }
252
253 1
            resolved.add(target);
254
        }
255
    }
256
257
    public final class EffectScope {
258
        private final SpellEffect effect;
259
        private final Collection<PassiveFighter> targets;
260
261 1
        public EffectScope(SpellEffect effect, Collection<PassiveFighter> targets) {
262 1
            this.effect = effect;
263 1
            this.targets = targets;
264 1
        }
265
266
        /**
267
         * The related effect
268
         */
269
        public SpellEffect effect() {
270 1
            return effect;
271
        }
272
273
        /**
274
         * Get all targeted fighters for the current effect
275
         */
276
        public Collection<PassiveFighter> targets() {
277 1
            return targets.stream()
278 1
                .map(CastScope.this::resolveTarget)
279 1
                .filter(fighter -> fighter != null && !fighter.dead())
280 1
                .collect(Collectors.toList())
281
            ;
282
        }
283
    }
284
}
285