Aerie and flying creatures: bats, chain gargoyles, arcane owls, ember wyrms
- New room: aerie (key Y, rooms category, campaign from mission 7); flyers sleep there - Arrivals: aerie 4+ bats; + torture chamber gargoyles; + library owls; big aerie and treasury (16+ each) a rare ember wyrm; portal checks free places in lairs and aerie separately - Flyers cross water and lava (path finding, flag, attacks), are not burnt by lava, patrol far when idle, attack single enemies, flee to the aerie below 25% health and rest there up to 75% - Owls shoot arcane bolts, the wyrm breathes fire with splash damage; shots from the air - Hovering sprites with flapping, aerie floor, perches, roosts, plinths, twig and ember nests - Room counts are recounted on load (no longer stored); tests for flying and arrivals Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
729dd4fe4c
commit
b7d7fc2b52
10 files changed
+454
-56
No files matched your search
+11
-1
@@ -145,7 +145,7 @@ help.k_slap = Shift+Links Hand: Rückhand für eigene, Faust für Helden au
|
||||
help.k_pan = Rechts ziehen Karte verschieben (auch WASD/Pfeile, Bildrand)
|
||||
help.k_rot = Mitte ziehen Kamera drehen und neigen (auch Q/E, R/F)
|
||||
help.k_zoom = Mausrad Zoomen
|
||||
help.k_tools = 1-9,0,X,V,K,J,B,C,T,U Werkzeug
|
||||
help.k_tools = 1-9,0,X,V,K,J,B,C,T,U,Y Werkzeug
|
||||
help.k_misc = P / Leertaste Pause M Ton an/aus +/- Lautstärke
|
||||
help.k_misc2 = G Kachel-Umrisse an/aus H Diese Hilfe
|
||||
help.s1 = 1. Grabe zum Portal und lass es von Grubenwichten erobern.
|
||||
@@ -517,3 +517,13 @@ rank.7 = Hauptfeldwebel
|
||||
rank.8 = Hauptmann
|
||||
rank.9 = Oberst
|
||||
rank.10 = Feldherr
|
||||
|
||||
# aerie and flying creatures
|
||||
tool.aerie.label = Horst
|
||||
tool.aerie.desc = Sitzstangen und Nester für Flugwesen. Ab 4 Feldern kommen Riesenfledermäuse; mit Folterkammer Kettengargoyles, mit Bibliothek Arkane Eulen; ein großer Horst (16+) mit Schatzkammer (16+) lockt vielleicht einen Glutwyrm. Flugwesen überfliegen Wasser und Lava.
|
||||
room.aerie = Horst
|
||||
unit.bat = Riesenfledermaus
|
||||
unit.gargoyle = Kettengargoyle
|
||||
unit.owl = Arkane Eule
|
||||
unit.wyrm = Glutwyrm
|
||||
job.rest = Erholt sich im Horst
|
||||
+11
-1
@@ -146,7 +146,7 @@ help.k_slap = Shift+Left Hand: backhand own units, punch heroes on your gro
|
||||
help.k_pan = Right drag Move the map (also WASD/arrows, screen edge)
|
||||
help.k_rot = Middle drag Rotate and tilt the camera (also Q/E, R/F)
|
||||
help.k_zoom = Mouse wheel Zoom
|
||||
help.k_tools = 1-9,0,X,V,K,J,B,C,T,U Select tool
|
||||
help.k_tools = 1-9,0,X,V,K,J,B,C,T,U,Y Select tool
|
||||
help.k_misc = P / Space Pause M Sound on/off +/- Volume
|
||||
help.k_misc2 = G Tile outlines on/off H This help
|
||||
help.s1 = 1. Dig to the portal and let your Pit Imps claim it.
|
||||
@@ -518,3 +518,13 @@ rank.7 = Master Sergeant
|
||||
rank.8 = Captain
|
||||
rank.9 = Colonel
|
||||
rank.10 = Warlord
|
||||
|
||||
# aerie and flying creatures
|
||||
tool.aerie.label = Aerie
|
||||
tool.aerie.desc = Perches and nests for flying creatures. 4+ tiles attract giant bats; with a torture chamber chain gargoyles, with a library arcane owls; a big aerie (16+) and treasury (16+) may lure an ember wyrm. Flyers cross water and lava.
|
||||
room.aerie = Aerie
|
||||
unit.bat = Giant Bat
|
||||
unit.gargoyle = Chain Gargoyle
|
||||
unit.owl = Arcane Owl
|
||||
unit.wyrm = Ember Wyrm
|
||||
job.rest = Recovering in the aerie
|
||||
+2
-2
@@ -9,8 +9,8 @@ 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];
|
||||
// torture, temple, arrow tower, cannon tower, magic tower, bridge, aerie
|
||||
pub const TOOL_UNLOCK: [usize; 19] = [0, 0, 0, 0, 0, 2, 4, 0, 0, 3, 4, 0, 6, 9, 3, 5, 9, 1, 6];
|
||||
|
||||
pub struct FortDef {
|
||||
pub at: (f32, f32), // centre as a fraction of the map size
|
||||
|
||||
+256
-36
@@ -85,7 +85,7 @@ pub struct SfxEvent {
|
||||
pub const MANA_MAX: f32 = 400.0;
|
||||
pub const CARRY_MAX: i32 = 300;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)]
|
||||
pub enum EK {
|
||||
Worker,
|
||||
Kobold,
|
||||
@@ -100,6 +100,11 @@ pub enum EK {
|
||||
Jailer, // attracted by the torture chamber
|
||||
Priestess, // attracted by the temple
|
||||
Chainbreaker, // rare and very strong
|
||||
// flying creatures, attracted by the aerie
|
||||
Bat, // fast and weak
|
||||
Gargoyle, // aerie + torture chamber: stone, tough
|
||||
Owl, // aerie + library: arcane bolts from afar
|
||||
Wyrm, // big aerie + treasury: rare, very strong, fire breath
|
||||
}
|
||||
|
||||
pub struct St {
|
||||
@@ -130,9 +135,18 @@ pub fn st(k: EK) -> St {
|
||||
EK::Jailer => s("unit.jailer", 260.0, 16.0, 1.0, 1.6, 1.3, 140, 0),
|
||||
EK::Priestess => s("unit.priestess", 70.0, 14.0, 5.0, 2.0, 1.6, 120, 0),
|
||||
EK::Chainbreaker => s("unit.chainbreaker", 600.0, 36.0, 1.1, 2.3, 1.1, 400, 0),
|
||||
EK::Bat => s("unit.bat", 55.0, 6.0, 1.0, 3.6, 0.7, 30, 0),
|
||||
EK::Gargoyle => s("unit.gargoyle", 260.0, 15.0, 1.0, 2.2, 1.2, 120, 0),
|
||||
EK::Owl => s("unit.owl", 70.0, 12.0, 4.5, 2.8, 1.3, 100, 0),
|
||||
EK::Wyrm => s("unit.wyrm", 650.0, 30.0, 3.0, 2.5, 1.4, 380, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// flying creatures cross water and lava and sleep in the aerie
|
||||
pub fn flies(k: EK) -> bool {
|
||||
matches!(k, EK::Bat | EK::Gargoyle | EK::Owl | EK::Wyrm)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||
pub enum Job {
|
||||
None,
|
||||
@@ -155,6 +169,7 @@ pub enum Job {
|
||||
Pray(usize), // temple tile
|
||||
HealCore, // flame priestess heals the soul core
|
||||
TearBridge(usize), // knight tears down a bridge of the player
|
||||
Rest(usize), // flying creature recovers in the aerie (flees when badly hurt)
|
||||
Help(usize), // imp with nothing else to do helps another imp at this tile
|
||||
}
|
||||
|
||||
@@ -334,7 +349,8 @@ pub struct Game {
|
||||
pub state: u8, // 0 running, 1 victory, 2 defeat
|
||||
pub paused: bool,
|
||||
pub hand: Vec<u32>,
|
||||
pub counts: [usize; 7], // treasury, lair, farm, training hall, library, torture chamber, temple
|
||||
#[serde(skip)]
|
||||
pub counts: [usize; 8], // treasury, lair, farm, training hall, library, torture chamber, temple, aerie (recounted)
|
||||
pub gate_hp: f32,
|
||||
pub gate_hit_t: f32,
|
||||
pub gate_stage: u32, // defender thresholds triggered (0..3)
|
||||
@@ -395,9 +411,9 @@ impl Torch {
|
||||
}
|
||||
}
|
||||
|
||||
pub const ROOM_KINDS: [Kind; 7] = [Kind::Treasury, Kind::Lair, Kind::Farm, Kind::Training, Kind::Library, Kind::Torture, Kind::Temple];
|
||||
pub const ROOM_COST: [i32; 7] = [60, 80, 100, 140, 160, 200, 180];
|
||||
pub const ROOM_NAMES: [&str; 7] = ["room.treasury", "room.lair", "room.farm", "room.training", "room.library", "room.torture", "room.temple"];
|
||||
pub const ROOM_KINDS: [Kind; 8] = [Kind::Treasury, Kind::Lair, Kind::Farm, Kind::Training, Kind::Library, Kind::Torture, Kind::Temple, Kind::Aerie];
|
||||
pub const ROOM_COST: [i32; 8] = [60, 80, 100, 140, 160, 200, 180, 120];
|
||||
pub const ROOM_NAMES: [&str; 8] = ["room.treasury", "room.lair", "room.farm", "room.training", "room.library", "room.torture", "room.temple", "room.aerie"];
|
||||
|
||||
// bridge cost per tile: over water, over lava
|
||||
pub const BRIDGE_COST: [i32; 2] = [30, 75];
|
||||
@@ -482,7 +498,7 @@ impl Game {
|
||||
state: 0,
|
||||
paused: false,
|
||||
hand: Vec::new(),
|
||||
counts: [0; 7],
|
||||
counts: [0; 8],
|
||||
gate_hp: GATE_HP,
|
||||
gate_hit_t: -100.0,
|
||||
gate_stage: 0,
|
||||
@@ -630,7 +646,7 @@ impl Game {
|
||||
}
|
||||
|
||||
pub fn recount(&mut self) {
|
||||
let mut c = [0usize; 7];
|
||||
let mut c = [0usize; 8];
|
||||
for t in &self.map.t {
|
||||
if t.owner == Owner::Player {
|
||||
if let Some(r) = room_index(t.kind) {
|
||||
@@ -1011,19 +1027,37 @@ impl Game {
|
||||
|
||||
// free beds in the lairs: much room lets creatures arrive faster, little room slower
|
||||
pub fn free_beds(&self) -> usize {
|
||||
self.counts[1].min(30).saturating_sub(self.creature_count())
|
||||
let (lair, aerie) = self.free_lair_aerie();
|
||||
lair + aerie
|
||||
}
|
||||
|
||||
// free places in the lairs (walking creatures) and in the aerie (flying creatures)
|
||||
pub fn free_lair_aerie(&self) -> (usize, usize) {
|
||||
let flyers = self.ents.iter().filter(|e| e.alive && e.creature() && flies(e.kind)).count();
|
||||
let walkers = self.creature_count() - flyers;
|
||||
(self.counts[1].min(30).saturating_sub(walkers), self.counts[7].min(20).saturating_sub(flyers))
|
||||
}
|
||||
|
||||
pub fn lair_factor(&self) -> f32 {
|
||||
(8.0 / (self.free_beds() as f32 + 2.0)).clamp(0.35, 3.0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn try_portal_for_test(&mut self) {
|
||||
let (px, py) = self.map.portals[0];
|
||||
for y in 0..3 {
|
||||
for x in 0..3 {
|
||||
self.map.t[idx(px + x, py + y)].owner = Owner::Player;
|
||||
}
|
||||
}
|
||||
self.try_portal();
|
||||
}
|
||||
|
||||
fn try_portal(&mut self) {
|
||||
let creatures = self.creature_count();
|
||||
let lair = self.counts[1];
|
||||
let aerie = self.counts[7];
|
||||
let farm = self.counts[2];
|
||||
let cap = lair.min(30);
|
||||
if lair == 0 {
|
||||
if lair == 0 && aerie == 0 {
|
||||
self.hint(0, "hint.portal_no_lair");
|
||||
return;
|
||||
}
|
||||
@@ -1031,11 +1065,46 @@ impl Game {
|
||||
self.hint(1, "hint.no_farm");
|
||||
return;
|
||||
}
|
||||
if creatures >= cap {
|
||||
let (free_lair, free_aerie) = self.free_lair_aerie();
|
||||
if free_lair == 0 && free_aerie == 0 {
|
||||
self.hint(2, "hint.lair_full");
|
||||
return;
|
||||
}
|
||||
let mut opts: Vec<(EK, u32)> = vec![(EK::Kobold, 4)];
|
||||
let mut opts: Vec<(EK, u32)> = Vec::new();
|
||||
// flying creatures need room in the aerie
|
||||
if free_aerie > 0 {
|
||||
if aerie >= 4 {
|
||||
opts.push((EK::Bat, 4));
|
||||
}
|
||||
if aerie >= 6 && self.counts[5] >= 4 {
|
||||
opts.push((EK::Gargoyle, 2));
|
||||
}
|
||||
if aerie >= 6 && self.counts[4] >= 6 {
|
||||
opts.push((EK::Owl, 2));
|
||||
}
|
||||
let has_wyrm = self.ents.iter().any(|e| e.alive && e.kind == EK::Wyrm && !e.hero);
|
||||
if aerie >= 16 && self.counts[0] >= 16 && !has_wyrm {
|
||||
opts.push((EK::Wyrm, 1));
|
||||
}
|
||||
}
|
||||
if free_lair == 0 {
|
||||
if opts.is_empty() {
|
||||
self.hint(2, "hint.lair_full");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
self.portal_walkers(&mut opts);
|
||||
}
|
||||
if opts.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.portal_arrival(opts);
|
||||
}
|
||||
|
||||
// walking creatures that the rooms attract
|
||||
fn portal_walkers(&mut self, opts: &mut Vec<(EK, u32)>) {
|
||||
let farm = self.counts[2];
|
||||
opts.push((EK::Kobold, 4));
|
||||
if farm >= 6 {
|
||||
opts.push((EK::Spider, 3));
|
||||
}
|
||||
@@ -1058,6 +1127,9 @@ impl Game {
|
||||
if room_tiles >= 40 && veteran && self.gold >= 4000.0 && !has_one {
|
||||
opts.push((EK::Chainbreaker, 1));
|
||||
}
|
||||
}
|
||||
|
||||
fn portal_arrival(&mut self, opts: Vec<(EK, u32)>) {
|
||||
let total: u32 = opts.iter().map(|o| o.1).sum();
|
||||
let mut r = self.rng.next() % total;
|
||||
let mut kind = EK::Kobold;
|
||||
@@ -1313,7 +1385,7 @@ impl Game {
|
||||
}
|
||||
let next = e.path[0];
|
||||
let tile = self.map.t[next];
|
||||
if !tile.walkable() {
|
||||
if !tile.walkable() && !(flies(e.kind) && tile.liquid()) {
|
||||
if e.hero && tile.diggable() {
|
||||
// heroes dig their way through
|
||||
e.working = true;
|
||||
@@ -1470,7 +1542,8 @@ impl Game {
|
||||
if e.path.is_empty() || e.repath <= 0.0 {
|
||||
e.repath = 0.6;
|
||||
let tt = idx(tx as i32, ty as i32);
|
||||
match self.map.path_to(e.tile(), tt) {
|
||||
let p = if flies(e.kind) { self.map.fly_path_to(e.tile(), tt) } else { self.map.path_to(e.tile(), tt) };
|
||||
match p {
|
||||
Some(p) => e.path = p,
|
||||
None => return false,
|
||||
}
|
||||
@@ -1496,9 +1569,13 @@ impl Game {
|
||||
EK::Mage => 0xffff44cc,
|
||||
EK::Priest => 0xff88ffff,
|
||||
EK::Priestess => 0xff3080ff,
|
||||
EK::Owl => 0xffffa060,
|
||||
EK::Wyrm => 0xff2080ff,
|
||||
_ => 0xff70b0d0,
|
||||
};
|
||||
self.fx(0, e.x, e.y, tx, ty, 0.25, col);
|
||||
// flyers shoot from the air
|
||||
let h = if flies(e.kind) { 0.8 } else { 0.45 };
|
||||
self.shot(e.x, e.y, h, tx, ty, 0.45, 0.25, col);
|
||||
self.sfx(Sfx::Shoot, e.x, e.y);
|
||||
} else {
|
||||
self.fx(2, tx, ty, 0.0, 0.0, 0.25, 0xffffffff);
|
||||
@@ -1524,8 +1601,8 @@ impl Game {
|
||||
}
|
||||
}
|
||||
}
|
||||
if e.kind == EK::Priestess {
|
||||
// fireball: half damage to enemies around the target
|
||||
if e.kind == EK::Priestess || e.kind == EK::Wyrm {
|
||||
// fireball / fire breath: half damage to enemies around the target
|
||||
self.fx(3, tx, ty, 0.0, 0.0, 0.5, 0xff3080ff);
|
||||
let side = !e.hero;
|
||||
let splash: Vec<usize> = self
|
||||
@@ -1651,6 +1728,15 @@ impl Game {
|
||||
let e = &self.ents[j];
|
||||
let (nx, ny) = (e.x + dx, e.y + dy);
|
||||
let Some(t) = self.map.get(nx as i32, ny as i32).copied() else { return };
|
||||
if flies(self.ents[j].kind) {
|
||||
if t.walkable() || t.liquid() {
|
||||
let e = &mut self.ents[j];
|
||||
e.x = nx;
|
||||
e.y = ny;
|
||||
e.path.clear();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if t.kind == Kind::Lava {
|
||||
let e = &mut self.ents[j];
|
||||
e.x = nx;
|
||||
@@ -1670,7 +1756,7 @@ impl Game {
|
||||
let (x, y) = xy(i);
|
||||
for j in 0..self.ents.len() {
|
||||
let e = &self.ents[j];
|
||||
if !e.alive || e.held || e.tile() != i {
|
||||
if !e.alive || e.held || e.tile() != i || flies(e.kind) {
|
||||
continue;
|
||||
}
|
||||
if lava {
|
||||
@@ -1973,6 +2059,38 @@ impl Game {
|
||||
false
|
||||
}
|
||||
|
||||
// flying creatures patrol far around (also over rivers and the land of the enemy)
|
||||
fn patrol_flight(&mut self, e: &mut Ent) {
|
||||
let (x, y) = xy(e.tile());
|
||||
for _ in 0..8 {
|
||||
let nx = x + self.rng.range(-14, 14);
|
||||
let ny = y + self.rng.range(-14, 14);
|
||||
let ok = self.map.get(nx, ny).map_or(false, |t| t.walkable() || t.liquid());
|
||||
if ok {
|
||||
if let Some(p) = self.map.fly_path_to(e.tile(), idx(nx, ny)) {
|
||||
if p.len() <= 40 {
|
||||
e.path = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
e.job = Job::Wander;
|
||||
e.timer = 2.0 + self.rng.f() * 3.0;
|
||||
}
|
||||
|
||||
// badly hurt flyers flee to the aerie and rest until they are healed to 75%
|
||||
fn start_rest(&mut self, e: &mut Ent) -> bool {
|
||||
let cur = e.tile();
|
||||
let target = e.bed.filter(|&b| self.map.t[b].kind == Kind::Aerie).or_else(|| {
|
||||
self.map.search(cur, self.map.fly_cost(), |i| self.map.t[i].kind == Kind::Aerie, true).map(|p| p.last().copied().unwrap_or(cur))
|
||||
});
|
||||
let Some(b) = target else { return false };
|
||||
e.path = self.map.fly_path_to(cur, b).unwrap_or_default();
|
||||
e.job = Job::Rest(b);
|
||||
true
|
||||
}
|
||||
|
||||
fn wander(&mut self, e: &mut Ent, radius: i32) {
|
||||
let (x, y) = xy(e.tile());
|
||||
for _ in 0..6 {
|
||||
@@ -2303,9 +2421,13 @@ impl Game {
|
||||
if e.think <= 0.0 {
|
||||
e.think = 0.4 + self.rng.f() * 0.3;
|
||||
// a new creature builds its own bed in the lair soon after its arrival
|
||||
if e.bed.is_none() && self.counts[1] > 0 && !matches!(e.job, Job::Attack(_) | Job::Leave | Job::Rally) && self.rng.f() < 0.15 {
|
||||
let bed_kind = if flies(e.kind) { Kind::Aerie } else { Kind::Lair };
|
||||
let beds = if flies(e.kind) { self.counts[7] } else { self.counts[1] };
|
||||
if e.bed.is_none() && beds > 0 && !matches!(e.job, Job::Attack(_) | Job::Leave | Job::Rally) && self.rng.f() < 0.15 {
|
||||
let cur = e.tile();
|
||||
if let Some(p) = self.map.search(cur, self.map.walk_cost(), |i| self.map.t[i].kind == Kind::Lair && self.map.t[i].bed == 0, true) {
|
||||
let free = |i: usize| self.map.t[i].kind == bed_kind && self.map.t[i].bed == 0;
|
||||
let found = if flies(e.kind) { self.map.search(cur, self.map.fly_cost(), free, true) } else { self.map.search(cur, self.map.walk_cost(), free, true) };
|
||||
if let Some(p) = found {
|
||||
let b = p.last().copied().unwrap_or(cur);
|
||||
self.map.t[b].bed = me;
|
||||
e.bed = Some(b);
|
||||
@@ -2313,8 +2435,15 @@ impl Game {
|
||||
self.fx(8, bx as f32 + 0.5, by as f32 + 0.5, 0.0, 0.0, 0.6, 0xff40d0ff);
|
||||
}
|
||||
}
|
||||
if !matches!(e.job, Job::Attack(_) | Job::Leave) {
|
||||
let radius = if e.job == Job::Rally { 8.5 } else { 7.0 };
|
||||
// flyers flee when badly hurt and come back at 75%
|
||||
if flies(e.kind) && e.hp < e.max_hp * 0.25 && !matches!(e.job, Job::Rest(_) | Job::Leave) {
|
||||
self.unreserve(me);
|
||||
if self.start_rest(e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if !matches!(e.job, Job::Attack(_) | Job::Leave | Job::Rest(_)) {
|
||||
let radius = if e.job == Job::Rally { 8.5 } else if flies(e.kind) { 9.0 } else { 7.0 };
|
||||
if let Some(id) = self.nearest_enemy(e, radius) {
|
||||
self.unreserve(me);
|
||||
e.job = Job::Attack(id);
|
||||
@@ -2338,6 +2467,28 @@ impl Game {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if let Job::Rest(b) = e.job {
|
||||
if self.step_path(e, dt) {
|
||||
return;
|
||||
}
|
||||
let (bx, by) = xy(b);
|
||||
let at_aerie = (e.x - bx as f32 - 0.5).abs() < 1.5 && (e.y - by as f32 - 0.5).abs() < 1.5;
|
||||
if !at_aerie {
|
||||
// pushed away on the way: fly on, or give up if there is no way
|
||||
match self.map.fly_path_to(e.tile(), b) {
|
||||
Some(p) if !p.is_empty() => e.path = p,
|
||||
_ => e.job = Job::None,
|
||||
}
|
||||
return;
|
||||
}
|
||||
// resting in the aerie heals quickly, back to work at 75%
|
||||
e.hp = (e.hp + e.max_hp * dt / 12.0).min(e.max_hp);
|
||||
e.energy = (e.energy + dt / 14.0).min(1.0);
|
||||
if e.hp >= e.max_hp * 0.75 {
|
||||
e.job = Job::None;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if e.kind == EK::Chainbreaker {
|
||||
// restless: without a fight for two minutes its mood sinks (skill_cd counts down as idle time)
|
||||
if matches!(e.job, Job::Attack(_)) {
|
||||
@@ -2529,7 +2680,8 @@ impl Game {
|
||||
}
|
||||
if e.path.is_empty() || e.repath <= 0.0 {
|
||||
e.repath = 1.5;
|
||||
match self.map.path_to(e.tile(), target) {
|
||||
let p = if flies(e.kind) { self.map.fly_path_to(e.tile(), target) } else { self.map.path_to(e.tile(), target) };
|
||||
match p {
|
||||
Some(p) => e.path = p,
|
||||
None => {
|
||||
self.hint(11, "hint.flag_unreachable");
|
||||
@@ -2592,22 +2744,20 @@ impl Game {
|
||||
return;
|
||||
}
|
||||
if e.energy < 0.25 {
|
||||
// flying creatures sleep in the aerie
|
||||
let bed_kind = if flies(e.kind) { Kind::Aerie } else { Kind::Lair };
|
||||
if let Some(b) = e.bed {
|
||||
let t = &self.map.t[b];
|
||||
if t.kind != Kind::Lair || t.bed != me {
|
||||
if t.kind != bed_kind || t.bed != me {
|
||||
e.bed = None;
|
||||
}
|
||||
}
|
||||
if e.bed.is_none() {
|
||||
let found = self.map.search(
|
||||
cur,
|
||||
self.map.walk_cost(),
|
||||
|i| {
|
||||
let t = &self.map.t[i];
|
||||
t.kind == Kind::Lair && t.bed == 0
|
||||
},
|
||||
true,
|
||||
);
|
||||
let free = |i: usize| {
|
||||
let t = &self.map.t[i];
|
||||
t.kind == bed_kind && t.bed == 0
|
||||
};
|
||||
let found = if flies(e.kind) { self.map.search(cur, self.map.fly_cost(), free, true) } else { self.map.search(cur, self.map.walk_cost(), free, true) };
|
||||
if let Some(p) = found {
|
||||
let b = p.last().copied().unwrap_or(cur);
|
||||
self.map.t[b].bed = me;
|
||||
@@ -2615,7 +2765,8 @@ impl Game {
|
||||
}
|
||||
}
|
||||
if let Some(b) = e.bed {
|
||||
if let Some(p) = self.map.path_to(cur, b) {
|
||||
let way = if flies(e.kind) { self.map.fly_path_to(cur, b) } else { self.map.path_to(cur, b) };
|
||||
if let Some(p) = way {
|
||||
e.path = p;
|
||||
e.job = Job::Sleep(b);
|
||||
return;
|
||||
@@ -2692,7 +2843,11 @@ impl Game {
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.wander(e, 4);
|
||||
if flies(e.kind) {
|
||||
self.patrol_flight(e);
|
||||
} else {
|
||||
self.wander(e, 4);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- prisoners
|
||||
@@ -3910,6 +4065,7 @@ pub fn job_name(e: &Ent) -> &'static str {
|
||||
Job::Study(_) => "job.study",
|
||||
Job::Attack(_) => "job.fight",
|
||||
Job::TearBridge(_) => "job.tear_bridge",
|
||||
Job::Rest(_) => "job.rest",
|
||||
Job::Help(_) => "job.help",
|
||||
Job::AttackCore => {
|
||||
if e.working {
|
||||
@@ -4382,6 +4538,70 @@ mod tests {
|
||||
assert!(best <= 2.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flyers_cross_rivers_flee_and_heal() {
|
||||
let (mut g, rx) = river_game(true);
|
||||
let (cx, cy) = g.map.core;
|
||||
// an aerie in the start area
|
||||
g.gold = 5000.0;
|
||||
g.build_rect(7, cx - 3, cy - 3, cx - 2, cy + 2);
|
||||
assert!(g.counts[7] >= 8);
|
||||
// a knight on the far side of the lava: an owl reaches it, a kobold does not
|
||||
let far = (rx + 4, cy);
|
||||
for x in rx + 3..rx + 7 {
|
||||
let t = &mut g.map.t[idx(x, cy)];
|
||||
t.kind = Kind::Floor;
|
||||
t.owner = Owner::None;
|
||||
}
|
||||
let k = g.spawn(EK::Knight, true, far.0 as f32 + 0.5, far.1 as f32 + 0.5, 1);
|
||||
let owl = g.spawn(EK::Owl, false, (cx + 4) as f32 + 0.5, cy as f32 + 0.5, 3);
|
||||
assert!(g.map.fly_path_to(idx(cx + 4, cy), idx(far.0, far.1)).is_some());
|
||||
assert!(g.map.path_to(idx(cx + 4, cy), idx(far.0, far.1)).is_none());
|
||||
let hp0 = g.ents.iter().find(|e| e.id == k).unwrap().hp;
|
||||
for _ in 0..300 {
|
||||
g.update(0.05);
|
||||
}
|
||||
let hit = g.ents.iter().find(|e| e.id == k).map_or(true, |e| e.hp < hp0);
|
||||
assert!(hit, "the owl attacks across the lava");
|
||||
// a badly hurt flyer flees to the aerie and heals up to 75%
|
||||
g.ents.retain(|e| !e.hero);
|
||||
let j = g.ents.iter().position(|e| e.id == owl).unwrap();
|
||||
g.ents[j].hp = g.ents[j].max_hp * 0.2;
|
||||
let mut rested = false;
|
||||
for _ in 0..800 {
|
||||
g.update(0.05);
|
||||
let e = g.ents.iter().find(|e| e.id == owl).unwrap();
|
||||
rested |= matches!(e.job, Job::Rest(_));
|
||||
if e.hp >= e.max_hp * 0.75 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let e = g.ents.iter().find(|e| e.id == owl).unwrap();
|
||||
assert!(rested && e.hp >= e.max_hp * 0.74, "fled and healed: {} / {}", e.hp, e.max_hp);
|
||||
// lava does not burn flyers
|
||||
let j = g.ents.iter().position(|e| e.id == owl).unwrap();
|
||||
g.ents[j].x = (rx - 1) as f32 + 0.5;
|
||||
g.ents[j].y = cy as f32 + 0.5;
|
||||
g.knock(j, 1.0, 0.0);
|
||||
assert!(g.ents[j].alive);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aerie_attracts_flyers() {
|
||||
let mut g = Game::new(3, 64, 64);
|
||||
g.counts = [16, 0, 9, 0, 6, 4, 0, 16];
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for _ in 0..200 {
|
||||
g.try_portal_for_test();
|
||||
for e in g.ents.iter().filter(|e| e.creature()) {
|
||||
seen.insert(e.kind);
|
||||
}
|
||||
g.ents.retain(|e| !e.creature());
|
||||
}
|
||||
assert!(seen.contains(&EK::Bat) && seen.contains(&EK::Gargoyle) && seen.contains(&EK::Owl), "{:?}", seen);
|
||||
assert!(seen.iter().all(|&k| flies(k)), "no lair: only flyers come ({:?})", seen);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn imps_reinforce_walls() {
|
||||
let mut g = Game::new(3, 64, 64);
|
||||
|
||||
+88
-2
@@ -64,6 +64,7 @@ pub const D_RUBY: u32 = 122;
|
||||
pub const D_DIAMOND: u32 = 123;
|
||||
pub const D_RUBBLE: u32 = 124; // floor decals: pebbles and fragments
|
||||
pub const D_CRACK: u32 = 125; // a long crack through several stones
|
||||
pub const T_AERIE: u32 = 126; // 4 variants: straw, twigs and feathers
|
||||
|
||||
pub struct Gfx {
|
||||
pub atlas: Texture2D,
|
||||
@@ -726,6 +727,23 @@ fn gen_atlas() -> Image {
|
||||
let h = hash(cu, cv, seed) % 3;
|
||||
Some(if e { lo } else if h == 0 || (u % 4 == 1 && v % 4 == 1) { hi } else if d > 0.65 { lo } else { mid })
|
||||
};
|
||||
// aerie: straw and twigs on stone, a few feathers
|
||||
for v in 0..4u32 {
|
||||
cv.cell(T_AERIE + v, |x, y| {
|
||||
let base = stones(x, y, v + 230, (70, 62, 58), (36, 30, 30), 8);
|
||||
let twig = (x * 3 + y * 7 + v as i32 * 5) % 13 == 0 || (x * 5 - y * 2 + v as i32) % 17 == 0;
|
||||
let straw = rnd(x / 2, y, v + 231) > 0.62;
|
||||
if hash(x, y, v + 232) % 41 == 0 {
|
||||
Some(c(235, 232, 225)) // feather
|
||||
} else if twig {
|
||||
Some(c(96, 66, 40))
|
||||
} else if straw {
|
||||
Some(shade((190, 160, 80), (rnd(x, y, v + 233) * 20.0) as i32 - 10))
|
||||
} else {
|
||||
Some(base)
|
||||
}
|
||||
});
|
||||
}
|
||||
// floor decals
|
||||
cv.cell(D_RUBBLE, |x, y| {
|
||||
for k in 0..9 {
|
||||
@@ -890,6 +908,55 @@ fn sprite_rows(k: EK) -> [&'static str; 10] {
|
||||
".kRRRRRRk.",
|
||||
"..kkkkkk..",
|
||||
],
|
||||
// flying creatures
|
||||
EK::Bat => [
|
||||
"..........",
|
||||
"k..k..k..k",
|
||||
"Pk.kPPk.kP",
|
||||
"PPkPPPPkPP",
|
||||
"PPPPrPrPPP",
|
||||
"PPPPPPPPPP",
|
||||
".PPkPPkPP.",
|
||||
"..k.PP.k..",
|
||||
".....k....",
|
||||
"..........",
|
||||
],
|
||||
EK::Gargoyle => [
|
||||
"w........w",
|
||||
"Cw.kkkk.wC",
|
||||
"CCkceeckCC",
|
||||
"CCkcccckCC",
|
||||
"CkcccccckC",
|
||||
"k.kcCCck.k",
|
||||
".kcccccck.",
|
||||
".kcCkkCck.",
|
||||
".kck..kck.",
|
||||
".kk....kk.",
|
||||
],
|
||||
EK::Owl => [
|
||||
"..k....k..",
|
||||
".kBk..kBk.",
|
||||
".kbbbbbbk.",
|
||||
"kbeebbeebk",
|
||||
"kbekbbekbk",
|
||||
".kbbyybbk.",
|
||||
"kubbbbbbuk",
|
||||
"kubbwwbbuk",
|
||||
".kubbbbuk.",
|
||||
"..ky..yk..",
|
||||
],
|
||||
EK::Wyrm => [
|
||||
"R.......kw",
|
||||
"RR....krrk",
|
||||
"RRR..krekr",
|
||||
"RRRRkrrrrk",
|
||||
".RRrrrrok.",
|
||||
"..rrooork.",
|
||||
".krrooorr.",
|
||||
"krrrrrrRk.",
|
||||
"k.rrk.rk..",
|
||||
"..kk..kk..",
|
||||
],
|
||||
// obsidian demon with glowing cracks, broken chains and twin axes
|
||||
EK::Chainbreaker => [
|
||||
"s.kkkkkk.s",
|
||||
@@ -947,9 +1014,27 @@ fn pal(ch: char) -> Option<Color> {
|
||||
})
|
||||
}
|
||||
|
||||
pub const SPRITE_KINDS: [EK; 13] = [EK::Worker, EK::Kobold, EK::Spider, EK::Mage, EK::Ogre, EK::Knight, EK::Archer, EK::Priest, EK::Dwarf, EK::Paladin, EK::Jailer, EK::Priestess, EK::Chainbreaker];
|
||||
pub const SPRITE_KINDS: [EK; 17] = [
|
||||
EK::Worker,
|
||||
EK::Kobold,
|
||||
EK::Spider,
|
||||
EK::Mage,
|
||||
EK::Ogre,
|
||||
EK::Knight,
|
||||
EK::Archer,
|
||||
EK::Priest,
|
||||
EK::Dwarf,
|
||||
EK::Paladin,
|
||||
EK::Jailer,
|
||||
EK::Priestess,
|
||||
EK::Chainbreaker,
|
||||
EK::Bat,
|
||||
EK::Gargoyle,
|
||||
EK::Owl,
|
||||
EK::Wyrm,
|
||||
];
|
||||
// width of the sprite texture in pixels
|
||||
pub const SPRITE_TEX_W: f32 = (13 * 12) as f32;
|
||||
pub const SPRITE_TEX_W: f32 = (17 * 12) as f32;
|
||||
pub const SPRITE_TEX_H: f32 = 24.0;
|
||||
|
||||
pub fn sprite_index(k: EK) -> u32 {
|
||||
@@ -1143,6 +1228,7 @@ impl Gfx {
|
||||
Kind::Bridge => T_BRIDGE + (v & 1),
|
||||
Kind::LavaBridge => T_STONEBRIDGE + (v & 1),
|
||||
Kind::Shard => T_HEROFLOOR + v,
|
||||
Kind::Aerie => T_AERIE + v,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+13
-1
@@ -57,6 +57,7 @@ pub enum Kind {
|
||||
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)
|
||||
}
|
||||
|
||||
// options for the map generator (campaign missions)
|
||||
@@ -85,7 +86,7 @@ pub struct River {
|
||||
}
|
||||
|
||||
// order matters for the packed savegame codec: only append new kinds
|
||||
const KINDS: [Kind; 23] = [
|
||||
const KINDS: [Kind; 24] = [
|
||||
Kind::Rock,
|
||||
Kind::Earth,
|
||||
Kind::Gold,
|
||||
@@ -109,6 +110,7 @@ const KINDS: [Kind; 23] = [
|
||||
Kind::Silver,
|
||||
Kind::Ruby,
|
||||
Kind::Diamond,
|
||||
Kind::Aerie,
|
||||
];
|
||||
|
||||
// treasures (veins, treasury content, imp loads): 0 gold, 1 silver, 2 ruby, 3 diamond
|
||||
@@ -188,6 +190,7 @@ impl Tile {
|
||||
| Kind::Bridge
|
||||
| Kind::LavaBridge
|
||||
| Kind::Shard
|
||||
| Kind::Aerie
|
||||
)
|
||||
}
|
||||
// river tiles (open, but nobody can walk there)
|
||||
@@ -620,6 +623,15 @@ impl Map {
|
||||
move |i| if self.t[i].walkable() { Some(1) } else { None }
|
||||
}
|
||||
|
||||
// flying creatures also cross water and lava
|
||||
pub fn fly_cost(&self) -> impl Fn(usize) -> Option<u32> + '_ {
|
||||
move |i| if self.t[i].walkable() || self.t[i].liquid() { Some(1) } else { None }
|
||||
}
|
||||
|
||||
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> + '_ {
|
||||
move |i| {
|
||||
let tl = &self.t[i];
|
||||
|
||||
@@ -84,6 +84,12 @@ pub fn draw_props_solid(g: &Game, cam: &Cam, t: f32) {
|
||||
bed(kind, c, h, l, t);
|
||||
}
|
||||
}
|
||||
Kind::Aerie if tile.bed != 0 => {
|
||||
if let Some(&kind) = u.beds.get(&tile.bed) {
|
||||
bed(kind, c, h, l, t);
|
||||
}
|
||||
}
|
||||
Kind::Aerie if h % 3 == 0 => perch(c, h, l),
|
||||
Kind::Treasury if chest_tile(x, z) => chest(c, h, l, tile.gold as f32 / TREASURY_TILE, tile.res),
|
||||
Kind::Training if (x + z) % 2 == 0 => training(c, h, l, t, u.busy.contains(&i) || u.busy.iter().any(|&b| adj8(b, i))),
|
||||
Kind::Torture if (x + z) % 2 == 1 => torture(c, h, l, t, u.tortured.contains(&i) || u.tortured.iter().any(|&b| adj8(b, i))),
|
||||
@@ -145,6 +151,16 @@ pub fn draw_props_glow(g: &Game, cam: &Cam, t: f32) {
|
||||
}
|
||||
}
|
||||
}
|
||||
Kind::Aerie if tile.bed != 0 => {
|
||||
// the wyrm's nest glows
|
||||
if let Some(EK::Wyrm) = u.beds.get(&tile.bed) {
|
||||
for n in 0..5 {
|
||||
let a = n as f32 * 1.3 + k;
|
||||
let f = 0.6 + 0.4 * (t * 3.0 + a).sin();
|
||||
draw_sphere(c + vec3(a.cos() * 0.2, 0.06, a.sin() * 0.2), 0.06 * f, None, Color::new(1.0, 0.5, 0.1, 0.9));
|
||||
}
|
||||
}
|
||||
}
|
||||
Kind::Lair if tile.bed != 0 => {
|
||||
// the flame priestess sleeps on embers
|
||||
if let Some(EK::Priestess) = u.beds.get(&tile.bed) {
|
||||
@@ -285,6 +301,33 @@ fn bed(kind: EK, c: Vec3, h: u32, l: f32, t: f32) {
|
||||
cube(at(0.0, 0.0, 0.05), vec3(0.6, 0.1, 0.48), rgb(150, 40, 30, l));
|
||||
cube(at(0.0, 0.0, 0.11), vec3(0.4, 0.03, 0.3), rgb(220, 110, 40, l));
|
||||
}
|
||||
EK::Bat => {
|
||||
// roost: a beam between two posts to hang from
|
||||
for side in [-0.32f32, 0.32] {
|
||||
cube(at(side, 0.0, 0.45), vec3(0.06, 0.9, 0.06), rgb(90, 64, 40, l));
|
||||
}
|
||||
cube(at(0.0, 0.0, 0.88), vec3(0.72, 0.06, 0.06), rgb(110, 78, 48, l));
|
||||
}
|
||||
EK::Gargoyle => {
|
||||
// stone plinth to sit on
|
||||
cube(at(0.0, 0.0, 0.2), vec3(0.5, 0.4, 0.5), rgb(110, 110, 118, l));
|
||||
cube(at(0.0, 0.0, 0.42), vec3(0.58, 0.06, 0.58), rgb(130, 130, 138, l));
|
||||
}
|
||||
EK::Owl => {
|
||||
// twig nest on a post
|
||||
cube(at(0.0, 0.0, 0.3), vec3(0.08, 0.6, 0.08), rgb(90, 64, 40, l));
|
||||
for n in 0..6 {
|
||||
let a = n as f32 * 1.05 + rot;
|
||||
cube(c + vec3(a.cos() * 0.16, 0.62, a.sin() * 0.16), vec3(0.16, 0.06, 0.05), rgb(110 + (n * 6) as u8, 80, 44, l));
|
||||
}
|
||||
}
|
||||
EK::Wyrm => {
|
||||
// a big nest of dark rocks (the embers glow in the glow pass)
|
||||
for n in 0..9 {
|
||||
let a = n as f32 * 0.7 + rot;
|
||||
cube(c + vec3(a.cos() * 0.42, 0.09, a.sin() * 0.42), vec3(0.2, 0.18 + (n % 3) as f32 * 0.04, 0.2), rgb(60, 44, 44, l));
|
||||
}
|
||||
}
|
||||
EK::Chainbreaker => {
|
||||
// a hollow of broken rocks and a burst chain
|
||||
for n in 0..7 {
|
||||
@@ -308,6 +351,13 @@ fn bed(kind: EK, c: Vec3, h: u32, l: f32, t: f32) {
|
||||
let _ = t;
|
||||
}
|
||||
|
||||
// an empty perch in the aerie: post with a crossbar
|
||||
fn perch(c: Vec3, h: u32, l: f32) {
|
||||
let dx = ((h >> 4) % 3) as f32 * 0.12 - 0.12;
|
||||
cube(c + vec3(dx, 0.35, 0.0), vec3(0.06, 0.7, 0.06), rgb(96, 68, 42, l));
|
||||
cube(c + vec3(dx, 0.68, 0.0), vec3(0.4, 0.05, 0.05), rgb(110, 78, 48, l));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- training hall
|
||||
|
||||
fn training(c: Vec3, h: u32, l: f32, t: f32, busy: bool) {
|
||||
|
||||
+15
-7
@@ -1088,11 +1088,14 @@ impl R3d {
|
||||
if !e.alive || e.held {
|
||||
continue;
|
||||
}
|
||||
let big = matches!(e.kind, EK::Ogre | EK::Paladin | EK::Jailer | EK::Chainbreaker);
|
||||
let w = if e.kind == EK::Chainbreaker { 1.0 } else if big { 0.8 } else { 0.62 };
|
||||
let w = sprite_size(e.kind);
|
||||
let h = w;
|
||||
let sleeping = matches!(e.job, Job::Sleep(_)) && !e.moving;
|
||||
let mut bob = if e.moving { (e.anim * 12.0).sin().abs() * 0.06 } else { 0.0 };
|
||||
// flying creatures hover and flap (on their perch while sleeping)
|
||||
if flies(e.kind) && !sleeping {
|
||||
bob = 0.45 + (t * 7.0 + e.anim * 3.0).sin() * 0.06;
|
||||
}
|
||||
let mut rot = if e.working && !sleeping { (t * 14.0 + e.anim).sin() * 0.15 } else { 0.0 };
|
||||
// prisoners: lying unconscious, carried above an imp, or shaking in the torture chamber
|
||||
match e.captive {
|
||||
@@ -1157,8 +1160,9 @@ impl R3d {
|
||||
if !e.alive || e.held {
|
||||
continue;
|
||||
}
|
||||
let big = matches!(e.kind, EK::Ogre | EK::Paladin | EK::Jailer | EK::Chainbreaker);
|
||||
let head = proj(vec3(e.x, if big { 0.9 } else { 0.72 }, e.y));
|
||||
let big = matches!(e.kind, EK::Ogre | EK::Paladin | EK::Jailer | EK::Chainbreaker | EK::Wyrm | EK::Gargoyle);
|
||||
let lift = if flies(e.kind) && !(matches!(e.job, Job::Sleep(_)) && !e.moving) { 0.45 } else { 0.0 };
|
||||
let head = proj(vec3(e.x, lift + if big { 0.9 } else { 0.72 }, e.y));
|
||||
// bar width in block characters, depending on the zoom
|
||||
let cols = ((px_per_tile * 0.6 / cw) as usize).clamp(3, 8);
|
||||
if (e.hp < e.max_hp || e.hero) && e.captive == 0 {
|
||||
@@ -1222,9 +1226,9 @@ fn shade_c(r: u8, g: u8, b: u8, f: f32) -> Color {
|
||||
|
||||
// size of a unit's sprite (same as in the billboards)
|
||||
fn sprite_size(k: EK) -> f32 {
|
||||
if k == EK::Chainbreaker {
|
||||
if matches!(k, EK::Chainbreaker | EK::Wyrm) {
|
||||
1.0
|
||||
} else if matches!(k, EK::Ogre | EK::Paladin | EK::Jailer) {
|
||||
} else if matches!(k, EK::Ogre | EK::Paladin | EK::Jailer | EK::Gargoyle) {
|
||||
0.8
|
||||
} else {
|
||||
0.62
|
||||
@@ -1242,7 +1246,10 @@ fn draw_gear(g: &Game, cam: &Cam) {
|
||||
let toward = cam.eye_dir();
|
||||
for e in g.ents.iter().filter(|e| gear_shown(e)) {
|
||||
let w = sprite_size(e.kind);
|
||||
let bob = if e.moving { (e.anim * 12.0).sin().abs() * 0.06 } else { 0.0 };
|
||||
let mut bob = if e.moving { (e.anim * 12.0).sin().abs() * 0.06 } else { 0.0 };
|
||||
if flies(e.kind) {
|
||||
bob = 0.45;
|
||||
}
|
||||
let base = vec3(e.x, bob, e.y) + toward * 0.5;
|
||||
let at = |dx: f32, dy: f32| base + r * (dx * w) + Vec3::Y * (dy * w);
|
||||
let metal = if e.level >= 8 {
|
||||
@@ -1709,6 +1716,7 @@ pub fn minimap_color(tile: &Tile) -> [u8; 4] {
|
||||
Kind::Bridge => [150, 110, 60, 255],
|
||||
Kind::LavaBridge => [130, 120, 115, 255],
|
||||
Kind::Shard => [255, 60, 120, 255],
|
||||
Kind::Aerie => [170, 140, 90, 255],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -7,7 +7,7 @@ use std::collections::BTreeMap;
|
||||
// Rebindable keyboard actions. Esc (menu) and the arrow keys (camera) are fixed.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum Action {
|
||||
Tool(usize), // 0..18, same order as ui::TOOLS
|
||||
Tool(usize), // 0..19, same order as ui::TOOLS
|
||||
CamForward,
|
||||
CamBack,
|
||||
CamLeft,
|
||||
@@ -25,7 +25,7 @@ pub enum Action {
|
||||
}
|
||||
|
||||
pub fn all_actions() -> Vec<Action> {
|
||||
let mut v: Vec<Action> = (0..18).map(Action::Tool).collect();
|
||||
let mut v: Vec<Action> = (0..19).map(Action::Tool).collect();
|
||||
v.extend_from_slice(&[
|
||||
Action::CamForward,
|
||||
Action::CamBack,
|
||||
@@ -70,7 +70,7 @@ impl Action {
|
||||
fn default_key(self) -> KeyCode {
|
||||
use KeyCode::*;
|
||||
match self {
|
||||
Action::Tool(i) => [Key1, Key2, Key3, Key4, Key5, Key6, Key7, Key8, Key9, Key0, X, V, K, J, B, C, T, U][i.min(17)],
|
||||
Action::Tool(i) => [Key1, Key2, Key3, Key4, Key5, Key6, Key7, Key8, Key9, Key0, X, V, K, J, B, C, T, U, Y][i.min(18)],
|
||||
Action::CamForward => W,
|
||||
Action::CamBack => S,
|
||||
Action::CamLeft => A,
|
||||
|
||||
@@ -159,7 +159,7 @@ pub enum Tool {
|
||||
}
|
||||
|
||||
// New tools are appended so that existing key bindings ("tool<i>") keep their meaning
|
||||
pub const TOOLS: [Tool; 18] = [
|
||||
pub const TOOLS: [Tool; 19] = [
|
||||
Tool::Dig,
|
||||
Tool::Hand,
|
||||
Tool::Room(0),
|
||||
@@ -178,6 +178,7 @@ pub const TOOLS: [Tool; 18] = [
|
||||
Tool::Tower(TowerKind::Cannon),
|
||||
Tool::Tower(TowerKind::Magic),
|
||||
Tool::Bridge,
|
||||
Tool::Room(7),
|
||||
];
|
||||
|
||||
|
||||
@@ -198,7 +199,8 @@ pub fn tool_info(t: Tool) -> ToolInfo {
|
||||
Tool::Room(3) => ("training", T_TRAINING, Some(3)),
|
||||
Tool::Room(4) => ("library", T_LIBRARY, Some(4)),
|
||||
Tool::Room(5) => ("torture", T_TORTURE, Some(5)),
|
||||
Tool::Room(_) => ("temple", T_TEMPLE, Some(6)),
|
||||
Tool::Room(6) => ("temple", T_TEMPLE, Some(6)),
|
||||
Tool::Room(_) => ("aerie", T_AERIE, Some(7)),
|
||||
Tool::Tower(TowerKind::Arrow) => ("arrow", I_ARROW, None),
|
||||
Tool::Tower(TowerKind::Cannon) => ("cannon", I_CANNON, None),
|
||||
Tool::Tower(TowerKind::Magic) => ("magic", I_MAGIC, None),
|
||||
@@ -239,7 +241,7 @@ pub fn tool_cost(t: Tool, g: &Game) -> String {
|
||||
pub const CATS: [(&str, &[usize]); 6] = [
|
||||
("cat.dig", &[0]),
|
||||
("cat.hand", &[1]),
|
||||
("cat.rooms", &[2, 3, 4, 5, 6, 12, 13, 17]),
|
||||
("cat.rooms", &[2, 3, 4, 5, 6, 12, 13, 18, 17]),
|
||||
("cat.towers", &[14, 15, 16]),
|
||||
("cat.spells", &[9, 10, 11]),
|
||||
("cat.orders", &[8, 7]),
|
||||
|
||||
Reference in new issue
Block a user