Lines 99.13% 114 / 115
Methods 88.88% 8 / 9
Classes 0.00% 0 / 1
Name Lines Methods CRAP
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 mentions 100.00% 1 / 1 100.00% 1 / 1 1
 from 100.00% 35 / 35 100.00% 1 / 1 4
 item 100.00% 25 / 25 100.00% 1 / 1 7
 materials 100.00% 8 / 8 100.00% 1 / 1 3
 detail 100.00% 28 / 28 100.00% 1 / 1 28
 slot 100.00% 10 / 10 100.00% 1 / 1 10
 summary 0.00% 0 / 1 0.00% 0 / 1 2
 [Lorekeeper\Server\Event\NamedAfterClass] name 100.00% 6 / 6 100.00% 1 / 1 1
37final readonly class ShopLoadedEvent implements Event, Mentions
38{
39    use NamedAfterClass;
40
41    /**
42     * @param list<ShopLoadedItem> $items
43     * @param list<Mention> $mentions
44     */
45    public function __construct(
46        public ShopId $id,
47        public ShopName $name,
48        public bool $memberOnly,
49        public array $items,
50        private array $mentions,
51    ) {}
52
53    /** @return list<Mention> */
54    #[Override]
55    public function mentions(): array
56    {
57        return $this->mentions;
58    }
59
60    /** @return Option\Option<static> */
61    #[Override]
62    public static function from(Message $message): Option\Option
63    {
64        if (!$message instanceof JsonMessage || ($message->data['cmd'] ?? null) !== 'loadShop') {
65            return Option\none();
66        }
67
68        try {
69            $shop = Coerce\shape([
70                'ShopID' => Coerce\positive_int(),
71                'sName' => Coerce\non_empty_string(),
72                'bUpgrd' => Coerce\optional(Coerce\bool()),
73                'items' => Coerce\vec(Coerce\shape([
74                    'ItemID' => Coerce\positive_int(),
75                    'sName' => Coerce\non_empty_string(),
76                    'sType' => Coerce\string(),
77                    'sES' => Coerce\string(),
78                    'sDesc' => Coerce\optional(Coerce\string()),
79                    'iCost' => Coerce\optional(Coerce\uint()),
80                    'bCoins' => Coerce\optional(Coerce\bool()),
81                    'sFile' => Coerce\optional(Coerce\nullable(Coerce\string())),
82                    'sLink' => Coerce\optional(Coerce\string()),
83                    'bUpg' => Coerce\optional(Coerce\bool()),
84                    'turnin' => Coerce\optional(Coerce\vec(Coerce\shape([
85                        'ItemID' => Coerce\positive_int(),
86                        'sName' => Coerce\non_empty_string(),
87                        'iQty' => Coerce\positive_int(),
88                    ], allowUnknownFields: true))),
89                ], allowUnknownFields: true)),
90            ], allowUnknownFields: true)->coerce($message->data['shopinfo'] ?? null);
91        } catch (CoercionException $exception) {
92            throw new MalformedEventException(static::class, 'shopinfo is missing or invalid', $exception);
93        }
94
95        return Option\some(
96            new static(
97                new ShopId($shop['ShopID']),
98                new ShopName($shop['sName']),
99                $shop['bUpgrd'] ?? false,
100                array_map(self::item(...), $shop['items']),
101                self::materials($shop['items']),
102            ),
103        );
104    }
105
106    /** @param Item $item */
107    private static function item(array $item): ShopLoadedItem
108    {
109        $description = trim($item['sDesc'] ?? '');
110        $cost = $item['iCost'] ?? 0;
111        $file = $item['sFile'] ?? '';
112        $link = $item['sLink'] ?? '';
113        $hasFile = str_ends_with($file, '.swf');
114
115        return new ShopLoadedItem(
116            new ItemId($item['ItemID']),
117            new ItemName($item['sName']),
118            self::detail($item['sType']),
119            $description === '' ? Option\none() : Option\some(new ItemDescription($description)),
120            $cost === 0
121                ? Option\none()
122                : Option\some(new ItemPrice($item['bCoins'] ?? false ? new AdventureCoins($cost) : new Gold($cost))),
123            $hasFile ? Option\some(new Gamefile($file)) : Option\none(),
124            $hasFile && $link !== '' ? Option\some(new Linkage($link)) : Option\none(),
125            self::slot($item['sES']),
126            $item['bUpg'] ?? false,
127            array_map(
128                static fn(array $material): MergeCost => new MergeCost(
129                    new ItemId($material['ItemID']),
130                    $material['iQty'],
131                ),
132                $item['turnin'] ?? [],
133            ),
134        );
135    }
136
137    /**
138     * @param list<Item> $items
139     *
140     * @return list<Mention<ItemId, ItemName>>
141     */
142    private static function materials(array $items): array
143    {
144        $mentions = [];
145        foreach ($items as $item) {
146            foreach ($item['turnin'] ?? [] as $material) {
147                $mentions[$material['ItemID']] ??= new Mention(
148                    new ItemId($material['ItemID']),
149                    new ItemName($material['sName']),
150                );
151            }
152        }
153
154        return array_values($mentions);
155    }
156
157    /** @throws MalformedEventException */
158    private static function detail(string $type): Type\Equipment|Type\Weapon|Type\House|Type\Misc
159    {
160        return match ($type) {
161            'Armor' => Type\Equipment::Armor,
162            'Cape' => Type\Equipment::Cape,
163            'Class' => Type\Equipment::CharacterClass,
164            'Misc' => Type\Equipment::Ground,
165            'Helm' => Type\Equipment::Helm,
166            'Necklace' => Type\Equipment::Necklace,
167            'Pet' => Type\Equipment::Pet,
168            'Axe' => Type\Weapon::Axe,
169            'Bow' => Type\Weapon::Bow,
170            'Dagger' => Type\Weapon::Dagger,
171            'Gauntlet' => Type\Weapon::Gauntlet,
172            'Gun' => Type\Weapon::Gun,
173            'HandGun' => Type\Weapon::HandGun,
174            'Mace' => Type\Weapon::Mace,
175            'Polearm' => Type\Weapon::Polearm,
176            'Rifle' => Type\Weapon::Rifle,
177            'Staff' => Type\Weapon::Staff,
178            'Sword' => Type\Weapon::Sword,
179            'Wand' => Type\Weapon::Wand,
180            'Whip' => Type\Weapon::Whip,
181            'Floor Item' => Type\House::Floor,
182            'House' => Type\House::House,
183            'Wall Item' => Type\House::Wall,
184            'ServerUse' => Type\Misc::Boost,
185            'Enhancement' => Type\Misc::Enhancement,
186            'Item', 'Note', 'Quest Item', 'Resource' => Type\Misc::Generic,
187            default => throw new MalformedEventException(static::class, "unknown item type '{$type}'"),
188        };
189    }
190
191    /**
192     * @throws MalformedEventException
193     *
194     * @return Option\Option<EquipmentSlot>
195     */
196    private static function slot(string $code): Option\Option
197    {
198        return match ($code) {
199            'None' => Option\none(),
200            'Weapon' => Option\some(EquipmentSlot::Weapon),
201            'ar', 'co' => Option\some(EquipmentSlot::Armor),
202            'he' => Option\some(EquipmentSlot::Helm),
203            'ba' => Option\some(EquipmentSlot::Cape),
204            'pe' => Option\some(EquipmentSlot::Pet),
205            'mi' => Option\some(EquipmentSlot::Ground),
206            'am' => Option\some(EquipmentSlot::Necklace),
207            default => throw new MalformedEventException(static::class, "unknown equipment slot '{$code}'"),
208        };
209    }
210
211    #[Override]
212    public function summary(): string
213    {
214        return "{$this->name->value}: " . count($this->items) . ' items';
215    }
216}

From Lorekeeper\Server\Event\NamedAfterClass

7trait NamedAfterClass
8{
9    public function name(): string
10    {
11        $class = static::class;
12
13        return (string) preg_replace(
14            '/Event$/',
15            replacement: '',
16            subject: substr($class, (int) strrpos($class, needle: '\\') + 1),
17        );
18    }
19}