Completed
Push — master ( 7c8b7e...f3e1d4 )
by Jakub
02:23
created

Character::heal()   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 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 $maxHitpointsBase;
95
  /** @var int */
96
  protected $hitpoints;
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->hitpoints = $this->maxHitpoints = $this->maxHitpointsBase = $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 1
    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 getMaxHitpointsBase(): int {
268 1
    return $this->maxHitpointsBase;
269
  }
270
  
271
  public function getHitpoints(): int {
272 1
    return $this->hitpoints;
273
  }
274
  
275
  public function getDamage(): int {
276 1
    return $this->damage;
277
  }
278
  
279
  public function getDamageBase(): int {
280
    return $this->damageBase;
281
  }
282
  
283
  public function getHit(): int {
284 1
    return $this->hit;
285
  }
286
  
287
  public function getHitBase(): int {
288
    return $this->hitBase;
289
  }
290
  
291
  public function getDodge(): int {
292 1
    return $this->dodge;
293
  }
294
  
295
  public function getDodgeBase(): int {
296
    return $this->dodgeBase;
297
  }
298
  
299
  public function getInitiative(): int {
300 1
    return $this->initiative;
301
  }
302
  
303
  public function getInitiativeBase(): int {
304
    return $this->initiativeBase;
305
  }
306
  
307
  public function getInitiativeFormula(): string {
308
    return $this->initiativeFormula;
309
  }
310
  
311
  public function getDefense(): int {
312 1
    return (int) $this->defense;
313
  }
314
  
315
  public function getDefenseBase(): int {
316
    return (int) $this->defenseBase;
317
  }
318
  
319
  /**
320
   * @return Equipment[]
321
   */
322
  public function getEquipment(): array {
323 1
    return $this->equipment;
324
  }
325
  
326
  /**
327
   * @return Pet[]
328
   */
329
  public function getPets(): array {
330
    return $this->pets;
331
  }
332
  
333
  /**
334
   * @return BaseCharacterSkill[]
335
   */
336
  public function getSkills(): array {
337 1
    return $this->skills;
338
  }
339
  
340
  public function getActivePet(): ?int {
341 1
    return $this->activePet;
342
  }
343
  
344
  /**
345
   * @return CharacterEffect[]
346
   */
347
  public function getEffects(): array {
348 1
    return $this->effects;
349
  }
350
  
351
  public function isStunned(): bool {
352 1
    return $this->stunned;
353
  }
354
  
355
  public function getSpecialization(): string {
356
    return $this->specialization;
357
  }
358
  
359
  public function getIntelligence(): int {
360 1
    return $this->intelligence;
361
  }
362
  
363
  public function getIntelligenceBase(): int {
364
    return $this->intelligenceBase;
365
  }
366
  
367
  /**
368
   * Applies new effect on the character
369
   */
370
  public function addEffect(CharacterEffect $effect): void {
371 1
    $this->effects[] = $effect;
372 1
    $this->recalculateStats();
373 1
  }
374
  
375
  /**
376
   * Removes specified effect from the character
377
   *
378
   * @throws \OutOfBoundsException
379
   */
380
  public function removeEffect(string $effectId): void {
381 1
    foreach($this->effects as $i => $effect) {
382 1
      if($effect->id == $effectId) {
383 1
        unset($this->effects[$i]);
384 1
        $effect->onRemove($this, $effect);
385 1
        $this->recalculateStats();
386 1
        return;
387
      }
388
    }
389
    throw new \OutOfBoundsException("Effect to remove was not found.");
390
  }
391
  
392
  /**
393
   * Get specified equipment of the character
394
   *
395
   * @throws \OutOfBoundsException
396
   */
397
  public function getItem(int $itemId): Equipment {
398 1
    if(isset($this->equipment[$itemId])) {
399 1
      return $this->equipment[$itemId];
400
    }
401
    throw new \OutOfBoundsException("Item was not found.");
402
  }
403
  
404
  /**
405
   * Equips an owned item
406
   *
407
   * @throws \OutOfBoundsException
408
   */
