In-game campaign, map and block script editors
Mission graph with branches, mission settings and goals, map editor on the 3D view (terrain, places, units, towers, traps, forts, rivals, zones; undo, brush, rectangle, fill, mirror), block editor with palette, nesting, live problems, undo, copy/paste and search; test play with a script debug view; demo campaign and built-in missions as templates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
65677504f6
commit
30dd8441b9
10 files changed
+3372
-19
No files matched your search
@@ -0,0 +1,747 @@
|
||||
// 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,
|
||||
}
|
||||
|
||||
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, .. } => 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;
|
||||
}
|
||||
gfx.text(&title, CW() * 14.0, CH() * 1.0, S(), FRAME_HI);
|
||||
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);
|
||||
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;
|
||||
}
|
||||
gfx.text(&tr("ed.blocks_hint"), CW() * 0.5, h - CH() * 6.0, 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);
|
||||
let header = Rect::new(x, y, w, CH() * ROW);
|
||||
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, CH() * ROW * 0.5 + CH() * 0.3), lp.to_vec(), k));
|
||||
slots.push((Rect::new(x, y + CH() * ROW * 0.5, w, CH() * ROW * 0.5), lp.to_vec(), k + 1));
|
||||
let nested = matches!(list[k], Block::If { .. } | Block::Repeat { .. });
|
||||
// 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, CH() * ROW);
|
||||
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 mut cx = x + CW() * 1.8;
|
||||
self.block_fields(im, gfx, &mut list[k], &path, &mut cx, fy, ctx);
|
||||
// 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 += CH() * ROW;
|
||||
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, .. } => 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 row
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn block_fields(&mut self, im: &mut Im, gfx: &Gfx, b: &mut Block, path: &[usize], cx: &mut f32, y: f32, ctx: &Ctx) {
|
||||
let word = |cx: &mut f32, s: &str| {
|
||||
gfx.text(s, *cx, y + CH() * 0.25, S(), TXT);
|
||||
*cx += gfx.measure(s, S()) + CW() * 0.6;
|
||||
};
|
||||
let name = tr(&format!("blk.{}", b.kind_name()));
|
||||
word(cx, &name);
|
||||
let is_set = matches!(b, Block::SetVar { .. });
|
||||
let fh = CH() * 1.5;
|
||||
match b {
|
||||
Block::Message { text } | Block::Objective { text } | Block::Hint { text } => {
|
||||
im.text_field(gfx, fid(path, 0), Rect::new(*cx, y, CW() * 44.0, fh), text, 200);
|
||||
*cx += CW() * 45.0;
|
||||
}
|
||||
Block::Spawn { side, kind, count, level, place, name } => {
|
||||
self.value(im, gfx, count, Ty::Num, path, 1, cx, y, ctx);
|
||||
word(cx, "×");
|
||||
self.kind_dd(im, gfx, kind, false, path, 2, cx, y);
|
||||
word(cx, &tr("blk.w_level"));
|
||||
self.value(im, gfx, level, Ty::Num, path, 3, cx, y, ctx);
|
||||
self.side_dd(im, gfx, side, false, path, 4, cx, y);
|
||||
word(cx, &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()]),
|
||||
};
|
||||
if let Some(c) = im.dropdown(gfx, fid(path, 5), Rect::new(*cx, y, CW() * 16.0, fh), &cur, &opts) {
|
||||
*place = match c {
|
||||
0 => Place::Core,
|
||||
1 => Place::Event,
|
||||
c => Place::Zone(ctx.zones[c - 2].clone()),
|
||||
};
|
||||
}
|
||||
*cx += CW() * 17.0;
|
||||
word(cx, &tr("blk.w_name"));
|
||||
im.text_field(gfx, fid(path, 6), Rect::new(*cx, y, CW() * 10.0, fh), name, 16);
|
||||
*cx += CW() * 11.0;
|
||||
}
|
||||
Block::Wave { count } | Block::Gold { amount: count } | Block::Mana { amount: count } => self.value(im, gfx, count, Ty::Num, path, 1, cx, y, ctx),
|
||||
Block::SetVar { name, value } | Block::AddVar { name, value } => {
|
||||
im.text_field(gfx, fid(path, 0), Rect::new(*cx, y, CW() * 12.0, fh), name, 24);
|
||||
*cx += CW() * 13.0;
|
||||
word(cx, &tr(if is_set { "blk.w_to" } else { "blk.w_by" }));
|
||||
self.value(im, gfx, value, Ty::Num, path, 1, cx, y, ctx);
|
||||
}
|
||||
Block::If { cond, .. } => self.value(im, gfx, cond, Ty::Bool, path, 1, cx, y, ctx),
|
||||
Block::Repeat { times, .. } => {
|
||||
self.value(im, gfx, times, Ty::Num, path, 1, cx, y, ctx);
|
||||
word(cx, &tr("blk.w_times"));
|
||||
}
|
||||
Block::Win | Block::Lose => {}
|
||||
Block::Reveal { zone } | Block::Camera { zone } => self.zone_dd(im, gfx, zone, path, 1, cx, y, ctx),
|
||||
Block::Terrain { zone, kind } => {
|
||||
self.zone_dd(im, gfx, zone, path, 1, cx, y, ctx);
|
||||
word(cx, &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));
|
||||
if let Some(c) = im.dropdown(gfx, fid(path, 2), Rect::new(*cx, y, CW() * 14.0, fh), &cur, &opts) {
|
||||
*kind = TERRAIN[c].to_string();
|
||||
}
|
||||
*cx += CW() * 15.0;
|
||||
}
|
||||
Block::Kill { zone, side } => {
|
||||
self.side_dd(im, gfx, side, true, path, 2, cx, y);
|
||||
word(cx, &tr("blk.w_in"));
|
||||
self.zone_dd(im, gfx, zone, path, 1, cx, y, ctx);
|
||||
}
|
||||
Block::Trigger { name, on } => {
|
||||
let cur = if name.is_empty() { tr("ed.pick_trigger") } else { name.clone() };
|
||||
if let Some(c) = im.dropdown(gfx, fid(path, 1), Rect::new(*cx, y, CW() * 16.0, fh), &cur, &ctx.triggers) {
|
||||
*name = ctx.triggers[c].clone();
|
||||
}
|
||||
*cx += CW() * 17.0;
|
||||
let opts = vec![tr("blk.on"), tr("blk.off")];
|
||||
if let Some(c) = im.dropdown(gfx, fid(path, 2), Rect::new(*cx, y, CW() * 10.0, fh), &opts[if *on { 0 } else { 1 }], &opts) {
|
||||
*on = c == 0;
|
||||
}
|
||||
*cx += CW() * 11.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn zone_dd(&mut self, im: &mut Im, gfx: &Gfx, zone: &mut String, path: &[usize], f: usize, cx: &mut f32, y: f32, ctx: &Ctx) {
|
||||
let cur = if zone.is_empty() { tr("ed.pick_zone") } else { zone.clone() };
|
||||
if let Some(c) = im.dropdown(gfx, fid(path, f), Rect::new(*cx, y, CW() * 14.0, CH() * 1.5), &cur, &ctx.zones) {
|
||||
*zone = ctx.zones[c].clone();
|
||||
}
|
||||
*cx += CW() * 15.0;
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn side_dd(&mut self, im: &mut Im, gfx: &Gfx, side: &mut i32, any: bool, path: &[usize], f: usize, cx: &mut f32, y: f32) {
|
||||
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();
|
||||
if let Some(c) = im.dropdown(gfx, fid(path, f), Rect::new(*cx, y, CW() * 14.0, CH() * 1.5), &side_label(*side), &opts) {
|
||||
*side = sides[c];
|
||||
}
|
||||
*cx += CW() * 15.0;
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn kind_dd(&mut self, im: &mut Im, gfx: &Gfx, kind: &mut String, any: bool, path: &[usize], f: usize, cx: &mut f32, y: f32) {
|
||||
let mut opts: Vec<String> = Vec::new();
|
||||
if any {
|
||||
opts.push(tr("ed.any_kind"));
|
||||
}
|
||||
opts.extend(KINDS.iter().map(|k| kind_label(k)));
|
||||
if let Some(c) = im.dropdown(gfx, fid(path, f), Rect::new(*cx, y, CW() * 14.0, CH() * 1.5), &kind_label(kind), &opts) {
|
||||
*kind = if any && c == 0 { String::new() } else { KINDS[c - any as usize].to_string() };
|
||||
}
|
||||
*cx += CW() * 15.0;
|
||||
}
|
||||
|
||||
// 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, cx: &mut f32, y: f32, ctx: &Ctx) {
|
||||
let fh = CH() * 1.5;
|
||||
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_ = Rect::new(*cx, y, CW() * 1.6, fh);
|
||||
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;
|
||||
}
|
||||
}
|
||||
*cx += CW() * 1.8;
|
||||
let word = |cx: &mut f32, s: &str| {
|
||||
gfx.text(s, *cx, y + CH() * 0.25, S(), TXT);
|
||||
*cx += gfx.measure(s, S()) + CW() * 0.5;
|
||||
};
|
||||
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 = Rect::new(*cx, y, CW() * 8.0, fh);
|
||||
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;
|
||||
}
|
||||
}
|
||||
*cx += CW() * 8.6;
|
||||
}
|
||||
Val::Bool { b } => {
|
||||
let o = vec![tr("blk.yes"), tr("blk.no")];
|
||||
if let Some(c) = im.dropdown(gfx, fid(path, sub(0)), Rect::new(*cx, y, CW() * 7.0, fh), &o[if *b { 0 } else { 1 }], &o) {
|
||||
*b = c == 0;
|
||||
}
|
||||
*cx += CW() * 7.6;
|
||||
}
|
||||
Val::Var { name } => {
|
||||
let cur = if name.is_empty() { tr("ed.pick_var") } else { name.clone() };
|
||||
if let Some(c) = im.dropdown(gfx, fid(path, sub(0)), Rect::new(*cx, y, CW() * 12.0, fh), &cur, &ctx.vars) {
|
||||
*name = ctx.vars[c].clone();
|
||||
}
|
||||
*cx += CW() * 12.6;
|
||||
}
|
||||
Val::Gold | Val::Mana | Val::Time => word(cx, &tr(&format!("blk.v.{}", v.kind_name()))),
|
||||
Val::Creatures { side, kind } => {
|
||||
word(cx, &tr("blk.v.creatures"));
|
||||
self.kind_dd(im, gfx, kind, true, path, sub(0), cx, y);
|
||||
self.side_dd(im, gfx, side, true, path, sub(1), cx, y);
|
||||
}
|
||||
Val::InZone { zone, side, kind } => {
|
||||
word(cx, &tr("blk.v.inzone"));
|
||||
self.kind_dd(im, gfx, kind, true, path, sub(0), cx, y);
|
||||
self.side_dd(im, gfx, side, true, path, sub(1), cx, y);
|
||||
word(cx, &tr("blk.w_in"));
|
||||
self.zone_dd(im, gfx, zone, path, sub(2), cx, y, ctx);
|
||||
}
|
||||
Val::Random { a, b } => {
|
||||
word(cx, &tr("blk.v.random"));
|
||||
self.value(im, gfx, a, Ty::Num, path, sub(0), cx, y, ctx);
|
||||
word(cx, "..");
|
||||
self.value(im, gfx, b, Ty::Num, path, sub(1), cx, y, ctx);
|
||||
}
|
||||
Val::Math { op, a, b } | Val::Cmp { op, a, b } => {
|
||||
let ops: Vec<String> = if matches!(v_kind_is_math(op), true) { MATH_OPS.iter().map(|s| s.to_string()).collect() } else { CMP_OPS.iter().map(|s| s.to_string()).collect() };
|
||||
word(cx, "(");
|
||||
self.value(im, gfx, a, Ty::Num, path, sub(0), cx, y, ctx);
|
||||
if let Some(c) = im.dropdown(gfx, fid(path, sub(1)), Rect::new(*cx, y, CW() * 4.0, fh), op, &ops) {
|
||||
*op = ops[c].clone();
|
||||
}
|
||||
*cx += CW() * 4.6;
|
||||
self.value(im, gfx, b, Ty::Num, path, sub(2), cx, y, ctx);
|
||||
word(cx, ")");
|
||||
}
|
||||
Val::And { a, b } | Val::Or { a, b } => {
|
||||
word(cx, "(");
|
||||
self.value(im, gfx, a, Ty::Bool, path, sub(0), cx, y, ctx);
|
||||
word(cx, &tr(if and { "blk.and" } else { "blk.or" }));
|
||||
self.value(im, gfx, b, Ty::Bool, path, sub(1), cx, y, ctx);
|
||||
word(cx, ")");
|
||||
}
|
||||
Val::Not { a } => {
|
||||
word(cx, &tr("blk.not"));
|
||||
self.value(im, gfx, a, Ty::Bool, path, sub(0), cx, y, ctx);
|
||||
}
|
||||
Val::EventKind { kind } => {
|
||||
word(cx, &tr("blk.v.eventkind"));
|
||||
self.kind_dd(im, gfx, kind, false, path, sub(0), cx, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user