Completed
Push — master ( 843ccd...b2206c )
by Jakub
02:38
created

CombatBase::applyEffectProviders()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 6
cts 6
cp 1
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 6
nc 3
nop 1
crap 3
1
<?php
2
declare(strict_types=1);
3
4
namespace HeroesofAbenez\Combat;
5
6
use Nexendrie\Utils\Numbers;
7
8
/**
9
 * Handles combat
10
 * 
11
 * @author Jakub Konečný
12
 * @property-read CombatLogger $log Log from the combat
13
 * @property-read int $winner Team which won the combat/0 if there is no winner yet
14
 * @property-read int $round Number of current round
15
 * @property-read int $roundLimit
16
 * @property-read Team $team1
17
 * @property-read Team $team2
18
 * @property-read int $team1Damage
19
 * @property-read int $team2Damage
20
 * @property ISuccessCalculator $successCalculator
21
 * @property ICombatActionSelector $actionSelector
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
 * @method void onAttack(Character $attacker, Character $defender)
30
 * @method void onSkillAttack(Character $attacker, Character $defender, CharacterAttackSkill $skill)
31
 * @method void onSkillSpecial(Character $character1, Character $target, CharacterSpecialSkill $skill)
32
 * @method void onHeal(Character $healer, Character $patient)
33
 */
34 1
class CombatBase {
35 1
  use \Nette\SmartObject;
36
  
37
  /** @var Team First team */
38
  protected $team1;
39
  /** @var Team Second team */
40
  protected $team2;
41
  /** @var CombatLogger */
42
  protected $log;
43
  /** @var int Number of current round */
44
  protected $round = 0;
45
  /** @var int Round limit */
46
  protected $roundLimit = 30;
47
  /** @var array Dealt damage by team */
48
  protected $damage = [1 => 0, 2 => 0];
49
  /** @var callable[] */
50
  public $onCombatStart = [];
51
  /** @var callable[] */
52
  public $onCombatEnd = [];
53
  /** @var callable[] */
54
  public $onRoundStart = [];
55
  /** @var callable[] */
56
  public $onRound = [];
57
  /** @var callable[] */
58
  public $onRoundEnd = [];
59
  /** @var callable[] */
60
  public $onAttack = [];
61
  /** @var callable[] */
62
  public $onSkillAttack = [];
63
  /** @var callable[] */
64
  public $onSkillSpecial = [];
65
  /** @var callable[] */
66
  public $onHeal = [];
67
  /** @var callable */
68
  protected $victoryCondition;
69
  /** @var callable */
70
  protected $healers;
71
  /** @var ISuccessCalculator */
72
  protected $successCalculator;
73
  /** @var ICombatActionSelector */
74
  protected $actionSelector;
75
  
76
  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...
77 1
    $this->log = $logger;
78 1
    $this->onCombatStart[] = [$this, "applyEffectProviders"];
79 1
    $this->onCombatStart[] = [$this, "setSkillsCooldowns"];
80 1
    $this->onCombatStart[] = [$this, "assignPositions"];
81 1
    $this->onCombatEnd[] = [$this, "removeCombatEffects"];
82 1
    $this->onCombatEnd[] = [$this, "logCombatResult"];
83 1
    $this->onCombatEnd[] = [$this, "resetInitiative"];
84 1
    $this->onRoundStart[] = [$this, "decreaseEffectsDuration"];
85 1
    $this->onRoundStart[] = [$this ,"recalculateStats"];
86 1
    $this->onRoundStart[] = [$this, "logRoundNumber"];
87 1
    $this->onRoundStart[] = [$this, "applyPoison"];
88 1
    $this->onRound[] = [$this, "mainStage"];
89 1
    $this->onRoundEnd[] = [$this, "decreaseSkillsCooldowns"];
90 1
    $this->onRoundEnd[] = [$this, "resetInitiative"];
91 1
    $this->onAttack[] = [$this, "attackHarm"];
92 1
    $this->onSkillAttack[] = [$this, "useAttackSkill"];
93 1
    $this->onSkillSpecial[] = [$this, "useSpecialSkill"];
94 1
    $this->onHeal[] = [$this, "heal"];
95 1
    $this->victoryCondition = [VictoryConditions::class, "moreDamage"];
96 1
    $this->successCalculator = $successCalculator ?? new RandomSuccessCalculator();
97 1
    $this->actionSelector = $actionSelector ?? new CombatActionSelector();
98 1
    $this->healers = function(): Team {
99
      return new Team("healers");
100
    };
101 1
  }
