Passed
Push — master ( bc69e6...465240 )
by Jakub
12:35
created

Character   F

Complexity

Total Complexity 83

Size/Duplication

Total Lines 475
Duplicated Lines 0 %

Test Coverage

Coverage 92.35%

Importance

Changes 14
Bugs 2 Features 0
Metric Value
wmc 83
eloc 227
dl 0
loc 475
rs 2
c 14
b 2
f 0
ccs 169
cts 183
cp 0.9235

57 Methods

Rating   Name   Duplication   Size   Complexity  
A getDamageBase() 0 2 1
A setPositionColumn() 0 2 1
A getCharisma() 0 2 1
A getIntelligenceBase() 0 2 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 2
A getHitpoints() 0 2 1
A getMaxHitpoints() 0 2 1
A getPositionColumn() 0 2 1
A canDefend() 0 5 2
A setStats() 0 36 5
A getInitiative() 0 2 1
A getId() 0 2 1
A getName() 0 2 1
A getStrengthBase() 0 2 1
A getSpecialization() 0 2 1
A getLevel() 0 2 1
A getStrength() 0 2 1
A getPositionRow() 0 2 1
A getDefenseBase() 0 2 1
A getStatus() 0 5 2
A getOccupation() 0 2 1
A setInitiativeFormulaParser() 0 5 2
A isPoisoned() 0 2 1
A setPositionRow() 0 2 1
A getCharismaBase() 0 2 1
A registerDefaultStatuses() 0 15 2
A getInitiativeBase() 0 2 1
A isStunned() 0 2 1
A applyEffectProviders() 0 6 2
B recalculateStats() 0 31 9
A getHit() 0 2 1
A getDamage() 0 2 1
A getConstitutionBase() 0 2 1
A __construct() 0 12 1
A getGender() 0 2 1
A resetInitiative() 0 2 1
A getDodge() 0 2 1
A getMaxHitpointsBase() 0 2 1
A heal() 0 2 1
A getInitiativeFormulaParser() 0 2 1
A getHitBase() 0 2 1
A getDexterityBase() 0 2 1
A hasStatus() 0 5 2
A canAct() 0 7 3
A registerStatus() 0 2 1
A isHidden() 0 2 1
A getDefense() 0 2 1
A getRace() 0 2 1
A damageStat() 0 7 2
A recalculateSecondaryStats() 0 19 5
A getInitiativeFormula() 0 2 1
A getUsableSkills() 0 2 1
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
use 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 string $specialization
19
 * @property-read int $level
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 int|null $activePet
45
 * @property-read bool $stunned
46
 * @property-read bool $poisoned
47
 * @property-read bool $hidden
48
 * @property-read BaseCharacterSkill[] $usableSkills
49
 * @property IInitiativeFormulaParser $initiativeFormulaParser
50
 * @property int $positionRow
51
 * @property int $positionColumn
52
 */
