Skip to content

Commit

Permalink
WIP waypoint stuff
Browse files Browse the repository at this point in the history
  • Loading branch information
rsaarelm committed Jan 2, 2025
1 parent 6821048 commit 10581af
Show file tree
Hide file tree
Showing 4 changed files with 234 additions and 7 deletions.
2 changes: 2 additions & 0 deletions world/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down
4 changes: 2 additions & 2 deletions world/src/mapgen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand All @@ -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;
Expand Down
201 changes: 201 additions & 0 deletions world/src/waypoints.rs
Original file line number Diff line number Diff line change
@@ -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<Item = WaypointPair> + '_ {
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<Level> {
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<WaypointPair> {
// 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<WaypointPair, Vec<Level>> {
// 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<Level> = self
.skeleton
.iter()
.filter_map(|(lev, seg)| seg.has_waypoint().then_some(lev))
.copied()
.collect();

let mut distances: HashMap<Level, BinaryHeap<(usize, Level)>> =
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::<Vec<_>>();
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<crate::Patch> {
// 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!();
}
}
34 changes: 29 additions & 5 deletions world/src/world.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<Level, Segment>,
pub(crate) skeleton: HashMap<Level, Segment>,

/// Memory of which sectors have been generated.
gen_status: HashMap<Level, GenStatus>,

/// 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<WaypointPair, Vec<Level>>,
}

// Do this manually because otherwise I get complaints about no Clone impl
Expand Down Expand Up @@ -117,6 +123,12 @@ pub struct Segment {
pub generator: Box<dyn MapGenerator>,
}

impl Segment {
pub fn has_waypoint(&self) -> bool {
self.generator.has_waypoint()
}
}

impl From<World> for SerWorld {
fn from(value: World) -> Self {
value.inner
Expand Down Expand Up @@ -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<Item = Level> + '_ {
// 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
Expand Down

0 comments on commit 10581af

Please sign in to comment.