Completed
Push — master ( 10c9e5...cb21fe )
by Jakub
02:04
created

CombatBase::mainStage()   C

Complexity

Conditions 7
Paths 7

Size

Total Lines 28
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 7.0046

Importance

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