Completed
Push — master ( 29e2cf...033bf5 )
by Jakub
02:40
created

CombatBase::attackHarm()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 17
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 14
dl 0
loc 17
ccs 14
cts 14
cp 1
c 0
b 0
f 0
rs 9.7998
cc 3
nc 4
nop 2
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, "setSkillsCooldowns"];
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
   * Get winner of combat
181
   * 
182
   * @staticvar int $result
183
   * @return int Winning team/0
184
   */
185
  public function getWinner(): int {
186 1
    static $result = 0;
187 1
    if($result === 0) {
188 1
      $result = call_user_func($this->victoryCondition, $this);
189 1
      $result = Numbers::range($result, 0, 2);
190
    }
191 1
    return $result;
192
  }
193
194
  /**
195
   * @internal
196
   */
197
  public function getTeam(Character $character): Team {
198 1
    return $this->team1->hasItems(["id" => $character->id]) ? $this->team1 : $this->team2;
199
  }
200
201
  /**
202
   * @internal
203
   */
204
  public function getEnemyTeam(Character $character): Team {
205 1
    return $this->team1->hasItems(["id" => $character->id]) ? $this->team2 : $this->team1;
206
  }
207
  
208
  public function applyEffectProviders(self $combat): void {
209
    /** @var Character[] $characters */
210 1
    $characters = array_merge($combat->team1->toArray(), $combat->team2->toArray());
211 1
    foreach($characters as $character) {
212 1
      $character->applyEffectProviders();
213
    }
214 1
  }
215
  
216
  /**
217
   * Set skills' cooldowns
218
   */
219
  public function setSkillsCooldowns(self $combat): void {
220
    /** @var Character[] $characters */
221 1
    $characters = array_merge($combat->team1->toArray(), $combat->team2->toArray());
222 1
    foreach($characters as $character) {
223 1
      foreach($character->skills as $skill) {
224 1
        $skill->resetCooldown();
225
      }
226
    }
227 1
  }
228
  
229
  public function assignPositions(self $combat): void {
230 1
    $assignPositions = function(Team $team) {
231 1
      $row = 1;
232 1
      $column = 0;
233
      /** @var Character $character */
234 1
      foreach($team as $character) {
235
        try {
236 1
          $column++;
237 1
          if($character->positionRow > 0 AND $character->positionColumn > 0) {
238 1
            continue;
239
          }
240
          setPosition:
0 ignored issues
show
introduced by
Use of the GOTO language construct is discouraged
Loading history...
241 1
          $team->setCharacterPosition($character->id, $row, $column);
242 1
        } catch(InvalidCharacterPositionException $e) {
243 1
          if($e->getCode() === InvalidCharacterPositionException::ROW_FULL) {
244 1
            $row++;
245 1
            $column = 1;
246
          } elseif($e->getCode() === InvalidCharacterPositionException::POSITION_OCCUPIED) {
247
            $column++;
248
          } else {
249
            throw $e;
250
          }
251 1
          goto setPosition;
0 ignored issues
show
introduced by
Use of the GOTO language construct is discouraged
Loading history...
252
        }
253
      }
254 1
    };
255 1
    $assignPositions($combat->team1);
256 1
    $assignPositions($combat->team2);
257 1
  }
258
  
259
  public function decreaseEffectsDuration(self $combat): void {
260
    /** @var Character[] $characters */
261 1
    $characters = array_merge($combat->team1->toArray(), $combat->team2->toArray());
262 1
    foreach($characters as $character) {
263 1
      foreach($character->effects as $effect) {
264 1
        if(is_int($effect->duration)) {
265 1
          $effect->duration--;
266
        }
267
      }
268
    }
269 1
  }
270
  
271
  /**
272
   * Decrease skills' cooldowns
273
   */
274
  public function decreaseSkillsCooldowns(self $combat): void {
275
    /** @var Character[] $characters */
276 1
    $characters = array_merge($combat->team1->toArray(), $combat->team2->toArray());
277 1
    foreach($characters as $character) {
278 1
      foreach($character->skills as $skill) {
279 1
        $skill->decreaseCooldown();
280
      }
281
    }
282 1
  }
283
  
284
  /**
285
   * Remove combat effects from character at the end of the combat
286
   */
287
  public function removeCombatEffects(self $combat): void {
288
    /** @var Character[] $characters */
289 1
    $characters = array_merge($combat->team1->toArray(), $combat->team2->toArray());
290 1
    foreach($characters as $character) {
291 1
      foreach($character->effects as $effect) {
292 1
        if($effect->duration === CharacterEffect::DURATION_COMBAT OR is_int($effect->duration)) {
293 1
          $character->removeEffect($effect->id);
294
        }
295
      }
296
    }
297 1
  }
298
  
299
  /**
300
   * Add winner to the log
301
   */
302
  public function logCombatResult(self $combat): void {
303 1
    $combat->log->round = 5000;
304
    $params = [
305 1
      "team1name" => $combat->team1->name, "team1damage" => $combat->damage[1],
306 1
      "team2name" => $combat->team2->name, "team2damage" => $combat->damage[2],
307
    ];
308 1
    if($combat->winner === 1) {
309 1
      $params["winner"] = $combat->team1->name;
310
    } else {
311 1
      $params["winner"] = $combat->team2->name;
312
    }
313 1
    $combat->log->logText("combat.log.combatEnd", $params);
314 1
  }
315
  
316
  /**
317
   * Log start of a round
318
   */
319
  public function logRoundNumber(self $combat): void {
320 1
    $combat->log->round = ++$this->round;
321 1
  }
322
  
323
  /**
324
   * Decrease duration of effects and recalculate stats
325
   */
