Files
dungeoncraft/src/editor/blocks_ui.rs
T

813 lines
35 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 block editor: a script as a stack of coloured puzzle blocks. Blocks come from the
// palette on the left (drag them into the script), are moved by their grip, nest inside
// "if" and "repeat", and have their settings right inside them. Problems (missing zones,
// unset variables, wrong values) are marked red while editing; undo/redo, copy/paste and a
// search are there too.
use super::widgets::*;
use super::{kind_label, side_label, ScriptRef};
use crate::blocks::*;
use crate::campaign_doc::MissionDoc;
use crate::gfx::*;
use crate::i18n::{tr, trf};
use crate::ui::*;
use macroquad::prelude::*;
#[allow(unused_imports)]
use crate::ui::{BLUE, GOLD, GREEN, RED};
#[derive(Clone)]
struct Drag {
block: Block,
from: Option<(Vec<usize>, usize)>, // where it was taken from (None: the palette)
}
#[derive(Default)]
pub struct BlockEdit {
cat: usize,
undo: Vec<String>,
redo: Vec<String>,
last_json: String,
last_change: f64,
clipboard: Option<Block>,
selected: Option<Vec<usize>>,
drag: Option<Drag>,
search: String,
scroll: f32,
num_buf: String, // the number being typed (focused number field)
num_focus: Option<u64>,
help: String,
rows: std::collections::HashMap<u64, usize>, // rows each block's fields needed (wrapping)
}
enum Op {
Remove(Vec<usize>),
Select(Vec<usize>),
StartDrag(Vec<usize>),
}
// what the fields may offer
struct Ctx {
zones: Vec<String>,
vars: Vec<String>,
triggers: Vec<String>,
problems: Vec<(Vec<usize>, &'static str)>,
search: String,
}
fn cat_color(cat: &str) -> Color {
match cat {
"say" => Color::new(0.42, 0.25, 0.55, 1.0),
"units" => Color::new(0.58, 0.2, 0.18, 1.0),
"world" => Color::new(0.3, 0.42, 0.2, 1.0),
"logic" => Color::new(0.62, 0.4, 0.12, 1.0),
"vars" => Color::new(0.18, 0.34, 0.58, 1.0),
_ => Color::new(0.55, 0.47, 0.14, 1.0),
}
}
fn fid(path: &[usize], field: usize) -> u64 {
let mut h: u64 = 0x9e3779b97f4a7c15;
for &p in path {
h = (h ^ p as u64).wrapping_mul(0x100000001b3);
}
(h ^ (field as u64 + 7919)).wrapping_mul(0x100000001b3)
}
fn list_mut<'a>(root: &'a mut Vec<Block>, lp: &[usize]) -> Option<&'a mut Vec<Block>> {
let mut cur = root;
let mut k = 0;
while k + 1 < lp.len() + 1 && k < lp.len() {
let (i, branch) = (lp[k], *lp.get(k + 1)?);
cur = match cur.get_mut(i)? {
Block::If { then, els, .. } => {
if branch == 0 {
then
} else {
els
}
}
Block::Repeat { body, .. } | Block::Until { body, .. } | Block::DoUntil { body, .. } => body,
_ => return None,
};
k += 2;
}
Some(cur)
}
fn script_mut<'a>(m: &'a mut MissionDoc, s: ScriptRef) -> Option<&'a mut Vec<Block>> {
Some(match s {
ScriptRef::Start => &mut m.scripts.start,
ScriptRef::Victory => &mut m.scripts.victory,
ScriptRef::Defeat => &mut m.scripts.defeat,
ScriptRef::Tick => &mut m.scripts.tick,
ScriptRef::Trigger(k) => &mut m.scripts.triggers.get_mut(k)?.script,
})
}
// does a block (or anything in it) contain the search text?
fn block_text(b: &Block) -> String {
match b {
Block::Message { text } | Block::Objective { text } | Block::Hint { text } => text.clone(),
Block::SetVar { name, .. } | Block::AddVar { name, .. } => name.clone(),
Block::Spawn { kind, name, place, .. } => format!("{} {} {:?}", kind, name, place),
Block::Reveal { zone } | Block::Camera { zone } | Block::Kill { zone, .. } | Block::Terrain { zone, .. } => zone.clone(),
Block::Trigger { name, .. } => name.clone(),
_ => String::new(),
}
}
const ROW: f32 = 2.2; // block row height in CH
impl BlockEdit {
pub fn open(&mut self, _s: ScriptRef) {
self.undo.clear();
self.redo.clear();
self.last_json.clear();
self.selected = None;
self.drag = None;
self.scroll = 0.0;
}
// returns true when the editor is left
pub fn frame(&mut self, im: &mut Im, gfx: &Gfx, m: &mut MissionDoc, sref: ScriptRef) -> bool {
let (w, h) = (sw(), sh());
let mut back = false;
let title = match sref {
ScriptRef::Start => tr("ed.script_start"),
ScriptRef::Victory => tr("ed.script_victory"),
ScriptRef::Defeat => tr("ed.script_defeat"),
ScriptRef::Tick => tr("ed.script_tick"),
ScriptRef::Trigger(k) => trf("ed.trigger_script", &[m.scripts.triggers.get(k).map(|t| t.name.clone()).unwrap_or_default()]),
};
let probs = {
let list = match sref {
ScriptRef::Start => &m.scripts.start,
ScriptRef::Victory => &m.scripts.victory,
ScriptRef::Defeat => &m.scripts.defeat,
ScriptRef::Tick => &m.scripts.tick,
ScriptRef::Trigger(k) => match m.scripts.triggers.get(k) {
Some(t) => &t.script,
None => return true,
},
};
m.problems(list)
};
let ctx = Ctx {
zones: m.zone_names(),
vars: m.vars(),
triggers: m.trigger_names(),
problems: probs.iter().map(|p| (p.path.clone(), p.key)).collect(),
search: self.search.to_lowercase(),
};
let Some(list) = script_mut(m, sref) else { return true };
let before = serde_json::to_string(&*list).unwrap_or_default();
if self.last_json.is_empty() {
self.last_json = before.clone();
}
// ---- top bar
panel(gfx, Rect::new(0.0, 0.0, w, CH() * 3.2), "");
if im.button(gfx, Rect::new(CW(), CH() * 0.7, CW() * 12.0, CH() * 1.7), &tr("ed.back")) || (is_key_pressed(KeyCode::Escape) && !im.typing && self.drag.is_none()) {
back = true;
}
let mut bx = w - CW();
let buttons = [tr("ed.delete"), tr("ed.paste"), tr("ed.copy"), tr("ed.redo"), tr("ed.undo")];
let mut pressed_btn = None;
for (k, l) in buttons.iter().enumerate() {
let bw = gfx.measure(l, S()) + CW() * 2.0;
bx -= bw;
if im.button(gfx, Rect::new(bx, CH() * 0.7, bw, CH() * 1.7), l) {
pressed_btn = Some(k);
}
bx -= CW() * 0.5;
}
let sw_ = CW() * 18.0;
bx -= sw_ + CW();
im.text_field(gfx, id("bsearch", 0), Rect::new(bx, CH() * 0.7, sw_, CH() * 1.7), &mut self.search, 24);
// the script's name, as much as fits before the search field
let room = ((bx - CW() * 15.0) / CW()).max(4.0) as usize;
let title: String = title.chars().take(room).collect();
gfx.text(&title, CW() * 14.0, CH() * 1.0, S(), FRAME_HI);
if self.search.is_empty() && im.focus != Some(id("bsearch", 0)) {
gfx.text(&tr("ed.search"), bx + CW() * 0.5, CH() * 1.0, S(), DIM);
}
let ptext = if probs.is_empty() { tr("ed.no_problems") } else { trf("ed.problems", &[probs.len().to_string()]) };
gfx.text(&ptext, CW() * 14.0, CH() * 2.2, SMALL(), if probs.is_empty() { GREEN } else { RED });
// keyboard shortcuts
let keys = !im.typing;
if keys && im.ctrl && is_key_pressed(KeyCode::F) {
im.focus = Some(id("bsearch", 0));
}
let mut act = pressed_btn;
if keys && im.ctrl && is_key_pressed(KeyCode::Z) {
act = Some(4);
}
if keys && im.ctrl && is_key_pressed(KeyCode::Y) {
act = Some(3);
}
if keys && im.ctrl && is_key_pressed(KeyCode::C) {
act = Some(2);
}
if keys && im.ctrl && is_key_pressed(KeyCode::V) {
act = Some(1);
}
if keys && is_key_pressed(KeyCode::Delete) {
act = Some(0);
}
match act {
Some(4) => {
if let Some(s) = self.undo.pop() {
self.redo.push(before.clone());
if let Ok(l) = serde_json::from_str(&s) {
*list = l;
}
self.last_json = s;
self.selected = None;
}
}
Some(3) => {
if let Some(s) = self.redo.pop() {
self.undo.push(before.clone());
if let Ok(l) = serde_json::from_str(&s) {
*list = l;
}
self.last_json = s;
self.selected = None;
}
}
Some(2) => {
if let Some(p) = self.selected.clone() {
if let Some(l) = list_mut(list, &p[..p.len() - 1]) {
self.clipboard = l.get(p[p.len() - 1]).cloned();
}
}
}
Some(1) => {
if let Some(b) = self.clipboard.clone() {
match self.selected.clone() {
Some(p) => {
if let Some(l) = list_mut(list, &p[..p.len() - 1]) {
let at = (p[p.len() - 1] + 1).min(l.len());
l.insert(at, b);
}
}
None => list.push(b),
}
}
}
Some(0) => {
if let Some(p) = self.selected.take() {
if let Some(l) = list_mut(list, &p[..p.len() - 1]) {
if p[p.len() - 1] < l.len() {
l.remove(p[p.len() - 1]);
}
}
}
}
_ => {}
}
// ---- palette
let pal_w = CW() * 28.0;
let top = CH() * 3.2;
panel(gfx, Rect::new(0.0, top, pal_w, h - top), "");
let mut y = top + CH() * 0.6;
for (k, c) in CATEGORIES.iter().enumerate() {
let r = Rect::new(CW() * 0.5 + (k % 2) as f32 * (pal_w * 0.5 - CW() * 0.25), y + (k / 2) as f32 * CH() * 1.8, pal_w * 0.5 - CW() * 0.75, CH() * 1.6);
draw_rectangle(r.x, r.y, r.w, r.h, if self.cat == k { cat_color(c) } else { Color::new(0.1, 0.07, 0.07, 1.0) });
draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0, cat_color(c));
gfx.text(&tr(&format!("blk.cat.{}", c)), r.x + CW() * 0.5, r.y + CH() * 0.3, S(), TXT);
if im.clicked(r) {
self.cat = k;
}
}
y += CH() * 6.0;
self.help.clear();
for (c, kind) in PALETTE.iter().filter(|(c, _)| *c == CATEGORIES[self.cat]) {
let r = Rect::new(CW() * 0.5, y, pal_w - CW(), CH() * 1.8);
draw_block_shape(r, cat_color(c), im.over(r), false);
gfx.text(&tr(&format!("blk.{}", kind)), r.x + CW() * 1.6, r.y + CH() * 0.4, S(), TXT);
if im.over(r) {
self.help = tr(&format!("blk.help.{}", kind));
}
if im.pressed && im.over(r) {
im.pressed = false;
if let Some(b) = Block::template(kind) {
self.drag = Some(Drag { block: b, from: None });
}
}
y += CH() * 2.2;
}
for (k, l) in wrap(&tr("ed.blocks_hint"), ((pal_w - CW()) / (GW * SMALL())) as usize).iter().enumerate() {
gfx.text(l, CW() * 0.5, h - CH() * 6.0 + k as f32 * GH * SMALL() * 1.2, SMALL(), DIM);
}
// ---- the script
let ws = Rect::new(pal_w, top, w - pal_w, h - top - CH() * 2.0);
if im.over(ws) && im.wheel != 0.0 && !im.popup_open() {
self.scroll = (self.scroll - im.wheel.signum() * CH() * 3.0).max(0.0);
}
let mut ops = Vec::new();
let mut slots: Vec<(Rect, Vec<usize>, usize)> = Vec::new();
let x0 = ws.x + CW() * 2.0;
let y0 = ws.y + CH() - self.scroll;
let height = self.draw_list(im, gfx, list, &[], x0, y0, ws.w - CW() * 4.0, &ctx, &mut ops, &mut slots);
self.scroll = self.scroll.min((height - ws.h * 0.5).max(0.0));
if list.is_empty() {
gfx.text(&tr("ed.empty_script"), x0, y0 + CH(), S(), DIM);
}
// apply what was clicked
for op in ops {
match op {
Op::Select(p) => self.selected = Some(p),
Op::Remove(p) => {
if let Some(l) = list_mut(list, &p[..p.len() - 1]) {
if p[p.len() - 1] < l.len() {
l.remove(p[p.len() - 1]);
}
}
self.selected = None;
}
Op::StartDrag(p) => {
if let Some(l) = list_mut(list, &p[..p.len() - 1]) {
let k = p[p.len() - 1];
if k < l.len() {
let b = l.remove(k);
self.drag = Some(Drag { block: b, from: Some((p[..p.len() - 1].to_vec(), k)) });
self.selected = None;
}
}
}
}
}
// dragging: the block follows the pointer; dropped into the nearest gap
if let Some(d) = self.drag.clone() {
let target = slots
.iter()
.filter(|(r, _, _)| r.contains(vec2(im.mx, im.my)))
.max_by_key(|(_, lp, _)| lp.len())
.map(|(r, lp, k)| (*r, lp.clone(), *k))
.or_else(|| {
// below everything: the end of the script
(im.over(ws)).then(|| (Rect::new(x0, y0 + height, ws.w, CH()), Vec::new(), list.len()))
});
if let Some((r, _, _)) = &target {
draw_rectangle(r.x, r.y + r.h * 0.5 - 2.0, ws.w * 0.5, 4.0, GOLD);
}
let gr = Rect::new(im.mx - CW(), im.my - CH() * 0.5, CW() * 24.0, CH() * 1.8);
draw_block_shape(gr, Color::new(cat_color(d.block.category()).r, cat_color(d.block.category()).g, cat_color(d.block.category()).b, 0.85), true, false);
gfx.text(&tr(&format!("blk.{}", d.block.kind_name())), gr.x + CW() * 1.6, gr.y + CH() * 0.4, S(), TXT);
if !im.down {
let over_palette = im.mx < pal_w;
match (target, over_palette) {
(Some((_, lp, k)), false) => {
if let Some(l) = list_mut(list, &lp) {
let k = k.min(l.len());
l.insert(k, d.block.clone());
let mut p = lp.clone();
p.push(k);
self.selected = Some(p);
}
}
(_, true) => {} // dropped on the palette: gone
_ => {
// nowhere: back where it came from
if let Some((lp, k)) = d.from {
if let Some(l) = list_mut(list, &lp) {
l.insert(k.min(l.len()), d.block.clone());
}
}
}
}
self.drag = None;
}
}
// the help line
panel(gfx, Rect::new(pal_w, h - CH() * 2.0, w - pal_w, CH() * 2.0), "");
if !self.help.is_empty() {
let line: String = self.help.chars().take(((w - pal_w) / CW()) as usize - 2).collect();
gfx.text(&line, pal_w + CW(), h - CH() * 1.5, S(), TXT);
}
// undo: one step per change (typing in one field within a second is one step)
let after = serde_json::to_string(&*list).unwrap_or_default();
if after != self.last_json && act.map_or(true, |a| a < 3) {
let now = get_time();
if !(im.typing && now - self.last_change < 1.0) {
self.undo.push(self.last_json.clone());
if self.undo.len() > 100 {
self.undo.remove(0);
}
self.redo.clear();
}
self.last_change = now;
self.last_json = after;
} else if act.map_or(false, |a| a >= 3) {
self.last_json = after;
}
back
}
// draws a list of blocks; returns its height
#[allow(clippy::too_many_arguments)]
fn draw_list(&mut self, im: &mut Im, gfx: &Gfx, list: &mut Vec<Block>, lp: &[usize], x: f32, y0: f32, w: f32, ctx: &Ctx, ops: &mut Vec<Op>, slots: &mut Vec<(Rect, Vec<usize>, usize)>) -> f32 {
let mut y = y0;
if list.is_empty() {
slots.push((Rect::new(x, y, w, CH() * 1.6), lp.to_vec(), 0));
if !lp.is_empty() {
draw_rectangle_lines(x, y, CW() * 18.0, CH() * 1.4, 1.0, Color::new(0.4, 0.33, 0.25, 0.8));
gfx.text(&tr("ed.drop_here"), x + CW() * 0.5, y + CH() * 0.2, SMALL(), DIM);
}
return CH() * 1.7;
}
for k in 0..list.len() {
let mut path = lp.to_vec();
path.push(k);
let cat = list[k].category();
let col = cat_color(cat);
// the spawn block has many fields: two rows
// rows the fields needed last frame (they wrap when the block is too narrow)
let rows = self.rows.get(&fid(&path, 9999)).copied().unwrap_or(1) as f32;
let header = Rect::new(x, y, w, CH() * ROW * rows);
let problem = ctx.problems.iter().find(|(p, _)| *p == path).map(|(_, k)| *k);
let selected = self.selected.as_ref() == Some(&path);
let found = !ctx.search.is_empty() && (tr(&format!("blk.{}", list[k].kind_name())).to_lowercase().contains(&ctx.search) || block_text(&list[k]).to_lowercase().contains(&ctx.search));
// slots: upper half before this block, lower half after it
slots.push((Rect::new(x, y - CH() * 0.3, w, header.h * 0.5 + CH() * 0.3), lp.to_vec(), k));
slots.push((Rect::new(x, y + header.h * 0.5, w, header.h * 0.5), lp.to_vec(), k + 1));
let nested = matches!(list[k], Block::If { .. } | Block::Repeat { .. } | Block::Until { .. } | Block::DoUntil { .. });
// the block: header row with its fields
draw_block_shape(header, col, selected, found);
if let Some(p) = problem {
draw_rectangle_lines(header.x - 2.0, header.y - 2.0, header.w + 4.0, header.h + 4.0, 3.0, RED);
im.tip(header, &tr(p));
}
// grip: drag the block from here
let grip = Rect::new(x, y, CW() * 1.4, header.h);
gfx.text("≡", x + CW() * 0.3, y + CH() * 0.6, S(), Color::new(1.0, 1.0, 1.0, 0.6));
if im.pressed && im.over(grip) {
im.pressed = false;
ops.push(Op::StartDrag(path.clone()));
}
let fy = y + (CH() * ROW - CH() * 1.5) * 0.5;
let cx = x + CW() * 1.8;
let mut lay = Lay { x: cx, y: fy, x2: x + CW() * 3.0, max: x + w - CW() * 2.5, rows: 1 };
self.block_fields(im, gfx, &mut list[k], &path, &mut lay, ctx);
self.rows.insert(fid(&path, 9999), lay.rows);
// a click on the block's free area selects it
if im.clicked(header) {
ops.push(Op::Select(path.clone()));
}
// the delete cross
let xr = Rect::new(x + w - CW() * 2.0, y + CH() * 0.4, CW() * 1.6, CH() * 1.4);
if im.over(header) {
gfx.text("x", xr.x + CW() * 0.4, xr.y + CH() * 0.15, S(), TXT);
if im.clicked(xr) {
ops.push(Op::Remove(path.clone()));
}
self.help = tr(&format!("blk.help.{}", list[k].kind_name()));
}
y += header.h;
if nested {
// the C shape: bodies indented, with an "else" bar for if
let inner_x = x + CW() * 2.5;
let inner_w = w - CW() * 2.5;
let branches = if matches!(list[k], Block::If { .. }) { 2 } else { 1 };
for b in 0..branches {
let mut inner_lp = path.clone();
inner_lp.push(b);
let body: &mut Vec<Block> = match &mut list[k] {
Block::If { then, els, .. } => {
if b == 0 {
then
} else {
els
}
}
Block::Repeat { body, .. } | Block::Until { body, .. } | Block::DoUntil { body, .. } => body,
_ => unreachable!(),
};
let start = y;
let hh = self.draw_list(im, gfx, body, &inner_lp, inner_x, y + CH() * 0.3, inner_w, ctx, ops, slots);
y += hh + CH() * 0.6;
draw_rectangle(x, start, CW() * 1.2, y - start, col);
if b == 0 && branches == 2 {
let er = Rect::new(x, y, w * 0.4, CH() * 1.4);
draw_rectangle(er.x, er.y, er.w, er.h, col);
gfx.text(&tr("blk.else"), er.x + CW() * 1.6, er.y + CH() * 0.2, S(), TXT);
y += CH() * 1.4;
}
}
draw_rectangle(x, y, w * 0.3, CH() * 0.6, col);
y += CH() * 0.6;
}
y += CH() * 0.25;
}
y - y0
}
// the fields inside a block's header (they wrap into more rows when the block is too narrow)
fn block_fields(&mut self, im: &mut Im, gfx: &Gfx, b: &mut Block, path: &[usize], lay: &mut Lay, ctx: &Ctx) {
let name = tr(&format!("blk.{}", b.kind_name()));
lay.word(gfx, &name);
let is_set = matches!(b, Block::SetVar { .. });
match b {
Block::Message { text } | Block::Objective { text } | Block::Hint { text } => {
let w = (lay.max - lay.x).max(CW() * 24.0).min(CW() * 60.0);
let r = lay.take(w);
im.text_field(gfx, fid(path, 0), r, text, 200);
}
Block::Spawn { side, kind, count, level, place, name } => {
self.value(im, gfx, count, Ty::Num, path, 1, lay, ctx);
lay.word(gfx, "×");
self.kind_dd(im, gfx, kind, false, path, 2, lay);
lay.word(gfx, &tr("blk.w_level"));
self.value(im, gfx, level, Ty::Num, path, 3, lay, ctx);
self.side_dd(im, gfx, side, false, path, 4, lay);
lay.word(gfx, &tr("blk.w_at"));
let mut opts = vec![tr("blk.place_core"), tr("blk.place_event")];
opts.extend(ctx.zones.iter().map(|z| trf("blk.place_zone", &[z.clone()])));
let cur = match place {
Place::Core => opts[0].clone(),
Place::Event => opts[1].clone(),
Place::Zone(z) => trf("blk.place_zone", &[z.clone()]),
};
let r = lay.take(CW() * 24.0);
if let Some(c) = im.dropdown(gfx, fid(path, 5), r, &cur, &opts) {
*place = match c {
0 => Place::Core,
1 => Place::Event,
c => Place::Zone(ctx.zones[c - 2].clone()),
};
}
lay.word(gfx, &tr("blk.w_name"));
let r = lay.take(CW() * 10.0);
im.text_field(gfx, fid(path, 6), r, name, 16);
}
Block::Wave { count } | Block::Gold { amount: count } | Block::Mana { amount: count } | Block::TalentXp { amount: count } => self.value(im, gfx, count, Ty::Num, path, 1, lay, ctx),
Block::Weather { kind, secs } => {
weather_dd(im, gfx, kind, fid(path, 2), lay);
lay.word(gfx, &tr("blk.w_for"));
self.value(im, gfx, secs, Ty::Num, path, 1, lay, ctx);
lay.word(gfx, &tr("blk.w_seconds"));
}
Block::SetVar { name, value } | Block::AddVar { name, value } => {
let r = lay.take(CW() * 12.0);
im.text_field(gfx, fid(path, 0), r, name, 24);
lay.word(gfx, &tr(if is_set { "blk.w_to" } else { "blk.w_by" }));
self.value(im, gfx, value, Ty::Num, path, 1, lay, ctx);
}
Block::If { cond, .. } | Block::Until { cond, .. } | Block::DoUntil { cond, .. } => self.value(im, gfx, cond, Ty::Bool, path, 1, lay, ctx),
Block::Repeat { times, .. } => {
self.value(im, gfx, times, Ty::Num, path, 1, lay, ctx);
lay.word(gfx, &tr("blk.w_times"));
}
Block::Wait { secs } => {
self.value(im, gfx, secs, Ty::Num, path, 1, lay, ctx);
lay.word(gfx, &tr("blk.w_seconds"));
}
Block::Win | Block::Lose => {}
Block::Reveal { zone } | Block::Camera { zone } => self.zone_dd(im, gfx, zone, path, 1, lay, ctx),
Block::Terrain { zone, kind } => {
self.zone_dd(im, gfx, zone, path, 1, lay, ctx);
lay.word(gfx, &tr("blk.w_to"));
let opts: Vec<String> = TERRAIN.iter().map(|t| tr(&format!("ed.terrain.{}", t))).collect();
let cur = tr(&format!("ed.terrain.{}", kind));
let r = lay.take(CW() * 19.0);
if let Some(c) = im.dropdown(gfx, fid(path, 2), r, &cur, &opts) {
*kind = TERRAIN[c].to_string();
}
}
Block::Kill { zone, side } => {
self.side_dd(im, gfx, side, true, path, 2, lay);
lay.word(gfx, &tr("blk.w_in"));
self.zone_dd(im, gfx, zone, path, 1, lay, ctx);
}
Block::Trigger { name, on } => {
let cur = if name.is_empty() { tr("ed.pick_trigger") } else { name.clone() };
let r = lay.take(CW() * 16.0);
if let Some(c) = im.dropdown(gfx, fid(path, 1), r, &cur, &ctx.triggers) {
*name = ctx.triggers[c].clone();
}
let opts = vec![tr("blk.on"), tr("blk.off")];
let r = lay.take(CW() * 10.0);
if let Some(c) = im.dropdown(gfx, fid(path, 2), r, &opts[if *on { 0 } else { 1 }], &opts) {
*on = c == 0;
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn zone_dd(&mut self, im: &mut Im, gfx: &Gfx, zone: &mut String, path: &[usize], f: usize, lay: &mut Lay, ctx: &Ctx) {
let cur = if zone.is_empty() { tr("ed.pick_zone") } else { zone.clone() };
let r = lay.take(CW() * 14.0);
if let Some(c) = im.dropdown(gfx, fid(path, f), r, &cur, &ctx.zones) {
*zone = ctx.zones[c].clone();
}
}
#[allow(clippy::too_many_arguments)]
fn side_dd(&mut self, im: &mut Im, gfx: &Gfx, side: &mut i32, any: bool, path: &[usize], f: usize, lay: &mut Lay) {
let sides: Vec<i32> = SIDES.iter().copied().filter(|&s| any || s >= 0).collect();
let opts: Vec<String> = sides.iter().map(|&s| side_label(s)).collect();
let r = lay.take(CW() * 16.0);
if let Some(c) = im.dropdown(gfx, fid(path, f), r, &side_label(*side), &opts) {
*side = sides[c];
}
}
#[allow(clippy::too_many_arguments)]
fn kind_dd(&mut self, im: &mut Im, gfx: &Gfx, kind: &mut String, any: bool, path: &[usize], f: usize, lay: &mut Lay) {
let mut opts: Vec<String> = Vec::new();
if any {
opts.push(tr("ed.any_kind"));
}
opts.extend(KINDS.iter().map(|k| kind_label(k)));
let r = lay.take(CW() * 17.0);
if let Some(c) = im.dropdown(gfx, fid(path, f), r, &kind_label(kind), &opts) {
*kind = if any && c == 0 { String::new() } else { KINDS[c - any as usize].to_string() };
}
}
// a value slot: a small switch for the value's kind, then the value itself
#[allow(clippy::too_many_arguments)]
fn value(&mut self, im: &mut Im, gfx: &Gfx, v: &mut Val, want: Ty, path: &[usize], f: usize, lay: &mut Lay, ctx: &Ctx) {
let kinds: &[&str] = if want == Ty::Num { &NUM_VALUES } else { &BOOL_VALUES };
let opts: Vec<String> = kinds.iter().map(|k| tr(&format!("blk.v.{}", k))).collect();
// the kind switch
let sw_ = lay.take_tight(CW() * 1.6);
draw_rectangle(sw_.x, sw_.y, sw_.w, sw_.h, Color::new(0.0, 0.0, 0.0, 0.35));
if let Some(c) = im.dropdown(gfx, fid(path, f * 31 + 1), sw_, "", &opts) {
if let Some(nv) = Val::template(kinds[c]) {
*v = nv;
}
}
let sub = |k: usize| f * 31 + 2 + k;
let and = matches!(v, Val::And { .. });
match v {
Val::Num { n } => {
let id_ = fid(path, f * 31);
let r = lay.take(CW() * 8.0);
let mut s = if im.focus == Some(id_) && self.num_focus == Some(id_) { self.num_buf.clone() } else { fmt_num(*n) };
if im.focus == Some(id_) && self.num_focus != Some(id_) {
self.num_focus = Some(id_);
self.num_buf = s.clone();
}
if im.text_field(gfx, id_, r, &mut s, 10) {
self.num_buf = s.clone();
if let Ok(x) = s.trim().replace(',', ".").parse::<f64>() {
*n = x;
}
}
}
Val::Bool { b } => {
let o = vec![tr("blk.yes"), tr("blk.no")];
let r = lay.take(CW() * 7.0);
if let Some(c) = im.dropdown(gfx, fid(path, sub(0)), r, &o[if *b { 0 } else { 1 }], &o) {
*b = c == 0;
}
}
Val::Var { name } => {
let cur = if name.is_empty() { tr("ed.pick_var") } else { name.clone() };
let r = lay.take(CW() * 15.0);
if let Some(c) = im.dropdown(gfx, fid(path, sub(0)), r, &cur, &ctx.vars) {
*name = ctx.vars[c].clone();
}
}
Val::Gold | Val::Mana | Val::Time => lay.word(gfx, &tr(&format!("blk.v.{}", v.kind_name()))),
Val::Creatures { side, kind } => {
lay.word(gfx, &tr("blk.v.creatures"));
self.kind_dd(im, gfx, kind, true, path, sub(0), lay);
self.side_dd(im, gfx, side, true, path, sub(1), lay);
}
Val::InZone { zone, side, kind } => {
lay.word(gfx, &tr("blk.v.inzone"));
self.kind_dd(im, gfx, kind, true, path, sub(0), lay);
self.side_dd(im, gfx, side, true, path, sub(1), lay);
lay.word(gfx, &tr("blk.w_in"));
self.zone_dd(im, gfx, zone, path, sub(2), lay, ctx);
}
Val::Random { a, b } => {
lay.word(gfx, &tr("blk.v.random"));
self.value(im, gfx, a, Ty::Num, path, sub(0), lay, ctx);
lay.word(gfx, "..");
self.value(im, gfx, b, Ty::Num, path, sub(1), lay, ctx);
}
Val::Math { op, a, b } | Val::Cmp { op, a, b } => {
let ops: Vec<String> = if v_kind_is_math(op) { MATH_OPS.iter().map(|s| s.to_string()).collect() } else { CMP_OPS.iter().map(|s| s.to_string()).collect() };
lay.word(gfx, "(");
self.value(im, gfx, a, Ty::Num, path, sub(0), lay, ctx);
let r = lay.take(CW() * 4.0);
if let Some(c) = im.dropdown(gfx, fid(path, sub(1)), r, op, &ops) {
*op = ops[c].clone();
}
self.value(im, gfx, b, Ty::Num, path, sub(2), lay, ctx);
lay.word(gfx, ")");
}
Val::And { a, b } | Val::Or { a, b } => {
lay.word(gfx, "(");
self.value(im, gfx, a, Ty::Bool, path, sub(0), lay, ctx);
lay.word(gfx, &tr(if and { "blk.and" } else { "blk.or" }));
self.value(im, gfx, b, Ty::Bool, path, sub(1), lay, ctx);
lay.word(gfx, ")");
}
Val::Not { a } => {
lay.word(gfx, &tr("blk.not"));
self.value(im, gfx, a, Ty::Bool, path, sub(0), lay, ctx);
}
Val::EventKind { kind } => {
lay.word(gfx, &tr("blk.v.eventkind"));
self.kind_dd(im, gfx, kind, false, path, sub(0), lay);
}
Val::WeatherIs { kind } => {
lay.word(gfx, &tr("blk.v.weatheris"));
weather_dd(im, gfx, kind, fid(path, sub(0)), lay);
}
}
}
}
// a dropdown with the kinds of weather
fn weather_dd(im: &mut Im, gfx: &Gfx, kind: &mut String, id: u64, lay: &mut Lay) {
let opts: Vec<String> = WEATHER.iter().map(|w| tr(&format!("weather.{}", w))).collect();
let cur = tr(&format!("weather.{}", kind));
let r = lay.take(CW() * 12.0);
if let Some(c) = im.dropdown(gfx, id, r, &cur, &opts) {
*kind = WEATHER[c].to_string();
}
}
// where the next field of a block goes: left to right, into a new row at the block's edge
struct Lay {
x: f32,
y: f32,
x2: f32, // start of a new row
max: f32, // right edge
rows: usize,
}
impl Lay {
fn need(&mut self, w: f32) {
if self.x + w > self.max && self.x > self.x2 + 1.0 {
self.x = self.x2;
self.y += CH() * ROW;
self.rows += 1;
}
}
// room for a field of width w (wraps if needed)
fn take(&mut self, w: f32) -> Rect {
self.need(w);
let r = Rect::new(self.x, self.y, w, CH() * 1.5);
self.x += w + CW() * 0.6;
r
}
fn take_tight(&mut self, w: f32) -> Rect {
// the kind switch stays with its value: room for both
self.need(w + CW() * 8.0);
let r = Rect::new(self.x, self.y, w, CH() * 1.5);
self.x += w + CW() * 0.2;
r
}
fn word(&mut self, gfx: &Gfx, s: &str) {
let w = gfx.measure(s, S());
self.need(w);
gfx.text(s, self.x, self.y + CH() * 0.25, S(), TXT);
self.x += w + CW() * 0.6;
}
}
// math operators are + - * /, comparisons the rest
fn v_kind_is_math(op: &str) -> bool {
MATH_OPS.contains(&op)
}
fn fmt_num(n: f64) -> String {
if n.fract() == 0.0 && n.abs() < 1e12 {
format!("{}", n as i64)
} else {
format!("{}", n)
}
}
// a block: a coloured plate with a notch on top and a tab below (like a puzzle piece)
fn draw_block_shape(r: Rect, col: Color, selected: bool, found: bool) {
draw_rectangle(r.x, r.y, r.w, r.h, col);
let dark = Color::new(col.r * 0.6, col.g * 0.6, col.b * 0.6, col.a);
let light = Color::new((col.r * 1.3).min(1.0), (col.g * 1.3).min(1.0), (col.b * 1.3).min(1.0), col.a);
draw_rectangle(r.x, r.y, r.w, 2.0, light);
draw_rectangle(r.x, r.y + r.h - 3.0, r.w, 3.0, dark);
// notch and tab
let nx = r.x + CW() * 2.0;
draw_rectangle(nx, r.y, CW() * 1.5, CH() * 0.25, Color::new(0.04, 0.03, 0.03, 1.0));
draw_rectangle(nx, r.y + r.h, CW() * 1.5, CH() * 0.25, col);
if selected {
draw_rectangle_lines(r.x - 1.0, r.y - 1.0, r.w + 2.0, r.h + 2.0, 3.0, FRAME_HI);
}
if found {
draw_rectangle_lines(r.x - 3.0, r.y - 3.0, r.w + 6.0, r.h + 6.0, 2.0, GOLD);
}
}