Completed
Push — master ( 46997b...5a9e97 )
by Jakub
03:13
created

Character.php$3 ➔ canDefend()   A

Complexity

Conditions 2

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

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