Rank badges and level equipment; no edge scrolling before the mouse has moved

- Military rank badges instead of level numbers: Lv 2-4 bronze chevrons, Lv 5-7 silver bars,
  Lv 8-9 gold stars, Lv 10 gold star in a wreath; rank names in the unit info (EN/DE)
- Equipment grows with the level: shoulder pieces (bronze/silver/gold), helmets with horns
  (creatures) or plumes (heroes), capes from Lv 7, glowing ring from Lv 9, aura at Lv 10
- Fix: before the first mouse move the pointer counts as (0, 0) and the edge scrolling
  moved the camera at the start and after loading a savegame

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-03 12:47:34 +01:00
1 parent 52d49b0243
commit 729dd4fe4c
6 files changed
+170 -5

No files matched your search

+57
View File
@@ -1183,6 +1183,63 @@ impl Gfx {
s.chars().count() as f32 * GW * scale
}
// Military rank badge for a level (k = size of one badge pixel in device pixels):
// Lv 2-4: 1-3 bronze chevrons, Lv 5-7: 1-3 silver bars, Lv 8-9: 1-2 gold stars,
// Lv 10: a gold star in a green wreath. Lv 1 has none. Returns the width drawn.
pub fn badge(&self, x: f32, y: f32, level: u32, k: f32) -> f32 {
let px = |gx: i32, gy: i32, c: Color| draw_rectangle(x + gx as f32 * k, y + gy as f32 * k, k, k, c);
let plate = |w: i32, h: i32| draw_rectangle(x - k, y - k, (w + 2) as f32 * k, (h + 2) as f32 * k, Color::new(0.05, 0.03, 0.04, 0.85));
let bronze = Color::from_rgba(214, 140, 70, 255);
let silver = Color::from_rgba(215, 222, 235, 255);
let gold = Color::from_rgba(255, 210, 70, 255);
let star = |ox: i32, oy: i32, c: Color| {
for (gx, gy) in [(2, 0), (1, 1), (2, 1), (3, 1), (0, 2), (1, 2), (2, 2), (3, 2), (4, 2), (1, 3), (2, 3), (3, 3), (0, 4), (1, 4), (3, 4), (4, 4)] {
px(ox + gx, oy + gy, c);
}
};
match level {
0 | 1 => 0.0,
2..=4 => {
let n = (level - 1) as i32;
plate(5, n * 2 + 1);
for c in 0..n {
// a chevron pointing up
for (gx, gy) in [(2, 0), (1, 1), (3, 1), (0, 2), (4, 2)] {
px(gx, c * 2 + gy - if gy == 2 { 1 } else { 0 }, bronze);
}
}
7.0 * k
}
5..=7 => {
let n = (level - 4) as i32;
plate(5, n * 2 - 1);
for b in 0..n {
for gx in 0..5 {
px(gx, b * 2, silver);
}
}
7.0 * k
}
8 | 9 => {
let n = (level - 7) as i32;
plate(n * 6 - 1, 5);
for s in 0..n {
star(s * 6, 0, gold);
}
(n * 6 + 1) as f32 * k
}
_ => {
plate(9, 7);
let green = Color::from_rgba(90, 170, 80, 255);
for (gx, gy) in [(0, 2), (0, 3), (0, 4), (1, 5), (2, 6), (8, 2), (8, 3), (8, 4), (7, 5), (6, 6), (3, 6), (5, 6), (1, 1), (7, 1)] {
px(gx, gy, green);
}
star(2, 1, gold);
11.0 * k
}
}
}
// text with a dark shadow
pub fn text_sh(&self, s: &str, x: f32, y: f32, scale: f32, color: Color) {
self.text(s, x + scale, y + scale, scale, Color::new(0.0, 0.0, 0.0, color.a * 0.85));
+7 -2
View File
@@ -322,7 +322,9 @@ async fn main() {
app.game.hints = app.settings.hints;
let mut inp = Input { ldown: None, rdown: None, rlast: (0.0, 0.0), rdrag: false, mlast: None, minimap_drag: false, tlast: None, touch_world: false, inspect: None };
let mut touch = TouchInput::new();
let mut auto_touch = platform::coarse_pointer(); // automatic mode: touch interface after a touch, desktop after a mouse click
let mut auto_touch = platform::coarse_pointer();
// until the mouse has really moved its position is (0, 0): no edge scrolling before that
let mut mouse_moved = false; // automatic mode: touch interface after a touch, desktop after a mouse click
let mut autosave_t = 0.0f32;
let mut cache_t = 0.0f32;
@@ -504,7 +506,10 @@ async fn main() {
move_cam(pan, 0.0, cam);
}
let edge = 6.0;
if keys.edge_scroll && !touch_mode && mx >= 0.0 && my >= 0.0 && mx < screen_width() && my < screen_height() {
if !mouse_moved && mouse_delta_position() != Vec2::ZERO {
mouse_moved = true;
}
if keys.edge_scroll && mouse_moved && !touch_mode && mx >= 0.0 && my >= 0.0 && mx < screen_width() && my < screen_height() {
if mx < edge {
move_cam(-pan, 0.0, cam);
} else if mx > screen_width() - edge {
+79 -3
View File
@@ -1020,6 +1020,7 @@ impl R3d {
}
draw_landmarks_solid(g, t);
draw_towers_solid(g);
draw_gear(g, cam);
crate::props::draw_props_solid(g, cam, t);
// ---- unlit glow / overlays
@@ -1052,6 +1053,7 @@ impl R3d {
// translucent glows and effects last (they write depth, see glow_material)
draw_landmarks_glow(g, t);
draw_towers_glow(g, t);
draw_gear_glow(g, cam, t);
crate::props::draw_props_glow(g, cam, t);
for f in &g.fx {
draw_fx(f, t);
@@ -1176,9 +1178,11 @@ impl R3d {
if e.guard {
gfx.glyph(0x0F, head.x - (GW * small * 0.5).floor(), head.y - 2.0 * GH * small, small, Color::from_rgba(255, 220, 90, 255));
}
if e.level > 1 && px_per_tile >= 24.0 * F() as f32 {
let x = head.x - (cols as f32 * cw * 0.5).floor() - 2.0 * cw;
gfx.text_sh(&e.level.to_string(), x, head.y - GH * small, small, Color::from_rgba(255, 220, 90, 255));
// rank badge left of the health bar
if e.level > 1 && px_per_tile >= 20.0 * F() as f32 {
let k = 2.0 * F() as f32;
let x = head.x - (cols as f32 * cw * 0.5).floor() - 12.0 * k;
gfx.badge(x, head.y - GH * small - 4.0 * k, e.level, k);
}
if matches!(e.job, Job::Sleep(_)) && !e.moving {
let k = (t * 1.5 + e.anim) % 1.0;
@@ -1216,6 +1220,78 @@ fn shade_c(r: u8, g: u8, b: u8, f: f32) -> Color {
Color::new(r as f32 / 255.0 * f, g as f32 / 255.0 * f, b as f32 / 255.0 * f, 1.0)
}
// size of a unit's sprite (same as in the billboards)
fn sprite_size(k: EK) -> f32 {
if k == EK::Chainbreaker {
1.0
} else if matches!(k, EK::Ogre | EK::Paladin | EK::Jailer) {
0.8
} else {
0.62
}
}
fn gear_shown(e: &Ent) -> bool {
e.alive && !e.held && e.captive == 0 && e.level >= 3 && !(matches!(e.job, Job::Sleep(_)) && !e.moving)
}
// Equipment that grows with the level: shoulder pieces (Lv 3+: bronze, 5+: silver, 8+: gold),
// a helmet (Lv 5+: horns for creatures, a plume for heroes) and a cape (Lv 7+).
fn draw_gear(g: &Game, cam: &Cam) {
let (_, r, _) = cam.basis();
let toward = cam.eye_dir();
for e in g.ents.iter().filter(|e| gear_shown(e)) {
let w = sprite_size(e.kind);
let bob = if e.moving { (e.anim * 12.0).sin().abs() * 0.06 } else { 0.0 };
let base = vec3(e.x, bob, e.y) + toward * 0.5;
let at = |dx: f32, dy: f32| base + r * (dx * w) + Vec3::Y * (dy * w);
let metal = if e.level >= 8 {
Color::from_rgba(240, 200, 70, 255)
} else if e.level >= 5 {
Color::from_rgba(200, 206, 220, 255)
} else {
Color::from_rgba(190, 120, 60, 255)
};
if e.level >= 7 {
// cape behind the figure
let back = base - toward * 0.1;
draw_cube(back + Vec3::Y * (0.5 * w), vec3(0.42 * w, 0.55 * w, 0.42 * w), None, if e.hero { Color::from_rgba(40, 70, 150, 255) } else { Color::from_rgba(120, 20, 30, 255) });
}
for side in [-1.0f32, 1.0] {
draw_cube(at(side * 0.26, 0.6), Vec3::splat(0.09 * w + 0.02), None, metal);
}
if e.level >= 5 {
draw_cube(at(0.0, 0.93), vec3(0.2 * w, 0.06 * w, 0.2 * w), None, metal);
if e.hero {
draw_cube(at(0.0, 1.02), vec3(0.04, 0.1 * w, 0.04), None, Color::from_rgba(200, 40, 40, 255));
} else {
for side in [-1.0f32, 1.0] {
draw_cube(at(side * 0.16, 1.0), vec3(0.035, 0.09 * w, 0.035), None, Color::from_rgba(230, 220, 200, 255));
}
}
}
}
}
// Lv 9+: a glowing ring above the head, Lv 10 also a faint aura at the feet
fn draw_gear_glow(g: &Game, cam: &Cam, t: f32) {
let toward = cam.eye_dir();
for e in g.ents.iter().filter(|e| gear_shown(e) && e.level >= 9) {
let w = sprite_size(e.kind);
let base = vec3(e.x, 0.0, e.y) + toward * 0.5;
let c = if e.hero { Color::new(0.8, 0.9, 1.0, 0.8) } else { Color::new(1.0, 0.8, 0.3, 0.8) };
let y = w * 1.12 + (t * 2.0 + e.anim).sin() * 0.02;
for k in 0..8 {
let a0 = k as f32 * 0.785 + t;
let a1 = a0 + 0.785;
line3(base + vec3(a0.cos() * 0.12, y, a0.sin() * 0.12), base + vec3(a1.cos() * 0.12, y, a1.sin() * 0.12), 0.025, c);
}
if e.level >= 10 {
disc(vec3(e.x, 0.015, e.y), 0.42 + 0.03 * (t * 3.0).sin(), Color::new(c.r, c.g, c.b, 0.18), 16);
}
}
}
fn draw_towers_solid(g: &Game) {
for tw in &g.towers {
let (x, z) = tw.pos();
+1
View File
@@ -1023,6 +1023,7 @@ pub fn hover_info(g: &Game, ent: Option<usize>, tile: Option<(i32, i32)>) -> Vec
let e = &g.ents[j];
let s = st(e.kind);
out.push((trf("info.level", &[tr(s.name), e.level.to_string()]), if e.hero { BLUE } else { FRAME_HI }));
out.push((trf("info.rank", &[tr(&format!("rank.{}", e.level.clamp(1, 10)))]), GOLD));
out.push((trf("info.hp", &[(e.hp.max(0.0) as i32).to_string(), (e.max_hp as i32).to_string()]), GREEN));
if e.hero && e.captive != 0 {
out.push((tr(["", "info.unconscious", "info.carried", "info.tortured"][e.captive.min(3) as usize]), RED));