409
  public function equipItem(int $itemId): void {
410
    try {
411 1
      $item = $this->getItem($itemId);
412
    } catch (\OutOfBoundsException $e) {
413
      throw $e;
414
    }
415 1
    $itemBonus = new CharacterEffect($item->deployParams);
416 1
    $this->addEffect($itemBonus);
417 1
    $itemBonus->onApply($this, $itemBonus);
418 1
  }
419
  
420
  /**
421
   * Unequips an item
422
   *
423
   * @throws \OutOfBoundsException
424
   */
425
  public function unequipItem(int $itemId): void {
426
    try {
427
      $item = $this->getItem($itemId);
428
    } catch (\OutOfBoundsException $e) {
429
      throw $e;
430
    }
431
    $itemBonus = $item->deployParams;
432
    $this->removeEffect($itemBonus["id"]);
433
  }
434
  
435
  /**
436
   * Get specified pet
437
   *
438
   * @throws \OutOfBoundsException
439
   */
440
  public function getPet(int $petId): Pet {
441 1
    if(isset($this->pets[$petId]) AND $this->pets[$petId] instanceof Pet) {
442 1
      return $this->pets[$petId];
443
    }
444
    throw new \OutOfBoundsException("Pet was not found.");
445
  }
446
  
447
  /**
448
   * Deploy specified pet (for bonuses)
449
   *
450
   * @throws \OutOfBoundsException
451
   */
452
  public function deployPet(int $petId): void {
453
    try {
454 1
      $this->getPet($petId);
455
    } catch(\OutOfBoundsException $e) {
456
      throw $e;
457
    }
458 1
    $this->activePet = $petId;
459 1
  }
460
  
461
  /**
462
   * Dismisses active pet
463
   */
464
  public function dismissPet(): void {
465
    $this->activePet = NULL;
466
  }
467
  
468
  /**
469
   * @return BaseCharacterSkill[]
470
   */
471
  public function getUsableSkills(): array {
472 1
    $skills = [];
473 1
    foreach($this->skills as $skill) {
474 1
      if($skill->canUse()) {
475 1
        $skills[] = $skill;
476
      }
477
    }
478 1
    return $skills;
479
  }
480
  
481
  /**
482
   * Harm the character
483
   */
484
  public function harm(int $amount): void {
485 1
    $this->hitpoints -= Numbers::range($amount, 0, $this->hitpoints);
486 1
  }
487
  
488
  /**
489
   * Heal the character
490
   */
491
  public function heal(int $amount): void {
492 1
    $this->hitpoints += Numbers::range($amount, 0, $this->maxHitpoints - $this->hitpoints);
493 1
  }
494
  
495
  /**
496
   * Determine which (primary) stat should be used to calculate damage
497
   */
498
  public function damageStat(): string {
499 1
    $stat = "strength";
500 1
    foreach($this->equipment as $item) {
501 1
      if(!$item->worn OR $item->slot != Equipment::SLOT_WEAPON) {
502
        continue;
503
      }
504 1
      switch($item->type) {
505 1
        case Equipment::TYPE_STAFF:
506
          $stat = "intelligence";
507
          break;
508 1
        case Equipment::TYPE_CLUB:
509
          $stat = "constitution";
510
          break;
511 1
        case Equipment::TYPE_BOW:
512 1
        case Equipment::TYPE_THROWING_KNIFE:
513
          $stat = "dexterity";
514 1
          break;
515
      }
516
    }
517 1
    return $stat;
518
  }
519
  
520
  /**
521
   * Recalculate secondary stats from the the primary ones
522
   */
523
  public function recalculateSecondaryStats(): void {
524
    $stats = [
525 1
      "damage" => $this->damageStat(), "hit" => "dexterity", "dodge" => "dexterity", "maxHitpoints" => "constitution",
526
    ];
527 1
    foreach($stats as $secondary => $primary) {
528 1
      $gain = $this->$secondary - $this->{$secondary . "Base"};
529 1
      if($secondary === "damage") {
530 1
        $base = (int) round($this->$primary / 2);
531 1
      } elseif($secondary === "maxHitpoints") {
532 1
        $base = $this->$primary * 5;
533
      } else {
534 1
        $base = $this->$primary * 3;
535
      }
536 1
      $this->{$secondary . "Base"} = $base;
537 1
      $this->$secondary = $base + $gain;
538
    }
539 1
  }
