Character::hasStatus()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 3
c 1
b 0
f 0
dl 0
loc 6
rs 10
ccs 3
cts 3
cp 1
cc 2
nc 2
nop 1
crap 2
1
<?php
2
declare(strict_types=1);
3
4
namespace HeroesofAbenez\Combat;
5
6
use Nexendrie\Utils\Numbers;
7
use Symfony\Component\OptionsResolver\OptionsResolver;
8
9
/**
10
 * Structure for single character
11
 *
12
 * @author Jakub Konečný
13
 * @property-read int|string $id
14
 * @property-read string $name
15
 * @property-read string $gender
16
 * @property-read string $race
17
 * @property-read string $occupation
18
 * @property-read string $specialization
19
 * @property-read int $level
20
 * @property-read int $strength
21
 * @property-read int $strengthBase
22
 * @property-read int $dexterity
23
 * @property-read int $dexterityBase
24
 * @property-read int $constitution
25
 * @property-read int $constitutionBase
26
 * @property-read int $intelligence
27
 * @property-read int $intelligenceBase
28
 * @property-read int $charisma
29
 * @property-read int $charismaBase
30
 * @property-read int $maxHitpoints
31
 * @property-read int $maxHitpointsBase
32
 * @property-read int $hitpoints
33
 * @property-read int $damage
34
 * @property-read int $damageBase
35
 * @property-read int $hit
36
 * @property-read int $hitBase
37
 * @property-read int $dodge
38
 * @property-read int $dodgeBase
39
 * @property-read int $initiative
40
 * @property-read int $initiativeBase
41
 * @property-read string $initiativeFormula
42
 * @property-read int $defense
43
 * @property-read int $defenseBase
44
 * @property-read int|null $activePet
45
 * @property-read bool $stunned
46
 * @property-read bool $poisoned
47
 * @property-read bool $hidden
48
 * @property-read BaseCharacterSkill[] $usableSkills
49
 * @property IInitiativeFormulaParser $initiativeFormulaParser
50
 * @property int $positionRow
51
 * @property int $positionColumn
52
 */
