Merge branch 'juliancoffee/balance_update' into 'master'

Abilitiy tweaks and /make_npc command

See merge request veloren/veloren!2769
This commit is contained in:
Samuel Keiffer 2021-08-16 16:06:59 +00:00
commit 5116c31f20
21 changed files with 339 additions and 180 deletions

View File

@ -2,7 +2,7 @@ ComboMelee(
stage_data: [
(
stage: 1,
base_damage: 90,
base_damage: 110,
base_poise_damage: 12,
damage_increase: 10,
poise_damage_increase: 0,

View File

@ -14,6 +14,7 @@ DashMelee(
charge_duration: 3.0,
swing_duration: 0.35,
recover_duration: 1.2,
ori_modifier: 0.3,
charge_through: false,
is_interruptible: true,
damage_kind: Slashing,

View File

@ -14,6 +14,7 @@ DashMelee(
charge_duration: 1.0,
swing_duration: 0.1,
recover_duration: 1.0,
ori_modifier: 0.3,
charge_through: false,
is_interruptible: false,
damage_kind: Crushing,

View File

@ -14,6 +14,7 @@ DashMelee(
charge_duration: 3.0,
swing_duration: 0.1,
recover_duration: 0.7,
ori_modifier: 0.3,
charge_through: false,
is_interruptible: false,
damage_kind: Crushing,

View File

@ -14,6 +14,7 @@ DashMelee(
charge_duration: 4.0,
swing_duration: 0.1,
recover_duration: 0.5,
ori_modifier: 0.3,
charge_through: false,
is_interruptible: false,
damage_kind: Piercing,

View File

@ -14,6 +14,7 @@ DashMelee(
charge_duration: 1.0,
swing_duration: 0.1,
recover_duration: 1.0,
ori_modifier: 0.3,
charge_through: false,
is_interruptible: false,
damage_kind: Crushing,

View File

@ -14,6 +14,7 @@ DashMelee(
charge_duration: 0.8,
swing_duration: 0.1,
recover_duration: 1.0,
ori_modifier: 0.3,
charge_through: false,
is_interruptible: false,
damage_kind: Crushing,

View File

@ -14,6 +14,7 @@ DashMelee(
charge_duration: 1.2,
swing_duration: 0.1,
recover_duration: 1.1,
ori_modifier: 0.3,
charge_through: false,
is_interruptible: false,
damage_kind: Crushing,

View File

@ -14,6 +14,7 @@ DashMelee(
charge_duration: 1.0,
swing_duration: 0.1,
recover_duration: 1.0,
ori_modifier: 0.3,
charge_through: false,
is_interruptible: false,
damage_kind: Crushing,

View File

@ -14,6 +14,7 @@ DashMelee(
charge_duration: 1.2,
swing_duration: 0.1,
recover_duration: 1.1,
ori_modifier: 0.3,
charge_through: false,
is_interruptible: false,
damage_kind: Crushing,

View File

@ -14,6 +14,7 @@ DashMelee(
charge_duration: 2.0,
swing_duration: 0.1,
recover_duration: 0.5,
ori_modifier: 0.3,
charge_through: false,
is_interruptible: false,
damage_kind: Crushing,

View File

@ -3,8 +3,8 @@ ChargedMelee(
energy_drain: 300,
initial_damage: 10,
scaled_damage: 160,
initial_poise_damage: 20,
scaled_poise_damage: 60,
initial_poise_damage: 5,
scaled_poise_damage: 75,
initial_knockback: 5.0,
scaled_knockback: 20.0,
range: 3.5,

View File

@ -14,6 +14,7 @@ DashMelee(
charge_duration: 1.0,
swing_duration: 0.1,
recover_duration: 0.8,
ori_modifier: 0.3,
charge_through: false,
is_interruptible: true,
damage_kind: Piercing,

View File

@ -14,6 +14,7 @@ DashMelee(
charge_duration: 1.2,
swing_duration: 0.1,
recover_duration: 0.5,
ori_modifier: 0.3,
charge_through: true,
is_interruptible: true,
damage_kind: Piercing,

View File

@ -14,6 +14,7 @@ DashMelee(
charge_duration: 1.2,
swing_duration: 0.1,
recover_duration: 0.9,
ori_modifier: 0.3,
charge_through: false,
is_interruptible: true,
damage_kind: Piercing,

View File

@ -1,6 +1,7 @@
use crate::{
assets,
comp::{self, buff::BuffKind, inventory::item::try_all_item_defs, AdminRole as Role, Skill},
generation::try_all_entity_configs,
npc, terrain,
};
use assets::AssetExt;
@ -76,6 +77,7 @@ pub enum ChatCommand {
Lantern,
Light,
MakeBlock,
MakeNpc,
MakeSprite,
Motd,
Object,
@ -242,6 +244,15 @@ lazy_static! {
items
};
/// List of all entity configs. Useful for tab completing
static ref ENTITY_CONFIGS: Vec<String> = {
try_all_entity_configs()
.unwrap_or_else(|e| {
warn!(?e, "Failed to load entity configs");
Vec::new()
})
};
static ref KITS: Vec<String> = {
if let Ok(kits) = KitManifest::load(KIT_MANIFEST_PATH) {
kits.read().0.keys().cloned().collect()
@ -461,6 +472,14 @@ impl ChatCommand {
"Make a block at your location with a color",
Some(Admin),
),
ChatCommand::MakeNpc => cmd(
vec![
Enum("entity_config", ENTITY_CONFIGS.clone(), Required),
Integer("num", 1, Optional),
],
"Spawn entity from config near you",
Some(Admin),
),
ChatCommand::MakeSprite => cmd(
vec![Enum("sprite", SPRITE_KINDS.clone(), Required)],
"Make a sprite at your location",
@ -521,8 +540,8 @@ impl ChatCommand {
"Set the server description",
Some(Admin),
),
// Uses Message because site names can contain spaces, which would be assumed to be
// separators otherwise
// Uses Message because site names can contain spaces,
// which would be assumed to be separators otherwise
ChatCommand::Site => cmd(
vec![Message(Required)],
"Teleport to a site",
@ -634,6 +653,7 @@ impl ChatCommand {
ChatCommand::Lantern => "lantern",
ChatCommand::Light => "light",
ChatCommand::MakeBlock => "make_block",
ChatCommand::MakeNpc => "make_npc",
ChatCommand::MakeSprite => "make_sprite",
ChatCommand::Motd => "motd",
ChatCommand::Object => "object",
@ -788,7 +808,6 @@ pub enum ArgumentSpec {
}
impl ArgumentSpec {
#[allow(clippy::nonstandard_macro_braces)] //tmp as of false positive !?
pub fn usage_string(&self) -> String {
match self {
ArgumentSpec::PlayerName(req) => {
@ -836,9 +855,9 @@ impl ArgumentSpec {
ArgumentSpec::SubCommand => "<[/]command> [args...]".to_string(),
ArgumentSpec::Enum(label, _, req) => {
if &Requirement::Required == req {
format! {"<{}>", label}
format!("<{}>", label)
} else {
format! {"[{}]", label}
format!("[{}]", label)
}
},
ArgumentSpec::Boolean(label, _, req) => {

View File

@ -119,6 +119,7 @@ pub enum CharacterAbility {
charge_duration: f32,
swing_duration: f32,
recover_duration: f32,
ori_modifier: f32,
charge_through: bool,
is_interruptible: bool,
damage_kind: DamageKind,
@ -497,6 +498,7 @@ impl CharacterAbility {
charge_duration: _,
ref mut swing_duration,
ref mut recover_duration,
ori_modifier: _,
charge_through: _,
is_interruptible: _,
damage_kind: _,
@ -1531,6 +1533,7 @@ impl From<(&CharacterAbility, AbilityInfo, &JoinData<'_>)> for CharacterState {
charge_duration,
swing_duration,
recover_duration,
ori_modifier,
charge_through,
is_interruptible,
damage_kind,
@ -1552,6 +1555,7 @@ impl From<(&CharacterAbility, AbilityInfo, &JoinData<'_>)> for CharacterState {
charge_duration: Duration::from_secs_f32(*charge_duration),
swing_duration: Duration::from_secs_f32(*swing_duration),
recover_duration: Duration::from_secs_f32(*recover_duration),
ori_modifier: *ori_modifier,
is_interruptible: *is_interruptible,
damage_effect: *damage_effect,
ability_info,

View File

@ -1,5 +1,5 @@
use crate::{
assets::{self, AssetExt},
assets::{self, AssetExt, Error},
comp::{
self, agent, humanoid,
inventory::loadout_builder::{ItemSpec, LoadoutBuilder},
@ -140,11 +140,16 @@ impl EntityConfig {
}
}
/// Return all entity config specifiers
pub fn try_all_entity_configs() -> Result<Vec<String>, Error> {
let configs = assets::load_dir::<EntityConfig>("common.entity", true)?;
Ok(configs.ids().map(|id| id.to_owned()).collect())
}
#[derive(Clone)]
pub struct EntityInfo {
pub pos: Vec3<f32>,
pub is_waypoint: bool, // Edge case, overrides everything else
pub is_giant: bool,
pub has_agency: bool,
pub alignment: Alignment,
pub agent_mark: Option<agent::Mark>,
@ -170,7 +175,6 @@ impl EntityInfo {
Self {
pos,
is_waypoint: false,
is_giant: false,
has_agency: true,
alignment: Alignment::Wild,
agent_mark: None,
@ -307,11 +311,6 @@ impl EntityInfo {
self
}
pub fn into_giant(mut self) -> Self {
self.is_giant = true;
self
}
pub fn with_alignment(mut self, alignment: Alignment) -> Self {
self.alignment = alignment;
self
@ -382,7 +381,7 @@ impl EntityInfo {
pub fn with_automatic_name(mut self) -> Self {
let npc_names = NPC_NAMES.read();
self.name = match &self.body {
let name = match &self.body {
Body::Humanoid(body) => Some(get_npc_name(&npc_names.humanoid, body.species)),
Body::QuadrupedMedium(body) => {
Some(get_npc_name(&npc_names.quadruped_medium, body.species))
@ -400,14 +399,8 @@ impl EntityInfo {
Body::Golem(body) => Some(get_npc_name(&npc_names.golem, body.species)),
Body::BipedLarge(body) => Some(get_npc_name(&npc_names.biped_large, body.species)),
_ => None,
}
.map(|s| {
if self.is_giant {
format!("Giant {}", s)
} else {
s.to_string()
}
});
};
self.name = name.map(str::to_owned);
self
}
@ -554,10 +547,10 @@ mod tests {
#[test]
fn test_all_entity_assets() {
// It just load everything that could
let entity_configs = assets::load_dir::<EntityConfig>("common.entity", true)
.expect("Failed to access entity directory");
for config_asset in entity_configs.ids() {
// Get list of entity configs, load everything, validate content.
let entity_configs =
try_all_entity_configs().expect("Failed to access entity configs directory");
for config_asset in entity_configs {
// print asset name so we don't need to find errors everywhere
// it'll be ignored by default so you'll see it only in case of errors
//
@ -568,7 +561,7 @@ mod tests {
// 2) Add try_from_asset() for LoadoutBuilder and
// SkillSet builder which will return Result and we will happily
// panic in validate_meta() with the name of config_asset
println!("{}:", config_asset);
println!("{}:", &config_asset);
let EntityConfig {
hands,
@ -577,12 +570,12 @@ mod tests {
loot,
meta,
alignment: _alignment, // can't fail if serialized, it's a boring enum
} = EntityConfig::from_asset_expect(config_asset);
} = EntityConfig::from_asset_expect(&config_asset);
validate_hands(hands, config_asset);
validate_body_and_name(body, name, config_asset);
validate_loot(loot, config_asset);
validate_meta(meta, config_asset);
validate_hands(hands, &config_asset);
validate_body_and_name(body, name, &config_asset);
validate_loot(loot, &config_asset);
validate_meta(meta, &config_asset);
}
}
}

View File

@ -43,6 +43,8 @@ pub struct StaticData {
pub swing_duration: Duration,
/// How long the state has until exiting
pub recover_duration: Duration,
/// How fast can you turn during charge
pub ori_modifier: f32,
/// Whether the state can be interrupted by other abilities
pub is_interruptible: bool,
/// Adds an effect onto the main damage of the attack
@ -107,13 +109,14 @@ impl CharacterBehavior for Data {
/ self.static_data.charge_duration.as_secs_f32())
.min(1.0);
handle_orientation(data, &mut update, 0.6);
handle_orientation(data, &mut update, self.static_data.ori_modifier);
handle_forced_movement(data, &mut update, ForcedMovement::Forward {
strength: self.static_data.forward_speed * charge_frac.sqrt(),
});
// This logic basically just decides if a charge should end, and prevents the
// character state spamming attacks while checking if it has hit something
// This logic basically just decides if a charge should end,
// and prevents the character state spamming attacks
// while checking if it has hit something.
if !self.exhausted {
// Hit attempt
let poise = AttackEffect::new(

View File

@ -1,11 +1,12 @@
//! # Implementing new commands.
//! To implement a new command, add an instance of `ChatCommand` to
//! `CHAT_COMMANDS` and provide a handler function.
//! To implement a new command provide a handler function
//! in [do_command].
use crate::{
settings::{
Ban, BanAction, BanInfo, EditableSetting, SettingError, WhitelistInfo, WhitelistRecord,
},
sys::terrain::NpcData,
wiring::{Logic, OutputFormula},
Server, SpawnPoint, StateExt,
};
@ -28,6 +29,7 @@ use common::{
depot,
effect::Effect,
event::{EventBus, ServerEvent},
generation::EntityInfo,
npc::{self, get_npc_name},
resources::{PlayerPhysicsSettings, TimeOfDay},
terrain::{Block, BlockKind, SpriteKind, TerrainChunkSize},
@ -68,6 +70,7 @@ impl ChatCommandExt for ChatCommand {
}
type CmdResult<T> = Result<T, String>;
/// Handler function called when the command is executed.
/// # Arguments
/// * `&mut Server` - the `Server` instance executing the command.
@ -140,6 +143,7 @@ fn do_command(
ChatCommand::Lantern => handle_lantern,
ChatCommand::Light => handle_light,
ChatCommand::MakeBlock => handle_make_block,
ChatCommand::MakeNpc => handle_make_npc,
ChatCommand::MakeSprite => handle_make_sprite,
ChatCommand::Motd => handle_motd,
ChatCommand::Object => handle_object,
@ -547,6 +551,93 @@ fn handle_make_block(
}
}
fn handle_make_npc(
server: &mut Server,
client: EcsEntity,
target: EcsEntity,
args: Vec<String>,
action: &ChatCommand,
) -> CmdResult<()> {
let (entity_config, number) = parse_args!(args, String, i8);
let entity_config = entity_config.ok_or_else(|| action.help_string())?;
let number = match number {
Some(i8::MIN..=0) => {
return Err("Number of entities should be at least 1".to_owned());
},
Some(50..=i8::MAX) => {
return Err("Number of entities should be less than 50".to_owned());
},
Some(number) => number,
None => 1,
};
let rng = &mut rand::thread_rng();
for _ in 0..number {
let comp::Pos(pos) = position(server, target, "target")?;
let entity_info = EntityInfo::at(pos).with_asset_expect(&entity_config);
match NpcData::from_entity_info(entity_info, rng) {
NpcData::Waypoint(_) => {
return Err("Waypoint spawning is not implemented".to_owned());
},
NpcData::Data {
loadout,
pos,
stats,
skill_set,
poise,
health,
body,
agent,
alignment,
scale,
drop_item,
} => {
let inventory = Inventory::new_with_loadout(loadout);
let mut entity_builder = server
.state
.create_npc(pos, stats, skill_set, health, poise, inventory, body)
.with(alignment)
.with(scale)
.with(comp::Vel(Vec3::new(0.0, 0.0, 0.0)))
.with(comp::MountState::Unmounted);
if let Some(agent) = agent {
entity_builder = entity_builder.with(agent);
}
if let Some(drop_item) = drop_item {
entity_builder = entity_builder.with(comp::ItemDrop(drop_item));
}
// Some would say it's a hack, some would say it's incomplete
// simulation. But this is what we do to avoid PvP between npc.
use comp::Alignment;
let npc_group = match alignment {
Alignment::Enemy => Some(comp::group::ENEMY),
Alignment::Npc | Alignment::Tame => Some(comp::group::NPC),
Alignment::Wild | Alignment::Passive | Alignment::Owned(_) => None,
};
if let Some(group) = npc_group {
entity_builder = entity_builder.with(group);
}
entity_builder.build();
},
};
}
server.notify_client(
client,
ServerGeneral::server_msg(
ChatType::CommandInfo,
format!("Spawned {} entities from config: {}", number, entity_config),
),
);
Ok(())
}
fn handle_make_sprite(
server: &mut Server,
_client: EcsEntity,

View File

@ -10,10 +10,9 @@ use crate::{
SpawnPoint, Tick,
};
use common::{
comp::{self, agent, bird_medium, Alignment, BehaviorCapability, ForceUpdate, Pos, Waypoint},
comp::{self, agent, bird_medium, BehaviorCapability, ForceUpdate, Pos, Waypoint},
event::{EventBus, ServerEvent},
generation::{get_npc_name, EntityInfo},
npc::NPC_NAMES,
generation::EntityInfo,
resources::Time,
terrain::TerrainGrid,
LoadoutBuilder, SkillSetBuilder,
@ -22,6 +21,7 @@ use common_ecs::{Job, Origin, Phase, System};
use common_net::msg::{SerializedTerrainChunk, ServerGeneral};
use common_state::TerrainChanges;
use comp::Behavior;
use rand::Rng;
use specs::{Entities, Join, Read, ReadExpect, ReadStorage, Write, WriteExpect, WriteStorage};
use std::sync::Arc;
use vek::*;
@ -199,145 +199,43 @@ impl<'a> System<'a> for Sys {
"Chunk spawned entity that wasn't nearby",
);
if entity.is_waypoint {
server_emitter.emit(ServerEvent::CreateWaypoint(entity.pos));
continue;
}
let mut body = entity.body;
let name = entity.name.unwrap_or_else(|| "Unnamed".to_string());
let alignment = entity.alignment;
let mut stats = comp::Stats::new(name);
let mut scale = entity.scale;
// Replace stuff if it's a boss
if entity.is_giant {
if rand::random::<f32>() < 0.65 && entity.alignment != Alignment::Enemy {
let body_new = comp::humanoid::Body::random();
let npc_names = NPC_NAMES.read();
body = comp::Body::Humanoid(body_new);
stats = comp::Stats::new(format!(
"Gentle Giant {}",
get_npc_name(&npc_names.humanoid, body_new.species)
));
}
scale = 2.0 + rand::random::<f32>();
}
let EntityInfo {
skillset_asset,
main_tool,
second_tool,
loadout_asset,
make_loadout,
trading_information: economy,
..
} = entity;
let skill_set = {
let skillset_builder = SkillSetBuilder::default();
if let Some(skillset_asset) = skillset_asset {
skillset_builder.with_asset_expect(&skillset_asset).build()
} else {
skillset_builder.build()
}
};
let loadout = {
let mut loadout_builder = LoadoutBuilder::empty();
let rng = &mut rand::thread_rng();
// If main tool is passed, use it. Otherwise fallback to default tool
if let Some(main_tool) = main_tool {
loadout_builder = loadout_builder.active_mainhand(Some(main_tool));
} else {
loadout_builder = loadout_builder.with_default_maintool(&body);
}
// If second tool is passed, use it as well
if let Some(second_tool) = second_tool {
loadout_builder = loadout_builder.active_offhand(Some(second_tool));
}
// If there is config, apply it.
// If not, use default equipement for this body.
if let Some(asset) = loadout_asset {
loadout_builder = loadout_builder.with_asset_expect(&asset, rng);
} else {
loadout_builder = loadout_builder.with_default_equipment(&body);
}
// Evaluate lazy function for loadout creation
if let Some(make_loadout) = make_loadout {
loadout_builder =
loadout_builder.with_creator(make_loadout, economy.as_ref());
}
loadout_builder.build()
};
let health = Some(comp::Health::new(body, entity.level.unwrap_or(0)));
let poise = comp::Poise::new(body);
let can_speak = match body {
comp::Body::Humanoid(_) => true,
comp::Body::BirdMedium(bird_medium) => match bird_medium.species {
// Parrots like to have a word in this, too...
bird_medium::Species::Parrot => alignment == comp::Alignment::Npc,
_ => false,
let rng = &mut rand::thread_rng();
let data = NpcData::from_entity_info(entity, rng);
match data {
NpcData::Waypoint(pos) => {
server_emitter.emit(ServerEvent::CreateWaypoint(pos));
},
_ => false,
};
let trade_for_site = if matches!(entity.agent_mark, Some(agent::Mark::Merchant)) {
economy.map(|e| e.id)
} else {
None
};
// TODO: This code sets an appropriate base_damage for the enemy. This doesn't
// work because the damage is now saved in an ability
/*
if let Some(item::ItemKind::Tool(item::ToolData { base_damage, .. })) =
&mut loadout.active_item.map(|i| i.item.kind)
{
*base_damage = stats.level.level() as u32 * 3;
}
*/
server_emitter.emit(ServerEvent::CreateNpc {
pos: Pos(entity.pos),
stats,
skill_set,
health,
poise,
loadout,
agent: if entity.has_agency {
Some(
comp::Agent::from_body(&body)
.with_behavior(
Behavior::default()
.maybe_with_capabilities(
can_speak.then(|| BehaviorCapability::SPEAK),
)
.with_trade_site(trade_for_site),
)
.with_patrol_origin(entity.pos)
.with_no_flee(!matches!(
entity.agent_mark,
Some(agent::Mark::Guard)
)),
)
} else {
None
NpcData::Data {
pos,
stats,
skill_set,
health,
poise,
loadout,
agent,
body,
alignment,
scale,
drop_item,
} => {
server_emitter.emit(ServerEvent::CreateNpc {
pos,
stats,
skill_set,
health,
poise,
loadout,
agent,
body,
alignment,
scale,
anchor: Some(comp::Anchor::Chunk(key)),
drop_item,
rtsim_entity: None,
projectile: None,
});
},
body,
alignment,
scale: comp::Scale(scale),
anchor: Some(comp::Anchor::Chunk(key)),
drop_item: entity.loot_drop,
rtsim_entity: None,
projectile: None,
})
}
}
// Insert a safezone if chunk contains the spawn position
@ -442,6 +340,144 @@ impl<'a> System<'a> for Sys {
}
}
/// Convinient structure to use when you need to create new npc
/// from EntityInfo
// TODO: better name?
pub enum NpcData {
Data {
pos: Pos,
stats: comp::Stats,
skill_set: comp::SkillSet,
health: Option<comp::Health>,
poise: comp::Poise,
loadout: comp::inventory::loadout::Loadout,
agent: Option<comp::Agent>,
body: comp::Body,
alignment: comp::Alignment,
scale: comp::Scale,
drop_item: Option<comp::Item>,
},
Waypoint(Vec3<f32>),
}
impl NpcData {
pub fn from_entity_info(entity: EntityInfo, rng: &mut impl Rng) -> Self {
let EntityInfo {
// flags
is_waypoint,
has_agency,
agent_mark,
alignment,
// stats
body,
name,
scale,
pos,
level,
loot_drop,
// tools and skills
skillset_asset,
main_tool,
second_tool,
loadout_asset,
make_loadout,
trading_information: economy,
// unused
pet: _, // TODO: I had no idea we have this.
} = entity;
if is_waypoint {
return Self::Waypoint(pos);
}
let name = name.unwrap_or_else(|| "Unnamed".to_string());
let stats = comp::Stats::new(name);
let skill_set = {
let skillset_builder = SkillSetBuilder::default();
if let Some(skillset_asset) = skillset_asset {
skillset_builder.with_asset_expect(&skillset_asset).build()
} else {
skillset_builder.build()
}
};
let loadout = {
let mut loadout_builder = LoadoutBuilder::empty();
// If main tool is passed, use it. Otherwise fallback to default tool
if let Some(main_tool) = main_tool {
loadout_builder = loadout_builder.active_mainhand(Some(main_tool));
} else {
loadout_builder = loadout_builder.with_default_maintool(&body);
}
// If second tool is passed, use it as well
if let Some(second_tool) = second_tool {
loadout_builder = loadout_builder.active_offhand(Some(second_tool));
}
// If there is config, apply it.
// If not, use default equipement for this body.
if let Some(asset) = loadout_asset {
loadout_builder = loadout_builder.with_asset_expect(&asset, rng);
} else {
loadout_builder = loadout_builder.with_default_equipment(&body);
}
// Evaluate lazy function for loadout creation
if let Some(make_loadout) = make_loadout {
loadout_builder = loadout_builder.with_creator(make_loadout, economy.as_ref());
}
loadout_builder.build()
};
let health = Some(comp::Health::new(body, level.unwrap_or(0)));
let poise = comp::Poise::new(body);
let can_speak = match body {
comp::Body::Humanoid(_) => true,
comp::Body::BirdMedium(bird_medium) => match bird_medium.species {
// Parrots like to have a word in this, too...
bird_medium::Species::Parrot => alignment == comp::Alignment::Npc,
_ => false,
},
_ => false,
};
let trade_for_site = if matches!(agent_mark, Some(agent::Mark::Merchant)) {
economy.map(|e| e.id)
} else {
None
};
let agent = has_agency.then(|| {
comp::Agent::from_body(&body)
.with_behavior(
Behavior::default()
.maybe_with_capabilities(can_speak.then(|| BehaviorCapability::SPEAK))
.with_trade_site(trade_for_site),
)
.with_patrol_origin(pos)
.with_no_flee(!matches!(agent_mark, Some(agent::Mark::Guard)))
});
NpcData::Data {
pos: Pos(pos),
stats,
skill_set,
health,
poise,
loadout,
agent,
body,
alignment,
scale: comp::Scale(scale),
drop_item: loot_drop,
}
}
}
pub fn chunk_in_vd(
player_pos: Vec3<f32>,
chunk_pos: Vec2<i32>,