540
  
541
  /**
542
   * Recalculates stats of the character (mostly used during combat)
543
   */
544
  public function recalculateStats(): void {
0 ignored issues
show
introduced by
Function's cyclomatic complexity (14) exceeds 10; consider refactoring the function
Loading history...
545
    $stats = [
546 1
      "strength", "dexterity", "constitution", "intelligence", "charisma",
547
      "damage", "hit", "dodge", "initiative", "defense", "maxHitpoints"
548
    ];
549 1
    $stunned = false;
550 1
    $debuffs = [];
551 1
    foreach($stats as $stat) {
552 1
      $$stat = $this->{$stat . "Base"};
553 1
      $debuffs[$stat] = 0;
554
    }
555 1
    foreach($this->effects as $i => $effect) {
556 1
      $stat = $effect->stat;
557 1
      $type = $effect->type;
558 1
      $duration = $effect->duration;
559 1
      if(is_int($duration) AND $duration < 0) {
560
        unset($this->effects[$i]);
561
        continue;
562
      }
563 1
      switch($effect->source) {
564 1
        case CharacterEffect::SOURCE_PET:
565 1
        case CharacterEffect::SOURCE_SKILL:
566 1
          if(!in_array($type, SkillSpecial::NO_STAT_TYPES, true)) {
567 1
            $bonus_value = $$stat / 100 * $effect->value;
568
          }
569 1
          break;
570 1
        case CharacterEffect::SOURCE_EQUIPMENT:
571 1
          if(!in_array($type, SkillSpecial::NO_STAT_TYPES, true)) {
572 1
            $bonus_value = $effect->value;
573
          }
574 1
          break;
575
      }
576 1
      if($type == SkillSpecial::TYPE_BUFF) {
577 1
        $$stat += $bonus_value;
578
      } elseif($type == SkillSpecial::TYPE_DEBUFF) {
579
        $debuffs[$stat] += $bonus_value;
580
      } elseif($type == SkillSpecial::TYPE_STUN) {
581
        $stunned = true;
582
      }
583 1
      unset($stat, $type, $duration, $bonus_value);
584
    }
585 1
    foreach($debuffs as $stat => $value) {
586 1
      $value = min($value, 80);
587 1
      $bonus_value = $$stat / 100 * $value;
588 1
      $$stat -= $bonus_value;
589
    }
590 1
    foreach($stats as $stat) {
591 1
      $this->$stat = (int) round($$stat);
592
    }
593 1
    $this->recalculateSecondaryStats();
594 1
    $this->stunned = $stunned;
595 1
  }
596
  
597
  /**
598
   * Calculate character's initiative
599
   */
600
  public function calculateInitiative(): void {
601 1
    $result = 0;
602
    $stats = [
603 1
      "INT" => $this->intelligence, "DEX" => $this->dexterity, "STR" => $this->strength, "CON" => $this->constitution,
604 1
      "CHAR" => $this->charisma,
605
    ];
606 1
    $formula = str_replace(array_keys($stats), array_values($stats), $this->initiativeFormula);
607 1
    preg_match_all("/^([1-9]+)d([1-9]+)/", $formula, $dices);
608 1
    for($i = 1; $i <= (int) $dices[1][0]; $i++) {
609 1
      $result += rand(1, (int) $dices[2][0]);
610
    }
611 1
    preg_match_all("/\+([0-9]+)\/([0-9]+)/", $formula, $ammendum);
612 1
    $result += (int) $ammendum[1][0] / (int) $ammendum[2][0];
613 1
    $this->initiative = (int) $result;
614 1
  }
615
  
616
  /**
617
   * Reset character's initiative
618
   */
619
  public function resetInitiative(): void {
620 1
    $this->initiative = $this->initiativeBase;
621 1
  }
622
}
623
?>