Merge branch 'juliancoffee/glider-ability' into 'master'

Vroom Glider

See merge request veloren/veloren!4282
This commit is contained in:
Illia Denysenko 2024-03-06 13:13:49 +00:00
commit 840e95f21c
29 changed files with 551 additions and 275 deletions

View File

@ -933,6 +933,13 @@
Simple(None, "common.abilities.debug.evolve"),
],
),
Custom("Admin's Eagle"): (
primary: Simple(None, "common.abilities.debug.glide_speeder"),
secondary: Simple(None, "common.abilities.debug.glide_boost"),
abilities: [
Simple(None, "common.abilities.debug.eaglify"),
],
),
Tool(Farming): (
primary: Simple(None, "common.abilities.farming.basic"),
secondary: Simple(None, "common.abilities.farming.basic"),

View File

@ -0,0 +1,7 @@
Transform(
buildup_duration: 2.0,
recover_duration: 0.5,
target: "common.entity.wild.peaceful.eagle",
specifier: Some(Evolve),
allow_players: true,
)

View File

@ -0,0 +1,3 @@
GlideBoost(
booster: Upward(100.0),
)

View File

@ -0,0 +1,3 @@
GlideBoost(
booster: Forward(0.75),
)

View File

@ -1974,6 +1974,9 @@
Simple(
"common.items.debug.admin",
): "armor-tabard-admin_tabard",
Simple(
"common.items.debug.glider",
): "other-glider-vroom-debug",
Simple(
"common.items.debug.admin_back",
): "armor-misc-back-admin_back",

View File

@ -0,0 +1,8 @@
ItemDef(
legacy_name: "Vroom Glider",
legacy_description: "goes brrr.",
kind: Glider,
quality: Debug,
tags: [],
ability_spec: Some(Custom("Admin's Eagle")),
)

View File

@ -5,6 +5,7 @@
(Item("common.items.debug.admin_sword"),1),
(Item("common.items.debug.velorite_bow_debug"), 1),
(Item("common.items.debug.admin"),1),
(Item("common.items.debug.glider"),1),
(Item("common.items.debug.golden_cheese"),100),
],
"consumables": [

View File

@ -1,20 +1,7 @@
common-abilities-debug-possess = Possessing Arrow
.desc = Shoots a poisonous arrow. Lets you control your target.
common-abilities-debug-evolve = Evolve
.desc = You become your better self.
common-abilities-hammer-leap = Smash of Doom
.desc = An AOE attack with knockback. Leaps to position of cursor.
common-abilities-bow-shotgun = Burst
.desc = Launches a burst of arrows.
common-abilities-staff-fireshockwave = Ring of Fire
.desc = Ignites the ground with fiery shockwave.
common-abilities-sceptre-wardingaura = Warding Aura
.desc = Wards your allies against enemy attacks.
# Internal terms, currently only used in zh-Hans.
# If we remove them here, they also get auto-removed in zh-Hans,
# so please keep them, even when not used in English file.
# See https://github.com/WeblateOrg/weblate/issues/9895
## Internal terms, currently only used in zh-Hans.
## If we remove them here, they also get auto-removed in zh-Hans,
## so please keep them, even when not used in English file.
## See https://github.com/WeblateOrg/weblate/issues/9895
-heavy_stance = ""
-agile_stance = ""
@ -39,7 +26,27 @@ common-abilities-sceptre-wardingaura = Warding Aura
-enter_stance = ""
-require_stance = ""
# Sword abilities
## Debug abilities
common-abilities-debug-possess = Possessing Arrow
.desc = Shoots a poisonous arrow. Lets you control your target.
common-abilities-debug-evolve = Evolve
.desc = You become your better self.
common-abilities-debug-glide_boost = Vroom
.desc = Gives you the force to reach the sky
common-abilities-debug-glide_speeder = Vroom
.desc = Gives you the force to reach wherever your eyes look
## Hotbar abilities
common-abilities-hammer-leap = Smash of Doom
.desc = An AOE attack with knockback. Leaps to position of cursor.
common-abilities-bow-shotgun = Burst
.desc = Launches a burst of arrows
common-abilities-staff-fireshockwave = Ring of Fire
.desc = Ignites the ground with fiery shockwave.
common-abilities-sceptre-wardingaura = Warding Aura
.desc = Wards your allies against enemy attacks.
## Sword abilities
veloren-core-pseudo_abilities-sword-heavy_stance = Heavy Stance
.desc = Attacks in this stance can stagger enemies and deal more damage to staggered enemies but are slower.
veloren-core-pseudo_abilities-sword-agile_stance = Agile Stance
@ -295,7 +302,7 @@ common-abilities-sword-cleaving_sky_splitter = Sky Splitter
A powerful strike that purportedly can even split the sky, but will split through enemies.
Requires cleaving stance.
# Axe abilities
## Axe abilities
common-abilities-axe-triple_chop = Triple Chop
.desc =
Three quick strikes.

View File

@ -1,3 +1,12 @@
## internal terms, currently only used in ES
## If we remove them here, they also get auto-removed in ES,
## so please keep them, even when not used in English file.
## See https://github.com/WeblateOrg/weblate/issues/9895
-hud-skill-sc_wardaura_title = ""
-hud-skill-bow_shotgun_title = ""
-hud-skill-st_shockwave_title = ""
## SkillTree UI
hud-rank_up = New skillpoint
hud-skill-sp_available =
{ $number ->
@ -13,11 +22,6 @@ hud-skill-inc_health = Increases your maximum health by { $boost } points.{ $SP
hud-skill-inc_energy_title = Increase energy
hud-skill-inc_energy = Increases your maximum energy by { $boost } points.{ $SP }
# internally translations, used by ES file currently
-hud-skill-sc_wardaura_title = ""
-hud-skill-bow_shotgun_title = ""
-hud-skill-st_shockwave_title = ""
hud-skill-unlck_sword_title = Sword proficiency
hud-skill-unlck_sword = Unlocks the sword skill tree.{ $SP }
hud-skill-unlck_axe_title = Axe proficiency

View File

@ -51,3 +51,5 @@ weapon-sword-frost-admin_sword = Admin Greatsword
weapon-bow-velorite-debug = Admin Velorite Bow
.desc = Infused with Velorite power.
other-glider-vroom-debug = Vroom Glider
.desc = goes brrr

View File

@ -3816,6 +3816,10 @@
"voxel.weapon.tool.broom_belzeshrub_purple",
(0.0, 0.0, 0.0), (-135.0, 90.0, 0.0), 1.1,
),
Simple("common.items.debug.glider"): VoxTrans(
"voxel.glider.cloverleaf",
(-2.0, 0.0, 0.0), (-50.0, 30.0, 20.0), 0.9,
),
// Misc
Simple("common.items.weapons.tool.golf_club"): VoxTrans(
"voxel.weapon.tool.golf_club",

View File

@ -68,5 +68,9 @@
vox_spec: ("glider.winter_wings", (-26.0, -26.0, 0.0)),
color: None
),
"common.items.debug.glider": (
vox_spec: ("glider.cloverleaf", (-19.0, -5.0, 0.0)),
color: None
),
},
))

