Test Failed
Push — master ( 4204bb...4b507f )
by Jakub
02:18
created

CombatBase   F

Complexity

Total Complexity 66

Size/Duplication

Total Lines 386
Duplicated Lines 0 %

Test Coverage

Coverage 94.01%

Importance

Changes 29
Bugs 0 Features 5
Metric Value
wmc 66
eloc 169
c 29
b 0
f 5
dl 0
loc 386
ccs 157
cts 167
cp 0.9401
rs 3.12

70 Methods

Rating   Name   Duplication   Size   Complexity  
resetInitiative() 0 5 ?
A hp$0 ➔ setDuelParticipants() 0 6 1
getTeam2() 0 2 ?
A hp$0 ➔ getHealers() 0 2 1
getWinner() 0 7 ?
A hp$0 ➔ recalculateStats() 0 5 2
A hp$0 ➔ setRoundLimit() 0 2 1
logRoundNumber() 0 2 ?
A hp$0 ➔ getCombatActions() 0 2 1
decreaseEffectsDuration() 0 7 ?
A hp$0 ➔ registerDefaultHandlers() 0 13 1
applyEffectProviders() 0 5 ?
A hp$0 ➔ getRound() 0 2 1
A hp$0 ➔ getTeam1() 0 2 1
getVictoryCondition() 0 2 ?
getTeam() 0 2 ?
A hp$0 ➔ logRoundNumber() 0 2 1
A hp$0 ➔ decreaseEffectsDuration() 0 7 4
A hp$0 ➔ execute() 0 18 5
assignPositions() 0 28 ?
logCombatResult() 0 8 ?
getRoundLimit() 0 2 ?
getRound() 0 2 ?
removeCombatEffects() 0 5 ?
setDuelParticipants() 0 6 ?
setRoundLimit() 0 2 ?
A hp$0 ➔ __construct() 0 13 1
getCombatActions() 0 2 ?
A hp$0 ➔ decreaseSkillsCooldowns() 0 6 3
registerDefaultHandlers() 0 13 ?
setVictoryCondition() 0 2 ?
A hp$0 ➔ getVictoryCondition() 0 2 1
setTeams() 0 7 ?
getEnemyTeam() 0 2 ?
getLog() 0 2 ?
A hp$0 ➔ getRoundLimit() 0 2 1
A hp$0 ➔ getTeam2() 0 2 1
A hp$0 ➔ setTeams() 0 7 2
A hp$0 ➔ setHealers() 0 2 1
A hp$0 ➔ removeCombatEffects() 0 5 2
A hp$0 ➔ getLog() 0 2 1
recalculateStats() 0 5 ?
A hp$0 ➔ getEnemyTeam() 0 2 2
A hp$0 ➔ logDamage() 0 3 2
execute() 0 18 ?
A hp$0 ➔ applyEffectProviders() 0 5 2
registerDefaultCombatActions() 0 5 ?
logDamage() 0 3 ?
A hp$0 ➔ setVictoryCondition() 0 2 1
getTeam1() 0 2 ?
A hp$0 ➔ applyPoison() 0 14 2
A hp$0 ➔ registerDefaultCombatActions() 0 5 1
A hp$0 ➔ logCombatResult() 0 8 2
A hp$0 ➔ resetInitiative() 0 5 2
__construct() 0 13 ?
A hp$0 ➔ getWinner() 0 7 2
B hp$0 ➔ assignPositions() 0 28 7
getTeam1Damage() 0 2 ?
A hp$0 ➔ getTeam() 0 2 2
getHealers() 0 2 ?
decreaseSkillsCooldowns() 0 6 ?
setHealers() 0 2 ?
applyPoison() 0 14 ?
A hp$0 ➔ getTeam1Damage() 0 2 1
selectAttackTarget() 0 17 ?
A hp$0 ➔ selectAttackTarget() 0 17 4
mainStage() 0 13 ?
A hp$0 ➔ getTeam2Damage() 0 2 1
A hp$0 ➔ mainStage() 0 13 3
getTeam2Damage() 0 2 ?

How to fix   Complexity   

Complex Class

Complex classes like CombatBase 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.

