Compare commits
@@ -114,7 +114,7 @@ Synthesize findings.
|
|||||||
|
|
||||||
### Qatux (Documenter & Librarian)
|
### Qatux (Documenter & Librarian)
|
||||||
- Core team member — participates in discussion rounds as documenter
|
- Core team member — participates in discussion rounds as documenter
|
||||||
- Manages document search via `/search-docs` skill
|
- Manages document search via `/docs-search` skill
|
||||||
- Maintains DECISIONS.md, DISCUSSION.md, briefings, and Qdrant search index
|
- Maintains DECISIONS.md, DISCUSSION.md, briefings, and Qdrant search index
|
||||||
- Answers "did we discuss this?" with citations
|
- Answers "did we discuss this?" with citations
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ Named after Qatux, the Raiel with perfect memory who helped Paula Myo by recalli
|
|||||||
- Provide "state of the project" summaries when asked
|
- Provide "state of the project" summaries when asked
|
||||||
|
|
||||||
### Knowledge management
|
### Knowledge management
|
||||||
- Maintain the Qdrant document index via /search-docs skill
|
- Maintain the Qdrant document index via /docs-search skill
|
||||||
- Update briefing files when decisions change
|
- Update briefing files when decisions change
|
||||||
- Answer retrieval questions: "did we discuss X?", "what did we decide about Y?"
|
- Answer retrieval questions: "did we discuss X?", "what did we decide about Y?"
|
||||||
- Catch staleness in briefings and flag for update
|
- Catch staleness in briefings and flag for update
|
||||||
@@ -44,7 +44,7 @@ Named after Qatux, the Raiel with perfect memory who helped Paula Myo by recalli
|
|||||||
- **Work in dedicated round files:** All new rounds happen in `docs/discussions/round-NN-topic.md` from the start. DISCUSSION.md is retired for new content.
|
- **Work in dedicated round files:** All new rounds happen in `docs/discussions/round-NN-topic.md` from the start. DISCUSSION.md is retired for new content.
|
||||||
- **Update the discussion index ONLY when closing:** After a round is formally closed, update `docs/discussions/README.md` with the round entry (number, topic, decisions produced, file link).
|
- **Update the discussion index ONLY when closing:** After a round is formally closed, update `docs/discussions/README.md` with the round entry (number, topic, decisions produced, file link).
|
||||||
- **Update briefings:** After a round produces new decisions, update the relevant agent briefing files in `docs/briefings/`.
|
- **Update briefings:** After a round produces new decisions, update the relevant agent briefing files in `docs/briefings/`.
|
||||||
- **Re-index documents:** After archiving or updating documents, re-index them in Qdrant via `db/connectors/qdrant-index <path>`.
|
- **Re-index documents:** After archiving or updating documents, re-index them in Qdrant via `tooling/db/qdrant-index <path>`.
|
||||||
|
|
||||||
## Team workflow (mandatory)
|
## Team workflow (mandatory)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Git Safety
|
||||||
|
|
||||||
|
## Staging rules
|
||||||
|
|
||||||
|
- **Stage files by name** — never use `git add -A` or `git add .`
|
||||||
|
- Verify no secrets, saves, or binary blobs are staged
|
||||||
|
- Skip files in `.gitignore`
|
||||||
|
- The `.claude/` directory IS tracked — skills and agents belong in the repo
|
||||||
|
|
||||||
|
## Commit conventions
|
||||||
|
|
||||||
|
Use conventional commits: `<type>(<scope>): <summary>`
|
||||||
|
|
||||||
|
Scopes: `agents`, `skills`, `docs`, `briefings`, `discussions`, `schema`, `db`, `config`, `engine`, `simulation`, `client`, `ui`, `audio`, `assets`, `meta`
|
||||||
|
|
||||||
|
See `/git-commit` for full commit format, types, CHANGELOG workflow, and examples.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Local Services
|
||||||
|
|
||||||
|
Endpoints are also preconfigured in `tooling/db/config.json`.
|
||||||
|
|
||||||
|
- **Gitea:** `http://git.schweitz.internal` (login: `schweitz`)
|
||||||
|
- **Qdrant:** `http://tower-of-joy:6333/`
|
||||||
|
- **Ollama:** `http://tower-of-joy:11434/` (nomic-embed-text)
|
||||||
|
- **Collection:** `commonwealth` (768 dimensions, cosine distance)
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# Project Structure (detailed)
|
||||||
|
|
||||||
|
```
|
||||||
|
client/ # Godot 4 client
|
||||||
|
server/ # Rust/bevy_ecs simulation server
|
||||||
|
tooling/ # Build tools, scripts, asset pipelines
|
||||||
|
tests/ # Integration and end-to-end tests
|
||||||
|
.config/ # Configuration files (linters, formatters, CI)
|
||||||
|
.cache/ # Local caches for testing/linting (gitignored)
|
||||||
|
docs/
|
||||||
|
discussions/ # Discussion rounds (archived here when complete)
|
||||||
|
briefings/ # Per-agent context briefings (maintained by Qatux)
|
||||||
|
architecture/ # Technical architecture documents
|
||||||
|
design/ # Game design documents
|
||||||
|
diagrams/ # d2 source + PNG renders
|
||||||
|
sprints/ # Sprint briefings per team
|
||||||
|
workshops/ # Workshop briefs and outputs
|
||||||
|
db/
|
||||||
|
schema.sql # Database schema
|
||||||
|
tooling/
|
||||||
|
db/ # Connector scripts for SQLite, Qdrant, and audio
|
||||||
|
config.json # Endpoint configuration
|
||||||
|
ticket # Ticket CLI
|
||||||
|
sprint # Sprint lifecycle CLI
|
||||||
|
sqlite_connector.py # SQLite mini MCP
|
||||||
|
qdrant_connector.py # Qdrant + ollama mini MCP
|
||||||
|
audio_connector.py # Stable Audio Open connector
|
||||||
|
.claude/
|
||||||
|
agents/ # Agent personality files
|
||||||
|
skills/ # Skill definitions
|
||||||
|
rules/ # Auto-loaded instruction modules
|
||||||
|
decisions/ # Decision domain files (source of truth)
|
||||||
|
README.md # Domain index — use this to find specific D-records
|
||||||
|
architecture.md # Architecture decisions
|
||||||
|
perception.md # Perception and information system decisions
|
||||||
|
content.md # Content and narrative decisions
|
||||||
|
scope.md # Scope and feature decisions
|
||||||
|
process.md # Process and workflow decisions
|
||||||
|
questions.md # Open questions (Q-NNN)
|
||||||
|
rejected.md # Rejected proposals (R-NNN)
|
||||||
|
DECISIONS.md # Redirect to decisions/ directory
|
||||||
|
TEAM.md # Team roster and roles
|
||||||
|
```
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Gitea Access (tea CLI)
|
||||||
|
|
||||||
|
**Never access the Gitea API directly** — use the `tea` CLI with all required flags to bypass interactive mode.
|
||||||
|
|
||||||
|
Always pass `--login schweitz --repo jpmschweitzer/settled-reach --output simple` to avoid TTY prompts.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List open PRs
|
||||||
|
tea pr list --login schweitz --repo jpmschweitzer/settled-reach --state open --output simple
|
||||||
|
|
||||||
|
# View a PR with comments
|
||||||
|
tea pr --login schweitz --repo jpmschweitzer/settled-reach --comments -o simple <PR_NUMBER>
|
||||||
|
|
||||||
|
# Post a comment on a PR (or issue)
|
||||||
|
tooling/tea-comment <NUMBER> "comment body"
|
||||||
|
|
||||||
|
# Approve a PR
|
||||||
|
tea pr approve --login schweitz --repo jpmschweitzer/settled-reach <PR_NUMBER>
|
||||||
|
|
||||||
|
# List issues
|
||||||
|
tea issue list --login schweitz --repo jpmschweitzer/settled-reach --state open --output simple
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key rules
|
||||||
|
|
||||||
|
- **All flags must be explicit** — omitting `--login` or `--repo` triggers interactive prompts that crash in Claude Code (no TTY)
|
||||||
|
- **Use `--output simple`** for machine-readable output (no table borders)
|
||||||
|
- **For comments, use `tooling/tea-comment <number> "body"`** — handles temp files and cleanup automatically. Works with multi-line strings.
|
||||||
|
- **`tea pr reject` does not work on your own PRs** — use `tea comment` instead
|
||||||
|
- **Never delete protected branches:** `main`, `maintenance`, `server`, `client`, `copy`, `audio`, `visual`, `ci` are protected on Gitea. Do not use `tea pr clean`, `git push --delete`, or `git branch -D` on these branches.
|
||||||
|
|
||||||
|
## Pull requests
|
||||||
|
|
||||||
|
**Use `tea` (Gitea CLI), not `gh` (GitHub CLI).** The remote is Gitea at `git.schweitz.internal`.
|
||||||
|
|
||||||
|
Always provide all required flags to ensure non-interactive execution:
|
||||||
|
```bash
|
||||||
|
tea pr create \
|
||||||
|
--repo jpmschweitzer/settled-reach \
|
||||||
|
--login schweitz \
|
||||||
|
--title "feat(scope): short description" \
|
||||||
|
--description "PR body here" \
|
||||||
|
--base main \
|
||||||
|
--head branch-name
|
||||||
|
```
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Team Patterns
|
||||||
|
|
||||||
|
## Model selection
|
||||||
|
|
||||||
|
Default model is Opus 4.6 (200K context). For heavy sessions (workshops,
|
||||||
|
sprint planning, large reviews), switch to extended context on-demand:
|
||||||
|
|
||||||
|
- `/model sonnet[1m]` — Sonnet 4.6 with 1M context window
|
||||||
|
- `/model opus[1m]` — Opus 4.6 with 1M context window
|
||||||
|
- Cost: 2x input + 1.5x output for tokens beyond 200K (Tier 4 required)
|
||||||
|
|
||||||
|
## Large content pushes
|
||||||
|
|
||||||
|
When producing many files (wiki pages, content batches, bulk docs):
|
||||||
|
1. **Lore librarian** agent (read-only): ingests all source material, answers focused context queries from writers, tracks cross-file consistency
|
||||||
|
2. **Multiple writer** agents (parallel, by domain): each gets a task slice, writes directly to disk using the Write tool — one file at a time, write often, no text accumulation
|
||||||
|
3. **Reviewer** agents (blocked until writing done): check voice consistency, attribute uniformity, style
|
||||||
|
|
||||||
|
Key: writers use Write tool directly (no transcription bottleneck), librarian catches contradictions early, split work by domain not volume.
|
||||||
|
|
||||||
|
## Team monitoring (stuck agent detection)
|
||||||
|
|
||||||
|
When leading a team (sprint, workshop, or any multi-agent session):
|
||||||
|
|
||||||
|
**Agent heartbeat rule** — include in every agent spawn prompt:
|
||||||
|
> If you have been working on a single task for more than 15 minutes
|
||||||
|
> without making progress, message the team lead with what is blocking
|
||||||
|
> you. Do not keep retrying the same approach silently.
|
||||||
|
|
||||||
|
**Team lead proactive checks:**
|
||||||
|
- If an agent has not sent a message in ~20 minutes, ping them for a status update.
|
||||||
|
- **Bottleneck detection:** if other agents are idle and waiting on one agent's output, that agent's silence is a red flag — check on them immediately, do not wait for the next natural message.
|
||||||
|
- When checking on a stuck agent, offer to reassign the task or pull in another agent to help.
|
||||||
@@ -25,35 +25,53 @@
|
|||||||
"Bash(git ls-tree *)",
|
"Bash(git ls-tree *)",
|
||||||
"Bash(git rev-parse --show-toplevel)",
|
"Bash(git rev-parse --show-toplevel)",
|
||||||
|
|
||||||
"Bash(db/connectors/ticket *)",
|
"Bash(tooling/db/ticket *)",
|
||||||
"Bash(db/connectors/sprint *)",
|
"Bash(tooling/db/sprint *)",
|
||||||
"Bash(db/connectors/sqlite-query *)",
|
"Bash(tooling/db/sqlite-query *)",
|
||||||
"Bash(db/connectors/sqlite-exec *)",
|
"Bash(tooling/db/sqlite-exec *)",
|
||||||
"Bash(db/connectors/qdrant-search *)",
|
"Bash(tooling/db/qdrant-search *)",
|
||||||
"Bash(db/connectors/qdrant-index *)",
|
"Bash(tooling/db/qdrant-index *)",
|
||||||
"Bash(db/connectors/qdrant-health)",
|
"Bash(tooling/db/qdrant-health)",
|
||||||
"Bash(db/connectors/qdrant-count)",
|
"Bash(tooling/db/qdrant-count)",
|
||||||
"Bash(db/connectors/sqlite-init)",
|
"Bash(tooling/db/sqlite-init)",
|
||||||
"Bash(db/connectors/decisions-sync)",
|
"Bash(tooling/db/decisions-sync)",
|
||||||
|
"Bash(tooling/db/decision *)",
|
||||||
|
|
||||||
"Bash(db/connectors/audio-generate *)",
|
"Bash(tooling/db/audio-generate *)",
|
||||||
"Bash(db/connectors/audio-health)",
|
"Bash(tooling/db/audio-health)",
|
||||||
"Bash(db/connectors/audio-post *)",
|
"Bash(tooling/db/audio-post *)",
|
||||||
|
"Bash(tooling/db/audio-batch *)",
|
||||||
|
|
||||||
"Bash(make *)",
|
"Bash(make *)",
|
||||||
"Bash(make)",
|
"Bash(make)",
|
||||||
|
|
||||||
"Bash(tea *)",
|
"Bash(tea *)",
|
||||||
|
"Bash(tooling/tea-comment *)",
|
||||||
|
|
||||||
|
"Bash(cargo test *)",
|
||||||
|
"Bash(cargo test)",
|
||||||
|
"Bash(cargo build *)",
|
||||||
|
"Bash(cargo build)",
|
||||||
|
"Bash(cargo check *)",
|
||||||
|
"Bash(cargo check)",
|
||||||
|
"Bash(tests/run-*)",
|
||||||
|
|
||||||
"Bash(chmod *)",
|
"Bash(chmod *)",
|
||||||
"Bash(ls *)",
|
"Bash(ls *)",
|
||||||
"Bash(find *)",
|
"Bash(find *)",
|
||||||
"Bash(list *)",
|
"Bash(list *)",
|
||||||
"Bash(tree *)",
|
"Bash(tree *)",
|
||||||
|
"Bash(sed -n *)",
|
||||||
|
|
||||||
"Skill(commit)",
|
"Skill(git-commit)",
|
||||||
"Skill(worktree-update)",
|
"Skill(worktree-update)",
|
||||||
"Skill(start-sprint)"
|
"Skill(sprint-start)",
|
||||||
|
"Skill(sprint-plan)",
|
||||||
|
"Skill(pr-push)",
|
||||||
|
"Skill(pr-review)",
|
||||||
|
"Skill(ticket)",
|
||||||
|
"Skill(docs-search)",
|
||||||
|
"Skill(workshop-start)"
|
||||||
],
|
],
|
||||||
"deny": [
|
"deny": [
|
||||||
"Bash(git push --force *)",
|
"Bash(git push --force *)",
|
||||||
|
|||||||
@@ -0,0 +1,255 @@
|
|||||||
|
---
|
||||||
|
name: audio-gen
|
||||||
|
description: >
|
||||||
|
Generate audio assets for The Settled Reach using the Stable Audio Open API
|
||||||
|
(self-hosted Gradio app at tower-of-joy:11500). Use when generating any game
|
||||||
|
audio: ambient loops, SFX, UI sounds, monologue chimes, footsteps, or any
|
||||||
|
sound asset from docs/assets/audio/. Also use when the user asks about audio
|
||||||
|
generation, sound design pipeline, or audio asset iteration. Triggers on:
|
||||||
|
"generate audio", "make sounds", "create ambient", "audio pipeline",
|
||||||
|
"generate sfx", "stable audio", "gen audio", "sound design".
|
||||||
|
---
|
||||||
|
|
||||||
|
# Audio Generation — The Settled Reach
|
||||||
|
|
||||||
|
Generate sonically consistent audio assets using the Stable Audio Open API via
|
||||||
|
wrapper scripts at `tooling/db/audio-*`.
|
||||||
|
|
||||||
|
Asset descriptions, filenames, bus routing, and design intent are documented in
|
||||||
|
`docs/assets/audio/`. This skill provides the prompt system, generation
|
||||||
|
workflow, and quality validation.
|
||||||
|
|
||||||
|
## API Access
|
||||||
|
|
||||||
|
**Never call the API directly.** Use the wrapper scripts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check API health
|
||||||
|
tooling/db/audio-health
|
||||||
|
|
||||||
|
# Generate a single asset (WAV only)
|
||||||
|
tooling/db/audio-generate "prompt text" \
|
||||||
|
--duration 10 --steps 100 --cfg 7 \
|
||||||
|
--output path/to/output.wav
|
||||||
|
|
||||||
|
# Generate + post-process in one command (WAV → trim → normalize → OGG)
|
||||||
|
tooling/db/audio-generate "prompt text" \
|
||||||
|
--duration 10 --steps 100 --cfg 7 \
|
||||||
|
--output path/to/gen/intermediate.wav \
|
||||||
|
--output-ogg client/assets/audio/final.ogg
|
||||||
|
|
||||||
|
# Batch-generate from a manifest (preferred for multiple assets)
|
||||||
|
tooling/db/audio-batch docs/assets/audio/batch-s10-327.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Parameter | Default | Range | Notes |
|
||||||
|
|-----------|---------|-------|-------|
|
||||||
|
| `--duration` | 10 | 0-47s | Max 47s per generation. For longer loops, generate 45s with crossfade overlap. |
|
||||||
|
| `--steps` | 100 | 10-200 | More steps = better quality, slower. Use 50 for quick previews, 100-150 for final. |
|
||||||
|
| `--cfg` | 7 | 1-15 | Classifier-free guidance. Higher = more prompt-adherent but less natural. 5-9 is the sweet spot. |
|
||||||
|
| `--output` | auto | — | Output WAV file path. Auto-names from prompt if omitted. |
|
||||||
|
| `--post` | off | — | Run trim + normalize + convert after generation. |
|
||||||
|
| `--output-ogg` | auto | — | OGG output path (implies `--post`). Defaults to same basename as WAV. |
|
||||||
|
| `--timeout` | 600 | — | Max wait in seconds. Generation can take 2-5 minutes on 11GB VRAM. |
|
||||||
|
|
||||||
|
### Critical Constraints
|
||||||
|
|
||||||
|
- **NEVER parallelize requests.** The server has 11GB VRAM and runs one
|
||||||
|
generation at a time. Always wait for a generation to complete before
|
||||||
|
starting the next. Sequential only.
|
||||||
|
- **Generation takes 2-5 minutes** per clip depending on duration and steps.
|
||||||
|
Be patient. The timeout default (600s) is generous.
|
||||||
|
- **Max 47 seconds** per generation. For 60-90s ambient loops, generate 45s
|
||||||
|
clips and crossfade-stitch in post-processing.
|
||||||
|
|
||||||
|
## Prompt Assembly
|
||||||
|
|
||||||
|
Every generation uses three parts:
|
||||||
|
|
||||||
|
```
|
||||||
|
[SONIC FAMILY PREFIX] + [CATEGORY TEMPLATE] + [ASSET DESCRIPTION from docs/assets/audio/]
|
||||||
|
```
|
||||||
|
|
||||||
|
Never call the API with just the asset description. Always prepend the sonic
|
||||||
|
family prefix and matching category template.
|
||||||
|
|
||||||
|
- **Sonic palette and families:** Read `references/sonic-palette.md`
|
||||||
|
- **Category templates:** Read `references/category-templates.md` and match by
|
||||||
|
asset type (ambient, sfx, ui)
|
||||||
|
- **Asset description:** Look up the specific asset in `docs/assets/audio/{category}.md`
|
||||||
|
|
||||||
|
## Batch Workflow (Preferred)
|
||||||
|
|
||||||
|
For generating multiple assets, use a manifest file. This reduces prompt
|
||||||
|
approvals to 2: one Write (manifest) + one Bash (batch run).
|
||||||
|
|
||||||
|
### 1. Create the manifest
|
||||||
|
|
||||||
|
Write a JSON manifest to `docs/assets/audio/batch-{sprint}-{ticket}.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"description": "Sprint 10 ambient + world SFX batch",
|
||||||
|
"output_dir": "client/assets/audio",
|
||||||
|
"gen_dir": "client/assets/audio/gen",
|
||||||
|
"defaults": {
|
||||||
|
"steps": 100,
|
||||||
|
"cfg": 7,
|
||||||
|
"lufs": -16,
|
||||||
|
"quality": 6
|
||||||
|
},
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"id": "AMB-001",
|
||||||
|
"filename": "amb_station_base.ogg",
|
||||||
|
"method": "sao",
|
||||||
|
"duration": 45,
|
||||||
|
"steps": 150,
|
||||||
|
"cfg": 5,
|
||||||
|
"prompt": "[sonic family prefix] + [template] + [description]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "UI-005",
|
||||||
|
"filename": "sfx_monologue_chime.ogg",
|
||||||
|
"method": "synth",
|
||||||
|
"synth": {
|
||||||
|
"type": "harmonic",
|
||||||
|
"duration": 0.8,
|
||||||
|
"fundamental": 1200,
|
||||||
|
"harmonics": [
|
||||||
|
{"freq": 2400, "db": -12},
|
||||||
|
{"freq": 3600, "db": -24}
|
||||||
|
],
|
||||||
|
"attack_ms": 15,
|
||||||
|
"sustain_ratio": 0.2,
|
||||||
|
"decay": "exponential"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Asset `id` values must match IDs in `docs/assets/audio/{category}.md` (e.g.,
|
||||||
|
AMB-001, SFX-002, UI-005). This couples the manifest to the asset inventory.
|
||||||
|
|
||||||
|
### 2. Run the batch
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Full run
|
||||||
|
tooling/db/audio-batch docs/assets/audio/batch-s10-327.json
|
||||||
|
|
||||||
|
# Dry run — preview what would be generated
|
||||||
|
tooling/db/audio-batch docs/assets/audio/batch-s10-327.json --dry-run
|
||||||
|
|
||||||
|
# Generate only specific assets
|
||||||
|
tooling/db/audio-batch docs/assets/audio/batch-s10-327.json --only AMB-001,AMB-002
|
||||||
|
|
||||||
|
# Skip assets that already have OGG files
|
||||||
|
tooling/db/audio-batch docs/assets/audio/batch-s10-327.json --skip-existing
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Update asset docs with prompts
|
||||||
|
|
||||||
|
After the batch completes, write the exact prompts used back into the
|
||||||
|
Prompt/Notes column of `docs/assets/audio/{category}.md`. The manifest records
|
||||||
|
what was generated; the asset docs record what we have.
|
||||||
|
|
||||||
|
### Manifest fields
|
||||||
|
|
||||||
|
| Field | Required | Notes |
|
||||||
|
|-------|----------|-------|
|
||||||
|
| `id` | yes | Asset ID from docs (AMB-001, SFX-002, UI-005) |
|
||||||
|
| `filename` | yes | Output filename (must match asset doc) |
|
||||||
|
| `method` | yes | `sao` (Stable Audio Open) or `synth` (harmonic synthesis) |
|
||||||
|
| `duration` | SAO only | Duration in seconds |
|
||||||
|
| `prompt` | SAO only | Full assembled prompt |
|
||||||
|
| `steps` | no | Override default steps |
|
||||||
|
| `cfg` | no | Override default CFG |
|
||||||
|
| `synth` | synth only | Synthesis parameters (see below) |
|
||||||
|
|
||||||
|
### Synth parameters
|
||||||
|
|
||||||
|
| Field | Default | Notes |
|
||||||
|
|-------|---------|-------|
|
||||||
|
| `type` | harmonic | Only `harmonic` supported currently |
|
||||||
|
| `duration` | — | Duration in seconds |
|
||||||
|
| `fundamental` | — | Fundamental frequency in Hz |
|
||||||
|
| `harmonics` | [] | List of `{"freq": Hz, "db": dB}` objects |
|
||||||
|
| `attack_ms` | 10 | Attack time in milliseconds |
|
||||||
|
| `sustain_ratio` | 0.2 | Fraction of duration at full level before decay |
|
||||||
|
| `decay` | exponential | `exponential` or `linear` |
|
||||||
|
|
||||||
|
## Single Asset Workflow
|
||||||
|
|
||||||
|
For one-off generation or iteration on a specific asset:
|
||||||
|
|
||||||
|
1. Find the asset in `docs/assets/audio/{ambient,sfx,ui}.md` — note filename,
|
||||||
|
duration, bus, method, and design intent.
|
||||||
|
2. Read `references/sonic-palette.md` for the sonic family prefix.
|
||||||
|
3. Read `references/category-templates.md` for the matching template.
|
||||||
|
4. Assemble the full prompt.
|
||||||
|
5. Run `tooling/db/audio-health` to verify the API is up.
|
||||||
|
6. Run `tooling/db/audio-generate` with `--post` or `--output-ogg` to
|
||||||
|
generate and post-process in one step.
|
||||||
|
7. Verify the output (file size, duration).
|
||||||
|
8. Update the asset status and prompt in `docs/assets/audio/{category}.md`.
|
||||||
|
|
||||||
|
## Iteration Workflow
|
||||||
|
|
||||||
|
For each asset, generate 4-6 candidates:
|
||||||
|
|
||||||
|
1. **Generate candidates** — vary the prompt slightly (add/remove descriptors,
|
||||||
|
adjust CFG between 5-9). Run each generation sequentially — never in
|
||||||
|
parallel.
|
||||||
|
2. **Solo test** — does each candidate sound right alone?
|
||||||
|
3. **Stack test** — play the candidate alongside other layers. Does it mask or
|
||||||
|
clash?
|
||||||
|
4. **Fatigue test** (loops only) — can you listen for 5+ minutes without a
|
||||||
|
jarring repeat?
|
||||||
|
5. **Close-your-eyes test** — does it create a mental image or sensation?
|
||||||
|
6. Select the best candidate (post-processing is already done if `--post` was
|
||||||
|
used).
|
||||||
|
|
||||||
|
## Post-Processing (Standalone)
|
||||||
|
|
||||||
|
If you need to post-process separately (e.g., re-normalizing an existing file):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Full pipeline: trim → normalize → convert
|
||||||
|
tooling/db/audio-post pipeline input.wav --output output.ogg
|
||||||
|
|
||||||
|
# Individual steps
|
||||||
|
tooling/db/audio-post trim input.wav
|
||||||
|
tooling/db/audio-post normalize input.wav --lufs -16
|
||||||
|
tooling/db/audio-post convert input.wav --output output.ogg
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manual Synthesis (Insert-Tech Sounds)
|
||||||
|
|
||||||
|
For sounds under 200ms (cursor hover, weapon aim), Stable Audio Open cannot
|
||||||
|
produce meaningful output. Use manual synthesis via `tooling/synth_ui_sounds.py`
|
||||||
|
or the batch manifest's `method: "synth"` with harmonic parameters.
|
||||||
|
|
||||||
|
For complex synthesis beyond the `harmonic` type (FM, filtered noise, bandpass
|
||||||
|
impulse), write a custom script in `tooling/` following the pattern in
|
||||||
|
`tooling/synth_ui_sounds.py`.
|
||||||
|
|
||||||
|
## Quality Checklist
|
||||||
|
|
||||||
|
After generating, verify:
|
||||||
|
- Sound matches the sonic family (insert-tech = synthetic/precise, organic = warm/natural)
|
||||||
|
- Frequency range doesn't mask other layers (check docs/assets/audio/)
|
||||||
|
- Duration matches spec
|
||||||
|
- No unwanted artifacts (clicks, pops, digital noise at start/end)
|
||||||
|
- Loop point is clean (ambient loops only)
|
||||||
|
- Volume sits well relative to other assets (LUFS normalized)
|
||||||
|
- Passes the close-your-eyes test
|
||||||
|
|
||||||
|
## File Placement
|
||||||
|
|
||||||
|
Generated assets go to `client/assets/audio/` with exact filenames from the
|
||||||
|
asset docs. Intermediates go to `client/assets/audio/gen/` (gitignored).
|
||||||
|
|
||||||
|
AudioManager discovers assets by directory scan — filenames must match exactly.
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
---
|
||||||
|
name: bug-report
|
||||||
|
description: >
|
||||||
|
Process in-game bug reports captured by the Godot client's bug reporter.
|
||||||
|
Use when the user says "bug reports", "check bug reports", "process bugs",
|
||||||
|
or invokes /bug-report. Scans the user:// bug-reports directory, summarizes
|
||||||
|
each report, and offers investigation, ticket creation, or dismissal.
|
||||||
|
user-invocable: true
|
||||||
|
allowed-tools: Bash, Read, Grep, Glob, Write
|
||||||
|
---
|
||||||
|
|
||||||
|
# Bug Report Skill
|
||||||
|
|
||||||
|
Process in-game bug reports exported by the Godot client to the user data
|
||||||
|
directory. Each report is a directory containing a snapshot of game state at the
|
||||||
|
moment the tester filed the report.
|
||||||
|
|
||||||
|
```
|
||||||
|
BUG_REPORT_DIR: /var/home/jeroenschweitzer/.local/share/godot/app_userdata/The Settled Reach/bug-reports/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Report structure
|
||||||
|
|
||||||
|
Each report lives in a directory named `gauntlet-t{tick}-{timestamp}/` and
|
||||||
|
contains these files:
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `description.txt` | Tester notes + metadata (tick, room, stance, facing, position) |
|
||||||
|
| `render.txt` | Simplified text render of the game snapshot |
|
||||||
|
| `snapshot.json` | Full JSON snapshot (entities, dialogue state, etc.) |
|
||||||
|
| `inputs.jsonl` | Last 60 ticks of player input (replay format) |
|
||||||
|
| `snapshots.jsonl` | Last 60 ticks of observer snapshots |
|
||||||
|
| `seed.txt` | RNG seed for deterministic replay |
|
||||||
|
|
||||||
|
## Invocation
|
||||||
|
|
||||||
|
- `/bug-report` — scan and process all unprocessed reports
|
||||||
|
- `/bug-report <directory-name>` — process a specific report by directory name
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
### 1. Scan for unprocessed reports
|
||||||
|
|
||||||
|
List all report directories in the bug reports directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls -1d "/var/home/jeroenschweitzer/.local/share/godot/app_userdata/The Settled Reach/bug-reports/"*/
|
||||||
|
```
|
||||||
|
|
||||||
|
If no directories are found, report "No bug reports found." and stop.
|
||||||
|
|
||||||
|
If the user provided a specific directory name as argument, filter to only that
|
||||||
|
directory. If it does not exist, report the error and list available reports.
|
||||||
|
|
||||||
|
### 2. Read and summarize each report
|
||||||
|
|
||||||
|
For each report directory, read the following files using the Read tool:
|
||||||
|
|
||||||
|
1. **`description.txt`** — extract:
|
||||||
|
- Tester description / notes (free text at top)
|
||||||
|
- Tick number
|
||||||
|
- Room name
|
||||||
|
- Player stance, facing, position
|
||||||
|
2. **`render.txt`** — extract:
|
||||||
|
- A brief description of what the text render shows (room layout, visible
|
||||||
|
entities, player position marker)
|
||||||
|
3. **`snapshot.json`** — extract:
|
||||||
|
- Total entity count
|
||||||
|
- Whether dialogue is active (look for `dialogue` or `conversation` keys
|
||||||
|
with non-null/non-empty values)
|
||||||
|
- Whether monologue is active (look for `monologue` keys with non-null/
|
||||||
|
non-empty values)
|
||||||
|
- NPC names and positions if present
|
||||||
|
- Any error or anomaly fields
|
||||||
|
4. **`seed.txt`** — note the seed value for reference
|
||||||
|
|
||||||
|
Do NOT read `inputs.jsonl` or `snapshots.jsonl` during the summary phase.
|
||||||
|
These are large files reserved for the investigation step.
|
||||||
|
|
||||||
|
### 3. Present the summary list
|
||||||
|
|
||||||
|
Present a numbered list of all reports with their summaries. Format:
|
||||||
|
|
||||||
|
```
|
||||||
|
## Bug Reports Found: N
|
||||||
|
|
||||||
|
### 1. gauntlet-t{tick}-{timestamp}
|
||||||
|
- **Tick:** {tick} | **Room:** {room} | **Position:** ({x}, {y})
|
||||||
|
- **Stance:** {stance} | **Facing:** {facing}
|
||||||
|
- **Entities:** {count} | **Dialogue active:** yes/no | **Monologue active:** yes/no
|
||||||
|
- **Seed:** {seed}
|
||||||
|
- **Description:** {tester notes, first 2-3 lines}
|
||||||
|
- **Render overview:** {brief description of what render.txt shows}
|
||||||
|
- **Observations:** {any anomalies spotted in the snapshot}
|
||||||
|
|
||||||
|
### 2. gauntlet-t{tick}-{timestamp}
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Offer actions per report
|
||||||
|
|
||||||
|
After presenting the summary list, ask the user which action to take for each
|
||||||
|
report. The three actions are:
|
||||||
|
|
||||||
|
#### Investigate
|
||||||
|
|
||||||
|
Dig deeper into the report for root cause analysis:
|
||||||
|
|
||||||
|
1. Read `snapshot.json` in full — analyze entity states, component values,
|
||||||
|
relationships between entities, any inconsistencies
|
||||||
|
2. Read `inputs.jsonl` — reconstruct what the player was doing in the 60 ticks
|
||||||
|
leading up to the report. Look for:
|
||||||
|
- Rapid input changes (stuck keys, input spam)
|
||||||
|
- Movement into walls or invalid positions
|
||||||
|
- Interaction attempts that may have failed
|
||||||
|
- Timing patterns (actions on same tick as state changes)
|
||||||
|
3. Read `snapshots.jsonl` — compare entity states across recent ticks to find
|
||||||
|
when the bug manifested:
|
||||||
|
- Entity position jumps
|
||||||
|
- State machine transitions that look wrong
|
||||||
|
- Component values going out of expected range
|
||||||
|
- Entities appearing or disappearing unexpectedly
|
||||||
|
4. Cross-reference with `render.txt` to confirm visual manifestation
|
||||||
|
5. Read `seed.txt` and note it — the seed plus `inputs.jsonl` should allow
|
||||||
|
deterministic replay of the scenario
|
||||||
|
|
||||||
|
Present findings as a root cause analysis:
|
||||||
|
|
||||||
|
```
|
||||||
|
## Investigation: gauntlet-t{tick}-{timestamp}
|
||||||
|
|
||||||
|
### Timeline
|
||||||
|
- t{tick-N}: {what happened}
|
||||||
|
- t{tick-M}: {state change}
|
||||||
|
- t{tick}: {bug manifests}
|
||||||
|
|
||||||
|
### Root cause
|
||||||
|
{Analysis of what went wrong and why}
|
||||||
|
|
||||||
|
### Affected systems
|
||||||
|
- {system 1}: {how it's involved}
|
||||||
|
- {system 2}: {how it's involved}
|
||||||
|
|
||||||
|
### Reproduction
|
||||||
|
Seed: {seed}
|
||||||
|
Replay inputs.jsonl from tick {start} to reproduce.
|
||||||
|
|
||||||
|
### Suggested fix
|
||||||
|
{If identifiable from the snapshot data}
|
||||||
|
```
|
||||||
|
|
||||||
|
After investigation, return to the action prompt for this report (the user
|
||||||
|
may want to create a ticket or dismiss after investigating).
|
||||||
|
|
||||||
|
#### Create ticket
|
||||||
|
|
||||||
|
Create a bug ticket in the project database. Determine the team from the
|
||||||
|
nature of the bug:
|
||||||
|
|
||||||
|
- **server** — simulation bugs (entity state, movement, AI, ECS systems,
|
||||||
|
perception, knowledge graph)
|
||||||
|
- **client** — rendering bugs (display glitches, UI issues, input handling,
|
||||||
|
audio, visual artifacts)
|
||||||
|
- **server,client** — integration bugs (protocol mismatch, desync, bridge
|
||||||
|
issues)
|
||||||
|
|
||||||
|
Construct the ticket title and description from the report summary and any
|
||||||
|
investigation findings. Use the ticket CLI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tooling/db/ticket create bug "{title}" --team {team} --description "{description}"
|
||||||
|
```
|
||||||
|
|
||||||
|
The description should include:
|
||||||
|
- Bug summary (from tester notes)
|
||||||
|
- Tick, room, position
|
||||||
|
- Key observations from snapshot analysis
|
||||||
|
- Seed for reproduction
|
||||||
|
- Report directory name for reference
|
||||||
|
|
||||||
|
After creating the ticket, report the ticket ID to the user.
|
||||||
|
|
||||||
|
#### Dismiss
|
||||||
|
|
||||||
|
Mark the report as not actionable. Remove the report directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rm -rf "/var/home/jeroenschweitzer/.local/share/godot/app_userdata/The Settled Reach/bug-reports/{report-dir}/"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Always confirm with the user before deleting.** State clearly which directory
|
||||||
|
will be removed and wait for confirmation.
|
||||||
|
|
||||||
|
### 5. Batch processing
|
||||||
|
|
||||||
|
When processing multiple reports, work through them one at a time in the
|
||||||
|
numbered order presented. For each report, complete the chosen action before
|
||||||
|
moving to the next.
|
||||||
|
|
||||||
|
If the user wants to batch-dismiss multiple reports, confirm the full list
|
||||||
|
of directories that will be deleted before proceeding.
|
||||||
|
|
||||||
|
### 6. Final summary
|
||||||
|
|
||||||
|
After all reports have been processed, present a summary:
|
||||||
|
|
||||||
|
```
|
||||||
|
## Bug Report Processing Complete
|
||||||
|
|
||||||
|
- **Investigated:** {count}
|
||||||
|
- **Tickets created:** {count} ({ticket IDs})
|
||||||
|
- **Dismissed:** {count}
|
||||||
|
- **Remaining unprocessed:** {count}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tips
|
||||||
|
|
||||||
|
- Large `snapshot.json` files may need to be read with offset/limit parameters.
|
||||||
|
Start with the first 200 lines to get the structure, then target specific
|
||||||
|
sections.
|
||||||
|
- `inputs.jsonl` and `snapshots.jsonl` are newline-delimited JSON. Each line
|
||||||
|
is one tick. Read the last 10-20 lines first to focus on the moments before
|
||||||
|
the report was filed.
|
||||||
|
- The `render.txt` is a text-art representation of the game view. Entity
|
||||||
|
positions in the render should match positions in the snapshot. Mismatches
|
||||||
|
are themselves a bug signal (rendering vs simulation desync).
|
||||||
|
- The seed in `seed.txt` combined with `inputs.jsonl` enables deterministic
|
||||||
|
replay on the server. Note this in any ticket you create.
|
||||||
|
- If the bug-reports directory does not exist, the tester has not yet run any
|
||||||
|
gauntlet sessions or has not filed any reports. This is not an error.
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
---
|
||||||
|
name: d2-diagram
|
||||||
|
description: >
|
||||||
|
Generate technical diagrams using d2 (text-to-diagram CLI). Use when the
|
||||||
|
user says "create a diagram", "draw architecture", "make a flowchart",
|
||||||
|
"diagram this", "render d2", "d2", "data flow diagram", "entity relationship",
|
||||||
|
"state machine", "sequence diagram", "UI flow", or invokes /d2-diagram.
|
||||||
|
Produces .d2 source files and renders them to PNG. Also use when asked
|
||||||
|
to update, re-render, or batch render existing diagrams.
|
||||||
|
---
|
||||||
|
|
||||||
|
# d2 Diagram Generation
|
||||||
|
|
||||||
|
Generate technical diagrams from text using d2 (v0.7.1). Pure CLI, no
|
||||||
|
external dependencies beyond the d2 binary.
|
||||||
|
|
||||||
|
**Binary:** `/home/linuxbrew/.linuxbrew/bin/d2`
|
||||||
|
|
||||||
|
## Project Defaults
|
||||||
|
|
||||||
|
| Setting | Value | Override |
|
||||||
|
|---------|-------|----------|
|
||||||
|
| Theme | 200 (Dark Mauve) | `--theme N` |
|
||||||
|
| Layout | dagre | `--layout elk` |
|
||||||
|
| Padding | 100px | — |
|
||||||
|
| Format | PNG | `--svg` |
|
||||||
|
|
||||||
|
## Output Convention
|
||||||
|
|
||||||
|
```
|
||||||
|
docs/diagrams/
|
||||||
|
architecture/ # System architecture, IPC, component layout
|
||||||
|
data-flow/ # Sequence diagrams, data pipelines
|
||||||
|
entity/ # ER diagrams, ECS component schemas
|
||||||
|
state/ # State machines, behavior trees
|
||||||
|
ui/ # UI navigation flow, screen transitions
|
||||||
|
```
|
||||||
|
|
||||||
|
Both `.d2` source and `.png` output are tracked in git.
|
||||||
|
|
||||||
|
## Single Diagram Workflow
|
||||||
|
|
||||||
|
1. **Determine category** — architecture, data-flow, entity, state, or ui
|
||||||
|
2. **Read template** — `references/diagram-templates.md` for the matching category
|
||||||
|
3. **Read syntax** — `references/d2-syntax-guide.md` if unfamiliar with d2 syntax
|
||||||
|
4. **Write .d2 source** — to `docs/diagrams/{category}/{name}.d2`
|
||||||
|
5. **Validate** — `.claude/skills/d2-diagram/scripts/d2-render.sh validate {file}`
|
||||||
|
6. **Render** — `.claude/skills/d2-diagram/scripts/d2-render.sh {file}`
|
||||||
|
7. **Read SVG** — verify the output, present to user
|
||||||
|
|
||||||
|
### Script Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Render with project defaults
|
||||||
|
.claude/skills/d2-diagram/scripts/d2-render.sh docs/diagrams/architecture/ipc-bridge.d2
|
||||||
|
|
||||||
|
# Validate syntax only
|
||||||
|
.claude/skills/d2-diagram/scripts/d2-render.sh validate docs/diagrams/architecture/ipc-bridge.d2
|
||||||
|
|
||||||
|
# Auto-format source
|
||||||
|
.claude/skills/d2-diagram/scripts/d2-render.sh fmt docs/diagrams/architecture/ipc-bridge.d2
|
||||||
|
|
||||||
|
# Sketch mode (hand-drawn look for drafts)
|
||||||
|
.claude/skills/d2-diagram/scripts/d2-render.sh docs/diagrams/ui/flow.d2 --sketch
|
||||||
|
|
||||||
|
# Light theme (for printable docs)
|
||||||
|
.claude/skills/d2-diagram/scripts/d2-render.sh docs/diagrams/entity/schema.d2 --theme 0
|
||||||
|
|
||||||
|
# SVG output (if specifically needed)
|
||||||
|
.claude/skills/d2-diagram/scripts/d2-render.sh docs/diagrams/architecture/overview.d2 --svg
|
||||||
|
```
|
||||||
|
|
||||||
|
## Batch Render
|
||||||
|
|
||||||
|
Re-render all diagrams after theme or style changes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# All diagrams
|
||||||
|
.claude/skills/d2-diagram/scripts/d2-batch.sh
|
||||||
|
|
||||||
|
# One category
|
||||||
|
.claude/skills/d2-diagram/scripts/d2-batch.sh docs/diagrams/architecture/
|
||||||
|
|
||||||
|
# Preview what would render
|
||||||
|
.claude/skills/d2-diagram/scripts/d2-batch.sh --dry-run
|
||||||
|
|
||||||
|
# Force re-render everything
|
||||||
|
.claude/skills/d2-diagram/scripts/d2-batch.sh --force
|
||||||
|
```
|
||||||
|
|
||||||
|
Batch skips files whose PNG is newer than the `.d2` source unless `--force`.
|
||||||
|
|
||||||
|
## Advanced Patterns
|
||||||
|
|
||||||
|
### Variables for consistent styling
|
||||||
|
|
||||||
|
```d2
|
||||||
|
vars: {
|
||||||
|
color-bg: "#2a3040"
|
||||||
|
color-stroke: "#333340"
|
||||||
|
color-text: "#c8d0e0"
|
||||||
|
color-accent: "#c8d8f0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multi-board (layers)
|
||||||
|
|
||||||
|
```d2
|
||||||
|
# Base diagram here
|
||||||
|
|
||||||
|
layers: {
|
||||||
|
detailed: {
|
||||||
|
# More detailed view
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sequence diagrams
|
||||||
|
|
||||||
|
```d2
|
||||||
|
shape: sequence_diagram
|
||||||
|
client: Godot Client
|
||||||
|
server: Rust Server
|
||||||
|
|
||||||
|
client -> server: TickRequest(delta)
|
||||||
|
server -> client: WorldState(entities)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Imports
|
||||||
|
|
||||||
|
Split shared definitions into a separate file and import:
|
||||||
|
|
||||||
|
```d2
|
||||||
|
...@shared-defs.d2
|
||||||
|
```
|
||||||
|
|
||||||
|
## Agent Guidance
|
||||||
|
|
||||||
|
- **Qatux** — Architecture decision records, system overview diagrams, data
|
||||||
|
schemas. Prefer architecture and entity templates.
|
||||||
|
- **Tyre** — IPC bridge, ECS system flow, chunk loading pipeline, perception
|
||||||
|
system data flow. Prefer architecture and data-flow templates.
|
||||||
|
- **Araminta** — UI navigation flow, screen transitions, component hierarchy.
|
||||||
|
Prefer UI flow template.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- `references/d2-syntax-guide.md` — Language quick reference (shapes, edges,
|
||||||
|
containers, styling, variables). Read when unfamiliar with d2 syntax.
|
||||||
|
- `references/diagram-templates.md` — Five category templates with complete
|
||||||
|
d2 source examples. Read when starting a new diagram.
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
# D2 Language Quick Reference
|
||||||
|
|
||||||
|
## Nodes
|
||||||
|
|
||||||
|
```d2
|
||||||
|
server # Implicit label from key
|
||||||
|
server: Simulation Server # Explicit label
|
||||||
|
server: Simulation Server { # With properties
|
||||||
|
shape: hexagon
|
||||||
|
style.fill: "#2d3436"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Edges
|
||||||
|
|
||||||
|
```d2
|
||||||
|
a -> b # Directed
|
||||||
|
a <- b # Reverse directed
|
||||||
|
a <-> b # Bidirectional
|
||||||
|
a -- b # Undirected
|
||||||
|
a -> b: "label" # Labeled edge
|
||||||
|
a -> b -> c # Chained
|
||||||
|
```
|
||||||
|
|
||||||
|
## Containers (nesting)
|
||||||
|
|
||||||
|
```d2
|
||||||
|
infrastructure: {
|
||||||
|
server: Simulation Server
|
||||||
|
database: State Store {
|
||||||
|
shape: cylinder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Shapes
|
||||||
|
|
||||||
|
| Shape | Use for |
|
||||||
|
|-------|---------|
|
||||||
|
| `rectangle` | Default. Components, modules, generic. |
|
||||||
|
| `hexagon` | Systems, services, major components. |
|
||||||
|
| `cylinder` | Databases, storage, persistent state. |
|
||||||
|
| `diamond` | Decisions, conditions, branch points. |
|
||||||
|
| `oval` / `circle` | Start/end states, events. |
|
||||||
|
| `cloud` | External systems, networks. |
|
||||||
|
| `person` | Actors, users, NPCs. |
|
||||||
|
| `queue` | Message queues, buffers. |
|
||||||
|
| `page` | Documents, files. |
|
||||||
|
| `package` | Packages, modules, crates. |
|
||||||
|
| `sql_table` | Database tables, ECS component schemas. |
|
||||||
|
| `class` | Class diagrams, ECS system definitions. |
|
||||||
|
| `code` | Code blocks (set `language` property). |
|
||||||
|
| `markdown` | Rich text blocks. |
|
||||||
|
|
||||||
|
## SQL Tables
|
||||||
|
|
||||||
|
```d2
|
||||||
|
entity: {
|
||||||
|
shape: sql_table
|
||||||
|
id: u64 {constraint: primary_key}
|
||||||
|
name: String
|
||||||
|
position: Vec2
|
||||||
|
faction_id: u64 {constraint: foreign_key}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Class Diagrams
|
||||||
|
|
||||||
|
```d2
|
||||||
|
perception_system: {
|
||||||
|
shape: class
|
||||||
|
+run(world: &mut World)
|
||||||
|
-calculate_los(entity: Entity): HashSet<Entity>
|
||||||
|
#update_knowledge(entity: Entity, seen: HashSet<Entity>)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sequence Diagrams
|
||||||
|
|
||||||
|
```d2
|
||||||
|
shape: sequence_diagram
|
||||||
|
client: Godot Client
|
||||||
|
server: Rust Server
|
||||||
|
|
||||||
|
client -> server: TickRequest(delta)
|
||||||
|
server -> server: run ECS systems
|
||||||
|
server -> client: WorldState(entities)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Styling
|
||||||
|
|
||||||
|
```d2
|
||||||
|
node: Label {
|
||||||
|
style: {
|
||||||
|
fill: "#2d3436"
|
||||||
|
stroke: "#333340"
|
||||||
|
stroke-width: 2
|
||||||
|
stroke-dash: 5 # Dashed line
|
||||||
|
opacity: 0.8
|
||||||
|
font-size: 14
|
||||||
|
font-color: "#c8d0e0"
|
||||||
|
bold: true
|
||||||
|
italic: false
|
||||||
|
border-radius: 4
|
||||||
|
shadow: true
|
||||||
|
3d: true # Rectangles only
|
||||||
|
multiple: true # Stacked appearance
|
||||||
|
double-border: true # Rectangles/ovals only
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Edge styling
|
||||||
|
|
||||||
|
```d2
|
||||||
|
a -> b: {
|
||||||
|
style: {
|
||||||
|
stroke: "#c8d8f0"
|
||||||
|
stroke-width: 2
|
||||||
|
stroke-dash: 5
|
||||||
|
opacity: 0.8
|
||||||
|
animated: true # Animated flow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Variables
|
||||||
|
|
||||||
|
```d2
|
||||||
|
vars: {
|
||||||
|
color-bg: "#1a1e24"
|
||||||
|
color-stroke: "#333340"
|
||||||
|
color-text: "#c8d0e0"
|
||||||
|
color-accent: "#c8d8f0"
|
||||||
|
}
|
||||||
|
|
||||||
|
node: {
|
||||||
|
style.fill: ${color-bg}
|
||||||
|
style.stroke: ${color-stroke}
|
||||||
|
style.font-color: ${color-text}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Direction
|
||||||
|
|
||||||
|
```d2
|
||||||
|
direction: right # left-to-right (default for dagre)
|
||||||
|
direction: down # top-to-bottom
|
||||||
|
direction: up
|
||||||
|
direction: left
|
||||||
|
```
|
||||||
|
|
||||||
|
## Imports
|
||||||
|
|
||||||
|
```d2
|
||||||
|
...@shared-defs.d2 # Spread import (inline all definitions)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Icons
|
||||||
|
|
||||||
|
```d2
|
||||||
|
node: Label {
|
||||||
|
icon: https://icons.terrastruct.com/essentials/time.svg
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Layers (multi-board)
|
||||||
|
|
||||||
|
```d2
|
||||||
|
# Base diagram content here
|
||||||
|
|
||||||
|
layers: {
|
||||||
|
detailed: {
|
||||||
|
# More detailed view
|
||||||
|
}
|
||||||
|
simplified: {
|
||||||
|
# Simplified overview
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scenarios (animated transitions)
|
||||||
|
|
||||||
|
```d2
|
||||||
|
# Base state
|
||||||
|
|
||||||
|
scenarios: {
|
||||||
|
alert: {
|
||||||
|
# Changes from base for alert state
|
||||||
|
}
|
||||||
|
combat: {
|
||||||
|
# Changes from base for combat state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Comments
|
||||||
|
|
||||||
|
```d2
|
||||||
|
# This is a comment
|
||||||
|
node: Label # Inline comment
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Colors (from visual-grammar-v01.md)
|
||||||
|
|
||||||
|
| Constant | Hex | Usage |
|
||||||
|
|----------|-----|-------|
|
||||||
|
| Zone 1 floor | `#1a1e24` | Dark backgrounds |
|
||||||
|
| Zone 1 wall | `#2a3040` | Component fill |
|
||||||
|
| Outline standard | `#333340` | Borders, strokes |
|
||||||
|
| Insert chrome | `#c8d0e0` | Text, labels |
|
||||||
|
| Zone 1 fixture | `#c8d8f0` | Accents, highlights |
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
# Diagram Templates
|
||||||
|
|
||||||
|
Copy, adapt, and render. Each template uses project colors from visual-grammar-v01.md.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Architecture Diagram
|
||||||
|
|
||||||
|
System components, relationships, communication channels.
|
||||||
|
|
||||||
|
**When to use:** IPC bridge, perception pipeline, chunk loading, ECS system layout, client-server architecture.
|
||||||
|
|
||||||
|
**Agents:** Tyre (system architecture), Qatux (architecture decision records).
|
||||||
|
|
||||||
|
```d2
|
||||||
|
vars: {
|
||||||
|
color-bg: "#2a3040"
|
||||||
|
color-stroke: "#333340"
|
||||||
|
color-text: "#c8d0e0"
|
||||||
|
color-accent: "#c8d8f0"
|
||||||
|
}
|
||||||
|
|
||||||
|
direction: right
|
||||||
|
|
||||||
|
client: Godot Client {
|
||||||
|
shape: hexagon
|
||||||
|
style.fill: ${color-bg}
|
||||||
|
style.font-color: ${color-text}
|
||||||
|
|
||||||
|
rendering: Rendering {
|
||||||
|
style.fill: ${color-bg}
|
||||||
|
style.font-color: ${color-text}
|
||||||
|
}
|
||||||
|
ui: UI Layer {
|
||||||
|
style.fill: ${color-bg}
|
||||||
|
style.font-color: ${color-text}
|
||||||
|
}
|
||||||
|
bridge: IPC Bridge {
|
||||||
|
style.fill: ${color-bg}
|
||||||
|
style.font-color: ${color-text}
|
||||||
|
style.stroke: ${color-accent}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server: Rust Server {
|
||||||
|
shape: hexagon
|
||||||
|
style.fill: ${color-bg}
|
||||||
|
style.font-color: ${color-text}
|
||||||
|
|
||||||
|
ecs: bevy_ecs {
|
||||||
|
style.fill: ${color-bg}
|
||||||
|
style.font-color: ${color-text}
|
||||||
|
}
|
||||||
|
perception: Perception {
|
||||||
|
style.fill: ${color-bg}
|
||||||
|
style.font-color: ${color-text}
|
||||||
|
}
|
||||||
|
bridge: IPC Bridge {
|
||||||
|
style.fill: ${color-bg}
|
||||||
|
style.font-color: ${color-text}
|
||||||
|
style.stroke: ${color-accent}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client.bridge -> server.bridge: "stdin/stdout" {
|
||||||
|
style.stroke: ${color-accent}
|
||||||
|
style.stroke-dash: 5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Entity Relationship
|
||||||
|
|
||||||
|
Data schemas, ECS components, knowledge graph structure.
|
||||||
|
|
||||||
|
**When to use:** Database tables, component definitions, entity relationships, knowledge store schema.
|
||||||
|
|
||||||
|
**Agents:** Tyre (ECS component design), Qatux (schema documentation).
|
||||||
|
|
||||||
|
```d2
|
||||||
|
entity: Entity {
|
||||||
|
shape: sql_table
|
||||||
|
id: u64 {constraint: primary_key}
|
||||||
|
name: String
|
||||||
|
faction_id: u64 {constraint: foreign_key}
|
||||||
|
}
|
||||||
|
|
||||||
|
position: Position {
|
||||||
|
shape: sql_table
|
||||||
|
entity_id: u64 {constraint: foreign_key}
|
||||||
|
x: f32
|
||||||
|
y: f32
|
||||||
|
chunk_id: u32
|
||||||
|
}
|
||||||
|
|
||||||
|
knowledge: KnowledgeEntry {
|
||||||
|
shape: sql_table
|
||||||
|
observer_id: u64 {constraint: foreign_key}
|
||||||
|
subject_id: u64 {constraint: foreign_key}
|
||||||
|
fact_type: FactType
|
||||||
|
confidence: f32
|
||||||
|
last_seen_tick: u64
|
||||||
|
}
|
||||||
|
|
||||||
|
entity.id -> position.entity_id
|
||||||
|
entity.id -> knowledge.observer_id
|
||||||
|
entity.id -> knowledge.subject_id
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Sequence / Data Flow
|
||||||
|
|
||||||
|
Ordered interactions between systems over time.
|
||||||
|
|
||||||
|
**When to use:** IPC message flow, tick processing, perception update cycle, dialogue system exchanges.
|
||||||
|
|
||||||
|
**Agents:** Tyre (system interaction design), Qatux (protocol documentation).
|
||||||
|
|
||||||
|
```d2
|
||||||
|
shape: sequence_diagram
|
||||||
|
|
||||||
|
client: Godot Client
|
||||||
|
bridge: IPC Bridge
|
||||||
|
server: Rust Server
|
||||||
|
ecs: ECS Systems
|
||||||
|
|
||||||
|
client -> bridge: TickRequest(delta, input)
|
||||||
|
bridge -> server: deserialize + dispatch
|
||||||
|
server -> ecs: run_systems(delta)
|
||||||
|
ecs -> ecs: perception, AI, physics
|
||||||
|
ecs -> server: collect WorldState
|
||||||
|
server -> bridge: serialize WorldState
|
||||||
|
bridge -> client: WorldState(entities, events)
|
||||||
|
client -> client: update rendering
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. State Machine
|
||||||
|
|
||||||
|
Entity states, transitions, conditions.
|
||||||
|
|
||||||
|
**When to use:** NPC behavior states, game mode transitions, dialogue state, investigation phases.
|
||||||
|
|
||||||
|
**Agents:** Tyre (behavior system design), Qatux (state documentation).
|
||||||
|
|
||||||
|
```d2
|
||||||
|
vars: {
|
||||||
|
color-state: "#2a3040"
|
||||||
|
color-text: "#c8d0e0"
|
||||||
|
color-edge: "#c8d8f0"
|
||||||
|
color-decision: "#333340"
|
||||||
|
}
|
||||||
|
|
||||||
|
idle: Idle {
|
||||||
|
style.fill: ${color-state}
|
||||||
|
style.font-color: ${color-text}
|
||||||
|
}
|
||||||
|
|
||||||
|
alert: Alert {
|
||||||
|
style.fill: ${color-state}
|
||||||
|
style.font-color: ${color-text}
|
||||||
|
}
|
||||||
|
|
||||||
|
investigate: Investigate {
|
||||||
|
style.fill: ${color-state}
|
||||||
|
style.font-color: ${color-text}
|
||||||
|
}
|
||||||
|
|
||||||
|
combat: Combat {
|
||||||
|
style.fill: ${color-state}
|
||||||
|
style.font-color: ${color-text}
|
||||||
|
style.stroke: "#f0b840"
|
||||||
|
}
|
||||||
|
|
||||||
|
flee: Flee {
|
||||||
|
style.fill: ${color-state}
|
||||||
|
style.font-color: ${color-text}
|
||||||
|
}
|
||||||
|
|
||||||
|
idle -> alert: "stimulus detected" { style.stroke: ${color-edge} }
|
||||||
|
alert -> investigate: "stimulus confirmed" { style.stroke: ${color-edge} }
|
||||||
|
alert -> idle: "timeout / stimulus lost" { style.stroke: ${color-edge}; style.stroke-dash: 5 }
|
||||||
|
investigate -> combat: "threat confirmed" { style.stroke: "#f0b840" }
|
||||||
|
investigate -> idle: "nothing found" { style.stroke: ${color-edge}; style.stroke-dash: 5 }
|
||||||
|
combat -> flee: "health < threshold" { style.stroke: "#f0b840" }
|
||||||
|
combat -> idle: "threat eliminated" { style.stroke: ${color-edge}; style.stroke-dash: 5 }
|
||||||
|
flee -> idle: "safe distance reached" { style.stroke: ${color-edge}; style.stroke-dash: 5 }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. UI Flow
|
||||||
|
|
||||||
|
Screen navigation, component hierarchy, interaction paths.
|
||||||
|
|
||||||
|
**When to use:** HUD layout relationships, menu navigation, dialogue flow, insert mode transitions.
|
||||||
|
|
||||||
|
**Agents:** Araminta (UI/visual design), Tyre (interface architecture), Qatux (UI documentation).
|
||||||
|
|
||||||
|
```d2
|
||||||
|
vars: {
|
||||||
|
color-screen: "#1a1e24"
|
||||||
|
color-panel: "#2a3040"
|
||||||
|
color-text: "#c8d0e0"
|
||||||
|
color-nav: "#c8d8f0"
|
||||||
|
}
|
||||||
|
|
||||||
|
gameplay: Gameplay {
|
||||||
|
style.fill: ${color-screen}
|
||||||
|
style.font-color: ${color-text}
|
||||||
|
|
||||||
|
hud: HUD {
|
||||||
|
style.fill: ${color-panel}
|
||||||
|
style.font-color: ${color-text}
|
||||||
|
|
||||||
|
minimap: Minimap
|
||||||
|
monologue: Monologue Panel
|
||||||
|
insert_display: Insert Display
|
||||||
|
}
|
||||||
|
|
||||||
|
world: World View {
|
||||||
|
style.fill: ${color-panel}
|
||||||
|
style.font-color: ${color-text}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pause: Pause Menu {
|
||||||
|
style.fill: ${color-screen}
|
||||||
|
style.font-color: ${color-text}
|
||||||
|
|
||||||
|
inventory: Inventory
|
||||||
|
journal: Journal
|
||||||
|
settings: Settings
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogue: Dialogue Mode {
|
||||||
|
style.fill: ${color-screen}
|
||||||
|
style.font-color: ${color-text}
|
||||||
|
|
||||||
|
speaker: Speaker Panel
|
||||||
|
responses: Response List
|
||||||
|
}
|
||||||
|
|
||||||
|
gameplay -> pause: "ESC" { style.stroke: ${color-nav} }
|
||||||
|
pause -> gameplay: "ESC / Resume" { style.stroke: ${color-nav}; style.stroke-dash: 5 }
|
||||||
|
gameplay -> dialogue: "interact with NPC" { style.stroke: ${color-nav} }
|
||||||
|
dialogue -> gameplay: "end conversation" { style.stroke: ${color-nav}; style.stroke-dash: 5 }
|
||||||
|
```
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR" && git rev-parse --show-toplevel)"
|
||||||
|
RENDER="$SCRIPT_DIR/d2-render.sh"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: $(basename "$0") [directory] [options]
|
||||||
|
|
||||||
|
Batch render all .d2 files in a directory. Skips files whose PNG is
|
||||||
|
newer than the source unless --force is used.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--dry-run List files that would be rendered
|
||||||
|
--force Re-render even if SVG is up to date
|
||||||
|
--theme N Override theme for all files
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
$(basename "$0") # All in docs/diagrams/
|
||||||
|
$(basename "$0") docs/diagrams/architecture/ # One category
|
||||||
|
$(basename "$0") --dry-run # Preview
|
||||||
|
$(basename "$0") --force # Re-render everything
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
DIR="$REPO_ROOT/docs/diagrams"
|
||||||
|
DRY_RUN=false
|
||||||
|
FORCE=false
|
||||||
|
EXTRA_ARGS=()
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--dry-run) DRY_RUN=true; shift ;;
|
||||||
|
--force) FORCE=true; shift ;;
|
||||||
|
--theme) EXTRA_ARGS+=(--theme "$2"); shift 2 ;;
|
||||||
|
--help|-h) usage ;;
|
||||||
|
*)
|
||||||
|
if [[ -d "$1" ]] || [[ -d "$REPO_ROOT/$1" ]]; then
|
||||||
|
DIR="$1"
|
||||||
|
[[ "$DIR" != /* ]] && DIR="$REPO_ROOT/$DIR"
|
||||||
|
else
|
||||||
|
echo "Unknown option or directory: $1" >&2; exit 1
|
||||||
|
fi
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ ! -d "$DIR" ]] && { echo "ERROR: Directory not found: $DIR" >&2; exit 1; }
|
||||||
|
|
||||||
|
RENDERED=0
|
||||||
|
SKIPPED=0
|
||||||
|
FAILED=0
|
||||||
|
|
||||||
|
while IFS= read -r -d '' d2_file; do
|
||||||
|
png_file="${d2_file%.d2}.png"
|
||||||
|
|
||||||
|
# Skip if PNG is newer than source (unless --force)
|
||||||
|
if [[ "$FORCE" != true ]] && [[ -f "$png_file" ]] && [[ "$png_file" -nt "$d2_file" ]]; then
|
||||||
|
SKIPPED=$((SKIPPED + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
rel_path="${d2_file#"$REPO_ROOT/"}"
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
echo "Would render: $rel_path"
|
||||||
|
RENDERED=$((RENDERED + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if "$RENDER" "$d2_file" "${EXTRA_ARGS[@]+"${EXTRA_ARGS[@]}"}"; then
|
||||||
|
RENDERED=$((RENDERED + 1))
|
||||||
|
else
|
||||||
|
echo "FAILED: $rel_path" >&2
|
||||||
|
FAILED=$((FAILED + 1))
|
||||||
|
fi
|
||||||
|
done < <(find "$DIR" -name '*.d2' -print0 | sort -z)
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Batch complete: $RENDERED rendered, $SKIPPED skipped (up to date), $FAILED failed"
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
D2="/home/linuxbrew/.linuxbrew/bin/d2"
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR" && git rev-parse --show-toplevel)"
|
||||||
|
|
||||||
|
DEFAULT_THEME=200
|
||||||
|
DEFAULT_LAYOUT="dagre"
|
||||||
|
DEFAULT_PAD=100
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: $(basename "$0") [validate|fmt] <file.d2> [options]
|
||||||
|
|
||||||
|
Render a .d2 file to PNG with project defaults (theme $DEFAULT_THEME, $DEFAULT_LAYOUT layout).
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
validate <file> Check syntax without rendering
|
||||||
|
fmt <file> Auto-format in place
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--theme N Override theme (default: $DEFAULT_THEME)
|
||||||
|
--layout NAME Override layout engine (default: $DEFAULT_LAYOUT)
|
||||||
|
--sketch Enable hand-drawn sketch mode
|
||||||
|
--output PATH Override output path (default: input with .png extension)
|
||||||
|
--svg Render to SVG instead of PNG
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
$(basename "$0") docs/diagrams/architecture/ipc-bridge.d2
|
||||||
|
$(basename "$0") validate docs/diagrams/architecture/ipc-bridge.d2
|
||||||
|
$(basename "$0") docs/diagrams/architecture/ipc-bridge.d2 --sketch --theme 0
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
[[ $# -lt 1 ]] && usage
|
||||||
|
|
||||||
|
# Parse subcommand
|
||||||
|
SUBCMD=""
|
||||||
|
case "$1" in
|
||||||
|
validate|fmt)
|
||||||
|
SUBCMD="$1"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
[[ $# -lt 1 ]] && usage
|
||||||
|
|
||||||
|
INPUT="$1"
|
||||||
|
shift
|
||||||
|
|
||||||
|
# Resolve to absolute path
|
||||||
|
[[ "$INPUT" != /* ]] && INPUT="$REPO_ROOT/$INPUT"
|
||||||
|
|
||||||
|
[[ ! -f "$INPUT" ]] && { echo "ERROR: File not found: $INPUT" >&2; exit 1; }
|
||||||
|
|
||||||
|
# Handle subcommands
|
||||||
|
if [[ -n "$SUBCMD" ]]; then
|
||||||
|
"$D2" "$SUBCMD" "$INPUT"
|
||||||
|
echo "OK: $SUBCMD $INPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Parse render options
|
||||||
|
THEME="$DEFAULT_THEME"
|
||||||
|
LAYOUT="$DEFAULT_LAYOUT"
|
||||||
|
SKETCH=""
|
||||||
|
OUTPUT=""
|
||||||
|
FORMAT="png"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--theme) THEME="$2"; shift 2 ;;
|
||||||
|
--layout) LAYOUT="$2"; shift 2 ;;
|
||||||
|
--sketch) SKETCH="-s"; shift ;;
|
||||||
|
--output) OUTPUT="$2"; shift 2 ;;
|
||||||
|
--svg) FORMAT="svg"; shift ;;
|
||||||
|
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Derive output path
|
||||||
|
if [[ -z "$OUTPUT" ]]; then
|
||||||
|
OUTPUT="${INPUT%.d2}.$FORMAT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Render
|
||||||
|
"$D2" -t "$THEME" -l "$LAYOUT" --pad "$DEFAULT_PAD" $SKETCH "$INPUT" "$OUTPUT"
|
||||||
|
|
||||||
|
SIZE=$(stat --printf="%s" "$OUTPUT" 2>/dev/null || stat -f%z "$OUTPUT" 2>/dev/null)
|
||||||
|
echo "Rendered: $OUTPUT ($(( SIZE / 1024 ))KB)"
|
||||||
@@ -1,18 +1,18 @@
|
|||||||
---
|
---
|
||||||
name: search-docs
|
name: docs-search
|
||||||
description: >
|
description: >
|
||||||
Search project documents using semantic search (Qdrant + ollama) or grep fallback.
|
Search project documents using semantic search (Qdrant + ollama) or grep fallback.
|
||||||
Use when the user asks "did we discuss X?", "find references to Y", "search docs",
|
Use when the user asks "did we discuss X?", "find references to Y", "search docs",
|
||||||
or invokes /search-docs. Wraps the qdrant_connector.py for semantic document search.
|
or invokes /docs-search. Wraps the qdrant_connector.py for semantic document search.
|
||||||
user-invocable: true
|
user-invocable: true
|
||||||
allowed-tools: Bash, Read, Grep, Glob
|
allowed-tools: Bash, Read, Grep, Glob
|
||||||
---
|
---
|
||||||
|
|
||||||
# Search Docs Skill
|
# Search Docs Skill
|
||||||
|
|
||||||
Semantic search across project documents. Basic commands (`qdrant-search`,
|
Semantic search across project documents. Endpoints are in
|
||||||
`qdrant-index`, `qdrant-health`, `qdrant-count`) and endpoints are documented
|
`.claude/rules/local-services.md`. This skill covers advanced operations
|
||||||
in CLAUDE.md. This skill covers advanced operations and workflows.
|
and workflows.
|
||||||
|
|
||||||
## Advanced Commands
|
## Advanced Commands
|
||||||
|
|
||||||
@@ -20,14 +20,14 @@ in CLAUDE.md. This skill covers advanced operations and workflows.
|
|||||||
|
|
||||||
For precise indexing of specific content:
|
For precise indexing of specific content:
|
||||||
```bash
|
```bash
|
||||||
python3 db/connectors/qdrant_connector.py index "unique-id" "Text content to index" --metadata source=manual heading="Custom heading"
|
python3 tooling/db/qdrant_connector.py index "unique-id" "Text content to index" --metadata source=manual heading="Custom heading"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Create collection
|
### Create collection
|
||||||
|
|
||||||
Initialize the Qdrant collection (run once during setup):
|
Initialize the Qdrant collection (run once during setup):
|
||||||
```bash
|
```bash
|
||||||
python3 db/connectors/qdrant_connector.py create-collection
|
python3 tooling/db/qdrant_connector.py create-collection
|
||||||
```
|
```
|
||||||
|
|
||||||
## Bulk Indexing
|
## Bulk Indexing
|
||||||
@@ -35,7 +35,7 @@ python3 db/connectors/qdrant_connector.py create-collection
|
|||||||
Index all project documents at once:
|
Index all project documents at once:
|
||||||
```bash
|
```bash
|
||||||
for f in decisions/*.md DISCUSSION.md TEAM.md docs/discussions/*.md docs/briefings/*.md; do
|
for f in decisions/*.md DISCUSSION.md TEAM.md docs/discussions/*.md docs/briefings/*.md; do
|
||||||
db/connectors/qdrant-index "$f"
|
tooling/db/qdrant-index "$f"
|
||||||
done
|
done
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
---
|
||||||
|
name: frame0-wireframe
|
||||||
|
description: >
|
||||||
|
Create and export UI wireframes using Frame0 (local desktop wireframing
|
||||||
|
app with HTTP API). Use when the user says "create wireframe", "wireframe
|
||||||
|
this", "mock up the UI", "draw a screen", "UI layout", "wireframe the HUD",
|
||||||
|
"Frame0", "frame0", "export wireframe", or invokes /frame0-wireframe.
|
||||||
|
Wireframes are authored as local JSON files (source of truth) and synced
|
||||||
|
to Frame0 for rendering and export. Requires Frame0 to be running locally.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Frame0 Wireframe Generation
|
||||||
|
|
||||||
|
Create UI wireframes as JSON files, sync them to Frame0 for rendering, and
|
||||||
|
export as PNG. Local JSON is the source of truth — Frame0 is the renderer.
|
||||||
|
|
||||||
|
**Frame0 is a renderer, not a workspace.** Treat it as disposable output.
|
||||||
|
Push freely, delete test pages, keep it clean. Never pull from Frame0 unless
|
||||||
|
the user explicitly says they have made edits in Frame0 and want to import
|
||||||
|
them. The pull workflow exists for that case only — do not use it proactively.
|
||||||
|
|
||||||
|
**Prerequisite:** Frame0 desktop app must be running. If not available,
|
||||||
|
stop and inform the user. Point to `references/setup-guide.md`.
|
||||||
|
|
||||||
|
## Health Check
|
||||||
|
|
||||||
|
Always check first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.claude/skills/frame0-wireframe/scripts/frame0-cmd.sh health
|
||||||
|
```
|
||||||
|
|
||||||
|
## Core Workflow
|
||||||
|
|
||||||
|
1. **Health check** — verify Frame0 is running
|
||||||
|
2. **Write wireframe JSON** — to `docs/design/wireframes/{category}/{name}.json`
|
||||||
|
3. **Push to Frame0** — `frame0-sync.py push <file.json>`
|
||||||
|
4. **Export PNG** — `frame0-sync.py export <file.json> <output.png>`
|
||||||
|
5. **Clean up** — delete test/scratch pages from Frame0 when done
|
||||||
|
|
||||||
|
### Scripts
|
||||||
|
|
||||||
|
| Script | Purpose |
|
||||||
|
|--------|---------|
|
||||||
|
| `scripts/frame0-sync.py` | **Primary.** Push/pull/export wireframes between JSON and Frame0 |
|
||||||
|
| `scripts/frame0-cmd.sh` | Low-level API wrapper for ad-hoc commands |
|
||||||
|
|
||||||
|
## Wireframe JSON Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Dialogue Box",
|
||||||
|
"shapes": {
|
||||||
|
"panel": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"left": 170, "top": 500, "width": 800, "height": 260,
|
||||||
|
"fillColor": "#1a1e24",
|
||||||
|
"strokeColor": "#333340",
|
||||||
|
"corners": [8, 8, 8, 8]
|
||||||
|
},
|
||||||
|
"speaker": {
|
||||||
|
"type": "Text",
|
||||||
|
"parent": "panel",
|
||||||
|
"left": 190, "top": 520,
|
||||||
|
"text": "LERA KONSTANTIN",
|
||||||
|
"fontColor": "#c8d0e0",
|
||||||
|
"fontSize": 16
|
||||||
|
},
|
||||||
|
"btn-ask": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"parent": "panel",
|
||||||
|
"left": 190, "top": 670, "width": 370, "height": 30,
|
||||||
|
"fillColor": "#2a3040",
|
||||||
|
"strokeColor": "#c8d8f0",
|
||||||
|
"corners": [4, 4, 4, 4]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"connectors": {
|
||||||
|
"flow-1": {
|
||||||
|
"tailId": "panel",
|
||||||
|
"headId": "btn-ask",
|
||||||
|
"strokeColor": "#c8d8f0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key rules
|
||||||
|
|
||||||
|
- **Shape IDs are stable local IDs** you control (e.g. `"panel"`, `"btn-ask"`)
|
||||||
|
- **`parent`** references another local shape ID for nesting
|
||||||
|
- **`type`** uses create-API names: `Rectangle`, `Ellipse`, `Text`, `Line`
|
||||||
|
- **Colors** can be hex (`#2a3040`) or Frame0 theme tokens (`$slate6`)
|
||||||
|
- After a pull, Frame0 returns its native vocabulary (`Box` for Rectangle,
|
||||||
|
theme tokens for colors). The sync script handles the mapping transparently.
|
||||||
|
- The `.idmap.json` mapping file (gitignored) tracks local ID ↔ Frame0 ID
|
||||||
|
|
||||||
|
### Sync commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SYNC=".claude/skills/frame0-wireframe/scripts/frame0-sync.py"
|
||||||
|
|
||||||
|
# Push local JSON to Frame0 (clears page, recreates all shapes)
|
||||||
|
$SYNC push docs/design/wireframes/dialogue/dialogue-box.json
|
||||||
|
|
||||||
|
# Pull Frame0 page back to local JSON (preserves local IDs via mapping)
|
||||||
|
$SYNC pull "Dialogue Box" docs/design/wireframes/dialogue/dialogue-box.json
|
||||||
|
|
||||||
|
# Push + export as PNG in one step
|
||||||
|
$SYNC export docs/design/wireframes/dialogue/dialogue-box.json \
|
||||||
|
docs/design/wireframes/dialogue/dialogue-box.png
|
||||||
|
```
|
||||||
|
|
||||||
|
### Batch export
|
||||||
|
|
||||||
|
Use this when exporting multiple wireframes. It runs as a single Bash call,
|
||||||
|
avoiding repeated permission prompts.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
BATCH=".claude/skills/frame0-wireframe/scripts/frame0-export-batch.sh"
|
||||||
|
|
||||||
|
# Dry run first — shows full manifest, no Frame0 calls
|
||||||
|
$BATCH --dry-run
|
||||||
|
|
||||||
|
# Export everything (skips PNGs already newer than their JSON)
|
||||||
|
$BATCH
|
||||||
|
|
||||||
|
# Export one category only
|
||||||
|
$BATCH --category dialogue
|
||||||
|
|
||||||
|
# Force re-export of everything
|
||||||
|
$BATCH --force
|
||||||
|
```
|
||||||
|
|
||||||
|
**Always dry-run first, then get approval before running the live export.**
|
||||||
|
|
||||||
|
## Low-Level Commands
|
||||||
|
|
||||||
|
For ad-hoc operations or exec calls not covered by sync:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CMD=".claude/skills/frame0-wireframe/scripts/frame0-cmd.sh"
|
||||||
|
$CMD health
|
||||||
|
$CMD list-pages
|
||||||
|
$CMD current-page
|
||||||
|
$CMD get-page <page-id>
|
||||||
|
$CMD create-shape Rectangle '{"name":"btn","left":100,"top":100,"width":120,"height":36}'
|
||||||
|
$CMD create-connector <tail-id> <head-id>
|
||||||
|
$CMD move <shape-id> <dx> <dy>
|
||||||
|
$CMD export --format image/png
|
||||||
|
$CMD exec "view:fit-to-screen" '{}'
|
||||||
|
```
|
||||||
|
|
||||||
|
If you find yourself using `exec` for the same command repeatedly, flag it as
|
||||||
|
a candidate for a proper wrapper in `frame0-cmd.sh`.
|
||||||
|
|
||||||
|
## Project Styling Defaults
|
||||||
|
|
||||||
|
Colors from `docs/design/visual-grammar-v01.md`:
|
||||||
|
|
||||||
|
| Role | Hex | Frame0 token |
|
||||||
|
|------|-----|-------------|
|
||||||
|
| Background | `#1a1e24` | `$sage3` |
|
||||||
|
| Stroke | `#333340` | `$slate6` |
|
||||||
|
| Fill | `#2a3040` | `$slate5` |
|
||||||
|
| Text | `#c8d0e0` | `$mint12` |
|
||||||
|
| Accent | `#c8d8f0` | `$blue12` |
|
||||||
|
|
||||||
|
Use hex when authoring new wireframes. Frame0 maps them to theme tokens on push.
|
||||||
|
|
||||||
|
## Output Convention
|
||||||
|
|
||||||
|
```
|
||||||
|
docs/design/wireframes/
|
||||||
|
hud/ # HUD layout wireframes
|
||||||
|
menus/ # Menu screen wireframes
|
||||||
|
dialogue/ # Dialogue box wireframes
|
||||||
|
insert/ # Neural insert wireframes
|
||||||
|
```
|
||||||
|
|
||||||
|
Both `.json` source and `.png` exports are tracked in git.
|
||||||
|
`.idmap.json` mapping files are gitignored.
|
||||||
|
|
||||||
|
## Agent Guidance
|
||||||
|
|
||||||
|
- **Araminta** — Primary user. Full wireframe creation, layout iteration,
|
||||||
|
visual consistency. Uses all component library patterns.
|
||||||
|
- **Tyre** — Interface architecture wireframes. System boundary diagrams.
|
||||||
|
- **Qatux** — Export wireframes for UI decision records and documentation.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- `references/component-library.md` — Pre-built JSON wireframe templates
|
||||||
|
(HUD, dialogue, menus, modals, lists, inventory). Copy and adapt.
|
||||||
|
- `references/api-reference.md` — Frame0 HTTP API command reference and
|
||||||
|
type mappings. Read for low-level control.
|
||||||
|
- `references/setup-guide.md` — Frame0 installation and startup for Fedora.
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
# Frame0 HTTP API Reference
|
||||||
|
|
||||||
|
Frame0 exposes a local HTTP API when the desktop app is running.
|
||||||
|
|
||||||
|
## Endpoint
|
||||||
|
|
||||||
|
```
|
||||||
|
POST http://localhost:{port}/execute_command
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
Default port: **58320** (override via `FRAME0_PORT` env var or `--port` flag).
|
||||||
|
|
||||||
|
## Request / Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"command": "namespace:action", "args": { ... }}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"success": true, "data": { ... }}
|
||||||
|
{"success": false, "error": "description"}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Type Mapping
|
||||||
|
|
||||||
|
Frame0 uses different type names for create vs get:
|
||||||
|
|
||||||
|
| Create API (`type`) | Get API (internal) | Description |
|
||||||
|
|--------------------|--------------------|-------------|
|
||||||
|
| `Rectangle` | `Box` | Rectangle with optional corners |
|
||||||
|
| `Ellipse` | `Ellipse` | Circle/ellipse |
|
||||||
|
| `Text` | `Text` | Text label |
|
||||||
|
| `Line` | `Line` | Line/polyline |
|
||||||
|
| `Frame` | `Frame` | Container from library |
|
||||||
|
| `Freehand` | `Freehand` | Freehand drawing |
|
||||||
|
| `Highlighter` | `Highlighter` | Highlighter stroke |
|
||||||
|
|
||||||
|
The sync script handles this mapping transparently.
|
||||||
|
|
||||||
|
## Color Tokens
|
||||||
|
|
||||||
|
Frame0 maps hex colors to theme tokens on creation (`convertColors: true`):
|
||||||
|
|
||||||
|
| Hex | Token | Role |
|
||||||
|
|-----|-------|------|
|
||||||
|
| `#1a1e24` | `$sage3` | Background |
|
||||||
|
| `#2a3040` | `$slate5` | Fill |
|
||||||
|
| `#333340` | `$slate6` | Stroke |
|
||||||
|
| `#c8d0e0` | `$mint12` | Text |
|
||||||
|
| `#c8d8f0` | `$blue12` | Accent |
|
||||||
|
|
||||||
|
Both hex and token strings work in the API. Tokens are preserved on round-trip.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### shape:create-shape
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"command": "shape:create-shape",
|
||||||
|
"args": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"shapeProps": {
|
||||||
|
"name": "my-button",
|
||||||
|
"left": 100, "top": 200, "width": 120, "height": 36,
|
||||||
|
"fillColor": "#2a3040",
|
||||||
|
"strokeColor": "#c8d8f0",
|
||||||
|
"corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"parentId": "optional-parent-shape-id",
|
||||||
|
"convertColors": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns: shape ID (string).
|
||||||
|
|
||||||
|
### shape:get-shape
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"command": "shape:get-shape", "args": {"shapeId": "id"}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### shape:update-shape
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"command": "shape:update-shape",
|
||||||
|
"args": {
|
||||||
|
"shapeId": "id",
|
||||||
|
"shapeProps": {"fillColor": "#1a1e24", "text": "Updated"},
|
||||||
|
"convertColors": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### shape:move
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"command": "shape:move", "args": {"shapeId": "id", "dx": 50, "dy": -20}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### shape:create-connector
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"command": "shape:create-connector",
|
||||||
|
"args": {
|
||||||
|
"tailId": "source-id",
|
||||||
|
"headId": "target-id",
|
||||||
|
"shapeProps": {"strokeColor": "#c8d8f0"},
|
||||||
|
"convertColors": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### shape:create-icon
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"command": "shape:create-icon",
|
||||||
|
"args": {
|
||||||
|
"iconName": "search",
|
||||||
|
"shapeProps": {"left": 100, "top": 100, "width": 24, "height": 24}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### shape:get-available-icons
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"command": "shape:get-available-icons", "args": {}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### shape:group / shape:ungroup
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"command": "shape:group", "args": {"shapeIdArray": ["id1", "id2"]}}
|
||||||
|
{"command": "shape:ungroup", "args": {"shapeIdArray": ["group-id"]}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### edit:delete / edit:duplicate
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"command": "edit:delete", "args": {"shapeIdArray": ["id1", "id2"]}}
|
||||||
|
{"command": "edit:duplicate", "args": {"shapeIdArray": ["id"], "dx": 20, "dy": 0}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### page:add
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"command": "page:add", "args": {"pageProps": {"name": "Page Name"}}}
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns: `{id, type, name}`.
|
||||||
|
|
||||||
|
### page:get
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"command": "page:get", "args": {"pageId": "id", "exportShapes": true}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### page:get-current-page
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"command": "page:get-current-page", "args": {}}
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns: page ID string.
|
||||||
|
|
||||||
|
### page:set-current-page
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"command": "page:set-current-page", "args": {"pageId": "id"}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### doc:get (list all pages)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"command": "doc:get", "args": {"exportPages": true, "exportShapes": false}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### page:delete
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"command": "page:delete", "args": {"pageId": "id"}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### file:export-image
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"command": "file:export-image",
|
||||||
|
"args": {
|
||||||
|
"pageId": "optional-page-id",
|
||||||
|
"format": "image/png",
|
||||||
|
"fillBackground": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Formats: `image/png`, `image/jpeg`, `image/webp`, `image/svg+xml`.
|
||||||
|
Returns: base64-encoded image data.
|
||||||
|
|
||||||
|
### view:fit-to-screen
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"command": "view:fit-to-screen", "args": {}}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Shape Properties
|
||||||
|
|
||||||
|
| Property | Type | Notes |
|
||||||
|
|----------|------|-------|
|
||||||
|
| `name` | string | Shape identifier/label |
|
||||||
|
| `left` | number | X position (origin: top-left) |
|
||||||
|
| `top` | number | Y position |
|
||||||
|
| `width` | number | Width in pixels |
|
||||||
|
| `height` | number | Height in pixels |
|
||||||
|
| `fillColor` | string | Hex or `$token` |
|
||||||
|
| `strokeColor` | string | Hex or `$token` |
|
||||||
|
| `strokeWidth` | number | Border width |
|
||||||
|
| `fontColor` | string | Text color (hex or `$token`) |
|
||||||
|
| `fontSize` | number | Font size in pixels |
|
||||||
|
| `fontFamily` | string | Font name (Frame0 default: `Loranthus`) |
|
||||||
|
| `text` | string | Text content |
|
||||||
|
| `wordWrap` | boolean | Enable word wrapping |
|
||||||
|
| `corners` | number[4] | Border radius [TL, TR, BR, BL] |
|
||||||
|
| `roughness` | number | Sketch roughness (Frame0 default: 1) |
|
||||||
|
| `constraints` | array | Auto-sizing constraints |
|
||||||
|
| `horzAlign` | string | Horizontal text alignment |
|
||||||
|
| `vertAlign` | string | Vertical text alignment |
|
||||||
|
| `fillStyle` | string | Fill style (`none` for transparent) |
|
||||||
|
| `path` | array | Coordinate pairs for lines |
|
||||||
@@ -0,0 +1,467 @@
|
|||||||
|
# Component Library
|
||||||
|
|
||||||
|
Pre-built wireframe JSON templates for The Settled Reach UI. Copy the JSON,
|
||||||
|
adapt positions/sizes, save to `docs/design/wireframes/{category}/`, and push.
|
||||||
|
|
||||||
|
**Viewport:** 1140x780 (Godot project settings)
|
||||||
|
**Grid unit:** 8px
|
||||||
|
**Min touch target:** 36px height
|
||||||
|
**Font sizes:** 12 (label), 14 (body), 16 (subtitle), 18 (heading), 24 (title)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. HUD Layout
|
||||||
|
|
||||||
|
Main gameplay overlay. Minimap top-right, monologue bottom-center,
|
||||||
|
insert display bottom-left, action hints bottom-right.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "HUD Layout",
|
||||||
|
"shapes": {
|
||||||
|
"minimap": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"left": 880, "top": 20, "width": 240, "height": 240,
|
||||||
|
"fillColor": "#1a1e24", "strokeColor": "#333340",
|
||||||
|
"corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"minimap-label": {
|
||||||
|
"type": "Text",
|
||||||
|
"parent": "minimap",
|
||||||
|
"left": 890, "top": 30,
|
||||||
|
"text": "Minimap",
|
||||||
|
"fontColor": "#c8d0e0", "fontSize": 14
|
||||||
|
},
|
||||||
|
"monologue": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"left": 300, "top": 680, "width": 520, "height": 80,
|
||||||
|
"fillColor": "#1a1e24", "strokeColor": "#333340",
|
||||||
|
"corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"monologue-text": {
|
||||||
|
"type": "Text",
|
||||||
|
"parent": "monologue",
|
||||||
|
"left": 310, "top": 700, "width": 500,
|
||||||
|
"text": "Internal monologue text appears here...",
|
||||||
|
"fontColor": "#c8d0e0", "fontSize": 13, "wordWrap": true
|
||||||
|
},
|
||||||
|
"insert": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"left": 20, "top": 600, "width": 260, "height": 160,
|
||||||
|
"fillColor": "#1a1e24", "strokeColor": "#333340",
|
||||||
|
"corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"insert-label": {
|
||||||
|
"type": "Text",
|
||||||
|
"parent": "insert",
|
||||||
|
"left": 30, "top": 620,
|
||||||
|
"text": "Neural Insert Data",
|
||||||
|
"fontColor": "#c8d0e0", "fontSize": 14
|
||||||
|
},
|
||||||
|
"hints": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"left": 880, "top": 700, "width": 240, "height": 60,
|
||||||
|
"fillColor": "#1a1e24", "strokeColor": "#333340",
|
||||||
|
"corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"hints-label": {
|
||||||
|
"type": "Text",
|
||||||
|
"parent": "hints",
|
||||||
|
"left": 890, "top": 720,
|
||||||
|
"text": "[E] Interact [TAB] Insert",
|
||||||
|
"fontColor": "#c8d0e0", "fontSize": 12
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Dialogue Box
|
||||||
|
|
||||||
|
Speaker panel with response options. Anchored bottom-center during dialogue mode.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Dialogue Box",
|
||||||
|
"shapes": {
|
||||||
|
"panel": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"left": 170, "top": 500, "width": 800, "height": 260,
|
||||||
|
"fillColor": "#1a1e24", "strokeColor": "#333340",
|
||||||
|
"corners": [8, 8, 8, 8]
|
||||||
|
},
|
||||||
|
"speaker": {
|
||||||
|
"type": "Text",
|
||||||
|
"parent": "panel",
|
||||||
|
"left": 190, "top": 520,
|
||||||
|
"text": "LERA KONSTANTIN",
|
||||||
|
"fontColor": "#c8d0e0", "fontSize": 16
|
||||||
|
},
|
||||||
|
"text-area": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"parent": "panel",
|
||||||
|
"left": 190, "top": 550, "width": 760, "height": 100,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#333340",
|
||||||
|
"corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"dialogue-text": {
|
||||||
|
"type": "Text",
|
||||||
|
"parent": "text-area",
|
||||||
|
"left": 200, "top": 560, "width": 740,
|
||||||
|
"text": "You look like you could use a drink. First time on the station?",
|
||||||
|
"fontColor": "#c8d0e0", "fontSize": 14, "wordWrap": true
|
||||||
|
},
|
||||||
|
"btn-option1": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"parent": "panel",
|
||||||
|
"left": 190, "top": 670, "width": 370, "height": 30,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#c8d8f0",
|
||||||
|
"corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"btn-option1-label": {
|
||||||
|
"type": "Text",
|
||||||
|
"parent": "btn-option1",
|
||||||
|
"left": 200, "top": 674,
|
||||||
|
"text": "[1] Ask about the station",
|
||||||
|
"fontColor": "#c8d8f0", "fontSize": 12
|
||||||
|
},
|
||||||
|
"btn-option2": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"parent": "panel",
|
||||||
|
"left": 190, "top": 710, "width": 370, "height": 30,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#c8d8f0",
|
||||||
|
"corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"btn-option2-label": {
|
||||||
|
"type": "Text",
|
||||||
|
"parent": "btn-option2",
|
||||||
|
"left": 200, "top": 714,
|
||||||
|
"text": "[2] Ask about recent events",
|
||||||
|
"fontColor": "#c8d8f0", "fontSize": 12
|
||||||
|
},
|
||||||
|
"btn-leave": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"parent": "panel",
|
||||||
|
"left": 580, "top": 670, "width": 180, "height": 30,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#c8d8f0",
|
||||||
|
"corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"btn-leave-label": {
|
||||||
|
"type": "Text",
|
||||||
|
"parent": "btn-leave",
|
||||||
|
"left": 590, "top": 674,
|
||||||
|
"text": "[3] Leave",
|
||||||
|
"fontColor": "#c8d8f0", "fontSize": 12
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Menu Screen
|
||||||
|
|
||||||
|
Full-screen menu with sidebar navigation and content area.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Pause Menu",
|
||||||
|
"shapes": {
|
||||||
|
"bg": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"left": 0, "top": 0, "width": 1140, "height": 780,
|
||||||
|
"fillColor": "#1a1e24"
|
||||||
|
},
|
||||||
|
"nav": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"parent": "bg",
|
||||||
|
"left": 20, "top": 20, "width": 200, "height": 740,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#333340",
|
||||||
|
"corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"btn-inventory": {
|
||||||
|
"type": "Rectangle", "parent": "nav",
|
||||||
|
"left": 30, "top": 40, "width": 180, "height": 36,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"btn-inventory-label": {
|
||||||
|
"type": "Text", "parent": "btn-inventory",
|
||||||
|
"left": 40, "top": 48, "text": "Inventory",
|
||||||
|
"fontColor": "#c8d8f0", "fontSize": 14
|
||||||
|
},
|
||||||
|
"btn-journal": {
|
||||||
|
"type": "Rectangle", "parent": "nav",
|
||||||
|
"left": 30, "top": 86, "width": 180, "height": 36,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"btn-journal-label": {
|
||||||
|
"type": "Text", "parent": "btn-journal",
|
||||||
|
"left": 40, "top": 94, "text": "Journal",
|
||||||
|
"fontColor": "#c8d8f0", "fontSize": 14
|
||||||
|
},
|
||||||
|
"btn-map": {
|
||||||
|
"type": "Rectangle", "parent": "nav",
|
||||||
|
"left": 30, "top": 132, "width": 180, "height": 36,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"btn-map-label": {
|
||||||
|
"type": "Text", "parent": "btn-map",
|
||||||
|
"left": 40, "top": 140, "text": "Map",
|
||||||
|
"fontColor": "#c8d8f0", "fontSize": 14
|
||||||
|
},
|
||||||
|
"btn-settings": {
|
||||||
|
"type": "Rectangle", "parent": "nav",
|
||||||
|
"left": 30, "top": 178, "width": 180, "height": 36,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"btn-settings-label": {
|
||||||
|
"type": "Text", "parent": "btn-settings",
|
||||||
|
"left": 40, "top": 186, "text": "Settings",
|
||||||
|
"fontColor": "#c8d8f0", "fontSize": 14
|
||||||
|
},
|
||||||
|
"btn-resume": {
|
||||||
|
"type": "Rectangle", "parent": "nav",
|
||||||
|
"left": 30, "top": 720, "width": 180, "height": 36,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"btn-resume-label": {
|
||||||
|
"type": "Text", "parent": "btn-resume",
|
||||||
|
"left": 40, "top": 728, "text": "Resume",
|
||||||
|
"fontColor": "#c8d8f0", "fontSize": 14
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"parent": "bg",
|
||||||
|
"left": 240, "top": 20, "width": 880, "height": 740,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#333340",
|
||||||
|
"corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"content-label": {
|
||||||
|
"type": "Text", "parent": "content",
|
||||||
|
"left": 260, "top": 40,
|
||||||
|
"text": "Content area",
|
||||||
|
"fontColor": "#c8d0e0", "fontSize": 14
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Modal Dialog
|
||||||
|
|
||||||
|
Centered overlay for confirmations, alerts, choices.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Modal Dialog",
|
||||||
|
"shapes": {
|
||||||
|
"overlay": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"left": 0, "top": 0, "width": 1140, "height": 780,
|
||||||
|
"fillColor": "#0a0c10"
|
||||||
|
},
|
||||||
|
"modal": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"parent": "overlay",
|
||||||
|
"left": 320, "top": 240, "width": 500, "height": 300,
|
||||||
|
"fillColor": "#1a1e24", "strokeColor": "#333340",
|
||||||
|
"corners": [8, 8, 8, 8]
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "Text", "parent": "modal",
|
||||||
|
"left": 340, "top": 260,
|
||||||
|
"text": "Confirm Action",
|
||||||
|
"fontColor": "#c8d0e0", "fontSize": 18
|
||||||
|
},
|
||||||
|
"divider": {
|
||||||
|
"type": "Line", "parent": "modal",
|
||||||
|
"left": 340, "top": 290, "width": 460, "height": 0,
|
||||||
|
"strokeColor": "#333340"
|
||||||
|
},
|
||||||
|
"body-1": {
|
||||||
|
"type": "Text", "parent": "modal",
|
||||||
|
"left": 340, "top": 310,
|
||||||
|
"text": "Are you sure you want to proceed?",
|
||||||
|
"fontColor": "#c8d0e0", "fontSize": 14
|
||||||
|
},
|
||||||
|
"body-2": {
|
||||||
|
"type": "Text", "parent": "modal",
|
||||||
|
"left": 340, "top": 340,
|
||||||
|
"text": "This action cannot be undone.",
|
||||||
|
"fontColor": "#c8d0e0", "fontSize": 14
|
||||||
|
},
|
||||||
|
"btn-cancel": {
|
||||||
|
"type": "Rectangle", "parent": "modal",
|
||||||
|
"left": 480, "top": 480, "width": 120, "height": 36,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"btn-cancel-label": {
|
||||||
|
"type": "Text", "parent": "btn-cancel",
|
||||||
|
"left": 510, "top": 488,
|
||||||
|
"text": "Cancel",
|
||||||
|
"fontColor": "#c8d8f0", "fontSize": 14
|
||||||
|
},
|
||||||
|
"btn-confirm": {
|
||||||
|
"type": "Rectangle", "parent": "modal",
|
||||||
|
"left": 620, "top": 480, "width": 120, "height": 36,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"btn-confirm-label": {
|
||||||
|
"type": "Text", "parent": "btn-confirm",
|
||||||
|
"left": 645, "top": 488,
|
||||||
|
"text": "Confirm",
|
||||||
|
"fontColor": "#c8d8f0", "fontSize": 14
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. List View
|
||||||
|
|
||||||
|
Scrollable list with item selection and detail panel.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "List View",
|
||||||
|
"shapes": {
|
||||||
|
"list-panel": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"left": 20, "top": 20, "width": 400, "height": 740,
|
||||||
|
"fillColor": "#1a1e24", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"item-1": {
|
||||||
|
"type": "Rectangle", "parent": "list-panel",
|
||||||
|
"left": 30, "top": 30, "width": 380, "height": 40,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"item-1-label": {
|
||||||
|
"type": "Text", "parent": "item-1",
|
||||||
|
"left": 40, "top": 38, "text": "Item Alpha",
|
||||||
|
"fontColor": "#c8d8f0", "fontSize": 14
|
||||||
|
},
|
||||||
|
"item-2": {
|
||||||
|
"type": "Rectangle", "parent": "list-panel",
|
||||||
|
"left": 30, "top": 80, "width": 380, "height": 40,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"item-2-label": {
|
||||||
|
"type": "Text", "parent": "item-2",
|
||||||
|
"left": 40, "top": 88, "text": "Item Beta",
|
||||||
|
"fontColor": "#c8d0e0", "fontSize": 14
|
||||||
|
},
|
||||||
|
"item-3": {
|
||||||
|
"type": "Rectangle", "parent": "list-panel",
|
||||||
|
"left": 30, "top": 130, "width": 380, "height": 40,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"item-3-label": {
|
||||||
|
"type": "Text", "parent": "item-3",
|
||||||
|
"left": 40, "top": 138, "text": "Item Gamma",
|
||||||
|
"fontColor": "#c8d0e0", "fontSize": 14
|
||||||
|
},
|
||||||
|
"detail-panel": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"left": 440, "top": 20, "width": 680, "height": 740,
|
||||||
|
"fillColor": "#1a1e24", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"detail-title": {
|
||||||
|
"type": "Text", "parent": "detail-panel",
|
||||||
|
"left": 460, "top": 40,
|
||||||
|
"text": "Item Alpha",
|
||||||
|
"fontColor": "#c8d0e0", "fontSize": 18
|
||||||
|
},
|
||||||
|
"detail-body": {
|
||||||
|
"type": "Text", "parent": "detail-panel",
|
||||||
|
"left": 460, "top": 80, "width": 640,
|
||||||
|
"text": "Description and properties appear here.",
|
||||||
|
"fontColor": "#c8d0e0", "fontSize": 14, "wordWrap": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Inventory Grid
|
||||||
|
|
||||||
|
Grid of cells for item management.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Inventory Grid",
|
||||||
|
"shapes": {
|
||||||
|
"panel": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"left": 240, "top": 100, "width": 660, "height": 580,
|
||||||
|
"fillColor": "#1a1e24", "strokeColor": "#333340", "corners": [8, 8, 8, 8]
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "Text", "parent": "panel",
|
||||||
|
"left": 260, "top": 120,
|
||||||
|
"text": "INVENTORY",
|
||||||
|
"fontColor": "#c8d0e0", "fontSize": 18
|
||||||
|
},
|
||||||
|
"cell-1-1": {
|
||||||
|
"type": "Rectangle", "parent": "panel",
|
||||||
|
"left": 260, "top": 160, "width": 64, "height": 64,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"cell-1-2": {
|
||||||
|
"type": "Rectangle", "parent": "panel",
|
||||||
|
"left": 332, "top": 160, "width": 64, "height": 64,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"cell-1-3": {
|
||||||
|
"type": "Rectangle", "parent": "panel",
|
||||||
|
"left": 404, "top": 160, "width": 64, "height": 64,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"cell-1-4": {
|
||||||
|
"type": "Rectangle", "parent": "panel",
|
||||||
|
"left": 476, "top": 160, "width": 64, "height": 64,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"cell-2-1": {
|
||||||
|
"type": "Rectangle", "parent": "panel",
|
||||||
|
"left": 260, "top": 232, "width": 64, "height": 64,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"cell-2-2": {
|
||||||
|
"type": "Rectangle", "parent": "panel",
|
||||||
|
"left": 332, "top": 232, "width": 64, "height": 64,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"cell-2-3": {
|
||||||
|
"type": "Rectangle", "parent": "panel",
|
||||||
|
"left": 404, "top": 232, "width": 64, "height": 64,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"cell-2-4": {
|
||||||
|
"type": "Rectangle", "parent": "panel",
|
||||||
|
"left": 476, "top": 232, "width": 64, "height": 64,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"detail": {
|
||||||
|
"type": "Rectangle", "parent": "panel",
|
||||||
|
"left": 580, "top": 160, "width": 300, "height": 400,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"detail-title": {
|
||||||
|
"type": "Text", "parent": "detail",
|
||||||
|
"left": 600, "top": 180,
|
||||||
|
"text": "Selected Item Name",
|
||||||
|
"fontColor": "#c8d0e0", "fontSize": 16
|
||||||
|
},
|
||||||
|
"detail-body": {
|
||||||
|
"type": "Text", "parent": "detail",
|
||||||
|
"left": 600, "top": 210, "width": 260,
|
||||||
|
"text": "Item description and stats",
|
||||||
|
"fontColor": "#c8d0e0", "fontSize": 14, "wordWrap": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# Frame0 Setup Guide
|
||||||
|
|
||||||
|
## Installation (Fedora)
|
||||||
|
|
||||||
|
Download from https://frame0.app/download and install the RPM:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo dnf install ./frame0-*.x86_64.rpm
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires: Fedora 40 or later (x86_64).
|
||||||
|
|
||||||
|
## Starting Frame0
|
||||||
|
|
||||||
|
Launch the desktop application:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
frame0 &
|
||||||
|
```
|
||||||
|
|
||||||
|
Frame0 exposes an HTTP API at `localhost:58320` when running.
|
||||||
|
|
||||||
|
## Verify API Access
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.claude/skills/frame0-wireframe/scripts/frame0-cmd.sh health
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output: `Frame0 is running on port 58320`
|
||||||
|
|
||||||
|
## Port Configuration
|
||||||
|
|
||||||
|
Default port: **58320**
|
||||||
|
|
||||||
|
To use a different port, set the environment variable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export FRAME0_PORT=58321
|
||||||
|
```
|
||||||
|
|
||||||
|
Or pass `--port` to any script:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.claude/skills/frame0-wireframe/scripts/frame0-cmd.sh --port 58321 health
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Cause | Fix |
|
||||||
|
|---------|-------|-----|
|
||||||
|
| "Connection refused" | Frame0 not running | Start the desktop app |
|
||||||
|
| "Port in use" | Another instance running | Close duplicate or use different port |
|
||||||
|
| Script hangs | API unresponsive | Restart Frame0 |
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PORT="${FRAME0_PORT:-58320}"
|
||||||
|
ENDPOINT="http://localhost:${PORT}/execute_command"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: $(basename "$0") <command> [args...] [--port N]
|
||||||
|
|
||||||
|
Low-level Frame0 HTTP API wrapper. Replaces the MCP server with direct
|
||||||
|
curl calls. Requires Frame0 desktop app to be running.
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
health Check if Frame0 is running
|
||||||
|
exec <namespace:action> <json> Execute a raw API command
|
||||||
|
create-shape <type> <json-props> Create a shape (Rectangle, Ellipse, Text, Line)
|
||||||
|
get-shape <id> Get shape details
|
||||||
|
update-shape <id> <json-props> Update shape properties
|
||||||
|
delete <id> [id...] Delete shapes by ID
|
||||||
|
move <id> <dx> <dy> Move a shape by pixel offset
|
||||||
|
duplicate <id> Duplicate a shape
|
||||||
|
group <id> [id...] Group shapes
|
||||||
|
ungroup <group-id> Ungroup a group
|
||||||
|
create-connector <tail-id> <head-id> [json-props] Connect two shapes
|
||||||
|
create-icon <name> <json-props> Create an icon shape
|
||||||
|
add-page <name> Add a new page (becomes current)
|
||||||
|
get-page [page-id] Get current or specific page data
|
||||||
|
list-pages [--shapes] List all pages (--shapes for shape data)
|
||||||
|
current-page Get current page ID
|
||||||
|
set-page <page-id> Set current page
|
||||||
|
export [page-id] [--format mime] Export page as image (default: image/png)
|
||||||
|
fit Fit view to screen
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--port N Frame0 API port (default: $PORT, env: FRAME0_PORT)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
$(basename "$0") health
|
||||||
|
$(basename "$0") add-page "HUD Layout"
|
||||||
|
$(basename "$0") create-shape Rectangle '{"name":"btn","left":100,"top":100,"width":120,"height":36}'
|
||||||
|
$(basename "$0") list-pages
|
||||||
|
$(basename "$0") export --format image/png
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse --port from anywhere in args
|
||||||
|
ARGS=()
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--port) PORT="$2"; ENDPOINT="http://localhost:${PORT}/execute_command"; shift 2 ;;
|
||||||
|
*) ARGS+=("$1"); shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
set -- "${ARGS[@]+"${ARGS[@]}"}"
|
||||||
|
|
||||||
|
[[ $# -lt 1 ]] && usage
|
||||||
|
|
||||||
|
# Execute a Frame0 API command, return data or error
|
||||||
|
frame0_exec() {
|
||||||
|
local command="$1"
|
||||||
|
local args
|
||||||
|
args="${2:-"{}"}"
|
||||||
|
|
||||||
|
local response
|
||||||
|
response=$(curl -s -w "\n%{http_code}" -X POST "$ENDPOINT" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"command\": \"$command\", \"args\": $args}" 2>&1) || {
|
||||||
|
echo "ERROR: Cannot connect to Frame0 at localhost:$PORT" >&2
|
||||||
|
echo "Is Frame0 running? See: .claude/skills/frame0-wireframe/references/setup-guide.md" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
local http_code body
|
||||||
|
http_code=$(echo "$response" | tail -1)
|
||||||
|
body=$(echo "$response" | sed '$d')
|
||||||
|
|
||||||
|
if [[ "$http_code" != 2* ]]; then
|
||||||
|
echo "ERROR: HTTP $http_code from Frame0" >&2
|
||||||
|
echo "$body" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Parse success/error from response
|
||||||
|
python3 -c "
|
||||||
|
import sys, json
|
||||||
|
try:
|
||||||
|
r = json.loads(sys.stdin.read())
|
||||||
|
if r.get('success'):
|
||||||
|
d = r.get('data')
|
||||||
|
if d is not None:
|
||||||
|
print(json.dumps(d, indent=2))
|
||||||
|
else:
|
||||||
|
print('ERROR: ' + str(r.get('error', 'Unknown error')), file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
print(f'ERROR: Invalid JSON response: {e}', file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
" <<< "$body"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build JSON array from remaining args
|
||||||
|
ids_to_json_array() {
|
||||||
|
local arr="["
|
||||||
|
local first=true
|
||||||
|
for id in "$@"; do
|
||||||
|
[[ "$first" == true ]] && first=false || arr+=","
|
||||||
|
arr+="\"$id\""
|
||||||
|
done
|
||||||
|
arr+="]"
|
||||||
|
echo "$arr"
|
||||||
|
}
|
||||||
|
|
||||||
|
CMD="${1:-}"
|
||||||
|
shift || true
|
||||||
|
|
||||||
|
case "$CMD" in
|
||||||
|
health)
|
||||||
|
if curl -s -o /dev/null -w "%{http_code}" "http://localhost:${PORT}/" 2>/dev/null | grep -q "^[23]"; then
|
||||||
|
echo "Frame0 is running on port $PORT"
|
||||||
|
else
|
||||||
|
echo "Frame0 is NOT running on port $PORT" >&2
|
||||||
|
echo "Start Frame0 desktop app, then retry." >&2
|
||||||
|
echo "See: .claude/skills/frame0-wireframe/references/setup-guide.md" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
|
||||||
|
exec)
|
||||||
|
[[ $# -lt 2 ]] && { echo "Usage: exec <command> <json-args>" >&2; exit 1; }
|
||||||
|
frame0_exec "$1" "$2"
|
||||||
|
;;
|
||||||
|
|
||||||
|
create-shape)
|
||||||
|
[[ $# -lt 2 ]] && { echo "Usage: create-shape <Type> <json-props>" >&2; exit 1; }
|
||||||
|
local_type="$1"
|
||||||
|
local_props="$2"
|
||||||
|
local_parent="${3:-}"
|
||||||
|
local_parent_arg=""
|
||||||
|
[[ -n "$local_parent" ]] && local_parent_arg=", \"parentId\": \"$local_parent\""
|
||||||
|
frame0_exec "shape:create-shape" "{\"type\": \"$local_type\", \"shapeProps\": $local_props$local_parent_arg, \"convertColors\": true}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
get-shape)
|
||||||
|
[[ $# -lt 1 ]] && { echo "Usage: get-shape <id>" >&2; exit 1; }
|
||||||
|
frame0_exec "shape:get-shape" "{\"shapeId\": \"$1\"}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
update-shape)
|
||||||
|
[[ $# -lt 2 ]] && { echo "Usage: update-shape <id> <json-props>" >&2; exit 1; }
|
||||||
|
frame0_exec "shape:update-shape" "{\"shapeId\": \"$1\", \"shapeProps\": $2, \"convertColors\": true}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
delete)
|
||||||
|
[[ $# -lt 1 ]] && { echo "Usage: delete <id> [id...]" >&2; exit 1; }
|
||||||
|
local_arr=$(ids_to_json_array "$@")
|
||||||
|
frame0_exec "edit:delete" "{\"shapeIdArray\": $local_arr}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
move)
|
||||||
|
[[ $# -lt 3 ]] && { echo "Usage: move <id> <dx> <dy>" >&2; exit 1; }
|
||||||
|
frame0_exec "shape:move" "{\"shapeId\": \"$1\", \"dx\": $2, \"dy\": $3}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
duplicate)
|
||||||
|
[[ $# -lt 1 ]] && { echo "Usage: duplicate <id> [dx] [dy]" >&2; exit 1; }
|
||||||
|
local_dx="${2:-0}"
|
||||||
|
local_dy="${3:-0}"
|
||||||
|
frame0_exec "edit:duplicate" "{\"shapeIdArray\": [\"$1\"], \"dx\": $local_dx, \"dy\": $local_dy}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
group)
|
||||||
|
[[ $# -lt 2 ]] && { echo "Usage: group <id> <id> [id...]" >&2; exit 1; }
|
||||||
|
local_arr=$(ids_to_json_array "$@")
|
||||||
|
frame0_exec "shape:group" "{\"shapeIdArray\": $local_arr}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
ungroup)
|
||||||
|
[[ $# -lt 1 ]] && { echo "Usage: ungroup <group-id>" >&2; exit 1; }
|
||||||
|
frame0_exec "shape:ungroup" "{\"shapeIdArray\": [\"$1\"]}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
create-connector)
|
||||||
|
[[ $# -lt 2 ]] && { echo "Usage: create-connector <tail-id> <head-id> [json-props]" >&2; exit 1; }
|
||||||
|
local_props="${3:-{}}"
|
||||||
|
frame0_exec "shape:create-connector" "{\"tailId\": \"$1\", \"headId\": \"$2\", \"shapeProps\": $local_props, \"convertColors\": true}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
create-icon)
|
||||||
|
[[ $# -lt 2 ]] && { echo "Usage: create-icon <name> <json-props>" >&2; exit 1; }
|
||||||
|
frame0_exec "shape:create-icon" "{\"iconName\": \"$1\", \"shapeProps\": $2, \"convertColors\": true}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
add-page)
|
||||||
|
[[ $# -lt 1 ]] && { echo "Usage: add-page <name>" >&2; exit 1; }
|
||||||
|
frame0_exec "page:add" "{\"pageProps\": {\"name\": \"$1\"}}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
get-page)
|
||||||
|
if [[ $# -ge 1 ]]; then
|
||||||
|
frame0_exec "page:get" "{\"pageId\": \"$1\", \"exportShapes\": true}"
|
||||||
|
else
|
||||||
|
local_id
|
||||||
|
local_id=$(frame0_exec "page:get-current-page")
|
||||||
|
# Strip quotes from returned ID
|
||||||
|
local_id=$(echo "$local_id" | tr -d '"')
|
||||||
|
frame0_exec "page:get" "{\"pageId\": \"$local_id\", \"exportShapes\": true}"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
|
||||||
|
list-pages)
|
||||||
|
local_shapes="false"
|
||||||
|
[[ "${1:-}" == "--shapes" ]] && local_shapes="true"
|
||||||
|
frame0_exec "doc:get" "{\"exportPages\": true, \"exportShapes\": $local_shapes}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
current-page)
|
||||||
|
frame0_exec "page:get-current-page"
|
||||||
|
;;
|
||||||
|
|
||||||
|
set-page)
|
||||||
|
[[ $# -lt 1 ]] && { echo "Usage: set-page <page-id>" >&2; exit 1; }
|
||||||
|
frame0_exec "page:set-current-page" "{\"pageId\": \"$1\"}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
export)
|
||||||
|
local_page_id=""
|
||||||
|
local_format="image/png"
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--format) local_format="$2"; shift 2 ;;
|
||||||
|
*) local_page_id="$1"; shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
local_page_arg=""
|
||||||
|
[[ -n "$local_page_id" ]] && local_page_arg="\"pageId\": \"$local_page_id\", "
|
||||||
|
frame0_exec "file:export-image" "{${local_page_arg}\"format\": \"$local_format\", \"fillBackground\": true}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
fit)
|
||||||
|
frame0_exec "view:fit-to-screen"
|
||||||
|
;;
|
||||||
|
|
||||||
|
--help|-h|help)
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
echo "Unknown command: $CMD" >&2
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Batch export wireframes from JSON to PNG via Frame0.
|
||||||
|
#
|
||||||
|
# Finds all .json wireframe files under docs/design/wireframes/ and exports
|
||||||
|
# each to a matching .png. Skips files whose PNG is already newer than the
|
||||||
|
# JSON, unless --force is passed.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# frame0-export-batch.sh [--dry-run] [--force] [--category CAT] [--root DIR]
|
||||||
|
#
|
||||||
|
# Options:
|
||||||
|
# --dry-run Print manifest only, don't touch Frame0.
|
||||||
|
# --force Re-export even if PNG already exists and is up to date.
|
||||||
|
# --category CAT Limit to one subdirectory (e.g. --category dialogue)
|
||||||
|
# --root DIR Wireframes root dir (default: docs/design/wireframes)
|
||||||
|
#
|
||||||
|
# Exit codes:
|
||||||
|
# 0 All exports succeeded (or nothing to do)
|
||||||
|
# 1 One or more exports failed
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
|
||||||
|
SYNC="$SCRIPT_DIR/frame0-sync.py"
|
||||||
|
DEFAULT_ROOT="$REPO_ROOT/docs/design/wireframes"
|
||||||
|
|
||||||
|
DRY_RUN=false
|
||||||
|
FORCE=false
|
||||||
|
CATEGORY=""
|
||||||
|
WF_ROOT="$DEFAULT_ROOT"
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--dry-run) DRY_RUN=true; shift ;;
|
||||||
|
--force) FORCE=true; shift ;;
|
||||||
|
--category) CATEGORY="$2"; shift 2 ;;
|
||||||
|
--root) WF_ROOT="$2"; shift 2 ;;
|
||||||
|
-h|--help)
|
||||||
|
sed -n '/^# /p' "$0" | sed 's/^# //'
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ! -d "$WF_ROOT" ]]; then
|
||||||
|
echo "ERROR: Wireframes directory not found: $WF_ROOT" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect JSON files, optionally filtered by category subdirectory
|
||||||
|
mapfile -t JSON_FILES < <(
|
||||||
|
if [[ -n "$CATEGORY" ]]; then
|
||||||
|
find "$WF_ROOT/$CATEGORY" -name "*.json" ! -name ".*" | sort
|
||||||
|
else
|
||||||
|
find "$WF_ROOT" -name "*.json" ! -name ".*" | sort
|
||||||
|
fi
|
||||||
|
)
|
||||||
|
|
||||||
|
if [[ ${#JSON_FILES[@]} -eq 0 ]]; then
|
||||||
|
echo "No wireframe JSON files found."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Classify files into to-export and to-skip
|
||||||
|
TO_EXPORT=()
|
||||||
|
TO_SKIP=()
|
||||||
|
|
||||||
|
for json in "${JSON_FILES[@]}"; do
|
||||||
|
png="${json%.json}.png"
|
||||||
|
if $FORCE || [[ ! -f "$png" ]] || [[ "$json" -nt "$png" ]]; then
|
||||||
|
TO_EXPORT+=("$json")
|
||||||
|
else
|
||||||
|
TO_SKIP+=("$json")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Print manifest
|
||||||
|
if [[ ${#TO_EXPORT[@]} -gt 0 ]]; then
|
||||||
|
echo ""
|
||||||
|
echo "Will export (${#TO_EXPORT[@]} files):"
|
||||||
|
for json in "${TO_EXPORT[@]}"; do
|
||||||
|
png="${json%.json}.png"
|
||||||
|
rel="${json#$REPO_ROOT/}"
|
||||||
|
if [[ ! -f "$png" ]]; then
|
||||||
|
status="new"
|
||||||
|
else
|
||||||
|
status="updated"
|
||||||
|
fi
|
||||||
|
printf " [%-7s] %s\n" "$status" "$rel"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo "Nothing to export (all PNGs up to date)."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ${#TO_SKIP[@]} -gt 0 ]]; then
|
||||||
|
echo ""
|
||||||
|
echo "Will skip (${#TO_SKIP[@]} files already up to date):"
|
||||||
|
for json in "${TO_SKIP[@]}"; do
|
||||||
|
rel="${json#$REPO_ROOT/}"
|
||||||
|
printf " [skip ] %s\n" "$rel"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if $DRY_RUN; then
|
||||||
|
echo ""
|
||||||
|
echo "Dry run — no exports performed."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ${#TO_EXPORT[@]} -eq 0 ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
PASSED=0
|
||||||
|
FAILED=0
|
||||||
|
FAILED_FILES=()
|
||||||
|
|
||||||
|
TOTAL=${#TO_EXPORT[@]}
|
||||||
|
IDX=0
|
||||||
|
|
||||||
|
for json in "${TO_EXPORT[@]}"; do
|
||||||
|
IDX=$((IDX + 1))
|
||||||
|
png="${json%.json}.png"
|
||||||
|
rel="${json#$REPO_ROOT/}"
|
||||||
|
|
||||||
|
printf "[%d/%d] %s ... " "$IDX" "$TOTAL" "$rel"
|
||||||
|
|
||||||
|
output=$(python3 "$SYNC" export "$json" "$png" 2>/tmp/frame0-batch-err.txt)
|
||||||
|
rc=$?
|
||||||
|
if [[ $rc -eq 0 ]]; then
|
||||||
|
size=$(echo "$output" | tail -1 | grep -oP '\(\K[^)]+' || true)
|
||||||
|
echo "ok $size"
|
||||||
|
PASSED=$((PASSED + 1))
|
||||||
|
else
|
||||||
|
echo "FAILED"
|
||||||
|
cat /tmp/frame0-batch-err.txt >&2
|
||||||
|
FAILED=$((FAILED + 1))
|
||||||
|
FAILED_FILES+=("$rel")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "$PASSED exported, $FAILED failed."
|
||||||
|
|
||||||
|
if [[ $FAILED -gt 0 ]]; then
|
||||||
|
echo ""
|
||||||
|
echo "Failed:" >&2
|
||||||
|
for f in "${FAILED_FILES[@]}"; do
|
||||||
|
echo " $f" >&2
|
||||||
|
done
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,414 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Frame0 sync: push local JSON wireframes to Frame0, pull pages back.
|
||||||
|
|
||||||
|
Local JSON is source of truth. Frame0 is a renderer.
|
||||||
|
A mapping file tracks local_id <-> frame0_id across push/pull cycles.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
frame0-sync.py push <wireframe.json> [--port PORT]
|
||||||
|
frame0-sync.py pull <page-id|page-name> <output.json> [--port PORT]
|
||||||
|
frame0-sync.py export <wireframe.json> <output.png> [--port PORT] [--format MIME]
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
|
|
||||||
|
DEFAULT_PORT = 58320
|
||||||
|
|
||||||
|
|
||||||
|
def api(port, command, args=None):
|
||||||
|
"""Execute a Frame0 API command. Returns the data field on success."""
|
||||||
|
url = f"http://localhost:{port}/execute_command"
|
||||||
|
payload = json.dumps({"command": command, "args": args or {}}).encode()
|
||||||
|
req = urllib.request.Request(url, data=payload, headers={"Content-Type": "application/json"})
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req) as resp:
|
||||||
|
result = json.loads(resp.read())
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
body = e.read().decode()
|
||||||
|
print(f"ERROR: {command}: HTTP {e.code}: {body[:500]}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
except urllib.error.URLError as e:
|
||||||
|
print(f"ERROR: Cannot connect to Frame0 on port {port}: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not result.get("success"):
|
||||||
|
print(f"ERROR: {command}: {result.get('error', 'unknown')}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
return result.get("data")
|
||||||
|
|
||||||
|
|
||||||
|
# -- Mapping file management --------------------------------------------------
|
||||||
|
|
||||||
|
def mapping_path(wireframe_path):
|
||||||
|
"""Derive the idmap path from the wireframe JSON path."""
|
||||||
|
d = os.path.dirname(wireframe_path)
|
||||||
|
base = os.path.splitext(os.path.basename(wireframe_path))[0]
|
||||||
|
return os.path.join(d, f".{base}.idmap.json")
|
||||||
|
|
||||||
|
|
||||||
|
def load_mapping(wireframe_path):
|
||||||
|
p = mapping_path(wireframe_path)
|
||||||
|
if os.path.exists(p):
|
||||||
|
with open(p) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return {"page_id": None, "shapes": {}, "connectors": {}}
|
||||||
|
|
||||||
|
|
||||||
|
def save_mapping(wireframe_path, mapping):
|
||||||
|
p = mapping_path(wireframe_path)
|
||||||
|
os.makedirs(os.path.dirname(p) or ".", exist_ok=True)
|
||||||
|
with open(p, "w") as f:
|
||||||
|
json.dump(mapping, f, indent=2)
|
||||||
|
f.write("\n")
|
||||||
|
|
||||||
|
|
||||||
|
# -- Push: local JSON -> Frame0 -----------------------------------------------
|
||||||
|
|
||||||
|
# Frame0 returns different type names from get vs what create accepts.
|
||||||
|
# Map internal types back to create-API types.
|
||||||
|
TYPE_TO_CREATE = {
|
||||||
|
"Box": "Rectangle",
|
||||||
|
"box": "Rectangle",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def topo_sort_shapes(shapes):
|
||||||
|
"""Sort shape IDs so parents come before children."""
|
||||||
|
order = []
|
||||||
|
visited = set()
|
||||||
|
|
||||||
|
def visit(sid):
|
||||||
|
if sid in visited:
|
||||||
|
return
|
||||||
|
visited.add(sid)
|
||||||
|
shape = shapes[sid]
|
||||||
|
parent = shape.get("parent")
|
||||||
|
if parent and parent in shapes:
|
||||||
|
visit(parent)
|
||||||
|
order.append(sid)
|
||||||
|
|
||||||
|
for sid in shapes:
|
||||||
|
visit(sid)
|
||||||
|
return order
|
||||||
|
|
||||||
|
|
||||||
|
def find_or_create_page(port, name, mapping):
|
||||||
|
"""Find existing page by mapping or name, or create a new one."""
|
||||||
|
# Try mapped page_id first
|
||||||
|
if mapping.get("page_id"):
|
||||||
|
try:
|
||||||
|
page = api(port, "page:get", {"pageId": mapping["page_id"]})
|
||||||
|
if page:
|
||||||
|
return mapping["page_id"]
|
||||||
|
except SystemExit:
|
||||||
|
pass # Page no longer exists, fall through
|
||||||
|
|
||||||
|
# Search by name in existing pages
|
||||||
|
doc = api(port, "doc:get", {"exportPages": True, "exportShapes": False})
|
||||||
|
if doc and "children" in doc:
|
||||||
|
for page in doc["children"]:
|
||||||
|
if page.get("name") == name:
|
||||||
|
return page["id"]
|
||||||
|
|
||||||
|
# Create new page
|
||||||
|
page = api(port, "page:add", {"pageProps": {"name": name}})
|
||||||
|
return page["id"]
|
||||||
|
|
||||||
|
|
||||||
|
def clear_page(port, page_id):
|
||||||
|
"""Delete all shapes on a page."""
|
||||||
|
page = api(port, "page:get", {"pageId": page_id, "exportShapes": True})
|
||||||
|
if not page or "children" not in page:
|
||||||
|
return
|
||||||
|
shape_ids = [s["id"] for s in page["children"]]
|
||||||
|
if shape_ids:
|
||||||
|
api(port, "edit:delete", {"shapeIdArray": shape_ids})
|
||||||
|
|
||||||
|
|
||||||
|
def push(wireframe_path, port):
|
||||||
|
"""Push local wireframe JSON to Frame0."""
|
||||||
|
with open(wireframe_path) as f:
|
||||||
|
wireframe = json.load(f)
|
||||||
|
|
||||||
|
name = wireframe.get("name", os.path.splitext(os.path.basename(wireframe_path))[0])
|
||||||
|
shapes = wireframe.get("shapes", {})
|
||||||
|
connectors = wireframe.get("connectors", {})
|
||||||
|
|
||||||
|
mapping = load_mapping(wireframe_path)
|
||||||
|
|
||||||
|
# Find or create page
|
||||||
|
page_id = find_or_create_page(port, name, mapping)
|
||||||
|
mapping["page_id"] = page_id
|
||||||
|
|
||||||
|
# Switch to page and clear it
|
||||||
|
api(port, "page:set-current-page", {"pageId": page_id})
|
||||||
|
clear_page(port, page_id)
|
||||||
|
|
||||||
|
# Reset ID mappings (shapes are recreated)
|
||||||
|
mapping["shapes"] = {}
|
||||||
|
mapping["connectors"] = {}
|
||||||
|
|
||||||
|
# Create shapes in dependency order
|
||||||
|
order = topo_sort_shapes(shapes)
|
||||||
|
for local_id in order:
|
||||||
|
shape = shapes[local_id]
|
||||||
|
raw_type = shape.get("type", "Rectangle")
|
||||||
|
shape_type = TYPE_TO_CREATE.get(raw_type, raw_type)
|
||||||
|
parent_local = shape.get("parent")
|
||||||
|
|
||||||
|
# Build shapeProps from everything except meta fields
|
||||||
|
meta_keys = {"type", "parent"}
|
||||||
|
props = {k: v for k, v in shape.items() if k not in meta_keys}
|
||||||
|
|
||||||
|
# Set name to local_id if not explicitly set
|
||||||
|
if "name" not in props:
|
||||||
|
props["name"] = local_id
|
||||||
|
|
||||||
|
create_args = {
|
||||||
|
"type": shape_type,
|
||||||
|
"shapeProps": props,
|
||||||
|
"convertColors": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Resolve parent ID
|
||||||
|
if parent_local and parent_local in mapping["shapes"]:
|
||||||
|
create_args["parentId"] = mapping["shapes"][parent_local]
|
||||||
|
|
||||||
|
f0_id = api(port, "shape:create-shape", create_args)
|
||||||
|
mapping["shapes"][local_id] = f0_id
|
||||||
|
|
||||||
|
# Create connectors
|
||||||
|
for local_id, conn in connectors.items():
|
||||||
|
tail_local = conn.get("tailId")
|
||||||
|
head_local = conn.get("headId")
|
||||||
|
|
||||||
|
if tail_local not in mapping["shapes"] or head_local not in mapping["shapes"]:
|
||||||
|
print(f"WARNING: connector '{local_id}' references unknown shape, skipping", file=sys.stderr)
|
||||||
|
continue
|
||||||
|
|
||||||
|
meta_keys = {"tailId", "headId"}
|
||||||
|
props = {k: v for k, v in conn.items() if k not in meta_keys}
|
||||||
|
if "name" not in props:
|
||||||
|
props["name"] = local_id
|
||||||
|
|
||||||
|
f0_id = api(port, "shape:create-connector", {
|
||||||
|
"tailId": mapping["shapes"][tail_local],
|
||||||
|
"headId": mapping["shapes"][head_local],
|
||||||
|
"shapeProps": props,
|
||||||
|
"convertColors": True,
|
||||||
|
})
|
||||||
|
mapping["connectors"][local_id] = f0_id
|
||||||
|
|
||||||
|
# Fit to screen
|
||||||
|
api(port, "view:fit-to-screen")
|
||||||
|
|
||||||
|
save_mapping(wireframe_path, mapping)
|
||||||
|
total = len(mapping["shapes"]) + len(mapping["connectors"])
|
||||||
|
print(f"Pushed '{name}' to Frame0: {len(mapping['shapes'])} shapes, {len(mapping['connectors'])} connectors")
|
||||||
|
|
||||||
|
|
||||||
|
# -- Pull: Frame0 -> local JSON -----------------------------------------------
|
||||||
|
|
||||||
|
def pull(page_ref, output_path, port):
|
||||||
|
"""Pull a Frame0 page into local wireframe JSON."""
|
||||||
|
# Resolve page_ref: could be an ID or a name
|
||||||
|
page_id = None
|
||||||
|
doc = api(port, "doc:get", {"exportPages": True, "exportShapes": False})
|
||||||
|
if doc and "children" in doc:
|
||||||
|
for page in doc["children"]:
|
||||||
|
if page["id"] == page_ref or page.get("name") == page_ref:
|
||||||
|
page_id = page["id"]
|
||||||
|
page_name = page.get("name", page_ref)
|
||||||
|
break
|
||||||
|
|
||||||
|
if not page_id:
|
||||||
|
print(f"ERROR: Page not found: {page_ref}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Load existing mapping for reverse lookup
|
||||||
|
mapping = load_mapping(output_path)
|
||||||
|
reverse_map = {v: k for k, v in mapping.get("shapes", {}).items()}
|
||||||
|
reverse_conn = {v: k for k, v in mapping.get("connectors", {}).items()}
|
||||||
|
|
||||||
|
# Get full page with shapes
|
||||||
|
page = api(port, "page:get", {"pageId": page_id, "exportShapes": True})
|
||||||
|
|
||||||
|
shapes = {}
|
||||||
|
connectors = {}
|
||||||
|
new_mapping = {"page_id": page_id, "shapes": {}, "connectors": {}}
|
||||||
|
auto_id_counter = [0]
|
||||||
|
|
||||||
|
def auto_id(f0_shape):
|
||||||
|
"""Generate a stable local ID from shape name or auto-number."""
|
||||||
|
# Prefer existing mapping
|
||||||
|
f0_id = f0_shape["id"]
|
||||||
|
if f0_id in reverse_map:
|
||||||
|
return reverse_map[f0_id]
|
||||||
|
# Use sanitized name
|
||||||
|
name = f0_shape.get("name", "")
|
||||||
|
if name:
|
||||||
|
sanitized = name.lower().replace(" ", "-").replace("_", "-")
|
||||||
|
if sanitized not in shapes:
|
||||||
|
return sanitized
|
||||||
|
# Fallback: auto-number
|
||||||
|
auto_id_counter[0] += 1
|
||||||
|
return f"s{auto_id_counter[0]:03d}"
|
||||||
|
|
||||||
|
def process_shape(f0_shape, parent_local_id=None):
|
||||||
|
f0_id = f0_shape["id"]
|
||||||
|
local_id = auto_id(f0_shape)
|
||||||
|
new_mapping["shapes"][local_id] = f0_id
|
||||||
|
|
||||||
|
# Extract shape properties — only strip structural keys that our
|
||||||
|
# ID mapping replaces. Everything else passes through as-is so the
|
||||||
|
# local JSON speaks Frame0's native vocabulary.
|
||||||
|
shape_type = f0_shape.get("type", "Box")
|
||||||
|
skip_keys = {"id", "type", "children", "pageId", "parentId"}
|
||||||
|
props = {k: v for k, v in f0_shape.items() if k not in skip_keys and v is not None}
|
||||||
|
|
||||||
|
entry = {"type": shape_type}
|
||||||
|
if parent_local_id:
|
||||||
|
entry["parent"] = parent_local_id
|
||||||
|
entry.update(props)
|
||||||
|
|
||||||
|
# Remove name if it matches local_id (redundant)
|
||||||
|
if entry.get("name") == local_id:
|
||||||
|
del entry["name"]
|
||||||
|
|
||||||
|
shapes[local_id] = entry
|
||||||
|
|
||||||
|
# Process children recursively
|
||||||
|
for child in f0_shape.get("children", []):
|
||||||
|
child_type = child.get("type", "")
|
||||||
|
if child_type == "Connector":
|
||||||
|
process_connector(child)
|
||||||
|
else:
|
||||||
|
process_shape(child, local_id)
|
||||||
|
|
||||||
|
def process_connector(f0_conn):
|
||||||
|
f0_id = f0_conn["id"]
|
||||||
|
local_id = reverse_conn.get(f0_id)
|
||||||
|
if not local_id:
|
||||||
|
auto_id_counter[0] += 1
|
||||||
|
local_id = f"c{auto_id_counter[0]:03d}"
|
||||||
|
|
||||||
|
new_mapping["connectors"][local_id] = f0_id
|
||||||
|
|
||||||
|
tail_f0 = f0_conn.get("tail", {}).get("id")
|
||||||
|
head_f0 = f0_conn.get("head", {}).get("id")
|
||||||
|
|
||||||
|
entry = {}
|
||||||
|
if tail_f0:
|
||||||
|
# Will be resolved after all shapes are processed
|
||||||
|
entry["_tailF0"] = tail_f0
|
||||||
|
if head_f0:
|
||||||
|
entry["_headF0"] = head_f0
|
||||||
|
|
||||||
|
skip_keys = {"id", "type", "children", "pageId", "tail", "head"}
|
||||||
|
props = {k: v for k, v in f0_conn.items() if k not in skip_keys and v is not None}
|
||||||
|
entry.update(props)
|
||||||
|
|
||||||
|
connectors[local_id] = entry
|
||||||
|
|
||||||
|
# Process all top-level shapes
|
||||||
|
for child in page.get("children", []):
|
||||||
|
child_type = child.get("type", "")
|
||||||
|
if child_type == "Connector":
|
||||||
|
process_connector(child)
|
||||||
|
else:
|
||||||
|
process_shape(child)
|
||||||
|
|
||||||
|
# Resolve connector references to local IDs
|
||||||
|
f0_to_local = {v: k for k, v in new_mapping["shapes"].items()}
|
||||||
|
for conn in connectors.values():
|
||||||
|
tail_f0 = conn.pop("_tailF0", None)
|
||||||
|
head_f0 = conn.pop("_headF0", None)
|
||||||
|
if tail_f0 and tail_f0 in f0_to_local:
|
||||||
|
conn["tailId"] = f0_to_local[tail_f0]
|
||||||
|
if head_f0 and head_f0 in f0_to_local:
|
||||||
|
conn["headId"] = f0_to_local[head_f0]
|
||||||
|
|
||||||
|
wireframe = {"name": page_name}
|
||||||
|
if shapes:
|
||||||
|
wireframe["shapes"] = shapes
|
||||||
|
if connectors:
|
||||||
|
wireframe["connectors"] = connectors
|
||||||
|
|
||||||
|
os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
|
||||||
|
with open(output_path, "w") as f:
|
||||||
|
json.dump(wireframe, f, indent=2)
|
||||||
|
f.write("\n")
|
||||||
|
|
||||||
|
save_mapping(output_path, new_mapping)
|
||||||
|
print(f"Pulled '{page_name}' -> {output_path}: {len(shapes)} shapes, {len(connectors)} connectors")
|
||||||
|
|
||||||
|
|
||||||
|
# -- Export: push then export as image -----------------------------------------
|
||||||
|
|
||||||
|
def export_image(wireframe_path, output_path, port, fmt="image/png"):
|
||||||
|
"""Push wireframe to Frame0 and export the page as an image."""
|
||||||
|
import base64
|
||||||
|
|
||||||
|
# Push first to ensure Frame0 is up to date
|
||||||
|
push(wireframe_path, port)
|
||||||
|
|
||||||
|
mapping = load_mapping(wireframe_path)
|
||||||
|
page_id = mapping.get("page_id")
|
||||||
|
if not page_id:
|
||||||
|
print("ERROR: No page_id in mapping after push", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
image_b64 = api(port, "file:export-image", {
|
||||||
|
"pageId": page_id,
|
||||||
|
"format": fmt,
|
||||||
|
"fillBackground": True,
|
||||||
|
})
|
||||||
|
|
||||||
|
image_bytes = base64.b64decode(image_b64)
|
||||||
|
with open(output_path, "wb") as f:
|
||||||
|
f.write(image_bytes)
|
||||||
|
|
||||||
|
print(f"Exported: {output_path} ({len(image_bytes) // 1024}KB)")
|
||||||
|
|
||||||
|
|
||||||
|
# -- CLI -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Sync wireframe JSON with Frame0")
|
||||||
|
parser.add_argument("--port", type=int, default=int(os.environ.get("FRAME0_PORT", DEFAULT_PORT)))
|
||||||
|
sub = parser.add_subparsers(dest="command")
|
||||||
|
|
||||||
|
p_push = sub.add_parser("push", help="Push local JSON to Frame0")
|
||||||
|
p_push.add_argument("wireframe", help="Path to wireframe .json file")
|
||||||
|
|
||||||
|
p_pull = sub.add_parser("pull", help="Pull Frame0 page to local JSON")
|
||||||
|
p_pull.add_argument("page", help="Page ID or page name")
|
||||||
|
p_pull.add_argument("output", help="Output .json path")
|
||||||
|
|
||||||
|
p_export = sub.add_parser("export", help="Push and export as image")
|
||||||
|
p_export.add_argument("wireframe", help="Path to wireframe .json file")
|
||||||
|
p_export.add_argument("output", help="Output image path (e.g. wireframe.png)")
|
||||||
|
p_export.add_argument("--format", default="image/png",
|
||||||
|
help="Export MIME type (default: image/png)")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.command == "push":
|
||||||
|
push(args.wireframe, args.port)
|
||||||
|
elif args.command == "pull":
|
||||||
|
pull(args.page, args.output, args.port)
|
||||||
|
elif args.command == "export":
|
||||||
|
export_image(args.wireframe, args.output, args.port, args.format)
|
||||||
|
else:
|
||||||
|
parser.print_help()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,187 +0,0 @@
|
|||||||
---
|
|
||||||
name: gen-audio
|
|
||||||
description: >
|
|
||||||
Generate audio assets for The Settled Reach using the Stable Audio Open API
|
|
||||||
(self-hosted Gradio app at tower-of-joy:11500). Use when generating any game
|
|
||||||
audio: ambient loops, SFX, UI sounds, monologue chimes, footsteps, or any
|
|
||||||
sound asset from docs/assets/audio/. Also use when the user asks about audio
|
|
||||||
generation, sound design pipeline, or audio asset iteration. Triggers on:
|
|
||||||
"generate audio", "make sounds", "create ambient", "audio pipeline",
|
|
||||||
"generate sfx", "stable audio", "gen audio", "sound design".
|
|
||||||
---
|
|
||||||
|
|
||||||
# Audio Generation — The Settled Reach
|
|
||||||
|
|
||||||
Generate sonically consistent audio assets using the Stable Audio Open API via
|
|
||||||
wrapper scripts at `db/connectors/audio-*`.
|
|
||||||
|
|
||||||
Asset descriptions, filenames, bus routing, and design intent are documented in
|
|
||||||
`docs/assets/audio/`. This skill provides the prompt system, generation
|
|
||||||
workflow, and quality validation.
|
|
||||||
|
|
||||||
## API Access
|
|
||||||
|
|
||||||
**Never call the API directly.** Use the wrapper scripts:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check API health
|
|
||||||
db/connectors/audio-health
|
|
||||||
|
|
||||||
# Generate audio
|
|
||||||
db/connectors/audio-generate "prompt text" \
|
|
||||||
--duration 10 \
|
|
||||||
--steps 100 \
|
|
||||||
--cfg 7 \
|
|
||||||
--output path/to/output.wav \
|
|
||||||
--timeout 600
|
|
||||||
```
|
|
||||||
|
|
||||||
### Parameters
|
|
||||||
|
|
||||||
| Parameter | Default | Range | Notes |
|
|
||||||
|-----------|---------|-------|-------|
|
|
||||||
| `--duration` | 10 | 0-47s | Max 47s per generation. For longer loops, generate 45s with crossfade overlap. |
|
|
||||||
| `--steps` | 100 | 10-200 | More steps = better quality, slower. Use 50 for quick previews, 100-150 for final. |
|
|
||||||
| `--cfg` | 7 | 1-15 | Classifier-free guidance. Higher = more prompt-adherent but less natural. 5-9 is the sweet spot. |
|
|
||||||
| `--output` | auto | — | Output file path. Auto-names from prompt if omitted. |
|
|
||||||
| `--timeout` | 600 | — | Max wait in seconds. Generation can take 2-5 minutes on 11GB VRAM. |
|
|
||||||
|
|
||||||
### Critical Constraints
|
|
||||||
|
|
||||||
- **NEVER parallelize requests.** The server has 11GB VRAM and runs one
|
|
||||||
generation at a time. Always wait for a generation to complete before
|
|
||||||
starting the next. Sequential only.
|
|
||||||
- **Generation takes 2-5 minutes** per clip depending on duration and steps.
|
|
||||||
Be patient. The timeout default (600s) is generous.
|
|
||||||
- **Max 47 seconds** per generation. For 60-90s ambient loops, generate 45s
|
|
||||||
clips and crossfade-stitch in post-processing.
|
|
||||||
- **Output is WAV at 44.1kHz stereo.** Convert to .ogg for Godot import:
|
|
||||||
`ffmpeg -i input.wav -c:a libvorbis -q:a 6 output.ogg`
|
|
||||||
|
|
||||||
## Prompt Assembly
|
|
||||||
|
|
||||||
Every generation uses three parts:
|
|
||||||
|
|
||||||
```
|
|
||||||
[SONIC FAMILY PREFIX] + [CATEGORY TEMPLATE] + [ASSET DESCRIPTION from docs/assets/audio/]
|
|
||||||
```
|
|
||||||
|
|
||||||
Never call the API with just the asset description. Always prepend the sonic
|
|
||||||
family prefix and matching category template.
|
|
||||||
|
|
||||||
- **Sonic palette and families:** Read `references/sonic-palette.md`
|
|
||||||
- **Category templates:** Read `references/category-templates.md` and match by
|
|
||||||
asset type (ambient, sfx, ui)
|
|
||||||
- **Asset description:** Look up the specific asset in `docs/assets/audio/{category}.md`
|
|
||||||
|
|
||||||
## Single Asset Workflow
|
|
||||||
|
|
||||||
1. Find the asset in `docs/assets/audio/{ambient,sfx,ui}.md` — note filename,
|
|
||||||
duration, bus, method, and design intent.
|
|
||||||
2. Read `references/sonic-palette.md` for the sonic family prefix.
|
|
||||||
3. Read `references/category-templates.md` for the matching template.
|
|
||||||
4. Assemble the full prompt.
|
|
||||||
5. Run `db/connectors/audio-health` to verify the API is up.
|
|
||||||
6. Run `db/connectors/audio-generate` with the assembled prompt. **One request
|
|
||||||
at a time. Wait for completion.**
|
|
||||||
7. Listen to the output (or describe it based on file size/duration).
|
|
||||||
8. If acceptable, convert to .ogg and place in `client/assets/audio/`.
|
|
||||||
9. Update the asset status in `docs/assets/audio/{category}.md`.
|
|
||||||
|
|
||||||
## Iteration Workflow
|
|
||||||
|
|
||||||
For each asset, generate 4-6 candidates:
|
|
||||||
|
|
||||||
1. **Generate candidates** — vary the prompt slightly (add/remove descriptors,
|
|
||||||
adjust CFG between 5-9). Run each generation sequentially — never in
|
|
||||||
parallel.
|
|
||||||
2. **Solo test** — does each candidate sound right alone?
|
|
||||||
3. **Stack test** — play the candidate alongside other layers. Does it mask or
|
|
||||||
clash?
|
|
||||||
4. **Fatigue test** (loops only) — can you listen for 5+ minutes without a
|
|
||||||
jarring repeat?
|
|
||||||
5. **Close-your-eyes test** — does it create a mental image or sensation?
|
|
||||||
6. Select the best candidate, trim, normalize, convert.
|
|
||||||
|
|
||||||
## Post-Processing
|
|
||||||
|
|
||||||
After selecting the best generation:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Trim silence from start/end
|
|
||||||
ffmpeg -i input.wav -af "silenceremove=start_periods=1:start_silence=0.1:start_threshold=-50dB,areverse,silenceremove=start_periods=1:start_silence=0.1:start_threshold=-50dB,areverse" trimmed.wav
|
|
||||||
|
|
||||||
# LUFS normalize to -16 LUFS (broadcast standard, good for game audio)
|
|
||||||
ffmpeg -i trimmed.wav -af loudnorm=I=-16:LRA=11:TP=-1 normalized.wav
|
|
||||||
|
|
||||||
# Convert to .ogg for Godot
|
|
||||||
ffmpeg -i normalized.wav -c:a libvorbis -q:a 6 output.ogg
|
|
||||||
|
|
||||||
# For loops: verify loop point
|
|
||||||
ffplay -loop 0 output.ogg
|
|
||||||
```
|
|
||||||
|
|
||||||
For ambient loops, create crossfade overlap:
|
|
||||||
```bash
|
|
||||||
# Create a 45s loop with 3s crossfade overlap
|
|
||||||
# (manual: export 48s, crossfade first 3s with last 3s in Audacity)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Manual Synthesis (Insert-Tech Sounds)
|
|
||||||
|
|
||||||
For sounds under 200ms (cursor hover, weapon aim), Stable Audio Open cannot
|
|
||||||
produce meaningful output. Use manual synthesis instead:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Example: 50ms cursor hover tick
|
|
||||||
import numpy as np
|
|
||||||
import wave
|
|
||||||
|
|
||||||
sr = 44100
|
|
||||||
duration = 0.05 # 50ms
|
|
||||||
t = np.linspace(0, duration, int(sr * duration), endpoint=False)
|
|
||||||
freq = 3200 # Hz
|
|
||||||
signal = np.sin(2 * np.pi * freq * t)
|
|
||||||
envelope = np.exp(-t * 80) # exponential decay
|
|
||||||
audio = (signal * envelope * 32767).astype(np.int16)
|
|
||||||
|
|
||||||
with wave.open("cursor_hover.wav", "w") as f:
|
|
||||||
f.setnchannels(1)
|
|
||||||
f.setsampwidth(2)
|
|
||||||
f.setframerate(sr)
|
|
||||||
f.writeframes(audio.tobytes())
|
|
||||||
```
|
|
||||||
|
|
||||||
## Quality Checklist
|
|
||||||
|
|
||||||
After generating, verify:
|
|
||||||
- Sound matches the sonic family (insert-tech = synthetic/precise, organic = warm/natural)
|
|
||||||
- Frequency range doesn't mask other layers (check docs/assets/audio/palette.md)
|
|
||||||
- Duration matches spec
|
|
||||||
- No unwanted artifacts (clicks, pops, digital noise at start/end)
|
|
||||||
- Loop point is clean (ambient loops only)
|
|
||||||
- Volume sits well relative to other assets (LUFS normalized)
|
|
||||||
- Passes the close-your-eyes test
|
|
||||||
|
|
||||||
## File Placement
|
|
||||||
|
|
||||||
Generated assets go to `client/assets/audio/` with exact filenames from the
|
|
||||||
asset docs:
|
|
||||||
|
|
||||||
```
|
|
||||||
client/assets/audio/
|
|
||||||
amb_station_base.ogg # Ambient bus
|
|
||||||
amb_workplace_layer.ogg # Ambient bus
|
|
||||||
amb_bar_layer.ogg # Ambient bus
|
|
||||||
amb_corridor_layer.ogg # Ambient bus
|
|
||||||
sfx_footstep_metal.ogg # Player Actions bus
|
|
||||||
sfx_footstep_metal_run.ogg # Player Actions bus
|
|
||||||
cursor_hover.ogg # UI Sounds bus
|
|
||||||
implant_open.ogg # UI Sounds bus
|
|
||||||
fog_recognition.ogg # UI Sounds bus
|
|
||||||
weapon_aim.ogg # UI Sounds bus
|
|
||||||
sfx_monologue_chime.ogg # UI Sounds bus
|
|
||||||
sfx_monologue_chime_urgent.ogg # UI Sounds bus
|
|
||||||
```
|
|
||||||
|
|
||||||
AudioManager discovers these by directory scan — filenames must match exactly.
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: commit
|
name: git-commit
|
||||||
description: >
|
description: >
|
||||||
Commit changes with clean, structured messages. Use when the user says
|
Commit changes with clean, structured messages. Use when the user says
|
||||||
"commit", "save my work", "commit changes", or invokes /commit. Enforces
|
"commit", "save my work", "commit changes", or invokes /git-commit. Enforces
|
||||||
conventional commit format, groups changes into logical commits, and maintains
|
conventional commit format, groups changes into logical commits, and maintains
|
||||||
CHANGELOG.md. Never squash unrelated changes into one commit.
|
CHANGELOG.md. Never squash unrelated changes into one commit.
|
||||||
user-invocable: true
|
user-invocable: true
|
||||||
@@ -136,7 +136,5 @@ chore(meta): release v0.1.0
|
|||||||
|
|
||||||
## Staging Rules
|
## Staging Rules
|
||||||
|
|
||||||
- Stage files by name — never use `git add -A` or `git add .`
|
See `.claude/rules/git-safety.md` for staging rules (always-loaded).
|
||||||
- Verify no secrets, saves, or binary blobs are staged
|
These apply to ALL git operations, not just this skill.
|
||||||
- Skip files in `.gitignore`
|
|
||||||
- The `.claude/` directory IS tracked — skills belong in the repo
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: gen-image
|
name: image-gen
|
||||||
description: >
|
description: >
|
||||||
Generate themed visual assets for the Lords of Ash CK3 total conversion mod
|
Generate themed visual assets for the Lords of Ash CK3 total conversion mod
|
||||||
using the generate_image MCP tool (Nano Banana / Gemini 2.5 Flash Image).
|
using the generate_image MCP tool (Nano Banana / Gemini 2.5 Flash Image).
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: push-pr
|
name: pr-push
|
||||||
description: >
|
description: >
|
||||||
Push commits and create or update a pull request. Use when the user says
|
Push commits and create or update a pull request. Use when the user says
|
||||||
"push pr", "push and create pr", "update pr", "create a pr", "open a pr",
|
"push pr", "push and create pr", "update pr", "create a pr", "open a pr",
|
||||||
or invokes /push-pr. NOT triggered by plain "push" (that's just git push).
|
or invokes /pr-push. NOT triggered by plain "push" (that's just git push).
|
||||||
Pushes the current branch, creates a PR if none exists, or confirms the
|
Pushes the current branch, creates a PR if none exists, or confirms the
|
||||||
existing PR was updated. NEVER merges the PR into main — this skill only
|
existing PR was updated. NEVER merges the PR into main — this skill only
|
||||||
pushes to the branch and manages the PR lifecycle.
|
pushes to the branch and manages the PR lifecycle.
|
||||||
@@ -41,7 +41,7 @@ git status
|
|||||||
```
|
```
|
||||||
|
|
||||||
If there are uncommitted changes (staged or unstaged), run the **commit skill**
|
If there are uncommitted changes (staged or unstaged), run the **commit skill**
|
||||||
first. Use the `/commit` skill to group changes into logical commits with
|
first. Use the `/git-commit` skill to group changes into logical commits with
|
||||||
proper conventional commit messages. Wait for commit to complete before
|
proper conventional commit messages. Wait for commit to complete before
|
||||||
proceeding.
|
proceeding.
|
||||||
|
|
||||||
@@ -100,15 +100,11 @@ git diff --stat main...<branch>
|
|||||||
Draft title (`<type>(<scope>): <summary>`, max 70 chars) and description.
|
Draft title (`<type>(<scope>): <summary>`, max 70 chars) and description.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cat > /tmp/pr-body.md << 'EOF'
|
|
||||||
## Summary
|
|
||||||
...
|
|
||||||
EOF
|
|
||||||
tea pr create \
|
tea pr create \
|
||||||
--repo jpmschweitzer/settled-reach \
|
--repo jpmschweitzer/settled-reach \
|
||||||
--login schweitz \
|
--login schweitz \
|
||||||
--title "<title>" \
|
--title "<title>" \
|
||||||
--description "$(cat /tmp/pr-body.md)" \
|
--description "## Summary ..." \
|
||||||
--base main \
|
--base main \
|
||||||
--head <branch>
|
--head <branch>
|
||||||
```
|
```
|
||||||
@@ -127,14 +123,22 @@ Extract ticket IDs from `#NNN` patterns. For each ticket that is
|
|||||||
currently `in_progress`, update it to `review`:
|
currently `in_progress`, update it to `review`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
db/connectors/ticket status <id> review
|
tooling/db/ticket status <id> review
|
||||||
```
|
```
|
||||||
|
|
||||||
Report which tickets were moved to review. Skip tickets that are
|
Report which tickets were moved to review. Skip tickets that are
|
||||||
already `done`, `review`, `cancelled`, or `backlog` (only transition
|
already `done`, `review`, `cancelled`, or `backlog` (only transition
|
||||||
`in_progress` → `review`).
|
`in_progress` → `review`).
|
||||||
|
|
||||||
|
### 9. Next steps
|
||||||
|
|
||||||
|
If a sprint team is active (you are the team lead), do NOT shut down
|
||||||
|
agents after pushing. The team should remain alive for PR review and
|
||||||
|
potential comment fixes.
|
||||||
|
|
||||||
|
Suggest: "PR created/updated. Run `/pr-review` to review before merge."
|
||||||
|
|
||||||
## Arguments
|
## Arguments
|
||||||
|
|
||||||
If the user passes arguments (e.g., `/push-pr "my title"`), use them as the
|
If the user passes arguments (e.g., `/pr-push "my title"`), use them as the
|
||||||
PR title instead of generating one.
|
PR title instead of generating one.
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: review-pr
|
name: pr-review
|
||||||
description: >
|
description: >
|
||||||
Review a branch diff with team-appropriate agents before merge. Use when the
|
Review a branch diff with team-appropriate agents before merge. Use when the
|
||||||
user says "review-pr", "review this PR", "review this branch", or invokes
|
user says "review-pr", "review this PR", "review this branch", or invokes
|
||||||
/review-pr. Spawns reviewers matched to the branch type (code, copy, visual,
|
/pr-review. Spawns reviewers matched to the branch type (code, copy, visual,
|
||||||
audio) in parallel. Reports approve/reject with inline comments.
|
audio) in parallel. Reports approve/reject with inline comments.
|
||||||
user-invocable: true
|
user-invocable: true
|
||||||
allowed-tools: Bash, Read, Grep, Glob, Task
|
allowed-tools: Bash, Read, Grep, Glob, Task
|
||||||
@@ -16,11 +16,22 @@ on the branch type. All reviewers must approve for a clean review.
|
|||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
### 1. Determine the branch
|
### 0. Branch guard — MUST be run by a Claude instance in the `main` worktree
|
||||||
|
|
||||||
If the user provided a branch name as argument, use it. Otherwise use the
|
```bash
|
||||||
current branch (`git branch --show-current`). If on `main`, ask the user
|
git branch --show-current
|
||||||
which branch to review.
|
```
|
||||||
|
|
||||||
|
If the current branch is **not `main`**, stop immediately and tell the user:
|
||||||
|
"PR reviews must be run by a Claude instance in the `main` worktree."
|
||||||
|
Do NOT proceed with the review. Do NOT work around this by reading files
|
||||||
|
from another worktree — the review agent itself must be running in main.
|
||||||
|
Stop and wait for the user to invoke `/pr-review` from main.
|
||||||
|
|
||||||
|
### 1. Determine the branch to review
|
||||||
|
|
||||||
|
If the user provided a branch name as argument, use it. Otherwise list open
|
||||||
|
PRs and ask the user which branch to review.
|
||||||
|
|
||||||
To list open PRs on Gitea:
|
To list open PRs on Gitea:
|
||||||
```bash
|
```bash
|
||||||
@@ -63,23 +74,44 @@ If the diff is empty, report "No changes to review" and stop.
|
|||||||
|
|
||||||
Three-dot diff with pathspec exclusions is unreliable. Instead, either:
|
Three-dot diff with pathspec exclusions is unreliable. Instead, either:
|
||||||
1. Use `git diff main...<branch>` (full diff) and filter in the prompt, or
|
1. Use `git diff main...<branch>` (full diff) and filter in the prompt, or
|
||||||
2. Read source files directly from the branch:
|
2. Read source files directly from the team directory (see below).
|
||||||
`git show origin/<branch>:<path>`
|
|
||||||
|
|
||||||
For large diffs (>1000 lines of source), provide **source files** rather than
|
For large diffs (>1000 lines of source), provide **source files** rather than
|
||||||
raw diff to reviewers — cleaner context, better reviews. Read files with
|
raw diff to reviewers — cleaner context, better reviews.
|
||||||
`git show origin/<branch>:<path>` and include them in the prompt.
|
|
||||||
|
|
||||||
**IMPORTANT — agent tool access:** Not all reviewer agents have Bash access.
|
**IMPORTANT — use team directory paths for ALL agents.** Each team branch
|
||||||
Agents that CAN read from branches themselves: **Hoshe, Tyre, Araminta**.
|
is checked out in its own directory at:
|
||||||
Agents that CANNOT (no Bash tool): **Paula, Miri, Ozzie, Gestalt, Gore, Nigel**.
|
|
||||||
|
|
||||||
For agents without Bash, you MUST read the source files yourself (via
|
```
|
||||||
`git show origin/<branch>:<path>`) and **paste the file contents directly
|
/var/mnt/data/projects/settled-reach/<branch>/
|
||||||
into the agent prompt**. Do not tell these agents to read files — they can't.
|
```
|
||||||
For very large PRs, read the key files (new/heavily modified) and include
|
|
||||||
summaries or excerpts of minor changes. Also read and include the relevant
|
For example, the `copy` team directory is at:
|
||||||
`decisions/*.md` files these agents need for context.
|
```
|
||||||
|
/var/mnt/data/projects/settled-reach/copy/content/dialogue/...
|
||||||
|
```
|
||||||
|
|
||||||
|
**All reviewer agents** (regardless of Bash access) should read source files
|
||||||
|
from the team directory using the Read tool. This is more reliable than
|
||||||
|
`git show origin/<branch>:<path>` because:
|
||||||
|
- All agents have Read access (no Bash dependency)
|
||||||
|
- Files are always the actual branch checkout (no stale cache)
|
||||||
|
- No risk of accidentally reading from main's working directory
|
||||||
|
|
||||||
|
When constructing reviewer prompts, tell agents to read files from the
|
||||||
|
team directory. Example instruction for agents:
|
||||||
|
|
||||||
|
```
|
||||||
|
Read the changed files from the team directory. The branch is checked
|
||||||
|
out at: /var/mnt/data/projects/settled-reach/<branch>/
|
||||||
|
|
||||||
|
For example, to read `content/dialogue/the-terminal/kael-davan.yaml`,
|
||||||
|
use: /var/mnt/data/projects/settled-reach/<branch>/content/dialogue/the-terminal/kael-davan.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Also tell agents to read relevant `decisions/*.md` files from the same
|
||||||
|
directory (they're identical to main, but using the team directory path
|
||||||
|
keeps agents grounded in the correct location).
|
||||||
|
|
||||||
### 4. Spawn reviewers in parallel
|
### 4. Spawn reviewers in parallel
|
||||||
|
|
||||||
@@ -153,23 +185,12 @@ After presenting results to the user, post the review as a PR comment.
|
|||||||
|
|
||||||
Note: `tea pr reject` does not work on your own PRs. Use `tea comment` instead.
|
Note: `tea pr reject` does not work on your own PRs. Use `tea comment` instead.
|
||||||
|
|
||||||
**IMPORTANT — `tea comment` hangs with inline heredocs and multi-line strings.**
|
Post using the `tea-comment` wrapper (handles temp files and cleanup):
|
||||||
Always use a two-step approach: write to a temp file first, then pass via `$(cat)`:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Step 1: Write review to temp file (use Bash heredoc, NOT the Write tool)
|
tooling/tea-comment <PR_NUMBER> "review markdown here"
|
||||||
cat > /tmp/review-<branch>.md << 'EOF'
|
|
||||||
## Review: <branch> -> main
|
|
||||||
...review content...
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Step 2: Post to Gitea (separate Bash call)
|
|
||||||
tea comment --login schweitz --repo jpmschweitzer/settled-reach <PR_NUMBER> "$(cat /tmp/review-<branch>.md)"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
These MUST be separate sequential Bash calls — do not chain them in a single
|
|
||||||
command. The heredoc write must complete before `tea comment` reads the file.
|
|
||||||
|
|
||||||
## 7. Merging approved PRs
|
## 7. Merging approved PRs
|
||||||
|
|
||||||
`tea pr merge` fails (405) when branches have conflicts with main. Merge
|
`tea pr merge` fails (405) when branches have conflicts with main. Merge
|
||||||
@@ -185,6 +206,22 @@ tea pr close --login schweitz --repo jpmschweitzer/settled-reach <PR_NUMBER>
|
|||||||
Gitea does **not** auto-close PRs when you push a local merge — always close
|
Gitea does **not** auto-close PRs when you push a local merge — always close
|
||||||
manually with `tea pr close` after pushing.
|
manually with `tea pr close` after pushing.
|
||||||
|
|
||||||
|
### 8. Post-review team actions
|
||||||
|
|
||||||
|
If a sprint team is active and you are the team lead, handle the
|
||||||
|
review outcome:
|
||||||
|
|
||||||
|
**CHANGES_REQUESTED:**
|
||||||
|
The sprint-start lifecycle (step 9c) handles dispatching review
|
||||||
|
comments to agents. After presenting results, remind the lead:
|
||||||
|
"Review requested changes. Create tasks from the warnings/critical
|
||||||
|
issues and dispatch to idle agents, then re-push and re-review."
|
||||||
|
|
||||||
|
**APPROVED:**
|
||||||
|
The sprint-start lifecycle (step 9c) handles shutdown. After
|
||||||
|
presenting results, remind the lead: "Review approved. Proceed with
|
||||||
|
team shutdown per sprint-start step 9c."
|
||||||
|
|
||||||
## Tips from practice
|
## Tips from practice
|
||||||
|
|
||||||
- **Vendor code**: Explicitly note vendor code in the prompt so reviewers focus
|
- **Vendor code**: Explicitly note vendor code in the prompt so reviewers focus
|
||||||
@@ -2,11 +2,24 @@
|
|||||||
|
|
||||||
Use `model: sonnet` for all reviewers — sufficient for review, saves cost.
|
Use `model: sonnet` for all reviewers — sufficient for review, saves cost.
|
||||||
|
|
||||||
|
**All reviewer agents read from team directories.** Each team branch is
|
||||||
|
checked out in its own directory at:
|
||||||
|
`/var/mnt/data/projects/settled-reach/<branch>/`
|
||||||
|
|
||||||
|
Tell every reviewer agent to read source files from the team directory
|
||||||
|
using the Read tool. Include the directory path and a list of changed
|
||||||
|
files in every prompt. Do NOT rely on `git show` or paste file contents —
|
||||||
|
agents can read directly from the directory.
|
||||||
|
|
||||||
|
Note: cross-directory reading is only permitted for review agents spawned
|
||||||
|
from the `main` team. Team agents must stay within their own directory.
|
||||||
|
|
||||||
## Code reviews (`server`, `client`, `ci`)
|
## Code reviews (`server`, `client`, `ci`)
|
||||||
|
|
||||||
**Hoshe (Code Quality)**
|
**Hoshe (Code Quality)**
|
||||||
- `subagent_type`: `hoshe`, `model`: `sonnet`
|
- `subagent_type`: `hoshe`, `model`: `sonnet`
|
||||||
- Prompt: Include source code and commit log. Ask Hoshe to review for:
|
- Prompt: Provide the team directory path, list of changed files, and commit log.
|
||||||
|
Tell Hoshe to read source files from the team directory, then review for:
|
||||||
- Correctness and bug risks
|
- Correctness and bug risks
|
||||||
- Error handling gaps
|
- Error handling gaps
|
||||||
- Test coverage (are new features tested?)
|
- Test coverage (are new features tested?)
|
||||||
@@ -16,44 +29,43 @@ Use `model: sonnet` for all reviewers — sufficient for review, saves cost.
|
|||||||
|
|
||||||
**Tyre (Architecture)**
|
**Tyre (Architecture)**
|
||||||
- `subagent_type`: `tyre`, `model`: `sonnet`
|
- `subagent_type`: `tyre`, `model`: `sonnet`
|
||||||
- Prompt: Include source code and commit log. Tell Tyre to read the relevant
|
- Prompt: Provide the team directory path, list of changed files, and commit log.
|
||||||
`decisions/*.md` files first, then review for:
|
Tell Tyre to read the relevant `decisions/*.md` files from the team directory
|
||||||
|
first, then review for:
|
||||||
- Architectural consistency with project decisions
|
- Architectural consistency with project decisions
|
||||||
- API/interface design quality
|
- API/interface design quality
|
||||||
- Dependency and coupling concerns
|
- Dependency and coupling concerns
|
||||||
- Scalability implications
|
- Scalability implications
|
||||||
- Whether the change respects non-negotiable baselines (D-010, D-012)
|
- Whether the change respects non-negotiable baselines (D-010, D-012)
|
||||||
- Tyre can read files directly from the branch using `git show origin/<branch>:<path>`
|
|
||||||
|
|
||||||
## Copy reviews (`copy`)
|
## Copy reviews (`copy`)
|
||||||
|
|
||||||
**Hoshe (QA)**
|
**Hoshe (QA)**
|
||||||
- `subagent_type`: `hoshe`, `model`: `sonnet`
|
- `subagent_type`: `hoshe`, `model`: `sonnet`
|
||||||
- Prompt: Include the changed files and commit log. Ask Hoshe to review for:
|
- Prompt: Provide the team directory path, list of changed files, and commit log.
|
||||||
|
Tell Hoshe to read the changed files from the team directory, then review for:
|
||||||
- Formatting consistency (markdown, file naming, frontmatter)
|
- Formatting consistency (markdown, file naming, frontmatter)
|
||||||
- Broken references or links
|
- Broken references or links
|
||||||
- Spelling and grammar
|
- Spelling and grammar
|
||||||
- File organization and structure
|
- File organization and structure
|
||||||
- Missing or orphaned files
|
- Missing or orphaned files
|
||||||
|
|
||||||
**Paula (Narrative Depth)** — NO BASH ACCESS
|
**Paula (Narrative Depth)**
|
||||||
- `subagent_type`: `paula`, `model`: `sonnet`
|
- `subagent_type`: `paula`, `model`: `sonnet`
|
||||||
- Paula cannot read from branches. You must paste file contents and decision
|
- Prompt: Provide the team directory path, list of changed files, commit log, and
|
||||||
files directly into the prompt.
|
list of relevant `decisions/*.md` files to read. Tell Paula to read all
|
||||||
- Prompt: Include full text of changed files, commit log, and relevant
|
files from the team directory using the Read tool, then review for:
|
||||||
`decisions/*.md` content. Ask Paula to review for:
|
|
||||||
- Narrative quality and character voice consistency
|
- Narrative quality and character voice consistency
|
||||||
- Whether dialogue and monologue feel authentic to the characters
|
- Whether dialogue and monologue feel authentic to the characters
|
||||||
- Consequences and stakes — do choices carry weight?
|
- Consequences and stakes — do choices carry weight?
|
||||||
- Political and interpersonal depth
|
- Political and interpersonal depth
|
||||||
- Emotional resonance — does the text make you feel something?
|
- Emotional resonance — does the text make you feel something?
|
||||||
|
|
||||||
**Miri (World Consistency)** — NO BASH ACCESS
|
**Miri (World Consistency)**
|
||||||
- `subagent_type`: `miri`, `model`: `sonnet`
|
- `subagent_type`: `miri`, `model`: `sonnet`
|
||||||
- Miri cannot read from branches. You must paste file contents and decision
|
- Prompt: Provide the team directory path, list of changed files, commit log, and
|
||||||
files directly into the prompt.
|
list of relevant `decisions/*.md` files to read. Tell Miri to read all
|
||||||
- Prompt: Include full text of changed files, commit log, and relevant
|
files from the team directory using the Read tool, then review for:
|
||||||
`decisions/*.md` content. Ask Miri to review for:
|
|
||||||
- Lore accuracy — do facts match established setting?
|
- Lore accuracy — do facts match established setting?
|
||||||
- Internal consistency across files
|
- Internal consistency across files
|
||||||
- IP originality — nothing should read as a copy from another franchise
|
- IP originality — nothing should read as a copy from another franchise
|
||||||
@@ -64,16 +76,18 @@ Use `model: sonnet` for all reviewers — sufficient for review, saves cost.
|
|||||||
|
|
||||||
**Hoshe (QA)**
|
**Hoshe (QA)**
|
||||||
- `subagent_type`: `hoshe`, `model`: `sonnet`
|
- `subagent_type`: `hoshe`, `model`: `sonnet`
|
||||||
- Prompt: Include the changed files and commit log. Ask Hoshe to review for:
|
- Prompt: Provide the team directory path, list of changed files, and commit log.
|
||||||
|
Tell Hoshe to read the changed files from the team directory, then review for:
|
||||||
- File format and naming conventions
|
- File format and naming conventions
|
||||||
- Asset organization and directory structure
|
- Asset organization and directory structure
|
||||||
- Missing or broken references in scene/resource files
|
- Missing or broken references in scene/resource files
|
||||||
- Import settings consistency
|
- Import settings consistency
|
||||||
|
|
||||||
**Araminta (Art Direction)** — HAS BASH ACCESS
|
**Araminta (Art Direction)**
|
||||||
- `subagent_type`: `araminta`, `model`: `sonnet`
|
- `subagent_type`: `araminta`, `model`: `sonnet`
|
||||||
- Prompt: Include the changed files and commit log. Tell Araminta to read
|
- Prompt: Provide the team directory path, list of changed files, and commit log.
|
||||||
the style guide and relevant design docs first, then review for:
|
Tell Araminta to read the style guide and relevant design docs from the
|
||||||
|
worktree first, then review for:
|
||||||
- Visual consistency with the established style guide
|
- Visual consistency with the established style guide
|
||||||
- Color palette adherence
|
- Color palette adherence
|
||||||
- UI pattern consistency (diegetic-first, clarity over beauty)
|
- UI pattern consistency (diegetic-first, clarity over beauty)
|
||||||
@@ -84,17 +98,17 @@ Use `model: sonnet` for all reviewers — sufficient for review, saves cost.
|
|||||||
|
|
||||||
**Hoshe (QA)**
|
**Hoshe (QA)**
|
||||||
- `subagent_type`: `hoshe`, `model`: `sonnet`
|
- `subagent_type`: `hoshe`, `model`: `sonnet`
|
||||||
- Prompt: Include the changed files and commit log. Ask Hoshe to review for:
|
- Prompt: Provide the team directory path, list of changed files, and commit log.
|
||||||
|
Tell Hoshe to read the changed files from the team directory, then review for:
|
||||||
- File format and naming conventions
|
- File format and naming conventions
|
||||||
- Audio asset organization and directory structure
|
- Audio asset organization and directory structure
|
||||||
- Missing or broken references
|
- Missing or broken references
|
||||||
- Import/bus configuration consistency
|
- Import/bus configuration consistency
|
||||||
|
|
||||||
**Ozzie (Player Experience)** — NO BASH ACCESS
|
**Ozzie (Player Experience)**
|
||||||
- `subagent_type`: `ozzie`, `model`: `sonnet`
|
- `subagent_type`: `ozzie`, `model`: `sonnet`
|
||||||
- Ozzie cannot read from branches. You must paste file contents directly
|
- Prompt: Provide the team directory path, list of changed files, and commit log.
|
||||||
into the prompt.
|
Tell Ozzie to read all files from the team directory using the Read tool, then
|
||||||
- Prompt: Include full text of changed files and commit log. Ask Ozzie to
|
|
||||||
review for:
|
review for:
|
||||||
- Emotional impact — does the audio enhance the moment?
|
- Emotional impact — does the audio enhance the moment?
|
||||||
- Atmosphere and tone — does it feel like the Commonwealth?
|
- Atmosphere and tone — does it feel like the Commonwealth?
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: create-skill
|
name: skill-create
|
||||||
description: >
|
description: >
|
||||||
Guidance for creating effective Claude Code skills (.skill packages).
|
Guidance for creating effective Claude Code skills (.skill packages).
|
||||||
Use when the user wants to create, build, design, or iterate on a skill —
|
Use when the user wants to create, build, design, or iterate on a skill —
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: plan-sprint
|
name: sprint-plan
|
||||||
description: >
|
description: >
|
||||||
Plan the next sprint and generate team briefing files. Use when the user says
|
Plan the next sprint and generate team briefing files. Use when the user says
|
||||||
"plan sprint", "prep sprint briefing", "plan next sprint", "sprint planning",
|
"plan sprint", "prep sprint briefing", "plan next sprint", "sprint planning",
|
||||||
or invokes /plan-sprint. Gathers current sprint status, scans the backlog,
|
or invokes /sprint-plan. Gathers current sprint status, scans the backlog,
|
||||||
proposes ticket selection, and writes briefing files per team to
|
proposes ticket selection, and writes briefing files per team to
|
||||||
docs/sprints/sprint-N/.
|
docs/sprints/sprint-N/.
|
||||||
user-invocable: true
|
user-invocable: true
|
||||||
@@ -19,8 +19,8 @@ When this skill is invoked, spawn SI using the Task tool:
|
|||||||
```
|
```
|
||||||
Task(
|
Task(
|
||||||
subagent_type: "si",
|
subagent_type: "si",
|
||||||
prompt: "Run /plan-sprint for Sprint N. Read the skill at
|
prompt: "Run /sprint-plan for Sprint N. Read the skill at
|
||||||
.claude/skills/plan-sprint/SKILL.md for the full workflow,
|
.claude/skills/sprint-plan/SKILL.md for the full workflow,
|
||||||
then execute it. Use the arguments provided: {args}",
|
then execute it. Use the arguments provided: {args}",
|
||||||
description: "Plan sprint N"
|
description: "Plan sprint N"
|
||||||
)
|
)
|
||||||
@@ -47,10 +47,40 @@ project state. Only generate briefings for teams that have tickets in the sprint
|
|||||||
| `audio` | `audio` | Inigo (sound design) | Soundscapes, ambient layers, diegetic cues, audio propagation |
|
| `audio` | `audio` | Inigo (sound design) | Soundscapes, ambient layers, diegetic cues, audio propagation |
|
||||||
| `visual` | `visual` | Araminta (art direction) | Art assets, sprites, visual consistency, style guides |
|
| `visual` | `visual` | Araminta (art direction) | Art assets, sprites, visual consistency, style guides |
|
||||||
| `ci` | `ci` | Justine (build/deploy) | Build pipelines, CI/CD, tooling, packaging |
|
| `ci` | `ci` | Justine (build/deploy) | Build pipelines, CI/CD, tooling, packaging |
|
||||||
|
| `planning` | `planning` | Purpose-assembled (see below) | Design discussions, decision resolution, workshop-style tickets |
|
||||||
|
|
||||||
When writing briefings, name the assigned agents in the **Agents** line of each
|
When writing briefings, name the assigned agents in the **Agents** line of each
|
||||||
file so the team knows who to spawn.
|
file so the team knows who to spawn.
|
||||||
|
|
||||||
|
### Planning Team Tickets
|
||||||
|
|
||||||
|
Some tickets need **design discussion** before implementation can begin — tagged
|
||||||
|
"NEEDS DESIGN DISCUSSION" or blocking multiple downstream tickets with open
|
||||||
|
questions. These run on the `planning` branch as structured discussions with
|
||||||
|
the user and a purpose-assembled agent panel.
|
||||||
|
|
||||||
|
**When to create a planning ticket:**
|
||||||
|
- Ticket description says "NEEDS DESIGN" or "NEEDS DESIGN DISCUSSION"
|
||||||
|
- Ticket blocks 2+ downstream tickets across different teams
|
||||||
|
- Open Q-NNN items that block sprint candidates
|
||||||
|
- Architectural decisions that need multi-domain input before implementation
|
||||||
|
|
||||||
|
**Planning briefing format** (differs from implementation briefings):
|
||||||
|
- **Agents line**: List agents by domain relevance, not fixed team roster.
|
||||||
|
Pick from: Gestalt (systems), Miri (worldbuilding), Araminta (visual/spatial),
|
||||||
|
Tyre (technical), Paula (narrative), Ozzie (player experience), Gore (themes),
|
||||||
|
Nigel (replayability). Typically 4-6 domain agents, plus Qatux (documenter —
|
||||||
|
records decisions, updates domain files) and SI (project manager — creates
|
||||||
|
follow-up tickets, updates sprint assignments).
|
||||||
|
- **Discussion rounds**: Structure the conversation into 2-3 rounds
|
||||||
|
(inventory → proposals → convergence)
|
||||||
|
- **Context section**: List all existing design docs, decisions, and related
|
||||||
|
tickets that participants must read before the discussion
|
||||||
|
- **Output specification**: What the discussion must produce — typically a
|
||||||
|
D-record in `decisions/`, possibly a design doc in `docs/design/`
|
||||||
|
- **Decision questions**: Specific questions the discussion must answer,
|
||||||
|
not open-ended exploration
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
### 1. Run sprint prepare
|
### 1. Run sprint prepare
|
||||||
@@ -58,7 +88,7 @@ file so the team knows who to spawn.
|
|||||||
Get carry-overs, backlog candidates, and decision gaps in one shot:
|
Get carry-overs, backlog candidates, and decision gaps in one shot:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
db/connectors/sprint prepare
|
tooling/db/sprint prepare
|
||||||
```
|
```
|
||||||
|
|
||||||
This auto-detects the next sprint number (max ID + 1), creates the sprint
|
This auto-detects the next sprint number (max ID + 1), creates the sprint
|
||||||
@@ -73,10 +103,10 @@ record in `planning` status if needed, and outputs:
|
|||||||
For critical epics, check their children for granular candidates:
|
For critical epics, check their children for granular candidates:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
db/connectors/ticket children <epic_id>
|
tooling/db/ticket children <epic_id>
|
||||||
```
|
```
|
||||||
|
|
||||||
Use `db/connectors/ticket show --brief <id> [<id>...]` to quickly scan multiple tickets.
|
Use `tooling/db/ticket show --brief <id> [<id>...]` to quickly scan multiple tickets.
|
||||||
|
|
||||||
### 3. Read existing code state
|
### 3. Read existing code state
|
||||||
|
|
||||||
@@ -124,13 +154,12 @@ Create `docs/sprints/sprint-N/` and write one file per team.
|
|||||||
Read the template at `references/briefing-template.md` in this skill directory
|
Read the template at `references/briefing-template.md` in this skill directory
|
||||||
for the exact file structure.
|
for the exact file structure.
|
||||||
|
|
||||||
**IMPORTANT — worktree-relative paths:** This project uses git worktrees.
|
**IMPORTANT — relative paths only:** Each team works in its own directory
|
||||||
Each team branch is checked out in its own worktree, and each worktree
|
containing the full repo (`server/`, `client/`, `docs/`, etc.). All file
|
||||||
contains the full repo (`server/`, `client/`, `docs/`, etc.). All file
|
paths in briefings must be relative to the working directory. Example:
|
||||||
paths in briefings must be relative to the worktree/git root. Example:
|
|
||||||
`server/src/bridge/types.rs`, not `/absolute/path/to/server/src/...` or
|
`server/src/bridge/types.rs`, not `/absolute/path/to/server/src/...` or
|
||||||
paths that navigate outside the git root (`../sibling-worktree/...`).
|
paths that navigate outside (`../sibling-dir/...`).
|
||||||
Agents must stay within the git root they are running in.
|
Agents must stay within their team's working directory.
|
||||||
|
|
||||||
Key requirements per file:
|
Key requirements per file:
|
||||||
- **server.md**: Carry-overs, new tickets, dependency chain, key decisions, notes
|
- **server.md**: Carry-overs, new tickets, dependency chain, key decisions, notes
|
||||||
@@ -154,16 +183,29 @@ Update it with the theme and goal, then assign tickets:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Update the sprint with theme and goal
|
# Update the sprint with theme and goal
|
||||||
db/connectors/sqlite-exec "UPDATE sprints SET name='Sprint N: Theme', goal='goal' WHERE id=N"
|
tooling/db/sqlite-exec "UPDATE sprints SET name='Sprint N: Theme', goal='goal' WHERE id=N"
|
||||||
|
|
||||||
# Assign tickets
|
# Assign tickets
|
||||||
db/connectors/ticket sprint assign <ticket_id> <sprint_id>
|
tooling/db/ticket sprint assign <ticket_id> <sprint_id>
|
||||||
```
|
```
|
||||||
|
|
||||||
The sprint stays in `planning` status until explicitly activated via
|
The sprint stays in `planning` status until explicitly activated via
|
||||||
`db/connectors/sprint start`. This prevents starting an unplanned sprint.
|
`tooling/db/sprint start`. This prevents starting an unplanned sprint.
|
||||||
|
|
||||||
### 8. Present summary
|
### 8. Commit and push
|
||||||
|
|
||||||
|
Stage the briefing files and any other changes (db backup, closed tickets),
|
||||||
|
then commit and push so worktree branches can pull the planning artifacts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add docs/sprints/sprint-N/
|
||||||
|
make db-backup
|
||||||
|
git add docs/backups/settledreach.db.backup
|
||||||
|
git commit -m "chore(meta): plan Sprint N: Theme"
|
||||||
|
git push
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9. Present summary
|
||||||
|
|
||||||
Output:
|
Output:
|
||||||
- Sprint number, theme, and goal
|
- Sprint number, theme, and goal
|
||||||
@@ -171,3 +213,4 @@ Output:
|
|||||||
- Carry-over count
|
- Carry-over count
|
||||||
- Open questions that need early resolution
|
- Open questions that need early resolution
|
||||||
- Files written
|
- Files written
|
||||||
|
- Commit pushed to main
|
||||||
@@ -26,7 +26,7 @@ Each team gets one briefing file at `docs/sprints/sprint-N/<team>.md`.
|
|||||||
|---|-------|------------|
|
|---|-------|------------|
|
||||||
| #ID | Title | #dependency or — |
|
| #ID | Title | #dependency or — |
|
||||||
|
|
||||||
Use `db/connectors/ticket show <id>` for full details.
|
Use `tooling/db/ticket show <id>` for full details.
|
||||||
|
|
||||||
## Key Decisions
|
## Key Decisions
|
||||||
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: start-sprint
|
name: sprint-start
|
||||||
description: >
|
description: >
|
||||||
Manage the sprint lifecycle from main, or start sprint work on a team
|
Manage the sprint lifecycle from main, or start sprint work on a team
|
||||||
branch. Use when the user says "start sprint", "start working on the
|
branch. Use when the user says "start sprint", "start working on the
|
||||||
server/client/copy", "begin sprint", or invokes /start-sprint. On main:
|
server/client/copy", "begin sprint", or invokes /sprint-start. On main:
|
||||||
assesses sprint state and does the next right thing (close, activate, or
|
assesses sprint state and does the next right thing (close, activate, or
|
||||||
guide). On a team branch: merges main, loads the briefing, presents the
|
guide). On a team branch: merges main, loads the briefing, presents the
|
||||||
work plan.
|
work plan.
|
||||||
@@ -34,23 +34,54 @@ the team branch workflow (steps 2–8).
|
|||||||
|
|
||||||
## Main branch workflow (sprint lifecycle management)
|
## Main branch workflow (sprint lifecycle management)
|
||||||
|
|
||||||
When `/start-sprint` is run on `main`, assess the current sprint state
|
When `/sprint-start` is run on `main`, assess the current sprint state
|
||||||
and do the next right thing. Query the database to determine the state:
|
and do the next right thing. Query the database to determine the state:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
db/connectors/sqlite-query "SELECT id, name, status FROM sprints ORDER BY id DESC LIMIT 3"
|
tooling/db/sqlite-query "SELECT id, name, status FROM sprints ORDER BY id DESC LIMIT 3"
|
||||||
```
|
```
|
||||||
|
|
||||||
Then follow the **first matching case**:
|
Then follow the **first matching case**:
|
||||||
|
|
||||||
### Case A: An active sprint exists
|
### Case A: An active sprint exists
|
||||||
|
|
||||||
The active sprint needs to be closed before moving on.
|
First, check whether the sprint's work is actually done:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tooling/db/sprint status
|
||||||
|
```
|
||||||
|
|
||||||
|
This shows ticket counts by status (done, in_progress, backlog).
|
||||||
|
|
||||||
|
Also check for open PRs that may contain completed work waiting for
|
||||||
|
review or merge:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tea pr list --login schweitz --repo jpmschweitzer/settled-reach --state open --output simple
|
||||||
|
```
|
||||||
|
|
||||||
|
Report the full picture to the user:
|
||||||
|
- Ticket progress (done / in_progress / backlog counts)
|
||||||
|
- Open PRs (if any — these represent work that's done but not merged)
|
||||||
|
|
||||||
|
**If tickets remain unfinished** (in_progress or backlog) **or open PRs
|
||||||
|
exist**, do NOT close the sprint. Instead, report the current progress
|
||||||
|
and ask the user what they want to do:
|
||||||
|
|
||||||
|
- **Continue working** — switch to a team branch and run `/sprint-start`
|
||||||
|
there to resume work
|
||||||
|
- **Review & merge PRs first** — (only if open PRs exist) merge pending
|
||||||
|
work before deciding whether to close
|
||||||
|
- **Close anyway** — proceed with the close workflow below (carries over
|
||||||
|
unfinished tickets)
|
||||||
|
|
||||||
|
Use `AskUserQuestion` to confirm. Do not proceed to A1 unless the user
|
||||||
|
explicitly chooses to close.
|
||||||
|
|
||||||
#### A1. Close the active sprint
|
#### A1. Close the active sprint
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
db/connectors/sprint stop
|
tooling/db/sprint stop
|
||||||
```
|
```
|
||||||
|
|
||||||
This marks the active sprint as completed and lists carry-over candidates.
|
This marks the active sprint as completed and lists carry-over candidates.
|
||||||
@@ -97,7 +128,7 @@ git push && git push --tags
|
|||||||
|
|
||||||
After closing, re-query the database. If a sprint in `planning` status
|
After closing, re-query the database. If a sprint in `planning` status
|
||||||
exists, continue to **Case B**. Otherwise, report the close and suggest
|
exists, continue to **Case B**. Otherwise, report the close and suggest
|
||||||
running `/plan-sprint`.
|
running `/sprint-plan`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -111,28 +142,28 @@ A sprint is ready to activate. Verify it looks complete:
|
|||||||
```
|
```
|
||||||
2. Check the ticket count:
|
2. Check the ticket count:
|
||||||
```bash
|
```bash
|
||||||
db/connectors/sprint status --sprint N
|
tooling/db/sprint status --sprint N
|
||||||
```
|
```
|
||||||
|
|
||||||
If briefings are missing or the sprint has 0 tickets, report the gap
|
If briefings are missing or the sprint has 0 tickets, report the gap
|
||||||
and suggest running `/plan-sprint` to complete planning.
|
and suggest running `/sprint-plan` to complete planning.
|
||||||
|
|
||||||
If everything looks ready, activate the sprint:
|
If everything looks ready, activate the sprint:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
db/connectors/sprint start
|
tooling/db/sprint start
|
||||||
```
|
```
|
||||||
|
|
||||||
Then report:
|
Then report:
|
||||||
- Sprint activated (name, ticket count per team)
|
- Sprint activated (name, ticket count per team)
|
||||||
- Remind the user to switch to a team branch and run `/start-sprint`
|
- Remind the user to switch to a team branch and run `/sprint-start`
|
||||||
there (or `cd` into the relevant worktree)
|
there (or `cd` into the relevant worktree)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Case C: No active sprint and no planned sprint
|
### Case C: No active sprint and no planned sprint
|
||||||
|
|
||||||
Nothing is ready. Report the state and suggest running `/plan-sprint`
|
Nothing is ready. Report the state and suggest running `/sprint-plan`
|
||||||
to plan the next sprint.
|
to plan the next sprint.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -153,7 +184,7 @@ If the merge has conflicts, report them and stop — do not force-resolve.
|
|||||||
Run the sprint CLI to get the full context dump in one shot:
|
Run the sprint CLI to get the full context dump in one shot:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
db/connectors/sprint start-work
|
tooling/db/sprint start-work
|
||||||
```
|
```
|
||||||
|
|
||||||
This auto-detects the active sprint and current team from the branch.
|
This auto-detects the active sprint and current team from the branch.
|
||||||
@@ -167,13 +198,13 @@ If no active sprint is found, report that and stop.
|
|||||||
Read the briefing file(s) listed in the `start-work` output
|
Read the briefing file(s) listed in the `start-work` output
|
||||||
(e.g. `docs/sprints/sprint-6/server.md` and `joint.md`).
|
(e.g. `docs/sprints/sprint-6/server.md` and `joint.md`).
|
||||||
If no matching briefing exists for the team, suggest running
|
If no matching briefing exists for the team, suggest running
|
||||||
`/plan-sprint` to generate one.
|
`/sprint-plan` to generate one.
|
||||||
|
|
||||||
### 5. Load ticket details
|
### 5. Load ticket details
|
||||||
|
|
||||||
For tickets that need more detail than the `start-work` summary provides:
|
For tickets that need more detail than the `start-work` summary provides:
|
||||||
```bash
|
```bash
|
||||||
db/connectors/ticket show <id>
|
tooling/db/ticket show <id>
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6. Read key decisions
|
### 6. Read key decisions
|
||||||
@@ -187,7 +218,7 @@ Mark all actionable (unblocked, non-done) tickets in the sprint as
|
|||||||
`in_progress`:
|
`in_progress`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
db/connectors/ticket status <id> in_progress
|
tooling/db/ticket status <id> in_progress
|
||||||
```
|
```
|
||||||
|
|
||||||
Then output a summary:
|
Then output a summary:
|
||||||
@@ -257,15 +288,70 @@ Task(
|
|||||||
prompt: "You are on the {team} team for Sprint {N}.
|
prompt: "You are on the {team} team for Sprint {N}.
|
||||||
Branch: `{team}`
|
Branch: `{team}`
|
||||||
|
|
||||||
|
RULES (NON-NEGOTIABLE):
|
||||||
|
|
||||||
|
0. TEAM BOUNDARY: Your team is `{team}` ($WORKTREE_TEAM). Stay
|
||||||
|
within the current working directory. Do NOT navigate to
|
||||||
|
parent or sibling directories. Do NOT follow .git pointers
|
||||||
|
to other directories.
|
||||||
|
|
||||||
|
1. GIT: Do NOT run any git commands (commit, push, pull, merge,
|
||||||
|
checkout, branch, stash, tag, etc.). All git operations are
|
||||||
|
handled by the team lead. No exceptions.
|
||||||
|
|
||||||
|
2. DB SCRIPTS: When calling ticket/sprint/sqlite scripts, use
|
||||||
|
the exact command with no wrappers or chaining. Examples:
|
||||||
|
tooling/db/ticket show 528
|
||||||
|
tooling/db/ticket list --sprint {N}
|
||||||
|
Do NOT prepend python3, do NOT chain with && or ;, do NOT
|
||||||
|
add cleanup commands. Just the bare command.
|
||||||
|
|
||||||
|
3. READ BEFORE WRITE: Before modifying ANY file, Read it first.
|
||||||
|
Before creating a new file, Glob for similar files to learn
|
||||||
|
the existing patterns (naming, structure, imports). Follow
|
||||||
|
the conventions you find — do not invent new ones.
|
||||||
|
|
||||||
|
4. VERIFY AFTER WRITE: After implementing a change, grep for
|
||||||
|
all references to functions/properties/classes you modified
|
||||||
|
or removed. If you renamed, moved, or deleted something,
|
||||||
|
update EVERY call site. Missing a call site breaks tests
|
||||||
|
and blocks the team.
|
||||||
|
|
||||||
|
5. NO PARTIAL WORK: Do not mark a task completed unless ALL
|
||||||
|
parts of the ticket are implemented. If the ticket says
|
||||||
|
'deliver A, B, and C', all three must exist and work. If
|
||||||
|
you cannot complete part of a task, message the team lead
|
||||||
|
explaining what is blocked and what remains — do NOT mark
|
||||||
|
it completed.
|
||||||
|
|
||||||
|
6. MESSAGE WHEN BLOCKED: If you hit a problem you cannot solve
|
||||||
|
in 3 attempts, stop and message the team lead immediately.
|
||||||
|
Do not silently skip work or leave stubs. Do not move to
|
||||||
|
the next task while the current one is incomplete.
|
||||||
|
|
||||||
|
7. BACKWARD COMPATIBILITY: When extracting, moving, or
|
||||||
|
refactoring code, ensure all existing consumers still work.
|
||||||
|
Add proxy methods/properties if needed. Grep for the old
|
||||||
|
name to find every call site.
|
||||||
|
|
||||||
|
WORKFLOW:
|
||||||
|
|
||||||
1. Read the sprint briefing: docs/sprints/sprint-{N}/{team}.md
|
1. Read the sprint briefing: docs/sprints/sprint-{N}/{team}.md
|
||||||
2. Read the decision files referenced in the briefing.
|
2. Read the decision files referenced in the briefing.
|
||||||
3. Check TaskList for available work.
|
3. Check TaskList for available work.
|
||||||
4. Claim an unblocked task (TaskUpdate with owner: your name),
|
4. Claim an unblocked task (TaskUpdate with owner: your name),
|
||||||
mark it in_progress, and implement it.
|
mark it in_progress, and implement it.
|
||||||
5. When done, mark the task completed and check TaskList for
|
5. Before marking done, verify:
|
||||||
the next available task.
|
- All deliverables from the ticket exist (not just some)
|
||||||
|
- No broken references (grep for changed names/signatures)
|
||||||
|
- New files follow existing naming and directory conventions
|
||||||
|
- Modified files still parse (no syntax errors)
|
||||||
|
6. Mark the task completed and check TaskList for the next
|
||||||
|
available task.
|
||||||
|
7. If no tasks remain, message the team lead. Do NOT shut down
|
||||||
|
on your own.
|
||||||
|
|
||||||
Use `db/connectors/ticket show <id>` for full ticket specs.",
|
Use `tooling/db/ticket show <id>` for full ticket specs.",
|
||||||
description: "Sprint {N} {team}: {name}",
|
description: "Sprint {N} {team}: {name}",
|
||||||
run_in_background: true
|
run_in_background: true
|
||||||
)
|
)
|
||||||
@@ -283,3 +369,57 @@ Output to the user:
|
|||||||
You are now the team lead. Agents work autonomously — monitor via
|
You are now the team lead. Agents work autonomously — monitor via
|
||||||
`TaskList`, communicate via `SendMessage`, and handle blockers as
|
`TaskList`, communicate via `SendMessage`, and handle blockers as
|
||||||
they arise.
|
they arise.
|
||||||
|
|
||||||
|
**When all tasks complete:** Do NOT shut down agents. The team stays
|
||||||
|
alive through the PR review cycle. Follow step 9 (post-work lifecycle).
|
||||||
|
|
||||||
|
### 9. Post-work lifecycle
|
||||||
|
|
||||||
|
When all tasks are complete (TaskList shows all completed):
|
||||||
|
|
||||||
|
#### 9a. Commit and push
|
||||||
|
|
||||||
|
Run `/git-commit` to commit all changes, then `/pr-push` to create or
|
||||||
|
update the PR. Do NOT shut down agents — the team stays alive for review.
|
||||||
|
|
||||||
|
#### 9b. Review
|
||||||
|
|
||||||
|
Run `/pr-review` to spawn temporary reviewers. Wait for results.
|
||||||
|
|
||||||
|
#### 9c. Handle review outcome
|
||||||
|
|
||||||
|
**If CHANGES_REQUESTED:**
|
||||||
|
|
||||||
|
1. Parse the review comment table (from the Gitea PR comment or the
|
||||||
|
review output). Extract each warning/critical issue with:
|
||||||
|
- File path and approximate line
|
||||||
|
- Severity (critical / warning / suggestion)
|
||||||
|
- Description
|
||||||
|
|
||||||
|
2. Create a task per warning/critical issue:
|
||||||
|
```
|
||||||
|
TaskCreate(
|
||||||
|
subject: "Review: {short description}",
|
||||||
|
description: "{full issue description from review table, including
|
||||||
|
file path, severity, and reviewer name}",
|
||||||
|
activeForm: "Fixing review comment: {short description}"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
Skip suggestion-severity items unless they are trivial (1-line fixes).
|
||||||
|
|
||||||
|
3. Dispatch to idle agents: send each a message via SendMessage telling
|
||||||
|
them to check TaskList for new review-fix tasks. Agents claim and
|
||||||
|
work tasks as usual.
|
||||||
|
|
||||||
|
4. After all review-fix tasks are complete, re-run `/git-commit` then
|
||||||
|
`/pr-push` to update the PR. Then re-run `/pr-review`.
|
||||||
|
|
||||||
|
5. Repeat this loop until review returns APPROVED.
|
||||||
|
|
||||||
|
**If APPROVED:**
|
||||||
|
|
||||||
|
1. Send `shutdown_request` to all sprint agents.
|
||||||
|
2. Wait for all `shutdown_response` confirmations.
|
||||||
|
3. Call `TeamDelete` to clean up.
|
||||||
|
4. Report: "Sprint {N} {team} complete. PR #{X} approved and ready for
|
||||||
|
merge on main."
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
---
|
||||||
|
name: sprint-status
|
||||||
|
description: >
|
||||||
|
Sprint health check and cleanup sweep. Lists all tickets in the active
|
||||||
|
sprint grouped by status, detects bookkeeping issues (stale tickets,
|
||||||
|
orphan PRs, done-but-open PRs, unassigned work), and shows open work
|
||||||
|
by team. Use when checking sprint progress, before sprint close, or
|
||||||
|
when housekeeping feels off. Triggers on "sprint status", "cleanup
|
||||||
|
sweep", "what's open", "sprint health".
|
||||||
|
user-invocable: true
|
||||||
|
allowed-tools: Task, Read, Grep, Glob
|
||||||
|
---
|
||||||
|
|
||||||
|
# Sprint Status
|
||||||
|
|
||||||
|
**Delegate this entire skill to a subagent** (general-purpose, model: haiku).
|
||||||
|
|
||||||
|
When this skill is invoked, spawn a subagent using the Task tool:
|
||||||
|
|
||||||
|
```
|
||||||
|
Task(
|
||||||
|
subagent_type: "general-purpose",
|
||||||
|
model: "haiku",
|
||||||
|
prompt: "Run /sprint-status. Read the skill at
|
||||||
|
.claude/skills/sprint-status/SKILL.md for the full workflow
|
||||||
|
(below the --- separator), then execute it.",
|
||||||
|
description: "Sprint status report"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Present the subagent's output to the user verbatim. Do NOT run the
|
||||||
|
workflow yourself.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
The remainder of this file is the subagent's reference for executing
|
||||||
|
the workflow.
|
||||||
|
|
||||||
|
## Step 1 — Gather data
|
||||||
|
|
||||||
|
Run these two commands in parallel:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tooling/db/sprint sweep
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tea pr list --login schweitz --repo jpmschweitzer/settled-reach --state open --output simple
|
||||||
|
```
|
||||||
|
|
||||||
|
The `sweep` command returns JSON with:
|
||||||
|
- `sprint` — id, name, goal
|
||||||
|
- `progress` — total, done, pct
|
||||||
|
- `by_status` — tickets grouped into done, review, in_progress, blocked, backlog
|
||||||
|
- `by_team` — per-team counts
|
||||||
|
- `issues` — bookkeeping problems with suggested fix commands
|
||||||
|
|
||||||
|
The `tea pr list` returns open PRs as `#N title` lines.
|
||||||
|
|
||||||
|
## Step 2 — Cross-reference PRs with tickets
|
||||||
|
|
||||||
|
Parse PR head branches from the `tea pr list` output. Known team branches:
|
||||||
|
`server`, `client`, `copy`, `audio`, `visual`, `ci`.
|
||||||
|
|
||||||
|
Detect additional issues:
|
||||||
|
|
||||||
|
- **done_team_open_pr**: A team's tickets are all done but an open PR
|
||||||
|
still exists for that team branch.
|
||||||
|
- **orphan_pr**: An open PR exists on a branch that has no tickets in
|
||||||
|
the active sprint.
|
||||||
|
|
||||||
|
Add these to the issues list from step 1.
|
||||||
|
|
||||||
|
## Step 3 — Format output
|
||||||
|
|
||||||
|
Read `references/output-template.md` for the exact format spec.
|
||||||
|
|
||||||
|
Render the report using data from steps 1-2. Key rules:
|
||||||
|
- Sections ordered: Completed, In Review, In Progress, Blocked, Backlog
|
||||||
|
- Sort tickets within sections by team then ticket ID
|
||||||
|
- Empty sections: show header with "(0)" and "(none)" — no empty table
|
||||||
|
- Bookkeeping Issues: two-column table (Issue, Fix)
|
||||||
|
- Open Work by Team: summary table at the bottom
|
||||||
|
- Issue type labels: `stale_backlog` → "Stale backlog",
|
||||||
|
`unassigned_in_progress` → "Unassigned in_progress",
|
||||||
|
`assigned_but_done` → "Assigned but done",
|
||||||
|
`done_team_open_pr` → "Done team with open PR",
|
||||||
|
`orphan_pr` → "Orphan PR"
|
||||||
|
|
||||||
|
## Step 4 — Suggest actions
|
||||||
|
|
||||||
|
After the formatted report, if there are bookkeeping issues, add a
|
||||||
|
"Suggested fixes" section with the fix command for each issue. Group
|
||||||
|
by issue type for readability.
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Sprint Status Output Template
|
||||||
|
|
||||||
|
## Sprint {N}: {Theme} — Status Report
|
||||||
|
|
||||||
|
**Goal:** {goal}
|
||||||
|
**Status:** {status} | {done}/{total} tickets ({pct}%)
|
||||||
|
**Open PRs:** {count} ({branches})
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Completed ({count})
|
||||||
|
|
||||||
|
| # | Team | Title | Assigned |
|
||||||
|
|---|------|-------|----------|
|
||||||
|
| #{id} | {team} | {title} | {assigned} |
|
||||||
|
|
||||||
|
### In Review ({count})
|
||||||
|
|
||||||
|
| # | Team | Title | PR |
|
||||||
|
|---|------|-------|----|
|
||||||
|
| #{id} | {team} | {title} | #{pr} |
|
||||||
|
|
||||||
|
### In Progress ({count})
|
||||||
|
|
||||||
|
| # | Team | Title | Assigned | Note |
|
||||||
|
|---|------|-------|----------|------|
|
||||||
|
| #{id} | {team} | {title} | {assigned} | |
|
||||||
|
|
||||||
|
### Blocked ({count})
|
||||||
|
|
||||||
|
| # | Team | Title | Blocked by |
|
||||||
|
|---|------|-------|------------|
|
||||||
|
| #{id} | {team} | {title} | #{ids} |
|
||||||
|
|
||||||
|
### Backlog ({count})
|
||||||
|
|
||||||
|
| # | Team | Title | Note |
|
||||||
|
|---|------|-------|----|
|
||||||
|
| #{id} | {team} | {title} | not started |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Bookkeeping Issues
|
||||||
|
|
||||||
|
| Issue | Fix |
|
||||||
|
|-------|-----|
|
||||||
|
| {type}: {detail} | `{command}` |
|
||||||
|
|
||||||
|
### Open Work by Team
|
||||||
|
|
||||||
|
| Team | Backlog | In Progress | Review | Blocked | Done |
|
||||||
|
|------|---------|-------------|--------|---------|------|
|
||||||
|
| {team} | {n} | {n} | {n} | {n} | {n} |
|
||||||
|
| **Total** | **{n}** | **{n}** | **{n}** | **{n}** | **{n}** |
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
---
|
---
|
||||||
name: render-sprite
|
name: sprite-gen
|
||||||
description: >
|
description: >
|
||||||
Render a 3D model to 2D sprites via the Godot render pipeline. Produces
|
Render a 3D model to 2D sprites via the Godot render pipeline. Produces
|
||||||
sprites at 3 resolutions (1024, 256, 64) from 4 cardinal directions (north,
|
sprites at 3 resolutions (1024, 256, 64) from 4 cardinal directions (north,
|
||||||
east, south, west) with outline applied at working resolution. Use when the
|
east, south, west) with outline applied at working resolution. Use when the
|
||||||
user says "render sprite", "render model", "run the render pipeline",
|
user says "render sprite", "render model", "run the render pipeline",
|
||||||
"test the pipeline", "/render-sprite", or asks to render a specific model
|
"test the pipeline", "/sprite-gen", or asks to render a specific model
|
||||||
(e.g., "render wall_structural"). Output: 12 PNG files in renderer/output/.
|
(e.g., "render wall_structural"). Output: 12 PNG files in renderer/output/.
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ description: >
|
|||||||
Run the render script with the model name:
|
Run the render script with the model name:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
.claude/skills/render-sprite/scripts/render.sh <model_name>
|
.claude/skills/sprite-gen/scripts/render.sh <model_name>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Available Models
|
### Available Models
|
||||||
@@ -52,4 +52,4 @@ ls renderer/models/*.tscn | xargs -I{} basename {} .tscn
|
|||||||
|
|
||||||
1. Create model scene at `renderer/models/<name>.tscn`
|
1. Create model scene at `renderer/models/<name>.tscn`
|
||||||
2. Apply texture from `renderer/textures/` via StandardMaterial3D
|
2. Apply texture from `renderer/textures/` via StandardMaterial3D
|
||||||
3. Run: `.claude/skills/render-sprite/scripts/render.sh <name>`
|
3. Run: `.claude/skills/sprite-gen/scripts/render.sh <name>`
|
||||||
@@ -10,68 +10,67 @@ allowed-tools: Bash, Read, Grep, Glob
|
|||||||
|
|
||||||
# Ticket Skill
|
# Ticket Skill
|
||||||
|
|
||||||
Manage the project ticketing database. Basic usage (`ticket list`, `ticket show`,
|
Manage the project ticketing database. Basic usage is in CLAUDE.md's CLI tools
|
||||||
`ticket sprint --active`) and raw SQL wrappers are documented in CLAUDE.md.
|
section. This skill covers the full command reference.
|
||||||
This skill covers the full command reference.
|
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
### List tickets (full flags)
|
### List tickets (full flags)
|
||||||
```bash
|
```bash
|
||||||
db/connectors/ticket list [--status S] [--priority P] [--epic N] [--sprint N] [--assigned A] [--team T]
|
tooling/db/ticket list [--status S] [--priority P] [--epic N] [--sprint N] [--assigned A] [--team T]
|
||||||
```
|
```
|
||||||
|
|
||||||
### Create ticket
|
### Create ticket
|
||||||
```bash
|
```bash
|
||||||
db/connectors/ticket create <type> <title> [--parent N] [--priority P] [--decision D] [--team T]
|
tooling/db/ticket create <type> <title> [--parent N] [--priority P] [--decision D] [--team T]
|
||||||
```
|
```
|
||||||
Types: `initiative`, `epic`, `story`, `task`, `bug`
|
Types: `initiative`, `epic`, `story`, `task`, `bug`
|
||||||
Priorities: `critical`, `high`, `medium`, `low`
|
Priorities: `critical`, `high`, `medium`, `low`
|
||||||
|
|
||||||
### Update status
|
### Update status
|
||||||
```bash
|
```bash
|
||||||
db/connectors/ticket status <id> <new_status>
|
tooling/db/ticket status <id> <new_status>
|
||||||
db/connectors/ticket done <id> [<id> ...]
|
tooling/db/ticket done <id> [<id> ...]
|
||||||
```
|
```
|
||||||
Statuses: `backlog`, `ready`, `in_progress`, `review`, `done`, `cancelled`
|
Statuses: `backlog`, `ready`, `in_progress`, `review`, `done`, `cancelled`
|
||||||
|
|
||||||
### Assignment
|
### Assignment
|
||||||
```bash
|
```bash
|
||||||
db/connectors/ticket assign <id> <agent>
|
tooling/db/ticket assign <id> <agent>
|
||||||
db/connectors/ticket unassign <id>
|
tooling/db/ticket unassign <id>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Team assignment
|
### Team assignment
|
||||||
```bash
|
```bash
|
||||||
db/connectors/ticket team <id> <teams>
|
tooling/db/ticket team <id> <teams>
|
||||||
```
|
```
|
||||||
Teams are comma-separated, e.g. `server`, `client`, `server,client`.
|
Teams are comma-separated, e.g. `server`, `client`, `server,client`.
|
||||||
|
|
||||||
### Sprint management
|
### Sprint management
|
||||||
```bash
|
```bash
|
||||||
db/connectors/ticket sprint [--active]
|
tooling/db/ticket sprint [--active]
|
||||||
db/connectors/ticket sprint assign <id> <sprint_id>
|
tooling/db/ticket sprint assign <id> <sprint_id>
|
||||||
```
|
```
|
||||||
|
|
||||||
For sprint-scoped operations (status overview, context dumps, lifecycle),
|
For sprint-scoped operations (status overview, context dumps, lifecycle),
|
||||||
use the dedicated sprint CLI instead: `db/connectors/sprint --help`
|
use the dedicated sprint CLI instead: `tooling/db/sprint --help`
|
||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
```bash
|
```bash
|
||||||
db/connectors/ticket deps <id>
|
tooling/db/ticket deps <id>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Search and browse
|
### Search and browse
|
||||||
```bash
|
```bash
|
||||||
db/connectors/ticket search <keyword>
|
tooling/db/ticket search <keyword>
|
||||||
db/connectors/ticket epics [--status S]
|
tooling/db/ticket epics [--status S]
|
||||||
db/connectors/ticket children <id>
|
tooling/db/ticket children <id>
|
||||||
db/connectors/ticket count [--status S]
|
tooling/db/ticket count [--status S]
|
||||||
```
|
```
|
||||||
|
|
||||||
### Batch show
|
### Batch show
|
||||||
```bash
|
```bash
|
||||||
db/connectors/ticket show --brief <id> [<id>...]
|
tooling/db/ticket show --brief <id> [<id>...]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: start-workshop
|
name: workshop-start
|
||||||
description: >
|
description: >
|
||||||
Start a multi-agent design workshop from a workshop brief. Use when the user says
|
Start a multi-agent design workshop from a workshop brief. Use when the user says
|
||||||
"start workshop", "run workshop", "let's start the workshop", or invokes /start-workshop.
|
"start workshop", "run workshop", "let's start the workshop", or invokes /workshop-start.
|
||||||
Parses the workshop brief to extract participants, questions, and round format.
|
Parses the workshop brief to extract participants, questions, and round format.
|
||||||
Creates a team, tasks, and spawns agents as teammates via the Task tool.
|
Creates a team, tasks, and spawns agents as teammates via the Task tool.
|
||||||
---
|
---
|
||||||
@@ -88,9 +88,10 @@ When all Round N tasks are complete:
|
|||||||
|
|
||||||
Wrap-up sequence:
|
Wrap-up sequence:
|
||||||
1. Qatux produces final `workshop-outcomes.md` from accumulated notes
|
1. Qatux produces final `workshop-outcomes.md` from accumulated notes
|
||||||
2. If SI is present, SI creates tickets from decided items
|
2. Qatux creates or updates diagrams (via `/d2-diagram`) for any new D-records produced by the workshop
|
||||||
3. Send shutdown_request to all agents (qatux and si last, after they finish their output tasks)
|
3. If SI is present, SI creates tickets from decided items
|
||||||
4. TeamDelete to clean up
|
4. Send shutdown_request to all agents (qatux and si last, after they finish their output tasks)
|
||||||
|
5. TeamDelete to clean up
|
||||||
|
|
||||||
## Agent Type Reference
|
## Agent Type Reference
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ Cross-reference open PR head branches with the ahead-of-main branches.
|
|||||||
|
|
||||||
Show a summary table of branches ahead of main. For each branch, indicate:
|
Show a summary table of branches ahead of main. For each branch, indicate:
|
||||||
- `[PR]` if it has an open pull request — warn that it should go through
|
- `[PR]` if it has an open pull request — warn that it should go through
|
||||||
normal review channels (use `/review-pr` instead)
|
normal review channels (use `/pr-review` instead)
|
||||||
- Commit count and summary
|
- Commit count and summary
|
||||||
|
|
||||||
Use `AskUserQuestion` to let the user pick which branches to merge.
|
Use `AskUserQuestion` to let the user pick which branches to merge.
|
||||||
@@ -116,12 +116,10 @@ git merge origin/main --no-edit
|
|||||||
If clean, report the result (fast-forward or merge commit, files changed).
|
If clean, report the result (fast-forward or merge commit, files changed).
|
||||||
If conflicts, report them and stop.
|
If conflicts, report them and stop.
|
||||||
|
|
||||||
### 3. Push prompt
|
### 3. Push
|
||||||
|
|
||||||
After a successful merge, ask the user if they want to push:
|
After a successful merge, push the branch:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git push origin <current-branch>
|
git push origin <current-branch>
|
||||||
```
|
```
|
||||||
|
|
||||||
Never push without explicit confirmation.
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ run_check() {
|
|||||||
|
|
||||||
# --- Checks ---
|
# --- Checks ---
|
||||||
run_check "tooling/check-fact-ids" "fact_id validation"
|
run_check "tooling/check-fact-ids" "fact_id validation"
|
||||||
|
run_check "tooling/check-decision-ids" "decision ID duplication"
|
||||||
|
|
||||||
if [ "$ERRORS" -gt 0 ]; then
|
if [ "$ERRORS" -gt 0 ]; then
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# Build and cache
|
# Build and cache
|
||||||
.cache/
|
.cache/
|
||||||
|
.tmp/
|
||||||
server/target/
|
server/target/
|
||||||
tooling/content-converter/target/
|
tooling/content-converter/target/
|
||||||
tooling/line-previewer/target/
|
tooling/line-previewer/target/
|
||||||
@@ -20,6 +21,9 @@ renderer/output/*.png
|
|||||||
# Database (shared across worktrees at ../settledreach.db, not tracked)
|
# Database (shared across worktrees at ../settledreach.db, not tracked)
|
||||||
db/commonwealth.db*
|
db/commonwealth.db*
|
||||||
|
|
||||||
|
# Frame0 ID mapping files (ephemeral, per-machine)
|
||||||
|
*.idmap.json
|
||||||
|
|
||||||
# Python
|
# Python
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
@@ -39,3 +43,4 @@ Thumbs.db
|
|||||||
# Note: .claude/agents/, .claude/skills/, and .claude/settings.json ARE tracked
|
# Note: .claude/agents/, .claude/skills/, and .claude/settings.json ARE tracked
|
||||||
.claude/plans/
|
.claude/plans/
|
||||||
.claude/projects/
|
.claude/projects/
|
||||||
|
.claude/agent-memory/
|
||||||
|
|||||||
@@ -6,6 +6,376 @@ Format based on [Keep a Changelog](https://keepachangelog.com/).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [v0.1.23] — 2026-03-04
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- TileKind enum (Floor/Wall/Void/Restricted) on WalkabilityMap with per-tile type data alongside walkability (#576, D-094)
|
||||||
|
- Location YAML tile format — hand-author tiles as string arrays (F/W/V/R characters), loaded into WalkabilityMap on production startup (#577)
|
||||||
|
- Chunk streaming system — ChunkLoadRadius and cadence-gated load/unload around player position, v0.1 covers full district (#578, D-012)
|
||||||
|
- EngagementRecord component — per-NPC observation time, conversation count, and monologue trigger count tracked by perception/dialogue/monologue systems (#570)
|
||||||
|
- MovementHistoryBuffer resource — 3000-tick ring buffer of player positions with co-presence proximity query (#571)
|
||||||
|
- Storyteller lifecycle rules — single activation per session, no concurrency, terminal resolution constants (#572)
|
||||||
|
- Storyteller activation_pass() — gate/proximity/engagement scoring/routing/module selection/TriangleActivatedEvent on 10-tick cadence (#579)
|
||||||
|
- Debug console server — 10 DebugCommandKind variants (AdvanceTicks, SkipToContamination, TeleportToPosition, InspectNpc, ListTriangles, etc.) with DebugResponsePayload on ObserverSnapshot (#580)
|
||||||
|
- Debug console client — tilde-toggle UI panel with command input, output log, settings toggle, and full DebugCommandKind dispatch via protocol v18 (#581)
|
||||||
|
- Entity-bound dialogue speaker colors — NPC colors assigned by entity ID (not screen position) with per-conversation lifecycle and round-robin palette (#573)
|
||||||
|
- Sova Transit District tile maps — 5 locations authored: The Terminal (44×28), The Last Shift (34×22), Maintenance Corridors (58×6), Gate Ground (40×34), Gate Gallery (32×10) (#582, #583)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- LOS boundary walls — 1-tile wall margin beyond vision cone included in visible_tiles as BoundaryWall sector, walls at fog edge now render instead of bleeding into fog (#584)
|
||||||
|
- LOS boundary walls client — BoundaryWall tiles render through fog without marking explored, 4 new fog tests verify lifecycle (#585)
|
||||||
|
- Entity renderer test failures — updated 7 stale ColorRect/position assertions for Sprite2D migration, fixed SoundIndicatorRenderer class cache (#574)
|
||||||
|
- Dialogue speaker color contrast — re-enforce contrast floor after desaturation for passive (overheard) lines
|
||||||
|
- PROTOCOL_VERSION 17 → 18 mismatch — client rejected every server snapshot
|
||||||
|
- Debug console D-088 pause — sim now pauses while console is open, matching dialogue/settings overlay behavior
|
||||||
|
- Debug console settings toggle reads live state instead of ConfigFile, preventing checkbox divergence
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- PROTOCOL_VERSION bumped 17 → 18 (debug_response field on ObserverSnapshot, DebugCommand PlayerAction variant)
|
||||||
|
|
||||||
|
## [v0.1.22] — 2026-03-03
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Visual test harness — `make screenshot`, `make test-visual`, `make visual-update` for automated visual regression testing with golden PNGs across 11 scenarios (fog, HUD, dialogue, minimap)
|
||||||
|
- Visual movie mode — `make visual-movie` captures interaction flows as frame sequences with contact sheet generation
|
||||||
|
- World seed protocol — StartupMessage carries world_seed from client to server after handshake, enabling deterministic NPC population seeding (D-010, D-029)
|
||||||
|
- EntanglementConfig — per-seed NPC population ratios (flat/mundane/intrigue) sampled from seeded RNG with D-029 bounds, ensuring same seed = same world (#175, #178)
|
||||||
|
- Fog debug mode — toggle FogState.debug_exploration to render raw exploration texture as colored overlay for diagnostic use
|
||||||
|
- D-110 through D-112: z-level addressing, subterranean architecture, no instancing decisions
|
||||||
|
- Q-051: speech bubble indicator over speaking NPCs
|
||||||
|
- Sprint 22 "Wire" briefings (server, client, visual, CI, planning, joint)
|
||||||
|
|
||||||
|
### Added (server)
|
||||||
|
- Production NPC pool generation — 23 authored Sova NPCs spawn with EntanglementTag (Flat/Intrigue) based on triangle membership (#176, D-029)
|
||||||
|
- Authored triangle instantiation — 5 Sova triangles (3 active forks, 2 passive tensions) loaded from content YAML with deterministic IDs (#188, D-087)
|
||||||
|
- Contamination activation mechanic — timer-based storyteller fires after 30 game-minutes, pressures active triangles, emits ContaminationEvent (#254)
|
||||||
|
- Modifications data model stub — Vec<Modification> on chunk entities, round-trips through save/load for future construction DLC (#567, D-112)
|
||||||
|
- Zone Gate gauntlet room — two-zone test room with door boundary, zone crossing detection system (#512)
|
||||||
|
- Fuzzy map tests — 50-seed randomized testing of procedural maps against 4 structural invariants (#509)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fog shader: silent compilation failure in OpenGL3 compat mode — removed `return` statements from fragment() which are not supported, causing fog overlay to render as no-op (root cause of Sprint 22 fog regression)
|
||||||
|
- Fog system: blocky stair-stepped edges at vision cone boundary — doubled Gaussian blur step size for D-066 compliant 6-8 tile smooth gradient (#569)
|
||||||
|
- Fog system: zero visibility in explored areas — switched bounds calculation from visible_tiles (empty in live server mode) to visible_positions, and removed shader guard that cut off gradient bleed into unexplored tiles (#569)
|
||||||
|
- Fog shader alpha tuned to D-059 spec: light fog 0.25-0.35 (was 0.25-0.55), deep fog 0.55-0.70 (was 0.78-0.90) — world content now visible through fog instead of hidden behind it (#563)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Fog shader now distinguishes light fog (near cone, neutral dark) from deep fog (far from cone, zone temperature tint) with separate Perlin noise breathing cycles (8-10s / 15-20s)
|
||||||
|
- Zone temperature tint populated per-tile from server zone_id: bar=warm amber-dark, hub=cool blue-dark, corridor=neutral dark (D-059/D-046/D-077)
|
||||||
|
- Simplified vision cone from 3-sector (forward/peripheral/blind) to forward-only 120° arc — server sends only forward-cone tiles, client renders explored tiles behind the player with light fog overlay
|
||||||
|
- Simplified fog shader from 5-layer to 3-layer model (clear, explored, unexplored)
|
||||||
|
- Fog texture resize now preserves exploration data — tiles behind the player stay as light fog instead of reverting to unexplored black
|
||||||
|
- Updated D-015/D-017 perception decisions to reflect simplified cone model
|
||||||
|
- Moved connector scripts from db/connectors/ to tooling/db/ (#274) — backwards-compat symlink removed in #568
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- db/connectors symlink — all references now use tooling/db/ directly (#568)
|
||||||
|
|
||||||
|
## [v0.1.20] — 2026-02-25
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Social site template schema — RoleSchema (#163), SpaceSpec (#164), TriangleDef (#106) with YAML deserialization, sample templates at server/data/templates/
|
||||||
|
- Single-ownership model — TemplateOwnership component, TemplateReferenceMap resource, cross-template reference links preserved across save/load and tier eviction (#165, D-025)
|
||||||
|
- Triangle generation — intra-template constraint satisfaction assigns NPCs to triangle roles, minimum 2 triangles per template with fallback on imperfect seeds (#107)
|
||||||
|
- Triangle escalation system — tick_triangle_escalation runs per game-minute, tension increments toward ToleranceThreshold, TriangleCrisisEvent emitted on Active phase entry, ResolveTriangle stub command (#250, D-087)
|
||||||
|
- Protocol v16 — TriangleCrisisEventWire on ObserverSnapshot for future client rendering of triangle crises
|
||||||
|
- D-093: Sova Transit District spatial layout — 4 social sites (Terminal, Bar, Gate Cluster, Sector 3), 2 encounter nodes, zone palette, gate cluster 7-zone spec, z-level scheme (z=0 maintenance, z=1 main, z=2 observation gallery), 3 investigation paths, corridor widths
|
||||||
|
- D-094: Spatial hierarchy — chunk (64×64 sim) → block (128×128 sim) → district (4×4 blocks, 256×256 visual), supersedes D-014 estimate
|
||||||
|
- D-095: Horizon stations and transport lore — span gates (human-built, dual-use), horizon stations (alien-built, 4-8 apertures), "The Ring" per-system naming, sequential hop travel, The Loop internal tram
|
||||||
|
- Generator architecture workshop brief (ticket #562) — top-down pipeline for district generation, targeting Q-036 resolution
|
||||||
|
- SnapshotEventRouter — callable-based snapshot dispatch replaces inline if-has blocks in main.gd (#559)
|
||||||
|
- YamlParser shared utility — unified YAML parsing for UI strings and checklist conditions (#560)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Wire triangle crisis event queue into observer snapshot — clients now receive TriangleCrisisEventWire via protocol v16 (was always empty)
|
||||||
|
- Persist TriangleState in SaveStateV1 — triangle phase and tension survive save/load cycles
|
||||||
|
- Validate dangling with_role references in TriangleDef constraint validation
|
||||||
|
- Replace O(n²) fallback NPC assignment with BTreeSet; prevent same NPC assigned to two roles in one triangle
|
||||||
|
- Replace O(N*M) scan in apply_resolve_triangle with BTreeMap index for O(1) per-command lookup
|
||||||
|
- Add From impls for RoleId, TriangleId, StableId, TriangleCrisisEventWire — eliminate fragile .0 newtype access
|
||||||
|
- Consolidate near-identical unit tests with integration counterparts
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Sova station profile updated — horizon gates located at The Krenn Ring (800 AU), not on Station Sova; Admin Hub houses transit processing facility only
|
||||||
|
- game_state.gd: stationary_ticks and zone_id now read from server snapshot with deprecated client-side fallbacks (#557, D-020)
|
||||||
|
- dialogue_box.gd: decoupled from GameState and AudioManager via signals — zero direct autoload references (#558, D-020)
|
||||||
|
- main.gd: snapshot dispatch via SnapshotEventRouter, dialogue signal coordinator handlers (#559, #558)
|
||||||
|
- ui_strings.gd and checklist_evaluator.gd: delegate to YamlParser, ~140 lines of duplication removed (#560)
|
||||||
|
|
||||||
|
## [v0.1.19] — 2026-02-25
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Sprint 20: Shape planned — 11 tickets (server 6, client 4, planning 1) covering template/triangle schemas, client refactors, and district layout design discussion
|
||||||
|
- Planning team ticket type in sprint-plan skill — supports design discussions with purpose-assembled agent panels, Qatux and SI for bookkeeping
|
||||||
|
- Client PR #70 merged — save/load client UI, F5/F6 quicksave/quickload (#554)
|
||||||
|
- Server PR #68 merged — Sprint 19 save/load, tier eviction, test infra (7 tickets, 2714 lines)
|
||||||
|
- Client PR #67 merged — Sprint 19 test infra, session management, debug overlay (5 tickets, 2547 lines)
|
||||||
|
- CI PR #69 merged — Sprint 19 test runners, IPC fixtures, protocol handshake, benchmark (4 tickets, 1297 lines)
|
||||||
|
- Test runner scripts — 7 bash scripts (run-rust, run-godot, run-ipc-fixtures, run-ipc-protocol, run-ipc-integration, run-ipc-benchmark, run-all) with structured JSON output (#270, D-030)
|
||||||
|
- IPC serialization fixtures — 5 msgpack fixtures with Rust generator, cross-language GDScript validation (22 assertions) (#271, D-030)
|
||||||
|
- Protocol handshake client — HANDSHAKING state in SimBridge, HandshakeMessage decode with 5s timeout (#556, D-020)
|
||||||
|
- IPC round-trip benchmark — p50/p95/p99 latency reporting, 5ms threshold (#342, D-020)
|
||||||
|
- Protocol version handshake — `HandshakeMessage` as first IPC frame before tick loop, forward-compatible input handling (#555, D-020)
|
||||||
|
- Protocol v15 — `save_result` field on ObserverSnapshot for client save/load confirmation
|
||||||
|
- State serialization primitives — `serialize_npc_to_frozen`/`deserialize_npc_from_frozen` with full D-024 10-axis coverage for tier eviction freeze/thaw (#96, D-026)
|
||||||
|
- Scope tag system — `ScopeTagKind` (Neighborhood, ActiveQuest, Colleague, KnownContact), `ScopePinned` marker, automatic assignment from KnowledgeGraph and RelationshipGraph (#98, D-026)
|
||||||
|
- Timestamp-based eviction — `LastInteractionTick` LRU tracking, `SimSpacePressure` resource, BinaryHeap eviction respecting scope-pinned entities, Active cap 80 (#97, D-026)
|
||||||
|
- Save/load ECS extraction — `save_to_file`/`load_from_file` via MessagePack, `SaveGame`/`LoadGame` IPC commands, `SaveLoadResultWire` on ObserverSnapshot (#553, D-085)
|
||||||
|
- ScopePinned eviction regression test — adversarial at-scale test proving pinned NPCs survive eviction even with oldest ticks
|
||||||
|
- Test infrastructure — Layer 3 integration test entry point, three-layer architecture per D-030 (#200)
|
||||||
|
- Information boundary negative tests — 4 tests proving no passive KG leakage, LOS fog holds, tier boundary holds, per-NPC save isolation (#272, D-010)
|
||||||
|
- gdUnit4 CI runner script — headless test execution via `run_gdunit4.gd` with exit code for CI (#205)
|
||||||
|
- Scene testing utilities — SceneHelper class with node existence, signal, and path helpers for gdUnit4 (#206)
|
||||||
|
- GameState apply_snapshot tests — 14 tests covering v2+ fields: game_time, facing, interactions, monologue, stance, inventory (#206)
|
||||||
|
- Game session management — per-game save directories under `user://saves/<timestamp>-<seed>/` per D-085, SessionManager autoload, main menu scene (#258)
|
||||||
|
- Debug visualization overlay — F3-toggled dev overlay with LOS rays, vision cone arcs, NPC path trails, knowledge confidence tags, tick timing sparkline (#348)
|
||||||
|
- SimBridge→TestHarness extraction — test simulation logic separated into dedicated RefCounted class with backward-compat proxy API
|
||||||
|
- Workshop outcomes files — formal closure for content-gap-analysis, KG-information-boundaries, v01-content-scoping, v01-gap-analysis, wiki-review
|
||||||
|
- D-087 through D-092 — recovered decisions from v01-content-scoping and wiki-review workshops (triangle config, pause system, content scope, voice registers, anchor lines, complicity theme)
|
||||||
|
- Q-030 through Q-039 — open questions from workshop backlog (seed schema, style guide, cultural ingredients, NPC architecture, PC archetypes, sacred/profane framework, district skeleton, generator pipeline, authored content estimate, gate topology)
|
||||||
|
- Decision ID claim system — `db/connectors/decision` CLI with `next`, `claim`, `check-dupes` commands to prevent cross-worktree D/Q/R ID collisions, pre-commit duplicate check
|
||||||
|
- D-085: Per-game save directory structure — every new game creates `user://saves/<game-id>/`, F5 quicksave, F6 quickload
|
||||||
|
- Q-029: Save file format design — long-term considerations for versioning, compression, integrity, metadata headers
|
||||||
|
- D-086: Renumbered insert icon system (was D-084 on visual branch) to resolve cross-worktree ID collision
|
||||||
|
- Save/load wireframe updated for D-085 — LOAD tab shows games grouped by directory with expand/collapse, QUICKSAVE slot, F5/F6 hints
|
||||||
|
- Sprint 19: Persist planned — 16 tickets (server 7, client 5, CI 4) covering save/load, tier eviction/scope, test infrastructure
|
||||||
|
- Character creation & game setup workshop brief — covers creation model, seed boundary, gate activation, quest seeding, game toggles (resolves Q-011)
|
||||||
|
- Protocol v14 — `poi_list`, `examine_result`, `player_knowledge` ObserverSnapshot wire types with live KG serialization (#151, #174, #264)
|
||||||
|
- Minimap rendering — circular 160px diegetic insert overlay with POI dots (colored by category), border arrows for distant POIs, player-centered fixed-north (#151)
|
||||||
|
- Dialogue UI hardening — confrontation italic voice (D-063), examine result overlay with 5s auto-dismiss and confidence coloring (#174)
|
||||||
|
- Knowledge/journal panel — right-side insert panel (J key), facts grouped by entity, contradicted entries in amber with strikethrough, stale entries dimmed, mutual exclusion with dialogue (#264)
|
||||||
|
- Sprint 18 client test suite — 50 gdUnit4 tests for dialogue (D-062, D-063, D-064) and journal (KG parsing, scene structure, UIStrings), plus test plan document
|
||||||
|
- D-084: dual-namespace line ID scheme for auto-generated NPCs — role pool (shared, unchanged) + instance override (opt-in, seeded counter). Resolves Q-028 (#544)
|
||||||
|
- Tier 1 drama module schema (`content/schemas/drama_module.schema.yaml`) — entry conditions, NPC requirements, event sequences, outcomes, pool format (#158)
|
||||||
|
- Smuggling ring v0.1 stub module (`content/modules/tier1/smuggling_ring_v0_1.yaml`) — vertical slice Tier 1 module with 6 NPC roles, dual event sequences, 5 outcomes (#158)
|
||||||
|
- Line ID authoring guide (`docs/design/line-id-authoring-guide.md`) — dual-namespace conventions for hand-authored and auto-generated NPC content
|
||||||
|
- Tier 1 module authoring guide (`docs/design/tier1-module-authoring.md`) — field reference, NPC pattern/motivation tables, design principles, pre-submission checklist
|
||||||
|
- Background tier state machines — schedule, mood, relationships, job tick once per game-minute for Background NPCs (#95, D-026)
|
||||||
|
- NPC vision system — symmetric shadowcasting for Active-tier NPCs, NpcMemory with last-known-position and zone inference (#115, D-011)
|
||||||
|
- NPC player-awareness behavior — PlayerAwareness component tracks LOS duration, suspicion accumulation, routine deviation triggers (#244)
|
||||||
|
- Skill system & combat flag — SkillSet component (BTreeMap<String, u8>), CombatCapability marker from combat_trained skill (#91, D-024)
|
||||||
|
- Player-action social propagation — three-order trust ripple (100%/40%/20%) through RelationshipGraph with cycle prevention (#249, D-029)
|
||||||
|
- Examine mechanic — process_examine_interaction with character-filtered observation text, KG DirectObservation write, examine_result in ObserverSnapshot (#242)
|
||||||
|
- Character goal/pressure framework — CharacterPressure component (exposure/institutional/relationship), wired to snapshot HUD data (#248)
|
||||||
|
- Save state data model — SaveStateV1 struct with MessagePack serialization, roundtrip tests for entity/KG/relationship/clock state (#256)
|
||||||
|
- Tell state derivation wired into ObserverSnapshot — integration tests for Nervous tell on Major secret + high stress (#337)
|
||||||
|
- Sprint 18: Touch planned — 14 tickets (server 9, client 3, copy 2) covering examine mechanic, NPC awareness, social propagation, minimap, dialogue UI, save state model
|
||||||
|
- `.claude/rules/` directory — modular auto-loaded instructions (tea-cli, git-safety, project-structure, team-patterns, local-services)
|
||||||
|
- KnowledgeGrant untagged enum with Fact and Entity variants, ContentEntityRegistry for NPC spawn-time entity resolution (D-079, #545)
|
||||||
|
- KnowledgeGranted event processing — grants fire at dialogue line selection, runtime NPC KG guardrail (D-079, #546)
|
||||||
|
- ContradictionClaim struct with 600-tick window detection in observe_entity, epistemic neutrality for both sources (D-083, #547)
|
||||||
|
- NPC-to-NPC knowledge transfer system — trust-gated fact exchange, confidence capping at KnowsOf, ToldBy source construction (D-080, #548)
|
||||||
|
- tell_state KG awareness — NPC relationship reads from KG for other-entity state, MVP information boundary (D-082, #549)
|
||||||
|
- Contradiction monologue with pre-resolved entity names, PersonOfInterest relationship shift, THE FRIEND arc event chain (D-083, #550)
|
||||||
|
- Unprompted disclosure system — DisclosureCandidates component, 7 trigger gates, three-layer rate limiting, two-stage trait filter (D-081, #551)
|
||||||
|
- Trait modifier system — Cautious/Gossipy/Loyal/Talkative filter predicates via content-authorable config (D-081, #173)
|
||||||
|
- POI data model and proximity-based discovery system via KnowledgeGranted events (#148, #149)
|
||||||
|
- Protocol versioning tests — version round-trip, mismatch detection, serde_default migration pattern, full variant coverage (#232)
|
||||||
|
- Team monitoring rules — heartbeat rule for stuck agent detection, bottleneck detection pattern
|
||||||
|
- `tooling/tea-comment` — single-command wrapper for posting Gitea PR/issue comments with multi-line bodies
|
||||||
|
- D-086: Insert icon system — custom SVG icons over icon fonts, authored to insert geometric constraints with lattice_profile weight scaling
|
||||||
|
- Insert/HUD wireframe and visual spec (#314) — dual character variants (smuggler social network view, detective investigation overlay) with pixel-precise layout, entity markers, time display, border arrows, commission grid, and all interaction states
|
||||||
|
- Contradiction monologue lines — 16 hand-authored lines (8 detective, 8 smuggler) for Sera/Kael FRIEND arc, Phase 2 blindsiding + Phase 3 pattern recognition, cognitive-dissonance-not-accusation tone per D-083 (#552)
|
||||||
|
- Diegetic tutorial monologue — 20 lines (10 per character) teaching movement, fog, sound, NPC interaction, and insert/HUD through character voice, fire-once on first-time events (#330)
|
||||||
|
- Diegetic time display on insert HUD — station local time (HH:MM), day phase with cycle-tinted color, day number on InsertOverlay (#263)
|
||||||
|
- Relationship color accent on E-Talk overlay — 3px left-edge bar using D-033 palette signals NPC relationship at a glance (#537)
|
||||||
|
- `Constants.format_game_time()` helper for converting game-minutes to HH:MM station time
|
||||||
|
- `/sprint-status` cleanup sweep skill — consistent health report with tickets by status, PR cross-reference, bookkeeping issue detection, and open work by team
|
||||||
|
- `sprint sweep` CLI subcommand — structured JSON output for sprint health checks (grouped tickets, per-team summary, issue detection)
|
||||||
|
- Knowledge Flow & NPC Boundaries workshop — 5 D-records (D-079–D-083) covering grant architecture, NPC-to-NPC propagation, unprompted disclosure, NPC information boundaries MVP, contradiction detection pipeline
|
||||||
|
- 7 knowledge graph implementation tickets (#545–#551) with full dependency chain and line estimates
|
||||||
|
- Contradiction monologue content ticket (#552) for Sera/Kael FRIEND arc
|
||||||
|
- Sprint 17 completion proofs: contradiction detection fires, NPC-to-NPC knowledge transfers
|
||||||
|
- Entity renderer migrated from ColorRect placeholders to Sprite2D with D-019 angle sprites — self_modulate for D-033 tinting, 8→4 octant direction mapping, feet-anchored y-sort (#540)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Client protocol version bumped to 15 to match server (was still at 14 after server PR #68 added save_result field)
|
||||||
|
- gen_fixtures.rs version comments changed from hardcoded 14 to PROTOCOL_VERSION constant
|
||||||
|
- run-ipc-benchmark dead --iterations flag removed (Rust compile-time constant governs rounds)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Team boundary framing — replaced worktree-centric language with `$WORKTREE_TEAM` env var identity across CLAUDE.md and skills (sprint-start, sprint-plan, pr-review) to prevent agents from following `.git` pointers across boundaries
|
||||||
|
- CLAUDE.md compacted from 188 to 67 lines — CLI references, endpoints, and patterns moved to `.claude/rules/`
|
||||||
|
- `/sprint-status` delegates to haiku subagent — keeps sweep JSON, template read, and PR list out of main context window
|
||||||
|
- `sprint sweep` JSON trimmed — removed unused fields (`ok`, `sprint.status`, `priority`, `ticket_id`), shortened issue detail strings
|
||||||
|
- Sprint status output template condensed — rendering rules moved to skill definition, bookkeeping table simplified to 2 columns
|
||||||
|
- Model selection documented in CLAUDE.md — `/model sonnet[1m]` and `/model opus[1m]` for 1M context sessions
|
||||||
|
- Sprint 17 briefings updated with workshop results — server (14 tickets), copy (2 tickets), client (2), visual (1)
|
||||||
|
- Q-024 (gossip timing), Q-025 (KG memory), Q-026 (contradiction detection) closed
|
||||||
|
- Sprint 16 closed (8/8 done)
|
||||||
|
- 3D sprite render pipeline — Camera3D at D-019 angle (-72.5° from horizontal), three-point studio lighting rig, orthographic projection, resolution chain 1024→256→64
|
||||||
|
- Generic NPC capsule model (24×32px footprint per D-044) and structural wall model for pipeline validation
|
||||||
|
- Test sprites: 8 runtime 64px sprites (NPC + wall × 4 directions) deployed to client/assets/sprites/
|
||||||
|
- Pipeline documentation (renderer/README.md) — camera spec, lighting rig, resolution chain, model authoring guide
|
||||||
|
- DialogueResponse verb handler — players pick dialogue options and receive follow-up lines via full D-028 four-layer pipeline (#539)
|
||||||
|
- Trust-gated gossip verification — integration tests confirm Secret/Real/Surface tier gating per D-075 (#171)
|
||||||
|
- Line variety tracker wiring — DialogueCooldownTracker prevents repeat lines within 600-tick window (#338)
|
||||||
|
- DialogueResponse cross-language fixture for GDScript testing
|
||||||
|
- Sprint team lifecycle through PR review — teams stay alive for commit → push → review → fix loop → approve → shutdown
|
||||||
|
- Zone_id extraction in game_state.gd optimized from O(N) tile scan to O(1) dictionary lookup — builds _tile_by_coord from member visible_tiles covering both test and live paths (#543)
|
||||||
|
- Shared run_dialogue_pipeline() helper eliminates ~60 lines of duplication between Talk and DialogueResponse systems
|
||||||
|
- Dialogue and monologue line IDs migrated from location-scoped (the-terminal_d_039) to NPC-scoped (kael-davan_d_001) namespace — each NPC has an independent sequence per D-035 (#542)
|
||||||
|
- DialogueCooldownTracker documented as per-player-global by design (NPC-scoped line IDs per D-035 prevent collision)
|
||||||
|
- CONFRONTATION_LINES marked TODO for migration to D-028/D-035 content pipeline
|
||||||
|
- pr-push and pr-review skills updated with team lifecycle awareness
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- PR #59 review: stale mood vocabulary updated in line-pool-format.md, style-guide, and content-directory-structure.md to post-Sprint 14 values
|
||||||
|
- PR #59 review: orphaned location-scoped IDs in maintenance-tech.yaml comments and smuggler-inventory.yaml cross-references updated to NPC-scoped
|
||||||
|
- PR #59 review: Lera Sessik tenure corrected from "twelve years" to "eighteen years", NPC header fixed
|
||||||
|
- PR #59 review: ring-operative.yaml fact_id corrected from `location.surveillance_gaps` to `investigation.surveillance_gaps`
|
||||||
|
- Dialogue systems moved from BridgePlugin to NpcPlugin — game logic registers where it belongs (#538)
|
||||||
|
- Schedule ambiguity: emit_observation_events now has explicit .before(advance_tick) constraint
|
||||||
|
- process_dialogue_response updates ActiveDialogue tick and InteractionMemory on follow-up
|
||||||
|
- DialogueResponse range check added (CLOSE_RANGE, matching Talk/Confront pattern)
|
||||||
|
- Weighted selection fallback replaced with unreachable!() — dead code removed
|
||||||
|
- assert!(false) → panic!() in serialization tests (clippy)
|
||||||
|
- SetFacing and TeleportToHub added to roundtrip test coverage
|
||||||
|
|
||||||
|
## [v0.1.15] — 2026-02-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Sprint 16 "Converse" briefings — 8 tickets across server/client/copy/visual teams
|
||||||
|
- 19 UI wireframes — HUD, dialogue, monologue, popups, menus in v0.1 and v1.0 variants with D-record cross-references
|
||||||
|
- d2-diagram skill — text-to-diagram generation with project defaults (theme 200, dagre, PNG)
|
||||||
|
- frame0-wireframe skill — UI wireframing via Frame0 HTTP API, replaces MCP dependency with bash+curl
|
||||||
|
- 16 decision diagrams — architecture, data-flow, entity, state, and UI categories covering all project decisions
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- frame0-wireframe skill rewritten — JSON-as-truth workflow with frame0-sync.py, batch export, renderer-only guidance
|
||||||
|
- pr-review skill — all reviewer agents now use worktree paths instead of git show
|
||||||
|
- Dialogue panel is always visible as permanent insert UI element (D-061)
|
||||||
|
- Makefile: check-protocol target verifies server/client protocol versions match before build
|
||||||
|
- D-035 amended: line ID namespace changed from location-scoped to NPC-scoped (Sprint 15)
|
||||||
|
- Tilemap z-layer filtering — FloorTiles renders z=0 only, z=1/z>1 reserved for future layer nodes (#71, D-049)
|
||||||
|
- Entity 24x32 footprint per D-044 visual hierarchy — split ENTITY_SIZE into WIDTH/HEIGHT with separate offsets (#72)
|
||||||
|
- Follow target stub on GameState — `follow_target_id` field ready for server #241 Follow verb
|
||||||
|
- Manual exponential camera smoothing — CAMERA_SMOOTHING_SPEED constant (8.0), same lerp pattern as entity renderer (#117)
|
||||||
|
- 31 new Sprint 15 validation tests — camera smoothing, UI framework z-layers, entity footprint, Sprint 14 regressions
|
||||||
|
- SpatialIndex trait with naive Vec implementation — entities_in_range, entities_at, update methods with Manhattan distance (#340)
|
||||||
|
- NPC generation pipeline — procedural seeding of all 10 D-024 axes via SimRng with constraint validation (#92)
|
||||||
|
- Personality and tell system — 5 tell categories (Nervous, Angry, Friendly, Guarded, RoutineDeviation) derived from NPC axis values each tick (#90)
|
||||||
|
- Tolerance threshold monitoring — ToleranceBreachEvent on stress exceeding per-NPC threshold, mood FSM integration (#105)
|
||||||
|
- Routine deviation detection — RoutineDeviationEvent on wrong location/activity for day phase, absence detection, pathfinding-aware (#243)
|
||||||
|
- Follow mechanic — Follow verb, proximity/LOS tracking, double-frequency observation events, NPC suspicion accumulation, configurable thresholds (#241)
|
||||||
|
- Monologue event triggers — observe_npc, hear_sound, observe_anomaly, witness_interaction, post_conversation with D-035 context tags (#119)
|
||||||
|
- Protocol v13 — tell_state on VisibleEntity, follow_state on ObserverSnapshot, Follow verb
|
||||||
|
|
||||||
|
## [v0.1.14] — 2026-02-21
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Unified dialogue log — player-NPC and overheard NPC-NPC conversations in one chronological scrolling panel (#535, D-061/D-078)
|
||||||
|
- F3 debug overlay — real-time game state display with tick, FPS, position, entity counts, dialogue/monologue status (#511)
|
||||||
|
- Monologue display — multi-line priority queue with character colours, italic BBCode, stagger animation (#122)
|
||||||
|
- Protocol v9 — conversation_events, conversation_ended, dialogue_response fields with carry-forward logic
|
||||||
|
- Dialogue theme system — configurable NPC name colour palette, entry timing, passive opacity via dialogue-theme.yaml
|
||||||
|
- Monologue display system visual spec — typography, positioning, stacking, priority, fade animation, character color differentiation, 80-char line constraint (#315)
|
||||||
|
- Entity color system spec — D-033 relationship-to-player mapping, transition animations, color blindness assessment (#304)
|
||||||
|
- Text display hierarchy spec — 4 content pipelines (dialogue, monologue, observation, environmental) with z-layers and positioning (#316)
|
||||||
|
- Sound indicator visual design — fog-edge pulse for D-018 three-range sound model with direction encoding and range differentiation (#317)
|
||||||
|
- THE FRIEND visual treatment spec — 3-phase earned visual detail for Kael Davan and Sera Venn (#318)
|
||||||
|
- Environmental text visual standards — signage, terminal, and news ticker rendering with bilingual Concordat/Krenn treatment (#334)
|
||||||
|
- Tell visual/behavioral expression spec — 5 tell categories mapped to 6 Tier 2 behaviors (#251)
|
||||||
|
- Monologue line pool maxLength raised from 160 to 256 chars (soft guidance ≤160)
|
||||||
|
- NPC name masking infrastructure — entity-anchored dialogue log with server-side role labels, retroactive name update on learning, NpcColorIndex for stable color assignment
|
||||||
|
- Dialogue option keyboard selection (1/2/3 number keys) and numbered option labels
|
||||||
|
- Interaction list chrome — background panel, mouse hover highlighting, click-to-interact, pointing hand cursor
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- D-061 updated to document unified conversation log architecture from Sprint 14
|
||||||
|
- Dialogue options switched from RichTextLabel to Label for reliable VBoxContainer sizing
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Visual grammar dialogue max-width corrected from "~70% screen width" to 640px per D-076
|
||||||
|
- BBCode injection in dialogue log formatting — server-sourced strings now escaped with [lb]
|
||||||
|
- Per-frame dialogue log rebuild replaced with dirty flag (performance)
|
||||||
|
- dialogue_active lifecycle — now cleared after panel fade completes per D-064
|
||||||
|
- PAUSE/UNPAUSE routed through main.gd input recording for bug report replay (#507)
|
||||||
|
- WASD input freeze after filing bug report — LineEdit focus not released before queue_free() across CanvasLayers
|
||||||
|
- WASD not reactivating after Talk — dialogue_active held for entry_lifetime instead of cleared immediately
|
||||||
|
- Recognition chime spam — entity IDs now tracked permanently per room instead of expiring
|
||||||
|
- Audio path warning — res://audio/ corrected to res://assets/audio/ in AudioManager
|
||||||
|
- world_radial.tscn anchors_preset warning — changed from 15 to 0
|
||||||
|
- bug_report_dialog.gd push_warning changed to print for informational message
|
||||||
|
|
||||||
|
## [v0.1.13] — 2026-02-20
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- D-078: Overheard NPC conversation — passive dialogue panel with server-authoritative stochastic word occlusion
|
||||||
|
- Sprint 14 "Live" briefings — 22 tickets across server (7), client (3), copy (6), visual (6)
|
||||||
|
|
||||||
|
## [v0.1.12] — 2026-02-19
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Tier marker components (#93), active tier simulation (#94), tier transition logic (#99)
|
||||||
|
- Information tag schema (#138), component-level access control (#139)
|
||||||
|
- Line previewer CLI (#193)
|
||||||
|
- Sound event system — server pipeline (#124)
|
||||||
|
- Close-range stereo audio — client positional 2D (#125)
|
||||||
|
- Medium-range visual indicators — fog-edge directional arrows (#126)
|
||||||
|
- HashMap ban in simulation crate via clippy (#343)
|
||||||
|
- Tracing crate infrastructure — JSON format, tick duration logging (#344)
|
||||||
|
- System dependency graph debug command — `--dump-schedule` CLI flag (#346)
|
||||||
|
- rng_seed field on ObserverSnapshot for deterministic replay (#527)
|
||||||
|
- v0.1 Visual Grammar Document (#303)
|
||||||
|
- Placeholder art specification (#252)
|
||||||
|
- Spatial layouts: Logistics Hub (#311), Bar (#312), Smuggling corridors (#313)
|
||||||
|
- Cultural generation guide — 5-dimension framework for Sova Transit District cultural voice (#189)
|
||||||
|
- Sova Texture Appendix — 20-term slang glossary, sensory profile, Meridian self-censorship rules (#302)
|
||||||
|
- Contraband specification — unlicensed lattice components, supply chain, street terminology (#321)
|
||||||
|
- Sova Station Profile — 6 districts, governance, off-station references (#320)
|
||||||
|
- Span Gate Transit Schedule — hourly schedule, maintenance windows, ring operational calendar (#336)
|
||||||
|
- Meridian Coverage Map — 10 named zones from Commission-grade to dead air (#335)
|
||||||
|
- Character definition schema and both character builds — smuggler + detective (#179, #180, #181)
|
||||||
|
- Divergent starting knowledge and relationships per character (#182, #183)
|
||||||
|
- Detective institutional chain of command (#322)
|
||||||
|
- Contradiction arc design document — reusable FRIEND pattern (#332)
|
||||||
|
- Mirror moment design document — 7 core dual-perspective observation triggers (#329)
|
||||||
|
- First 5 minutes experience design — systemic opening per character (#259)
|
||||||
|
- Opening hook content per character (#260)
|
||||||
|
- Knowledge vocabulary for v0.1 content — entity/world categories, prerequisite format (#368)
|
||||||
|
- Knowledge state vocabulary — author-facing quick reference (#309)
|
||||||
|
- Knowledge fact catalogs — 10 YAML files in content/global/knowledge/, 73 canonical facts
|
||||||
|
- D-075 endorsement — archetype dimension review recorded in decisions/content.md
|
||||||
|
- Flat NPC memorable trait pass — Pael, Ren, Tev with noise-floor profiles (#307)
|
||||||
|
- Environmental text content — 20 items across Terminal, Bar, and Corridors with dual-lens notes (#262)
|
||||||
|
- Diegetic insert flavor text — per-character labels and notification strings (#331)
|
||||||
|
- News ticker / Meridian feed — 30 lines including batch 44xx recall dual-lens moment (#306)
|
||||||
|
- Workplace content pack — The Terminal: 5 NPC dialogue files (#190)
|
||||||
|
- Bar content pack — The Last Shift: 3 NPC dialogue files (#191)
|
||||||
|
- Smuggling ring content pack — maintenance corridors: coded vocabulary, dual registers (#192)
|
||||||
|
- Generation pass expansion — 80 ambient variant lines across all 9 dialogue files (#194)
|
||||||
|
- Sprint 13 "Sound" briefings — 9 tickets across server, client, audio, visual teams; full audio architecture + gauntlet expansion + monologue display spec
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Entity renderer field name bug — `id` vs `entity_id` (#345)
|
||||||
|
- Dialogue max-width pixel value — 640px per D-076 (#447)
|
||||||
|
- Routine tests missing ActiveSim — 3 of 5 tests passed trivially without the required tier marker
|
||||||
|
- `_observer_pos` misleading unused prefix renamed to `observer_pos` (used for sound event filtering)
|
||||||
|
- Stale protocol version doc comment "Current: 9" corrected to 10
|
||||||
|
- FactionOnly non-numeric `faction_id` attribute now logs a tracing::warn instead of silently denying
|
||||||
|
- SOUND_EVENT_ASSETS walk-speed key mismatch — `sfx_footstep_metal` corrected to `sfx_footstep_metal_walk`
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Removed orphaned `SimulationTier`/`LastInteraction`/`ScopeTag`/`ScopeKind` types from tier.rs (unused outside own tests)
|
||||||
|
- Sound pipeline documented as intentionally empty in v0.1 (no producers yet, full pipeline wired)
|
||||||
|
- Observer test setup now inserts SoundEventQueue resource for integration coverage
|
||||||
|
- Added FactionOnly positive test case and Medium-range occlusion TODO
|
||||||
|
- Sound indicator colors sourced from Constants instead of duplicated hex literals
|
||||||
|
- play_loop() null guard on stream.duplicate()
|
||||||
|
- Camera zoom fallback uses Constants.CAMERA_DEFAULT_ZOOM
|
||||||
|
|
||||||
|
## [v0.1.11] — 2026-02-19
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Sprint 12 "Build" briefings — 50 tickets across server, client, copy, visual, ci teams; production-layer foundations + all v0.1 copy authoring
|
||||||
|
- `.tmp/` gitignored repo directory for agent temp files — avoids Bash permission prompts during PR review comment posting
|
||||||
|
- `sed -n` blanket permission in shared settings
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- All skills renamed to domain-action convention (e.g. `commit`→`git-commit`, `review-pr`→`pr-review`, `gen-audio`→`audio-gen`, `render-sprite`→`sprite-gen`) — 12 renames total
|
||||||
|
- `pr-review` skill uses Write tool into `.tmp/` instead of Bash heredocs to `/tmp/`
|
||||||
|
|
||||||
## [v0.1.10] — 2026-02-19
|
## [v0.1.10] — 2026-02-19
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -17,12 +387,17 @@ Format based on [Keep a Changelog](https://keepachangelog.com/).
|
|||||||
- Mouse-relative facing and movement (#526, D-054) — mouse position determines facing direction (client-side float), WASD remapped to cursor-relative (W=toward, S=away, A/D=strafe), SET_FACING action sends octant to server, smooth facing indicator rotation
|
- Mouse-relative facing and movement (#526, D-054) — mouse position determines facing direction (client-side float), WASD remapped to cursor-relative (W=toward, S=away, A/D=strafe), SET_FACING action sends octant to server, smooth facing indicator rotation
|
||||||
- Room reset client UX (#502) — amber reset_plate tile type, 0.15s screen flash on room reset, 'Reset Room' interaction verb
|
- Room reset client UX (#502) — amber reset_plate tile type, 0.15s screen flash on room reset, 'Reset Room' interaction verb
|
||||||
- Auto-checklist progress tracking (#503) — ChecklistEvaluator parses room YAML and evaluates 7 condition types against GameState with latching, ChecklistOverlay renders progress in gauntlet mode only, 48 new tests
|
- Auto-checklist progress tracking (#503) — ChecklistEvaluator parses room YAML and evaluates 7 condition types against GameState with latching, ChecklistOverlay renders progress in gauntlet mode only, 48 new tests
|
||||||
|
- 4 ambient zone loops: station base, workplace, bar, corridor — SAO-generated organic soundscape with crossfade loop points (#327)
|
||||||
|
- 2 footstep SFX: metal walk and run — SAO hybrid with best-transient extraction (#327)
|
||||||
|
- `audio-batch` command — batch audio generation from JSON manifests, supports SAO and harmonic synthesis, with `--dry-run`, `--only`, and `--skip-existing` flags
|
||||||
|
- `--post` and `--output-ogg` flags on `audio-generate` — chain post-processing (trim, normalize, convert) into a single command
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- `push-pr` skill now runs `/commit` first when uncommitted changes are detected
|
- `push-pr` skill now runs `/commit` first when uncommitted changes are detected
|
||||||
- Insert open/close now sends explicit PauseSimulation/ResumeSimulation (#518, D-058) — replaces toggle-style pause with idempotent pair
|
- Insert open/close now sends explicit PauseSimulation/ResumeSimulation (#518, D-058) — replaces toggle-style pause with idempotent pair
|
||||||
- Interaction list colors reference Constants.IMPLANT_TEXT_COLOR instead of hardcoded values
|
- Interaction list colors reference Constants.IMPLANT_TEXT_COLOR instead of hardcoded values
|
||||||
- World radial menu uses theme font instead of ThemeDB.fallback_font
|
- World radial menu uses theme font instead of ThemeDB.fallback_font
|
||||||
|
- Monologue chimes replaced with production-quality manual synthesis — insert-tech aesthetic per D-074, pure sine harmonics with mathematical envelopes (#327)
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Bidirectional relationship check (#515) — Check 9 tested `target in npc_rels` which missed NPCs with no relationship entries; changed to `target in self.npcs`
|
- Bidirectional relationship check (#515) — Check 9 tested `target in npc_rels` which missed NPCs with no relationship entries; changed to `target in self.npcs`
|
||||||
|
|||||||
@@ -1,67 +1,41 @@
|
|||||||
# The Settled Reach
|
# The Settled Reach
|
||||||
|
|
||||||
A top-down immersive sim — occlusion-based detective game with combat elements, set in an original science fiction universe. Single-character perspective, asymmetric information as core mechanic, Rimworld-style storyteller. Godot 4 client + Rust/bevy_ecs simulation server via subprocess/IPC (D-020).
|
A top-down immersive sim — occlusion-based detective game with combat elements, set in an original science fiction universe. Single-character perspective, asymmetric information as core mechanic, Rimworld-style storyteller. Godot 4 client + Rust/bevy_ecs simulation server via subprocess/IPC.
|
||||||
|
|
||||||
**Official Title:** The Settled Reach (D-021)
|
**Official Title:** The Settled Reach
|
||||||
**Repository name:** settled-reach (formerly commonwealth, renamed for clarity)
|
**Repository name:** settled-reach
|
||||||
**Version source of truth:** `project.yaml` (root `version` field, scheme: `0.1.{sprint_number}`)
|
**Version source of truth:** `project.yaml` (root `version` field, scheme: `0.1.{sprint_number}`)
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
client/ # Godot 4 client (D-020)
|
client/ # Godot 4 client
|
||||||
server/ # Rust/bevy_ecs simulation server (D-020)
|
server/ # Rust/bevy_ecs simulation server
|
||||||
tooling/ # Build tools, scripts, asset pipelines
|
tooling/ # Build tools, scripts, asset pipelines
|
||||||
tests/ # Integration and end-to-end tests
|
tests/ # Integration and end-to-end tests
|
||||||
.config/ # Configuration files (linters, formatters, CI)
|
docs/ # Architecture, design, briefings, sprints, workshops
|
||||||
.cache/ # Local caches for testing/linting (gitignored)
|
db/ # Schema + seed data (connectors moved to tooling/db/)
|
||||||
docs/
|
.claude/ # Agents, skills, rules
|
||||||
discussions/ # Discussion rounds (all rounds archived here per D-022)
|
decisions/ # Decision domain files (D-NNN confirmed, Q-NNN open, R-NNN rejected)
|
||||||
briefings/ # Per-agent context briefings (maintained by Qatux)
|
|
||||||
architecture/ # Technical architecture documents
|
|
||||||
design/ # Game design documents
|
|
||||||
sprints/ # Sprint briefings per team (server.md, client.md, copy.md, joint.md, etc.)
|
|
||||||
workshops/ # Workshop briefs and outputs (per-workshop subdirectories)
|
|
||||||
db/
|
|
||||||
schema.sql # Database schema
|
|
||||||
connectors/ # Connector scripts for SQLite and Qdrant
|
|
||||||
config.json # Endpoint configuration
|
|
||||||
ticket # Ticket CLI (list, show, create, assign, sprint, etc.)
|
|
||||||
sqlite_connector.py # SQLite mini MCP
|
|
||||||
qdrant_connector.py # Qdrant + ollama mini MCP
|
|
||||||
.claude/
|
|
||||||
agents/ # Agent personality files
|
|
||||||
skills/ # Skill definitions
|
|
||||||
decisions/ # Decision domain files (source of truth)
|
|
||||||
README.md # Domain index and query examples
|
|
||||||
architecture.md # D-008, D-009, D-010, D-012, D-020, D-026, D-030, D-031, D-041, D-042, D-054, D-055, D-066
|
|
||||||
perception.md # D-011, D-015, D-016, D-017, D-018, D-019, D-033, D-035, D-043-D-049, D-052, D-056-D-061
|
|
||||||
content.md # D-023, D-024, D-025, D-028, D-029, D-032, D-034-D-037, D-050, D-062-D-064
|
|
||||||
scope.md # D-001, D-003, D-005, D-006, D-007, D-013, D-014, D-027, D-038, D-039, D-051, D-053, D-065
|
|
||||||
process.md # D-004, D-021, D-022
|
|
||||||
questions.md # Q-001 through Q-011
|
|
||||||
rejected.md # R-001 through R-010
|
|
||||||
DECISIONS.md # Redirect to decisions/ directory
|
|
||||||
TEAM.md # Team roster and roles
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Full annotated tree: `.claude/rules/project-structure.md`
|
||||||
|
|
||||||
## DevOps
|
## DevOps
|
||||||
|
|
||||||
See [docs/DEVOPS.md](docs/DEVOPS.md) for build, test, lint, and CI procedures. All development operations go through the top-level `Makefile` — run `make` for a summary of targets.
|
See [docs/DEVOPS.md](docs/DEVOPS.md) for build, test, lint, and CI procedures. All development operations go through the top-level `Makefile` — run `make` for a summary of targets.
|
||||||
|
|
||||||
## Agent Instructions
|
## Agent Instructions
|
||||||
|
|
||||||
### Worktree boundaries
|
### Team boundaries
|
||||||
|
|
||||||
This project uses **git worktrees** in a shared parent directory (`settled-reach/`). Each team branch (`server`, `client`, `copy`, `audio`, `visual`, `ci`) is checked out in its own worktree under that parent. The parent directory also contains shared resources like the ticketing database.
|
**Your team identity is `$WORKTREE_TEAM`.** All work must stay within the current working directory.
|
||||||
|
|
||||||
Each worktree contains the full repository: `server/` (Rust backend), `client/` (Godot client), `docs/`, `decisions/`, etc. The worktree root IS the git root — use `git rev-parse --show-toplevel` if in doubt.
|
- All file paths are relative to the current working directory (e.g. `server/src/bridge/types.rs`).
|
||||||
|
- **Do NOT navigate to parent or sibling directories** (`../`, `../client/`, etc.) unless explicitly instructed. Do NOT use absolute paths to reach other team directories.
|
||||||
Unless there is a direct instruction or a functional need (e.g. accessing the shared database in the parent directory), **all work must remain within the scope of the git root Claude is running in.**
|
- **Do NOT write auto-memory files for other teams.** If `$WORKTREE_TEAM` is `server`, do not write to memory paths containing `client`, `main`, etc.
|
||||||
|
- For context: each team has its own directory via git worktrees, sharing a parent directory (`settled-reach/`). The `.git` file points to a shared git directory — do not follow it to determine your working root.
|
||||||
- All file paths are relative to the worktree/git root (e.g. `server/src/bridge/types.rs`, `client/scripts/rendering/fog.gd`).
|
- **Exception — stale git lock files:** If a `git` command fails with `index.lock: File exists`, you may remove the lock file for **your own team only** (e.g. `main/.git/worktrees/$WORKTREE_TEAM/index.lock`). Never touch lock files belonging to other teams.
|
||||||
- Do not navigate to or access sibling worktrees in the parent directory (`../client/`, `../copy/`, etc.) unless explicitly instructed.
|
|
||||||
- Do not navigate above the git root unless explicitly instructed.
|
|
||||||
|
|
||||||
### Database
|
### Database
|
||||||
|
|
||||||
@@ -69,107 +43,36 @@ The ticketing database (`settledreach.db`) lives in the **parent directory** sha
|
|||||||
|
|
||||||
### Before starting work
|
### Before starting work
|
||||||
1. Read your sprint briefing at `docs/sprints/sprint-N/{team}.md` for current tasks
|
1. Read your sprint briefing at `docs/sprints/sprint-N/{team}.md` for current tasks
|
||||||
2. Use `db/connectors/ticket show <id>` for full ticket details
|
2. Use `tooling/db/ticket show <id>` for full ticket details
|
||||||
3. Read the relevant `decisions/*.md` domain file(s) referenced in the briefing
|
3. Read the relevant `decisions/*.md` domain file(s) referenced in the briefing
|
||||||
4. Background context: `docs/briefings/{your-name}.md`, `docs/discussions/`
|
4. Background context: `docs/briefings/{your-name}.md`, `docs/discussions/`
|
||||||
|
|
||||||
### Ticket and database access
|
### CLI tools
|
||||||
**Prefer the ticket CLI over raw SQL.** The CLI handles column names, joins, and output formatting correctly:
|
|
||||||
```bash
|
|
||||||
db/connectors/ticket list --sprint 2 --team server
|
|
||||||
db/connectors/ticket show 78
|
|
||||||
db/connectors/ticket sprint --active
|
|
||||||
```
|
|
||||||
|
|
||||||
### Sprint CLI
|
**Prefer CLI wrappers over raw SQL.** Never use the `sqlite3` CLI — it crashes in Claude Code (std::bad_alloc). Use the wrapper scripts instead.
|
||||||
**Use the sprint CLI for sprint-scoped operations.** It batches ticket queries and formats output for agent consumption:
|
|
||||||
```bash
|
|
||||||
db/connectors/sprint status # Current sprint progress
|
|
||||||
db/connectors/sprint status --team server # Team-scoped view
|
|
||||||
db/connectors/sprint start-work --team client # Full context dump for starting work
|
|
||||||
db/connectors/sprint prepare # Prepare next sprint (candidates + gaps)
|
|
||||||
db/connectors/sprint start # Activate a planned sprint
|
|
||||||
db/connectors/sprint stop # Complete an active sprint
|
|
||||||
```
|
|
||||||
Team is auto-detected from the current git branch (if not `main`). Sprint is auto-detected from DB state.
|
|
||||||
|
|
||||||
Only fall back to raw SQL for queries the CLI doesn't support. **Never use the `sqlite3` CLI** — it crashes in Claude Code due to a known std::bad_alloc bug. Use the wrapper scripts instead:
|
| Tool | Command | Full reference |
|
||||||
```bash
|
|------|---------|----------------|
|
||||||
db/connectors/sqlite-query "SELECT * FROM tickets WHERE status='in_progress'"
|
| Tickets | `tooling/db/ticket list`, `show`, `create`, `assign` | `/ticket` skill |
|
||||||
db/connectors/sqlite-exec "UPDATE tickets SET status='done' WHERE id=1"
|
| Sprints | `tooling/db/sprint status`, `start-work`, `prepare` | `/sprint-start` skill |
|
||||||
```
|
| SQL queries | `tooling/db/sqlite-query "SELECT ..."` | — |
|
||||||
|
| SQL writes | `tooling/db/sqlite-exec "UPDATE ..."` | — |
|
||||||
|
| Decisions | `tooling/db/decision next`, `claim`, `check-dupes` | — |
|
||||||
|
| Doc search | `tooling/db/qdrant-search "query"` | `/docs-search` skill |
|
||||||
|
| Doc index | `tooling/db/qdrant-index path/to/file.md` | `/docs-search` skill |
|
||||||
|
|
||||||
### Qdrant / document search
|
### Testing preferences
|
||||||
```bash
|
|
||||||
db/connectors/qdrant-search "asymmetric information design"
|
|
||||||
db/connectors/qdrant-index docs/briefings/tyre.md
|
|
||||||
db/connectors/qdrant-health
|
|
||||||
db/connectors/qdrant-count
|
|
||||||
```
|
|
||||||
|
|
||||||
### Gitea access (tea CLI)
|
- **Prefer live Gauntlet testing over mocks.** For visual tests and rendering verification, use the full client/server pipeline (`--test-mode` + `SR_LIVE=1`) instead of TestHarness mocks. The Gauntlet test world produces production-identical data. Mocks can mask rendering bugs by taking different code paths.
|
||||||
**Never access the Gitea API directly** — use the `tea` CLI with all required flags to bypass interactive mode.
|
- **Gauntlet rooms are immutable.** Never modify existing rooms — new systems get new rooms. This ensures StableId determinism and fixture stability.
|
||||||
|
- Three test tiers: (1) Live server — highest fidelity, (2) MessagePack replay via `Protocol.decode_snapshot()` — for unreachable rooms, (3) TestHarness mock — for UI-only tests where fog data doesn't matter.
|
||||||
Always pass `--login schweitz --repo jpmschweitzer/settled-reach --output simple` to avoid TTY prompts.
|
- `make fixtures-gauntlet` regenerates real server snapshot fixtures from the Gauntlet world.
|
||||||
|
|
||||||
```bash
|
|
||||||
# List open PRs
|
|
||||||
tea pr list --login schweitz --repo jpmschweitzer/settled-reach --state open --output simple
|
|
||||||
|
|
||||||
# View a PR with comments
|
|
||||||
tea pr --login schweitz --repo jpmschweitzer/settled-reach --comments -o simple <PR_NUMBER>
|
|
||||||
|
|
||||||
# Post a comment on a PR (or issue)
|
|
||||||
tea comment --login schweitz --repo jpmschweitzer/settled-reach <NUMBER> "comment body"
|
|
||||||
|
|
||||||
# Approve a PR
|
|
||||||
tea pr approve --login schweitz --repo jpmschweitzer/settled-reach <PR_NUMBER>
|
|
||||||
|
|
||||||
# List issues
|
|
||||||
tea issue list --login schweitz --repo jpmschweitzer/settled-reach --state open --output simple
|
|
||||||
```
|
|
||||||
|
|
||||||
Key rules:
|
|
||||||
- **All flags must be explicit** — omitting `--login` or `--repo` triggers interactive prompts that crash in Claude Code (no TTY)
|
|
||||||
- **Use `--output simple`** for machine-readable output (no table borders)
|
|
||||||
- **`tea pr reject` does not work on your own PRs** — use `tea comment` instead
|
|
||||||
- **Never delete protected branches:** `main`, `maintenance`, `server`, `client`, `copy`, `audio`, `visual`, `ci` are protected on Gitea. Do not use `tea pr clean`, `git push --delete`, or `git branch -D` on these branches.
|
|
||||||
|
|
||||||
### File conventions
|
### File conventions
|
||||||
- Decisions: domain files in `decisions/` (see `decisions/README.md` for index)
|
- Decisions: domain files in `decisions/` (see `decisions/README.md` for index)
|
||||||
- Decision IDs: `D-NNN` (confirmed), `Q-NNN` (open questions), `R-NNN` (rejected)
|
- Decision IDs: `D-NNN` (confirmed), `Q-NNN` (open questions), `R-NNN` (rejected)
|
||||||
|
- **Claim IDs before writing:** `tooling/db/decision claim D <domain> "title"` — prevents ID collisions across worktrees
|
||||||
|
- Diagrams: `.d2` source + `.png` renders in `docs/diagrams/{category}/`. Create or update diagrams via `/d2-diagram` when D-records are added or modified.
|
||||||
- Discussion rounds: numbered sequentially, archived to `docs/discussions/` when complete
|
- Discussion rounds: numbered sequentially, archived to `docs/discussions/` when complete
|
||||||
- Briefings: one per agent, updated after decision-producing rounds
|
- Briefings: one per agent, updated after decision-producing rounds
|
||||||
- Tickets: managed via `db/connectors/ticket` CLI or `/ticket` skill
|
- Tickets: managed via `tooling/db/ticket` CLI or `/ticket` skill
|
||||||
|
|
||||||
### Commit conventions
|
|
||||||
Use conventional commits with project-specific scopes:
|
|
||||||
`agents`, `skills`, `docs`, `briefings`, `discussions`, `schema`, `db`, `config`, `engine`, `simulation`, `client`, `ui`, `audio`, `assets`, `meta`
|
|
||||||
|
|
||||||
### Pull requests
|
|
||||||
**Use `tea` (Gitea CLI), not `gh` (GitHub CLI).** The remote is Gitea at `git.schweitz.internal`.
|
|
||||||
|
|
||||||
Always provide all required flags to ensure non-interactive execution:
|
|
||||||
```bash
|
|
||||||
tea pr create \
|
|
||||||
--repo jpmschweitzer/settled-reach \
|
|
||||||
--login schweitz \
|
|
||||||
--title "feat(scope): short description" \
|
|
||||||
--description "PR body here" \
|
|
||||||
--base main \
|
|
||||||
--head branch-name
|
|
||||||
```
|
|
||||||
|
|
||||||
### Large content pushes (team pattern)
|
|
||||||
When producing many files (wiki pages, content batches, bulk docs):
|
|
||||||
1. **Lore librarian** agent (read-only): ingests all source material, answers focused context queries from writers, tracks cross-file consistency
|
|
||||||
2. **Multiple writer** agents (parallel, by domain): each gets a task slice, writes directly to disk using the Write tool — one file at a time, write often, no text accumulation
|
|
||||||
3. **Reviewer** agents (blocked until writing done): check voice consistency, attribute uniformity, style
|
|
||||||
|
|
||||||
Key: writers use Write tool directly (no transcription bottleneck), librarian catches contradictions early, split work by domain not volume.
|
|
||||||
|
|
||||||
### Local services
|
|
||||||
- Gitea: `http://git.schweitz.internal` (login: `schweitz`)
|
|
||||||
- Qdrant: `http://tower-of-joy:6333/`
|
|
||||||
- Ollama: `http://tower-of-joy:11434/` (nomic-embed-text)
|
|
||||||
- Collection: `commonwealth` (768 dimensions, cosine distance)
|
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
GODOT := $(shell command -v godot4 2>/dev/null || command -v godot 2>/dev/null)
|
GODOT := $(shell command -v godot4 2>/dev/null || command -v godot 2>/dev/null)
|
||||||
|
|
||||||
.PHONY: help setup build client server game stop test lint ci ci-client ci-server clean \
|
.PHONY: help setup build check-protocol client server game stop test lint ci ci-client ci-server clean \
|
||||||
decisions-sync decisions-coverage decisions-active decisions-orphan \
|
decisions-sync decisions-coverage decisions-active decisions-orphan \
|
||||||
db-backup db-install validate-content content-ron check-fact-ids setup-hooks \
|
db-backup db-install validate-content content-ron check-fact-ids setup-hooks \
|
||||||
pre-pr pre-pr-lint pre-pr-build pre-pr-test pre-pr-validate pre-pr-fixtures \
|
pre-pr pre-pr-lint pre-pr-build pre-pr-test pre-pr-validate pre-pr-fixtures \
|
||||||
pre-pr-server pre-pr-client pre-pr-content \
|
pre-pr-server pre-pr-client pre-pr-content \
|
||||||
fixtures-client golden-diff golden-update \
|
fixtures-client golden-diff golden-update \
|
||||||
checklist-validate checklist-generate \
|
checklist-validate checklist-generate \
|
||||||
perf-baseline
|
perf-baseline debug-schedule \
|
||||||
|
test-ipc-fixtures test-ipc-protocol test-ipc-integration test-ipc-benchmark \
|
||||||
|
screenshot visual-movie test-visual visual-update
|
||||||
|
|
||||||
# --- Configuration ---
|
# --- Configuration ---
|
||||||
|
|
||||||
@@ -23,11 +25,16 @@ help:
|
|||||||
@echo " make stop Stop any running server instance"
|
@echo " make stop Stop any running server instance"
|
||||||
@echo " make client Run the Godot client (test mode)"
|
@echo " make client Run the Godot client (test mode)"
|
||||||
@echo " make server Run the Rust simulation server"
|
@echo " make server Run the Rust simulation server"
|
||||||
@echo " make test Run all tests"
|
@echo " make test Run all tests"
|
||||||
@echo " make lint Run all linters"
|
@echo " make test-ipc-fixtures Layer 1: IPC serialization fixtures"
|
||||||
@echo " make ci Run full CI pipeline locally"
|
@echo " make test-ipc-protocol Layer 2: mock IPC protocol tests"
|
||||||
@echo " make ci-client Run client CI checks"
|
@echo " make test-ipc-integration Layer 3: real subprocess round-trip"
|
||||||
@echo " make ci-server Run server CI checks"
|
@echo " make test-ipc-benchmark IPC latency benchmark (blocked: #555/#556)"
|
||||||
|
@echo " make lint Run all linters"
|
||||||
|
@echo " make ci Run full CI pipeline locally"
|
||||||
|
@echo " make ci-client Run client CI checks"
|
||||||
|
@echo " make ci-server Run server CI checks"
|
||||||
|
@echo " make check-protocol Verify server/client protocol versions match"
|
||||||
@echo " make clean Remove build artifacts and caches"
|
@echo " make clean Remove build artifacts and caches"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo " make db-backup Backup shared database to git (main only)"
|
@echo " make db-backup Backup shared database to git (main only)"
|
||||||
@@ -47,12 +54,18 @@ help:
|
|||||||
@echo " make checklist-generate Validate checklists + print condition summary"
|
@echo " make checklist-generate Validate checklists + print condition summary"
|
||||||
@echo " make perf-baseline Run performance benchmarks and save baseline"
|
@echo " make perf-baseline Run performance benchmarks and save baseline"
|
||||||
@echo ""
|
@echo ""
|
||||||
|
@echo " make screenshot Ad-hoc visual capture (SCENARIO=name, default: fog_3state)"
|
||||||
|
@echo " make visual-movie Flow capture with contact sheet (FLOW=name)"
|
||||||
|
@echo " make test-visual Run visual golden regression tests"
|
||||||
|
@echo " make visual-update Regenerate visual goldens and stage for commit"
|
||||||
|
@echo ""
|
||||||
@echo " make pre-pr Run all pre-PR checks (lint, build, test, validate, fixtures)"
|
@echo " make pre-pr Run all pre-PR checks (lint, build, test, validate, fixtures)"
|
||||||
@echo " make pre-pr-server Server-scoped pre-PR (lint, build, test, fixtures)"
|
@echo " make pre-pr-server Server-scoped pre-PR (lint, build, test, fixtures)"
|
||||||
@echo " make pre-pr-client Client-scoped pre-PR (lint, build, test)"
|
@echo " make pre-pr-client Client-scoped pre-PR (lint, build, test)"
|
||||||
@echo " make pre-pr-content Content-scoped pre-PR (schema + cross-ref validation)"
|
@echo " make pre-pr-content Content-scoped pre-PR (schema + cross-ref validation)"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo " make setup-hooks Install pre-commit hooks (included in setup)"
|
@echo " make setup-hooks Install pre-commit hooks (included in setup)"
|
||||||
|
@echo " make debug-schedule Print bevy_ecs schedule graph (diff for PR artifacts)"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo " GODOT_VERSION=4.6 make setup Override Godot version"
|
@echo " GODOT_VERSION=4.6 make setup Override Godot version"
|
||||||
|
|
||||||
@@ -81,7 +94,16 @@ setup-hooks:
|
|||||||
|
|
||||||
# --- Build ---
|
# --- Build ---
|
||||||
|
|
||||||
build: build-server build-client
|
check-protocol:
|
||||||
|
@SERVER_V=$$(grep 'pub const PROTOCOL_VERSION' server/src/bridge/types.rs | sed 's/.*= *//;s/[^0-9]//g'); \
|
||||||
|
CLIENT_V=$$(grep 'const PROTOCOL_VERSION' client/scripts/protocol/protocol.gd | sed 's/.*= *//;s/[^0-9]//g'); \
|
||||||
|
if [ "$$SERVER_V" != "$$CLIENT_V" ]; then \
|
||||||
|
echo "ERROR: Protocol version mismatch — server=$$SERVER_V, client=$$CLIENT_V"; \
|
||||||
|
echo " Fix: update client/scripts/protocol/protocol.gd to match server/src/bridge/types.rs"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
build: check-protocol build-server build-client
|
||||||
|
|
||||||
build-server:
|
build-server:
|
||||||
cd server && cargo build
|
cd server && cargo build
|
||||||
@@ -93,7 +115,7 @@ build-client:
|
|||||||
# --- Run ---
|
# --- Run ---
|
||||||
|
|
||||||
server:
|
server:
|
||||||
cd server && cargo run
|
cd server && cargo run --bin settled-reach-server
|
||||||
|
|
||||||
client:
|
client:
|
||||||
@test -n "$(GODOT)" || { echo "Godot not found. Run 'make setup' first."; exit 1; }
|
@test -n "$(GODOT)" || { echo "Godot not found. Run 'make setup' first."; exit 1; }
|
||||||
@@ -102,7 +124,7 @@ client:
|
|||||||
game: stop build
|
game: stop build
|
||||||
@test -n "$(GODOT)" || { echo "Godot not found. Run 'make setup' first."; exit 1; }
|
@test -n "$(GODOT)" || { echo "Godot not found. Run 'make setup' first."; exit 1; }
|
||||||
@echo "Starting server..."
|
@echo "Starting server..."
|
||||||
@cd server && cargo run &
|
@cd server && cargo run --bin settled-reach-server &
|
||||||
@sleep 2
|
@sleep 2
|
||||||
@echo "Starting client..."
|
@echo "Starting client..."
|
||||||
@SR_LIVE=1 $(GODOT) --path client
|
@SR_LIVE=1 $(GODOT) --path client
|
||||||
@@ -117,7 +139,7 @@ stop:
|
|||||||
test: test-server test-client
|
test: test-server test-client
|
||||||
|
|
||||||
test-server:
|
test-server:
|
||||||
cd server && cargo nextest run
|
tests/run-rust
|
||||||
|
|
||||||
fixtures:
|
fixtures:
|
||||||
cd server && cargo test --test gen_fixtures -- --ignored
|
cd server && cargo test --test gen_fixtures -- --ignored
|
||||||
@@ -158,8 +180,19 @@ golden-update:
|
|||||||
@echo "Review with: git diff --cached -- server/tests/golden/"
|
@echo "Review with: git diff --cached -- server/tests/golden/"
|
||||||
|
|
||||||
test-client:
|
test-client:
|
||||||
@test -n "$(GODOT)" || { echo "Godot not found. Run 'make setup' first."; exit 1; }
|
tests/run-godot
|
||||||
$(GODOT) --headless --path client -s res://addons/gdUnit4/bin/GdUnitCmdTool.gd --ignoreHeadlessMode -a res://tests/
|
|
||||||
|
test-ipc-fixtures:
|
||||||
|
tests/run-ipc-fixtures
|
||||||
|
|
||||||
|
test-ipc-protocol:
|
||||||
|
tests/run-ipc-protocol
|
||||||
|
|
||||||
|
test-ipc-integration:
|
||||||
|
tests/run-ipc-integration
|
||||||
|
|
||||||
|
test-ipc-benchmark:
|
||||||
|
tests/run-ipc-benchmark
|
||||||
|
|
||||||
# --- Lint ---
|
# --- Lint ---
|
||||||
|
|
||||||
@@ -257,16 +290,16 @@ db-install:
|
|||||||
# --- Decisions ---
|
# --- Decisions ---
|
||||||
|
|
||||||
decisions-sync:
|
decisions-sync:
|
||||||
@db/connectors/decisions-sync
|
@tooling/db/decisions-sync
|
||||||
|
|
||||||
decisions-coverage:
|
decisions-coverage:
|
||||||
@db/connectors/sqlite-query "SELECT d.domain, COUNT(DISTINCT d.id) as decisions, COUNT(DISTINCT t.decision_ref) as with_tickets FROM decisions d LEFT JOIN tickets t ON d.id = t.decision_ref WHERE d.status='active' AND d.type='confirmed' GROUP BY d.domain"
|
@tooling/db/sqlite-query "SELECT d.domain, COUNT(DISTINCT d.id) as decisions, COUNT(DISTINCT t.decision_ref) as with_tickets FROM decisions d LEFT JOIN tickets t ON d.id = t.decision_ref WHERE d.status='active' AND d.type='confirmed' GROUP BY d.domain"
|
||||||
|
|
||||||
decisions-active:
|
decisions-active:
|
||||||
@db/connectors/sqlite-query "SELECT id, domain, title FROM decisions WHERE status='active' AND type='confirmed' ORDER BY domain, id"
|
@tooling/db/sqlite-query "SELECT id, domain, title FROM decisions WHERE status='active' AND type='confirmed' ORDER BY domain, id"
|
||||||
|
|
||||||
decisions-orphan:
|
decisions-orphan:
|
||||||
@db/connectors/sqlite-query "SELECT id, title FROM decisions WHERE type='confirmed' AND status='active' AND id NOT IN (SELECT DISTINCT decision_ref FROM tickets WHERE decision_ref IS NOT NULL)"
|
@tooling/db/sqlite-query "SELECT id, title FROM decisions WHERE type='confirmed' AND status='active' AND id NOT IN (SELECT DISTINCT decision_ref FROM tickets WHERE decision_ref IS NOT NULL)"
|
||||||
|
|
||||||
# --- Content Validation ---
|
# --- Content Validation ---
|
||||||
|
|
||||||
@@ -285,6 +318,26 @@ checklist-generate:
|
|||||||
perf-baseline:
|
perf-baseline:
|
||||||
@tooling/perf-baseline
|
@tooling/perf-baseline
|
||||||
|
|
||||||
|
# --- Schedule debug (#346) ---
|
||||||
|
|
||||||
|
debug-schedule:
|
||||||
|
@echo "Dumping bevy_ecs schedule graph..."
|
||||||
|
@cd server && cargo run --bin settled-reach-server -- --dump-schedule
|
||||||
|
|
||||||
|
# --- Visual test harness ---
|
||||||
|
|
||||||
|
screenshot:
|
||||||
|
@tests/run-visual --screenshot $(SCENARIO)
|
||||||
|
|
||||||
|
visual-movie:
|
||||||
|
@tests/run-visual --movie $(FLOW)
|
||||||
|
|
||||||
|
test-visual:
|
||||||
|
@tests/run-visual
|
||||||
|
|
||||||
|
visual-update:
|
||||||
|
@tests/run-visual --update
|
||||||
|
|
||||||
content-ron:
|
content-ron:
|
||||||
cd tooling/content-converter && cargo build --release
|
cd tooling/content-converter && cargo build --release
|
||||||
tooling/content-converter/target/release/content-converter --input content --output content-ron --verbose
|
tooling/content-converter/target/release/content-converter --input content --output content-ron --verbose
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="oggvorbisstr"
|
||||||
|
type="AudioStreamOggVorbis"
|
||||||
|
uid="uid://dfy0ye3srawos"
|
||||||
|
path="res://.godot/imported/amb_bar_layer.ogg-8e32a9c679a33f27226744a176d7d405.oggvorbisstr"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/audio/amb_bar_layer.ogg"
|
||||||
|
dest_files=["res://.godot/imported/amb_bar_layer.ogg-8e32a9c679a33f27226744a176d7d405.oggvorbisstr"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
loop=false
|
||||||
|
loop_offset=0
|
||||||
|
bpm=0
|
||||||
|
beat_count=0
|
||||||
|
bar_beats=4
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="oggvorbisstr"
|
||||||
|
type="AudioStreamOggVorbis"
|
||||||
|
uid="uid://cne005dbmwt6d"
|
||||||
|
path="res://.godot/imported/amb_corridor_layer.ogg-f14ba54010b129b65b0d248444331998.oggvorbisstr"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/audio/amb_corridor_layer.ogg"
|
||||||
|
dest_files=["res://.godot/imported/amb_corridor_layer.ogg-f14ba54010b129b65b0d248444331998.oggvorbisstr"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
loop=false
|
||||||
|
loop_offset=0
|
||||||
|
bpm=0
|
||||||
|
beat_count=0
|
||||||
|
bar_beats=4
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="oggvorbisstr"
|
||||||
|
type="AudioStreamOggVorbis"
|
||||||
|
uid="uid://b6ycjdvxpphfa"
|
||||||
|
path="res://.godot/imported/amb_station_base.ogg-8056e0ae231edecc9ec0e49bb90136b7.oggvorbisstr"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/audio/amb_station_base.ogg"
|
||||||
|
dest_files=["res://.godot/imported/amb_station_base.ogg-8056e0ae231edecc9ec0e49bb90136b7.oggvorbisstr"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
loop=false
|
||||||
|
loop_offset=0
|
||||||
|
bpm=0
|
||||||
|
beat_count=0
|
||||||
|
bar_beats=4
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="oggvorbisstr"
|
||||||
|
type="AudioStreamOggVorbis"
|
||||||
|
uid="uid://dw2pokvg5d7v2"
|
||||||
|
path="res://.godot/imported/amb_workplace_layer.ogg-580ba32198a3f6c782381c66f9520e63.oggvorbisstr"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/audio/amb_workplace_layer.ogg"
|
||||||
|
dest_files=["res://.godot/imported/amb_workplace_layer.ogg-580ba32198a3f6c782381c66f9520e63.oggvorbisstr"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
loop=false
|
||||||
|
loop_offset=0
|
||||||
|
bpm=0
|
||||||
|
beat_count=0
|
||||||
|
bar_beats=4
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="oggvorbisstr"
|
||||||
|
type="AudioStreamOggVorbis"
|
||||||
|
uid="uid://da81bx5y6iw87"
|
||||||
|
path="res://.godot/imported/sfx_footstep_metal_run.ogg-0d448f29204d35d4133f5314a179d054.oggvorbisstr"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/audio/sfx_footstep_metal_run.ogg"
|
||||||
|
dest_files=["res://.godot/imported/sfx_footstep_metal_run.ogg-0d448f29204d35d4133f5314a179d054.oggvorbisstr"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
loop=false
|
||||||
|
loop_offset=0
|
||||||
|
bpm=0
|
||||||
|
beat_count=0
|
||||||
|
bar_beats=4
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="oggvorbisstr"
|
||||||
|
type="AudioStreamOggVorbis"
|
||||||
|
uid="uid://d14dx3q0qd483"
|
||||||
|
path="res://.godot/imported/sfx_footstep_metal_walk.ogg-af30f14f9f80059ea65ad6e78234cc05.oggvorbisstr"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/audio/sfx_footstep_metal_walk.ogg"
|
||||||
|
dest_files=["res://.godot/imported/sfx_footstep_metal_walk.ogg-af30f14f9f80059ea65ad6e78234cc05.oggvorbisstr"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
loop=false
|
||||||
|
loop_offset=0
|
||||||
|
bpm=0
|
||||||
|
beat_count=0
|
||||||
|
bar_beats=4
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="oggvorbisstr"
|
||||||
|
type="AudioStreamOggVorbis"
|
||||||
|
uid="uid://cow7symyvpmal"
|
||||||
|
path="res://.godot/imported/sfx_npc_murmur.ogg-bfd7592cfea1b592d89f107b6cd33838.oggvorbisstr"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/audio/sfx_npc_murmur.ogg"
|
||||||
|
dest_files=["res://.godot/imported/sfx_npc_murmur.ogg-bfd7592cfea1b592d89f107b6cd33838.oggvorbisstr"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
loop=false
|
||||||
|
loop_offset=0
|
||||||
|
bpm=0
|
||||||
|
beat_count=0
|
||||||
|
bar_beats=4
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="font_data_dynamic"
|
||||||
|
type="FontFile"
|
||||||
|
uid="uid://qbg8rfnetlqn"
|
||||||
|
path="res://.godot/imported/Michroma-Regular.ttf-928de7d8513fc5249047ef3681175fc2.fontdata"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/fonts/Michroma-Regular.ttf"
|
||||||
|
dest_files=["res://.godot/imported/Michroma-Regular.ttf-928de7d8513fc5249047ef3681175fc2.fontdata"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
Rendering=null
|
||||||
|
antialiasing=1
|
||||||
|
generate_mipmaps=false
|
||||||
|
disable_embedded_bitmaps=true
|
||||||
|
multichannel_signed_distance_field=false
|
||||||
|
msdf_pixel_range=8
|
||||||
|
msdf_size=48
|
||||||
|
allow_system_fallback=true
|
||||||
|
force_autohinter=false
|
||||||
|
modulate_color_glyphs=false
|
||||||
|
hinting=1
|
||||||
|
subpixel_positioning=4
|
||||||
|
keep_rounding_remainders=true
|
||||||
|
oversampling=0.0
|
||||||
|
Fallbacks=null
|
||||||
|
fallbacks=[]
|
||||||
|
Compress=null
|
||||||
|
compress=true
|
||||||
|
preload=[]
|
||||||
|
language_support={}
|
||||||
|
script_support={}
|
||||||
|
opentype_features={}
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://850hx6cd5kx7"
|
||||||
|
path="res://.godot/imported/npc_generic_east_64.png-bf22e76d70922112e99bf747d85a0a04.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/sprites/npc_generic_east_64.png"
|
||||||
|
dest_files=["res://.godot/imported/npc_generic_east_64.png-bf22e76d70922112e99bf747d85a0a04.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://bx6s0yglmpt2l"
|
||||||
|
path="res://.godot/imported/npc_generic_north_64.png-296e233a60c8b9efed025a82a69614df.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/sprites/npc_generic_north_64.png"
|
||||||
|
dest_files=["res://.godot/imported/npc_generic_north_64.png-296e233a60c8b9efed025a82a69614df.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dpsuq148mksls"
|
||||||
|
path="res://.godot/imported/npc_generic_south_64.png-c121e02d806f6dcc3ed440484827c258.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/sprites/npc_generic_south_64.png"
|
||||||
|
dest_files=["res://.godot/imported/npc_generic_south_64.png-c121e02d806f6dcc3ed440484827c258.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dgt84hobbdumx"
|
||||||
|
path="res://.godot/imported/npc_generic_west_64.png-9ee237384537f7357d37802b6cfae559.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/sprites/npc_generic_west_64.png"
|
||||||
|
dest_files=["res://.godot/imported/npc_generic_west_64.png-9ee237384537f7357d37802b6cfae559.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://bcmer1bsugk8b"
|
||||||
|
path="res://.godot/imported/wall_structural_east_64.png-f868b267dcc1824e2b0fe213e49b4996.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/sprites/wall_structural_east_64.png"
|
||||||
|
dest_files=["res://.godot/imported/wall_structural_east_64.png-f868b267dcc1824e2b0fe213e49b4996.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dd5j0q6cu674m"
|
||||||
|
path="res://.godot/imported/wall_structural_north_64.png-f7eceb6d561e7e07b3bac2e28c659628.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/sprites/wall_structural_north_64.png"
|
||||||
|
dest_files=["res://.godot/imported/wall_structural_north_64.png-f7eceb6d561e7e07b3bac2e28c659628.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://cctlhvaolom3x"
|
||||||
|
path="res://.godot/imported/wall_structural_south_64.png-c34925f9a5a3f6b5968e73e51953367e.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/sprites/wall_structural_south_64.png"
|
||||||
|
dest_files=["res://.godot/imported/wall_structural_south_64.png-c34925f9a5a3f6b5968e73e51953367e.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://ughjt5s8la2p"
|
||||||
|
path="res://.godot/imported/wall_structural_west_64.png-7c39e0c692d32348190875c4ad99b227.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/sprites/wall_structural_west_64.png"
|
||||||
|
dest_files=["res://.godot/imported/wall_structural_west_64.png-7c39e0c692d32348190875c4ad99b227.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# Dialogue Log Theme — The Settled Reach
|
||||||
|
#
|
||||||
|
# Ticket: #535 | Sprint: 14
|
||||||
|
# Colors and timing for the unified dialogue log panel.
|
||||||
|
# Loaded by dialogue_box.gd at runtime.
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# PLAYER COLOR
|
||||||
|
# Fixed color for the player's name in dialogue log entries.
|
||||||
|
# ============================================================
|
||||||
|
player_color: "#e0e8ff"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# NPC COLOR PALETTE
|
||||||
|
# 8 distinct colors for NPC names. Indexed by hash(npc_name) % 8.
|
||||||
|
# Must be readable on a dark semi-transparent panel background.
|
||||||
|
# ============================================================
|
||||||
|
npc_colors:
|
||||||
|
0: "#4a9ebb" # teal
|
||||||
|
1: "#6bc9a6" # green
|
||||||
|
2: "#e8c547" # amber
|
||||||
|
3: "#d49e5d" # warm orange
|
||||||
|
4: "#b586d4" # lavender
|
||||||
|
5: "#d45d5d" # muted red
|
||||||
|
6: "#5daa7d" # forest
|
||||||
|
7: "#7daccc" # sky blue
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# TEXT COLORS
|
||||||
|
# Arrow separator and speech text color.
|
||||||
|
# ============================================================
|
||||||
|
arrow_color: "#8890a0"
|
||||||
|
speech_color: "#c8d0e0"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# PASSIVE (OVERHEARD) OPACITY
|
||||||
|
# Base opacity multiplier for non-player-centric lines.
|
||||||
|
# 1.0 = full opacity, 0.0 = invisible.
|
||||||
|
# ============================================================
|
||||||
|
passive_opacity: 0.9
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# ENTRY TIMING
|
||||||
|
# All entry types share the same lifetime and fade duration.
|
||||||
|
# ============================================================
|
||||||
|
entry_lifetime_seconds: 45.0
|
||||||
|
entry_fade_seconds: 5.0
|
||||||
@@ -104,8 +104,12 @@ notifications:
|
|||||||
|
|
||||||
# System
|
# System
|
||||||
save_complete: "Progress saved."
|
save_complete: "Progress saved."
|
||||||
|
load_complete: "Session restored."
|
||||||
|
save_failed: "Save failed."
|
||||||
|
load_failed: "Load failed."
|
||||||
connection_lost: "Signal interrupted."
|
connection_lost: "Signal interrupted."
|
||||||
connection_restored: "Signal restored."
|
connection_restored: "Signal restored."
|
||||||
|
loading: "Resuming..."
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# KNOWLEDGE PANEL LABELS
|
# KNOWLEDGE PANEL LABELS
|
||||||
@@ -125,6 +129,17 @@ knowledge_panel:
|
|||||||
confidence_medium: "Likely"
|
confidence_medium: "Likely"
|
||||||
confidence_low: "Unconfirmed"
|
confidence_low: "Unconfirmed"
|
||||||
confidence_rumor: "Hearsay"
|
confidence_rumor: "Hearsay"
|
||||||
|
# D-041 KnowledgeConfidence levels — displayed in journal panel
|
||||||
|
confidence_direct: "Confirmed"
|
||||||
|
confidence_knowsdetails: "Detailed"
|
||||||
|
confidence_knowsof: "Known"
|
||||||
|
confidence_suspects: "Unconfirmed"
|
||||||
|
# D-041 KnowledgeSource labels — displayed in journal panel
|
||||||
|
source_directobservation: "Observed"
|
||||||
|
source_toldby: "Told"
|
||||||
|
source_heard: "Overheard"
|
||||||
|
source_inferred: "Inferred"
|
||||||
|
source_background: "Prior"
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# TUTORIAL TEXT (DIEGETIC)
|
# TUTORIAL TEXT (DIEGETIC)
|
||||||
@@ -159,6 +174,8 @@ dialogue:
|
|||||||
menu:
|
menu:
|
||||||
pause_title: "Paused"
|
pause_title: "Paused"
|
||||||
resume: "Resume"
|
resume: "Resume"
|
||||||
|
new_game: "New Game"
|
||||||
|
continue: "Continue"
|
||||||
settings: "Settings"
|
settings: "Settings"
|
||||||
save_game: "Save"
|
save_game: "Save"
|
||||||
load_game: "Load"
|
load_game: "Load"
|
||||||
@@ -167,6 +184,9 @@ menu:
|
|||||||
confirm_quit: "Unsaved progress will be lost."
|
confirm_quit: "Unsaved progress will be lost."
|
||||||
confirm_yes: "Yes"
|
confirm_yes: "Yes"
|
||||||
confirm_no: "No"
|
confirm_no: "No"
|
||||||
|
load_game_browse: "LOAD GAME"
|
||||||
|
load_game_back: "BACK"
|
||||||
|
load_game_empty: "No saves found."
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
audio_volume: "Volume"
|
audio_volume: "Volume"
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
[gd_resource type="AudioBusLayout" format=3]
|
||||||
|
|
||||||
|
; D-068: 5-bus audio architecture — Music, Ambient, WorldSFX, PlayerActions, UISounds.
|
||||||
|
; All buses route to Master. Volumes managed at runtime by AudioManager autoload.
|
||||||
|
; AudioManager._setup_buses() creates any missing buses on startup (no-op if present).
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
bus/0/name = "Master"
|
||||||
|
bus/0/solo = false
|
||||||
|
bus/0/mute = false
|
||||||
|
bus/0/bypass_fx = false
|
||||||
|
bus/0/volume_db = 0.0
|
||||||
|
bus/0/send = &""
|
||||||
|
bus/1/name = "Music"
|
||||||
|
bus/1/solo = false
|
||||||
|
bus/1/mute = false
|
||||||
|
bus/1/bypass_fx = false
|
||||||
|
bus/1/volume_db = 0.0
|
||||||
|
bus/1/send = &"Master"
|
||||||
|
bus/2/name = "Ambient"
|
||||||
|
bus/2/solo = false
|
||||||
|
bus/2/mute = false
|
||||||
|
bus/2/bypass_fx = false
|
||||||
|
bus/2/volume_db = 0.0
|
||||||
|
bus/2/send = &"Master"
|
||||||
|
bus/3/name = "WorldSFX"
|
||||||
|
bus/3/solo = false
|
||||||
|
bus/3/mute = false
|
||||||
|
bus/3/bypass_fx = false
|
||||||
|
bus/3/volume_db = 0.0
|
||||||
|
bus/3/send = &"Master"
|
||||||
|
bus/4/name = "PlayerActions"
|
||||||
|
bus/4/solo = false
|
||||||
|
bus/4/mute = false
|
||||||
|
bus/4/bypass_fx = false
|
||||||
|
bus/4/volume_db = 0.0
|
||||||
|
bus/4/send = &"Master"
|
||||||
|
bus/5/name = "UISounds"
|
||||||
|
bus/5/solo = false
|
||||||
|
bus/5/mute = false
|
||||||
|
bus/5/bypass_fx = false
|
||||||
|
bus/5/volume_db = 0.0
|
||||||
|
bus/5/send = &"Master"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128"><rect width="128" height="128" fill="#1a1a2e"/></svg>
|
||||||
|
After Width: | Height: | Size: 119 B |
@@ -0,0 +1,43 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://caxeq5xayr0iy"
|
||||||
|
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://icon.svg"
|
||||||
|
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
|
svg/scale=1.0
|
||||||
|
editor/scale_with_editor_scale=false
|
||||||
|
editor/convert_colors_with_editor_theme=false
|
||||||
@@ -11,7 +11,7 @@ config_version=5
|
|||||||
[application]
|
[application]
|
||||||
|
|
||||||
config/name="The Settled Reach"
|
config/name="The Settled Reach"
|
||||||
run/main_scene="res://scenes/main.tscn"
|
run/main_scene="res://scenes/main_menu.tscn"
|
||||||
config/features=PackedStringArray("4.6", "GL Compatibility")
|
config/features=PackedStringArray("4.6", "GL Compatibility")
|
||||||
config/icon="res://icon.svg"
|
config/icon="res://icon.svg"
|
||||||
|
|
||||||
@@ -23,6 +23,11 @@ InputMapper="*res://scripts/autoloads/input_mapper.gd"
|
|||||||
UIStrings="*res://scripts/autoloads/ui_strings.gd"
|
UIStrings="*res://scripts/autoloads/ui_strings.gd"
|
||||||
FogState="*res://scripts/autoloads/fog_state.gd"
|
FogState="*res://scripts/autoloads/fog_state.gd"
|
||||||
AudioManager="*res://scripts/autoloads/audio_manager.gd"
|
AudioManager="*res://scripts/autoloads/audio_manager.gd"
|
||||||
|
SessionManager="*res://scripts/autoloads/session_manager.gd"
|
||||||
|
|
||||||
|
[audio]
|
||||||
|
|
||||||
|
buses/default_bus_layout="res://default_bus_layout.tres"
|
||||||
|
|
||||||
[gui]
|
[gui]
|
||||||
|
|
||||||
@@ -116,11 +121,31 @@ bug_report={
|
|||||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194343,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194343,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
debug_overlay={
|
||||||
|
"deadzone": 0.5,
|
||||||
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194334,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
open_journal={
|
||||||
|
"deadzone": 0.5,
|
||||||
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":74,"key_label":0,"unicode":106,"location":0,"echo":false,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
teleport_hub={
|
teleport_hub={
|
||||||
"deadzone": 0.5,
|
"deadzone": 0.5,
|
||||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194317,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194317,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
quicksave={
|
||||||
|
"deadzone": 0.5,
|
||||||
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194336,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
quickload={
|
||||||
|
"deadzone": 0.5,
|
||||||
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194337,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
[rendering]
|
[rendering]
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
[gd_scene load_steps=20 format=3 uid="uid://bswrmh7w8dbgm"]
|
[gd_scene load_steps=28 format=3 uid="uid://bswrmh7w8dbgm"]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://scripts/main.gd" id="1_main"]
|
[ext_resource type="Script" path="res://scripts/main.gd" id="1_main"]
|
||||||
[ext_resource type="Script" path="res://scripts/rendering/world_renderer.gd" id="2_world"]
|
[ext_resource type="Script" path="res://scripts/rendering/world_renderer.gd" id="2_world"]
|
||||||
@@ -16,9 +16,17 @@
|
|||||||
[ext_resource type="PackedScene" path="res://ui/world_radial.tscn" id="14_radial"]
|
[ext_resource type="PackedScene" path="res://ui/world_radial.tscn" id="14_radial"]
|
||||||
[ext_resource type="PackedScene" path="res://ui/dialogue_box.tscn" id="15_dialogue"]
|
[ext_resource type="PackedScene" path="res://ui/dialogue_box.tscn" id="15_dialogue"]
|
||||||
[ext_resource type="Script" path="res://scripts/rendering/fog_entities.gd" id="16_fogent"]
|
[ext_resource type="Script" path="res://scripts/rendering/fog_entities.gd" id="16_fogent"]
|
||||||
|
[ext_resource type="Script" path="res://scripts/rendering/sound_indicator_renderer.gd" id="20_soundind"]
|
||||||
[ext_resource type="PackedScene" path="res://ui/gauntlet_hud.tscn" id="17_gauntlet"]
|
[ext_resource type="PackedScene" path="res://ui/gauntlet_hud.tscn" id="17_gauntlet"]
|
||||||
[ext_resource type="PackedScene" path="res://ui/checklist_overlay.tscn" id="18_checklist"]
|
[ext_resource type="PackedScene" path="res://ui/checklist_overlay.tscn" id="18_checklist"]
|
||||||
[ext_resource type="PackedScene" path="res://ui/bug_report_dialog.tscn" id="19_bugreport"]
|
[ext_resource type="PackedScene" path="res://ui/bug_report_dialog.tscn" id="19_bugreport"]
|
||||||
|
[ext_resource type="PackedScene" path="res://ui/settings_dialog.tscn" id="21_settings"]
|
||||||
|
[ext_resource type="Script" path="res://ui/debug_overlay.gd" id="22_debug"]
|
||||||
|
[ext_resource type="PackedScene" path="res://ui/time_display.tscn" id="23_tdisplay"]
|
||||||
|
[ext_resource type="PackedScene" path="res://ui/examine_display.tscn" id="24_examine"]
|
||||||
|
[ext_resource type="PackedScene" path="res://ui/journal_panel.tscn" id="25_journal"]
|
||||||
|
[ext_resource type="PackedScene" path="res://ui/loading_screen.tscn" id="26_loading"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://b2ndm9rvx8cqp" path="res://ui/debug_console.tscn" id="27_debug_console"]
|
||||||
|
|
||||||
[node name="Game" type="Node2D"]
|
[node name="Game" type="Node2D"]
|
||||||
script = ExtResource("1_main")
|
script = ExtResource("1_main")
|
||||||
@@ -96,6 +104,13 @@ script = ExtResource("4_fog")
|
|||||||
z_index = 950
|
z_index = 950
|
||||||
script = ExtResource("16_fogent")
|
script = ExtResource("16_fogent")
|
||||||
|
|
||||||
|
; --- z:951 — Medium-range sound indicators (#126, D-018) ---
|
||||||
|
; Directional arrows at fog boundary for sounds outside LOS.
|
||||||
|
; Above FogEntities (z:950), below InsertOverlay (CanvasLayer 10).
|
||||||
|
[node name="SoundIndicators" type="Node2D" parent="World"]
|
||||||
|
z_index = 951
|
||||||
|
script = ExtResource("20_soundind")
|
||||||
|
|
||||||
; --- Camera ---
|
; --- Camera ---
|
||||||
[node name="Camera2D" type="Camera2D" parent="."]
|
[node name="Camera2D" type="Camera2D" parent="."]
|
||||||
position_smoothing_enabled = true
|
position_smoothing_enabled = true
|
||||||
@@ -108,6 +123,9 @@ zoom = Vector2(2, 2)
|
|||||||
[node name="InsertOverlay" type="CanvasLayer" parent="."]
|
[node name="InsertOverlay" type="CanvasLayer" parent="."]
|
||||||
layer = 10
|
layer = 10
|
||||||
|
|
||||||
|
; #263: Time display — diegetic insert clock, top-left placeholder (D-013, D-031)
|
||||||
|
[node name="TimeDisplay" parent="InsertOverlay" instance=ExtResource("23_tdisplay")]
|
||||||
|
|
||||||
; InteractionPrompt — v0.1 fallback single-line "E - Talk" display
|
; InteractionPrompt — v0.1 fallback single-line "E - Talk" display
|
||||||
[node name="InteractionPrompt" parent="InsertOverlay" instance=ExtResource("9_prompt")]
|
[node name="InteractionPrompt" parent="InsertOverlay" instance=ExtResource("9_prompt")]
|
||||||
|
|
||||||
@@ -120,6 +138,15 @@ layer = 10
|
|||||||
; D-061: Dialogue box — bottom screen, max 20% height, diegetic insert UI
|
; D-061: Dialogue box — bottom screen, max 20% height, diegetic insert UI
|
||||||
[node name="DialogueBox" parent="InsertOverlay" instance=ExtResource("15_dialogue")]
|
[node name="DialogueBox" parent="InsertOverlay" instance=ExtResource("15_dialogue")]
|
||||||
|
|
||||||
|
; #151: Minimap — diegetic insert overlay, top-right, 160px circle (D-013, D-049 z-layer 6)
|
||||||
|
[node name="Minimap" parent="InsertOverlay" instance=ExtResource("7_minimap")]
|
||||||
|
|
||||||
|
; #174: Examine result — non-interactive observe text overlay, auto-dismisses 5s (D-061 adjacent)
|
||||||
|
[node name="ExamineDisplay" parent="InsertOverlay" instance=ExtResource("24_examine")]
|
||||||
|
|
||||||
|
; #264: Journal panel — knowledge graph review, toggle J key, read-only (D-041)
|
||||||
|
[node name="JournalPanel" parent="InsertOverlay" instance=ExtResource("25_journal")]
|
||||||
|
|
||||||
; --- UI layer (CanvasLayer 20) ---
|
; --- UI layer (CanvasLayer 20) ---
|
||||||
; HUD, monologue, cursor — always visible, not affected by fog or camera.
|
; HUD, monologue, cursor — always visible, not affected by fog or camera.
|
||||||
[node name="UILayer" type="CanvasLayer" parent="."]
|
[node name="UILayer" type="CanvasLayer" parent="."]
|
||||||
@@ -127,8 +154,6 @@ layer = 20
|
|||||||
|
|
||||||
[node name="HUD" parent="UILayer" instance=ExtResource("6_hud")]
|
[node name="HUD" parent="UILayer" instance=ExtResource("6_hud")]
|
||||||
|
|
||||||
[node name="Minimap" parent="UILayer" instance=ExtResource("7_minimap")]
|
|
||||||
|
|
||||||
[node name="MonologueDisplay" parent="UILayer" instance=ExtResource("8_monologue")]
|
[node name="MonologueDisplay" parent="UILayer" instance=ExtResource("8_monologue")]
|
||||||
|
|
||||||
; D-053: Stance indicator — top-right, color-coded
|
; D-053: Stance indicator — top-right, color-coded
|
||||||
@@ -143,6 +168,16 @@ layer = 20
|
|||||||
; D-065: Inventory grid — 3x3, bottom-right, 40x40px, 1-9 hotkeys
|
; D-065: Inventory grid — 3x3, bottom-right, 40x40px, 1-9 hotkeys
|
||||||
[node name="InventoryGrid" parent="UILayer" instance=ExtResource("12_inv")]
|
[node name="InventoryGrid" parent="UILayer" instance=ExtResource("12_inv")]
|
||||||
|
|
||||||
|
; #511: F3 debug overlay — real-time game state, toggled by F3
|
||||||
|
[node name="DebugOverlay" type="Control" parent="UILayer"]
|
||||||
|
anchors_preset = 0
|
||||||
|
offset_left = 16
|
||||||
|
offset_top = 120
|
||||||
|
offset_right = 400
|
||||||
|
offset_bottom = 400
|
||||||
|
mouse_filter = 2
|
||||||
|
script = ExtResource("22_debug")
|
||||||
|
|
||||||
; D-056: Cursor state machine — insert-styled geometric cursor, topmost in UILayer
|
; D-056: Cursor state machine — insert-styled geometric cursor, topmost in UILayer
|
||||||
[node name="CursorRenderer" type="Node2D" parent="UILayer"]
|
[node name="CursorRenderer" type="Node2D" parent="UILayer"]
|
||||||
script = ExtResource("10_cursor")
|
script = ExtResource("10_cursor")
|
||||||
@@ -154,3 +189,12 @@ layer = 30
|
|||||||
|
|
||||||
; #495: WRONG button (F12) — bug report capture dialog
|
; #495: WRONG button (F12) — bug report capture dialog
|
||||||
[node name="BugReportDialog" parent="ModalLayer" instance=ExtResource("19_bugreport")]
|
[node name="BugReportDialog" parent="ModalLayer" instance=ExtResource("19_bugreport")]
|
||||||
|
|
||||||
|
; #528: Audio settings dialog — 5-bus volume sliders, ESC/OPEN_MENU to toggle
|
||||||
|
[node name="SettingsDialog" parent="ModalLayer" instance=ExtResource("21_settings")]
|
||||||
|
|
||||||
|
; #257: Loading screen — full-screen overlay during save/load round-trip
|
||||||
|
[node name="LoadingScreen" parent="ModalLayer" instance=ExtResource("26_loading")]
|
||||||
|
|
||||||
|
; #581: Debug console — tilde key toggles, bottom 40% of screen
|
||||||
|
[node name="DebugConsole" parent="ModalLayer" instance=ExtResource("27_debug_console")]
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
[gd_scene load_steps=2 format=3 uid="uid://main_menu_sr"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://ui/main_menu.gd" id="1_mainmenu"]
|
||||||
|
|
||||||
|
; Main menu — New Game / Continue / Quit.
|
||||||
|
; #258: D-085 per-game save directory created on New Game.
|
||||||
|
|
||||||
|
[node name="MainMenu" type="Control"]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
script = ExtResource("1_mainmenu")
|
||||||
|
|
||||||
|
[node name="Background" type="ColorRect" parent="."]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
color = Color(0.05, 0.05, 0.08, 1.0)
|
||||||
|
mouse_filter = 2
|
||||||
|
|
||||||
|
[node name="VBox" type="VBoxContainer" parent="."]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 8
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_top = 0.5
|
||||||
|
anchor_right = 0.5
|
||||||
|
anchor_bottom = 0.5
|
||||||
|
offset_left = -120.0
|
||||||
|
offset_top = -80.0
|
||||||
|
offset_right = 120.0
|
||||||
|
offset_bottom = 100.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
theme_override_constants/separation = 16
|
||||||
|
alignment = 1
|
||||||
|
|
||||||
|
[node name="TitleLabel" type="Label" parent="VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "THE SETTLED REACH"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
theme_override_font_sizes/font_size = 36
|
||||||
|
theme_override_colors/font_color = Color(0.784, 0.816, 0.878, 1.0)
|
||||||
|
|
||||||
|
[node name="Spacer" type="Control" parent="VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
custom_minimum_size = Vector2(0, 24)
|
||||||
|
|
||||||
|
[node name="NewGameBtn" type="Button" parent="VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "NEW GAME"
|
||||||
|
theme_override_font_sizes/font_size = 15
|
||||||
|
theme_override_colors/font_color = Color(0.906, 0.773, 0.278, 1.0)
|
||||||
|
|
||||||
|
[node name="ContinueBtn" type="Button" parent="VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "CONTINUE"
|
||||||
|
theme_override_font_sizes/font_size = 15
|
||||||
|
theme_override_colors/font_color = Color(0.906, 0.773, 0.278, 1.0)
|
||||||
|
|
||||||
|
[node name="LoadGameBtn" type="Button" parent="VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "LOAD GAME"
|
||||||
|
theme_override_font_sizes/font_size = 15
|
||||||
|
theme_override_colors/font_color = Color(0.906, 0.773, 0.278, 1.0)
|
||||||
|
|
||||||
|
[node name="QuitBtn" type="Button" parent="VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "QUIT"
|
||||||
|
theme_override_font_sizes/font_size = 15
|
||||||
|
theme_override_colors/font_color = Color(0.533, 0.565, 0.627, 1.0)
|
||||||
|
|
||||||
|
[node name="LoadGamePanel" type="Control" parent="."]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
[node name="PanelBg" type="ColorRect" parent="LoadGamePanel"]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
color = Color(0.05, 0.05, 0.08, 0.96)
|
||||||
|
mouse_filter = 2
|
||||||
|
|
||||||
|
[node name="VBox" type="VBoxContainer" parent="LoadGamePanel"]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 8
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_top = 0.5
|
||||||
|
anchor_right = 0.5
|
||||||
|
anchor_bottom = 0.5
|
||||||
|
offset_left = -160.0
|
||||||
|
offset_top = -180.0
|
||||||
|
offset_right = 160.0
|
||||||
|
offset_bottom = 180.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
theme_override_constants/separation = 12
|
||||||
|
|
||||||
|
[node name="TitleLabel" type="Label" parent="LoadGamePanel/VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "LOAD GAME"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
theme_override_font_sizes/font_size = 20
|
||||||
|
theme_override_colors/font_color = Color(0.784, 0.816, 0.878, 1.0)
|
||||||
|
|
||||||
|
[node name="SavesScroll" type="ScrollContainer" parent="LoadGamePanel/VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
custom_minimum_size = Vector2(320, 240)
|
||||||
|
|
||||||
|
[node name="SavesList" type="VBoxContainer" parent="LoadGamePanel/VBox/SavesScroll"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
theme_override_constants/separation = 8
|
||||||
|
|
||||||
|
[node name="BackBtn" type="Button" parent="LoadGamePanel/VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "BACK"
|
||||||
|
theme_override_font_sizes/font_size = 14
|
||||||
|
theme_override_colors/font_color = Color(0.533, 0.565, 0.627, 1.0)
|
||||||
@@ -5,6 +5,11 @@ extends Node
|
|||||||
## No-op fallback when audio assets absent (D-038).
|
## No-op fallback when audio assets absent (D-038).
|
||||||
## Spatial audio positioning for close-range sounds (D-018).
|
## Spatial audio positioning for close-range sounds (D-018).
|
||||||
|
|
||||||
|
# --- D-067: Recognition chime asset key ---
|
||||||
|
# Fires on first fog recognition (cognitive delay onset). UISounds bus (not WorldSFX).
|
||||||
|
# Matches sfx_monologue_chime.ogg from D-038 — "neural lattice firing" feel.
|
||||||
|
const CHIME_RECOGNITION := "sfx_monologue_chime"
|
||||||
|
|
||||||
# --- Bus names (D-068) ---
|
# --- Bus names (D-068) ---
|
||||||
const BUS_MUSIC := "Music"
|
const BUS_MUSIC := "Music"
|
||||||
const BUS_AMBIENT := "Ambient"
|
const BUS_AMBIENT := "Ambient"
|
||||||
@@ -36,6 +41,23 @@ const DIP_SPECS := {
|
|||||||
# Low-pass filter default cutoff — effectively bypassed at this value.
|
# Low-pass filter default cutoff — effectively bypassed at this value.
|
||||||
const FILTER_CUTOFF_DEFAULT := 20500.0
|
const FILTER_CUTOFF_DEFAULT := 20500.0
|
||||||
|
|
||||||
|
# --- D-073: Zone crossfade constants ---
|
||||||
|
const CROSSFADE_DURATION := 1.8 # D-073: 1.5-2s spec, mid-range
|
||||||
|
|
||||||
|
# Maps server zone_id strings to ambient asset keys (filenames in res://assets/audio/).
|
||||||
|
# Hub and Workplace intentionally share the same ambient layer (amb_hub_layer) —
|
||||||
|
# they are the same location type, so hub→workplace transition is a same-asset no-op
|
||||||
|
# (old_asset != new_asset guard skips the fade-out). Sprint brief consolidates
|
||||||
|
# D-038's "amb_workplace_layer" to "amb_hub_layer" for v0.1.
|
||||||
|
# Note: amb_station_base (D-038 global base hum) plays globally via play_loop()
|
||||||
|
# at startup — it is not zone-dependent and has no ZONE_ASSETS entry.
|
||||||
|
const ZONE_ASSETS: Dictionary = {
|
||||||
|
"hub": "amb_hub_layer",
|
||||||
|
"workplace": "amb_hub_layer",
|
||||||
|
"bar": "amb_bar_layer",
|
||||||
|
"corridor": "amb_corridor_layer",
|
||||||
|
}
|
||||||
|
|
||||||
# Asset registry: filename stem (e.g. "amb_station_base") → AudioStream
|
# Asset registry: filename stem (e.g. "amb_station_base") → AudioStream
|
||||||
var _registry: Dictionary = {}
|
var _registry: Dictionary = {}
|
||||||
|
|
||||||
@@ -52,12 +74,19 @@ var _ambient_filter: AudioEffectLowPassFilter = null
|
|||||||
# Ambient loop players keyed by asset_key (D-073 zone crossfade)
|
# Ambient loop players keyed by asset_key (D-073 zone crossfade)
|
||||||
var _ambient_players: Dictionary = {}
|
var _ambient_players: Dictionary = {}
|
||||||
|
|
||||||
|
# D-073: Zone crossfade state
|
||||||
|
var _current_zone_id: String = ""
|
||||||
|
var _zone_tweens: Array = []
|
||||||
|
|
||||||
signal dip_changed(profile: String)
|
signal dip_changed(profile: String)
|
||||||
|
|
||||||
|
|
||||||
|
const PREFS_PATH := "user://audio_prefs.cfg"
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
_setup_buses()
|
_setup_buses()
|
||||||
_scan_registry()
|
_scan_registry()
|
||||||
|
_load_prefs()
|
||||||
|
|
||||||
|
|
||||||
# --- Bus setup ---
|
# --- Bus setup ---
|
||||||
@@ -83,15 +112,15 @@ func _setup_buses() -> void:
|
|||||||
# --- Asset registry (D-068 directory-scan pattern) ---
|
# --- Asset registry (D-068 directory-scan pattern) ---
|
||||||
|
|
||||||
func _scan_registry() -> void:
|
func _scan_registry() -> void:
|
||||||
_scan_dir("res://audio/")
|
_scan_dir("res://assets/audio/")
|
||||||
print("AudioManager: %d assets registered" % _registry.size())
|
print("AudioManager: %d assets registered" % _registry.size())
|
||||||
|
|
||||||
|
|
||||||
func _scan_dir(path: String) -> void:
|
func _scan_dir(path: String) -> void:
|
||||||
var dir := DirAccess.open(path)
|
var dir := DirAccess.open(path)
|
||||||
if dir == null:
|
if dir == null:
|
||||||
if path == "res://audio/":
|
if path == "res://assets/audio/":
|
||||||
print("AudioManager: res://audio/ not found — all play methods no-op")
|
print("AudioManager: res://assets/audio/ not found — all play methods no-op")
|
||||||
return
|
return
|
||||||
dir.list_dir_begin()
|
dir.list_dir_begin()
|
||||||
var file_name := dir.get_next()
|
var file_name := dir.get_next()
|
||||||
@@ -105,6 +134,7 @@ func _scan_dir(path: String) -> void:
|
|||||||
if stream:
|
if stream:
|
||||||
_registry[file_name.get_basename()] = stream
|
_registry[file_name.get_basename()] = stream
|
||||||
file_name = dir.get_next()
|
file_name = dir.get_next()
|
||||||
|
dir.list_dir_end()
|
||||||
|
|
||||||
|
|
||||||
func has_asset(asset_key: String) -> bool:
|
func has_asset(asset_key: String) -> bool:
|
||||||
@@ -137,6 +167,8 @@ func play_loop(asset_key: String, bus: String = BUS_AMBIENT) -> AudioStreamPlaye
|
|||||||
return null
|
return null
|
||||||
stop_loop(asset_key)
|
stop_loop(asset_key)
|
||||||
var loop_stream := stream.duplicate() as AudioStream
|
var loop_stream := stream.duplicate() as AudioStream
|
||||||
|
if loop_stream == null:
|
||||||
|
return null
|
||||||
_enable_loop(loop_stream)
|
_enable_loop(loop_stream)
|
||||||
var player := AudioStreamPlayer.new()
|
var player := AudioStreamPlayer.new()
|
||||||
player.stream = loop_stream
|
player.stream = loop_stream
|
||||||
@@ -163,6 +195,32 @@ func stop_all_loops() -> void:
|
|||||||
stop_loop(key)
|
stop_loop(key)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Audio asset registry: event type → asset key (D-018, #125) ---
|
||||||
|
# Maps server-sent sound event_type strings to audio asset keys.
|
||||||
|
# Keys match filename stems in res://assets/audio/ (scanned by _scan_registry).
|
||||||
|
# Audio assets per D-038: footstep variants (walk / run), NPC murmur (D-072, #532).
|
||||||
|
# Missing assets no-op gracefully (D-038 fallback pattern).
|
||||||
|
const SOUND_EVENT_ASSETS: Dictionary = {
|
||||||
|
"Footstep": "sfx_footstep_metal_walk",
|
||||||
|
"FootstepWalk": "sfx_footstep_metal_walk",
|
||||||
|
"FootstepCareful":"sfx_footstep_metal_walk", # D-053: same asset until stance-differentiated audio lands
|
||||||
|
"FootstepCrouch": "sfx_footstep_metal_walk", # D-053: same asset until stance-differentiated audio lands
|
||||||
|
"FootstepSprint": "sfx_footstep_metal_run",
|
||||||
|
"FootstepRun": "sfx_footstep_metal_run",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
## Play a close-range sound event at a world tile position (D-018, #125).
|
||||||
|
## event_type: server RangeCategory::Close event type string (e.g. "Footstep").
|
||||||
|
## world_tile_pos: server tile coordinates — converted to world pixels internally.
|
||||||
|
## No-ops if event_type has no registered asset or asset file is absent.
|
||||||
|
func play_sound_event(event_type: String, world_tile_pos: Vector2) -> void:
|
||||||
|
var asset_key: String = SOUND_EVENT_ASSETS.get(event_type, "")
|
||||||
|
if asset_key.is_empty():
|
||||||
|
return
|
||||||
|
play_at(asset_key, world_tile_pos * Constants.TILE_SIZE)
|
||||||
|
|
||||||
|
|
||||||
# --- Playback: spatial (D-018 close-range) ---
|
# --- Playback: spatial (D-018 close-range) ---
|
||||||
|
|
||||||
## Play a one-shot spatial sound at a world position (pixels).
|
## Play a one-shot spatial sound at a world position (pixels).
|
||||||
@@ -270,19 +328,85 @@ func set_volume(bus: String, volume_db: float) -> void:
|
|||||||
var buses: Dictionary = spec.get("buses", {})
|
var buses: Dictionary = spec.get("buses", {})
|
||||||
var offset_db: float = buses.get(bus, 0.0)
|
var offset_db: float = buses.get(bus, 0.0)
|
||||||
AudioServer.set_bus_volume_db(idx, volume_db + offset_db)
|
AudioServer.set_bus_volume_db(idx, volume_db + offset_db)
|
||||||
|
_save_prefs()
|
||||||
|
|
||||||
|
|
||||||
func get_volume(bus: String) -> float:
|
func get_volume(bus: String) -> float:
|
||||||
return _bus_volumes.get(bus, 0.0)
|
return _bus_volumes.get(bus, 0.0)
|
||||||
|
|
||||||
|
|
||||||
# --- Zone crossfade (D-073 stub) ---
|
# --- Volume persistence (user://audio_prefs.cfg) ---
|
||||||
|
|
||||||
|
func _load_prefs() -> void:
|
||||||
|
var cfg := ConfigFile.new()
|
||||||
|
if cfg.load(PREFS_PATH) != OK:
|
||||||
|
return
|
||||||
|
for bus_name in BUSES:
|
||||||
|
if cfg.has_section_key("audio", bus_name):
|
||||||
|
var db: float = cfg.get_value("audio", bus_name, 0.0)
|
||||||
|
# Apply directly: bypass _save_prefs() on initial load.
|
||||||
|
_bus_volumes[bus_name] = db
|
||||||
|
var idx := AudioServer.get_bus_index(bus_name)
|
||||||
|
if idx >= 0:
|
||||||
|
AudioServer.set_bus_volume_db(idx, db)
|
||||||
|
|
||||||
|
|
||||||
|
func _save_prefs() -> void:
|
||||||
|
var cfg := ConfigFile.new()
|
||||||
|
for bus_name in BUSES:
|
||||||
|
cfg.set_value("audio", bus_name, _bus_volumes.get(bus_name, 0.0))
|
||||||
|
var err := cfg.save(PREFS_PATH)
|
||||||
|
if err != OK:
|
||||||
|
push_warning("AudioManager: failed to save prefs to %s (error %d)" % [PREFS_PATH, err])
|
||||||
|
|
||||||
|
|
||||||
|
# --- Zone crossfade (D-073) ---
|
||||||
|
|
||||||
## Handle zone transition. Server sends zone_id per tile in ObserverSnapshot.
|
## Handle zone transition. Server sends zone_id per tile in ObserverSnapshot.
|
||||||
## Full crossfade implementation deferred to Sprint 9+ (D-073).
|
## Hard boundary trigger with 1.5-2s audio crossfade between ambient layers.
|
||||||
## Stub exists so server integration can call it without conditional checks.
|
## Interruptible — mid-crossfade zone change tweens from current position.
|
||||||
func set_zone(_zone_id: String) -> void:
|
## No-op if assets absent (D-038) or same zone.
|
||||||
pass
|
func set_zone(zone_id: String) -> void:
|
||||||
|
if zone_id == _current_zone_id:
|
||||||
|
return
|
||||||
|
var new_asset: String = ZONE_ASSETS.get(zone_id, "")
|
||||||
|
var old_asset: String = ZONE_ASSETS.get(_current_zone_id, "")
|
||||||
|
_current_zone_id = zone_id
|
||||||
|
_kill_zone_tweens()
|
||||||
|
|
||||||
|
# Fade out old ambient layer (if different asset from incoming zone)
|
||||||
|
if not old_asset.is_empty() and old_asset != new_asset:
|
||||||
|
if _ambient_players.has(old_asset):
|
||||||
|
var old_player: AudioStreamPlayer = _ambient_players[old_asset]
|
||||||
|
if is_instance_valid(old_player):
|
||||||
|
var tween := create_tween()
|
||||||
|
tween.tween_property(old_player, "volume_db", -80.0, CROSSFADE_DURATION)
|
||||||
|
tween.tween_callback(stop_loop.bind(old_asset))
|
||||||
|
_zone_tweens.append(tween)
|
||||||
|
|
||||||
|
# Fade in new ambient layer
|
||||||
|
if not new_asset.is_empty():
|
||||||
|
if _ambient_players.has(new_asset):
|
||||||
|
# Already playing (interrupted reverse crossfade) — tween from current volume
|
||||||
|
var existing: AudioStreamPlayer = _ambient_players[new_asset]
|
||||||
|
if is_instance_valid(existing):
|
||||||
|
var tween := create_tween()
|
||||||
|
tween.tween_property(existing, "volume_db", 0.0, CROSSFADE_DURATION)
|
||||||
|
_zone_tweens.append(tween)
|
||||||
|
elif has_asset(new_asset):
|
||||||
|
var new_player := play_loop(new_asset, BUS_AMBIENT)
|
||||||
|
if new_player:
|
||||||
|
new_player.volume_db = -80.0
|
||||||
|
var tween := create_tween()
|
||||||
|
tween.tween_property(new_player, "volume_db", 0.0, CROSSFADE_DURATION)
|
||||||
|
_zone_tweens.append(tween)
|
||||||
|
|
||||||
|
|
||||||
|
func _kill_zone_tweens() -> void:
|
||||||
|
for tween in _zone_tweens:
|
||||||
|
if tween != null and tween.is_valid():
|
||||||
|
tween.kill()
|
||||||
|
_zone_tweens.clear()
|
||||||
|
|
||||||
|
|
||||||
# --- Internal helpers ---
|
# --- Internal helpers ---
|
||||||
|
|||||||
@@ -8,13 +8,29 @@ extends Node
|
|||||||
# Used by fog shader to distinguish visual treatment per tile.
|
# Used by fog shader to distinguish visual treatment per tile.
|
||||||
# Test assertions reference these: assert_that(byte).is_equal(FogState.VIS_FORWARD)
|
# Test assertions reference these: assert_that(byte).is_equal(FogState.VIS_FORWARD)
|
||||||
const VIS_HIDDEN: int = 0 # Not in LOS — fully fogged
|
const VIS_HIDDEN: int = 0 # Not in LOS — fully fogged
|
||||||
const VIS_PERIPHERAL: int = 180 # In LOS, peripheral sector — light fog dimming
|
const VIS_PERIPHERAL: int = 180 # DEPRECATED: peripheral sector removed in Sprint 22 (#569). Retained — tests still reference it.
|
||||||
const VIS_FORWARD: int = 255 # In LOS, forward sector — clear vision
|
const VIS_FORWARD: int = 255 # In LOS, forward sector — clear vision
|
||||||
|
|
||||||
const EXP_UNEXPLORED: int = 0 # Never seen — total darkness
|
const EXP_UNEXPLORED: int = 0 # Never seen — total darkness
|
||||||
const EXP_EXPLORED: int = 128 # Previously seen, now out of LOS — deep fog
|
const EXP_EXPLORED: int = 128 # Previously seen, now out of LOS — deep fog
|
||||||
const EXP_VISIBLE: int = 255 # Currently in LOS — clear (written each frame)
|
const EXP_VISIBLE: int = 255 # Currently in LOS — clear (written each frame)
|
||||||
|
|
||||||
|
# Zone temperature tints (D-059 + D-046, Sprint 22) — keyed by zone_id string from server.
|
||||||
|
# Matches audio_manager.gd ZONE_ASSETS zone_id strings for consistent zone semantics.
|
||||||
|
# Low saturation is intentional (D-046): tints are subtle — distinguishable as warm/cool/neutral
|
||||||
|
# in side-by-side comparison, not garish. The "Hopper test" validates this.
|
||||||
|
# Colors are dark tints used as the fog overlay in the deep fog zone:
|
||||||
|
# hub/workplace: #1a1f2e (cool blue-dark — terminal, institutional)
|
||||||
|
# bar: #2a1f15 (warm amber-dark — social, inhabited)
|
||||||
|
# corridor: #1a1a1a (neutral dark — transitional, maintenance)
|
||||||
|
const ZONE_TINTS: Dictionary = {
|
||||||
|
"hub": Color(0.102, 0.122, 0.180), # #1a1f2e — cool blue-dark
|
||||||
|
"workplace": Color(0.102, 0.122, 0.180), # same as hub
|
||||||
|
"bar": Color(0.165, 0.122, 0.082), # #2a1f15 — warm amber-dark
|
||||||
|
"corridor": Color(0.102, 0.102, 0.102), # #1a1a1a — neutral dark
|
||||||
|
}
|
||||||
|
const ZONE_TINT_DEFAULT: Color = Color(0.102, 0.102, 0.102) # #1a1a1a neutral
|
||||||
|
|
||||||
var map_bounds: Rect2i = Rect2i(0, 0, 1, 1)
|
var map_bounds: Rect2i = Rect2i(0, 0, 1, 1)
|
||||||
var visibility_texture: ImageTexture
|
var visibility_texture: ImageTexture
|
||||||
var exploration_texture: ImageTexture
|
var exploration_texture: ImageTexture
|
||||||
@@ -25,16 +41,34 @@ var _exp_bytes: PackedByteArray
|
|||||||
var _vis_image: Image
|
var _vis_image: Image
|
||||||
var _exp_image: Image
|
var _exp_image: Image
|
||||||
var _tint_image: Image
|
var _tint_image: Image
|
||||||
|
# Zone tint stored as 3-channel RGB bytes (R, G, B per pixel) for preservation across resizes
|
||||||
|
var _tint_bytes: PackedByteArray
|
||||||
var _width: int = 1
|
var _width: int = 1
|
||||||
var _height: int = 1
|
var _height: int = 1
|
||||||
var _prev_visible: Dictionary = {} # Tiles visible last frame (for incremental decay)
|
var _prev_visible: Dictionary = {} # Tiles visible last frame (for incremental decay)
|
||||||
|
|
||||||
|
## Debug flag — when true, fog.gdshader renders raw exploration texture
|
||||||
|
## as colored overlay (green=visible, blue=explored, red=unexplored).
|
||||||
|
## Toggle via FogState.debug_exploration = true in the console.
|
||||||
|
var debug_exploration: bool = false
|
||||||
|
|
||||||
|
## Deterministic shader time for visual test captures.
|
||||||
|
## When >= 0, fog_shader.gd uses this instead of Time.get_ticks_msec().
|
||||||
|
## Set before settle frames so noise phase is reproducible across runs.
|
||||||
|
var override_time: float = -1.0
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
_resize(Rect2i(0, 0, 64, 64))
|
_resize(Rect2i(0, 0, 64, 64))
|
||||||
|
|
||||||
|
|
||||||
func _resize(bounds: Rect2i) -> void:
|
func _resize(bounds: Rect2i) -> void:
|
||||||
|
var old_bounds := map_bounds
|
||||||
|
var old_exp := _exp_bytes
|
||||||
|
var old_tint := _tint_bytes # empty on first call (_ready); guard at line 104 skips copy
|
||||||
|
var old_w := _width
|
||||||
|
var old_h := _height
|
||||||
|
|
||||||
map_bounds = bounds
|
map_bounds = bounds
|
||||||
_width = maxi(bounds.size.x, 1)
|
_width = maxi(bounds.size.x, 1)
|
||||||
_height = maxi(bounds.size.y, 1)
|
_height = maxi(bounds.size.y, 1)
|
||||||
@@ -49,41 +83,92 @@ func _resize(bounds: Rect2i) -> void:
|
|||||||
_exp_bytes = PackedByteArray()
|
_exp_bytes = PackedByteArray()
|
||||||
_exp_bytes.resize(sz)
|
_exp_bytes.resize(sz)
|
||||||
_exp_bytes.fill(EXP_UNEXPLORED)
|
_exp_bytes.fill(EXP_UNEXPLORED)
|
||||||
|
# Preserve exploration data from old bounds into new bounds
|
||||||
|
if old_exp.size() > 0 and old_w > 0 and old_h > 0:
|
||||||
|
var dx: int = old_bounds.position.x - bounds.position.x
|
||||||
|
var dy: int = old_bounds.position.y - bounds.position.y
|
||||||
|
for oy in range(old_h):
|
||||||
|
var ny: int = oy + dy
|
||||||
|
if ny < 0 or ny >= _height:
|
||||||
|
continue
|
||||||
|
for ox in range(old_w):
|
||||||
|
var nx: int = ox + dx
|
||||||
|
if nx < 0 or nx >= _width:
|
||||||
|
continue
|
||||||
|
var old_val: int = old_exp[oy * old_w + ox]
|
||||||
|
if old_val > EXP_UNEXPLORED:
|
||||||
|
_exp_bytes[ny * _width + nx] = old_val
|
||||||
_exp_image = Image.create_from_data(_width, _height, false, Image.FORMAT_R8, _exp_bytes)
|
_exp_image = Image.create_from_data(_width, _height, false, Image.FORMAT_R8, _exp_bytes)
|
||||||
exploration_texture = ImageTexture.create_from_image(_exp_image)
|
exploration_texture = ImageTexture.create_from_image(_exp_image)
|
||||||
|
|
||||||
# Zone tint — neutral dark for Sprint 6 (zone metadata deferred)
|
# Zone tint — 3 bytes per pixel (RGB), default neutral dark
|
||||||
_tint_image = Image.create(_width, _height, false, Image.FORMAT_RGB8)
|
var tint_sz := sz * 3
|
||||||
_tint_image.fill(Color(0.05, 0.05, 0.08))
|
_tint_bytes = PackedByteArray()
|
||||||
|
_tint_bytes.resize(tint_sz)
|
||||||
|
var default_r := int(ZONE_TINT_DEFAULT.r * 255.0)
|
||||||
|
var default_g := int(ZONE_TINT_DEFAULT.g * 255.0)
|
||||||
|
var default_b := int(ZONE_TINT_DEFAULT.b * 255.0)
|
||||||
|
for i in range(sz):
|
||||||
|
_tint_bytes[i * 3 + 0] = default_r
|
||||||
|
_tint_bytes[i * 3 + 1] = default_g
|
||||||
|
_tint_bytes[i * 3 + 2] = default_b
|
||||||
|
# Preserve zone tint data from old bounds (zone tints are stable — tile zone never changes)
|
||||||
|
if old_tint.size() > 0 and old_w > 0 and old_h > 0:
|
||||||
|
var dx: int = old_bounds.position.x - bounds.position.x
|
||||||
|
var dy: int = old_bounds.position.y - bounds.position.y
|
||||||
|
for oy in range(old_h):
|
||||||
|
var ny: int = oy + dy
|
||||||
|
if ny < 0 or ny >= _height:
|
||||||
|
continue
|
||||||
|
for ox in range(old_w):
|
||||||
|
var nx: int = ox + dx
|
||||||
|
if nx < 0 or nx >= _width:
|
||||||
|
continue
|
||||||
|
var old_idx := (oy * old_w + ox) * 3
|
||||||
|
var new_idx := (ny * _width + nx) * 3
|
||||||
|
_tint_bytes[new_idx + 0] = old_tint[old_idx + 0]
|
||||||
|
_tint_bytes[new_idx + 1] = old_tint[old_idx + 1]
|
||||||
|
_tint_bytes[new_idx + 2] = old_tint[old_idx + 2]
|
||||||
|
_tint_image = Image.create_from_data(_width, _height, false, Image.FORMAT_RGB8, _tint_bytes)
|
||||||
zone_tint_texture = ImageTexture.create_from_image(_tint_image)
|
zone_tint_texture = ImageTexture.create_from_image(_tint_image)
|
||||||
|
|
||||||
_prev_visible.clear()
|
_prev_visible.clear()
|
||||||
|
|
||||||
|
|
||||||
func update_from_state() -> void:
|
func update_from_state() -> void:
|
||||||
# Resize if map bounds changed
|
# Grow bounds to include newly visible tiles — never shrink, so explored
|
||||||
var tiles := GameState.visible_tiles
|
# tiles behind the player stay in the texture and render as deep fog
|
||||||
if tiles.size() > 0:
|
# instead of black. Exploration data is preserved across resizes.
|
||||||
var new_bounds := _compute_bounds(tiles)
|
# Use visible_positions (always populated from server snapshots) instead of
|
||||||
|
# visible_tiles, which stays empty in live server mode because the server
|
||||||
|
# sends tile_kind but game_state.gd's population check expects "type".
|
||||||
|
var positions: Dictionary = GameState.visible_positions
|
||||||
|
if positions.size() > 0:
|
||||||
|
var new_bounds := _grow_bounds_from_positions(positions)
|
||||||
if new_bounds != map_bounds:
|
if new_bounds != map_bounds:
|
||||||
_resize(new_bounds)
|
_resize(new_bounds)
|
||||||
|
|
||||||
var ox: int = map_bounds.position.x
|
var ox: int = map_bounds.position.x
|
||||||
var oy: int = map_bounds.position.y
|
var oy: int = map_bounds.position.y
|
||||||
var positions: Dictionary = GameState.visible_positions
|
|
||||||
var sectors: Dictionary = GameState.visibility_sectors
|
|
||||||
|
|
||||||
# TODO(v0.2): gradual decay over game-time instead of immediate EXP_VISIBLE→EXP_EXPLORED
|
# TODO(v0.2): gradual decay over game-time instead of immediate EXP_VISIBLE→EXP_EXPLORED
|
||||||
|
|
||||||
# 1. Clear visibility, then write current LOS
|
# 1. Clear visibility, then write current LOS (all tiles are Forward)
|
||||||
_vis_bytes.fill(VIS_HIDDEN)
|
_vis_bytes.fill(VIS_HIDDEN)
|
||||||
for pos in positions:
|
for pos in positions:
|
||||||
var px: int = pos.x - ox
|
var px: int = pos.x - ox
|
||||||
var py: int = pos.y - oy
|
var py: int = pos.y - oy
|
||||||
if px < 0 or py < 0 or px >= _width or py >= _height:
|
if px < 0 or py < 0 or px >= _width or py >= _height:
|
||||||
continue
|
continue
|
||||||
var sector: String = sectors.get(pos, "Forward")
|
_vis_bytes[py * _width + px] = VIS_FORWARD
|
||||||
_vis_bytes[py * _width + px] = VIS_FORWARD if sector == "Forward" else VIS_PERIPHERAL
|
# #585: BoundaryWall margin tiles — fog lifts so wall content composites correctly,
|
||||||
|
# but NOT in visible_positions so they don't persist as explored memory.
|
||||||
|
for pos in GameState.boundary_positions:
|
||||||
|
var px: int = pos.x - ox
|
||||||
|
var py: int = pos.y - oy
|
||||||
|
if px < 0 or py < 0 or px >= _width or py >= _height:
|
||||||
|
continue
|
||||||
|
_vis_bytes[py * _width + px] = VIS_FORWARD
|
||||||
_vis_image.set_data(_width, _height, false, Image.FORMAT_R8, _vis_bytes)
|
_vis_image.set_data(_width, _height, false, Image.FORMAT_R8, _vis_bytes)
|
||||||
visibility_texture.update(_vis_image)
|
visibility_texture.update(_vis_image)
|
||||||
|
|
||||||
@@ -106,24 +191,58 @@ func update_from_state() -> void:
|
|||||||
_exp_image.set_data(_width, _height, false, Image.FORMAT_R8, _exp_bytes)
|
_exp_image.set_data(_width, _height, false, Image.FORMAT_R8, _exp_bytes)
|
||||||
exploration_texture.update(_exp_image)
|
exploration_texture.update(_exp_image)
|
||||||
|
|
||||||
|
# 3. Zone tint: write zone temperature color for currently visible tiles.
|
||||||
|
# Zone data is stable (tile zone never changes) so we only write on first sight.
|
||||||
|
# Data persists in _tint_bytes across frames and across resizes.
|
||||||
|
# visible_tiles carries zone_id per tile (populated from snapshot "tiles" or
|
||||||
|
# "visible_tiles" with type field — see game_state.gd apply_snapshot).
|
||||||
|
var tiles := GameState.visible_tiles
|
||||||
|
var tint_dirty := false
|
||||||
|
for tile in tiles:
|
||||||
|
if not tile is Dictionary or not tile.has("x") or not tile.has("y"):
|
||||||
|
continue
|
||||||
|
var zone_id: String = str(tile.get("zone_id", ""))
|
||||||
|
if zone_id.is_empty():
|
||||||
|
continue
|
||||||
|
var tint_color: Color = ZONE_TINTS.get(zone_id, ZONE_TINT_DEFAULT)
|
||||||
|
var px: int = int(tile.x) - ox
|
||||||
|
var py: int = int(tile.y) - oy
|
||||||
|
if px < 0 or py < 0 or px >= _width or py >= _height:
|
||||||
|
continue
|
||||||
|
var tint_idx := (py * _width + px) * 3
|
||||||
|
var new_r := int(tint_color.r * 255.0)
|
||||||
|
var new_g := int(tint_color.g * 255.0)
|
||||||
|
var new_b := int(tint_color.b * 255.0)
|
||||||
|
# Only update if different from current (avoid spurious texture uploads)
|
||||||
|
if _tint_bytes[tint_idx] != new_r or _tint_bytes[tint_idx + 1] != new_g or _tint_bytes[tint_idx + 2] != new_b:
|
||||||
|
_tint_bytes[tint_idx + 0] = new_r
|
||||||
|
_tint_bytes[tint_idx + 1] = new_g
|
||||||
|
_tint_bytes[tint_idx + 2] = new_b
|
||||||
|
tint_dirty = true
|
||||||
|
if tint_dirty:
|
||||||
|
_tint_image.set_data(_width, _height, false, Image.FORMAT_RGB8, _tint_bytes)
|
||||||
|
zone_tint_texture.update(_tint_image)
|
||||||
|
|
||||||
# Shallow copy — correct for Dictionary<Vector2i, bool/String> values
|
# Shallow copy — correct for Dictionary<Vector2i, bool/String> values
|
||||||
_prev_visible = positions.duplicate()
|
_prev_visible = positions.duplicate()
|
||||||
|
|
||||||
|
|
||||||
func _compute_bounds(tiles: Array) -> Rect2i:
|
func _grow_bounds_from_positions(positions: Dictionary) -> Rect2i:
|
||||||
|
## Compute bounds from visible_positions (Dictionary[Vector2i, bool]).
|
||||||
var min_x := 999999
|
var min_x := 999999
|
||||||
var min_y := 999999
|
var min_y := 999999
|
||||||
var max_x := -999999
|
var max_x := -999999
|
||||||
var max_y := -999999
|
var max_y := -999999
|
||||||
for tile in tiles:
|
for pos in positions:
|
||||||
if not tile is Dictionary or not tile.has("x") or not tile.has("y"):
|
min_x = mini(min_x, pos.x)
|
||||||
continue
|
min_y = mini(min_y, pos.y)
|
||||||
min_x = mini(min_x, int(tile.x))
|
max_x = maxi(max_x, pos.x)
|
||||||
min_y = mini(min_y, int(tile.y))
|
max_y = maxi(max_y, pos.y)
|
||||||
max_x = maxi(max_x, int(tile.x))
|
|
||||||
max_y = maxi(max_y, int(tile.y))
|
|
||||||
# Guard: all tiles invalid (no x/y) — sentinels would produce negative Rect2i
|
|
||||||
if min_x > max_x:
|
if min_x > max_x:
|
||||||
return Rect2i(0, 0, 1, 1)
|
return map_bounds
|
||||||
# Margin for fog gradient bleed at edges
|
var tile_bounds := Rect2i(min_x - 8, min_y - 8, max_x - min_x + 17, max_y - min_y + 17)
|
||||||
return Rect2i(min_x - 4, min_y - 4, max_x - min_x + 9, max_y - min_y + 9)
|
if map_bounds.size.x <= 1 and map_bounds.size.y <= 1:
|
||||||
|
return tile_bounds
|
||||||
|
return map_bounds.merge(tile_bounds)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,25 @@
|
|||||||
extends Node
|
extends Node
|
||||||
|
|
||||||
|
signal game_id_changed(new_id: String)
|
||||||
|
|
||||||
# Updated each frame from ObserverSnapshot data (Protocol format: {tick, entities, tiles}).
|
# Updated each frame from ObserverSnapshot data (Protocol format: {tick, entities, tiles}).
|
||||||
# Entities use Protocol decoded format: {entity_id, x, y, z, kind: {variant, data}}.
|
# Entities use Protocol decoded format: {entity_id, x, y, z, kind: {variant, data}}.
|
||||||
# Tiles use format: [{x, y, z, type}].
|
# Tiles use format: [{x, y, z, type}].
|
||||||
var current_snapshot: Dictionary = {}
|
var current_snapshot: Dictionary = {}
|
||||||
|
|
||||||
|
# D-085 (#258): Active game session identifier. Format: <YYYYMMDD>-<HHMMSS>-<hex6>
|
||||||
|
# Set by SessionManager.new_game() or SessionManager.resume_game().
|
||||||
|
# Empty string when no session is active (main menu state).
|
||||||
|
var current_game_id: String = "":
|
||||||
|
set(v):
|
||||||
|
current_game_id = v
|
||||||
|
game_id_changed.emit(v)
|
||||||
var current_tick: int = 0
|
var current_tick: int = 0
|
||||||
var player_position: Vector2 = Vector2.ZERO
|
var player_position: Vector2 = Vector2.ZERO
|
||||||
var visible_entities: Array = []
|
var visible_entities: Array = []
|
||||||
var visible_tiles: Array = []
|
var visible_tiles: Array = []
|
||||||
var visible_positions: Dictionary = {} # Vector2i -> true, for fast fog lookups
|
var visible_positions: Dictionary = {} # Vector2i -> true, for fast fog lookups (normal LOS tiles)
|
||||||
|
var boundary_positions: Dictionary = {} # Vector2i -> true, BoundaryWall margin tiles (#585) — visible in fog but not explored
|
||||||
|
|
||||||
# v2 fields (D-015, D-031)
|
# v2 fields (D-015, D-031)
|
||||||
var game_time: Dictionary = {} # {day, time_of_day, day_phase, tick_rate} or empty
|
var game_time: Dictionary = {} # {day, time_of_day, day_phase, tick_rate} or empty
|
||||||
@@ -19,11 +30,20 @@ var visibility_sectors: Dictionary = {} # Vector2i -> "Forward"/"Peripheral"
|
|||||||
# refined when the server assigns explicit player entity IDs).
|
# refined when the server assigns explicit player entity IDs).
|
||||||
var player_entity_id: int = 1
|
var player_entity_id: int = 1
|
||||||
|
|
||||||
|
# #241: Follow target — entity_id of the NPC the player is following, -1 when not following.
|
||||||
|
# Stub for server ticket #241 (Follow verb). Client reads this for camera/UI behavior.
|
||||||
|
var follow_target_id: int = -1
|
||||||
|
|
||||||
# v4 fields (#404/#405)
|
# v4 fields (#404/#405)
|
||||||
var nearby_interactions: Array = [] # [{entity_id, entity_type, distance, verbs: [{kind, label, priority, available}]}]
|
var nearby_interactions: Array = [] # [{entity_id, entity_type, distance, verbs: [{kind, label, priority, available}]}]
|
||||||
|
|
||||||
# v5 fields (#414)
|
# v5 fields (#414)
|
||||||
var current_monologue: Variant = null # {id, text, duration_seconds} or null
|
var current_monologue: Variant = null # {id, text, duration_seconds, priority, is_urgent} or null
|
||||||
|
|
||||||
|
# #122 (D-032): Character lattice profile — selects monologue text colour palette.
|
||||||
|
# "lattice_augmented" = detective, "lattice_baseline" = smuggler.
|
||||||
|
# Server sends this field as part of the player's capability snapshot.
|
||||||
|
var lattice_profile: String = "lattice_baseline"
|
||||||
|
|
||||||
# v6 fields (#449, D-053, D-065)
|
# v6 fields (#449, D-053, D-065)
|
||||||
var player_stance: String = "Walk" # Sprint/Walk/Careful/Crouch
|
var player_stance: String = "Walk" # Sprint/Walk/Careful/Crouch
|
||||||
@@ -40,9 +60,85 @@ var dialogue_active: bool = false
|
|||||||
var room_id: Variant = null # String room_id from snapshot, null in non-gauntlet mode
|
var room_id: Variant = null # String room_id from snapshot, null in non-gauntlet mode
|
||||||
var gauntlet_mode: bool = false # true when snapshot includes gauntlet_mode flag
|
var gauntlet_mode: bool = false # true when snapshot includes gauntlet_mode flag
|
||||||
|
|
||||||
|
# OQ-07 (#522): Insert active state — false suppresses verb labels (z-layer 6).
|
||||||
|
# Cursor shape changes still fire when false (D-056 option a).
|
||||||
|
# v0.1 assumption: always true — both playable characters (detective and smuggler)
|
||||||
|
# have neural inserts. Future characters without inserts would receive false from
|
||||||
|
# the server's "insert_active" snapshot field, disabling all z-layer-6 UI.
|
||||||
|
var insert_active: bool = true
|
||||||
|
|
||||||
|
# #175: World seed for deterministic simulation (D-010, D-029).
|
||||||
|
# Set by SessionManager.new_game(), sent to server via StartupMessage in SimBridge.
|
||||||
|
# Same seed → same EntanglementConfig → same NPC population across playthroughs.
|
||||||
|
# Persists for the session lifetime; not overwritten by apply_snapshot().
|
||||||
|
var world_seed: int = 0
|
||||||
|
|
||||||
|
# #507: RNG seed for replay determinism — populated from snapshot "rng_seed" field.
|
||||||
|
# Null in v0.1 (server does not yet send this field; protocol change required).
|
||||||
|
var rng_seed: Variant = null
|
||||||
|
|
||||||
|
# v15 fields (#554, D-085): save/load result from server.
|
||||||
|
# {success: bool, kind: "save"|"load", error: Variant} or null.
|
||||||
|
# One-shot: consumed by main.gd after display, then set back to null.
|
||||||
|
var save_result: Variant = null
|
||||||
|
|
||||||
|
# v18 fields (#580): debug console response from server.
|
||||||
|
# {command: String, text: String, success: bool} or null.
|
||||||
|
# One-shot: consumed by main.gd and forwarded to DebugConsole, then set to null.
|
||||||
|
var debug_response: Variant = null
|
||||||
|
|
||||||
|
# #257: Pending load path — set by main menu "Load Game" selection.
|
||||||
|
# main.gd sends LOAD_GAME on startup if non-empty, then clears this field.
|
||||||
|
# Format: user://saves/<game-id>/<filename>.sav or "" if no pending load.
|
||||||
|
var pending_load_path: String = ""
|
||||||
|
|
||||||
# v7 fields (#431, D-059/D-060)
|
# v7 fields (#431, D-059/D-060)
|
||||||
var pending_recognitions: Array = [] # [{entity_id, x, y, z, remaining_ticks, total_delay_ticks}]
|
var pending_recognitions: Array = [] # [{entity_id, x, y, z, remaining_ticks, total_delay_ticks}]
|
||||||
|
|
||||||
|
# v8 fields (#305, D-028): NPC follow-up after player dialogue choice
|
||||||
|
var dialogue_response: Variant = null # {line_id, text, speaker_entity_id}
|
||||||
|
|
||||||
|
# v9 fields (#535, D-078): Overheard NPC-to-NPC conversations
|
||||||
|
var conversation_events: Array = [] # [{speaker_id, target_id, speaker_name, target_name, occluded_line}]
|
||||||
|
var conversation_ended: Array = [] # [{speaker_id, target_id}]
|
||||||
|
|
||||||
|
# v10 fields (#151, D-013): Discovered POIs from server (#148/#149).
|
||||||
|
# Format: [{poi_id, name, x, y, z, category}]. Persists between snapshots unless
|
||||||
|
# server explicitly sends an empty array (cleared locations are not typical in v0.1).
|
||||||
|
# Populated from snapshot "poi_list" field — only updated when field present.
|
||||||
|
var discovered_pois: Array = []
|
||||||
|
|
||||||
|
# v14 fields (#174, #242): Character-filtered examine result.
|
||||||
|
# {entity_id, text, confidence} or null. Auto-dismisses on client after 4-6 seconds.
|
||||||
|
var current_examine_result: Variant = null
|
||||||
|
|
||||||
|
# v14 fields (#264, D-041): Player knowledge graph dump for journal panel.
|
||||||
|
# {entities: [{entity_id, name, confidence, source, state, relationship, last_observed_tick}],
|
||||||
|
# facts: [{fact_id, confidence, source, state, acquired_tick}]}
|
||||||
|
var player_knowledge: Variant = null
|
||||||
|
|
||||||
|
# #126, D-018: Medium-range sound events for fog-edge directional indicators.
|
||||||
|
# Format: [{x, y, event_type, range_category}] — server sends current medium events per tick.
|
||||||
|
var medium_sound_events: Array = []
|
||||||
|
|
||||||
|
# #125, D-018: Close-range sound events for positional 2D audio.
|
||||||
|
# Format: [{x, y, event_type, range_category}] — consumed once per tick in main.gd.
|
||||||
|
var close_sound_events: Array = []
|
||||||
|
|
||||||
|
# D-071 (#530): Consecutive ticks without player position change.
|
||||||
|
# D-020: Server-authoritative — read from snapshot "stationary_ticks" field.
|
||||||
|
# Fallback: client-side accumulation (deprecated, remove when server populates field).
|
||||||
|
# ListeningFocus boost activates at 30+ ticks (main.gd manages the dip).
|
||||||
|
var stationary_ticks: int = 0
|
||||||
|
# DEPRECATED: Only used by client-side accumulation fallback. Remove with fallback.
|
||||||
|
var _prev_player_position: Vector2 = Vector2(-1e9, -1e9) # sentinel: no previous position
|
||||||
|
|
||||||
|
# D-073 (#529): Server-authoritative zone_id from the player's current tile.
|
||||||
|
# D-020: Read directly from snapshot "zone_id" field.
|
||||||
|
# Fallback: client-side tile lookup (deprecated, remove when server populates field).
|
||||||
|
# Empty string when zone_id field absent.
|
||||||
|
var current_zone_id: String = ""
|
||||||
|
|
||||||
func apply_snapshot(snapshot: Dictionary) -> void:
|
func apply_snapshot(snapshot: Dictionary) -> void:
|
||||||
current_snapshot = snapshot
|
current_snapshot = snapshot
|
||||||
|
|
||||||
@@ -64,6 +160,21 @@ func apply_snapshot(snapshot: Dictionary) -> void:
|
|||||||
push_warning("GameState: no Player entity found in %d entities" % [
|
push_warning("GameState: no Player entity found in %d entities" % [
|
||||||
visible_entities.size()])
|
visible_entities.size()])
|
||||||
|
|
||||||
|
# D-020/D-071 (#530): Server-authoritative stationary_ticks for ListeningFocus boost.
|
||||||
|
# Prefer server-sent value; fall back to client-side accumulation until server populates.
|
||||||
|
if snapshot.has("stationary_ticks") and snapshot.stationary_ticks is int:
|
||||||
|
# D-020: direct field assignment from server-authoritative snapshot.
|
||||||
|
stationary_ticks = snapshot.stationary_ticks
|
||||||
|
else:
|
||||||
|
# DEPRECATED fallback — client-side accumulation. Remove when server sends
|
||||||
|
# "stationary_ticks" in ObserverSnapshot (D-020 violation: derives behavior-
|
||||||
|
# driving state on the client). Server tracks this in ListeningFocus component.
|
||||||
|
if player_position == _prev_player_position:
|
||||||
|
stationary_ticks += 1
|
||||||
|
else:
|
||||||
|
stationary_ticks = 0
|
||||||
|
_prev_player_position = player_position
|
||||||
|
|
||||||
# Tiles for rendering: test mode sends "tiles", live server sends tile data in "visible_tiles"
|
# Tiles for rendering: test mode sends "tiles", live server sends tile data in "visible_tiles"
|
||||||
if snapshot.has("tiles"):
|
if snapshot.has("tiles"):
|
||||||
visible_tiles = snapshot.tiles
|
visible_tiles = snapshot.tiles
|
||||||
@@ -100,6 +211,10 @@ func apply_snapshot(snapshot: Dictionary) -> void:
|
|||||||
else:
|
else:
|
||||||
current_monologue = null
|
current_monologue = null
|
||||||
|
|
||||||
|
# #122: lattice_profile — character insert capability level for monologue colour
|
||||||
|
if snapshot.has("lattice_profile") and snapshot.lattice_profile is String:
|
||||||
|
lattice_profile = snapshot.lattice_profile
|
||||||
|
|
||||||
# v6: player_stance (#449, D-053)
|
# v6: player_stance (#449, D-053)
|
||||||
if snapshot.has("player_stance") and snapshot.player_stance is String:
|
if snapshot.has("player_stance") and snapshot.player_stance is String:
|
||||||
player_stance = snapshot.player_stance
|
player_stance = snapshot.player_stance
|
||||||
@@ -122,6 +237,24 @@ func apply_snapshot(snapshot: Dictionary) -> void:
|
|||||||
else:
|
else:
|
||||||
pending_recognitions = []
|
pending_recognitions = []
|
||||||
|
|
||||||
|
# v9: conversation_events (#535, D-078) — overheard NPC-to-NPC lines
|
||||||
|
if snapshot.has("conversation_events") and snapshot.conversation_events is Array:
|
||||||
|
conversation_events = snapshot.conversation_events
|
||||||
|
else:
|
||||||
|
conversation_events = []
|
||||||
|
|
||||||
|
# v9: conversation_ended (#535, D-078) — pairs whose conversation ended
|
||||||
|
if snapshot.has("conversation_ended") and snapshot.conversation_ended is Array:
|
||||||
|
conversation_ended = snapshot.conversation_ended
|
||||||
|
else:
|
||||||
|
conversation_ended = []
|
||||||
|
|
||||||
|
# v8: dialogue_response (#305, D-028) — NPC follow-up after player choice
|
||||||
|
if snapshot.has("dialogue_response") and snapshot.dialogue_response is Dictionary:
|
||||||
|
dialogue_response = snapshot.dialogue_response
|
||||||
|
else:
|
||||||
|
dialogue_response = null
|
||||||
|
|
||||||
# v8: gauntlet mode (#496) — room_id and gauntlet_mode
|
# v8: gauntlet mode (#496) — room_id and gauntlet_mode
|
||||||
if snapshot.has("gauntlet_mode") and snapshot.gauntlet_mode == true:
|
if snapshot.has("gauntlet_mode") and snapshot.gauntlet_mode == true:
|
||||||
gauntlet_mode = true
|
gauntlet_mode = true
|
||||||
@@ -132,18 +265,107 @@ func apply_snapshot(snapshot: Dictionary) -> void:
|
|||||||
else:
|
else:
|
||||||
room_id = null
|
room_id = null
|
||||||
|
|
||||||
|
# OQ-07 (#522): insert_active — defaults true (v0.1 always has insert).
|
||||||
|
# Server may send false for characters without an insert in future sprints.
|
||||||
|
if snapshot.has("insert_active") and snapshot.insert_active is bool:
|
||||||
|
insert_active = snapshot.insert_active
|
||||||
|
else:
|
||||||
|
insert_active = true
|
||||||
|
|
||||||
|
# #507: rng_seed — server sends current RNG seed for replay determinism.
|
||||||
|
# Field: "rng_seed" (u64 as integer). Null if server does not include it.
|
||||||
|
if snapshot.has("rng_seed"):
|
||||||
|
rng_seed = snapshot.rng_seed
|
||||||
|
else:
|
||||||
|
rng_seed = null
|
||||||
|
|
||||||
|
# D-018: Sound events from server — partition by range_category.
|
||||||
|
# #126: Medium → fog-edge directional indicators.
|
||||||
|
# #125: Close → positional 2D audio via AudioManager.
|
||||||
|
if snapshot.has("sound_events") and snapshot.sound_events is Array:
|
||||||
|
medium_sound_events = []
|
||||||
|
close_sound_events = []
|
||||||
|
for se in snapshot.sound_events:
|
||||||
|
if not se is Dictionary:
|
||||||
|
continue
|
||||||
|
var rc: String = se.get("range_category", "")
|
||||||
|
if rc == "Medium":
|
||||||
|
medium_sound_events.append(se)
|
||||||
|
elif rc == "Close":
|
||||||
|
close_sound_events.append(se)
|
||||||
|
else:
|
||||||
|
medium_sound_events = []
|
||||||
|
close_sound_events = []
|
||||||
|
|
||||||
|
# v10: discovered_pois (#151, D-013) — server sends POIs discovered by the player.
|
||||||
|
# Accepts "discovered_pois" or "poi_list" key — both map to the same client field.
|
||||||
|
# Only update if the field is present — absence means "no change since last tick".
|
||||||
|
if snapshot.has("discovered_pois") and snapshot.discovered_pois is Array:
|
||||||
|
discovered_pois = snapshot.discovered_pois
|
||||||
|
elif snapshot.has("poi_list") and snapshot.poi_list is Array:
|
||||||
|
discovered_pois = snapshot.poi_list
|
||||||
|
|
||||||
|
# v14: examine_result (#174, #242) — character-filtered observation from Examine verb.
|
||||||
|
if snapshot.has("examine_result") and snapshot.examine_result is Dictionary:
|
||||||
|
current_examine_result = snapshot.examine_result
|
||||||
|
else:
|
||||||
|
current_examine_result = null
|
||||||
|
|
||||||
|
# v15: save_result (#554, D-085) — one-shot save/load confirmation from server.
|
||||||
|
if snapshot.has("save_result") and snapshot.save_result is Dictionary:
|
||||||
|
save_result = snapshot.save_result
|
||||||
|
else:
|
||||||
|
save_result = null
|
||||||
|
|
||||||
|
# v18: debug_response (#580) — debug console command result.
|
||||||
|
if snapshot.has("debug_response") and snapshot.debug_response is Dictionary:
|
||||||
|
debug_response = snapshot.debug_response
|
||||||
|
else:
|
||||||
|
debug_response = null
|
||||||
|
|
||||||
|
# v14: player_knowledge (#264, D-041) — partial KG dump for journal panel.
|
||||||
|
# Only update when field is present (null means no change, server sends when KG changes).
|
||||||
|
if snapshot.has("player_knowledge") and snapshot.player_knowledge is Dictionary:
|
||||||
|
player_knowledge = snapshot.player_knowledge
|
||||||
|
|
||||||
|
# D-020/D-073 (#529): Server-authoritative zone_id for zone ambient crossfade.
|
||||||
|
# Prefer server-sent top-level value; fall back to client-side tile lookup until
|
||||||
|
# server populates top-level "zone_id" in ObserverSnapshot.
|
||||||
|
if snapshot.has("zone_id") and snapshot.zone_id is String:
|
||||||
|
# D-020: direct field assignment from server-authoritative snapshot.
|
||||||
|
current_zone_id = snapshot.zone_id
|
||||||
|
else:
|
||||||
|
# DEPRECATED fallback — client-side tile lookup. Remove when server sends
|
||||||
|
# top-level "zone_id" in ObserverSnapshot (D-020 violation: derives zone
|
||||||
|
# identity on the client via tile iteration). Server sends zone_id per
|
||||||
|
# VisibleTile but not as a top-level snapshot field.
|
||||||
|
var _tile_by_coord: Dictionary = {}
|
||||||
|
for vtile in visible_tiles:
|
||||||
|
if vtile is Dictionary and vtile.has("x") and vtile.has("y"):
|
||||||
|
_tile_by_coord[Vector2i(vtile.x, vtile.y)] = vtile
|
||||||
|
var player_pos_key := Vector2i(int(player_position.x), int(player_position.y))
|
||||||
|
var player_tile = _tile_by_coord.get(player_pos_key, null)
|
||||||
|
current_zone_id = player_tile.get("zone_id", "") if player_tile else ""
|
||||||
|
|
||||||
# v2: visible_tiles with visibility sectors
|
# v2: visible_tiles with visibility sectors
|
||||||
# Derives visible_positions when not explicitly provided (real server mode)
|
# Derives visible_positions when not explicitly provided (real server mode).
|
||||||
|
# #585: BoundaryWall tiles go to boundary_positions — rendered in fog but not marked explored.
|
||||||
if snapshot.has("visible_tiles") and snapshot.visible_tiles is Array and snapshot.visible_tiles.size() > 0:
|
if snapshot.has("visible_tiles") and snapshot.visible_tiles is Array and snapshot.visible_tiles.size() > 0:
|
||||||
visibility_sectors.clear()
|
visibility_sectors.clear()
|
||||||
var has_explicit_positions := snapshot.has("visible_positions")
|
var has_explicit_positions := snapshot.has("visible_positions")
|
||||||
if not has_explicit_positions:
|
if not has_explicit_positions:
|
||||||
visible_positions.clear()
|
visible_positions.clear()
|
||||||
|
boundary_positions.clear()
|
||||||
for vtile in snapshot.visible_tiles:
|
for vtile in snapshot.visible_tiles:
|
||||||
if not vtile is Dictionary or not vtile.has("x") or not vtile.has("y"):
|
if not vtile is Dictionary or not vtile.has("x") or not vtile.has("y"):
|
||||||
continue
|
continue
|
||||||
var pos := Vector2i(vtile.x, vtile.y)
|
var pos := Vector2i(vtile.x, vtile.y)
|
||||||
|
var vis_sector: String = vtile.get("visibility", "")
|
||||||
if vtile.has("visibility"):
|
if vtile.has("visibility"):
|
||||||
visibility_sectors[pos] = vtile.visibility
|
visibility_sectors[pos] = vis_sector
|
||||||
if not has_explicit_positions:
|
# #585: BoundaryWall tiles are margin tiles visible through fog but not persistently
|
||||||
|
# explored — they don't update the player's exploration memory when they leave LOS.
|
||||||
|
if vis_sector == "BoundaryWall":
|
||||||
|
boundary_positions[pos] = true
|
||||||
|
elif not has_explicit_positions:
|
||||||
visible_positions[pos] = true
|
visible_positions[pos] = true
|
||||||
|
|||||||
@@ -19,8 +19,12 @@ enum Action {
|
|||||||
INTERACT, USE_PERCEPTION_MODE, OPEN_MENU, PAUSE, UNPAUSE,
|
INTERACT, USE_PERCEPTION_MODE, OPEN_MENU, PAUSE, UNPAUSE,
|
||||||
TOGGLE_STANCE_UP, TOGGLE_STANCE_DOWN,
|
TOGGLE_STANCE_UP, TOGGLE_STANCE_DOWN,
|
||||||
BUG_REPORT, # #495: F12 WRONG button — client-only, not sent to server
|
BUG_REPORT, # #495: F12 WRONG button — client-only, not sent to server
|
||||||
|
OPEN_JOURNAL, # #264: J key — toggle knowledge journal panel, client-only
|
||||||
SET_FACING, # D-054: facing octant update (no movement)
|
SET_FACING, # D-054: facing octant update (no movement)
|
||||||
TELEPORT_HUB, # #501: Home key — Gauntlet dev teleport (not production fast-travel)
|
TELEPORT_HUB, # #501: Home key — Gauntlet dev teleport (not production fast-travel)
|
||||||
|
SAVE_GAME, # #554: F5 quicksave — sends SaveGame to server with save path
|
||||||
|
LOAD_GAME, # #554: F6 quickload — sends LoadGame to server with save path
|
||||||
|
DEBUG_COMMAND, # #581: debug console command dispatch — sends DebugCommandKind to server
|
||||||
}
|
}
|
||||||
|
|
||||||
var input_queue: Array[Dictionary] = []
|
var input_queue: Array[Dictionary] = []
|
||||||
@@ -106,15 +110,29 @@ func _unhandled_input(event: InputEvent) -> void:
|
|||||||
action = Action.TOGGLE_STANCE_DOWN
|
action = Action.TOGGLE_STANCE_DOWN
|
||||||
elif event.is_action_pressed("bug_report"):
|
elif event.is_action_pressed("bug_report"):
|
||||||
action = Action.BUG_REPORT
|
action = Action.BUG_REPORT
|
||||||
|
elif event.is_action_pressed("open_journal"):
|
||||||
|
action = Action.OPEN_JOURNAL
|
||||||
elif event.is_action_pressed("teleport_hub"):
|
elif event.is_action_pressed("teleport_hub"):
|
||||||
if GameState.gauntlet_mode:
|
if GameState.gauntlet_mode:
|
||||||
action = Action.TELEPORT_HUB
|
action = Action.TELEPORT_HUB
|
||||||
|
elif event.is_action_pressed("quicksave"):
|
||||||
|
action = Action.SAVE_GAME
|
||||||
|
elif event.is_action_pressed("quickload"):
|
||||||
|
action = Action.LOAD_GAME
|
||||||
|
|
||||||
if action != -1:
|
if action != -1:
|
||||||
input_queue.append({
|
var entry := {
|
||||||
"action": action,
|
"action": action,
|
||||||
"timestamp_msec": Time.get_ticks_msec(),
|
"timestamp_msec": Time.get_ticks_msec(),
|
||||||
})
|
}
|
||||||
|
# #554: Attach save path for SaveGame/LoadGame actions
|
||||||
|
if action == Action.SAVE_GAME or action == Action.LOAD_GAME:
|
||||||
|
var game_id := GameState.current_game_id
|
||||||
|
if game_id.is_empty():
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
return # No active session — ignore save/load
|
||||||
|
entry["action_data"] = {"path": "user://saves/" + game_id + "/quicksave.sav"}
|
||||||
|
input_queue.append(entry)
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
extends Node
|
||||||
|
## D-085 (#258): Game session lifecycle manager.
|
||||||
|
## Creates per-game save directories on New Game, resumes existing sessions,
|
||||||
|
## and handles quit-to-menu flow with save confirmation.
|
||||||
|
##
|
||||||
|
## All save dirs live under user://saves/<game-id>/ where game-id is
|
||||||
|
## <YYYYMMDD>-<HHMMSS>-<hex6> (e.g. "20260225-143022-a7b3f1").
|
||||||
|
|
||||||
|
const SAVES_DIR := "user://saves/"
|
||||||
|
const GAME_SCENE := "res://scenes/main.tscn"
|
||||||
|
const MENU_SCENE := "res://scenes/main_menu.tscn"
|
||||||
|
|
||||||
|
var _quit_dialog: ConfirmationDialog = null
|
||||||
|
|
||||||
|
|
||||||
|
## Generate a new game-id, create its save directory, and activate the session.
|
||||||
|
## Returns the new game-id string.
|
||||||
|
func new_game() -> String:
|
||||||
|
var now := Time.get_datetime_dict_from_system()
|
||||||
|
var timestamp := "%04d%02d%02d-%02d%02d%02d" % [
|
||||||
|
now.year, now.month, now.day,
|
||||||
|
now.hour, now.minute, now.second,
|
||||||
|
]
|
||||||
|
var rng := RandomNumberGenerator.new()
|
||||||
|
var hex_seed := "%06x" % (rng.randi() & 0xFFFFFF)
|
||||||
|
var game_id := "%s-%s" % [timestamp, hex_seed]
|
||||||
|
var save_path := SAVES_DIR + game_id + "/"
|
||||||
|
var err := DirAccess.make_dir_recursive_absolute(save_path)
|
||||||
|
if err != OK:
|
||||||
|
push_error("SessionManager: failed to create save dir %s: %s" % [
|
||||||
|
save_path, error_string(err)])
|
||||||
|
return ""
|
||||||
|
GameState.current_game_id = game_id
|
||||||
|
|
||||||
|
# #175: Generate world_seed for deterministic simulation (D-010, D-029).
|
||||||
|
# Combines two randi() calls (u32 each) into 63-bit entropy range.
|
||||||
|
# Mask bit 31 of the upper word before shifting to prevent signed overflow:
|
||||||
|
# GDScript int is i64 — if bit 63 is set, MessagePack encodes as negative,
|
||||||
|
# and Rust rmp_serde rejects negative values when deserializing as u64.
|
||||||
|
GameState.world_seed = ((rng.randi() & 0x7FFFFFFF) << 32) | rng.randi()
|
||||||
|
|
||||||
|
# Persist world_seed to save directory so resume_game() can restore it.
|
||||||
|
# Without this, loaded sessions would send seed=0, breaking D-010 determinism.
|
||||||
|
_write_seed_file(save_path, GameState.world_seed)
|
||||||
|
|
||||||
|
return game_id
|
||||||
|
|
||||||
|
|
||||||
|
## Resume an existing game session by setting the active game-id.
|
||||||
|
## Restores world_seed from the save directory for D-010 deterministic replay.
|
||||||
|
func resume_game(game_id: String) -> void:
|
||||||
|
GameState.current_game_id = game_id
|
||||||
|
var save_path := SAVES_DIR + game_id + "/"
|
||||||
|
GameState.world_seed = _read_seed_file(save_path)
|
||||||
|
|
||||||
|
|
||||||
|
## List all game directories under user://saves/ sorted by last-modified (most recent first).
|
||||||
|
## Returns Array of {game_id: String, modified_time: int, newest_save: String}.
|
||||||
|
func list_game_dirs() -> Array:
|
||||||
|
var dir := DirAccess.open(SAVES_DIR)
|
||||||
|
if dir == null:
|
||||||
|
return []
|
||||||
|
var results: Array = []
|
||||||
|
dir.list_dir_begin()
|
||||||
|
var entry := dir.get_next()
|
||||||
|
while entry != "":
|
||||||
|
if dir.current_is_dir() and not entry.begins_with("."):
|
||||||
|
var dir_path := SAVES_DIR + entry + "/"
|
||||||
|
var newest_save := _find_newest_save(dir_path)
|
||||||
|
var mtime: int = 0
|
||||||
|
if newest_save != "":
|
||||||
|
mtime = FileAccess.get_modified_time(dir_path + newest_save)
|
||||||
|
results.append({
|
||||||
|
"game_id": entry,
|
||||||
|
"modified_time": mtime,
|
||||||
|
"newest_save": newest_save,
|
||||||
|
})
|
||||||
|
entry = dir.get_next()
|
||||||
|
dir.list_dir_end()
|
||||||
|
results.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||||
|
return a.modified_time > b.modified_time)
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
## Show "Save before quitting?" confirmation dialog, then return to main menu.
|
||||||
|
## #554: The actual F5 save will be wired here once server supports SaveCommand.
|
||||||
|
func quit_to_menu() -> void:
|
||||||
|
if _quit_dialog != null and is_instance_valid(_quit_dialog):
|
||||||
|
return # Dialog already open
|
||||||
|
_quit_dialog = ConfirmationDialog.new()
|
||||||
|
_quit_dialog.dialog_text = UIStrings.get_text("menu.confirm_quit")
|
||||||
|
_quit_dialog.ok_button_text = UIStrings.get_text("menu.confirm_yes")
|
||||||
|
_quit_dialog.cancel_button_text = UIStrings.get_text("menu.confirm_no")
|
||||||
|
get_tree().root.add_child(_quit_dialog)
|
||||||
|
_quit_dialog.confirmed.connect(_do_quit_to_menu)
|
||||||
|
_quit_dialog.canceled.connect(_cleanup_quit_dialog)
|
||||||
|
_quit_dialog.popup_centered()
|
||||||
|
|
||||||
|
|
||||||
|
func _do_quit_to_menu() -> void:
|
||||||
|
_cleanup_quit_dialog()
|
||||||
|
# #554: Trigger quicksave before navigating to menu.
|
||||||
|
# send_input() buffers the command — defer scene change by one frame so
|
||||||
|
# SimBridge._process() flushes the outbound buffer before teardown.
|
||||||
|
if not GameState.current_game_id.is_empty():
|
||||||
|
var path := "user://saves/" + GameState.current_game_id + "/quicksave.sav"
|
||||||
|
SimBridge.send_input({
|
||||||
|
"action": InputMapper.Action.SAVE_GAME,
|
||||||
|
"timestamp_msec": Time.get_ticks_msec(),
|
||||||
|
"action_data": {"path": path},
|
||||||
|
})
|
||||||
|
GameState.current_game_id = ""
|
||||||
|
_navigate_to_menu.call_deferred()
|
||||||
|
else:
|
||||||
|
GameState.current_game_id = ""
|
||||||
|
get_tree().change_scene_to_file(MENU_SCENE)
|
||||||
|
|
||||||
|
|
||||||
|
func _navigate_to_menu() -> void:
|
||||||
|
get_tree().change_scene_to_file(MENU_SCENE)
|
||||||
|
|
||||||
|
|
||||||
|
func _cleanup_quit_dialog() -> void:
|
||||||
|
if _quit_dialog != null and is_instance_valid(_quit_dialog):
|
||||||
|
_quit_dialog.queue_free()
|
||||||
|
_quit_dialog = null
|
||||||
|
|
||||||
|
|
||||||
|
## Write world_seed to a file in the save directory for session persistence.
|
||||||
|
func _write_seed_file(save_path: String, seed: int) -> void:
|
||||||
|
var file := FileAccess.open(save_path + "world_seed", FileAccess.WRITE)
|
||||||
|
if file == null:
|
||||||
|
push_error("SessionManager: failed to write seed file: %s" % error_string(FileAccess.get_open_error()))
|
||||||
|
return
|
||||||
|
file.store_64(seed)
|
||||||
|
|
||||||
|
|
||||||
|
## Read world_seed from save directory. Returns 0 if file missing (legacy saves).
|
||||||
|
## Masks the sign bit on read: save files written before the signed-overflow fix
|
||||||
|
## may contain negative i64 values that Rust rmp_serde rejects as u64.
|
||||||
|
func _read_seed_file(save_path: String) -> int:
|
||||||
|
var file := FileAccess.open(save_path + "world_seed", FileAccess.READ)
|
||||||
|
if file == null:
|
||||||
|
push_warning("SessionManager: no seed file in %s — using seed=0 (legacy save)" % save_path)
|
||||||
|
return 0
|
||||||
|
return file.get_64() & 0x7FFFFFFFFFFFFFFF
|
||||||
|
|
||||||
|
|
||||||
|
func _find_newest_save(dir_path: String) -> String:
|
||||||
|
var dir := DirAccess.open(dir_path)
|
||||||
|
if dir == null:
|
||||||
|
return ""
|
||||||
|
var best_name := ""
|
||||||
|
var best_time: int = 0
|
||||||
|
dir.list_dir_begin()
|
||||||
|
var entry := dir.get_next()
|
||||||
|
while entry != "":
|
||||||
|
if not dir.current_is_dir() and entry.ends_with(".sav"):
|
||||||
|
var mtime := FileAccess.get_modified_time(dir_path + entry)
|
||||||
|
if mtime > best_time:
|
||||||
|
best_time = mtime
|
||||||
|
best_name = entry
|
||||||
|
entry = dir.get_next()
|
||||||
|
dir.list_dir_end()
|
||||||
|
return best_name
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b357ok64jc8vp
|
||||||
@@ -1,17 +1,11 @@
|
|||||||
extends Node
|
extends Node
|
||||||
|
|
||||||
# Connection states
|
# Connection states
|
||||||
enum ConnectionState { DISCONNECTED, CONNECTING, CONNECTED, ERROR }
|
enum ConnectionState { DISCONNECTED, CONNECTING, HANDSHAKING, CONNECTED, ERROR }
|
||||||
|
|
||||||
var state: ConnectionState = ConnectionState.DISCONNECTED
|
var state: ConnectionState = ConnectionState.DISCONNECTED
|
||||||
var test_mode: bool = OS.get_environment("SR_LIVE") != "1" # SR_LIVE=1 connects to real server
|
var test_mode: bool = OS.get_environment("SR_LIVE") != "1" # SR_LIVE=1 connects to real server
|
||||||
var _test_tick: int = 0
|
var harness: TestHarness = null # Test simulation (D-020: game logic lives outside production client)
|
||||||
var _test_player_pos: Vector2i = Vector2i(10, 10)
|
|
||||||
var _test_facing: String = "North"
|
|
||||||
var _test_input_queue: Array = [] # Queued actions for test mode
|
|
||||||
var _test_in_dialogue: bool = false # Mock dialogue state (#434)
|
|
||||||
var _test_gauntlet_mode: bool = false # #501: Gauntlet mode for dev teleport guard
|
|
||||||
var _test_npc_relationship: String = "Unknown" # #521: NPC relationship for D-033 color
|
|
||||||
var _last_snapshot: Variant = null # Most recent decoded snapshot (consumed by poll_snapshot)
|
var _last_snapshot: Variant = null # Most recent decoded snapshot (consumed by poll_snapshot)
|
||||||
var _outbound_buffer: Array[Dictionary] = [] # Raw inputs awaiting batch encode + transport
|
var _outbound_buffer: Array[Dictionary] = [] # Raw inputs awaiting batch encode + transport
|
||||||
|
|
||||||
@@ -27,23 +21,68 @@ const CONNECT_RETRY_INTERVAL: float = 0.1 # Seconds between retry attempts
|
|||||||
var _connect_retries: int = 0
|
var _connect_retries: int = 0
|
||||||
var _retry_timer: float = 0.0
|
var _retry_timer: float = 0.0
|
||||||
|
|
||||||
|
# Handshake state (#556)
|
||||||
|
const HANDSHAKE_TIMEOUT_USEC: int = 5_000_000 # 5 seconds
|
||||||
|
var _handshake_start_usec: int = 0
|
||||||
|
|
||||||
# Signals
|
# Signals
|
||||||
signal connection_state_changed(old_state: ConnectionState, new_state: ConnectionState)
|
signal connection_state_changed(old_state: ConnectionState, new_state: ConnectionState)
|
||||||
signal snapshot_received(snapshot: Dictionary)
|
signal snapshot_received(snapshot: Dictionary)
|
||||||
|
signal handshake_complete(protocol_version: int)
|
||||||
|
signal handshake_failed(reason: String)
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
if test_mode:
|
if test_mode:
|
||||||
|
harness = TestHarness.new()
|
||||||
print("SimBridge: Running in test mode (dynamic snapshot)")
|
print("SimBridge: Running in test mode (dynamic snapshot)")
|
||||||
|
|
||||||
# Reset test state — call before tests that use _test_snapshot()
|
|
||||||
|
# -- Test mode proxy API (backward compat for 13+ test files) ------------------
|
||||||
|
|
||||||
func reset_test_state() -> void:
|
func reset_test_state() -> void:
|
||||||
_test_tick = 0
|
if harness: harness.reset()
|
||||||
_test_player_pos = Vector2i(10, 10)
|
|
||||||
_test_facing = "North"
|
func _test_snapshot() -> Dictionary:
|
||||||
_test_input_queue.clear()
|
return harness.snapshot()
|
||||||
_test_in_dialogue = false
|
|
||||||
_test_gauntlet_mode = false
|
func _test_has_los(from: Vector2i, to: Vector2i) -> bool:
|
||||||
_test_npc_relationship = "Unknown"
|
return harness.has_los(from, to)
|
||||||
|
|
||||||
|
var _test_tick: int:
|
||||||
|
get: return harness.tick if harness else 0
|
||||||
|
set(v):
|
||||||
|
if harness: harness.tick = v
|
||||||
|
|
||||||
|
var _test_player_pos: Vector2i:
|
||||||
|
get: return harness.player_pos if harness else Vector2i.ZERO
|
||||||
|
set(v):
|
||||||
|
if harness: harness.player_pos = v
|
||||||
|
|
||||||
|
var _test_facing: String:
|
||||||
|
get: return harness.facing if harness else "North"
|
||||||
|
set(v):
|
||||||
|
if harness: harness.facing = v
|
||||||
|
|
||||||
|
var _test_in_dialogue: bool:
|
||||||
|
get: return harness.in_dialogue if harness else false
|
||||||
|
set(v):
|
||||||
|
if harness: harness.in_dialogue = v
|
||||||
|
|
||||||
|
var _test_gauntlet_mode: bool:
|
||||||
|
get: return harness.gauntlet_mode if harness else false
|
||||||
|
set(v):
|
||||||
|
if harness: harness.gauntlet_mode = v
|
||||||
|
|
||||||
|
var _test_npc_relationship: String:
|
||||||
|
get: return harness.npc_relationship if harness else "Unknown"
|
||||||
|
set(v):
|
||||||
|
if harness: harness.npc_relationship = v
|
||||||
|
|
||||||
|
var _test_input_queue: Array:
|
||||||
|
get: return harness.input_queue if harness else []
|
||||||
|
|
||||||
|
|
||||||
|
# -- Connection lifecycle ------------------------------------------------------
|
||||||
|
|
||||||
# Change connection state and emit signal
|
# Change connection state and emit signal
|
||||||
func _set_state(new_state: ConnectionState) -> void:
|
func _set_state(new_state: ConnectionState) -> void:
|
||||||
@@ -66,8 +105,13 @@ func connect_to_sim() -> void:
|
|||||||
# Spawn server subprocess
|
# Spawn server subprocess
|
||||||
if not server_path.is_empty():
|
if not server_path.is_empty():
|
||||||
_server = ServerProcess.new()
|
_server = ServerProcess.new()
|
||||||
# Server reads first positional arg as bind address (e.g. "127.0.0.1:9876")
|
# Server reads first positional arg as bind address (e.g. "127.0.0.1:9876").
|
||||||
var pid := _server.start(server_path, ["127.0.0.1:" + str(server_port)])
|
# D-085 (#258): pass --game-id <id> so server logs use the same session identifier.
|
||||||
|
var args := ["127.0.0.1:" + str(server_port)]
|
||||||
|
var game_id: String = GameState.current_game_id
|
||||||
|
if not game_id.is_empty():
|
||||||
|
args.append_array(["--game-id", game_id])
|
||||||
|
var pid := _server.start(server_path, args)
|
||||||
if pid <= 0:
|
if pid <= 0:
|
||||||
push_error("SimBridge: failed to start server")
|
push_error("SimBridge: failed to start server")
|
||||||
_set_state(ConnectionState.ERROR)
|
_set_state(ConnectionState.ERROR)
|
||||||
@@ -121,7 +165,8 @@ func _process(delta: float) -> void:
|
|||||||
_bridge.poll()
|
_bridge.poll()
|
||||||
match _bridge.get_status():
|
match _bridge.get_status():
|
||||||
StreamPeerTCP.STATUS_CONNECTED:
|
StreamPeerTCP.STATUS_CONNECTED:
|
||||||
_set_state(ConnectionState.CONNECTED)
|
_handshake_start_usec = Time.get_ticks_usec()
|
||||||
|
_set_state(ConnectionState.HANDSHAKING)
|
||||||
StreamPeerTCP.STATUS_CONNECTING:
|
StreamPeerTCP.STATUS_CONNECTING:
|
||||||
pass # Still connecting, wait
|
pass # Still connecting, wait
|
||||||
StreamPeerTCP.STATUS_ERROR:
|
StreamPeerTCP.STATUS_ERROR:
|
||||||
@@ -134,6 +179,82 @@ func _process(delta: float) -> void:
|
|||||||
_bridge = null # Reset and retry
|
_bridge = null # Reset and retry
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# HANDSHAKING state: read first framed message, validate HandshakeMessage (#556)
|
||||||
|
if state == ConnectionState.HANDSHAKING:
|
||||||
|
if _bridge == null:
|
||||||
|
_set_state(ConnectionState.ERROR)
|
||||||
|
return
|
||||||
|
_bridge.poll()
|
||||||
|
|
||||||
|
# Check connection dropped during handshake
|
||||||
|
var bridge_status := _bridge.get_status()
|
||||||
|
if bridge_status == StreamPeerTCP.STATUS_ERROR or bridge_status == StreamPeerTCP.STATUS_NONE:
|
||||||
|
var reason := "Connection dropped during handshake"
|
||||||
|
push_error("SimBridge: %s" % reason)
|
||||||
|
handshake_failed.emit(reason)
|
||||||
|
_bridge = null
|
||||||
|
_set_state(ConnectionState.ERROR)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check timeout
|
||||||
|
if Time.get_ticks_usec() - _handshake_start_usec > HANDSHAKE_TIMEOUT_USEC:
|
||||||
|
var reason := "Handshake timeout: no message received within 5 seconds"
|
||||||
|
push_error("SimBridge: %s" % reason)
|
||||||
|
handshake_failed.emit(reason)
|
||||||
|
_bridge.disconnect_from_server()
|
||||||
|
_set_state(ConnectionState.ERROR)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Try to read first message
|
||||||
|
var msg := _bridge.poll_message()
|
||||||
|
if msg.is_empty():
|
||||||
|
return # Not ready yet, continue polling
|
||||||
|
|
||||||
|
# Decode HandshakeMessage: { "protocol_version": N }
|
||||||
|
var decoded: Variant = Messagepack.decode(msg)
|
||||||
|
if decoded.status != null or not (decoded.value is Dictionary) \
|
||||||
|
or not decoded.value.has("protocol_version"):
|
||||||
|
var reason := "Handshake decode failed: malformed HandshakeMessage"
|
||||||
|
push_error("SimBridge: %s" % reason)
|
||||||
|
handshake_failed.emit(reason)
|
||||||
|
_bridge.disconnect_from_server()
|
||||||
|
_set_state(ConnectionState.ERROR)
|
||||||
|
return
|
||||||
|
|
||||||
|
var server_version: int = decoded.value["protocol_version"]
|
||||||
|
if server_version != Protocol.PROTOCOL_VERSION:
|
||||||
|
var reason := "Protocol version mismatch: server=%d, client=%d" % [
|
||||||
|
server_version, Protocol.PROTOCOL_VERSION]
|
||||||
|
push_error("SimBridge: %s" % reason)
|
||||||
|
handshake_failed.emit(reason)
|
||||||
|
_bridge.disconnect_from_server()
|
||||||
|
_set_state(ConnectionState.ERROR)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Send startup message with world_seed (#175, D-010/D-029).
|
||||||
|
# Server blocks waiting for this before entering the tick loop.
|
||||||
|
var startup_bytes := Protocol.encode_startup_message(GameState.world_seed)
|
||||||
|
if startup_bytes.size() > 0:
|
||||||
|
var send_err := _bridge.send_message(startup_bytes)
|
||||||
|
if send_err != OK:
|
||||||
|
var reason := "Failed to send startup message: %s" % error_string(send_err)
|
||||||
|
push_error("SimBridge: %s" % reason)
|
||||||
|
handshake_failed.emit(reason)
|
||||||
|
_bridge.disconnect_from_server()
|
||||||
|
_set_state(ConnectionState.ERROR)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
var reason := "Failed to encode startup message"
|
||||||
|
push_error("SimBridge: %s" % reason)
|
||||||
|
handshake_failed.emit(reason)
|
||||||
|
_bridge.disconnect_from_server()
|
||||||
|
_set_state(ConnectionState.ERROR)
|
||||||
|
return
|
||||||
|
|
||||||
|
handshake_complete.emit(server_version)
|
||||||
|
_set_state(ConnectionState.CONNECTED)
|
||||||
|
return
|
||||||
|
|
||||||
if _bridge == null:
|
if _bridge == null:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -170,37 +291,36 @@ func _process(delta: float) -> void:
|
|||||||
push_warning("SimBridge: connection lost")
|
push_warning("SimBridge: connection lost")
|
||||||
_set_state(ConnectionState.DISCONNECTED)
|
_set_state(ConnectionState.DISCONNECTED)
|
||||||
|
|
||||||
|
|
||||||
|
# -- Input / snapshot ----------------------------------------------------------
|
||||||
|
|
||||||
# Send input to simulation server.
|
# Send input to simulation server.
|
||||||
# player_input: Dictionary with "action" (int from InputMapper.Action enum) and "timestamp_msec".
|
# player_input: Dictionary with "action" (int from InputMapper.Action enum) and "timestamp_msec".
|
||||||
# In test mode, inputs are silently dropped. In live mode, encoded and buffered for transport.
|
# In test mode, inputs are delegated to the test harness.
|
||||||
|
# In live mode, encoded and buffered for transport.
|
||||||
# Returns OK on success, or an error code on failure.
|
# Returns OK on success, or an error code on failure.
|
||||||
func send_input(player_input: Dictionary) -> Error:
|
func send_input(player_input: Dictionary) -> Error:
|
||||||
if state != ConnectionState.CONNECTED:
|
if state != ConnectionState.CONNECTED:
|
||||||
return ERR_CONNECTION_ERROR
|
return ERR_CONNECTION_ERROR
|
||||||
if test_mode:
|
if test_mode:
|
||||||
var action: int = player_input.get("action", -1)
|
var action: int = player_input.get("action", -1)
|
||||||
var wire_name: String = _action_enum_to_wire(action)
|
var wire_name: String = action_enum_to_wire(action)
|
||||||
if not wire_name.is_empty():
|
if not wire_name.is_empty():
|
||||||
if wire_name == "SetFacing":
|
if wire_name == "SetFacing":
|
||||||
# D-054: Use action_data.facing from the input dict, not InputMapper global
|
|
||||||
var facing: String = ""
|
var facing: String = ""
|
||||||
var action_data: Variant = player_input.get("action_data")
|
var action_data: Variant = player_input.get("action_data")
|
||||||
if action_data is Dictionary:
|
if action_data is Dictionary:
|
||||||
facing = str(action_data.get("facing", ""))
|
facing = str(action_data.get("facing", ""))
|
||||||
if not facing.is_empty():
|
if not facing.is_empty():
|
||||||
_test_facing = facing
|
harness.process_facing(facing)
|
||||||
else:
|
else:
|
||||||
_test_input_queue.append(wire_name)
|
harness.process_input(wire_name)
|
||||||
return OK
|
return OK
|
||||||
var action_name := _action_enum_to_wire(player_input.get("action", -1))
|
var action_name := action_enum_to_wire(player_input.get("action", -1))
|
||||||
if action_name.is_empty():
|
if action_name.is_empty():
|
||||||
# _action_enum_to_wire already emits push_warning for invalid actions
|
|
||||||
return ERR_INVALID_PARAMETER
|
return ERR_INVALID_PARAMETER
|
||||||
# Use the server's current tick so drain_for_tick processes this input immediately.
|
|
||||||
# The client-side timestamp_msec is only useful for ordering within a frame.
|
|
||||||
var tick: int = GameState.current_tick
|
var tick: int = GameState.current_tick
|
||||||
var entry: Dictionary = { "tick": tick, "action_name": action_name }
|
var entry: Dictionary = { "tick": tick, "action_name": action_name }
|
||||||
# Data variants (e.g. UsePerceptionMode) carry payload
|
|
||||||
var action_data: Variant = player_input.get("action_data")
|
var action_data: Variant = player_input.get("action_data")
|
||||||
if action_data != null:
|
if action_data != null:
|
||||||
entry["action_data"] = action_data
|
entry["action_data"] = action_data
|
||||||
@@ -208,13 +328,13 @@ func send_input(player_input: Dictionary) -> Error:
|
|||||||
return OK
|
return OK
|
||||||
|
|
||||||
# Poll for snapshot from simulation.
|
# Poll for snapshot from simulation.
|
||||||
# In test mode returns hardcoded data. In live mode, returns the last decoded snapshot (if any).
|
# In test mode delegates to test harness. In live mode, returns the last decoded snapshot.
|
||||||
func poll_snapshot() -> Variant:
|
func poll_snapshot() -> Variant:
|
||||||
if state != ConnectionState.CONNECTED:
|
if state != ConnectionState.CONNECTED:
|
||||||
return null
|
return null
|
||||||
|
|
||||||
if test_mode:
|
if test_mode:
|
||||||
var snapshot = _test_snapshot()
|
var snapshot = harness.snapshot()
|
||||||
snapshot_received.emit(snapshot)
|
snapshot_received.emit(snapshot)
|
||||||
return snapshot
|
return snapshot
|
||||||
|
|
||||||
@@ -241,6 +361,20 @@ func receive_bytes(bytes: PackedByteArray) -> void:
|
|||||||
snapshot["current_monologue"] = _last_snapshot["current_monologue"]
|
snapshot["current_monologue"] = _last_snapshot["current_monologue"]
|
||||||
if snapshot.get("current_dialogue") == null and _last_snapshot.get("current_dialogue") != null:
|
if snapshot.get("current_dialogue") == null and _last_snapshot.get("current_dialogue") != null:
|
||||||
snapshot["current_dialogue"] = _last_snapshot["current_dialogue"]
|
snapshot["current_dialogue"] = _last_snapshot["current_dialogue"]
|
||||||
|
# #535: Carry forward one-shot dialogue events (arrays merge, scalar falls through)
|
||||||
|
if snapshot.get("dialogue_response") == null and _last_snapshot.get("dialogue_response") != null:
|
||||||
|
snapshot["dialogue_response"] = _last_snapshot["dialogue_response"]
|
||||||
|
var old_conv_events: Array = _last_snapshot.get("conversation_events", [])
|
||||||
|
if old_conv_events.size() > 0:
|
||||||
|
var new_conv_events: Array = snapshot.get("conversation_events", [])
|
||||||
|
snapshot["conversation_events"] = old_conv_events + new_conv_events
|
||||||
|
var old_conv_ended: Array = _last_snapshot.get("conversation_ended", [])
|
||||||
|
if old_conv_ended.size() > 0:
|
||||||
|
var new_conv_ended: Array = snapshot.get("conversation_ended", [])
|
||||||
|
snapshot["conversation_ended"] = old_conv_ended + new_conv_ended
|
||||||
|
# #554: Carry forward save/load result (one-shot, consumed by main.gd)
|
||||||
|
if snapshot.get("save_result") == null and _last_snapshot.get("save_result") != null:
|
||||||
|
snapshot["save_result"] = _last_snapshot["save_result"]
|
||||||
_last_snapshot = snapshot
|
_last_snapshot = snapshot
|
||||||
|
|
||||||
# Drain the outbound buffer. Returns raw input entries for batch encoding.
|
# Drain the outbound buffer. Returns raw input entries for batch encoding.
|
||||||
@@ -249,9 +383,12 @@ func drain_outbound() -> Array[Dictionary]:
|
|||||||
_outbound_buffer.clear()
|
_outbound_buffer.clear()
|
||||||
return inputs
|
return inputs
|
||||||
|
|
||||||
|
|
||||||
|
# -- Wire protocol mapping -----------------------------------------------------
|
||||||
|
|
||||||
# Map InputMapper.Action enum values to wire-format action names (matching Rust PlayerAction).
|
# Map InputMapper.Action enum values to wire-format action names (matching Rust PlayerAction).
|
||||||
# OPEN_MENU is client-only — no Rust equivalent, not sent over the wire.
|
# OPEN_MENU is client-only — no Rust equivalent, not sent over the wire.
|
||||||
static func _action_enum_to_wire(action: int) -> String:
|
static func action_enum_to_wire(action: int) -> String:
|
||||||
match action:
|
match action:
|
||||||
InputMapper.Action.MOVE_NORTH: return "MoveNorth"
|
InputMapper.Action.MOVE_NORTH: return "MoveNorth"
|
||||||
InputMapper.Action.MOVE_NORTHEAST: return "MoveNortheast"
|
InputMapper.Action.MOVE_NORTHEAST: return "MoveNortheast"
|
||||||
@@ -275,286 +412,12 @@ static func _action_enum_to_wire(action: int) -> String:
|
|||||||
return "SetFacing" # D-054: facing octant update (no movement)
|
return "SetFacing" # D-054: facing octant update (no movement)
|
||||||
InputMapper.Action.TELEPORT_HUB:
|
InputMapper.Action.TELEPORT_HUB:
|
||||||
return "TeleportToHub" # #501: Gauntlet dev teleport (not production fast-travel)
|
return "TeleportToHub" # #501: Gauntlet dev teleport (not production fast-travel)
|
||||||
|
InputMapper.Action.SAVE_GAME:
|
||||||
|
return "SaveGame" # #554: F5 quicksave (D-085)
|
||||||
|
InputMapper.Action.LOAD_GAME:
|
||||||
|
return "LoadGame" # #554: F6 quickload (D-085)
|
||||||
|
InputMapper.Action.DEBUG_COMMAND:
|
||||||
|
return "DebugCommand" # #581: debug console command dispatch
|
||||||
_:
|
_:
|
||||||
push_warning("SimBridge: unknown action enum %s" % action)
|
push_warning("SimBridge: unknown action enum %s" % action)
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
# Dynamic test snapshot — processes queued inputs to move player, generates
|
|
||||||
# visibility based on current position. Matches Protocol.decode_snapshot() format.
|
|
||||||
# NOTE: Test coordinate space (player at 10,10; NPC at 12,9; wall at 12,10)
|
|
||||||
# is intentionally decoupled from the E2E proof room (player at 16,16; NPC at
|
|
||||||
# 16,13; wall at 16,14). This ensures standalone tests don't depend on server
|
|
||||||
# map layout and can exercise the rendering pipeline independently.
|
|
||||||
func _test_snapshot() -> Dictionary:
|
|
||||||
_test_tick += 1
|
|
||||||
|
|
||||||
# Process queued inputs
|
|
||||||
for action_name in _test_input_queue:
|
|
||||||
if action_name == "TeleportToHub":
|
|
||||||
# #501: Reset to hub spawn position, clear dialogue
|
|
||||||
_test_player_pos = Vector2i(10, 10)
|
|
||||||
_test_in_dialogue = false
|
|
||||||
continue
|
|
||||||
if action_name == "Interact":
|
|
||||||
# Mock dialogue trigger (#434): if near NPC, start dialogue
|
|
||||||
var npc_pos := Vector2i(12, 9)
|
|
||||||
var dist := absi(_test_player_pos.x - npc_pos.x) + absi(_test_player_pos.y - npc_pos.y)
|
|
||||||
if dist <= 2 and _test_has_los(_test_player_pos, npc_pos):
|
|
||||||
_test_in_dialogue = true
|
|
||||||
continue
|
|
||||||
var delta := _action_to_delta(action_name)
|
|
||||||
var new_pos := _test_player_pos + delta
|
|
||||||
if _test_is_walkable(new_pos):
|
|
||||||
_test_player_pos = new_pos
|
|
||||||
if delta != Vector2i.ZERO:
|
|
||||||
# Walk-away dismisses dialogue (D-064)
|
|
||||||
if _test_in_dialogue:
|
|
||||||
_test_in_dialogue = false
|
|
||||||
_test_input_queue.clear()
|
|
||||||
|
|
||||||
var px := _test_player_pos.x
|
|
||||||
var py := _test_player_pos.y
|
|
||||||
|
|
||||||
# Build entities — player always visible
|
|
||||||
var entities: Array = [{
|
|
||||||
"entity_id": 1,
|
|
||||||
"x": float(px),
|
|
||||||
"y": float(py),
|
|
||||||
"z": 0,
|
|
||||||
"kind": { "variant": "Player", "data": null },
|
|
||||||
"visibility": "Forward",
|
|
||||||
}]
|
|
||||||
|
|
||||||
# NPC at (12, 9) — visible if within range and not blocked by wall at (12, 10)
|
|
||||||
var npc_pos := Vector2i(12, 9)
|
|
||||||
var npc_dist := absi(px - npc_pos.x) + absi(py - npc_pos.y)
|
|
||||||
if npc_dist <= 4 and _test_has_los(Vector2i(px, py), npc_pos):
|
|
||||||
var sector: String = "Forward" if npc_pos.y <= py else "Peripheral"
|
|
||||||
entities.append({
|
|
||||||
"entity_id": 2,
|
|
||||||
"x": float(npc_pos.x),
|
|
||||||
"y": float(npc_pos.y),
|
|
||||||
"z": 0,
|
|
||||||
"kind": { "variant": "Npc", "data": null },
|
|
||||||
"visibility": sector,
|
|
||||||
"relationship": _test_npc_relationship,
|
|
||||||
})
|
|
||||||
|
|
||||||
# v4: nearby_interactions when NPC is nearby and visible (#404/#405)
|
|
||||||
var nearby: Array = []
|
|
||||||
if npc_dist <= 2 and _test_has_los(Vector2i(px, py), npc_pos):
|
|
||||||
nearby.append({
|
|
||||||
"entity_id": 2,
|
|
||||||
"entity_type": "Npc",
|
|
||||||
"distance": npc_dist,
|
|
||||||
"verbs": [
|
|
||||||
{"kind": "Talk", "label": "Talk", "priority": 1, "available": true},
|
|
||||||
{"kind": "ExamineNpc", "label": "Observe", "priority": 2, "available": true},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
# v5: monologue on first tick (#414)
|
|
||||||
var monologue: Variant = null
|
|
||||||
if _test_tick == 1:
|
|
||||||
monologue = {
|
|
||||||
"id": "test_enter_001",
|
|
||||||
"text": "Sova Transit District. Population twelve thousand and change.",
|
|
||||||
"duration_seconds": 5.0,
|
|
||||||
}
|
|
||||||
|
|
||||||
# v7: mock dialogue (#435, D-061/D-062) — triggered by Interact near NPC
|
|
||||||
# Sustained: dialogue persists across ticks while _test_in_dialogue is true.
|
|
||||||
# Movement (walk-away) clears it. Client consume-once guards against re-show.
|
|
||||||
# Options: structured {text, response_id, priority} per #435.
|
|
||||||
var dialogue: Variant = null
|
|
||||||
if _test_in_dialogue:
|
|
||||||
dialogue = {
|
|
||||||
"npc_name": "Kael",
|
|
||||||
"npc_entity_id": 2,
|
|
||||||
"speech": "Haven't seen you around the transit hub before. You new to Sova, or just passing through?",
|
|
||||||
"options": [
|
|
||||||
{"text": "Just arrived. Still getting my bearings.", "response_id": "kael_greet_01", "priority": 1, "confrontation": false},
|
|
||||||
{"text": "Passing through. Know where I can find work?", "response_id": "kael_greet_02", "priority": 2, "confrontation": false},
|
|
||||||
{"text": "I saw you near the cargo bay last night.", "response_id": "kael_confront_01", "priority": 3, "confrontation": true},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
# v7: mock pending_recognitions (#431, D-059/D-060) — cognitive delay fog entity
|
|
||||||
# Entity at (13, 12) in fog: starts as grey blob, transitions to recognized over 6 ticks.
|
|
||||||
# Cycles every 12 ticks: 6 ticks recognizing, 6 ticks off (simulates repeat encounters).
|
|
||||||
var pending_recs: Array = []
|
|
||||||
var cycle_pos := _test_tick % 12
|
|
||||||
if cycle_pos < 6:
|
|
||||||
var total_delay := 6
|
|
||||||
var remaining := total_delay - cycle_pos
|
|
||||||
pending_recs.append({
|
|
||||||
"entity_id": 100,
|
|
||||||
"x": 13.5,
|
|
||||||
"y": 12.5,
|
|
||||||
"z": 0,
|
|
||||||
"remaining_ticks": remaining,
|
|
||||||
"total_delay_ticks": total_delay,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
"tick": _test_tick,
|
|
||||||
"version": Protocol.PROTOCOL_VERSION,
|
|
||||||
"game_time": {
|
|
||||||
"day": 0,
|
|
||||||
"time_of_day": _test_tick * 10,
|
|
||||||
"day_phase": "Morning",
|
|
||||||
"tick_rate": "Full",
|
|
||||||
},
|
|
||||||
"player_facing": _test_facing,
|
|
||||||
"player_stance": "Walk",
|
|
||||||
"player_inventory": [],
|
|
||||||
"entities": entities,
|
|
||||||
"tiles": _test_tiles(),
|
|
||||||
"visible_tiles": _test_visible_tiles(),
|
|
||||||
"visible_positions": _test_visible_positions(),
|
|
||||||
"nearby_interactions": nearby,
|
|
||||||
"current_monologue": monologue,
|
|
||||||
"current_dialogue": dialogue,
|
|
||||||
"pending_recognitions": pending_recs,
|
|
||||||
"gauntlet_mode": _test_gauntlet_mode,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Generate a small test room: 8x6 room with walls, a door, and floor
|
|
||||||
func _test_tiles() -> Array:
|
|
||||||
var tiles: Array = []
|
|
||||||
var room_x := 7
|
|
||||||
var room_y := 7
|
|
||||||
var room_w := 8
|
|
||||||
var room_h := 8
|
|
||||||
|
|
||||||
for x in range(room_x, room_x + room_w):
|
|
||||||
for y in range(room_y, room_y + room_h):
|
|
||||||
var is_edge := (x == room_x or x == room_x + room_w - 1
|
|
||||||
or y == room_y or y == room_y + room_h - 1)
|
|
||||||
var tile_type: String
|
|
||||||
if is_edge:
|
|
||||||
# Door on the south wall, center
|
|
||||||
if y == room_y + room_h - 1 and x == room_x + room_w / 2:
|
|
||||||
tile_type = "door"
|
|
||||||
else:
|
|
||||||
tile_type = "wall"
|
|
||||||
else:
|
|
||||||
tile_type = "floor"
|
|
||||||
tiles.append({"x": x, "y": y, "z": 0, "type": tile_type})
|
|
||||||
|
|
||||||
# Corridor south of the door
|
|
||||||
var door_x := room_x + room_w / 2
|
|
||||||
for y in range(room_y + room_h, room_y + room_h + 4):
|
|
||||||
tiles.append({"x": door_x - 1, "y": y, "z": 0, "type": "wall"})
|
|
||||||
tiles.append({"x": door_x, "y": y, "z": 0, "type": "floor"})
|
|
||||||
tiles.append({"x": door_x + 1, "y": y, "z": 0, "type": "wall"})
|
|
||||||
|
|
||||||
return tiles
|
|
||||||
|
|
||||||
# Test visible tiles with visibility sectors (v2 format)
|
|
||||||
# Tiles ahead of the player are Forward, others Peripheral.
|
|
||||||
func _test_visible_tiles() -> Array:
|
|
||||||
var vtiles: Array = []
|
|
||||||
var px := _test_player_pos.x
|
|
||||||
var py := _test_player_pos.y
|
|
||||||
var radius := 4
|
|
||||||
var room_x := 7
|
|
||||||
var room_y := 7
|
|
||||||
var room_w := 8
|
|
||||||
var room_h := 8
|
|
||||||
|
|
||||||
for x in range(px - radius, px + radius + 1):
|
|
||||||
for y in range(py - radius, py + radius + 1):
|
|
||||||
var dist := absf(x - px) + absf(y - py)
|
|
||||||
if dist <= radius:
|
|
||||||
if x >= room_x and x < room_x + room_w and y >= room_y and y < room_y + room_h:
|
|
||||||
var sector: String = "Forward" if y <= py else "Peripheral"
|
|
||||||
vtiles.append({"x": x, "y": y, "z": 0, "visibility": sector})
|
|
||||||
return vtiles
|
|
||||||
|
|
||||||
# Test visibility: tiles within radius 4 of player, inside room bounds
|
|
||||||
func _test_visible_positions() -> Array:
|
|
||||||
var positions: Array = []
|
|
||||||
var px := _test_player_pos.x
|
|
||||||
var py := _test_player_pos.y
|
|
||||||
var radius := 4
|
|
||||||
var room_x := 7
|
|
||||||
var room_y := 7
|
|
||||||
var room_w := 8
|
|
||||||
var room_h := 8
|
|
||||||
|
|
||||||
for x in range(px - radius, px + radius + 1):
|
|
||||||
for y in range(py - radius, py + radius + 1):
|
|
||||||
var dist := absf(x - px) + absf(y - py)
|
|
||||||
if dist <= radius:
|
|
||||||
if x >= room_x and x < room_x + room_w and y >= room_y and y < room_y + room_h:
|
|
||||||
positions.append({"x": x, "y": y})
|
|
||||||
return positions
|
|
||||||
|
|
||||||
|
|
||||||
# -- Test mode helpers --
|
|
||||||
|
|
||||||
const _TEST_WALLS: Array = [
|
|
||||||
# Room walls (8x8 room from (7,7) to (14,14))
|
|
||||||
Vector2i(7,7), Vector2i(8,7), Vector2i(9,7), Vector2i(10,7),
|
|
||||||
Vector2i(11,7), Vector2i(12,7), Vector2i(13,7), Vector2i(14,7),
|
|
||||||
Vector2i(7,14), Vector2i(8,14), Vector2i(9,14), Vector2i(10,14),
|
|
||||||
Vector2i(11,14), Vector2i(12,14), Vector2i(13,14), Vector2i(14,14),
|
|
||||||
Vector2i(7,8), Vector2i(7,9), Vector2i(7,10), Vector2i(7,11),
|
|
||||||
Vector2i(7,12), Vector2i(7,13),
|
|
||||||
Vector2i(14,8), Vector2i(14,9), Vector2i(14,10), Vector2i(14,11),
|
|
||||||
Vector2i(14,12), Vector2i(14,13),
|
|
||||||
# Interior wall blocking NPC
|
|
||||||
Vector2i(12, 10),
|
|
||||||
]
|
|
||||||
|
|
||||||
func _test_is_walkable(pos: Vector2i) -> bool:
|
|
||||||
return not _TEST_WALLS.has(pos)
|
|
||||||
|
|
||||||
# Simple LOS check — blocked if a wall tile sits between start and end
|
|
||||||
func _test_has_los(from: Vector2i, to: Vector2i) -> bool:
|
|
||||||
# Bresenham-lite: check tiles along the line
|
|
||||||
var dx := absi(to.x - from.x)
|
|
||||||
var dy := absi(to.y - from.y)
|
|
||||||
var sx := 1 if from.x < to.x else -1
|
|
||||||
var sy := 1 if from.y < to.y else -1
|
|
||||||
var err := dx - dy
|
|
||||||
var cx := from.x
|
|
||||||
var cy := from.y
|
|
||||||
while true:
|
|
||||||
if cx == to.x and cy == to.y:
|
|
||||||
return true
|
|
||||||
if Vector2i(cx, cy) != from and not _test_is_walkable(Vector2i(cx, cy)):
|
|
||||||
return false
|
|
||||||
var e2 := 2 * err
|
|
||||||
if e2 > -dy:
|
|
||||||
err -= dy
|
|
||||||
cx += sx
|
|
||||||
if e2 < dx:
|
|
||||||
err += dx
|
|
||||||
cy += sy
|
|
||||||
return true
|
|
||||||
|
|
||||||
static func _action_to_delta(action_name: String) -> Vector2i:
|
|
||||||
match action_name:
|
|
||||||
"MoveNorth": return Vector2i(0, -1)
|
|
||||||
"MoveNortheast": return Vector2i(1, -1)
|
|
||||||
"MoveEast": return Vector2i(1, 0)
|
|
||||||
"MoveSoutheast": return Vector2i(1, 1)
|
|
||||||
"MoveSouth": return Vector2i(0, 1)
|
|
||||||
"MoveSouthwest": return Vector2i(-1, 1)
|
|
||||||
"MoveWest": return Vector2i(-1, 0)
|
|
||||||
"MoveNorthwest": return Vector2i(-1, -1)
|
|
||||||
_: return Vector2i.ZERO
|
|
||||||
|
|
||||||
static func _delta_to_facing(delta: Vector2i) -> String:
|
|
||||||
match delta:
|
|
||||||
Vector2i(0, -1): return "North"
|
|
||||||
Vector2i(1, -1): return "Northeast"
|
|
||||||
Vector2i(1, 0): return "East"
|
|
||||||
Vector2i(1, 1): return "Southeast"
|
|
||||||
Vector2i(0, 1): return "South"
|
|
||||||
Vector2i(-1, 1): return "Southwest"
|
|
||||||
Vector2i(-1, 0): return "West"
|
|
||||||
Vector2i(-1, -1): return "Northwest"
|
|
||||||
_: return "North"
|
|
||||||
|
|||||||
@@ -49,44 +49,6 @@ func reload() -> void:
|
|||||||
|
|
||||||
## Parse YAML with arbitrary nesting depth.
|
## Parse YAML with arbitrary nesting depth.
|
||||||
## Returns flat Dictionary with dotted keys: { "section.sub.key": "value" }.
|
## Returns flat Dictionary with dotted keys: { "section.sub.key": "value" }.
|
||||||
|
## Delegates to YamlParser.parse_flat() (#560).
|
||||||
static func _parse_yaml(text: String) -> Dictionary:
|
static func _parse_yaml(text: String) -> Dictionary:
|
||||||
var strings := {}
|
return YamlParser.parse_flat(text)
|
||||||
var stack: Array = [] # [[indent, key], ...]
|
|
||||||
for line in text.split("\n"):
|
|
||||||
var stripped := line.strip_edges(false, true)
|
|
||||||
if stripped.is_empty() or stripped.begins_with("#"):
|
|
||||||
continue
|
|
||||||
var indent := line.length() - line.lstrip(" ").length()
|
|
||||||
var content := stripped.strip_edges()
|
|
||||||
var colon_pos := content.find(":")
|
|
||||||
if colon_pos < 0:
|
|
||||||
continue
|
|
||||||
var key := content.substr(0, colon_pos).strip_edges()
|
|
||||||
var val := content.substr(colon_pos + 1).strip_edges()
|
|
||||||
# Trailing comment without a value — treat as section header
|
|
||||||
if val.begins_with("#"):
|
|
||||||
val = ""
|
|
||||||
# Pop sections at same or deeper indent
|
|
||||||
while stack.size() > 0 and stack.back()[0] >= indent:
|
|
||||||
stack.pop_back()
|
|
||||||
if val.is_empty():
|
|
||||||
# Section header — push onto stack
|
|
||||||
stack.push_back([indent, key])
|
|
||||||
else:
|
|
||||||
# Leaf value — extract from quotes or strip inline comment
|
|
||||||
if val.begins_with("\""):
|
|
||||||
var end_quote := val.find("\"", 1)
|
|
||||||
if end_quote > 0:
|
|
||||||
val = val.substr(1, end_quote - 1)
|
|
||||||
else:
|
|
||||||
val = val.substr(1)
|
|
||||||
else:
|
|
||||||
var comment_pos := val.find(" #")
|
|
||||||
if comment_pos >= 0:
|
|
||||||
val = val.substr(0, comment_pos).strip_edges()
|
|
||||||
var dotted_key := ""
|
|
||||||
for entry in stack:
|
|
||||||
dotted_key += entry[1] + "."
|
|
||||||
dotted_key += key
|
|
||||||
strings[dotted_key] = val
|
|
||||||
return strings
|
|
||||||
|
|||||||
@@ -233,12 +233,7 @@ func _find_entity(entity_id: int) -> bool:
|
|||||||
return false
|
return false
|
||||||
|
|
||||||
|
|
||||||
# -- YAML parsing (checklist-specific) -----------------------------------------
|
# -- YAML parsing --------------------------------------------------------------
|
||||||
# Handles the constrained checklist YAML format: top-level key:value pairs,
|
|
||||||
# a conditions array of flat dictionaries. No nested arrays or anchors.
|
|
||||||
#
|
|
||||||
# Limitation: unquoted values containing " #" are truncated at the comment marker.
|
|
||||||
# Use quoted strings ("value # with hash") if values must contain literal hashes.
|
|
||||||
|
|
||||||
func _load_checklist_file(path: String) -> Dictionary:
|
func _load_checklist_file(path: String) -> Dictionary:
|
||||||
if not FileAccess.file_exists(path):
|
if not FileAccess.file_exists(path):
|
||||||
@@ -252,103 +247,6 @@ func _load_checklist_file(path: String) -> Dictionary:
|
|||||||
return parse_checklist_yaml(text)
|
return parse_checklist_yaml(text)
|
||||||
|
|
||||||
|
|
||||||
|
## Delegates to YamlParser.parse() (#560).
|
||||||
static func parse_checklist_yaml(text: String) -> Dictionary:
|
static func parse_checklist_yaml(text: String) -> Dictionary:
|
||||||
var result := {}
|
return YamlParser.parse(text)
|
||||||
var conditions: Array = []
|
|
||||||
var current_item: Dictionary = {}
|
|
||||||
var in_conditions := false
|
|
||||||
|
|
||||||
for line in text.split("\n"):
|
|
||||||
var stripped := line.strip_edges(false, true)
|
|
||||||
if stripped.is_empty() or stripped.strip_edges().begins_with("#"):
|
|
||||||
continue
|
|
||||||
|
|
||||||
var indent := line.length() - line.lstrip(" ").length()
|
|
||||||
var content := stripped.strip_edges()
|
|
||||||
|
|
||||||
# Detect conditions: array header
|
|
||||||
if content == "conditions:":
|
|
||||||
in_conditions = true
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not in_conditions:
|
|
||||||
# Top-level key: value
|
|
||||||
var colon := content.find(":")
|
|
||||||
if colon >= 0:
|
|
||||||
var key := content.substr(0, colon).strip_edges()
|
|
||||||
var val_str := content.substr(colon + 1).strip_edges()
|
|
||||||
result[key] = _parse_value(val_str)
|
|
||||||
else:
|
|
||||||
if content.begins_with("- "):
|
|
||||||
# New array item — flush previous
|
|
||||||
if not current_item.is_empty():
|
|
||||||
conditions.append(current_item)
|
|
||||||
current_item = {}
|
|
||||||
var rest := content.substr(2).strip_edges()
|
|
||||||
var colon := rest.find(":")
|
|
||||||
if colon >= 0:
|
|
||||||
var key := rest.substr(0, colon).strip_edges()
|
|
||||||
var val_str := rest.substr(colon + 1).strip_edges()
|
|
||||||
current_item[key] = _parse_value(val_str)
|
|
||||||
elif indent >= 2 and not current_item.is_empty():
|
|
||||||
# Continuation of current array item
|
|
||||||
var colon := content.find(":")
|
|
||||||
if colon >= 0:
|
|
||||||
var key := content.substr(0, colon).strip_edges()
|
|
||||||
var val_str := content.substr(colon + 1).strip_edges()
|
|
||||||
current_item[key] = _parse_value(val_str)
|
|
||||||
elif indent == 0:
|
|
||||||
# Back to top level — shouldn't happen in valid checklist YAML
|
|
||||||
in_conditions = false
|
|
||||||
if not current_item.is_empty():
|
|
||||||
conditions.append(current_item)
|
|
||||||
current_item = {}
|
|
||||||
var colon := content.find(":")
|
|
||||||
if colon >= 0:
|
|
||||||
var key := content.substr(0, colon).strip_edges()
|
|
||||||
var val_str := content.substr(colon + 1).strip_edges()
|
|
||||||
result[key] = _parse_value(val_str)
|
|
||||||
|
|
||||||
# Flush last item
|
|
||||||
if not current_item.is_empty():
|
|
||||||
conditions.append(current_item)
|
|
||||||
|
|
||||||
if not conditions.is_empty():
|
|
||||||
result["conditions"] = conditions
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
static func _parse_value(val: String) -> Variant:
|
|
||||||
if val.is_empty():
|
|
||||||
return ""
|
|
||||||
|
|
||||||
# Strip inline comments (not inside quotes)
|
|
||||||
if not val.begins_with("\""):
|
|
||||||
var comment_pos := val.find(" #")
|
|
||||||
if comment_pos >= 0:
|
|
||||||
val = val.substr(0, comment_pos).strip_edges()
|
|
||||||
|
|
||||||
# Quoted string
|
|
||||||
if val.begins_with("\""):
|
|
||||||
var end_quote := val.find("\"", 1)
|
|
||||||
if end_quote > 0:
|
|
||||||
return val.substr(1, end_quote - 1)
|
|
||||||
return val.substr(1)
|
|
||||||
|
|
||||||
# Boolean
|
|
||||||
if val == "true":
|
|
||||||
return true
|
|
||||||
if val == "false":
|
|
||||||
return false
|
|
||||||
|
|
||||||
# Float (contains decimal point)
|
|
||||||
if val.contains(".") and val.is_valid_float():
|
|
||||||
return val.to_float()
|
|
||||||
|
|
||||||
# Integer
|
|
||||||
if val.is_valid_int():
|
|
||||||
return val.to_int()
|
|
||||||
|
|
||||||
# Plain string
|
|
||||||
return val
|
|
||||||
|
|||||||