Completed
Push — master ( 10c9e5...cb21fe )
by Jakub
02:04
created

Character::setStats()   B

Complexity

Conditions 5
Paths 12

Size

Total Lines 37
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 31
CRAP Score 5

Importance

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