Implement blades (partially), + fix some attack bugs.

This commit is contained in:
Condorra 2023-09-11 21:40:41 +10:00
parent c054c8473a
commit e42bb6b4a9
10 changed files with 312 additions and 16 deletions

View File

@ -12,7 +12,6 @@ use crate::static_content::{possession_type::PossessionType, room::Direction};
use crate::DResult; use crate::DResult;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use deadpool_postgres::{Manager, ManagerConfig, Object, Pool, RecyclingMethod, Transaction}; use deadpool_postgres::{Manager, ManagerConfig, Object, Pool, RecyclingMethod, Transaction};
use futures::FutureExt;
#[cfg(test)] #[cfg(test)]
use mockall::automock; use mockall::automock;
use ouroboros::self_referencing; use ouroboros::self_referencing;
@ -169,7 +168,18 @@ impl DBPool {
let conn = self.get_conn().await?; let conn = self.get_conn().await?;
Ok(DBTransAsyncSendTryBuilder { Ok(DBTransAsyncSendTryBuilder {
conn, conn,
trans_builder: |conn| Box::pin(conn.transaction().map(|r| r.map(Some))), trans_builder: |conn| {
Box::pin(async {
match conn.transaction().await {
Err(e) => Err(e),
Ok(t) => {
t.execute("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE", &[])
.await?;
Ok(Some(t))
}
}
})
},
} }
.try_build() .try_build()
.await?) .await?)

View File

@ -75,6 +75,10 @@ impl UserVerb for Verb {
user_error("There's no point attacking the dead!".to_string())? user_error("There's no point attacking the dead!".to_string())?
} }
if attack_whom.active_climb.is_some() {
user_error("They are already climbing away!".to_string())?
}
start_attack(&ctx.trans, &player_item, &attack_whom).await start_attack(&ctx.trans, &player_item, &attack_whom).await
} }
} }

View File

@ -531,12 +531,16 @@ async fn attempt_move_immediate(
let mut attacker_items = Vec::new(); let mut attacker_items = Vec::new();
if let Some((_, session_dat)) = session.as_ref() { if let Some((_, session_dat)) = session.as_ref() {
for attacker in &attackers[..] { for attacker in &attackers[..] {
if let Some((acode, atype)) = attacker.split_once("/") { if let Some((atype, acode)) = attacker.split_once("/") {
if let Some(aitem) = if let Some(aitem) =
ctx.trans.find_item_by_type_code(acode, atype).await? ctx.trans.find_item_by_type_code(atype, acode).await?
{ {
attacker_names.push(aitem.display_for_session(session_dat)); attacker_names.push(aitem.display_for_session(session_dat));
attacker_items.push(aitem); // We don't push the actual attacker Item, because another attacker
// might re-target this attacker when we escape, causing the structure
// to be out of date. Instead, we push the type, code pair and look it
// up when we need it.
attacker_items.push((atype, acode));
} }
} }
} }
@ -566,11 +570,17 @@ async fn attempt_move_immediate(
) )
.await?; .await?;
} }
for item in &attacker_items[..] { for (item_type, item_code) in &attacker_items[..] {
let mut item_mut = (**item).clone(); if let Some(item) = ctx
.trans
.find_item_by_type_code(item_type, item_code)
.await?
{
let mut item_mut = (*item).clone();
stop_attacking_mut(ctx.trans, &mut item_mut, ctx.item, true).await?; stop_attacking_mut(ctx.trans, &mut item_mut, ctx.item, true).await?;
ctx.trans.save_item_model(&item_mut).await?; ctx.trans.save_item_model(&item_mut).await?;
} }
}
} else { } else {
if let Some((sess, _)) = session.as_ref() { if let Some((sess, _)) = session.as_ref() {
ctx.trans ctx.trans

View File

@ -141,15 +141,15 @@ async fn process_attack(
{ {
let msg_exp = format!( let msg_exp = format!(
"{} looks like {} wanted to attack {}, but was too tired and stressed to do it.\n", "{} looks like {} wanted to attack {}, but was too tired and stressed to do it.\n",
attacker_item.display_for_sentence(true, 1, false), attacker_item.display_for_sentence(true, 1, true),
attacker_item.pronouns.subject, attacker_item.pronouns.subject,
victim_item.display_for_sentence(true, 1, true), victim_item.display_for_sentence(true, 1, false),
); );
let msg_nonexp = format!( let msg_nonexp = format!(
"{} looks like {} wanted to attack {}, but was too tired and stressed to do it.\n", "{} looks like {} wanted to attack {}, but was too tired and stressed to do it.\n",
attacker_item.display_for_sentence(false, 1, false), attacker_item.display_for_sentence(false, 1, true),
attacker_item.pronouns.subject, attacker_item.pronouns.subject,
victim_item.display_for_sentence(false, 1, true), victim_item.display_for_sentence(false, 1, false),
); );
broadcast_to_room( broadcast_to_room(
ctx.trans, ctx.trans,
@ -508,6 +508,17 @@ pub async fn handle_death(trans: &DBTrans, whom: &mut Item) -> DResult<()> {
} }
} }
} }
match whom.urges.as_mut() {
None => {}
Some(urges) => {
urges.hunger = Default::default();
urges.bladder = Default::default();
urges.thirst = Default::default();
urges.stress = Default::default();
}
}
if vic_is_npc { if vic_is_npc {
trans trans
.upsert_task(&Task { .upsert_task(&Task {

View File

@ -350,6 +350,9 @@ pub async fn change_stress_considering_cool(
who: &mut Item, who: &mut Item,
max_magnitude: i64, max_magnitude: i64,
) -> DResult<()> { ) -> DResult<()> {
if !who.flags.contains(&ItemFlag::HasUrges) {
return Ok(());
}
let cool = who.total_stats.get(&StatType::Cool).unwrap_or(&8.0); let cool = who.total_stats.get(&StatType::Cool).unwrap_or(&8.0);
let stress_factor = 1.0 - 1.0 / (1.0 + (-0.7 * (cool - 8.0)).exp()); let stress_factor = 1.0 - 1.0 / (1.0 + (-0.7 * (cool - 8.0)).exp());
match who.urges.as_mut() { match who.urges.as_mut() {

View File

@ -491,6 +491,7 @@ impl TaskHandler for NPCAggroTaskHandler {
(it.item_type == "player" || it.item_type == "npc") (it.item_type == "player" || it.item_type == "npc")
&& it.death_data.is_none() && it.death_data.is_none()
&& !it.flags.contains(&ItemFlag::NPCsDontAttack) && !it.flags.contains(&ItemFlag::NPCsDontAttack)
&& it.active_climb.is_none()
&& (it.item_type != item.item_type || it.item_code != item.item_code) && (it.item_type != item.item_type || it.item_code != item.item_code)
}) })
.choose(&mut thread_rng()); .choose(&mut thread_rng());

View File

@ -404,6 +404,12 @@ pub enum PossessionType {
LeatherWhip, LeatherWhip,
// Weapons: Blades // Weapons: Blades
ButcherKnife, ButcherKnife,
Dagger,
RusticKatana,
SawtoothMachete,
Electroblade,
FlameScimitar,
NanobladeGladius,
// Medical // Medical
MediumTraumaKit, MediumTraumaKit,
EmptyMedicalBox, EmptyMedicalBox,

View File

@ -1,5 +1,5 @@
use super::{PossessionData, PossessionType, WeaponAttackData, WeaponData}; use super::{PossessionData, PossessionType, WeaponAttackData, WeaponData};
use crate::models::item::SkillType; use crate::{models::item::SkillType, static_content::possession_type::DamageType};
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> { pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
@ -37,11 +37,252 @@ pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
), ),
mean_damage: 2.0, mean_damage: 2.0,
stdev_damage: 2.0, stdev_damage: 2.0,
base_damage_type: DamageType::Pierce,
other_damage_types: vec!((0.5, DamageType::Slash)),
..Default::default() ..Default::default()
}, },
..Default::default() ..Default::default()
}), }),
..Default::default() ..Default::default()
}) }),
(PossessionType::Dagger,
PossessionData {
display: "dagger",
details: "A 30 cm long stainless steel blade, sharp on two edges with a pointy tip, this weapon looks ideal for getting started with bladed close combat.",
weight: 250,
can_butcher: true,
weapon_data: Some(WeaponData {
uses_skill: SkillType::Blades,
raw_min_to_learn: 1.0,
raw_max_to_learn: 3.0,
normal_attack: WeaponAttackData {
start_messages: vec!(
Box::new(|attacker, victim, exp|
format!("{} points {} dagger menancingly, preparing to attack {}",
&attacker.display_for_sentence(exp, 1, true),
&attacker.pronouns.possessive,
&victim.display_for_sentence(exp, 1, false),
)
)
),
success_messages: vec!(
Box::new(|attacker, victim, part, exp|
format!("{}'s dagger cuts into {}'s {}",
&attacker.display_for_sentence(exp, 1, true),
&victim.display_for_sentence(exp, 1, false),
&part.display(victim.sex.clone())
)
)
),
mean_damage: 3.0,
stdev_damage: 2.0,
base_damage_type: DamageType::Slash,
other_damage_types: vec!((1.0, DamageType::Pierce)),
..Default::default()
},
..Default::default()
}),
..Default::default()
}),
(PossessionType::SawtoothMachete,
PossessionData {
display: "sawtooth machete",
details: "A menacing 45 cm long heavy steel blade, with a very sharp serated edge. You could theoretically use this to harvest sugarcane, cutting ruthlessly through the heavy stalks with one hard blow... but it could also be a very formidable weapon in skilled hands.",
aliases: vec!("machete"),
weight: 900,
can_butcher: true,
weapon_data: Some(WeaponData {
uses_skill: SkillType::Blades,
raw_min_to_learn: 2.0,
raw_max_to_learn: 4.0,
normal_attack: WeaponAttackData {
start_messages: vec!(
Box::new(|attacker, victim, exp|
format!("{} grasps {} machete, preparing to strike {}",
&attacker.display_for_sentence(exp, 1, true),
&attacker.pronouns.possessive,
&victim.display_for_sentence(exp, 1, false),
)
)
),
success_messages: vec!(
Box::new(|attacker, victim, part, exp|
format!("{}'s machete hits {}'s {} with a cutting blow",
&attacker.display_for_sentence(exp, 1, true),
&victim.display_for_sentence(exp, 1, false),
&part.display(victim.sex.clone())
)
)
),
mean_damage: 5.0,
stdev_damage: 3.0,
base_damage_type: DamageType::Slash,
other_damage_types: vec!((2.0, DamageType::Beat)),
..Default::default()
},
..Default::default()
}),
..Default::default()
}),
(PossessionType::RusticKatana,
PossessionData {
display: "rustic katana",
details: "A curved 70 cm long steel blade, with a wooden handle wound with cord. It is engraved with 'J. Hannow - Blacksmith. This weapon is clearly a post-apocalyptic creation - it looks a bit, well, rustic, but a big sharp blade of this kind is undoubtedly lethal in the right hands",
aliases: vec!("katana", "sword"),
weight: 1000,
can_butcher: false,
weapon_data: Some(WeaponData {
uses_skill: SkillType::Blades,
raw_min_to_learn: 4.0,
raw_max_to_learn: 8.0,
normal_attack: WeaponAttackData {
start_messages: vec!(
Box::new(|attacker, victim, exp|
format!("{} raises {} katana menancingly, preparing to attack {}",
&attacker.display_for_sentence(exp, 1, true),
&attacker.pronouns.possessive,
&victim.display_for_sentence(exp, 1, false),
)
)
),
success_messages: vec!(
Box::new(|attacker, victim, part, exp|
format!("{}'s katana slashes into {}'s {}",
&attacker.display_for_sentence(exp, 1, true),
&victim.display_for_sentence(exp, 1, false),
&part.display(victim.sex.clone())
)
)
),
mean_damage: 8.0,
stdev_damage: 3.0,
base_damage_type: DamageType::Slash,
other_damage_types: vec!((3.0, DamageType::Pierce)),
..Default::default()
},
..Default::default()
}),
..Default::default()
}),
(PossessionType::Electroblade,
PossessionData {
display: "electroblade",
details: "At first sight, this is an ordinary 70 cm long steel sword, except that it blue sparks arc from its blade, leaving a smell of ozone. This appears to be a weapon that, in skilled hands, will simultaneously slash and electrocute anyone unfortunate to be on the receiving end",
aliases: vec!("sword", "blade"),
weight: 1100,
can_butcher: true,
weapon_data: Some(WeaponData {
uses_skill: SkillType::Blades,
raw_min_to_learn: 7.0,
raw_max_to_learn: 13.0,
normal_attack: WeaponAttackData {
start_messages: vec!(
Box::new(|attacker, victim, exp|
format!("{} flicks {} electroblade, causing it to spark in anticipation of attacking {}",
&attacker.display_for_sentence(exp, 1, true),
&attacker.pronouns.possessive,
&victim.display_for_sentence(exp, 1, false),
)
)
),
success_messages: vec!(
Box::new(|attacker, victim, part, exp|
format!("{}'s electroblade zaps {}'s {}",
&attacker.display_for_sentence(exp, 1, true),
&victim.display_for_sentence(exp, 1, false),
&part.display(victim.sex.clone())
)
)
),
mean_damage: 13.0,
stdev_damage: 7.0,
base_damage_type: DamageType::Shock,
other_damage_types: vec!((3.0, DamageType::Slash)),
..Default::default()
},
..Default::default()
}),
..Default::default()
}),
(PossessionType::FlameScimitar,
PossessionData {
display: "flame scimitar",
details: "A slender curved sword, with the curious property that it glows red hot merely from being swung. In the hands of a blades expert, this would probably through flesh like butter.",
aliases: vec!("butcher", "knife"),
weight: 800,
can_butcher: false,
weapon_data: Some(WeaponData {
uses_skill: SkillType::Blades,
raw_min_to_learn: 12.0,
raw_max_to_learn: 14.0,
normal_attack: WeaponAttackData {
start_messages: vec!(
Box::new(|attacker, victim, exp|
format!("{} raises {} flame scimitar, swiping it through the air and making it glow red hot, in preparation to attack {}",
&attacker.display_for_sentence(exp, 1, true),
&attacker.pronouns.possessive,
&victim.display_for_sentence(exp, 1, false),
)
)
),
success_messages: vec!(
Box::new(|attacker, victim, part, exp|
format!("{}'s flame scimitar slices into {}'s {}",
&attacker.display_for_sentence(exp, 1, true),
&victim.display_for_sentence(exp, 1, false),
&part.display(victim.sex.clone())
)
)
),
mean_damage: 15.0,
stdev_damage: 8.0,
base_damage_type: DamageType::Slash,
other_damage_types: vec!((3.0, DamageType::Pierce)),
..Default::default()
},
..Default::default()
}),
..Default::default()
}),
(PossessionType::NanobladeGladius,
PossessionData {
display: "nanoblade gladius",
details: "It looks at first to be an ordinary sword, only 60 cm long... but one which you quickly realise is actually the legendary nanoblade gladius; a lethal combination of an ancient sword design, with modern pre-fall nanotechnology employed to keep the blade hard and atomically sharp so it can slice through nearly anything, and also inflict terrible damage to any tissue it so much as grazes",
aliases: vec!("gladius", "sword"),
weight: 700,
can_butcher: true,
weapon_data: Some(WeaponData {
uses_skill: SkillType::Blades,
raw_min_to_learn: 13.0,
raw_max_to_learn: 100.0,
normal_attack: WeaponAttackData {
start_messages: vec!(
Box::new(|attacker, victim, exp|
format!("{} raises {} nanoblade gladius menancingly, preparing to attack {}",
&attacker.display_for_sentence(exp, 1, true),
&attacker.pronouns.possessive,
&victim.display_for_sentence(exp, 1, false),
)
)
),
success_messages: vec!(
Box::new(|attacker, victim, part, exp|
format!("{}'s nanoblade cuts into {}'s {}, activating the horrendous power of the nanites",
&attacker.display_for_sentence(exp, 1, true),
&victim.display_for_sentence(exp, 1, false),
&part.display(victim.sex.clone())
)
)
),
mean_damage: 25.0,
stdev_damage: 5.0,
base_damage_type: DamageType::Slash,
other_damage_types: vec!((10.0, DamageType::Pierce)),
..Default::default()
},
..Default::default()
}),
..Default::default()
}),
)) ))
} }

