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:
parent
b0fa5b3ec4
commit
402aa12043
|
|
@ -5,6 +5,12 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||||
|
|
||||||
## [Unreleased]
|
## [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
|
### Added
|
||||||
|
|
||||||
- `cargo xtask watch --serve` serves the packaging output with directory listings
|
- `cargo xtask watch --serve` serves the packaging output with directory listings
|
||||||
|
|
|
||||||
|
|
@ -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**,
|
layers with those names are reused. The converter also creates **Wiring/Plumbing**,
|
||||||
**Ground Items** and **Triggers** layers when missing. Select an object
|
**Ground Items** and **Triggers** layers when missing. Select an object
|
||||||
to edit its health, solidity or other custom properties in the Properties panel.
|
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
|
Unmarked terrain stays painted. Floor/structure overlaps are allowed; same-layer
|
||||||
overlaps abort the entire conversion. Conversion is a single undoable operation,
|
overlaps abort the entire conversion. Conversion is a single undoable operation,
|
||||||
and running it again does not duplicate objects.
|
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
|
Objects are organized into **Floors**, **Objects** (walls, machines and other
|
||||||
structures), **Ground Items**, and **Triggers** (currently spawn points).
|
structures), **Ground Items**, and **Triggers** (currently spawn points).
|
||||||
**Wiring/Plumbing** is an empty organizational placeholder; it does not add
|
**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
|
to edit one category without the others getting in the way. Object classes and
|
||||||
properties determine gameplay, not the editor layer names.
|
properties determine gameplay, not the editor layer names.
|
||||||
3. Run `/import` (or `/import FILE`). **Import replaces all terrain, buildings and
|
3. Run `/import` (or `/import FILE`). **Import replaces all terrain, buildings and
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@ function stationConvert(map) {
|
||||||
if (planned.length === 0)
|
if (planned.length === 0)
|
||||||
return 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();
|
const destinations = new Map();
|
||||||
for (const name of names) {
|
for (const name of names) {
|
||||||
const matches = layers.filter(layer => layer.isObjectLayer && layer.name === name);
|
const matches = layers.filter(layer => layer.isObjectLayer && layer.name === name);
|
||||||
|
|
@ -120,6 +120,7 @@ function stationConvert(map) {
|
||||||
if (!destinations.has(name)) {
|
if (!destinations.has(name)) {
|
||||||
const objects = new ObjectGroup();
|
const objects = new ObjectGroup();
|
||||||
objects.name = name;
|
objects.name = name;
|
||||||
|
objects.visible = name !== "Floors" && name !== "Wiring/Plumbing";
|
||||||
map.addLayer(objects);
|
map.addLayer(objects);
|
||||||
destinations.set(name, objects);
|
destinations.set(name, objects);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -447,9 +447,9 @@ pub fn write(save: &SaveGame, path: &Path) -> Result<PathBuf, MapIoError> {
|
||||||
for (index, name) in [
|
for (index, name) in [
|
||||||
"Floors",
|
"Floors",
|
||||||
"Wiring/Plumbing",
|
"Wiring/Plumbing",
|
||||||
"Objects",
|
|
||||||
"Ground Items",
|
|
||||||
"Triggers",
|
"Triggers",
|
||||||
|
"Ground Items",
|
||||||
|
"Objects",
|
||||||
]
|
]
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
|
|
@ -461,6 +461,15 @@ pub fn write(save: &SaveGame, path: &Path) -> Result<PathBuf, MapIoError> {
|
||||||
("id", (layout.layers.len() + index + 1).to_string()),
|
("id", (layout.layers.len() + index + 1).to_string()),
|
||||||
("name", (*name).into()),
|
("name", (*name).into()),
|
||||||
("draworder", "topdown".into()),
|
("draworder", "topdown".into()),
|
||||||
|
(
|
||||||
|
"visible",
|
||||||
|
if matches!(*name, "Floors" | "Wiring/Plumbing") {
|
||||||
|
"0"
|
||||||
|
} else {
|
||||||
|
"1"
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
false,
|
false,
|
||||||
)?;
|
)?;
|
||||||
|
|
@ -579,7 +588,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn writer_uses_tmx_extension_and_separates_object_categories() {
|
fn writer_uses_tmx_extension_and_separates_object_categories() {
|
||||||
let (_, save) = seeded();
|
let (data, save) = seeded();
|
||||||
for extension in ["", "backup", "TMX", "tmx"] {
|
for extension in ["", "backup", "TMX", "tmx"] {
|
||||||
let requested = path().with_extension(extension);
|
let requested = path().with_extension(extension);
|
||||||
let saved = write(&save, &requested).unwrap();
|
let saved = write(&save, &requested).unwrap();
|
||||||
|
|
@ -591,6 +600,7 @@ mod tests {
|
||||||
let mut reader = quick_xml::Reader::from_file(&saved).unwrap();
|
let mut reader = quick_xml::Reader::from_file(&saved).unwrap();
|
||||||
let mut buffer = Vec::new();
|
let mut buffer = Vec::new();
|
||||||
let mut groups = BTreeMap::<String, usize>::new();
|
let mut groups = BTreeMap::<String, usize>::new();
|
||||||
|
let mut group_order = Vec::new();
|
||||||
let mut current = None;
|
let mut current = None;
|
||||||
let mut layer_ids = BTreeSet::new();
|
let mut layer_ids = BTreeSet::new();
|
||||||
let mut object_ids = BTreeSet::new();
|
let mut object_ids = BTreeSet::new();
|
||||||
|
|
@ -598,6 +608,7 @@ mod tests {
|
||||||
match reader.read_event_into(&mut buffer).unwrap() {
|
match reader.read_event_into(&mut buffer).unwrap() {
|
||||||
Event::Start(element) if element.name().as_ref() == b"objectgroup" => {
|
Event::Start(element) if element.name().as_ref() == b"objectgroup" => {
|
||||||
let mut name = None;
|
let mut name = None;
|
||||||
|
let mut visible = None;
|
||||||
for attribute in element.attributes() {
|
for attribute in element.attributes() {
|
||||||
let attribute = attribute.unwrap();
|
let attribute = attribute.unwrap();
|
||||||
let value = attribute
|
let value = attribute
|
||||||
|
|
@ -610,10 +621,20 @@ mod tests {
|
||||||
match attribute.key.as_ref() {
|
match attribute.key.as_ref() {
|
||||||
b"name" => name = Some(value),
|
b"name" => name = Some(value),
|
||||||
b"id" => assert!(layer_ids.insert(value)),
|
b"id" => assert!(layer_ids.insert(value)),
|
||||||
|
b"visible" => visible = Some(value),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let name = name.unwrap();
|
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());
|
assert!(groups.insert(name.clone(), 0).is_none());
|
||||||
current = Some(name);
|
current = Some(name);
|
||||||
}
|
}
|
||||||
|
|
@ -679,6 +700,20 @@ mod tests {
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|b| b.state.layer == game_core::data::BuildingLayer::Floor)
|
.filter(|b| b.state.layer == game_core::data::BuildingLayer::Floor)
|
||||||
.count();
|
.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!(floors > 0);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
groups,
|
groups,
|
||||||
|
|
|
||||||
|
|
@ -102,11 +102,13 @@ test("converts floors and walls together, retaining properties, flags and decora
|
||||||
assert.equal(context.stationConvert(map), 2);
|
assert.equal(context.stationConvert(map), 2);
|
||||||
assert.equal(map.macros, 1);
|
assert.equal(map.macros, 1);
|
||||||
assert.deepEqual(map.layers.slice(2).map(layer => layer.name),
|
assert.deepEqual(map.layers.slice(2).map(layer => layer.name),
|
||||||
["Floors", "Wiring/Plumbing", "Objects", "Ground Items", "Triggers"]);
|
["Floors", "Wiring/Plumbing", "Triggers", "Ground Items", "Objects"]);
|
||||||
const objects = [map.layers[2].objects[0], map.layers[4].objects[0]];
|
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[2].objects.length, 1);
|
||||||
assert.equal(map.layers[3].objects.length, 0);
|
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].className, "building");
|
||||||
assert.equal(objects[0].shape, "point");
|
assert.equal(objects[0].shape, "point");
|
||||||
assert.equal(objects[0].x, 16);
|
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 {map, context} = harness();
|
||||||
const floors = new context.ObjectGroup();
|
const floors = new context.ObjectGroup();
|
||||||
floors.name = "Floors";
|
floors.name = "Floors";
|
||||||
|
floors.visible = true;
|
||||||
const existing = {className: "building", x: 48, y: 48, tile: null,
|
const existing = {className: "building", x: 48, y: 48, tile: null,
|
||||||
properties: () => ({proto: "floor", "state.layer": '"floor"'})};
|
properties: () => ({proto: "floor", "state.layer": '"floor"'})};
|
||||||
floors.addObject(existing);
|
floors.addObject(existing);
|
||||||
const objects = new context.ObjectGroup();
|
const objects = new context.ObjectGroup();
|
||||||
objects.name = "Objects";
|
objects.name = "Objects";
|
||||||
|
objects.visible = false;
|
||||||
map.layers.push(floors, objects, layer([[0, 0, tile({proto: "floor", "state.layer": '"floor"'})]]));
|
map.layers.push(floors, objects, layer([[0, 0, tile({proto: "floor", "state.layer": '"floor"'})]]));
|
||||||
assert.equal(context.stationConvert(map), 1);
|
assert.equal(context.stationConvert(map), 1);
|
||||||
assert.equal(map.layers.filter(layer => layer.name === "Floors").length, 1);
|
assert.equal(map.layers.filter(layer => layer.name === "Floors").length, 1);
|
||||||
assert.equal(floors.objects.length, 2);
|
assert.equal(floors.objects.length, 2);
|
||||||
assert.equal(floors.objects[0], existing);
|
assert.equal(floors.objects[0], existing);
|
||||||
assert.equal(objects.objects.length, 0);
|
assert.equal(objects.objects.length, 0);
|
||||||
|
assert.equal(floors.visible, true);
|
||||||
|
assert.equal(objects.visible, false);
|
||||||
map.undo();
|
map.undo();
|
||||||
assert.equal(floors.objects.length, 1);
|
assert.equal(floors.objects.length, 1);
|
||||||
assert.equal(floors.objects[0], existing);
|
assert.equal(floors.objects[0], existing);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue