diff --git a/README.md b/README.md index 4f1d90df..64bf0bc8 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,6 @@ The latest schema is under `schema`. Create a user with a secret password, and username `blast`. Create a production database called `blast`. To get to the latest schema: -* Run `psql /tmp/update.sql` -* Check `/tmp/update.sql` and if it looks good, apply it with `psql -u blast -d blast DResult<()> { + self.pg_trans()? + .execute("DELETE FROM items WHERE \ + details->>'item_type' = $2 AND \ + details->>'item_code' = $3", + &[&item_type, &item_code]).await?; + Ok(()) + } + pub async fn find_session_for_player(self: &Self, item_code: &str) -> DResult> { Ok(self.pg_trans()? .query_opt("SELECT u.current_listener, u.current_session, s.details \ @@ -563,6 +572,10 @@ impl DBTrans { ).await?; Ok(()) } + + pub async fn alloc_item_code(&self) -> DResult { + Ok(self.pg_trans()?.query_one("SELECT NEXTVAL('item_seq')", &[]).await?.get(1)) + } pub async fn commit(mut self: Self) -> DResult<()> { let trans_opt = self.with_trans_mut(|t| std::mem::replace(t, None)); diff --git a/blastmud_game/src/models/item.rs b/blastmud_game/src/models/item.rs index 6f171aa5..9823eb79 100644 --- a/blastmud_game/src/models/item.rs +++ b/blastmud_game/src/models/item.rs @@ -363,7 +363,7 @@ impl Default for Item { is_dead: false, is_challenge_attack_only: true, species: SpeciesType::Human, - health: 40, + health: 24, total_xp: 0, total_stats: BTreeMap::new(), total_skills: BTreeMap::new(), diff --git a/blastmud_game/src/models/task.rs b/blastmud_game/src/models/task.rs index dbfd2a19..a6bdc8e5 100644 --- a/blastmud_game/src/models/task.rs +++ b/blastmud_game/src/models/task.rs @@ -15,7 +15,13 @@ pub enum TaskDetails { npc_code: String, say_code: String }, - AttackTick + AttackTick, + RecloneNPC { + npc_code: String + }, + RotCorpse { + corpse_code: String + }, } impl TaskDetails { pub fn name(self: &Self) -> &'static str { @@ -23,7 +29,9 @@ impl TaskDetails { match self { RunQueuedCommand => "RunQueuedCommand", NPCSay { .. } => "NPCSay", - AttackTick => "AttackTick" + AttackTick => "AttackTick", + RecloneNPC { .. } => "RecloneNPC", + RotCorpse { .. } => "RotCorpse", } } } diff --git a/blastmud_game/src/regular_tasks.rs b/blastmud_game/src/regular_tasks.rs index 6b9e07e0..31dea0a4 100644 --- a/blastmud_game/src/regular_tasks.rs +++ b/blastmud_game/src/regular_tasks.rs @@ -34,7 +34,9 @@ fn task_handler_registry() -> &'static BTreeMap<&'static str, &'static (dyn Task || vec!( ("RunQueuedCommand", queued_command::HANDLER.clone()), ("NPCSay", npc::SAY_HANDLER.clone()), - ("AttackTick", combat::TASK_HANDLER.clone()) + ("AttackTick", combat::TASK_HANDLER.clone()), + ("RecloneNPC", npc::RECLONE_HANDLER.clone()), + ("RotCorpse", combat::ROT_CORPSE_HANDLER.clone()), ).into_iter().collect() ) } diff --git a/blastmud_game/src/services/combat.rs b/blastmud_game/src/services/combat.rs index dc6d75ca..b544388a 100644 --- a/blastmud_game/src/services/combat.rs +++ b/blastmud_game/src/services/combat.rs @@ -159,6 +159,18 @@ pub async fn handle_death(trans: &DBTrans, whom: &mut Item) -> DResult<()> { } } } + if whom.item_type == "npc" { + trans.upsert_task(&Task { + meta: TaskMeta { + task_code: whom.item_code.clone(), + next_scheduled: Utc::now() + chrono::Duration::seconds(120), + ..Default::default() + }, + details: TaskDetails::RecloneNPC { + npc_code: whom.item_code.clone() + } + }).await?; + } broadcast_to_room(trans, &whom.location, None, &msg_exp, Some(&msg_nonexp)).await } @@ -289,3 +301,74 @@ pub async fn start_attack(trans: &DBTrans, by_whom: &Item, to_whom: &Item) -> UR Ok(()) } + +pub async fn corpsify_item(trans: &DBTrans, base_item: &Item) -> DResult<()> { + let mut new_item = base_item.clone(); + new_item.item_type = "corpse".to_owned(); + new_item.item_code = format!("{}", trans.alloc_item_code().await?); + trans.save_item_model(&new_item).await?; + trans.upsert_task(&Task { + meta: TaskMeta { + task_code: new_item.item_code.clone(), + next_scheduled: Utc::now() + chrono::Duration::minutes(5), + ..Default::default() + }, + details: TaskDetails::RotCorpse { corpse_code: new_item.item_code.clone() } + }).await?; + + Ok(()) +} + +pub struct NPCRecloneTaskHandler; +#[async_trait] +impl TaskHandler for NPCRecloneTaskHandler { + async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult> { + let npc_code = match &ctx.task.details { + TaskDetails::RecloneNPC { npc_code } => npc_code.clone(), + _ => Err("Expected RecloneNPC type")? + }; + let mut npc_item = match ctx.trans.find_item_by_type_code("npc", &npc_code).await? { + None => { return Ok(None) }, + Some(r) => (*r).clone() + }; + + let npc = match npc_by_code().get(npc_code.as_str()) { + None => { return Ok(None) }, + Some(r) => r + }; + + if !npc_item.is_dead { + return Ok(None); + } + + corpsify_item(ctx.trans, &npc_item).await?; + + npc_item.is_dead = false; + npc_item.health = max_health(&npc_item); + npc_item.location = npc.spawn_location.to_owned(); + ctx.trans.save_item_model(&npc_item).await?; + return Ok(None); + } +} +pub struct RotCorpseTaskHandler; +#[async_trait] +impl TaskHandler for RotCorpseTaskHandler { + async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult> { + let corpse_code = match &ctx.task.details { + TaskDetails::RotCorpse { corpse_code } => corpse_code.clone(), + _ => Err("Expected RotCorpse type")? + }; + let corpse = match ctx.trans.find_item_by_type_code("corpse", &corpse_code).await? { + None => { return Ok(None) } + Some(r) => r + }; + ctx.trans.delete_item("corpse", &corpse_code).await?; + let msg_exp = format!("{} rots away to nothing.\n", + corpse.display_for_sentence(true, 1, true)); + let msg_nonexp = format!("{} rots away to nothing.\n", + corpse.display_for_sentence(false, 1, true)); + broadcast_to_room(ctx.trans, &corpse.location, None, &msg_exp, Some(&msg_nonexp)).await?; + Ok(None) + } +} +pub static ROT_CORPSE_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &RotCorpseTaskHandler; diff --git a/blastmud_game/src/static_content/npc.rs b/blastmud_game/src/static_content/npc.rs index 7ebffcd0..46d582c4 100644 --- a/blastmud_game/src/static_content/npc.rs +++ b/blastmud_game/src/static_content/npc.rs @@ -8,6 +8,12 @@ use crate::models::{ item::{Item, Pronouns, SkillType}, task::{Task, TaskMeta, TaskRecurrence, TaskDetails} }; +use crate::services::{ + combat::{ + max_health, + corpsify_item, + } +}; use once_cell::sync::OnceCell; use std::collections::BTreeMap; use crate::message_handler::user_commands::{ @@ -218,3 +224,36 @@ impl TaskHandler for NPCSayTaskHandler { } } pub static SAY_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &NPCSayTaskHandler; + +pub struct NPCRecloneTaskHandler; +#[async_trait] +impl TaskHandler for NPCRecloneTaskHandler { + async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult> { + let npc_code = match &ctx.task.details { + TaskDetails::RecloneNPC { npc_code } => npc_code.clone(), + _ => Err("Expected RecloneNPC type")? + }; + let mut npc_item = match ctx.trans.find_item_by_type_code("npc", &npc_code).await? { + None => { return Ok(None) }, + Some(r) => (*r).clone() + }; + + let npc = match npc_by_code().get(npc_code.as_str()) { + None => { return Ok(None) }, + Some(r) => r + }; + + if !npc_item.is_dead { + return Ok(None); + } + + corpsify_item(ctx.trans, &npc_item).await?; + + npc_item.is_dead = false; + npc_item.health = max_health(&npc_item); + npc_item.location = npc.spawn_location.to_owned(); + ctx.trans.save_item_model(&npc_item).await?; + return Ok(None); + } +} +pub static RECLONE_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &NPCRecloneTaskHandler; diff --git a/schema/schema.sql b/schema/schema.sql index 33011e5d..5670a8f7 100644 --- a/schema/schema.sql +++ b/schema/schema.sql @@ -17,6 +17,8 @@ CREATE TABLE sessions ( ); CREATE INDEX session_by_listener ON sessions(listener); +CREATE SEQUENCE item_seq; + CREATE TABLE items ( item_id BIGSERIAL NOT NULL PRIMARY KEY, details JSONB NOT NULL