View File

@ -1663,7 +1663,12 @@ pub fn room_list() -> Vec<Room> {
possession_type: PossessionType::AntennaWhip, possession_type: PossessionType::AntennaWhip,
list_price: 100, list_price: 100,
..Default::default() ..Default::default()
} },
RoomStock {
possession_type: PossessionType::Dagger,
list_price: 90,
..Default::default()
},
), ),
..Default::default() ..Default::default()
}, },

View File

@ -17,3 +17,8 @@ select count(*) from items i where details->>'item_type' = 'corpse' and not exis
This is expected to be 0 - haven't seen any instances of it deviating, so any bugs seen involving corpses probably aren't due to stale data. This is expected to be 0 - haven't seen any instances of it deviating, so any bugs seen involving corpses probably aren't due to stale data.
## NPC combat non-symmetrical attacking / attacked_by
select i1.details->>'item_code' as i1_code, i1.details->>'active_combat' as i1_combat, i1.details->>'location' as i1_loc, i2.details->>'item_code' as i2_code, i2.details->>'active_combat' as i2_combat, i2.details->>'location' as i2_loc from items i1 join items i2 on i2.details->>'item_type' = (regexp_split_to_array(i1.details->'active_combat'->>'attacking', '/'))[1] and i2.details->>'item_code' = (regexp_split_to_array(i1.details->'active_combat'->>'attacking', '/'))[2] where not exists (select 1 from jsonb_array_elements_text(i2.details->'active_combat'->'attacked_by') e where e = ((i1.details->>'item_type') || '/' || (i1.details->>'item_code')));
This should be empty, but there has been a bug breaking this.