fix(tiled): match default layer order and visibility

Place Objects above Ground Items and Triggers, with Floors and Wiring/Plumbing hidden by default. Apply the defaults to exports and newly created conversion layers while preserving existing layer visibility. Verify hidden layers still import gameplay entities.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Steve Biedermann 2026-10-21 22:53:24 +02:00
parent b0fa5b3ec4
commit 402aa12043
5 changed files with 62 additions and 8 deletions

View File

@ -5,6 +5,12 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [Unreleased]
### Changed
- Tiled exports and newly created conversion layers place Objects above Ground
Items, Triggers, Wiring/Plumbing and Floors, with Floors and Wiring/Plumbing
hidden by default in the editor without affecting gameplay.
### Added
- `cargo xtask watch --serve` serves the packaging output with directory listings

View File

@ -421,6 +421,8 @@ Tiles to Objects**. The extension replaces all marked painted cells with selecte
layers with those names are reused. The converter also creates **Wiring/Plumbing**,
**Ground Items** and **Triggers** layers when missing. Select an object
to edit its health, solidity or other custom properties in the Properties panel.
New layers use the same order and visibility defaults as world exports below;
existing layers retain their visibility settings.
Unmarked terrain stays painted. Floor/structure overlaps are allowed; same-layer
overlaps abort the entire conversion. Conversion is a single undoable operation,
and running it again does not duplicate objects.
@ -452,7 +454,11 @@ and coordinate conventions; conversion is optional before loading a map.
Objects are organized into **Floors**, **Objects** (walls, machines and other
structures), **Ground Items**, and **Triggers** (currently spawn points).
**Wiring/Plumbing** is an empty organizational placeholder; it does not add
wiring, plumbing or trigger mechanics. Hide or lock individual layers in Tiled
wiring, plumbing or trigger mechanics. From top to bottom, Tiled shows
**Objects**, **Ground Items**, **Triggers**, **Wiring/Plumbing**, **Floors**,
then the painted terrain layers (for the default map, **Walls** above **Ground**).
**Floors** and **Wiring/Plumbing** are hidden by default to reduce editor clutter;
hidden layers still load into the game. Hide or lock individual layers in Tiled
to edit one category without the others getting in the way. Object classes and
properties determine gameplay, not the editor layer names.
3. Run `/import` (or `/import FILE`). **Import replaces all terrain, buildings and

View File

