Passed
Push — master ( 9ad4f6...7fb37f )
by Jakub
01:42
created

CombatBase.php$0 ➔ getCombatActions()   A

Complexity

Conditions 1

Size

Total Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

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