configs/
tests_game_config.rs

1use essences::{
2    abilities::{
3        AbilityCastType, AbilityFightUiVisibility, AbilityRarity, AbilityTag, AbilityTargetType,
4        AbilityTemplate,
5    },
6    ability_presets,
7    ability_stones::AbilityStoneTemplate,
8    bundles, class,
9    currency::{Currency, CurrencyUnit},
10    dungeons::{DungeonTemplate, DungeonTip, RewardType},
11    effect::Effect,
12    entity::Coordinates,
13    fighting::{
14        EntityTeam, EntityType, FightEntity, FightTemplate, FightType, PrepareFightEntityPower,
15        PrepareFightSpawn, PrepareFightWaves,
16    },
17    game::{
18        AbilitySlotsLevel, Chapter, CharacterLevel, CharacterLevelAttributeFormula,
19        CharacterLevelFormula, EnemyReward, EntityAttribute, EntityTemplate, PetSlotsLevel,
20    },
21    gatings::{AutoChestGatings, CastleGating, Gatings, NavBarNavigation, SideBarNavigation},
22    generation::{
23        BotsSettings, PhotoGenerationSettings, UsernameGenerationSettings, UsersGeneratingSettings,
24    },
25    gift::{GiftTemplate, GiftType},
26    item_case::{
27        AutoChestSettings, InventoryLevel, InventorySlotConfig, ItemCaseRarityWeight,
28        ItemCasesSettingsByLevel,
29    },
30    items::{Attribute, ItemAttributeSettings, ItemRarity, ItemTemplate, ItemType, WorldSide},
31    mail::{MailRepeatType, MailSendType, MailTemplate},
32    offers::{OfferTemplate, PaymentType, ShopTab, ShopTabConfig},
33    pet_facets::PetFacet,
34    pets::{PetRarity, PetSecondaryStat, PetTemplate},
35    quest::{
36        QuestGroupType, QuestTemplate, QuestsProgressionPointSettings, QuestsProgressionSettings,
37    },
38    ratings::{RatingRangeReward, RatingSettings, RatingType},
39    referrals::ReferralLevelInfo,
40    skins::{ConfigSkin, SkinType, SkinsSettings},
41    vassals::VassalTaskTemplate,
42};
43
44use uuid::{Uuid, uuid};
45
46use crate::artifacts::{
47    ArtifactOwnershipBonusStat, ArtifactSocketUnlock, ArtifactStoneDropSettings, ArtifactStoneRule,
48    ArtifactStoneTemplate, ArtifactTemplate, ArtifactTierMagnitude, ArtifactUpgradeStep,
49    ArtifactWorldLaw, ArtifactsSettings,
50};
51use crate::local_notifications::{
52    LocalNotificationText, LocalNotificationsSettings, WinBackNotification,
53};
54use crate::plinko::{
55    PlinkoAxisTranche, PlinkoLevelSlotLayout, PlinkoPinBonus, PlinkoSettings, PlinkoSlotGroup,
56    PlinkoTranche,
57};
58use crate::statue::{
59    StatueBonusGradeValue, StatueBonusGradeWeight, StatueBonusTypeConfig, StatueLevelConfig,
60    StatueRollCostConfig, StatueSettings,
61};
62use crate::stones::{
63    EffectStoneAction, EffectStoneTemplate, StoneKillDropSettings, StoneRarityWeight,
64    StoneSocketUnlock, StoneStat, StoneTierCoefficient, StoneUpgradeStep, StonesSettings,
65    TriggerCondition, TriggerStoneTemplate,
66};
67use crate::validated_types::{
68    NonEmptyVec, NonZeroU64, PositiveF64, PositiveI32, PositiveI64, WeightMultiplier,
69};
70use crate::{
71    abilities::{
72        AbilityCaseCheckpointAbilityReward, AbilityCaseCheckpointReward, AbilityCaseEvolveRule,
73        AbilityCaseRarityWeight, AbilityCasesSettingsByLevel, AbilityLevel, BigRollShard,
74        Projectile,
75    },
76    ability_stones::{AbilityStoneSettings, AbilityStoneSocketSettings},
77    ads_settings::{AdSpeedupConfig, AdsSettings, BirdAdConfig, BirdAdVariantWeight, BirdVariant},
78    afk_rewards::{
79        AfkRewardBonusType, AfkRewardBonusWeight, AfkRewardsByLevel, AfkRewardsSettings,
80        CurrencyRate,
81    },
82    buffs::{BuffEffect, BuffTemplate},
83    events::EventDescription,
84    fighting::{ArenaLeague, ArenaSettings, FightSettings, PvpSettings},
85    flip::FlipSettings,
86    game_config::GameConfig,
87    game_settings::{DailyCurrencyReset, GameSettings},
88    mana::ManaSettings,
89    matchmaking::{MatchmakingSettings, OpponentPositionSettings},
90    pets::{
91        PetBigRollShard, PetCaseCheckpointPetReward, PetCaseCheckpointReward, PetCaseRarityWeight,
92        PetCasesSettingsByLevel, PetLevel,
93    },
94    portal_rarities::PortalRarity,
95    reports::ReportsSettings,
96    vassals::VassalsSettings,
97};
98use essences::progress_pass::{ProgressPassConfig, ProgressPassTierTemplate};
99use essences::statue::StatueBonusGrade;
100use essences::talent_tree::{
101    TalentAttributeBonus, TalentBackendModifier, TalentLevel, TalentPosition, TalentTemplate,
102    TalentTreeSettings, TalentUnlockCondition,
103};
104
105/// Twin-core settings for tests (OVT-2517). Shapes mirror production; the
106/// numbers are the smallest set that still exercises every rule: a 5-level cap,
107/// 5 slot cap, one slot per level, the BAL-010 multiplier formula and one rhythm row
108/// per law type.
109fn generate_cores_settings() -> crate::cores::CoresSettings {
110    crate::cores::CoresSettings {
111        // Non-zero, but deliberately LOW. Since BAL-010 the gate GRANTS both
112        // cores at level 1, so a zero here would hand every test character a
113        // core stat multiplier and silently scale unrelated assertions (stones,
114        // plinko). Keeping it at 1 lets cores tests satisfy the gate while a
115        // character below it stays core-free. Kept as LOW as possible (1) so the
116        // cores tests, which build at exactly this chapter, still fight the
117        // easiest rung of the BAL-037 curve; anything higher made their fights
118        // hard enough to change the outcome.
119        unlock_chapter: 1,
120        // Deliberately ABOVE the slot ladder, like the shipped config: levels
121        // 1..=5 open slots, grant laws and bind the bridge limit, and everything
122        // above them only grows the multiplier. 50 rather than the shipped 9999
123        // so a test that walks the whole ladder stays cheap while still covering
124        // the extension.
125        max_core_level: 50,
126        // 5 slots at one slot per level: slots = level, possible bridges =
127        // slots, allowed = min(level, 5) - 1 — the bridge limit binds at every
128        // level, which is what `BL-01 Extra Span` buys past (post-merge plan §8).
129        max_slots_per_core: 5,
130        core_levels_per_slot: PositiveI64::new(1),
131        // NOT `currencies[0]`: several existing tests assert on the first
132        // currency's balance after mob kills, and a second faucet landing in the
133        // same pot would silently corrupt them.
134        upgrade_currency_id: uuid!("22297520-abb8-45b9-9fbb-c418b8001246"),
135        // Production uses a D1-calibrated one-time grant. Keep the generic
136        // fixture neutral so unrelated fight tests do not acquire currency.
137        unlock_essence_grant: 0,
138        // The placeholder mob-kill faucets are OFF in the test fixture. They
139        // draw from the fight RNG and add currency on every kill, which would
140        // perturb every seed-sensitive combat/economy test in the workspace.
141        // `test_cores.rs` turns them on explicitly where it measures them.
142        essence_chapter_bands: vec![
143            (21, 1_000.0, 2, 0.961_538),
144            (52, 1_500.0, 3, 0.961_538),
145            (95, 2_250.0, 5, 0.865_385),
146            (135, 3_400.0, 7, 0.934_066),
147            (201, 5_000.0, 10, 0.961_538),
148            (251, 7_600.0, 15, 0.974_359),
149            (301, 11_400.0, 22, 0.996_503),
150            (351, 17_100.0, 33, 0.996_503),
151            (401, 25_600.0, 50, 0.984_615),
152        ]
153        .into_iter()
154        .map(
155            |(from_chapter, r_value, packet, base_chance)| crate::cores::CoreEssenceChapterBand {
156                from_chapter,
157                r_value,
158                packet: PositiveI64::new(packet),
159                base_chance,
160            },
161        )
162        .collect(),
163        kill_drop: crate::cores::CoreCurrencyKillDrop {
164            chance: 0.0,
165            amount: PositiveI64::new(1),
166        },
167        law_drop_chance: 0.0,
168        // BAL-010: the priced ladder starts at level 2 -- level 1 is granted with
169        // the ch21 unlock, not bought.
170        level_costs: NonEmptyVec::new(
171            (crate::cores::CORE_FIRST_PURCHASABLE_LEVEL..=5)
172                .map(|level| crate::cores::CoreLevelCost {
173                    level,
174                    cost: PositiveI64::new(100 * level),
175                })
176                .collect(),
177        ),
178        // Doubling per level above the last tabled rung, matching the shape the
179        // tabled rows themselves have (100 -> 500 is not a doubling here, but a
180        // clean factor keeps the extension easy to assert on).
181        level_cost_growth: PositiveF64::new(2.0),
182        multiplied_attribute_ids: vec![
183            uuid!("45eca0a7-7430-487b-bd65-b796c6d88c08"),
184            uuid!("6ad485d7-9567-4b77-8ddc-a2418dd1dfe6"),
185            uuid!("3a6ec1c8-7494-43df-a345-d23e297b892d"),
186        ],
187        law_upgrade_ladder: NonEmptyVec::new(vec![
188            crate::cores::LawUpgradeStep {
189                level: 2,
190                required_copies: PositiveI64::new(2),
191            },
192            crate::cores::LawUpgradeStep {
193                level: 3,
194                required_copies: PositiveI64::new(3),
195            },
196            crate::cores::LawUpgradeStep {
197                level: 4,
198                required_copies: PositiveI64::new(5),
199            },
200            crate::cores::LawUpgradeStep {
201                level: 5,
202                required_copies: PositiveI64::new(8),
203            },
204        ]),
205        bridge_charge: crate::cores::BridgeChargeConfig {
206            capacity: PositiveI64::new(10),
207            amplification_cap_permyriad: PositiveI64::new(5_000),
208        },
209    }
210}
211
212/// `armor.mod` — the percentage companion of `armor` (see `generate_attributes`).
213pub const TEST_ATTR_ARMOR_MOD: uuid::Uuid = uuid!("019f1000-0000-7000-8000-0000000000a0");
214/// Neutral incoming-damage multiplier. Its presence is what lets a real swing
215/// deal damage at all — see the attribute's own comment.
216pub const TEST_ATTR_RECEIVED_DAMAGE: uuid::Uuid = uuid!("019f1000-0000-7000-8000-0000000000a1");
217
218/// A Basic Attack that really swings — `mechanics::fight::attack`, not a flat
219/// `Damage` event. Needed because every other fixture ability short-circuits
220/// the damage primitives, so nothing armed for "the next attack" was ever
221/// observed being spent.
222///
223/// Deliberately NOT in any class kit here: characters are granted every basic
224/// ability of their class, so putting it in the shared fixture would change the
225/// starting loadout for every test in the workspace. A test that needs it to
226/// classify as Basic adds it to the class kit in its own config copy.
227pub const TEST_ABILITY_SWING_BASIC: uuid::Uuid = uuid!("019f1000-0000-7000-8000-0000000000b1");
228/// The Skill-family counterpart of [`TEST_ABILITY_SWING_BASIC`]: same swing,
229/// deliberately absent from every class kit so it classifies as a Skill.
230pub const TEST_ABILITY_SWING_SKILL: uuid::Uuid = uuid!("019f1000-0000-7000-8000-0000000000b2");
231/// Multi-target swing: one cast, one real swing per living enemy, all on the
232/// cast's own tick. `SkillMinTargets` counts targets within a single cast, so
233/// it is only reachable from an ability like this one.
234pub const TEST_ABILITY_SWING_AOE: uuid::Uuid = uuid!("019f1000-0000-7000-8000-0000000000b3");
235/// Fixed `power` both swinging fixtures attack with, so a test can compare a
236/// run with a law against a run without one and attribute the whole difference
237/// to the law.
238pub const TEST_SWING_POWER: f64 = 100.0;
239
240pub const TEST_LAW_REAL_ATTACK: uuid::Uuid = uuid!("019f1000-0000-7000-8000-000000000001");
241pub const TEST_LAW_REAL_DEFENSE: uuid::Uuid = uuid!("019f1000-0000-7000-8000-000000000002");
242pub const TEST_LAW_FANTASY_HEALING: uuid::Uuid = uuid!("019f1000-0000-7000-8000-000000000003");
243pub const TEST_LAW_FANTASY_ATTACK: uuid::Uuid = uuid!("019f1000-0000-7000-8000-000000000004");
244/// Fires on a Core Dodge, so a test can drive a law from a defensive outcome.
245pub const TEST_LAW_REAL_DODGE: uuid::Uuid = uuid!("019f1000-0000-7000-8000-000000000005");
246/// Fires on "a Core action killed an enemy" — the `RL-11` / `FL-09` shape.
247pub const TEST_LAW_FANTASY_KILL: uuid::Uuid = uuid!("019f1000-0000-7000-8000-000000000006");
248/// Fires on an original Skill carrying the `Heal` tag (`RL-06` / `FL-06`).
249pub const TEST_LAW_REAL_HEAL_TAG: uuid::Uuid = uuid!("019f1000-0000-7000-8000-000000000007");
250/// Mana-dependent and therefore shipped INACTIVE, exactly like `RL-07`,
251/// `RL-08` and `FL-08` in the live catalog.
252pub const TEST_LAW_FANTASY_INACTIVE: uuid::Uuid = uuid!("019f1000-0000-7000-8000-000000000008");
253/// Fires when the owner's side comes up (`RL-12` / `FL-11`), once per phase.
254pub const TEST_LAW_REAL_PHASE: uuid::Uuid = uuid!("019f1000-0000-7000-8000-000000000009");
255
256// The six effect families below carry no live law in the fixture otherwise, so
257// without them the whole group ships untested — which is how the charge-gating
258// regression in `NEXT_ATTACK_BONUS` reached a green suite.
259/// Arms the owner's next BASIC attack (`RL-02` / `RL-08`).
260pub const TEST_LAW_REAL_NEXT_ATTACK: uuid::Uuid = uuid!("019f1000-0000-7000-8000-00000000000a");
261/// Takes ticks off the single longest remaining cooldown (`RL-03`).
262pub const TEST_LAW_REAL_CUT_LONGEST: uuid::Uuid = uuid!("019f1000-0000-7000-8000-00000000000b");
263/// Takes ticks off every remaining cooldown (`RL-11` / `FL-04`).
264pub const TEST_LAW_REAL_CUT_ALL: uuid::Uuid = uuid!("019f1000-0000-7000-8000-00000000000c");
265/// Makes the shortest remaining cooldown ready (`FL-11`).
266pub const TEST_LAW_FANTASY_READY: uuid::Uuid = uuid!("019f1000-0000-7000-8000-00000000000d");
267/// Timed incoming-damage reduction on an HP threshold (`RL-10`).
268pub const TEST_LAW_REAL_HP_GUARD: uuid::Uuid = uuid!("019f1000-0000-7000-8000-00000000000e");
269/// Arms a derived repeat of the next original Skill (`FL-06` / `FL-12`).
270pub const TEST_LAW_FANTASY_ECHO: uuid::Uuid = uuid!("019f1000-0000-7000-8000-00000000000f");
271
272// The two conditions below carried no fixture at all, so three ENABLED laws in
273// the live catalog (`Crowd_Opening`, `Wide_Explosion`, `Three_Spell_Rule`) fired
274// on logic no test had ever evaluated.
275/// Fires on a Skill that reaches at least `condition_value` targets
276/// (`Crowd_Opening` / `Wide_Explosion`).
277pub const TEST_LAW_REAL_WIDE_CAST: uuid::Uuid = uuid!("019f1000-0000-7000-8000-000000000010");
278/// Fires on `condition_value` DISTINCT Skills inside `condition_window_ticks`
279/// (`Three_Spell_Rule`) — the only condition with a live time window.
280pub const TEST_LAW_FANTASY_THREE_SPELLS: uuid::Uuid = uuid!("019f1000-0000-7000-8000-000000000011");
281
282/// The law fixture, built to exercise the v0.2 model rather than to be
283/// balanced: both sides, a pre-cast condition, a reaction condition, a
284/// phase-start condition, a tag condition, an inactive mana law, and passive
285/// modifiers on the first four so the attribute-delta path stays covered.
286fn generate_laws() -> Vec<essences::cores::LawTemplate> {
287    let mut laws = build_laws();
288    // Spread the deterministic unlock schedule across the slot ladder, per side
289    // and in catalog order: side-local index 0 -> level 1, 1 -> level 2, …,
290    // wrapping at the last slot level. Every side gets a law on level 1 (so a
291    // freshly bought core grants something) and the catalog is fully covered by
292    // the last slot level, which is the invariant
293    // `validate_cores_settings` enforces on the shipped content too.
294    let slot_levels = 5;
295    let mut seen_per_side = std::collections::HashMap::new();
296    for law in &mut laws {
297        let index: i64 = *seen_per_side
298            .entry(law.side)
299            .and_modify(|n| *n += 1)
300            .or_insert(0);
301        law.unlock_core_level = index % slot_levels + 1;
302    }
303    laws
304}
305
306fn build_laws() -> Vec<essences::cores::LawTemplate> {
307    use essences::abilities::AbilityTag;
308    use essences::cores::{LawAttributeModifier, LawCondition, LawEffect, LawTemplate};
309    let strength = uuid!("3a6ec1c8-7494-43df-a345-d23e297b892d");
310
311    let law = |id: uuid::Uuid,
312               name: &str,
313               side: essences::flip::WorldSide,
314               condition: LawCondition,
315               condition_value: i64,
316               effect: LawEffect,
317               effect_value: i64,
318               resonance: i64|
319     -> LawTemplate {
320        LawTemplate {
321            id,
322            name: i18n::I18nString::Translated(name.to_string()),
323            description: i18n::I18nString::Translated(name.to_string()),
324            side,
325            // Overwritten below by the round-robin spread; the closure has no
326            // counter to work with.
327            unlock_core_level: 1,
328            condition,
329            condition_value,
330            condition_tag: None,
331            condition_window_ticks: 0,
332            once_per_phase: false,
333            effect,
334            effect_value,
335            effect_duration_ticks: 0,
336            resonance,
337            is_active: true,
338            power_q_first_rank: 1.10,
339            power_q_max_rank: 1.30,
340            modifiers: Vec::new(),
341            icon_path: String::new(),
342        }
343    };
344
345    let mut real_attack = law(
346        TEST_LAW_REAL_ATTACK,
347        "Test Real Attack Law",
348        essences::flip::WorldSide::Real,
349        LawCondition::NthBasicAttack,
350        3,
351        LawEffect::ThisAttackDamageBonus,
352        10_000,
353        1,
354    );
355    real_attack.modifiers = vec![LawAttributeModifier {
356        attribute_id: strength,
357        base_value: 100,
358    }];
359
360    let mut real_defense = law(
361        TEST_LAW_REAL_DEFENSE,
362        "Test Real Defense Law",
363        essences::flip::WorldSide::Real,
364        LawCondition::NthHitTaken,
365        3,
366        LawEffect::HealPercentMaxHp,
367        400,
368        2,
369    );
370    real_defense.modifiers = vec![LawAttributeModifier {
371        attribute_id: strength,
372        base_value: 40,
373    }];
374
375    let mut fantasy_healing = law(
376        TEST_LAW_FANTASY_HEALING,
377        "Test Fantasy Healing Law",
378        essences::flip::WorldSide::Fantasy,
379        LawCondition::OriginalSkillCast,
380        0,
381        LawEffect::AoeDerivedDamage,
382        6_000,
383        2,
384    );
385    fantasy_healing.modifiers = vec![LawAttributeModifier {
386        attribute_id: strength,
387        base_value: 200,
388    }];
389
390    // Deliberately a pure ARM rather than derived damage: tests that bank
391    // Resonance over several crits must not have the law's own effect feeding
392    // the flip gauge and moving the very flip they are measuring.
393    let mut fantasy_attack = law(
394        TEST_LAW_FANTASY_ATTACK,
395        "Test Fantasy Attack Law",
396        essences::flip::WorldSide::Fantasy,
397        LawCondition::CriticalHit,
398        0,
399        LawEffect::NextSkillPayloadBonus,
400        3_000,
401        2,
402    );
403    fantasy_attack.modifiers = vec![LawAttributeModifier {
404        attribute_id: strength,
405        base_value: 50,
406    }];
407
408    let mut real_heal_tag = law(
409        TEST_LAW_REAL_HEAL_TAG,
410        "Test Real Heal Tag Law",
411        essences::flip::WorldSide::Real,
412        LawCondition::SkillWithTag,
413        0,
414        LawEffect::DamageBuff,
415        2_000,
416        2,
417    );
418    real_heal_tag.condition_tag = Some(AbilityTag::Heal);
419    real_heal_tag.effect_duration_ticks = 4_000;
420
421    let mut real_phase = law(
422        TEST_LAW_REAL_PHASE,
423        "Test Real Phase Law",
424        essences::flip::WorldSide::Real,
425        LawCondition::PhaseStarted,
426        0,
427        LawEffect::AttackSpeedBuff,
428        2_000,
429        4,
430    );
431    real_phase.once_per_phase = true;
432    real_phase.effect_duration_ticks = 5_000;
433
434    let mut fantasy_inactive = law(
435        TEST_LAW_FANTASY_INACTIVE,
436        "Test Fantasy Inactive Law",
437        essences::flip::WorldSide::Fantasy,
438        LawCondition::SkillManaAtLeast,
439        30,
440        LawEffect::NextSkillPayloadBonus,
441        3_000,
442        3,
443    );
444    fantasy_inactive.is_active = false;
445    fantasy_inactive.modifiers = vec![LawAttributeModifier {
446        attribute_id: strength,
447        base_value: 1_000,
448    }];
449
450    vec![
451        real_attack,
452        real_defense,
453        fantasy_healing,
454        fantasy_attack,
455        law(
456            TEST_LAW_REAL_DODGE,
457            "Test Real Dodge Law",
458            essences::flip::WorldSide::Real,
459            LawCondition::Dodge,
460            0,
461            LawEffect::NextAttackSplash,
462            15_000,
463            4,
464        ),
465        law(
466            TEST_LAW_FANTASY_KILL,
467            "Test Fantasy Kill Law",
468            essences::flip::WorldSide::Fantasy,
469            LawCondition::KilledEnemy,
470            0,
471            LawEffect::AoeDerivedDamage,
472            15_000,
473            2,
474        ),
475        real_heal_tag,
476        real_phase,
477        fantasy_inactive,
478        law(
479            TEST_LAW_REAL_NEXT_ATTACK,
480            "Test Real Next Attack Law",
481            essences::flip::WorldSide::Real,
482            LawCondition::OriginalSkillCast,
483            0,
484            LawEffect::NextAttackDamageBonus,
485            6_000,
486            2,
487        ),
488        law(
489            TEST_LAW_REAL_CUT_LONGEST,
490            "Test Real Cut Longest Law",
491            essences::flip::WorldSide::Real,
492            LawCondition::CriticalHit,
493            0,
494            LawEffect::ReduceLongestCooldown,
495            500,
496            2,
497        ),
498        law(
499            TEST_LAW_REAL_CUT_ALL,
500            "Test Real Cut All Law",
501            essences::flip::WorldSide::Real,
502            LawCondition::KilledEnemy,
503            0,
504            LawEffect::ReduceAllCooldowns,
505            500,
506            2,
507        ),
508        law(
509            TEST_LAW_FANTASY_READY,
510            "Test Fantasy Ready Law",
511            essences::flip::WorldSide::Fantasy,
512            LawCondition::KilledEnemy,
513            0,
514            LawEffect::ReadyShortestCooldown,
515            0,
516            2,
517        ),
518        {
519            let mut guard = law(
520                TEST_LAW_REAL_HP_GUARD,
521                "Test Real HP Guard Law",
522                essences::flip::WorldSide::Real,
523                LawCondition::HpFellBelow,
524                5_000,
525                LawEffect::DamageReduction,
526                2_500,
527                5,
528            );
529            guard.once_per_phase = true;
530            guard.effect_duration_ticks = 5_000;
531            guard
532        },
533        law(
534            TEST_LAW_FANTASY_ECHO,
535            "Test Fantasy Echo Law",
536            essences::flip::WorldSide::Fantasy,
537            LawCondition::OriginalSkillCast,
538            0,
539            LawEffect::NextSkillEcho,
540            4_000,
541            2,
542        ),
543        // Both use `HealPercentMaxHp`: its outcome is one event with one
544        // number, so a test on the CONDITION fails only because of the
545        // condition.
546        law(
547            TEST_LAW_REAL_WIDE_CAST,
548            "Test Real Wide Cast Law",
549            essences::flip::WorldSide::Real,
550            LawCondition::SkillMinTargets,
551            2,
552            LawEffect::HealPercentMaxHp,
553            400,
554            2,
555        ),
556        {
557            let mut three = law(
558                TEST_LAW_FANTASY_THREE_SPELLS,
559                "Test Fantasy Three Spells Law",
560                essences::flip::WorldSide::Fantasy,
561                LawCondition::DistinctSkillsWithin,
562                3,
563                LawEffect::HealPercentMaxHp,
564                400,
565                2,
566            );
567            // Small on purpose: the fight clock in the test harness advances
568            // one unit per dispatched event, so a 6000-wide window would need
569            // 6000 events to step outside. The boundary logic is the same.
570            three.condition_window_ticks = 5;
571            three
572        },
573    ]
574}
575
576/// The `received_damage` attribute, deliberately NOT part of the default test
577/// config.
578///
579/// It is the ×10000 incoming multiplier every landed hit is scaled by, and
580/// `mechanics::fight::damage_entity` deals NO damage at all while it has no base
581/// value (`has_attribute_base`). Adding it to the shared fixture therefore turns
582/// real damage on for every `attack` in the workspace at once — arena outcomes,
583/// flip thresholds and fight lengths all move. A test that needs a swing to land
584/// pushes this onto its own config copy instead:
585///
586/// ```ignore
587/// let mut config = load_config();
588/// config.attributes.push(received_damage_attribute());
589/// ```
590pub fn received_damage_attribute() -> Attribute {
591    Attribute {
592        id: TEST_ATTR_RECEIVED_DAMAGE,
593        name: i18n::I18nString::Translated("Received Damage".to_string()),
594        prefix: None,
595        suffix: Some(i18n::I18nString::Translated("%".to_string())),
596        code: "received_damage".to_string(),
597        icon: String::new(),
598        icon_path: "/armor_icon.webp".to_string(),
599        db_code: 5,
600        is_percent: false,
601        denominator: Some(10000),
602        calculation_behavior: Some("attr_zero".to_string()),
603        order: 6,
604        is_ui_visible: false,
605        base_value: Some(10_000),
606        description: i18n::I18nString::Translated(
607            "Multiplier applied to incoming damage".to_string(),
608        ),
609    }
610}
611
612fn generate_attributes() -> Vec<Attribute> {
613    vec![
614        Attribute {
615            id: uuid!("45eca0a7-7430-487b-bd65-b796c6d88c08"),
616            name: i18n::I18nString::new(
617                "content.attributes.45eca0a7-7430-487b-bd65-b796c6d88c08:name",
618            ),
619            prefix: Some(i18n::I18nString::new(
620                "content.attributes.45eca0a7-7430-487b-bd65-b796c6d88c08:name",
621            )),
622            suffix: Some(i18n::I18nString::new(
623                "content.attributes.45eca0a7-7430-487b-bd65-b796c6d88c08:name",
624            )),
625            code: "hp".to_string(),
626            icon: "/hp_icon.webp".to_string(),
627            icon_path: "/hp_icon.webp".to_string(),
628            db_code: 0,
629            denominator: None,
630            is_percent: false,
631            // Native item-attribute fn required by `try_finalize_item`
632            // (bot/gacha item generation). `hp` is a required attribute, so
633            // generated items always finalize it. Maps to the `attr_health` port.
634            calculation_behavior: Some("attr_health".to_string()),
635            order: 0,
636            is_ui_visible: true,
637            base_value: None,
638            description: i18n::I18nString::new(
639                "content.attributes.45eca0a7-7430-487b-bd65-b796c6d88c08:name",
640            ),
641        },
642        Attribute {
643            id: uuid!("6ad485d7-9567-4b77-8ddc-a2418dd1dfe6"),
644            name: i18n::I18nString::new(
645                "content.attributes.6ad485d7-9567-4b77-8ddc-a2418dd1dfe6:name",
646            ),
647            prefix: Some(i18n::I18nString::new(
648                "content.attributes.45eca0a7-7430-487b-bd65-b796c6d88c08:name",
649            )),
650            suffix: Some(i18n::I18nString::new(
651                "content.attributes.45eca0a7-7430-487b-bd65-b796c6d88c08:name",
652            )),
653            code: "damage".to_string(),
654            icon: "/damage_icon.webp".to_string(),
655            icon_path: "/damage_icon.webp".to_string(),
656            db_code: 1,
657            is_percent: false,
658            denominator: None,
659            // `damage` is also a required attribute -> finalized on every
660            // generated item. Maps to the `attr_damage` port.
661            calculation_behavior: Some("attr_damage".to_string()),
662            order: 1,
663            is_ui_visible: true,
664            base_value: None,
665            description: i18n::I18nString::new(
666                "content.attributes.45eca0a7-7430-487b-bd65-b796c6d88c08:name",
667            ),
668        },
669        Attribute {
670            id: uuid!("3a6ec1c8-7494-43df-a345-d23e297b892d"),
671            name: i18n::I18nString::new(
672                "content.attributes.3a6ec1c8-7494-43df-a345-d23e297b892d:name",
673            ),
674            prefix: Some(i18n::I18nString::new(
675                "content.attributes.45eca0a7-7430-487b-bd65-b796c6d88c08:name",
676            )),
677            suffix: Some(i18n::I18nString::new(
678                "content.attributes.45eca0a7-7430-487b-bd65-b796c6d88c08:name",
679            )),
680            code: "strength".to_string(),
681            icon: "/strength_icon.webp".to_string(),
682            icon_path: "/strength_icon.webp".to_string(),
683            db_code: 2,
684            is_percent: false,
685            denominator: None,
686            // Not a required/optional attribute on test item templates, so not
687            // finalized in practice; map to a registered fn for completeness.
688            calculation_behavior: Some("attr_zero".to_string()),
689            order: 2,
690            is_ui_visible: true,
691            base_value: None,
692            description: i18n::I18nString::new(
693                "content.attributes.45eca0a7-7430-487b-bd65-b796c6d88c08:name",
694            ),
695        },
696        Attribute {
697            id: uuid!("42aea610-2ab7-41f7-95bf-5d6f00341ac9"),
698            name: i18n::I18nString::new(
699                "content.attributes.42aea610-2ab7-41f7-95bf-5d6f00341ac9:name",
700            ),
701            prefix: Some(i18n::I18nString::new(
702                "content.attributes.45eca0a7-7430-487b-bd65-b796c6d88c08:name",
703            )),
704            suffix: Some(i18n::I18nString::new(
705                "content.attributes.45eca0a7-7430-487b-bd65-b796c6d88c08:name",
706            )),
707            code: "agility".to_string(),
708            icon: "/agility_icon.webp".to_string(),
709            icon_path: "/agility_icon.webp".to_string(),
710            db_code: 3,
711            is_percent: false,
712            denominator: None,
713            calculation_behavior: Some("attr_zero".to_string()),
714            order: 3,
715            is_ui_visible: true,
716            base_value: None,
717            description: i18n::I18nString::new(
718                "content.attributes.45eca0a7-7430-487b-bd65-b796c6d88c08:name",
719            ),
720        },
721        Attribute {
722            id: uuid!("f82eb3fa-8a09-44fb-b8ed-979cc2db28f4"),
723            name: i18n::I18nString::new(
724                "content.attributes.f82eb3fa-8a09-44fb-b8ed-979cc2db28f4:name",
725            ),
726            prefix: Some(i18n::I18nString::new(
727                "content.attributes.45eca0a7-7430-487b-bd65-b796c6d88c08:name",
728            )),
729            suffix: Some(i18n::I18nString::new(
730                "content.attributes.45eca0a7-7430-487b-bd65-b796c6d88c08:name",
731            )),
732            code: "armor".to_string(),
733            icon: "/armor_icon.webp".to_string(),
734            icon_path: "/armor_icon.webp".to_string(),
735            db_code: 4,
736            is_percent: false,
737            denominator: None,
738            // Maps to the `attr_armor` port (optional attribute on some items).
739            calculation_behavior: Some("attr_armor".to_string()),
740            order: 4,
741            is_ui_visible: false,
742            base_value: None,
743            description: i18n::I18nString::new(
744                "content.attributes.45eca0a7-7430-487b-bd65-b796c6d88c08:name",
745            ),
746        },
747        // Percentage companion of `armor`, mirroring `hp.mod` / `attack.mod`:
748        // permyriad, `+10000 == +1.0`, composed by `get_entity_stat` as
749        // `(armor + armor.bonus) × (1 + armor.mod/10000)` and floored at
750        // `MIN_STAT_MOD_MULT`. Shares `armor`'s `db_code` exactly as `hp.mod`
751        // shares `hp`'s — `.mod` attributes never ride on items, so the
752        // db_code is never serialized for them.
753        Attribute {
754            id: TEST_ATTR_ARMOR_MOD,
755            name: i18n::I18nString::Translated("Bonus Armor".to_string()),
756            prefix: None,
757            suffix: Some(i18n::I18nString::Translated("%".to_string())),
758            code: "armor.mod".to_string(),
759            icon: String::new(),
760            icon_path: "/armor_icon.webp".to_string(),
761            db_code: 4,
762            is_percent: false,
763            denominator: Some(10000),
764            calculation_behavior: Some("attr_zero".to_string()),
765            order: 5,
766            is_ui_visible: true,
767            base_value: None,
768            description: i18n::I18nString::Translated(
769                "Increases your Armor by a percentage".to_string(),
770            ),
771        },
772    ]
773}
774
775fn generate_item_cases_settings() -> Vec<ItemCasesSettingsByLevel> {
776    vec![
777        ItemCasesSettingsByLevel {
778            level: 1,
779            required_chapter_level: 0,
780            upgrade_cost: vec![],
781            upgrade_time_secs: 600,
782            rarity_weights: vec![
783                ItemCaseRarityWeight {
784                    rarity_id: uuid!("8c8acba2-67d3-40e0-ac62-2f7acce64ccc"),
785                    weight: 512.0,
786                },
787                ItemCaseRarityWeight {
788                    rarity_id: uuid!("272f3e73-6b3e-43a0-88d6-d34bdaf5aeae"),
789                    weight: 256.0,
790                },
791                ItemCaseRarityWeight {
792                    rarity_id: uuid!("c45eed5f-ad7e-4af0-b0a5-46b311573bf4"),
793                    weight: 128.0,
794                },
795                ItemCaseRarityWeight {
796                    rarity_id: uuid!("a9a0575f-9d09-4118-b305-14e927817302"),
797                    weight: 64.0,
798                },
799                ItemCaseRarityWeight {
800                    rarity_id: uuid!("19e2db05-5f17-4d89-9732-924134056776"),
801                    weight: 32.0,
802                },
803                ItemCaseRarityWeight {
804                    rarity_id: uuid!("9d662020-0929-439b-8afa-f286b72070a9"),
805                    weight: 16.0,
806                },
807                ItemCaseRarityWeight {
808                    rarity_id: uuid!("363dded9-e5a4-4bb9-bde6-63037d808246"),
809                    weight: 8.0,
810                },
811                ItemCaseRarityWeight {
812                    rarity_id: uuid!("9e1842cc-4192-4332-8c13-0f901ca442a4"),
813                    weight: 4.0,
814                },
815                ItemCaseRarityWeight {
816                    rarity_id: uuid!("d59e98d8-fc7d-4b8b-9da0-a374400df154"),
817                    weight: 0.0,
818                },
819                ItemCaseRarityWeight {
820                    rarity_id: uuid!("c0d049eb-359e-4b1e-9de2-eda276043ea8"),
821                    weight: 0.0,
822                },
823            ],
824            auto_chest_settings: AutoChestSettings {
825                max_batch_size: 2,
826                auto_only_max_batch_size: 1,
827            },
828        },
829        ItemCasesSettingsByLevel {
830            level: 2,
831            required_chapter_level: 0,
832            upgrade_cost: vec![CurrencyUnit {
833                currency_id: uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
834                amount: 20,
835            }],
836            upgrade_time_secs: 600,
837            rarity_weights: vec![
838                ItemCaseRarityWeight {
839                    rarity_id: uuid!("8c8acba2-67d3-40e0-ac62-2f7acce64ccc"),
840                    weight: 256.0,
841                },
842                ItemCaseRarityWeight {
843                    rarity_id: uuid!("272f3e73-6b3e-43a0-88d6-d34bdaf5aeae"),
844                    weight: 256.0,
845                },
846                ItemCaseRarityWeight {
847                    rarity_id: uuid!("c45eed5f-ad7e-4af0-b0a5-46b311573bf4"),
848                    weight: 128.0,
849                },
850                ItemCaseRarityWeight {
851                    rarity_id: uuid!("a9a0575f-9d09-4118-b305-14e927817302"),
852                    weight: 64.0,
853                },
854                ItemCaseRarityWeight {
855                    rarity_id: uuid!("19e2db05-5f17-4d89-9732-924134056776"),
856                    weight: 32.0,
857                },
858                ItemCaseRarityWeight {
859                    rarity_id: uuid!("9d662020-0929-439b-8afa-f286b72070a9"),
860                    weight: 16.0,
861                },
862                ItemCaseRarityWeight {
863                    rarity_id: uuid!("363dded9-e5a4-4bb9-bde6-63037d808246"),
864                    weight: 8.0,
865                },
866                ItemCaseRarityWeight {
867                    rarity_id: uuid!("9e1842cc-4192-4332-8c13-0f901ca442a4"),
868                    weight: 4.0,
869                },
870                ItemCaseRarityWeight {
871                    rarity_id: uuid!("d59e98d8-fc7d-4b8b-9da0-a374400df154"),
872                    weight: 0.0,
873                },
874                ItemCaseRarityWeight {
875                    rarity_id: uuid!("c0d049eb-359e-4b1e-9de2-eda276043ea8"),
876                    weight: 0.0,
877                },
878            ],
879            auto_chest_settings: AutoChestSettings {
880                max_batch_size: 2,
881                auto_only_max_batch_size: 1,
882            },
883        },
884        ItemCasesSettingsByLevel {
885            level: 3,
886            required_chapter_level: 5,
887            upgrade_cost: vec![CurrencyUnit {
888                currency_id: uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
889                amount: 20,
890            }],
891            upgrade_time_secs: 600,
892            rarity_weights: vec![
893                ItemCaseRarityWeight {
894                    rarity_id: uuid!("8c8acba2-67d3-40e0-ac62-2f7acce64ccc"),
895                    weight: 256.0,
896                },
897                ItemCaseRarityWeight {
898                    rarity_id: uuid!("272f3e73-6b3e-43a0-88d6-d34bdaf5aeae"),
899                    weight: 256.0,
900                },
901                ItemCaseRarityWeight {
902                    rarity_id: uuid!("c45eed5f-ad7e-4af0-b0a5-46b311573bf4"),
903                    weight: 128.0,
904                },
905                ItemCaseRarityWeight {
906                    rarity_id: uuid!("a9a0575f-9d09-4118-b305-14e927817302"),
907                    weight: 64.0,
908                },
909                ItemCaseRarityWeight {
910                    rarity_id: uuid!("19e2db05-5f17-4d89-9732-924134056776"),
911                    weight: 32.0,
912                },
913                ItemCaseRarityWeight {
914                    rarity_id: uuid!("9d662020-0929-439b-8afa-f286b72070a9"),
915                    weight: 16.0,
916                },
917                ItemCaseRarityWeight {
918                    rarity_id: uuid!("363dded9-e5a4-4bb9-bde6-63037d808246"),
919                    weight: 8.0,
920                },
921                ItemCaseRarityWeight {
922                    rarity_id: uuid!("9e1842cc-4192-4332-8c13-0f901ca442a4"),
923                    weight: 4.0,
924                },
925                ItemCaseRarityWeight {
926                    rarity_id: uuid!("d59e98d8-fc7d-4b8b-9da0-a374400df154"),
927                    weight: 2.0,
928                },
929                ItemCaseRarityWeight {
930                    rarity_id: uuid!("c0d049eb-359e-4b1e-9de2-eda276043ea8"),
931                    weight: 1.0,
932                },
933            ],
934            auto_chest_settings: AutoChestSettings {
935                max_batch_size: 3,
936                auto_only_max_batch_size: 1,
937            },
938        },
939        // Одна редкость: доска целиком под неё.
940        ItemCasesSettingsByLevel {
941            level: 4,
942            // Заведомо недостижимый порог: эти уровни существуют только ради
943            // форм раскладки Plinko и не должны менять доступность апгрейда
944            // сундука в остальных тестах.
945            required_chapter_level: 500,
946            upgrade_cost: vec![CurrencyUnit {
947                currency_id: uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
948                amount: 1_000_000,
949            }],
950            upgrade_time_secs: 600,
951            rarity_weights: vec![ItemCaseRarityWeight {
952                rarity_id: uuid!("8c8acba2-67d3-40e0-ac62-2f7acce64ccc"),
953                weight: 64.0,
954            }],
955            auto_chest_settings: AutoChestSettings {
956                max_batch_size: 3,
957                auto_only_max_batch_size: 1,
958            },
959        },
960        // Редкостей ровно столько же, сколько пар слотов, — полная симметрия.
961        ItemCasesSettingsByLevel {
962            level: 5,
963            // Заведомо недостижимый порог: эти уровни существуют только ради
964            // форм раскладки Plinko и не должны менять доступность апгрейда
965            // сундука в остальных тестах.
966            required_chapter_level: 500,
967            upgrade_cost: vec![CurrencyUnit {
968                currency_id: uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
969                amount: 1_000_000,
970            }],
971            upgrade_time_secs: 600,
972            rarity_weights: vec![
973                ItemCaseRarityWeight {
974                    rarity_id: uuid!("8c8acba2-67d3-40e0-ac62-2f7acce64ccc"),
975                    weight: 64.0,
976                },
977                ItemCaseRarityWeight {
978                    rarity_id: uuid!("272f3e73-6b3e-43a0-88d6-d34bdaf5aeae"),
979                    weight: 32.0,
980                },
981                ItemCaseRarityWeight {
982                    rarity_id: uuid!("c45eed5f-ad7e-4af0-b0a5-46b311573bf4"),
983                    weight: 16.0,
984                },
985                ItemCaseRarityWeight {
986                    rarity_id: uuid!("a9a0575f-9d09-4118-b305-14e927817302"),
987                    weight: 8.0,
988                },
989                ItemCaseRarityWeight {
990                    rarity_id: uuid!("19e2db05-5f17-4d89-9732-924134056776"),
991                    weight: 4.0,
992                },
993            ],
994            auto_chest_settings: AutoChestSettings {
995                max_batch_size: 3,
996                auto_only_max_batch_size: 1,
997            },
998        },
999        // Редкостей больше, чем пар слотов, — крайние слоты раздаются по одному.
1000        ItemCasesSettingsByLevel {
1001            level: 6,
1002            // Заведомо недостижимый порог: эти уровни существуют только ради
1003            // форм раскладки Plinko и не должны менять доступность апгрейда
1004            // сундука в остальных тестах.
1005            required_chapter_level: 500,
1006            upgrade_cost: vec![CurrencyUnit {
1007                currency_id: uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
1008                amount: 1_000_000,
1009            }],
1010            upgrade_time_secs: 600,
1011            rarity_weights: vec![
1012                ItemCaseRarityWeight {
1013                    rarity_id: uuid!("8c8acba2-67d3-40e0-ac62-2f7acce64ccc"),
1014                    weight: 64.0,
1015                },
1016                ItemCaseRarityWeight {
1017                    rarity_id: uuid!("272f3e73-6b3e-43a0-88d6-d34bdaf5aeae"),
1018                    weight: 32.0,
1019                },
1020                ItemCaseRarityWeight {
1021                    rarity_id: uuid!("c45eed5f-ad7e-4af0-b0a5-46b311573bf4"),
1022                    weight: 16.0,
1023                },
1024                ItemCaseRarityWeight {
1025                    rarity_id: uuid!("a9a0575f-9d09-4118-b305-14e927817302"),
1026                    weight: 8.0,
1027                },
1028                ItemCaseRarityWeight {
1029                    rarity_id: uuid!("19e2db05-5f17-4d89-9732-924134056776"),
1030                    weight: 4.0,
1031                },
1032                ItemCaseRarityWeight {
1033                    rarity_id: uuid!("9d662020-0929-439b-8afa-f286b72070a9"),
1034                    weight: 2.0,
1035                },
1036                ItemCaseRarityWeight {
1037                    rarity_id: uuid!("363dded9-e5a4-4bb9-bde6-63037d808246"),
1038                    weight: 1.0,
1039                },
1040            ],
1041            auto_chest_settings: AutoChestSettings {
1042                max_batch_size: 3,
1043                auto_only_max_batch_size: 1,
1044            },
1045        },
1046    ]
1047}
1048
1049fn generate_game_settings() -> GameSettings {
1050    GameSettings {
1051        power_estimator: crate::game_settings::PowerEstimatorSettings {
1052            reference_trigger_proc_rate: 0.75,
1053            trigger_only_q: 1.001,
1054            bridge_expected_fill: 0.5,
1055        },
1056        // BAL-035's schedule in miniature: two bands and a pity of 4, so the
1057        // fixture exercises both the band walk and the guarantee.
1058        mastery: crate::game_settings::MasterySettings {
1059            shard_currency_id: uuid!("6c050b07-5282-4a23-8bbf-dabecd27cade"),
1060            crystal_currency_id: uuid!("85d98a50-510e-4853-b00a-84a51ad5cb50"),
1061            shard_bands: vec![
1062                crate::game_settings::MasteryShardBand {
1063                    up_to_clear: 10,
1064                    shards: 10,
1065                },
1066                crate::game_settings::MasteryShardBand {
1067                    up_to_clear: 20,
1068                    shards: 25,
1069                },
1070            ],
1071            crystal_chance: 0.25,
1072            crystal_pity_clears: 4,
1073        },
1074        required_attributes: vec![
1075            uuid!("45eca0a7-7430-487b-bd65-b796c6d88c08"),
1076            uuid!("6ad485d7-9567-4b77-8ddc-a2418dd1dfe6"),
1077        ],
1078        resist_task_id: uuid!("05e915bf-55fe-4419-bcf0-5f90833f17bd"),
1079        hp_attribute_id: uuid!("45eca0a7-7430-487b-bd65-b796c6d88c08"),
1080        time_skip_ticket_currency_id: uuid!("22297520-abb8-45b9-9fbb-c418b8001246"),
1081        chest_upgrade_skip_ticket_skips_sec: 750,
1082        chest_upgrade_skip_currency_id: uuid!("6c050b07-5282-4a23-8bbf-dabecd27cade"),
1083        chest_upgrade_skip_currency_skips_sec: 400,
1084        ability_gacha: crate::game_settings::AbilityGachaSettings {
1085            currency_id: uuid!("22297520-abb8-45b9-9fbb-c418b8001246"),
1086            roll_price_in_diamonds: PositiveI64::new(2),
1087            small_roll_cost: PositiveI64::new(3),
1088            big_roll_cost: PositiveI64::new(10),
1089            big_roll_bonus_drops: 1,
1090            wishlist_slots: 5,
1091            wishlist_weight_multiplier: WeightMultiplier::new(2.0),
1092            slot_upgrade_currency_id: uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
1093            slot_max_level: PositiveI64::new(6),
1094            slot_level_costs: vec![
1095                PositiveI64::new(100),
1096                PositiveI64::new(200),
1097                PositiveI64::new(300),
1098                PositiveI64::new(400),
1099                PositiveI64::new(500),
1100                PositiveI64::new(600),
1101            ],
1102            slot_level_bonus_levels: vec![0, 1, 2, 3, 4, 5, 6],
1103        },
1104        default_loop_task_behavior: "test_default_loop_task".to_string(),
1105        item_case_currency_id: uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
1106        initial_class_id: uuid!("00000000-0000-0000-0000-000000000000"),
1107        chat_message_ttl_secs: 3600,
1108        baseline_speed: 10_000,
1109        currency_ids_to_show: vec![
1110            uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
1111            uuid!("22297520-abb8-45b9-9fbb-c418b8001246"),
1112            uuid!("6c050b07-5282-4a23-8bbf-dabecd27cade"),
1113        ],
1114        auto_chest_pause_duration_ticks: 20,
1115        auto_chest_per_ball_pause_ticks: 5,
1116        arena_fight_template_id: uuid!("360fc654-dea1-4f5d-97af-249ab30608ac"),
1117        vassal_fight_id: uuid!("b92c69a2-6544-4b83-809f-5af290b75473"),
1118        fight_progress_tick: 1000,
1119        db_power_update_frequency: 2,
1120        deleted_user_username: "Deleted".to_owned(),
1121        diamond_currency_id: uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
1122        username_change_cooldown_sec: 604800,
1123        username_min_length: 3,
1124        username_max_length: 32,
1125        dungeons_keys_reset_amount: 2,
1126        daily_currency_resets: vec![
1127            DailyCurrencyReset {
1128                currency_id: uuid!("6c050b07-5282-4a23-8bbf-dabecd27cade"),
1129                reset_amount: 5,
1130            },
1131            // Regression fixture for the day-1 spurious-refill bug. Unlike the
1132            // arena-ticket entry above (whose reset row is seeded at character
1133            // creation), this currency is intentionally NOT granted/seeded at
1134            // creation, so a fresh character holds 0 of it with no reset
1135            // record — the exact shape production dungeon keys have. A fresh
1136            // character must NOT have it refilled on its creation day. Test-only
1137            // id; referenced solely by test_daily_currency_reset_* tests.
1138            DailyCurrencyReset {
1139                currency_id: uuid!("0da11000-0000-0000-0000-000000000001"),
1140                reset_amount: 3,
1141            },
1142        ],
1143        pet_gacha: crate::game_settings::PetGachaSettings {
1144            currency_id: uuid!("22297520-abb8-45b9-9fbb-c418b8001246"),
1145            roll_price_in_diamonds: PositiveI64::new(2),
1146            small_roll_cost: PositiveI64::new(3),
1147            big_roll_cost: PositiveI64::new(10),
1148            big_roll_bonus_drops: 1,
1149            wishlist_slots: 5,
1150            wishlist_weight_multiplier: WeightMultiplier::new(2.0),
1151            first_copy_pity_pulls: 30,
1152        },
1153        default_shop_tab: ShopTab::DailyDeals,
1154        rate_us_chapter: None,
1155        boss_reward_chapter_growth: None,
1156        boss_reward_max_multiplier: None,
1157        sell_price_exp: None,
1158        sell_price_coef: None,
1159    }
1160}
1161
1162fn generate_ads_settings() -> AdsSettings {
1163    AdsSettings {
1164        chest_upgrade_speedup: AdSpeedupConfig {
1165            speedup_seconds: 300,
1166            cooldown_sec: 5,
1167            daily_limit: 3,
1168        },
1169        talent_upgrade_speedup: AdSpeedupConfig {
1170            speedup_seconds: 300,
1171            cooldown_sec: 5,
1172            daily_limit: 3,
1173        },
1174        free_ability_case_cooldown_sec: 86400,
1175        skill_gacha_ad_daily_limit: 3,
1176        skill_gacha_ad_rolls: 3,
1177        pet_gacha_ad_daily_limit: 3,
1178        pet_gacha_ad_rolls: 3,
1179        afk_boost_ad_daily_limit: 1,
1180        afk_boost_multiplier: 1.5,
1181        // Two views a day, as shipped: the second one extends the first
1182        // (BAL-016), so the fixture has to allow both.
1183        daily_booster_ad_daily_limit: 2,
1184        daily_booster_ad_buff_template_id: uuid!("b2c3d4e5-f6a7-8901-bcde-f12345678901"),
1185        daily_booster_ad_buff_duration: 3600,
1186        arena_refresh_ad_daily_limit: 3,
1187        dungeon_raid_ad_daily_limit: 2,
1188        bird_ad: BirdAdConfig {
1189            daily_limit: 5,
1190            cooldown_sec: 60,
1191            post_show_cooldown_sec: 10,
1192            flight_duration_sec: 8,
1193            variants: vec![
1194                BirdAdVariantWeight {
1195                    variant_id: uuid!("d1e2f3a4-b5c6-7890-abcd-ef1234567891"),
1196                    weight: 70,
1197                },
1198                BirdAdVariantWeight {
1199                    variant_id: uuid!("d1e2f3a4-b5c6-7890-abcd-ef1234567892"),
1200                    weight: 30,
1201                },
1202            ],
1203        },
1204    }
1205}
1206
1207/// Тестовая доска Plinko: 9 рядов колышков, 10 физических слотов, раскладка
1208/// на каждый уровень сундука. Слотов именно 10, потому что на 3-м уровне
1209/// тестового сундука выпадают все 10 редкостей, а раскладка уровня обязана
1210/// покрывать каждую выпадающую там редкость (`GameConfig::validate`).
1211///
1212/// Раскладки собраны по тому же принципу, что и боевой конфиг: чем ниже тир
1213/// редкости, тем ближе к центру доски её слоты, — тир не убывает от центра к
1214/// краям.
1215/// Каждый колышек что-то даёт, чтобы любой путь был наблюдаемым; потолки
1216/// намеренно низкие, чтобы упор проверялся за несколько открытий.
1217/// Тексты уже переведённые (`Translated`), чтобы тесты не требовали файлов
1218/// локализации. Ступени возврата укорочены относительно боевого конфига —
1219/// тестам важен сам порядок, а не продуктовые интервалы.
1220fn generate_local_notifications_settings() -> LocalNotificationsSettings {
1221    let text = |title: &str, body: &str| LocalNotificationText {
1222        title: i18n::I18nString::Translated(title.to_string()),
1223        body: i18n::I18nString::Translated(body.to_string()),
1224    };
1225
1226    LocalNotificationsSettings {
1227        enabled: true,
1228        win_back: vec![
1229            WinBackNotification {
1230                delay_minutes: NonZeroU64::new(60),
1231                text: text("Come back", "Your kingdom misses you"),
1232            },
1233            WinBackNotification {
1234                delay_minutes: NonZeroU64::new(1440),
1235                text: text("Still waiting", "Your kingdom misses you"),
1236            },
1237        ],
1238        afk_rewards_capped: text("Rewards are full", "Collect your offline rewards"),
1239        item_case_upgrade_finished: text("Plinko ready", "Your chest upgrade is done"),
1240        talent_upgrade_finished: text("Talent ready", "Your talent upgrade is done"),
1241        daily_reset: text("Daily reset", "New daily tasks are available"),
1242        boost_expiring: text("Boost ending", "Your boost expires soon"),
1243        boost_expiring_lead_minutes: NonZeroU64::new(60),
1244        quiet_hours_start: 23,
1245        quiet_hours_end: 9,
1246    }
1247}
1248
1249fn generate_plinko_settings() -> PlinkoSettings {
1250    const ROWS: i64 = 9;
1251    const HP: uuid::Uuid = uuid!("45eca0a7-7430-487b-bd65-b796c6d88c08");
1252    const DAMAGE: uuid::Uuid = uuid!("6ad485d7-9567-4b77-8ddc-a2418dd1dfe6");
1253    const ARMOR: uuid::Uuid = uuid!("f82eb3fa-8a09-44fb-b8ed-979cc2db28f4");
1254
1255    // Редкости тестового конфига по возрастанию order: чем дальше по списку,
1256    // тем выше тир.
1257    let rarities = [
1258        uuid!("8c8acba2-67d3-40e0-ac62-2f7acce64ccc"),
1259        uuid!("272f3e73-6b3e-43a0-88d6-d34bdaf5aeae"),
1260        uuid!("c45eed5f-ad7e-4af0-b0a5-46b311573bf4"),
1261        uuid!("a9a0575f-9d09-4118-b305-14e927817302"),
1262        uuid!("19e2db05-5f17-4d89-9732-924134056776"),
1263        uuid!("9d662020-0929-439b-8afa-f286b72070a9"),
1264        uuid!("363dded9-e5a4-4bb9-bde6-63037d808246"),
1265        uuid!("9e1842cc-4192-4332-8c13-0f901ca442a4"),
1266        uuid!("d59e98d8-fc7d-4b8b-9da0-a374400df154"),
1267        uuid!("c0d049eb-359e-4b1e-9de2-eda276043ea8"),
1268    ];
1269
1270    // Слоты 0..=ROWS, подписанные редкостью; соседние одинаковые сливаются в
1271    // одну группу, поэтому раскладку удобно писать по слотам.
1272    let layout = |chest_level: i64, slots: [uuid::Uuid; (ROWS + 1) as usize]| {
1273        let mut groups: Vec<PlinkoSlotGroup> = Vec::new();
1274        for (slot, rarity_id) in slots.into_iter().enumerate() {
1275            match groups.last_mut() {
1276                Some(last) if last.rarity_id == rarity_id => last.to_slot = slot as i64,
1277                _ => groups.push(PlinkoSlotGroup {
1278                    from_slot: slot as i64,
1279                    to_slot: slot as i64,
1280                    rarity_id,
1281                    is_locked: false,
1282                }),
1283            }
1284        }
1285        PlinkoLevelSlotLayout {
1286            chest_level,
1287            slot_groups: NonEmptyVec::new(groups),
1288        }
1289    };
1290
1291    // Уровни 1 и 2 роняют 8 редкостей из 10: два самых низких тира получают
1292    // по паре слотов в центре, остальные — по одному слоту от центра к краям.
1293    let eight_rarities = [
1294        rarities[6],
1295        rarities[4],
1296        rarities[2],
1297        rarities[1],
1298        rarities[0],
1299        rarities[0],
1300        rarities[1],
1301        rarities[3],
1302        rarities[5],
1303        rarities[7],
1304    ];
1305    // Уровень 4 роняет одну редкость: доска целиком под неё.
1306    let one_rarity = [rarities[0]; (ROWS + 1) as usize];
1307    // Уровень 5 роняет 5 редкостей — ровно столько же, сколько пар слотов:
1308    // каждая занимает пару, доска полностью симметрична.
1309    let five_rarities = [
1310        rarities[4],
1311        rarities[3],
1312        rarities[2],
1313        rarities[1],
1314        rarities[0],
1315        rarities[0],
1316        rarities[1],
1317        rarities[2],
1318        rarities[3],
1319        rarities[4],
1320    ];
1321    // Уровень 6 роняет 7 редкостей — больше, чем пар слотов: три самых низких
1322    // тира держат пары в центре, остальные четыре получают по одному крайнему
1323    // слоту, и самый высокий тир достаётся последнему слоту.
1324    let seven_rarities = [
1325        rarities[5],
1326        rarities[3],
1327        rarities[2],
1328        rarities[1],
1329        rarities[0],
1330        rarities[0],
1331        rarities[1],
1332        rarities[2],
1333        rarities[4],
1334        rarities[6],
1335    ];
1336    // На 3-м уровне выпадают все 10 редкостей — по слоту на каждую, от
1337    // низшего тира в центре к высшему по краям.
1338    let ten_rarities = [
1339        rarities[8],
1340        rarities[6],
1341        rarities[4],
1342        rarities[2],
1343        rarities[0],
1344        rarities[1],
1345        rarities[3],
1346        rarities[5],
1347        rarities[7],
1348        rarities[9],
1349    ];
1350
1351    let mut pin_bonuses = Vec::new();
1352    for row in 0..ROWS {
1353        for column in 0..=row {
1354            let attribute_id = match (row + column) % 3 {
1355                0 => HP,
1356                1 => DAMAGE,
1357                _ => ARMOR,
1358            };
1359            pin_bonuses.push(PlinkoPinBonus {
1360                row,
1361                column,
1362                attribute_id,
1363                value: row + 1,
1364            });
1365        }
1366    }
1367
1368    PlinkoSettings {
1369        rows: ROWS,
1370        slot_layouts: NonEmptyVec::new(vec![
1371            layout(1, eight_rarities),
1372            layout(2, eight_rarities),
1373            layout(3, ten_rarities),
1374            layout(4, one_rarity),
1375            layout(5, five_rarities),
1376            layout(6, seven_rarities),
1377        ]),
1378        pin_bonuses,
1379        // One tranche per chest level of the fixture. The caps grow with the
1380        // level and the scale is a whole stat point per unit of weight, so the
1381        // fixture's arithmetic stays readable.
1382        tranches: (1..=6)
1383            .map(|chest_level| PlinkoTranche {
1384                chest_level,
1385                axes: [(HP, 40), (DAMAGE, 20), (ARMOR, 20)]
1386                    .into_iter()
1387                    .map(|(attribute_id, cap)| PlinkoAxisTranche {
1388                        attribute_id,
1389                        cap_micro: cap * chest_level * crate::plinko::PLINKO_MICRO,
1390                        scale_micro: crate::plinko::PLINKO_MICRO,
1391                    })
1392                    .collect(),
1393            })
1394            .collect(),
1395    }
1396}
1397
1398fn generate_item_rarities() -> Vec<ItemRarity> {
1399    vec![
1400        ItemRarity {
1401            id: uuid!("8c8acba2-67d3-40e0-ac62-2f7acce64ccc"),
1402            code: 1,
1403            name: i18n::I18nString::new(
1404                "content.item_rarities.8c8acba2-67d3-40e0-ac62-2f7acce64ccc:name",
1405            ),
1406            rarity_icon: "2b51dc17-022f-404d-a31b-6a7fe1d8603a.png".to_string(),
1407            rarity_icon_path: "2b51dc17-022f-404d-a31b-6a7fe1d8603a.png".to_string(),
1408            text_color: "#E9E9D4".to_string(),
1409            ribbon_icon: "".to_string(),
1410            ribbon_icon_path: "".to_string(),
1411            order: 1,
1412            q: 1,
1413            sell_q: 1,
1414        },
1415        ItemRarity {
1416            id: uuid!("272f3e73-6b3e-43a0-88d6-d34bdaf5aeae"),
1417            code: 2,
1418            name: i18n::I18nString::new(
1419                "content.item_rarities.272f3e73-6b3e-43a0-88d6-d34bdaf5aeae:name",
1420            ),
1421            rarity_icon: "df3529d5-9a9a-4399-9f16-34ea61b66c93.png".to_string(),
1422            rarity_icon_path: "df3529d5-9a9a-4399-9f16-34ea61b66c93.png".to_string(),
1423            text_color: "#36DB89".to_string(),
1424            ribbon_icon: "".to_string(),
1425            ribbon_icon_path: "".to_string(),
1426            order: 2,
1427            q: 1,
1428            sell_q: 1,
1429        },
1430        ItemRarity {
1431            id: uuid!("c45eed5f-ad7e-4af0-b0a5-46b311573bf4"),
1432            code: 3,
1433            name: i18n::I18nString::new(
1434                "content.item_rarities.c45eed5f-ad7e-4af0-b0a5-46b311573bf4:name",
1435            ),
1436            rarity_icon: "28d57678-8db0-4870-9791-1899b7228f96.png".to_string(),
1437            rarity_icon_path: "28d57678-8db0-4870-9791-1899b7228f96.png".to_string(),
1438            text_color: "#399AE1".to_string(),
1439            ribbon_icon: "".to_string(),
1440            ribbon_icon_path: "".to_string(),
1441            order: 3,
1442            q: 1,
1443            sell_q: 1,
1444        },
1445        ItemRarity {
1446            id: uuid!("a9a0575f-9d09-4118-b305-14e927817302"),
1447            code: 4,
1448            name: i18n::I18nString::new(
1449                "content.item_rarities.a9a0575f-9d09-4118-b305-14e927817302:name",
1450            ),
1451            rarity_icon: "b08a1167-c58d-42a4-8230-576c3b69063c.png".to_string(),
1452            rarity_icon_path: "b08a1167-c58d-42a4-8230-576c3b69063c.png".to_string(),
1453            text_color: "#CC34E3".to_string(),
1454            ribbon_icon: "".to_string(),
1455            ribbon_icon_path: "".to_string(),
1456            order: 4,
1457            q: 1,
1458            sell_q: 1,
1459        },
1460        ItemRarity {
1461            id: uuid!("19e2db05-5f17-4d89-9732-924134056776"),
1462            code: 5,
1463            name: i18n::I18nString::new(
1464                "content.item_rarities.19e2db05-5f17-4d89-9732-924134056776:name",
1465            ),
1466            rarity_icon: "1edfd7ed-8945-4c95-8bd5-40ba9422d7c2.png".to_string(),
1467            rarity_icon_path: "1edfd7ed-8945-4c95-8bd5-40ba9422d7c2.png".to_string(),
1468            text_color: "#FDCA5B".to_string(),
1469            ribbon_icon: "".to_string(),
1470            ribbon_icon_path: "".to_string(),
1471            order: 5,
1472            q: 1,
1473            sell_q: 1,
1474        },
1475        ItemRarity {
1476            id: uuid!("9d662020-0929-439b-8afa-f286b72070a9"),
1477            code: 6,
1478            name: i18n::I18nString::new(
1479                "content.item_rarities.9d662020-0929-439b-8afa-f286b72070a9:name",
1480            ),
1481            rarity_icon: "60b73aa0-c1dd-4bac-9efe-ebc25030dbef.png".to_string(),
1482            rarity_icon_path: "60b73aa0-c1dd-4bac-9efe-ebc25030dbef.png".to_string(),
1483            text_color: "#F78B46".to_string(),
1484            ribbon_icon: "".to_string(),
1485            ribbon_icon_path: "".to_string(),
1486            order: 6,
1487            q: 1,
1488            sell_q: 1,
1489        },
1490        ItemRarity {
1491            id: uuid!("363dded9-e5a4-4bb9-bde6-63037d808246"),
1492            code: 7,
1493            name: i18n::I18nString::new(
1494                "content.item_rarities.363dded9-e5a4-4bb9-bde6-63037d808246:name",
1495            ),
1496            rarity_icon: "59126b12-2d5c-45f4-8f58-1283f6ba9ccd.png".to_string(),
1497            rarity_icon_path: "59126b12-2d5c-45f4-8f58-1283f6ba9ccd.png".to_string(),
1498            text_color: "#ED581D".to_string(),
1499            ribbon_icon: "".to_string(),
1500            ribbon_icon_path: "".to_string(),
1501            order: 7,
1502            q: 1,
1503            sell_q: 1,
1504        },
1505        ItemRarity {
1506            id: uuid!("9e1842cc-4192-4332-8c13-0f901ca442a4"),
1507            code: 8,
1508            name: i18n::I18nString::new(
1509                "content.item_rarities.9e1842cc-4192-4332-8c13-0f901ca442a4:name",
1510            ),
1511            rarity_icon: "5dbec395-33dd-43da-b35a-8d1262409c9d.png".to_string(),
1512            rarity_icon_path: "5dbec395-33dd-43da-b35a-8d1262409c9d.png".to_string(),
1513            text_color: "#FA91FF".to_string(),
1514            ribbon_icon: "".to_string(),
1515            ribbon_icon_path: "".to_string(),
1516            order: 8,
1517            q: 1,
1518            sell_q: 1,
1519        },
1520        ItemRarity {
1521            id: uuid!("d59e98d8-fc7d-4b8b-9da0-a374400df154"),
1522            code: 9,
1523            name: i18n::I18nString::new(
1524                "content.item_rarities.d59e98d8-fc7d-4b8b-9da0-a374400df154:name",
1525            ),
1526            rarity_icon: "0d0a6bfa-d2db-40bc-88c9-3bce4472e376.png".to_string(),
1527            rarity_icon_path: "0d0a6bfa-d2db-40bc-88c9-3bce4472e376.png".to_string(),
1528            text_color: "#5FECF3".to_string(),
1529            ribbon_icon: "".to_string(),
1530            ribbon_icon_path: "".to_string(),
1531            order: 9,
1532            q: 1,
1533            sell_q: 1,
1534        },
1535        ItemRarity {
1536            id: uuid!("c0d049eb-359e-4b1e-9de2-eda276043ea8"),
1537            code: 10,
1538            name: i18n::I18nString::new(
1539                "content.item_rarities.c0d049eb-359e-4b1e-9de2-eda276043ea8:name",
1540            ),
1541            rarity_icon: "b4e58214-77c1-45fa-a1ec-260c1e76b5cd.png".to_string(),
1542            rarity_icon_path: "b4e58214-77c1-45fa-a1ec-260c1e76b5cd.png".to_string(),
1543            text_color: "#E73636".to_string(),
1544            ribbon_icon: "".to_string(),
1545            ribbon_icon_path: "".to_string(),
1546            order: 10,
1547            q: 1,
1548            sell_q: 1,
1549        },
1550    ]
1551}
1552
1553fn generate_portal_rarities(item_rarities: &[ItemRarity]) -> Vec<PortalRarity> {
1554    item_rarities
1555        .iter()
1556        .enumerate()
1557        .map(|(index, item_rarity)| PortalRarity {
1558            id: Uuid::from_u128(0x0199_0000_0000_7000_8000_0000_0000_0001 + index as u128),
1559            item_rarity_id: item_rarity.id,
1560            icon_path: format!("/portal_rarity_{}.png", index + 1),
1561        })
1562        .collect()
1563}
1564
1565fn generate_effects() -> Vec<Effect> {
1566    vec![
1567        Effect {
1568            id: uuid!("ccc47912-61c0-4efa-88f9-7911fa1b074f"),
1569            name: i18n::I18nString::new(
1570                "content.effects.ccc47912-61c0-4efa-88f9-7911fa1b074f:name",
1571            ),
1572            behavior: Some("bloodleak_tick".to_string()),
1573            icon_url: "18789966-dfbb-445a-8ec1-c55397604203.webp".to_string(),
1574            interval_ticks: Some(5000),
1575            required_attributes: Some(vec!["bloodleak".to_string()]),
1576            events_subscribe: None,
1577            positive: false,
1578            code: "".into(),
1579            icon_path: "".into(),
1580            vfx_path: "".into(),
1581            duration: None,
1582        },
1583        Effect {
1584            id: uuid!("3b136901-137c-47cc-8c7e-bc0ce387eb1c"),
1585            name: i18n::I18nString::new(
1586                "content.effects.3b136901-137c-47cc-8c7e-bc0ce387eb1c:name",
1587            ),
1588            behavior: Some("low_hp_heal_on_damage".to_string()),
1589            icon_url: "8e991eae-e001-4a76-8238-277c38b35889.webp".to_string(),
1590            interval_ticks: None,
1591            required_attributes: None,
1592            events_subscribe: Some(vec!["Damage".to_string()]),
1593            positive: true,
1594            code: "".into(),
1595            icon_path: "".into(),
1596            vfx_path: "".into(),
1597            duration: None,
1598        },
1599        Effect {
1600            id: uuid!("39f135d2-b930-42a7-abfa-f1ec49f3cc00"),
1601            name: i18n::I18nString::new(
1602                "content.effects.3b136901-137c-47cc-8c7e-bc0ce387eb1c:name",
1603            ),
1604            behavior: Some("spawn_two_on_death".to_string()),
1605            icon_url: "8e991eae-e001-4a76-8238-277c38b35889.webp".to_string(),
1606            interval_ticks: None,
1607            required_attributes: None,
1608            events_subscribe: Some(vec!["EntityDeath".to_string()]),
1609            positive: true,
1610            code: "".into(),
1611            icon_path: "".into(),
1612            vfx_path: "".into(),
1613            duration: None,
1614        },
1615    ]
1616}
1617
1618fn generate_abilities() -> Vec<AbilityTemplate> {
1619    vec![
1620        AbilityTemplate {
1621            id: uuid!("da6c582b-7364-40bd-9b2d-946d8e20eaac"),
1622            is_fight_ui_visible: false,
1623            fight_ui_visibility: AbilityFightUiVisibility::Hidden,
1624            name: i18n::I18nString::new(
1625                "content.abilities.da6c582b-7364-40bd-9b2d-946d8e20eaac:name",
1626            ),
1627            start_behavior: Some("attack_first_enemy".to_string()),
1628            behavior: Some("ability_damage_target_5".to_string()),
1629            icon_url: "18789966-dfbb-445a-8ec1-c55397604203.webp".to_string(),
1630            icon_path: "18789966-dfbb-445a-8ec1-c55397604203.webp".to_string(),
1631            cooldown: 10000,
1632            mana_cost: 15,
1633            is_gacha_ability: false,
1634            available_to_bots: true,
1635            rarity_id: uuid!("33a0af72-2390-4df9-bd4f-f66b27ed7792"),
1636            description: i18n::I18nString::new(
1637                "content.abilities.da6c582b-7364-40bd-9b2d-946d8e20eaac:description",
1638            ),
1639            description_values_script: Some("Result.push(1); Result.push(100)".to_string()),
1640            vfx_object_path: "".into(),
1641            cast_type: AbilityCastType::default(),
1642            target_type: AbilityTargetType::default(),
1643            range: 1,
1644            max_targets: None,
1645            tags: vec![AbilityTag::Damage, AbilityTag::Targeted],
1646        },
1647        AbilityTemplate {
1648            id: uuid!("7e584338-057d-41a2-a80e-8e2c76e98f6a"),
1649            is_fight_ui_visible: true,
1650            fight_ui_visibility: AbilityFightUiVisibility::Slotted,
1651            name: i18n::I18nString::new(
1652                "content.abilities.7e584338-057d-41a2-a80e-8e2c76e98f6a:name",
1653            ),
1654            start_behavior: Some("attack_first_enemy".to_string()),
1655            behavior: Some("ability_damage_target_5".to_string()),
1656            icon_url: "8e991eae-e001-4a76-8238-277c38b35889.webp".to_string(),
1657            icon_path: "8e991eae-e001-4a76-8238-277c38b35889.webp".to_string(),
1658            cooldown: 30000,
1659            mana_cost: 45,
1660            is_gacha_ability: true,
1661            available_to_bots: true,
1662            rarity_id: uuid!("81b082cc-2736-4545-87d1-301f5de8951a"),
1663            description: i18n::I18nString::new(
1664                "content.abilities.7e584338-057d-41a2-a80e-8e2c76e98f6a:description",
1665            ),
1666            description_values_script: Some(
1667                "Result.push(2); Result.push(200); Result.push(20000)".to_string(),
1668            ),
1669            vfx_object_path: "".into(),
1670            cast_type: AbilityCastType::default(),
1671            target_type: AbilityTargetType::default(),
1672            range: 1,
1673            max_targets: None,
1674            tags: vec![
1675                AbilityTag::Damage,
1676                AbilityTag::Targeted,
1677                AbilityTag::Projectile,
1678            ],
1679        },
1680        AbilityTemplate {
1681            id: uuid!("487a23e5-7401-4c34-a6c6-2401f95249cf"),
1682            is_fight_ui_visible: true,
1683            fight_ui_visibility: AbilityFightUiVisibility::Slotted,
1684            name: i18n::I18nString::new(
1685                "content.abilities.487a23e5-7401-4c34-a6c6-2401f95249cf:name",
1686            ),
1687            start_behavior: Some("attack_first_enemy".to_string()),
1688            behavior: Some("ability_damage_target_5".to_string()),
1689            icon_url: "2cd222a0-1a1a-42d2-b553-7d3754b08e0d.webp".to_string(),
1690            icon_path: "2cd222a0-1a1a-42d2-b553-7d3754b08e0d.webp".to_string(),
1691            cooldown: 15000,
1692            mana_cost: 23,
1693            is_gacha_ability: true,
1694            available_to_bots: true,
1695            rarity_id: uuid!("6f0f1099-8ab1-4378-aea5-ac6bcbe6ec3e"),
1696            description: i18n::I18nString::new(
1697                "content.abilities.487a23e5-7401-4c34-a6c6-2401f95249cf:description",
1698            ),
1699            description_values_script: Some("Result.push(3);".to_string()),
1700            vfx_object_path: "".into(),
1701            cast_type: AbilityCastType::default(),
1702            target_type: AbilityTargetType::default(),
1703            range: 1,
1704            max_targets: None,
1705            tags: vec![AbilityTag::Damage, AbilityTag::Targeted],
1706        },
1707        AbilityTemplate {
1708            id: uuid!("fd053ec6-8b23-4ba1-a31f-31b56d8c57bc"),
1709            is_fight_ui_visible: true,
1710            fight_ui_visibility: AbilityFightUiVisibility::Slotted,
1711            name: i18n::I18nString::new(
1712                "content.abilities.fd053ec6-8b23-4ba1-a31f-31b56d8c57bc:name",
1713            ),
1714            start_behavior: Some("attack_first_enemy".to_string()),
1715            behavior: Some("ability_damage_target_5".to_string()),
1716            icon_url: "18789966-dfbb-445a-8ec1-c55397604203.webp".to_string(),
1717            icon_path: "18789966-dfbb-445a-8ec1-c55397604203.webp".to_string(),
1718            cooldown: 10000,
1719            mana_cost: 15,
1720            is_gacha_ability: true,
1721            available_to_bots: true,
1722            rarity_id: uuid!("33a0af72-2390-4df9-bd4f-f66b27ed7792"),
1723            description: i18n::I18nString::new(
1724                "content.abilities.fd053ec6-8b23-4ba1-a31f-31b56d8c57bc:description",
1725            ),
1726            description_values_script: None,
1727            vfx_object_path: "".into(),
1728            cast_type: AbilityCastType::default(),
1729            target_type: AbilityTargetType::default(),
1730            range: 1,
1731            max_targets: None,
1732            tags: vec![AbilityTag::Damage, AbilityTag::Targeted],
1733        },
1734        AbilityTemplate {
1735            id: uuid!("50260b1c-7748-49bf-a1ed-34d69d029697"),
1736            is_fight_ui_visible: true,
1737            fight_ui_visibility: AbilityFightUiVisibility::Slotted,
1738            name: i18n::I18nString::new(
1739                "content.abilities.50260b1c-7748-49bf-a1ed-34d69d029697:name",
1740            ),
1741            start_behavior: Some("attack_first_enemy".to_string()),
1742            behavior: Some("ability_apply_bloodleak_effect".to_string()),
1743            icon_url: "18789966-dfbb-445a-8ec1-c55397604203.webp".to_string(),
1744            icon_path: "18789966-dfbb-445a-8ec1-c55397604203.webp".to_string(),
1745            cooldown: 10000,
1746            mana_cost: 15,
1747            is_gacha_ability: false,
1748            available_to_bots: true,
1749            rarity_id: uuid!("33a0af72-2390-4df9-bd4f-f66b27ed7792"),
1750            description: i18n::I18nString::new(
1751                "content.abilities.50260b1c-7748-49bf-a1ed-34d69d029697:description",
1752            ),
1753            description_values_script: Some("Result.push(5); Result.push(500)".to_string()),
1754            vfx_object_path: "".into(),
1755            cast_type: AbilityCastType::default(),
1756            target_type: AbilityTargetType::default(),
1757            range: 1,
1758            max_targets: None,
1759            tags: vec![
1760                AbilityTag::Debuff,
1761                AbilityTag::Duration,
1762                AbilityTag::Targeted,
1763            ],
1764        },
1765        AbilityTemplate {
1766            id: uuid!("41ee5532-a293-4f88-bfb6-5ffca1e57acc"),
1767            is_fight_ui_visible: true,
1768            fight_ui_visibility: AbilityFightUiVisibility::Slotted,
1769            name: i18n::I18nString::new(
1770                "content.abilities.41ee5532-a293-4f88-bfb6-5ffca1e57acc:name",
1771            ),
1772            start_behavior: Some("self_attack_500".to_string()),
1773            behavior: Some("ability_apply_low_hp_heal_effect".to_string()),
1774            icon_url: "18789966-dfbb-445a-8ec1-c55397604203.webp".to_string(),
1775            icon_path: "18789966-dfbb-445a-8ec1-c55397604203.webp".to_string(),
1776            cooldown: 10000,
1777            mana_cost: 15,
1778            is_gacha_ability: false,
1779            available_to_bots: true,
1780            rarity_id: uuid!("81b082cc-2736-4545-87d1-301f5de8951a"),
1781            description: i18n::I18nString::new(
1782                "content.abilities.41ee5532-a293-4f88-bfb6-5ffca1e57acc:description",
1783            ),
1784            description_values_script: Some("Result.push(6); Result.push(600)".to_string()),
1785            vfx_object_path: "".into(),
1786            cast_type: AbilityCastType::default(),
1787            target_type: AbilityTargetType::default(),
1788            range: 1,
1789            max_targets: None,
1790            // The fixture's healing ability. `RL-06` / `FL-06` fire on
1791            // "an original Skill with the Heal tag resolved", so the tag has to
1792            // sit on real content for those two to be testable at all.
1793            tags: vec![AbilityTag::Heal, AbilityTag::SelfCast],
1794        },
1795        AbilityTemplate {
1796            id: uuid!("9f8e7d6c-5b4a-3c2d-1e0f-9a8b7c6d5e4f"),
1797            is_fight_ui_visible: true,
1798            fight_ui_visibility: AbilityFightUiVisibility::Slotted,
1799            name: i18n::I18nString::new(
1800                "content.abilities.9f8e7d6c-5b4a-3c2d-1e0f-9a8b7c6d5e4f:name",
1801            ),
1802            start_behavior: Some("attack_first_enemy".to_string()),
1803            behavior: Some("ability_damage_target_5".to_string()),
1804            icon_url: "18789966-dfbb-445a-8ec1-c55397604203.webp".to_string(),
1805            icon_path: "18789966-dfbb-445a-8ec1-c55397604203.webp".to_string(),
1806            cooldown: 0,
1807            mana_cost: 0,
1808            is_gacha_ability: true,
1809            available_to_bots: true,
1810            rarity_id: uuid!("33a0af72-2390-4df9-bd4f-f66b27ed7792"),
1811            description: i18n::I18nString::new(
1812                "content.abilities.9f8e7d6c-5b4a-3c2d-1e0f-9a8b7c6d5e4f:description",
1813            ),
1814            description_values_script: Some("Result.push(7); Result.push(700)".to_string()),
1815            vfx_object_path: "".into(),
1816            cast_type: AbilityCastType::default(),
1817            target_type: AbilityTargetType::default(),
1818            range: 1,
1819            max_targets: None,
1820            tags: vec![AbilityTag::Damage, AbilityTag::Targeted],
1821        },
1822        AbilityTemplate {
1823            id: uuid!("59e317c9-0108-4eb0-a41f-9d513b821542"),
1824            is_fight_ui_visible: true,
1825            fight_ui_visibility: AbilityFightUiVisibility::Class,
1826            name: i18n::I18nString::new(
1827                "content.abilities.59e317c9-0108-4eb0-a41f-9d513b821542:name",
1828            ),
1829            start_behavior: Some("attack_first_enemy".to_string()),
1830            behavior: Some("ability_damage_target_5".to_string()),
1831            icon_url: "18789966-dfbb-445a-8ec1-c55397604203.webp".to_string(),
1832            icon_path: "18789966-dfbb-445a-8ec1-c55397604203.webp".to_string(),
1833            cooldown: 10000000000000,
1834            mana_cost: 45,
1835            is_gacha_ability: false,
1836            available_to_bots: true,
1837            rarity_id: uuid!("33a0af72-2390-4df9-bd4f-f66b27ed7792"),
1838            description: i18n::I18nString::new(
1839                "content.abilities.59e317c9-0108-4eb0-a41f-9d513b821542:description",
1840            ),
1841            description_values_script: Some("Result.push(1); Result.push(100)".to_string()),
1842            vfx_object_path: "".into(),
1843            cast_type: AbilityCastType::default(),
1844            target_type: AbilityTargetType::default(),
1845            range: 1,
1846            max_targets: None,
1847            tags: vec![AbilityTag::Damage, AbilityTag::Targeted],
1848        },
1849        AbilityTemplate {
1850            id: uuid!("c9c291ea-8a10-4bab-99df-ff0dc40e66f6"),
1851            is_fight_ui_visible: true,
1852            fight_ui_visibility: AbilityFightUiVisibility::Slotted,
1853            name: i18n::I18nString::new(
1854                "content.abilities.fd053ec6-8b23-4ba1-a31f-31b56d8c57bc:name",
1855            ),
1856            start_behavior: Some("attack_first_enemy".to_string()),
1857            behavior: Some("ability_damage_target_5".to_string()),
1858            icon_url: "18789966-dfbb-445a-8ec1-c55397604203.webp".to_string(),
1859            icon_path: "18789966-dfbb-445a-8ec1-c55397604203.webp".to_string(),
1860            cooldown: 0,
1861            mana_cost: 0,
1862            is_gacha_ability: true,
1863            available_to_bots: true,
1864            rarity_id: uuid!("33a0af72-2390-4df9-bd4f-f66b27ed7792"),
1865            description: i18n::I18nString::new(
1866                "content.abilities.fd053ec6-8b23-4ba1-a31f-31b56d8c57bc:description",
1867            ),
1868            description_values_script: None,
1869            vfx_object_path: "".into(),
1870            cast_type: AbilityCastType::default(),
1871            target_type: AbilityTargetType::default(),
1872            range: 1,
1873            max_targets: None,
1874            tags: vec![AbilityTag::Damage, AbilityTag::Targeted],
1875        },
1876        // BASIC_ABILITY referenced by the native `default_opponent_generation`
1877        // (every generated bot is granted this ability) and by the bot class
1878        // below. Id must match the `BASIC_ABILITY` const in
1879        // `behaviors/opponent_generation.rs`. Hidden (non-slotted) basic.
1880        AbilityTemplate {
1881            id: uuid!("0194d64e-20f2-75e5-89c8-4cb812672485"),
1882            is_fight_ui_visible: false,
1883            fight_ui_visibility: AbilityFightUiVisibility::Hidden,
1884            name: i18n::I18nString::new(
1885                "content.abilities.da6c582b-7364-40bd-9b2d-946d8e20eaac:name",
1886            ),
1887            start_behavior: Some("attack_first_enemy".to_string()),
1888            behavior: Some("ability_damage_target_5".to_string()),
1889            icon_url: "18789966-dfbb-445a-8ec1-c55397604203.webp".to_string(),
1890            icon_path: "18789966-dfbb-445a-8ec1-c55397604203.webp".to_string(),
1891            cooldown: 10000,
1892            mana_cost: 15,
1893            is_gacha_ability: false,
1894            available_to_bots: true,
1895            rarity_id: uuid!("33a0af72-2390-4df9-bd4f-f66b27ed7792"),
1896            description: i18n::I18nString::new(
1897                "content.abilities.da6c582b-7364-40bd-9b2d-946d8e20eaac:description",
1898            ),
1899            description_values_script: Some("Result.push(1); Result.push(100)".to_string()),
1900            vfx_object_path: "".into(),
1901            cast_type: AbilityCastType::default(),
1902            target_type: AbilityTargetType::default(),
1903            range: 1,
1904            // Fixture-only AoE cap: gives the coverage supports (Widen /
1905            // Condense / Reach) something to multiply in tests.
1906            max_targets: Some(2),
1907            tags: vec![AbilityTag::Damage, AbilityTag::Aoe],
1908        },
1909        // DEAD ability: empty `start_behavior` — never begins a cast cycle in
1910        // combat (the shipped state of several class abilities). Used by the
1911        // honest-power tests to pin that such abilities contribute no power.
1912        AbilityTemplate {
1913            id: uuid!("00000000-dead-7000-8000-000000000001"),
1914            is_fight_ui_visible: false,
1915            fight_ui_visibility: AbilityFightUiVisibility::Hidden,
1916            // Translated variant: the key-less fixture must not hit the real
1917            // translator (integration envs translate the test config).
1918            name: i18n::I18nString::Translated("Dead test ability".to_string()),
1919            start_behavior: None,
1920            behavior: Some("ability_damage_target_5".to_string()),
1921            icon_url: "".to_string(),
1922            icon_path: "".to_string(),
1923            cooldown: 3000,
1924            mana_cost: 5,
1925            is_gacha_ability: false,
1926            available_to_bots: false,
1927            rarity_id: uuid!("33a0af72-2390-4df9-bd4f-f66b27ed7792"),
1928            description: i18n::I18nString::Translated("Dead test ability".to_string()),
1929            description_values_script: None,
1930            vfx_object_path: "".into(),
1931            cast_type: AbilityCastType::default(),
1932            target_type: AbilityTargetType::default(),
1933            range: 1,
1934            max_targets: None,
1935            tags: vec![AbilityTag::Damage, AbilityTag::Targeted],
1936        },
1937        // Dedicated pet-ult of test pet 1, mirroring live content: a pet's active
1938        // skill is its OWN ability entity, never one the player can also equip
1939        // (the cast pipeline keys by template id, so a shared id would shadow).
1940        AbilityTemplate {
1941            id: uuid!("b0000000-0000-0000-0000-0000000000a1"),
1942            is_fight_ui_visible: true,
1943            fight_ui_visibility: AbilityFightUiVisibility::Hidden,
1944            name: i18n::I18nString::Translated("Test Pet 1 Ult".to_string()),
1945            start_behavior: Some("attack_first_enemy".to_string()),
1946            behavior: Some("ability_damage_target_5".to_string()),
1947            icon_url: "18789966-dfbb-445a-8ec1-c55397604203.webp".to_string(),
1948            icon_path: "18789966-dfbb-445a-8ec1-c55397604203.webp".to_string(),
1949            cooldown: 10000,
1950            mana_cost: 15,
1951            is_gacha_ability: false,
1952            available_to_bots: false,
1953            rarity_id: uuid!("33a0af72-2390-4df9-bd4f-f66b27ed7792"),
1954            description: i18n::I18nString::Translated("Test Pet 1 Ult description".to_string()),
1955            description_values_script: Some("Result.push(1); Result.push(100)".to_string()),
1956            vfx_object_path: "".into(),
1957            cast_type: AbilityCastType::default(),
1958            target_type: AbilityTargetType::default(),
1959            range: 1,
1960            max_targets: None,
1961            tags: vec![AbilityTag::Damage, AbilityTag::Targeted, AbilityTag::Pet],
1962        },
1963        AbilityTemplate {
1964            id: uuid!("b0000000-0000-0000-0000-0000000000a2"),
1965            is_fight_ui_visible: true,
1966            fight_ui_visibility: AbilityFightUiVisibility::Hidden,
1967            name: i18n::I18nString::Translated("Test Pet 2 Ult".to_string()),
1968            start_behavior: Some("attack_first_enemy".to_string()),
1969            behavior: Some("ability_damage_target_5".to_string()),
1970            icon_url: "18789966-dfbb-445a-8ec1-c55397604203.webp".to_string(),
1971            icon_path: "18789966-dfbb-445a-8ec1-c55397604203.webp".to_string(),
1972            cooldown: 10000,
1973            mana_cost: 15,
1974            is_gacha_ability: false,
1975            available_to_bots: false,
1976            rarity_id: uuid!("33a0af72-2390-4df9-bd4f-f66b27ed7792"),
1977            description: i18n::I18nString::Translated("Test Pet 2 Ult description".to_string()),
1978            description_values_script: Some("Result.push(1); Result.push(100)".to_string()),
1979            vfx_object_path: "".into(),
1980            cast_type: AbilityCastType::default(),
1981            target_type: AbilityTargetType::default(),
1982            range: 1,
1983            max_targets: None,
1984            tags: vec![AbilityTag::Damage, AbilityTag::Targeted, AbilityTag::Pet],
1985        },
1986        AbilityTemplate {
1987            id: uuid!("b0000000-0000-0000-0000-0000000000a3"),
1988            is_fight_ui_visible: true,
1989            fight_ui_visibility: AbilityFightUiVisibility::Hidden,
1990            name: i18n::I18nString::Translated("Test Pet 3 Ult".to_string()),
1991            start_behavior: Some("attack_first_enemy".to_string()),
1992            behavior: Some("ability_damage_target_5".to_string()),
1993            icon_url: "18789966-dfbb-445a-8ec1-c55397604203.webp".to_string(),
1994            icon_path: "18789966-dfbb-445a-8ec1-c55397604203.webp".to_string(),
1995            cooldown: 10000,
1996            mana_cost: 15,
1997            is_gacha_ability: false,
1998            available_to_bots: false,
1999            rarity_id: uuid!("33a0af72-2390-4df9-bd4f-f66b27ed7792"),
2000            description: i18n::I18nString::Translated("Test Pet 3 Ult description".to_string()),
2001            description_values_script: Some("Result.push(1); Result.push(100)".to_string()),
2002            vfx_object_path: "".into(),
2003            cast_type: AbilityCastType::default(),
2004            target_type: AbilityTargetType::default(),
2005            range: 1,
2006            max_targets: None,
2007            tags: vec![AbilityTag::Damage, AbilityTag::Targeted, AbilityTag::Pet],
2008        },
2009        // Appended LAST on purpose. Something downstream reads this vec
2010        // positionally — inserting these at the head silently stopped campaign
2011        // and arena fights from progressing (no flip, no EndFight) across the
2012        // monolith suite. New fixture abilities go at the end.
2013        //
2014        // The two swinging fixtures. Identical but for the family: the Basic
2015        // one is in every class kit below, the Skill one deliberately is not,
2016        // so `combat_facts::cast_kind` separates them. Both run
2017        // `ability_test_swing`, the only fixture behaviour that reaches
2018        // `mechanics::fight::attack`.
2019        AbilityTemplate {
2020            id: TEST_ABILITY_SWING_BASIC,
2021            is_fight_ui_visible: false,
2022            fight_ui_visibility: AbilityFightUiVisibility::Hidden,
2023            name: i18n::I18nString::Translated("Test Swing (Basic)".to_string()),
2024            start_behavior: Some("attack_first_enemy".to_string()),
2025            behavior: Some("ability_test_swing".to_string()),
2026            icon_url: "18789966-dfbb-445a-8ec1-c55397604203.webp".to_string(),
2027            icon_path: "18789966-dfbb-445a-8ec1-c55397604203.webp".to_string(),
2028            cooldown: 10000,
2029            is_gacha_ability: false,
2030            available_to_bots: false,
2031            rarity_id: uuid!("33a0af72-2390-4df9-bd4f-f66b27ed7792"),
2032            description: i18n::I18nString::Translated("Swings for real.".to_string()),
2033            description_values_script: None,
2034            vfx_object_path: "".into(),
2035            cast_type: AbilityCastType::default(),
2036            target_type: AbilityTargetType::default(),
2037            range: 1,
2038            max_targets: None,
2039            mana_cost: 0,
2040            tags: Vec::new(),
2041        },
2042        AbilityTemplate {
2043            id: TEST_ABILITY_SWING_AOE,
2044            is_fight_ui_visible: true,
2045            fight_ui_visibility: AbilityFightUiVisibility::Slotted,
2046            name: i18n::I18nString::Translated("Test Swing (AoE)".to_string()),
2047            start_behavior: Some("attack_first_enemy".to_string()),
2048            behavior: Some("ability_test_swing_aoe".to_string()),
2049            icon_url: "8e991eae-e001-4a76-8238-277c38b35889.webp".to_string(),
2050            icon_path: "8e991eae-e001-4a76-8238-277c38b35889.webp".to_string(),
2051            cooldown: 30000,
2052            is_gacha_ability: false,
2053            available_to_bots: false,
2054            rarity_id: uuid!("81b082cc-2736-4545-87d1-301f5de8951a"),
2055            description: i18n::I18nString::Translated("Swings at every enemy.".to_string()),
2056            description_values_script: None,
2057            vfx_object_path: "".into(),
2058            cast_type: AbilityCastType::default(),
2059            target_type: AbilityTargetType::default(),
2060            range: 1,
2061            max_targets: None,
2062            mana_cost: 0,
2063            tags: Vec::new(),
2064        },
2065        AbilityTemplate {
2066            id: TEST_ABILITY_SWING_SKILL,
2067            is_fight_ui_visible: true,
2068            fight_ui_visibility: AbilityFightUiVisibility::Slotted,
2069            name: i18n::I18nString::Translated("Test Swing (Skill)".to_string()),
2070            start_behavior: Some("attack_first_enemy".to_string()),
2071            behavior: Some("ability_test_swing".to_string()),
2072            icon_url: "8e991eae-e001-4a76-8238-277c38b35889.webp".to_string(),
2073            icon_path: "8e991eae-e001-4a76-8238-277c38b35889.webp".to_string(),
2074            cooldown: 30000,
2075            is_gacha_ability: false,
2076            available_to_bots: false,
2077            rarity_id: uuid!("81b082cc-2736-4545-87d1-301f5de8951a"),
2078            description: i18n::I18nString::Translated("Swings for real.".to_string()),
2079            description_values_script: None,
2080            vfx_object_path: "".into(),
2081            cast_type: AbilityCastType::default(),
2082            target_type: AbilityTargetType::default(),
2083            range: 1,
2084            max_targets: None,
2085            mana_cost: 0,
2086            tags: Vec::new(),
2087        },
2088    ]
2089}
2090
2091/// Ability-stone catalog for tests: one support per operation family, each with
2092/// an op set narrow enough that a test can address it unambiguously, so every
2093/// branch of the modifier fold and of the shape expansion has a fixture to
2094/// drive it. The shipped 20-support catalog lives in the storage YAML; its
2095/// numbers are checked against the deployed config in
2096/// `tests/test_support_stone_catalog.rs`.
2097pub fn generate_ability_stones() -> Vec<AbilityStoneTemplate> {
2098    use essences::abilities::AbilityTag;
2099    use essences::ability_stones::{
2100        AbilityStoneCompatibility, AbilityStoneFamily, AbilityStoneOp, AbilityStoneOpKind,
2101    };
2102
2103    let stone = |n: u128,
2104                 name: &str,
2105                 family: AbilityStoneFamily,
2106                 compatibility: AbilityStoneCompatibility,
2107                 ops: Vec<AbilityStoneOp>| AbilityStoneTemplate {
2108        id: uuid::Uuid::from_u128(0x019fb000_0000_7000_8000_000000000000 + n),
2109        name: i18n::I18nString::Translated(name.to_string()),
2110        description: i18n::I18nString::Translated(format!("{name} (test fixture)")),
2111        family,
2112        compatibility,
2113        ops,
2114        power_q_first_rank: 1.05,
2115        power_q_max_rank: 1.15,
2116        icon_path: String::new(),
2117    };
2118    let any = AbilityStoneCompatibility::any;
2119    let requires = |tags: Vec<AbilityTag>| AbilityStoneCompatibility {
2120        required_any_of: tags,
2121        forbidden: vec![],
2122    };
2123    let scalar = AbilityStoneOp::scalar;
2124
2125    vec![
2126        // --- single-op stones: one operation each, addressable by kind -------
2127        stone(
2128            1,
2129            "Заточка",
2130            AbilityStoneFamily::Magnitude,
2131            any(),
2132            vec![scalar(AbilityStoneOpKind::PayloadMult, 1.12, 0.06)],
2133        ),
2134        stone(
2135            2,
2136            "Экономия",
2137            AbilityStoneFamily::Economy,
2138            any(),
2139            vec![scalar(AbilityStoneOpKind::ManaCostMult, 0.85, -0.075)],
2140        ),
2141        stone(
2142            3,
2143            "Ускорение",
2144            AbilityStoneFamily::Cadence,
2145            any(),
2146            vec![scalar(AbilityStoneOpKind::CooldownMult, 0.88, -0.06)],
2147        ),
2148        stone(
2149            4,
2150            "Продление",
2151            AbilityStoneFamily::Duration,
2152            any(),
2153            vec![scalar(AbilityStoneOpKind::EffectDurationMult, 1.20, 0.10)],
2154        ),
2155        stone(
2156            5,
2157            "Охват",
2158            AbilityStoneFamily::Coverage,
2159            requires(vec![AbilityTag::Aoe]),
2160            vec![scalar(AbilityStoneOpKind::ExtraTargets, 1.0, 1.0)],
2161        ),
2162        stone(
2163            6,
2164            "Отдача",
2165            AbilityStoneFamily::Conversion,
2166            any(),
2167            vec![scalar(
2168                AbilityStoneOpKind::MissingManaDamagePercent,
2169                8.0,
2170                4.0,
2171            )],
2172        ),
2173        stone(
2174            7,
2175            "Быстрый каст",
2176            AbilityStoneFamily::Delivery,
2177            any(),
2178            vec![scalar(AbilityStoneOpKind::CastTimeMult, 0.50, -0.05)],
2179        ),
2180        // --- multi-op supports, mirroring the shipped ops -------------------
2181        stone(
2182            8,
2183            "Точность",
2184            AbilityStoneFamily::Critical,
2185            requires(vec![AbilityTag::Damage]),
2186            vec![
2187                scalar(AbilityStoneOpKind::CritChanceBonus, 25.0, 5.0),
2188                scalar(AbilityStoneOpKind::PayloadMult, 0.95, 0.0),
2189                scalar(AbilityStoneOpKind::ManaCostMult, 1.15, 0.0),
2190            ],
2191        ),
2192        stone(
2193            9,
2194            "Расщепление",
2195            AbilityStoneFamily::Shape,
2196            requires(vec![AbilityTag::Projectile, AbilityTag::Targeted]),
2197            vec![
2198                AbilityStoneOp {
2199                    kind: AbilityStoneOpKind::SplitCopies,
2200                    base_value: 70.0,
2201                    value_per_level: 5.0,
2202                    count: 1,
2203                    interval_ms: 0,
2204                },
2205                scalar(AbilityStoneOpKind::ManaCostMult, 1.25, 0.0),
2206            ],
2207        ),
2208        stone(
2209            10,
2210            "Цепь",
2211            AbilityStoneFamily::Shape,
2212            requires(vec![AbilityTag::Projectile, AbilityTag::Targeted]),
2213            vec![
2214                AbilityStoneOp {
2215                    kind: AbilityStoneOpKind::ChainTargets,
2216                    base_value: 45.0,
2217                    value_per_level: 5.0,
2218                    count: 2,
2219                    interval_ms: 0,
2220                },
2221                scalar(AbilityStoneOpKind::ManaCostMult, 1.35, 0.0),
2222            ],
2223        ),
2224        stone(
2225            11,
2226            "Пробой",
2227            AbilityStoneFamily::Targeting,
2228            requires(vec![AbilityTag::Projectile]),
2229            vec![
2230                AbilityStoneOp {
2231                    kind: AbilityStoneOpKind::PierceTargets,
2232                    base_value: 70.0,
2233                    value_per_level: 5.0,
2234                    count: 2,
2235                    interval_ms: 0,
2236                },
2237                scalar(AbilityStoneOpKind::ManaCostMult, 1.20, 0.0),
2238            ],
2239        ),
2240        stone(
2241            12,
2242            "Повтор",
2243            AbilityStoneFamily::Replay,
2244            AbilityStoneCompatibility {
2245                required_any_of: vec![],
2246                forbidden: vec![AbilityTag::Channelled],
2247            },
2248            vec![
2249                AbilityStoneOp {
2250                    kind: AbilityStoneOpKind::RepeatCast,
2251                    base_value: 50.0,
2252                    value_per_level: 5.0,
2253                    count: 1,
2254                    interval_ms: 600,
2255                },
2256                scalar(AbilityStoneOpKind::ManaCostMult, 1.30, 0.0),
2257            ],
2258        ),
2259        stone(
2260            13,
2261            "Пульс",
2262            AbilityStoneFamily::Replay,
2263            requires(vec![AbilityTag::Aoe, AbilityTag::Duration]),
2264            vec![
2265                AbilityStoneOp {
2266                    kind: AbilityStoneOpKind::PulseSplit,
2267                    base_value: 45.0,
2268                    value_per_level: 3.0,
2269                    count: 3,
2270                    interval_ms: 400,
2271                },
2272                scalar(AbilityStoneOpKind::ManaCostMult, 1.25, 0.0),
2273            ],
2274        ),
2275        stone(
2276            14,
2277            "Сжатие",
2278            AbilityStoneFamily::Shape,
2279            requires(vec![
2280                AbilityTag::Projectile,
2281                AbilityTag::Aoe,
2282                AbilityTag::MultiTarget,
2283            ]),
2284            vec![
2285                scalar(AbilityStoneOpKind::Condense, 1.60, 0.10),
2286                scalar(AbilityStoneOpKind::ManaCostMult, 1.15, 0.0),
2287            ],
2288        ),
2289        stone(
2290            15,
2291            "Расширение",
2292            AbilityStoneFamily::Coverage,
2293            requires(vec![AbilityTag::Aoe]),
2294            vec![
2295                scalar(AbilityStoneOpKind::CoverageMult, 1.60, 0.10),
2296                scalar(AbilityStoneOpKind::PayloadMult, 0.85, 0.0),
2297                scalar(AbilityStoneOpKind::ManaCostMult, 1.10, 0.0),
2298            ],
2299        ),
2300        stone(
2301            16,
2302            "Вытягивание",
2303            AbilityStoneFamily::Conversion,
2304            requires(vec![AbilityTag::Damage]),
2305            vec![
2306                scalar(AbilityStoneOpKind::LeechPercent, 15.0, 3.0),
2307                scalar(AbilityStoneOpKind::PayloadMult, 0.90, 0.0),
2308            ],
2309        ),
2310        stone(
2311            17,
2312            "Укрепление",
2313            AbilityStoneFamily::Defense,
2314            any(),
2315            vec![
2316                scalar(AbilityStoneOpKind::GuardNextHit, 30.0, 5.0),
2317                scalar(AbilityStoneOpKind::PayloadMult, 0.90, 0.0),
2318            ],
2319        ),
2320        stone(
2321            19,
2322            "Спешка",
2323            AbilityStoneFamily::Cadence,
2324            any(),
2325            vec![
2326                scalar(AbilityStoneOpKind::PayloadMult, 0.80, 0.02),
2327                scalar(AbilityStoneOpKind::ManaCostMult, 1.10, 0.0),
2328                scalar(AbilityStoneOpKind::CooldownMult, 0.75, -0.02),
2329            ],
2330        ),
2331        stone(
2332            20,
2333            "Перегрузка",
2334            AbilityStoneFamily::Magnitude,
2335            any(),
2336            vec![
2337                scalar(AbilityStoneOpKind::PayloadMult, 1.50, 0.10),
2338                scalar(AbilityStoneOpKind::ManaCostMult, 1.50, 0.0),
2339            ],
2340        ),
2341        stone(
2342            18,
2343            "Убежище",
2344            AbilityStoneFamily::Defense,
2345            requires(vec![
2346                AbilityTag::SelfCast,
2347                AbilityTag::Heal,
2348                AbilityTag::Protection,
2349            ]),
2350            vec![
2351                AbilityStoneOp {
2352                    kind: AbilityStoneOpKind::IncomingDamageReduction,
2353                    base_value: 20.0,
2354                    value_per_level: 4.0,
2355                    count: 0,
2356                    interval_ms: 3000,
2357                },
2358                scalar(AbilityStoneOpKind::PayloadMult, 0.85, 0.0),
2359            ],
2360        ),
2361    ]
2362}
2363
2364pub fn generate_ability_stone_settings() -> AbilityStoneSettings {
2365    AbilityStoneSettings {
2366        // 0 in tests so low-chapter scenarios can use stones; gate tests
2367        // override this per-test. The shipped value is 45 (BAL-005).
2368        unlock_chapter: 0,
2369        sockets: vec![
2370            AbilityStoneSocketSettings {
2371                side: WorldSide::Real,
2372                unlock_ability_level: 2,
2373            },
2374            AbilityStoneSocketSettings {
2375                side: WorldSide::Fantasy,
2376                unlock_ability_level: 4,
2377            },
2378            AbilityStoneSocketSettings {
2379                side: WorldSide::Real,
2380                unlock_ability_level: 6,
2381            },
2382            AbilityStoneSocketSettings {
2383                side: WorldSide::Fantasy,
2384                unlock_ability_level: 8,
2385            },
2386        ],
2387        class_sockets: vec![
2388            AbilityStoneSocketSettings {
2389                side: WorldSide::Real,
2390                unlock_ability_level: 2,
2391            },
2392            AbilityStoneSocketSettings {
2393                side: WorldSide::Fantasy,
2394                unlock_ability_level: 4,
2395            },
2396            AbilityStoneSocketSettings {
2397                side: WorldSide::Real,
2398                unlock_ability_level: 6,
2399            },
2400            AbilityStoneSocketSettings {
2401                side: WorldSide::Fantasy,
2402                unlock_ability_level: 7,
2403            },
2404        ],
2405        upgrade_copies: vec![2, 3, 5],
2406        // 1 in tests so a single chapter clear exercises the faucet; the
2407        // shipped value is larger.
2408        chapter_reward_period: 1,
2409    }
2410}
2411
2412fn generate_ability_cases_settings() -> NonEmptyVec<AbilityCasesSettingsByLevel> {
2413    NonEmptyVec::new(vec![
2414        AbilityCasesSettingsByLevel {
2415            level: 1,
2416            opens_to_upgrade: 0,
2417            is_boundary_level: false,
2418            rarity_weights: vec![
2419                AbilityCaseRarityWeight {
2420                    rarity_id: uuid!("33a0af72-2390-4df9-bd4f-f66b27ed7792"),
2421                    weight: PositiveF64::new(256.0),
2422                },
2423                AbilityCaseRarityWeight {
2424                    rarity_id: uuid!("6f0f1099-8ab1-4378-aea5-ac6bcbe6ec3e"),
2425                    weight: PositiveF64::new(128.0),
2426                },
2427                AbilityCaseRarityWeight {
2428                    rarity_id: uuid!("81b082cc-2736-4545-87d1-301f5de8951a"),
2429                    weight: PositiveF64::new(64.0),
2430                },
2431            ],
2432            checkpoints: vec![AbilityCaseCheckpointReward {
2433                required_opens: 1,
2434                currency_rewards: vec![],
2435                ability_rewards: vec![AbilityCaseCheckpointAbilityReward {
2436                    amount: 1,
2437                    min_rarity_id: uuid!("33a0af72-2390-4df9-bd4f-f66b27ed7792"),
2438                }],
2439            }],
2440            allowed_wishlist_rarity_ids: vec![
2441                uuid!("33a0af72-2390-4df9-bd4f-f66b27ed7792"),
2442                uuid!("6f0f1099-8ab1-4378-aea5-ac6bcbe6ec3e"),
2443            ],
2444            evolve_rules: vec![AbilityCaseEvolveRule {
2445                from_rarity_id: uuid!("33a0af72-2390-4df9-bd4f-f66b27ed7792"),
2446                small_roll_tries: 1,
2447                big_roll_tries: 1,
2448                to_rarity_weights: vec![AbilityCaseRarityWeight {
2449                    rarity_id: uuid!("6f0f1099-8ab1-4378-aea5-ac6bcbe6ec3e"),
2450                    weight: PositiveF64::new(1.0),
2451                }],
2452            }],
2453            big_roll_currency_rewards: vec![],
2454            big_roll_shards: vec![
2455                BigRollShard {
2456                    min_rarity_id: uuid!("33a0af72-2390-4df9-bd4f-f66b27ed7792"),
2457                    count: 7,
2458                },
2459                BigRollShard {
2460                    min_rarity_id: uuid!("6f0f1099-8ab1-4378-aea5-ac6bcbe6ec3e"),
2461                    count: 3,
2462                },
2463                BigRollShard {
2464                    min_rarity_id: uuid!("81b082cc-2736-4545-87d1-301f5de8951a"),
2465                    count: 1,
2466                },
2467            ],
2468        },
2469        AbilityCasesSettingsByLevel {
2470            level: 2,
2471            opens_to_upgrade: 10,
2472            is_boundary_level: false,
2473            rarity_weights: vec![
2474                AbilityCaseRarityWeight {
2475                    rarity_id: uuid!("33a0af72-2390-4df9-bd4f-f66b27ed7792"),
2476                    weight: PositiveF64::new(256.0),
2477                },
2478                AbilityCaseRarityWeight {
2479                    rarity_id: uuid!("6f0f1099-8ab1-4378-aea5-ac6bcbe6ec3e"),
2480                    weight: PositiveF64::new(128.0),
2481                },
2482                AbilityCaseRarityWeight {
2483                    rarity_id: uuid!("81b082cc-2736-4545-87d1-301f5de8951a"),
2484                    weight: PositiveF64::new(64.0),
2485                },
2486            ],
2487            checkpoints: vec![],
2488            allowed_wishlist_rarity_ids: vec![uuid!("81b082cc-2736-4545-87d1-301f5de8951a")],
2489            evolve_rules: vec![],
2490            big_roll_currency_rewards: vec![],
2491            big_roll_shards: vec![
2492                BigRollShard {
2493                    min_rarity_id: uuid!("33a0af72-2390-4df9-bd4f-f66b27ed7792"),
2494                    count: 7,
2495                },
2496                BigRollShard {
2497                    min_rarity_id: uuid!("6f0f1099-8ab1-4378-aea5-ac6bcbe6ec3e"),
2498                    count: 3,
2499                },
2500                BigRollShard {
2501                    min_rarity_id: uuid!("81b082cc-2736-4545-87d1-301f5de8951a"),
2502                    count: 1,
2503                },
2504            ],
2505        },
2506        AbilityCasesSettingsByLevel {
2507            level: 3,
2508            opens_to_upgrade: 20,
2509            is_boundary_level: true,
2510            rarity_weights: vec![],
2511            checkpoints: vec![],
2512            allowed_wishlist_rarity_ids: vec![],
2513            evolve_rules: vec![],
2514            big_roll_currency_rewards: vec![],
2515            big_roll_shards: vec![],
2516        },
2517    ])
2518}
2519
2520fn generate_pet_rarities() -> Vec<PetRarity> {
2521    vec![
2522        PetRarity {
2523            id: uuid!("a0000000-0000-0000-0000-000000000001"),
2524            name: i18n::I18nString::Translated("Common Pet Rarity".to_string()),
2525            order: 1,
2526            color: "#ffffff".to_string(),
2527            bg_color: "#000000".to_string(),
2528            icon_path: "/pet_rarity_common.webp".to_string(),
2529            square_icon_path: "/pet_rarity_common.webp".to_string(),
2530        },
2531        PetRarity {
2532            id: uuid!("a0000000-0000-0000-0000-000000000002"),
2533            name: i18n::I18nString::Translated("Uncommon Pet Rarity".to_string()),
2534            order: 2,
2535            color: "#00ff00".to_string(),
2536            bg_color: "#003300".to_string(),
2537            icon_path: "/pet_rarity_uncommon.webp".to_string(),
2538            square_icon_path: "/pet_rarity_uncommon.webp".to_string(),
2539        },
2540    ]
2541}
2542
2543fn generate_pet_templates() -> Vec<PetTemplate> {
2544    vec![
2545        PetTemplate {
2546            id: uuid!("b0000000-0000-0000-0000-000000000001"),
2547            name: i18n::I18nString::Translated("Test Pet 1".to_string()),
2548            icon_path: "/pet1.webp".to_string(),
2549            spine_path: "/pet1_spine".to_string(),
2550            rarity_id: uuid!("a0000000-0000-0000-0000-000000000001"),
2551            stats: vec![
2552                PetSecondaryStat {
2553                    attribute_id: uuid!("45eca0a7-7430-487b-bd65-b796c6d88c08"),
2554                    base_value: 100,
2555                    per_level_value: 10,
2556                },
2557                PetSecondaryStat {
2558                    attribute_id: uuid!("6ad485d7-9567-4b77-8ddc-a2418dd1dfe6"),
2559                    base_value: 50,
2560                    per_level_value: 5,
2561                },
2562            ],
2563            is_gacha_pet: true,
2564
2565            real_facet: PetFacet::HeadStart,
2566            fantasy_facet: PetFacet::SecondSpark,
2567        },
2568        PetTemplate {
2569            id: uuid!("b0000000-0000-0000-0000-000000000002"),
2570            name: i18n::I18nString::Translated("Test Pet 2".to_string()),
2571            icon_path: "/pet2.webp".to_string(),
2572            spine_path: "/pet2_spine".to_string(),
2573            rarity_id: uuid!("a0000000-0000-0000-0000-000000000001"),
2574            stats: vec![PetSecondaryStat {
2575                attribute_id: uuid!("45eca0a7-7430-487b-bd65-b796c6d88c08"),
2576                base_value: 200,
2577                per_level_value: 20,
2578            }],
2579            is_gacha_pet: true,
2580
2581            real_facet: PetFacet::BudgetPlan,
2582            fantasy_facet: PetFacet::OpenTab,
2583        },
2584        PetTemplate {
2585            id: uuid!("b0000000-0000-0000-0000-000000000003"),
2586            name: i18n::I18nString::Translated("Test Pet 3 Uncommon".to_string()),
2587            icon_path: "/pet3.webp".to_string(),
2588            spine_path: "/pet3_spine".to_string(),
2589            rarity_id: uuid!("a0000000-0000-0000-0000-000000000002"),
2590            stats: vec![PetSecondaryStat {
2591                attribute_id: uuid!("45eca0a7-7430-487b-bd65-b796c6d88c08"),
2592                base_value: 300,
2593                per_level_value: 30,
2594            }],
2595            is_gacha_pet: true,
2596
2597            real_facet: PetFacet::Overtime,
2598            fantasy_facet: PetFacet::MagicVolley,
2599        },
2600        // Pets 4..10 exist only to carry the remaining Pet Facets so every facet
2601        // is rollable in tests. Their ult is not what is under test, so they
2602        // share pet 1's — `validate_pet_config` requires a dedicated non-gacha,
2603        // non-class ability, not a distinct one per pet.
2604        PetTemplate {
2605            id: uuid!("b0000000-0000-0000-0000-000000000004"),
2606            name: i18n::I18nString::Translated("Test Pet 4".to_string()),
2607            icon_path: "/pet4.webp".to_string(),
2608            spine_path: "/pet4_spine".to_string(),
2609            rarity_id: uuid!("a0000000-0000-0000-0000-000000000001"),
2610            stats: vec![PetSecondaryStat {
2611                attribute_id: uuid!("45eca0a7-7430-487b-bd65-b796c6d88c08"),
2612                base_value: 100,
2613                per_level_value: 10,
2614            }],
2615            is_gacha_pet: false,
2616            real_facet: PetFacet::SafetyNet,
2617            fantasy_facet: PetFacet::Retaliation,
2618        },
2619        PetTemplate {
2620            id: uuid!("b0000000-0000-0000-0000-000000000005"),
2621            name: i18n::I18nString::Translated("Test Pet 5".to_string()),
2622            icon_path: "/pet5.webp".to_string(),
2623            spine_path: "/pet5_spine".to_string(),
2624            rarity_id: uuid!("a0000000-0000-0000-0000-000000000001"),
2625            stats: vec![PetSecondaryStat {
2626                attribute_id: uuid!("45eca0a7-7430-487b-bd65-b796c6d88c08"),
2627                base_value: 100,
2628                per_level_value: 10,
2629            }],
2630            is_gacha_pet: false,
2631            real_facet: PetFacet::Calibration,
2632            fantasy_facet: PetFacet::LuckyStar,
2633        },
2634        PetTemplate {
2635            id: uuid!("b0000000-0000-0000-0000-000000000006"),
2636            name: i18n::I18nString::Translated("Test Pet 6".to_string()),
2637            icon_path: "/pet6.webp".to_string(),
2638            spine_path: "/pet6_spine".to_string(),
2639            rarity_id: uuid!("a0000000-0000-0000-0000-000000000001"),
2640            stats: vec![PetSecondaryStat {
2641                attribute_id: uuid!("45eca0a7-7430-487b-bd65-b796c6d88c08"),
2642                base_value: 100,
2643                per_level_value: 10,
2644            }],
2645            is_gacha_pet: false,
2646            real_facet: PetFacet::LeadReading,
2647            fantasy_facet: PetFacet::WildReading,
2648        },
2649        PetTemplate {
2650            id: uuid!("b0000000-0000-0000-0000-000000000007"),
2651            name: i18n::I18nString::Translated("Test Pet 7".to_string()),
2652            icon_path: "/pet7.webp".to_string(),
2653            spine_path: "/pet7_spine".to_string(),
2654            rarity_id: uuid!("a0000000-0000-0000-0000-000000000001"),
2655            stats: vec![PetSecondaryStat {
2656                attribute_id: uuid!("45eca0a7-7430-487b-bd65-b796c6d88c08"),
2657                base_value: 100,
2658                per_level_value: 10,
2659            }],
2660            is_gacha_pet: false,
2661            real_facet: PetFacet::AdvanceNotice,
2662            fantasy_facet: PetFacet::RisingGate,
2663        },
2664        PetTemplate {
2665            id: uuid!("b0000000-0000-0000-0000-000000000008"),
2666            name: i18n::I18nString::Translated("Test Pet 8".to_string()),
2667            icon_path: "/pet8.webp".to_string(),
2668            spine_path: "/pet8_spine".to_string(),
2669            rarity_id: uuid!("a0000000-0000-0000-0000-000000000001"),
2670            stats: vec![PetSecondaryStat {
2671                attribute_id: uuid!("45eca0a7-7430-487b-bd65-b796c6d88c08"),
2672                base_value: 100,
2673                per_level_value: 10,
2674            }],
2675            is_gacha_pet: false,
2676            real_facet: PetFacet::PreparedSlots,
2677            fantasy_facet: PetFacet::FirstSpell,
2678        },
2679        PetTemplate {
2680            id: uuid!("b0000000-0000-0000-0000-000000000009"),
2681            name: i18n::I18nString::Translated("Test Pet 9".to_string()),
2682            icon_path: "/pet9.webp".to_string(),
2683            spine_path: "/pet9_spine".to_string(),
2684            rarity_id: uuid!("a0000000-0000-0000-0000-000000000001"),
2685            stats: vec![PetSecondaryStat {
2686                attribute_id: uuid!("45eca0a7-7430-487b-bd65-b796c6d88c08"),
2687                base_value: 100,
2688                per_level_value: 10,
2689            }],
2690            is_gacha_pet: false,
2691            real_facet: PetFacet::Souvenir,
2692            fantasy_facet: PetFacet::DreamReader,
2693        },
2694        PetTemplate {
2695            id: uuid!("b0000000-0000-0000-0000-00000000000a"),
2696            name: i18n::I18nString::Translated("Test Pet 10".to_string()),
2697            icon_path: "/pet10.webp".to_string(),
2698            spine_path: "/pet10_spine".to_string(),
2699            rarity_id: uuid!("a0000000-0000-0000-0000-000000000001"),
2700            stats: vec![PetSecondaryStat {
2701                attribute_id: uuid!("45eca0a7-7430-487b-bd65-b796c6d88c08"),
2702                base_value: 100,
2703                per_level_value: 10,
2704            }],
2705            is_gacha_pet: false,
2706            real_facet: PetFacet::PackedLunch,
2707            fantasy_facet: PetFacet::LifeBloom,
2708        },
2709    ]
2710}
2711
2712/// Pet Facet numbers for tests — the shipped design values, so a behavioural
2713/// test asserts the number the doc names (1.2 s, 5 bolts, 15% Max HP …) rather
2714/// than a fixture-only one.
2715///
2716/// `rank_growth_permyriad` is the one deliberate deviation: 0 here, so a test
2717/// pet's level never silently rescales the magnitude it is asserting.
2718/// `test_pet_facets::rank_scales_numbers_and_never_rules` turns it on
2719/// explicitly.
2720fn generate_pet_facet_settings() -> crate::pet_facets::PetFacetSettings {
2721    crate::pet_facets::PetFacetSettings {
2722        rank_growth_permyriad: 0,
2723        head_start_ticks: 1_200,
2724        second_spark_percent: 60.0,
2725        budget_plan_cost_multiplier: 0.75,
2726        budget_plan_casts: 3,
2727        open_tab_surcharge_percent: 50.0,
2728        open_tab_payload_share_percent: 60.0,
2729        overtime_attack_speed_percent: 35.0,
2730        overtime_duration_ticks: 6_000,
2731        magic_volley_bolts: 5,
2732        magic_volley_percent: 35.0,
2733        safety_net_reduction_percent: 30.0,
2734        safety_net_duration_ticks: 6_000,
2735        retaliation_percent: 220.0,
2736        calibration_crit_percent: 20.0,
2737        calibration_attacks: 5,
2738        lucky_star_percent: 180.0,
2739        lead_reading_resonance_percent: 50.0,
2740        lead_reading_activations: 2,
2741        wild_reading_effect_percent: 40.0,
2742        wild_reading_duration_ticks: 6_000,
2743        advance_notice_gauge_percent: 12.0,
2744        rising_gate_procs: 4,
2745        rising_gate_multiplier_l1: 2.0,
2746        rising_gate_multiplier_l10: 4.0,
2747        prepared_slots_percent: 140.0,
2748        first_spell_percent: 100.0,
2749        souvenir_procs: 3,
2750        souvenir_percent: 30.0,
2751        dream_reader_percent: 100.0,
2752        packed_lunch_heal_percent: 15.0,
2753        life_bloom_percent_l1: 5.0,
2754        life_bloom_percent_l10: 10.0,
2755        life_bloom_duration_ticks: 10_000,
2756        power_q: power_q_rows(),
2757    }
2758}
2759
2760/// The shipped Power seeds of all twenty facets (BAL-030), in enum order.
2761fn power_q_rows() -> Vec<crate::pet_facets::PetFacetPowerQ> {
2762    use essences::pet_facets::PetFacet;
2763
2764    [
2765        (PetFacet::HeadStart, 1.050, 1.070),
2766        (PetFacet::SecondSpark, 1.010, 1.015),
2767        (PetFacet::BudgetPlan, 1.030, 1.050),
2768        (PetFacet::OpenTab, 1.010, 1.020),
2769        (PetFacet::Overtime, 1.035, 1.051),
2770        (PetFacet::MagicVolley, 1.015, 1.021),
2771        (PetFacet::SafetyNet, 1.100, 1.170),
2772        (PetFacet::Retaliation, 1.018, 1.027),
2773        (PetFacet::Calibration, 1.010, 1.014),
2774        (PetFacet::LuckyStar, 1.015, 1.022),
2775        (PetFacet::LeadReading, 1.020, 1.030),
2776        (PetFacet::WildReading, 1.020, 1.030),
2777        (PetFacet::AdvanceNotice, 1.020, 1.030),
2778        (PetFacet::RisingGate, 1.010, 1.035),
2779        (PetFacet::PreparedSlots, 1.050, 1.120),
2780        (PetFacet::FirstSpell, 1.030, 1.050),
2781        (PetFacet::Souvenir, 1.020, 1.030),
2782        (PetFacet::DreamReader, 1.010, 1.020),
2783        (PetFacet::PackedLunch, 1.120, 1.180),
2784        (PetFacet::LifeBloom, 1.020, 1.040),
2785    ]
2786    .into_iter()
2787    .map(
2788        |(facet, q_first_level, q_max_level)| crate::pet_facets::PetFacetPowerQ {
2789            facet,
2790            q_first_level,
2791            q_max_level,
2792        },
2793    )
2794    .collect()
2795}
2796
2797fn generate_pet_levels() -> Vec<PetLevel> {
2798    vec![
2799        PetLevel {
2800            level: 2,
2801            rarity_id: uuid!("a0000000-0000-0000-0000-000000000001"),
2802            required_shards: 10,
2803        },
2804        PetLevel {
2805            level: 3,
2806            rarity_id: uuid!("a0000000-0000-0000-0000-000000000001"),
2807            required_shards: 20,
2808        },
2809        PetLevel {
2810            level: 2,
2811            rarity_id: uuid!("a0000000-0000-0000-0000-000000000002"),
2812            required_shards: 15,
2813        },
2814        PetLevel {
2815            level: 3,
2816            rarity_id: uuid!("a0000000-0000-0000-0000-000000000002"),
2817            required_shards: 30,
2818        },
2819    ]
2820}
2821
2822fn generate_pet_slots_levels() -> Vec<PetSlotsLevel> {
2823    vec![PetSlotsLevel {
2824        from_chapter_level: 1,
2825        pet_slots: 3,
2826    }]
2827}
2828
2829fn generate_pet_cases_settings() -> Vec<PetCasesSettingsByLevel> {
2830    // Pet rarity IDs from test config:
2831    // common   = a0000000-...-000000000001 (order 1, weight 256)
2832    // uncommon = a0000000-...-000000000002 (order 2, weight 128)
2833    vec![
2834        PetCasesSettingsByLevel {
2835            level: 1,
2836            opens_to_upgrade: 0,
2837            is_boundary_level: false,
2838            rarity_weights: vec![
2839                PetCaseRarityWeight {
2840                    rarity_id: uuid!("a0000000-0000-0000-0000-000000000001"),
2841                    weight: PositiveF64::new(256.0),
2842                },
2843                PetCaseRarityWeight {
2844                    rarity_id: uuid!("a0000000-0000-0000-0000-000000000002"),
2845                    weight: PositiveF64::new(128.0),
2846                },
2847            ],
2848            checkpoints: vec![PetCaseCheckpointReward {
2849                required_opens: 1,
2850                currency_rewards: vec![],
2851                pet_rewards: vec![PetCaseCheckpointPetReward {
2852                    amount: 1,
2853                    min_rarity_id: uuid!("a0000000-0000-0000-0000-000000000001"),
2854                }],
2855            }],
2856            allowed_wishlist_rarity_ids: vec![uuid!("a0000000-0000-0000-0000-000000000001")],
2857            big_roll_currency_rewards: vec![],
2858            big_roll_shards: vec![
2859                PetBigRollShard {
2860                    min_rarity_id: uuid!("a0000000-0000-0000-0000-000000000001"),
2861                    count: 8,
2862                },
2863                PetBigRollShard {
2864                    min_rarity_id: uuid!("a0000000-0000-0000-0000-000000000002"),
2865                    count: 3,
2866                },
2867            ],
2868        },
2869        PetCasesSettingsByLevel {
2870            level: 2,
2871            opens_to_upgrade: 10,
2872            is_boundary_level: false,
2873            rarity_weights: vec![
2874                PetCaseRarityWeight {
2875                    rarity_id: uuid!("a0000000-0000-0000-0000-000000000001"),
2876                    weight: PositiveF64::new(256.0),
2877                },
2878                PetCaseRarityWeight {
2879                    rarity_id: uuid!("a0000000-0000-0000-0000-000000000002"),
2880                    weight: PositiveF64::new(128.0),
2881                },
2882            ],
2883            checkpoints: vec![],
2884            allowed_wishlist_rarity_ids: vec![uuid!("a0000000-0000-0000-0000-000000000002")],
2885            big_roll_currency_rewards: vec![],
2886            big_roll_shards: vec![
2887                PetBigRollShard {
2888                    min_rarity_id: uuid!("a0000000-0000-0000-0000-000000000001"),
2889                    count: 8,
2890                },
2891                PetBigRollShard {
2892                    min_rarity_id: uuid!("a0000000-0000-0000-0000-000000000002"),
2893                    count: 3,
2894                },
2895            ],
2896        },
2897        PetCasesSettingsByLevel {
2898            level: 3,
2899            opens_to_upgrade: 20,
2900            is_boundary_level: true,
2901            rarity_weights: vec![],
2902            checkpoints: vec![],
2903            allowed_wishlist_rarity_ids: vec![],
2904            big_roll_currency_rewards: vec![],
2905            big_roll_shards: vec![],
2906        },
2907    ]
2908}
2909
2910fn generate_ability_rarities() -> Vec<AbilityRarity> {
2911    vec![
2912        AbilityRarity {
2913            id: uuid!("33a0af72-2390-4df9-bd4f-f66b27ed7792"),
2914            name: i18n::I18nString::new(
2915                "content.ability_rarities.33a0af72-2390-4df9-bd4f-f66b27ed7792:name",
2916            ),
2917            order: 1,
2918            color: "#97ca7a".to_string(),
2919            bg_color: "#244026".to_string(),
2920            icon_url: "e0bcf4ea-b2b5-47d6-9d76-985797a5a818.webp".to_string(),
2921            icon_path: "e0bcf4ea-b2b5-47d6-9d76-985797a5a818.webp".to_string(),
2922            square_icon_path: "e0bcf4ea-b2b5-47d6-9d76-985797a5a818.webp".to_string(),
2923            eff: 1.0,
2924        },
2925        AbilityRarity {
2926            id: uuid!("6f0f1099-8ab1-4378-aea5-ac6bcbe6ec3e"),
2927            name: i18n::I18nString::new(
2928                "content.ability_rarities.6f0f1099-8ab1-4378-aea5-ac6bcbe6ec3e:name",
2929            ),
2930            order: 2,
2931            color: "#97ca7a".to_string(),
2932            bg_color: "#244026".to_string(),
2933            icon_url: "e0bcf4ea-b2b5-47d6-9d76-985797a5a818.webp".to_string(),
2934            icon_path: "e0bcf4ea-b2b5-47d6-9d76-985797a5a818.webp".to_string(),
2935            square_icon_path: "e0bcf4ea-b2b5-47d6-9d76-985797a5a818.webp".to_string(),
2936            eff: 1.0,
2937        },
2938        AbilityRarity {
2939            id: uuid!("81b082cc-2736-4545-87d1-301f5de8951a"),
2940            name: i18n::I18nString::new(
2941                "content.ability_rarities.81b082cc-2736-4545-87d1-301f5de8951a:name",
2942            ),
2943            order: 3,
2944            color: "#97ca7a".to_string(),
2945            bg_color: "#244026".to_string(),
2946            icon_url: "e0bcf4ea-b2b5-47d6-9d76-985797a5a818.webp".to_string(),
2947            icon_path: "e0bcf4ea-b2b5-47d6-9d76-985797a5a818.webp".to_string(),
2948            square_icon_path: "e0bcf4ea-b2b5-47d6-9d76-985797a5a818.webp".to_string(),
2949            eff: 1.0,
2950        },
2951        AbilityRarity {
2952            id: uuid!("88ed32be-33cd-43a5-88e1-65c078e6372d"),
2953            name: i18n::I18nString::new(
2954                "content.ability_rarities.88ed32be-33cd-43a5-88e1-65c078e6372d:name",
2955            ),
2956            order: 4,
2957            color: "#97ca7a".to_string(),
2958            bg_color: "#244026".to_string(),
2959            icon_url: "e0bcf4ea-b2b5-47d6-9d76-985797a5a818.webp".to_string(),
2960            icon_path: "e0bcf4ea-b2b5-47d6-9d76-985797a5a818.webp".to_string(),
2961            square_icon_path: "e0bcf4ea-b2b5-47d6-9d76-985797a5a818.webp".to_string(),
2962            eff: 1.0,
2963        },
2964    ]
2965}
2966
2967fn generate_ability_levels() -> Vec<AbilityLevel> {
2968    vec![
2969        AbilityLevel {
2970            level: 1,
2971            rarity_id: uuid!("33a0af72-2390-4df9-bd4f-f66b27ed7792"),
2972            required_shards: 5,
2973        },
2974        AbilityLevel {
2975            level: 2,
2976            rarity_id: uuid!("33a0af72-2390-4df9-bd4f-f66b27ed7792"),
2977            required_shards: 10,
2978        },
2979        AbilityLevel {
2980            level: 3,
2981            rarity_id: uuid!("33a0af72-2390-4df9-bd4f-f66b27ed7792"),
2982            required_shards: 10,
2983        },
2984        AbilityLevel {
2985            level: 1,
2986            rarity_id: uuid!("6f0f1099-8ab1-4378-aea5-ac6bcbe6ec3e"),
2987            required_shards: 5,
2988        },
2989        AbilityLevel {
2990            level: 2,
2991            rarity_id: uuid!("6f0f1099-8ab1-4378-aea5-ac6bcbe6ec3e"),
2992            required_shards: 10,
2993        },
2994        AbilityLevel {
2995            level: 3,
2996            rarity_id: uuid!("6f0f1099-8ab1-4378-aea5-ac6bcbe6ec3e"),
2997            required_shards: 15,
2998        },
2999        AbilityLevel {
3000            level: 4,
3001            rarity_id: uuid!("6f0f1099-8ab1-4378-aea5-ac6bcbe6ec3e"),
3002            required_shards: 100,
3003        },
3004        AbilityLevel {
3005            level: 1,
3006            rarity_id: uuid!("81b082cc-2736-4545-87d1-301f5de8951a"),
3007            required_shards: 5,
3008        },
3009        AbilityLevel {
3010            level: 2,
3011            rarity_id: uuid!("81b082cc-2736-4545-87d1-301f5de8951a"),
3012            required_shards: 10,
3013        },
3014        AbilityLevel {
3015            level: 3,
3016            rarity_id: uuid!("81b082cc-2736-4545-87d1-301f5de8951a"),
3017            required_shards: 15,
3018        },
3019    ]
3020}
3021
3022fn generate_fight_settings() -> FightSettings {
3023    FightSettings {
3024        start_fight_delay_ticks_default: 2000,
3025        prepare_fight_win_delay_ticks_default: 1000,
3026        prepare_fight_lose_delay_ticks_default: 1000,
3027        end_fight_delay_ticks_default: 2000,
3028        player_start_position: Coordinates { x: 1, y: 1 },
3029        party_start_position: Coordinates { x: 1, y: 0 },
3030        pvp_enemy_start_position: Coordinates { x: 4, y: 1 },
3031        ally_run_speed_mult: 1.0,
3032        wave_entrance_offset_cells: 4,
3033        wave_entrance_rush_ms_per_cell: 200,
3034        wave_entrance_walk_ms_per_cell: 500,
3035        formation_advance_ticks: 1150,
3036        // 2.0 keeps the escalation test math round: 17000 → 34000 → 68000.
3037    }
3038}
3039
3040fn generate_entities() -> Vec<EntityTemplate> {
3041    vec![
3042        EntityTemplate {
3043            id: uuid!("969f80ff-cf60-45e5-be6a-036911309ade"),
3044            name: "forest_goblin".into(),
3045            spine: "fe551450-4b55-4641-b376-a0184291fa05".into(),
3046            spine_skin_path: "Goblin:default".into(),
3047            spine_scale: 1,
3048            cast_time: 500,
3049            width: 1,
3050            ability_ids: vec![uuid!("50260b1c-7748-49bf-a1ed-34d69d029697")],
3051            attributes: vec![
3052                EntityAttribute {
3053                    attribute_id: uuid!("45eca0a7-7430-487b-bd65-b796c6d88c08"),
3054                    value: 6,
3055                },
3056                EntityAttribute {
3057                    attribute_id: uuid!("6ad485d7-9567-4b77-8ddc-a2418dd1dfe6"),
3058                    value: 5,
3059                },
3060                EntityAttribute {
3061                    attribute_id: uuid!("42aea610-2ab7-41f7-95bf-5d6f00341ac9"),
3062                    value: 0,
3063                },
3064            ],
3065            rewards: vec![EnemyReward {
3066                currency_id: uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
3067                from: 100,
3068                to: 100,
3069                drop_chance: 100.0,
3070            }],
3071            is_boss: false,
3072        },
3073        EntityTemplate {
3074            id: uuid!("bcc325dd-dc9a-4c30-b3c7-bfefdeec9a5f"),
3075            name: "shadow_wolf".into(),
3076            spine: "".into(),
3077            spine_skin_path: "Goblin:default".into(),
3078            spine_scale: 1,
3079            cast_time: 500,
3080            width: 1,
3081            ability_ids: vec![uuid!("50260b1c-7748-49bf-a1ed-34d69d029697")],
3082            attributes: vec![
3083                EntityAttribute {
3084                    attribute_id: uuid!("45eca0a7-7430-487b-bd65-b796c6d88c08"),
3085                    value: 4,
3086                },
3087                EntityAttribute {
3088                    attribute_id: uuid!("6ad485d7-9567-4b77-8ddc-a2418dd1dfe6"),
3089                    value: 8,
3090                },
3091                EntityAttribute {
3092                    attribute_id: uuid!("42aea610-2ab7-41f7-95bf-5d6f00341ac9"),
3093                    value: 1,
3094                },
3095            ],
3096            rewards: vec![EnemyReward {
3097                currency_id: uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
3098                from: 100,
3099                to: 100,
3100                drop_chance: 100.0,
3101            }],
3102            is_boss: false,
3103        },
3104        EntityTemplate {
3105            id: uuid!("94b110ce-a22c-461a-ab78-8a928ac1fad0"),
3106            name: "stone_giant".into(),
3107            spine: "".into(),
3108            spine_skin_path: "Goblin:default".into(),
3109            spine_scale: 1,
3110            cast_time: 500,
3111            width: 1,
3112            ability_ids: vec![uuid!("7e584338-057d-41a2-a80e-8e2c76e98f6a")],
3113            attributes: vec![
3114                EntityAttribute {
3115                    attribute_id: uuid!("45eca0a7-7430-487b-bd65-b796c6d88c08"),
3116                    value: 10,
3117                },
3118                EntityAttribute {
3119                    attribute_id: uuid!("6ad485d7-9567-4b77-8ddc-a2418dd1dfe6"),
3120                    value: 2,
3121                },
3122                EntityAttribute {
3123                    attribute_id: uuid!("42aea610-2ab7-41f7-95bf-5d6f00341ac9"),
3124                    value: 5,
3125                },
3126            ],
3127            rewards: vec![EnemyReward {
3128                currency_id: uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
3129                from: 100,
3130                to: 100,
3131                drop_chance: 100.0,
3132            }],
3133            is_boss: false,
3134        },
3135        EntityTemplate {
3136            id: uuid!("d414698e-afbd-4cd8-a594-901e8dcef231"),
3137            name: "fire_dragon".into(),
3138            spine: "".into(),
3139            spine_skin_path: "Goblin:default".into(),
3140            spine_scale: 1,
3141            cast_time: 500,
3142            width: 1,
3143            ability_ids: vec![
3144                uuid!("487a23e5-7401-4c34-a6c6-2401f95249cf"),
3145                uuid!("fd053ec6-8b23-4ba1-a31f-31b56d8c57bc"),
3146            ],
3147            attributes: vec![
3148                EntityAttribute {
3149                    attribute_id: uuid!("45eca0a7-7430-487b-bd65-b796c6d88c08"),
3150                    value: 45,
3151                },
3152                EntityAttribute {
3153                    attribute_id: uuid!("6ad485d7-9567-4b77-8ddc-a2418dd1dfe6"),
3154                    value: 4,
3155                },
3156                EntityAttribute {
3157                    attribute_id: uuid!("42aea610-2ab7-41f7-95bf-5d6f00341ac9"),
3158                    value: 7,
3159                },
3160            ],
3161            rewards: vec![EnemyReward {
3162                currency_id: uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
3163                from: 100,
3164                to: 100,
3165                drop_chance: 100.0,
3166            }],
3167            is_boss: false,
3168        },
3169        EntityTemplate {
3170            id: uuid!("447000b9-e155-4b74-82e3-df481ffc8136"),
3171            name: "phantom_assassin".into(),
3172            spine: "".into(),
3173            spine_skin_path: "Goblin:default".into(),
3174            spine_scale: 1,
3175            cast_time: 500,
3176            width: 1,
3177            ability_ids: vec![
3178                uuid!("da6c582b-7364-40bd-9b2d-946d8e20eaac"),
3179                uuid!("7e584338-057d-41a2-a80e-8e2c76e98f6a"),
3180                uuid!("50260b1c-7748-49bf-a1ed-34d69d029697"),
3181            ],
3182            attributes: vec![
3183                EntityAttribute {
3184                    attribute_id: uuid!("45eca0a7-7430-487b-bd65-b796c6d88c08"),
3185                    value: 6,
3186                },
3187                EntityAttribute {
3188                    attribute_id: uuid!("6ad485d7-9567-4b77-8ddc-a2418dd1dfe6"),
3189                    value: 10,
3190                },
3191                EntityAttribute {
3192                    attribute_id: uuid!("42aea610-2ab7-41f7-95bf-5d6f00341ac9"),
3193                    value: 2,
3194                },
3195            ],
3196            rewards: vec![EnemyReward {
3197                currency_id: uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
3198                from: 100,
3199                to: 100,
3200                drop_chance: 100.0,
3201            }],
3202            is_boss: false,
3203        },
3204        EntityTemplate {
3205            id: uuid!("0195e7d2-5ae7-70e4-88ec-b9417813b007"),
3206            name: "mini_roshan".into(),
3207            spine: "".into(),
3208            spine_skin_path: "Goblin:default".into(),
3209            spine_scale: 1,
3210            cast_time: 500,
3211            width: 1,
3212            ability_ids: vec![
3213                uuid!("da6c582b-7364-40bd-9b2d-946d8e20eaac"),
3214                uuid!("7e584338-057d-41a2-a80e-8e2c76e98f6a"),
3215            ],
3216            attributes: vec![
3217                EntityAttribute {
3218                    attribute_id: uuid!("45eca0a7-7430-487b-bd65-b796c6d88c08"),
3219                    value: 150,
3220                },
3221                EntityAttribute {
3222                    attribute_id: uuid!("6ad485d7-9567-4b77-8ddc-a2418dd1dfe6"),
3223                    value: 150,
3224                },
3225                EntityAttribute {
3226                    attribute_id: uuid!("42aea610-2ab7-41f7-95bf-5d6f00341ac9"),
3227                    value: 150,
3228                },
3229            ],
3230            rewards: vec![EnemyReward {
3231                currency_id: uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
3232                from: 500,
3233                to: 500,
3234                drop_chance: 100.0,
3235            }],
3236            is_boss: false,
3237        },
3238        EntityTemplate {
3239            id: uuid!("0195e7ce-ba97-7f6b-9ac6-0de3c160cde7"),
3240            name: "roshan".into(),
3241            spine: "".into(),
3242            spine_skin_path: "Goblin:default".into(),
3243            spine_scale: 1,
3244            cast_time: 500,
3245            width: 1,
3246            ability_ids: vec![
3247                uuid!("da6c582b-7364-40bd-9b2d-946d8e20eaac"),
3248                uuid!("7e584338-057d-41a2-a80e-8e2c76e98f6a"),
3249                uuid!("50260b1c-7748-49bf-a1ed-34d69d029697"),
3250            ],
3251            attributes: vec![
3252                EntityAttribute {
3253                    attribute_id: uuid!("45eca0a7-7430-487b-bd65-b796c6d88c08"),
3254                    value: 300,
3255                },
3256                EntityAttribute {
3257                    attribute_id: uuid!("6ad485d7-9567-4b77-8ddc-a2418dd1dfe6"),
3258                    value: 300,
3259                },
3260                EntityAttribute {
3261                    attribute_id: uuid!("42aea610-2ab7-41f7-95bf-5d6f00341ac9"),
3262                    value: 300,
3263                },
3264            ],
3265            rewards: vec![EnemyReward {
3266                currency_id: uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
3267                from: 1000,
3268                to: 1000,
3269                drop_chance: 100.0,
3270            }],
3271            is_boss: false,
3272        },
3273        EntityTemplate {
3274            id: uuid!("0486f548-5040-4b70-b202-8b35a9880939"),
3275            name: "spawner".into(),
3276            spine: "".into(),
3277            spine_skin_path: "Goblin:default".into(),
3278            spine_scale: 1,
3279            cast_time: 500,
3280            width: 1,
3281            ability_ids: vec![],
3282            attributes: vec![
3283                EntityAttribute {
3284                    attribute_id: uuid!("45eca0a7-7430-487b-bd65-b796c6d88c08"),
3285                    value: 5,
3286                },
3287                EntityAttribute {
3288                    attribute_id: uuid!("6ad485d7-9567-4b77-8ddc-a2418dd1dfe6"),
3289                    value: 5,
3290                },
3291                EntityAttribute {
3292                    attribute_id: uuid!("42aea610-2ab7-41f7-95bf-5d6f00341ac9"),
3293                    value: 5,
3294                },
3295            ],
3296            rewards: vec![EnemyReward {
3297                currency_id: uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
3298                from: 1000,
3299                to: 1000,
3300                drop_chance: 100.0,
3301            }],
3302            is_boss: false,
3303        },
3304    ]
3305}
3306
3307fn generate_fight_templates() -> Vec<FightTemplate> {
3308    vec![
3309        FightTemplate {
3310            id: uuid!("360fc654-dea1-4f5d-97af-249ab30608ac"),
3311            title: i18n::I18nString::new(
3312                "content.quests.e3bc23d8-2035-44d0-b8c1-bb1996ac6bc5:title",
3313            ),
3314            power: Some(300),
3315            fight_entities: vec![FightEntity {
3316                entity_type: EntityType::PVPEntity,
3317                position: Coordinates { x: 3, y: 1 },
3318                has_big_hp_bar: false,
3319                team: EntityTeam::Enemy,
3320            }],
3321            background: "".into(),
3322            max_duration_ticks: 300000,
3323            starting_fx: "".to_string(),
3324            fight_type: FightType::ArenaPVP,
3325            waves_amount: 1,
3326            prepare_fight_waves: None,
3327            start_behavior: Some("noop".to_string()),
3328            prepare_fight_win_duration_ticks: None,
3329            prepare_fight_lose_duration_ticks: None,
3330            start_fight_delay_ticks: None,
3331            end_fight_delay_ticks: None,
3332            bundle_reward_id: Some(uuid!("53e8ffaa-7ee4-4612-bbfc-bb157979ca8d")),
3333            stop_on_win: false,
3334            stop_on_lose: false,
3335            show_vs_screen: false,
3336            show_stages: None,
3337            target_width_cells: 6,
3338            is_dungeon: false,
3339            is_bossfight: false,
3340        },
3341        FightTemplate {
3342            id: uuid!("b92c69a2-6544-4b83-809f-5af290b75473"),
3343            title: i18n::I18nString::new(
3344                "content.quests.e3bc23d8-2035-44d0-b8c1-bb1996ac6bc5:title",
3345            ),
3346            power: Some(300),
3347            fight_entities: vec![FightEntity {
3348                entity_type: EntityType::PVPEntity,
3349                position: Coordinates { x: 3, y: 1 },
3350                has_big_hp_bar: false,
3351                team: EntityTeam::Enemy,
3352            }],
3353            background: "".into(),
3354            max_duration_ticks: 300000,
3355            starting_fx: "".to_string(),
3356            fight_type: FightType::VassalPVP,
3357            waves_amount: 1,
3358            prepare_fight_waves: None,
3359            start_behavior: Some("noop".to_string()),
3360            prepare_fight_win_duration_ticks: None,
3361            prepare_fight_lose_duration_ticks: None,
3362            start_fight_delay_ticks: None,
3363            end_fight_delay_ticks: None,
3364            bundle_reward_id: Some(uuid!("53e8ffaa-7ee4-4612-bbfc-bb157979ca8d")),
3365            stop_on_win: false,
3366            stop_on_lose: false,
3367            show_vs_screen: false,
3368            show_stages: None,
3369            target_width_cells: 6,
3370            is_dungeon: false,
3371            is_bossfight: false,
3372        },
3373        FightTemplate {
3374            id: uuid!("b9d46e46-e8aa-4f73-9b67-694f567f1c48"),
3375            title: i18n::I18nString::new(
3376                "content.quests.e3bc23d8-2035-44d0-b8c1-bb1996ac6bc5:title",
3377            ),
3378            power: Some(300),
3379            fight_entities: vec![
3380                FightEntity {
3381                    entity_type: EntityType::PVEEntity {
3382                        entity_template_id: uuid!("969f80ff-cf60-45e5-be6a-036911309ade"),
3383                    },
3384                    position: Coordinates { x: 3, y: 1 },
3385                    has_big_hp_bar: false,
3386                    team: EntityTeam::Enemy,
3387                },
3388                FightEntity {
3389                    entity_type: EntityType::PVEEntity {
3390                        entity_template_id: uuid!("969f80ff-cf60-45e5-be6a-036911309ade"),
3391                    },
3392                    position: Coordinates { x: 3, y: 2 },
3393                    has_big_hp_bar: false,
3394                    team: EntityTeam::Enemy,
3395                },
3396            ],
3397            background: "".into(),
3398            max_duration_ticks: 300000,
3399            starting_fx: "".to_string(),
3400            fight_type: FightType::CampaignFight,
3401            waves_amount: 1,
3402            prepare_fight_waves: None,
3403            start_behavior: Some("noop".to_string()),
3404            prepare_fight_win_duration_ticks: None,
3405            prepare_fight_lose_duration_ticks: None,
3406            start_fight_delay_ticks: None,
3407            end_fight_delay_ticks: None,
3408            bundle_reward_id: None,
3409            stop_on_win: false,
3410            stop_on_lose: false,
3411            show_vs_screen: false,
3412            show_stages: None,
3413            target_width_cells: 6,
3414            is_dungeon: false,
3415            is_bossfight: false,
3416        },
3417        FightTemplate {
3418            id: uuid!("44ca267b-7b6f-4508-ac74-a17b9dbeed99"),
3419            title: i18n::I18nString::new(
3420                "content.quests.e3bc23d8-2035-44d0-b8c1-bb1996ac6bc5:title",
3421            ),
3422            power: Some(300),
3423            fight_entities: vec![FightEntity {
3424                entity_type: EntityType::PVEEntity {
3425                    entity_template_id: uuid!("bcc325dd-dc9a-4c30-b3c7-bfefdeec9a5f"),
3426                },
3427                position: Coordinates { x: 3, y: 1 },
3428                has_big_hp_bar: false,
3429                team: EntityTeam::Enemy,
3430            }],
3431            background: "".into(),
3432            max_duration_ticks: 300000,
3433            starting_fx: "".to_string(),
3434            fight_type: FightType::CampaignFight,
3435            waves_amount: 1,
3436            prepare_fight_waves: None,
3437            start_behavior: Some("noop".to_string()),
3438            prepare_fight_win_duration_ticks: None,
3439            prepare_fight_lose_duration_ticks: None,
3440            start_fight_delay_ticks: None,
3441            end_fight_delay_ticks: None,
3442            bundle_reward_id: None,
3443            stop_on_win: false,
3444            stop_on_lose: false,
3445            show_vs_screen: false,
3446            show_stages: None,
3447            target_width_cells: 6,
3448            is_dungeon: false,
3449            is_bossfight: false,
3450        },
3451        FightTemplate {
3452            id: uuid!("26f23346-541f-4a74-9ae0-efc3523b0ebf"),
3453            title: i18n::I18nString::new(
3454                "content.quests.e3bc23d8-2035-44d0-b8c1-bb1996ac6bc5:title",
3455            ),
3456            power: Some(300),
3457            fight_entities: vec![
3458                FightEntity {
3459                    entity_type: EntityType::PVEEntity {
3460                        entity_template_id: uuid!("bcc325dd-dc9a-4c30-b3c7-bfefdeec9a5f"),
3461                    },
3462                    position: Coordinates { x: 2, y: 1 },
3463                    has_big_hp_bar: false,
3464                    team: EntityTeam::Enemy,
3465                },
3466                FightEntity {
3467                    entity_type: EntityType::PVEEntity {
3468                        entity_template_id: uuid!("94b110ce-a22c-461a-ab78-8a928ac1fad0"),
3469                    },
3470                    position: Coordinates { x: 2, y: 2 },
3471                    has_big_hp_bar: true,
3472                    team: EntityTeam::Enemy,
3473                },
3474            ],
3475            background: "".into(),
3476            max_duration_ticks: 300000,
3477            starting_fx: "".to_string(),
3478            fight_type: FightType::CampaignBossFight,
3479            waves_amount: 1,
3480            prepare_fight_waves: None,
3481            start_behavior: Some("noop".to_string()),
3482            prepare_fight_win_duration_ticks: None,
3483            prepare_fight_lose_duration_ticks: None,
3484            start_fight_delay_ticks: None,
3485            end_fight_delay_ticks: None,
3486            bundle_reward_id: Some(uuid!("53e8ffaa-7ee4-4612-bbfc-bb157979ca8d")),
3487            stop_on_win: false,
3488            stop_on_lose: false,
3489            show_vs_screen: false,
3490            show_stages: None,
3491            target_width_cells: 6,
3492            is_dungeon: false,
3493            is_bossfight: false,
3494        },
3495        FightTemplate {
3496            id: uuid!("f32d7a64-23b4-4d3f-bc31-801ae0f9acda"),
3497            title: i18n::I18nString::new(
3498                "content.quests.e3bc23d8-2035-44d0-b8c1-bb1996ac6bc5:title",
3499            ),
3500            power: Some(300),
3501            fight_entities: vec![
3502                FightEntity {
3503                    entity_type: EntityType::PVEEntity {
3504                        entity_template_id: uuid!("969f80ff-cf60-45e5-be6a-036911309ade"),
3505                    },
3506                    position: Coordinates { x: 3, y: 1 },
3507                    has_big_hp_bar: false,
3508                    team: EntityTeam::Enemy,
3509                },
3510                FightEntity {
3511                    entity_type: EntityType::PVEEntity {
3512                        entity_template_id: uuid!("94b110ce-a22c-461a-ab78-8a928ac1fad0"),
3513                    },
3514                    position: Coordinates { x: 3, y: 2 },
3515                    has_big_hp_bar: false,
3516                    team: EntityTeam::Enemy,
3517                },
3518            ],
3519            background: "".into(),
3520            max_duration_ticks: 300000,
3521            starting_fx: "".to_string(),
3522            fight_type: FightType::CampaignFight,
3523            waves_amount: 1,
3524            prepare_fight_waves: None,
3525            start_behavior: Some("damage_first_entity_5".to_string()),
3526            prepare_fight_win_duration_ticks: None,
3527            prepare_fight_lose_duration_ticks: None,
3528            start_fight_delay_ticks: None,
3529            end_fight_delay_ticks: None,
3530            bundle_reward_id: None,
3531            stop_on_win: false,
3532            stop_on_lose: false,
3533            show_vs_screen: false,
3534            show_stages: None,
3535            target_width_cells: 6,
3536            is_dungeon: false,
3537            is_bossfight: false,
3538        },
3539        FightTemplate {
3540            id: uuid!("83f96aa7-1553-445f-b0d4-4bb1a13c68a1"),
3541            title: i18n::I18nString::new(
3542                "content.quests.e3bc23d8-2035-44d0-b8c1-bb1996ac6bc5:title",
3543            ),
3544            power: Some(300),
3545            fight_entities: vec![
3546                FightEntity {
3547                    entity_type: EntityType::PVEEntity {
3548                        entity_template_id: uuid!("94b110ce-a22c-461a-ab78-8a928ac1fad0"),
3549                    },
3550                    position: Coordinates { x: 3, y: 1 },
3551                    has_big_hp_bar: false,
3552                    team: EntityTeam::Enemy,
3553                },
3554                FightEntity {
3555                    entity_type: EntityType::PVEEntity {
3556                        entity_template_id: uuid!("d414698e-afbd-4cd8-a594-901e8dcef231"),
3557                    },
3558                    position: Coordinates { x: 3, y: 2 },
3559                    has_big_hp_bar: false,
3560                    team: EntityTeam::Enemy,
3561                },
3562            ],
3563            background: "".into(),
3564            max_duration_ticks: 300000,
3565            starting_fx: "".to_string(),
3566            fight_type: FightType::CampaignFight,
3567            waves_amount: 1,
3568            prepare_fight_waves: None,
3569            start_behavior: Some("noop".to_string()),
3570            prepare_fight_win_duration_ticks: None,
3571            prepare_fight_lose_duration_ticks: None,
3572            start_fight_delay_ticks: None,
3573            end_fight_delay_ticks: None,
3574            bundle_reward_id: None,
3575            stop_on_win: false,
3576            stop_on_lose: false,
3577            show_vs_screen: false,
3578            show_stages: None,
3579            target_width_cells: 6,
3580            is_dungeon: false,
3581            is_bossfight: false,
3582        },
3583        FightTemplate {
3584            id: uuid!("a57fe553-3506-4211-a871-d199165a0f6a"),
3585            title: i18n::I18nString::new(
3586                "content.quests.e3bc23d8-2035-44d0-b8c1-bb1996ac6bc5:title",
3587            ),
3588            power: Some(300),
3589            fight_entities: vec![
3590                FightEntity {
3591                    entity_type: EntityType::PVEEntity {
3592                        entity_template_id: uuid!("94b110ce-a22c-461a-ab78-8a928ac1fad0"),
3593                    },
3594                    position: Coordinates { x: 2, y: 1 },
3595                    has_big_hp_bar: false,
3596                    team: EntityTeam::Enemy,
3597                },
3598                FightEntity {
3599                    entity_type: EntityType::PVEEntity {
3600                        entity_template_id: uuid!("0195e7d2-5ae7-70e4-88ec-b9417813b007"),
3601                    },
3602                    position: Coordinates { x: 2, y: 2 },
3603                    has_big_hp_bar: true,
3604                    team: EntityTeam::Enemy,
3605                },
3606            ],
3607            background: "".into(),
3608            max_duration_ticks: 300000,
3609            starting_fx: "".to_string(),
3610            fight_type: FightType::CampaignBossFight,
3611            waves_amount: 1,
3612            prepare_fight_waves: None,
3613            start_behavior: Some("noop".to_string()),
3614            prepare_fight_win_duration_ticks: None,
3615            prepare_fight_lose_duration_ticks: None,
3616            start_fight_delay_ticks: None,
3617            end_fight_delay_ticks: None,
3618            bundle_reward_id: None,
3619            stop_on_win: false,
3620            stop_on_lose: false,
3621            show_vs_screen: false,
3622            show_stages: None,
3623            target_width_cells: 6,
3624            is_dungeon: false,
3625            is_bossfight: false,
3626        },
3627        FightTemplate {
3628            id: uuid!("c274b0bb-173e-49e3-9d36-2c32f1e2c8a1"),
3629            title: i18n::I18nString::new(
3630                "content.quests.e3bc23d8-2035-44d0-b8c1-bb1996ac6bc5:title",
3631            ),
3632            power: Some(300),
3633            fight_entities: vec![FightEntity {
3634                entity_type: EntityType::PVEEntity {
3635                    entity_template_id: uuid!("d414698e-afbd-4cd8-a594-901e8dcef231"),
3636                },
3637                position: Coordinates { x: 3, y: 1 },
3638                has_big_hp_bar: false,
3639                team: EntityTeam::Enemy,
3640            }],
3641            background: "".into(),
3642            max_duration_ticks: 300000,
3643            starting_fx: "".to_string(),
3644            fight_type: FightType::CampaignFight,
3645            waves_amount: 1,
3646            prepare_fight_waves: None,
3647            start_behavior: Some("noop".to_string()),
3648            prepare_fight_win_duration_ticks: None,
3649            prepare_fight_lose_duration_ticks: None,
3650            start_fight_delay_ticks: None,
3651            end_fight_delay_ticks: None,
3652            bundle_reward_id: None,
3653            stop_on_win: true,
3654            stop_on_lose: false,
3655            show_vs_screen: false,
3656            show_stages: None,
3657            target_width_cells: 6,
3658            is_dungeon: false,
3659            is_bossfight: false,
3660        },
3661        FightTemplate {
3662            id: uuid!("d02ac74e-5ad2-49c2-9a3e-927194c62adf"),
3663            title: i18n::I18nString::new(
3664                "content.quests.e3bc23d8-2035-44d0-b8c1-bb1996ac6bc5:title",
3665            ),
3666            power: Some(300),
3667            fight_entities: vec![
3668                FightEntity {
3669                    entity_type: EntityType::PVEEntity {
3670                        entity_template_id: uuid!("969f80ff-cf60-45e5-be6a-036911309ade"),
3671                    },
3672                    position: Coordinates { x: 3, y: 1 },
3673                    has_big_hp_bar: false,
3674                    team: EntityTeam::Enemy,
3675                },
3676                FightEntity {
3677                    entity_type: EntityType::PVEEntity {
3678                        entity_template_id: uuid!("bcc325dd-dc9a-4c30-b3c7-bfefdeec9a5f"),
3679                    },
3680                    position: Coordinates { x: 3, y: 2 },
3681                    has_big_hp_bar: false,
3682                    team: EntityTeam::Enemy,
3683                },
3684                FightEntity {
3685                    entity_type: EntityType::PVEEntity {
3686                        entity_template_id: uuid!("d414698e-afbd-4cd8-a594-901e8dcef231"),
3687                    },
3688                    position: Coordinates { x: 4, y: 1 },
3689                    has_big_hp_bar: false,
3690                    team: EntityTeam::Enemy,
3691                },
3692            ],
3693            background: "".into(),
3694            max_duration_ticks: 300000,
3695            starting_fx: "".to_string(),
3696            fight_type: FightType::CampaignFight,
3697            waves_amount: 1,
3698            prepare_fight_waves: None,
3699            start_behavior: Some("noop".to_string()),
3700            prepare_fight_win_duration_ticks: None,
3701            prepare_fight_lose_duration_ticks: None,
3702            start_fight_delay_ticks: None,
3703            end_fight_delay_ticks: None,
3704            bundle_reward_id: None,
3705            stop_on_win: false,
3706            stop_on_lose: true,
3707            show_vs_screen: false,
3708            show_stages: None,
3709            target_width_cells: 6,
3710            is_dungeon: false,
3711            is_bossfight: false,
3712        },
3713        FightTemplate {
3714            id: uuid!("1f2b6501-d76f-4ed9-a6b6-7057c3ee16dc"),
3715            title: i18n::I18nString::new(
3716                "content.quests.e3bc23d8-2035-44d0-b8c1-bb1996ac6bc5:title",
3717            ),
3718            power: Some(300),
3719            fight_entities: vec![
3720                FightEntity {
3721                    entity_type: EntityType::PVEEntity {
3722                        entity_template_id: uuid!("d414698e-afbd-4cd8-a594-901e8dcef231"),
3723                    },
3724                    position: Coordinates { x: 2, y: 1 },
3725                    has_big_hp_bar: false,
3726                    team: EntityTeam::Enemy,
3727                },
3728                FightEntity {
3729                    entity_type: EntityType::PVEEntity {
3730                        entity_template_id: uuid!("0195e7ce-ba97-7f6b-9ac6-0de3c160cde7"),
3731                    },
3732                    position: Coordinates { x: 2, y: 2 },
3733                    has_big_hp_bar: true,
3734                    team: EntityTeam::Enemy,
3735                },
3736            ],
3737            background: "".into(),
3738            max_duration_ticks: 300000,
3739            starting_fx: "".to_string(),
3740            fight_type: FightType::CampaignBossFight,
3741            waves_amount: 1,
3742            prepare_fight_waves: None,
3743            start_behavior: Some("noop".to_string()),
3744            prepare_fight_win_duration_ticks: None,
3745            prepare_fight_lose_duration_ticks: None,
3746            start_fight_delay_ticks: None,
3747            end_fight_delay_ticks: None,
3748            bundle_reward_id: None,
3749            stop_on_win: false,
3750            stop_on_lose: false,
3751            show_vs_screen: false,
3752            show_stages: None,
3753            target_width_cells: 6,
3754            is_dungeon: false,
3755            is_bossfight: false,
3756        },
3757        FightTemplate {
3758            id: uuid!("09207dc1-ef01-4222-8382-ee853770fc3d"),
3759            title: i18n::I18nString::new(
3760                "content.quests.e3bc23d8-2035-44d0-b8c1-bb1996ac6bc5:title",
3761            ),
3762            power: Some(300),
3763            fight_entities: vec![
3764                FightEntity {
3765                    entity_type: EntityType::PVEEntity {
3766                        entity_template_id: uuid!("0486f548-5040-4b70-b202-8b35a9880939"),
3767                    },
3768                    position: Coordinates { x: 2, y: 1 },
3769                    has_big_hp_bar: false,
3770                    team: EntityTeam::Enemy,
3771                },
3772                FightEntity {
3773                    entity_type: EntityType::PVEEntity {
3774                        entity_template_id: uuid!("0486f548-5040-4b70-b202-8b35a9880939"),
3775                    },
3776                    position: Coordinates { x: 2, y: 2 },
3777                    has_big_hp_bar: true,
3778                    team: EntityTeam::Enemy,
3779                },
3780            ],
3781            background: "".into(),
3782            max_duration_ticks: 300000,
3783            starting_fx: "".to_string(),
3784            fight_type: FightType::CampaignBossFight,
3785            waves_amount: 1,
3786            prepare_fight_waves: None,
3787            start_behavior: Some("apply_spawn_on_death_to_player".to_string()),
3788            prepare_fight_win_duration_ticks: None,
3789            prepare_fight_lose_duration_ticks: None,
3790            start_fight_delay_ticks: None,
3791            end_fight_delay_ticks: None,
3792            bundle_reward_id: None,
3793            stop_on_win: false,
3794            stop_on_lose: false,
3795            show_vs_screen: false,
3796            show_stages: None,
3797            target_width_cells: 6,
3798            is_dungeon: false,
3799            is_bossfight: false,
3800        },
3801        FightTemplate {
3802            id: uuid!("669c9bac-7734-457a-8619-1c5b65802ac8"),
3803            title: i18n::I18nString::new(
3804                "content.quests.e3bc23d8-2035-44d0-b8c1-bb1996ac6bc5:title",
3805            ),
3806            power: Some(300),
3807            fight_entities: vec![FightEntity {
3808                entity_type: EntityType::PVEEntity {
3809                    entity_template_id: uuid!("0486f548-5040-4b70-b202-8b35a9880939"),
3810                },
3811                position: Coordinates { x: 2, y: 1 },
3812                has_big_hp_bar: false,
3813                team: EntityTeam::Enemy,
3814            }],
3815            background: "".into(),
3816            max_duration_ticks: 300000,
3817            starting_fx: "".to_string(),
3818            fight_type: FightType::CampaignBossFight,
3819            waves_amount: 3,
3820            // Typed mirror of the bespoke `prepare_fight_script` spawn above, plus
3821            // the per-wave spawns this 3-wave boss fight needs. The native
3822            // wave 0 spawns the extra enemy at prepare time; waves 1 and 2 each
3823            // spawn one enemy when the previous wave is cleared.
3824            prepare_fight_waves: Some(PrepareFightWaves {
3825                entities: vec![PrepareFightEntityPower {
3826                    entity_id: Some("0486f548-5040-4b70-b202-8b35a9880939".to_string()),
3827                    power: Some(1.0),
3828                }],
3829                waves: vec![
3830                    vec![PrepareFightSpawn {
3831                        entity_id: "0486f548-5040-4b70-b202-8b35a9880939".to_string(),
3832                        delay: Some(0.0),
3833                        position: Some(Coordinates { x: 3, y: 3 }),
3834                        cooldown_seconds: None,
3835                        row: None,
3836                    }],
3837                    vec![PrepareFightSpawn {
3838                        entity_id: "0486f548-5040-4b70-b202-8b35a9880939".to_string(),
3839                        delay: Some(0.0),
3840                        position: Some(Coordinates { x: 3, y: 1 }),
3841                        cooldown_seconds: None,
3842                        row: None,
3843                    }],
3844                    vec![PrepareFightSpawn {
3845                        entity_id: "0486f548-5040-4b70-b202-8b35a9880939".to_string(),
3846                        delay: Some(0.0),
3847                        position: Some(Coordinates { x: 3, y: 1 }),
3848                        cooldown_seconds: None,
3849                        row: None,
3850                    }],
3851                ],
3852                time: 1.0,
3853                power: 100.0,
3854                stream_active_count: None,
3855                reward_mob_budget: None,
3856                enemy_damage_mult: None,
3857                summon_wave_at_hp_fraction: None,
3858            }),
3859            start_behavior: Some("noop".to_string()),
3860            prepare_fight_win_duration_ticks: None,
3861            prepare_fight_lose_duration_ticks: None,
3862            start_fight_delay_ticks: None,
3863            end_fight_delay_ticks: None,
3864            bundle_reward_id: None,
3865            stop_on_win: false,
3866            stop_on_lose: false,
3867            show_vs_screen: false,
3868            show_stages: None,
3869            target_width_cells: 6,
3870            is_dungeon: false,
3871            is_bossfight: false,
3872        },
3873        // Boss-summon fixture: wave 1 = the boss, wave 2 = the SUMMON wave
3874        // (fires when the boss drops below 30% max HP, never on wave clear).
3875        FightTemplate {
3876            id: uuid!("7a1e5f30-99a1-4d69-8a41-2b5c00551111"),
3877            title: i18n::I18nString::new(
3878                "content.quests.e3bc23d8-2035-44d0-b8c1-bb1996ac6bc5:title",
3879            ),
3880            power: Some(300),
3881            fight_entities: vec![FightEntity {
3882                entity_type: EntityType::PVEEntity {
3883                    entity_template_id: uuid!("0486f548-5040-4b70-b202-8b35a9880939"),
3884                },
3885                position: Coordinates { x: 2, y: 1 },
3886                has_big_hp_bar: true,
3887                team: EntityTeam::Enemy,
3888            }],
3889            background: "".into(),
3890            max_duration_ticks: 300000,
3891            starting_fx: "".to_string(),
3892            fight_type: FightType::CampaignBossFight,
3893            waves_amount: 2,
3894            prepare_fight_waves: Some(PrepareFightWaves {
3895                entities: vec![PrepareFightEntityPower {
3896                    entity_id: Some("0486f548-5040-4b70-b202-8b35a9880939".to_string()),
3897                    power: Some(1.0),
3898                }],
3899                waves: vec![
3900                    vec![PrepareFightSpawn {
3901                        entity_id: "0486f548-5040-4b70-b202-8b35a9880939".to_string(),
3902                        delay: Some(0.0),
3903                        position: Some(Coordinates { x: 3, y: 1 }),
3904                        cooldown_seconds: None,
3905                        row: None,
3906                    }],
3907                    vec![
3908                        PrepareFightSpawn {
3909                            entity_id: "0486f548-5040-4b70-b202-8b35a9880939".to_string(),
3910                            delay: Some(0.0),
3911                            position: Some(Coordinates { x: 3, y: 0 }),
3912                            cooldown_seconds: None,
3913                            row: Some(0),
3914                        },
3915                        PrepareFightSpawn {
3916                            entity_id: "0486f548-5040-4b70-b202-8b35a9880939".to_string(),
3917                            delay: Some(0.0),
3918                            position: Some(Coordinates { x: 3, y: 2 }),
3919                            cooldown_seconds: None,
3920                            row: Some(2),
3921                        },
3922                    ],
3923                ],
3924                time: 1.0,
3925                power: 100.0,
3926                stream_active_count: None,
3927                reward_mob_budget: None,
3928                enemy_damage_mult: None,
3929                summon_wave_at_hp_fraction: Some(0.3),
3930            }),
3931            start_behavior: Some("noop".to_string()),
3932            prepare_fight_win_duration_ticks: None,
3933            prepare_fight_lose_duration_ticks: None,
3934            start_fight_delay_ticks: None,
3935            end_fight_delay_ticks: None,
3936            bundle_reward_id: None,
3937            stop_on_win: false,
3938            stop_on_lose: false,
3939            show_vs_screen: false,
3940            show_stages: None,
3941            target_width_cells: 6,
3942            is_dungeon: false,
3943            is_bossfight: true,
3944        },
3945        FightTemplate {
3946            id: uuid!("bceb9758-a0e1-4f4c-90e9-f841e412baa2"),
3947            title: i18n::I18nString::new(
3948                "content.quests.e3bc23d8-2035-44d0-b8c1-bb1996ac6bc5:title",
3949            ),
3950            power: Some(300),
3951            fight_entities: vec![FightEntity {
3952                entity_type: EntityType::PVEEntity {
3953                    entity_template_id: uuid!("0486f548-5040-4b70-b202-8b35a9880939"),
3954                },
3955                position: Coordinates { x: 2, y: 1 },
3956                has_big_hp_bar: false,
3957                team: EntityTeam::Enemy,
3958            }],
3959            background: "".into(),
3960            max_duration_ticks: 300000,
3961            starting_fx: "".to_string(),
3962            fight_type: FightType::CampaignBossFight,
3963            waves_amount: 1,
3964            prepare_fight_waves: None,
3965            start_behavior: Some("noop".to_string()),
3966            prepare_fight_win_duration_ticks: Some(1),
3967            prepare_fight_lose_duration_ticks: Some(1),
3968            start_fight_delay_ticks: Some(1),
3969            end_fight_delay_ticks: Some(1),
3970            bundle_reward_id: Some(uuid!("53e8ffaa-7ee4-4612-bbfc-bb157979ca8d")),
3971            stop_on_win: false,
3972            stop_on_lose: false,
3973            show_vs_screen: false,
3974            show_stages: None,
3975            target_width_cells: 6,
3976            is_dungeon: false,
3977            is_bossfight: false,
3978        },
3979        FightTemplate {
3980            id: uuid!("3ba961a4-6eab-4251-a048-1edef27da219"),
3981            title: i18n::I18nString::new(
3982                "content.quests.e3bc23d8-2035-44d0-b8c1-bb1996ac6bc5:title",
3983            ),
3984            power: Some(300),
3985            fight_entities: vec![FightEntity {
3986                entity_type: EntityType::PVEEntity {
3987                    entity_template_id: uuid!("0486f548-5040-4b70-b202-8b35a9880939"),
3988                },
3989                position: Coordinates { x: 2, y: 1 },
3990                has_big_hp_bar: false,
3991                team: EntityTeam::Enemy,
3992            }],
3993            background: "".into(),
3994            max_duration_ticks: 300000,
3995            starting_fx: "".to_string(),
3996            fight_type: FightType::CampaignBossFight,
3997            waves_amount: 2,
3998            prepare_fight_waves: None,
3999            start_behavior: Some("noop".to_string()),
4000            prepare_fight_win_duration_ticks: Some(1),
4001            prepare_fight_lose_duration_ticks: Some(1),
4002            start_fight_delay_ticks: Some(1),
4003            end_fight_delay_ticks: Some(1),
4004            bundle_reward_id: Some(uuid!("53e8ffaa-7ee4-4612-bbfc-bb157979ca8d")),
4005            stop_on_win: false,
4006            stop_on_lose: false,
4007            show_vs_screen: false,
4008            show_stages: None,
4009            target_width_cells: 6,
4010            is_dungeon: false,
4011            is_bossfight: false,
4012        },
4013    ]
4014}
4015
4016fn generate_dungeon_templates() -> Vec<DungeonTemplate> {
4017    vec![DungeonTemplate {
4018        id: uuid!("cfa28006-f242-47dc-9f69-e12587ea54fb"),
4019        title: i18n::I18nString::new("content.quests.e3bc23d8-2035-44d0-b8c1-bb1996ac6bc5:title"),
4020        description: i18n::I18nString::new(
4021            "content.quests.e3bc23d8-2035-44d0-b8c1-bb1996ac6bc5:title",
4022        ),
4023        key_currency_id: uuid!("85d98a50-510e-4853-b00a-84a51ad5cb50"),
4024        max_difficulty_level: 2,
4025        ui_background_path: "".to_string(),
4026        ui_banner_path: "".to_string(),
4027        tips: vec![
4028            DungeonTip {
4029                difficulty_level: 1,
4030                tip: i18n::I18nString::new(
4031                    "content.quests.e3bc23d8-2035-44d0-b8c1-bb1996ac6bc5:title",
4032                ),
4033            },
4034            DungeonTip {
4035                difficulty_level: 2,
4036                tip: i18n::I18nString::new(
4037                    "content.quests.e3bc23d8-2035-44d0-b8c1-bb1996ac6bc5:title",
4038                ),
4039            },
4040        ],
4041        fight_template_ids: vec![
4042            uuid!("bceb9758-a0e1-4f4c-90e9-f841e412baa2"),
4043            uuid!("bceb9758-a0e1-4f4c-90e9-f841e412baa2"),
4044        ],
4045        rewards_by_difficulty: vec![],
4046        main_reward: RewardType::Currency(uuid!("2ed77d25-cc1e-41ee-8f46-c4934ca8f684")),
4047        chapter_level_unlock: 2,
4048        between_wave_spawn_delay_ticks: None,
4049        advance_formation_between_waves: None,
4050    }]
4051}
4052
4053fn generate_chapters() -> Vec<Chapter> {
4054    vec![
4055        Chapter {
4056            id: uuid!("9a51675f-9aa7-4f2b-9843-f2ce0cf8e6fd"),
4057            title: i18n::I18nString::new(
4058                "content.quests.e3bc23d8-2035-44d0-b8c1-bb1996ac6bc5:title",
4059            ),
4060            level: 0,
4061            fight_ids: vec![
4062                uuid!("b9d46e46-e8aa-4f73-9b67-694f567f1c48"),
4063                uuid!("44ca267b-7b6f-4508-ac74-a17b9dbeed99"),
4064                uuid!("26f23346-541f-4a74-9ae0-efc3523b0ebf"),
4065            ],
4066        },
4067        Chapter {
4068            id: uuid!("a073ff29-425d-47d2-93e2-e254b5da3ec3"),
4069            title: i18n::I18nString::new(
4070                "content.quests.e3bc23d8-2035-44d0-b8c1-bb1996ac6bc5:title",
4071            ),
4072            level: 1,
4073            fight_ids: vec![
4074                uuid!("f32d7a64-23b4-4d3f-bc31-801ae0f9acda"),
4075                uuid!("83f96aa7-1553-445f-b0d4-4bb1a13c68a1"),
4076                uuid!("a57fe553-3506-4211-a871-d199165a0f6a"),
4077            ],
4078        },
4079        Chapter {
4080            id: uuid!("35e6ccd4-7412-4910-ac1e-e6bdb6e1e1fb"),
4081            title: i18n::I18nString::new(
4082                "content.quests.e3bc23d8-2035-44d0-b8c1-bb1996ac6bc5:title",
4083            ),
4084            level: 2,
4085            fight_ids: vec![
4086                uuid!("c274b0bb-173e-49e3-9d36-2c32f1e2c8a1"),
4087                uuid!("d02ac74e-5ad2-49c2-9a3e-927194c62adf"),
4088                uuid!("1f2b6501-d76f-4ed9-a6b6-7057c3ee16dc"),
4089            ],
4090        },
4091        Chapter {
4092            id: uuid!("fda76007-ff26-4a66-abef-0bd0f7ac3e60"),
4093            title: i18n::I18nString::new(
4094                "content.quests.e3bc23d8-2035-44d0-b8c1-bb1996ac6bc5:title",
4095            ),
4096            level: 3,
4097            fight_ids: vec![uuid!("09207dc1-ef01-4222-8382-ee853770fc3d")],
4098        },
4099        Chapter {
4100            id: uuid!("e1c72dcf-017f-4d6b-8834-8e32f77c7534"),
4101            title: i18n::I18nString::new(
4102                "content.quests.e3bc23d8-2035-44d0-b8c1-bb1996ac6bc5:title",
4103            ),
4104            level: 4,
4105            fight_ids: vec![uuid!("669c9bac-7734-457a-8619-1c5b65802ac8")],
4106        },
4107        Chapter {
4108            id: uuid!("de35b2ff-9330-4b1d-a268-5a82f7646936"),
4109            title: i18n::I18nString::new(
4110                "content.quests.e3bc23d8-2035-44d0-b8c1-bb1996ac6bc5:title",
4111            ),
4112            level: 5,
4113            fight_ids: vec![uuid!("3ba961a4-6eab-4251-a048-1edef27da219")],
4114        },
4115    ]
4116}
4117
4118fn generate_character_levels() -> Vec<CharacterLevel> {
4119    let mut levels = vec![
4120        // Level 0 entry required by the native `balance::character_power`
4121        // (`character_level(0)` lookup) for bot/base-power computation.
4122        CharacterLevel {
4123            level: 0,
4124            ability_slots: 0,
4125            required_experience: 0,
4126            attributes: vec![EntityAttribute {
4127                attribute_id: uuid!("45eca0a7-7430-487b-bd65-b796c6d88c08"),
4128                value: 10,
4129            }],
4130        },
4131        CharacterLevel {
4132            level: 1,
4133            ability_slots: 0,
4134            required_experience: 0,
4135            attributes: vec![EntityAttribute {
4136                attribute_id: uuid!("45eca0a7-7430-487b-bd65-b796c6d88c08"),
4137                value: 10,
4138            }],
4139        },
4140        CharacterLevel {
4141            level: 2,
4142            ability_slots: 1,
4143            required_experience: 10000,
4144            attributes: vec![EntityAttribute {
4145                attribute_id: uuid!("45eca0a7-7430-487b-bd65-b796c6d88c08"),
4146                value: 30,
4147            }],
4148        },
4149    ];
4150    // Generated tail (levels 3..=500): the native PvP bot generator
4151    // (`default_opponent_generation` -> `level_by_day`) and the fight-prep
4152    // stats path (`calculate_player_entity_stats_with_zeroes`, which requires
4153    // an EXACT `character_level(level)` match) can pick high bot levels (e.g.
4154    // 72). The shipped config authors a full level table; the test fixture
4155    // mirrors that with a simple generated tail so any generated bot level
4156    // resolves. Levels 0..=2 above stay hand-authored (other tests assert on
4157    // their exact values).
4158    levels.extend((3..=500).map(|level| CharacterLevel {
4159        level,
4160        ability_slots: 1,
4161        required_experience: 10000 * level,
4162        // Flat low base attributes (matching level 2), so generated PvP bots —
4163        // whose stored level can be high (e.g. 72 for rating 1600 via
4164        // produced (its `character_power_calculate_script` was a trivial `1`).
4165        // Keeps arena fight outcomes calibrated to the existing assertions.
4166        attributes: vec![EntityAttribute {
4167            attribute_id: uuid!("45eca0a7-7430-487b-bd65-b796c6d88c08"),
4168            value: 30,
4169        }],
4170    }));
4171    levels
4172}
4173
4174/// Unbounded extrapolation of `generate_character_levels`. The authored tail
4175/// stops at level 500 (flat attr value 30, `required_experience = 10000*level`);
4176/// the formula continues past it so `character_level(level)` and
4177/// `level_for_experience` are defined for ANY level. Continuous with the tail at
4178/// the anchor: value 30, marginal exp 10000. `ability_slots` stays flat at 1 so
4179/// slots never grow with infinite levels.
4180fn generate_character_level_formula() -> CharacterLevelFormula {
4181    CharacterLevelFormula {
4182        anchor_level: 500,
4183        anchor_experience: 10000 * 500,
4184        experience_marginal_base: 10000,
4185        experience_marginal_growth: 1.2,
4186        // The fixture keeps one band: the late growth repeats the first, so the
4187        // seam is invisible and the tail stays the simple geometric one the
4188        // tests read.
4189        experience_late_band_level: 600,
4190        experience_marginal_growth_late: 1.2,
4191        ability_slots: 1,
4192        attributes: vec![CharacterLevelAttributeFormula {
4193            attribute_id: uuid!("45eca0a7-7430-487b-bd65-b796c6d88c08"),
4194            anchor_value: 30.0,
4195            coefficient: 0.0,
4196            exponent: 1.0,
4197        }],
4198    }
4199}
4200
4201fn generate_ability_slots_levels() -> Vec<AbilitySlotsLevel> {
4202    vec![
4203        AbilitySlotsLevel {
4204            from_chapter_level: 0,
4205            ability_slots: 2,
4206        },
4207        AbilitySlotsLevel {
4208            from_chapter_level: 2,
4209            ability_slots: 10,
4210        },
4211    ]
4212}
4213
4214fn generate_patron_levels() -> Vec<ReferralLevelInfo> {
4215    vec![
4216        ReferralLevelInfo {
4217            level: 1,
4218            required_experience: 0,
4219            lvlup_reward: vec![],
4220            daily_reward: vec![],
4221        },
4222        ReferralLevelInfo {
4223            level: 2,
4224            required_experience: 200,
4225            lvlup_reward: vec![CurrencyUnit {
4226                currency_id: uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
4227                amount: 100,
4228            }],
4229            daily_reward: vec![CurrencyUnit {
4230                currency_id: uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
4231                amount: 100,
4232            }],
4233        },
4234    ]
4235}
4236fn generate_quests() -> Vec<QuestTemplate> {
4237    vec![
4238        QuestTemplate {
4239            id: uuid!("e3bc23d8-2035-44d0-b8c1-bb1996ac6bc5"),
4240            title: i18n::I18nString::new(
4241                "content.quests.e3bc23d8-2035-44d0-b8c1-bb1996ac6bc5:title",
4242            ),
4243            description: i18n::I18nString::new(
4244                "content.quests.78552c1c-5e8a-4f95-8f48-a5671051852e:title",
4245            ),
4246            progress_behavior: Some("quest_event_level_1_then_2".to_string()),
4247            additional_quests_behavior: None,
4248            progress_target: 100,
4249            quest_group_type: QuestGroupType::Daily,
4250            bundle_id: Some(uuid!("9cc6c6e4-e87f-4443-99ce-2f88b2bdbe82")),
4251            progression_points: 70,
4252            starting: true,
4253            next_quest_ids: vec![],
4254            events_subscribe: vec!["NewCharacterLevel".to_string()],
4255            progress_if_inactive: false,
4256            screen_reference: None,
4257            code: None,
4258        },
4259        QuestTemplate {
4260            id: uuid!("4cf49879-8129-481c-b356-9e0f29335e84"),
4261            title: i18n::I18nString::new(
4262                "content.quests.e3bc23d8-2035-44d0-b8c1-bb1996ac6bc5:title",
4263            ),
4264            description: i18n::I18nString::new(
4265                "content.quests.78552c1c-5e8a-4f95-8f48-a5671051852e:title",
4266            ),
4267            progress_behavior: Some("quest_current_0_then_1_else_2".to_string()),
4268            additional_quests_behavior: None,
4269            progress_target: 2,
4270            quest_group_type: QuestGroupType::Daily,
4271            bundle_id: Some(uuid!("9cc6c6e4-e87f-4443-99ce-2f88b2bdbe82")),
4272            progression_points: 70,
4273            starting: true,
4274            next_quest_ids: vec![],
4275            events_subscribe: vec!["PlayerNewItems".to_string()],
4276            progress_if_inactive: false,
4277            screen_reference: None,
4278            code: None,
4279        },
4280        QuestTemplate {
4281            id: uuid!("305300e2-f432-4cbf-ad46-85386a9a9410"),
4282            title: i18n::I18nString::new(
4283                "content.quests.305300e2-f432-4cbf-ad46-85386a9a9410:title",
4284            ),
4285            description: i18n::I18nString::new(
4286                "content.quests.78552c1c-5e8a-4f95-8f48-a5671051852e:title",
4287            ),
4288            progress_behavior: Some("quest_current_0_then_1_else_2".to_string()),
4289            additional_quests_behavior: Some("test_loop_task_1".to_string()),
4290            progress_target: 2,
4291            quest_group_type: QuestGroupType::LoopTask,
4292            bundle_id: Some(uuid!("9cc6c6e4-e87f-4443-99ce-2f88b2bdbe82")),
4293            progression_points: 80,
4294            starting: false,
4295            next_quest_ids: vec![uuid!("305300e2-f432-4cbf-ad46-85386a9a9410")],
4296            events_subscribe: vec!["PlayerNewItems".to_string()],
4297            progress_if_inactive: true,
4298            screen_reference: None,
4299            code: None,
4300        },
4301        QuestTemplate {
4302            id: uuid!("29540ca2-21f3-4f0e-a478-240adafed4e3"),
4303            title: i18n::I18nString::new(
4304                "content.quests.29540ca2-21f3-4f0e-a478-240adafed4e3:title",
4305            ),
4306            description: i18n::I18nString::new(
4307                "content.quests.78552c1c-5e8a-4f95-8f48-a5671051852e:title",
4308            ),
4309            progress_behavior: Some("increment_one".to_string()),
4310            additional_quests_behavior: Some("test_loop_task_2".to_string()),
4311            progress_target: 3,
4312            quest_group_type: QuestGroupType::LoopTask,
4313            bundle_id: Some(uuid!("9cc6c6e4-e87f-4443-99ce-2f88b2bdbe82")),
4314            progression_points: 100,
4315            starting: false,
4316            next_quest_ids: vec![uuid!("29540ca2-21f3-4f0e-a478-240adafed4e3")],
4317            events_subscribe: vec!["PlayerNewItems".to_string()],
4318            progress_if_inactive: false,
4319            screen_reference: None,
4320            code: None,
4321        },
4322        QuestTemplate {
4323            id: uuid!("9d3d2428-af3c-409f-a580-593de0fd06cf"),
4324            title: i18n::I18nString::new(
4325                "content.quests.29540ca2-21f3-4f0e-a478-240adafed4e3:title",
4326            ),
4327            description: i18n::I18nString::new(
4328                "content.quests.78552c1c-5e8a-4f95-8f48-a5671051852e:title",
4329            ),
4330            progress_behavior: Some("increment_one".to_string()),
4331            additional_quests_behavior: Some("test_loop_task_0".to_string()),
4332            progress_target: 5,
4333            quest_group_type: QuestGroupType::LoopTask,
4334            bundle_id: Some(uuid!("9cc6c6e4-e87f-4443-99ce-2f88b2bdbe82")),
4335            progression_points: 100,
4336            starting: false,
4337            next_quest_ids: vec![uuid!("9d3d2428-af3c-409f-a580-593de0fd06cf")],
4338            events_subscribe: vec!["PlayerNewItems".to_string()],
4339            progress_if_inactive: true,
4340            screen_reference: None,
4341            code: None,
4342        },
4343        QuestTemplate {
4344            id: uuid!("fa166167-f172-424f-b10b-a661fa8895db"),
4345            title: i18n::I18nString::new(
4346                "content.quests.fa166167-f172-424f-b10b-a661fa8895db:title",
4347            ),
4348            description: i18n::I18nString::new(
4349                "content.quests.78552c1c-5e8a-4f95-8f48-a5671051852e:title",
4350            ),
4351            progress_behavior: Some("quest_event_level_1_then_2".to_string()),
4352            additional_quests_behavior: None,
4353            progress_target: 2,
4354            quest_group_type: QuestGroupType::PatronLifetime,
4355            bundle_id: Some(uuid!("9cc6c6e4-e87f-4443-99ce-2f88b2bdbe82")),
4356            progression_points: 50,
4357            starting: true,
4358            next_quest_ids: vec![],
4359            events_subscribe: vec!["NewCharacterLevel".to_string()],
4360            progress_if_inactive: false,
4361            screen_reference: None,
4362            code: None,
4363        },
4364        QuestTemplate {
4365            id: uuid!("2cf76eea-5b4b-4258-9051-3d3188845c5e"),
4366            title: i18n::I18nString::new(
4367                "content.quests.fa166167-f172-424f-b10b-a661fa8895db:title",
4368            ),
4369            description: i18n::I18nString::new(
4370                "content.quests.78552c1c-5e8a-4f95-8f48-a5671051852e:title",
4371            ),
4372            progress_behavior: Some("quest_current_0_then_1_else_2".to_string()),
4373            additional_quests_behavior: None,
4374            progress_target: 2,
4375            quest_group_type: QuestGroupType::PatronLifetime,
4376            bundle_id: Some(uuid!("9cc6c6e4-e87f-4443-99ce-2f88b2bdbe82")),
4377            progression_points: 50,
4378            starting: true,
4379            next_quest_ids: vec![],
4380            events_subscribe: vec!["PlayerNewItems".to_string()],
4381            progress_if_inactive: false,
4382            screen_reference: None,
4383            code: None,
4384        },
4385        QuestTemplate {
4386            id: uuid!("78552c1c-5e8a-4f95-8f48-a5671051852e"),
4387            title: i18n::I18nString::new(
4388                "content.quests.78552c1c-5e8a-4f95-8f48-a5671051852e:title",
4389            ),
4390            description: i18n::I18nString::new(
4391                "content.quests.78552c1c-5e8a-4f95-8f48-a5671051852e:title",
4392            ),
4393            progress_behavior: Some("quest_current_0_then_1_else_2".to_string()),
4394            additional_quests_behavior: None,
4395            progress_target: 2,
4396            quest_group_type: QuestGroupType::PatronDaily,
4397            bundle_id: Some(uuid!("9cc6c6e4-e87f-4443-99ce-2f88b2bdbe82")),
4398            progression_points: 1000,
4399            starting: true,
4400            next_quest_ids: vec![],
4401            events_subscribe: vec!["PlayerNewItems".to_string()],
4402            progress_if_inactive: false,
4403            screen_reference: None,
4404            code: None,
4405        },
4406        QuestTemplate {
4407            id: uuid!("b16fea6d-6b9b-484c-b25b-8bec70234691"),
4408            title: i18n::I18nString::new(
4409                "content.quests.e3bc23d8-2035-44d0-b8c1-bb1996ac6bc5:title",
4410            ),
4411            description: i18n::I18nString::new(
4412                "content.quests.78552c1c-5e8a-4f95-8f48-a5671051852e:title",
4413            ),
4414            progress_behavior: Some("quest_current_0_then_1_else_2".to_string()),
4415            additional_quests_behavior: None,
4416            progress_target: 2,
4417            quest_group_type: QuestGroupType::Hidden,
4418            bundle_id: None,
4419            progression_points: 0,
4420            starting: true,
4421            next_quest_ids: vec![uuid!("ce92f997-e41e-4cfe-9963-e100cc121289")],
4422            events_subscribe: vec!["PlayerNewItems".to_string()],
4423            progress_if_inactive: false,
4424            screen_reference: None,
4425            code: None,
4426        },
4427        QuestTemplate {
4428            id: uuid!("ce92f997-e41e-4cfe-9963-e100cc121289"),
4429            title: i18n::I18nString::new(
4430                "content.quests.e3bc23d8-2035-44d0-b8c1-bb1996ac6bc5:title",
4431            ),
4432            description: i18n::I18nString::new(
4433                "content.quests.78552c1c-5e8a-4f95-8f48-a5671051852e:title",
4434            ),
4435            progress_behavior: Some("quest_current_0_then_1_else_2".to_string()),
4436            additional_quests_behavior: None,
4437            progress_target: 2,
4438            quest_group_type: QuestGroupType::Hidden,
4439            bundle_id: None,
4440            progression_points: 0,
4441            starting: false,
4442            next_quest_ids: vec![uuid!("db3c73c6-e300-42dd-be47-3f820ab8b095")],
4443            events_subscribe: vec!["PlayerNewItems".to_string()],
4444            progress_if_inactive: false,
4445            screen_reference: None,
4446            code: None,
4447        },
4448        QuestTemplate {
4449            id: uuid!("c3ae0d0c-a0c3-49f4-b538-fd19f388d0b2"),
4450            title: i18n::I18nString::new(
4451                "content.quests.e3bc23d8-2035-44d0-b8c1-bb1996ac6bc5:title",
4452            ),
4453            description: i18n::I18nString::new(
4454                "content.quests.78552c1c-5e8a-4f95-8f48-a5671051852e:title",
4455            ),
4456            progress_behavior: Some("increment_one".to_string()),
4457            additional_quests_behavior: Some("test_loop_task_seed".to_string()),
4458            progress_target: 1,
4459            quest_group_type: QuestGroupType::Hidden,
4460            bundle_id: None,
4461            progression_points: 0,
4462            starting: true,
4463            next_quest_ids: vec![],
4464            events_subscribe: vec!["PlayerNewItems".to_string()],
4465            progress_if_inactive: false,
4466            screen_reference: None,
4467            code: None,
4468        },
4469        QuestTemplate {
4470            id: uuid!("a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"),
4471            title: i18n::I18nString::new(
4472                "content.quests.e3bc23d8-2035-44d0-b8c1-bb1996ac6bc5:title",
4473            ),
4474            description: i18n::I18nString::new(
4475                "content.quests.78552c1c-5e8a-4f95-8f48-a5671051852e:title",
4476            ),
4477            progress_behavior: Some("quest_current_0_then_1_else_2".to_string()),
4478            additional_quests_behavior: None,
4479            progress_target: 2,
4480            quest_group_type: QuestGroupType::Hidden,
4481            bundle_id: Some(uuid!("9cc6c6e4-e87f-4443-99ce-2f88b2bdbe82")),
4482            progression_points: 0,
4483            starting: false,
4484            next_quest_ids: vec![],
4485            events_subscribe: vec!["NewCharacterLevel".to_string()],
4486            progress_if_inactive: false,
4487            screen_reference: None,
4488            code: None,
4489        },
4490        QuestTemplate {
4491            id: uuid!("db3c73c6-e300-42dd-be47-3f820ab8b095"),
4492            title: i18n::I18nString::new(
4493                "content.quests.e3bc23d8-2035-44d0-b8c1-bb1996ac6bc5:title",
4494            ),
4495            description: i18n::I18nString::new(
4496                "content.quests.78552c1c-5e8a-4f95-8f48-a5671051852e:title",
4497            ),
4498            progress_behavior: Some("quest_current_0_then_1_else_2".to_string()),
4499            additional_quests_behavior: None,
4500            progress_target: 2,
4501            quest_group_type: QuestGroupType::Lifetime,
4502            bundle_id: Some(uuid!("9cc6c6e4-e87f-4443-99ce-2f88b2bdbe82")),
4503            progression_points: 0,
4504            starting: false,
4505            next_quest_ids: vec![],
4506            events_subscribe: vec!["PlayerNewItems".to_string()],
4507            progress_if_inactive: false,
4508            screen_reference: None,
4509            code: None,
4510        },
4511        QuestTemplate {
4512            id: uuid!("d1986660-eb69-4051-a256-dbb1d8e5377f"),
4513            title: i18n::I18nString::new(
4514                "content.quests.e3bc23d8-2035-44d0-b8c1-bb1996ac6bc5:title",
4515            ),
4516            description: i18n::I18nString::new(
4517                "content.quests.78552c1c-5e8a-4f95-8f48-a5671051852e:title",
4518            ),
4519            progress_behavior: Some("quest_current_0_then_1_else_2".to_string()),
4520            additional_quests_behavior: None,
4521            progress_target: 2,
4522            quest_group_type: QuestGroupType::Lifetime,
4523            bundle_id: Some(uuid!("9cc6c6e4-e87f-4443-99ce-2f88b2bdbe82")),
4524            progression_points: 0,
4525            starting: true,
4526            next_quest_ids: vec![],
4527            events_subscribe: vec!["PlayerNewItems".to_string()],
4528            progress_if_inactive: false,
4529            screen_reference: None,
4530            code: None,
4531        },
4532        // ========== Achievement quests (independent, all starting, no chaining) ==========
4533        // Achievement 1: "Sell 2 items"
4534        QuestTemplate {
4535            id: uuid!("a1000000-0000-0000-0000-000000000001"),
4536            title: i18n::I18nString::new(
4537                "content.quests.e3bc23d8-2035-44d0-b8c1-bb1996ac6bc5:title",
4538            ),
4539            description: i18n::I18nString::new(
4540                "content.quests.78552c1c-5e8a-4f95-8f48-a5671051852e:title",
4541            ),
4542            progress_behavior: Some("quest_current_0_then_1_else_2".to_string()),
4543            additional_quests_behavior: None,
4544            progress_target: 2,
4545            quest_group_type: QuestGroupType::Achievement,
4546            progression_points: 50,
4547            bundle_id: Some(uuid!("9cc6c6e4-e87f-4443-99ce-2f88b2bdbe82")),
4548            starting: true,
4549            next_quest_ids: vec![],
4550            events_subscribe: vec!["PlayerNewItems".to_string()],
4551            progress_if_inactive: false,
4552            screen_reference: None,
4553            code: None,
4554        },
4555        // Achievement 2: "Sell 3 items"
4556        QuestTemplate {
4557            id: uuid!("a1000000-0000-0000-0000-000000000002"),
4558            title: i18n::I18nString::new(
4559                "content.quests.e3bc23d8-2035-44d0-b8c1-bb1996ac6bc5:title",
4560            ),
4561            description: i18n::I18nString::new(
4562                "content.quests.78552c1c-5e8a-4f95-8f48-a5671051852e:title",
4563            ),
4564            progress_behavior: Some("increment_one".to_string()),
4565            additional_quests_behavior: None,
4566            progress_target: 3,
4567            quest_group_type: QuestGroupType::Achievement,
4568            progression_points: 50,
4569            bundle_id: Some(uuid!("9cc6c6e4-e87f-4443-99ce-2f88b2bdbe82")),
4570            starting: true,
4571            next_quest_ids: vec![],
4572            events_subscribe: vec!["PlayerNewItems".to_string()],
4573            progress_if_inactive: false,
4574            screen_reference: None,
4575            code: None,
4576        },
4577        // ========== Progress Pass quests (starting: false — rows are pre-seeded
4578        // in test_progress_pass.rs via storage::quests::update_current_progress) ==========
4579        QuestTemplate {
4580            id: uuid!("aa000000-0000-0000-0000-000000000001"),
4581            title: i18n::I18nString::new(
4582                "content.quests.e3bc23d8-2035-44d0-b8c1-bb1996ac6bc5:title",
4583            ),
4584            description: i18n::I18nString::new(
4585                "content.quests.78552c1c-5e8a-4f95-8f48-a5671051852e:title",
4586            ),
4587            progress_behavior: Some("increment_one".to_string()),
4588            additional_quests_behavior: None,
4589            progress_target: 1,
4590            quest_group_type: QuestGroupType::ProgressPass,
4591            progression_points: 0,
4592            bundle_id: None,
4593            starting: false,
4594            next_quest_ids: vec![],
4595            events_subscribe: vec!["PlayerNewItems".to_string()],
4596            progress_if_inactive: false,
4597            screen_reference: None,
4598            code: None,
4599        },
4600        QuestTemplate {
4601            id: uuid!("bb000000-0000-0000-0000-000000000002"),
4602            title: i18n::I18nString::new(
4603                "content.quests.e3bc23d8-2035-44d0-b8c1-bb1996ac6bc5:title",
4604            ),
4605            description: i18n::I18nString::new(
4606                "content.quests.78552c1c-5e8a-4f95-8f48-a5671051852e:title",
4607            ),
4608            progress_behavior: Some("increment_one".to_string()),
4609            additional_quests_behavior: None,
4610            progress_target: 2,
4611            quest_group_type: QuestGroupType::ProgressPass,
4612            progression_points: 0,
4613            bundle_id: None,
4614            starting: false,
4615            next_quest_ids: vec![],
4616            events_subscribe: vec!["PlayerNewItems".to_string()],
4617            progress_if_inactive: false,
4618            screen_reference: None,
4619            code: None,
4620        },
4621        QuestTemplate {
4622            id: uuid!("cc000000-0000-0000-0000-000000000003"),
4623            title: i18n::I18nString::new(
4624                "content.quests.e3bc23d8-2035-44d0-b8c1-bb1996ac6bc5:title",
4625            ),
4626            description: i18n::I18nString::new(
4627                "content.quests.78552c1c-5e8a-4f95-8f48-a5671051852e:title",
4628            ),
4629            progress_behavior: Some("increment_one".to_string()),
4630            additional_quests_behavior: None,
4631            progress_target: 1,
4632            quest_group_type: QuestGroupType::ProgressPass,
4633            progression_points: 0,
4634            bundle_id: None,
4635            starting: false,
4636            next_quest_ids: vec![],
4637            events_subscribe: vec!["PlayerNewItems".to_string()],
4638            progress_if_inactive: false,
4639            screen_reference: None,
4640            code: None,
4641        },
4642    ]
4643}
4644
4645fn generate_quests_progression_settings() -> QuestsProgressionSettings {
4646    let point = |points: u64, amount: i64| QuestsProgressionPointSettings {
4647        points,
4648        reward: vec![CurrencyUnit {
4649            currency_id: uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
4650            amount,
4651        }],
4652    };
4653    QuestsProgressionSettings {
4654        daily: vec![point(35, 10), point(70, 70), point(100, 100)],
4655        weekly: vec![point(30, 100), point(60, 500), point(100, 1000)],
4656        achievement: vec![point(50, 10), point(100, 50), point(200, 100)],
4657    }
4658}
4659
4660fn generate_vassals_settings() -> VassalsSettings {
4661    VassalsSettings {
4662        vassal_shield_duration_secs: 0,
4663        // Ports of the test fixture's `vassal_reward_script` /
4664        // `suzerain_reward_script` (both push CurrencyUnit(b59b33a2, 42)).
4665        suzerain_reward_fn: "test_vassal_link_reward_42".to_string(),
4666        vassal_reward_fn: "test_vassal_link_reward_42".to_string(),
4667    }
4668}
4669
4670fn generate_pvp_settings() -> PvpSettings {
4671    PvpSettings {
4672        max_matchmaking_opponents_count: 10,
4673        matchmaking_character_level_data: 2,
4674    }
4675}
4676
4677fn generate_vassal_tasks() -> Vec<VassalTaskTemplate> {
4678    vec![
4679        VassalTaskTemplate {
4680            id: uuid!("7b4a7bf5-01e0-4078-9cc1-52b742ed2b1e"),
4681            title: i18n::I18nString::new(
4682                "content.vassal_tasks.7b4a7bf5-01e0-4078-9cc1-52b742ed2b1e:title",
4683            ),
4684            duration_sec: 2,
4685            // Ports of the fixture's `if GoodTask {42} else {32}` scripts.
4686            reward_fn: "test_vassal_task_reward_42_32".to_string(),
4687            loyalty_fn: "test_vassal_task_loyalty_42_32".to_string(),
4688        },
4689        VassalTaskTemplate {
4690            id: uuid!("05e915bf-55fe-4419-bcf0-5f90833f17bd"),
4691            title: i18n::I18nString::new(
4692                "content.vassal_tasks.05e915bf-55fe-4419-bcf0-5f90833f17bd:title",
4693            ),
4694            duration_sec: 10,
4695            // Ports of the fixture's `if GoodTask {0} else {42}` scripts.
4696            reward_fn: "test_vassal_task_reward_0_42".to_string(),
4697            loyalty_fn: "test_vassal_task_loyalty_0_42".to_string(),
4698        },
4699    ]
4700}
4701
4702fn generate_gifts() -> Vec<GiftTemplate> {
4703    vec![
4704        GiftTemplate {
4705            id: uuid!("5e4b493e-78d2-45cd-931a-3d12ff669038"),
4706            gift_type: GiftType::VassalGift,
4707            price: vec![CurrencyUnit {
4708                currency_id: uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
4709                amount: 50,
4710            }],
4711            reward: vec![CurrencyUnit {
4712                currency_id: uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
4713                amount: 50,
4714            }],
4715            loyalty: 50,
4716        },
4717        GiftTemplate {
4718            id: uuid!("64e8ead6-44cf-4798-a18d-db648d598930"),
4719            gift_type: GiftType::GenericGift,
4720            price: vec![CurrencyUnit {
4721                currency_id: uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
4722                amount: 50,
4723            }],
4724            reward: vec![CurrencyUnit {
4725                currency_id: uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
4726                amount: 50,
4727            }],
4728            loyalty: 0,
4729        },
4730    ]
4731}
4732
4733fn generate_currencies() -> Vec<Currency> {
4734    vec![
4735        Currency {
4736            id: uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
4737            name: i18n::I18nString::new(
4738                "content.currencies.b59b33a2-4d19-4e2c-9cea-e03ea15882a0:name",
4739            ),
4740            description: i18n::I18nString::new(
4741                "content.currencies.b59b33a2-4d19-4e2c-9cea-e03ea15882a0:name",
4742            ),
4743            icon_url: "e0bcf4ea-b2b5-47d6-9d76-985797a5a818.webp".to_string(),
4744            icon_path: "e0bcf4ea-b2b5-47d6-9d76-985797a5a818.webp".to_string(),
4745        },
4746        Currency {
4747            id: uuid!("22297520-abb8-45b9-9fbb-c418b8001246"),
4748            name: i18n::I18nString::new(
4749                "content.currencies.22297520-abb8-45b9-9fbb-c418b8001246:name",
4750            ),
4751            description: i18n::I18nString::new(
4752                "content.currencies.22297520-abb8-45b9-9fbb-c418b8001246:name",
4753            ),
4754            icon_url: "b2446d1a-368d-4747-8b92-82a279bc6193.webp".to_string(),
4755            icon_path: "b2446d1a-368d-4747-8b92-82a279bc6193.webp".to_string(),
4756        },
4757        Currency {
4758            id: uuid!("6c050b07-5282-4a23-8bbf-dabecd27cade"),
4759            name: i18n::I18nString::new(
4760                "content.currencies.6c050b07-5282-4a23-8bbf-dabecd27cade:name",
4761            ),
4762            description: i18n::I18nString::new(
4763                "content.currencies.6c050b07-5282-4a23-8bbf-dabecd27cade:name",
4764            ),
4765            icon_url: "50a5cd4b-6b68-4e44-b4dd-9acb928bfea5.webp".to_string(),
4766            icon_path: "50a5cd4b-6b68-4e44-b4dd-9acb928bfea5.webp".to_string(),
4767        },
4768    ]
4769}
4770
4771fn generate_mail_templates() -> Vec<MailTemplate> {
4772    vec![
4773        MailTemplate {
4774            id: uuid!("f3d6c5a1-2b4e-4c9a-9f61-8d2e9af2a1b1"),
4775            title: i18n::I18nString::new(
4776                "content.mails.f3d6c5a1-2b4e-4c9a-9f61-8d2e9af2a1b1:title",
4777            ),
4778            message: i18n::I18nString::new(
4779                "content.mails.f3d6c5a1-2b4e-4c9a-9f61-8d2e9af2a1b1:message",
4780            ),
4781            bundle_id: Some(uuid!("53e8ffaa-7ee4-4612-bbfc-bb157979ca8d")),
4782            lifetime_secs: 86_400,
4783            send_type: MailSendType {
4784                scheduled_at: None,
4785                repeat: None,
4786            },
4787            restrictions: None,
4788            is_auto_send: true,
4789        },
4790        MailTemplate {
4791            id: uuid!("7216d615-6adc-46ab-b5af-4d8e26f38baf"),
4792            title: i18n::I18nString::new(
4793                "content.mails.f3d6c5a1-2b4e-4c9a-9f61-8d2e9af2a1b1:title",
4794            ),
4795            message: i18n::I18nString::new(
4796                "content.mails.f3d6c5a1-2b4e-4c9a-9f61-8d2e9af2a1b1:message",
4797            ),
4798            bundle_id: None,
4799            lifetime_secs: 86_400,
4800            send_type: MailSendType {
4801                scheduled_at: None,
4802                repeat: None,
4803            },
4804            restrictions: None,
4805            is_auto_send: true,
4806        },
4807        MailTemplate {
4808            id: uuid!("5dd4c42f-95af-4f10-8fa7-7f567686e756"),
4809            title: i18n::I18nString::new(
4810                "content.mails.f3d6c5a1-2b4e-4c9a-9f61-8d2e9af2a1b1:title",
4811            ),
4812            message: i18n::I18nString::new(
4813                "content.mails.f3d6c5a1-2b4e-4c9a-9f61-8d2e9af2a1b1:message",
4814            ),
4815            bundle_id: Some(uuid!("53e8ffaa-7ee4-4612-bbfc-bb157979ca8d")),
4816            lifetime_secs: 86_400,
4817            send_type: MailSendType {
4818                scheduled_at: Some("2015-01-01-00-00-00".to_string()),
4819                repeat: Some(MailRepeatType::Daily),
4820            },
4821            restrictions: None,
4822            is_auto_send: true,
4823        },
4824        MailTemplate {
4825            id: uuid!("7b1d8c15-74c5-44d1-b7fb-bb027d5389c6"),
4826            title: i18n::I18nString::new(
4827                "content.mails.f3d6c5a1-2b4e-4c9a-9f61-8d2e9af2a1b1:title",
4828            ),
4829            message: i18n::I18nString::new(
4830                "content.mails.f3d6c5a1-2b4e-4c9a-9f61-8d2e9af2a1b1:message",
4831            ),
4832            bundle_id: Some(uuid!("53e8ffaa-7ee4-4612-bbfc-bb157979ca8d")),
4833            lifetime_secs: 86_400,
4834            send_type: MailSendType {
4835                scheduled_at: Some("2015-01-01-00-00-00".to_string()),
4836                repeat: Some(MailRepeatType::Daily),
4837            },
4838            restrictions: None,
4839            is_auto_send: false,
4840        },
4841        MailTemplate {
4842            id: uuid!("98eea253-ac84-49ea-becb-4d5ef9c18b5c"),
4843            title: i18n::I18nString::new(
4844                "content.mails.f3d6c5a1-2b4e-4c9a-9f61-8d2e9af2a1b1:title",
4845            ),
4846            message: i18n::I18nString::new(
4847                "content.mails.f3d6c5a1-2b4e-4c9a-9f61-8d2e9af2a1b1:message",
4848            ),
4849            bundle_id: Some(uuid!("53e8ffaa-7ee4-4612-bbfc-bb157979ca8d")),
4850            lifetime_secs: 86_400,
4851            send_type: MailSendType {
4852                scheduled_at: Some("2015-01-01-00-00-00".to_string()),
4853                repeat: Some(MailRepeatType::Daily),
4854            },
4855            restrictions: None,
4856            is_auto_send: false,
4857        },
4858        MailTemplate {
4859            id: uuid!("0042b76d-bfbe-4e02-a6a6-4881e229da88"),
4860            title: i18n::I18nString::new(
4861                "content.mails.f3d6c5a1-2b4e-4c9a-9f61-8d2e9af2a1b1:title",
4862            ),
4863            message: i18n::I18nString::new(
4864                "content.mails.f3d6c5a1-2b4e-4c9a-9f61-8d2e9af2a1b1:message",
4865            ),
4866            bundle_id: Some(uuid!("53e8ffaa-7ee4-4612-bbfc-bb157979ca8d")),
4867            lifetime_secs: 86_400,
4868            send_type: MailSendType {
4869                scheduled_at: Some("2015-01-01-00-00-00".to_string()),
4870                repeat: Some(MailRepeatType::Daily),
4871            },
4872            restrictions: None,
4873            is_auto_send: false,
4874        },
4875    ]
4876}
4877
4878fn generate_inventory_levels() -> Vec<InventoryLevel> {
4879    vec![InventoryLevel {
4880        from_chapter_level: 0,
4881        slots: vec![
4882            InventorySlotConfig {
4883                item_type: ItemType::Weapon,
4884                world_side: Some(WorldSide::Fantasy),
4885            },
4886            InventorySlotConfig {
4887                item_type: ItemType::Weapon,
4888                world_side: Some(WorldSide::Real),
4889            },
4890            InventorySlotConfig {
4891                item_type: ItemType::Torso,
4892                world_side: Some(WorldSide::Fantasy),
4893            },
4894            InventorySlotConfig {
4895                item_type: ItemType::Head,
4896                world_side: Some(WorldSide::Fantasy),
4897            },
4898            InventorySlotConfig {
4899                item_type: ItemType::Gloves,
4900                world_side: Some(WorldSide::Real),
4901            },
4902            InventorySlotConfig {
4903                item_type: ItemType::Shoulders,
4904                world_side: Some(WorldSide::Real),
4905            },
4906            InventorySlotConfig {
4907                item_type: ItemType::Boots,
4908                world_side: None,
4909            },
4910            InventorySlotConfig {
4911                item_type: ItemType::Legs,
4912                world_side: None,
4913            },
4914            InventorySlotConfig {
4915                item_type: ItemType::Waist,
4916                world_side: None,
4917            },
4918            InventorySlotConfig {
4919                item_type: ItemType::Neck,
4920                world_side: None,
4921            },
4922            InventorySlotConfig {
4923                item_type: ItemType::Ring,
4924                world_side: None,
4925            },
4926        ],
4927    }]
4928}
4929
4930fn generate_arena_settings() -> ArenaSettings {
4931    ArenaSettings {
4932        base_rating: 1000,
4933        matches_history_size: 5,
4934        rating_prey_power_ratio: 0.85,
4935        rating_stretch_power_ratio: 1.10,
4936        win_points_prey: 5,
4937        win_points_peer: 9,
4938        win_points_stretch: 14,
4939        lose_points_prey: 8,
4940        lose_points_peer: 5,
4941        lose_points_stretch: 2,
4942        lose_protection_rating: 1100,
4943        win_streak_bonus_step: 1,
4944        win_streak_bonus_max: 5,
4945        loss_streak_protection_after: 2,
4946        leaderboard_size: 5,
4947        arena_tickets_currency_id: uuid!("6c050b07-5282-4a23-8bbf-dabecd27cade"),
4948        matchmaking_character_rating_delta: 30,
4949        pvp_cooldown_secs: 60,
4950        win_reward_bundle_id: uuid!("53e8ffaa-7ee4-4612-bbfc-bb157979ca8d"),
4951        lose_reward_bundle_id: uuid!("e18fc101-6615-440c-9a1d-42f509986ccc"),
4952        arena_ticket_buy_currency_id: uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
4953        arena_ticket_price: CurrencyUnit {
4954            currency_id: uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
4955            amount: 100,
4956        },
4957        arena_matches_ttl_days: 30,
4958        rematch_max_rating_increase: 250,
4959        rematch_max_rating_decrease: -100,
4960    }
4961}
4962
4963fn generate_arena_leagues() -> Vec<ArenaLeague> {
4964    vec![
4965        ArenaLeague {
4966            id: uuid!("842ce55f-c6b9-4d2f-b86b-6288529a8658"),
4967            name: i18n::I18nString::new(
4968                "content.arena.leagues.842ce55f-c6b9-4d2f-b86b-6288529a8658:name",
4969            ),
4970            min_rating: 0,
4971            max_rating: 999,
4972        },
4973        ArenaLeague {
4974            id: uuid!("d1936748-b14e-4a7a-b17d-aaf2d70e5077"),
4975            name: i18n::I18nString::new(
4976                "content.arena.leagues.d1936748-b14e-4a7a-b17d-aaf2d70e5077:name",
4977            ),
4978            min_rating: 1000,
4979            max_rating: 1999,
4980        },
4981        ArenaLeague {
4982            id: uuid!("1e69174c-5628-4a65-ba87-55ac6e31868f"),
4983            name: i18n::I18nString::new(
4984                "content.arena.leagues.1e69174c-5628-4a65-ba87-55ac6e31868f:name",
4985            ),
4986            min_rating: 2000,
4987            max_rating: 10000,
4988        },
4989    ]
4990}
4991
4992fn generate_matchmaking_settings() -> MatchmakingSettings {
4993    MatchmakingSettings {
4994        opponent_positions: vec![
4995            OpponentPositionSettings {
4996                min_rating_diff_percent: 15.0,
4997                max_rating_diff_percent: 20.0,
4998                max_rating_for_bots: 1000,
4999                bot_power_multiplier: 1.0,
5000                min_power_ratio: None,
5001                max_power_ratio: None,
5002            },
5003            OpponentPositionSettings {
5004                min_rating_diff_percent: -7.0,
5005                max_rating_diff_percent: 7.0,
5006                max_rating_for_bots: 1000,
5007                bot_power_multiplier: 1.0,
5008                min_power_ratio: None,
5009                max_power_ratio: None,
5010            },
5011            OpponentPositionSettings {
5012                min_rating_diff_percent: -7.0,
5013                max_rating_diff_percent: 7.0,
5014                max_rating_for_bots: 1000,
5015                bot_power_multiplier: 1.0,
5016                min_power_ratio: None,
5017                max_power_ratio: None,
5018            },
5019            OpponentPositionSettings {
5020                min_rating_diff_percent: -17.0,
5021                max_rating_diff_percent: -15.0,
5022                max_rating_for_bots: 1000,
5023                bot_power_multiplier: 1.0,
5024                min_power_ratio: None,
5025                max_power_ratio: None,
5026            },
5027        ],
5028        percentile_threshold: 30.0,
5029        max_bots: 2,
5030    }
5031}
5032
5033fn generate_event_descriptions() -> Vec<EventDescription> {
5034    vec![EventDescription {
5035        name: "NewCharacterLevel".to_string(),
5036        description: i18n::I18nString::new("content.event.newcharacterlevel:name"),
5037        color: "#008000".to_string(),
5038        icon_url: "tmp".to_string(),
5039        bg_url: "tmp".to_string(),
5040        bg_path: "tmp".to_string(),
5041        text_url: "tmp".to_string(),
5042        effect_url: "tmp".to_string(),
5043        ttl_milliseconds: 2000,
5044    }]
5045}
5046
5047fn generate_classes() -> Vec<class::Class> {
5048    vec![
5049        class::Class {
5050            passive: class::ClassPassive::None,
5051            passive_power_q_l1: 1.0,
5052            passive_power_q_l20: 1.0,
5053            ability_power_q: vec![],
5054            passive_percent_l1: 0.0,
5055            passive_percent_l20: 0.0,
5056            passive_period_ticks: 0,
5057            id: uuid!("00000000-0000-0000-0000-000000000000"),
5058            character_asset: "2f7d1e2e-0e33-4730-a3b6-de7122ed9044".to_string(),
5059            spine: "".into(),
5060            cast_time: 500,
5061            basic_abilities: vec![uuid!("da6c582b-7364-40bd-9b2d-946d8e20eaac")],
5062            // The INITIAL class (`game_settings.initial_class_id`): its starter
5063            // bundle is what every real-creation-path test claims, and it holds
5064            // one Artifact step so the storage-only grant arm stays covered.
5065            starter_bundle_id: Some(STARTER_ARTIFACT_BUNDLE_ID),
5066            attributes: vec![],
5067            ability_rarity_id: uuid!("88ed32be-33cd-43a5-88e1-65c078e6372d"),
5068            tier: class::ClassTier::C,
5069            background_image_path: "".to_string(),
5070            icon_path: "".to_string(),
5071            description: i18n::I18nString::new("content.event.newcharacterlevel:name"),
5072            name: i18n::I18nString::new("content.event.newcharacterlevel:name"),
5073            demo_skins: vec![],
5074            weapon_type: i18n::I18nString::Translated("".into()),
5075            class_abilities: vec![],
5076            main_attribute: uuid!("45eca0a7-7430-487b-bd65-b796c6d88c08"),
5077            main_attribute_optimal_value: 0,
5078            default_weapon_skin_id: uuid!("44d8fbed-9ec9-4a57-865a-59d32465a1f2"),
5079        },
5080        class::Class {
5081            passive: class::ClassPassive::BlockHeal,
5082            passive_power_q_l1: 1.0,
5083            passive_power_q_l20: 1.0,
5084            ability_power_q: vec![],
5085            passive_percent_l1: 2.0,
5086            passive_percent_l20: 2.0,
5087            passive_period_ticks: 0,
5088            id: uuid!("5956e37c-ca7f-45cf-8bfb-49601dc9aca3"),
5089            character_asset: "2f7d1e2e-0e33-4730-a3b6-de7122ed9044".to_string(),
5090            spine: "".into(),
5091            cast_time: 500,
5092            basic_abilities: vec![uuid!("da6c582b-7364-40bd-9b2d-946d8e20eaac")],
5093            starter_bundle_id: Some(uuid!("c86eed27-03d0-4c06-a6b2-e8688c09f7ed")),
5094            attributes: vec![
5095                class::ClassAttribute::EntityAttribute(EntityAttribute {
5096                    attribute_id: uuid!("45eca0a7-7430-487b-bd65-b796c6d88c08"),
5097                    value: 50,
5098                }),
5099                class::ClassAttribute::EntityAttribute(EntityAttribute {
5100                    attribute_id: uuid!("6ad485d7-9567-4b77-8ddc-a2418dd1dfe6"),
5101                    value: 20,
5102                }),
5103            ],
5104            ability_rarity_id: uuid!("88ed32be-33cd-43a5-88e1-65c078e6372d"),
5105            tier: class::ClassTier::A,
5106            background_image_path: "".to_string(),
5107            icon_path: "".to_string(),
5108            description: i18n::I18nString::new("content.event.newcharacterlevel:name"),
5109            name: i18n::I18nString::new("content.event.newcharacterlevel:name"),
5110            demo_skins: vec![],
5111            weapon_type: i18n::I18nString::Translated("".into()),
5112            class_abilities: vec![],
5113            main_attribute: uuid!("45eca0a7-7430-487b-bd65-b796c6d88c08"),
5114            main_attribute_optimal_value: 0,
5115            default_weapon_skin_id: uuid!("44d8fbed-9ec9-4a57-865a-59d32465a1f2"),
5116        },
5117        class::Class {
5118            passive: class::ClassPassive::DoubleStrike,
5119            passive_power_q_l1: 1.0,
5120            passive_power_q_l20: 1.0,
5121            ability_power_q: vec![],
5122            passive_percent_l1: 5.0,
5123            passive_percent_l20: 17.0,
5124            passive_period_ticks: 0,
5125            id: uuid!("95d314ee-ce2c-4b10-a8bc-596b0f03ab8a"),
5126            character_asset: "2f7d1e2e-0e33-4730-a3b6-de7122ed9044".to_string(),
5127            spine: "".into(),
5128            cast_time: 500,
5129            basic_abilities: vec![
5130                uuid!("59e317c9-0108-4eb0-a41f-9d513b821542"),
5131                uuid!("41ee5532-a293-4f88-bfb6-5ffca1e57acc"),
5132            ],
5133            starter_bundle_id: None,
5134            attributes: vec![],
5135            ability_rarity_id: uuid!("88ed32be-33cd-43a5-88e1-65c078e6372d"),
5136            tier: class::ClassTier::A,
5137            background_image_path: "".to_string(),
5138            icon_path: "".to_string(),
5139            description: i18n::I18nString::new("content.event.newcharacterlevel:name"),
5140            name: i18n::I18nString::new("content.event.newcharacterlevel:name"),
5141            demo_skins: vec![],
5142            weapon_type: i18n::I18nString::Translated("".into()),
5143            class_abilities: vec![],
5144            main_attribute: uuid!("45eca0a7-7430-487b-bd65-b796c6d88c08"),
5145            main_attribute_optimal_value: 0,
5146            default_weapon_skin_id: uuid!("44d8fbed-9ec9-4a57-865a-59d32465a1f2"),
5147        },
5148        // BOT_CLASS assigned to generated PvP bots by the native
5149        // `default_opponent_generation` (`set_class_id`). Id must match the
5150        // `BOT_CLASS` const in `behaviors/opponent_generation.rs`.
5151        class::Class {
5152            passive: class::ClassPassive::LowestAllyHeal,
5153            passive_power_q_l1: 1.0,
5154            passive_power_q_l20: 1.0,
5155            ability_power_q: vec![],
5156            passive_percent_l1: 10.0,
5157            passive_percent_l20: 10.0,
5158            passive_period_ticks: 10000,
5159            id: uuid!("0195c7de-144f-7b53-b370-468e9ae8f744"),
5160            character_asset: "2f7d1e2e-0e33-4730-a3b6-de7122ed9044".to_string(),
5161            spine: "".into(),
5162            cast_time: 500,
5163            basic_abilities: vec![uuid!("0194d64e-20f2-75e5-89c8-4cb812672485")],
5164            starter_bundle_id: None,
5165            attributes: vec![],
5166            ability_rarity_id: uuid!("88ed32be-33cd-43a5-88e1-65c078e6372d"),
5167            tier: class::ClassTier::C,
5168            background_image_path: "".to_string(),
5169            icon_path: "".to_string(),
5170            description: i18n::I18nString::new("content.event.newcharacterlevel:name"),
5171            name: i18n::I18nString::new("content.event.newcharacterlevel:name"),
5172            demo_skins: vec![],
5173            weapon_type: i18n::I18nString::Translated("".into()),
5174            class_abilities: vec![],
5175            main_attribute: uuid!("45eca0a7-7430-487b-bd65-b796c6d88c08"),
5176            main_attribute_optimal_value: 0,
5177            default_weapon_skin_id: uuid!("44d8fbed-9ec9-4a57-865a-59d32465a1f2"),
5178        },
5179    ]
5180}
5181
5182fn generate_projectiles() -> Vec<Projectile> {
5183    vec![Projectile {
5184        id: uuid!("1ca53a04-5321-4d93-bdc4-55caed5ab83b"),
5185        start_behavior: Some("projectile_fixed_500_damage_300".to_string()),
5186        behavior: Some("projectile_damage_target_5".to_string()),
5187        vfx: "".to_string(),
5188    }]
5189}
5190
5191fn generate_basic_items() -> Vec<ItemTemplate> {
5192    vec![
5193        ItemTemplate {
5194            id: uuid!("176c4cc5-7b93-43b4-9f90-958e2791a8d6"),
5195            name: i18n::I18nString::new("content.items.176c4cc5-7b93-43b4-9f90-958e2791a8d6:name"),
5196            icon_url: "/leather_boots.png".to_string(),
5197            icon_path: "/leather_boots.png".to_string(),
5198            item_type: ItemType::Weapon,
5199            world_side: Some(WorldSide::Fantasy),
5200            rarity_id: uuid!("8c8acba2-67d3-40e0-ac62-2f7acce64ccc"),
5201            attributes_settings: ItemAttributeSettings {
5202                optional_attributes_count: 0,
5203                optional_attributes_ids: vec![],
5204            },
5205            skin_id: Some(uuid!("44d8fbed-9ec9-4a57-865a-59d32465a1f2")),
5206            exclude_from_mimic: false,
5207            required_class: None,
5208            fixed_power: None,
5209            next_mimic_item_code: None,
5210        },
5211        ItemTemplate {
5212            id: uuid!("9f86cff5-2ce6-4191-95cf-2271cc3686f5"),
5213            name: i18n::I18nString::new("content.items.9f86cff5-2ce6-4191-95cf-2271cc3686f5:name"),
5214            icon_url: "/iron_chestplate.png".to_string(),
5215            icon_path: "/iron_chestplate.png".to_string(),
5216            item_type: ItemType::Torso,
5217            world_side: Some(WorldSide::Fantasy),
5218            rarity_id: uuid!("8c8acba2-67d3-40e0-ac62-2f7acce64ccc"),
5219            attributes_settings: ItemAttributeSettings {
5220                optional_attributes_count: 0,
5221                optional_attributes_ids: vec![],
5222            },
5223            skin_id: Some(uuid!("a1b2c3d4-e5f6-7890-abcd-ef1234567890")),
5224            exclude_from_mimic: false,
5225            required_class: None,
5226            fixed_power: None,
5227            next_mimic_item_code: None,
5228        },
5229        ItemTemplate {
5230            id: uuid!("6779d933-1663-4d02-8e47-2bf8ce06aeee"),
5231            name: i18n::I18nString::new("content.items.6779d933-1663-4d02-8e47-2bf8ce06aeee:name"),
5232            icon_url: "/mask_of_fury.png".to_string(),
5233            icon_path: "/mask_of_fury.png".to_string(),
5234            item_type: ItemType::Head,
5235            world_side: Some(WorldSide::Fantasy),
5236            rarity_id: uuid!("8c8acba2-67d3-40e0-ac62-2f7acce64ccc"),
5237            attributes_settings: ItemAttributeSettings {
5238                optional_attributes_count: 0,
5239                optional_attributes_ids: vec![],
5240            },
5241            skin_id: None,
5242            exclude_from_mimic: false,
5243            required_class: None,
5244            fixed_power: None,
5245            next_mimic_item_code: None,
5246        },
5247        ItemTemplate {
5248            id: uuid!("a1b2c3d4-e5f6-7890-abcd-ef1234567891"),
5249            name: i18n::I18nString::new("content.items.a1b2c3d4-e5f6-7890-abcd-ef1234567891:name"),
5250            icon_url: "/simple_sword.png".to_string(),
5251            icon_path: "/simple_sword.png".to_string(),
5252            item_type: ItemType::Weapon,
5253            world_side: Some(WorldSide::Real),
5254            rarity_id: uuid!("8c8acba2-67d3-40e0-ac62-2f7acce64ccc"),
5255            attributes_settings: ItemAttributeSettings {
5256                optional_attributes_count: 0,
5257                optional_attributes_ids: vec![],
5258            },
5259            skin_id: None,
5260            exclude_from_mimic: false,
5261            required_class: None,
5262            fixed_power: None,
5263            next_mimic_item_code: None,
5264        },
5265        ItemTemplate {
5266            id: uuid!("37108f62-1595-4e2d-8d51-708b96311511"),
5267            name: i18n::I18nString::new("content.items.37108f62-1595-4e2d-8d51-708b96311511:name"),
5268            icon_url: "/gloves_of_agility.png".to_string(),
5269            icon_path: "/gloves_of_agility.png".to_string(),
5270            item_type: ItemType::Legs,
5271            world_side: None,
5272            rarity_id: uuid!("8c8acba2-67d3-40e0-ac62-2f7acce64ccc"),
5273            attributes_settings: ItemAttributeSettings {
5274                optional_attributes_count: 1,
5275                optional_attributes_ids: vec![uuid!("3a6ec1c8-7494-43df-a345-d23e297b892d")],
5276            },
5277            skin_id: None,
5278            exclude_from_mimic: false,
5279            required_class: None,
5280            fixed_power: None,
5281            next_mimic_item_code: None,
5282        },
5283        ItemTemplate {
5284            id: uuid!("edee37e9-cfea-4406-b16e-64f5f3aed7c2"),
5285            name: i18n::I18nString::new("content.items.edee37e9-cfea-4406-b16e-64f5f3aed7c2:name"),
5286            icon_url: "/wizard_hat.png".to_string(),
5287            icon_path: "/wizard_hat.png".to_string(),
5288            item_type: ItemType::Shoulders,
5289            world_side: Some(WorldSide::Real),
5290            rarity_id: uuid!("8c8acba2-67d3-40e0-ac62-2f7acce64ccc"),
5291            attributes_settings: ItemAttributeSettings {
5292                optional_attributes_count: 1,
5293                optional_attributes_ids: vec![uuid!("3a6ec1c8-7494-43df-a345-d23e297b892d")],
5294            },
5295            skin_id: None,
5296            exclude_from_mimic: false,
5297            required_class: None,
5298            fixed_power: None,
5299            next_mimic_item_code: None,
5300        },
5301        ItemTemplate {
5302            id: uuid!("88ee0a22-b495-40bd-ad1b-132998a1cd37"),
5303            name: i18n::I18nString::new("content.items.88ee0a22-b495-40bd-ad1b-132998a1cd37:name"),
5304            icon_url: "/mystic_pants.png".to_string(),
5305            icon_path: "/mystic_pants.png".to_string(),
5306            item_type: ItemType::Boots,
5307            world_side: None,
5308            rarity_id: uuid!("8c8acba2-67d3-40e0-ac62-2f7acce64ccc"),
5309            attributes_settings: ItemAttributeSettings {
5310                optional_attributes_count: 1,
5311                optional_attributes_ids: vec![uuid!("3a6ec1c8-7494-43df-a345-d23e297b892d")],
5312            },
5313            skin_id: None,
5314            exclude_from_mimic: false,
5315            required_class: None,
5316            fixed_power: None,
5317            next_mimic_item_code: None,
5318        },
5319        ItemTemplate {
5320            id: uuid!("1eda3aa8-8868-4f77-8ac3-cb2c0b080f1d"),
5321            name: i18n::I18nString::new("content.items.1eda3aa8-8868-4f77-8ac3-cb2c0b080f1d:name"),
5322            icon_url: "/immortal_blade.png".to_string(),
5323            icon_path: "/immortal_blade.png".to_string(),
5324            item_type: ItemType::Gloves,
5325            world_side: Some(WorldSide::Real),
5326            rarity_id: uuid!("8c8acba2-67d3-40e0-ac62-2f7acce64ccc"),
5327            attributes_settings: ItemAttributeSettings {
5328                optional_attributes_count: 1,
5329                optional_attributes_ids: vec![uuid!("3a6ec1c8-7494-43df-a345-d23e297b892d")],
5330            },
5331            skin_id: None,
5332            exclude_from_mimic: false,
5333            required_class: None,
5334            fixed_power: None,
5335            next_mimic_item_code: None,
5336        },
5337        ItemTemplate {
5338            id: uuid!("255ad949-72cc-48dc-8129-30584a5a69b7"),
5339            name: i18n::I18nString::new("content.items.255ad949-72cc-48dc-8129-30584a5a69b7:name"),
5340            icon_url: "/divine_armor.png".to_string(),
5341            icon_path: "/divine_armor.png".to_string(),
5342            item_type: ItemType::Waist,
5343            world_side: None,
5344            rarity_id: uuid!("8c8acba2-67d3-40e0-ac62-2f7acce64ccc"),
5345            attributes_settings: ItemAttributeSettings {
5346                optional_attributes_count: 1,
5347                optional_attributes_ids: vec![uuid!("3a6ec1c8-7494-43df-a345-d23e297b892d")],
5348            },
5349            skin_id: None,
5350            exclude_from_mimic: false,
5351            required_class: None,
5352            fixed_power: None,
5353            next_mimic_item_code: None,
5354        },
5355        ItemTemplate {
5356            id: uuid!("32be4f79-5327-4118-9594-e67a3a97959d"),
5357            name: i18n::I18nString::new("content.items.32be4f79-5327-4118-9594-e67a3a97959d:name"),
5358            icon_url: "/ancient_crown.png".to_string(),
5359            icon_path: "/ancient_crown.png".to_string(),
5360            item_type: ItemType::Neck,
5361            world_side: None,
5362            rarity_id: uuid!("8c8acba2-67d3-40e0-ac62-2f7acce64ccc"),
5363            attributes_settings: ItemAttributeSettings {
5364                optional_attributes_count: 1,
5365                optional_attributes_ids: vec![uuid!("3a6ec1c8-7494-43df-a345-d23e297b892d")],
5366            },
5367            skin_id: None,
5368            exclude_from_mimic: false,
5369            required_class: None,
5370            fixed_power: None,
5371            next_mimic_item_code: None,
5372        },
5373        ItemTemplate {
5374            id: uuid!("8c7cdde1-e80b-4b40-a5d8-039bf39db419"),
5375            name: i18n::I18nString::new("content.items.8c7cdde1-e80b-4b40-a5d8-039bf39db419:name"),
5376            icon_url: "/godlike_scepter.png".to_string(),
5377            icon_path: "/godlike_scepter.png".to_string(),
5378            item_type: ItemType::Ring,
5379            world_side: None,
5380            rarity_id: uuid!("8c8acba2-67d3-40e0-ac62-2f7acce64ccc"),
5381            attributes_settings: ItemAttributeSettings {
5382                optional_attributes_count: 1,
5383                optional_attributes_ids: vec![uuid!("3a6ec1c8-7494-43df-a345-d23e297b892d")],
5384            },
5385            skin_id: None,
5386            exclude_from_mimic: false,
5387            required_class: None,
5388            fixed_power: None,
5389            next_mimic_item_code: None,
5390        },
5391    ]
5392}
5393
5394fn generate_items(item_rarities: &[ItemRarity]) -> Vec<ItemTemplate> {
5395    let basic_items = generate_basic_items();
5396    let mut result = basic_items.clone();
5397    for rarity in &item_rarities[1..] {
5398        for item in &basic_items {
5399            result.push(ItemTemplate {
5400                id: Uuid::now_v7(),
5401                rarity_id: rarity.id,
5402                ..item.clone()
5403            });
5404        }
5405    }
5406    result
5407}
5408
5409fn generate_bundles() -> Vec<bundles::BundleRaw> {
5410    let soft = uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0");
5411    let hard = uuid!("22297520-abb8-45b9-9fbb-c418b8001246");
5412    let unit = |currency_id: Uuid, amount: i64| CurrencyUnit {
5413        currency_id,
5414        amount,
5415    };
5416    let ability_1 = uuid!("7e584338-057d-41a2-a80e-8e2c76e98f6a");
5417    let ability_2 = uuid!("487a23e5-7401-4c34-a6c6-2401f95249cf");
5418    let shard = |ability_id: Uuid, amount: i64| bundles::BundleShardAmount { ability_id, amount };
5419    let test_item_id = uuid!("176c4cc5-7b93-43b4-9f90-958e2791a8d6");
5420    let empty_step = |item_type: bundles::BundleStepType| bundles::BundleRawStep {
5421        item_type,
5422        currencies: vec![],
5423        currency_branch: None,
5424        behavior: None,
5425        shards: vec![],
5426        item_template_ids: vec![],
5427        artifact_template_ids: vec![],
5428        item_ttl_seconds: None,
5429        has_pop_up: false,
5430    };
5431
5432    let first_steps = vec![
5433        bundles::BundleRawStep {
5434            currencies: vec![unit(soft, 80), unit(hard, 70)],
5435            has_pop_up: true,
5436            ..empty_step(bundles::BundleStepType::Currency)
5437        },
5438        bundles::BundleRawStep {
5439            shards: vec![shard(ability_1, 11), shard(ability_2, 1)],
5440            has_pop_up: true,
5441            ..empty_step(bundles::BundleStepType::Ability)
5442        },
5443        bundles::BundleRawStep {
5444            item_template_ids: vec![test_item_id],
5445            ..empty_step(bundles::BundleStepType::Item)
5446        },
5447    ];
5448
5449    let second_steps = vec![
5450        bundles::BundleRawStep {
5451            currencies: vec![unit(soft, 50), unit(hard, 170)],
5452            has_pop_up: true,
5453            ..empty_step(bundles::BundleStepType::Currency)
5454        },
5455        bundles::BundleRawStep {
5456            shards: vec![shard(ability_1, 6), shard(ability_2, 18)],
5457            has_pop_up: true,
5458            ..empty_step(bundles::BundleStepType::Ability)
5459        },
5460        // (`f86cff5-2ce6-4191-95cf-2271cc3686f5`, 7 hex in the first group).
5461        // No test asserts a second-bundle item reward (`test_multiple_bundles`
5462        // only walks the currency + ability steps), so this step grants nothing.
5463        empty_step(bundles::BundleStepType::Item),
5464    ];
5465
5466    let fourth_steps = vec![
5467        bundles::BundleRawStep {
5468            behavior: Some("test_afk_currency_step0".to_string()),
5469            has_pop_up: true,
5470            ..empty_step(bundles::BundleStepType::Currency)
5471        },
5472        empty_step(bundles::BundleStepType::Ability),
5473        empty_step(bundles::BundleStepType::Item),
5474    ];
5475
5476    let all_at_once_steps = vec![
5477        bundles::BundleRawStep {
5478            currencies: vec![unit(soft, 100), unit(hard, 200)],
5479            has_pop_up: true,
5480            ..empty_step(bundles::BundleStepType::Currency)
5481        },
5482        bundles::BundleRawStep {
5483            shards: vec![shard(ability_1, 25), shard(ability_2, 10)],
5484            ..empty_step(bundles::BundleStepType::Ability)
5485        },
5486        bundles::BundleRawStep {
5487            item_template_ids: vec![test_item_id],
5488            ..empty_step(bundles::BundleStepType::Item)
5489        },
5490    ];
5491
5492    let basic_bundle = vec![bundles::BundleRawStep {
5493        currencies: vec![unit(soft, 100)],
5494        has_pop_up: true,
5495        ..empty_step(bundles::BundleStepType::Currency)
5496    }];
5497
5498    let basic_bundle_no_popup = vec![bundles::BundleRawStep {
5499        currencies: vec![unit(soft, 100)],
5500        ..empty_step(bundles::BundleStepType::Currency)
5501    }];
5502
5503    let daily_rating_bundle = vec![bundles::BundleRawStep {
5504        currencies: vec![unit(soft, 20)],
5505        has_pop_up: true,
5506        ..empty_step(bundles::BundleStepType::Currency)
5507    }];
5508
5509    // The initial class's starter bundle, carrying ONE Artifact step and nothing
5510    // else. It exists to keep the storage-only claim arm
5511    // (`claim_raw_bundle_step_storage_only` -> `grant_bundle_artifacts_storage_only`)
5512    // under test: that arm runs BEFORE the starter-artifact grant at creation,
5513    // and a blind `ArtifactCollection::default()` write there used to delete
5514    // whatever it granted. `has_pop_up: false` — the creation path warns about
5515    // popups it cannot show.
5516    let starter_artifact_bundle = vec![bundles::BundleRawStep {
5517        artifact_template_ids: vec![THIN_MIRROR_ARTIFACT_ID],
5518        has_pop_up: false,
5519        ..empty_step(bundles::BundleStepType::Artifact)
5520    }];
5521
5522    // Mirrors the shipped top-1 weekly rating bundles: some currency plus the
5523    // artifact itself. Since the acquisition pass this is the ONLY way a
5524    // non-starter artifact can be earned, so the fixture has to carry the shape.
5525    let artifact_reward_bundle = vec![
5526        bundles::BundleRawStep {
5527            currencies: vec![unit(soft, 500)],
5528            has_pop_up: true,
5529            ..empty_step(bundles::BundleStepType::Currency)
5530        },
5531        bundles::BundleRawStep {
5532            artifact_template_ids: vec![HALFWAY_BELL_ARTIFACT_ID],
5533            has_pop_up: true,
5534            ..empty_step(bundles::BundleStepType::Artifact)
5535        },
5536    ];
5537
5538    vec![
5539        bundles::BundleRaw {
5540            id: uuid!("53e8ffaa-7ee4-4612-bbfc-bb157979ca8d"),
5541            steps: first_steps,
5542            claim_mode: bundles::BundleClaimMode::Sequential,
5543        },
5544        bundles::BundleRaw {
5545            id: uuid!("e18fc101-6615-440c-9a1d-42f509986ccc"),
5546            steps: second_steps,
5547            claim_mode: bundles::BundleClaimMode::Sequential,
5548        },
5549        bundles::BundleRaw {
5550            id: uuid!("c86eed27-03d0-4c06-a6b2-e8688c09f7ed"),
5551            steps: vec![],
5552            claim_mode: bundles::BundleClaimMode::Sequential,
5553        },
5554        bundles::BundleRaw {
5555            id: uuid!("1ed77d25-cc1e-41ee-8f46-c4934ca8f684"),
5556            steps: fourth_steps,
5557            claim_mode: bundles::BundleClaimMode::Sequential,
5558        },
5559        bundles::BundleRaw {
5560            id: uuid!("a7f3b8e2-9c4d-4a5e-b6f1-d8e9c7a2b1f0"),
5561            steps: all_at_once_steps,
5562            claim_mode: bundles::BundleClaimMode::AllAtOnce,
5563        },
5564        bundles::BundleRaw {
5565            id: uuid!("8838388b-6f13-4dc8-b0d4-561d8fe84da8"),
5566            steps: basic_bundle,
5567            claim_mode: bundles::BundleClaimMode::AllAtOnce,
5568        },
5569        bundles::BundleRaw {
5570            id: uuid!("9cc6c6e4-e87f-4443-99ce-2f88b2bdbe82"),
5571            steps: basic_bundle_no_popup,
5572            claim_mode: bundles::BundleClaimMode::AllAtOnce,
5573        },
5574        bundles::BundleRaw {
5575            id: uuid!("d1111111-1111-4111-8111-111111111111"),
5576            steps: daily_rating_bundle,
5577            claim_mode: bundles::BundleClaimMode::AllAtOnce,
5578        },
5579        bundles::BundleRaw {
5580            id: ARTIFACT_REWARD_BUNDLE_ID,
5581            steps: artifact_reward_bundle,
5582            claim_mode: bundles::BundleClaimMode::AllAtOnce,
5583        },
5584        bundles::BundleRaw {
5585            id: STARTER_ARTIFACT_BUNDLE_ID,
5586            steps: starter_artifact_bundle,
5587            claim_mode: bundles::BundleClaimMode::AllAtOnce,
5588        },
5589    ]
5590}
5591
5592/// Starter bundle of the INITIAL class (`game_settings.initial_class_id`),
5593/// granting `THIN_MIRROR_ARTIFACT_ID` and nothing else.
5594///
5595/// Every character created through the real creation path claims this, so it is
5596/// what keeps the storage-only artifact arm covered — see the comment on
5597/// `starter_artifact_bundle`.
5598pub const STARTER_ARTIFACT_BUNDLE_ID: uuid::Uuid = uuid!("d3333333-3333-4333-8333-333333333333");
5599
5600/// The fixture's stand-in for a shipped top-1 weekly rating bundle: currency +
5601/// `HALFWAY_BELL_ARTIFACT_ID`, claimed all at once. Appended LAST so every
5602/// index-based bundle test keeps pointing at the bundle it used to.
5603pub const ARTIFACT_REWARD_BUNDLE_ID: uuid::Uuid = uuid!("d2222222-2222-4222-8222-222222222222");
5604
5605fn generate_ability_presets_settings() -> ability_presets::AbilityPresetsSettings {
5606    ability_presets::AbilityPresetsSettings {
5607        max_presets_count: 4,
5608        default_preset_name: i18n::I18nString::new("content.ability_presets.default_preseset:name"),
5609    }
5610}
5611
5612fn generate_bots_settings() -> BotsSettings {
5613    BotsSettings {
5614        username_generation_settings: UsernameGenerationSettings {
5615            prefixes: vec!["Angry".to_string(), "Chubby".to_string()],
5616            suffixes: vec!["Lord".to_string(), "Queen".to_string()],
5617            template: "{prefix}{suffix} [bot]".to_string(),
5618        },
5619        photo_generation_settings: PhotoGenerationSettings {
5620            templates: vec!["img1".to_string(), "img2".to_string()],
5621        },
5622    }
5623}
5624
5625fn generate_reports_settings() -> ReportsSettings {
5626    ReportsSettings {
5627        ticket_categories: vec!["Technical support".to_string(), "Bug".to_string()],
5628        bug_categories: vec!["Bug".to_string()],
5629    }
5630}
5631
5632fn generate_afk_rewards_levels() -> Vec<AfkRewardsByLevel> {
5633    vec![AfkRewardsByLevel {
5634        chapter_level: 0,
5635        currency_rates: vec![CurrencyRate {
5636            currency_id: uuid!("2ed77d25-cc1e-41ee-8f46-c4934ca8f684"),
5637            rate_per_minute: PositiveF64::new(10.0),
5638        }],
5639        bonus_weights: NonEmptyVec::new(vec![
5640            AfkRewardBonusWeight {
5641                bonus_type: AfkRewardBonusType::Currency(uuid!(
5642                    "2ed77d25-cc1e-41ee-8f46-c4934ca8f684"
5643                )),
5644                weight: PositiveF64::new(1.0),
5645                count: PositiveI64::new(100),
5646            },
5647            AfkRewardBonusWeight {
5648                bonus_type: AfkRewardBonusType::Ability(uuid!(
5649                    "3ed77d25-cc1e-41ee-8f46-c4934ca8f684"
5650                )),
5651                weight: PositiveF64::new(0.5),
5652                count: PositiveI64::new(1),
5653            },
5654            AfkRewardBonusWeight {
5655                bonus_type: AfkRewardBonusType::Item(uuid!("4ed77d25-cc1e-41ee-8f46-c4934ca8f684")),
5656                weight: PositiveF64::new(0.5),
5657                count: PositiveI64::new(1),
5658            },
5659        ]),
5660    }]
5661}
5662
5663fn generate_afk_rewards_settings() -> AfkRewardsSettings {
5664    AfkRewardsSettings {
5665        bundle_id: uuid!("1ed77d25-cc1e-41ee-8f46-c4934ca8f684"),
5666        min_required_time_sec: 4,
5667        max_possible_time_sec: 8,
5668        bonus_calculation_rate_sec: NonZeroU64::new(60),
5669        instant_reward_duration_sec: 6,
5670        instant_reward_gems_prices: vec![50, 100, 150],
5671        afk_instant_ad_daily_limit: 2,
5672    }
5673}
5674
5675fn generate_users_generating_settings() -> UsersGeneratingSettings {
5676    UsersGeneratingSettings {
5677        username_generation_settings: UsernameGenerationSettings {
5678            prefixes: vec!["Big".to_string(), "Small".to_string()],
5679            suffixes: vec!["Egor".to_string(), "Danya".to_string()],
5680            template: "{prefix} {suffix}".to_string(),
5681        },
5682        photo_generation_settings: PhotoGenerationSettings {
5683            templates: vec!["img1".to_string(), "img2".to_string()],
5684        },
5685    }
5686}
5687
5688fn generate_skins() -> Vec<ConfigSkin> {
5689    vec![
5690        ConfigSkin {
5691            id: uuid!("59a4dd34-b395-436f-8f81-2fb36ee1b714"),
5692            name: i18n::I18nString::new("content.skins.59a4dd34-b395-436f-8f81-2fb36ee1b714:name"),
5693            unlock_description: Some(i18n::I18nString::Translated("Default face unlock".into())),
5694            code: "default_face".to_string(),
5695            skin_type: SkinType::Face,
5696            icon_url: "/skins/face_default.webp".to_string(),
5697            icon_path: "/skins/face_default.webp".to_string(),
5698            spine_path: "".to_string(),
5699            rarity_id: uuid!("8c8acba2-67d3-40e0-ac62-2f7acce64ccc"),
5700            price: None,
5701            color: Some("".into()),
5702        },
5703        ConfigSkin {
5704            id: uuid!("44d8fbed-9ec9-4a57-865a-59d32465a1f2"),
5705            name: i18n::I18nString::new("content.skins.44d8fbed-9ec9-4a57-865a-59d32465a1f2:name"),
5706            unlock_description: Some(i18n::I18nString::Translated(
5707                "Default hairstyle unlock".into(),
5708            )),
5709            code: "default_hairstyle".to_string(),
5710            skin_type: SkinType::Hairstyle,
5711            icon_url: "/skins/hair_default.webp".to_string(),
5712            icon_path: "/skins/face_default.webp".to_string(),
5713            spine_path: "".to_string(),
5714            rarity_id: uuid!("272f3e73-6b3e-43a0-88d6-d34bdaf5aeae"),
5715            price: Some(vec![CurrencyUnit {
5716                currency_id: uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
5717                amount: 100,
5718            }]),
5719            color: Some("".into()),
5720        },
5721        ConfigSkin {
5722            id: uuid!("a1b2c3d4-e5f6-7890-abcd-ef1234567890"),
5723            name: i18n::I18nString::new("content.skins.a1b2c3d4-e5f6-7890-abcd-ef1234567890:name"),
5724            unlock_description: Some(i18n::I18nString::Translated("Test armor unlock".into())),
5725            code: "test_armor".to_string(),
5726            skin_type: SkinType::Armor,
5727            icon_url: "/skins/armor_test.webp".to_string(),
5728            icon_path: "/skins/face_default.webp".to_string(),
5729            spine_path: "".to_string(),
5730            rarity_id: uuid!("8c8acba2-67d3-40e0-ac62-2f7acce64ccc"),
5731            price: None,
5732            color: Some("".into()),
5733        },
5734        ConfigSkin {
5735            id: uuid!("c0ffee00-1234-5678-9abc-def012345678"),
5736            name: i18n::I18nString::Translated("Helmet".into()),
5737            unlock_description: Some(i18n::I18nString::Translated("Test helmet unlock".into())),
5738            code: "test_helmet".to_string(),
5739            skin_type: SkinType::Helmet,
5740            icon_url: "/skins/helmet_test.webp".to_string(),
5741            icon_path: "/skins/helmet_test.webp".to_string(),
5742            spine_path: "".to_string(),
5743            rarity_id: uuid!("8c8acba2-67d3-40e0-ac62-2f7acce64ccc"),
5744            price: Some(vec![CurrencyUnit {
5745                currency_id: uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
5746                amount: 200,
5747            }]),
5748            color: Some("".into()),
5749        },
5750        ConfigSkin {
5751            id: uuid!("a1b2c3d4-e5f6-7890-abcd-ef1234567892"),
5752            name: i18n::I18nString::Translated("Alt armor".into()),
5753            unlock_description: Some(i18n::I18nString::Translated("Alt armor unlock".into())),
5754            code: "test_armor_alt".to_string(),
5755            skin_type: SkinType::Armor,
5756            icon_url: "/skins/armor_alt.webp".to_string(),
5757            icon_path: "/skins/armor_alt.webp".to_string(),
5758            spine_path: "".to_string(),
5759            rarity_id: uuid!("8c8acba2-67d3-40e0-ac62-2f7acce64ccc"),
5760            price: None,
5761            color: Some("".into()),
5762        },
5763    ]
5764}
5765
5766fn generate_skins_settings() -> SkinsSettings {
5767    SkinsSettings {
5768        default_unlocked_skins: vec![uuid!("59a4dd34-b395-436f-8f81-2fb36ee1b714")],
5769        default_equipped_skins: vec![uuid!("a1b2c3d4-e5f6-7890-abcd-ef1234567890")],
5770    }
5771}
5772
5773fn generate_offers_templates() -> Vec<OfferTemplate> {
5774    vec![
5775        OfferTemplate {
5776            id: uuid!("0fab3fca-d35c-4fbd-8f95-4033e548be6f"),
5777            alias: "First Daily".to_string(),
5778            title: i18n::I18nString::new("content.skins.a1b2c3d4-e5f6-7890-abcd-ef1234567890:name"),
5779            icon_path: String::new(),
5780            reward_bundle_id: Some(uuid!("8838388b-6f13-4dc8-b0d4-561d8fe84da8")),
5781            payment_type: PaymentType::InGameCurrency(vec![CurrencyUnit {
5782                currency_id: uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
5783                amount: 50,
5784            }]),
5785            limit_of_buys: Some(3),
5786            buys_reset_seconds: Some(86400),
5787            limit_buy_text: None,
5788            events_subscribe: vec![],
5789            shop_tab: ShopTab::DailyDeals,
5790            enabled: true,
5791            flag_new: false,
5792            flag_sale: None,
5793            value: None,
5794            priority: 0,
5795        },
5796        OfferTemplate {
5797            id: uuid!("4af19145-037d-4d9b-b9a5-98e7d8c7b127"),
5798            alias: "Second Daily".to_string(),
5799            title: i18n::I18nString::new("content.skins.a1b2c3d4-e5f6-7890-abcd-ef1234567890:name"),
5800            icon_path: String::new(),
5801            reward_bundle_id: Some(uuid!("8838388b-6f13-4dc8-b0d4-561d8fe84da8")),
5802            payment_type: PaymentType::InGameCurrency(vec![CurrencyUnit {
5803                currency_id: uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
5804                amount: 50,
5805            }]),
5806            limit_of_buys: Some(1),
5807            buys_reset_seconds: Some(86400),
5808            limit_buy_text: None,
5809            events_subscribe: vec![],
5810            shop_tab: ShopTab::DailyDeals,
5811            enabled: true,
5812            flag_new: false,
5813            flag_sale: None,
5814            value: None,
5815            priority: 0,
5816        },
5817        OfferTemplate {
5818            id: uuid!("6fea60b3-db1c-4a20-a03c-94716a32afbc"),
5819            alias: "First triggered".to_string(),
5820            title: i18n::I18nString::new("content.skins.a1b2c3d4-e5f6-7890-abcd-ef1234567890:name"),
5821            icon_path: String::new(),
5822            reward_bundle_id: Some(uuid!("8838388b-6f13-4dc8-b0d4-561d8fe84da8")),
5823            payment_type: PaymentType::InGameCurrency(vec![CurrencyUnit {
5824                currency_id: uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
5825                amount: 50,
5826            }]),
5827            limit_of_buys: Some(2),
5828            buys_reset_seconds: Some(86400),
5829            limit_buy_text: None,
5830            events_subscribe: vec!["EnableAutoSell".to_string()],
5831            shop_tab: ShopTab::Resources,
5832            enabled: true,
5833            flag_new: false,
5834            flag_sale: None,
5835            value: None,
5836            priority: 0,
5837        },
5838        OfferTemplate {
5839            id: uuid!("11111111-1111-1111-1111-111111111111"),
5840            alias: "Disabled offer".to_string(),
5841            title: i18n::I18nString::new("content.skins.a1b2c3d4-e5f6-7890-abcd-ef1234567890:name"),
5842            icon_path: String::new(),
5843            reward_bundle_id: Some(uuid!("8838388b-6f13-4dc8-b0d4-561d8fe84da8")),
5844            payment_type: PaymentType::InGameCurrency(vec![CurrencyUnit {
5845                currency_id: uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
5846                amount: 50,
5847            }]),
5848            limit_of_buys: Some(1),
5849            buys_reset_seconds: Some(86400),
5850            limit_buy_text: None,
5851            events_subscribe: vec![],
5852            shop_tab: ShopTab::DailyDeals,
5853            enabled: false,
5854            flag_new: false,
5855            flag_sale: None,
5856            value: None,
5857            priority: 0,
5858        },
5859        OfferTemplate {
5860            id: uuid!("22222222-2222-2222-2222-222222222222"),
5861            alias: "Free offer".to_string(),
5862            title: i18n::I18nString::new("content.skins.a1b2c3d4-e5f6-7890-abcd-ef1234567890:name"),
5863            icon_path: String::new(),
5864            reward_bundle_id: Some(uuid!("8838388b-6f13-4dc8-b0d4-561d8fe84da8")),
5865            payment_type: PaymentType::Free,
5866            limit_of_buys: Some(1),
5867            buys_reset_seconds: Some(86400),
5868            limit_buy_text: None,
5869            events_subscribe: vec!["EnableAutoChest".to_string()],
5870            shop_tab: ShopTab::DailyDeals,
5871            enabled: true,
5872            flag_new: false,
5873            flag_sale: None,
5874            value: None,
5875            priority: 0,
5876        },
5877        OfferTemplate {
5878            id: uuid!("33333333-3333-3333-3333-333333333333"),
5879            alias: "Ad offer".to_string(),
5880            title: i18n::I18nString::new("content.skins.a1b2c3d4-e5f6-7890-abcd-ef1234567890:name"),
5881            icon_path: String::new(),
5882            reward_bundle_id: Some(uuid!("8838388b-6f13-4dc8-b0d4-561d8fe84da8")),
5883            payment_type: PaymentType::Ad,
5884            limit_of_buys: Some(1),
5885            buys_reset_seconds: Some(86400),
5886            limit_buy_text: None,
5887            events_subscribe: vec!["DisableAutoChest".to_string()],
5888            shop_tab: ShopTab::Diamonds,
5889            enabled: true,
5890            flag_new: false,
5891            flag_sale: None,
5892            value: None,
5893            priority: 0,
5894        },
5895        OfferTemplate {
5896            id: uuid!("0b5cfa47-43bf-46db-930d-1d4a975f17d9"),
5897            alias: "Cash offer".to_string(),
5898            title: i18n::I18nString::new("content.skins.a1b2c3d4-e5f6-7890-abcd-ef1234567890:name"),
5899            icon_path: String::new(),
5900            reward_bundle_id: Some(uuid!("8838388b-6f13-4dc8-b0d4-561d8fe84da8")),
5901            payment_type: PaymentType::RealMoney,
5902            limit_of_buys: Some(1),
5903            buys_reset_seconds: Some(86400),
5904            limit_buy_text: None,
5905            events_subscribe: vec!["DisableCaseUpgradePopUp".to_string()],
5906            shop_tab: ShopTab::Diamonds,
5907            enabled: true,
5908            flag_new: false,
5909            flag_sale: None,
5910            value: None,
5911            priority: 0,
5912        },
5913        // Triggered by DisableAutoSell
5914        OfferTemplate {
5915            id: uuid!("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"),
5916            alias: "Chain start offer".to_string(),
5917            title: i18n::I18nString::new("content.skins.a1b2c3d4-e5f6-7890-abcd-ef1234567890:name"),
5918            icon_path: String::new(),
5919            reward_bundle_id: Some(uuid!("8838388b-6f13-4dc8-b0d4-561d8fe84da8")),
5920            payment_type: PaymentType::InGameCurrency(vec![CurrencyUnit {
5921                currency_id: uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
5922                amount: 50,
5923            }]),
5924            limit_of_buys: Some(1),
5925            buys_reset_seconds: Some(86400),
5926            limit_buy_text: None,
5927            events_subscribe: vec!["DisableAutoSell".to_string()],
5928            shop_tab: ShopTab::Resources,
5929            enabled: true,
5930            flag_new: false,
5931            flag_sale: None,
5932            value: None,
5933            priority: 0,
5934        },
5935        // Unlimited offer: no buy limit, added manually via NewOffer in tests.
5936        // Has a fake events_subscribe entry so it is not auto-created at character init.
5937        OfferTemplate {
5938            id: uuid!("dddddddd-dddd-dddd-dddd-dddddddddddd"),
5939            alias: "Unlimited offer".to_string(),
5940            title: i18n::I18nString::new("content.skins.a1b2c3d4-e5f6-7890-abcd-ef1234567890:name"),
5941            icon_path: String::new(),
5942            reward_bundle_id: Some(uuid!("8838388b-6f13-4dc8-b0d4-561d8fe84da8")),
5943            payment_type: PaymentType::Free,
5944            limit_of_buys: None,
5945            buys_reset_seconds: None,
5946            limit_buy_text: None,
5947            events_subscribe: vec!["_never".to_string()],
5948            shop_tab: ShopTab::DailyDeals,
5949            enabled: true,
5950            flag_new: false,
5951            flag_sale: None,
5952            value: None,
5953            priority: 0,
5954        },
5955        // Short-reset daily offer: auto-created, resets every 60 seconds (for reset tests)
5956        OfferTemplate {
5957            id: uuid!("eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee"),
5958            alias: "Short reset daily".to_string(),
5959            title: i18n::I18nString::new("content.skins.a1b2c3d4-e5f6-7890-abcd-ef1234567890:name"),
5960            icon_path: String::new(),
5961            reward_bundle_id: Some(uuid!("8838388b-6f13-4dc8-b0d4-561d8fe84da8")),
5962            payment_type: PaymentType::Free,
5963            limit_of_buys: Some(1),
5964            buys_reset_seconds: Some(60),
5965            limit_buy_text: None,
5966            events_subscribe: vec![],
5967            shop_tab: ShopTab::DailyDeals,
5968            enabled: true,
5969            flag_new: false,
5970            flag_sale: None,
5971            value: None,
5972            priority: 0,
5973        },
5974        // Progress Pass premium offer: lifetime one-shot (buys_reset_seconds: None),
5975        // no reward bundle (purchase effect is has_premium=true).
5976        OfferTemplate {
5977            id: uuid!("ff000000-0000-0000-0000-000000000001"),
5978            alias: "Progress Pass Premium".to_string(),
5979            title: i18n::I18nString::new("content.skins.a1b2c3d4-e5f6-7890-abcd-ef1234567890:name"),
5980            icon_path: String::new(),
5981            reward_bundle_id: None,
5982            payment_type: PaymentType::InGameCurrency(vec![CurrencyUnit {
5983                currency_id: uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
5984                amount: 50,
5985            }]),
5986            limit_of_buys: Some(1),
5987            buys_reset_seconds: None,
5988            limit_buy_text: None,
5989            events_subscribe: vec![],
5990            shop_tab: ShopTab::ProgressPass,
5991            enabled: true,
5992            flag_new: false,
5993            flag_sale: None,
5994            value: None,
5995            priority: 0,
5996        },
5997        // Disabled triggered offer: subscribes to DisableAutoSell but must never be activated
5998        OfferTemplate {
5999            id: uuid!("cccccccc-cccc-cccc-cccc-cccccccccccc"),
6000            alias: "Disabled triggered offer".to_string(),
6001            title: i18n::I18nString::new("content.skins.a1b2c3d4-e5f6-7890-abcd-ef1234567890:name"),
6002            icon_path: String::new(),
6003            reward_bundle_id: Some(uuid!("8838388b-6f13-4dc8-b0d4-561d8fe84da8")),
6004            payment_type: PaymentType::Free,
6005            limit_of_buys: Some(1),
6006            buys_reset_seconds: Some(86400),
6007            limit_buy_text: None,
6008            events_subscribe: vec!["DisableAutoSell".to_string()],
6009            shop_tab: ShopTab::DailyDeals,
6010            enabled: false,
6011            flag_new: false,
6012            flag_sale: None,
6013            value: None,
6014            priority: 0,
6015        },
6016    ]
6017}
6018
6019fn generate_shop_tabs() -> Vec<ShopTabConfig> {
6020    vec![
6021        ShopTabConfig {
6022            tab: ShopTab::DailyDeals,
6023            name: i18n::I18nString::Translated("Daily Deals".to_string()),
6024        },
6025        ShopTabConfig {
6026            tab: ShopTab::Diamonds,
6027            name: i18n::I18nString::Translated("Diamonds".to_string()),
6028        },
6029        ShopTabConfig {
6030            tab: ShopTab::Resources,
6031            name: i18n::I18nString::Translated("Resources".to_string()),
6032        },
6033        ShopTabConfig {
6034            tab: ShopTab::ProgressPass,
6035            name: i18n::I18nString::Translated("Progress Pass".to_string()),
6036        },
6037    ]
6038}
6039
6040fn generate_ratings_settings() -> Vec<RatingSettings> {
6041    vec![
6042        RatingSettings {
6043            id: uuid!("2a743358-aace-4404-8582-aae2c67bcb7b"),
6044            rating_type: RatingType::Arena,
6045            weekly_rating_range_rewards: vec![RatingRangeReward {
6046                bundle_id: uuid!("8838388b-6f13-4dc8-b0d4-561d8fe84da8"),
6047                legacy_mail_template_id: Some(uuid!("f3d6c5a1-2b4e-4c9a-9f61-8d2e9af2a1b1")),
6048                diapason_start: 1,
6049                diapason_end: Some(1),
6050            }],
6051            daily_rating_range_rewards: vec![RatingRangeReward {
6052                bundle_id: uuid!("d1111111-1111-4111-8111-111111111111"),
6053                legacy_mail_template_id: None,
6054                diapason_start: 1,
6055                diapason_end: Some(1),
6056            }],
6057        },
6058        RatingSettings {
6059            id: uuid!("a4b3388c-6d77-437d-aa41-91c4850ab01a"),
6060            rating_type: RatingType::Power,
6061            weekly_rating_range_rewards: vec![],
6062            daily_rating_range_rewards: vec![],
6063        },
6064        RatingSettings {
6065            id: uuid!("998fa5ac-9bd6-4b19-a13a-2e8c7ad6fcbd"),
6066            rating_type: RatingType::PvE,
6067            weekly_rating_range_rewards: vec![RatingRangeReward {
6068                bundle_id: uuid!("8838388b-6f13-4dc8-b0d4-561d8fe84da8"),
6069                legacy_mail_template_id: None,
6070                diapason_start: 1,
6071                diapason_end: Some(1),
6072            }],
6073            daily_rating_range_rewards: vec![RatingRangeReward {
6074                bundle_id: uuid!("d1111111-1111-4111-8111-111111111111"),
6075                legacy_mail_template_id: None,
6076                diapason_start: 1,
6077                diapason_end: Some(1),
6078            }],
6079        },
6080    ]
6081}
6082
6083fn generate_gatings() -> Gatings {
6084    Gatings {
6085        afk_rewards_button_unlock_chapter: 2,
6086        sidebar_navigation: SideBarNavigation {
6087            quests_button_unlock_chapter: 2,
6088            progress_pass_button_unlock_chapter: 2,
6089            arena_button_unlock_chapter: 2,
6090            ratings_button_unlock_chapter: 2,
6091            mail_button_unlock_chapter: 2,
6092        },
6093        party_unlock_chapter: 2,
6094        navbar_navigation: NavBarNavigation {
6095            castle: CastleGating {
6096                statue_unlock_chapter: 2,
6097                ..Default::default()
6098            },
6099            ..Default::default()
6100        },
6101        autochest: AutoChestGatings {
6102            chest_upgrade_button_unlock_chapter: 2,
6103            show_chest_click_tip_until_stage: 10,
6104            ..Default::default()
6105        },
6106        customization_screen_unlock_chapter: 37,
6107        ..Default::default()
6108    }
6109}
6110
6111pub fn generate_test_talents() -> Vec<TalentTemplate> {
6112    vec![
6113        TalentTemplate {
6114            id: uuid!("a1000000-0000-0000-0000-000000000001"),
6115            name: i18n::I18nString::new("content.skins.a1b2c3d4-e5f6-7890-abcd-ef1234567890:name"),
6116            description: i18n::I18nString::new(
6117                "content.skins.a1b2c3d4-e5f6-7890-abcd-ef1234567890:name",
6118            ),
6119            description_values_script: None,
6120            icon_url: String::new(),
6121            icon_path: String::new(),
6122            levels: vec![
6123                TalentLevel {
6124                    level: 1,
6125                    cost: CurrencyUnit {
6126                        currency_id: uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
6127                        amount: 100,
6128                    },
6129                    duration_sec: 10,
6130                    attribute_bonuses: vec![TalentAttributeBonus {
6131                        attribute_id: uuid!("45eca0a7-7430-487b-bd65-b796c6d88c08"),
6132                        value: 50,
6133                    }],
6134                    backend_modifiers: vec![],
6135                },
6136                TalentLevel {
6137                    level: 2,
6138                    cost: CurrencyUnit {
6139                        currency_id: uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
6140                        amount: 200,
6141                    },
6142                    duration_sec: 20,
6143                    attribute_bonuses: vec![TalentAttributeBonus {
6144                        attribute_id: uuid!("45eca0a7-7430-487b-bd65-b796c6d88c08"),
6145                        value: 50,
6146                    }],
6147                    backend_modifiers: vec![],
6148                },
6149            ],
6150            unlock_condition: TalentUnlockCondition {
6151                required_talents: vec![],
6152                required_points: 0,
6153            },
6154            position: TalentPosition { col: 0, row: 0 },
6155        },
6156        TalentTemplate {
6157            id: uuid!("a1000000-0000-0000-0000-000000000002"),
6158            name: i18n::I18nString::new("content.skins.a1b2c3d4-e5f6-7890-abcd-ef1234567890:name"),
6159            description: i18n::I18nString::new(
6160                "content.skins.a1b2c3d4-e5f6-7890-abcd-ef1234567890:name",
6161            ),
6162            description_values_script: None,
6163            icon_url: String::new(),
6164            icon_path: String::new(),
6165            levels: vec![
6166                TalentLevel {
6167                    level: 1,
6168                    cost: CurrencyUnit {
6169                        currency_id: uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
6170                        amount: 150,
6171                    },
6172                    duration_sec: 30,
6173                    attribute_bonuses: vec![],
6174                    backend_modifiers: vec![TalentBackendModifier::TalentResearchSpeedPercent(
6175                        20.0,
6176                    )],
6177                },
6178                TalentLevel {
6179                    level: 2,
6180                    cost: CurrencyUnit {
6181                        currency_id: uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
6182                        amount: 300,
6183                    },
6184                    duration_sec: 60,
6185                    attribute_bonuses: vec![],
6186                    backend_modifiers: vec![TalentBackendModifier::TalentResearchSpeedPercent(
6187                        20.0,
6188                    )],
6189                },
6190            ],
6191            unlock_condition: TalentUnlockCondition {
6192                required_talents: vec![uuid!("a1000000-0000-0000-0000-000000000001")],
6193                required_points: 0,
6194            },
6195            position: TalentPosition { col: 1, row: 0 },
6196        },
6197        // Long-duration talent for partial-skip tests (duration > skip_currency_skips_sec=900).
6198        TalentTemplate {
6199            id: uuid!("a1000000-0000-0000-0000-000000000003"),
6200            name: i18n::I18nString::new("content.skins.a1b2c3d4-e5f6-7890-abcd-ef1234567890:name"),
6201            description: i18n::I18nString::new(
6202                "content.skins.a1b2c3d4-e5f6-7890-abcd-ef1234567890:name",
6203            ),
6204            description_values_script: None,
6205            icon_url: String::new(),
6206            icon_path: String::new(),
6207            levels: vec![TalentLevel {
6208                level: 1,
6209                cost: CurrencyUnit {
6210                    currency_id: uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
6211                    amount: 100,
6212                },
6213                duration_sec: 2000,
6214                attribute_bonuses: vec![],
6215                backend_modifiers: vec![],
6216            }],
6217            unlock_condition: TalentUnlockCondition {
6218                required_talents: vec![],
6219                required_points: 0,
6220            },
6221            position: TalentPosition { col: 2, row: 0 },
6222        },
6223    ]
6224}
6225
6226fn generate_statue_bonus_grades() -> NonEmptyVec<StatueBonusGrade> {
6227    NonEmptyVec::new(vec![
6228        StatueBonusGrade {
6229            id: uuid!("a1000000-0000-0000-0000-000000000001"),
6230            name: i18n::I18nString::Translated("F".to_string()),
6231            color: "#808080".to_string(),
6232            order: 0,
6233        },
6234        StatueBonusGrade {
6235            id: uuid!("a1000000-0000-0000-0000-000000000002"),
6236            name: i18n::I18nString::Translated("D".to_string()),
6237            color: "#00aa00".to_string(),
6238            order: 1,
6239        },
6240        StatueBonusGrade {
6241            id: uuid!("a1000000-0000-0000-0000-000000000003"),
6242            name: i18n::I18nString::Translated("C".to_string()),
6243            color: "#0000ff".to_string(),
6244            order: 2,
6245        },
6246        StatueBonusGrade {
6247            id: uuid!("a1000000-0000-0000-0000-000000000009"),
6248            name: i18n::I18nString::Translated("SSS+".to_string()),
6249            color: "#ff0000".to_string(),
6250            order: 8,
6251        },
6252    ])
6253}
6254
6255fn generate_statue_settings() -> StatueSettings {
6256    StatueSettings {
6257        // statue currency
6258        currency_id: uuid!("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"),
6259        roll_costs: NonEmptyVec::new(vec![
6260            StatueRollCostConfig {
6261                locked_count: 0,
6262                cost: PositiveI64::new(100),
6263            },
6264            StatueRollCostConfig {
6265                locked_count: 1,
6266                cost: PositiveI64::new(300),
6267            },
6268            StatueRollCostConfig {
6269                locked_count: 2,
6270                cost: PositiveI64::new(500),
6271            },
6272            StatueRollCostConfig {
6273                locked_count: 3,
6274                cost: PositiveI64::new(700),
6275            },
6276            StatueRollCostConfig {
6277                locked_count: 4,
6278                cost: PositiveI64::new(900),
6279            },
6280            StatueRollCostConfig {
6281                locked_count: 5,
6282                cost: PositiveI64::new(1200),
6283            },
6284        ]),
6285    }
6286}
6287
6288fn generate_statue_bonus_type_configs() -> NonEmptyVec<StatueBonusTypeConfig> {
6289    let grade_f = uuid!("a1000000-0000-0000-0000-000000000001");
6290    let grade_d = uuid!("a1000000-0000-0000-0000-000000000002");
6291    let grade_c = uuid!("a1000000-0000-0000-0000-000000000003");
6292    let grade_sss_plus = uuid!("a1000000-0000-0000-0000-000000000009");
6293    let attr_hp = uuid!("45eca0a7-7430-487b-bd65-b796c6d88c08");
6294    let attr_damage = uuid!("6ad485d7-9567-4b77-8ddc-a2418dd1dfe6");
6295
6296    NonEmptyVec::new(vec![
6297        StatueBonusTypeConfig {
6298            attribute_id: attr_hp,
6299            grade_values: NonEmptyVec::new(vec![
6300                StatueBonusGradeValue {
6301                    grade_id: grade_f,
6302                    value: PositiveI32::new(100),
6303                },
6304                StatueBonusGradeValue {
6305                    grade_id: grade_d,
6306                    value: PositiveI32::new(200),
6307                },
6308                StatueBonusGradeValue {
6309                    grade_id: grade_c,
6310                    value: PositiveI32::new(300),
6311                },
6312                StatueBonusGradeValue {
6313                    grade_id: grade_sss_plus,
6314                    value: PositiveI32::new(1200),
6315                },
6316            ]),
6317        },
6318        StatueBonusTypeConfig {
6319            attribute_id: attr_damage,
6320            grade_values: NonEmptyVec::new(vec![
6321                StatueBonusGradeValue {
6322                    grade_id: grade_f,
6323                    value: PositiveI32::new(100),
6324                },
6325                StatueBonusGradeValue {
6326                    grade_id: grade_d,
6327                    value: PositiveI32::new(200),
6328                },
6329                StatueBonusGradeValue {
6330                    grade_id: grade_c,
6331                    value: PositiveI32::new(300),
6332                },
6333                StatueBonusGradeValue {
6334                    grade_id: grade_sss_plus,
6335                    value: PositiveI32::new(1200),
6336                },
6337            ]),
6338        },
6339    ])
6340}
6341
6342fn generate_statue_level_configs() -> NonEmptyVec<StatueLevelConfig> {
6343    let grade_f = uuid!("a1000000-0000-0000-0000-000000000001");
6344    let grade_d = uuid!("a1000000-0000-0000-0000-000000000002");
6345    let grade_c = uuid!("a1000000-0000-0000-0000-000000000003");
6346    let grade_sss_plus = uuid!("a1000000-0000-0000-0000-000000000009");
6347
6348    NonEmptyVec::new(vec![
6349        StatueLevelConfig {
6350            level: 1,
6351            required_experience: 0,
6352            slot_count: NonZeroU64::new(3),
6353            sets_count: NonZeroU64::new(1),
6354            grade_weights: NonEmptyVec::new(vec![
6355                StatueBonusGradeWeight {
6356                    grade_id: grade_f,
6357                    weight: PositiveF64::new(5.0),
6358                },
6359                StatueBonusGradeWeight {
6360                    grade_id: grade_d,
6361                    weight: PositiveF64::new(3.0),
6362                },
6363                StatueBonusGradeWeight {
6364                    grade_id: grade_c,
6365                    weight: PositiveF64::new(2.0),
6366                },
6367            ]),
6368        },
6369        StatueLevelConfig {
6370            level: 2,
6371            required_experience: 200,
6372            slot_count: NonZeroU64::new(4),
6373            sets_count: NonZeroU64::new(2),
6374            grade_weights: NonEmptyVec::new(vec![
6375                StatueBonusGradeWeight {
6376                    grade_id: grade_f,
6377                    weight: PositiveF64::new(4.0),
6378                },
6379                StatueBonusGradeWeight {
6380                    grade_id: grade_d,
6381                    weight: PositiveF64::new(3.0),
6382                },
6383                StatueBonusGradeWeight {
6384                    grade_id: grade_c,
6385                    weight: PositiveF64::new(2.0),
6386                },
6387                StatueBonusGradeWeight {
6388                    grade_id: grade_sss_plus,
6389                    weight: PositiveF64::new(1.0),
6390                },
6391            ]),
6392        },
6393    ])
6394}
6395
6396pub fn generate_game_config_for_tests() -> GameConfig {
6397    let item_rarities = generate_item_rarities();
6398    let mut game_config = GameConfig {
6399        attributes: generate_attributes(),
6400        item_cases_settings: generate_item_cases_settings(),
6401        game_settings: generate_game_settings(),
6402        ads_settings: generate_ads_settings(),
6403        items: vec![],
6404        portal_rarities: generate_portal_rarities(&item_rarities),
6405        item_rarities,
6406        skins: generate_skins(),
6407        skins_settings: generate_skins_settings(),
6408        effects: generate_effects(),
6409        abilities: generate_abilities(),
6410        ability_cases_settings: generate_ability_cases_settings(),
6411        ability_rarities: generate_ability_rarities(),
6412        ability_levels: generate_ability_levels(),
6413        ability_stones: generate_ability_stones(),
6414        ability_stone_settings: generate_ability_stone_settings(),
6415        mana_settings: ManaSettings {
6416            pool: 200.0,
6417            regen_per_second: 8.0,
6418        },
6419        fight_settings: generate_fight_settings(),
6420        flip_settings: FlipSettings {
6421            unlock_chapter: 0,
6422            progress_threshold: 1_000.0,
6423            // 0 keeps the damage source inert for ordinary fight tests, the
6424            // same convention as kill_drop.chance: damage-gauge tests set it
6425            // explicitly. The shipped value is 80 (BAL-026).
6426            damage_gauge_coefficient: 0.0,
6427        },
6428        cores_settings: generate_cores_settings(),
6429        pet_facet_settings: generate_pet_facet_settings(),
6430        laws: generate_laws(),
6431        stones_settings: generate_stones_settings(),
6432        // BAL-038 signed table: Cookies 2000, laws 5.659, equipment 42.14
6433        // (Trigger+Effect combined), artifact campaign kills 2.585 mob + 0.940
6434        // boss. Core Essence is band-driven and deliberately absent.
6435        kill_faucet_settings: crate::kill_faucets::KillFaucetSettings {
6436            direct_cookies_d: 2_000.0,
6437            // A currency distinct from `upgrade_currency_id` above, so a test can
6438            // tell a Cookie grant apart from a Core Essence grant.
6439            direct_cookie_currency_id: uuid!("6c050b07-5282-4a23-8bbf-dabecd27cade"),
6440            direct_cookie_tickets_per_kill: 5,
6441            direct_cookie_p0: 1.0,
6442            direct_cookie_b: 3_332.0,
6443            law_copies_d: 5.659,
6444            equipment_stones_d: 42.14,
6445            artifact_stones_d: 3.525,
6446            skill_chapter_currency_id: uuid!("0195964c-61d6-7340-9748-32a3a4b1cd57"),
6447            skill_chapter_cap_with_pass: 45,
6448            skill_chapter_cap_after_pass: 48,
6449            boss_gems_currency_id: uuid!("0194d64e-2162-76d3-8449-3e850f6e39e9"),
6450            boss_gems_daily_cap: 1_500,
6451        },
6452        trigger_stones: generate_trigger_stones(),
6453        effect_stones: generate_effect_stones(),
6454        artifacts_settings: generate_artifacts_settings(),
6455        artifacts: generate_artifacts(),
6456        artifact_stones: generate_artifact_stones(),
6457        collection_power: generate_collection_power(),
6458        entities: generate_entities(),
6459        fight_templates: generate_fight_templates(),
6460        dungeon_templates: generate_dungeon_templates(),
6461        chapters: generate_chapters(),
6462        character_levels: generate_character_levels(),
6463        character_level_formula: generate_character_level_formula(),
6464        ability_slots_levels: generate_ability_slots_levels(),
6465        patron_levels: generate_patron_levels(),
6466        quests: generate_quests(),
6467        quests_progression_settings: generate_quests_progression_settings(),
6468        vassals_settings: generate_vassals_settings(),
6469        vassal_tasks: generate_vassal_tasks(),
6470        pvp_settings: generate_pvp_settings(),
6471        gifts: generate_gifts(),
6472        currencies: generate_currencies(),
6473        inventory_levels: generate_inventory_levels(),
6474        arena_settings: generate_arena_settings(),
6475        arena_leagues: generate_arena_leagues(),
6476        matchmaking_settings: generate_matchmaking_settings(),
6477        event_descriptions: generate_event_descriptions(),
6478        classes: generate_classes(),
6479        class_levels: vec![],
6480        projectiles: generate_projectiles(),
6481        mail_templates: generate_mail_templates(),
6482        bundles: generate_bundles(),
6483        ability_presets_settings: generate_ability_presets_settings(),
6484        bots_settings: generate_bots_settings(),
6485        reports_settings: generate_reports_settings(),
6486        afk_rewards_settings: generate_afk_rewards_settings(),
6487        afk_rewards_levels: generate_afk_rewards_levels(),
6488        users_generating_settings: generate_users_generating_settings(),
6489        tutorial_steps: vec![],
6490        chats_settings: vec![],
6491        offers_templates: generate_offers_templates(),
6492        shop_tabs: generate_shop_tabs(),
6493        ratings_settings: generate_ratings_settings(),
6494        cheat_scripts: vec![],
6495        test_player_scripts: vec![],
6496        gatings: generate_gatings(),
6497        pet_rarities: generate_pet_rarities(),
6498        pet_templates: generate_pet_templates(),
6499        pet_levels: generate_pet_levels(),
6500        pet_slots_levels: generate_pet_slots_levels(),
6501        pet_cases_settings: generate_pet_cases_settings(),
6502        talent_tree_settings: TalentTreeSettings {
6503            time_skip_ticket_skips_sec: 1800,
6504            skip_currency_id: uuid!("6c050b07-5282-4a23-8bbf-dabecd27cade"),
6505            skip_currency_skips_sec: 900,
6506        },
6507        talents: generate_test_talents(),
6508        statue_bonus_grades: generate_statue_bonus_grades(),
6509        statue_bonus_type_configs: generate_statue_bonus_type_configs(),
6510        statue_level_configs: generate_statue_level_configs(),
6511        statue_settings: generate_statue_settings(),
6512        progress_pass: ProgressPassConfig {
6513            premium_offer_template_id: uuid!("ff000000-0000-0000-0000-000000000001"),
6514            tiers: vec![
6515                ProgressPassTierTemplate {
6516                    tier: 1,
6517                    quest_template_ids: vec![uuid!("aa000000-0000-0000-0000-000000000001")],
6518                    free_reward_bundle_id: uuid!("9cc6c6e4-e87f-4443-99ce-2f88b2bdbe82"),
6519                    paid_reward_bundle_id: uuid!("53e8ffaa-7ee4-4612-bbfc-bb157979ca8d"),
6520                },
6521                ProgressPassTierTemplate {
6522                    tier: 2,
6523                    quest_template_ids: vec![
6524                        uuid!("aa000000-0000-0000-0000-000000000001"),
6525                        uuid!("bb000000-0000-0000-0000-000000000002"),
6526                    ],
6527                    free_reward_bundle_id: uuid!("9cc6c6e4-e87f-4443-99ce-2f88b2bdbe82"),
6528                    paid_reward_bundle_id: uuid!("53e8ffaa-7ee4-4612-bbfc-bb157979ca8d"),
6529                },
6530                ProgressPassTierTemplate {
6531                    tier: 3,
6532                    quest_template_ids: vec![uuid!("cc000000-0000-0000-0000-000000000003")],
6533                    free_reward_bundle_id: uuid!("9cc6c6e4-e87f-4443-99ce-2f88b2bdbe82"),
6534                    paid_reward_bundle_id: uuid!("53e8ffaa-7ee4-4612-bbfc-bb157979ca8d"),
6535                },
6536            ],
6537        },
6538        buff_templates: vec![BuffTemplate {
6539            id: uuid!("b2c3d4e5-f6a7-8901-bcde-f12345678901"),
6540            title: i18n::I18nString::Translated("Daily Booster x1.5".to_string()),
6541            description: i18n::I18nString::Translated(
6542                "Exp and currency gain multiplied by 1.5 until end of day".to_string(),
6543            ),
6544            icon_path: String::new(),
6545            effect: BuffEffect::CurrencyExpBoost {
6546                multiplier: 1.5,
6547                currency_id: uuid!("00000000-0000-0000-0000-000000000000"),
6548            },
6549        }],
6550        bird_variants: vec![
6551            BirdVariant {
6552                id: uuid!("d1e2f3a4-b5c6-7890-abcd-ef1234567891"),
6553                spine_path: "/bird_variant_1".to_string(),
6554                bundle_id: uuid!("53e8ffaa-7ee4-4612-bbfc-bb157979ca8d"),
6555            },
6556            BirdVariant {
6557                id: uuid!("d1e2f3a4-b5c6-7890-abcd-ef1234567892"),
6558                spine_path: "/bird_variant_2".to_string(),
6559                bundle_id: uuid!("e18fc101-6615-440c-9a1d-42f509986ccc"),
6560            },
6561        ],
6562        plinko_settings: generate_plinko_settings(),
6563        local_notifications: generate_local_notifications_settings(),
6564    };
6565
6566    game_config.items = generate_items(&game_config.item_rarities);
6567
6568    game_config
6569}
6570
6571/// Stone tunables for tests. Sockets are all open from chapter 0 so a test does
6572/// not have to fake progression to exercise them; the shipped schedule is
6573/// gradual (see the storage YAML).
6574fn generate_stones_settings() -> StonesSettings {
6575    use essences::stones::{StoneSocketKey, StoneTier};
6576    use strum::IntoEnumIterator;
6577
6578    StonesSettings {
6579        global_trigger_cooldown_ticks: 1_000,
6580        tier_coefficients: StoneTier::iter()
6581            .flat_map(|trigger_tier| {
6582                StoneTier::iter().map(move |effect_tier| StoneTierCoefficient {
6583                    trigger_tier,
6584                    effect_tier,
6585                    multiplier: 1.0,
6586                })
6587            })
6588            .collect(),
6589        rarity_weights: vec![
6590            StoneRarityWeight {
6591                tier: StoneTier::Common,
6592                weight: 55.0,
6593            },
6594            StoneRarityWeight {
6595                tier: StoneTier::Rare,
6596                weight: 25.0,
6597            },
6598            StoneRarityWeight {
6599                tier: StoneTier::Epic,
6600                weight: 15.0,
6601            },
6602            StoneRarityWeight {
6603                tier: StoneTier::Legendary,
6604                weight: 5.0,
6605            },
6606        ],
6607        socket_unlocks: StoneSocketKey::all()
6608            .into_iter()
6609            .map(|key| StoneSocketUnlock {
6610                item_type: key.item_type,
6611                socket: key.socket,
6612                unlock_chapter: 0,
6613            })
6614            .collect(),
6615        // The shipped placeholder ladder: 2/3/5 copies for levels 2-4 come from
6616        // the design doc, the rest continues that sequence to the cap.
6617        upgrade_ladder: [2, 3, 5, 8, 13, 21, 34, 55, 89]
6618            .into_iter()
6619            .enumerate()
6620            .map(|(index, copies)| StoneUpgradeStep {
6621                level: index as i64 + 2,
6622                copies,
6623            })
6624            .collect(),
6625        max_stone_level: 10,
6626        // OFF in the fixture, for the same reason `cores_settings.kill_drop` is:
6627        // the roll draws from the fight RNG on every enemy death AND can emit a
6628        // `PlayerNewStones` event, which shifts the logical clock and therefore
6629        // every later seed. Left at 0.05 it desynced `test_wave_share`'s
6630        // fixed-seed equivalence by five drops. Tests that measure the faucet
6631        // (`test_stones.rs`) set it explicitly.
6632        kill_drop: StoneKillDropSettings {
6633            chance: 0.0,
6634            trigger_share: 0.5,
6635        },
6636        // Empty so ordinary tests never receive surprise grants; milestone
6637        // grant tests set their own schedule explicitly.
6638        milestone_grants: vec![],
6639    }
6640}
6641
6642/// Placeholder passive stat block of a test stone, scaled by the tier
6643/// multiplier the shipped content uses (Common 1, Rare 2, Epic 4).
6644///
6645/// `hp` and `armor` are deliberate: both are read by `power_from_attrs`, so a
6646/// socketed stone moves the power scalar in tests, which is what the stone-stat
6647/// tests assert. (This fixture's damage attribute has code `damage`, which the
6648/// scalar does not read.)
6649fn stone_stats(tier_multiplier: i64) -> Vec<StoneStat> {
6650    vec![
6651        StoneStat {
6652            attribute_id: uuid!("45eca0a7-7430-487b-bd65-b796c6d88c08"),
6653            value: 100 * tier_multiplier,
6654            value_per_level: 25 * tier_multiplier,
6655        },
6656        StoneStat {
6657            attribute_id: uuid!("f82eb3fa-8a09-44fb-b8ed-979cc2db28f4"),
6658            value: 20 * tier_multiplier,
6659            value_per_level: 5 * tier_multiplier,
6660        },
6661    ]
6662}
6663
6664/// The 24 Trigger Stones of design v0.2 §5, in catalog order.
6665///
6666/// Kept byte-for-byte in step with `overlord/admin/storage/content/trigger_stones`:
6667/// the runtime tests balance against these numbers, so a fixture that drifted
6668/// from the shipped catalog would prove the wrong thing.
6669fn generate_trigger_stones() -> Vec<TriggerStoneTemplate> {
6670    use essences::stones::StoneTier;
6671
6672    vec![
6673        // TR-C01
6674        TriggerStoneTemplate {
6675            id: uuid!("0199c0de-4c77-7c9c-8512-95fadb974672"),
6676            name: i18n::I18nString::Translated("Пять атак".to_string()),
6677            description: i18n::I18nString::Translated("Срабатывает на каждой 5-й обычной атаке.".to_string()),
6678            tier: StoneTier::Common,
6679            condition: TriggerCondition::EveryNthBasicAttack,
6680            condition_value: 5,
6681            condition_window_ticks: 0,
6682            active: true,
6683            gauge_gain: 1.0,
6684            power_proc_rate: 0.9,
6685            stats: stone_stats(1),
6686            icon_path: "".to_string(),
6687        },
6688        // TR-C02
6689        TriggerStoneTemplate {
6690            id: uuid!("0199c0de-399a-78a7-84c0-a473bbba0b36"),
6691            name: i18n::I18nString::Translated("Четыре полученных удара".to_string()),
6692            description: i18n::I18nString::Translated("Срабатывает на каждом 4-м полученном ударе.".to_string()),
6693            tier: StoneTier::Common,
6694            condition: TriggerCondition::EveryNthHitTaken,
6695            condition_value: 4,
6696            condition_window_ticks: 0,
6697            active: true,
6698            gauge_gain: 1.0,
6699            power_proc_rate: 0.75,
6700            stats: stone_stats(1),
6701            icon_path: "".to_string(),
6702        },
6703        // TR-C03
6704        TriggerStoneTemplate {
6705            id: uuid!("0199c0de-15fc-7d79-8b6a-5012b83f4bac"),
6706            name: i18n::I18nString::Translated("Дешёвый скилл".to_string()),
6707            description: i18n::I18nString::Translated("Срабатывает на скилле, который стоил не больше 15 маны.".to_string()),
6708            tier: StoneTier::Common,
6709            condition: TriggerCondition::SkillManaAtMost,
6710            condition_value: 15,
6711            condition_window_ticks: 0,
6712            active: true,
6713            gauge_gain: 1.0,
6714            power_proc_rate: 0.65,
6715            stats: stone_stats(1),
6716            icon_path: "".to_string(),
6717        },
6718        // TR-C04
6719        TriggerStoneTemplate {
6720            id: uuid!("0199c0de-8656-7836-84c4-8d3db364b8da"),
6721            name: i18n::I18nString::Translated("Два скилла".to_string()),
6722            description: i18n::I18nString::Translated("Срабатывает на каждом 2-м касте скилла.".to_string()),
6723            tier: StoneTier::Common,
6724            condition: TriggerCondition::EveryNthSkillCast,
6725            condition_value: 2,
6726            condition_window_ticks: 0,
6727            active: true,
6728            gauge_gain: 1.0,
6729            power_proc_rate: 0.65,
6730            stats: stone_stats(1),
6731            icon_path: "".to_string(),
6732        },
6733        // TR-C05
6734        TriggerStoneTemplate {
6735            id: uuid!("0199c0de-cd18-7d9b-8fac-16c4c59b1296"),
6736            name: i18n::I18nString::Translated("Три победы".to_string()),
6737            description: i18n::I18nString::Translated("Срабатывает на каждом 3-м убитом враге.".to_string()),
6738            tier: StoneTier::Common,
6739            condition: TriggerCondition::EveryNthDefeat,
6740            condition_value: 3,
6741            condition_window_ticks: 0,
6742            active: true,
6743            gauge_gain: 1.0,
6744            power_proc_rate: 0.5,
6745            stats: stone_stats(1),
6746            icon_path: "".to_string(),
6747        },
6748        // TR-C06
6749        TriggerStoneTemplate {
6750            id: uuid!("0199c0de-169b-7576-890a-617186f0d30b"),
6751            name: i18n::I18nString::Translated("Атака после скилла".to_string()),
6752            description: i18n::I18nString::Translated("Срабатывает на первой обычной атаке после каста скилла.".to_string()),
6753            tier: StoneTier::Common,
6754            condition: TriggerCondition::FirstAttackAfterSkill,
6755            condition_value: 0,
6756            condition_window_ticks: 0,
6757            active: true,
6758            gauge_gain: 1.0,
6759            power_proc_rate: 0.65,
6760            stats: stone_stats(1),
6761            icon_path: "".to_string(),
6762        },
6763        // TR-R01
6764        TriggerStoneTemplate {
6765            id: uuid!("0199c0de-f4a4-7672-830e-51aecbecd45d"),
6766            name: i18n::I18nString::Translated("На скилл".to_string()),
6767            description: i18n::I18nString::Translated("Срабатывает на каждом касте скилла.".to_string()),
6768            tier: StoneTier::Rare,
6769            condition: TriggerCondition::OnSkillCast,
6770            condition_value: 0,
6771            condition_window_ticks: 0,
6772            active: true,
6773            gauge_gain: 2.0,
6774            power_proc_rate: 0.75,
6775            stats: stone_stats(2),
6776            icon_path: "".to_string(),
6777        },
6778        // TR-R02
6779        TriggerStoneTemplate {
6780            id: uuid!("0199c0de-0c85-758a-8597-c470b7ab0b96"),
6781            name: i18n::I18nString::Translated("На крит".to_string()),
6782            description: i18n::I18nString::Translated("Срабатывает на каждом критическом ударе.".to_string()),
6783            tier: StoneTier::Rare,
6784            condition: TriggerCondition::OnCrit,
6785            condition_value: 0,
6786            condition_window_ticks: 0,
6787            active: true,
6788            gauge_gain: 2.0,
6789            power_proc_rate: 0.6,
6790            stats: stone_stats(2),
6791            icon_path: "".to_string(),
6792        },
6793        // TR-R03
6794        TriggerStoneTemplate {
6795            id: uuid!("0199c0de-d43b-748f-8f33-b48ee29e56e2"),
6796            name: i18n::I18nString::Translated("На победу".to_string()),
6797            description: i18n::I18nString::Translated("Срабатывает, когда враг убит.".to_string()),
6798            tier: StoneTier::Rare,
6799            condition: TriggerCondition::OnDefeat,
6800            condition_value: 0,
6801            condition_window_ticks: 0,
6802            active: true,
6803            gauge_gain: 2.0,
6804            power_proc_rate: 0.6,
6805            stats: stone_stats(2),
6806            icon_path: "".to_string(),
6807        },
6808        // TR-R04
6809        TriggerStoneTemplate {
6810            id: uuid!("0199c0de-ef30-7f95-8feb-a814ef433d84"),
6811            name: i18n::I18nString::Translated("Широкий каст".to_string()),
6812            description: i18n::I18nString::Translated("Срабатывает, когда скилл задел минимум 2 цели.".to_string()),
6813            tier: StoneTier::Rare,
6814            condition: TriggerCondition::SkillHitsAtLeastTargets,
6815            condition_value: 2,
6816            condition_window_ticks: 0,
6817            active: true,
6818            gauge_gain: 2.0,
6819            power_proc_rate: 0.5,
6820            stats: stone_stats(2),
6821            icon_path: "".to_string(),
6822        },
6823        // TR-R05
6824        TriggerStoneTemplate {
6825            id: uuid!("0199c0de-402c-7400-8fad-d1d6ac2159d6"),
6826            name: i18n::I18nString::Translated("Смена".to_string()),
6827            description: i18n::I18nString::Translated("Срабатывает, когда скилл отличается от предыдущего.".to_string()),
6828            tier: StoneTier::Rare,
6829            condition: TriggerCondition::SkillDiffersFromPrevious,
6830            condition_value: 0,
6831            condition_window_ticks: 0,
6832            active: true,
6833            gauge_gain: 2.0,
6834            power_proc_rate: 0.65,
6835            stats: stone_stats(2),
6836            icon_path: "".to_string(),
6837        },
6838        // TR-R06
6839        TriggerStoneTemplate {
6840            id: uuid!("0199c0de-a833-7d7d-8d44-3a130d34b7b0"),
6841            name: i18n::I18nString::Translated("Поправка прицела".to_string()),
6842            description: i18n::I18nString::Translated("Срабатывает на крите сразу после некритического удара.".to_string()),
6843            tier: StoneTier::Rare,
6844            condition: TriggerCondition::CritAfterNonCrit,
6845            condition_value: 0,
6846            condition_window_ticks: 0,
6847            active: true,
6848            gauge_gain: 2.0,
6849            power_proc_rate: 0.225,
6850            stats: stone_stats(2),
6851            icon_path: "".to_string(),
6852        },
6853        // TR-E01
6854        TriggerStoneTemplate {
6855            id: uuid!("0199c0de-4a31-7fc4-89f6-d5364a681cb6"),
6856            name: i18n::I18nString::Translated("На уклонение".to_string()),
6857            description: i18n::I18nString::Translated("Срабатывает на каждом уклонении.".to_string()),
6858            tier: StoneTier::Epic,
6859            condition: TriggerCondition::OnEvasion,
6860            condition_value: 0,
6861            condition_window_ticks: 0,
6862            active: true,
6863            gauge_gain: 4.0,
6864            power_proc_rate: 0.45,
6865            stats: stone_stats(4),
6866            icon_path: "".to_string(),
6867        },
6868        // TR-E02
6869        TriggerStoneTemplate {
6870            id: uuid!("0199c0de-3c04-73c6-809f-1dd7bc465b67"),
6871            name: i18n::I18nString::Translated("Большая трата".to_string()),
6872            description: i18n::I18nString::Translated("Срабатывает на скилле, который стоил не меньше 30 маны.".to_string()),
6873            tier: StoneTier::Epic,
6874            condition: TriggerCondition::SkillManaAtLeast,
6875            condition_value: 30,
6876            condition_window_ticks: 0,
6877            active: true,
6878            gauge_gain: 4.0,
6879            power_proc_rate: 0.45,
6880            stats: stone_stats(4),
6881            icon_path: "".to_string(),
6882        },
6883        // TR-E03
6884        TriggerStoneTemplate {
6885            id: uuid!("0199c0de-ed26-7766-8eea-0a0a88de6019"),
6886            name: i18n::I18nString::Translated("Опасная черта".to_string()),
6887            description: i18n::I18nString::Translated("Срабатывает один раз за фазу, когда здоровье падает ниже 50%.".to_string()),
6888            tier: StoneTier::Epic,
6889            condition: TriggerCondition::HpCrossedBelowPercent,
6890            condition_value: 50,
6891            condition_window_ticks: 0,
6892            active: true,
6893            gauge_gain: 4.0,
6894            power_proc_rate: 0.04,
6895            stats: stone_stats(4),
6896            icon_path: "".to_string(),
6897        },
6898        // TR-E04
6899        TriggerStoneTemplate {
6900            id: uuid!("0199c0de-06fe-7597-8e19-9a1733098e69"),
6901            name: i18n::I18nString::Translated("Двойной крит".to_string()),
6902            description: i18n::I18nString::Translated("Срабатывает после двух критических ударов подряд.".to_string()),
6903            tier: StoneTier::Epic,
6904            condition: TriggerCondition::OnCritStreak,
6905            condition_value: 2,
6906            condition_window_ticks: 0,
6907            active: true,
6908            gauge_gain: 4.0,
6909            power_proc_rate: 0.45,
6910            stats: stone_stats(4),
6911            icon_path: "".to_string(),
6912        },
6913        // TR-E05
6914        TriggerStoneTemplate {
6915            id: uuid!("0199c0de-d74d-725f-8001-431a5dd07bf4"),
6916            name: i18n::I18nString::Translated("Три разных скилла".to_string()),
6917            description: i18n::I18nString::Translated("Срабатывает, когда за 6 секунд исполнены три разных скилла.".to_string()),
6918            tier: StoneTier::Epic,
6919            condition: TriggerCondition::DistinctSkillsWithin,
6920            condition_value: 3,
6921            condition_window_ticks: 6000,
6922            active: true,
6923            gauge_gain: 4.0,
6924            power_proc_rate: 0.3,
6925            stats: stone_stats(4),
6926            icon_path: "".to_string(),
6927        },
6928        // TR-E06
6929        TriggerStoneTemplate {
6930            id: uuid!("0199c0de-7a64-7ab4-88f4-3226cff1740a"),
6931            name: i18n::I18nString::Translated("Держать строй".to_string()),
6932            description: i18n::I18nString::Translated("Срабатывает, когда один удар снял минимум 15% максимального здоровья, а герой выжил.".to_string()),
6933            tier: StoneTier::Epic,
6934            condition: TriggerCondition::SurvivedBigHitPercent,
6935            condition_value: 15,
6936            condition_window_ticks: 0,
6937            active: true,
6938            gauge_gain: 4.0,
6939            power_proc_rate: 0.15,
6940            stats: stone_stats(4),
6941            icon_path: "".to_string(),
6942        },
6943        // TR-L01
6944        TriggerStoneTemplate {
6945            id: uuid!("0199c0de-9ad7-7c75-831e-2457c152584a"),
6946            name: i18n::I18nString::Translated("Тройной крит".to_string()),
6947            description: i18n::I18nString::Translated("Срабатывает после трёх критических ударов подряд.".to_string()),
6948            tier: StoneTier::Legendary,
6949            condition: TriggerCondition::OnCritStreak,
6950            condition_value: 3,
6951            condition_window_ticks: 0,
6952            active: true,
6953            gauge_gain: 8.0,
6954            power_proc_rate: 0.525,
6955            stats: stone_stats(8),
6956            icon_path: "".to_string(),
6957        },
6958        // TR-L02
6959        TriggerStoneTemplate {
6960            id: uuid!("0199c0de-34b4-76ba-88bf-604f5d2b9a98"),
6961            name: i18n::I18nString::Translated("Двойное уклонение".to_string()),
6962            description: i18n::I18nString::Translated("Срабатывает на двух уклонениях за 3 секунды.".to_string()),
6963            tier: StoneTier::Legendary,
6964            condition: TriggerCondition::DodgesWithin,
6965            condition_value: 2,
6966            condition_window_ticks: 3000,
6967            active: true,
6968            gauge_gain: 8.0,
6969            power_proc_rate: 0.425,
6970            stats: stone_stats(8),
6971            icon_path: "".to_string(),
6972        },
6973        // TR-L03
6974        TriggerStoneTemplate {
6975            id: uuid!("0199c0de-e7bd-702d-8eac-79e00f21c10d"),
6976            name: i18n::I18nString::Translated("Полный гримуар".to_string()),
6977            description: i18n::I18nString::Translated("Срабатывает, когда каждый экипированный скилл исполнен хотя бы раз за фазу.".to_string()),
6978            tier: StoneTier::Legendary,
6979            condition: TriggerCondition::AllEquippedSkillsCastThisPhase,
6980            condition_value: 0,
6981            condition_window_ticks: 0,
6982            active: true,
6983            gauge_gain: 8.0,
6984            power_proc_rate: 0.04,
6985            stats: stone_stats(8),
6986            icon_path: "".to_string(),
6987        },
6988        // TR-L04
6989        TriggerStoneTemplate {
6990            id: uuid!("0199c0de-fef6-77f2-881d-b895a0b4f222"),
6991            name: i18n::I18nString::Translated("Идеальная связка".to_string()),
6992            description: i18n::I18nString::Translated("Срабатывает на связке скилл — обычная атака — другой скилл за 5 секунд.".to_string()),
6993            tier: StoneTier::Legendary,
6994            condition: TriggerCondition::SkillAttackSkillWithin,
6995            condition_value: 0,
6996            condition_window_ticks: 5000,
6997            active: true,
6998            gauge_gain: 8.0,
6999            power_proc_rate: 0.45,
7000            stats: stone_stats(8),
7001            icon_path: "".to_string(),
7002        },
7003        // TR-L05
7004        TriggerStoneTemplate {
7005            id: uuid!("0199c0de-4efa-7670-8c73-cd8e51982422"),
7006            name: i18n::I18nString::Translated("Каст без урона".to_string()),
7007            description: i18n::I18nString::Translated("Срабатывает на трёх скиллах подряд без полученного урона.".to_string()),
7008            tier: StoneTier::Legendary,
7009            condition: TriggerCondition::SkillsWithoutDamageTaken,
7010            condition_value: 3,
7011            condition_window_ticks: 0,
7012            active: true,
7013            gauge_gain: 8.0,
7014            power_proc_rate: 0.2,
7015            stats: stone_stats(8),
7016            icon_path: "".to_string(),
7017        },
7018        // TR-L06
7019        TriggerStoneTemplate {
7020            id: uuid!("0199c0de-8daa-792a-8c55-99744f71a2d7"),
7021            name: i18n::I18nString::Translated("Каст на последнем издыхании".to_string()),
7022            description: i18n::I18nString::Translated("Срабатывает на скилле, исполненном при здоровье ниже 20%.".to_string()),
7023            tier: StoneTier::Legendary,
7024            condition: TriggerCondition::SkillBelowHpPercent,
7025            condition_value: 20,
7026            condition_window_ticks: 0,
7027            active: true,
7028            gauge_gain: 8.0,
7029            power_proc_rate: 0.75,
7030            stats: stone_stats(8),
7031            icon_path: "".to_string(),
7032        },
7033    ]
7034}
7035
7036/// Ids of the shipped artifact content, shared with the storage YAML so a test
7037/// and the deployed config address the same objects.
7038pub const STARTER_ARTIFACT_ID: Uuid = uuid!("b9d88224-f738-4d79-9ac8-7cda79b52ffe");
7039/// `ART-03 Halfway Bell` — the flip-threshold World Law.
7040pub const HALFWAY_BELL_ARTIFACT_ID: Uuid = uuid!("e2b048b3-6b1e-47d5-875b-021e6dff4721");
7041/// `ART-02 Thin Mirror` — the hidden-law World Law.
7042pub const THIN_MIRROR_ARTIFACT_ID: Uuid = uuid!("5f9b07e5-b254-4f70-ab27-88dbd19f23de");
7043// Visible Aspect (VA-01 … VA-05). The first four ids are the ones the v0.1
7044// catalog already shipped, kept so an owned stone survives the renumbering.
7045pub const ASPECT_CRESCENDO_ID: Uuid = uuid!("a78e8c10-f50b-4a14-bd41-e7a071a2d0db");
7046pub const ASPECT_ALTERNATOR_ID: Uuid = uuid!("7481d5d0-8ab1-440c-a015-5039e5e3a02f");
7047pub const ASPECT_FOCUSED_ENGINE_ID: Uuid = uuid!("36923139-4945-44af-b535-dd3145daa3d0");
7048pub const ASPECT_OPENING_FIVE_ID: Uuid = uuid!("d75832de-f369-49ac-be81-1c2ea87ceb9f");
7049pub const ASPECT_SMALL_GEARS_ID: Uuid = uuid!("34bca40b-f50d-4d85-a568-ccd0ba7a78e5");
7050// Hidden Aspect (HA-01 … HA-05).
7051pub const ASPECT_BACKGROUND_VOICE_ID: Uuid = uuid!("eef53064-4778-4a1e-9d67-dd8c84047681");
7052pub const ASPECT_SHADOW_ROUND_ID: Uuid = uuid!("bedf8d6c-2dd7-42d6-a24e-6f951c73338d");
7053pub const ASPECT_CROSS_RELAY_ID: Uuid = uuid!("ab82f1e8-aab1-4cf2-b08a-56e698357bd1");
7054pub const ASPECT_SECOND_HALF_ID: Uuid = uuid!("91a3d5ba-d0d5-4ddb-ad99-f5d384ad9fe0");
7055pub const ASPECT_CATCH_UP_ID: Uuid = uuid!("595c10cb-b039-46df-80b8-fddf139ea0ae");
7056// Flip Aspect (FA-01 … FA-05).
7057pub const ASPECT_INCOMING_SALVO_ID: Uuid = uuid!("eb6c98ae-a999-405b-a4ae-f9c5f55e6e59");
7058pub const ASPECT_LAST_WORD_ID: Uuid = uuid!("2ecc953c-3b42-4431-b7dc-d00389958d9f");
7059pub const ASPECT_WARM_START_ID: Uuid = uuid!("5b30217b-7f4f-4669-ab85-6af46af98778");
7060pub const ASPECT_AFTERIMAGE_ID: Uuid = uuid!("1055fc02-6258-4818-b74e-5df9696a21b5");
7061pub const ASPECT_STAGGERED_ENTRANCE_ID: Uuid = uuid!("2af67d9f-8e4c-41d9-8fe5-7f252b1da76a");
7062// Visible Law (VL-01 … VL-05).
7063pub const LAW_SOURCE_FOCUS_ID: Uuid = uuid!("8336729e-bf34-470b-b3c2-cadfa105ab3e");
7064pub const LAW_CURRENT_POWER_ID: Uuid = uuid!("ed19ae9e-0de6-450c-b553-450dfca6705d");
7065pub const LAW_EXPORT_POWER_ID: Uuid = uuid!("8049b7d9-d693-4a91-acf4-efd6878e9b02");
7066pub const LAW_FULL_CONFIDENCE_ID: Uuid = uuid!("186af62d-664c-4413-b0f1-5eba8309d04a");
7067pub const LAW_FIRST_ARTICLE_ID: Uuid = uuid!("cb13d06f-79c8-4fc6-99a5-526f7f5df31f");
7068// Hidden Law (HL-01 … HL-05).
7069pub const LAW_PROXY_READER_ID: Uuid = uuid!("cc94cb7e-fe8e-4231-8c8c-811adce89731");
7070pub const LAW_LINKED_ECHO_ID: Uuid = uuid!("5ce4b4cb-4a17-4dad-a000-0ea52ea001ad");
7071pub const LAW_LATE_AWAKENING_ID: Uuid = uuid!("3f645b9f-0b57-422a-a52f-19f68e9ba9a2");
7072pub const LAW_SINGLE_LESSON_ID: Uuid = uuid!("83befea5-eea3-4870-9d20-d4935a619213");
7073pub const LAW_READY_TARGET_ID: Uuid = uuid!("ce6a4e39-205c-4f77-a4c0-3087d41568d9");
7074// Bridge Law (BL-01 … BL-03, BL-05 — `BL-04 Priority Gate` was cut, plan §8).
7075pub const LAW_EXTRA_SPAN_ID: Uuid = uuid!("7ccbec28-ce12-49ef-a67e-bf36211bdfa5");
7076pub const LAW_DEEP_SPAN_ID: Uuid = uuid!("18d7eb52-db67-4fc7-8f25-da11319159fe");
7077pub const LAW_SHORT_SPAN_ID: Uuid = uuid!("0fafa4e7-57b0-4105-970e-5758a0f697df");
7078pub const LAW_RECIPROCAL_GATE_ID: Uuid = uuid!("39170ce6-4f95-456b-993c-8575b32721b6");
7079
7080/// Artifact tunables for tests.
7081///
7082/// Every number is a design placeholder (plan §10 leaves them unassigned). All
7083/// six sockets are open from the unlock chapter so a test does not have to fake
7084/// progression; the shipped schedule (`artifacts_settings/_data.yaml`) staggers
7085/// them, and `test_artifacts.rs` covers the gate separately with its own
7086/// settings.
7087fn generate_artifacts_settings() -> ArtifactsSettings {
7088    use essences::artifacts::ArtifactSocketSlot;
7089
7090    ArtifactsSettings {
7091        // Must match CoresSettings::unlock_chapter (validate_artifacts asserts
7092        // it): artifacts and cores unlock together in the shipped config, both
7093        // at 21. Kept as low as cores allows, which is 1 since BAL-010.
7094        unlock_chapter: 1,
7095        starter_artifact_id: STARTER_ARTIFACT_ID,
7096        max_artifact_level: 3,
7097        upgrade_ladder: vec![
7098            ArtifactUpgradeStep {
7099                level: 2,
7100                copies: 1,
7101            },
7102            ArtifactUpgradeStep {
7103                level: 3,
7104                copies: 2,
7105            },
7106        ],
7107        socket_unlocks: vec![
7108            ArtifactSocketUnlock {
7109                socket: ArtifactSocketSlot::VisibleAspect,
7110                unlock_chapter: 0,
7111            },
7112            ArtifactSocketUnlock {
7113                socket: ArtifactSocketSlot::HiddenAspect,
7114                unlock_chapter: 0,
7115            },
7116            ArtifactSocketUnlock {
7117                socket: ArtifactSocketSlot::FlipAspect,
7118                unlock_chapter: 0,
7119            },
7120            ArtifactSocketUnlock {
7121                socket: ArtifactSocketSlot::VisibleLaw,
7122                unlock_chapter: LAW_COLUMN_UNLOCK_CHAPTER,
7123            },
7124            ArtifactSocketUnlock {
7125                socket: ArtifactSocketSlot::HiddenLaw,
7126                unlock_chapter: LAW_COLUMN_UNLOCK_CHAPTER,
7127            },
7128            ArtifactSocketUnlock {
7129                socket: ArtifactSocketSlot::BridgeLaw,
7130                unlock_chapter: LAW_COLUMN_UNLOCK_CHAPTER,
7131            },
7132        ],
7133        max_stone_level: 5,
7134        stone_upgrade_ladder: vec![
7135            ArtifactUpgradeStep {
7136                level: 2,
7137                copies: 2,
7138            },
7139            ArtifactUpgradeStep {
7140                level: 3,
7141                copies: 3,
7142            },
7143            ArtifactUpgradeStep {
7144                level: 4,
7145                copies: 5,
7146            },
7147            ArtifactUpgradeStep {
7148                level: 5,
7149                copies: 8,
7150            },
7151        ],
7152        halfway_bell_threshold_share: 0.6,
7153        thin_mirror_share: 0.3,
7154        stone_drop: ArtifactStoneDropSettings {
7155            chapter_boss_chance: 0.15,
7156            dungeon_chance: 0.25,
7157            // OFF in the fixture: a per-mob roll would perturb every
7158            // seed-sensitive combat test. `test_artifacts.rs` turns it on where
7159            // it measures the faucet.
7160            mob_kill_chance: 0.0,
7161        },
7162    }
7163}
7164
7165/// Chapter the Law column is scheduled at IN TESTS: open from the start, like
7166/// the Aspect column, so a right-column test does not have to fake progression.
7167/// The shipped schedule is in `artifacts_settings/_data.yaml`.
7168pub const LAW_COLUMN_UNLOCK_CHAPTER: i64 = 0;
7169
7170/// The fixture's `hp` attribute — mod-capable, so an ownership bonus on it is
7171/// actually composed (`configs::artifacts::MOD_CAPABLE_ATTRIBUTE_CODES`).
7172const HP_ATTRIBUTE_ID: Uuid = uuid!("45eca0a7-7430-487b-bd65-b796c6d88c08");
7173/// The fixture's `armor` attribute — likewise mod-capable.
7174const ARMOR_ATTRIBUTE_ID: Uuid = uuid!("f82eb3fa-8a09-44fb-b8ed-979cc2db28f4");
7175
7176fn bonus(attribute_id: Uuid, percent: f64, percent_per_level: f64) -> ArtifactOwnershipBonusStat {
7177    ArtifactOwnershipBonusStat {
7178        attribute_id,
7179        percent,
7180        percent_per_level,
7181    }
7182}
7183
7184/// The starter frame (`ART-00`), `ART-03 Halfway Bell` and `ART-02 Thin Mirror`
7185/// — the whole v0.2 pool.
7186///
7187/// Ownership bonuses are per-template now: the starter pays armor (it is no
7188/// longer a free-but-worthless key), the bell pays hp, and the mirror pays two
7189/// stats at once so the multi-attribute path is covered.
7190fn generate_artifacts() -> Vec<ArtifactTemplate> {
7191    vec![
7192        ArtifactTemplate {
7193            id: STARTER_ARTIFACT_ID,
7194            name: i18n::I18nString::Translated("Простой ключ от ворот".to_string()),
7195            description: i18n::I18nString::Translated("Нет закона мира.".to_string()),
7196            world_law: None,
7197            power_q: 1.0,
7198            ownership_bonuses: vec![bonus(ARMOR_ATTRIBUTE_ID, 1.0, 2.0)],
7199            icon_path: "".to_string(),
7200        },
7201        ArtifactTemplate {
7202            id: HALFWAY_BELL_ARTIFACT_ID,
7203            name: i18n::I18nString::Translated("Колокол на полпути".to_string()),
7204            description: i18n::I18nString::Translated(
7205                "Переворот мира происходит на 60% обычного порога. Остальные источники шкалы не \
7206                 меняются."
7207                    .to_string(),
7208            ),
7209            world_law: Some(ArtifactWorldLaw::HalfwayBell),
7210            power_q: 1.1,
7211            ownership_bonuses: vec![bonus(HP_ATTRIBUTE_ID, 1.0, 2.0)],
7212            icon_path: "".to_string(),
7213        },
7214        ArtifactTemplate {
7215            id: THIN_MIRROR_ARTIFACT_ID,
7216            name: i18n::I18nString::Translated("Тонкое зеркало".to_string()),
7217            description: i18n::I18nString::Translated(
7218                "Законы скрытой стороны продолжают проверять свои условия и применяют эффект на \
7219                 30% силы. Скрытая активация не даёт ни резонанса, ни заряда моста, ни шкалы \
7220                 переворота."
7221                    .to_string(),
7222            ),
7223            world_law: Some(ArtifactWorldLaw::ThinMirror),
7224            // The MULTI-attribute case, which the shipped catalog does not
7225            // currently exercise: one artifact paying two different stats at two
7226            // different rates. Shipped Thin Mirror pays `attack`; the fixture has
7227            // no `attack` attribute (its damage stat is coded `damage`, which
7228            // nothing composes as `.mod`), so it pays hp + armor here instead —
7229            // the shape under test is "a list", not which stats are in it.
7230            power_q: 1.06,
7231            ownership_bonuses: vec![
7232                bonus(HP_ATTRIBUTE_ID, 2.0, 1.0),
7233                bonus(ARMOR_ATTRIBUTE_ID, 4.0, 0.0),
7234            ],
7235            icon_path: "".to_string(),
7236        },
7237    ]
7238}
7239
7240/// The whole catalog: fifteen left-column Aspect stones and fourteen
7241/// right-column Law stones — five per socket, except Bridge Law's four
7242/// (`BL-04 Priority Gate` was cut, post-merge plan §8).
7243///
7244/// Rank scales the number and opens no new mechanic, so a level moves
7245/// `magnitude` alone. Nothing ships switched off any more, and
7246/// `ArtifactStoneRule::unavailable_dependency` is still what decides that here
7247/// rather than a hand-maintained list.
7248fn generate_artifact_stones() -> Vec<ArtifactStoneTemplate> {
7249    /// `numbers` is the catalog row of the rule: its magnitude at rank 1, the
7250    /// step a level adds, its second percentage, and its own parameter.
7251    fn stone(
7252        id: Uuid,
7253        name: &str,
7254        description: &str,
7255        rule: ArtifactStoneRule,
7256        numbers: (f64, f64, f64, i64),
7257    ) -> ArtifactStoneTemplate {
7258        let (magnitude, magnitude_per_level, secondary_magnitude, rule_param) = numbers;
7259        ArtifactStoneTemplate {
7260            id,
7261            name: i18n::I18nString::Translated(name.to_string()),
7262            description: i18n::I18nString::Translated(description.to_string()),
7263            socket: rule.socket(),
7264            rule,
7265            magnitude,
7266            magnitude_per_level,
7267            secondary_magnitude,
7268            rule_param,
7269            tier_magnitudes: Vec::new(),
7270            power_q_first_rank: 1.03,
7271            power_q_max_rank: 1.06,
7272            active: rule.unavailable_dependency().is_none(),
7273            icon_path: "".to_string(),
7274        }
7275    }
7276
7277    let mut small_gears = stone(
7278        ASPECT_SMALL_GEARS_ID,
7279        "Мелкие шестерни",
7280        "Эффекты от триггеров младших тиров работают сильнее, от старших — обычно.",
7281        ArtifactStoneRule::SmallGears,
7282        (130.0, 5.0, 0.0, 0),
7283    );
7284    small_gears.tier_magnitudes = vec![
7285        ArtifactTierMagnitude {
7286            tier: essences::stones::StoneTier::Common,
7287            magnitude: 130.0,
7288        },
7289        ArtifactTierMagnitude {
7290            tier: essences::stones::StoneTier::Rare,
7291            magnitude: 115.0,
7292        },
7293        ArtifactTierMagnitude {
7294            tier: essences::stones::StoneTier::Epic,
7295            magnitude: 105.0,
7296        },
7297        ArtifactTierMagnitude {
7298            tier: essences::stones::StoneTier::Legendary,
7299            magnitude: 100.0,
7300        },
7301    ];
7302
7303    vec![
7304        // Visible Aspect — how active gear effects behave.
7305        stone(
7306            ASPECT_CRESCENDO_ID,
7307            "Крещендо",
7308            "Активные эффекты растут по мере заполнения шкалы: от 85% на пустой до 130% перед \
7309             переворотом.",
7310            ArtifactStoneRule::Crescendo,
7311            (130.0, 10.0, 85.0, 0),
7312        ),
7313        stone(
7314            ASPECT_ALTERNATOR_ID,
7315            "Чередование",
7316            "Срабатывание другого слота идёт на 125%, повтор предыдущего — на 90%.",
7317            ArtifactStoneRule::Alternator,
7318            (125.0, 5.0, 90.0, 0),
7319        ),
7320        stone(
7321            ASPECT_FOCUSED_ENGINE_ID,
7322            "Сфокусированный мотор",
7323            "Повтор одного слота добавляет 10%, максимум 30%; другой слот сбрасывает бонус.",
7324            ArtifactStoneRule::FocusedEngine,
7325            (10.0, 2.0, 30.0, 0),
7326        ),
7327        stone(
7328            ASPECT_OPENING_FIVE_ID,
7329            "Открывающая пятёрка",
7330            "Первое срабатывание каждого из пяти слотов за фазу идёт на 150%.",
7331            ArtifactStoneRule::OpeningFive,
7332            (150.0, 20.0, 0.0, 0),
7333        ),
7334        small_gears,
7335        // Hidden Aspect — what hidden gear effects do.
7336        stone(
7337            ASPECT_BACKGROUND_VOICE_ID,
7338            "Фоновый голос",
7339            "Каждый триггер запускает активный эффект целиком и парный скрытый на 30%.",
7340            ArtifactStoneRule::BackgroundVoice,
7341            (30.0, 5.0, 0.0, 0),
7342        ),
7343        stone(
7344            ASPECT_SHADOW_ROUND_ID,
7345            "Теневой круг",
7346            "Раз в 2 секунды следующий скрытый эффект по порядку слотов срабатывает на 60%.",
7347            ArtifactStoneRule::ShadowRound,
7348            (60.0, 10.0, 0.0, 2_000),
7349        ),
7350        stone(
7351            ASPECT_CROSS_RELAY_ID,
7352            "Перекрёстная передача",
7353            "Срабатывание слота дополнительно запускает скрытый эффект следующего слота на 35%.",
7354            ArtifactStoneRule::CrossRelay,
7355            (35.0, 5.0, 0.0, 0),
7356        ),
7357        stone(
7358            ASPECT_SECOND_HALF_ID,
7359            "Вторая половина",
7360            "После половины шкалы парный скрытый эффект сопровождает каждое срабатывание на 50%.",
7361            ArtifactStoneRule::SecondHalf,
7362            (50.0, 5.0, 50.0, 0),
7363        ),
7364        stone(
7365            ASPECT_CATCH_UP_ID,
7366            "Догоняющий",
7367            "Если активный эффект применить некуда, вместо него на 100% работает парный скрытый.",
7368            ArtifactStoneRule::CatchUp,
7369            (100.0, 10.0, 0.0, 0),
7370        ),
7371        // Flip Aspect — the transition itself.
7372        stone(
7373            ASPECT_INCOMING_SALVO_ID,
7374            "Входящий залп",
7375            "После переворота все пять новых активных эффектов срабатывают на 50%.",
7376            ArtifactStoneRule::IncomingSalvo,
7377            (50.0, 10.0, 0.0, 0),
7378        ),
7379        stone(
7380            ASPECT_LAST_WORD_ID,
7381            "Последнее слово",
7382            "Перед переворотом все пять уходящих активных эффектов срабатывают на 50%.",
7383            ArtifactStoneRule::LastWord,
7384            (50.0, 10.0, 0.0, 0),
7385        ),
7386        stone(
7387            ASPECT_WARM_START_ID,
7388            "Тёплый старт",
7389            "Первое обычное срабатывание каждого нового активного эффекта повторяется на 60%.",
7390            ArtifactStoneRule::WarmStart,
7391            (60.0, 10.0, 0.0, 0),
7392        ),
7393        stone(
7394            ASPECT_AFTERIMAGE_ID,
7395            "Послеобраз",
7396            "Первые пять срабатываний после переворота сопровождает уходящий парный эффект на 40%.",
7397            ArtifactStoneRule::Afterimage,
7398            (40.0, 10.0, 0.0, 5),
7399        ),
7400        stone(
7401            ASPECT_STAGGERED_ENTRANCE_ID,
7402            "Поочерёдный вход",
7403            "Пять новых активных эффектов идут по порядку с шагом 0,5 секунды на 70%.",
7404            ArtifactStoneRule::StaggeredEntrance,
7405            (70.0, 10.0, 0.0, 500),
7406        ),
7407        // Visible Law — the laws of the side that is currently up. The negative
7408        // second numbers are the design, not a typo: every one of these is a
7409        // trade of Effect against Resonance in one direction or the other.
7410        stone(
7411            LAW_SOURCE_FOCUS_ID,
7412            "Фокус источника",
7413            "Выбранный закон даёт на 50% больше резонанса, а его эффект слабее на 15%.",
7414            ArtifactStoneRule::SourceFocus,
7415            (50.0, 10.0, -15.0, 0),
7416        ),
7417        stone(
7418            LAW_CURRENT_POWER_ID,
7419            "Сила сейчас",
7420            "Эффект выбранного закона сильнее на 35%, его резонанс слабее на 25%.",
7421            ArtifactStoneRule::CurrentPower,
7422            (35.0, 5.0, -25.0, 0),
7423        ),
7424        stone(
7425            LAW_EXPORT_POWER_ID,
7426            "Экспорт силы",
7427            "Резонанс выбранного закона больше на 75%, его эффект слабее на 25%.",
7428            ArtifactStoneRule::ExportPower,
7429            (75.0, 15.0, -25.0, 0),
7430        ),
7431        stone(
7432            LAW_FULL_CONFIDENCE_ID,
7433            "Полная уверенность",
7434            "Как только выбранный закон заполнил свой мост, его эффект до переворота сильнее на \
7435             30%.",
7436            ArtifactStoneRule::FullConfidence,
7437            (30.0, 5.0, 0.0, 0),
7438        ),
7439        stone(
7440            LAW_FIRST_ARTICLE_ID,
7441            "Первая статья",
7442            "Первое за фазу срабатывание каждого закона активной стороны применяет эффект на \
7443             150%. Резонанс обычный.",
7444            ArtifactStoneRule::FirstArticle,
7445            (150.0, 10.0, 0.0, 0),
7446        ),
7447        // Hidden Law — the laws of the side that is currently down.
7448        stone(
7449            LAW_PROXY_READER_ID,
7450            "Чтец по доверенности",
7451            "Выбранный скрытый закон проверяет свои условия и применяет эффект на 30%.",
7452            ArtifactStoneRule::ProxyReader,
7453            (30.0, 5.0, 0.0, 0),
7454        ),
7455        stone(
7456            LAW_LINKED_ECHO_ID,
7457            "Связанное эхо",
7458            "При срабатывании выбранного закона его партнёр по мосту применяет эффект на 20%, не \
7459             проверяя собственное условие.",
7460            ArtifactStoneRule::LinkedEcho,
7461            (20.0, 5.0, 0.0, 0),
7462        ),
7463        stone(
7464            LAW_LATE_AWAKENING_ID,
7465            "Позднее пробуждение",
7466            "После 70% шкалы выбранный скрытый закон работает на 100%.",
7467            ArtifactStoneRule::LateAwakening,
7468            (100.0, 10.0, 70.0, 0),
7469        ),
7470        stone(
7471            LAW_SINGLE_LESSON_ID,
7472            "Один урок",
7473            "Первое подходящее Core-событие за фазу будит выбранный скрытый закон на 100%, дальше \
7474             камень молчит до переворота.",
7475            ArtifactStoneRule::SingleLesson,
7476            (100.0, 10.0, 0.0, 0),
7477        ),
7478        stone(
7479            LAW_READY_TARGET_ID,
7480            "Готовая цель",
7481            "Как только выбранный мост заполнен, его скрытый конец работает на 80% до переворота.",
7482            ArtifactStoneRule::ReadyTarget,
7483            (80.0, 10.0, 0.0, 0),
7484        ),
7485        // Bridge Law — the bridges themselves.
7486        stone(
7487            LAW_EXTRA_SPAN_ID,
7488            "Лишний пролёт",
7489            "Разрешён один дополнительный мост, все мосты копят на 10% больше резонанса.",
7490            ArtifactStoneRule::ExtraSpan,
7491            (10.0, 5.0, 0.0, 1),
7492        ),
7493        stone(
7494            LAW_DEEP_SPAN_ID,
7495            "Глубокий пролёт",
7496            "Ёмкость всех мостов и потолок усиления получателя больше на 40% и растут вместе с \
7497             рангом.",
7498            ArtifactStoneRule::DeepSpan,
7499            (40.0, 5.0, 0.0, 0),
7500        ),
7501        stone(
7502            LAW_SHORT_SPAN_ID,
7503            "Короткий пролёт",
7504            "Ёмкость мостов меньше на 40%, усиление получателя — на 20%: мост короче, но каждая \
7505             единица заряда дороже.",
7506            ArtifactStoneRule::ShortSpan,
7507            (-40.0, -5.0, -20.0, 0),
7508        ),
7509        stone(
7510            LAW_RECIPROCAL_GATE_ID,
7511            "Обоюдные ворота",
7512            "Оба направления выбранного моста получают на 25% больше резонанса.",
7513            ArtifactStoneRule::ReciprocalGate,
7514            (25.0, 5.0, 0.0, 0),
7515        ),
7516    ]
7517}
7518
7519/// The 32 Effect Stones of design v0.2 §6, in catalog order. The pool is shared
7520/// between the two world sides: a slot picks its Real and its Fantasy effect out
7521/// of this same list.
7522fn generate_effect_stones() -> Vec<EffectStoneTemplate> {
7523    use essences::stones::StoneTier;
7524
7525    vec![
7526        // EF-C01
7527        EffectStoneTemplate {
7528            id: uuid!("0199c0de-4049-7b1d-877d-d777b61fab31"),
7529            name: i18n::I18nString::Translated("Прямой удар".to_string()),
7530            description: i18n::I18nString::Translated(
7531                "Наносит текущей цели 80% от атаки.".to_string(),
7532            ),
7533            tier: StoneTier::Common,
7534            action: EffectStoneAction::InstantDamage,
7535            magnitude: 80.0,
7536            magnitude_per_level: 20.0,
7537            secondary_magnitude: 0.0,
7538            charges: 0,
7539            duration_ticks: 0,
7540            power_q_base_first_rank: 1.1,
7541            power_q_base_max_rank: 1.326,
7542            stats: stone_stats(1),
7543            icon_path: "".to_string(),
7544        },
7545        // EF-C02
7546        EffectStoneTemplate {
7547            id: uuid!("0199c0de-601c-7e3e-8011-13fe757b80c1"),
7548            name: i18n::I18nString::Translated("Широкий взмах".to_string()),
7549            description: i18n::I18nString::Translated(
7550                "Наносит всем врагам 45% от атаки.".to_string(),
7551            ),
7552            tier: StoneTier::Common,
7553            action: EffectStoneAction::InstantDamageAll,
7554            magnitude: 45.0,
7555            magnitude_per_level: 11.0,
7556            secondary_magnitude: 0.0,
7557            charges: 0,
7558            duration_ticks: 0,
7559            power_q_base_first_rank: 1.168,
7560            power_q_base_max_rank: 1.54,
7561            stats: stone_stats(1),
7562            icon_path: "".to_string(),
7563        },
7564        // EF-C03
7565        EffectStoneTemplate {
7566            id: uuid!("0199c0de-8810-7f82-8f0d-64931d97daa1"),
7567            name: i18n::I18nString::Translated("Быстрая перевязка".to_string()),
7568            description: i18n::I18nString::Translated(
7569                "Восстанавливает 4% максимального здоровья.".to_string(),
7570            ),
7571            tier: StoneTier::Common,
7572            action: EffectStoneAction::HealMaxHpPercent,
7573            magnitude: 4.0,
7574            magnitude_per_level: 1.0,
7575            secondary_magnitude: 0.0,
7576            charges: 0,
7577            duration_ticks: 0,
7578            power_q_base_first_rank: 1.3,
7579            power_q_base_max_rank: 1.9,
7580            stats: stone_stats(1),
7581            icon_path: "".to_string(),
7582        },
7583        // EF-C04
7584        EffectStoneTemplate {
7585            id: uuid!("0199c0de-40c5-7be3-8f8c-3418bfa8cf5c"),
7586            name: i18n::I18nString::Translated("Приготовиться к удару".to_string()),
7587            description: i18n::I18nString::Translated(
7588                "Следующий полученный удар наносит на 40% меньше.".to_string(),
7589            ),
7590            tier: StoneTier::Common,
7591            action: EffectStoneAction::NextHitTakenReduction,
7592            magnitude: 40.0,
7593            magnitude_per_level: 5.0,
7594            secondary_magnitude: 0.0,
7595            charges: 1,
7596            duration_ticks: 0,
7597            power_q_base_first_rank: 1.18,
7598            power_q_base_max_rank: 1.44,
7599            stats: stone_stats(1),
7600            icon_path: "".to_string(),
7601        },
7602        // EF-C05
7603        EffectStoneTemplate {
7604            id: uuid!("0199c0de-9a1a-7cd7-871c-e64ba01c5d36"),
7605            name: i18n::I18nString::Translated("Заточка".to_string()),
7606            description: i18n::I18nString::Translated(
7607                "Следующая обычная атака наносит на 60% больше.".to_string(),
7608            ),
7609            tier: StoneTier::Common,
7610            action: EffectStoneAction::NextAttackDamageBonus,
7611            magnitude: 60.0,
7612            magnitude_per_level: 15.0,
7613            secondary_magnitude: 0.0,
7614            charges: 1,
7615            duration_ticks: 0,
7616            power_q_base_first_rank: 1.076,
7617            power_q_base_max_rank: 1.244,
7618            stats: stone_stats(1),
7619            icon_path: "".to_string(),
7620        },
7621        // EF-C06
7622        EffectStoneTemplate {
7623            id: uuid!("0199c0de-a0d9-7cd7-83ae-b2472a0c07fb"),
7624            name: i18n::I18nString::Translated("Прицелиться".to_string()),
7625            description: i18n::I18nString::Translated(
7626                "Следующая атака получает +25% шанса крита.".to_string(),
7627            ),
7628            tier: StoneTier::Common,
7629            action: EffectStoneAction::NextAttackCritChance,
7630            magnitude: 25.0,
7631            magnitude_per_level: 5.0,
7632            secondary_magnitude: 0.0,
7633            charges: 1,
7634            duration_ticks: 0,
7635            power_q_base_first_rank: 1.032,
7636            power_q_base_max_rank: 1.088,
7637            stats: stone_stats(1),
7638            icon_path: "".to_string(),
7639        },
7640        // EF-C07
7641        EffectStoneTemplate {
7642            id: uuid!("0199c0de-5a61-78d7-8e06-afb643564e77"),
7643            name: i18n::I18nString::Translated("Быстрый шаг".to_string()),
7644            description: i18n::I18nString::Translated(
7645                "Самый долгий оставшийся кулдаун скилла сокращается на 0,8 с.".to_string(),
7646            ),
7647            tier: StoneTier::Common,
7648            action: EffectStoneAction::SkillCooldownReduction,
7649            magnitude: 800.0,
7650            magnitude_per_level: 100.0,
7651            secondary_magnitude: 0.0,
7652            charges: 1,
7653            duration_ticks: 0,
7654            power_q_base_first_rank: 1.03,
7655            power_q_base_max_rank: 1.07,
7656            stats: stone_stats(1),
7657            icon_path: "".to_string(),
7658        },
7659        // EF-C08
7660        EffectStoneTemplate {
7661            id: uuid!("0199c0de-0115-7a20-80c8-0014540a923d"),
7662            name: i18n::I18nString::Translated("Натиск".to_string()),
7663            description: i18n::I18nString::Translated(
7664                "Следующий скилл наносит на 20% больше.".to_string(),
7665            ),
7666            tier: StoneTier::Common,
7667            action: EffectStoneAction::NextSkillPayloadBonus,
7668            magnitude: 20.0,
7669            magnitude_per_level: 5.0,
7670            secondary_magnitude: 0.0,
7671            charges: 1,
7672            duration_ticks: 0,
7673            power_q_base_first_rank: 1.034,
7674            power_q_base_max_rank: 1.108,
7675            stats: stone_stats(1),
7676            icon_path: "".to_string(),
7677        },
7678        // EF-R01
7679        EffectStoneTemplate {
7680            id: uuid!("0199c0de-cc78-7005-8e01-0a690a9077bc"),
7681            name: i18n::I18nString::Translated("Окно ярости".to_string()),
7682            description: i18n::I18nString::Translated("+30% скорости атаки на 3 с.".to_string()),
7683            tier: StoneTier::Rare,
7684            action: EffectStoneAction::AttackSpeedBuff,
7685            magnitude: 30.0,
7686            magnitude_per_level: 6.0,
7687            secondary_magnitude: 0.0,
7688            charges: 0,
7689            duration_ticks: 3000,
7690            power_q_base_first_rank: 1.04,
7691            power_q_base_max_rank: 1.11,
7692            stats: stone_stats(2),
7693            icon_path: "".to_string(),
7694        },
7695        // EF-R02
7696        EffectStoneTemplate {
7697            id: uuid!("0199c0de-ac12-757b-89a1-4930de54c162"),
7698            name: i18n::I18nString::Translated("Окно сосредоточения".to_string()),
7699            description: i18n::I18nString::Translated("+20% шанса крита на 4 с.".to_string()),
7700            tier: StoneTier::Rare,
7701            action: EffectStoneAction::CritChanceBuff,
7702            magnitude: 20.0,
7703            magnitude_per_level: 4.0,
7704            secondary_magnitude: 0.0,
7705            charges: 0,
7706            duration_ticks: 4000,
7707            power_q_base_first_rank: 1.06,
7708            power_q_base_max_rank: 1.16,
7709            stats: stone_stats(2),
7710            icon_path: "".to_string(),
7711        },
7712        // EF-R03
7713        EffectStoneTemplate {
7714            id: uuid!("0199c0de-0939-7a4a-8674-9f94fe220ba2"),
7715            name: i18n::I18nString::Translated("Всплеск скиллов".to_string()),
7716            description: i18n::I18nString::Translated("+25% силы скиллов на 4 с.".to_string()),
7717            tier: StoneTier::Rare,
7718            action: EffectStoneAction::SkillPayloadBuff,
7719            magnitude: 25.0,
7720            magnitude_per_level: 5.0,
7721            secondary_magnitude: 0.0,
7722            charges: 0,
7723            duration_ticks: 4000,
7724            power_q_base_first_rank: 1.05,
7725            power_q_base_max_rank: 1.14,
7726            stats: stone_stats(2),
7727            icon_path: "".to_string(),
7728        },
7729        // EF-R04
7730        EffectStoneTemplate {
7731            id: uuid!("0199c0de-fbdc-7485-836f-19f9558bdc33"),
7732            name: i18n::I18nString::Translated("Окно крепости".to_string()),
7733            description: i18n::I18nString::Translated("-25% входящего урона на 4 с.".to_string()),
7734            tier: StoneTier::Rare,
7735            action: EffectStoneAction::IncomingDamageReduction,
7736            magnitude: 25.0,
7737            magnitude_per_level: 4.0,
7738            secondary_magnitude: 0.0,
7739            charges: 0,
7740            duration_ticks: 4000,
7741            power_q_base_first_rank: 1.12,
7742            power_q_base_max_rank: 1.36,
7743            stats: stone_stats(2),
7744            icon_path: "".to_string(),
7745        },
7746        // EF-R05
7747        EffectStoneTemplate {
7748            id: uuid!("0199c0de-8c3c-7caf-835d-16c363abfcd4"),
7749            name: i18n::I18nString::Translated("Окно вампиризма".to_string()),
7750            description: i18n::I18nString::Translated(
7751                "15% нанесённого урона лечит героя в течение 4 с.".to_string(),
7752            ),
7753            tier: StoneTier::Rare,
7754            action: EffectStoneAction::LifestealBuff,
7755            magnitude: 15.0,
7756            magnitude_per_level: 3.0,
7757            secondary_magnitude: 0.0,
7758            charges: 0,
7759            duration_ticks: 4000,
7760            power_q_base_first_rank: 1.08,
7761            power_q_base_max_rank: 1.24,
7762            stats: stone_stats(2),
7763            icon_path: "".to_string(),
7764        },
7765        // EF-R06
7766        EffectStoneTemplate {
7767            id: uuid!("0199c0de-c64c-70db-8b46-c0fefeb1dbce"),
7768            name: i18n::I18nString::Translated("Тройной порыв".to_string()),
7769            description: i18n::I18nString::Translated(
7770                "Следующие 3 обычные атаки наносят на 30% больше.".to_string(),
7771            ),
7772            tier: StoneTier::Rare,
7773            action: EffectStoneAction::NextAttackDamageBonus,
7774            magnitude: 30.0,
7775            magnitude_per_level: 6.0,
7776            secondary_magnitude: 0.0,
7777            charges: 3,
7778            duration_ticks: 0,
7779            power_q_base_first_rank: 1.11,
7780            power_q_base_max_rank: 1.3,
7781            stats: stone_stats(2),
7782            icon_path: "".to_string(),
7783        },
7784        // EF-R07
7785        EffectStoneTemplate {
7786            id: uuid!("0199c0de-95bb-7ad2-8ab4-51f3433242c0"),
7787            name: i18n::I18nString::Translated("Ровный каст".to_string()),
7788            description: i18n::I18nString::Translated(
7789                "Кулдауны скиллов восстанавливаются на 25% быстрее в течение 4 с.".to_string(),
7790            ),
7791            tier: StoneTier::Rare,
7792            action: EffectStoneAction::CooldownRecoveryBuff,
7793            magnitude: 25.0,
7794            magnitude_per_level: 5.0,
7795            secondary_magnitude: 0.0,
7796            charges: 0,
7797            duration_ticks: 4000,
7798            power_q_base_first_rank: 1.06,
7799            power_q_base_max_rank: 1.18,
7800            stats: stone_stats(2),
7801            icon_path: "".to_string(),
7802        },
7803        // EF-R08
7804        EffectStoneTemplate {
7805            id: uuid!("0199c0de-467c-737f-855d-771c0e6a9de5"),
7806            name: i18n::I18nString::Translated("Широкий каст".to_string()),
7807            description: i18n::I18nString::Translated(
7808                "Следующий скилл дополнительно бьёт всех врагов на 50% от атаки.".to_string(),
7809            ),
7810            tier: StoneTier::Rare,
7811            action: EffectStoneAction::NextSkillSplash,
7812            magnitude: 50.0,
7813            magnitude_per_level: 10.0,
7814            secondary_magnitude: 0.0,
7815            charges: 1,
7816            duration_ticks: 0,
7817            power_q_base_first_rank: 1.09,
7818            power_q_base_max_rank: 1.24,
7819            stats: stone_stats(2),
7820            icon_path: "".to_string(),
7821        },
7822        // EF-E01
7823        EffectStoneTemplate {
7824            id: uuid!("0199c0de-68df-7a60-8cfd-561529ae6f57"),
7825            name: i18n::I18nString::Translated("Печать двойного каста".to_string()),
7826            description: i18n::I18nString::Translated(
7827                "Следующий скилл получает производную копию на 50%.".to_string(),
7828            ),
7829            tier: StoneTier::Epic,
7830            action: EffectStoneAction::NextSkillEchoCopies,
7831            magnitude: 50.0,
7832            magnitude_per_level: 10.0,
7833            secondary_magnitude: 0.0,
7834            charges: 1,
7835            duration_ticks: 0,
7836            power_q_base_first_rank: 1.084,
7837            power_q_base_max_rank: 1.234,
7838            stats: stone_stats(4),
7839            icon_path: "".to_string(),
7840        },
7841        // EF-E02
7842        EffectStoneTemplate {
7843            id: uuid!("0199c0de-775b-773e-8998-d45c910391cb"),
7844            name: i18n::I18nString::Translated("Печать тройного удара".to_string()),
7845            description: i18n::I18nString::Translated(
7846                "Следующая обычная атака добавляет 3 производных удара по 55%.".to_string(),
7847            ),
7848            tier: StoneTier::Epic,
7849            action: EffectStoneAction::NextAttackDerivedHits,
7850            magnitude: 55.0,
7851            magnitude_per_level: 11.0,
7852            secondary_magnitude: 0.0,
7853            charges: 3,
7854            duration_ticks: 0,
7855            power_q_base_first_rank: 1.16,
7856            power_q_base_max_rank: 1.44,
7857            stats: stone_stats(4),
7858            icon_path: "".to_string(),
7859        },
7860        // EF-E03
7861        EffectStoneTemplate {
7862            id: uuid!("0199c0de-a6dd-7ee3-8f76-5b23afb8f035"),
7863            name: i18n::I18nString::Translated("Клинки на орбите".to_string()),
7864            description: i18n::I18nString::Translated(
7865                "За 5 с выпускает 5 производных снарядов по 35% от атаки.".to_string(),
7866            ),
7867            tier: StoneTier::Epic,
7868            action: EffectStoneAction::OrbitingProjectiles,
7869            magnitude: 35.0,
7870            magnitude_per_level: 7.0,
7871            secondary_magnitude: 0.0,
7872            charges: 5,
7873            duration_ticks: 5000,
7874            power_q_base_first_rank: 1.12,
7875            power_q_base_max_rank: 1.36,
7876            stats: stone_stats(4),
7877            icon_path: "".to_string(),
7878        },
7879        // EF-E04
7880        EffectStoneTemplate {
7881            id: uuid!("0199c0de-b386-71ad-8e06-48baa8b2b30e"),
7882            name: i18n::I18nString::Translated("Отложенный двойник".to_string()),
7883            description: i18n::I18nString::Translated(
7884                "Через 2 с эффект повторяется на 70%.".to_string(),
7885            ),
7886            tier: StoneTier::Epic,
7887            action: EffectStoneAction::DelayedRepeat,
7888            magnitude: 70.0,
7889            magnitude_per_level: 14.0,
7890            secondary_magnitude: 0.0,
7891            charges: 0,
7892            duration_ticks: 2000,
7893            power_q_base_first_rank: 1.08,
7894            power_q_base_max_rank: 1.24,
7895            stats: stone_stats(4),
7896            icon_path: "".to_string(),
7897        },
7898        // EF-E05
7899        EffectStoneTemplate {
7900            id: uuid!("0199c0de-9556-7ddd-86b0-2c8d8c20c4a7"),
7901            name: i18n::I18nString::Translated("Разрез времени".to_string()),
7902            description: i18n::I18nString::Translated(
7903                "Два самых долгих оставшихся кулдауна скиллов сокращаются на 2 с.".to_string(),
7904            ),
7905            tier: StoneTier::Epic,
7906            action: EffectStoneAction::SkillCooldownReduction,
7907            magnitude: 2000.0,
7908            magnitude_per_level: 200.0,
7909            secondary_magnitude: 0.0,
7910            charges: 2,
7911            duration_ticks: 0,
7912            power_q_base_first_rank: 1.08,
7913            power_q_base_max_rank: 1.18,
7914            stats: stone_stats(4),
7915            icon_path: "".to_string(),
7916        },
7917        // EF-E06
7918        EffectStoneTemplate {
7919            id: uuid!("0199c0de-5c29-70dc-808f-91d974ae4a11"),
7920            name: i18n::I18nString::Translated("Идеальный блок".to_string()),
7921            description: i18n::I18nString::Translated(
7922                "Следующий полученный удар блокируется, ответный удар — 150% от атаки.".to_string(),
7923            ),
7924            tier: StoneTier::Epic,
7925            action: EffectStoneAction::NextHitBlockedRetaliation,
7926            magnitude: 150.0,
7927            magnitude_per_level: 30.0,
7928            secondary_magnitude: 0.0,
7929            charges: 1,
7930            duration_ticks: 0,
7931            power_q_base_first_rank: 1.14,
7932            power_q_base_max_rank: 1.4,
7933            stats: stone_stats(4),
7934            icon_path: "".to_string(),
7935        },
7936        // EF-E07
7937        EffectStoneTemplate {
7938            id: uuid!("0199c0de-bdba-731c-813b-59871a70ce22"),
7939            name: i18n::I18nString::Translated("Пятикратный порыв".to_string()),
7940            description: i18n::I18nString::Translated(
7941                "Следующие 5 обычных атак наносят на 45% больше.".to_string(),
7942            ),
7943            tier: StoneTier::Epic,
7944            action: EffectStoneAction::NextAttackDamageBonus,
7945            magnitude: 45.0,
7946            magnitude_per_level: 9.0,
7947            secondary_magnitude: 0.0,
7948            charges: 5,
7949            duration_ticks: 0,
7950            power_q_base_first_rank: 1.22,
7951            power_q_base_max_rank: 1.6,
7952            stats: stone_stats(4),
7953            icon_path: "".to_string(),
7954        },
7955        // EF-E08
7956        EffectStoneTemplate {
7957            id: uuid!("0199c0de-d965-74ca-8f8f-856429526ba0"),
7958            name: i18n::I18nString::Translated("Призматический каст".to_string()),
7959            description: i18n::I18nString::Translated(
7960                "Следующий скилл раздаёт по 30% своего урона максимум 3 дополнительным целям."
7961                    .to_string(),
7962            ),
7963            tier: StoneTier::Epic,
7964            action: EffectStoneAction::NextSkillSplitCopies,
7965            magnitude: 30.0,
7966            magnitude_per_level: 6.0,
7967            secondary_magnitude: 0.0,
7968            charges: 3,
7969            duration_ticks: 0,
7970            power_q_base_first_rank: 1.1,
7971            power_q_base_max_rank: 1.28,
7972            stats: stone_stats(4),
7973            icon_path: "".to_string(),
7974        },
7975        // EF-L01
7976        EffectStoneTemplate {
7977            id: uuid!("0199c0de-b41d-79a2-860b-1e7d898b460c"),
7978            name: i18n::I18nString::Translated("Сокрушитель мира".to_string()),
7979            description: i18n::I18nString::Translated(
7980                "Наносит всем врагам 500% от атаки.".to_string(),
7981            ),
7982            tier: StoneTier::Legendary,
7983            action: EffectStoneAction::InstantDamageAll,
7984            magnitude: 500.0,
7985            magnitude_per_level: 100.0,
7986            secondary_magnitude: 0.0,
7987            charges: 0,
7988            duration_ticks: 0,
7989            power_q_base_first_rank: 1.6,
7990            power_q_base_max_rank: 2.4,
7991            stats: stone_stats(8),
7992            icon_path: "".to_string(),
7993        },
7994        // EF-L02
7995        EffectStoneTemplate {
7996            id: uuid!("0199c0de-e175-7bc5-870b-e8d8292131b2"),
7997            name: i18n::I18nString::Translated("Великое эхо".to_string()),
7998            description: i18n::I18nString::Translated(
7999                "Следующий скилл получает две производные копии: 60% и 30%.".to_string(),
8000            ),
8001            tier: StoneTier::Legendary,
8002            action: EffectStoneAction::NextSkillEchoCopies,
8003            magnitude: 60.0,
8004            magnitude_per_level: 12.0,
8005            secondary_magnitude: 30.0,
8006            charges: 2,
8007            duration_ticks: 0,
8008            power_q_base_first_rank: 1.24,
8009            power_q_base_max_rank: 1.7,
8010            stats: stone_stats(8),
8011            icon_path: "".to_string(),
8012        },
8013        // EF-L03
8014        EffectStoneTemplate {
8015            id: uuid!("0199c0de-40b4-7c71-8ef6-e69e48e4b4f9"),
8016            name: i18n::I18nString::Translated("Шторм клинков".to_string()),
8017            description: i18n::I18nString::Translated(
8018                "Следующие 6 обычных атак дополнительно бьют всех врагов на 140%.".to_string(),
8019            ),
8020            tier: StoneTier::Legendary,
8021            action: EffectStoneAction::NextAttacksSplash,
8022            magnitude: 140.0,
8023            magnitude_per_level: 28.0,
8024            secondary_magnitude: 0.0,
8025            charges: 6,
8026            duration_ticks: 0,
8027            power_q_base_first_rank: 1.36,
8028            power_q_base_max_rank: 2.0,
8029            stats: stone_stats(8),
8030            icon_path: "".to_string(),
8031        },
8032        // EF-L04
8033        EffectStoneTemplate {
8034            id: uuid!("0199c0de-c89b-70ad-8111-9fb5935194e9"),
8035            name: i18n::I18nString::Translated("Схлопывание времени".to_string()),
8036            description: i18n::I18nString::Translated("Все скиллы становятся готовы.".to_string()),
8037            tier: StoneTier::Legendary,
8038            action: EffectStoneAction::ResetSkillCooldowns,
8039            magnitude: 0.0,
8040            magnitude_per_level: 0.0,
8041            secondary_magnitude: 0.0,
8042            charges: 0,
8043            duration_ticks: 0,
8044            power_q_base_first_rank: 1.3,
8045            power_q_base_max_rank: 1.3,
8046            stats: stone_stats(8),
8047            icon_path: "".to_string(),
8048        },
8049        // EF-L05
8050        EffectStoneTemplate {
8051            id: uuid!("0199c0de-00c6-7ba8-8821-d527ee5e683a"),
8052            name: i18n::I18nString::Translated("Последний свет".to_string()),
8053            description: i18n::I18nString::Translated(
8054                "5 с урон не может опустить здоровье героя ниже 1.".to_string(),
8055            ),
8056            tier: StoneTier::Legendary,
8057            action: EffectStoneAction::DamageFloorGuard,
8058            magnitude: 0.0,
8059            magnitude_per_level: 0.0,
8060            secondary_magnitude: 0.0,
8061            charges: 0,
8062            duration_ticks: 5000,
8063            power_q_base_first_rank: 1.5,
8064            power_q_base_max_rank: 1.5,
8065            stats: stone_stats(8),
8066            icon_path: "".to_string(),
8067        },
8068        // EF-L06
8069        EffectStoneTemplate {
8070            id: uuid!("0199c0de-4405-7bcc-8ec4-3b6efafa7422"),
8071            name: i18n::I18nString::Translated("Королевский порыв".to_string()),
8072            description: i18n::I18nString::Translated(
8073                "+50% силы обычных атак и скиллов на 8 с.".to_string(),
8074            ),
8075            tier: StoneTier::Legendary,
8076            action: EffectStoneAction::DamageDealtBuff,
8077            magnitude: 50.0,
8078            magnitude_per_level: 10.0,
8079            secondary_magnitude: 0.0,
8080            charges: 0,
8081            duration_ticks: 8000,
8082            power_q_base_first_rank: 1.34,
8083            power_q_base_max_rank: 1.8,
8084            stats: stone_stats(8),
8085            icon_path: "".to_string(),
8086        },
8087        // EF-L07
8088        EffectStoneTemplate {
8089            id: uuid!("0199c0de-67b0-78ca-8c83-02b37b6bf21f"),
8090            name: i18n::I18nString::Translated("Полное восстановление".to_string()),
8091            description: i18n::I18nString::Translated(
8092                "Восстанавливает 35% максимального здоровья.".to_string(),
8093            ),
8094            tier: StoneTier::Legendary,
8095            action: EffectStoneAction::HealMaxHpPercent,
8096            magnitude: 35.0,
8097            magnitude_per_level: 7.0,
8098            secondary_magnitude: 0.0,
8099            charges: 0,
8100            duration_ticks: 0,
8101            power_q_base_first_rank: 1.7,
8102            power_q_base_max_rank: 2.6,
8103            stats: stone_stats(8),
8104            icon_path: "".to_string(),
8105        },
8106        // EF-L08
8107        EffectStoneTemplate {
8108            id: uuid!("0199c0de-0d1d-7d4b-8d49-8ff299980974"),
8109            name: i18n::I18nString::Translated("Каст казни".to_string()),
8110            description: i18n::I18nString::Translated(
8111                "Следующий атакующий скилл наносит текущей цели на 150% больше.".to_string(),
8112            ),
8113            tier: StoneTier::Legendary,
8114            action: EffectStoneAction::NextSkillPayloadBonus,
8115            magnitude: 150.0,
8116            magnitude_per_level: 30.0,
8117            secondary_magnitude: 0.0,
8118            charges: 1,
8119            duration_ticks: 0,
8120            power_q_base_first_rank: 1.25,
8121            power_q_base_max_rank: 1.7,
8122            stats: stone_stats(8),
8123            icon_path: "".to_string(),
8124        },
8125    ]
8126}
8127
8128/// Collection Power fixture.
8129///
8130/// Deliberately NOT the shipped budget: the fixture's catalogs are a handful of
8131/// templates, so shipped `k` values would produce bonuses too small to assert
8132/// on. These are round numbers chosen to make the arithmetic legible in tests —
8133/// the shipped budget is asserted against `config-local.json` in
8134/// `test_collection_power_budget`.
8135///
8136/// `hp` and `armor` are the fixture's only mod-capable attributes, so they are
8137/// the two the multiplier is authored on.
8138fn generate_collection_power() -> crate::collection_power::CollectionPowerSettings {
8139    use crate::collection_power::{
8140        CollectionPowerFamily, CollectionPowerSettings, PetRarityCollectionWeight,
8141        StoneTierCollectionWeight,
8142    };
8143    use essences::stones::StoneTier;
8144
8145    CollectionPowerSettings {
8146        first_ownership_share: 0.10,
8147        multiplied_attribute_ids: vec![HP_ATTRIBUTE_ID, ARMOR_ATTRIBUTE_ID],
8148        equipment_stones: CollectionPowerFamily { k: 0.001 },
8149        laws: CollectionPowerFamily { k: 0.001 },
8150        artifact_stones: CollectionPowerFamily { k: 0.001 },
8151        ability_stones: CollectionPowerFamily { k: 0.001 },
8152        gacha_abilities: CollectionPowerFamily { k: 0.001 },
8153        pets: CollectionPowerFamily { k: 0.001 },
8154        stone_tier_weights: vec![
8155            StoneTierCollectionWeight {
8156                tier: StoneTier::Common,
8157                weight: 1.0,
8158            },
8159            StoneTierCollectionWeight {
8160                tier: StoneTier::Rare,
8161                weight: 2.0,
8162            },
8163            StoneTierCollectionWeight {
8164                tier: StoneTier::Epic,
8165                weight: 4.0,
8166            },
8167            StoneTierCollectionWeight {
8168                tier: StoneTier::Legendary,
8169                weight: 10.0,
8170            },
8171        ],
8172        pet_rarity_weights: vec![
8173            PetRarityCollectionWeight {
8174                rarity_id: uuid!("a0000000-0000-0000-0000-000000000001"),
8175                weight: 1.0,
8176            },
8177            PetRarityCollectionWeight {
8178                rarity_id: uuid!("a0000000-0000-0000-0000-000000000002"),
8179                weight: 2.0,
8180            },
8181        ],
8182    }
8183}