Passed
Push — master ( f3e1d4...54fdf8 )
by Jakub
02:24
created

Character::addEffectProvider()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

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