326
  public function recalculateStats(self $combat): void {
327
    /** @var Character[] $characters */
328 1
    $characters = array_merge($combat->team1->toArray(), $combat->team2->toArray());
329 1
    foreach($characters as $character) {
330 1
      $character->recalculateStats();
331
    }
332 1
  }
333
  
334
  /**
335
   * Reset characters' initiative
336
   */
337
  public function resetInitiative(self $combat): void {
338
    /** @var Character[] $characters */
339 1
    $characters = array_merge($combat->team1->toArray(), $combat->team2->toArray());
340 1
    foreach($characters as $character) {
341 1
      $character->resetInitiative();
342
    }
343 1
  }
344
  
345
  /**
346
   * Select target for attack
347
   *
348
   * @internal
349
   */
350
  public function selectAttackTarget(Character $attacker): ?Character {
351 1
    $enemyTeam = $this->getEnemyTeam($attacker);
352 1
    $rangedWeapon = false;
353 1
    foreach($attacker->equipment as $equipment) {
354 1
      if($equipment instanceof Weapon AND $equipment->isWorn() AND $equipment->ranged) {
355
        $rangedWeapon = true;
356 1
        break;
357
      }
358
    }
359 1
    if(!$rangedWeapon) {
360 1
      $rowToAttack = $enemyTeam->rowToAttack;
361 1
      if(is_null($rowToAttack)) {
362
        return null;
363
      }
364
      /** @var Team $enemies */
365 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...
366
    } else {
367
      $enemies = $enemyTeam;
368
    }
369 1
    $target = $enemies->getLowestHpCharacter();
370 1
    if(!is_null($target)) {
371
      return $target;
372
    }
373 1
    return $enemies->getRandomCharacter();
374
  }
375
  
376
  /**
377
   * Select target for healing
378
   *
379
   * @internal
380
   */
381
  public function selectHealingTarget(Character $healer): ?Character {
382 1
    return $this->getTeam($healer)->getLowestHpCharacter();
383
  }
384
  
385
  /**
386
   * @internal
387
   */
388
  public function findHealers(): Team {
389 1
    $healers = call_user_func($this->healers, $this->team1, $this->team2);
390 1
    if($healers instanceof Team) {
391 1
      return $healers;
392
    }
393
    return new Team("healers");
394
  }
395
  
396
  /**
397
   * Main stage of a round
398
   *
399
   * @throws NotImplementedException
400
   */
401
  public function mainStage(self $combat): void {
402
    /** @var Character[] $characters */
403 1
    $characters = array_merge($combat->team1->usableMembers, $combat->team2->usableMembers);
404 1
    usort($characters, function(Character $a, Character $b) {
405 1
      return -1 * strcmp((string) $a->initiative, (string) $b->initiative);
406 1
    });
407 1
    foreach($characters as $character) {
408 1
      $action = $combat->actionSelector->chooseAction($combat, $character);
409 1
      if(is_null($action)) {
410
        break;
411
      }
412
      /** @var ICombatAction $combatAction */
413 1
      foreach($this->combatActions as $combatAction) {
414 1
        if($combatAction->getName() === $action) {
415 1
          $combatAction->do($this, $character);
416 1
          break 2;
417
        }
418
      }
419
      throw new NotImplementedException("Action $action is not implemented.");
420
    }
421 1
  }
422
  
423
  /**
424
   * Start next round
425
   * 
426
   * @return int Winning team/0
427
   */
428
  protected function startRound(): int {
429 1
    $this->onRoundStart($this);
430 1
    return $this->getWinner();
431
  }
432
  
433
  /**
434
   * Do a round
435
   */
436
  protected function doRound(): void {
437 1
    $this->onRound($this);
438 1
  }
439
  
440
  /**
441
   * End round
442
   * 
443
   * @return int Winning team/0
444
   */
445
  protected function endRound(): int {
446 1
    $this->onRoundEnd($this);
447 1
    return $this->getWinner();
448
  }
449
  
450
  /**
451
   * Executes the combat
452
   * 
453
   * @return int Winning team
454
   */
455
  public function execute(): int {
456 1
    if(!isset($this->team1)) {
457 1
      throw new InvalidStateException("Teams are not set.");
458
    }
459 1
    $this->onCombatStart($this);
460 1
    while($this->round <= $this->roundLimit) {
461 1
      if($this->startRound() > 0) {
462 1
        break;
463
      }
464 1
      $this->doRound();
465 1
      if($this->endRound() > 0) {
466
        break;
467
      }
468
    }
469 1
    $this->onCombatEnd($this);
470 1
    return $this->getWinner();
471
  }
472
473
  /**
474
   * Harm poisoned characters at start of round
475
   */
476
  public function applyPoison(self $combat): void {
477
    /** @var Character[] $characters */
478 1
    $characters = array_merge($combat->team1->aliveMembers, $combat->team2->aliveMembers);
479 1
    foreach($characters as $character) {
480 1
      foreach($character->effects as $effect) {
481 1
        if($effect->type === SkillSpecial::TYPE_POISON) {
482 1
          $character->harm($effect->value);
483
          $action = [
484 1
            "action" => CombatLogEntry::ACTION_POISON, "result" => true, "amount" => $effect->value,
485 1
            "character1" => $character, "character2" => $character,
486
          ];
487 1
          $combat->log->log($action);
488
        }
489
      }
490
    }
491 1
  }
492
  
493
  /**
494
   * Log dealt damage
495
   */
496
  public function logDamage(Character $attacker, int $amount): void {
497 1
    $team = $this->team1->hasItems(["id" => $attacker->id]) ? 1 : 2;
498 1
    $this->damage[$team] += $amount;
499 1
  }
500
  
501
  public function getLog(): CombatLogger {
502 1
    return $this->log;
503
  }
504
}
505
?>