53 1
class Character {
54
  use \Nette\SmartObject;
55
  
56
  public const HITPOINTS_PER_CONSTITUTION = 5;
57
  public const STAT_STRENGTH = "strength";
58
  public const STAT_DEXTERITY = "dexterity";
59
  public const STAT_CONSTITUTION = "constitution";
60
  public const STAT_INTELLIGENCE = "intelligence";
61
  public const STAT_CHARISMA = "charisma";
62
  public const STAT_MAX_HITPOINTS = "maxHitpoints";
63
  public const STAT_DAMAGE = "damage";
64
  public const STAT_DEFENSE = "defense";
65
  public const STAT_HIT = "hit";
66
  public const STAT_DODGE = "dodge";
67
  public const STAT_INITIATIVE = "initiative";
68
  public const BASE_STATS = [
69
    self::STAT_STRENGTH, self::STAT_DEXTERITY, self::STAT_CONSTITUTION, self::STAT_INTELLIGENCE, self::STAT_CHARISMA,
70
  ];
71
  public const SECONDARY_STATS = [
72
    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...
73
  ];
74
  public const STATUS_STUNNED = "stunned";
75
  public const STATUS_POISONED = "poisoned";
76
  public const STATUS_HIDDEN = "hidden";
77
78
  protected int|string $id;
79
  protected string $name;
80
  protected string $gender = "male";
81
  protected string $race;
82
  protected string $occupation;
83
  protected string $specialization;
84
  protected int $level;
85
  protected int $strength;
86
  protected int $strengthBase;
87
  protected int $dexterity;
88
  protected int $dexterityBase;
89
  protected int $constitution;
90
  protected int $constitutionBase;
91
  protected int $intelligence;
92
  protected int $intelligenceBase;
93
  protected int $charisma;
94
  protected int $charismaBase;
95
  protected int $maxHitpoints;
96
  protected int $maxHitpointsBase;
97
  protected int $hitpoints;
98
  protected int $damage = 0;
99
  protected int $damageBase = 0;
100
  protected int $hit = 0;
101
  protected int $hitBase = 0;
102
  protected int $dodge = 0;
103
  protected int $dodgeBase = 0;
104
  protected int $initiative = 0;
105
  protected int $initiativeBase = 0;
106
  protected string $initiativeFormula;
107
  protected IInitiativeFormulaParser $initiativeFormulaParser;
108
  protected float $defense = 0;
109
  protected float $defenseBase = 0;
110
  /** @var Equipment[]|EquipmentCollection Character's equipment */
111
  public EquipmentCollection $equipment;
112
  /** @var Pet[]|PetsCollection Character's pets */
113
  public PetsCollection $pets;
114
  /** @var BaseCharacterSkill[]|CharacterSkillsCollection Character's skills */
115
  public CharacterSkillsCollection $skills;
116
  protected ?int $activePet = null;
117
  /** @var CharacterEffect[]|CharacterEffectsCollection Active effects */
118
  public CharacterEffectsCollection $effects;
119
  /** @var ICharacterEffectsProvider[]|CharacterEffectsProvidersCollection */
120
  public CharacterEffectsProvidersCollection $effectProviders;
121
  protected int $positionRow = 0;
122
  protected int $positionColumn = 0;
123
  /** @var callable[] */
124
  protected array $statuses = [];
125
  
126
  /**
127
   *
128
   * @param array $stats Stats of the character
129
   * @param Equipment[] $equipment Equipment of the character
130
   * @param Pet[] $pets Pets owned by the character
131
   * @param BaseCharacterSkill[] $skills Skills acquired by the character
132
   */
133
  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...
134 1
    $this->initiativeFormulaParser = $initiativeFormulaParser ?? new InitiativeFormulaParser();
135 1
    $this->equipment = EquipmentCollection::fromArray($equipment);
136 1
    $this->pets = PetsCollection::fromArray($pets);
137 1
    $this->effectProviders = CharacterEffectsProvidersCollection::fromArray(array_merge($equipment, $pets));
138 1
    $this->skills = CharacterSkillsCollection::fromArray($skills);
139 1
    $this->equipment->lock();
140 1
    $this->pets->lock();
141 1
    $this->skills->lock();
142 1
    $this->setStats($stats);
143 1
    $this->effects = new CharacterEffectsCollection($this);
144 1
    $this->registerDefaultStatuses();
145 1
  }
146
147
  protected function registerDefaultStatuses(): void {
148 1
    $this->registerStatus(static::STATUS_STUNNED, function(Character $character): bool {
149 1
      return $character->effects->hasItems(["type" => SkillSpecial::TYPE_STUN]);
150 1
    });
151 1
    $this->registerStatus(static::STATUS_POISONED, function(Character $character): int {
152 1
      $poisons = $character->effects->getItems(["type" => SkillSpecial::TYPE_POISON]);
153 1
      $poisonValue = 0;
154
      /** @var CharacterEffect $poison */
155 1
      foreach($poisons as $poison) {
156 1
        $poisonValue += $poison->value;
157
      }
158 1
      return $poisonValue;
159 1
    });
160 1
    $this->registerStatus(static::STATUS_HIDDEN, function(Character $character): bool {
161 1
      return $character->effects->hasItems(["type" => SkillSpecial::TYPE_HIDE]);
162 1
    });
163 1
  }
164
  
