removeFighter(Fighter)   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 4
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
dl 0
loc 4
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
eloc 4
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-2019 Vincent Quatrevieux
18
 */
19
20
package fr.quatrevieux.araknemu.game.fight.state;
21
22
import fr.quatrevieux.araknemu.core.event.EventsSubscriber;
23
import fr.quatrevieux.araknemu.core.event.Listener;
24
import fr.quatrevieux.araknemu.game.fight.Fight;
25
import fr.quatrevieux.araknemu.game.fight.JoinFightError;
26
import fr.quatrevieux.araknemu.game.fight.ending.EndFightResults;
27
import fr.quatrevieux.araknemu.game.fight.ending.reward.FightRewardsSheet;
28
import fr.quatrevieux.araknemu.game.fight.event.FightJoined;
29
import fr.quatrevieux.araknemu.game.fight.event.FightLeaved;
30
import fr.quatrevieux.araknemu.game.fight.event.FighterAdded;
31
import fr.quatrevieux.araknemu.game.fight.event.FighterPlaceChanged;
32
import fr.quatrevieux.araknemu.game.fight.event.FighterRemoved;
33
import fr.quatrevieux.araknemu.game.fight.exception.FightException;
34
import fr.quatrevieux.araknemu.game.fight.exception.FightMapException;
35
import fr.quatrevieux.araknemu.game.fight.exception.InvalidFightStateException;
36
import fr.quatrevieux.araknemu.game.fight.exception.JoinFightException;
37
import fr.quatrevieux.araknemu.game.fight.fighter.Fighter;
38
import fr.quatrevieux.araknemu.game.fight.map.FightCell;
39
import fr.quatrevieux.araknemu.game.fight.map.util.PlacementCellsGenerator;
40
import fr.quatrevieux.araknemu.game.fight.team.FightTeam;
41
import fr.quatrevieux.araknemu.game.listener.fight.SendFighterPositions;
42
import fr.quatrevieux.araknemu.game.listener.fight.SendFighterReadyState;
43
import fr.quatrevieux.araknemu.game.listener.fight.SendJoinTeamOptionChangedMessage;
44
import fr.quatrevieux.araknemu.game.listener.fight.SendNeedHelpOptionChangedMessage;
45
import fr.quatrevieux.araknemu.game.listener.fight.SendNewFighter;
46
import fr.quatrevieux.araknemu.game.listener.fight.StartFightWhenAllReady;
47
import fr.quatrevieux.araknemu.game.listener.fight.fighter.ClearFighter;
48
import fr.quatrevieux.araknemu.game.listener.fight.fighter.SendFighterRemoved;
49
import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
50
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
51
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
52
import org.checkerframework.checker.nullness.util.NullnessUtil;
53
54
import java.util.Collection;
55
import java.util.Collections;
56
import java.util.HashMap;
57
import java.util.Map;
58
import java.util.concurrent.ScheduledFuture;
59
import java.util.stream.Collectors;
60
61
/**
62
 * Placement before start fight
63
 */
