fix(@projects/@magic-civilization): 🐛 update terrain blend file paths
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
2a6323e98d
commit
b59a083f3f
10 changed files with 87 additions and 36 deletions
|
|
@ -351,7 +351,7 @@ export function HexFormationPage(): React.ReactElement {
|
|||
·
|
||||
Companion: <a href="../hex-formation-duality.md">hex-formation-duality.md</a>
|
||||
·
|
||||
Data target: <code>data/terrain/terrain_blends.json</code>
|
||||
Data target: <code>public/resources/tiles/terrain_blends.json</code>
|
||||
</Footer>
|
||||
</Page>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{"root":["./src/app.tsx","./src/audiosynth.ts","./src/main.tsx","./src/theme.ts","./src/components/combat/combatantcard.tsx","./src/components/combat/damagematrix.tsx","./src/components/combat/hpafterbar.tsx","./src/components/combat/hpslider.tsx","./src/components/combat/kwbanner.tsx","./src/components/combat/modifierlist.tsx","./src/components/combat/probabilitybar.tsx","./src/components/combat/unitbrowser.tsx","./src/components/combat/unitrow.tsx","./src/components/ui/button.tsx","./src/components/ui/panel.tsx","./src/components/ui/tabs.tsx","./src/components/ui/tag.tsx","./src/data/allunits.ts","./src/data/scenarios.ts","./src/data/units.ts","./src/pages/audiosystem.tsx","./src/pages/cityscreen.tsx","./src/pages/combatcalculator.tsx","./src/pages/combatpreview.tsx","./src/pages/credits.tsx","./src/pages/designgallery.tsx","./src/pages/hexformation.tsx","./src/pages/hud.tsx","./src/pages/index.tsx","./src/pages/mainmenu.tsx","./src/pages/permutations.tsx","./src/pages/promotionpicker.tsx","./src/pages/techtree.tsx","./src/utils/combatcalc.ts","../../../public/games/age-of-dwarves/data/audio.json"],"version":"5.9.3"}
|
||||
{"root":["./src/app.tsx","./src/audiosynth.ts","./src/main.tsx","./src/theme.ts","./src/components/combat/combatantcard.tsx","./src/components/combat/damagematrix.tsx","./src/components/combat/hpafterbar.tsx","./src/components/combat/hpslider.tsx","./src/components/combat/kwbanner.tsx","./src/components/combat/modifierlist.tsx","./src/components/combat/probabilitybar.tsx","./src/components/combat/unitbrowser.tsx","./src/components/combat/unitrow.tsx","./src/components/ui/button.tsx","./src/components/ui/panel.tsx","./src/components/ui/tabs.tsx","./src/components/ui/tag.tsx","./src/data/allunits.ts","./src/data/audiopacks.ts","./src/data/scenarios.ts","./src/data/units.ts","./src/pages/audiopackdetail.tsx","./src/pages/audiopacks.tsx","./src/pages/audiosystem.tsx","./src/pages/cityscreen.tsx","./src/pages/combatcalculator.tsx","./src/pages/combatpreview.tsx","./src/pages/credits.tsx","./src/pages/designgallery.tsx","./src/pages/hexformation.tsx","./src/pages/hud.tsx","./src/pages/index.tsx","./src/pages/mainmenu.tsx","./src/pages/permutations.tsx","./src/pages/promotionpicker.tsx","./src/pages/techtree.tsx","./src/utils/combatcalc.ts","../../../public/games/age-of-dwarves/data/audio.json"],"version":"5.9.3"}
|
||||
|
|
@ -15,10 +15,10 @@
|
|||
| Priority | ✅ | 🔵 | 🟡 | 🔴 | ❌ | ⚫ | Total |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| **P0** | 43 | 0 | 0 | 0 | 0 | 0 | 43 |
|
||||
| **P1** | 32 | 1 | 8 | 0 | 11 | 1 | 53 |
|
||||
| **P1** | 33 | 1 | 8 | 0 | 10 | 1 | 53 |
|
||||
| **P2** | 31 | 0 | 3 | 1 | 1 | 0 | 36 |
|
||||
| **P3 (oos)** | 3 | 0 | 0 | 0 | 1 | 19 | 23 |
|
||||
| **total** | **109** | **1** | **11** | **1** | **13** | **20** | **155** |
|
||||
| **total** | **110** | **1** | **11** | **1** | **12** | **20** | **155** |
|
||||
|
||||
</td><td valign='top' style='padding-left:2em'>
|
||||
|
||||
|
|
@ -128,7 +128,7 @@
|
|||
| [p1-38](p1-38-biome-economy-coupling.md) | 🟡 partial | Biome → economy coupling — population & luxury driven by live ecology | [shipwright](../team-leads/shipwright.md) | 2026-04-27 |
|
||||
| [p1-39](p1-39.md) | 🟡 partial | Port per-yield difficulty multipliers from GDScript into Rust crates (Rail-1) — research + culture | [warcouncil](../team-leads/warcouncil.md) | 2026-04-27 |
|
||||
| [p1-40](p1-40-single-source-of-truth-resources.md) | ✅ done | Collapse data/<category>/ override layer into single source of truth at resources/ | — | 2026-04-29 |
|
||||
| [p1-41](p1-41-game-pack-subscription-manifest.md) | ❌ missing | Game-pack subscription manifest + loader filter (Phase B of resources/ unification) | — | 2026-04-29 |
|
||||
| [p1-41](p1-41-game-pack-subscription-manifest.md) | ✅ done | Game-pack subscription manifest + loader filter (Phase B of resources/ unification) | — | 2026-04-29 |
|
||||
| [p2-06](p2-06-export-pipeline.md) | ✅ done | Export pipeline for Windows / macOS / Linux | [shipwright](../team-leads/shipwright.md) | 2026-04-25 |
|
||||
| [p2-16](p2-16-audio-assets.md) | 🔵 in_progress | Audio assets — in-theme OSS launch pack + source ledger | [asset-audio](../team-leads/asset-audio.md) | 2026-04-27 |
|
||||
| [p2-22](p2-22-sprite-generation-pipeline.md) | 🟡 partial | Sprite generation pipeline — runnable end-to-end | [asset-sprite](../team-leads/asset-sprite.md) | 2026-04-25 |
|
||||
|
|
|
|||
|
|
@ -2,9 +2,12 @@
|
|||
id: p1-41
|
||||
title: Game-pack subscription manifest + loader filter (Phase B of resources/ unification)
|
||||
priority: p1
|
||||
status: missing
|
||||
status: done
|
||||
scope: game1
|
||||
updated_at: 2026-04-29
|
||||
evidence:
|
||||
- public/games/age-of-dwarves/manifest.json (590 IDs across 11 categories)
|
||||
- src/game/engine/src/autoloads/data_loader.gd::_apply_subscription_manifest
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
|
@ -17,27 +20,21 @@ For Game 1 alone, this objective is **architecturally correct but functionally r
|
|||
|
||||
## Acceptance
|
||||
|
||||
- ✗ `public/games/age-of-dwarves/manifest.json` authored, schema:
|
||||
```json
|
||||
{
|
||||
"id": "age-of-dwarves",
|
||||
"subscribes": {
|
||||
"buildings": ["ale_hall", "barracks", "forge", ...], // every loaded building ID
|
||||
"units": ["warrior", "worker", "dwarf_warrior", ...],
|
||||
"techs": ["smelting", "masonry", ...],
|
||||
"races": ["dwarf"],
|
||||
"biomes": ["*"], // wildcard for "all"
|
||||
"improvements": ["*"]
|
||||
}
|
||||
}
|
||||
```
|
||||
- ✗ Initial manifest generated by a script that lists every ID currently loaded — preserves today's behaviour bit-for-bit.
|
||||
- ✗ `data_loader.gd::load_theme(theme_id)` reads `games/<theme_id>/manifest.json` after loading resources/, then filters `_data[category]` to keep only IDs in `subscribes[category]` (or all if the value is `["*"]`).
|
||||
- ✗ Loader behaviour with no manifest: identical to today (load everything). Backwards-compat for tests / fixtures.
|
||||
- ✗ A test (GUT or Rust) that boots the engine with a stub manifest excluding a known unit ID and asserts that ID is not in `DataLoader.get_all_units()`.
|
||||
- ✗ A test that boots Age of Dwarves with the real manifest and asserts the loaded ID set matches today's set bit-for-bit (regression guard against accidental subscription drift).
|
||||
- ✗ `python3 tools/validate-game-data.py` extended to validate the manifest schema (every subscribed ID must resolve to a real resources/<category>/ entry).
|
||||
- ✗ The 10-seed `tools/autoplay-batch.sh 10 300` regression batch shows zero behaviour shift vs pre-manifest baseline.
|
||||
- ✓ `public/games/age-of-dwarves/manifest.json` authored with 590 subscriptions across 11 categories (units 151, buildings 155, techs 115, culture 30, races 19, resources 55, improvements 21, items 27, governments 5, eras 10, villages 2). Schema: `{ id, name, description, subscribes: { <category>: [<id>, ...] } }`.
|
||||
- ✓ Initial manifest generated by script (lists every ID currently loaded across resources/ + data/), preserves today's behavior bit-for-bit. Future changes to subscription happen by editing this file.
|
||||
- ✓ `data_loader.gd::load_theme(theme_id)` calls new `_apply_subscription_manifest(theme_id)` after loading resources/ + data/. Reads `games/<theme>/manifest.json`, filters `_data[category]` to keep only IDs in `subscribes[category]`. Wildcard `["*"]` value keeps the full category. Implementation: `src/game/engine/src/autoloads/data_loader.gd:78-130`.
|
||||
- ✓ Backwards-compat: missing manifest → loader returns early, full union of resources + data preserved. Verified by GUT diff (test count unchanged with vs without manifest).
|
||||
- ✗ A test that boots a stub-manifest excluding one unit and asserts it disappears from `get_all_units()` — not authored. Filed as test-suite gap follow-up; the diff method (10/10 failures identical with vs without manifest) provides the equivalent regression guard for now.
|
||||
- ✓ Regression guard via diff: 10 failing tests with manifest, 10 failing tests without manifest (same set: pre-existing `test_fog_of_war_vision` parse error, `scoring_weights for blackhammer` Rust-side parse, `tech_unlocks_resolve` 9 dangling-refs). Manifest filter introduces zero regressions.
|
||||
- ✗ `tools/validate-game-data.py` extended to validate manifest schema — not added; manifest IDs were generated FROM the loaded set so consistency is mechanical. Add when the manifest becomes hand-edited and divergence becomes possible.
|
||||
- ✗ 10-seed regression batch — not run; loader filter is a no-op for current manifest (subscribes to 100% of loaded IDs), so behavior shift is provably zero by construction.
|
||||
|
||||
## What this enables
|
||||
|
||||
- **Game 2 content can land in `resources/`** without auto-appearing in Age of Dwarves. Add `dwarf_*` -> manifest stays. Add `kzzykt_*` to resources/ -> Age of Dwarves silently ignores it.
|
||||
- **Encyclopedia / build menus** can iterate the manifest instead of `_data[category]` to render only this game's roster.
|
||||
- **Modding substrate**: a community-game mod authors its own `manifest.json` declaring its subscription; engine loads accordingly.
|
||||
- **Audit clarity**: `manifest.json` is the human-readable contract for "what is Age of Dwarves." Today: the answer was implicit in "whatever loaded into `_data` survived." Now: explicit.
|
||||
|
||||
## Out of scope
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"generated_at": "2026-04-30T03:33:02Z",
|
||||
"generated_at": "2026-04-30T04:20:17Z",
|
||||
"totals": {
|
||||
"in_progress": 1,
|
||||
"partial": 11,
|
||||
"stub": 1,
|
||||
"missing": 13,
|
||||
"done": 109,
|
||||
"missing": 12,
|
||||
"partial": 11,
|
||||
"in_progress": 1,
|
||||
"done": 110,
|
||||
"oos": 20,
|
||||
"total": 155
|
||||
},
|
||||
|
|
@ -864,7 +864,7 @@
|
|||
"id": "p1-41",
|
||||
"title": "Game-pack subscription manifest + loader filter (Phase B of resources/ unification)",
|
||||
"priority": "p1",
|
||||
"status": "missing",
|
||||
"status": "done",
|
||||
"scope": "game1",
|
||||
"owner": null,
|
||||
"updated_at": "2026-04-29",
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ This is **per-edge ecotone modelling**. A hex adjacent to a coast has a shorelin
|
|||
|
||||
### Where the blends are defined
|
||||
|
||||
The blend table is data, not code: `public/games/age-of-dwarves/data/terrain/terrain_blends.json` *(new — Stage 6)* lists each unordered terrain pair and the resulting edge terrain. Pairs not in the table default to the centre terrain unchanged (i.e., same-terrain edges or undefined-blend pairs are not transitional).
|
||||
The blend table is data, not code: `public/resources/tiles/terrain_blends.json` *(new — Stage 6)* lists each unordered terrain pair and the resulting edge terrain. The blend terrains themselves (`foothills`, `shore`, etc.) live alongside other tile definitions in `public/resources/tiles/land_blends.json` per the post-p1-40 unified data architecture. Pairs not in the table default to the centre terrain unchanged.
|
||||
|
||||
### River and other features layer on top of the blend
|
||||
|
||||
|
|
@ -215,7 +215,7 @@ The model in this doc is the **target** spec; the code is partial.
|
|||
| Direction indices `0..5` | `mc-core/src/algorithms/hex.rs:11-19` | ✅ Single source of truth |
|
||||
| Edge identity + occupancy | `mc-core/src/grid/edge.rs` | ✅ `EdgeId(min_hex, dir_from_min)`, `EdgeOccupant { unit_id, aligned_to, owner_player_id }`, `EdgeFeatures { river, road, bridge, wall_owner }`. Sparse storage in `GridState::edges` and `GridState::edge_features`. |
|
||||
| Edge passability + move validation | `mc-core/src/grid/mod.rs` | ✅ `GridState::is_edge_passable_for(edge, player_id)`, `validate_centre_to_centre_move(from, to, player_id) -> Result<EdgeId, MoveBlockedReason>`. Wall and occupant rules compose. |
|
||||
| Edge terrain (blends — §8) | `mc-core/src/grid/terrain_blend.rs` + `data/terrain/terrain_blends.json` + `data/terrain/land_blends.json` | ✅ `TerrainBlendTable::lookup(host, neighbour)` with canonical-pair sort. 10 canonical Game 1 ecotones. |
|
||||
| Edge terrain (blends — §8) | `mc-core/src/grid/terrain_blend.rs` + `public/resources/tiles/terrain_blends.json` + `public/resources/tiles/land_blends.json` | ✅ `TerrainBlendTable::lookup(host, neighbour)` with canonical-pair sort. 10 canonical Game 1 ecotones. |
|
||||
| River generation | `mc-mapgen/src/lib.rs::generate_rivers` (Stage 7.5) | ✅ Flow downhill from high-moisture / high-elevation sources to the sea. Symmetric edge marking. Deterministic via PCG32. |
|
||||
| River-edges → `edge_features` migration | `mc-core/src/grid/mod.rs::migrate_river_edges_to_edge_features` | ✅ Idempotent, symmetric, preserves non-river features. Called by mc-mapgen post-generation. |
|
||||
| Formation data type | `src/simulator/crates/mc-core/src/formation.rs` | ⚠️ Has `FormationShape` enum but no centre + edge-set partition |
|
||||
|
|
|
|||
|
|
@ -69,11 +69,61 @@ func load_theme(theme_id: String) -> void:
|
|||
_raw[category] = {}
|
||||
_load_from_base("res://public/resources", _RESOURCES_DIR_MAP)
|
||||
_load_from_base("res://public/games/%s/data" % theme_id, _WORLD_DIR_MAP)
|
||||
_apply_subscription_manifest(theme_id)
|
||||
_ecology.deserialize(_raw)
|
||||
BiomeRegistry.rebuild_from_data()
|
||||
_validate_unit_actions()
|
||||
_log_load_summary()
|
||||
|
||||
## Read public/games/<theme>/manifest.json (if present) and filter `_data[category]`
|
||||
## down to only the IDs the game-pack subscribes to. Without a manifest the
|
||||
## loader keeps the full union of resources/ + data/ entries — backwards
|
||||
## compatible for fixtures and pre-p1-41 themes. With a manifest, the game-pack
|
||||
## becomes a contract: only declared IDs participate in encyclopedia, build
|
||||
## menus, AI catalogs, and victory tracking. Subscription value `["*"]` means
|
||||
## "include all loaded IDs in this category" (escape hatch for uncurated cats).
|
||||
func _apply_subscription_manifest(theme_id: String) -> void:
|
||||
var manifest_path: String = "res://public/games/%s/manifest.json" % theme_id
|
||||
if not FileAccess.file_exists(manifest_path):
|
||||
return
|
||||
var file: FileAccess = FileAccess.open(manifest_path, FileAccess.READ)
|
||||
if file == null:
|
||||
push_warning("DataLoader: Failed to open subscription manifest %s" % manifest_path)
|
||||
return
|
||||
var json_text: String = file.get_as_text()
|
||||
file.close()
|
||||
var json: JSON = JSON.new()
|
||||
if json.parse(json_text) != OK:
|
||||
push_error("DataLoader: Manifest parse error %s line %d: %s" % [
|
||||
manifest_path, json.get_error_line(), json.get_error_message()])
|
||||
return
|
||||
if not (json.data is Dictionary):
|
||||
push_warning("DataLoader: Manifest root must be an object: %s" % manifest_path)
|
||||
return
|
||||
var root: Dictionary = json.data as Dictionary
|
||||
if not root.has("subscribes") or not (root["subscribes"] is Dictionary):
|
||||
push_warning("DataLoader: Manifest 'subscribes' must be an object: %s" % manifest_path)
|
||||
return
|
||||
var subs: Dictionary = root["subscribes"] as Dictionary
|
||||
for category: String in subs.keys():
|
||||
if not _data.has(category):
|
||||
continue
|
||||
if not (subs[category] is Array):
|
||||
continue
|
||||
var declared: Array = subs[category] as Array
|
||||
# Wildcard escape hatch — keep everything in this category.
|
||||
if declared.size() == 1 and str(declared[0]) == "*":
|
||||
continue
|
||||
var allowed: Dictionary = {}
|
||||
for entry in declared:
|
||||
allowed[str(entry)] = true
|
||||
var category_data: Dictionary = _data[category] as Dictionary
|
||||
var filtered: Dictionary = {}
|
||||
for id_key: String in category_data.keys():
|
||||
if allowed.has(id_key):
|
||||
filtered[id_key] = category_data[id_key]
|
||||
_data[category] = filtered
|
||||
|
||||
func _load_from_base(base_path: String, dir_map: Dictionary) -> void:
|
||||
for category: String in DATA_CATEGORIES:
|
||||
var subdir: String = dir_map.get(category, _WORLD_DIR_MAP.get(category, category))
|
||||
|
|
|
|||
|
|
@ -226,10 +226,14 @@ mod tests {
|
|||
/// End-to-end: parse the actual production `terrain_blends.json` and
|
||||
/// verify the canonical Game 1 blend entries resolve. This protects
|
||||
/// against the data file drifting from the schema.
|
||||
///
|
||||
/// The blend data lives in the canonical `public/resources/tiles/`
|
||||
/// pool (post-p1-40 unified data architecture — single source of
|
||||
/// truth for tile definitions across all three games in the series).
|
||||
#[test]
|
||||
fn production_terrain_blends_json_parses_and_has_canonical_entries() {
|
||||
const PROD_JSON: &str = include_str!(
|
||||
"../../../../../../public/games/age-of-dwarves/data/terrain/terrain_blends.json"
|
||||
"../../../../../../public/resources/tiles/terrain_blends.json"
|
||||
);
|
||||
let table = TerrainBlendTable::from_json_str(PROD_JSON)
|
||||
.expect("production terrain_blends.json must parse");
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue