diff --git a/assets/voxygen/i18n/en/command.ftl b/assets/voxygen/i18n/en/command.ftl index b840ca39cd..963caa7360 100644 --- a/assets/voxygen/i18n/en/command.ftl +++ b/assets/voxygen/i18n/en/command.ftl @@ -64,7 +64,9 @@ command-battlemode-available-modes = Available modes: pvp, pve command-battlemode-same = Attempted to set the same battlemode command-battlemode-updated = New battlemode: { $battlemode } command-buff-unknown = Unknown buff: { $buff } -command-buff-complex = /buff doesn't work with this buff, use /buff_complex +command-buff-complex = /buff doesn't work with [{ $buff }], use /buff_complex +command-buff-simple = /buff_complex doesn't work with [{ $buff }], use /buff +command-buff-body-unknown = Unknown body spec: { $spec } command-skillpreset-load-error = Error while loading presets command-skillpreset-broken = Skill preset is broken command-skillpreset-missing = Preset does not exist: { $preset } diff --git a/common/src/cmd.rs b/common/src/cmd.rs index a41a9c93b7..a7d7228490 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -200,13 +200,23 @@ lazy_static! { buff_pack }; - static ref BUFFS: Vec = { - let mut buff_pack: Vec<_> = BUFF_PARSER.keys().cloned().collect(); - // Add all as valid command + static ref BUFFS_SIMPLE: Vec = { + let mut buff_pack: Vec = BUFF_PARSER + .iter() + .filter_map(|(key, kind)| kind.is_simple().then_some(key.clone())) + .collect(); + + // Add `all` as valid command buff_pack.push("all".to_string()); buff_pack }; + static ref BUFFS_COMPLEX: Vec = + BUFF_PARSER + .iter() + .filter_map(|(key, kind)| (!kind.is_simple()).then_some(key.clone())) + .collect(); + static ref BLOCK_KINDS: Vec = terrain::block::BlockKind::iter() .map(|bk| bk.to_string()) .collect(); @@ -312,6 +322,7 @@ pub enum ServerChatCommand { BattleModeForce, Body, Buff, + BuffComplex, Build, Campfire, CreateLocation, @@ -426,13 +437,23 @@ impl ServerChatCommand { ), ServerChatCommand::Buff => cmd( vec![ - Enum("buff", BUFFS.clone(), Required), + Enum("buff", BUFFS_SIMPLE.clone(), Required), Float("strength", 0.01, Optional), Float("duration", 10.0, Optional), ], "Cast a buff on player", Some(Admin), ), + ServerChatCommand::BuffComplex => cmd( + vec![ + Enum("buff", BUFFS_COMPLEX.clone(), Required), + Any("buff data spec", Required), + Float("strength", 0.01, Optional), + Float("duration", 10.0, Optional), + ], + "Cast a complex buff on player", + Some(Admin), + ), ServerChatCommand::Ban => cmd( vec![ PlayerName(Required), @@ -934,6 +955,7 @@ impl ServerChatCommand { ServerChatCommand::BattleModeForce => "battlemode_force", ServerChatCommand::Body => "body", ServerChatCommand::Buff => "buff", + ServerChatCommand::BuffComplex => "buff_complex", ServerChatCommand::Build => "build", ServerChatCommand::AreaAdd => "area_add", ServerChatCommand::AreaList => "area_list", diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 8c559a80ee..afe0a243cc 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -26,7 +26,7 @@ use common::{ }, comp::{ self, - buff::{Buff, BuffData, BuffKind, BuffSource}, + buff::{Buff, BuffData, BuffKind, BuffSource, MiscBuffData}, inventory::{ item::{tool::AbilityMap, MaterialStatManifest, Quality}, slot::Slot, @@ -131,6 +131,7 @@ fn do_command( ServerChatCommand::BattleModeForce => handle_battlemode_force, ServerChatCommand::Body => handle_body, ServerChatCommand::Buff => handle_buff, + ServerChatCommand::BuffComplex => handle_buff_complex, ServerChatCommand::Build => handle_build, ServerChatCommand::AreaAdd => handle_area_add, ServerChatCommand::AreaList => handle_area_list, @@ -4140,49 +4141,116 @@ fn handle_buff( args: Vec, action: &ServerChatCommand, ) -> CmdResult<()> { - if let (Some(buff), strength, duration) = parse_cmd_args!(args, String, f32, f64) { - let strength = strength.unwrap_or(0.01); - let duration = duration.unwrap_or(1.0); - let buffdata = BuffData::new(strength, Some(Secs(duration))); - if buff != "all" { - let buffkind = parse_buffkind(&buff).ok_or_else(|| { - Content::localized_with_args("command-buff-unknown", [("buff", buff.clone())]) - })?; + let (Some(buff), strength, duration) = parse_cmd_args!(args, String, f32, f64) else { + return Err(Content::Plain(action.help_string())); + }; - if buffkind.is_simple() { - cast_buff(buffkind, buffdata, server, target) - } else { - return Err(Content::localized_with_args("command-buff-complex", [( - "buff", buff, - )])); - } - } else { - for kind_key in BUFF_PACK.iter() { - let buffkind = parse_buffkind(kind_key).ok_or_else(|| { - Content::localized_with_args("command-buff-unknown", [( - "buff", - kind_key.to_owned(), - )]) - })?; + let strength = strength.unwrap_or(0.01); + let duration = duration.unwrap_or(1.0); + let buffdata = BuffData::new(strength, Some(Secs(duration))); - // Execute only simple buffs, ignore complex - if buffkind.is_simple() { - cast_buff(buffkind, buffdata, server, target)?; - } - } - Ok(()) - } + if buff == "all" { + BUFF_PACK + .iter() + .filter_map(|kind_key| parse_buffkind(kind_key)) + .filter(|buffkind| buffkind.is_simple()) + .for_each(|buffkind| cast_buff(buffkind, buffdata, server, target)); } else { - Err(Content::Plain(action.help_string())) + let buffkind = parse_buffkind(&buff).ok_or_else(|| { + Content::localized_with_args("command-buff-unknown", [("buff", buff.clone())]) + })?; + + if !buffkind.is_simple() { + return Err(Content::localized_with_args("command-buff-complex", [( + "buff", buff, + )])); + } + + cast_buff(buffkind, buffdata, server, target); } + + Ok(()) } -fn cast_buff( - buffkind: BuffKind, - data: BuffData, +fn handle_buff_complex( server: &mut Server, + _client: EcsEntity, target: EcsEntity, + args: Vec, + action: &ServerChatCommand, ) -> CmdResult<()> { + let (Some(buff), Some(spec), strength, duration) = + parse_cmd_args!(args, String, String, f32, f64) + else { + return Err(Content::Plain(action.help_string())); + }; + + let buffkind = parse_buffkind(&buff).ok_or_else(|| { + Content::localized_with_args("command-buff-unknown", [("buff", buff.clone())]) + })?; + + if buffkind.is_simple() { + return Err(Content::localized_with_args("command-buff-simple", [( + "buff", buff, + )])); + } + + // 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"), + }; + + let strength = strength.unwrap_or(0.01); + let duration = duration.unwrap_or(20.0); + + let buffdata = BuffData::new(strength, Some(Secs(duration))).with_misc_data(misc_data); + + cast_buff(buffkind, buffdata, server, target); + Ok(()) +} + +fn cast_buff(buffkind: BuffKind, data: BuffData, server: &mut Server, target: EcsEntity) { let ecs = &server.state.ecs(); let mut buffs_all = ecs.write_storage::(); let stats = ecs.read_storage::(); @@ -4202,8 +4270,6 @@ fn cast_buff( *time, ); } - - Ok(()) } fn parse_buffkind(buff: &str) -> Option { BUFF_PARSER.get(buff).copied() }