102
  
103
  public function getRound(): int {
104 1
    return $this->round;
105
  }
106
  
107
  public function getRoundLimit(): int {
108 1
    return $this->roundLimit;
109
  }
110
  
111
  /**
112
   * Set teams
113
   */
114
  public function setTeams(Team $team1, Team $team2): void {
115 1
    if(isset($this->team1)) {
116 1
      throw new ImmutableException("Teams has already been set.");
117
    }
118 1
    $this->team1 = & $team1;
119 1
    $this->team2 = & $team2;
120 1
    $this->log->setTeams($team1, $team2);
121 1
  }
122
  
123
  /**
124
   * Set participants for duel
125
   * Creates teams named after the member
126
   */
127
  public function setDuelParticipants(Character $player, Character $opponent): void {
128 1
    $team1 = new Team($player->name);
129 1
    $team1[] = $player;
130 1
    $team2 = new Team($opponent->name);
131 1
    $team2[] = $opponent;
132 1
    $this->setTeams($team1, $team2);
133 1
  }
134
  
135
  public function getTeam1(): Team {
136 1
    return $this->team1;
137
  }
138
  
139
  public function getTeam2(): Team {
140 1
    return $this->team2;
141
  }
142
  
143
  public function getVictoryCondition(): callable {
144 1
    return $this->victoryCondition;
145
  }
146
  
147
  public function setVictoryCondition(callable $victoryCondition) {
148 1
    $this->victoryCondition = $victoryCondition;
149 1
  }
150
  
151
  public function getHealers(): callable {
152
    return $this->healers;
153
  }
154
  
155
  public function setHealers(callable $healers) {
156 1
    $this->healers = $healers;
157 1
  }
158
  
159
  public function getTeam1Damage(): int {
160 1
    return $this->damage[1];
161
  }
162
  
163
  public function getTeam2Damage(): int {
164 1
    return $this->damage[2];
165
  }
166
  
167
  public function getSuccessCalculator(): ISuccessCalculator {
168 1
    return $this->successCalculator;
169
  }
170
  
171
  public function setSuccessCalculator(ISuccessCalculator $successCalculator): void {
172 1
    $this->successCalculator = $successCalculator;
173 1
  }
174
  
175
  public function getActionSelector(): ICombatActionSelector {
176 1
    return $this->actionSelector;
177
  }
178
  
179
  public function setActionSelector(ICombatActionSelector $actionSelector): void {
180 1
    $this->actionSelector = $actionSelector;
181 1
  }
182
  
183
  /**
184
   * Get winner of combat
185
   * 
186
   * @staticvar int $result
187
   * @return int Winning team/0
188
   */
189
  public function getWinner(): int {
190 1
    static $result = 0;
191 1
    if($result === 0) {
192 1
      $result = call_user_func($this->victoryCondition, $this);
193 1
      $result = Numbers::range($result, 0, 2);
194
    }
195 1
    return $result;
196
  }
197
  
198
  protected function getTeam(Character $character): Team {
199 1
    return $this->team1->hasItems(["id" => $character->id]) ? $this->team1 : $this->team2;
200
  }
201
  
202
  protected function getEnemyTeam(Character $character): Team {
203 1
    return $this->team1->hasItems(["id" => $character->id]) ? $this->team2 : $this->team1;
204
  }
205
  
206
  public function applyEffectProviders(self $combat): void {
207
    /** @var Character[] $characters */
208 1
    $characters = array_merge($combat->team1->toArray(), $combat->team2->toArray());
209 1
    foreach($characters as $character) {
210 1
      foreach($character->effectProviders as $item) {
211 1
        $effects = $item->getCombatEffects();
212 1
        array_walk($effects, function(CharacterEffect $effect) use($character) {
213 1
          $character->addEffect($effect);
214 1
        });
215
      }
216
    }
217 1
  }
218
  
219
  /**
220
   * Set skills' cooldowns
221
   */
222
  public function setSkillsCooldowns(self $combat): void {
223
    /** @var Character[] $characters */
224 1
    $characters = array_merge($combat->team1->toArray(), $combat->team2->toArray());
225 1
    foreach($characters as $character) {
226 1
      foreach($character->skills as $skill) {
227 1
        $skill->resetCooldown();
228
      }
229
    }
230 1
  }
