Passed
Push — master ( dd3471...ff1e5a )
by Jakub
02:42
created

CombatBase.php$0 ➔ setSkillsCooldowns()   A

Complexity

Conditions 3

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3

Importance

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