Passed
Pull Request — master (#229)
by Vincent
11:03
created

targets()   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
dl 0
loc 2
ccs 1
cts 1
cp 1
crap 1
rs 10
c 0
b 0
f 0
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
        // Use lazy instantiation and do not use stream API to optimise memory allocations
213 1
        PassiveFighter firstTarget = null;
214 1
        Collection<PassiveFighter> targets = null;
215
216 1
        for (FightCell cell : effect.area().resolve(target, caster.cell())) {
217 1
            final Optional<PassiveFighter> resolvedTarget = cell.fighter().filter(fighter -> effect.target().test(caster, fighter));
218
219 1
            if (!resolvedTarget.isPresent()) {
220 1
                continue;
221
            }
222
223
            // Found the first target
224 1
            if (firstTarget == null) {
225 1
                firstTarget = resolvedTarget.get();
226 1
                continue;
227
            }
228
229
            // Multiple targets are found : instantiate the collection
230 1
            if (targets == null) {
231 1
                targets = new ArrayList<>();
232 1
                targets.add(firstTarget);
233
            }
234
235 1
            targets.add(resolvedTarget.get());
236 1
        }
237
238
        // There is multiple targets
239 1
        if (targets != null) {
240 1
            return targets;
241
        }
242
243
        // There is only one target : create a singleton
244 1
        if (firstTarget != null) {
245 1
            return Collections.singleton(firstTarget);
246
        }
247
248
        // No targets are resolved
249 1
        return Collections.emptyList();
250
    }
251
252
    /**
253
     * Resolve the target mapping
254
     *
255
     * @param baseTarget The base target of the effect
256
     *
257
     * @return Resolved target. Null if the target is removed
258
     */
259
    private PassiveFighter resolveTarget(PassiveFighter baseTarget) {
260 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...
261
262
        // Target is removed, or it's the original one : do not resolve chaining
263 1
        if (target == null || target.equals(baseTarget)) {
264 1
            return target;
265
        }
266
267
        // Keep list of visited mapping to break recursion
268 1
        final Set<PassiveFighter> resolved = new HashSet<>();
269
270 1
        resolved.add(baseTarget);
271 1
        resolved.add(target);
272
273
        // Resolve chaining
274
        for (;;) {
275 1
            target = targetMapping.get(target);
276
277
            // The target is removed, or already visited (can be itself)
278 1
            if (target == null || resolved.contains(target)) {
279 1
                return target;
280
            }
281
282 1
            resolved.add(target);
283
        }
284
    }
285
286
    public final class EffectScope {
287
        private final SpellEffect effect;
288
        private final Collection<PassiveFighter> targets;
289
290 1
        public EffectScope(SpellEffect effect, Collection<PassiveFighter> targets) {
291 1
            this.effect = effect;
292 1
            this.targets = targets;
293 1
        }
294
295
        /**
296
         * The related effect
297
         */
298
        public SpellEffect effect() {
299 1
            return effect;
300
        }
301
302
        /**
303
         * Get all targeted fighters for the current effect
304
         */
305
        public Collection<PassiveFighter> targets() {
306 1
            return targets.stream()
307 1
                .map(CastScope.this::resolveTarget)
308 1
                .filter(fighter -> fighter != null && !fighter.dead())
309 1
                .collect(Collectors.toList())
310
            ;
311
        }
312
    }
313
}
314