3657 lines
172 KiB
Rust
3657 lines
172 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 = (crate::gfx::ATLAS_ROWS * 16) as f32;
|
|
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)
|
|
#[serde(skip, default = "zero_f32")]
|
|
pub rx: f32, // smoothed look-at target used by the screen (world x)
|
|
#[serde(skip, default = "zero_f32")]
|
|
pub rz: f32, // smoothed look-at target used by the screen (world z = map y)
|
|
#[serde(skip, default = "cam_uninit")]
|
|
pub render_init: bool,
|
|
}
|
|
|
|
pub const DEFAULT_YAW: f32 = 0.785;
|
|
pub const DEFAULT_PITCH: f32 = 0.6155;
|
|
|
|
fn zero_f32() -> f32 {
|
|
0.0
|
|
}
|
|
|
|
fn cam_uninit() -> bool {
|
|
false
|
|
}
|
|
|
|
impl Cam {
|
|
pub fn new(tx: f32, tz: f32) -> Cam {
|
|
Cam { tx, tz, yaw: DEFAULT_YAW, pitch: DEFAULT_PITCH, zoom: 18.0, rx: tx, rz: tz, render_init: true }
|
|
}
|
|
|
|
// the standard view angle and turn; the position and zoom stay
|
|
pub fn reset_view(&mut self) {
|
|
self.yaw = DEFAULT_YAW;
|
|
self.pitch = DEFAULT_PITCH;
|
|
}
|
|
|
|
pub fn snap(&mut self) {
|
|
self.rx = self.tx;
|
|
self.rz = self.tz;
|
|
self.render_init = true;
|
|
}
|
|
|
|
pub fn update_render(&mut self, dt: f32) {
|
|
if !self.render_init {
|
|
self.snap();
|
|
return;
|
|
}
|
|
let dist = (self.tx - self.rx).abs() + (self.tz - self.rz).abs();
|
|
if dist > self.zoom.max(1.0) {
|
|
self.snap();
|
|
return;
|
|
}
|
|
let k = (1.0 - (-28.0 * dt.max(0.0)).exp()).clamp(0.0, 1.0);
|
|
self.rx += (self.tx - self.rx) * k;
|
|
self.rz += (self.tz - self.rz) * k;
|
|
}
|
|
|
|
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.rx, 0.0, self.rz)
|
|
}
|
|
|
|
// visible world height in tiles: the zoom counts on the shorter side (upright screens: across)
|
|
pub fn view_h(&self) -> f32 {
|
|
let (w, h) = (screen_width().max(1.0), screen_height().max(1.0));
|
|
if h > w { self.zoom * h / w } else { self.zoom }
|
|
}
|
|
|
|
pub fn camera(&self) -> Camera3D {
|
|
Camera3D {
|
|
position: self.target() + self.eye_dir() * 60.0,
|
|
target: self.target(),
|
|
up: Vec3::Y,
|
|
fovy: self.view_h(),
|
|
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.view_h() * 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);
|
|
// unexplored tiles are drawn as blocks: they are picked like walls
|
|
if inb(x, z) && (map.t[idx(x, z)].solid() || !map.shown(idx(x, z))) && 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;
|
|
uniform highp float Time;
|
|
|
|
void main() {
|
|
vec4 w = Model * vec4(position, 1.0);
|
|
// river surfaces (marked by the vertex alpha, see LIQ_WATER_A / LIQ_LAVA_A) swell gently;
|
|
// lava rolls slower and flatter than water
|
|
float a = color0.a;
|
|
if (a > 96.0 && a < 192.0) {
|
|
bool lava = a < 128.0;
|
|
float sp = lava ? 0.8 : 1.6;
|
|
float amp = lava ? 0.04 : 0.05;
|
|
w.y += amp * (sin(w.x * 1.3 + w.z * 0.4 + Time * sp) * 0.6 + sin(w.z * 1.9 - w.x * 0.5 - Time * sp * 1.3) * 0.4);
|
|
}
|
|
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;
|
|
uniform highp float Time; // same precision as in the vertex shader (required for linking)
|
|
uniform vec2 AtlasSize;
|
|
uniform float CellSize; // texels per atlas cell (16; high-res 64)
|
|
uniform float HdLiquid; // 1: high-res liquids (2x2 blocks of tiles, a second flowing layer)
|
|
|
|
// a flowing liquid's texture at world position p inside the kind's cells (uv names one of them);
|
|
// high-res: the four variant cells form a 2x2 block that repeats every two tiles
|
|
vec2 liquid_uv(vec2 p) {
|
|
vec2 cell = floor(uv * AtlasSize / CellSize);
|
|
if (HdLiquid > 0.5) {
|
|
vec2 q = mod(floor(p), 2.0);
|
|
cell.x = floor(cell.x / 4.0) * 4.0 + q.x + 2.0 * q.y;
|
|
}
|
|
return (cell + fract(p)) * CellSize / AtlasSize;
|
|
}
|
|
|
|
void main() {
|
|
vec2 tuv = uv;
|
|
float wave = 0.0;
|
|
bool liquid = color.a > 0.38 && color.a < 0.75;
|
|
if (liquid) {
|
|
// flowing texture: sampled in world space inside the tile's (seamless) atlas cell, moving
|
|
// along the river (its direction comes in the normal's x/z, see liquid_flow)
|
|
bool lava = color.a < 0.5;
|
|
vec2 dir = wnorm.xz / max(length(wnorm.xz), 0.001);
|
|
float speed = lava ? 0.16 : 0.32;
|
|
float ts = lava ? 0.8 : 1.6; // wave tempo: lava rolls slower
|
|
// swell: broad moving bands; their slope shades the surface and bends the texture
|
|
float ph1 = wpos.x * 1.3 + wpos.z * 0.4 + Time * ts;
|
|
float ph2 = wpos.z * 1.9 - wpos.x * 0.5 - Time * ts * 1.3;
|
|
vec2 slope = vec2(cos(ph1) * 1.3 * 0.6 - cos(ph2) * 0.5 * 0.4, cos(ph1) * 0.4 * 0.6 + cos(ph2) * 1.9 * 0.4);
|
|
vec2 p = wpos.xz - dir * speed * Time + slope * (lava ? 0.05 : 0.07);
|
|
tuv = liquid_uv(p);
|
|
float ripple = sin(wpos.x * 5.0 + wpos.z * 3.0 + Time * ts * 1.4) * sin(wpos.z * 4.0 - wpos.x * 2.0 + Time * ts);
|
|
wave = dot(slope, vec2(0.45, 0.35)) * 0.6 + ripple * 0.4;
|
|
}
|
|
vec4 tex = texture2D(Texture, tuv);
|
|
if (liquid && HdLiquid > 0.5) {
|
|
// a second, larger layer drifting slower and sideways: the surface churns instead of
|
|
// only sliding along
|
|
bool lava = color.a < 0.5;
|
|
vec2 dir = wnorm.xz / max(length(wnorm.xz), 0.001);
|
|
vec2 side = vec2(-dir.y, dir.x);
|
|
float speed = lava ? 0.07 : 0.15;
|
|
vec2 p2 = wpos.xz * 0.6 + vec2(0.37, 0.71) - dir * speed * Time + side * sin(Time * (lava ? 0.21 : 0.33)) * 0.3;
|
|
vec4 tex2 = texture2D(Texture, liquid_uv(p2));
|
|
tex = vec4(mix(tex.rgb, tex2.rgb, 0.42), 1.0);
|
|
}
|
|
if (tex.a < 0.5) discard;
|
|
// see-through units (invisibility): every other pixel left out
|
|
if (color.a < 0.37 && mod(floor(gl_FragCoord.x) + floor(gl_FragCoord.y), 2.0) < 1.0) discard;
|
|
vec3 n = liquid ? vec3(0.0, 1.0, 0.0) : 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;
|
|
}
|
|
// light rippling over the surface: glints on water; lava glows by itself and pulses
|
|
if (liquid) {
|
|
if (color.a < 0.5) {
|
|
light = max(light, vec3(1.9, 1.65, 1.45)) * (1.0 + wave * 0.18);
|
|
} else {
|
|
light *= 1.0 + wave * 0.28;
|
|
}
|
|
}
|
|
gl_FragColor = vec4(tex.rgb * min(light, vec3(liquid && color.a < 0.5 ? 2.4 : 1.9)), 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),
|
|
UniformDesc::new("Time", UniformType::Float1),
|
|
UniformDesc::new("AtlasSize", UniformType::Float2),
|
|
UniformDesc::new("CellSize", UniformType::Float1),
|
|
UniformDesc::new("HdLiquid", UniformType::Float1),
|
|
],
|
|
textures: vec![],
|
|
},
|
|
)
|
|
.expect("lit material")
|
|
}
|
|
|
|
// Unlit, alpha-blended material for glows, shadows and effects
|
|
// the world target onto the virtual screen: fully opaque. Translucent glows (flames, the heart)
|
|
// leave the target's alpha below one; blended, the black behind would shine through and dim them.
|
|
pub(crate) fn opaque_material() -> Material {
|
|
const V: &str = r#"#version 100
|
|
attribute vec3 position;
|
|
attribute vec2 texcoord;
|
|
attribute vec4 color0;
|
|
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 = vec4(color.rgb * texture2D(Texture, uv).rgb, 1.0);
|
|
}
|
|
"#;
|
|
load_material(ShaderSource::Glsl { vertex: V, fragment: F }, MaterialParams { pipeline_params: PipelineParams::default(), uniforms: vec![], textures: vec![] }).expect("opaque material")
|
|
}
|
|
|
|
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],
|
|
);
|
|
}
|
|
}
|
|
// An axis-aligned box without its bottom (plants: stems, caps, trunks, crowns)
|
|
fn boxy(&mut self, lo: Vec3, hi: Vec3, cell: u32, c: [u8; 4], top: [u8; 4]) {
|
|
let (a, b) = cell_uv(cell);
|
|
let uv = [vec2(a.x, b.y), vec2(b.x, b.y), vec2(b.x, a.y), vec2(a.x, a.y)];
|
|
let (x0, y0, z0, x1, y1, z1) = (lo.x, lo.y, lo.z, hi.x, hi.y, hi.z);
|
|
self.quad([vec3(x0, y1, z0), vec3(x1, y1, z0), vec3(x1, y1, z1), vec3(x0, y1, z1)], uv, [top; 4], [Vec3::Y; 4]);
|
|
self.quad([vec3(x0, y0, z0), vec3(x1, y0, z0), vec3(x1, y1, z0), vec3(x0, y1, z0)], uv, [c; 4], [-Vec3::Z; 4]);
|
|
self.quad([vec3(x1, y0, z1), vec3(x0, y0, z1), vec3(x0, y1, z1), vec3(x1, y1, z1)], uv, [c; 4], [Vec3::Z; 4]);
|
|
self.quad([vec3(x0, y0, z1), vec3(x0, y0, z0), vec3(x0, y1, z0), vec3(x0, y1, z1)], uv, [c; 4], [-Vec3::X; 4]);
|
|
self.quad([vec3(x1, y0, z0), vec3(x1, y0, z1), vec3(x1, y1, z1), vec3(x1, y1, z0)], uv, [c; 4], [Vec3::X; 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()) })
|
|
}
|
|
}
|
|
}
|
|
|
|
// the season of the real date: 0 none, 2 winter (Dec - Feb: snow above ground); the holidays
|
|
// are in crate::holiday
|
|
pub fn season() -> u8 {
|
|
thread_local! {
|
|
static SEASON: std::cell::Cell<u8> = const { std::cell::Cell::new(255) };
|
|
}
|
|
SEASON.with(|s| {
|
|
if s.get() == 255 {
|
|
let (_, m, d, _, _) = crate::cloud::civil(macroquad::miniquad::date::now() as i64);
|
|
s.set(match (m, d) {
|
|
(12, _) | (1, _) | (2, _) => 2,
|
|
_ => 0,
|
|
});
|
|
}
|
|
s.get()
|
|
})
|
|
}
|
|
|
|
// the researched level of the room on tile t (1: none)
|
|
fn room_level_at(map: &Map, t: &Tile) -> u8 {
|
|
if map.upgrades.is_empty() {
|
|
return 1;
|
|
}
|
|
let (Some(r), side) = (room_index(t.kind), t.owner.side()) else { return 1 };
|
|
map.upgrades.iter().find(|u| u.0 == side && u.1 as usize == r).map_or(1, |u| u.2)
|
|
}
|
|
|
|
// Ambient light field: own territory is lit, foreign areas are dim
|
|
thread_local! {
|
|
// daylight above ground (bad weather dims it), in tenths
|
|
static SKY_LIGHT: std::cell::Cell<u32> = const { std::cell::Cell::new(16) };
|
|
}
|
|
|
|
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 | Owner::Rival1 | Owner::Rival2 | Owner::Rival3) => 1.0,
|
|
(false, Owner::Hero) => 0.6,
|
|
_ => 0.0,
|
|
};
|
|
// daylight above ground (cliffs and trees too)
|
|
if map.sky(i) {
|
|
f[i] = SKY_LIGHT.with(|s| s.get()) as f32 / 10.0;
|
|
}
|
|
// rooms of a higher level shine brighter
|
|
let lv = room_level_at(map, t);
|
|
if lv > 1 && f[i] > 0.0 {
|
|
f[i] += 0.25 * (lv - 1) as f32;
|
|
}
|
|
if t.river() == Some(Kind::Lava) {
|
|
f[i] = f[i].max(0.8); // lava lights up its surroundings
|
|
}
|
|
// the spores of a mushroom grove glow faintly
|
|
if t.kind == Kind::Grove || (t.kind == Kind::Mushroom && t.res & MUSH_GROVE != 0) {
|
|
f[i] = f[i].max(0.45);
|
|
}
|
|
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)
|
|
}
|
|
|
|
// ---- lava light: an orange glow up to LAVA_REACH tiles around lava, fading with the distance
|
|
const LAVA_REACH: f32 = 10.0;
|
|
const LAVA_LIGHT: Vec3 = Vec3::new(1.3, 0.56, 0.1); // added light at full strength (next to the lava)
|
|
|
|
// glow per tile: 1 on lava, falling to 0 at LAVA_REACH tiles. The light spreads over open ground
|
|
// (walls are lit on their faces but do not let it through).
|
|
fn lava_field(map: &Map) -> Vec<f32> {
|
|
use std::cmp::Reverse;
|
|
use std::collections::BinaryHeap;
|
|
const STEP: u32 = 10; // distances in tenths of a tile
|
|
let max = (LAVA_REACH * STEP as f32) as u32;
|
|
let mut dist = vec![u32::MAX; n()];
|
|
let mut heap = BinaryHeap::new();
|
|
for (i, t) in map.t.iter().enumerate() {
|
|
if t.river() == Some(Kind::Lava) {
|
|
dist[i] = 0;
|
|
heap.push(Reverse((0u32, i)));
|
|
}
|
|
}
|
|
while let Some(Reverse((d, i))) = heap.pop() {
|
|
if d > dist[i] || (d > 0 && map.t[i].solid()) {
|
|
continue; // stale entry, or a wall: lit, but it does not pass the light on
|
|
}
|
|
let (x, y) = xy(i);
|
|
for (k, &(dx, dy)) in DIR8.iter().enumerate() {
|
|
let (nx, ny) = (x + dx, y + dy);
|
|
if !inb(nx, ny) {
|
|
continue;
|
|
}
|
|
let nd = d + if k >= 4 { 14 } else { STEP };
|
|
let ni = idx(nx, ny);
|
|
if nd <= max && nd < dist[ni] {
|
|
dist[ni] = nd;
|
|
heap.push(Reverse((nd, ni)));
|
|
}
|
|
}
|
|
}
|
|
dist.iter()
|
|
.map(|&d| {
|
|
if d == u32::MAX {
|
|
0.0
|
|
} else {
|
|
let k = (1.0 - d as f32 / max as f32).clamp(0.0, 1.0);
|
|
k.powf(1.6)
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
// glow at a point (bilinear between tile centres)
|
|
fn lava_at(f: &[f32], x: f32, z: f32) -> f32 {
|
|
if f.len() != n() {
|
|
return 0.0;
|
|
}
|
|
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;
|
|
a * (1.0 - fz) + b * fz
|
|
}
|
|
|
|
// the direction a river flows at a tile, carried to the shader in the surface normal's x/z
|
|
// (y stays 1 so the normal still points up): along the longer run of river tiles around it
|
|
fn liquid_flow(map: &Map, x: i32, z: i32) -> Vec3 {
|
|
let river = |dx: i32, dz: i32| map.get(x + dx, z + dz).map_or(false, |t| t.river().is_some()) as i32;
|
|
let along_x: i32 = (-3..=3).map(|d| river(d, 0)).sum();
|
|
let along_z: i32 = (-3..=3).map(|d| river(0, d)).sum();
|
|
if along_x > along_z {
|
|
vec3(0.5, 1.0, 0.0)
|
|
} else {
|
|
vec3(0.0, 1.0, 0.5)
|
|
}
|
|
}
|
|
|
|
// a surface colour with the lava light added
|
|
fn lava_lit(base: Vec3, glow: f32) -> Vec3 {
|
|
base + LAVA_LIGHT * glow
|
|
}
|
|
|
|
const LIGHT_DIR: Vec3 = Vec3::new(0.45, 0.8, 0.35);
|
|
|
|
// fog of war: units in unexplored tiles, and enemies out of sight, are not drawn
|
|
pub fn unit_hidden(g: &Game, e: &Ent) -> bool {
|
|
let i = e.tile();
|
|
!g.map.shown(i) || (e.side() != 0 && !g.map.in_sight(i))
|
|
}
|
|
|
|
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 LIQ_D: f32 = 0.3; // depth of the river surface below the floor
|
|
// vertex alpha that marks river surfaces for the lit shader (waves and flowing texture)
|
|
const LIQ_WATER_A: f32 = 0.6;
|
|
const LIQ_LAVA_A: f32 = 0.42;
|
|
const LIQ_SKIRT: f32 = 0.1; // river banks reach a bit below the surface, so waves leave no gap
|
|
const DECK_Y: f32 = 0.03; // height of bridge decks
|
|
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()
|
|
}
|
|
|
|
thread_local! {
|
|
// the side of the master looking at the map (own dig marks are yellow)
|
|
static VIEW_SIDE: std::cell::Cell<u8> = const { std::cell::Cell::new(0) };
|
|
}
|
|
|
|
fn view_side() -> u8 {
|
|
VIEW_SIDE.with(|v| v.get())
|
|
}
|
|
|
|
// colour of a dig mark: always the colour of the master who set it
|
|
fn mark_rgb(by: u8) -> [u8; 3] {
|
|
let c = master_rgb(by);
|
|
[(c.x * 255.0) as u8, (c.y * 255.0) as u8, (c.z * 255.0) as u8]
|
|
}
|
|
|
|
// Glowing box in the marker's colour around a block that is marked for digging
|
|
fn mark_block(mk: &mut MB, map: &Map, x: i32, z: i32, n: usize, by: u8) {
|
|
let [r, g, b] = mark_rgb(by);
|
|
let line = [r, (g as u16 + 15).min(255) as u8, (b as u16 + 20).min(255) as u8, 235];
|
|
block_shell(mk, map, x, z, n, [r, g, b, 70], 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
// `hd`: the high-res atlas; its generated textures are laid out by the tiles' positions
|
|
fn build_chunk(map: &Map, amb: &[f32], glow: &[f32], cx: i32, cz: i32, atlas: &Texture2D, wall_sub: usize, hd: bool) -> [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 mut tex = Gfx::tile_tex(t.kind, t.owner, t.var, t.reinforced);
|
|
// open land above ground is grass (until a master claims it)
|
|
if map.sky(idx(x, z)) && t.kind == Kind::Floor && t.owner == Owner::None {
|
|
tex = if season() == 2 { T_SNOW } else { T_GRASS } + (t.var as u32 & 3);
|
|
}
|
|
// a grove's mushroom stands on moss
|
|
if t.kind == Kind::Mushroom && t.res & MUSH_GROVE != 0 {
|
|
tex = crate::gfx::T_MOSS + (t.var as u32 & 3);
|
|
}
|
|
if hd {
|
|
tex = crate::gfx::hd_tile(tex, x, z);
|
|
}
|
|
let (ua, ub) = cell_uv(tex);
|
|
let (xf, zf) = (x as f32, z as f32);
|
|
if !map.shown(idx(x, z)) {
|
|
// fog of war: a black block, with black sides towards known open ground;
|
|
// own tiles marked for digging glow darkly in the master's colour so the plan stays visible
|
|
let black = col(Vec3::splat(0.0), 1.0);
|
|
let top = if t.marked && t.mark_by == view_side() { col(master_rgb(t.mark_by) * 0.42, 1.0) } else { black };
|
|
mb.quad([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)], [ua, ua, ua, ua], [top; 4], [Vec3::Y; 4]);
|
|
for (d, &(dx, dz)) in DIR4.iter().enumerate() {
|
|
let open = map.get(x + dx, z + dz).map_or(false, |o| !o.solid()) && map.shown(idx((x + dx).max(0), (z + dz).max(0)));
|
|
if !open {
|
|
continue;
|
|
}
|
|
let (p, q) = match d {
|
|
0 => (vec3(xf + 1.0, 0.0, zf), vec3(xf, 0.0, zf)),
|
|
1 => (vec3(xf + 1.0, 0.0, zf + 1.0), vec3(xf + 1.0, 0.0, zf)),
|
|
2 => (vec3(xf, 0.0, zf + 1.0), vec3(xf + 1.0, 0.0, zf + 1.0)),
|
|
_ => (vec3(xf, 0.0, zf), vec3(xf, 0.0, zf + 1.0)),
|
|
};
|
|
let out = vec3(dx as f32, 0.0, dz as f32);
|
|
mb.quad([p + Vec3::Y * WALL_H, q + Vec3::Y * WALL_H, q - Vec3::Y * LIQ_D, p - Vec3::Y * LIQ_D], [ua, ua, ua, ua], [black; 4], [out; 4]);
|
|
}
|
|
continue;
|
|
}
|
|
if let Some(river) = t.river() {
|
|
// ---- river bed: sunken surface, banks where the ground or a wall begins
|
|
let lava = river == Kind::Lava;
|
|
let (la, lb) = cell_uv(if lava { T_LAVA } else { T_WATER } + (t.var as u32 & 3));
|
|
let a = amb_at(amb, xf + 0.5, zf + 0.5);
|
|
let lg = lava_at(glow, xf + 0.5, zf + 0.5);
|
|
let lc = if lava { col(vec3(1.0, 0.95, 0.9), LIQ_LAVA_A) } else { col(lava_lit(Vec3::splat(0.25 + a * 0.9), lg), LIQ_WATER_A) };
|
|
mb.quad(
|
|
[vec3(xf, -LIQ_D, zf), vec3(xf + 1.0, -LIQ_D, zf), vec3(xf + 1.0, -LIQ_D, zf + 1.0), vec3(xf, -LIQ_D, zf + 1.0)],
|
|
// the shader samples the flowing texture in world space; the uv only names the cell
|
|
// (its centre, robust even with low precision uvs)
|
|
[(la + lb) * 0.5; 4],
|
|
[lc; 4],
|
|
[liquid_flow(map, x, z); 4],
|
|
);
|
|
let (fa, fb) = cell_uv(T_FACE_EARTH);
|
|
for (d, &(dx, dz)) in DIR4.iter().enumerate() {
|
|
if map.get(x + dx, z + dz).map_or(false, |o| o.river().is_some()) {
|
|
continue;
|
|
}
|
|
// edge seen from inside the river: from a to b, facing back into this tile
|
|
let (p, q) = match d {
|
|
0 => (vec3(xf, 0.0, zf), vec3(xf + 1.0, 0.0, zf)),
|
|
1 => (vec3(xf + 1.0, 0.0, zf), vec3(xf + 1.0, 0.0, zf + 1.0)),
|
|
2 => (vec3(xf + 1.0, 0.0, zf + 1.0), vec3(xf, 0.0, zf + 1.0)),
|
|
_ => (vec3(xf, 0.0, zf + 1.0), vec3(xf, 0.0, zf)),
|
|
};
|
|
let inward = vec3(-dx as f32, 0.0, -dz as f32);
|
|
let bc = col(if lava { Vec3::splat(0.7) } else { lava_lit(Vec3::splat(0.18 + a * 0.5), lg) }, 1.0);
|
|
mb.quad(
|
|
[p, q, q - Vec3::Y * (LIQ_D + LIQ_SKIRT), p - Vec3::Y * (LIQ_D + LIQ_SKIRT)],
|
|
[vec2(fa.x, fa.y), vec2(fb.x, fa.y), vec2(fb.x, fb.y), vec2(fa.x, fb.y)],
|
|
[bc; 4],
|
|
[inward; 4],
|
|
);
|
|
}
|
|
if t.kind == Kind::Obsidian {
|
|
// a crust of cooled lava just above the glowing river
|
|
let (oa, ob) = cell_uv(T_OBSIDIAN + (t.var as u32 & 3));
|
|
let oc = col(lava_lit(Vec3::splat(amb_at(amb, xf + 0.5, zf + 0.5).max(0.45)), lg), 1.0);
|
|
let y = -0.02;
|
|
mb.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(oa.x, oa.y), vec2(ob.x, oa.y), vec2(ob.x, ob.y), vec2(oa.x, ob.y)], [oc; 4], [Vec3::Y; 4]);
|
|
continue;
|
|
}
|
|
if t.raised() {
|
|
// a raised drawbridge: the flap stands upright at the bank it is hinged on
|
|
let tex = if lava { T_STONEBRIDGE } else { T_BRIDGE };
|
|
let (ba, bb) = cell_uv(tex);
|
|
let fc = col(lava_lit(Vec3::splat(amb_at(amb, xf + 0.5, zf + 0.5).max(if lava { 0.6 } else { 0.0 }) * 0.85), lg), 1.0);
|
|
for (d, &(dx, dz)) in DIR4.iter().enumerate() {
|
|
if !map.get(x + dx, z + dz).map_or(false, |o| o.walkable() && !o.bridge()) {
|
|
continue;
|
|
}
|
|
// the hinge edge, from p to q
|
|
let (p, q) = match d {
|
|
0 => (vec3(xf, 0.0, zf), vec3(xf + 1.0, 0.0, zf)),
|
|
1 => (vec3(xf + 1.0, 0.0, zf), vec3(xf + 1.0, 0.0, zf + 1.0)),
|
|
2 => (vec3(xf + 1.0, 0.0, zf + 1.0), vec3(xf, 0.0, zf + 1.0)),
|
|
_ => (vec3(xf, 0.0, zf + 1.0), vec3(xf, 0.0, zf)),
|
|
};
|
|
let inward = vec3(-dx as f32, 0.0, -dz as f32);
|
|
let off = inward * 0.12;
|
|
let up = Vec3::Y * 1.0;
|
|
// both faces of the flap, a little in front of the bank
|
|
mb.quad([p + off, q + off, q + off + up, p + off + up], [vec2(ba.x, bb.y), vec2(bb.x, bb.y), vec2(bb.x, ba.y), vec2(ba.x, ba.y)], [fc; 4], [inward; 4]);
|
|
mb.quad([q + off, p + off, p + off + up, q + off + up], [vec2(ba.x, bb.y), vec2(bb.x, bb.y), vec2(bb.x, ba.y), vec2(ba.x, ba.y)], [fc; 4], [-inward; 4]);
|
|
ln.strip(p + off + up, q + off + up, inward, OUTLINE_W, OUTLINE_C);
|
|
// the chains from the top of the flap down to the bank
|
|
let chain = col(Vec3::splat(0.55), 1.0);
|
|
for e in [p, q] {
|
|
let a = e + off + up + (if e == p { (q - p) * 0.1 } else { (p - q) * 0.1 });
|
|
let b = e - inward * 0.6 + Vec3::Y * 1.3;
|
|
ln.strip(a, b, inward, OUTLINE_W * 0.7, chain);
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
if t.bridge() {
|
|
// deck: planks across the direction of the bridge, rails along it
|
|
let run_x = |dx: i32| map.get(x + dx, z).map_or(false, |o| o.bridge() || (o.walkable() && !o.bridge()));
|
|
let run_z = |dz: i32| map.get(x, z + dz).map_or(false, |o| o.bridge() || (o.walkable() && !o.bridge()));
|
|
let along_x = (run_x(-1) as i32 + run_x(1) as i32) > (run_z(-1) as i32 + run_z(1) as i32);
|
|
// a drawbridge over lava is made of stone
|
|
let (ba, bb) = cell_uv(if t.kind == Kind::Drawbridge && lava { T_STONEBRIDGE + (t.var as u32 & 1) } else { tex });
|
|
let dc = col(lava_lit(Vec3::splat(amb_at(amb, xf + 0.5, zf + 0.5).max(if lava { 0.6 } else { 0.0 })), lg), 1.0);
|
|
let uv = if along_x {
|
|
[vec2(ba.x, ba.y), vec2(ba.x, bb.y), vec2(bb.x, bb.y), vec2(bb.x, ba.y)]
|
|
} else {
|
|
[vec2(ba.x, ba.y), vec2(bb.x, ba.y), vec2(bb.x, bb.y), vec2(ba.x, bb.y)]
|
|
};
|
|
mb.quad([vec3(xf, DECK_Y, zf), vec3(xf + 1.0, DECK_Y, zf), vec3(xf + 1.0, DECK_Y, zf + 1.0), vec3(xf, DECK_Y, zf + 1.0)], uv, [dc; 4], [Vec3::Y; 4]);
|
|
if t.kind == Kind::Drawbridge {
|
|
// iron bands across the planks mark a drawbridge
|
|
let iron = col(Vec3::splat(0.32), 1.0);
|
|
for f in [0.18, 0.82] {
|
|
let (a, b) = if along_x { (vec3(xf + f, DECK_Y + 0.005, zf), vec3(xf + f, DECK_Y + 0.005, zf + 1.0)) } else { (vec3(xf, DECK_Y + 0.005, zf + f), vec3(xf + 1.0, DECK_Y + 0.005, zf + f)) };
|
|
ln.strip(a, b, Vec3::Y, OUTLINE_W * 1.6, iron);
|
|
}
|
|
}
|
|
// deck edges above open river
|
|
for (d, &(dx, dz)) in DIR4.iter().enumerate() {
|
|
if !map.get(x + dx, z + dz).map_or(false, |o| o.liquid()) {
|
|
continue;
|
|
}
|
|
let (p, q) = match d {
|
|
0 => (vec3(xf + 1.0, DECK_Y, zf), vec3(xf, DECK_Y, zf)),
|
|
1 => (vec3(xf + 1.0, DECK_Y, zf + 1.0), vec3(xf + 1.0, DECK_Y, zf)),
|
|
2 => (vec3(xf, DECK_Y, zf + 1.0), vec3(xf + 1.0, DECK_Y, zf + 1.0)),
|
|
_ => (vec3(xf, DECK_Y, zf), vec3(xf, DECK_Y, zf + 1.0)),
|
|
};
|
|
let out = vec3(dx as f32, 0.0, dz as f32);
|
|
let ec = col(Vec3::splat(0.3), 1.0);
|
|
mb.quad([p, q, q - Vec3::Y * 0.14, p - Vec3::Y * 0.14], [vec2(ba.x, ba.y), vec2(bb.x, ba.y), vec2(bb.x, ba.y + 0.002), vec2(ba.x, ba.y + 0.002)], [ec; 4], [out; 4]);
|
|
ln.strip(p + out * 0.01, q + out * 0.01, Vec3::Y, OUTLINE_W, OUTLINE_C);
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
if t.kind == Kind::Shard {
|
|
let dc = col(Vec3::splat(1.0), 1.0);
|
|
mb.cross(xf + 0.5, zf + 0.5, 0.7, 0.8, D_SHARD, dc);
|
|
}
|
|
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(lava_lit(Vec3::splat(a), lava_at(glow, vx as f32, vz as f32) * ao), 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);
|
|
// open land above ground has none
|
|
let outdoors = map.sky(idx(x, z)) && t.owner == Owner::None;
|
|
if !solid_at(map, x, z - 1) && !outdoors {
|
|
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) && !outdoors {
|
|
ln.strip(vec3(xf, 0.0, zf), vec3(xf, 0.0, zf + 1.0), Vec3::Y, OUTLINE_W, OUTLINE_C);
|
|
}
|
|
// a room of a higher level: gold trim along its border (twice at level 3)
|
|
let lv = room_level_at(map, &t);
|
|
if lv > 1 {
|
|
let gold = col(vec3(1.0, 0.78, 0.25), 1.0);
|
|
for (d, &(dx, dz)) in DIR4.iter().enumerate() {
|
|
if map.get(x + dx, z + dz).map_or(false, |o| o.kind == t.kind && o.owner == t.owner) {
|
|
continue;
|
|
}
|
|
for k in 0..(lv - 1) {
|
|
let m = 0.08 + 0.12 * k as f32;
|
|
let (p, q) = match d {
|
|
0 => (vec3(xf + m, 0.012, zf + m), vec3(xf + 1.0 - m, 0.012, zf + m)),
|
|
1 => (vec3(xf + 1.0 - m, 0.012, zf + m), vec3(xf + 1.0 - m, 0.012, zf + 1.0 - m)),
|
|
2 => (vec3(xf + 1.0 - m, 0.012, zf + 1.0 - m), vec3(xf + m, 0.012, zf + 1.0 - m)),
|
|
_ => (vec3(xf + m, 0.012, zf + 1.0 - m), vec3(xf + m, 0.012, zf + m)),
|
|
};
|
|
ln.strip(p, q, Vec3::Y, OUTLINE_W * 1.5, gold);
|
|
}
|
|
}
|
|
}
|
|
// room decor
|
|
let dc = col(lava_lit(Vec3::splat(amb_at(amb, xf + 0.5, zf + 0.5)), lava_at(glow, xf + 0.5, zf + 0.5)), 1.0);
|
|
// holidays: now and then something on the dungeon's plain floor - a glowing
|
|
// pumpkin (Halloween), a little fir tree with a star and a present (Christmas),
|
|
// painted eggs (Easter)
|
|
let holiday = crate::holiday::current();
|
|
if holiday != crate::holiday::NONE && t.kind == Kind::Floor && t.owner == Owner::Player {
|
|
let hsh = (x as u32).wrapping_mul(2246822519) ^ (z as u32).wrapping_mul(3266489917);
|
|
if (hsh >> 7) % 37 == 0 {
|
|
let (cx, cz) = (xf + 0.3 + ((hsh >> 3) % 4) as f32 * 0.1, zf + 0.35);
|
|
let pc = col(Vec3::splat(1.4), 1.0);
|
|
match holiday {
|
|
crate::holiday::HALLOWEEN => mb.boxy(vec3(cx - 0.14, 0.0, cz - 0.14), vec3(cx + 0.14, 0.24, cz + 0.14), T_PUMPKIN, pc, pc),
|
|
crate::holiday::CHRISTMAS => {
|
|
let fc = col(Vec3::splat(1.2), 1.0);
|
|
for (w, y0, y1) in [(0.22f32, 0.06f32, 0.26f32), (0.16, 0.24, 0.42), (0.1, 0.4, 0.56)] {
|
|
mb.boxy(vec3(cx - w, y0, cz - w), vec3(cx + w, y1, cz + w), crate::gfx::T_FIR, fc, fc);
|
|
}
|
|
mb.boxy(vec3(cx - 0.03, 0.0, cz - 0.03), vec3(cx + 0.03, 0.08, cz + 0.03), T_BARK, fc, fc);
|
|
mb.boxy(vec3(cx - 0.035, 0.56, cz - 0.035), vec3(cx + 0.035, 0.63, cz + 0.035), crate::gfx::T_GIFT, pc, pc);
|
|
let gx = cx + 0.26;
|
|
mb.boxy(vec3(gx - 0.08, 0.0, cz + 0.02), vec3(gx + 0.08, 0.13, cz + 0.18), crate::gfx::T_GIFT, pc, pc);
|
|
}
|
|
_ => {
|
|
for k in 0..3u32 {
|
|
let (ex, ez) = (cx + (k as f32 - 1.0) * 0.16, cz + ((hsh >> (k * 3)) % 3) as f32 * 0.08 - 0.08);
|
|
let egg = crate::gfx::T_EGG + (hsh >> (k + 11)) % 2;
|
|
mb.boxy(vec3(ex - 0.045, 0.0, ez - 0.035), vec3(ex + 0.045, 0.12, ez + 0.035), egg, pc, pc);
|
|
mb.boxy(vec3(ex - 0.03, 0.12, ez - 0.025), vec3(ex + 0.03, 0.15, ez + 0.025), egg, pc, pc);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if t.plant() {
|
|
// a giant mushroom or a tree (marked for felling: a yellow tint)
|
|
let (cx, cz) = (xf + 0.5, zf + 0.5);
|
|
let a = amb_at(amb, cx, cz);
|
|
let lg = lava_at(glow, cx, cz);
|
|
let tint = |v: Vec3| col(lava_lit(if t.marked { v * vec3(1.3, 1.15, 0.5) } else { v }, lg), 1.0);
|
|
let s = 0.85 + 0.1 * (t.var & 3) as f32;
|
|
if t.kind == Kind::Mushroom {
|
|
// it grows from a sprout to full size in steps; grown, its cap glows teal
|
|
let stage = mushroom_stage(&t);
|
|
let s = s * (0.3 + 0.7 * stage as f32 / 4.0);
|
|
let h = 0.75 * s;
|
|
let sc = tint(Vec3::splat(a.max(0.35)));
|
|
let w = 0.12 * s.max(0.6);
|
|
mb.boxy(vec3(cx - w, 0.0, cz - w), vec3(cx + w, h, cz + w), T_STEM, sc, sc);
|
|
// the cap glows a little in the dark; a grown one in its own colour
|
|
let hue = if stage >= 4 { vec3(0.55, 1.35, 1.25) } else { vec3(0.95, 0.8, 0.62) };
|
|
let cc = tint(Vec3::splat(a.max(0.7)) * hue);
|
|
mb.boxy(vec3(cx - 0.42 * s, h, cz - 0.42 * s), vec3(cx + 0.42 * s, h + 0.26, cz + 0.42 * s), T_CAP, cc, cc);
|
|
mb.boxy(vec3(cx - 0.26 * s, h + 0.26, cz - 0.26 * s), vec3(cx + 0.26 * s, h + 0.38, cz + 0.26 * s), T_CAP, cc, cc);
|
|
} else {
|
|
let tc = tint(Vec3::splat(a.max(0.3)));
|
|
mb.boxy(vec3(cx - 0.12, 0.0, cz - 0.12), vec3(cx + 0.12, 0.9 * s, cz + 0.12), T_BARK, tc, tc);
|
|
let lc = tint(Vec3::splat(a.max(0.3) * 0.9));
|
|
// snow on the crowns in winter
|
|
let top = if season() == 2 { col(Vec3::splat(a.max(0.3) * 2.2), 1.0) } else { tint(Vec3::splat(a.max(0.3) * 1.1)) };
|
|
let leaves = T_LEAVES + (t.var as u32 & 1);
|
|
mb.boxy(vec3(cx - 0.48, 0.75 * s, cz - 0.48), vec3(cx + 0.48, 1.55 * s, cz + 0.48), leaves, lc, top);
|
|
mb.boxy(vec3(cx - 0.3, 1.55 * s, cz - 0.3), vec3(cx + 0.3, 1.95 * s, cz + 0.3), leaves, lc, top);
|
|
}
|
|
}
|
|
// wear and tear: grit and cracks on plain floors
|
|
if matches!(t.kind, Kind::Floor) && !outdoors {
|
|
let hsh = (x as u32).wrapping_mul(2654435761) ^ (z as u32).wrapping_mul(2246822519);
|
|
let hsh = (hsh ^ (hsh >> 15)).wrapping_mul(2246822519);
|
|
let decal = match hsh % 11 {
|
|
0 | 1 => Some(D_RUBBLE),
|
|
2 => Some(D_CRACK),
|
|
_ => None,
|
|
};
|
|
if let Some(d) = decal {
|
|
let (a, b) = cell_uv(d);
|
|
// rotate by a quarter turn for variety
|
|
let uv = [vec2(a.x, a.y), vec2(b.x, a.y), vec2(b.x, b.y), vec2(a.x, b.y)];
|
|
let r = (hsh >> 8) as usize % 4;
|
|
let uv = [uv[r % 4], uv[(r + 1) % 4], uv[(r + 2) % 4], uv[(r + 3) % 4]];
|
|
mb.quad([vec3(xf, 0.012, zf), vec3(xf + 1.0, 0.012, zf), vec3(xf + 1.0, 0.012, zf + 1.0), vec3(xf, 0.012, zf + 1.0)], uv, [dc; 4], [Vec3::Y; 4]);
|
|
}
|
|
}
|
|
match t.kind {
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
// training, torture, temple and library furniture and the beds: props.rs
|
|
// each treasury tile shows what it holds (up to TREASURY_TILE)
|
|
// (every third treasury tile holds a chest instead, see props.rs)
|
|
Kind::Treasury if t.gold > 10 && !crate::props::chest_tile(x, z) => {
|
|
let fill = (t.gold as f32 / TREASURY_TILE).clamp(0.0, 1.0);
|
|
let s = 0.35 + 0.55 * fill;
|
|
let sprite = [D_COINS, D_SILVER, D_RUBY, D_DIAMOND][(t.res & 3) as usize];
|
|
mb.cross(xf + 0.5, zf + 0.5, s, s * 0.8, sprite, 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| lava_lit(Vec3::splat(top_amb * (0.75 + 0.25 * nrm.dot(ld).max(0.0))), lava_at(glow, p.x, p.z) * 0.6),
|
|
);
|
|
// 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, t.mark_by);
|
|
}
|
|
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);
|
|
// the face is lit by the lava in front of it
|
|
let lg = lava_at(glow, p.x + out.x * 0.5, p.z + out.z * 0.5);
|
|
lava_lit(Vec3::splat(side_amb * (0.5 + 0.5 * nrm.dot(ld).max(0.0)) * (0.55 + 0.45 * h)), lg * (1.0 - 0.35 * 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,
|
|
opaque: Material, // the world target onto the screen without blending
|
|
pub pixel_rows: u32, // pixel look: the world is rendered this many rows high (0: off)
|
|
target: Option<(RenderTarget, u32, u32)>, // low resolution render target
|
|
lava: Vec<f32>, // lava light per tile (see lava_field)
|
|
chunks_hd: bool, // the chunks were built with the high-res atlas
|
|
}
|
|
|
|
// The 3D resolution setting refers to a window 1080 device pixels high: smaller windows use a
|
|
// proportionally smaller divisor, so the 3D pixels keep their size relative to the window.
|
|
// 1 (full resolution) stays 1.
|
|
|
|
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) -> u64 {
|
|
let mut h: u64 = 1469598103934665603;
|
|
// researched room levels change the light and the trim of every room
|
|
for u in &map.upgrades {
|
|
h = (h ^ (u.0 as u64 | (u.1 as u64) << 8 | (u.2 as u64) << 16)).wrapping_mul(1099511628211);
|
|
}
|
|
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 => (t.gold / 50) as u64 | (t.res as u64) << 5,
|
|
Kind::Drawbridge => t.res as u64,
|
|
Kind::Mushroom => mushroom_stage(&t) as u64 | (t.res as u64) << 3,
|
|
_ => 0,
|
|
};
|
|
let shown = map.shown(idx(x, z)) as u64 | (map.sky(idx(x, z)) as u64) << 1;
|
|
(t.kind as u64) | (t.owner as u64) << 5 | (t.marked as u64 | (t.mark_by as u64) << 50) << 8 | (t.reinforced as u64) << 9 | shown << 10 | extra << 11
|
|
}
|
|
None => 0xff,
|
|
};
|
|
h = (h ^ v).wrapping_mul(1099511628211);
|
|
}
|
|
}
|
|
// the lava light reaches further: lava and walls up to LAVA_REACH tiles around the chunk
|
|
let r = LAVA_REACH as i32;
|
|
for z in cz * CHUNK - r..(cz + 1) * CHUNK + r {
|
|
for x in cx * CHUNK - r..(cx + 1) * CHUNK + r {
|
|
if let Some(t) = map.get(x, z) {
|
|
let v = (t.river() == Some(Kind::Lava)) as u64 | (t.solid() as u64) << 1;
|
|
h = (h ^ (v + 7)).wrapping_mul(1099511628211);
|
|
}
|
|
}
|
|
}
|
|
h
|
|
}
|
|
|
|
impl R3d {
|
|
pub fn new() -> R3d {
|
|
R3d { lit: lit_material(), glow: glow_material(), opaque: opaque_material(), chunks: Vec::new(), ncx: 0, ncz: 0, outlines: true, wall_sub: 3, max_lights: MAX_LIGHTS, pixel_rows: crate::settings::PIXEL_ART_ROWS as u32, target: None, lava: Vec::new(), chunks_hd: false }
|
|
}
|
|
|
|
// 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 side = g.me.side().min(7);
|
|
// the weather dims the daylight: the chunks above ground are lit again in steps
|
|
let sky = ((1.6 * (1.0 - g.weather.gloom())) * 10.0).round() as u32;
|
|
if SKY_LIGHT.with(|s| s.get()) != sky {
|
|
SKY_LIGHT.with(|s| s.set(sky));
|
|
for ch in self.chunks.iter_mut() {
|
|
ch.sig = 0;
|
|
}
|
|
}
|
|
// the graphics style changed: the chunks are built again with the other atlas
|
|
let hd = self.pixel_rows >= crate::settings::HIGH_RES_ROWS as u32;
|
|
if hd != self.chunks_hd {
|
|
self.chunks_hd = hd;
|
|
for ch in self.chunks.iter_mut() {
|
|
ch.sig = 0;
|
|
}
|
|
}
|
|
if view_side() != side {
|
|
// another master looks: the marks change colour
|
|
VIEW_SIDE.with(|v| v.set(side));
|
|
for ch in self.chunks.iter_mut() {
|
|
ch.sig = 0;
|
|
}
|
|
}
|
|
let mut amb: Option<Vec<f32>> = None;
|
|
let mut lava_fresh = false;
|
|
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);
|
|
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));
|
|
if !lava_fresh {
|
|
// the map changed: the lava light is worked out again (once per frame at most)
|
|
self.lava = lava_field(&g.map);
|
|
lava_fresh = true;
|
|
}
|
|
let ch = &mut self.chunks[(cz * self.ncx + cx) as usize];
|
|
let atlas = if self.chunks_hd { &gfx.atlas_hd } else { &gfx.atlas };
|
|
let [m, o, l, k] = build_chunk(&g.map, a, &self.lava, cx, cz, atlas, self.wall_sub, self.chunks_hd);
|
|
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, tr.flame_h() + 0.13, z), 5.5, vec3(1.0, 0.62, 0.28), 1.5 * 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.6, 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 tint = portal_tint(g, k);
|
|
let c = tint.map_or(vec3(0.6, 0.55, 0.7), |c| c * 0.8 + vec3(0.1, 0.05, 0.15));
|
|
push(vec3(px as f32 + 1.5, 0.8, py as f32 + 1.5), 6.0, c, if tint.is_some() { 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);
|
|
}
|
|
for rv in g.rivals.iter().filter(|r| r.alive) {
|
|
let (rx, rz) = rv.centre();
|
|
let c = master_rgb(rv.side);
|
|
push(vec3(rx, 1.4, rz), 7.0, c, 1.1, 50.0);
|
|
}
|
|
// glowing lava near the camera (every third tile)
|
|
let r = (cam.zoom * 0.9) as i32;
|
|
let (ccx, ccz) = (cam.tx as i32, cam.tz as i32);
|
|
for z in (ccz - r).max(0)..(ccz + r).min(mh() as i32) {
|
|
if z % 3 != 0 {
|
|
continue;
|
|
}
|
|
for x in (ccx - r).max(0)..(ccx + r).min(mw() as i32) {
|
|
if x % 3 == 0 && g.map.t[idx(x, z)].kind == Kind::Lava {
|
|
let fl = 0.8 + 0.2 * (t * 2.0 + (x * 7 + z * 3) as f32).sin();
|
|
push(vec3(x as f32 + 0.5, 0.3, z as f32 + 0.5), 5.5, vec3(1.0, 0.45, 0.12), 1.7 * fl, 0.0);
|
|
}
|
|
}
|
|
}
|
|
// burning bridges flicker
|
|
for &(i, _) in g.bridgework.burning.iter().filter(|b| g.map.shown(b.0)) {
|
|
let (x, z) = xy(i);
|
|
let fl = 0.8 + 0.2 * (t * 11.0 + i as f32).sin();
|
|
push(vec3(x as f32 + 0.5, 0.6, z as f32 + 0.5), 4.5, vec3(1.0, 0.5, 0.15), 1.6 * fl, 5.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);
|
|
}
|
|
// towers: the dungeon's glow red from their joints, the knights' carry a cold lantern;
|
|
// magic and rune towers shine in their colour; every shot flares up
|
|
for tw in g.towers.iter().filter(|tw| g.map.shown(tw.tile)) {
|
|
let (x, z) = tw.pos();
|
|
let top = tower_height(tw);
|
|
let (c, i, r) = match tw.kind {
|
|
TowerKind::Magic => (vec3(0.75, 0.4, 1.0), 0.9, 4.5),
|
|
TowerKind::Rune => (vec3(0.4, 0.8, 1.0), 1.0, 5.0),
|
|
_ if tw.hero => (vec3(0.85, 0.9, 1.0), 0.8, 4.0),
|
|
_ => (vec3(1.0, 0.3, 0.12), 0.85, 4.0),
|
|
};
|
|
let pulse = 0.9 + 0.1 * (t * 2.0 + tw.id as f32).sin();
|
|
push(vec3(x, top + 0.4, z), r, c, i * pulse + tw.flash * 3.0, 10.0);
|
|
}
|
|
// traps: each kind in its colour, brighter while it goes off. Another master's traps
|
|
// stay dark until they have gone off once (so the light does not betray them)
|
|
for tr in g.traps.iter().filter(|tr| tr.stage == crate::traps::ARMED && g.map.shown(tr.tile)) {
|
|
use crate::traps::TrapKind as K;
|
|
if !g.trap_glows(tr) {
|
|
continue;
|
|
}
|
|
let fresh = (1.0 - tr.fired / 0.8).clamp(0.0, 1.0); // just went off
|
|
let (c, i, r) = match tr.kind {
|
|
K::Poison => (vec3(0.35, 1.0, 0.3), 0.55, 2.5),
|
|
K::Fire => (vec3(1.0, 0.5, 0.12), 0.6, 3.0),
|
|
K::Alarm => (vec3(1.0, 0.15, 0.1), 0.35 + 0.35 * (0.5 + 0.5 * (t * 4.0).sin()), 3.0),
|
|
K::Mine => (vec3(0.7, 0.4, 1.0), 0.5, 2.5),
|
|
K::Frost => (vec3(0.5, 0.85, 1.0), 0.65, 3.0),
|
|
K::Spikes | K::Roller => (vec3(1.0, 0.85, 0.6), 0.0, 2.5),
|
|
_ => continue, // doors
|
|
};
|
|
if i + fresh <= 0.0 {
|
|
continue;
|
|
}
|
|
let (x, z) = xy(tr.tile);
|
|
push(vec3(x as f32 + 0.5, 0.35, z as f32 + 0.5), r + fresh, c, i + 2.5 * fresh, 5.0);
|
|
}
|
|
// effects light up their surroundings: lightning, explosions, glowing projectiles, spells
|
|
let tint = |c: u32| {
|
|
let c = rgba(c);
|
|
vec3(c.r, c.g, c.b)
|
|
};
|
|
for f in &g.fx {
|
|
let left = 1.0 - (f.t / f.max).clamp(0.0, 1.0);
|
|
match f.kind {
|
|
1 => push(vec3(f.x, 2.0, f.y), 8.0, vec3(1.0, 1.0, 0.85), 3.0 * left, 200.0),
|
|
0 => {
|
|
use crate::particles::*;
|
|
let c = match f.style {
|
|
ST_FIRE => Some(vec3(1.0, 0.55, 0.15)),
|
|
ST_SHADOW => Some(vec3(0.75, 0.3, 1.0)),
|
|
ST_HOLY => Some(vec3(1.0, 0.92, 0.6)),
|
|
ST_ARCANE => Some(vec3(1.0, 0.7, 0.4)),
|
|
ST_RUNE => Some(vec3(0.4, 0.8, 1.0)),
|
|
ST_MAGIC => Some(vec3(1.0, 0.4, 0.9)),
|
|
_ => None,
|
|
};
|
|
if let (Some(c), true) = (c, left > 0.0) {
|
|
let (x, z, h) = fx_pos(f, 1.0 - left);
|
|
push(vec3(x, h, z), 3.0 + f.size, c, 1.4, 120.0);
|
|
}
|
|
}
|
|
// healing and spells (level ups, specials): a glow in the effect's colour
|
|
4 => push(vec3(f.x, 0.8, f.y), 4.0, vec3(0.5, 1.0, 0.55), 1.6 * left, 130.0),
|
|
7 => push(vec3(f.x, 0.9, f.y), 5.0, tint(f.col), 2.0 * left, 130.0),
|
|
_ => {}
|
|
}
|
|
}
|
|
// explosions (fireballs, cannonballs, rune strikes): a bright, short flash of light
|
|
for p in g.parts.iter().filter(|p| p.kind == crate::particles::FLASH) {
|
|
let left = 1.0 - (p.t / p.max).clamp(0.0, 1.0);
|
|
push(vec3(p.x, p.h + 0.4, p.y), 3.0 + 4.0 * p.size, tint(p.col), 3.5 * left * left.sqrt(), 180.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.view_h();
|
|
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.disp_x, 0.3, e.disp_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: the world is rendered at a fixed number of rows and scaled up to the screen
|
|
// on the virtual screen (ui.rs) the world takes exactly k x k interface pixels per pixel
|
|
let vrows = crate::ui::virtual_rows();
|
|
let pix = if vrows > 0 && self.pixel_rows > 0 {
|
|
// the pixel rows count on the shorter side (upright screens: across)
|
|
let short = vrows.min(crate::ui::sw().round() as u32);
|
|
let k = (short / self.pixel_rows).max(1);
|
|
let th = vrows / k;
|
|
Some(((crate::ui::sw() / k as f32).ceil() as u32, th, k as f32))
|
|
} else {
|
|
None
|
|
};
|
|
let div = if pix.is_some() { 2 } else { 1 }; // > 1: a target is used
|
|
let mut cam3 = cam.camera();
|
|
if let Some((tw, th, _)) = pix {
|
|
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);
|
|
// wrapped so the shader keeps its precision (the flow speeds are whole tiles per period)
|
|
self.lit.set_uniform("Time", (time % 1000.0) as f32);
|
|
// high-res: the 128x128 tiles where a tile gets nearly that many pixels, else the 64x64
|
|
// ones (same layout: the chunks keep their uvs, only the texture changes)
|
|
let hd_tiles = self.chunks_hd;
|
|
let rows = pix.map_or(screen_height() * screen_dpi_scale(), |(_, th, _)| th as f32);
|
|
let tile = if !hd_tiles {
|
|
16
|
|
} else if rows / cam.view_h().max(0.1) >= crate::gfx::HD_TILE2 as f32 * 0.85 {
|
|
crate::gfx::HD_TILE2
|
|
} else {
|
|
crate::gfx::HD_TILE
|
|
};
|
|
let atlas = match tile {
|
|
16 => &gfx.atlas,
|
|
crate::gfx::HD_TILE => &gfx.atlas_hd,
|
|
_ => &gfx.atlas_hd2,
|
|
};
|
|
for ch in self.chunks.iter_mut() {
|
|
for mesh in [&mut ch.mesh, &mut ch.overlay].into_iter().flatten() {
|
|
if mesh.texture.as_ref() != Some(atlas) {
|
|
mesh.texture = Some(atlas.clone());
|
|
}
|
|
}
|
|
}
|
|
self.lit.set_uniform("AtlasSize", vec2((crate::gfx::ATLAS_COLS * tile) as f32, (crate::gfx::ATLAS_ROWS * tile) as f32));
|
|
self.lit.set_uniform("CellSize", tile as f32);
|
|
self.lit.set_uniform("HdLiquid", if hd_tiles { 1.0f32 } else { 0.0 });
|
|
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
|
|
// the world's pixel rows (detailed sprites only where they get enough pixels)
|
|
let world_rows = pix.map_or(screen_height() * screen_dpi_scale(), |(_, th, _)| th as f32);
|
|
let (small, detailed) = self.billboards(g, gfx, cam, t, world_rows);
|
|
for mesh in [small, detailed].iter().flatten() {
|
|
draw_mesh(mesh);
|
|
}
|
|
// solid unlit objects (default material, depth tested)
|
|
gl_use_default_material();
|
|
for tr in g.torches.iter().filter(|tr| g.map.shown(tr.wall)) {
|
|
let (x, z) = tr.pos();
|
|
if tr.stand {
|
|
draw_torch_stand(x, z);
|
|
} else {
|
|
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);
|
|
// the furniture's materials in the graphics style's resolution
|
|
crate::props::set_materials(if self.chunks_hd { &gfx.materials } else { &gfx.materials_px });
|
|
crate::props::draw_props_solid(g, cam, t);
|
|
draw_parts_solid(g);
|
|
draw_traps_solid(g, t);
|
|
|
|
// ---- 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().filter(|(_, tr)| g.map.shown(tr.wall)) {
|
|
draw_torch_flame(tr.pos(), tr.flame_h(), k as f32, t);
|
|
}
|
|
draw_shadows(g);
|
|
if let Some(f) = g.flag {
|
|
draw_flag(f, t);
|
|
}
|
|
for &m in &g.guard_marks {
|
|
draw_shield_mark(m, t);
|
|
}
|
|
for &m in &g.attack_marks {
|
|
draw_sword_mark(m, t);
|
|
}
|
|
draw_marks(g, t);
|
|
draw_overlay(&g.map, ov, self.wall_sub);
|
|
draw_entry_warnings(g, t, self.wall_sub);
|
|
draw_scorch_glow(g);
|
|
// translucent glows and effects last (they write depth, see glow_material)
|
|
draw_landmarks_glow(g, t, cam.basis().1);
|
|
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);
|
|
}
|
|
draw_traps_glow(g, t);
|
|
draw_parts_glow(g);
|
|
draw_weather(g, cam, t);
|
|
gl_use_default_material();
|
|
crate::ui::begin_ui();
|
|
if let (Some((_, _, k)), Some((rt, tw, th))) = (pix, self.target.as_ref()) {
|
|
// the world onto the virtual screen, every pixel k x k (scaled to the device at the end)
|
|
gl_use_material(&self.opaque);
|
|
draw_texture_ex(&rt.texture, 0.0, 0.0, WHITE, DrawTextureParams { dest_size: Some(vec2(*tw as f32 * k, *th as f32 * k)), flip_y: true, ..Default::default() });
|
|
gl_use_default_material();
|
|
}
|
|
|
|
// ---- 2D overlay in device pixels: health bars, sleep marks, levels, gate HP
|
|
self.draw_2d(g, gfx, cam, &m, t);
|
|
}
|
|
|
|
// the units as billboards: the 10x10 sprites and the detailed ones (two textures)
|
|
fn billboards(&self, g: &Game, gfx: &Gfx, cam: &Cam, t: f32, world_rows: f32) -> (Option<Mesh>, 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| matches!(t.owner, Owner::Player | Owner::Rival1 | Owner::Rival2 | Owner::Rival3));
|
|
if inb(ix, iz) && g.map.sky(idx(ix, iz)) {
|
|
0.95 * (1.0 - 0.6 * g.weather.gloom())
|
|
} else if own {
|
|
0.75
|
|
} else {
|
|
0.5
|
|
}
|
|
};
|
|
// the camera's right and viewing direction on the ground (for the eight views)
|
|
let rg = vec2(r.x, r.z).normalize_or_zero();
|
|
let tg = vec2(toward.x, toward.z).normalize_or_zero();
|
|
let mut mb = MB::new();
|
|
let mut mb_hd = MB::new();
|
|
let sw = SPRITE_TEX_W;
|
|
let sh = SPRITE_TEX_H;
|
|
// detailed sprites only in the high-res style, and only where a sprite's cell (1.2 sizes
|
|
// high) gives every texel nearly a pixel; fewer pixels would only turn it into noise
|
|
let hd = self.pixel_rows >= crate::settings::HIGH_RES_ROWS as u32;
|
|
let px_per_tile = world_rows / cam.view_h().max(0.1);
|
|
for e in &g.ents {
|
|
if !e.alive || e.held || unit_hidden(g, e) {
|
|
continue;
|
|
}
|
|
let w = sprite_size(e.kind);
|
|
let h = w;
|
|
let sleeping = matches!(e.job, Job::Sleep(_)) && !e.moving;
|
|
// which of the eight directions the unit shows (five views, three of them mirrored)
|
|
let d = vec2(e.dir.cos(), e.dir.sin());
|
|
let ang = d.dot(rg).atan2(d.dot(tg));
|
|
let view = ((ang / std::f32::consts::FRAC_PI_4).round().abs() as u32).min(4);
|
|
let mirror = ang < 0.0;
|
|
let fwd = if mirror { -1.0 } else { 1.0 }; // screen direction the unit faces
|
|
let ph = t * 10.0 + e.id as f32 * 1.7;
|
|
let (mut sx, mut sy, mut lunge) = (1.0f32, 1.0f32, 0.0f32);
|
|
let mut bob = if e.moving { (e.anim * 12.0).sin().abs() * 0.06 } else { 0.0 };
|
|
let mut rot = 0.0;
|
|
if e.moving {
|
|
// a little sway while walking; loads are carried leaning back
|
|
rot = (e.anim * 12.0).sin() * 0.05;
|
|
if e.carry > 0 || matches!(e.job, Job::Carry(_)) {
|
|
rot += 0.12 * fwd;
|
|
bob *= 0.6;
|
|
}
|
|
} else if e.working && !sleeping {
|
|
match e.job {
|
|
// claiming: stomping the ground, squashed on landing
|
|
Job::Claim(_) | Job::Help(_) => {
|
|
let k = (ph * 1.1).sin();
|
|
bob = k.max(0.0) * 0.13;
|
|
if k < 0.0 {
|
|
sy = 1.0 + k * 0.14;
|
|
sx = 1.0 - k * 0.1;
|
|
}
|
|
}
|
|
// digging: a swing at the wall
|
|
Job::Dig(_) | Job::TearBridge(_) => {
|
|
let k = (ph * 1.3).sin();
|
|
rot = -(k * 0.35).max(-0.12) * fwd;
|
|
lunge = k.max(0.0) * 0.07;
|
|
}
|
|
// hammering
|
|
Job::Reinforce(_) | Job::Repair(_) => {
|
|
let k = (ph * 2.2).sin();
|
|
rot = -k.max(0.0) * 0.25 * fwd;
|
|
bob = (ph * 2.2).cos().max(0.0) * 0.03;
|
|
}
|
|
// fighting: lunging at the enemy
|
|
Job::Attack(_) | Job::AttackTower(_) | Job::AttackCore | Job::Torment(_) => {
|
|
let k = (ph * 0.9).sin().max(0.0).powi(2);
|
|
lunge = k * 0.14;
|
|
rot = -k * 0.2 * fwd;
|
|
}
|
|
// training: quick punches
|
|
Job::Train(_) => {
|
|
let k = (ph * 1.6).sin();
|
|
lunge = k.max(0.0) * 0.08;
|
|
rot = -k * 0.12 * fwd;
|
|
bob = (ph * 3.2).sin().abs() * 0.03;
|
|
}
|
|
// eating: nodding over the food
|
|
Job::Eat(_) => {
|
|
sy = 1.0 - (ph * 0.8).sin().abs() * 0.1;
|
|
rot = -(ph * 0.8).sin().abs() * 0.12 * fwd;
|
|
}
|
|
// reading and praying: a slow sway
|
|
Job::Study(_) | Job::Pray(_) | Job::HealCore => rot = (t * 1.8 + e.id as f32).sin() * 0.07,
|
|
_ => rot = (ph * 1.4).sin() * 0.15,
|
|
}
|
|
} else if !sleeping {
|
|
// breathing
|
|
sy = 1.0 + (t * 2.4 + e.id as f32).sin() * 0.025;
|
|
}
|
|
// flying creatures hover and flap (on their perch while sleeping)
|
|
if flies(e.kind) && !sleeping {
|
|
bob = 0.45 + (t * 7.0 + e.anim * 3.0).sin() * 0.06;
|
|
sx *= 1.0 + (t * 14.0 + e.id as f32).sin() * 0.06;
|
|
} else if sleeping {
|
|
// asleep: lying in bed
|
|
rot = std::f32::consts::FRAC_PI_2 * 0.9 * fwd;
|
|
bob = 0.04;
|
|
}
|
|
// 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,
|
|
_ => {}
|
|
}
|
|
if e.chained {
|
|
// pulling at the chains
|
|
rot = (t * 3.0 + e.id as f32).sin() * 0.1;
|
|
lunge = 0.0;
|
|
}
|
|
let white = e.flash > 0.07;
|
|
let v = if e.captive != 0 { 0 } else { view };
|
|
// screen pixels of the cell per texel of a detailed cell (64x64 level), the 32x32
|
|
// level has two texels per pixel
|
|
let texel = w * 1.2 * px_per_tile / crate::gfx::HD_CELL as f32;
|
|
let detailed = hd && texel * 2.0 >= 0.85;
|
|
let fine = texel >= 0.85;
|
|
// treasure in a sack on the back; digging as a drawn animation (detailed sprites),
|
|
// frame by frame with the same rhythm as the swing below
|
|
let sack = e.carry > 0 && Gfx::has_sack(e.kind);
|
|
let hacking = e.working && !e.moving && !sleeping && matches!(e.job, Job::Dig(_) | Job::TearBridge(_));
|
|
let frame = ((ph * 1.3 / std::f32::consts::TAU).rem_euclid(1.0) * crate::gfx::HACK_FRAMES as f32) as u32;
|
|
let hd = if detailed { Gfx::hd_cell_view(e.kind, white, v, hacking.then_some(frame), sack, fine) } else { None };
|
|
if hd.is_some() && hacking {
|
|
// the frames swing the pickaxe: no tilting of the whole figure
|
|
rot = 0.0;
|
|
lunge = 0.0;
|
|
}
|
|
let src = hd.or_else(|| sack.then(|| Gfx::sprite_cell_sack(e.kind, white, e.level, v)).flatten()).unwrap_or_else(|| Gfx::sprite_cell_view(e.kind, white, e.level, v));
|
|
let (sw, sh) = if hd.is_some() { (crate::gfx::HD_TEX_W, crate::gfx::HD_TEX_H) } else { (sw, sh) };
|
|
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 mirror {
|
|
std::mem::swap(&mut u0, &mut u1);
|
|
}
|
|
let a = amb(e.disp_x, e.disp_y) * if sleeping || e.captive == 1 { 0.6 } else { 1.0 };
|
|
// spell looks: stone grey, plague green, see-through when invisible
|
|
let alpha = if e.invis > 0.0 { 0.35 } 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 if e.shield > 0.0 {
|
|
col(vec3(a * 0.85, a * 0.85, a * 0.8), alpha)
|
|
} else if e.plague > 0.0 {
|
|
col(vec3(a * 0.75, a * 1.15, a * 0.6), alpha)
|
|
} else {
|
|
col(Vec3::splat(a), alpha)
|
|
};
|
|
// the lava light colours units near the lava
|
|
let c = if white {
|
|
c
|
|
} else {
|
|
let lg = lava_at(&self.lava, e.disp_x, e.disp_y) * 255.0;
|
|
let add = |v: u8, k: f32| (v as f32 + k * lg).min(255.0) as u8;
|
|
[add(c[0], LAVA_LIGHT.x), add(c[1], LAVA_LIGHT.y), add(c[2], LAVA_LIGHT.z), c[3]]
|
|
};
|
|
// pulled towards the camera: unchanged on screen (orthographic), but walls behind
|
|
// the unit no longer cut into the sprite
|
|
let base = vec3(e.disp_x, bob, e.disp_y) + toward * 0.45 + r * (lunge * fwd);
|
|
let (sr, cr) = rot.sin_cos();
|
|
let corner = |dx: f32, dy: f32| {
|
|
let (dx, dy) = (dx * sx, dy * sy);
|
|
let x = dx * cr - dy * sr;
|
|
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
|
|
let mb = if hd.is_some() { &mut mb_hd } else { &mut mb };
|
|
mb.quad(
|
|
[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],
|
|
);
|
|
}
|
|
let mesh = |mb: MB, tex: &Texture2D| if mb.v.is_empty() { None } else { Some(Mesh { vertices: mb.v, indices: mb.i, texture: Some(tex.clone()) }) };
|
|
(mesh(mb, &gfx.sprites), mesh(mb_hd, &gfx.hd_sprites))
|
|
}
|
|
|
|
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 || unit_hidden(g, e) {
|
|
continue;
|
|
}
|
|
let big = matches!(e.kind, EK::Ogre | EK::Paladin | EK::Jailer | EK::Chainbreaker | EK::Wyrm | EK::Gargoyle | EK::Giant);
|
|
let lift = if flies(e.kind) && !(matches!(e.job, Job::Sleep(_)) && !e.moving) { 0.45 } else { 0.0 };
|
|
let head = proj(vec3(e.disp_x, lift + if big { 0.9 } else { 0.72 }, e.disp_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('☼', head.x - (GW * small * 0.5).floor(), head.y - 2.0 * GH * small, small, Color::from_rgba(255, 220, 90, 255));
|
|
}
|
|
// equipment from the forge: a little mark right of the health bar
|
|
if e.gear > 0 && 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).ceil() + 2.0 * k;
|
|
let c = [Color::from_rgba(170, 170, 180, 255), Color::from_rgba(110, 160, 255, 255), Color::from_rgba(90, 240, 255, 255)][(e.gear as usize - 1).min(2)];
|
|
draw_rectangle(x, head.y - GH * small, 3.0 * k, 4.0 * k, Color::from_rgba(20, 16, 20, 255));
|
|
draw_rectangle(x + k * 0.5, head.y - GH * small + k * 0.5, 2.0 * k, 3.0 * k, c);
|
|
}
|
|
// 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;
|
|
let rise = (k * 14.0 * F() as f32).floor();
|
|
gfx.glyph('z', head.x + cw * 2.0, head.y - GH * S() - rise, S(), Color::new(0.8, 0.85, 1.0, 1.0 - k));
|
|
}
|
|
// treasure without a sack sprite: a coin over the unit (imps carry a sack instead)
|
|
if e.carry > 0 && !Gfx::has_sack(e.kind) {
|
|
let p = proj(vec3(e.disp_x, 0.35, e.disp_y));
|
|
gfx.glyph('☼', 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 {
|
|
t.top()
|
|
}
|
|
|
|
// the colours of the knights' towers from the same model colours: brightness is stretched
|
|
// (strong contrast between masonry and joints), warm parts (wood, bronze) turn royal blue
|
|
fn knight_c(r: u8, g: u8, b: u8, f: f32) -> Color {
|
|
let l = ((r as f32 + g as f32 + b as f32) / (3.0 * 255.0) * 1.7).min(1.0);
|
|
let lerp = |a: f32, b: f32| a + (b - a) * l;
|
|
let warm = r as i32 - b as i32 > 30;
|
|
let (cr, cg, cb) = if warm {
|
|
(lerp(20.0, 70.0), lerp(30.0, 110.0), lerp(80.0, 230.0))
|
|
} else {
|
|
(lerp(28.0, 238.0), lerp(34.0, 232.0), lerp(72.0, 214.0))
|
|
};
|
|
Color::new(cr / 255.0 * f, cg / 255.0 * f, cb / 255.0 * f, 1.0)
|
|
}
|
|
|
|
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 matches!(k, EK::Chainbreaker | EK::Wyrm | EK::Giant) {
|
|
1.0
|
|
} else if matches!(k, EK::Ogre | EK::Paladin | EK::Jailer | EK::Gargoyle) {
|
|
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)
|
|
}
|
|
|
|
// 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 lift = if flies(e.kind) { 0.45 } else { 0.0 };
|
|
let base = vec3(e.disp_x, lift, e.disp_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.disp_x, floor_y(g, e.disp_x, e.disp_y) + 0.015, e.disp_y), 0.42 + 0.03 * (t * 3.0).sin(), Color::new(c.r, c.g, c.b, 0.18), 16);
|
|
}
|
|
}
|
|
}
|
|
|
|
// the dungeon's own towers: black obsidian and dark iron, bone spikes, skulls and chains (the
|
|
// red glowing joints are drawn in the glow pass); every level adds visible parts
|
|
fn draw_dark_tower(tw: &Tower) {
|
|
let (x, z) = tw.pos();
|
|
let c = vec3(x, 0.0, z);
|
|
let h = tower_height(tw);
|
|
let f = 0.8 + tw.flash * 1.5;
|
|
let l = tw.level;
|
|
let obs = |k: u8| shade_c(30 + k, 24 + k, 34 + k, f);
|
|
let iron = shade_c(58, 56, 64, f);
|
|
let bone = shade_c(214, 204, 178, f);
|
|
let blood = shade_c(128, 18, 26, f);
|
|
let bx = |p: Vec3, s: Vec3, cl: Color| draw_cube(c + p, s, None, cl);
|
|
let skull = |p: Vec3, dir: Vec3| {
|
|
bx(p, vec3(0.15, 0.14, 0.15), bone);
|
|
let side = vec3(dir.z, 0.0, -dir.x);
|
|
for sgn in [-1.0f32, 1.0] {
|
|
bx(p + dir * 0.076 + side * (0.035 * sgn) + vec3(0.0, 0.015, 0.0), vec3(0.04, 0.04, 0.04), shade_c(20, 8, 10, 1.0));
|
|
}
|
|
bx(p + vec3(0.0, -0.08, 0.0) + dir * 0.02, vec3(0.1, 0.04, 0.1), bone);
|
|
};
|
|
let faces = [vec3(0.0, 0.0, 1.0), vec3(1.0, 0.0, 0.0), vec3(0.0, 0.0, -1.0), vec3(-1.0, 0.0, 0.0)];
|
|
match tw.kind {
|
|
TowerKind::Arrow => {
|
|
// spike spire: stepped obsidian foot, a tapering keep, a crown of bone spikes
|
|
bx(vec3(0.0, 0.07, 0.0), vec3(0.92, 0.14, 0.92), obs(4));
|
|
bx(vec3(0.0, 0.19, 0.0), vec3(0.78, 0.1, 0.78), obs(10));
|
|
let segs = 4;
|
|
for k in 0..segs {
|
|
let y0 = 0.24 + (h - 0.24) * k as f32 / segs as f32;
|
|
let hh = (h - 0.24) / segs as f32;
|
|
let w = 0.64 - k as f32 * 0.04;
|
|
bx(vec3(0.0, y0 + hh * 0.5, 0.0), vec3(w, hh - 0.03, w), obs((k * 4) as u8 + 8));
|
|
}
|
|
// buttress spikes on the corners
|
|
for (dx, dz) in [(0.36f32, 0.36f32), (-0.36, 0.36), (0.36, -0.36), (-0.36, -0.36)] {
|
|
bx(vec3(dx, 0.35, dz), vec3(0.12, 0.5, 0.12), obs(6));
|
|
line3(c + vec3(dx, 0.58, dz), c + vec3(dx * 1.45, 0.95, dz * 1.45), 0.06, bone);
|
|
}
|
|
bx(vec3(0.0, h + 0.03, 0.0), vec3(0.86, 0.07, 0.86), iron);
|
|
// crown of bone spikes (more with every level)
|
|
let n = 4 + 2 * l as usize;
|
|
for k in 0..n {
|
|
let a = k as f32 / n as f32 * std::f32::consts::TAU;
|
|
let p = c + vec3(a.cos() * 0.4, h + 0.06, a.sin() * 0.4);
|
|
line3(p, p + vec3(a.cos() * 0.08, 0.26 + 0.06 * (k % 2) as f32, a.sin() * 0.08), 0.05, bone);
|
|
}
|
|
// a small ballista turned towards the last target
|
|
let (ax, az) = (tw.aim.cos(), tw.aim.sin());
|
|
line3(c + vec3(-ax * 0.25, h + 0.16, -az * 0.25), c + vec3(ax * 0.32, h + 0.16, az * 0.32), 0.06, iron);
|
|
line3(c + vec3(-az * 0.22, h + 0.2, ax * 0.22), c + vec3(az * 0.22, h + 0.2, -ax * 0.22), 0.04, shade_c(90, 60, 40, f));
|
|
skull(vec3(0.0, h * 0.72, 0.33), faces[0]);
|
|
if l >= 2 {
|
|
// banner of the dungeon
|
|
bx(vec3(0.34, h + 0.5, -0.34), vec3(0.03, 0.9, 0.03), iron);
|
|
bx(vec3(0.46, h + 0.72, -0.34), vec3(0.22, 0.34, 0.02), blood);
|
|
skull(vec3(0.33, h * 0.72, 0.0), faces[1]);
|
|
}
|
|
if l >= 3 {
|
|
skull(vec3(0.0, h * 0.72, -0.33), faces[2]);
|
|
skull(vec3(-0.33, h * 0.72, 0.0), faces[3]);
|
|
for sgn in [-1.0f32, 1.0] {
|
|
line3(c + vec3(sgn * 0.3, h + 0.08, 0.0), c + vec3(sgn * 0.55, h + 0.55, 0.0), 0.08, bone);
|
|
line3(c + vec3(sgn * 0.55, h + 0.55, 0.0), c + vec3(sgn * 0.45, h + 0.75, 0.0), 0.05, bone);
|
|
}
|
|
}
|
|
}
|
|
TowerKind::Cannon => {
|
|
// hellmouth: a riveted iron drum on an obsidian foot, a demon-head barrel on top
|
|
bx(vec3(0.0, 0.07, 0.0), vec3(0.96, 0.14, 0.96), obs(4));
|
|
for k in 0..3 {
|
|
let y = 0.14 + (h - 0.14) * (k as f32 + 0.5) / 3.0;
|
|
let r = 0.86 - k as f32 * 0.04;
|
|
bx(vec3(0.0, y, 0.0), vec3(r, (h - 0.14) / 3.0 - 0.02, r * 0.8), obs(14));
|
|
bx(vec3(0.0, y, 0.0), vec3(r * 0.8, (h - 0.14) / 3.0 - 0.02, r), obs(10));
|
|
// rivets
|
|
for q in 0..8 {
|
|
let a = q as f32 * 0.785;
|
|
bx(vec3(a.cos() * r * 0.46, y, a.sin() * r * 0.46), Vec3::splat(0.045), iron);
|
|
}
|
|
}
|
|
bx(vec3(0.0, h + 0.04, 0.0), vec3(0.66, 0.08, 0.66), iron);
|
|
let (ax, az) = (tw.aim.cos(), tw.aim.sin());
|
|
let along = |d: f32, y: f32| c + vec3(ax * d, h + y, az * d);
|
|
// the demon head: a heavy block with horns, its open jaw is the muzzle
|
|
draw_cube(along(0.0, 0.24), Vec3::splat(0.34), None, iron);
|
|
for k in 1..4 {
|
|
draw_cube(along(k as f32 * 0.14, 0.24), Vec3::splat(0.3 - k as f32 * 0.02), None, shade_c(44, 42, 50, f));
|
|
}
|
|
draw_cube(along(0.56, 0.24), Vec3::splat(0.26), None, obs(20));
|
|
for sgn in [-1.0f32, 1.0] {
|
|
let side = vec3(-az * sgn, 0.0, ax * sgn);
|
|
line3(along(0.05, 0.4) + side * 0.14, along(-0.1, 0.72) + side * 0.26, 0.07, bone);
|
|
}
|
|
if l >= 2 {
|
|
// chains around the drum
|
|
for k in 0..8 {
|
|
let a0 = k as f32 * 0.785;
|
|
let a1 = a0 + 0.785;
|
|
line3(c + vec3(a0.cos() * 0.47, h * 0.55, a0.sin() * 0.47), c + vec3(a1.cos() * 0.47, h * 0.5 - 0.04, a1.sin() * 0.47), 0.035, iron);
|
|
}
|
|
skull(vec3(0.0, h * 0.5, 0.45), faces[0]);
|
|
}
|
|
if l >= 3 {
|
|
for k in 0..8 {
|
|
let a = k as f32 * 0.785 + 0.39;
|
|
let p = c + vec3(a.cos() * 0.44, h * 0.85, a.sin() * 0.44);
|
|
line3(p, p + vec3(a.cos() * 0.25, 0.08, a.sin() * 0.25), 0.05, bone);
|
|
}
|
|
skull(vec3(0.0, h * 0.5, -0.45), faces[2]);
|
|
}
|
|
}
|
|
_ => {
|
|
// dark spire: an obsidian needle between curved horns, claws hold the crystal
|
|
bx(vec3(0.0, 0.06, 0.0), vec3(0.86, 0.12, 0.86), obs(4));
|
|
bx(vec3(0.0, 0.17, 0.0), vec3(0.6, 0.1, 0.6), obs(12));
|
|
bx(vec3(0.0, h * 0.5, 0.0), vec3(0.34, h, 0.34), obs(8));
|
|
bx(vec3(0.0, h * 0.3, 0.0), vec3(0.44, h * 0.3, 0.44), obs(14));
|
|
let horns = 2 + l as usize;
|
|
for k in 0..horns {
|
|
let a = k as f32 / horns as f32 * std::f32::consts::TAU + 0.4;
|
|
let mut prev = c + vec3(a.cos() * 0.4, 0.2, a.sin() * 0.4);
|
|
for q in 1..=5 {
|
|
let fq = q as f32 / 5.0;
|
|
let r = 0.4 + (fq * 3.14).sin() * 0.18 - fq * 0.22;
|
|
let next = c + vec3(a.cos() * r, 0.2 + fq * (h - 0.1), a.sin() * r);
|
|
line3(prev, next, 0.09 - fq * 0.05, if q == 5 { bone } else { obs(18) });
|
|
prev = next;
|
|
}
|
|
}
|
|
for k in 0..4 {
|
|
let a = k as f32 * 1.57 + 0.78;
|
|
line3(c + vec3(a.cos() * 0.14, h, a.sin() * 0.14), c + vec3(a.cos() * 0.24, h + 0.42, a.sin() * 0.24), 0.05, bone);
|
|
}
|
|
skull(vec3(0.0, h * 0.35, 0.24), faces[0]);
|
|
if l >= 3 {
|
|
skull(vec3(0.0, h * 0.35, -0.24), faces[2]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn draw_towers_solid(g: &Game) {
|
|
for tw in g.towers.iter().filter(|t| g.map.shown(t.tile)) {
|
|
if !tw.hero && tw.kind != TowerKind::Rune {
|
|
draw_dark_tower(tw);
|
|
continue;
|
|
}
|
|
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: bright limestone with dark blue shadows, wood and fittings in royal blue
|
|
let col = |r: u8, g: u8, b: u8| if tw.hero { knight_c(r, g, b, 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 => {
|
|
// 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 => {
|
|
// squat round tower (stacked rings), iron bands, carriage, barrel, cannon balls
|
|
for k in 0..4 {
|
|
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 => {
|
|
// 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));
|
|
}
|
|
}
|
|
TowerKind::Rune => {
|
|
// dwarven rune forge: broad granite foot, bronze bands, a tapering keep with
|
|
// carved buttresses, an anvil crown and a forge hearth on top (runes glow)
|
|
bx(vec3(0.0, 0.07, 0.0), vec3(0.96, 0.14, 0.96), col(70, 66, 70));
|
|
bx(vec3(0.0, 0.19, 0.0), vec3(0.84, 0.1, 0.84), col(84, 80, 86));
|
|
let steps = 5;
|
|
for k in 0..steps {
|
|
let y0 = 0.24 + (h - 0.24) * k as f32 / steps as f32;
|
|
let hh = (h - 0.24) / steps as f32;
|
|
let w = 0.72 - k as f32 * 0.05;
|
|
bx(vec3(0.0, y0 + hh * 0.5, 0.0), vec3(w, hh + 0.005, w), col(96 - k as u8 * 2, 92 - k as u8 * 2, 98));
|
|
bx(vec3(0.0, y0 + 0.01, 0.0), vec3(w + 0.03, 0.035, w + 0.03), shade_c(176, 112, 56, f));
|
|
}
|
|
for (dx, dz) in [(0.38f32, 0.38f32), (-0.38, 0.38), (0.38, -0.38), (-0.38, -0.38)] {
|
|
bx(vec3(dx, h * 0.3, dz), vec3(0.14, h * 0.6, 0.14), col(78, 74, 80));
|
|
bx(vec3(dx, h * 0.6 + 0.05, dz), vec3(0.17, 0.08, 0.17), shade_c(176, 112, 56, f));
|
|
}
|
|
// gate with a bronze door
|
|
bx(vec3(0.0, 0.42, 0.37), vec3(0.26, 0.4, 0.04), shade_c(120, 76, 40, f));
|
|
bx(vec3(0.0, 0.64, 0.38), vec3(0.32, 0.06, 0.05), col(110, 106, 112));
|
|
// anvil crown and hearth
|
|
bx(vec3(0.0, h + 0.05, 0.0), vec3(0.62, 0.1, 0.62), col(60, 58, 64));
|
|
bx(vec3(0.0, h + 0.16, 0.0), vec3(0.3, 0.12, 0.42), col(48, 48, 54));
|
|
bx(vec3(0.0, h + 0.25, 0.0), vec3(0.42, 0.08, 0.56), col(56, 56, 62));
|
|
for (dx, dz) in [(-0.27f32, -0.27f32), (0.27, -0.27), (0.27, 0.27), (-0.27, 0.27)] {
|
|
bx(vec3(dx, h + 0.16, dz), vec3(0.08, 0.22, 0.08), shade_c(176, 112, 56, f));
|
|
}
|
|
}
|
|
}
|
|
// 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));
|
|
}
|
|
}
|
|
}
|
|
|
|
fn draw_towers_glow(g: &Game, t: f32) {
|
|
for (k, tw) in g.towers.iter().enumerate().filter(|(_, t)| g.map.shown(t.tile)) {
|
|
if !tw.hero && tw.kind != TowerKind::Rune {
|
|
// red glowing joints, arrow slits and the hellmouth's fire
|
|
let (x, z) = tw.pos();
|
|
let c = vec3(x, 0.0, z);
|
|
let h = tower_height(tw);
|
|
let glow = 0.55 + 0.35 * (t * 2.2 + k as f32).sin() + tw.flash * 2.0;
|
|
let ember = Color::new(1.0, 0.22, 0.08, glow.min(1.0));
|
|
match tw.kind {
|
|
TowerKind::Arrow => {
|
|
for q in 1..4 {
|
|
let y = 0.24 + (h - 0.24) * q as f32 / 4.0;
|
|
let w = 0.64 - q as f32 * 0.04 + 0.01;
|
|
draw_cube(c + vec3(0.0, y - 0.015, 0.0), vec3(w, 0.025, w), None, ember);
|
|
}
|
|
for d in [vec3(0.0, 0.0, 0.31), vec3(0.31, 0.0, 0.0), vec3(0.0, 0.0, -0.31), vec3(-0.31, 0.0, 0.0)] {
|
|
draw_cube(c + d + vec3(0.0, h * 0.45, 0.0), vec3(if d.x == 0.0 { 0.05 } else { 0.02 }, 0.18, if d.z == 0.0 { 0.05 } else { 0.02 }), None, Color::new(1.0, 0.35, 0.1, 0.9));
|
|
}
|
|
}
|
|
TowerKind::Cannon => {
|
|
let (ax, az) = (tw.aim.cos(), tw.aim.sin());
|
|
let m = c + vec3(ax * 0.7, h + 0.24, az * 0.7);
|
|
draw_sphere(m, 0.1 * (1.0 + tw.flash * 3.0), None, Color::new(1.0, 0.5, 0.1, 0.95));
|
|
draw_sphere(m, 0.2 * (1.0 + tw.flash * 3.0), None, Color::new(1.0, 0.3, 0.05, 0.3));
|
|
for q in 1..3 {
|
|
let y = 0.14 + (h - 0.14) * q as f32 / 3.0;
|
|
draw_cube(c + vec3(0.0, y, 0.0), vec3(0.84, 0.02, 0.84), None, ember);
|
|
}
|
|
}
|
|
_ => {
|
|
// the crystal in the claws and shards around it
|
|
let cc = vec3(x, h + 0.42 + (t * 2.0 + k as f32).sin() * 0.05, z);
|
|
let pulse = 1.0 + (t * 4.0 + k as f32).sin() * 0.1 + tw.flash * 2.0;
|
|
draw_sphere(cc, 0.15 * pulse, None, Color::new(0.85, 0.3, 1.0, 0.95));
|
|
draw_sphere(cc, 0.32 * pulse, None, Color::new(0.7, 0.2, 1.0, 0.22));
|
|
for q in 0..(1 + tw.level) {
|
|
let a = t * 1.5 + q as f32 * 2.1;
|
|
draw_cube(cc + vec3(a.cos() * 0.42, (t * 2.0 + q as f32).sin() * 0.12, a.sin() * 0.42), vec3(0.05, 0.12, 0.05), None, Color::new(0.85, 0.4, 1.0, 0.9));
|
|
}
|
|
for q in 0..3 {
|
|
let y = h * (0.45 + q as f32 * 0.17);
|
|
let gl = 0.4 + 0.6 * ((t * 2.5 - q as f32 * 0.8 + k as f32).sin() * 0.5 + 0.5);
|
|
for (dx, dz) in [(0.175f32, 0.0f32), (-0.175, 0.0), (0.0, 0.175), (0.0, -0.175)] {
|
|
draw_cube(c + vec3(dx, y, dz), vec3(if dx == 0.0 { 0.08 } else { 0.02 }, 0.07, if dz == 0.0 { 0.08 } else { 0.02 }), None, Color::new(1.0, 0.25, 0.4, gl));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
if tw.kind == TowerKind::Rune {
|
|
// forge glow on the crown, runes on the keep and three rune stones circling it
|
|
let (x, z) = tw.pos();
|
|
let h = tower_height(tw);
|
|
let pulse = 1.0 + (t * 3.0 + k as f32).sin() * 0.12 + tw.flash * 2.5;
|
|
draw_sphere(vec3(x, h + 0.34, z), 0.1 * pulse, None, Color::new(1.0, 0.75, 0.3, 0.95));
|
|
draw_sphere(vec3(x, h + 0.36, z), 0.24 * pulse, None, Color::new(1.0, 0.55, 0.15, 0.22));
|
|
for r in 0..4 {
|
|
let y = 0.4 + (h - 0.5) * r as f32 / 4.0;
|
|
let glow = 0.35 + 0.65 * ((t * 2.0 - r as f32 * 0.9 + k as f32).sin() * 0.5 + 0.5);
|
|
let w = 0.72 - r as f32 * 0.05;
|
|
for (dx, dz) in [(w * 0.5 + 0.01, 0.0f32), (-w * 0.5 - 0.01, 0.0), (0.0, w * 0.5 + 0.01), (0.0, -w * 0.5 - 0.01)] {
|
|
draw_cube(vec3(x + dx, y + 0.1, z + dz), vec3(if dx == 0.0 { 0.12 } else { 0.02 }, 0.1, if dz == 0.0 { 0.12 } else { 0.02 }), None, Color::new(0.35, 0.8, 1.0, glow));
|
|
}
|
|
}
|
|
for n in 0..(2 + tw.level) {
|
|
let a = t * 1.2 + n as f32 * std::f32::consts::TAU / (2 + tw.level) as f32;
|
|
let p = vec3(x + a.cos() * 0.62, h * 0.7 + (t * 2.0 + n as f32).sin() * 0.08, z + a.sin() * 0.62);
|
|
draw_cube(p, vec3(0.1, 0.12, 0.1), None, Color::new(0.3, 0.75, 1.0, 0.9));
|
|
draw_sphere(p, 0.12, None, Color::new(0.3, 0.7, 1.0, 0.2));
|
|
}
|
|
continue;
|
|
}
|
|
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));
|
|
// 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));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------ 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)
|
|
pub 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 });
|
|
}
|
|
|
|
pub 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 });
|
|
}
|
|
|
|
// an upright ellipse in the x-y plane (portal vortex)
|
|
fn vdisc(c: Vec3, rx: f32, ry: f32, color: Color, seg: usize) {
|
|
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];
|
|
let mut v = vec![Vertex { position: c, uv: Vec2::ZERO, color: cc, normal: Vec4::Z }];
|
|
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() * rx, a.sin() * ry, 0.0), uv: Vec2::ZERO, color: cc, normal: Vec4::Z });
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
|
|
// the obsidian throne of a soul core (centre c on the floor)
|
|
fn draw_throne(c: Vec3, t: f32) {
|
|
let obs = |k: u8| Color::from_rgba(28 + k, 14 + k / 2, 20 + k / 2, 255);
|
|
draw_cube(c + vec3(0.0, 0.08, 0.0), vec3(2.9, 0.16, 2.9), None, obs(6));
|
|
draw_cube(c + vec3(0.0, 0.22, 0.0), vec3(2.3, 0.14, 2.3), None, obs(14));
|
|
draw_cube(c + vec3(0.0, 0.36, 0.0), vec3(1.5, 0.14, 1.5), None, obs(24));
|
|
draw_cube(c + vec3(0.0, 0.48, 0.0), vec3(0.8, 0.1, 0.8), None, obs(32));
|
|
let heart = c + vec3(0.0, 1.55, 0.0);
|
|
let iron = Color::from_rgba(70, 64, 72, 255);
|
|
for (k, (dx, dz)) in [(-1.2f32, -1.2f32), (1.2, -1.2), (1.2, 1.2), (-1.2, 1.2)].iter().enumerate() {
|
|
let b = c + vec3(*dx, 0.0, *dz);
|
|
draw_cube(b + vec3(0.0, 0.3, 0.0), vec3(0.36, 0.6, 0.36), None, obs(4));
|
|
// a curved spike leaning towards the heart
|
|
let mut prev = b + vec3(0.0, 0.55, 0.0);
|
|
for q in 1..=5 {
|
|
let f = q as f32 / 5.0;
|
|
let next = b + vec3(dx * 0.22 * f * f, 0.55 + f * 0.9, dz * 0.22 * f * f);
|
|
line3(prev, next, 0.2 - f * 0.14, if q == 5 { Color::from_rgba(200, 180, 150, 255) } else { obs(10) });
|
|
prev = next;
|
|
}
|
|
// a sagging chain from the spike to the heart (links alternate)
|
|
let top = b + vec3(dx * 0.12, 1.3, dz * 0.12);
|
|
for q in 0..9 {
|
|
let f0 = q as f32 / 9.0;
|
|
let f1 = (q + 1) as f32 / 9.0;
|
|
let sag = |f: f32| -0.18 * (f * std::f32::consts::PI).sin() + (t * 1.3 + k as f32).sin() * 0.02;
|
|
let p0 = top.lerp(heart, f0 * 0.82) + vec3(0.0, sag(f0), 0.0);
|
|
let p1 = top.lerp(heart, f1 * 0.82) + vec3(0.0, sag(f1), 0.0);
|
|
line3(p0, p1, if q % 2 == 0 { 0.06 } else { 0.035 }, iron);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn draw_landmarks_solid(g: &Game, t: f32) {
|
|
// soul core: an obsidian throne whose four curved spikes hold the crystal heart in chains
|
|
let (cx, cy) = g.map.core;
|
|
draw_throne(vec3(cx as f32 + 1.5, 0.0, cy as f32 + 1.5), t);
|
|
// memorial stones of fallen creatures: a headstone on a little plinth, a rune that glows
|
|
for m in &g.memorials {
|
|
if !g.map.shown(m.tile) {
|
|
continue;
|
|
}
|
|
let (x, y) = xy(m.tile);
|
|
let h = (m.name % 7) as f32 * 0.02;
|
|
let c = vec3(x as f32 + 0.5 + ((m.name >> 3) % 5) as f32 * 0.04 - 0.08, 0.0, y as f32 + 0.5);
|
|
let stone = Color::from_rgba(118, 114, 124, 255);
|
|
draw_cube(c + vec3(0.0, 0.04, 0.0), vec3(0.42, 0.08, 0.26), None, Color::from_rgba(70, 66, 74, 255));
|
|
draw_cube(c + vec3(0.0, 0.3 + h * 0.5, 0.0), vec3(0.3, 0.46 + h, 0.1), None, stone);
|
|
draw_cube(c + vec3(0.0, 0.56 + h, 0.0), vec3(0.2, 0.06, 0.1), None, stone);
|
|
let glow = if m.level >= 8 { Color::from_rgba(255, 210, 90, 255) } else { Color::from_rgba(150, 120, 255, 255) };
|
|
draw_cube(c + vec3(0.0, 0.34 + h * 0.5, 0.052), vec3(0.06, 0.14, 0.01), None, glow);
|
|
}
|
|
// a ranger's snares: iron jaws on the floor, open and waiting
|
|
for &i in &g.snares {
|
|
if !g.map.shown(i) {
|
|
continue;
|
|
}
|
|
let (x, y) = xy(i);
|
|
let c = vec3(x as f32 + 0.5, 0.0, y as f32 + 0.5);
|
|
let iron = Color::from_rgba(96, 92, 100, 255);
|
|
let dark = Color::from_rgba(40, 36, 42, 255);
|
|
draw_cube(c + vec3(0.0, 0.02, 0.0), vec3(0.34, 0.03, 0.34), None, dark);
|
|
for side in [-1.0f32, 1.0] {
|
|
draw_cube(c + vec3(side * 0.16, 0.06, 0.0), vec3(0.04, 0.08, 0.36), None, iron);
|
|
for k in 0..3 {
|
|
let z = -0.12 + k as f32 * 0.12;
|
|
draw_cube(c + vec3(side * 0.12, 0.11, z), vec3(0.03, 0.05, 0.03), None, Color::from_rgba(200, 200, 210, 255));
|
|
}
|
|
}
|
|
}
|
|
// the other masters' cores: the same throne (their hearts glow in their colours)
|
|
for rv in g.rivals.iter().filter(|r| r.alive && g.map.shown(idx(r.core.0 + 1, r.core.1 + 1))) {
|
|
let (rx, rz) = rv.centre();
|
|
draw_throne(vec3(rx, 0.0, rz), t + rv.side as f32);
|
|
}
|
|
// portals: a demon gate - an arch of stone blocks with bone ribs and horns on a stepped
|
|
// platform (the vortex glows in the glow pass); grey and dead until claimed
|
|
for (pk, &(px, py)) in g.map.portals.iter().enumerate() {
|
|
if !g.map.shown(idx(px + 1, py + 1)) {
|
|
continue;
|
|
}
|
|
let tint = portal_tint(g, pk);
|
|
let owned = tint.is_some();
|
|
let pc = vec3(px as f32 + 1.5, 0.0, py as f32 + 1.5);
|
|
let stone = |k: u8| if owned { hue_to(tint, Color::from_rgba(46 + k, 30 + k / 2, 58 + k, 255)) } else { Color::from_rgba(56 + k, 54 + k, 60 + k, 255) };
|
|
let bone = if owned { Color::from_rgba(212, 200, 176, 255) } else { Color::from_rgba(150, 146, 140, 255) };
|
|
draw_cube(pc + vec3(0.0, 0.05, 0.0), vec3(2.9, 0.1, 2.9), None, stone(0));
|
|
draw_cube(pc + vec3(0.0, 0.14, 0.0), vec3(2.4, 0.08, 1.4), None, stone(8));
|
|
let r = 1.25;
|
|
for side in [-1.0f32, 1.0] {
|
|
// pillars of stacked, slightly offset blocks
|
|
for k in 0..5 {
|
|
let off = if k % 2 == 0 { 0.03 } else { -0.03 };
|
|
draw_cube(pc + vec3(side * (r + off), 0.32 + k as f32 * 0.36, 0.0), vec3(0.42, 0.34, 0.5), None, stone((k * 5) as u8 + 6));
|
|
}
|
|
// a bone rib along the inner edge
|
|
line3(pc + vec3(side * (r - 0.27), 0.2, 0.26), pc + vec3(side * (r - 0.27), 1.95, 0.26), 0.06, bone);
|
|
line3(pc + vec3(side * (r - 0.27), 0.2, -0.26), pc + vec3(side * (r - 0.27), 1.95, -0.26), 0.06, bone);
|
|
}
|
|
// the arch: blocks on a half circle
|
|
for k in 0..=8 {
|
|
let a = k as f32 / 8.0 * std::f32::consts::PI;
|
|
let p = pc + vec3(a.cos() * r, 2.0 + a.sin() * r * 0.9, 0.0);
|
|
draw_cube(p, vec3(0.4, 0.34, 0.52), None, stone(16 + (k % 3) as u8 * 4));
|
|
}
|
|
// keystone skull and horns
|
|
let top = pc + vec3(0.0, 2.0 + r * 0.9 + 0.12, 0.0);
|
|
draw_cube(top, vec3(0.3, 0.28, 0.3), None, bone);
|
|
for sgn in [-1.0f32, 1.0] {
|
|
draw_cube(top + vec3(sgn * 0.07, 0.03, 0.155), vec3(0.07, 0.07, 0.02), None, Color::from_rgba(20, 8, 16, 255));
|
|
let mut prev = top + vec3(sgn * 0.15, 0.08, 0.0);
|
|
for q in 1..=4 {
|
|
let f = q as f32 / 4.0;
|
|
let next = top + vec3(sgn * (0.15 + f * 0.55), 0.08 + (f * 2.2).sin() * 0.45, 0.0);
|
|
line3(prev, next, 0.1 - f * 0.07, bone);
|
|
prev = next;
|
|
}
|
|
}
|
|
}
|
|
// gate pillars
|
|
let (gx, gz) = g.gate_center();
|
|
if g.gate_hp > 0.0 && !g.map.shown(idx(gx as i32, gz as i32)) {
|
|
// unexplored: not drawn
|
|
} else if g.gate_hp > 0.0 {
|
|
let stone = Color::from_rgba(150, 155, 172, 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;
|
|
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;
|
|
}
|
|
|
|
// rain streaks and fog veils over the land above ground near the camera; a storm's lightning
|
|
// lights the sky up for a moment
|
|
fn draw_weather(g: &Game, cam: &Cam, t: f32) {
|
|
let w = &g.weather;
|
|
if w.level <= 0.01 || g.map.sky.is_empty() {
|
|
return;
|
|
}
|
|
let (cx, cz) = (cam.rx as i32, cam.rz as i32);
|
|
let reach = (cam.view_h() * 1.2) as i32 + 4;
|
|
let sky = |x: i32, z: i32| inb(x, z) && g.map.sky(idx(x, z));
|
|
// fog: veils at two heights, one plane per run of open sky tiles in a row
|
|
let fog = w.fog();
|
|
if fog > 0.01 {
|
|
for (h, a) in [(0.7f32, 0.22f32), (1.6, 0.16)] {
|
|
let col = Color::new(0.75, 0.78, 0.8, a * fog);
|
|
for z in cz - reach..=cz + reach {
|
|
let mut x = cx - reach;
|
|
while x <= cx + reach {
|
|
if !sky(x, z) {
|
|
x += 1;
|
|
continue;
|
|
}
|
|
let x0 = x;
|
|
while x <= cx + reach && sky(x, z) {
|
|
x += 1;
|
|
}
|
|
let len = (x - x0) as f32;
|
|
let drift = (t * 0.2 + z as f32 * 0.7).sin() * 0.15;
|
|
draw_plane(vec3(x0 as f32 + len * 0.5 + drift, h, z as f32 + 0.5), vec2(len * 0.5, 0.5), None, col);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// rain: slanted streaks falling over the open land
|
|
let rain = w.rain();
|
|
if rain > 0.01 {
|
|
let n = (1100.0 * rain) as u32;
|
|
let col = Color::new(0.75, 0.82, 0.95, 0.8);
|
|
let span = (2 * reach + 1) as u32;
|
|
for k in 0..n {
|
|
let h = k.wrapping_mul(2654435761) ^ 0x9e37_79b9;
|
|
let x = cx - reach + (h % span) as i32;
|
|
let z = cz - reach + ((h >> 11) % span) as i32;
|
|
if !sky(x, z) {
|
|
continue;
|
|
}
|
|
let fx = x as f32 + ((h >> 3) % 100) as f32 / 100.0;
|
|
let fz = z as f32 + ((h >> 17) % 100) as f32 / 100.0;
|
|
let fall = (t * 1.6 + (h % 1000) as f32 / 1000.0).fract();
|
|
let y = 3.0 * (1.0 - fall);
|
|
line3(vec3(fx, y + 0.5, fz - 0.08), vec3(fx + 0.05, y, fz + 0.08), 0.035, col);
|
|
}
|
|
}
|
|
// the flash of a lightning bolt
|
|
let since = g.time - w.flash;
|
|
if w.flash > 0.0 && (0.0..0.25).contains(&since) {
|
|
let a = 0.35 * (1.0 - since / 0.25);
|
|
for z in (cz - reach..=cz + reach).step_by(4) {
|
|
draw_plane(vec3(cx as f32, 0.05, z as f32 + 2.0), vec2(reach as f32, 2.0), None, Color::new(0.9, 0.92, 1.0, a));
|
|
}
|
|
}
|
|
}
|
|
|
|
// growth stage of a giant mushroom: 0 a sprout .. 4 fully grown (felling only then)
|
|
fn mushroom_stage(t: &Tile) -> u32 {
|
|
if t.grown() {
|
|
4
|
|
} else {
|
|
((t.growth() * 4.0) as u32).min(3)
|
|
}
|
|
}
|
|
|
|
// the colour of a master side (the first master red, rivals green, blue and yellow)
|
|
pub fn master_rgb(side: u8) -> Vec3 {
|
|
match side {
|
|
2 => vec3(0.3, 0.95, 0.4),
|
|
3 => vec3(0.35, 0.55, 1.0),
|
|
4 => vec3(1.0, 0.85, 0.25),
|
|
_ => vec3(1.0, 0.2, 0.2),
|
|
}
|
|
}
|
|
|
|
// a portal claimed by a master glows in his colour; None: unclaimed (grey and still)
|
|
fn portal_tint(g: &Game, k: usize) -> Option<Vec3> {
|
|
let side = g.map.portal_owner(k).side();
|
|
Owner::MASTER_SIDES.contains(&side).then(|| master_rgb(side))
|
|
}
|
|
|
|
// recolours a red-tinted colour into `tint`: keeps its brightness and whiteness, swaps the hue
|
|
fn hue_to(tint: Option<Vec3>, c: Color) -> Color {
|
|
let Some(t) = tint else { return c };
|
|
let hi = c.r.max(c.g).max(c.b);
|
|
let lo = c.r.min(c.g).min(c.b);
|
|
let m = t.x.max(t.y).max(t.z).max(0.001);
|
|
let k = (hi - lo) / m;
|
|
Color::new(lo + t.x * k, lo + t.y * k, lo + t.z * k, c.a)
|
|
}
|
|
|
|
// a soul core's crystal heart beating lub-dub (faster when hurt, cracked when badly hurt),
|
|
// glowing veins across the floor, rising embers; `tint`: another master's colour for the red
|
|
fn draw_heart(base: Vec3, hp: f32, t: f32, cam_r: Vec3, tint: Option<Vec3>) {
|
|
let tc = |r: f32, g: f32, b: f32, a: f32| hue_to(tint, Color::new(r, g, b, a));
|
|
let rate = 0.9 + (1.0 - hp) * 1.6;
|
|
let ph = (t * rate).fract();
|
|
let beat = (-((ph - 0.02) / 0.06).powi(2)).exp() + 0.65 * (-((ph - 0.2) / 0.06).powi(2)).exp();
|
|
let pulse = 1.0 + beat * 0.09;
|
|
let hc = base + vec3(0.0, 1.55 + (t * 1.1).sin() * 0.03, 0.0);
|
|
let right = vec3(cam_r.x, 0.0, cam_r.z).normalize_or_zero();
|
|
let up = Vec3::Y;
|
|
let eye = EYE.with(|e| e.get());
|
|
let front = vec3(eye.x, 0.0, eye.z).normalize_or_zero(); // towards the viewer
|
|
let red = tc(0.38 + 0.38 * hp + 0.2 * beat, 0.02 + 0.04 * beat, 0.07, 1.0);
|
|
let hot = tc(1.0, 0.35 + 0.25 * beat, 0.3, 0.7);
|
|
// two lobes and the point below, then the facets as bright chips
|
|
let s0 = 0.36 * pulse;
|
|
for sgn in [-1.0f32, 1.0] {
|
|
draw_sphere(hc + right * (sgn * 0.21 * pulse) + up * (0.12 * pulse), s0 * 0.85, None, red);
|
|
}
|
|
for (k, (y, r)) in [(-0.05f32, 0.36f32), (-0.22, 0.28), (-0.36, 0.18), (-0.46, 0.09)].iter().enumerate() {
|
|
draw_sphere(hc + up * (y * pulse), r * pulse, None, if k == 0 { red } else { tc(red.r * 0.9, red.g, red.b, 1.0) });
|
|
}
|
|
draw_sphere(hc + right * (-0.12 * pulse) + up * (0.2 * pulse), 0.1 * pulse, None, tc(1.0, 0.75, 0.7, 0.75));
|
|
for k in 0..7 {
|
|
let a = k as f32 * 0.9 + 0.4;
|
|
let p = hc + right * (a.cos() * 0.22 * pulse) + up * (a.sin() * 0.2 * pulse - 0.05) + front * 0.3;
|
|
draw_cube(p, Vec3::splat(0.05), None, tc(1.0, 0.5 + 0.3 * ((t * 3.0 + k as f32).sin() * 0.5 + 0.5), 0.45, 0.8));
|
|
}
|
|
draw_sphere(hc, 0.75 * pulse, None, tc(1.0, 0.15, 0.15, 0.16 + 0.12 * beat));
|
|
draw_sphere(hc, 1.1 * pulse, None, tc(1.0, 0.1, 0.1, 0.06 + 0.05 * beat));
|
|
// cracks when hurt
|
|
let cracks = ((1.0 - hp) * 7.0) as usize;
|
|
for k in 0..cracks.min(6) {
|
|
let a = k as f32 * 2.3;
|
|
let p0 = hc + right * (a.cos() * 0.08) + up * (a.sin() * 0.1);
|
|
let p1 = p0 + right * (a.cos() * 0.25) + up * ((a * 1.7).sin() * 0.22);
|
|
line3(p0 + front * 0.37, p1 + front * 0.33, 0.025, tc(0.08, 0.0, 0.02, 1.0));
|
|
}
|
|
// veins across the floor, a pulse running outwards with every beat
|
|
for k in 0..8 {
|
|
let a = k as f32 * 0.785 + 0.2;
|
|
let mut prev = base + vec3(a.cos() * 1.3, 0.02, a.sin() * 1.3);
|
|
for q in 1..=6 {
|
|
let f = q as f32 / 6.0;
|
|
let wig = ((k * 7 + q * 3) as f32).sin() * 0.18;
|
|
let next = base + vec3(a.cos() * (1.3 + f * 2.2) - a.sin() * wig, 0.02, a.sin() * (1.3 + f * 2.2) + a.cos() * wig);
|
|
let wave = (-((f - ph * 1.6) / 0.18).powi(2)).exp();
|
|
line3(prev, next, 0.05, tc(1.0, 0.15 + 0.3 * wave, 0.1, (0.25 + 0.7 * wave) * (1.0 - f * 0.5) * (0.4 + 0.6 * hp)));
|
|
prev = next;
|
|
}
|
|
}
|
|
// embers rising from the heart
|
|
for k in 0..10 {
|
|
let e = (t * 0.35 + k as f32 * 0.137).fract();
|
|
let a = k as f32 * 2.4;
|
|
let p = hc + vec3(a.cos() * (0.2 + e * 0.5), -0.2 + e * 2.0, a.sin() * (0.2 + e * 0.5));
|
|
draw_cube(p, Vec3::splat(0.04 * (1.0 - e) + 0.01), None, tc(1.0, 0.45 + 0.4 * (1.0 - e), 0.15, 1.0 - e));
|
|
}
|
|
let _ = hot;
|
|
}
|
|
|
|
fn draw_landmarks_glow(g: &Game, t: f32, cam_r: Vec3) {
|
|
let (cx, cy) = g.map.core;
|
|
let hp = (g.core_hp / CORE_HP).clamp(0.0, 1.0);
|
|
draw_heart(vec3(cx as f32 + 1.5, 0.0, cy as f32 + 1.5), hp, t, cam_r, None);
|
|
// the other masters' hearts: the same heart in their colours
|
|
for rv in g.rivals.iter().filter(|r| r.alive && g.map.shown(idx(r.core.0 + 1, r.core.1 + 1))) {
|
|
let (rx, rz) = rv.centre();
|
|
let hp = (rv.hp / crate::rival::RIVAL_CORE_HP).clamp(0.0, 1.0);
|
|
draw_heart(vec3(rx, 0.0, rz), hp, t + rv.side as f32 * 0.37, cam_r, Some(master_rgb(rv.side)));
|
|
}
|
|
// portals: runes on the arch and a vertical vortex inside it, sparks pouring out and falling
|
|
for (pk, &(px, py)) in g.map.portals.iter().enumerate() {
|
|
if !g.map.shown(idx(px + 1, py + 1)) {
|
|
continue;
|
|
}
|
|
let tint = portal_tint(g, pk);
|
|
let owned = tint.is_some();
|
|
let tc = |c: Color| hue_to(tint, c);
|
|
let pc = vec3(px as f32 + 1.5, 0.0, py as f32 + 1.5);
|
|
let arm = if owned { tc(Color::new(0.8, 0.35, 1.0, 1.0)) } else { Color::new(0.45, 0.42, 0.5, 1.0) };
|
|
let mid = pc + vec3(0.0, 1.3, 0.0);
|
|
let (rx, ry) = (0.95, 1.55);
|
|
// the dark heart of the vortex: a vertical disc in the arch
|
|
vdisc(mid, rx, ry, if owned { tc(Color::new(0.16, 0.02, 0.26, 0.92)) } else { Color::new(0.08, 0.08, 0.1, 0.85) }, 28);
|
|
let speed = if owned { 2.0 } else { 0.25 };
|
|
for armk in 0..3 {
|
|
for k in 0..22 {
|
|
let f = k as f32 / 22.0;
|
|
let ang = t * speed + armk as f32 * 2.094 + f * 5.0;
|
|
let p = mid + vec3(ang.cos() * rx * f, ang.sin() * ry * f * 0.9, 0.0);
|
|
draw_cube(p, Vec3::splat(0.05 + 0.05 * f), None, Color::new(arm.r, arm.g, arm.b, if owned { 0.3 + 0.7 * f } else { 0.25 }));
|
|
}
|
|
}
|
|
// runes on the pillars and the arch
|
|
for k in 0..10 {
|
|
let glow = 0.5 + 0.5 * (t * 2.0 - k as f32 * 0.6).sin();
|
|
let p = if k < 6 {
|
|
let side = if k % 2 == 0 { -1.0 } else { 1.0 };
|
|
pc + vec3(side * 1.25, 0.4 + (k / 2) as f32 * 0.6, 0.27)
|
|
} else {
|
|
let a = (k - 6) as f32 / 3.0 * std::f32::consts::PI * 0.7 + 0.47;
|
|
pc + vec3(a.cos() * 1.25, 2.0 + a.sin() * 1.12, 0.27)
|
|
};
|
|
draw_cube(p, vec3(0.12, 0.14, 0.02), None, Color::new(arm.r, arm.g, arm.b, if owned { 0.35 + 0.65 * glow } else { 0.2 }));
|
|
}
|
|
if owned {
|
|
draw_sphere(mid, 0.35 + 0.05 * (t * 3.0).sin(), None, tc(Color::new(0.95, 0.75, 1.0, 0.5)));
|
|
// sparks pour out of the vortex and fall to the floor
|
|
for k in 0..14 {
|
|
let e = (t * 0.5 + k as f32 * 0.071).fract();
|
|
let a = k as f32 * 2.4;
|
|
let dir = if k % 2 == 0 { 1.0 } else { -1.0 };
|
|
let p = mid + vec3(a.cos() * 0.5 * (1.0 - e), a.sin() * 0.6 + e * 0.4 - e * e * 1.9, dir * e * 1.1);
|
|
draw_cube(p, Vec3::splat(0.045 * (1.0 - e) + 0.015), None, tc(Color::new(0.95, 0.65, 1.0, 1.0 - e)));
|
|
}
|
|
disc(pc + vec3(0.0, 0.2, 0.0), 1.1, tc(Color::new(0.6, 0.25, 0.9, 0.12 + 0.05 * (t * 2.0).sin())), 24);
|
|
}
|
|
}
|
|
// gate light
|
|
if g.gate_hp > 0.0 && g.map.shown(idx(g.gate_center().0 as i32, g.gate_center().1 as i32)) {
|
|
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));
|
|
}
|
|
}
|
|
}
|
|
|
|
// height of the walkable surface (bridge decks lie above the floor)
|
|
fn floor_y(g: &Game, x: f32, z: f32) -> f32 {
|
|
if g.map.get(x as i32, z as i32).map_or(false, |t| t.bridge()) {
|
|
DECK_Y
|
|
} else {
|
|
0.0
|
|
}
|
|
}
|
|
|
|
fn draw_shadows(g: &Game) {
|
|
for e in &g.ents {
|
|
if !e.alive || e.held || unit_hidden(g, e) {
|
|
continue;
|
|
}
|
|
let r = if matches!(e.kind, EK::Ogre | EK::Paladin | EK::Jailer | EK::Chainbreaker | EK::Giant) { 0.32 } else { 0.24 };
|
|
let fy = floor_y(g, e.disp_x, e.disp_y);
|
|
disc(vec3(e.disp_x, fy + 0.012, e.disp_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.dwarf() {
|
|
Color::new(0.85, 0.55, 0.25, 0.85)
|
|
} else if e.rival() {
|
|
let c = master_rgb(e.side);
|
|
Color::new(c.x, c.y, c.z, 0.85)
|
|
} 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.disp_x, fy + 0.02, e.disp_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 });
|
|
}
|
|
|
|
// a flat polygon in a vertical plane (x along `right`, y up) around `base`
|
|
fn draw_plate(base: Vec3, right: Vec3, pts: &[(f32, f32)], c: [u8; 4]) {
|
|
let v: Vec<Vertex> = pts.iter().map(|&(x, y)| Vertex { position: base + right * x + Vec3::Y * y, uv: Vec2::ZERO, color: c, normal: Vec4::Y }).collect();
|
|
let mut idx = Vec::new();
|
|
for k in 1..pts.len() as u16 - 1 {
|
|
idx.extend([0, k, k + 1]);
|
|
}
|
|
draw_mesh(&Mesh { vertices: v, indices: idx, texture: None });
|
|
}
|
|
|
|
// guard marker: a shield on a short pole, gently bobbing
|
|
fn draw_shield_mark(m: usize, t: f32) {
|
|
let (x, z) = xy(m);
|
|
let base = vec3(x as f32 + 0.5, 0.0, z as f32 + 0.5);
|
|
draw_cube(base + vec3(0.0, 0.45, 0.0), vec3(0.05, 0.9, 0.05), None, Color::from_rgba(120, 90, 60, 255));
|
|
let bob = (t * 2.0 + m as f32).sin() * 0.03;
|
|
let c = base + vec3(0.0, 0.95 + bob, 0.0);
|
|
let right = vec3(0.7071, 0.0, -0.7071);
|
|
let outline: [(f32, f32); 7] = [(-0.26, 0.24), (0.26, 0.24), (0.26, 0.02), (0.18, -0.16), (0.0, -0.3), (-0.18, -0.16), (-0.26, 0.02)];
|
|
// the rim, the face and a cross (both sides, so it shows from every angle)
|
|
for (r, off) in [(right, 0.0f32), (-right, 0.01)] {
|
|
let at = c + vec3(0.0, 0.0, 0.0) + right.cross(Vec3::Y) * off;
|
|
draw_plate(at, r, &outline, [230, 190, 70, 255]);
|
|
let inner: Vec<(f32, f32)> = outline.iter().map(|&(a, b)| (a * 0.8, b * 0.8 + 0.01)).collect();
|
|
draw_plate(at + right.cross(Vec3::Y) * 0.004, r, &inner, [50, 80, 170, 255]);
|
|
draw_plate(at + right.cross(Vec3::Y) * 0.008, r, &[(-0.03, 0.18), (0.03, 0.18), (0.03, -0.2), (-0.03, -0.2)], [230, 190, 70, 255]);
|
|
draw_plate(at + right.cross(Vec3::Y) * 0.008, r, &[(-0.16, 0.06), (0.16, 0.06), (0.16, 0.0), (-0.16, 0.0)], [230, 190, 70, 255]);
|
|
}
|
|
}
|
|
|
|
// attack marker: a sword stuck in the ground, its blade glinting
|
|
fn draw_sword_mark(m: usize, t: f32) {
|
|
let (x, z) = xy(m);
|
|
let base = vec3(x as f32 + 0.5, 0.0, z as f32 + 0.5);
|
|
let glint = 200 + ((t * 3.0 + m as f32).sin() * 40.0) as i32;
|
|
let blade = Color::from_rgba(glint.clamp(0, 255) as u8, glint.clamp(0, 255) as u8, (glint + 15).clamp(0, 255) as u8, 255);
|
|
draw_cube(base + vec3(0.0, 0.4, 0.0), vec3(0.07, 0.8, 0.02), None, blade);
|
|
draw_cube(base + vec3(0.0, 0.82, 0.0), vec3(0.34, 0.05, 0.06), None, Color::from_rgba(150, 120, 50, 255));
|
|
draw_cube(base + vec3(0.0, 0.98, 0.0), vec3(0.05, 0.26, 0.05), None, Color::from_rgba(170, 30, 30, 255));
|
|
draw_cube(base + vec3(0.0, 1.13, 0.0), vec3(0.08, 0.06, 0.08), None, Color::from_rgba(230, 190, 70, 255));
|
|
}
|
|
|
|
fn mark_color(kind: u8) -> Color {
|
|
match kind {
|
|
MARK_DIG => Color::new(0.45, 0.95, 0.55, 1.0),
|
|
MARK_ROOM => Color::new(0.35, 0.85, 1.0, 1.0),
|
|
MARK_SELL => Color::new(0.95, 0.78, 0.22, 1.0),
|
|
MARK_BRIDGE => Color::new(0.70, 0.48, 0.24, 1.0),
|
|
MARK_STEEL => Color::new(0.78, 0.84, 0.92, 1.0),
|
|
MARK_TOWER => Color::new(0.74, 0.42, 0.95, 1.0),
|
|
MARK_TRAP => Color::new(1.0, 0.58, 0.18, 1.0),
|
|
MARK_SPELL => Color::new(0.42, 0.88, 0.98, 1.0),
|
|
MARK_LIGHTNING => Color::new(0.98, 0.94, 0.35, 1.0),
|
|
MARK_HEAL => Color::new(0.48, 0.96, 0.66, 1.0),
|
|
MARK_SUMMON => Color::new(0.95, 0.36, 0.24, 1.0),
|
|
MARK_FLAG => Color::new(0.88, 0.18, 0.18, 1.0),
|
|
MARK_PICKUP => Color::new(0.72, 0.76, 0.82, 1.0),
|
|
MARK_DROP => Color::new(0.72, 0.76, 0.82, 1.0),
|
|
MARK_SLAP => Color::new(1.0, 0.46, 0.24, 1.0),
|
|
MARK_ATTACK => Color::new(0.94, 0.18, 0.20, 1.0),
|
|
MARK_GUARD => Color::new(0.98, 0.82, 0.22, 1.0),
|
|
MARK_CANCEL => Color::new(0.58, 0.64, 0.78, 1.0),
|
|
_ => Color::new(0.88, 0.18, 0.18, 1.0),
|
|
}
|
|
}
|
|
|
|
fn flag_rgba(c: Color) -> [u8; 4] {
|
|
[
|
|
(c.r.clamp(0.0, 1.0) * 255.0) as u8,
|
|
(c.g.clamp(0.0, 1.0) * 255.0) as u8,
|
|
(c.b.clamp(0.0, 1.0) * 255.0) as u8,
|
|
(c.a.clamp(0.0, 1.0) * 255.0) as u8,
|
|
]
|
|
}
|
|
|
|
fn mark_side_color(side: u8) -> Color {
|
|
match side {
|
|
0 => Color::new(1.0, 0.85, 0.45, 1.0),
|
|
1 => Color::new(0.85, 0.55, 0.25, 1.0),
|
|
2 => Color::new(0.35, 0.95, 0.40, 1.0),
|
|
3 => Color::new(0.40, 0.60, 1.00, 1.0),
|
|
4 => Color::new(0.85, 0.75, 0.30, 1.0),
|
|
_ => Color::new(0.82, 0.84, 0.88, 1.0),
|
|
}
|
|
}
|
|
|
|
fn draw_command_flag(x: f32, z: f32, cloth: Color, pole: Color, t: f32) {
|
|
let base = vec3(x - 0.04, 0.0, z);
|
|
draw_cube(base + vec3(0.0, 0.65, 0.0), vec3(0.035, 1.30, 0.035), None, Color::new(pole.r, pole.g, pole.b, pole.a));
|
|
let wave = (t * 6.0).sin() * 0.07;
|
|
let p1 = base + vec3(0.0, 1.28, 0.0);
|
|
let p2 = base + vec3(0.48, 1.14 + wave, 0.08);
|
|
let p3 = base + vec3(0.0, 0.92, 0.0);
|
|
let col = flag_rgba(cloth);
|
|
let v = vec![
|
|
Vertex { position: p1, uv: Vec2::ZERO, color: col, normal: Vec4::Y },
|
|
Vertex { position: p2, uv: Vec2::ZERO, color: col, normal: Vec4::Y },
|
|
Vertex { position: p3, uv: Vec2::ZERO, color: col, normal: Vec4::Y },
|
|
];
|
|
draw_mesh(&Mesh { vertices: v, indices: vec![0, 1, 2], texture: None });
|
|
}
|
|
|
|
fn draw_marks(g: &Game, t: f32) {
|
|
if g.marks.is_empty() {
|
|
return;
|
|
}
|
|
for m in &g.marks {
|
|
if !g.debug_markers {
|
|
continue;
|
|
}
|
|
let left = m.until.saturating_sub(g.sim_steps) as f32 * STEP;
|
|
if left <= 0.0 {
|
|
continue;
|
|
}
|
|
let a = if m.fixed { 1.0 } else { (left / 2.0).min(1.0).max(0.18) };
|
|
let base = mark_color(m.kind);
|
|
let side = mark_side_color(m.side);
|
|
draw_command_flag(m.x, m.y, Color::new(base.r, base.g, base.b, a), Color::new(side.r, side.g, side.b, a), t);
|
|
}
|
|
}
|
|
|
|
// projectiles with a look of their own (styles in particles.rs)
|
|
fn draw_projectile(f: &Fx, p: f32, t: f32) {
|
|
use crate::particles::*;
|
|
let at = |q: f32| {
|
|
let (x, y, h) = fx_pos(f, q);
|
|
vec3(x, h, y)
|
|
};
|
|
let head = at(p);
|
|
let c = rgba(f.col);
|
|
let sz = f.size;
|
|
match f.style {
|
|
ST_ARROW | ST_ARROW_WALL | ST_ARROW_FALL => {
|
|
let back = at((p - 0.08).max(0.0));
|
|
let dir = (head - back).normalize_or_zero();
|
|
line3(head - dir * 0.3, head, 0.03, Color::from_rgba(120, 84, 48, 255));
|
|
draw_cube(head, Vec3::splat(0.035), None, Color::from_rgba(200, 200, 210, 255));
|
|
draw_cube(head - dir * 0.28, Vec3::splat(0.05), None, Color::from_rgba(230, 230, 230, 255));
|
|
}
|
|
ST_CANNON => {
|
|
draw_sphere(head, 0.1 * sz, None, Color::from_rgba(24, 24, 28, 255));
|
|
draw_sphere(head, 0.14 * sz, None, Color::new(1.0, 0.6, 0.2, 0.15));
|
|
}
|
|
ST_FIRE => {
|
|
let flick = 1.0 + (t * 30.0 + f.x).sin() * 0.12;
|
|
draw_sphere(head, 0.08 * sz * flick, None, Color::new(1.0, 0.95, 0.6, 1.0));
|
|
draw_sphere(head, 0.14 * sz * flick, None, Color::new(1.0, 0.55, 0.12, 0.8));
|
|
draw_sphere(head, 0.24 * sz * flick, None, Color::new(1.0, 0.3, 0.05, 0.25));
|
|
}
|
|
_ => {
|
|
let tail = at((p - 0.3).max(0.0));
|
|
line3(tail, head, 0.06 * sz, Color::new(c.r, c.g, c.b, 0.7));
|
|
draw_sphere(head, 0.08 * sz, None, c);
|
|
draw_sphere(head, 0.17 * sz, None, Color::new(c.r, c.g, c.b, 0.25));
|
|
}
|
|
}
|
|
}
|
|
|
|
// a crate with a finished trap or door (in the forge or carried by an imp)
|
|
fn trap_crate(at: Vec3) {
|
|
draw_cube(at + vec3(0.0, 0.14, 0.0), vec3(0.34, 0.28, 0.34), None, Color::from_rgba(120, 82, 46, 255));
|
|
for dy in [0.04f32, 0.24] {
|
|
draw_cube(at + vec3(0.0, dy, 0.0), vec3(0.36, 0.035, 0.36), None, Color::from_rgba(70, 70, 76, 255));
|
|
}
|
|
draw_cube(at + vec3(0.0, 0.29, 0.0), vec3(0.1, 0.02, 0.1), None, Color::from_rgba(200, 60, 40, 255));
|
|
}
|
|
|
|
// traps and doors: the installed pieces, crates in the forge and on the way
|
|
fn draw_traps_solid(g: &Game, t: f32) {
|
|
use crate::traps::*;
|
|
for tr in &g.traps {
|
|
let (tx, tz) = xy(tr.tile);
|
|
let c = vec3(tx as f32 + 0.5, 0.0, tz as f32 + 0.5);
|
|
match tr.stage {
|
|
CRATED => {
|
|
let (x, z) = xy(tr.crate_tile);
|
|
let k = (tr.id % 3) as f32 * 0.2 - 0.2;
|
|
trap_crate(vec3(x as f32 + 0.5 + k, 0.0, z as f32 + 0.5 - k));
|
|
}
|
|
CARRIED => {
|
|
if let Some(e) = g.ents.iter().find(|e| e.id + 1 == tr.carrier) {
|
|
trap_crate(vec3(e.disp_x, 0.42, e.disp_y));
|
|
}
|
|
}
|
|
ARMED => {
|
|
let iron = Color::from_rgba(70, 70, 78, 255);
|
|
let fresh = (1.0 - tr.fired / 0.6).clamp(0.0, 1.0);
|
|
match tr.kind {
|
|
TrapKind::Spikes => {
|
|
draw_cube(c + vec3(0.0, 0.02, 0.0), vec3(0.8, 0.04, 0.8), None, iron);
|
|
let up = 0.06 + 0.3 * fresh;
|
|
for q in 0..9 {
|
|
let (dx, dz) = ((q % 3) as f32 * 0.24 - 0.24, (q / 3) as f32 * 0.24 - 0.24);
|
|
line3(c + vec3(dx, 0.03, dz), c + vec3(dx, 0.03 + up, dz), 0.05, Color::from_rgba(200, 200, 212, 255));
|
|
}
|
|
}
|
|
TrapKind::Poison => {
|
|
draw_cube(c + vec3(0.0, 0.03, 0.0), vec3(0.6, 0.06, 0.6), None, iron);
|
|
for q in 0..3 {
|
|
draw_cube(c + vec3(q as f32 * 0.16 - 0.16, 0.065, 0.0), vec3(0.06, 0.02, 0.5), None, Color::from_rgba(30, 40, 26, 255));
|
|
}
|
|
}
|
|
TrapKind::Fire => {
|
|
// a demon head on a pedestal, its mouth towards the last target
|
|
draw_cube(c + vec3(0.0, 0.15, 0.0), vec3(0.36, 0.3, 0.36), None, Color::from_rgba(50, 40, 44, 255));
|
|
let (ax, az) = (tr.aim.cos(), tr.aim.sin());
|
|
draw_cube(c + vec3(0.0, 0.4, 0.0), vec3(0.3, 0.22, 0.3), None, Color::from_rgba(70, 56, 52, 255));
|
|
draw_cube(c + vec3(ax * 0.16, 0.38, az * 0.16), vec3(0.14, 0.1, 0.14), None, Color::from_rgba(30, 20, 20, 255));
|
|
for sgn in [-1.0f32, 1.0] {
|
|
line3(c + vec3(-az * 0.12 * sgn, 0.5, ax * 0.12 * sgn), c + vec3(-az * 0.2 * sgn - ax * 0.08, 0.68, ax * 0.2 * sgn - az * 0.08), 0.04, Color::from_rgba(214, 204, 178, 255));
|
|
}
|
|
}
|
|
TrapKind::Roller => {
|
|
// the boulder waits in its niche (until it rolls)
|
|
if tr.fired > 18.0 || tr.cd <= 0.0 {
|
|
draw_sphere(c + vec3(0.0, 0.3, 0.0), 0.3, None, Color::from_rgba(120, 112, 104, 255));
|
|
}
|
|
draw_cube(c + vec3(0.0, 0.03, 0.0), vec3(0.8, 0.06, 0.8), None, Color::from_rgba(60, 56, 52, 255));
|
|
}
|
|
TrapKind::Alarm => {
|
|
draw_cube(c + vec3(0.0, 0.35, 0.0), vec3(0.06, 0.7, 0.06), None, Color::from_rgba(90, 64, 40, 255));
|
|
draw_cube(c + vec3(0.0, 0.7, 0.0), vec3(0.34, 0.05, 0.06), None, Color::from_rgba(90, 64, 40, 255));
|
|
let swing = (t * 18.0).sin() * 0.06 * fresh.max(((tr.fired < 3.0) as u8) as f32);
|
|
draw_cube(c + vec3(swing, 0.58, 0.0), vec3(0.18, 0.18, 0.18), None, Color::from_rgba(220, 180, 70, 255));
|
|
}
|
|
TrapKind::Mine => {
|
|
draw_cube(c + vec3(0.0, 0.015, 0.0), vec3(0.4, 0.03, 0.4), None, Color::from_rgba(50, 54, 66, 255));
|
|
}
|
|
TrapKind::Frost => {
|
|
draw_cube(c + vec3(0.0, 0.06, 0.0), vec3(0.4, 0.12, 0.4), None, Color::from_rgba(80, 90, 110, 255));
|
|
}
|
|
_ => {
|
|
// doors: posts, lintel and the leaf across the corridor
|
|
let across_x = g.map.get(tx - 1, tz).map_or(true, |t| t.solid()) || g.map.get(tx + 1, tz).map_or(true, |t| t.solid());
|
|
let (len, thin) = if across_x { (vec3(1.0, 0.0, 0.0), vec3(0.0, 0.0, 1.0)) } else { (vec3(0.0, 0.0, 1.0), vec3(1.0, 0.0, 0.0)) };
|
|
let size = |l: f32, h: f32, w: f32| len * l + Vec3::Y * h + thin * w;
|
|
let (leaf, trim) = match tr.kind {
|
|
TrapKind::WoodDoor => (Color::from_rgba(120, 80, 44, 255), Color::from_rgba(70, 68, 74, 255)),
|
|
TrapKind::IronDoor => (Color::from_rgba(96, 98, 108, 255), Color::from_rgba(150, 150, 160, 255)),
|
|
_ => (Color::from_rgba(70, 36, 100, 255), Color::from_rgba(30, 20, 40, 255)),
|
|
};
|
|
for sgn in [-1.0f32, 1.0] {
|
|
draw_cube(c + len * (0.44 * sgn) + Vec3::Y * 0.6, size(0.12, 1.2, 0.3), None, Color::from_rgba(40, 32, 36, 255));
|
|
}
|
|
draw_cube(c + Vec3::Y * 1.22, size(1.0, 0.14, 0.32), None, Color::from_rgba(40, 32, 36, 255));
|
|
let shake = if tr.fired < 0.2 { (t * 60.0).sin() * 0.02 } else { 0.0 };
|
|
draw_cube(c + Vec3::Y * 0.56 + thin * shake, size(0.76, 1.1, 0.1), None, leaf);
|
|
for y in [0.25f32, 0.85] {
|
|
draw_cube(c + Vec3::Y * y, size(0.78, 0.06, 0.12), None, trim);
|
|
}
|
|
if tr.locked {
|
|
draw_cube(c + Vec3::Y * 0.6, size(0.86, 0.08, 0.16), None, Color::from_rgba(60, 60, 66, 255));
|
|
draw_cube(c + Vec3::Y * 0.6 + thin * 0.09, vec3(0.1, 0.12, 0.1), None, Color::from_rgba(230, 190, 70, 255));
|
|
}
|
|
// damage: darker the more it is beaten
|
|
let _ = tr.hp / tr.kind.hp();
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
for r in &g.rollers {
|
|
let spin = r.t * 9.0;
|
|
let at = vec3(r.x, 0.3, r.y);
|
|
draw_sphere(at, 0.3, None, Color::from_rgba(120, 112, 104, 255));
|
|
draw_cube(at + vec3(spin.cos() * 0.22 * r.dy.abs(), spin.sin() * 0.22, spin.cos() * 0.22 * r.dx.abs()), Vec3::splat(0.1), None, Color::from_rgba(80, 74, 70, 255));
|
|
}
|
|
}
|
|
|
|
fn draw_traps_glow(g: &Game, t: f32) {
|
|
use crate::traps::*;
|
|
for tr in &g.traps {
|
|
let (tx, tz) = xy(tr.tile);
|
|
let c = vec3(tx as f32 + 0.5, 0.0, tz as f32 + 0.5);
|
|
if tr.stage == ORDERED || tr.stage == CRATED || tr.stage == CARRIED {
|
|
// the planned spot: a pulsing outline with a small progress bar
|
|
let a = 0.4 + 0.3 * (t * 3.0 + tr.id as f32).sin();
|
|
let col = Color::new(0.4, 0.85, 1.0, a);
|
|
let y = 0.05;
|
|
for (p0, p1) in [((-0.42f32, -0.42f32), (0.42f32, -0.42f32)), ((0.42, -0.42), (0.42, 0.42)), ((0.42, 0.42), (-0.42, 0.42)), ((-0.42, 0.42), (-0.42, -0.42))] {
|
|
line3(c + vec3(p0.0, y, p0.1), c + vec3(p1.0, y, p1.1), 0.04, col);
|
|
}
|
|
let f = if tr.stage == ORDERED { (tr.progress / tr.kind.work()).clamp(0.0, 1.0) } else { 1.0 };
|
|
line3(c + vec3(-0.35, y, 0.0), c + vec3(-0.35 + 0.7 * f, y, 0.0), 0.08, Color::new(1.0, 0.7, 0.3, 0.8));
|
|
continue;
|
|
}
|
|
if tr.stage != ARMED || !g.trap_glows(tr) {
|
|
continue;
|
|
}
|
|
if tr.revealed && !tr.kind.door() {
|
|
// found by a fairy: a white warning glint above it
|
|
let a = 0.5 + 0.5 * (t * 6.0).sin();
|
|
draw_cube(c + vec3(0.0, 0.7 + 0.05 * (t * 3.0).sin(), 0.0), vec3(0.06, 0.18, 0.06), None, Color::new(1.0, 1.0, 1.0, a));
|
|
}
|
|
match tr.kind {
|
|
TrapKind::Poison => disc(c + vec3(0.0, 0.07, 0.0), 0.22, Color::new(0.4, 1.0, 0.3, 0.3 + 0.15 * (t * 2.0).sin()), 12),
|
|
TrapKind::Fire => {
|
|
let (ax, az) = (tr.aim.cos(), tr.aim.sin());
|
|
let f = 0.5 + 0.5 * (t * 6.0).sin();
|
|
draw_sphere(c + vec3(ax * 0.24, 0.38, az * 0.24), 0.05 + 0.03 * f, None, Color::new(1.0, 0.5, 0.1, 0.9));
|
|
}
|
|
TrapKind::Mine => {
|
|
let glow = 0.4 + 0.4 * (t * 2.5 + tr.id as f32).sin();
|
|
ring(c + vec3(0.0, 0.04, 0.0), 0.16, Color::new(0.35, 0.8, 1.0, glow));
|
|
line3(c + vec3(-0.1, 0.04, 0.0), c + vec3(0.1, 0.04, 0.0), 0.03, Color::new(0.35, 0.8, 1.0, glow));
|
|
line3(c + vec3(0.0, 0.04, -0.1), c + vec3(0.0, 0.04, 0.1), 0.03, Color::new(0.35, 0.8, 1.0, glow));
|
|
}
|
|
TrapKind::Frost => {
|
|
let p = 1.0 + 0.1 * (t * 2.0).sin();
|
|
draw_cube(c + vec3(0.0, 0.3, 0.0), vec3(0.12, 0.36 * p, 0.12), None, Color::new(0.75, 0.92, 1.0, 0.9));
|
|
draw_cube(c + vec3(0.1, 0.22, 0.05), vec3(0.07, 0.2, 0.07), None, Color::new(0.7, 0.9, 1.0, 0.85));
|
|
draw_cube(c + vec3(-0.08, 0.2, -0.06), vec3(0.07, 0.16, 0.07), None, Color::new(0.7, 0.9, 1.0, 0.85));
|
|
disc(c + vec3(0.0, 0.02, 0.0), 2.5, Color::new(0.6, 0.85, 1.0, 0.06), 24);
|
|
}
|
|
TrapKind::MagicDoor => {
|
|
let across_x = g.map.get(tx - 1, tz).map_or(true, |t| t.solid()) || g.map.get(tx + 1, tz).map_or(true, |t| t.solid());
|
|
let thin = if across_x { vec3(0.0, 0.0, 1.0) } else { vec3(1.0, 0.0, 0.0) };
|
|
let glow = 0.5 + 0.5 * (t * 2.0).sin();
|
|
for side in [-1.0f32, 1.0] {
|
|
let p = c + Vec3::Y * 0.6 + thin * (0.06 * side);
|
|
draw_cube(p, vec3(0.06, 0.4, 0.06), None, Color::new(0.9, 0.5, 1.0, 0.5 + 0.4 * glow));
|
|
draw_cube(p + Vec3::Y * 0.06, vec3(0.22, 0.06, 0.06), None, Color::new(0.9, 0.5, 1.0, 0.5 + 0.4 * glow));
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
// a torch on a little iron stand: three splayed legs, a ring and the fire bowl on top
|
|
fn draw_torch_stand(x: f32, z: f32) {
|
|
let iron = Color::from_rgba(52, 46, 44, 255);
|
|
let top = vec3(x, 0.5, z);
|
|
for k in 0..3 {
|
|
let a = k as f32 * std::f32::consts::TAU / 3.0 + 0.4;
|
|
let foot = vec3(x + a.cos() * 0.2, 0.0, z + a.sin() * 0.2);
|
|
line3(foot, top, 0.03, iron);
|
|
}
|
|
draw_cube(vec3(x, 0.26, z), vec3(0.16, 0.025, 0.16), None, iron);
|
|
draw_cube(vec3(x, 0.55, z), vec3(0.17, 0.09, 0.17), None, Color::from_rgba(70, 58, 50, 255));
|
|
draw_cube(vec3(x, 0.6, z), vec3(0.12, 0.02, 0.12), None, Color::from_rgba(255, 140, 40, 255));
|
|
}
|
|
|
|
// a torch flame: stacked glowing blocks, hot and white at the bottom, red at the tip, that
|
|
// sway and flicker; a soft halo around it (the smoke comes from particles.rs)
|
|
fn draw_torch_flame((x, z): (f32, f32), base: f32, k: f32, t: f32) {
|
|
let flick = 0.8 + 0.2 * (t * 13.0 + k * 2.1).sin() + 0.1 * (t * 29.0 + k * 0.7).sin();
|
|
let n = 6;
|
|
for i in 0..n {
|
|
let f = i as f32 / (n - 1) as f32;
|
|
let sway = (t * 7.0 + k * 1.3 + f * 2.5).sin() * 0.035 * f;
|
|
let sway2 = (t * 5.3 + k * 2.7 + f * 1.9).cos() * 0.025 * f;
|
|
let h = base + f * 0.24 * flick;
|
|
let size = (0.11 - f * 0.08) * (1.0 + 0.18 * (t * 17.0 + k * 3.1 + f * 4.0).sin());
|
|
// bright: almost white at the bottom, golden in the middle, orange at the tip
|
|
let c = if f < 0.4 {
|
|
vec3(1.0, 1.0, 0.88).lerp(vec3(1.0, 0.86, 0.38), f / 0.4)
|
|
} else {
|
|
vec3(1.0, 0.86, 0.38).lerp(vec3(1.0, 0.56, 0.16), (f - 0.4) / 0.6)
|
|
};
|
|
draw_cube(vec3(x + sway, h, z + sway2), Vec3::splat(size), None, Color::new(c.x, c.y, c.z, 1.0 - f * 0.15));
|
|
}
|
|
// the halo last: the glow material writes depth, so anything drawn after it inside the
|
|
// sphere would be hidden; light and thin, so it does not dim the flame
|
|
draw_sphere(vec3(x, base + 0.1, z), 0.26 * flick, None, Color::new(1.0, 0.78, 0.4, 0.12));
|
|
}
|
|
|
|
// particles that glow (embers, sparks, smoke, flashes, shock waves)
|
|
fn draw_scorch_glow(g: &Game) {
|
|
use crate::particles::*;
|
|
// all marks go into one mesh (one draw call)
|
|
let mut v: Vec<Vertex> = Vec::new();
|
|
let mut ix: Vec<u16> = Vec::new();
|
|
let mut disc = |c: Vec3, r: f32, color: Color, seg: usize| {
|
|
if v.len() + seg + 1 > 60000 {
|
|
return;
|
|
}
|
|
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];
|
|
let b = v.len() as u16;
|
|
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 });
|
|
}
|
|
for k in 0..seg as u16 {
|
|
ix.extend_from_slice(&[b, b + 1 + k, b + 1 + (k + 1) % seg as u16]);
|
|
}
|
|
};
|
|
for p in g.stuck.iter().filter(|p| p.kind == SCORCH) {
|
|
let life = (p.t / p.max).clamp(0.0, 1.0);
|
|
// stays dark for most of its time, then fades
|
|
let fade = if life < 0.6 { 1.0 } else { 1.0 - (life - 0.6) / 0.4 };
|
|
let base = vec3(p.x, floor_y(g, p.x, p.y) + 0.016, p.y);
|
|
// a ragged blotch of soot: overlapping soft spots around the centre, their places
|
|
// fixed by the position (so the mark keeps its shape)
|
|
let mut h = ((p.x * 73.0) as i32 as u32).wrapping_mul(2654435761) ^ ((p.y * 91.0) as i32 as u32).wrapping_mul(2246822519);
|
|
let mut next = || {
|
|
h ^= h >> 13;
|
|
h = h.wrapping_mul(1274126177);
|
|
(h >> 8) as f32 / 16777216.0
|
|
};
|
|
for k in 0..6 {
|
|
let (a, d) = (next() * std::f32::consts::TAU, if k == 0 { 0.0 } else { p.size * (0.25 + 0.3 * next()) });
|
|
let r = p.size * if k == 0 { 0.65 } else { 0.3 + 0.25 * next() };
|
|
let at = base + vec3(a.cos() * d, 0.0005 * k as f32, a.sin() * d);
|
|
disc(at, r, Color::new(0.03, 0.025, 0.022, 0.32 * fade), 10);
|
|
disc(at, r * 0.6, Color::new(0.015, 0.012, 0.01, 0.3 * fade), 8);
|
|
}
|
|
// freshly burnt: embers glow in the middle for a few seconds
|
|
if p.t < 4.0 {
|
|
let k = 1.0 - p.t / 4.0;
|
|
disc(base + Vec3::Y * 0.004, p.size * 0.45, Color::new(1.0, 0.38, 0.08, 0.45 * k * k), 12);
|
|
}
|
|
}
|
|
if !ix.is_empty() {
|
|
draw_mesh(&Mesh { vertices: v, indices: ix, texture: None });
|
|
}
|
|
}
|
|
|
|
fn draw_parts_glow(g: &Game) {
|
|
use crate::particles::*;
|
|
for p in &g.parts {
|
|
let c = rgba(p.col);
|
|
let life = (p.t / p.max).clamp(0.0, 1.0);
|
|
let at = vec3(p.x, p.h, p.y);
|
|
match p.kind {
|
|
WISP => {
|
|
// a thin thread of smoke: small pixels that fade, they do not grow
|
|
draw_cube(at, Vec3::splat(p.size), None, Color::new(c.r, c.g, c.b, 0.55 * (1.0 - life)));
|
|
}
|
|
EMBER | SPARK | MOTE | SPLASH => {
|
|
let s = p.size * (1.0 - life * 0.6);
|
|
draw_cube(at, Vec3::splat(s), None, Color::new(c.r, c.g, c.b, 1.0 - life * life));
|
|
}
|
|
SMOKE => draw_sphere(at, p.size, None, Color::new(c.r, c.g, c.b, 0.14 * (1.0 - life))),
|
|
FLASH => {
|
|
let r = p.size * (0.3 + life * 0.9);
|
|
draw_sphere(at, r * 0.6, None, Color::new(1.0, 0.95, 0.8, 0.95 * (1.0 - life)));
|
|
draw_sphere(at, r, None, Color::new(c.r, c.g, c.b, 0.55 * (1.0 - life)));
|
|
}
|
|
RING => ring(at, p.size * (0.2 + life), Color::new(c.r, c.g, c.b, 0.8 * (1.0 - life))),
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
// solid particles: debris, coins and the arrows stuck in walls and floors
|
|
fn draw_parts_solid(g: &Game) {
|
|
use crate::particles::*;
|
|
for p in &g.parts {
|
|
let c = rgba(p.col);
|
|
let at = vec3(p.x, p.h, p.y);
|
|
match p.kind {
|
|
DEBRIS | COIN => {
|
|
let life = (p.t / p.max).clamp(0.0, 1.0);
|
|
let s = p.size * if life > 0.75 { (1.0 - life) * 4.0 } else { 1.0 };
|
|
draw_cube(at, vec3(s, if p.kind == COIN { s * 0.4 } else { s }, s), None, c);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
// arrows drifting on lava: the shaft glows and chars, then crumbles
|
|
for p in g.parts.iter().filter(|p| p.kind == BURNING_ARROW) {
|
|
let life = (p.t / p.max).clamp(0.0, 1.0);
|
|
let k = 1.0 - life * life;
|
|
let bob = (p.t * 3.0 + p.x * 2.0).sin() * 0.01;
|
|
let mid = vec3(p.x, -LIQ_D + 0.07 + bob, p.y); // above the swell of the waves
|
|
let dir = vec3(p.vx, 0.0, p.vy).normalize_or_zero();
|
|
let half = dir * (0.17 * k);
|
|
let wood = vec3(0.45, 0.31, 0.18);
|
|
let glow = vec3(1.0, 0.45, 0.1);
|
|
let char_ = vec3(0.08, 0.05, 0.04);
|
|
let c = if life < 0.5 { wood.lerp(glow, life * 2.0) } else { glow.lerp(char_, (life - 0.5) * 2.0) };
|
|
line3(mid - half, mid + half, 0.03, Color::new(c.x, c.y, c.z, 1.0));
|
|
if life < 0.6 {
|
|
draw_cube(mid + half, Vec3::splat(0.035), None, Color::from_rgba(200, 200, 210, 255));
|
|
}
|
|
}
|
|
for p in g.stuck.iter().filter(|p| p.kind == ARROW) {
|
|
let left = p.max - p.t;
|
|
let k = if left < 10.0 { left / 10.0 } else { 1.0 };
|
|
let tip = vec3(p.x, p.h, p.y);
|
|
let dir = vec3(p.vx, p.vh, p.vy).normalize_or_zero();
|
|
let tail = tip - dir * (0.32 * k);
|
|
line3(tail, tip + dir * 0.03, 0.03, Color::from_rgba(116, 80, 46, 255));
|
|
draw_cube(tail, vec3(0.05, 0.05, 0.05) * k, None, Color::from_rgba(226, 226, 226, 255));
|
|
}
|
|
}
|
|
|
|
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, f.h, f.y);
|
|
match f.kind {
|
|
0 if f.style != crate::particles::ST_PLAIN => draw_projectile(f, p, t),
|
|
0 => {
|
|
let from = vec3(f.x, f.h, f.y);
|
|
let to = vec3(f.x2, f.h2, f.y2);
|
|
let head = from.lerp(to, p);
|
|
let tail = from.lerp(to, (p - 0.35 * f.size.min(1.5)).max(0.0));
|
|
line3(tail, head, 0.06 * f.size, a(0.8));
|
|
draw_sphere(head, 0.08 * f.size, None, c);
|
|
if f.size > 1.35 {
|
|
// experienced shooters: a glowing halo around the projectile
|
|
draw_sphere(head, 0.16 * f.size, None, a(0.25));
|
|
}
|
|
}
|
|
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) * f.size;
|
|
let n = if f.size > 1.35 { 10 } else { 6 };
|
|
for k in 0..n {
|
|
let ang = k as f32 * std::f32::consts::TAU / n as f32;
|
|
line3(at, at + vec3(ang.cos() * r, (k as f32 - n as f32 * 0.5 + 0.5) * 0.06 * f.size, ang.sin() * r), 0.04 * f.size, a(1.0 - p));
|
|
}
|
|
if f.size > 1.6 {
|
|
disc(vec3(f.x, 0.03, f.y), r * 1.3, a(0.3 * (1.0 - p)), 12);
|
|
}
|
|
}
|
|
3 => draw_sphere(vec3(f.x, 0.3, f.y), (0.2 + 0.5 * p) * f.size, 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 {
|
|
let realm = g.map.get(ex, ey).map_or(Owner::Player, |t| t.owner);
|
|
// the entry tile itself: a pulsing red plate (also where an open corridor leads in)
|
|
let c = vec3(ex as f32 + 0.5, 0.03, ey as f32 + 0.5);
|
|
mb.quad(
|
|
[c + vec3(-0.5, 0.0, -0.5), c + vec3(0.5, 0.0, -0.5), c + vec3(0.5, 0.0, 0.5), c + vec3(-0.5, 0.0, 0.5)],
|
|
[Vec2::ZERO; 4],
|
|
[col(vec3(1.0, 0.16, 0.12) * (0.6 + 0.6 * p), 0.45 + 0.35 * p); 4],
|
|
[Vec3::Y; 4],
|
|
);
|
|
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 == realm && !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::Silver => [190, 195, 210, 255],
|
|
Kind::Ruby => [220, 40, 70, 255],
|
|
Kind::Diamond => [170, 235, 255, 255],
|
|
Kind::Floor => match tile.owner {
|
|
Owner::Player => [140, 60, 80, 255],
|
|
Owner::Hero => [170, 175, 190, 255],
|
|
Owner::Rival1 => [70, 150, 80, 255],
|
|
Owner::Rival2 => [70, 100, 180, 255],
|
|
Owner::Rival3 => [150, 130, 50, 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 => match tile.owner {
|
|
Owner::Rival1 => [90, 240, 110, 255],
|
|
Owner::Rival2 => [100, 150, 255, 255],
|
|
Owner::Rival3 => [250, 220, 90, 255],
|
|
_ => [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],
|
|
Kind::Water => [40, 80, 150, 255],
|
|
Kind::Lava => [250, 110, 30, 255],
|
|
Kind::Bridge => [150, 110, 60, 255],
|
|
Kind::LavaBridge => [130, 120, 115, 255],
|
|
Kind::Drawbridge => [120, 80, 50, 255],
|
|
Kind::Obsidian => [60, 40, 70, 255],
|
|
Kind::Mushroom => [150, 90, 200, 255],
|
|
Kind::Grove => [50, 110, 92, 255],
|
|
Kind::Tree => [40, 100, 40, 255],
|
|
Kind::Shard => [255, 60, 120, 255],
|
|
Kind::Aerie => [170, 140, 90, 255],
|
|
Kind::Steel => [150, 160, 180, 255],
|
|
Kind::ManaSpring => [90, 230, 255, 255],
|
|
Kind::RuneChest => [80, 200, 255, 255],
|
|
Kind::Forge => [150, 70, 40, 255],
|
|
Kind::Prison => [90, 90, 100, 255],
|
|
Kind::Graveyard => [70, 90, 60, 255],
|
|
Kind::GuardPost => [150, 50, 50, 255],
|
|
Kind::Arena => [200, 170, 110, 255],
|
|
Kind::Altar => [120, 20, 40, 255],
|
|
Kind::Door => [140, 90, 50, 255],
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn pixel_size_follows_the_window() {
|
|
// (the pixel look's sizes: see ui::set_virtual_rows)
|
|
assert!(true);
|
|
}
|
|
|
|
#[test]
|
|
fn lava_lights_ten_tiles_and_fades() {
|
|
let mut g = Game::new(3, 64, 64);
|
|
// an open hall with one lava tile in the middle and a wall to the west
|
|
for y in 10..40 {
|
|
for x in 10..40 {
|
|
let t = &mut g.map.t[idx(x, y)];
|
|
t.kind = Kind::Floor;
|
|
t.owner = Owner::None;
|
|
}
|
|
}
|
|
g.map.t[idx(25, 25)].kind = Kind::Lava;
|
|
for y in 10..40 {
|
|
g.map.t[idx(20, y)].kind = Kind::Rock;
|
|
}
|
|
let f = lava_field(&g.map);
|
|
let at = |x: i32, y: i32| f[idx(x, y)];
|
|
assert_eq!(at(25, 25), 1.0);
|
|
assert!(at(26, 25) > at(28, 25) && at(28, 25) > at(32, 25), "fades with the distance");
|
|
assert!(at(34, 25) > 0.0, "nine tiles away still a little light");
|
|
assert_eq!(at(36, 25), 0.0, "nothing beyond ten tiles");
|
|
assert!(at(20, 25) > 0.0, "the wall face is lit");
|
|
assert_eq!(at(19, 25), 0.0, "but no light behind the wall");
|
|
}
|
|
}
|