64
public final class PlacementState implements LeavableState, EventsSubscriber {
65
    private long startTime;
66
    private @MonotonicNonNull Fight fight;
67
    private Listener @MonotonicNonNull[] listeners;
68
    private @MonotonicNonNull Map<FightTeam, PlacementCellsGenerator> cellsGenerators;
69
    private @MonotonicNonNull ScheduledFuture<?> timer;
70
71
    private final boolean randomize;
72
73
    public PlacementState() {
74 1
        this(true);
75 1
    }
76
77 1
    public PlacementState(boolean randomize) {
78 1
        this.randomize = randomize;
79 1
    }
80
81
    @Override
82
    public void start(Fight fight) {
83 1
        this.fight = fight;
84 1
        this.cellsGenerators = new HashMap<>();
85
86 1
        for (FightTeam team : fight.teams()) {
87 1
            cellsGenerators.put(
88
                team,
89 1
                randomize
90 1
                    ? PlacementCellsGenerator.randomized(fight.map(), team.startPlaces())
91 1
                    : new PlacementCellsGenerator(fight.map(), team.startPlaces())
92
            );
93 1
        }
94
95 1
        fight.dispatcher().register(this);
96 1
        startTime = System.currentTimeMillis();
97
98
        // Add all fighters to fight
99
        // Note: fight.fighters() cannot be used because at this state fighters are not yet on fight
100 1
        addFighters(fight.teams().stream().flatMap(team -> team.fighters().stream()).collect(Collectors.toList()));
101
102 1
        if (fight.type().hasPlacementTimeLimit()) {
103 1
            timer = fight.schedule(this::innerStartFight, fight.type().placementDuration());
104
        }
105 1
    }
106
107
    @Override
108
    public int id() {
109 1
        return 2;
110
    }
111
112
    @Override
113
    public Listener[] listeners() {
114 1
        if (listeners != null) {
115 1
            return listeners;
116
        }
117
118 1
        final Fight fight = this.fight;
0 ignored issues
show
Comprehensibility introduced by
The variable fightshadows a field with the same name declared in line 66. Consider renaming it.
Loading history...
119
120 1
        if (fight == null) {
121 1
            throw new IllegalStateException("State must be started");
122
        }
123
124 1
        return listeners = new Listener[] {
125
            new SendFighterPositions(fight),
126
            new SendFighterReadyState(fight),
127
            new StartFightWhenAllReady(fight, this),
128
            new SendNewFighter(fight),
129
            new ClearFighter(),
130
            new SendFighterRemoved(fight),
131
            new SendJoinTeamOptionChangedMessage(),
132
            new SendNeedHelpOptionChangedMessage(),
133
        };
134
    }
135
136
    /**
137
     * Get the remaining placement time, in milliseconds
138
     */
139
    public long remainingTime() {
140 1
        if (fight == null) {
141 1
            throw new IllegalStateException("State must be started");
142
        }
143
144 1
        if (!fight.type().hasPlacementTimeLimit()) {
145 1
            throw new UnsupportedOperationException("The fight has no placement time limit");
146
        }
147
148 1
        return fight.type().placementDuration().toMillis() + startTime - System.currentTimeMillis();
149
    }
150
151
    /**
152
     * Try to change fighter start place
153
     *
154
     * @param fighter Fighter to move
155
     * @param cell The target cell
156
     */
157
    public synchronized void changePlace(Fighter fighter, FightCell cell) {
158 1
        if (invalidState()) {
159 1
            return;
160
        }
161
162 1
        if (fighter.ready()) {
163 1
            throw new FightException("The fighter is ready");
164
        }
165
166 1
        if (!cell.walkable()) {
167 1
            throw new FightMapException("Not walkable");
168
        }
169
170 1
        if (!fighter.team().startPlaces().contains(cell)) {
171 1
            throw new FightException("Bad start cell");
172
        }
173
174 1
        fighter.move(cell);
175 1
        fight.dispatch(new FighterPlaceChanged(fighter));
176 1
    }
177
178
    /**
179
     * Try to join a team
180
     *
181
     * @param fighter The fighter to add
182
     * @param team The team
183
     *
184
     * @throws JoinFightException When cannot join the team
185
     */
186
    public synchronized void joinTeam(Fighter fighter, FightTeam team) throws JoinFightException {
187 1
        if (invalidState()) {
188 1
            throw new JoinFightException(JoinFightError.CANT_DO_TOO_LATE);
189
        }
190
191 1
        team.join(fighter);
192 1
        addFighters(Collections.singleton(fighter));
193 1
    }
194
195
    @Override
196
    public synchronized void leave(Fighter fighter) {
197 1
        if (invalidState()) {
198 1
            throw new InvalidFightStateException(getClass());
199
        }
200
201
        // Not allowed to leave the fight : punish the fighter
202 1
        if (!fight.type().canCancel()) {
203 1
            punishDeserter(fighter);
204
        }
205
206
        // Remove fighter
207 1
        leaveFromFight(fighter);
208 1
    }
209
210
    /**
211
     * Kick a fighter during placement
212
     * Unlike leave, this method will not punish the fighter
213
     *
214
     * @param fighter Fighter to kick
215
     *
216
     * @throws InvalidFightStateException When the session state has changed
217
     */
218
    public synchronized void kick(Fighter fighter) {
219 1
        if (invalidState()) {
220 1
            throw new InvalidFightStateException(getClass());
221
        }
222
223
        // Remove fighter
224 1
        leaveFromFight(fighter);
225 1
    }
226
227
    /**
228
     * Manually start the fight
229
     */
230
    public synchronized void startFight() {
231
        // Try to cancel the timer
232 1
        if (timer != null) {
233 1
            if (!timer.cancel(false)) {
234
                return; // Should not occurs : the fight is already started by the timer
235
            }
236
        }
237
238 1
        innerStartFight();
239 1
    }
240
241
    /**
242
     * Start the fight
243
     */
244
    private void innerStartFight() {
245 1
        if (invalidState()) {
246 1
            return;
247
        }
248
249 1
        fight.dispatcher().unregister(this);
250 1
        fight.nextState();
251 1
    }
252
253
    /**
254
     * Leave from a fight :
255
     * - If the fighter is the team leader, the team is dissolved
256
     * - The fighter is removed
257
     * - Check if the fight is valid (has at least two teams)
258
     */
259
    @RequiresNonNull("fight")
260
    private void leaveFromFight(Fighter fighter) {
261
        // The team leader quit the fight => Dissolve team
262 1
        if (fighter.isTeamLeader()) {
263 1
            fight.teams().remove(fighter.team());
264 1
            fighter.team().fighters().forEach(this::removeFighter);
265
        } else {
266 1
            fighter.team().kick(fighter);
267 1
            removeFighter(fighter);
268
        }
269
270 1
        checkFightValid();
271 1
    }
272
273
    /**
274
     * Punish the deserter fighter
275
     */
276
    @RequiresNonNull("fight")
277
    private void punishDeserter(Fighter fighter) {
278 1
        final FightRewardsSheet rewardsSheet = fight.type().rewards().generate(
279
            new EndFightResults(
280
                fight,
281 1
                Collections.emptyList(),
282 1
                Collections.singletonList(fighter)
283
            )
284
        );
285
286 1
        fighter.dispatch(new FightLeaved(rewardsSheet.rewards().get(0)));
287 1
    }
288
289
    @RequiresNonNull("fight")
290
    @SuppressWarnings("dereference.of.nullable") // cellsGenerators.get(fighter.team()) cannot be null
291
    private void addFighters(Collection<Fighter> fighters) {
292 1
        for (Fighter fighter : fighters) {
293 1
            fighter.joinFight(fight, NullnessUtil.castNonNull(cellsGenerators).get(fighter.team()).next());
294 1
        }
295
296 1
        for (Fighter fighter : fighters) {
297 1
            fighter.dispatch(new FightJoined(fight, fighter));
298 1
        }
299
300 1
        for (Fighter fighter : fighters) {
301 1
            fight.dispatch(new FighterAdded(fighter));
302 1
        }
303 1
    }
304
305
    /**
306
     * Notify fight that a fighter has leave
307
     */
308
    @RequiresNonNull("fight")
309
    private void removeFighter(Fighter fighter) {
310 1
        fighter.dispatch(new FightLeaved());
311 1
        fight.dispatch(new FighterRemoved(fighter, fight));
312 1
    }
313
314
    /**
315
     * Check if the fight is valid after fighter leaved
316
     */
317
    @RequiresNonNull("fight")
318
    private void checkFightValid() {
319 1
        if (fight.teams().stream().filter(FightTeam::alive).count() > 1) {
320 1
            return;
321
        }
322
323 1
        fight.cancel();
324
325 1
        if (timer != null) {
326 1
            timer.cancel(true);
327
        }
328 1
    }
329
330
    /**
331
     * Check if the fight state is not placement
332
     * This method will return true is the state is active (or following), or if it's cancelled or finished
333
     */
334
    @EnsuresNonNullIf(expression = "fight", result = false)
335
    private boolean invalidState() {
336 1
        return fight == null || fight.state() != this || !fight.alive();
337
    }
338
}
339