From f113e1085cd33de7ef7b3e94019c715c06d45eb5 Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Tue, 3 Nov 2026 08:53:21 +0100 Subject: [PATCH] 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 --- DESIGN.md | 3 +- README.md | 3 +- lang/de.lang | 6 +- lang/en.lang | 6 +- src/game.rs | 183 ++++++++++++++++++++++++++++++++++++++++++++------- src/gfx.rs | 108 +++++++++++++++++++++++++++++- src/main.rs | 13 +++- src/sfx.rs | 2 + src/ui.rs | 80 +++++++++++++++++++++- 9 files changed, 372 insertions(+), 32 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index ac70bcb..bc80721 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -63,7 +63,8 @@ An original dungeon management game (genre: dungeon building & defence) that run - The Soul Core starts surrounded by a **one tile wide ring of treasury** (16 tiles) - Claim the **portal** → creatures: kobold, cave spider, shadow mage, stone ogre (depending on rooms) - **Needs**: sleep, food, wages (payday); unhappy creatures leave -- **Hand**: pick up / drop own units (max. 8) +- **Hand**: left click picks up own units (max. 8) and unconscious heroes on own ground; right click drops them on own ground (prisoners onto a free torture place are locked up); Shift + left click slaps: backhand for own units (15 s +50% speed, −5% HP, a little mood, wakes sleepers), fist for heroes on own ground (15 damage, 1 s stun, 0.5 s cooldown) +- **Pixel-art mouse pointers**: pointing monster hand with the icon of the active tool; grabbing hand / fist for the hand tool; animated backhand and punch; the active tool is highlighted in the toolbar (pulsing frame and arrow) - **Spells** (mana): lightning, heal, summon Pit Imp - **Heroes**: knight, archer, priest, dwarf miner, paladin of light (they dig their way to the Soul Core) diff --git a/README.md b/README.md index 2fc753d..87b5413 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,8 @@ The soundtrack consists of FastTracker 2 modules in `assets/music/`, generated b |---|---| | Esc | Pause menu (save, load, settings) | | Left click / drag | Use the active tool (dig and rooms: drag a rectangle) | -| Right click | Drop a unit from your hand | +| Right click | Drop a unit from your hand (prisoners anywhere on your ground; onto a free torture place = locked up) | +| Shift + left click (hand) | Backhand own units (15 s faster, costs a little HP and mood) / punch heroes on your ground (15 damage, 1 s stun) | | Right drag, WASD, arrows, screen edge | Move the camera | | Middle drag, Q/E, R/F | Rotate / tilt the camera | | Mouse wheel | Zoom | diff --git a/lang/de.lang b/lang/de.lang index 419e541..b322690 100644 --- a/lang/de.lang +++ b/lang/de.lang @@ -140,7 +140,8 @@ ui.pause_hint = P: weiter # ---- Hilfe help.goal = Ziel: Zerstöre das Heldentor und schütze deinen Seelenkern. help.k_left = Linksklick Werkzeug benutzen (Graben/Räume: Rechteck ziehen) -help.k_right = Rechtsklick Einheit aus der Hand absetzen +help.k_right = Rechtsklick Einheit absetzen (Gefangene auch in den Kerker) +help.k_slap = Shift+Links Hand: Rückhand für eigene, Faust für Helden auf eigenem Gebiet 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 @@ -336,3 +337,6 @@ dir.nw = Nordwesten val.auto = Auto ({0}x) set.render_scale = 3D-Auflösung + +hint.pickup_prisoner_where = Gefangene können nur auf eigenem Gebiet aufgehoben werden. +hint.punch_where = Die Hand kann Helden nur auf eigenem Gebiet schlagen. diff --git a/lang/en.lang b/lang/en.lang index db7083a..5bc5800 100644 --- a/lang/en.lang +++ b/lang/en.lang @@ -141,7 +141,8 @@ ui.pause_hint = P: continue # ---- help help.goal = Goal: destroy the Hero Gate and protect your Soul Core. help.k_left = Left click Use tool (dig/rooms: drag a rectangle) -help.k_right = Right click Drop a unit from your hand +help.k_right = Right click Drop a unit (prisoners: also into the torture chamber) +help.k_slap = Shift+Left Hand: backhand own units, punch heroes on your ground 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 @@ -337,3 +338,6 @@ dir.nw = north-west val.auto = Auto ({0}x) set.render_scale = 3D resolution + +hint.pickup_prisoner_where = Prisoners can only be picked up on your own territory. +hint.punch_where = The hand can only punch heroes on your own territory. diff --git a/src/game.rs b/src/game.rs index ea6165d..3a78404 100644 --- a/src/game.rs +++ b/src/game.rs @@ -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, #[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) -> 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) -> 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); + } } + diff --git a/src/gfx.rs b/src/gfx.rs index c1b489d..fb8c605 100644 --- a/src/gfx.rs +++ b/src/gfx.rs @@ -52,6 +52,7 @@ pub const D_ALTAR: u32 = 88; pub struct Gfx { pub atlas: Texture2D, pub sprites: Texture2D, + pub cursors: Texture2D, // mouse pointer frames, 16x16 each (see CURSOR_*) pub font: Texture2D, // DINOBYTE, CP437, 16x16 glyphs of 6x8 px } @@ -746,6 +747,109 @@ fn gen_sprites() -> Image { img } +// ------------------------------------------------------------------ mouse pointers (16x16) + +pub const CURSOR_POINT: u32 = 0; // pointing monster hand (hot spot: finger tip) +pub const CURSOR_OPEN: u32 = 1; // open grabbing hand (hot spot: palm centre) +pub const CURSOR_FIST: u32 = 2; // holding something +pub const CURSOR_BACKHAND: u32 = 3; // slap, with motion streaks +pub const CURSOR_PUNCH: u32 = 4; // punch, with impact star +pub const CURSOR_HOT: [(f32, f32); 5] = [(2.0, 1.0), (7.0, 7.0), (7.0, 7.0), (7.0, 7.0), (7.0, 7.0)]; + +const CUR_POINT: [&str; 16] = [ + "..kk............", + ".kwwk...........", + ".kggk...........", + ".kggk...........", + ".kggkkk.........", + ".kggkggkkk......", + ".kggkggkggkk....", + "kkggggggggggk...", + "kgkgggggggggk...", + "kggggggggggGk...", + ".kgggggggggGk...", + "..kggggggggGk...", + "..kgggggggGk....", + "...kGGGGGGk.....", + "...kkkkkkkk.....", + "................", +]; +const CUR_OPEN: [&str; 16] = [ + "................", + ".kw.kw.kw.......", + ".kgkkgkkgk..kw..", + ".kgkkgkkgk.kgk..", + ".kgkkgkkgkkggk..", + ".kgggggggggggk..", + "kkgggggggggggk..", + "kgkggggggggggk..", + "kggkgggggggGk...", + ".kggggggggggk...", + "..kgggggggGk....", + "...kggggggGk....", + "...kgggggGk.....", + "....kGGGGk......", + "....kkkkkk......", + "................", +]; +const CUR_FIST: [&str; 16] = [ + "................", + "................", + "................", + "...kkkkkkkk.....", + "..kgkgkgkgGk....", + "..kgkgkgkgGk....", + ".kkggggggggGk...", + "kggkgggggggGk...", + "kgggggggggGGk...", + ".kggggggggGk....", + "..kgggggggGk....", + "...kggggggGk....", + "...kGGGGGGk.....", + "....kkkkkk......", + "................", + "................", +]; + +fn cursor_pal(ch: char) -> Option { + Some(match ch { + 'k' => c(20, 16, 18), + 'g' => c(112, 152, 84), + 'G' => c(70, 100, 56), + 'w' => c(236, 230, 208), + 'y' => c(255, 220, 80), + 's' => c(255, 255, 255), + _ => return None, + }) +} + +fn gen_cursors() -> Image { + let mut img = Image::gen_image_color(16 * 5, 16, Color::new(0.0, 0.0, 0.0, 0.0)); + let mut put = |frame: u32, rows: &[&str; 16], mirror: bool| { + for (y, row) in rows.iter().enumerate() { + for (x, ch) in row.chars().enumerate() { + if let Some(col) = cursor_pal(ch) { + let xx = if mirror { 15 - x } else { x }; + img.set_pixel(frame * 16 + xx as u32, y as u32, col); + } + } + } + }; + put(CURSOR_POINT, &CUR_POINT, false); + put(CURSOR_OPEN, &CUR_OPEN, false); + put(CURSOR_FIST, &CUR_FIST, false); + put(CURSOR_BACKHAND, &CUR_OPEN, true); + put(CURSOR_PUNCH, &CUR_FIST, false); + // motion streaks for the backhand, impact star for the punch + for &(x, y) in &[(0, 5), (1, 5), (0, 7), (1, 7), (2, 7), (0, 9), (1, 9)] { + img.set_pixel(CURSOR_BACKHAND * 16 + x, y, c(255, 255, 255)); + } + for &(x, y) in &[(0, 0), (2, 1), (1, 2), (0, 3), (3, 0), (4, 2), (2, 4)] { + img.set_pixel(CURSOR_PUNCH * 16 + x, y, c(255, 220, 80)); + } + img +} + impl Gfx { pub fn new() -> Gfx { let atlas = Texture2D::from_image(&gen_atlas()); @@ -760,7 +864,9 @@ impl Gfx { } let font = Texture2D::from_image(&img); font.set_filter(FilterMode::Nearest); - Gfx { atlas, sprites, font } + let cursors = Texture2D::from_image(&gen_cursors()); + cursors.set_filter(FilterMode::Nearest); + Gfx { atlas, sprites, cursors, font } } pub fn cell(id: u32) -> Rect { diff --git a/src/main.rs b/src/main.rs index 3f093f5..0eef2be 100644 --- a/src/main.rs +++ b/src/main.rs @@ -209,6 +209,7 @@ impl App { #[macroquad::main(conf)] async fn main() { let gfx = Gfx::new(); + show_mouse(false); // the game draws its own pixel-art pointer let mut r3d = R3d::new(); let mut sounds = SoundBank::new(); let mut music = MusicDirector::new(); @@ -506,8 +507,15 @@ async fn main() { match (ui.tool, hover_tile) { (Tool::Dig | Tool::Room(_) | Tool::Sell, Some(t)) => inp.ldown = Some(t), (Tool::Hand, _) => { - let j = r3d.pick_ent(&app.game, &app.cam, mx, my, true); - app.game.pickup(j); + let j = r3d.pick_ent(&app.game, &app.cam, mx, my, false); + if is_key_down(KeyCode::LeftShift) || is_key_down(KeyCode::RightShift) { + let kind = app.game.slap(j); + if kind > 0 { + ui.hand_anim = Some((kind, now)); + } + } else { + app.game.pickup(j); + } } (Tool::Flag, Some((x, z))) => app.game.set_flag(x, z), (Tool::Lightning, _) => app.game.lightning(wx, wy), @@ -640,4 +648,5 @@ fn draw_frame(app: &mut App, gfx: &Gfx, r3d: &mut R3d, ui: &mut Ui, menu: &mut M if !menu.open() || ui.show_help || ui.show_langsel { ui.draw_dialogs(&app.game, gfx); } + ui.draw_cursor(gfx, &app.game, now, blocked); } diff --git a/src/sfx.rs b/src/sfx.rs index 7895381..83207da 100644 --- a/src/sfx.rs +++ b/src/sfx.rs @@ -155,6 +155,8 @@ fn def(s: Sfx, r: f32) -> Vec { seg(Square(0.25), 900.0, 900.0, 0.08, 0.16, 15.0), ], 46), Sfx::Convert => l(&arp(Saw, &[220.0, 262.0, 330.0, 440.0], 0.1, 0.22), 47), + Sfx::Slap => mix(vec![l(&[seg(Noise, 5000.0, 2500.0, 0.06, 0.5, 40.0)], 48), l(&[seg(Square(0.5), 900.0, 500.0, 0.05, 0.12, 30.0)], 49)]), + Sfx::Punch => mix(vec![l(&[seg(Tri, 160.0, 60.0, 0.15, 0.8, 18.0)], 50), l(&[seg(Noise, 3000.0, 800.0, 0.08, 0.4, 30.0)], 51)]), } } diff --git a/src/ui.rs b/src/ui.rs index 62791cc..54a4a4d 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -229,6 +229,7 @@ pub struct Ui { tool_rects: Vec, end_button: Rect, lang_rects: Vec<(Rect, String)>, + pub hand_anim: Option<(u8, f64)>, // 1 = backhand, 2 = punch, start time } fn wrap(s: &str, cols: usize) -> Vec { @@ -274,6 +275,7 @@ impl Ui { tool_rects: Vec::new(), end_button: Rect::new(0.0, 0.0, 0.0, 0.0), lang_rects: Vec::new(), + hand_anim: None, } } @@ -413,8 +415,14 @@ impl Ui { hover_tool = Some(i); } let sel = self.tool == t; - draw_rectangle(r.x, r.y, r.w, r.h, if sel { Color::new(0.35, 0.22, 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) }); - draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0 * px, if sel { FRAME_HI } else { Color::new(0.35, 0.28, 0.2, 1.0) }); + 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 and an arrow in the toolbar frame above it + let pulse = (now * 2.5) as i64 % 2 == 0; + let frame = if sel { if pulse { WHITE } else { FRAME_HI } } else { Color::new(0.35, 0.28, 0.2, 1.0) }; + draw_rectangle_lines(r.x, r.y, r.w, r.h, if sel { 3.0 * px } else { 2.0 * px }, frame); + if sel { + gfx.glyph(0x1F, bx + CW() * 2.5, tb_y, S(), if pulse { WHITE } else { FRAME_HI }); + } let ti = tool_info(t); // icon: the 16 px atlas cell at 2*F, centred on the grid let icon = 32.0 * px; @@ -581,6 +589,7 @@ impl Ui { ("", TXT), ("help.k_left", TXT), ("help.k_right", TXT), + ("help.k_slap", TXT), ("help.k_pan", TXT), ("help.k_rot", TXT), ("help.k_zoom", TXT), @@ -678,6 +687,73 @@ impl Ui { } } +impl Ui { + // Pixel-art mouse pointer: pointing hand with the active tool's icon, or the grabbing + // hand for the hand tool (fist while holding units, animated slap/punch) + pub fn draw_cursor(&self, gfx: &Gfx, g: &Game, now: f64, in_menu: bool) { + let (mx, my) = mouse(); + let px = K(); + let sc = 2.0 * px; // cursor pixel size + let anim = self.hand_anim.filter(|(_, t)| now - t < 0.25); + let hand_tool = self.tool == Tool::Hand && !in_menu; + let (frame, mut ox, mut oy) = match (anim, hand_tool) { + (Some((1, t)), _) => { + // backhand: swings from right to left + let k = ((now - t) / 0.25) as f32; + (CURSOR_BACKHAND, ((1.0 - 2.0 * k) * 6.0).round() * sc, 0.0) + } + (Some((_, t)), _) => { + // punch: short thrust forward and back + let k = ((now - t) / 0.25) as f32; + let d = if k < 0.4 { -k / 0.4 } else { -(1.0 - k) / 0.6 }; + (CURSOR_PUNCH, (d * 3.0).round() * sc, (d * 3.0).round() * sc) + } + (None, true) if !g.hand.is_empty() => (CURSOR_FIST, 0.0, 0.0), + (None, true) => (CURSOR_OPEN, 0.0, 0.0), + _ => (CURSOR_POINT, 0.0, 0.0), + }; + // idle: the pointing hand wiggles by one pixel + if frame == CURSOR_POINT && (now * 2.0) as i64 % 2 == 0 { + oy += sc; + } + if frame == CURSOR_OPEN && (now * 1.5) as i64 % 2 == 0 { + ox += sc; + } + let (hx, hy) = CURSOR_HOT[frame as usize]; + let x = (mx - hx * sc + ox).floor(); + let y = (my - hy * sc + oy).floor(); + // units in the hand dangle below the fist + if hand_tool && !g.hand.is_empty() { + if let Some(e) = g.hand.last().and_then(|id| g.ents.iter().find(|e| e.id == *id)) { + let s = 10.0 * sc; + draw_texture_ex( + &gfx.sprites, + x + 3.0 * sc, + y + 12.0 * sc, + WHITE, + DrawTextureParams { dest_size: Some(vec2(s, s)), source: Some(Gfx::sprite_rect(e.kind, false)), rotation: 0.3, ..Default::default() }, + ); + } + gfx.text_sh(&g.hand.len().to_string(), x + 15.0 * sc, y + 10.0 * sc, S(), FRAME_HI); + } + draw_texture_ex( + &gfx.cursors, + x, + y, + WHITE, + DrawTextureParams { dest_size: Some(vec2(16.0 * sc, 16.0 * sc)), source: Some(Rect::new(frame as f32 * 16.0, 0.0, 16.0, 16.0)), ..Default::default() }, + ); + // small icon of the active action next to the pointing hand + if frame == CURSOR_POINT && !in_menu { + let icon = 16.0 * px; + let (ix, iy) = (x + 11.0 * sc, y + 11.0 * sc); + draw_rectangle(ix - px, iy - px, icon + 2.0 * px, icon + 2.0 * px, Color::new(0.05, 0.03, 0.04, 0.85)); + draw_rectangle_lines(ix - px, iy - px, icon + 2.0 * px, icon + 2.0 * px, px, FRAME); + gfx.draw_cell(tool_info(self.tool).icon, ix, iy, icon, icon, WHITE); + } + } +} + // Info text for the unit or tile under the mouse (first line = title) pub fn hover_info(g: &Game, ent: Option, tile: Option<(i32, i32)>) -> Vec<(String, Color)> { let mut out = Vec::new();