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
+358 -18

No files matched your search

+2 -1
View File
@@ -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) - 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) - Claim the **portal** → creatures: kobold, cave spider, shadow mage, stone ogre (depending on rooms)
- **Needs**: sleep, food, wages (payday); unhappy creatures leave - **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 - **Spells** (mana): lightning, heal, summon Pit Imp
- **Heroes**: knight, archer, priest, dwarf miner, paladin of light (they dig their way to the Soul Core) - **Heroes**: knight, archer, priest, dwarf miner, paladin of light (they dig their way to the Soul Core)
+2 -1
View File
@@ -51,7 +51,8 @@ The soundtrack consists of FastTracker 2 modules in `assets/music/`, generated b
|---|---| |---|---|
| Esc | Pause menu (save, load, settings) | | Esc | Pause menu (save, load, settings) |
| Left click / drag | Use the active tool (dig and rooms: drag a rectangle) | | 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 | | Right drag, WASD, arrows, screen edge | Move the camera |
| Middle drag, Q/E, R/F | Rotate / tilt the camera | | Middle drag, Q/E, R/F | Rotate / tilt the camera |
| Mouse wheel | Zoom | | Mouse wheel | Zoom |
+5 -1
View File
@@ -140,7 +140,8 @@ ui.pause_hint = P: weiter
# ---- Hilfe # ---- Hilfe
help.goal = Ziel: Zerstöre das Heldentor und schütze deinen Seelenkern. 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_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_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_rot = Mitte ziehen Kamera drehen und neigen (auch Q/E, R/F)
help.k_zoom = Mausrad Zoomen help.k_zoom = Mausrad Zoomen
@@ -336,3 +337,6 @@ dir.nw = Nordwesten
val.auto = Auto ({0}x) val.auto = Auto ({0}x)
set.render_scale = 3D-Auflösung 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.
+5 -1
View File
@@ -141,7 +141,8 @@ ui.pause_hint = P: continue
# ---- help # ---- help
help.goal = Goal: destroy the Hero Gate and protect your Soul Core. 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_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_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_rot = Middle drag Rotate and tilt the camera (also Q/E, R/F)
help.k_zoom = Mouse wheel Zoom help.k_zoom = Mouse wheel Zoom
@@ -337,3 +338,6 @@ dir.nw = north-west
val.auto = Auto ({0}x) val.auto = Auto ({0}x)
set.render_scale = 3D resolution 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.
+147 -10
View File
@@ -47,14 +47,16 @@ pub enum Sfx {
Magic, Magic,
Capture, Capture,
Convert, 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::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::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::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::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 { impl Sfx {
@@ -188,6 +190,10 @@ pub struct Ent {
pub skill_cd: f32, // special abilities (jailer pull, priestess heal) pub skill_cd: f32, // special abilities (jailer pull, priestess heal)
#[serde(default)] #[serde(default)]
pub waypoint: Option<(i32, i32)>, // heroes: entry point into the player's realm 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) pub guard: bool, // stays at its home (gate defenders, treasure guards)
#[serde(default)] #[serde(default)]
pub home: Option<(f32, f32)>, // guard post pub home: Option<(f32, f32)>, // guard post
@@ -229,6 +235,8 @@ impl Ent {
torture_t: 0.0, torture_t: 0.0,
skill_cd: 0.0, skill_cd: 0.0,
waypoint: None, waypoint: None,
boost: 0.0,
stun: 0.0,
guard: false, guard: false,
home: None, home: None,
} }
@@ -294,12 +302,19 @@ pub struct Game {
#[serde(default)] #[serde(default)]
pub towers: Vec<Tower>, pub towers: Vec<Tower>,
#[serde(default)] #[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 pub entry_history: Vec<(i32, i32)>, // entry points of recent waves
#[serde(default)] #[serde(default)]
pub planned_entries: Vec<(i32, i32)>, // entry points of the coming/current wave (minimap) pub planned_entries: Vec<(i32, i32)>, // entry points of the coming/current wave (minimap)
#[serde(skip)] #[serde(skip)]
pub hints: bool, // show hint messages (setting) 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 // 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, last_wave_value: 0.0,
torches: Vec::new(), torches: Vec::new(),
towers: Vec::new(), towers: Vec::new(),
punch_ready: 0.0,
entry_history: Vec::new(), entry_history: Vec::new(),
planned_entries: Vec::new(), planned_entries: Vec::new(),
hints: true, hints: true,
hint_t: [-1000.0; 16], hint_t: hint_times(),
}; };
let (cx, cy) = g.map.core; let (cx, cy) = g.map.core;
// the soul core starts surrounded by a one tile wide ring of treasury // 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.flash = (e.flash - dt).max(0.0);
e.slow = (e.slow - dt).max(0.0); e.slow = (e.slow - dt).max(0.0);
e.skill_cd -= dt; 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.cd -= dt;
e.think -= dt; e.think -= dt;
e.repath -= dt; e.repath -= dt;
@@ -2662,12 +2686,28 @@ impl Game {
true 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 { pub fn pickup(&mut self, target: Option<usize>) -> bool {
if self.hand.len() >= 8 { if self.hand.len() >= 8 {
self.hint(8, "hint.hand_full"); self.hint(8, "hint.hand_full");
return false; 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 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 id = self.ents[j].id; let id = self.ents[j].id;
let me = id + 1; let me = id + 1;
self.unreserve(me); self.unreserve(me);
@@ -2679,40 +2719,97 @@ impl Game {
} }
let e = &mut self.ents[j]; let e = &mut self.ents[j];
e.held = true; e.held = true;
e.carrier = 0;
e.job = Job::None; e.job = Job::None;
e.path.clear(); e.path.clear();
self.hand.push(id); self.hand.push(id);
self.sfx_ui(Sfx::Pickup); self.sfx_ui(Sfx::Pickup);
return true; true
}
false
} }
// 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 { pub fn drop(&mut self, wx: f32, wy: f32) -> bool {
let Some(&id) = self.hand.last() else { return false }; let Some(&id) = self.hand.last() else { return false };
let (x, y) = (wx as i32, wy as i32); let (x, y) = (wx as i32, wy as i32);
if !inb(x, y) { if !inb(x, y) {
return false; 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 { if !t.walkable() || t.owner != Owner::Player {
self.hint(9, "hint.drop_where"); self.hint(9, "hint.drop_where");
self.sfx_ui(Sfx::Error);
return false; return false;
} }
self.hand.pop(); 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.held = false;
e.x = wx; e.x = wx;
e.y = wy; e.y = wy;
e.job = Job::None; e.job = Job::None;
e.think = 0.0; e.think = 0.0;
e.path.clear(); 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.fx(3, wx, wy, 0.0, 0.0, 0.4, 0xff707090);
self.sfx(Sfx::Drop, wx, wy); self.sfx(Sfx::Drop, wx, wy);
true 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) { pub fn lightning(&mut self, wx: f32, wy: f32) {
const COST: f32 = 50.0; const COST: f32 = 50.0;
if self.mana < COST { if self.mana < COST {
@@ -3134,4 +3231,44 @@ mod tests {
assert_eq!(ring, 16); assert_eq!(ring, 16);
assert_eq!(g.counts[0], 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);
}
}
+107 -1
View File
@@ -52,6 +52,7 @@ pub const D_ALTAR: u32 = 88;
pub struct Gfx { pub struct Gfx {
pub atlas: Texture2D, pub atlas: Texture2D,
pub sprites: Texture2D, pub sprites: Texture2D,
pub cursors: Texture2D, // mouse pointer frames, 16x16 each (see CURSOR_*)
pub font: Texture2D, // DINOBYTE, CP437, 16x16 glyphs of 6x8 px pub font: Texture2D, // DINOBYTE, CP437, 16x16 glyphs of 6x8 px
} }
@@ -746,6 +747,109 @@ fn gen_sprites() -> Image {
img 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<Color> {
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 { impl Gfx {
pub fn new() -> Gfx { pub fn new() -> Gfx {
let atlas = Texture2D::from_image(&gen_atlas()); let atlas = Texture2D::from_image(&gen_atlas());
@@ -760,7 +864,9 @@ impl Gfx {
} }
let font = Texture2D::from_image(&img); let font = Texture2D::from_image(&img);
font.set_filter(FilterMode::Nearest); 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 { pub fn cell(id: u32) -> Rect {
+10 -1
View File
@@ -209,6 +209,7 @@ impl App {
#[macroquad::main(conf)] #[macroquad::main(conf)]
async fn main() { async fn main() {
let gfx = Gfx::new(); let gfx = Gfx::new();
show_mouse(false); // the game draws its own pixel-art pointer
let mut r3d = R3d::new(); let mut r3d = R3d::new();
let mut sounds = SoundBank::new(); let mut sounds = SoundBank::new();
let mut music = MusicDirector::new(); let mut music = MusicDirector::new();
@@ -506,9 +507,16 @@ async fn main() {
match (ui.tool, hover_tile) { match (ui.tool, hover_tile) {
(Tool::Dig | Tool::Room(_) | Tool::Sell, Some(t)) => inp.ldown = Some(t), (Tool::Dig | Tool::Room(_) | Tool::Sell, Some(t)) => inp.ldown = Some(t),
(Tool::Hand, _) => { (Tool::Hand, _) => {
let j = r3d.pick_ent(&app.game, &app.cam, mx, my, true); 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); app.game.pickup(j);
} }
}
(Tool::Flag, Some((x, z))) => app.game.set_flag(x, z), (Tool::Flag, Some((x, z))) => app.game.set_flag(x, z),
(Tool::Lightning, _) => app.game.lightning(wx, wy), (Tool::Lightning, _) => app.game.lightning(wx, wy),
(Tool::Heal, _) => app.game.heal(wx, wy), (Tool::Heal, _) => app.game.heal(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 { if !menu.open() || ui.show_help || ui.show_langsel {
ui.draw_dialogs(&app.game, gfx); ui.draw_dialogs(&app.game, gfx);
} }
ui.draw_cursor(gfx, &app.game, now, blocked);
} }
+2
View File
@@ -155,6 +155,8 @@ fn def(s: Sfx, r: f32) -> Vec<f32> {
seg(Square(0.25), 900.0, 900.0, 0.08, 0.16, 15.0), seg(Square(0.25), 900.0, 900.0, 0.08, 0.16, 15.0),
], 46), ], 46),
Sfx::Convert => l(&arp(Saw, &[220.0, 262.0, 330.0, 440.0], 0.1, 0.22), 47), 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)]),
} }
} }
+78 -2
View File
@@ -229,6 +229,7 @@ pub struct Ui {
tool_rects: Vec<Rect>, tool_rects: Vec<Rect>,
end_button: Rect, end_button: Rect,
lang_rects: Vec<(Rect, String)>, lang_rects: Vec<(Rect, String)>,
pub hand_anim: Option<(u8, f64)>, // 1 = backhand, 2 = punch, start time
} }
fn wrap(s: &str, cols: usize) -> Vec<String> { fn wrap(s: &str, cols: usize) -> Vec<String> {
@@ -274,6 +275,7 @@ impl Ui {
tool_rects: Vec::new(), tool_rects: Vec::new(),
end_button: Rect::new(0.0, 0.0, 0.0, 0.0), end_button: Rect::new(0.0, 0.0, 0.0, 0.0),
lang_rects: Vec::new(), lang_rects: Vec::new(),
hand_anim: None,
} }
} }
@@ -413,8 +415,14 @@ impl Ui {
hover_tool = Some(i); hover_tool = Some(i);
} }
let sel = self.tool == t; 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(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) });
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) }); // 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); let ti = tool_info(t);
// icon: the 16 px atlas cell at 2*F, centred on the grid // icon: the 16 px atlas cell at 2*F, centred on the grid
let icon = 32.0 * px; let icon = 32.0 * px;
@@ -581,6 +589,7 @@ impl Ui {
("", TXT), ("", TXT),
("help.k_left", TXT), ("help.k_left", TXT),
("help.k_right", TXT), ("help.k_right", TXT),
("help.k_slap", TXT),
("help.k_pan", TXT), ("help.k_pan", TXT),
("help.k_rot", TXT), ("help.k_rot", TXT),
("help.k_zoom", 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) // Info text for the unit or tile under the mouse (first line = title)
pub fn hover_info(g: &Game, ent: Option<usize>, tile: Option<(i32, i32)>) -> Vec<(String, Color)> { pub fn hover_info(g: &Game, ent: Option<usize>, tile: Option<(i32, i32)>) -> Vec<(String, Color)> {
let mut out = Vec::new(); let mut out = Vec::new();