Passed
Push — master ( bc69e6...465240 )
by Jakub
12:35
created

CombatBase::decreaseEffectsDuration()

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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