Files
airdef/airdef-mod/src/monitor.rs
T

232 lines
9.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! The air-defence monitor: a system's state, its radar's picture round it
//! and its camera (a real view of the world from the system, the game
//! renders it) locked on its target. K shows one system at a time, J all of
//! them in a grid.
use crate::panel::distance;
use crate::sim::{Sam, SamState, Sim, heading_of, roles_text};
use glam::DVec3;
use horizon_mod_sdk::render::{self, CameraView, FillShape, Point, QuadShape, Rect, Shape, TextShape, ViewShape};
fn fill(x: f32, y: f32, w: f32, h: f32, color: u32) -> Shape {
Shape::Fill(FillShape { rect: Rect { x, y, w, h }, color })
}
fn text(t: impl Into<String>, x: f32, y: f32, color: u32) -> Shape {
Shape::Text(TextShape { text: t.into(), at: Point { x, y }, color, centered: false, shadow: true })
}
fn quad(a: (f32, f32), b: (f32, f32), c: (f32, f32), d: (f32, f32), color: u32) -> Shape {
let p = |(x, y): (f32, f32)| Point { x, y };
Shape::Quad(QuadShape { corners: (p(a), p(b), p(c), p(d)), color })
}
/// `t` cut to `w` GUI pixels, with an ellipsis.
fn fit(t: String, w: f32) -> String {
if render::text_width(&t) <= w {
return t;
}
let mut cut: String = t;
while !cut.is_empty() && render::text_width(&format!("{cut}…")) > w {
cut.pop();
}
format!("{cut}…")
}
fn argb(rgb: u32, a: u32) -> u32 {
(a << 24) | (rgb & 0xFFFFFF)
}
/// Corner brackets of a box.
fn brackets(s: &mut Vec<Shape>, x: f32, y: f32, w: f32, h: f32, len: f32, color: u32) {
for (cx, cy, dx, dy) in [(x, y, 1.0, 1.0), (x + w, y, -1.0, 1.0), (x, y + h, 1.0, -1.0), (x + w, y + h, -1.0, -1.0)] {
let fx = if dx > 0.0 { cx } else { cx - len };
s.push(fill(fx, if dy > 0.0 { cy } else { cy - 1.0 }, len, 1.0, color));
let gx = if dx > 0.0 { cx } else { cx - 1.0 };
s.push(fill(gx, if dy > 0.0 { cy } else { cy - len }, 1.0, len, color));
}
}
/// The camera's picture size, pixels.
const PICTURE: (u32, u32) = (320, 200);
/// Where system `sam`'s camera is and looks: at its target, zoomed so the
/// target fills a good part of the picture; else ahead and a little up.
pub fn camera(sim: &Sim, sam: &Sam) -> CameraView {
let eye = sam.pos + DVec3::Y * 3.5;
let (dir, fov) = match sim.watched(sam) {
Some(d) => {
let rel = d.pos - eye;
let dist = rel.length().max(1.0);
// About 14 blocks of view round a missile far off, wider when it is near.
let fov = (2.0 * (7.0 / dist).atan()).to_degrees().clamp(1.5, 50.0);
(rel / dist, fov as f32)
}
None => {
let h = sam.heading + (sim.tick as f64 * 0.004).sin() * 0.6;
(DVec3::new(-h.sin(), 0.25, -h.cos()).normalize(), 55.0)
}
};
// Minecraft's yaw: 0 looks to +z, 90 to -x; pitch 90 looks down.
let yaw = (-dir.x).atan2(dir.z).to_degrees() as f32;
let pitch = (-dir.y).asin().to_degrees() as f32;
CameraView { id: sam.id, x: eye.x, y: eye.y, z: eye.z, yaw, pitch, fov, width: PICTURE.0, height: PICTURE.1 }
}
/// One system's monitor in the box (x, y, w, h).
fn cell(sim: &Sim, index: usize, x: f32, y: f32, w: f32, h: f32, s: &mut Vec<Shape>) {
let Some(sam) = sim.sams.get(index) else { return };
let spec = sam.kind.spec();
let tick = sim.tick;
s.push(fill(x, y, w, h, 0xC0080C0A));
s.push(fill(x, y, w, 12.0, 0xE0142018));
let state = sam.state;
let lock = matches!(state, SamState::Lock | SamState::Launch | SamState::Fire);
let blink = lock && (tick / 4) % 2 == 0;
let label = state.name();
let lw = render::text_width(label);
s.push(text(fit(format!("№{} {}", index + 1, spec.name), w - lw - 12.0), x + 3.0, y + 2.0, 0xFF9FE0FF));
s.push(text(label, x + w - 3.0 - lw, y + 2.0, argb(state.color(), if blink { 0x80 } else { 0xFF })));
// The radar, at the left: round the system, north up.
let rr = ((h - 28.0) / 2.0).min(w * 0.2).max(10.0);
let (rx, ry) = (x + 4.0 + rr, y + 14.0 + (h - 26.0) / 2.0);
s.push(fill(rx - rr, ry - rr, rr * 2.0, rr * 2.0, 0x60103018));
let seg = if rr > 30.0 { 32 } else { 20 };
for k in [0.5f32, 1.0] {
for i in 0..seg {
let a0 = i as f32 / seg as f32 * std::f32::consts::TAU;
let a1 = (i + 1) as f32 / seg as f32 * std::f32::consts::TAU;
let p = |a: f32, r: f32| (rx + a.sin() * r, ry - a.cos() * r);
let r = rr * k;
s.push(quad(p(a0, r - 0.4), p(a0, r + 0.4), p(a1, r + 0.4), p(a1, r - 0.4), 0x9040FF60));
}
}
let reach = sam.kind.range();
let sweep = (tick as f32 * 0.12 + index as f32) % std::f32::consts::TAU;
for k in 0..6 {
let a1 = sweep - k as f32 * 0.09;
let a0 = a1 - 0.09;
let p = |a: f32| (rx + a.sin() * rr, ry - a.cos() * rr);
s.push(quad((rx, ry), p(a0), p(a1), p(a1), argb(0x40FF70, (0x70 - k * 10) as u32)));
}
let target = sim.watched(sam);
let mut seen = 0;
for d in sim.drones.iter().filter(|d| d.alive()) {
let rel = d.pos - sam.pos;
let flat = (rel.x * rel.x + rel.z * rel.z).sqrt();
if flat > reach * 1.25 {
continue;
}
seen += 1;
let k = (rr as f64 / (reach * 1.25)) as f32;
let (bx, by) = (rx + rel.x as f32 * k, ry + rel.z as f32 * k);
let allowed = sam.roles & d.kind.spec().class.bit() != 0;
let color = if Some(d.id) == target.map(|t| t.id) {
0xFFFFE040
} else if allowed {
0xFFFF4040
} else {
0xFF808080
};
s.push(fill(bx - 1.5, by - 1.5, 3.0, 3.0, color));
}
s.push(fill(rx - 1.5, ry - 1.5, 3.0, 3.0, 0xFF40A0FF));
s.push(text(fit(format!("РЛС {} · {}", distance(reach), seen), rr * 2.0 + 4.0), x + 4.0, y + h - 10.0, 0xFF80FF90));
// The camera, at the right: the world as the system sees it, thermal.
let cx = rx + rr + 6.0;
let (cy, cw, ch) = (y + 15.0, x + w - 4.0 - cx, h - 28.0);
s.push(fill(cx - 1.0, cy - 1.0, cw + 2.0, ch + 2.0, 0xFF203020));
// Searching: colour by day, night vision at night; tracking: thermal.
let night = horizon_mod_sdk::game::get_clock().is_some_and(|c| (13000..23000).contains(&(c.day_time % 24000)));
let mode = match (target.is_some(), night) {
(true, _) => 1,
(false, true) => 2,
(false, false) => 0,
};
s.push(Shape::View(ViewShape { view: sam.id, rect: Rect { x: cx, y: cy, w: cw, h: ch }, mode }));
// Crosshair.
s.push(fill(cx + cw / 2.0 - 0.5, cy + 3.0, 1.0, ch * 0.18, 0x90FFFFFF));
s.push(fill(cx + cw / 2.0 - 0.5, cy + ch * 0.82 - 3.0, 1.0, ch * 0.18, 0x90FFFFFF));
s.push(fill(cx + 3.0, cy + ch / 2.0 - 0.5, cw * 0.18, 1.0, 0x90FFFFFF));
s.push(fill(cx + cw * 0.82 - 3.0, cy + ch / 2.0 - 0.5, cw * 0.18, 1.0, 0x90FFFFFF));
match target {
Some(t) => {
let color = if lock { if blink { 0xFFFF3030 } else { 0xFFFF8080 } } else { 0xFFFFE060 };
let (bw, bh) = (cw * 0.3, ch * 0.34);
brackets(s, cx + cw / 2.0 - bw / 2.0, cy + ch / 2.0 - bh / 2.0, bw, bh, 6.0, color);
if lock {
s.push(text("ЗАХВАТ", cx + cw / 2.0 - 18.0, cy + 3.0, color));
}
if !t.alive() {
s.push(text("ПОРАЖЕНА", cx + cw / 2.0 - 24.0, cy + ch / 2.0 + bh / 2.0 + 2.0, 0xFF60FF60));
}
let rel = t.pos - sam.pos;
let bearing = (heading_of(rel).to_degrees() + 180.0).rem_euclid(360.0);
if ch > 50.0 {
s.push(text(fit(t.kind.spec().name.into(), cw - 6.0), cx + 3.0, cy + ch - 20.0, 0xFFFFFFFF));
}
let mut info = format!("{} в{:.0} {:.0}км/ч", distance(rel.length()), t.pos.y - sam.pos.y, t.vel.length() * 72.0);
if cw > 220.0 {
info += &format!(" П{bearing:03.0}");
}
s.push(text(fit(info, cw - 6.0), cx + 3.0, cy + ch - 10.0, 0xFFC0FFC0));
}
None => s.push(text("ПОИСК", cx + 3.0, cy + ch - 10.0, 0xFF80FF90)),
}
// What it carries and does.
let mut line = String::new();
if !matches!(sam.kind, crate::sim::SamKind::Gepard) {
line += &format!("Р{} ", sam.missiles);
}
if sam.bursts > 0 || spec.key == "gepard" {
line += &format!("С{} ", sam.bursts);
}
line += &format!("сб.{} [{}]", sam.kills, roles_text(sam.roles));
s.push(text(fit(line, cw), cx, y + h - 10.0, 0xFF9FE0FF));
}
/// One system, top left.
pub fn draw(sim: &Sim, index: usize) -> Vec<Shape> {
let mut s = Vec::new();
cell(sim, index, 4.0, 4.0, 300.0, 150.0, &mut s);
s
}
/// Every system in a grid over the screen.
pub fn grid(sim: &Sim, (gw, gh): (f32, f32)) -> Vec<Shape> {
let mut s = Vec::new();
let n = sim.sams.len().min(8);
s.push(fill(0.0, 0.0, gw, gh, 0xE0050806));
s.push(text(
fit(
format!("КОМАНДНЫЙ ПУНКТ ПВО · комплексов {n} · целей {} · сбито {} · J закрыть", sim.drones.iter().filter(|d| d.alive()).count(), sim.kills),
gw - 16.0,
),
8.0,
6.0,
0xFF9FE0FF,
));
if n == 0 {
s.push(text("Нет комплексов: поставьте их пультами развёртывания", 8.0, 24.0, 0xFFFFC060));
return s;
}
let cols = match n {
1 => 1,
2..=4 => 2,
_ => 3,
};
let rows = n.div_ceil(cols);
let (top, gap) = (18.0, 4.0);
let cw = (gw - gap * (cols as f32 + 1.0)) / cols as f32;
let ch = ((gh - top - gap * rows as f32) / rows as f32).min(cw * 0.62);
for i in 0..n {
let (c, r) = (i % cols, i / cols);
let x = gap + c as f32 * (cw + gap);
let y = top + r as f32 * (ch + gap);
cell(sim, i, x, y, cw, ch, &mut s);
}
s
}