Issues (30)

src/CombatBase.php (6 issues)

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