Passed
Push — master ( 46de71...20452c )
by Jakub
02:15
created

CombatBase.php$0 ➔ selectAttackTarget()   A

Complexity

Conditions 4

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 4.074

Importance

Changes 0
Metric Value
dl 0
loc 18
ccs 10
cts 12
cp 0.8333
rs 9.6666
c 0
b 0
f 0
cc 4
crap 4.074
1
<?php
2
declare(strict_types=1);
3
4
namespace HeroesofAbenez\Combat;
5
6
use Nexendrie\Utils\Numbers;
7
use Nexendrie\Utils\Collection;
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 int $roundLimit
17
 * @property Team $team1
18
 * @property Team $team2
19
 * @property-read int $team1Damage
20
 * @property-read int $team2Damage
21
 * @property ISuccessCalculator $successCalculator
22
 * @property ICombatActionSelector $actionSelector
23
 * @property Collection|ICombatAction[] $combatActions
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
  protected $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->victoryCondition = [VictoryConditions::class, "moreDamage"];
71 1
    $this->successCalculator = $successCalculator ?? new RandomSuccessCalculator();
72 1
    $this->actionSelector = $actionSelector ?? new CombatActionSelector();
73 1
    $this->healers = function(): Team {
74
      return new Team("healers");
75
    };
76 1
    $this->combatActions = new class extends Collection {
77
      /** @var string */
78
      protected $class = ICombatAction::class;
79
    };
80 1
    $this->registerDefaultHandlers();
81 1
    $this->registerDefaultCombatActions();
82 1
  }
83
84
  protected function registerDefaultHandlers(): void {
85 1
    $this->onCombatStart[] = [$this, "assignPositions"];
86 1
    $this->onCombatEnd[] = [$this, "removeCombatEffects"];
87 1
    $this->onCombatEnd[] = [$this, "logCombatResult"];
88 1
    $this->onCombatEnd[] = [$this, "resetInitiative"];
89 1
    $this->onRoundStart[] = [$this, "applyEffectProviders"];
90 1
    $this->onRoundStart[] = [$this, "decreaseEffectsDuration"];
91 1
    $this->onRoundStart[] = [$this, "recalculateStats"];
92 1
    $this->onRoundStart[] = [$this, "logRoundNumber"];
93 1
    $this->onRoundStart[] = [$this, "applyPoison"];
94 1
    $this->onRound[] = [$this, "mainStage"];
95 1
    $this->onRoundEnd[] = [$this, "decreaseSkillsCooldowns"];
96 1
    $this->onRoundEnd[] = [$this, "resetInitiative"];
97 1
  }
98
99
  protected function registerDefaultCombatActions(): void {
100 1
    $this->combatActions[] = new CombatActions\Attack();
101 1
    $this->combatActions[] = new CombatActions\Heal();
102 1
    $this->combatActions[] = new CombatActions\SkillAttack();
103 1
    $this->combatActions[] = new CombatActions\SkillSpecial();
104 1
  }
105
  
106
  public function getRound(): int {
107 1
    return $this->round;
108
  }
109
  
110
  public function getRoundLimit(): int {
111 1
    return $this->roundLimit;
112
  }
113
114
  public function setRoundLimit(int $roundLimit): void {
115
    $this->roundLimit = Numbers::range($roundLimit, 1, PHP_INT_MAX);
116
  }
117
118
  /**
119
   * Set teams
120
   */
121
  public function setTeams(Team $team1, Team $team2): void {
122 1
    if(isset($this->team1)) {
123 1
      throw new ImmutableException("Teams has already been set.");
124
    }
125 1
    $this->team1 = & $team1;
126 1
    $this->team2 = & $team2;
127 1
    $this->log->setTeams($team1, $team2);
128 1
  }
129
  
130
  /**
131
   * Set participants for duel
132
   * Creates teams named after the member
133
   */
134
  public function setDuelParticipants(Character $player, Character $opponent): void {
135 1
    $team1 = new Team($player->name);
136 1
    $team1[] = $player;
137 1
    $team2 = new Team($opponent->name);
138 1
    $team2[] = $opponent;
139 1
    $this->setTeams($team1, $team2);
140 1
  }
141
  
142
  public function getTeam1(): Team {
143 1
    return $this->team1;
144
  }
145
  
146
  public function getTeam2(): Team {
147 1
    return $this->team2;
148
  }
149
  
150
  public function getVictoryCondition(): callable {
151 1
    return $this->victoryCondition;
152
  }
153
  
154
  public function setVictoryCondition(callable $victoryCondition): void {
155 1
    $this->victoryCondition = $victoryCondition;
156 1
  }
157
  
158
  public function getHealers(): callable {
159 1
    return $this->healers;
160
  }
161
  
162
  public function setHealers(callable $healers): void {
163 1
    $this->healers = $healers;
164 1
  }
