Passed
Push — master ( 50241d...54d247 )
by Jakub
09:15
created

CombatBase.php$0 ➔ assignPositions()   B

Complexity

Conditions 7

Size

Total Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 7.1929

Importance

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