Passed
Push — master ( 7a48d2...d0f71f )
by Jakub
12:49
created

CombatBase.php$0 ➔ getTeam1()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

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