Skip to main content

tengu_api/state/
mission.rs

1//! Mission (expedition) account. One per Dojo. Supports multiple concurrent missions.
2
3use super::DojosAccount;
4use solana_program::keccak;
5use steel::*;
6
7/// One mission entry. Empty when slots_on_mission == 0.
8#[repr(C)]
9#[derive(Clone, Copy, Debug, PartialEq, Default, bytemuck::Pod, bytemuck::Zeroable)]
10pub struct MissionEntry {
11    /// Bitmask: bit i = 1 if slot i is on this mission.
12    pub slots_on_mission: u64,
13    pub mission_type: u64,
14    pub started_slot: u64,
15    pub duration_slots: u64,
16}
17
18/// Mission account. PDA: ["mission", dojo_pda].
19#[repr(C)]
20#[derive(Clone, Copy, Debug, PartialEq, Default, bytemuck::Pod, bytemuck::Zeroable)]
21pub struct Mission {
22    pub dojo: Pubkey,
23    pub entries: [MissionEntry; crate::consts::MAX_MISSION_ENTRIES],
24    pub buffer: [u64; 4],
25}
26
27account!(DojosAccount, Mission);
28
29impl Mission {
30    /// Slots currently on any active mission (not yet complete).
31    pub fn slots_on_any_active_mission(&self, now: u64) -> u64 {
32        self.entries
33            .iter()
34            .filter(|e| e.slots_on_mission != 0 && now < e.started_slot.saturating_add(e.duration_slots))
35            .fold(0u64, |acc, e| acc | e.slots_on_mission)
36    }
37
38    /// For a slot on mission (active or complete, not yet claimed), returns the mission's started_slot (when mining stopped).
39    /// Mining stays paused until user claims; same pattern as forge cooldown (manual clear).
40    /// Returns None if the slot is not on any mission.
41    pub fn mission_start_slot_for(&self, slot_index: usize, _now: u64) -> Option<u64> {
42        let bit = 1u64 << slot_index;
43        for e in &self.entries {
44            if e.slots_on_mission != 0 && (e.slots_on_mission & bit) != 0 {
45                return Some(e.started_slot);
46            }
47        }
48        None
49    }
50
51    /// Slots required for mission type. None if invalid.
52    pub fn slots_required(mission_type: u64) -> Option<u64> {
53        match mission_type {
54            crate::consts::MISSION_TYPE_SHORT => Some(crate::consts::MISSION_SLOTS_SHORT),
55            crate::consts::MISSION_TYPE_MEDIUM => Some(crate::consts::MISSION_SLOTS_MEDIUM),
56            crate::consts::MISSION_TYPE_LONG => Some(crate::consts::MISSION_SLOTS_LONG),
57            _ => None,
58        }
59    }
60
61    /// Duration in slots for mission type. None if invalid.
62    pub fn duration(mission_type: u64) -> Option<u64> {
63        match mission_type {
64            crate::consts::MISSION_TYPE_SHORT => Some(crate::consts::MISSION_DURATION_SHORT),
65            crate::consts::MISSION_TYPE_MEDIUM => Some(crate::consts::MISSION_DURATION_MEDIUM),
66            crate::consts::MISSION_TYPE_LONG => Some(crate::consts::MISSION_DURATION_LONG),
67            _ => None,
68        }
69    }
70
71    /// DOJO cost (raw, 6 decimals) for mission type. None if invalid.
72    pub fn cost(mission_type: u64) -> Option<u64> {
73        match mission_type {
74            crate::consts::MISSION_TYPE_SHORT => Some(crate::consts::MISSION_COST_SHORT),
75            crate::consts::MISSION_TYPE_MEDIUM => Some(crate::consts::MISSION_COST_MEDIUM),
76            crate::consts::MISSION_TYPE_LONG => Some(crate::consts::MISSION_COST_LONG),
77            _ => None,
78        }
79    }
80
81    /// Popcount of slot mask (number of bits set).
82    pub fn slot_mask_popcount(mask: u64) -> u32 {
83        mask.count_ones()
84    }
85
86    /// Reward caps for mission type. (shards_raw, tickets, free_rolls).
87    pub fn reward_caps(mission_type: u64) -> Option<(u64, u64, u64)> {
88        match mission_type {
89            crate::consts::MISSION_TYPE_SHORT => Some((
90                crate::consts::MISSION_REWARD_SHARDS_SHORT,
91                crate::consts::MISSION_REWARD_TICKETS_SHORT,
92                crate::consts::MISSION_REWARD_FREE_ROLLS_SHORT,
93            )),
94            crate::consts::MISSION_TYPE_MEDIUM => Some((
95                crate::consts::MISSION_REWARD_SHARDS_MEDIUM,
96                crate::consts::MISSION_REWARD_TICKETS_MEDIUM,
97                crate::consts::MISSION_REWARD_FREE_ROLLS_MEDIUM,
98            )),
99            crate::consts::MISSION_TYPE_LONG => Some((
100                crate::consts::MISSION_REWARD_SHARDS_LONG,
101                crate::consts::MISSION_REWARD_TICKETS_LONG,
102                crate::consts::MISSION_REWARD_FREE_ROLLS_LONG,
103            )),
104            _ => None,
105        }
106    }
107
108    /// Three Scrolls: derive (shards, tickets, free_rolls) from seed. If all zero, returns (0, 1, 0).
109    pub fn three_scrolls_rewards(
110        seed: [u8; 32],
111        mission_index: u64,
112        mission_type: u64,
113    ) -> (u64, u64, u64) {
114        let (cap_shards, cap_tickets, cap_rolls) = Self::reward_caps(mission_type).unwrap_or((0, 0, 0));
115
116        let scroll = |buf: &mut [u8; 48], i: u8| {
117            buf[40] = i;
118            let h = keccak::hashv(&[&buf[..41]]).to_bytes();
119            u32::from_le_bytes([h[0], h[1], h[2], h[3]]) as u64
120        };
121
122        let mut buf = [0u8; 48];
123        buf[0..32].copy_from_slice(&seed);
124        buf[32..40].copy_from_slice(&mission_index.to_le_bytes());
125
126        let p = scroll(&mut buf, 0) % 100;
127        let pct_score = if p < 70 {
128            30 * p / 70
129        } else if p < 90 {
130            30 + 40 * (p - 70) / 20
131        } else {
132            70 + 30 * (p - 90) / 10
133        };
134        let shards = cap_shards * pct_score / 100;
135
136        let tickets = scroll(&mut buf, 1) % (cap_tickets + 1);
137        let free_rolls = scroll(&mut buf, 2) % (cap_rolls + 1);
138
139        if shards == 0 && tickets == 0 && free_rolls == 0 {
140            (0, 1, 0)
141        } else {
142            (shards, tickets, free_rolls)
143        }
144    }
145}