While breaking up the class, it is a good idea to analyze how other classes use CombatBase, and based on these observations, apply Extract Interface, too.

1
<?php
2
declare(strict_types=1);
3
4
namespace HeroesofAbenez\Combat;
5
6
use Nexendrie\Utils\Numbers;
7
use Nexendrie\Utils\Collection;
8
9
/**
10
 * Handles combat
11
 * 
12
 * @author Jakub Konečný
13
 * @property-read CombatLogger $log Log from the combat
14
 * @property-read int $winner Team which won the combat/0 if there is no winner yet
15
 * @property-read int $round Number of current round
16
 * @property int $roundLimit
17
 * @property Team $team1
18
 * @property Team $team2
19
 * @property-read int $team1Damage
20
 * @property-read int $team2Damage
21
 * @property Collection|ICombatAction[] $combatActions
22
 * @property callable $victoryCondition To evaluate the winner of combat. Gets combat as parameter, should return winning team (1/2) or 0 if there is not winner (yet)
0 ignored issues
show
introduced by
Line exceeds 120 characters; contains 166 characters
Loading history...
23
 * @property callable $healers To determine characters that are supposed to heal their team. Gets team1 and team2 as parameters, should return Team
0 ignored issues
show
introduced by
Line exceeds 120 characters; contains 147 characters
Loading history...
24
 * @method void onCombatStart(CombatBase $combat)
25
 * @method void onCombatEnd(CombatBase $combat)
26
 * @method void onRoundStart(CombatBase $combat)
27
 * @method void onRound(CombatBase $combat)
28
 * @method void onRoundEnd(CombatBase $combat)
29
 */
30 1
class CombatBase {
31
  use \Nette\SmartObject;
32
33
  protected Team $team1;
34
  protected Team $team2;
35
  protected CombatLogger $log;
36
  protected int $round = 0;
37
  protected int $roundLimit = 30;
38
  /** @var int[] Dealt damage by team */
39
  protected array $damage = [1 => 0, 2 => 0];
40
  /** @var callable[] */
41
  public array $onCombatStart = [];
42
  /** @var callable[] */
43
  public array $onCombatEnd = [];
44
  /** @var callable[] */
45
  public array $onRoundStart = [];
46
  /** @var callable[] */
47
  public array $onRound = [];
48
  /** @var callable[] */
49
  public array $onRoundEnd = [];
50
  /** @var callable */
51
  protected $victoryCondition;
52
  /** @var callable */
53
  protected $healers;
54
  public ISuccessCalculator $successCalculator;
55
  public ICombatActionSelector $actionSelector;
56
  /** @var Collection|ICombatAction[] */
57
  protected Collection $combatActions;
58
  
59
  public function __construct(CombatLogger $logger, ?ISuccessCalculator $successCalculator = null, ?ICombatActionSelector $actionSelector = null) {
0 ignored issues
show
introduced by
Line exceeds 120 characters; contains 147 characters
Loading history...
60 1
    $this->log = $logger;
61 1
    $this->victoryCondition = [VictoryConditions::class, "moreDamage"];
62 1
    $this->successCalculator = $successCalculator ?? new RandomSuccessCalculator();
63 1
    $this->actionSelector = $actionSelector ?? new CombatActionSelector();
64 1
    $this->healers = function(): Team {
65
      return new Team("healers");
66
    };
67 1
    $this->combatActions = new class extends Collection {
68
      protected string $class = ICombatAction::class;
69
    };
70 1
    $this->registerDefaultHandlers();
71 1
    $this->registerDefaultCombatActions();
72 1
  }
73
74
  protected function registerDefaultHandlers(): void {
75 1
    $this->onCombatStart[] = [$this, "assignPositions"];
76 1
    $this->onCombatEnd[] = [$this, "removeCombatEffects"];
77 1
    $this->onCombatEnd[] = [$this, "logCombatResult"];
78 1
    $this->onCombatEnd[] = [$this, "resetInitiative"];
79 1
    $this->onRoundStart[] = [$this, "applyEffectProviders"];
80 1
    $this->onRoundStart[] = [$this, "decreaseEffectsDuration"];
81 1
    $this->onRoundStart[] = [$this, "recalculateStats"];
82 1
    $this->onRoundStart[] = [$this, "logRoundNumber"];
83 1
    $this->onRoundStart[] = [$this, "applyPoison"];
84 1
    $this->onRound[] = [$this, "mainStage"];
85 1
    $this->onRoundEnd[] = [$this, "decreaseSkillsCooldowns"];
86 1
    $this->onRoundEnd[] = [$this, "resetInitiative"];
87 1
  }
88
89
  protected function registerDefaultCombatActions(): void {
90 1
    $this->combatActions[] = new CombatActions\Attack();
91 1
    $this->combatActions[] = new CombatActions\Heal();
92 1
    $this->combatActions[] = new CombatActions\SkillAttack();
93 1
    $this->combatActions[] = new CombatActions\SkillSpecial();
94 1
  }
95
  
96
  public function getRound(): int {
97 1
    return $this->round;
98
  }
99
  
100
  public function getRoundLimit(): int {
101 1
    return $this->roundLimit;
102
  }
103
104
  public function setRoundLimit(int $roundLimit): void {
105
    $this->roundLimit = Numbers::range($roundLimit, 1, PHP_INT_MAX);
106
  }
107
108
  /**
109
   * Set teams
110
   */
111
  public function setTeams(Team $team1, Team $team2): void {
112 1
    if(isset($this->team1)) {
113 1
      throw new ImmutableException("Teams has already been set.");
114
    }
115 1
    $this->team1 = $team1;
116 1
    $this->team2 = $team2;
117 1
    $this->log->setTeams($team1, $team2);
118 1
  }
119
  
120
  /**
121
   * Set participants for duel
122
   * Creates teams named after the member
123
   */
124
  public function setDuelParticipants(Character $player, Character $opponent): void {
125 1
    $team1 = new Team($player->name);
126 1
    $team1[] = $player;
127 1
    $team2 = new Team($opponent->name);
128 1
    $team2[] = $opponent;
129 1
    $this->setTeams($team1, $team2);
130 1
  }
131
  
132
  public function getTeam1(): Team {
133 1
    return $this->team1;
134
  }
135
  
136
  public function getTeam2(): Team {
137 1
    return $this->team2;
138
  }
139
  
140
  public function getVictoryCondition(): callable {
141 1
    return $this->victoryCondition;
142
  }
143
  
144
  public function setVictoryCondition(callable $victoryCondition): void {
145 1
    $this->victoryCondition = $victoryCondition;
146 1
  }
147
  
148
  public function getHealers(): callable {
149 1
    return $this->healers;
150
  }
151
  
152
  public function setHealers(callable $healers): void {
153 1
    $this->healers = $healers;
154 1
  }
155
  
156
  public function getTeam1Damage(): int {
157 1
    return $this->damage[1];
158
  }
159
  
160
  public function getTeam2Damage(): int {
161
    return $this->damage[2];
162
  }
163
164
  /**
165
   * @return Collection|ICombatAction[]
166
   */
167
  public function getCombatActions(): Collection {
168 1
    return $this->combatActions;
169
  }
170
171
  /**
172
   * Get winner of combat
173
   * 
174
   * @staticvar int $result
175
   * @return int Winning team/0
176
   */
177
  public function getWinner(): int {
178 1
    static $result = 0;
179 1
    if($result === 0) {
180 1
      $result = call_user_func($this->victoryCondition, $this);
181 1
      $result = Numbers::range($result, 0, 2);
182
    }
183 1
    return $result;
184
  }
185
186
  /**
187
   * @internal
188
   */
189
  public function getTeam(Character $character): Team {
190 1
    return $this->team1->hasItems(["id" => $character->id]) ? $this->team1 : $this->team2;
191
  }
192
193
  /**
194
   * @internal
195
   */
196
  public function getEnemyTeam(Character $character): Team {
197 1
    return $this->team1->hasItems(["id" => $character->id]) ? $this->team2 : $this->team1;
198
  }
199
  
200
  public function applyEffectProviders(self $combat): void {
201
    /** @var Character[] $characters */
202 1
    $characters = array_merge($combat->team1->toArray(), $combat->team2->toArray());
203 1
    foreach($characters as $character) {
204 1
      $character->applyEffectProviders();
205
    }
206 1
  }
207
208
  public function assignPositions(self $combat): void {
209 1
    $assignPositions = function(Team $team): void {
210 1
      $row = 1;
211 1
      $column = 0;
212
      /** @var Character $character */
213 1
      foreach($team as $character) {
214
        try {
215 1
          $column++;
216 1
          if($character->positionRow > 0 && $character->positionColumn > 0) {
217 1
            continue;
218
          }
219
          setPosition:
0 ignored issues
show
introduced by
Use of the GOTO language construct is discouraged
Loading history...
220 1
          $team->setCharacterPosition($character->id, $row, $column);
221 1
        } catch(InvalidCharacterPositionException $e) {
222 1
          if($e->getCode() === InvalidCharacterPositionException::ROW_FULL) {
223 1
            $row++;
224 1
            $column = 1;
225
          } elseif($e->getCode() === InvalidCharacterPositionException::POSITION_OCCUPIED) {
226
            $column++;
227
          } else {
228
            throw $e;
229
          }
230 1
          goto setPosition;
0 ignored issues
show
introduced by
Use of the GOTO language construct is discouraged
Loading history...
231
        }
232
      }
233 1
    };
234 1
    $assignPositions($combat->team1);
235 1
    $assignPositions($combat->team2);
236 1
  }
237
  
238
  public function decreaseEffectsDuration(self $combat): void {
239
    /** @var Character[] $characters */
240 1
    $characters = array_merge($combat->team1->toArray(), $combat->team2->toArray());
241 1
    foreach($characters as $character) {
242 1
      foreach($character->effects as $effect) {
243 1
        if(is_int($effect->duration)) {
244 1
          $effect->duration--;
245
        }
246
      }
247
    }
248 1
  }
249
  
250
  /**
251
   * Decrease skills' cooldowns
252
   */
253
  public function decreaseSkillsCooldowns(self $combat): void {
254
    /** @var Character[] $characters */
255 1
    $characters = array_merge($combat->team1->toArray(), $combat->team2->toArray());
256 1
    foreach($characters as $character) {
257 1
      foreach($character->skills as $skill) {
258 1
        $skill->decreaseCooldown();
259
      }
260
    }
261 1
  }
262
  
263
  /**
264
   * Remove combat effects from character at the end of the combat
265
   */
266
  public function removeCombatEffects(self $combat): void {
267
    /** @var Character[] $characters */
268 1
    $characters = array_merge($combat->team1->toArray(), $combat->team2->toArray());
269 1
    foreach($characters as $character) {
270 1
      $character->effects->removeByFilter(["duration!=" => CharacterEffect::DURATION_FOREVER]);
271
    }
272 1
  }
273
  
274
  /**
275
   * Add winner to the log
276
   */
277
  public function logCombatResult(self $combat): void {
278 1
    $combat->log->round = 5000;
279
    $params = [
280 1
      "team1name" => $combat->team1->name, "team1damage" => $combat->damage[1],
281 1
      "team2name" => $combat->team2->name, "team2damage" => $combat->damage[2],
282
    ];
283 1
    $params["winner"] = ($combat->winner === 1) ? $combat->team1->name : $combat->team2->name;
284 1
    $combat->log->logText("combat.log.combatEnd", $params);
285 1
  }
286
  
287
  /**
288
   * Log start of a round
289
   */
290
  public function logRoundNumber(self $combat): void {
291 1
    $combat->log->round = ++$this->round;
292 1
  }
293
  
294
  /**
295
   * Decrease duration of effects and recalculate stats
296
   */
297
  public function recalculateStats(self $combat): void {
298
    /** @var Character[] $characters */
299 1
    $characters = array_merge($combat->team1->toArray(), $combat->team2->toArray());
300 1
    foreach($characters as $character) {
301 1
      $character->recalculateStats();
302
    }
303 1
  }
304
  
305
  /**
306
   * Reset characters' initiative
307
   */
308
  public function resetInitiative(self $combat): void {
309
    /** @var Character[] $characters */
310 1
    $characters = array_merge($combat->team1->toArray(), $combat->team2->toArray());
311 1
    foreach($characters as $character) {
312 1
      $character->resetInitiative();
313
    }
314 1
  }
315
  
316
  /**
317
   * Select target for attack
318
   *
319
   * @internal
320
   */
321
  public function selectAttackTarget(Character $attacker): ?Character {
322 1
    $enemyTeam = $this->getEnemyTeam($attacker);
323 1
    $rangedWeapon = ($attacker->equipment->hasItems(["%class%" => Weapon::class, "worn" => true, "ranged" => true, ]));
324 1
    if(!$rangedWeapon) {
325 1
      $rowToAttack = $enemyTeam->rowToAttack;
326 1
      if($rowToAttack === null) {
327
        return null;
328
      }
329 1
      $enemies = Team::fromArray($enemyTeam->getItems(["positionRow" => $rowToAttack, "hitpoints>" => 0, "hidden" => false, ]), $enemyTeam->name);
0 ignored issues
show
introduced by
Line exceeds 120 characters; contains 146 characters
Loading history...
330
    } else {
331
      $enemies = $enemyTeam;
332
    }
333 1
    $target = $enemies->getLowestHpCharacter();
334 1
    if($target !== null) {
335 1
      return $target;
336
    }
337 1
    return $enemies->getRandomCharacter();
338
  }
339
  
340
  /**
341
   * Main stage of a round
342
   *
343
   * @throws NotImplementedException
344
   */
345
  public function mainStage(self $combat): void {
346
    /** @var Character[] $characters */
347 1
    $characters = array_merge($combat->team1->usableMembers, $combat->team2->usableMembers);
348 1
    usort($characters, function(Character $a, Character $b): int {
349 1
      return -1 * strcmp((string) $a->initiative, (string) $b->initiative);
350 1
    });
351 1
    foreach($characters as $character) {
352
      /** @var ICombatAction|null $combatAction */
353 1
      $combatAction = $combat->actionSelector->chooseAction($combat, $character);
354 1
      if($combatAction === null) {
355 1
        break;
356
      }
357 1
      $combatAction->do($combat, $character);
358
    }
359 1
  }
360
  
361
  /**
362
   * Executes the combat
363
   * 
364
   * @return int Winning team
365
   */
366
  public function execute(): int {
367 1
    if(!isset($this->team1)) {
368 1
      throw new InvalidStateException("Teams are not set.");
369
    }
370 1
    $this->onCombatStart($this);
371 1
    while($this->round <= $this->roundLimit) {
372 1
      $this->onRoundStart($this);
373 1
      if($this->getWinner() > 0) {
374
        break;
375
      }
376 1
      $this->onRound($this);
377 1
      $this->onRoundEnd($this);
378 1
      if($this->getWinner() > 0) {
379 1
        break;
380
      }
381
    }
382 1
    $this->onCombatEnd($this);
383 1
    return $this->getWinner();
384
  }
385
386
  /**
387
   * Harm poisoned characters at start of round
388
   */
389
  public function applyPoison(self $combat): void {
390
    /** @var Character[] $characters */
391 1
    $characters = array_merge(
392 1
        $combat->team1->getItems(["hitpoints>" => 0, "poisoned!=" => false, ]),
393 1
        $combat->team2->getItems(["hitpoints>" => 0, "poisoned!=" => false, ])
394
    );
395 1
    foreach($characters as $character) {
396 1
      $poisonValue = $character->getStatus(Character::STATUS_POISONED);
397 1
      $character->harm($poisonValue);
398
      $action = [
399 1
        "action" => CombatLogEntry::ACTION_POISON, "result" => true, "amount" => $poisonValue,
400 1
        "character1" => $character, "character2" => $character,
401
      ];
402 1
      $combat->log->log($action);
403
    }
404 1
  }
405
  
406
  /**
407
   * Log dealt damage
408
   */
409
  public function logDamage(Character $attacker, int $amount): void {
410 1
    $team = $this->team1->hasItems(["id" => $attacker->id]) ? 1 : 2;
411 1
    $this->damage[$team] += $amount;
412 1
  }
413
  
414
  public function getLog(): CombatLogger {
415 1
    return $this->log;
416
  }
417
}
418
?>