Lines 91.11% 41 / 45
Functions and Methods 60.00% 3 / 5
Classes and Traits 0.00% 0 / 1
Name Lines Functions and Methods CRAP Classes and Traits
SkillsLoadedEvent 91.11% 41 / 45 60.00% 3 / 5 22.34 0.00% 0 / 1
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 from 90.00% 27 / 30 0.00% 0 / 1 7.05
 slot 87.50% 7 / 8 0.00% 0 / 1 8.12
 target 100.00% 5 / 5 100.00% 1 / 1 5
 summary 100.00% 1 / 1 100.00% 1 / 1 1
1<?php
2
3declare(strict_types=1);
4
5namespace Lorekeeper\Server\Event\Combat;
6
7use Lorekeeper\Core\Attribute\Mana;
8use Lorekeeper\Core\CharacterClass\Skill\Skill;
9use Lorekeeper\Core\CharacterClass\Skill\SkillSlot;
10use Lorekeeper\Core\CharacterClass\Skill\SkillTarget;
11use Lorekeeper\Server\Event\Event;
12use Lorekeeper\Server\Event\NamedAfterClass;
13use Lorekeeper\Server\Exception\MalformedEventException;
14use Lorekeeper\Server\Message\JsonMessage;
15use Lorekeeper\Server\Message\Message;
16use Override;
17use Psl\DateTime\Duration;
18use Psl\Option;
19use Psl\Type;
20use Psl\Type\Exception\CoercionException;
21
22final readonly class SkillsLoadedEvent implements Event
23{
24    use NamedAfterClass;
25
26    /**
27     * @param list<Skill> $skills
28     * @param list<SkillSlot> $usable
29     */
30    public function __construct(
31        public array $skills,
32        public array $usable,
33    ) {}
34
35    /** @return Option\Option<static> */
36    #[Override]
37    public static function from(Message $message): Option\Option
38    {
39        if (!$message instanceof JsonMessage || ($message->data['cmd'] ?? null) !== 'sAct') {
40            return Option\none();
41        }
42
43        try {
44            $active = Type\shape([
45                'actions' => Type\shape([
46                    'active' => Type\vec(Type\shape([
47                        'ref' => Type\non_empty_string(),
48                        'tgt' => Type\string(),
49                        'mp' => Type\uint(),
50                        'cd' => Type\uint(),
51                        'isOK' => Type\optional(Type\bool()),
52                    ], allowUnknownFields: true)),
53                ], allowUnknownFields: true),
54            ], allowUnknownFields: true)->coerce($message->data)['actions']['active'];
55        } catch (CoercionException $exception) {
56            throw new MalformedEventException(static::class, 'skills are missing or invalid', $exception);
57        }
58
59        $skills = [];
60        $usable = [];
61        foreach ($active as $skill) {
62            $slot = self::slot($skill['ref']);
63            if ($slot === null) {
64                continue;
65            }
66
67            $skills[] = new Skill(
68                $slot,
69                self::target($skill['tgt']),
70                new Mana($skill['mp']),
71                Duration::milliseconds($skill['cd']),
72            );
73            if ($skill['isOK'] ?? false) {
74                $usable[] = $slot;
75            }
76        }
77
78        return Option\some(new static($skills, $usable));
79    }
80
81    private static function slot(string $ref): ?SkillSlot
82    {
83        return match ($ref) {
84            'aa' => SkillSlot::AutoAttack,
85            'a1' => SkillSlot::First,
86            'a2' => SkillSlot::Second,
87            'a3' => SkillSlot::Third,
88            'a4' => SkillSlot::Fourth,
89            'i1' => SkillSlot::Potion,
90            default => null,
91        };
92    }
93
94    /** @throws MalformedEventException */
95    private static function target(string $target): SkillTarget
96    {
97        return match ($target) {
98            'h' => SkillTarget::Enemy,
99            's' => SkillTarget::Self,
100            'f' => SkillTarget::Ally,
101            default => throw new MalformedEventException(static::class, "unknown skill target '{$target}'"),
102        };
103    }
104
105    #[Override]
106    public function summary(): string
107    {
108        return count($this->skills) . ' skills, ' . count($this->usable) . ' unlocked';
109    }
110}