1462 lines
58 KiB
Rust
1462 lines
58 KiB
Rust
// 3D world renderer: isometric (orthographic) camera with free rotation and tilt,
|
|
// chunked tile meshes with irregular (noise-displaced) walls, a lit material with
|
|
// flickering point lights (torches, core, portal, gate), billboard sprites and 3D effects.
|
|
|
|
use crate::game::*;
|
|
use crate::gfx::*;
|
|
use crate::map::*;
|
|
use macroquad::miniquad::{BlendFactor, BlendState, BlendValue, Comparison, Equation, PipelineParams, UniformDesc, UniformType};
|
|
use macroquad::models::{Mesh, Vertex};
|
|
use macroquad::prelude::*;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
pub const WALL_H: f32 = 0.95;
|
|
const CHUNK: i32 = 16;
|
|
pub const MAX_LIGHTS: usize = 24;
|
|
const ATLAS_W: f32 = 256.0;
|
|
const ATLAS_H: f32 = 96.0;
|
|
const DISP_AMP: f32 = 0.11; // wall surface irregularity
|
|
|
|
// ------------------------------------------------------------------ camera
|
|
|
|
#[derive(Clone, Serialize, Deserialize)]
|
|
pub struct Cam {
|
|
pub tx: f32, // look-at target on the ground (world x)
|
|
pub tz: f32, // look-at target on the ground (world z = map y)
|
|
pub yaw: f32,
|
|
pub pitch: f32,
|
|
pub zoom: f32, // visible world height in tiles (orthographic)
|
|
}
|
|
|
|
impl Cam {
|
|
pub fn new(tx: f32, tz: f32) -> Cam {
|
|
Cam { tx, tz, yaw: 0.785, pitch: 0.6155, zoom: 18.0 }
|
|
}
|
|
|
|
pub fn eye_dir(&self) -> Vec3 {
|
|
vec3(self.pitch.cos() * self.yaw.sin(), self.pitch.sin(), self.pitch.cos() * self.yaw.cos())
|
|
}
|
|
|
|
fn target(&self) -> Vec3 {
|
|
vec3(self.tx, 0.0, self.tz)
|
|
}
|
|
|
|
pub fn camera(&self) -> Camera3D {
|
|
Camera3D {
|
|
position: self.target() + self.eye_dir() * 60.0,
|
|
target: self.target(),
|
|
up: Vec3::Y,
|
|
fovy: self.zoom,
|
|
projection: Projection::Orthographics,
|
|
z_near: 1.0,
|
|
z_far: 200.0,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
// forward (into the screen), right, up
|
|
fn basis(&self) -> (Vec3, Vec3, Vec3) {
|
|
let f = -self.eye_dir();
|
|
let r = f.cross(Vec3::Y).normalize();
|
|
let u = r.cross(f);
|
|
(f, r, u)
|
|
}
|
|
|
|
// Ground-plane directions for panning: screen-right and screen-up
|
|
pub fn ground_axes(&self) -> (Vec2, Vec2) {
|
|
let (f, r, _) = self.basis();
|
|
(vec2(r.x, r.z).normalize(), vec2(f.x, f.z).normalize())
|
|
}
|
|
|
|
pub fn ray(&self, sx: f32, sy: f32) -> (Vec3, Vec3) {
|
|
let (f, r, u) = self.basis();
|
|
let half_h = self.zoom * 0.5;
|
|
let half_w = half_h * screen_width() / screen_height();
|
|
let nx = sx / screen_width() * 2.0 - 1.0;
|
|
let ny = 1.0 - sy / screen_height() * 2.0;
|
|
let pos = self.target() + self.eye_dir() * 60.0;
|
|
(pos + r * (nx * half_w) + u * (ny * half_h), f)
|
|
}
|
|
|
|
pub fn ground_point(&self, sx: f32, sy: f32) -> Vec2 {
|
|
let (o, d) = self.ray(sx, sy);
|
|
let t = -o.y / d.y.min(-0.001);
|
|
let p = o + d * t;
|
|
vec2(p.x, p.z)
|
|
}
|
|
|
|
pub fn matrix(&self) -> Mat4 {
|
|
self.camera().matrix()
|
|
}
|
|
|
|
pub fn project(m: &Mat4, p: Vec3) -> Vec2 {
|
|
let c = *m * p.extend(1.0);
|
|
let n = c.truncate() / c.w;
|
|
vec2((n.x + 1.0) * 0.5 * screen_width(), (1.0 - n.y) * 0.5 * screen_height())
|
|
}
|
|
|
|
// Tile under the mouse: wall tops/sides first, otherwise the floor
|
|
pub fn pick_tile(&self, map: &Map, sx: f32, sy: f32) -> Option<(i32, i32, Vec3)> {
|
|
let (o, d) = self.ray(sx, sy);
|
|
if d.y >= -0.001 {
|
|
return None;
|
|
}
|
|
let top = WALL_H + 0.3;
|
|
let t0 = (o.y - top) / -d.y;
|
|
let t1 = o.y / -d.y;
|
|
let mut t = t0;
|
|
while t <= t1 {
|
|
let p = o + d * t;
|
|
let (x, z) = (p.x.floor() as i32, p.z.floor() as i32);
|
|
if inb(x, z) && map.t[idx(x, z)].solid() && p.y <= WALL_H {
|
|
return Some((x, z, p));
|
|
}
|
|
t += 0.03;
|
|
}
|
|
let p = o + d * t1;
|
|
let (x, z) = (p.x.floor() as i32, p.z.floor() as i32);
|
|
if inb(x, z) {
|
|
Some((x, z, p))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------ noise
|
|
|
|
fn h3(x: i32, y: i32, z: i32, s: u32) -> f32 {
|
|
let mut h = (x as u32).wrapping_mul(374761393) ^ (y as u32).wrapping_mul(668265263) ^ (z as u32).wrapping_mul(2147483647) ^ s.wrapping_mul(2246822519);
|
|
h = (h ^ (h >> 13)).wrapping_mul(1274126177);
|
|
h ^= h >> 16;
|
|
(h & 0xffff) as f32 / 32767.5 - 1.0
|
|
}
|
|
|
|
fn vnoise(p: Vec3, s: u32) -> f32 {
|
|
let (x0, y0, z0) = (p.x.floor(), p.y.floor(), p.z.floor());
|
|
let (fx, fy, fz) = (p.x - x0, p.y - y0, p.z - z0);
|
|
let sm = |t: f32| t * t * (3.0 - 2.0 * t);
|
|
let (ux, uy, uz) = (sm(fx), sm(fy), sm(fz));
|
|
let (xi, yi, zi) = (x0 as i32, y0 as i32, z0 as i32);
|
|
let mut acc = 0.0;
|
|
for dz in 0..2 {
|
|
for dy in 0..2 {
|
|
for dx in 0..2 {
|
|
let w = (if dx == 1 { ux } else { 1.0 - ux }) * (if dy == 1 { uy } else { 1.0 - uy }) * (if dz == 1 { uz } else { 1.0 - uz });
|
|
acc += w * h3(xi + dx, yi + dy, zi + dz, s);
|
|
}
|
|
}
|
|
}
|
|
acc
|
|
}
|
|
|
|
// Displacement depends only on the world position -> seamless between neighbours
|
|
fn displace(p: Vec3) -> Vec3 {
|
|
let q = p * 2.3;
|
|
let r = p * 5.1;
|
|
let n = |s: u32| vnoise(q, s) + 0.35 * vnoise(r, s + 10);
|
|
let dy = if p.y < 0.01 { 0.0 } else { n(2) * 0.6 };
|
|
vec3(n(1), dy, n(3)) * DISP_AMP
|
|
}
|
|
|
|
// ------------------------------------------------------------------ lit material
|
|
|
|
const VERTEX: &str = r#"#version 100
|
|
attribute vec3 position;
|
|
attribute vec2 texcoord;
|
|
attribute vec4 color0;
|
|
attribute vec4 normal;
|
|
|
|
varying lowp vec2 uv;
|
|
varying lowp vec4 color;
|
|
varying highp vec3 wpos;
|
|
varying mediump vec3 wnorm;
|
|
|
|
uniform mat4 Model;
|
|
uniform mat4 Projection;
|
|
|
|
void main() {
|
|
vec4 w = Model * vec4(position, 1.0);
|
|
wpos = w.xyz;
|
|
wnorm = normal.xyz;
|
|
color = color0 / 255.0;
|
|
uv = texcoord;
|
|
gl_Position = Projection * w;
|
|
}
|
|
"#;
|
|
|
|
const FRAGMENT: &str = r#"#version 100
|
|
precision mediump float;
|
|
varying lowp vec2 uv;
|
|
varying lowp vec4 color;
|
|
varying highp vec3 wpos;
|
|
varying mediump vec3 wnorm;
|
|
|
|
uniform sampler2D Texture;
|
|
uniform vec4 LightPos[24];
|
|
uniform vec4 LightCol[24];
|
|
uniform float LightCount;
|
|
|
|
void main() {
|
|
vec4 tex = texture2D(Texture, uv);
|
|
if (tex.a < 0.5) discard;
|
|
vec3 n = normalize(wnorm);
|
|
vec3 light = color.rgb;
|
|
for (int i = 0; i < 24; i++) {
|
|
if (float(i) >= LightCount) break;
|
|
vec3 d = LightPos[i].xyz - wpos;
|
|
float dist = length(d);
|
|
float att = clamp(1.0 - dist / LightPos[i].w, 0.0, 1.0);
|
|
att *= att;
|
|
float ndl = max(dot(n, d / max(dist, 0.001)), 0.0) * 0.8 + 0.2;
|
|
light += LightCol[i].rgb * LightCol[i].a * att * ndl;
|
|
}
|
|
gl_FragColor = vec4(tex.rgb * min(light, vec3(1.7)), 1.0);
|
|
}
|
|
"#;
|
|
|
|
fn lit_material() -> Material {
|
|
load_material(
|
|
ShaderSource::Glsl { vertex: VERTEX, fragment: FRAGMENT },
|
|
MaterialParams {
|
|
pipeline_params: PipelineParams { depth_test: Comparison::LessOrEqual, depth_write: true, ..Default::default() },
|
|
uniforms: vec![
|
|
UniformDesc::new("LightPos", UniformType::Float4).array(MAX_LIGHTS),
|
|
UniformDesc::new("LightCol", UniformType::Float4).array(MAX_LIGHTS),
|
|
UniformDesc::new("LightCount", UniformType::Float1),
|
|
],
|
|
textures: vec![],
|
|
},
|
|
)
|
|
.expect("lit material")
|
|
}
|
|
|
|
// Unlit, alpha-blended material for glows, shadows and effects
|
|
fn glow_material() -> Material {
|
|
const V: &str = r#"#version 100
|
|
attribute vec3 position;
|
|
attribute vec2 texcoord;
|
|
attribute vec4 color0;
|
|
attribute vec4 normal;
|
|
varying lowp vec2 uv;
|
|
varying lowp vec4 color;
|
|
uniform mat4 Model;
|
|
uniform mat4 Projection;
|
|
void main() {
|
|
gl_Position = Projection * Model * vec4(position, 1.0);
|
|
color = color0 / 255.0;
|
|
uv = texcoord;
|
|
}
|
|
"#;
|
|
const F: &str = r#"#version 100
|
|
precision mediump float;
|
|
varying lowp vec4 color;
|
|
varying lowp vec2 uv;
|
|
uniform sampler2D Texture;
|
|
void main() {
|
|
gl_FragColor = color * texture2D(Texture, uv);
|
|
}
|
|
"#;
|
|
load_material(
|
|
ShaderSource::Glsl { vertex: V, fragment: F },
|
|
MaterialParams {
|
|
pipeline_params: PipelineParams {
|
|
depth_test: Comparison::LessOrEqual,
|
|
// miniquad only enables the depth *test* together with depth writes, so this
|
|
// material writes depth too; large translucent glows are therefore drawn last
|
|
depth_write: true,
|
|
color_blend: Some(BlendState::new(Equation::Add, BlendFactor::Value(BlendValue::SourceAlpha), BlendFactor::OneMinusValue(BlendValue::SourceAlpha))),
|
|
..Default::default()
|
|
},
|
|
uniforms: vec![],
|
|
textures: vec![],
|
|
},
|
|
)
|
|
.expect("glow material")
|
|
}
|
|
|
|
// ------------------------------------------------------------------ mesh building
|
|
|
|
struct MB {
|
|
v: Vec<Vertex>,
|
|
i: Vec<u16>,
|
|
}
|
|
|
|
fn col(c: Vec3, a: f32) -> [u8; 4] {
|
|
[(c.x.clamp(0.0, 1.0) * 255.0) as u8, (c.y.clamp(0.0, 1.0) * 255.0) as u8, (c.z.clamp(0.0, 1.0) * 255.0) as u8, (a.clamp(0.0, 1.0) * 255.0) as u8]
|
|
}
|
|
|
|
fn cell_uv(id: u32) -> (Vec2, Vec2) {
|
|
let r = Gfx::cell(id);
|
|
let inset = 0.3;
|
|
(vec2((r.x + inset) / ATLAS_W, (r.y + inset) / ATLAS_H), vec2((r.x + r.w - inset) / ATLAS_W, (r.y + r.h - inset) / ATLAS_H))
|
|
}
|
|
|
|
impl MB {
|
|
fn new() -> MB {
|
|
MB { v: Vec::new(), i: Vec::new() }
|
|
}
|
|
fn vert(&mut self, p: Vec3, uv: Vec2, c: [u8; 4], n: Vec3) -> u16 {
|
|
self.v.push(Vertex { position: p, uv, color: c, normal: n.extend(0.0) });
|
|
(self.v.len() - 1) as u16
|
|
}
|
|
fn quad(&mut self, p: [Vec3; 4], uv: [Vec2; 4], c: [[u8; 4]; 4], n: [Vec3; 4]) {
|
|
let b = self.v.len() as u16;
|
|
for k in 0..4 {
|
|
self.vert(p[k], uv[k], c[k], n[k]);
|
|
}
|
|
self.i.extend_from_slice(&[b, b + 1, b + 2, b, b + 2, b + 3]);
|
|
}
|
|
// Two crossed vertical quads (decor, billboards in the mesh)
|
|
fn cross(&mut self, cx: f32, cz: f32, w: f32, h: f32, cell: u32, c: [u8; 4]) {
|
|
let (a, b) = cell_uv(cell);
|
|
let n = Vec3::Y;
|
|
for k in 0..2 {
|
|
let (dx, dz) = if k == 0 { (w * 0.5, 0.0) } else { (0.0, w * 0.5) };
|
|
self.quad(
|
|
[vec3(cx - dx, h, cz - dz), vec3(cx + dx, h, cz + dz), vec3(cx + dx, 0.0, cz + dz), vec3(cx - dx, 0.0, cz - dz)],
|
|
[vec2(a.x, a.y), vec2(b.x, a.y), vec2(b.x, b.y), vec2(a.x, b.y)],
|
|
[c; 4],
|
|
[n; 4],
|
|
);
|
|
}
|
|
}
|
|
// Thin strip lying on a surface (normal n) from p to q, used for tile outlines
|
|
fn strip(&mut self, p: Vec3, q: Vec3, n: Vec3, w: f32, c: [u8; 4]) {
|
|
let side = (q - p).cross(n).normalize_or_zero() * (w * 0.5);
|
|
let o = n * 0.006;
|
|
self.quad([p - side + o, q - side + o, q + side + o, p + side + o], [Vec2::ZERO; 4], [c; 4], [n; 4]);
|
|
}
|
|
// Polyline along displaced points
|
|
fn polyline(&mut self, pts: &[Vec3], n: Vec3, w: f32, c: [u8; 4]) {
|
|
for k in 0..pts.len().saturating_sub(1) {
|
|
self.strip(pts[k], pts[k + 1], n, w, c);
|
|
}
|
|
}
|
|
fn into_plain_mesh(self) -> Option<Mesh> {
|
|
if self.v.is_empty() {
|
|
None
|
|
} else {
|
|
Some(Mesh { vertices: self.v, indices: self.i, texture: None })
|
|
}
|
|
}
|
|
fn into_mesh(self, tex: &Texture2D) -> Option<Mesh> {
|
|
if self.v.is_empty() {
|
|
None
|
|
} else {
|
|
Some(Mesh { vertices: self.v, indices: self.i, texture: Some(tex.clone()) })
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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()];
|
|
for (i, t) in map.t.iter().enumerate() {
|
|
f[i] = match (t.solid(), t.owner) {
|
|
(false, Owner::Player) => 1.0,
|
|
(false, Owner::Hero) => 0.6,
|
|
_ => 0.0,
|
|
};
|
|
if t.kind == Kind::Core {
|
|
f[i] = 1.0;
|
|
}
|
|
}
|
|
for _ in 0..2 {
|
|
let src = f.clone();
|
|
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 {
|
|
for dx in -1..=1 {
|
|
if inb(x + dx, y + dy) {
|
|
s += src[idx(x + dx, y + dy)];
|
|
n += 1.0;
|
|
}
|
|
}
|
|
}
|
|
f[idx(x, y)] = (s / n).max(src[idx(x, y)] * 0.85);
|
|
}
|
|
}
|
|
}
|
|
f
|
|
}
|
|
|
|
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 (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 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)
|
|
}
|
|
|
|
const LIGHT_DIR: Vec3 = Vec3::new(0.45, 0.8, 0.35);
|
|
|
|
fn solid_at(map: &Map, x: i32, z: i32) -> bool {
|
|
map.get(x, z).map_or(true, |t| t.solid())
|
|
}
|
|
|
|
const OUTLINE_W: f32 = 0.035;
|
|
const OUTLINE_C: [u8; 4] = [8, 5, 6, 150];
|
|
|
|
// Displaced points along a straight edge (same displacement as the wall grids -> lines follow the surface)
|
|
fn edge_pts(a: Vec3, b: Vec3, n: usize) -> Vec<Vec3> {
|
|
(0..=n)
|
|
.map(|k| {
|
|
let p = a.lerp(b, k as f32 / n as f32);
|
|
p + displace(p)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
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, n: usize) {
|
|
block_shell(mk, map, x, z, n, MARK_FILL, MARK_LINE, 1.0, 0.03);
|
|
}
|
|
|
|
// Colored, slightly lifted copy of a displaced face (same grid and displacement as the wall)
|
|
fn shell_face(mk: &mut MB, pos: impl Fn(f32, f32) -> Vec3, face_n: Vec3, n: usize, fill: [u8; 4], lift: f32) {
|
|
let m = n + 1;
|
|
let mut p = Vec::with_capacity(m * m);
|
|
for j in 0..m {
|
|
for i in 0..m {
|
|
let base = pos(i as f32 / n as f32, j as f32 / n as f32);
|
|
p.push(base + displace(base) + face_n * lift);
|
|
}
|
|
}
|
|
let b = mk.v.len() as u16;
|
|
for &q in &p {
|
|
mk.vert(q, Vec2::ZERO, fill, face_n);
|
|
}
|
|
for j in 0..n {
|
|
for i in 0..n {
|
|
let a = b + (j * m + i) as u16;
|
|
mk.i.extend_from_slice(&[a, a + 1, a + m as u16 + 1, a, a + m as u16 + 1, a + m as u16]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Highlight that follows the block's real (irregular) shape: tinted top and open sides,
|
|
// outline along the displaced edges. lw = line width factor, lift = distance off the surface.
|
|
fn block_shell(mk: &mut MB, map: &Map, x: i32, z: i32, n: usize, fill: [u8; 4], line: [u8; 4], lw: f32, lift: f32) {
|
|
let (xf, zf) = (x as f32, z as f32);
|
|
let up = Vec3::Y * lift;
|
|
shell_face(mk, |u, v| vec3(xf + u, WALL_H, zf + v), Vec3::Y, n, fill, lift);
|
|
let tops = [vec3(xf, WALL_H, zf), vec3(xf + 1.0, WALL_H, zf), vec3(xf + 1.0, WALL_H, zf + 1.0), vec3(xf, WALL_H, zf + 1.0)];
|
|
for k in 0..4 {
|
|
let (dx, dz) = DIR4[k];
|
|
let out = vec3(dx as f32, 0.0, dz as f32);
|
|
let (ta, tb) = (tops[k], tops[(k + 1) % 4]);
|
|
// top edge (lifted a bit outwards so it sits on the rim)
|
|
let rim: Vec<Vec3> = edge_pts(ta, tb, n).into_iter().map(|q| q + up + out * lift).collect();
|
|
mk.polyline(&rim, Vec3::Y, 0.06 * lw, line);
|
|
let open = !solid_at(map, x + dx, z + dz);
|
|
if open {
|
|
let (ga, gb) = (ta - Vec3::Y * WALL_H, tb - Vec3::Y * WALL_H);
|
|
shell_face(mk, |u, v| ga + (gb - ga) * u + vec3(0.0, WALL_H * (1.0 - v), 0.0), out, n, fill, lift);
|
|
let foot: Vec<Vec3> = edge_pts(ga, gb, n).into_iter().map(|q| q + out * lift + Vec3::Y * 0.02).collect();
|
|
mk.polyline(&foot, out, 0.05 * lw, line);
|
|
}
|
|
// vertical edge at corner ta: only where one of the two sides meeting there is open
|
|
let (pdx, pdz) = DIR4[(k + 3) % 4];
|
|
if open || !solid_at(map, x + pdx, z + pdz) {
|
|
let g = ta - Vec3::Y * WALL_H;
|
|
let pout = vec3(pdx as f32, 0.0, pdz as f32);
|
|
let col: Vec<Vec3> = edge_pts(g, ta, n).into_iter().map(|q| q + (out + pout) * lift).collect();
|
|
mk.polyline(&col, (out + pout).normalize(), 0.05 * lw, 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 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).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, t.reinforced);
|
|
let (ua, ub) = cell_uv(tex);
|
|
let (xf, zf) = (x as f32, z as f32);
|
|
if !t.solid() {
|
|
// floor with corner ambient occlusion
|
|
let corners = [(0, 0), (1, 0), (1, 1), (0, 1)];
|
|
let mut c = [[0u8; 4]; 4];
|
|
for (k, &(ox, oz)) in corners.iter().enumerate() {
|
|
let (vx, vz) = (x + ox, z + oz);
|
|
let solid_n = [(vx - 1, vz - 1), (vx, vz - 1), (vx - 1, vz), (vx, vz)].iter().filter(|&&(a, b)| solid_at(map, a, b)).count();
|
|
let ao = 1.0 - 0.17 * solid_n as f32;
|
|
let a = amb_at(amb, vx as f32, vz as f32) * ao;
|
|
c[k] = col(Vec3::splat(a), 1.0);
|
|
}
|
|
mb.quad(
|
|
[vec3(xf, 0.0, zf), vec3(xf + 1.0, 0.0, zf), vec3(xf + 1.0, 0.0, zf + 1.0), vec3(xf, 0.0, zf + 1.0)],
|
|
[vec2(ua.x, ua.y), vec2(ub.x, ua.y), vec2(ub.x, ub.y), vec2(ua.x, ub.y)],
|
|
c,
|
|
[Vec3::Y; 4],
|
|
);
|
|
// outlines: north and west edge, only between two open tiles (each edge once)
|
|
if !solid_at(map, x, z - 1) {
|
|
ln.strip(vec3(xf, 0.0, zf), vec3(xf + 1.0, 0.0, zf), Vec3::Y, OUTLINE_W, OUTLINE_C);
|
|
}
|
|
if !solid_at(map, x - 1, z) {
|
|
ln.strip(vec3(xf, 0.0, zf), vec3(xf, 0.0, zf + 1.0), Vec3::Y, OUTLINE_W, OUTLINE_C);
|
|
}
|
|
// room decor
|
|
let dc = col(Vec3::splat(amb_at(amb, xf + 0.5, zf + 0.5)), 1.0);
|
|
match t.kind {
|
|
Kind::Lair => {
|
|
let (a, b) = cell_uv(D_BED);
|
|
mb.quad(
|
|
[vec3(xf + 0.05, 0.02, zf + 0.05), vec3(xf + 0.95, 0.02, zf + 0.05), vec3(xf + 0.95, 0.02, zf + 0.95), vec3(xf + 0.05, 0.02, zf + 0.95)],
|
|
[vec2(a.x, a.y), vec2(b.x, a.y), vec2(b.x, b.y), vec2(a.x, b.y)],
|
|
[dc; 4],
|
|
[Vec3::Y; 4],
|
|
);
|
|
}
|
|
Kind::Farm => {
|
|
let pos = [(0.3, 0.3), (0.7, 0.4), (0.45, 0.72)];
|
|
for (k, &(ox, oz)) in pos.iter().enumerate() {
|
|
if t.food >= k as f32 + 1.0 {
|
|
mb.cross(xf + ox, zf + oz, 0.45, 0.45, D_MUSHROOM, dc);
|
|
}
|
|
}
|
|
}
|
|
Kind::Training if (x + z) % 2 == 0 => mb.cross(xf + 0.5, zf + 0.5, 0.7, 0.75, D_DUMMY, dc),
|
|
Kind::Library if z % 2 == 0 => mb.cross(xf + 0.5, zf + 0.5, 0.9, 0.85, D_SHELF, dc),
|
|
Kind::Torture if (x + z) % 2 == 1 => mb.cross(xf + 0.5, zf + 0.5, 0.8, 0.8, D_RACK, dc),
|
|
Kind::Temple if x % 3 == 1 && z % 3 == 1 => mb.cross(xf + 0.5, zf + 0.5, 0.8, 0.8, D_ALTAR, dc),
|
|
Kind::Treasury if gold_fill > 0.02 => {
|
|
let s = 0.35 + 0.55 * gold_fill;
|
|
mb.cross(xf + 0.5, zf + 0.5, s, s * 0.8, D_COINS, dc);
|
|
}
|
|
_ => {}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// ---- wall: dark floor below (fills gaps), bumpy top, irregular open sides
|
|
let dark = col(Vec3::splat(0.04), 1.0);
|
|
mb.quad(
|
|
[vec3(xf, -0.01, zf), vec3(xf + 1.0, -0.01, zf), vec3(xf + 1.0, -0.01, zf + 1.0), vec3(xf, -0.01, zf + 1.0)],
|
|
[ua, ua, ua, ua],
|
|
[dark; 4],
|
|
[Vec3::Y; 4],
|
|
);
|
|
let n = wall_sub;
|
|
let top_amb = amb_at(amb, xf + 0.5, zf + 0.5) * 0.95 + 0.1;
|
|
build_grid(
|
|
&mut mb,
|
|
|u, v| vec3(xf + u, WALL_H, zf + v),
|
|
Vec3::Y,
|
|
n,
|
|
(ua, ub),
|
|
|_p, nrm| Vec3::splat(top_amb * (0.75 + 0.25 * nrm.dot(ld).max(0.0))),
|
|
);
|
|
// outlines on the wall top: north/west always, south/east where no wall continues
|
|
let tl = vec3(xf, WALL_H, zf);
|
|
let tr = vec3(xf + 1.0, WALL_H, zf);
|
|
let br = vec3(xf + 1.0, WALL_H, zf + 1.0);
|
|
let bl = vec3(xf, WALL_H, zf + 1.0);
|
|
ln.polyline(&edge_pts(tl, tr, n), Vec3::Y, OUTLINE_W, OUTLINE_C);
|
|
ln.polyline(&edge_pts(tl, bl, n), Vec3::Y, OUTLINE_W, OUTLINE_C);
|
|
if !solid_at(map, x, z + 1) {
|
|
ln.polyline(&edge_pts(bl, br, n), Vec3::Y, OUTLINE_W, OUTLINE_C);
|
|
}
|
|
if !solid_at(map, x + 1, z) {
|
|
ln.polyline(&edge_pts(tr, br, n), Vec3::Y, OUTLINE_W, OUTLINE_C);
|
|
}
|
|
// vertex budget: one draw call holds at most 40000 vertices
|
|
if t.marked && mk.v.len() < 34000 {
|
|
mark_block(&mut mk, map, x, z, n);
|
|
}
|
|
for (d, &(dx, dz)) in DIR4.iter().enumerate() {
|
|
if solid_at(map, x + dx, z + dz) {
|
|
continue;
|
|
}
|
|
let side_amb = amb_at(amb, xf + 0.5 + dx as f32 * 0.9, zf + 0.5 + dz as f32 * 0.9);
|
|
// edge from a to b as seen from outside, u along the edge, v = height
|
|
let (a, b) = match d {
|
|
0 => (vec3(xf + 1.0, 0.0, zf), vec3(xf, 0.0, zf)), // north
|
|
1 => (vec3(xf + 1.0, 0.0, zf + 1.0), vec3(xf + 1.0, 0.0, zf)), // east
|
|
2 => (vec3(xf, 0.0, zf + 1.0), vec3(xf + 1.0, 0.0, zf + 1.0)), // south
|
|
_ => (vec3(xf, 0.0, zf), vec3(xf, 0.0, zf + 1.0)), // west
|
|
};
|
|
let out = vec3(dx as f32, 0.0, dz as f32);
|
|
// vertical outlines at both ends of the exposed face (tile borders and corners)
|
|
for end in [a, b] {
|
|
ln.polyline(&edge_pts(end, end + vec3(0.0, WALL_H, 0.0), n), out, OUTLINE_W, OUTLINE_C);
|
|
}
|
|
build_grid(
|
|
&mut mb,
|
|
|u, v| a + (b - a) * u + vec3(0.0, WALL_H * (1.0 - v), 0.0),
|
|
out,
|
|
n,
|
|
(ua, ub),
|
|
|p, nrm| {
|
|
let h = (p.y / WALL_H).clamp(0.0, 1.0);
|
|
Vec3::splat(side_amb * (0.5 + 0.5 * nrm.dot(ld).max(0.0)) * (0.55 + 0.45 * h))
|
|
},
|
|
);
|
|
}
|
|
}
|
|
}
|
|
[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).
|
|
fn build_grid(mb: &mut MB, pos: impl Fn(f32, f32) -> Vec3, face_n: Vec3, n: usize, uv: (Vec2, Vec2), shade: impl Fn(Vec3, Vec3) -> Vec3) {
|
|
let m = n + 1;
|
|
let mut p = Vec::with_capacity(m * m);
|
|
for j in 0..m {
|
|
for i in 0..m {
|
|
let base = pos(i as f32 / n as f32, j as f32 / n as f32);
|
|
p.push(base + displace(base));
|
|
}
|
|
}
|
|
let at = |i: usize, j: usize| p[j * m + i];
|
|
let base_idx = mb.v.len() as u16;
|
|
for j in 0..m {
|
|
for i in 0..m {
|
|
let du = at((i + 1).min(n), j) - at(i.saturating_sub(1), j);
|
|
let dv = at(i, (j + 1).min(n)) - at(i, j.saturating_sub(1));
|
|
let mut nrm = du.cross(dv).normalize_or_zero();
|
|
if nrm.dot(face_n) < 0.0 {
|
|
nrm = -nrm;
|
|
}
|
|
if nrm == Vec3::ZERO {
|
|
nrm = face_n;
|
|
}
|
|
let u = i as f32 / n as f32;
|
|
let v = j as f32 / n as f32;
|
|
let tuv = vec2(uv.0.x + (uv.1.x - uv.0.x) * u, uv.0.y + (uv.1.y - uv.0.y) * v);
|
|
let c = shade(at(i, j), nrm);
|
|
mb.vert(at(i, j), tuv, col(c, 1.0), nrm);
|
|
}
|
|
}
|
|
for j in 0..n {
|
|
for i in 0..n {
|
|
let a = base_idx + (j * m + i) as u16;
|
|
let b = a + 1;
|
|
let c = a + m as u16 + 1;
|
|
let d = a + m as u16;
|
|
mb.i.extend_from_slice(&[a, b, c, a, c, d]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------ renderer
|
|
|
|
struct Chunk {
|
|
sig: u64,
|
|
mesh: Option<Mesh>,
|
|
overlay: Option<Mesh>,
|
|
lines: Option<Mesh>,
|
|
marks: Option<Mesh>,
|
|
}
|
|
|
|
pub struct Overlay {
|
|
pub hover: Option<(i32, i32)>,
|
|
pub rect: Option<(i32, i32, i32, i32)>,
|
|
pub color: Color,
|
|
pub radius: Option<(f32, f32, f32)>,
|
|
pub high: bool, // outline on wall tops (digging)
|
|
}
|
|
|
|
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,
|
|
pub render_div: u32, // 3D resolution divisor (pixel look)
|
|
target: Option<(RenderTarget, u32, u32)>, // low resolution render target
|
|
}
|
|
|
|
fn rgba(u: u32) -> Color {
|
|
Color::from_rgba(u as u8, (u >> 8) as u8, (u >> 16) as u8, (u >> 24) as u8)
|
|
}
|
|
|
|
fn chunk_sig(map: &Map, cx: i32, cz: i32, fill_bucket: u8) -> u64 {
|
|
let mut h: u64 = 1469598103934665603;
|
|
for z in cz * CHUNK - 3..(cz + 1) * CHUNK + 3 {
|
|
for x in cx * CHUNK - 3..(cx + 1) * CHUNK + 3 {
|
|
let v = match map.get(x, z) {
|
|
Some(t) => {
|
|
let extra = match t.kind {
|
|
Kind::Farm => t.food as u64,
|
|
Kind::Treasury => fill_bucket as u64,
|
|
_ => 0,
|
|
};
|
|
(t.kind as u64) | (t.owner as u64) << 5 | (t.marked as u64) << 7 | (t.reinforced as u64) << 8 | extra << 9
|
|
}
|
|
None => 0xff,
|
|
};
|
|
h = (h ^ v).wrapping_mul(1099511628211);
|
|
}
|
|
}
|
|
h
|
|
}
|
|
|
|
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, render_div: 1, target: None }
|
|
}
|
|
|
|
// Changing the wall detail rebuilds all chunks
|
|
pub fn set_wall_detail(&mut self, sub: usize) {
|
|
if sub != self.wall_sub {
|
|
self.wall_sub = sub;
|
|
self.invalidate();
|
|
}
|
|
}
|
|
|
|
pub fn invalidate(&mut self) {
|
|
for c in self.chunks.iter_mut() {
|
|
c.sig = 0;
|
|
}
|
|
}
|
|
|
|
// (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..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 * self.ncx + cx) as usize];
|
|
if ch.sig != sig {
|
|
let a = amb.get_or_insert_with(|| ambient_field(&g.map));
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn chunk_visible(m: &Mat4, cx: i32, cz: i32) -> bool {
|
|
let (x0, z0) = ((cx * CHUNK) as f32, (cz * CHUNK) as f32);
|
|
let (x1, z1) = (x0 + CHUNK as f32, z0 + CHUNK as f32);
|
|
let (mut minx, mut miny, mut maxx, mut maxy) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN);
|
|
for &x in &[x0, x1] {
|
|
for &z in &[z0, z1] {
|
|
for &y in &[0.0, WALL_H + 0.5] {
|
|
let s = Cam::project(m, vec3(x, y, z));
|
|
minx = minx.min(s.x);
|
|
maxx = maxx.max(s.x);
|
|
miny = miny.min(s.y);
|
|
maxy = maxy.max(s.y);
|
|
}
|
|
}
|
|
}
|
|
maxx >= 0.0 && minx <= screen_width() && maxy >= 0.0 && miny <= screen_height()
|
|
}
|
|
|
|
fn lights(&self, g: &Game, cam: &Cam, t: f32) -> (Vec<Vec4>, Vec<Vec4>) {
|
|
let mut l: Vec<(f32, Vec4, Vec4)> = Vec::new();
|
|
let center = vec2(cam.tx, cam.tz);
|
|
let mut push = |p: Vec3, radius: f32, c: Vec3, intensity: f32, prio: f32| {
|
|
let d = vec2(p.x, p.z).distance(center) - prio;
|
|
l.push((d, p.extend(radius), c.extend(intensity)));
|
|
};
|
|
for (k, tr) in g.torches.iter().enumerate() {
|
|
let (x, z) = tr.pos();
|
|
let fl = 0.85 + 0.1 * (t * 9.0 + k as f32 * 1.7).sin() + 0.05 * (t * 23.0 + k as f32).sin();
|
|
push(vec3(x, 0.75, z), 4.5, vec3(1.0, 0.62, 0.28), 0.9 * fl, 0.0);
|
|
}
|
|
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);
|
|
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);
|
|
}
|
|
let pulse = 0.5 + 0.5 * (t * 4.0).sin();
|
|
for &(ex, ey) in &g.planned_entries {
|
|
push(vec3(ex as f32 + 0.5, 1.2, ey as f32 + 0.5), 5.0, vec3(1.0, 0.12, 0.08), 0.6 + 1.0 * pulse, 30.0);
|
|
}
|
|
for tw in &g.towers {
|
|
if tw.kind == TowerKind::Magic {
|
|
let (x, z) = tw.pos();
|
|
push(vec3(x, tower_height(tw) + 0.6, z), 4.5, vec3(0.75, 0.4, 1.0), 0.8 + tw.flash * 3.0, 10.0);
|
|
}
|
|
}
|
|
for f in &g.fx {
|
|
if f.kind == 1 {
|
|
push(vec3(f.x, 2.0, f.y), 8.0, vec3(1.0, 1.0, 0.85), 3.0 * (1.0 - f.t / f.max), 200.0);
|
|
}
|
|
}
|
|
l.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
|
|
l.truncate(self.max_lights.min(MAX_LIGHTS));
|
|
let mut pos = vec![Vec4::ZERO; MAX_LIGHTS];
|
|
let mut colr = vec![Vec4::ZERO; MAX_LIGHTS];
|
|
for (k, (_, p, c)) in l.iter().enumerate() {
|
|
pos[k] = *p;
|
|
colr[k] = *c;
|
|
}
|
|
let n = l.len() as f32;
|
|
pos.push(Vec4::new(n, 0.0, 0.0, 0.0));
|
|
(pos, colr)
|
|
}
|
|
|
|
// Entity under the mouse (screen-space test against the projected sprite)
|
|
pub fn pick_ent(&self, g: &Game, cam: &Cam, sx: f32, sy: f32, player_only: bool) -> Option<usize> {
|
|
let m = cam.matrix();
|
|
let px_per_tile = screen_height() / cam.zoom;
|
|
let mut best = None;
|
|
let mut bd = (px_per_tile * 0.4).max(10.0);
|
|
for (j, e) in g.ents.iter().enumerate() {
|
|
if !e.alive || e.held || (player_only && e.hero) {
|
|
continue;
|
|
}
|
|
let s = Cam::project(&m, vec3(e.x, 0.3, e.y));
|
|
let d = s.distance(vec2(sx, sy));
|
|
if d < bd {
|
|
bd = d;
|
|
best = Some(j);
|
|
}
|
|
}
|
|
best
|
|
}
|
|
|
|
pub fn draw(&mut self, g: &Game, gfx: &Gfx, cam: &Cam, time: f64, ov: &Overlay) {
|
|
let t = time as f32;
|
|
let m = cam.matrix();
|
|
self.update_chunks(g, gfx, &m);
|
|
EYE.with(|e| e.set(cam.eye_dir()));
|
|
// 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);
|
|
let count = lp[MAX_LIGHTS].x;
|
|
self.lit.set_uniform_array("LightPos", &lp[..MAX_LIGHTS]);
|
|
self.lit.set_uniform_array("LightCol", &lc[..]);
|
|
self.lit.set_uniform("LightCount", count);
|
|
gl_use_material(&self.lit);
|
|
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 * self.ncx + cx) as usize].mesh {
|
|
draw_mesh(mesh);
|
|
}
|
|
}
|
|
}
|
|
// sprites as billboards
|
|
if let Some(mesh) = self.billboards(g, gfx, cam, t) {
|
|
draw_mesh(&mesh);
|
|
}
|
|
// solid unlit objects (default material, depth tested)
|
|
gl_use_default_material();
|
|
for tr in &g.torches {
|
|
let (x, z) = tr.pos();
|
|
draw_cube(vec3(x, 0.55, z), vec3(0.07, 0.3, 0.07), None, Color::from_rgba(70, 45, 28, 255));
|
|
}
|
|
draw_landmarks_solid(g, t);
|
|
draw_towers_solid(g);
|
|
|
|
// ---- unlit glow / overlays
|
|
gl_use_material(&self.glow);
|
|
for c in &self.chunks {
|
|
if self.outlines {
|
|
if let Some(l) = &c.lines {
|
|
draw_mesh(l);
|
|
}
|
|
}
|
|
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();
|
|
let fl = 1.0 + 0.15 * (t * 11.0 + k as f32 * 2.3).sin();
|
|
draw_sphere(vec3(x, 0.78, z), 0.09 * fl, None, Color::new(1.0, 0.55, 0.15, 0.9));
|
|
draw_sphere(vec3(x, 0.8, z), 0.05 * fl, None, Color::new(1.0, 0.95, 0.6, 1.0));
|
|
}
|
|
draw_shadows(g);
|
|
if let Some(f) = g.flag {
|
|
draw_flag(f, t);
|
|
}
|
|
draw_overlay(&g.map, ov, self.wall_sub);
|
|
draw_entry_warnings(g, t, self.wall_sub);
|
|
// translucent glows and effects last (they write depth, see glow_material)
|
|
draw_landmarks_glow(g, t);
|
|
draw_towers_glow(g, t);
|
|
for f in &g.fx {
|
|
draw_fx(f, t);
|
|
}
|
|
gl_use_default_material();
|
|
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 in device pixels: health bars, sleep marks, levels, gate HP
|
|
self.draw_2d(g, gfx, cam, &m, t);
|
|
}
|
|
|
|
fn billboards(&self, g: &Game, gfx: &Gfx, cam: &Cam, t: f32) -> Option<Mesh> {
|
|
let (_, r, _) = cam.basis();
|
|
let toward = cam.eye_dir();
|
|
let amb = |x: f32, z: f32| {
|
|
let (ix, iz) = (x as i32, z as i32);
|
|
let own = g.map.get(ix, iz).map_or(false, |t| t.owner == Owner::Player);
|
|
if own {
|
|
0.75
|
|
} else {
|
|
0.5
|
|
}
|
|
};
|
|
let mut mb = MB::new();
|
|
let sw = SPRITE_TEX_W;
|
|
let sh = SPRITE_TEX_H;
|
|
for e in &g.ents {
|
|
if !e.alive || e.held {
|
|
continue;
|
|
}
|
|
let big = matches!(e.kind, EK::Ogre | EK::Paladin | EK::Jailer | EK::Chainbreaker);
|
|
let w = if e.kind == EK::Chainbreaker { 1.0 } else if big { 0.8 } else { 0.62 };
|
|
let h = w;
|
|
let sleeping = matches!(e.job, Job::Sleep(_)) && !e.moving;
|
|
let mut bob = if e.moving { (e.anim * 12.0).sin().abs() * 0.06 } else { 0.0 };
|
|
let mut rot = if e.working && !sleeping { (t * 14.0 + e.anim).sin() * 0.15 } else { 0.0 };
|
|
// prisoners: lying unconscious, carried above an imp, or shaking in the torture chamber
|
|
match e.captive {
|
|
1 => rot = std::f32::consts::FRAC_PI_2,
|
|
2 => {
|
|
rot = std::f32::consts::FRAC_PI_2;
|
|
bob = 0.4;
|
|
}
|
|
3 => rot = (t * 25.0 + e.anim).sin() * 0.08,
|
|
_ => {}
|
|
}
|
|
let white = e.flash > 0.07;
|
|
let src = Gfx::sprite_rect(e.kind, white);
|
|
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 {
|
|
std::mem::swap(&mut u0, &mut u1);
|
|
}
|
|
let a = amb(e.x, e.y) * if sleeping || e.captive == 1 { 0.6 } else { 1.0 };
|
|
let c = if white {
|
|
[255, 255, 255, 255]
|
|
} else if e.captive == 3 {
|
|
col(vec3(a * 1.2, a * 0.55, a * 0.55), 1.0)
|
|
} else {
|
|
col(Vec3::splat(a), 1.0)
|
|
};
|
|
// pulled towards the camera: unchanged on screen (orthographic), but walls behind
|
|
// the unit no longer cut into the sprite
|
|
let base = vec3(e.x, bob, e.y) + toward * 0.45;
|
|
let (sr, cr) = rot.sin_cos();
|
|
let corner = |dx: f32, dy: f32| {
|
|
let x = dx * cr - dy * sr;
|
|
let y = dx * sr + dy * cr;
|
|
base + r * x + Vec3::Y * y
|
|
};
|
|
mb.quad(
|
|
[corner(-w * 0.5, h), corner(w * 0.5, h), corner(w * 0.5, 0.0), corner(-w * 0.5, 0.0)],
|
|
[vec2(u0, v0), vec2(u1, v0), vec2(u1, v1), vec2(u0, v1)],
|
|
[c; 4],
|
|
[toward; 4],
|
|
);
|
|
}
|
|
if mb.v.is_empty() {
|
|
None
|
|
} else {
|
|
Some(Mesh { vertices: mb.v, indices: mb.i, texture: Some(gfx.sprites.clone()) })
|
|
}
|
|
}
|
|
|
|
fn draw_2d(&self, g: &Game, gfx: &Gfx, cam: &Cam, m: &Mat4, t: f32) {
|
|
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 | EK::Chainbreaker);
|
|
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)
|
|
} else if f > 0.5 {
|
|
Color::from_rgba(90, 220, 90, 255)
|
|
} else if f > 0.25 {
|
|
Color::from_rgba(240, 200, 60, 255)
|
|
} else {
|
|
Color::from_rgba(230, 60, 50, 255)
|
|
};
|
|
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 - (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));
|
|
}
|
|
if matches!(e.job, Job::Sleep(_)) && !e.moving {
|
|
let k = (t * 1.5 + e.anim) % 1.0;
|
|
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 = 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 = 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 = 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());
|
|
}
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------ towers
|
|
|
|
pub fn tower_height(t: &Tower) -> f32 {
|
|
match t.kind {
|
|
TowerKind::Arrow => 1.0 + 0.3 * (t.level - 1) as f32,
|
|
TowerKind::Cannon => 0.6 + 0.15 * (t.level - 1) as f32,
|
|
TowerKind::Magic => 1.2 + 0.3 * (t.level - 1) as f32,
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
fn draw_towers_solid(g: &Game) {
|
|
for tw in &g.towers {
|
|
let (x, z) = tw.pos();
|
|
let h = tower_height(tw);
|
|
let f = 0.75 + tw.flash * 1.5; // brightens briefly when firing
|
|
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));
|
|
}
|
|
}
|
|
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());
|
|
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));
|
|
}
|
|
}
|
|
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));
|
|
}
|
|
}
|
|
// level pips
|
|
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));
|
|
}
|
|
}
|
|
}
|
|
|
|
fn draw_towers_glow(g: &Game, t: f32) {
|
|
for (k, tw) in g.towers.iter().enumerate() {
|
|
if tw.kind != TowerKind::Magic {
|
|
continue;
|
|
}
|
|
let (x, z) = tw.pos();
|
|
let c = vec3(x, tower_height(tw) + 0.45 + (t * 2.0 + k as f32).sin() * 0.06, z);
|
|
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));
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------ landmarks, fx, overlay
|
|
|
|
// Camera direction for camera-facing line quads (set once per frame)
|
|
thread_local! {
|
|
static EYE: std::cell::Cell<Vec3> = std::cell::Cell::new(Vec3::Y);
|
|
}
|
|
|
|
// Thick 3D line as a camera-facing quad (GL lines don't work with custom materials)
|
|
fn line3(a: Vec3, b: Vec3, w: f32, color: Color) {
|
|
let eye = EYE.with(|e| e.get());
|
|
let side = (b - a).cross(eye).normalize_or_zero() * (w * 0.5);
|
|
let c = [(color.r * 255.0) as u8, (color.g * 255.0) as u8, (color.b * 255.0) as u8, (color.a * 255.0) as u8];
|
|
let v = |p: Vec3| Vertex { position: p, uv: Vec2::ZERO, color: c, normal: Vec4::Y };
|
|
draw_mesh(&Mesh { vertices: vec![v(a - side), v(a + side), v(b + side), v(b - side)], indices: vec![0, 1, 2, 0, 2, 3], texture: None });
|
|
}
|
|
|
|
fn disc(c: Vec3, r: f32, color: Color, seg: usize) {
|
|
let mut v = Vec::with_capacity(seg + 1);
|
|
let cc = [(color.r * 255.0) as u8, (color.g * 255.0) as u8, (color.b * 255.0) as u8, (color.a * 255.0) as u8];
|
|
v.push(Vertex { position: c, uv: Vec2::ZERO, color: cc, normal: Vec4::Y });
|
|
for k in 0..seg {
|
|
let a = k as f32 / seg as f32 * std::f32::consts::TAU;
|
|
v.push(Vertex { position: c + vec3(a.cos() * r, 0.0, a.sin() * r), uv: Vec2::ZERO, color: cc, normal: Vec4::Y });
|
|
}
|
|
let mut i = Vec::with_capacity(seg * 3);
|
|
for k in 0..seg as u16 {
|
|
i.extend_from_slice(&[0, 1 + k, 1 + (k + 1) % seg as u16]);
|
|
}
|
|
draw_mesh(&Mesh { vertices: v, indices: i, texture: None });
|
|
}
|
|
|
|
fn ring(c: Vec3, r: f32, color: Color) {
|
|
let seg = 32;
|
|
for k in 0..seg {
|
|
let a0 = k as f32 / seg as f32 * std::f32::consts::TAU;
|
|
let a1 = (k + 1) as f32 / seg as f32 * std::f32::consts::TAU;
|
|
line3(c + vec3(a0.cos() * r, 0.0, a0.sin() * r), c + vec3(a1.cos() * r, 0.0, a1.sin() * r), 0.035, color);
|
|
}
|
|
}
|
|
|
|
fn draw_landmarks_solid(g: &Game, t: f32) {
|
|
// core pedestal
|
|
let (cx, cy) = g.map.core;
|
|
let c = vec3(cx as f32 + 1.5, 0.0, cy as f32 + 1.5);
|
|
draw_cube(c + vec3(0.0, 0.15, 0.0), vec3(2.4, 0.3, 2.4), None, Color::from_rgba(45, 16, 20, 255));
|
|
draw_cube(c + vec3(0.0, 0.4, 0.0), vec3(1.4, 0.2, 1.4), None, Color::from_rgba(70, 26, 30, 255));
|
|
// gate pillars
|
|
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));
|
|
} else {
|
|
for k in 0..10 {
|
|
let a = k as f32 * 1.7;
|
|
draw_cube(vec3(gx + a.cos() * 1.0, 0.15, gz + a.sin() * 0.8), vec3(0.35, 0.3, 0.3), None, Color::from_rgba(150, 150, 160, 255));
|
|
}
|
|
}
|
|
let _ = t;
|
|
}
|
|
|
|
fn draw_landmarks_glow(g: &Game, t: f32) {
|
|
// soul core
|
|
let (cx, cy) = g.map.core;
|
|
let hp = (g.core_hp / CORE_HP).clamp(0.0, 1.0);
|
|
let pulse = 1.0 + (t * (2.0 + (1.0 - hp) * 6.0)).sin() * 0.06;
|
|
let c = vec3(cx as f32 + 1.5, 1.15 + (t * 1.3).sin() * 0.06, cy as f32 + 1.5);
|
|
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));
|
|
// 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);
|
|
let speed = if owned { 1.6 } else { 0.3 };
|
|
for a in 0..3 {
|
|
for k in 0..18 {
|
|
let f = k as f32 / 18.0;
|
|
let ang = t * speed + a as f32 * 2.094 + f * 4.0;
|
|
let r = f * 1.3;
|
|
let p = pc + vec3(ang.cos() * r, 0.1 + (1.0 - f) * 0.5, ang.sin() * r);
|
|
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();
|
|
let gc = vec3(gx, 1.0, gz);
|
|
draw_sphere(gc, 0.45 + 0.04 * (t * 3.0).sin(), None, Color::new(0.95, 0.97, 1.0, 0.95));
|
|
draw_sphere(gc, 0.8, None, Color::new(0.7, 0.85, 1.0, 0.2));
|
|
for k in 0..8 {
|
|
let a = t * 0.6 + k as f32 * 0.785;
|
|
line3(gc, gc + vec3(a.cos() * 1.0, a.sin() * 0.9, 0.0), 0.05, Color::new(0.85, 0.92, 1.0, 0.6));
|
|
}
|
|
}
|
|
}
|
|
|
|
fn draw_shadows(g: &Game) {
|
|
for e in &g.ents {
|
|
if !e.alive || e.held {
|
|
continue;
|
|
}
|
|
let r = if matches!(e.kind, EK::Ogre | EK::Paladin | EK::Jailer | EK::Chainbreaker) { 0.32 } else { 0.24 };
|
|
disc(vec3(e.x, 0.012, e.y), r, Color::new(0.0, 0.0, 0.0, 0.4), 12);
|
|
let rc = if e.captive != 0 {
|
|
Color::new(0.6, 0.6, 0.6, 0.6)
|
|
} else if e.guard {
|
|
Color::new(1.0, 0.85, 0.3, 0.9)
|
|
} else if e.hero {
|
|
Color::new(0.5, 0.8, 1.0, 0.8)
|
|
} else {
|
|
Color::new(0.95, 0.3, 0.35, 0.7)
|
|
};
|
|
ring(vec3(e.x, 0.02, e.y), r + 0.03, rc);
|
|
}
|
|
}
|
|
|
|
fn draw_flag(f: usize, t: f32) {
|
|
let (fx, fz) = xy(f);
|
|
let base = vec3(fx as f32 + 0.4, 0.0, fz as f32 + 0.5);
|
|
draw_cube(base + vec3(0.0, 0.7, 0.0), vec3(0.05, 1.4, 0.05), None, Color::from_rgba(200, 200, 200, 255));
|
|
let wave = (t * 6.0).sin() * 0.08;
|
|
let p1 = base + vec3(0.0, 1.38, 0.0);
|
|
let p2 = base + vec3(0.6, 1.22 + wave, 0.1);
|
|
let p3 = base + vec3(0.0, 1.0, 0.0);
|
|
let c = [210, 40, 40, 255];
|
|
let v = vec![
|
|
Vertex { position: p1, uv: Vec2::ZERO, color: c, normal: Vec4::Y },
|
|
Vertex { position: p2, uv: Vec2::ZERO, color: c, normal: Vec4::Y },
|
|
Vertex { position: p3, uv: Vec2::ZERO, color: c, normal: Vec4::Y },
|
|
];
|
|
draw_mesh(&Mesh { vertices: v, indices: vec![0, 1, 2], texture: None });
|
|
}
|
|
|
|
fn draw_fx(f: &Fx, t: f32) {
|
|
let p = (f.t / f.max).clamp(0.0, 1.0);
|
|
let c = rgba(f.col);
|
|
let a = |alpha: f32| Color::new(c.r, c.g, c.b, alpha);
|
|
let at = vec3(f.x, 0.4, f.y);
|
|
match f.kind {
|
|
0 => {
|
|
let from = vec3(f.x, 0.45, f.y);
|
|
let to = vec3(f.x2, 0.45, f.y2);
|
|
let head = from.lerp(to, p);
|
|
let tail = from.lerp(to, (p - 0.35).max(0.0));
|
|
line3(tail, head, 0.06, a(0.8));
|
|
draw_sphere(head, 0.08, None, c);
|
|
}
|
|
1 => {
|
|
let mut prev = vec3(f.x, 7.0, f.y);
|
|
for k in 1..=9 {
|
|
let y = 7.0 - 7.0 * k as f32 / 9.0;
|
|
let j = if k == 9 { 0.0 } else { (t * 50.0 + k as f32 * 13.7).sin() * 0.5 };
|
|
let next = vec3(f.x + j, y, f.y + j * 0.5);
|
|
line3(prev, next, 0.12, Color::new(1.0, 1.0, 0.85, 1.0 - p));
|
|
prev = next;
|
|
}
|
|
disc(vec3(f.x, 0.05, f.y), 1.6 * (0.5 + p), Color::new(1.0, 1.0, 0.7, 0.35 * (1.0 - p)), 24);
|
|
}
|
|
2 => {
|
|
let r = 0.1 + 0.3 * p;
|
|
for k in 0..6 {
|
|
let ang = k as f32 * 1.047;
|
|
line3(at, at + vec3(ang.cos() * r, (k as f32 - 2.5) * 0.06, ang.sin() * r), 0.04, a(1.0 - p));
|
|
}
|
|
}
|
|
3 => draw_sphere(vec3(f.x, 0.3, f.y), 0.2 + 0.5 * p, None, a(0.4 * (1.0 - p))),
|
|
4 => {
|
|
for k in 0..3 {
|
|
let o = vec3((k as f32 - 1.0) * 0.2, 0.3 + p * 0.9 + k as f32 * 0.1, 0.0);
|
|
draw_cube(at + o, vec3(0.16, 0.05, 0.05), None, a(1.0 - p));
|
|
draw_cube(at + o, vec3(0.05, 0.16, 0.05), None, a(1.0 - p));
|
|
}
|
|
}
|
|
5 => {
|
|
for k in 0..4 {
|
|
let ang = k as f32 * 1.9 + f.x * 3.0;
|
|
let o = vec3(ang.cos() * 0.3 * p, 0.2 + p * 0.7, ang.sin() * 0.3 * p);
|
|
draw_cube(at + o, Vec3::splat(0.06), None, a(1.0 - p));
|
|
}
|
|
}
|
|
6 => {
|
|
for k in 0..4 {
|
|
let ang = k as f32 * 1.7 + f.y * 5.0;
|
|
let o = vec3(ang.cos() * 0.35 * p, 0.6 - p * 0.5, ang.sin() * 0.35 * p);
|
|
draw_cube(at + o, Vec3::splat(0.06), None, a(1.0 - p));
|
|
}
|
|
}
|
|
7 => ring(vec3(f.x, 0.05, f.y), 0.3 + 1.2 * p, a(1.0 - p)),
|
|
8 => disc(vec3(f.x, 0.03, f.y), 0.55, a(0.45 * (1.0 - p)), 4),
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
fn outline(x0: f32, z0: f32, x1: f32, z1: f32, y: f32, c: Color) {
|
|
let p = [vec3(x0, y, z0), vec3(x1, y, z0), vec3(x1, y, z1), vec3(x0, y, z1)];
|
|
for k in 0..4 {
|
|
line3(p[k], p[(k + 1) % 4], 0.06, c);
|
|
}
|
|
}
|
|
|
|
// Expected hero entry points: the walls around them glow red and pulse
|
|
fn draw_entry_warnings(g: &Game, t: f32, n: usize) {
|
|
if g.planned_entries.is_empty() {
|
|
return;
|
|
}
|
|
let p = 0.5 + 0.5 * (t * 4.0).sin();
|
|
let fill = [255, 40, 30, (50.0 + 90.0 * p) as u8];
|
|
let line = [255, 90, 60, (120.0 + 120.0 * p) as u8];
|
|
let mut mb = MB::new();
|
|
for &(ex, ey) in &g.planned_entries {
|
|
for dy in -2..=2 {
|
|
for dx in -2..=2 {
|
|
let (x, y) = (ex + dx, ey + dy);
|
|
let Some(tile) = g.map.get(x, y) else { continue };
|
|
// walls of the realm's border near the entry point (not rock)
|
|
let border = DIR4.iter().any(|&(ax, ay)| g.map.get(x + ax, y + ay).map_or(false, |o| o.owner == Owner::Player && !o.solid()));
|
|
if tile.diggable() && border && dx.abs() + dy.abs() <= 3 && mb.v.len() < 30000 {
|
|
block_shell(&mut mb, &g.map, x, y, n, fill, line, 1.0, 0.04);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if let Some(m) = mb.into_plain_mesh() {
|
|
draw_mesh(&m);
|
|
}
|
|
}
|
|
|
|
// Hover feedback: wall blocks get a faint version of the dig-mark box (in the tool colour),
|
|
// floor tiles an outline. While dragging a dig rectangle, every block inside is tinted.
|
|
fn draw_overlay(map: &Map, ov: &Overlay, n: usize) {
|
|
const MAX_BOXES: usize = 150; // keeps the per-frame mesh within one draw call
|
|
let c = ov.color;
|
|
let fill = [(c.r * 255.0) as u8, (c.g * 255.0) as u8, (c.b * 255.0) as u8, 28];
|
|
let line = [(c.r * 255.0) as u8, (c.g * 255.0) as u8, (c.b * 255.0) as u8, 110];
|
|
let solid = |x: i32, z: i32| map.get(x, z).map_or(false, |t| t.solid());
|
|
let mut mb = MB::new();
|
|
if let Some((x0, z0, x1, z1)) = ov.rect {
|
|
let (ax, bx) = (x0.min(x1), x0.max(x1));
|
|
let (az, bz) = (z0.min(z1), z0.max(z1));
|
|
let mut boxes = 0;
|
|
if ov.high {
|
|
'outer: for z in az..=bz {
|
|
for x in ax..=bx {
|
|
if map.get(x, z).map_or(false, |t| t.diggable()) {
|
|
if boxes >= MAX_BOXES || mb.v.len() > 30000 {
|
|
break 'outer;
|
|
}
|
|
block_shell(&mut mb, map, x, z, n, fill, line, 0.6, 0.05);
|
|
boxes += 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let y = if ov.high { WALL_H + 0.18 } else { 0.04 };
|
|
outline(ax as f32, az as f32, bx as f32 + 1.0, bz as f32 + 1.0, y, ov.color);
|
|
} else if let Some((x, z)) = ov.hover {
|
|
if solid(x, z) {
|
|
block_shell(&mut mb, map, x, z, n, fill, line, 0.6, 0.05);
|
|
} else {
|
|
outline(x as f32, z as f32, x as f32 + 1.0, z as f32 + 1.0, 0.04, ov.color);
|
|
}
|
|
}
|
|
if let Some(m) = mb.into_plain_mesh() {
|
|
draw_mesh(&m);
|
|
}
|
|
if let Some((x, z, r)) = ov.radius {
|
|
ring(vec3(x, 0.05, z), r, ov.color);
|
|
}
|
|
}
|
|
|
|
// Minimap colour of a tile
|
|
pub fn minimap_color(tile: &Tile) -> [u8; 4] {
|
|
match tile.kind {
|
|
Kind::Rock => [40, 40, 48, 255],
|
|
Kind::Earth if tile.reinforced => [120, 70, 70, 255],
|
|
Kind::Earth => [86, 60, 40, 255],
|
|
Kind::Gold => [220, 180, 50, 255],
|
|
Kind::Floor => match tile.owner {
|
|
Owner::Player => [140, 60, 80, 255],
|
|
Owner::Hero => [170, 175, 190, 255],
|
|
Owner::None => [70, 64, 60, 255],
|
|
},
|
|
Kind::Treasury => [200, 160, 60, 255],
|
|
Kind::Lair => [150, 110, 60, 255],
|
|
Kind::Farm => [70, 140, 60, 255],
|
|
Kind::Training => [160, 100, 60, 255],
|
|
Kind::Library => [150, 40, 60, 255],
|
|
Kind::Core => [255, 60, 60, 255],
|
|
Kind::Portal => [190, 90, 240, 255],
|
|
Kind::Gate => [230, 230, 255, 255],
|
|
Kind::Torture => [120, 40, 40, 255],
|
|
Kind::Tower => [255, 140, 60, 255],
|
|
Kind::Temple => [200, 110, 50, 255],
|
|
}
|
|
}
|