231
  
232
  public function assignPositions(self $combat): void {
233 1
    $assignPositions = function(Team $team) {
234 1
      $row = 1;
235 1
      $column = 0;
236
      /** @var Character $character */
237 1
      foreach($team as $character) {
238
        try {
239 1
          $column++;
240 1
          if($character->positionRow > 0 AND $character->positionColumn > 0) {
241 1
            continue;
242
          }
243
          setPosition:
0 ignored issues
show
introduced by
Use of the GOTO language construct is discouraged
Loading history...
244 1
          $team->setCharacterPosition($character->id, $row, $column);
245 1
        } catch(InvalidCharacterPositionException $e) {
246 1
          if($e->getCode() === InvalidCharacterPositionException::ROW_FULL) {
247 1
            $row++;
248 1
            $column = 1;
249
          } elseif($e->getCode() === InvalidCharacterPositionException::POSITION_OCCUPIED) {
250
            $column++;
251
          } else {
252
            throw $e;
253
          }
254 1
          goto setPosition;
0 ignored issues
show
introduced by
Use of the GOTO language construct is discouraged
Loading history...
255
        }
256
      }
257 1
    };
258 1
    $assignPositions($combat->team1);
259 1
    $assignPositions($combat->team2);
260 1
  }
261
  
262
  public function decreaseEffectsDuration(self $combat): void {
263
    /** @var Character[] $characters */
264 1
    $characters = array_merge($combat->team1->toArray(), $combat->team2->toArray());
265 1
    foreach($characters as $character) {
266 1
      foreach($character->effects as $effect) {
267 1
        $effect->duration--;
268
      }
269
    }
270 1
  }
271
  
272
  /**
273
   * Decrease skills' cooldowns
274
   */
275
  public function decreaseSkillsCooldowns(self $combat): void {
276
    /** @var Character[] $characters */
277 1
    $characters = array_merge($combat->team1->toArray(), $combat->team2->toArray());
278 1
    foreach($characters as $character) {
279 1
      foreach($character->skills as $skill) {
280 1
        $skill->decreaseCooldown();
281
      }
282
    }
283 1
  }
284
  
285
  /**
286
   * Remove combat effects from character at the end of the combat
287
   */
288
  public function removeCombatEffects(self $combat): void {
289
    /** @var Character[] $characters */
290 1
    $characters = array_merge($combat->team1->toArray(), $combat->team2->toArray());
291 1
    foreach($characters as $character) {
292 1
      foreach($character->effects as $effect) {
293 1
        if($effect->duration === CharacterEffect::DURATION_COMBAT OR is_int($effect->duration)) {
294 1
          $character->removeEffect($effect->id);
295
        }
296
      }
297
    }
298 1
  }
299
  
300
  /**
301
   * Add winner to the log
302
   */
303
  public function logCombatResult(self $combat): void {
304 1
    $combat->log->round = 5000;
305
    $params = [
306 1
      "team1name" => $combat->team1->name, "team1damage" => $combat->damage[1],
307 1
      "team2name" => $combat->team2->name, "team2damage" => $combat->damage[2],
308
    ];
309 1
    if($combat->winner === 1) {
310
      $params["winner"] = $combat->team1->name;
311
    } else {
312 1
      $params["winner"] = $combat->team2->name;
313
    }
314 1
    $combat->log->logText("combat.log.combatEnd", $params);
315 1
  }
316
  
317
  /**
318
   * Log start of a round
319
   */
320
  public function logRoundNumber(self $combat): void {
321 1
    $combat->log->round = ++$this->round;
322 1
  }
323
  
324
  /**
325
   * Decrease duration of effects and recalculate stats
326
   */
327
  public function recalculateStats(self $combat): void {
328
    /** @var Character[] $characters */
329 1
    $characters = array_merge($combat->team1->toArray(), $combat->team2->toArray());
330 1
    foreach($characters as $character) {
331 1
      $character->recalculateStats();
332
    }
333 1
  }
334
  
335
  /**
336
   * Reset characters' initiative
337
   */
338
  public function resetInitiative(self $combat): void {
339
    /** @var Character[] $characters */
340 1
    $characters = array_merge($combat->team1->toArray(), $combat->team2->toArray());
341 1
    foreach($characters as $character) {
342 1
      $character->resetInitiative();
343
    }
344 1
  }
345
  
346
  /**
347
   * Select target for attack
348
   *
349
   * @internal
350
   */
351
  public function selectAttackTarget(Character $attacker): ?Character {
352 1
    $enemyTeam = $this->getEnemyTeam($attacker);
353 1
    $rangedWeapon = false;
354 1
    foreach($attacker->equipment as $equipment) {
355 1
      if($equipment instanceof Weapon AND $equipment->isWorn() AND $equipment->ranged) {
356
        $rangedWeapon = true;
357 1
        break;
358
      }
359
    }
360 1
    if(!$rangedWeapon) {
361 1
      $rowToAttack = $enemyTeam->rowToAttack;
362 1
      if(is_null($rowToAttack)) {
363
        return null;
364
      }
365
      /** @var Team $enemies */
366 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...
367
    } else {
368
      $enemies = $enemyTeam;
369
    }
370 1
    $target = $enemies->getLowestHpCharacter();
371 1
    if(!is_null($target)) {
372 1
      return $target;
373
    }
374 1
    return $enemies->getRandomCharacter();
375
  }
376
  
377
  /**
378
   * Select target for healing
379
   *
380
   * @internal
381
   */
382
  public function selectHealingTarget(Character $healer): ?Character {
383 1
    return $this->getTeam($healer)->getLowestHpCharacter();
384
  }
385
  
386
  /**
387
   * @internal
388
   */
389
  public function findHealers(): Team {
390 1
    $healers = call_user_func($this->healers, $this->team1, $this->team2);
391 1
    if($healers instanceof Team) {
392 1
      return $healers;
393
    }
394
    return new Team("healers");
395
  }
396
  
397
  protected function doAttackSkill(Character $character, CharacterAttackSkill $skill): void {
398 1
    $targets = [];
399
    /** @var Character $primaryTarget */
400 1
    $primaryTarget = $this->selectAttackTarget($character);
401 1
    switch($skill->skill->target) {
402 1
      case SkillAttack::TARGET_SINGLE:
403 1
        $targets[] = $primaryTarget;
404 1
        break;
405
      case SkillAttack::TARGET_ROW:
406
        $targets = $this->getTeam($primaryTarget)->getItems(["positionRow" => $primaryTarget->positionRow]);
407
        break;
408
      case SkillAttack::TARGET_COLUMN:
409
        $targets = $this->getTeam($primaryTarget)->getItems(["positionColumn" => $primaryTarget->positionColumn]);
410
        break;
411
      default:
412
        throw new NotImplementedException("Target $skill->skill->target for attack skills is not implemented.");
413
    }
414 1
    foreach($targets as $target) {
415 1
      for($i = 1; $i <= $skill->skill->strikes; $i++) {
416 1
        $this->onSkillAttack($character, $target, $skill);
417
      }
418
    }
419 1
  }
420
  
421
  protected function doSpecialSkill(Character $character, CharacterSpecialSkill $skill): void {
422 1
    $targets = [];
423 1
    switch($skill->skill->target) {
424 1
      case SkillSpecial::TARGET_ENEMY:
425
        $targets[] = $this->selectAttackTarget($character);
426
        break;
427 1
      case SkillSpecial::TARGET_SELF:
428 1
        $targets[] = $character;
429 1
        break;
430
      case SkillSpecial::TARGET_PARTY:
431
        $targets = $this->getTeam($character)->toArray();
432
        break;
433
      case SkillSpecial::TARGET_ENEMY_PARTY:
434
        $targets = $this->getEnemyTeam($character)->toArray();
435
        break;
436
      default:
437
        throw new NotImplementedException("Target $skill->skill->target for special skills is not implemented.");
438
    }
439 1
    foreach($targets as $target) {
440 1
      $this->onSkillSpecial($character, $target, $skill);
441
    }
442 1
  }
443
  
444
  /**
445
   * Main stage of a round
446
   */
