Files
dungeoncraft/src/map.rs
T

969 lines
35 KiB
Rust

// Map, tiles, generation and path finding.
//
// The map size is chosen per game (64..256 tiles per side). Only one map is active at a time;
// its dimensions are kept in a thread-local so the many small helpers (idx, xy, inb) stay simple.
// Map::generate and loading a savegame (Map::activate) set them.
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::cell::Cell;
use std::cmp::Reverse;
use std::collections::BinaryHeap;
pub const MIN_SIZE: usize = 64;
pub const MAX_SIZE: usize = 256;
thread_local! {
static DIMS: Cell<(usize, usize)> = Cell::new((64, 64));
}
pub fn set_dims(w: usize, h: usize) {
DIMS.with(|d| d.set((w, h)));
}
#[inline]
pub fn mw() -> usize {
DIMS.with(|d| d.get().0)
}
#[inline]
pub fn mh() -> usize {
DIMS.with(|d| d.get().1)
}
#[inline]
pub fn n() -> usize {
mw() * mh()
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub enum Kind {
Rock,
Earth,
Gold,
Floor,
Treasury,
Lair,
Farm,
Training,
Library,
Core,
Portal,
Gate,
Torture,
Tower, // floor tile occupied by a defence tower (not walkable)
Temple,
Water, // underground river: not walkable, needs a bridge
Lava, // lava river: not walkable, burns what falls in
Bridge, // wooden bridge over water (walkable)
LavaBridge, // stone bridge over lava (walkable)
Shard, // floor with a shard of the soul core (campaign)
Silver, // silver vein: common, little value
Ruby, // ruby vein: rare, valuable
Diamond, // diamond vein: very rare, very valuable
Aerie, // room for flying creatures (their beds)
Steel, // steel wall: hardly anyone gets through
ManaSpring, // crystal spring: once claimed, more mana and a higher mana maximum
RuneChest, // dwarven chest with the plans of a rune forge tower (claim it: one free build)
Forge, // room: creatures manufacture traps and doors
Door, // floor with an installed door (res bit 0: locked)
Prison, // room: prisoners wait in cells
Graveyard, // room: the fallen are buried, some rise again
GuardPost, // room: creatures hold the position
Arena, // room: sparring
Altar, // room: sacrifices
Drawbridge, // bridge that can be raised (res: see DB_RAISED, DB_AUTO, DB_LAVA)
Obsidian, // lava cooled by frost: walkable until it melts again
Mushroom, // giant mushroom: imps fell it for wood once it is grown (food: growth left, 0 = grown; res: MUSH_GROVE)
Tree, // tree above ground: imps fell it for wood
Grove, // mushroom grove: mossy ground nobody can claim, giant mushrooms grow (back) here
}
// a giant mushroom of a grove: when felled, the ground turns back into grove (Tile::res)
pub const MUSH_GROVE: u8 = 1;
// drawbridge state in Tile::res
pub const DB_RAISED: u8 = 1; // raised: nobody can cross
pub const DB_AUTO: u8 = 2; // rises by itself when enemies come near
pub const DB_LAVA: u8 = 4; // spans lava (stone), else water (wood)
// options for the map generator (campaign missions)
#[derive(Clone, Debug)]
pub struct GenOpts {
pub portals: Option<usize>, // None: one per 64 x 64 tiles
pub treasures: Option<usize>, // knight treasure caves; None: none (free play has dwarf caves instead)
pub gate: bool, // hero gate at the far side
pub core_y: Option<i32>, // row of the core (None: about the middle)
}
impl Default for GenOpts {
fn default() -> GenOpts {
GenOpts { portals: None, treasures: None, gate: true, core_y: None }
}
}
// An underground river across the map
#[derive(Clone, Debug)]
pub struct River {
pub lava: bool,
pub vertical: bool, // runs from top to bottom (else from left to right)
pub pos: f32, // position across the map (0..1)
pub width: i32,
pub bridges: u32, // fixed bridges of the knights
}
// order matters for the packed savegame codec: only append new kinds
const KINDS: [Kind; 39] = [
Kind::Rock,
Kind::Earth,
Kind::Gold,
Kind::Floor,
Kind::Treasury,
Kind::Lair,
Kind::Farm,
Kind::Training,
Kind::Library,
Kind::Core,
Kind::Portal,
Kind::Gate,
Kind::Torture,
Kind::Tower,
Kind::Temple,
Kind::Water,
Kind::Lava,
Kind::Bridge,
Kind::LavaBridge,
Kind::Shard,
Kind::Silver,
Kind::Ruby,
Kind::Diamond,
Kind::Aerie,
Kind::Steel,
Kind::ManaSpring,
Kind::RuneChest,
Kind::Forge,
Kind::Door,
Kind::Prison,
Kind::Graveyard,
Kind::GuardPost,
Kind::Arena,
Kind::Altar,
Kind::Drawbridge,
Kind::Obsidian,
Kind::Mushroom,
Kind::Tree,
Kind::Grove,
];
// treasures (veins, treasury content, imp loads): 0 gold, 1 silver, 2 ruby, 3 diamond
pub const RES_GOLD: u8 = 0;
// value taken per pick strike for each treasure
pub const RES_STRIKE: [i32; 4] = [25, 20, 40, 60];
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub enum Owner {
None,
Player,
Hero,
Rival1, // AI dungeon masters (free play, campaign)
Rival2,
Rival3, // the fourth master side: a third rival, or a human master in a match
}
const OWNERS: [Owner; 6] = [Owner::None, Owner::Player, Owner::Hero, Owner::Rival1, Owner::Rival2, Owner::Rival3];
impl Owner {
// the side of a ground owner (255: nobody)
pub fn side(self) -> u8 {
match self {
Owner::Player => 0,
Owner::Hero => 1,
Owner::Rival1 => 2,
Owner::Rival2 => 3,
Owner::Rival3 => 4,
Owner::None => 255,
}
}
pub fn of_side(side: u8) -> Owner {
match side {
0 => Owner::Player,
1 => Owner::Hero,
2 => Owner::Rival1,
3 => Owner::Rival2,
4 => Owner::Rival3,
_ => Owner::None,
}
}
// the masters of a match: the four sides a dungeon master can play (1 is the heroes' side)
#[allow(dead_code)] // the sides of a match; the lobby (step C/D) uses them
pub const MASTER_SIDES: [u8; 4] = [0, 2, 3, 4];
}
// Short serde names (only used by version 1 savegames, newer ones use the packed codec)
#[derive(Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Tile {
#[serde(rename = "k")]
pub kind: Kind,
#[serde(rename = "o")]
pub owner: Owner,
#[serde(rename = "h")]
pub hp: f32, // dig resistance
#[serde(rename = "g")]
pub gold: i32, // gold left (gold vein)
#[serde(rename = "m")]
pub marked: bool, // marked for digging
#[serde(rename = "r")]
pub reserved: u32, // entity id + 1 working on this tile
#[serde(rename = "f")]
pub food: f32, // mushrooms in the mushroom garden
#[serde(rename = "b")]
pub bed: u32, // entity id + 1 sleeping here
#[serde(rename = "v")]
pub var: u8, // texture variant
#[serde(rename = "rf", default)]
pub reinforced: bool, // wall reinforced by imps
#[serde(rename = "rs", default)]
pub res: u8, // treasury: kind of treasure stored here (see RES_GOLD)
#[serde(rename = "mb", default)]
pub mark_by: u8, // side of the master who marked it for digging (its colour)
}
impl Tile {
pub fn new(kind: Kind) -> Tile {
Tile { kind, owner: Owner::None, hp: 0.0, gold: 0, marked: false, reserved: 0, food: 0.0, bed: 0, var: 0, reinforced: false, res: 0, mark_by: 0 }
}
pub fn solid(&self) -> bool {
matches!(self.kind, Kind::Rock | Kind::Earth | Kind::Gold | Kind::Silver | Kind::Ruby | Kind::Diamond | Kind::Steel)
}
pub fn diggable(&self) -> bool {
matches!(self.kind, Kind::Earth | Kind::Gold | Kind::Silver | Kind::Ruby | Kind::Diamond | Kind::Steel | Kind::Tree) || (self.kind == Kind::Mushroom && self.grown())
}
// a giant mushroom is felled only when it is fully grown (everything else counts as grown)
pub fn grown(&self) -> bool {
self.kind != Kind::Mushroom || self.food <= 0.0
}
// how far a giant mushroom has grown: 0 a sprout .. 1 fully grown
pub fn growth(&self) -> f32 {
if self.kind == Kind::Mushroom { (1.0 - self.food).clamp(0.0, 1.0) } else { 1.0 }
}
// a giant mushroom or a tree: felled like a wall is dug, gives wood
pub fn plant(&self) -> bool {
matches!(self.kind, Kind::Mushroom | Kind::Tree)
}
// the treasure of a vein (None for everything else)
pub fn vein(&self) -> Option<u8> {
match self.kind {
Kind::Gold => Some(0),
Kind::Silver => Some(1),
Kind::Ruby => Some(2),
Kind::Diamond => Some(3),
_ => None,
}
}
pub fn walkable(&self) -> bool {
matches!(
self.kind,
Kind::Floor
| Kind::Treasury
| Kind::Lair
| Kind::Farm
| Kind::Training
| Kind::Library
| Kind::Portal
| Kind::Gate
| Kind::Torture
| Kind::Temple
| Kind::Bridge
| Kind::LavaBridge
| Kind::Shard
| Kind::Aerie
| Kind::ManaSpring
| Kind::RuneChest
| Kind::Forge
| Kind::Door
| Kind::Prison
| Kind::Graveyard
| Kind::GuardPost
| Kind::Arena
| Kind::Altar
| Kind::Obsidian
| Kind::Grove
) || (self.kind == Kind::Drawbridge && self.res & DB_RAISED == 0)
}
// river tiles (open, but nobody can walk there)
pub fn liquid(&self) -> bool {
matches!(self.kind, Kind::Water | Kind::Lava)
}
pub fn bridge(&self) -> bool {
matches!(self.kind, Kind::Bridge | Kind::LavaBridge | Kind::Drawbridge)
}
pub fn raised(&self) -> bool {
self.kind == Kind::Drawbridge && self.res & DB_RAISED != 0
}
// the river below a bridge or river tile (obsidian: the lava under the crust)
pub fn river(&self) -> Option<Kind> {
match self.kind {
Kind::Water | Kind::Bridge => Some(Kind::Water),
Kind::Lava | Kind::LavaBridge | Kind::Obsidian => Some(Kind::Lava),
Kind::Drawbridge if self.res & DB_LAVA != 0 => Some(Kind::Lava),
Kind::Drawbridge => Some(Kind::Water),
_ => None,
}
}
}
pub fn idx(x: i32, y: i32) -> usize {
y as usize * mw() + x as usize
}
pub fn xy(i: usize) -> (i32, i32) {
let w = mw();
((i % w) as i32, (i / w) as i32)
}
pub fn inb(x: i32, y: i32) -> bool {
x >= 0 && y >= 0 && (x as usize) < mw() && (y as usize) < mh()
}
pub const DIR4: [(i32, i32); 4] = [(0, -1), (1, 0), (0, 1), (-1, 0)];
pub const DIR8: [(i32, i32); 8] = [(0, -1), (1, 0), (0, 1), (-1, 0), (1, -1), (1, 1), (-1, 1), (-1, -1)];
#[derive(Serialize, Deserialize)]
pub struct Rng(pub u32);
impl Rng {
pub fn next(&mut self) -> u32 {
let mut x = self.0;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
self.0 = x;
x
}
pub fn f(&mut self) -> f32 {
(self.next() >> 8) as f32 / (1u32 << 24) as f32
}
pub fn range(&mut self, a: i32, b: i32) -> i32 {
if b <= a {
return a;
}
a + (self.next() % (b - a + 1) as u32) as i32
}
}
#[derive(Serialize, Deserialize)]
pub struct Map {
#[serde(default = "default_size")]
pub w: usize,
#[serde(default = "default_size")]
pub h: usize,
#[serde(serialize_with = "tiles_ser", deserialize_with = "tiles_de")]
pub t: Vec<Tile>,
pub core: (i32, i32), // top-left corner of the core (3x3)
#[serde(default)]
pub portals: Vec<(i32, i32)>, // top-left corners of the portals (3x3)
#[serde(default, rename = "portal", skip_serializing)]
legacy_portal: Option<(i32, i32)>, // version 1 savegames had a single portal
pub gate: (i32, i32),
#[serde(default)]
pub treasures: Vec<(i32, i32)>, // centres of the treasure caves
#[serde(default, with = "crate::fog::bits")]
pub explored: Vec<bool>, // fog of war: tiles the dungeon knows (empty: no fog)
#[serde(skip)]
pub visible: Vec<bool>, // fog of war: tiles in sight right now
#[serde(default, with = "crate::fog::bits")]
pub sky: Vec<bool>, // tiles above ground (daylight, grass); empty: all underground
#[serde(default)]
pub upgrades: Vec<(u8, u8, u8)>, // researched room levels for the look: (master side, room index, level)
}
fn default_size() -> usize {
64
}
impl Map {
// Make this map's dimensions the active ones (after loading a savegame)
pub fn activate(&mut self) {
set_dims(self.w, self.h);
if self.portals.is_empty() {
if let Some(p) = self.legacy_portal.take() {
self.portals.push(p);
}
}
}
// tile i lies above ground
pub fn sky(&self, i: usize) -> bool {
self.sky.get(i).copied().unwrap_or(false)
}
pub fn set_sky(&mut self, i: usize, on: bool) {
if self.sky.len() != self.t.len() {
if !on {
return;
}
self.sky = vec![false; self.t.len()];
}
self.sky[i] = on;
}
pub fn get(&self, x: i32, y: i32) -> Option<&Tile> {
if inb(x, y) {
Some(&self.t[idx(x, y)])
} else {
None
}
}
pub fn walk(&self, x: i32, y: i32) -> bool {
inb(x, y) && self.t[idx(x, y)].walkable()
}
// Index of the portal containing tile (x, y)
pub fn portal_at(&self, x: i32, y: i32) -> Option<usize> {
self.portals.iter().position(|&(px, py)| x >= px && x < px + 3 && y >= py && y < py + 3)
}
// whose portal k is (any master can claim one)
pub fn portal_owner(&self, k: usize) -> Owner {
let (px, py) = self.portals[k];
self.t[idx(px, py)].owner
}
pub fn generate(seed: u32, w: usize, h: usize) -> Map {
Map::generate_with(seed, w, h, &GenOpts::default())
}
pub fn generate_with(seed: u32, w: usize, h: usize, opts: &GenOpts) -> Map {
let w = w.clamp(MIN_SIZE, MAX_SIZE);
let h = h.clamp(MIN_SIZE, MAX_SIZE);
set_dims(w, h);
let mut s = seed;
loop {
let m = Map::try_generate(s, w, h, opts);
// make sure heroes can reach the core
if !opts.gate || m.hero_path_to_core(idx(m.gate.0 + 1, m.gate.1 + 1)).is_some() {
return m;
}
s = s.wrapping_mul(747796405).wrapping_add(2891336453) | 1;
}
}
fn try_generate(seed: u32, w: usize, h: usize, opts: &GenOpts) -> Map {
let (wi, hi) = (w as i32, h as i32);
let mut rng = Rng(seed | 1);
for _ in 0..10 {
rng.next();
}
// content scales with the area; one portal per 64x64 tiles
let area = (w * h) as f32 / 4096.0;
let scale = |n: f32| (n * area).round() as i32;
let n_portals = opts.portals.unwrap_or((area.round() as usize).max(1));
let n_treasures = opts.treasures.unwrap_or(0);
let mut t = vec![Tile::new(Kind::Earth); w * h];
for (i, tile) in t.iter_mut().enumerate() {
let (x, y) = ((i % w) as i32, (i / w) as i32);
tile.var = (rng.next() % 4) as u8;
tile.hp = 6.0;
if x < 2 || y < 2 || x >= wi - 2 || y >= hi - 2 {
tile.kind = Kind::Rock;
}
}
let inner = |x: i32, y: i32| x >= 2 && y >= 2 && x < wi - 2 && y < hi - 2;
let core = (11, opts.core_y.unwrap_or(hi / 2 + rng.range(-5, 3)));
let gate = (wi - 13 + rng.range(-2, 2), rng.range(10, hi - 14));
// places features far apart: best of several random candidates
let mut taken: Vec<(i32, i32)> = vec![(core.0 + 1, core.1 + 1), (gate.0 + 1, gate.1 + 1)];
let place = |rng: &mut Rng, taken: &mut Vec<(i32, i32)>, margin: i32| -> (i32, i32) {
let mut best = (margin, margin);
let mut bd = -1;
for _ in 0..40 {
let p = (rng.range(margin, wi - margin - 3), rng.range(margin, hi - margin - 3));
let d = taken.iter().map(|q| (p.0 - q.0).pow(2) + (p.1 - q.1).pow(2)).min().unwrap_or(0);
if d > bd {
bd = d;
best = p;
}
}
taken.push(best);
best
};
// the first portal is close to the start
let first = (core.0 + rng.range(13, 19), (core.1 + if rng.f() < 0.5 { rng.range(-14, -10) } else { rng.range(10, 14) }).clamp(5, hi - 9));
taken.push(first);
let mut portals = if n_portals > 0 { vec![first] } else { Vec::new() };
for _ in 1..n_portals {
portals.push(place(&mut rng, &mut taken, 6));
}
let mut treasures = Vec::new();
for _ in 0..n_treasures {
let p = place(&mut rng, &mut taken, 7);
treasures.push((p.0 + 1, p.1 + 1));
}
let blob = |t: &mut Vec<Tile>, rng: &mut Rng, cx: i32, cy: i32, r: i32, k: Kind, gold: (i32, i32)| {
for y in cy - r..=cy + r {
for x in cx - r..=cx + r {
if !inner(x, y) {
continue;
}
let d = ((x - cx) * (x - cx) + (y - cy) * (y - cy)) as f32;
if d <= (r as f32 + 0.5).powi(2) - rng.f() * r as f32 {
let tl = &mut t[y as usize * w + x as usize];
tl.kind = k;
if tl.vein().is_some() {
tl.gold = rng.range(gold.0, gold.1);
}
}
}
}
};
for _ in 0..scale(22.0) {
let (cx, cy) = (rng.range(4, wi - 5), rng.range(4, hi - 5));
let r = rng.range(1, 3);
blob(&mut t, &mut rng, cx, cy, r, Kind::Rock, (0, 0));
}
for _ in 0..scale(16.0) {
let (cx, cy) = (rng.range(4, wi - 5), rng.range(4, hi - 5));
let r = rng.range(1, 2);
blob(&mut t, &mut rng, cx, cy, r, Kind::Gold, (250, 550));
}
// more treasures: silver is common, rubies are rare, diamonds very rare and far away
for _ in 0..scale(7.0) {
let (cx, cy) = (rng.range(4, wi - 5), rng.range(4, hi - 5));
let r = rng.range(1, 2);
blob(&mut t, &mut rng, cx, cy, r, Kind::Silver, (200, 400));
}
for _ in 0..scale(3.0) {
let (cx, cy) = (rng.range(wi / 4, wi - 5), rng.range(4, hi - 5));
blob(&mut t, &mut rng, cx, cy, 1, Kind::Ruby, (500, 800));
}
for _ in 0..scale(1.2).max(1) {
let (cx, cy) = (rng.range(wi / 3, wi - 5), rng.range(4, hi - 5));
let r = rng.range(0, 1);
blob(&mut t, &mut rng, cx, cy, r, Kind::Diamond, (1000, 1500));
}
// guaranteed gold near the start
for k in 0..3 {
let cx = core.0 + rng.range(-3, 9);
let cy = core.1 + if k % 2 == 0 { rng.range(-9, -6) } else { rng.range(7, 9) };
blob(&mut t, &mut rng, cx, cy, 1, Kind::Gold, (250, 550));
}
let room = |t: &mut Vec<Tile>, x0: i32, y0: i32, x1: i32, y1: i32, owner: Owner| {
for y in y0..=y1 {
for x in x0..=x1 {
if inner(x, y) {
let tl = &mut t[y as usize * w + x as usize];
tl.kind = Kind::Floor;
tl.owner = owner;
tl.gold = 0;
}
}
}
};
// small natural caves
for _ in 0..scale(4.0) {
let (cx, cy) = (rng.range(20, wi - 16), rng.range(6, hi - 6));
let (cw, ch) = (rng.range(1, 2), rng.range(1, 2));
room(&mut t, cx - cw, cy - ch, cx + cw, cy + ch, Owner::None);
}
// treasure caves: a cave surrounded by very rich gold veins (guarded by heroes, see Game::new)
for &(cx, cy) in &treasures {
blob(&mut t, &mut rng, cx, cy, 4, Kind::Gold, (1000, 1600));
// some of the riches are gems
for y in cy - 4..=cy + 4 {
for x in cx - 4..=cx + 4 {
if inner(x, y) && t[y as usize * w + x as usize].kind == Kind::Gold {
let r = rng.f();
let tl = &mut t[y as usize * w + x as usize];
if r < 0.08 {
tl.kind = Kind::Diamond;
tl.gold = rng.range(1200, 1800);
} else if r < 0.3 {
tl.kind = Kind::Ruby;
tl.gold = rng.range(800, 1200);
}
}
}
}
room(&mut t, cx - 2, cy - 2, cx + 2, cy + 2, Owner::None);
}
// starting area and core
room(&mut t, core.0 - 3, core.1 - 3, core.0 + 5, core.1 + 5, Owner::Player);
for y in 0..3 {
for x in 0..3 {
let tl = &mut t[(core.1 + y) as usize * w + (core.0 + x) as usize];
tl.kind = Kind::Core;
tl.owner = Owner::Player;
}
}
// portal caves
for &(px, py) in &portals {
room(&mut t, px - 2, py - 2, px + 4, py + 4, Owner::None);
for y in 0..3 {
for x in 0..3 {
t[(py + y) as usize * w + (px + x) as usize].kind = Kind::Portal;
}
}
}
// hero gate (campaign maps place the gate of the order in a fortress instead)
let gate = if opts.gate {
room(&mut t, gate.0 - 2, gate.1 - 2, gate.0 + 4, gate.1 + 4, Owner::Hero);
for y in 0..3 {
for x in 0..3 {
let tl = &mut t[(gate.1 + y) as usize * w + (gate.0 + x) as usize];
tl.kind = Kind::Gate;
tl.owner = Owner::Hero;
}
}
gate
} else {
(0, 0)
};
for tl in t.iter_mut() {
if tl.vein().is_some() {
tl.hp = 6.0;
}
}
Map { w, h, t, core, portals, legacy_portal: None, gate, treasures, explored: Vec::new(), visible: Vec::new(), sky: Vec::new(), upgrades: Vec::new() }
}
// Carves a meandering river; the player's area, the core and portals stay dry.
// Returns the tiles of the knight bridges.
pub fn carve_river(&mut self, r: &River, seed: u32) -> Vec<usize> {
let mut rng = Rng(seed | 1);
let (w, h) = (self.w as i32, self.h as i32);
let len = if r.vertical { h } else { w };
let across = if r.vertical { w } else { h };
let base = (r.pos * across as f32) as i32;
let (f1, f2) = (0.05 + rng.f() * 0.06, 0.13 + rng.f() * 0.08);
let (p1, p2) = (rng.f() * 6.0, rng.f() * 6.0);
let amp = (across as f32 * 0.06).min(6.0);
let mut centre = Vec::with_capacity(len as usize);
for s in 0..len {
let c = base as f32 + (s as f32 * f1 + p1).sin() * amp + (s as f32 * f2 + p2).sin() * amp * 0.4;
centre.push(c.round() as i32);
}
let at = |s: i32, o: i32| if r.vertical { (o, s) } else { (s, o) };
let kind = if r.lava { Kind::Lava } else { Kind::Water };
for s in 1..len - 1 {
for k in 0..r.width {
let o = centre[s as usize] - r.width / 2 + k;
let (x, y) = at(s, o);
if x < 1 || y < 1 || x >= w - 1 || y >= h - 1 {
continue;
}
let t = &mut self.t[idx(x, y)];
if t.owner == Owner::Player || matches!(t.kind, Kind::Core | Kind::Portal | Kind::Gate) {
continue;
}
t.kind = kind;
t.owner = Owner::None;
t.gold = 0;
t.marked = false;
t.reinforced = false;
}
}
// knight bridges, evenly spread, with a little landing at both ends
let mut out = Vec::new();
for b in 0..r.bridges {
let s = ((b as f32 + 0.5) / r.bridges as f32 * (len - 6) as f32) as i32 + 3;
for k in -1..=r.width {
let o = centre[s as usize] - r.width / 2 + k;
let (x, y) = at(s, o);
if !inb(x, y) || x < 1 || y < 1 || x >= w - 1 || y >= h - 1 {
continue;
}
let t = &mut self.t[idx(x, y)];
if t.liquid() {
t.kind = if r.lava { Kind::LavaBridge } else { Kind::Bridge };
t.owner = Owner::Hero;
out.push(idx(x, y));
} else if t.solid() && t.kind != Kind::Rock {
t.kind = Kind::Floor;
t.owner = Owner::None;
t.gold = 0;
}
}
}
out
}
// Generic Dijkstra search.
// cost(i) -> None = impassable, Some(c) = cost of entering.
// goal(i) -> true = goal reached.
pub fn search<C, G>(&self, start: usize, cost: C, goal: G, diag: bool) -> Option<Vec<usize>>
where
C: Fn(usize) -> Option<u32>,
G: Fn(usize) -> bool,
{
let mut dist = vec![u32::MAX; self.t.len()];
let mut prev = vec![usize::MAX; self.t.len()];
let mut heap = BinaryHeap::new();
dist[start] = 0;
heap.push(Reverse((0u32, start)));
while let Some(Reverse((d, i))) = heap.pop() {
if d > dist[i] {
continue;
}
if goal(i) {
let mut path = Vec::new();
let mut c = i;
while c != start {
path.push(c);
c = prev[c];
}
path.reverse();
return Some(path);
}
let (x, y) = xy(i);
let dirs: &[(i32, i32)] = if diag { &DIR8 } else { &DIR4 };
for (k, &(dx, dy)) in dirs.iter().enumerate() {
let nx = x + dx;
let ny = y + dy;
if !inb(nx, ny) {
continue;
}
let ni = idx(nx, ny);
let Some(c) = cost(ni) else { continue };
let mut step = c * 10;
if k >= 4 {
// diagonal only if both neighbours are walkable (no corner cutting)
// (judged with the same cost, so flyers may cut corners over water and lava)
let open = |cx: i32, cy: i32| cost(idx(cx, cy)) == Some(1);
if c > 1 || !open(x + dx, y) || !open(x, y + dy) {
continue;
}
step = 14;
}
let nd = d + step;
if nd < dist[ni] {
dist[ni] = nd;
prev[ni] = i;
heap.push(Reverse((nd, ni)));
}
}
}
None
}
pub fn walk_cost(&self) -> impl Fn(usize) -> Option<u32> + '_ {
self.creature_cost(false)
}
// flying creatures also cross water and lava
pub fn fly_cost(&self) -> impl Fn(usize) -> Option<u32> + '_ {
self.creature_cost(true)
}
// the dungeon's creatures walk on open ground; flyers also cross water and lava
pub fn creature_cost(&self, fly: bool) -> impl Fn(usize) -> Option<u32> + '_ {
move |i| {
let t = &self.t[i];
if fly && t.liquid() {
return Some(1);
}
// a locked door stops the dungeon's own creatures too (flyers included)
if t.walkable() && !(t.kind == Kind::Door && t.res & 1 == 1) {
Some(1)
} else {
None
}
}
}
// path for a walking or flying creature
pub fn creature_path(&self, fly: bool, start: usize, target: usize) -> Option<Vec<usize>> {
self.search(start, self.creature_cost(fly), |i| i == target, true)
}
pub fn fly_path_to(&self, start: usize, target: usize) -> Option<Vec<usize>> {
self.search(start, self.fly_cost(), |i| i == target, true)
}
pub fn hero_cost(&self) -> impl Fn(usize) -> Option<u32> + '_ {
self.hero_cost_as(false)
}
// attackers dig through earth; flying ones also cross water and lava
pub fn hero_cost_as(&self, fly: bool) -> impl Fn(usize) -> Option<u32> + '_ {
move |i| {
let tl = &self.t[i];
match tl.kind {
Kind::Water | Kind::Lava if fly => Some(1),
Kind::Earth if tl.reinforced => Some(70),
Kind::Steel => Some(300),
Kind::Earth => Some(7),
Kind::Gold | Kind::Silver | Kind::Ruby | Kind::Diamond => Some(12),
Kind::Rock | Kind::Core | Kind::Tower | Kind::Water | Kind::Lava => None,
Kind::Door => Some(10),
Kind::Mushroom | Kind::Tree => Some(6),
Kind::Drawbridge if tl.res & DB_RAISED != 0 => None,
_ => Some(1),
}
}
}
// next to the player's soul core
pub fn adj_core(&self, i: usize) -> bool {
self.adj_core_of(i, Owner::Player)
}
// next to the soul core of a master (player or rival)
pub fn adj_core_of(&self, i: usize, owner: Owner) -> bool {
let (x, y) = xy(i);
DIR4.iter().any(|&(dx, dy)| self.get(x + dx, y + dy).map_or(false, |t| t.kind == Kind::Core && t.owner == owner))
}
pub fn hero_path_to_core(&self, start: usize) -> Option<Vec<usize>> {
self.hero_path_to_core_as(false, start)
}
pub fn hero_path_to_core_as(&self, fly: bool, start: usize) -> Option<Vec<usize>> {
self.search(start, self.hero_cost_as(fly), |i| self.adj_core(i), true)
}
pub fn path_to(&self, start: usize, target: usize) -> Option<Vec<usize>> {
self.search(start, self.walk_cost(), |i| i == target, true)
}
}
// ------------------------------------------------------------------ compact tile codec
//
// Savegame version 2 stores the tiles as one base64 string: the 2-bit texture variants,
// then run-length encoded tile records (kind, owner, marked, hp, gold, food, reserved, bed).
// A 256x256 map shrinks from ~4.5 MB of JSON to a few hundred KB.
const B64: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
pub fn b64_encode(data: &[u8]) -> String {
let mut out = String::with_capacity(data.len() * 4 / 3 + 4);
for chunk in data.chunks(3) {
let b = [chunk[0], *chunk.get(1).unwrap_or(&0), *chunk.get(2).unwrap_or(&0)];
let v = (b[0] as u32) << 16 | (b[1] as u32) << 8 | b[2] as u32;
for k in 0..4 {
if k <= chunk.len() {
out.push(B64[(v >> (18 - 6 * k) & 63) as usize] as char);
} else {
out.push('=');
}
}
}
out
}
pub fn b64_decode(s: &str) -> Option<Vec<u8>> {
let mut val = [255u8; 256];
for (i, &c) in B64.iter().enumerate() {
val[c as usize] = i as u8;
}
let bytes: Vec<u8> = s.bytes().filter(|&c| c != b'=').collect();
let mut out = Vec::with_capacity(bytes.len() * 3 / 4);
for chunk in bytes.chunks(4) {
let mut v = 0u32;
for (k, &c) in chunk.iter().enumerate() {
let d = val[c as usize];
if d == 255 {
return None;
}
v |= (d as u32) << (18 - 6 * k);
}
let n = chunk.len() * 6 / 8;
for k in 0..n {
out.push((v >> (16 - 8 * k)) as u8);
}
}
Some(out)
}
const REC: usize = 23;
fn tile_record(t: &Tile) -> [u8; REC] {
let mut r = [0u8; REC];
r[0] = KINDS.iter().position(|&k| k == t.kind).unwrap_or(0) as u8;
r[1] = OWNERS.iter().position(|&o| o == t.owner).unwrap_or(0) as u8;
r[2] = t.marked as u8 | (t.reinforced as u8) << 1 | (t.res & 3) << 2 | (t.mark_by & 7) << 4;
r[3..7].copy_from_slice(&t.hp.to_le_bytes());
r[7..11].copy_from_slice(&t.gold.to_le_bytes());
r[11..15].copy_from_slice(&t.food.to_le_bytes());
r[15..19].copy_from_slice(&t.reserved.to_le_bytes());
r[19..23].copy_from_slice(&t.bed.to_le_bytes());
r
}
fn tiles_ser<S: Serializer>(tiles: &[Tile], s: S) -> Result<S::Ok, S::Error> {
let mut out = Vec::new();
out.extend_from_slice(&(tiles.len() as u32).to_le_bytes());
// texture variants, 4 per byte
for chunk in tiles.chunks(4) {
let mut b = 0u8;
for (k, t) in chunk.iter().enumerate() {
b |= (t.var & 3) << (k * 2);
}
out.push(b);
}
// runs of identical records
let mut i = 0;
while i < tiles.len() {
let rec = tile_record(&tiles[i]);
let mut run = 1;
while i + run < tiles.len() && run < 65535 && tile_record(&tiles[i + run]) == rec {
run += 1;
}
out.extend_from_slice(&(run as u16).to_le_bytes());
out.extend_from_slice(&rec);
i += run;
}
s.serialize_str(&format!("v2:{}", b64_encode(&out)))
}
#[derive(Deserialize)]
#[serde(untagged)]
enum TilesIn {
Packed(String),
Plain(Vec<Tile>), // version 1 savegames
}
fn tiles_de<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<Tile>, D::Error> {
use serde::de::Error;
match TilesIn::deserialize(d)? {
TilesIn::Plain(v) => Ok(v),
TilesIn::Packed(s) => {
let data = s.strip_prefix("v2:").and_then(b64_decode).ok_or_else(|| D::Error::custom("bad tile data"))?;
let rd32 = |o: usize| -> Option<[u8; 4]> { data.get(o..o + 4).map(|s| [s[0], s[1], s[2], s[3]]) };
let count = u32::from_le_bytes(rd32(0).ok_or_else(|| D::Error::custom("bad tile data"))?) as usize;
if count > MAX_SIZE * MAX_SIZE {
return Err(D::Error::custom("map too large"));
}
let mut tiles = Vec::with_capacity(count);
let mut o = 4 + (count + 3) / 4;
while tiles.len() < count {
let run = data.get(o..o + 2).map(|s| u16::from_le_bytes([s[0], s[1]])).ok_or_else(|| D::Error::custom("bad tile data"))? as usize;
let r = data.get(o + 2..o + 2 + REC).ok_or_else(|| D::Error::custom("bad tile data"))?;
let f = |a: usize| [r[a], r[a + 1], r[a + 2], r[a + 3]];
let t = Tile {
kind: *KINDS.get(r[0] as usize).unwrap_or(&Kind::Rock),
owner: *OWNERS.get(r[1] as usize).unwrap_or(&Owner::None),
marked: r[2] & 1 != 0,
reinforced: r[2] & 2 != 0,
res: (r[2] >> 2) & 3,
mark_by: (r[2] >> 4) & 7,
hp: f32::from_le_bytes(f(3)),
gold: i32::from_le_bytes(f(7)),
food: f32::from_le_bytes(f(11)),
reserved: u32::from_le_bytes(f(15)),
bed: u32::from_le_bytes(f(19)),
var: 0,
};
for _ in 0..run.min(count - tiles.len()) {
tiles.push(t);
}
o += 2 + REC;
}
for (i, t) in tiles.iter_mut().enumerate() {
t.var = (data[4 + i / 4] >> ((i % 4) * 2)) & 3;
}
Ok(tiles)
}
}
}