Merge branch 'master' of gitlab.com:veloren/veloren into sharp/small-fixes

This commit is contained in:
Joshua Yanovski 2020-07-12 18:47:00 +02:00
commit 6332cbe006
44 changed files with 876 additions and 243 deletions

View File

@ -44,8 +44,6 @@ coverage:
- cargo build --release
- cp -r target/release/veloren-server-cli $CI_PROJECT_DIR
- cp -r target/release/veloren-voxygen $CI_PROJECT_DIR
- strip --strip-all veloren-server-cli
- strip --strip-all veloren-voxygen
artifacts:
paths:
- veloren-server-cli

View File

@ -40,6 +40,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Better pathfinding
- Bombs
- Training dummy items
- Added spin attack for axe
- Creature specific stats
### Changed

2
Cargo.lock generated
View File

@ -3717,7 +3717,7 @@ dependencies = [
[[package]]
name = "specs-idvs"
version = "0.1.0"
source = "git+https://gitlab.com/veloren/specs-idvs.git#530ebb00b60685d6d1f47a3bc16c5b1f25c5e530"
source = "git+https://gitlab.com/veloren/specs-idvs.git?branch=specs-git#fcb0b2306b571f62f9f85d89e79e087454d95efd"
dependencies = [
"specs",
]

View File

@ -72,7 +72,3 @@ debug = false
[profile.releasedebuginfo]
inherits = 'release'
debug = 1
# living on the edge
[patch.crates-io]
specs = { git = "https://github.com/amethyst/specs.git", rev = "7a2e348ab2223818bad487695c66c43db88050a5" }

BIN
assets/voxygen/element/icons/2haxe_m1.png (Stored with Git LFS)

Binary file not shown.

BIN
assets/voxygen/element/icons/skill_axespin.png (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

View File

@ -19,7 +19,7 @@ num = "0.2.0"
num_cpus = "1.10.1"
tracing = { version = "0.1", default-features = false }
rayon = "^1.3.0"
specs = "0.16.1"
specs = { git = "https://github.com/amethyst/specs.git", rev = "7a2e348ab2223818bad487695c66c43db88050a5" }
vek = { version = "0.11.0", features = ["serde"] }
hashbrown = { version = "0.7.2", features = ["rayon", "serde", "nightly"] }
authc = { git = "https://gitlab.com/veloren/auth.git", rev = "223a4097f7ebc8d451936dccb5e6517194bbf086" }

View File

@ -39,6 +39,7 @@ use network::{Address, Network, Participant, Pid, Stream, PROMISES_CONSISTENCY,
use num::traits::FloatConst;
use rayon::prelude::*;
use std::{
collections::VecDeque,
net::SocketAddr,
sync::Arc,
time::{Duration, Instant},
@ -58,6 +59,7 @@ const SERVER_TIMEOUT: f64 = 20.0;
// After this duration has elapsed, the user will begin getting kick warnings in
// their chat window
const SERVER_TIMEOUT_GRACE_PERIOD: f64 = 14.0;
const PING_ROLLING_AVERAGE_SECS: usize = 10;
pub enum Event {
Chat(comp::ChatMsg),
@ -102,6 +104,7 @@ pub struct Client {
last_server_ping: f64,
last_server_pong: f64,
last_ping_delta: f64,
ping_deltas: VecDeque<f64>,
tick: u64,
state: State,
@ -344,6 +347,7 @@ impl Client {
last_server_ping: 0.0,
last_server_pong: 0.0,
last_ping_delta: 0.0,
ping_deltas: VecDeque::new(),
tick: 0,
state,
@ -1030,8 +1034,15 @@ impl Client {
},
ServerMsg::Pong => {
self.last_server_pong = self.state.get_time();
self.last_ping_delta = self.state.get_time() - self.last_server_ping;
self.last_ping_delta = (self.state.get_time() - self.last_server_ping).round();
// Maintain the correct number of deltas for calculating the rolling average
// ping. The client sends a ping to the server every second so we should be
// receiving a pong reply roughly every second.
while self.ping_deltas.len() > PING_ROLLING_AVERAGE_SECS - 1 {
self.ping_deltas.pop_front();
}
self.ping_deltas.push_back(self.last_ping_delta);
},
ServerMsg::ChatMsg(m) => frontend_events.push(Event::Chat(m)),
ServerMsg::SetPlayerEntity(uid) => {
@ -1174,6 +1185,22 @@ impl Client {
pub fn get_ping_ms(&self) -> f64 { self.last_ping_delta * 1000.0 }
pub fn get_ping_ms_rolling_avg(&self) -> f64 {
let mut total_weight = 0.;
let pings = self.ping_deltas.len() as f64;
(self
.ping_deltas
.iter()
.enumerate()
.fold(0., |acc, (i, ping)| {
let weight = i as f64 + 1. / pings;
total_weight += weight;
acc + (weight * ping)
})
/ total_weight)
* 1000.0
}
/// Get a reference to the client's worker thread pool. This pool should be
/// used for any computationally expensive operations that run outside
/// of the main thread (i.e., threads that block on I/O operations are
@ -1197,6 +1224,18 @@ impl Client {
.collect()
}
/// Return true if this client is an admin on the server
pub fn is_admin(&self) -> bool {
let client_uid = self
.state
.read_component_cloned::<Uid>(self.entity)
.expect("Client doesn't have a Uid!!!");
self.player_list
.get(&client_uid)
.map_or(false, |info| info.is_admin)
}
/// Clean client ECS state
fn clean_state(&mut self) {
let client_uid = self

View File

@ -8,9 +8,9 @@ edition = "2018"
no-assets = []
[dependencies]
specs-idvs = { git = "https://gitlab.com/veloren/specs-idvs.git" }
specs-idvs = { git = "https://gitlab.com/veloren/specs-idvs.git", branch = "specs-git" }
specs = { version = "0.16.1", features = ["serde", "storage-event-control"] }
specs = { git = "https://github.com/amethyst/specs.git", features = ["serde", "storage-event-control"], rev = "7a2e348ab2223818bad487695c66c43db88050a5" }
vek = { version = "0.11.0", features = ["serde"] }
dot_vox = "4.0"
image = { version = "0.22.5", default-features = false, features = ["png"] }

View File

@ -20,6 +20,7 @@ pub enum CharacterAbilityType {
BasicBlock,
TripleStrike(Stage),
LeapMelee,
SpinMelee,
}
impl From<&CharacterState> for CharacterAbilityType {
@ -32,6 +33,7 @@ impl From<&CharacterState> for CharacterAbilityType {
CharacterState::BasicBlock => Self::BasicBlock,
CharacterState::LeapMelee(_) => Self::LeapMelee,
CharacterState::TripleStrike(data) => Self::TripleStrike(data.stage),
CharacterState::SpinMelee(_) => Self::SpinMelee,
_ => Self::BasicMelee,
}
}
@ -80,6 +82,12 @@ pub enum CharacterAbility {
recover_duration: Duration,
base_damage: u32,
},
SpinMelee {
energy_cost: u32,
buildup_duration: Duration,
recover_duration: Duration,
base_damage: u32,
},
}
impl CharacterAbility {
@ -117,6 +125,10 @@ impl CharacterAbility {
.energy
.try_change_by(-(*energy_cost as i32), EnergySource::Ability)
.is_ok(),
CharacterAbility::SpinMelee { energy_cost, .. } => update
.energy
.try_change_by(-(*energy_cost as i32), EnergySource::Ability)
.is_ok(),
_ => true,
}
}
@ -239,6 +251,26 @@ impl From<&CharacterAbility> for CharacterState {
recover_duration: *recover_duration,
base_damage: *base_damage,
}),
CharacterAbility::SpinMelee {
energy_cost,
buildup_duration,
recover_duration,
base_damage,
} => CharacterState::SpinMelee(spin_melee::Data {
exhausted: false,
energy_cost: *energy_cost,
buildup_duration: *buildup_duration,
buildup_duration_default: *buildup_duration,
recover_duration: *recover_duration,
recover_duration_default: *recover_duration,
base_damage: *base_damage,
// This isn't needed for it's continuous implementation, but is left in should this
// skill be moved to the skillbar
hits_remaining: 1,
hits_remaining_default: 1, /* Should be the same value as hits_remaining, also
* this value can be removed if ability moved to
* skillbar */
}),
}
}
}

View File

@ -64,6 +64,8 @@ pub enum CharacterState {
TripleStrike(triple_strike::Data),
/// A leap followed by a small aoe ground attack
LeapMelee(leap_melee::Data),
/// Spin around, dealing damage to enemies surrounding you
SpinMelee(spin_melee::Data),
}
impl CharacterState {
@ -75,7 +77,8 @@ impl CharacterState {
| CharacterState::DashMelee(_)
| CharacterState::TripleStrike(_)
| CharacterState::BasicBlock
| CharacterState::LeapMelee(_) => true,
| CharacterState::LeapMelee(_)
| CharacterState::SpinMelee(_) => true,
_ => false,
}
}
@ -86,7 +89,8 @@ impl CharacterState {
| CharacterState::BasicRanged(_)
| CharacterState::DashMelee(_)
| CharacterState::TripleStrike(_)
| CharacterState::LeapMelee(_) => true,
| CharacterState::LeapMelee(_)
| CharacterState::SpinMelee(_) => true,
_ => false,
}
}

View File

@ -221,13 +221,11 @@ impl Tool {
base_damage: 7,
needs_timing: true,
},
BasicMelee {
SpinMelee {
energy_cost: 100,
buildup_duration: Duration::from_millis(700),
recover_duration: Duration::from_millis(100),
base_healthchange: -12,
range: 3.5,
max_angle: 30.0,
buildup_duration: Duration::from_millis(125),
recover_duration: Duration::from_millis(125),
base_damage: 5,
},
],
Hammer(_) => vec![

View File

@ -131,6 +131,81 @@ pub struct Stats {
pub fitness: u32,
pub willpower: u32,
pub is_dead: bool,
pub body_type: Body,
}
impl Body {
pub fn base_health(&self) -> u32 {
match self {
Body::Humanoid(_) => 52,
Body::QuadrupedSmall(_) => 44,
Body::QuadrupedMedium(_) => 72,
Body::BirdMedium(_) => 36,
Body::FishMedium(_) => 32,
Body::Dragon(_) => 256,
Body::BirdSmall(_) => 24,
Body::FishSmall(_) => 20,
Body::BipedLarge(_) => 144,
Body::Object(_) => 100,
Body::Golem(_) => 168,
Body::Critter(_) => 32,
Body::QuadrupedLow(_) => 64,
}
}
pub fn base_health_increase(&self) -> u32 {
match self {
Body::Humanoid(_) => 5,
Body::QuadrupedSmall(_) => 4,
Body::QuadrupedMedium(_) => 7,
Body::BirdMedium(_) => 4,
Body::FishMedium(_) => 3,
Body::Dragon(_) => 26,
Body::BirdSmall(_) => 2,
Body::FishSmall(_) => 2,
Body::BipedLarge(_) => 14,
Body::Object(_) => 0,
Body::Golem(_) => 17,
Body::Critter(_) => 3,
Body::QuadrupedLow(_) => 6,
}
}
pub fn base_exp(&self) -> u32 {
match self {
Body::Humanoid(_) => 15,
Body::QuadrupedSmall(_) => 12,
Body::QuadrupedMedium(_) => 28,
Body::BirdMedium(_) => 10,
Body::FishMedium(_) => 8,
Body::Dragon(_) => 160,
Body::BirdSmall(_) => 5,
Body::FishSmall(_) => 4,
Body::BipedLarge(_) => 75,
Body::Object(_) => 0,
Body::Golem(_) => 75,
Body::Critter(_) => 8,
Body::QuadrupedLow(_) => 24,
}
}
pub fn base_exp_increase(&self) -> u32 {
match self {
Body::Humanoid(_) => 3,
Body::QuadrupedSmall(_) => 2,
Body::QuadrupedMedium(_) => 6,
Body::BirdMedium(_) => 2,
Body::FishMedium(_) => 2,
Body::Dragon(_) => 32,
Body::BirdSmall(_) => 1,
Body::FishSmall(_) => 1,
Body::BipedLarge(_) => 15,
Body::Object(_) => 0,
Body::Golem(_) => 15,
Body::Critter(_) => 2,
Body::QuadrupedLow(_) => 5,
}
}
}
impl Stats {
@ -143,7 +218,10 @@ impl Stats {
}
// TODO: Delete this once stat points will be a thing
pub fn update_max_hp(&mut self) { self.health.set_maximum(52 + 3 * self.level.amount); }
pub fn update_max_hp(&mut self, body: Body) {
self.health
.set_maximum(body.base_health() + body.base_health_increase() * self.level.amount);
}
}
impl Stats {
@ -187,9 +265,10 @@ impl Stats {
fitness,
willpower,
is_dead: false,
body_type: body,
};
stats.update_max_hp();
stats.update_max_hp(body);
stats
.health
.set_to(stats.health.maximum(), HealthSource::Revive);

View File

@ -2,7 +2,7 @@ use crate::{
assets,
comp::{
item::{Item, ItemKind},
CharacterAbility, ItemConfig, Loadout,
Body, CharacterAbility, ItemConfig, Loadout,
},
};
use std::time::Duration;
@ -24,6 +24,44 @@ use std::time::Duration;
/// ```
pub struct LoadoutBuilder(Loadout);
impl Body {
pub fn base_dmg(&self) -> u32 {
match self {
Body::Humanoid(_) => 8,
Body::QuadrupedSmall(_) => 7,
Body::QuadrupedMedium(_) => 10,
Body::BirdMedium(_) => 6,
Body::FishMedium(_) => 5,
Body::Dragon(_) => 75,
Body::BirdSmall(_) => 4,
Body::FishSmall(_) => 3,
Body::BipedLarge(_) => 30,
Body::Object(_) => 0,
Body::Golem(_) => 30,
Body::Critter(_) => 6,
Body::QuadrupedLow(_) => 9,
}
}
pub fn base_range(&self) -> f32 {
match self {
Body::Humanoid(_) => 5.0,
Body::QuadrupedSmall(_) => 4.5,
Body::QuadrupedMedium(_) => 5.5,
Body::BirdMedium(_) => 3.5,
Body::FishMedium(_) => 3.5,
Body::Dragon(_) => 12.5,
Body::BirdSmall(_) => 3.0,
Body::FishSmall(_) => 3.0,
Body::BipedLarge(_) => 10.0,
Body::Object(_) => 3.0,
Body::Golem(_) => 7.5,
Body::Critter(_) => 3.0,
Body::QuadrupedLow(_) => 4.5,
}
}
}
impl LoadoutBuilder {
#[allow(clippy::new_without_default)] // TODO: Pending review in #587
pub fn new() -> Self {
@ -63,7 +101,7 @@ impl LoadoutBuilder {
}
/// Default animal configuration
pub fn animal() -> Self {
pub fn animal(body: Body) -> Self {
Self(Loadout {
active_item: Some(ItemConfig {
item: assets::load_expect_cloned("common.items.weapons.empty"),
@ -71,8 +109,8 @@ impl LoadoutBuilder {
energy_cost: 10,
buildup_duration: Duration::from_millis(600),
recover_duration: Duration::from_millis(100),
base_healthchange: -6,
range: 5.0,
base_healthchange: -(body.base_dmg() as i32),
range: body.base_range(),
max_angle: 80.0,
}),
ability2: None,

View File

@ -12,6 +12,7 @@ pub mod idle;
pub mod leap_melee;
pub mod roll;
pub mod sit;
pub mod spin_melee;
pub mod triple_strike;
pub mod utils;
pub mod wielding;

View File

@ -0,0 +1,146 @@
use crate::{
comp::{Attacking, CharacterState, EnergySource, StateUpdate},
sys::character_behavior::*,
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
use vek::Vec3;
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize, Eq, Hash)]
pub struct Data {
/// How long until the state attacks
pub buildup_duration: Duration,
/// Allows for buildup_duration to be reset to default value
pub buildup_duration_default: Duration,
/// How long until state ends
pub recover_duration: Duration,
/// Allows for recover_duration to be reset to default value
pub recover_duration_default: Duration,
/// Base damage
pub base_damage: u32,
/// Whether the attack can deal more damage
pub exhausted: bool,
/// How many hits it can do before ending
pub hits_remaining: u32,
/// Allows for hits_remaining to be reset to default value
pub hits_remaining_default: u32,
/// Energy cost per attack
pub energy_cost: u32,
}
const MOVE_SPEED: f32 = 5.0;
impl CharacterBehavior for Data {
fn behavior(&self, data: &JoinData) -> StateUpdate {
let mut update = StateUpdate::from(data);
if self.buildup_duration != Duration::default() {
// Allows for moving
update.vel.0 =
Vec3::new(data.inputs.move_dir.x, data.inputs.move_dir.y, 0.0) * MOVE_SPEED;
update.character = CharacterState::SpinMelee(Data {
buildup_duration: self
.buildup_duration
.checked_sub(Duration::from_secs_f32(data.dt.0))
.unwrap_or_default(),
buildup_duration_default: self.buildup_duration_default,
recover_duration: self.recover_duration,
recover_duration_default: self.recover_duration_default,
base_damage: self.base_damage,
exhausted: self.exhausted,
hits_remaining: self.hits_remaining,
hits_remaining_default: self.hits_remaining_default,
energy_cost: self.energy_cost,
});
} else if !self.exhausted {
//Hit attempt
data.updater.insert(data.entity, Attacking {
base_healthchange: -(self.base_damage as i32),
range: 3.5,
max_angle: 360_f32.to_radians(),
applied: false,
hit_count: 0,
knockback: 0.0,
});
update.character = CharacterState::SpinMelee(Data {
buildup_duration: self.buildup_duration,
buildup_duration_default: self.buildup_duration_default,
recover_duration: self.recover_duration,
recover_duration_default: self.recover_duration_default,
base_damage: self.base_damage,
exhausted: true,
hits_remaining: self.hits_remaining - 1,
hits_remaining_default: self.hits_remaining_default,
energy_cost: self.energy_cost,
});
} else if self.recover_duration != Duration::default() {
// Allows for moving
update.vel.0 =
Vec3::new(data.inputs.move_dir.x, data.inputs.move_dir.y, 0.0) * MOVE_SPEED;
update.character = CharacterState::SpinMelee(Data {
buildup_duration: self.buildup_duration,
buildup_duration_default: self.buildup_duration_default,
recover_duration: self
.recover_duration
.checked_sub(Duration::from_secs_f32(data.dt.0))
.unwrap_or_default(),
recover_duration_default: self.recover_duration_default,
base_damage: self.base_damage,
exhausted: self.exhausted,
hits_remaining: self.hits_remaining,
hits_remaining_default: self.hits_remaining_default,
energy_cost: self.energy_cost,
});
} else if self.hits_remaining != 0 {
// Allows for one ability usage to have multiple hits
// This isn't needed for it's continuous implementation, but is left in should
// this skill be moved to the skillbar
update.character = CharacterState::SpinMelee(Data {
buildup_duration: self.buildup_duration_default,
buildup_duration_default: self.buildup_duration_default,
recover_duration: self.recover_duration_default,
recover_duration_default: self.recover_duration_default,
base_damage: self.base_damage,
exhausted: false,
hits_remaining: self.hits_remaining,
hits_remaining_default: self.hits_remaining_default,
energy_cost: self.energy_cost,
});
} else if update.energy.current() >= self.energy_cost && data.inputs.secondary.is_pressed()
{
update.character = CharacterState::SpinMelee(Data {
buildup_duration: self.buildup_duration_default,
buildup_duration_default: self.buildup_duration_default,
recover_duration: self.recover_duration_default,
recover_duration_default: self.recover_duration_default,
base_damage: self.base_damage,
exhausted: false,
hits_remaining: self.hits_remaining_default,
hits_remaining_default: self.hits_remaining_default,
energy_cost: self.energy_cost,
});
// Consumes energy if there's enough left and RMB is held down
update
.energy
.change_by(-(self.energy_cost as i32), EnergySource::Ability);
} else {
// Done
update.character = CharacterState::Wielding;
// Make sure attack component is removed
data.updater.remove::<Attacking>(data.entity);
}
// Grant energy on successful hit
if let Some(attack) = data.attacking {
if attack.applied && attack.hit_count > 0 {
data.updater.remove::<Attacking>(data.entity);
update.energy.change_by(10, EnergySource::HitEnemy);
}
}
update
}
}

View File

@ -244,6 +244,7 @@ impl<'a> System<'a> for Sys {
CharacterState::Boost(data) => data.handle_event(&j, action),
CharacterState::DashMelee(data) => data.handle_event(&j, action),
CharacterState::LeapMelee(data) => data.handle_event(&j, action),
CharacterState::SpinMelee(data) => data.handle_event(&j, action),
};
local_emitter.append(&mut state_update.local_events);
server_emitter.append(&mut state_update.server_events);
@ -269,6 +270,7 @@ impl<'a> System<'a> for Sys {
CharacterState::Boost(data) => data.behavior(&j),
CharacterState::DashMelee(data) => data.behavior(&j),
CharacterState::LeapMelee(data) => data.behavior(&j),
CharacterState::SpinMelee(data) => data.behavior(&j),
};
local_emitter.append(&mut state_update.local_events);

View File

@ -1,5 +1,8 @@
use crate::{
comp::{CharacterState, ControlEvent, Controller},
comp::{
slot::{EquipSlot, Slot},
CharacterState, ControlEvent, Controller, InventoryManip,
},
event::{EventBus, LocalEvent, ServerEvent},
state::DeltaTime,
sync::{Uid, UidAllocator},
@ -79,7 +82,18 @@ impl<'a> System<'a> for Sys {
server_emitter.emit(ServerEvent::ToggleLantern(entity))
},
ControlEvent::InventoryManip(manip) => {
// Unwield if a wielded equipment slot is being modified, to avoid entering
// a barehanded wielding state.
if character_state.is_wield() {
match manip {
InventoryManip::Drop(Slot::Equip(EquipSlot::Mainhand))
| InventoryManip::Swap(_, Slot::Equip(EquipSlot::Mainhand))
| InventoryManip::Swap(Slot::Equip(EquipSlot::Mainhand), _) => {
*character_state = CharacterState::Idle;
},
_ => (),
}
}
server_emitter.emit(ServerEvent::InventoryManip(entity, manip))
},
ControlEvent::Respawn => server_emitter.emit(ServerEvent::Respawn(entity)),

View File

@ -62,7 +62,7 @@ impl<'a> System<'a> for Sys {
server_event_emitter.emit(ServerEvent::LevelUp(entity, stat.level.level()));
}
stat.update_max_hp();
stat.update_max_hp(stat.body_type);
stat.health
.set_to(stat.health.maximum(), HealthSource::LevelUp);
}
@ -103,6 +103,7 @@ impl<'a> System<'a> for Sys {
CharacterState::BasicMelee { .. }
| CharacterState::DashMelee { .. }
| CharacterState::LeapMelee { .. }
| CharacterState::SpinMelee { .. }
| CharacterState::TripleStrike { .. }
| CharacterState::BasicRanged { .. } => {
if energy.get_unchecked().regen_rate != 0.0 {

View File

@ -13,10 +13,10 @@ common = { package = "veloren-common", path = "../common" }
world = { package = "veloren-world", path = "../world" }
network = { package = "veloren_network", path = "../network", default-features = false }
specs-idvs = { git = "https://gitlab.com/veloren/specs-idvs.git" }
specs-idvs = { git = "https://gitlab.com/veloren/specs-idvs.git", branch = "specs-git" }
tracing = "0.1"
specs = { version = "0.16.1", features = ["shred-derive"] }
specs = { git = "https://github.com/amethyst/specs.git", features = ["shred-derive"], rev = "7a2e348ab2223818bad487695c66c43db88050a5" }
vek = { version = "0.11.0", features = ["serde"] }
uvth = "3.1.1"
futures-util = "0.3"

View File

@ -544,7 +544,7 @@ fn handle_spawn(
.create_npc(
pos,
comp::Stats::new(get_npc_name(id).into(), body),
LoadoutBuilder::animal().build(),
LoadoutBuilder::animal(body).build(),
body,
)
.with(comp::Vel(vel))
@ -1568,7 +1568,7 @@ fn handle_set_level(
{
stats.level.set_level(lvl);
stats.update_max_hp();
stats.update_max_hp(stats.body_type);
stats
.health
.set_to(stats.health.maximum(), comp::HealthSource::LevelUp);

View File

@ -63,9 +63,12 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, cause: HealthSourc
if let Some(attacker_stats) = stats.get_mut(attacker) {
// TODO: Discuss whether we should give EXP by Player
// Killing or not.
attacker_stats
.exp
.change_by((entity_stats.level.level() * 10) as i64);
attacker_stats.exp.change_by(
(entity_stats.body_type.base_exp()
+ entity_stats.level.level()
* entity_stats.body_type.base_exp_increase())
as i64,
);
}
});
}

View File

@ -0,0 +1 @@
-- Nothing to undo since up.sql only creates missing inventory/loadout records

View File

@ -0,0 +1,15 @@
-- This migration was added in conjunction with a change that no longer creates missing inventory/loadout records
-- for characters that don't have one, to prevent such characters from being unable to log in.
-- Create a default loadout for any characters that don't have one
INSERT INTO loadout (character_id, items)
SELECT c."id", "{""active_item"":{""item"":{""name"":""Battered Sword"",""description"":""Two-Hand Sword\n\nPower: 2-10\n\nHeld together by Rust and hope.\n\n<Right-Click to use>"",""kind"":{""Tool"":{""kind"":{""Sword"":""BasicSword""},""equip_time_millis"":300}}},""ability1"":{""TripleStrike"":{""base_damage"":5,""needs_timing"":false}},""ability2"":{""DashMelee"":{""energy_cost"":700,""buildup_duration"":{""secs"":0,""nanos"":500000000},""recover_duration"":{""secs"":0,""nanos"":500000000},""base_damage"":10}},""ability3"":null,""block_ability"":""BasicBlock"",""dodge_ability"":""Roll""},""second_item"":null,""shoulder"":null,""chest"":{""name"":""Rugged Shirt"",""description"":""Chest\n\nArmor: 0\n\nSmells like Adventure.\n\n<Right-Click to use>"",""kind"":{""Armor"":{""kind"":{""Chest"":""Rugged0""},""stats"":20}}},""belt"":null,""hand"":null,""pants"":{""name"":""Rugged Commoner's Pants"",""description"":""Legs\n\nArmor: 0\n\nThey remind you of the old days.\n\n<Right-Click to use>"",""kind"":{""Armor"":{""kind"":{""Pants"":""Rugged0""},""stats"":20}}},""foot"":{""name"":""Worn out Sandals"",""description"":""Feet\n\nArmor: 0\n\nLoyal companions.\n\n<Right-Click to use>"",""kind"":{""Armor"":{""kind"":{""Foot"":""Sandal0""},""stats"":20}}},""back"":null,""ring"":null,""neck"":null,""lantern"":{""name"":""Black Lantern"",""description"":""Used by city guards."",""kind"":{""Lantern"":{""kind"":""Black0"",""color"":{""r"":255,""g"":190,""b"":75},""strength_thousandths"":3000,""flicker_thousandths"":300}}},""head"":null,""tabard"":null}"
FROM character c
WHERE (SELECT COUNT(1) FROM loadout WHERE character_id = c.id) = 0;
-- Create a default inventory for any characters that don't have one
INSERT INTO inventory (character_id, items)
SELECT c."id", "{""slots"":[{""name"":""Dwarven Cheese"",""description"":""Restores 15 Health\n\nAromatic and nutritious\n\n<Right-Click to use>"",""kind"":{""Consumable"":{""kind"":""Cheese"",""effect"":{""Health"":{""amount"":15,""cause"":""Item""}},""amount"":1}}},{""name"":""Apple"",""description"":""Restores 20 Health\n\nRed and juicy\n\n<Right-Click to use>"",""kind"":{""Consumable"":{""kind"":""Apple"",""effect"":{""Health"":{""amount"":20,""cause"":""Item""}},""amount"":1}}},null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],""amount"":2}"
FROM character c
WHERE (SELECT COUNT(1) FROM inventory WHERE character_id = c.id) = 0;

View File

@ -0,0 +1 @@
-- Nothing to undo since up.sql only fixes corrupt JSON in loadouts

View File

@ -0,0 +1,7 @@
-- This is a migration to fix loadouts that contain old versions of items with the DashMelee skill before the
-- energy_cost field was added to it in https://gitlab.com/veloren/veloren/-/merge_requests/1140
-- This missing field in the JSON prevents accounts with affected characters from being able log in due to JSON
-- deserialization failure.
UPDATE loadout
SET items = REPLACE(items, '{"DashMelee":{"buildup_duration"','{"DashMelee":{"energy_cost":700,"buildup_duration"')
WHERE items LIKE '%DashMelee%';

View File

@ -236,71 +236,35 @@ impl Drop for CharacterLoader {
fn load_character_data(player_uuid: &str, character_id: i32, db_dir: &str) -> CharacterDataResult {
let connection = establish_connection(db_dir);
let (character_data, body_data, stats_data, maybe_inventory, maybe_loadout) =
schema::character::dsl::character
let result = schema::character::dsl::character
.filter(schema::character::id.eq(character_id))
.filter(schema::character::player_uuid.eq(player_uuid))
.inner_join(schema::body::table)
.inner_join(schema::stats::table)
.left_join(schema::inventory::table)
.left_join(schema::loadout::table)
.first::<(Character, Body, Stats, Option<Inventory>, Option<Loadout>)>(&connection)?;
.inner_join(schema::inventory::table)
.inner_join(schema::loadout::table)
.first::<(Character, Body, Stats, Inventory, Loadout)>(&connection);
Ok((
match result {
Ok((character_data, body_data, stats_data, inventory, loadout)) => Ok((
comp::Body::from(&body_data),
comp::Stats::from(StatsJoinData {
alias: &character_data.alias,
body: &comp::Body::from(&body_data),
stats: &stats_data,
}),
maybe_inventory.map_or_else(
|| {
// If no inventory record was found for the character, create it now
let row = Inventory::from((character_data.id, comp::Inventory::default()));
if let Err(error) = diesel::insert_into(schema::inventory::table)
.values(&row)
.execute(&connection)
{
warn!(
"Failed to create an inventory record for character {}: {}",
&character_data.id, error
)
}
comp::Inventory::default()
},
comp::Inventory::from,
),
maybe_loadout.map_or_else(
|| {
// Create if no record was found
let default_loadout = LoadoutBuilder::new()
.defaults()
.active_item(LoadoutBuilder::default_item_config_from_str(
character_data.tool.as_deref(),
))
.build();
let row = NewLoadout::from((character_data.id, &default_loadout));
if let Err(e) = diesel::insert_into(schema::loadout::table)
.values(&row)
.execute(&connection)
{
let char_id = character_data.id;
warn!(
comp::Inventory::from(inventory),
comp::Loadout::from(&loadout),
)),
Err(e) => {
error!(
?e,
?char_id,
"Failed to create an loadout record for character",
)
}
default_loadout
?character_id,
"Failed to load character data for character"
);
Err(Error::CharacterDataError)
},
|data| comp::Loadout::from(&data),
),
))
}
}
/// Loads a list of characters belonging to the player. This data is a small
@ -311,31 +275,22 @@ fn load_character_data(player_uuid: &str, character_id: i32, db_dir: &str) -> Ch
/// stats, body, etc...) the character is skipped, and no entry will be
/// returned.
fn load_character_list(player_uuid: &str, db_dir: &str) -> CharacterListResult {
let data = schema::character::dsl::character
let result = schema::character::dsl::character
.filter(schema::character::player_uuid.eq(player_uuid))
.order(schema::character::id.desc())
.inner_join(schema::body::table)
.inner_join(schema::stats::table)
.left_join(schema::loadout::table)
.load::<(Character, Body, Stats, Option<Loadout>)>(&establish_connection(db_dir))?;
.inner_join(schema::loadout::table)
.load::<(Character, Body, Stats, Loadout)>(&establish_connection(db_dir));
Ok(data
match result {
Ok(data) => Ok(data
.iter()
.map(|(character_data, body_data, stats_data, maybe_loadout)| {
.map(|(character_data, body_data, stats_data, loadout)| {
let character = CharacterData::from(character_data);
let body = comp::Body::from(body_data);
let level = stats_data.level as usize;
let loadout = maybe_loadout.as_ref().map_or_else(
|| {
LoadoutBuilder::new()
.defaults()
.active_item(LoadoutBuilder::default_item_config_from_str(
character.tool.as_deref(),
))
.build()
},
comp::Loadout::from,
);
let loadout = comp::Loadout::from(loadout);
CharacterItem {
character,
@ -344,7 +299,12 @@ fn load_character_list(player_uuid: &str, db_dir: &str) -> CharacterListResult {
loadout,
}
})
.collect())
.collect()),
Err(e) => {
error!(?e, ?player_uuid, "Failed to load character list for player");
Err(Error::CharacterDataError)
},
}
}
/// Create a new character with provided comp::Character and comp::Body data.

View File

@ -2,7 +2,7 @@ extern crate serde_json;
use super::schema::{body, character, inventory, loadout, stats};
use crate::comp;
use common::{character::Character as CharacterData, LoadoutBuilder};
use common::character::Character as CharacterData;
use diesel::sql_types::Text;
use serde::{Deserialize, Serialize};
use tracing::warn;
@ -106,7 +106,7 @@ impl From<StatsJoinData<'_>> for comp::Stats {
base_stats.exp.set_current(data.stats.exp as u32);
base_stats.update_max_hp();
base_stats.update_max_hp(base_stats.body_type);
base_stats
.health
.set_to(base_stats.health.maximum(), comp::HealthSource::Revive);
@ -212,14 +212,7 @@ where
bytes: Option<&<DB as diesel::backend::Backend>::RawValue>,
) -> diesel::deserialize::Result<Self> {
let t = String::from_sql(bytes)?;
match serde_json::from_str(&t) {
Ok(data) => Ok(Self(data)),
Err(e) => {
warn!(?e, "Failed to deserialise inventory data");
Ok(Self(comp::Inventory::default()))
},
}
serde_json::from_str(&t).map_err(Box::from)
}
}
@ -298,23 +291,7 @@ where
bytes: Option<&<DB as diesel::backend::Backend>::RawValue>,
) -> diesel::deserialize::Result<Self> {
let t = String::from_sql(bytes)?;
match serde_json::from_str(&t) {
Ok(data) => Ok(Self(data)),
Err(e) => {
warn!(?e, "Failed to deserialise loadout data");
// We don't have a weapon reference here, so we default to sword
let loadout = LoadoutBuilder::new()
.defaults()
.active_item(LoadoutBuilder::default_item_config_from_str(Some(
"common.items.weapons.sword.starter_sword",
)))
.build();
Ok(Self(loadout))
},
}
serde_json::from_str(&t).map_err(Box::from)
}
}

View File

@ -215,7 +215,7 @@ impl<'a> System<'a> for Sys {
head: None,
tabard: None,
},
_ => LoadoutBuilder::animal().build(),
_ => LoadoutBuilder::animal(entity.body).build(),
};
let mut scale = entity.scale;
@ -296,7 +296,7 @@ impl<'a> System<'a> for Sys {
scale = 2.0 + rand::random::<f32>();
}
stats.update_max_hp();
stats.update_max_hp(stats.body_type);
stats
.health

View File

@ -34,8 +34,8 @@ conrod_winit = { git = "https://gitlab.com/veloren/conrod.git", branch = "pre-wi
euc = { git = "https://github.com/zesterer/euc.git" }
# ECS
specs = "0.16.1"
specs-idvs = { git = "https://gitlab.com/veloren/specs-idvs.git" }
specs = { git = "https://github.com/amethyst/specs.git", rev = "7a2e348ab2223818bad487695c66c43db88050a5" }
specs-idvs = { git = "https://gitlab.com/veloren/specs-idvs.git", branch = "specs-git" }
# Mathematics
vek = { version = "0.11.0", features = [

View File

@ -17,6 +17,7 @@ pub mod run;
pub mod shoot;
pub mod sit;
pub mod spin;
pub mod spinmelee;
pub mod stand;
pub mod swim;
pub mod wield;
@ -28,8 +29,8 @@ pub use self::{
dance::DanceAnimation, dash::DashAnimation, equip::EquipAnimation,
glidewield::GlideWieldAnimation, gliding::GlidingAnimation, idle::IdleAnimation,
jump::JumpAnimation, leapmelee::LeapAnimation, roll::RollAnimation, run::RunAnimation,
shoot::ShootAnimation, sit::SitAnimation, spin::SpinAnimation, stand::StandAnimation,
swim::SwimAnimation, wield::WieldAnimation,
shoot::ShootAnimation, sit::SitAnimation, spin::SpinAnimation, spinmelee::SpinMeleeAnimation,
stand::StandAnimation, swim::SwimAnimation, wield::WieldAnimation,
};
use super::{Bone, FigureBoneData, Skeleton};

View File

@ -0,0 +1,170 @@
use super::{super::Animation, CharacterSkeleton, SkeletonAttr};
use common::comp::item::{Hands, ToolKind};
use std::f32::consts::PI;
use vek::*;
pub struct SpinMeleeAnimation;
impl Animation for SpinMeleeAnimation {
type Dependency = (Option<ToolKind>, Option<ToolKind>, Vec3<f32>, f64);
type Skeleton = CharacterSkeleton;
#[cfg(feature = "use-dyn-lib")]
const UPDATE_FN: &'static [u8] = b"character_spinmelee\0";
#[cfg_attr(feature = "be-dyn-lib", export_name = "character_spinmelee")]
#[allow(clippy::approx_constant)] // TODO: Pending review in #587
fn update_skeleton_inner(
skeleton: &Self::Skeleton,
(active_tool_kind, second_tool_kind, velocity, _global_time): Self::Dependency,
anim_time: f64,
rate: &mut f32,
skeleton_attr: &SkeletonAttr,
) -> Self::Skeleton {
*rate = 1.0;
let lab = 1.0;
let speed = Vec2::<f32>::from(velocity).magnitude();
let mut next = (*skeleton).clone();
//torso movement
let xshift = if velocity.z.abs() < 0.1 {
((anim_time as f32 - 1.1) * lab as f32 * 3.0).sin()
} else {
0.0
};
let yshift = if velocity.z.abs() < 0.1 {
((anim_time as f32 - 1.1) * lab as f32 * 3.0 + PI / 2.0).sin()
} else {
0.0
};
let spin = if anim_time < 1.1 && velocity.z.abs() < 0.1 {
0.5 * ((anim_time as f32).powf(2.0))
} else {
lab as f32 * anim_time as f32 * 0.9
};
//feet
let slowersmooth = (anim_time as f32 * lab as f32 * 4.0).sin();
let quick = (anim_time as f32 * lab as f32 * 8.0).sin();
if let Some(ToolKind::Axe(_)) = active_tool_kind {
next.l_hand.offset = Vec3::new(-0.5, 0.0, 4.0);
next.l_hand.ori = Quaternion::rotation_x(PI / 2.0)
* Quaternion::rotation_z(0.0)
* Quaternion::rotation_y(PI);
next.l_hand.scale = Vec3::one() * 1.08;
next.r_hand.offset = Vec3::new(0.5, 0.0, -2.5);
next.r_hand.ori = Quaternion::rotation_x(PI / 2.0)
* Quaternion::rotation_z(0.0)
* Quaternion::rotation_y(0.0);
next.r_hand.scale = Vec3::one() * 1.06;
next.main.offset = Vec3::new(-0.0, -2.0, -1.0);
next.main.ori = Quaternion::rotation_x(0.0)
* Quaternion::rotation_y(0.0)
* Quaternion::rotation_z(0.0);
next.control.offset = Vec3::new(0.0, 16.0, 3.0);
next.control.ori = Quaternion::rotation_x(-1.4)
* Quaternion::rotation_y(0.0)
* Quaternion::rotation_z(1.4);
next.control.scale = Vec3::one();
next.head.offset = Vec3::new(0.0, skeleton_attr.head.0, skeleton_attr.head.1);
next.head.ori = Quaternion::rotation_z(0.0)
* Quaternion::rotation_x(-0.15)
* Quaternion::rotation_y(0.08);
next.chest.offset = Vec3::new(
0.0,
skeleton_attr.chest.0 - 3.0,
skeleton_attr.chest.1 - 2.0,
);
next.chest.ori = Quaternion::rotation_z(0.0)
* Quaternion::rotation_x(-0.1)
* Quaternion::rotation_y(0.3);
next.chest.scale = Vec3::one();
next.belt.offset = Vec3::new(0.0, 1.0, -1.0);
next.belt.ori = Quaternion::rotation_z(0.0)
* Quaternion::rotation_x(0.4)
* Quaternion::rotation_y(0.0);
next.belt.scale = Vec3::one() * 0.98;
next.shorts.offset = Vec3::new(0.0, 3.0, -2.5);
next.shorts.ori = Quaternion::rotation_z(0.0)
* Quaternion::rotation_x(0.7)
* Quaternion::rotation_y(0.0);
next.shorts.scale = Vec3::one();
next.torso.offset = Vec3::new(
-xshift * (anim_time as f32).min(0.6),
-yshift * (anim_time as f32).min(0.6),
0.0,
) * skeleton_attr.scaler;
next.torso.ori = Quaternion::rotation_z(spin * -16.0)
* Quaternion::rotation_x(0.0)
* Quaternion::rotation_y(0.0);
next.torso.scale = Vec3::one() / 11.0 * skeleton_attr.scaler;
}
if velocity.z.abs() > 0.1 {
next.l_foot.offset = Vec3::new(-skeleton_attr.foot.0, 8.0, skeleton_attr.foot.2 + 2.0);
next.l_foot.ori = Quaternion::rotation_x(1.0) * Quaternion::rotation_z(0.0);
next.l_foot.scale = Vec3::one();
next.r_foot.offset = Vec3::new(skeleton_attr.foot.0, 8.0, skeleton_attr.foot.2 + 2.0);
next.r_foot.ori = Quaternion::rotation_x(1.0);
next.r_foot.scale = Vec3::one();
} else if speed < 0.5 {
next.l_foot.offset = Vec3::new(
-skeleton_attr.foot.0,
2.0 + quick * -6.0,
skeleton_attr.foot.2,
);
next.l_foot.ori =
Quaternion::rotation_x(0.5 + slowersmooth * 0.2) * Quaternion::rotation_z(0.0);
next.l_foot.scale = Vec3::one();
next.r_foot.offset = Vec3::new(skeleton_attr.foot.0, 4.0, skeleton_attr.foot.2);
next.r_foot.ori =
Quaternion::rotation_x(0.5 - slowersmooth * 0.2) * Quaternion::rotation_y(-0.4);
next.r_foot.scale = Vec3::one();
} else {
next.l_foot.offset = Vec3::new(
-skeleton_attr.foot.0,
2.0 + quick * -6.0,
skeleton_attr.foot.2,
);
next.l_foot.ori =
Quaternion::rotation_x(0.5 + slowersmooth * 0.2) * Quaternion::rotation_z(0.0);
next.l_foot.scale = Vec3::one();
next.r_foot.offset = Vec3::new(
skeleton_attr.foot.0,
2.0 + quick * 6.0,
skeleton_attr.foot.2,
);
next.r_foot.ori =
Quaternion::rotation_x(0.5 - slowersmooth * 0.2) * Quaternion::rotation_z(0.0);
next.r_foot.scale = Vec3::one();
};
next.lantern.offset = Vec3::new(
skeleton_attr.lantern.0,
skeleton_attr.lantern.1,
skeleton_attr.lantern.2,
);
next.lantern.ori = Quaternion::rotation_z(0.0)
* Quaternion::rotation_x(0.7)
* Quaternion::rotation_y(-0.8);
next.glider.offset = Vec3::new(0.0, 0.0, 10.0);
next.glider.scale = Vec3::one() * 0.0;
next.l_control.scale = Vec3::one();
next.r_control.scale = Vec3::one();
next.second.scale = match (
active_tool_kind.map(|tk| tk.into_hands()),
second_tool_kind.map(|tk| tk.into_hands()),
) {
(Some(Hands::OneHand), Some(Hands::OneHand)) => Vec3::one(),
(_, _) => Vec3::zero(),
};
next
}
}

View File

@ -131,7 +131,7 @@ impl Animation for WieldAnimation {
let hand_scale = 1.12;
next.control.offset = Vec3::new(0.0, 0.0, 0.0);
// next.control.ori = Quaternion::rotation_x(u_slow * 0.15 + 1.0)
//next.control.ori = Quaternion::rotation_x(slow * 1.0);
// * Quaternion::rotation_y(0.0)
// * Quaternion::rotation_z(u_slowalt * 0.08);
// next.control.scale = Vec3::one();
@ -172,26 +172,52 @@ impl Animation for WieldAnimation {
// next.r_control.scale = Vec3::one();
},
Some(ToolKind::Axe(_)) => {
next.l_hand.offset = Vec3::new(-4.0, 3.0, 6.0);
next.l_hand.ori = Quaternion::rotation_x(-0.3)
* Quaternion::rotation_z(3.14 - 0.3)
* Quaternion::rotation_y(-0.8);
next.l_hand.scale = Vec3::one() * 1.08;
next.r_hand.offset = Vec3::new(-2.5, 9.0, 4.0);
next.r_hand.ori = Quaternion::rotation_x(-0.3)
* Quaternion::rotation_z(3.14 - 0.3)
* Quaternion::rotation_y(-0.8);
next.r_hand.scale = Vec3::one() * 1.06;
next.main.offset = Vec3::new(-6.0, 10.0, -1.0);
next.main.ori = Quaternion::rotation_x(1.27)
* Quaternion::rotation_y(-0.3)
* Quaternion::rotation_z(-0.8);
next.control.offset = Vec3::new(0.0, 0.0, 0.0);
next.control.ori = Quaternion::rotation_x(u_slowalt * 0.1 + 0.2)
* Quaternion::rotation_y(-0.3)
* Quaternion::rotation_z(u_slow * 0.1 + 0.0);
if velocity < 0.5 {
next.head.offset = Vec3::new(
0.0,
-3.5 + skeleton_attr.head.0,
skeleton_attr.head.1 + u_slow * 0.1,
);
next.head.ori = Quaternion::rotation_z(head_look.x)
* Quaternion::rotation_x(0.35 + head_look.y.abs());
next.head.scale = Vec3::one() * skeleton_attr.head_scale;
next.chest.ori = Quaternion::rotation_x(-0.35)
* Quaternion::rotation_y(u_slowalt * 0.04)
* Quaternion::rotation_z(0.15);
next.belt.offset =
Vec3::new(0.0, 1.0 + skeleton_attr.belt.0, skeleton_attr.belt.1);
next.belt.ori = Quaternion::rotation_x(0.15)
* Quaternion::rotation_y(u_slowalt * 0.03)
* Quaternion::rotation_z(0.15);
next.shorts.offset =
Vec3::new(0.0, 1.0 + skeleton_attr.shorts.0, skeleton_attr.shorts.1);
next.shorts.ori = Quaternion::rotation_x(0.15) * Quaternion::rotation_z(0.25);
next.control.ori = Quaternion::rotation_x(1.8)
* Quaternion::rotation_y(-0.5)
* Quaternion::rotation_z(PI - 0.2);
next.control.scale = Vec3::one();
} else {
next.control.ori = Quaternion::rotation_x(2.1)
* Quaternion::rotation_y(-0.4)
* Quaternion::rotation_z(PI - 0.2);
next.control.scale = Vec3::one();
}
next.l_hand.offset = Vec3::new(-0.5, 0.0, 4.0);
next.l_hand.ori = Quaternion::rotation_x(PI / 2.0)
* Quaternion::rotation_z(0.0)
* Quaternion::rotation_y(0.0);
next.l_hand.scale = Vec3::one() * 1.08;
next.r_hand.offset = Vec3::new(0.5, 0.0, -2.5);
next.r_hand.ori = Quaternion::rotation_x(PI / 2.0)
* Quaternion::rotation_z(0.0)
* Quaternion::rotation_y(0.0);
next.r_hand.scale = Vec3::one() * 1.06;
next.main.offset = Vec3::new(-0.0, -2.0, -1.0);
next.main.ori = Quaternion::rotation_x(0.0)
* Quaternion::rotation_y(0.0)
* Quaternion::rotation_z(0.0);
next.control.offset = Vec3::new(-3.0, 11.0, 3.0);
},
Some(ToolKind::Hammer(_)) => {
next.l_hand.offset = Vec3::new(-12.0, 0.0, 0.0);

View File

@ -117,6 +117,7 @@ image_ids! {
flyingrod_m2: "voxygen.element.icons.debug_wand_m2",
charge: "voxygen.element.icons.skill_charge_3",
hammerleap: "voxygen.element.icons.skill_hammerleap",
axespin: "voxygen.element.icons.skill_axespin",
// Skillbar
level_up: "voxygen.element.misc_bg.level_up",

View File

@ -708,7 +708,7 @@ impl<'a> Widget for Skillbar<'a> {
Some(ToolKind::Dagger(_)) => self.imgs.onehdagger_m2,
Some(ToolKind::Shield(_)) => self.imgs.onehshield_m2,
Some(ToolKind::Hammer(_)) => self.imgs.hammerleap,
Some(ToolKind::Axe(_)) => self.imgs.nothing,
Some(ToolKind::Axe(_)) => self.imgs.axespin,
Some(ToolKind::Bow(_)) => self.imgs.bow_m2,
Some(ToolKind::Staff(StaffKind::Sceptre)) => self.imgs.heal_0,
Some(ToolKind::Staff(_)) => self.imgs.staff_m2,

View File

@ -8,6 +8,7 @@ pub const FAR_PLANE: f32 = 100000.0;
const FIRST_PERSON_INTERP_TIME: f32 = 0.1;
const THIRD_PERSON_INTERP_TIME: f32 = 0.1;
const FREEFLY_INTERP_TIME: f32 = 0.0;
const LERP_ORI_RATE: f32 = 15.0;
pub const MIN_ZOOM: f32 = 0.1;
@ -16,6 +17,7 @@ pub const MIN_ZOOM: f32 = 0.1;
pub enum CameraMode {
FirstPerson = 0,
ThirdPerson = 1,
Freefly = 2,
}
impl Default for CameraMode {
@ -75,15 +77,7 @@ impl Camera {
/// and position of the camera.
pub fn compute_dependents(&mut self, terrain: &impl ReadVol) {
let dist = {
let (start, end) = (
self.focus
+ (Vec3::new(
-f32::sin(self.ori.x) * f32::cos(self.ori.y),
-f32::cos(self.ori.x) * f32::cos(self.ori.y),
f32::sin(self.ori.y),
) * self.dist),
self.focus,
);
let (start, end) = (self.focus - self.forward() * self.dist, self.focus);
match terrain
.ray(start, end)
@ -161,13 +155,10 @@ impl Camera {
/// Zoom the camera by the given delta, limiting the input accordingly.
pub fn zoom_by(&mut self, delta: f32) {
match self.mode {
CameraMode::ThirdPerson => {
if self.mode == CameraMode::ThirdPerson {
// Clamp camera dist to the 2 <= x <= infinity range
self.tgt_dist = (self.tgt_dist + delta).max(2.0);
},
CameraMode::FirstPerson => {},
};
}
}
/// Zoom with the ability to switch between first and third-person mode.
@ -187,6 +178,7 @@ impl Camera {
self.set_mode(CameraMode::ThirdPerson);
self.tgt_dist = MIN_THIRD_PERSON;
},
_ => {},
}
}
}
@ -250,6 +242,7 @@ impl Camera {
match self.mode {
CameraMode::FirstPerson => FIRST_PERSON_INTERP_TIME,
CameraMode::ThirdPerson => THIRD_PERSON_INTERP_TIME,
CameraMode::Freefly => FREEFLY_INTERP_TIME,
}
}
@ -293,6 +286,9 @@ impl Camera {
CameraMode::FirstPerson => {
self.set_distance(MIN_ZOOM);
},
CameraMode::Freefly => {
self.zoom_by(0.0);
},
}
}
}
@ -307,4 +303,45 @@ impl Camera {
mode => mode,
}
}
/// Cycle the camera to its next valid mode. If is_admin is false then only
/// modes which are accessible without admin access will be cycled to.
pub fn next_mode(&mut self, is_admin: bool) {
self.set_mode(match self.mode {
CameraMode::ThirdPerson => CameraMode::FirstPerson,
CameraMode::FirstPerson => {
if is_admin {
CameraMode::Freefly
} else {
CameraMode::ThirdPerson
}
},
CameraMode::Freefly => CameraMode::ThirdPerson,
});
}
/// Return a unit vector in the forward direction for the current camera
/// orientation
pub fn forward(&self) -> Vec3<f32> {
Vec3::new(
f32::sin(self.ori.x) * f32::cos(self.ori.y),
f32::cos(self.ori.x) * f32::cos(self.ori.y),
-f32::sin(self.ori.y),
)
}
/// Return a unit vector in the right direction for the current camera
/// orientation
pub fn right(&self) -> Vec3<f32> {
const UP: Vec3<f32> = Vec3::new(0.0, 0.0, 1.0);
self.forward().cross(UP).normalized()
}
/// Return a unit vector in the forward direction on the XY plane for
/// the current camera orientation
pub fn forward_xy(&self) -> Vec2<f32> { Vec2::new(f32::sin(self.ori.x), f32::cos(self.ori.x)) }
/// Return a unit vector in the right direction on the XY plane for
/// the current camera orientation
pub fn right_xy(&self) -> Vec2<f32> { Vec2::new(f32::cos(self.ori.x), -f32::sin(self.ori.x)) }
}

View File

@ -122,42 +122,50 @@ impl<Skel: Skeleton> FigureModelCache<Skel> {
[
match camera_mode {
CameraMode::ThirdPerson => Some(
CameraMode::ThirdPerson | CameraMode::Freefly => Some(
humanoid_head_spec
.mesh_head(&body, |segment, offset| generate_mesh(segment, offset)),
),
CameraMode::FirstPerson => None,
},
match camera_mode {
CameraMode::ThirdPerson => Some(humanoid_armor_chest_spec.mesh_chest(
CameraMode::ThirdPerson | CameraMode::Freefly => {
Some(humanoid_armor_chest_spec.mesh_chest(
&body,
loadout,
|segment, offset| generate_mesh(segment, offset),
)),
))
},
CameraMode::FirstPerson => None,
},
match camera_mode {
CameraMode::ThirdPerson => Some(humanoid_armor_belt_spec.mesh_belt(
CameraMode::ThirdPerson | CameraMode::Freefly => {
Some(humanoid_armor_belt_spec.mesh_belt(
&body,
loadout,
|segment, offset| generate_mesh(segment, offset),
)),
))
},
CameraMode::FirstPerson => None,
},
match camera_mode {
CameraMode::ThirdPerson => Some(humanoid_armor_back_spec.mesh_back(
CameraMode::ThirdPerson | CameraMode::Freefly => {
Some(humanoid_armor_back_spec.mesh_back(
&body,
loadout,
|segment, offset| generate_mesh(segment, offset),
)),
))
},
CameraMode::FirstPerson => None,
},
match camera_mode {
CameraMode::ThirdPerson => Some(humanoid_armor_pants_spec.mesh_pants(
CameraMode::ThirdPerson | CameraMode::Freefly => {
Some(humanoid_armor_pants_spec.mesh_pants(
&body,
loadout,
|segment, offset| generate_mesh(segment, offset),
)),
))
},
CameraMode::FirstPerson => None,
},
Some(humanoid_armor_hand_spec.mesh_left_hand(
@ -181,7 +189,7 @@ impl<Skel: Skeleton> FigureModelCache<Skel> {
|segment, offset| generate_mesh(segment, offset),
)),
match camera_mode {
CameraMode::ThirdPerson => {
CameraMode::ThirdPerson | CameraMode::Freefly => {
Some(humanoid_armor_shoulder_spec.mesh_left_shoulder(
&body,
loadout,
@ -191,7 +199,7 @@ impl<Skel: Skeleton> FigureModelCache<Skel> {
CameraMode::FirstPerson => None,
},
match camera_mode {
CameraMode::ThirdPerson => {
CameraMode::ThirdPerson | CameraMode::Freefly => {
Some(humanoid_armor_shoulder_spec.mesh_right_shoulder(
&body,
loadout,

View File

@ -715,6 +715,15 @@ impl FigureMgr {
skeleton_attr,
)
},
CharacterState::SpinMelee(_) => {
anim::character::SpinMeleeAnimation::update_skeleton(
&target_base,
(active_tool_kind, second_tool_kind, vel.0, time),
state.state_time,
&mut state_animation_rate,
skeleton_attr,
)
},
CharacterState::TripleStrike(s) => match s.stage {
triple_strike::Stage::First => {
anim::character::AlphaAnimation::update_skeleton(

View File

@ -395,10 +395,17 @@ impl Scene {
},
CameraMode::ThirdPerson if scene_data.is_aiming => player_scale * 2.1,
CameraMode::ThirdPerson => player_scale * 1.65,
CameraMode::Freefly => 0.0,
};
self.camera
.set_focus_pos(player_pos + Vec3::unit_z() * (up - tilt.min(0.0).sin() * dist * 0.6));
match self.camera.get_mode() {
CameraMode::FirstPerson | CameraMode::ThirdPerson => {
self.camera.set_focus_pos(
player_pos + Vec3::unit_z() * (up - tilt.min(0.0).sin() * dist * 0.6),
);
},
CameraMode::Freefly => {},
};
// Tick camera for interpolation.
self.camera.update(

View File

@ -177,7 +177,9 @@ impl PlayState for SessionState {
)
.unwrap();
let mut ori = self.scene.camera().get_orientation();
let mut walk_forward_dir = self.scene.camera().forward_xy();
let mut walk_right_dir = self.scene.camera().right_xy();
let mut freefly_vel = Vec3::zero();
let mut free_look = false;
let mut auto_walk = false;
@ -526,6 +528,14 @@ impl PlayState for SessionState {
_ => {},
}
},
Event::InputUpdate(GameInput::CycleCamera, true) => {
// Prevent accessing camera modes which aren't available in multiplayer
// unless you are an admin. This is an easily bypassed clientside check.
// The server should do its own filtering of which entities are sent to
// clients to prevent abuse.
let camera = self.scene.camera_mut();
camera.next_mode(self.client.borrow().is_admin());
},
Event::AnalogGameInput(input) => match input {
AnalogGameInput::MovementX(v) => {
self.key_state.analog_matrix.x = v;
@ -552,17 +562,60 @@ impl PlayState for SessionState {
}
if !free_look {
ori = self.scene.camera().get_orientation();
walk_forward_dir = self.scene.camera().forward_xy();
walk_right_dir = self.scene.camera().right_xy();
self.inputs.look_dir = Dir::from_unnormalized(cam_dir + aim_dir_offset).unwrap();
}
// Calculate the movement input vector of the player from the current key
// presses and the camera direction.
let unit_vecs = (
Vec2::new(ori[0].cos(), -ori[0].sin()),
Vec2::new(ori[0].sin(), ori[0].cos()),
);
let dir_vec = self.key_state.dir_vec();
self.inputs.move_dir = unit_vecs.0 * dir_vec[0] + unit_vecs.1 * dir_vec[1];
// Get the current state of movement related inputs
let input_vec = self.key_state.dir_vec();
let (axis_right, axis_up) = (input_vec[0], input_vec[1]);
match self.scene.camera().get_mode() {
camera::CameraMode::FirstPerson | camera::CameraMode::ThirdPerson => {
// Move the player character based on their walking direction.
// This could be different from the camera direction if free look is enabled.
self.inputs.move_dir = walk_right_dir * axis_right + walk_forward_dir * axis_up;
freefly_vel = Vec3::zero();
},
camera::CameraMode::Freefly => {
// Move the camera freely in 3d space. Apply acceleration so that
// the movement feels more natural and controlled.
const FREEFLY_ACCEL: f32 = 120.0;
const FREEFLY_DAMPING: f32 = 80.0;
const FREEFLY_MAX_SPEED: f32 = 50.0;
let forward = self.scene.camera().forward();
let right = self.scene.camera().right();
let dir = right * axis_right + forward * axis_up;
let dt = clock.get_last_delta().as_secs_f32();
if freefly_vel.magnitude_squared() > 0.01 {
let new_vel =
freefly_vel - freefly_vel.normalized() * (FREEFLY_DAMPING * dt);
if freefly_vel.dot(new_vel) > 0.0 {
freefly_vel = new_vel;
} else {
freefly_vel = Vec3::zero();
}
}
if dir.magnitude_squared() > 0.01 {
freefly_vel += dir * (FREEFLY_ACCEL * dt);
if freefly_vel.magnitude() > FREEFLY_MAX_SPEED {
freefly_vel = freefly_vel.normalized() * FREEFLY_MAX_SPEED;
}
}
let pos = self.scene.camera().get_focus_pos();
self.scene
.camera_mut()
.set_focus_pos(pos + freefly_vel * dt);
// Do not apply any movement to the player character
self.inputs.move_dir = Vec2::zero();
},
};
self.inputs.climb = self.key_state.climb();
@ -597,7 +650,7 @@ impl PlayState for SessionState {
global_state,
DebugInfo {
tps: clock.get_tps(),
ping_ms: self.client.borrow().get_ping_ms(),
ping_ms: self.client.borrow().get_ping_ms_rolling_avg(),
coordinates: self
.client
.borrow()

View File

@ -142,6 +142,7 @@ impl ControlSettings {
//GameInput::Charge => KeyMouse::Key(VirtualKeyCode::Key1),
GameInput::FreeLook => KeyMouse::Key(VirtualKeyCode::L),
GameInput::AutoWalk => KeyMouse::Key(VirtualKeyCode::Period),
GameInput::CycleCamera => KeyMouse::Key(VirtualKeyCode::Key0),
GameInput::Slot1 => KeyMouse::Key(VirtualKeyCode::Key1),
GameInput::Slot2 => KeyMouse::Key(VirtualKeyCode::Key2),
GameInput::Slot3 => KeyMouse::Key(VirtualKeyCode::Key3),
@ -204,6 +205,7 @@ impl Default for ControlSettings {
//GameInput::Charge,
GameInput::FreeLook,
GameInput::AutoWalk,
GameInput::CycleCamera,
GameInput::Slot1,
GameInput::Slot2,
GameInput::Slot3,

View File

@ -47,6 +47,7 @@ impl Singleplayer {
let paused = Arc::new(AtomicBool::new(false));
let paused1 = paused.clone();
let thread = thread::spawn(move || {
let server = Server::new(settings2).expect("Failed to create server instance!");
let server = match thread_pool {
@ -54,7 +55,6 @@ impl Singleplayer {
None => server,
};
let thread = thread::spawn(move || {
run_server(server, receiver, paused1);
});

View File

@ -65,6 +65,7 @@ pub enum GameInput {
SwapLoadout,
FreeLook,
AutoWalk,
CycleCamera,
}
impl GameInput {
@ -89,6 +90,7 @@ impl GameInput {
GameInput::Mount => "gameinput.mount",
GameInput::Enter => "gameinput.enter",
GameInput::Command => "gameinput.command",
GameInput::CycleCamera => "gameinput.cyclecamera",
GameInput::Escape => "gameinput.escape",
GameInput::Map => "gameinput.map",
GameInput::Bag => "gameinput.bag",