Configurable map size (64-256, non-square), one portal per 64x64 tiles, guarded treasure caves, glowing dig-mark blocks
- New Game dialog with map width/height (steps of 32), remembered in settings - map dimensions are per game; renderer chunks, minimap and camera follow them - content scales with area; grace period grows with the distance to the Hero Gate - treasure caves with rich gold, guarded by hero squads that hold their post - marked blocks: translucent yellow box with outline on all edges - savegame v2: packed run-length encoded tiles (256x256 map ~320 KB), v1 still loads - tests: savegame round trip on 96x128, 256x256 generation/simulation/save Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
925be17e82
commit
f710a19351
12 files changed
+588
-137
No files matched your search
+88
-37
@@ -12,7 +12,6 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const WALL_H: f32 = 0.95;
|
||||
const CHUNK: i32 = 16;
|
||||
const NCH: i32 = MW as i32 / CHUNK;
|
||||
pub const MAX_LIGHTS: usize = 24;
|
||||
const ATLAS_W: f32 = 256.0;
|
||||
const ATLAS_H: f32 = 80.0;
|
||||
@@ -350,7 +349,7 @@ impl MB {
|
||||
|
||||
// Ambient light field: own territory is lit, foreign areas are dim
|
||||
fn ambient_field(map: &Map) -> Vec<f32> {
|
||||
let mut f = vec![0.0f32; N];
|
||||
let mut f = vec![0.0f32; n()];
|
||||
for (i, t) in map.t.iter().enumerate() {
|
||||
f[i] = match (t.solid(), t.owner) {
|
||||
(false, Owner::Player) => 1.0,
|
||||
@@ -363,8 +362,8 @@ fn ambient_field(map: &Map) -> Vec<f32> {
|
||||
}
|
||||
for _ in 0..2 {
|
||||
let src = f.clone();
|
||||
for y in 0..MH as i32 {
|
||||
for x in 0..MW as i32 {
|
||||
for y in 0..mh() as i32 {
|
||||
for x in 0..mw() as i32 {
|
||||
let mut s = 0.0;
|
||||
let mut n = 0.0;
|
||||
for dy in -1..=1 {
|
||||
@@ -383,11 +382,11 @@ fn ambient_field(map: &Map) -> Vec<f32> {
|
||||
}
|
||||
|
||||
fn amb_at(f: &[f32], x: f32, z: f32) -> f32 {
|
||||
let gx = (x - 0.5).clamp(0.0, MW as f32 - 1.001);
|
||||
let gz = (z - 0.5).clamp(0.0, MH as f32 - 1.001);
|
||||
let gx = (x - 0.5).clamp(0.0, mw() as f32 - 1.001);
|
||||
let gz = (z - 0.5).clamp(0.0, mh() as f32 - 1.001);
|
||||
let (x0, z0) = (gx.floor() as i32, gz.floor() as i32);
|
||||
let (fx, fz) = (gx - x0 as f32, gz - z0 as f32);
|
||||
let g = |x: i32, z: i32| f[idx(x.min(MW as i32 - 1), z.min(MH as i32 - 1))];
|
||||
let g = |x: i32, z: i32| f[idx(x.min(mw() as i32 - 1), z.min(mh() as i32 - 1))];
|
||||
let a = g(x0, z0) * (1.0 - fx) + g(x0 + 1, z0) * fx;
|
||||
let b = g(x0, z0 + 1) * (1.0 - fx) + g(x0 + 1, z0 + 1) * fx;
|
||||
0.15 + 0.45 * (a * (1.0 - fz) + b * fz)
|
||||
@@ -412,13 +411,48 @@ fn edge_pts(a: Vec3, b: Vec3, n: usize) -> Vec<Vec3> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn build_chunk(map: &Map, amb: &[f32], cx: i32, cz: i32, gold_fill: f32, atlas: &Texture2D, wall_sub: usize) -> (Option<Mesh>, Option<Mesh>, Option<Mesh>) {
|
||||
const MARK_FILL: [u8; 4] = [255, 205, 50, 70];
|
||||
const MARK_LINE: [u8; 4] = [255, 220, 70, 235];
|
||||
|
||||
// Glowing yellow box around a block that is marked for digging
|
||||
fn mark_block(mk: &mut MB, map: &Map, x: i32, z: i32) {
|
||||
let (x0, z0) = (x as f32 - 0.06, z as f32 - 0.06);
|
||||
let (x1, z1) = (x as f32 + 1.06, z as f32 + 1.06);
|
||||
let top = WALL_H + 0.16;
|
||||
let c = [x0, z0, x1, z1];
|
||||
let p = |xx: f32, y: f32, zz: f32| vec3(xx, y, zz);
|
||||
// top face
|
||||
mk.quad([p(c[0], top, c[1]), p(c[2], top, c[1]), p(c[2], top, c[3]), p(c[0], top, c[3])], [Vec2::ZERO; 4], [MARK_FILL; 4], [Vec3::Y; 4]);
|
||||
let corners = [p(c[0], 0.0, c[1]), p(c[2], 0.0, c[1]), p(c[2], 0.0, c[3]), p(c[0], 0.0, c[3])];
|
||||
// side faces towards open neighbours, outline on all edges
|
||||
for k in 0..4 {
|
||||
let a = corners[k];
|
||||
let b = corners[(k + 1) % 4];
|
||||
let (dx, dz) = DIR4[k];
|
||||
let up = vec3(0.0, top, 0.0);
|
||||
let out = vec3(dx as f32, 0.0, dz as f32);
|
||||
if !solid_at(map, x + dx, z + dz) {
|
||||
mk.quad([a, b, b + up, a + up], [Vec2::ZERO; 4], [MARK_FILL; 4], [out; 4]);
|
||||
mk.strip(a + vec3(0.0, 0.02, 0.0), b + vec3(0.0, 0.02, 0.0), out, 0.05, MARK_LINE);
|
||||
}
|
||||
mk.strip(a + up, b + up, Vec3::Y, 0.06, MARK_LINE);
|
||||
// vertical edge at this corner: down to the floor only where a side is open,
|
||||
// otherwise just the part that sticks out above the neighbouring walls
|
||||
let (pdx, pdz) = DIR4[(k + 3) % 4];
|
||||
let open_here = !solid_at(map, x + dx, z + dz) || !solid_at(map, x + pdx, z + pdz);
|
||||
let bottom = if open_here { 0.0 } else { WALL_H - 0.05 };
|
||||
mk.strip(a + vec3(0.0, bottom, 0.0), a + up, out, 0.05, MARK_LINE);
|
||||
}
|
||||
}
|
||||
|
||||
fn build_chunk(map: &Map, amb: &[f32], cx: i32, cz: i32, gold_fill: f32, atlas: &Texture2D, wall_sub: usize) -> [Option<Mesh>; 4] {
|
||||
let mut mb = MB::new();
|
||||
let mut ov = MB::new();
|
||||
let ov = MB::new(); // textured overlays (currently unused)
|
||||
let mut ln = MB::new();
|
||||
let mut mk = MB::new();
|
||||
let ld = LIGHT_DIR.normalize();
|
||||
for z in cz * CHUNK..(cz + 1) * CHUNK {
|
||||
for x in cx * CHUNK..(cx + 1) * CHUNK {
|
||||
for z in cz * CHUNK..((cz + 1) * CHUNK).min(mh() as i32) {
|
||||
for x in cx * CHUNK..((cx + 1) * CHUNK).min(mw() as i32) {
|
||||
let t = map.t[idx(x, z)];
|
||||
let tex = Gfx::tile_tex(t.kind, t.owner == Owner::Player, t.owner == Owner::Hero, t.var);
|
||||
let (ua, ub) = cell_uv(tex);
|
||||
@@ -510,14 +544,7 @@ fn build_chunk(map: &Map, amb: &[f32], cx: i32, cz: i32, gold_fill: f32, atlas:
|
||||
ln.polyline(&edge_pts(tr, br, n), Vec3::Y, OUTLINE_W, OUTLINE_C);
|
||||
}
|
||||
if t.marked {
|
||||
let (a, b) = cell_uv(T_MARK);
|
||||
let y = WALL_H + 0.12;
|
||||
ov.quad(
|
||||
[vec3(xf, y, zf), vec3(xf + 1.0, y, zf), vec3(xf + 1.0, y, zf + 1.0), vec3(xf, y, zf + 1.0)],
|
||||
[vec2(a.x, a.y), vec2(b.x, a.y), vec2(b.x, b.y), vec2(a.x, b.y)],
|
||||
[[255, 255, 255, 230]; 4],
|
||||
[Vec3::Y; 4],
|
||||
);
|
||||
mark_block(&mut mk, map, x, z);
|
||||
}
|
||||
for (d, &(dx, dz)) in DIR4.iter().enumerate() {
|
||||
if solid_at(map, x + dx, z + dz) {
|
||||
@@ -550,7 +577,7 @@ fn build_chunk(map: &Map, amb: &[f32], cx: i32, cz: i32, gold_fill: f32, atlas:
|
||||
}
|
||||
}
|
||||
}
|
||||
(mb.into_mesh(atlas), ov.into_mesh(atlas), ln.into_plain_mesh())
|
||||
[mb.into_mesh(atlas), ov.into_mesh(atlas), ln.into_plain_mesh(), mk.into_plain_mesh()]
|
||||
}
|
||||
|
||||
// Subdivided, displaced face. pos(u, v) gives the undisplaced point (u, v in 0..1).
|
||||
@@ -601,6 +628,7 @@ struct Chunk {
|
||||
mesh: Option<Mesh>,
|
||||
overlay: Option<Mesh>,
|
||||
lines: Option<Mesh>,
|
||||
marks: Option<Mesh>,
|
||||
}
|
||||
|
||||
pub struct Overlay {
|
||||
@@ -615,6 +643,8 @@ pub struct R3d {
|
||||
lit: Material,
|
||||
glow: Material,
|
||||
chunks: Vec<Chunk>,
|
||||
ncx: i32, // chunks per row / column for the current map size
|
||||
ncz: i32,
|
||||
pub outlines: bool, // tile outlines on/off
|
||||
wall_sub: usize, // wall face subdivisions (detail)
|
||||
pub max_lights: usize,
|
||||
@@ -647,8 +677,7 @@ fn chunk_sig(map: &Map, cx: i32, cz: i32, fill_bucket: u8) -> u64 {
|
||||
|
||||
impl R3d {
|
||||
pub fn new() -> R3d {
|
||||
let chunks = (0..NCH * NCH).map(|_| Chunk { sig: 0, mesh: None, overlay: None, lines: None }).collect();
|
||||
R3d { lit: lit_material(), glow: glow_material(), chunks, 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 }
|
||||
}
|
||||
|
||||
// Changing the wall detail rebuilds all chunks
|
||||
@@ -665,20 +694,37 @@ impl R3d {
|
||||
}
|
||||
}
|
||||
|
||||
fn update_chunks(&mut self, g: &Game, gfx: &Gfx) {
|
||||
// (Re)creates the chunk grid when the map size changes
|
||||
fn ensure_chunks(&mut self) {
|
||||
let ncx = (mw() as i32 + CHUNK - 1) / CHUNK;
|
||||
let ncz = (mh() as i32 + CHUNK - 1) / CHUNK;
|
||||
if ncx != self.ncx || ncz != self.ncz {
|
||||
self.ncx = ncx;
|
||||
self.ncz = ncz;
|
||||
self.chunks = (0..ncx * ncz).map(|_| Chunk { sig: 0, mesh: None, overlay: None, lines: None, marks: None }).collect();
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuilds visible chunks whose content changed (off-screen chunks are updated when they appear)
|
||||
fn update_chunks(&mut self, g: &Game, gfx: &Gfx, m: &Mat4) {
|
||||
self.ensure_chunks();
|
||||
let fill = (g.gold / g.gold_cap()).clamp(0.0, 1.0);
|
||||
let bucket = (fill * 6.0) as u8;
|
||||
let mut amb: Option<Vec<f32>> = None;
|
||||
for cz in 0..NCH {
|
||||
for cx in 0..NCH {
|
||||
for cz in 0..self.ncz {
|
||||
for cx in 0..self.ncx {
|
||||
if !Self::chunk_visible(m, cx, cz) {
|
||||
continue;
|
||||
}
|
||||
let sig = chunk_sig(&g.map, cx, cz, bucket);
|
||||
let ch = &mut self.chunks[(cz * NCH + cx) as usize];
|
||||
let ch = &mut self.chunks[(cz * self.ncx + cx) as usize];
|
||||
if ch.sig != sig {
|
||||
let a = amb.get_or_insert_with(|| ambient_field(&g.map));
|
||||
let (m, o, l) = build_chunk(&g.map, a, cx, cz, bucket as f32 / 6.0, &gfx.atlas, self.wall_sub);
|
||||
let [m, o, l, k] = build_chunk(&g.map, a, cx, cz, bucket as f32 / 6.0, &gfx.atlas, self.wall_sub);
|
||||
ch.mesh = m;
|
||||
ch.overlay = o;
|
||||
ch.lines = l;
|
||||
ch.marks = k;
|
||||
ch.sig = sig;
|
||||
}
|
||||
}
|
||||
@@ -718,9 +764,10 @@ impl R3d {
|
||||
let (cx, cy) = g.map.core;
|
||||
let hp = (g.core_hp / CORE_HP).clamp(0.0, 1.0);
|
||||
push(vec3(cx as f32 + 1.5, 1.2, cy as f32 + 1.5), 7.0, vec3(1.0, 0.25, 0.2), 1.1 + 0.2 * (t * 3.0).sin() * (1.5 - hp), 100.0);
|
||||
let (px, py) = g.map.portal;
|
||||
let owned = g.portal_owned();
|
||||
push(vec3(px as f32 + 1.5, 0.8, py as f32 + 1.5), 6.0, vec3(0.7, 0.3, 1.0), if owned { 1.0 } else { 0.35 }, 50.0);
|
||||
for (k, &(px, py)) in g.map.portals.iter().enumerate() {
|
||||
let owned = g.map.portal_owned(k);
|
||||
push(vec3(px as f32 + 1.5, 0.8, py as f32 + 1.5), 6.0, vec3(0.7, 0.3, 1.0), if owned { 1.0 } else { 0.35 }, 20.0);
|
||||
}
|
||||
if g.gate_hp > 0.0 {
|
||||
let (gx, gz) = g.gate_center();
|
||||
push(vec3(gx, 1.2, gz), 7.0, vec3(0.75, 0.85, 1.0), 1.0, 50.0);
|
||||
@@ -765,8 +812,8 @@ impl R3d {
|
||||
|
||||
pub fn draw(&mut self, g: &Game, gfx: &Gfx, cam: &Cam, time: f64, ov: &Overlay) {
|
||||
let t = time as f32;
|
||||
self.update_chunks(g, gfx);
|
||||
let m = cam.matrix();
|
||||
self.update_chunks(g, gfx, &m);
|
||||
EYE.with(|e| e.set(cam.eye_dir()));
|
||||
set_camera(&cam.camera());
|
||||
|
||||
@@ -777,12 +824,12 @@ impl R3d {
|
||||
self.lit.set_uniform_array("LightCol", &lc[..]);
|
||||
self.lit.set_uniform("LightCount", count);
|
||||
gl_use_material(&self.lit);
|
||||
for cz in 0..NCH {
|
||||
for cx in 0..NCH {
|
||||
for cz in 0..self.ncz {
|
||||
for cx in 0..self.ncx {
|
||||
if !Self::chunk_visible(&m, cx, cz) {
|
||||
continue;
|
||||
}
|
||||
if let Some(mesh) = &self.chunks[(cz * NCH + cx) as usize].mesh {
|
||||
if let Some(mesh) = &self.chunks[(cz * self.ncx + cx) as usize].mesh {
|
||||
draw_mesh(mesh);
|
||||
}
|
||||
}
|
||||
@@ -810,6 +857,9 @@ impl R3d {
|
||||
if let Some(o) = &c.overlay {
|
||||
draw_mesh(o);
|
||||
}
|
||||
if let Some(k) = &c.marks {
|
||||
draw_mesh(k);
|
||||
}
|
||||
}
|
||||
for (k, tr) in g.torches.iter().enumerate() {
|
||||
let (x, z) = tr.pos();
|
||||
@@ -1008,9 +1058,9 @@ fn draw_landmarks_glow(g: &Game, t: f32) {
|
||||
draw_sphere(c, 0.75 * pulse, None, Color::new(0.4 + 0.5 * hp, 0.05, 0.08, 1.0));
|
||||
draw_sphere(c, 0.95 * pulse, None, Color::new(1.0, 0.2, 0.15, 0.25));
|
||||
draw_sphere(c + vec3(-0.2, 0.25, 0.2), 0.18, None, Color::new(1.0, 0.85, 0.7, 0.8));
|
||||
// portal
|
||||
let (px, py) = g.map.portal;
|
||||
let owned = g.portal_owned();
|
||||
// portals
|
||||
for (pk, &(px, py)) in g.map.portals.iter().enumerate() {
|
||||
let owned = g.map.portal_owned(pk);
|
||||
let pc = vec3(px as f32 + 1.5, 0.03, py as f32 + 1.5);
|
||||
let arm = if owned { Color::new(0.8, 0.4, 1.0, 1.0) } else { Color::new(0.45, 0.42, 0.5, 1.0) };
|
||||
disc(pc, 1.35, if owned { Color::new(0.2, 0.05, 0.3, 0.9) } else { Color::new(0.1, 0.1, 0.12, 0.9) }, 32);
|
||||
@@ -1024,6 +1074,7 @@ fn draw_landmarks_glow(g: &Game, t: f32) {
|
||||
draw_cube(p, Vec3::splat(0.05 + 0.06 * (1.0 - f)), None, Color::new(arm.r, arm.g, arm.b, 0.4 + 0.6 * (1.0 - f)));
|
||||
}
|
||||
}
|
||||
}
|
||||
// gate light
|
||||
if g.gate_hp > 0.0 {
|
||||
let (gx, gz) = g.gate_center();
|
||||
|
||||
Reference in new issue
Block a user