Orders: attack (fighting creatures nearby go for an enemy or enemy tower) and guard (up to three creatures hold a room for two minutes); right click with an order tool cancels
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b3583f5e7e
commit
0606c1f994
9 files changed
+230
-6
No files matched your search
+1
-1
@@ -10,7 +10,7 @@ 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, aerie, steel
|
||||
pub const TOOL_UNLOCK: [usize; 45] = [0, 0, 0, 0, 0, 2, 5, 0, 0, 3, 5, 0, 8, 12, 3, 7, 12, 1, 8, 17, 0, 7, 7, 10, 12, 12, 7, 16, 16, 7, 10, 16, 9, 13, 6, 10, 14, 17, 19, 22, 9, 13, 6, 11, 18];
|
||||
pub const TOOL_UNLOCK: [usize; 47] = [0, 0, 0, 0, 0, 2, 5, 0, 0, 3, 5, 0, 8, 12, 3, 7, 12, 1, 8, 17, 0, 7, 7, 10, 12, 12, 7, 16, 16, 7, 10, 16, 9, 13, 6, 10, 14, 17, 19, 22, 9, 13, 6, 11, 18, 0, 0];
|
||||
|
||||
pub struct FortDef {
|
||||
pub at: (f32, f32), // centre as a fraction of the map size
|
||||
|
||||
+3
-2
@@ -1909,7 +1909,7 @@ impl Game {
|
||||
self.sfx(Sfx::DigDone, x as f32 + 0.5, y as f32 + 0.5);
|
||||
}
|
||||
|
||||
fn unreserve(&mut self, me: u32) {
|
||||
pub(crate) fn unreserve(&mut self, me: u32) {
|
||||
for t in self.map.t.iter_mut() {
|
||||
if t.reserved == me {
|
||||
t.reserved = 0;
|
||||
@@ -3200,8 +3200,9 @@ impl Game {
|
||||
}
|
||||
}
|
||||
Job::Guard(t) => {
|
||||
// a guard post, or a guard order in any room
|
||||
e.timer -= dt;
|
||||
if cur != t || self.map.t[t].kind != Kind::GuardPost || e.timer <= 0.0 {
|
||||
if cur != t || !self.map.t[t].walkable() || self.map.t[t].owner != Owner::Player || e.timer <= 0.0 {
|
||||
self.unreserve(me);
|
||||
e.job = Job::None;
|
||||
}
|
||||
|
||||
+20
@@ -78,6 +78,7 @@ pub const T_GRAVEYARD: u32 = 170;
|
||||
pub const T_GUARDPOST: u32 = 174;
|
||||
pub const T_ARENA: u32 = 178;
|
||||
pub const T_ALTAR: u32 = 182;
|
||||
pub const I_ORDERS: u32 = 186; // attack, guard
|
||||
|
||||
pub struct Gfx {
|
||||
pub atlas: Texture2D,
|
||||
@@ -996,6 +997,25 @@ fn gen_atlas() -> Image {
|
||||
for (k, f) in spells.iter().enumerate() {
|
||||
cv.cell(I_SPELLS + k as u32, |x, y| f(x, y));
|
||||
}
|
||||
// order icons: crossed blades (attack), a shield (guard)
|
||||
cv.cell(I_ORDERS, |x, y| {
|
||||
let blade = (x - y).abs() <= 1 || (x + y - 15).abs() <= 1;
|
||||
if blade && (2..=13).contains(&x) && (2..=13).contains(&y) {
|
||||
Some(if (x - y).abs() == 0 || (x + y - 15) == 0 { c(240, 240, 250) } else { c(170, 170, 185) })
|
||||
} else if (y == 12 || y == 13) && (x == 2 || x == 3 || x == 12 || x == 13) {
|
||||
Some(c(140, 90, 40))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
cv.cell(I_ORDERS + 1, |x, y| {
|
||||
let w = if y < 9 { 6 } else { 6 - (y - 8) };
|
||||
if (2..=14).contains(&y) && (x - 8).abs() <= w.max(0) {
|
||||
Some(if (x - 8).abs() == w.max(0) || y == 2 { c(220, 190, 80) } else if x == 8 || y == 6 { c(220, 190, 80) } else { c(150, 30, 36) })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
// aerie: straw and twigs on stone, a few feathers
|
||||
for v in 0..4u32 {
|
||||
cv.cell(T_AERIE + v, |x, y| {
|
||||
|
||||
@@ -14,6 +14,7 @@ mod map;
|
||||
mod menu;
|
||||
mod personality;
|
||||
mod platform;
|
||||
mod orders;
|
||||
mod particles;
|
||||
mod props;
|
||||
mod research;
|
||||
@@ -123,6 +124,11 @@ fn world_action(app: &mut App, ui: &mut Ui, r3d: &R3d, inp: &mut Input, mx: f32,
|
||||
(Tool::Tower(k), Some((x, z))) => app.game.build_tower(k, x, z),
|
||||
(Tool::Trap(k), Some((x, z))) => app.game.place_trap(k, x, z),
|
||||
(Tool::Spell(sp), _) => app.game.cast(sp, wx, wy),
|
||||
(Tool::Order(0), _) => {
|
||||
let j = r3d.pick_ent(&app.game, &app.cam, mx, my, false);
|
||||
app.game.order_attack(j, wx, wy);
|
||||
}
|
||||
(Tool::Order(_), Some((x, z))) => app.game.order_guard(x, z),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -720,6 +726,8 @@ async fn main() {
|
||||
if !inp.rdrag && !over_ui && app.game.state == 0 {
|
||||
if inp.ldown.is_some() {
|
||||
inp.ldown = None;
|
||||
} else if app.game.hand.is_empty() && matches!(ui.tool, Tool::Order(_)) {
|
||||
app.game.cancel_orders();
|
||||
} else {
|
||||
app.game.drop(wx, wy);
|
||||
}
|
||||
@@ -1055,6 +1063,7 @@ fn draw_frame(app: &mut App, gfx: &Gfx, r3d: &mut R3d, ui: &mut Ui, menu: &mut M
|
||||
Tool::Tower(_) => (Color::new(1.0, 0.6, 0.25, 1.0), None),
|
||||
Tool::Trap(_) => (Color::new(1.0, 0.4, 0.3, 1.0), None),
|
||||
Tool::Spell(sp) => (Color::new(0.8, 0.55, 1.0, 1.0), Some((ground.x, ground.y, sp.radius().max(0.6)))),
|
||||
Tool::Order(_) => (Color::new(1.0, 0.3, 0.3, 1.0), None),
|
||||
};
|
||||
let ov = Overlay {
|
||||
hover: if over_ui { None } else { hover_tile.filter(|&(x, z)| inb(x, z)) },
|
||||
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
// Orders from the hand: "attack" sends the fighting creatures nearby at an enemy unit or tower,
|
||||
// "guard" posts a few creatures in a room. Right click with an order tool cancels the orders.
|
||||
|
||||
use crate::game::*;
|
||||
use crate::map::*;
|
||||
|
||||
const ORDER_RADIUS: f32 = 18.0;
|
||||
const GUARDS: usize = 3;
|
||||
|
||||
fn ready(e: &Ent) -> bool {
|
||||
e.alive && !e.held && e.creature() && !matches!(e.job, Job::Leave | Job::Register | Job::MakeBed(_)) && e.hp > e.max_hp * 0.25
|
||||
}
|
||||
|
||||
impl Game {
|
||||
// attack: the enemy (unit, or tower) at (wx, wy)
|
||||
pub fn order_attack(&mut self, picked: Option<usize>, wx: f32, wy: f32) {
|
||||
let unit = picked.filter(|&j| j < self.ents.len() && self.ents[j].alive && self.ents[j].side() != 0 && self.ents[j].captive == 0).or_else(|| {
|
||||
self.ents.iter().position(|e| e.alive && !e.held && e.side() != 0 && e.captive == 0 && !e.chained && (e.x - wx).powi(2) + (e.y - wy).powi(2) < 1.5 * 1.5)
|
||||
});
|
||||
let tower = if unit.is_none() {
|
||||
self.towers.iter().find(|t| t.hero && (t.pos().0 - wx).powi(2) + (t.pos().1 - wy).powi(2) < 1.2 * 1.2).map(|t| (t.id, t.pos()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let (tx, ty) = match (unit, tower) {
|
||||
(Some(j), _) => (self.ents[j].x, self.ents[j].y),
|
||||
(None, Some((_, p))) => p,
|
||||
_ => {
|
||||
self.hint(31, "hint.order_attack");
|
||||
self.sfx_ui(Sfx::Error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let mut n = 0;
|
||||
for e in self.ents.iter_mut().filter(|e| ready(e) && (e.x - tx).powi(2) + (e.y - ty).powi(2) < ORDER_RADIUS * ORDER_RADIUS) {
|
||||
e.job = match (unit, tower) {
|
||||
(Some(_), _) => Job::Attack(0),
|
||||
(None, Some((id, _))) => Job::AttackTower(id),
|
||||
_ => continue,
|
||||
};
|
||||
e.path.clear();
|
||||
e.repath = 0.0;
|
||||
n += 1;
|
||||
}
|
||||
if let Some(j) = unit {
|
||||
let id = self.ents[j].id;
|
||||
for e in self.ents.iter_mut() {
|
||||
if e.job == Job::Attack(0) {
|
||||
e.job = Job::Attack(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if n == 0 {
|
||||
self.hint(31, "hint.order_nobody");
|
||||
return;
|
||||
}
|
||||
self.fx(7, tx, ty, 0.0, 0.0, 0.6, 0xff3040ff);
|
||||
self.sfx_ui(Sfx::FlagSet);
|
||||
self.msg("msg.order_attack", vec![n.to_string()]);
|
||||
}
|
||||
|
||||
// guard: a few creatures take positions in the room at (x, y)
|
||||
pub fn order_guard(&mut self, x: i32, y: i32) {
|
||||
if !inb(x, y) {
|
||||
return;
|
||||
}
|
||||
let i = idx(x, y);
|
||||
let kind = self.map.t[i].kind;
|
||||
if self.map.t[i].owner != Owner::Player || room_index(kind).is_none() && kind != Kind::Floor {
|
||||
self.hint(31, "hint.order_guard");
|
||||
self.sfx_ui(Sfx::Error);
|
||||
return;
|
||||
}
|
||||
// the spots: tiles of the same room (or floor) around the click
|
||||
let mut spots: Vec<usize> = Vec::new();
|
||||
for dy in -3..=3 {
|
||||
for dx in -3..=3 {
|
||||
if let Some(t) = self.map.get(x + dx, y + dy) {
|
||||
let j = idx(x + dx, y + dy);
|
||||
if t.kind == kind && t.owner == Owner::Player && t.reserved == 0 && !self.ents.iter().any(|e| e.job == Job::Guard(j)) {
|
||||
spots.push(j);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
spots.sort_by_key(|&j| {
|
||||
let (sx, sy) = xy(j);
|
||||
(sx - x).pow(2) + (sy - y).pow(2)
|
||||
});
|
||||
let (fx, fy) = (x as f32 + 0.5, y as f32 + 0.5);
|
||||
let mut cands: Vec<usize> = (0..self.ents.len()).filter(|&j| ready(&self.ents[j]) && !matches!(self.ents[j].job, Job::Attack(_) | Job::Guard(_))).collect();
|
||||
cands.sort_by(|&a, &b| {
|
||||
let da = (self.ents[a].x - fx).powi(2) + (self.ents[a].y - fy).powi(2);
|
||||
let db = (self.ents[b].x - fx).powi(2) + (self.ents[b].y - fy).powi(2);
|
||||
da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
let mut n = 0;
|
||||
for (&j, &spot) in cands.iter().zip(spots.iter()).take(GUARDS) {
|
||||
let cur = self.ents[j].tile();
|
||||
let Some(p) = self.map.path_to(cur, spot) else { continue };
|
||||
let me = self.ents[j].id + 1;
|
||||
self.unreserve(me);
|
||||
self.map.t[spot].reserved = me;
|
||||
let e = &mut self.ents[j];
|
||||
e.job = Job::Guard(spot);
|
||||
e.path = p;
|
||||
e.timer = 120.0;
|
||||
n += 1;
|
||||
}
|
||||
if n == 0 {
|
||||
self.hint(31, "hint.order_nobody");
|
||||
return;
|
||||
}
|
||||
self.fx(8, fx, fy, 0.0, 0.0, 0.6, 0xff40d0ff);
|
||||
self.sfx_ui(Sfx::FlagSet);
|
||||
self.msg("msg.order_guard", vec![n.to_string()]);
|
||||
}
|
||||
|
||||
// right click with an order tool: everybody stands down
|
||||
pub fn cancel_orders(&mut self) {
|
||||
let mut n = 0;
|
||||
for k in 0..self.ents.len() {
|
||||
if matches!(self.ents[k].job, Job::Guard(_)) && self.ents[k].creature() {
|
||||
let me = self.ents[k].id + 1;
|
||||
self.unreserve(me);
|
||||
self.ents[k].job = Job::None;
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
if n > 0 {
|
||||
self.msg("msg.orders_cancelled", vec![]);
|
||||
self.sfx_ui(Sfx::Click);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn creatures_follow_attack_and_guard_orders() {
|
||||
let mut g = Game::new(14, 64, 64);
|
||||
let (cx, cy) = g.map.core;
|
||||
let ids: Vec<u32> = (0..4).map(|k| g.spawn(EK::Ogre, false, cx as f32 - 0.5, cy as f32 + 0.5 + k as f32 * 0.3, 2)).collect();
|
||||
let knight = g.spawn(EK::Knight, true, cx as f32 + 2.5, cy as f32 - 1.5, 1);
|
||||
let j = g.find(knight).unwrap();
|
||||
g.order_attack(Some(j), 0.0, 0.0);
|
||||
assert!(ids.iter().all(|&id| g.ents[g.find(id).unwrap()].job == Job::Attack(knight)));
|
||||
// guard the treasury ring around the core
|
||||
for &id in &ids {
|
||||
let j = g.find(id).unwrap();
|
||||
g.ents[j].job = Job::None;
|
||||
}
|
||||
g.order_guard(cx - 1, cy - 1);
|
||||
let guards = g.ents.iter().filter(|e| matches!(e.job, Job::Guard(_))).count();
|
||||
assert_eq!(guards, 3);
|
||||
g.cancel_orders();
|
||||
assert_eq!(g.ents.iter().filter(|e| matches!(e.job, Job::Guard(_))).count(), 0);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -43,7 +43,7 @@ pub const RESEARCH: &[(usize, f32)] = &[
|
||||
];
|
||||
|
||||
// known from the start in free play
|
||||
pub const BASE_TOOLS: &[usize] = &[0, 1, 2, 3, 4, 6, 7, 8, 9, 14, 17, 20];
|
||||
pub const BASE_TOOLS: &[usize] = &[0, 1, 2, 3, 4, 6, 7, 8, 9, 14, 17, 20, 45, 46];
|
||||
|
||||
// campaign: what the next missions bring may be researched early
|
||||
const LOOKAHEAD: usize = 4;
|
||||
|
||||
@@ -159,10 +159,11 @@ pub enum Tool {
|
||||
Steel,
|
||||
Trap(crate::traps::TrapKind),
|
||||
Spell(crate::spells::Spell),
|
||||
Order(u8), // 0 attack, 1 guard
|
||||
}
|
||||
|
||||
// New tools are appended so that existing key bindings ("tool<i>") keep their meaning
|
||||
pub const TOOLS: [Tool; 45] = [
|
||||
pub const TOOLS: [Tool; 47] = [
|
||||
Tool::Dig,
|
||||
Tool::Hand,
|
||||
Tool::Room(0),
|
||||
@@ -208,6 +209,8 @@ pub const TOOLS: [Tool; 45] = [
|
||||
Tool::Room(11),
|
||||
Tool::Room(12),
|
||||
Tool::Room(13),
|
||||
Tool::Order(0),
|
||||
Tool::Order(1),
|
||||
];
|
||||
|
||||
|
||||
@@ -237,6 +240,8 @@ pub fn tool_info(t: Tool) -> ToolInfo {
|
||||
Tool::Room(12) => ("arena", T_ARENA, Some(12)),
|
||||
Tool::Room(_) => ("altar", T_ALTAR, Some(13)),
|
||||
Tool::Trap(k) => (k.id(), I_TRAPS + crate::traps::TRAP_KINDS.iter().position(|&t| t == k).unwrap_or(0) as u32, None),
|
||||
Tool::Order(0) => ("order_attack", I_ORDERS, None),
|
||||
Tool::Order(_) => ("order_guard", I_ORDERS + 1, None),
|
||||
Tool::Spell(sp) => (sp.id(), I_SPELLS + crate::spells::SPELLS.iter().position(|&t| t == sp).unwrap_or(0) as u32, None),
|
||||
Tool::Tower(TowerKind::Arrow) => ("arrow", I_ARROW, None),
|
||||
Tool::Tower(TowerKind::Cannon) => ("cannon", I_CANNON, None),
|
||||
@@ -313,7 +318,7 @@ pub const CATS: [(&str, &[usize]); 7] = [
|
||||
("cat.towers", &[14, 15, 16, 20]),
|
||||
("cat.traps", &[22, 23, 24, 25, 26, 27, 28, 29, 30, 31]),
|
||||
("cat.spells", &[9, 10, 11, 32, 33, 34, 35, 36, 37, 38, 39]),
|
||||
("cat.orders", &[8, 7, 19]),
|
||||
("cat.orders", &[8, 45, 46, 7, 19]),
|
||||
];
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||
|
||||
Reference in new issue
Block a user