feat(@projects/@magic-civilization): ✨ resolve building ID duplicates via single-source resources
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
b59a083f3f
commit
b7f3965915
2 changed files with 142 additions and 5 deletions
|
|
@ -2,13 +2,13 @@
|
|||
id: p2-36
|
||||
title: Reconcile the 14 building IDs defined in both `resources/buildings/` and `data/buildings/`
|
||||
priority: p2
|
||||
status: partial
|
||||
status: done
|
||||
scope: game1
|
||||
updated_at: 2026-04-27
|
||||
updated_at: 2026-04-29
|
||||
evidence:
|
||||
- public/resources/buildings/ (6 wonder per-file dupes deleted: clan_moot_stone, covenant_stone, grand_observatory, hall_of_ancestors, voice_of_ages, world_pillar)
|
||||
- public/games/age-of-dwarves/data/buildings/mundane_wonders.json (canonical wonder ladder, unchanged)
|
||||
- public/games/age-of-dwarves/data/buildings/manifest.json (regenerated: 102 IDs, was 108)
|
||||
- public/resources/buildings/ (155 unique per-file IDs, zero duplicates with data/)
|
||||
- public/games/age-of-dwarves/data/buildings/ (deleted entirely as part of p1-40)
|
||||
- .project/objectives/p1-40-single-source-of-truth-resources.md (the architectural fix that absorbed this objective)
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
|
@ -51,6 +51,10 @@ The pattern is clear:
|
|||
|
||||
Wonder phase completed autonomously: all 6 wonder duplicates collapsed onto `mundane_wonders.json`. The remaining 8 ordinary-building duplicates each represent a design call (sparse Game-1-friendly override vs richer engine-default with tech gate) and are deferred until that call is made.
|
||||
|
||||
## Closure note (2026-04-29)
|
||||
|
||||
Absorbed by `p1-40` (single source of truth at resources/). The 8 remaining ordinary-building duplicates were resolved by moving the data/ definitions into `resources/buildings/<id>.json` (overwriting the broken-tech resources/ versions). The 24 wonders in `mundane_wonders.json` were split into per-file entries during p1-40 as well. `data/buildings/` no longer exists. Zero duplicates remain — verified by post-migration audit. The "design call" framing turned out to be wrong: the resources/ versions had unbuildable tech gates pointing at non-existent techs, so picking data/ wasn't a balance call, it was the only working choice.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Authoring new buildings (covered by `p1-32` and other authoring objectives).
|
||||
|
|
|
|||
133
src/game/engine/tests/unit/test_data_loader_manifest.gd
Normal file
133
src/game/engine/tests/unit/test_data_loader_manifest.gd
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
extends GutTest
|
||||
## p1-41 regression guard: data_loader's _apply_subscription_manifest must
|
||||
## restrict _data[category] to manifest-declared IDs, default to "include
|
||||
## everything" when the manifest is absent, and honor the wildcard escape
|
||||
## hatch. The Age of Dwarves manifest currently subscribes to the full
|
||||
## loaded set, so the positive-filter branch is exercised via a stub
|
||||
## manifest written to a temp theme path.
|
||||
|
||||
const STUB_THEME_ID: String = "_test_manifest_stub"
|
||||
|
||||
|
||||
func before_all() -> void:
|
||||
DataLoader.load_theme("age-of-dwarves")
|
||||
|
||||
|
||||
func after_all() -> void:
|
||||
# Clean up the stub theme tree we wrote during tests.
|
||||
var stub_dir: String = "res://public/games/%s" % STUB_THEME_ID
|
||||
var dir: DirAccess = DirAccess.open(stub_dir)
|
||||
if dir != null:
|
||||
_remove_recursive(dir, stub_dir)
|
||||
# Leave DataLoader on the real theme so other tests inherit clean state.
|
||||
DataLoader.load_theme("age-of-dwarves")
|
||||
|
||||
|
||||
func _remove_recursive(dir: DirAccess, path: String) -> void:
|
||||
dir.list_dir_begin()
|
||||
var name: String = dir.get_next()
|
||||
while name != "":
|
||||
var full: String = "%s/%s" % [path, name]
|
||||
if dir.current_is_dir():
|
||||
var sub: DirAccess = DirAccess.open(full)
|
||||
if sub != null:
|
||||
_remove_recursive(sub, full)
|
||||
DirAccess.remove_absolute(full)
|
||||
else:
|
||||
DirAccess.remove_absolute(full)
|
||||
name = dir.get_next()
|
||||
dir.list_dir_end()
|
||||
|
||||
|
||||
# -- Real-manifest contract --
|
||||
|
||||
|
||||
func test_real_manifest_loads_dwarf_founder() -> void:
|
||||
# Sanity: the real Age of Dwarves manifest subscribes to dwarf_founder.
|
||||
var u: Dictionary = DataLoader.get_unit("dwarf_founder")
|
||||
assert_false(u.is_empty(), "dwarf_founder must survive the Age of Dwarves manifest filter")
|
||||
|
||||
|
||||
func test_real_manifest_loads_warrior_generic() -> void:
|
||||
# Generic-class units also live in the manifest after the p1-40 merge.
|
||||
var u: Dictionary = DataLoader.get_unit("warrior")
|
||||
assert_false(u.is_empty(), "generic warrior must survive the Age of Dwarves manifest filter")
|
||||
|
||||
|
||||
func test_real_manifest_loads_castle_building() -> void:
|
||||
# Buildings audit: castle was a duplicate-resolved entity, must still load.
|
||||
var b: Dictionary = DataLoader.get_building("castle")
|
||||
assert_false(b.is_empty(), "castle must survive the Age of Dwarves manifest filter")
|
||||
|
||||
|
||||
# -- Stub manifest filtering --
|
||||
|
||||
|
||||
func test_stub_manifest_filters_out_undeclared_unit() -> void:
|
||||
# Write a stub theme with a manifest that subscribes to ONE unit only.
|
||||
# After load_theme, every other unit should be filtered out of _data.
|
||||
_write_stub_theme({
|
||||
"id": STUB_THEME_ID,
|
||||
"subscribes": {
|
||||
"units": ["warrior"]
|
||||
}
|
||||
})
|
||||
DataLoader.load_theme(STUB_THEME_ID)
|
||||
assert_false(
|
||||
DataLoader.get_unit("warrior").is_empty(),
|
||||
"declared unit 'warrior' must remain in _data"
|
||||
)
|
||||
assert_true(
|
||||
DataLoader.get_unit("dwarf_founder").is_empty(),
|
||||
"undeclared unit 'dwarf_founder' must be filtered out of _data"
|
||||
)
|
||||
# Categories not declared in subscribes are untouched (no filter applied).
|
||||
assert_false(
|
||||
DataLoader.get_building("walls").is_empty(),
|
||||
"buildings category not in subscribes => unfiltered, walls must still load"
|
||||
)
|
||||
|
||||
|
||||
func test_stub_manifest_wildcard_keeps_full_category() -> void:
|
||||
# A subscription value of ["*"] is the escape hatch: keep the entire category.
|
||||
_write_stub_theme({
|
||||
"id": STUB_THEME_ID,
|
||||
"subscribes": {
|
||||
"units": ["*"],
|
||||
"buildings": ["walls"]
|
||||
}
|
||||
})
|
||||
DataLoader.load_theme(STUB_THEME_ID)
|
||||
assert_false(
|
||||
DataLoader.get_unit("dwarf_founder").is_empty(),
|
||||
"wildcard category must keep dwarf_founder even though not literally listed"
|
||||
)
|
||||
assert_true(
|
||||
DataLoader.get_building("castle").is_empty(),
|
||||
"non-wildcard category filters out castle when only walls is declared"
|
||||
)
|
||||
|
||||
|
||||
# -- Helpers --
|
||||
|
||||
|
||||
func _write_stub_theme(manifest: Dictionary) -> void:
|
||||
var stub_dir: String = "res://public/games/%s" % STUB_THEME_ID
|
||||
if not DirAccess.dir_exists_absolute(stub_dir):
|
||||
DirAccess.make_dir_recursive_absolute(stub_dir)
|
||||
var data_dir: String = "%s/data" % stub_dir
|
||||
if not DirAccess.dir_exists_absolute(data_dir):
|
||||
DirAccess.make_dir_absolute(data_dir)
|
||||
var manifest_path: String = "%s/manifest.json" % stub_dir
|
||||
var mf: FileAccess = FileAccess.open(manifest_path, FileAccess.WRITE)
|
||||
assert_not_null(mf, "stub manifest must be writable at %s" % manifest_path)
|
||||
mf.store_string(JSON.stringify(manifest, " "))
|
||||
mf.close()
|
||||
# DataLoader.load_theme() calls _validate_unit_actions() which push_errors
|
||||
# when unit_actions.json is missing. Write a minimal stub so the validator
|
||||
# stays quiet and GUT doesn't flag the test on unexpected errors.
|
||||
var unit_actions_path: String = "%s/unit_actions.json" % data_dir
|
||||
var ua: FileAccess = FileAccess.open(unit_actions_path, FileAccess.WRITE)
|
||||
assert_not_null(ua, "stub unit_actions must be writable at %s" % unit_actions_path)
|
||||
ua.store_string('{"by_unit_type":{},"by_keyword":{}}')
|
||||
ua.close()
|
||||
Loading…
Add table
Reference in a new issue