Implement auth, persistence, typed responses, and injectable RNG
Auth: JWT tokens (sb-auth) with argon2 password hashing, login/register endpoints, Bearer token middleware protecting all routes except auth. Authorization checks ensure users can only access their own characters. Persistence: SQLite via rusqlite (sb-db) with write-through on all mutations. Optional via SB_DB_PATH env var; falls back to in-memory. Typed responses: ~20 response DTOs in sb-shared replacing ad-hoc JSON. Combat: Injectable RNG via &mut impl Rng for deterministic tests. Clean Architecture: Combat types and response DTOs in sb-shared; domain services in sb-core; auth in sb-auth; persistence in sb-db.
This commit is contained in:
parent
7a75a523f0
commit
6410e0a86d
39 changed files with 3450 additions and 48 deletions
142
crates/sb-shared/src/character.rs
Normal file
142
crates/sb-shared/src/character.rs
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CharacterStats {
|
||||
pub strength: i64,
|
||||
pub intelligence: i64,
|
||||
pub dexterity: i64,
|
||||
pub constitution: i64,
|
||||
pub luck: i64,
|
||||
}
|
||||
|
||||
impl CharacterStats {
|
||||
pub fn total(&self) -> i64 {
|
||||
self.strength + self.intelligence + self.dexterity + self.constitution + self.luck
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Race {
|
||||
Human,
|
||||
Elf,
|
||||
Dwarf,
|
||||
Hobbit,
|
||||
HalfElf,
|
||||
}
|
||||
|
||||
impl Race {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Race::Human => "human",
|
||||
Race::Elf => "elf",
|
||||
Race::Dwarf => "dwarf",
|
||||
Race::Hobbit => "hobbit",
|
||||
Race::HalfElf => "half_elf",
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
pub fn from_str(s: &str) -> Self {
|
||||
match s {
|
||||
"human" => Race::Human,
|
||||
"elf" => Race::Elf,
|
||||
"dwarf" => Race::Dwarf,
|
||||
"hobbit" => Race::Hobbit,
|
||||
"half_elf" => Race::HalfElf,
|
||||
_ => Race::Human,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Class {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Class::Warrior => "warrior",
|
||||
Class::Paladin => "paladin",
|
||||
Class::Rogue => "rogue",
|
||||
Class::Bard => "bard",
|
||||
Class::Hunter => "hunter",
|
||||
Class::Monk => "monk",
|
||||
Class::Mage => "mage",
|
||||
Class::Wizard => "wizard",
|
||||
Class::Conjurer => "conjurer",
|
||||
Class::Sorcerer => "sorcerer",
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
pub fn from_str(s: &str) -> Self {
|
||||
match s {
|
||||
"warrior" => Class::Warrior,
|
||||
"paladin" => Class::Paladin,
|
||||
"rogue" => Class::Rogue,
|
||||
"bard" => Class::Bard,
|
||||
"hunter" => Class::Hunter,
|
||||
"monk" => Class::Monk,
|
||||
"mage" => Class::Mage,
|
||||
"wizard" => Class::Wizard,
|
||||
"conjurer" => Class::Conjurer,
|
||||
"sorcerer" => Class::Sorcerer,
|
||||
_ => Class::Warrior,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Class {
|
||||
Warrior,
|
||||
Paladin,
|
||||
Rogue,
|
||||
Bard,
|
||||
Hunter,
|
||||
Monk,
|
||||
Mage,
|
||||
Wizard,
|
||||
Conjurer,
|
||||
Sorcerer,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Character {
|
||||
pub id: u32,
|
||||
pub user_id: u32,
|
||||
pub name: String,
|
||||
pub race: Race,
|
||||
pub class: Class,
|
||||
pub level: i32,
|
||||
pub xp: i64,
|
||||
pub stats: CharacterStats,
|
||||
pub hp: i32,
|
||||
pub max_hp: i32,
|
||||
pub energy: i32,
|
||||
pub max_energy: i32,
|
||||
pub nerve: i32,
|
||||
pub max_nerve: i32,
|
||||
pub gold: i32,
|
||||
pub last_regen: i64,
|
||||
pub equipment: HashMap<String, Option<String>>,
|
||||
pub inventory: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub active_song: Option<String>,
|
||||
#[serde(default)]
|
||||
pub known_spells: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub active_spell: Option<String>,
|
||||
#[serde(default = "default_sp")]
|
||||
pub spell_points: i32,
|
||||
#[serde(default = "default_max_sp")]
|
||||
pub max_spell_points: i32,
|
||||
}
|
||||
|
||||
fn default_sp() -> i32 { 0 }
|
||||
fn default_max_sp() -> i32 { 0 }
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct CreateCharacter {
|
||||
pub name: String,
|
||||
pub race: Race,
|
||||
pub class: Class,
|
||||
}
|
||||
46
crates/sb-shared/src/item.rs
Normal file
46
crates/sb-shared/src/item.rs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Item {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub r#type: ItemType,
|
||||
pub quality: Quality,
|
||||
pub slot: String,
|
||||
pub stats: HashMap<String, i64>,
|
||||
pub description: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub attack_type: Option<AttackType>,
|
||||
pub value: i32,
|
||||
#[serde(default)]
|
||||
pub always_in_shop: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ItemType {
|
||||
Weapon,
|
||||
Armor,
|
||||
Accessory,
|
||||
Consumable,
|
||||
Material,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AttackType {
|
||||
Slash,
|
||||
Crush,
|
||||
Puncture,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Quality {
|
||||
Common,
|
||||
Uncommon,
|
||||
Rare,
|
||||
Epic,
|
||||
Legendary,
|
||||
}
|
||||
|
|
@ -1,3 +1,10 @@
|
|||
pub mod character;
|
||||
pub mod errors;
|
||||
pub mod item;
|
||||
pub mod responses;
|
||||
pub mod user;
|
||||
|
||||
pub use errors::AppError;
|
||||
pub use character::{Character, CharacterStats, Class, CreateCharacter, Race};
|
||||
pub use item::{AttackType, Item, ItemType, Quality};
|
||||
pub use responses::*;
|
||||
pub use user::{Role, User};
|
||||
|
|
|
|||
201
crates/sb-shared/src/responses.rs
Normal file
201
crates/sb-shared/src/responses.rs
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use crate::character::Character;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CritEntry {
|
||||
pub name: String,
|
||||
pub damage_multiplier: f64,
|
||||
pub stun_rounds: u8,
|
||||
pub bleed_per_round: i32,
|
||||
pub bleed_rounds: u8,
|
||||
pub effects: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CritResult {
|
||||
pub severity: String,
|
||||
pub crit: CritEntry,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CombatRound {
|
||||
pub attacker: String,
|
||||
pub defender: String,
|
||||
pub raw_roll: i32,
|
||||
pub ob: i32,
|
||||
pub db: i32,
|
||||
pub result: i32,
|
||||
pub damage: i32,
|
||||
pub crit: Option<CritResult>,
|
||||
pub defender_hp: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CombatLog {
|
||||
pub rounds: Vec<CombatRound>,
|
||||
pub winner: String,
|
||||
pub loser: String,
|
||||
pub xp_gained: i32,
|
||||
pub gold_gained: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ApiInfoResponse {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RacesResponse {
|
||||
pub races: Vec<RaceInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RaceInfo {
|
||||
pub race: String,
|
||||
pub name: String,
|
||||
pub str_mod: i64,
|
||||
pub int_mod: i64,
|
||||
pub dex_mod: i64,
|
||||
pub con_mod: i64,
|
||||
pub luck_mod: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ClassesResponse {
|
||||
pub classes: Vec<ClassInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ClassInfo {
|
||||
pub class: String,
|
||||
pub name: String,
|
||||
pub primary_stat: String,
|
||||
pub secondary_stat: String,
|
||||
pub base_hp: i32,
|
||||
pub hp_per_con: f64,
|
||||
pub is_caster: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EnemiesResponse {
|
||||
pub enemies: Vec<EnemySummary>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EnemySummary {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub level: i32,
|
||||
pub hp: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AdventuresResponse {
|
||||
pub zones: Vec<ZoneSummary>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ZoneSummary {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub min_level: i32,
|
||||
pub nerve_cost: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GymResponse {
|
||||
pub energy: i32,
|
||||
pub max_energy: i32,
|
||||
pub gold: i32,
|
||||
pub stats: Vec<TrainingStatResponse>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TrainingStatResponse {
|
||||
pub stat: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub value: i64,
|
||||
pub cost: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TrainStatResponse {
|
||||
pub success: bool,
|
||||
pub message: String,
|
||||
pub character: Option<Character>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ShopResponse {
|
||||
pub gold: i32,
|
||||
pub inventory: Vec<InventoryItemResponse>,
|
||||
pub shop_stock: Vec<ShopItemResponse>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct InventoryItemResponse {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub item_type: String,
|
||||
pub quality: String,
|
||||
pub slot: Option<String>,
|
||||
pub stats: std::collections::HashMap<String, i64>,
|
||||
pub description: String,
|
||||
pub attack_type: Option<String>,
|
||||
pub value: i32,
|
||||
pub equipped: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ShopItemResponse {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub item_type: String,
|
||||
pub quality: String,
|
||||
pub slot: Option<String>,
|
||||
pub stats: std::collections::HashMap<String, i64>,
|
||||
pub description: String,
|
||||
pub attack_type: Option<String>,
|
||||
pub value: i32,
|
||||
pub stock: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ShopActionResponse {
|
||||
pub success: bool,
|
||||
pub message: String,
|
||||
pub gold: i32,
|
||||
pub inventory: Vec<InventoryItemResponse>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SongsResponse {
|
||||
pub active_song: Option<String>,
|
||||
pub available_songs: Vec<SongSummary>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SongSummary {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub required_level: i32,
|
||||
pub combat_effect: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SetSongResponse {
|
||||
pub success: bool,
|
||||
pub message: String,
|
||||
pub active_song: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LevelUpResponse {
|
||||
pub success: bool,
|
||||
pub message: String,
|
||||
pub character: Option<Character>,
|
||||
}
|
||||
17
crates/sb-shared/src/user.rs
Normal file
17
crates/sb-shared/src/user.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct User {
|
||||
pub id: u32,
|
||||
pub name: String,
|
||||
pub role: Role,
|
||||
#[serde(skip_serializing)]
|
||||
pub password_hash: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Role {
|
||||
Admin,
|
||||
Player,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue