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
+358
-18
No files matched your search
@@ -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)
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
+5
-1
@@ -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.
|
||||
+5
-1
@@ -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.
|
||||
+147
-10
@@ -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,12 +2686,28 @@ 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 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 me = id + 1;
|
||||
self.unreserve(me);
|
||||
@@ -2679,40 +2719,97 @@ impl Game {
|
||||
}
|
||||
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);
|
||||
return true;
|
||||
}
|
||||
false
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
+107
-1
@@ -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<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 {
|
||||
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 {
|
||||
|
||||
+10
-1
@@ -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,9 +507,16 @@ 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);
|
||||
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),
|
||||
(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 {
|
||||
ui.draw_dialogs(&app.game, gfx);
|
||||
}
|
||||
ui.draw_cursor(gfx, &app.game, now, blocked);
|
||||
}
|
||||
@@ -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),
|
||||
], 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)]),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -229,6 +229,7 @@ pub struct Ui {
|
||||
tool_rects: Vec<Rect>,
|
||||
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<String> {
|
||||
@@ -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<usize>, tile: Option<(i32, i32)>) -> Vec<(String, Color)> {
|
||||
let mut out = Vec::new();
|
||||
|
||||
Reference in new issue
Block a user