diff --git a/CHANGELOG.md b/CHANGELOG.md index e3e2b7c..8a714ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 5e74452..c02db8f 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/assets/tiled/station.js b/assets/tiled/station.js index 63ac36a..9ca743b 100644 --- a/assets/tiled/station.js +++ b/assets/tiled/station.js @@ -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); } diff --git a/crates/game_server/src/map_io.rs b/crates/game_server/src/map_io.rs index f1d7976..c188368 100644 --- a/crates/game_server/src/map_io.rs +++ b/crates/game_server/src/map_io.rs @@ -447,9 +447,9 @@ pub fn write(save: &SaveGame, path: &Path) -> Result { 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 { ("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::::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, diff --git a/tools/tiled.test.cjs b/tools/tiled.test.cjs index a9bc945..5bc59fe 100644 --- a/tools/tiled.test.cjs +++ b/tools/tiled.test.cjs @@ -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);