447
  public function mainStage(self $combat): void {
448
    /** @var Character[] $characters */
449 1
    $characters = array_merge($combat->team1->usableMembers, $combat->team2->usableMembers);
450 1
    usort($characters, function(Character $a, Character $b) {
451 1
      return -1 * strcmp((string) $a->initiative, (string) $b->initiative);
452 1
    });
453 1
    foreach($characters as $character) {
454 1
      $action = $combat->actionSelector->chooseAction($combat, $character);
455 1
      if(!in_array($action, $this->actionSelector->getAllowedActions(), true)) {
456
        continue;
457
      }
458
      switch($action) {
459 1
        case CombatAction::ACTION_ATTACK:
460 1
          $combat->onAttack($character, $combat->selectAttackTarget($character));
461 1
          break;
462 1
        case CombatAction::ACTION_SKILL_ATTACK:
463
          /** @var CharacterAttackSkill $skill */
464 1
          $skill = $character->usableSkills[0];
465 1
          $combat->doAttackSkill($character, $skill);
466 1
          break;
467 1
        case CombatAction::ACTION_SKILL_SPECIAL:
468
          /** @var CharacterSpecialSkill $skill */
469 1
          $skill = $character->usableSkills[0];
470 1
          $combat->doSpecialSkill($character, $skill);
471 1
          break;
472 1
        case CombatAction::ACTION_HEALING:
473 1
          $combat->onHeal($character, $combat->selectHealingTarget($character));
474 1
          break;
475
      }
476
    }
477 1
  }
478
  
479
  /**
480
   * Start next round
481
   * 
482
   * @return int Winning team/0
483
   */
484
  protected function startRound(): int {
485 1
    $this->onRoundStart($this);
486 1
    return $this->getWinner();
487
  }
488
  
489
  /**
490
   * Do a round
491
   */
492
  protected function doRound(): void {
493 1
    $this->onRound($this);
494 1
  }
495
  
496
  /**
497
   * End round
498
   * 
499
   * @return int Winning team/0
500
   */
501
  protected function endRound(): int {
502 1
    $this->onRoundEnd($this);
503 1
    return $this->getWinner();
504
  }
505
  
506
  /**
507
   * Executes the combat
508
   * 
509
   * @return int Winning team
510
   */
511
  public function execute(): int {
512 1
    if(!isset($this->team1)) {
513 1
      throw new InvalidStateException("Teams are not set.");
514
    }
515 1
    $this->onCombatStart($this);
516 1
    while($this->round <= $this->roundLimit) {
517 1
      if($this->startRound() > 0) {
518 1
        break;
519
      }
520 1
      $this->doRound();
521 1
      if($this->endRound() > 0) {
522
        break;
523
      }
524
    }
525 1
    $this->onCombatEnd($this);
526 1
    return $this->getWinner();
527
  }
528
  
529
  /**
530
   * Calculate hit chance for attack/skill attack
531
   */
532
  protected function calculateHitChance(Character $character1, Character $character2, CharacterAttackSkill $skill = null): int {
0 ignored issues
show
introduced by
Line exceeds 120 characters; contains 128 characters
Loading history...
533 1
    $hitChance = $this->successCalculator->calculateHitChance($character1, $character2, $skill);
534 1
    return Numbers::range($hitChance, ISuccessCalculator::MIN_HIT_CHANCE, ISuccessCalculator::MAX_HIT_CHANCE);
535
  }
536
  
537
  /**
538
   * Check whether action succeeded
539
   */
540
  protected function hasHit(int $hitChance): bool {
541 1
    return $this->successCalculator->hasHit($hitChance);
542
  }
543
  
544
  /**
545
   * Do an attack
546
   * Hit chance = Attacker's hit - Defender's dodge, but at least 15%
547
   * Damage = Attacker's damage - defender's defense
548
   */
549
  public function attackHarm(Character $attacker, Character $defender): void {
550 1
    $result = [];
551 1
    $hitChance = $this->calculateHitChance($attacker, $defender);
552 1
    $result["result"] = $this->hasHit($hitChance);
553 1
    $result["amount"] = 0;
554 1
    if($result["result"]) {
555 1
      $amount = $attacker->damage - $defender->defense;
556 1
      $result["amount"] = Numbers::range($amount, 0, $defender->hitpoints);
557
    }
558 1
    if($result["amount"] > 0) {
559 1
      $defender->harm($result["amount"]);
560
    }
561 1
    $result["action"] = CombatAction::ACTION_ATTACK;
562 1
    $result["name"] = "";
563 1
    $result["character1"] = $attacker;
564 1
    $result["character2"] = $defender;
565 1
    $this->logDamage($attacker, $result["amount"]);
566 1
    $this->log->log($result);
567 1
  }
568
  
569
  /**
570
   * Use an attack skill
571
   */
