From c11d2812813d12a3041940b3a0f76bf4d4420c7d Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Thu, 5 Nov 2026 21:28:38 +0100 Subject: [PATCH] Editor: remove tiles from painted zones, scrollable zone list Co-Authored-By: Claude Opus 5.5 --- docs/CAMPAIGN_EDITOR.md | 2 +- lang/cs.lang | 1 + lang/da.lang | 1 + lang/de.lang | 1 + lang/el.lang | 1 + lang/en.lang | 1 + lang/es.lang | 1 + lang/fi.lang | 1 + lang/hr.lang | 1 + lang/hu.lang | 1 + lang/it.lang | 1 + lang/nl.lang | 1 + lang/no.lang | 1 + lang/pt.lang | 1 + lang/ro.lang | 1 + lang/ru.lang | 1 + lang/sk.lang | 1 + lang/sr.lang | 1 + lang/sv.lang | 1 + src/editor/map.rs | 75 +++++++++++++++++++++++++++++++++++++++-- 20 files changed, 92 insertions(+), 3 deletions(-) diff --git a/docs/CAMPAIGN_EDITOR.md b/docs/CAMPAIGN_EDITOR.md index 8fafea8..07071ab 100644 --- a/docs/CAMPAIGN_EDITOR.md +++ b/docs/CAMPAIGN_EDITOR.md @@ -95,7 +95,7 @@ The map in the game's 3D view; tool bar on the left, options on top. | Towers | arrow, cannon, magic, rune forge tower; the dungeon's or the knights', level 1–3 | | Traps | the seven traps and three doors, armed | | Forts | a knight fortress (with or without a shard), rival masters (Mossgrim, Lady Nightwell, rival 3); level = strength | -| Zones | trigger zones: rectangle, circle (drag from the middle), painted tiles (paint onto the selected tiles zone), line (drag from start to end); name, rename, delete | +| Zones | trigger zones: rectangle, circle (drag from the middle), painted tiles (paint onto the selected tiles zone; *Remove tiles* takes them out again), line (drag from start to end); name, rename, delete (the "x"); the list scrolls with the wheel | | Erase | removes units, towers, traps and doors on a tile | Options: brush size 1–5, rectangle, fill (connected area of the same ground), mirror (left/right, diff --git a/lang/cs.lang b/lang/cs.lang index 468d71c..9e97c5e 100644 --- a/lang/cs.lang +++ b/lang/cs.lang @@ -1205,6 +1205,7 @@ ed.level = Úroveň ed.zone_shape = Tvar nové zóny ed.zone_new_name = Název nové zóny ed.zones = Zóny +ed.zone_erase = Odebrat pole ed.shape.rect = Obdélník ed.shape.circle = Kruh ed.shape.tiles = Políčka diff --git a/lang/da.lang b/lang/da.lang index eb25c46..7aab002 100644 --- a/lang/da.lang +++ b/lang/da.lang @@ -1204,6 +1204,7 @@ ed.level = Niveau ed.zone_shape = Form på ny zone ed.zone_new_name = Navn på ny zone ed.zones = Zoner +ed.zone_erase = Fjern felter ed.shape.rect = Rektangel ed.shape.circle = Cirkel ed.shape.tiles = Felter diff --git a/lang/de.lang b/lang/de.lang index bafa00a..ae2dee7 100644 --- a/lang/de.lang +++ b/lang/de.lang @@ -1204,6 +1204,7 @@ ed.level = Stufe ed.zone_shape = Form einer neuen Zone ed.zone_new_name = Name einer neuen Zone ed.zones = Zonen +ed.zone_erase = Kacheln entfernen ed.shape.rect = Rechteck ed.shape.circle = Kreis ed.shape.tiles = Kacheln diff --git a/lang/el.lang b/lang/el.lang index ecb94fb..a114b85 100644 --- a/lang/el.lang +++ b/lang/el.lang @@ -1205,6 +1205,7 @@ ed.level = Επίπεδο ed.zone_shape = Σχήμα νέας ζώνης ed.zone_new_name = Όνομα νέας ζώνης ed.zones = Ζώνες +ed.zone_erase = Αφαίρεση πλακιδίων ed.shape.rect = Ορθογώνιο ed.shape.circle = Κύκλος ed.shape.tiles = Πλακίδια diff --git a/lang/en.lang b/lang/en.lang index 36cee12..5a70ea3 100644 --- a/lang/en.lang +++ b/lang/en.lang @@ -1205,6 +1205,7 @@ ed.level = Level ed.zone_shape = Shape of a new zone ed.zone_new_name = Name of a new zone ed.zones = Zones +ed.zone_erase = Remove tiles ed.shape.rect = Rectangle ed.shape.circle = Circle ed.shape.tiles = Tiles diff --git a/lang/es.lang b/lang/es.lang index 705c7a0..6f80952 100644 --- a/lang/es.lang +++ b/lang/es.lang @@ -1197,6 +1197,7 @@ ed.level = Nivel ed.zone_shape = Forma de la nueva zona ed.zone_new_name = Nombre de la nueva zona ed.zones = Zonas +ed.zone_erase = Quitar casillas ed.shape.rect = Rectángulo ed.shape.circle = Círculo ed.shape.tiles = Casillas diff --git a/lang/fi.lang b/lang/fi.lang index 89e38ec..75cc1e4 100644 --- a/lang/fi.lang +++ b/lang/fi.lang @@ -1205,6 +1205,7 @@ ed.level = Taso ed.zone_shape = Uuden alueen muoto ed.zone_new_name = Uuden alueen nimi ed.zones = Alueet +ed.zone_erase = Poista ruutuja ed.shape.rect = Suorakulmio ed.shape.circle = Ympyrä ed.shape.tiles = Ruudut diff --git a/lang/hr.lang b/lang/hr.lang index a9a5d9d..7407e58 100644 --- a/lang/hr.lang +++ b/lang/hr.lang @@ -1205,6 +1205,7 @@ ed.level = Razina ed.zone_shape = Oblik nove zone ed.zone_new_name = Ime nove zone ed.zones = Zone +ed.zone_erase = Ukloni polja ed.shape.rect = Pravokutnik ed.shape.circle = Krug ed.shape.tiles = Polja diff --git a/lang/hu.lang b/lang/hu.lang index 578c688..c15133b 100644 --- a/lang/hu.lang +++ b/lang/hu.lang @@ -1205,6 +1205,7 @@ ed.level = Szint ed.zone_shape = Új zóna alakja ed.zone_new_name = Új zóna neve ed.zones = Zónák +ed.zone_erase = Mezők törlése ed.shape.rect = Téglalap ed.shape.circle = Kör ed.shape.tiles = Mezők diff --git a/lang/it.lang b/lang/it.lang index 3298a48..bf57ff7 100644 --- a/lang/it.lang +++ b/lang/it.lang @@ -1197,6 +1197,7 @@ ed.level = Livello ed.zone_shape = Forma della nuova zona ed.zone_new_name = Nome della nuova zona ed.zones = Zone +ed.zone_erase = Togli caselle ed.shape.rect = Rettangolo ed.shape.circle = Cerchio ed.shape.tiles = Caselle diff --git a/lang/nl.lang b/lang/nl.lang index d98be03..480be00 100644 --- a/lang/nl.lang +++ b/lang/nl.lang @@ -1205,6 +1205,7 @@ ed.level = Niveau ed.zone_shape = Vorm van een nieuwe zone ed.zone_new_name = Naam van een nieuwe zone ed.zones = Zones +ed.zone_erase = Tegels weghalen ed.shape.rect = Rechthoek ed.shape.circle = Cirkel ed.shape.tiles = Vakken diff --git a/lang/no.lang b/lang/no.lang index 2d557e4..f61ef1b 100644 --- a/lang/no.lang +++ b/lang/no.lang @@ -1204,6 +1204,7 @@ ed.level = Nivå ed.zone_shape = Form på ny sone ed.zone_new_name = Navn på ny sone ed.zones = Soner +ed.zone_erase = Fjern ruter ed.shape.rect = Rektangel ed.shape.circle = Sirkel ed.shape.tiles = Ruter diff --git a/lang/pt.lang b/lang/pt.lang index 81ffd2c..5db87b1 100644 --- a/lang/pt.lang +++ b/lang/pt.lang @@ -1205,6 +1205,7 @@ ed.level = Nível ed.zone_shape = Forma da nova zona ed.zone_new_name = Nome da nova zona ed.zones = Zonas +ed.zone_erase = Remover casas ed.shape.rect = Retângulo ed.shape.circle = Círculo ed.shape.tiles = Casas diff --git a/lang/ro.lang b/lang/ro.lang index 0f08e42..198d376 100644 --- a/lang/ro.lang +++ b/lang/ro.lang @@ -1205,6 +1205,7 @@ ed.level = Nivel ed.zone_shape = Forma unei zone noi ed.zone_new_name = Numele unei zone noi ed.zones = Zone +ed.zone_erase = Scoate pătrate ed.shape.rect = Dreptunghi ed.shape.circle = Cerc ed.shape.tiles = Dale diff --git a/lang/ru.lang b/lang/ru.lang index 8ee2d1c..7785901 100644 --- a/lang/ru.lang +++ b/lang/ru.lang @@ -1197,6 +1197,7 @@ ed.level = Уровень ed.zone_shape = Форма новой зоны ed.zone_new_name = Имя новой зоны ed.zones = Зоны +ed.zone_erase = Убрать клетки ed.shape.rect = Прямоугольник ed.shape.circle = Круг ed.shape.tiles = Клетки diff --git a/lang/sk.lang b/lang/sk.lang index 3cab772..8ab10c5 100644 --- a/lang/sk.lang +++ b/lang/sk.lang @@ -1205,6 +1205,7 @@ ed.level = Úroveň ed.zone_shape = Tvar novej zóny ed.zone_new_name = Názov novej zóny ed.zones = Zóny +ed.zone_erase = Odobrať polia ed.shape.rect = Obdĺžnik ed.shape.circle = Kruh ed.shape.tiles = Políčka diff --git a/lang/sr.lang b/lang/sr.lang index 39d3ba7..447082f 100644 --- a/lang/sr.lang +++ b/lang/sr.lang @@ -1205,6 +1205,7 @@ ed.level = Ниво ed.zone_shape = Облик нове зоне ed.zone_new_name = Име нове зоне ed.zones = Зоне +ed.zone_erase = Уклони поља ed.shape.rect = Правоугаоник ed.shape.circle = Круг ed.shape.tiles = Поља diff --git a/lang/sv.lang b/lang/sv.lang index e65703f..d88faa8 100644 --- a/lang/sv.lang +++ b/lang/sv.lang @@ -1205,6 +1205,7 @@ ed.level = Nivå ed.zone_shape = Form på ny zon ed.zone_new_name = Namn på ny zon ed.zones = Zoner +ed.zone_erase = Ta bort rutor ed.shape.rect = Rektangel ed.shape.circle = Cirkel ed.shape.tiles = Rutor diff --git a/src/editor/map.rs b/src/editor/map.rs index 0129b27..212e091 100644 --- a/src/editor/map.rs +++ b/src/editor/map.rs @@ -61,6 +61,8 @@ pub struct MapEdit { zone: usize, // selected zone zone_shape: usize, // shape for a new zone zone_name: String, + zone_erase: bool, // painting a tiles zone takes tiles out of it again + zone_scroll: usize, // first zone shown in the list (usize::MAX: scroll to the end) last_mouse: Option, } @@ -117,6 +119,8 @@ impl MapEdit { zone: 0, zone_shape: 0, zone_name: String::new(), + zone_erase: false, + zone_scroll: 0, last_mouse: None, } } @@ -460,9 +464,18 @@ impl MapEdit { } m.zones.push(z); self.zone = m.zones.len() - 1; + self.zone_scroll = usize::MAX; self.zone_name.clear(); } + // the brush takes tiles out of the selected tiles zone + fn erase_zone_tiles(&mut self, m: &mut MissionDoc, x: i32, y: i32) { + let targets = self.targets(x, y); + if let Some(t) = m.zones.get_mut(self.zone).filter(|z| z.shape == "tiles").and_then(|z| z.tiles.as_mut()) { + t.retain(|p| !targets.contains(p)); + } + } + pub fn frame(&mut self, im: &mut Im, gfx: &Gfx, r3d: &mut R3d, dt: f32, now: f64, m: &mut MissionDoc) -> MapResult { let (cssx, cssy) = mouse_position(); let bar_w = CW() * 30.0; @@ -547,6 +560,15 @@ impl MapEdit { } im.pressed = false; } + // a tiles zone is painted (or erased) all along the drag + let tiles_zone = self.cat == Cat::Zones && ZONE_SHAPES[self.zone_shape] == "tiles"; + if tiles_zone && im.down && self.drag.is_some() { + if self.zone_erase { + self.erase_zone_tiles(m, x, y); + } else { + self.draw_zone(m, x, y, x, y); + } + } if self.stroke && im.down { for (tx, ty) in self.targets(x, y) { self.paint_tile(tx, ty); @@ -555,7 +577,11 @@ impl MapEdit { if im.released { if let Some((sx, sy)) = self.drag { if self.cat == Cat::Zones { - self.draw_zone(m, sx, sy, x, y); + if ZONE_SHAPES[self.zone_shape] == "tiles" && self.zone_erase { + self.erase_zone_tiles(m, x, y); + } else { + self.draw_zone(m, sx, sy, x, y); + } } else { for (tx, ty) in self.targets(x, y) { self.paint_tile(tx, ty); @@ -738,7 +764,24 @@ impl MapEdit { label(gfx, x, y, &tr("ed.zones"), FRAME_HI); y += CH() * 1.4; let mut remove = None; - for k in 0..m.zones.len() { + // the list scrolls (wheel over it) when there are more zones than room + let row = CH() * 1.7; + let room = (((h - CH() * 3.0 - y) / row).floor() as usize).max(1); + let over_list = im.mx < bar_w && im.my >= y && im.my < y + room as f32 * row; + if over_list && im.wheel != 0.0 { + self.zone_scroll = if im.wheel > 0.0 { self.zone_scroll.min(m.zones.len()).saturating_sub(1) } else { self.zone_scroll.saturating_add(1) }; + } + let max_scroll = m.zones.len().saturating_sub(room); + if self.zone_scroll == usize::MAX { + // a new zone: show it + self.zone_scroll = self.zone.saturating_sub(room - 1); + } + self.zone_scroll = self.zone_scroll.min(max_scroll); + if self.zone_scroll > 0 || max_scroll > 0 { + let more = format!("{}-{} / {}", self.zone_scroll + 1, (self.zone_scroll + room).min(m.zones.len()), m.zones.len()); + gfx.text(&more, x + fw - gfx.measure(&more, SMALL()), y - CH() * 1.3, SMALL(), DIM); + } + for k in self.zone_scroll..m.zones.len() { if y > h - CH() * 3.0 { break; } @@ -778,6 +821,10 @@ impl MapEdit { } bx += gfx.measure(&tr("ed.fill"), S()) + CW() * 4.0; } + if self.cat == Cat::Zones && ZONE_SHAPES[self.zone_shape] == "tiles" { + im.toggle(gfx, bx, by + CH() * 0.15, &tr("ed.zone_erase"), &mut self.zone_erase); + bx += gfx.measure(&tr("ed.zone_erase"), S()) + CW() * 4.0; + } let mirrors: Vec = (0..4).map(|k| tr(&format!("ed.mirror{}", k))).collect(); if let Some(c) = im.dropdown(gfx, id("mirror", 0), Rect::new(bx, by, CW() * 23.0, CH() * 1.7), &mirrors[self.mirror as usize], &mirrors) { self.mirror = c as u8; @@ -799,3 +846,27 @@ impl MapEdit { done } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn tiles_of_a_painted_zone_can_be_taken_out_again() { + let mut ed = MapEdit::new(Game::new(3, 64, 64)); + let mut m = MissionDoc::default(); + ed.zone_shape = ZONE_SHAPES.iter().position(|s| *s == "tiles").unwrap(); + ed.brush = 2; + ed.draw_zone(&mut m, 10, 10, 10, 10); + ed.draw_zone(&mut m, 14, 10, 14, 10); + assert_eq!(m.zones.len(), 1, "painting goes on in the selected tiles zone"); + let n = m.zones[0].tiles.as_ref().unwrap().len(); + assert!(n >= 2); + ed.erase_zone_tiles(&mut m, 10, 10); + let t = m.zones[0].tiles.as_ref().unwrap(); + assert!(!t.contains(&(10, 10)) && t.contains(&(14, 10))); + assert!(t.len() < n); + // a new zone scrolls the list to it + assert_eq!(ed.zone_scroll, usize::MAX); + } +}