View File

@ -39,6 +39,9 @@ use std::{borrow::Cow, convert::TryFrom, time::Duration};
use super::shockwave::ShockwaveDodgeable;
pub const BASE_ABILITY_LIMIT: usize = 5;
// NOTE: different AbilitySpec on same ToolKind share the same key
/// Descriptor to pick the right (auxiliary) ability set
pub type AuxiliaryKey = (Option<ToolKind>, Option<ToolKind>);
// TODO: Potentially look into storing previous ability sets for weapon
@ -72,6 +75,25 @@ impl Default for ActiveAbilities {
}
}
// make it pub, for UI stuff, if you want
enum AbilitySource {
Weapons,
Glider,
}
impl AbilitySource {
// Get all needed data here and pick the right ability source
//
// make it pub, for UI stuff, if you want
fn determine(char_state: Option<&CharacterState>) -> Self {
if char_state.is_some_and(|c| c.is_glide_wielded()) {
Self::Glider
} else {
Self::Weapons
}
}
}
impl ActiveAbilities {
pub fn from_auxiliary(
auxiliary_sets: HashMap<AuxiliaryKey, Vec<AuxiliaryAbility>>,
@ -176,7 +198,7 @@ impl ActiveAbilities {
let ability_set = |equip_slot| {
inv.and_then(|inv| inv.equipped(equip_slot))
.map(|i| &i.item_config_expect().abilities)
.and_then(|i| i.item_config().map(|c| &c.abilities))
};
let scale_ability = |ability: CharacterAbility, equip_slot| {
@ -194,64 +216,62 @@ impl ActiveAbilities {
context_index,
};
// This function is an attempt to generalize ability handling
let inst_ability = |slot: EquipSlot, offhand: bool| {
ability_set(slot).and_then(|abilities| {
// We use AbilityInput here as an object to match on, which
// roughly corresponds to all needed data we need to know about
// ability.
use AbilityInput as I;
// Also we don't provide `ability`, nor `ability_input` as an
// argument to the closure, and that wins us a bit of code
// duplication we would need to do otherwise, but it's
// important that we can and do re-create all needed Ability
// information here to make decisions.
//
// For example, we should't take `input` argument provided to
// activate_abilities, because in case of Auxiliary abilities,
// it has wrong index.
//
// We could alternatively just take `ability`, but it works too.
let dispatched = match ability.try_ability_set_key()? {
I::Guard => abilities.guard(Some(skill_set), context),
I::Primary => abilities.primary(Some(skill_set), context),
I::Secondary => abilities.secondary(Some(skill_set), context),
I::Auxiliary(index) => abilities.auxiliary(index, Some(skill_set), context),
I::Movement => return None,
};
dispatched
.map(|(a, i)| (a.ability.clone(), i))
.map(|(a, i)| (scale_ability(a, slot), offhand, spec_ability(i)))
})
};
let source = AbilitySource::determine(char_state);
match ability {
Ability::ToolGuard => {
Ability::ToolGuard => match source {
AbilitySource::Weapons => {
let equip_slot = combat::get_equip_slot_by_block_priority(inv);
ability_set(equip_slot)
.and_then(|abilities| {
abilities
.guard(Some(skill_set), context)
.map(|(a, i)| (a.ability.clone(), i))
})
.map(|(ability, i)| {
(
scale_ability(ability, equip_slot),
matches!(equip_slot, EquipSlot::ActiveOffhand),
spec_ability(i),
)
})
inst_ability(equip_slot, matches!(equip_slot, EquipSlot::ActiveOffhand))
},
Ability::ToolPrimary => ability_set(EquipSlot::ActiveMainhand)
.and_then(|abilities| {
abilities
.primary(Some(skill_set), context)
.map(|(a, i)| (a.ability.clone(), i))
})
.map(|(ability, i)| {
(
scale_ability(ability, EquipSlot::ActiveMainhand),
false,
spec_ability(i),
)
}),
Ability::ToolSecondary => ability_set(EquipSlot::ActiveOffhand)
.and_then(|abilities| {
abilities
.secondary(Some(skill_set), context)
.map(|(a, i)| (a.ability.clone(), i))
})
.map(|(ability, i)| {
(
scale_ability(ability, EquipSlot::ActiveOffhand),
true,
spec_ability(i),
)
})
.or_else(|| {
ability_set(EquipSlot::ActiveMainhand)
.and_then(|abilities| {
abilities
.secondary(Some(skill_set), context)
.map(|(a, i)| (a.ability.clone(), i))
})
.map(|(ability, i)| {
(
scale_ability(ability, EquipSlot::ActiveMainhand),
false,
spec_ability(i),
)
})
}),
AbilitySource::Glider => None,
},
Ability::ToolPrimary => match source {
AbilitySource::Weapons => inst_ability(EquipSlot::ActiveMainhand, false),
AbilitySource::Glider => inst_ability(EquipSlot::Glider, false),
},
Ability::ToolSecondary => match source {
AbilitySource::Weapons => inst_ability(EquipSlot::ActiveOffhand, true)
.or_else(|| inst_ability(EquipSlot::ActiveMainhand, false)),
AbilitySource::Glider => inst_ability(EquipSlot::Glider, false),
},
Ability::MainWeaponAux(_) => inst_ability(EquipSlot::ActiveMainhand, false),
Ability::OffWeaponAux(_) => inst_ability(EquipSlot::ActiveOffhand, true),
Ability::GliderAux(_) => inst_ability(EquipSlot::Glider, false),
Ability::Empty => None,
Ability::SpeciesMovement => matches!(body, Some(Body::Humanoid(_)))
.then(|| CharacterAbility::default_roll(char_state))
.map(|ability| {
@ -261,44 +281,17 @@ impl ActiveAbilities {
spec_ability(None),
)
}),
Ability::MainWeaponAux(index) => ability_set(EquipSlot::ActiveMainhand)
.and_then(|abilities| {
abilities
.auxiliary(index, Some(skill_set), context)
.map(|(a, i)| (a.ability.clone(), i))
})
.map(|(ability, i)| {
(
scale_ability(ability, EquipSlot::ActiveMainhand),
false,
spec_ability(i),
)
}),
Ability::OffWeaponAux(index) => ability_set(EquipSlot::ActiveOffhand)
.and_then(|abilities| {
abilities
.auxiliary(index, Some(skill_set), context)
.map(|(a, i)| (a.ability.clone(), i))
})
.map(|(ability, i)| {
(
scale_ability(ability, EquipSlot::ActiveOffhand),
true,
spec_ability(i),
)
}),
Ability::Empty => None,
}
}
pub fn iter_available_abilities<'a>(
pub fn iter_available_abilities_on<'a>(
inv: Option<&'a Inventory>,
skill_set: Option<&'a SkillSet>,
equip_slot: EquipSlot,
) -> impl Iterator<Item = usize> + 'a {
inv.and_then(|inv| inv.equipped(equip_slot))
inv.and_then(|inv| inv.equipped(equip_slot).and_then(|i| i.item_config()))
.into_iter()
.flat_map(|i| &i.item_config_expect().abilities.abilities)
.flat_map(|config| &config.abilities.abilities)
.enumerate()
.filter_map(move |(i, a)| match a {
AbilityKind::Simple(skill, _) => skill
@ -316,15 +309,54 @@ impl ActiveAbilities {
})
}
pub fn all_available_abilities(
inv: Option<&Inventory>,
skill_set: Option<&SkillSet>,
) -> Vec<AuxiliaryAbility> {
let mut ability_buff = vec![];
// Check if uses combo of two "equal" weapons
let paired = inv
.and_then(|inv| {
let a = inv.equipped(EquipSlot::ActiveMainhand)?;
let b = inv.equipped(EquipSlot::ActiveOffhand)?;
if let (ItemKind::Tool(tool_a), ItemKind::Tool(tool_b)) = (&*a.kind(), &*b.kind()) {
Some((a.ability_spec(), tool_a.kind, b.ability_spec(), tool_b.kind))
} else {
None
}
})
.is_some_and(|(a_spec, a_kind, b_spec, b_kind)| (a_spec, a_kind) == (b_spec, b_kind));
// Push main weapon abilities
Self::iter_available_abilities_on(inv, skill_set, EquipSlot::ActiveMainhand)
.map(AuxiliaryAbility::MainWeapon)
.for_each(|a| ability_buff.push(a));
// Push secondary weapon abilities, if different
// If equal, just take the first
if !paired {
Self::iter_available_abilities_on(inv, skill_set, EquipSlot::ActiveOffhand)
.map(AuxiliaryAbility::OffWeapon)
.for_each(|a| ability_buff.push(a));
}
// Push glider abilities
Self::iter_available_abilities_on(inv, skill_set, EquipSlot::Glider)
.map(AuxiliaryAbility::Glider)
.for_each(|a| ability_buff.push(a));
ability_buff
}
fn default_ability_set<'a>(
inv: Option<&'a Inventory>,
skill_set: Option<&'a SkillSet>,
limit: Option<usize>,
) -> Vec<AuxiliaryAbility> {
let mut iter = Self::iter_available_abilities(inv, skill_set, EquipSlot::ActiveMainhand)
let mut iter = Self::iter_available_abilities_on(inv, skill_set, EquipSlot::ActiveMainhand)
.map(AuxiliaryAbility::MainWeapon)
.chain(
Self::iter_available_abilities(inv, skill_set, EquipSlot::ActiveOffhand)
Self::iter_available_abilities_on(inv, skill_set, EquipSlot::ActiveOffhand)
.map(AuxiliaryAbility::OffWeapon),
);
@ -338,6 +370,7 @@ impl ActiveAbilities {
}
}
#[derive(Debug, Copy, Clone)]
pub enum AbilityInput {
Guard,
Primary,
@ -354,21 +387,42 @@ pub enum Ability {
SpeciesMovement,
MainWeaponAux(usize),
OffWeaponAux(usize),
GliderAux(usize),
Empty,
/* For future use
* ArmorAbility(usize), */
}
impl Ability {
// Used for generic ability dispatch (inst_ability) in this file
//
// It does use AbilityInput to avoid creating just another enum, but it is
// semantically different.
fn try_ability_set_key(&self) -> Option<AbilityInput> {
let input = match self {
Self::ToolGuard => AbilityInput::Guard,
Self::ToolPrimary => AbilityInput::Primary,
Self::ToolSecondary => AbilityInput::Secondary,
Self::SpeciesMovement => AbilityInput::Movement,
Self::GliderAux(idx) | Self::OffWeaponAux(idx) | Self::MainWeaponAux(idx) => {
AbilityInput::Auxiliary(*idx)
},
Self::Empty => return None,
};
Some(input)
}
pub fn ability_id<'a>(
self,
char_state: Option<&CharacterState>,
inv: Option<&'a Inventory>,
skillset: Option<&'a SkillSet>,
skill_set: Option<&'a SkillSet>,
context: &AbilityContext,
) -> Option<&'a str> {
let ability_set = |equip_slot| {
inv.and_then(|inv| inv.equipped(equip_slot))
.map(|i| &i.item_config_expect().abilities)
.and_then(|i| i.item_config().map(|c| &c.abilities))
};
let contextual_id = |kind: Option<&'a AbilityKind<_>>| -> Option<&'a str> {
@ -383,72 +437,75 @@ impl Ability {
}
};
match self {
Ability::ToolGuard => ability_set(combat::get_equip_slot_by_block_priority(inv))
.and_then(|abilities| {
abilities
.guard(skillset, context)
.map(|a| a.0.id.as_str())
.or_else(|| {
abilities
let inst_ability = |slot: EquipSlot| {
ability_set(slot).and_then(|abilities| {
use AbilityInput as I;
let dispatched = match self.try_ability_set_key()? {
I::Guard => abilities.guard(skill_set, context),
I::Primary => abilities.primary(skill_set, context),
I::Secondary => abilities.secondary(skill_set, context),
I::Auxiliary(index) => abilities.auxiliary(index, skill_set, context),
I::Movement => return None,
};
dispatched.map(|(a, _)| a.id.as_str()).or_else(|| {
match self.try_ability_set_key()? {
I::Guard => abilities
.guard
.as_ref()
.and_then(|g| contextual_id(Some(g)))
.and_then(|g| contextual_id(Some(g))),
I::Primary => contextual_id(Some(&abilities.primary)),
I::Secondary => contextual_id(Some(&abilities.secondary)),
I::Auxiliary(index) => contextual_id(abilities.abilities.get(index)),
I::Movement => None,
}
})
}),
Ability::ToolPrimary => ability_set(EquipSlot::ActiveMainhand).and_then(|abilities| {
abilities
.primary(skillset, context)
.map(|a| a.0.id.as_str())
.or_else(|| contextual_id(Some(&abilities.primary)))
}),
Ability::ToolSecondary => ability_set(EquipSlot::ActiveOffhand)
.and_then(|abilities| {
abilities
.secondary(skillset, context)
.map(|a| a.0.id.as_str())
.or_else(|| contextual_id(Some(&abilities.secondary)))
})
.or_else(|| {
ability_set(EquipSlot::ActiveMainhand).and_then(|abilities| {
abilities
.secondary(skillset, context)
.map(|a| a.0.id.as_str())
.or_else(|| contextual_id(Some(&abilities.secondary)))
})
}),
};
let source = AbilitySource::determine(char_state);
match source {
AbilitySource::Glider => match self {
Ability::ToolGuard => None,
Ability::ToolPrimary => inst_ability(EquipSlot::Glider),
Ability::ToolSecondary => inst_ability(EquipSlot::Glider),
Ability::SpeciesMovement => None, // TODO: Make not None
Ability::MainWeaponAux(index) => {
ability_set(EquipSlot::ActiveMainhand).and_then(|abilities| {
abilities
.auxiliary(index, skillset, context)
.map(|a| a.0.id.as_str())
.or_else(|| contextual_id(abilities.abilities.get(index)))
})
},
Ability::OffWeaponAux(index) => {
ability_set(EquipSlot::ActiveOffhand).and_then(|abilities| {
abilities
.auxiliary(index, skillset, context)
.map(|a| a.0.id.as_str())
.or_else(|| contextual_id(abilities.abilities.get(index)))
})
},
Ability::MainWeaponAux(_) => inst_ability(EquipSlot::ActiveMainhand),
Ability::OffWeaponAux(_) => inst_ability(EquipSlot::ActiveOffhand),
Ability::GliderAux(_) => inst_ability(EquipSlot::Glider),
Ability::Empty => None,
},
AbilitySource::Weapons => match self {
Ability::ToolGuard => {
let equip_slot = combat::get_equip_slot_by_block_priority(inv);
inst_ability(equip_slot)
},
Ability::ToolPrimary => inst_ability(EquipSlot::ActiveMainhand),
Ability::ToolSecondary => inst_ability(EquipSlot::ActiveOffhand)
.or_else(|| inst_ability(EquipSlot::ActiveMainhand)),
Ability::SpeciesMovement => None, // TODO: Make not None
Ability::MainWeaponAux(_) => inst_ability(EquipSlot::ActiveMainhand),
Ability::OffWeaponAux(_) => inst_ability(EquipSlot::ActiveOffhand),
Ability::GliderAux(_) => inst_ability(EquipSlot::Glider),
Ability::Empty => None,
},
}
}
pub fn is_from_tool(&self) -> bool {
pub fn is_from_wielded(&self) -> bool {
match self {
Ability::ToolPrimary
| Ability::ToolSecondary
| Ability::MainWeaponAux(_)
| Ability::GliderAux(_)
| Ability::OffWeaponAux(_)
| Ability::ToolGuard => true,
Ability::SpeciesMovement | Ability::Empty => false,
}
}
}
#[derive(Copy, Clone, Serialize, Deserialize, Debug)]
pub enum GuardAbility {
Tool,
@ -471,10 +528,14 @@ pub struct SpecifiedAbility {
}
impl SpecifiedAbility {
pub fn ability_id(self, inv: Option<&Inventory>) -> Option<&str> {
pub fn ability_id<'a>(
self,
char_state: Option<&CharacterState>,
inv: Option<&'a Inventory>,
) -> Option<&'a str> {
let ability_set = |equip_slot| {
inv.and_then(|inv| inv.equipped(equip_slot))
.map(|i| &i.item_config_expect().abilities)
.and_then(|i| i.item_config().map(|c| &c.abilities))
};
fn ability_id(spec_ability: SpecifiedAbility, ability: &AbilityKind<AbilityItem>) -> &str {
@ -490,23 +551,44 @@ impl SpecifiedAbility {
}
}
match self.ability {
Ability::ToolPrimary => ability_set(EquipSlot::ActiveMainhand)
.map(|abilities| ability_id(self, &abilities.primary)),
Ability::ToolSecondary => ability_set(EquipSlot::ActiveOffhand)
.map(|abilities| ability_id(self, &abilities.secondary))
.or_else(|| {
ability_set(EquipSlot::ActiveMainhand)
.map(|abilities| ability_id(self, &abilities.secondary))
}),
Ability::ToolGuard => ability_set(combat::get_equip_slot_by_block_priority(inv))
.and_then(|abilities| abilities.guard.as_ref().map(|a| ability_id(self, a))),
Ability::SpeciesMovement => None, // TODO: Make not None
Ability::MainWeaponAux(index) => ability_set(EquipSlot::ActiveMainhand)
.and_then(|abilities| abilities.abilities.get(index).map(|a| ability_id(self, a))),
Ability::OffWeaponAux(index) => ability_set(EquipSlot::ActiveOffhand)
.and_then(|abilities| abilities.abilities.get(index).map(|a| ability_id(self, a))),
let inst_ability = |slot: EquipSlot| {
ability_set(slot).and_then(|abilities| {
use AbilityInput as I;
let dispatched = match self.ability.try_ability_set_key()? {
I::Guard => abilities.guard.as_ref(),
I::Primary => Some(&abilities.primary),
I::Secondary => Some(&abilities.secondary),
I::Auxiliary(index) => abilities.abilities.get(index),
I::Movement => return None,
};
dispatched.map(|a| ability_id(self, a))
})
};
let source = AbilitySource::determine(char_state);
match source {
AbilitySource::Glider => match self.ability {
Ability::ToolGuard => None,
Ability::ToolPrimary => inst_ability(EquipSlot::Glider),
Ability::ToolSecondary => inst_ability(EquipSlot::Glider),
Ability::SpeciesMovement => None,
Ability::MainWeaponAux(_) => inst_ability(EquipSlot::ActiveMainhand),
Ability::OffWeaponAux(_) => inst_ability(EquipSlot::ActiveOffhand),
Ability::GliderAux(_) => inst_ability(EquipSlot::Glider),
Ability::Empty => None,
},
AbilitySource::Weapons => match self.ability {
Ability::ToolGuard => inst_ability(combat::get_equip_slot_by_block_priority(inv)),
Ability::ToolPrimary => inst_ability(EquipSlot::ActiveMainhand),
Ability::ToolSecondary => inst_ability(EquipSlot::ActiveOffhand)
.or_else(|| inst_ability(EquipSlot::ActiveMainhand)),
Ability::SpeciesMovement => None, // TODO: Make not None
Ability::MainWeaponAux(_) => inst_ability(EquipSlot::ActiveMainhand),
Ability::OffWeaponAux(_) => inst_ability(EquipSlot::ActiveOffhand),
Ability::GliderAux(_) => inst_ability(EquipSlot::Glider),
Ability::Empty => None,
},
}
}
}
@ -560,6 +642,7 @@ impl From<MovementAbility> for Ability {
pub enum AuxiliaryAbility {
MainWeapon(usize),
OffWeapon(usize),
Glider(usize),
Empty,
}
@ -568,6 +651,7 @@ impl From<AuxiliaryAbility> for Ability {
match primary {
AuxiliaryAbility::MainWeapon(i) => Ability::MainWeaponAux(i),
AuxiliaryAbility::OffWeapon(i) => Ability::OffWeaponAux(i),
AuxiliaryAbility::Glider(i) => Ability::GliderAux(i),
AuxiliaryAbility::Empty => Ability::Empty,
}
}
@ -708,6 +792,11 @@ pub enum CharacterAbility {
#[serde(default)]
meta: AbilityMeta,
},
GlideBoost {
booster: glide::Boost,
#[serde(default)]
meta: AbilityMeta,
},
DashMelee {
energy_cost: f32,
energy_drain: f32,
@ -1125,6 +1214,7 @@ impl CharacterAbility {
},
CharacterAbility::ComboMeleeDeprecated { .. }
| CharacterAbility::Boost { .. }
| CharacterAbility::GlideBoost { .. }
| CharacterAbility::BasicBeam { .. }
| CharacterAbility::Blink { .. }
| CharacterAbility::Music { .. }
@ -1688,6 +1778,7 @@ impl CharacterAbility {
*buildup_duration /= stats.speed;
*recover_duration /= stats.speed;
},
GlideBoost { .. } => {},
}
self
}
@ -1724,6 +1815,7 @@ impl CharacterAbility {
}
},
Boost { .. }
| GlideBoost { .. }
| ComboMeleeDeprecated { .. }
| Blink { .. }
| Music { .. }
@ -1773,6 +1865,7 @@ impl CharacterAbility {
| RiposteMelee { .. }
| BasicBeam { .. }
| Boost { .. }
| GlideBoost { .. }
| ComboMeleeDeprecated { .. }
| Blink { .. }
| Music { .. }
@ -1801,6 +1894,7 @@ impl CharacterAbility {
| SelfBuff { meta, .. }
| BasicBeam { meta, .. }
| Boost { meta, .. }
| GlideBoost { meta, .. }
| ComboMeleeDeprecated { meta, .. }
| ComboMelee2 { meta, .. }
| Blink { meta, .. }
@ -2312,6 +2406,13 @@ impl From<(&CharacterAbility, AbilityInfo, &JoinData<'_>)> for CharacterState {
},
timer: Duration::default(),
}),
CharacterAbility::GlideBoost { booster, meta: _ } => {
let scale = data.body.dimensions().z.sqrt();
let mut glide_data = glide::Data::new(scale * 4.5, scale, *data.ori);
glide_data.booster = Some(*booster);
CharacterState::Glide(glide_data)
},
CharacterAbility::DashMelee {
energy_cost: _,
energy_drain,

View File

@ -228,6 +228,13 @@ impl CharacterState {
}
}
pub fn is_glide_wielded(&self) -> bool {
matches!(
self,
CharacterState::Glide { .. } | CharacterState::GlideWield { .. }
)
}
pub fn is_stealthy(&self) -> bool {
matches!(
self,

View File

@ -751,7 +751,8 @@ impl TryFrom<(&Item, &AbilityMap, &MaterialStatManifest)> for ItemConfig {
// TODO: Either remove msm or use it as argument in fn kind
(item, ability_map, _msm): (&Item, &AbilityMap, &MaterialStatManifest),
) -> Result<Self, Self::Error> {
if let ItemKind::Tool(tool) = &*item.kind() {
match &*item.kind() {
ItemKind::Tool(tool) => {
// If no custom ability set is specified, fall back to abilityset of tool kind.
let tool_default = |tool_kind| {
let key = &AbilitySpec::Tool(tool_kind);
@ -763,8 +764,8 @@ impl TryFrom<(&Item, &AbilityMap, &MaterialStatManifest)> for ItemConfig {
.modified_by_tool(tool, item.stats_durability_multiplier())
} else {
error!(
"Custom ability set: {:?} references non-existent set, falling back to \
default ability set.",
"Custom ability set: {:?} references non-existent set, falling back \
to default ability set.",
set_key
);
tool_default(tool.kind).cloned().unwrap_or_default()
@ -774,15 +775,23 @@ impl TryFrom<(&Item, &AbilityMap, &MaterialStatManifest)> for ItemConfig {
.modified_by_tool(tool, item.stats_durability_multiplier())
} else {
error!(
"No ability set defined for tool: {:?}, falling back to default ability set.",
"No ability set defined for tool: {:?}, falling back to default ability \
set.",
tool.kind
);
Default::default()
};
Ok(ItemConfig { abilities })
} else {
Err(ItemConfigError::BadItemKind)
},
ItemKind::Glider => item
.ability_spec()
.and_then(|set_key| ability_map.get_ability_set(&set_key))
.map(|abilities| ItemConfig {
abilities: abilities.clone(),
})
.ok_or(ItemConfigError::BadItemKind),
_ => Err(ItemConfigError::BadItemKind),
}
}
}
@ -1314,11 +1323,7 @@ impl Item {
pub fn slots_mut(&mut self) -> &mut [InvSlot] { &mut self.slots }
pub fn item_config_expect(&self) -> &ItemConfig {
self.item_config
.as_ref()
.expect("Item was expected to have an ItemConfig")
}
pub fn item_config(&self) -> Option<&ItemConfig> { self.item_config.as_deref() }
pub fn free_slots(&self) -> usize { self.slots.iter().filter(|x| x.is_none()).count() }

View File

@ -37,14 +37,13 @@ pub enum ToolKind {
Farming,
Pick,
Shovel,
/// Music Instruments
Instrument,
// npcs
/// Intended for invisible weapons (e.g. a creature using its claws or
/// biting)
Natural,
/// This is an placeholder item, it is used by non-humanoid npcs to attack
/// Music Instruments
Instrument,
/// This is an placeholder item, it is used by non-humanoid npcs to attack
Empty,
}

View File

@ -18,6 +18,14 @@ use vek::*;
const PITCH_SLOW_TIME: f32 = 0.5;
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum Boost {
/// Slowly increases XY speed
Forward(f32),
/// Gives Z impulse
Upward(f32),
}
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Data {
/// The aspect ratio is the ratio of the span squared to actual planform
@ -28,6 +36,7 @@ pub struct Data {
last_vel: Vel,
pub timer: Duration,
inputs_disabled: bool,
pub booster: Option<Boost>,
}
impl Data {
@ -47,6 +56,7 @@ impl Data {
last_vel: Vel::zero(),
timer: Duration::default(),
inputs_disabled: true,
booster: None,
}
}
@ -75,12 +85,28 @@ impl Data {
}
impl CharacterBehavior for Data {
fn behavior(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
let mut update = StateUpdate::from(data);
// reset booster
update.character = CharacterState::Glide(Self {
booster: None,
..*self
});
// If player is on ground, end glide
let gained_booster = self.booster;
handle_glider_input_or(data, &mut update, output_events, |_, _| {});
// If switched state, let it do its thing
if !matches!(update.character, CharacterState::Glide { .. }) {
return update;
}
// If player is on the ground and effectively doesn't have any gliding
// power left, end the glide
if data.physics.on_ground.is_some()
&& (data.vel.0 - data.physics.ground_vel).magnitude_squared() < 2_f32.powi(2)
&& gained_booster.is_none()
{
update.character = CharacterState::GlideWield(glide_wield::Data::from(data));
} else if data.physics.in_liquid().is_some()
@ -189,11 +215,44 @@ impl CharacterBehavior for Data {
slerp_s,
)
};
// If we gained a booster
if let Some(booster) = gained_booster {
match booster {
Boost::Upward(speed) => {
update.vel.0.z += speed * data.dt.0;
},
Boost::Forward(speed) => {
if data.physics.on_ground.is_some() {
// quality of life hack: help with starting
//
// other velocities are intentionally ignored
update.vel.0.z += 500.0 * data.dt.0;
} else {
update.vel.0.x *= 1.0 + speed * data.dt.0;
update.vel.0.y *= 1.0 + speed * data.dt.0;
}
},
}
};
// Don't override gained booster, if any, otherwise set to None
let next_booster = if let CharacterState::Glide(Data {
booster: Some(booster),
..
}) = update.character
{
Some(booster)
} else {
None
};
update.character = CharacterState::Glide(Self {
ori,
last_vel: *data.vel,
timer: tick_attack_or_default(data, self.timer, None),
inputs_disabled,
booster: next_booster,
..*self
});
}

View File

@ -66,7 +66,7 @@ impl CharacterBehavior for Data {
if input_is_pressed(data, InputKind::Roll) {
handle_input(data, output_events, &mut update, InputKind::Roll);
}
handle_wield(data, &mut update);
handle_glider_input_or(data, &mut update, output_events, handle_wield);
// If still in this state, do the things
if matches!(update.character, CharacterState::GlideWield(_)) {

View File

@ -95,7 +95,7 @@ impl CharacterBehavior for Data {
.static_data
.ability_info
.ability
.map_or(false, |a| a.ability.is_from_tool())
.map_or(false, |a| a.ability.is_from_wielded())
{
vec![BuffCategory::RemoveOnLoadoutChange]
} else {

View File

@ -1330,6 +1330,30 @@ pub fn handle_input(
}
}
// NOTE: Quality of Life hack
//
// Uses glider ability if has any, otherwise fallback
pub fn handle_glider_input_or(
data: &JoinData<'_>,
update: &mut StateUpdate,
output_events: &mut OutputEvents,
fallback_fn: fn(&JoinData<'_>, &mut StateUpdate),
) {
if data
.inventory
.and_then(|inv| inv.equipped(EquipSlot::Glider))
.and_then(|glider| glider.item_config())
.is_none()
{
fallback_fn(data, update);
return;
};
if let Some(input) = data.controller.queued_inputs.keys().next() {
handle_ability(data, update, output_events, *input);
};
}
pub fn attempt_input(
data: &JoinData<'_>,
output_events: &mut OutputEvents,

View File

@ -162,8 +162,8 @@ impl<'a> System<'a> for Sys {
CharacterState::Idle(_)
| CharacterState::Talk
| CharacterState::Dance
| CharacterState::Glide(_)
| CharacterState::Skate(_)
| CharacterState::Glide(_)
| CharacterState::GlideWield(_)
| CharacterState::Wielding(_)
| CharacterState::Equipping(_)

View File

@ -127,6 +127,7 @@ fn aux_ability_to_string(ability: comp::ability::AuxiliaryAbility) -> String {
match ability {
AuxiliaryAbility::MainWeapon(index) => format!("Main Weapon:index:{}", index),
AuxiliaryAbility::OffWeapon(index) => format!("Off Weapon:index:{}", index),
AuxiliaryAbility::Glider(index) => format!("Glider:index:{}", index),
AuxiliaryAbility::Empty => String::from("Empty"),
}
}
@ -177,6 +178,27 @@ fn aux_ability_from_string(ability: &str) -> comp::ability::AuxiliaryAbility {
AuxiliaryAbility::Empty
},
},
Some("Glider") => match parts
.next()
.map(|index| index.parse::<usize>().map_err(|_| index))
{
Some(Ok(index)) => AuxiliaryAbility::Glider(index),
Some(Err(error)) => {
dev_panic!(format!(
"Conversion from database to ability set failed. Unable to parse index for \
offhand abilities: {}",
error
));
AuxiliaryAbility::Empty
},
None => {
dev_panic!(String::from(
"Conversion from database to ability set failed. Unable to find an index for \
offhand abilities"
));
AuxiliaryAbility::Empty
},
},
Some("Empty") => AuxiliaryAbility::Empty,
unknown => {
dev_panic!(format!(

View File

@ -37,7 +37,7 @@ use common::{
RollSkill, SceptreSkill, Skill, StaffSkill, SwimSkill, SwordSkill, SKILL_MODIFIERS,
},
skillset::{SkillGroupKind, SkillSet},
Body, Energy, Health, Inventory, Poise,
Body, CharacterState, Energy, Health, Inventory, Poise,
},
consts::{ENERGY_PER_LEVEL, HP_PER_LEVEL},
};
@ -212,6 +212,7 @@ pub struct Diary<'a> {
skill_set: &'a SkillSet,
active_abilities: &'a ActiveAbilities,
inventory: &'a Inventory,
char_state: &'a CharacterState,
health: &'a Health,
energy: &'a Energy,
poise: &'a Poise,
@ -258,6 +259,7 @@ impl<'a> Diary<'a> {
skill_set: &'a SkillSet,
active_abilities: &'a ActiveAbilities,
inventory: &'a Inventory,
char_state: &'a CharacterState,
health: &'a Health,
energy: &'a Energy,
poise: &'a Poise,
@ -280,6 +282,7 @@ impl<'a> Diary<'a> {
skill_set,
active_abilities,
inventory,
char_state,
health,
energy,
poise,
@ -838,6 +841,7 @@ impl<'a> Widget for Diary<'a> {
self.inventory,
self.skill_set,
self.context,
Some(self.char_state),
),
image_source: self.imgs,
slot_manager: Some(self.slot_manager),
@ -852,7 +856,12 @@ impl<'a> Widget for Diary<'a> {
Some(self.inventory),
Some(self.skill_set),
)
.ability_id(Some(self.inventory), Some(self.skill_set), self.context);
.ability_id(
Some(self.char_state),
Some(self.inventory),
Some(self.skill_set),
self.context,
);
let (ability_title, ability_desc) = if let Some(ability_id) = ability_id {
util::ability_description(ability_id, self.localized_strings)
} else {
@ -912,59 +921,23 @@ impl<'a> Widget for Diary<'a> {
.set(state.ids.active_abilities_keys[i], ui);
}
let same_weap_kinds = self
.inventory
.equipped(EquipSlot::ActiveMainhand)
.zip(self.inventory.equipped(EquipSlot::ActiveOffhand))
.map_or(false, |(a, b)| {
if let (ItemKind::Tool(tool_a), ItemKind::Tool(tool_b)) =
(&*a.kind(), &*b.kind())
{
(a.ability_spec(), tool_a.kind) == (b.ability_spec(), tool_b.kind)
} else {
false
}
});
let main_weap_abilities = ActiveAbilities::iter_available_abilities(
let abilities: Vec<_> = ActiveAbilities::all_available_abilities(
Some(self.inventory),
Some(self.skill_set),
EquipSlot::ActiveMainhand,
)
.map(AuxiliaryAbility::MainWeapon)
.into_iter()
.map(|a| {
(
Ability::from(a).ability_id(
Some(self.char_state),
Some(self.inventory),
Some(self.skill_set),
self.context,
),
a,
)
});
let off_weap_abilities = ActiveAbilities::iter_available_abilities(
Some(self.inventory),
Some(self.skill_set),
EquipSlot::ActiveOffhand,
)
.map(AuxiliaryAbility::OffWeapon)
.map(|a| {
(
Ability::from(a).ability_id(
Some(self.inventory),
Some(self.skill_set),
self.context,
),
a,
)
});
let abilities: Vec<_> = if same_weap_kinds {
// When the weapons have the same ability kind take only the main weapons,
main_weap_abilities.collect()
} else {
main_weap_abilities.chain(off_weap_abilities).collect()
};
})
.collect();
const ABILITIES_PER_PAGE: usize = 12;
@ -1065,12 +1038,27 @@ impl<'a> Widget for Diary<'a> {
self.inventory,
self.skill_set,
self.context,
Some(self.char_state),
),
image_source: self.imgs,
slot_manager: Some(self.slot_manager),
pulse: 0.0,
};
let same_weap_kinds = self
.inventory
.equipped(EquipSlot::ActiveMainhand)
.zip(self.inventory.equipped(EquipSlot::ActiveOffhand))
.map_or(false, |(a, b)| {
if let (ItemKind::Tool(tool_a), ItemKind::Tool(tool_b)) =
(&*a.kind(), &*b.kind())
{
(a.ability_spec(), tool_a.kind) == (b.ability_spec(), tool_b.kind)
} else {
false
}
});
for (id_index, (ability_id, ability)) in abilities
.iter()
.skip(ability_start)

View File

@ -3684,6 +3684,7 @@ impl Hud {
if let (
Some(skill_set),
Some(inventory),
Some(char_state),
Some(health),
Some(energy),
Some(body),
@ -3691,6 +3692,7 @@ impl Hud {
) = (
skill_sets.get(entity),
inventories.get(entity),
char_states.get(entity),
healths.get(entity),
energies.get(entity),
bodies.get(entity),
@ -3704,6 +3706,7 @@ impl Hud {
skill_set,
active_abilities.get(entity).unwrap_or(&Default::default()),
inventory,
char_state,
health,
energy,
poise,

View File

@ -1043,6 +1043,7 @@ impl<'a> Skillbar<'a> {
.get(i)
.and_then(|a| {
Ability::from(*a).ability_id(
self.char_state,
Some(inventory),
Some(skill_set),
contexts,
@ -1118,6 +1119,7 @@ impl<'a> Skillbar<'a> {
let primary_ability_id = self.active_abilities.and_then(|a| {
Ability::from(a.primary).ability_id(
self.char_state,
Some(self.inventory),
Some(self.skillset),
self.context,
@ -1148,6 +1150,7 @@ impl<'a> Skillbar<'a> {
let secondary_ability_id = self.active_abilities.and_then(|a| {
Ability::from(a.secondary).ability_id(
self.char_state,
Some(self.inventory),
Some(self.skillset),
self.context,

View File

@ -168,7 +168,12 @@ impl<'a> SlotKey<HotbarSource<'a>, HotbarImageSource<'a>> for HotbarSlot {
a.auxiliary_set(Some(inventory), Some(skillset))
.get(i)
.and_then(|a| {
Ability::from(*a).ability_id(Some(inventory), Some(skillset), contexts)
Ability::from(*a).ability_id(
*char_state,
Some(inventory),
Some(skillset),
contexts,
)
})
});
@ -241,6 +246,7 @@ type AbilitiesSource<'a> = (
&'a Inventory,
&'a SkillSet,
&'a AbilityContext,
Option<&'a CharacterState>,
);
impl<'a> SlotKey<AbilitiesSource<'a>, img_ids::Imgs> for AbilitySlot {
@ -248,7 +254,7 @@ impl<'a> SlotKey<AbilitiesSource<'a>, img_ids::Imgs> for AbilitySlot {
fn image_key(
&self,
(active_abilities, inventory, skillset, contexts): &AbilitiesSource<'a>,
(active_abilities, inventory, skillset, contexts, char_state): &AbilitiesSource<'a>,
) -> Option<(Self::ImageKey, Option<Color>)> {
let ability_id = match self {
Self::Slot(index) => active_abilities
@ -257,10 +263,13 @@ impl<'a> SlotKey<AbilitiesSource<'a>, img_ids::Imgs> for AbilitySlot {
Some(inventory),
Some(skillset),
)
.ability_id(Some(inventory), Some(skillset), contexts),
Self::Ability(ability) => {
Ability::from(*ability).ability_id(Some(inventory), Some(skillset), contexts)
},
.ability_id(*char_state, Some(inventory), Some(skillset), contexts),
Self::Ability(ability) => Ability::from(*ability).ability_id(
*char_state,
Some(inventory),
Some(skillset),
contexts,
),
};
ability_id.map(|id| (String::from(id), None))

View File

@ -590,6 +590,9 @@ pub fn ability_image(imgs: &img_ids::Imgs, ability_id: &str) -> image::Id {
"common.abilities.music.washboard" => imgs.instrument,
"common.abilities.music.steeltonguedrum" => imgs.instrument,
"common.abilities.music.shamisen" => imgs.instrument,
// Glider
"common.abilities.debug.glide_boost" => imgs.flyingrod_m2,
"common.abilities.debug.glide_speeder" => imgs.flyingrod_m1,
_ => imgs.not_found,
}
}

View File

@ -1062,7 +1062,7 @@ impl FigureMgr {
let ability_id = character.and_then(|c| {
c.ability_info()
.and_then(|a| a.ability)
.and_then(|a| a.ability_id(inventory))
.and_then(|a| a.ability_id(Some(c), inventory))
});
let move_dir = {