Make dead NPCs auto-respawn.
This commit is contained in:
parent
618d88bb06
commit
e6e712e255
@ -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`.
|
Create a user with a secret password, and username `blast`. Create a production database called `blast`.
|
||||||
|
|
||||||
To get to the latest schema:
|
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`
|
* 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`
|
||||||
|
@ -440,6 +440,15 @@ impl DBTrans {
|
|||||||
Ok(())
|
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)>> {
|
pub async fn find_session_for_player(self: &Self, item_code: &str) -> DResult<Option<(ListenerSession, Session)>> {
|
||||||
Ok(self.pg_trans()?
|
Ok(self.pg_trans()?
|
||||||
.query_opt("SELECT u.current_listener, u.current_session, s.details \
|
.query_opt("SELECT u.current_listener, u.current_session, s.details \
|
||||||
@ -563,6 +572,10 @@ impl DBTrans {
|
|||||||
).await?;
|
).await?;
|
||||||
Ok(())
|
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<()> {
|
pub async fn commit(mut self: Self) -> DResult<()> {
|
||||||
let trans_opt = self.with_trans_mut(|t| std::mem::replace(t, None));
|
let trans_opt = self.with_trans_mut(|t| std::mem::replace(t, None));
|
||||||
|
@ -363,7 +363,7 @@ impl Default for Item {
|
|||||||
is_dead: false,
|
is_dead: false,
|
||||||
is_challenge_attack_only: true,
|
is_challenge_attack_only: true,
|
||||||
species: SpeciesType::Human,
|
species: SpeciesType::Human,
|
||||||
health: 40,
|
health: 24,
|
||||||
total_xp: 0,
|
total_xp: 0,
|
||||||
total_stats: BTreeMap::new(),
|
total_stats: BTreeMap::new(),
|
||||||
total_skills: BTreeMap::new(),
|
total_skills: BTreeMap::new(),
|
||||||
|
@ -15,7 +15,13 @@ pub enum TaskDetails {
|
|||||||
npc_code: String,
|
npc_code: String,
|
||||||
say_code: String
|
say_code: String
|
||||||
},
|
},
|
||||||
AttackTick
|
AttackTick,
|
||||||
|
RecloneNPC {
|
||||||
|
npc_code: String
|
||||||
|
},
|
||||||
|
RotCorpse {
|
||||||
|
corpse_code: String
|
||||||
|
},
|
||||||
}
|
}
|
||||||
impl TaskDetails {
|
impl TaskDetails {
|
||||||
pub fn name(self: &Self) -> &'static str {
|
pub fn name(self: &Self) -> &'static str {
|
||||||
@ -23,7 +29,9 @@ impl TaskDetails {
|
|||||||
match self {
|
match self {
|
||||||
RunQueuedCommand => "RunQueuedCommand",
|
RunQueuedCommand => "RunQueuedCommand",
|
||||||
NPCSay { .. } => "NPCSay",
|
NPCSay { .. } => "NPCSay",
|
||||||
AttackTick => "AttackTick"
|
AttackTick => "AttackTick",
|
||||||
|
RecloneNPC { .. } => "RecloneNPC",
|
||||||
|
RotCorpse { .. } => "RotCorpse",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,9 @@ fn task_handler_registry() -> &'static BTreeMap<&'static str, &'static (dyn Task
|
|||||||
|| vec!(
|
|| vec!(
|
||||||
("RunQueuedCommand", queued_command::HANDLER.clone()),
|
("RunQueuedCommand", queued_command::HANDLER.clone()),
|
||||||
("NPCSay", npc::SAY_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()
|
).into_iter().collect()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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
|
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(())
|
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;
|
||||||
|
@ -8,6 +8,12 @@ use crate::models::{
|
|||||||
item::{Item, Pronouns, SkillType},
|
item::{Item, Pronouns, SkillType},
|
||||||
task::{Task, TaskMeta, TaskRecurrence, TaskDetails}
|
task::{Task, TaskMeta, TaskRecurrence, TaskDetails}
|
||||||
};
|
};
|
||||||
|
use crate::services::{
|
||||||
|
combat::{
|
||||||
|
max_health,
|
||||||
|
corpsify_item,
|
||||||
|
}
|
||||||
|
};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use crate::message_handler::user_commands::{
|
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 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;
|
||||||
|
@ -17,6 +17,8 @@ CREATE TABLE sessions (
|
|||||||
);
|
);
|
||||||
CREATE INDEX session_by_listener ON sessions(listener);
|
CREATE INDEX session_by_listener ON sessions(listener);
|
||||||
|
|
||||||
|
CREATE SEQUENCE item_seq;
|
||||||
|
|
||||||
CREATE TABLE items (
|
CREATE TABLE items (
|
||||||
item_id BIGSERIAL NOT NULL PRIMARY KEY,
|
item_id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||||
details JSONB NOT NULL
|
details JSONB NOT NULL
|
||||||
|
Loading…
Reference in New Issue
Block a user