From 10581af41302730118317022b439056a87c9d019 Mon Sep 17 00:00:00 2001 From: Risto Saarelma Date: Thu, 2 Jan 2025 15:10:03 +0200 Subject: [PATCH] WIP waypoint stuff --- world/src/lib.rs | 2 + world/src/mapgen.rs | 4 +- world/src/waypoints.rs | 201 +++++++++++++++++++++++++++++++++++++++++ world/src/world.rs | 34 ++++++- 4 files changed, 234 insertions(+), 7 deletions(-) create mode 100644 world/src/waypoints.rs diff --git a/world/src/lib.rs b/world/src/lib.rs index 0d073b9..9a52b20 100644 --- a/world/src/lib.rs +++ b/world/src/lib.rs @@ -21,6 +21,8 @@ pub use mapgen::{Lot, MapGenerator, Patch}; pub mod sector_map; pub use sector_map::SectorMap; +mod waypoints; + mod world; pub use world::{Level, World}; diff --git a/world/src/mapgen.rs b/world/src/mapgen.rs index 1537c50..c609b4a 100644 --- a/world/src/mapgen.rs +++ b/world/src/mapgen.rs @@ -25,7 +25,7 @@ pub trait MapGenerator { /// /// Must also return true if an altar will show up in this sector after /// some game event, usually defeating a dungeon boss enemy. - fn has_altar(&self) -> bool { + fn has_waypoint(&self) -> bool { false } } @@ -35,7 +35,7 @@ impl MapGenerator for Patch { Ok(self.clone()) } - fn has_altar(&self) -> bool { + fn has_waypoint(&self) -> bool { // If any terrain voxel is an altar, return true. if self.terrain.values().any(|v| v == &Some(Block::Altar)) { return true; diff --git a/world/src/waypoints.rs b/world/src/waypoints.rs new file mode 100644 index 0000000..6267519 --- /dev/null +++ b/world/src/waypoints.rs @@ -0,0 +1,201 @@ +//! Logic for determining which waypoints affect which levels of a world. +//! +//! The core waypoint mechanics: +//! +//! If you die, you respawn at the last waypoint you rested at, with all the +//! regular enemies you damaged or killed restored to full health. Bosses are +//! exceptions that stay dead no matter what after they've been defeated the +//! first time. +//! +//! If you return to rest at the same waypoint you last rested at, enemies +//! will respawn similarly as if you had died. This is to prevent the player +//! from clearing areas by slow attrition where they go back to rest after +//! killing each individual enemy and never need to face the area at full +//! strength while minding their own limited resources. +//! +//! If you start at one waypoint and rest at a different one though, any +//! changes made *in the area between the two waypoints* will be permanent. So +//! it is possible to eventually clear up areas, as long as you're able to +//! actually travel through them without resting. +//! +//! This module is about the logic to figure out just how the "area between +//! two waypoints" is determined. + +use std::collections::BinaryHeap; + +use serde::{Deserialize, Serialize}; +use util::{HashMap, HashSet}; + +use crate::{Level, Location, World}; + +impl World { + /// Return all segments between adjacent waypoints that contain given + /// waypoint. + fn waypoint_connections( + &self, + a: Location, + ) -> impl Iterator + '_ { + self.segment_cover + .keys() + .filter(move |p| p.contains(a)) + .copied() + } + + /// Return a list of sectors that will have changes permanently applied to + /// them when the player started from waypoint `a` and stops at waypoint + /// `b`. The waypoints must correspond to valid altars and be different, + /// or the result will be empty. + /// + /// If the set of sectors where both waypoints are within the + /// second-closest distance to that sector is non-empty, the waypoints are + /// considered to be connected and this set is returned as the result. + /// Otherwise, the result will be the union of the affected areas of all + /// pairs of connected waypoints that form the shortest paths between `a` + /// and `b`. + fn area_between_waypoints( + &self, + a: Location, + b: Location, + ) -> HashSet { + self.shortest_paths_between_waypoints(a, b) + .into_iter() + .flat_map(|p| &self.segment_cover[&p]) + .copied() + .collect() + } + + /// Return all the adjacent waypoint to waypoint connections that for + /// every shortest path between waypoints a and b. + fn shortest_paths_between_waypoints( + &self, + a: Location, + b: Location, + ) -> Vec { + // Start from set of waypoint_connections for a. + // If b is found in this set, just return the pair. + // + // Return all pairs that are on a path from a to be such that no + // shorter path exists connecting a and b. + + todo!() + } + + fn compute_segment_covers(&self) -> HashMap> { + // Set of all levels: The keys of skeleton. + // Set of altars: Keys in skeleton where the value is_altar. + + // Procedure: for each level start floodfilling skeleton-space in 3D + // taxicab metric of unit level jumps in each direction along + // connectivity (this needs an utility method cuz connectivity can get + // hairy if we get to path segment analysis). Each level gets a vector + // value that contains the taxicab-in-level-units distance to every + // waypoint. + // + // The set of legs the level belongs to is all the ordered pairs of + // the indices of the vector that are within two closest values to the + // level. This is usually two waypoints, but it can be arbitrarily + // many with weird geometries, so then you need to generate a bunch of + // pairs for it. + + let waypoints: Vec = self + .skeleton + .iter() + .filter_map(|(lev, seg)| seg.has_waypoint().then_some(lev)) + .copied() + .collect(); + + let mut distances: HashMap> = + Default::default(); + let mut iters = waypoints + .iter() + .map(|&lev| { + util::bfs( + |&(origin, lev)| { + self.level_neighbors(lev).map(move |lev| (origin, lev)) + }, + vec![(lev, lev)], + ) + }) + .collect::>(); + let mut idx = iters.len() - 1; + + // Set up a custom multizip iterator that advances the floodfill out + // from each waypoint in lockstep... + + // XXX Okay shit scratch this, this won't work, we can't actually stop + // any single iteration from here, would need to wire the machinery + // right down to the bfs neighbors for each, so what I'd need instead + // is some kind of multistructure of the bfs stacks. + + for ((origin, lev), dist) in std::iter::from_fn(move || { + idx += 1; + if idx >= iters.len() { + idx = 0; + } + iters[idx].next() + }) { + // TODO + } + + todo!() + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub struct WaypointPair(Location, Location); + +impl From<(Location, Location)> for WaypointPair { + fn from((a, b): (Location, Location)) -> Self { + // Normalize the order of the points. + if a.to_array() < b.to_array() { + WaypointPair(a, b) + } else { + WaypointPair(b, a) + } + } +} + +impl WaypointPair { + pub fn contains(&self, loc: Location) -> bool { + self.0 == loc || self.1 == loc + } +} + +#[cfg(test)] +mod test { + use crate::MapGenerator; + + use super::*; + + struct DummyGenerator(bool); + + impl MapGenerator for DummyGenerator { + fn run( + &self, + _rng: &mut dyn rand::RngCore, + _lot: &crate::Lot, + ) -> anyhow::Result { + // No-op, these won't be actually run. + Ok(Default::default()) + } + + fn has_waypoint(&self) -> bool { + // We only care about them for marking waypoints in the skeleton. + self.0 + } + } + + #[test] + fn waypoint_cover() { + // Set up a world with a skeleton of segments with "yes + // waypoint" and "no waypoint" dummy map generators. It should have + // surface layer with spread-out waypoints and some dungeons with + // waypoints at the bottom. Also some stuff that exercise multiple + // possible paths between waypoints. + + // Test: Three altars at equal distances, making a level belong to + // multiple arcs. + + todo!(); + } +} diff --git a/world/src/world.rs b/world/src/world.rs index 0a412fc..ce32b19 100644 --- a/world/src/world.rs +++ b/world/src/world.rs @@ -5,12 +5,15 @@ use glam::{ivec2, ivec3, IVec2, IVec3}; use rand::prelude::*; use serde::{Deserialize, Serialize, Serializer}; use static_assertions::const_assert; -use util::{a3, v2, v3, HashMap, HashSet, IndexMap, Neighbors2D, Silo}; +use util::{ + a3, v2, v3, HashMap, HashSet, IndexMap, Neighbors2D, Silo, AXIS_DIRS, +}; use crate::{ - data::Region, Block, Coordinates, Cube, Environs, Location, Lot, - MapGenerator, Patch, Pod, Rect, Scenario, Terrain, Voxel, Zone, DOWN, - LEVEL_DEPTH, NORTH, SECTOR_HEIGHT, SECTOR_WIDTH, UP, WEST, + data::Region, waypoints::WaypointPair, Block, Coordinates, Cube, Environs, + Location, Lot, MapGenerator, Patch, Pod, Rect, Scenario, Terrain, Voxel, + Zone, DOWN, LEVEL_BASIS, LEVEL_DEPTH, NORTH, SECTOR_HEIGHT, SECTOR_WIDTH, + UP, WEST, }; /// Non-cached world data that goes in a save file. @@ -62,13 +65,16 @@ pub struct World { // SerWorld type. /// Complete description for how to instantiate parts of the game world, /// built from `Scenario` data. - skeleton: HashMap, + pub(crate) skeleton: HashMap, /// Memory of which sectors have been generated. gen_status: HashMap, /// Where the player enters the world. player_entrance: Location, + + /// List of levels which are covered by pairs of adjacent waypoints. + pub(crate) segment_cover: HashMap>, } // Do this manually because otherwise I get complaints about no Clone impl @@ -117,6 +123,12 @@ pub struct Segment { pub generator: Box, } +impl Segment { + pub fn has_waypoint(&self) -> bool { + self.generator.has_waypoint() + } +} + impl From for SerWorld { fn from(value: World) -> Self { value.inner @@ -469,6 +481,18 @@ impl World { fn default_terrain(&self, _loc: Location) -> Voxel { Some(Block::Stone) } + + pub fn level_neighbors( + &self, + level: Level, + ) -> impl Iterator + '_ { + // NB. Assumes there are no adjacent but unconnected sectors in the + // skeleton. + AXIS_DIRS + .iter() + .map(move |&dir| level + dir * LEVEL_BASIS) + .filter(|&s| self.skeleton.contains_key(&s)) + } } // NB. This is specifically the sort of Cube you get from Zone::level(), but