use std::mem::take; use std::time::Instant; use glc::{ math::{clamp, linear_step}, matrix::Matrix4f, misc::BindGuard, shader::{Program, Type, Uniform}, vector::{Vector2f, Vector4f}, }; use shrev::{EventChannel, ReaderId}; use space_crush_common::{ components::{Fleet, Position, ShipCount}, constants::VECTOR_2F_POS_X, misc::{LogResult, WorldHelper as _}, resources::Global, return_if_none, }; use specs::{prelude::*, Entities, ReadExpect, ReadStorage, System, World}; use crate::{ components::FleetInfo, constants::{ FLEET_SELECT_ANIMATION_TIME, FLEET_SELECT_DETAIL_TIMEOUT, FLEET_SELECT_OFFSET, FLEET_SELECT_TEXT_OFFSET, FLEET_SELECT_TEXT_SIZE, FLEET_SELECT_WIDTH, UNIFORM_BUFFER_INDEX_CAMERA, }, misc::{ HorizontalAlign, MouseEvent, Text, TextCache, TextManager, VerticalAlign, WorldHelper as _, }, resources::{Camera, Config, Geometry, InputState, PlayerState, Selection}, Error, }; pub struct SelectFleet { program_simple: Program, program_detail: Program, cache: TextCache, text_total: Text, text_fighter: Text, text_bomber: Text, text_transporter: Text, mouse_event_id: ReaderId, select_mode: SelectMode, camera_counter: usize, need_value_update: bool, values_changed_once: bool, is_new_selection: bool, mouse_pos: Vector2f, count: ShipCount, values: Vector4f, marker: Vector2f, shape_size: f32, ring0: f32, ring1: f32, zoom: f32, } #[derive(Copy, Clone, Debug)] enum SelectMode { None, Init(Instant), Simple(f32), Detail(f32), SimpleClose(f32), DetailClose(f32), } #[derive(SystemData)] pub struct FleetSelectData<'a> { player_state: WriteExpect<'a, PlayerState>, camera: ReadExpect<'a, Camera>, entities: Entities<'a>, mouse_events: ReadExpect<'a, EventChannel>, input_state: ReadExpect<'a, InputState>, geometry: ReadExpect<'a, Geometry>, global: ReadExpect<'a, Global>, config: ReadExpect<'a, Config>, fleet_infos: ReadStorage<'a, FleetInfo>, positions: ReadStorage<'a, Position>, fleets: ReadStorage<'a, Fleet>, } macro_rules! selection { (&$data:expr) => { return_if_none!(&$data.player_state.selection) }; (&mut $data:expr) => { return_if_none!(&mut $data.player_state.selection) }; } macro_rules! fleet_info { (&$data:expr, $id:expr) => { return_if_none!($data.fleet_infos.get($id)) }; } macro_rules! position { (&$data:expr, $id:expr) => { return_if_none!($data.positions.get($id)) }; } impl SelectFleet { pub fn new(world: &World, text_manager: &TextManager) -> Result { let program_simple = world.load_program(vec![ (Type::Vertex, "resources/shader/fleet_select/vert.glsl"), ( Type::Fragment, "resources/shader/fleet_select/simple_frag.glsl", ), ])?; program_simple.uniform_block_binding("Camera", UNIFORM_BUFFER_INDEX_CAMERA)?; let program_detail = world.load_program(vec![ (Type::Vertex, "resources/shader/fleet_select/vert.glsl"), ( Type::Fragment, "resources/shader/fleet_select/detail_frag.glsl", ), ])?; program_detail.uniform_block_binding("Camera", UNIFORM_BUFFER_INDEX_CAMERA)?; let cache = text_manager.create_cache()?; let text_total = new_text(&cache)?; let text_fighter = new_text(&cache)?; let text_bomber = new_text(&cache)?; let text_transporter = new_text(&cache)?; let select_mode = SelectMode::None; let mouse_event_id = world.register_event_reader::()?; Ok(Self { program_simple, program_detail, cache, text_total, text_fighter, text_bomber, text_transporter, mouse_event_id, select_mode, camera_counter: 0, need_value_update: true, values_changed_once: false, is_new_selection: true, mouse_pos: Default::default(), count: Default::default(), values: Default::default(), marker: Default::default(), shape_size: Default::default(), ring0: Default::default(), ring1: Default::default(), zoom: Default::default(), }) } fn update_camera(&mut self, d: &FleetSelectData<'_>) { let camera_counter = d.camera.update_counter(); if self.camera_counter != camera_counter { self.camera_counter = camera_counter; self.need_value_update = true; } } fn handle_events(&mut self, d: &mut FleetSelectData<'_>) { let events = d.mouse_events.read(&mut self.mouse_event_id); for event in events { match event { MouseEvent::ButtonDown(button) if button == &d.config.input.fleet_select_button => { let pos = d.camera.view_to_world(d.input_state.mouse_pos); let selection = d.player_state.selection.take(); for (id, position, fleet) in (&d.entities, &d.positions, &d.fleets).join() { let r = fleet.orbit_max * fleet.orbit_max; if (position.pos - pos).length_sqr() <= r { d.player_state.selection = match selection { Some(s) if s.fleet == id => { self.is_new_selection = false; Some(s) } _ => { self.is_new_selection = true; Some(Selection { fleet: id, count: ShipCount::none(), }) } }; let selection = selection!(&d); let fleet_info = fleet_info!(&d, selection.fleet); let timeout = Instant::now() + FLEET_SELECT_DETAIL_TIMEOUT; self.mouse_pos = d.input_state.mouse_pos; self.select_mode = SelectMode::Init(timeout); self.values_changed_once = false; self.set_count(selection.count, &fleet_info.count); break; } } } MouseEvent::ButtonUp(button) if button == &d.config.input.fleet_select_button => { self.select_mode = match self.select_mode { SelectMode::Simple(progress) => { selection!(&mut d).count = self.count; self.mouse_pos = d.input_state.mouse_pos; SelectMode::SimpleClose(progress) } SelectMode::Detail(progress) => { selection!(&mut d).count = self.count; self.mouse_pos = d.input_state.mouse_pos; SelectMode::DetailClose(progress) } SelectMode::Init(_) if self.is_new_selection => { selection!(&mut d).count = ShipCount::all(); SelectMode::None } _ => SelectMode::None, } } MouseEvent::Move(_, _) if self.select_mode.is_init() => { self.need_value_update = true; self.values_changed_once = false; self.mouse_pos = d.input_state.mouse_pos; self.select_mode = SelectMode::Simple(0.0); } MouseEvent::Move(_, _) if self.select_mode.is_active() => { self.need_value_update = true; self.mouse_pos = d.input_state.mouse_pos; } _ => (), } } } fn update(&mut self, d: &FleetSelectData<'_>) { if !self.select_mode.is_active() { return; } if !take(&mut self.need_value_update) { return; } /* calculate values */ let selection = selection!(&d); let position = position!(&d, selection.fleet); let fleet_info = fleet_info!(&d, selection.fleet); self.marker = d.camera.view_to_world(self.mouse_pos) - position.pos; self.zoom = d.camera.view().axis_x.as_vec3().length(); self.shape_size = position.shape.radius().unwrap_or(0.0); self.ring0 = self.shape_size + FLEET_SELECT_OFFSET / self.zoom; self.ring1 = self.ring0 + FLEET_SELECT_WIDTH / self.zoom; let is_simple = self.select_mode.is_simple(); let angle = self .marker .angle2(&VECTOR_2F_POS_X) .normalize() .into_deg() .into_inner(); let sector = match angle { x if (135.0 >= x && x > 45.0) || is_simple => 3, x if 45.0 >= x && x > -45.0 => 0, x if -45.0 >= x && x > -135.0 => 1, _ => 2, }; /* calulate ship value */ let value = self.marker.length(); if value > self.ring1 { self.values_changed_once = true; match sector { x @ 0..=2 => { let mut count = selection.count; count[x] = usize::MAX; self.set_count(count, &fleet_info.count); self.values[x] = 1.0; self.update_values(&fleet_info.count); } _ => { self.count = ShipCount::all(); self.values = Vector4f::new(1.0, 1.0, 1.0, 1.0); } } } else if value > self.shape_size { let value = linear_step(self.ring0, self.ring1, value); self.values_changed_once = true; match sector { x @ 0..=2 => { let mut count = selection.count; count[x] = (fleet_info.count[x] as f32 * value).round() as usize; self.set_count(count, &fleet_info.count); self.values[x] = value; self.update_values(&fleet_info.count); } _ => { self.count = fleet_info.count * value; self.values = value.into(); } } } else if self.values_changed_once { match sector { x @ 0..=2 => { let mut count = selection.count; count[x] = 0; self.set_count(count, &fleet_info.count); self.values[x] = 0.0; self.update_values(&fleet_info.count); } _ => { self.count = Default::default(); self.values = Default::default(); } } } /* update texts */ let c = self.count.merge(&fleet_info.count); let _guard = self.cache.begin_update(); self.text_total .update(0, c.total().to_string()) .error("Unable to update text for ship count (total)"); self.text_fighter .update(0, c.fighter.to_string()) .error("Unable to update text for ship count (fighter)"); self.text_bomber .update(0, c.bomber.to_string()) .error("Unable to update text for ship count (bomber)"); self.text_transporter .update(0, c.transporter.to_string()) .error("Unable to update text for ship count (transporter)"); } fn progress(&mut self, delta: f32) -> Option { match &mut self.select_mode { SelectMode::Init(detail_timeout) if *detail_timeout < Instant::now() => { self.need_value_update = true; self.values_changed_once = false; self.select_mode = SelectMode::Detail(0.0); Some(0.0) } SelectMode::Simple(progress) => { *progress += delta / FLEET_SELECT_ANIMATION_TIME; *progress = clamp(0.0, 1.0, *progress); Some(*progress) } SelectMode::SimpleClose(progress) => { *progress += delta / FLEET_SELECT_ANIMATION_TIME; if *progress > 2.0 { self.select_mode = SelectMode::None; Some(2.0) } else { Some(*progress) } } SelectMode::Detail(progress) => { *progress += delta / FLEET_SELECT_ANIMATION_TIME; *progress = clamp(0.0, 1.0, *progress); Some(*progress) } SelectMode::DetailClose(progress) => { *progress += delta / FLEET_SELECT_ANIMATION_TIME; if *progress > 2.0 { self.select_mode = SelectMode::None; Some(2.0) } else { Some(*progress) } } _ => None, } } fn render(&mut self, progress: f32, d: &FleetSelectData<'_>) { /* select program */ let is_simple = self.select_mode.is_simple(); let program = if is_simple { &self.program_simple } else { &self.program_detail }; /* extract system data */ let selection = selection!(&d); let position = position!(&d, selection.fleet); /* calculate shared values */ let size = self.ring1 + 50.0; let rings = Vector2f::new(self.ring0 / size, self.ring1 / size); let px = position.pos.x; let py = position.pos.y; let m = Matrix4f::new( Vector4f::new(size, 0.0, 0.0, 0.0), Vector4f::new(0.0, size, 0.0, 0.0), Vector4f::new(0.0, 0.0, size, 0.0), Vector4f::new(px, py, 0.0, 1.0), ); /* update uniforms */ let guard = BindGuard::new(&program); program .uniform(0, Uniform::Matrix4f(&m)) .error("Error while updating model matrix"); program .uniform(1, Uniform::Vector2f(&rings)) .error("Error while updating rings"); program .uniform(2, Uniform::Vector2f(&self.marker)) .error("Error while updating marker"); program .uniform(3, Uniform::Vector4f(&self.values)) .error("Error while updating value"); program .uniform(4, Uniform::Float(progress)) .error("Error while updating progress"); program .uniform(5, Uniform::Float(self.shape_size / size)) .error("Error while updating size"); /* render selection menu */ d.geometry.render_quad(); drop(guard); /* render text */ let alpha = if progress <= 1.0 { progress } else { 2.0 - progress }; if is_simple { self.text_total .color(Vector4f::new(1.0, 1.0, 1.0, alpha)) .render_offset(&self.text_pos(self.marker, position.pos, d)); } else { self.text_total .color(Vector4f::new(1.0, 1.0, 1.0, alpha)) .render_offset(&self.text_pos((0.0, 1.0), position.pos, d)); self.text_fighter .color(Vector4f::new(1.0, 1.0, 1.0, alpha)) .render_offset(&self.text_pos((1.0, 0.0), position.pos, d)); self.text_bomber .color(Vector4f::new(1.0, 1.0, 1.0, alpha)) .render_offset(&self.text_pos((0.0, -1.0), position.pos, d)); self.text_transporter .color(Vector4f::new(1.0, 1.0, 1.0, alpha)) .render_offset(&self.text_pos((-1.0, 0.0), position.pos, d)); } } fn set_count(&mut self, count: ShipCount, fleet_count: &ShipCount) { let c = count.merge(fleet_count); let f = &fleet_count; self.count = count; self.values = Vector4f::new( clamp(0.0, 1.0, c.fighter as f32 / f.fighter as f32), clamp(0.0, 1.0, c.bomber as f32 / f.bomber as f32), clamp(0.0, 1.0, c.transporter as f32 / f.transporter as f32), clamp(0.0, 1.0, c.total() as f32 / f.total() as f32), ); } fn update_values(&mut self, fleet_count: &ShipCount) { let total_max = fleet_count.total() as f32; let sum = fleet_count[0] as f32 * self.values[0] + fleet_count[1] as f32 * self.values[1] + fleet_count[2] as f32 * self.values[2]; self.values[3] = sum / total_max; } fn text_pos>( &self, dir: T, fleet_pos: Vector2f, d: &FleetSelectData<'_>, ) -> Vector2f { let text_offset = self.ring1 + FLEET_SELECT_TEXT_OFFSET / self.zoom.sqrt(); let pos = fleet_pos + T::into(dir).normalize() * text_offset; d.camera.world_to_window(pos) } } impl<'a> System<'a> for SelectFleet { type SystemData = FleetSelectData<'a>; fn run(&mut self, mut data: Self::SystemData) { self.update_camera(&data); self.handle_events(&mut data); self.update(&data); let progress = self.progress(data.global.delta); if let Some(progress) = progress { gl::enable(gl::BLEND); gl::blend_func(gl::SRC_ALPHA, gl::ONE_MINUS_SRC_ALPHA); self.render(progress, &data); gl::disable(gl::BLEND); } } } impl SelectMode { pub fn is_init(&self) -> bool { matches!(self, Self::Init { .. }) } pub fn is_simple(&self) -> bool { match self { Self::Simple { .. } => true, Self::SimpleClose { .. } => true, _ => false, } } pub fn is_active(&self) -> bool { match self { Self::Simple { .. } => true, Self::Detail { .. } => true, _ => false, } } } fn new_text(cache: &TextCache) -> Result { cache .new_text() .scale(FLEET_SELECT_TEXT_SIZE) .font("resources/fonts/DroidSansMono.ttf") .color(1.0, 1.0, 1.0, 0.75) .nowrap() .vert_align(VerticalAlign::Center) .horz_align(HorizontalAlign::Center) .text("0") .build() }