Make dead NPCs auto-respawn.

This commit is contained in:
Condorra 2023-01-23 22:52:01 +11:00
parent 618d88bb06
commit e6e712e255
8 changed files with 153 additions and 6 deletions

View File

@ -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 <schema/schema.sql` to create the temporary `blast_schemaonly` database.
* Run `psql -d template1 <schema/schema.sql` to create the temporary `blast_schemaonly` database.
* Run `migra "postgres:///blast" "postgres:///blast_schemaonly" > /tmp/update.sql`
* Check `/tmp/update.sql` and if it looks good, apply it with `psql -u blast -d blast </tmp/update.sql`
* Check `/tmp/update.sql` and if it looks good, apply it with `psql -U blast -d blast </tmp/update.sql`

View File

@ -440,6 +440,15 @@ impl DBTrans {
Ok(())
}
pub async fn delete_item(self: &Self, item_type: &str, item_code: &str) -> 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<Option<(ListenerSession, Session)>> {
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<i64> {
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));

View File

@ -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(),

View File

@ -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",
}
}
}

View File

@ -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()
)
}

View File

@ -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<Option<time::Duration>> {
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<Option<time::Duration>> {
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;

View File

@ -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<Option<time::Duration>> {
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;

View File

@ -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