Completed
Push — master ( 25ba12...d7e08b )
by Jakub
02:28
created

Character   F

Complexity

Total Complexity 101

Size/Duplication

Total Lines 563
Duplicated Lines 0 %

Test Coverage

Coverage 74.88%

Importance

Changes 0
Metric Value
wmc 101
dl 0
loc 563
ccs 155
cts 207
cp 0.7488
rs 1.5789
c 0
b 0
f 0

55 Methods

Rating   Name   Duplication   Size   Complexity  
A getSkills() 0 2 1
A getDamageBase() 0 2 1
A getCharisma() 0 2 1
A getIntelligenceBase() 0 2 1
A addEffect() 0 3 1
A getDodgeBase() 0 2 1
A getDexterity() 0 2 1
A harm() 0 2 1
A getIntelligence() 0 2 1
A getActivePet() 0 2 1
A getHitpoints() 0 2 1
A getMaxHitpoints() 0 2 1
A getPets() 0 2 1
B setStats() 0 37 5
A getInitiative() 0 2 1
A getItem() 0 5 2
A getStrengthBase() 0 2 1
A getName() 0 2 1
A getId() 0 2 1
A equipItem() 0 8 2
A getSpecialization() 0 2 1
A getLevel() 0 2 1
A getStrength() 0 2 1
A getDefenseBase() 0 2 1
A getOccupation() 0 2 1
A getEffects() 0 2 1
A removeEffect() 0 9 3
A getInitiativeBase() 0 2 1
A getCharismaBase() 0 2 1
A getExperience() 0 2 1
A isStunned() 0 2 1
D recalculateStats() 0 54 16
A unequipItem() 0 8 2
A getHit() 0 2 1
A getDamage() 0 2 1
A getConstitutionBase() 0 2 1
B __construct() 0 18 8
A resetInitiative() 0 2 1
A getGender() 0 2 1
A getDodge() 0 2 1
A getEquipment() 0 2 1
A heal() 0 2 1
A getPet() 0 5 3
A getHitBase() 0 2 1
A getDexterityBase() 0 2 1
A dismissPet() 0 2 1
A getDefense() 0 2 1
A getRace() 0 2 1
A deployPet() 0 7 2
B damageStat() 0 20 8
A recalculateSecondaryStats() 0 17 3
A calculateInitiative() 0 10 2
A getInitiativeFormula() 0 2 1
A getUsableSkills() 0 8 3
A getConstitution() 0 2 1

How to fix   Complexity   

Complex Class

Complex classes like Character often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Character, and based on these observations, apply Extract Interface, too.

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