Passed
Push — master ( f80d3f...9f26a1 )
by Jakub
02:54
created

Character::getPets()

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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