diff --git a/egregoria/src/lib.rs b/egregoria/src/lib.rs index da5b4068..db3f3691 100644 --- a/egregoria/src/lib.rs +++ b/egregoria/src/lib.rs @@ -15,6 +15,7 @@ use geom::Vec3; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::any::Any; use std::collections::BTreeMap; +use std::fmt::{Display, Formatter}; use std::hash::Hash; use std::time::{Duration, Instant}; use utils::rand_provider::RandProvider; @@ -67,6 +68,16 @@ pub enum SoulID { FreightStation(FreightStationID), } +impl Display for SoulID { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + SoulID::Human(id) => write!(f, "{:?}", id), + SoulID::GoodsCompany(id) => write!(f, "{:?}", id), + SoulID::FreightStation(id) => write!(f, "{:?}", id), + } + } +} + impl From for AnyEntity { fn from(value: SoulID) -> Self { match value { diff --git a/egregoria/src/map_dynamic/router.rs b/egregoria/src/map_dynamic/router.rs index e0944e82..3b95472c 100644 --- a/egregoria/src/map_dynamic/router.rs +++ b/egregoria/src/map_dynamic/router.rs @@ -14,7 +14,7 @@ use slotmapd::HopSlotMap; pub struct Router { steps: Vec, cur_step: Option, - target_dest: Option, + pub target_dest: Option, cur_dest: Option, vehicle: Option, pub personal_car: Option, diff --git a/egregoria/src/souls/desire/buyfood.rs b/egregoria/src/souls/desire/buyfood.rs index 52d331c4..b6ab3567 100644 --- a/egregoria/src/souls/desire/buyfood.rs +++ b/egregoria/src/souls/desire/buyfood.rs @@ -21,7 +21,7 @@ debug_inspect_impl!(BuyFoodState); #[derive(Inspect, Clone, Serialize, Deserialize, Debug)] pub struct BuyFood { - last_ate: GameInstant, + pub last_ate: GameInstant, state: BuyFoodState, bread: ItemID, } diff --git a/egregoria/src/souls/desire/home.rs b/egregoria/src/souls/desire/home.rs index cbe71a66..e358653b 100644 --- a/egregoria/src/souls/desire/home.rs +++ b/egregoria/src/souls/desire/home.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; #[derive(Inspect, Clone, Serialize, Deserialize, Debug)] pub struct Home { - house: BuildingID, + pub house: BuildingID, } impl Home { diff --git a/egregoria/src/utils/time.rs b/egregoria/src/utils/time.rs index a46aebbe..40d1bcda 100644 --- a/egregoria/src/utils/time.rs +++ b/egregoria/src/utils/time.rs @@ -1,5 +1,6 @@ use egui_inspect::Inspect; use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; pub const SECONDS_PER_REALTIME_SECOND: i32 = 15; pub const SECONDS_PER_HOUR: i32 = 3600; @@ -195,6 +196,19 @@ impl GameInstant { } } +impl Display for GameInstant { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let d = GameTime::new(0.0, self.timestamp); + write!(f, "{}", d.daytime) + } +} + +impl Display for DayTime { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}d {:02}:{:02}", self.day, self.hour, self.second) + } +} + #[cfg(test)] mod test { use common::timestep::UP_DT; diff --git a/egregoria/src/world.rs b/egregoria/src/world.rs index 7612da47..e76124c0 100644 --- a/egregoria/src/world.rs +++ b/egregoria/src/world.rs @@ -18,6 +18,7 @@ use geom::{Transform, Vec2, Vec3}; use serde::Deserialize; use slotmapd::__impl::Serialize; use slotmapd::{new_key_type, HopSlotMap}; +use std::fmt::{Display, Formatter}; new_key_type! { pub struct VehicleID; @@ -429,3 +430,16 @@ impl< .chain(self.4) } } + +impl Display for AnyEntity { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + AnyEntity::HumanID(id) => write!(f, "{:?}", id), + AnyEntity::VehicleID(id) => write!(f, "{:?}", id), + AnyEntity::TrainID(id) => write!(f, "{:?}", id), + AnyEntity::WagonID(id) => write!(f, "{:?}", id), + AnyEntity::FreightStationID(id) => write!(f, "{:?}", id), + AnyEntity::CompanyID(id) => write!(f, "{:?}", id), + } + } +} diff --git a/native_app/src/context.rs b/native_app/src/context.rs index e8dd4e42..7c4f0e95 100644 --- a/native_app/src/context.rs +++ b/native_app/src/context.rs @@ -2,6 +2,7 @@ use crate::audio::AudioContext; use crate::game_loop; use crate::init::SOUNDS_LIST; use crate::input::InputContext; +use crate::rendering::CameraHandler3D; use egregoria::utils::time::GameTime; use geom::{vec2, vec3, LinearColor}; use std::time::Instant; @@ -130,12 +131,14 @@ impl Context { let params = self.gfx.render_params.value_mut(); params.time_always = (params.time_always + self.delta) % 3600.0; params.sun_col = sun.z.max(0.0).sqrt().sqrt() * LinearColor::new(1.0, 0.95 + sun.z * 0.05, 0.95 + sun.z * 0.05, 1.0); - params.cam_pos = state.camera.camera.eye(); - params.cam_dir = -state.camera.camera.dir(); + let camera = state.uiw.read::(); + params.cam_pos = camera.camera.eye(); + params.cam_dir = -camera.camera.dir(); params.sun = sun; params.viewport = vec2(self.gfx.size.0 as f32, self.gfx.size.1 as f32); params.sun_shadow_proj = - state.camera.camera.build_sun_shadowmap_matrix(sun, params.shadow_mapping_resolution as f32, &state.camera.frustrum).try_into().unwrap(); + camera.camera.build_sun_shadowmap_matrix(sun, params.shadow_mapping_resolution as f32, &camera.frustrum).try_into().unwrap(); + drop(camera); let c = egregoria::config(); params.grass_col = c.grass_col.into(); params.sand_col = c.sand_col.into(); diff --git a/native_app/src/game_loop.rs b/native_app/src/game_loop.rs index c4a6c3a0..1ddc1f9d 100644 --- a/native_app/src/game_loop.rs +++ b/native_app/src/game_loop.rs @@ -34,8 +34,6 @@ pub struct State { pub game_schedule: SeqSchedule, - pub camera: CameraHandler3D, - egui_render: EguiWrapper, instanced_renderer: InstancedRender, @@ -74,6 +72,7 @@ impl State { let gui: Gui = common::saveload::JSON::load("gui").unwrap_or_default(); uiworld.insert(camera.camera); + uiworld.insert(camera); log::info!("version is {}", VERSION); @@ -87,7 +86,6 @@ impl State { let me = Self { uiw: uiworld, game_schedule, - camera, egui_render, instanced_renderer: InstancedRender::new(&mut ctx.gfx), map_renderer: MapRenderer::new(&mut ctx.gfx, &goria), @@ -111,9 +109,12 @@ impl State { if !self.egui_render.last_mouse_captured { let goria = self.goria.read().unwrap(); let map = goria.map(); - let unproj = self.camera.unproject(ctx.input.mouse.screen, |p| { - map.terrain.height(p).map(|x| x + 0.01) - }); + let unproj = self + .uiw + .read::() + .unproject(ctx.input.mouse.screen, |p| { + map.terrain.height(p).map(|x| x + 0.01) + }); self.uiw.write::().unprojected = unproj; } @@ -149,7 +150,7 @@ impl State { .update(&self.goria.read().unwrap(), &mut self.uiw, &mut ctx.audio); FollowEntity::update_camera(self); - self.camera.update(ctx); + self.uiw.camera_mut().update(ctx); } pub fn reset(&mut self, ctx: &mut Context) { @@ -157,21 +158,22 @@ impl State { self.goria.write().unwrap().map().dispatch_all(); } - #[profiling::function] pub fn render(&mut self, ctx: &mut FrameContext<'_>) { + profiling::scope!("main render"); let start = Instant::now(); let goria = self.goria.read().unwrap(); self.immtess.meshbuilder.clear(); - self.camera.cull_tess(&mut self.immtess); + let camera = self.uiw.read::(); + camera.cull_tess(&mut self.immtess); let time: GameTime = *self.goria.read().unwrap().read::(); self.map_renderer.render( &goria.map(), time.seconds, - &self.camera.camera, - &self.camera.frustrum, + &camera.camera, + &camera.frustrum, MapRenderOptions { show_arrows: self.uiw.read::().show_arrows(), }, @@ -286,14 +288,14 @@ impl State { let goria = self.goria.read().unwrap(); let map = goria.map(); // self.camera.movespeed = settings.camera_sensibility / 100.0; - self.camera.camera_movement( + self.uiw.camera_mut().camera_movement( ctx, ctx.delta, &self.uiw.read::(), &self.uiw.read::(), |p| map.terrain.height(p), ); - *self.uiw.write::() = self.camera.camera; + *self.uiw.write::() = self.uiw.read::().camera; drop(map); } @@ -303,7 +305,8 @@ impl State { } pub fn resized(&mut self, ctx: &mut Context, size: PhysicalSize) { - self.camera + self.uiw + .write::() .resize(ctx, size.width as f32, size.height as f32); } } diff --git a/native_app/src/gui/follow.rs b/native_app/src/gui/follow.rs index ae3301aa..202482a9 100644 --- a/native_app/src/gui/follow.rs +++ b/native_app/src/gui/follow.rs @@ -40,7 +40,7 @@ impl FollowEntity { if let Some(e) = state.uiw.read::().0 { if let Some(pos) = state.goria.read().unwrap().pos_any(e) { - state.camera.follow(pos); + state.uiw.camera_mut().follow(pos); } } } diff --git a/native_app/src/gui/inspect.rs b/native_app/src/gui/inspect.rs index 1da421ba..ff16b1d1 100644 --- a/native_app/src/gui/inspect.rs +++ b/native_app/src/gui/inspect.rs @@ -1,6 +1,8 @@ use crate::gui::follow::FollowEntity; +use crate::gui::{InspectedBuilding, InspectedEntity}; use crate::uiworld::UiWorld; use egregoria::economy::{ItemRegistry, Market}; +use egregoria::map::BuildingID; use egregoria::transportation::Location; use egregoria::{ AnyEntity, CompanyEnt, Egregoria, FreightStationEnt, HumanEnt, SoulID, TrainEnt, VehicleEnt, @@ -147,3 +149,21 @@ impl InspectRenderer { } } } + +pub fn building_link(uiworld: &mut UiWorld, goria: &Egregoria, ui: &mut Ui, b: BuildingID) { + if ui.link(format!("{:?}", b)).clicked() { + uiworld.write::().e = Some(b); + if let Some(b) = goria.map().buildings().get(b) { + uiworld.camera_mut().targetpos = b.door_pos; + } + } +} + +pub fn entity_link(uiworld: &mut UiWorld, goria: &Egregoria, ui: &mut Ui, e: AnyEntity) { + if ui.link(format!("{}", e)).clicked() { + uiworld.write::().e = Some(e); + if let Some(pos) = goria.pos_any(e) { + uiworld.camera_mut().targetpos = pos + } + } +} diff --git a/native_app/src/gui/inspect_building.rs b/native_app/src/gui/inspect_building.rs index 64d460de..85c8a2d8 100644 --- a/native_app/src/gui/inspect_building.rs +++ b/native_app/src/gui/inspect_building.rs @@ -4,7 +4,8 @@ use egregoria::engine_interaction::WorldCommand; use egregoria::{Egregoria, SoulID}; use egui::{Context, Ui, Widget}; -use crate::gui::{item_icon, InspectedEntity}; +use crate::gui::inspect::entity_link; +use crate::gui::item_icon; use egregoria::map::{Building, BuildingID, BuildingKind, Zone, MAX_ZONE_AREA}; use egregoria::map_dynamic::BuildingInfos; use egregoria::souls::freight_station::FreightTrainState; @@ -72,19 +73,17 @@ pub fn inspect_building(uiworld: &mut UiWorld, goria: &Egregoria, ui: &Context, fn render_house(ui: &mut Ui, uiworld: &mut UiWorld, goria: &Egregoria, b: &Building) { let binfos = goria.read::(); let Some(info) = binfos.get(b.id) else { return; }; - let Some(owner) = info.owner else { return; }; + let Some(SoulID::Human(owner)) = info.owner else { return; }; - let mut inspected = uiworld.write::(); - - if ui.button(format!("Owner: {owner:?}")).clicked() { - inspected.e = Some(owner.into()); - } + ui.horizontal(|ui| { + ui.label("Owner"); + entity_link(uiworld, goria, ui, owner.into()); + }); ui.label("Currently in the house:"); for &soul in info.inside.iter() { - if ui.button(format!("{soul:?}")).clicked() { - inspected.e = Some(soul.into()); - } + let SoulID::Human(soul) = soul else { continue; }; + entity_link(uiworld, goria, ui, soul.into()); } } diff --git a/native_app/src/gui/inspect_human.rs b/native_app/src/gui/inspect_human.rs new file mode 100644 index 00000000..8d4bded0 --- /dev/null +++ b/native_app/src/gui/inspect_human.rs @@ -0,0 +1,93 @@ +use egui::Context; + +use egregoria::economy::{ItemRegistry, Market}; +use egregoria::map_dynamic::Destination; +use egregoria::souls::desire::WorkKind; +use egregoria::transportation::Location; +use egregoria::{AnyEntity, Egregoria, HumanID}; + +use crate::gui::inspect::building_link; +use crate::gui::{item_icon, FollowEntity}; +use crate::uiworld::UiWorld; + +/// Inspect a specific building, showing useful information about it +pub fn inspect_human(uiworld: &mut UiWorld, goria: &Egregoria, ui: &Context, id: HumanID) -> bool { + let Some(human) = goria.get(id) else { return false; }; + + let mut is_open = true; + egui::Window::new("Human") + .resizable(false) + .auto_sized() + .open(&mut is_open) + .show(ui, |ui| { + if cfg!(debug_assertions) { + ui.label(format!("{:?}", id)); + } + + match human.location { + Location::Outside => {} + Location::Vehicle(_) => { + ui.label("In a vehicle"); + } + Location::Building(x) => { + ui.horizontal(|ui| { + ui.label("In a building:"); + building_link(uiworld, goria, ui, x); + }); + } + } + + if let Some(ref dest) = human.router.target_dest { + match dest { + Destination::Outside(pos) => { + ui.label(format!("Going to {}", pos)); + } + Destination::Building(b) => { + ui.horizontal(|ui| { + ui.label("Going to building"); + building_link(uiworld, goria, ui, *b); + }); + } + } + } + + ui.horizontal(|ui| { + ui.label("House is"); + building_link(uiworld, goria, ui, human.home.house); + }); + + ui.label(format!("Last ate: {}", human.food.last_ate)); + + if let Some(x) = human.work { + match x.kind { + WorkKind::Driver { .. } => { + ui.label("Work: Driver"); + } + WorkKind::Worker => { + ui.label("Work: Worker"); + } + } + } + + let market = goria.read::(); + let itemregistry = goria.read::(); + + ui.add_space(10.0); + + let jobopening = itemregistry.id("job-opening"); + for (&item_id, m) in market.iter() { + let Some(v) = m.capital(id.into()) else { continue }; + if item_id == jobopening { + continue; + } + let Some(item) = itemregistry.get(item_id) else { continue }; + + item_icon(ui, uiworld, item, v); + } + + if ui.small_button("follow").clicked() { + uiworld.write::().0 = Some(AnyEntity::HumanID(id)); + } + }); + is_open +} diff --git a/native_app/src/gui/mod.rs b/native_app/src/gui/mod.rs index ae31528c..b0b741a8 100644 --- a/native_app/src/gui/mod.rs +++ b/native_app/src/gui/mod.rs @@ -24,6 +24,7 @@ pub mod topgui; pub mod addtrain; pub mod inspect_building; +pub mod inspect_human; pub mod windows; pub mod zoneedit; diff --git a/native_app/src/gui/topgui.rs b/native_app/src/gui/topgui.rs index 011b06c9..fd01fa7d 100644 --- a/native_app/src/gui/topgui.rs +++ b/native_app/src/gui/topgui.rs @@ -1,5 +1,6 @@ use crate::gui::bulldozer::BulldozerState; use crate::gui::inspect_building::inspect_building; +use crate::gui::inspect_human::inspect_human; use crate::gui::lotbrush::LotBrushResource; use crate::gui::roadeditor::RoadEditorResource; use crate::gui::specialbuilding::{SpecialBuildKind, SpecialBuildingResource}; @@ -19,7 +20,7 @@ use egregoria::map::{ }; use egregoria::souls::goods_company::GoodsCompanyRegistry; use egregoria::utils::time::{GameTime, SECONDS_PER_HOUR}; -use egregoria::Egregoria; +use egregoria::{AnyEntity, Egregoria}; use egui::{Align2, Color32, Context, Frame, Id, Response, RichText, Style, Ui, Widget, Window}; use egui_inspect::{Inspect, InspectArgs}; use geom::{Polygon, Vec2}; @@ -596,8 +597,8 @@ impl Gui { } } - #[profiling::function] pub fn inspector(ui: &Context, uiworld: &mut UiWorld, goria: &Egregoria) { + profiling::scope!("topgui::inspector"); let inspected_building = *uiworld.read::(); if let Some(b) = inspected_building.e { inspect_building(uiworld, goria, ui, b); @@ -607,16 +608,24 @@ impl Gui { let e = unwrap_or!(inspected.e, return); let mut is_open = true; - Window::new("Inspect") - .default_size([400.0, 500.0]) - .default_pos([30.0, 160.0]) - .resizable(true) - .open(&mut is_open) - .show(ui, |ui| { - let mut ins = crate::gui::inspect::InspectRenderer { entity: e }; - ins.render(uiworld, goria, ui); - inspected.e = Some(ins.entity); - }); + match e { + AnyEntity::HumanID(id) => { + is_open = inspect_human(uiworld, goria, ui, id); + } + _ => { + Window::new("Inspect") + .default_size([400.0, 500.0]) + .default_pos([30.0, 160.0]) + .resizable(true) + .open(&mut is_open) + .show(ui, |ui| { + let mut ins = crate::gui::inspect::InspectRenderer { entity: e }; + ins.render(uiworld, goria, ui); + inspected.e = Some(ins.entity); + }); + } + } + if !is_open { inspected.e = None; } diff --git a/native_app/src/uiworld.rs b/native_app/src/uiworld.rs index 3c254466..b7f9ec1f 100644 --- a/native_app/src/uiworld.rs +++ b/native_app/src/uiworld.rs @@ -61,6 +61,10 @@ impl UiWorld { self.resources.insert(res); } + pub fn camera_mut(&self) -> RefMut { + self.write::() + } + pub fn check_present(&mut self, res: fn() -> T) { self.resources.get_mut_or_insert_with(res); }