2109 lines
98 KiB
Rust
2109 lines
98 KiB
Rust
// In-canvas user interface: text-mode windows with the Dinowyde bitmap font.
|
|
// All texts come from the language files (see i18n.rs).
|
|
|
|
use crate::game::*;
|
|
use crate::gfx::*;
|
|
use crate::i18n::{languages, tr, trf};
|
|
use crate::map::*;
|
|
use crate::render3d::{minimap_color, Cam};
|
|
use crate::settings::{key_name, Action, Settings};
|
|
use macroquad::prelude::*;
|
|
|
|
// ---- interface scaling
|
|
// The interface is drawn on a virtual screen with a fixed number of rows (pixel look: twice the
|
|
// world's rows, the world's rows on touch screens), which is scaled up to the device at the end of
|
|
// the frame (see begin_ui, end_frame) - on every device the same pixel grid. Small screens
|
|
// draw in device pixels. Every size is an integer multiple
|
|
// of the font's 6x8 pixel glyphs: small text = 1*F, normal text = 2*F, titles = 4*F, where F
|
|
// is the interface factor (1 on the virtual screen, whose rows follow the "interface size"
|
|
// setting; automatic from the resolution in device pixels).
|
|
thread_local! {
|
|
static TOUCH: std::cell::Cell<bool> = std::cell::Cell::new(false);
|
|
static POINTER: std::cell::Cell<(f32, f32)> = std::cell::Cell::new((-1e5, -1e5));
|
|
static VIRTUAL_ROWS: std::cell::Cell<u32> = std::cell::Cell::new(0);
|
|
static UI_TARGET: std::cell::RefCell<Option<(RenderTarget, u32, u32)>> = std::cell::RefCell::new(None);
|
|
static UP_TARGET: std::cell::RefCell<Option<(RenderTarget, u32, u32)>> = std::cell::RefCell::new(None);
|
|
static FRAME_STARTED: std::cell::Cell<bool> = std::cell::Cell::new(false);
|
|
static OPAQUE: std::cell::RefCell<Option<Material>> = std::cell::RefCell::new(None);
|
|
}
|
|
|
|
// the rows of the virtual screen for this frame (0: device pixels). Set once per frame before
|
|
// anything is drawn.
|
|
pub fn set_virtual_rows(rows: u32) {
|
|
let device_h = screen_height() * screen_dpi_scale();
|
|
// a screen smaller than the virtual one draws in device pixels
|
|
let rows = if rows > 0 && device_h >= rows as f32 { rows } else { 0 };
|
|
VIRTUAL_ROWS.with(|c| c.set(rows));
|
|
}
|
|
pub fn virtual_rows() -> u32 {
|
|
VIRTUAL_ROWS.with(|c| c.get())
|
|
}
|
|
// device pixels per CSS pixel
|
|
pub fn device_dpr() -> f32 {
|
|
screen_dpi_scale()
|
|
}
|
|
|
|
// touch interface active (bigger interface, category toolbar, gestures)
|
|
pub fn set_touch(on: bool) {
|
|
TOUCH.with(|c| c.set(on));
|
|
}
|
|
pub fn touch_ui() -> bool {
|
|
TOUCH.with(|c| c.get())
|
|
}
|
|
// in touch mode the pointer is the finger (device pixels), set by the input code each frame
|
|
pub fn set_pointer(x: f32, y: f32) {
|
|
POINTER.with(|c| c.set((x, y)));
|
|
}
|
|
|
|
// interface units per CSS pixel (device pixels without the virtual screen)
|
|
pub fn dpr() -> f32 {
|
|
match virtual_rows() {
|
|
0 => screen_dpi_scale(),
|
|
rows => rows as f32 / screen_height().max(1.0),
|
|
}
|
|
}
|
|
// screen size and mouse in device pixels
|
|
pub fn sw() -> f32 {
|
|
(screen_width() * dpr()).round()
|
|
}
|
|
pub fn sh() -> f32 {
|
|
(screen_height() * dpr()).round()
|
|
}
|
|
pub fn mouse() -> (f32, f32) {
|
|
if touch_ui() {
|
|
return POINTER.with(|c| c.get());
|
|
}
|
|
let (x, y) = mouse_position();
|
|
(x * dpr(), y * dpr())
|
|
}
|
|
|
|
// interface factor (integer)
|
|
#[allow(non_snake_case)]
|
|
pub fn F() -> u32 {
|
|
// the virtual screen already has the right size for the interface
|
|
if virtual_rows() > 0 {
|
|
return 1;
|
|
}
|
|
let auto = ((sw() / 1280.0).min(sh() / 760.0).floor() as u32).clamp(1, 4);
|
|
if touch_ui() {
|
|
// fingers need bigger buttons: about 2/3 of the device pixel ratio,
|
|
// as long as at least 40 columns fit across the screen
|
|
let want = (dpr() * 2.0 / 3.0).round() as u32;
|
|
let fit = (sw() / (40.0 * GW * 2.0)).floor() as u32;
|
|
return auto.max(want.min(fit)).clamp(1, 4);
|
|
}
|
|
auto
|
|
}
|
|
// text scales: 1x, 2x and 4x the glyph size, times F
|
|
#[allow(non_snake_case)]
|
|
pub fn SMALL() -> f32 {
|
|
F() as f32
|
|
}
|
|
#[allow(non_snake_case)]
|
|
pub fn S() -> f32 {
|
|
2.0 * F() as f32
|
|
}
|
|
#[allow(non_snake_case)]
|
|
pub fn TITLE() -> f32 {
|
|
4.0 * F() as f32
|
|
}
|
|
// character cell of normal text = the layout grid
|
|
#[allow(non_snake_case)]
|
|
pub fn CW() -> f32 {
|
|
GW * S()
|
|
}
|
|
#[allow(non_snake_case)]
|
|
pub fn CH() -> f32 {
|
|
GH * S()
|
|
}
|
|
// pixel factor for icons and other pixel art
|
|
#[allow(non_snake_case)]
|
|
pub fn K() -> f32 {
|
|
F() as f32
|
|
}
|
|
|
|
// x position that centres a width on the character grid
|
|
pub fn grid_center(total: f32, w: f32) -> f32 {
|
|
(((total - w) * 0.5) / CW()).floor() * CW()
|
|
}
|
|
|
|
pub fn grid_center_y(total: f32, h: f32) -> f32 {
|
|
(((total - h) * 0.5) / CH()).floor() * CH()
|
|
}
|
|
|
|
// 2D camera mapping one unit to one interface pixel (top-left origin): on the virtual screen
|
|
// (cleared the first time in a frame) or straight on the device
|
|
pub fn begin_ui() {
|
|
let (w, h) = (sw(), sh());
|
|
if virtual_rows() == 0 {
|
|
set_camera(&Camera2D { target: vec2(w * 0.5, h * 0.5), zoom: vec2(2.0 / w, 2.0 / h), ..Default::default() });
|
|
return;
|
|
}
|
|
let (tw, th) = (w.max(1.0) as u32, h.max(1.0) as u32);
|
|
let rt = UI_TARGET.with(|t| {
|
|
let mut t = t.borrow_mut();
|
|
if t.as_ref().map_or(true, |(_, a, b)| *a != tw || *b != th) {
|
|
let rt = render_target_ex(tw, th, RenderTargetParams { sample_count: 1, depth: false });
|
|
rt.texture.set_filter(FilterMode::Nearest);
|
|
*t = Some((rt, tw, th));
|
|
}
|
|
t.as_ref().unwrap().0.clone()
|
|
});
|
|
let mut c = Camera2D::from_display_rect(Rect::new(0.0, 0.0, w, h));
|
|
c.render_target = Some(rt);
|
|
set_camera(&c);
|
|
if !FRAME_STARTED.with(|f| f.replace(true)) {
|
|
clear_background(Color::new(0.0, 0.0, 0.0, 0.0));
|
|
}
|
|
}
|
|
|
|
// end of a frame: the virtual screen goes to the device - up by the next whole factor (nearest),
|
|
// then smoothly to the device size, so every pixel has the same size and stays crisp
|
|
pub fn end_frame() {
|
|
if !FRAME_STARTED.with(|f| f.replace(false)) || virtual_rows() == 0 {
|
|
set_default_camera();
|
|
return;
|
|
}
|
|
let Some((rt, tw, th)) = UI_TARGET.with(|t| t.borrow().clone()) else { return };
|
|
let scale = screen_height() * screen_dpi_scale() / th as f32;
|
|
let n = scale.ceil().max(1.0) as u32;
|
|
let (uw, uh) = (tw * n, th * n);
|
|
let up = UP_TARGET.with(|t| {
|
|
let mut t = t.borrow_mut();
|
|
if t.as_ref().map_or(true, |(_, a, b)| *a != uw || *b != uh) {
|
|
let up = render_target_ex(uw, uh, RenderTargetParams { sample_count: 1, depth: false });
|
|
up.texture.set_filter(FilterMode::Linear);
|
|
*t = Some((up, uw, uh));
|
|
}
|
|
t.as_ref().unwrap().0.clone()
|
|
});
|
|
let mut c = Camera2D::from_display_rect(Rect::new(0.0, 0.0, uw as f32, uh as f32));
|
|
c.render_target = Some(up.clone());
|
|
set_camera(&c);
|
|
clear_background(Color::new(0.0, 0.0, 0.0, 1.0));
|
|
// opaque: translucent parts must not let the black below shine through
|
|
OPAQUE.with(|m| {
|
|
let mut m = m.borrow_mut();
|
|
if m.is_none() {
|
|
*m = Some(crate::render3d::opaque_material());
|
|
}
|
|
gl_use_material(m.as_ref().unwrap());
|
|
});
|
|
draw_texture_ex(&rt.texture, 0.0, 0.0, WHITE, DrawTextureParams { dest_size: Some(vec2(uw as f32, uh as f32)), flip_y: true, ..Default::default() });
|
|
gl_use_default_material();
|
|
set_default_camera();
|
|
draw_texture_ex(&up.texture, 0.0, 0.0, WHITE, DrawTextureParams { dest_size: Some(vec2(screen_width(), screen_height())), flip_y: true, ..Default::default() });
|
|
}
|
|
|
|
// bar made of block characters: full blocks, an eighth block for the rest, shaded background
|
|
const EIGHTHS: [char; 7] = ['▏', '▎', '▍', '▌', '▋', '▊', '▉'];
|
|
|
|
pub fn ascii_bar(gfx: &Gfx, x: f32, y: f32, cols: usize, frac: f32, col: Color, scale: f32) {
|
|
let eighths = (frac.clamp(0.0, 1.0) * cols as f32 * 8.0).round() as usize;
|
|
let (full, part) = (eighths / 8, eighths % 8);
|
|
let dim = Color::new(col.r * 0.35, col.g * 0.35, col.b * 0.35, col.a);
|
|
for k in 0..cols {
|
|
let px = x + k as f32 * GW * scale;
|
|
if k < full {
|
|
gfx.glyph('█', px, y, scale, col);
|
|
} else {
|
|
gfx.glyph('░', px, y, scale, dim);
|
|
if k == full && part > 0 {
|
|
gfx.glyph(EIGHTHS[part - 1], px, y, scale, col);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub const FRAME: Color = Color::new(0.62, 0.52, 0.36, 1.0);
|
|
pub const FRAME_HI: Color = Color::new(0.95, 0.8, 0.4, 1.0);
|
|
pub const BG: Color = Color::new(0.06, 0.04, 0.05, 0.92);
|
|
pub const TXT: Color = Color::new(0.9, 0.86, 0.78, 1.0);
|
|
pub const DIM: Color = Color::new(0.6, 0.56, 0.5, 1.0);
|
|
|
|
pub fn side_color(side: Option<u8>) -> Color {
|
|
match side {
|
|
Some(0) => Color::new(1.0, 0.85, 0.45, 1.0),
|
|
Some(1) => Color::new(0.85, 0.55, 0.25, 1.0),
|
|
Some(2) => Color::new(0.35, 0.95, 0.40, 1.0),
|
|
Some(3) => Color::new(0.40, 0.60, 1.00, 1.0),
|
|
Some(4) => Color::new(0.85, 0.75, 0.30, 1.0),
|
|
_ => TXT,
|
|
}
|
|
}
|
|
|
|
pub fn chat_color(side: Option<u8>, alpha: f32) -> Color {
|
|
let c = side_color(side);
|
|
Color::new(c.r, c.g, c.b, alpha)
|
|
}
|
|
pub const GOLD: Color = Color::new(1.0, 0.84, 0.25, 1.0);
|
|
pub const MANA: Color = Color::new(0.7, 0.45, 1.0, 1.0);
|
|
pub const RED: Color = Color::new(0.95, 0.3, 0.3, 1.0);
|
|
pub const GREEN: Color = Color::new(0.45, 0.9, 0.45, 1.0);
|
|
pub const BLUE: Color = Color::new(0.5, 0.75, 1.0, 1.0);
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
|
pub enum Tool {
|
|
Dig,
|
|
Hand,
|
|
Room(usize),
|
|
Sell,
|
|
Flag,
|
|
Lightning,
|
|
Heal,
|
|
Summon,
|
|
Tower(TowerKind),
|
|
Bridge,
|
|
Steel,
|
|
Drawbridge,
|
|
Wall,
|
|
Siege(u8), // 0 battering ram, 1 catapult
|
|
Trap(crate::traps::TrapKind),
|
|
Spell(crate::spells::Spell),
|
|
Order(u8), // 0 attack, 1 guard
|
|
}
|
|
|
|
// New tools are appended so that existing key bindings ("tool<i>") keep their meaning
|
|
pub const TOOLS: [Tool; 52] = [
|
|
Tool::Dig,
|
|
Tool::Hand,
|
|
Tool::Room(0),
|
|
Tool::Room(1),
|
|
Tool::Room(2),
|
|
Tool::Room(3),
|
|
Tool::Room(4),
|
|
Tool::Sell,
|
|
Tool::Flag,
|
|
Tool::Lightning,
|
|
Tool::Heal,
|
|
Tool::Summon,
|
|
Tool::Room(5),
|
|
Tool::Room(6),
|
|
Tool::Tower(TowerKind::Arrow),
|
|
Tool::Tower(TowerKind::Cannon),
|
|
Tool::Tower(TowerKind::Magic),
|
|
Tool::Bridge,
|
|
Tool::Room(7),
|
|
Tool::Steel,
|
|
Tool::Tower(TowerKind::Rune),
|
|
Tool::Room(8),
|
|
Tool::Trap(crate::traps::TrapKind::Spikes),
|
|
Tool::Trap(crate::traps::TrapKind::Poison),
|
|
Tool::Trap(crate::traps::TrapKind::Fire),
|
|
Tool::Trap(crate::traps::TrapKind::Roller),
|
|
Tool::Trap(crate::traps::TrapKind::Alarm),
|
|
Tool::Trap(crate::traps::TrapKind::Mine),
|
|
Tool::Trap(crate::traps::TrapKind::Frost),
|
|
Tool::Trap(crate::traps::TrapKind::WoodDoor),
|
|
Tool::Trap(crate::traps::TrapKind::IronDoor),
|
|
Tool::Trap(crate::traps::TrapKind::MagicDoor),
|
|
Tool::Spell(crate::spells::Spell::Eye),
|
|
Tool::Spell(crate::spells::Spell::CaveIn),
|
|
Tool::Spell(crate::spells::Spell::Haste),
|
|
Tool::Spell(crate::spells::Spell::Stoneskin),
|
|
Tool::Spell(crate::spells::Spell::GoldRain),
|
|
Tool::Spell(crate::spells::Spell::Invisible),
|
|
Tool::Spell(crate::spells::Spell::Plague),
|
|
Tool::Spell(crate::spells::Spell::Quake),
|
|
Tool::Room(9),
|
|
Tool::Room(10),
|
|
Tool::Room(11),
|
|
Tool::Room(12),
|
|
Tool::Room(13),
|
|
Tool::Order(0),
|
|
Tool::Order(1),
|
|
Tool::Drawbridge,
|
|
Tool::Spell(crate::spells::Spell::Freeze),
|
|
Tool::Wall,
|
|
Tool::Siege(0),
|
|
Tool::Siege(1),
|
|
];
|
|
|
|
|
|
pub struct ToolInfo {
|
|
pub label: String,
|
|
pub name: String,
|
|
pub desc: String,
|
|
pub icon: u32,
|
|
}
|
|
|
|
pub fn tool_info(t: Tool) -> ToolInfo {
|
|
let (id, icon, room) = match t {
|
|
Tool::Dig => ("dig", I_PICK, None),
|
|
Tool::Hand => ("hand", I_HAND, None),
|
|
Tool::Room(0) => ("treasury", T_TREASURY, Some(0)),
|
|
Tool::Room(1) => ("lair", T_LAIR, Some(1)),
|
|
Tool::Room(2) => ("farm", T_FARM, Some(2)),
|
|
Tool::Room(3) => ("training", T_TRAINING, Some(3)),
|
|
Tool::Room(4) => ("library", T_LIBRARY, Some(4)),
|
|
Tool::Room(5) => ("torture", T_TORTURE, Some(5)),
|
|
Tool::Room(6) => ("temple", T_TEMPLE, Some(6)),
|
|
Tool::Room(7) => ("aerie", T_AERIE, Some(7)),
|
|
Tool::Room(8) => ("forge", T_FORGE, Some(8)),
|
|
Tool::Room(9) => ("prison", T_PRISON, Some(9)),
|
|
Tool::Room(10) => ("graveyard", T_GRAVEYARD, Some(10)),
|
|
Tool::Room(11) => ("guardpost", T_GUARDPOST, Some(11)),
|
|
Tool::Room(12) => ("arena", T_ARENA, Some(12)),
|
|
Tool::Room(_) => ("altar", T_ALTAR, Some(13)),
|
|
Tool::Trap(k) => (k.id(), I_TRAPS + crate::traps::TRAP_KINDS.iter().position(|&t| t == k).unwrap_or(0) as u32, None),
|
|
Tool::Order(0) => ("order_attack", I_ORDERS, None),
|
|
Tool::Order(_) => ("order_guard", I_ORDERS + 1, None),
|
|
Tool::Spell(sp) => (sp.id(), crate::spells::spell_icon(sp), None),
|
|
Tool::Tower(TowerKind::Arrow) => ("arrow", I_ARROW, None),
|
|
Tool::Tower(TowerKind::Cannon) => ("cannon", I_CANNON, None),
|
|
Tool::Tower(TowerKind::Magic) => ("magic", I_MAGIC, None),
|
|
Tool::Tower(TowerKind::Rune) => ("rune", I_RUNE, None),
|
|
Tool::Sell => ("sell", I_SELL, None),
|
|
Tool::Flag => ("flag", I_FLAG, None),
|
|
Tool::Lightning => ("lightning", I_BOLT, None),
|
|
Tool::Heal => ("heal", I_HEAL, None),
|
|
Tool::Summon => ("summon", I_SUMMON, None),
|
|
Tool::Bridge => ("bridge", I_BRIDGE, None),
|
|
Tool::Drawbridge => ("drawbridge", I_DRAWBRIDGE, None),
|
|
Tool::Wall => ("wall", I_WALL, None),
|
|
Tool::Siege(0) => ("ram", I_RAM, None),
|
|
Tool::Siege(_) => ("catapult", I_CATAPULT, None),
|
|
Tool::Steel => ("steel", I_STEEL, None),
|
|
};
|
|
let name = match room {
|
|
Some(r) => tr(ROOM_NAMES[r]),
|
|
None => tr(&format!("tool.{}.name", id)),
|
|
};
|
|
ToolInfo { label: tr(&format!("tool.{}.label", id)), name, desc: tr(&format!("tool.{}.desc", id)), icon }
|
|
}
|
|
|
|
// costs that do not depend on the game (bestiary)
|
|
pub fn tool_cost_static(t: Tool) -> String {
|
|
match t {
|
|
Tool::Spell(sp) => trf("cost.mana", &[(sp.cost() as i32).to_string()]),
|
|
Tool::Trap(k) => trf("cost.work", &[(k.work() as i32).to_string()]),
|
|
Tool::Bridge => trf("cost.bridge", &[BRIDGE_COST[0].to_string(), (crate::wood::BRIDGE_WOOD as i32).to_string(), BRIDGE_COST[1].to_string()]),
|
|
Tool::Wall => trf("cost.gold_tile", &[(crate::walls::WALL_COST as i32).to_string()]),
|
|
Tool::Siege(w) => {
|
|
let (g, wd) = if w == 0 { crate::siege::RAM_COST } else { crate::siege::CATAPULT_COST };
|
|
trf("cost.gold_wood", &[(g as i32).to_string(), (wd as i32).to_string()])
|
|
}
|
|
Tool::Drawbridge => trf("cost.drawbridge", &[DRAWBRIDGE_COST[0].to_string(), (crate::wood::DRAWBRIDGE_WOOD as i32).to_string(), DRAWBRIDGE_COST[1].to_string()]),
|
|
Tool::Steel => trf("cost.steel", &[STEEL_COST[0].to_string(), STEEL_COST[1].to_string()]),
|
|
Tool::Tower(TowerKind::Rune) => {
|
|
let s = tower_stats(TowerKind::Rune);
|
|
trf("best.cost_rune", &[s.cost[1].to_string(), s.cost[2].to_string()])
|
|
}
|
|
Tool::Tower(k) => {
|
|
let s = tower_stats(k);
|
|
let mut c = trf("cost.tower", &[s.cost[0].to_string(), s.cost[1].to_string(), s.cost[2].to_string()]);
|
|
if s.mana > 0.0 {
|
|
c += &trf("cost.per_shot", &[(s.mana as i32).to_string()]);
|
|
}
|
|
c
|
|
}
|
|
_ => String::new(),
|
|
}
|
|
}
|
|
|
|
pub fn tool_cost(t: Tool, g: &Game) -> String {
|
|
match t {
|
|
Tool::Trap(_) | Tool::Spell(_) => tool_cost_static(t),
|
|
Tool::Room(r) => trf("cost.gold_tile", &[ROOM_COST[r].to_string()]),
|
|
Tool::Lightning => trf("cost.mana", &["50".into()]),
|
|
Tool::Heal => trf("cost.mana", &["60".into()]),
|
|
Tool::Summon => trf("cost.mana", &[(g.summon_cost() as i32).to_string()]),
|
|
Tool::Bridge => trf("cost.bridge", &[BRIDGE_COST[0].to_string(), (crate::wood::BRIDGE_WOOD as i32).to_string(), BRIDGE_COST[1].to_string()]),
|
|
Tool::Wall => trf("cost.gold_tile", &[(crate::walls::WALL_COST as i32).to_string()]),
|
|
Tool::Siege(w) => {
|
|
let (g, wd) = if w == 0 { crate::siege::RAM_COST } else { crate::siege::CATAPULT_COST };
|
|
trf("cost.gold_wood", &[(g as i32).to_string(), (wd as i32).to_string()])
|
|
}
|
|
Tool::Drawbridge => trf("cost.drawbridge", &[DRAWBRIDGE_COST[0].to_string(), (crate::wood::DRAWBRIDGE_WOOD as i32).to_string(), DRAWBRIDGE_COST[1].to_string()]),
|
|
Tool::Steel => trf("cost.steel", &[STEEL_COST[0].to_string(), STEEL_COST[1].to_string()]),
|
|
Tool::Tower(TowerKind::Rune) => {
|
|
let s = tower_stats(TowerKind::Rune);
|
|
trf("cost.rune", &[g.rune_available().to_string(), s.cost[1].to_string(), s.cost[2].to_string()])
|
|
}
|
|
Tool::Tower(k) => {
|
|
let s = tower_stats(k);
|
|
let mut c = trf("cost.tower", &[s.cost[0].to_string(), s.cost[1].to_string(), s.cost[2].to_string()]);
|
|
if s.mana > 0.0 {
|
|
c += &trf("cost.per_shot", &[(s.mana as i32).to_string()]);
|
|
}
|
|
c
|
|
}
|
|
_ => String::new(),
|
|
}
|
|
}
|
|
|
|
// Touch toolbar: tool categories (indices into TOOLS); categories with several tools open a row
|
|
pub const CATS: [(&str, &[usize]); 7] = [
|
|
("cat.dig", &[0]),
|
|
("cat.hand", &[1]),
|
|
("cat.rooms", &[2, 3, 4, 5, 6, 12, 40, 13, 18, 21, 41, 42, 43, 44, 17, 47, 49]),
|
|
("cat.towers", &[14, 15, 16, 20]),
|
|
("cat.traps", &[22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 19, 50, 51]),
|
|
("cat.spells", &[9, 10, 11, 32, 33, 34, 35, 36, 37, 38, 39, 48]),
|
|
("cat.orders", &[8, 45, 46, 7]),
|
|
];
|
|
|
|
#[derive(Clone, Copy, PartialEq, Debug)]
|
|
enum TBtn {
|
|
Cat(usize),
|
|
Tool(usize),
|
|
Menu,
|
|
Overview,
|
|
Talents, // the master's talent window
|
|
Learn(u8), // spend a point on a branch
|
|
OvRow(u32),
|
|
OvScroll(i32),
|
|
OvFallen, // switch the overview between the living and the fallen
|
|
GoldTo, // the next ally to receive gold
|
|
Gold(u32), // send that much gold to him
|
|
}
|
|
|
|
pub enum UiAction {
|
|
None,
|
|
EndNext, // mission over: next mission (won) or try again (lost)
|
|
EndCampaign, // mission over: back to the campaign
|
|
Briefing, // show the briefing of the current mission again
|
|
Locked, // a tool that is not available yet (campaign)
|
|
OpenMenu,
|
|
SelectTool(Tool),
|
|
MinimapJump(f32, f32),
|
|
CloseHelp,
|
|
NewGame,
|
|
SelectLanguage(String),
|
|
ChatSetTeam(bool),
|
|
ChatSend,
|
|
SendGold(u8, u32), // gold for an ally of a match (side, amount)
|
|
LearnTalent(u8), // spend a talent point on a branch
|
|
Consumed,
|
|
}
|
|
|
|
pub struct Ui {
|
|
pub tool: Tool,
|
|
pub show_help: bool,
|
|
pub show_overview: bool, // list of all creatures
|
|
pub show_talents: bool, // the master's talents
|
|
pub coop_end: Option<(bool, bool, bool)>, // end of an editor co-op mission: (host, a next mission, asked for it)
|
|
gold_to: usize, // the ally chosen in the creature overview
|
|
pub ov_scroll: usize,
|
|
pub ov_fallen: bool, // the overview shows the hall of the fallen
|
|
ov_rect: Option<Rect>, // where the creature overview is (mouse wheel, swipe)
|
|
pub show_langsel: bool,
|
|
pub log: Vec<(Msg, f64)>,
|
|
pub chat_log: Vec<(Option<u8>, String, String, f64)>,
|
|
pub chat_open: bool,
|
|
pub chat_text: String,
|
|
pub chat_team: bool,
|
|
chat_rects: [(Rect, Option<bool>); 2],
|
|
chat_send_rect: Rect,
|
|
pub rects: Vec<Rect>,
|
|
minimap_img: Image,
|
|
minimap_tex: Texture2D,
|
|
minimap_t: f64,
|
|
minimap_rect: Rect,
|
|
pub last_locked: Option<usize>, // the locked tool clicked last
|
|
end_buttons: Vec<(Rect, u8)>,
|
|
goals_rect: Rect, // mission goals panel (click: briefing) // end dialog: 0 new game, 1 next/again, 2 campaign
|
|
lang_rects: Vec<(Rect, String)>,
|
|
langsel_scroll: usize, // first line of the first-start language grid
|
|
pub hand_anim: Option<(u8, f64)>, // 1 = backhand, 2 = punch, start time
|
|
// touch interface
|
|
open_cat: Option<usize>,
|
|
tip: Option<(usize, f64)>, // tool whose description is shown, since
|
|
touch_btns: Vec<(Rect, TBtn)>,
|
|
hud_h: f32,
|
|
}
|
|
|
|
// word wrap to `cols` characters: keeps line breaks, splits words longer than a line
|
|
pub fn wrap(s: &str, cols: usize) -> Vec<String> {
|
|
let cols = cols.max(1);
|
|
let mut out = Vec::new();
|
|
for para in s.split('\n') {
|
|
let mut line = String::new();
|
|
for word in para.split(' ') {
|
|
let mut word: Vec<char> = word.chars().collect();
|
|
while word.len() > cols {
|
|
if !line.is_empty() {
|
|
out.push(std::mem::take(&mut line));
|
|
}
|
|
out.push(word.drain(..cols).collect());
|
|
}
|
|
let word: String = word.into_iter().collect();
|
|
let len = line.chars().count();
|
|
if len > 0 && len + 1 + word.chars().count() > cols {
|
|
out.push(std::mem::take(&mut line));
|
|
}
|
|
if !line.is_empty() {
|
|
line.push(' ');
|
|
}
|
|
line.push_str(&word);
|
|
}
|
|
out.push(line);
|
|
}
|
|
while out.last().map_or(false, |l| l.is_empty()) {
|
|
out.pop();
|
|
}
|
|
out
|
|
}
|
|
|
|
// what a talent branch gives at its level now, and at the next level
|
|
fn branch_text(t: &crate::talents::Talents, b: usize) -> String {
|
|
use crate::talents::*;
|
|
let desc = |lv: u8| {
|
|
let l = lv as i32;
|
|
let (a, c) = match b {
|
|
BUILDER => (15 * l, 10 * l),
|
|
WARLORD => (5 * l, 5 * l),
|
|
WARLOCK => (15 * l, 6 * l),
|
|
_ => (6 * l, 6 * l),
|
|
};
|
|
trf(&format!("{}.desc", NAMES[b]), &[a.to_string(), c.to_string()])
|
|
};
|
|
let lv = t.lv[b];
|
|
let now = if lv == 0 { tr("talent.none") } else { desc(lv) };
|
|
if lv < MAX_LEVEL {
|
|
format!("{} {}", now, trf("talent.then", &[desc(lv + 1)]))
|
|
} else {
|
|
now
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod wrap_tests {
|
|
#[test]
|
|
fn wrap_keeps_every_line_inside() {
|
|
let l = super::wrap("Cost: 30 gold + 2 wood/tile over water, 60 gold over lava", 20);
|
|
assert!(l.len() >= 3 && l.iter().all(|x| x.chars().count() <= 20), "{:?}", l);
|
|
let l = super::wrap("a\nsuperlongwordwithoutanyspaces", 8);
|
|
assert_eq!(l[0], "a");
|
|
assert!(l.iter().all(|x| x.chars().count() <= 8), "{:?}", l);
|
|
}
|
|
}
|
|
|
|
fn mmss(t: f32) -> String {
|
|
let t = t.max(0.0) as i32;
|
|
format!("{}:{:02}", t / 60, t % 60)
|
|
}
|
|
|
|
impl Ui {
|
|
pub fn new() -> Ui {
|
|
let img = Image::gen_image_color(64, 64, BLACK);
|
|
let tex = Texture2D::from_image(&img);
|
|
tex.set_filter(FilterMode::Nearest);
|
|
Ui {
|
|
tool: Tool::Dig,
|
|
show_help: true,
|
|
show_overview: false,
|
|
show_talents: false,
|
|
coop_end: None,
|
|
gold_to: 0,
|
|
ov_scroll: 0,
|
|
ov_fallen: false,
|
|
ov_rect: None,
|
|
show_langsel: false,
|
|
log: Vec::new(),
|
|
chat_log: Vec::new(),
|
|
chat_open: false,
|
|
chat_text: String::new(),
|
|
chat_team: false,
|
|
chat_rects: [(Rect::new(0.0, 0.0, 0.0, 0.0), Some(false)); 2],
|
|
chat_send_rect: Rect::new(0.0, 0.0, 0.0, 0.0),
|
|
rects: Vec::new(),
|
|
minimap_img: img,
|
|
minimap_tex: tex,
|
|
minimap_t: -1.0,
|
|
minimap_rect: Rect::new(0.0, 0.0, 0.0, 0.0),
|
|
last_locked: None,
|
|
end_buttons: Vec::new(),
|
|
goals_rect: Rect::new(0.0, 0.0, 0.0, 0.0),
|
|
lang_rects: Vec::new(),
|
|
langsel_scroll: 0,
|
|
hand_anim: None,
|
|
open_cat: None,
|
|
tip: None,
|
|
touch_btns: Vec::new(),
|
|
hud_h: 0.0,
|
|
}
|
|
}
|
|
|
|
pub fn over_ui(&self, x: f32, y: f32) -> bool {
|
|
self.chat_open || self.show_help || self.show_langsel || self.rects.iter().any(|r| r.contains(vec2(x, y)))
|
|
}
|
|
|
|
pub fn minimap_contains(&self, x: f32, y: f32) -> bool {
|
|
self.minimap_rect.contains(vec2(x, y))
|
|
}
|
|
|
|
pub fn minimap_world(&self, x: f32, y: f32) -> (f32, f32) {
|
|
let r = self.minimap_rect;
|
|
((x - r.x) / r.w * mw() as f32, (y - r.y) / r.h * mh() as f32)
|
|
}
|
|
|
|
pub fn click(&mut self, x: f32, y: f32, g: &Game) -> UiAction {
|
|
let p = vec2(x, y);
|
|
if self.chat_open {
|
|
if self.chat_rects[0].0.contains(p) {
|
|
self.chat_team = false;
|
|
return UiAction::ChatSetTeam(false);
|
|
}
|
|
if self.chat_rects[1].0.contains(p) {
|
|
self.chat_team = true;
|
|
return UiAction::ChatSetTeam(true);
|
|
}
|
|
if self.chat_send_rect.contains(p) {
|
|
return UiAction::ChatSend;
|
|
}
|
|
return UiAction::Consumed;
|
|
}
|
|
if self.show_langsel {
|
|
for (r, code) in &self.lang_rects {
|
|
if r.contains(p) {
|
|
return UiAction::SelectLanguage(code.clone());
|
|
}
|
|
}
|
|
return UiAction::Consumed;
|
|
}
|
|
if g.state != 0 {
|
|
for &(r, code) in &self.end_buttons {
|
|
if r.contains(p) {
|
|
return match code {
|
|
0 => UiAction::NewGame,
|
|
1 => UiAction::EndNext,
|
|
_ => UiAction::EndCampaign,
|
|
};
|
|
}
|
|
}
|
|
return UiAction::Consumed;
|
|
}
|
|
if self.show_help {
|
|
return UiAction::CloseHelp;
|
|
}
|
|
if let Some(&(_, b)) = self.touch_btns.iter().find(|(r, _)| r.contains(p)) {
|
|
return match b {
|
|
TBtn::Menu => {
|
|
self.open_cat = None;
|
|
UiAction::OpenMenu
|
|
}
|
|
TBtn::Overview => {
|
|
self.open_cat = None;
|
|
self.show_overview = !self.show_overview;
|
|
self.show_talents = false;
|
|
self.ov_scroll = 0;
|
|
UiAction::Consumed
|
|
}
|
|
TBtn::Talents => {
|
|
self.open_cat = None;
|
|
self.show_talents = !self.show_talents;
|
|
self.show_overview = false;
|
|
UiAction::Consumed
|
|
}
|
|
TBtn::Learn(b) => UiAction::LearnTalent(b),
|
|
TBtn::OvFallen => {
|
|
self.ov_fallen = !self.ov_fallen;
|
|
self.ov_scroll = 0;
|
|
UiAction::Consumed
|
|
}
|
|
TBtn::OvScroll(d) => {
|
|
self.ov_scroll = (self.ov_scroll as i32 + d).max(0) as usize;
|
|
UiAction::Consumed
|
|
}
|
|
TBtn::GoldTo => {
|
|
self.gold_to += 1;
|
|
UiAction::Consumed
|
|
}
|
|
TBtn::Gold(amount) => {
|
|
let allies = g.gold_allies();
|
|
match allies.get(self.gold_to % allies.len().max(1)) {
|
|
Some(&to) => UiAction::SendGold(to, amount),
|
|
None => UiAction::Consumed,
|
|
}
|
|
}
|
|
TBtn::OvRow(id) => match g.ents.iter().find(|e| e.id == id) {
|
|
Some(e) => UiAction::MinimapJump(e.disp_x, e.disp_y),
|
|
None => UiAction::Consumed,
|
|
},
|
|
TBtn::Tool(i) if !g.tool_allowed(i) => {
|
|
self.last_locked = Some(i);
|
|
UiAction::Locked
|
|
}
|
|
TBtn::Tool(i) => {
|
|
self.open_cat = None;
|
|
self.tip = Some((i, get_time()));
|
|
UiAction::SelectTool(TOOLS[i])
|
|
}
|
|
TBtn::Cat(c) => {
|
|
let tools = CATS[c].1;
|
|
if tools.len() == 1 && !g.tool_allowed(tools[0]) {
|
|
self.last_locked = Some(tools[0]);
|
|
UiAction::Locked
|
|
} else if tools.len() == 1 {
|
|
self.open_cat = None;
|
|
self.tip = Some((tools[0], get_time()));
|
|
UiAction::SelectTool(TOOLS[tools[0]])
|
|
} else {
|
|
self.open_cat = if self.open_cat == Some(c) { None } else { Some(c) };
|
|
UiAction::Consumed
|
|
}
|
|
}
|
|
};
|
|
}
|
|
// a tap elsewhere closes an open category row
|
|
self.open_cat = None;
|
|
if g.mission.is_some() && self.goals_rect.contains(p) {
|
|
return UiAction::Briefing;
|
|
}
|
|
if self.minimap_rect.contains(p) {
|
|
let (wx, wy) = self.minimap_world(x, y);
|
|
return UiAction::MinimapJump(wx, wy);
|
|
}
|
|
if self.over_ui(x, y) {
|
|
return UiAction::Consumed;
|
|
}
|
|
UiAction::None
|
|
}
|
|
|
|
pub fn draw(&mut self, g: &Game, gfx: &Gfx, cam: &Cam, settings: &Settings, info: &[(String, Color)], now: f64) {
|
|
self.rects.clear();
|
|
let keys: Vec<String> = (0..TOOLS.len()).map(|i| key_name(settings.key(Action::Tool(i)))).collect();
|
|
let sw = sw();
|
|
let sh = sh();
|
|
let (mx, my) = mouse();
|
|
|
|
// ---------------- top bar: segments flow over up to three lines on narrow screens
|
|
let cols = (sw / CW()).floor() as i32;
|
|
enum Part {
|
|
T(String, Color),
|
|
Bar(f32, Color),
|
|
Gap,
|
|
}
|
|
let txt = |s: String, c: Color| Part::T(s, c);
|
|
let cap = g.creature_cap();
|
|
let wave_col = if g.wave_timer < 30.0 { RED } else { TXT };
|
|
let vol = if settings.muted { tr("hud.off") } else { format!("{}%", (settings.volume * 100.0).round() as i32) };
|
|
let mut gold_seg = vec![txt(tr("hud.gold"), GOLD), Part::Gap, txt(format!("{}/{}", g.gold as i32, g.gold_cap() as i32), TXT)];
|
|
let due = g.next_payday_cost().ceil() as i32;
|
|
if due > 0 {
|
|
// what the next payday costs, in red
|
|
gold_seg.push(Part::Gap);
|
|
gold_seg.push(txt(format!("-{}", due), RED));
|
|
}
|
|
let mut segs: Vec<Vec<Part>> = vec![
|
|
gold_seg,
|
|
vec![txt(tr("hud.wood"), WOOD_C), Part::Gap, txt((g.wood.floor() as i32).to_string(), TXT)],
|
|
vec![txt(tr("hud.mana"), MANA), Part::Gap, Part::Bar(g.mana / g.mana_max(), MANA), Part::Gap, txt(format!("{:<3}", g.mana as i32), TXT)],
|
|
vec![txt(tr("hud.core"), RED), Part::Gap, Part::Bar(g.core_hp / CORE_HP, RED)],
|
|
vec![txt(tr("hud.creatures"), TXT), Part::Gap, txt(format!("{}/{}", g.creature_count(), cap), TXT)],
|
|
vec![txt(tr("hud.workers"), TXT), Part::Gap, txt(g.worker_count().to_string(), TXT)],
|
|
];
|
|
// campaign missions: gate, waves and time limit only where the mission has them
|
|
if g.has_gate() {
|
|
segs.insert(3, vec![txt(tr("hud.gate"), BLUE), Part::Gap, Part::Bar(g.gate_hp / GATE_HP, BLUE)]);
|
|
}
|
|
// the soul cores of the rival masters
|
|
for rv in g.rivals.iter().filter(|r| r.alive) {
|
|
let c = crate::render3d::master_rgb(rv.side);
|
|
let col = Color::new(c.x, c.y, c.z, 1.0);
|
|
let key = match rv.side {
|
|
2 => "hud.rival1",
|
|
3 => "hud.rival2",
|
|
_ => "net.side4",
|
|
};
|
|
segs.insert(3, vec![txt(tr(key), col), Part::Gap, Part::Bar(rv.hp / crate::rival::RIVAL_CORE_HP, col)]);
|
|
}
|
|
// the weather above ground (maps with land above ground only)
|
|
if g.has_sky() {
|
|
let w = &g.weather;
|
|
let k = if w.level > 0.05 { w.shown } else { crate::weather::FAIR };
|
|
let col = [GOLD, Color::new(0.55, 0.7, 1.0, 1.0), Color::new(0.75, 0.78, 0.8, 1.0), Color::new(0.75, 0.6, 1.0, 1.0)][k as usize];
|
|
segs.push(vec![txt(tr(crate::weather::NAMES[k as usize]), col)]);
|
|
}
|
|
if g.waves_on() {
|
|
segs.push(vec![txt(trf("hud.wave", &[(g.wave + 1).to_string()]), wave_col), Part::Gap, txt(mmss(g.wave_timer), TXT)]);
|
|
}
|
|
if let Some(limit) = g.mission.as_ref().map(|m| m.time_limit).filter(|&l| l > 0.0) {
|
|
let left = (limit - g.time).max(0.0);
|
|
segs.insert(0, vec![txt(tr("hud.time_left"), if left < 120.0 { RED } else { GOLD }), Part::Gap, txt(mmss(left), TXT)]);
|
|
}
|
|
if let Some(k) = g.research_target().filter(|_| g.counts[4] > 0) {
|
|
let (tool, cost) = crate::research::RESEARCH[k];
|
|
let name: String = tool_info(TOOLS[tool]).name.chars().take(18).collect();
|
|
segs.push(vec![txt(tr("hud.research"), BLUE), Part::Gap, txt(name, TXT), Part::Gap, Part::Bar(g.research / cost, BLUE)]);
|
|
} else if let Some(u) = g.upgrade_target().filter(|_| g.counts[4] > 0) {
|
|
// everything known: the library works on room levels
|
|
let (r, lv, cost) = crate::research::UPGRADES[u];
|
|
let name: String = format!("{} {}", tr(ROOM_NAMES[r]), if lv == 2 { "II" } else { "III" }).chars().take(20).collect();
|
|
segs.push(vec![txt(tr("hud.research"), BLUE), Part::Gap, txt(name, TXT), Part::Gap, Part::Bar(g.research / cost, BLUE)]);
|
|
}
|
|
segs.push(vec![txt(tr("hud.payday"), if g.payday <= PAYDAY_WARN { RED } else { GOLD }), Part::Gap, txt(mmss(g.payday), TXT)]);
|
|
segs.push(vec![txt(tr("hud.army"), TXT), Part::Gap, txt((g.army_value() as i32).to_string(), TXT)]);
|
|
if !g.hand.is_empty() {
|
|
segs.push(vec![txt(tr("hud.hand"), FRAME_HI), Part::Gap, txt(format!("{}/8", g.hand.len()), TXT)]);
|
|
}
|
|
segs.push(vec![txt("\u{266a}".to_string(), DIM), Part::Gap, txt(vol, DIM)]);
|
|
let width = |seg: &Vec<Part>| -> usize {
|
|
seg.iter().map(|p| match p {
|
|
Part::T(s, _) => s.chars().count(),
|
|
Part::Bar(..) => 6,
|
|
Part::Gap => 1,
|
|
}).sum()
|
|
};
|
|
// place the segments: (line, column)
|
|
let mut placed: Vec<(usize, usize)> = Vec::new();
|
|
let (mut line, mut c) = (0usize, 2usize);
|
|
for seg in &segs {
|
|
let w = width(seg);
|
|
if c > 2 && c + w > cols as usize - 1 {
|
|
line += 1;
|
|
c = 2;
|
|
}
|
|
if line >= 3 || c + w > cols as usize - 1 {
|
|
break;
|
|
}
|
|
placed.push((line, c));
|
|
c += w + 2;
|
|
}
|
|
let lines = placed.iter().map(|p| p.0).max().unwrap_or(0) + 1;
|
|
let hud_rows = lines as i32 + 2;
|
|
self.hud_h = hud_rows as f32 * CH();
|
|
gfx.window(0.0, 0.0, cols, hud_rows, S(), false, FRAME, BG);
|
|
self.rects.push(Rect::new(0.0, 0.0, sw, self.hud_h));
|
|
for (seg, &(l, c0)) in segs.iter().zip(placed.iter()) {
|
|
let y = CH() * (1 + l) as f32;
|
|
let mut c = c0;
|
|
for p in seg {
|
|
match p {
|
|
Part::T(s, col) => {
|
|
gfx.text(s, c as f32 * CW(), y, S(), *col);
|
|
c += s.chars().count();
|
|
}
|
|
Part::Bar(f, col) => {
|
|
ascii_bar(gfx, c as f32 * CW(), y, 6, *f, *col, S());
|
|
c += 6;
|
|
}
|
|
Part::Gap => c += 1,
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------- a champion of the Order on the field: his name and health under the top bar
|
|
if let Some(b) = g.boss_alive() {
|
|
let name = crate::i18n::tr(crate::bosses::boss_name(b.boss).trim_start_matches('@'));
|
|
let bw = (name.chars().count() as i32 + 26).min(cols - 4);
|
|
let bx = ((cols - bw) / 2) as f32 * CW();
|
|
let by = self.hud_h;
|
|
gfx.window(bx, by, bw, 3, S(), false, RED, BG);
|
|
gfx.text(&name, bx + CW() * 2.0, by + CH(), S(), GOLD);
|
|
let bar_cols = (bw - name.chars().count() as i32 - 6).max(6);
|
|
ascii_bar(gfx, bx + CW() * (name.chars().count() as f32 + 4.0), by + CH(), bar_cols as usize, b.hp / b.max_hp, RED, S());
|
|
}
|
|
|
|
// ---------------- toolbar: buttons of 6 x 3 characters, icons and labels on the grid
|
|
let tb_rows = 5;
|
|
let tb_y = sh - tb_rows as f32 * CH();
|
|
let tb_y = (tb_y / CH()).floor() * CH();
|
|
let tb_h = sh - tb_y;
|
|
let tb_rows = (tb_h / CH()).floor() as i32;
|
|
gfx.window(0.0, tb_y, cols, tb_rows, S(), false, FRAME, BG);
|
|
self.rects.push(Rect::new(0.0, tb_y, sw, tb_h));
|
|
self.touch_btns.clear();
|
|
let mut hover_tool = None;
|
|
let px = K(); // one pixel of the font, times the factor
|
|
let pulse = (now * 2.5) as i64 % 2 == 0;
|
|
// one toolbar button: icon, optional key, label in small text
|
|
// locked tools (campaign: not yet available) are drawn dark
|
|
let tool_button = |bx: f32, by: f32, bw: f32, icon_id: u32, key: &str, label: &str, sel: bool, hovered: bool, locked: bool| {
|
|
let r = Rect::new(bx + px, by, bw - 2.0 * px, 3.0 * CH());
|
|
let fill = if sel { Color::new(0.42, 0.26, 0.1, 1.0) } else if hovered { Color::new(0.18, 0.13, 0.1, 1.0) } else { Color::new(0.1, 0.07, 0.07, 1.0) };
|
|
if crate::gfx::high_res_ui() {
|
|
// high-res: a slab of stone in the button's colour (the material is light grey)
|
|
let k = 1.6;
|
|
draw_texture_ex(&gfx.materials[1], r.x, r.y, Color::new(fill.r * k, fill.g * k, fill.b * k, 1.0), DrawTextureParams { dest_size: Some(vec2(r.w, r.h)), ..Default::default() });
|
|
} else {
|
|
draw_rectangle(r.x, r.y, r.w, r.h, fill);
|
|
}
|
|
// the active tool: pulsing frame
|
|
let frame = if sel { if pulse { WHITE } else { FRAME_HI } } else { Color::new(0.35, 0.28, 0.2, 1.0) };
|
|
draw_rectangle_lines(r.x, r.y, r.w, r.h, if sel { 3.0 * px } else { 2.0 * px }, frame);
|
|
// icon: the 16 px atlas cell at 2*F, centred on the grid
|
|
let icon = 32.0 * px;
|
|
gfx.draw_cell(icon_id, bx + (bw - icon) * 0.5, by + 4.0 * px, icon, icon, if locked { Color::new(0.25, 0.22, 0.2, 1.0) } else { WHITE });
|
|
if !key.is_empty() {
|
|
gfx.text(key, bx + 3.0 * px, by + 3.0 * px, SMALL(), if sel { FRAME_HI } else { DIM });
|
|
}
|
|
// label in small text, centred on whole small-character cells
|
|
let maxc = ((bw - 2.0 * px) / (GW * SMALL())).floor() as usize;
|
|
let label: String = label.chars().take(maxc.min(11)).collect();
|
|
let lw = gfx.measure(&label, SMALL());
|
|
let lx = bx + ((bw - lw) / (GW * SMALL()) / 2.0).floor() * GW * SMALL();
|
|
gfx.text(&label, lx, by + 2.0 * CH() + 4.0 * px, SMALL(), if sel { FRAME_HI } else if locked { DIM } else { TXT });
|
|
r
|
|
};
|
|
let mut popup_top = tb_y;
|
|
// categories + menu button; a category with several tools opens a row above
|
|
// (the same layout for mouse and touch; on the desktop the keys still select tools directly)
|
|
let n = CATS.len() + 3;
|
|
let bcols = (((cols - 2) as usize / n) as f32).clamp(4.0, 8.0);
|
|
let bw = bcols * CW();
|
|
let x0 = grid_center(sw, bw * n as f32);
|
|
let by = tb_y + CH();
|
|
let cur = TOOLS.iter().position(|&t| t == self.tool).unwrap_or(0);
|
|
for (c, (key, tools)) in CATS.iter().enumerate() {
|
|
let bx = x0 + c as f32 * bw;
|
|
let has = tools.contains(&cur);
|
|
let shown = if has { cur } else { tools[0] };
|
|
let ti = tool_info(TOOLS[shown]);
|
|
let label = if tools.len() == 1 { ti.label.clone() } else { tr(key) };
|
|
let hovered = !touch_ui() && Rect::new(bx + px, by, bw - 2.0 * px, 3.0 * CH()).contains(vec2(mx, my));
|
|
// single tools show their key (desktop) and their description when hovered
|
|
let key = if tools.len() == 1 && !touch_ui() { keys[tools[0]].as_str() } else { "" };
|
|
if hovered && tools.len() == 1 {
|
|
hover_tool = Some(tools[0]);
|
|
}
|
|
let locked = !tools.iter().any(|&t| g.tool_allowed(t));
|
|
let r = tool_button(bx, by, bw, ti.icon, key, &label, has, self.open_cat == Some(c) || hovered, locked);
|
|
if has {
|
|
gfx.glyph('▼', bx + (bw - CW()) * 0.5, tb_y, S(), if pulse { WHITE } else { FRAME_HI });
|
|
}
|
|
if tools.len() > 1 {
|
|
// small arrow: this button opens a row
|
|
gfx.glyph('▲', r.x + r.w - GW * SMALL() - 2.0 * px, r.y + 2.0 * px, SMALL(), if self.open_cat == Some(c) { FRAME_HI } else { DIM });
|
|
}
|
|
self.touch_btns.push((r, TBtn::Cat(c)));
|
|
}
|
|
// creature overview button
|
|
let bx = x0 + CATS.len() as f32 * bw;
|
|
let r = Rect::new(bx + px, by, bw - 2.0 * px, 3.0 * CH());
|
|
let hovered = !touch_ui() && r.contains(vec2(mx, my));
|
|
draw_rectangle(r.x, r.y, r.w, r.h, if hovered || self.show_overview { Color::new(0.18, 0.13, 0.1, 1.0) } else { Color::new(0.1, 0.07, 0.07, 1.0) });
|
|
draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0 * px, if self.show_overview { FRAME_HI } else { Color::new(0.35, 0.28, 0.2, 1.0) });
|
|
gfx.glyph('☻', bx + (bw - GW * TITLE()) * 0.5, by + 2.0 * px, TITLE(), FRAME_HI);
|
|
let ol = tr("cat.overview");
|
|
let maxc = ((bw - 2.0 * px) / (GW * SMALL())).floor() as usize;
|
|
let ol: String = ol.chars().take(maxc).collect();
|
|
gfx.text(&ol, bx + (bw - gfx.measure(&ol, SMALL())) * 0.5, by + 2.0 * CH() + 4.0 * px, SMALL(), TXT);
|
|
self.touch_btns.push((r, TBtn::Overview));
|
|
// the master's talents: the frame pulses while points are free
|
|
let bx = x0 + (CATS.len() + 1) as f32 * bw;
|
|
let r = Rect::new(bx + px, by, bw - 2.0 * px, 3.0 * CH());
|
|
let hovered = !touch_ui() && r.contains(vec2(mx, my));
|
|
let free = g.talents.free();
|
|
let glow = free > 0 && ((now * 3.0) as i64) % 2 == 0;
|
|
draw_rectangle(r.x, r.y, r.w, r.h, if hovered || self.show_talents { Color::new(0.18, 0.13, 0.1, 1.0) } else { Color::new(0.1, 0.07, 0.07, 1.0) });
|
|
draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0 * px, if glow { GOLD } else if self.show_talents { FRAME_HI } else { Color::new(0.35, 0.28, 0.2, 1.0) });
|
|
let isz = 2.0 * CH() - 2.0 * px;
|
|
draw_texture_ex(&gfx.atlas, bx + (bw - isz) * 0.5, by + 2.0 * px, WHITE, DrawTextureParams { source: Some(Gfx::cell(I_RUNE)), dest_size: Some(vec2(isz, isz)), ..Default::default() });
|
|
if free > 0 {
|
|
gfx.text(&free.to_string(), r.x + 3.0 * px, r.y + 3.0 * px, SMALL(), if glow { WHITE } else { GOLD });
|
|
}
|
|
let tl = tr("cat.master");
|
|
let tl: String = tl.chars().take(maxc).collect();
|
|
gfx.text(&tl, bx + (bw - gfx.measure(&tl, SMALL())) * 0.5, by + 2.0 * CH() + 4.0 * px, SMALL(), TXT);
|
|
self.touch_btns.push((r, TBtn::Talents));
|
|
// menu button
|
|
let bx = x0 + (CATS.len() + 2) as f32 * bw;
|
|
let r = Rect::new(bx + px, by, bw - 2.0 * px, 3.0 * CH());
|
|
let hovered = !touch_ui() && r.contains(vec2(mx, my));
|
|
draw_rectangle(r.x, r.y, r.w, r.h, if hovered { Color::new(0.18, 0.13, 0.1, 1.0) } else { Color::new(0.1, 0.07, 0.07, 1.0) });
|
|
draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0 * px, Color::new(0.35, 0.28, 0.2, 1.0));
|
|
gfx.glyph('≡', bx + (bw - GW * TITLE()) * 0.5, by + 2.0 * px, TITLE(), FRAME_HI);
|
|
let ml = tr("cat.menu");
|
|
let maxc = ((bw - 2.0 * px) / (GW * SMALL())).floor() as usize;
|
|
let ml: String = ml.chars().take(maxc).collect();
|
|
gfx.text(&ml, bx + (bw - gfx.measure(&ml, SMALL())) * 0.5, by + 2.0 * CH() + 4.0 * px, SMALL(), TXT);
|
|
self.touch_btns.push((r, TBtn::Menu));
|
|
// open category row: (category, x0, button width, columns)
|
|
let popup = self.open_cat.map(|c| (c, x0, bw, bcols));
|
|
// touch: after choosing a tool its description is shown for a moment
|
|
if let Some((i, t)) = self.tip.filter(|_| touch_ui()) {
|
|
if now - t < 3.5 && self.open_cat.is_none() {
|
|
hover_tool = Some(i);
|
|
} else if now - t >= 3.5 {
|
|
self.tip = None;
|
|
}
|
|
}
|
|
|
|
// ---------------- minimap: whole device pixels per tile
|
|
let mm_size = if touch_ui() { 10.0 } else { 14.0 };
|
|
let ms = ((mm_size * CW()) / mw().max(mh()) as f32).floor().max(1.0);
|
|
let (map_w, map_h) = (mw() as f32 * ms, mh() as f32 * ms);
|
|
let mm_cols = ((map_w / CW()).ceil() as i32 + 2).max(10);
|
|
let mm_rows = (map_h / CH()).ceil() as i32 + 2;
|
|
let mm_w = mm_cols as f32 * CW();
|
|
let mm_h = mm_rows as f32 * CH();
|
|
let mm_x = ((sw - mm_w) / CW()).floor() * CW();
|
|
let mm_y = tb_y - mm_h;
|
|
gfx.window_titled(mm_x, mm_y, mm_cols, mm_rows, S(), false, FRAME, BG, &tr("ui.map"), FRAME_HI);
|
|
self.rects.push(Rect::new(mm_x, mm_y, mm_w, mm_h));
|
|
// the minimap image follows the map size
|
|
if self.minimap_img.width as usize != mw() || self.minimap_img.height as usize != mh() {
|
|
self.minimap_img = Image::gen_image_color(mw() as u16, mh() as u16, BLACK);
|
|
self.minimap_tex = Texture2D::from_image(&self.minimap_img);
|
|
self.minimap_tex.set_filter(FilterMode::Nearest);
|
|
self.minimap_t = -1.0;
|
|
}
|
|
if now - self.minimap_t > 0.25 {
|
|
self.minimap_t = now;
|
|
for (i, t) in g.map.t.iter().enumerate() {
|
|
let (x, y) = xy(i);
|
|
let mut c = minimap_color(t);
|
|
if !g.map.shown(i) {
|
|
c = [0, 0, 0, 255];
|
|
} else if t.marked {
|
|
c = [255, 230, 90, 255];
|
|
}
|
|
self.minimap_img.set_pixel(x as u32, y as u32, Color::from_rgba(c[0], c[1], c[2], c[3]));
|
|
}
|
|
self.minimap_tex.update(&self.minimap_img);
|
|
}
|
|
let mrx = (mm_x + (mm_w - map_w) * 0.5).floor();
|
|
let mry = (mm_y + (mm_h - map_h) * 0.5).floor();
|
|
self.minimap_rect = Rect::new(mrx, mry, map_w, map_h);
|
|
draw_texture_ex(&self.minimap_tex, mrx, mry, WHITE, DrawTextureParams { dest_size: Some(vec2(map_w, map_h)), ..Default::default() });
|
|
let dot = (2.0 * px).max(ms);
|
|
for e in &g.ents {
|
|
if e.alive && !e.held && !crate::render3d::unit_hidden(g, e) {
|
|
let c = if e.hero {
|
|
Color::new(0.6, 0.85, 1.0, 1.0)
|
|
} else if e.dwarf() {
|
|
Color::new(0.85, 0.55, 0.25, 1.0)
|
|
} else if e.rival() {
|
|
let c = crate::render3d::master_rgb(e.side);
|
|
Color::new(c.x, c.y, c.z, 1.0)
|
|
} else if e.kind == EK::Worker { Color::new(1.0, 0.6, 0.3, 1.0) } else { Color::new(1.0, 0.25, 0.25, 1.0) };
|
|
draw_rectangle((mrx + e.disp_x.floor() * ms).floor(), (mry + e.disp_y.floor() * ms).floor(), dot, dot, c);
|
|
}
|
|
}
|
|
if let Some(f) = g.flag {
|
|
let (fx, fy) = xy(f);
|
|
draw_rectangle(mrx + fx as f32 * ms - px, mry + fy as f32 * ms - px, dot + 2.0 * px, dot + 2.0 * px, RED);
|
|
}
|
|
// expected entry points of the hero wave (blinking)
|
|
if (now * 3.0) as i64 % 2 == 0 {
|
|
for &(ex, ey) in &g.planned_entries {
|
|
let (ppx, ppy) = (mrx + ex as f32 * ms, mry + ey as f32 * ms);
|
|
draw_rectangle_lines(ppx - 4.0 * px, ppy - 4.0 * px, 9.0 * px, 9.0 * px, 2.0 * px, Color::new(0.6, 0.85, 1.0, 1.0));
|
|
}
|
|
}
|
|
// camera footprint on the ground (the 3D camera works in CSS pixels)
|
|
let (lw, lh) = (screen_width(), screen_height());
|
|
let corners = [cam.ground_point(0.0, 0.0), cam.ground_point(lw, 0.0), cam.ground_point(lw, lh), cam.ground_point(0.0, lh)];
|
|
// the edges are cut off at the map border (moving the corners would distort the shape)
|
|
for k in 0..4 {
|
|
if let Some((a, b)) = clip_segment(corners[k], corners[(k + 1) % 4], mw() as f32, mh() as f32) {
|
|
draw_line(mrx + a.x * ms, mry + a.y * ms, mrx + b.x * ms, mry + b.y * ms, px, WHITE);
|
|
}
|
|
}
|
|
// the camera: a monster's eye on the viewer's side of the footprint, looking in
|
|
{
|
|
let near = (corners[2] + corners[3]) * 0.5;
|
|
let look = vec2(-cam.yaw.sin(), -cam.yaw.cos()); // from the eye towards the target (map units)
|
|
let r = (4.0 * px).max(ms * 2.5);
|
|
let (ex, ey) = (mrx + near.x * ms - look.x * r * 1.6, mry + near.y * ms - look.y * r * 1.6);
|
|
let (ex, ey) = (ex.clamp(mrx + r, mrx + map_w - r), ey.clamp(mry + r, mry + map_h - r));
|
|
let side = vec2(-look.y, look.x);
|
|
// the lids: an almond across the view direction
|
|
let lid = |t: f32, w: f32| vec2(ex, ey) + side * (t * r * 1.25) + look * w;
|
|
let n = 8;
|
|
let mut upper = Vec::new();
|
|
let mut lower = Vec::new();
|
|
for k in 0..=n {
|
|
let t = k as f32 / n as f32 * 2.0 - 1.0;
|
|
let h = (1.0 - t * t).max(0.0).sqrt() * r * 0.75;
|
|
upper.push(lid(t, h));
|
|
lower.push(lid(t, -h));
|
|
}
|
|
let c = vec2(ex, ey);
|
|
let sclera = Color::new(0.95, 0.85, 0.35, 1.0);
|
|
for k in 0..n {
|
|
draw_triangle(c, upper[k], upper[k + 1], sclera);
|
|
draw_triangle(c, lower[k], lower[k + 1], sclera);
|
|
}
|
|
// the iris looks towards the target, with a slit pupil across it
|
|
let iris = c + look * (r * 0.2);
|
|
draw_circle(iris.x, iris.y, r * 0.55, Color::new(0.85, 0.3, 0.08, 1.0));
|
|
let a = iris + side * (r * 0.45);
|
|
let b = iris - side * (r * 0.45);
|
|
draw_line(a.x, a.y, b.x, b.y, (r * 0.22).max(px), Color::new(0.05, 0.0, 0.02, 1.0));
|
|
for k in 0..n {
|
|
draw_line(upper[k].x, upper[k].y, upper[k + 1].x, upper[k + 1].y, px.max(1.0), Color::new(0.2, 0.05, 0.08, 1.0));
|
|
draw_line(lower[k].x, lower[k].y, lower[k + 1].x, lower[k + 1].y, px.max(1.0), Color::new(0.2, 0.05, 0.08, 1.0));
|
|
}
|
|
}
|
|
|
|
// ---------------- mission goals (campaign)
|
|
if let Some(m) = &g.mission {
|
|
let gcols = 34.min(cols - 2);
|
|
let mut lines: Vec<(String, Color)> = Vec::new();
|
|
for &goal in &m.goals {
|
|
let done = g.goal_done(goal);
|
|
let progress = match goal {
|
|
Goal::Shards => format!(" {}/{}", g.shards_taken.min(m.shards), m.shards),
|
|
Goal::Heart => format!(" {}%", (g.gate_hp / GATE_HP * 100.0).ceil().max(0.0) as i32),
|
|
Goal::KillAll => trf("goal.left", &[g.ents.iter().filter(|e| e.alive && e.active_hero()).count().to_string()]),
|
|
Goal::Loot(n) => format!(" {}/{}", (g.loot as u32).min(n), n),
|
|
Goal::Towers => trf("goal.left", &[g.towers.iter().filter(|t| t.hero).count().to_string()]),
|
|
Goal::Survive(n) => format!(" {}/{}", g.wave.min(n), n),
|
|
Goal::Rivals => trf("goal.left", &[g.rivals.iter().filter(|r| r.alive).count().to_string()]),
|
|
Goal::Springs(n) => format!(" {}/{}", g.springs.min(n as usize), n),
|
|
Goal::Freed(n) => format!(" {}/{}", g.freed.min(n), n),
|
|
};
|
|
let text = format!("{} {}{}", if done { "\u{221a}" } else { "\u{25ba}" }, crate::menu::goal_text(goal), progress);
|
|
for l in wrap(&text, gcols as usize - 4) {
|
|
lines.push((l, if done { GREEN } else { TXT }));
|
|
}
|
|
}
|
|
let grows = lines.len() as i32 + 2;
|
|
gfx.window_titled(0.0, self.hud_h, gcols, grows, S(), false, FRAME, BG, &tr("camp.goals_panel"), GOLD);
|
|
let r = Rect::new(0.0, self.hud_h, gcols as f32 * CW(), grows as f32 * CH());
|
|
self.rects.push(r);
|
|
self.goals_rect = r;
|
|
for (k, (l, c)) in lines.iter().enumerate() {
|
|
gfx.text(l, CW() * 2.0, self.hud_h + CH() * (k as f32 + 1.0), S(), *c);
|
|
}
|
|
}
|
|
|
|
// ---------------- info panel
|
|
if !info.is_empty() {
|
|
let icols = 30.min(cols - 2);
|
|
let mut lines: Vec<(String, Color)> = Vec::new();
|
|
for (s, c) in info.iter().skip(1) {
|
|
for l in wrap(s, icols as usize - 4) {
|
|
lines.push((l, *c));
|
|
}
|
|
}
|
|
let irows = lines.len() as i32 + 2;
|
|
let ix = sw - icols as f32 * CW();
|
|
let iy = self.hud_h;
|
|
gfx.window_titled(ix, iy, icols, irows, S(), false, FRAME, BG, &info[0].0, info[0].1);
|
|
for (k, (l, c)) in lines.iter().enumerate() {
|
|
gfx.text(l, ix + CW() * 2.0, iy + CH() * (k as f32 + 1.0), S(), *c);
|
|
}
|
|
}
|
|
|
|
// ---------------- messages + chat (translated at display time, so a language switch applies at once)
|
|
let mut recent: Vec<(f64, String, Color)> = Vec::new();
|
|
for (m, t) in self.log.iter().filter(|(_, t)| now - t < 14.0) {
|
|
let a = (1.0 - ((now - t) as f32 - 11.0) / 3.0).clamp(0.2, 1.0);
|
|
recent.push((*t, trf(m.key, &m.args), Color::new(TXT.r, TXT.g, TXT.b, a)));
|
|
}
|
|
for (side, name, text, t) in self.chat_log.iter().filter(|(_, _, _, t)| now - t < 14.0) {
|
|
let a = (1.0 - ((now - t) as f32 - 11.0) / 3.0).clamp(0.2, 1.0);
|
|
recent.push((*t, format!("{name}: {text}"), chat_color(*side, a)));
|
|
}
|
|
recent.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
|
|
if !recent.is_empty() || self.chat_open {
|
|
let mcols = (((sw - mm_w) / CW()) as i32 - 2).clamp(34, 70);
|
|
let mut lines: Vec<(String, Color)> = Vec::new();
|
|
let keep = if touch_ui() { 3 } else { 5 };
|
|
for (_, line, col) in recent.iter().rev().take(keep).rev() {
|
|
for l in wrap(line, mcols as usize - 4) {
|
|
lines.push((l.clone(), *col));
|
|
}
|
|
}
|
|
let chat_rows = if self.chat_open { 2 } else { 0 };
|
|
let mrows = lines.len() as i32 + chat_rows + 2;
|
|
let my0 = tb_y - mrows as f32 * CH();
|
|
gfx.window(0.0, my0, mcols, mrows, S(), false, FRAME, Color::new(0.06, 0.04, 0.05, 0.82));
|
|
self.rects.push(Rect::new(0.0, my0, mcols as f32 * CW(), mrows as f32 * CH()));
|
|
for (k, (l, col)) in lines.iter().enumerate() {
|
|
gfx.text(l, CW() * 2.0, my0 + CH() * (k as f32 + 1.0), S(), *col);
|
|
}
|
|
if self.chat_open {
|
|
let cy = my0 + CH() * (lines.len() as f32 + 1.0);
|
|
let all = tr("chat.all");
|
|
let team = tr("chat.team");
|
|
let send = tr("chat.send");
|
|
let r0 = Rect::new(CW() * 2.0, cy, 10.0 * CW(), CH());
|
|
let r1 = Rect::new(CW() * 14.0, cy, 10.0 * CW(), CH());
|
|
let r2 = Rect::new(mcols as f32 * CW() - 8.0 * CW(), cy + CH(), 6.0 * CW(), CH());
|
|
self.chat_rects = [(r0, Some(false)), (r1, Some(true))];
|
|
self.chat_send_rect = r2;
|
|
self.rects.push(r0);
|
|
self.rects.push(r1);
|
|
self.rects.push(r2);
|
|
let all_line = format!("{} {all}", if self.chat_team { " " } else { "o" });
|
|
let team_line = format!("{} {team}", if self.chat_team { "o" } else { " " });
|
|
gfx.text(&all_line, r0.x + CW(), cy + 4.0 * px, SMALL(), if !self.chat_team { FRAME_HI } else { DIM });
|
|
gfx.text(&team_line, r1.x + CW(), cy + 4.0 * px, SMALL(), if self.chat_team { FRAME_HI } else { DIM });
|
|
let shown = format!("> {}_", self.chat_text);
|
|
let maxc = (((mcols as f32 * CW() - 4.0 * CW() - 8.0 * CW()) / (GW * SMALL())) as usize).max(8);
|
|
let clipped: String = shown.chars().take(maxc).collect();
|
|
gfx.text(&clipped, CW() * 2.0, cy + CH() + 4.0 * px, SMALL(), TXT);
|
|
gfx.text(&send, r2.x + CW() * 1.5, cy + CH() + 4.0 * px, SMALL(), TXT);
|
|
}
|
|
}
|
|
if self.log.len() > 100 {
|
|
self.log.drain(0..50);
|
|
}
|
|
if self.chat_log.len() > 100 {
|
|
self.chat_log.drain(0..50);
|
|
}
|
|
|
|
// the open row of tools (wraps into several rows if needed)
|
|
if let Some((c, x0, bw, bcols)) = popup {
|
|
let tools: Vec<usize> = CATS[c].1.iter().copied().filter(|&i| g.tool_visible(i)).collect();
|
|
let per_row = (((cols - 2) as f32 / bcols).floor() as usize).max(1).min(tools.len());
|
|
let nrows = (tools.len() + per_row - 1) / per_row;
|
|
let pw = per_row as f32 * bw + 2.0 * CW();
|
|
let ph = (nrows as f32 * 3.0 + 2.0) * CH();
|
|
let pcols = (pw / CW()) as i32;
|
|
let prows = (ph / CH()) as i32;
|
|
let px0 = ((x0 + c as f32 * bw + bw * 0.5 - pw * 0.5) / CW()).floor().clamp(0.0, ((sw - pw) / CW()).floor()) * CW();
|
|
let py0 = tb_y - ph;
|
|
gfx.window_titled(px0, py0, pcols, prows, S(), false, FRAME_HI, BG, &tr(CATS[c].0), FRAME_HI);
|
|
self.rects.push(Rect::new(px0, py0, pw, ph));
|
|
for (k, &i) in tools.iter().enumerate() {
|
|
let bx = px0 + CW() + (k % per_row) as f32 * bw;
|
|
let by = py0 + CH() + (k / per_row) as f32 * 3.0 * CH();
|
|
let ti = tool_info(TOOLS[i]);
|
|
let hovered = !touch_ui() && Rect::new(bx + px, by, bw - 2.0 * px, 3.0 * CH()).contains(vec2(mx, my));
|
|
if hovered {
|
|
hover_tool = Some(i);
|
|
}
|
|
let key = if touch_ui() { "" } else { keys[i].as_str() };
|
|
let r = tool_button(bx, by, bw, ti.icon, key, &ti.label, self.tool == TOOLS[i], hovered, !g.tool_allowed(i) || (matches!(TOOLS[i], Tool::Siege(_)) && g.forge_tiles() == 0));
|
|
self.touch_btns.push((r, TBtn::Tool(i)));
|
|
}
|
|
popup_top = py0;
|
|
}
|
|
|
|
// ---------------- creature overview
|
|
if self.show_overview {
|
|
self.draw_overview(g, gfx, tb_y);
|
|
}
|
|
if self.show_talents {
|
|
self.draw_talents(g, gfx, tb_y);
|
|
}
|
|
|
|
// ---------------- Tooltip
|
|
if let Some(i) = hover_tool {
|
|
let t = TOOLS[i];
|
|
let ti = tool_info(t);
|
|
let tcols = 34.min(cols as usize);
|
|
let mut lines: Vec<(String, Color)> = wrap(&ti.desc, tcols - 4).into_iter().map(|l| (l, TXT)).collect();
|
|
// every line wraps; the window grows with the lines
|
|
let cost = tool_cost(t, g);
|
|
if !cost.is_empty() {
|
|
lines.extend(wrap(&trf("ui.cost", &[cost]), tcols - 4).into_iter().map(|l| (l, GOLD)));
|
|
}
|
|
if !touch_ui() {
|
|
lines.extend(wrap(&trf("ui.key", &[keys[i].clone()]), tcols - 4).into_iter().map(|l| (l, DIM)));
|
|
}
|
|
let rows = lines.len() as i32 + 2;
|
|
let anchor = if touch_ui() { sw * 0.5 } else { mx };
|
|
let tx = (((anchor - tcols as f32 * CW() * 0.5) / CW()).floor() * CW()).clamp(0.0, (sw - tcols as f32 * CW()).max(0.0));
|
|
let ty = popup_top - rows as f32 * CH();
|
|
gfx.window_titled(tx, ty, tcols as i32, rows, S(), false, FRAME_HI, BG, &ti.name, FRAME_HI);
|
|
for (k, (l, c)) in lines.iter().enumerate() {
|
|
gfx.text(l, tx + CW() * 2.0, ty + CH() * (k as f32 + 1.0), S(), *c);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// all creatures, grouped by kind: level, mood, wage, job, health; a click jumps there
|
|
// the master's talents: four branches of five levels, points from experience
|
|
fn draw_talents(&mut self, g: &Game, gfx: &Gfx, bottom: f32) {
|
|
use crate::talents::*;
|
|
let (mx, my) = mouse();
|
|
let t = &g.talents;
|
|
let cols = ((sw() / CW()) as i32 - 2).min(48);
|
|
let inner = (cols - 4) as usize;
|
|
// the lines of each branch: name and level, what it gives now (wrapped)
|
|
let rows_of = |b: usize| wrap(&branch_text(t, b), inner.saturating_sub(6)).len() as i32 + 2;
|
|
let rows = 5 + (0..BRANCHES).map(rows_of).sum::<i32>();
|
|
let x = grid_center(sw(), cols as f32 * CW()).max(0.0);
|
|
let y = (bottom - rows as f32 * CH() - CH()).max(CH() * 4.0);
|
|
gfx.window_titled(x, y, cols, rows, S(), false, FRAME_HI, BG, &tr("talent.title"), FRAME_HI);
|
|
self.rects.push(Rect::new(x, y, cols as f32 * CW(), rows as f32 * CH()));
|
|
let ix = x + CW() * 2.0;
|
|
let mut yy = y + CH();
|
|
// points and the way to the next one
|
|
gfx.text(&trf("talent.points", &[t.free().to_string()]), ix, yy, S(), if t.free() > 0 { GOLD } else { TXT });
|
|
yy += CH() * 1.2;
|
|
match t.progress() {
|
|
Some((have, need)) => {
|
|
ascii_bar(gfx, ix, yy, 16, (have / need).clamp(0.0, 1.0), MANA, SMALL());
|
|
gfx.text(&trf("talent.next", &[(have as i32).to_string(), (need as i32).to_string()]), ix + CW() * 17.0, yy, SMALL(), DIM);
|
|
}
|
|
None => gfx.text(&tr("talent.all"), ix, yy, SMALL(), DIM),
|
|
}
|
|
yy += CH() * 1.8;
|
|
for b in 0..BRANCHES {
|
|
let lv = t.lv[b];
|
|
let pips: String = (0..MAX_LEVEL).map(|k| if k < lv { '■' } else { '·' }).collect();
|
|
gfx.text(&format!("{} {}", tr(NAMES[b]), pips), ix, yy, S(), FRAME_HI);
|
|
// a button to learn the next level
|
|
if t.can_learn(b) {
|
|
let r = Rect::new(x + (cols - 7) as f32 * CW(), yy - CH() * 0.2, CW() * 4.0, CH() * 1.6);
|
|
let hov = r.contains(vec2(mx, my));
|
|
draw_rectangle(r.x, r.y, r.w, r.h, if hov { Color::new(0.35, 0.22, 0.1, 1.0) } else { Color::new(0.15, 0.1, 0.08, 1.0) });
|
|
draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0, GOLD);
|
|
gfx.text("+", r.x + (r.w - gfx.measure("+", S())) * 0.5, r.y + (r.h - GH * S()) * 0.5, S(), GOLD);
|
|
self.touch_btns.push((r, TBtn::Learn(b as u8)));
|
|
}
|
|
yy += CH() * 1.2;
|
|
for l in wrap(&branch_text(t, b), inner.saturating_sub(6)) {
|
|
gfx.text(&l, ix + CW() * 2.0, yy, SMALL(), DIM);
|
|
yy += CH();
|
|
}
|
|
yy += CH() * 0.8;
|
|
}
|
|
}
|
|
|
|
// the hall of the fallen of this game: name, kind, level, heroes slain, how it fell
|
|
fn draw_hall(&mut self, g: &Game, gfx: &Gfx, x: f32, y: f32, cols: i32, rows: i32) {
|
|
gfx.text(&tr("hall.title"), x + CW() * 2.0, y + CH(), S(), GOLD);
|
|
let mut lines: Vec<(String, Color)> = Vec::new();
|
|
for f in g.hall.iter().rev() {
|
|
lines.push((format!("{:>2} {}", f.level, crate::names::full_name(f.kind, f.name, f.level)), FRAME_HI));
|
|
let kills = if f.kills > 0 { format!(" · {}", trf("hall.kills", &[f.kills.to_string()])) } else { String::new() };
|
|
lines.push((format!(" {} · {}{} · {}", tr(st(f.kind).name), crate::names::cause(f.bane), kills, mmss(f.time)), DIM));
|
|
}
|
|
if lines.is_empty() {
|
|
lines.push((tr("hall.empty"), DIM));
|
|
}
|
|
let first = y + CH() * 2.6;
|
|
let visible = (rows - 4).max(1) as usize;
|
|
self.ov_scroll = self.ov_scroll.min(lines.len().saturating_sub(visible));
|
|
let maxc = (cols - 4) as usize;
|
|
for (n, (text, col)) in lines.iter().enumerate().skip(self.ov_scroll).take(visible) {
|
|
let t: String = text.chars().take(maxc).collect();
|
|
gfx.text(&t, x + CW() * 2.0, first + (n - self.ov_scroll) as f32 * CH(), S(), *col);
|
|
}
|
|
if lines.len() > visible {
|
|
let up = Rect::new(x + (cols - 4) as f32 * CW(), y + CH() * 2.4, CW() * 2.0, CH() * 1.5);
|
|
let down = Rect::new(x + (cols - 4) as f32 * CW(), y + (rows - 2) as f32 * CH(), CW() * 2.0, CH() * 1.5);
|
|
gfx.text("\u{25b2}", up.x + CW() * 0.5, up.y, S(), FRAME_HI);
|
|
gfx.text("\u{25bc}", down.x + CW() * 0.5, down.y, S(), FRAME_HI);
|
|
self.touch_btns.push((up, TBtn::OvScroll(-2)));
|
|
self.touch_btns.push((down, TBtn::OvScroll(2)));
|
|
}
|
|
}
|
|
|
|
fn draw_overview(&mut self, g: &Game, gfx: &Gfx, bottom: f32) {
|
|
let (sw, _) = (sw(), sh());
|
|
let (mx, my) = mouse();
|
|
let cols = ((sw / CW()) as i32 - 2).min(56);
|
|
let top = CH() * 4.0;
|
|
let rows = (((bottom - top) / CH()) as i32 - 1).clamp(8, 40);
|
|
let x = (sw - cols as f32 * CW() - CW()).max(0.0);
|
|
let y = (bottom - rows as f32 * CH()).max(top);
|
|
gfx.window_titled(x, y, cols, rows, S(), false, FRAME_HI, BG, &tr("ov.title"), FRAME_HI);
|
|
self.rects.push(Rect::new(x, y, cols as f32 * CW(), rows as f32 * CH()));
|
|
self.ov_rect = Some(Rect::new(x, y, cols as f32 * CW(), rows as f32 * CH()));
|
|
let mine: Vec<&Ent> = g.ents.iter().filter(|e| e.alive && e.creature()).collect();
|
|
let wage: f32 = mine.iter().map(|e| st(e.kind).wage as f32 * (1.0 + 0.1 * (e.level - 1) as f32)).sum();
|
|
let mood = if mine.is_empty() { 0.0 } else { mine.iter().map(|e| e.happy.clamp(0.0, 1.0)).sum::<f32>() / mine.len() as f32 };
|
|
// the switch to the hall of the fallen (top right)
|
|
{
|
|
let label = if self.ov_fallen { tr("ov.living") } else { trf("ov.fallen", &[g.hall.len().to_string()]) };
|
|
let bw = gfx.measure(&label, SMALL()) + CW() * 2.0;
|
|
let r = Rect::new(x + (cols - 3) as f32 * CW() - bw, y + CH() * 0.8, bw, CH() * 1.4);
|
|
let hov = r.contains(vec2(mx, my));
|
|
draw_rectangle(r.x, r.y, r.w, r.h, if hov { Color::new(0.35, 0.22, 0.1, 1.0) } else { Color::new(0.15, 0.1, 0.08, 1.0) });
|
|
draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0, FRAME_HI);
|
|
gfx.text(&label, r.x + CW(), r.y + (r.h - GH * SMALL()) * 0.5, SMALL(), FRAME_HI);
|
|
self.touch_btns.push((r, TBtn::OvFallen));
|
|
}
|
|
if self.ov_fallen {
|
|
self.draw_hall(g, gfx, x, y, cols, rows);
|
|
return;
|
|
}
|
|
let head = trf("ov.summary", &[mine.len().to_string(), (wage as i32).to_string(), ((mood * 100.0) as i32).to_string()]);
|
|
gfx.text(&head, x + CW() * 2.0, y + CH(), S(), GOLD);
|
|
// a match with allies: a row to send them gold (the name switches between the allies)
|
|
let allies = g.gold_allies();
|
|
let gold_row = !allies.is_empty();
|
|
if gold_row {
|
|
let to = allies[self.gold_to % allies.len()];
|
|
let gy = y + CH() * 2.2;
|
|
let name = trf("gold.send_to", &[tr(&format!("net.side{to}"))]);
|
|
let mut bx = x + CW() * 2.0;
|
|
let r = Rect::new(bx, gy, gfx.measure(&name, SMALL()) + CW() * 2.0, CH() * 1.6);
|
|
let hov = r.contains(vec2(mx, my));
|
|
draw_rectangle(r.x, r.y, r.w, r.h, if hov { Color::new(0.35, 0.22, 0.1, 1.0) } else { Color::new(0.15, 0.1, 0.08, 1.0) });
|
|
draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0, side_color(Some(to)));
|
|
gfx.text(&name, r.x + CW(), r.y + (r.h - GH * SMALL()) * 0.5, SMALL(), side_color(Some(to)));
|
|
self.touch_btns.push((r, TBtn::GoldTo));
|
|
bx += r.w + CW();
|
|
for amount in crate::netplay::GOLD_AMOUNTS {
|
|
let label = if amount == crate::netplay::GOLD_ALL { tr("gold.all") } else { amount.to_string() };
|
|
let r = Rect::new(bx, gy, gfx.measure(&label, SMALL()) + CW() * 2.0, CH() * 1.6);
|
|
let hov = r.contains(vec2(mx, my));
|
|
draw_rectangle(r.x, r.y, r.w, r.h, if hov { Color::new(0.35, 0.22, 0.1, 1.0) } else { Color::new(0.15, 0.1, 0.08, 1.0) });
|
|
draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0, GOLD);
|
|
gfx.text(&label, r.x + CW(), r.y + (r.h - GH * SMALL()) * 0.5, SMALL(), GOLD);
|
|
self.touch_btns.push((r, TBtn::Gold(amount)));
|
|
bx += r.w + CW() * 0.5;
|
|
}
|
|
}
|
|
// lines: kind headers and creatures
|
|
let mut kinds: Vec<EK> = Vec::new();
|
|
for e in &mine {
|
|
if !kinds.contains(&e.kind) {
|
|
kinds.push(e.kind);
|
|
}
|
|
}
|
|
kinds.sort_by_key(|k| std::cmp::Reverse(mine.iter().filter(|e| e.kind == *k).count()));
|
|
let mut lines: Vec<(String, Color, Option<u32>, f32)> = Vec::new();
|
|
for k in kinds {
|
|
let group: Vec<&&Ent> = mine.iter().filter(|e| e.kind == k).collect();
|
|
lines.push((format!("{} x{}", tr(st(k).name), group.len()), FRAME_HI, None, -1.0));
|
|
let mut group = group;
|
|
group.sort_by_key(|e| std::cmp::Reverse(e.level));
|
|
for e in group {
|
|
let w = (st(e.kind).wage as f32 * (1.0 + 0.1 * (e.level - 1) as f32)) as i32;
|
|
let happy = (e.happy.clamp(0.0, 1.0) * 100.0) as i32;
|
|
let job: String = tr(job_name(e)).chars().take(12).collect();
|
|
let name: String = crate::names::display_name(e).chars().take(18).collect();
|
|
let line = format!(" {:>2} {:<18} {:>3}% {:>4}g {:<12}", e.level, name, happy, w, job);
|
|
let col = if e.happy > 0.6 { TXT } else if e.happy > 0.25 { GOLD } else { RED };
|
|
lines.push((line, col, Some(e.id), e.hp / e.max_hp));
|
|
}
|
|
}
|
|
let first = y + CH() * if gold_row { 4.2 } else { 3.0 };
|
|
let visible = (rows - if gold_row { 7 } else { 5 }).max(1) as usize;
|
|
self.ov_scroll = self.ov_scroll.min(lines.len().saturating_sub(visible));
|
|
let lw = (cols - 4) as f32 * CW();
|
|
for (n, (text, col, id, hp)) in lines.iter().enumerate().skip(self.ov_scroll).take(visible) {
|
|
let ly = first + (n - self.ov_scroll) as f32 * CH();
|
|
let r = Rect::new(x + CW() * 2.0, ly, lw, CH());
|
|
if id.is_some() && r.contains(vec2(mx, my)) {
|
|
draw_rectangle(r.x, r.y, r.w, r.h, Color::new(0.25, 0.16, 0.08, 1.0));
|
|
}
|
|
gfx.text(text, r.x, ly, S(), *col);
|
|
if let Some(id) = id {
|
|
ascii_bar(gfx, r.x + r.w - CW() * 4.0, ly + (CH() - GH * SMALL()) * 0.5, 6, *hp, if *hp > 0.5 { GREEN } else { RED }, SMALL());
|
|
self.touch_btns.push((r, TBtn::OvRow(*id)));
|
|
}
|
|
}
|
|
if lines.len() > visible {
|
|
let up = Rect::new(x + (cols - 4) as f32 * CW(), y + CH(), CW() * 2.0, CH() * 1.5);
|
|
let down = Rect::new(x + (cols - 4) as f32 * CW(), y + (rows - 2) as f32 * CH(), CW() * 2.0, CH() * 1.5);
|
|
gfx.text("\u{25b2}", up.x + CW() * 0.5, up.y, S(), FRAME_HI);
|
|
gfx.text("\u{25bc}", down.x + CW() * 0.5, down.y, S(), FRAME_HI);
|
|
self.touch_btns.push((up, TBtn::OvScroll(-(visible as i32) / 2)));
|
|
self.touch_btns.push((down, TBtn::OvScroll(visible as i32 / 2)));
|
|
}
|
|
if mine.is_empty() {
|
|
gfx.text(&tr("ov.none"), x + CW() * 2.0, first, S(), DIM);
|
|
}
|
|
let hint = tr("ov.hint");
|
|
gfx.text(&hint, x + CW() * 2.0, y + (rows - 2) as f32 * CH(), SMALL(), DIM);
|
|
}
|
|
|
|
// Dialogs on top of everything (help, language selection, end screen, pause)
|
|
pub fn draw_dialogs(&mut self, g: &Game, gfx: &Gfx) {
|
|
let sw = sw();
|
|
let sh = sh();
|
|
if g.spectating() && g.state == 0 && !self.show_langsel {
|
|
// the heart fell: the player watches the rest of the match
|
|
let t = tr("net.spectating");
|
|
let w = ((gfx.measure(&t, S()) / CW()).ceil() as i32 + 4).min((sw / CW()) as i32);
|
|
let x = grid_center(sw, w as f32 * CW());
|
|
let y = self.hud_h + CH() * 0.5;
|
|
gfx.window(x, y, w, 3, S(), false, RED, BG);
|
|
gfx.text(&t, grid_center(sw, gfx.measure(&t, S())), y + CH(), S(), RED);
|
|
}
|
|
if self.show_langsel {
|
|
self.draw_langsel(gfx);
|
|
} else if g.state != 0 {
|
|
self.draw_end(g, gfx);
|
|
} else if self.show_help {
|
|
self.draw_help(gfx);
|
|
} else if g.paused {
|
|
let w = 24;
|
|
let x = grid_center(sw, w as f32 * CW());
|
|
let y = sh * 0.35;
|
|
gfx.window(x, y, w, 5, S(), true, FRAME_HI, BG);
|
|
let t = tr("ui.pause");
|
|
gfx.text(&t, grid_center(sw, gfx.measure(&t, TITLE())), y + CH(), TITLE(), FRAME_HI);
|
|
let t2 = tr("ui.pause_hint");
|
|
gfx.text(&t2, grid_center(sw, gfx.measure(&t2, S())), y + CH() * 3.0, S(), DIM);
|
|
}
|
|
}
|
|
|
|
fn draw_help(&mut self, gfx: &Gfx) {
|
|
let controls: &[&str] = if touch_ui() {
|
|
&["help.t_tap", "help.t_drag", "help.t_two", "help.t_long", "help.t_double", "help.t_tools"]
|
|
} else {
|
|
&["help.k_left", "help.k_right", "help.k_slap", "help.k_pan", "help.k_rot", "help.k_reset", "help.k_zoom", "help.k_tools", "help.k_misc", "help.k_misc2"]
|
|
};
|
|
let mut keys: Vec<(&str, Color)> = vec![("help.goal", GOLD), ("", TXT)];
|
|
keys.extend(controls.iter().map(|k| (*k, TXT)));
|
|
keys.extend_from_slice(&[
|
|
("", TXT),
|
|
("help.s1", TXT),
|
|
("help.s2", TXT),
|
|
("help.s3", TXT),
|
|
("help.s4", TXT),
|
|
("help.s5", TXT),
|
|
("", TXT),
|
|
("help.font", DIM),
|
|
("", TXT),
|
|
("help.start", FRAME_HI),
|
|
]);
|
|
let cols = 74.min((sw() / CW()) as i32);
|
|
let mut lines: Vec<(String, Color)> = Vec::new();
|
|
for (k, c) in &keys {
|
|
if k.is_empty() {
|
|
lines.push((String::new(), *c));
|
|
} else {
|
|
let key = if *k == "help.start" && touch_ui() { "help.start_touch" } else { k };
|
|
for l in wrap(&tr(key), cols as usize - 6) {
|
|
lines.push((l, *c));
|
|
}
|
|
}
|
|
}
|
|
let rows = lines.len() as i32 + 4;
|
|
let sw = sw();
|
|
let sh = sh();
|
|
let x = (grid_center(sw, cols as f32 * CW())).max(0.0);
|
|
let y = (grid_center_y(sh, rows as f32 * CH())).max(0.0);
|
|
gfx.window_titled(x, y, cols, rows, S(), true, FRAME_HI, Color::new(0.05, 0.03, 0.04, 0.97), "DungeonCraft", FRAME_HI);
|
|
for (k, (l, c)) in lines.iter().enumerate() {
|
|
gfx.text(l, x + CW() * 3.0, y + CH() * (k as f32 + 2.0), S(), *c);
|
|
}
|
|
self.rects.push(Rect::new(0.0, 0.0, sw, sh));
|
|
}
|
|
|
|
// First start: choose the language (names shown in their own language)
|
|
// first start: every language as a button with its flag, in a grid
|
|
fn draw_langsel(&mut self, gfx: &Gfx) {
|
|
use crate::menu::flags::{draw_flag, FLAG_H, FLAG_W};
|
|
let sw = sw();
|
|
let sh = sh();
|
|
draw_rectangle(0.0, 0.0, sw, sh, Color::new(0.0, 0.0, 0.0, 0.6));
|
|
let langs = languages();
|
|
let bw_c = 22; // button width in characters
|
|
let grid = (((sw / CW()) as i32 - 6) / (bw_c + 2)).clamp(1, 3);
|
|
let lines = (langs.len() as i32 + grid - 1) / grid;
|
|
let cols = (grid * (bw_c + 2) + 4).min((sw / CW()) as i32);
|
|
let rows = (lines * 3 + 6).min((sh / CH()) as i32);
|
|
let x = (grid_center(sw, cols as f32 * CW())).max(0.0);
|
|
let y = (grid_center_y(sh, rows as f32 * CH())).max(0.0);
|
|
// no language is chosen yet: the title in English, the hint in English and German
|
|
let title = crate::i18n::tr_in("en", "langsel.title");
|
|
gfx.window_titled(x, y, cols, rows, S(), true, FRAME_HI, Color::new(0.05, 0.03, 0.04, 0.97), &title, FRAME_HI);
|
|
self.lang_rects.clear();
|
|
let (mx, my) = mouse();
|
|
// more lines than fit: the grid scrolls (mouse wheel, swipe)
|
|
let fit = ((rows - 6) / 3).max(1);
|
|
self.langsel_scroll = self.langsel_scroll.min((lines - fit).max(0) as usize);
|
|
let bw = bw_c as f32 * CW();
|
|
let gx = x + (cols as f32 * CW() - grid as f32 * (bw + 2.0 * CW()) + 2.0 * CW()) * 0.5;
|
|
for (k, (code, name)) in langs.iter().enumerate() {
|
|
let line = k as i32 / grid - self.langsel_scroll as i32;
|
|
if line < 0 || line >= fit {
|
|
continue;
|
|
}
|
|
let bx = gx + (k as i32 % grid) as f32 * (bw + 2.0 * CW());
|
|
let by = y + CH() * (2.0 + line as f32 * 3.0);
|
|
let r = Rect::new(bx, by, bw, CH() * 2.0);
|
|
let hov = r.contains(vec2(mx, my));
|
|
draw_rectangle(r.x, r.y, r.w, r.h, if hov { Color::new(0.35, 0.22, 0.1, 1.0) } else { Color::new(0.15, 0.1, 0.08, 1.0) });
|
|
draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0, if hov { FRAME_HI } else { FRAME });
|
|
let px = S();
|
|
draw_flag(code, (bx + CW() * 0.75).floor(), (by + (r.h - FLAG_H as f32 * px) * 0.5).floor(), px);
|
|
let tx = bx + CW() * 0.75 + FLAG_W as f32 * px + CW();
|
|
let name: String = name.chars().take(((bx + bw - tx) / CW()).max(1.0) as usize - 1).collect();
|
|
gfx.text(&name, tx, by + CH() * 0.5, S(), TXT);
|
|
self.lang_rects.push((r, code.clone()));
|
|
}
|
|
let hint_y = y + CH() * (2.0 + fit as f32 * 3.0 + 0.5);
|
|
for (k, c) in ["en", "de"].iter().enumerate() {
|
|
let h = crate::i18n::tr_in(c, "langsel.hint");
|
|
let h: String = h.chars().take((cols as usize).saturating_sub(4) * 2).collect();
|
|
gfx.text(&h, x + CW() * 2.0, hint_y + CH() * k as f32, SMALL(), DIM);
|
|
}
|
|
self.rects.push(Rect::new(0.0, 0.0, sw, sh));
|
|
}
|
|
|
|
// mouse wheel / swipe over the creature overview: scrolls it; true if it was there
|
|
pub fn overview_wheel(&mut self, x: f32, y: f32, d: i32) -> bool {
|
|
if !self.show_overview || !self.ov_rect.map_or(false, |r| r.contains(vec2(x, y))) {
|
|
return false;
|
|
}
|
|
self.ov_scroll = (self.ov_scroll as i32 + d).max(0) as usize;
|
|
true
|
|
}
|
|
|
|
// mouse wheel / swipe on the first-start language grid
|
|
pub fn langsel_wheel(&mut self, d: i32) {
|
|
self.langsel_scroll = (self.langsel_scroll as i32 + d).max(0) as usize;
|
|
}
|
|
|
|
fn draw_end(&mut self, g: &Game, gfx: &Gfx) {
|
|
let sw = sw();
|
|
let sh = sh();
|
|
draw_rectangle(0.0, 0.0, sw, sh, Color::new(0.0, 0.0, 0.0, 0.5));
|
|
let win = g.state == 1;
|
|
let mission = g.mission.as_ref().map(|m| m.index);
|
|
let last = mission.map_or(false, |i| i + 1 == crate::campaign::missions().len());
|
|
let cols = 50.min((sw / CW()) as i32);
|
|
let tw = (cols - 6) as usize;
|
|
let (title, sub) = match (mission, win) {
|
|
(Some(_), true) => ("end.mission_won", if last { "camp.epilogue" } else { "end.mission_won_sub" }),
|
|
(Some(_), false) => ("end.mission_lost", "end.mission_lost_sub"),
|
|
(None, true) => ("end.victory", "end.victory_sub"),
|
|
(None, false) => ("end.defeat", "end.defeat_sub"),
|
|
};
|
|
let mut lines: Vec<String> = Vec::new();
|
|
for (k, para) in tr(sub).split(" | ").enumerate() {
|
|
if k > 0 {
|
|
lines.push(String::new());
|
|
}
|
|
lines.extend(wrap(para, tw));
|
|
}
|
|
// the chart of the game (gold, creatures, army) when there are a few samples
|
|
let chart = g.stats.samples.len() >= 3 && sh > CH() * (22.0 + lines.len() as f32);
|
|
let chart_rows = if chart { 10 } else { 0 };
|
|
let rows = 9 + lines.len() as i32 + chart_rows;
|
|
let x = grid_center(sw, cols as f32 * CW());
|
|
let y = grid_center_y(sh, rows as f32 * CH()).max(0.0);
|
|
let col = if win { GOLD } else { RED };
|
|
gfx.window(x, y, cols, rows, S(), true, col, Color::new(0.05, 0.03, 0.04, 0.97));
|
|
let title = tr(title);
|
|
let big = if gfx.measure(&title, TITLE()) < cols as f32 * CW() - CW() * 4.0 { TITLE() } else { S() };
|
|
gfx.text(&title, grid_center(sw, gfx.measure(&title, big)), y + CH(), big, col);
|
|
for (k, l) in lines.iter().enumerate() {
|
|
gfx.text(l, x + CW() * 3.0, y + CH() * (4.0 + k as f32), S(), TXT);
|
|
}
|
|
let st = trf("end.stats", &[mmss(g.time), g.wave.to_string(), (g.army_value() as i32).to_string()]);
|
|
gfx.text(&st, grid_center(sw, gfx.measure(&st, S())), y + CH() * (4.5 + lines.len() as f32), S(), DIM);
|
|
if chart {
|
|
use crate::stats as k;
|
|
let cy = y + CH() * (6.0 + lines.len() as f32);
|
|
let c = &g.stats;
|
|
let line2 = trf("end.stats2", &[(c.get(k::HEROES) as i32).to_string(), (c.get(k::LOST) as i32).to_string(), (c.get(k::DUG) as i32).to_string(), (c.get(k::GOLD) as i32).to_string(), (c.get(k::WOOD) as i32).to_string()]);
|
|
gfx.text(&line2, grid_center(sw, gfx.measure(&line2, SMALL())), cy, SMALL(), DIM);
|
|
// three lines, each scaled to its own maximum
|
|
let (px, py, pw, ph) = (x + CW() * 3.0, cy + CH() * 1.2, (cols - 6) as f32 * CW(), CH() * 6.0);
|
|
draw_rectangle(px, py, pw, ph, Color::new(0.0, 0.0, 0.0, 0.35));
|
|
draw_rectangle_lines(px, py, pw, ph, 1.0, Color::new(0.4, 0.3, 0.2, 1.0));
|
|
let n = c.samples.len();
|
|
let t_end = c.samples[n - 1].0.max(1.0);
|
|
let series: [(Box<dyn Fn(&(f32, f32, u16, f32)) -> f32>, Color); 3] = [(Box::new(|s| s.1), GOLD), (Box::new(|s| s.2 as f32), GREEN), (Box::new(|s| s.3), RED)];
|
|
for (f, col) in series.iter() {
|
|
let max = c.samples.iter().map(|s| f(s)).fold(1.0f32, f32::max);
|
|
for w in c.samples.windows(2) {
|
|
let (a, b) = (&w[0], &w[1]);
|
|
let p = |s: &(f32, f32, u16, f32)| (px + pw * (s.0 / t_end), py + ph - ph * 0.92 * (f(s) / max));
|
|
let (x0, y0) = p(a);
|
|
let (x1, y1) = p(b);
|
|
draw_line(x0, y0, x1, y1, 2.0 * K(), *col);
|
|
}
|
|
}
|
|
let legend = format!("{} {} {}", tr("hud.gold"), tr("hud.creatures"), tr("hud.army"));
|
|
let lx = px + CW();
|
|
gfx.text(&tr("hud.gold"), lx, py + ph + CH() * 0.3, SMALL(), GOLD);
|
|
let w1 = gfx.measure(&format!("{} ", tr("hud.gold")), SMALL());
|
|
gfx.text(&tr("hud.creatures"), lx + w1, py + ph + CH() * 0.3, SMALL(), GREEN);
|
|
let w2 = gfx.measure(&format!("{} {} ", tr("hud.gold"), tr("hud.creatures")), SMALL());
|
|
gfx.text(&tr("hud.army"), lx + w2, py + ph + CH() * 0.3, SMALL(), RED);
|
|
let _ = legend;
|
|
}
|
|
// buttons: free game: new game; mission: next / again and back to the campaign
|
|
let labels: Vec<(String, u8)> = match (mission, self.coop_end) {
|
|
// an editor campaign in co-op: the host goes on for everybody, the others wait or leave
|
|
(_, Some((true, true, false))) => vec![(tr(if win { "end.next" } else { "end.again" }), 1), (tr("net.leave"), 2)],
|
|
(_, Some((true, true, true))) | (_, Some((false, true, _))) => vec![(tr("net.leave"), 2)],
|
|
(_, Some((_, false, _))) => vec![(tr("net.coop_run_over"), 2)],
|
|
(None, None) => vec![(tr("end.new"), 0)],
|
|
(Some(_), None) => vec![(tr(if !win { "end.again" } else if last { "end.to_campaign" } else { "end.next" }), 1), (tr("end.campaign"), 2)],
|
|
};
|
|
if let Some((host, true, sent)) = self.coop_end {
|
|
if !host || sent {
|
|
let t = tr("net.coop_wait_host");
|
|
gfx.text(&t, grid_center(sw, gfx.measure(&t, SMALL())), y + CH() * (5.3 + lines.len() as f32 + chart_rows as f32), SMALL(), GOLD);
|
|
}
|
|
}
|
|
let by = y + CH() * (6.0 + lines.len() as f32 + chart_rows as f32);
|
|
let widths: Vec<f32> = labels.iter().map(|(l, _)| gfx.measure(l, S()) + CW() * 4.0).collect();
|
|
let total: f32 = widths.iter().sum::<f32>() + CW() * 2.0 * (labels.len() as f32 - 1.0);
|
|
let mut bx = grid_center(sw, total);
|
|
let (mx, my) = mouse();
|
|
self.end_buttons.clear();
|
|
for ((label, code), w) in labels.iter().zip(widths) {
|
|
let r = Rect::new(bx, by, w, CH() * 2.0);
|
|
let hov = r.contains(vec2(mx, my));
|
|
draw_rectangle(r.x, r.y, r.w, r.h, if hov { Color::new(0.35, 0.22, 0.1, 1.0) } else { Color::new(0.15, 0.1, 0.08, 1.0) });
|
|
draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0, col);
|
|
gfx.text(label, r.x + CW() * 2.0, r.y + CH() * 0.5, S(), TXT);
|
|
self.end_buttons.push((r, *code));
|
|
bx += w + CW() * 2.0;
|
|
}
|
|
self.rects.push(Rect::new(0.0, 0.0, sw, sh));
|
|
}
|
|
}
|
|
|
|
impl Ui {
|
|
// Pixel-art mouse pointer: pointing hand with the active tool's icon, or the grabbing
|
|
// hand for the hand tool (fist while holding units, animated slap/punch)
|
|
pub fn draw_cursor(&self, gfx: &Gfx, g: &Game, now: f64, in_menu: bool) {
|
|
let (mx, my) = mouse();
|
|
let px = K();
|
|
let sc = 2.0 * px; // cursor pixel size
|
|
let anim = self.hand_anim.filter(|(_, t)| now - t < 0.25);
|
|
let hand_tool = self.tool == Tool::Hand && !in_menu;
|
|
let (frame, mut ox, mut oy) = match (anim, hand_tool) {
|
|
(Some((1, t)), _) => {
|
|
// backhand: swings from right to left
|
|
let k = ((now - t) / 0.25) as f32;
|
|
(CURSOR_BACKHAND, ((1.0 - 2.0 * k) * 6.0).round() * sc, 0.0)
|
|
}
|
|
(Some((_, t)), _) => {
|
|
// punch: short thrust forward and back
|
|
let k = ((now - t) / 0.25) as f32;
|
|
let d = if k < 0.4 { -k / 0.4 } else { -(1.0 - k) / 0.6 };
|
|
(CURSOR_PUNCH, (d * 3.0).round() * sc, (d * 3.0).round() * sc)
|
|
}
|
|
(None, true) if !g.hand.is_empty() => (CURSOR_FIST, 0.0, 0.0),
|
|
(None, true) => (CURSOR_OPEN, 0.0, 0.0),
|
|
_ => (CURSOR_POINT, 0.0, 0.0),
|
|
};
|
|
// idle: the pointing hand wiggles by one pixel
|
|
if frame == CURSOR_POINT && (now * 2.0) as i64 % 2 == 0 {
|
|
oy += sc;
|
|
}
|
|
if frame == CURSOR_OPEN && (now * 1.5) as i64 % 2 == 0 {
|
|
ox += sc;
|
|
}
|
|
let (hx, hy) = CURSOR_HOT[frame as usize];
|
|
let x = (mx - hx * sc + ox).floor();
|
|
let y = (my - hy * sc + oy).floor();
|
|
// units in the hand dangle below the fist
|
|
if hand_tool && !g.hand.is_empty() {
|
|
if let Some(e) = g.hand.last().and_then(|id| g.ents.iter().find(|e| e.id == *id)) {
|
|
let s = 10.0 * sc;
|
|
draw_texture_ex(
|
|
&gfx.sprites,
|
|
x + 3.0 * sc,
|
|
y + 12.0 * sc,
|
|
WHITE,
|
|
DrawTextureParams { dest_size: Some(vec2(s, s)), source: Some(Gfx::sprite_rect(e.kind, false)), rotation: 0.3, ..Default::default() },
|
|
);
|
|
}
|
|
gfx.text_sh(&g.hand.len().to_string(), x + 15.0 * sc, y + 10.0 * sc, S(), FRAME_HI);
|
|
}
|
|
draw_texture_ex(
|
|
&gfx.cursors,
|
|
x,
|
|
y,
|
|
WHITE,
|
|
DrawTextureParams { dest_size: Some(vec2(16.0 * sc, 16.0 * sc)), source: Some(Rect::new(frame as f32 * 16.0, 0.0, 16.0, 16.0)), ..Default::default() },
|
|
);
|
|
// small icon of the active action next to the pointing hand
|
|
if frame == CURSOR_POINT && !in_menu {
|
|
let icon = 16.0 * px;
|
|
let (ix, iy) = (x + 11.0 * sc, y + 11.0 * sc);
|
|
draw_rectangle(ix - px, iy - px, icon + 2.0 * px, icon + 2.0 * px, Color::new(0.05, 0.03, 0.04, 0.85));
|
|
draw_rectangle_lines(ix - px, iy - px, icon + 2.0 * px, icon + 2.0 * px, px, FRAME);
|
|
gfx.draw_cell(tool_info(self.tool).icon, ix, iy, icon, icon, WHITE);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Info text for the unit or tile under the mouse (first line = title)
|
|
// the colour of wood in the interface
|
|
const WOOD_C: Color = Color::new(0.78, 0.56, 0.32, 1.0);
|
|
|
|
// fire and wear of a bridge tile
|
|
fn bridge_state(g: &Game, i: usize, out: &mut Vec<(String, Color)>) {
|
|
if g.burning(i) {
|
|
out.push((tr("tile.bridge_burning"), RED));
|
|
}
|
|
let w = g.wear_of(i);
|
|
if w > 0.0 {
|
|
out.push((trf("tile.bridge_worn", &[((w * 100.0) as i32).to_string()]), GOLD));
|
|
}
|
|
}
|
|
|
|
pub fn hover_info(g: &Game, ent: Option<usize>, tile: Option<(i32, i32)>) -> Vec<(String, Color)> {
|
|
let mut out = Vec::new();
|
|
if let Some(j) = ent {
|
|
let e = &g.ents[j];
|
|
let s = st(e.kind);
|
|
// the trader: his offer, the price and how long he waits
|
|
if let Some((key, price, left)) = g.trader_offer(e.id) {
|
|
out.push((tr(s.name), GOLD));
|
|
out.push((trf("trader.info", &[tr(key), (price as i32).to_string()]), FRAME_HI));
|
|
out.push((trf("trader.left", &[(left.ceil() as i32).to_string()]), DIM));
|
|
return out;
|
|
}
|
|
// a named creature: its name is the title
|
|
if e.name != 0 {
|
|
out.push((crate::names::display_name(e), GOLD));
|
|
}
|
|
out.push((trf("info.level", &[tr(s.name), e.level.to_string()]), if e.hero { BLUE } else { FRAME_HI }));
|
|
out.push((trf("info.rank", &[tr(&format!("rank.{}", e.level.clamp(1, 10)))]), GOLD));
|
|
out.push((trf("info.hp", &[(e.hp.max(0.0) as i32).to_string(), (e.max_hp as i32).to_string()]), GREEN));
|
|
if e.gear > 0 {
|
|
out.push((tr(&format!("gear.{}", e.gear.min(3))), BLUE));
|
|
}
|
|
if e.hero && e.captive != 0 {
|
|
out.push((tr(["", "info.unconscious", "info.carried", "info.tortured"][e.captive.min(3) as usize]), RED));
|
|
return out;
|
|
}
|
|
if e.dwarf() {
|
|
out.push((tr("info.dwarf"), Color::new(0.9, 0.6, 0.3, 1.0)));
|
|
out.push((tr("info.dwarf_neutral"), DIM));
|
|
return out;
|
|
}
|
|
out.push((tr(job_name(e)), TXT));
|
|
if e.hero {
|
|
out.push((tr(if e.guard { "info.guard" } else { "info.intruder" }), BLUE));
|
|
} else if e.creature() {
|
|
let pct = |v: f32| ((v.clamp(0.0, 1.0) * 100.0).round() as i32).to_string();
|
|
out.push((trf("info.needs", &[pct(e.energy), pct(e.hunger / 2.0)]), TXT));
|
|
let mood = if e.happy > 0.6 {
|
|
("mood.happy", GREEN)
|
|
} else if e.happy > 0.25 {
|
|
("mood.grumpy", GOLD)
|
|
} else {
|
|
("mood.angry", RED)
|
|
};
|
|
out.push((trf("info.mood", &[tr(mood.0)]), mood.1));
|
|
let wage = (s.wage as f32 * (1.0 + 0.1 * (e.level - 1) as f32)) as i32;
|
|
out.push((trf("info.wage", &[wage.to_string()]), GOLD));
|
|
} else if e.carry > 0 {
|
|
out.push((trf("info.carries", &[e.carry.to_string()]), GOLD));
|
|
}
|
|
return out;
|
|
}
|
|
let Some((x, y)) = tile else { return out };
|
|
if !g.map.shown(idx(x, y)) {
|
|
out.push((tr("tile.unexplored"), DIM));
|
|
if g.map.t[idx(x, y)].marked {
|
|
out.push((tr("tile.marked"), GOLD));
|
|
}
|
|
return out;
|
|
}
|
|
if let Some(tr) = g.trap_at(idx(x, y)) {
|
|
use crate::traps::*;
|
|
out.push((tr_name(tr.kind), FRAME_HI));
|
|
let state = match tr.stage {
|
|
ORDERED => trf("trap.ordered", &[((tr.progress / tr.kind.work() * 100.0) as i32).to_string()]),
|
|
CRATED => tr_key("trap.crated"),
|
|
CARRIED => tr_key("trap.carried"),
|
|
_ if tr.kind.door() => trf("info.hp", &[(tr.hp.max(0.0) as i32).to_string(), (tr.kind.hp() as i32).to_string()]),
|
|
_ => tr_key("trap.armed"),
|
|
};
|
|
out.push((state, if tr.stage == ARMED { GREEN } else { GOLD }));
|
|
if tr.kind.door() && tr.stage == ARMED {
|
|
out.push((tr_key(if tr.locked { "trap.locked" } else { "trap.unlocked" }), DIM));
|
|
}
|
|
return out;
|
|
}
|
|
let Some(t) = g.map.get(x, y) else { return out };
|
|
let i = idx(x, y);
|
|
// a memorial stone: who lies here
|
|
if let Some(m) = g.memorial_at(i) {
|
|
let name = crate::names::full_name(m.kind, m.name, m.level);
|
|
out.push((tr("tile.memorial"), DIM));
|
|
out.push((trf("tile.memorial_info", &[name, tr(st(m.kind).name), m.level.to_string()]), TXT));
|
|
return out;
|
|
}
|
|
let owner = match t.owner {
|
|
Owner::Player => "owner.player",
|
|
Owner::Hero => "owner.hero",
|
|
Owner::Rival1 => "owner.rival1",
|
|
Owner::Rival2 => "owner.rival2",
|
|
Owner::Rival3 => "owner.rival3",
|
|
Owner::None => "owner.none",
|
|
};
|
|
match t.kind {
|
|
Kind::Rock => out.push((tr("tile.rock"), DIM)),
|
|
Kind::Steel => {
|
|
out.push((tr("tile.steel"), FRAME_HI));
|
|
out.push((tr("tile.steel_info"), DIM));
|
|
}
|
|
Kind::Earth if t.reinforced => {
|
|
out.push((tr("tile.reinforced"), FRAME_HI));
|
|
out.push((tr("tile.reinforced_info"), DIM));
|
|
}
|
|
Kind::Earth => out.push((tr("tile.earth"), FRAME_HI)),
|
|
Kind::Gold | Kind::Silver | Kind::Ruby | Kind::Diamond => {
|
|
let key = ["tile.gold", "tile.silver", "tile.ruby", "tile.diamond"][t.vein().unwrap_or(0) as usize];
|
|
out.push((tr(key), GOLD));
|
|
out.push((trf("tile.gold_amount", &[t.gold.to_string()]), GOLD));
|
|
}
|
|
Kind::Floor => {
|
|
out.push((tr("tile.floor"), TXT));
|
|
out.push((tr(owner), DIM));
|
|
}
|
|
Kind::Core => {
|
|
out.push((tr("tile.core"), RED));
|
|
out.push((trf("info.hp", &[(g.core_hp as i32).to_string(), (CORE_HP as i32).to_string()]), RED));
|
|
}
|
|
Kind::Portal => {
|
|
out.push((tr("tile.portal"), MANA));
|
|
out.push((tr(if t.owner == Owner::Player { "portal.claimed" } else { "portal.neutral" }), DIM));
|
|
}
|
|
Kind::Tower => {
|
|
if let Some(tw) = g.tower_at(idx(x, y)) {
|
|
out.push((trf("info.tower", &[tr(tower_name(tw.kind)), tw.level.to_string()]), FRAME_HI));
|
|
out.push((trf("info.hp", &[(tw.hp as i32).to_string(), (tw.max_hp() as i32).to_string()]), GREEN));
|
|
out.push((trf("info.tower_stats", &[format!("{:.0}", tw.dmg()), format!("{:.1}", tw.range())]), TXT));
|
|
if tw.level < 3 {
|
|
out.push((trf("info.tower_upgrade", &[tower_stats(tw.kind).cost[tw.level as usize].to_string()]), GOLD));
|
|
}
|
|
}
|
|
}
|
|
Kind::Water => {
|
|
out.push((tr("tile.water"), BLUE));
|
|
out.push((tr("tile.water_info"), DIM));
|
|
}
|
|
Kind::Lava => {
|
|
out.push((tr("tile.lava"), RED));
|
|
out.push((tr("tile.lava_info"), DIM));
|
|
}
|
|
Kind::Bridge | Kind::LavaBridge => {
|
|
out.push((tr(if t.kind == Kind::Bridge { "tile.bridge" } else { "tile.lavabridge" }), FRAME_HI));
|
|
out.push((tr(owner), DIM));
|
|
bridge_state(g, i, &mut out);
|
|
}
|
|
Kind::Drawbridge => {
|
|
out.push((tr("tile.drawbridge"), FRAME_HI));
|
|
out.push((tr(owner), DIM));
|
|
let state = if t.res & DB_AUTO != 0 {
|
|
if t.raised() { "tile.drawbridge_auto_up" } else { "tile.drawbridge_auto" }
|
|
} else if t.raised() {
|
|
"tile.drawbridge_up"
|
|
} else {
|
|
"tile.drawbridge_down"
|
|
};
|
|
out.push((tr(state), BLUE));
|
|
bridge_state(g, i, &mut out);
|
|
}
|
|
Kind::Mushroom | Kind::Tree => {
|
|
let tree = t.kind == Kind::Tree;
|
|
out.push((tr(if tree { "tile.tree" } else { "tile.mushroom" }), WOOD_C));
|
|
if t.grown() {
|
|
if !tree {
|
|
out.push((tr("tile.mushroom_grown"), GREEN));
|
|
}
|
|
let wood = if tree { crate::wood::WOOD_TREE } else { crate::wood::WOOD_MUSHROOM };
|
|
out.push((trf("tile.plant_info", &[(wood as i32).to_string()]), DIM));
|
|
} else {
|
|
// a young mushroom: not yet to be felled
|
|
out.push((trf("tile.mushroom_growing", &[((t.growth() * 100.0) as i32).to_string()]), DIM));
|
|
}
|
|
}
|
|
Kind::Grove => {
|
|
out.push((tr("tile.grove"), GREEN));
|
|
out.push((tr("tile.grove_info"), DIM));
|
|
}
|
|
Kind::Obsidian => {
|
|
out.push((tr("tile.obsidian"), FRAME_HI));
|
|
let left = g.bridgework.cooled.iter().find(|c| c.0 == i).map_or(0.0, |c| c.1);
|
|
out.push((trf("tile.obsidian_info", &[(left.ceil() as i32).to_string()]), if left < crate::bridges::MELT_WARN { RED } else { DIM }));
|
|
}
|
|
Kind::Shard => {
|
|
out.push((tr("tile.shard"), RED));
|
|
out.push((tr("tile.shard_info"), DIM));
|
|
}
|
|
Kind::RuneChest => {
|
|
out.push((tr("tile.rune_chest"), BLUE));
|
|
out.push((tr("tile.rune_chest_info"), DIM));
|
|
}
|
|
Kind::Gate => {
|
|
out.push((tr("tile.gate"), BLUE));
|
|
out.push((trf("info.hp", &[(g.gate_hp as i32).to_string(), (GATE_HP as i32).to_string()]), BLUE));
|
|
}
|
|
k => {
|
|
if let Some(r) = room_index(k) {
|
|
out.push((tr(ROOM_NAMES[r]), FRAME_HI));
|
|
match k {
|
|
Kind::Farm => out.push((trf("farm.food", &[(t.food as i32).to_string()]), GREEN)),
|
|
Kind::Library => {
|
|
let q = g.research_queue(4);
|
|
if q.is_empty() {
|
|
out.push((tr("research.done"), DIM));
|
|
}
|
|
for (n, k) in q.iter().enumerate() {
|
|
let (tool, cost) = crate::research::RESEARCH[*k];
|
|
let name = tool_info(TOOLS[tool]).name;
|
|
if n == 0 {
|
|
out.push((trf("research.now", &[name, ((g.research / cost * 100.0).min(99.0) as i32).to_string()]), BLUE));
|
|
} else {
|
|
out.push((trf("research.next", &[name]), DIM));
|
|
}
|
|
}
|
|
}
|
|
Kind::Treasury if t.gold > 0 => {
|
|
let what = ["res.gold", "res.silver", "res.ruby", "res.diamond"][(t.res & 3) as usize];
|
|
out.push((trf("treasury.content", &[t.gold.to_string(), tr(what)]), GOLD));
|
|
}
|
|
Kind::Lair => {
|
|
out.push((tr(if t.bed != 0 { "lair.taken" } else { "lair.free" }), DIM));
|
|
let speed = (100.0 / g.lair_factor()).round() as i32;
|
|
out.push((trf("lair.arrivals", &[g.free_beds().to_string(), speed.to_string()]), if speed >= 100 { GREEN } else { GOLD }));
|
|
}
|
|
Kind::Torture => {
|
|
let used = g.ents.iter().filter(|e| e.alive && e.hero && e.captive != 0).count();
|
|
out.push((trf("info.prison", &[used.to_string(), (g.counts[5] / 2).to_string()]), DIM));
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if t.kind == Kind::Rock {
|
|
out.push((tr("tile.indestructible"), DIM));
|
|
}
|
|
if t.marked {
|
|
out.push((tr("tile.marked"), GOLD));
|
|
}
|
|
out
|
|
}
|
|
|
|
// the part of the segment a-b inside the rectangle (0, 0)-(w, h) (Liang-Barsky)
|
|
fn clip_segment(a: Vec2, b: Vec2, w: f32, h: f32) -> Option<(Vec2, Vec2)> {
|
|
let d = b - a;
|
|
let (mut t0, mut t1) = (0.0f32, 1.0f32);
|
|
for (p, q) in [(-d.x, a.x), (d.x, w - a.x), (-d.y, a.y), (d.y, h - a.y)] {
|
|
if p == 0.0 {
|
|
if q < 0.0 {
|
|
return None;
|
|
}
|
|
} else {
|
|
let r = q / p;
|
|
if p < 0.0 {
|
|
t0 = t0.max(r);
|
|
} else {
|
|
t1 = t1.min(r);
|
|
}
|
|
if t0 > t1 {
|
|
return None;
|
|
}
|
|
}
|
|
}
|
|
Some((a + d * t0, a + d * t1))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod clip_tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn view_edges_are_cut_at_the_border() {
|
|
// inside: unchanged
|
|
let (a, b) = clip_segment(vec2(1.0, 1.0), vec2(5.0, 5.0), 10.0, 10.0).unwrap();
|
|
assert_eq!((a, b), (vec2(1.0, 1.0), vec2(5.0, 5.0)));
|
|
// crossing the left border: cut at x = 0, same direction
|
|
let (a, b) = clip_segment(vec2(-4.0, 2.0), vec2(4.0, 6.0), 10.0, 10.0).unwrap();
|
|
assert!((a.x - 0.0).abs() < 1e-5 && (a.y - 4.0).abs() < 1e-5 && b == vec2(4.0, 6.0));
|
|
// completely outside: nothing
|
|
assert!(clip_segment(vec2(-5.0, -1.0), vec2(-1.0, 20.0), 10.0, 10.0).is_none());
|
|
}
|
|
}
|
|
|
|
fn tr_name(k: crate::traps::TrapKind) -> String {
|
|
tr(&format!("trap.{}", k.id()))
|
|
}
|
|
|
|
fn tr_key(key: &str) -> String {
|
|
tr(key)
|
|
}
|