165
  protected function setStats(array $stats): void {
166 1
    $requiredStats = array_merge(["id", "name", "level", "initiativeFormula", ], static::BASE_STATS);
167 1
    $allStats = array_merge($requiredStats, ["occupation", "race", "specialization", "gender", ]);
168 1
    $numberStats = static::BASE_STATS;
169 1
    $textStats = ["name", "race", "occupation", "specialization", "initiativeFormula", ];
170 1
    $resolver = new OptionsResolver();
171 1
    $resolver->setDefined($allStats);
172 1
    $resolver->setAllowedTypes("id", ["integer", "string", ]);
173 1
    foreach($numberStats as $stat) {
174 1
      $resolver->setAllowedTypes($stat, ["integer", "float"]);
175 1
      $resolver->setNormalizer($stat, function(OptionsResolver $resolver, $value): int {
176 1
        return (int) $value;
177 1
      });
178
    }
179 1
    foreach($textStats as $stat) {
180 1
      $resolver->setNormalizer($stat, function(OptionsResolver $resolver, $value): string {
181 1
        return (string) $value;
182 1
      });
183
    }
184 1
    $resolver->setRequired($requiredStats);
185 1
    $stats = array_filter($stats, function($key) use($allStats): bool {
186 1
      return in_array($key, $allStats, true);
187 1
    }, ARRAY_FILTER_USE_KEY);
188 1
    $stats = $resolver->resolve($stats);
189 1
    foreach($stats as $key => $value) {
190 1
      if(in_array($key, $numberStats, true)) {
191 1
        $this->$key = $value;
192 1
        $this->{$key . "Base"} = $value;
193
      } else {
194 1
        $this->$key = $value;
195
      }
196
    }
197 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...
198 1
    $this->recalculateSecondaryStats();
199 1
    $this->hitBase = $this->hit;
200 1
    $this->dodgeBase = $this->dodge;
201 1
  }
202
203
  protected function getId(): int|string {
204 1
    return $this->id;
205
  }
206
207
  protected function getName(): string {
208 1
    return $this->name;
209
  }
210
  
211
  protected function getGender(): string {
212
    return $this->gender;
213
  }
214
  
215
  protected function getRace(): string {
216
    return $this->race;
217
  }
218
  
219
  protected function getOccupation(): string {
220
    return $this->occupation;
221
  }
222
  
223
  protected function getLevel(): int {
224 1
    return $this->level;
225
  }
226
  
227
  protected function getStrength(): int {
228 1
    return $this->strength;
229
  }
230
  
231
  protected function getStrengthBase(): int {
232
    return $this->strengthBase;
233
  }
234
  
235
  protected function getDexterity(): int {
236 1
    return $this->dexterity;
237
  }
238
  
239
  protected function getDexterityBase(): int {
240
    return $this->dexterityBase;
241
  }
242
  
243
  protected function getConstitution(): int {
244 1
    return $this->constitution;
245
  }
246
  
247
  protected function getConstitutionBase(): int {
248
    return $this->constitutionBase;
249
  }
250
  
251
  protected function getCharisma(): int {
252 1
    return $this->charisma;
253
  }
254
  
255
  protected function getCharismaBase(): int {
256
    return $this->charismaBase;
257
  }
258
  
259
  protected function getMaxHitpoints(): int {
260 1
    return $this->maxHitpoints;
261
  }
262
  
263
  protected function getMaxHitpointsBase(): int {
264 1
    return $this->maxHitpointsBase;
265
  }
266
  
267
  protected function getHitpoints(): int {
268 1
    return $this->hitpoints;
269
  }
270
  
271
  protected function getDamage(): int {
272 1
    return $this->damage;
273
  }
274
  
275
  protected function getDamageBase(): int {
276
    return $this->damageBase;
277
  }
278
  
279
  protected function getHit(): int {
280 1
    return $this->hit;
281
  }
282
  
283
  protected function getHitBase(): int {
284
    return $this->hitBase;
285
  }
286
  
287
  protected function getDodge(): int {
288 1
    return $this->dodge;
289
  }
290
  
291
  protected function getDodgeBase(): int {
292
    return $this->dodgeBase;
293
  }
294
  
295
  protected function getInitiative(): int {
296 1
    return $this->initiative;
297
  }
298
  
299
  protected function getInitiativeBase(): int {
300 1
    return $this->initiativeBase;
301
  }
302
  
303
  protected function getInitiativeFormula(): string {
304 1
    return $this->initiativeFormula;
305
  }
306
  
