Completed
Push — master ( 54fdf8...606b80 )
by Jakub
02:17
created

Character   F

Complexity

Total Complexity 98

Size/Duplication

Total Lines 534
Duplicated Lines 0 %

Test Coverage

Coverage 76.8%

Importance

Changes 0
Metric Value
wmc 98
dl 0
loc 534
ccs 149
cts 194
cp 0.768
rs 1.5789
c 0
b 0
f 0

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