165
  
166
  public function getTeam1Damage(): int {
167 1
    return $this->damage[1];
168
  }
169
  
170
  public function getTeam2Damage(): int {
171 1
    return $this->damage[2];
172
  }
173
  
174
  public function getSuccessCalculator(): ISuccessCalculator {
175 1
    return $this->successCalculator;
176
  }
177
  
178
  public function setSuccessCalculator(ISuccessCalculator $successCalculator): void {
179 1
    $this->successCalculator = $successCalculator;
180 1
  }
181
  
182
  public function getActionSelector(): ICombatActionSelector {
183 1
    return $this->actionSelector;
184
  }
185
  
186
  public function setActionSelector(ICombatActionSelector $actionSelector): void {
187 1
    $this->actionSelector = $actionSelector;
188 1
  }
189
190
  /**
191
   * @return Collection|ICombatAction[]
192
   */
193
  public function getCombatActions(): Collection {
194 1
    return $this->combatActions;
195
  }
196
197
  /**
198
   * Get winner of combat
199
   * 
200
   * @staticvar int $result
201
   * @return int Winning team/0
202
   */
203
  public function getWinner(): int {
204 1
    static $result = 0;
205 1
    if($result === 0) {
206 1
      $result = call_user_func($this->victoryCondition, $this);
207 1
      $result = Numbers::range($result, 0, 2);
208
    }
209 1
    return $result;
210
  }
211
212
  /**
213
   * @internal
214
   */
215
  public function getTeam(Character $character): Team {
216 1
    return $this->team1->hasItems(["id" => $character->id]) ? $this->team1 : $this->team2;
217
  }
218
219
  /**
220
   * @internal
221
   */
222
  public function getEnemyTeam(Character $character): Team {
223 1
    return $this->team1->hasItems(["id" => $character->id]) ? $this->team2 : $this->team1;
224
  }
225
  
226
  public function applyEffectProviders(self $combat): void {
227
    /** @var Character[] $characters */
228 1
    $characters = array_merge($combat->team1->toArray(), $combat->team2->toArray());
229 1
    foreach($characters as $character) {
230 1
      $character->applyEffectProviders();
231
    }
232 1
  }
233
234
  public function assignPositions(self $combat): void {
235 1
    $assignPositions = function(Team $team): void {
236 1
      $row = 1;
237 1
      $column = 0;
238
      /** @var Character $character */
239 1
      foreach($team as $character) {
240
        try {
241 1
          $column++;
242 1
          if($character->positionRow > 0 && $character->positionColumn > 0) {
243 1
            continue;
244
          }
245
          setPosition:
0 ignored issues
show
introduced by
Use of the GOTO language construct is discouraged
Loading history...
246 1
          $team->setCharacterPosition($character->id, $row, $column);
247 1
        } catch(InvalidCharacterPositionException $e) {
248 1
          if($e->getCode() === InvalidCharacterPositionException::ROW_FULL) {
249 1
            $row++;
250 1
            $column = 1;
251
          } elseif($e->getCode() === InvalidCharacterPositionException::POSITION_OCCUPIED) {
252
            $column++;
253
          } else {
254
            throw $e;
255
          }
256 1
          goto setPosition;
0 ignored issues
show
introduced by
Use of the GOTO language construct is discouraged
Loading history...
257
        }
258
      }
259 1
    };
260 1
    $assignPositions($combat->team1);
261 1
    $assignPositions($combat->team2);
262 1
  }
263
  
264
  public function decreaseEffectsDuration(self $combat): void {
265
    /** @var Character[] $characters */
266 1
    $characters = array_merge($combat->team1->toArray(), $combat->team2->toArray());
267 1
    foreach($characters as $character) {
268 1
      foreach($character->effects as $effect) {
269 1
        if(is_int($effect->duration)) {
270 1
          $effect->duration--;
271
        }
272
      }
273
    }
274 1
  }
275
  
276
  /**
277
   * Decrease skills' cooldowns
278
   */
279
  public function decreaseSkillsCooldowns(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->skills as $skill) {
284 1
        $skill->decreaseCooldown();
285
      }
286
    }
287 1
  }
288
  
289
  /**
290
   * Remove combat effects from character at the end of the combat
291
   */
292
  public function removeCombatEffects(self $combat): void {
293
    /** @var Character[] $characters */
294 1
    $characters = array_merge($combat->team1->toArray(), $combat->team2->toArray());
295 1
    foreach($characters as $character) {
296 1
      $character->effects->removeByFilter(["duration!=" => CharacterEffect::DURATION_FOREVER]);
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
    $params["winner"] = ($combat->winner === 1) ? $combat->team1->name : $combat->team2->name;
310 1
    $combat->log->logText("combat.log.combatEnd", $params);
311 1
  }
312
  
313
  /**
314
   * Log start of a round
315
   */
316
  public function logRoundNumber(self $combat): void {
317 1
    $combat->log->round = ++$this->round;
318 1
  }
319
  
320
  /**
321
   * Decrease duration of effects and recalculate stats
322
   */
323
  public function recalculateStats(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->recalculateStats();
328
    }
329 1
  }
