Melee weapons can now block.

This commit is contained in:
Sam 2021-04-09 23:40:20 -04:00
parent 3cfc94af45
commit 91c6288213
18 changed files with 289 additions and 60 deletions

View File

@ -1 +1,6 @@
BasicBlock
BasicBlock(
buildup_duration: 0.1,
recover_duration: 0.1,
max_angle: 90.0,
block_strength: 0.8,
)

View File

@ -12,8 +12,8 @@ use crate::{
},
poise::PoiseChange,
skills::SkillGroupKind,
Body, Combo, Energy, EnergyChange, EnergySource, Health, HealthChange, HealthSource,
Inventory, SkillSet, Stats,
Body, CharacterState, Combo, Energy, EnergyChange, EnergySource, Health, HealthChange, HealthSource,
Inventory, Ori, SkillSet, Stats,
},
event::ServerEvent,
outcome::Outcome,
@ -39,6 +39,15 @@ pub enum GroupTarget {
OutOfGroup,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
pub enum AttackSource {
Melee,
Projectile,
Beam,
Shockwave,
Explosion,
}
#[cfg(not(target_arch = "wasm32"))]
#[derive(Copy, Clone)]
pub struct AttackerInfo<'a> {
@ -55,6 +64,8 @@ pub struct TargetInfo<'a> {
pub stats: Option<&'a Stats>,
pub health: Option<&'a Health>,
pub pos: Vec3<f32>,
pub ori: Option<&'a Ori>,
pub char_state: Option<&'a CharacterState>,
}
#[cfg(not(target_arch = "wasm32"))]
@ -105,6 +116,28 @@ impl Attack {
pub fn effects(&self) -> impl Iterator<Item = &AttackEffect> { self.effects.iter() }
pub fn compute_damage_reduction(target: &TargetInfo, source: AttackSource, dir: Dir) -> f32 {
let damage_reduction = Damage::compute_damage_reduction(target.inventory, target.stats);
let block_reduction = match source {
AttackSource::Melee => {
if let (Some(CharacterState::BasicBlock(data)), Some(ori)) =
(target.char_state, target.ori)
{
if ori.look_vec().angle_between(-*dir) < data.static_data.max_angle.to_radians()
{
data.static_data.block_strength
} else {
0.0
}
} else {
0.0
}
},
_ => 0.0,
};
1.0 - (1.0 - damage_reduction) * (1.0 - block_reduction)
}
#[allow(clippy::too_many_arguments)]
pub fn apply_attack(
&self,
@ -115,6 +148,7 @@ impl Attack {
target_dodging: bool,
// Currently just modifies damage, maybe look into modifying strength of other effects?
strength_modifier: f32,
attack_source: AttackSource,
mut emit: impl FnMut(ServerEvent),
mut emit_outcome: impl FnMut(Outcome),
) {
@ -126,7 +160,7 @@ impl Attack {
.filter(|d| d.target.map_or(true, |t| t == target_group))
.filter(|d| !(matches!(d.target, Some(GroupTarget::OutOfGroup)) && target_dodging))
{
let damage_reduction = Damage::compute_damage_reduction(target.inventory, target.stats);
let damage_reduction = Attack::compute_damage_reduction(&target, attack_source, dir);
let change = damage.damage.calculate_health_change(
damage_reduction,
attacker.map(|a| a.uid),

View File

@ -39,7 +39,7 @@ impl From<&CharacterState> for CharacterAbilityType {
CharacterState::BasicRanged(_) => Self::BasicRanged,
CharacterState::Boost(_) => Self::Boost,
CharacterState::DashMelee(data) => Self::DashMelee(data.stage_section),
CharacterState::BasicBlock => Self::BasicBlock,
CharacterState::BasicBlock(_) => Self::BasicBlock,
CharacterState::LeapMelee(data) => Self::LeapMelee(data.stage_section),
CharacterState::ComboMelee(data) => Self::ComboMelee(data.stage_section, data.stage),
CharacterState::SpinMelee(data) => Self::SpinMelee(data.stage_section),
@ -114,7 +114,12 @@ pub enum CharacterAbility {
charge_through: bool,
is_interruptible: bool,
},
BasicBlock,
BasicBlock {
buildup_duration: f32,
recover_duration: f32,
max_angle: f32,
block_strength: f32,
},
Roll {
energy_cost: f32,
buildup_duration: f32,
@ -341,6 +346,15 @@ impl CharacterAbility {
}
}
pub fn default_block() -> CharacterAbility {
CharacterAbility::BasicBlock {
buildup_duration: 0.1,
recover_duration: 0.1,
max_angle: 60.0,
block_strength: 0.5,
}
}
pub fn adjusted_by_stats(mut self, power: f32, poise_strength: f32, speed: f32) -> Self {
use CharacterAbility::*;
match self {
@ -408,7 +422,15 @@ impl CharacterAbility {
*swing_duration /= speed;
*recover_duration /= speed;
},
BasicBlock => {},
BasicBlock {
ref mut buildup_duration,
ref mut recover_duration,
// Block strength explicitly not modified by power, that will be a separate stat
..
} => {
*buildup_duration /= speed;
*recover_duration /= speed;
},
Roll {
ref mut buildup_duration,
ref mut movement_duration,
@ -586,7 +608,11 @@ impl CharacterAbility {
0
}
},
BasicBlock | Boost { .. } | ComboMelee { .. } | Blink { .. } | BasicSummon { .. } => 0,
BasicBlock { .. }
| Boost { .. }
| ComboMelee { .. }
| Blink { .. }
| BasicSummon { .. } => 0,
}
}
@ -1235,7 +1261,23 @@ impl From<(&CharacterAbility, AbilityInfo)> for CharacterState {
stage_section: StageSection::Buildup,
exhausted: false,
}),
CharacterAbility::BasicBlock => CharacterState::BasicBlock,
CharacterAbility::BasicBlock {
buildup_duration,
recover_duration,
max_angle,
block_strength,
} => CharacterState::BasicBlock(basic_block::Data {
static_data: basic_block::StaticData {
buildup_duration: Duration::from_secs_f32(*buildup_duration),
recover_duration: Duration::from_secs_f32(*recover_duration),
max_angle: *max_angle,
block_strength: *block_strength,
ability_info,
},
timer: Duration::default(),
stage_section: StageSection::Buildup,
parry: false,
}),
CharacterAbility::Roll {
energy_cost: _,
buildup_duration,

View File

@ -55,7 +55,7 @@ pub enum CharacterState {
/// A stunned state
Stunned(stunned::Data),
/// A basic blocking state
BasicBlock,
BasicBlock(basic_block::Data),
/// Player is busy equipping or unequipping weapons
Equipping(equipping::Data),
/// Player is holding a weapon and can perform other actions
@ -110,7 +110,7 @@ impl CharacterState {
| CharacterState::BasicRanged(_)
| CharacterState::DashMelee(_)
| CharacterState::ComboMelee(_)
| CharacterState::BasicBlock
| CharacterState::BasicBlock(_)
| CharacterState::LeapMelee(_)
| CharacterState::SpinMelee(_)
| CharacterState::ChargedMelee(_)
@ -153,7 +153,7 @@ impl CharacterState {
| CharacterState::BasicRanged(_)
| CharacterState::DashMelee(_)
| CharacterState::ComboMelee(_)
| CharacterState::BasicBlock
| CharacterState::BasicBlock(_)
| CharacterState::LeapMelee(_)
| CharacterState::ChargedMelee(_)
| CharacterState::ChargedRanged(_)
@ -180,7 +180,7 @@ impl CharacterState {
)
}
pub fn is_block(&self) -> bool { matches!(self, CharacterState::BasicBlock) }
pub fn is_block(&self) -> bool { matches!(self, CharacterState::BasicBlock(_)) }
pub fn is_dodge(&self) -> bool { matches!(self, CharacterState::Roll(_)) }

View File

@ -149,10 +149,11 @@ impl ControlAction {
pub enum InputKind {
Primary = 0,
Secondary = 1,
Ability(usize) = 2,
Roll = 3,
Jump = 4,
Fly = 5,
Block = 2,
Ability(usize) = 3,
Roll = 4,
Jump = 5,
Fly = 6,
}
impl InputKind {

View File

@ -326,6 +326,17 @@ impl Tool {
Default::default()
}
}
pub fn can_block(&self) -> bool {
matches!(
self.kind,
ToolKind::Sword
| ToolKind::Axe
| ToolKind::Hammer
| ToolKind::Shield
| ToolKind::Dagger
)
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]

View File

@ -1,15 +1,38 @@
use super::utils::*;
use crate::{
comp::StateUpdate,
comp::{CharacterState, InputKind, StateUpdate},
states::behavior::{CharacterBehavior, JoinData},
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
// const BLOCK_ACCEL: f32 = 30.0;
// const BLOCK_SPEED: f32 = 75.0;
/// Separated out to condense update portions of character state
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct StaticData {
/// How long until state should deal damage
pub buildup_duration: Duration,
/// How long the state has until exiting
pub recover_duration: Duration,
/// Max angle (45.0 will give you a 90.0 angle window)
pub max_angle: f32,
/// What percentage incoming damage is reduced by
pub block_strength: f32,
/// What key is used to press ability
pub ability_info: AbilityInfo,
}
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize, Eq, Hash)]
pub struct Data;
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Data {
/// Struct containing data that does not change over the course of the
/// character state
pub static_data: StaticData,
/// Timer for each stage
pub timer: Duration,
/// What section the character stage is in
pub stage_section: StageSection,
/// Whether the block was cancelled early enough to become a parry
pub parry: bool,
}
impl CharacterBehavior for Data {
fn behavior(&self, data: &JoinData) -> StateUpdate {
@ -17,6 +40,72 @@ impl CharacterBehavior for Data {
handle_move(&data, &mut update, 0.4);
match self.stage_section {
StageSection::Buildup => {
if self.timer < self.static_data.buildup_duration {
// Build up
update.character = CharacterState::BasicBlock(Data {
timer: self
.timer
.checked_add(Duration::from_secs_f32(data.dt.0))
.unwrap_or_default(),
parry: !input_is_pressed(data, InputKind::Block),
..*self
});
} else {
// Transitions to swing section of stage
update.character = CharacterState::BasicBlock(Data {
timer: Duration::default(),
stage_section: StageSection::Swing,
..*self
});
}
},
StageSection::Swing => {
if input_is_pressed(data, InputKind::Block) {
// Block
update.character = CharacterState::BasicBlock(Data {
timer: self
.timer
.checked_add(Duration::from_secs_f32(data.dt.0))
.unwrap_or_default(),
..*self
});
} else {
// Transitions to recover section of stage
update.character = CharacterState::BasicBlock(Data {
timer: Duration::default(),
stage_section: StageSection::Recover,
..*self
});
}
},
StageSection::Recover => {
if self.timer < self.static_data.recover_duration {
// Recovery
update.character = CharacterState::BasicBlock(Data {
timer: self
.timer
.checked_add(Duration::from_secs_f32(data.dt.0))
.unwrap_or_default(),
..*self
});
} else {
// Done
update.character = CharacterState::Wielding;
}
},
_ => {
// If it somehow ends up in an incorrect stage section
update.character = CharacterState::Wielding;
},
}
// At end of state logic so an interrupt isn't overwritten
if !input_is_pressed(data, self.static_data.ability_info.input) {
handle_state_interrupt(data, &mut update, false);
}
update
}
}

View File

@ -579,7 +579,7 @@ fn handle_ability(data: &JoinData, update: &mut StateUpdate, input: InputKind) {
.get(skill_index)
.cloned()
.and_then(unlocked),
InputKind::Roll | InputKind::Jump | InputKind::Fly => None,
InputKind::Roll | InputKind::Jump | InputKind::Fly | InputKind::Block => None,
})
.map(|a| {
let tool = unwrap_tool_data(data, equip_slot).map(|t| t.kind);
@ -615,6 +615,7 @@ pub fn handle_input(data: &JoinData, update: &mut StateUpdate, input: InputKind)
InputKind::Jump => {
handle_jump(data, update, 1.0);
},
InputKind::Block => handle_block_input(data, update),
InputKind::Fly => {},
}
}
@ -626,6 +627,21 @@ pub fn attempt_input(data: &JoinData, update: &mut StateUpdate) {
}
}
/// Checks that player can block, then attempts to block
pub fn handle_block_input(data: &JoinData, update: &mut StateUpdate) {
let can_block =
|equip_slot| matches!(unwrap_tool_data(data, equip_slot), Some(tool) if tool.can_block());
if input_is_pressed(data, InputKind::Block) && can_block(EquipSlot::Mainhand) {
let ability = CharacterAbility::default_block();
if ability.requirements_paid(data, update) {
update.character = CharacterState::from((
&ability,
AbilityInfo::from_input(data, false, InputKind::Roll),
));
}
}
}
/// Checks that player can perform a dodge, then
/// attempts to perform their dodge ability
pub fn handle_dodge_input(data: &JoinData, update: &mut StateUpdate) {

View File

@ -1,8 +1,8 @@
use common::{
combat::{AttackerInfo, TargetInfo},
combat::{AttackSource, AttackerInfo, TargetInfo},
comp::{
Beam, BeamSegment, Body, Combo, Energy, Group, Health, HealthSource, Inventory, Ori, Pos,
Scale, Stats,
Beam, BeamSegment, Body, CharacterState, Combo, Energy, Group, Health, HealthSource,
Inventory, Ori, Pos, Scale, Stats,
},
event::{EventBus, ServerEvent},
outcome::Outcome,
@ -40,6 +40,7 @@ pub struct ReadData<'a> {
energies: ReadStorage<'a, Energy>,
stats: ReadStorage<'a, Stats>,
combos: ReadStorage<'a, Combo>,
character_states: ReadStorage<'a, CharacterState>,
}
/// This system is responsible for handling beams that heal or do damage
@ -188,6 +189,8 @@ impl<'a> System<'a> for Sys {
stats: read_data.stats.get(target),
health: read_data.healths.get(target),
pos: pos.0,
ori: read_data.orientations.get(target),
char_state: read_data.character_states.get(target),
};
beam_segment.properties.attack.apply_attack(
@ -197,6 +200,7 @@ impl<'a> System<'a> for Sys {
ori.look_dir(),
false,
1.0,
AttackSource::Beam,
|e| server_events.push(e),
|o| outcomes.push(o),
);

View File

@ -301,9 +301,7 @@ impl<'a> System<'a> for Sys {
CharacterState::Sneak => {
states::sneak::Data::handle_event(&states::sneak::Data, &j, action)
},
CharacterState::BasicBlock => {
states::basic_block::Data.handle_event(&j, action)
},
CharacterState::BasicBlock(data) => data.handle_event(&j, action),
CharacterState::Roll(data) => data.handle_event(&j, action),
CharacterState::Wielding => states::wielding::Data.handle_event(&j, action),
CharacterState::Equipping(data) => data.handle_event(&j, action),
@ -357,7 +355,7 @@ impl<'a> System<'a> for Sys {
CharacterState::Sit => states::sit::Data::behavior(&states::sit::Data, &j),
CharacterState::Dance => states::dance::Data::behavior(&states::dance::Data, &j),
CharacterState::Sneak => states::sneak::Data::behavior(&states::sneak::Data, &j),
CharacterState::BasicBlock => states::basic_block::Data.behavior(&j),
CharacterState::BasicBlock(data) => data.behavior(&j),
CharacterState::Roll(data) => data.behavior(&j),
CharacterState::Wielding => states::wielding::Data.behavior(&j),
CharacterState::Equipping(data) => data.behavior(&j),

View File

@ -1,5 +1,5 @@
use common::{
combat::{AttackerInfo, TargetInfo},
combat::{AttackSource, AttackerInfo, TargetInfo},
comp::{
Body, CharacterState, Combo, Energy, Group, Health, Inventory, Melee, Ori, Pos, Scale,
Stats,
@ -147,6 +147,8 @@ impl<'a> System<'a> for Sys {
stats: read_data.stats.get(target),
health: read_data.healths.get(target),
pos: pos.0,
ori: read_data.orientations.get(target),
char_state: read_data.char_states.get(target),
};
melee_attack.attack.apply_attack(
@ -156,6 +158,7 @@ impl<'a> System<'a> for Sys {
dir,
is_dodge,
1.0,
AttackSource::Melee,
|e| server_emitter.emit(e),
|o| outcomes.push(o),
);

View File

@ -1,8 +1,8 @@
use common::{
combat::{AttackerInfo, TargetInfo},
combat::{AttackSource, AttackerInfo, TargetInfo},
comp::{
projectile, Body, Combo, Energy, Group, Health, HealthSource, Inventory, Ori, PhysicsState,
Pos, Projectile, Stats, Vel,
projectile, Body, CharacterState, Combo, Energy, Group, Health, HealthSource, Inventory,
Ori, PhysicsState, Pos, Projectile, Stats, Vel,
},
event::{EventBus, ServerEvent},
outcome::Outcome,
@ -36,6 +36,7 @@ pub struct ReadData<'a> {
combos: ReadStorage<'a, Combo>,
healths: ReadStorage<'a, Health>,
bodies: ReadStorage<'a, Body>,
character_states: ReadStorage<'a, CharacterState>,
}
/// This system is responsible for handling projectile effect triggers
@ -130,6 +131,10 @@ impl<'a> System<'a> for Sys {
stats: read_data.stats.get(target),
health: read_data.healths.get(target),
pos: pos.0,
// TODO: Let someone smarter figure this out
// ori: orientations.get(target),
ori: None,
char_state: read_data.character_states.get(target),
};
if let Some(&body) = read_data.bodies.get(entity) {
@ -152,6 +157,7 @@ impl<'a> System<'a> for Sys {
ori.look_dir(),
false,
1.0,
AttackSource::Projectile,
|e| server_emitter.emit(e),
|o| outcomes.push(o),
);

View File

@ -1,8 +1,8 @@
use common::{
combat::{AttackerInfo, TargetInfo},
combat::{AttackSource, AttackerInfo, TargetInfo},
comp::{
Body, Combo, Energy, Group, Health, HealthSource, Inventory, Ori, PhysicsState, Pos, Scale,
Shockwave, ShockwaveHitEntities, Stats,
Body, CharacterState, Combo, Energy, Group, Health, HealthSource, Inventory, Ori,
PhysicsState, Pos, Scale, Shockwave, ShockwaveHitEntities, Stats,
},
event::{EventBus, ServerEvent},
outcome::Outcome,
@ -37,6 +37,7 @@ pub struct ReadData<'a> {
energies: ReadStorage<'a, Energy>,
stats: ReadStorage<'a, Stats>,
combos: ReadStorage<'a, Combo>,
character_states: ReadStorage<'a, CharacterState>,
}
/// This system is responsible for handling accepted inputs like moving or
@ -193,6 +194,8 @@ impl<'a> System<'a> for Sys {
stats: read_data.stats.get(target),
health: read_data.healths.get(target),
pos: pos.0,
ori: read_data.orientations.get(target),
char_state: read_data.character_states.get(target),
};
shockwave.properties.attack.apply_attack(
@ -202,6 +205,7 @@ impl<'a> System<'a> for Sys {
dir,
false,
1.0,
AttackSource::Shockwave,
|e| server_emitter.emit(e),
|o| outcomes.push(o),
);

View File

@ -253,26 +253,11 @@ impl<'a> System<'a> for Sys {
energy.get_mut_unchecked().regen_rate = 0.0
}
},
// recover small amount of passive energy from blocking, and bonus energy from
// blocking attacks?
CharacterState::BasicBlock => {
let res = {
let energy = energy.get_unchecked();
energy.current() < energy.maximum()
};
if res {
energy.get_mut_unchecked().change_by(EnergyChange {
amount: -3,
source: EnergySource::Regen,
});
}
},
// Non-combat abilities that consume energy;
// temporarily stall energy gain, but preserve regen_rate.
// Abilities that temporarily stall energy gain, but preserve regen_rate.
CharacterState::Roll { .. }
| CharacterState::Climb { .. }
| CharacterState::Stunned { .. } => {},
| CharacterState::Stunned { .. }
| CharacterState::BasicBlock { .. } => {},
}
}

View File

@ -672,16 +672,31 @@ pub fn handle_explosion(server: &Server, pos: Vec3<f32>, explosion: Explosion, o
RadiusEffect::Attack(attack) => {
let energies = &ecs.read_storage::<comp::Energy>();
let combos = &ecs.read_storage::<comp::Combo>();
for (entity_b, pos_b, health_b, inventory_b_maybe, stats_b_maybe, body_b_maybe) in (
for (
entity_b,
pos_b,
health_b,
(
body_b_maybe,
inventory_b_maybe,
stats_b_maybe,
ori_b_maybe,
char_state_b_maybe,
),
) in (
&ecs.entities(),
&ecs.read_storage::<comp::Pos>(),
&ecs.read_storage::<comp::Health>(),
ecs.read_storage::<comp::Inventory>().maybe(),
ecs.read_storage::<comp::Stats>().maybe(),
ecs.read_storage::<comp::Body>().maybe(),
(
ecs.read_storage::<comp::Body>().maybe(),
ecs.read_storage::<comp::Inventory>().maybe(),
ecs.read_storage::<comp::Stats>().maybe(),
ecs.read_storage::<comp::Ori>().maybe(),
ecs.read_storage::<comp::CharacterState>().maybe(),
),
)
.join()
.filter(|(_, _, h, _, _, _)| !h.is_dead)
.filter(|(_, _, h, _)| !h.is_dead)
{
// Check if it is a hit
let strength = if let Some(body) = body_b_maybe {
@ -725,6 +740,8 @@ pub fn handle_explosion(server: &Server, pos: Vec3<f32>, explosion: Explosion, o
stats: stats_b_maybe,
health: Some(health_b),
pos,
ori: ori_b_maybe,
char_state: char_state_b_maybe,
};
let server_eventbus = ecs.read_resource::<EventBus<ServerEvent>>();
@ -736,6 +753,7 @@ pub fn handle_explosion(server: &Server, pos: Vec3<f32>, explosion: Explosion, o
dir,
false,
strength,
combat::AttackSource::Explosion,
|e| server_eventbus.emit_now(e),
|o| outcomes.push(o),
);

View File

@ -418,6 +418,15 @@ impl PlayState for SessionState {
);
}
},
GameInput::Block => {
let mut client = self.client.borrow_mut();
client.handle_input(
InputKind::Block,
state,
select_pos,
target_entity.map(|t| t.0),
);
},
GameInput::Roll => {
let mut client = self.client.borrow_mut();
if can_build {

View File

@ -111,6 +111,7 @@ impl ControlSettings {
match game_input {
GameInput::Primary => KeyMouse::Mouse(MouseButton::Left),
GameInput::Secondary => KeyMouse::Mouse(MouseButton::Right),
GameInput::Block => KeyMouse::Key(VirtualKeyCode::LAlt),
GameInput::ToggleCursor => KeyMouse::Key(VirtualKeyCode::Comma),
GameInput::Escape => KeyMouse::Key(VirtualKeyCode::Escape),
GameInput::Chat => KeyMouse::Key(VirtualKeyCode::Return),

View File

@ -21,6 +21,7 @@ use winit::monitor::VideoMode;
pub enum GameInput {
Primary,
Secondary,
Block,
Slot1,
Slot2,
Slot3,
@ -85,6 +86,7 @@ impl GameInput {
match *self {
GameInput::Primary => "gameinput.primary",
GameInput::Secondary => "gameinput.secondary",
GameInput::Block => "gameinput.block",
GameInput::ToggleCursor => "gameinput.togglecursor",
GameInput::MoveForward => "gameinput.moveforward",
GameInput::MoveLeft => "gameinput.moveleft",
@ -149,6 +151,7 @@ impl GameInput {
[
GameInput::Primary,
GameInput::Secondary,
GameInput::Block,
GameInput::ToggleCursor,
GameInput::MoveForward,
GameInput::MoveLeft,