Campaign mission system: 20 mission definitions, goals, unlocks, carry-over
- campaign.rs: missions as key data (size, rivers, fortresses, goals, waves, time limit, gifts); start_mission builds the map with the generator; tools unlock step by step; the strongest 5 creatures and 20% of the gold are carried into the next mission; campaign progress type - Map generator options (portals, treasure caves, hero gate) and meandering rivers with knight bridges - Mission goals: shards, gate of the order, all knights, vault loot, knight towers, waves; time limits; waves and gate defenders only where the mission wants them - Locked tools are shown dark and cannot be chosen; HUD shows gate/waves/time only when used - Fix: patrol routes no longer start on tower spots - Tests: every mission builds, its goals are possible, wave heroes reach the core Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e15b16cc60
commit
46755d68ac
8 files changed
+750
-77
No files matched your search
@@ -423,3 +423,10 @@ msg.bridge_torn = Die Ritter haben eine deiner Brücken eingerissen!
|
||||
job.tear_bridge = Reißt eine Brücke ein
|
||||
job.patrol = Auf Patrouille
|
||||
job.guard = Hält Wache
|
||||
|
||||
# campaign
|
||||
msg.mission_start = Mission: {0}
|
||||
msg.mission_won = Mission erfüllt!
|
||||
msg.time_up = Die Zeit ist um - die Ritter haben ihre Schätze in Sicherheit gebracht.
|
||||
hud.time_left = Zeit
|
||||
msg.tool_locked = Das steht in dieser Mission noch nicht zur Verfügung.
|
||||
@@ -424,3 +424,10 @@ msg.bridge_torn = The knights have torn down one of your bridges!
|
||||
job.tear_bridge = Tearing down a bridge
|
||||
job.patrol = On patrol
|
||||
job.guard = On guard
|
||||
|
||||
# campaign
|
||||
msg.mission_start = Mission: {0}
|
||||
msg.mission_won = Mission accomplished!
|
||||
msg.time_up = Time is up - the knights have secured their treasures.
|
||||
hud.time_left = Time
|
||||
msg.tool_locked = This is not available yet in this mission.
|
||||
+442
@@ -0,0 +1,442 @@
|
||||
// Campaign "The Broken Core": 20 missions. Each mission is a set of key data (size, rivers,
|
||||
// fortresses, goals, available tools); the map generator builds the map from it.
|
||||
// Texts (title, story, hints) come from the language files: camp.<key>.title / .story
|
||||
|
||||
use crate::fortress::FortSpec;
|
||||
use crate::game::*;
|
||||
use crate::map::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// From which mission (0-based) on each tool is available (same order as ui::TOOLS):
|
||||
// dig, hand, treasury, lair, farm, training, library, sell, flag, lightning, heal, imp,
|
||||
// torture, temple, arrow tower, cannon tower, magic tower, bridge
|
||||
pub const TOOL_UNLOCK: [usize; 18] = [0, 0, 0, 0, 0, 2, 4, 0, 0, 3, 4, 0, 6, 9, 3, 5, 9, 1];
|
||||
|
||||
pub struct FortDef {
|
||||
pub at: (f32, f32), // centre as a fraction of the map size
|
||||
pub size: (i32, i32),
|
||||
pub doors: &'static [usize],
|
||||
pub towers: &'static [TowerKind],
|
||||
pub tower_level: u32,
|
||||
pub guards: u32, // number of guards
|
||||
pub patrols: u32, // patrol groups of two
|
||||
pub level: u32, // level of the knights
|
||||
pub boss: Option<(EK, u32)>,
|
||||
pub vaults: u32,
|
||||
pub vault_value: i32,
|
||||
pub shard: bool,
|
||||
pub heart: bool,
|
||||
}
|
||||
|
||||
pub struct MissionDef {
|
||||
pub key: &'static str,
|
||||
pub size: (usize, usize),
|
||||
pub seed: u32,
|
||||
pub gold: f32,
|
||||
pub portals: usize,
|
||||
pub treasures: usize,
|
||||
pub goals: Vec<Goal>,
|
||||
pub waves: bool, // hero waves (from the gate)
|
||||
pub wave_limit: u32, // 0 = endless
|
||||
pub first_wave: f32, // seconds until the first wave
|
||||
pub gate: bool, // free-game hero gate at the far side (wave missions)
|
||||
pub gate_defenders: bool,
|
||||
pub time_limit: f32,
|
||||
pub rivers: Vec<River>,
|
||||
pub forts: Vec<FortDef>,
|
||||
pub gift: Vec<(EK, u32)>, // creatures that join at the start
|
||||
}
|
||||
|
||||
// carried over from the last won mission: the strongest creatures and part of the gold
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct Carry {
|
||||
pub creatures: Vec<(EK, u32, f32)>, // kind, level, experience
|
||||
pub gold: f32,
|
||||
}
|
||||
|
||||
pub const CARRY_CREATURES: usize = 5;
|
||||
pub const CARRY_GOLD: f32 = 0.2;
|
||||
|
||||
// progress of the campaign (stored in the browser)
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct Progress {
|
||||
pub done: Vec<bool>,
|
||||
pub carry: Vec<Option<Carry>>, // carry[i] = taken from mission i into mission i + 1
|
||||
}
|
||||
|
||||
impl Progress {
|
||||
pub fn unlocked(&self, i: usize) -> bool {
|
||||
i == 0 || self.done.get(i - 1).copied().unwrap_or(false) || self.done.get(i).copied().unwrap_or(false)
|
||||
}
|
||||
pub fn is_done(&self, i: usize) -> bool {
|
||||
self.done.get(i).copied().unwrap_or(false)
|
||||
}
|
||||
pub fn set_done(&mut self, i: usize, carry: Carry) {
|
||||
let n = missions().len();
|
||||
self.done.resize(n, false);
|
||||
self.carry.resize(n, None);
|
||||
self.done[i] = true;
|
||||
self.carry[i] = Some(carry);
|
||||
}
|
||||
// what the player brings into mission i
|
||||
pub fn carry_into(&self, i: usize) -> Option<&Carry> {
|
||||
if i == 0 {
|
||||
return None;
|
||||
}
|
||||
self.carry.get(i - 1).and_then(|c| c.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
use EK::*;
|
||||
use TowerKind::{Arrow, Cannon, Magic};
|
||||
|
||||
fn river(lava: bool, vertical: bool, pos: f32, width: i32, bridges: u32) -> River {
|
||||
River { lava, vertical, pos, width, bridges }
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn fort(at: (f32, f32), size: (i32, i32), doors: &'static [usize], towers: &'static [TowerKind], tower_level: u32, guards: u32, patrols: u32, level: u32, vaults: u32, vault_value: i32) -> FortDef {
|
||||
FortDef { at, size, doors, towers, tower_level, guards, patrols, level, boss: None, vaults, vault_value, shard: false, heart: false }
|
||||
}
|
||||
|
||||
impl FortDef {
|
||||
fn shard(mut self) -> FortDef {
|
||||
self.shard = true;
|
||||
self
|
||||
}
|
||||
fn heart(mut self) -> FortDef {
|
||||
self.heart = true;
|
||||
self
|
||||
}
|
||||
fn boss(mut self, k: EK, level: u32) -> FortDef {
|
||||
self.boss = Some((k, level));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn base(key: &'static str, size: (usize, usize), seed: u32, goals: Vec<Goal>) -> MissionDef {
|
||||
MissionDef {
|
||||
key,
|
||||
size,
|
||||
seed,
|
||||
gold: 3000.0,
|
||||
portals: 1,
|
||||
treasures: 0,
|
||||
goals,
|
||||
waves: false,
|
||||
wave_limit: 0,
|
||||
first_wave: 300.0,
|
||||
gate: false,
|
||||
gate_defenders: false,
|
||||
time_limit: 0.0,
|
||||
rivers: Vec::new(),
|
||||
forts: Vec::new(),
|
||||
gift: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn missions() -> Vec<MissionDef> {
|
||||
let mut v = Vec::new();
|
||||
// ---- Act I: awakening
|
||||
v.push(MissionDef { gold: 2500.0, forts: vec![fort((0.74, 0.5), (15, 13), &[3], &[], 1, 4, 0, 1, 1, 1200).shard()], ..base("m01", (64, 64), 101, vec![Goal::Shards]) });
|
||||
v.push(MissionDef {
|
||||
gold: 2500.0,
|
||||
rivers: vec![river(false, true, 0.47, 3, 0)],
|
||||
forts: vec![fort((0.8, 0.5), (15, 15), &[3], &[Arrow], 1, 4, 1, 1, 1, 1500).shard()],
|
||||
..base("m02", (80, 64), 202, vec![Goal::Shards])
|
||||
});
|
||||
v.push(MissionDef { forts: vec![fort((0.74, 0.5), (19, 17), &[3, 0], &[Arrow, Arrow], 1, 6, 2, 1, 1, 1500)], ..base("m03", (80, 80), 303, vec![Goal::KillAll]) });
|
||||
v.push(MissionDef {
|
||||
portals: 2,
|
||||
treasures: 1,
|
||||
waves: true,
|
||||
wave_limit: 4,
|
||||
first_wave: 270.0,
|
||||
gate: true,
|
||||
..base("m04", (80, 64), 404, vec![Goal::Survive(4)])
|
||||
});
|
||||
v.push(MissionDef {
|
||||
rivers: vec![river(false, true, 0.42, 3, 1)],
|
||||
forts: vec![fort((0.76, 0.5), (21, 21), &[3, 2], &[Arrow, Arrow, Arrow], 1, 6, 2, 2, 4, 2000)],
|
||||
..base("m05", (96, 80), 505, vec![Goal::Loot(6000)])
|
||||
});
|
||||
// ---- Act II: the rivers
|
||||
v.push(MissionDef {
|
||||
rivers: vec![river(false, true, 0.52, 5, 1)],
|
||||
forts: vec![fort((0.82, 0.5), (21, 19), &[3], &[Arrow, Cannon, Arrow, Cannon], 1, 8, 2, 2, 2, 1500).heart()],
|
||||
..base("m06", (96, 80), 606, vec![Goal::Heart])
|
||||
});
|
||||
v.push(MissionDef {
|
||||
rivers: vec![river(true, true, 0.45, 3, 0)],
|
||||
forts: vec![fort((0.79, 0.45), (19, 19), &[3], &[Arrow, Cannon], 2, 7, 2, 2, 2, 1800).shard()],
|
||||
..base("m07", (96, 96), 707, vec![Goal::Shards])
|
||||
});
|
||||
v.push(MissionDef {
|
||||
time_limit: 900.0,
|
||||
gift: vec![(Ogre, 3), (Kobold, 3), (Kobold, 3), (Spider, 2)],
|
||||
forts: vec![
|
||||
fort((0.6, 0.27), (17, 15), &[3, 2], &[Arrow], 1, 4, 1, 2, 3, 1500),
|
||||
fort((0.78, 0.72), (17, 15), &[3, 0], &[Arrow, Cannon], 1, 4, 1, 2, 3, 1500),
|
||||
],
|
||||
..base("m08", (96, 80), 808, vec![Goal::Loot(8000)])
|
||||
});
|
||||
v.push(MissionDef {
|
||||
rivers: vec![river(false, false, 0.5, 3, 2)],
|
||||
forts: vec![
|
||||
fort((0.76, 0.24), (19, 17), &[3, 2], &[Arrow, Cannon], 2, 7, 2, 2, 1, 1500).shard(),
|
||||
fort((0.76, 0.76), (19, 17), &[3, 0], &[Arrow, Cannon], 2, 7, 2, 2, 1, 1500).shard(),
|
||||
],
|
||||
..base("m09", (112, 96), 909, vec![Goal::Shards])
|
||||
});
|
||||
v.push(MissionDef {
|
||||
gold: 4000.0,
|
||||
portals: 2,
|
||||
treasures: 1,
|
||||
waves: true,
|
||||
wave_limit: 6,
|
||||
first_wave: 240.0,
|
||||
gate: true,
|
||||
..base("m10", (96, 80), 1010, vec![Goal::Survive(6)])
|
||||
});
|
||||
v.push(MissionDef {
|
||||
forts: vec![fort((0.72, 0.5), (25, 25), &[3, 0, 2], &[Arrow, Cannon, Magic, Arrow, Cannon], 2, 8, 4, 3, 2, 2000)],
|
||||
..base("m11", (112, 96), 1111, vec![Goal::KillAll])
|
||||
});
|
||||
v.push(MissionDef {
|
||||
rivers: vec![river(false, true, 0.4, 3, 1), river(true, true, 0.62, 3, 1)],
|
||||
forts: vec![fort((0.85, 0.5), (21, 25), &[3], &[Arrow, Cannon, Magic, Arrow], 2, 10, 2, 3, 2, 2000).heart().shard()],
|
||||
..base("m12", (112, 96), 1212, vec![Goal::Heart, Goal::Shards])
|
||||
});
|
||||
// ---- Act III: the citadel
|
||||
v.push(MissionDef {
|
||||
rivers: vec![river(false, false, 0.22, 3, 1)],
|
||||
forts: vec![fort((0.75, 0.55), (27, 23), &[3, 0], &[Arrow, Cannon, Magic, Arrow, Cannon, Magic], 2, 12, 3, 3, 4, 3500)],
|
||||
..base("m13", (112, 96), 1313, vec![Goal::Loot(13000)])
|
||||
});
|
||||
v.push(MissionDef {
|
||||
forts: vec![fort((0.7, 0.5), (25, 25), &[3, 1], &[Arrow, Cannon, Magic, Arrow, Cannon, Magic, Arrow, Cannon], 3, 8, 2, 3, 2, 2000)],
|
||||
..base("m14", (112, 112), 1414, vec![Goal::Towers])
|
||||
});
|
||||
v.push(MissionDef {
|
||||
portals: 0,
|
||||
gold: 5000.0,
|
||||
gift: vec![(Ogre, 5), (Ogre, 4), (Mage, 5), (Mage, 4), (Spider, 4), (Kobold, 4), (Kobold, 4), (Mage, 3)],
|
||||
rivers: vec![river(false, true, 0.42, 4, 1)],
|
||||
forts: vec![fort((0.74, 0.5), (21, 21), &[3, 0, 2], &[Arrow, Cannon, Magic], 2, 10, 2, 3, 2, 2000)],
|
||||
..base("m15", (112, 96), 1515, vec![Goal::KillAll])
|
||||
});
|
||||
v.push(MissionDef {
|
||||
waves: true,
|
||||
first_wave: 360.0,
|
||||
gate_defenders: true,
|
||||
rivers: vec![river(true, true, 0.55, 3, 2)],
|
||||
forts: vec![fort((0.82, 0.5), (23, 23), &[3], &[Arrow, Cannon, Magic, Arrow], 2, 10, 2, 3, 2, 2000).heart()],
|
||||
..base("m16", (112, 96), 1616, vec![Goal::Heart])
|
||||
});
|
||||
v.push(MissionDef {
|
||||
portals: 2,
|
||||
rivers: vec![river(false, true, 0.36, 3, 2), river(false, true, 0.56, 3, 2), river(false, false, 0.5, 3, 2)],
|
||||
forts: vec![
|
||||
fort((0.8, 0.24), (19, 17), &[3, 2], &[Arrow, Cannon, Magic], 2, 8, 2, 3, 2, 2000).shard(),
|
||||
fort((0.8, 0.76), (19, 17), &[3, 0], &[Arrow, Cannon, Magic], 2, 8, 2, 3, 2, 2000).shard(),
|
||||
],
|
||||
..base("m17", (128, 112), 1717, vec![Goal::Shards, Goal::Loot(6000)])
|
||||
});
|
||||
v.push(MissionDef {
|
||||
portals: 2,
|
||||
rivers: vec![river(true, true, 0.5, 4, 1)],
|
||||
forts: vec![fort((0.82, 0.5), (25, 25), &[3], &[Arrow, Cannon, Magic, Arrow, Cannon, Magic], 3, 14, 3, 4, 3, 2500).heart().boss(Paladin, 5)],
|
||||
..base("m18", (128, 112), 1818, vec![Goal::Heart])
|
||||
});
|
||||
v.push(MissionDef {
|
||||
portals: 2,
|
||||
rivers: vec![river(false, true, 0.42, 3, 2), river(true, false, 0.5, 3, 2)],
|
||||
forts: vec![
|
||||
fort((0.62, 0.22), (19, 17), &[3, 2], &[Arrow, Cannon, Magic], 3, 8, 2, 4, 1, 2500).shard(),
|
||||
fort((0.86, 0.5), (19, 19), &[3], &[Arrow, Cannon, Magic, Arrow], 3, 10, 2, 4, 1, 2500).shard(),
|
||||
fort((0.62, 0.8), (19, 17), &[3, 0], &[Arrow, Cannon, Magic], 3, 8, 2, 4, 1, 2500).shard(),
|
||||
],
|
||||
..base("m19", (144, 128), 1919, vec![Goal::Shards])
|
||||
});
|
||||
v.push(MissionDef {
|
||||
portals: 3,
|
||||
gold: 5000.0,
|
||||
waves: true,
|
||||
first_wave: 420.0,
|
||||
gate_defenders: true,
|
||||
rivers: vec![river(false, true, 0.6, 6, 2)],
|
||||
forts: vec![fort((0.83, 0.5), (29, 29), &[3], &[Arrow, Cannon, Magic, Arrow, Cannon, Magic, Arrow, Magic], 3, 16, 4, 5, 4, 3000)
|
||||
.heart()
|
||||
.shard()
|
||||
.boss(Paladin, 10)],
|
||||
..base("m20", (144, 144), 2020, vec![Goal::Heart, Goal::Shards])
|
||||
});
|
||||
v
|
||||
}
|
||||
|
||||
// the knights of a fortress: mostly knights and archers, some priests and dwarves
|
||||
fn knights(n: u32, level: u32) -> Vec<(EK, u32)> {
|
||||
let mix = [Knight, Archer, Knight, Priest, Dwarf, Archer, Knight];
|
||||
(0..n).map(|k| (mix[k as usize % mix.len()], level)).collect()
|
||||
}
|
||||
|
||||
impl FortDef {
|
||||
fn spec(&self, w: usize, h: usize) -> FortSpec {
|
||||
let (fw, fh) = self.size;
|
||||
let x = ((self.at.0 * w as f32) as i32 - fw / 2).clamp(3, w as i32 - fw - 3);
|
||||
let y = ((self.at.1 * h as f32) as i32 - fh / 2).clamp(3, h as i32 - fh - 3);
|
||||
let mut guards = knights(self.guards, self.level);
|
||||
if let Some(b) = self.boss {
|
||||
guards.insert(0, b);
|
||||
}
|
||||
let patrols = (0..self.patrols).map(|k| vec![(if k % 2 == 0 { Knight } else { Archer }, self.level), (Knight, self.level)]).collect();
|
||||
FortSpec {
|
||||
x,
|
||||
y,
|
||||
w: fw,
|
||||
h: fh,
|
||||
doors: self.doors.to_vec(),
|
||||
towers: self.towers.to_vec(),
|
||||
tower_level: self.tower_level,
|
||||
guards,
|
||||
patrols,
|
||||
vaults: self.vaults,
|
||||
vault_value: self.vault_value,
|
||||
shard: self.shard,
|
||||
heart: self.heart,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Builds the game of mission i (carry: what the last mission handed over)
|
||||
pub fn start_mission(i: usize, carry: Option<&Carry>) -> Game {
|
||||
let all = missions();
|
||||
let d = &all[i.min(all.len() - 1)];
|
||||
let (w, h) = d.size;
|
||||
let opts = GenOpts { portals: Some(d.portals), treasures: Some(d.treasures), gate: d.gate, core_y: None };
|
||||
let map = Map::generate_with(d.seed, w, h, &opts);
|
||||
let mut g = Game::from_map(map, d.seed);
|
||||
g.gold = d.gold;
|
||||
for (k, r) in d.rivers.iter().enumerate() {
|
||||
let seed = d.seed.wrapping_mul(31).wrapping_add(k as u32 * 977);
|
||||
g.map.carve_river(r, seed);
|
||||
}
|
||||
let mut shards = 0;
|
||||
for f in &d.forts {
|
||||
g.build_fortress(&f.spec(w, h));
|
||||
shards += f.shard as u32;
|
||||
}
|
||||
let heart = d.gate || d.forts.iter().any(|f| f.heart);
|
||||
if !heart {
|
||||
g.gate_hp = 0.0;
|
||||
}
|
||||
g.guard_treasures();
|
||||
g.place_all_torches();
|
||||
g.mission = Some(MissionState {
|
||||
index: i,
|
||||
goals: d.goals.clone(),
|
||||
tools: TOOL_UNLOCK.iter().map(|&u| u <= i).collect(),
|
||||
waves: d.waves,
|
||||
wave_limit: d.wave_limit,
|
||||
gate_defenders: d.gate_defenders,
|
||||
heart,
|
||||
time_limit: d.time_limit,
|
||||
shards,
|
||||
});
|
||||
g.wave_timer = d.first_wave;
|
||||
// new creatures stand around the core
|
||||
let (cx, cy) = g.map.core;
|
||||
let mut joiners: Vec<(EK, u32, f32)> = d.gift.iter().map(|&(k, l)| (k, l, 0.0)).collect();
|
||||
if let Some(c) = carry {
|
||||
joiners.extend(c.creatures.iter().copied());
|
||||
g.gold += c.gold;
|
||||
}
|
||||
for (k, &(kind, level, xp)) in joiners.iter().enumerate() {
|
||||
let a = k as f32 * 0.9;
|
||||
let (x, y) = (cx as f32 + 1.5 + a.cos() * 3.2, cy as f32 + 1.5 + a.sin() * 3.2);
|
||||
let id = g.spawn(kind, false, x, y, level.max(1));
|
||||
if let Some(e) = g.ents.iter_mut().find(|e| e.id == id) {
|
||||
e.xp = xp;
|
||||
e.happy = 1.0;
|
||||
}
|
||||
}
|
||||
g.msg("msg.mission_start", vec![format!("@camp.{}.title", d.key)]);
|
||||
g
|
||||
}
|
||||
|
||||
// the strongest creatures and a fifth of the gold go along into the next mission
|
||||
pub fn carry_from(g: &Game) -> Carry {
|
||||
let mut c: Vec<&Ent> = g.ents.iter().filter(|e| e.alive && e.creature()).collect();
|
||||
let power = |e: &Ent| e.max_hp * e.atk() / st(e.kind).interval;
|
||||
c.sort_by(|a, b| power(b).partial_cmp(&power(a)).unwrap_or(std::cmp::Ordering::Equal));
|
||||
Carry { creatures: c.iter().take(CARRY_CREATURES).map(|e| (e.kind, e.level, e.xp)).collect(), gold: (g.gold * CARRY_GOLD).floor() }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn all_missions_build() {
|
||||
for (i, d) in missions().iter().enumerate() {
|
||||
let g = start_mission(i, None);
|
||||
let m = g.mission.as_ref().unwrap();
|
||||
let heroes = g.ents.iter().filter(|e| e.hero).count();
|
||||
println!("{} {}x{}: {} knights, {} knight towers, {} shards, goals {:?}", d.key, d.size.0, d.size.1, heroes, g.towers.iter().filter(|t| t.hero).count(), m.shards, m.goals);
|
||||
// every patrol group got its round
|
||||
let patrols = g.ents.iter().filter(|e| !e.patrol.is_empty()).count() as u32;
|
||||
assert_eq!(patrols, d.forts.iter().map(|f| f.patrols * 2).sum::<u32>(), "{}", d.key);
|
||||
// the core area is intact
|
||||
let (cx, cy) = g.map.core;
|
||||
assert_eq!(g.map.t[idx(cx + 1, cy + 1)].kind, Kind::Core, "{}", d.key);
|
||||
// goals that need something on the map have it
|
||||
for goal in &m.goals {
|
||||
match goal {
|
||||
Goal::Shards => assert!(m.shards > 0, "{}", d.key),
|
||||
Goal::Heart => assert!(m.heart && g.gate_hp > 0.0, "{}", d.key),
|
||||
Goal::Towers => assert!(g.towers.iter().any(|t| t.hero), "{}", d.key),
|
||||
Goal::KillAll => assert!(heroes > 0, "{}", d.key),
|
||||
Goal::Loot(n) => {
|
||||
let vaults: i32 = g.map.t.iter().filter(|t| t.kind == Kind::Treasury && t.owner == Owner::Hero).map(|t| t.gold).sum();
|
||||
assert!(vaults as u32 >= *n, "{}: vaults hold {}", d.key, vaults);
|
||||
}
|
||||
Goal::Survive(_) => assert!(m.waves, "{}", d.key),
|
||||
}
|
||||
}
|
||||
// wave missions: the heroes can reach the core
|
||||
if m.waves {
|
||||
let (gx, gy) = g.map.gate;
|
||||
assert!(g.map.hero_path_to_core(idx(gx + 1, gy + 1)).is_some(), "{}: no hero path", d.key);
|
||||
}
|
||||
// every shard and the heart can be reached by digging and bridging from the core
|
||||
let reach = g.map.search(idx(cx + 1, cy - 2).max(0), |i| {
|
||||
let t = &g.map.t[i];
|
||||
if t.kind == Kind::Rock || t.kind == Kind::Core || t.kind == Kind::Tower {
|
||||
None
|
||||
} else {
|
||||
Some(1)
|
||||
}
|
||||
}, |i| g.map.t[i].kind == Kind::Shard || g.map.t[i].kind == Kind::Gate || g.map.t[i].owner == Owner::Hero, true);
|
||||
assert!(reach.is_some() || heroes == 0, "{}: fortress unreachable", d.key);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn carry_over_takes_the_strongest() {
|
||||
let mut g = start_mission(0, None);
|
||||
for (k, l) in [(Kobold, 1), (Ogre, 5), (Spider, 2), (Mage, 4), (Kobold, 2), (Ogre, 2), (Spider, 1)] {
|
||||
g.spawn(k, false, 20.0, 20.0, l);
|
||||
}
|
||||
g.gold = 5000.0;
|
||||
let c = carry_from(&g);
|
||||
assert_eq!(c.creatures.len(), CARRY_CREATURES);
|
||||
assert_eq!(c.creatures[0].0, Ogre);
|
||||
assert_eq!(c.gold, 1000.0);
|
||||
let g2 = start_mission(1, Some(&c));
|
||||
assert_eq!(g2.ents.iter().filter(|e| e.creature()).count(), CARRY_CREATURES);
|
||||
assert_eq!(g2.gold, 2500.0 + 1000.0);
|
||||
// the tools of mission 2: bridges yes, training no
|
||||
assert!(g2.tool_allowed(17));
|
||||
assert!(!g2.tool_allowed(5));
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -157,7 +157,8 @@ impl Game {
|
||||
e.home = Some((hx, hy));
|
||||
}
|
||||
// patrols: round through the courtyard along the inside of the walls
|
||||
let route = vec![(x0 + 2, cy), (cx, y0 + 2), (x1 - 2, cy), (cx, y1 - 2)];
|
||||
// (next to the tower spots in the middle of the walls)
|
||||
let route = vec![(x0 + 3, cy + 2), (cx + 2, y0 + 3), (x1 - 3, cy - 2), (cx - 2, y1 - 3)];
|
||||
let route: Vec<(i32, i32)> = route.into_iter().filter(|&(x, y)| self.map.walk(x, y)).collect();
|
||||
for (g, group) in f.patrols.iter().enumerate() {
|
||||
if route.is_empty() {
|
||||
|
||||
+158
-57
@@ -272,6 +272,30 @@ impl Ent {
|
||||
}
|
||||
}
|
||||
|
||||
// Goals of a campaign mission (all must be reached)
|
||||
#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)]
|
||||
pub enum Goal {
|
||||
Shards, // take back all shards of the soul core
|
||||
Heart, // destroy the gate of the order
|
||||
KillAll, // defeat every knight (prisoners count as defeated)
|
||||
Loot(u32), // take treasure worth this much from knight vaults
|
||||
Towers, // raze all knight towers
|
||||
Survive(u32), // beat this many waves
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct MissionState {
|
||||
pub index: usize, // number of the mission in the campaign
|
||||
pub goals: Vec<Goal>,
|
||||
pub tools: Vec<bool>, // allowed tools (ui::TOOLS order)
|
||||
pub waves: bool,
|
||||
pub wave_limit: u32, // 0 = endless
|
||||
pub gate_defenders: bool,
|
||||
pub heart: bool, // the map has a gate of the order
|
||||
pub time_limit: f32, // seconds, 0 = none
|
||||
pub shards: u32, // shards on the map
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Fx {
|
||||
pub kind: u8, // 0 projectile, 1 lightning, 2 hit, 3 smoke, 4 heal, 5 gold, 6 dust, 7 level up, 8 claim
|
||||
@@ -340,7 +364,9 @@ pub struct Game {
|
||||
#[serde(default)]
|
||||
pub towers_razed: u32, // knight towers destroyed
|
||||
#[serde(skip)]
|
||||
pub contested: Vec<bool>, // tiles next to fighting heroes: knight ground there cannot be claimed
|
||||
pub contested: Vec<bool>,
|
||||
#[serde(default)]
|
||||
pub mission: Option<MissionState>, // campaign mission (None: free game) // tiles next to fighting heroes: knight ground there cannot be claimed
|
||||
#[serde(skip)]
|
||||
pub hints: bool, // show hint messages (setting)
|
||||
#[serde(skip, default = "hint_times")]
|
||||
@@ -403,7 +429,38 @@ fn sgn(v: f32) -> f32 {
|
||||
|
||||
impl Game {
|
||||
pub fn new(seed: u32, w: usize, h: usize) -> Game {
|
||||
let map = Map::generate(seed, w, h);
|
||||
let mut g = Game::from_map(Map::generate(seed, w, h), seed);
|
||||
// heroes take longer to reach the core on big maps: longer grace period
|
||||
let (cx, cy) = g.map.core;
|
||||
let (gx, gy) = g.map.gate;
|
||||
let dist = ((gx - cx).abs() + (gy - cy).abs()) as f32;
|
||||
g.wave_timer = FIRST_WAVE + ((dist - 45.0) * 1.5).clamp(0.0, 300.0);
|
||||
g.guard_treasures();
|
||||
g.msg("msg.welcome1", vec![]);
|
||||
g.msg("msg.welcome2", vec![]);
|
||||
g.msg("msg.welcome3", vec![]);
|
||||
g.msg("msg.welcome4", vec![]);
|
||||
g
|
||||
}
|
||||
|
||||
// treasure caves are guarded by heroes
|
||||
pub fn guard_treasures(&mut self) {
|
||||
for k in 0..self.map.treasures.len() {
|
||||
let (tx, ty) = self.map.treasures[k];
|
||||
let squad = [EK::Knight, EK::Archer, EK::Dwarf, if k % 2 == 0 { EK::Priest } else { EK::Knight }];
|
||||
for (j, &kind) in squad.iter().enumerate() {
|
||||
let x = tx as f32 + 0.5 + (j as f32 - 1.5) * 0.6;
|
||||
let id = self.spawn(kind, true, x, ty as f32 + 0.5, 2);
|
||||
if let Some(e) = self.ents.iter_mut().find(|e| e.id == id) {
|
||||
e.guard = true;
|
||||
e.home = Some((tx as f32 + 0.5, ty as f32 + 0.5));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// game state around a generated map: treasury ring around the core, four imps, torches
|
||||
pub fn from_map(map: Map, seed: u32) -> Game {
|
||||
let mut g = Game {
|
||||
map,
|
||||
ents: Vec::new(),
|
||||
@@ -445,6 +502,7 @@ impl Game {
|
||||
shards_taken: 0,
|
||||
towers_razed: 0,
|
||||
contested: Vec::new(),
|
||||
mission: None,
|
||||
};
|
||||
let (cx, cy) = g.map.core;
|
||||
// the soul core starts surrounded by a one tile wide ring of treasury
|
||||
@@ -461,35 +519,46 @@ impl Game {
|
||||
g.spawn(EK::Worker, false, cx as f32 - 0.5, cy as f32 + k as f32 * 0.7 + 0.3, 1);
|
||||
}
|
||||
g.recount();
|
||||
for i in 0..n() {
|
||||
if g.map.t[i].owner == Owner::Player {
|
||||
g.place_torches(i);
|
||||
}
|
||||
}
|
||||
// heroes take longer to reach the core on big maps: longer grace period
|
||||
let (gx, gy) = g.map.gate;
|
||||
let dist = ((gx - cx).abs() + (gy - cy).abs()) as f32;
|
||||
g.wave_timer = FIRST_WAVE + ((dist - 45.0) * 1.5).clamp(0.0, 300.0);
|
||||
// treasure caves are guarded by heroes
|
||||
for k in 0..g.map.treasures.len() {
|
||||
let (tx, ty) = g.map.treasures[k];
|
||||
let squad = [EK::Knight, EK::Archer, EK::Dwarf, if k % 2 == 0 { EK::Priest } else { EK::Knight }];
|
||||
for (j, &kind) in squad.iter().enumerate() {
|
||||
let x = tx as f32 + 0.5 + (j as f32 - 1.5) * 0.6;
|
||||
let id = g.spawn(kind, true, x, ty as f32 + 0.5, 2);
|
||||
if let Some(e) = g.ents.iter_mut().find(|e| e.id == id) {
|
||||
e.guard = true;
|
||||
e.home = Some((tx as f32 + 0.5, ty as f32 + 0.5));
|
||||
}
|
||||
}
|
||||
}
|
||||
g.msg("msg.welcome1", vec![]);
|
||||
g.msg("msg.welcome2", vec![]);
|
||||
g.msg("msg.welcome3", vec![]);
|
||||
g.msg("msg.welcome4", vec![]);
|
||||
g.place_all_torches();
|
||||
g
|
||||
}
|
||||
|
||||
pub fn place_all_torches(&mut self) {
|
||||
// torches on walls that are gone (rivers, fortresses) are removed
|
||||
let map = &self.map;
|
||||
self.torches.retain(|t| map.t[t.wall].solid());
|
||||
for i in 0..n() {
|
||||
if self.map.t[i].owner == Owner::Player {
|
||||
self.place_torches(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- campaign missions
|
||||
|
||||
pub fn tool_allowed(&self, tool: usize) -> bool {
|
||||
self.mission.as_ref().map_or(true, |m| m.tools.get(tool).copied().unwrap_or(true))
|
||||
}
|
||||
|
||||
pub fn goal_done(&self, goal: Goal) -> bool {
|
||||
match goal {
|
||||
Goal::Shards => self.shards_taken >= self.mission.as_ref().map_or(0, |m| m.shards),
|
||||
Goal::Heart => self.gate_hp <= 0.0,
|
||||
Goal::KillAll => !self.ents.iter().any(|e| e.alive && e.active_hero()),
|
||||
Goal::Loot(n) => self.loot >= n as f32,
|
||||
Goal::Towers => !self.towers.iter().any(|t| t.hero),
|
||||
Goal::Survive(n) => self.wave >= n && !self.ents.iter().any(|e| e.alive && e.active_hero() && !e.guard && e.patrol.is_empty()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn waves_on(&self) -> bool {
|
||||
self.mission.as_ref().map_or(true, |m| m.waves && (m.wave_limit == 0 || self.wave < m.wave_limit))
|
||||
}
|
||||
|
||||
pub fn has_gate(&self) -> bool {
|
||||
self.mission.as_ref().map_or(true, |m| m.heart)
|
||||
}
|
||||
|
||||
pub fn msg(&mut self, key: &'static str, args: Vec<String>) {
|
||||
self.msgs.push(Msg { key, args });
|
||||
if self.msgs.len() > 50 {
|
||||
@@ -788,6 +857,59 @@ impl Game {
|
||||
self.do_payday();
|
||||
}
|
||||
|
||||
if self.waves_on() {
|
||||
self.wave_tick(dt);
|
||||
}
|
||||
if self.time - self.gate_hit_t > 10.0 && self.has_gate() {
|
||||
self.gate_hp = (self.gate_hp + 8.0 * dt).min(GATE_HP);
|
||||
}
|
||||
|
||||
self.update_contested();
|
||||
for i in 0..self.ents.len() {
|
||||
if !self.ents[i].alive || self.ents[i].held {
|
||||
continue;
|
||||
}
|
||||
let mut e = std::mem::replace(&mut self.ents[i], Ent::dummy());
|
||||
self.update_ent(&mut e, dt);
|
||||
self.ents[i] = e;
|
||||
}
|
||||
self.ents.retain(|e| e.alive);
|
||||
self.update_towers(dt);
|
||||
self.check_end();
|
||||
}
|
||||
|
||||
fn check_end(&mut self) {
|
||||
if self.core_hp <= 0.0 {
|
||||
self.core_hp = 0.0;
|
||||
self.state = 2;
|
||||
self.msg("msg.defeat", vec![]);
|
||||
self.sfx_ui(Sfx::Defeat);
|
||||
return;
|
||||
}
|
||||
if let Some(m) = &self.mission {
|
||||
if m.time_limit > 0.0 && self.time > m.time_limit {
|
||||
self.state = 2;
|
||||
self.msg("msg.time_up", vec![]);
|
||||
self.sfx_ui(Sfx::Defeat);
|
||||
return;
|
||||
}
|
||||
let goals = m.goals.clone();
|
||||
if !goals.is_empty() && goals.iter().all(|&g| self.goal_done(g)) {
|
||||
self.state = 1;
|
||||
self.msg("msg.mission_won", vec![]);
|
||||
self.sfx_ui(Sfx::Victory);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if self.gate_hp <= 0.0 {
|
||||
self.gate_hp = 0.0;
|
||||
self.state = 1;
|
||||
self.msg("msg.victory", vec![]);
|
||||
self.sfx_ui(Sfx::Victory);
|
||||
}
|
||||
}
|
||||
|
||||
fn wave_tick(&mut self, dt: f32) {
|
||||
self.wave_timer -= dt;
|
||||
if !self.warned && self.wave_timer < 30.0 {
|
||||
self.warned = true;
|
||||
@@ -814,33 +936,6 @@ impl Game {
|
||||
self.wave_timer = WAVE_GAP + self.rng.f() * 40.0;
|
||||
self.warned = false;
|
||||
}
|
||||
if self.time - self.gate_hit_t > 10.0 {
|
||||
self.gate_hp = (self.gate_hp + 8.0 * dt).min(GATE_HP);
|
||||
}
|
||||
|
||||
self.update_contested();
|
||||
for i in 0..self.ents.len() {
|
||||
if !self.ents[i].alive || self.ents[i].held {
|
||||
continue;
|
||||
}
|
||||
let mut e = std::mem::replace(&mut self.ents[i], Ent::dummy());
|
||||
self.update_ent(&mut e, dt);
|
||||
self.ents[i] = e;
|
||||
}
|
||||
self.ents.retain(|e| e.alive);
|
||||
self.update_towers(dt);
|
||||
|
||||
if self.core_hp <= 0.0 {
|
||||
self.core_hp = 0.0;
|
||||
self.state = 2;
|
||||
self.msg("msg.defeat", vec![]);
|
||||
self.sfx_ui(Sfx::Defeat);
|
||||
} else if self.gate_hp <= 0.0 {
|
||||
self.gate_hp = 0.0;
|
||||
self.state = 1;
|
||||
self.msg("msg.victory", vec![]);
|
||||
self.sfx_ui(Sfx::Victory);
|
||||
}
|
||||
}
|
||||
|
||||
fn try_portal(&mut self) {
|
||||
@@ -1082,8 +1177,10 @@ impl Game {
|
||||
}
|
||||
self.sfx(Sfx::GateHit, gx, gy);
|
||||
self.hint(10, "hint.gate_attack");
|
||||
if self.mission.as_ref().map_or(true, |m| m.gate_defenders) {
|
||||
self.check_gate_stage();
|
||||
}
|
||||
}
|
||||
|
||||
fn update_ent(&mut self, e: &mut Ent, dt: f32) {
|
||||
e.anim += dt;
|
||||
@@ -3835,14 +3932,18 @@ mod tests {
|
||||
patrols: vec![vec![(EK::Archer, 1)]],
|
||||
..spec(x, y)
|
||||
});
|
||||
// no imps around (the patrol would shoot them through the door)
|
||||
g.ents.retain(|e| e.hero);
|
||||
let pj = g.ents.iter().position(|e| !e.patrol.is_empty()).unwrap();
|
||||
let pid = g.ents[pj].id;
|
||||
let start = (g.ents[pj].x, g.ents[pj].y);
|
||||
let mut far = 0.0f32;
|
||||
for _ in 0..200 {
|
||||
g.update(0.05);
|
||||
}
|
||||
let p = g.ents.iter().find(|e| e.id == pid).unwrap();
|
||||
assert!((p.x - start.0).abs() + (p.y - start.1).abs() > 3.0, "the patrol moves");
|
||||
far = far.max((p.x - start.0).abs() + (p.y - start.1).abs());
|
||||
}
|
||||
assert!(far > 6.0, "the patrol walks its round");
|
||||
// an intruder next to one guard: the others join
|
||||
let (hx, hy) = g.ents.iter().find(|e| e.guard).unwrap().home.unwrap();
|
||||
g.spawn(EK::Ogre, false, hx + 1.0, hy, 8);
|
||||
|
||||
+6
-1
@@ -1,6 +1,7 @@
|
||||
// DungeonCraft – dungeon building & defence in the browser (Rust + macroquad, WASM).
|
||||
|
||||
mod audio;
|
||||
mod campaign;
|
||||
mod cloud;
|
||||
mod fortress;
|
||||
mod game;
|
||||
@@ -413,7 +414,7 @@ async fn main() {
|
||||
// ---------------- keyboard (only while playing)
|
||||
if playing {
|
||||
for i in 0..TOOLS.len() {
|
||||
if is_key_pressed(key(Action::Tool(i))) {
|
||||
if is_key_pressed(key(Action::Tool(i))) && app.game.tool_allowed(i) {
|
||||
ui.tool = TOOLS[i];
|
||||
inp.ldown = None;
|
||||
}
|
||||
@@ -664,6 +665,10 @@ async fn main() {
|
||||
app.game.hints = app.settings.hints;
|
||||
}
|
||||
UiAction::OpenMenu => menu.page = Some(Page::Pause),
|
||||
UiAction::Locked => {
|
||||
app.game.msg("msg.tool_locked", vec![]);
|
||||
app.game.sfx_ui(game::Sfx::Error);
|
||||
}
|
||||
UiAction::CloseHelp | UiAction::SelectLanguage(_) | UiAction::Consumed => {}
|
||||
UiAction::None => {
|
||||
if touch_mode {
|
||||
|
||||
+103
-8
@@ -59,6 +59,31 @@ pub enum Kind {
|
||||
Diamond, // diamond vein: very rare, very valuable
|
||||
}
|
||||
|
||||
// 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>, // guarded treasure caves; None: portals - 1
|
||||
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; 23] = [
|
||||
Kind::Rock,
|
||||
@@ -274,21 +299,25 @@ impl Map {
|
||||
}
|
||||
|
||||
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);
|
||||
let m = Map::try_generate(s, w, h, opts);
|
||||
// make sure heroes can reach the core
|
||||
if m.hero_path_to_core(idx(m.gate.0 + 1, m.gate.1 + 1)).is_some() {
|
||||
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) -> Map {
|
||||
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 {
|
||||
@@ -297,8 +326,8 @@ impl Map {
|
||||
// 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 = (area.round() as usize).max(1);
|
||||
let n_treasures = n_portals - 1;
|
||||
let n_portals = opts.portals.unwrap_or((area.round() as usize).max(1));
|
||||
let n_treasures = opts.treasures.unwrap_or(n_portals.saturating_sub(1));
|
||||
|
||||
let mut t = vec![Tile::new(Kind::Earth); w * h];
|
||||
for (i, tile) in t.iter_mut().enumerate() {
|
||||
@@ -310,7 +339,7 @@ impl Map {
|
||||
}
|
||||
}
|
||||
let inner = |x: i32, y: i32| x >= 2 && y >= 2 && x < wi - 2 && y < hi - 2;
|
||||
let core = (11, hi / 2 + rng.range(-5, 3));
|
||||
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
|
||||
@@ -332,7 +361,7 @@ impl Map {
|
||||
// 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 = vec![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));
|
||||
}
|
||||
@@ -448,7 +477,8 @@ impl Map {
|
||||
}
|
||||
}
|
||||
}
|
||||
// hero gate
|
||||
// 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 {
|
||||
@@ -457,6 +487,10 @@ impl Map {
|
||||
tl.owner = Owner::Hero;
|
||||
}
|
||||
}
|
||||
gate
|
||||
} else {
|
||||
(0, 0)
|
||||
};
|
||||
for tl in t.iter_mut() {
|
||||
if tl.vein().is_some() {
|
||||
tl.hp = 6.0;
|
||||
@@ -465,6 +499,67 @@ impl Map {
|
||||
Map { w, h, t, core, portals, legacy_portal: None, gate, treasures }
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
||||
@@ -254,6 +254,7 @@ enum TBtn {
|
||||
|
||||
pub enum UiAction {
|
||||
None,
|
||||
Locked, // a tool that is not available yet (campaign)
|
||||
OpenMenu,
|
||||
SelectTool(Tool),
|
||||
MinimapJump(f32, f32),
|
||||
@@ -371,6 +372,7 @@ impl Ui {
|
||||
self.open_cat = None;
|
||||
UiAction::OpenMenu
|
||||
}
|
||||
TBtn::Tool(i) if !g.tool_allowed(i) => UiAction::Locked,
|
||||
TBtn::Tool(i) => {
|
||||
self.open_cat = None;
|
||||
self.tip = Some((i, get_time()));
|
||||
@@ -378,7 +380,9 @@ impl Ui {
|
||||
}
|
||||
TBtn::Cat(c) => {
|
||||
let tools = CATS[c].1;
|
||||
if tools.len() == 1 {
|
||||
if tools.len() == 1 && !g.tool_allowed(tools[0]) {
|
||||
UiAction::Locked
|
||||
} else if tools.len() == 1 {
|
||||
self.open_cat = None;
|
||||
self.tip = Some((tools[0], get_time()));
|
||||
UiAction::SelectTool(TOOLS[tools[0]])
|
||||
@@ -430,13 +434,22 @@ impl Ui {
|
||||
gold_seg,
|
||||
vec![txt(tr("hud.mana"), MANA), Part::Gap, Part::Bar(g.mana / 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.gate"), BLUE), Part::Gap, Part::Bar(g.gate_hp / GATE_HP, BLUE)],
|
||||
vec![txt(tr("hud.creatures"), TXT), Part::Gap, txt(format!("{}/{}", g.creature_count(), cap), TXT)],
|
||||
vec![txt(tr("hud.workers"), TXT), Part::Gap, txt(g.worker_count().to_string(), TXT)],
|
||||
vec![txt(trf("hud.wave", &[(g.wave + 1).to_string()]), wave_col), Part::Gap, txt(mmss(g.wave_timer), TXT)],
|
||||
vec![txt(tr("hud.payday"), if g.payday <= PAYDAY_WARN { RED } else { GOLD }), Part::Gap, txt(mmss(g.payday), TXT)],
|
||||
vec![txt(tr("hud.army"), TXT), Part::Gap, txt((g.army_value() as i32).to_string(), TXT)],
|
||||
];
|
||||
// campaign missions: gate, waves and time limit only where the mission has them
|
||||
if g.has_gate() {
|
||||
segs.insert(3, vec![txt(tr("hud.gate"), BLUE), Part::Gap, Part::Bar(g.gate_hp / GATE_HP, BLUE)]);
|
||||
}
|
||||
if g.waves_on() {
|
||||
segs.push(vec![txt(trf("hud.wave", &[(g.wave + 1).to_string()]), wave_col), Part::Gap, txt(mmss(g.wave_timer), TXT)]);
|
||||
}
|
||||
if let Some(limit) = g.mission.as_ref().map(|m| m.time_limit).filter(|&l| l > 0.0) {
|
||||
let left = (limit - g.time).max(0.0);
|
||||
segs.insert(0, vec![txt(tr("hud.time_left"), if left < 120.0 { RED } else { GOLD }), Part::Gap, txt(mmss(left), TXT)]);
|
||||
}
|
||||
segs.push(vec![txt(tr("hud.payday"), if g.payday <= PAYDAY_WARN { RED } else { GOLD }), Part::Gap, txt(mmss(g.payday), TXT)]);
|
||||
segs.push(vec![txt(tr("hud.army"), TXT), Part::Gap, txt((g.army_value() as i32).to_string(), TXT)]);
|
||||
if !g.hand.is_empty() {
|
||||
segs.push(vec![txt(tr("hud.hand"), FRAME_HI), Part::Gap, txt(format!("{}/8", g.hand.len()), TXT)]);
|
||||
}
|
||||
@@ -499,7 +512,8 @@ impl Ui {
|
||||
let px = K(); // one pixel of the font, times the factor
|
||||
let pulse = (now * 2.5) as i64 % 2 == 0;
|
||||
// one toolbar button: icon, optional key, label in small text
|
||||
let tool_button = |bx: f32, by: f32, bw: f32, icon_id: u32, key: &str, label: &str, sel: bool, hovered: bool| {
|
||||
// locked tools (campaign: not yet available) are drawn dark
|
||||
let tool_button = |bx: f32, by: f32, bw: f32, icon_id: u32, key: &str, label: &str, sel: bool, hovered: bool, locked: bool| {
|
||||
let r = Rect::new(bx + px, by, bw - 2.0 * px, 3.0 * CH());
|
||||
draw_rectangle(r.x, r.y, r.w, r.h, if sel { Color::new(0.42, 0.26, 0.1, 1.0) } else if hovered { Color::new(0.18, 0.13, 0.1, 1.0) } else { Color::new(0.1, 0.07, 0.07, 1.0) });
|
||||
// the active tool: pulsing frame
|
||||
@@ -507,7 +521,7 @@ impl Ui {
|
||||
draw_rectangle_lines(r.x, r.y, r.w, r.h, if sel { 3.0 * px } else { 2.0 * px }, frame);
|
||||
// icon: the 16 px atlas cell at 2*F, centred on the grid
|
||||
let icon = 32.0 * px;
|
||||
gfx.draw_cell(icon_id, bx + (bw - icon) * 0.5, by + 4.0 * px, icon, icon, WHITE);
|
||||
gfx.draw_cell(icon_id, bx + (bw - icon) * 0.5, by + 4.0 * px, icon, icon, if locked { Color::new(0.25, 0.22, 0.2, 1.0) } else { WHITE });
|
||||
if !key.is_empty() {
|
||||
gfx.text(key, bx + 3.0 * px, by + 3.0 * px, SMALL(), if sel { FRAME_HI } else { DIM });
|
||||
}
|
||||
@@ -516,7 +530,7 @@ impl Ui {
|
||||
let label: String = label.chars().take(maxc.min(11)).collect();
|
||||
let lw = gfx.measure(&label, SMALL());
|
||||
let lx = bx + ((bw - lw) / (GW * SMALL()) / 2.0).floor() * GW * SMALL();
|
||||
gfx.text(&label, lx, by + 2.0 * CH() + 4.0 * px, SMALL(), if sel { FRAME_HI } else { TXT });
|
||||
gfx.text(&label, lx, by + 2.0 * CH() + 4.0 * px, SMALL(), if sel { FRAME_HI } else if locked { DIM } else { TXT });
|
||||
r
|
||||
};
|
||||
let mut popup_top = tb_y;
|
||||
@@ -540,7 +554,8 @@ impl Ui {
|
||||
if hovered && tools.len() == 1 {
|
||||
hover_tool = Some(tools[0]);
|
||||
}
|
||||
let r = tool_button(bx, by, bw, ti.icon, key, &label, has, self.open_cat == Some(c) || hovered);
|
||||
let locked = !tools.iter().any(|&t| g.tool_allowed(t));
|
||||
let r = tool_button(bx, by, bw, ti.icon, key, &label, has, self.open_cat == Some(c) || hovered, locked);
|
||||
if has {
|
||||
gfx.glyph(0x1F, bx + (bw - CW()) * 0.5, tb_y, S(), if pulse { WHITE } else { FRAME_HI });
|
||||
}
|
||||
@@ -701,7 +716,7 @@ impl Ui {
|
||||
hover_tool = Some(i);
|
||||
}
|
||||
let key = if touch_ui() { "" } else { keys[i].as_str() };
|
||||
let r = tool_button(bx, by, bw, ti.icon, key, &ti.label, self.tool == TOOLS[i], hovered);
|
||||
let r = tool_button(bx, by, bw, ti.icon, key, &ti.label, self.tool == TOOLS[i], hovered, !g.tool_allowed(i));
|
||||
self.touch_btns.push((r, TBtn::Tool(i)));
|
||||
}
|
||||
popup_top = py0;
|
||||
|
||||
Reference in new issue
Block a user