diff --git a/src/gfx.rs b/src/gfx.rs index c633901..305747c 100644 --- a/src/gfx.rs +++ b/src/gfx.rs @@ -1062,25 +1062,121 @@ pub const SPRITE_KINDS: [EK; 17] = [ ]; // width of the sprite texture in pixels pub const SPRITE_TEX_W: f32 = (17 * 12) as f32; -pub const SPRITE_TEX_H: f32 = 24.0; +pub const SPRITE_TEX_H: f32 = (2 * GEAR_TIERS * SPR_CELL) as f32; pub fn sprite_index(k: EK) -> u32 { SPRITE_KINDS.iter().position(|&s| s == k).unwrap_or(0) as u32 } -// row 0: normal sprites, row 1: white silhouettes (hit flash) +// Equipment tier of a level: 0 (Lv 1-2) none, 1 (3-4) shoulder pieces, 2 (5-6) + helmet, +// 3 (7-8) + cape, 4 (9-10) all in gold with a crown +pub const GEAR_TIERS: u32 = 5; +pub fn gear_tier(level: u32) -> u32 { + ((level.max(1) - 1) / 2).min(GEAR_TIERS - 1) +} + +fn is_hero_kind(k: EK) -> bool { + matches!(k, EK::Knight | EK::Archer | EK::Priest | EK::Dwarf | EK::Paladin) +} + +// the 12x12 cell of a kind with its gear painted around and onto the silhouette +fn sprite_with_gear(k: EK, tier: u32) -> [[Option; 12]; 12] { + let mut g = [[None; 12]; 12]; + for (y, row) in sprite_rows(k).iter().enumerate() { + for (x, ch) in row.chars().enumerate().take(SPR) { + g[y + 1][x + 1] = pal(ch); + } + } + if tier == 0 { + return g; + } + let metal = match tier { + 1 => c(196, 126, 60), + 4 => c(250, 206, 72), + _ => c(208, 214, 228), + }; + let dark = Color::new(metal.r * 0.6, metal.g * 0.6, metal.b * 0.6, 1.0); + let span = |g: &[[Option; 12]; 12], y: usize| -> Option<(usize, usize)> { + let xs: Vec = (0..12).filter(|&x| g[y][x].is_some()).collect(); + Some((*xs.first()?, *xs.last()?)) + }; + let top = (0..12).find(|&y| g[y].iter().any(|p| p.is_some())).unwrap_or(1); + // cape (behind the figure: only where the sprite is empty) + if tier >= 3 { + let cape = if is_hero_kind(k) { c(44, 74, 156) } else { c(128, 22, 34) }; + for y in (top + 3).min(11)..=(top + 8).min(11) { + if let Some((l, r)) = span(&g, y) { + for x in [l.wrapping_sub(1), r + 1] { + if x < 12 && g[y][x].is_none() { + g[y][x] = Some(cape); + } + } + } + } + } + // shoulder pieces at the widest row of the upper body + let shoulder = (top + 2..(top + 6).min(11)).max_by_key(|&y| span(&g, y).map_or(0, |(l, r)| r - l)).unwrap_or(top + 3); + if let Some((l, r)) = span(&g, shoulder) { + for x in [l, r] { + g[shoulder][x] = Some(metal); + if shoulder + 1 < 12 && g[shoulder + 1][x].is_some() { + g[shoulder + 1][x] = Some(dark); + } + } + } + // helmet: the top row in metal, horns (creatures) or a plume (heroes) + if tier >= 2 { + if let Some((l, r)) = span(&g, top) { + for x in l..=r { + if g[top][x].is_some() { + g[top][x] = Some(metal); + } + } + if top >= 1 { + if is_hero_kind(k) { + g[top - 1][(l + r) / 2] = Some(c(200, 40, 40)); + } else { + let bone = c(232, 222, 200); + if l >= 1 { + g[top - 1][l - 1] = Some(bone); + g[top][l - 1] = Some(bone); + } + if r + 1 < 12 { + g[top - 1][r + 1] = Some(bone); + g[top][r + 1] = Some(bone); + } + } + } + } + } + // crown + if tier >= 4 && top >= 1 { + if let Some((l, r)) = span(&g, top) { + let m = (l + r) / 2; + for x in [m.saturating_sub(1), m + 1] { + if x < 12 { + g[top - 1][x] = Some(c(255, 230, 90)); + } + } + } + } + g +} + +// rows 2*tier: sprites with the gear of the tier, rows 2*tier+1: white silhouettes (hit flash) fn gen_sprites() -> Image { let n = SPRITE_KINDS.len() as u32; - let mut img = Image::gen_image_color((n * SPR_CELL) as u16, (2 * SPR_CELL) as u16, Color::new(0.0, 0.0, 0.0, 0.0)); + let mut img = Image::gen_image_color((n * SPR_CELL) as u16, (2 * GEAR_TIERS * SPR_CELL) as u16, Color::new(0.0, 0.0, 0.0, 0.0)); for (i, &k) in SPRITE_KINDS.iter().enumerate() { - let rows = sprite_rows(k); - for (y, row) in rows.iter().enumerate() { - for (x, ch) in row.chars().enumerate().take(SPR) { - if let Some(col) = pal(ch) { - let px = i as u32 * SPR_CELL + 1 + x as u32; - let py = 1 + y as u32; - img.set_pixel(px, py, col); - img.set_pixel(px, py + SPR_CELL, WHITE); + for tier in 0..GEAR_TIERS { + let g = sprite_with_gear(k, tier); + for (y, row) in g.iter().enumerate() { + for (x, px) in row.iter().enumerate() { + if let Some(col) = px { + let (px, py) = (i as u32 * SPR_CELL + x as u32, tier * 2 * SPR_CELL + y as u32); + img.set_pixel(px, py, *col); + img.set_pixel(px, py + SPR_CELL, WHITE); + } } } } @@ -1214,11 +1310,19 @@ impl Gfx { Rect::new((id % ATLAS_COLS) as f32 * CELL, (id / ATLAS_COLS) as f32 * CELL, CELL, CELL) } + // the plain 10x10 sprite (interface icons) pub fn sprite_rect(k: EK, white: bool) -> Rect { let i = sprite_index(k); Rect::new((i * SPR_CELL + 1) as f32, (1 + if white { SPR_CELL } else { 0 }) as f32, SPR as f32, SPR as f32) } + // the whole 12x12 cell with the gear of a level (billboards) + pub fn sprite_cell(k: EK, white: bool, level: u32) -> Rect { + let i = sprite_index(k); + let row = gear_tier(level) * 2 + white as u32; + Rect::new((i * SPR_CELL) as f32, (row * SPR_CELL) as f32, SPR_CELL as f32, SPR_CELL as f32) + } + pub fn tile_tex(kind: Kind, claimed: bool, hero: bool, var: u8, reinforced: bool) -> u32 { let v = var as u32 & 3; match kind { diff --git a/src/render3d.rs b/src/render3d.rs index 5435ad9..f6d8412 100644 --- a/src/render3d.rs +++ b/src/render3d.rs @@ -1020,7 +1020,6 @@ 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 @@ -1108,7 +1107,7 @@ impl R3d { _ => {} } let white = e.flash > 0.07; - let src = Gfx::sprite_rect(e.kind, white); + let src = Gfx::sprite_cell(e.kind, white, e.level); let (mut u0, v0) = (src.x / sw, src.y / sh); let (mut u1, v1) = ((src.x + src.w) / sw, (src.y + src.h) / sh); if e.face < 0.0 { @@ -1131,8 +1130,10 @@ impl R3d { let y = dx * sr + dy * cr; base + r * x + Vec3::Y * y }; + // the 12x12 cell (sprite with a one pixel border for the gear): the sprite's feet + // (bottom of its 11th pixel row) stay on the ground mb.quad( - [corner(-w * 0.5, h), corner(w * 0.5, h), corner(w * 0.5, 0.0), corner(-w * 0.5, 0.0)], + [corner(-w * 0.6, h * 1.1), corner(w * 0.6, h * 1.1), corner(w * 0.6, -h * 0.1), corner(-w * 0.6, -h * 0.1)], [vec2(u0, v0), vec2(u1, v0), vec2(u1, v1), vec2(u0, v1)], [c; 4], [toward; 4], @@ -1239,53 +1240,13 @@ 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 mut bob = if e.moving { (e.anim * 12.0).sin().abs() * 0.06 } else { 0.0 }; - if flies(e.kind) { - bob = 0.45; - } - 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 lift = if flies(e.kind) { 0.45 } else { 0.0 }; + let base = vec3(e.x, lift, 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 { @@ -1302,34 +1263,87 @@ fn draw_gear_glow(g: &Game, cam: &Cam, t: f32) { fn draw_towers_solid(g: &Game) { for tw in &g.towers { let (x, z) = tw.pos(); + let c = vec3(x, 0.0, z); let h = tower_height(tw); let f = 0.75 + tw.flash * 1.5; // brightens briefly when firing // knight towers: pale stone with blue tones - let shade_c = |r: u8, g: u8, b: u8, f: f32| if tw.hero { shade_c(r / 3 + 110, g / 3 + 115, b / 3 + 135, f) } else { shade_c(r, g, b, f) }; + let col = |r: u8, g: u8, b: u8| if tw.hero { shade_c(r / 3 + 110, g / 3 + 115, b / 3 + 135, f) } else { shade_c(r, g, b, f) }; + let bx = |p: Vec3, s: Vec3, cl: Color| draw_cube(c + p, s, None, cl); match tw.kind { TowerKind::Arrow => { - draw_cube(vec3(x, h * 0.5, z), vec3(0.7, h, 0.7), None, shade_c(110, 75, 45, f)); - draw_cube(vec3(x, h + 0.06, z), vec3(0.92, 0.12, 0.92), None, shade_c(130, 90, 55, f)); - for (dx, dz) in [(-0.38, -0.38), (0.38, -0.38), (0.38, 0.38), (-0.38, 0.38)] { - draw_cube(vec3(x + dx, h + 0.2, z + dz), vec3(0.15, 0.18, 0.15), None, shade_c(130, 90, 55, f)); + // stone plinth, masonry body with courses and arrow slits + bx(vec3(0.0, 0.08, 0.0), vec3(0.86, 0.16, 0.86), col(86, 80, 76)); + bx(vec3(0.0, h * 0.5, 0.0), vec3(0.66, h, 0.66), col(118, 108, 98)); + let courses = (h / 0.22) as i32; + for k in 1..courses { + bx(vec3(0.0, k as f32 * 0.22, 0.0), vec3(0.68, 0.025, 0.68), col(80, 72, 66)); } + for (dx, dz) in [(0.0f32, 0.34f32), (0.34, 0.0), (0.0, -0.34), (-0.34, 0.0)] { + bx(vec3(dx, h * 0.62, dz), vec3(if dx == 0.0 { 0.06 } else { 0.02 }, 0.2, if dz == 0.0 { 0.06 } else { 0.02 }), col(24, 18, 16)); + } + // corbels and the wooden platform + for (dx, dz) in [(-0.3f32, -0.3f32), (0.3, -0.3), (0.3, 0.3), (-0.3, 0.3)] { + bx(vec3(dx * 1.2, h - 0.08, dz * 1.2), vec3(0.12, 0.12, 0.12), col(96, 88, 80)); + } + bx(vec3(0.0, h + 0.04, 0.0), vec3(0.94, 0.08, 0.94), col(120, 84, 50)); + // battlements: merlons with gaps + for k in 0..4 { + let d = -0.39 + k as f32 * 0.26; + for (px, pz) in [(d, -0.42), (d, 0.42), (-0.42, d), (0.42, d)] { + bx(vec3(px, h + 0.17, pz), vec3(0.11, 0.18, 0.11), col(128, 118, 106)); + } + } + // pennant and ladder + bx(vec3(0.3, h + 0.45, 0.3), vec3(0.03, 0.7, 0.03), col(140, 140, 150)); + bx(vec3(0.42, h + 0.7, 0.3), vec3(0.22, 0.12, 0.02), if tw.hero { shade_c(60, 90, 180, f) } else { shade_c(170, 30, 40, f) }); + for k in 0..((h / 0.15) as i32) { + bx(vec3(-0.2, 0.1 + k as f32 * 0.15, 0.35), vec3(0.2, 0.025, 0.03), col(110, 76, 44)); + } + bx(vec3(-0.29, h * 0.5, 0.35), vec3(0.025, h, 0.03), col(100, 70, 40)); + bx(vec3(-0.11, h * 0.5, 0.35), vec3(0.025, h, 0.03), col(100, 70, 40)); } TowerKind::Cannon => { - draw_cube(vec3(x, h * 0.5, z), vec3(0.88, h, 0.88), None, shade_c(100, 100, 110, f)); - draw_cube(vec3(x, h + 0.08, z), vec3(0.5, 0.16, 0.5), None, shade_c(70, 70, 78, f)); - let (ax, az) = (tw.aim.cos(), tw.aim.sin()); + // squat round tower (stacked rings), iron bands, carriage, barrel, cannon balls for k in 0..4 { - let d = 0.1 + k as f32 * 0.14; - draw_cube(vec3(x + ax * d, h + 0.2, z + az * d), Vec3::splat(0.2 - k as f32 * 0.015), None, shade_c(40, 40, 46, f)); + let r = 0.92 - k as f32 * 0.03; + let y = h * (k as f32 + 0.5) / 4.0; + bx(vec3(0.0, y, 0.0), vec3(r, h / 4.0 + 0.01, r * 0.82), col(104, 104, 112)); + bx(vec3(0.0, y, 0.0), vec3(r * 0.82, h / 4.0 + 0.01, r), col(96, 96, 104)); + } + bx(vec3(0.0, h * 0.35, 0.0), vec3(0.94, 0.05, 0.94), col(54, 54, 60)); + bx(vec3(0.0, h * 0.75, 0.0), vec3(0.9, 0.05, 0.9), col(54, 54, 60)); + bx(vec3(0.0, h + 0.05, 0.0), vec3(0.62, 0.1, 0.62), col(80, 62, 44)); + let (ax, az) = (tw.aim.cos(), tw.aim.sin()); + // carriage cheeks beside the barrel + for side in [-1.0f32, 1.0] { + bx(vec3(-az * 0.14 * side, h + 0.17, ax * 0.14 * side), vec3(0.14, 0.16, 0.14), col(96, 70, 44)); + } + for k in 0..5 { + let d = 0.0 + k as f32 * 0.13; + let r = 0.2 - k as f32 * 0.012; + bx(vec3(ax * d, h + 0.22, az * d), Vec3::splat(r), col(40, 40, 46)); + } + bx(vec3(ax * 0.58, h + 0.22, az * 0.58), Vec3::splat(0.2), col(70, 70, 78)); + for (dx, dz, y) in [(0.3f32, -0.28f32, 0.0f32), (0.38, -0.2, 0.0), (0.34, -0.24, 0.08)] { + bx(vec3(dx, h + 0.15 + y, dz), Vec3::splat(0.07), col(30, 30, 34)); } } TowerKind::Magic => { - draw_cube(vec3(x, h * 0.5, z), vec3(0.42, h, 0.42), None, shade_c(84, 74, 104, f)); - draw_cube(vec3(x, 0.1, z), vec3(0.75, 0.2, 0.75), None, shade_c(70, 62, 86, f)); - draw_cube(vec3(x, h, z), vec3(0.6, 0.1, 0.6), None, shade_c(110, 96, 130, f)); + // rune-cut obelisk on a stepped base with buttresses (crystal and runes glow) + bx(vec3(0.0, 0.06, 0.0), vec3(0.82, 0.12, 0.82), col(62, 56, 76)); + bx(vec3(0.0, 0.17, 0.0), vec3(0.62, 0.1, 0.62), col(72, 64, 90)); + bx(vec3(0.0, h * 0.5, 0.0), vec3(0.4, h, 0.4), col(84, 74, 104)); + bx(vec3(0.0, h * 0.75, 0.0), vec3(0.32, h * 0.5, 0.46), col(92, 82, 114)); + for (dx, dz) in [(0.25f32, 0.0f32), (-0.25, 0.0), (0.0, 0.25), (0.0, -0.25)] { + bx(vec3(dx, h * 0.22, dz), vec3(if dx == 0.0 { 0.12 } else { 0.1 }, h * 0.44, if dz == 0.0 { 0.12 } else { 0.1 }), col(70, 62, 88)); + } + bx(vec3(0.0, h, 0.0), vec3(0.58, 0.08, 0.58), col(110, 96, 130)); + for (dx, dz) in [(-0.24f32, -0.24f32), (0.24, -0.24), (0.24, 0.24), (-0.24, 0.24)] { + bx(vec3(dx, h + 0.12, dz), vec3(0.06, 0.18, 0.06), col(120, 104, 140)); + } } } - // level pips + // level pips on the plinth for k in 0..tw.level { draw_cube(vec3(x - 0.2 + k as f32 * 0.2, 0.25, z + 0.46), Vec3::splat(0.09), None, Color::from_rgba(240, 200, 70, 255)); } @@ -1346,6 +1360,15 @@ fn draw_towers_glow(g: &Game, t: f32) { let pulse = 1.0 + (t * 4.0 + k as f32).sin() * 0.1 + tw.flash * 2.0; draw_sphere(c, 0.16 * pulse, None, Color::new(0.85, 0.5, 1.0, 0.95)); draw_sphere(c, 0.3 * pulse, None, Color::new(0.7, 0.3, 1.0, 0.25)); + // runes on the four faces of the obelisk, glowing one after another + let h = tower_height(tw); + for r in 0..3 { + let y = h * (0.35 + r as f32 * 0.22); + let glow = 0.4 + 0.6 * ((t * 2.5 - r as f32 * 0.8 + k as f32).sin() * 0.5 + 0.5); + for (dx, dz) in [(0.21f32, 0.0f32), (-0.21, 0.0), (0.0, 0.21), (0.0, -0.21)] { + draw_cube(vec3(x + dx, y, z + dz), vec3(if dx == 0.0 { 0.1 } else { 0.02 }, 0.08, if dz == 0.0 { 0.1 } else { 0.02 }), None, Color::new(0.8, 0.45, 1.0, glow)); + } + } } } @@ -1419,10 +1442,33 @@ fn draw_landmarks_solid(g: &Game, t: f32) { let (gx, gz) = g.gate_center(); if g.gate_hp > 0.0 { let stone = Color::from_rgba(150, 155, 172, 255); - draw_cube(vec3(gx - 1.1, 1.0, gz), vec3(0.45, 2.0, 0.45), None, stone); - draw_cube(vec3(gx + 1.1, 1.0, gz), vec3(0.45, 2.0, 0.45), None, stone); - draw_cube(vec3(gx, 2.1, gz), vec3(2.8, 0.35, 0.55), None, stone); - draw_cube(vec3(gx, 2.35, gz), vec3(0.5, 0.2, 0.5), None, Color::from_rgba(230, 200, 100, 255)); + let dark = Color::from_rgba(112, 116, 132, 255); + let gold = Color::from_rgba(230, 200, 100, 255); + let iron = Color::from_rgba(56, 58, 66, 255); + for side in [-1.1f32, 1.1] { + draw_cube(vec3(gx + side, 0.12, gz), vec3(0.6, 0.24, 0.6), None, dark); // plinth + draw_cube(vec3(gx + side, 1.0, gz), vec3(0.45, 1.8, 0.45), None, stone); + for y in [0.6f32, 1.2] { + draw_cube(vec3(gx + side, y, gz), vec3(0.48, 0.05, 0.48), None, dark); // bands + } + draw_cube(vec3(gx + side, 1.92, gz), vec3(0.56, 0.12, 0.56), None, dark); // capital + // banner of the order: white with a golden sun + draw_cube(vec3(gx + side, 1.35, gz + 0.25), vec3(0.32, 0.7, 0.02), None, Color::from_rgba(232, 234, 242, 255)); + draw_cube(vec3(gx + side, 1.45, gz + 0.265), vec3(0.14, 0.14, 0.01), None, gold); + draw_cube(vec3(gx + side, 1.02, gz + 0.26), vec3(0.32, 0.05, 0.02), None, Color::from_rgba(60, 90, 170, 255)); + } + // stepped arch with a keystone + draw_cube(vec3(gx, 2.1, gz), vec3(2.8, 0.3, 0.55), None, stone); + draw_cube(vec3(gx, 1.88, gz), vec3(1.7, 0.14, 0.5), None, dark); + draw_cube(vec3(gx, 2.32, gz), vec3(2.2, 0.14, 0.5), None, dark); + draw_cube(vec3(gx, 2.2, gz), vec3(0.34, 0.5, 0.58), None, gold); + // portcullis (iron bars) in the opening, half raised + for k in 0..5 { + draw_cube(vec3(gx - 0.6 + k as f32 * 0.3, 1.45, gz), vec3(0.05, 0.85, 0.05), None, iron); + } + for y in [1.15f32, 1.5, 1.8] { + draw_cube(vec3(gx, y, gz), vec3(1.7, 0.05, 0.05), None, iron); + } } else { for k in 0..10 { let a = k as f32 * 1.7;