mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge master and build
This commit is contained in:
@ -10,11 +10,11 @@ pub struct RollAnimation;
|
||||
|
||||
impl Animation for RollAnimation {
|
||||
type Skeleton = CharacterSkeleton;
|
||||
type Dependency = (Option<ToolKind>, f64);
|
||||
type Dependency = (Option<ToolKind>, Vec3<f32>, Vec3<f32>, f64);
|
||||
|
||||
fn update_skeleton(
|
||||
skeleton: &Self::Skeleton,
|
||||
(_active_tool_kind, _global_time): Self::Dependency,
|
||||
(_active_tool_kind, orientation, last_ori, _global_time): Self::Dependency,
|
||||
anim_time: f64,
|
||||
_rate: &mut f32,
|
||||
skeleton_attr: &SkeletonAttr,
|
||||
@ -27,6 +27,20 @@ impl Animation for RollAnimation {
|
||||
let wave_slow = (anim_time as f32 * 2.8 + PI).sin();
|
||||
let wave_dub = (anim_time as f32 * 5.5).sin();
|
||||
|
||||
let ori = Vec2::from(orientation);
|
||||
let last_ori = Vec2::from(last_ori);
|
||||
let tilt = if Vec2::new(ori, last_ori)
|
||||
.map(|o| Vec2::<f32>::from(o).magnitude_squared())
|
||||
.map(|m| m > 0.001 && m.is_finite())
|
||||
.reduce_and()
|
||||
&& ori.angle_between(last_ori).is_finite()
|
||||
{
|
||||
ori.angle_between(last_ori).min(0.5)
|
||||
* last_ori.determine_side(Vec2::zero(), ori).signum()
|
||||
} else {
|
||||
0.0
|
||||
} * 1.3;
|
||||
|
||||
next.head.offset = Vec3::new(
|
||||
0.0 + skeleton_attr.neck_right,
|
||||
-2.0 + skeleton_attr.neck_forward,
|
||||
@ -102,7 +116,7 @@ impl Animation for RollAnimation {
|
||||
|
||||
next.torso.offset =
|
||||
Vec3::new(0.0, 11.0, 0.1 + wave_dub * 16.0) / 11.0 * skeleton_attr.scaler;
|
||||
next.torso.ori = Quaternion::rotation_x(wave_slow * 6.5);
|
||||
next.torso.ori = Quaternion::rotation_x(wave_slow * 6.5) * Quaternion::rotation_y(tilt);
|
||||
next.torso.scale = Vec3::one() / 11.0 * skeleton_attr.scaler;
|
||||
next
|
||||
}
|
||||
|
@ -56,7 +56,6 @@ impl Animation for RunAnimation {
|
||||
|
||||
let ori = Vec2::from(orientation);
|
||||
let last_ori = Vec2::from(last_ori);
|
||||
|
||||
let tilt = if Vec2::new(ori, last_ori)
|
||||
.map(|o| Vec2::<f32>::from(o).magnitude_squared())
|
||||
.map(|m| m > 0.001 && m.is_finite())
|
||||
|
27
voxygen/src/audio/sfx/event_mapper/mod.rs
Normal file
27
voxygen/src/audio/sfx/event_mapper/mod.rs
Normal file
@ -0,0 +1,27 @@
|
||||
pub mod movement;
|
||||
pub mod progression;
|
||||
|
||||
use movement::MovementEventMapper;
|
||||
use progression::ProgressionEventMapper;
|
||||
|
||||
use super::SfxTriggers;
|
||||
use client::Client;
|
||||
|
||||
pub struct SfxEventMapper {
|
||||
progression_event_mapper: ProgressionEventMapper,
|
||||
movement_event_mapper: MovementEventMapper,
|
||||
}
|
||||
|
||||
impl SfxEventMapper {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
progression_event_mapper: ProgressionEventMapper::new(),
|
||||
movement_event_mapper: MovementEventMapper::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn maintain(&mut self, client: &Client, triggers: &SfxTriggers) {
|
||||
self.progression_event_mapper.maintain(client, triggers);
|
||||
self.movement_event_mapper.maintain(client, triggers);
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/// sfx::event_mapper watches the local entities and determines which sfx to emit,
|
||||
/// and the position at which the sound should be emitted from
|
||||
/// event_mapper::movement watches all local entities movements and determines which
|
||||
/// sfx to emit, and the position at which the sound should be emitted from
|
||||
use crate::audio::sfx::{SfxTriggerItem, SfxTriggers};
|
||||
|
||||
use client::Client;
|
||||
@ -11,7 +11,7 @@ use common::{
|
||||
event::{EventBus, SfxEvent, SfxEventItem},
|
||||
};
|
||||
use hashbrown::HashMap;
|
||||
use specs::{Entity as EcsEntity, Join};
|
||||
use specs::{Entity as EcsEntity, Join, WorldExt};
|
||||
use std::time::{Duration, Instant};
|
||||
use vek::*;
|
||||
|
||||
@ -21,11 +21,11 @@ struct LastSfxEvent {
|
||||
time: Instant,
|
||||
}
|
||||
|
||||
pub struct SfxEventMapper {
|
||||
pub struct MovementEventMapper {
|
||||
event_history: HashMap<EcsEntity, LastSfxEvent>,
|
||||
}
|
||||
|
||||
impl SfxEventMapper {
|
||||
impl MovementEventMapper {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
event_history: HashMap::new(),
|
||||
@ -41,11 +41,10 @@ impl SfxEventMapper {
|
||||
.get(client.entity())
|
||||
.map_or(Vec3::zero(), |pos| pos.0);
|
||||
|
||||
for (entity, pos, body, stats, character) in (
|
||||
for (entity, pos, body, character) in (
|
||||
&ecs.entities(),
|
||||
&ecs.read_storage::<Pos>(),
|
||||
&ecs.read_storage::<Body>(),
|
||||
&ecs.read_storage::<Stats>(),
|
||||
ecs.read_storage::<CharacterState>().maybe(),
|
||||
)
|
||||
.join()
|
||||
@ -53,7 +52,7 @@ impl SfxEventMapper {
|
||||
(e_pos.0.distance_squared(player_position)) < SFX_DIST_LIMIT_SQR
|
||||
})
|
||||
{
|
||||
if let (pos, body, Some(character), stats) = (pos, body, character, stats) {
|
||||
if let Some(character) = character {
|
||||
let state = self
|
||||
.event_history
|
||||
.entry(entity)
|
||||
@ -63,22 +62,16 @@ impl SfxEventMapper {
|
||||
});
|
||||
|
||||
let mapped_event = match body {
|
||||
Body::Humanoid(_) => {
|
||||
Self::map_character_event(character, state.event.clone(), stats)
|
||||
}
|
||||
Body::Humanoid(_) => Self::map_movement_event(character, state.event.clone()),
|
||||
Body::QuadrupedMedium(_) => {
|
||||
Self::map_quadriped_event(character, state.event.clone(), stats)
|
||||
// TODO: Quadriped running sfx
|
||||
SfxEvent::Idle
|
||||
}
|
||||
_ => SfxEvent::Idle,
|
||||
};
|
||||
|
||||
// Check for SFX config entry for this movement
|
||||
let sfx_trigger_item: Option<&SfxTriggerItem> = triggers
|
||||
.items
|
||||
.iter()
|
||||
.find(|item| item.trigger == mapped_event);
|
||||
|
||||
if Self::should_emit(state, sfx_trigger_item) {
|
||||
if Self::should_emit(state, triggers.get_key_value(&mapped_event)) {
|
||||
ecs.read_resource::<EventBus<SfxEventItem>>()
|
||||
.emitter()
|
||||
.emit(SfxEventItem::new(mapped_event, Some(pos.0)));
|
||||
@ -115,10 +108,10 @@ impl SfxEventMapper {
|
||||
/// 2. The sfx has not been played since it's timeout threshold has elapsed, which prevents firing every tick
|
||||
fn should_emit(
|
||||
last_play_entry: &LastSfxEvent,
|
||||
sfx_trigger_item: Option<&SfxTriggerItem>,
|
||||
sfx_trigger_item: Option<(&SfxEvent, &SfxTriggerItem)>,
|
||||
) -> bool {
|
||||
if let Some(item) = sfx_trigger_item {
|
||||
if last_play_entry.event == item.trigger {
|
||||
if let Some((event, item)) = sfx_trigger_item {
|
||||
if &last_play_entry.event == event {
|
||||
last_play_entry.time.elapsed().as_secs_f64() >= item.threshold
|
||||
} else {
|
||||
true
|
||||
@ -132,60 +125,25 @@ impl SfxEventMapper {
|
||||
/// however that list does not provide enough resolution to target specific entity events, such
|
||||
/// as opening or closing the glider. These methods translate those entity states with some additional
|
||||
/// data into more specific `SfxEvent`'s which we attach sounds to
|
||||
fn map_quadriped_event(
|
||||
current_event: &CharacterState,
|
||||
previous_event: SfxEvent,
|
||||
stats: &Stats,
|
||||
) -> SfxEvent {
|
||||
match (
|
||||
current_event.move_state,
|
||||
current_event.action_state,
|
||||
previous_event,
|
||||
stats,
|
||||
) {
|
||||
(_, ActionState::Attack { .. }, _, stats) => match stats.name.as_ref() {
|
||||
"Wolf" => SfxEvent::AttackWolf,
|
||||
_ => SfxEvent::Idle,
|
||||
},
|
||||
_ => SfxEvent::Idle,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_character_event(
|
||||
current_event: &CharacterState,
|
||||
previous_event: SfxEvent,
|
||||
stats: &Stats,
|
||||
) -> SfxEvent {
|
||||
match (
|
||||
current_event.move_state,
|
||||
current_event.action_state,
|
||||
previous_event,
|
||||
stats,
|
||||
) {
|
||||
(_, ActionState::Dodge(_), ..) => SfxEvent::Roll,
|
||||
fn map_movement_event(current_event: &CharacterState, previous_event: SfxEvent) -> SfxEvent {
|
||||
match (current_event.movement, current_event.action, previous_event) {
|
||||
(_, ActionState::Roll(_), _) => SfxEvent::Roll,
|
||||
(MoveState::Climb(_), ..) => SfxEvent::Climb,
|
||||
(MoveState::Swim(_), ..) => SfxEvent::Swim,
|
||||
(MoveState::Run(_), ..) => SfxEvent::Run,
|
||||
(MoveState::Fall(_), _, previous_event, _) => {
|
||||
if previous_event != SfxEvent::Glide {
|
||||
SfxEvent::Fall
|
||||
} else {
|
||||
SfxEvent::GliderClose
|
||||
}
|
||||
}
|
||||
(MoveState::Glide(_), _, previous_event, ..) => {
|
||||
(MoveState::Jump(_), ..) => SfxEvent::Jump,
|
||||
(MoveState::Fall(_), _, SfxEvent::Glide) => SfxEvent::GliderClose,
|
||||
(MoveState::Stand(_), _, SfxEvent::Fall) => SfxEvent::Run,
|
||||
(MoveState::Fall(_), _, SfxEvent::Jump) => SfxEvent::Idle,
|
||||
(MoveState::Fall(_), _, _) => SfxEvent::Fall,
|
||||
(MoveState::Glide(_), _, previous_event) => {
|
||||
if previous_event != SfxEvent::GliderOpen && previous_event != SfxEvent::Glide {
|
||||
SfxEvent::GliderOpen
|
||||
} else {
|
||||
SfxEvent::Glide
|
||||
}
|
||||
}
|
||||
(_, ActionState::Attack { .. }, _, stats) => {
|
||||
match &stats.equipment.main.as_ref().map(|i| &i.kind) {
|
||||
Some(ItemKind::Tool(ToolData { kind, .. })) => SfxEvent::Attack(*kind),
|
||||
_ => SfxEvent::Idle,
|
||||
}
|
||||
}
|
||||
(MoveState::Stand(_), _, SfxEvent::Glide) => SfxEvent::GliderClose,
|
||||
_ => SfxEvent::Idle,
|
||||
}
|
||||
}
|
||||
@ -208,15 +166,16 @@ mod tests {
|
||||
time: Instant::now(),
|
||||
};
|
||||
|
||||
let result = SfxEventMapper::should_emit(&last_sfx_event, None);
|
||||
let result = MovementEventMapper::should_emit(&last_sfx_event, None);
|
||||
|
||||
assert_eq!(result, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_but_played_since_threshold_no_emit() {
|
||||
let event = SfxEvent::Run;
|
||||
|
||||
let trigger_item = SfxTriggerItem {
|
||||
trigger: SfxEvent::Run,
|
||||
files: vec![String::from("some.path.to.sfx.file")],
|
||||
threshold: 1.0,
|
||||
};
|
||||
@ -227,15 +186,17 @@ mod tests {
|
||||
time: Instant::now(),
|
||||
};
|
||||
|
||||
let result = SfxEventMapper::should_emit(&last_sfx_event, Some(&trigger_item));
|
||||
let result =
|
||||
MovementEventMapper::should_emit(&last_sfx_event, Some((&event, &trigger_item)));
|
||||
|
||||
assert_eq!(result, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_and_not_played_since_threshold_emits() {
|
||||
let event = SfxEvent::Run;
|
||||
|
||||
let trigger_item = SfxTriggerItem {
|
||||
trigger: SfxEvent::Run,
|
||||
files: vec![String::from("some.path.to.sfx.file")],
|
||||
threshold: 0.5,
|
||||
};
|
||||
@ -245,15 +206,17 @@ mod tests {
|
||||
time: Instant::now().checked_add(Duration::from_secs(1)).unwrap(),
|
||||
};
|
||||
|
||||
let result = SfxEventMapper::should_emit(&last_sfx_event, Some(&trigger_item));
|
||||
let result =
|
||||
MovementEventMapper::should_emit(&last_sfx_event, Some((&event, &trigger_item)));
|
||||
|
||||
assert_eq!(result, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_previous_event_elapsed_emits() {
|
||||
let event = SfxEvent::Run;
|
||||
|
||||
let trigger_item = SfxTriggerItem {
|
||||
trigger: SfxEvent::Run,
|
||||
files: vec![String::from("some.path.to.sfx.file")],
|
||||
threshold: 0.5,
|
||||
};
|
||||
@ -265,22 +228,20 @@ mod tests {
|
||||
.unwrap(),
|
||||
};
|
||||
|
||||
let result = SfxEventMapper::should_emit(&last_sfx_event, Some(&trigger_item));
|
||||
let result =
|
||||
MovementEventMapper::should_emit(&last_sfx_event, Some((&event, &trigger_item)));
|
||||
|
||||
assert_eq!(result, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_idle() {
|
||||
let stats = Stats::new(String::from("Test"), None);
|
||||
|
||||
let result = SfxEventMapper::map_character_event(
|
||||
let result = MovementEventMapper::map_movement_event(
|
||||
&CharacterState {
|
||||
move_state: MoveState::Stand(None),
|
||||
action_state: ActionState::Idle(None),
|
||||
},
|
||||
SfxEvent::Idle,
|
||||
&stats,
|
||||
);
|
||||
|
||||
assert_eq!(result, SfxEvent::Idle);
|
||||
@ -288,15 +249,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn maps_run() {
|
||||
let stats = Stats::new(String::from("Test"), None);
|
||||
|
||||
let result = SfxEventMapper::map_character_event(
|
||||
let result = MovementEventMapper::map_movement_event(
|
||||
&CharacterState {
|
||||
move_state: MoveState::Run(None),
|
||||
action_state: ActionState::Idle(None),
|
||||
},
|
||||
SfxEvent::Idle,
|
||||
&stats,
|
||||
);
|
||||
|
||||
assert_eq!(result, SfxEvent::Run);
|
||||
@ -304,15 +262,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn maps_roll() {
|
||||
let stats = Stats::new(String::from("Test"), None);
|
||||
|
||||
let result = SfxEventMapper::map_character_event(
|
||||
let result = MovementEventMapper::map_movement_event(
|
||||
&CharacterState {
|
||||
action_state: ActionState::Dodge(Roll(None)),
|
||||
move_state: MoveState::Run(None),
|
||||
},
|
||||
SfxEvent::Run,
|
||||
&stats,
|
||||
);
|
||||
|
||||
assert_eq!(result, SfxEvent::Roll);
|
||||
@ -320,31 +275,38 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn maps_fall() {
|
||||
let stats = Stats::new(String::from("Test"), None);
|
||||
|
||||
let result = SfxEventMapper::map_character_event(
|
||||
let result = MovementEventMapper::map_movement_event(
|
||||
&CharacterState {
|
||||
move_state: MoveState::Fall(None),
|
||||
action_state: ActionState::Idle(None),
|
||||
},
|
||||
SfxEvent::Idle,
|
||||
&stats,
|
||||
SfxEvent::Fall,
|
||||
);
|
||||
|
||||
assert_eq!(result, SfxEvent::Fall);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_glider_open() {
|
||||
let stats = Stats::new(String::from("Test"), None);
|
||||
fn maps_land_on_ground_to_run() {
|
||||
let result = MovementEventMapper::map_movement_event(
|
||||
&CharacterState {
|
||||
movement: MovementState::Stand,
|
||||
action: ActionState::Idle,
|
||||
},
|
||||
SfxEvent::Fall,
|
||||
);
|
||||
|
||||
let result = SfxEventMapper::map_character_event(
|
||||
assert_eq!(result, SfxEvent::Run);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_glider_open() {
|
||||
let result = MovementEventMapper::map_movement_event(
|
||||
&CharacterState {
|
||||
move_state: MoveState::Glide(None),
|
||||
action_state: ActionState::Idle(None),
|
||||
},
|
||||
SfxEvent::Jump,
|
||||
&stats,
|
||||
);
|
||||
|
||||
assert_eq!(result, SfxEvent::GliderOpen);
|
||||
@ -352,54 +314,40 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn maps_glide() {
|
||||
let stats = Stats::new(String::from("Test"), None);
|
||||
|
||||
let result = SfxEventMapper::map_character_event(
|
||||
let result = MovementEventMapper::map_movement_event(
|
||||
&CharacterState {
|
||||
move_state: MoveState::Glide(None),
|
||||
action_state: ActionState::Idle(None),
|
||||
},
|
||||
SfxEvent::Glide,
|
||||
&stats,
|
||||
);
|
||||
|
||||
assert_eq!(result, SfxEvent::Glide);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_glider_close() {
|
||||
let stats = Stats::new(String::from("Test"), None);
|
||||
|
||||
let result = SfxEventMapper::map_character_event(
|
||||
fn maps_glider_close_when_closing_mid_flight() {
|
||||
let result = MovementEventMapper::map_movement_event(
|
||||
&CharacterState {
|
||||
move_state: MoveState::Fall(None),
|
||||
action_state: ActionState::Idle(None),
|
||||
},
|
||||
SfxEvent::Glide,
|
||||
&stats,
|
||||
);
|
||||
|
||||
assert_eq!(result, SfxEvent::GliderClose);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_attack() {
|
||||
let stats = Stats::new(
|
||||
String::from("Test"),
|
||||
Some(assets::load_expect_cloned(
|
||||
"common.items.weapons.starter_sword",
|
||||
)),
|
||||
);
|
||||
|
||||
let result = SfxEventMapper::map_character_event(
|
||||
fn maps_glider_close_when_landing() {
|
||||
let result = MovementEventMapper::map_movement_event(
|
||||
&CharacterState {
|
||||
move_state: MoveState::Stand(None),
|
||||
action_state: ActionState::Attack(BasicAttack(None)),
|
||||
movement: MoveState::Stand(None),
|
||||
action: ActionState::Idle(None),
|
||||
},
|
||||
SfxEvent::Idle,
|
||||
&stats,
|
||||
SfxEvent::Glide,
|
||||
);
|
||||
|
||||
// assert_eq!(result, SfxEvent::Attack(Sword(_)));
|
||||
assert_eq!(result, SfxEvent::GliderClose);
|
||||
}
|
||||
}
|
120
voxygen/src/audio/sfx/event_mapper/progression.rs
Normal file
120
voxygen/src/audio/sfx/event_mapper/progression.rs
Normal file
@ -0,0 +1,120 @@
|
||||
/// event_mapper::progression watches the the current player's level
|
||||
/// and experience and emits associated SFX
|
||||
use crate::audio::sfx::SfxTriggers;
|
||||
|
||||
use client::Client;
|
||||
use common::{
|
||||
comp::Stats,
|
||||
event::{EventBus, SfxEvent, SfxEventItem},
|
||||
};
|
||||
use specs::WorldExt;
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
struct ProgressionState {
|
||||
level: u32,
|
||||
exp: u32,
|
||||
}
|
||||
|
||||
impl Default for ProgressionState {
|
||||
fn default() -> Self {
|
||||
Self { level: 1, exp: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ProgressionEventMapper {
|
||||
state: ProgressionState,
|
||||
}
|
||||
|
||||
impl ProgressionEventMapper {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
state: ProgressionState::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn maintain(&mut self, client: &Client, triggers: &SfxTriggers) {
|
||||
let ecs = client.state().ecs();
|
||||
|
||||
// level and exp changes
|
||||
let next_state =
|
||||
ecs.read_storage::<Stats>()
|
||||
.get(client.entity())
|
||||
.map_or(self.state.clone(), |stats| ProgressionState {
|
||||
level: stats.level.level(),
|
||||
exp: stats.exp.current(),
|
||||
});
|
||||
|
||||
if &self.state != &next_state {
|
||||
if let Some(mapped_event) = self.map_event(&next_state) {
|
||||
let sfx_trigger_item = triggers.get_trigger(&mapped_event);
|
||||
|
||||
if sfx_trigger_item.is_some() {
|
||||
ecs.read_resource::<EventBus<SfxEventItem>>()
|
||||
.emitter()
|
||||
.emit(SfxEventItem::at_player_position(mapped_event));
|
||||
}
|
||||
}
|
||||
|
||||
self.state = next_state;
|
||||
}
|
||||
}
|
||||
|
||||
fn map_event(&mut self, next_state: &ProgressionState) -> Option<SfxEvent> {
|
||||
let sfx_event = if next_state.level > self.state.level {
|
||||
Some(SfxEvent::LevelUp)
|
||||
} else if next_state.exp > self.state.exp {
|
||||
Some(SfxEvent::ExperienceGained)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
sfx_event
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use common::event::SfxEvent;
|
||||
|
||||
#[test]
|
||||
fn no_change_returns_none() {
|
||||
let mut mapper = ProgressionEventMapper::new();
|
||||
let next_client_state = ProgressionState::default();
|
||||
|
||||
assert_eq!(mapper.map_event(&next_client_state), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn change_level_returns_levelup() {
|
||||
let mut mapper = ProgressionEventMapper::new();
|
||||
let next_client_state = ProgressionState { level: 2, exp: 0 };
|
||||
|
||||
assert_eq!(
|
||||
mapper.map_event(&next_client_state),
|
||||
Some(SfxEvent::LevelUp)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn change_exp_returns_expup() {
|
||||
let mut mapper = ProgressionEventMapper::new();
|
||||
let next_client_state = ProgressionState { level: 1, exp: 100 };
|
||||
|
||||
assert_eq!(
|
||||
mapper.map_event(&next_client_state),
|
||||
Some(SfxEvent::ExperienceGained)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn level_up_and_gained_exp_prioritises_levelup() {
|
||||
let mut mapper = ProgressionEventMapper::new();
|
||||
let next_client_state = ProgressionState { level: 2, exp: 100 };
|
||||
|
||||
assert_eq!(
|
||||
mapper.map_event(&next_client_state),
|
||||
Some(SfxEvent::LevelUp)
|
||||
);
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
/// The SfxManager listens for SFX events and plays the sound at the provided position
|
||||
/// The Sfx Manager manages individual sfx event system, listens for
|
||||
/// SFX events and plays the sound at the requested position, or the current player position
|
||||
mod event_mapper;
|
||||
|
||||
use crate::audio::AudioFrontend;
|
||||
@ -8,36 +9,53 @@ use common::{
|
||||
comp::{Ori, Pos},
|
||||
event::{EventBus, SfxEvent, SfxEventItem},
|
||||
};
|
||||
use event_mapper::SfxEventMapper;
|
||||
use hashbrown::HashMap;
|
||||
use serde::Deserialize;
|
||||
use specs::WorldExt;
|
||||
use vek::*;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SfxTriggerItem {
|
||||
pub trigger: SfxEvent,
|
||||
pub files: Vec<String>,
|
||||
pub threshold: f64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SfxTriggers {
|
||||
pub items: Vec<SfxTriggerItem>,
|
||||
pub struct SfxTriggers(HashMap<SfxEvent, SfxTriggerItem>);
|
||||
|
||||
impl Default for SfxTriggers {
|
||||
fn default() -> Self {
|
||||
Self(HashMap::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl SfxTriggers {
|
||||
pub fn get_trigger(&self, trigger: &SfxEvent) -> Option<&SfxTriggerItem> {
|
||||
self.0.get(trigger)
|
||||
}
|
||||
|
||||
pub fn get_key_value(&self, trigger: &SfxEvent) -> Option<(&SfxEvent, &SfxTriggerItem)> {
|
||||
self.0.get_key_value(trigger)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SfxMgr {
|
||||
triggers: SfxTriggers,
|
||||
event_mapper: event_mapper::SfxEventMapper,
|
||||
event_mapper: SfxEventMapper,
|
||||
}
|
||||
|
||||
impl SfxMgr {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
triggers: Self::load_sfx_items(),
|
||||
event_mapper: event_mapper::SfxEventMapper::new(),
|
||||
event_mapper: SfxEventMapper::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn maintain(&mut self, audio: &mut AudioFrontend, client: &Client) {
|
||||
self.event_mapper.maintain(client, &self.triggers);
|
||||
|
||||
let ecs = client.state().ecs();
|
||||
|
||||
let player_position = ecs
|
||||
@ -60,16 +78,7 @@ impl SfxMgr {
|
||||
_ => player_position,
|
||||
};
|
||||
|
||||
// Get the SFX config entry for this movement
|
||||
let sfx_trigger_item: Option<&SfxTriggerItem> = self
|
||||
.triggers
|
||||
.items
|
||||
.iter()
|
||||
.find(|item| item.trigger == event.sfx);
|
||||
|
||||
if sfx_trigger_item.is_some() {
|
||||
let item = sfx_trigger_item.expect("Invalid sfx item");
|
||||
|
||||
if let Some(item) = self.triggers.get_trigger(&event.sfx) {
|
||||
let sfx_file = match item.files.len() {
|
||||
1 => item
|
||||
.files
|
||||
@ -90,6 +99,16 @@ impl SfxMgr {
|
||||
let file = assets::load_file("voxygen.audio.sfx", &["ron"])
|
||||
.expect("Failed to load the sfx config file");
|
||||
|
||||
ron::de::from_reader(file).expect("Error parsing sfx manifest")
|
||||
match ron::de::from_reader(file) {
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
log::warn!(
|
||||
"Error parsing sfx config file, sfx will not be available: {}",
|
||||
format!("{:#?}", e)
|
||||
);
|
||||
|
||||
SfxTriggers::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,143 +0,0 @@
|
||||
use crate::DISCORD_INSTANCE;
|
||||
use crossbeam::channel::{unbounded, Sender};
|
||||
use discord_rpc_sdk::{DiscordUser, EventHandlers, RichPresence, RPC};
|
||||
//use parking_lot::Mutex;
|
||||
use std::sync::Mutex;
|
||||
use std::thread;
|
||||
use std::thread::JoinHandle;
|
||||
use std::time::SystemTime;
|
||||
|
||||
/// Connects to the discord application where Images and more resides
|
||||
/// can be viewed at https://discordapp.com/developers/applications/583662036194689035/rich-presence/assets
|
||||
/// but requires an invitation.
|
||||
const DISCORD_APPLICATION_ID: &str = "583662036194689035";
|
||||
|
||||
/// Represents an update of the game which should be reflected in Discord
|
||||
pub enum DiscordUpdate {
|
||||
Details(String),
|
||||
State(String),
|
||||
SmallImg(String),
|
||||
SmallImgDesc(String),
|
||||
LargeImg(String),
|
||||
LargeImgDesc(String),
|
||||
Shutdown,
|
||||
}
|
||||
|
||||
pub struct DiscordState {
|
||||
pub tx: Sender<DiscordUpdate>,
|
||||
pub thread: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
pub fn run() -> Mutex<DiscordState> {
|
||||
let (tx, rx) = unbounded();
|
||||
|
||||
Mutex::new(DiscordState {
|
||||
tx,
|
||||
thread: Some(thread::spawn(move || {
|
||||
let rpc: RPC = match RPC::init::<Handlers>(DISCORD_APPLICATION_ID, true, None) {
|
||||
Ok(rpc) => rpc,
|
||||
Err(e) => {
|
||||
log::error!("failed to initiate discord_game_sdk: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
//Set initial Status
|
||||
let mut current_presence = RichPresence {
|
||||
details: Some("In Menu".to_string()),
|
||||
state: Some("Idling".to_string()),
|
||||
start_time: Some(SystemTime::now()),
|
||||
//end_time: Some(SystemTime::now().checked_add(Duration::from_secs(360)).unwrap()),
|
||||
large_image_key: Some("snow_background".to_string()),
|
||||
large_image_text: Some("Veloren.net".to_string()),
|
||||
small_image_key: Some("veloren_logo_1024".to_string()),
|
||||
small_image_text: Some("*insert joke here*".to_string()),
|
||||
//party_id: Some("randompartyid".to_owned()),
|
||||
//party_size: Some(4),
|
||||
//party_max: Some(13),
|
||||
//spectate_secret: Some("randomhash".to_string()),
|
||||
//join_secret: Some("anotherrandomhash".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
match rpc.update_presence(current_presence.clone()) {
|
||||
Ok(_) => {}
|
||||
Err(e) => log::error!("Failed to update discord presence: {}", e),
|
||||
}
|
||||
|
||||
'outer: loop {
|
||||
rpc.run_callbacks();
|
||||
|
||||
let msg = rx.try_recv();
|
||||
match msg {
|
||||
Ok(update) => {
|
||||
match update {
|
||||
DiscordUpdate::Details(x) => current_presence.details = Some(x),
|
||||
DiscordUpdate::State(x) => current_presence.state = Some(x),
|
||||
DiscordUpdate::SmallImg(x) => {
|
||||
current_presence.small_image_key = Some(x)
|
||||
}
|
||||
DiscordUpdate::SmallImgDesc(x) => {
|
||||
current_presence.small_image_text = Some(x)
|
||||
}
|
||||
DiscordUpdate::LargeImg(x) => {
|
||||
current_presence.large_image_key = Some(x)
|
||||
}
|
||||
DiscordUpdate::LargeImgDesc(x) => {
|
||||
current_presence.large_image_text = Some(x)
|
||||
}
|
||||
DiscordUpdate::Shutdown => break 'outer,
|
||||
};
|
||||
|
||||
match rpc.update_presence(current_presence.clone()) {
|
||||
Ok(_) => {}
|
||||
Err(e) => log::error!("Failed to update discord presence: {}", e),
|
||||
}
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
rpc.clear_presence();
|
||||
})),
|
||||
})
|
||||
}
|
||||
|
||||
struct Handlers;
|
||||
|
||||
impl EventHandlers for Handlers {
|
||||
fn ready(user: DiscordUser) {
|
||||
log::debug!("We're ready! {:?}", user);
|
||||
}
|
||||
|
||||
fn errored(errcode: i32, message: &str) {
|
||||
log::warn!("Error {}: {}", errcode, message);
|
||||
}
|
||||
|
||||
fn disconnected(errcode: i32, message: &str) {
|
||||
log::debug!("Disconnected {}: {}", errcode, message);
|
||||
}
|
||||
|
||||
fn join_game(secret: &str) {
|
||||
log::debug!("Joining {}", secret);
|
||||
}
|
||||
|
||||
fn spectate_game(secret: &str) {
|
||||
log::debug!("Spectating {}", secret);
|
||||
}
|
||||
|
||||
fn join_request(from: DiscordUser) {
|
||||
log::debug!("Join request from {:?}", from);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_all(updates: Vec<DiscordUpdate>) {
|
||||
match DISCORD_INSTANCE.lock() {
|
||||
Ok(disc) => {
|
||||
for update in updates {
|
||||
let _ = disc.tx.send(update);
|
||||
}
|
||||
}
|
||||
Err(e) => log::error!("couldn't send Update to discord: {}", e),
|
||||
}
|
||||
}
|
34
voxygen/src/ecs/comp.rs
Normal file
34
voxygen/src/ecs/comp.rs
Normal file
@ -0,0 +1,34 @@
|
||||
use specs::Component;
|
||||
use specs_idvs::IDVStorage;
|
||||
use vek::*;
|
||||
|
||||
// Floats over entity that has had a health change, rising up over time until it vanishes
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct HpFloater {
|
||||
pub timer: f32,
|
||||
// Numbers of times significant damage has been dealt
|
||||
pub hp_change: i32,
|
||||
// Used for randomly offseting
|
||||
pub rand: f32,
|
||||
}
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct HpFloaterList {
|
||||
// Order oldest to newest
|
||||
pub floaters: Vec<HpFloater>,
|
||||
// Keep from spawning more floaters from same hp change
|
||||
// Note: this can't detect a change if equivalent healing and damage take place simultaneously
|
||||
pub last_hp: u32,
|
||||
}
|
||||
impl Component for HpFloaterList {
|
||||
type Storage = IDVStorage<Self>;
|
||||
}
|
||||
|
||||
// Used for smooth interpolation of visual elements that are tied to entity position
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct Interpolated {
|
||||
pub pos: Vec3<f32>,
|
||||
pub ori: Vec3<f32>,
|
||||
}
|
||||
impl Component for Interpolated {
|
||||
type Storage = IDVStorage<Self>;
|
||||
}
|
28
voxygen/src/ecs/mod.rs
Normal file
28
voxygen/src/ecs/mod.rs
Normal file
@ -0,0 +1,28 @@
|
||||
pub mod comp;
|
||||
pub mod sys;
|
||||
|
||||
use specs::{Entity, World, WorldExt};
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct MyEntity(pub Entity);
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct ExpFloater {
|
||||
pub exp_change: i32, // Maybe you can loose exp :p
|
||||
pub timer: f32,
|
||||
// Used to randomly offset position
|
||||
pub rand: (f32, f32),
|
||||
}
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct MyExpFloaterList {
|
||||
pub floaters: Vec<ExpFloater>,
|
||||
pub last_exp: u32,
|
||||
pub last_level: u32,
|
||||
pub last_exp_max: u32,
|
||||
}
|
||||
|
||||
pub fn init(world: &mut World) {
|
||||
world.register::<comp::HpFloaterList>();
|
||||
world.register::<comp::Interpolated>();
|
||||
world.insert(MyExpFloaterList::default());
|
||||
}
|
17
voxygen/src/ecs/sys.rs
Normal file
17
voxygen/src/ecs/sys.rs
Normal file
@ -0,0 +1,17 @@
|
||||
pub mod floater;
|
||||
mod interpolation;
|
||||
|
||||
use specs::DispatcherBuilder;
|
||||
|
||||
// System names
|
||||
const FLOATER_SYS: &str = "floater_voxygen_sys";
|
||||
const INTERPOLATION_SYS: &str = "interpolation_voxygen_sys";
|
||||
|
||||
pub fn add_local_systems(dispatch_builder: &mut DispatcherBuilder) {
|
||||
dispatch_builder.add(
|
||||
interpolation::Sys,
|
||||
INTERPOLATION_SYS,
|
||||
&[common::sys::PHYS_SYS],
|
||||
);
|
||||
dispatch_builder.add(floater::Sys, FLOATER_SYS, &[INTERPOLATION_SYS]);
|
||||
}
|
181
voxygen/src/ecs/sys/floater.rs
Normal file
181
voxygen/src/ecs/sys/floater.rs
Normal file
@ -0,0 +1,181 @@
|
||||
use crate::ecs::{
|
||||
comp::{HpFloater, HpFloaterList},
|
||||
ExpFloater, MyEntity, MyExpFloaterList,
|
||||
};
|
||||
use common::{
|
||||
comp::{HealthSource, Pos, Stats},
|
||||
state::DeltaTime,
|
||||
sync::Uid,
|
||||
};
|
||||
use specs::{Entities, Join, Read, ReadExpect, ReadStorage, System, Write, WriteStorage};
|
||||
|
||||
// How long floaters last (in seconds)
|
||||
pub const HP_SHOWTIME: f32 = 3.0;
|
||||
pub const MY_HP_SHOWTIME: f32 = 2.5;
|
||||
pub const MY_EXP_SHOWTIME: f32 = 4.0;
|
||||
|
||||
pub struct Sys;
|
||||
impl<'a> System<'a> for Sys {
|
||||
type SystemData = (
|
||||
Entities<'a>,
|
||||
ReadExpect<'a, MyEntity>,
|
||||
Read<'a, DeltaTime>,
|
||||
Write<'a, MyExpFloaterList>,
|
||||
ReadStorage<'a, Uid>,
|
||||
ReadStorage<'a, Pos>,
|
||||
ReadStorage<'a, Stats>,
|
||||
WriteStorage<'a, HpFloaterList>,
|
||||
);
|
||||
|
||||
fn run(
|
||||
&mut self,
|
||||
(entities, my_entity, dt, mut my_exp_floater_list, uids, pos, stats, mut hp_floater_lists): Self::SystemData,
|
||||
) {
|
||||
// Add hp floater lists to all entities with stats and a postion
|
||||
// Note: neccessary in order to know last_hp
|
||||
for (entity, last_hp) in (&entities, &stats, &pos, !&hp_floater_lists)
|
||||
.join()
|
||||
.map(|(e, s, _, _)| (e, s.health.current()))
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
let _ = hp_floater_lists.insert(
|
||||
entity,
|
||||
HpFloaterList {
|
||||
floaters: Vec::new(),
|
||||
last_hp,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Add hp floaters to all entities that have been damaged
|
||||
let my_uid = uids.get(my_entity.0);
|
||||
for (entity, health, hp_floater_list) in (&entities, &stats, &mut hp_floater_lists)
|
||||
.join()
|
||||
.map(|(e, s, fl)| (e, s.health, fl))
|
||||
{
|
||||
// Check if health has changed (won't work if damaged and then healed with
|
||||
// equivalently in the same frame)
|
||||
if hp_floater_list.last_hp != health.current() {
|
||||
hp_floater_list.last_hp = health.current();
|
||||
// TODO: What if multiple health changes occured since last check here
|
||||
// Also, If we make stats store a vec of the last_changes (from say the last frame),
|
||||
// what if the client recieves the stats component from two different server ticks at
|
||||
// once, then one will be lost (tbf this is probably a rare occurance and the results
|
||||
// would just be a transient glitch in the display of these damage numbers) (maybe
|
||||
// health changes could be sent to the client as a list of events)
|
||||
if match health.last_change.1.cause {
|
||||
HealthSource::Attack { by } => {
|
||||
my_entity.0 == entity || my_uid.map_or(false, |&uid| by == uid)
|
||||
}
|
||||
HealthSource::Suicide => my_entity.0 == entity,
|
||||
HealthSource::World => my_entity.0 == entity,
|
||||
HealthSource::Revive => false,
|
||||
HealthSource::Command => true,
|
||||
HealthSource::LevelUp => my_entity.0 == entity,
|
||||
HealthSource::Item => true,
|
||||
HealthSource::Unknown => false,
|
||||
} {
|
||||
hp_floater_list.floaters.push(HpFloater {
|
||||
timer: 0.0,
|
||||
hp_change: health.last_change.1.amount,
|
||||
rand: rand::random(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove floater lists on entities without stats or without posistion
|
||||
for entity in (&entities, !&stats, &hp_floater_lists)
|
||||
.join()
|
||||
.map(|(e, _, _)| e)
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
hp_floater_lists.remove(entity);
|
||||
}
|
||||
for entity in (&entities, !&pos, &hp_floater_lists)
|
||||
.join()
|
||||
.map(|(e, _, _)| e)
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
hp_floater_lists.remove(entity);
|
||||
}
|
||||
|
||||
// Maintain existing floaters
|
||||
for (
|
||||
entity,
|
||||
HpFloaterList {
|
||||
ref mut floaters,
|
||||
ref last_hp,
|
||||
},
|
||||
) in (&entities, &mut hp_floater_lists).join()
|
||||
{
|
||||
for mut floater in floaters.iter_mut() {
|
||||
// Increment timer
|
||||
floater.timer += dt.0;
|
||||
}
|
||||
// Clear floaters if newest floater is past show time or health runs out
|
||||
if floaters.last().map_or(false, |f| {
|
||||
f.timer
|
||||
> if entity != my_entity.0 {
|
||||
HP_SHOWTIME
|
||||
} else {
|
||||
MY_HP_SHOWTIME
|
||||
}
|
||||
|| *last_hp == 0
|
||||
}) {
|
||||
floaters.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Update MyExpFloaterList
|
||||
if let Some(stats) = stats.get(my_entity.0) {
|
||||
let mut fl = my_exp_floater_list;
|
||||
// Add a floater if exp changed
|
||||
// TODO: can't handle if you level up more than once (maybe store total exp in stats)
|
||||
let exp_change = if stats.level.level() != fl.last_level {
|
||||
if stats.level.level() > fl.last_level {
|
||||
stats.exp.current() as i32 + fl.last_exp_max as i32 - fl.last_exp as i32
|
||||
} else {
|
||||
// Level down
|
||||
stats.exp.current() as i32 - stats.exp.maximum() as i32 - fl.last_exp as i32
|
||||
}
|
||||
} else {
|
||||
stats.exp.current() as i32 - fl.last_exp as i32
|
||||
};
|
||||
|
||||
if exp_change != 0 {
|
||||
fl.floaters.push(ExpFloater {
|
||||
timer: 0.0,
|
||||
exp_change,
|
||||
rand: (rand::random(), rand::random()),
|
||||
});
|
||||
}
|
||||
|
||||
// Increment timers
|
||||
for mut floater in &mut fl.floaters {
|
||||
floater.timer += dt.0;
|
||||
}
|
||||
|
||||
// Clear if the newest is past show time
|
||||
if fl
|
||||
.floaters
|
||||
.last()
|
||||
.map_or(false, |f| f.timer > MY_EXP_SHOWTIME)
|
||||
{
|
||||
fl.floaters.clear();
|
||||
}
|
||||
|
||||
// Update stored values
|
||||
fl.last_exp = stats.exp.current();
|
||||
fl.last_exp_max = stats.exp.maximum();
|
||||
fl.last_level = stats.level.level();
|
||||
} else {
|
||||
// Clear if stats component doesn't exist
|
||||
my_exp_floater_list.floaters.clear();
|
||||
// Clear stored values
|
||||
my_exp_floater_list.last_exp = 0;
|
||||
my_exp_floater_list.last_exp_max = 0;
|
||||
my_exp_floater_list.last_level = 0;
|
||||
}
|
||||
}
|
||||
}
|
80
voxygen/src/ecs/sys/interpolation.rs
Normal file
80
voxygen/src/ecs/sys/interpolation.rs
Normal file
@ -0,0 +1,80 @@
|
||||
use crate::ecs::comp::Interpolated;
|
||||
use common::{
|
||||
comp::{Ori, Pos, Vel},
|
||||
state::DeltaTime,
|
||||
};
|
||||
use log::warn;
|
||||
use specs::{Entities, Join, Read, ReadStorage, System, WriteStorage};
|
||||
use vek::*;
|
||||
|
||||
/// This system will allow NPCs to modify their controller
|
||||
pub struct Sys;
|
||||
impl<'a> System<'a> for Sys {
|
||||
type SystemData = (
|
||||
Entities<'a>,
|
||||
Read<'a, DeltaTime>,
|
||||
ReadStorage<'a, Pos>,
|
||||
ReadStorage<'a, Ori>,
|
||||
ReadStorage<'a, Vel>,
|
||||
WriteStorage<'a, Interpolated>,
|
||||
);
|
||||
|
||||
fn run(
|
||||
&mut self,
|
||||
(entities, dt, positions, orientations, velocities, mut interpolated): Self::SystemData,
|
||||
) {
|
||||
// Update interpolated positions and orientations
|
||||
for (pos, ori, i, vel) in (&positions, &orientations, &mut interpolated, &velocities).join()
|
||||
{
|
||||
// Update interpolation values
|
||||
if i.pos.distance_squared(pos.0) < 64.0 * 64.0 {
|
||||
i.pos = Lerp::lerp(i.pos, pos.0 + vel.0 * 0.03, 10.0 * dt.0);
|
||||
let ori_interp = Slerp::slerp(i.ori, ori.0, 5.0 * dt.0);
|
||||
// Check for NaNs
|
||||
// TODO: why are we getting NaNs here! Zero-length ori vectors?
|
||||
i.ori = if !ori_interp.map(|e| e.is_nan()).reduce_or() {
|
||||
ori_interp
|
||||
} else {
|
||||
ori.0
|
||||
};
|
||||
} else {
|
||||
i.pos = pos.0;
|
||||
i.ori = ori.0;
|
||||
}
|
||||
}
|
||||
// Insert interpolation components for entities which don't have them
|
||||
for (entity, pos, ori) in (&entities, &positions, &orientations, !&interpolated)
|
||||
.join()
|
||||
.map(|(e, p, o, _)| (e, p.0, o.0))
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
interpolated
|
||||
.insert(entity, Interpolated { pos, ori })
|
||||
.err()
|
||||
.map(|err| warn!("Error inserting Interpolated component: {}", err));
|
||||
}
|
||||
// Remove Interpolated component from entities which don't have a position or an
|
||||
// orientation or a velocity
|
||||
for entity in (&entities, !&positions, &interpolated)
|
||||
.join()
|
||||
.map(|(e, _, _)| e)
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
interpolated.remove(entity);
|
||||
}
|
||||
for entity in (&entities, !&orientations, &interpolated)
|
||||
.join()
|
||||
.map(|(e, _, _)| e)
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
interpolated.remove(entity);
|
||||
}
|
||||
for entity in (&entities, !&velocities, &interpolated)
|
||||
.join()
|
||||
.map(|(e, _, _)| e)
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
interpolated.remove(entity);
|
||||
}
|
||||
}
|
||||
}
|
@ -5,9 +5,6 @@ use conrod_core::{
|
||||
|
||||
use super::{img_ids::Imgs, settings_window::SettingsTab, Fonts, TEXT_COLOR};
|
||||
|
||||
#[cfg(feature = "discord")]
|
||||
use crate::{discord, discord::DiscordUpdate};
|
||||
|
||||
widget_ids! {
|
||||
struct Ids {
|
||||
esc_bg,
|
||||
@ -153,15 +150,6 @@ impl<'a> Widget for EscMenu<'a> {
|
||||
.set(state.ids.menu_button_5, ui)
|
||||
.was_clicked()
|
||||
{
|
||||
#[cfg(feature = "discord")]
|
||||
{
|
||||
discord::send_all(vec![
|
||||
DiscordUpdate::Details("Menu".into()),
|
||||
DiscordUpdate::State("Idling".into()),
|
||||
DiscordUpdate::LargeImg("bg_main".into()),
|
||||
]);
|
||||
}
|
||||
|
||||
return Some(Event::Logout);
|
||||
};
|
||||
// Quit
|
||||
|
@ -118,10 +118,14 @@ image_ids! {
|
||||
grass: "voxygen.element.icons.item_grass",
|
||||
apple: "voxygen.element.icons.item_apple",
|
||||
mushroom: "voxygen.element.icons.item_mushroom",
|
||||
skull: "voxygen.element.icons.skull",
|
||||
skull_2: "voxygen.element.icons.skull_2",
|
||||
|
||||
// Map
|
||||
map_indicator: "voxygen.element.buttons.map_indicator",
|
||||
indicator_mmap: "voxygen.element.buttons.indicator_mmap",
|
||||
indicator_mmap_2: "voxygen.element.buttons.indicator_mmap_2",
|
||||
indicator_mmap_3: "voxygen.element.buttons.indicator_mmap_3",
|
||||
|
||||
// Crosshair
|
||||
|
||||
@ -154,6 +158,12 @@ image_ids! {
|
||||
mmap_open: "voxygen.element.buttons.button_mmap_open",
|
||||
mmap_open_hover: "voxygen.element.buttons.button_mmap_open_hover",
|
||||
mmap_open_press: "voxygen.element.buttons.button_mmap_open_press",
|
||||
mmap_plus: "voxygen.element.buttons.min_plus.mmap_button-plus",
|
||||
mmap_plus_hover: "voxygen.element.buttons.min_plus.mmap_button-plus_hover",
|
||||
mmap_plus_press: "voxygen.element.buttons.min_plus.mmap_button-plus_hover",
|
||||
mmap_minus: "voxygen.element.buttons.min_plus.mmap_button-min",
|
||||
mmap_minus_hover: "voxygen.element.buttons.min_plus.mmap_button-min_hover",
|
||||
mmap_minus_press: "voxygen.element.buttons.min_plus.mmap_button-min_press",
|
||||
|
||||
// Grid
|
||||
grid: "voxygen.element.buttons.grid",
|
||||
@ -222,6 +232,9 @@ image_ids! {
|
||||
key_gold: "voxygen.voxel.object.key_gold",
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
<ImageGraphic>
|
||||
@ -233,6 +246,13 @@ image_ids! {
|
||||
charwindow_gradient:"voxygen.element.misc_bg.charwindow",
|
||||
map_placeholder: "voxygen.background.map",
|
||||
|
||||
death_bg: "voxygen.background.death",
|
||||
hurt_bg: "voxygen.background.hurt",
|
||||
|
||||
// Enemy Healthbar
|
||||
enemy_health: "voxygen.element.frames.enemybar",
|
||||
// Enemy Bar Content:
|
||||
enemy_bar: "voxygen.element.skillbar.enemy_bar_content",
|
||||
// Spell Book Window
|
||||
spellbook_icon: "voxygen.element.icons.spellbook",
|
||||
// Bag
|
||||
|
@ -5,10 +5,10 @@ use conrod_core::{
|
||||
color,
|
||||
image::Id,
|
||||
widget::{self, Button, Image, Rectangle, Text},
|
||||
widget_ids, Colorable, Positionable, Sizeable, Widget, WidgetCommon,
|
||||
widget_ids, Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon,
|
||||
};
|
||||
use specs::WorldExt;
|
||||
use vek::*;
|
||||
|
||||
widget_ids! {
|
||||
struct Ids {
|
||||
map_frame,
|
||||
@ -30,12 +30,13 @@ widget_ids! {
|
||||
pub struct Map<'a> {
|
||||
_show: &'a Show,
|
||||
client: &'a Client,
|
||||
|
||||
_world_map: Id,
|
||||
imgs: &'a Imgs,
|
||||
fonts: &'a Fonts,
|
||||
#[conrod(common_builder)]
|
||||
common: widget::CommonBuilder,
|
||||
pulse: f32,
|
||||
velocity: f32,
|
||||
}
|
||||
impl<'a> Map<'a> {
|
||||
pub fn new(
|
||||
@ -44,6 +45,8 @@ impl<'a> Map<'a> {
|
||||
imgs: &'a Imgs,
|
||||
world_map: Id,
|
||||
fonts: &'a Fonts,
|
||||
pulse: f32,
|
||||
velocity: f32,
|
||||
) -> Self {
|
||||
Self {
|
||||
_show: show,
|
||||
@ -52,6 +55,8 @@ impl<'a> Map<'a> {
|
||||
client,
|
||||
fonts: fonts,
|
||||
common: widget::CommonBuilder::default(),
|
||||
pulse,
|
||||
velocity,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -81,6 +86,11 @@ impl<'a> Widget for Map<'a> {
|
||||
|
||||
fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
|
||||
let widget::UpdateArgs { state, ui, .. } = args;
|
||||
// Set map transparency to 0.5 when player is moving
|
||||
let mut fade = 1.0;
|
||||
if self.velocity > 2.5 {
|
||||
fade = 0.7
|
||||
};
|
||||
|
||||
// BG
|
||||
Rectangle::fill_with([824.0, 976.0], color::TRANSPARENT)
|
||||
@ -93,24 +103,29 @@ impl<'a> Widget for Map<'a> {
|
||||
Image::new(self.imgs.map_frame_l)
|
||||
.top_left_with_margins_on(state.ids.map_bg, 0.0, 0.0)
|
||||
.w_h(412.0, 488.0)
|
||||
.color(Some(Color::Rgba(1.0, 1.0, 1.0, fade)))
|
||||
.set(state.ids.map_frame_l, ui);
|
||||
Image::new(self.imgs.map_frame_r)
|
||||
.right_from(state.ids.map_frame_l, 0.0)
|
||||
.color(Some(Color::Rgba(1.0, 1.0, 1.0, fade)))
|
||||
.w_h(412.0, 488.0)
|
||||
.set(state.ids.map_frame_r, ui);
|
||||
Image::new(self.imgs.map_frame_br)
|
||||
.down_from(state.ids.map_frame_r, 0.0)
|
||||
.w_h(412.0, 488.0)
|
||||
.color(Some(Color::Rgba(1.0, 1.0, 1.0, fade)))
|
||||
.set(state.ids.map_frame_br, ui);
|
||||
Image::new(self.imgs.map_frame_bl)
|
||||
.down_from(state.ids.map_frame_l, 0.0)
|
||||
.w_h(412.0, 488.0)
|
||||
.color(Some(Color::Rgba(1.0, 1.0, 1.0, fade)))
|
||||
.set(state.ids.map_frame_bl, ui);
|
||||
|
||||
// Icon
|
||||
Image::new(self.imgs.map_icon)
|
||||
.w_h(224.0 / 3.0, 224.0 / 3.0)
|
||||
.top_left_with_margins_on(state.ids.map_frame, -10.0, -10.0)
|
||||
.color(Some(Color::Rgba(1.0, 1.0, 1.0, fade)))
|
||||
.set(state.ids.map_icon, ui);
|
||||
|
||||
// X-Button
|
||||
@ -118,6 +133,7 @@ impl<'a> Widget for Map<'a> {
|
||||
.w_h(28.0, 28.0)
|
||||
.hover_image(self.imgs.close_button_hover)
|
||||
.press_image(self.imgs.close_button_press)
|
||||
.color(Color::Rgba(1.0, 1.0, 1.0, fade - 0.5))
|
||||
.top_right_with_margins_on(state.ids.map_frame_r, 0.0, 0.0)
|
||||
.set(state.ids.map_close, ui)
|
||||
.was_clicked()
|
||||
@ -128,9 +144,10 @@ impl<'a> Widget for Map<'a> {
|
||||
// Location Name
|
||||
match self.client.current_chunk() {
|
||||
Some(chunk) => Text::new(chunk.meta().name())
|
||||
.mid_top_with_margin_on(state.ids.map_bg, 70.0)
|
||||
.font_size(20)
|
||||
.mid_top_with_margin_on(state.ids.map_bg, 55.0)
|
||||
.font_size(60)
|
||||
.color(TEXT_COLOR)
|
||||
.font_id(self.fonts.alkhemi)
|
||||
.parent(state.ids.map_frame_r)
|
||||
.set(state.ids.location_name, ui),
|
||||
None => Text::new(" ")
|
||||
@ -140,9 +157,11 @@ impl<'a> Widget for Map<'a> {
|
||||
.color(TEXT_COLOR)
|
||||
.set(state.ids.location_name, ui),
|
||||
}
|
||||
|
||||
// Map Image
|
||||
Image::new(/*self.world_map*/ self.imgs.map_placeholder)
|
||||
.middle_of(state.ids.map_bg)
|
||||
.color(Some(Color::Rgba(1.0, 1.0, 1.0, fade - 0.1)))
|
||||
.w_h(700.0, 700.0)
|
||||
.parent(state.ids.map_bg)
|
||||
.set(state.ids.grid, ui);
|
||||
@ -156,15 +175,33 @@ impl<'a> Widget for Map<'a> {
|
||||
.map_or(Vec3::zero(), |pos| pos.0);
|
||||
|
||||
let worldsize = 32768.0; // TODO This has to get the actual world size and not be hardcoded
|
||||
let x = player_pos.x as f64 / worldsize * 700.0;
|
||||
let x = player_pos.x as f64 / worldsize * 700.0/*= x-Size of the map image*/;
|
||||
let y = (/*1.0 -*/player_pos.y as f64 / worldsize) * 700.0;
|
||||
let indic_ani = (self.pulse * 6.0/*animation speed*/).cos()/*starts at 1.0*/ * 0.5 + 0.50; // changes the animation frame
|
||||
let indic_scale = 1.2;
|
||||
// Indicator
|
||||
Image::new(self.imgs.map_indicator)
|
||||
.bottom_left_with_margins_on(state.ids.grid, y, x - (12.0 * 1.4) / 2.0)
|
||||
.w_h(12.0 * 1.4, 21.0 * 1.4)
|
||||
.floating(true)
|
||||
.parent(ui.window)
|
||||
.set(state.ids.indicator, ui);
|
||||
Image::new(if indic_ani <= 0.3 {
|
||||
self.imgs.indicator_mmap
|
||||
} else if indic_ani <= 0.6 {
|
||||
self.imgs.indicator_mmap_2
|
||||
} else {
|
||||
self.imgs.indicator_mmap_3
|
||||
})
|
||||
.bottom_left_with_margins_on(state.ids.grid, y, x - (20.0 * 1.2) / 2.0)
|
||||
.w_h(
|
||||
22.0 * 1.2,
|
||||
if indic_ani <= 0.3 {
|
||||
16.0 * indic_scale
|
||||
} else if indic_ani <= 0.6 {
|
||||
23.0 * indic_scale
|
||||
} else {
|
||||
34.0 * indic_scale
|
||||
},
|
||||
)
|
||||
.color(Some(Color::Rgba(1.0, 1.0, 1.0, fade + 0.2)))
|
||||
.floating(true)
|
||||
.parent(ui.window)
|
||||
.set(state.ids.indicator, ui);
|
||||
|
||||
None
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ use conrod_core::{
|
||||
widget::{self, Button, Image, Rectangle, Text},
|
||||
widget_ids, Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon,
|
||||
};
|
||||
use specs::WorldExt;
|
||||
use std::time::{Duration, Instant};
|
||||
use vek::*;
|
||||
|
||||
@ -16,6 +17,8 @@ widget_ids! {
|
||||
mmap_frame_bg,
|
||||
mmap_location,
|
||||
mmap_button,
|
||||
mmap_plus,
|
||||
mmap_minus,
|
||||
zone_display_bg,
|
||||
zone_display,
|
||||
grid,
|
||||
@ -34,6 +37,8 @@ pub struct MiniMap<'a> {
|
||||
fonts: &'a Fonts,
|
||||
#[conrod(common_builder)]
|
||||
common: widget::CommonBuilder,
|
||||
pulse: f32,
|
||||
zoom: f32,
|
||||
}
|
||||
|
||||
impl<'a> MiniMap<'a> {
|
||||
@ -43,6 +48,8 @@ impl<'a> MiniMap<'a> {
|
||||
imgs: &'a Imgs,
|
||||
world_map: Id,
|
||||
fonts: &'a Fonts,
|
||||
pulse: f32,
|
||||
zoom: f32,
|
||||
) -> Self {
|
||||
Self {
|
||||
show,
|
||||
@ -51,6 +58,8 @@ impl<'a> MiniMap<'a> {
|
||||
_world_map: world_map,
|
||||
fonts: fonts,
|
||||
common: widget::CommonBuilder::default(),
|
||||
pulse,
|
||||
zoom,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -86,20 +95,49 @@ impl<'a> Widget for MiniMap<'a> {
|
||||
|
||||
fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
|
||||
let widget::UpdateArgs { state, ui, .. } = args;
|
||||
|
||||
let zoom = self.zoom as f64;
|
||||
if self.show.mini_map {
|
||||
Image::new(self.imgs.mmap_frame)
|
||||
.w_h(100.0 * 4.0, 100.0 * 4.0)
|
||||
.w_h(100.0 * 4.0 * zoom, 100.0 * 4.0 * zoom)
|
||||
.top_right_with_margins_on(ui.window, 5.0, 5.0)
|
||||
.set(state.ids.mmap_frame, ui);
|
||||
|
||||
Rectangle::fill_with([92.0 * 4.0, 82.0 * 4.0], color::TRANSPARENT)
|
||||
.mid_top_with_margin_on(state.ids.mmap_frame, 13.0 * 4.0 + 4.0)
|
||||
.mid_top_with_margin_on(state.ids.mmap_frame, 13.0 * 4.0 + 4.0 * zoom)
|
||||
.set(state.ids.mmap_frame_bg, ui);
|
||||
// Zoom Buttons
|
||||
// TODO: Add zoomable minimap
|
||||
|
||||
/*if Button::image(self.imgs.mmap_plus)
|
||||
.w_h(100.0 * 0.2 * zoom, 100.0 * 0.2 * zoom)
|
||||
.hover_image(self.imgs.mmap_plus_hover)
|
||||
.press_image(self.imgs.mmap_plus_press)
|
||||
.top_left_with_margins_on(state.ids.mmap_frame, 0.0, 0.0)
|
||||
.set(state.ids.mmap_plus, ui)
|
||||
.was_clicked()
|
||||
{
|
||||
if zoom > 0.0 {
|
||||
zoom = zoom + 1.0
|
||||
} else if zoom == 5.0 {
|
||||
}
|
||||
}
|
||||
if Button::image(self.imgs.mmap_minus)
|
||||
.w_h(100.0 * 0.2 * zoom, 100.0 * 0.2 * zoom)
|
||||
.hover_image(self.imgs.mmap_minus_hover)
|
||||
.press_image(self.imgs.mmap_minus_press)
|
||||
.down_from(state.ids.mmap_plus, 0.0)
|
||||
.set(state.ids.mmap_minus, ui)
|
||||
.was_clicked()
|
||||
{
|
||||
if zoom < 6.0 {
|
||||
zoom = zoom - 1.0
|
||||
} else if zoom == 0.0 {
|
||||
}
|
||||
}*/
|
||||
// Map Image
|
||||
Image::new(/*self.world_map*/ self.imgs.map_placeholder)
|
||||
.middle_of(state.ids.mmap_frame_bg)
|
||||
.w_h(92.0 * 4.0, 82.0 * 4.0)
|
||||
.w_h(92.0 * 4.0 * zoom, 82.0 * 4.0 * zoom)
|
||||
.parent(state.ids.mmap_frame_bg)
|
||||
.set(state.ids.grid, ui);
|
||||
// Coordinates
|
||||
@ -114,13 +152,28 @@ impl<'a> Widget for MiniMap<'a> {
|
||||
let worldsize = 32768.0; // TODO This has to get the actual world size and not be hardcoded
|
||||
let x = player_pos.x as f64 / worldsize * 92.0 * 4.0;
|
||||
let y = (/*1.0X-*/player_pos.y as f64 / worldsize) * 82.0 * 4.0;
|
||||
let indic_ani = (self.pulse * 6.0).cos() * 0.5 + 0.5; //Animation timer
|
||||
let indic_scale = 0.8;
|
||||
// Indicator
|
||||
Image::new(self.imgs.indicator_mmap)
|
||||
.bottom_left_with_margins_on(state.ids.grid, y, x - 5.0)
|
||||
.w_h(10.0, 10.0)
|
||||
.floating(true)
|
||||
.parent(ui.window)
|
||||
.set(state.ids.indicator, ui);
|
||||
Image::new(if indic_ani <= 0.5 {
|
||||
self.imgs.indicator_mmap
|
||||
} else {
|
||||
self.imgs.indicator_mmap_2
|
||||
})
|
||||
.bottom_left_with_margins_on(state.ids.grid, y, x - 5.0)
|
||||
.w_h(
|
||||
// Animation frames depening on timer value from 0.0 to 1.0
|
||||
22.0 * 0.8,
|
||||
if indic_ani <= 0.5 {
|
||||
18.0 * indic_scale
|
||||
} else {
|
||||
23.0 * indic_scale
|
||||
},
|
||||
)
|
||||
.color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
|
||||
.floating(true)
|
||||
.parent(ui.window)
|
||||
.set(state.ids.indicator, ui);
|
||||
} else {
|
||||
Image::new(self.imgs.mmap_frame_closed)
|
||||
.w_h(100.0 * 2.0, 11.0 * 2.0)
|
||||
@ -136,7 +189,7 @@ impl<'a> Widget for MiniMap<'a> {
|
||||
.wh(if self.show.mini_map {
|
||||
[100.0 * 0.4; 2]
|
||||
} else {
|
||||
[100.0 * 0.2; 2]
|
||||
[100.0 * 0.2 * zoom; 2]
|
||||
})
|
||||
.hover_image(if self.show.mini_map {
|
||||
self.imgs.mmap_open_hover
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -4,7 +4,7 @@ use super::{
|
||||
};
|
||||
use crate::{
|
||||
render::AaMode,
|
||||
ui::{ImageSlider, RadioList, ScaleMode, ToggleButton},
|
||||
ui::{ImageSlider, ScaleMode, ToggleButton},
|
||||
GlobalState,
|
||||
};
|
||||
use conrod_core::{
|
||||
@ -18,6 +18,7 @@ const FPS_CHOICES: [u32; 11] = [15, 30, 40, 50, 60, 90, 120, 144, 240, 300, 500]
|
||||
widget_ids! {
|
||||
struct Ids {
|
||||
settings_content,
|
||||
settings_content_r,
|
||||
settings_icon,
|
||||
settings_button_mo,
|
||||
settings_close,
|
||||
@ -84,8 +85,8 @@ widget_ids! {
|
||||
fov_slider,
|
||||
fov_text,
|
||||
fov_value,
|
||||
aa_radio_buttons,
|
||||
aa_mode_text,
|
||||
aa_mode_list,
|
||||
audio_volume_slider,
|
||||
audio_volume_text,
|
||||
sfx_volume_slider,
|
||||
@ -109,7 +110,24 @@ widget_ids! {
|
||||
placeholder,
|
||||
chat_transp_title,
|
||||
chat_transp_text,
|
||||
chat_transp_slider
|
||||
chat_transp_slider,
|
||||
sct_title,
|
||||
sct_show_text,
|
||||
sct_show_radio,
|
||||
sct_single_dmg_text,
|
||||
sct_single_dmg_radio,
|
||||
sct_show_batch_text,
|
||||
sct_show_batch_radio,
|
||||
sct_batched_dmg_text,
|
||||
sct_batched_dmg_radio,
|
||||
sct_inc_dmg_text,
|
||||
sct_inc_dmg_radio,
|
||||
sct_batch_inc_text,
|
||||
sct_batch_inc_radio,
|
||||
sct_num_dur_text,
|
||||
sct_num_dur_slider,
|
||||
sct_num_dur_value,
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -178,6 +196,9 @@ pub enum Event {
|
||||
CrosshairType(CrosshairType),
|
||||
UiScale(ScaleChange),
|
||||
ChatTransp(f32),
|
||||
Sct(bool),
|
||||
SctPlayerBatch(bool),
|
||||
SctDamageBatch(bool),
|
||||
}
|
||||
|
||||
pub enum ScaleChange {
|
||||
@ -229,6 +250,10 @@ impl<'a> Widget for SettingsWindow<'a> {
|
||||
.scroll_kids()
|
||||
.scroll_kids_vertically()
|
||||
.set(state.ids.settings_content, ui);
|
||||
Rectangle::fill_with([198.0 * 4.0 * 0.5, 97.0 * 4.0], color::TRANSPARENT)
|
||||
.top_right_with_margins_on(state.ids.settings_content, 0.0, 0.0)
|
||||
.parent(state.ids.settings_content)
|
||||
.set(state.ids.settings_content_r, ui);
|
||||
Scrollbar::y_axis(state.ids.settings_content)
|
||||
.thickness(5.0)
|
||||
.rgba(0.33, 0.33, 0.33, 1.0)
|
||||
@ -279,7 +304,7 @@ impl<'a> Widget for SettingsWindow<'a> {
|
||||
events.push(Event::ChangeTab(SettingsTab::Interface));
|
||||
}
|
||||
|
||||
// Contents
|
||||
// Contents Left Side
|
||||
if let SettingsTab::Interface = self.show.settings_tab {
|
||||
let crosshair_transp = self.global_state.settings.gameplay.crosshair_transp;
|
||||
let crosshair_type = self.global_state.settings.gameplay.crosshair_type;
|
||||
@ -309,7 +334,7 @@ impl<'a> Widget for SettingsWindow<'a> {
|
||||
events.push(Event::ToggleHelp);
|
||||
}
|
||||
|
||||
Text::new("Show Help Window")
|
||||
Text::new("Help Window")
|
||||
.right_from(state.ids.button_help, 10.0)
|
||||
.font_size(14)
|
||||
.font_id(self.fonts.cyri)
|
||||
@ -333,7 +358,7 @@ impl<'a> Widget for SettingsWindow<'a> {
|
||||
events.push(Event::ToggleDebug);
|
||||
}
|
||||
|
||||
Text::new("Show Debug Info")
|
||||
Text::new("Debug Info")
|
||||
.right_from(state.ids.debug_button, 10.0)
|
||||
.font_size(14)
|
||||
.font_id(self.fonts.cyri)
|
||||
@ -360,7 +385,7 @@ impl<'a> Widget for SettingsWindow<'a> {
|
||||
Intro::Never => events.push(Event::Intro(Intro::Show)),
|
||||
}
|
||||
};
|
||||
Text::new("Show Tips on Startup")
|
||||
Text::new("Tips on Startup")
|
||||
.right_from(state.ids.tips_button, 10.0)
|
||||
.font_size(14)
|
||||
.font_id(self.fonts.cyri)
|
||||
@ -451,7 +476,7 @@ impl<'a> Widget for SettingsWindow<'a> {
|
||||
if let Some(new_val) = ImageSlider::continuous(
|
||||
scale.log(2.0),
|
||||
0.5f64.log(2.0),
|
||||
1.2f64.log(2.0),
|
||||
1.0f64.log(2.0),
|
||||
self.imgs.slider_indicator,
|
||||
self.imgs.slider,
|
||||
)
|
||||
@ -683,7 +708,7 @@ impl<'a> Widget for SettingsWindow<'a> {
|
||||
XpBar::OnGain => events.push(Event::ToggleXpBar(XpBar::Always)),
|
||||
}
|
||||
}
|
||||
Text::new("Always show Experience Bar")
|
||||
Text::new("Toggle Experience Bar")
|
||||
.right_from(state.ids.show_xpbar_button, 10.0)
|
||||
.font_size(14)
|
||||
.font_id(self.fonts.cyri)
|
||||
@ -717,7 +742,7 @@ impl<'a> Widget for SettingsWindow<'a> {
|
||||
}
|
||||
}
|
||||
}
|
||||
Text::new("Always show Shortcuts")
|
||||
Text::new("Toggle Shortcuts")
|
||||
.right_from(state.ids.show_shortcuts_button, 10.0)
|
||||
.font_size(14)
|
||||
.font_id(self.fonts.cyri)
|
||||
@ -725,10 +750,151 @@ impl<'a> Widget for SettingsWindow<'a> {
|
||||
.color(TEXT_COLOR)
|
||||
.set(state.ids.show_shortcuts_text, ui);
|
||||
|
||||
Rectangle::fill_with([60.0 * 4.0, 1.0 * 4.0], color::TRANSPARENT)
|
||||
.down_from(state.ids.show_shortcuts_text, 30.0)
|
||||
.set(state.ids.placeholder, ui);
|
||||
|
||||
// Content Right Side
|
||||
|
||||
/*Scrolling Combat text
|
||||
|
||||
O Show Damage Numbers
|
||||
O Show single Damage Numbers
|
||||
O Show batched dealt Damage
|
||||
O Show incoming Damage
|
||||
O Batch incoming Numbers
|
||||
|
||||
Number Display Duration: 1s ----I----5s
|
||||
*/
|
||||
// SCT/ Scrolling Combat Text
|
||||
Text::new("Scrolling Combat Text")
|
||||
.top_left_with_margins_on(state.ids.settings_content_r, 5.0, 5.0)
|
||||
.font_size(18)
|
||||
.font_id(self.fonts.cyri)
|
||||
.color(TEXT_COLOR)
|
||||
.set(state.ids.sct_title, ui);
|
||||
// Generally toggle the SCT
|
||||
let show_sct = ToggleButton::new(
|
||||
self.global_state.settings.gameplay.sct,
|
||||
self.imgs.checkbox,
|
||||
self.imgs.checkbox_checked,
|
||||
)
|
||||
.w_h(18.0, 18.0)
|
||||
.down_from(state.ids.sct_title, 20.0)
|
||||
.hover_images(self.imgs.checkbox_mo, self.imgs.checkbox_checked_mo)
|
||||
.press_images(self.imgs.checkbox_press, self.imgs.checkbox_checked)
|
||||
.set(state.ids.sct_show_radio, ui);
|
||||
|
||||
if self.global_state.settings.gameplay.sct != show_sct {
|
||||
events.push(Event::Sct(!self.global_state.settings.gameplay.sct))
|
||||
}
|
||||
Text::new("Scrolling Combat Text")
|
||||
.right_from(state.ids.sct_show_radio, 10.0)
|
||||
.font_size(14)
|
||||
.font_id(self.fonts.cyri)
|
||||
.graphics_for(state.ids.sct_show_radio)
|
||||
.color(TEXT_COLOR)
|
||||
.set(state.ids.sct_show_text, ui);
|
||||
if self.global_state.settings.gameplay.sct {
|
||||
// Toggle single damage numbers
|
||||
let show_sct_damage_batch = !ToggleButton::new(
|
||||
!self.global_state.settings.gameplay.sct_damage_batch,
|
||||
self.imgs.checkbox,
|
||||
self.imgs.checkbox_checked,
|
||||
)
|
||||
.w_h(18.0, 18.0)
|
||||
.down_from(state.ids.sct_show_text, 8.0)
|
||||
.hover_images(self.imgs.checkbox_mo, self.imgs.checkbox_checked_mo)
|
||||
.press_images(self.imgs.checkbox_press, self.imgs.checkbox_checked)
|
||||
.set(state.ids.sct_single_dmg_radio, ui);
|
||||
|
||||
Text::new("Single Damage Numbers")
|
||||
.right_from(state.ids.sct_single_dmg_radio, 10.0)
|
||||
.font_size(14)
|
||||
.font_id(self.fonts.cyri)
|
||||
.graphics_for(state.ids.sct_single_dmg_radio)
|
||||
.color(TEXT_COLOR)
|
||||
.set(state.ids.sct_single_dmg_text, ui);
|
||||
// Toggle Batched Damage
|
||||
let show_sct_damage_batch = ToggleButton::new(
|
||||
show_sct_damage_batch,
|
||||
self.imgs.checkbox,
|
||||
self.imgs.checkbox_checked,
|
||||
)
|
||||
.w_h(18.0, 18.0)
|
||||
.down_from(state.ids.sct_single_dmg_radio, 8.0)
|
||||
.hover_images(self.imgs.checkbox_mo, self.imgs.checkbox_checked_mo)
|
||||
.press_images(self.imgs.checkbox_press, self.imgs.checkbox_checked)
|
||||
.set(state.ids.sct_show_batch_radio, ui);
|
||||
|
||||
if self.global_state.settings.gameplay.sct_damage_batch != show_sct_damage_batch {
|
||||
events.push(Event::SctDamageBatch(
|
||||
!self.global_state.settings.gameplay.sct_damage_batch,
|
||||
))
|
||||
}
|
||||
Text::new("Cumulated Damage")
|
||||
.right_from(state.ids.sct_show_batch_radio, 10.0)
|
||||
.font_size(14)
|
||||
.font_id(self.fonts.cyri)
|
||||
.graphics_for(state.ids.sct_batched_dmg_radio)
|
||||
.color(TEXT_COLOR)
|
||||
.set(state.ids.sct_show_batch_text, ui);
|
||||
// Toggle Incoming Damage
|
||||
let show_sct_player_batch = !ToggleButton::new(
|
||||
!self.global_state.settings.gameplay.sct_player_batch,
|
||||
self.imgs.checkbox,
|
||||
self.imgs.checkbox_checked,
|
||||
)
|
||||
.w_h(18.0, 18.0)
|
||||
.down_from(state.ids.sct_show_batch_radio, 8.0)
|
||||
.hover_images(self.imgs.checkbox_mo, self.imgs.checkbox_checked_mo)
|
||||
.press_images(self.imgs.checkbox_press, self.imgs.checkbox_checked)
|
||||
.set(state.ids.sct_inc_dmg_radio, ui);
|
||||
|
||||
Text::new("Incoming Damage")
|
||||
.right_from(state.ids.sct_inc_dmg_radio, 10.0)
|
||||
.font_size(14)
|
||||
.font_id(self.fonts.cyri)
|
||||
.graphics_for(state.ids.sct_inc_dmg_radio)
|
||||
.color(TEXT_COLOR)
|
||||
.set(state.ids.sct_inc_dmg_text, ui);
|
||||
// Toggle Batched Incoming Damage
|
||||
let show_sct_player_batch = ToggleButton::new(
|
||||
show_sct_player_batch,
|
||||
self.imgs.checkbox,
|
||||
self.imgs.checkbox_checked,
|
||||
)
|
||||
.w_h(18.0, 18.0)
|
||||
.down_from(state.ids.sct_inc_dmg_radio, 8.0)
|
||||
.hover_images(self.imgs.checkbox_mo, self.imgs.checkbox_checked_mo)
|
||||
.press_images(self.imgs.checkbox_press, self.imgs.checkbox_checked)
|
||||
.set(state.ids.sct_batch_inc_radio, ui);
|
||||
|
||||
if self.global_state.settings.gameplay.sct_player_batch != show_sct_player_batch {
|
||||
events.push(Event::SctPlayerBatch(
|
||||
!self.global_state.settings.gameplay.sct_player_batch,
|
||||
))
|
||||
}
|
||||
Text::new("Cumulated Incoming Damage")
|
||||
.right_from(state.ids.sct_batch_inc_radio, 10.0)
|
||||
.font_size(14)
|
||||
.font_id(self.fonts.cyri)
|
||||
.graphics_for(state.ids.sct_batch_inc_radio)
|
||||
.color(TEXT_COLOR)
|
||||
.set(state.ids.sct_batch_inc_text, ui);
|
||||
}
|
||||
|
||||
// Energybars Numbers
|
||||
// Hotbar text
|
||||
Text::new("Energybar Numbers")
|
||||
.down_from(state.ids.show_shortcuts_button, 20.0)
|
||||
.down_from(
|
||||
if self.global_state.settings.gameplay.sct {
|
||||
state.ids.sct_batch_inc_radio
|
||||
} else {
|
||||
state.ids.sct_show_radio
|
||||
},
|
||||
20.0,
|
||||
)
|
||||
.font_size(18)
|
||||
.font_id(self.fonts.cyri)
|
||||
.color(TEXT_COLOR)
|
||||
@ -857,9 +1023,6 @@ impl<'a> Widget for SettingsWindow<'a> {
|
||||
{
|
||||
events.push(Event::ChatTransp(new_val));
|
||||
}
|
||||
Rectangle::fill_with([40.0 * 4.0, 1.0 * 4.0], color::TRANSPARENT)
|
||||
.down_from(state.ids.chat_transp_title, 30.0)
|
||||
.set(state.ids.placeholder, ui);
|
||||
}
|
||||
|
||||
// 2) Gameplay Tab --------------------------------
|
||||
@ -1332,30 +1495,38 @@ impl<'a> Widget for SettingsWindow<'a> {
|
||||
.font_id(self.fonts.cyri)
|
||||
.color(TEXT_COLOR)
|
||||
.set(state.ids.aa_mode_text, ui);
|
||||
let mode_label_list = [
|
||||
(&AaMode::None, "No AA"),
|
||||
(&AaMode::Fxaa, "FXAA"),
|
||||
(&AaMode::MsaaX4, "MSAA x4"),
|
||||
(&AaMode::MsaaX8, "MSAA x8"),
|
||||
(&AaMode::MsaaX16, "MSAA x16 (experimental)"),
|
||||
(&AaMode::SsaaX4, "SSAA x4"),
|
||||
|
||||
let mode_list = [
|
||||
AaMode::None,
|
||||
AaMode::Fxaa,
|
||||
AaMode::MsaaX4,
|
||||
AaMode::MsaaX8,
|
||||
AaMode::MsaaX16,
|
||||
AaMode::SsaaX4,
|
||||
];
|
||||
if let Some((_, mode)) = RadioList::new(
|
||||
(0..mode_label_list.len())
|
||||
.find(|i| *mode_label_list[*i].0 == self.global_state.settings.graphics.aa_mode)
|
||||
.unwrap_or(0),
|
||||
self.imgs.check,
|
||||
self.imgs.check_checked,
|
||||
&mode_label_list,
|
||||
)
|
||||
.hover_images(self.imgs.check_mo, self.imgs.check_checked_mo)
|
||||
.press_images(self.imgs.check_press, self.imgs.check_press)
|
||||
.down_from(state.ids.aa_mode_text, 8.0)
|
||||
.text_color(TEXT_COLOR)
|
||||
.font_size(12)
|
||||
.set(state.ids.aa_radio_buttons, ui)
|
||||
let mode_label_list = [
|
||||
"No AA",
|
||||
"FXAA",
|
||||
"MSAA x4",
|
||||
"MSAA x8",
|
||||
"MSAA x16 (experimental)",
|
||||
"SSAA x4",
|
||||
];
|
||||
|
||||
// Get which AA mode is currently active
|
||||
let selected = mode_list
|
||||
.iter()
|
||||
.position(|x| *x == self.global_state.settings.graphics.aa_mode);
|
||||
|
||||
if let Some(clicked) = DropDownList::new(&mode_label_list, selected)
|
||||
.w_h(400.0, 22.0)
|
||||
.color(MENU_BG)
|
||||
.label_color(TEXT_COLOR)
|
||||
.label_font_id(self.fonts.cyri)
|
||||
.down_from(state.ids.aa_mode_text, 8.0)
|
||||
.set(state.ids.aa_mode_list, ui)
|
||||
{
|
||||
events.push(Event::ChangeAaMode(*mode))
|
||||
events.push(Event::ChangeAaMode(mode_list[clicked]));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,7 @@ use super::{
|
||||
LOW_HP_COLOR, MANA_COLOR, TEXT_COLOR, XP_COLOR,
|
||||
};
|
||||
use crate::GlobalState;
|
||||
use common::comp::{item::Debug, item::ToolData, item::ToolKind, ItemKind, Stats};
|
||||
use common::comp::{item::Debug, item::ToolData, item::ToolKind, Energy, ItemKind, Stats};
|
||||
use conrod_core::{
|
||||
color,
|
||||
widget::{self, Button, Image, Rectangle, Text},
|
||||
@ -70,14 +70,19 @@ widget_ids! {
|
||||
healthbar_bg,
|
||||
healthbar_filling,
|
||||
health_text,
|
||||
health_text_bg,
|
||||
energybar_bg,
|
||||
energybar_filling,
|
||||
energy_text,
|
||||
energy_text_bg,
|
||||
level_up,
|
||||
level_down,
|
||||
level_align,
|
||||
level_message,
|
||||
level_message_bg,
|
||||
stamina_wheel,
|
||||
death_bg,
|
||||
hurt_bg,
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,6 +97,8 @@ pub struct Skillbar<'a> {
|
||||
imgs: &'a Imgs,
|
||||
fonts: &'a Fonts,
|
||||
stats: &'a Stats,
|
||||
energy: &'a Energy,
|
||||
pulse: f32,
|
||||
#[conrod(common_builder)]
|
||||
common: widget::CommonBuilder,
|
||||
current_resource: ResourceType,
|
||||
@ -103,14 +110,19 @@ impl<'a> Skillbar<'a> {
|
||||
imgs: &'a Imgs,
|
||||
fonts: &'a Fonts,
|
||||
stats: &'a Stats,
|
||||
energy: &'a Energy,
|
||||
pulse: f32,
|
||||
) -> Self {
|
||||
Self {
|
||||
global_state,
|
||||
imgs,
|
||||
fonts,
|
||||
stats,
|
||||
energy,
|
||||
global_state,
|
||||
current_resource: ResourceType::Mana,
|
||||
common: widget::CommonBuilder::default(),
|
||||
pulse,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -154,8 +166,7 @@ impl<'a> Widget for Skillbar<'a> {
|
||||
|
||||
let hp_percentage =
|
||||
self.stats.health.current() as f64 / self.stats.health.maximum() as f64 * 100.0;
|
||||
let energy_percentage =
|
||||
self.stats.energy.current() as f64 / self.stats.energy.maximum() as f64 * 100.0;
|
||||
let energy_percentage = self.energy.current() as f64 / self.energy.maximum() as f64 * 100.0;
|
||||
|
||||
let scale = 2.0;
|
||||
|
||||
@ -164,6 +175,8 @@ impl<'a> Widget for Skillbar<'a> {
|
||||
|
||||
const BG_COLOR: Color = Color::Rgba(1.0, 1.0, 1.0, 0.8);
|
||||
const BG_COLOR_2: Color = Color::Rgba(0.0, 0.0, 0.0, 0.99);
|
||||
let hp_ani = (self.pulse * 4.0/*speed factor*/).cos() * 0.5 + 0.8; //Animation timer
|
||||
let crit_hp_color: Color = Color::Rgba(0.79, 0.19, 0.17, hp_ani);
|
||||
|
||||
// Stamina Wheel
|
||||
/*
|
||||
@ -210,7 +223,7 @@ impl<'a> Widget for Skillbar<'a> {
|
||||
// Update last_value
|
||||
state.update(|s| s.last_level = current_level);
|
||||
state.update(|s| s.last_update_level = Instant::now());
|
||||
}
|
||||
};
|
||||
|
||||
let seconds_level = state.last_update_level.elapsed().as_secs_f32();
|
||||
let fade_level = if current_level == 1 {
|
||||
@ -231,6 +244,12 @@ impl<'a> Widget for Skillbar<'a> {
|
||||
.middle_of(state.ids.level_align)
|
||||
.font_size(30)
|
||||
.font_id(self.fonts.cyri)
|
||||
.color(Color::Rgba(0.0, 0.0, 0.0, fade_level))
|
||||
.set(state.ids.level_message_bg, ui);
|
||||
Text::new(&level_up_text)
|
||||
.bottom_left_with_margins_on(state.ids.level_message_bg, 2.0, 2.0)
|
||||
.font_size(30)
|
||||
.font_id(self.fonts.cyri)
|
||||
.color(Color::Rgba(1.0, 1.0, 1.0, fade_level))
|
||||
.set(state.ids.level_message, ui);
|
||||
Image::new(self.imgs.level_up)
|
||||
@ -246,35 +265,38 @@ impl<'a> Widget for Skillbar<'a> {
|
||||
.graphics_for(state.ids.level_align)
|
||||
.set(state.ids.level_down, ui);
|
||||
// Death message
|
||||
if hp_percentage == 0.0 {
|
||||
if self.stats.is_dead {
|
||||
Text::new("You Died")
|
||||
.mid_top_with_margin_on(ui.window, 60.0)
|
||||
.font_size(40)
|
||||
.middle_of(ui.window)
|
||||
.font_size(50)
|
||||
.font_id(self.fonts.cyri)
|
||||
.color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
|
||||
.set(state.ids.death_message_1_bg, ui);
|
||||
Text::new(&format!(
|
||||
"Press {:?} to respawn.",
|
||||
"Press {:?} to respawn at your Waypoint.\n\
|
||||
\n\
|
||||
Press Enter, type in /waypoint and confirm to set it here.",
|
||||
self.global_state.settings.controls.respawn
|
||||
))
|
||||
.mid_bottom_with_margin_on(state.ids.death_message_1, -30.0)
|
||||
.font_size(15)
|
||||
.mid_bottom_with_margin_on(state.ids.death_message_1_bg, -120.0)
|
||||
.font_size(30)
|
||||
.font_id(self.fonts.cyri)
|
||||
.color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
|
||||
.set(state.ids.death_message_2_bg, ui);
|
||||
|
||||
Text::new("You Died")
|
||||
.top_left_with_margins_on(state.ids.death_message_1_bg, -2.0, -2.0)
|
||||
.font_size(40)
|
||||
.bottom_left_with_margins_on(state.ids.death_message_1_bg, 2.0, 2.0)
|
||||
.font_size(50)
|
||||
.font_id(self.fonts.cyri)
|
||||
.color(CRITICAL_HP_COLOR)
|
||||
.set(state.ids.death_message_1, ui);
|
||||
Text::new(&format!(
|
||||
"Press {:?} to respawn.",
|
||||
"Press {:?} to respawn at your Waypoint.\n\
|
||||
\n\
|
||||
Press Enter, type in /waypoint and confirm to set it here.",
|
||||
self.global_state.settings.controls.respawn
|
||||
))
|
||||
.top_left_with_margins_on(state.ids.death_message_2_bg, -1.5, -1.5)
|
||||
.font_size(15)
|
||||
.bottom_left_with_margins_on(state.ids.death_message_2_bg, 2.0, 2.0)
|
||||
.font_size(30)
|
||||
.color(CRITICAL_HP_COLOR)
|
||||
.set(state.ids.death_message_2, ui);
|
||||
}
|
||||
@ -300,7 +322,6 @@ impl<'a> Widget for Skillbar<'a> {
|
||||
.top_left_with_margins_on(state.ids.xp_bar_left, 2.0 * scale, 10.0 * scale)
|
||||
.set(state.ids.xp_bar_filling, ui);
|
||||
// Level Display
|
||||
|
||||
if self.stats.level.level() < 10 {
|
||||
Text::new(&level)
|
||||
.bottom_left_with_margins_on(
|
||||
@ -777,7 +798,7 @@ impl<'a> Widget for Skillbar<'a> {
|
||||
Image::new(self.imgs.bar_content)
|
||||
.w_h(97.0 * scale * hp_percentage / 100.0, 16.0 * scale)
|
||||
.color(Some(if hp_percentage <= 20.0 {
|
||||
CRITICAL_HP_COLOR
|
||||
crit_hp_color
|
||||
} else if hp_percentage <= 40.0 {
|
||||
LOW_HP_COLOR
|
||||
} else {
|
||||
@ -808,18 +829,30 @@ impl<'a> Widget for Skillbar<'a> {
|
||||
self.stats.health.maximum() as u32
|
||||
);
|
||||
Text::new(&hp_text)
|
||||
.mid_top_with_margin_on(state.ids.healthbar_bg, 5.0 * scale)
|
||||
.mid_top_with_margin_on(state.ids.healthbar_bg, 6.0 * scale)
|
||||
.font_size(14)
|
||||
.font_id(self.fonts.cyri)
|
||||
.color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
|
||||
.set(state.ids.health_text_bg, ui);
|
||||
Text::new(&hp_text)
|
||||
.bottom_left_with_margins_on(state.ids.health_text_bg, 2.0, 2.0)
|
||||
.font_size(14)
|
||||
.font_id(self.fonts.cyri)
|
||||
.color(TEXT_COLOR)
|
||||
.set(state.ids.health_text, ui);
|
||||
let energy_text = format!(
|
||||
"{}/{}",
|
||||
self.stats.energy.current() as u32,
|
||||
self.stats.energy.maximum() as u32
|
||||
self.energy.current() as u32,
|
||||
self.energy.maximum() as u32
|
||||
);
|
||||
Text::new(&energy_text)
|
||||
.mid_top_with_margin_on(state.ids.energybar_bg, 5.0 * scale)
|
||||
.mid_top_with_margin_on(state.ids.energybar_bg, 6.0 * scale)
|
||||
.font_size(14)
|
||||
.font_id(self.fonts.cyri)
|
||||
.color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
|
||||
.set(state.ids.energy_text_bg, ui);
|
||||
Text::new(&energy_text)
|
||||
.bottom_left_with_margins_on(state.ids.energy_text_bg, 2.0, 2.0)
|
||||
.font_size(14)
|
||||
.font_id(self.fonts.cyri)
|
||||
.color(TEXT_COLOR)
|
||||
@ -829,14 +862,26 @@ impl<'a> Widget for Skillbar<'a> {
|
||||
if let BarNumbers::Percent = bar_values {
|
||||
let hp_text = format!("{}%", hp_percentage as u32);
|
||||
Text::new(&hp_text)
|
||||
.mid_top_with_margin_on(state.ids.healthbar_bg, 5.0 * scale)
|
||||
.mid_top_with_margin_on(state.ids.healthbar_bg, 6.0 * scale)
|
||||
.font_size(14)
|
||||
.font_id(self.fonts.cyri)
|
||||
.color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
|
||||
.set(state.ids.health_text_bg, ui);
|
||||
Text::new(&hp_text)
|
||||
.bottom_left_with_margins_on(state.ids.health_text_bg, 2.0, 2.0)
|
||||
.font_size(14)
|
||||
.font_id(self.fonts.cyri)
|
||||
.color(TEXT_COLOR)
|
||||
.set(state.ids.health_text, ui);
|
||||
let energy_text = format!("{}%", energy_percentage as u32);
|
||||
Text::new(&energy_text)
|
||||
.mid_top_with_margin_on(state.ids.energybar_bg, 5.0 * scale)
|
||||
.mid_top_with_margin_on(state.ids.energybar_bg, 6.0 * scale)
|
||||
.font_size(14)
|
||||
.font_id(self.fonts.cyri)
|
||||
.color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
|
||||
.set(state.ids.energy_text_bg, ui);
|
||||
Text::new(&energy_text)
|
||||
.bottom_left_with_margins_on(state.ids.energy_text_bg, 2.0, 2.0)
|
||||
.font_size(14)
|
||||
.font_id(self.fonts.cyri)
|
||||
.color(TEXT_COLOR)
|
||||
|
@ -1,13 +1,11 @@
|
||||
use super::{img_ids::Imgs, Fonts, Show, TEXT_COLOR, TEXT_COLOR_3};
|
||||
|
||||
use common::comp;
|
||||
use conrod_core::{
|
||||
color,
|
||||
widget::{self, Button, Image, Rectangle, Scrollbar, Text},
|
||||
widget_ids, /*, Color*/
|
||||
Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon,
|
||||
};
|
||||
use specs::Join;
|
||||
|
||||
use client::{self, Client};
|
||||
|
||||
@ -178,25 +176,12 @@ impl<'a> Widget for Social<'a> {
|
||||
|
||||
// Players list
|
||||
// TODO: this list changes infrequently enough that it should not have to be recreated every frame
|
||||
let ecs = self.client.state().ecs();
|
||||
let players = ecs.read_storage::<comp::Player>();
|
||||
let mut count = 0;
|
||||
for player in players.join() {
|
||||
if ids.player_names.len() <= count {
|
||||
ids.update(|ids| {
|
||||
ids.player_names
|
||||
.resize(count + 1, &mut ui.widget_id_generator())
|
||||
})
|
||||
}
|
||||
|
||||
Text::new(&player.alias)
|
||||
.down_from(ids.online_title, count as f64 * (15.0 + 3.0))
|
||||
.font_size(15)
|
||||
.font_id(self.fonts.cyri)
|
||||
.color(TEXT_COLOR)
|
||||
.set(ids.player_names[count], ui);
|
||||
|
||||
count += 1;
|
||||
let count = self.client.player_list.len();
|
||||
if ids.player_names.len() < count {
|
||||
ids.update(|ids| {
|
||||
ids.player_names
|
||||
.resize(count, &mut ui.widget_id_generator())
|
||||
})
|
||||
}
|
||||
Text::new(&format!("{} player(s) online\n", count))
|
||||
.top_left_with_margins_on(ids.content_align, -2.0, 7.0)
|
||||
@ -204,6 +189,14 @@ impl<'a> Widget for Social<'a> {
|
||||
.font_id(self.fonts.cyri)
|
||||
.color(TEXT_COLOR)
|
||||
.set(ids.online_title, ui);
|
||||
for (i, (_, player_alias)) in self.client.player_list.iter().enumerate() {
|
||||
Text::new(player_alias)
|
||||
.down(3.0)
|
||||
.font_size(15)
|
||||
.font_id(self.fonts.cyri)
|
||||
.color(TEXT_COLOR)
|
||||
.set(ids.player_names[i], ui);
|
||||
}
|
||||
}
|
||||
|
||||
// Friends Tab
|
||||
|
71
voxygen/src/logging.rs
Normal file
71
voxygen/src/logging.rs
Normal file
@ -0,0 +1,71 @@
|
||||
use fern::colors::{Color, ColoredLevelConfig};
|
||||
|
||||
use crate::settings::Settings;
|
||||
|
||||
pub fn init(
|
||||
settings: &Settings,
|
||||
term_log_level: log::LevelFilter,
|
||||
file_log_level: log::LevelFilter,
|
||||
) {
|
||||
let colors = ColoredLevelConfig::new()
|
||||
.error(Color::Red)
|
||||
.warn(Color::Yellow)
|
||||
.info(Color::Cyan)
|
||||
.debug(Color::Green)
|
||||
.trace(Color::BrightBlack);
|
||||
|
||||
let mut base = fern::Dispatch::new()
|
||||
.level_for("dot_vox::parser", log::LevelFilter::Warn)
|
||||
.level_for("gfx_device_gl::factory", log::LevelFilter::Warn)
|
||||
.level_for("uvth", log::LevelFilter::Warn)
|
||||
.level_for("tiny_http", log::LevelFilter::Warn);
|
||||
|
||||
let time = chrono::offset::Utc::now();
|
||||
|
||||
let mut file_cfg =
|
||||
fern::Dispatch::new()
|
||||
.level(file_log_level)
|
||||
.format(|out, message, record| {
|
||||
out.finish(format_args!(
|
||||
"{}[{}:{}][{}] {}",
|
||||
chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"),
|
||||
record.target(),
|
||||
record
|
||||
.line()
|
||||
.map(|x| x.to_string())
|
||||
.unwrap_or("X".to_string()),
|
||||
record.level(),
|
||||
message
|
||||
))
|
||||
});
|
||||
|
||||
// Try to create the log file.
|
||||
// Incase of it failing we simply print it out to the console.
|
||||
let mut log_file_created = Ok(());
|
||||
match fern::log_file(&format!("voxygen-{}.log", time.format("%Y-%m-%d-%H"))) {
|
||||
Ok(log_file) => file_cfg = file_cfg.chain(log_file),
|
||||
Err(e) => log_file_created = Err(e),
|
||||
}
|
||||
|
||||
let stdout_cfg = fern::Dispatch::new()
|
||||
.level(term_log_level)
|
||||
.format(move |out, message, record| {
|
||||
out.finish(format_args!(
|
||||
"[{}] {}",
|
||||
colors.color(record.level()),
|
||||
message
|
||||
))
|
||||
})
|
||||
.chain(std::io::stdout());
|
||||
|
||||
if settings.log.log_to_file {
|
||||
base = base.chain(file_cfg);
|
||||
}
|
||||
base.chain(stdout_cfg)
|
||||
.apply()
|
||||
.expect("Failed to setup logging!");
|
||||
|
||||
if let Err(e) = log_file_created {
|
||||
log::error!("Failed to create log file! {}", e);
|
||||
}
|
||||
}
|
@ -2,23 +2,15 @@
|
||||
#![feature(drain_filter)]
|
||||
#![recursion_limit = "2048"]
|
||||
|
||||
#[cfg(feature = "discord")]
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
|
||||
#[cfg(feature = "discord")]
|
||||
pub mod discord;
|
||||
|
||||
#[cfg(feature = "discord")]
|
||||
use std::sync::Mutex;
|
||||
|
||||
#[macro_use]
|
||||
pub mod ui;
|
||||
pub mod anim;
|
||||
pub mod audio;
|
||||
mod ecs;
|
||||
pub mod error;
|
||||
pub mod hud;
|
||||
pub mod key_state;
|
||||
mod logging;
|
||||
pub mod menu;
|
||||
pub mod mesh;
|
||||
pub mod render;
|
||||
@ -33,9 +25,7 @@ pub mod window;
|
||||
pub use crate::error::Error;
|
||||
|
||||
use crate::{audio::AudioFrontend, menu::main::MainMenuState, settings::Settings, window::Window};
|
||||
use log::{self, debug, error, info};
|
||||
|
||||
use fern::colors::{Color, ColoredLevelConfig};
|
||||
use log::{debug, error};
|
||||
use std::{mem, panic, str::FromStr};
|
||||
|
||||
/// A type used to store state that is shared between all play states.
|
||||
@ -88,17 +78,25 @@ pub trait PlayState {
|
||||
fn name(&self) -> &'static str;
|
||||
}
|
||||
|
||||
#[cfg(feature = "discord")]
|
||||
lazy_static! {
|
||||
//Set up discord rich presence
|
||||
static ref DISCORD_INSTANCE: Mutex<discord::DiscordState> = {
|
||||
discord::run()
|
||||
};
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Initialize logging.
|
||||
let term_log_level = std::env::var_os("VOXYGEN_LOG")
|
||||
.and_then(|env| env.to_str().map(|s| s.to_owned()))
|
||||
.and_then(|s| log::LevelFilter::from_str(&s).ok())
|
||||
.unwrap_or(log::LevelFilter::Warn);
|
||||
|
||||
let file_log_level = std::env::var_os("VOXYGEN_FILE_LOG")
|
||||
.and_then(|env| env.to_str().map(|s| s.to_owned()))
|
||||
.and_then(|s| log::LevelFilter::from_str(&s).ok())
|
||||
.unwrap_or(log::LevelFilter::Debug);
|
||||
|
||||
// Load the settings
|
||||
// Note: This won't log anything due to it being called before ``logging::init``.
|
||||
// The issue is we need to read a setting to decide whether we create a log file or not.
|
||||
let settings = Settings::load();
|
||||
|
||||
logging::init(&settings, term_log_level, file_log_level);
|
||||
|
||||
// Save settings to add new fields or create the file if it is not already there
|
||||
if let Err(err) = settings.save_to_file() {
|
||||
panic!("Failed to save settings: {:?}", err);
|
||||
@ -124,67 +122,7 @@ fn main() {
|
||||
info_message: None,
|
||||
};
|
||||
|
||||
let settings = &global_state.settings;
|
||||
|
||||
// Initialize logging.
|
||||
let term_log_level = std::env::var_os("VOXYGEN_LOG")
|
||||
.and_then(|env| env.to_str().map(|s| s.to_owned()))
|
||||
.and_then(|s| log::LevelFilter::from_str(&s).ok())
|
||||
.unwrap_or(log::LevelFilter::Warn);
|
||||
|
||||
let colors = ColoredLevelConfig::new()
|
||||
.error(Color::Red)
|
||||
.warn(Color::Yellow)
|
||||
.info(Color::Cyan)
|
||||
.debug(Color::Green)
|
||||
.trace(Color::BrightBlack);
|
||||
|
||||
let base = fern::Dispatch::new()
|
||||
.level_for("dot_vox::parser", log::LevelFilter::Warn)
|
||||
.level_for("gfx_device_gl::factory", log::LevelFilter::Warn)
|
||||
.level_for("veloren_voxygen::discord", log::LevelFilter::Warn);
|
||||
// TODO: Filter tracing better such that our own tracing gets seen more easily
|
||||
|
||||
let time = chrono::offset::Utc::now();
|
||||
|
||||
let file_cfg = fern::Dispatch::new()
|
||||
.level(log::LevelFilter::Trace)
|
||||
.format(|out, message, record| {
|
||||
out.finish(format_args!(
|
||||
"{}[{}:{}][{}] {}",
|
||||
chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"),
|
||||
record.target(),
|
||||
record
|
||||
.line()
|
||||
.map(|x| x.to_string())
|
||||
.unwrap_or("X".to_string()),
|
||||
record.level(),
|
||||
message
|
||||
))
|
||||
})
|
||||
.chain(
|
||||
fern::log_file(&format!("voxygen-{}.log", time.format("%Y-%m-%d-%H")))
|
||||
.expect("Failed to create log file!"),
|
||||
);
|
||||
|
||||
let stdout_cfg = fern::Dispatch::new()
|
||||
.level(term_log_level)
|
||||
.format(move |out, message, record| {
|
||||
out.finish(format_args!(
|
||||
"[{}] {}",
|
||||
colors.color(record.level()),
|
||||
message
|
||||
))
|
||||
})
|
||||
.chain(std::io::stdout());
|
||||
|
||||
base.chain(file_cfg)
|
||||
.chain(stdout_cfg)
|
||||
.apply()
|
||||
.expect("Failed to setup logging!");
|
||||
|
||||
// Set up panic handler to relay swish panic messages to the user
|
||||
let settings_clone = settings.clone();
|
||||
let default_hook = panic::take_hook();
|
||||
panic::set_hook(Box::new(move |panic_info| {
|
||||
let panic_info_payload = panic_info.payload();
|
||||
@ -231,7 +169,12 @@ fn main() {
|
||||
\n\
|
||||
Panic Payload: {:?}\n\
|
||||
PanicInfo: {}",
|
||||
settings_clone.log.file, reason, panic_info,
|
||||
// TODO: Verify that this works
|
||||
Settings::get_settings_path()
|
||||
.join("voxygen-<date>.log")
|
||||
.display(),
|
||||
reason,
|
||||
panic_info,
|
||||
);
|
||||
|
||||
error!(
|
||||
@ -241,27 +184,22 @@ fn main() {
|
||||
);
|
||||
|
||||
#[cfg(feature = "msgbox")]
|
||||
msgbox::create("Voxygen has panicked", &msg, msgbox::IconType::Error);
|
||||
{
|
||||
#[cfg(target_os = "macos")]
|
||||
dispatch::Queue::main()
|
||||
.sync(|| msgbox::create("Voxygen has panicked", &msg, msgbox::IconType::Error));
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
msgbox::create("Voxygen has panicked", &msg, msgbox::IconType::Error);
|
||||
}
|
||||
|
||||
default_hook(panic_info);
|
||||
}));
|
||||
|
||||
// Initialise Discord
|
||||
#[cfg(feature = "discord")]
|
||||
{
|
||||
use discord::DiscordUpdate;
|
||||
discord::send_all(vec![
|
||||
DiscordUpdate::Details("Menu".into()),
|
||||
DiscordUpdate::State("Idling".into()),
|
||||
DiscordUpdate::LargeImg("bg_main".into()),
|
||||
]);
|
||||
}
|
||||
|
||||
// Set up the initial play state.
|
||||
let mut states: Vec<Box<dyn PlayState>> = vec![Box::new(MainMenuState::new(&mut global_state))];
|
||||
states
|
||||
.last()
|
||||
.map(|current_state| info!("Started game with state '{}'", current_state.name()));
|
||||
.map(|current_state| debug!("Started game with state '{}'", current_state.name()));
|
||||
|
||||
// What's going on here?
|
||||
// ---------------------
|
||||
@ -279,7 +217,7 @@ fn main() {
|
||||
match state_result {
|
||||
PlayStateResult::Shutdown => {
|
||||
direction = Direction::Backwards;
|
||||
info!("Shutting down all states...");
|
||||
debug!("Shutting down all states...");
|
||||
while states.last().is_some() {
|
||||
states.pop().map(|old_state| {
|
||||
debug!("Popped state '{}'.", old_state.name());
|
||||
@ -315,25 +253,6 @@ fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
//Properly shutdown discord thread
|
||||
#[cfg(feature = "discord")]
|
||||
{
|
||||
match DISCORD_INSTANCE.lock() {
|
||||
Ok(mut disc) => {
|
||||
let _ = disc.tx.send(discord::DiscordUpdate::Shutdown);
|
||||
match disc.thread.take() {
|
||||
Some(th) => {
|
||||
let _ = th.join();
|
||||
}
|
||||
None => {
|
||||
error!("couldn't gracefully shutdown discord thread");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => error!("couldn't gracefully shutdown discord thread: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
// Save any unsaved changes to settings
|
||||
global_state.settings.save_to_file_warn();
|
||||
}
|
||||
|
@ -108,11 +108,11 @@ impl PlayState for CharSelectionState {
|
||||
.render(global_state.window.renderer_mut(), self.scene.globals());
|
||||
|
||||
// Tick the client (currently only to keep the connection alive).
|
||||
if let Err(err) = self
|
||||
.client
|
||||
.borrow_mut()
|
||||
.tick(comp::ControllerInputs::default(), clock.get_last_delta())
|
||||
{
|
||||
if let Err(err) = self.client.borrow_mut().tick(
|
||||
comp::ControllerInputs::default(),
|
||||
clock.get_last_delta(),
|
||||
|_| {},
|
||||
) {
|
||||
global_state.info_message = Some(
|
||||
"Connection lost!\nDid the server restart?\nIs the client up to date?"
|
||||
.to_owned(),
|
||||
|
@ -21,6 +21,7 @@ use common::{
|
||||
terrain::BlockKind,
|
||||
};
|
||||
use log::error;
|
||||
use specs::WorldExt;
|
||||
use vek::*;
|
||||
|
||||
struct Skybox {
|
||||
@ -168,6 +169,8 @@ impl Scene {
|
||||
1.0 / 60.0, // TODO: Use actual deltatime here?
|
||||
1.0,
|
||||
1.0,
|
||||
0,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,10 @@
|
||||
use client::{error::Error as ClientError, Client};
|
||||
use common::comp;
|
||||
use common::{comp, net::PostError};
|
||||
use crossbeam::channel::{unbounded, Receiver, TryRecvError};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::{net::ToSocketAddrs, thread, time::Duration};
|
||||
|
||||
#[cfg(feature = "discord")]
|
||||
use crate::{discord, discord::DiscordUpdate};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
// Error parsing input string or error resolving host name.
|
||||
@ -54,7 +51,7 @@ impl ClientInit {
|
||||
|
||||
let mut last_err = None;
|
||||
|
||||
'tries: for _ in 0..=60 {
|
||||
'tries: for _ in 0..60 + 1 {
|
||||
// 300 Seconds
|
||||
if cancel2.load(Ordering::Relaxed) {
|
||||
break;
|
||||
@ -72,21 +69,14 @@ impl ClientInit {
|
||||
}
|
||||
//client.register(player, password);
|
||||
let _ = tx.send(Ok(client));
|
||||
|
||||
#[cfg(feature = "discord")]
|
||||
{
|
||||
if !server_address.eq("127.0.0.1") {
|
||||
discord::send_all(vec![
|
||||
DiscordUpdate::Details(server_address),
|
||||
DiscordUpdate::State("Playing...".into()),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
Err(err) => {
|
||||
match err {
|
||||
ClientError::Network(PostError::Bincode(_)) => {
|
||||
last_err = Some(Error::ConnectionFailed(err));
|
||||
break 'tries;
|
||||
}
|
||||
// Assume the connection failed and try again soon
|
||||
ClientError::Network(_) => {}
|
||||
ClientError::TooManyPlayers => {
|
||||
|
@ -45,7 +45,7 @@ impl PlayState for MainMenuState {
|
||||
if self.title_music_channel.is_none() && global_state.settings.audio.audio_on {
|
||||
self.title_music_channel = global_state
|
||||
.audio
|
||||
.play_music("voxygen.audio.soundtrack.veloren_title_tune-3");
|
||||
.play_music("voxygen.audio.soundtrack.veloren_title_tune");
|
||||
}
|
||||
|
||||
// Reset singleplayer server if it was running already
|
||||
@ -69,8 +69,10 @@ impl PlayState for MainMenuState {
|
||||
|
||||
// Poll client creation.
|
||||
match client_init.as_ref().and_then(|init| init.poll()) {
|
||||
Some(Ok(client)) => {
|
||||
Some(Ok(mut client)) => {
|
||||
self.main_menu_ui.connected();
|
||||
// Register voxygen components / resources
|
||||
crate::ecs::init(client.state_mut().ecs_mut());
|
||||
return PlayStateResult::Push(Box::new(CharSelectionState::new(
|
||||
global_state,
|
||||
std::rc::Rc::new(std::cell::RefCell::new(client)),
|
||||
|
@ -1,3 +1,4 @@
|
||||
use crate::ui::Graphic;
|
||||
use crate::{
|
||||
render::Renderer,
|
||||
ui::{
|
||||
@ -7,6 +8,7 @@ use crate::{
|
||||
},
|
||||
GlobalState,
|
||||
};
|
||||
use common::assets::load_expect;
|
||||
use conrod_core::{
|
||||
color,
|
||||
color::TRANSPARENT,
|
||||
@ -14,6 +16,7 @@ use conrod_core::{
|
||||
widget::{text_box::Event as TextBoxEvent, Button, Image, List, Rectangle, Text, TextBox},
|
||||
widget_ids, Borderable, Color, Colorable, Labelable, Positionable, Sizeable, Widget,
|
||||
};
|
||||
use rand::{seq::SliceRandom, thread_rng};
|
||||
use std::time::Duration;
|
||||
|
||||
widget_ids! {
|
||||
@ -84,7 +87,6 @@ image_ids! {
|
||||
|
||||
<ImageGraphic>
|
||||
bg: "voxygen.background.bg_main",
|
||||
load: "voxygen.background.bg_load",
|
||||
|
||||
<BlankGraphic>
|
||||
nothing: (),
|
||||
@ -129,6 +131,7 @@ pub enum PopupType {
|
||||
Error,
|
||||
ConnectionInfo,
|
||||
}
|
||||
|
||||
pub struct PopupData {
|
||||
msg: String,
|
||||
button_text: String,
|
||||
@ -150,6 +153,7 @@ pub struct MainMenuUi {
|
||||
show_servers: bool,
|
||||
show_disclaimer: bool,
|
||||
time: f32,
|
||||
bg_img_id: conrod_core::image::Id,
|
||||
}
|
||||
|
||||
impl MainMenuUi {
|
||||
@ -157,6 +161,18 @@ impl MainMenuUi {
|
||||
let window = &mut global_state.window;
|
||||
let networking = &global_state.settings.networking;
|
||||
let gameplay = &global_state.settings.gameplay;
|
||||
// Randomly loaded background images
|
||||
let bg_imgs = [
|
||||
"voxygen.background.bg_1",
|
||||
"voxygen.background.bg_2",
|
||||
"voxygen.background.bg_3",
|
||||
"voxygen.background.bg_4",
|
||||
"voxygen.background.bg_5",
|
||||
"voxygen.background.bg_6",
|
||||
"voxygen.background.bg_7",
|
||||
"voxygen.background.bg_8",
|
||||
];
|
||||
let mut rng = thread_rng();
|
||||
|
||||
let mut ui = Ui::new(window).unwrap();
|
||||
ui.set_scaling_mode(gameplay.ui_scale);
|
||||
@ -165,6 +181,9 @@ impl MainMenuUi {
|
||||
// Load images
|
||||
let imgs = Imgs::load(&mut ui).expect("Failed to load images");
|
||||
let rot_imgs = ImgsRot::load(&mut ui).expect("Failed to load images!");
|
||||
let bg_img_id = ui.add_graphic(Graphic::Image(load_expect(
|
||||
bg_imgs.choose(&mut rng).unwrap(),
|
||||
)));
|
||||
// Load fonts
|
||||
let fonts = Fonts::load(&mut ui).expect("Failed to load fonts");
|
||||
|
||||
@ -183,6 +202,7 @@ impl MainMenuUi {
|
||||
connect: false,
|
||||
time: 0.0,
|
||||
show_disclaimer: global_state.settings.show_disclaimer,
|
||||
bg_img_id,
|
||||
}
|
||||
}
|
||||
|
||||
@ -225,14 +245,14 @@ impl MainMenuUi {
|
||||
.desc_text_color(TEXT_COLOR_2);
|
||||
|
||||
// Background image, Veloren logo, Alpha-Version Label
|
||||
|
||||
Image::new(if self.connect {
|
||||
self.imgs.load
|
||||
self.bg_img_id
|
||||
} else {
|
||||
self.imgs.bg
|
||||
})
|
||||
.middle_of(ui_widgets.window)
|
||||
.set(self.ids.bg, ui_widgets);
|
||||
|
||||
// Version displayed top right corner
|
||||
Text::new(&version)
|
||||
.color(TEXT_COLOR)
|
||||
|
@ -236,7 +236,27 @@ impl<V: RectRasterableVol<Vox = Block> + ReadVol + Debug> Meshable<TerrainPipeli
|
||||
offs,
|
||||
&colors, //&[[[colors[1][1][1]; 3]; 3]; 3],
|
||||
|pos, norm, col, ao, light| {
|
||||
TerrainVertex::new(pos, norm, col, light.min(ao))
|
||||
let light = (light.min(ao) * 255.0) as u32;
|
||||
let norm = if norm.x != 0.0 {
|
||||
if norm.x < 0.0 {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
}
|
||||
} else if norm.y != 0.0 {
|
||||
if norm.y < 0.0 {
|
||||
2
|
||||
} else {
|
||||
3
|
||||
}
|
||||
} else {
|
||||
if norm.z < 0.0 {
|
||||
4
|
||||
} else {
|
||||
5
|
||||
}
|
||||
};
|
||||
TerrainVertex::new(norm, light, pos, col)
|
||||
},
|
||||
&lights,
|
||||
);
|
||||
|
@ -42,26 +42,18 @@ gfx_defines! {
|
||||
}
|
||||
|
||||
impl Vertex {
|
||||
pub fn new(pos: Vec3<f32>, norm: Vec3<f32>, col: Rgb<f32>, light: f32) -> Self {
|
||||
let (norm_axis, norm_dir) = norm
|
||||
.as_slice()
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.find(|(_i, e)| **e != 0.0)
|
||||
.unwrap_or((0, &1.0));
|
||||
let norm_bits = (norm_axis << 1) | if *norm_dir > 0.0 { 1 } else { 0 };
|
||||
|
||||
pub fn new(norm_bits: u32, light: u32, pos: Vec3<f32>, col: Rgb<f32>) -> Self {
|
||||
Self {
|
||||
pos_norm: 0
|
||||
| ((pos.x as u32) & 0x00FF) << 0
|
||||
| ((pos.y as u32) & 0x00FF) << 8
|
||||
| ((pos.z.max(0.0).min((1 << 13) as f32) as u32) & 0x1FFF) << 16
|
||||
| ((norm_bits as u32) & 0x7) << 29,
|
||||
| (norm_bits & 0x7) << 29,
|
||||
col_light: 0
|
||||
| ((col.r.mul(255.0) as u32) & 0xFF) << 8
|
||||
| ((col.g.mul(255.0) as u32) & 0xFF) << 16
|
||||
| ((col.b.mul(255.0) as u32) & 0xFF) << 24
|
||||
| ((light.mul(255.0) as u32) & 0xFF) << 0,
|
||||
| (light & 0xFF) << 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -43,10 +43,10 @@ impl Pipeline for UiPipeline {
|
||||
type Vertex = Vertex;
|
||||
}
|
||||
|
||||
impl From<Vec3<f32>> for Locals {
|
||||
fn from(pos: Vec3<f32>) -> Self {
|
||||
impl From<Vec4<f32>> for Locals {
|
||||
fn from(pos: Vec4<f32>) -> Self {
|
||||
Self {
|
||||
pos: [pos.x, pos.y, pos.z, 1.0],
|
||||
pos: pos.into_array(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
use client::Client;
|
||||
use common::vol::{ReadVol, Vox};
|
||||
use frustum_query::frustum::Frustum;
|
||||
use std::f32::consts::PI;
|
||||
use treeculler::Frustum;
|
||||
use vek::*;
|
||||
|
||||
const NEAR_PLANE: f32 = 0.5;
|
||||
@ -100,13 +100,10 @@ impl Camera {
|
||||
(view_mat, proj_mat, cam_pos)
|
||||
}
|
||||
|
||||
pub fn frustum(&self, client: &Client) -> Frustum {
|
||||
pub fn frustum(&self, client: &Client) -> Frustum<f32> {
|
||||
let (view_mat, proj_mat, _) = self.compute_dependents(client);
|
||||
|
||||
Frustum::from_modelview_and_projection(
|
||||
&view_mat.into_col_array(),
|
||||
&proj_mat.into_col_array(),
|
||||
)
|
||||
Frustum::from_modelview_projection((proj_mat * view_mat).into_col_arrays())
|
||||
}
|
||||
|
||||
/// Rotate the camera about its focus by the given delta, limiting the input accordingly.
|
||||
|
@ -26,7 +26,8 @@ use common::{
|
||||
};
|
||||
use hashbrown::HashMap;
|
||||
use log::trace;
|
||||
use specs::{Entity as EcsEntity, Join};
|
||||
use specs::{Entity as EcsEntity, Join, WorldExt};
|
||||
use treeculler::{BVol, BoundingSphere};
|
||||
use vek::*;
|
||||
|
||||
const DAMAGE_FADE_COEFFICIENT: f64 = 5.0;
|
||||
@ -66,12 +67,13 @@ impl FigureMgr {
|
||||
self.model_cache.clean(tick);
|
||||
}
|
||||
|
||||
pub fn maintain(&mut self, renderer: &mut Renderer, client: &Client) {
|
||||
pub fn maintain(&mut self, renderer: &mut Renderer, client: &Client, camera: &Camera) {
|
||||
let time = client.state().get_time();
|
||||
let tick = client.get_tick();
|
||||
let ecs = client.state().ecs();
|
||||
let view_distance = client.view_distance().unwrap_or(1);
|
||||
let dt = client.state().get_delta_time();
|
||||
let frustum = camera.frustum(client);
|
||||
// Get player position.
|
||||
let player_pos = ecs
|
||||
.read_storage::<Pos>()
|
||||
@ -133,6 +135,172 @@ impl FigureMgr {
|
||||
}
|
||||
continue;
|
||||
} else if vd_frac > 1.0 {
|
||||
match body {
|
||||
Body::Humanoid(_) => {
|
||||
self.character_states
|
||||
.get_mut(&entity)
|
||||
.map(|state| state.visible = false);
|
||||
}
|
||||
Body::QuadrupedSmall(_) => {
|
||||
self.quadruped_small_states
|
||||
.get_mut(&entity)
|
||||
.map(|state| state.visible = false);
|
||||
}
|
||||
Body::QuadrupedMedium(_) => {
|
||||
self.quadruped_medium_states
|
||||
.get_mut(&entity)
|
||||
.map(|state| state.visible = false);
|
||||
}
|
||||
Body::BirdMedium(_) => {
|
||||
self.bird_medium_states
|
||||
.get_mut(&entity)
|
||||
.map(|state| state.visible = false);
|
||||
}
|
||||
Body::FishMedium(_) => {
|
||||
self.fish_medium_states
|
||||
.get_mut(&entity)
|
||||
.map(|state| state.visible = false);
|
||||
}
|
||||
Body::Dragon(_) => {
|
||||
self.dragon_states
|
||||
.get_mut(&entity)
|
||||
.map(|state| state.visible = false);
|
||||
}
|
||||
Body::BirdSmall(_) => {
|
||||
self.bird_small_states
|
||||
.get_mut(&entity)
|
||||
.map(|state| state.visible = false);
|
||||
}
|
||||
Body::FishSmall(_) => {
|
||||
self.fish_small_states
|
||||
.get_mut(&entity)
|
||||
.map(|state| state.visible = false);
|
||||
}
|
||||
Body::BipedLarge(_) => {
|
||||
self.biped_large_states
|
||||
.get_mut(&entity)
|
||||
.map(|state| state.visible = false);
|
||||
}
|
||||
Body::Object(_) => {
|
||||
self.object_states
|
||||
.get_mut(&entity)
|
||||
.map(|state| state.visible = false);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Don't process figures outside the frustum spectrum
|
||||
let (in_frustum, lpindex) =
|
||||
BoundingSphere::new(pos.0.into_array(), scale.unwrap_or(&Scale(1.0)).0 * 2.0)
|
||||
.coherent_test_against_frustum(
|
||||
&frustum,
|
||||
match body {
|
||||
Body::Humanoid(_) => self
|
||||
.character_states
|
||||
.get(&entity)
|
||||
.map(|state| state.lpindex),
|
||||
Body::QuadrupedSmall(_) => self
|
||||
.quadruped_small_states
|
||||
.get(&entity)
|
||||
.map(|state| state.lpindex),
|
||||
Body::QuadrupedMedium(_) => self
|
||||
.quadruped_medium_states
|
||||
.get(&entity)
|
||||
.map(|state| state.lpindex),
|
||||
Body::BirdMedium(_) => self
|
||||
.bird_medium_states
|
||||
.get(&entity)
|
||||
.map(|state| state.lpindex),
|
||||
Body::FishMedium(_) => self
|
||||
.fish_medium_states
|
||||
.get(&entity)
|
||||
.map(|state| state.lpindex),
|
||||
Body::Dragon(_) => {
|
||||
self.dragon_states.get(&entity).map(|state| state.lpindex)
|
||||
}
|
||||
Body::BirdSmall(_) => self
|
||||
.bird_small_states
|
||||
.get(&entity)
|
||||
.map(|state| state.lpindex),
|
||||
Body::FishSmall(_) => self
|
||||
.fish_small_states
|
||||
.get(&entity)
|
||||
.map(|state| state.lpindex),
|
||||
Body::BipedLarge(_) => self
|
||||
.biped_large_states
|
||||
.get(&entity)
|
||||
.map(|state| state.lpindex),
|
||||
Body::Object(_) => {
|
||||
self.object_states.get(&entity).map(|state| state.lpindex)
|
||||
}
|
||||
}
|
||||
.unwrap_or(0),
|
||||
);
|
||||
|
||||
if !in_frustum {
|
||||
match body {
|
||||
Body::Humanoid(_) => {
|
||||
self.character_states.get_mut(&entity).map(|state| {
|
||||
state.lpindex = lpindex;
|
||||
state.visible = false
|
||||
});
|
||||
}
|
||||
Body::QuadrupedSmall(_) => {
|
||||
self.quadruped_small_states.get_mut(&entity).map(|state| {
|
||||
state.lpindex = lpindex;
|
||||
state.visible = false
|
||||
});
|
||||
}
|
||||
Body::QuadrupedMedium(_) => {
|
||||
self.quadruped_medium_states.get_mut(&entity).map(|state| {
|
||||
state.lpindex = lpindex;
|
||||
state.visible = false
|
||||
});
|
||||
}
|
||||
Body::BirdMedium(_) => {
|
||||
self.bird_medium_states.get_mut(&entity).map(|state| {
|
||||
state.lpindex = lpindex;
|
||||
state.visible = false
|
||||
});
|
||||
}
|
||||
Body::FishMedium(_) => {
|
||||
self.fish_medium_states.get_mut(&entity).map(|state| {
|
||||
state.lpindex = lpindex;
|
||||
state.visible = false
|
||||
});
|
||||
}
|
||||
Body::Dragon(_) => {
|
||||
self.dragon_states.get_mut(&entity).map(|state| {
|
||||
state.lpindex = lpindex;
|
||||
state.visible = false
|
||||
});
|
||||
}
|
||||
Body::BirdSmall(_) => {
|
||||
self.bird_small_states.get_mut(&entity).map(|state| {
|
||||
state.lpindex = lpindex;
|
||||
state.visible = false
|
||||
});
|
||||
}
|
||||
Body::FishSmall(_) => {
|
||||
self.fish_small_states.get_mut(&entity).map(|state| {
|
||||
state.lpindex = lpindex;
|
||||
state.visible = false
|
||||
});
|
||||
}
|
||||
Body::BipedLarge(_) => {
|
||||
self.biped_large_states.get_mut(&entity).map(|state| {
|
||||
state.lpindex = lpindex;
|
||||
state.visible = false
|
||||
});
|
||||
}
|
||||
Body::Object(_) => {
|
||||
self.object_states.get_mut(&entity).map(|state| {
|
||||
state.lpindex = lpindex;
|
||||
state.visible = false
|
||||
});
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -294,7 +462,7 @@ impl FigureMgr {
|
||||
),
|
||||
(_, Dodge(_)) => anim::character::RollAnimation::update_skeleton(
|
||||
&target_base,
|
||||
(active_tool_kind, time),
|
||||
(active_tool_kind, ori.0, state.last_ori, time),
|
||||
state.action_time,
|
||||
&mut action_animation_rate,
|
||||
skeleton_attr,
|
||||
@ -320,6 +488,8 @@ impl FigureMgr {
|
||||
dt,
|
||||
move_state_animation_rate,
|
||||
action_animation_rate,
|
||||
lpindex,
|
||||
true,
|
||||
);
|
||||
}
|
||||
Body::QuadrupedSmall(_) => {
|
||||
@ -377,6 +547,8 @@ impl FigureMgr {
|
||||
dt,
|
||||
move_state_animation_rate,
|
||||
action_animation_rate,
|
||||
lpindex,
|
||||
true,
|
||||
);
|
||||
}
|
||||
Body::QuadrupedMedium(_) => {
|
||||
@ -434,6 +606,8 @@ impl FigureMgr {
|
||||
dt,
|
||||
move_state_animation_rate,
|
||||
action_animation_rate,
|
||||
lpindex,
|
||||
true,
|
||||
);
|
||||
}
|
||||
Body::BirdMedium(_) => {
|
||||
@ -489,6 +663,8 @@ impl FigureMgr {
|
||||
dt,
|
||||
move_state_animation_rate,
|
||||
action_animation_rate,
|
||||
lpindex,
|
||||
true,
|
||||
);
|
||||
}
|
||||
Body::FishMedium(_) => {
|
||||
@ -544,6 +720,8 @@ impl FigureMgr {
|
||||
dt,
|
||||
move_state_animation_rate,
|
||||
action_animation_rate,
|
||||
lpindex,
|
||||
true,
|
||||
);
|
||||
}
|
||||
Body::Dragon(_) => {
|
||||
@ -599,6 +777,8 @@ impl FigureMgr {
|
||||
dt,
|
||||
move_state_animation_rate,
|
||||
action_animation_rate,
|
||||
lpindex,
|
||||
true,
|
||||
);
|
||||
}
|
||||
Body::BirdSmall(_) => {
|
||||
@ -654,6 +834,8 @@ impl FigureMgr {
|
||||
dt,
|
||||
move_state_animation_rate,
|
||||
action_animation_rate,
|
||||
lpindex,
|
||||
true,
|
||||
);
|
||||
}
|
||||
Body::FishSmall(_) => {
|
||||
@ -709,6 +891,8 @@ impl FigureMgr {
|
||||
dt,
|
||||
move_state_animation_rate,
|
||||
action_animation_rate,
|
||||
lpindex,
|
||||
true,
|
||||
);
|
||||
}
|
||||
Body::BipedLarge(_) => {
|
||||
@ -764,6 +948,8 @@ impl FigureMgr {
|
||||
dt,
|
||||
move_state_animation_rate,
|
||||
action_animation_rate,
|
||||
lpindex,
|
||||
true,
|
||||
);
|
||||
}
|
||||
Body::Object(_) => {
|
||||
@ -783,6 +969,8 @@ impl FigureMgr {
|
||||
dt,
|
||||
move_state_animation_rate,
|
||||
action_animation_rate,
|
||||
lpindex,
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -823,8 +1011,6 @@ impl FigureMgr {
|
||||
let tick = client.get_tick();
|
||||
let ecs = client.state().ecs();
|
||||
|
||||
let frustum = camera.frustum(client);
|
||||
|
||||
let character_state_storage = client
|
||||
.state()
|
||||
.read_storage::<common::comp::CharacterState>();
|
||||
@ -839,60 +1025,55 @@ impl FigureMgr {
|
||||
ecs.read_storage::<Scale>().maybe(),
|
||||
)
|
||||
.join()
|
||||
// Don't render figures outside of frustum (camera viewport, max draw distance is farplane)
|
||||
.filter(|(_, pos, _, _, _, scale)| {
|
||||
frustum.sphere_intersecting(
|
||||
&pos.0.x,
|
||||
&pos.0.y,
|
||||
&pos.0.z,
|
||||
&(scale.unwrap_or(&Scale(1.0)).0 * 2.0),
|
||||
)
|
||||
})
|
||||
// Don't render dead entities
|
||||
.filter(|(_, _, _, _, stats, _)| stats.map_or(true, |s| !s.is_dead))
|
||||
{
|
||||
if let Some((locals, bone_consts)) = match body {
|
||||
if let Some((locals, bone_consts, visible)) = match body {
|
||||
Body::Humanoid(_) => self
|
||||
.character_states
|
||||
.get(&entity)
|
||||
.map(|state| (state.locals(), state.bone_consts())),
|
||||
.map(|state| (state.locals(), state.bone_consts(), state.visible)),
|
||||
Body::QuadrupedSmall(_) => self
|
||||
.quadruped_small_states
|
||||
.get(&entity)
|
||||
.map(|state| (state.locals(), state.bone_consts())),
|
||||
.map(|state| (state.locals(), state.bone_consts(), state.visible)),
|
||||
Body::QuadrupedMedium(_) => self
|
||||
.quadruped_medium_states
|
||||
.get(&entity)
|
||||
.map(|state| (state.locals(), state.bone_consts())),
|
||||
.map(|state| (state.locals(), state.bone_consts(), state.visible)),
|
||||
Body::BirdMedium(_) => self
|
||||
.bird_medium_states
|
||||
.get(&entity)
|
||||
.map(|state| (state.locals(), state.bone_consts())),
|
||||
.map(|state| (state.locals(), state.bone_consts(), state.visible)),
|
||||
Body::FishMedium(_) => self
|
||||
.fish_medium_states
|
||||
.get(&entity)
|
||||
.map(|state| (state.locals(), state.bone_consts())),
|
||||
.map(|state| (state.locals(), state.bone_consts(), state.visible)),
|
||||
Body::Dragon(_) => self
|
||||
.dragon_states
|
||||
.get(&entity)
|
||||
.map(|state| (state.locals(), state.bone_consts())),
|
||||
.map(|state| (state.locals(), state.bone_consts(), state.visible)),
|
||||
Body::BirdSmall(_) => self
|
||||
.bird_small_states
|
||||
.get(&entity)
|
||||
.map(|state| (state.locals(), state.bone_consts())),
|
||||
.map(|state| (state.locals(), state.bone_consts(), state.visible)),
|
||||
Body::FishSmall(_) => self
|
||||
.fish_small_states
|
||||
.get(&entity)
|
||||
.map(|state| (state.locals(), state.bone_consts())),
|
||||
.map(|state| (state.locals(), state.bone_consts(), state.visible)),
|
||||
Body::BipedLarge(_) => self
|
||||
.biped_large_states
|
||||
.get(&entity)
|
||||
.map(|state| (state.locals(), state.bone_consts())),
|
||||
.map(|state| (state.locals(), state.bone_consts(), state.visible)),
|
||||
Body::Object(_) => self
|
||||
.object_states
|
||||
.get(&entity)
|
||||
.map(|state| (state.locals(), state.bone_consts())),
|
||||
.map(|state| (state.locals(), state.bone_consts(), state.visible)),
|
||||
} {
|
||||
if !visible {
|
||||
continue;
|
||||
}
|
||||
|
||||
let is_player = entity == client.entity();
|
||||
|
||||
let player_camera_mode = if is_player {
|
||||
@ -919,6 +1100,64 @@ impl FigureMgr {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn figure_count(&self) -> usize {
|
||||
self.character_states.len()
|
||||
+ self.quadruped_small_states.len()
|
||||
+ self.character_states.len()
|
||||
+ self.quadruped_medium_states.len()
|
||||
+ self.bird_medium_states.len()
|
||||
+ self.fish_medium_states.len()
|
||||
+ self.dragon_states.len()
|
||||
+ self.bird_small_states.len()
|
||||
+ self.fish_small_states.len()
|
||||
+ self.biped_large_states.len()
|
||||
+ self.object_states.len()
|
||||
}
|
||||
|
||||
pub fn figure_count_visible(&self) -> usize {
|
||||
self.character_states
|
||||
.iter()
|
||||
.filter(|(_, c)| c.visible)
|
||||
.count()
|
||||
+ self
|
||||
.quadruped_small_states
|
||||
.iter()
|
||||
.filter(|(_, c)| c.visible)
|
||||
.count()
|
||||
+ self
|
||||
.quadruped_medium_states
|
||||
.iter()
|
||||
.filter(|(_, c)| c.visible)
|
||||
.count()
|
||||
+ self
|
||||
.bird_medium_states
|
||||
.iter()
|
||||
.filter(|(_, c)| c.visible)
|
||||
.count()
|
||||
+ self
|
||||
.fish_medium_states
|
||||
.iter()
|
||||
.filter(|(_, c)| c.visible)
|
||||
.count()
|
||||
+ self.dragon_states.iter().filter(|(_, c)| c.visible).count()
|
||||
+ self
|
||||
.bird_small_states
|
||||
.iter()
|
||||
.filter(|(_, c)| c.visible)
|
||||
.count()
|
||||
+ self
|
||||
.fish_small_states
|
||||
.iter()
|
||||
.filter(|(_, c)| c.visible)
|
||||
.count()
|
||||
+ self
|
||||
.biped_large_states
|
||||
.iter()
|
||||
.filter(|(_, c)| c.visible)
|
||||
.count()
|
||||
+ self.object_states.iter().filter(|(_, c)| c.visible).count()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FigureState<S: Skeleton> {
|
||||
@ -930,6 +1169,8 @@ pub struct FigureState<S: Skeleton> {
|
||||
pos: Vec3<f32>,
|
||||
ori: Vec3<f32>,
|
||||
last_ori: Vec3<f32>,
|
||||
lpindex: u8,
|
||||
visible: bool,
|
||||
}
|
||||
|
||||
impl<S: Skeleton> FigureState<S> {
|
||||
@ -945,6 +1186,8 @@ impl<S: Skeleton> FigureState<S> {
|
||||
pos: Vec3::zero(),
|
||||
ori: Vec3::zero(),
|
||||
last_ori: Vec3::zero(),
|
||||
lpindex: 0,
|
||||
visible: false,
|
||||
}
|
||||
}
|
||||
|
||||
@ -959,10 +1202,15 @@ impl<S: Skeleton> FigureState<S> {
|
||||
dt: f32,
|
||||
move_state_rate: f32,
|
||||
action_rate: f32,
|
||||
lpindex: u8,
|
||||
visible: bool,
|
||||
) {
|
||||
self.visible = visible;
|
||||
self.lpindex = lpindex;
|
||||
self.last_ori = Lerp::lerp(self.last_ori, ori, 15.0 * dt);
|
||||
|
||||
// Update interpolation values
|
||||
// TODO: use values from Interpolated component instead of recalculating
|
||||
if self.pos.distance_squared(pos) < 64.0 * 64.0 {
|
||||
self.pos = Lerp::lerp(self.pos, pos + vel * 0.03, 10.0 * dt);
|
||||
self.ori = Slerp::slerp(self.ori, ori, 5.0 * dt);
|
||||
@ -974,6 +1222,7 @@ impl<S: Skeleton> FigureState<S> {
|
||||
self.move_state_time += (dt * move_state_rate) as f64;
|
||||
self.action_time += (dt * action_rate) as f64;
|
||||
|
||||
// TODO: what are the interpolated ori values used for if not here???
|
||||
let mat = Mat4::<f32>::identity()
|
||||
* Mat4::translation_3d(self.pos)
|
||||
* Mat4::rotation_z(-ori.x.atan2(ori.y))
|
||||
|
@ -22,7 +22,7 @@ use common::{
|
||||
terrain::{BlockKind, TerrainChunk},
|
||||
vol::ReadVol,
|
||||
};
|
||||
use specs::Join;
|
||||
use specs::{Join, WorldExt};
|
||||
use vek::*;
|
||||
|
||||
// TODO: Don't hard-code this.
|
||||
@ -94,7 +94,7 @@ impl Scene {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference to the scene's globals
|
||||
/// Get a reference to the scene's globals.
|
||||
pub fn globals(&self) -> &Consts<Globals> {
|
||||
&self.globals
|
||||
}
|
||||
@ -104,6 +104,16 @@ impl Scene {
|
||||
&self.camera
|
||||
}
|
||||
|
||||
/// Get a reference to the scene's terrain.
|
||||
pub fn terrain(&self) -> &Terrain<TerrainChunk> {
|
||||
&self.terrain
|
||||
}
|
||||
|
||||
/// Get a reference to the scene's figure manager.
|
||||
pub fn figure_mgr(&self) -> &FigureMgr {
|
||||
&self.figure_mgr
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the scene's camera.
|
||||
pub fn camera_mut(&mut self) -> &mut Camera {
|
||||
&mut self.camera
|
||||
@ -205,23 +215,31 @@ impl Scene {
|
||||
let mut lights = (
|
||||
&client.state().ecs().read_storage::<comp::Pos>(),
|
||||
client.state().ecs().read_storage::<comp::Ori>().maybe(),
|
||||
client
|
||||
.state()
|
||||
.ecs()
|
||||
.read_storage::<crate::ecs::comp::Interpolated>()
|
||||
.maybe(),
|
||||
&client.state().ecs().read_storage::<comp::LightEmitter>(),
|
||||
)
|
||||
.join()
|
||||
.filter(|(pos, _, _)| {
|
||||
.filter(|(pos, _, _, _)| {
|
||||
(pos.0.distance_squared(player_pos) as f32)
|
||||
< self.loaded_distance.powf(2.0) + LIGHT_DIST_RADIUS
|
||||
})
|
||||
.map(|(pos, ori, light_emitter)| {
|
||||
.map(|(pos, ori, interpolated, light_emitter)| {
|
||||
// Use interpolated values if they are available
|
||||
let (pos, ori) =
|
||||
interpolated.map_or((pos.0, ori.map(|o| o.0)), |i| (i.pos, Some(i.ori)));
|
||||
let rot = {
|
||||
if let Some(o) = ori {
|
||||
Mat3::rotation_z(-o.0.x.atan2(o.0.y))
|
||||
Mat3::rotation_z(-o.x.atan2(o.y))
|
||||
} else {
|
||||
Mat3::identity()
|
||||
}
|
||||
};
|
||||
Light::new(
|
||||
pos.0 + (rot * light_emitter.offset),
|
||||
pos + (rot * light_emitter.offset),
|
||||
light_emitter.col,
|
||||
light_emitter.strength,
|
||||
)
|
||||
@ -236,17 +254,28 @@ impl Scene {
|
||||
// Update shadow constants
|
||||
let mut shadows = (
|
||||
&client.state().ecs().read_storage::<comp::Pos>(),
|
||||
client
|
||||
.state()
|
||||
.ecs()
|
||||
.read_storage::<crate::ecs::comp::Interpolated>()
|
||||
.maybe(),
|
||||
client.state().ecs().read_storage::<comp::Scale>().maybe(),
|
||||
&client.state().ecs().read_storage::<comp::Body>(),
|
||||
&client.state().ecs().read_storage::<comp::Stats>(),
|
||||
)
|
||||
.join()
|
||||
.filter(|(_, _, _, stats)| !stats.is_dead)
|
||||
.filter(|(pos, _, _, _)| {
|
||||
.filter(|(_, _, _, _, stats)| !stats.is_dead)
|
||||
.filter(|(pos, _, _, _, _)| {
|
||||
(pos.0.distance_squared(player_pos) as f32)
|
||||
< (self.loaded_distance.min(SHADOW_MAX_DIST) + SHADOW_DIST_RADIUS).powf(2.0)
|
||||
})
|
||||
.map(|(pos, scale, _, _)| Shadow::new(pos.0, scale.map(|s| s.0).unwrap_or(1.0)))
|
||||
.map(|(pos, interpolated, scale, _, _)| {
|
||||
Shadow::new(
|
||||
// Use interpolated values pos if it is available
|
||||
interpolated.map_or(pos.0, |i| i.pos),
|
||||
scale.map_or(1.0, |s| s.0),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
shadows.sort_by_key(|shadow| shadow.get_pos().distance_squared(player_pos) as i32);
|
||||
shadows.truncate(MAX_SHADOW_COUNT);
|
||||
@ -291,7 +320,7 @@ impl Scene {
|
||||
);
|
||||
|
||||
// Maintain the figures.
|
||||
self.figure_mgr.maintain(renderer, client);
|
||||
self.figure_mgr.maintain(renderer, client, &self.camera);
|
||||
|
||||
// Remove unused figures.
|
||||
self.figure_mgr.clean(client.get_tick());
|
||||
|
@ -16,9 +16,9 @@ use common::{
|
||||
};
|
||||
use crossbeam::channel;
|
||||
use dot_vox::DotVoxData;
|
||||
use frustum_query::frustum::Frustum;
|
||||
use hashbrown::HashMap;
|
||||
use std::{f32, fmt::Debug, i32, marker::PhantomData, ops::Mul, time::Duration};
|
||||
use hashbrown::{hash_map::Entry, HashMap};
|
||||
use std::{f32, fmt::Debug, i32, marker::PhantomData, time::Duration};
|
||||
use treeculler::{BVol, Frustum, AABB};
|
||||
use vek::*;
|
||||
|
||||
struct TerrainChunkData {
|
||||
@ -31,6 +31,7 @@ struct TerrainChunkData {
|
||||
|
||||
visible: bool,
|
||||
z_bounds: (f32, f32),
|
||||
frustum_last_plane_index: u8,
|
||||
}
|
||||
|
||||
struct ChunkMeshState {
|
||||
@ -836,31 +837,48 @@ impl<V: RectRasterableVol> Terrain<V> {
|
||||
.map(|(p, _)| *p)
|
||||
{
|
||||
let chunk_pos = client.state().terrain().pos_key(pos);
|
||||
let new_mesh_state = ChunkMeshState {
|
||||
pos: chunk_pos,
|
||||
started_tick: current_tick,
|
||||
active_worker: None,
|
||||
};
|
||||
// Only mesh if this chunk has all its neighbors
|
||||
// If it does have all its neighbors either it should have already been meshed or is in
|
||||
// mesh_todo
|
||||
match self.mesh_todo.entry(chunk_pos) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
entry.insert(new_mesh_state);
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
if self.chunks.contains_key(&chunk_pos) {
|
||||
entry.insert(new_mesh_state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.mesh_todo.insert(
|
||||
chunk_pos,
|
||||
ChunkMeshState {
|
||||
pos: chunk_pos,
|
||||
started_tick: current_tick,
|
||||
active_worker: None,
|
||||
},
|
||||
);
|
||||
|
||||
// Handle chunks on chunk borders
|
||||
// Handle block changes on chunk borders
|
||||
for x in -1..2 {
|
||||
for y in -1..2 {
|
||||
let neighbour_pos = pos + Vec3::new(x, y, 0);
|
||||
let neighbour_chunk_pos = client.state().terrain().pos_key(neighbour_pos);
|
||||
|
||||
if neighbour_chunk_pos != chunk_pos {
|
||||
self.mesh_todo.insert(
|
||||
neighbour_chunk_pos,
|
||||
ChunkMeshState {
|
||||
pos: neighbour_chunk_pos,
|
||||
started_tick: current_tick,
|
||||
active_worker: None,
|
||||
},
|
||||
);
|
||||
let new_mesh_state = ChunkMeshState {
|
||||
pos: neighbour_chunk_pos,
|
||||
started_tick: current_tick,
|
||||
active_worker: None,
|
||||
};
|
||||
// Only mesh if this chunk has all its neighbors
|
||||
match self.mesh_todo.entry(neighbour_chunk_pos) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
entry.insert(new_mesh_state);
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
if self.chunks.contains_key(&neighbour_chunk_pos) {
|
||||
entry.insert(new_mesh_state);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Remesh all neighbours because we have complex lighting now
|
||||
@ -1004,6 +1022,7 @@ impl<V: RectRasterableVol> Terrain<V> {
|
||||
.expect("Failed to upload chunk locals to the GPU!"),
|
||||
visible: false,
|
||||
z_bounds: response.z_bounds,
|
||||
frustum_last_plane_index: 0,
|
||||
},
|
||||
);
|
||||
|
||||
@ -1018,10 +1037,7 @@ impl<V: RectRasterableVol> Terrain<V> {
|
||||
}
|
||||
|
||||
// Construct view frustum
|
||||
let frustum = Frustum::from_modelview_and_projection(
|
||||
&view_mat.into_col_array(),
|
||||
&proj_mat.into_col_array(),
|
||||
);
|
||||
let frustum = Frustum::from_modelview_projection((proj_mat * view_mat).into_col_arrays());
|
||||
|
||||
// Update chunk visibility
|
||||
let chunk_sz = V::RECT_SIZE.x as f32;
|
||||
@ -1033,28 +1049,35 @@ impl<V: RectRasterableVol> Terrain<V> {
|
||||
let in_range = Vec2::<f32>::from(focus_pos).distance_squared(nearest_in_chunk)
|
||||
< loaded_distance.powf(2.0);
|
||||
|
||||
// Ensure the chunk is within the view frustrum
|
||||
let chunk_mid = Vec3::new(
|
||||
chunk_pos.x + chunk_sz / 2.0,
|
||||
chunk_pos.y + chunk_sz / 2.0,
|
||||
(chunk.z_bounds.0 + chunk.z_bounds.1) * 0.5,
|
||||
);
|
||||
let chunk_radius = ((chunk.z_bounds.1 - chunk.z_bounds.0) / 2.0)
|
||||
.max(chunk_sz / 2.0)
|
||||
.powf(2.0)
|
||||
.mul(2.0)
|
||||
.sqrt();
|
||||
let in_frustum = frustum.sphere_intersecting(
|
||||
&chunk_mid.x,
|
||||
&chunk_mid.y,
|
||||
&chunk_mid.z,
|
||||
&chunk_radius,
|
||||
);
|
||||
if !in_range {
|
||||
chunk.visible = in_range;
|
||||
continue;
|
||||
}
|
||||
|
||||
chunk.visible = in_range && in_frustum;
|
||||
// Ensure the chunk is within the view frustum
|
||||
let chunk_min = [chunk_pos.x, chunk_pos.y, chunk.z_bounds.0];
|
||||
let chunk_max = [
|
||||
chunk_pos.x + chunk_sz,
|
||||
chunk_pos.y + chunk_sz,
|
||||
chunk.z_bounds.1,
|
||||
];
|
||||
|
||||
let (in_frustum, last_plane_index) = AABB::new(chunk_min, chunk_max)
|
||||
.coherent_test_against_frustum(&frustum, chunk.frustum_last_plane_index);
|
||||
|
||||
chunk.frustum_last_plane_index = last_plane_index;
|
||||
chunk.visible = in_frustum;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn chunk_count(&self) -> usize {
|
||||
self.chunks.len()
|
||||
}
|
||||
|
||||
pub fn visible_chunk_count(&self) -> usize {
|
||||
self.chunks.iter().filter(|(_, c)| c.visible).count()
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&self,
|
||||
renderer: &mut Renderer,
|
||||
|
@ -1,4 +1,5 @@
|
||||
use crate::{
|
||||
ecs::MyEntity,
|
||||
hud::{DebugInfo, Event as HudEvent, Hud},
|
||||
key_state::KeyState,
|
||||
render::Renderer,
|
||||
@ -17,7 +18,7 @@ use common::{
|
||||
ChatType,
|
||||
};
|
||||
use log::error;
|
||||
use specs::Join;
|
||||
use specs::{Join, WorldExt};
|
||||
use std::{cell::RefCell, rc::Rc, time::Duration};
|
||||
use vek::*;
|
||||
|
||||
@ -40,6 +41,14 @@ impl SessionState {
|
||||
.camera_mut()
|
||||
.set_fov_deg(global_state.settings.graphics.fov);
|
||||
let hud = Hud::new(global_state, &client.borrow());
|
||||
{
|
||||
let my_entity = client.borrow().entity();
|
||||
client
|
||||
.borrow_mut()
|
||||
.state_mut()
|
||||
.ecs_mut()
|
||||
.insert(MyEntity(my_entity));
|
||||
}
|
||||
Self {
|
||||
scene,
|
||||
client,
|
||||
@ -55,7 +64,11 @@ impl SessionState {
|
||||
/// Tick the session (and the client attached to it).
|
||||
fn tick(&mut self, dt: Duration) -> Result<(), Error> {
|
||||
self.inputs.tick(dt);
|
||||
for event in self.client.borrow_mut().tick(self.inputs.clone(), dt)? {
|
||||
for event in self.client.borrow_mut().tick(
|
||||
self.inputs.clone(),
|
||||
dt,
|
||||
crate::ecs::sys::add_local_systems,
|
||||
)? {
|
||||
match event {
|
||||
Chat {
|
||||
chat_type: _,
|
||||
@ -121,9 +134,7 @@ impl PlayState for SessionState {
|
||||
|
||||
// Game loop
|
||||
let mut current_client_state = self.client.borrow().get_client_state();
|
||||
while let ClientState::Pending | ClientState::Character | ClientState::Dead =
|
||||
current_client_state
|
||||
{
|
||||
while let ClientState::Pending | ClientState::Character = current_client_state {
|
||||
// Compute camera data
|
||||
let (view_mat, _, cam_pos) = self
|
||||
.scene
|
||||
@ -388,8 +399,13 @@ impl PlayState for SessionState {
|
||||
.read_storage::<Vel>()
|
||||
.get(self.client.borrow().entity())
|
||||
.cloned(),
|
||||
num_chunks: self.scene.terrain().chunk_count() as u32,
|
||||
num_visible_chunks: self.scene.terrain().visible_chunk_count() as u32,
|
||||
num_figures: self.scene.figure_mgr().figure_count() as u32,
|
||||
num_figures_visible: self.scene.figure_mgr().figure_count_visible() as u32,
|
||||
},
|
||||
&self.scene.camera(),
|
||||
clock.get_last_delta(),
|
||||
);
|
||||
|
||||
// Maintain the UI.
|
||||
@ -421,6 +437,22 @@ impl PlayState for SessionState {
|
||||
global_state.settings.gameplay.zoom_inversion = zoom_inverted;
|
||||
global_state.settings.save_to_file_warn();
|
||||
}
|
||||
HudEvent::Sct(sct) => {
|
||||
global_state.settings.gameplay.sct = sct;
|
||||
global_state.settings.save_to_file_warn();
|
||||
}
|
||||
HudEvent::SctPlayerBatch(sct_player_batch) => {
|
||||
global_state.settings.gameplay.sct_player_batch = sct_player_batch;
|
||||
global_state.settings.save_to_file_warn();
|
||||
}
|
||||
HudEvent::SctDamageBatch(sct_damage_batch) => {
|
||||
global_state.settings.gameplay.sct_damage_batch = sct_damage_batch;
|
||||
global_state.settings.save_to_file_warn();
|
||||
}
|
||||
HudEvent::ToggleDebug(toggle_debug) => {
|
||||
global_state.settings.gameplay.toggle_debug = toggle_debug;
|
||||
global_state.settings.save_to_file_warn();
|
||||
}
|
||||
HudEvent::ToggleMouseYInvert(mouse_y_inverted) => {
|
||||
global_state.window.mouse_y_inversion = mouse_y_inverted;
|
||||
global_state.settings.gameplay.mouse_y_inversion = mouse_y_inverted;
|
||||
|
@ -85,8 +85,8 @@ impl Default for ControlSettings {
|
||||
screenshot: KeyMouse::Key(VirtualKeyCode::F4),
|
||||
toggle_ingame_ui: KeyMouse::Key(VirtualKeyCode::F6),
|
||||
roll: KeyMouse::Mouse(MouseButton::Middle),
|
||||
respawn: KeyMouse::Mouse(MouseButton::Left),
|
||||
interact: KeyMouse::Key(VirtualKeyCode::E),
|
||||
respawn: KeyMouse::Key(VirtualKeyCode::Space),
|
||||
interact: KeyMouse::Mouse(MouseButton::Right),
|
||||
toggle_wield: KeyMouse::Key(VirtualKeyCode::T),
|
||||
charge: KeyMouse::Key(VirtualKeyCode::V),
|
||||
}
|
||||
@ -100,6 +100,10 @@ pub struct GameplaySettings {
|
||||
pub pan_sensitivity: u32,
|
||||
pub zoom_sensitivity: u32,
|
||||
pub zoom_inversion: bool,
|
||||
pub toggle_debug: bool,
|
||||
pub sct: bool,
|
||||
pub sct_player_batch: bool,
|
||||
pub sct_damage_batch: bool,
|
||||
pub mouse_y_inversion: bool,
|
||||
pub crosshair_transp: f32,
|
||||
pub chat_transp: f32,
|
||||
@ -118,6 +122,10 @@ impl Default for GameplaySettings {
|
||||
zoom_sensitivity: 100,
|
||||
zoom_inversion: false,
|
||||
mouse_y_inversion: false,
|
||||
toggle_debug: false,
|
||||
sct: true,
|
||||
sct_player_batch: true,
|
||||
sct_damage_batch: false,
|
||||
crosshair_transp: 0.6,
|
||||
chat_transp: 0.4,
|
||||
crosshair_type: CrosshairType::Round,
|
||||
@ -151,18 +159,18 @@ impl Default for NetworkingSettings {
|
||||
}
|
||||
}
|
||||
|
||||
/// `Log` stores the name to the log file.
|
||||
/// `Log` stores whether we should create a log file
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct Log {
|
||||
pub file: PathBuf,
|
||||
// Whether to create a log file or not.
|
||||
// Default is to create one.
|
||||
pub log_to_file: bool,
|
||||
}
|
||||
|
||||
impl Default for Log {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
file: "voxygen.log".into(),
|
||||
}
|
||||
Self { log_to_file: true }
|
||||
}
|
||||
}
|
||||
|
||||
@ -179,9 +187,9 @@ pub struct GraphicsSettings {
|
||||
impl Default for GraphicsSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
view_distance: 5,
|
||||
view_distance: 10,
|
||||
max_fps: 60,
|
||||
fov: 75,
|
||||
fov: 50,
|
||||
aa_mode: AaMode::Fxaa,
|
||||
}
|
||||
}
|
||||
@ -290,7 +298,7 @@ impl Settings {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_settings_path() -> PathBuf {
|
||||
pub fn get_settings_path() -> PathBuf {
|
||||
if let Some(val) = std::env::var_os("VOXYGEN_CONFIG") {
|
||||
let settings = PathBuf::from(val).join("settings.ron");
|
||||
if settings.exists() || settings.parent().map(|x| x.exists()).unwrap_or(false) {
|
||||
|
@ -8,9 +8,6 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
#[cfg(feature = "discord")]
|
||||
use crate::{discord, discord::DiscordUpdate};
|
||||
|
||||
const TPS: u64 = 30;
|
||||
|
||||
enum Msg {
|
||||
@ -68,14 +65,6 @@ fn run_server(mut server: Server, rec: Receiver<Msg>) {
|
||||
// Set up an fps clock
|
||||
let mut clock = Clock::start();
|
||||
|
||||
#[cfg(feature = "discord")]
|
||||
{
|
||||
discord::send_all(vec![
|
||||
DiscordUpdate::Details("Singleplayer".into()),
|
||||
DiscordUpdate::State("Playing...".into()),
|
||||
]);
|
||||
}
|
||||
|
||||
loop {
|
||||
let events = server
|
||||
.tick(Input::default(), clock.get_last_delta())
|
||||
|
@ -305,7 +305,7 @@ impl Ui {
|
||||
enum Placement {
|
||||
Interface,
|
||||
// Number of primitives left to render ingame and relative scaling/resolution
|
||||
InWorld(usize, Option<f32>),
|
||||
InWorld(usize, Option<(f64, f64)>),
|
||||
};
|
||||
|
||||
let mut placement = Placement::Interface;
|
||||
@ -342,7 +342,7 @@ impl Ui {
|
||||
// moving origin to top-left corner (from middle).
|
||||
let min_x = self.ui.win_w / 2.0 + l;
|
||||
let min_y = self.ui.win_h / 2.0 - b - h;
|
||||
Aabr {
|
||||
let intersection = Aabr {
|
||||
min: Vec2 {
|
||||
x: (min_x * scale_factor) as u16,
|
||||
y: (min_y * scale_factor) as u16,
|
||||
@ -352,7 +352,13 @@ impl Ui {
|
||||
y: ((min_y + h) * scale_factor) as u16,
|
||||
},
|
||||
}
|
||||
.intersection(window_scissor)
|
||||
.intersection(window_scissor);
|
||||
|
||||
if intersection.is_valid() {
|
||||
intersection
|
||||
} else {
|
||||
Aabr::new_empty(Vec2::zero())
|
||||
}
|
||||
};
|
||||
if new_scissor != current_scissor {
|
||||
// Finish the current command.
|
||||
@ -393,7 +399,7 @@ impl Ui {
|
||||
|
||||
// Functions for converting for conrod scalar coords to GL vertex coords (-1.0 to 1.0).
|
||||
let (ui_win_w, ui_win_h) = match placement {
|
||||
Placement::InWorld(_, Some(res)) => (res as f64, res as f64),
|
||||
Placement::InWorld(_, Some(res)) => res,
|
||||
// Behind the camera or far away
|
||||
Placement::InWorld(_, None) => continue,
|
||||
Placement::Interface => (self.ui.win_w, self.ui.win_h),
|
||||
@ -618,10 +624,12 @@ impl Ui {
|
||||
.parameters;
|
||||
|
||||
let pos_in_view = view_mat * Vec4::from_point(parameters.pos);
|
||||
|
||||
let scale_factor = self.ui.win_w as f64
|
||||
/ (-2.0
|
||||
* pos_in_view.z as f64
|
||||
* (0.5 * fov as f64).tan()
|
||||
// TODO: make this have no effect for fixed scale
|
||||
* parameters.res as f64);
|
||||
// Don't process ingame elements behind the camera or very far away
|
||||
placement = if scale_factor > 0.2 {
|
||||
@ -636,32 +644,43 @@ impl Ui {
|
||||
});
|
||||
start = mesh.vertices().len();
|
||||
// Push new position command
|
||||
let mut world_pos = Vec4::from_point(parameters.pos);
|
||||
if parameters.fixed_scale {
|
||||
world_pos.w = -1.0
|
||||
};
|
||||
|
||||
if self.ingame_locals.len() > ingame_local_index {
|
||||
renderer
|
||||
.update_consts(
|
||||
&mut self.ingame_locals[ingame_local_index],
|
||||
&[parameters.pos.into()],
|
||||
&[world_pos.into()],
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
self.ingame_locals.push(
|
||||
renderer.create_consts(&[parameters.pos.into()]).unwrap(),
|
||||
);
|
||||
self.ingame_locals
|
||||
.push(renderer.create_consts(&[world_pos.into()]).unwrap());
|
||||
}
|
||||
self.draw_commands
|
||||
.push(DrawCommand::WorldPos(Some(ingame_local_index)));
|
||||
ingame_local_index += 1;
|
||||
|
||||
p_scale_factor = ((scale_factor * 10.0).log2().round().powi(2)
|
||||
/ 10.0)
|
||||
.min(1.6)
|
||||
.max(0.2);
|
||||
p_scale_factor = if parameters.fixed_scale {
|
||||
self.scale.scale_factor_physical()
|
||||
} else {
|
||||
((scale_factor * 10.0).log2().round().powi(2) / 10.0)
|
||||
.min(1.6)
|
||||
.max(0.2)
|
||||
};
|
||||
|
||||
// Scale down ingame elements that are close to the camera
|
||||
let res = if scale_factor > 3.2 {
|
||||
parameters.res * scale_factor as f32 / 3.2
|
||||
let res = if parameters.fixed_scale {
|
||||
(self.ui.win_w, self.ui.win_h)
|
||||
} else if scale_factor > 3.2 {
|
||||
let res = parameters.res * scale_factor as f32 / 3.2;
|
||||
(res as f64, res as f64)
|
||||
} else {
|
||||
parameters.res
|
||||
let res = parameters.res;
|
||||
(res as f64, res as f64)
|
||||
};
|
||||
|
||||
Placement::InWorld(parameters.num, Some(res))
|
||||
@ -750,7 +769,7 @@ impl Ui {
|
||||
}
|
||||
|
||||
fn default_scissor(renderer: &Renderer) -> Aabr<u16> {
|
||||
let (screen_w, screen_h) = renderer.get_resolution().map(|e| e as u16).into_tuple();
|
||||
let (screen_w, screen_h) = renderer.get_resolution().into_tuple();
|
||||
Aabr {
|
||||
min: Vec2 { x: 0, y: 0 },
|
||||
max: Vec2 {
|
||||
|
@ -57,6 +57,9 @@ pub struct IngameParameters {
|
||||
// Used for widgets that are rasterized before being sent to the gpu (text & images)
|
||||
// Potentially make this automatic based on distance to camera?
|
||||
pub res: f32,
|
||||
// Whether the widgets should be scaled based on distance to the camera or if they should be a
|
||||
// fixed size (res is ignored in that case)
|
||||
pub fixed_scale: bool,
|
||||
}
|
||||
|
||||
pub struct State {
|
||||
@ -74,10 +77,15 @@ impl<W: Ingameable> Ingame<W> {
|
||||
num: widget.prim_count(),
|
||||
pos,
|
||||
res: 1.0,
|
||||
fixed_scale: false,
|
||||
},
|
||||
widget,
|
||||
}
|
||||
}
|
||||
pub fn fixed_scale(mut self) -> Self {
|
||||
self.parameters.fixed_scale = true;
|
||||
self
|
||||
}
|
||||
builder_methods! {
|
||||
pub resolution { parameters.res = f32 }
|
||||
}
|
||||
@ -147,6 +155,7 @@ impl IngameAnchor {
|
||||
num: 0,
|
||||
pos,
|
||||
res: 1.0,
|
||||
fixed_scale: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user