53 1
class Character
54
{
55
    use \Nette\SmartObject;
56
57
    public const HITPOINTS_PER_CONSTITUTION = 5;
58
    public const STAT_STRENGTH = "strength";
59
    public const STAT_DEXTERITY = "dexterity";
60
    public const STAT_CONSTITUTION = "constitution";
61
    public const STAT_INTELLIGENCE = "intelligence";
62
    public const STAT_CHARISMA = "charisma";
63
    public const STAT_MAX_HITPOINTS = "maxHitpoints";
64
    public const STAT_DAMAGE = "damage";
65
    public const STAT_DEFENSE = "defense";
66
    public const STAT_HIT = "hit";
67
    public const STAT_DODGE = "dodge";
68
    public const STAT_INITIATIVE = "initiative";
69
    public const BASE_STATS = [
70
        self::STAT_STRENGTH,
71
        self::STAT_DEXTERITY,
72
        self::STAT_CONSTITUTION,
73
        self::STAT_INTELLIGENCE,
74
        self::STAT_CHARISMA,
75
    ];
76
    public const SECONDARY_STATS = [
77
        self::STAT_MAX_HITPOINTS,
78
        self::STAT_DAMAGE,
79
        self::STAT_DEFENSE,
80
        self::STAT_HIT,
81
        self::STAT_DODGE,
82
        self::STAT_INITIATIVE,
83
    ];
84
    public const STATUS_STUNNED = "stunned";
85
    public const STATUS_POISONED = "poisoned";
86
    public const STATUS_HIDDEN = "hidden";
87
88
    protected int|string $id;
89
    protected string $name;
90
    protected string $gender = "male";
91
    protected string $race;
92
    protected string $occupation;
93
    protected string $specialization;
94
    protected int $level;
95
    protected int $strength;
96
    protected int $strengthBase;
97
    protected int $dexterity;
98
    protected int $dexterityBase;
99
    protected int $constitution;
100
    protected int $constitutionBase;
101
    protected int $intelligence;
102
    protected int $intelligenceBase;
103
    protected int $charisma;
104
    protected int $charismaBase;
105
    protected int $maxHitpoints;
106
    protected int $maxHitpointsBase;
107
    protected int $hitpoints;
108
    protected int $damage = 0;
109
    protected int $damageBase = 0;
110
    protected int $hit = 0;
111
    protected int $hitBase = 0;
112
    protected int $dodge = 0;
113
    protected int $dodgeBase = 0;
114
    protected int $initiative = 0;
115
    protected int $initiativeBase = 0;
116
    protected string $initiativeFormula;
117
    protected IInitiativeFormulaParser $initiativeFormulaParser;
118
    protected float $defense = 0;
119
    protected float $defenseBase = 0;
120
    /** @var Equipment[]|EquipmentCollection Character's equipment */
121
    public EquipmentCollection $equipment;
122
    /** @var Pet[]|PetsCollection Character's pets */
123
    public PetsCollection $pets;
124
    /** @var BaseCharacterSkill[]|CharacterSkillsCollection Character's skills */
125
    public CharacterSkillsCollection $skills;
126
    protected ?int $activePet = null;
127
    /** @var CharacterEffect[]|CharacterEffectsCollection Active effects */
128
    public CharacterEffectsCollection $effects;
129
    /** @var ICharacterEffectsProvider[]|CharacterEffectsProvidersCollection */
130
    public CharacterEffectsProvidersCollection $effectProviders;
131
    protected int $positionRow = 0;
132
    protected int $positionColumn = 0;
133
    /** @var callable[] */
134
    protected array $statuses = [];
135
136
    /**
137
     *
138
     * @param array $stats Stats of the character
139
     * @param Equipment[] $equipment Equipment of the character
140
     * @param Pet[] $pets Pets owned by the character
141
     * @param BaseCharacterSkill[] $skills Skills acquired by the character
142
     */
143
    public function __construct(
144
        array $stats,
145
        array $equipment = [],
146
        array $pets = [],
147
        array $skills = [],
148
        IInitiativeFormulaParser $initiativeFormulaParser = new InitiativeFormulaParser()
149
    ) {
150 1
        $this->initiativeFormulaParser = $initiativeFormulaParser;
151 1
        $this->equipment = EquipmentCollection::fromArray($equipment);
152 1
        $this->pets = PetsCollection::fromArray($pets);
153 1
        $this->effectProviders = CharacterEffectsProvidersCollection::fromArray(array_merge($equipment, $pets));
154 1
        $this->skills = CharacterSkillsCollection::fromArray($skills);
155 1
        $this->equipment->lock();
156 1
        $this->pets->lock();
157 1
        $this->skills->lock();
158 1
        $this->setStats($stats);
159 1
        $this->effects = new CharacterEffectsCollection($this);
160 1
        $this->registerDefaultStatuses();
161 1
    }
162
163
    protected function registerDefaultStatuses(): void
164
    {
165 1
        $this->registerStatus(static::STATUS_STUNNED, function (Character $character): bool {
166 1
            return $character->effects->hasItems(["type" => SkillSpecial::TYPE_STUN]);
167 1
        });
168 1
        $this->registerStatus(static::STATUS_POISONED, function (Character $character): int {
169 1
            $poisons = $character->effects->getItems(["type" => SkillSpecial::TYPE_POISON]);
170 1
            $poisonValue = 0;
171
            /** @var CharacterEffect $poison */
172 1
            foreach ($poisons as $poison) {
173 1
                $poisonValue += $poison->value;
174
            }
175 1
            return $poisonValue;
176 1
        });
177 1
        $this->registerStatus(static::STATUS_HIDDEN, function (Character $character): bool {
178 1
            return $character->effects->hasItems(["type" => SkillSpecial::TYPE_HIDE]);
179 1
        });
180 1
    }
181
182
    protected function setStats(array $stats): void
183
    {
184 1
        $requiredStats = array_merge(["id", "name", "level", "initiativeFormula",], static::BASE_STATS);
185 1
        $allStats = array_merge($requiredStats, ["occupation", "race", "specialization", "gender",]);
186 1
        $numberStats = static::BASE_STATS;
187 1
        $textStats = ["name", "race", "occupation", "specialization", "initiativeFormula",];
188 1
        $resolver = new OptionsResolver();
189 1
        $resolver->setDefined($allStats);
190 1
        $resolver->setAllowedTypes("id", ["integer", "string",]);
191 1
        foreach ($numberStats as $stat) {
192 1
            $resolver->setAllowedTypes($stat, ["integer", "float"]);
193 1
            $resolver->setNormalizer($stat, function (OptionsResolver $resolver, $value): int {
194 1
                return (int) $value;
195 1
            });
196
        }
197 1
        foreach ($textStats as $stat) {
198 1
            $resolver->setNormalizer($stat, function (OptionsResolver $resolver, $value): string {
199 1
                return (string) $value;
200 1
            });
201
        }
202 1
        $resolver->setRequired($requiredStats);
203 1
        $stats = array_filter($stats, function ($key) use ($allStats): bool {
204 1
            return in_array($key, $allStats, true);
205 1
        }, ARRAY_FILTER_USE_KEY);
206 1
        $stats = $resolver->resolve($stats);
207 1
        foreach ($stats as $key => $value) {
208 1
            $this->$key = $value;
209 1
            if (in_array($key, $numberStats, true)) {
210 1
                $this->{$key . "Base"} = $value;
211
            }
212
        }
213 1
        $this->hitpoints = $this->maxHitpoints = $this->maxHitpointsBase = $this->constitution * static::HITPOINTS_PER_CONSTITUTION;
0 ignored issues
show
introduced by
Line exceeds 120 characters; contains 132 characters
Loading history...
214 1
        $this->recalculateSecondaryStats();
215 1
        $this->hitBase = $this->hit;
216 1
        $this->dodgeBase = $this->dodge;
217 1
    }
218
219
    protected function getId(): int|string
220
    {
221 1
        return $this->id;
222
    }
223
224
    protected function getName(): string
225
    {
226 1
        return $this->name;
227
    }
228
229
    protected function getGender(): string
230
    {
231
        return $this->gender;
232
    }
233
234
    protected function getRace(): string
235
    {
236
        return $this->race;
237
    }
238
239
    protected function getOccupation(): string
240
    {
241
        return $this->occupation;
242
    }
243
244
    protected function getLevel(): int
245
    {
246 1
        return $this->level;
247
    }
248
249
    protected function getStrength(): int
250
    {
251 1
        return $this->strength;
252
    }
253
254
    protected function getStrengthBase(): int
255
    {
256
        return $this->strengthBase;
257
    }
258
259
    protected function getDexterity(): int
260
    {
261 1
        return $this->dexterity;
262
    }
263
264
    protected function getDexterityBase(): int
265
    {
266
        return $this->dexterityBase;
267
    }
268
269
    protected function getConstitution(): int
270
    {
271 1
        return $this->constitution;
272
    }
273
274
    protected function getConstitutionBase(): int
275
    {
276
        return $this->constitutionBase;
277
    }
278
279
    protected function getCharisma(): int
280
    {
281 1
        return $this->charisma;
282
    }
283
284
    protected function getCharismaBase(): int
285
    {
286
        return $this->charismaBase;
287
    }
288
289
    protected function getMaxHitpoints(): int
290
    {
291 1
        return $this->maxHitpoints;
292
    }
293
294
    protected function getMaxHitpointsBase(): int
295
    {
296 1
        return $this->maxHitpointsBase;
297
    }
298
299
    protected function getHitpoints(): int
300
    {
301 1
        return $this->hitpoints;
302
    }
303
304
    protected function getDamage(): int
305
    {
306 1
        return $this->damage;
307
    }
308
309
    protected function getDamageBase(): int
310
    {
311
        return $this->damageBase;
312
    }
313
314
    protected function getHit(): int
315
    {
316 1
        return $this->hit;
317
    }
318
319
    protected function getHitBase(): int
320
    {
321
        return $this->hitBase;
322
    }
323
324
    protected function getDodge(): int
325
    {
326 1
        return $this->dodge;
327
    }
328
329
    protected function getDodgeBase(): int
330
    {
331
        return $this->dodgeBase;
332
    }
333
334
    protected function getInitiative(): int
335
    {
336 1
        return $this->initiative;
337
    }
338
339
    protected function getInitiativeBase(): int
340
    {
341 1
        return $this->initiativeBase;
342
    }
343
344
    protected function getInitiativeFormula(): string
345
    {
346 1
        return $this->initiativeFormula;
347
    }
348
349
    protected function getDefense(): int
350
    {
351 1
        return (int) $this->defense;
352
    }
353
354
    protected function getDefenseBase(): int
355
    {
356
        return (int) $this->defenseBase;
357
    }
358
359
    protected function getActivePet(): ?int
360
    {
361
        /** @var Pet|null $pet */
362 1
        $pet = $this->pets->getItem(["deployed" => true]);
363 1
        if ($pet === null) {
364 1
            return null;
365
        }
366 1
        return $pet->id;
367
    }
368
369
    protected function isStunned(): bool
370
    {
371
        return $this->hasStatus(static::STATUS_STUNNED);
372
    }
373
374
    protected function isPoisoned(): bool
375
    {
376 1
        return $this->hasStatus(static::STATUS_POISONED);
377
    }
378
379
    protected function isHidden(): bool
380
    {
381 1
        return $this->hasStatus(static::STATUS_HIDDEN);
382
    }
383
384
    protected function getSpecialization(): string
385
    {
386
        return $this->specialization;
387
    }
388
389
    protected function getIntelligence(): int
390
    {
391 1
        return $this->intelligence;
392
    }
393
394
    protected function getIntelligenceBase(): int
395
    {
396
        return $this->intelligenceBase;
397
    }
398
399
    protected function getInitiativeFormulaParser(): IInitiativeFormulaParser
400
    {
401 1
        return $this->initiativeFormulaParser;
402
    }
403
404
    protected function setInitiativeFormulaParser(IInitiativeFormulaParser $initiativeFormulaParser): void
405
    {
406 1
        $oldParser = $this->initiativeFormulaParser;
407 1
        $this->initiativeFormulaParser = $initiativeFormulaParser;
408 1
        if ($oldParser !== $initiativeFormulaParser) {
409 1
            $this->recalculateStats();
410
        }
411 1
    }
412
413
    protected function getPositionRow(): int
414
    {
415 1
        return $this->positionRow;
416
    }
417
418
    protected function setPositionRow(int $positionRow): void
419
    {
420 1
        $this->positionRow = Numbers::range($positionRow, 1, PHP_INT_MAX);
421 1
    }
422
423
    protected function getPositionColumn(): int
424
    {
425 1
        return $this->positionColumn;
426
    }
427
428
    protected function setPositionColumn(int $positionColumn): void
429
    {
430 1
        $this->positionColumn = Numbers::range($positionColumn, 1, PHP_INT_MAX);
431 1
    }
432
433
    /**
434
     * Register a new possible character status
435
     *
436
     * @param string $name
437
     * @param callable $callback Used to determine whether the status is active/what value does it have. Is called with Character instance as parameter
0 ignored issues
show
introduced by
Line exceeds 120 characters; contains 151 characters
Loading history...
438
     */
439
    public function registerStatus(string $name, callable $callback): void
440
    {
441 1
        $this->statuses[$name] = $callback;
442 1
    }
443
444
    public function getStatus(string $name): mixed
445
    {
446 1
        if (!array_key_exists($name, $this->statuses)) {
447 1
            return null;
448
        }
449 1
        return (call_user_func($this->statuses[$name], $this));
450
    }
451
452
    public function hasStatus(string $status): bool
453
    {
454 1
        if (!array_key_exists($status, $this->statuses)) {
455 1
            return false;
456
        }
457 1
        return (bool) (call_user_func($this->statuses[$status], $this));
458
    }
459
460
    /**
461
     * @internal
462
     */
463
    public function applyEffectProviders(): void
464
    {
465 1
        foreach ($this->effectProviders as $item) {
466 1
            $effects = $item->getCombatEffects();
467 1
            array_walk($effects, function (CharacterEffect $effect): void {
468 1
                $this->effects->removeByFilter(["id" => $effect->id]);
469 1
                $this->effects[] = $effect;
470 1
            });
471
        }
472 1
    }
473
474
    /**
475
     * @return BaseCharacterSkill[]
476
     */
477
    protected function getUsableSkills(): array
478
    {
479 1
        return $this->skills->getItems(["usable" => true]);
480
    }
481
482
    /**
483
     * Harm the character
484
     */
485
    public function harm(int $amount): void
486
    {
487 1
        $this->hitpoints -= Numbers::range($amount, 0, $this->hitpoints);
488 1
    }
489
490
    /**
491
     * Heal the character
492
     */
493
    public function heal(int $amount): void
494
    {
495 1
        $this->hitpoints += Numbers::range($amount, 0, $this->maxHitpoints - $this->hitpoints);
496 1
    }
497
498
    /**
499
     * Determine which (primary) stat should be used to calculate damage
500
     */
501
    public function damageStat(): string
502
    {
503
        /** @var Weapon|null $item */
504 1
        $item = $this->equipment->getItem(["%class%" => Weapon::class, "worn" => true,]);
505 1
        if ($item === null) {
506 1
            return static::STAT_STRENGTH;
507
        }
508 1
        return $item->damageStat;
509
    }
510
511
    /**
512
     * Recalculate secondary stats from the the primary ones
513
     */
514
    public function recalculateSecondaryStats(): void
515
    {
516 1
        $stats = [
517 1
            static::STAT_DAMAGE => $this->damageStat(), static::STAT_HIT => static::STAT_DEXTERITY,
518 1
            static::STAT_DODGE => static::STAT_DEXTERITY, static::STAT_MAX_HITPOINTS => static::STAT_CONSTITUTION,
519 1
            static::STAT_INITIATIVE => "",
520
        ];
521 1
        foreach ($stats as $secondary => $primary) {
522 1
            $gain = $this->$secondary - $this->{$secondary . "Base"};
523 1
            if ($secondary === static::STAT_DAMAGE) {
524 1
                $base = (int) round($this->$primary / 2);
525 1
            } elseif ($secondary === static::STAT_MAX_HITPOINTS) {
526 1
                $base = $this->$primary * static::HITPOINTS_PER_CONSTITUTION;
527 1
            } elseif ($secondary === static::STAT_INITIATIVE) {
528 1
                $base = $this->initiativeFormulaParser->calculateInitiative($this);
529
            } else {
530 1
                $base = $this->$primary * 3;
531
            }
532 1
            $this->{$secondary . "Base"} = $base;
533 1
            $this->$secondary = $base + $gain;
534
        }
535 1
    }
536
537
    /**
538
     * Recalculates stats of the character (mostly used during combat)
539
     */
540
    public function recalculateStats(): void
541
    {
542 1
        $stats = array_merge(static::BASE_STATS, static::SECONDARY_STATS);
543 1
        $debuffs = [];
544 1
        foreach ($stats as $stat) {
545 1
            $$stat = $this->{$stat . "Base"};
546 1
            $debuffs[$stat] = 0;
547
        }
548 1
        $this->effects->removeByFilter(["duration<" => 1]);
549 1
        foreach ($this->effects as $effect) {
550 1
            $stat = $effect->stat;
551 1
            $type = $effect->type;
552 1
            $bonus_value = 0;
553 1
            if (!in_array($type, SkillSpecial::NO_STAT_TYPES, true)) {
554 1
                $bonus_value = ($effect->valueAbsolute) ? $effect->value : $$stat / 100 * $effect->value;
555
            }
556 1
            if ($type === SkillSpecial::TYPE_BUFF) {
557 1
                $$stat += $bonus_value;
558 1
            } elseif ($type === SkillSpecial::TYPE_DEBUFF) {
559 1
                $debuffs[$stat] += $bonus_value;
560
            }
561 1
            unset($stat, $type, $bonus_value);
562
        }
563 1
        foreach ($debuffs as $stat => $value) {
564 1
            $value = min($value, 80);
565 1
            $bonus_value = $$stat / 100 * $value;
566 1
            $$stat -= $bonus_value;
567
        }
568 1
        foreach ($stats as $stat) {
569 1
            $this->$stat = (int) round($$stat);
570
        }
571 1
        $this->recalculateSecondaryStats();
572 1
    }
573
574
    /**
575
     * Reset character's initiative
576
     */
577
    public function resetInitiative(): void
578
    {
579 1
        $this->initiative = $this->initiativeBase = 0;
580 1
    }
581
582
    public function canAct(): bool
583
    {
584 1
        return !$this->hasStatus(static::STATUS_STUNNED) && $this->hitpoints > 0;
585
    }
586
587
    public function canDefend(): bool
588
    {
589 1
        return !$this->hasStatus(static::STATUS_STUNNED);
590
    }
591
}
592