Passed
Push — master ( 9ad4f6...7fb37f )
by Jakub
01:42
created

Character.php$2 ➔ getInitiativeFormulaParser()   A

Complexity

Conditions 1

Size

Total Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

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