Passed
Push — master ( f67b76...ea1fdc )
by Jakub
02:54
created

Character.php$3 ➔ getDefenseBase()   A

Complexity

Conditions 1

Size

Total Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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