overlord_event_system/behaviors/quests/
loop_tasks.rs

1//! Native ports for the `additional_quests` category — quest
2//! `additional_quests_behavior`s (run via `run_event` in
3//! `logic::quests::{handle_claim_quest, handle_hidden_quest_completed}`,
4//! returning `Vec<OverlordEvent>`).
5//!
6//! These scripts fire when a quest is claimed (or a hidden quest completes) and
7//! issue follow-up quests. The shipped bodies fall into a small set of patterns:
8//!
9//! 1. **loop-task cycle** — `loop_tasks::on_finish_regular_loop_task` +
10//!    `loop_tasks::advance_loop` (the regular loop-task quest chain).
11//! 2. **loop-task cycle + first-loop flag** — pattern 1 plus a
12//!    `SetCustomValue("loop_tasks.first_loop_skill_crystals_received", 1)`.
13//! 3. **loop-task prepare** — `prepare_loop` (RNG) + `advance_loop`, optionally
14//!    preceded by `on_finish_regular_loop_task`.
15//! 4. **milestone marker** — `SetCustomValue("loop_tasks.last_milestone.<k>", n)`
16//!    then `advance_loop`.
17//! 5. **set custom value** — a single `SetCustomValue(key, n)` (mimic-item codes,
18//!    events, so they are not ported.
19//! 6. **redirect loop task** — a single
20//!    `UpdateActiveLoopTaskId(<fixed quest uuid>)`.
21//!
22//! ## RNG
23//! Only the `prepare_loop` pattern (#3) consumes RNG (slot-draw `randint`s). The
24//! native draw order matches exactly. All other patterns ignore the snapshot
25//! (harmless).
26//!
27//! ## Scope (per the `run_event` call site in `quests.rs`)
28//! `Random`, `CharacterState`, `State` — plus `config`/`lookups` the loop_tasks
29//! logic needs.
30
31use configs::game_config::GameConfig;
32use essences::character_state::CharacterState;
33use event_system::script::random::GameRng;
34use uuid::Uuid;
35
36use crate::behaviors::{BehaviorKind, BehaviorMeta, BehaviorRegistry};
37use crate::event::OverlordEvent;
38use crate::mechanics::content_lookups::ContentLookups;
39use crate::mechanics::loop_tasks;
40
41/// Inputs available to an `additional_quests` native fn — the quest
42/// `additional_quests_behavior` scope plus config/lookups the loop_tasks logic
43/// needs.
44pub struct AdditionalQuestsCtx<'a> {
45    /// `CharacterState` const.
46    pub character_state: &'a CharacterState,
47    /// RNG snapshot (clone at the same state) so `prepare_loop` draws identically
48    pub rng: &'a GameRng,
49    pub config: &'a GameConfig,
50    pub lookups: &'a ContentLookups,
51}
52
53/// Signature of an `additional_quests` native fn.
54pub type AdditionalQuestsFn = fn(&AdditionalQuestsCtx) -> anyhow::Result<Vec<OverlordEvent>>;
55
56fn set_custom_value(key: &str, value: i64) -> OverlordEvent {
57    OverlordEvent::SetCustomValue {
58        key: key.to_string(),
59        value,
60    }
61}
62
63// ---------------------------------------------------------------------------
64// Shared parametrized bodies
65// ---------------------------------------------------------------------------
66
67/// `on_finish_regular_loop_task` (no-op) + `advance_loop`.
68fn finish_and_advance(ctx: &AdditionalQuestsCtx) -> anyhow::Result<Vec<OverlordEvent>> {
69    let mut events = Vec::new();
70    loop_tasks::on_finish_regular_loop_task(&mut events, ctx.character_state);
71    loop_tasks::advance_loop(&mut events, ctx.config, ctx.lookups, ctx.character_state);
72    Ok(events)
73}
74
75/// `SetCustomValue(key, value)` then `advance_loop` (the milestone markers).
76fn set_value_then_advance(
77    ctx: &AdditionalQuestsCtx,
78    key: &str,
79    value: i64,
80) -> anyhow::Result<Vec<OverlordEvent>> {
81    let mut events = vec![set_custom_value(key, value)];
82    loop_tasks::advance_loop(&mut events, ctx.config, ctx.lookups, ctx.character_state);
83    Ok(events)
84}
85
86/// A single `SetCustomValue(key, value)` (no advance).
87fn set_value_only(
88    _ctx: &AdditionalQuestsCtx,
89    key: &str,
90    value: i64,
91) -> anyhow::Result<Vec<OverlordEvent>> {
92    Ok(vec![set_custom_value(key, value)])
93}
94
95// ---------------------------------------------------------------------------
96// Distinct ports
97// ---------------------------------------------------------------------------
98
99/// Pattern 1: on_finish_regular_loop_task + advance_loop. Shared by the regular
100/// loop-task quests (019a4b43 / 019b561c / 019b561e / 019b561f / 019b5621 /
101/// 019bc1e6 / 019bc29c-572e / 019bc29c-b647 / 019d768d).
102pub fn loop_finish_advance(ctx: &AdditionalQuestsCtx) -> anyhow::Result<Vec<OverlordEvent>> {
103    finish_and_advance(ctx)
104}
105
106/// Pattern 2 (019a6ef3): finish + advance, then mark the first-loop crystals flag.
107pub fn loop_finish_advance_first_crystals(
108    ctx: &AdditionalQuestsCtx,
109) -> anyhow::Result<Vec<OverlordEvent>> {
110    let mut events = finish_and_advance(ctx)?;
111    events.push(set_custom_value(
112        "loop_tasks.first_loop_skill_crystals_received",
113        1,
114    ));
115    Ok(events)
116}
117
118/// Pattern 3a (019bdb53): on_finish_regular_loop_task + prepare_loop (RNG) +
119/// advance_loop.
120pub fn loop_finish_prepare_advance(
121    ctx: &AdditionalQuestsCtx,
122) -> anyhow::Result<Vec<OverlordEvent>> {
123    let mut events = Vec::new();
124    loop_tasks::on_finish_regular_loop_task(&mut events, ctx.character_state);
125    loop_tasks::prepare_loop(&mut events, ctx.config, ctx.character_state, ctx.rng);
126    loop_tasks::advance_loop(&mut events, ctx.config, ctx.lookups, ctx.character_state);
127    Ok(events)
128}
129
130/// Pattern 3b (019cd269): prepare_loop (RNG) + advance_loop (no finish call).
131pub fn loop_prepare_advance(ctx: &AdditionalQuestsCtx) -> anyhow::Result<Vec<OverlordEvent>> {
132    let mut events = Vec::new();
133    loop_tasks::prepare_loop(&mut events, ctx.config, ctx.character_state, ctx.rng);
134    loop_tasks::advance_loop(&mut events, ctx.config, ctx.lookups, ctx.character_state);
135    Ok(events)
136}
137
138// --- Pattern 4: level milestone markers (SetCustomValue + advance) ---
139
140/// Pattern 4: `SetCustomValue("loop_tasks.last_milestone.level", N)` + advance.
141pub fn milestone_level<const N: i64>(
142    ctx: &AdditionalQuestsCtx,
143) -> anyhow::Result<Vec<OverlordEvent>> {
144    set_value_then_advance(ctx, "loop_tasks.last_milestone.level", N)
145}
146
147/// Pattern 4: `SetCustomValue("loop_tasks.last_milestone.stage", N)` + advance.
148pub fn milestone_stage<const N: i64>(
149    ctx: &AdditionalQuestsCtx,
150) -> anyhow::Result<Vec<OverlordEvent>> {
151    set_value_then_advance(ctx, "loop_tasks.last_milestone.stage", N)
152}
153
154// --- Pattern 5: single SetCustomValue (mimic item codes) ---
155
156/// Pattern 5: `SetCustomValue("next_mimic_item_code", CODE)`.
157pub fn mimic_next_item<const CODE: i64>(
158    ctx: &AdditionalQuestsCtx,
159) -> anyhow::Result<Vec<OverlordEvent>> {
160    set_value_only(ctx, "next_mimic_item_code", CODE)
161}
162
163// --- Pattern 6: redirect to a fixed loop task quest id ---
164
165// Redirect-target loop-task quest ids, exported for deploy-time content
166// validation (see `super::validate`).
167pub const REDIRECT_KILL_3_ENEMIES: u128 = 0x019cd267_a50f_7d1b_81a5_83147141fb8c;
168pub const REDIRECT_OPEN_CHEST_6: u128 = 0x019cd268_6b9e_7b89_826c_96b0e1cb4c55;
169pub const REDIRECT_SELL_5_GEAR: u128 = 0x019cd268_a523_7e35_bb43_3cff76868568;
170pub const REDIRECT_REACH_LEVEL_3: u128 = 0x019cd269_8571_7361_936b_08a48ca9cffe;
171
172/// Pattern 6: `UpdateActiveLoopTaskId(QUEST)`.
173pub fn redirect_to<const QUEST: u128>(
174    _ctx: &AdditionalQuestsCtx,
175) -> anyhow::Result<Vec<OverlordEvent>> {
176    Ok(vec![OverlordEvent::UpdateActiveLoopTaskId {
177        quest_id: Uuid::from_u128(QUEST),
178    }])
179}
180
181// ---------------------------------------------------------------------------
182// Test-fixture loop-task chain ports
183// ---------------------------------------------------------------------------
184//
185// `tests_game_config.rs` defines a 3-quest loop-task cycle
186// `additional_quests_behavior` pushed two events on claim:
187//   `SetCustomValue("loop_task", N)` and
188//   `UpdateActiveLoopTaskId(<next quest id>)`.
189// value was never written (tests index `custom_values["loop_task"]`) and the
190// active loop task never advanced. Faithful ports, one per quest slot.
191
192fn set_loop_task_and_redirect(value: i64, next_quest_id: &str) -> Vec<OverlordEvent> {
193    let quest_id = Uuid::parse_str(next_quest_id).expect("valid quest uuid literal");
194    vec![
195        set_custom_value("loop_task", value),
196        OverlordEvent::UpdateActiveLoopTaskId { quest_id },
197    ]
198}
199
200/// Test loop-task 305300e2 on claim: `loop_task = 1`, advance to 29540ca2.
201pub fn test_loop_task_1(_ctx: &AdditionalQuestsCtx) -> anyhow::Result<Vec<OverlordEvent>> {
202    Ok(set_loop_task_and_redirect(
203        1,
204        "29540ca2-21f3-4f0e-a478-240adafed4e3",
205    ))
206}
207
208/// Test loop-task 29540ca2 on claim: `loop_task = 2`, advance to 9d3d2428.
209pub fn test_loop_task_2(_ctx: &AdditionalQuestsCtx) -> anyhow::Result<Vec<OverlordEvent>> {
210    Ok(set_loop_task_and_redirect(
211        2,
212        "9d3d2428-af3c-409f-a580-593de0fd06cf",
213    ))
214}
215
216/// Test loop-task 9d3d2428 on claim: `loop_task = 0`, advance back to 305300e2.
217pub fn test_loop_task_0(_ctx: &AdditionalQuestsCtx) -> anyhow::Result<Vec<OverlordEvent>> {
218    Ok(set_loop_task_and_redirect(
219        0,
220        "305300e2-f432-4cbf-ad46-85386a9a9410",
221    ))
222}
223
224/// Test loop-task seeder (hidden starting quest c3ae0d0c, completes after the
225///   `SetCustomValue("loop_task", 0)` +
226///   `give_quests([305300e2, 29540ca2, 9d3d2428])` +
227///   `UpdateActiveLoopTaskId(305300e2)`.
228/// This is what first unlocks the 3-quest loop-task cycle; the migration
229/// stubbed it to `None`, so loop tasks were never given.
230pub fn test_loop_task_seed(_ctx: &AdditionalQuestsCtx) -> anyhow::Result<Vec<OverlordEvent>> {
231    Ok(vec![
232        set_custom_value("loop_task", 0),
233        OverlordEvent::NewQuests {
234            quest_ids: vec![
235                Uuid::parse_str("305300e2-f432-4cbf-ad46-85386a9a9410").unwrap(),
236                Uuid::parse_str("29540ca2-21f3-4f0e-a478-240adafed4e3").unwrap(),
237                Uuid::parse_str("9d3d2428-af3c-409f-a580-593de0fd06cf").unwrap(),
238            ],
239        },
240        OverlordEvent::UpdateActiveLoopTaskId {
241            quest_id: Uuid::parse_str("305300e2-f432-4cbf-ad46-85386a9a9410").unwrap(),
242        },
243    ])
244}
245
246/// Register this category's native fns.
247pub fn register(registry: &mut BehaviorRegistry) {
248    let mut reg = |name: &str, title: &str, desc: &str, f: AdditionalQuestsFn| {
249        registry.register_additional_quests(
250            BehaviorMeta {
251                name: name.to_string(),
252                category: BehaviorKind::AdditionalQuests,
253                title: title.to_string(),
254                description: desc.to_string(),
255            },
256            f,
257        );
258    };
259
260    reg(
261        "loop_finish_advance",
262        "Доп. квесты: завершить и продвинуть loop",
263        "on_finish_regular_loop_task (no-op) + advance_loop; общий для регулярных loop-квестов.",
264        loop_finish_advance,
265    );
266    reg(
267        "loop_finish_advance_first_crystals",
268        "Доп. квесты: завершить/продвинуть + флаг первых кристаллов (019a6ef3)",
269        "finish + advance, затем SetCustomValue(loop_tasks.first_loop_skill_crystals_received, 1).",
270        loop_finish_advance_first_crystals,
271    );
272    reg(
273        "loop_finish_prepare_advance",
274        "Доп. квесты: завершить + prepare(RNG) + продвинуть (019bdb53)",
275        "on_finish_regular_loop_task + prepare_loop (RNG) + advance_loop.",
276        loop_finish_prepare_advance,
277    );
278    reg(
279        "loop_prepare_advance",
280        "Доп. квесты: prepare(RNG) + продвинуть (019cd269)",
281        "prepare_loop (RNG) + advance_loop.",
282        loop_prepare_advance,
283    );
284
285    // Milestone markers.
286    let milestones: &[(&str, &str, AdditionalQuestsFn)] = &[
287        ("aq_milestone_level_1", "level=1", milestone_level::<1>),
288        ("aq_milestone_level_2", "level=2", milestone_level::<2>),
289        ("aq_milestone_level_3", "level=3", milestone_level::<3>),
290        ("aq_milestone_level_4", "level=4", milestone_level::<4>),
291        ("aq_milestone_level_5", "level=5", milestone_level::<5>),
292        ("aq_milestone_level_6", "level=6", milestone_level::<6>),
293        ("aq_milestone_level_7", "level=7", milestone_level::<7>),
294        ("aq_milestone_level_8", "level=8", milestone_level::<8>),
295        ("aq_milestone_level_9", "level=9", milestone_level::<9>),
296        ("aq_milestone_level_10", "level=10", milestone_level::<10>),
297        ("aq_milestone_level_11", "level=11", milestone_level::<11>),
298        ("aq_milestone_level_12", "level=12", milestone_level::<12>),
299        ("aq_milestone_level_13", "level=13", milestone_level::<13>),
300        ("aq_milestone_level_14", "level=14", milestone_level::<14>),
301        ("aq_milestone_stage_1", "stage=1", milestone_stage::<1>),
302        ("aq_milestone_stage_2", "stage=2", milestone_stage::<2>),
303        ("aq_milestone_stage_3", "stage=3", milestone_stage::<3>),
304        ("aq_milestone_stage_4", "stage=4", milestone_stage::<4>),
305        ("aq_milestone_stage_5", "stage=5", milestone_stage::<5>),
306        ("aq_milestone_stage_6", "stage=6", milestone_stage::<6>),
307        ("aq_milestone_stage_7", "stage=7", milestone_stage::<7>),
308        ("aq_milestone_stage_8", "stage=8", milestone_stage::<8>),
309        ("aq_milestone_stage_9", "stage=9", milestone_stage::<9>),
310        ("aq_milestone_stage_10", "stage=10", milestone_stage::<10>),
311        ("aq_milestone_stage_11", "stage=11", milestone_stage::<11>),
312        ("aq_milestone_stage_12", "stage=12", milestone_stage::<12>),
313        ("aq_milestone_stage_13", "stage=13", milestone_stage::<13>),
314    ];
315    for (name, label, f) in milestones {
316        reg(
317            name,
318            &format!("Доп. квесты: веха {label} + advance"),
319            "SetCustomValue(loop_tasks.last_milestone.<k>, n) + advance_loop.",
320            *f,
321        );
322    }
323
324    // Mimic item-code setters.
325    let mimic_codes: &[(&str, AdditionalQuestsFn)] = &[
326        ("aq_mimic_next_item_0", mimic_next_item::<0>),
327        ("aq_mimic_next_item_1002", mimic_next_item::<1002>),
328        ("aq_mimic_next_item_1011", mimic_next_item::<1011>),
329        ("aq_mimic_next_item_2010", mimic_next_item::<2010>),
330        ("aq_mimic_next_item_3002", mimic_next_item::<3002>),
331        ("aq_mimic_next_item_4001", mimic_next_item::<4001>),
332    ];
333    for (name, f) in mimic_codes {
334        let code = name.rsplit('_').next().unwrap();
335        reg(
336            name,
337            &format!("Доп. квесты: next_mimic_item_code={code}"),
338            "SetCustomValue(next_mimic_item_code, <код>).",
339            *f,
340        );
341    }
342
343    // Loop-task redirects.
344    let redirects: &[(&str, &str, AdditionalQuestsFn)] = &[
345        (
346            "aq_redirect_to_kill_3_enemies",
347            "019cd267",
348            redirect_to::<REDIRECT_KILL_3_ENEMIES>,
349        ),
350        (
351            "aq_redirect_to_open_chest_6",
352            "019cd268-6b9e",
353            redirect_to::<REDIRECT_OPEN_CHEST_6>,
354        ),
355        (
356            "aq_redirect_to_sell_5_gear",
357            "019cd268-a523",
358            redirect_to::<REDIRECT_SELL_5_GEAR>,
359        ),
360        (
361            "aq_redirect_to_reach_level_3",
362            "019cd269",
363            redirect_to::<REDIRECT_REACH_LEVEL_3>,
364        ),
365    ];
366    for (name, target, f) in redirects {
367        reg(
368            name,
369            &format!("Доп. квесты: переключить loop task → {target}"),
370            "UpdateActiveLoopTaskId(<целевой loop-task квест>).",
371            *f,
372        );
373    }
374
375    // Test-fixture loop-task chain (tests_game_config.rs).
376    reg(
377        "test_loop_task_1",
378        "Тест: loop_task=1 → 29540ca2",
379        "SetCustomValue(loop_task, 1) + UpdateActiveLoopTaskId(29540ca2).",
380        test_loop_task_1,
381    );
382    reg(
383        "test_loop_task_2",
384        "Тест: loop_task=2 → 9d3d2428",
385        "SetCustomValue(loop_task, 2) + UpdateActiveLoopTaskId(9d3d2428).",
386        test_loop_task_2,
387    );
388    reg(
389        "test_loop_task_0",
390        "Тест: loop_task=0 → 305300e2",
391        "SetCustomValue(loop_task, 0) + UpdateActiveLoopTaskId(305300e2).",
392        test_loop_task_0,
393    );
394    reg(
395        "test_loop_task_seed",
396        "Тест: выдать loop-task цикл (305300e2/29540ca2/9d3d2428)",
397        "SetCustomValue(loop_task, 0) + NewQuests([3 loop tasks]) + UpdateActiveLoopTaskId(305300e2).",
398        test_loop_task_seed,
399    );
400
401    register_default_loop_tasks(registry);
402}
403
404/// Inputs for the default-loop-task selector
405/// (`game_settings.default_loop_task_behavior`).
406pub struct DefaultLoopTaskCtx;
407
408/// Signature of a default-loop-task fn (returns the loop-task quest id).
409pub type DefaultLoopTaskFn = fn(&DefaultLoopTaskCtx) -> anyhow::Result<Uuid>;
410
411/// The shipped default loop task: a constant quest id.
412pub fn default_loop_task_const(_ctx: &DefaultLoopTaskCtx) -> anyhow::Result<Uuid> {
413    Ok(Uuid::from_u128(0x019cd267_a50f_7d1b_81a5_83147141fb8c))
414}
415
416/// Test default loop task: the fixture's first loop-task quest.
417pub fn test_default_loop_task(_ctx: &DefaultLoopTaskCtx) -> anyhow::Result<Uuid> {
418    Ok(Uuid::from_u128(0x305300e2_f432_4cbf_ad46_85386a9a9410))
419}
420
421/// Register the default-loop-task selectors (the loop-task quest behaviors
422/// above are registered in [`register`]).
423fn register_default_loop_tasks(registry: &mut BehaviorRegistry) {
424    registry.register_default_loop_task(
425        BehaviorMeta {
426            name: "default_loop_task_const".to_string(),
427            category: BehaviorKind::DefaultLoopTask,
428            title: "Дефолтное loop-задание (константный uuid)".to_string(),
429            description: "Возвращает константный uuid loop-задания.".to_string(),
430        },
431        default_loop_task_const,
432    );
433    registry.register_default_loop_task(
434        BehaviorMeta {
435            name: "test_default_loop_task".to_string(),
436            category: BehaviorKind::DefaultLoopTask,
437            title: "Тест: дефолтное loop-задание (305300e2)".to_string(),
438            description: "Возвращает 305300e2 (фикстурный путь восстановления loop-task)."
439                .to_string(),
440        },
441        test_default_loop_task,
442    );
443}