330
  
331
  /**
332
   * Reset characters' initiative
333
   */
334
  public function resetInitiative(self $combat): void {
335
    /** @var Character[] $characters */
336 1
    $characters = array_merge($combat->team1->toArray(), $combat->team2->toArray());
337 1
    foreach($characters as $character) {
338 1
      $character->resetInitiative();
339
    }
340 1
  }
341
  
342
  /**
343
   * Select target for attack
344
   *
345
   * @internal
346
   */
347
  public function selectAttackTarget(Character $attacker): ?Character {
348 1
    $enemyTeam = $this->getEnemyTeam($attacker);
349 1
    $rangedWeapon = ($attacker->equipment->hasItems(["%class%" => Weapon::class, "worn" => true, "ranged" => true, ]));
350 1
    if(!$rangedWeapon) {
351 1
      $rowToAttack = $enemyTeam->rowToAttack;
352 1
      if($rowToAttack === null) {
353
        return null;
354
      }
355
      /** @var Team $enemies */
356 1
      $enemies = Team::fromArray($enemyTeam->getItems(["positionRow" => $rowToAttack, "hitpoints>" => 0, "hidden" => false, ]), $enemyTeam->name);
0 ignored issues
show
introduced by
Line exceeds 120 characters; contains 146 characters
Loading history...
357
    } else {
358
      $enemies = $enemyTeam;
359
    }
360 1
    $target = $enemies->getLowestHpCharacter();
361 1
    if($target !== null) {
362 1
      return $target;
363
    }
364 1
    return $enemies->getRandomCharacter();
365
  }
366
  
367
  /**
368
   * Main stage of a round
369
   *
370
   * @throws NotImplementedException
371
   */
372
  public function mainStage(self $combat): void {
373
    /** @var Character[] $characters */
374 1
    $characters = array_merge($combat->team1->usableMembers, $combat->team2->usableMembers);
375 1
    usort($characters, function(Character $a, Character $b): int {
376 1
      return -1 * strcmp((string) $a->initiative, (string) $b->initiative);
377 1
    });
378 1
    foreach($characters as $character) {
379
      /** @var ICombatAction|null $combatAction */
380 1
      $combatAction = $combat->actionSelector->chooseAction($combat, $character);
381 1
      if($combatAction === null) {
382
        break;
383
      }
384 1
      $combatAction->do($combat, $character);
385
    }
386 1
  }
387
  
388
  /**
389
   * Executes the combat
390
   * 
391
   * @return int Winning team
392
   */
393
  public function execute(): int {
394 1
    if(!isset($this->team1)) {
395 1
      throw new InvalidStateException("Teams are not set.");
396
    }
397 1
    $this->onCombatStart($this);
398 1
    while($this->round <= $this->roundLimit) {
399 1
      $this->onRoundStart($this);
400 1
      if($this->getWinner() > 0) {
401 1
        break;
402
      }
403 1
      $this->onRound($this);
404 1
      $this->onRoundEnd($this);
405 1
      if($this->getWinner() > 0) {
406
        break;
407
      }
408
    }
409 1
    $this->onCombatEnd($this);
410 1
    return $this->getWinner();
411
  }
412
413
  /**
414
   * Harm poisoned characters at start of round
415
   */
416
  public function applyPoison(self $combat): void {
417
    /** @var Character[] $characters */
418 1
    $characters = array_merge(
419 1
        $combat->team1->getItems(["hitpoints>" => 0, "poisoned!=" => false, ]),
420 1
        $combat->team2->getItems(["hitpoints>" => 0, "poisoned!=" => false, ])
421
    );
422 1
    foreach($characters as $character) {
423 1
      $poisonValue = $character->getStatus(Character::STATUS_POISONED);
424 1
      $character->harm($poisonValue);
425
      $action = [
426 1
        "action" => CombatLogEntry::ACTION_POISON, "result" => true, "amount" => $poisonValue,
427 1
        "character1" => $character, "character2" => $character,
428
      ];
429 1
      $combat->log->log($action);
430
    }
431 1
  }
432
  
433
  /**
434
   * Log dealt damage
435
   */
436
  public function logDamage(Character $attacker, int $amount): void {
437 1
    $team = $this->team1->hasItems(["id" => $attacker->id]) ? 1 : 2;
438 1
    $this->damage[$team] += $amount;
439 1
  }
440
  
441
  public function getLog(): CombatLogger {
442 1
    return $this->log;
443
  }
444
}
445
?>