Passed
Push — master ( 465240...c8cc94 )
by Jakub
02:15
created

CombatBase.php$0 ➔ getLog()   A

Complexity

Conditions 1

Size

Total Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 2
ccs 1
cts 1
cp 1
rs 10
c 0
b 0
f 0
cc 1
crap 1
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 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 int $roundLimit
16
 * @property Team $team1
17
 * @property Team $team2
18
 * @property-read int $team1Damage
19
 * @property-read int $team2Damage
20
 * @property Collection|ICombatAction[] $combatActions
21
 * @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...
22
 * @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...
23
 * @method void onCombatStart(CombatBase $combat)
24
 * @method void onCombatEnd(CombatBase $combat)
25
 * @method void onRoundStart(CombatBase $combat)
26
 * @method void onRound(CombatBase $combat)
27
 * @method void onRoundEnd(CombatBase $combat)
28
 */
29 1
class CombatBase {
30
  use \Nette\SmartObject;
31
32
  protected Team $team1;
33
  protected Team $team2;
34
  protected int $round = 0;
35
  protected int $roundLimit = 30;
36
  /** @var int[] Dealt damage by team */
37
  protected array $damage = [1 => 0, 2 => 0];
38
  /** @var callable[] */
39
  public array $onCombatStart = [];
40
  /** @var callable[] */
41
  public array $onCombatEnd = [];
42
  /** @var callable[] */
43
  public array $onRoundStart = [];
44
  /** @var callable[] */
45
  public array $onRound = [];
46
  /** @var callable[] */
47
  public array $onRoundEnd = [];
48
  /** @var callable */
49
  protected $victoryCondition;
50
  /** @var callable */
51
  protected $healers;
52
  /** @var Collection|ICombatAction[] */
53
  protected Collection $combatActions;
54
55
  /**
56
   * @param CombatLogger $log Log from the combat
57
   */
58 1
  public function __construct(public readonly CombatLogger $log, public ISuccessCalculator $successCalculator = new RandomSuccessCalculator(), public ICombatActionSelector $actionSelector = new CombatActionSelector()) {
0 ignored issues
show
introduced by
Line exceeds 120 characters; contains 219 characters
Loading history...
59 1
    $this->victoryCondition = [VictoryConditions::class, "moreDamage"];
60 1
    $this->healers = function(): Team {
61
      return new Team("healers");
62
    };
63 1
    $this->combatActions = new class extends Collection {
64
      protected string $class = ICombatAction::class;
65
    };
66 1
    $this->registerDefaultHandlers();
67 1
    $this->registerDefaultCombatActions();
68 1
  }
69
70
  protected function registerDefaultHandlers(): void {
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, "applyEffectProviders"];
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
  }
84
85
  protected function registerDefaultCombatActions(): void {
86 1
    $this->combatActions[] = new CombatActions\Attack();
87 1
    $this->combatActions[] = new CombatActions\Heal();
88 1
    $this->combatActions[] = new CombatActions\SkillAttack();
89 1
    $this->combatActions[] = new CombatActions\SkillSpecial();
90 1
  }
91
  
92
  public function getRound(): int {
93 1
    return $this->round;
94
  }
95
  
96
  public function getRoundLimit(): int {
97 1
    return $this->roundLimit;
98
  }
99
100
  public function setRoundLimit(int $roundLimit): void {
101
    $this->roundLimit = Numbers::range($roundLimit, 1, PHP_INT_MAX);
102
  }
103
104
  /**
105
   * Set teams
106
   */
107
  public function setTeams(Team $team1, Team $team2): void {
108 1
    if(isset($this->team1)) {
109 1
      throw new ImmutableException("Teams has already been set.");
110
    }
111 1
    $this->team1 = $team1;
112 1
    $this->team2 = $team2;
113 1
    $this->log->setTeams($team1, $team2);
114 1
  }
115
  
116
  /**
117
   * Set participants for duel
118
   * Creates teams named after the member
119
   */
120
  public function setDuelParticipants(Character $player, Character $opponent): void {
121 1
    $team1 = new Team($player->name);
122 1
    $team1[] = $player;
123 1
    $team2 = new Team($opponent->name);
124 1
    $team2[] = $opponent;
125 1
    $this->setTeams($team1, $team2);
126 1
  }
127
  
128
  public function getTeam1(): Team {
129 1
    return $this->team1;
130
  }
131
  
132
  public function getTeam2(): Team {
133 1
    return $this->team2;
134
  }
135
  
136
  public function getVictoryCondition(): callable {
137 1
    return $this->victoryCondition;
138
  }
139
  
140
  public function setVictoryCondition(callable $victoryCondition): void {
141 1
    $this->victoryCondition = $victoryCondition;
142 1
  }
