Lines
97.57%
201 / 206
Functions and Methods
92.30%
12 / 13
Classes and Traits
0.00%
0 / 1
| Name | Lines | Functions and Methods | CRAP | Classes and Traits | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| QuestLoadedEvent | 97.57% | 201 / 206 | 92.30% | 12 / 13 | 83 | 0.00% | 0 / 1 | |||
| __construct | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| mentions | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| from | 100.00% | 8 / 8 | 100.00% | 1 / 1 | 4 | |||||
| questType | 100.00% | 49 / 49 | 100.00% | 1 / 1 | 1 | |||||
| quest | 100.00% | 17 / 17 | 100.00% | 1 / 1 | 2 | |||||
| requirements | 100.00% | 21 / 21 | 100.00% | 1 / 1 | 10 | |||||
| rewards | 100.00% | 15 / 15 | 100.00% | 1 / 1 | 6 | |||||
| rewardType | 100.00% | 4 / 4 | 100.00% | 1 / 1 | 4 | |||||
| mentionsOf | 100.00% | 27 / 27 | 100.00% | 1 / 1 | 8 | |||||
| itemValues | 100.00% | 20 / 20 | 100.00% | 1 / 1 | 7 | |||||
| detail | 100.00% | 28 / 28 | 100.00% | 1 / 1 | 28 | |||||
| slot | 100.00% | 10 / 10 | 100.00% | 1 / 1 | 10 | |||||
| summary | 0.00% | 0 / 5 | 0.00% | 0 / 1 | 2 | |||||
| 1 | <?php | |
| 2 | ||
| 3 | declare(strict_types=1); | |
| 4 | ||
| 5 | namespace Lorekeeper\Server\Event\Quest; | |
| 6 | ||
| 7 | use Lorekeeper\Core\Asset\Gamefile; | |
| 8 | use Lorekeeper\Core\Asset\Linkage; | |
| 9 | use Lorekeeper\Core\CharacterClass\ClassId; | |
| 10 | use Lorekeeper\Core\CharacterClass\ClassName; | |
| 11 | use Lorekeeper\Core\Currency\AdventureCoins; | |
| 12 | use Lorekeeper\Core\Currency\Gold; | |
| 13 | use Lorekeeper\Core\Item\EquipmentSlot; | |
| 14 | use Lorekeeper\Core\Item\ItemDescription; | |
| 15 | use Lorekeeper\Core\Item\ItemId; | |
| 16 | use Lorekeeper\Core\Item\ItemName; | |
| 17 | use Lorekeeper\Core\Item\ItemPrice; | |
| 18 | use Lorekeeper\Core\Item\Type; | |
| 19 | use Lorekeeper\Core\Player\PlayerLevel; | |
| 20 | use Lorekeeper\Core\Progression\ClassPoints; | |
| 21 | use Lorekeeper\Core\Progression\Experience; | |
| 22 | use Lorekeeper\Core\Progression\Rank; | |
| 23 | use Lorekeeper\Core\Progression\Reputation; | |
| 24 | use Lorekeeper\Core\World\Faction\FactionId; | |
| 25 | use Lorekeeper\Core\World\Faction\FactionName; | |
| 26 | use Lorekeeper\Core\World\Quest\Objective\ItemObjective; | |
| 27 | use Lorekeeper\Core\World\Quest\Objective\QuestObjective; | |
| 28 | use Lorekeeper\Core\World\Quest\QuestDescription; | |
| 29 | use Lorekeeper\Core\World\Quest\QuestId; | |
| 30 | use Lorekeeper\Core\World\Quest\QuestName; | |
| 31 | use Lorekeeper\Core\World\Quest\Requirement\ClassRankRequirement; | |
| 32 | use Lorekeeper\Core\World\Quest\Requirement\FactionRankRequirement; | |
| 33 | use Lorekeeper\Core\World\Quest\Requirement\ItemRequirement; | |
| 34 | use Lorekeeper\Core\World\Quest\Requirement\MemberRequirement; | |
| 35 | use Lorekeeper\Core\World\Quest\Requirement\PlayerLevelRequirement; | |
| 36 | use Lorekeeper\Core\World\Quest\Requirement\PreviousQuestRequirement; | |
| 37 | use Lorekeeper\Core\World\Quest\Requirement\QuestRequirement; | |
| 38 | use Lorekeeper\Core\World\Quest\Reward\DropRate; | |
| 39 | use Lorekeeper\Core\World\Quest\Reward\ExperienceReward; | |
| 40 | use Lorekeeper\Core\World\Quest\Reward\GoldReward; | |
| 41 | use Lorekeeper\Core\World\Quest\Reward\ItemReward; | |
| 42 | use Lorekeeper\Core\World\Quest\Reward\ItemRewardType; | |
| 43 | use Lorekeeper\Core\World\Quest\Reward\QuestReward; | |
| 44 | use Lorekeeper\Core\World\Quest\Reward\ReputationReward; | |
| 45 | use Lorekeeper\Server\Event\Event; | |
| 46 | use Lorekeeper\Server\Event\Mention\Mention; | |
| 47 | use Lorekeeper\Server\Event\Mention\Mentions; | |
| 48 | use Lorekeeper\Server\Event\NamedAfterClass; | |
| 49 | use Lorekeeper\Server\Exception\MalformedEventException; | |
| 50 | use Lorekeeper\Server\Message\JsonMessage; | |
| 51 | use Lorekeeper\Server\Message\Message; | |
| 52 | use Override; | |
| 53 | use Psl\Option; | |
| 54 | use Psl\Type as Coerce; | |
| 55 | use Psl\Type\Exception\CoercionException; | |
| 56 | ||
| 57 | /** | |
| 58 | * @psalm-type Item = array{ItemID: positive-int, sName: non-empty-string, sType: string, sES: string, sDesc?: string, iCost?: non-negative-int, bCoins?: bool, sFile?: null|string, sLink?: string, bUpg?: bool, ...} | |
| 59 | * @psalm-type Quest = array{QuestID: positive-int, sName: non-empty-string, sDesc: string, sEndText?: string, iLvl: non-negative-int, iExp: non-negative-int, iGold: non-negative-int, iClass: non-negative-int, sClass?: string, iReqCP: non-negative-int, FactionID: positive-int, sFaction: string, iReqRep: non-negative-int, iRep: non-negative-int, bUpg: bool, bQuest?: bool|int, strPrevious?: string, turnin?: list<array{ItemID: positive-int, iQty: positive-int, ...}>, reward?: list<array{ItemID: positive-int, iQty: positive-int, iRate: float|int, iType: int, ...}>, reqd?: list<array{ItemID: positive-int, iQty: positive-int, ...}>, oItems?: array<array-key, Item>, oReqd?: array<array-key, Item>, oRewards?: array{itemsS?: array<array-key, Item>, itemsR?: array<array-key, Item>, ...}, ...} | |
| 60 | * | |
| 61 | * @mago-expect lint:cyclomatic-complexity | |
| 62 | * @mago-expect lint:kan-defect | |
| 63 | * @mago-expect lint:too-many-methods | |
| 64 | */ | |
| 65 | final readonly class QuestLoadedEvent implements Event, Mentions | |
| 66 | { | |
| 67 | use NamedAfterClass; | |
| 68 | ||
| 69 | private const int NO_FACTION = 1; | |
| 70 | ||
| 71 | /** | |
| 72 | * @param list<QuestLoadedQuest> $quests | |
| 73 | * @param list<Mention> $mentions | |
| 74 | */ | |
| 75 | public function __construct( | |
| 76 | public array $quests, | |
| 77 | private array $mentions, | |
| 78 | ) {} | |
| 79 | ||
| 80 | /** @return list<Mention> */ | |
| 81 | #[Override] | |
| 82 | public function mentions(): array | |
| 83 | { | |
| 84 | return $this->mentions; | |
| 85 | } | |
| 86 | ||
| 87 | /** @return Option\Option<static> */ | |
| 88 | #[Override] | |
| 89 | public static function from(Message $message): Option\Option | |
| 90 | { | |
| 91 | if (!$message instanceof JsonMessage || ($message->data['cmd'] ?? null) !== 'getQuests') { | |
| 92 | return Option\none(); | |
| 93 | } | |
| 94 | ||
| 95 | try { | |
| 96 | $quests = array_values(Coerce\dict(Coerce\array_key(), self::questType())->coerce( | |
| 97 | $message->data['quests'] ?? null, | |
| 98 | )); | |
| 99 | } catch (CoercionException $exception) { | |
| 100 | throw new MalformedEventException(static::class, 'quests are missing or invalid', $exception); | |
| 101 | } | |
| 102 | ||
| 103 | return Option\some(new static(array_map(self::quest(...), $quests), self::mentionsOf($quests))); | |
| 104 | } | |
| 105 | ||
| 106 | /** @return Coerce\TypeInterface<Quest> */ | |
| 107 | private static function questType(): Coerce\TypeInterface | |
| 108 | { | |
| 109 | $items = Coerce\optional(Coerce\dict(Coerce\array_key(), Coerce\shape([ | |
| 110 | 'ItemID' => Coerce\positive_int(), | |
| 111 | 'sName' => Coerce\non_empty_string(), | |
| 112 | 'sType' => Coerce\string(), | |
| 113 | 'sES' => Coerce\string(), | |
| 114 | 'sDesc' => Coerce\optional(Coerce\string()), | |
| 115 | 'iCost' => Coerce\optional(Coerce\uint()), | |
| 116 | 'bCoins' => Coerce\optional(Coerce\bool()), | |
| 117 | 'sFile' => Coerce\optional(Coerce\nullable(Coerce\string())), | |
| 118 | 'sLink' => Coerce\optional(Coerce\string()), | |
| 119 | 'bUpg' => Coerce\optional(Coerce\bool()), | |
| 120 | ], allowUnknownFields: true))); | |
| 121 | $entry = Coerce\shape([ | |
| 122 | 'ItemID' => Coerce\positive_int(), | |
| 123 | 'iQty' => Coerce\positive_int(), | |
| 124 | ], allowUnknownFields: true); | |
| 125 | ||
| 126 | return Coerce\shape([ | |
| 127 | 'QuestID' => Coerce\positive_int(), | |
| 128 | 'sName' => Coerce\non_empty_string(), | |
| 129 | 'sDesc' => Coerce\string(), | |
| 130 | 'sEndText' => Coerce\optional(Coerce\string()), | |
| 131 | 'iLvl' => Coerce\uint(), | |
| 132 | 'iExp' => Coerce\uint(), | |
| 133 | 'iGold' => Coerce\uint(), | |
| 134 | 'iClass' => Coerce\uint(), | |
| 135 | 'sClass' => Coerce\optional(Coerce\string()), | |
| 136 | 'iReqCP' => Coerce\uint(), | |
| 137 | 'FactionID' => Coerce\positive_int(), | |
| 138 | 'sFaction' => Coerce\string(), | |
| 139 | 'iReqRep' => Coerce\uint(), | |
| 140 | 'iRep' => Coerce\uint(), | |
| 141 | 'bUpg' => Coerce\bool(), | |
| 142 | 'bQuest' => Coerce\optional(Coerce\union(Coerce\bool(), Coerce\int())), | |
| 143 | 'strPrevious' => Coerce\optional(Coerce\string()), | |
| 144 | 'turnin' => Coerce\optional(Coerce\vec($entry)), | |
| 145 | 'reward' => Coerce\optional(Coerce\vec(Coerce\shape([ | |
| 146 | 'ItemID' => Coerce\positive_int(), | |
| 147 | 'iQty' => Coerce\positive_int(), | |
| 148 | 'iRate' => Coerce\num(), | |
| 149 | 'iType' => Coerce\int(), | |
| 150 | ], allowUnknownFields: true))), | |
| 151 | 'reqd' => Coerce\optional(Coerce\vec($entry)), | |
| 152 | 'oItems' => $items, | |
| 153 | 'oReqd' => $items, | |
| 154 | 'oRewards' => Coerce\optional(Coerce\shape([ | |
| 155 | 'itemsS' => $items, | |
| 156 | 'itemsR' => $items, | |
| 157 | ], allowUnknownFields: true)), | |
| 158 | ], allowUnknownFields: true); | |
| 159 | } | |
| 160 | ||
| 161 | /** @param Quest $quest */ | |
| 162 | private static function quest(array $quest): QuestLoadedQuest | |
| 163 | { | |
| 164 | $description = trim($quest['sDesc']); | |
| 165 | ||
| 166 | return new QuestLoadedQuest( | |
| 167 | new QuestId($quest['QuestID']), | |
| 168 | new QuestName($quest['sName']), | |
| 169 | $description === '' | |
| 170 | ? Option\none() | |
| 171 | : Option\some(new QuestDescription($quest['sDesc'], trim($quest['sEndText'] ?? ''))), | |
| 172 | self::requirements($quest), | |
| 173 | array_map( | |
| 174 | static fn(array $turnIn): QuestObjective => new ItemObjective( | |
| 175 | new ItemId($turnIn['ItemID']), | |
| 176 | $turnIn['iQty'], | |
| 177 | ), | |
| 178 | $quest['turnin'] ?? [], | |
| 179 | ), | |
| 180 | self::rewards($quest), | |
| 181 | ); | |
| 182 | } | |
| 183 | ||
| 184 | /** | |
| 185 | * @param Quest $quest | |
| 186 | * | |
| 187 | * @return list<QuestRequirement> | |
| 188 | */ | |
| 189 | private static function requirements(array $quest): array | |
| 190 | { | |
| 191 | $requirements = []; | |
| 192 | ||
| 193 | if ($quest['iLvl'] > 1) { | |
| 194 | $requirements[] = new PlayerLevelRequirement(new PlayerLevel($quest['iLvl'])); | |
| 195 | } | |
| 196 | ||
| 197 | if ($quest['iClass'] > 0) { | |
| 198 | $requirements[] = new ClassRankRequirement( | |
| 199 | new ClassId($quest['iClass']), | |
| 200 | Rank::fromPoints(new ClassPoints($quest['iReqCP'])), | |
| 201 | ); | |
| 202 | } | |
| 203 | ||
| 204 | if ($quest['iReqRep'] > 0 && $quest['FactionID'] !== self::NO_FACTION) { | |
| 205 | $requirements[] = new FactionRankRequirement( | |
| 206 | new FactionId($quest['FactionID']), | |
| 207 | Rank::fromPoints(new Reputation($quest['iReqRep'])), | |
| 208 | ); | |
| 209 | } | |
| 210 | ||
| 211 | if ($quest['bUpg']) { | |
| 212 | $requirements[] = new MemberRequirement(); | |
| 213 | } | |
| 214 | ||
| 215 | $previous = $quest['strPrevious'] ?? ''; | |
| 216 | if (($quest['bQuest'] ?? false) && ctype_digit($previous) && (int) $previous > 0) { | |
| 217 | $requirements[] = new PreviousQuestRequirement(new QuestId((int) $previous)); | |
| 218 | } | |
| 219 | ||
| 220 | foreach ($quest['reqd'] ?? [] as $item) { | |
| 221 | $requirements[] = new ItemRequirement(new ItemId($item['ItemID']), $item['iQty']); | |
| 222 | } | |
| 223 | ||
| 224 | return $requirements; | |
| 225 | } | |
| 226 | ||
| 227 | /** | |
| 228 | * @param Quest $quest | |
| 229 | * | |
| 230 | * @return list<QuestReward> | |
| 231 | */ | |
| 232 | private static function rewards(array $quest): array | |
| 233 | { | |
| 234 | $rewards = []; | |
| 235 | ||
| 236 | if ($quest['iGold'] > 0) { | |
| 237 | $rewards[] = new GoldReward(new Gold($quest['iGold'])); | |
| 238 | } | |
| 239 | ||
| 240 | if ($quest['iExp'] > 0) { | |
| 241 | $rewards[] = new ExperienceReward(new Experience($quest['iExp'])); | |
| 242 | } | |
| 243 | ||
| 244 | if ($quest['iRep'] > 0 && $quest['FactionID'] !== self::NO_FACTION) { | |
| 245 | $rewards[] = new ReputationReward(new FactionId($quest['FactionID']), new Reputation($quest['iRep'])); | |
| 246 | } | |
| 247 | ||
| 248 | foreach ($quest['reward'] ?? [] as $item) { | |
| 249 | $rewards[] = new ItemReward( | |
| 250 | new ItemId($item['ItemID']), | |
| 251 | new DropRate((float) $item['iRate']), | |
| 252 | $item['iQty'], | |
| 253 | self::rewardType($item['iType']), | |
| 254 | ); | |
| 255 | } | |
| 256 | ||
| 257 | return $rewards; | |
| 258 | } | |
| 259 | ||
| 260 | /** @throws MalformedEventException */ | |
| 261 | private static function rewardType(int $type): ItemRewardType | |
| 262 | { | |
| 263 | return match ($type) { | |
| 264 | 0 => ItemRewardType::Guaranteed, | |
| 265 | 1 => ItemRewardType::Random, | |
| 266 | default => throw new MalformedEventException(static::class, "unknown reward type '{$type}'"), | |
| 267 | }; | |
| 268 | } | |
| 269 | ||
| 270 | /** | |
| 271 | * @param list<Quest> $quests | |
| 272 | * | |
| 273 | * @return list<Mention<ItemId|FactionId, object>> | |
| 274 | */ | |
| 275 | private static function mentionsOf(array $quests): array | |
| 276 | { | |
| 277 | $mentions = []; | |
| 278 | foreach ($quests as $quest) { | |
| 279 | $rewards = $quest['oRewards'] ?? []; | |
| 280 | $items = [ | |
| 281 | ...($quest['oItems'] ?? []), | |
| 282 | ...($quest['oReqd'] ?? []), | |
| 283 | ...($rewards['itemsS'] ?? []), | |
| 284 | ...($rewards['itemsR'] ?? []), | |
| 285 | ]; | |
| 286 | foreach ($items as $item) { | |
| 287 | foreach (self::itemValues($item) as $value) { | |
| 288 | $mentions['item:' . $item['ItemID'] . ':' . $value::class] ??= new Mention( | |
| 289 | new ItemId($item['ItemID']), | |
| 290 | $value, | |
| 291 | ); | |
| 292 | } | |
| 293 | } | |
| 294 | ||
| 295 | if ($quest['FactionID'] !== self::NO_FACTION && $quest['sFaction'] !== '') { | |
| 296 | $mentions["faction:{$quest['FactionID']}"] ??= new Mention( | |
| 297 | new FactionId($quest['FactionID']), | |
| 298 | new FactionName($quest['sFaction']), | |
| 299 | ); | |
| 300 | } | |
| 301 | ||
| 302 | $class = trim($quest['sClass'] ?? ''); | |
| 303 | if ($quest['iClass'] > 0 && $class !== '') { | |
| 304 | $mentions["class:{$quest['iClass']}"] ??= new Mention( | |
| 305 | new ClassId($quest['iClass']), | |
| 306 | new ClassName($class), | |
| 307 | ); | |
| 308 | } | |
| 309 | } | |
| 310 | ||
| 311 | return array_values($mentions); | |
| 312 | } | |
| 313 | ||
| 314 | /** | |
| 315 | * @param Item $item | |
| 316 | * | |
| 317 | * @return list<object> | |
| 318 | */ | |
| 319 | private static function itemValues(array $item): array | |
| 320 | { | |
| 321 | $description = trim($item['sDesc'] ?? ''); | |
| 322 | $cost = $item['iCost'] ?? 0; | |
| 323 | $file = $item['sFile'] ?? ''; | |
| 324 | $link = $item['sLink'] ?? ''; | |
| 325 | $hasFile = str_ends_with($file, '.swf'); | |
| 326 | ||
| 327 | return [ | |
| 328 | new ItemName($item['sName']), | |
| 329 | self::detail($item['sType']), | |
| 330 | ...($description === '' ? [] : [new ItemDescription($description)]), | |
| 331 | ...( | |
| 332 | $cost === 0 | |
| 333 | ? [] | |
| 334 | : [new ItemPrice($item['bCoins'] ?? false ? new AdventureCoins($cost) : new Gold($cost))] | |
| 335 | ), | |
| 336 | ...($hasFile ? [new Gamefile($file)] : []), | |
| 337 | ...($hasFile && $link !== '' ? [new Linkage($link)] : []), | |
| 338 | ...self::slot($item['sES']) | |
| 339 | ->map(static fn(EquipmentSlot $slot): array => [$slot]) | |
| 340 | ->unwrapOr([]), | |
| 341 | ]; | |
| 342 | } | |
| 343 | ||
| 344 | /** @throws MalformedEventException */ | |
| 345 | private static function detail(string $type): Type\Equipment|Type\Weapon|Type\House|Type\Misc | |
| 346 | { | |
| 347 | return match ($type) { | |
| 348 | 'Armor' => Type\Equipment::Armor, | |
| 349 | 'Cape' => Type\Equipment::Cape, | |
| 350 | 'Class' => Type\Equipment::CharacterClass, | |
| 351 | 'Misc' => Type\Equipment::Ground, | |
| 352 | 'Helm' => Type\Equipment::Helm, | |
| 353 | 'Necklace' => Type\Equipment::Necklace, | |
| 354 | 'Pet' => Type\Equipment::Pet, | |
| 355 | 'Axe' => Type\Weapon::Axe, | |
| 356 | 'Bow' => Type\Weapon::Bow, | |
| 357 | 'Dagger' => Type\Weapon::Dagger, | |
| 358 | 'Gauntlet' => Type\Weapon::Gauntlet, | |
| 359 | 'Gun' => Type\Weapon::Gun, | |
| 360 | 'HandGun' => Type\Weapon::HandGun, | |
| 361 | 'Mace' => Type\Weapon::Mace, | |
| 362 | 'Polearm' => Type\Weapon::Polearm, | |
| 363 | 'Rifle' => Type\Weapon::Rifle, | |
| 364 | 'Staff' => Type\Weapon::Staff, | |
| 365 | 'Sword' => Type\Weapon::Sword, | |
| 366 | 'Wand' => Type\Weapon::Wand, | |
| 367 | 'Whip' => Type\Weapon::Whip, | |
| 368 | 'Floor Item' => Type\House::Floor, | |
| 369 | 'House' => Type\House::House, | |
| 370 | 'Wall Item' => Type\House::Wall, | |
| 371 | 'ServerUse' => Type\Misc::Boost, | |
| 372 | 'Enhancement' => Type\Misc::Enhancement, | |
| 373 | 'Item', 'Note', 'Quest Item', 'Resource' => Type\Misc::Generic, | |
| 374 | default => throw new MalformedEventException(static::class, "unknown item type '{$type}'"), | |
| 375 | }; | |
| 376 | } | |
| 377 | ||
| 378 | /** | |
| 379 | * @throws MalformedEventException | |
| 380 | * | |
| 381 | * @return Option\Option<EquipmentSlot> | |
| 382 | */ | |
| 383 | private static function slot(string $code): Option\Option | |
| 384 | { | |
| 385 | return match ($code) { | |
| 386 | 'None' => Option\none(), | |
| 387 | 'Weapon' => Option\some(EquipmentSlot::Weapon), | |
| 388 | 'ar', 'co' => Option\some(EquipmentSlot::Armor), | |
| 389 | 'he' => Option\some(EquipmentSlot::Helm), | |
| 390 | 'ba' => Option\some(EquipmentSlot::Cape), | |
| 391 | 'pe' => Option\some(EquipmentSlot::Pet), | |
| 392 | 'mi' => Option\some(EquipmentSlot::Ground), | |
| 393 | 'am' => Option\some(EquipmentSlot::Necklace), | |
| 394 | default => throw new MalformedEventException(static::class, "unknown equipment slot '{$code}'"), | |
| 395 | }; | |
| 396 | } | |
| 397 | ||
| 398 | #[Override] | |
| 399 | public function summary(): string | |
| 400 | { | |
| 401 | return ( | |
| 402 | count($this->quests) | |
| 403 | . ' quests: ' | |
| 404 | . implode(', ', array_map(static fn(QuestLoadedQuest $quest): string => $quest->name->value, $this->quests)) | |
| 405 | ); | |
| 406 | } | |
| 407 | } |