307
  protected function getDefense(): int {
308 1
    return (int) $this->defense;
309
  }
310
  
311
  protected function getDefenseBase(): int {
312
    return (int) $this->defenseBase;
313
  }
314
315
  protected function getActivePet(): ?int {
316
    /** @var Pet|null $pet */
317 1
    $pet = $this->pets->getItem(["deployed" => true]);
318 1
    if($pet === null) {
319 1
      return null;
320
    }
321 1
    return $pet->id;
322
  }
323
  
324
  protected function isStunned(): bool {
325
    return $this->hasStatus(static::STATUS_STUNNED);
326
  }
327
328
  protected function isPoisoned(): bool {
329 1
    return $this->hasStatus(static::STATUS_POISONED);
330
  }
331
332
  protected function isHidden(): bool {
333 1
    return $this->hasStatus(static::STATUS_HIDDEN);
334
  }
335
  
336
  protected function getSpecialization(): string {
337
    return $this->specialization;
338
  }
339
  
340
  protected function getIntelligence(): int {
341 1
    return $this->intelligence;
342
  }
343
  
344
  protected function getIntelligenceBase(): int {
345
    return $this->intelligenceBase;
346
  }
347
  
348
  protected function getInitiativeFormulaParser(): IInitiativeFormulaParser {
349 1
    return $this->initiativeFormulaParser;
350
  }
351
  
352
  protected function setInitiativeFormulaParser(IInitiativeFormulaParser $initiativeFormulaParser): void {
353 1
    $oldParser = $this->initiativeFormulaParser;
354 1
    $this->initiativeFormulaParser = $initiativeFormulaParser;
355 1
    if($oldParser !== $initiativeFormulaParser) {
356 1
      $this->recalculateStats();
357
    }
358 1
  }
359
  
360
  protected function getPositionRow(): int {
361 1
    return $this->positionRow;
362
  }
363
  
364
  protected function setPositionRow(int $positionRow): void {
365 1
    $this->positionRow = Numbers::range($positionRow, 1, PHP_INT_MAX);
366 1
  }
367
  
368
  protected function getPositionColumn(): int {
369 1
    return $this->positionColumn;
370
  }
371
  
372
  protected function setPositionColumn(int $positionColumn): void {
373 1
    $this->positionColumn = Numbers::range($positionColumn, 1, PHP_INT_MAX);
374 1
  }
375
376
  /**
377
   * Register a new possible character status
378
   *
379
   * @param string $name
380
   * @param callable $callback Used to determine whether the status is active/what value does it have. Is called with Character instance as parameter
0 ignored issues
show
introduced by
Line exceeds 120 characters; contains 149 characters
Loading history...
381
   */
382
  public function registerStatus(string $name, callable $callback): void {
383 1
    $this->statuses[$name] = $callback;
384 1
  }
385
386
  public function getStatus(string $name): mixed {
387 1
    if(!array_key_exists($name, $this->statuses)) {
388 1
      return null;
389
    }
390 1
    return (call_user_func($this->statuses[$name], $this));
391
  }
392
393
  public function hasStatus(string $status): bool {
394 1
    if(!array_key_exists($status, $this->statuses)) {
395 1
      return false;
396
    }
397 1
    return (bool) (call_user_func($this->statuses[$status], $this));
398
  }
399
  
400
  /**
401
   * @internal
402
   */
403
  public function applyEffectProviders(): void {
404 1
    foreach($this->effectProviders as $item) {
405 1
      $effects = $item->getCombatEffects();
406 1
      array_walk($effects, function(CharacterEffect $effect): void {
407 1
        $this->effects->removeByFilter(["id" => $effect->id]);
408 1
        $this->effects[] = $effect;
409 1
      });
410
    }
411 1
  }
412
  
413
  /**
414
   * @return BaseCharacterSkill[]
415
   */
416
  protected function getUsableSkills(): array {
417 1
    return $this->skills->getItems(["usable" => true]);
418
  }
419
  
420
  /**
421
   * Harm the character
422
   */
423
  public function harm(int $amount): void {
424 1
    $this->hitpoints -= Numbers::range($amount, 0, $this->hitpoints);
425 1
  }
426
  
427
  /**
428
   * Heal the character
429
   */
