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:
1 parent
50d013bb28
commit
f113e1085c
9 files changed
+372
-32
No files matched your search
+160
-23
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user