572
  public function useAttackSkill(Character $attacker, Character $defender, CharacterAttackSkill $skill): void {
573 1
    $result = [];
574 1
    $hitChance = $this->calculateHitChance($attacker, $defender, $skill);
575 1
    $result["result"] = $this->hasHit($hitChance);
576 1
    $result["amount"] = 0;
577 1
    if($result["result"]) {
578 1
      $amount = (int) ($attacker->damage - $defender->defense / 100 * $skill->damage);
579 1
      $result["amount"] = Numbers::range($amount, 0, $defender->hitpoints);
580
    }
581 1
    if($result["amount"]) {
582 1
      $defender->harm($result["amount"]);
583
    }
584 1
    $result["action"] = CombatAction::ACTION_SKILL_ATTACK;
585 1
    $result["name"] = $skill->skill->name;
586 1
    $result["character1"] = $attacker;
587 1
    $result["character2"] = $defender;
588 1
    $this->logDamage($attacker, $result["amount"]);
589 1
    $this->log->log($result);
590 1
    $skill->resetCooldown();
591 1
  }
592
  
593
  /**
594
   * Use a special skill
595
   */
596
  public function useSpecialSkill(Character $character1, Character $target, CharacterSpecialSkill $skill): void {
597
    $result = [
598 1
      "result" => true, "amount" => 0, "action" => CombatAction::ACTION_SKILL_SPECIAL, "name" => $skill->skill->name,
599 1
      "character1" => $character1, "character2" => $target,
600
    ];
601 1
    $effect = new CharacterEffect([
602 1
      "id" => "skill{$skill->skill->id}Effect",
603 1
      "type" => $skill->skill->type,
604 1
      "stat" => ((in_array($skill->skill->type, SkillSpecial::NO_STAT_TYPES, true)) ? null : $skill->skill->stat),
605 1
      "value" => $skill->value,
606 1
      "source" => CharacterEffect::SOURCE_SKILL,
607 1
      "duration" => $skill->skill->duration,
608
    ]);
609 1
    $target->addEffect($effect);
610 1
    $this->log->log($result);
611 1
    $skill->resetCooldown();
612 1
  }
613
  
614
  /**
615
   * Heal a character
616
   */
617
  public function heal(Character $healer, Character $patient): void {
618 1
    $result = [];
619 1
    $hitChance = $this->successCalculator->calculateHealingSuccessChance($healer);
620 1
    $hitChance = Numbers::range($hitChance, 0, 100);
621 1
    $result["result"] = $this->successCalculator->hasHit($hitChance);
622 1
    $amount = ($result["result"]) ? (int) ($healer->intelligence / 2) : 0;
623 1
    $result["amount"] = Numbers::range($amount, 0, $patient->maxHitpoints - $patient->hitpoints);
624 1
    if($result["amount"]) {
625 1
      $patient->heal($result["amount"]);
626
    }
627 1
    $result["action"] = CombatAction::ACTION_HEALING;
628 1
    $result["name"] = "";
629 1
    $result["character1"] = $healer;
630 1
    $result["character2"] = $patient;
631 1
    $this->log->log($result);
632 1
  }
633
  
634
  /**
635
   * Harm poisoned characters at start of round
636
   */
637
  public function applyPoison(self $combat): void {
638
    /** @var Character[] $characters */
639 1
    $characters = array_merge($combat->team1->aliveMembers, $combat->team2->aliveMembers);
640 1
    foreach($characters as $character) {
641 1
      foreach($character->effects as $effect) {
642 1
        if($effect->type === SkillSpecial::TYPE_POISON) {
643 1
          $character->harm($effect->value);
644
          $action = [
645 1
            "action" => CombatAction::ACTION_POISON, "result" => true, "amount" => $effect->value,
646 1
            "character1" => $character, "character2" => $character,
647
          ];
648 1
          $combat->log->log($action);
649
        }
650
      }
651
    }
652 1
  }
653
  
654
  /**
655
   * Log dealt damage
656
   */
657
  public function logDamage(Character $attacker, int $amount): void {
658 1
    $team = $this->team1->hasItems(["id" => $attacker->id]) ? 1 : 2;
659 1
    $this->damage[$team] += $amount;
660 1
  }
661
  
662
  public function getLog(): CombatLogger {
663 1
    return $this->log;
664
  }
665
}
666
?>