813 lines
35 KiB
Rust
813 lines
35 KiB
Rust
// 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);
|
||
}
|
||
}
|