overlord_event_system/
game_config_helpers.rs

1use anyhow::Context;
2
3use configs::{
4    abilities::{AbilityCasesSettingsByLevel, AbilityLevel, Projectile, ProjectileId},
5    cheats::{CheatScript, CheatScriptId},
6    fighting::ArenaLeague,
7    game_config::GameConfig,
8    pets::{PetCasesSettingsByLevel, PetLevel},
9    stones::{EffectStoneTemplate, TriggerStoneTemplate},
10};
11
12use essences::{
13    abilities::{AbilityId, AbilityRarity, AbilityRarityId, AbilityTemplate},
14    ability_stones::AbilityStoneId,
15    bundles::{BundleId, BundleRaw},
16    class::{Class, ClassId},
17    dungeons::{DungeonTemplate, DungeonTemplateId},
18    effect::{Effect, EffectId},
19    fighting::{FightTemplate, FightTemplateId},
20    game::{Chapter, CharacterLevel, EntityTemplate, EntityTemplateId},
21    gift::GiftTemplate,
22    item_case::ItemCasesSettingsByLevel,
23    items::{Attribute, AttributeId, ItemRarity, ItemRarityId, ItemTemplate, ItemTemplateId},
24    mail::MailTemplate,
25    pets::{PetId, PetRarity, PetRarityId, PetTemplate},
26    quest::QuestTemplate,
27    referrals::ReferralLevelInfo,
28    skins::{ConfigSkin, SkinId},
29    stones::StoneTemplateId,
30    talent_tree::{TalentId, TalentTemplate},
31    vassals::VassalTaskTemplate,
32};
33
34pub trait GameConfigLookup {
35    fn attribute(&self, attribute_id: AttributeId) -> Option<&Attribute>;
36    fn require_attribute(&self, attribute_id: AttributeId) -> anyhow::Result<&Attribute>;
37
38    fn attribute_by_db_code(&self, db_code: u8) -> Option<&Attribute>;
39    fn require_attribute_by_db_code(&self, db_code: u8) -> anyhow::Result<&Attribute>;
40
41    fn ability_template(&self, ability_id: AbilityId) -> Option<&AbilityTemplate>;
42    fn require_ability_template(&self, ability_id: AbilityId) -> anyhow::Result<&AbilityTemplate>;
43
44    fn ability_rarity(&self, rarity_id: AbilityRarityId) -> Option<&AbilityRarity>;
45    fn require_ability_rarity(&self, rarity_id: AbilityRarityId) -> anyhow::Result<&AbilityRarity>;
46
47    fn ability_stone_template(
48        &self,
49        stone_id: AbilityStoneId,
50    ) -> Option<&essences::ability_stones::AbilityStoneTemplate>;
51
52    fn ability_level(&self, rarity_id: AbilityRarityId, level: i64) -> Option<&AbilityLevel>;
53    fn require_ability_level(
54        &self,
55        rarity_id: AbilityRarityId,
56        level: i64,
57    ) -> anyhow::Result<&AbilityLevel>;
58
59    fn ability_case_settings_by_level(&self, level: i64) -> Option<&AbilityCasesSettingsByLevel>;
60    fn require_ability_case_settings_by_level(
61        &self,
62        level: i64,
63    ) -> anyhow::Result<&AbilityCasesSettingsByLevel>;
64
65    fn bundle(&self, bundle_id: BundleId) -> Option<&BundleRaw>;
66    fn require_bundle(&self, bundle_id: BundleId) -> anyhow::Result<&BundleRaw>;
67
68    fn chapter_by_level(&self, chapter_level: i64) -> Option<&Chapter>;
69    fn require_chapter_by_level(&self, chapter_level: i64) -> anyhow::Result<&Chapter>;
70
71    fn ability_slots_for_chapter_level(&self, chapter_level: i64) -> Option<u64>;
72
73    fn character_level(&self, level: i64) -> Option<CharacterLevel>;
74    fn require_character_level(&self, level: i64) -> anyhow::Result<CharacterLevel>;
75    /// Highest level reachable with `experience`, unbounded via the level formula.
76    fn level_for_experience(&self, experience: i64) -> i64;
77
78    fn class(&self, class_id: ClassId) -> Option<&Class>;
79    fn require_class(&self, class_id: ClassId) -> anyhow::Result<&Class>;
80
81    fn dungeon_template(&self, dungeon_id: DungeonTemplateId) -> Option<&DungeonTemplate>;
82    fn require_dungeon_template(
83        &self,
84        dungeon_id: DungeonTemplateId,
85    ) -> anyhow::Result<&DungeonTemplate>;
86
87    fn effect(&self, effect_id: EffectId) -> Option<&Effect>;
88    fn require_effect(&self, effect_id: EffectId) -> anyhow::Result<&Effect>;
89
90    fn fight_template(&self, fight_id: FightTemplateId) -> Option<&FightTemplate>;
91    fn require_fight_template(&self, fight_id: FightTemplateId) -> anyhow::Result<&FightTemplate>;
92
93    fn arena_league_for_rating(&self, rating: i64) -> Option<&ArenaLeague>;
94    fn require_arena_league_for_rating(&self, rating: i64) -> anyhow::Result<&ArenaLeague>;
95
96    fn entity_template(&self, entity_template_id: EntityTemplateId) -> Option<&EntityTemplate>;
97    fn require_entity_template(
98        &self,
99        entity_template_id: EntityTemplateId,
100    ) -> anyhow::Result<&EntityTemplate>;
101
102    fn gift_template(&self, gift_id: uuid::Uuid) -> Option<&GiftTemplate>;
103    fn require_gift_template(&self, gift_id: uuid::Uuid) -> anyhow::Result<&GiftTemplate>;
104
105    fn item_case_settings_by_level(&self, level: i64) -> Option<&ItemCasesSettingsByLevel>;
106    fn require_item_case_settings_by_level(
107        &self,
108        level: i64,
109    ) -> anyhow::Result<&ItemCasesSettingsByLevel>;
110
111    fn item_rarity(&self, rarity_id: ItemRarityId) -> Option<&ItemRarity>;
112    fn require_item_rarity(&self, rarity_id: ItemRarityId) -> anyhow::Result<&ItemRarity>;
113
114    fn item_template(&self, item_id: ItemTemplateId) -> Option<&ItemTemplate>;
115    fn require_item_template(&self, item_id: ItemTemplateId) -> anyhow::Result<&ItemTemplate>;
116
117    fn mail_template(&self, template_id: uuid::Uuid) -> Option<&MailTemplate>;
118    fn require_mail_template(&self, template_id: uuid::Uuid) -> anyhow::Result<&MailTemplate>;
119
120    fn patron_level_info(&self, level: i64) -> Option<&ReferralLevelInfo>;
121    fn require_patron_level_info(&self, level: i64) -> anyhow::Result<&ReferralLevelInfo>;
122
123    fn projectile(&self, projectile_id: ProjectileId) -> Option<&Projectile>;
124    fn require_projectile(&self, projectile_id: ProjectileId) -> anyhow::Result<&Projectile>;
125
126    fn quest(&self, quest_id: uuid::Uuid) -> Option<&QuestTemplate>;
127    fn require_quest(&self, quest_id: uuid::Uuid) -> anyhow::Result<&QuestTemplate>;
128
129    fn quest_by_code(&self, code: &str) -> Option<&QuestTemplate>;
130
131    fn skin(&self, skin_id: SkinId) -> Option<&ConfigSkin>;
132    fn require_skin(&self, skin_id: SkinId) -> anyhow::Result<&ConfigSkin>;
133
134    fn vassal_task_template(&self, task_template_id: uuid::Uuid) -> Option<&VassalTaskTemplate>;
135    fn require_vassal_task_template(
136        &self,
137        task_template_id: uuid::Uuid,
138    ) -> anyhow::Result<&VassalTaskTemplate>;
139
140    fn cheat_script(&self, cheat_script_id: CheatScriptId) -> Option<&CheatScript>;
141
142    fn talent(&self, talent_id: TalentId) -> Option<&TalentTemplate>;
143    fn require_talent(&self, talent_id: TalentId) -> anyhow::Result<&TalentTemplate>;
144
145    fn allowed_wishlist_rarity_ids(
146        &self,
147        ability_case_level: i64,
148    ) -> Vec<essences::abilities::AbilityRarityId>;
149
150    fn pet_template(&self, pet_id: PetId) -> Option<&PetTemplate>;
151    fn require_pet_template(&self, pet_id: PetId) -> anyhow::Result<&PetTemplate>;
152
153    fn pet_rarity(&self, rarity_id: PetRarityId) -> Option<&PetRarity>;
154
155    fn pet_level(&self, rarity_id: PetRarityId, level: i64) -> Option<&PetLevel>;
156
157    fn pet_slots_for_chapter_level(&self, chapter_level: i64) -> Option<u64>;
158
159    fn pet_case_settings_by_level(&self, level: i64) -> Option<&PetCasesSettingsByLevel>;
160    fn require_pet_case_settings_by_level(
161        &self,
162        level: i64,
163    ) -> anyhow::Result<&PetCasesSettingsByLevel>;
164
165    fn allowed_pet_wishlist_rarity_ids(
166        &self,
167        pet_case_level: i64,
168    ) -> Vec<essences::pets::PetRarityId>;
169
170    /// Catalog entry an owned Trigger Stone is a copy of.
171    ///
172    /// Deliberately has no `require_*` twin. `character_stones.template_id` is
173    /// registered with `collection: none`, so the deploy-time orphan check does
174    /// not cover it and `row_to_stone` does not validate against config: a
175    /// template deleted while players own copies leaves live rows pointing
176    /// nowhere. Every caller must skip-and-log such a stone, never fail the
177    /// session.
178    fn trigger_stone_template(&self, template_id: StoneTemplateId)
179    -> Option<&TriggerStoneTemplate>;
180
181    /// Catalog entry an owned Effect Stone is a copy of. Same no-`require_*`
182    /// rule as [`GameConfigLookup::trigger_stone_template`].
183    fn effect_stone_template(&self, template_id: StoneTemplateId) -> Option<&EffectStoneTemplate>;
184}
185
186impl GameConfigLookup for GameConfig {
187    fn attribute(&self, attribute_id: AttributeId) -> Option<&Attribute> {
188        self.attributes.iter().find(|a| a.id == attribute_id)
189    }
190
191    fn require_attribute(&self, attribute_id: AttributeId) -> anyhow::Result<&Attribute> {
192        self.attribute(attribute_id)
193            .with_context(|| format!("No attribute in config with id={attribute_id}"))
194    }
195
196    fn attribute_by_db_code(&self, db_code: u8) -> Option<&Attribute> {
197        self.attributes.iter().find(|a| a.db_code == db_code)
198    }
199
200    fn require_attribute_by_db_code(&self, db_code: u8) -> anyhow::Result<&Attribute> {
201        self.attribute_by_db_code(db_code)
202            .with_context(|| format!("No attribute in config with db_code={db_code}"))
203    }
204
205    fn ability_template(&self, ability_id: AbilityId) -> Option<&AbilityTemplate> {
206        self.abilities.iter().find(|a| a.id == ability_id)
207    }
208
209    fn require_ability_template(&self, ability_id: AbilityId) -> anyhow::Result<&AbilityTemplate> {
210        self.ability_template(ability_id)
211            .with_context(|| format!("No ability_template in config with id={ability_id}"))
212    }
213
214    fn ability_rarity(&self, rarity_id: AbilityRarityId) -> Option<&AbilityRarity> {
215        self.ability_rarities.iter().find(|r| r.id == rarity_id)
216    }
217
218    fn require_ability_rarity(&self, rarity_id: AbilityRarityId) -> anyhow::Result<&AbilityRarity> {
219        self.ability_rarity(rarity_id)
220            .with_context(|| format!("No ability_rarity in config with id={rarity_id}"))
221    }
222
223    fn ability_stone_template(
224        &self,
225        stone_id: AbilityStoneId,
226    ) -> Option<&essences::ability_stones::AbilityStoneTemplate> {
227        self.ability_stones.iter().find(|s| s.id == stone_id)
228    }
229
230    fn ability_level(&self, rarity_id: AbilityRarityId, level: i64) -> Option<&AbilityLevel> {
231        self.ability_levels
232            .iter()
233            .find(|x| x.rarity_id == rarity_id && x.level == level)
234    }
235
236    fn require_ability_level(
237        &self,
238        rarity_id: AbilityRarityId,
239        level: i64,
240    ) -> anyhow::Result<&AbilityLevel> {
241        self.ability_level(rarity_id, level).with_context(|| {
242            format!("No ability_level in config with rarity_id={rarity_id} and level={level}")
243        })
244    }
245
246    fn ability_case_settings_by_level(&self, level: i64) -> Option<&AbilityCasesSettingsByLevel> {
247        self.ability_cases_settings
248            .iter()
249            .find(|x| x.level == level)
250    }
251
252    fn require_ability_case_settings_by_level(
253        &self,
254        level: i64,
255    ) -> anyhow::Result<&AbilityCasesSettingsByLevel> {
256        self.ability_case_settings_by_level(level)
257            .with_context(|| format!("No ability_case_settings with level={level}"))
258    }
259
260    fn bundle(&self, bundle_id: BundleId) -> Option<&BundleRaw> {
261        self.bundles.iter().find(|b| b.id == bundle_id)
262    }
263
264    fn require_bundle(&self, bundle_id: BundleId) -> anyhow::Result<&BundleRaw> {
265        self.bundle(bundle_id)
266            .with_context(|| format!("No bundle in config with id={bundle_id}"))
267    }
268
269    fn chapter_by_level(&self, chapter_level: i64) -> Option<&Chapter> {
270        self.chapters.iter().find(|c| c.level == chapter_level)
271    }
272
273    fn require_chapter_by_level(&self, chapter_level: i64) -> anyhow::Result<&Chapter> {
274        self.chapter_by_level(chapter_level)
275            .with_context(|| format!("No chapter with level={chapter_level}"))
276    }
277
278    fn ability_slots_for_chapter_level(&self, chapter_level: i64) -> Option<u64> {
279        self.ability_slots_levels
280            .iter()
281            .rev()
282            .find(|level| level.from_chapter_level <= chapter_level)
283            .map(|level| level.ability_slots)
284    }
285
286    fn character_level(&self, level: i64) -> Option<CharacterLevel> {
287        // Above the authored table the level is computed from the formula, so
288        // levels are unbounded. At or below the anchor the authored table wins.
289        if level > self.character_level_formula.anchor_level {
290            return Some(self.character_level_formula.character_level(level));
291        }
292        self.character_levels
293            .iter()
294            .find(|x| x.level == level)
295            .cloned()
296    }
297
298    fn require_character_level(&self, level: i64) -> anyhow::Result<CharacterLevel> {
299        self.character_level(level)
300            .with_context(|| format!("No character_level with level={level}"))
301    }
302
303    fn level_for_experience(&self, experience: i64) -> i64 {
304        let formula = &self.character_level_formula;
305        if experience >= formula.anchor_experience {
306            return formula.level_for_experience(experience);
307        }
308        // Below the anchor: highest authored level whose requirement is met.
309        self.character_levels
310            .iter()
311            .filter(|x| x.required_experience <= experience)
312            .map(|x| x.level)
313            .max()
314            .unwrap_or(0)
315    }
316
317    fn class(&self, class_id: ClassId) -> Option<&Class> {
318        self.classes.iter().find(|c| c.id == class_id)
319    }
320
321    fn require_class(&self, class_id: ClassId) -> anyhow::Result<&Class> {
322        self.class(class_id)
323            .with_context(|| format!("No class in config with id={class_id}"))
324    }
325
326    fn dungeon_template(&self, dungeon_id: DungeonTemplateId) -> Option<&DungeonTemplate> {
327        self.dungeon_templates.iter().find(|d| d.id == dungeon_id)
328    }
329
330    fn require_dungeon_template(
331        &self,
332        dungeon_id: DungeonTemplateId,
333    ) -> anyhow::Result<&DungeonTemplate> {
334        self.dungeon_template(dungeon_id)
335            .with_context(|| format!("No dungeon_template in config with id={dungeon_id}"))
336    }
337
338    fn effect(&self, effect_id: EffectId) -> Option<&Effect> {
339        self.effects.iter().find(|e| e.id == effect_id)
340    }
341
342    fn require_effect(&self, effect_id: EffectId) -> anyhow::Result<&Effect> {
343        self.effect(effect_id)
344            .with_context(|| format!("No effect in config with id={effect_id}"))
345    }
346
347    fn fight_template(&self, fight_id: FightTemplateId) -> Option<&FightTemplate> {
348        self.fight_templates.iter().find(|f| f.id == fight_id)
349    }
350
351    fn require_fight_template(&self, fight_id: FightTemplateId) -> anyhow::Result<&FightTemplate> {
352        self.fight_template(fight_id)
353            .with_context(|| format!("No fight_template in config with id={fight_id}"))
354    }
355
356    fn arena_league_for_rating(&self, rating: i64) -> Option<&ArenaLeague> {
357        self.arena_leagues
358            .iter()
359            .find(|league| rating >= league.min_rating && rating <= league.max_rating)
360    }
361
362    fn require_arena_league_for_rating(&self, rating: i64) -> anyhow::Result<&ArenaLeague> {
363        self.arena_league_for_rating(rating)
364            .with_context(|| format!("No arena_league in config for rating={rating}"))
365    }
366
367    fn entity_template(&self, entity_template_id: EntityTemplateId) -> Option<&EntityTemplate> {
368        self.entities.iter().find(|e| e.id == entity_template_id)
369    }
370
371    fn require_entity_template(
372        &self,
373        entity_template_id: EntityTemplateId,
374    ) -> anyhow::Result<&EntityTemplate> {
375        self.entity_template(entity_template_id)
376            .with_context(|| format!("No entity_template in config with id={entity_template_id}"))
377    }
378
379    fn gift_template(&self, gift_id: uuid::Uuid) -> Option<&GiftTemplate> {
380        self.gifts.iter().find(|g| g.id == gift_id)
381    }
382
383    fn require_gift_template(&self, gift_id: uuid::Uuid) -> anyhow::Result<&GiftTemplate> {
384        self.gift_template(gift_id)
385            .with_context(|| format!("No gift_template in config with id={gift_id}"))
386    }
387
388    fn item_case_settings_by_level(&self, level: i64) -> Option<&ItemCasesSettingsByLevel> {
389        self.item_cases_settings.iter().find(|s| s.level == level)
390    }
391
392    fn require_item_case_settings_by_level(
393        &self,
394        level: i64,
395    ) -> anyhow::Result<&ItemCasesSettingsByLevel> {
396        self.item_case_settings_by_level(level)
397            .with_context(|| format!("No item_case_settings with level={level}"))
398    }
399
400    fn item_rarity(&self, rarity_id: ItemRarityId) -> Option<&ItemRarity> {
401        self.item_rarities.iter().find(|r| r.id == rarity_id)
402    }
403
404    fn require_item_rarity(&self, rarity_id: ItemRarityId) -> anyhow::Result<&ItemRarity> {
405        self.item_rarity(rarity_id)
406            .with_context(|| format!("No item_rarity in config with id={rarity_id}"))
407    }
408
409    fn item_template(&self, item_id: ItemTemplateId) -> Option<&ItemTemplate> {
410        self.items.iter().find(|i| i.id == item_id)
411    }
412
413    fn require_item_template(&self, item_id: ItemTemplateId) -> anyhow::Result<&ItemTemplate> {
414        self.item_template(item_id)
415            .with_context(|| format!("No item_template in config with id={item_id}"))
416    }
417
418    fn mail_template(&self, template_id: uuid::Uuid) -> Option<&MailTemplate> {
419        self.mail_templates.iter().find(|t| t.id == template_id)
420    }
421
422    fn require_mail_template(&self, template_id: uuid::Uuid) -> anyhow::Result<&MailTemplate> {
423        self.mail_template(template_id)
424            .with_context(|| format!("No mail_template in config with id={template_id}"))
425    }
426
427    fn patron_level_info(&self, level: i64) -> Option<&ReferralLevelInfo> {
428        self.patron_levels.iter().find(|x| x.level == level)
429    }
430
431    fn require_patron_level_info(&self, level: i64) -> anyhow::Result<&ReferralLevelInfo> {
432        self.patron_level_info(level)
433            .with_context(|| format!("No patron_level_info in config with level={level}"))
434    }
435
436    fn projectile(&self, projectile_id: ProjectileId) -> Option<&Projectile> {
437        self.projectiles.iter().find(|p| p.id == projectile_id)
438    }
439
440    fn require_projectile(&self, projectile_id: ProjectileId) -> anyhow::Result<&Projectile> {
441        self.projectile(projectile_id)
442            .with_context(|| format!("No projectile in config with id={projectile_id}"))
443    }
444
445    fn quest(&self, quest_id: uuid::Uuid) -> Option<&QuestTemplate> {
446        self.quests.iter().find(|q| q.id == quest_id)
447    }
448
449    fn require_quest(&self, quest_id: uuid::Uuid) -> anyhow::Result<&QuestTemplate> {
450        self.quest(quest_id)
451            .with_context(|| format!("No quest in config with id={quest_id}"))
452    }
453
454    fn quest_by_code(&self, code: &str) -> Option<&QuestTemplate> {
455        self.quests.iter().find(|q| q.code.as_deref() == Some(code))
456    }
457
458    fn skin(&self, skin_id: SkinId) -> Option<&ConfigSkin> {
459        self.skins.iter().find(|s| s.id == skin_id)
460    }
461
462    fn require_skin(&self, skin_id: SkinId) -> anyhow::Result<&ConfigSkin> {
463        self.skin(skin_id)
464            .with_context(|| format!("No skin in config with id={skin_id}"))
465    }
466
467    fn vassal_task_template(&self, task_template_id: uuid::Uuid) -> Option<&VassalTaskTemplate> {
468        self.vassal_tasks.iter().find(|t| t.id == task_template_id)
469    }
470
471    fn require_vassal_task_template(
472        &self,
473        task_template_id: uuid::Uuid,
474    ) -> anyhow::Result<&VassalTaskTemplate> {
475        self.vassal_task_template(task_template_id)
476            .with_context(|| {
477                format!("No vassal_task_template in config with id={task_template_id}")
478            })
479    }
480
481    fn cheat_script(&self, cheat_script_id: CheatScriptId) -> Option<&CheatScript> {
482        self.cheat_scripts.iter().find(|c| c.id == cheat_script_id)
483    }
484
485    fn talent(&self, talent_id: TalentId) -> Option<&TalentTemplate> {
486        self.talents.iter().find(|t| t.id == talent_id)
487    }
488
489    fn require_talent(&self, talent_id: TalentId) -> anyhow::Result<&TalentTemplate> {
490        self.talent(talent_id)
491            .with_context(|| format!("No talent in config with id={talent_id}"))
492    }
493
494    fn allowed_wishlist_rarity_ids(&self, ability_case_level: i64) -> Vec<AbilityRarityId> {
495        self.ability_case_settings_by_level(ability_case_level)
496            .map(|settings| settings.allowed_wishlist_rarity_ids.clone())
497            .unwrap_or_default()
498    }
499
500    fn pet_template(&self, pet_id: PetId) -> Option<&PetTemplate> {
501        self.pet_templates.iter().find(|t| t.id == pet_id)
502    }
503
504    fn require_pet_template(&self, pet_id: PetId) -> anyhow::Result<&PetTemplate> {
505        self.pet_template(pet_id)
506            .with_context(|| format!("No pet_template in config with id={pet_id}"))
507    }
508
509    fn pet_rarity(&self, rarity_id: PetRarityId) -> Option<&PetRarity> {
510        self.pet_rarities.iter().find(|r| r.id == rarity_id)
511    }
512
513    fn pet_level(&self, rarity_id: PetRarityId, level: i64) -> Option<&PetLevel> {
514        self.pet_levels
515            .iter()
516            .find(|x| x.rarity_id == rarity_id && x.level == level)
517    }
518
519    fn pet_slots_for_chapter_level(&self, chapter_level: i64) -> Option<u64> {
520        self.pet_slots_levels
521            .iter()
522            .rev()
523            .find(|level| level.from_chapter_level <= chapter_level)
524            .map(|level| level.pet_slots)
525    }
526
527    fn pet_case_settings_by_level(&self, level: i64) -> Option<&PetCasesSettingsByLevel> {
528        self.pet_cases_settings.iter().find(|x| x.level == level)
529    }
530
531    fn require_pet_case_settings_by_level(
532        &self,
533        level: i64,
534    ) -> anyhow::Result<&PetCasesSettingsByLevel> {
535        self.pet_case_settings_by_level(level)
536            .with_context(|| format!("No pet_case_settings with level={level}"))
537    }
538
539    fn allowed_pet_wishlist_rarity_ids(
540        &self,
541        pet_case_level: i64,
542    ) -> Vec<essences::pets::PetRarityId> {
543        self.pet_case_settings_by_level(pet_case_level)
544            .map(|settings| settings.allowed_wishlist_rarity_ids.clone())
545            .unwrap_or_default()
546    }
547
548    fn trigger_stone_template(
549        &self,
550        template_id: StoneTemplateId,
551    ) -> Option<&TriggerStoneTemplate> {
552        self.trigger_stones.iter().find(|t| t.id == template_id)
553    }
554
555    fn effect_stone_template(&self, template_id: StoneTemplateId) -> Option<&EffectStoneTemplate> {
556        self.effect_stones.iter().find(|t| t.id == template_id)
557    }
558}