143
  
144
  public function getHealers(): callable {
145 1
    return $this->healers;
146
  }
147
  
148
  public function setHealers(callable $healers): void {
149 1
    $this->healers = $healers;
150 1
  }
151
  
152
  public function getTeam1Damage(): int {
153 1
    return $this->damage[1];
154
  }
155
  
156
  public function getTeam2Damage(): int {
157 1
    return $this->damage[2];
158
  }
159
160
  /**
161
   * @return Collection|ICombatAction[]
162
   */
163
  public function getCombatActions(): Collection {
164 1
    return $this->combatActions;
165
  }
166
167
  /**
168
   * Get winner of combat
169
   * 
170
   * @staticvar int $result
171
   * @return int Winning team/0
172
   */
173
  public function getWinner(): int {
174 1
    static $result = 0;
175 1
    if($result === 0) {
176 1
      $result = call_user_func($this->victoryCondition, $this);
177 1
      $result = Numbers::range($result, 0, 2);
178
    }
179 1
    return $result;
180
  }
181
182
  /**
183
   * @internal
184
   */
185
  public function getTeam(Character $character): Team {
186 1
    return $this->team1->hasItems(["id" => $character->id]) ? $this->team1 : $this->team2;
187
  }
188
189
  /**
190
   * @internal
191
   */
192
  public function getEnemyTeam(Character $character): Team {
193 1
    return $this->team1->hasItems(["id" => $character->id]) ? $this->team2 : $this->team1;
194
  }
195
  
196
  public function applyEffectProviders(self $combat): void {
197
    /** @var Character[] $characters */
198 1
    $characters = array_merge($combat->team1->toArray(), $combat->team2->toArray());
199 1
    foreach($characters as $character) {
200 1
      $character->applyEffectProviders();
201
    }
202 1
  }
203
204
  public function assignPositions(self $combat): void {
205 1
    $assignPositions = function(Team $team): void {
206 1
      $row = 1;
207 1
      $column = 0;
208
      /** @var Character $character */
209 1
      foreach($team as $character) {
210
        try {
211 1
          $column++;
212 1
          if($character->positionRow > 0 && $character->positionColumn > 0) {
213 1
            continue;
214
          }
215
          setPosition:
0 ignored issues
show
introduced by
Use of the GOTO language construct is discouraged
Loading history...
216 1
          $team->setCharacterPosition($character->id, $row, $column);
217 1
        } catch(InvalidCharacterPositionException $e) {
218 1
          if($e->getCode() === InvalidCharacterPositionException::ROW_FULL) {
219 1
            $row++;
220 1
            $column = 1;
221
          } elseif($e->getCode() === InvalidCharacterPositionException::POSITION_OCCUPIED) {
222
            $column++;
223
          } else {
224
            throw $e;
225
          }
226 1
          goto setPosition;
0 ignored issues
show
introduced by
Use of the GOTO language construct is discouraged
Loading history...
227
        }
228
      }
229 1
    };
230 1
    $assignPositions($combat->team1);
231 1
    $assignPositions($combat->team2);
232 1
  }
233
  
234
  public function decreaseEffectsDuration(self $combat): void {
235
    /** @var Character[] $characters */
236 1
    $characters = array_merge($combat->team1->toArray(), $combat->team2->toArray());
237 1
    foreach($characters as $character) {
238 1
      foreach($character->effects as $effect) {
239 1
        if(is_int($effect->duration)) {
240 1
          $effect->duration--;
241
        }
242
      }
243
    }
244 1
  }
245
  
246
  /**
247
   * Decrease skills' cooldowns
248
   */
249
  public function decreaseSkillsCooldowns(self $combat): void {
250
    /** @var Character[] $characters */
251 1
    $characters = array_merge($combat->team1->toArray(), $combat->team2->toArray());
252 1
    foreach($characters as $character) {
253 1
      foreach($character->skills as $skill) {
254 1
        $skill->decreaseCooldown();
255
      }
256
    }
257 1
  }
258
  
259
  /**
260
   * Remove combat effects from character at the end of the combat
261
   */
262
  public function removeCombatEffects(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
      $character->effects->removeByFilter(["duration!=" => CharacterEffect::DURATION_FOREVER]);
267
    }
268 1
  }
269
  
270
  /**
271
   * Add winner to the log
272
   */
273
  public function logCombatResult(self $combat): void {
274 1
    $combat->log->round = 5000;
275 1
    $params = [
276 1
      "team1name" => $combat->team1->name, "team1damage" => $combat->damage[1],
277 1
      "team2name" => $combat->team2->name, "team2damage" => $combat->damage[2],
278
    ];
279 1
    $params["winner"] = ($combat->winner === 1) ? $combat->team1->name : $combat->team2->name;
280 1
    $combat->log->logText("combat.log.combatEnd", $params);
281 1
  }
