feat(@projects): ✨ update wasm build and guide deployment workflows
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
b568d85966
commit
2d9554d9ff
26 changed files with 382 additions and 145 deletions
|
|
@ -244,7 +244,7 @@ jobs:
|
|||
archive_name="magic-civilization-$version-wasm.tar.gz"
|
||||
archive_path="$out_dir/$archive_name"
|
||||
mkdir -p "$out_dir/staging/pkg" "$out_dir/staging/guide"
|
||||
cp -r src/simulator/pkg/. "$out_dir/staging/pkg/"
|
||||
cp -r .local/build/wasm/. "$out_dir/staging/pkg/"
|
||||
cp -r public/games/age-of-dwarves/guide/dist/. "$out_dir/staging/guide/"
|
||||
tar -C "$out_dir/staging" -czf "$archive_path" .
|
||||
rm -rf "$out_dir/staging"
|
||||
|
|
|
|||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -50,6 +50,10 @@ build/
|
|||
|
||||
# Rust build artifacts
|
||||
src/simulator/target/
|
||||
# Safety net: src/ is source-only. wasm-pack's default --out-dir is
|
||||
# <crate>/pkg/; build-wasm.sh overrides to .local/build/wasm/, but pin this
|
||||
# path down in case anyone runs wasm-pack directly.
|
||||
src/simulator/pkg/
|
||||
.local/
|
||||
|
||||
# Python bytecode
|
||||
|
|
|
|||
|
|
@ -94,3 +94,16 @@ TypeScript errors") is measurably false at 32 errors. Fix is scoped to
|
|||
`guide-web`, not `devops-engineer`. Dashboard citation for this pass
|
||||
shows the deploy script + one-level-deep import fix + partial
|
||||
re-exports; full `done` unblocks after guide-web closes the drift.
|
||||
|
||||
---
|
||||
|
||||
## Footer — 2026-04-17 (tourguide, path relocation)
|
||||
|
||||
References above to `src/simulator/pkg/` are historical as of this date.
|
||||
The wasm-pack output was relocated to `.local/build/wasm/` per objective
|
||||
p1-11 (`build-output-src-purge`) as part of enforcing the project-wide
|
||||
"build output never under `src/`" rule. See
|
||||
`.claude/instructions/build-output-locations.md` for the canonical rule
|
||||
and `./run verify` step 16 (`_verify_no_build_in_src`) for the
|
||||
mechanical enforcement. Existing entries above are preserved verbatim
|
||||
as a record of the pre-relocation audit and are not rewritten.
|
||||
|
|
|
|||
|
|
@ -8,14 +8,30 @@
|
|||
|
||||
## Totals
|
||||
|
||||
| Status | Count |
|
||||
<table><tr><td valign='top'>
|
||||
|
||||
**By Priority**
|
||||
|
||||
| Priority | ✅ | 🟡 | 🔴 | ❌ | ⚫ | Total |
|
||||
|---|---|---|---|---|---|---|
|
||||
| **P0** | 19 | 4 | 0 | 0 | 0 | 23 |
|
||||
| **P1** | 7 | 2 | 0 | 3 | 0 | 12 |
|
||||
| **P2** | 7 | 6 | 0 | 2 | 0 | 15 |
|
||||
| **P3 (oos)** | 0 | 0 | 0 | 0 | 9 | 9 |
|
||||
| **total** | **33** | **12** | **0** | **5** | **9** | **59** |
|
||||
|
||||
</td><td valign='top' style='padding-left:2em'>
|
||||
|
||||
**Left To Do by Lead**
|
||||
|
||||
| Team Lead | Remaining |
|
||||
|---|---|
|
||||
| ✅ done | 33 |
|
||||
| 🟡 partial | 12 |
|
||||
| 🔴 stub | 0 |
|
||||
| ❌ missing | 5 |
|
||||
| ⚫ oos | 9 |
|
||||
| **total** | **59** |
|
||||
| [shipwright](../team-leads/shipwright.md) | 6 |
|
||||
| [warcouncil](../team-leads/warcouncil.md) | 3 |
|
||||
| [tourguide](../team-leads/tourguide.md) | 3 |
|
||||
| [testwright](../team-leads/testwright.md) | 2 |
|
||||
|
||||
</td></tr></table>
|
||||
|
||||
## P0 — Blockers for "completely playable"
|
||||
|
||||
|
|
@ -42,8 +58,8 @@
|
|||
| [p0-19](p0-19-biome-economy-integration.md) | ✅ done | Biome-driven collectibles → tile yields → happiness end-to-end | — | 2026-04-16 |
|
||||
| [p0-20](p0-20-gpu-mcts-rollouts.md) | 🟡 partial | GPU-accelerated MCTS rollouts for look-ahead decision-making | [warcouncil](../team-leads/warcouncil.md) | 2026-04-17 |
|
||||
| [p0-21](p0-21-audio-system-capability.md) | ✅ done | Audio system capability — manifest + autoload + EventBus wiring | [shipwright](../team-leads/shipwright.md) | 2026-04-17 |
|
||||
| [p0-22](p0-22-sprite-rendering-capability.md) | 🟡 partial | Sprite rendering capability — replace procedural draw_* with texture rendering | [shipwright](../team-leads/shipwright.md) | 2026-04-17 |
|
||||
| [p0-22](p0-22-ultimate-ai-stress-test.md) | 🟡 partial | Ultimate AI stress test — 5 clans, huge map, deep lookahead | [warcouncil](../team-leads/warcouncil.md) | 2026-04-17 |
|
||||
| [p0-23](p0-23-sprite-rendering-capability.md) | 🟡 partial | Sprite rendering capability — replace procedural draw_* with texture rendering | [shipwright](../team-leads/shipwright.md) | 2026-04-17 |
|
||||
|
||||
## P1 — Ship-readiness
|
||||
|
||||
|
|
|
|||
|
|
@ -86,6 +86,37 @@ byte-for-byte port of `rollout::walk`.
|
|||
apricot with `vulkan-tools` + `mesa-vulkan-drivers` added alongside `weston`.
|
||||
Rebooted bootc images now include these without needing the transient script.
|
||||
|
||||
### 2026-04-17 update — fresh A5 attempt post-fix (failed on host SIGTERM)
|
||||
|
||||
After the four WGSL parity fixes landed and GDExtension rebuilt, fresh A5
|
||||
batches were attempted under multiple process-isolation strategies:
|
||||
|
||||
| Strategy | Batch dir | Result |
|
||||
|---|---|---|
|
||||
| plain nohup | `.local/iter/a5-fresh-20260417_122847/` | exit 143, seeds `in_progress` T5–T10 before kill |
|
||||
| nohup + new dir | `.local/iter/a5-final-20260417_122936/` | games launched, no completion.marker written (process killed) |
|
||||
| bash SIGTERM trap | `.local/iter/a5-trap-20260417_123021/` | trap handler received NO signal; script exited rc=143 |
|
||||
| strace signal trace | `.local/iter/a5-strace-20260417_123200/` | revealed autoplay-batch.sh exits status **1** (not 143); no SIGTERM to parent. Root cause: `0/N games produced turn_stats.jsonl` check fires because flatpak Godot scopes end at 3–10s |
|
||||
| `systemd-run --user` | `.local/iter/warcouncil-a5-systemd-*/` | same — service `Active: inactive (dead)` after 2s, scope children SIGTERMed |
|
||||
| `KillMode=none` | `.local/iter/warcouncil-a5-systemd-*` (2nd) | games reached T9–T10 only; same kill pattern |
|
||||
| plain `bash autoplay-batch` synchronous | `.local/iter/a5-direct-123300/` | 10 games with 0-line `turn_stats.jsonl` — games get SIGTERMed during map generation |
|
||||
|
||||
Seven distinct execution strategies, same failure pattern: flatpak Godot
|
||||
scopes SIGTERMed within 3–10s of launch, before any turn completes. Investigation
|
||||
found the signal is NOT delivered by systemd-oomd (failed service), rpm-ostree
|
||||
automatic updates (timer inactive), or apricot-rail-watchdog (emit-only). The
|
||||
actual SIGTERM source could not be identified in the apricot user session.
|
||||
Parallel agent's own batches from earlier the same day (e.g.
|
||||
`.local/batches/blackhammer_tune_20260417_101447/`) completed fine, so the
|
||||
issue is transient/session-bound, NOT a permanent host failure.
|
||||
|
||||
**Fresh A5 verdict — NOT HEALTHY, B5 therefore not launched.** Per
|
||||
warcouncil's integrity rule: we report the measurement failure honestly
|
||||
rather than claim parity-fix-correctness translated into fresh gameplay
|
||||
evidence. Existing p0-01 batch data from pre-parity-fix binary (at
|
||||
`blackhammer_tune_20260417_101447`) still stands as the most recent
|
||||
successful A5/B5 evidence in the repo.
|
||||
|
||||
## Design outline
|
||||
|
||||
- `AbstractRolloutState` — a ~256 byte `#[repr(C)]` `Pod + Zeroable` compression of
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
id: p0-22
|
||||
id: p0-23
|
||||
title: Sprite rendering capability — replace procedural draw_* with texture rendering
|
||||
priority: p0
|
||||
status: partial
|
||||
|
|
@ -26,7 +26,7 @@ evidence:
|
|||
- tools/deploy-guide.sh
|
||||
- .project/history/20260417_guide_web_deploy_audit.md
|
||||
acceptance_audit:
|
||||
build_zero_ts_errors: "🟡 — `pnpm typecheck` now reports 0 errors in both `src/packages/guide` (was 488) and `public/games/age-of-dwarves/guide` (was 221). Vite build transforms 3339 modules cleanly; final bundle step fails only on missing `src/simulator/pkg/magic_civ_physics.js` (WASM pkg is a RUN-host artifact built on apricot via `bash build-wasm.sh`, not on EDIT host). Scope-narrow Option 2 landed per CLAUDE.md Game 1 rule: deleted 10 Game-2 pages (Magic schools, Archons, Disciplines, LeyLines, Spells, Ep2/Ep3, Hive, Silvandel). Remaining: run WASM build on apricot and re-run `pnpm build` to produce dist/."
|
||||
build_zero_ts_errors: "🟡 — `pnpm typecheck` now reports 0 errors in both `src/packages/guide` (was 488) and `public/games/age-of-dwarves/guide` (was 221). Vite build transforms 3339 modules cleanly; final bundle step fails only on missing `.local/build/wasm/magic_civ_physics.js` (WASM artifact is per-host, built via `bash src/simulator/build-wasm.sh` locally or on apricot and rsync'd; path relocated from `src/simulator/pkg/` → `.local/build/wasm/` per p1-11 on 2026-04-17 — build output never under src/). Scope-narrow Option 2 landed per CLAUDE.md Game 1 rule: deleted 10 Game-2 pages (Magic schools, Archons, Disciplines, LeyLines, Spells, Ep2/Ep3, Hive, Silvandel). Remaining: run WASM build and re-run `pnpm build` to produce dist/."
|
||||
deployed_to_public_url: "✗ — no external hosting target committed for Early Access. `tools/deploy-guide.sh apricot` ships dist/ to apricot:~/public/guide/<version>/ for LAN preview; zip mode produces a handoff artifact for whichever public host wins. External hosting decision remains out-of-scope for this objective."
|
||||
game_json_import_fixed: "✓ — public/games/age-of-dwarves/guide/src/app/guide-data.ts import resolves to the canonical public/games/age-of-dwarves/game.json. Vite transform phase passes cleanly; verified as part of the full 3339-module transform in this pass."
|
||||
data_driven_content: "✓ — the guide reads data via `import.meta.glob('../../../games/age-of-dwarves/data/*.json')` per guide CLAUDE.md convention. Architecture unchanged. Changing a deposit JSON propagates on the next `pnpm build`."
|
||||
|
|
@ -59,7 +59,7 @@ Per CLAUDE.md's hard Game-1 scope rule (*"do NOT ship Game 2 features into Game
|
|||
- **Path aliases:** consumer's `@magic-civ/*` paths were off-by-one (`../../../` → `../../../../`); fixed. Added `@magic-civ/web-civmap` alias to guide-engine's tsconfig.
|
||||
- **Null-guard fixes:** eight consumer pages now guard optional fields before dereferencing (UnitsPage, CommunicationsPage, EncyclopediaModal, EncyclopediaPage, WondersPage, LairsPage, LensesPage, DevSpritesPage).
|
||||
|
||||
**Remaining blocker to flip ✅ done:** `pnpm --filter @magic-civilization/guide-age-of-dwarves build` fails at the final rollup step because `src/simulator/pkg/magic_civ_physics.js` is absent on the EDIT host (WASM is an apricot-built artifact per CLAUDE.md two-host workflow). Apricot was unreachable during this pass (`ssh lilith@apricot.local` timed out). Once apricot is reachable:
|
||||
**Remaining blocker to flip ✅ done:** `pnpm --filter @magic-civilization/guide-age-of-dwarves build` fails at the final rollup step because `.local/build/wasm/magic_civ_physics.js` is absent on the EDIT host (WASM is a per-host artifact; see `.claude/instructions/build-output-locations.md`, path was relocated from `src/simulator/pkg/` per p1-11 on 2026-04-17). Apricot was unreachable during the initial audit pass (`ssh lilith@apricot.local` timed out). Once apricot is reachable:
|
||||
|
||||
```
|
||||
ssh "$AUTOPLAY_HOST" "cd $PROJECT_ROOT_REMOTE/src/simulator && bash build-wasm.sh"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
id: p2-14
|
||||
title: Additional playable races beyond Dwarves — Game 2+
|
||||
priority: p2
|
||||
priority: p3
|
||||
status: oos
|
||||
scope: game2
|
||||
updated_at: 2026-04-17
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -96,15 +96,18 @@ Dark gold Dwarf palette:
|
|||
## Climate Simulation Subsystem
|
||||
|
||||
The WASM physics engine compiles from `src/simulator/` via `build-wasm.sh`. The guide consumes it
|
||||
via `@magic-civ/physics-rs` (points to `src/simulator/pkg/`).
|
||||
via `@magic-civ/physics-rs` (points to `.local/build/wasm/` at the repo root — gitignored,
|
||||
per-host; build output never lives under `src/`, see `.claude/instructions/build-output-locations.md`).
|
||||
|
||||
- **Worker:** `src/simulation/simulation.worker.ts` — offloads physics to background thread
|
||||
- **Hook:** `src/hooks/useClimateSimulation.ts` — React hook wrapping worker communication
|
||||
- **Rendering:** `src/components/climate-sim/HexGLRenderer.tsx` — Three.js WebGL canvas
|
||||
|
||||
Rebuilding WASM: `cd src/simulator && bash build-wasm.sh`
|
||||
Rebuilding WASM: `cd src/simulator && bash build-wasm.sh` → output at `.local/build/wasm/`.
|
||||
|
||||
Never edit `src/simulator/pkg/` directly — fix Rust source and rebuild.
|
||||
Never edit `.local/build/wasm/` directly — fix Rust source and rebuild. Do NOT resurrect
|
||||
`src/simulator/pkg/` — `./run verify` step 16 (`_verify_no_build_in_src`) fails the regression
|
||||
gate if content appears there.
|
||||
|
||||
## Safety Notes
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,10 @@
|
|||
// scoped to the modules the consumer actually imports.
|
||||
|
||||
// ─── @magic-civ/physics-rs ambient stub ───────────────────────────────────
|
||||
// The real WASM pkg is built on apricot; EDIT host has no `pkg/` dir.
|
||||
// The real WASM output lives at `.local/build/wasm/` (gitignored, per-host).
|
||||
// Build locally via `(cd src/simulator && bash build-wasm.sh)` or rsync from
|
||||
// apricot. Build output NEVER under src/ — see
|
||||
// .claude/instructions/build-output-locations.md.
|
||||
declare module '@magic-civ/physics-rs' {
|
||||
export class WasmClimatePhysics {
|
||||
constructor(paramsJson: string, terrainJson: string, specJson: string)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
cmd_build_wasm() {
|
||||
echo -e "${BLUE}Building WASM (simulator → guide)...${NC}"
|
||||
(cd "$SIMULATOR_DIR" && bash build-wasm.sh "$@")
|
||||
echo -e "${GREEN}✓ WASM built → src/simulator/pkg/${NC}"
|
||||
echo -e "${GREEN}✓ WASM built → .local/build/wasm/${NC}"
|
||||
}
|
||||
|
||||
cmd_build_gdext() {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,18 @@ cmd_editor() {
|
|||
}
|
||||
|
||||
cmd_guide() {
|
||||
# Pre-check: surface WASM artifact location issues before Vite starts so
|
||||
# alias-resolution errors are self-describing instead of a cryptic
|
||||
# "Rollup failed to resolve import".
|
||||
if [ -d "$REPO_ROOT/src/simulator/pkg" ] && [ -n "$(ls -A "$REPO_ROOT/src/simulator/pkg" 2>/dev/null)" ]; then
|
||||
echo -e "${YELLOW}warning: src/simulator/pkg/ has content — build output must live in .local/build/wasm/ (src/ is source-only).${NC}"
|
||||
echo -e "${YELLOW} Re-run: (cd src/simulator && bash build-wasm.sh)${NC}"
|
||||
fi
|
||||
if [ ! -f "$REPO_ROOT/.local/build/wasm/magic_civ_physics.js" ]; then
|
||||
echo -e "${YELLOW}warning: .local/build/wasm/magic_civ_physics.js missing — WASM not built locally.${NC}"
|
||||
echo -e "${YELLOW} Build locally: (cd src/simulator && bash build-wasm.sh)${NC}"
|
||||
echo -e "${YELLOW} Or rsync from apricot: rsync -a \"\$AUTOPLAY_HOST:\$PROJECT_ROOT_REMOTE/.local/build/wasm/\" .local/build/wasm/${NC}"
|
||||
fi
|
||||
echo -e "${BLUE}Starting guide dev server (port 5800)...${NC}"
|
||||
pnpm --prefix "$GUIDE_DIR" dev
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,12 +85,17 @@ cmd_verify() {
|
|||
echo -e "${BLUE}─────────────────────────────────────────────────${NC}"
|
||||
}
|
||||
|
||||
local TOTAL=16
|
||||
local TOTAL=17
|
||||
|
||||
# Step 0 — Game data schema validation
|
||||
_verify_step 0 $TOTAL "game data JSON schemas" \
|
||||
python3 "$REPO_ROOT/tools/validate-game-data.py"
|
||||
|
||||
# Step 16 — "Build output never under src/" invariant.
|
||||
# Rule source: .claude/instructions/build-output-locations.md.
|
||||
_verify_step 16 $TOTAL "no build output under src/" \
|
||||
_verify_no_build_in_src
|
||||
|
||||
# Step 1 — i18n: no hardcoded user-visible strings outside ThemeVocabulary
|
||||
_verify_step 1 $TOTAL "i18n: no hardcoded UI strings" \
|
||||
python3 "$REPO_ROOT/tools/validate-i18n.py"
|
||||
|
|
@ -230,6 +235,28 @@ _verify_file_size_cap() {
|
|||
return 0
|
||||
}
|
||||
|
||||
_verify_no_build_in_src() {
|
||||
# Enforce: src/ is source-only. Covers wasm-pack's default <crate>/pkg/
|
||||
# and cargo's default target/ — both must be redirected to .local/build/**.
|
||||
# Rule doc: .claude/instructions/build-output-locations.md.
|
||||
local violations=0
|
||||
if [ -d "$REPO_ROOT/src/simulator/pkg" ] && [ -n "$(ls -A "$REPO_ROOT/src/simulator/pkg" 2>/dev/null)" ]; then
|
||||
echo -e "${RED}src/simulator/pkg/ has content — wasm-pack output must go to .local/build/wasm/${NC}"
|
||||
violations=$(( violations + 1 ))
|
||||
fi
|
||||
while IFS= read -r target_dir; do
|
||||
if [ -n "$(ls -A "$target_dir" 2>/dev/null)" ]; then
|
||||
echo -e "${RED}${target_dir#$REPO_ROOT/} has content — cargo target must go to .local/build/rust/${NC}"
|
||||
violations=$(( violations + 1 ))
|
||||
fi
|
||||
done < <(find "$REPO_ROOT/src" -type d -name target 2>/dev/null)
|
||||
if [ "$violations" -gt 0 ]; then
|
||||
echo -e "${RED}Rule: build output is never inside src/ (see .claude/instructions/build-output-locations.md).${NC}"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
_verify_autoplay_smoke() {
|
||||
# Skips when no RUN host and no local flatpak — dev boxes without a batch
|
||||
# target still get the rest of the pipeline.
|
||||
|
|
|
|||
|
|
@ -51,7 +51,8 @@ var _movement_range: Dictionary = {}
|
|||
## Animation pixel overrides: unit_id -> Vector2 (mid-tween position)
|
||||
var _anim_pixels: Dictionary = {}
|
||||
|
||||
## Unit sprite cache: type_id -> Texture2D (null if not available)
|
||||
## Unit sprite cache: sprite_key -> Texture2D (null if not available)
|
||||
## Keys are composed as "<type_id>_<race_id>_<sex>" or bare "<type_id>" fallback.
|
||||
var _unit_sprite_cache: Dictionary = {}
|
||||
|
||||
## Local player index — used to determine which units are "enemy" for fog gating.
|
||||
|
|
@ -85,16 +86,20 @@ func sync_units(units: Array) -> void:
|
|||
if uid == "":
|
||||
continue
|
||||
var tid: String = _resolve_type_id(unit)
|
||||
var race_id: String = str(unit.get("race_id") if "race_id" in unit else "")
|
||||
var sex: String = str(unit.get("sex") if "sex" in unit else "")
|
||||
_units[uid] = {
|
||||
"position": unit.position,
|
||||
"color": _get_unit_color(unit),
|
||||
"label": _get_unit_label(unit),
|
||||
"type_id": tid,
|
||||
"race_id": race_id,
|
||||
"sex": sex,
|
||||
"hp": unit.hp,
|
||||
"max_hp": unit.max_hp,
|
||||
"owner": unit.owner,
|
||||
}
|
||||
_cache_unit_sprite(tid)
|
||||
_cache_unit_sprites(tid, race_id, sex)
|
||||
queue_redraw()
|
||||
|
||||
|
||||
|
|
@ -172,29 +177,32 @@ func _draw() -> void:
|
|||
var pos: Vector2i = data["position"]
|
||||
pixel = HexUtilsScript.axial_to_pixel(pos) + HexUtilsScript.hex_center
|
||||
|
||||
# Try sprite first, fall back to colored circle
|
||||
# Baseline: always draw the colored circle + label (never removed)
|
||||
var color: Color = data.get("color", Color.WHITE)
|
||||
draw_circle(pixel, UNIT_RADIUS, color)
|
||||
|
||||
var label_text: String = data.get("label", "?")
|
||||
var font: Font = ThemeDB.fallback_font
|
||||
var font_size: int = 18
|
||||
var text_size: Vector2 = font.get_string_size(
|
||||
label_text, HORIZONTAL_ALIGNMENT_CENTER, -1, font_size
|
||||
)
|
||||
var text_pos: Vector2 = (
|
||||
pixel - text_size * 0.5 + Vector2(0, text_size.y * 0.35)
|
||||
)
|
||||
draw_string(
|
||||
font, text_pos, label_text,
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, Color.WHITE,
|
||||
)
|
||||
|
||||
# Optional overlay: draw sprite on top when available
|
||||
var type_id: String = data.get("type_id", "")
|
||||
var sprite: Texture2D = _get_unit_sprite(type_id)
|
||||
var race_id: String = data.get("race_id", "")
|
||||
var sex: String = data.get("sex", "")
|
||||
var sprite: Texture2D = _get_unit_sprite(type_id, race_id, sex)
|
||||
if sprite != null:
|
||||
var tex_size: Vector2 = sprite.get_size()
|
||||
draw_texture(sprite, pixel - tex_size * 0.5)
|
||||
else:
|
||||
var color: Color = data.get("color", Color.WHITE)
|
||||
draw_circle(pixel, UNIT_RADIUS, color)
|
||||
|
||||
var label_text: String = data.get("label", "?")
|
||||
var font: Font = ThemeDB.fallback_font
|
||||
var font_size: int = 18
|
||||
var text_size: Vector2 = font.get_string_size(
|
||||
label_text, HORIZONTAL_ALIGNMENT_CENTER, -1, font_size
|
||||
)
|
||||
var text_pos: Vector2 = (
|
||||
pixel - text_size * 0.5 + Vector2(0, text_size.y * 0.35)
|
||||
)
|
||||
draw_string(
|
||||
font, text_pos, label_text,
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, Color.WHITE,
|
||||
)
|
||||
|
||||
# Draw HP bar if damaged
|
||||
_draw_hp_bar(pixel, data)
|
||||
|
|
@ -343,14 +351,18 @@ func _on_unit_created(unit: RefCounted, _player_index: int) -> void:
|
|||
if uid == "":
|
||||
return
|
||||
var tid: String = _resolve_type_id(u)
|
||||
var race_id: String = str(u.get("race_id") if "race_id" in u else "")
|
||||
var sex: String = str(u.get("sex") if "sex" in u else "")
|
||||
_units[uid] = {
|
||||
"position": u.position,
|
||||
"color": _get_unit_color(u),
|
||||
"label": _get_unit_label(u),
|
||||
"type_id": tid,
|
||||
"race_id": race_id,
|
||||
"sex": sex,
|
||||
"owner": u.owner,
|
||||
}
|
||||
_cache_unit_sprite(tid)
|
||||
_cache_unit_sprites(tid, race_id, sex)
|
||||
queue_redraw()
|
||||
|
||||
|
||||
|
|
|
|||
13
src/packages/guide/src/types/ambient.d.ts
vendored
13
src/packages/guide/src/types/ambient.d.ts
vendored
|
|
@ -1,10 +1,13 @@
|
|||
// ─── @magic-civ/physics-rs ambient stub ───────────────────────────────────
|
||||
//
|
||||
// The WASM build output lives at `src/simulator/pkg/` on the RUN host only
|
||||
// (see CLAUDE.md two-host workflow: WASM builds run on apricot; EDIT host
|
||||
// has no Rust toolchain and `pkg/` is gitignored there). Guide-engine's
|
||||
// tsc --noEmit therefore can't resolve the real `.d.ts` shipped with the
|
||||
// WASM pkg.
|
||||
// The WASM build output lives at `.local/build/wasm/` (gitignored, per-host).
|
||||
// Build output NEVER under src/ — see
|
||||
// .claude/instructions/build-output-locations.md. WASM builds typically run
|
||||
// on apricot (two-host workflow) and rsync back; EDIT host can also build
|
||||
// locally via `(cd src/simulator && bash build-wasm.sh)` when rustup +
|
||||
// wasm-pack are installed. Guide-engine's tsc --noEmit therefore can't
|
||||
// resolve the real `.d.ts` shipped with the WASM output from source-only
|
||||
// tree checks.
|
||||
//
|
||||
// This file is a pure ambient declaration file (no imports/exports —
|
||||
// intentional, so these `declare module` blocks register globally). It
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "@magic-civ/physics-rs",
|
||||
"version": "0.1.0",
|
||||
"description": "Rust physics engine — compiled to WASM for web, GDExtension for Godot",
|
||||
"main": "pkg/magic_civ_physics.js",
|
||||
"types": "pkg/magic_civ_physics.d.ts",
|
||||
"description": "Rust physics engine — compiled to WASM for web, GDExtension for Godot. Build output lives at repo-root .local/build/wasm/ (gitignored, per-host). Consumers resolve via Vite/Vitest alias, not the main/types fields below — those are informational only.",
|
||||
"main": "../../.local/build/wasm/magic_civ_physics.js",
|
||||
"types": "../../.local/build/wasm/magic_civ_physics.d.ts",
|
||||
"scripts": {
|
||||
"build": "bash build-wasm.sh",
|
||||
"build:gdext": "bash build-gdext.sh"
|
||||
},
|
||||
"files": ["pkg/"]
|
||||
"files": []
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@ src/simulator/crates/mc-* ← SOURCE OF TRUTH (pure Rust)
|
|||
│
|
||||
├─ native test: cargo test -p mc-<domain> --test golden
|
||||
│
|
||||
├─ api-wasm → pkg/magic_civ_physics_bg.wasm
|
||||
├─ api-wasm → .local/build/wasm/magic_civ_physics_bg.wasm
|
||||
│ (build output never under src/ — see
|
||||
│ .claude/instructions/build-output-locations.md)
|
||||
│ consumed by: pnpm --filter guide-age-of-dwarves test golden
|
||||
│ (Vitest test runs inside simulation.worker.ts)
|
||||
│
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ Modules live at `.claude/instructions/<file>.md` (symlink resolves to `tooling/c
|
|||
| Porting code/data from `@magic-civilization.messy/` | `atomic-porting.md` |
|
||||
| Writing tests, `--headless` compatibility | `headless-tests.md` |
|
||||
| Ad-hoc shell/python pipelines — when to extract to `scripts/` | `scripts-extraction.md` |
|
||||
| Cargo target, Godot exports, WASM output, `.local/build/**` | `build-output-locations.md` |
|
||||
| Cargo target, Godot exports, WASM output, `.local/build/**` — build output **NEVER** under `src/` (enforced by `./run verify`) | `build-output-locations.md` |
|
||||
| `ThemeAssets`, `EventBus`, `/tmp` rule, rsync binary rule | `safety-rules-local.md` |
|
||||
| Which language-standards file (global) to load | `language-standards.md` |
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,10 @@ src/packages/
|
|||
scenarios.ts — all scenario definitions
|
||||
index.ts — re-exports
|
||||
|
||||
src/simulator/pkg/ — @magic-civ/physics-rs wasm-pack output
|
||||
.local/build/wasm/ — @magic-civ/physics-rs wasm-pack output (gitignored,
|
||||
built per-host via src/simulator/build-wasm.sh).
|
||||
Build output NEVER lives under src/ — see
|
||||
.claude/instructions/build-output-locations.md.
|
||||
magic_civ_physics.js — WASM bindings (top-level await for init)
|
||||
magic_civ_physics.d.ts
|
||||
magic_civ_physics_bg.wasm
|
||||
|
|
@ -53,7 +56,7 @@ src/simulator/pkg/ — @magic-civ/physics-rs wasm-pack output
|
|||
| `@worlds/` | `public/resources/worlds/` |
|
||||
| `@magic-civ/engine-ts` | `src/packages/engine-ts/src/index.ts` |
|
||||
| `@magic-civ/guide-engine` | `src/packages/guide/src/index.ts` |
|
||||
| `@magic-civ/physics-rs` | `src/simulator/pkg/magic_civ_physics.js` |
|
||||
| `@magic-civ/physics-rs` | `.local/build/wasm/magic_civ_physics.js` |
|
||||
|
||||
Configured in `public/games/age-of-dwarves/guide/vite.config.ts` and `tsconfig.json`.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
name: simulator-infra
|
||||
description: Use for Rust workspace structure (src/simulator/Cargo.toml, crate skeletons), api-wasm crate (wasm-bindgen surface), api-gdext crate (godot-rust surface), build scripts (build-wasm.sh, build-gdext.sh), wasm-pack output (pkg/), Cargo feature flags, cross-compilation targets, dependency management across crates.
|
||||
description: Use for Rust workspace structure (src/simulator/Cargo.toml, crate skeletons), api-wasm crate (wasm-bindgen surface), api-gdext crate (godot-rust surface), build scripts (build-wasm.sh, build-gdext.sh), wasm-pack output (.local/build/wasm/), Cargo feature flags, cross-compilation targets, dependency management across crates.
|
||||
---
|
||||
|
||||
You are the Rust simulator infrastructure specialist for Magic Civilization. You own the Cargo workspace layout, the two API surface crates (WASM + GDExtension), build scripts, and the plumbing that connects domain crates to their consumers. You do NOT implement domain logic — that belongs to the domain-specialist agents.
|
||||
|
|
@ -11,7 +11,8 @@ You are the Rust simulator infrastructure specialist for Magic Civilization. You
|
|||
src/simulator/
|
||||
Cargo.toml — workspace root (resolver = "2", lists all members)
|
||||
Cargo.lock
|
||||
build-wasm.sh — wasm-pack build api-wasm --target bundler --out-dir ../pkg
|
||||
build-wasm.sh — wasm-pack build api-wasm --target bundler --out-dir ../../.local/build/wasm
|
||||
(build output never under src/ — see build-output-locations.md)
|
||||
build-gdext.sh — cargo build --release -p api-gdext --target $TARGET; copies .so
|
||||
|
||||
crates/ — domain logic crates (pure Rust + serde, no wasm/gdext deps)
|
||||
|
|
@ -37,11 +38,19 @@ src/simulator/
|
|||
src/lib.rs — GdClimatePhysics, GdCombatResolver, GdAiPlayer, ...
|
||||
Cargo.toml — depends on domain crates + godot (no wasm-bindgen dep)
|
||||
|
||||
pkg/ — wasm-pack output (gitignored except package.json)
|
||||
magic_civ_physics.js
|
||||
magic_civ_physics.d.ts
|
||||
magic_civ_physics_bg.wasm
|
||||
package.json — "@magic-civ/physics-rs" package entry
|
||||
package.json — "@magic-civ/physics-rs" pnpm workspace entry
|
||||
(wasm-pack output lives at .local/build/wasm/,
|
||||
NOT inside this src/ tree — see
|
||||
.claude/instructions/build-output-locations.md)
|
||||
```
|
||||
|
||||
WASM build artifacts land at repo-root `.local/build/wasm/`:
|
||||
|
||||
```
|
||||
.local/build/wasm/
|
||||
magic_civ_physics.js
|
||||
magic_civ_physics.d.ts
|
||||
magic_civ_physics_bg.wasm
|
||||
```
|
||||
|
||||
## Workspace Rules
|
||||
|
|
@ -55,7 +64,8 @@ src/simulator/
|
|||
```bash
|
||||
# Web guide WASM
|
||||
cd src/simulator && bash build-wasm.sh
|
||||
# Output: pkg/ (imported by public/games/age-of-dwarves/guide/ as @magic-civ/physics-rs)
|
||||
# Output: .local/build/wasm/ (repo-root, gitignored; imported by
|
||||
# public/games/age-of-dwarves/guide/ as @magic-civ/physics-rs via Vite alias)
|
||||
|
||||
# Godot GDExtension (Linux dev)
|
||||
cd src/simulator && bash build-gdext.sh
|
||||
|
|
@ -75,7 +85,7 @@ cargo test --workspace # from src/simulator/
|
|||
|
||||
## WASM Top-Level Await Warning
|
||||
|
||||
`pkg/magic_civ_physics.js` uses top-level await for WASM init. If two modules in the same Web Worker module graph both statically import this file, the worker hangs silently. The fix in `src/packages/engine-ts/src/runner.ts` re-exports WASM classes so the worker only needs one import path. Do not break this pattern.
|
||||
`.local/build/wasm/magic_civ_physics.js` uses top-level await for WASM init. If two modules in the same Web Worker module graph both statically import this file, the worker hangs silently. The fix in `src/packages/engine-ts/src/runner.ts` re-exports WASM classes so the worker only needs one import path. Do not break this pattern.
|
||||
|
||||
## GDExtension Class Naming
|
||||
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ tooling/claude/
|
|||
| `atomic-porting.md` | Porting code/data from `@magic-civilization.messy/` | ~200 |
|
||||
| `headless-tests.md` | Writing GUT tests / `--headless` compatibility | ~400 |
|
||||
| `scripts-extraction.md` | Inline pipelines → `scripts/` rule | ~300 |
|
||||
| `build-output-locations.md` | `.local/build/**` topology, artifact paths | ~200 |
|
||||
| `build-output-locations.md` | `.local/build/**` topology, artifact paths (ENFORCED by `./run verify` step 16) | ~450 |
|
||||
| `safety-rules-local.md` | ThemeAssets, EventBus, rsync binaries, `/tmp` avoidance | ~500 |
|
||||
| `language-standards.md` | Which `~/.claude/instructions/<lang>-code-standards.md` to load | ~150 |
|
||||
|
||||
|
|
|
|||
|
|
@ -2,13 +2,66 @@
|
|||
|
||||
**Load when:** configuring build tools, troubleshooting "where did the output go?", auditing `.local/`, or writing `.gitignore` entries for generated artifacts.
|
||||
|
||||
All build artifacts land under `.local/build/` (gitignored, per-host, ~25GB typical):
|
||||
## Hard rule: build output is never inside `src/`
|
||||
|
||||
`src/` is source-only. Every generated / built artifact — Rust `target/`,
|
||||
wasm-pack `pkg/`, TypeScript `dist/`, Godot exports, anything else a tool
|
||||
emits — goes under `.local/build/**` (gitignored, per-host) or under a
|
||||
package-local `dist/` outside of `src/`.
|
||||
|
||||
This rule exists because of two incidents:
|
||||
|
||||
1. **Rust `target/` inside `src/` (historical)** — cargo's default target
|
||||
sat at `src/simulator/target/` and accidentally committed ~25 GB /
|
||||
65k files to git before being purged. Target was relocated to
|
||||
`.local/build/rust/` via `src/simulator/.cargo/config.toml`.
|
||||
2. **wasm-pack `pkg/` inside `src/` (2026-04-17)** — `build-wasm.sh`
|
||||
used wasm-pack's default `--out-dir ../pkg` and emitted
|
||||
`src/simulator/pkg/`, violating the rule. `pkg/` was relocated to
|
||||
`.local/build/wasm/` via `--out-dir ../../.local/build/wasm`.
|
||||
|
||||
`./run verify` enforces this invariant — step 16
|
||||
`_verify_no_build_in_src` fails the regression gate if
|
||||
`src/simulator/pkg/` or any `src/**/target/` tree has content. `.gitignore`
|
||||
also explicitly lists `src/simulator/pkg/` as a belt-and-suspenders
|
||||
safety net in case anyone runs wasm-pack directly without the script.
|
||||
|
||||
**If you find yourself writing into `src/**/target/`, `src/**/pkg/`,
|
||||
`src/**/dist/`, or `src/**/build/`, stop — something's misconfigured.**
|
||||
|
||||
## Canonical artifact paths
|
||||
|
||||
| System | Output path | Configured in |
|
||||
|---|---|---|
|
||||
| Rust (cargo) | `.local/build/rust/` | `src/simulator/.cargo/config.toml` |
|
||||
| Godot exports | `.local/build/godot/<version>/<platform>/` | `scripts/run/remote.sh` |
|
||||
| WASM (wasm-pack) | `src/simulator/pkg/` | `src/simulator/build-wasm.sh` (wasm-pack default) |
|
||||
| TypeScript (vite) | `<pkg>/dist/` per package | each `vite.config.ts` (vite default) |
|
||||
| Godot exports | `.local/build/godot/<version>/<platform>/` | `scripts/run/remote.sh`, `scripts/run/export.sh` |
|
||||
| WASM (wasm-pack) | `.local/build/wasm/` | `src/simulator/build-wasm.sh` (`--out-dir` override) |
|
||||
| GDExtension binary | `src/game/engine/addons/magic_civ_physics/*.{so,dll,dylib}` | `src/simulator/build-gdext.sh` |
|
||||
| TypeScript (Vite) | `<pkg>/dist/` per package | each `vite.config.ts` (Vite default) |
|
||||
|
||||
**`src/` is source-only** — never commit build artifacts there. Rust's `target/` used to sit at `src/simulator/target/` and got 65k files (~25GB) committed to git by accident; that's been purged. If you find yourself writing into `src/**/target/`, `src/**/dist/`, or `src/**/build/`, stop — something's misconfigured.
|
||||
The GDExtension output is the one exception where a build product
|
||||
lands inside `src/` — Godot's GDExtension loader requires the binary
|
||||
to live next to the `.gdextension` manifest, so the path is dictated
|
||||
by the consumer runtime, not our build system. Those files are
|
||||
gitignored (`.gitignore:69-72`) and built per-host from a different
|
||||
build script (`build-gdext.sh`) that reads from `.local/build/rust/`.
|
||||
|
||||
## When a Vite alias or Dockerfile references the WASM output
|
||||
|
||||
Use `.local/build/wasm/magic_civ_physics.js` as the canonical entry
|
||||
point. Concrete examples:
|
||||
|
||||
```ts
|
||||
// public/games/age-of-dwarves/guide/vite.config.ts
|
||||
'@magic-civ/physics-rs': path.resolve(__dirname, '../../../../.local/build/wasm/magic_civ_physics.js'),
|
||||
```
|
||||
|
||||
```dockerfile
|
||||
# public/games/age-of-dwarves/guide/e2e/Dockerfile.web
|
||||
COPY .local/build/wasm/ ./.local/build/wasm/
|
||||
```
|
||||
|
||||
Do NOT resurrect `src/simulator/pkg/` in any new config. It is
|
||||
gitignored, the verify gate fails on content there, and the doc
|
||||
surfaces below are single-sourced — a stale reference creeping in
|
||||
will be caught by `grep -R "src/simulator/pkg" . --exclude-dir=node_modules`.
|
||||
|
|
|
|||
|
|
@ -29,9 +29,10 @@ src/simulator/crates/ ← SOURCE OF TRUTH (all s
|
|||
├── compiled via api-gdext/ → src/game/addons/magic_civ_physics/*.so/.dll
|
||||
│ ↓ loaded by Godot
|
||||
│ src/game/engine/src/modules/climate/climate.gd ← thin GDExtension wrapper
|
||||
└── compiled via api-wasm/ → src/simulator/pkg/
|
||||
↓ imported by web worker
|
||||
public/games/age-of-dwarves/guide/src/simulation/simulation.worker.ts
|
||||
└── compiled via api-wasm/ → .local/build/wasm/ (NOT src/simulator/pkg/ —
|
||||
↓ imported by web worker build output never under src/,
|
||||
public/games/age-of-dwarves/guide/src/simulation/ see build-output-locations.md)
|
||||
simulation.worker.ts
|
||||
```
|
||||
|
||||
## Build commands
|
||||
|
|
|
|||
|
|
@ -7,10 +7,12 @@
|
|||
# tools/deploy-guide.sh apricot [version] Build + rsync to apricot:~/public/guide/<version>/
|
||||
# tools/deploy-guide.sh zip [version] Build + zip dist/ into .local/guide-<version>.zip
|
||||
#
|
||||
# WASM prerequisite: src/simulator/pkg/ must exist. If the guide's
|
||||
# `@magic-civ/physics-rs` alias can't resolve, the build fails. Rebuild
|
||||
# via: ssh "$AUTOPLAY_HOST" "cd $PROJECT_ROOT_REMOTE/src/simulator && bash build-wasm.sh"
|
||||
# (per CLAUDE.md Two-Host Workflow — WASM is an apricot-side artifact).
|
||||
# WASM prerequisite: .local/build/wasm/magic_civ_physics.js must exist.
|
||||
# (Build output never under src/ — see .claude/instructions/build-output-locations.md.)
|
||||
# If the guide's `@magic-civ/physics-rs` alias can't resolve, the build fails.
|
||||
# Rebuild via: ssh "$AUTOPLAY_HOST" "cd $PROJECT_ROOT_REMOTE/src/simulator && bash build-wasm.sh"
|
||||
# (per CLAUDE.md Two-Host Workflow — WASM is an apricot-side artifact)
|
||||
# or locally via: (cd src/simulator && bash build-wasm.sh).
|
||||
#
|
||||
# External hosting (GitHub Pages / S3 / Cloudflare Pages) is TODO: no public
|
||||
# host has been committed for Early Access. Until that decision lands, `zip`
|
||||
|
|
@ -32,9 +34,10 @@ PKG_NAME="@magic-civilization/guide-age-of-dwarves"
|
|||
mode="${1:-build}"
|
||||
|
||||
_run_build() {
|
||||
if [ ! -f "$REPO_ROOT/src/simulator/pkg/magic_civ_physics.js" ]; then
|
||||
echo -e "${YELLOW}warning: src/simulator/pkg/magic_civ_physics.js missing — WASM may not be built.${NC}"
|
||||
if [ ! -f "$REPO_ROOT/.local/build/wasm/magic_civ_physics.js" ]; then
|
||||
echo -e "${YELLOW}warning: .local/build/wasm/magic_civ_physics.js missing — WASM may not be built.${NC}"
|
||||
echo -e "${YELLOW} Rebuild via: ssh \"\$AUTOPLAY_HOST\" 'cd \$PROJECT_ROOT_REMOTE/src/simulator && bash build-wasm.sh'${NC}"
|
||||
echo -e "${YELLOW} or locally: (cd src/simulator && bash build-wasm.sh)${NC}"
|
||||
fi
|
||||
echo -e "${BLUE}Building $PKG_NAME ...${NC}"
|
||||
(cd "$REPO_ROOT" && pnpm --filter "$PKG_NAME" build)
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ def load_objectives() -> list[Objective]:
|
|||
|
||||
|
||||
def render(objectives: list[Objective]) -> str:
|
||||
by_priority: dict[str, list[Objective]] = {"p0": [], "p1": [], "p2": []}
|
||||
by_priority: dict[str, list[Objective]] = {"p0": [], "p1": [], "p2": [], "p3": []}
|
||||
for o in objectives:
|
||||
by_priority[o.priority].append(o)
|
||||
|
||||
|
|
@ -166,11 +166,52 @@ def render(objectives: list[Objective]) -> str:
|
|||
lines.append("")
|
||||
lines.append("## Totals")
|
||||
lines.append("")
|
||||
lines.append("| Status | Count |")
|
||||
lines.append("|---|---|")
|
||||
for status in ("done", "partial", "stub", "missing", "oos"):
|
||||
lines.append(f"| {STATUS_ICON[status]} {status} | {counts[status]} |")
|
||||
lines.append(f"| **total** | **{total}** |")
|
||||
|
||||
# --- by-priority breakdown ---
|
||||
prio_labels = {"p0": "P0", "p1": "P1", "p2": "P2", "p3": "P3 (oos)"}
|
||||
prio_table: list[str] = [
|
||||
"| Priority | ✅ | 🟡 | 🔴 | ❌ | ⚫ | Total |",
|
||||
"|---|---|---|---|---|---|---|",
|
||||
]
|
||||
for prio in ("p0", "p1", "p2", "p3"):
|
||||
grp = by_priority[prio]
|
||||
row = {s: sum(1 for o in grp if o.status == s) for s in VALID_STATUS}
|
||||
prio_table.append(
|
||||
f"| **{prio_labels[prio]}** "
|
||||
f"| {row['done']} | {row['partial']} | {row['stub']} "
|
||||
f"| {row['missing']} | {row['oos']} | {len(grp)} |"
|
||||
)
|
||||
prio_table.append(
|
||||
f"| **total** | **{counts['done']}** | **{counts['partial']}** "
|
||||
f"| **{counts['stub']}** | **{counts['missing']}** "
|
||||
f"| **{counts['oos']}** | **{total}** |"
|
||||
)
|
||||
|
||||
# --- left-to-do by team lead (partial + stub + missing only) ---
|
||||
by_lead: dict[str, int] = {}
|
||||
for o in objectives:
|
||||
if o.status in ("partial", "stub", "missing") and o.owner:
|
||||
by_lead[o.owner] = by_lead.get(o.owner, 0) + 1
|
||||
lead_table: list[str] = ["| Team Lead | Remaining |", "|---|---|"]
|
||||
for lead, cnt in sorted(by_lead.items(), key=lambda x: -x[1]):
|
||||
lead_table.append(f"| [{lead}](../team-leads/{lead}.md) | {cnt} |")
|
||||
if not by_lead:
|
||||
lead_table.append("| — | 0 |")
|
||||
|
||||
# side-by-side via HTML (works in Forgejo/GitHub markdown)
|
||||
lines.append("<table><tr><td valign='top'>")
|
||||
lines.append("")
|
||||
lines.append("**By Priority**")
|
||||
lines.append("")
|
||||
lines.extend(prio_table)
|
||||
lines.append("")
|
||||
lines.append("</td><td valign='top' style='padding-left:2em'>")
|
||||
lines.append("")
|
||||
lines.append("**Left To Do by Lead**")
|
||||
lines.append("")
|
||||
lines.extend(lead_table)
|
||||
lines.append("")
|
||||
lines.append("</td></tr></table>")
|
||||
lines.append("")
|
||||
|
||||
priority_heading = {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue