fmt: repo-wide baseline (WI-2, issue #2)
This commit is contained in:
parent
899fde8fc3
commit
337e9bcf2c
19 changed files with 847 additions and 335 deletions
|
|
@ -3,9 +3,9 @@
|
|||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
|
||||
pub mod middleware;
|
||||
pub mod routes;
|
||||
pub mod ws;
|
||||
pub mod middleware;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use axum::{
|
||||
extract::Request,
|
||||
http::{header, StatusCode},
|
||||
http::{StatusCode, header},
|
||||
middleware::Next,
|
||||
response::Response,
|
||||
};
|
||||
|
|
@ -13,19 +13,14 @@ pub struct AuthUser {
|
|||
pub role: String,
|
||||
}
|
||||
|
||||
pub async fn auth_middleware(
|
||||
mut request: Request,
|
||||
next: Next,
|
||||
) -> Result<Response, StatusCode> {
|
||||
pub async fn auth_middleware(mut request: Request, next: Next) -> Result<Response, StatusCode> {
|
||||
let auth_header = request
|
||||
.headers()
|
||||
.get(header::AUTHORIZATION)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("");
|
||||
|
||||
let token = auth_header
|
||||
.strip_prefix("Bearer ")
|
||||
.unwrap_or(auth_header);
|
||||
let token = auth_header.strip_prefix("Bearer ").unwrap_or(auth_header);
|
||||
|
||||
if token.is_empty() {
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
|
||||
|
|
@ -1,26 +1,23 @@
|
|||
#![allow(private_interfaces)]
|
||||
|
||||
use crate::middleware::auth::AuthUser;
|
||||
use axum::extract::{Path, Query, State};
|
||||
use axum::routing::{get, post};
|
||||
use axum::{Extension, Json, Router};
|
||||
use crate::middleware::auth::AuthUser;
|
||||
use sb_core::character::{self, CLASSES, RACES, TRAINING_GROUNDS};
|
||||
use sb_core::combat::{self, CombatStats};
|
||||
use sb_core::combat::parse_attack_type;
|
||||
use sb_core::combat::{self, CombatStats};
|
||||
use sb_core::data;
|
||||
use sb_core::services as domain;
|
||||
use sb_core::items;
|
||||
use sb_core::services as domain;
|
||||
use sb_core::songs;
|
||||
use sb_core::spells;
|
||||
use sb_shared::{
|
||||
Character, Class, CreateCharacter, Item, Role, User,
|
||||
ApiInfoResponse, RacesResponse, RaceInfo,
|
||||
ClassesResponse, ClassInfo, EnemiesResponse, EnemySummary,
|
||||
AdventuresResponse, ZoneSummary,
|
||||
GymResponse, TrainStatResponse, TrainingStatResponse, ShopResponse,
|
||||
InventoryItemResponse, ShopItemResponse, ShopActionResponse,
|
||||
SongsResponse, SongSummary, SetSongResponse,
|
||||
LevelUpResponse,
|
||||
AdventuresResponse, ApiInfoResponse, Character, Class, ClassInfo, ClassesResponse,
|
||||
CreateCharacter, EnemiesResponse, EnemySummary, GymResponse, InventoryItemResponse, Item,
|
||||
LevelUpResponse, RaceInfo, RacesResponse, Role, SetSongResponse, ShopActionResponse,
|
||||
ShopItemResponse, ShopResponse, SongSummary, SongsResponse, TrainStatResponse,
|
||||
TrainingStatResponse, User, ZoneSummary,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
|
@ -93,7 +90,10 @@ pub fn build(state: Arc<AppState>) -> Router {
|
|||
.route("/api/classes", get(get_classes))
|
||||
.route("/api/enemies", get(get_enemies))
|
||||
.route("/api/adventures", get(get_adventures))
|
||||
.route("/api/users/{user_id}/characters", get(get_characters).post(create_character))
|
||||
.route(
|
||||
"/api/users/{user_id}/characters",
|
||||
get(get_characters).post(create_character),
|
||||
)
|
||||
.route("/api/characters/{id}", get(get_character))
|
||||
.route("/api/characters/{id}/adventure", get(run_adventure))
|
||||
.route("/api/characters/{id}/equip", post(equip_item))
|
||||
|
|
@ -102,7 +102,10 @@ pub fn build(state: Arc<AppState>) -> Router {
|
|||
.route("/api/characters/{id}/shop", get(get_shop).post(shop_action))
|
||||
.route("/api/characters/{id}/heal", post(heal_character))
|
||||
.route("/api/characters/{id}/songs", get(get_songs).post(set_song))
|
||||
.route("/api/characters/{id}/spells", get(get_spells).post(learn_spell_level))
|
||||
.route(
|
||||
"/api/characters/{id}/spells",
|
||||
get(get_spells).post(learn_spell_level),
|
||||
)
|
||||
.route("/api/characters/{id}/level-up", post(level_up))
|
||||
.route_layer(axum::middleware::from_fn(
|
||||
crate::middleware::auth::auth_middleware,
|
||||
|
|
@ -264,7 +267,11 @@ async fn get_characters(
|
|||
for c in chars.iter_mut() {
|
||||
sb_core::character::apply_regen(c, now);
|
||||
}
|
||||
let user_chars: Vec<Character> = chars.iter().filter(|c| c.user_id == user_id).cloned().collect();
|
||||
let user_chars: Vec<Character> = chars
|
||||
.iter()
|
||||
.filter(|c| c.user_id == user_id)
|
||||
.cloned()
|
||||
.collect();
|
||||
Ok(Json(user_chars))
|
||||
}
|
||||
|
||||
|
|
@ -317,8 +324,10 @@ async fn get_character(
|
|||
let char = chars.iter().find(|c| c.id == id);
|
||||
match char {
|
||||
None => return Err(axum::http::StatusCode::NOT_FOUND),
|
||||
Some(c) if c.user_id != auth.user_id && auth.role != "admin" => return Err(axum::http::StatusCode::FORBIDDEN),
|
||||
Some(_) => {},
|
||||
Some(c) if c.user_id != auth.user_id && auth.role != "admin" => {
|
||||
return Err(axum::http::StatusCode::FORBIDDEN);
|
||||
}
|
||||
Some(_) => {}
|
||||
}
|
||||
}
|
||||
let now = std::time::SystemTime::now()
|
||||
|
|
@ -326,7 +335,9 @@ async fn get_character(
|
|||
.unwrap()
|
||||
.as_millis() as i64;
|
||||
let mut chars = state.characters.write().await;
|
||||
let char = chars.iter_mut().find(|c| c.id == id)
|
||||
let char = chars
|
||||
.iter_mut()
|
||||
.find(|c| c.id == id)
|
||||
.ok_or(axum::http::StatusCode::NOT_FOUND)?;
|
||||
sb_core::character::apply_regen(char, now);
|
||||
Ok(Json(char.clone()))
|
||||
|
|
@ -343,7 +354,10 @@ async fn equip_item(
|
|||
Path(char_id): Path<u32>,
|
||||
Json(input): Json<EquipRequest>,
|
||||
) -> Result<Json<Character>, axum::http::StatusCode> {
|
||||
let item = state.items.iter().find(|i| i.id == input.item_id)
|
||||
let item = state
|
||||
.items
|
||||
.iter()
|
||||
.find(|i| i.id == input.item_id)
|
||||
.ok_or(axum::http::StatusCode::NOT_FOUND)?;
|
||||
|
||||
{
|
||||
|
|
@ -351,14 +365,18 @@ async fn equip_item(
|
|||
let char = chars.iter().find(|c| c.id == char_id);
|
||||
match char {
|
||||
None => return Err(axum::http::StatusCode::NOT_FOUND),
|
||||
Some(c) if c.user_id != auth.user_id && auth.role != "admin" => return Err(axum::http::StatusCode::FORBIDDEN),
|
||||
Some(_) => {},
|
||||
Some(c) if c.user_id != auth.user_id && auth.role != "admin" => {
|
||||
return Err(axum::http::StatusCode::FORBIDDEN);
|
||||
}
|
||||
Some(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
let updated = {
|
||||
let mut chars = state.characters.write().await;
|
||||
let char = chars.iter_mut().find(|c| c.id == char_id)
|
||||
let char = chars
|
||||
.iter_mut()
|
||||
.find(|c| c.id == char_id)
|
||||
.ok_or(axum::http::StatusCode::NOT_FOUND)?;
|
||||
|
||||
domain::equip_item(char, item, &input.item_id)
|
||||
|
|
@ -387,17 +405,20 @@ async fn unequip_item(
|
|||
let char = chars.iter().find(|c| c.id == char_id);
|
||||
match char {
|
||||
None => return Err(axum::http::StatusCode::NOT_FOUND),
|
||||
Some(c) if c.user_id != auth.user_id && auth.role != "admin" => return Err(axum::http::StatusCode::FORBIDDEN),
|
||||
Some(_) => {},
|
||||
Some(c) if c.user_id != auth.user_id && auth.role != "admin" => {
|
||||
return Err(axum::http::StatusCode::FORBIDDEN);
|
||||
}
|
||||
Some(_) => {}
|
||||
}
|
||||
}
|
||||
let updated = {
|
||||
let mut chars = state.characters.write().await;
|
||||
let char = chars.iter_mut().find(|c| c.id == char_id)
|
||||
let char = chars
|
||||
.iter_mut()
|
||||
.find(|c| c.id == char_id)
|
||||
.ok_or(axum::http::StatusCode::NOT_FOUND)?;
|
||||
|
||||
domain::unequip_item(char, &input.slot)
|
||||
.map_err(|_| axum::http::StatusCode::BAD_REQUEST)?;
|
||||
domain::unequip_item(char, &input.slot).map_err(|_| axum::http::StatusCode::BAD_REQUEST)?;
|
||||
|
||||
char.clone()
|
||||
};
|
||||
|
|
@ -419,12 +440,17 @@ async fn run_adventure(
|
|||
let char = chars.iter().find(|c| c.id == char_id);
|
||||
match char {
|
||||
None => return Err(axum::http::StatusCode::NOT_FOUND),
|
||||
Some(c) if c.user_id != auth.user_id && auth.role != "admin" => return Err(axum::http::StatusCode::FORBIDDEN),
|
||||
Some(_) => {},
|
||||
Some(c) if c.user_id != auth.user_id && auth.role != "admin" => {
|
||||
return Err(axum::http::StatusCode::FORBIDDEN);
|
||||
}
|
||||
Some(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
let zone = state.zones.iter().find(|z| z.id == zone_id)
|
||||
let zone = state
|
||||
.zones
|
||||
.iter()
|
||||
.find(|z| z.id == zone_id)
|
||||
.ok_or(axum::http::StatusCode::NOT_FOUND)?;
|
||||
|
||||
let now = std::time::SystemTime::now()
|
||||
|
|
@ -436,7 +462,9 @@ async fn run_adventure(
|
|||
|
||||
let (char, enemy_def) = {
|
||||
let mut chars = state.characters.write().await;
|
||||
let char = chars.iter_mut().find(|c| c.id == char_id)
|
||||
let char = chars
|
||||
.iter_mut()
|
||||
.find(|c| c.id == char_id)
|
||||
.ok_or(axum::http::StatusCode::NOT_FOUND)?;
|
||||
|
||||
sb_core::character::apply_regen(char, now);
|
||||
|
|
@ -500,7 +528,10 @@ async fn run_adventure(
|
|||
enemy_def.gold_min,
|
||||
enemy_def.gold_max,
|
||||
char.active_song.as_deref().and_then(|sid| {
|
||||
songs::BARD_SONGS.iter().find(|s| s.id == sid).map(|s| s.combat_effect)
|
||||
songs::BARD_SONGS
|
||||
.iter()
|
||||
.find(|s| s.id == sid)
|
||||
.map(|s| s.combat_effect)
|
||||
}),
|
||||
spell_effect.as_ref(),
|
||||
&mut rng,
|
||||
|
|
@ -514,7 +545,9 @@ async fn run_adventure(
|
|||
if log.winner == c.name {
|
||||
c.xp += log.xp_gained as i64;
|
||||
c.gold += log.gold_gained;
|
||||
let char_hp = log.rounds.iter()
|
||||
let char_hp = log
|
||||
.rounds
|
||||
.iter()
|
||||
.rfind(|r| r.defender == c.name)
|
||||
.map(|r| r.defender_hp);
|
||||
if let Some(hp) = char_hp {
|
||||
|
|
@ -548,9 +581,16 @@ async fn run_adventure(
|
|||
|
||||
let updated = chars.iter().find(|c| c.id == char_id).cloned();
|
||||
drop(chars);
|
||||
let loot: Vec<String> = dropped_ids.iter().filter_map(|id| {
|
||||
state.items.iter().find(|i| &i.id == id).map(|i| i.name.clone())
|
||||
}).collect();
|
||||
let loot: Vec<String> = dropped_ids
|
||||
.iter()
|
||||
.filter_map(|id| {
|
||||
state
|
||||
.items
|
||||
.iter()
|
||||
.find(|i| &i.id == id)
|
||||
.map(|i| i.name.clone())
|
||||
})
|
||||
.collect();
|
||||
(loot, updated)
|
||||
};
|
||||
|
||||
|
|
@ -576,14 +616,19 @@ async fn get_gym(
|
|||
let char = chars.iter().find(|c| c.id == char_id);
|
||||
match char {
|
||||
None => return Err(axum::http::StatusCode::NOT_FOUND),
|
||||
Some(c) if c.user_id != auth.user_id && auth.role != "admin" => return Err(axum::http::StatusCode::FORBIDDEN),
|
||||
Some(_) => {},
|
||||
Some(c) if c.user_id != auth.user_id && auth.role != "admin" => {
|
||||
return Err(axum::http::StatusCode::FORBIDDEN);
|
||||
}
|
||||
Some(_) => {}
|
||||
}
|
||||
}
|
||||
let char = {
|
||||
let chars = state.characters.read().await;
|
||||
chars.iter().find(|c| c.id == char_id)
|
||||
.ok_or(axum::http::StatusCode::NOT_FOUND)?.clone()
|
||||
chars
|
||||
.iter()
|
||||
.find(|c| c.id == char_id)
|
||||
.ok_or(axum::http::StatusCode::NOT_FOUND)?
|
||||
.clone()
|
||||
};
|
||||
let stats = [
|
||||
("strength", char.stats.strength),
|
||||
|
|
@ -592,16 +637,22 @@ async fn get_gym(
|
|||
("constitution", char.stats.constitution),
|
||||
("luck", char.stats.luck),
|
||||
];
|
||||
let stat_responses: Vec<TrainingStatResponse> = stats.iter().map(|(name, val)| {
|
||||
let ground = TRAINING_GROUNDS.iter().find(|g| g.stat == *name).expect("valid stat");
|
||||
TrainingStatResponse {
|
||||
stat: name.to_string(),
|
||||
name: ground.name.to_string(),
|
||||
description: ground.description.to_string(),
|
||||
value: *val,
|
||||
cost: character::training_cost(*val),
|
||||
}
|
||||
}).collect();
|
||||
let stat_responses: Vec<TrainingStatResponse> = stats
|
||||
.iter()
|
||||
.map(|(name, val)| {
|
||||
let ground = TRAINING_GROUNDS
|
||||
.iter()
|
||||
.find(|g| g.stat == *name)
|
||||
.expect("valid stat");
|
||||
TrainingStatResponse {
|
||||
stat: name.to_string(),
|
||||
name: ground.name.to_string(),
|
||||
description: ground.description.to_string(),
|
||||
value: *val,
|
||||
cost: character::training_cost(*val),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Ok(Json(GymResponse {
|
||||
energy: char.energy,
|
||||
max_energy: char.max_energy,
|
||||
|
|
@ -626,8 +677,10 @@ async fn train_stat(
|
|||
let char = chars.iter().find(|c| c.id == char_id);
|
||||
match char {
|
||||
None => return Err(axum::http::StatusCode::NOT_FOUND),
|
||||
Some(c) if c.user_id != auth.user_id && auth.role != "admin" => return Err(axum::http::StatusCode::FORBIDDEN),
|
||||
Some(_) => {},
|
||||
Some(c) if c.user_id != auth.user_id && auth.role != "admin" => {
|
||||
return Err(axum::http::StatusCode::FORBIDDEN);
|
||||
}
|
||||
Some(_) => {}
|
||||
}
|
||||
}
|
||||
let now = std::time::SystemTime::now()
|
||||
|
|
@ -635,18 +688,20 @@ async fn train_stat(
|
|||
.unwrap()
|
||||
.as_millis() as i64;
|
||||
|
||||
let (updated, _value) = { let mut chars = state.characters.write().await;
|
||||
let char = chars.iter_mut().find(|c| c.id == char_id)
|
||||
let (updated, _value) = {
|
||||
let mut chars = state.characters.write().await;
|
||||
let char = chars
|
||||
.iter_mut()
|
||||
.find(|c| c.id == char_id)
|
||||
.ok_or(axum::http::StatusCode::NOT_FOUND)?;
|
||||
|
||||
sb_core::character::apply_regen(char, now);
|
||||
|
||||
let new_val = domain::train_stat(char, &input.stat)
|
||||
.map_err(|e| match e {
|
||||
"not enough energy" => axum::http::StatusCode::UNPROCESSABLE_ENTITY,
|
||||
"not enough gold" => axum::http::StatusCode::UNPROCESSABLE_ENTITY,
|
||||
_ => axum::http::StatusCode::BAD_REQUEST,
|
||||
})?;
|
||||
let new_val = domain::train_stat(char, &input.stat).map_err(|e| match e {
|
||||
"not enough energy" => axum::http::StatusCode::UNPROCESSABLE_ENTITY,
|
||||
"not enough gold" => axum::http::StatusCode::UNPROCESSABLE_ENTITY,
|
||||
_ => axum::http::StatusCode::BAD_REQUEST,
|
||||
})?;
|
||||
|
||||
(char.clone(), new_val)
|
||||
};
|
||||
|
|
@ -669,48 +724,76 @@ async fn get_shop(
|
|||
let char = chars.iter().find(|c| c.id == char_id);
|
||||
match char {
|
||||
None => return Err(axum::http::StatusCode::NOT_FOUND),
|
||||
Some(c) if c.user_id != auth.user_id && auth.role != "admin" => return Err(axum::http::StatusCode::FORBIDDEN),
|
||||
Some(_) => {},
|
||||
Some(c) if c.user_id != auth.user_id && auth.role != "admin" => {
|
||||
return Err(axum::http::StatusCode::FORBIDDEN);
|
||||
}
|
||||
Some(_) => {}
|
||||
}
|
||||
}
|
||||
let char = {
|
||||
let chars = state.characters.read().await;
|
||||
chars.iter().find(|c| c.id == char_id)
|
||||
.ok_or(axum::http::StatusCode::NOT_FOUND)?.clone()
|
||||
chars
|
||||
.iter()
|
||||
.find(|c| c.id == char_id)
|
||||
.ok_or(axum::http::StatusCode::NOT_FOUND)?
|
||||
.clone()
|
||||
};
|
||||
let shop_stock = state.shop_stock.read().await;
|
||||
let shop_items: Vec<ShopItemResponse> = state.items.iter().filter(|item| {
|
||||
item.always_in_shop || shop_stock.contains_key(&item.id)
|
||||
}).map(|item| {
|
||||
ShopItemResponse {
|
||||
let shop_items: Vec<ShopItemResponse> = state
|
||||
.items
|
||||
.iter()
|
||||
.filter(|item| item.always_in_shop || shop_stock.contains_key(&item.id))
|
||||
.map(|item| ShopItemResponse {
|
||||
id: item.id.clone(),
|
||||
name: item.name.clone(),
|
||||
item_type: serde_json::to_value(&item.r#type).unwrap().as_str().unwrap().to_string(),
|
||||
quality: serde_json::to_value(&item.quality).unwrap().as_str().unwrap().to_string(),
|
||||
item_type: serde_json::to_value(&item.r#type)
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
quality: serde_json::to_value(&item.quality)
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
slot: Some(item.slot.clone()),
|
||||
stats: item.stats.clone(),
|
||||
description: item.description.clone(),
|
||||
attack_type: item.attack_type.as_ref().map(|at| format!("{:?}", at)),
|
||||
value: items::item_price(item),
|
||||
stock: shop_stock.get(&item.id).copied().unwrap_or(0),
|
||||
}
|
||||
}).collect();
|
||||
let bag_items: Vec<InventoryItemResponse> = char.inventory.iter().filter_map(|id| {
|
||||
state.items.iter().find(|i| &i.id == id).map(|item| {
|
||||
InventoryItemResponse {
|
||||
id: item.id.clone(),
|
||||
name: item.name.clone(),
|
||||
item_type: serde_json::to_value(&item.r#type).unwrap().as_str().unwrap().to_string(),
|
||||
quality: serde_json::to_value(&item.quality).unwrap().as_str().unwrap().to_string(),
|
||||
slot: Some(item.slot.clone()),
|
||||
stats: item.stats.clone(),
|
||||
description: item.description.clone(),
|
||||
attack_type: item.attack_type.as_ref().map(|t| format!("{:?}", t)),
|
||||
value: item.value,
|
||||
equipped: false,
|
||||
}
|
||||
})
|
||||
}).collect();
|
||||
.collect();
|
||||
let bag_items: Vec<InventoryItemResponse> = char
|
||||
.inventory
|
||||
.iter()
|
||||
.filter_map(|id| {
|
||||
state
|
||||
.items
|
||||
.iter()
|
||||
.find(|i| &i.id == id)
|
||||
.map(|item| InventoryItemResponse {
|
||||
id: item.id.clone(),
|
||||
name: item.name.clone(),
|
||||
item_type: serde_json::to_value(&item.r#type)
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
quality: serde_json::to_value(&item.quality)
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
slot: Some(item.slot.clone()),
|
||||
stats: item.stats.clone(),
|
||||
description: item.description.clone(),
|
||||
attack_type: item.attack_type.as_ref().map(|t| format!("{:?}", t)),
|
||||
value: item.value,
|
||||
equipped: false,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
Ok(Json(ShopResponse {
|
||||
gold: char.gold,
|
||||
inventory: bag_items,
|
||||
|
|
@ -735,8 +818,10 @@ async fn shop_action(
|
|||
let char = chars.iter().find(|c| c.id == char_id);
|
||||
match char {
|
||||
None => return Err(axum::http::StatusCode::NOT_FOUND),
|
||||
Some(c) if c.user_id != auth.user_id && auth.role != "admin" => return Err(axum::http::StatusCode::FORBIDDEN),
|
||||
Some(_) => {},
|
||||
Some(c) if c.user_id != auth.user_id && auth.role != "admin" => {
|
||||
return Err(axum::http::StatusCode::FORBIDDEN);
|
||||
}
|
||||
Some(_) => {}
|
||||
}
|
||||
}
|
||||
match input.action.as_str() {
|
||||
|
|
@ -751,14 +836,19 @@ async fn buy_item(
|
|||
Path(char_id): Path<u32>,
|
||||
input: ShopAction,
|
||||
) -> Result<Json<ShopActionResponse>, axum::http::StatusCode> {
|
||||
let item = state.items.iter().find(|i| i.id == input.item_id)
|
||||
let item = state
|
||||
.items
|
||||
.iter()
|
||||
.find(|i| i.id == input.item_id)
|
||||
.ok_or(axum::http::StatusCode::NOT_FOUND)?;
|
||||
|
||||
let price = items::item_price(item);
|
||||
|
||||
{
|
||||
let chars = state.characters.read().await;
|
||||
let char = chars.iter().find(|c| c.id == char_id)
|
||||
let char = chars
|
||||
.iter()
|
||||
.find(|c| c.id == char_id)
|
||||
.ok_or(axum::http::StatusCode::NOT_FOUND)?;
|
||||
if char.gold < price {
|
||||
return Err(axum::http::StatusCode::UNPROCESSABLE_ENTITY);
|
||||
|
|
@ -779,7 +869,9 @@ async fn buy_item(
|
|||
|
||||
let updated = {
|
||||
let mut chars = state.characters.write().await;
|
||||
let char = chars.iter_mut().find(|c| c.id == char_id)
|
||||
let char = chars
|
||||
.iter_mut()
|
||||
.find(|c| c.id == char_id)
|
||||
.ok_or(axum::http::StatusCode::NOT_FOUND)?;
|
||||
char.gold -= price;
|
||||
char.inventory.push(input.item_id.clone());
|
||||
|
|
@ -803,17 +895,25 @@ async fn sell_item(
|
|||
Path(char_id): Path<u32>,
|
||||
input: ShopAction,
|
||||
) -> Result<Json<ShopActionResponse>, axum::http::StatusCode> {
|
||||
let item = state.items.iter().find(|i| i.id == input.item_id)
|
||||
let item = state
|
||||
.items
|
||||
.iter()
|
||||
.find(|i| i.id == input.item_id)
|
||||
.ok_or(axum::http::StatusCode::NOT_FOUND)?;
|
||||
|
||||
let sell_price = items::item_sell_price(item);
|
||||
|
||||
let updated = {
|
||||
let mut chars = state.characters.write().await;
|
||||
let char = chars.iter_mut().find(|c| c.id == char_id)
|
||||
let char = chars
|
||||
.iter_mut()
|
||||
.find(|c| c.id == char_id)
|
||||
.ok_or(axum::http::StatusCode::NOT_FOUND)?;
|
||||
|
||||
let pos = char.inventory.iter().position(|id| id == &input.item_id)
|
||||
let pos = char
|
||||
.inventory
|
||||
.iter()
|
||||
.position(|id| id == &input.item_id)
|
||||
.ok_or(axum::http::StatusCode::BAD_REQUEST)?;
|
||||
|
||||
char.inventory.remove(pos);
|
||||
|
|
@ -855,13 +955,17 @@ async fn heal_character(
|
|||
let char = chars.iter().find(|c| c.id == char_id);
|
||||
match char {
|
||||
None => return Err(axum::http::StatusCode::NOT_FOUND),
|
||||
Some(c) if c.user_id != auth.user_id && auth.role != "admin" => return Err(axum::http::StatusCode::FORBIDDEN),
|
||||
Some(_) => {},
|
||||
Some(c) if c.user_id != auth.user_id && auth.role != "admin" => {
|
||||
return Err(axum::http::StatusCode::FORBIDDEN);
|
||||
}
|
||||
Some(_) => {}
|
||||
}
|
||||
}
|
||||
let updated = {
|
||||
let mut chars = state.characters.write().await;
|
||||
let char = chars.iter_mut().find(|c| c.id == char_id)
|
||||
let char = chars
|
||||
.iter_mut()
|
||||
.find(|c| c.id == char_id)
|
||||
.ok_or(axum::http::StatusCode::NOT_FOUND)?;
|
||||
|
||||
let missing = char.max_hp - char.hp;
|
||||
|
|
@ -906,28 +1010,34 @@ async fn get_songs(
|
|||
let char = chars.iter().find(|c| c.id == char_id);
|
||||
match char {
|
||||
None => return Err(axum::http::StatusCode::NOT_FOUND),
|
||||
Some(c) if c.user_id != auth.user_id && auth.role != "admin" => return Err(axum::http::StatusCode::FORBIDDEN),
|
||||
Some(_) => {},
|
||||
Some(c) if c.user_id != auth.user_id && auth.role != "admin" => {
|
||||
return Err(axum::http::StatusCode::FORBIDDEN);
|
||||
}
|
||||
Some(_) => {}
|
||||
}
|
||||
}
|
||||
let char = {
|
||||
let chars = state.characters.read().await;
|
||||
chars.iter().find(|c| c.id == char_id)
|
||||
.ok_or(axum::http::StatusCode::NOT_FOUND)?.clone()
|
||||
chars
|
||||
.iter()
|
||||
.find(|c| c.id == char_id)
|
||||
.ok_or(axum::http::StatusCode::NOT_FOUND)?
|
||||
.clone()
|
||||
};
|
||||
if char.class != Class::Bard {
|
||||
return Err(axum::http::StatusCode::FORBIDDEN);
|
||||
}
|
||||
let available = songs::available_songs(char.level);
|
||||
let songs_json: Vec<SongSummary> = available.iter().map(|s| {
|
||||
SongSummary {
|
||||
let songs_json: Vec<SongSummary> = available
|
||||
.iter()
|
||||
.map(|s| SongSummary {
|
||||
id: s.id.to_string(),
|
||||
name: s.name.to_string(),
|
||||
description: s.description.to_string(),
|
||||
required_level: s.required_level,
|
||||
combat_effect: s.combat_effect.to_string(),
|
||||
}
|
||||
}).collect();
|
||||
})
|
||||
.collect();
|
||||
Ok(Json(SongsResponse {
|
||||
active_song: char.active_song.clone(),
|
||||
available_songs: songs_json,
|
||||
|
|
@ -950,13 +1060,17 @@ async fn set_song(
|
|||
let char = chars.iter().find(|c| c.id == char_id);
|
||||
match char {
|
||||
None => return Err(axum::http::StatusCode::NOT_FOUND),
|
||||
Some(c) if c.user_id != auth.user_id && auth.role != "admin" => return Err(axum::http::StatusCode::FORBIDDEN),
|
||||
Some(_) => {},
|
||||
Some(c) if c.user_id != auth.user_id && auth.role != "admin" => {
|
||||
return Err(axum::http::StatusCode::FORBIDDEN);
|
||||
}
|
||||
Some(_) => {}
|
||||
}
|
||||
}
|
||||
let updated = {
|
||||
let mut chars = state.characters.write().await;
|
||||
let char = chars.iter_mut().find(|c| c.id == char_id)
|
||||
let char = chars
|
||||
.iter_mut()
|
||||
.find(|c| c.id == char_id)
|
||||
.ok_or(axum::http::StatusCode::NOT_FOUND)?;
|
||||
|
||||
if char.class != Class::Bard {
|
||||
|
|
@ -964,9 +1078,13 @@ async fn set_song(
|
|||
}
|
||||
|
||||
match &input.song_id {
|
||||
None => { char.active_song = None; }
|
||||
None => {
|
||||
char.active_song = None;
|
||||
}
|
||||
Some(song_id) => {
|
||||
let song = songs::BARD_SONGS.iter().find(|s| s.id == *song_id)
|
||||
let song = songs::BARD_SONGS
|
||||
.iter()
|
||||
.find(|s| s.id == *song_id)
|
||||
.ok_or(axum::http::StatusCode::NOT_FOUND)?;
|
||||
if song.required_level > char.level {
|
||||
return Err(axum::http::StatusCode::UNPROCESSABLE_ENTITY);
|
||||
|
|
@ -981,7 +1099,10 @@ async fn set_song(
|
|||
|
||||
Ok(Json(SetSongResponse {
|
||||
success: true,
|
||||
message: format!("Song set to {}", updated.active_song.as_deref().unwrap_or("none")),
|
||||
message: format!(
|
||||
"Song set to {}",
|
||||
updated.active_song.as_deref().unwrap_or("none")
|
||||
),
|
||||
active_song: updated.active_song.clone(),
|
||||
}))
|
||||
}
|
||||
|
|
@ -996,14 +1117,19 @@ async fn get_spells(
|
|||
let char = chars.iter().find(|c| c.id == char_id);
|
||||
match char {
|
||||
None => return Err(axum::http::StatusCode::NOT_FOUND),
|
||||
Some(c) if c.user_id != auth.user_id && auth.role != "admin" => return Err(axum::http::StatusCode::FORBIDDEN),
|
||||
Some(_) => {},
|
||||
Some(c) if c.user_id != auth.user_id && auth.role != "admin" => {
|
||||
return Err(axum::http::StatusCode::FORBIDDEN);
|
||||
}
|
||||
Some(_) => {}
|
||||
}
|
||||
}
|
||||
let char = {
|
||||
let chars = state.characters.read().await;
|
||||
chars.iter().find(|c| c.id == char_id)
|
||||
.ok_or(axum::http::StatusCode::NOT_FOUND)?.clone()
|
||||
chars
|
||||
.iter()
|
||||
.find(|c| c.id == char_id)
|
||||
.ok_or(axum::http::StatusCode::NOT_FOUND)?
|
||||
.clone()
|
||||
};
|
||||
let class_str = char.class.as_str().to_string();
|
||||
if !spells::is_caster_class(&class_str) {
|
||||
|
|
@ -1011,23 +1137,26 @@ async fn get_spells(
|
|||
}
|
||||
let spell_level = (char.level / 3) + 1;
|
||||
let available = spells::spells_for_class(&class_str, spell_level);
|
||||
let spells_json: Vec<serde_json::Value> = available.iter().map(|s| {
|
||||
serde_json::json!({
|
||||
"id": &s.id,
|
||||
"code": &s.code,
|
||||
"name": &s.name,
|
||||
"spell_level": s.spell_level,
|
||||
"spell_points": s.spell_points,
|
||||
"range": &s.range,
|
||||
"description": &s.description,
|
||||
"combat_effect": &s.combat_effect,
|
||||
"combat_damage_min": s.combat_damage_min,
|
||||
"combat_damage_max": s.combat_damage_max,
|
||||
"heal_min": s.heal_min,
|
||||
"heal_max": s.heal_max,
|
||||
"known": char.known_spells.contains(&s.id.to_string()),
|
||||
let spells_json: Vec<serde_json::Value> = available
|
||||
.iter()
|
||||
.map(|s| {
|
||||
serde_json::json!({
|
||||
"id": &s.id,
|
||||
"code": &s.code,
|
||||
"name": &s.name,
|
||||
"spell_level": s.spell_level,
|
||||
"spell_points": s.spell_points,
|
||||
"range": &s.range,
|
||||
"description": &s.description,
|
||||
"combat_effect": &s.combat_effect,
|
||||
"combat_damage_min": s.combat_damage_min,
|
||||
"combat_damage_max": s.combat_damage_max,
|
||||
"heal_min": s.heal_min,
|
||||
"heal_max": s.heal_max,
|
||||
"known": char.known_spells.contains(&s.id.to_string()),
|
||||
})
|
||||
})
|
||||
}).collect();
|
||||
.collect();
|
||||
Ok(Json(serde_json::json!({
|
||||
"caster_class": class_str,
|
||||
"spell_level": spell_level,
|
||||
|
|
@ -1051,7 +1180,6 @@ struct SpellRequest {
|
|||
spell_id: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
async fn level_up(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Extension(auth): Extension<AuthUser>,
|
||||
|
|
@ -1062,13 +1190,17 @@ async fn level_up(
|
|||
let char = chars.iter().find(|c| c.id == char_id);
|
||||
match char {
|
||||
None => return Err(axum::http::StatusCode::NOT_FOUND),
|
||||
Some(c) if c.user_id != auth.user_id && auth.role != "admin" => return Err(axum::http::StatusCode::FORBIDDEN),
|
||||
Some(_) => {},
|
||||
Some(c) if c.user_id != auth.user_id && auth.role != "admin" => {
|
||||
return Err(axum::http::StatusCode::FORBIDDEN);
|
||||
}
|
||||
Some(_) => {}
|
||||
}
|
||||
}
|
||||
let updated = {
|
||||
let mut chars = state.characters.write().await;
|
||||
let char = chars.iter_mut().find(|c| c.id == char_id)
|
||||
let char = chars
|
||||
.iter_mut()
|
||||
.find(|c| c.id == char_id)
|
||||
.ok_or(axum::http::StatusCode::NOT_FOUND)?;
|
||||
match character::level_up(char) {
|
||||
Ok(_) => char.clone(),
|
||||
|
|
@ -1100,14 +1232,18 @@ async fn learn_spell_level(
|
|||
let char = chars.iter().find(|c| c.id == char_id);
|
||||
match char {
|
||||
None => return Err(axum::http::StatusCode::NOT_FOUND),
|
||||
Some(c) if c.user_id != auth.user_id && auth.role != "admin" => return Err(axum::http::StatusCode::FORBIDDEN),
|
||||
Some(_) => {},
|
||||
Some(c) if c.user_id != auth.user_id && auth.role != "admin" => {
|
||||
return Err(axum::http::StatusCode::FORBIDDEN);
|
||||
}
|
||||
Some(_) => {}
|
||||
}
|
||||
}
|
||||
if let Some(spell_id) = input.spell_id {
|
||||
let updated = {
|
||||
let mut chars = state.characters.write().await;
|
||||
let char = chars.iter_mut().find(|c| c.id == char_id)
|
||||
let char = chars
|
||||
.iter_mut()
|
||||
.find(|c| c.id == char_id)
|
||||
.ok_or(axum::http::StatusCode::NOT_FOUND)?;
|
||||
if spell_id.is_empty() {
|
||||
char.active_spell = None;
|
||||
|
|
@ -1131,11 +1267,14 @@ async fn learn_spell_level(
|
|||
})));
|
||||
}
|
||||
|
||||
let spell_level = input.spell_level
|
||||
let spell_level = input
|
||||
.spell_level
|
||||
.ok_or(axum::http::StatusCode::BAD_REQUEST)?;
|
||||
let updated = {
|
||||
let mut chars = state.characters.write().await;
|
||||
let char = chars.iter_mut().find(|c| c.id == char_id)
|
||||
let char = chars
|
||||
.iter_mut()
|
||||
.find(|c| c.id == char_id)
|
||||
.ok_or(axum::http::StatusCode::NOT_FOUND)?;
|
||||
|
||||
let class_str = char.class.as_str().to_string();
|
||||
|
|
@ -1158,7 +1297,8 @@ async fn learn_spell_level(
|
|||
}
|
||||
|
||||
let spell_list = spells::spells_for_class(&class_str, spell_level);
|
||||
let new_ids: Vec<String> = spell_list.iter()
|
||||
let new_ids: Vec<String> = spell_list
|
||||
.iter()
|
||||
.filter(|s| !char.known_spells.contains(&s.id.to_string()))
|
||||
.map(|s| s.id.to_string())
|
||||
.collect();
|
||||
|
|
@ -1256,7 +1396,9 @@ async fn login(
|
|||
Json(input): Json<LoginRequest>,
|
||||
) -> Result<Json<AuthResponse>, axum::http::StatusCode> {
|
||||
let users = state.users.read().await;
|
||||
let user = users.iter().find(|u| u.name == input.name)
|
||||
let user = users
|
||||
.iter()
|
||||
.find(|u| u.name == input.name)
|
||||
.ok_or(axum::http::StatusCode::UNAUTHORIZED)?;
|
||||
|
||||
if !sb_auth::hash::verify_password(&input.password, &user.password_hash) {
|
||||
|
|
@ -1282,28 +1424,30 @@ async fn login(
|
|||
pub fn seed_users() -> Arc<AppState> {
|
||||
let db_path = std::env::var("SB_DB_PATH");
|
||||
let db = match db_path.as_deref() {
|
||||
Ok(path) if !path.is_empty() => {
|
||||
match sb_db::Database::open(std::path::Path::new(path)) {
|
||||
Ok(db) => {
|
||||
tracing::info!("database loaded from {}", path);
|
||||
Some(db)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("failed to open database at {}: {}, falling back to in-memory", path, e);
|
||||
None
|
||||
}
|
||||
Ok(path) if !path.is_empty() => match sb_db::Database::open(std::path::Path::new(path)) {
|
||||
Ok(db) => {
|
||||
tracing::info!("database loaded from {}", path);
|
||||
Some(db)
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
"failed to open database at {}: {}, falling back to in-memory",
|
||||
path,
|
||||
e
|
||||
);
|
||||
None
|
||||
}
|
||||
},
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let admin_pass = std::env::var("SB_ADMIN_PASS").unwrap_or_else(|_| "admin".into());
|
||||
let player_pass = std::env::var("SB_PLAYER_PASS").unwrap_or_else(|_| "player".into());
|
||||
|
||||
let admin_hash = sb_auth::hash::hash_password(&admin_pass)
|
||||
.expect("failed to hash admin password");
|
||||
let player_hash = sb_auth::hash::hash_password(&player_pass)
|
||||
.expect("failed to hash player password");
|
||||
let admin_hash =
|
||||
sb_auth::hash::hash_password(&admin_pass).expect("failed to hash admin password");
|
||||
let player_hash =
|
||||
sb_auth::hash::hash_password(&player_pass).expect("failed to hash player password");
|
||||
|
||||
let (users, characters, shop_stock, next_id) = if let Some(ref db) = db {
|
||||
let users = db.load_users().unwrap_or_else(|e| {
|
||||
|
|
@ -1325,8 +1469,18 @@ pub fn seed_users() -> Arc<AppState> {
|
|||
};
|
||||
|
||||
let mut initial_users = vec![
|
||||
User { id: 1, name: "admin".into(), role: Role::Admin, password_hash: admin_hash },
|
||||
User { id: 2, name: "player".into(), role: Role::Player, password_hash: player_hash },
|
||||
User {
|
||||
id: 1,
|
||||
name: "admin".into(),
|
||||
role: Role::Admin,
|
||||
password_hash: admin_hash,
|
||||
},
|
||||
User {
|
||||
id: 2,
|
||||
name: "player".into(),
|
||||
role: Role::Player,
|
||||
password_hash: player_hash,
|
||||
},
|
||||
];
|
||||
|
||||
if let Some(ref db) = db {
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
|
||||
|
|
@ -0,0 +1 @@
|
|||
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
pub mod handler;
|
||||
pub mod channels;
|
||||
pub mod handler;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue