Passed
Pull Request — master (#203)
by Vincent
14:28 queued 03:42
created

fr.quatrevieux.araknemu.game.fight.state.PlacementState   A

Complexity

Total Complexity 40

Size/Duplication

Total Lines 253
Duplicated Lines 0 %

Test Coverage

Coverage 98.92%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 116
dl 0
loc 253
ccs 92
cts 93
cp 0.9892
rs 9.2
c 1
b 0
f 0
wmc 40

18 Methods

Rating   Name   Duplication   Size   Complexity  
A joinTeam(Fighter,FightTeam) 0 7 2
A changePlace(Fighter,FightCell) 0 19 5
A listeners() 0 15 2
A PlacementState(boolean) 0 2 1
A PlacementState() 0 2 1
A id() 0 3 1
A start(Fight) 0 20 4
A remainingTime() 0 6 2
A leave(Fighter) 0 13 3
A checkFightValid() 0 9 3
A punishDeserter(Fighter) 0 10 1
A kick(Fighter) 0 7 2
A startFight() 0 9 3
A invalidState() 0 2 1
A innerStartFight() 0 7 2
A leaveFromFight(Fighter) 0 11 2
A removeFighter(Fighter) 0 3 1
A addFighters(Collection) 0 11 4

How to fix   Complexity   

Complexity

Complex classes like fr.quatrevieux.araknemu.game.fight.state.PlacementState often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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
50
import java.util.Collection;
51
import java.util.Collections;
52
import java.util.HashMap;
53
import java.util.Map;
54
import java.util.concurrent.ScheduledFuture;
55
56
/**
57
 * Placement before start fight
58
 */
59
public final class PlacementState implements LeavableState, EventsSubscriber {
60
    private long startTime;
61
    private Fight fight;
62
    private Listener[] listeners;
63
    private Map<FightTeam, PlacementCellsGenerator> cellsGenerators;
64
    private ScheduledFuture<?> timer;
65
66
    private final boolean randomize;
67
68
    public PlacementState() {
69 1
        this(true);
70 1
    }
71
72 1
    public PlacementState(boolean randomize) {
73 1
        this.randomize = randomize;
74 1
    }
75
76
    @Override
77
    public void start(Fight fight) {
78 1
        this.fight = fight;
79 1
        this.cellsGenerators = new HashMap<>();
80
81 1
        for (FightTeam team : fight.teams()) {
82 1
            cellsGenerators.put(
83
                team,
84
                randomize
85 1
                    ? PlacementCellsGenerator.randomized(fight.map(), team.startPlaces())
86 1
                    : new PlacementCellsGenerator(fight.map(), team.startPlaces())
87
            );
88 1
        }
89
90 1
        fight.dispatcher().register(this);
91 1
        startTime = System.currentTimeMillis();
92 1
        addFighters(fight.fighters(false));
93
94 1
        if (fight.type().hasPlacementTimeLimit()) {
95 1
            timer = fight.schedule(this::innerStartFight, fight.type().placementDuration());
96
        }
97 1
    }
98
99
    @Override
100
    public int id() {
101 1
        return 2;
102
    }
103
104
    @Override
105
    public Listener[] listeners() {
106 1
        if (listeners != null) {
107 1
            return listeners;
108
        }
109
110 1
        return listeners = new Listener[] {
111
            new SendFighterPositions(fight),
112
            new SendFighterReadyState(fight),
113
            new StartFightWhenAllReady(fight, this),
114
            new SendNewFighter(fight),
115
            new ClearFighter(),
116
            new SendFighterRemoved(fight),
117
            new SendJoinTeamOptionChangedMessage(),
118
            new SendNeedHelpOptionChangedMessage(),
119
        };
120
    }
121
122
    /**
123
     * Get the remaining placement time, in milliseconds
124
     */
125
    public long remainingTime() {
126 1
        if (!fight.type().hasPlacementTimeLimit()) {
127 1
            throw new UnsupportedOperationException("The fight has no placement time limit");
128
        }
129
130 1
        return fight.type().placementDuration().toMillis() + startTime - System.currentTimeMillis();
131
    }
132
133
    /**
134
     * Try to change fighter start place
135
     *
136
     * @param fighter Fighter to move
137
     * @param cell The target cell
138
     */
139
    public synchronized void changePlace(Fighter fighter, FightCell cell) {
140 1
        if (invalidState()) {
141 1
            return;
142
        }
143
144 1
        if (fighter.ready()) {
145 1
            throw new FightException("The fighter is ready");
146
        }
147
148 1
        if (!cell.walkable()) {
149 1
            throw new FightMapException("Not walkable");
150
        }
151
152 1
        if (!fighter.team().startPlaces().contains(cell.id())) {
153 1
            throw new FightException("Bad start cell");
154
        }
155
156 1
        fighter.move(cell);
157 1
        fight.dispatch(new FighterPlaceChanged(fighter));
158 1
    }
159
160
    /**
161
     * Try to join a team
162
     *
163
     * @param fighter The fighter to add
164
     * @param team The team
165
     *
166
     * @throws JoinFightException When cannot join the team
167
     */
168
    public synchronized void joinTeam(Fighter fighter, FightTeam team) throws JoinFightException {
169 1
        if (invalidState()) {
170 1
            throw new JoinFightException(JoinFightError.CANT_DO_TOO_LATE);
171
        }
172
173 1
        team.join(fighter);
174 1
        addFighters(Collections.singleton(fighter));
175 1
    }
176
177
    @Override
178
    public synchronized void leave(Fighter fighter) {
179 1
        if (invalidState()) {
180 1
            throw new InvalidFightStateException(getClass());
181
        }
182
183
        // Not allowed to leave the fight : punish the fighter
184 1
        if (!fight.type().canCancel()) {
185 1
            punishDeserter(fighter);
186
        }
187
188
        // Remove fighter
189 1
        leaveFromFight(fighter);
190 1
    }
191
192
    /**
193
     * Kick a fighter during placement
194
     * Unlike leave, this method will not punish the fighter
195
     *
196
     * @param fighter Fighter to kick
197
     *
198
     * @throws InvalidFightStateException When the session state has changed
199
     */
200
    public synchronized void kick(Fighter fighter) {
201 1
        if (invalidState()) {
202 1
            throw new InvalidFightStateException(getClass());
203
        }
204
205
        // Remove fighter
206 1
        leaveFromFight(fighter);
207 1
    }
208
209
    /**
210
     * Manually start the fight
211
     */
212
    public synchronized void startFight() {
213
        // Try to cancel the timer
214 1
        if (timer != null) {
215 1
            if (!timer.cancel(false)) {
216
                return; // Should not occurs : the fight is already started by the timer
217
            }
218
        }
219
220 1
        innerStartFight();
221 1
    }
222
223
    /**
224
     * Start the fight
225
     */
226
    private void innerStartFight() {
227 1
        if (invalidState()) {
228 1
            return;
229
        }
230
231 1
        fight.dispatcher().unregister(this);
232 1
        fight.nextState();
233 1
    }
234
235
    /**
236
     * Leave from a fight :
237
     * - If the fighter is the team leader, the team is dissolved
238
     * - The fighter is removed
239
     * - Check if the fight is valid (has at least two teams)
240
     */
241
    private void leaveFromFight(Fighter fighter) {
242
        // The team leader quit the fight => Dissolve team
243 1
        if (fighter.isTeamLeader()) {
244 1
            fight.teams().remove(fighter.team());
245 1
            fighter.team().fighters().forEach(this::removeFighter);
246
        } else {
247 1
            fighter.team().kick(fighter);
248 1
            removeFighter(fighter);
249
        }
250
251 1
        checkFightValid();
252 1
    }
253
254
    /**
255
     * Punish the deserter fighter
256
     */
257
    private void punishDeserter(Fighter fighter) {
258 1
        final FightRewardsSheet rewardsSheet = fight.type().rewards().generate(
259
            new EndFightResults(
260
                fight,
261 1
                Collections.emptyList(),
262 1
                Collections.singletonList(fighter)
263
            )
264
        );
265
266 1
        fighter.dispatch(new FightLeaved(rewardsSheet.rewards().get(0)));
267 1
    }
268
269
    private void addFighters(Collection<Fighter> fighters) {
270 1
        for (Fighter fighter : fighters) {
271 1
            fighter.joinFight(fight, cellsGenerators.get(fighter.team()).next());
272 1
        }
273
274 1
        for (Fighter fighter : fighters) {
275 1
            fighter.dispatch(new FightJoined(fight, fighter));
276 1
        }
277
278 1
        for (Fighter fighter : fighters) {
279 1
            fight.dispatch(new FighterAdded(fighter));
280 1
        }
281 1
    }
282
283
    /**
284
     * Notify fight that a fighter has leave
285
     */
286
    private void removeFighter(Fighter fighter) {
287 1
        fighter.dispatch(new FightLeaved());
288 1
        fight.dispatch(new FighterRemoved(fighter, fight));
289 1
    }
290
291
    /**
292
     * Check if the fight is valid after fighter leaved
293
     */
294
    private void checkFightValid() {
295 1
        if (fight.teams().stream().filter(FightTeam::alive).count() > 1) {
296 1
            return;
297
        }
298
299 1
        fight.cancel();
300
301 1
        if (timer != null) {
302 1
            timer.cancel(true);
303
        }
304 1
    }
305
306
    /**
307
     * Check if the fight state is not placement
308
     * This method will return true is the state is active (or following), or if it's cancelled or finished
309
     */
310
    private boolean invalidState() {
311 1
        return fight.state() != this || !fight.alive();
312
    }
313
}
314