282
  
283
  /**
284
   * Log start of a round
285
   */
286
  public function logRoundNumber(self $combat): void {
287 1
    $combat->log->round = ++$this->round;
288 1
  }
289
  
290
  /**
291
   * Decrease duration of effects and recalculate stats
292
   */
293
  public function recalculateStats(self $combat): void {
294
    /** @var Character[] $characters */
295 1
    $characters = array_merge($combat->team1->toArray(), $combat->team2->toArray());
296 1
    foreach($characters as $character) {
297 1
      $character->recalculateStats();
298
    }
299 1
  }
300
  
301
  /**
302
   * Reset characters' initiative
303
   */
304
  public function resetInitiative(self $combat): void {
305
    /** @var Character[] $characters */
306 1
    $characters = array_merge($combat->team1->toArray(), $combat->team2->toArray());
307 1
    foreach($characters as $character) {
308 1
      $character->resetInitiative();
309
    }
310 1
  }
311
  
312
  /**
313
   * Select target for attack
314
   *
315
   * @internal
316
   */
317
  public function selectAttackTarget(Character $attacker): ?Character {
318 1
    $enemyTeam = $this->getEnemyTeam($attacker);
319 1
    $rangedWeapon = ($attacker->equipment->hasItems(["%class%" => Weapon::class, "worn" => true, "ranged" => true, ]));
320 1
    if(!$rangedWeapon) {
321 1
      $rowToAttack = $enemyTeam->rowToAttack;
322 1
      if($rowToAttack === null) {
323
        return null;
324
      }
325 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...
326
    } else {
327
      $enemies = $enemyTeam;
328
    }
329 1
    $target = $enemies->getLowestHpCharacter();
330 1
    if($target !== null) {
331
      return $target;
332
    }
333 1
    return $enemies->getRandomCharacter();
334
  }
335
  
336
  /**
337
   * Main stage of a round
338
   *
339
   * @throws NotImplementedException
340
   */
341
  public function mainStage(self $combat): void {
342
    /** @var Character[] $characters */
343 1
    $characters = array_merge($combat->team1->usableMembers, $combat->team2->usableMembers);
344 1
    usort($characters, function(Character $a, Character $b): int {
345 1
      return -1 * strcmp((string) $a->initiative, (string) $b->initiative);
346 1
    });
347 1
    foreach($characters as $character) {
348
      /** @var ICombatAction|null $combatAction */
349 1
      $combatAction = $combat->actionSelector->chooseAction($combat, $character);
350 1
      if($combatAction === null) {
351
        break;
352
      }
353 1
      $combatAction->do($combat, $character);
354
    }
355 1
  }
356
  
357
  /**
358
   * Executes the combat
359
   * 
360
   * @return int Winning team
361
   */
362
  public function execute(): int {
363 1
    if(!isset($this->team1)) {
364 1
      throw new InvalidStateException("Teams are not set.");
365
    }
366 1
    $this->onCombatStart($this);
367 1
    while($this->round <= $this->roundLimit) {
368 1
      $this->onRoundStart($this);
369 1
      if($this->getWinner() > 0) {
370 1
        break;
371
      }
372 1
      $this->onRound($this);
373 1
      $this->onRoundEnd($this);
374 1
      if($this->getWinner() > 0) {
375
        break;
376
      }
377
    }
378 1
    $this->onCombatEnd($this);
379 1
    return $this->getWinner();
380
  }
381
382
  /**
383
   * Harm poisoned characters at start of round
384
   */
385
  public function applyPoison(self $combat): void {
386
    /** @var Character[] $characters */
387 1
    $characters = array_merge(
388 1
        $combat->team1->getItems(["hitpoints>" => 0, "poisoned!=" => false, ]),
389 1
        $combat->team2->getItems(["hitpoints>" => 0, "poisoned!=" => false, ])
390
    );
391 1
    foreach($characters as $character) {
392 1
      $poisonValue = $character->getStatus(Character::STATUS_POISONED);
393 1
      $character->harm($poisonValue);
394 1
      $action = [
395 1
        "action" => CombatLogEntry::ACTION_POISON, "result" => true, "amount" => $poisonValue,
396 1
        "character1" => $character, "character2" => $character,
397
      ];
398 1
      $combat->log->log($action);
399
    }
400 1
  }
401
  
402
  /**
403
   * Log dealt damage
404
   */
405
  public function logDamage(Character $attacker, int $amount): void {
406 1
    $team = $this->team1->hasItems(["id" => $attacker->id]) ? 1 : 2;
407 1
    $this->damage[$team] += $amount;
408 1
  }
409
}
410
?>