Wood: giant mushrooms underground, felled by imps; wooden bridges and doors cost wood

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-05 22:07:59 +01:00
1 parent c5adc3e902
commit 398bb95681
28 files changed
+447 -31

No files matched your search

+5 -1
View File
@@ -35,7 +35,7 @@ enum Cat {
const CATS: [Cat; 9] = [Cat::Terrain, Cat::Rooms, Cat::Places, Cat::Units, Cat::Towers, Cat::Traps, Cat::Forts, Cat::Zones, Cat::Erase];
const TERRAIN: [&str; 13] = ["floor", "earth", "reinforced", "rock", "water", "lava", "bridge", "lavabridge", "gold", "silver", "ruby", "diamond", "steel"];
const TERRAIN: [&str; 14] = ["floor", "earth", "reinforced", "rock", "water", "lava", "bridge", "lavabridge", "gold", "silver", "ruby", "diamond", "steel", "mushroom"];
const PLACES: [&str; 7] = ["core", "portal", "gate", "spring", "shard", "chest", "chained"];
const OWNERS: [Owner; 6] = [Owner::Player, Owner::Hero, Owner::Rival1, Owner::Rival2, Owner::Rival3, Owner::None];
const ZONE_SHAPES: [&str; 4] = ["rect", "circle", "tiles", "line"];
@@ -75,6 +75,7 @@ fn kind_of_terrain(t: &str) -> (Kind, bool) {
"lava" => (Kind::Lava, false),
"bridge" => (Kind::Bridge, false),
"lavabridge" => (Kind::LavaBridge, false),
"mushroom" => (Kind::Mushroom, false),
"gold" => (Kind::Gold, false),
"silver" => (Kind::Silver, false),
"ruby" => (Kind::Ruby, false),
@@ -216,6 +217,9 @@ impl MapEdit {
tl.reinforced = reinforced;
tl.owner = if reinforced && owner == Owner::None { Owner::Player } else { owner };
tl.marked = false;
if tl.plant() {
tl.hp = crate::wood::MUSHROOM_HP;
}
tl.gold = match k {
Kind::Gold => 1200,
Kind::Silver => 900,
+29 -6
View File
@@ -462,6 +462,7 @@ pub struct MasterState {
pub core: (i32, i32),
pub core_hp: f32,
pub gold: f32,
pub wood: f32,
pub mana: f32,
pub payday: f32,
pub payday_warned: bool,
@@ -586,6 +587,10 @@ pub struct Game {
#[serde(default)]
pub bridgework: crate::bridges::BridgeWork, // burning and worn bridges
#[serde(default)]
pub wood: f32, // the second resource (see wood.rs)
#[serde(default)]
pub woodwork: crate::wood::WoodWork, // felled mushrooms growing back
#[serde(default)]
pub dwarf_t: f32,
#[serde(default)]
pub rune_free: u32, // rune forge towers that can be built for free (from chests)
@@ -749,9 +754,9 @@ pub const ROOM_NAMES: [&str; N_ROOMS] = [
];
// bridge cost per tile: over water, over lava
pub const BRIDGE_COST: [i32; 2] = [30, 75];
// a drawbridge tile (over water or lava)
pub const DRAWBRIDGE_COST: i32 = 150;
pub const BRIDGE_COST: [i32; 2] = [10, 75];
// a drawbridge tile: over water (and wood, see wood.rs), over lava
pub const DRAWBRIDGE_COST: [i32; 2] = [120, 150];
// steel wall cost per tile: from earth, from a reinforced wall
pub const STEEL_COST: [i32; 2] = [80, 40];
pub const STEEL_HP: f32 = 40.0;
@@ -904,6 +909,8 @@ impl Game {
vrng: crate::particles::vrng(),
dwarf_caves: Vec::new(),
bridgework: Default::default(),
wood: crate::wood::WOOD_START,
woodwork: Default::default(),
dwarf_t: 30.0,
rune_free: 0,
rune_final: 0,
@@ -1394,6 +1401,7 @@ impl Game {
core: self.map.core,
core_hp: self.core_hp,
gold: self.gold,
wood: self.wood,
mana: self.mana,
payday: self.payday,
payday_warned: self.payday_warned,
@@ -1423,6 +1431,7 @@ impl Game {
self.map.core = m.core;
self.core_hp = m.core_hp;
self.gold = m.gold;
self.wood = m.wood;
self.mana = m.mana;
self.payday = m.payday;
self.payday_warned = m.payday_warned;
@@ -1657,6 +1666,7 @@ impl Game {
}
self.update_dwarves(dt);
self.update_bridges(dt);
self.update_wood(dt);
// fallen hearts of a match are shared state: every client marks them in the same step
self.update_fallen_masters();
// the end of the game and the fog are about the master at this computer
@@ -2896,7 +2906,7 @@ impl Game {
}
pub fn build_bridges_of(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, draw: bool) {
let (mut built, mut broke, mut any_river) = (0, false, false);
let (mut built, mut broke, mut any_river, mut no_wood) = (0, false, false, false);
loop {
let mut add = Vec::new();
for y in y0.min(y1)..=y0.max(y1) {
@@ -2917,12 +2927,19 @@ impl Game {
let mut progress = false;
for i in add {
let lava = self.map.t[i].kind == Kind::Lava;
let cost = if draw { DRAWBRIDGE_COST } else { BRIDGE_COST[lava as usize] } as f32;
let cost = if draw { DRAWBRIDGE_COST[lava as usize] } else { BRIDGE_COST[lava as usize] } as f32;
// wooden bridges also cost wood
let wood = if lava { 0.0 } else if draw { crate::wood::DRAWBRIDGE_WOOD } else { crate::wood::BRIDGE_WOOD };
if self.gold < cost {
broke = true;
continue;
}
if self.wood + 1e-3 < wood {
no_wood = true;
continue;
}
self.gold -= cost;
self.wood -= wood;
let t = &mut self.map.t[i];
t.kind = if draw {
Kind::Drawbridge
@@ -2948,6 +2965,9 @@ impl Game {
if broke {
self.msg("msg.no_gold", vec![]);
self.sfx_ui(Sfx::Error);
} else if no_wood {
self.msg("msg.no_wood", vec![]);
self.sfx_ui(Sfx::Error);
} else if built == 0 {
self.hint(21, if any_river { "hint.bridge_where" } else { "hint.bridge_river" });
}
@@ -3300,6 +3320,9 @@ impl Game {
self.sfx(Sfx::Dig, fxx, fxy);
}
if done {
if self.map.t[t].plant() {
self.felled(t);
}
self.dig_out(t);
e.job = Job::None;
}
@@ -4728,7 +4751,7 @@ impl Game {
}
if t.owner == self.me && t.bridge() {
let lava = t.river() == Some(Kind::Lava);
let cost = if t.kind == Kind::Drawbridge { DRAWBRIDGE_COST } else { BRIDGE_COST[lava as usize] };
let cost = if t.kind == Kind::Drawbridge { DRAWBRIDGE_COST[lava as usize] } else { BRIDGE_COST[lava as usize] };
t.kind = if lava { Kind::Lava } else { Kind::Water };
t.owner = Owner::None;
t.reserved = 0;
+40
View File
@@ -83,6 +83,11 @@ pub const T_RIVAL3: u32 = 189; // 4 variants: floor of the yellow master
pub const I_DRAWBRIDGE: u32 = 193; // drawbridge tool: a raised flap on chains
pub const I_FREEZE: u32 = 194; // frost spell: a snowflake
pub const T_OBSIDIAN: u32 = 195; // 4 variants: cooled lava, dark glass with glowing seams
pub const T_STEM: u32 = 199; // giant mushroom stem
pub const T_CAP: u32 = 200; // giant mushroom cap, glowing spots
pub const T_BARK: u32 = 201; // tree trunk
pub const T_LEAVES: u32 = 202; // 2 variants: tree crown
pub const T_GRASS: u32 = 204; // 4 variants: grass above ground
pub struct Gfx {
pub atlas: Texture2D,
@@ -835,6 +840,39 @@ fn gen_atlas() -> Image {
None
}
});
// giant mushroom: pale ribbed stem, a violet cap with glowing spots
cv.cell(T_STEM, |x, y| {
let r = hash(x, y, 410);
Some(if x % 4 == 0 { c(150, 140, 130) } else if r % 9 == 0 { c(170, 160, 150) } else { c(200, 192, 178) })
});
cv.cell(T_CAP, |x, y| {
let spot = |cx: i32, cy: i32| (x - cx).pow(2) + (y - cy).pow(2) <= 2;
if spot(4, 4) || spot(11, 6) || spot(6, 11) || spot(12, 12) {
Some(c(150, 255, 230))
} else if (x + y) % 5 == 0 {
Some(c(90, 50, 130))
} else {
Some(c(120, 66, 170))
}
});
// tree: bark and two kinds of crown
cv.cell(T_BARK, |x, y| {
let r = hash(x, y, 420);
Some(if x % 5 == 0 || r % 7 == 0 { c(60, 40, 24) } else { c(96, 66, 40) })
});
for v in 0..2u32 {
cv.cell(T_LEAVES + v, |x, y| {
let r = hash(x, y, 430 + v);
Some(if r % 5 == 0 { c(30, 70, 30) } else if r % 7 == 0 { c(110, 160, 60) } else if v == 0 { c(56, 110, 44) } else { c(70, 120, 40) })
});
}
// grass above ground
for v in 0..4u32 {
cv.cell(T_GRASS + v, |x, y| {
let r = hash(x, y, 440 + v);
Some(if r % 11 == 0 { c(120, 170, 70) } else if r % 6 == 0 { c(50, 96, 40) } else if r % 23 == 0 { c(200, 190, 90) } else { c(74, 126, 52) })
});
}
// obsidian: cooled lava, dark glass with a few glowing seams
for v in 0..4u32 {
cv.cell(T_OBSIDIAN + v, |x, y| {
@@ -1976,6 +2014,8 @@ impl Gfx {
Kind::Lava => T_LAVA + v,
Kind::Bridge | Kind::Drawbridge => T_BRIDGE + (v & 1),
Kind::Obsidian => T_OBSIDIAN + v,
Kind::Mushroom => T_FLOOR + v,
Kind::Tree => T_GRASS + v,
Kind::LavaBridge => T_STONEBRIDGE + (v & 1),
Kind::Shard => T_HEROFLOOR + v,
Kind::Aerie => T_AERIE + v,
+1
View File
@@ -42,6 +42,7 @@ mod touch;
mod ui;
mod voice;
mod wilds;
mod wood;
mod xm;
use cloud::{Cloud, CloudEvent};
+12 -2
View File
@@ -70,6 +70,8 @@ pub enum Kind {
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 on open ground: imps fell it for wood (grows back)
Tree, // tree above ground: imps fell it for wood
}
// drawbridge state in Tile::res
@@ -103,7 +105,7 @@ pub struct River {
}
// order matters for the packed savegame codec: only append new kinds
const KINDS: [Kind; 36] = [
const KINDS: [Kind; 38] = [
Kind::Rock,
Kind::Earth,
Kind::Gold,
@@ -140,6 +142,8 @@ const KINDS: [Kind; 36] = [
Kind::Altar,
Kind::Drawbridge,
Kind::Obsidian,
Kind::Mushroom,
Kind::Tree,
];
// treasures (veins, treasury content, imp loads): 0 gold, 1 silver, 2 ruby, 3 diamond
@@ -223,7 +227,11 @@ impl Tile {
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)
matches!(self.kind, Kind::Earth | Kind::Gold | Kind::Silver | Kind::Ruby | Kind::Diamond | Kind::Steel | Kind::Mushroom | Kind::Tree)
}
// 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> {
@@ -751,6 +759,8 @@ impl Map {
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),
}
}
+38
View File
@@ -401,6 +401,17 @@ impl MB {
);
}
}
// An axis-aligned box without its bottom (plants: stems, caps, trunks, crowns)
fn boxy(&mut self, lo: Vec3, hi: Vec3, cell: u32, c: [u8; 4], top: [u8; 4]) {
let (a, b) = cell_uv(cell);
let uv = [vec2(a.x, b.y), vec2(b.x, b.y), vec2(b.x, a.y), vec2(a.x, a.y)];
let (x0, y0, z0, x1, y1, z1) = (lo.x, lo.y, lo.z, hi.x, hi.y, hi.z);
self.quad([vec3(x0, y1, z0), vec3(x1, y1, z0), vec3(x1, y1, z1), vec3(x0, y1, z1)], uv, [top; 4], [Vec3::Y; 4]);
self.quad([vec3(x0, y0, z0), vec3(x1, y0, z0), vec3(x1, y1, z0), vec3(x0, y1, z0)], uv, [c; 4], [-Vec3::Z; 4]);
self.quad([vec3(x1, y0, z1), vec3(x0, y0, z1), vec3(x0, y1, z1), vec3(x1, y1, z1)], uv, [c; 4], [Vec3::Z; 4]);
self.quad([vec3(x0, y0, z1), vec3(x0, y0, z0), vec3(x0, y1, z0), vec3(x0, y1, z1)], uv, [c; 4], [-Vec3::X; 4]);
self.quad([vec3(x1, y0, z0), vec3(x1, y0, z1), vec3(x1, y1, z1), vec3(x1, y1, z0)], uv, [c; 4], [Vec3::X; 4]);
}
// Thin strip lying on a surface (normal n) from p to q, used for tile outlines
fn strip(&mut self, p: Vec3, q: Vec3, n: Vec3, w: f32, c: [u8; 4]) {
let side = (q - p).cross(n).normalize_or_zero() * (w * 0.5);
@@ -834,6 +845,31 @@ fn build_chunk(map: &Map, amb: &[f32], glow: &[f32], cx: i32, cz: i32, atlas: &T
}
// room decor
let dc = col(lava_lit(Vec3::splat(amb_at(amb, xf + 0.5, zf + 0.5)), lava_at(glow, xf + 0.5, zf + 0.5)), 1.0);
if t.plant() {
// a giant mushroom or a tree (marked for felling: a yellow tint)
let (cx, cz) = (xf + 0.5, zf + 0.5);
let a = amb_at(amb, cx, cz);
let lg = lava_at(glow, cx, cz);
let tint = |v: Vec3| col(lava_lit(if t.marked { v * vec3(1.3, 1.15, 0.5) } else { v }, lg), 1.0);
let s = 0.85 + 0.1 * (t.var & 3) as f32;
if t.kind == Kind::Mushroom {
let h = 0.75 * s;
let sc = tint(Vec3::splat(a.max(0.35)));
mb.boxy(vec3(cx - 0.12, 0.0, cz - 0.12), vec3(cx + 0.12, h, cz + 0.12), T_STEM, sc, sc);
// the cap glows a little in the dark
let cc = tint(Vec3::splat(a.max(0.7)));
mb.boxy(vec3(cx - 0.42 * s, h, cz - 0.42 * s), vec3(cx + 0.42 * s, h + 0.26, cz + 0.42 * s), T_CAP, cc, cc);
mb.boxy(vec3(cx - 0.26 * s, h + 0.26, cz - 0.26 * s), vec3(cx + 0.26 * s, h + 0.38, cz + 0.26 * s), T_CAP, cc, cc);
} else {
let tc = tint(Vec3::splat(a.max(0.3)));
mb.boxy(vec3(cx - 0.12, 0.0, cz - 0.12), vec3(cx + 0.12, 0.9 * s, cz + 0.12), T_BARK, tc, tc);
let lc = tint(Vec3::splat(a.max(0.3) * 0.9));
let top = tint(Vec3::splat(a.max(0.3) * 1.1));
let leaves = T_LEAVES + (t.var as u32 & 1);
mb.boxy(vec3(cx - 0.48, 0.75 * s, cz - 0.48), vec3(cx + 0.48, 1.55 * s, cz + 0.48), leaves, lc, top);
mb.boxy(vec3(cx - 0.3, 1.55 * s, cz - 0.3), vec3(cx + 0.3, 1.95 * s, cz + 0.3), leaves, lc, top);
}
}
// wear and tear: grit and cracks on plain floors
if matches!(t.kind, Kind::Floor) {
let hsh = (x as u32).wrapping_mul(2654435761) ^ (z as u32).wrapping_mul(2246822519);
@@ -3083,6 +3119,8 @@ pub fn minimap_color(tile: &Tile) -> [u8; 4] {
Kind::LavaBridge => [130, 120, 115, 255],
Kind::Drawbridge => [120, 80, 50, 255],
Kind::Obsidian => [60, 40, 70, 255],
Kind::Mushroom => [150, 90, 200, 255],
Kind::Tree => [40, 100, 40, 255],
Kind::Shard => [255, 60, 120, 255],
Kind::Aerie => [170, 140, 90, 255],
Kind::Steel => [150, 160, 180, 255],
+6
View File
@@ -178,6 +178,12 @@ impl Game {
self.sfx_ui(Sfx::Error);
return;
}
// a wooden door costs wood
if kind == TrapKind::WoodDoor && !self.spend_wood(crate::wood::DOOR_WOOD) {
self.msg("msg.no_wood", vec![]);
self.sfx_ui(Sfx::Error);
return;
}
let id = self.next_id;
self.next_id += 1;
self.traps.push(Trap { id, kind, tile: i, stage: ORDERED, crate_tile: 0, progress: 0.0, cd: 0.0, hp: kind.hp(), locked: false, carrier: 0, fired: 99.0, aim: 0.0, revealed: false, sprung: false });
+14 -4
View File
@@ -288,8 +288,8 @@ pub fn tool_cost_static(t: Tool) -> String {
match t {
Tool::Spell(sp) => trf("cost.mana", &[(sp.cost() as i32).to_string()]),
Tool::Trap(k) => trf("cost.work", &[(k.work() as i32).to_string()]),
Tool::Bridge => trf("cost.bridge", &[BRIDGE_COST[0].to_string(), BRIDGE_COST[1].to_string()]),
Tool::Drawbridge => trf("cost.gold_tile", &[DRAWBRIDGE_COST.to_string()]),
Tool::Bridge => trf("cost.bridge", &[BRIDGE_COST[0].to_string(), (crate::wood::BRIDGE_WOOD as i32).to_string(), BRIDGE_COST[1].to_string()]),
Tool::Drawbridge => trf("cost.drawbridge", &[DRAWBRIDGE_COST[0].to_string(), (crate::wood::DRAWBRIDGE_WOOD as i32).to_string(), DRAWBRIDGE_COST[1].to_string()]),
Tool::Steel => trf("cost.steel", &[STEEL_COST[0].to_string(), STEEL_COST[1].to_string()]),
Tool::Tower(TowerKind::Rune) => {
let s = tower_stats(TowerKind::Rune);
@@ -314,8 +314,8 @@ pub fn tool_cost(t: Tool, g: &Game) -> String {
Tool::Lightning => trf("cost.mana", &["50".into()]),
Tool::Heal => trf("cost.mana", &["60".into()]),
Tool::Summon => trf("cost.mana", &[(g.summon_cost() as i32).to_string()]),
Tool::Bridge => trf("cost.bridge", &[BRIDGE_COST[0].to_string(), BRIDGE_COST[1].to_string()]),
Tool::Drawbridge => trf("cost.gold_tile", &[DRAWBRIDGE_COST.to_string()]),
Tool::Bridge => trf("cost.bridge", &[BRIDGE_COST[0].to_string(), (crate::wood::BRIDGE_WOOD as i32).to_string(), BRIDGE_COST[1].to_string()]),
Tool::Drawbridge => trf("cost.drawbridge", &[DRAWBRIDGE_COST[0].to_string(), (crate::wood::DRAWBRIDGE_WOOD as i32).to_string(), DRAWBRIDGE_COST[1].to_string()]),
Tool::Steel => trf("cost.steel", &[STEEL_COST[0].to_string(), STEEL_COST[1].to_string()]),
Tool::Tower(TowerKind::Rune) => {
let s = tower_stats(TowerKind::Rune);
@@ -622,6 +622,7 @@ impl Ui {
}
let mut segs: Vec<Vec<Part>> = vec![
gold_seg,
vec![txt(tr("hud.wood"), WOOD_C), Part::Gap, txt((g.wood.floor() as i32).to_string(), TXT)],
vec![txt(tr("hud.mana"), MANA), Part::Gap, Part::Bar(g.mana / g.mana_max(), MANA), Part::Gap, txt(format!("{:<3}", g.mana as i32), TXT)],
vec![txt(tr("hud.core"), RED), Part::Gap, Part::Bar(g.core_hp / CORE_HP, RED)],
vec![txt(tr("hud.creatures"), TXT), Part::Gap, txt(format!("{}/{}", g.creature_count(), cap), TXT)],
@@ -1418,6 +1419,9 @@ impl Ui {
}
// Info text for the unit or tile under the mouse (first line = title)
// the colour of wood in the interface
const WOOD_C: Color = Color::new(0.78, 0.56, 0.32, 1.0);
// fire and wear of a bridge tile
fn bridge_state(g: &Game, i: usize, out: &mut Vec<(String, Color)>) {
if g.burning(i) {
@@ -1565,6 +1569,12 @@ pub fn hover_info(g: &Game, ent: Option<usize>, tile: Option<(i32, i32)>) -> Vec
out.push((tr(state), BLUE));
bridge_state(g, i, &mut out);
}
Kind::Mushroom | Kind::Tree => {
let tree = t.kind == Kind::Tree;
out.push((tr(if tree { "tile.tree" } else { "tile.mushroom" }), WOOD_C));
let wood = if tree { crate::wood::WOOD_TREE } else { crate::wood::WOOD_MUSHROOM };
out.push((trf("tile.plant_info", &[(wood as i32).to_string()]), DIM));
}
Kind::Obsidian => {
out.push((tr("tile.obsidian"), FRAME_HI));
let left = g.bridgework.cooled.iter().find(|c| c.0 == i).map_or(0.0, |c| c.1);
+2
View File
@@ -71,6 +71,8 @@ impl Game {
}
self.carve_cavern(x, y, &keep, &mut rng);
}
// giant mushrooms in the open: the underground's little wood
self.grow_mushrooms(seed);
}
// a cavern: a few overlapping blobs, ragged edges
+156
View File
@@ -0,0 +1,156 @@
// 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 grows back
pub const REGROW: f32 = 240.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;
// giant mushrooms in a cavern of a random map: one per this many open tiles
const MUSHROOM_SHARE: usize = 70;
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct WoodWork {
pub regrow: Vec<(usize, f32)>, // felled mushroom tile, seconds until it grows back
}
// a tile where a plant stands alone: all eight neighbours open ground, 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) || map.t[idx(x, y)].kind != Kind::Floor || map.t[idx(x, y)].owner != Owner::None {
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))
&& (-2..=2).all(|dy| (-2..=2).all(|dx| !map.get(x + dx, y + dy).map_or(false, |n| n.plant())))
}
impl Game {
// giant mushrooms in the open caverns of a random map (deterministic, from the seed)
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: Vec<usize> = (0..self.map.t.len())
.filter(|&i| {
let (x, y) = xy(i);
(x - cx).abs().max((y - cy).abs()) > 10 && lonely_spot(&self.map, x, y)
})
.collect();
let want = (open.len() / MUSHROOM_SHARE).max(6);
let mut placed = 0;
let mut tries = 0;
while placed < want && tries < want * 20 && !open.is_empty() {
tries += 1;
let i = open[(rng.next() as usize) % open.len()];
let (x, y) = xy(i);
if self.ents.iter().any(|e| e.alive && e.tile() == i) || !lonely_spot(&self.map, x, y) {
continue;
}
self.plant_mushroom(i);
placed += 1;
}
}
pub fn plant_mushroom(&mut self, i: usize) {
let t = &mut self.map.t[i];
t.kind = Kind::Mushroom;
t.owner = Owner::None;
t.hp = MUSHROOM_HP;
t.marked = false;
}
// 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;
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: felled mushrooms grow back where there is room
pub(crate) fn update_wood(&mut self, dt: f32) {
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) {
self.plant_mushroom(i);
self.woodwork.regrow.swap_remove(k);
} else if self.map.t[i].kind != Kind::Floor || self.map.t[i].owner != Owner::None {
// 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;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn random_maps_have_lonely_giant_mushrooms() {
let g = Game::new(9, 96, 96);
let n = g.map.t.iter().filter(|t| t.kind == Kind::Mushroom).count();
assert!(n >= 6, "{} mushrooms", n);
assert!((g.wood - WOOD_START).abs() < 1e-3);
// none closes a corridor: every neighbour is open ground
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() || n.plant())));
}
}
}
#[test]
fn felling_gives_wood_and_mushrooms_grow_back() {
let mut g = Game::new(9, 96, 96);
let i = g.map.t.iter().position(|t| t.kind == Kind::Mushroom).unwrap();
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::Floor);
g.woodwork.regrow[0].1 = 0.01;
g.update_wood(0.05);
assert_eq!(g.map.t[i].kind, Kind::Mushroom, "grown back");
assert!(g.spend_wood(3.0) && !g.spend_wood(1000.0));
}
}