430
  public function heal(int $amount): void {
431 1
    $this->hitpoints += Numbers::range($amount, 0, $this->maxHitpoints - $this->hitpoints);
432 1
  }
433
  
434
  /**
435
   * Determine which (primary) stat should be used to calculate damage
436
   */
437
  public function damageStat(): string {
438
    /** @var Weapon|null $item */
439 1
    $item = $this->equipment->getItem(["%class%" => Weapon::class, "worn" => true, ]);
440 1
    if($item === null) {
441 1
      return static::STAT_STRENGTH;
442
    }
443 1
    return $item->damageStat;
444
  }
445
  
446
  /**
447
   * Recalculate secondary stats from the the primary ones
448
   */
449
  public function recalculateSecondaryStats(): void {
450 1
    $stats = [
451 1
      static::STAT_DAMAGE => $this->damageStat(), static::STAT_HIT => static::STAT_DEXTERITY,
452 1
      static::STAT_DODGE => static::STAT_DEXTERITY, static::STAT_MAX_HITPOINTS => static::STAT_CONSTITUTION,
453 1
      static::STAT_INITIATIVE => "",
454
    ];
455 1
    foreach($stats as $secondary => $primary) {
456 1
      $gain = $this->$secondary - $this->{$secondary . "Base"};
457 1
      if($secondary === static::STAT_DAMAGE) {
458 1
        $base = (int) round($this->$primary / 2);
459 1
      } elseif($secondary === static::STAT_MAX_HITPOINTS) {
460 1
        $base = $this->$primary * static::HITPOINTS_PER_CONSTITUTION;
461 1
      } elseif($secondary === static::STAT_INITIATIVE) {
462 1
        $base = $this->initiativeFormulaParser->calculateInitiative($this);
463
      } else {
464 1
        $base = $this->$primary * 3;
465
      }
466 1
      $this->{$secondary . "Base"} = $base;
467 1
      $this->$secondary = $base + $gain;
468
    }
469 1
  }
470
  
471
  /**
472
   * Recalculates stats of the character (mostly used during combat)
473
   */
474
  public function recalculateStats(): void {
475 1
    $stats = array_merge(static::BASE_STATS, static::SECONDARY_STATS);
476 1
    $debuffs = [];
477 1
    foreach($stats as $stat) {
478 1
      $$stat = $this->{$stat . "Base"};
479 1
      $debuffs[$stat] = 0;
480
    }
481 1
    $this->effects->removeByFilter(["duration<" => 1]);
482 1
    foreach($this->effects as $effect) {
483 1
      $stat = $effect->stat;
484 1
      $type = $effect->type;
485 1
      $bonus_value = 0;
486 1
      if(!in_array($type, SkillSpecial::NO_STAT_TYPES, true)) {
487 1
        $bonus_value = ($effect->valueAbsolute) ? $effect->value : $$stat / 100 * $effect->value;
488
      }
489 1
      if($type === SkillSpecial::TYPE_BUFF) {
490 1
        $$stat += $bonus_value;
491 1
      } elseif($type === SkillSpecial::TYPE_DEBUFF) {
492 1
        $debuffs[$stat] += $bonus_value;
493
      }
494 1
      unset($stat, $type, $bonus_value);
495
    }
496 1
    foreach($debuffs as $stat => $value) {
497 1
      $value = min($value, 80);
498 1
      $bonus_value = $$stat / 100 * $value;
499 1
      $$stat -= $bonus_value;
500
    }
501 1
    foreach($stats as $stat) {
502 1
      $this->$stat = (int) round($$stat);
503
    }
504 1
    $this->recalculateSecondaryStats();
505 1
  }
506
  
507
  /**
508
   * Reset character's initiative
509
   */
510
  public function resetInitiative(): void {
511 1
    $this->initiative = $this->initiativeBase = 0;
512 1
  }
513
514
  public function canAct(): bool {
515 1
    if($this->hasStatus(static::STATUS_STUNNED)) {
516 1
      return false;
517 1
    } elseif($this->hitpoints < 1) {
518 1
      return false;
519
    }
520 1
    return true;
521
  }
522
523
  public function canDefend(): bool {
524 1
    if($this->hasStatus(static::STATUS_STUNNED)) {
525 1
      return false;
526
    }
527 1
    return true;
528
  }
529
}
530
?>