Make bats easier to combat and fix hitbox scaling bug

This commit is contained in:
James Melkonian 2023-01-14 18:03:18 -08:00
parent 696b575611
commit 9d31baf500
7 changed files with 88 additions and 64 deletions

View File

@ -11,11 +11,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Command to toggle experimental shaders. - Command to toggle experimental shaders.
### Changed ### Changed
- Bats move slower and use a simple proportional controller to maintain altitude
- Bats now have less health
### Removed ### Removed
### Fixed ### Fixed
- Doors - Doors
- Debug hitboxes now scale with the `Scale` component
## [0.14.0] - 2023-01-07 ## [0.14.0] - 2023-01-07

View File

@ -425,7 +425,7 @@ impl Body {
bird_medium::Species::Duck => Vec3::new(0.9, 1.0, 1.4), bird_medium::Species::Duck => Vec3::new(0.9, 1.0, 1.4),
bird_medium::Species::Goose => Vec3::new(1.0, 1.2, 1.5), bird_medium::Species::Goose => Vec3::new(1.0, 1.2, 1.5),
bird_medium::Species::Peacock => Vec3::new(1.3, 1.1, 1.4), bird_medium::Species::Peacock => Vec3::new(1.3, 1.1, 1.4),
bird_medium::Species::Bat => Vec3::new(2.0, 2.0, 1.5), bird_medium::Species::Bat => Vec3::new(4.0, 2.0, 2.0),
_ => Vec3::new(2.0, 1.0, 1.5), _ => Vec3::new(2.0, 1.0, 1.5),
}, },
Body::BirdLarge(body) => match body.species { Body::BirdLarge(body) => match body.species {
@ -717,7 +717,7 @@ impl Body {
bird_medium::Species::Eagle => 45, bird_medium::Species::Eagle => 45,
bird_medium::Species::Owl => 45, bird_medium::Species::Owl => 45,
bird_medium::Species::Duck => 10, bird_medium::Species::Duck => 10,
bird_medium::Species::Bat => 20, bird_medium::Species::Bat => 1,
_ => 15, _ => 15,
}, },
Body::FishMedium(_) => 15, Body::FishMedium(_) => 15,

View File

@ -3,14 +3,14 @@ use crate::{
combat, combat,
comp::{ comp::{
ability::{Ability, AbilityInput, AbilityMeta, Capability}, ability::{Ability, AbilityInput, AbilityMeta, Capability},
arthropod, biped_large, biped_small, arthropod, biped_large, biped_small, bird_medium,
character_state::OutputEvents, character_state::OutputEvents,
inventory::slot::{ArmorSlot, EquipSlot, Slot}, inventory::slot::{ArmorSlot, EquipSlot, Slot},
item::{armor::Friction, Hands, ItemKind, ToolKind, tool::AbilityContext}, item::{armor::Friction, tool::AbilityContext, Hands, ItemKind, ToolKind},
quadruped_low, quadruped_medium, quadruped_small, quadruped_low, quadruped_medium, quadruped_small,
skills::{Skill, SwimSkill, SKILL_MODIFIERS}, skills::{Skill, SwimSkill, SKILL_MODIFIERS},
theropod, Body, CharacterAbility, CharacterState, Density, InputAttr, InputKind, theropod, Body, CharacterAbility, CharacterState, Density, InputAttr, InputKind,
InventoryAction, StateUpdate, Melee, InventoryAction, Melee, StateUpdate,
}, },
consts::{FRIC_GROUND, GRAVITY, MAX_PICKUP_RANGE}, consts::{FRIC_GROUND, GRAVITY, MAX_PICKUP_RANGE},
event::{LocalEvent, ServerEvent}, event::{LocalEvent, ServerEvent},
@ -250,7 +250,10 @@ impl Body {
/// Returns thrust force if the body type can fly, otherwise None /// Returns thrust force if the body type can fly, otherwise None
pub fn fly_thrust(&self) -> Option<f32> { pub fn fly_thrust(&self) -> Option<f32> {
match self { match self {
Body::BirdMedium(_) => Some(GRAVITY * self.mass().0 * 2.0), Body::BirdMedium(body) => match body.species {
bird_medium::Species::Bat => Some(GRAVITY * self.mass().0 * 0.5),
_ => Some(GRAVITY * self.mass().0 * 2.0),
},
Body::BirdLarge(_) => Some(GRAVITY * self.mass().0 * 0.5), Body::BirdLarge(_) => Some(GRAVITY * self.mass().0 * 0.5),
Body::Dragon(_) => Some(200_000.0), Body::Dragon(_) => Some(200_000.0),
Body::Ship(ship) if ship.can_fly() => Some(300_000.0), Body::Ship(ship) if ship.can_fly() => Some(300_000.0),
@ -1074,7 +1077,11 @@ pub fn handle_dodge_input(data: &JoinData<'_>, update: &mut StateUpdate) -> bool
)); ));
if let CharacterState::Roll(roll) = &mut update.character { if let CharacterState::Roll(roll) = &mut update.character {
if let CharacterState::ComboMelee(c) = data.character { if let CharacterState::ComboMelee(c) = data.character {
roll.was_combo = c.static_data.ability_info.input.map(|input| (input, c.stage)); roll.was_combo = c
.static_data
.ability_info
.input
.map(|input| (input, c.stage));
roll.was_wielded = true; roll.was_wielded = true;
} else { } else {
if data.character.is_wield() { if data.character.is_wield() {
@ -1095,10 +1102,7 @@ pub fn handle_dodge_input(data: &JoinData<'_>, update: &mut StateUpdate) -> bool
} }
/// Returns whether an interrupt occurred /// Returns whether an interrupt occurred
pub fn handle_interrupts( pub fn handle_interrupts(data: &JoinData, update: &mut StateUpdate) -> bool {
data: &JoinData,
update: &mut StateUpdate,
) -> bool {
let can_dodge = { let can_dodge = {
let in_buildup = data let in_buildup = data
.character .character
@ -1106,16 +1110,22 @@ pub fn handle_interrupts(
.map_or(true, |stage_section| { .map_or(true, |stage_section| {
matches!(stage_section, StageSection::Buildup) matches!(stage_section, StageSection::Buildup)
}); });
let interruptible = data.character.ability_info().and_then(|info| info.ability_meta).map_or(false, |meta| { let interruptible = data
meta.capabilities .character
.contains(Capability::ROLL_INTERRUPT) .ability_info()
}); .and_then(|info| info.ability_meta)
.map_or(false, |meta| {
meta.capabilities.contains(Capability::ROLL_INTERRUPT)
});
in_buildup || interruptible in_buildup || interruptible
}; };
let can_block = data.character.ability_info().and_then(|info| info.ability_meta).map_or(false, |meta| { let can_block = data
meta.capabilities .character
.contains(Capability::BLOCK_INTERRUPT) .ability_info()
}); .and_then(|info| info.ability_meta)
.map_or(false, |meta| {
meta.capabilities.contains(Capability::BLOCK_INTERRUPT)
});
if can_dodge { if can_dodge {
handle_dodge_input(data, update) handle_dodge_input(data, update)
} else if can_block { } else if can_block {
@ -1348,8 +1358,13 @@ impl AbilityInfo {
None None
}; };
// If this ability should not be returned to, check if this ability was going to return to another ability, and return to that one instead // If this ability should not be returned to, check if this ability was going to
let return_ability = return_ability.or_else(|| char_state.ability_info().and_then(|info| info.return_ability)); // return to another ability, and return to that one instead
let return_ability = return_ability.or_else(|| {
char_state
.ability_info()
.and_then(|info| info.return_ability)
});
Self { Self {
tool: None, tool: None,
@ -1364,19 +1379,32 @@ impl AbilityInfo {
} }
pub fn end_ability(data: &JoinData<'_>, update: &mut StateUpdate) { pub fn end_ability(data: &JoinData<'_>, update: &mut StateUpdate) {
// If an ability has a return ability specified, and is not itself an ability that should be returned to (to prevent bouncing between two abilities), attempt to return to the specified ability, otherwise return to wield or idle depending on whether or not leaving a wield state // If an ability has a return ability specified, and is not itself an ability
let returned = if let Some(return_ability) = (!data.character.should_be_returned_to()).then_some(data.character.ability_info().and_then(|info| info.return_ability)).flatten() { // that should be returned to (to prevent bouncing between two abilities),
// attempt to return to the specified ability, otherwise return to wield or idle
// depending on whether or not leaving a wield state
let returned = if let Some(return_ability) = (!data.character.should_be_returned_to())
.then_some(
data.character
.ability_info()
.and_then(|info| info.return_ability),
)
.flatten()
{
handle_ability(data, update, return_ability) handle_ability(data, update, return_ability)
} else { } else {
false false
}; };
if !returned { if !returned {
if data.character.is_wield() || data.character.was_wielded() { if data.character.is_wield() || data.character.was_wielded() {
update.character = update.character = CharacterState::Wielding(wielding::Data {
CharacterState::Wielding(wielding::Data { is_sneaking: data.character.is_stealthy() }); is_sneaking: data.character.is_stealthy(),
});
} else { } else {
update.character = update.character = CharacterState::Idle(idle::Data {
CharacterState::Idle(idle::Data { is_sneaking: data.character.is_stealthy(), footwear: None }); is_sneaking: data.character.is_stealthy(),
footwear: None,
});
} }
} }
} }

View File

@ -348,23 +348,20 @@ impl<'a> AgentData<'a> {
{ {
// Bats don't like the ground, so make sure they are always flying // Bats don't like the ground, so make sure they are always flying
controller.push_basic_input(InputKind::Fly); controller.push_basic_input(InputKind::Fly);
if read_data // Use a proportional controller with a coefficient of 1.0 to
// maintain altitude
let alt = read_data
.terrain .terrain
.ray(self.pos.0, self.pos.0 - (Vec3::unit_z() * 5.0)) .ray(self.pos.0, self.pos.0 - (Vec3::unit_z() * 7.0))
.until(Block::is_solid) .until(Block::is_solid)
.cast() .cast()
.1 .0;
.map_or(true, |b| b.is_some()) let set_point = 5.0;
{ let error = set_point - alt;
// Fly up controller.inputs.move_z = error;
controller.inputs.move_z = 1.0; // If on the ground, jump
// If on the ground, jump if self.physics_state.on_ground.is_some() {
if self.physics_state.on_ground.is_some() { controller.push_basic_input(InputKind::Jump);
controller.push_basic_input(InputKind::Jump);
}
} else {
// Fly down
controller.inputs.move_z = -1.0;
} }
} }
agent.bearing += Vec2::new(rng.gen::<f32>() - 0.5, rng.gen::<f32>() - 0.5) * 0.1 agent.bearing += Vec2::new(rng.gen::<f32>() - 0.5, rng.gen::<f32>() - 0.5) * 0.1

View File

@ -73,31 +73,24 @@ impl<'a> AgentData<'a> {
// Always fly! If the floor can't touch you, it can't hurt you... // Always fly! If the floor can't touch you, it can't hurt you...
controller.push_basic_input(InputKind::Fly); controller.push_basic_input(InputKind::Fly);
// Flee from the ground! The internet told me it was lava! // Flee from the ground! The internet told me it was lava!
// If on the ground, jump with every last ounce of energy, holding onto all that // If on the ground, jump with every last ounce of energy, holding onto
// is dear in life and straining for the wide open skies. // all that is dear in life and straining for the wide open skies.
if self.physics_state.on_ground.is_some() { if self.physics_state.on_ground.is_some() {
controller.push_basic_input(InputKind::Jump); controller.push_basic_input(InputKind::Jump);
} else { } else {
// Only fly down if close enough to target in the xy plane // Use a proportional controller with a coefficient of 1.0 to
// Otherwise fly towards the target bouncing around a 5 block altitude // maintain altidude at the the provided set point
let mut maintain_altitude = |altitude| { let mut maintain_altitude = |set_point| {
if read_data let alt = read_data
.terrain .terrain
.ray(self.pos.0, self.pos.0 - (Vec3::unit_z() * altitude)) .ray(self.pos.0, self.pos.0 - (Vec3::unit_z() * 7.0))
.until(Block::is_solid) .until(Block::is_solid)
.cast() .cast()
.1 .0;
.map_or(true, |b| b.is_some()) let error = set_point - alt;
{ controller.inputs.move_z = error;
// Fly up
controller.inputs.move_z = 1.0;
} else {
// Fly down
controller.inputs.move_z = -1.0;
}
}; };
if (tgt_data.pos.0 - self.pos.0).xy().magnitude_squared() > (5.0_f32).powi(2) { if (tgt_data.pos.0 - self.pos.0).xy().magnitude_squared() > (5.0_f32).powi(2) {
// If above 5 blocks, fly down
maintain_altitude(5.0); maintain_altitude(5.0);
} else { } else {
maintain_altitude(2.0); maintain_altitude(2.0);

View File

@ -1221,7 +1221,7 @@ fn handle_spawn(
pos, pos,
comp::Stats::new(get_npc_name(id, npc::BodyType::from_body(body))), comp::Stats::new(get_npc_name(id, npc::BodyType::from_body(body))),
comp::SkillSet::default(), comp::SkillSet::default(),
Some(comp::Health::new(body, 1)), Some(comp::Health::new(body, 0)),
comp::Poise::new(body), comp::Poise::new(body),
inventory, inventory,
body, body,

View File

@ -1405,12 +1405,14 @@ impl Scene {
let positions = ecs.read_component::<comp::Pos>(); let positions = ecs.read_component::<comp::Pos>();
let colliders = ecs.read_component::<comp::Collider>(); let colliders = ecs.read_component::<comp::Collider>();
let orientations = ecs.read_component::<comp::Ori>(); let orientations = ecs.read_component::<comp::Ori>();
let scales = ecs.read_component::<comp::Scale>();
let groups = ecs.read_component::<comp::Group>(); let groups = ecs.read_component::<comp::Group>();
for (entity, pos, collider, ori, group) in ( for (entity, pos, collider, ori, scale, group) in (
&ecs.entities(), &ecs.entities(),
&positions, &positions,
&colliders, &colliders,
&orientations, &orientations,
scales.maybe(),
groups.maybe(), groups.maybe(),
) )
.join() .join()
@ -1424,12 +1426,13 @@ impl Scene {
z_max, z_max,
} => { } => {
current_entities.insert(entity); current_entities.insert(entity);
let s = scale.map_or(1.0, |sc| sc.0);
let shape_id = hitboxes.entry(entity).or_insert_with(|| { let shape_id = hitboxes.entry(entity).or_insert_with(|| {
self.debug.add_shape(DebugShape::CapsulePrism { self.debug.add_shape(DebugShape::CapsulePrism {
p0: *p0, p0: *p0 * s,
p1: *p1, p1: *p1 * s,
radius: *radius, radius: *radius * s,
height: *z_max - *z_min, height: (*z_max - *z_min) * s,
}) })
}); });
let hb_pos = [pos.0.x, pos.0.y, pos.0.z + *z_min, 0.0]; let hb_pos = [pos.0.x, pos.0.y, pos.0.z + *z_min, 0.0];