/aura command

This commit is contained in:
crabman 2024-03-22 22:36:11 +00:00
parent af4f147fda
commit 8dd7e3e0d5
No known key found for this signature in database
5 changed files with 269 additions and 76 deletions

View File

@ -85,6 +85,9 @@ command-message-group-missing = You are using group chat but do not belong to a
/region to change chat.
command-tell-request = { $sender } wants to talk to you.
command-transform-invalid-presence = Cannot transform in the current presence
command-aura-invalid-buff-parameters = Invalid buff parameters for aura
command-aura-spawn = Spawned new aura attached to entity
command-aura-spawn-new-entity = Spawned new aura
# Unreachable/untestable but added for consistency

View File

@ -1,6 +1,12 @@
use crate::{
assets::{self, AssetCombined, Concatenate},
comp::{self, buff::BuffKind, inventory::item::try_all_item_defs, AdminRole as Role, Skill},
comp::{
self,
aura::{AuraKindVariant, SimpleAuraTarget},
buff::BuffKind,
inventory::item::try_all_item_defs,
AdminRole as Role, Skill,
},
generation::try_all_entity_configs,
npc, terrain,
};
@ -313,6 +319,7 @@ pub enum ServerChatCommand {
AreaAdd,
AreaList,
AreaRemove,
Aura,
Ban,
BattleMode,
BattleModeForce,
@ -431,6 +438,18 @@ impl ServerChatCommand {
"Change your alias",
Some(Moderator),
),
ServerChatCommand::Aura => cmd(
vec![
Float("aura_radius", 10.0, Required),
Float("aura_duration", 10.0, Optional),
Boolean("new_entity", "true".to_string(), Optional),
Enum("aura_target", SimpleAuraTarget::all_options(), Optional),
Enum("aura_kind", AuraKindVariant::all_options(), Required),
Any("aura spec", Optional),
],
"Create an aura",
Some(Admin),
),
ServerChatCommand::Buff => cmd(
vec![
Enum("buff", BUFFS.clone(), Required),
@ -955,6 +974,7 @@ impl ServerChatCommand {
ServerChatCommand::AreaAdd => "area_add",
ServerChatCommand::AreaList => "area_list",
ServerChatCommand::AreaRemove => "area_remove",
ServerChatCommand::Aura => "aura",
ServerChatCommand::Ban => "ban",
ServerChatCommand::BattleMode => "battlemode",
ServerChatCommand::BattleModeForce => "battlemode_force",
@ -1285,6 +1305,45 @@ impl ArgumentSpec {
}
}
pub trait CommandEnumArg: FromStr {
fn all_options() -> Vec<String>;
}
macro_rules! impl_from_to_str_cmd {
($enum:ident, ($($attribute:ident => $str:expr),*)) => {
impl std::str::FromStr for $enum {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
$(
$str => Ok($enum::$attribute),
)*
s => Err(format!("Invalid variant: {s}")),
}
}
}
impl $crate::cmd::CommandEnumArg for $enum {
fn all_options() -> Vec<String> {
vec![$($str.to_string()),*]
}
}
}
}
impl_from_to_str_cmd!(AuraKindVariant, (
Buff => "buff",
FriendlyFire => "friendly_fire",
IgnorePvE => "ingore_pve"
));
impl_from_to_str_cmd!(SimpleAuraTarget, (
Group => "group",
OutOfGroup => "out_of_group",
All => "all"
));
/// Parse a series of command arguments into values, including collecting all
/// trailing arguments.
#[macro_export]

View File

@ -28,6 +28,7 @@ use common::{
},
comp::{
self,
aura::{AuraKindVariant, AuraTarget, SimpleAuraTarget},
buff::{Buff, BuffData, BuffKind, BuffSource, MiscBuffData},
inventory::{
item::{all_items_expect, tool::AbilityMap, MaterialStatManifest, Quality},
@ -35,7 +36,8 @@ use common::{
},
invite::InviteKind,
misc::PortalData,
AdminRole, ChatType, Content, Inventory, Item, LightEmitter, WaypointArea,
AdminRole, Aura, AuraKind, BuffCategory, ChatType, Content, Inventory, Item, LightEmitter,
WaypointArea,
},
depot,
effect::Effect,
@ -67,7 +69,7 @@ use hashbrown::{HashMap, HashSet};
use humantime::Duration as HumanDuration;
use rand::{thread_rng, Rng};
use specs::{storage::StorageEntry, Builder, Entity as EcsEntity, Join, LendJoin, WorldExt};
use std::{fmt::Write, ops::DerefMut, str::FromStr, sync::Arc};
use std::{fmt::Write, ops::DerefMut, str::FromStr, sync::Arc, time::Duration};
use vek::*;
use wiring::{Circuit, Wire, WireNode, WiringAction, WiringActionEffect, WiringElement};
use world::util::{Sampler, LOCALITY};
@ -134,6 +136,7 @@ fn do_command(
ServerChatCommand::AreaAdd => handle_area_add,
ServerChatCommand::AreaList => handle_area_list,
ServerChatCommand::AreaRemove => handle_area_remove,
ServerChatCommand::Aura => handle_aura,
ServerChatCommand::Ban => handle_ban,
ServerChatCommand::BattleMode => handle_battlemode,
ServerChatCommand::BattleModeForce => handle_battlemode_force,
@ -3967,6 +3970,116 @@ fn handle_ban(
}
}
fn handle_aura(
server: &mut Server,
client: EcsEntity,
target: EcsEntity,
args: Vec<String>,
action: &ServerChatCommand,
) -> CmdResult<()> {
let target_uid = uid(server, target, "target")?;
let (Some(aura_radius), aura_duration, new_entity, aura_target, Some(aura_kind_variant), spec) =
parse_cmd_args!(args, f32, f32, bool, SimpleAuraTarget, AuraKindVariant, ..Vec<String>)
else {
return Err(Content::Plain(action.help_string()));
};
let (new_entity, aura_target) = (
new_entity.unwrap_or(false),
aura_target.unwrap_or(SimpleAuraTarget::OutOfGroup),
);
let aura_kind = match aura_kind_variant {
AuraKindVariant::Buff => {
let (Some(buff), strength, duration, misc_data_spec) =
parse_cmd_args!(spec, String, f32, f64, String)
else {
return Err(Content::localized("command-aura-invalid-buff-parameters"));
};
let buffkind = parse_buffkind(&buff).ok_or_else(|| {
Content::localized_with_args("command-buff-unknown", [("buff", buff.clone())])
})?;
let buffdata = build_buff(
buffkind,
strength.unwrap_or(1.0),
duration.unwrap_or(10.0),
(!buffkind.is_simple())
.then(|| {
misc_data_spec.ok_or_else(|| {
Content::localized_with_args("command-buff-data", [(
"buff",
buff.clone(),
)])
})
})
.transpose()?,
)?;
AuraKind::Buff {
kind: buffkind,
data: buffdata,
category: BuffCategory::Natural,
source: if new_entity {
BuffSource::World
} else {
BuffSource::Character { by: target_uid }
},
}
},
AuraKindVariant::FriendlyFire => AuraKind::FriendlyFire,
AuraKindVariant::IgnorePvE => AuraKind::ForcePvP,
};
let aura_target = server
.state
.read_component_copied::<Uid>(target)
.map(|uid| match aura_target {
SimpleAuraTarget::Group => AuraTarget::GroupOf(uid),
SimpleAuraTarget::OutOfGroup => AuraTarget::NotGroupOf(uid),
SimpleAuraTarget::All => AuraTarget::All,
})
.unwrap_or(AuraTarget::All);
let time = Time(server.state.get_time());
let aura = Aura::new(
aura_kind,
aura_radius,
aura_duration.map(|duration| Secs(duration as f64)),
aura_target,
time,
);
if new_entity {
let pos = position(server, target, "target")?;
server
.state
.create_empty(pos)
.with(comp::Auras::new(vec![aura]))
.maybe_with(aura_duration.map(|duration| comp::Object::DeleteAfter {
spawned_at: time,
timeout: Duration::from_secs_f32(duration),
}))
.build();
} else {
let mut auras = server.state.ecs().write_storage::<comp::Auras>();
if let Some(mut auras) = auras.get_mut(target) {
auras.insert(aura);
}
}
server.notify_client(
client,
ServerGeneral::server_msg(
ChatType::CommandInfo,
Content::localized(if new_entity {
"command-aura-spawn-new-entity"
} else {
"command-aura-spawn"
}),
),
);
Ok(())
}
fn handle_battlemode(
server: &mut Server,
client: EcsEntity,
@ -4222,81 +4335,89 @@ fn handle_buff(
let buffkind = parse_buffkind(&buff).ok_or_else(|| {
Content::localized_with_args("command-buff-unknown", [("buff", buff.clone())])
})?;
let buffdata = build_buff(
buffkind,
strength,
duration.unwrap_or(10.0),
(!buffkind.is_simple())
.then(|| {
misc_data_spec.ok_or_else(|| {
Content::localized_with_args("command-buff-data", [("buff", buff.clone())])
})
})
.transpose()?,
)?;
if buffkind.is_simple() {
let duration = duration.unwrap_or(10.0);
let buffdata = BuffData::new(strength, Some(Secs(duration)));
cast_buff(buffkind, buffdata, server, target);
Ok(())
} else {
// default duration is longer for complex buffs
let duration = duration.unwrap_or(20.0);
let spec = misc_data_spec.ok_or_else(|| {
Content::localized_with_args("command-buff-data", [("buff", buff.clone())])
})?;
cast_buff_complex(buffkind, server, target, spec, strength, duration)
}
cast_buff(buffkind, buffdata, server, target);
Ok(())
}
}
fn cast_buff_complex(
buffkind: BuffKind,
server: &mut Server,
target: EcsEntity,
spec: String,
fn build_buff(
buff_kind: BuffKind,
strength: f32,
duration: f64,
) -> CmdResult<()> {
// explicit match to remember that this function exists
let misc_data = match buffkind {
BuffKind::Polymorphed => {
let Ok(npc::NpcBody(_id, mut body)) = spec.parse() else {
return Err(Content::localized_with_args("command-buff-body-unknown", [
("spec", spec.clone()),
]));
};
MiscBuffData::Body(body())
},
BuffKind::Regeneration
| BuffKind::Saturation
| BuffKind::Potion
| BuffKind::Agility
| BuffKind::CampfireHeal
| BuffKind::Frenzied
| BuffKind::EnergyRegen
| BuffKind::IncreaseMaxEnergy
| BuffKind::IncreaseMaxHealth
| BuffKind::Invulnerability
| BuffKind::ProtectingWard
| BuffKind::Hastened
| BuffKind::Fortitude
| BuffKind::Reckless
| BuffKind::Flame
| BuffKind::Frigid
| BuffKind::Lifesteal
| BuffKind::ImminentCritical
| BuffKind::Fury
| BuffKind::Sunderer
| BuffKind::Defiance
| BuffKind::Bloodfeast
| BuffKind::Berserk
| BuffKind::Bleeding
| BuffKind::Cursed
| BuffKind::Burning
| BuffKind::Crippled
| BuffKind::Frozen
| BuffKind::Wet
| BuffKind::Ensnared
| BuffKind::Poisoned
| BuffKind::Parried
| BuffKind::PotionSickness
| BuffKind::Heatstroke => unreachable!("is_simple() above"),
};
spec: Option<String>,
) -> CmdResult<BuffData> {
if buff_kind.is_simple() {
Ok(BuffData::new(strength, Some(Secs(duration))))
} else {
let spec = spec.expect("spec must be passed to build_buff if buff_kind is not simple");
let buffdata = BuffData::new(strength, Some(Secs(duration))).with_misc_data(misc_data);
// Explicit match to remember that this function exists
let misc_data = match buff_kind {
BuffKind::Polymorphed => {
let Ok(npc::NpcBody(_id, mut body)) = spec.parse() else {
return Err(Content::localized_with_args("command-buff-body-unknown", [
("spec", spec.clone()),
]));
};
MiscBuffData::Body(body())
},
BuffKind::Regeneration
| BuffKind::Saturation
| BuffKind::Potion
| BuffKind::Agility
| BuffKind::CampfireHeal
| BuffKind::Frenzied
| BuffKind::EnergyRegen
| BuffKind::IncreaseMaxEnergy
| BuffKind::IncreaseMaxHealth
| BuffKind::Invulnerability
| BuffKind::ProtectingWard
| BuffKind::Hastened
| BuffKind::Fortitude
| BuffKind::Reckless
| BuffKind::Flame
| BuffKind::Frigid
| BuffKind::Lifesteal
| BuffKind::ImminentCritical
| BuffKind::Fury
| BuffKind::Sunderer
| BuffKind::Defiance
| BuffKind::Bloodfeast
| BuffKind::Berserk
| BuffKind::Bleeding
| BuffKind::Cursed
| BuffKind::Burning
| BuffKind::Crippled
| BuffKind::Frozen
| BuffKind::Wet
| BuffKind::Ensnared
| BuffKind::Poisoned
| BuffKind::Parried
| BuffKind::PotionSickness
| BuffKind::Heatstroke => {
if buff_kind.is_simple() {
unreachable!("is_simple() above")
} else {
panic!("Buff Kind {buff_kind:?} is complex but has no defined spec parser")
}
},
};
cast_buff(buffkind, buffdata, server, target);
Ok(())
Ok(BuffData::new(strength, Some(Secs(duration))).with_misc_data(misc_data))
}
}
fn cast_buff(buffkind: BuffKind, data: BuffData, server: &mut Server, target: EcsEntity) {

View File

@ -63,6 +63,8 @@ pub trait StateExt {
inventory: Inventory,
body: comp::Body,
) -> EcsEntityBuilder;
/// Create an entity with only a position
fn create_empty(&mut self, pos: comp::Pos) -> EcsEntityBuilder;
/// Build a static object entity
fn create_object(&mut self, pos: comp::Pos, object: comp::object::Body) -> EcsEntityBuilder;
/// Create an item drop or merge the item with an existing drop, if a
@ -314,16 +316,21 @@ impl StateExt for State {
.with(comp::Buffs::default())
.with(comp::Combo::default())
.with(comp::Auras::default())
.with(comp::EnteredAuras::default())
.with(comp::Stance::default())
}
fn create_object(&mut self, pos: comp::Pos, object: comp::object::Body) -> EcsEntityBuilder {
let body = comp::Body::Object(object);
fn create_empty(&mut self, pos: comp::Pos) -> EcsEntityBuilder {
self.ecs_mut()
.create_entity_synced()
.with(pos)
.with(comp::Vel(Vec3::zero()))
.with(comp::Ori::default())
}
fn create_object(&mut self, pos: comp::Pos, object: comp::object::Body) -> EcsEntityBuilder {
let body = comp::Body::Object(object);
self.create_empty(pos)
.with(body.mass())
.with(body.density())
.with(body.collider())
@ -628,6 +635,7 @@ impl StateExt for State {
self.write_component_ignore_entity_dead(entity, comp::Alignment::Owned(player_uid));
self.write_component_ignore_entity_dead(entity, comp::Buffs::default());
self.write_component_ignore_entity_dead(entity, comp::Auras::default());
self.write_component_ignore_entity_dead(entity, comp::EnteredAuras::default());
self.write_component_ignore_entity_dead(entity, comp::Combo::default());
self.write_component_ignore_entity_dead(entity, comp::Stance::default());

View File

@ -68,15 +68,15 @@ impl<'a> System<'a> for Sys {
&entities,
&positions,
&velocities,
&physics_states,
physics_states.maybe(),
&objects,
&bodies,
bodies.maybe(),
)
.join()
{
match object {
Object::Bomb { owner } => {
if physics.on_surface().is_some() {
if physics.is_some_and(|physics| physics.on_surface().is_some()) {
emitters.emit(DeleteEvent(entity));
emitters.emit(ExplosionEvent {
pos: pos.0,
@ -213,7 +213,9 @@ impl<'a> System<'a> for Sys {
})
});
if (*body == Body::Object(object::Body::PortalActive)) != is_active {
if body.is_some_and(|body| {
(*body == Body::Object(object::Body::PortalActive)) != is_active
}) {
emitters.emit(ChangeBodyEvent {
entity,
new_body: Body::Object(if is_active {