use std::collections::HashMap; use std::sync::Arc; use std::time::Instant; use tokio::sync::Mutex; use russh::server::Handle; use russh::ChannelId; use crate::db::{GameDb, SavedPlayer}; use crate::world::{Attitude, Class, Object, Race, World}; #[derive(Clone)] pub struct PlayerStats { pub max_hp: i32, pub hp: i32, pub attack: i32, pub defense: i32, pub level: i32, pub xp: i32, pub xp_to_next: i32, pub max_mana: i32, pub mana: i32, pub max_endurance: i32, pub endurance: i32, } pub struct Player { pub name: String, pub race_id: String, pub class_id: String, pub room_id: String, pub stats: PlayerStats, pub inventory: Vec, pub equipped: HashMap, pub guilds: HashMap, pub cooldowns: HashMap, pub is_admin: bool, } impl Player { pub fn equipped_in_slot(&self, slot: &str) -> Option<&Object> { self.equipped.get(slot) } pub fn total_equipped_damage(&self) -> i32 { self.equipped.values() .filter_map(|o| o.stats.damage) .sum() } pub fn total_equipped_armor(&self) -> i32 { self.equipped.values() .filter_map(|o| o.stats.armor) .sum() } pub fn effective_attack(&self, world: &World) -> i32 { let race_natural = world.races.iter() .find(|r| r.id == self.race_id) .map(|r| r.natural_attacks.iter().map(|a| a.damage).max().unwrap_or(0)) .unwrap_or(0); let weapon_bonus = self.total_equipped_damage(); let unarmed_or_weapon = weapon_bonus.max(race_natural); self.stats.attack + unarmed_or_weapon } pub fn effective_defense(&self, world: &World) -> i32 { let race_natural = world.races.iter() .find(|r| r.id == self.race_id) .map(|r| r.natural_armor) .unwrap_or(0); self.stats.defense + self.total_equipped_armor() + race_natural } } #[derive(Debug, Clone)] pub enum CombatAction { Attack, Defend, Flee, UseItem(usize), Cast(String), } pub struct CombatState { pub npc_id: String, pub action: Option, pub defending: bool, } pub struct NpcInstance { pub hp: i32, pub alive: bool, pub death_time: Option, pub race_id: String, pub class_id: String, } pub struct PlayerConnection { pub player: Player, pub channel: ChannelId, pub handle: Handle, pub combat: Option, } pub struct XorShift64 { state: u64, } impl XorShift64 { pub fn new(seed: u64) -> Self { XorShift64 { state: if seed == 0 { 0xdeadbeefcafe1234 } else { seed }, } } pub fn next(&mut self) -> u64 { let mut x = self.state; x ^= x << 13; x ^= x >> 7; x ^= x << 17; self.state = x; x } pub fn next_range(&mut self, min: i32, max: i32) -> i32 { if min >= max { return min; } let range = (max - min + 1) as u64; (self.next() % range) as i32 + min } } pub struct GameState { pub world: World, pub db: Arc, pub players: HashMap, pub npc_instances: HashMap, pub rng: XorShift64, pub tick_count: u64, } pub type SharedState = Arc>; pub fn resolve_npc_race_class( fixed_race: &Option, fixed_class: &Option, world: &World, rng: &mut XorShift64, ) -> (String, String) { let race_id = match fixed_race { Some(rid) if world.races.iter().any(|r| r.id == *rid) => rid.clone(), _ => { // Pick a random non-hidden race let candidates: Vec<&Race> = world.races.iter().filter(|r| !r.hidden).collect(); if candidates.is_empty() { world.races.first().map(|r| r.id.clone()).unwrap_or_default() } else { let idx = rng.next_range(0, candidates.len() as i32) as usize; candidates[idx].id.clone() } } }; let class_id = match fixed_class { Some(cid) if world.classes.iter().any(|c| c.id == *cid) => cid.clone(), _ => { let race = world.races.iter().find(|r| r.id == race_id); // Try race default_class first if let Some(ref dc) = race.and_then(|r| r.default_class.clone()) { if world.classes.iter().any(|c| c.id == *dc) { return (race_id, dc.clone()); } } // No default → pick random non-hidden class compatible with race let restricted = race .map(|r| &r.guild_compatibility.restricted) .cloned() .unwrap_or_default(); let candidates: Vec<&Class> = world.classes.iter() .filter(|c| !c.hidden) .filter(|c| { c.guild.as_ref().map(|gid| !restricted.contains(gid)).unwrap_or(true) }) .collect(); if candidates.is_empty() { world.classes.first().map(|c| c.id.clone()).unwrap_or_default() } else { let idx = rng.next_range(0, candidates.len() as i32) as usize; candidates[idx].id.clone() } } }; (race_id, class_id) } impl GameState { pub fn new(world: World, db: Arc) -> Self { let seed = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default() .as_nanos() as u64; let mut rng = XorShift64::new(seed); let mut npc_instances = HashMap::new(); for npc in world.npcs.values() { let (race_id, class_id) = resolve_npc_race_class( &npc.fixed_race, &npc.fixed_class, &world, &mut rng, ); let hp = npc.combat.as_ref().map(|c| c.max_hp).unwrap_or(20); npc_instances.insert( npc.id.clone(), NpcInstance { hp, alive: true, death_time: None, race_id, class_id }, ); } GameState { world, db, players: HashMap::new(), npc_instances, rng, tick_count: 0, } } pub fn spawn_room(&self) -> &str { &self.world.spawn_room } pub fn is_registration_open(&self) -> bool { self.db .get_setting("registration_open") .map(|v| v != "false") .unwrap_or(true) } pub fn npc_attitude_toward(&self, npc_id: &str, player_name: &str) -> Attitude { if let Some(val) = self.db.get_attitude(player_name, npc_id) { return Attitude::from_value(val); } self.world .get_npc(npc_id) .map(|n| n.base_attitude) .unwrap_or(Attitude::Neutral) } pub fn npc_attitude_value(&self, npc_id: &str, player_name: &str) -> i32 { if let Some(val) = self.db.get_attitude(player_name, npc_id) { return val; } self.world .get_npc(npc_id) .map(|n| n.base_attitude.default_value()) .unwrap_or(0) } pub fn shift_attitude(&self, npc_id: &str, player_name: &str, delta: i32) { let current = self.npc_attitude_value(npc_id, player_name); let new_val = (current + delta).clamp(-100, 100); self.db.save_attitude(player_name, npc_id, new_val); } pub fn shift_faction_attitude(&self, faction: &str, player_name: &str, delta: i32) { for npc in self.world.npcs.values() { if npc.faction.as_deref() == Some(faction) { self.shift_attitude(&npc.id, player_name, delta); } } } pub fn create_new_player( &mut self, id: usize, name: String, race_id: String, class_id: String, channel: ChannelId, handle: Handle, ) { let room_id = self.world.spawn_room.clone(); let race = self.world.races.iter().find(|r| r.id == race_id); let class = self.world.classes.iter().find(|c| c.id == class_id); let (base_hp, base_atk, base_def) = match class { Some(c) => (c.base_stats.max_hp, c.base_stats.attack, c.base_stats.defense), None => (100, 10, 10), }; let (con_mod, str_mod, dex_mod) = match race { Some(r) => (r.stats.constitution, r.stats.strength, r.stats.dexterity), None => (0, 0, 0), }; let max_hp = base_hp + con_mod * 5; let attack = base_atk + str_mod + dex_mod / 2; let defense = base_def + con_mod / 2; // Compute starting mana/endurance from initial guild let initial_guild_id = class.and_then(|c| c.guild.clone()); let (base_mana, base_endurance) = initial_guild_id.as_ref() .and_then(|gid| self.world.guilds.get(gid)) .map(|g| (g.base_mana, g.base_endurance)) .unwrap_or((0, 0)); let int_mod = race.map(|r| r.stats.intelligence).unwrap_or(0); let wis_mod = race.map(|r| r.stats.wisdom).unwrap_or(0); let max_mana = base_mana + int_mod * 5 + wis_mod * 3; let max_endurance = base_endurance + con_mod * 3 + str_mod * 2; let mut guilds = HashMap::new(); if let Some(ref gid) = initial_guild_id { if self.world.guilds.contains_key(gid) { guilds.insert(gid.clone(), 1); self.db.save_guild_membership(&name, gid, 1); } } let stats = PlayerStats { max_hp, hp: max_hp, attack, defense, level: 1, xp: 0, xp_to_next: 100, max_mana, mana: max_mana, max_endurance, endurance: max_endurance, }; self.players.insert( id, PlayerConnection { player: Player { name, race_id, class_id, room_id, stats, inventory: Vec::new(), equipped: HashMap::new(), guilds, cooldowns: HashMap::new(), is_admin: false, }, channel, handle, combat: None, }, ); } pub fn load_existing_player( &mut self, id: usize, saved: SavedPlayer, channel: ChannelId, handle: Handle, ) { let inventory: Vec = serde_json::from_str(&saved.inventory_json).unwrap_or_default(); let equipped: HashMap = serde_json::from_str(&saved.equipped_json).unwrap_or_default(); let room_id = if self.world.rooms.contains_key(&saved.room_id) { saved.room_id } else { self.world.spawn_room.clone() }; let guilds_vec = self.db.load_guild_memberships(&saved.name); let guilds: HashMap = guilds_vec.into_iter().collect(); let stats = PlayerStats { max_hp: saved.max_hp, hp: saved.hp, attack: saved.attack, defense: saved.defense, level: saved.level, xp: saved.xp, xp_to_next: saved.level * 100, max_mana: saved.max_mana, mana: saved.mana, max_endurance: saved.max_endurance, endurance: saved.endurance, }; self.players.insert( id, PlayerConnection { player: Player { name: saved.name, race_id: saved.race_id, class_id: saved.class_id, room_id, stats, inventory, equipped, guilds, cooldowns: HashMap::new(), is_admin: saved.is_admin, }, channel, handle, combat: None, }, ); } pub fn save_player_to_db(&self, player_id: usize) { if let Some(conn) = self.players.get(&player_id) { let p = &conn.player; let inv_json = serde_json::to_string(&p.inventory).unwrap_or_else(|_| "[]".into()); let equipped_json = serde_json::to_string(&p.equipped).unwrap_or_else(|_| "{}".into()); self.db.save_player(&SavedPlayer { name: p.name.clone(), race_id: p.race_id.clone(), class_id: p.class_id.clone(), room_id: p.room_id.clone(), level: p.stats.level, xp: p.stats.xp, hp: p.stats.hp, max_hp: p.stats.max_hp, attack: p.stats.attack, defense: p.stats.defense, inventory_json: inv_json, equipped_json, mana: p.stats.mana, max_mana: p.stats.max_mana, endurance: p.stats.endurance, max_endurance: p.stats.max_endurance, is_admin: p.is_admin, }); } } pub fn remove_player(&mut self, id: usize) -> Option { self.save_player_to_db(id); self.players.remove(&id) } pub fn players_in_room(&self, room_id: &str, exclude_id: usize) -> Vec<&PlayerConnection> { self.players .iter() .filter(|(&id, conn)| conn.player.room_id == room_id && id != exclude_id) .map(|(_, conn)| conn) .collect() } pub fn check_respawns(&mut self) { let now = Instant::now(); let npc_ids: Vec = self.npc_instances.keys().cloned().collect(); for npc_id in npc_ids { let instance = match self.npc_instances.get(&npc_id) { Some(i) => i, None => continue, }; if instance.alive { continue; } let npc = match self.world.npcs.get(&npc_id) { Some(n) => n, None => continue, }; let respawn_secs = match npc.respawn_secs { Some(s) => s, None => continue, }; let should_respawn = instance.death_time .map(|dt| now.duration_since(dt).as_secs() >= respawn_secs) .unwrap_or(false); if should_respawn { let hp = npc.combat.as_ref().map(|c| c.max_hp).unwrap_or(20); let fixed_race = npc.fixed_race.clone(); let fixed_class = npc.fixed_class.clone(); let (race_id, class_id) = resolve_npc_race_class( &fixed_race, &fixed_class, &self.world, &mut self.rng, ); if let Some(inst) = self.npc_instances.get_mut(&npc_id) { inst.hp = hp; inst.alive = true; inst.death_time = None; inst.race_id = race_id; inst.class_id = class_id; } } } } pub fn check_level_up(&mut self, player_id: usize) -> Option { let conn = self.players.get_mut(&player_id)?; let player = &mut conn.player; if player.stats.xp < player.stats.xp_to_next { return None; } player.stats.xp -= player.stats.xp_to_next; player.stats.level += 1; player.stats.xp_to_next = player.stats.level * 100; let class = self.world.classes.iter().find(|c| c.id == player.class_id); let (hp_g, atk_g, def_g) = match class { Some(c) => ( c.growth.hp_per_level, c.growth.attack_per_level, c.growth.defense_per_level, ), None => (10, 2, 1), }; player.stats.max_hp += hp_g; player.stats.hp = player.stats.max_hp; player.stats.attack += atk_g; player.stats.defense += def_g; Some(format!( "You are now level {}! HP:{} ATK:{} DEF:{}", player.stats.level, player.stats.max_hp, player.stats.attack, player.stats.defense )) } }