1use std::collections::HashMap;
5
6#[derive(Debug, Clone, PartialEq)]
12pub struct UserProfile {
13 pub id: u64,
14 pub display_name: String,
15 pub avatar_seed: u64,
17 pub created_at: u64,
18 pub last_login: u64,
19 pub play_time_seconds: u64,
20 pub preferences: HashMap<String, String>,
21}
22
23impl UserProfile {
24 pub fn new(id: u64, display_name: impl Into<String>, created_at: u64) -> Self {
25 Self {
26 id,
27 display_name: display_name.into(),
28 avatar_seed: id.wrapping_mul(0x9E37_79B9_7F4A_7C15),
29 created_at,
30 last_login: created_at,
31 play_time_seconds: 0,
32 preferences: HashMap::new(),
33 }
34 }
35
36 pub fn set_preference(&mut self, key: impl Into<String>, value: impl Into<String>) {
37 self.preferences.insert(key.into(), value.into());
38 }
39
40 pub fn get_preference(&self, key: &str) -> Option<&str> {
41 self.preferences.get(key).map(|s| s.as_str())
42 }
43
44 pub fn to_bytes(&self) -> Vec<u8> {
46 let mut out = Vec::new();
47 out.extend_from_slice(&self.id.to_le_bytes());
48 let name_bytes = self.display_name.as_bytes();
49 out.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes());
50 out.extend_from_slice(name_bytes);
51 out.extend_from_slice(&self.avatar_seed.to_le_bytes());
52 out.extend_from_slice(&self.created_at.to_le_bytes());
53 out.extend_from_slice(&self.last_login.to_le_bytes());
54 out.extend_from_slice(&self.play_time_seconds.to_le_bytes());
55 out.extend_from_slice(&(self.preferences.len() as u32).to_le_bytes());
56 for (k, v) in &self.preferences {
57 let kb = k.as_bytes();
58 let vb = v.as_bytes();
59 out.extend_from_slice(&(kb.len() as u32).to_le_bytes());
60 out.extend_from_slice(kb);
61 out.extend_from_slice(&(vb.len() as u32).to_le_bytes());
62 out.extend_from_slice(vb);
63 }
64 out
65 }
66
67 pub fn from_bytes(data: &[u8]) -> Result<Self, String> {
69 let mut pos = 0usize;
70
71 macro_rules! read_u32 {
72 () => {{
73 if pos + 4 > data.len() { return Err("truncated u32".into()); }
74 let v = u32::from_le_bytes([data[pos], data[pos+1], data[pos+2], data[pos+3]]);
75 pos += 4;
76 v
77 }};
78 }
79 macro_rules! read_u64 {
80 () => {{
81 if pos + 8 > data.len() { return Err("truncated u64".into()); }
82 let v = u64::from_le_bytes([
83 data[pos], data[pos+1], data[pos+2], data[pos+3],
84 data[pos+4], data[pos+5], data[pos+6], data[pos+7],
85 ]);
86 pos += 8;
87 v
88 }};
89 }
90 macro_rules! read_string {
91 () => {{
92 let len = read_u32!() as usize;
93 if pos + len > data.len() { return Err("truncated string".into()); }
94 let s = std::str::from_utf8(&data[pos..pos+len])
95 .map_err(|e| e.to_string())?.to_owned();
96 pos += len;
97 s
98 }};
99 }
100
101 let id = read_u64!();
102 let display_name = read_string!();
103 let avatar_seed = read_u64!();
104 let created_at = read_u64!();
105 let last_login = read_u64!();
106 let play_time = read_u64!();
107 let pref_count = read_u32!() as usize;
108
109 let mut preferences = HashMap::new();
110 for _ in 0..pref_count {
111 let k = read_string!();
112 let v = read_string!();
113 preferences.insert(k, v);
114 }
115
116 Ok(Self {
117 id,
118 display_name,
119 avatar_seed,
120 created_at,
121 last_login,
122 play_time_seconds: play_time,
123 preferences,
124 })
125 }
126}
127
128pub struct ProfileManager {
134 profiles: Vec<UserProfile>,
135 current_index: Option<usize>,
136 next_id: u64,
137}
138
139impl ProfileManager {
140 pub const MAX_PROFILES: usize = 8;
141
142 pub fn new() -> Self {
143 Self { profiles: Vec::new(), current_index: None, next_id: 1 }
144 }
145
146 pub fn create(&mut self, display_name: impl Into<String>, timestamp: u64) -> Result<u64, String> {
148 if self.profiles.len() >= Self::MAX_PROFILES {
149 return Err(format!("maximum {} profiles reached", Self::MAX_PROFILES));
150 }
151 let id = self.next_id;
152 self.next_id += 1;
153 let profile = UserProfile::new(id, display_name, timestamp);
154 self.profiles.push(profile);
155 if self.current_index.is_none() {
156 self.current_index = Some(0);
157 }
158 Ok(id)
159 }
160
161 pub fn delete(&mut self, id: u64) -> Result<(), String> {
163 let idx = self.profiles.iter().position(|p| p.id == id)
164 .ok_or_else(|| format!("profile {id} not found"))?;
165 self.profiles.remove(idx);
166 match self.current_index {
168 Some(cur) if cur == idx => {
169 self.current_index = if self.profiles.is_empty() { None } else { Some(0) };
170 }
171 Some(cur) if cur > idx => {
172 self.current_index = Some(cur - 1);
173 }
174 _ => {}
175 }
176 Ok(())
177 }
178
179 pub fn switch(&mut self, id: u64) -> Result<(), String> {
181 let idx = self.profiles.iter().position(|p| p.id == id)
182 .ok_or_else(|| format!("profile {id} not found"))?;
183 self.current_index = Some(idx);
184 Ok(())
185 }
186
187 pub fn current(&self) -> Option<&UserProfile> {
189 self.current_index.and_then(|i| self.profiles.get(i))
190 }
191
192 pub fn current_mut(&mut self) -> Option<&mut UserProfile> {
194 self.current_index.and_then(|i| self.profiles.get_mut(i))
195 }
196
197 pub fn list(&self) -> Vec<&UserProfile> {
198 self.profiles.iter().collect()
199 }
200
201 pub fn get_by_id(&self, id: u64) -> Option<&UserProfile> {
202 self.profiles.iter().find(|p| p.id == id)
203 }
204
205 pub fn get_by_id_mut(&mut self, id: u64) -> Option<&mut UserProfile> {
206 self.profiles.iter_mut().find(|p| p.id == id)
207 }
208
209 pub fn count(&self) -> usize {
210 self.profiles.len()
211 }
212
213 pub fn to_bytes(&self) -> Vec<u8> {
215 let mut out = Vec::new();
216 out.extend_from_slice(&(self.profiles.len() as u32).to_le_bytes());
217 out.extend_from_slice(&self.next_id.to_le_bytes());
218 let cur = self.current_index.map(|i| i as u64).unwrap_or(u64::MAX);
219 out.extend_from_slice(&cur.to_le_bytes());
220 for p in &self.profiles {
221 let pb = p.to_bytes();
222 out.extend_from_slice(&(pb.len() as u32).to_le_bytes());
223 out.extend_from_slice(&pb);
224 }
225 out
226 }
227
228 pub fn from_bytes(data: &[u8]) -> Result<Self, String> {
230 if data.len() < 20 {
231 return Err("ProfileManager bytes too short".into());
232 }
233 let count = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
234 let next_id = u64::from_le_bytes([
235 data[4], data[5], data[6], data[7],
236 data[8], data[9], data[10], data[11],
237 ]);
238 let cur_raw = u64::from_le_bytes([
239 data[12], data[13], data[14], data[15],
240 data[16], data[17], data[18], data[19],
241 ]);
242 let current_index = if cur_raw == u64::MAX { None } else { Some(cur_raw as usize) };
243
244 let mut pos = 20usize;
245 let mut profiles = Vec::with_capacity(count);
246 for _ in 0..count {
247 if pos + 4 > data.len() { return Err("truncated profile length".into()); }
248 let len = u32::from_le_bytes([data[pos], data[pos+1], data[pos+2], data[pos+3]]) as usize;
249 pos += 4;
250 if pos + len > data.len() { return Err("truncated profile data".into()); }
251 let p = UserProfile::from_bytes(&data[pos..pos+len])?;
252 profiles.push(p);
253 pos += len;
254 }
255
256 Ok(Self { profiles, current_index, next_id })
257 }
258}
259
260impl Default for ProfileManager {
261 fn default() -> Self {
262 Self::new()
263 }
264}
265
266#[derive(Debug, Clone, Default)]
272pub struct StatisticsRecord {
273 pub enemies_killed: HashMap<String, u32>,
274 pub damage_dealt: u64,
275 pub damage_received: u64,
276 pub distance_traveled: f64,
277 pub items_collected: u64,
278 pub deaths: u32,
279 pub highest_combo: u32,
280 pub max_damage_hit: u64,
281 pub areas_visited: Vec<String>,
282}
283
284impl StatisticsRecord {
285 pub fn new() -> Self {
286 Self::default()
287 }
288
289 pub fn record_kill(&mut self, enemy_type: impl Into<String>) {
290 *self.enemies_killed.entry(enemy_type.into()).or_insert(0) += 1;
291 }
292
293 pub fn record_damage_dealt(&mut self, amount: u64) {
294 self.damage_dealt += amount;
295 if amount > self.max_damage_hit {
296 self.max_damage_hit = amount;
297 }
298 }
299
300 pub fn record_damage_received(&mut self, amount: u64) {
301 self.damage_received += amount;
302 }
303
304 pub fn record_travel(&mut self, distance: f64) {
305 self.distance_traveled += distance;
306 }
307
308 pub fn collect_item(&mut self) {
309 self.items_collected += 1;
310 }
311
312 pub fn record_death(&mut self) {
313 self.deaths += 1;
314 }
315
316 pub fn record_combo(&mut self, combo: u32) {
317 if combo > self.highest_combo {
318 self.highest_combo = combo;
319 }
320 }
321
322 pub fn visit_area(&mut self, area: impl Into<String>) {
323 let area = area.into();
324 if !self.areas_visited.contains(&area) {
325 self.areas_visited.push(area);
326 }
327 }
328
329 pub fn total_kills(&self) -> u32 {
330 self.enemies_killed.values().sum()
331 }
332}
333
334#[derive(Debug, Clone, PartialEq)]
340pub struct LeaderboardEntry {
341 pub score: u64,
342 pub player_name: String,
343 pub metadata: HashMap<String, String>,
344}
345
346pub struct Leaderboard {
348 pub name: String,
349 entries: Vec<LeaderboardEntry>,
350 max_entries: usize,
351}
352
353impl Leaderboard {
354 pub fn new(name: impl Into<String>, max_entries: usize) -> Self {
355 Self { name: name.into(), entries: Vec::new(), max_entries }
356 }
357
358 pub fn submit(&mut self, score: u64, player_name: impl Into<String>, metadata: HashMap<String, String>) {
361 self.entries.push(LeaderboardEntry { score, player_name: player_name.into(), metadata });
362 self.entries.sort_by(|a, b| b.score.cmp(&a.score));
363 self.entries.truncate(self.max_entries);
364 }
365
366 pub fn get_top(&self, n: usize) -> &[LeaderboardEntry] {
367 let end = n.min(self.entries.len());
368 &self.entries[..end]
369 }
370
371 pub fn rank_of(&self, score: u64) -> usize {
373 self.entries.iter().position(|e| e.score == score)
374 .map(|i| i + 1)
375 .unwrap_or(self.entries.len() + 1)
376 }
377
378 pub fn percentile_of(&self, score: u64) -> f32 {
380 if self.entries.is_empty() { return 1.0; }
381 let beaten = self.entries.iter().filter(|e| e.score < score).count();
382 beaten as f32 / self.entries.len() as f32
383 }
384
385 pub fn len(&self) -> usize {
386 self.entries.len()
387 }
388
389 pub fn is_empty(&self) -> bool {
390 self.entries.is_empty()
391 }
392}
393
394#[derive(Debug, Clone, PartialEq)]
400pub enum UnlockCondition {
401 StatThreshold { stat: String, threshold: u64 },
402 AchievementCompleted(String),
403 ItemCollected(String),
404 LevelReached(u32),
405 Manual,
406}
407
408#[derive(Debug, Clone)]
410pub struct Unlockable {
411 pub id: String,
412 pub name: String,
413 pub unlock_condition: UnlockCondition,
414 pub unlocked: bool,
415 pub hidden_until_unlocked: bool,
417}
418
419impl Unlockable {
420 pub fn new(
421 id: impl Into<String>,
422 name: impl Into<String>,
423 condition: UnlockCondition,
424 hidden: bool,
425 ) -> Self {
426 Self {
427 id: id.into(),
428 name: name.into(),
429 unlock_condition: condition,
430 unlocked: false,
431 hidden_until_unlocked: hidden,
432 }
433 }
434
435 pub fn unlock(&mut self) {
436 self.unlocked = true;
437 }
438
439 pub fn is_visible(&self) -> bool {
440 self.unlocked || !self.hidden_until_unlocked
441 }
442}
443
444pub struct UnlockRegistry {
446 items: HashMap<String, Unlockable>,
447}
448
449impl UnlockRegistry {
450 pub fn new() -> Self {
451 Self { items: HashMap::new() }
452 }
453
454 pub fn register(&mut self, item: Unlockable) {
455 self.items.insert(item.id.clone(), item);
456 }
457
458 pub fn unlock(&mut self, id: &str) -> bool {
459 if let Some(item) = self.items.get_mut(id) {
460 item.unlock();
461 true
462 } else {
463 false
464 }
465 }
466
467 pub fn is_unlocked(&self, id: &str) -> bool {
468 self.items.get(id).map_or(false, |i| i.unlocked)
469 }
470
471 pub fn check_and_unlock(&mut self, stats: &StatisticsRecord, player_level: u32) -> Vec<String> {
473 let mut newly_unlocked = Vec::new();
474 for item in self.items.values_mut() {
475 if item.unlocked { continue; }
476 let should_unlock = match &item.unlock_condition {
477 UnlockCondition::StatThreshold { stat, threshold } => {
478 match stat.as_str() {
479 "kills" => stats.total_kills() as u64 >= *threshold,
480 "deaths" => stats.deaths as u64 >= *threshold,
481 "damage_dealt" => stats.damage_dealt >= *threshold,
482 "items_collected" => stats.items_collected >= *threshold,
483 _ => false,
484 }
485 }
486 UnlockCondition::LevelReached(lvl) => player_level >= *lvl,
487 UnlockCondition::Manual => false,
488 UnlockCondition::AchievementCompleted(_) => false,
489 UnlockCondition::ItemCollected(_) => false,
490 };
491 if should_unlock {
492 item.unlocked = true;
493 newly_unlocked.push(item.id.clone());
494 }
495 }
496 newly_unlocked
497 }
498
499 pub fn visible_items(&self) -> Vec<&Unlockable> {
500 self.items.values().filter(|i| i.is_visible()).collect()
501 }
502
503 pub fn all_items(&self) -> Vec<&Unlockable> {
504 self.items.values().collect()
505 }
506}
507
508impl Default for UnlockRegistry {
509 fn default() -> Self {
510 Self::new()
511 }
512}
513
514const MIN_LEVEL_FOR_PRESTIGE: u32 = 50;
519const PRESTIGE_CURRENCY_PER_LEVEL: u64 = 10;
520
521pub struct PrestigeSystem {
523 pub prestige_level: u32,
524 pub prestige_currency: u64,
525 pub lifetime_currency: u64,
527 pub current_player_level: u32,
528}
529
530impl PrestigeSystem {
531 pub fn new() -> Self {
532 Self {
533 prestige_level: 0,
534 prestige_currency: 0,
535 lifetime_currency: 0,
536 current_player_level: 1,
537 }
538 }
539
540 pub fn can_prestige(&self) -> bool {
542 self.current_player_level >= MIN_LEVEL_FOR_PRESTIGE
543 }
544
545 pub fn prestige(&mut self) -> Result<u64, String> {
549 if !self.can_prestige() {
550 return Err(format!(
551 "must reach level {} to prestige (current: {})",
552 MIN_LEVEL_FOR_PRESTIGE, self.current_player_level
553 ));
554 }
555 let earned = self.current_player_level as u64 * PRESTIGE_CURRENCY_PER_LEVEL;
556 self.prestige_level += 1;
557 self.prestige_currency += earned;
558 self.lifetime_currency += earned;
559 self.current_player_level = 1; Ok(earned)
561 }
562
563 pub fn get_bonus_multiplier(&self) -> f32 {
565 1.0 + (self.prestige_level as f32) * 0.1
566 }
567
568 pub fn spend_currency(&mut self, amount: u64) -> Result<(), String> {
570 if self.prestige_currency < amount {
571 return Err(format!(
572 "insufficient prestige currency: have {}, need {}",
573 self.prestige_currency, amount
574 ));
575 }
576 self.prestige_currency -= amount;
577 Ok(())
578 }
579
580 pub fn set_player_level(&mut self, level: u32) {
581 self.current_player_level = level;
582 }
583}
584
585impl Default for PrestigeSystem {
586 fn default() -> Self {
587 Self::new()
588 }
589}
590
591#[cfg(test)]
596mod tests {
597 use super::*;
598
599 #[test]
602 fn test_profile_serialization_roundtrip() {
603 let mut p = UserProfile::new(1, "Hero", 1000);
604 p.set_preference("theme", "dark");
605 let bytes = p.to_bytes();
606 let restored = UserProfile::from_bytes(&bytes).unwrap();
607 assert_eq!(restored.id, p.id);
608 assert_eq!(restored.display_name, p.display_name);
609 assert_eq!(restored.get_preference("theme"), Some("dark"));
610 }
611
612 #[test]
613 fn test_profile_manager_create_and_list() {
614 let mut mgr = ProfileManager::new();
615 mgr.create("Alice", 100).unwrap();
616 mgr.create("Bob", 200).unwrap();
617 assert_eq!(mgr.count(), 2);
618 let names: Vec<&str> = mgr.list().iter().map(|p| p.display_name.as_str()).collect();
619 assert!(names.contains(&"Alice"));
620 assert!(names.contains(&"Bob"));
621 }
622
623 #[test]
624 fn test_profile_manager_max_8() {
625 let mut mgr = ProfileManager::new();
626 for i in 0..8 {
627 mgr.create(format!("P{i}"), i as u64).unwrap();
628 }
629 let result = mgr.create("Extra", 999);
630 assert!(result.is_err());
631 }
632
633 #[test]
634 fn test_profile_manager_switch_and_delete() {
635 let mut mgr = ProfileManager::new();
636 let id1 = mgr.create("Alice", 1).unwrap();
637 let id2 = mgr.create("Bob", 2).unwrap();
638 mgr.switch(id2).unwrap();
639 assert_eq!(mgr.current().unwrap().display_name, "Bob");
640 mgr.delete(id2).unwrap();
641 assert_eq!(mgr.count(), 1);
642 let _ = mgr.switch(id1);
643 assert_eq!(mgr.current().unwrap().display_name, "Alice");
644 }
645
646 #[test]
647 fn test_profile_manager_serialization() {
648 let mut mgr = ProfileManager::new();
649 mgr.create("Alice", 1).unwrap();
650 mgr.create("Bob", 2).unwrap();
651 let bytes = mgr.to_bytes();
652 let restored = ProfileManager::from_bytes(&bytes).unwrap();
653 assert_eq!(restored.count(), 2);
654 }
655
656 #[test]
659 fn test_statistics_record() {
660 let mut stats = StatisticsRecord::new();
661 stats.record_kill("goblin");
662 stats.record_kill("goblin");
663 stats.record_kill("orc");
664 assert_eq!(stats.total_kills(), 3);
665 stats.record_damage_dealt(500);
666 stats.record_damage_dealt(1000);
667 assert_eq!(stats.max_damage_hit, 1000);
668 stats.record_travel(100.0);
669 stats.record_travel(50.0);
670 assert!((stats.distance_traveled - 150.0).abs() < 1e-6);
671 stats.visit_area("Forest");
672 stats.visit_area("Forest"); assert_eq!(stats.areas_visited.len(), 1);
674 }
675
676 #[test]
679 fn test_leaderboard_submit_and_rank() {
680 let mut lb = Leaderboard::new("level_1", 5);
681 lb.submit(1000, "Alice", HashMap::new());
682 lb.submit(2000, "Bob", HashMap::new());
683 lb.submit(500, "Carol", HashMap::new());
684 assert_eq!(lb.get_top(3)[0].score, 2000);
685 assert_eq!(lb.rank_of(2000), 1);
686 assert_eq!(lb.rank_of(1000), 2);
687 }
688
689 #[test]
690 fn test_leaderboard_max_entries() {
691 let mut lb = Leaderboard::new("test", 3);
692 for s in [100u64, 200, 300, 400, 500] {
693 lb.submit(s, "p", HashMap::new());
694 }
695 assert_eq!(lb.len(), 3);
696 assert_eq!(lb.get_top(1)[0].score, 500);
697 }
698
699 #[test]
700 fn test_leaderboard_percentile() {
701 let mut lb = Leaderboard::new("test", 10);
702 for s in [100u64, 200, 300, 400] {
703 lb.submit(s, "p", HashMap::new());
704 }
705 let pct = lb.percentile_of(300);
707 assert!((pct - 0.5).abs() < 1e-5, "got {pct}");
708 }
709
710 #[test]
713 fn test_unlock_registry_manual() {
714 let mut reg = UnlockRegistry::new();
715 reg.register(Unlockable::new("item_a", "Item A", UnlockCondition::Manual, false));
716 assert!(!reg.is_unlocked("item_a"));
717 reg.unlock("item_a");
718 assert!(reg.is_unlocked("item_a"));
719 }
720
721 #[test]
722 fn test_unlock_auto_by_kills() {
723 let mut reg = UnlockRegistry::new();
724 reg.register(Unlockable::new(
725 "killer",
726 "Killer Badge",
727 UnlockCondition::StatThreshold { stat: "kills".into(), threshold: 5 },
728 false,
729 ));
730 let mut stats = StatisticsRecord::new();
731 for _ in 0..5 { stats.record_kill("goblin"); }
732 let newly = reg.check_and_unlock(&stats, 1);
733 assert!(newly.contains(&"killer".to_string()));
734 assert!(reg.is_unlocked("killer"));
735 }
736
737 #[test]
740 fn test_prestige_system_cannot_prestige_early() {
741 let mut ps = PrestigeSystem::new();
742 ps.set_player_level(10);
743 assert!(!ps.can_prestige());
744 assert!(ps.prestige().is_err());
745 }
746
747 #[test]
748 fn test_prestige_system_prestige() {
749 let mut ps = PrestigeSystem::new();
750 ps.set_player_level(50);
751 assert!(ps.can_prestige());
752 let earned = ps.prestige().unwrap();
753 assert_eq!(earned, 50 * 10);
754 assert_eq!(ps.prestige_level, 1);
755 assert_eq!(ps.current_player_level, 1);
756 }
757
758 #[test]
759 fn test_prestige_bonus_multiplier() {
760 let mut ps = PrestigeSystem::new();
761 ps.set_player_level(50);
762 assert!((ps.get_bonus_multiplier() - 1.0).abs() < 1e-6);
763 ps.prestige().unwrap();
764 assert!((ps.get_bonus_multiplier() - 1.1).abs() < 1e-5);
765 }
766
767 #[test]
768 fn test_prestige_spend_currency() {
769 let mut ps = PrestigeSystem::new();
770 ps.set_player_level(50);
771 ps.prestige().unwrap();
772 let available = ps.prestige_currency;
773 assert!(ps.spend_currency(available / 2).is_ok());
774 assert!(ps.spend_currency(available).is_err());
775 }
776}