feat(@projects/@magic-civilization): ✨ add game-pack subscription manifest task
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
bdc82daeb8
commit
e02cb440fc
5 changed files with 168 additions and 8 deletions
|
|
@ -15,10 +15,10 @@
|
|||
| Priority | ✅ | 🔵 | 🟡 | 🔴 | ❌ | ⚫ | Total |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| **P0** | 43 | 0 | 0 | 0 | 0 | 0 | 43 |
|
||||
| **P1** | 32 | 1 | 8 | 0 | 10 | 1 | 52 |
|
||||
| **P1** | 32 | 1 | 8 | 0 | 11 | 1 | 53 |
|
||||
| **P2** | 31 | 0 | 3 | 1 | 1 | 0 | 36 |
|
||||
| **P3 (oos)** | 3 | 0 | 0 | 0 | 1 | 19 | 23 |
|
||||
| **total** | **109** | **1** | **11** | **1** | **12** | **20** | **154** |
|
||||
| **total** | **109** | **1** | **11** | **1** | **13** | **20** | **155** |
|
||||
|
||||
</td><td valign='top' style='padding-left:2em'>
|
||||
|
||||
|
|
@ -128,6 +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 |
|
||||
| [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 |
|
||||
|
|
|
|||
50
.project/objectives/p1-41-game-pack-subscription-manifest.md
Normal file
50
.project/objectives/p1-41-game-pack-subscription-manifest.md
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
---
|
||||
id: p1-41
|
||||
title: Game-pack subscription manifest + loader filter (Phase B of resources/ unification)
|
||||
priority: p1
|
||||
status: missing
|
||||
scope: game1
|
||||
updated_at: 2026-04-29
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Phase A (`p1-40`) collapsed the data/ override layer into single source of truth at `public/resources/<category>/`. All 155 unit IDs and 159 building IDs now live in resources/, one file each. The data loader still walks both layers — `resources/` then `data/<category>/` — but the data/ side is now empty for those categories.
|
||||
|
||||
Phase B introduces a per-game **subscription manifest** that declares which resource IDs each game uses, and a loader filter that restricts the in-memory `_data` dict to that subset. This is what makes the architecture viable when a second game exists: Age of Dwarves subscribes only to dwarf-relevant content; Age of Kzzykt (Game 2) subscribes to its own roster without inheriting dwarf-specific entities by accident.
|
||||
|
||||
For Game 1 alone, this objective is **architecturally correct but functionally redundant** — Age of Dwarves currently subscribes to 100% of resources/. The objective is filed at `p1` (not deferred to `p3`) because Game 2 work begins as soon as Game 1 ships EA, and the subscription mechanism needs to exist before content for Game 2 starts landing in `resources/<category>/` (otherwise Game 2 races and units would automatically appear in Age of Dwarves saves).
|
||||
|
||||
## 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.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Authoring resources/ content for Game 2 (separate Game 2 milestone).
|
||||
- Building a manifest UI / wizard for game-pack authors (post-EA tooling).
|
||||
- Mod / community-game support — manifest format is the substrate for that, but mod-loading machinery is its own objective.
|
||||
|
||||
## Blocking trigger
|
||||
|
||||
Land this **before any Game-2-specific entity** lands in `resources/<category>/`. Until then it's correct-but-inert. Adding the loader filter without any non-Game-1 content in resources/ has zero observable effect — the test gate is the whole value.
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
{
|
||||
"generated_at": "2026-04-29T20:06:03Z",
|
||||
"generated_at": "2026-04-30T03:33:02Z",
|
||||
"totals": {
|
||||
"oos": 20,
|
||||
"done": 109,
|
||||
"in_progress": 1,
|
||||
"missing": 12,
|
||||
"partial": 11,
|
||||
"stub": 1,
|
||||
"total": 154
|
||||
"missing": 13,
|
||||
"done": 109,
|
||||
"oos": 20,
|
||||
"total": 155
|
||||
},
|
||||
"objectives": [
|
||||
{
|
||||
|
|
@ -860,6 +860,16 @@
|
|||
"updated_at": "2026-04-29",
|
||||
"summary": "Today `public/games/age-of-dwarves/data/{units,buildings,techs}/` mirrors `public/resources/{units,buildings,techs}/` with override semantics: the loader walks resources/ first, then game/data/ overwrites by id. This created three real bugs in the last few sessions:\n1. Audit blind-spot: 9 \"missing\" buildings and 6 \"missing\" food/processing buildings turned out to live in resources/buildings bundled files that the per-file audit missed.\n2. Silent semantic drift: 14 building IDs are defined in both layers with different cost/tech/effects; the data/ version wins by accident-of-loader-order.\n3. Broken tech gates: 6 of 8 ordinary-building duplicates have `tech_required` in resources/ pointing at non-existent techs (`military_doctrine`, `smelting`, `husbandry`, `scholarship`, `ancestor_rites`, `masonry`, `mathematics`). The data/ overrides are the only thing keeping those buildings buildable.\n\nThe right architecture is one source of truth at `public/resources/<category>/`, with `public/games/<game>/` carrying only **game-pack-specific configuration** (clan personalities, setup, vocab, difficulty) and a manifest declaring which resource IDs the game subscribes to. No more override layer.\n\nThis objective is the **safe mechanical phase** — move all entity files to resources/ canonical locations and resolve the duplicates. The behavioral phase (subscription manifest + loader filter) splits to `p1-41`."
|
||||
},
|
||||
{
|
||||
"id": "p1-41",
|
||||
"title": "Game-pack subscription manifest + loader filter (Phase B of resources/ unification)",
|
||||
"priority": "p1",
|
||||
"status": "missing",
|
||||
"scope": "game1",
|
||||
"owner": null,
|
||||
"updated_at": "2026-04-29",
|
||||
"summary": "Phase A (`p1-40`) collapsed the data/ override layer into single source of truth at `public/resources/<category>/`. All 155 unit IDs and 159 building IDs now live in resources/, one file each. The data loader still walks both layers — `resources/` then `data/<category>/` — but the data/ side is now empty for those categories.\n\nPhase B introduces a per-game **subscription manifest** that declares which resource IDs each game uses, and a loader filter that restricts the in-memory `_data` dict to that subset. This is what makes the architecture viable when a second game exists: Age of Dwarves subscribes only to dwarf-relevant content; Age of Kzzykt (Game 2) subscribes to its own roster without inheriting dwarf-specific entities by accident.\n\nFor Game 1 alone, this objective is **architecturally correct but functionally redundant** — Age of Dwarves currently subscribes to 100% of resources/. The objective is filed at `p1` (not deferred to `p3`) because Game 2 work begins as soon as Game 1 ships EA, and the subscription mechanism needs to exist before content for Game 2 starts landing in `resources/<category>/` (otherwise Game 2 races and units would automatically appear in Age of Dwarves saves)."
|
||||
},
|
||||
{
|
||||
"id": "p2-06",
|
||||
"title": "Export pipeline for Windows / macOS / Linux",
|
||||
|
|
|
|||
|
|
@ -121,6 +121,49 @@ pub fn hexes_of_edge(edge: EdgeId) -> [(i32, i32); 2] {
|
|||
[edge.min_hex, (edge.min_hex.0 + dq, edge.min_hex.1 + dr)]
|
||||
}
|
||||
|
||||
/// ZOC reach for a unit standing on `edge` per `HEX_GEOMETRY.md` §9.
|
||||
///
|
||||
/// An edge unit contests both adjacent centres and the four other edges
|
||||
/// that share a vertex with it (two on each adjacent hex). Centres come
|
||||
/// from `hexes_of_edge`; the four vertex-adjacent edges are the
|
||||
/// neighbouring perimeter edges within each adjacent hex.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ZocReach {
|
||||
/// The two hex centres directly bordering this edge.
|
||||
pub centres: [(i32, i32); 2],
|
||||
/// The four edges that share a vertex with this edge (two on each
|
||||
/// adjacent hex). Order is stable for testability:
|
||||
/// `[hex_a_clockwise, hex_a_counterclockwise, hex_b_clockwise, hex_b_counterclockwise]`.
|
||||
pub adjacent_edges: [EdgeId; 4],
|
||||
}
|
||||
|
||||
/// Return the 6 edges projected by ZOC from a centre at `hex`. A centre
|
||||
/// unit projects into all 6 of its host hex's edges per §9.
|
||||
pub fn zoc_from_centre(hex: (i32, i32)) -> [EdgeId; 6] {
|
||||
edges_of_hex(hex)
|
||||
}
|
||||
|
||||
/// Return ZOC reach from a unit on `edge`. Both adjacent centres plus the
|
||||
/// four vertex-adjacent edges per §9.
|
||||
pub fn zoc_from_edge(edge: EdgeId) -> ZocReach {
|
||||
let [hex_a, hex_b] = hexes_of_edge(edge);
|
||||
let dir_a = edge.dir_from_min as usize;
|
||||
let dir_b = reverse_dir(edge.dir_from_min) as usize;
|
||||
|
||||
// Vertex-adjacent edges within a hex are the perimeter neighbours of
|
||||
// the chosen edge — directions `dir + 1` (counterclockwise) and
|
||||
// `dir + 5` (clockwise) modulo 6 in hex.rs's direction ordering.
|
||||
let edge_a_cw = canonical_edge(hex_a, ((dir_a + 5) % 6) as u8);
|
||||
let edge_a_ccw = canonical_edge(hex_a, ((dir_a + 1) % 6) as u8);
|
||||
let edge_b_cw = canonical_edge(hex_b, ((dir_b + 5) % 6) as u8);
|
||||
let edge_b_ccw = canonical_edge(hex_b, ((dir_b + 1) % 6) as u8);
|
||||
|
||||
ZocReach {
|
||||
centres: [hex_a, hex_b],
|
||||
adjacent_edges: [edge_a_cw, edge_a_ccw, edge_b_cw, edge_b_ccw],
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -213,4 +256,59 @@ mod tests {
|
|||
assert!(!f.bridge);
|
||||
assert!(f.wall_owner.is_none());
|
||||
}
|
||||
|
||||
/// Centre ZOC covers exactly the 6 own edges, no duplicates.
|
||||
#[test]
|
||||
fn zoc_from_centre_returns_six_distinct_edges() {
|
||||
let edges = zoc_from_centre((2, -3));
|
||||
for i in 0..6 {
|
||||
for j in (i + 1)..6 {
|
||||
assert_ne!(edges[i], edges[j], "duplicate at {i},{j}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Edge ZOC: 2 centres + 4 distinct vertex-adjacent edges, none equal to the source edge.
|
||||
#[test]
|
||||
fn zoc_from_edge_yields_two_centres_and_four_distinct_adjacent_edges() {
|
||||
let edge = canonical_edge((0, 0), 0); // E edge from origin
|
||||
let reach = zoc_from_edge(edge);
|
||||
|
||||
// Both adjacent centres must be the two hexes that share this edge.
|
||||
let pair = hexes_of_edge(edge);
|
||||
assert!(reach.centres.contains(&pair[0]));
|
||||
assert!(reach.centres.contains(&pair[1]));
|
||||
|
||||
// The four adjacent edges must be distinct from each other and from the source.
|
||||
for i in 0..4 {
|
||||
assert_ne!(
|
||||
reach.adjacent_edges[i], edge,
|
||||
"adjacent_edges[{i}] should not equal the source edge"
|
||||
);
|
||||
for j in (i + 1)..4 {
|
||||
assert_ne!(
|
||||
reach.adjacent_edges[i], reach.adjacent_edges[j],
|
||||
"adjacent_edges[{i}] and [{j}] must be distinct"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `zoc_from_edge` is symmetric: querying via canonical_edge from either side returns the same reach.
|
||||
#[test]
|
||||
fn zoc_from_edge_is_symmetric_across_canonical_form() {
|
||||
for hex in [(0, 0), (3, -2), (-4, 5)] {
|
||||
for dir in 0u8..6 {
|
||||
let (dq, dr) = AXIAL_DIRECTIONS[dir as usize];
|
||||
let other = (hex.0 + dq, hex.1 + dr);
|
||||
let from_a = zoc_from_edge(canonical_edge(hex, dir));
|
||||
let from_b = zoc_from_edge(canonical_edge(other, reverse_dir(dir)));
|
||||
// Same canonical edge → same reach
|
||||
assert_eq!(
|
||||
from_a, from_b,
|
||||
"ZOC reach differs across canonical-edge symmetric inputs at hex={hex:?} dir={dir}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ use std::collections::HashMap;
|
|||
|
||||
pub mod edge;
|
||||
pub use edge::{
|
||||
canonical_edge, edges_of_hex, hexes_of_edge, reverse_dir, EdgeFeatures, EdgeId, EdgeOccupant,
|
||||
canonical_edge, edges_of_hex, hexes_of_edge, reverse_dir, zoc_from_centre, zoc_from_edge,
|
||||
EdgeFeatures, EdgeId, EdgeOccupant, ZocReach,
|
||||
};
|
||||
|
||||
pub mod terrain_blend;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue