UI in device pixels on the character grid with fixed integer font sizes, ASCII bars; 3D render scale for a pixel look
- text only at 1x/2x/4x times an integer interface factor (auto or 1-4 in the settings) - high DPI rendering, own 2D camera in device pixels, mouse converted - toolbar buttons 6 columns, integer icon scale, minimap integer pixels per tile - all bars (top bar, unit/tower/gate HP) as CP437 block characters - 3D world optionally rendered at 1/2, 1/3, 1/4 and upscaled with nearest filtering Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
889ca66dbf
commit
50d013bb28
8 files changed
+344
-213
No files matched your search
+60
-25
@@ -681,6 +681,8 @@ pub struct R3d {
|
||||
pub outlines: bool, // tile outlines on/off
|
||||
wall_sub: usize, // wall face subdivisions (detail)
|
||||
pub max_lights: usize,
|
||||
pub render_div: u32, // 3D resolution divisor (pixel look)
|
||||
target: Option<(RenderTarget, u32, u32)>, // low resolution render target
|
||||
}
|
||||
|
||||
fn rgba(u: u32) -> Color {
|
||||
@@ -710,7 +712,7 @@ fn chunk_sig(map: &Map, cx: i32, cz: i32, fill_bucket: u8) -> u64 {
|
||||
|
||||
impl R3d {
|
||||
pub fn new() -> R3d {
|
||||
R3d { lit: lit_material(), glow: glow_material(), chunks: Vec::new(), ncx: 0, ncz: 0, outlines: true, wall_sub: 3, max_lights: MAX_LIGHTS }
|
||||
R3d { lit: lit_material(), glow: glow_material(), chunks: Vec::new(), ncx: 0, ncz: 0, outlines: true, wall_sub: 3, max_lights: MAX_LIGHTS, render_div: 1, target: None }
|
||||
}
|
||||
|
||||
// Changing the wall detail rebuilds all chunks
|
||||
@@ -854,7 +856,28 @@ impl R3d {
|
||||
let m = cam.matrix();
|
||||
self.update_chunks(g, gfx, &m);
|
||||
EYE.with(|e| e.set(cam.eye_dir()));
|
||||
set_camera(&cam.camera());
|
||||
// pixel look: render the world at 1/div resolution and scale it up by exactly div
|
||||
let div = self.render_div.max(1);
|
||||
let d = crate::ui::dpr();
|
||||
let (pw, ph) = ((screen_width() * d).round(), (screen_height() * d).round());
|
||||
let mut cam3 = cam.camera();
|
||||
if div > 1 {
|
||||
let (tw, th) = ((pw / div as f32).ceil() as u32, (ph / div as f32).ceil() as u32);
|
||||
if self.target.as_ref().map_or(true, |(_, w, h)| *w != tw || *h != th) {
|
||||
let rt = render_target_ex(tw, th, RenderTargetParams { sample_count: 1, depth: true });
|
||||
rt.texture.set_filter(FilterMode::Nearest);
|
||||
self.target = Some((rt, tw, th));
|
||||
}
|
||||
let (rt, tw, th) = self.target.as_ref().unwrap();
|
||||
cam3.render_target = Some(rt.clone());
|
||||
cam3.aspect = Some(*tw as f32 / *th as f32);
|
||||
} else {
|
||||
self.target = None;
|
||||
}
|
||||
set_camera(&cam3);
|
||||
if div > 1 {
|
||||
clear_background(Color::from_rgba(4, 3, 5, 255));
|
||||
}
|
||||
|
||||
// ---- lit geometry
|
||||
let (lp, lc) = self.lights(g, cam, t);
|
||||
@@ -919,9 +942,13 @@ impl R3d {
|
||||
draw_fx(f, t);
|
||||
}
|
||||
gl_use_default_material();
|
||||
set_default_camera();
|
||||
crate::ui::begin_ui();
|
||||
if let (true, Some((rt, tw, th))) = (div > 1, self.target.as_ref()) {
|
||||
let size = vec2((*tw * div) as f32, (*th * div) as f32);
|
||||
draw_texture_ex(&rt.texture, 0.0, 0.0, WHITE, DrawTextureParams { dest_size: Some(size), flip_y: true, ..Default::default() });
|
||||
}
|
||||
|
||||
// ---- 2D overlay: health bars, sleep marks, levels, gate HP
|
||||
// ---- 2D overlay in device pixels: health bars, sleep marks, levels, gate HP
|
||||
self.draw_2d(g, gfx, cam, &m, t);
|
||||
}
|
||||
|
||||
@@ -999,15 +1026,25 @@ impl R3d {
|
||||
}
|
||||
|
||||
fn draw_2d(&self, g: &Game, gfx: &Gfx, cam: &Cam, m: &Mat4, t: f32) {
|
||||
let px_per_tile = screen_height() / cam.zoom;
|
||||
use crate::ui::{ascii_bar, dpr, F, S, SMALL};
|
||||
let d = dpr();
|
||||
// projected positions are CSS pixels; the overlay is drawn in whole device pixels
|
||||
let proj = |p: Vec3| {
|
||||
let v = Cam::project(m, p) * d;
|
||||
vec2(v.x.floor(), v.y.floor())
|
||||
};
|
||||
let small = SMALL();
|
||||
let cw = GW * small; // width of one small block character
|
||||
let px_per_tile = screen_height() / cam.zoom * d;
|
||||
for e in &g.ents {
|
||||
if !e.alive || e.held {
|
||||
continue;
|
||||
}
|
||||
let big = matches!(e.kind, EK::Ogre | EK::Paladin | EK::Jailer);
|
||||
let head = Cam::project(m, vec3(e.x, if big { 0.9 } else { 0.72 }, e.y));
|
||||
let w = (px_per_tile * 0.6).clamp(12.0, 40.0);
|
||||
if e.hp < e.max_hp || e.hero {
|
||||
let head = proj(vec3(e.x, if big { 0.9 } else { 0.72 }, e.y));
|
||||
// bar width in block characters, depending on the zoom
|
||||
let cols = ((px_per_tile * 0.6 / cw) as usize).clamp(3, 8);
|
||||
if (e.hp < e.max_hp || e.hero) && e.captive == 0 {
|
||||
let f = (e.hp / e.max_hp).clamp(0.0, 1.0);
|
||||
let c = if e.hero {
|
||||
Color::from_rgba(120, 180, 255, 255)
|
||||
@@ -1018,40 +1055,38 @@ impl R3d {
|
||||
} else {
|
||||
Color::from_rgba(230, 60, 50, 255)
|
||||
};
|
||||
draw_rectangle(head.x - w * 0.5 - 1.0, head.y - 1.0, w + 2.0, 4.0, Color::new(0.0, 0.0, 0.0, 0.8));
|
||||
draw_rectangle(head.x - w * 0.5, head.y, w * f, 2.0, c);
|
||||
let x = head.x - (cols as f32 * cw * 0.5).floor();
|
||||
ascii_bar(gfx, x, head.y - GH * small, cols, f, c, small);
|
||||
}
|
||||
if e.guard {
|
||||
gfx.glyph(0x0F, head.x - 3.0, head.y - 11.0, 1.0, Color::from_rgba(255, 220, 90, 255));
|
||||
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 {
|
||||
gfx.text_sh(&e.level.to_string(), head.x - w * 0.5 - 8.0, head.y - 3.0, 1.0, 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));
|
||||
}
|
||||
if matches!(e.job, Job::Sleep(_)) && !e.moving {
|
||||
let k = (t * 1.5 + e.anim) % 1.0;
|
||||
gfx.glyph(b'z', head.x + w * 0.3, head.y - 6.0 - k * 14.0, 2.0, Color::new(0.8, 0.85, 1.0, 1.0 - k));
|
||||
let rise = (k * 14.0 * F() as f32).floor();
|
||||
gfx.glyph(b'z', head.x + cw * 2.0, head.y - GH * S() - rise, S(), Color::new(0.8, 0.85, 1.0, 1.0 - k));
|
||||
}
|
||||
if e.carry > 0 {
|
||||
let p = Cam::project(m, vec3(e.x, 0.35, e.y));
|
||||
draw_circle(p.x + w * 0.35, p.y, 3.0, Color::from_rgba(250, 210, 60, 255));
|
||||
let p = proj(vec3(e.x, 0.35, e.y));
|
||||
gfx.glyph(0x0F, p.x + cw * 2.0, p.y - GH * small, small, Color::from_rgba(250, 210, 60, 255));
|
||||
}
|
||||
}
|
||||
for tw in &g.towers {
|
||||
if tw.hp < tw.max_hp() {
|
||||
let (x, z) = tw.pos();
|
||||
let p = Cam::project(m, vec3(x, tower_height(tw) + 0.9, z));
|
||||
let w = 40.0;
|
||||
let f = (tw.hp / tw.max_hp()).clamp(0.0, 1.0);
|
||||
draw_rectangle(p.x - w * 0.5 - 1.0, p.y - 1.0, w + 2.0, 5.0, BLACK);
|
||||
draw_rectangle(p.x - w * 0.5, p.y, w * f, 3.0, Color::from_rgba(255, 160, 60, 255));
|
||||
let p = proj(vec3(x, tower_height(tw) + 0.9, z));
|
||||
ascii_bar(gfx, p.x - (3.0 * cw).floor(), p.y, 6, tw.hp / tw.max_hp(), Color::from_rgba(255, 160, 60, 255), small);
|
||||
}
|
||||
}
|
||||
if g.gate_hp > 0.0 && g.gate_hp < GATE_HP {
|
||||
let (gx, gz) = g.gate_center();
|
||||
let p = Cam::project(m, vec3(gx, 2.4, gz));
|
||||
let w = 100.0;
|
||||
draw_rectangle(p.x - w * 0.5 - 1.0, p.y - 1.0, w + 2.0, 8.0, BLACK);
|
||||
draw_rectangle(p.x - w * 0.5, p.y, w * g.gate_hp / GATE_HP, 6.0, Color::from_rgba(120, 180, 255, 255));
|
||||
let p = proj(vec3(gx, 2.4, gz));
|
||||
let cols = 12;
|
||||
ascii_bar(gfx, p.x - (cols as f32 * GW * S() * 0.5).floor(), p.y, cols, g.gate_hp / GATE_HP, Color::from_rgba(120, 180, 255, 255), S());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user