246 lines
10 KiB
Rust
246 lines
10 KiB
Rust
// Wood, the second resource. Underground it comes from giant mushrooms (few, growing back
|
|
// slowly), above ground from trees (many). Pit imps fell them like they dig (mark them with the
|
|
// pick); the wood goes to the master at once. Wooden bridges, wooden drawbridges and wooden
|
|
// doors cost wood; stone bridges, iron and magic doors do not.
|
|
|
|
use crate::game::*;
|
|
use crate::map::*;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
// every master starts with a little wood
|
|
pub const WOOD_START: f32 = 12.0;
|
|
// wood from one giant mushroom / one tree
|
|
pub const WOOD_MUSHROOM: f32 = 4.0;
|
|
pub const WOOD_TREE: f32 = 3.0;
|
|
// strikes a plant takes
|
|
pub const MUSHROOM_HP: f32 = 4.0;
|
|
#[allow(dead_code)] // trees come with the surface world
|
|
pub const TREE_HP: f32 = 6.0;
|
|
// seconds until a felled giant mushroom sprouts again, and from sprout to fully grown
|
|
pub const REGROW: f32 = 90.0;
|
|
pub const GROW_TIME: f32 = 300.0;
|
|
// wood a wooden bridge tile, a wooden drawbridge tile and a wooden door cost
|
|
pub const BRIDGE_WOOD: f32 = 2.0;
|
|
pub const DRAWBRIDGE_WOOD: f32 = 4.0;
|
|
pub const DOOR_WOOD: f32 = 3.0;
|
|
// mushroom groves of a random map: one per this many map tiles (at least 2, at most 6)
|
|
const GROVE_SHARE: usize = 2200;
|
|
const GROVE_RADIUS: i32 = 4;
|
|
|
|
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
|
pub struct WoodWork {
|
|
pub regrow: Vec<(usize, f32)>, // felled mushroom tile, seconds until it sprouts again
|
|
#[serde(default)]
|
|
pub growing: Vec<usize>, // giant mushrooms still growing (Tile::food: growth left)
|
|
}
|
|
|
|
// open ground of nobody (or a grove's moss)
|
|
fn open_ground(t: &Tile) -> bool {
|
|
(t.kind == Kind::Floor && t.owner == Owner::None) || t.kind == Kind::Grove
|
|
}
|
|
|
|
// a tile where a plant may stand: all eight neighbours open, no other plant next to it (so a
|
|
// plant never closes a corridor)
|
|
fn lonely_spot(map: &Map, x: i32, y: i32) -> bool {
|
|
if !inb(x, y) || !open_ground(&map.t[idx(x, y)]) {
|
|
return false;
|
|
}
|
|
DIR8.iter().all(|&(dx, dy)| map.get(x + dx, y + dy).map_or(false, |n| n.walkable() && !n.bridge() && n.kind != Kind::Door && !n.plant()))
|
|
}
|
|
|
|
impl Game {
|
|
// mushroom groves in the open caverns of a random map (deterministic, from the seed): mossy
|
|
// ground nobody can claim, giant mushrooms in a loose grid that grow back when felled
|
|
pub fn grow_mushrooms(&mut self, seed: u32) {
|
|
let mut rng = Rng(seed.wrapping_mul(0x9e37_79b9) ^ 0x6d2b_79f5 | 1);
|
|
let (cx, cy) = (self.map.core.0 + 1, self.map.core.1 + 1);
|
|
let open = |g: &Game, x: i32, y: i32| inb(x, y) && !g.map.sky(idx(x, y)) && open_ground(&g.map.t[idx(x, y)]) && !g.map.t[idx(x, y)].bridge();
|
|
// centres: open ground with open ground all around, away from the heart
|
|
let r = GROVE_RADIUS;
|
|
let centres: Vec<usize> = (0..self.map.t.len())
|
|
.filter(|&i| {
|
|
let (x, y) = xy(i);
|
|
(x - cx).abs().max((y - cy).abs()) > 12 && (-2..=2).all(|dy| (-2..=2).all(|dx| open(self, x + dx, y + dy)))
|
|
})
|
|
.collect();
|
|
let want = (self.map.t.len() / GROVE_SHARE).clamp(2, 6);
|
|
let mut groves: Vec<(i32, i32)> = Vec::new();
|
|
let mut tries = 0;
|
|
while groves.len() < want && tries < 200 && !centres.is_empty() {
|
|
tries += 1;
|
|
let (x, y) = xy(centres[(rng.next() as usize) % centres.len()]);
|
|
if groves.iter().any(|&(gx, gy)| (gx - x).abs().max((gy - y).abs()) < 3 * r) {
|
|
continue;
|
|
}
|
|
groves.push((x, y));
|
|
}
|
|
for &(gx, gy) in &groves {
|
|
// the moss: the open ground of a rough disc
|
|
for y in gy - r..=gy + r {
|
|
for x in gx - r..=gx + r {
|
|
let d2 = (x - gx).pow(2) + (y - gy).pow(2);
|
|
if d2 <= r * r + (hash32(x, y) % 3) as i32 - 1 && open(self, x, y) {
|
|
let t = &mut self.map.t[idx(x, y)];
|
|
t.kind = Kind::Grove;
|
|
t.owner = Owner::None;
|
|
}
|
|
}
|
|
}
|
|
// the mushrooms: every other tile in both directions, some still growing
|
|
for y in gy - r + 1..gy + r {
|
|
for x in gx - r + 1..gx + r {
|
|
if (x - gx) % 2 != 0 || (y - gy) % 2 != 0 {
|
|
continue;
|
|
}
|
|
let i = idx(x, y);
|
|
if self.map.t[i].kind != Kind::Grove || !lonely_spot(&self.map, x, y) || self.ents.iter().any(|e| e.alive && e.tile() == i) {
|
|
continue;
|
|
}
|
|
let left = if rng.f() < 0.6 { 0.0 } else { rng.f() };
|
|
self.plant_mushroom(i, left);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// a giant mushroom on tile i; `left`: how much of its growth is still to come (0 = grown)
|
|
pub fn plant_mushroom(&mut self, i: usize, left: f32) {
|
|
let grove = self.map.t[i].kind == Kind::Grove || self.map.t[i].res & MUSH_GROVE != 0;
|
|
let t = &mut self.map.t[i];
|
|
t.kind = Kind::Mushroom;
|
|
t.owner = Owner::None;
|
|
t.hp = MUSHROOM_HP;
|
|
t.marked = false;
|
|
t.food = left.clamp(0.0, 1.0);
|
|
t.res = if grove { MUSH_GROVE } else { 0 };
|
|
if t.food > 0.0 && !self.woodwork.growing.contains(&i) {
|
|
self.woodwork.growing.push(i);
|
|
}
|
|
}
|
|
|
|
// an imp felled the plant at tile i: wood for its master, a mushroom grows back later
|
|
pub(crate) fn felled(&mut self, i: usize) {
|
|
let kind = self.map.t[i].kind;
|
|
let wood = if kind == Kind::Tree { WOOD_TREE } else { WOOD_MUSHROOM };
|
|
self.wood += wood;
|
|
self.count(crate::stats::WOOD, wood);
|
|
if kind == Kind::Mushroom {
|
|
self.woodwork.regrow.push((i, REGROW));
|
|
}
|
|
let (x, y) = xy(i);
|
|
self.fx(5, x as f32 + 0.5, y as f32 + 0.5, 0.0, 0.0, 0.5, 0xff3070a0);
|
|
}
|
|
|
|
// pays wood; false (and nothing paid) if there is not enough
|
|
pub fn spend_wood(&mut self, n: f32) -> bool {
|
|
if self.wood + 1e-3 < n {
|
|
return false;
|
|
}
|
|
self.wood = (self.wood - n).max(0.0);
|
|
true
|
|
}
|
|
|
|
// the shared step: young mushrooms grow, felled ones sprout again where there is room
|
|
pub(crate) fn update_wood(&mut self, dt: f32) {
|
|
let mut k = 0;
|
|
while k < self.woodwork.growing.len() {
|
|
let i = self.woodwork.growing[k];
|
|
let t = &mut self.map.t[i];
|
|
if t.kind != Kind::Mushroom {
|
|
self.woodwork.growing.swap_remove(k);
|
|
continue;
|
|
}
|
|
t.food = (t.food - dt / GROW_TIME).max(0.0);
|
|
if t.food <= 0.0 {
|
|
self.woodwork.growing.swap_remove(k);
|
|
} else {
|
|
k += 1;
|
|
}
|
|
}
|
|
let mut k = 0;
|
|
while k < self.woodwork.regrow.len() {
|
|
self.woodwork.regrow[k].1 -= dt;
|
|
if self.woodwork.regrow[k].1 > 0.0 {
|
|
k += 1;
|
|
continue;
|
|
}
|
|
let i = self.woodwork.regrow[k].0;
|
|
let (x, y) = xy(i);
|
|
if lonely_spot(&self.map, x, y) && !self.ents.iter().any(|e| e.alive && e.tile() == i) {
|
|
// a sprout: it takes GROW_TIME until it can be felled again
|
|
self.plant_mushroom(i, 1.0);
|
|
self.woodwork.regrow.swap_remove(k);
|
|
} else if !open_ground(&self.map.t[i]) {
|
|
// built over or claimed: it does not come back
|
|
self.woodwork.regrow.swap_remove(k);
|
|
} else {
|
|
self.woodwork.regrow[k].1 = 30.0;
|
|
k += 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn hash32(x: i32, y: i32) -> u32 {
|
|
let h = (x as u32).wrapping_mul(2654435761) ^ (y as u32).wrapping_mul(2246822519);
|
|
(h ^ (h >> 15)).wrapping_mul(2246822519) >> 7
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn random_maps_have_mushroom_groves() {
|
|
let g = Game::new(9, 96, 96);
|
|
let groves = g.map.t.iter().filter(|t| t.kind == Kind::Grove).count();
|
|
let n = g.map.t.iter().filter(|t| t.kind == Kind::Mushroom).count();
|
|
assert!(groves >= 30, "{} grove tiles", groves);
|
|
assert!(n >= 6, "{} mushrooms", n);
|
|
assert!((g.wood - WOOD_START).abs() < 1e-3);
|
|
// none closes a corridor: every neighbour is open ground; all stand in a grove
|
|
for i in 0..g.map.t.len() {
|
|
if g.map.t[i].kind == Kind::Mushroom {
|
|
let (x, y) = xy(i);
|
|
assert!(DIR8.iter().all(|&(dx, dy)| g.map.get(x + dx, y + dy).map_or(false, |n| n.walkable())));
|
|
assert!(g.map.t[i].res & MUSH_GROVE != 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn grove_mushrooms_grow_slowly_are_felled_only_when_grown_and_come_back() {
|
|
let mut g = Game::new(9, 96, 96);
|
|
let i = g.map.t.iter().position(|t| t.kind == Kind::Mushroom && t.grown()).unwrap();
|
|
assert!(g.map.t[i].diggable());
|
|
let w0 = g.wood;
|
|
g.felled(i);
|
|
g.dig_out(i);
|
|
assert_eq!(g.wood, w0 + WOOD_MUSHROOM);
|
|
assert_eq!(g.map.t[i].kind, Kind::Grove, "the moss stays");
|
|
// the grove can't be claimed or marked
|
|
let (x, y) = xy(i);
|
|
g.mark_rect(x, y, x, y, true);
|
|
assert!(!g.map.t[i].marked);
|
|
assert!(!g.claimable(i, 1));
|
|
// it sprouts again: small, and not to be felled yet
|
|
let idx_r = g.woodwork.regrow.iter().position(|r| r.0 == i).unwrap();
|
|
g.woodwork.regrow[idx_r].1 = 0.01;
|
|
g.update_wood(0.05);
|
|
assert_eq!(g.map.t[i].kind, Kind::Mushroom, "a sprout");
|
|
assert!(!g.map.t[i].grown() && !g.map.t[i].diggable());
|
|
g.mark_rect(x, y, x, y, true);
|
|
assert!(!g.map.t[i].marked, "a young mushroom can't be marked");
|
|
// halfway
|
|
for _ in 0..(GROW_TIME as usize / 2) {
|
|
g.update_wood(1.0);
|
|
}
|
|
assert!(!g.map.t[i].grown() && g.map.t[i].growth() > 0.4);
|
|
for _ in 0..(GROW_TIME as usize / 2 + 2) {
|
|
g.update_wood(1.0);
|
|
}
|
|
assert!(g.map.t[i].grown() && g.map.t[i].diggable(), "fully grown: ready to fell");
|
|
assert!(g.spend_wood(3.0) && !g.spend_wood(1000.0));
|
|
}
|
|
}
|