No fog while painting, readable tool bars, a two-row spawn block, a dialog that keeps the clicks to itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1032 lines
46 KiB
Rust
1032 lines
46 KiB
Rust
// The in-game campaign editor: a campaign as a graph of mission cards, each mission with its
|
|
// settings, goals and branches, its map (random per campaign run, or painted in the map
|
|
// editor) and its block scripts (special scripts and triggers, edited in the block editor).
|
|
//
|
|
// While the editor is open it takes the whole frame: main.rs calls Editor::frame and only
|
|
// handles what it returns (test play, files, the savegame service).
|
|
|
|
pub mod blocks_ui;
|
|
pub mod map;
|
|
pub mod templates;
|
|
pub mod widgets;
|
|
|
|
use crate::campaign_doc::*;
|
|
use crate::gfx::*;
|
|
use crate::i18n::{languages, tr, trf};
|
|
use crate::render3d::R3d;
|
|
use crate::ui::*;
|
|
use macroquad::prelude::*;
|
|
#[allow(unused_imports)]
|
|
use crate::ui::{BLUE, GOLD, GREEN, RED};
|
|
use widgets::*;
|
|
|
|
// which list of blocks is being edited
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub enum ScriptRef {
|
|
Start,
|
|
Victory,
|
|
Defeat,
|
|
Tick,
|
|
Trigger(usize),
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub enum Screen {
|
|
Graph,
|
|
Mission(usize),
|
|
Map(usize),
|
|
Blocks(usize, ScriptRef),
|
|
}
|
|
|
|
// what main.rs has to do for the editor
|
|
pub enum EdAction {
|
|
None,
|
|
Close,
|
|
Test(usize),
|
|
Play, // the whole campaign from its first mission, following the branches
|
|
SaveFile,
|
|
OpenFile,
|
|
ServerSave,
|
|
ServerRefresh, // fetch the own and the public campaign lists
|
|
ServerLoad(String),
|
|
PublicLoad(String, String),
|
|
Publish(bool),
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct Graph {
|
|
pan: Vec2,
|
|
drag: Option<(usize, Vec2, bool)>, // card, grab offset, moved
|
|
link: Option<(usize, Option<usize>)>, // from card, branch (None: the "next" link)
|
|
panning: Option<Vec2>,
|
|
}
|
|
|
|
pub struct Editor {
|
|
pub doc: CampaignDoc,
|
|
pub screen: Screen,
|
|
tab: usize,
|
|
im: Im,
|
|
graph: Graph,
|
|
pub map: Option<map::MapEdit>,
|
|
pub blocks: blocks_ui::BlockEdit,
|
|
pub server_id: Option<String>,
|
|
pub published: bool,
|
|
pub own_list: Vec<(String, String)>, // (id, name) on the server
|
|
pub public_list: Vec<(String, String, String)>, // (owner, id, name)
|
|
open_dialog: bool,
|
|
template_pick: usize,
|
|
status: Option<(String, f64)>,
|
|
confirm_delete: Option<usize>,
|
|
scroll: f32, // scripts tab
|
|
}
|
|
|
|
pub const GOAL_KINDS: [&str; 9] = ["KillAll", "Shards", "Heart", "Towers", "Rivals", "Loot", "Survive", "Springs", "Freed"];
|
|
|
|
fn goal_parts(raw: &str) -> (String, u32) {
|
|
match raw.find('(') {
|
|
Some(p) => (raw[..p].to_string(), raw[p + 1..].trim_end_matches(')').parse().unwrap_or(0)),
|
|
None => (raw.to_string(), 0),
|
|
}
|
|
}
|
|
|
|
fn goal_has_number(kind: &str) -> bool {
|
|
matches!(kind, "Loot" | "Survive" | "Springs" | "Freed")
|
|
}
|
|
|
|
fn goal_raw(kind: &str, n: u32) -> String {
|
|
if goal_has_number(kind) {
|
|
format!("{}({})", kind, n.max(1))
|
|
} else {
|
|
kind.to_string()
|
|
}
|
|
}
|
|
|
|
pub fn goal_label(raw: &str) -> String {
|
|
let (k, n) = goal_parts(raw);
|
|
let name = tr(&format!("ed.goal.{}", k.to_lowercase()));
|
|
if goal_has_number(&k) {
|
|
format!("{} {}", name, n)
|
|
} else {
|
|
name
|
|
}
|
|
}
|
|
|
|
pub fn side_label(side: i32) -> String {
|
|
tr(match side {
|
|
0 => "ed.side.player",
|
|
1 => "ed.side.heroes",
|
|
2 => "ed.side.mossgrim",
|
|
3 => "ed.side.nightwell",
|
|
4 => "ed.side.rival3",
|
|
_ => "ed.side.any",
|
|
})
|
|
}
|
|
|
|
pub fn kind_label(kind: &str) -> String {
|
|
if kind.is_empty() {
|
|
return tr("ed.any_kind");
|
|
}
|
|
match crate::mscript::kind_by_name(kind) {
|
|
Some(k) => tr(crate::game::st(k).name),
|
|
None => kind.to_string(),
|
|
}
|
|
}
|
|
|
|
// a row of tabs; returns the clicked one
|
|
fn tabs(im: &mut Im, gfx: &Gfx, x: f32, y: f32, labels: &[String], cur: usize) -> Option<usize> {
|
|
let mut tx = x;
|
|
let mut out = None;
|
|
for (k, l) in labels.iter().enumerate() {
|
|
let w = gfx.measure(l, S()) + CW() * 2.0;
|
|
let r = Rect::new(tx, y, w, CH() * 1.8);
|
|
draw_rectangle(r.x, r.y, r.w, r.h, if k == cur { Color::new(0.35, 0.22, 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, if k == cur || im.over(r) { FRAME_HI } else { FRAME });
|
|
gfx.text(l, r.x + CW(), r.y + CH() * 0.4, S(), if k == cur { FRAME_HI } else { TXT });
|
|
if im.clicked(r) {
|
|
out = Some(k);
|
|
}
|
|
tx += w + CW() * 0.5;
|
|
}
|
|
out
|
|
}
|
|
|
|
impl Editor {
|
|
pub fn new(doc: CampaignDoc) -> Editor {
|
|
let mut doc = doc;
|
|
if doc.missions.is_empty() {
|
|
doc.add_mission();
|
|
}
|
|
if doc.language.is_empty() {
|
|
doc.language = crate::i18n::language();
|
|
}
|
|
Editor {
|
|
doc,
|
|
screen: Screen::Graph,
|
|
tab: 0,
|
|
im: Im::default(),
|
|
graph: Graph::default(),
|
|
map: None,
|
|
blocks: blocks_ui::BlockEdit::default(),
|
|
server_id: None,
|
|
published: false,
|
|
own_list: Vec::new(),
|
|
public_list: Vec::new(),
|
|
open_dialog: false,
|
|
template_pick: 0,
|
|
status: None,
|
|
confirm_delete: None,
|
|
scroll: 0.0,
|
|
}
|
|
}
|
|
|
|
pub fn set_status(&mut self, s: String) {
|
|
self.status = Some((s, get_time()));
|
|
}
|
|
|
|
// a campaign was loaded (file, server): edit it from the graph
|
|
pub fn load(&mut self, doc: CampaignDoc, server_id: Option<String>) {
|
|
let mut doc = doc;
|
|
if doc.missions.is_empty() {
|
|
doc.add_mission();
|
|
}
|
|
self.doc = doc;
|
|
self.server_id = server_id;
|
|
self.screen = Screen::Graph;
|
|
self.map = None;
|
|
self.open_dialog = false;
|
|
}
|
|
|
|
// the map editor works on its own game: its map has to be the active one while it is open
|
|
pub fn active_map(&mut self) -> Option<&mut crate::game::Game> {
|
|
match self.screen {
|
|
Screen::Map(_) => self.map.as_mut().map(|m| &mut m.game),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn frame(&mut self, gfx: &Gfx, r3d: &mut R3d, dt: f32, now: f64) -> EdAction {
|
|
clear_background(Color::from_rgba(4, 3, 5, 255));
|
|
self.im.begin();
|
|
// an open dialog takes the clicks: the page behind it only draws
|
|
let held = (self.im.pressed, self.im.released, self.im.wheel);
|
|
if self.open_dialog {
|
|
self.im.pressed = false;
|
|
self.im.released = false;
|
|
self.im.wheel = 0.0;
|
|
}
|
|
let mut act = EdAction::None;
|
|
match self.screen {
|
|
Screen::Map(i) => {
|
|
if let Some(m) = self.map.as_mut() {
|
|
match m.frame(&mut self.im, gfx, r3d, dt, now, &mut self.doc.missions[i]) {
|
|
map::MapResult::Done => self.screen = Screen::Mission(i),
|
|
map::MapResult::Stay => {}
|
|
}
|
|
} else {
|
|
self.screen = Screen::Mission(i);
|
|
}
|
|
}
|
|
Screen::Blocks(i, sref) => {
|
|
begin_ui();
|
|
if self.blocks.frame(&mut self.im, gfx, &mut self.doc.missions[i], sref) {
|
|
self.screen = Screen::Mission(i);
|
|
self.tab = 3;
|
|
}
|
|
}
|
|
Screen::Graph => {
|
|
begin_ui();
|
|
act = self.graph_screen(gfx);
|
|
}
|
|
Screen::Mission(i) => {
|
|
begin_ui();
|
|
act = self.mission_screen(gfx, i);
|
|
}
|
|
}
|
|
if self.open_dialog {
|
|
(self.im.pressed, self.im.released, self.im.wheel) = held;
|
|
begin_ui();
|
|
if let Some(a) = self.open_screen(gfx) {
|
|
act = a;
|
|
}
|
|
}
|
|
// status line
|
|
if let Some((s, t)) = self.status.clone() {
|
|
if now - t < 4.0 {
|
|
let w = gfx.measure(&s, S()) + CW() * 2.0;
|
|
let r = Rect::new(grid_center(sw(), w), sh() - CH() * 3.5, w, CH() * 1.6);
|
|
draw_rectangle(r.x, r.y, r.w, r.h, Color::new(0.05, 0.03, 0.04, 0.95));
|
|
draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0, GOLD);
|
|
gfx.text(&s, r.x + CW(), r.y + CH() * 0.3, S(), GOLD);
|
|
} else {
|
|
self.status = None;
|
|
}
|
|
}
|
|
self.im.finish(gfx);
|
|
act
|
|
}
|
|
|
|
// ------------------------------------------------------------------ campaign graph
|
|
|
|
fn graph_screen(&mut self, gfx: &Gfx) -> EdAction {
|
|
let mut act = EdAction::None;
|
|
let (w, h) = (sw(), sh());
|
|
let im = &mut self.im;
|
|
// top bar: the title and the buttons, then name, author, language
|
|
let bar_h = CH() * 5.0;
|
|
panel(gfx, Rect::new(0.0, 0.0, w, bar_h), "");
|
|
gfx.text(&tr("ed.campaign"), CW(), CH() * 0.8, S(), FRAME_HI);
|
|
let labels = [tr("ed.play"), tr("ed.new"), tr("ed.open"), tr("ed.save_file"), tr("ed.save_server"), tr("ed.close")];
|
|
let mut bx = w - CW();
|
|
for k in (0..labels.len()).rev() {
|
|
let bw = gfx.measure(&labels[k], S()) + CW() * 2.0;
|
|
bx -= bw;
|
|
let r = Rect::new(bx, CH() * 0.5, bw, CH() * 1.7);
|
|
let clicked = if k == 0 { im.button_c(gfx, r, &labels[k], GREEN) } else { im.button(gfx, r, &labels[k]) };
|
|
if clicked {
|
|
match k {
|
|
0 => act = EdAction::Play,
|
|
1 => {
|
|
self.doc = CampaignDoc { name: tr("ed.new_campaign"), language: crate::i18n::language(), ..CampaignDoc::default() };
|
|
self.doc.add_mission();
|
|
self.server_id = None;
|
|
}
|
|
2 => {
|
|
self.open_dialog = true;
|
|
act = EdAction::ServerRefresh;
|
|
}
|
|
3 => act = EdAction::SaveFile,
|
|
4 => act = EdAction::ServerSave,
|
|
_ => act = EdAction::Close,
|
|
}
|
|
}
|
|
bx -= CW() * 0.5;
|
|
}
|
|
let y2 = CH() * 2.9;
|
|
let fw = (CW() * 22.0).min(w * 0.2);
|
|
let mut x = CW();
|
|
label(gfx, x, y2 + CH() * 0.25, &tr("ed.name"), DIM);
|
|
x += gfx.measure(&tr("ed.name"), S()) + CW();
|
|
im.text_field(gfx, id("cname", 0), Rect::new(x, y2, fw, CH() * 1.5), &mut self.doc.name, 40);
|
|
x += fw + CW() * 1.5;
|
|
label(gfx, x, y2 + CH() * 0.25, &tr("ed.author"), DIM);
|
|
x += gfx.measure(&tr("ed.author"), S()) + CW();
|
|
im.text_field(gfx, id("cauthor", 0), Rect::new(x, y2, fw * 0.8, CH() * 1.5), &mut self.doc.author, 32);
|
|
x += fw * 0.8 + CW() * 1.5;
|
|
label(gfx, x, y2 + CH() * 0.25, &tr("ed.language"), DIM);
|
|
x += gfx.measure(&tr("ed.language"), S()) + CW();
|
|
let langs = languages();
|
|
let names: Vec<String> = langs.iter().map(|(_, n)| n.clone()).collect();
|
|
let cur = langs.iter().find(|(c, _)| *c == self.doc.language).map(|(_, n)| n.clone()).unwrap_or_else(|| self.doc.language.clone());
|
|
if let Some(k) = im.dropdown(gfx, id("clang", 0), Rect::new(x, y2, CW() * 14.0, CH() * 1.5), &cur, &names) {
|
|
self.doc.language = langs[k].0.clone();
|
|
}
|
|
x += CW() * 15.5;
|
|
let mut publ = self.published;
|
|
if self.server_id.is_some() && im.toggle(gfx, x, y2, &tr("ed.published"), &mut publ) {
|
|
self.published = publ;
|
|
act = EdAction::Publish(publ);
|
|
}
|
|
let al = tr("ed.add_mission");
|
|
let aw = gfx.measure(&al, S()) + CW() * 2.0;
|
|
if im.button(gfx, Rect::new(w - CW() - aw, y2 - CH() * 0.1, aw, CH() * 1.7), &al) {
|
|
let n = self.doc.missions.len();
|
|
self.doc.add_mission();
|
|
self.doc.missions[n].pos = (40.0 + (n % 5) as f32 * 220.0, 40.0 + (n / 5) as f32 * 140.0);
|
|
}
|
|
|
|
// the canvas: cards and arrows
|
|
let top = bar_h;
|
|
let card_w = CW() * 26.0;
|
|
let card_h = CH() * 7.0;
|
|
let scale = CW() / 8.0; // card positions are stored in a font-independent unit
|
|
let origin = vec2(CW(), top + CH()) + self.graph.pan;
|
|
let card_rect = |m: &MissionDoc| Rect::new(origin.x + m.pos.0 * scale, origin.y + m.pos.1 * scale, card_w, card_h);
|
|
let n = self.doc.missions.len();
|
|
let rects: Vec<Rect> = self.doc.missions.iter().map(card_rect).collect();
|
|
// ports: "next" on the right edge, branches below it
|
|
let port = |k: usize, b: Option<usize>| {
|
|
let r = rects[k];
|
|
match b {
|
|
None => vec2(r.x + r.w, r.y + CH() * 1.2),
|
|
Some(j) => vec2(r.x + r.w, r.y + CH() * (2.8 + j as f32 * 1.2)),
|
|
}
|
|
};
|
|
let key_index = |key: &str| self.doc.missions.iter().position(|m| m.key == key);
|
|
// arrows first (under the cards)
|
|
for k in 0..n {
|
|
let m = &self.doc.missions[k];
|
|
let mut links: Vec<(Option<usize>, usize, Color)> = Vec::new();
|
|
if let Some(t) = m.next.as_deref().and_then(key_index) {
|
|
links.push((None, t, FRAME_HI));
|
|
}
|
|
for (j, b) in m.branches.iter().enumerate() {
|
|
if let Some(t) = key_index(&b.next) {
|
|
links.push((Some(j), t, BLUE));
|
|
}
|
|
}
|
|
for (b, t, col) in links {
|
|
let a = port(k, b);
|
|
let tr_ = rects[t];
|
|
let e = vec2(tr_.x, tr_.y + tr_.h * 0.5);
|
|
let mid = (a.x + e.x) * 0.5;
|
|
draw_line(a.x, a.y, mid, a.y, 3.0, col);
|
|
draw_line(mid, a.y, mid, e.y, 3.0, col);
|
|
draw_line(mid, e.y, e.x, e.y, 3.0, col);
|
|
draw_triangle(vec2(e.x, e.y), vec2(e.x - CW(), e.y - CH() * 0.4), vec2(e.x - CW(), e.y + CH() * 0.4), col);
|
|
}
|
|
}
|
|
// a link being dragged
|
|
if let Some((from, b)) = self.graph.link {
|
|
let a = port(from, b);
|
|
draw_line(a.x, a.y, im.mx, im.my, 3.0, GOLD);
|
|
}
|
|
let mut open: Option<usize> = None;
|
|
let mut delete: Option<usize> = None;
|
|
for k in 0..n {
|
|
let r = rects[k];
|
|
let m = &self.doc.missions[k];
|
|
let probs = m.script_problems().len();
|
|
let hot = im.over(r) && self.graph.drag.is_none();
|
|
draw_rectangle(r.x, r.y, r.w, r.h, Color::new(0.1, 0.07, 0.06, 0.97));
|
|
draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0, if hot { FRAME_HI } else { FRAME });
|
|
let title: String = m.title.chars().take(22).collect();
|
|
gfx.text(&title, r.x + CW() * 0.6, r.y + CH() * 0.4, S(), FRAME_HI);
|
|
let map_kind = tr(match m.map {
|
|
MapDoc::Random => "ed.map_random",
|
|
MapDoc::Fixed { .. } => "ed.map_fixed",
|
|
});
|
|
gfx.text(&format!("{} {}x{}", map_kind, m.size.0, m.size.1), r.x + CW() * 0.6, r.y + CH() * 1.8, SMALL(), DIM);
|
|
let goals: Vec<String> = m.goals.iter().map(|g| goal_label(g)).collect();
|
|
let gl: String = goals.join(", ").chars().take(30).collect();
|
|
gfx.text(&gl, r.x + CW() * 0.6, r.y + CH() * 3.0, SMALL(), TXT);
|
|
let sc = trf("ed.card_scripts", &[m.scripts.triggers.len().to_string()]);
|
|
gfx.text(&sc, r.x + CW() * 0.6, r.y + CH() * 4.2, SMALL(), DIM);
|
|
if probs > 0 {
|
|
gfx.text(&trf("ed.card_problems", &[probs.to_string()]), r.x + CW() * 0.6, r.y + CH() * 5.4, SMALL(), RED);
|
|
}
|
|
// ports
|
|
let pn = port(k, None);
|
|
draw_circle(pn.x, pn.y, CW() * 0.5, FRAME_HI);
|
|
for j in 0..m.branches.len() {
|
|
let p = port(k, Some(j));
|
|
draw_circle(p.x, p.y, CW() * 0.45, BLUE);
|
|
}
|
|
let pnew = port(k, Some(m.branches.len()));
|
|
draw_circle_lines(pnew.x, pnew.y, CW() * 0.45, 2.0, BLUE);
|
|
// delete
|
|
let xr = Rect::new(r.x + r.w - CW() * 1.8, r.y + CH() * 0.2, CW() * 1.5, CH() * 1.2);
|
|
if hot {
|
|
let sure = self.confirm_delete == Some(k);
|
|
gfx.text(if sure { "?" } else { "x" }, xr.x + CW() * 0.4, xr.y, S(), if sure { RED } else { DIM });
|
|
if n > 1 && im.clicked(xr) {
|
|
if sure {
|
|
delete = Some(k);
|
|
} else {
|
|
self.confirm_delete = Some(k);
|
|
}
|
|
}
|
|
}
|
|
// start a link at a port, or grab the card
|
|
if im.pressed && self.graph.link.is_none() {
|
|
let near = |p: Vec2| (p - vec2(im.mx, im.my)).length() < CW();
|
|
if near(pn) {
|
|
self.graph.link = Some((k, None));
|
|
im.pressed = false;
|
|
} else if let Some(j) = (0..=m.branches.len()).find(|&j| near(port(k, Some(j)))) {
|
|
self.graph.link = Some((k, Some(j)));
|
|
im.pressed = false;
|
|
} else if im.over(r) {
|
|
self.graph.drag = Some((k, vec2(im.mx - r.x, im.my - r.y), false));
|
|
im.pressed = false;
|
|
}
|
|
}
|
|
}
|
|
// dragging a card
|
|
if let Some((k, off, moved)) = self.graph.drag {
|
|
if im.down {
|
|
let p = vec2(im.mx, im.my) - off - origin;
|
|
let np = ((p.x / scale).round(), (p.y / scale).round());
|
|
let m = &mut self.doc.missions[k];
|
|
let moved = moved || (np.0 - m.pos.0).abs() + (np.1 - m.pos.1).abs() > 2.0;
|
|
if moved {
|
|
m.pos = np;
|
|
}
|
|
self.graph.drag = Some((k, off, moved));
|
|
} else {
|
|
if !moved {
|
|
open = Some(k);
|
|
}
|
|
self.graph.drag = None;
|
|
}
|
|
}
|
|
// dropping a link on a card (or on empty space: the link is removed)
|
|
if let Some((from, b)) = self.graph.link {
|
|
if !im.down {
|
|
let target = rects.iter().position(|r| im.over(*r)).filter(|&t| t != from);
|
|
let key = target.map(|t| self.doc.missions[t].key.clone());
|
|
let m = &mut self.doc.missions[from];
|
|
match b {
|
|
None => m.next = key,
|
|
Some(j) => match key {
|
|
Some(key) if j < m.branches.len() => m.branches[j].next = key,
|
|
Some(key) => m.branches.push(BranchDoc { id: format!("b{}", j + 1), goal: "KillAll".into(), title: String::new(), next: key, priority: None }),
|
|
None if j < m.branches.len() => {
|
|
m.branches.remove(j);
|
|
}
|
|
None => {}
|
|
},
|
|
}
|
|
self.graph.link = None;
|
|
}
|
|
}
|
|
// panning the canvas (right button or middle button drag)
|
|
if is_mouse_button_down(MouseButton::Right) || is_mouse_button_down(MouseButton::Middle) {
|
|
let p = vec2(im.mx, im.my);
|
|
if let Some(last) = self.graph.panning {
|
|
self.graph.pan += p - last;
|
|
}
|
|
self.graph.panning = Some(p);
|
|
} else {
|
|
self.graph.panning = None;
|
|
}
|
|
if let Some(k) = delete {
|
|
let key = self.doc.missions[k].key.clone();
|
|
self.doc.missions.remove(k);
|
|
for m in self.doc.missions.iter_mut() {
|
|
if m.next.as_deref() == Some(key.as_str()) {
|
|
m.next = None;
|
|
}
|
|
m.branches.retain(|b| b.next != key);
|
|
}
|
|
self.confirm_delete = None;
|
|
}
|
|
if let Some(k) = open {
|
|
self.screen = Screen::Mission(k);
|
|
self.tab = 0;
|
|
self.confirm_delete = None;
|
|
}
|
|
let hint = tr("ed.graph_hint");
|
|
gfx.text(&hint, CW(), h - CH() * 1.5, SMALL(), DIM);
|
|
act
|
|
}
|
|
|
|
// ------------------------------------------------------------------ open dialog
|
|
|
|
fn open_screen(&mut self, gfx: &Gfx) -> Option<EdAction> {
|
|
let (w, h) = (sw(), sh());
|
|
let pw = (CW() * 70.0).min(w - CW() * 2.0);
|
|
let ph = (CH() * 34.0).min(h - CH() * 2.0);
|
|
let r = Rect::new(grid_center(w, pw), grid_center_y(h, ph), pw, ph);
|
|
draw_rectangle(0.0, 0.0, w, h, Color::new(0.0, 0.0, 0.0, 0.5));
|
|
panel(gfx, r, &tr("ed.open"));
|
|
let im = &mut self.im;
|
|
let mut out = None;
|
|
let x = r.x + CW() * 1.5;
|
|
let mut y = r.y + CH() * 2.0;
|
|
let bw = (pw - CW() * 4.0) * 0.5;
|
|
if im.button(gfx, Rect::new(x, y, bw, CH() * 1.7), &tr("ed.open_file")) {
|
|
out = Some(EdAction::OpenFile);
|
|
self.open_dialog = false;
|
|
}
|
|
if im.button(gfx, Rect::new(x + bw + CW(), y, bw, CH() * 1.7), &tr("ed.open_demo")) {
|
|
self.load(templates::demo_campaign(), None);
|
|
self.open_dialog = false;
|
|
return None;
|
|
}
|
|
y += CH() * 2.6;
|
|
// a mission of the built-in campaign as a copy
|
|
label(gfx, x, y, &tr("ed.template"), FRAME_HI);
|
|
y += CH() * 1.4;
|
|
let names: Vec<String> = (0..crate::campaign::missions().len()).map(|i| format!("{}. {}", i + 1, tr(&format!("camp.{}.title", crate::campaign::missions()[i].key)))).collect();
|
|
let cur = names.get(self.template_pick).cloned().unwrap_or_default();
|
|
if let Some(k) = im.dropdown(gfx, id("tpl", 0), Rect::new(x, y, bw * 1.3, CH() * 1.6), &cur, &names) {
|
|
self.template_pick = k;
|
|
}
|
|
if im.button(gfx, Rect::new(x + bw * 1.3 + CW(), y, bw * 0.7 - CW(), CH() * 1.6), &tr("ed.template_add")) {
|
|
let m = templates::builtin_mission(self.template_pick, &self.doc);
|
|
let n = self.doc.missions.len();
|
|
self.doc.missions.push(m);
|
|
self.doc.missions[n].pos = (40.0 + (n % 5) as f32 * 220.0, 40.0 + (n / 5) as f32 * 140.0);
|
|
self.open_dialog = false;
|
|
self.screen = Screen::Graph;
|
|
return None;
|
|
}
|
|
y += CH() * 2.6;
|
|
// the own campaigns on the server, and the published ones of all players
|
|
let half = (pw - CW() * 4.0) * 0.5;
|
|
label(gfx, x, y, &tr("ed.own_campaigns"), FRAME_HI);
|
|
label(gfx, x + half + CW(), y, &tr("ed.public_campaigns"), FRAME_HI);
|
|
y += CH() * 1.4;
|
|
let rows = (((r.y + r.h - CH() * 3.0) - y) / (CH() * 1.5)).max(1.0) as usize;
|
|
if self.own_list.is_empty() {
|
|
gfx.text(&tr("ed.none"), x, y, SMALL(), DIM);
|
|
}
|
|
for (k, (sid, name)) in self.own_list.iter().take(rows).enumerate() {
|
|
if im.select(gfx, Rect::new(x, y + k as f32 * CH() * 1.5, half, CH() * 1.4), name, false, TXT) {
|
|
out = Some(EdAction::ServerLoad(sid.clone()));
|
|
}
|
|
}
|
|
if self.public_list.is_empty() {
|
|
gfx.text(&tr("ed.none"), x + half + CW(), y, SMALL(), DIM);
|
|
}
|
|
for (k, (owner, pid, name)) in self.public_list.iter().take(rows).enumerate() {
|
|
if im.select(gfx, Rect::new(x + half + CW(), y + k as f32 * CH() * 1.5, half, CH() * 1.4), &format!("{} ({})", name, owner), false, TXT) {
|
|
out = Some(EdAction::PublicLoad(owner.clone(), pid.clone()));
|
|
}
|
|
}
|
|
if im.button(gfx, Rect::new(r.x + r.w - CW() * 12.0, r.y + r.h - CH() * 2.3, CW() * 10.5, CH() * 1.7), &tr("ed.cancel")) || is_key_pressed(KeyCode::Escape) {
|
|
self.open_dialog = false;
|
|
}
|
|
if out.is_some() {
|
|
self.open_dialog = false;
|
|
}
|
|
out
|
|
}
|
|
|
|
// ------------------------------------------------------------------ one mission
|
|
|
|
fn mission_screen(&mut self, gfx: &Gfx, i: usize) -> EdAction {
|
|
let mut act = EdAction::None;
|
|
let (w, h) = (sw(), sh());
|
|
if i >= self.doc.missions.len() {
|
|
self.screen = Screen::Graph;
|
|
return act;
|
|
}
|
|
panel(gfx, Rect::new(0.0, 0.0, w, h), "");
|
|
let im = &mut self.im;
|
|
if im.button(gfx, Rect::new(CW(), CH() * 0.6, CW() * 14.0, CH() * 1.7), &tr("ed.back_graph")) || (is_key_pressed(KeyCode::Escape) && !im.typing) {
|
|
self.screen = Screen::Graph;
|
|
return act;
|
|
}
|
|
let title: String = self.doc.missions[i].title.chars().take(40).collect();
|
|
gfx.text(&title, CW() * 16.5, CH() * 0.9, S(), FRAME_HI);
|
|
let tl = tr("ed.test");
|
|
let tw = gfx.measure(&tl, S()) + CW() * 3.0;
|
|
if im.button_c(gfx, Rect::new(w - tw - CW(), CH() * 0.6, tw, CH() * 1.7), &tl, GREEN) {
|
|
act = EdAction::Test(i);
|
|
}
|
|
let labels = [tr("ed.tab_general"), tr("ed.tab_goals"), tr("ed.tab_map"), tr("ed.tab_scripts")];
|
|
if let Some(t) = tabs(im, gfx, CW(), CH() * 3.0, &labels, self.tab) {
|
|
self.tab = t;
|
|
self.scroll = 0.0;
|
|
}
|
|
let area = Rect::new(CW(), CH() * 5.5, w - CW() * 2.0, h - CH() * 6.5);
|
|
match self.tab {
|
|
0 => self.general_tab(gfx, i, area),
|
|
1 => self.goals_tab(gfx, i, area),
|
|
2 => self.map_tab(gfx, i, area),
|
|
_ => self.scripts_tab(gfx, i, area),
|
|
}
|
|
act
|
|
}
|
|
|
|
fn general_tab(&mut self, gfx: &Gfx, i: usize, a: Rect) {
|
|
let im = &mut self.im;
|
|
let m = &mut self.doc.missions[i];
|
|
let lx = a.x + CW();
|
|
let fx = a.x + CW() * 22.0;
|
|
let fw = (a.w - CW() * 24.0).min(CW() * 60.0);
|
|
let mut y = a.y + CH() * 0.5;
|
|
label(gfx, lx, y + CH() * 0.25, &tr("ed.title"), TXT);
|
|
im.text_field(gfx, id("mtitle", i), Rect::new(fx, y, fw, CH() * 1.6), &mut m.title, 48);
|
|
y += CH() * 2.2;
|
|
label(gfx, lx, y + CH() * 0.25, &tr("ed.briefing"), TXT);
|
|
im.text_area(gfx, id("mstory", i), Rect::new(fx, y, fw, CH() * 9.0), &mut m.story, 1500);
|
|
y += CH() * 9.7;
|
|
let sw_ = CW() * 18.0;
|
|
label(gfx, lx, y + CH() * 0.25, &tr("ed.gold"), TXT);
|
|
let d = im.stepper(gfx, Rect::new(fx, y, sw_, CH() * 1.6), &format!("{}", m.gold as i32));
|
|
m.gold = (m.gold + d as f32 * 250.0).clamp(0.0, 100000.0);
|
|
y += CH() * 2.1;
|
|
label(gfx, lx, y + CH() * 0.25, &tr("ed.time_limit"), TXT);
|
|
let tl = if m.time_limit <= 0.0 { tr("ed.none") } else { format!("{} min", (m.time_limit / 60.0).round()) };
|
|
let d = im.stepper(gfx, Rect::new(fx, y, sw_, CH() * 1.6), &tl);
|
|
m.time_limit = (m.time_limit + d as f32 * 60.0).clamp(0.0, 600.0 * 60.0);
|
|
y += CH() * 2.1;
|
|
im.toggle(gfx, lx, y, &tr("ed.waves"), &mut m.waves);
|
|
if m.waves {
|
|
label(gfx, fx, y + CH() * 0.25, &tr("ed.wave_limit"), DIM);
|
|
let wl = if m.wave_limit == 0 { tr("ed.endless") } else { m.wave_limit.to_string() };
|
|
let d = im.stepper(gfx, Rect::new(fx + CW() * 16.0, y, sw_, CH() * 1.6), &wl);
|
|
m.wave_limit = (m.wave_limit as i32 + d).clamp(0, 99) as u32;
|
|
y += CH() * 2.1;
|
|
label(gfx, fx, y + CH() * 0.25, &tr("ed.first_wave"), DIM);
|
|
let d = im.stepper(gfx, Rect::new(fx + CW() * 16.0, y, sw_, CH() * 1.6), &format!("{} s", m.first_wave as i32));
|
|
m.first_wave = (m.first_wave + d as f32 * 30.0).clamp(10.0, 3600.0);
|
|
}
|
|
}
|
|
|
|
fn goals_tab(&mut self, gfx: &Gfx, i: usize, a: Rect) {
|
|
let im = &mut self.im;
|
|
let titles: Vec<String> = self.doc.missions.iter().map(|m| m.title.clone()).collect();
|
|
let keys: Vec<String> = self.doc.missions.iter().map(|m| m.key.clone()).collect();
|
|
let m = &mut self.doc.missions[i];
|
|
let goal_names: Vec<String> = GOAL_KINDS.iter().map(|k| tr(&format!("ed.goal.{}", k.to_lowercase()))).collect();
|
|
let lx = a.x + CW();
|
|
let mut y = a.y + CH() * 0.5;
|
|
label(gfx, lx, y, &tr("ed.goals"), FRAME_HI);
|
|
y += CH() * 1.6;
|
|
gfx.text(&tr("ed.goals_hint"), lx, y, SMALL(), DIM);
|
|
y += CH() * 1.4;
|
|
let mut remove = None;
|
|
for k in 0..m.goals.len() {
|
|
let (kind, n) = goal_parts(&m.goals[k]);
|
|
let cur = tr(&format!("ed.goal.{}", kind.to_lowercase()));
|
|
if let Some(c) = im.dropdown(gfx, id("goal", i * 100 + k), Rect::new(lx, y, CW() * 26.0, CH() * 1.6), &cur, &goal_names) {
|
|
m.goals[k] = goal_raw(GOAL_KINDS[c], n.max(1) * if goal_has_number(GOAL_KINDS[c]) { 1 } else { 0 });
|
|
if goal_has_number(GOAL_KINDS[c]) && n == 0 {
|
|
m.goals[k] = goal_raw(GOAL_KINDS[c], if GOAL_KINDS[c] == "Loot" { 3000 } else { 3 });
|
|
}
|
|
}
|
|
if goal_has_number(&kind) {
|
|
let step = if kind == "Loot" { 500 } else { 1 };
|
|
let d = im.stepper(gfx, Rect::new(lx + CW() * 27.0, y, CW() * 14.0, CH() * 1.6), &n.to_string());
|
|
if d != 0 {
|
|
m.goals[k] = goal_raw(&kind, (n as i32 + d * step).max(1) as u32);
|
|
}
|
|
}
|
|
if im.button(gfx, Rect::new(lx + CW() * 42.0, y, CW() * 3.0, CH() * 1.6), "x") {
|
|
remove = Some(k);
|
|
}
|
|
y += CH() * 2.0;
|
|
}
|
|
if let Some(k) = remove {
|
|
m.goals.remove(k);
|
|
}
|
|
if im.button(gfx, Rect::new(lx, y, CW() * 18.0, CH() * 1.6), &tr("ed.add_goal")) {
|
|
m.goals.push("KillAll".into());
|
|
}
|
|
y += CH() * 3.0;
|
|
// the next mission
|
|
label(gfx, lx, y + CH() * 0.25, &tr("ed.next_mission"), FRAME_HI);
|
|
let mut opts = vec![tr("ed.none")];
|
|
opts.extend(titles.iter().cloned());
|
|
let cur = m.next.as_ref().and_then(|n| keys.iter().position(|k| k == n)).map(|p| titles[p].clone()).unwrap_or_else(|| tr("ed.none"));
|
|
if let Some(c) = im.dropdown(gfx, id("next", i), Rect::new(lx + CW() * 22.0, y, CW() * 30.0, CH() * 1.6), &cur, &opts) {
|
|
m.next = if c == 0 { None } else { Some(keys[c - 1].clone()) };
|
|
}
|
|
y += CH() * 3.0;
|
|
// branches: another goal reached leads to another mission
|
|
label(gfx, lx, y, &tr("ed.branches"), FRAME_HI);
|
|
y += CH() * 1.6;
|
|
gfx.text(&tr("ed.branches_hint"), lx, y, SMALL(), DIM);
|
|
y += CH() * 1.4;
|
|
let mut remove = None;
|
|
for k in 0..m.branches.len() {
|
|
let (kind, n) = goal_parts(&m.branches[k].goal);
|
|
let cur = tr(&format!("ed.goal.{}", kind.to_lowercase()));
|
|
if let Some(c) = im.dropdown(gfx, id("bgoal", i * 100 + k), Rect::new(lx, y, CW() * 22.0, CH() * 1.6), &cur, &goal_names) {
|
|
m.branches[k].goal = goal_raw(GOAL_KINDS[c], if GOAL_KINDS[c] == "Loot" { 3000 } else { 3 });
|
|
}
|
|
let mut bx = lx + CW() * 23.0;
|
|
if goal_has_number(&kind) {
|
|
let step = if kind == "Loot" { 500 } else { 1 };
|
|
let d = im.stepper(gfx, Rect::new(bx, y, CW() * 12.0, CH() * 1.6), &n.to_string());
|
|
if d != 0 {
|
|
m.branches[k].goal = goal_raw(&kind, (n as i32 + d * step).max(1) as u32);
|
|
}
|
|
bx += CW() * 13.0;
|
|
}
|
|
gfx.text("→", bx, y + CH() * 0.3, S(), FRAME_HI);
|
|
let cur = keys.iter().position(|kk| *kk == m.branches[k].next).map(|p| titles[p].clone()).unwrap_or_else(|| tr("ed.none"));
|
|
if let Some(c) = im.dropdown(gfx, id("bnext", i * 100 + k), Rect::new(bx + CW() * 2.0, y, CW() * 24.0, CH() * 1.6), &cur, &titles) {
|
|
m.branches[k].next = keys[c].clone();
|
|
}
|
|
if im.button(gfx, Rect::new(bx + CW() * 27.0, y, CW() * 3.0, CH() * 1.6), "x") {
|
|
remove = Some(k);
|
|
}
|
|
y += CH() * 2.0;
|
|
}
|
|
if let Some(k) = remove {
|
|
m.branches.remove(k);
|
|
}
|
|
if im.button(gfx, Rect::new(lx, y, CW() * 18.0, CH() * 1.6), &tr("ed.add_branch")) && !keys.is_empty() {
|
|
let n = m.branches.len() + 1;
|
|
let target = keys.iter().find(|k| **k != m.key).cloned().unwrap_or_else(|| keys[0].clone());
|
|
m.branches.push(BranchDoc { id: format!("b{}", n), goal: "Loot(3000)".into(), title: String::new(), next: target, priority: Some(n as u32) });
|
|
}
|
|
}
|
|
|
|
fn map_tab(&mut self, gfx: &Gfx, i: usize, a: Rect) {
|
|
let im = &mut self.im;
|
|
let m = &mut self.doc.missions[i];
|
|
let lx = a.x + CW();
|
|
let fx = a.x + CW() * 24.0;
|
|
let sw_ = CW() * 16.0;
|
|
let mut y = a.y + CH() * 0.5;
|
|
let fixed = matches!(m.map, MapDoc::Fixed { .. });
|
|
gfx.text(&tr(if fixed { "ed.map_is_fixed" } else { "ed.map_is_random" }), lx, y, S(), FRAME_HI);
|
|
y += CH() * 1.6;
|
|
gfx.text(&tr(if fixed { "ed.map_fixed_hint" } else { "ed.map_random_hint" }), lx, y, SMALL(), DIM);
|
|
y += CH() * 2.0;
|
|
let mut rows: Vec<(String, String, i32)> = vec![
|
|
(tr("ed.map_width"), m.size.0.to_string(), 0),
|
|
(tr("ed.map_height"), m.size.1.to_string(), 1),
|
|
(tr("ed.map_seed"), m.seed.to_string(), 2),
|
|
(tr("ed.rivers"), m.gen.rivers.to_string(), 3),
|
|
(tr("ed.lava_rivers"), m.gen.lava.to_string(), 4),
|
|
(tr("ed.forts"), m.gen.forts.to_string(), 5),
|
|
(tr("ed.rivals"), m.gen.rivals.to_string(), 6),
|
|
(tr("ed.difficulty"), tr(["diff.0", "diff.1", "diff.2"][m.gen.difficulty.min(2) as usize]), 7),
|
|
(tr("ed.portals"), m.portals.to_string(), 8),
|
|
];
|
|
if fixed {
|
|
rows.truncate(0);
|
|
}
|
|
for (name, val, k) in rows {
|
|
label(gfx, lx, y + CH() * 0.25, &name, TXT);
|
|
let d = im.stepper(gfx, Rect::new(fx, y, sw_, CH() * 1.6), &val);
|
|
if d != 0 {
|
|
match k {
|
|
0 => m.size.0 = (m.size.0 as i32 + d.signum() * 8).clamp(48, 256) as usize,
|
|
1 => m.size.1 = (m.size.1 as i32 + d.signum() * 8).clamp(48, 256) as usize,
|
|
2 => m.seed = (m.seed as i64 + d as i64).max(0) as u32,
|
|
3 => m.gen.rivers = (m.gen.rivers as i32 + d.signum()).clamp(0, 3) as u8,
|
|
4 => m.gen.lava = (m.gen.lava as i32 + d.signum()).clamp(0, 2) as u8,
|
|
5 => m.gen.forts = (m.gen.forts as i32 + d.signum()).clamp(0, 3) as u8,
|
|
6 => m.gen.rivals = (m.gen.rivals as i32 + d.signum()).clamp(0, 2) as u8,
|
|
7 => m.gen.difficulty = (m.gen.difficulty as i32 + d.signum()).clamp(0, 2) as u8,
|
|
_ => m.portals = (m.portals as i32 + d.signum()).clamp(0, 6) as usize,
|
|
}
|
|
}
|
|
y += CH() * 2.0;
|
|
}
|
|
y += CH();
|
|
let bw = CW() * 30.0;
|
|
if fixed {
|
|
if im.button_c(gfx, Rect::new(lx, y, bw, CH() * 1.8), &tr("ed.edit_map"), GREEN) {
|
|
if let MapDoc::Fixed { game } = &m.map {
|
|
if let Some(g) = crate::campaign::load_fixed_map(game) {
|
|
self.map = Some(map::MapEdit::new(g));
|
|
self.screen = Screen::Map(i);
|
|
}
|
|
}
|
|
}
|
|
if im.button(gfx, Rect::new(lx + bw + CW(), y, bw, CH() * 1.8), &tr("ed.make_random")) {
|
|
m.map = MapDoc::Random;
|
|
}
|
|
} else if im.button_c(gfx, Rect::new(lx, y, bw, CH() * 1.8), &tr("ed.paint_map"), GREEN) {
|
|
// the generator's map becomes the start of a painted map
|
|
let g = crate::campaign::start_editor_mission(&MissionDoc { scripts: MissionScripts::default(), map: MapDoc::Random, ..m.clone() }, i, m.seed.max(1).wrapping_mul(7));
|
|
self.map = Some(map::MapEdit::new(g));
|
|
self.screen = Screen::Map(i);
|
|
}
|
|
y += CH() * 3.0;
|
|
let zones: Vec<String> = m.zones.iter().map(|z| z.name.clone()).collect();
|
|
gfx.text(&trf("ed.zones_list", &[zones.len().to_string(), zones.join(", ").chars().take(80).collect()]), lx, y, SMALL(), DIM);
|
|
}
|
|
|
|
fn scripts_tab(&mut self, gfx: &Gfx, i: usize, a: Rect) {
|
|
let im = &mut self.im;
|
|
let lx = a.x + CW();
|
|
let mut y = a.y + CH() * 0.5 - self.scroll;
|
|
let mut goto: Option<ScriptRef> = None;
|
|
{
|
|
let m = &self.doc.missions[i];
|
|
label(gfx, lx, y, &tr("ed.special_scripts"), FRAME_HI);
|
|
y += CH() * 1.8;
|
|
for (sref, key, list) in [
|
|
(ScriptRef::Start, "ed.script_start", &m.scripts.start),
|
|
(ScriptRef::Victory, "ed.script_victory", &m.scripts.victory),
|
|
(ScriptRef::Defeat, "ed.script_defeat", &m.scripts.defeat),
|
|
(ScriptRef::Tick, "ed.script_tick", &m.scripts.tick),
|
|
] {
|
|
let probs = m.problems(list).len();
|
|
label(gfx, lx, y + CH() * 0.25, &tr(key), TXT);
|
|
let info = if probs > 0 { trf("ed.blocks_problems", &[list.len().to_string(), probs.to_string()]) } else { trf("ed.blocks_count", &[list.len().to_string()]) };
|
|
gfx.text(&info, lx + CW() * 34.0, y + CH() * 0.25, SMALL(), if probs > 0 { RED } else { DIM });
|
|
if im.button(gfx, Rect::new(lx + CW() * 54.0, y, CW() * 14.0, CH() * 1.6), &tr("ed.edit_blocks")) {
|
|
goto = Some(sref);
|
|
}
|
|
y += CH() * 2.0;
|
|
}
|
|
}
|
|
y += CH();
|
|
label(gfx, lx, y, &tr("ed.triggers"), FRAME_HI);
|
|
y += CH() * 1.6;
|
|
gfx.text(&tr("ed.triggers_hint"), lx, y, SMALL(), DIM);
|
|
y += CH() * 1.6;
|
|
let zones = self.doc.missions[i].zone_names();
|
|
let vars = self.doc.missions[i].vars();
|
|
let kind_names: Vec<String> = TRIGGER_KINDS.iter().map(|k| tr(&format!("ed.trig.{}", k))).collect();
|
|
let mut remove = None;
|
|
let n = self.doc.missions[i].scripts.triggers.len();
|
|
for k in 0..n {
|
|
let probs = {
|
|
let m = &self.doc.missions[i];
|
|
let t = &m.scripts.triggers[k];
|
|
m.problems(&t.script).len() + t.kind.zone().map_or(0, |z| (!zones.iter().any(|x| x == z)) as usize)
|
|
};
|
|
let row = Rect::new(lx - CW() * 0.5, y - CH() * 0.3, a.w - CW(), CH() * 4.6);
|
|
draw_rectangle_lines(row.x, row.y, row.w, row.h, 1.0, if probs > 0 { RED } else { Color::new(0.3, 0.24, 0.18, 1.0) });
|
|
let t = &mut self.doc.missions[i].scripts.triggers[k];
|
|
im.text_field(gfx, id("tname", i * 1000 + k), Rect::new(lx, y, CW() * 18.0, CH() * 1.6), &mut t.name, 24);
|
|
let cur = tr(&format!("ed.trig.{}", t.kind.kind_name()));
|
|
if let Some(c) = im.dropdown(gfx, id("tkind", i * 1000 + k), Rect::new(lx + CW() * 19.0, y, CW() * 24.0, CH() * 1.6), &cur, &kind_names) {
|
|
if let Some(nk) = TriggerKind::template(TRIGGER_KINDS[c]) {
|
|
t.kind = nk;
|
|
if let TriggerKind::Timer { every: true, .. } = t.kind {
|
|
t.once = false;
|
|
}
|
|
}
|
|
}
|
|
im.toggle(gfx, lx + CW() * 44.0, y, &tr("ed.once"), &mut t.once);
|
|
im.toggle(gfx, lx + CW() * 54.0, y, &tr("ed.enabled"), &mut t.enabled);
|
|
if im.button(gfx, Rect::new(row.x + row.w - CW() * 26.0, y, CW() * 16.0, CH() * 1.6), &trf("ed.edit_trigger_blocks", &[t.script.len().to_string()])) {
|
|
goto = Some(ScriptRef::Trigger(k));
|
|
}
|
|
let sure = self.confirm_delete == Some(10000 + k);
|
|
if im.button_c(gfx, Rect::new(row.x + row.w - CW() * 9.0, y, CW() * 8.0, CH() * 1.6), if sure { "?" } else { "x" }, RED) {
|
|
if sure {
|
|
remove = Some(k);
|
|
} else {
|
|
self.confirm_delete = Some(10000 + k);
|
|
}
|
|
}
|
|
condition_form(im, gfx, lx, y + CH() * 2.2, i * 1000 + k, &mut t.kind, &zones, &vars);
|
|
y += CH() * 5.2;
|
|
}
|
|
if let Some(k) = remove {
|
|
self.doc.missions[i].scripts.triggers.remove(k);
|
|
self.confirm_delete = None;
|
|
}
|
|
if im.button(gfx, Rect::new(lx, y, CW() * 20.0, CH() * 1.6), &tr("ed.add_trigger")) {
|
|
let m = &mut self.doc.missions[i];
|
|
let mut n = m.scripts.triggers.len() + 1;
|
|
while m.scripts.triggers.iter().any(|t| t.name == format!("trigger{}", n)) {
|
|
n += 1;
|
|
}
|
|
let zone = m.zones.first().map(|z| z.name.clone()).unwrap_or_default();
|
|
let mut kind = TriggerKind::template("zone_enter").unwrap();
|
|
if let TriggerKind::ZoneEnter { zone: z, .. } = &mut kind {
|
|
*z = zone;
|
|
}
|
|
if m.zones.is_empty() {
|
|
kind = TriggerKind::template("timer").unwrap();
|
|
}
|
|
m.scripts.triggers.push(TriggerDoc { name: format!("trigger{}", n), kind, once: true, enabled: true, script: Vec::new() });
|
|
}
|
|
y += CH() * 3.0;
|
|
// scrolling the list
|
|
let content = y + self.scroll - a.y;
|
|
if im.over(a) && im.wheel != 0.0 && !im.popup_open() {
|
|
self.scroll = (self.scroll - im.wheel.signum() * CH() * 3.0).clamp(0.0, (content - a.h).max(0.0));
|
|
}
|
|
if let Some(s) = goto {
|
|
self.blocks.open(s);
|
|
self.screen = Screen::Blocks(i, s);
|
|
}
|
|
}
|
|
}
|
|
|
|
// the condition of a trigger: the fields its kind needs
|
|
fn condition_form(im: &mut Im, gfx: &Gfx, x: f32, y: f32, k: usize, kind: &mut TriggerKind, zones: &[String], vars: &[String]) {
|
|
let fh = CH() * 1.6;
|
|
let mut cx = x;
|
|
let zone_dd = |im: &mut Im, cx: &mut f32, zone: &mut String| {
|
|
let cur = if zone.is_empty() { tr("ed.pick_zone") } else { zone.clone() };
|
|
if let Some(c) = im.dropdown(gfx, id("tzone", k), Rect::new(*cx, y, CW() * 18.0, fh), &cur, zones) {
|
|
*zone = zones[c].clone();
|
|
}
|
|
*cx += CW() * 19.0;
|
|
};
|
|
let side_opts: Vec<String> = crate::blocks::SIDES.iter().map(|&s| side_label(s)).collect();
|
|
let side_dd = |im: &mut Im, cx: &mut f32, side: &mut i32| {
|
|
if let Some(c) = im.dropdown(gfx, id("tside", k), Rect::new(*cx, y, CW() * 16.0, fh), &side_label(*side), &side_opts) {
|
|
*side = crate::blocks::SIDES[c];
|
|
}
|
|
*cx += CW() * 17.0;
|
|
};
|
|
let mut kinds: Vec<String> = vec![tr("ed.any_kind")];
|
|
kinds.extend(crate::blocks::KINDS.iter().map(|k| kind_label(k)));
|
|
let kind_dd = |im: &mut Im, cx: &mut f32, kd: &mut String| {
|
|
if let Some(c) = im.dropdown(gfx, id("tkd", k), Rect::new(*cx, y, CW() * 18.0, fh), &kind_label(kd), &kinds) {
|
|
*kd = if c == 0 { String::new() } else { crate::blocks::KINDS[c - 1].to_string() };
|
|
}
|
|
*cx += CW() * 19.0;
|
|
};
|
|
let ops: Vec<String> = crate::blocks::CMP_OPS.iter().map(|s| s.to_string()).collect();
|
|
match kind {
|
|
TriggerKind::ZoneEnter { zone, side, kind } | TriggerKind::ZoneLeave { zone, side, kind } | TriggerKind::LineCross { zone, side, kind } => {
|
|
zone_dd(im, &mut cx, zone);
|
|
side_dd(im, &mut cx, side);
|
|
kind_dd(im, &mut cx, kind);
|
|
}
|
|
TriggerKind::ZoneClaimed { zone } | TriggerKind::ZoneDug { zone } => zone_dd(im, &mut cx, zone),
|
|
TriggerKind::ZoneCount { zone, side, kind, op, n } => {
|
|
zone_dd(im, &mut cx, zone);
|
|
side_dd(im, &mut cx, side);
|
|
kind_dd(im, &mut cx, kind);
|
|
if let Some(c) = im.dropdown(gfx, id("top", k), Rect::new(cx, y, CW() * 6.0, fh), op, &ops) {
|
|
*op = ops[c].clone();
|
|
}
|
|
cx += CW() * 7.0;
|
|
let d = im.stepper(gfx, Rect::new(cx, y, CW() * 10.0, fh), &format!("{}", *n as i64));
|
|
*n = (*n + d as f64).max(0.0);
|
|
}
|
|
TriggerKind::Timer { secs, every } => {
|
|
label(gfx, cx, y + CH() * 0.25, &tr(if *every { "ed.every" } else { "ed.after" }), DIM);
|
|
cx += CW() * 10.0;
|
|
let d = im.stepper(gfx, Rect::new(cx, y, CW() * 14.0, fh), &format!("{} s", *secs as i64));
|
|
*secs = (*secs + d as f64 * 5.0).clamp(1.0, 36000.0);
|
|
cx += CW() * 15.0;
|
|
im.toggle(gfx, cx, y, &tr("ed.repeat"), every);
|
|
}
|
|
TriggerKind::UnitDies { side, kind, name } => {
|
|
side_dd(im, &mut cx, side);
|
|
kind_dd(im, &mut cx, kind);
|
|
label(gfx, cx, y + CH() * 0.25, &tr("ed.unit_name"), DIM);
|
|
im.text_field(gfx, id("tuname", k), Rect::new(cx + CW() * 8.0, y, CW() * 14.0, fh), name, 20);
|
|
}
|
|
TriggerKind::TowerFalls { side } => {
|
|
let opts = vec![tr("ed.side.any"), tr("ed.towers_dungeon"), tr("ed.towers_knights")];
|
|
let cur = opts[(*side + 1).clamp(0, 2) as usize].clone();
|
|
if let Some(c) = im.dropdown(gfx, id("ttower", k), Rect::new(cx, y, CW() * 22.0, fh), &cur, &opts) {
|
|
*side = c as i32 - 1;
|
|
}
|
|
}
|
|
TriggerKind::Value { what, var, op, n } => {
|
|
let whats = ["gold", "mana", "creatures", "heroes", "var"];
|
|
let opts: Vec<String> = whats.iter().map(|w| tr(&format!("ed.value.{}", w))).collect();
|
|
let cur = tr(&format!("ed.value.{}", what));
|
|
if let Some(c) = im.dropdown(gfx, id("twhat", k), Rect::new(cx, y, CW() * 16.0, fh), &cur, &opts) {
|
|
*what = whats[c].to_string();
|
|
}
|
|
cx += CW() * 17.0;
|
|
if what == "var" {
|
|
let cur = if var.is_empty() { tr("ed.pick_var") } else { var.clone() };
|
|
if let Some(c) = im.dropdown(gfx, id("tvar", k), Rect::new(cx, y, CW() * 16.0, fh), &cur, vars) {
|
|
*var = vars[c].clone();
|
|
}
|
|
cx += CW() * 17.0;
|
|
}
|
|
if let Some(c) = im.dropdown(gfx, id("top", k), Rect::new(cx, y, CW() * 6.0, fh), op, &ops) {
|
|
*op = ops[c].clone();
|
|
}
|
|
cx += CW() * 7.0;
|
|
let step = if what == "gold" { 100.0 } else { 1.0 };
|
|
let d = im.stepper(gfx, Rect::new(cx, y, CW() * 14.0, fh), &format!("{}", *n as i64));
|
|
*n = (*n + d as f64 * step).max(0.0);
|
|
}
|
|
TriggerKind::RoomBuilt { room } => {
|
|
let mut opts = vec![tr("ed.any_room")];
|
|
opts.extend(crate::game::ROOM_KINDS.iter().map(|r| format!("{:?}", r)));
|
|
let cur = if room.is_empty() { tr("ed.any_room") } else { room.clone() };
|
|
if let Some(c) = im.dropdown(gfx, id("troom", k), Rect::new(cx, y, CW() * 20.0, fh), &cur, &opts) {
|
|
*room = if c == 0 { String::new() } else { opts[c].clone() };
|
|
}
|
|
}
|
|
TriggerKind::SpellCast { spell } => {
|
|
let mut opts = vec![tr("ed.any_spell")];
|
|
opts.extend(crate::spells::SPELLS.iter().map(|s| crate::mscript::spell_name(*s)));
|
|
let cur = if spell.is_empty() { tr("ed.any_spell") } else { spell.clone() };
|
|
if let Some(c) = im.dropdown(gfx, id("tspell", k), Rect::new(cx, y, CW() * 20.0, fh), &cur, &opts) {
|
|
*spell = if c == 0 { String::new() } else { opts[c].clone() };
|
|
}
|
|
}
|
|
TriggerKind::Slap | TriggerKind::Research => {
|
|
gfx.text(&tr("ed.no_settings"), cx, y + CH() * 0.25, SMALL(), DIM);
|
|
}
|
|
}
|
|
}
|