@ -103,7 +103,7 @@ function stationConvert(map) {
if (planned.length === 0)
return 0;
const names = ["Floors", "Wiring/Plumbing", "Objects", "Ground Items", "Triggers"];
const names = ["Floors", "Wiring/Plumbing", "Triggers", "Ground Items", "Objects"];
const destinations = new Map();
for (const name of names) {
const matches = layers.filter(layer => layer.isObjectLayer && layer.name === name);
@ -120,6 +120,7 @@ function stationConvert(map) {
if (!destinations.has(name)) {
const objects = new ObjectGroup();
objects.name = name;
objects.visible = name !== "Floors" && name !== "Wiring/Plumbing";
map.addLayer(objects);
destinations.set(name, objects);
}

View File

@ -447,9 +447,9 @@ pub fn write(save: &SaveGame, path: &Path) -> Result<PathBuf, MapIoError> {
for (index, name) in [
"Floors",
"Wiring/Plumbing",
"Objects",
"Ground Items",
"Triggers",
"Ground Items",
"Objects",
]
.iter()
.enumerate()
@ -461,6 +461,15 @@ pub fn write(save: &SaveGame, path: &Path) -> Result<PathBuf, MapIoError> {
("id", (layout.layers.len() + index + 1).to_string()),
("name", (*name).into()),
("draworder", "topdown".into()),
(
"visible",
if matches!(*name, "Floors" | "Wiring/Plumbing") {
"0"
} else {
"1"
}
.into(),
),
],
false,
)?;
@ -579,7 +588,7 @@ mod tests {
#[test]
fn writer_uses_tmx_extension_and_separates_object_categories() {
let (_, save) = seeded();
let (data, save) = seeded();
for extension in ["", "backup", "TMX", "tmx"] {
let requested = path().with_extension(extension);
let saved = write(&save, &requested).unwrap();
@ -591,6 +600,7 @@ mod tests {
let mut reader = quick_xml::Reader::from_file(&saved).unwrap();
let mut buffer = Vec::new();
let mut groups = BTreeMap::<String, usize>::new();
let mut group_order = Vec::new();
let mut current = None;
let mut layer_ids = BTreeSet::new();
let mut object_ids = BTreeSet::new();
@ -598,6 +608,7 @@ mod tests {
match reader.read_event_into(&mut buffer).unwrap() {
Event::Start(element) if element.name().as_ref() == b"objectgroup" => {
let mut name = None;
let mut visible = None;
for attribute in element.attributes() {
let attribute = attribute.unwrap();
let value = attribute
@ -610,10 +621,20 @@ mod tests {
match attribute.key.as_ref() {
b"name" => name = Some(value),
b"id" => assert!(layer_ids.insert(value)),
b"visible" => visible = Some(value),
_ => {}
}
}
let name = name.unwrap();
assert_eq!(
visible.as_deref(),
Some(if matches!(name.as_str(), "Floors" | "Wiring/Plumbing") {
"0"
} else {
"1"
})
);
group_order.push(name.clone());
assert!(groups.insert(name.clone(), 0).is_none());
current = Some(name);
}
@ -679,6 +700,20 @@ mod tests {
.iter()
.filter(|b| b.state.layer == game_core::data::BuildingLayer::Floor)
.count();
assert_eq!(
group_order,
[
"Floors",
"Wiring/Plumbing",
"Triggers",
"Ground Items",
"Objects"
]
);
let loaded =
MapLayout::load(&game_core::assets_dir(), saved.to_str().unwrap()).unwrap();
let imported = decode(loaded, &data).unwrap();
assert_eq!(imported.buildings.len(), save.buildings.len());
assert!(floors > 0);
assert_eq!(
groups,

View File

@ -102,11 +102,13 @@ test("converts floors and walls together, retaining properties, flags and decora
assert.equal(context.stationConvert(map), 2);
assert.equal(map.macros, 1);
assert.deepEqual(map.layers.slice(2).map(layer => layer.name),
["Floors", "Wiring/Plumbing", "Objects", "Ground Items", "Triggers"]);
const objects = [map.layers[2].objects[0], map.layers[4].objects[0]];
["Floors", "Wiring/Plumbing", "Triggers", "Ground Items", "Objects"]);
assert.deepEqual(map.layers.slice(2).map(layer => layer.visible),
[false, false, true, true, true]);
const objects = [map.layers[2].objects[0], map.layers[6].objects[0]];
assert.equal(map.layers[2].objects.length, 1);
assert.equal(map.layers[3].objects.length, 0);
assert.equal(map.layers[4].objects.length, 1);
assert.equal(map.layers[6].objects.length, 1);
assert.equal(objects[0].className, "building");
assert.equal(objects[0].shape, "point");
assert.equal(objects[0].x, 16);
@ -136,17 +138,21 @@ test("reuses categorized object layers and undoes additions without changing exi
const {map, context} = harness();
const floors = new context.ObjectGroup();
floors.name = "Floors";
floors.visible = true;
const existing = {className: "building", x: 48, y: 48, tile: null,
properties: () => ({proto: "floor", "state.layer": '"floor"'})};
floors.addObject(existing);
const objects = new context.ObjectGroup();
objects.name = "Objects";
objects.visible = false;
map.layers.push(floors, objects, layer([[0, 0, tile({proto: "floor", "state.layer": '"floor"'})]]));
assert.equal(context.stationConvert(map), 1);
assert.equal(map.layers.filter(layer => layer.name === "Floors").length, 1);
assert.equal(floors.objects.length, 2);
assert.equal(floors.objects[0], existing);
assert.equal(objects.objects.length, 0);
assert.equal(floors.visible, true);
assert.equal(objects.visible, false);
map.undo();
assert.equal(floors.objects.length, 1);
assert.equal(floors.objects[0], existing);