Pixel-art mouse pointers and hand actions

- pointing monster hand with the active tool's icon, grabbing hand / fist, animated backhand and punch
- active tool highlighted in the toolbar (pulsing frame, arrow)
- hand: pick up unconscious heroes on own ground, drop prisoners anywhere on own ground
  (onto a free torture place = locked up), Shift+click backhand (speed-up) / punch (stun)
- fix: hint slots overflowed with the new hints; hint times are no longer saved
- test for the hand rules

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-03 08:53:21 +01:00
1 parent 50d013bb28
commit f113e1085c
9 files changed
+372 -32

No files matched your search

+160 -23
View File
@@ -47,14 +47,16 @@ pub enum Sfx {
Magic,
Capture,
Convert,
Slap,
Punch,
}
pub const SFX_ALL: [Sfx; 34] = [
pub const SFX_ALL: [Sfx; 36] = [
Sfx::Dig, Sfx::GoldDig, Sfx::DigDone, Sfx::Claim, Sfx::Deposit, Sfx::Build, Sfx::Sell, Sfx::MeleeHit,
Sfx::Shoot, Sfx::Death, Sfx::HeroDeath, Sfx::Lightning, Sfx::Heal, Sfx::Summon, Sfx::PortalArrive,
Sfx::WaveAlarm, Sfx::WaveStart, Sfx::CoreHit, Sfx::GateHit, Sfx::GateDefenders, Sfx::LevelUp,
Sfx::Pickup, Sfx::Drop, Sfx::Payday, Sfx::FlagSet, Sfx::Victory, Sfx::Defeat, Sfx::Click, Sfx::Error,
Sfx::Reinforce, Sfx::Cannon, Sfx::Magic, Sfx::Capture, Sfx::Convert,
Sfx::Reinforce, Sfx::Cannon, Sfx::Magic, Sfx::Capture, Sfx::Convert, Sfx::Slap, Sfx::Punch,
];
impl Sfx {
@@ -188,6 +190,10 @@ pub struct Ent {
pub skill_cd: f32, // special abilities (jailer pull, priestess heal)
#[serde(default)]
pub waypoint: Option<(i32, i32)>, // heroes: entry point into the player's realm
#[serde(default)]
pub boost: f32, // seconds of "slapped" speed-up left
#[serde(default)]
pub stun: f32, // seconds of stun left (punched)
pub guard: bool, // stays at its home (gate defenders, treasure guards)
#[serde(default)]
pub home: Option<(f32, f32)>, // guard post
@@ -229,6 +235,8 @@ impl Ent {
torture_t: 0.0,
skill_cd: 0.0,
waypoint: None,
boost: 0.0,
stun: 0.0,
guard: false,
home: None,
}
@@ -294,12 +302,19 @@ pub struct Game {
#[serde(default)]
pub towers: Vec<Tower>,
#[serde(default)]
pub punch_ready: f32, // game time when the hand can punch again
#[serde(default)]
pub entry_history: Vec<(i32, i32)>, // entry points of recent waves
#[serde(default)]
pub planned_entries: Vec<(i32, i32)>, // entry points of the coming/current wave (minimap)
#[serde(skip)]
pub hints: bool, // show hint messages (setting)
hint_t: [f32; 16],
#[serde(skip, default = "hint_times")]
hint_t: [f32; 32], // last time each hint slot was shown
}
fn hint_times() -> [f32; 32] {
[-1000.0; 32]
}
// torch on a wall face: wall tile + direction (index into DIR4) towards the open side
@@ -380,10 +395,11 @@ impl Game {
last_wave_value: 0.0,
torches: Vec::new(),
towers: Vec::new(),
punch_ready: 0.0,
entry_history: Vec::new(),
planned_entries: Vec::new(),
hints: true,
hint_t: [-1000.0; 16],
hint_t: hint_times(),
};
let (cx, cy) = g.map.core;
// the soul core starts surrounded by a one tile wide ring of treasury
@@ -906,6 +922,14 @@ impl Game {
e.flash = (e.flash - dt).max(0.0);
e.slow = (e.slow - dt).max(0.0);
e.skill_cd -= dt;
e.boost = (e.boost - dt).max(0.0);
if e.stun > 0.0 {
e.stun -= dt;
e.moving = false;
return;
}
// slapped units work and move faster
let dt = if e.boost > 0.0 { dt * 1.5 } else { dt };
e.cd -= dt;
e.think -= dt;
e.repath -= dt;
@@ -2662,57 +2686,130 @@ impl Game {
true
}
fn on_own_ground(&self, x: f32, y: f32) -> bool {
self.map.get(x as i32, y as i32).map_or(false, |t| t.owner == Owner::Player)
}
// Left click with the hand: own units anywhere, unconscious heroes only on own ground
pub fn pickup(&mut self, target: Option<usize>) -> bool {
if self.hand.len() >= 8 {
self.hint(8, "hint.hand_full");
return false;
}
if let Some(j) = target.filter(|&j| j < self.ents.len() && self.ents[j].alive && !self.ents[j].held && !self.ents[j].hero) {
let id = self.ents[j].id;
let me = id + 1;
self.unreserve(me);
for o in self.ents.iter_mut() {
if o.carrier == id && o.captive != 0 {
o.captive = 1;
o.carrier = 0;
}
let Some(j) = target.filter(|&j| j < self.ents.len() && self.ents[j].alive && !self.ents[j].held) else { return false };
let e = &self.ents[j];
if e.hero {
if e.captive != 1 {
return false;
}
if !self.on_own_ground(e.x, e.y) {
self.hint(18, "hint.pickup_prisoner_where");
self.sfx_ui(Sfx::Error);
return false;
}
let e = &mut self.ents[j];
e.held = true;
e.job = Job::None;
e.path.clear();
self.hand.push(id);
self.sfx_ui(Sfx::Pickup);
return true;
}
false
let id = self.ents[j].id;
let me = id + 1;
self.unreserve(me);
for o in self.ents.iter_mut() {
if o.carrier == id && o.captive != 0 {
o.captive = 1;
o.carrier = 0;
}
}
let e = &mut self.ents[j];
e.held = true;
e.carrier = 0;
e.job = Job::None;
e.path.clear();
self.hand.push(id);
self.sfx_ui(Sfx::Pickup);
true
}
// Right click: drop the last picked unit on own walkable ground. Prisoners dropped on a
// free torture place are locked up at once.
pub fn drop(&mut self, wx: f32, wy: f32) -> bool {
let Some(&id) = self.hand.last() else { return false };
let (x, y) = (wx as i32, wy as i32);
if !inb(x, y) {
return false;
}
let t = self.map.t[idx(x, y)];
let i = idx(x, y);
let t = self.map.t[i];
if !t.walkable() || t.owner != Owner::Player {
self.hint(9, "hint.drop_where");
self.sfx_ui(Sfx::Error);
return false;
}
self.hand.pop();
if let Some(e) = self.ents.iter_mut().find(|e| e.id == id) {
let imprison = t.kind == Kind::Torture && t.bed == 0;
if let Some(k) = self.ents.iter().position(|e| e.id == id) {
let e = &mut self.ents[k];
e.held = false;
e.x = wx;
e.y = wy;
e.job = Job::None;
e.think = 0.0;
e.path.clear();
if e.hero {
e.carrier = 0;
e.torture_t = 0.0;
if imprison {
e.captive = 3;
e.hp = e.max_hp;
e.x = x as f32 + 0.5;
e.y = y as f32 + 0.5;
self.map.t[i].bed = id + 1;
self.sfx(Sfx::Capture, wx, wy);
} else {
e.captive = 1;
}
}
}
self.fx(3, wx, wy, 0.0, 0.0, 0.4, 0xff707090);
self.sfx(Sfx::Drop, wx, wy);
true
}
// Shift + left click: backhand for own units (speed-up), fist for heroes on own ground.
// Returns 1 = backhand, 2 = punch, 0 = nothing.
pub fn slap(&mut self, target: Option<usize>) -> u8 {
let Some(j) = target.filter(|&j| j < self.ents.len() && self.ents[j].alive && !self.ents[j].held) else { return 0 };
let (x, y) = (self.ents[j].x, self.ents[j].y);
if !self.ents[j].hero {
let e = &mut self.ents[j];
e.boost = 15.0;
e.hp = (e.hp - e.max_hp * 0.05).max(1.0);
e.happy -= 0.04;
e.flash = 0.15;
if matches!(e.job, Job::Sleep(_)) {
e.job = Job::None; // woken up
}
self.fx(2, x, y - 0.3, 0.0, 0.0, 0.3, 0xffffffff);
self.sfx(Sfx::Slap, x, y);
return 1;
}
if self.ents[j].captive != 0 {
return 0;
}
if !self.on_own_ground(x, y) {
self.hint(19, "hint.punch_where");
self.sfx_ui(Sfx::Error);
return 0;
}
if self.time < self.punch_ready {
return 0;
}
self.punch_ready = self.time + 0.5;
self.ents[j].stun = 1.0;
self.fx(2, x, y - 0.3, 0.0, 0.0, 0.35, 0xff40e0ff);
self.fx(7, x, y, 0.0, 0.0, 0.4, 0xff40e0ff);
self.sfx(Sfx::Punch, x, y);
self.tower_damage(j, 15.0);
2
}
pub fn lightning(&mut self, wx: f32, wy: f32) {
const COST: f32 = 50.0;
if self.mana < COST {
@@ -3134,4 +3231,44 @@ mod tests {
assert_eq!(ring, 16);
assert_eq!(g.counts[0], 16);
}
#[test]
fn hand_rules() {
let mut g = Game::new(4, 64, 64);
g.gold = 5000.0;
let (cx, cy) = g.map.core;
// backhand on an own imp: faster for 15 s, costs a little HP
let imp = g.ents.iter().position(|e| e.kind == EK::Worker).unwrap();
let hp = g.ents[imp].hp;
assert_eq!(g.slap(Some(imp)), 1);
assert_eq!(g.ents[imp].boost, 15.0);
assert!(g.ents[imp].hp < hp);
// fist: only on own ground
let far = g.spawn(EK::Knight, true, 40.5, 40.5, 1);
let jf = g.ents.iter().position(|e| e.id == far).unwrap();
if g.map.t[idx(40, 40)].owner != Owner::Player {
assert_eq!(g.slap(Some(jf)), 0);
}
let near = g.spawn(EK::Knight, true, (cx - 2) as f32 + 0.5, (cy - 2) as f32 + 0.5, 1);
let jn = g.ents.iter().position(|e| e.id == near).unwrap();
let hp = g.ents[jn].hp;
assert_eq!(g.slap(Some(jn)), 2);
assert!(g.ents[jn].hp < hp && g.ents[jn].stun > 0.0);
assert_eq!(g.slap(Some(jn)), 0, "cooldown");
// prisoners: pick up on own ground, drop onto a torture place = locked up
g.build_rect(5, cx - 3, cy - 3, cx - 2, cy + 1);
let jn = g.ents.iter().position(|e| e.id == near).unwrap();
g.kill(jn);
assert_eq!(g.ents[jn].captive, 1);
assert!(g.pickup(Some(jn)));
let t = (cx - 3, cy + 1);
assert!(g.drop(t.0 as f32 + 0.5, t.1 as f32 + 0.5));
let e = g.ents.iter().find(|e| e.id == near).unwrap();
assert_eq!(e.captive, 3);
// own units cannot be dropped outside own ground
let imp = g.ents.iter().position(|e| e.kind == EK::Worker).unwrap();
assert!(g.pickup(Some(imp)));
assert!(!g.drop(40.5, 40.5) || g.map.t[idx(40, 40)].owner == Owner::Player);
}
}