1use configs::game_config::GameConfig;
12use essences::character_state::CharacterState;
13use essences::fighting::ActiveFight;
14use essences::quest::{QuestGroupType, QuestInstance};
15use uuid::Uuid;
16
17use crate::behaviors::{BehaviorKind, BehaviorMeta, BehaviorRegistry};
18use crate::event::OverlordEvent;
19use crate::game_config_helpers::GameConfigLookup;
20use crate::mechanics::content_lookups::ContentLookups;
21
22pub struct ConditionalProgressCtx<'a> {
24 pub event: &'a OverlordEvent,
26 pub character_state: &'a CharacterState,
27 pub active_fight: &'a Option<ActiveFight>,
29 pub quest: &'a QuestInstance,
31 pub config: &'a GameConfig,
32 pub lookups: &'a ContentLookups,
33}
34
35pub type ConditionalProgressFn = fn(&ConditionalProgressCtx) -> anyhow::Result<i64>;
39
40pub const DUNGEON_1: u128 = 0x019a9206_800b_781e_8998_38cdc6e9826e;
43pub const DUNGEON_2: u128 = 0x019aee96_7303_7d3e_a382_d7776687d24f;
44pub const DUNGEON_3: u128 = 0x019d2eca_9508_71c9_abb3_a6fc17474502;
45pub const COLLECT_CURRENCY: u128 = 0x0194d64e_2162_76d3_8449_3e850f6e39e9;
46pub const RARITY_A: u128 = 0x0194d64e_2179_797b_90fe_8b783f349203;
47pub const RARITY_B: u128 = 0x0194d64e_2179_797b_90fe_8b799ae7a867;
48pub const RARITY_C: u128 = 0x0194d64e_2179_797b_90fe_8b7ad369fd71;
49pub const LOG_IN_QUEST: u128 = 0x019c2b16_a454_737b_b5b5_1123073e2fce;
50
51const COMPLETE_ALL_LOOP_TASKS: &str = "CompleteAllLoopTasks";
52
53fn is_custom_event(event: &OverlordEvent, ev: &str) -> bool {
55 matches!(event, OverlordEvent::CustomEvent { event_type, .. } if event_type == ev)
56}
57
58fn is_custom_event_any(event: &OverlordEvent) -> bool {
60 matches!(event, OverlordEvent::CustomEvent { .. })
61}
62
63fn is_ally_entity_death(ctx: &ConditionalProgressCtx) -> bool {
71 match ctx.event {
72 OverlordEvent::EntityDeath { entity_id, .. } => ctx
73 .active_fight
74 .as_ref()
75 .and_then(|af| af.party_player_id)
76 .is_some_and(|ally_id| ally_id == *entity_id),
77 _ => false,
78 }
79}
80
81fn is_summoned_entity_death(ctx: &ConditionalProgressCtx) -> bool {
87 match ctx.event {
88 OverlordEvent::EntityDeath { entity_id, .. } => ctx
89 .active_fight
90 .as_ref()
91 .is_some_and(|af| af.summoned_entity_ids.contains(entity_id)),
92 _ => false,
93 }
94}
95
96fn is_non_counting_entity_death(ctx: &ConditionalProgressCtx) -> bool {
100 is_ally_entity_death(ctx) || is_summoned_entity_death(ctx)
101}
102
103pub fn increment_one(ctx: &ConditionalProgressCtx) -> anyhow::Result<i64> {
108 if is_non_counting_entity_death(ctx) {
109 return Ok(ctx.quest.current);
110 }
111 Ok(ctx.quest.current + 1)
112}
113
114pub fn increment_by_batch_size(ctx: &ConditionalProgressCtx) -> anyhow::Result<i64> {
118 let batch = event_batch_size(ctx.event)?;
119 Ok(ctx.quest.current + batch)
120}
121
122fn event_batch_size(event: &OverlordEvent) -> anyhow::Result<i64> {
125 match event {
126 OverlordEvent::OpenItemCase { batch_size } => Ok(*batch_size),
127 OverlordEvent::AutoChestOpenItemCase { batch_size } => Ok(*batch_size),
128 OverlordEvent::UpdateAutoChestBatchSize { batch_size } => Ok(*batch_size),
129 OverlordEvent::AbilityCaseOpened { batch_size } => Ok(*batch_size as i64),
130 OverlordEvent::PetCaseOpened { batch_size } => Ok(*batch_size as i64),
131 other => Err(anyhow::anyhow!("event {other:?} has no batch_size")),
132 }
133}
134
135pub fn loop_task_increment_one<const N: i64>(ctx: &ConditionalProgressCtx) -> anyhow::Result<i64> {
140 if is_custom_event_any(ctx.event) {
141 if is_custom_event(ctx.event, COMPLETE_ALL_LOOP_TASKS) {
142 return Ok(N);
143 }
144 return Ok(ctx.quest.current);
145 }
146 if is_non_counting_entity_death(ctx) {
149 return Ok(ctx.quest.current);
150 }
151 Ok(ctx.quest.current + 1)
152}
153
154pub fn loop_task_increment_batch<const N: i64>(
156 ctx: &ConditionalProgressCtx,
157) -> anyhow::Result<i64> {
158 if is_custom_event_any(ctx.event) {
159 if is_custom_event(ctx.event, COMPLETE_ALL_LOOP_TASKS) {
160 return Ok(N);
161 }
162 return Ok(ctx.quest.current);
163 }
164 let batch = event_batch_size(ctx.event)?;
165 Ok(ctx.quest.current + batch)
166}
167
168fn event_level(event: &OverlordEvent) -> anyhow::Result<i64> {
171 match event {
172 OverlordEvent::NewCharacterLevel { level } => Ok(*level),
173 other => Err(anyhow::anyhow!("event {other:?} has no level")),
174 }
175}
176
177pub fn reach_level<const T: i64>(ctx: &ConditionalProgressCtx) -> anyhow::Result<i64> {
179 let level = event_level(ctx.event)?;
180 Ok(if level >= T { 1 } else { 0 })
181}
182
183pub fn loop_task_reach_level<const T: i64>(ctx: &ConditionalProgressCtx) -> anyhow::Result<i64> {
186 if is_custom_event_any(ctx.event) {
187 if is_custom_event(ctx.event, COMPLETE_ALL_LOOP_TASKS) {
188 return Ok(1);
189 }
190 return Ok(ctx.quest.current);
191 }
192 let level = event_level(ctx.event)?;
193 Ok(if level >= T { 1 } else { 0 })
194}
195
196pub fn reach_chapter_level<const T: i64>(ctx: &ConditionalProgressCtx) -> anyhow::Result<i64> {
200 let chapter = ctx.character_state.character.current_chapter_level;
201 Ok(if chapter >= T { 1 } else { 0 })
202}
203
204pub fn current_chapter_level(ctx: &ConditionalProgressCtx) -> anyhow::Result<i64> {
213 Ok(ctx.character_state.character.current_chapter_level)
214}
215
216pub fn loop_task_reach_chapter_level<const T: i64>(
218 ctx: &ConditionalProgressCtx,
219) -> anyhow::Result<i64> {
220 if is_custom_event_any(ctx.event) {
221 if is_custom_event(ctx.event, COMPLETE_ALL_LOOP_TASKS) {
222 return Ok(1);
223 }
224 return Ok(ctx.quest.current);
225 }
226 let chapter = ctx.character_state.character.current_chapter_level;
227 Ok(if chapter >= T { 1 } else { 0 })
228}
229
230fn pvp_win_increment(is_win: bool, is_pvp: bool, current: i64) -> i64 {
238 if is_win && is_pvp {
239 current + 1
240 } else {
241 current
242 }
243}
244
245fn pvp_win_signal(event: &OverlordEvent) -> (bool, bool) {
248 match event {
249 OverlordEvent::EndFight {
250 is_win, pvp_state, ..
251 } => (*is_win, pvp_state.is_some()),
252 _ => (false, false),
253 }
254}
255
256pub fn pvp_win(ctx: &ConditionalProgressCtx) -> anyhow::Result<i64> {
260 let (is_win, is_pvp) = pvp_win_signal(ctx.event);
261 Ok(pvp_win_increment(is_win, is_pvp, ctx.quest.current))
262}
263
264pub fn loop_task_pvp_win(ctx: &ConditionalProgressCtx) -> anyhow::Result<i64> {
267 if is_custom_event(ctx.event, COMPLETE_ALL_LOOP_TASKS) {
268 return Ok(1);
269 }
270 let (is_win, is_pvp) = pvp_win_signal(ctx.event);
271 Ok(pvp_win_increment(is_win, is_pvp, ctx.quest.current))
272}
273
274pub fn raid_dungeon<const D: u128>(ctx: &ConditionalProgressCtx) -> anyhow::Result<i64> {
279 let dungeon = Uuid::from_u128(D);
280 if let OverlordEvent::RaidDungeon { dungeon_id, .. } = ctx.event {
281 if *dungeon_id == dungeon {
282 return Ok(ctx.quest.current + 1);
283 }
284 return Ok(ctx.quest.current);
285 }
286 let Some(fight) = ctx.active_fight else {
287 return Ok(ctx.quest.current);
288 };
289 if fight.dungeon.as_ref().map(|d| d.id) == Some(dungeon) {
290 return Ok(ctx.quest.current + 1);
291 }
292 Ok(ctx.quest.current)
293}
294
295pub fn loop_task_raid_dungeon<const D: u128>(ctx: &ConditionalProgressCtx) -> anyhow::Result<i64> {
297 if is_custom_event_any(ctx.event) {
298 if is_custom_event(ctx.event, COMPLETE_ALL_LOOP_TASKS) {
299 return Ok(1);
300 }
301 return Ok(ctx.quest.current);
302 }
303 raid_dungeon::<D>(ctx)
304}
305
306fn event_quest_id(event: &OverlordEvent) -> anyhow::Result<Uuid> {
310 match event {
311 OverlordEvent::ClaimQuest { quest_id, .. }
312 | OverlordEvent::PatronQuestCompleted { quest_id, .. }
313 | OverlordEvent::HiddenQuestCompleted { quest_id, .. }
314 | OverlordEvent::QuestCompleted { quest_id, .. }
315 | OverlordEvent::UpdateActiveLoopTaskId { quest_id, .. } => Ok(*quest_id),
316 other => Err(anyhow::anyhow!("event {other:?} has no quest_id")),
317 }
318}
319
320fn complete_quest_of_group(
322 ctx: &ConditionalProgressCtx,
323 group: QuestGroupType,
324) -> anyhow::Result<i64> {
325 let quest_id = event_quest_id(ctx.event)?;
326 let tpl = ctx
327 .config
328 .quest(quest_id)
329 .ok_or_else(|| anyhow::anyhow!("quest {quest_id} not found"))?;
330 if tpl.quest_group_type == group {
331 return Ok(ctx.quest.current + 1);
332 }
333 Ok(ctx.quest.current)
334}
335
336pub fn complete_daily_quest(ctx: &ConditionalProgressCtx) -> anyhow::Result<i64> {
337 complete_quest_of_group(ctx, QuestGroupType::Daily)
338}
339pub fn complete_weekly_quest(ctx: &ConditionalProgressCtx) -> anyhow::Result<i64> {
340 complete_quest_of_group(ctx, QuestGroupType::Weekly)
341}
342pub fn complete_loop_task_quest(ctx: &ConditionalProgressCtx) -> anyhow::Result<i64> {
343 complete_quest_of_group(ctx, QuestGroupType::LoopTask)
344}
345
346pub fn loop_task_collect_currency(ctx: &ConditionalProgressCtx) -> anyhow::Result<i64> {
353 if is_custom_event_any(ctx.event) {
354 if is_custom_event(ctx.event, COMPLETE_ALL_LOOP_TASKS) {
355 return Ok(100);
356 }
357 return Ok(ctx.quest.current);
358 }
359 let target = Uuid::from_u128(COLLECT_CURRENCY);
360 let currencies: &[essences::currency::CurrencyUnit] = match ctx.event {
361 OverlordEvent::CurrencyIncrease { currencies, .. } => currencies,
362 OverlordEvent::CurrencyDecrease { currencies, .. } => currencies,
363 other => return Err(anyhow::anyhow!("event {other:?} has no currencies")),
364 };
365 for unit in currencies {
366 if unit.currency_id == target {
367 return Ok(ctx.quest.current + unit.amount);
368 }
369 }
370 Ok(0)
371}
372
373pub fn upgraded_abilities_delta(ctx: &ConditionalProgressCtx) -> anyhow::Result<i64> {
377 let OverlordEvent::UpgradedAbilities { upgraded_abilities } = ctx.event else {
378 return Err(anyhow::anyhow!(
379 "event {:?} has no upgraded_abilities",
380 ctx.event
381 ));
382 };
383 let mut result: i64 = 0;
384 for (_ability_id, (current, final_)) in upgraded_abilities.0.iter() {
385 result += final_ - current;
386 }
387 Ok(ctx.quest.current + result)
388}
389
390pub fn count_equipped_at_rarity<const R: u128>(
396 ctx: &ConditionalProgressCtx,
397) -> anyhow::Result<i64> {
398 let target_id = Uuid::from_u128(R);
399 let target_q = ctx
400 .lookups
401 .item_rarity_q
402 .get(&target_id)
403 .copied()
404 .ok_or_else(|| anyhow::anyhow!("no q for rarity {target_id}"))?;
405
406 if !matches!(ctx.event, OverlordEvent::PlayerEquipItem { .. }) {
407 return Ok(ctx.quest.current);
408 }
409
410 let mut res = 0i64;
411 for item in &ctx.character_state.inventory {
412 if !item.is_equipped {
413 continue;
414 }
415 let rarity_q = ctx
416 .lookups
417 .item_rarity_q
418 .get(&item.rarity.id)
419 .copied()
420 .ok_or_else(|| anyhow::anyhow!("no q for item rarity {}", item.rarity.id))?;
421 if rarity_q >= target_q {
422 res += 1;
423 }
424 }
425 Ok(res)
426}
427
428pub fn always_one(_ctx: &ConditionalProgressCtx) -> anyhow::Result<i64> {
432 Ok(1)
433}
434
435pub fn log_in_today(ctx: &ConditionalProgressCtx) -> anyhow::Result<i64> {
439 let quest_id = event_quest_id(ctx.event)?;
440 if quest_id == Uuid::from_u128(LOG_IN_QUEST) {
441 return Ok(ctx.quest.current + 1);
442 }
443 Ok(ctx.quest.current)
444}
445
446fn boss_win_step(is_win: bool, is_boss_fight: bool, current: i64) -> i64 {
450 if is_win && is_boss_fight {
451 current + 1
452 } else {
453 current
454 }
455}
456
457pub fn boss_win(ctx: &ConditionalProgressCtx) -> anyhow::Result<i64> {
465 let OverlordEvent::EndFight { is_win, .. } = ctx.event else {
466 return Ok(ctx.quest.current);
467 };
468 let is_boss = ctx.active_fight.as_ref().is_some_and(|f| {
469 f.dungeon.is_none()
470 && ctx
471 .config
472 .require_fight_template(f.fight_id)
473 .is_ok_and(|tpl| tpl.fight_type == essences::fighting::FightType::CampaignBossFight)
474 });
475 Ok(boss_win_step(*is_win, is_boss, ctx.quest.current))
476}
477
478pub fn loop_task_boss_win<const N: i64>(ctx: &ConditionalProgressCtx) -> anyhow::Result<i64> {
480 if is_custom_event_any(ctx.event) {
481 if is_custom_event(ctx.event, COMPLETE_ALL_LOOP_TASKS) {
482 return Ok(N);
483 }
484 return Ok(ctx.quest.current);
485 }
486 boss_win(ctx)
487}
488
489fn pvp_streak_step(is_win: bool, is_pvp: bool, current: i64) -> i64 {
494 match (is_pvp, is_win) {
495 (true, true) => current + 1,
496 (true, false) => 0,
497 (false, _) => current,
498 }
499}
500
501pub fn pvp_win_streak(ctx: &ConditionalProgressCtx) -> anyhow::Result<i64> {
503 let (is_win, is_pvp) = pvp_win_signal(ctx.event);
504 Ok(pvp_streak_step(is_win, is_pvp, ctx.quest.current))
505}
506
507fn pvp_revenge_step(is_win: bool, is_pvp: bool, current: i64) -> i64 {
514 if !is_pvp {
515 return current;
516 }
517 match (current, is_win) {
518 (0, false) => 1,
519 (1, true) => 2,
520 (c, _) => c,
521 }
522}
523
524pub fn pvp_revenge(ctx: &ConditionalProgressCtx) -> anyhow::Result<i64> {
527 let (is_win, is_pvp) = pvp_win_signal(ctx.event);
528 Ok(pvp_revenge_step(is_win, is_pvp, ctx.quest.current))
529}
530
531pub fn raid_dungeon_any(ctx: &ConditionalProgressCtx) -> anyhow::Result<i64> {
537 if matches!(ctx.event, OverlordEvent::RaidDungeon { .. }) {
538 return Ok(ctx.quest.current + 1);
539 }
540 Ok(ctx.quest.current)
541}
542
543pub fn raid_dungeon_difficulty_at_least<const N: i64>(
546 ctx: &ConditionalProgressCtx,
547) -> anyhow::Result<i64> {
548 if let OverlordEvent::RaidDungeon { difficulty, .. } = ctx.event
549 && *difficulty >= N
550 {
551 return Ok(ctx.quest.current + 1);
552 }
553 Ok(ctx.quest.current)
554}
555
556pub fn quest_event_level_1_then_2(ctx: &ConditionalProgressCtx) -> anyhow::Result<i64> {
560 let level = event_level(ctx.event)?;
561 Ok(match level {
562 1 => 1,
563 2 => 2,
564 _ => 0,
565 })
566}
567
568pub fn quest_current_0_then_1_else_2(ctx: &ConditionalProgressCtx) -> anyhow::Result<i64> {
570 Ok(if ctx.quest.current == 0 { 1 } else { 2 })
571}
572
573pub fn register(registry: &mut BehaviorRegistry) {
575 let mut reg = |name: &str, title: &str, desc: &str, f: ConditionalProgressFn| {
576 registry.register_conditional_progress(
577 BehaviorMeta {
578 name: name.to_string(),
579 category: BehaviorKind::ConditionalProgress,
580 title: title.to_string(),
581 description: desc.to_string(),
582 },
583 f,
584 );
585 };
586
587 reg(
588 "increment_one",
589 "Прогресс +1",
590 "Безусловно увеличивает прогресс квеста на 1.",
591 increment_one,
592 );
593 reg(
594 "increment_by_batch_size",
595 "Прогресс += batch_size",
596 "Увеличивает прогресс на Event.batch_size.",
597 increment_by_batch_size,
598 );
599
600 for (name, title, f) in [
601 (
602 "loop_task_increment_one_n1",
603 "Loop-task: +1, цель 1",
604 loop_task_increment_one::<1> as ConditionalProgressFn,
605 ),
606 (
607 "loop_task_increment_one_n2",
608 "Loop-task: +1, цель 2",
609 loop_task_increment_one::<2>,
610 ),
611 (
612 "loop_task_increment_one_n3",
613 "Loop-task: +1, цель 3",
614 loop_task_increment_one::<3>,
615 ),
616 (
617 "loop_task_increment_one_n5",
618 "Loop-task: +1, цель 5",
619 loop_task_increment_one::<5>,
620 ),
621 (
622 "loop_task_increment_one_n6",
623 "Loop-task: +1, цель 6",
624 loop_task_increment_one::<6>,
625 ),
626 (
627 "loop_task_increment_one_n10",
628 "Loop-task: +1, цель 10",
629 loop_task_increment_one::<10>,
630 ),
631 (
632 "loop_task_increment_one_n12",
633 "Loop-task: +1, цель 12",
634 loop_task_increment_one::<12>,
635 ),
636 (
637 "loop_task_increment_one_n20",
638 "Loop-task: +1, цель 20",
639 loop_task_increment_one::<20>,
640 ),
641 (
642 "loop_task_increment_one_n7",
643 "Loop-task: +1, цель 7",
644 loop_task_increment_one::<7>,
645 ),
646 (
647 "loop_task_increment_one_n17",
648 "Loop-task: +1, цель 17",
649 loop_task_increment_one::<17>,
650 ),
651 (
652 "loop_task_increment_one_n28",
653 "Loop-task: +1, цель 28",
654 loop_task_increment_one::<28>,
655 ),
656 (
657 "loop_task_increment_one_n42",
658 "Loop-task: +1, цель 42",
659 loop_task_increment_one::<42>,
660 ),
661 ] {
662 reg(
663 name,
664 title,
665 "CompleteAllLoopTasks => цель; иначе прогресс +1.",
666 f,
667 );
668 }
669 reg(
670 "loop_task_increment_batch_n1",
671 "Loop-task: += batch_size, цель 1",
672 "CompleteAllLoopTasks => 1; иначе += Event.batch_size (1 pull completes даже при batch).",
673 loop_task_increment_batch::<1>,
674 );
675 reg(
676 "loop_task_increment_batch_n10",
677 "Loop-task: += batch_size, цель 10",
678 "CompleteAllLoopTasks => 10; иначе += Event.batch_size.",
679 loop_task_increment_batch::<10>,
680 );
681
682 reg(
683 "reach_level_2",
684 "Достичь уровня 2",
685 "Event.level >= 2 => 1, иначе 0 (без loop-task guard).",
686 reach_level::<2>,
687 );
688 for (name, title, f) in [
689 (
690 "loop_task_reach_level_3",
691 "Loop-task: достичь уровня 3",
692 loop_task_reach_level::<3> as ConditionalProgressFn,
693 ),
694 (
695 "loop_task_reach_level_5",
696 "Loop-task: достичь уровня 5",
697 loop_task_reach_level::<5>,
698 ),
699 (
700 "loop_task_reach_level_10",
701 "Loop-task: достичь уровня 10",
702 loop_task_reach_level::<10>,
703 ),
704 (
705 "loop_task_reach_level_15",
706 "Loop-task: достичь уровня 15",
707 loop_task_reach_level::<15>,
708 ),
709 (
710 "loop_task_reach_level_20",
711 "Loop-task: достичь уровня 20",
712 loop_task_reach_level::<20>,
713 ),
714 (
715 "loop_task_reach_level_25",
716 "Loop-task: достичь уровня 25",
717 loop_task_reach_level::<25>,
718 ),
719 (
720 "loop_task_reach_level_26",
721 "Loop-task: достичь уровня 26",
722 loop_task_reach_level::<26>,
723 ),
724 (
725 "loop_task_reach_level_27",
726 "Loop-task: достичь уровня 27",
727 loop_task_reach_level::<27>,
728 ),
729 (
730 "loop_task_reach_level_28",
731 "Loop-task: достичь уровня 28",
732 loop_task_reach_level::<28>,
733 ),
734 (
735 "loop_task_reach_level_29",
736 "Loop-task: достичь уровня 29",
737 loop_task_reach_level::<29>,
738 ),
739 (
740 "loop_task_reach_level_30",
741 "Loop-task: достичь уровня 30",
742 loop_task_reach_level::<30>,
743 ),
744 (
745 "loop_task_reach_level_35",
746 "Loop-task: достичь уровня 35",
747 loop_task_reach_level::<35>,
748 ),
749 (
750 "loop_task_reach_level_40",
751 "Loop-task: достичь уровня 40",
752 loop_task_reach_level::<40>,
753 ),
754 (
755 "loop_task_reach_level_45",
756 "Loop-task: достичь уровня 45",
757 loop_task_reach_level::<45>,
758 ),
759 (
760 "loop_task_reach_level_50",
761 "Loop-task: достичь уровня 50",
762 loop_task_reach_level::<50>,
763 ),
764 (
765 "loop_task_reach_level_55",
766 "Loop-task: достичь уровня 55",
767 loop_task_reach_level::<55>,
768 ),
769 (
770 "loop_task_reach_level_60",
771 "Loop-task: достичь уровня 60",
772 loop_task_reach_level::<60>,
773 ),
774 (
775 "loop_task_reach_level_70",
776 "Loop-task: достичь уровня 70",
777 loop_task_reach_level::<70>,
778 ),
779 (
780 "loop_task_reach_level_80",
781 "Loop-task: достичь уровня 80",
782 loop_task_reach_level::<80>,
783 ),
784 (
785 "loop_task_reach_level_100",
786 "Loop-task: достичь уровня 100",
787 loop_task_reach_level::<100>,
788 ),
789 ] {
790 reg(
791 name,
792 title,
793 "CompleteAllLoopTasks => 1; иначе Event.level >= порог ? 1 : 0.",
794 f,
795 );
796 }
797
798 for (name, title, f) in [
801 (
802 "reach_chapter_level_1",
803 "Достичь главы 1",
804 reach_chapter_level::<1> as ConditionalProgressFn,
805 ),
806 (
807 "reach_chapter_level_2",
808 "Достичь главы 2",
809 reach_chapter_level::<2>,
810 ),
811 (
812 "reach_chapter_level_3",
813 "Достичь главы 3",
814 reach_chapter_level::<3>,
815 ),
816 (
817 "reach_chapter_level_4",
818 "Достичь главы 4",
819 reach_chapter_level::<4>,
820 ),
821 (
822 "reach_chapter_level_5",
823 "Достичь главы 5",
824 reach_chapter_level::<5>,
825 ),
826 (
827 "reach_chapter_level_6",
828 "Достичь главы 6",
829 reach_chapter_level::<6>,
830 ),
831 (
832 "reach_chapter_level_7",
833 "Достичь главы 7",
834 reach_chapter_level::<7>,
835 ),
836 (
837 "reach_chapter_level_8",
838 "Достичь главы 8",
839 reach_chapter_level::<8>,
840 ),
841 (
842 "reach_chapter_level_9",
843 "Достичь главы 9",
844 reach_chapter_level::<9>,
845 ),
846 (
847 "reach_chapter_level_10",
848 "Достичь главы 10",
849 reach_chapter_level::<10>,
850 ),
851 (
852 "reach_chapter_level_11",
853 "Достичь главы 11",
854 reach_chapter_level::<11>,
855 ),
856 (
857 "reach_chapter_level_12",
858 "Достичь главы 12",
859 reach_chapter_level::<12>,
860 ),
861 (
862 "reach_chapter_level_13",
863 "Достичь главы 13",
864 reach_chapter_level::<13>,
865 ),
866 (
867 "reach_chapter_level_14",
868 "Достичь главы 14",
869 reach_chapter_level::<14>,
870 ),
871 (
872 "reach_chapter_level_15",
873 "Достичь главы 15",
874 reach_chapter_level::<15>,
875 ),
876 (
877 "reach_chapter_level_16",
878 "Достичь главы 16",
879 reach_chapter_level::<16>,
880 ),
881 (
882 "reach_chapter_level_17",
883 "Достичь главы 17",
884 reach_chapter_level::<17>,
885 ),
886 (
887 "reach_chapter_level_18",
888 "Достичь главы 18",
889 reach_chapter_level::<18>,
890 ),
891 (
892 "reach_chapter_level_19",
893 "Достичь главы 19",
894 reach_chapter_level::<19>,
895 ),
896 (
897 "reach_chapter_level_20",
898 "Достичь главы 20",
899 reach_chapter_level::<20>,
900 ),
901 ] {
902 reg(
903 name,
904 title,
905 "current_chapter_level >= N => 1, иначе 0 (без guard).",
906 f,
907 );
908 }
909 reg(
912 "reach_chapter_level_28",
913 "Достичь главы 28",
914 "current_chapter_level >= 28 => 1, иначе 0 (без guard).",
915 reach_chapter_level::<28>,
916 );
917 reg(
920 "reach_chapter_level_35",
921 "Достичь главы 35",
922 "current_chapter_level >= 35 => 1, иначе 0 (без guard).",
923 reach_chapter_level::<35>,
924 );
925 for (name, title, f) in [
926 (
927 "loop_task_reach_chapter_level_5",
928 "Loop-task: глава 5",
929 loop_task_reach_chapter_level::<5> as ConditionalProgressFn,
930 ),
931 (
932 "loop_task_reach_chapter_level_6",
933 "Loop-task: глава 6",
934 loop_task_reach_chapter_level::<6>,
935 ),
936 (
937 "loop_task_reach_chapter_level_7",
938 "Loop-task: глава 7",
939 loop_task_reach_chapter_level::<7>,
940 ),
941 (
942 "loop_task_reach_chapter_level_8",
943 "Loop-task: глава 8",
944 loop_task_reach_chapter_level::<8>,
945 ),
946 (
947 "loop_task_reach_chapter_level_9",
948 "Loop-task: глава 9",
949 loop_task_reach_chapter_level::<9>,
950 ),
951 (
952 "loop_task_reach_chapter_level_10",
953 "Loop-task: глава 10",
954 loop_task_reach_chapter_level::<10>,
955 ),
956 (
957 "loop_task_reach_chapter_level_11",
958 "Loop-task: глава 11",
959 loop_task_reach_chapter_level::<11>,
960 ),
961 (
962 "loop_task_reach_chapter_level_15",
963 "Loop-task: глава 15",
964 loop_task_reach_chapter_level::<15>,
965 ),
966 (
967 "loop_task_reach_chapter_level_20",
968 "Loop-task: глава 20",
969 loop_task_reach_chapter_level::<20>,
970 ),
971 (
972 "loop_task_reach_chapter_level_25",
973 "Loop-task: глава 25",
974 loop_task_reach_chapter_level::<25>,
975 ),
976 (
977 "loop_task_reach_chapter_level_30",
978 "Loop-task: глава 30",
979 loop_task_reach_chapter_level::<30>,
980 ),
981 (
982 "loop_task_reach_chapter_level_35",
983 "Loop-task: глава 35",
984 loop_task_reach_chapter_level::<35>,
985 ),
986 (
987 "loop_task_reach_chapter_level_40",
988 "Loop-task: глава 40",
989 loop_task_reach_chapter_level::<40>,
990 ),
991 (
992 "loop_task_reach_chapter_level_45",
993 "Loop-task: глава 45",
994 loop_task_reach_chapter_level::<45>,
995 ),
996 (
997 "loop_task_reach_chapter_level_50",
998 "Loop-task: глава 50",
999 loop_task_reach_chapter_level::<50>,
1000 ),
1001 (
1002 "loop_task_reach_chapter_level_55",
1003 "Loop-task: глава 55",
1004 loop_task_reach_chapter_level::<55>,
1005 ),
1006 (
1007 "loop_task_reach_chapter_level_60",
1008 "Loop-task: глава 60",
1009 loop_task_reach_chapter_level::<60>,
1010 ),
1011 (
1012 "loop_task_reach_chapter_level_70",
1013 "Loop-task: глава 70",
1014 loop_task_reach_chapter_level::<70>,
1015 ),
1016 (
1017 "loop_task_reach_chapter_level_80",
1018 "Loop-task: глава 80",
1019 loop_task_reach_chapter_level::<80>,
1020 ),
1021 ] {
1022 reg(
1023 name,
1024 title,
1025 "CompleteAllLoopTasks => 1; иначе current_chapter_level >= порог ? 1 : 0.",
1026 f,
1027 );
1028 }
1029
1030 reg(
1031 "current_chapter_level",
1032 "Текущая глава (порог = progress_target)",
1033 "Возвращает current_chapter_level; квест завершён при достижении главы progress_target. Используется тирами прогресс-пасса (ch35→130).",
1034 current_chapter_level,
1035 );
1036
1037 reg(
1038 "pvp_win",
1039 "Победа в PvP",
1040 "EndFight с pvp_state => +1, иначе без изменений.",
1041 pvp_win,
1042 );
1043 reg(
1044 "loop_task_pvp_win",
1045 "Loop-task: победа в PvP",
1046 "CompleteAllLoopTasks => 1; иначе EndFight с pvp_state => +1.",
1047 loop_task_pvp_win,
1048 );
1049
1050 for (name, title, f) in [
1051 (
1052 "raid_dungeon_1",
1053 "Рейд подземелья D1",
1054 raid_dungeon::<DUNGEON_1> as ConditionalProgressFn,
1055 ),
1056 (
1057 "raid_dungeon_2",
1058 "Рейд подземелья D2",
1059 raid_dungeon::<DUNGEON_2>,
1060 ),
1061 (
1062 "raid_dungeon_3",
1063 "Рейд подземелья D3",
1064 raid_dungeon::<DUNGEON_3>,
1065 ),
1066 (
1067 "loop_task_raid_dungeon_1",
1068 "Loop-task: рейд D1",
1069 loop_task_raid_dungeon::<DUNGEON_1>,
1070 ),
1071 (
1072 "loop_task_raid_dungeon_2",
1073 "Loop-task: рейд D2",
1074 loop_task_raid_dungeon::<DUNGEON_2>,
1075 ),
1076 (
1077 "loop_task_raid_dungeon_3",
1078 "Loop-task: рейд D3",
1079 loop_task_raid_dungeon::<DUNGEON_3>,
1080 ),
1081 ] {
1082 reg(
1083 name,
1084 title,
1085 "RaidDungeon/ActiveFight по целевому dungeon => +1 (loop-task варианты с guard).",
1086 f,
1087 );
1088 }
1089
1090 reg(
1091 "complete_daily_quest",
1092 "Завершить дневной квест",
1093 "+1 если завершённый квест имеет group_type Daily.",
1094 complete_daily_quest,
1095 );
1096 reg(
1097 "complete_weekly_quest",
1098 "Завершить недельный квест",
1099 "+1 если завершённый квест имеет group_type Weekly.",
1100 complete_weekly_quest,
1101 );
1102 reg(
1103 "complete_loop_task_quest",
1104 "Завершить loop-task квест",
1105 "+1 если завершённый квест имеет group_type LoopTask.",
1106 complete_loop_task_quest,
1107 );
1108
1109 reg(
1110 "loop_task_collect_currency",
1111 "Loop-task: собрать валюту",
1112 "CompleteAllLoopTasks => 100; иначе += amount нужной валюты, иначе 0.",
1113 loop_task_collect_currency,
1114 );
1115 reg(
1116 "upgraded_abilities_delta",
1117 "Сумма апгрейдов способностей",
1118 "+= сумма (final - current) по Event.upgraded_abilities.",
1119 upgraded_abilities_delta,
1120 );
1121
1122 for (name, title, f) in [
1123 (
1124 "count_equipped_rarity_a",
1125 "Счётчик экип. предметов редкости A",
1126 count_equipped_at_rarity::<RARITY_A> as ConditionalProgressFn,
1127 ),
1128 (
1129 "count_equipped_rarity_b",
1130 "Счётчик экип. предметов редкости B",
1131 count_equipped_at_rarity::<RARITY_B>,
1132 ),
1133 (
1134 "count_equipped_rarity_c",
1135 "Счётчик экип. предметов редкости C",
1136 count_equipped_at_rarity::<RARITY_C>,
1137 ),
1138 ] {
1139 reg(
1140 name,
1141 title,
1142 "Кол-во экипированных предметов с q >= q целевой редкости (PlayerEquipItem).",
1143 f,
1144 );
1145 }
1146
1147 reg(
1148 "always_one",
1149 "Всегда 1",
1150 "Безусловно возвращает 1.",
1151 always_one,
1152 );
1153 reg(
1154 "log_in_today",
1155 "Вход сегодня",
1156 "+1 если Event.quest_id совпадает с дневным квестом входа.",
1157 log_in_today,
1158 );
1159
1160 reg(
1163 "boss_win",
1164 "Победи босса",
1165 "+1 за выигранный EndFight в бою типа CampaignBossFight (кампания и данжи).",
1166 boss_win,
1167 );
1168 for (name, title, f) in [
1169 (
1170 "loop_task_boss_win_n1",
1171 "Loop-task: победи босса",
1172 loop_task_boss_win::<1> as ConditionalProgressFn,
1173 ),
1174 (
1175 "loop_task_boss_win_n2",
1176 "Loop-task: победи 2 боссов",
1177 loop_task_boss_win::<2>,
1178 ),
1179 ] {
1180 reg(
1181 name,
1182 title,
1183 "CompleteAllLoopTasks => цель; иначе +1 за выигранный босс-бой.",
1184 f,
1185 );
1186 }
1187 reg(
1188 "pvp_win_streak",
1189 "Победы на арене подряд",
1190 "Победа в PvP +1, поражение сбрасывает в 0.",
1191 pvp_win_streak,
1192 );
1193 reg(
1194 "pvp_revenge",
1195 "Реванш после поражения",
1196 "target=2: поражение в PvP (0→1), затем победа (1→2); победа без поражения не двигает.",
1197 pvp_revenge,
1198 );
1199 reg(
1200 "raid_dungeon_any",
1201 "Рейд любого данжа",
1202 "+1 за любой RaidDungeon (тип не важен; свип = одно действие).",
1203 raid_dungeon_any,
1204 );
1205 for (name, title, f) in [
1206 (
1207 "raid_dungeon_difficulty_2",
1208 "Данж на сложности 2+",
1209 raid_dungeon_difficulty_at_least::<2> as ConditionalProgressFn,
1210 ),
1211 (
1212 "raid_dungeon_difficulty_4",
1213 "Данж на сложности 4+",
1214 raid_dungeon_difficulty_at_least::<4>,
1215 ),
1216 (
1217 "raid_dungeon_difficulty_6",
1218 "Данж на сложности 6+",
1219 raid_dungeon_difficulty_at_least::<6>,
1220 ),
1221 (
1222 "raid_dungeon_difficulty_8",
1223 "Данж на сложности 8+",
1224 raid_dungeon_difficulty_at_least::<8>,
1225 ),
1226 ] {
1227 reg(
1228 name,
1229 title,
1230 "+1 за RaidDungeon со сложностью не ниже порога.",
1231 f,
1232 );
1233 }
1234 for (name, title, f) in [
1235 (
1236 "loop_task_increment_one_n4",
1237 "Loop-task: +1, цель 4",
1238 loop_task_increment_one::<4> as ConditionalProgressFn,
1239 ),
1240 (
1241 "loop_task_increment_one_n8",
1242 "Loop-task: +1, цель 8",
1243 loop_task_increment_one::<8>,
1244 ),
1245 (
1246 "loop_task_increment_one_n15",
1247 "Loop-task: +1, цель 15",
1248 loop_task_increment_one::<15>,
1249 ),
1250 (
1251 "loop_task_increment_one_n30",
1252 "Loop-task: +1, цель 30",
1253 loop_task_increment_one::<30>,
1254 ),
1255 ] {
1256 reg(
1257 name,
1258 title,
1259 "CompleteAllLoopTasks => цель; иначе прогресс +1.",
1260 f,
1261 );
1262 }
1263 reg(
1264 "loop_task_increment_batch_n15",
1265 "Loop-task: += batch_size, цель 15",
1266 "CompleteAllLoopTasks => 15; иначе += Event.batch_size.",
1267 loop_task_increment_batch::<15>,
1268 );
1269 reg(
1270 "loop_task_increment_batch_n20",
1271 "Loop-task: += batch_size, цель 20",
1272 "CompleteAllLoopTasks => 20; иначе += Event.batch_size.",
1273 loop_task_increment_batch::<20>,
1274 );
1275
1276 reg(
1277 "quest_event_level_1_then_2",
1278 "Тест: level 1 => 1, level 2 => 2",
1279 "Тестовый прогресс: level 1 => 1, level 2 => 2, иначе 0.",
1280 quest_event_level_1_then_2,
1281 );
1282 reg(
1283 "quest_current_0_then_1_else_2",
1284 "Тест: current 0 => 1, иначе 2",
1285 "Тестовый прогресс: current 0 => 1, иначе 2.",
1286 quest_current_0_then_1_else_2,
1287 );
1288}
1289
1290#[cfg(test)]
1291mod tests {
1292 use super::*;
1293
1294 use essences::combat_origin::CombatEventOrigin;
1295
1296 #[test]
1301 fn pvp_win_counts_only_player_victories() {
1302 assert_eq!(pvp_win_increment(true, true, 5), 6);
1304 assert_eq!(pvp_win_increment(false, true, 5), 5);
1306 assert_eq!(pvp_win_increment(true, false, 5), 5);
1308 assert_eq!(pvp_win_increment(false, false, 5), 5);
1310 }
1311
1312 #[test]
1315 fn pvp_win_signal_reads_only_endfight() {
1316 let non_fight = OverlordEvent::PetCaseOpened { batch_size: 1 };
1317 assert_eq!(pvp_win_signal(&non_fight), (false, false));
1318 }
1319
1320 #[test]
1323 fn boss_win_requires_both_victory_and_boss_fight() {
1324 assert_eq!(boss_win_step(true, true, 3), 4);
1325 assert_eq!(boss_win_step(false, true, 3), 3);
1326 assert_eq!(boss_win_step(true, false, 3), 3);
1327 }
1328
1329 #[test]
1332 fn pvp_streak_resets_on_loss() {
1333 assert_eq!(pvp_streak_step(true, true, 2), 3);
1334 assert_eq!(pvp_streak_step(false, true, 2), 0);
1335 assert_eq!(pvp_streak_step(true, false, 2), 2);
1336 assert_eq!(pvp_streak_step(false, false, 2), 2);
1337 }
1338
1339 #[test]
1344 fn pvp_revenge_is_loss_then_win() {
1345 assert_eq!(pvp_revenge_step(true, true, 0), 0);
1346 assert_eq!(pvp_revenge_step(false, true, 0), 1);
1347 assert_eq!(pvp_revenge_step(false, true, 1), 1);
1348 assert_eq!(pvp_revenge_step(true, true, 1), 2);
1349 assert_eq!(pvp_revenge_step(true, false, 1), 1);
1350 }
1351
1352 #[test]
1357 fn kill_quest_skips_ally_death() {
1358 let config = configs::tests_game_config::generate_game_config_for_tests();
1359 let lookups = ContentLookups::default();
1360 let character_state = CharacterState::default();
1361 let quest = QuestInstance {
1362 id: uuid::Uuid::from_u128(99),
1363 current: 5,
1364 reward: vec![],
1365 is_claimed: false,
1366 };
1367 let ally_id = uuid::Uuid::from_u128(42);
1368 let fight = Some(ActiveFight {
1369 party_player_id: Some(ally_id),
1370 ..Default::default()
1371 });
1372 let enemy_death = OverlordEvent::EntityDeath {
1374 entity_id: uuid::Uuid::from_u128(1),
1375 reward: vec![],
1376 origin: CombatEventOrigin::Core,
1377 };
1378 let enemy_ctx = ConditionalProgressCtx {
1379 event: &enemy_death,
1380 character_state: &character_state,
1381 active_fight: &fight,
1382 quest: &quest,
1383 config: &config,
1384 lookups: &lookups,
1385 };
1386 assert_eq!(increment_one(&enemy_ctx).unwrap(), 6);
1387 assert_eq!(loop_task_increment_one::<12>(&enemy_ctx).unwrap(), 6);
1388
1389 let ally_death = OverlordEvent::EntityDeath {
1391 entity_id: ally_id,
1392 reward: vec![],
1393 origin: CombatEventOrigin::Core,
1394 };
1395 let ally_ctx = ConditionalProgressCtx {
1396 event: &ally_death,
1397 character_state: &character_state,
1398 active_fight: &fight,
1399 quest: &quest,
1400 config: &config,
1401 lookups: &lookups,
1402 };
1403 assert_eq!(increment_one(&ally_ctx).unwrap(), 5);
1404 assert_eq!(loop_task_increment_one::<12>(&ally_ctx).unwrap(), 5);
1405 }
1406
1407 #[test]
1411 fn kill_quest_skips_summoned_death() {
1412 let config = configs::tests_game_config::generate_game_config_for_tests();
1413 let lookups = ContentLookups::default();
1414 let character_state = CharacterState::default();
1415 let quest = QuestInstance {
1416 id: uuid::Uuid::from_u128(99),
1417 current: 5,
1418 reward: vec![],
1419 is_claimed: false,
1420 };
1421 let summoned_id = uuid::Uuid::from_u128(77);
1422 let fight = Some(ActiveFight {
1423 summoned_entity_ids: vec![summoned_id],
1424 ..Default::default()
1425 });
1426
1427 let boss_death = OverlordEvent::EntityDeath {
1429 entity_id: uuid::Uuid::from_u128(1),
1430 reward: vec![],
1431 origin: CombatEventOrigin::Core,
1432 };
1433 let boss_ctx = ConditionalProgressCtx {
1434 event: &boss_death,
1435 character_state: &character_state,
1436 active_fight: &fight,
1437 quest: &quest,
1438 config: &config,
1439 lookups: &lookups,
1440 };
1441 assert_eq!(increment_one(&boss_ctx).unwrap(), 6);
1442 assert_eq!(loop_task_increment_one::<12>(&boss_ctx).unwrap(), 6);
1443
1444 let summoned_death = OverlordEvent::EntityDeath {
1446 entity_id: summoned_id,
1447 reward: vec![],
1448 origin: CombatEventOrigin::Core,
1449 };
1450 let summoned_ctx = ConditionalProgressCtx {
1451 event: &summoned_death,
1452 character_state: &character_state,
1453 active_fight: &fight,
1454 quest: &quest,
1455 config: &config,
1456 lookups: &lookups,
1457 };
1458 assert_eq!(increment_one(&summoned_ctx).unwrap(), 5);
1459 assert_eq!(loop_task_increment_one::<12>(&summoned_ctx).unwrap(), 5);
1460 }
1461}