Merge master and build

This commit is contained in:
Adam Whitehurst
2020-01-16 05:27:30 -08:00
158 changed files with 5172 additions and 2247 deletions

View File

@ -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
}

View File

@ -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())

View 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);
}
}

View File

@ -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);
}
}

View 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)
);
}
}

View File

@ -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()
}
}
}
}

View File

@ -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
View 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
View 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
View 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]);
}

View 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;
}
}
}

View 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);
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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]));
}
}

View File

@ -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)

View File

@ -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
View 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);
}
}

View File

@ -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();
}

View File

@ -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(),

View File

@ -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,
);
}

View File

@ -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 => {

View File

@ -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)),

View File

@ -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)

View File

@ -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,
);

View File

@ -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,
}
}
}

View File

@ -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(),
}
}
}

View File

@ -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.

View File

@ -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))

View File

@ -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());

View File

@ -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,

View File

@ -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;

View File

@ -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) {

View File

@ -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())

View File

@ -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 {

View File

@ -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,
},
}
}