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    /// Slots required for mission type. None if invalid.
39    pub fn slots_required(mission_type: u64) -> Option<u64> {
40        match mission_type {
41            crate::consts::MISSION_TYPE_SHORT => Some(crate::consts::MISSION_SLOTS_SHORT),
42            crate::consts::MISSION_TYPE_MEDIUM => Some(crate::consts::MISSION_SLOTS_MEDIUM),
43            crate::consts::MISSION_TYPE_LONG => Some(crate::consts::MISSION_SLOTS_LONG),
44            _ => None,
45        }
46    }
47
48    /// Duration in slots for mission type. None if invalid.
49    pub fn duration(mission_type: u64) -> Option<u64> {
50        match mission_type {
51            crate::consts::MISSION_TYPE_SHORT => Some(crate::consts::MISSION_DURATION_SHORT),
52            crate::consts::MISSION_TYPE_MEDIUM => Some(crate::consts::MISSION_DURATION_MEDIUM),
53            crate::consts::MISSION_TYPE_LONG => Some(crate::consts::MISSION_DURATION_LONG),
54            _ => None,
55        }
56    }
57
58    /// DOJO cost (raw, 6 decimals) for mission type. None if invalid.
59    pub fn cost(mission_type: u64) -> Option<u64> {
60        match mission_type {
61            crate::consts::MISSION_TYPE_SHORT => Some(crate::consts::MISSION_COST_SHORT),
62            crate::consts::MISSION_TYPE_MEDIUM => Some(crate::consts::MISSION_COST_MEDIUM),
63            crate::consts::MISSION_TYPE_LONG => Some(crate::consts::MISSION_COST_LONG),
64            _ => None,
65        }
66    }
67
68    /// Popcount of slot mask (number of bits set).
69    pub fn slot_mask_popcount(mask: u64) -> u32 {
70        mask.count_ones()
71    }
72
73    /// Reward caps for mission type. (shards_raw, tickets, free_rolls).
74    pub fn reward_caps(mission_type: u64) -> Option<(u64, u64, u64)> {
75        match mission_type {
76            crate::consts::MISSION_TYPE_SHORT => Some((
77                crate::consts::MISSION_REWARD_SHARDS_SHORT,
78                crate::consts::MISSION_REWARD_TICKETS_SHORT,
79                crate::consts::MISSION_REWARD_FREE_ROLLS_SHORT,
80            )),
81            crate::consts::MISSION_TYPE_MEDIUM => Some((
82                crate::consts::MISSION_REWARD_SHARDS_MEDIUM,
83                crate::consts::MISSION_REWARD_TICKETS_MEDIUM,
84                crate::consts::MISSION_REWARD_FREE_ROLLS_MEDIUM,
85            )),
86            crate::consts::MISSION_TYPE_LONG => Some((
87                crate::consts::MISSION_REWARD_SHARDS_LONG,
88                crate::consts::MISSION_REWARD_TICKETS_LONG,
89                crate::consts::MISSION_REWARD_FREE_ROLLS_LONG,
90            )),
91            _ => None,
92        }
93    }
94
95    /// Three Scrolls: derive (shards, tickets, free_rolls) from seed. If all zero, returns (0, 1, 0).
96    pub fn three_scrolls_rewards(
97        seed: [u8; 32],
98        mission_index: u64,
99        mission_type: u64,
100    ) -> (u64, u64, u64) {
101        let (cap_shards, cap_tickets, cap_rolls) = Self::reward_caps(mission_type).unwrap_or((0, 0, 0));
102
103        let scroll = |buf: &mut [u8; 48], i: u8| {
104            buf[40] = i;
105            let h = keccak::hashv(&[&buf[..41]]).to_bytes();
106            u32::from_le_bytes([h[0], h[1], h[2], h[3]]) as u64
107        };
108
109        let mut buf = [0u8; 48];
110        buf[0..32].copy_from_slice(&seed);
111        buf[32..40].copy_from_slice(&mission_index.to_le_bytes());
112
113        let p = scroll(&mut buf, 0) % 100;
114        let pct_score = if p < 70 {
115            30 * p / 70
116        } else if p < 90 {
117            30 + 40 * (p - 70) / 20
118        } else {
119            70 + 30 * (p - 90) / 10
120        };
121        let shards = cap_shards * pct_score / 100;
122
123        let tickets = scroll(&mut buf, 1) % (cap_tickets + 1);
124        let free_rolls = scroll(&mut buf, 2) % (cap_rolls + 1);
125
126        if shards == 0 && tickets == 0 && free_rolls == 0 {
127            (0, 1, 0)
128        } else {
129            (shards, tickets, free_rolls)
130        }
131    }
132}