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