Skip to main content

tengu_api/state/
dojo.rs

1use super::DojosAccount;
2use crate::error::DojosError;
3use solana_program::program_error::ProgramError;
4use steel::*;
5
6/// Counts per (collection_index, prestige_level). 25 × 6 = 150.
7pub const FODDER_PRESTIGE_LEN: usize = 25 * crate::consts::PRESTIGE_LEVELS;
8
9/// Per-prestige fodder counts: [u64; 150]. Split into 128+22 so bytemuck derives Pod/Zeroable.
10#[repr(C)]
11#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
12pub struct FodderPrestigeCounts {
13    pub head: [u64; 128],
14    pub tail: [u64; 22],
15}
16
17impl Default for FodderPrestigeCounts {
18    fn default() -> Self {
19        Self { head: [0; 128], tail: [0; 22] }
20    }
21}
22
23impl FodderPrestigeCounts {
24    #[inline]
25    pub fn get(&self, i: usize) -> u64 {
26        *self.head.get(i).or_else(|| self.tail.get(i.wrapping_sub(128))).unwrap_or(&0)
27    }
28    #[inline]
29    pub fn get_mut(&mut self, i: usize) -> Option<&mut u64> {
30        if i < 128 {
31            self.head.get_mut(i)
32        } else {
33            self.tail.get_mut(i - 128)
34        }
35    }
36    #[inline]
37    pub fn slice(&self, start: usize, len: usize) -> impl Iterator<Item = u64> + '_ {
38        (start..start + len).map(|i| self.get(i))
39    }
40}
41
42#[repr(C)]
43#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
44pub struct Dojo {
45    pub owner: Pubkey,
46    /// PvP battles initiated (stats / future tasks).
47    pub battles_count: u64,
48    /// Refined ore buffer (same raw decimals as $DOJO) during `claim_shards`; minted to wallet in the same ix.
49    /// Dine / level / etc. burn wallet DOJO, not this field.
50    pub shard_balance: u64,
51    pub last_ore_claim_slot: u64,
52    pub referrer: Pubkey, // Referrer's Dojo PDA (who referred this player)
53    pub recruitment_ticket_balance: u64,
54    pub recruited_count: u64,
55    pub dine_count: u64,
56    /// Pity counter: pulls since last SR+. At 10, next pull is forced SR+. Resets on SR+.
57    pub pity_counter: u64,
58    pub flash_sale_count_today: u64,
59    pub last_flash_sale_reset_slot: u64,
60    pub amethyst_balance: u64,
61    pub active_scene_id: u64,
62    /// Per-prestige fodder counts: (collection_index * PRESTIGE_LEVELS + prestige - 1). Prestige 1–6.
63    /// collection_index = element×5+rarity (0–24). Recruit/merge add; seat/replace/prestige consume.
64    pub fodder_counts_prestige: FodderPrestigeCounts,
65    /// Reward points (XP): stake accrual + PvP wins + recruit + forge/barracks upgrades + first dine per slot, etc.
66    /// Burned when claiming SOL from `xp_reward_pool` (`claim_xp_rewards`).
67    pub xp: u64,
68}
69
70account!(DojosAccount, Dojo);
71
72impl Dojo {
73    /// Index for collection (element×5+rarity) at prestige level (1–6).
74    #[inline]
75    pub fn fodder_prestige_index(collection_index: usize, prestige: u64) -> usize {
76        let p = prestige.max(1).min(crate::consts::PRESTIGE_LEVELS as u64);
77        collection_index * crate::consts::PRESTIGE_LEVELS + (p - 1) as usize
78    }
79
80    /// Get fodder count at (collection_index, prestige).
81    #[inline]
82    pub fn fodder_prestige_get(&self, collection_index: usize, prestige: u64) -> u64 {
83        if collection_index >= 25 {
84            return 0;
85        }
86        let i = Self::fodder_prestige_index(collection_index, prestige);
87        self.fodder_counts_prestige.get(i)
88    }
89
90    /// Add to fodder at (collection_index, prestige).
91    #[inline]
92    pub fn fodder_prestige_add(&mut self, collection_index: usize, prestige: u64, amount: u64) {
93        if collection_index < 25 {
94            let i = Self::fodder_prestige_index(collection_index, prestige);
95            if let Some(c) = self.fodder_counts_prestige.get_mut(i) {
96                *c = c.saturating_add(amount);
97            }
98        }
99    }
100
101    /// Subtract from fodder at (collection_index, prestige). Returns false if insufficient.
102    #[inline]
103    pub fn fodder_prestige_sub(&mut self, collection_index: usize, prestige: u64, amount: u64) -> bool {
104        if collection_index >= 25 {
105            return false;
106        }
107        let i = Self::fodder_prestige_index(collection_index, prestige);
108        if let Some(c) = self.fodder_counts_prestige.get_mut(i) {
109            if *c >= amount {
110                *c -= amount;
111                return true;
112            }
113        }
114        false
115    }
116
117    /// Total fodder for collection across all prestige levels.
118    #[inline]
119    pub fn fodder_total_for_collection(&self, collection_index: usize) -> u64 {
120        if collection_index >= 25 {
121            return 0;
122        }
123        let base = collection_index * crate::consts::PRESTIGE_LEVELS;
124        self.fodder_counts_prestige.slice(base, crate::consts::PRESTIGE_LEVELS).sum()
125    }
126
127    /// Total fodder across all collections (for recruit cap check).
128    #[inline]
129    pub fn fodder_total(&self) -> u64 {
130        (0..25).map(|ci| self.fodder_total_for_collection(ci)).sum()
131    }
132
133    /// Consume `amount` from collection ci, taking from lowest prestige first. Returns false if insufficient.
134    #[inline]
135    pub fn fodder_prestige_consume_from_collection(&mut self, collection_index: usize, amount: u64) -> bool {
136        if collection_index >= 25 || self.fodder_total_for_collection(collection_index) < amount {
137            return false;
138        }
139        let mut take_rem = amount;
140        for p in 1..=crate::consts::PRESTIGE_LEVELS as u64 {
141            if take_rem == 0 {
142                break;
143            }
144            let have = self.fodder_prestige_get(collection_index, p);
145            let sub = take_rem.min(have);
146            if sub > 0 {
147                self.fodder_prestige_sub(collection_index, p, sub);
148                take_rem -= sub;
149            }
150        }
151        take_rem == 0
152    }
153
154    pub fn assert_owner(&self, signer: &Pubkey) -> Result<(), ProgramError> {
155        if self.owner != *signer {
156            return Err(ProgramError::IllegalOwner);
157        }
158        Ok(())
159    }
160
161    pub fn add_amethyst(&mut self, amount: u64) {
162        self.amethyst_balance = self.amethyst_balance.saturating_add(amount);
163    }
164
165    pub fn sub_amethyst(&mut self, amount: u64) -> Result<(), ProgramError> {
166        if self.amethyst_balance < amount {
167            return Err(ProgramError::InsufficientFunds);
168        }
169        self.amethyst_balance -= amount;
170        Ok(())
171    }
172
173    /// Spend in-game shards (dine, upgrades, rolls, etc.).
174    #[inline]
175    pub fn sub_shard_balance(&mut self, amount: u64) -> Result<(), ProgramError> {
176        if self.shard_balance < amount {
177            return Err(DojosError::InsufficientShards.into());
178        }
179        self.shard_balance -= amount;
180        Ok(())
181    }
182}