diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ff8dd00e3..da63bfdf8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - A way to target non-player entities with commands. With rtsim_id: `rtsim@`, with uid: `uid@`. - Shorthand in voxygen for specific entities in commands, some examples `@target`, `@mount`, `@viewpoint`. - Added hit_timing to BasicMelee abilities +- A tavern building where npcs go to relax. +- Toggle for walking instead of running (Default: `I`). ### Changed diff --git a/Cargo.lock b/Cargo.lock index d89a3ddff2..5861bca5eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1880,9 +1880,9 @@ dependencies = [ [[package]] name = "enumset" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e875f1719c16de097dee81ed675e2d9bb63096823ed3f0ca827b7dea3028bbbb" +checksum = "226c0da7462c13fb57e5cc9e0dc8f0635e7d27f276a3a7fd30054647f669007d" dependencies = [ "enumset_derive", ] @@ -7430,6 +7430,7 @@ dependencies = [ "csv", "deflate", "enum-map", + "enumset", "fallible-iterator", "flate2", "fxhash", diff --git a/assets/voxygen/i18n/en/gameinput.ftl b/assets/voxygen/i18n/en/gameinput.ftl index 8c6527660b..e3de56ea0e 100644 --- a/assets/voxygen/i18n/en/gameinput.ftl +++ b/assets/voxygen/i18n/en/gameinput.ftl @@ -75,3 +75,4 @@ gameinput-muteinactivemaster = Mute master volume (inactive window) gameinput-mutemusic = Mute music volume gameinput-mutesfx = Mute SFX volume gameinput-muteambience = Mute ambience volume +gameinput-togglewalk = Toggle Walking diff --git a/assets/voxygen/i18n/en/hud/misc.ftl b/assets/voxygen/i18n/en/hud/misc.ftl index 7f5b51943e..e1307e867e 100644 --- a/assets/voxygen/i18n/en/hud/misc.ftl +++ b/assets/voxygen/i18n/en/hud/misc.ftl @@ -59,6 +59,7 @@ hud-follow = Follow hud-stay= Stay hud-sit = Sit hud-steer = Steer +hud-lay = Lay hud-portal = Portal -server = Server diff --git a/assets/voxygen/i18n/en/hud/settings.ftl b/assets/voxygen/i18n/en/hud/settings.ftl index 92befd432e..fc2ec20847 100644 --- a/assets/voxygen/i18n/en/hud/settings.ftl +++ b/assets/voxygen/i18n/en/hud/settings.ftl @@ -53,6 +53,8 @@ hud-settings-invert_controller_y_axis = Invert Controller Y Axis hud-settings-enable_mouse_smoothing = Camera Smoothing hud-settings-free_look_behavior = Free look behavior hud-settings-auto_walk_behavior = Auto walk behavior +hud-settings-walking_speed_behavior = Walking speed behavior +hud-settings-walking_speed = Walking speed hud-settings-camera_clamp_behavior = Camera clamp behavior hud-settings-zoom_lock_behavior = Camera zoom lock behavior hud-settings-player_physics_behavior = Player physics (experimental) diff --git a/common/src/comp/ability.rs b/common/src/comp/ability.rs index a6669cad24..94729755a2 100644 --- a/common/src/comp/ability.rs +++ b/common/src/comp/ability.rs @@ -1071,7 +1071,6 @@ impl CharacterAbility { && match self { CharacterAbility::Roll { energy_cost, .. } => { data.physics.on_ground.is_some() - && data.inputs.move_dir.magnitude_squared() > 0.25 && update.energy.try_change_by(-*energy_cost).is_ok() }, CharacterAbility::DashMelee { energy_cost, .. } diff --git a/common/src/comp/character_state.rs b/common/src/comp/character_state.rs index 60fdf33870..1854ed5c00 100644 --- a/common/src/comp/character_state.rs +++ b/common/src/comp/character_state.rs @@ -986,6 +986,10 @@ pub struct CharacterActivity { /// `None` means that the look direction should be derived from the /// orientation pub look_dir: Option, + /// If the character is using a Helm, this is the y direction the + /// character steering. If the character is not steering this is + /// a stale value. + pub steer_dir: f32, /// If true, the owner has set this pet to stay at a fixed location and /// to not engage in combat pub is_pet_staying: bool, diff --git a/common/src/mounting.rs b/common/src/mounting.rs index 79b666b0d3..6b25985bb4 100644 --- a/common/src/mounting.rs +++ b/common/src/mounting.rs @@ -298,6 +298,12 @@ pub struct VolumeMounting { pub rider: Uid, } +impl VolumeMounting { + pub fn is_steering_entity(&self) -> bool { + matches!(self.pos.kind, Volume::Entity(..)) && self.block.is_controller() + } +} + impl Link for VolumeMounting { type CreateData<'a> = ( Write<'a, VolumeRiders>, diff --git a/common/src/resources.rs b/common/src/resources.rs index a85b4e564c..007f34d0bd 100644 --- a/common/src/resources.rs +++ b/common/src/resources.rs @@ -7,6 +7,10 @@ use std::ops::{Mul, MulAssign}; #[derive(Copy, Clone, Debug, Serialize, Deserialize, Default)] pub struct TimeOfDay(pub f64); +impl TimeOfDay { + pub fn day(&self) -> f64 { self.0.rem_euclid(24.0 * 3600.0) } +} + /// A resource that stores the tick (i.e: physics) time. #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq)] pub struct Time(pub f64); diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index 9dd2cd4a84..dc661ec1e0 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -252,7 +252,7 @@ pub enum NpcActivity { HuntAnimals, Dance(Option), Cheer(Option), - Sit(Option), + Sit(Option, Option>), } /// Represents event-like actions that rtsim NPCs can perform to interact with diff --git a/common/src/terrain/sprite.rs b/common/src/terrain/sprite.rs index 125a4c60ac..89e02d8950 100644 --- a/common/src/terrain/sprite.rs +++ b/common/src/terrain/sprite.rs @@ -566,22 +566,15 @@ impl SpriteKind { #[inline] pub fn mount_offset(&self) -> Option<(Vec3, Vec3)> { match self { - SpriteKind::ChairSingle | SpriteKind::ChairDouble | SpriteKind::Bench => Some(( - Vec3 { - x: 0.0, - y: 0.0, - z: 0.5, - }, - -Vec3::unit_y(), - )), - SpriteKind::Helm => Some(( - Vec3 { - x: 0.0, - y: -0.6, - z: 0.2, - }, - Vec3::unit_y(), - )), + SpriteKind::ChairSingle | SpriteKind::ChairDouble | SpriteKind::Bench => { + Some((Vec3::new(0.0, 0.0, 0.5), -Vec3::unit_y())) + }, + SpriteKind::Helm => Some((Vec3::new(0.0, -1.0, 0.0), Vec3::unit_y())), + SpriteKind::Bed => Some((Vec3::new(0.0, 0.0, 0.6), -Vec3::unit_y())), + SpriteKind::BedrollSnow | SpriteKind::BedrollPirate => { + Some((Vec3::new(0.0, 0.0, 0.1), -Vec3::unit_x())) + }, + SpriteKind::Bedroll => Some((Vec3::new(0.0, 0.0, 0.1), Vec3::unit_y())), _ => None, } } diff --git a/common/systems/src/mount.rs b/common/systems/src/mount.rs index ce6a7c46e4..ed1b7f3c46 100644 --- a/common/systems/src/mount.rs +++ b/common/systems/src/mount.rs @@ -1,5 +1,8 @@ use common::{ - comp::{Body, Collider, ControlAction, Controller, InputKind, Ori, Pos, Scale, Vel}, + comp::{ + Body, CharacterActivity, Collider, ControlAction, Controller, InputKind, Ori, Pos, Scale, + Vel, + }, link::Is, mounting::{Mount, VolumeRider}, terrain::TerrainGrid, @@ -24,6 +27,7 @@ impl<'a> System<'a> for Sys { WriteStorage<'a, Pos>, WriteStorage<'a, Vel>, WriteStorage<'a, Ori>, + WriteStorage<'a, CharacterActivity>, ReadStorage<'a, Body>, ReadStorage<'a, Scale>, ReadStorage<'a, Collider>, @@ -45,6 +49,7 @@ impl<'a> System<'a> for Sys { mut positions, mut velocities, mut orientations, + mut character_activity, bodies, scales, colliders, @@ -174,6 +179,12 @@ impl<'a> System<'a> for Sys { if is_volume_rider.block.is_controller() { if let Some((actions, inputs)) = inputs { + if let Some(mut character_activity) = character_activity + .get_mut(entity) + .filter(|c| c.steer_dir != inputs.move_dir.y) + { + character_activity.steer_dir = inputs.move_dir.y; + } match is_volume_rider.pos.kind { common::mounting::Volume::Entity(uid) => { if let Some(controller) = diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index 1912667c61..0f7ef02cb9 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -79,7 +79,9 @@ impl Controller { pub fn do_cheer(&mut self, dir: Option) { self.activity = Some(NpcActivity::Cheer(dir)); } - pub fn do_sit(&mut self, dir: Option) { self.activity = Some(NpcActivity::Sit(dir)); } + pub fn do_sit(&mut self, dir: Option, pos: Option>) { + self.activity = Some(NpcActivity::Sit(dir, pos)); + } pub fn say(&mut self, target: impl Into>, content: comp::Content) { self.actions.push(NpcAction::Say(target.into(), content)); diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index 67213bdaca..d2e0a5b355 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -113,6 +113,7 @@ impl Data { PlotKind::House(_) | PlotKind::Workshop(_) | PlotKind::AirshipDock(_) + | PlotKind::Tavern(_) | PlotKind::Plaza | PlotKind::SavannahPit(_) | PlotKind::SavannahHut(_) diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 8609dea34a..eedc7796c3 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -38,7 +38,7 @@ use vek::*; use world::{ civ::{self, Track}, site::{Site as WorldSite, SiteKind}, - site2::{self, PlotKind, TileKind}, + site2::{self, plot::tavern, PlotKind, TileKind}, util::NEIGHBORS, IndexRef, World, }; @@ -325,7 +325,9 @@ impl Rule for NpcAi { } } -fn idle() -> impl Action { just(|ctx, _| ctx.controller.do_idle()).debug(|| "idle") } +fn idle() -> impl Action + Clone { + just(|ctx, _| ctx.controller.do_idle()).debug(|| "idle") +} /// Try to walk toward a 3D position without caring for obstacles. fn goto(wpos: Vec3, speed_factor: f32, goal_dist: f32) -> impl Action { @@ -578,7 +580,7 @@ fn travel_to_site(tgt_site: SiteId, speed_factor: f32) -> impl Action< .map(|_, _| ()) } -fn talk_to(tgt: Actor, _subject: Option) -> impl Action { +fn talk_to(tgt: Actor, _subject: Option) -> impl Action + Clone { now(move |ctx, _| { if matches!(tgt, Actor::Npc(_)) && ctx.rng.gen_bool(0.2) { // Cut off the conversation sometimes to avoid infinite conversations (but only @@ -630,7 +632,7 @@ fn talk_to(tgt: Actor, _subject: Option) -> impl Action { }) } -fn socialize() -> impl Action { +fn socialize() -> impl Action + Clone { now(move |ctx, socialize: &mut EveryRange| { // Skip most socialising actions if we're not loaded if matches!(ctx.npc.mode, SimulationMode::Loaded) && socialize.should(ctx) { @@ -758,6 +760,8 @@ fn choose_plaza(ctx: &mut NpcCtx, site: SiteId) -> Option> { }) } +const WALKING_SPEED: f32 = 0.35; + fn villager(visiting_site: SiteId) -> impl Action { choose(move |ctx, state: &mut DefaultState| { // Consider moving home if the home site gets too full @@ -804,8 +808,9 @@ fn villager(visiting_site: SiteId) -> impl Action { .then(travel_to_site(new_home, 0.5)) .then(just(move |ctx, _| ctx.controller.set_new_home(new_home)))); } - - if DayPeriod::from(ctx.time_of_day.0).is_dark() + let day_period = DayPeriod::from(ctx.time_of_day.0); + let is_weekend = ctx.time_of_day.day() as u64 % 6 == 0; + if day_period.is_dark() && !matches!(ctx.npc.profession(), Some(Profession::Guard)) { return important( @@ -845,51 +850,142 @@ fn villager(visiting_site: SiteId) -> impl Action { }) .debug(|| "find somewhere to sleep"), ); - // Villagers with roles should perform those roles } - // Visiting villagers in DesertCity who are not Merchants should sit down in the Arena during the day - else if matches!(ctx.state.data().sites[visiting_site].world_site.map(|ws| &ctx.index.sites.get(ws).kind), Some(SiteKind::DesertCity(_))) - && !matches!(ctx.npc.profession(), Some(Profession::Merchant | Profession::Guard)) - && ctx.rng.gen_bool(1.0 / 3.0) - { - let wait_time = ctx.rng.gen_range(100.0..300.0); + // Go do something fun on evenings and holidays, or on random days. + else if + // Ain't no rest for the wicked + !matches!(ctx.npc.profession(), Some(Profession::Guard)) + && (matches!(day_period, DayPeriod::Evening) || is_weekend || ctx.rng.gen_bool(0.05)) { + let mut fun_stuff = Vec::new(); + if let Some(ws_id) = ctx.state.data().sites[visiting_site].world_site - && let Some(ws) = ctx.index.sites.get(ws_id).site2() - && let Some(arena) = ws.plots().find_map(|p| match p.kind() { PlotKind::DesertCityArena(a) => Some(a), _ => None}) - { - // We don't use Z coordinates for seats because they are complicated to calculate from the Ramp procedural generation - // and using goto_2d seems to work just fine. However it also means that NPC will never go seat on the stands - // on the first floor of the arena. This is a compromise that was made because in the current arena procedural generation - // there is also no pathways to the stands on the first floor for NPCs. - let arena_center = Vec3::new(arena.center.x, arena.center.y, arena.base).as_::(); - let stand_dist = arena.stand_dist as f32; - let seat_var_width = ctx.rng.gen_range(0..arena.stand_width) as f32; - let seat_var_length = ctx.rng.gen_range(-arena.stand_length..arena.stand_length) as f32; - // Select a seat on one of the 4 arena stands - let seat = match ctx.rng.gen_range(0..4) { - 0 => Vec3::new(arena_center.x - stand_dist + seat_var_width, arena_center.y + seat_var_length, arena_center.z), - 1 => Vec3::new(arena_center.x + stand_dist - seat_var_width, arena_center.y + seat_var_length, arena_center.z), - 2 => Vec3::new(arena_center.x + seat_var_length, arena_center.y - stand_dist + seat_var_width, arena_center.z), - _ => Vec3::new(arena_center.x + seat_var_length, arena_center.y + stand_dist - seat_var_width, arena_center.z), - }; - let look_dir = Dir::from_unnormalized(arena_center - seat); - // Walk to an arena seat, cheer, sit and dance - return casual(just(move |ctx, _| ctx.controller.say(None, Content::localized("npc-speech-arena"))) - .then(goto_2d(seat.xy(), 0.6, 1.0).debug(|| "go to arena")) - // Turn toward the centre of the arena and watch the action! - .then(choose(move |ctx, _| if ctx.rng.gen_bool(0.3) { - casual(just(move |ctx,_| ctx.controller.do_cheer(look_dir)).repeat().stop_if(timeout(5.0))) - } else if ctx.rng.gen_bool(0.15) { - casual(just(move |ctx,_| ctx.controller.do_dance(look_dir)).repeat().stop_if(timeout(5.0))) - } else { - casual(just(move |ctx,_| ctx.controller.do_sit(look_dir)).repeat().stop_if(timeout(15.0))) - }) + && let Some(ws) = ctx.index.sites.get(ws_id).site2() { + if let Some(arena) = ws.plots().find_map(|p| match p.kind() { PlotKind::DesertCityArena(a) => Some(a), _ => None}) { + let wait_time = ctx.rng.gen_range(100.0..300.0); + // We don't use Z coordinates for seats because they are complicated to calculate from the Ramp procedural generation + // and using goto_2d seems to work just fine. However it also means that NPC will never go seat on the stands + // on the first floor of the arena. This is a compromise that was made because in the current arena procedural generation + // there is also no pathways to the stands on the first floor for NPCs. + let arena_center = Vec3::new(arena.center.x, arena.center.y, arena.base).as_::(); + let stand_dist = arena.stand_dist as f32; + let seat_var_width = ctx.rng.gen_range(0..arena.stand_width) as f32; + let seat_var_length = ctx.rng.gen_range(-arena.stand_length..arena.stand_length) as f32; + // Select a seat on one of the 4 arena stands + let seat = match ctx.rng.gen_range(0..4) { + 0 => Vec3::new(arena_center.x - stand_dist + seat_var_width, arena_center.y + seat_var_length, arena_center.z), + 1 => Vec3::new(arena_center.x + stand_dist - seat_var_width, arena_center.y + seat_var_length, arena_center.z), + 2 => Vec3::new(arena_center.x + seat_var_length, arena_center.y - stand_dist + seat_var_width, arena_center.z), + _ => Vec3::new(arena_center.x + seat_var_length, arena_center.y + stand_dist - seat_var_width, arena_center.z), + }; + let look_dir = Dir::from_unnormalized(arena_center - seat); + // Walk to an arena seat, cheer, sit and dance + let action = casual(just(move |ctx, _| ctx.controller.say(None, Content::localized("npc-speech-arena"))) + .then(goto_2d(seat.xy(), 0.6, 1.0).debug(|| "go to arena")) + // Turn toward the centre of the arena and watch the action! + .then(choose(move |ctx, _| if ctx.rng.gen_bool(0.3) { + casual(just(move |ctx,_| ctx.controller.do_cheer(look_dir)).repeat().stop_if(timeout(5.0))) + } else if ctx.rng.gen_bool(0.15) { + casual(just(move |ctx,_| ctx.controller.do_dance(look_dir)).repeat().stop_if(timeout(5.0))) + } else { + casual(just(move |ctx,_| ctx.controller.do_sit(look_dir, None)).repeat().stop_if(timeout(15.0))) + }) + .repeat() + .stop_if(timeout(wait_time))) + .map(|_, _| ()) + .boxed()); + fun_stuff.push(action); + } + if let Some(tavern) = ws.plots().filter_map(|p| match p.kind() { PlotKind::Tavern(a) => Some(a), _ => None }).choose(&mut ctx.rng) { + let wait_time = ctx.rng.gen_range(100.0..300.0); + + let (stage_aabr, stage_z) = tavern.rooms.values().flat_map(|room| { + room.details.iter().filter_map(|detail| match detail { + tavern::Detail::Stage { aabr } => Some((*aabr, room.bounds.min.z + 1)), + _ => None, + }) + }).choose(&mut ctx.rng).unwrap_or((tavern.bounds, tavern.door_wpos.z)); + + let bar_pos = tavern.rooms.values().flat_map(|room| + room.details.iter().filter_map(|detail| match detail { + tavern::Detail::Bar { aabr } => { + let side = site2::util::Dir::from_vec2(room.bounds.center().xy() - aabr.center()); + let pos = side.select_aabr_with(*aabr, aabr.center()) + side.to_vec2(); + + Some(pos.with_z(room.bounds.min.z)) + } + _ => None, + }) + ).choose(&mut ctx.rng).unwrap_or(stage_aabr.center().with_z(stage_z)); + + // Pick a chair that is theirs for the stay + let chair_pos = tavern.rooms.values().flat_map(|room| { + let z = room.bounds.min.z; + room.details.iter().filter_map(move |detail| match detail { + tavern::Detail::Table { pos, chairs } => Some(chairs.into_iter().map(move |dir| pos.with_z(z) + dir.to_vec2())), + _ => None, + }) + .flatten() + } + ).choose(&mut ctx.rng) + // This path is possible, but highly unlikely. + .unwrap_or(bar_pos); + + let stage_aabr = stage_aabr.as_::(); + let stage_z = stage_z as f32; + + let action = casual(travel_to_point(tavern.door_wpos.xy().as_() + 0.5, 0.8).then(choose(move |ctx, (last_action, _)| { + let action = [0, 1, 2].into_iter().filter(|i| *last_action != Some(*i)).choose(&mut ctx.rng).expect("We have at least 2 elements"); + let socialize = socialize().map_state(|(_, timer)| timer).repeat(); + match action { + // Go and dance on a stage. + 0 => { + casual(now(move |ctx, (last_action, _)| { + *last_action = Some(action); + goto(stage_aabr.min.map2(stage_aabr.max, |a, b| ctx.rng.gen_range(a..b)).with_z(stage_z), WALKING_SPEED, 1.0) + }) + .then(just(move |ctx,_| ctx.controller.do_dance(None)).repeat().stop_if(timeout(ctx.rng.gen_range(20.0..30.0)))) + .map(|_, _| ()) + ) + }, + // Go and sit at a table. + 1 => { + casual( + now(move |ctx, (last_action, _)| { + *last_action = Some(action); + goto(chair_pos.as_() + 0.5, WALKING_SPEED, 1.0).then(just(move |ctx, _| ctx.controller.do_sit(None, Some(chair_pos)))).then(socialize.clone().stop_if(timeout(ctx.rng.gen_range(30.0..60.0)))).map(|_, _| ()) + }) + ) + }, + // Go to the bar. + _ => { + casual( + now(move |ctx, (last_action, _)| { + *last_action = Some(action); + goto(bar_pos.as_() + 0.5, WALKING_SPEED, 1.0).then(socialize.clone().stop_if(timeout(ctx.rng.gen_range(10.0..25.0)))).map(|_, _| ()) + }) + ) + }, + } + }) + .with_state((None::, every_range(5.0..10.0))) .repeat() .stop_if(timeout(wait_time))) - .map(|_, _| ()) - .boxed()); + .map(|_, _| ()) + .boxed() + ); + + fun_stuff.push(action); + } } - } else if matches!(ctx.npc.profession(), Some(Profession::Herbalist)) && ctx.rng.gen_bool(0.8) + + + if !fun_stuff.is_empty() { + let i = ctx.rng.gen_range(0..fun_stuff.len()); + return fun_stuff.swap_remove(i); + } + } + // Villagers with roles should perform those roles + else if matches!(ctx.npc.profession(), Some(Profession::Herbalist)) && ctx.rng.gen_bool(0.8) { if let Some(forest_wpos) = find_forest(ctx) { return casual( diff --git a/rtsim/src/rule/simulate_npcs.rs b/rtsim/src/rule/simulate_npcs.rs index c32faff891..f1b25dcfec 100644 --- a/rtsim/src/rule/simulate_npcs.rs +++ b/rtsim/src/rule/simulate_npcs.rs @@ -254,7 +254,7 @@ fn on_tick(ctx: EventCtx) { | NpcActivity::HuntAnimals | NpcActivity::Dance(_) | NpcActivity::Cheer(_) - | NpcActivity::Sit(_), + | NpcActivity::Sit(..), ) => { // TODO: Maybe they should walk around randomly // when gathering resources? diff --git a/server/agent/src/action_nodes.rs b/server/agent/src/action_nodes.rs index abf3ab97e3..bf83a93e44 100644 --- a/server/agent/src/action_nodes.rs +++ b/server/agent/src/action_nodes.rs @@ -29,6 +29,7 @@ use common::{ consts::MAX_MOUNT_RANGE, effect::{BuffEffect, Effect}, event::{Emitter, ServerEvent}, + mounting::VolumePos, path::TraversalConfig, rtsim::NpcActivity, states::basic_beam, @@ -51,9 +52,7 @@ impl<'a> AgentData<'a> { //////////////////////////////////////// pub fn glider_fall(&self, controller: &mut Controller, read_data: &ReadData) { - if read_data.is_riders.contains(*self.entity) { - controller.push_event(ControlEvent::Unmount); - } + self.dismount(controller, read_data); controller.push_action(ControlAction::GlideWield); @@ -73,9 +72,7 @@ impl<'a> AgentData<'a> { } pub fn fly_upward(&self, controller: &mut Controller, read_data: &ReadData) { - if read_data.is_riders.contains(*self.entity) { - controller.push_event(ControlEvent::Unmount); - } + self.dismount(controller, read_data); controller.push_basic_input(InputKind::Fly); controller.inputs.move_z = 1.0; @@ -96,9 +93,7 @@ impl<'a> AgentData<'a> { path: Path, speed_multiplier: Option, ) -> bool { - if read_data.is_riders.contains(*self.entity) { - controller.push_event(ControlEvent::Unmount); - } + self.dismount(controller, read_data); let partial_path_tgt_pos = |pos_difference: Vec3| { self.pos.0 @@ -242,6 +237,14 @@ impl<'a> AgentData<'a> { 'activity: { match agent.rtsim_controller.activity { Some(NpcActivity::Goto(travel_to, speed_factor)) => { + if read_data + .is_volume_riders + .get(*self.entity) + .map_or(false, |r| !r.is_steering_entity()) + { + controller.push_event(ControlEvent::Unmount); + } + // If it has an rtsim destination and can fly, then it should. // If it is flying and bumps something above it, then it should move down. if self.traversal_config.can_fly @@ -399,17 +402,26 @@ impl<'a> AgentData<'a> { controller.push_action(ControlAction::Talk); break 'activity; // Don't fall through to idle wandering }, - Some(NpcActivity::Sit(dir)) => { - if let Some(look_dir) = dir { - controller.inputs.look_dir = look_dir; - if self.ori.look_dir().dot(look_dir.to_vec()) < 0.95 { - controller.inputs.move_dir = look_dir.to_vec().xy() * 0.01; - break 'activity; - } else { - controller.inputs.move_dir = Vec2::zero(); + Some(NpcActivity::Sit(dir, pos)) => { + if let Some(pos) = + pos.filter(|p| read_data.terrain.get(*p).is_ok_and(|b| b.is_mountable())) + { + if !read_data.is_volume_riders.contains(*self.entity) { + controller + .push_event(ControlEvent::MountVolume(VolumePos::terrain(pos))); } + } else { + if let Some(look_dir) = dir { + controller.inputs.look_dir = look_dir; + if self.ori.look_dir().dot(look_dir.to_vec()) < 0.95 { + controller.inputs.move_dir = look_dir.to_vec().xy() * 0.01; + break 'activity; + } else { + controller.inputs.move_dir = Vec2::zero(); + } + } + controller.push_action(ControlAction::Sit); } - controller.push_action(ControlAction::Sit); break 'activity; // Don't fall through to idle wandering }, Some(NpcActivity::HuntAnimals) => { @@ -581,7 +593,9 @@ impl<'a> AgentData<'a> { read_data: &ReadData, tgt_pos: &Pos, ) { - if read_data.is_riders.contains(*self.entity) { + if read_data.is_riders.contains(*self.entity) + || read_data.is_volume_riders.contains(*self.entity) + { controller.push_event(ControlEvent::Unmount); } @@ -637,7 +651,9 @@ impl<'a> AgentData<'a> { // Proportion of full speed const MAX_FLEE_SPEED: f32 = 0.65; - if read_data.is_riders.contains(*self.entity) { + if read_data.is_riders.contains(*self.entity) + || read_data.is_volume_riders.contains(*self.entity) + { controller.push_event(ControlEvent::Unmount); } @@ -993,9 +1009,7 @@ impl<'a> AgentData<'a> { #[cfg(feature = "be-dyn-lib")] let rng = &mut thread_rng(); - if read_data.is_riders.contains(*self.entity) { - controller.push_event(ControlEvent::Unmount); - } + self.dismount(controller, read_data); let tool_tactic = |tool_kind| match tool_kind { ToolKind::Bow => Tactic::Bow, @@ -1999,4 +2013,15 @@ impl<'a> AgentData<'a> { } } } + + pub fn dismount(&self, controller: &mut Controller, read_data: &ReadData) { + if read_data.is_riders.contains(*self.entity) + || read_data + .is_volume_riders + .get(*self.entity) + .map_or(false, |r| !r.is_steering_entity()) + { + controller.push_event(ControlEvent::Unmount); + } + } } diff --git a/voxygen/anim/src/character/mod.rs b/voxygen/anim/src/character/mod.rs index 0c14bfc1a2..661b6e97c4 100644 --- a/voxygen/anim/src/character/mod.rs +++ b/voxygen/anim/src/character/mod.rs @@ -28,11 +28,13 @@ pub mod selfbuff; pub mod shockwave; pub mod shoot; pub mod sit; +pub mod sleep; pub mod sneak; pub mod sneakequip; pub mod sneakwield; pub mod staggered; pub mod stand; +pub mod steer; pub mod stunned; pub mod swim; pub mod swimwield; @@ -51,11 +53,11 @@ pub use self::{ mount::MountAnimation, music::MusicAnimation, rapidmelee::RapidMeleeAnimation, repeater::RepeaterAnimation, ripostemelee::RiposteMeleeAnimation, roll::RollAnimation, run::RunAnimation, selfbuff::SelfBuffAnimation, shockwave::ShockwaveAnimation, - shoot::ShootAnimation, sit::SitAnimation, sneak::SneakAnimation, + shoot::ShootAnimation, sit::SitAnimation, sleep::SleepAnimation, sneak::SneakAnimation, sneakequip::SneakEquipAnimation, sneakwield::SneakWieldAnimation, - staggered::StaggeredAnimation, stand::StandAnimation, stunned::StunnedAnimation, - swim::SwimAnimation, swimwield::SwimWieldAnimation, talk::TalkAnimation, - wallrun::WallrunAnimation, wield::WieldAnimation, + staggered::StaggeredAnimation, stand::StandAnimation, steer::SteerAnimation, + stunned::StunnedAnimation, swim::SwimAnimation, swimwield::SwimWieldAnimation, + talk::TalkAnimation, wallrun::WallrunAnimation, wield::WieldAnimation, }; use super::{make_bone, vek::*, FigureBoneData, Offsets, Skeleton, TrailSource}; use common::comp::{ @@ -431,8 +433,8 @@ impl CharacterSkeleton { * ((acc_vel * lab * 1.6).sin()); self.lantern.position = Vec3::new(s_a.lantern.0, s_a.lantern.1, s_a.lantern.2); - self.lantern.orientation = - Quaternion::rotation_x(shorte * 0.7 + 0.4) * Quaternion::rotation_y(shorte * 0.4); + self.lantern.orientation = Quaternion::rotation_x(shorte * 0.7 * speednorm.powi(2) + 0.4) + * Quaternion::rotation_y(shorte * 0.4 * speednorm.powi(2)); self.lantern.scale = Vec3::one() * 0.65; self.hold.scale = Vec3::one() * 0.0; diff --git a/voxygen/anim/src/character/run.rs b/voxygen/anim/src/character/run.rs index acd671cd97..a77c5cd096 100644 --- a/voxygen/anim/src/character/run.rs +++ b/voxygen/anim/src/character/run.rs @@ -152,7 +152,7 @@ impl Animation for RunAnimation { next.back.position = Vec3::new(0.0, s_a.back.0, s_a.back.1); next.back.orientation = Quaternion::rotation_x(-0.05 + short * 0.02 + noisea * 0.02 + noiseb * 0.02) - * Quaternion::rotation_y(foothorir * 0.2); + * Quaternion::rotation_y(foothorir * 0.35 * speednorm.powi(2)); next.shorts.position = Vec3::new(0.0, 0.65 + s_a.shorts.0, 0.65 * speednorm + s_a.shorts.1); next.shorts.orientation = Quaternion::rotation_x(0.2 * speednorm) @@ -163,22 +163,24 @@ impl Animation for RunAnimation { -s_a.hand.0 * 1.2 - foothorir * 1.3 * speednorm + (foothoril.abs().powi(2) - 0.5) * speednorm * 4.0, s_a.hand.1 * 1.3 + foothorir * -7.0 * speednorm.powi(2) * (1.0 - sideabs), - s_a.hand.2 - foothorir * 2.75 * speednorm + foothoril.abs().powi(3) * speednorm * 8.0, + s_a.hand.2 - foothorir * 2.75 * speednorm + + foothoril.abs().powi(3) * speednorm.powi(2) * 8.0, ); next.hand_l.orientation = Quaternion::rotation_x( - 0.6 * speednorm + (footrotr * -1.5 + 0.5) * speednorm * (1.0 - sideabs), + 0.6 * speednorm + (footrotr * -1.5 + 0.5) * speednorm.powi(2) * (1.0 - sideabs), ) * Quaternion::rotation_y(footrotr * 0.4 * speednorm + PI * 0.07); next.hand_r.position = Vec3::new( s_a.hand.0 * 1.2 + foothoril * 1.3 * speednorm - (foothorir.abs().powi(2) - 0.5) * speednorm * 4.0, s_a.hand.1 * 1.3 + foothoril * -7.0 * speednorm.powi(2) * (1.0 - sideabs), - s_a.hand.2 - foothoril * 2.75 * speednorm + foothorir.abs().powi(3) * speednorm * 8.0, + s_a.hand.2 - foothoril * 2.75 * speednorm + + foothorir.abs().powi(3) * speednorm.powi(2) * 8.0, ); next.hand_r.orientation = Quaternion::rotation_x( - 0.6 * speednorm + (footrotl * -1.5 + 0.5) * speednorm * (1.0 - sideabs), + 0.6 * speednorm + (footrotl * -1.5 + 0.5) * speednorm.powi(2) * (1.0 - sideabs), ) * Quaternion::rotation_y(footrotl * -0.4 * speednorm - PI * 0.07); next.foot_l.position = Vec3::new( diff --git a/voxygen/anim/src/character/sleep.rs b/voxygen/anim/src/character/sleep.rs new file mode 100644 index 0000000000..abeaa6ba96 --- /dev/null +++ b/voxygen/anim/src/character/sleep.rs @@ -0,0 +1,114 @@ +use super::{ + super::{vek::*, Animation}, + CharacterSkeleton, SkeletonAttr, +}; +use common::comp::item::ToolKind; +use std::{f32::consts::PI, ops::Mul}; + +pub struct SleepAnimation; + +impl Animation for SleepAnimation { + type Dependency<'a> = (Option, Option, f32); + type Skeleton = CharacterSkeleton; + + #[cfg(feature = "use-dyn-lib")] + const UPDATE_FN: &'static [u8] = b"character_sleep\0"; + + #[cfg_attr(feature = "be-dyn-lib", export_name = "character_sleep")] + fn update_skeleton_inner( + skeleton: &Self::Skeleton, + (_active_tool_kind, _second_tool_kind, global_time): Self::Dependency<'_>, + anim_time: f32, + _rate: &mut f32, + s_a: &SkeletonAttr, + ) -> Self::Skeleton { + let mut next = (*skeleton).clone(); + + let slow = (anim_time * 1.0).sin(); + let slowa = (anim_time * 1.0 + PI / 2.0).sin(); + let stop = (anim_time * 3.0).min(PI / 2.0).sin(); + + let head_look = Vec2::new( + (global_time * 0.05 + anim_time / 15.0) + .floor() + .mul(7331.0) + .sin() + * 0.25, + (global_time * 0.05 + anim_time / 15.0) + .floor() + .mul(1337.0) + .sin() + * 0.125, + ); + next.head.position = Vec3::new(0.0, s_a.head.0, s_a.head.1 + slow * 0.1 + stop * -0.8); + next.head.orientation = Quaternion::rotation_z(head_look.x + slow * 0.2 - slow * 0.1) + * Quaternion::rotation_x((slowa * -0.1 + slow * 0.1 + head_look.y).abs()); + + next.chest.position = Vec3::new( + 0.0, + s_a.chest.0 + stop * -0.4, + s_a.chest.1 + slow * 0.1 + stop * -0.8, + ); + next.chest.orientation = Quaternion::rotation_x(stop * 0.15 + 1.0); + + next.belt.position = Vec3::new(0.0, s_a.belt.0 + stop * 1.2, s_a.belt.1); + next.belt.orientation = Quaternion::rotation_x(stop * 0.3); + + next.back.position = Vec3::new(0.0, s_a.back.0, s_a.back.1); + + next.shorts.position = Vec3::new(0.0, s_a.shorts.0 + stop * 2.5, s_a.shorts.1 + stop * 0.6); + next.shorts.orientation = Quaternion::rotation_x(stop * 0.6); + + next.hand_l.position = Vec3::new( + -s_a.hand.0 - 1.0, + s_a.hand.1 + slowa * 0.15 + 2.0, + s_a.hand.2 + slow * 0.7 + stop * -2.0, + ); + next.hand_l.orientation = + Quaternion::rotation_x(slowa * -0.1 + slow * 0.1) * Quaternion::rotation_y(PI * 0.15); + + next.hand_r.position = Vec3::new( + s_a.hand.0 + 1.0, + s_a.hand.1 + slowa * 0.15 + 2.0, + s_a.hand.2 + slow * 0.7 + stop * -2.0, + ); + next.hand_r.orientation = + Quaternion::rotation_x(slow * -0.1 + slowa * 0.1) * Quaternion::rotation_y(PI * -0.15); + + next.foot_l.position = Vec3::new(-s_a.foot.0, 6.0 + s_a.foot.1, 6.0 + s_a.foot.2); + next.foot_l.orientation = Quaternion::rotation_x(slow * 0.1 + stop * 1.2 + slow * 0.1); + + next.foot_r.position = Vec3::new(s_a.foot.0, 6.0 + s_a.foot.1, 6.0 + s_a.foot.2); + next.foot_r.orientation = Quaternion::rotation_x(slowa * 0.1 + stop * 1.2 + slowa * 0.1); + + next.shoulder_l.position = Vec3::new(-s_a.shoulder.0, s_a.shoulder.1, s_a.shoulder.2); + next.shoulder_l.orientation = Quaternion::rotation_x(0.0); + + next.shoulder_r.position = Vec3::new(s_a.shoulder.0, s_a.shoulder.1, s_a.shoulder.2); + next.shoulder_r.orientation = Quaternion::rotation_x(0.0); + + next.torso.position = Vec3::new(0.0, -2.2, stop * -1.76); + + if skeleton.holding_lantern { + next.hand_r.position = Vec3::new( + s_a.hand.0 + 1.0 - head_look.x * 8.0, + s_a.hand.1 + 5.0 + head_look.x * 6.0, + s_a.hand.2 + 9.0 + head_look.y * 6.0, + ); + next.hand_r.orientation = Quaternion::rotation_x(2.25) + * Quaternion::rotation_z(0.9) + * Quaternion::rotation_y(head_look.x * 3.0) + * Quaternion::rotation_x(head_look.y * 3.0); + + let fast = (anim_time * 5.0).sin(); + let fast2 = (anim_time * 4.5 + 8.0).sin(); + + next.lantern.position = Vec3::new(-0.5, -0.5, -2.5); + next.lantern.orientation = next.hand_r.orientation.inverse() + * Quaternion::rotation_x(fast * 0.1) + * Quaternion::rotation_y(fast2 * 0.1); + } + + next + } +} diff --git a/voxygen/anim/src/character/steer.rs b/voxygen/anim/src/character/steer.rs new file mode 100644 index 0000000000..f1d270ef1f --- /dev/null +++ b/voxygen/anim/src/character/steer.rs @@ -0,0 +1,118 @@ +use super::{ + super::{vek::*, Animation}, + CharacterSkeleton, SkeletonAttr, +}; +use common::comp::item::ToolKind; +use std::{f32::consts::PI, ops::Mul}; + +pub struct SteerAnimation; + +impl Animation for SteerAnimation { + type Dependency<'a> = (Option, Option, f32, f32); + type Skeleton = CharacterSkeleton; + + #[cfg(feature = "use-dyn-lib")] + const UPDATE_FN: &'static [u8] = b"character_steer\0"; + + #[cfg_attr(feature = "be-dyn-lib", export_name = "character_steer")] + fn update_skeleton_inner( + skeleton: &Self::Skeleton, + (_active_tool_kind, _second_tool_kind, steer_dir, global_time): Self::Dependency<'_>, + anim_time: f32, + _rate: &mut f32, + s_a: &SkeletonAttr, + ) -> Self::Skeleton { + let mut next = (*skeleton).clone(); + + let slow = (anim_time * 1.0).sin(); + let head_look = Vec2::new( + (global_time + anim_time / 12.0).floor().mul(7331.0).sin() * 0.1, + (global_time + anim_time / 12.0).floor().mul(1337.0).sin() * 0.05, + ); + next.head.scale = Vec3::one() * s_a.head_scale; + next.chest.scale = Vec3::one() * 1.01; + next.hand_l.scale = Vec3::one() * 1.04; + next.hand_r.scale = Vec3::one() * 1.04; + next.back.scale = Vec3::one() * 1.02; + next.hold.scale = Vec3::one() * 0.0; + next.lantern.scale = Vec3::one() * 0.65; + next.shoulder_l.scale = Vec3::one() * 1.1; + next.shoulder_r.scale = Vec3::one() * 1.1; + + next.head.position = Vec3::new(0.0, s_a.head.0, s_a.head.1 + slow * 0.3); + next.head.orientation = + Quaternion::rotation_z(head_look.x) * Quaternion::rotation_x(head_look.y.abs()); + + next.chest.position = Vec3::new(0.0, s_a.chest.0, s_a.chest.1 + slow * 0.3); + next.chest.orientation = Quaternion::rotation_z(head_look.x * 0.06); + + next.belt.position = Vec3::new(0.0, s_a.belt.0, s_a.belt.1); + next.belt.orientation = Quaternion::rotation_z(head_look.x * -0.1); + + next.back.position = Vec3::new(0.0, s_a.back.0, s_a.back.1); + + next.shorts.position = Vec3::new(0.0, s_a.shorts.0, s_a.shorts.1); + next.shorts.orientation = Quaternion::rotation_z(head_look.x * -0.2); + + next.hand_l.position = Vec3::new( + -s_a.hand.0, + s_a.hand.1 + slow * 0.15, + s_a.hand.2 + slow * 0.5, + ); + + let helm_center = Vec3::new(0.0, 0.6, 0.75) / s_a.scaler * 11.0; + + let rot = steer_dir * 0.5; + + let hand_rotation = Quaternion::rotation_y(rot) * Quaternion::rotation_x(PI / 2.0); + + let hand_offset = Vec3::new(rot.cos(), 0.0, -rot.sin()) * 0.4 / s_a.scaler * 11.0; + + next.hand_l.position = helm_center - hand_offset; + next.hand_r.position = helm_center + hand_offset; + + let ori_l = Quaternion::rotation_x( + PI / 2.0 + (next.hand_l.position.z / next.hand_l.position.x).atan(), + ); + let ori_r = Quaternion::rotation_x( + PI / 2.0 - (next.hand_r.position.z / next.hand_r.position.x).atan(), + ); + + next.hand_l.orientation = hand_rotation * ori_l; + next.hand_r.orientation = -hand_rotation * ori_r; + + next.shoulder_l.position = Vec3::new(-s_a.shoulder.0, s_a.shoulder.1, s_a.shoulder.2); + next.shoulder_l.orientation = ori_r; + next.shoulder_r.position = Vec3::new(s_a.shoulder.0, s_a.shoulder.1, s_a.shoulder.2); + next.shoulder_r.orientation = ori_l; + + next.foot_l.position = Vec3::new(-s_a.foot.0, s_a.foot.1, s_a.foot.2); + next.foot_l.orientation = Quaternion::identity(); + + next.foot_r.position = Vec3::new(s_a.foot.0, s_a.foot.1, s_a.foot.2); + next.foot_r.orientation = Quaternion::identity(); + + next.glider.position = Vec3::new(0.0, 0.0, 10.0); + next.glider.scale = Vec3::one() * 0.0; + next.hold.position = Vec3::new(0.4, -0.3, -5.8); + + if skeleton.holding_lantern { + next.hand_r.position = Vec3::new( + s_a.hand.0 - head_look.x * 6.0, + s_a.hand.1 + 5.0 - head_look.y * 10.0 + slow * 0.15, + s_a.hand.2 + 12.0 + head_look.y * 6.0 + slow * 0.5, + ); + next.hand_r.orientation = Quaternion::rotation_x(2.25 + slow * -0.06) + * Quaternion::rotation_z(0.9) + * Quaternion::rotation_y(head_look.x * 1.5) + * Quaternion::rotation_x(head_look.y * 1.5); + + next.lantern.position = Vec3::new(-0.5, -0.5, -2.5); + next.lantern.orientation = next.hand_r.orientation.inverse(); + } + + next.torso.position = Vec3::new(0.0, 0.0, 0.0); + + next + } +} diff --git a/voxygen/src/game_input.rs b/voxygen/src/game_input.rs index e84a93f88c..0a494a1b66 100644 --- a/voxygen/src/game_input.rs +++ b/voxygen/src/game_input.rs @@ -170,6 +170,8 @@ pub enum GameInput { MuteSfx, #[strum(serialize = "gameinput-muteambience")] MuteAmbience, + #[strum(serialize = "gameinput-togglewalk")] + ToggleWalk, } impl GameInput { diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 94045298f0..8d43e185e3 100755 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -342,6 +342,10 @@ widget_ids! { auto_walk_txt, auto_walk_bg, + // Walking speed indicator + walking_speed_txt, + walking_speed_bg, + // Temporal (fading) camera zoom lock indicator zoom_lock_txt, zoom_lock_bg, @@ -2149,6 +2153,7 @@ impl Hud { BlockInteraction::Mount => { let key = match block.get_sprite() { Some(SpriteKind::Helm) => "hud-steer", + Some(SpriteKind::Bed | SpriteKind::Bedroll | SpriteKind::BedrollSnow | SpriteKind::BedrollPirate) => "hud-lay", _ => "hud-sit", }; vec![(Some(GameInput::Mount), i18n.get_msg(key).to_string())] diff --git a/voxygen/src/hud/settings_window/gameplay.rs b/voxygen/src/hud/settings_window/gameplay.rs index 666f8ba573..d64d4614c6 100644 --- a/voxygen/src/hud/settings_window/gameplay.rs +++ b/voxygen/src/hud/settings_window/gameplay.rs @@ -31,6 +31,9 @@ widget_ids! { camera_clamp_slider, camera_clamp_label, camera_clamp_value, + walking_speed_slider, + walking_speed_label, + walking_speed_value, mouse_y_invert_button, mouse_y_invert_label, controller_y_invert_button, @@ -42,6 +45,8 @@ widget_ids! { free_look_behavior_list, auto_walk_behavior_text, auto_walk_behavior_list, + walking_speed_behavior_text, + walking_speed_behavior_list, camera_clamp_behavior_text, camera_clamp_behavior_list, zoom_lock_behavior_text, @@ -124,6 +129,7 @@ impl<'a> Widget for Gameplay<'a> { let display_pan = self.global_state.settings.gameplay.pan_sensitivity; let display_zoom = self.global_state.settings.gameplay.zoom_sensitivity; let display_clamp = self.global_state.settings.gameplay.camera_clamp_angle; + let display_walking_speed = self.global_state.settings.gameplay.walking_speed; // Mouse Pan Sensitivity Text::new( @@ -233,6 +239,38 @@ impl<'a> Widget for Gameplay<'a> { .color(TEXT_COLOR) .set(state.ids.camera_clamp_value, ui); + // Walking speed + Text::new(&self.localized_strings.get_msg("hud-settings-walking_speed")) + .down_from(state.ids.camera_clamp_slider, 10.0) + .font_size(self.fonts.cyri.scale(14)) + .font_id(self.fonts.cyri.conrod_id) + .color(TEXT_COLOR) + .set(state.ids.walking_speed_label, ui); + + if let Some(new_val) = ImageSlider::continuous( + display_walking_speed, + 0.0, + 1.0, + self.imgs.slider_indicator, + self.imgs.slider, + ) + .w_h(550.0, 22.0) + .down_from(state.ids.walking_speed_label, 10.0) + .track_breadth(30.0) + .slider_length(10.0) + .pad_track((5.0, 5.0)) + .set(state.ids.walking_speed_slider, ui) + { + events.push(AdjustWalkingSpeed(new_val)); + } + + Text::new(&format!("{:.2}", display_walking_speed)) + .right_from(state.ids.walking_speed_slider, 8.0) + .font_size(self.fonts.cyri.scale(14)) + .font_id(self.fonts.cyri.conrod_id) + .color(TEXT_COLOR) + .set(state.ids.walking_speed_value, ui); + // Zoom Inversion let zoom_inverted = ToggleButton::new( self.global_state.settings.gameplay.zoom_inversion, @@ -240,7 +278,7 @@ impl<'a> Widget for Gameplay<'a> { self.imgs.checkbox_checked, ) .w_h(18.0, 18.0) - .down_from(state.ids.camera_clamp_slider, 20.0) + .down_from(state.ids.walking_speed_slider, 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.mouse_zoom_invert_button, ui); @@ -420,13 +458,43 @@ impl<'a> Widget for Gameplay<'a> { } } + // Walking speed behavior + Text::new( + &self + .localized_strings + .get_msg("hud-settings-walking_speed_behavior"), + ) + .down_from(state.ids.free_look_behavior_list, 10.0) + .font_size(self.fonts.cyri.scale(14)) + .font_id(self.fonts.cyri.conrod_id) + .color(TEXT_COLOR) + .set(state.ids.walking_speed_behavior_text, ui); + + let walking_speed_selected = + self.global_state.settings.gameplay.walking_speed_behavior as usize; + + if let Some(clicked) = DropDownList::new(&mode_label_list, Some(walking_speed_selected)) + .w_h(200.0, 30.0) + .color(MENU_BG) + .label_color(TEXT_COLOR) + .label_font_id(self.fonts.cyri.conrod_id) + .down_from(state.ids.walking_speed_behavior_text, 8.0) + .set(state.ids.walking_speed_behavior_list, ui) + { + match clicked { + 0 => events.push(ChangeWalkingSpeedBehavior(PressBehavior::Toggle)), + 1 => events.push(ChangeWalkingSpeedBehavior(PressBehavior::Hold)), + _ => unreachable!(), + } + } + // Camera clamp behavior Text::new( &self .localized_strings .get_msg("hud-settings-camera_clamp_behavior"), ) - .down_from(state.ids.free_look_behavior_list, 10.0) + .down_from(state.ids.auto_walk_behavior_list, 10.0) .font_size(self.fonts.cyri.scale(14)) .font_id(self.fonts.cyri.conrod_id) .color(TEXT_COLOR) @@ -545,7 +613,7 @@ impl<'a> Widget for Gameplay<'a> { .localized_strings .get_msg("hud-settings-zoom_lock_behavior"), ) - .down_from(state.ids.auto_walk_behavior_list, 10.0) + .down_from(state.ids.walking_speed_behavior_list, 10.0) .font_size(self.fonts.cyri.scale(14)) .font_id(self.fonts.cyri.conrod_id) .color(TEXT_COLOR) @@ -599,7 +667,7 @@ impl<'a> Widget for Gameplay<'a> { .w_h(RESET_BUTTONS_WIDTH, RESET_BUTTONS_HEIGHT) .hover_image(self.imgs.button_hover) .press_image(self.imgs.button_press) - .down_from(state.ids.camera_clamp_behavior_list, 12.0) + .down_from(state.ids.zoom_lock_behavior_list, 12.0) .label( &self .localized_strings diff --git a/voxygen/src/key_state.rs b/voxygen/src/key_state.rs index 0b9dfc155d..9d517e5131 100644 --- a/voxygen/src/key_state.rs +++ b/voxygen/src/key_state.rs @@ -11,6 +11,7 @@ pub struct KeyState { pub swim_down: bool, pub fly: bool, pub auto_walk: bool, + pub speed_mul: f32, pub trade: bool, pub analog_matrix: Vec2, } @@ -28,6 +29,7 @@ impl Default for KeyState { swim_down: false, fly: false, auto_walk: false, + speed_mul: 1.0, trade: false, analog_matrix: Vec2::zero(), } @@ -42,9 +44,11 @@ impl KeyState { if self.up || self.auto_walk { 1.0 } else { 0.0 } + if self.down { -1.0 } else { 0.0 }, ) + .try_normalized() + .unwrap_or_default() } else { self.analog_matrix - }; + } * self.speed_mul; if dir.magnitude_squared() <= 1.0 { dir diff --git a/voxygen/src/scene/figure/mod.rs b/voxygen/src/scene/figure/mod.rs index 84f2d9d753..720baee7f0 100644 --- a/voxygen/src/scene/figure/mod.rs +++ b/voxygen/src/scene/figure/mod.rs @@ -1110,7 +1110,10 @@ impl FigureMgr { && matches!(active_tool_hand, Some(Hands::One))) || !character.map_or(false, |c| c.is_wield())) && !character.map_or(false, |c| c.is_using_hands()) - && physics.in_liquid().is_none(); + && physics.in_liquid().is_none() + && is_volume_rider.map_or(true, |volume_rider| { + !matches!(volume_rider.block.get_sprite(), Some(SpriteKind::Helm)) + }); let back_carry_offset = inventory .and_then(|i| i.equipped(EquipSlot::Armor(ArmorSlot::Back))) @@ -2137,7 +2140,26 @@ impl FigureMgr { { match sprite { SpriteKind::Helm => { - anim::character::DanceAnimation::update_skeleton( + anim::character::SteerAnimation::update_skeleton( + &target_base, + ( + active_tool_kind, + second_tool_kind, + character_activity + .map(|a| a.steer_dir) + .unwrap_or(0.0), + time, + ), + state.state_time, + &mut state_animation_rate, + skeleton_attr, + ) + }, + SpriteKind::Bed + | SpriteKind::Bedroll + | SpriteKind::BedrollSnow + | SpriteKind::BedrollPirate => { + anim::character::SleepAnimation::update_skeleton( &target_base, (active_tool_kind, second_tool_kind, time), state.state_time, diff --git a/voxygen/src/scene/terrain/watcher.rs b/voxygen/src/scene/terrain/watcher.rs index cfb9a8e338..0109c06715 100644 --- a/voxygen/src/scene/terrain/watcher.rs +++ b/voxygen/src/scene/terrain/watcher.rs @@ -207,7 +207,9 @@ impl BlocksOfInterest { ) .with_z(0.0), )), - Some(SpriteKind::Sign) => interactables.push((pos, Interaction::Read)), + Some(SpriteKind::Sign | SpriteKind::HangingSign) => { + interactables.push((pos, Interaction::Read)) + }, _ if block.is_mountable() => interactables.push((pos, Interaction::Mount)), _ => {}, }, diff --git a/voxygen/src/session/mod.rs b/voxygen/src/session/mod.rs index a8eee89fd5..41dc581495 100644 --- a/voxygen/src/session/mod.rs +++ b/voxygen/src/session/mod.rs @@ -101,6 +101,7 @@ pub struct SessionState { walk_right_dir: Vec2, free_look: bool, auto_walk: bool, + walking_speed: bool, camera_clamp: bool, zoom_lock: bool, is_aiming: bool, @@ -171,6 +172,7 @@ impl SessionState { walk_right_dir, free_look: false, auto_walk: false, + walking_speed: false, camera_clamp: false, zoom_lock: false, is_aiming: false, @@ -710,6 +712,7 @@ impl PlayState for SessionState { } match input { GameInput::Primary => { + self.walking_speed = false; let mut client = self.client.borrow_mut(); // Mine and build targets can be the same block. make building // take precedence. @@ -728,6 +731,7 @@ impl PlayState for SessionState { } }, GameInput::Secondary => { + self.walking_speed = false; let mut client = self.client.borrow_mut(); if let Some(build_target) = build_target.filter(|bt| { state && can_build && nearest_block_dist == Some(bt.distance) @@ -747,6 +751,7 @@ impl PlayState for SessionState { } }, GameInput::Block => { + self.walking_speed = false; self.client.borrow_mut().handle_input( InputKind::Block, state, @@ -755,6 +760,7 @@ impl PlayState for SessionState { ); }, GameInput::Roll => { + self.walking_speed = false; let mut client = self.client.borrow_mut(); if can_build { if state { @@ -779,12 +785,14 @@ impl PlayState for SessionState { } }, GameInput::Respawn => { + self.walking_speed = false; self.stop_auto_walk(); if state { self.client.borrow_mut().respawn(); } }, GameInput::Jump => { + self.walking_speed = false; self.client.borrow_mut().handle_input( InputKind::Jump, state, @@ -847,6 +855,7 @@ impl PlayState for SessionState { self.key_state.right = state }, GameInput::Glide => { + self.walking_speed = false; let is_trading = self.client.borrow().is_trading(); if state && !is_trading { if global_state.settings.gameplay.stop_auto_walk_on_input { @@ -877,7 +886,11 @@ impl PlayState for SessionState { }, GameInput::ToggleWield => { if state { - self.client.borrow_mut().toggle_wield(); + let mut client = self.client.borrow_mut(); + if client.is_wielding().is_some_and(|b| !b) { + self.walking_speed = false; + } + client.toggle_wield(); } }, GameInput::SwapLoadout => { @@ -1214,6 +1227,13 @@ impl PlayState for SessionState { } } }, + GameInput::ToggleWalk if state => { + global_state + .settings + .gameplay + .walking_speed_behavior + .update(state, &mut self.walking_speed, |_| {}); + }, _ => {}, } }, @@ -1423,6 +1443,12 @@ impl PlayState for SessionState { } } + if self.walking_speed { + self.key_state.speed_mul = global_state.settings.gameplay.walking_speed; + } else { + self.key_state.speed_mul = 1.0; + } + // Recompute dependents just in case some input modified the camera self.scene .camera_mut() diff --git a/voxygen/src/session/settings_change.rs b/voxygen/src/session/settings_change.rs index 7315059f54..c624996aeb 100644 --- a/voxygen/src/session/settings_change.rs +++ b/voxygen/src/session/settings_change.rs @@ -59,6 +59,7 @@ pub enum Gameplay { AdjustMousePan(u32), AdjustMouseZoom(u32), AdjustCameraClamp(u32), + AdjustWalkingSpeed(f32), ToggleControllerYInvert(bool), ToggleMouseYInvert(bool), @@ -68,6 +69,7 @@ pub enum Gameplay { ChangeFreeLookBehavior(PressBehavior), ChangeAutoWalkBehavior(PressBehavior), + ChangeWalkingSpeedBehavior(PressBehavior), ChangeCameraClampBehavior(PressBehavior), ChangeZoomLockBehavior(AutoPressBehavior), ChangeStopAutoWalkOnInput(bool), @@ -379,6 +381,9 @@ impl SettingsChange { Gameplay::AdjustCameraClamp(angle) => { settings.gameplay.camera_clamp_angle = angle; }, + Gameplay::AdjustWalkingSpeed(speed) => { + settings.gameplay.walking_speed = speed; + }, Gameplay::ToggleControllerYInvert(controller_y_inverted) => { window.controller_settings.pan_invert_y = controller_y_inverted; settings.controller.pan_invert_y = controller_y_inverted; @@ -400,6 +405,9 @@ impl SettingsChange { Gameplay::ChangeAutoWalkBehavior(behavior) => { settings.gameplay.auto_walk_behavior = behavior; }, + Gameplay::ChangeWalkingSpeedBehavior(behavior) => { + settings.gameplay.walking_speed_behavior = behavior; + }, Gameplay::ChangeCameraClampBehavior(behavior) => { settings.gameplay.camera_clamp_behavior = behavior; }, diff --git a/voxygen/src/settings/control.rs b/voxygen/src/settings/control.rs index c7876901b7..a3a123def2 100644 --- a/voxygen/src/settings/control.rs +++ b/voxygen/src/settings/control.rs @@ -200,6 +200,7 @@ impl ControlSettings { GameInput::MuteMusic => Some(KeyMouse::Key(VirtualKeyCode::F8)), GameInput::MuteSfx => None, GameInput::MuteAmbience => None, + GameInput::ToggleWalk => Some(KeyMouse::Key(VirtualKeyCode::I)), } } } diff --git a/voxygen/src/settings/gameplay.rs b/voxygen/src/settings/gameplay.rs index 6a8fe35ce1..ebb06d6627 100644 --- a/voxygen/src/settings/gameplay.rs +++ b/voxygen/src/settings/gameplay.rs @@ -8,11 +8,13 @@ pub struct GameplaySettings { pub pan_sensitivity: u32, pub zoom_sensitivity: u32, pub camera_clamp_angle: u32, + pub walking_speed: f32, pub zoom_inversion: bool, pub mouse_y_inversion: bool, pub smooth_pan_enable: bool, pub free_look_behavior: PressBehavior, pub auto_walk_behavior: PressBehavior, + pub walking_speed_behavior: PressBehavior, pub camera_clamp_behavior: PressBehavior, pub zoom_lock_behavior: AutoPressBehavior, pub stop_auto_walk_on_input: bool, @@ -27,11 +29,13 @@ impl Default for GameplaySettings { pan_sensitivity: 100, zoom_sensitivity: 100, camera_clamp_angle: 45, + walking_speed: 0.35, zoom_inversion: false, mouse_y_inversion: false, smooth_pan_enable: false, free_look_behavior: PressBehavior::Toggle, auto_walk_behavior: PressBehavior::Toggle, + walking_speed_behavior: PressBehavior::Toggle, camera_clamp_behavior: PressBehavior::Toggle, zoom_lock_behavior: AutoPressBehavior::Auto, stop_auto_walk_on_input: true, diff --git a/world/Cargo.toml b/world/Cargo.toml index dba7579d06..353524e425 100644 --- a/world/Cargo.toml +++ b/world/Cargo.toml @@ -21,6 +21,7 @@ common-dynlib = {package = "veloren-common-dynlib", path = "../common/dynlib", o bincode = { workspace = true } bitvec = "1.0.1" enum-map = { workspace = true } +enumset = "1.1.3" fxhash = { workspace = true } image = { workspace = true } itertools = { workspace = true } diff --git a/world/src/civ/mod.rs b/world/src/civ/mod.rs index 4f4609eaaa..89424214d1 100644 --- a/world/src/civ/mod.rs +++ b/world/src/civ/mod.rs @@ -539,6 +539,7 @@ impl Civs { let size = Lerp::lerp(0.03, 1.0, rng.gen_range(0.0..1f32).powi(5)); WorldSite::refactor(site2::Site::generate_city( &Land::from_sim(ctx.sim), + index_ref, &mut rng, wpos, size, diff --git a/world/src/site/namegen.rs b/world/src/site/namegen.rs index 7ffde99f91..b1911fdbe4 100644 --- a/world/src/site/namegen.rs +++ b/world/src/site/namegen.rs @@ -689,4 +689,168 @@ impl<'a, R: Rng> NameGen<'a, R> { ]; self.generate_theme_from_parts(&start, &middle, &vowel, &end) } + + pub fn generate_tavern(&mut self) -> String { + let adjectives = [ + "Crazy", + "Big", + "Tiny", + "Slimy", + "Warm", + "Rigid", + "Soft", + "Wet", + "Humid", + "Smelly", + "Hidden", + "Smart", + "Fragile", + "Strong", + "Weak", + "Happy", + "Sad", + "Glad", + "Scared", + "Embarrassed", + "Goofy", + "Spicy", + "Salty", + "Peaceful", + "Awful", + "Sweet", + "Colossal", + "Puzzled", + "Cheap", + "Valuable", + "Rich", + "Obnoxious", + "Puzzled", + "Snoring", + "Fast", + "Quick", + "Magical", + "Violet", + "Red", + "Blue", + "Green", + "Yellow", + "Golden", + "Shiny", + "Tired", + "Twin", + "Incompetent", + "Light", + "Dark", + "Glorious", + "Best", + "Free", + "Odd", + "Juicy", + "Shaking", + "Tall", + "Short", + "Precious", + "Regular", + "Slow", + "Anxious", + "Naive", + "Sore", + "Next", + "Silver", + "Secret", + "Honorable", + "Rapid", + "Sleepy", + "Lying", + "Zesty", + "Fancy", + "Stylish", + "Thirsty", + "Dry", + "Dancing", + "Singing", + "Drunken", + ]; + let tavern_synonyms = ["Tavern", "Bar", "Pub"]; + let subjectives = [ + "Apple", + "Pumpkin", + "Cucumber", + "Squash", + "Demons", + "Mango", + "Coconut", + "Cats", + "Hill", + "Mountain", + "Squirrel", + "Rabbit", + "Moose", + "Driggle", + "Iron", + "Velorite", + "Plate", + "Eagle", + "Birds", + "Drumstick", + "Dog", + "Tiger", + "Knight", + "Leader", + "Huntress", + "Hunter", + "Dwarf", + "Toad", + "Clams", + "Bell", + "Avocado", + "Egg", + "Spade", + "Stream", + "Cabbage", + "Tomato", + "Rapier", + "Katana", + "Whisper", + "Hammer", + "Axe", + "Sword", + "Saurok", + "Danari", + "Elf", + "Human", + "Draugr", + "Orc", + "Pie", + "Stick", + "Rope", + "Knife", + "Shield", + "Bow", + "Spear", + "Staff", + "Crow", + "Crown", + "Parrot", + "Parrots", + "Pelican", + "Whale", + "Cube", + "Minotaur", + "Oni", + "Monster", + ]; + let kind = self.rng.gen_range(0..10); + let mut choose = |slice: &[&'static str]| *slice.choose(self.rng).unwrap(); + match kind { + 0 => format!("The {} {}", choose(&adjectives), choose(&tavern_synonyms)), + 1..=7 => format!("The {} {}", choose(&adjectives), choose(&subjectives)), + _ => format!( + "The {} {} {}", + choose(&adjectives), + choose(&subjectives), + choose(&tavern_synonyms) + ), + } + } } diff --git a/world/src/site2/mod.rs b/world/src/site2/mod.rs index a9cfbd08ac..4c252a0d56 100644 --- a/world/src/site2/mod.rs +++ b/world/src/site2/mod.rs @@ -573,7 +573,13 @@ impl Site { } // Size is 0..1 - pub fn generate_city(land: &Land, rng: &mut impl Rng, origin: Vec2, size: f32) -> Self { + pub fn generate_city( + land: &Land, + index: IndexRef, + rng: &mut impl Rng, + origin: Vec2, + size: f32, + ) -> Self { let mut rng = reseed(rng); let mut site = Site { @@ -593,6 +599,7 @@ impl Site { (5.0, 4), (5.0, 5), (15.0, 6), + (15.0, 7), ]); let mut castles = 0; @@ -600,6 +607,8 @@ impl Site { let mut workshops = 0; let mut airship_docks = 0; + + let mut taverns = 0; for _ in 0..(size * 200.0) as i32 { match *build_chance.choose_seeded(rng.gen()) { // Workshop @@ -917,6 +926,43 @@ impl Site { } } }, + 7 if (size > 0.125 && taverns < 2) => { + let size = (3.5 + rng.gen::().powf(5.0) * 2.0).round() as u32; + if let Some((aabr, door_tile, door_dir)) = attempt(32, || { + site.find_roadside_aabr( + &mut rng, + 7..(size + 1).pow(2), + Extent2::broadcast(size), + ) + }) { + let tavern = plot::Tavern::generate( + land, + index, + &mut reseed(&mut rng), + &site, + door_tile, + Dir::from_vec2(door_dir), + aabr, + ); + let tavern_alt = tavern.door_wpos.z; + let plot = site.create_plot(Plot { + kind: PlotKind::Tavern(tavern), + root_tile: aabr.center(), + tiles: aabr_tiles(aabr).collect(), + seed: rng.gen(), + }); + + site.blit_aabr(aabr, Tile { + kind: TileKind::Building, + plot: Some(plot), + hard_alt: Some(tavern_alt), + }); + + taverns += 1; + } else { + site.make_plaza(land, &mut rng); + } + }, _ => {}, } } @@ -1824,6 +1870,7 @@ impl Site { let (prim_tree, fills, mut entities) = match &self.plots[plot].kind { PlotKind::House(house) => house.render_collect(self, canvas), PlotKind::AirshipDock(airship_dock) => airship_dock.render_collect(self, canvas), + PlotKind::Tavern(tavern) => tavern.render_collect(self, canvas), PlotKind::CoastalHouse(coastal_house) => coastal_house.render_collect(self, canvas), PlotKind::CoastalWorkshop(coastal_workshop) => { coastal_workshop.render_collect(self, canvas) @@ -1963,7 +2010,19 @@ impl Site { } pub fn test_site() -> Site { - Site::generate_city(&Land::empty(), &mut thread_rng(), Vec2::zero(), 0.5) + let index = crate::index::Index::new(0); + let index_ref = IndexRef { + colors: &index.colors(), + features: &index.features(), + index: &index, + }; + Site::generate_city( + &Land::empty(), + index_ref, + &mut thread_rng(), + Vec2::zero(), + 0.5, + ) } fn wpos_is_hazard(land: &Land, wpos: Vec2) -> Option { diff --git a/world/src/site2/plot.rs b/world/src/site2/plot.rs index 15693441fc..ccb9118aff 100644 --- a/world/src/site2/plot.rs +++ b/world/src/site2/plot.rs @@ -22,6 +22,7 @@ mod savannah_hut; mod savannah_pit; mod savannah_workshop; mod sea_chapel; +pub mod tavern; mod troll_cave; mod workshop; @@ -34,7 +35,7 @@ pub use self::{ gnarling::GnarlingFortification, house::House, jungle_ruin::JungleRuin, pirate_hideout::PirateHideout, rock_circle::RockCircle, savannah_hut::SavannahHut, savannah_pit::SavannahPit, savannah_workshop::SavannahWorkshop, sea_chapel::SeaChapel, - troll_cave::TrollCave, workshop::Workshop, + tavern::Tavern, troll_cave::TrollCave, workshop::Workshop, }; use super::*; @@ -77,6 +78,7 @@ impl Plot { pub enum PlotKind { House(House), AirshipDock(AirshipDock), + Tavern(Tavern), CoastalHouse(CoastalHouse), CoastalWorkshop(CoastalWorkshop), Workshop(Workshop), diff --git a/world/src/site2/plot/adlet.rs b/world/src/site2/plot/adlet.rs index c333b6a756..4478520af8 100644 --- a/world/src/site2/plot/adlet.rs +++ b/world/src/site2/plot/adlet.rs @@ -129,7 +129,7 @@ impl AdletStronghold { let mut outer_structures = Vec::<(AdletStructure, Vec2, Dir)>::new(); - let entrance_dir = Dir::from_vector(entrance - cavern_center); + let entrance_dir = Dir::from_vec2(entrance - cavern_center); outer_structures.push((AdletStructure::TunnelEntrance, Vec2::zero(), entrance_dir)); let desired_structures = surface_radius.pow(2) / 100; @@ -176,7 +176,7 @@ impl AdletStronghold { Some((structure_center, structure_kind)) } }) { - let dir_to_wall = Dir::from_vector(rpos); + let dir_to_wall = Dir::from_vec2(rpos); let door_rng: u32 = rng.gen_range(0..9); let door_dir = match door_rng { 0..=3 => dir_to_wall, @@ -352,7 +352,7 @@ impl AdletStronghold { .then_some((structure, rpos)) }) { // Direction facing the central bonfire - let dir = Dir::from_vector(rpos).opposite(); + let dir = Dir::from_vec2(rpos).opposite(); cavern_structures.push((structure, rpos, dir)); } } @@ -493,7 +493,7 @@ impl Structure for AdletStronghold { // Tunnel let dist: f32 = self.cavern_center.as_().distance(self.entrance.as_()); - let dir = Dir::from_vector(self.entrance - self.cavern_center); + let dir = Dir::from_vec2(self.entrance - self.cavern_center); let tunnel_start: Vec3 = match dir { Dir::X => Vec2::new(self.entrance.x + 7, self.entrance.y), Dir::Y => Vec2::new(self.entrance.x, self.entrance.y + 7), diff --git a/world/src/site2/plot/bridge.rs b/world/src/site2/plot/bridge.rs index 44bda8eac1..c4d0575288 100644 --- a/world/src/site2/plot/bridge.rs +++ b/world/src/site2/plot/bridge.rs @@ -227,7 +227,7 @@ fn render_flat(bridge: &Bridge, painter: &Painter) { } .made_valid(); - let [ramp_aabr, aabr] = bridge.dir.split_aabr(aabr, height); + let [ramp_aabr, aabr] = bridge.dir.split_aabr_offset(aabr, height); let ramp_prim = |ramp_aabr: Aabr, offset: i32| { painter @@ -254,7 +254,7 @@ fn render_flat(bridge: &Bridge, painter: &Painter) { let vault_offset = 5; let bridge_thickness = 4; - let [vault, _] = bridge.dir.split_aabr(aabr, vault_width); + let [vault, _] = bridge.dir.split_aabr_offset(aabr, vault_width); let len = bridge.dir.select(aabr.size()); let true_offset = vault_width + vault_offset; @@ -321,8 +321,11 @@ fn render_heightened_viaduct(bridge: &Bridge, painter: &Painter, data: &Heighten } .made_valid(); - let [_start_aabr, rest] = bridge.dir.split_aabr(aabr, bridge_start_z - bridge.start.z); - let [_end_aabr, bridge_aabr] = (-bridge.dir).split_aabr(rest, bridge_start_z - bridge.end.z); + let [_start_aabr, rest] = bridge + .dir + .split_aabr_offset(aabr, bridge_start_z - bridge.start.z); + let [_end_aabr, bridge_aabr] = + (-bridge.dir).split_aabr_offset(rest, bridge_start_z - bridge.end.z); let under = bridge.center.z - 15; let bridge_prim = |bridge_width: i32| { @@ -334,11 +337,14 @@ fn render_heightened_viaduct(bridge: &Bridge, painter: &Painter, data: &Heighten } .made_valid(); - let [start_aabr, rest] = bridge.dir.split_aabr(aabr, bridge_start_z - bridge.start.z); - let [end_aabr, bridge_aabr] = (-bridge.dir).split_aabr(rest, bridge_start_z - bridge.end.z); + let [start_aabr, rest] = bridge + .dir + .split_aabr_offset(aabr, bridge_start_z - bridge.start.z); + let [end_aabr, bridge_aabr] = + (-bridge.dir).split_aabr_offset(rest, bridge_start_z - bridge.end.z); let [bridge_start, bridge_end] = bridge .dir - .split_aabr(bridge_aabr, bridge.dir.select(bridge_aabr.size()) / 2); + .split_aabr_offset(bridge_aabr, bridge.dir.select(bridge_aabr.size()) / 2); let ramp_in_aabr = |aabr: Aabr, dir: Dir, zmin, zmax| { let inset = dir.select(aabr.size()); @@ -592,7 +598,7 @@ fn render_tower(bridge: &Bridge, painter: &Painter, roof_kind: &RoofKind) { let aabr = bridge .dir .rotated_cw() - .split_aabr(tower_aabr, stair_thickness + 1)[1]; + .split_aabr_offset(tower_aabr, stair_thickness + 1)[1]; painter .aabb(aabb( @@ -748,7 +754,7 @@ fn render_hang(bridge: &Bridge, painter: &Painter) { let top_offset = 4; let top = bridge.end.z + top_offset; - let [ramp_f, aabr] = bridge.dir.split_aabr(aabr, top - bridge.start.z + 1); + let [ramp_f, aabr] = bridge.dir.split_aabr_offset(aabr, top - bridge.start.z + 1); painter .aabb(aabb( @@ -764,7 +770,7 @@ fn render_hang(bridge: &Bridge, painter: &Painter) { ) .fill(rock.clone()); - let [ramp_b, aabr] = (-bridge.dir).split_aabr(aabr, top_offset + 1); + let [ramp_b, aabr] = (-bridge.dir).split_aabr_offset(aabr, top_offset + 1); painter .aabb(aabb( ramp_b.min.with_z(bridge.end.z - 10), @@ -878,7 +884,7 @@ impl Bridge { let min_water_dist = 5; let find_edge = |start: Vec2, end: Vec2| { let mut test_start = start; - let dir = Dir::from_vector(end - start).to_vec2(); + let dir = Dir::from_vec2(end - start).to_vec2(); let mut last_alt = if let Some(col) = land.column_sample(start, index) { col.alt as i32 } else { @@ -932,7 +938,7 @@ impl Bridge { start, end, center, - dir: Dir::from_vector(end.xy() - start.xy()), + dir: Dir::from_vec2(end.xy() - start.xy()), kind: bridge, biome: land .get_chunk_wpos(center.xy()) diff --git a/world/src/site2/plot/gnarling.rs b/world/src/site2/plot/gnarling.rs index ce25e7ad2a..5e4fff446b 100644 --- a/world/src/site2/plot/gnarling.rs +++ b/world/src/site2/plot/gnarling.rs @@ -237,7 +237,7 @@ impl GnarlingFortification { )) } }) { - let dir_to_center = Dir::from_vector(hut_loc.xy()).opposite(); + let dir_to_center = Dir::from_vec2(hut_loc.xy()).opposite(); let door_rng: u32 = rng.gen_range(0..9); let door_dir = match door_rng { 0..=3 => dir_to_center, @@ -262,7 +262,7 @@ impl GnarlingFortification { let chieftain_hut_loc = ((inner_tower_locs[0] + inner_tower_locs[1]) + 2 * outer_wall_corners[chieftain_indices[1]]) / 4; - let chieftain_hut_ori = Dir::from_vector(chieftain_hut_loc).opposite(); + let chieftain_hut_ori = Dir::from_vec2(chieftain_hut_loc).opposite(); structure_locations.push(( GnarlingStructure::ChieftainHut, chieftain_hut_loc.with_z(rpos_height(chieftain_hut_loc)), diff --git a/world/src/site2/plot/tavern.rs b/world/src/site2/plot/tavern.rs new file mode 100644 index 0000000000..672c0f846b --- /dev/null +++ b/world/src/site2/plot/tavern.rs @@ -0,0 +1,1588 @@ +use std::{mem::swap, ops::RangeInclusive}; + +use common::{ + comp::Content, + lottery::Lottery, + store::{Id, Store}, + terrain::{BlockKind, SpriteCfg, SpriteKind}, +}; +use enum_map::EnumMap; +use enumset::EnumSet; +use hashbrown::HashSet; +use rand::{seq::IteratorRandom, Rng}; +use strum::{EnumIter, IntoEnumIterator}; +use vek::*; + +use crate::{ + site::namegen, + site2::{gen::PrimitiveTransform, Dir, Fill, Site, Structure}, + util::RandomField, + IndexRef, Land, +}; + +type Neighbor = Option>; + +pub struct Wall { + start: Vec2, + end: Vec2, + base_alt: i32, + top_alt: i32, + from: Neighbor, + to: Neighbor, + to_dir: Dir, + door: Option<(i32, i32)>, +} + +impl Wall { + pub fn door_pos(&self) -> Option> { + let wall_dir = Dir::from_vec2(self.end - self.start); + + self.door.map(|(door_min, door_max)| { + (self.start.as_() + wall_dir.to_vec2().as_() * (door_min + door_max) as f32 / 2.0 + 0.5) + .with_z(self.base_alt as f32) + }) + } + + pub fn door_bounds(&self) -> Option> { + let wall_dir = Dir::from_vec2(self.end - self.start); + + self.door.map(|(door_min, door_max)| { + Aabr { + min: self.start + wall_dir.to_vec2() * door_min, + max: self.start + wall_dir.to_vec2() * door_max, + } + .made_valid() + }) + } +} + +#[derive(Copy, Clone)] +enum RoofStyle { + Flat, + FlatBars { dir: Dir }, + LeanTo { dir: Dir, max_z: i32 }, + Gable { dir: Dir, max_z: i32 }, + Hip { max_z: i32 }, +} + +struct Roof { + bounds: Aabr, + min_z: i32, + style: RoofStyle, +} + +#[derive(Clone, Copy, EnumIter, enum_map::Enum)] +enum RoomKind { + Garden, + StageRoom, + BarRoom, + EntranceRoom, +} + +impl RoomKind { + /// Returns the (side length size range, area size range) + fn size_range(&self) -> (RangeInclusive, RangeInclusive) { + match self { + RoomKind::Garden => (4..=20, 25..=250), + RoomKind::StageRoom => (10..=20, 130..=400), + RoomKind::BarRoom => (7..=14, 56..=196), + RoomKind::EntranceRoom => (3..=10, 9..=50), + } + } +} + +#[derive(Clone, Copy)] +pub enum Detail { + Bar { + aabr: Aabr, + }, + Table { + pos: Vec2, + chairs: EnumSet, + }, + Stage { + aabr: Aabr, + }, +} + +pub struct Room { + /// Inclusive + pub bounds: Aabb, + kind: RoomKind, + // stairs: Option>, + walls: EnumMap>>, + roofs: Vec>, + detail_areas: Vec>, + pub details: Vec, +} + +impl Room { + fn new(bounds: Aabb, kind: RoomKind) -> Self { + Self { + bounds, + kind, + roofs: Default::default(), + walls: Default::default(), + detail_areas: Default::default(), + details: Default::default(), + } + } + + /// Are any of this rooms roofs fully covering it? + fn is_covered_by_roof(&self, roofs: &Store) -> bool { + let aabr = Aabr { + min: self.bounds.min.xy(), + max: self.bounds.max.xy(), + }; + for roof in self.roofs.iter() { + if roofs[*roof].bounds.contains_aabr(aabr) { + return true; + } + } + false + } +} + +pub struct Tavern { + name: String, + pub rooms: Store, + walls: Store, + roofs: Store, + /// Tile position of the door tile + pub door_tile: Vec2, + pub door_wpos: Vec3, + /// Axis aligned bounding region for the house + pub bounds: Aabr, +} + +impl Tavern { + pub fn generate( + land: &Land, + _index: IndexRef, + rng: &mut impl Rng, + site: &Site, + door_tile: Vec2, + door_dir: Dir, + tile_aabr: Aabr, + ) -> Self { + let name = namegen::NameGen::location(rng).generate_tavern(); + + let mut rooms = Store::default(); + let mut walls = Store::default(); + let mut roofs = Store::default(); + let mut room_counts = EnumMap::::default(); + + let bounds = Aabr { + min: site.tile_wpos(tile_aabr.min), + max: site.tile_wpos(tile_aabr.max), + }; + + let ibounds = Aabr { + min: bounds.min + 1, + max: bounds.max - 2, + }; + + let door_tile_center = site.tile_center_wpos(door_tile); + let door_wpos = door_dir.select_aabr_with(ibounds, door_tile_center); + + let door_alt = land.get_alt_approx(door_wpos); + let door_wpos = door_wpos.with_z(door_alt.ceil() as i32); + + /// Place room in bounds. + fn place_room_in( + room: RoomKind, + max_bounds: Aabr, + in_dir: Dir, + in_pos: Vec2, + rng: &mut impl Rng, + ) -> Option> { + let (size_range, area_range) = room.size_range(); + + let mut gen_range = |min, max, snap_max| { + let res = rng.gen_range(min..=max); + if snap_max <= max && snap_max - res <= 2 { + snap_max + } else { + res + } + }; + let min = *size_range.start(); + let snap_max = in_dir.select(max_bounds.size()); + let max = snap_max.min(*size_range.end()); + if max < min { + return None; + } + let size_x = gen_range(min, max, snap_max); + + let min = ((*area_range.start() + size_x - 1) / size_x).max(*size_range.start()); + let snap_max = in_dir.orthogonal().select(max_bounds.size()); + let max = snap_max + .min(*size_range.end()) + .min(*area_range.end() / size_x); + + if max < min { + return None; + } + let size_y = gen_range(min, max, snap_max); + + // calculate a valid aabr + let half_size_y = size_y / 2 + (size_y % 2) * rng.gen_range(0..=1); + let min = in_pos + in_dir.to_vec2() + in_dir.rotated_cw().to_vec2() * half_size_y; + let min = max_bounds.projected_point(min); + let max = min + in_dir.to_vec2() * size_x + in_dir.rotated_ccw().to_vec2() * size_y; + let max = max_bounds.projected_point(max); + let min = max - in_dir.to_vec2() * size_x + in_dir.rotated_cw().to_vec2() * size_y; + + let bounds = Aabr { min, max }.made_valid(); + Some(bounds) + } + struct RoomMeta { + id: Id, + walls: EnumSet, + } + + let mut room_metas = Vec::new(); + + { + let entrance_rooms = + Lottery::from(vec![(1.0, RoomKind::Garden), (2.0, RoomKind::EntranceRoom)]); + + let entrance_room = *entrance_rooms.choose_seeded(rng.gen()); + let entrance_room_hgt = rng.gen_range(3..=4); + let entrance_room_aabr = + place_room_in(entrance_room, ibounds, -door_dir, door_wpos.xy(), rng) + .expect("Not enough room in plot for a tavern"); + let entrance_room_aabb = Aabb { + min: entrance_room_aabr.min.with_z(door_wpos.z), + max: entrance_room_aabr + .max + .with_z(door_wpos.z + entrance_room_hgt), + } + .made_valid(); + + let entrance_id = rooms.insert(Room::new(entrance_room_aabb, entrance_room)); + + let start = door_dir.select_aabr_with( + entrance_room_aabr, + Vec2::broadcast(door_dir.rotated_cw().select_aabr(entrance_room_aabr)), + ) + door_dir.rotated_cw().to_vec2() + + door_dir.to_vec2(); + let door_center = door_dir.rotated_cw().select(door_wpos.xy() - start).abs(); + let wall_id = walls.insert(Wall { + start, + end: door_dir.select_aabr_with( + entrance_room_aabr, + Vec2::broadcast(door_dir.rotated_ccw().select_aabr(entrance_room_aabr)), + ) + door_dir.rotated_ccw().to_vec2() + + door_dir.to_vec2(), + base_alt: entrance_room_aabb.min.z, + top_alt: entrance_room_aabb.max.z, + from: None, + to: Some(entrance_id), + to_dir: -door_dir, + door: Some((door_center - 1, door_center + 1)), + }); + rooms[entrance_id].walls[door_dir].push(wall_id); + + room_metas.push(RoomMeta { + id: entrance_id, + walls: Dir::iter().filter(|d| *d != door_dir).collect(), + }); + + room_counts[entrance_room] += 1; + } + + let to_aabr = |aabb: Aabb| Aabr { + min: aabb.min.xy(), + max: aabb.max.xy(), + }; + // Extend a valid aabr + let extend_aabr = |aabr: Aabr, amount: i32| Aabr { + min: aabr.min - amount, + max: aabr.max + amount, + }; + 'room_gen: while !room_metas.is_empty() { + // Continue extending from a random existing room + let mut room_meta = room_metas.swap_remove(rng.gen_range(0..room_metas.len())); + if room_meta.walls.is_empty() { + continue 'room_gen; + } + + // Pick a direction to choose from + let Some(in_dir) = room_meta.walls.into_iter().choose(rng) else { + continue 'room_gen; + }; + room_meta.walls.remove(in_dir); + + let right = in_dir.orthogonal(); + let left = -right; + + let from_id = room_meta.id; + let from_room = &rooms[from_id]; + + // If there are more directions to continue from, push this room again. + if !room_meta.walls.is_empty() { + room_metas.push(room_meta); + } + + let from_bounds = to_aabr(from_room.bounds); + + // The maximum bounds, limited by the plot bounds and other rooms. + let mut max_bounds = Aabr { + min: in_dir.select_aabr_with(from_bounds, ibounds.min) + in_dir.to_vec2() * 2, + max: in_dir.select_aabr_with(ibounds, ibounds.max), + } + .made_valid(); + // Pick a height of the new room + let room_hgt = rng.gen_range(3..=5); + let wanted_alt = land.get_alt_approx(max_bounds.center()) as i32 + 1; + let max_stair_length = (in_dir.select(if wanted_alt < from_room.bounds.min.z { + from_bounds.size() + } else { + max_bounds.size() + }) / 2) + .min(5); + let alt = wanted_alt.clamp( + from_room.bounds.min.z - max_stair_length, + from_room.bounds.min.z + max_stair_length, + ); + let min_z = from_room.bounds.min.z.min(alt); + let max_z = from_room.bounds.max.z.max(alt + room_hgt); + + // Take other rooms into account when calculating `max_bounds`. We don't care + // about this room if it's the originating room or at another + // height. + for (_, room) in rooms.iter().filter(|(room_id, room)| { + *room_id != from_id + && room.bounds.min.z - 1 <= max_z + && room.bounds.max.z + 1 >= min_z + }) { + let bounds = to_aabr(room.bounds); + let bounds = extend_aabr(bounds, 2); + let intersection = bounds.intersection(max_bounds); + if intersection.is_valid() { + // Find the direction to shrink in that yields the highest area. + let Some(bounds) = Dir::iter() + .filter(|dir| { + *dir != in_dir + && dir.select_aabr(intersection) * dir.signum() + < dir.select_aabr(max_bounds) * dir.signum() + }) + .map(|min_dir| { + Aabr { + min: min_dir.select_aabr_with( + max_bounds, + Vec2::broadcast(min_dir.rotated_ccw().select_aabr(max_bounds)), + ), + max: min_dir.select_aabr_with( + intersection, + Vec2::broadcast(min_dir.rotated_cw().select_aabr(max_bounds)), + ), + } + .made_valid() + }) + .filter(|bounds| { + left.select_aabr(*bounds) < right.select_aabr(from_bounds) + && right.select_aabr(*bounds) > left.select_aabr(from_bounds) + }) + .max_by_key(|bounds| bounds.size().product()) + else { + continue 'room_gen; + }; + + max_bounds = bounds; + } + } + + // the smallest side on the maximum bounds + let max_min_size = max_bounds.size().reduce_min(); + // max bounds area + let max_area = max_bounds.size().product(); + + let room_lottery = RoomKind::iter() + // Filter out rooms that won't fit here. + .filter(|room_kind| { + let (size_range, area_range) = room_kind.size_range(); + *size_range.start() <= max_min_size && *area_range.start() <= max_area + }) + // Calculate chance for each room. + .map(|room_kind| { + ( + match room_kind { + RoomKind::Garden => { + 0.5 / (1.0 + room_counts[RoomKind::Garden] as f32 * 0.8) + }, + RoomKind::StageRoom => { + 2.0 / (1.0 + room_counts[RoomKind::StageRoom] as f32).powi(2) + }, + RoomKind::BarRoom => { + 2.0 / (1.0 + room_counts[RoomKind::BarRoom] as f32).powi(2) + }, + RoomKind::EntranceRoom => { + 0.05 / (1.0 + room_counts[RoomKind::EntranceRoom] as f32) + }, + }, + room_kind, + ) + }) + .collect::>(); + // We have no rooms to pick from. + if room_lottery.is_empty() { + continue 'room_gen; + } + + // Pick a room. + let room_lottery = Lottery::from(room_lottery); + let room_kind = *room_lottery.choose_seeded(rng.gen()); + + // Select a door position + let mut min = left + .select_aabr(from_bounds) + .max(left.select_aabr(max_bounds)); + let mut max = right + .select_aabr(from_bounds) + .min(right.select_aabr(max_bounds)); + if max < min { + swap(&mut min, &mut max); + } + if min + 2 > max { + continue 'room_gen; + } + let in_pos = rng.gen_range(min + 1..=max - 1); + let in_pos = + in_dir.select_aabr_with(from_bounds, Vec2::broadcast(in_pos)) + in_dir.to_vec2(); + + // Place the room in the given max bounds + let Some(bounds) = place_room_in(room_kind, max_bounds, in_dir, in_pos, rng) else { + continue 'room_gen; + }; + + let bounds3 = Aabb { + min: bounds.min.with_z(alt), + max: bounds.max.with_z(alt + room_hgt), + }; + let id = rooms.insert(Room::new(bounds3, room_kind)); + + let start = in_dir.select_aabr_with( + from_bounds, + Vec2::broadcast(left.select_aabr(from_bounds).max(left.select_aabr(bounds))), + ) + in_dir.to_vec2() + + left.to_vec2(); + + let end = in_dir.select_aabr_with( + from_bounds, + Vec2::broadcast( + right + .select_aabr(from_bounds) + .min(right.select_aabr(bounds)), + ), + ) + in_dir.to_vec2() + + right.to_vec2(); + + let door_center = right.select(in_pos - start); + let b = rng.gen_bool(0.5); + let door_min = door_center - b as i32; + let door_max = door_center - (!b) as i32; + let wall_id = walls.insert(Wall { + start, + end, + base_alt: min_z, + top_alt: max_z, + from: Some(from_id), + to: Some(id), + to_dir: in_dir, + door: Some((door_min, door_max)), + }); + + rooms[id].walls[-in_dir].push(wall_id); + rooms[from_id].walls[in_dir].push(wall_id); + + room_metas.push(RoomMeta { + id, + walls: Dir::iter().filter(|d| *d != -in_dir).collect(), + }); + room_counts[room_kind] += 1; + } + + // Place walls where needed. + for from_id in rooms.ids() { + let room_bounds = to_aabr(rooms[from_id].bounds); + let mut skip = HashSet::new(); + skip.insert(from_id); + let mut wall_ranges = EnumMap::>::default(); + for dir in Dir::iter() { + let orth = dir.orthogonal(); + let range = (orth.select(room_bounds.min), orth.select(room_bounds.max)); + wall_ranges[dir].push(range); + } + // Split the wall into parts. + let mut split_range = |dir: Dir, min: i32, max: i32| { + debug_assert!(min <= max); + let mut new_ranges = Vec::new(); + wall_ranges[dir].retain_mut(|(r_min, r_max)| { + if *r_min <= max && *r_max >= min { + match (*r_min >= min, *r_max <= max) { + (true, true) => false, + (true, false) => { + *r_min = max + 1; + true + }, + (false, true) => { + *r_max = min - 1; + true + }, + (false, false) => { + new_ranges.push((max + 1, *r_max)); + *r_max = min - 1; + true + }, + } + } else { + true + } + }); + wall_ranges[dir].extend(new_ranges); + }; + for dir in Dir::iter() { + let connected_walls = &mut rooms[from_id].walls[dir]; + skip.extend( + connected_walls + .iter() + .flat_map(|wall| walls[*wall].from.into_iter().chain(walls[*wall].to)), + ); + let orth = dir.orthogonal(); + // Divide wall ranges by existing walls. + for wall in connected_walls.iter() { + let wall = &walls[*wall]; + let mut min = orth.select(wall.start); + let mut max = orth.select(wall.end); + if min > max { + swap(&mut min, &mut max); + } + min += 1; + max -= 1; + split_range(dir, min, max); + } + } + + // Divide wall ranges by neighbouring rooms + for to_id in rooms.ids().filter(|id| !skip.contains(id)) { + let a_min_z = rooms[from_id].bounds.min.z; + let a_max_z = rooms[from_id].bounds.max.z; + let b_min_z = rooms[to_id].bounds.min.z; + let b_max_z = rooms[to_id].bounds.max.z; + if a_min_z >= b_max_z || a_max_z <= b_min_z { + // We are not at the same altitude. + continue; + } + let min_z = a_min_z.min(b_min_z); + let max_z = a_max_z.max(b_max_z); + let n_room_bounds = to_aabr(rooms[to_id].bounds); + + let p1 = n_room_bounds.projected_point(room_bounds.center()); + let p0 = room_bounds.projected_point(p1); + + let to_dir = Dir::from_vec2(p1 - p0); + + let intersection = to_dir + .extend_aabr(room_bounds, 1) + .intersection(to_dir.opposite().extend_aabr(n_room_bounds, 1)); + + if intersection.is_valid() { + let start = intersection.min; + let end = intersection.max; + + let orth = to_dir.orthogonal(); + + let min = orth.select(start); + let max = orth.select(end); + split_range(to_dir, min, max); + let door = if max - min > 2 && max_z - min_z > 3 && rng.gen_bool(0.8) { + let door_center = rng.gen_range(1..=max - min - 2); + Some((door_center, door_center + 1)) + } else { + None + }; + + let id = walls.insert(Wall { + start: start - orth.to_vec2(), + end: end + orth.to_vec2(), + base_alt: min_z, + top_alt: max_z, + from: Some(from_id), + to: Some(to_id), + to_dir, + door, + }); + + rooms[from_id].walls[to_dir].push(id); + rooms[to_id].walls[-to_dir].push(id); + } + } + // Place remaining walls. + for (dir, ranges) in wall_ranges { + for (min, max) in ranges { + let start = + dir.select_aabr_with(room_bounds, Vec2::broadcast(min - 1)) + dir.to_vec2(); + let end = + dir.select_aabr_with(room_bounds, Vec2::broadcast(max + 1)) + dir.to_vec2(); + + let wall_id = walls.insert(Wall { + start, + end, + base_alt: rooms[from_id].bounds.min.z, + top_alt: rooms[from_id].bounds.max.z, + from: Some(from_id), + to: None, + to_dir: dir, + door: None, + }); + + rooms[from_id].walls[dir].push(wall_id); + } + } + } + + // Compute detail areas + for room in rooms.values_mut() { + let bounds = to_aabr(room.bounds); + let walls = &walls; + let mut avoid = room + .walls + .iter() + .flat_map(|(dir, dir_walls)| { + dir_walls.iter().filter_map(move |wall_id| { + let wall = &walls[*wall_id]; + + let door_bounds = wall.door_bounds()?; + + Some( + Aabr { + min: dir.select_aabr_with(bounds, door_bounds.min), + max: dir.select_with(bounds.center(), door_bounds.max), + } + .made_valid(), + ) + }) + }) + .collect::>(); + + let mut x = bounds.min.x; + // Basically greedy meshing, but for aabrs + while x <= bounds.max.x { + let mut y = bounds.min.y; + 'y_loop: while y <= bounds.max.y { + let min = Vec2::new(x, y); + let mut max_y = bounds.max.y; + for area in avoid.iter() { + let contains_x = area.min.x <= min.x && min.x <= area.max.x; + let contains_y = area.min.y <= min.y && min.y <= area.max.y; + if contains_x && contains_y { + y = area.max.y + 1; + continue 'y_loop; + } + + if contains_x && min.y < area.min.y && area.min.y - 1 < max_y { + max_y = area.min.y - 1; + } + } + + let max_x = avoid + .iter() + .filter_map(|area| { + if area.min.x > x && area.min.y <= max_y && area.max.y >= min.y { + Some(area.min.x - 1) + } else { + None + } + }) + .min() + .unwrap_or(bounds.max.x); + + let area = Aabr { + min, + max: Vec2::new(max_x, max_y), + }; + avoid.push(area); + room.detail_areas.push(area); + y = max_y + 1; + } + x += 1; + } + } + + // Place details in detail areas. + for room in rooms.values_mut() { + let room_aabr = to_aabr(room.bounds); + let table = |pos: Vec2, aabr: Aabr| Detail::Table { + pos, + chairs: Dir::iter() + .filter(|dir| aabr.contains_point(pos + dir.to_vec2())) + .collect(), + }; + match room.kind { + RoomKind::Garden => room.detail_areas.retain(|&aabr| { + if aabr.size().reduce_max() > 1 && rng.gen_bool(0.7) { + room.details.push(table(aabr.center(), aabr)); + false + } else { + true + } + }), + RoomKind::StageRoom => { + let mut best = None; + let mut best_score = 0; + for (i, aabr) in room.detail_areas.iter().enumerate() { + let edges = Dir::iter() + .filter(|dir| dir.select_aabr(*aabr) == dir.select_aabr(room_aabr)) + .count() as i32; + let test_score = edges * aabr.size().product(); + if best_score < test_score { + best_score = test_score; + best = Some(i); + } + } + if let Some(aabr) = best.map(|i| room.detail_areas.swap_remove(i)) { + room.details.push(Detail::Stage { aabr }) + } + room.detail_areas.retain(|&aabr| { + if aabr.size().reduce_max() > 1 && rng.gen_bool(0.8) { + room.details.push(table(aabr.center(), aabr)); + false + } else { + true + } + }); + }, + RoomKind::BarRoom => { + let mut best = None; + let mut best_score = 0; + for (i, aabr) in room.detail_areas.iter().enumerate() { + let test_score = Dir::iter() + .any(|dir| dir.select_aabr(*aabr) == dir.select_aabr(room_aabr)) + as i32 + * aabr.size().product(); + if best_score < test_score { + best_score = test_score; + best = Some(i); + } + } + if let Some(aabr) = best.map(|i| room.detail_areas.swap_remove(i)) { + room.details.push(Detail::Bar { aabr }) + } + room.detail_areas.retain(|&aabr| { + if aabr.size().reduce_max() > 1 && rng.gen_bool(0.1) { + room.details.push(table(aabr.center(), aabr)); + false + } else { + true + } + }); + }, + RoomKind::EntranceRoom => {}, + } + } + + for room_id in rooms.ids() { + let room = &rooms[room_id]; + // If a room is already fully covered by a roof, we skip it. + if room.is_covered_by_roof(&roofs) { + continue; + } + let roof_min_z = room.bounds.max.z + 1; + let mut roof_bounds = to_aabr(room.bounds); + roof_bounds.min -= 2; + roof_bounds.max += 2; + let mut dirs = Vec::from(Dir::ALL); + + let mut over_rooms = vec![room_id]; + // Extend roof over adjecent rooms. + while !dirs.is_empty() { + let dir = dirs.swap_remove(rng.gen_range(0..dirs.len())); + let orth = dir.orthogonal(); + // Check for room intersections in this direction. + for (room_id, room) in rooms.iter() { + let room_aabr = to_aabr(room.bounds); + if room.bounds.max.z == roof_min_z + && dir.select_aabr(roof_bounds) + dir.signum() + == (-dir).select_aabr(room_aabr) + && orth.select_aabr(roof_bounds) <= orth.select_aabr(room_aabr) + 2 + && (-orth).select_aabr(roof_bounds) >= (-orth).select_aabr(room_aabr) - 2 + { + // If the room we found is fully covered by a roof already, we don't go in + // this direction. + if room.is_covered_by_roof(&roofs) { + break; + } + roof_bounds = dir.extend_aabr(roof_bounds, dir.select(room_aabr.size())); + dirs.push(dir); + over_rooms.push(room_id); + break; + } + } + } + + // Build a lottery of valid roofs to pick from + let mut valid_styles = vec![(0.5, RoofStyle::Flat)]; + + let gardens = over_rooms + .iter() + .filter(|id| matches!(rooms[**id].kind, RoomKind::Garden)) + .count(); + + // If we just have gardens, we can use FlatBars style. + if gardens == over_rooms.len() { + let ratio = Dir::X.select(roof_bounds.size()) as f32 + / Dir::Y.select(roof_bounds.size()) as f32; + valid_styles.extend([ + (5.0 * ratio, RoofStyle::FlatBars { dir: Dir::X }), + (5.0 / ratio, RoofStyle::FlatBars { dir: Dir::Y }), + ]); + } + + // Find heights of possible adjecent rooms. + let mut dir_zs = EnumMap::default(); + for dir in Dir::iter() { + let orth = dir.orthogonal(); + for room in rooms.values() { + let room_aabr = to_aabr(room.bounds); + if room.bounds.max.z > roof_min_z + && dir.select_aabr(roof_bounds) == (-dir).select_aabr(room_aabr) + && orth.select_aabr(roof_bounds) <= orth.select_aabr(room_aabr) + 2 + && (-orth).select_aabr(roof_bounds) >= (-orth).select_aabr(room_aabr) - 2 + { + dir_zs[dir] = Some(room.bounds.max.z); + break; + } + } + } + + for dir in [Dir::X, Dir::Y] { + if dir_zs[dir.orthogonal()].is_none() && dir_zs[-dir.orthogonal()].is_none() { + let max_z = + roof_min_z + (dir.orthogonal().select(roof_bounds.size()) / 2 - 1).min(7); + let max_z = match (dir_zs[dir], dir_zs[-dir]) { + (Some(a), Some(b)) => { + if a.min(b) >= roof_min_z + 3 { + max_z.min(a.min(b)) + } else { + max_z + } + }, + (None, None) => max_z, + _ => continue, + }; + + for max_z in roof_min_z + 3..=max_z { + valid_styles.push((1.0, RoofStyle::Gable { dir, max_z })) + } + } + } + + for dir in Dir::iter() { + if let (Some(h), None) = (dir_zs[dir], dir_zs[-dir]) { + for max_z in roof_min_z + 2..=h { + valid_styles.push((1.0, RoofStyle::LeanTo { dir, max_z })) + } + } + } + + if Dir::iter().all(|d| dir_zs[d].is_none()) { + for max_z in roof_min_z + 3..=roof_min_z + 7 { + valid_styles.push((0.8, RoofStyle::Hip { max_z })) + } + } + + let style_lottery = Lottery::from(valid_styles); + + debug_assert!( + roof_bounds.is_valid(), + "Roof bounds aren't valid: {:?}", + roof_bounds + ); + let roof_id = roofs.insert(Roof { + bounds: roof_bounds, + min_z: roof_min_z, + style: *style_lottery.choose_seeded(rng.gen()), + }); + + for room_id in over_rooms { + rooms[room_id].roofs.push(roof_id); + } + } + + Self { + name, + rooms, + walls, + roofs, + door_tile, + door_wpos, + bounds, + } + } +} + +fn aabb(mut aabb: Aabb) -> Aabb { + aabb.make_valid(); + aabb.max += 1; + aabb +} + +impl Structure for Tavern { + #[cfg(feature = "use-dyn-lib")] + const UPDATE_FN: &'static [u8] = b"render_tavern\0"; + + #[cfg_attr(feature = "be-dyn-lib", export_name = "render_tavern")] + fn render_inner(&self, _site: &Site, _land: &Land, painter: &crate::site2::Painter) { + let field = RandomField::new(740384); + + const DOWN: i32 = 6; + + let mut offset = 0; + let mut choose = |slice: &[Rgb]| -> Rgb { + offset += 1; + *field + .choose(self.door_wpos + offset, slice) + .expect("Color slice should not be empty.") + }; + + let detail_fill = Fill::Brick( + BlockKind::Rock, + choose(&[ + Rgb::new(55, 65, 64), + Rgb::new(46, 62, 100), + Rgb::new(46, 100, 62), + Rgb::new(100, 100, 105), + ]), + 15, + ); + let wall_fill = Fill::Brick( + BlockKind::Wood, + choose(&[ + Rgb::new(160, 53, 34), + Rgb::new(147, 51, 29), + Rgb::new(147, 101, 69), + Rgb::new(90, 90, 95), + Rgb::new(170, 140, 52), + ]), + 20, + ); + let wall_detail_fill = Fill::Brick( + BlockKind::Wood, + choose(&[Rgb::new(108, 100, 79), Rgb::new(150, 150, 150)]), + 25, + ); + let floor_fill = Fill::Brick( + BlockKind::Wood, + choose(&[Rgb::new(42, 44, 43), Rgb::new(56, 18, 10)]), + 10, + ); + let roof_fill = Fill::Brick( + BlockKind::Wood, + choose(&[ + Rgb::new(21, 43, 48), + Rgb::new(11, 23, 38), + Rgb::new(45, 28, 21), + Rgb::new(10, 55, 40), + Rgb::new(5, 35, 15), + Rgb::new(40, 5, 11), + Rgb::new(55, 45, 11), + ]), + 20, + ); + let simple_roof_fill = Fill::Brick( + BlockKind::Wood, + choose(&[Rgb::new(106, 73, 64), Rgb::new(85, 52, 43)]), + 20, + ); + + let get_kind = |room| self.rooms.get(room).kind; + let get_door_stair = |wall: &Wall, door: Aabr| { + let filter = |room: &Id| self.rooms[*room].bounds.min.z > wall.base_alt; + wall.to + .filter(filter) + .zip(Some(wall.to_dir)) + .or(wall.from.filter(filter).zip(Some(-wall.to_dir))) + .map(|(room, to_dir)| { + let room = &self.rooms[room]; + + let max = door.max + to_dir.to_vec2() * (room.bounds.min.z - wall.base_alt + 1); + (door.min, max, room, to_dir) + }) + }; + + for roof in self.roofs.values() { + match roof.style { + RoofStyle::Flat => { + painter + .aabb(aabb(Aabb { + min: roof.bounds.min.with_z(roof.min_z), + max: roof.bounds.max.with_z(roof.min_z), + })) + .fill(roof_fill.clone()); + }, + RoofStyle::FlatBars { dir } => painter + .aabb(aabb(Aabb { + min: dir + .select_aabr_with(roof.bounds, roof.bounds.min) + .with_z(roof.min_z), + max: dir + .select_aabr_with(roof.bounds, roof.bounds.max) + .with_z(roof.min_z), + })) + .repeat( + -dir.to_vec3() * 2, + (dir.select(roof.bounds.size()) as u32 + 3) / 2, + ) + .fill(simple_roof_fill.clone()), + RoofStyle::LeanTo { dir, max_z } => { + painter + .aabb(aabb(Aabb { + min: roof.bounds.min.with_z(roof.min_z), + max: roof.bounds.max.with_z(roof.min_z), + })) + .fill(roof_fill.clone()); + painter + .ramp( + aabb(Aabb { + min: roof.bounds.min.with_z(roof.min_z), + max: roof.bounds.max.with_z(max_z), + }), + dir, + ) + .fill(roof_fill.clone()); + for d in [dir.orthogonal(), -dir.orthogonal()] { + painter + .ramp( + aabb(Aabb { + min: (d.select_aabr_with(roof.bounds, roof.bounds.min) + - d.to_vec2()) + .with_z(roof.min_z - 1), + max: (d.select_aabr_with(roof.bounds, roof.bounds.max) + - d.to_vec2()) + .with_z(max_z - 1), + }), + dir, + ) + .fill(wall_fill.clone()); + painter + .ramp( + aabb(Aabb { + min: d + .select_aabr_with(roof.bounds, roof.bounds.min) + .with_z(roof.min_z - 1), + max: d + .select_aabr_with(roof.bounds, roof.bounds.max) + .with_z(max_z - 1), + }), + dir, + ) + .clear(); + } + }, + RoofStyle::Gable { dir, max_z } => { + painter + .gable( + aabb(Aabb { + min: roof.bounds.min.with_z(roof.min_z), + max: roof.bounds.max.with_z(max_z), + }), + max_z - roof.min_z + 1, + dir, + ) + .fill(roof_fill.clone()); + for dir in [dir, -dir] { + painter + .gable( + aabb(Aabb { + min: (dir.select_aabr_with(roof.bounds, roof.bounds.min + 1) + - dir.to_vec2()) + .with_z(roof.min_z), + max: (dir.select_aabr_with(roof.bounds, roof.bounds.max - 1) + - dir.to_vec2()) + .with_z(max_z - 1), + }), + max_z - roof.min_z, + dir, + ) + .fill(wall_fill.clone()); + painter + .aabb(aabb(Aabb { + min: (dir.select_aabr_with(roof.bounds, roof.bounds.min + 1) + - dir.to_vec2()) + .with_z(roof.min_z), + max: (dir.select_aabr_with(roof.bounds, roof.bounds.max - 1) + - dir.to_vec2()) + .with_z(roof.min_z), + })) + .fill(wall_detail_fill.clone()); + let center_bounds = Aabr { + min: (dir.select_aabr_with(roof.bounds, roof.bounds.center()) + - dir.to_vec2()), + max: (dir.select_aabr_with( + roof.bounds, + (roof.bounds.min + roof.bounds.max + 1) / 2, + ) - dir.to_vec2()), + }; + painter + .aabb(aabb(Aabb { + min: center_bounds.min.with_z(roof.min_z), + max: center_bounds.max.with_z(max_z - 1), + })) + .fill(wall_detail_fill.clone()); + for d in [dir.orthogonal(), -dir.orthogonal()] { + let hgt = max_z - roof.min_z; + let half_size = d.select(roof.bounds.size() + 1) / 2; + let e = half_size - hgt + 1; + let e = e - e % 2; + let f = half_size - e; + let hgt = (hgt - 1).min(e - f % 2) - (d.signum() - 1) / 2; + let mut aabr = Aabr { + min: d.select_aabr_with(center_bounds, center_bounds.min), + max: d.select_aabr_with(center_bounds, center_bounds.max) + + d.to_vec2() * hgt, + } + .made_valid(); + aabr.max += 1; + painter + .plane( + aabr, + aabr.min + .with_z(if d.signum() < 0 { + roof.min_z + hgt + } else { + roof.min_z + }) + .as_(), + d.to_vec2().as_(), + ) + .fill(wall_detail_fill.clone()); + } + painter + .gable( + aabb(Aabb { + min: dir + .select_aabr_with(roof.bounds, roof.bounds.min + 1) + .with_z(roof.min_z), + max: dir + .select_aabr_with(roof.bounds, roof.bounds.max - 1) + .with_z(max_z - 1), + }), + max_z - roof.min_z, + dir, + ) + .clear(); + } + }, + RoofStyle::Hip { max_z } => { + painter + .pyramid(aabb(Aabb { + min: roof.bounds.min.with_z(roof.min_z), + max: roof.bounds.max.with_z(max_z), + })) + .fill(roof_fill.clone()); + }, + } + } + + for room in self.rooms.values() { + painter + .aabb(aabb(Aabb { + min: room.bounds.min.with_z(room.bounds.min.z - DOWN), + max: room.bounds.max.with_z(room.bounds.min.z - 1), + })) + .fill(floor_fill.clone()); + } + for wall in self.walls.values() { + let wall_aabb = Aabb { + min: wall.start.with_z(wall.base_alt), + max: wall.end.with_z(wall.top_alt), + }; + let wall_dir = Dir::from_vec2(wall.end - wall.start); + match (wall.from.map(get_kind), wall.to.map(get_kind)) { + (Some(RoomKind::Garden), None) | (None, Some(RoomKind::Garden)) => { + let hgt = wall_aabb.min.z..=wall_aabb.max.z; + painter + .column(wall_aabb.min.xy(), hgt.clone()) + .fill(wall_detail_fill.clone()); + painter + .column(wall_aabb.max.xy(), hgt) + .fill(wall_detail_fill.clone()); + let z = (wall.base_alt + wall.top_alt) / 2; + + painter + .aabb(aabb(Aabb { + min: (wall_aabb.min + wall_dir.to_vec2()).with_z(wall_aabb.min.z + 1), + max: (wall_aabb.max - wall_dir.to_vec2()).with_z(wall_aabb.max.z - 1), + })) + .clear(); + + painter.rotated_sprite( + wall_aabb.min.with_z(z) + wall_dir.to_vec2(), + SpriteKind::WallSconce, + wall_dir.sprite_ori(), + ); + painter.rotated_sprite( + wall_aabb.max.with_z(z) - wall_dir.to_vec2(), + SpriteKind::WallSconce, + wall_dir.opposite().sprite_ori(), + ); + painter + .aabb(aabb(Aabb { + min: wall_aabb.min.with_z(wall_aabb.min.z - DOWN), + max: wall_aabb.max.with_z(wall_aabb.min.z), + })) + .fill(wall_detail_fill.clone()); + painter + .aabb(aabb(Aabb { + min: wall_aabb.min.with_z(wall_aabb.max.z), + max: wall_aabb.max, + })) + .fill(wall_detail_fill.clone()); + }, + (Some(RoomKind::Garden), Some(RoomKind::Garden)) => { + painter + .aabb(aabb(Aabb { + min: wall_aabb.min.with_z(wall_aabb.min.z - DOWN), + max: wall_aabb.max.with_z(wall_aabb.min.z - 1), + })) + .fill(floor_fill.clone()); + painter.aabb(aabb(wall_aabb)).clear(); + }, + (None, None) => {}, + _ => { + painter + .aabb(aabb(Aabb { + min: wall_aabb.min.with_z(wall_aabb.min.z - DOWN), + max: wall_aabb.max, + })) + .fill(wall_fill.clone()); + painter + .column(wall.start, wall.base_alt - DOWN..=wall.top_alt) + .fill(wall_detail_fill.clone()); + painter + .column(wall.end, wall.base_alt - DOWN..=wall.top_alt) + .fill(wall_detail_fill.clone()); + }, + } + if let Some(door) = wall.door_bounds() { + let orth = wall.to_dir.orthogonal(); + if let Some((min, max, room, to_dir)) = get_door_stair(wall, door) { + painter + .aabb(aabb(Aabb { + min: (min + to_dir.to_vec2() - orth.to_vec2()) + .with_z(wall.base_alt - 1), + max: (max + orth.to_vec2()).with_z(room.bounds.min.z - 1), + })) + .fill(floor_fill.clone()); + } + } + } + for room in self.rooms.values() { + painter.aabb(aabb(room.bounds)).clear(); + + let room_aabr = Aabr { + min: room.bounds.min.xy(), + max: room.bounds.max.xy(), + }; + match room.kind { + RoomKind::Garden => {}, + RoomKind::StageRoom => { + for aabr in room.detail_areas.iter().copied() { + for dir in Dir::iter().filter(|dir| { + dir.select_aabr(aabr) == dir.select_aabr(room_aabr) + && dir.rotated_cw().select_aabr(aabr) + == dir.rotated_cw().select_aabr(room_aabr) + }) { + let pos = dir.select_aabr_with( + aabr, + Vec2::broadcast(dir.rotated_cw().select_aabr(aabr)), + ); + painter.sprite(pos.with_z(room.bounds.min.z), SpriteKind::StreetLamp); + } + } + }, + RoomKind::BarRoom => { + for aabr in room.detail_areas.iter().copied() { + for dir in Dir::iter() + .filter(|dir| dir.select_aabr(aabr) == dir.select_aabr(room_aabr)) + { + let pos = dir + .select_aabr_with(aabr, aabr.center()) + .with_z(room.bounds.center().z); + + painter.rotated_sprite( + pos, + SpriteKind::WallLampSmall, + dir.opposite().sprite_ori(), + ); + } + } + }, + RoomKind::EntranceRoom => { + for aabr in room.detail_areas.iter() { + let edges = Dir::iter() + .filter(|dir| dir.select_aabr(*aabr) == dir.select_aabr(room_aabr)) + .count(); + let hanger_pos = if edges == 2 { + let pos = aabr.center().with_z(room.bounds.min.z); + painter.sprite(pos, SpriteKind::CoatRack); + Some(pos) + } else { + None + }; + + for dir in Dir::iter() + .filter(|dir| dir.select_aabr(*aabr) == dir.select_aabr(room_aabr)) + { + let pos = dir + .select_aabr_with(*aabr, aabr.center()) + .with_z(room.bounds.center().z + 1); + if hanger_pos.map_or(false, |p| p.xy() != pos.xy()) { + painter.rotated_sprite( + pos, + SpriteKind::WallLampSmall, + dir.opposite().sprite_ori(), + ); + } + } + } + }, + } + for detail in room.details.iter() { + match *detail { + Detail::Bar { aabr } => { + for dir in Dir::iter() { + let edge = dir.select_aabr(aabr); + let rot_dir = if field.chance(aabr.center().with_z(0), 0.5) { + dir.rotated_cw() + } else { + dir.rotated_ccw() + }; + let rot_edge = rot_dir.select_aabr(aabr); + match ( + edge == dir.select_aabr(room_aabr), + rot_edge == rot_dir.select_aabr(room_aabr), + ) { + (false, _) => { + let (min, max) = ( + dir.select_aabr_with( + aabr, + Vec2::broadcast(rot_dir.select_aabr(aabr)), + ), + dir.select_aabr_with( + aabr, + Vec2::broadcast(rot_dir.opposite().select_aabr(aabr)), + ), + ); + painter + .aabb(aabb(Aabb { + min: (min - rot_dir.to_vec2()) + .with_z(room.bounds.min.z), + max: max.with_z(room.bounds.min.z), + })) + .fill(wall_detail_fill.clone()); + painter + .aabb(aabb(Aabb { + min: min.with_z(room.bounds.min.z + 3), + max: max.with_z(room.bounds.max.z), + })) + .fill(wall_detail_fill.clone()); + }, + (true, true) => { + painter.sprite( + dir.abs().vec2(edge, rot_edge).with_z(room.bounds.min.z), + SpriteKind::CookingPot, + ); + }, + (true, false) => {}, + } + } + }, + Detail::Stage { aabr } => { + painter + .aabb(aabb(Aabb { + min: aabr.min.with_z(room.bounds.min.z), + max: aabr.max.with_z(room.bounds.min.z), + })) + .fill(detail_fill.clone()); + painter + .aabb(aabb(Aabb { + min: (aabr.min + 1).with_z(room.bounds.min.z), + max: (aabr.max - 1).with_z(room.bounds.min.z), + })) + .fill(wall_fill.clone()); + for dir in Dir::iter().filter(|dir| { + dir.select_aabr(aabr) != dir.select_aabr(room_aabr) + && dir.rotated_cw().select_aabr(aabr) + != dir.rotated_cw().select_aabr(room_aabr) + }) { + let pos = dir.select_aabr_with( + aabr, + Vec2::broadcast(dir.rotated_cw().select_aabr(aabr)), + ); + painter + .column(pos, room.bounds.min.z..=room.bounds.max.z) + .fill(wall_detail_fill.clone()); + + for dir in Dir::iter() { + painter.rotated_sprite( + pos.with_z(room.bounds.center().z + 1) + dir.to_vec2(), + SpriteKind::WallSconce, + dir.sprite_ori(), + ); + } + } + }, + Detail::Table { pos, chairs } => { + let pos = pos.with_z(room.bounds.min.z); + painter.sprite(pos, SpriteKind::TableDining); + for dir in chairs.into_iter() { + painter.rotated_sprite( + pos + dir.to_vec2(), + SpriteKind::ChairSingle, + dir.opposite().sprite_ori(), + ); + } + }, + } + } + } + + for wall in self.walls.values() { + let kinds = (wall.from.map(get_kind), wall.to.map(get_kind)); + let in_dir_room = if let (Some(room), to @ None) | (None, to @ Some(room)) = kinds { + let in_dir = if to.is_none() { + -wall.to_dir + } else { + wall.to_dir + }; + + Some((in_dir, room)) + } else { + None + }; + if let Some((in_dir, room)) = in_dir_room { + let width = in_dir.orthogonal().select(wall.end - wall.start).abs(); + let wall_center = (wall.start + wall.end) / 2; + let door_dist = wall.door_bounds().map_or(i32::MAX, |door| { + (door.min - wall_center) + .map(|x| x.abs()) + .reduce_max() + .max((door.max - wall_center).map(|x| x.abs()).reduce_max()) + }); + match room { + RoomKind::Garden => { + if door_dist >= 2 { + painter.rotated_sprite( + wall_center.with_z(wall.base_alt + 1), + SpriteKind::Planter, + in_dir.sprite_ori(), + ); + } + }, + _ => { + if width >= 5 && door_dist > 3 { + painter + .aabb(aabb(Aabb { + min: (wall_center + in_dir.rotated_ccw().to_vec2()) + .with_z(wall.base_alt + 1), + max: (wall_center + in_dir.rotated_cw().to_vec2()) + .with_z(wall.base_alt + 2), + })) + .fill(Fill::RotatedSprite( + SpriteKind::Window1, + in_dir.sprite_ori(), + )); + } + }, + } + } + if let Some(door) = wall.door_bounds() && !matches!(kinds, (Some(RoomKind::Garden), Some(RoomKind::Garden))) { + let orth = wall.to_dir.orthogonal(); + painter + .aabb(aabb(Aabb { + min: (door.min - orth.to_vec2()).with_z(wall.base_alt), + max: (door.max + orth.to_vec2()).with_z(wall.base_alt + 3), + })) + .fill(detail_fill.clone()); + painter + .aabb(aabb(Aabb { + min: (door.min - orth.to_vec2()).with_z(wall.base_alt - 1), + max: (door.max + orth.to_vec2()).with_z(wall.base_alt - 1), + })) + .fill(floor_fill.clone()); + painter + .aabb(aabb(Aabb { + min: (door.min + wall.to_dir.to_vec2()).with_z(wall.base_alt), + max: (door.max - wall.to_dir.to_vec2()).with_z(wall.base_alt + 2), + })) + .clear(); + if let Some((min, max, room, to_dir)) = get_door_stair(wall, door) { + // Place a ramp if the door is lower than the room alt. + painter + .ramp( + aabb(Aabb { + min: (min - to_dir.to_vec2() * 3).with_z(wall.base_alt), + max: max.with_z(room.bounds.min.z + 2), + }), + to_dir, + ) + // TOOD: For zoomy worldgen, this a sheared aabb. + .without( + painter + .ramp( + aabb(Aabb { + min: (min + to_dir.to_vec2() * 2).with_z(wall.base_alt), + max: max.with_z(room.bounds.min.z - 1), + }), + to_dir, + ) + ) + .clear(); + } + if let Some((in_dir, _room)) = in_dir_room { + let sprite = match in_dir.rotated_cw().select(door.size()) { + 2.. => SpriteKind::DoorWide, + _ => SpriteKind::Door, + }; + painter.rotated_sprite( + in_dir + .rotated_cw() + .select_aabr_with(door, door.min) + .with_z(wall.base_alt), + sprite, + in_dir.sprite_ori(), + ); + painter.rotated_sprite( + in_dir + .rotated_ccw() + .select_aabr_with(door, door.min) + .with_z(wall.base_alt), + sprite, + in_dir.opposite().sprite_ori(), + ); + + let dir = match field.chance(door.min.with_z(wall.base_alt), 0.5) { + true => in_dir.rotated_cw(), + false => in_dir.rotated_ccw(), + }; + + let pos = + dir.select_aabr_with(door, door.min) + dir.to_vec2() - in_dir.to_vec2(); + + painter.rotated_sprite_with_cfg( + pos.with_z(wall.base_alt + 2), + SpriteKind::HangingSign, + in_dir.opposite().sprite_ori(), + SpriteCfg { + unlock: None, + content: Some(Content::Plain(self.name.clone())), + }, + ); + } + } + } + } +} diff --git a/world/src/site2/util/mod.rs b/world/src/site2/util/mod.rs index b43266c8d6..a57542c931 100644 --- a/world/src/site2/util/mod.rs +++ b/world/src/site2/util/mod.rs @@ -6,7 +6,7 @@ use rand::Rng; use vek::*; /// A 2d direction. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Debug, enum_map::Enum, strum::EnumIter, enumset::EnumSetType)] pub enum Dir { X, Y, @@ -26,7 +26,7 @@ impl Dir { } } - pub fn from_vector(vec: Vec2) -> Dir { + pub fn from_vec2(vec: Vec2) -> Dir { if vec.x.abs() > vec.y.abs() { if vec.x > 0 { Dir::X } else { Dir::NegX } } else if vec.y > 0 { @@ -110,6 +110,17 @@ impl Dir { } } + /// Create a vec2 where x is in the direction of `self`, and y is anti + /// clockwise of `self`. + pub fn vec2(self, x: i32, y: i32) -> Vec2 { + match self { + Dir::X => Vec2::new(x, y), + Dir::NegX => Vec2::new(-x, -y), + Dir::Y => Vec2::new(y, x), + Dir::NegY => Vec2::new(-y, -x), + } + } + /// Returns a 3x3 matrix that rotates Vec3(1, 0, 0) to the direction you get /// in to_vec3. Inteded to be used with Primitive::Rotate. /// @@ -223,7 +234,7 @@ impl Dir { } } - pub fn split_aabr(self, aabr: Aabr, offset: T) -> [Aabr; 2] + pub fn split_aabr_offset(self, aabr: Aabr, offset: T) -> [Aabr; 2] where T: Copy + PartialOrd + Add + Sub, { @@ -241,6 +252,29 @@ impl Dir { } } + /// Try to split an aabr in a certain direction + pub fn try_split_aabr(self, aabr: Aabr, sp: T) -> Option<[Aabr; 2]> + where + T: Copy + PartialOrd + Add + Sub, + { + match self { + Dir::NegX | Dir::X => { + if aabr.min.x <= sp && sp <= aabr.max.x { + Some(aabr.split_at_x(sp)) + } else { + None + } + }, + Dir::NegY | Dir::Y => { + if aabr.min.y <= sp && sp <= aabr.max.y { + Some(aabr.split_at_y(sp)) + } else { + None + } + }, + } + } + pub fn trim_aabr(self, aabr: Aabr, offset: i32) -> Aabr { Aabr { min: aabr.min + self.abs().to_vec2() * offset, diff --git a/world/src/util/random.rs b/world/src/util/random.rs index 54aacf42ee..ceee42c7ab 100644 --- a/world/src/util/random.rs +++ b/world/src/util/random.rs @@ -15,6 +15,15 @@ impl RandomField { pub fn get_f32(&self, pos: Vec3) -> f32 { (self.get(pos) % (1 << 16)) as f32 / ((1 << 16) as f32) } + + pub fn choose<'a, T>(&self, pos: Vec3, slice: &'a [T]) -> Option<&'a T> { + if slice.is_empty() { + return None; + } + + let i = self.get(pos) as usize; + slice.get(i % slice.len()) + } } impl Sampler<'static> for RandomField {