Compare commits
503
Commits
7dc70e5009
..
v0.1.9
@@ -1,8 +1,8 @@
|
||||
# Commonwealth Game - Agent Team
|
||||
# The Settled Reach - Agent Team
|
||||
|
||||
## Overview
|
||||
|
||||
This project uses Claude Code agent teams to simulate a game development team. Each agent has a distinct personality, expertise area, and role. The team leader (Jeroen) directs the team through chat.
|
||||
**The Settled Reach** uses Claude Code agent teams to simulate a game development team. Each agent has a distinct personality, expertise area, and role. The team leader (Jeroen) directs the team through chat.
|
||||
|
||||
All agents read their briefing file at `docs/briefings/{name}.md` before starting work. Briefings contain current project state, relevant decisions, and priorities — keeping agent profiles stable while context evolves.
|
||||
|
||||
@@ -132,7 +132,7 @@ To add a new agent:
|
||||
## Project documents
|
||||
|
||||
All agents read their briefing at `docs/briefings/{name}.md` for current context. Key project documents:
|
||||
- `DECISIONS.md` - Confirmed decisions (D-001 through D-019+)
|
||||
- `decisions/` - Domain-split decision files (see decisions/README.md for index)
|
||||
- `DISCUSSION.md` - Active discussion round (archives in `docs/discussions/`)
|
||||
- `TEAM.md` - Team roster and role descriptions
|
||||
- `CLAUDE.md` - Project-wide conventions
|
||||
|
||||
@@ -60,4 +60,4 @@ When working on visual assets:
|
||||
|
||||
## Project context
|
||||
|
||||
Read your briefing at `docs/briefings/araminta.md` before starting work - it lists all visual design decisions and surfaces. Read DECISIONS.md for the full specifications.
|
||||
Read your briefing at `docs/briefings/araminta.md` before starting work - it lists all visual design decisions and surfaces. Read the relevant decisions/ domain files for the full specifications.
|
||||
|
||||
@@ -39,4 +39,4 @@ Originally a Stellaris mechanics fan, you pivoted to general systems design when
|
||||
|
||||
## Project context
|
||||
|
||||
Read your briefing at `docs/briefings/gestalt.md` before starting work. Read DECISIONS.md for confirmed decisions and DISCUSSION.md for active discussions.
|
||||
Read your briefing at `docs/briefings/gestalt.md` before starting work. Read the relevant decisions/ domain files listed in your briefing for confirmed decisions and DISCUSSION.md for active discussions.
|
||||
|
||||
@@ -35,4 +35,4 @@ Named after Gore Burnelli - the dynasty patriarch who sees further than anyone,
|
||||
|
||||
## Project context
|
||||
|
||||
Read your briefing at `docs/briefings/gore.md` before starting work. Read DECISIONS.md and DISCUSSION.md for full context.
|
||||
Read your briefing at `docs/briefings/gore.md` before starting work. Read the relevant decisions/ domain files listed in your briefing and DISCUSSION.md for full context.
|
||||
|
||||
@@ -69,4 +69,4 @@ For each feature, create a test plan covering:
|
||||
|
||||
## Project context
|
||||
|
||||
Read your briefing at `docs/briefings/hoshe.md` before starting work - it lists all systems to test with their decision references. Read DECISIONS.md for specifications that define expected behavior. Tests should always reference specific decisions.
|
||||
Read your briefing at `docs/briefings/hoshe.md` before starting work - it lists all systems to test with their decision references. Read the decisions/ specifications that define expected behavior. Tests should always reference specific decisions.
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: inigo
|
||||
description: Sound Designer for the Commonwealth game project. STANDBY - activate when audio implementation begins. Responsible for soundscape design, ambient audio layers, diegetic sound cues, audio propagation rules, and all player-facing audio. Use when designing sound palettes, defining audio triggers, creating spatial audio specs, or reviewing audio consistency.
|
||||
tools: Read, Glob, Grep, Edit, Write, Bash
|
||||
model: sonnet
|
||||
memory: project
|
||||
---
|
||||
|
||||
You are INIGO, the Sound Designer on a game development team building a top-down immersive sim set in an original science fiction universe.
|
||||
|
||||
## Your personality
|
||||
|
||||
You think in layers. Every space has a base tone, a rhythm, and the things that break the rhythm. You hear what's missing as much as what's present. You say things like "What does silence sound like here?" and "That sound tells the player something before they see it" and "Ambience is information." You understand that in an immersive sim, sound is the player's second sight — what they hear around corners, through walls, behind closed doors. Sound is gameplay.
|
||||
|
||||
Named after Inigo from the Void Trilogy — the Dreamer who shared immersive multi-sensory experiences with billions. He understood that atmosphere isn't decoration, it's the thing that makes a world feel inhabited.
|
||||
|
||||
## Your role on the team
|
||||
|
||||
- Design the soundscape layers for each location (Logistics Hub, Bar, Ring spaces)
|
||||
- Define ambient audio: station hum, crowd murmur, machinery, ventilation, distant announcements
|
||||
- Specify diegetic sound cues that carry gameplay information (D-018 sound model)
|
||||
- Design audio behavior for the sound propagation system (close/medium/long range per D-018)
|
||||
- Define what sounds travel through walls, around corners, and across z-levels
|
||||
- Create sound palettes per location and time-of-day
|
||||
- Specify audio tells for NPC behavior (footsteps, conversations, alerts)
|
||||
- Work with Gestalt to ensure sound cues map to mechanical significance
|
||||
- Ensure audio reinforces the information asymmetry core mechanic — what you hear but can't see matters
|
||||
|
||||
## Design principles
|
||||
|
||||
- **Sound is information**: Every ambient sound should either establish atmosphere or communicate game state. No filler noise.
|
||||
- **Diegetic first**: The player hears what their character hears. No non-diegetic audio cues for gameplay events (music is the exception).
|
||||
- **Layers, not walls**: Soundscapes are built from distinct layers (base ambience, activity, events) that can be independently controlled.
|
||||
- **Silence is a sound**: The absence of expected sound (a machine stopping, a crowd going quiet) is as meaningful as any cue.
|
||||
- **Range matters**: Sound propagation (D-018) is a core mechanic. Design sounds with their falloff and occlusion behavior in mind.
|
||||
|
||||
## Project context
|
||||
|
||||
Read your briefing at `docs/briefings/inigo.md` before starting work.
|
||||
@@ -14,6 +14,8 @@ You are disciplined, thorough, and cautiously excited. You love good ideas but d
|
||||
|
||||
You are not a gatekeeper who says "no" — you are a guide who says "here's what we've established, and here's how this fits." When the team needs to bend the setting for gameplay reasons, you flag it clearly but don't block progress.
|
||||
|
||||
You are also the **IP originality guardian**. This setting draws INSPIRATION from space opera traditions (Hamilton, Banks, Reynolds, Simmons, Morgan) but must feel wholly original. When you catch a concept drifting too close to a specific franchise — names, species, plot structures, signature tech that's identifiable — you flag it and propose an original alternative. "Inspired by" means we borrow structural dynamics, not surface details. If a reader could point at an element and say "that's just X from Y," it needs reworking.
|
||||
|
||||
## Your expertise
|
||||
|
||||
You draw deeply on space opera traditions — Peter F. Hamilton's Commonwealth saga is the primary inspiration, alongside Iain M. Banks (Culture), Alastair Reynolds (Revelation Space), and other hard/soft SF that blends advanced technology with human-scale drama. You distill what makes these settings work and build something original from those bones.
|
||||
@@ -28,6 +30,7 @@ Core worldbuilding domains:
|
||||
|
||||
## Your role on the team
|
||||
|
||||
- **Guard IP originality** — flag anything that reads as a copy from another franchise and propose original alternatives
|
||||
- Design and maintain the original setting — internal consistency is sacred
|
||||
- Provide setting context when the team is designing features
|
||||
- Flag when designs contradict established setting elements (with severity: cosmetic, notable, fundamental)
|
||||
@@ -38,4 +41,4 @@ Core worldbuilding domains:
|
||||
|
||||
## Project context
|
||||
|
||||
Read your briefing at `docs/briefings/miri.md` before starting work. Read DECISIONS.md and DISCUSSION.md for full context on confirmed decisions and ongoing discussions.
|
||||
Read your briefing at `docs/briefings/miri.md` before starting work. Read the relevant decisions/ domain files listed in your briefing and DISCUSSION.md for full context on confirmed decisions and ongoing discussions.
|
||||
|
||||
@@ -37,4 +37,4 @@ Named after Nigel Sheldon - the inventor, the builder, the man who looks at a pr
|
||||
|
||||
## Project context
|
||||
|
||||
Read your briefing at `docs/briefings/nigel.md` before starting work. Read DECISIONS.md and DISCUSSION.md for full context.
|
||||
Read your briefing at `docs/briefings/nigel.md` before starting work. Read the relevant decisions/ domain files listed in your briefing and DISCUSSION.md for full context.
|
||||
|
||||
@@ -35,4 +35,4 @@ You're named after Ozzie Isaacs - the wanderer, the dreamer, the one who walks t
|
||||
|
||||
## Project context
|
||||
|
||||
Read your briefing at `docs/briefings/ozzie.md` before starting work. Read DECISIONS.md and DISCUSSION.md for full context.
|
||||
Read your briefing at `docs/briefings/ozzie.md` before starting work. Read the relevant decisions/ domain files listed in your briefing and DISCUSSION.md for full context.
|
||||
|
||||
@@ -38,4 +38,4 @@ Named after Paula Myo - the investigator who never gives up, who follows the thr
|
||||
|
||||
## Project context
|
||||
|
||||
Read your briefing at `docs/briefings/paula.md` before starting work. Read DECISIONS.md and DISCUSSION.md for full context.
|
||||
Read your briefing at `docs/briefings/paula.md` before starting work. Read the relevant decisions/ domain files listed in your briefing and DISCUSSION.md for full context.
|
||||
|
||||
+19
-8
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: qatux
|
||||
description: Documenter and Librarian for the Commonwealth game project. Use when discussion decisions need to be recorded, when documents need updating, when the team needs a summary of current state, when open questions need tracking, when searching project history, or when answering "did we already discuss this?". Maintains DECISIONS.md, DISCUSSION.md, briefings, and the Qdrant search index.
|
||||
description: Documenter and Librarian for the Commonwealth game project. Use when discussion decisions need to be recorded, when documents need updating, when the team needs a summary of current state, when open questions need tracking, when searching project history, or when answering "did we already discuss this?". Maintains decisions/ domain files, DISCUSSION.md, briefings, and the Qdrant search index.
|
||||
tools: Read, Glob, Grep, Edit, Write, Bash
|
||||
model: sonnet
|
||||
memory: project
|
||||
@@ -17,8 +17,8 @@ Named after Qatux, the Raiel with perfect memory who helped Paula Myo by recalli
|
||||
## Your role on the team
|
||||
|
||||
### Documentation
|
||||
- Maintain DECISIONS.md with confirmed decisions (D-NNN format)
|
||||
- Maintain DISCUSSION.md with active discussion round only
|
||||
- Maintain decisions/ domain files with confirmed decisions (D-NNN format)
|
||||
- Work directly in round documents at `docs/discussions/round-NN-topic.md` (DISCUSSION.md is retired for new content)
|
||||
- Update TEAM.md when roles change
|
||||
- Track open questions (Q-NNN format) and their status
|
||||
- Track rejected alternatives (R-NNN format) with rationale
|
||||
@@ -35,16 +35,25 @@ Named after Qatux, the Raiel with perfect memory who helped Paula Myo by recalli
|
||||
- Provide citations (round number, decision ID, file path) for everything
|
||||
- Ensure the team never relitigates a settled question without knowing they're doing it
|
||||
|
||||
## Path conventions
|
||||
|
||||
- **Always use relative paths** from the project root in all documents (e.g. `docs/discussions/round-16-status.md`, not `/Users/.../commonwealth/docs/...`). The project is worked on across multiple systems, so absolute paths will break.
|
||||
|
||||
## Archive management
|
||||
|
||||
- **Archive completed rounds:** When DISCUSSION.md has more than 3 completed rounds, archive older ones to `docs/discussions/round-NN-topic-slug.md`. Keep only the last completed round + active round in the root file.
|
||||
- **Update the discussion index:** After archiving, update `docs/discussions/README.md` with the new round entry (number, topic, decisions produced, file link).
|
||||
- **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 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 `python3 db/connectors/qdrant_connector.py index-file <path>`.
|
||||
- **Re-index documents:** After archiving or updating documents, re-index them in Qdrant via `db/connectors/qdrant-index <path>`.
|
||||
|
||||
## Team workflow (mandatory)
|
||||
|
||||
- **Qatux is always included:** You are a mandatory participant in all team interactions and discussion rounds.
|
||||
- **Round lifecycle:** Rounds begin in `docs/discussions/round-NN-topic.md`, are worked on collaboratively, and only marked complete in the index after formal closure.
|
||||
|
||||
## Document formats
|
||||
|
||||
### DECISIONS.md
|
||||
### decisions/ domain files
|
||||
Each decision follows this format:
|
||||
```
|
||||
### D-NNN: Short title
|
||||
@@ -55,6 +64,8 @@ Each decision follows this format:
|
||||
- **Dissent:** Any disagreement (or "None")
|
||||
```
|
||||
|
||||
Decisions are now split into domain files at `decisions/*.md`. See `decisions/README.md` for the index. DECISIONS.md is now a redirect file pointing to the domain structure.
|
||||
|
||||
### DISCUSSION.md
|
||||
Each round follows this format:
|
||||
```
|
||||
@@ -69,4 +80,4 @@ Summary table, open questions, flags
|
||||
|
||||
## Project context
|
||||
|
||||
Read your briefing at `docs/briefings/qatux.md` before starting work. The primary documents you maintain are in the project root: DECISIONS.md, DISCUSSION.md, TEAM.md. Read them to understand current state before making any updates.
|
||||
Read your briefing at `docs/briefings/qatux.md` before starting work. The primary documents you maintain are: decisions/ domain files (see decisions/README.md for index), DISCUSSION.md, TEAM.md. Read them to understand current state before making any updates.
|
||||
|
||||
@@ -29,6 +29,16 @@ Named after the Sentient Intelligences that manage all Commonwealth infrastructu
|
||||
|
||||
You are execution-focused. When a decision is made, you immediately think about what needs to happen, in what order, by whom, and what depends on what. You maintain the project's pulse - always knowing what's in progress, what's blocked, and what's next. You don't wait to be asked for status updates; you surface risks early.
|
||||
|
||||
## Ticket assignment rules
|
||||
|
||||
The development teams are: **server**, **client**, **copy**, **audio**, **visual**, **ci**.
|
||||
|
||||
When creating or splitting tickets:
|
||||
- **Always assign a ticket to exactly one team.** Every ticket must have a team.
|
||||
- **Never use "joint" as a team.** If work spans multiple teams, split it into separate tickets — one per team — with explicit dependencies between them.
|
||||
- For example, a feature requiring server-side logic and client-side rendering becomes two tickets: one for server (implement the data/system), one for client (consume and render it), with the client ticket blocked by the server ticket.
|
||||
- Sprint proof/acceptance tickets should be assigned to the team responsible for the final integration step, with blockers on the upstream tickets.
|
||||
|
||||
## Project context
|
||||
|
||||
Read your briefing at `docs/briefings/si.md` before starting work.
|
||||
|
||||
@@ -32,4 +32,4 @@ You are task-oriented. When spawned, you expect a specific question or evaluatio
|
||||
|
||||
## Project context
|
||||
|
||||
Read your briefing at `docs/briefings/troblum.md` before starting work. Read DECISIONS.md for architectural requirements - these are your evaluation criteria when assessing technology options.
|
||||
Read your briefing at `docs/briefings/troblum.md` before starting work. Read decisions/architecture.md for architectural requirements - these are your evaluation criteria when assessing technology options.
|
||||
|
||||
@@ -29,4 +29,4 @@ You respect the team leader's 30 years of software/systems/cloud architecture ex
|
||||
|
||||
## Project context
|
||||
|
||||
Read your briefing at `docs/briefings/tyre.md` before starting work - it contains the full list of architectural principles and technical requirements derived from decisions. Read DECISIONS.md for confirmed decisions and DISCUSSION.md for active discussions. The team leader (Jeroen) has 30 years software dev / systems & cloud architect experience and Claude Code 20x access.
|
||||
Read your briefing at `docs/briefings/tyre.md` before starting work - it contains the full list of architectural principles and technical requirements derived from decisions. Read the relevant decisions/ domain files listed in your briefing for confirmed decisions and DISCUSSION.md for active discussions. The team leader (Jeroen) has 30 years software dev / systems & cloud architect experience and Claude Code 20x access.
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"env": {
|
||||
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1"
|
||||
},
|
||||
"teammateMode": "in-process",
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(git add *)",
|
||||
"Bash(git commit *)",
|
||||
"Bash(git push *)",
|
||||
"Bash(git fetch *)",
|
||||
"Bash(git merge *)",
|
||||
"Bash(git pull *)",
|
||||
"Bash(git status *)",
|
||||
"Bash(git log *)",
|
||||
"Bash(git diff *)",
|
||||
"Bash(git show *)",
|
||||
"Bash(git checkout *)",
|
||||
"Bash(git stash *)",
|
||||
"Bash(git branch *)",
|
||||
"Bash(git worktree *)",
|
||||
"Bash(git config *)",
|
||||
"Bash(git mv *)",
|
||||
"Bash(git rm *)",
|
||||
"Bash(git ls-tree *)",
|
||||
"Bash(git rev-parse --show-toplevel)",
|
||||
|
||||
"Bash(db/connectors/ticket *)",
|
||||
"Bash(db/connectors/sprint *)",
|
||||
"Bash(db/connectors/sqlite-query *)",
|
||||
"Bash(db/connectors/sqlite-exec *)",
|
||||
"Bash(db/connectors/qdrant-search *)",
|
||||
"Bash(db/connectors/qdrant-index *)",
|
||||
"Bash(db/connectors/qdrant-health)",
|
||||
"Bash(db/connectors/qdrant-count)",
|
||||
"Bash(db/connectors/sqlite-init)",
|
||||
"Bash(db/connectors/decisions-sync)",
|
||||
|
||||
"Bash(db/connectors/audio-generate *)",
|
||||
"Bash(db/connectors/audio-health)",
|
||||
"Bash(db/connectors/audio-post *)",
|
||||
|
||||
"Bash(make *)",
|
||||
"Bash(make)",
|
||||
|
||||
"Bash(tea *)",
|
||||
|
||||
"Bash(chmod *)",
|
||||
"Bash(ls *)",
|
||||
"Bash(find *)",
|
||||
"Bash(list *)",
|
||||
"Bash(tree *)",
|
||||
|
||||
"Skill(commit)",
|
||||
"Skill(worktree-update)",
|
||||
"Skill(start-sprint)"
|
||||
],
|
||||
"deny": [
|
||||
"Bash(git push --force *)",
|
||||
"Bash(git reset --hard *)",
|
||||
"Bash(git clean -f *)",
|
||||
"Bash(rm -rf *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -149,14 +149,14 @@ Establish a list of reusable resources: scripts, references, and assets.
|
||||
|
||||
### Step 3: Initialize the Skill
|
||||
|
||||
Run the init script to generate a template skill directory:
|
||||
Create the skill directory manually:
|
||||
|
||||
```
|
||||
scripts/init_skill.py <skill-name> --path <output-directory>
|
||||
mkdir -p <output-directory>/<skill-name>
|
||||
```
|
||||
|
||||
Creates: skill directory, SKILL.md template with TODO placeholders, example
|
||||
resource directories with sample files.
|
||||
Then create `SKILL.md` with frontmatter and body. Add `scripts/`, `references/`,
|
||||
and `assets/` subdirectories only as needed.
|
||||
|
||||
Skip if iterating on an existing skill.
|
||||
|
||||
@@ -178,26 +178,24 @@ For design patterns, consult:
|
||||
**Writing guidelines:** Always use imperative/infinitive form.
|
||||
|
||||
**Frontmatter:**
|
||||
- `name`: The skill name
|
||||
- `name`: The skill name — use **domain-action** naming: `{domain}-{action}`.
|
||||
The domain is the system/area the skill operates on, the action is what it does.
|
||||
Examples: `pr-review`, `sprint-plan`, `docs-search`, `git-commit`, `debt-scan`.
|
||||
Multi-action wrappers (like `ticket`) can use the domain name alone.
|
||||
The directory name must match the `name` field.
|
||||
- `description`: Primary triggering mechanism. Include what the skill does AND
|
||||
specific triggers/contexts. All "when to use" info goes here (not in body).
|
||||
|
||||
**Body:** Instructions for using the skill and its bundled resources.
|
||||
|
||||
### Step 5: Package the Skill
|
||||
### Step 5: Validate the Skill
|
||||
|
||||
```
|
||||
scripts/package_skill.py <path/to/skill-folder>
|
||||
```
|
||||
|
||||
Optional output directory:
|
||||
```
|
||||
scripts/package_skill.py <path/to/skill-folder> ./dist
|
||||
```
|
||||
|
||||
The script validates (frontmatter, naming, description, file organization) then
|
||||
packages into a `.skill` file (zip with .skill extension). Fix any validation
|
||||
errors and re-run if needed.
|
||||
Check the skill manually:
|
||||
- Frontmatter has `name` and `description`
|
||||
- SKILL.md body is under 500 lines
|
||||
- No extraneous files (README.md, CHANGELOG.md, etc.)
|
||||
- Scripts are executable and tested
|
||||
- References are referenced from SKILL.md
|
||||
|
||||
### Step 6: Iterate
|
||||
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
---
|
||||
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.
|
||||
@@ -0,0 +1,67 @@
|
||||
# Category Templates — Audio Generation
|
||||
|
||||
Match the asset's category to the right template. Append after the sonic family
|
||||
prefix and before the specific asset description.
|
||||
|
||||
## Ambient Loops
|
||||
|
||||
**Template:**
|
||||
> Continuous ambient soundscape loop, seamless looping audio, no distinct
|
||||
> beginning or end, steady background atmosphere, [DURATION]s duration
|
||||
|
||||
**Notes:**
|
||||
- Generate at 45s (the SAO sweet spot below the 47s ceiling)
|
||||
- Add 3-5s of overlap material for crossfade looping in post
|
||||
- These play continuously — they must be boring enough to fade into the
|
||||
background but rich enough to reward attention
|
||||
- Station baseline prefix is required (see sonic-palette.md)
|
||||
|
||||
**Steps:** 100-150 (higher quality for loops — artifacts are more noticeable)
|
||||
**CFG:** 5-7 (lower guidance = more natural variation, less mechanical)
|
||||
|
||||
## Sound Effects (SFX)
|
||||
|
||||
**Template:**
|
||||
> Single isolated sound effect, clean recording, [DURATION] duration,
|
||||
> clear attack and natural decay, no background noise, no reverb tail
|
||||
> beyond natural
|
||||
|
||||
**Notes:**
|
||||
- For footsteps: specify surface material, pace, weight
|
||||
- For environmental: specify the physical mechanism (door hinge, cargo latch)
|
||||
- Keep short and punchy — these are event-driven, not continuous
|
||||
|
||||
**Steps:** 80-100 (shorter sounds need fewer steps)
|
||||
**CFG:** 7-9 (more guidance = more precise sound matching)
|
||||
|
||||
## UI Sounds
|
||||
|
||||
**Template:**
|
||||
> Interface feedback sound, digital UI element, [DURATION] duration,
|
||||
> immediate attack, clean decay, isolated sound with no background
|
||||
|
||||
**Notes:**
|
||||
- Most UI sounds are under 200ms — use manual synthesis, not SAO
|
||||
- For SAO-generated UI sounds (>200ms like fog_recognition, chimes):
|
||||
generate at 3-5s duration, then trim to the best portion
|
||||
- Insert-tech prefix is required for lattice/interface sounds
|
||||
- Organic prefix for fog_recognition (cognitive, not technological)
|
||||
|
||||
**Steps:** 80-100
|
||||
**CFG:** 7-9 (precision matters for UI sounds)
|
||||
|
||||
## Chimes / Notification Sounds
|
||||
|
||||
**Template:**
|
||||
> Single musical tone, crystalline quality, [DURATION] duration, gentle
|
||||
> attack, resonant sustain, natural fade out, no accompaniment, no rhythm
|
||||
|
||||
**Notes:**
|
||||
- Generate at 3-5s to give SAO enough temporal context
|
||||
- Trim to the best 0.5-1.0s section
|
||||
- The attack transient is where the character lives — select for that
|
||||
- Normal vs urgent variants: same base prompt, vary "gentle/soft" vs
|
||||
"brighter/sharper/more present"
|
||||
|
||||
**Steps:** 100 (tonal quality matters)
|
||||
**CFG:** 6-8 (some freedom for natural harmonic content)
|
||||
@@ -0,0 +1,45 @@
|
||||
# Sonic Palette — Prompt Prefixes
|
||||
|
||||
Two sonic families. Every generated sound uses one of these as a prompt prefix.
|
||||
|
||||
## Insert-Tech (Synthetic)
|
||||
|
||||
For UI sounds, neural lattice interface, augmented cognition.
|
||||
|
||||
**Prompt prefix:**
|
||||
> Clean digital audio, synthetic electronic tone, precise and clinical sound
|
||||
> design, no reverb, no room ambience, studio-dry recording, futuristic
|
||||
> interface sound
|
||||
|
||||
**Character:** Mathematical precision. The sound of well-designed technology.
|
||||
No organic texture, no room reflections. Exists "inside the head," not in
|
||||
physical space.
|
||||
|
||||
**Frequency range:** 800Hz-4kHz primary. Sharp attack, controlled decay.
|
||||
|
||||
**Used for:** cursor_hover, implant_open, weapon_aim, sfx_monologue_chime,
|
||||
sfx_monologue_chime_urgent
|
||||
|
||||
## Organic (Biological/Environmental)
|
||||
|
||||
For environmental sounds, human cognition, physical world.
|
||||
|
||||
**Prompt prefix:**
|
||||
> Warm organic audio, natural sound recording, slight room ambience,
|
||||
> lived-in industrial space, realistic acoustic properties, authentic
|
||||
> environmental sound
|
||||
|
||||
**Character:** Human warmth. Natural attack and decay. Reverberant — exists
|
||||
in physical space. The sound of a real place with real materials.
|
||||
|
||||
**Frequency range:** 60Hz-3kHz primary. Soft or natural attack, room tail.
|
||||
|
||||
**Used for:** fog_recognition, all amb_* ambient loops, all sfx_footstep_*
|
||||
|
||||
## Station Baseline
|
||||
|
||||
For ambient loops specifically, add the station context:
|
||||
|
||||
> Interior of a large industrial space station, metal and composite
|
||||
> construction, background mechanical hum from power systems, air circulation
|
||||
> audible, no music, no prominent voices unless specified
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
name: asset-gen
|
||||
name: gen-image
|
||||
description: >
|
||||
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).
|
||||
@@ -0,0 +1,173 @@
|
||||
---
|
||||
name: plan-sprint
|
||||
description: >
|
||||
Plan the next sprint and generate team briefing files. Use when the user says
|
||||
"plan sprint", "prep sprint briefing", "plan next sprint", "sprint planning",
|
||||
or invokes /plan-sprint. Gathers current sprint status, scans the backlog,
|
||||
proposes ticket selection, and writes briefing files per team to
|
||||
docs/sprints/sprint-N/.
|
||||
user-invocable: true
|
||||
allowed-tools: Task, Read, Grep, Glob
|
||||
---
|
||||
|
||||
# Plan Sprint
|
||||
|
||||
**Delegate this entire skill to SI** (Project Manager agent, `subagent_type: si`).
|
||||
|
||||
When this skill is invoked, spawn SI using the Task tool:
|
||||
|
||||
```
|
||||
Task(
|
||||
subagent_type: "si",
|
||||
prompt: "Run /plan-sprint for Sprint N. Read the skill at
|
||||
.claude/skills/plan-sprint/SKILL.md for the full workflow,
|
||||
then execute it. Use the arguments provided: {args}",
|
||||
description: "Plan sprint N"
|
||||
)
|
||||
```
|
||||
|
||||
Pass through any arguments the user provided (e.g. sprint number).
|
||||
Present SI's output to the user when done. Do NOT run the workflow yourself.
|
||||
|
||||
---
|
||||
|
||||
The remainder of this file is SI's reference for executing the workflow.
|
||||
|
||||
Generate sprint briefing files for each active team (e.g. `server.md`,
|
||||
`client.md`, `copy.md`, `joint.md`) for the next sprint based on current
|
||||
project state. Only generate briefings for teams that have tickets in the sprint.
|
||||
|
||||
## Teams and Default Agents
|
||||
|
||||
| Team | Branch | Default Agents | Focus |
|
||||
|------|--------|----------------|-------|
|
||||
| `server` | `server` | Dudley (dev), Tyre (arch), Hoshe (QA) | Rust/bevy_ecs simulation, ECS systems, knowledge graph, perception |
|
||||
| `client` | `client` | Stig (dev), Tyre (arch), Hoshe (QA) | Godot 4 client, rendering, UI, HUD, input handling |
|
||||
| `copy` | `copy` | Mellanie (author), Paula (narrative), Gestalt (systems) | Dialogue, monologue, UI text, knowledge vocabulary, lore |
|
||||
| `audio` | `audio` | Inigo (sound design) | Soundscapes, ambient layers, diegetic cues, audio propagation |
|
||||
| `visual` | `visual` | Araminta (art direction) | Art assets, sprites, visual consistency, style guides |
|
||||
| `ci` | `ci` | Justine (build/deploy) | Build pipelines, CI/CD, tooling, packaging |
|
||||
|
||||
When writing briefings, name the assigned agents in the **Agents** line of each
|
||||
file so the team knows who to spawn.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Run sprint prepare
|
||||
|
||||
Get carry-overs, backlog candidates, and decision gaps in one shot:
|
||||
|
||||
```bash
|
||||
db/connectors/sprint prepare
|
||||
```
|
||||
|
||||
This auto-detects the next sprint number (max ID + 1), creates the sprint
|
||||
record in `planning` status if needed, and outputs:
|
||||
- Previous sprint status and carry-over candidates
|
||||
- Backlog candidates grouped by team
|
||||
- Decision coverage gaps
|
||||
- Already-assigned tickets (if any)
|
||||
|
||||
### 2. Deepen the scan
|
||||
|
||||
For critical epics, check their children for granular candidates:
|
||||
|
||||
```bash
|
||||
db/connectors/ticket children <epic_id>
|
||||
```
|
||||
|
||||
Use `db/connectors/ticket show --brief <id> [<id>...]` to quickly scan multiple tickets.
|
||||
|
||||
### 3. Read existing code state
|
||||
|
||||
Scan what's already built to write accurate "what exists" notes:
|
||||
|
||||
```bash
|
||||
# Server modules
|
||||
ls server/src/ server/src/*/
|
||||
|
||||
# Client scripts
|
||||
ls client/scripts/ client/scripts/*/
|
||||
```
|
||||
|
||||
Read key files that sprint tickets will build on (bridge types, existing
|
||||
renderers, etc.) to reference specific integration points in the briefing.
|
||||
|
||||
### 4. Select tickets — propose to user
|
||||
|
||||
Based on the backlog scan, propose a sprint with:
|
||||
|
||||
- **Sprint theme** — a short name (Sprint 1 was "Run", Sprint 2 was "See")
|
||||
- **Sprint goal** — one sentence shared across all teams
|
||||
- **Server tickets** — stories from server-side epics
|
||||
- **Client tickets** — stories from client-side epics
|
||||
- **Joint tasks** — integration proofs, pre-sprint decisions, schema work
|
||||
|
||||
Present the proposal using AskUserQuestion for the user to approve or adjust.
|
||||
|
||||
Selection heuristics:
|
||||
- Follow dependency chains (don't pick a ticket if its blocker isn't in scope)
|
||||
- Respect D-030 test priority phases (sprint 1-2: infra, sprint 3-4: integration)
|
||||
- Mix carry-overs with new work
|
||||
- Aim for 3-6 tickets per team, with parallel tracks where possible
|
||||
- Check `decisions/questions.md` for open Q-NNN items that block candidates
|
||||
|
||||
### 5. Read relevant decisions
|
||||
|
||||
For the selected tickets, identify which `decisions/*.md` files are relevant.
|
||||
Read them to provide accurate cross-references in the briefing.
|
||||
|
||||
### 6. Write briefing files
|
||||
|
||||
Create `docs/sprints/sprint-N/` and write one file per team.
|
||||
|
||||
Read the template at `references/briefing-template.md` in this skill directory
|
||||
for the exact file structure.
|
||||
|
||||
**IMPORTANT — worktree-relative paths:** This project uses git worktrees.
|
||||
Each team branch is checked out in its own worktree, and each worktree
|
||||
contains the full repo (`server/`, `client/`, `docs/`, etc.). All file
|
||||
paths in briefings must be relative to the worktree/git root. Example:
|
||||
`server/src/bridge/types.rs`, not `/absolute/path/to/server/src/...` or
|
||||
paths that navigate outside the git root (`../sibling-worktree/...`).
|
||||
Agents must stay within the git root they are running in.
|
||||
|
||||
Key requirements per file:
|
||||
- **server.md**: Carry-overs, new tickets, dependency chain, key decisions, notes
|
||||
referencing existing Rust modules by path from worktree root (e.g. `server/src/simulation/movement.rs`)
|
||||
- **client.md**: Same structure, notes referencing existing GDScript files by path from worktree root (e.g. `client/scripts/rendering/fog.gd`)
|
||||
- **copy.md**: In-game text tasks — dialogue, UI copy, tooltips, flavor text, lore
|
||||
- **audio.md**: Sound design, music, audio integration tasks
|
||||
- **visual.md**: Art direction, asset creation, visual consistency tasks
|
||||
- **ci.md**: Build pipeline, CI/CD, tooling, infrastructure tasks
|
||||
- **joint.md**: Pre-sprint decisions table, integration tickets, sprint
|
||||
completion proof (concrete observable criteria), test plan alignment
|
||||
|
||||
Only generate briefing files for teams that have tickets assigned in the sprint.
|
||||
Not every sprint will have work for every team.
|
||||
|
||||
### 7. Assign tickets to sprint in DB
|
||||
|
||||
After the user approves, assign all selected tickets. The sprint record
|
||||
was already created by `sprint prepare` in step 1 (status: `planning`).
|
||||
Update it with the theme and goal, then assign tickets:
|
||||
|
||||
```bash
|
||||
# Update the sprint with theme and goal
|
||||
db/connectors/sqlite-exec "UPDATE sprints SET name='Sprint N: Theme', goal='goal' WHERE id=N"
|
||||
|
||||
# Assign tickets
|
||||
db/connectors/ticket sprint assign <ticket_id> <sprint_id>
|
||||
```
|
||||
|
||||
The sprint stays in `planning` status until explicitly activated via
|
||||
`db/connectors/sprint start`. This prevents starting an unplanned sprint.
|
||||
|
||||
### 8. Present summary
|
||||
|
||||
Output:
|
||||
- Sprint number, theme, and goal
|
||||
- Ticket count per team
|
||||
- Carry-over count
|
||||
- Open questions that need early resolution
|
||||
- Files written
|
||||
@@ -0,0 +1,79 @@
|
||||
# Sprint Briefing Template
|
||||
|
||||
Each team gets one briefing file at `docs/sprints/sprint-N/<team>.md`.
|
||||
|
||||
## File structure
|
||||
|
||||
```markdown
|
||||
# Sprint N: <Theme> — <Team> Tasks
|
||||
|
||||
**Goal:** <One-sentence sprint goal, shared across all teams>
|
||||
|
||||
**Branch:** `<team>`
|
||||
**Agents:** <Agent names and roles>
|
||||
|
||||
## Carry-over from Sprint N-1
|
||||
|
||||
(Only if there are incomplete tickets from the previous sprint)
|
||||
|
||||
| # | Title | Status | Notes |
|
||||
|---|-------|--------|-------|
|
||||
| #ID | Title | status | Why it carried over |
|
||||
|
||||
## New Tickets
|
||||
|
||||
| # | Title | Blocked by |
|
||||
|---|-------|------------|
|
||||
| #ID | Title | #dependency or — |
|
||||
|
||||
Use `db/connectors/ticket show <id>` for full details.
|
||||
|
||||
## Key Decisions
|
||||
|
||||
- `decisions/<domain>.md` — D-NNN (short name), D-NNN (short name)
|
||||
|
||||
## Open Questions to Resolve Early
|
||||
|
||||
(Only if there are Q-NNN items that block sprint tickets)
|
||||
|
||||
- **Q-NNN: Title** — Brief context. Resolve before #ID starts.
|
||||
|
||||
## Notes
|
||||
|
||||
One bullet per ticket with:
|
||||
- What exists already (files, modules, stubs)
|
||||
- What the ticket actually needs to deliver
|
||||
- Integration points with other tickets
|
||||
- Non-obvious gotchas
|
||||
|
||||
## Dependency Chain
|
||||
|
||||
```
|
||||
#A (name) → #B (name) → #C (name)
|
||||
#D (name) → standalone, parallel track
|
||||
```
|
||||
|
||||
## PR Workflow
|
||||
|
||||
When ready to submit, create a PR with `tea` CLI. **All flags are required** to avoid TTY prompts (see CLAUDE.md "Gitea access" section):
|
||||
\```bash
|
||||
tea pr create --repo jpmschweitzer/settled-reach --login schweitz --title "feat(<scope>): description" --description "body" --base main --head <branch>
|
||||
\```
|
||||
```
|
||||
|
||||
## Joint briefing extras
|
||||
|
||||
The `joint.md` file additionally includes:
|
||||
|
||||
- **Pre-Sprint** table: decisions/schema work that must happen before implementation
|
||||
- **Sprint Completion Proof**: concrete observable criteria (what you can see/do when the sprint is done)
|
||||
- **Test plan** alignment with D-030 phases
|
||||
|
||||
## Team assignments
|
||||
|
||||
| Team | Branch | Agents | Scope |
|
||||
|------|--------|--------|-------|
|
||||
| server | `server` | Dudley (simulation), Oscar (networking) | Rust/bevy_ecs simulation |
|
||||
| client | `client` | Stig (UI), Oscar (networking) | Godot client rendering |
|
||||
| joint | both | All implementation agents | Integration, proofs, cross-team schema |
|
||||
| content | (none) | Mellanie, Paula, Miri, Araminta | Content authoring, no code branch |
|
||||
@@ -0,0 +1,128 @@
|
||||
---
|
||||
name: push-pr
|
||||
description: >
|
||||
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",
|
||||
or invokes /push-pr. NOT triggered by plain "push" (that's just git push).
|
||||
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
|
||||
pushes to the branch and manages the PR lifecycle.
|
||||
user-invocable: true
|
||||
allowed-tools: Bash, Read, Grep, Glob, AskUserQuestion
|
||||
---
|
||||
|
||||
# Push PR Skill
|
||||
|
||||
Push commits to remote and create or update a PR. Operates exclusively on the
|
||||
current branch — never touches main.
|
||||
|
||||
## Safety Rules (NON-NEGOTIABLE)
|
||||
|
||||
- **NEVER merge a PR into main.** No `tea pr merge`, no `git merge` into main.
|
||||
- **NEVER checkout or push to main.**
|
||||
- **NEVER force-push** unless the user explicitly requests it.
|
||||
- **NEVER use `--no-verify` or skip hooks.**
|
||||
- Only push to the current working branch.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Validate branch
|
||||
|
||||
```bash
|
||||
git branch --show-current
|
||||
```
|
||||
|
||||
If on `main`, stop: "You're on main. Switch to a team branch first."
|
||||
|
||||
### 2. Check for unpushed commits
|
||||
|
||||
```bash
|
||||
git fetch --all
|
||||
git status
|
||||
git log --oneline origin/<branch>..<branch>
|
||||
```
|
||||
|
||||
If no unpushed commits, skip to step 4 (PR check).
|
||||
|
||||
### 3. Check for conflicts with main
|
||||
|
||||
```bash
|
||||
git merge-tree --write-tree origin/main HEAD 2>&1
|
||||
```
|
||||
|
||||
If conflicts reported, merge main into current branch:
|
||||
|
||||
```bash
|
||||
git merge origin/main --no-edit
|
||||
```
|
||||
|
||||
If merge conflicts, **stop and report** — let the user resolve.
|
||||
If clean, continue.
|
||||
|
||||
### 4. Push
|
||||
|
||||
```bash
|
||||
git push origin <branch>
|
||||
```
|
||||
|
||||
If push fails, stop and report. Never force-push without explicit request.
|
||||
|
||||
### 5. Check for existing PR
|
||||
|
||||
```bash
|
||||
tea pr list --login schweitz --repo jpmschweitzer/settled-reach --state open --output simple
|
||||
```
|
||||
|
||||
Match current branch name in PR list.
|
||||
|
||||
- **PR exists**: Report "Pushed N commits to `<branch>`. PR #X updated." Done.
|
||||
- **No PR**: Continue to step 6.
|
||||
|
||||
### 6. Create a new PR
|
||||
|
||||
```bash
|
||||
git log --oneline main..<branch>
|
||||
git diff --stat main...<branch>
|
||||
```
|
||||
|
||||
Draft title (`<type>(<scope>): <summary>`, max 70 chars) and description.
|
||||
|
||||
```bash
|
||||
cat > /tmp/pr-body.md << 'EOF'
|
||||
## Summary
|
||||
...
|
||||
EOF
|
||||
tea pr create \
|
||||
--repo jpmschweitzer/settled-reach \
|
||||
--login schweitz \
|
||||
--title "<title>" \
|
||||
--description "$(cat /tmp/pr-body.md)" \
|
||||
--base main \
|
||||
--head <branch>
|
||||
```
|
||||
|
||||
Report PR URL when done.
|
||||
|
||||
### 7. Update ticket status to review
|
||||
|
||||
Scan all commit messages in the PR for ticket references (`#NNN`):
|
||||
|
||||
```bash
|
||||
git log --oneline main..<branch>
|
||||
```
|
||||
|
||||
Extract ticket IDs from `#NNN` patterns. For each ticket that is
|
||||
currently `in_progress`, update it to `review`:
|
||||
|
||||
```bash
|
||||
db/connectors/ticket status <id> review
|
||||
```
|
||||
|
||||
Report which tickets were moved to review. Skip tickets that are
|
||||
already `done`, `review`, `cancelled`, or `backlog` (only transition
|
||||
`in_progress` → `review`).
|
||||
|
||||
## Arguments
|
||||
|
||||
If the user passes arguments (e.g., `/push-pr "my title"`), use them as the
|
||||
PR title instead of generating one.
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
name: render-sprite
|
||||
description: >
|
||||
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,
|
||||
east, south, west) with outline applied at working resolution. Use when the
|
||||
user says "render sprite", "render model", "run the render pipeline",
|
||||
"test the pipeline", "/render-sprite", or asks to render a specific model
|
||||
(e.g., "render wall_structural"). Output: 12 PNG files in renderer/output/.
|
||||
---
|
||||
|
||||
## Render Pipeline
|
||||
|
||||
Run the render script with the model name:
|
||||
|
||||
```bash
|
||||
.claude/skills/render-sprite/scripts/render.sh <model_name>
|
||||
```
|
||||
|
||||
### Available Models
|
||||
|
||||
Models live at `renderer/models/<name>.tscn`. List them:
|
||||
|
||||
```bash
|
||||
ls renderer/models/*.tscn | xargs -I{} basename {} .tscn
|
||||
```
|
||||
|
||||
### Output
|
||||
|
||||
12 files per model in `renderer/output/`:
|
||||
|
||||
```
|
||||
<model>_north_1024.png <model>_north_256.png <model>_north_64.png
|
||||
<model>_east_1024.png <model>_east_256.png <model>_east_64.png
|
||||
<model>_south_1024.png <model>_south_256.png <model>_south_64.png
|
||||
<model>_west_1024.png <model>_west_256.png <model>_west_64.png
|
||||
```
|
||||
|
||||
### After Rendering
|
||||
|
||||
1. Read the 64x64 output files to visually inspect the runtime sprites
|
||||
2. Read the 256x256 files to check outline quality
|
||||
3. Report: does it read as the intended object at runtime scale?
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
- **No output files**: Godot needs a display. If running headless, prefix with `xvfb-run`.
|
||||
- **Model not found**: Check the model .tscn exists in `renderer/models/`.
|
||||
- **Godot not found**: Pass path as second arg: `render.sh wall_structural /path/to/godot`
|
||||
|
||||
### Adding New Models
|
||||
|
||||
1. Create model scene at `renderer/models/<name>.tscn`
|
||||
2. Apply texture from `renderer/textures/` via StandardMaterial3D
|
||||
3. Run: `.claude/skills/render-sprite/scripts/render.sh <name>`
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
#!/bin/bash
|
||||
# Render a 3D model to 2D sprites at 3 resolutions from 4 cardinal directions.
|
||||
# Usage: render.sh <model_name> [godot_path]
|
||||
# Example: render.sh wall_structural
|
||||
set -euo pipefail
|
||||
|
||||
MODEL_NAME="${1:?Usage: render.sh <model_name> [godot_path]}"
|
||||
GODOT="${2:-$HOME/bin/godot}"
|
||||
REPO_ROOT="$(cd "$(dirname "$0")" && git rev-parse --show-toplevel)"
|
||||
PROJECT="$REPO_ROOT/renderer"
|
||||
SCENE="res://render_scene.tscn"
|
||||
OUTPUT="$PROJECT/output"
|
||||
|
||||
# Verify model exists
|
||||
MODEL_FILE="$PROJECT/models/${MODEL_NAME}.tscn"
|
||||
if [ ! -f "$MODEL_FILE" ]; then
|
||||
echo "ERROR: Model not found: $MODEL_FILE"
|
||||
echo "Available models:"
|
||||
ls "$PROJECT/models/"*.tscn 2>/dev/null | xargs -I{} basename {} .tscn
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Rendering: $MODEL_NAME"
|
||||
echo "Godot: $GODOT"
|
||||
echo "Project: $PROJECT"
|
||||
|
||||
# Ensure resources are imported (headless, no display needed)
|
||||
echo "Importing resources..."
|
||||
"$GODOT" --path "$PROJECT" --headless --import 2>&1 || echo "Warning: import step had errors (may be non-fatal)"
|
||||
|
||||
# Run Godot with the render scene, passing model name as user arg
|
||||
"$GODOT" --path "$PROJECT" "$SCENE" -- "$MODEL_NAME" 2>&1
|
||||
|
||||
# Check output
|
||||
EXPECTED_FILES=0
|
||||
for dir in north east south west; do
|
||||
for res in 1024 256 64; do
|
||||
f="$OUTPUT/${MODEL_NAME}_${dir}_${res}.png"
|
||||
if [ -f "$f" ]; then
|
||||
EXPECTED_FILES=$((EXPECTED_FILES + 1))
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Generated $EXPECTED_FILES/12 files in $OUTPUT/"
|
||||
ls -la "$OUTPUT/${MODEL_NAME}"_*.png 2>/dev/null || echo "No output files found."
|
||||
@@ -0,0 +1,200 @@
|
||||
---
|
||||
name: review-pr
|
||||
description: >
|
||||
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
|
||||
/review-pr. Spawns reviewers matched to the branch type (code, copy, visual,
|
||||
audio) in parallel. Reports approve/reject with inline comments.
|
||||
user-invocable: true
|
||||
allowed-tools: Bash, Read, Grep, Glob, Task
|
||||
---
|
||||
|
||||
# PR Review Skill
|
||||
|
||||
Multi-agent review of a branch diff against main. Reviewer composition depends
|
||||
on the branch type. All reviewers must approve for a clean review.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Determine the branch
|
||||
|
||||
If the user provided a branch name as argument, use it. Otherwise use the
|
||||
current branch (`git branch --show-current`). If on `main`, ask the user
|
||||
which branch to review.
|
||||
|
||||
To list open PRs on Gitea:
|
||||
```bash
|
||||
tea pr list --login schweitz --repo jpmschweitzer/settled-reach --state open --output simple
|
||||
```
|
||||
|
||||
Fetch remote branches first:
|
||||
```bash
|
||||
git fetch --all
|
||||
```
|
||||
|
||||
### 2. Determine reviewer team
|
||||
|
||||
Map the branch name to a reviewer set. Use the branch prefix (before any `/`
|
||||
or `-` suffix) to classify:
|
||||
|
||||
| Branch type | Branches | Reviewers |
|
||||
|-------------|----------|-----------|
|
||||
| **code** | `server`, `client`, `ci`, or unknown | Hoshe (code quality) + Tyre (architecture) |
|
||||
| **copy** | `copy` | Hoshe (QA) + Paula (narrative depth) + Miri (world consistency) |
|
||||
| **visual** | `visual` | Hoshe (QA) + Araminta (art direction) |
|
||||
| **audio** | `audio` | Hoshe (QA) + Ozzie (player experience) |
|
||||
|
||||
If the branch name doesn't match any known type, default to **code** reviewers.
|
||||
|
||||
### 3. Generate the diff and read source files
|
||||
|
||||
```bash
|
||||
git log --oneline main..<branch>
|
||||
git diff main...<branch> --stat
|
||||
```
|
||||
|
||||
If the diff is empty, report "No changes to review" and stop.
|
||||
|
||||
**Exclude generated/vendor files from the review diff.** Common exclusions:
|
||||
- `Cargo.lock` (auto-generated)
|
||||
- `client/addons/gdUnit4/` (vendor test framework)
|
||||
- `*.uid` (Godot-generated)
|
||||
- `docs/backups/settledreach.db.backup` (binary)
|
||||
|
||||
Three-dot diff with pathspec exclusions is unreliable. Instead, either:
|
||||
1. Use `git diff main...<branch>` (full diff) and filter in the prompt, or
|
||||
2. Read source files directly from the branch:
|
||||
`git show origin/<branch>:<path>`
|
||||
|
||||
For large diffs (>1000 lines of source), provide **source files** rather than
|
||||
raw diff to reviewers — cleaner context, better reviews. Read files with
|
||||
`git show origin/<branch>:<path>` and include them in the prompt.
|
||||
|
||||
**IMPORTANT — agent tool access:** Not all reviewer agents have Bash access.
|
||||
Agents that CAN read from branches themselves: **Hoshe, Tyre, Araminta**.
|
||||
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
|
||||
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
|
||||
`decisions/*.md` files these agents need for context.
|
||||
|
||||
### 4. Spawn reviewers in parallel
|
||||
|
||||
Use the Task tool to spawn **all reviewers simultaneously** in a single message.
|
||||
|
||||
Read `references/reviewer-profiles.md` for the full per-branch-type reviewer
|
||||
specifications (agent types, models, prompt focus areas). Match the branch type
|
||||
from step 2 to the corresponding section.
|
||||
|
||||
All reviewers: request structured verdict: APPROVE or REQUEST_CHANGES with
|
||||
file-specific comments.
|
||||
|
||||
### 5. Present results
|
||||
|
||||
Format the combined review as a table per reviewer. Include one section per
|
||||
reviewer that was spawned (2 for code/visual/audio, 3 for copy):
|
||||
|
||||
```
|
||||
## Review: <branch> -> main (type: code|copy|visual|audio)
|
||||
|
||||
### <Reviewer Name> (<Focus>): [APPROVE | REQUEST_CHANGES]
|
||||
[Summary]
|
||||
| # | File | Severity | Issue |
|
||||
|---|------|----------|-------|
|
||||
| 1 | path:line | critical/warning/suggestion | description |
|
||||
|
||||
### <Reviewer Name> (<Focus>): [APPROVE | REQUEST_CHANGES]
|
||||
...
|
||||
|
||||
### Verdict: [APPROVED | CHANGES REQUESTED]
|
||||
```
|
||||
|
||||
The overall verdict is APPROVED only if **all** reviewers approve.
|
||||
|
||||
## Prompt template for reviewers
|
||||
|
||||
Use this structure when constructing the agent prompts (adapt as needed):
|
||||
|
||||
```
|
||||
Review the following {branch_type} branch diff for merge into main.
|
||||
|
||||
Branch: {branch}
|
||||
Branch type: {branch_type} (code|copy|visual|audio)
|
||||
Commits:
|
||||
{commit_log}
|
||||
|
||||
Diff stats:
|
||||
{diff_stat}
|
||||
|
||||
[Source files or diff here — exclude vendor/generated code]
|
||||
|
||||
Your review focus: {focus_area}
|
||||
|
||||
Respond with:
|
||||
1. Verdict: APPROVE or REQUEST_CHANGES
|
||||
2. Summary: 2-3 sentence overall assessment
|
||||
3. Comments: List of specific issues, each with:
|
||||
- File path and approximate location
|
||||
- Severity: critical / warning / suggestion
|
||||
- Description of the issue
|
||||
|
||||
Verdict rules:
|
||||
- Any **critical** or **warning** issue → REQUEST_CHANGES
|
||||
- Only **suggestion**-level issues (or none) → APPROVE
|
||||
If no issues found, say APPROVE with a brief positive summary.
|
||||
```
|
||||
|
||||
## 6. Posting results to Gitea
|
||||
|
||||
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.
|
||||
|
||||
**IMPORTANT — `tea comment` hangs with inline heredocs and multi-line strings.**
|
||||
Always use a two-step approach: write to a temp file first, then pass via `$(cat)`:
|
||||
|
||||
```bash
|
||||
# Step 1: Write review to temp file (use Bash heredoc, NOT the Write tool)
|
||||
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
|
||||
|
||||
`tea pr merge` fails (405) when branches have conflicts with main. Merge
|
||||
locally instead:
|
||||
|
||||
```bash
|
||||
git fetch --all
|
||||
git merge origin/<branch> # resolve conflicts if any
|
||||
git push origin main
|
||||
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
|
||||
manually with `tea pr close` after pushing.
|
||||
|
||||
## Tips from practice
|
||||
|
||||
- **Vendor code**: Explicitly note vendor code in the prompt so reviewers focus
|
||||
on project code. Mention it as a separate architectural concern (should it be
|
||||
gitignored? submoduled?).
|
||||
- **Large PRs**: For PRs touching many files, provide file-by-file source code
|
||||
rather than a single massive diff. Reviewers give better feedback.
|
||||
- **Multiple PRs**: When reviewing several PRs, spawn all reviewers in one
|
||||
parallel batch (4 agents for 2 PRs). This is faster than sequential.
|
||||
- **Tyre reads decisions**: Always tell Tyre to read the relevant `decisions/*.md`
|
||||
files — this grounds the review in project-specific architectural choices.
|
||||
- **Binary/DB files**: Exclude binary files from the diff. Note them in the
|
||||
prompt as "also changed" if relevant.
|
||||
@@ -0,0 +1,103 @@
|
||||
# Reviewer Profiles by Branch Type
|
||||
|
||||
Use `model: sonnet` for all reviewers — sufficient for review, saves cost.
|
||||
|
||||
## Code reviews (`server`, `client`, `ci`)
|
||||
|
||||
**Hoshe (Code Quality)**
|
||||
- `subagent_type`: `hoshe`, `model`: `sonnet`
|
||||
- Prompt: Include source code and commit log. Ask Hoshe to review for:
|
||||
- Correctness and bug risks
|
||||
- Error handling gaps
|
||||
- Test coverage (are new features tested?)
|
||||
- Code style and clarity
|
||||
- Security concerns (OWASP top 10, injection risks)
|
||||
- Performance issues
|
||||
|
||||
**Tyre (Architecture)**
|
||||
- `subagent_type`: `tyre`, `model`: `sonnet`
|
||||
- Prompt: Include source code and commit log. Tell Tyre to read the relevant
|
||||
`decisions/*.md` files first, then review for:
|
||||
- Architectural consistency with project decisions
|
||||
- API/interface design quality
|
||||
- Dependency and coupling concerns
|
||||
- Scalability implications
|
||||
- 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`)
|
||||
|
||||
**Hoshe (QA)**
|
||||
- `subagent_type`: `hoshe`, `model`: `sonnet`
|
||||
- Prompt: Include the changed files and commit log. Ask Hoshe to review for:
|
||||
- Formatting consistency (markdown, file naming, frontmatter)
|
||||
- Broken references or links
|
||||
- Spelling and grammar
|
||||
- File organization and structure
|
||||
- Missing or orphaned files
|
||||
|
||||
**Paula (Narrative Depth)** — NO BASH ACCESS
|
||||
- `subagent_type`: `paula`, `model`: `sonnet`
|
||||
- Paula cannot read from branches. You must paste file contents and decision
|
||||
files directly into the prompt.
|
||||
- Prompt: Include full text of changed files, commit log, and relevant
|
||||
`decisions/*.md` content. Ask Paula to review for:
|
||||
- Narrative quality and character voice consistency
|
||||
- Whether dialogue and monologue feel authentic to the characters
|
||||
- Consequences and stakes — do choices carry weight?
|
||||
- Political and interpersonal depth
|
||||
- Emotional resonance — does the text make you feel something?
|
||||
|
||||
**Miri (World Consistency)** — NO BASH ACCESS
|
||||
- `subagent_type`: `miri`, `model`: `sonnet`
|
||||
- Miri cannot read from branches. You must paste file contents and decision
|
||||
files directly into the prompt.
|
||||
- Prompt: Include full text of changed files, commit log, and relevant
|
||||
`decisions/*.md` content. Ask Miri to review for:
|
||||
- Lore accuracy — do facts match established setting?
|
||||
- Internal consistency across files
|
||||
- IP originality — nothing should read as a copy from another franchise
|
||||
- Faction, technology, and location details match the worldbuilding docs
|
||||
- Setting serves gameplay mechanics (asymmetric information, perception)
|
||||
|
||||
## Visual reviews (`visual`)
|
||||
|
||||
**Hoshe (QA)**
|
||||
- `subagent_type`: `hoshe`, `model`: `sonnet`
|
||||
- Prompt: Include the changed files and commit log. Ask Hoshe to review for:
|
||||
- File format and naming conventions
|
||||
- Asset organization and directory structure
|
||||
- Missing or broken references in scene/resource files
|
||||
- Import settings consistency
|
||||
|
||||
**Araminta (Art Direction)** — HAS BASH ACCESS
|
||||
- `subagent_type`: `araminta`, `model`: `sonnet`
|
||||
- Prompt: Include the changed files and commit log. Tell Araminta to read
|
||||
the style guide and relevant design docs first, then review for:
|
||||
- Visual consistency with the established style guide
|
||||
- Color palette adherence
|
||||
- UI pattern consistency (diegetic-first, clarity over beauty)
|
||||
- Whether assets scale gracefully (boxes-with-labels to full-art)
|
||||
- Mood and tone — sleek, advanced, subtle Commonwealth aesthetic
|
||||
|
||||
## Audio reviews (`audio`)
|
||||
|
||||
**Hoshe (QA)**
|
||||
- `subagent_type`: `hoshe`, `model`: `sonnet`
|
||||
- Prompt: Include the changed files and commit log. Ask Hoshe to review for:
|
||||
- File format and naming conventions
|
||||
- Audio asset organization and directory structure
|
||||
- Missing or broken references
|
||||
- Import/bus configuration consistency
|
||||
|
||||
**Ozzie (Player Experience)** — NO BASH ACCESS
|
||||
- `subagent_type`: `ozzie`, `model`: `sonnet`
|
||||
- Ozzie cannot read from branches. You must paste file contents directly
|
||||
into the prompt.
|
||||
- Prompt: Include full text of changed files and commit log. Ask Ozzie to
|
||||
review for:
|
||||
- Emotional impact — does the audio enhance the moment?
|
||||
- Atmosphere and tone — does it feel like the Commonwealth?
|
||||
- Player feedback clarity — can the player tell what just happened?
|
||||
- Pacing — do sounds support or fight the gameplay rhythm?
|
||||
- Memorable moments — will players remember these audio cues?
|
||||
@@ -10,74 +10,40 @@ allowed-tools: Bash, Read, Grep, Glob
|
||||
|
||||
# Search Docs Skill
|
||||
|
||||
Semantic search across Commonwealth project documents using Qdrant vector database and ollama embeddings.
|
||||
Semantic search across project documents. Basic commands (`qdrant-search`,
|
||||
`qdrant-index`, `qdrant-health`, `qdrant-count`) and endpoints are documented
|
||||
in CLAUDE.md. This skill covers advanced operations and workflows.
|
||||
|
||||
## Access Method
|
||||
|
||||
**Use the connector script:**
|
||||
```bash
|
||||
python3 db/connectors/qdrant_connector.py <command> [args]
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### Search
|
||||
Find documents semantically related to a query:
|
||||
```bash
|
||||
python3 db/connectors/qdrant_connector.py search "asymmetric information design"
|
||||
python3 db/connectors/qdrant_connector.py search "what did we decide about fog of war"
|
||||
python3 db/connectors/qdrant_connector.py search "engine requirements"
|
||||
```
|
||||
|
||||
Returns top 5 matching document chunks with source file, heading, and relevance score.
|
||||
|
||||
### Index a file
|
||||
Add or update a document in the search index:
|
||||
```bash
|
||||
python3 db/connectors/qdrant_connector.py index-file DECISIONS.md
|
||||
python3 db/connectors/qdrant_connector.py index-file docs/discussions/round-10-map-fog-borderless.md
|
||||
python3 db/connectors/qdrant_connector.py index-file docs/briefings/tyre.md
|
||||
```
|
||||
|
||||
Files are chunked by markdown headings (# and ##). Each chunk is embedded via ollama and stored in Qdrant with metadata (source_file, heading, chunk_index).
|
||||
## Advanced Commands
|
||||
|
||||
### Index a single chunk
|
||||
|
||||
For precise indexing of specific content:
|
||||
```bash
|
||||
python3 db/connectors/qdrant_connector.py index "unique-id" "Text content to index" --metadata source=manual heading="Custom heading"
|
||||
```
|
||||
|
||||
### Health check
|
||||
Verify connectivity to Qdrant and ollama:
|
||||
```bash
|
||||
python3 db/connectors/qdrant_connector.py health
|
||||
```
|
||||
|
||||
### Collection info
|
||||
Check how many documents are indexed:
|
||||
```bash
|
||||
python3 db/connectors/qdrant_connector.py count
|
||||
```
|
||||
|
||||
### Create collection
|
||||
|
||||
Initialize the Qdrant collection (run once during setup):
|
||||
```bash
|
||||
python3 db/connectors/qdrant_connector.py create-collection
|
||||
```
|
||||
|
||||
## Endpoints
|
||||
## Bulk Indexing
|
||||
|
||||
Configured in `db/connectors/config.json`:
|
||||
- **Qdrant:** `http://tower-of-joy:6333`
|
||||
- **Ollama:** `http://tower-of-joy:11434` (model: nomic-embed-text)
|
||||
- **Collection:** `commonwealth` (768 dimensions, cosine distance)
|
||||
Index all project documents at once:
|
||||
```bash
|
||||
for f in decisions/*.md DISCUSSION.md TEAM.md docs/discussions/*.md docs/briefings/*.md; do
|
||||
db/connectors/qdrant-index "$f"
|
||||
done
|
||||
```
|
||||
|
||||
## Fallback
|
||||
|
||||
If Qdrant or ollama is unreachable, fall back to grep-based search:
|
||||
```bash
|
||||
# Search across all project docs
|
||||
grep -r -i "search term" DECISIONS.md DISCUSSION.md docs/ --include="*.md"
|
||||
grep -r -i "search term" decisions/ DISCUSSION.md docs/ --include="*.md"
|
||||
```
|
||||
|
||||
## Workflow
|
||||
@@ -85,13 +51,5 @@ grep -r -i "search term" DECISIONS.md DISCUSSION.md docs/ --include="*.md"
|
||||
1. **Qatux (Librarian)** is the primary user of this skill
|
||||
2. After each discussion round, index the archived round file
|
||||
3. After briefing updates, re-index affected briefings
|
||||
4. After decision changes, re-index DECISIONS.md
|
||||
4. After decision changes, re-index the relevant decisions/*.md domain files
|
||||
5. Use search to answer "did we discuss this?" questions with citations
|
||||
|
||||
## Bulk indexing
|
||||
To index all project documents at once:
|
||||
```bash
|
||||
for f in DECISIONS.md DISCUSSION.md TEAM.md docs/discussions/*.md docs/briefings/*.md; do
|
||||
python3 db/connectors/qdrant_connector.py index-file "$f"
|
||||
done
|
||||
```
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
---
|
||||
name: start-sprint
|
||||
description: >
|
||||
Start sprint work on a team branch, or close a sprint from main. Use when
|
||||
the user says "start sprint", "start working on the server/client/copy",
|
||||
"begin sprint", or invokes /start-sprint. On a team branch: merges main,
|
||||
finds the active sprint, reads the briefing, presents the work plan. On
|
||||
main: closes the active sprint, bumps the version (v0.1.N), updates the
|
||||
changelog, tags, and pushes.
|
||||
user-invocable: true
|
||||
allowed-tools: Bash, Read, Grep, Glob, TeamCreate, Task, TaskCreate, TaskUpdate, TaskList, SendMessage, AskUserQuestion
|
||||
---
|
||||
|
||||
# Start Sprint Skill
|
||||
|
||||
Prepare a team branch for sprint work: sync with main, load the sprint
|
||||
briefing, and present actionable next steps.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Determine the team
|
||||
|
||||
The current branch IS the team. Read it with:
|
||||
```bash
|
||||
git branch --show-current
|
||||
```
|
||||
|
||||
Valid team branches: `server`, `client`, `copy`, `audio`, `visual`, `ci`.
|
||||
|
||||
If on `main`, follow the **Main branch workflow** below instead of
|
||||
the team branch workflow (steps 2–8).
|
||||
|
||||
---
|
||||
|
||||
## Main branch workflow (sprint close + version bump)
|
||||
|
||||
When `/start-sprint` is run on `main`, it means the user wants to close
|
||||
the current sprint, cut a version, and prepare for the next one.
|
||||
|
||||
### M1. Close the active sprint
|
||||
|
||||
```bash
|
||||
db/connectors/sprint stop
|
||||
```
|
||||
|
||||
This marks the active sprint as completed and lists carry-over candidates.
|
||||
Note the sprint number (N) from the output.
|
||||
|
||||
### M2. Bump the version
|
||||
|
||||
The project version scheme is `v0.1.{sprint_number}`. After closing
|
||||
sprint N, the version is `v0.1.N`.
|
||||
|
||||
Update `CHANGELOG.md`:
|
||||
- Move all entries under `## [Unreleased]` into a new section
|
||||
`## [v0.1.N] — YYYY-MM-DD` (using today's date).
|
||||
- Leave `## [Unreleased]` as an empty section above the new version.
|
||||
- Keep the existing sub-headings (Added, Fixed, Changed, Removed) —
|
||||
only move entries that have content.
|
||||
|
||||
### M3. Commit the release
|
||||
|
||||
Stage and commit `CHANGELOG.md`:
|
||||
```
|
||||
chore(meta): release v0.1.N
|
||||
```
|
||||
|
||||
### M4. Tag the release
|
||||
|
||||
```bash
|
||||
git tag v0.1.N
|
||||
```
|
||||
|
||||
### M5. Push
|
||||
|
||||
```bash
|
||||
git push && git push --tags
|
||||
```
|
||||
|
||||
### M6. Report
|
||||
|
||||
Output a summary:
|
||||
- Sprint closed (name, done/total tickets, carry-over count)
|
||||
- Version tagged (`v0.1.N`)
|
||||
- Carry-over candidates (if any)
|
||||
- Suggest running `/plan-sprint` next to prepare the next sprint
|
||||
|
||||
---
|
||||
|
||||
## Team branch workflow
|
||||
|
||||
### 2. Sync with main
|
||||
|
||||
```bash
|
||||
git fetch --all
|
||||
git merge origin/main --no-edit
|
||||
```
|
||||
|
||||
If the merge has conflicts, report them and stop — do not force-resolve.
|
||||
|
||||
### 3. Load sprint context
|
||||
|
||||
Run the sprint CLI to get the full context dump in one shot:
|
||||
|
||||
```bash
|
||||
db/connectors/sprint start-work
|
||||
```
|
||||
|
||||
This auto-detects the active sprint and current team from the branch.
|
||||
It outputs: sprint metadata, briefing paths, decision refs, actionable
|
||||
tickets, blocked tickets, and done tickets.
|
||||
|
||||
If no active sprint is found, report that and stop.
|
||||
|
||||
### 4. Read the sprint briefing
|
||||
|
||||
Read the briefing file(s) listed in the `start-work` output
|
||||
(e.g. `docs/sprints/sprint-6/server.md` and `joint.md`).
|
||||
If no matching briefing exists for the team, suggest running
|
||||
`/plan-sprint` to generate one.
|
||||
|
||||
### 5. Load ticket details
|
||||
|
||||
For tickets that need more detail than the `start-work` summary provides:
|
||||
```bash
|
||||
db/connectors/ticket show <id>
|
||||
```
|
||||
|
||||
### 6. Read key decisions
|
||||
|
||||
Read the decision files referenced in the sprint briefing so the agent has
|
||||
full architectural context before starting work.
|
||||
|
||||
### 7. Mark tickets in progress and present the work plan
|
||||
|
||||
Mark all actionable (unblocked, non-done) tickets in the sprint as
|
||||
`in_progress`:
|
||||
|
||||
```bash
|
||||
db/connectors/ticket status <id> in_progress
|
||||
```
|
||||
|
||||
Then output a summary:
|
||||
- Sprint name and goal
|
||||
- Branch status (clean merge or conflicts)
|
||||
- Tickets marked in_progress (list with IDs)
|
||||
- Blocked tickets (and what blocks them)
|
||||
- Key decisions loaded
|
||||
- Suggested first task (lowest ID unblocked ticket)
|
||||
|
||||
### 8. Confirm and spawn the team
|
||||
|
||||
Before spawning agents, use `AskUserQuestion` to confirm the work plan and
|
||||
agent lineup with the user. If declined, stop.
|
||||
|
||||
Once confirmed:
|
||||
|
||||
#### 8a. Parse agents from the briefing
|
||||
|
||||
Extract agent names from the `**Agents:**` line. Format:
|
||||
```
|
||||
**Agents:** Name (role), Name (role), ...
|
||||
```
|
||||
|
||||
Map each name to its `subagent_type` (lowercase):
|
||||
- "Dudley (simulation)" → `dudley`
|
||||
- "Stig (UI)" → `stig`
|
||||
- "Tyre (architecture)" → `tyre`
|
||||
- "Hoshe (QA)" → `hoshe`
|
||||
- "Mellanie (author)" → `mellanie`
|
||||
- etc. (see `.claude/agents/` for full roster)
|
||||
|
||||
#### 8b. Create the team
|
||||
|
||||
```
|
||||
TeamCreate(team_name: "sprint-{N}-{team}")
|
||||
```
|
||||
|
||||
This makes you the team lead.
|
||||
|
||||
#### 8c. Create tasks from tickets
|
||||
|
||||
For each ticket in the briefing, create a task:
|
||||
|
||||
```
|
||||
TaskCreate(
|
||||
subject: "#{id}: {title}",
|
||||
description: "Full ticket details from step 5, plus briefing notes
|
||||
and integration points for this ticket.",
|
||||
activeForm: "Working on #{id}: {short_title}"
|
||||
)
|
||||
```
|
||||
|
||||
After creating all tasks, mirror the dependency chain from the briefing
|
||||
using `TaskUpdate` with `addBlockedBy`.
|
||||
|
||||
#### 8d. Spawn agents
|
||||
|
||||
For each agent from the `**Agents:**` line, spawn a teammate in the
|
||||
background. Spawn all agents in parallel (one message, multiple Task calls):
|
||||
|
||||
```
|
||||
Task(
|
||||
subagent_type: "{name_lowercase}",
|
||||
team_name: "sprint-{N}-{team}",
|
||||
name: "{name_lowercase}",
|
||||
prompt: "You are on the {team} team for Sprint {N}.
|
||||
Branch: `{team}`
|
||||
|
||||
1. Read the sprint briefing: docs/sprints/sprint-{N}/{team}.md
|
||||
2. Read the decision files referenced in the briefing.
|
||||
3. Check TaskList for available work.
|
||||
4. Claim an unblocked task (TaskUpdate with owner: your name),
|
||||
mark it in_progress, and implement it.
|
||||
5. When done, mark the task completed and check TaskList for
|
||||
the next available task.
|
||||
|
||||
Use `db/connectors/ticket show <id>` for full ticket specs.",
|
||||
description: "Sprint {N} {team}: {name}",
|
||||
run_in_background: true
|
||||
)
|
||||
```
|
||||
|
||||
#### 8e. Report
|
||||
|
||||
Output to the user:
|
||||
- Team name: `sprint-{N}-{team}`
|
||||
- Agents spawned (names and roles)
|
||||
- Tasks created (count actionable vs blocked)
|
||||
- How to interact: `SendMessage` to talk to agents, `TaskList` to
|
||||
check progress
|
||||
|
||||
You are now the team lead. Agents work autonomously — monitor via
|
||||
`TaskList`, communicate via `SendMessage`, and handle blockers as
|
||||
they arise.
|
||||
@@ -0,0 +1,114 @@
|
||||
---
|
||||
name: start-workshop
|
||||
description: >
|
||||
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.
|
||||
Parses the workshop brief to extract participants, questions, and round format.
|
||||
Creates a team, tasks, and spawns agents as teammates via the Task tool.
|
||||
---
|
||||
|
||||
# Start Workshop
|
||||
|
||||
## Prerequisites
|
||||
|
||||
A workshop brief must exist at `docs/workshops/{name}/{name}-workshop-brief.md` containing:
|
||||
- **Participants** line (comma-separated agent names)
|
||||
- **Questions for participants** sections with numbered questions tagged by agent name
|
||||
- **Workshop Format** section defining number of rounds and their purpose
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Parse the Brief
|
||||
|
||||
Read the workshop brief. Extract:
|
||||
- Workshop name (from directory name)
|
||||
- Participant list (from `**Participants:**` line)
|
||||
- Per-participant questions (scan for `**{AgentName}**:` patterns in questions sections)
|
||||
- Round count and round descriptions (from `**Workshop Format**` section)
|
||||
|
||||
### 2. Create Team
|
||||
|
||||
```
|
||||
TeamCreate: team_name = "{workshop-name}", description from brief title
|
||||
```
|
||||
|
||||
### 3. Always-Present Agents
|
||||
|
||||
Two agents join every workshop regardless of the participant list:
|
||||
|
||||
| Agent | Role | Task | Participates in discussion? |
|
||||
|-------|------|------|-----------------------------|
|
||||
| **Qatux** | Documenter | Captures all decisions, questions, dissent, consensus. Writes `workshop-notes.md` per round, produces final `workshop-outcomes.md`. | No — observes and records only |
|
||||
| **SI** | Sprint prep | Suggest adding when outputs include tickets. Creates tickets from decisions, links to sprint backlog. | No — execution prep only |
|
||||
|
||||
Qatux and SI are **never dismissed early.** If the user reduces the team mid-workshop, keep qatux and si (if added). Documenting everything prevents loss of valuable information.
|
||||
|
||||
### 4. Create Tasks (Round 1)
|
||||
|
||||
One task per participant containing:
|
||||
- Instruction to read the full brief at its path
|
||||
- The specific questions assigned to that participant (extracted from all layers)
|
||||
- The output format from the brief's round description
|
||||
- **IMPORTANT — file output requirement:** Instruct each agent to write their full output to `docs/workshops/{name}/{agent}-round{N}.md` (e.g., `docs/workshops/test-architecture/gestalt-round1.md`). Agents must write to disk, not just send messages. This ensures Qatux and other agents can read all outputs directly without relying on message forwarding.
|
||||
|
||||
One task for Qatux: "Document Round N — read all agent output files at `docs/workshops/{name}/*-round{N}.md` and capture decisions, questions, and dissent."
|
||||
|
||||
Assign all tasks using TaskUpdate with `owner` = agent name.
|
||||
|
||||
### 5. Spawn Agents
|
||||
|
||||
Use the Task tool to spawn each agent as a teammate. Each call should:
|
||||
- Set `team_name` to the workshop team name
|
||||
- Set `name` to the agent name (e.g., "gestalt")
|
||||
- Set `subagent_type` to the matching agent type (same as name — see reference table)
|
||||
- Provide a prompt telling the agent to check TaskList for their assigned task
|
||||
|
||||
Spawn all agents in parallel (one Task call per agent in a single message). Agents will appear as teammates in the Claude Code UI and pick up their tasks from the shared task list.
|
||||
|
||||
For large workshops (>6 agents), spawn participants in batches to avoid overwhelming the system. Always-present agents (Qatux, SI) can run in background via `run_in_background: true`.
|
||||
|
||||
### 6. Monitor
|
||||
|
||||
- TaskList to check progress
|
||||
- SendMessage to nudge idle agents or provide clarification
|
||||
- Agents work autonomously — claim tasks, read the brief, produce responses
|
||||
|
||||
### 7. Between Rounds
|
||||
|
||||
When all Round N tasks are complete:
|
||||
1. Verify all agents wrote output files to `docs/workshops/{name}/`. If any are missing, nudge the agent or extract from their message and write the file yourself.
|
||||
2. Qatux reads all `*-round{N}.md` files and produces round summary in `round-{N}-notes.md`
|
||||
3. Create Round N+1 tasks (integration pass, synthesis, etc.) — include the same file output requirement
|
||||
4. Assign to agents with TaskUpdate
|
||||
5. Agents continue working
|
||||
|
||||
### 8. Wrap Up
|
||||
|
||||
**Always ask the user before wrapping up.** There may be more to discuss or additional rounds needed. Only proceed to wrap-up when the user confirms.
|
||||
|
||||
Wrap-up sequence:
|
||||
1. Qatux produces final `workshop-outcomes.md` from accumulated notes
|
||||
2. If SI is present, SI creates tickets from decided items
|
||||
3. Send shutdown_request to all agents (qatux and si last, after they finish their output tasks)
|
||||
4. TeamDelete to clean up
|
||||
|
||||
## Agent Type Reference
|
||||
|
||||
Agent name maps directly to subagent_type:
|
||||
|
||||
| Name | Domain | Typical workshop role |
|
||||
|------|--------|-----------------------|
|
||||
| gestalt | Systems design | Mechanics, fun factor, system interactions |
|
||||
| ozzie | Player experience | Wow moments, feel, emotional response |
|
||||
| tyre | Technical architect | Feasibility, performance, architecture |
|
||||
| stig | UI developer | Interface patterns, layout, interaction flows |
|
||||
| dudley | Server developer | Server-side systems, data flow, ECS |
|
||||
| paula | Narrative | Story, dialogue, character, political depth |
|
||||
| araminta | Visual design | Art direction, visual treatment, aesthetics |
|
||||
| nigel | Replayability | Emergent stories, second-playthrough, sandbox |
|
||||
| gore | Themes/endgame | What the game is about, philosophical questions |
|
||||
| mellanie | Copywriter | In-game text, voice, tone |
|
||||
| miri | Worldbuilder | Setting, factions, lore, consistency |
|
||||
| inigo | Sound design | Audio, soundscape, spatial audio |
|
||||
| troblum | Tech consultant | Second opinion on architecture/tech choices |
|
||||
| hoshe | QA/testing | Test plans, verification, quality |
|
||||
@@ -3,114 +3,83 @@ name: ticket
|
||||
description: >
|
||||
Manage project tickets in the SQLite ticketing database. Use when the user
|
||||
says "ticket", "create a ticket", "show tickets", "sprint", or invokes /ticket.
|
||||
Wraps the sqlite_connector.py for structured project management operations.
|
||||
Wraps the ticket CLI for structured project management operations.
|
||||
user-invocable: true
|
||||
allowed-tools: Bash, Read, Grep, Glob
|
||||
---
|
||||
|
||||
# Ticket Skill
|
||||
|
||||
Manage the Commonwealth project ticketing database at `db/commonwealth.db`.
|
||||
|
||||
## Access Method
|
||||
|
||||
**Always use the connector script, never the sqlite3 CLI:**
|
||||
```bash
|
||||
python3 db/connectors/sqlite_connector.py <command> [args]
|
||||
```
|
||||
Manage the project ticketing database. Basic usage (`ticket list`, `ticket show`,
|
||||
`ticket sprint --active`) and raw SQL wrappers are documented in CLAUDE.md.
|
||||
This skill covers the full command reference.
|
||||
|
||||
## Commands
|
||||
|
||||
### List tickets
|
||||
Show tickets filtered by status, type, or assignee:
|
||||
### List tickets (full flags)
|
||||
```bash
|
||||
python3 db/connectors/sqlite_connector.py query "SELECT id, type, title, status, priority, assigned_to FROM tickets WHERE status != 'cancelled' ORDER BY priority, id"
|
||||
```
|
||||
|
||||
Filter examples:
|
||||
```bash
|
||||
# By status
|
||||
python3 db/connectors/sqlite_connector.py query "SELECT id, type, title, status FROM tickets WHERE status='in_progress'"
|
||||
|
||||
# By type
|
||||
python3 db/connectors/sqlite_connector.py query "SELECT id, title, status FROM tickets WHERE type='initiative'"
|
||||
|
||||
# By assignee
|
||||
python3 db/connectors/sqlite_connector.py query "SELECT id, title, status FROM tickets WHERE assigned_to='tyre'"
|
||||
|
||||
# Current sprint
|
||||
python3 db/connectors/sqlite_connector.py query "SELECT t.id, t.title, t.status, t.assigned_to FROM tickets t JOIN sprints s ON t.sprint_id=s.id WHERE s.status='active'"
|
||||
```
|
||||
|
||||
### Show ticket detail
|
||||
```bash
|
||||
python3 db/connectors/sqlite_connector.py query "SELECT * FROM tickets WHERE id=<N>"
|
||||
db/connectors/ticket list [--status S] [--priority P] [--epic N] [--sprint N] [--assigned A] [--team T]
|
||||
```
|
||||
|
||||
### Create ticket
|
||||
```bash
|
||||
python3 db/connectors/sqlite_connector.py execute "INSERT INTO tickets (type, title, description, priority, decision_ref, parent_id) VALUES ('<type>', '<title>', '<description>', '<priority>', '<decision_ref>', <parent_id_or_NULL>)"
|
||||
db/connectors/ticket create <type> <title> [--parent N] [--priority P] [--decision D] [--team T]
|
||||
```
|
||||
|
||||
Types: `initiative`, `epic`, `story`, `task`, `bug`
|
||||
Priorities: `critical`, `high`, `medium`, `low`
|
||||
|
||||
### Update status
|
||||
```bash
|
||||
db/connectors/ticket status <id> <new_status>
|
||||
db/connectors/ticket done <id> [<id> ...]
|
||||
```
|
||||
Statuses: `backlog`, `ready`, `in_progress`, `review`, `done`, `cancelled`
|
||||
|
||||
### Update ticket
|
||||
### Assignment
|
||||
```bash
|
||||
python3 db/connectors/sqlite_connector.py execute "UPDATE tickets SET status='in_progress', assigned_to='tyre', updated_at=datetime('now') WHERE id=<N>"
|
||||
db/connectors/ticket assign <id> <agent>
|
||||
db/connectors/ticket unassign <id>
|
||||
```
|
||||
|
||||
### Ticket tree (parent-child hierarchy)
|
||||
### Team assignment
|
||||
```bash
|
||||
python3 db/connectors/sqlite_connector.py query "SELECT t.id, t.type, t.title, t.status, p.title as parent_title FROM tickets t LEFT JOIN tickets p ON t.parent_id=p.id ORDER BY COALESCE(t.parent_id, t.id), t.id"
|
||||
db/connectors/ticket team <id> <teams>
|
||||
```
|
||||
Teams are comma-separated, e.g. `server`, `client`, `server,client`.
|
||||
|
||||
### Sprint management
|
||||
```bash
|
||||
# Create sprint
|
||||
python3 db/connectors/sqlite_connector.py execute "INSERT INTO sprints (name, goal, start_date, end_date) VALUES ('<name>', '<goal>', '<start>', '<end>')"
|
||||
|
||||
# Activate sprint
|
||||
python3 db/connectors/sqlite_connector.py execute "UPDATE sprints SET status='active' WHERE id=<N>"
|
||||
|
||||
# Assign ticket to sprint
|
||||
python3 db/connectors/sqlite_connector.py execute "UPDATE tickets SET sprint_id=<sprint_id> WHERE id=<ticket_id>"
|
||||
|
||||
# Sprint status
|
||||
python3 db/connectors/sqlite_connector.py query "SELECT s.name, s.goal, s.status, COUNT(t.id) as tickets, SUM(CASE WHEN t.status='done' THEN 1 ELSE 0 END) as done FROM sprints s LEFT JOIN tickets t ON t.sprint_id=s.id WHERE s.status='active' GROUP BY s.id"
|
||||
db/connectors/ticket sprint [--active]
|
||||
db/connectors/ticket sprint assign <id> <sprint_id>
|
||||
```
|
||||
|
||||
For sprint-scoped operations (status overview, context dumps, lifecycle),
|
||||
use the dedicated sprint CLI instead: `db/connectors/sprint --help`
|
||||
|
||||
### Dependencies
|
||||
```bash
|
||||
# Add blocker
|
||||
python3 db/connectors/sqlite_connector.py execute "INSERT INTO ticket_deps (blocker_id, blocked_id) VALUES (<blocker>, <blocked>)"
|
||||
|
||||
# Show blockers for a ticket
|
||||
python3 db/connectors/sqlite_connector.py query "SELECT t.id, t.title, t.status FROM ticket_deps d JOIN tickets t ON d.blocker_id=t.id WHERE d.blocked_id=<N>"
|
||||
db/connectors/ticket deps <id>
|
||||
```
|
||||
|
||||
### Seed from decisions
|
||||
To re-seed the database with initiatives from DECISIONS.md:
|
||||
### Search and browse
|
||||
```bash
|
||||
python3 db/connectors/sqlite_connector.py seed-decisions
|
||||
db/connectors/ticket search <keyword>
|
||||
db/connectors/ticket epics [--status S]
|
||||
db/connectors/ticket children <id>
|
||||
db/connectors/ticket count [--status S]
|
||||
```
|
||||
|
||||
### Batch show
|
||||
```bash
|
||||
db/connectors/ticket show --brief <id> [<id>...]
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **SI (Project Manager)** is the primary user of this skill
|
||||
2. Decisions from DECISIONS.md become **initiatives**
|
||||
2. Decisions from decisions/ domain files become **initiatives**
|
||||
3. SI breaks initiatives into **epics** (major work areas)
|
||||
4. Epics break into **stories** (user-facing deliverables)
|
||||
5. Stories break into **tasks** (concrete work items assignable to agents)
|
||||
6. **Sprints** group tasks into time-boxed work periods
|
||||
7. Track history via `ticket_history` table for audit trail
|
||||
|
||||
## Labels
|
||||
```bash
|
||||
# Add label
|
||||
python3 db/connectors/sqlite_connector.py execute "INSERT INTO ticket_labels (ticket_id, label) VALUES (<id>, '<label>')"
|
||||
|
||||
# Find by label
|
||||
python3 db/connectors/sqlite_connector.py query "SELECT t.id, t.title FROM tickets t JOIN ticket_labels l ON t.id=l.ticket_id WHERE l.label='<label>'"
|
||||
```
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
---
|
||||
name: worktree-update
|
||||
description: >
|
||||
Sync worktree branches with main. Use when the user says "update worktrees",
|
||||
"sync branches", "merge main", "worktree update", or invokes /worktree-update.
|
||||
When on main: shows which worktree branches are ahead and lets the user pick
|
||||
which to merge into main (flags branches with open PRs). When on a non-main
|
||||
branch: merges main into the current branch. All operations are non-destructive.
|
||||
user-invocable: true
|
||||
allowed-tools: Bash, Read, AskUserQuestion
|
||||
---
|
||||
|
||||
# Worktree Update Skill
|
||||
|
||||
Sync worktree branches safely. Direction depends on the current branch.
|
||||
|
||||
## Safety Rules (NON-NEGOTIABLE)
|
||||
|
||||
- **Never force-push, reset --hard, rebase, or delete branches.**
|
||||
- **Never use `--no-verify` or skip hooks.**
|
||||
- **Always use `--no-edit` on merges** to avoid interactive editor prompts.
|
||||
- **Stop on merge conflicts** — report them and let the user decide. Never
|
||||
auto-resolve or abort a conflicted merge without asking.
|
||||
- **Fetch before comparing** — always `git fetch --all` first so commit
|
||||
comparisons are accurate.
|
||||
- **Dry-run first on main** — show the user exactly what will happen before
|
||||
merging anything into main.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Detect current branch
|
||||
|
||||
```bash
|
||||
git branch --show-current
|
||||
```
|
||||
|
||||
Branch determines the mode: `main` → outbound sync, anything else → inbound sync.
|
||||
|
||||
### 2a. On `main` — merge worktree branches into main
|
||||
|
||||
#### Fetch and compare
|
||||
|
||||
```bash
|
||||
git fetch --all
|
||||
```
|
||||
|
||||
Discover all worktree branches (excluding `main` itself):
|
||||
|
||||
```bash
|
||||
git worktree list | grep -v '\[main\]' | sed 's/.*\[//;s/\]//'
|
||||
```
|
||||
|
||||
For each worktree branch, check if it has commits ahead of main:
|
||||
|
||||
```bash
|
||||
git rev-list --count main..origin/<branch>
|
||||
```
|
||||
|
||||
Skip branches with 0 commits ahead. For branches that ARE ahead, collect:
|
||||
- Branch name
|
||||
- Number of commits ahead
|
||||
- One-line log of those commits: `git log --oneline main..<branch>`
|
||||
|
||||
#### Check for open PRs
|
||||
|
||||
```bash
|
||||
tea pr list --login schweitz --repo jpmschweitzer/settled-reach --state open --output simple
|
||||
```
|
||||
|
||||
Cross-reference open PR head branches with the ahead-of-main branches.
|
||||
|
||||
#### Present results
|
||||
|
||||
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
|
||||
normal review channels (use `/review-pr` instead)
|
||||
- Commit count and summary
|
||||
|
||||
Use `AskUserQuestion` to let the user pick which branches to merge.
|
||||
Exclude PR-flagged branches from the default options (but allow the user to
|
||||
override via "Other").
|
||||
|
||||
#### Merge selected branches
|
||||
|
||||
For each selected branch, one at a time:
|
||||
|
||||
```bash
|
||||
git merge <branch> --no-edit
|
||||
```
|
||||
|
||||
If a merge conflicts, **stop immediately**. Report the conflict and do NOT
|
||||
continue to the next branch. The user must resolve before proceeding.
|
||||
|
||||
After all merges, show the final state with `git log --oneline -N` (where N
|
||||
covers the new commits).
|
||||
|
||||
#### Backup the shared database
|
||||
|
||||
After successful merges on main, snapshot the database for git tracking:
|
||||
|
||||
```bash
|
||||
make db-backup
|
||||
```
|
||||
|
||||
This copies the shared `settledreach.db` (in the parent directory) to
|
||||
`docs/backups/settledreach.db.backup`. Stage and commit it with the merge
|
||||
if the file changed.
|
||||
|
||||
### 2b. Not on `main` — merge main into current branch
|
||||
|
||||
```bash
|
||||
git fetch --all
|
||||
git merge origin/main --no-edit
|
||||
```
|
||||
|
||||
If clean, report the result (fast-forward or merge commit, files changed).
|
||||
If conflicts, report them and stop.
|
||||
|
||||
### 3. Push prompt
|
||||
|
||||
After a successful merge, ask the user if they want to push:
|
||||
|
||||
```bash
|
||||
git push origin <current-branch>
|
||||
```
|
||||
|
||||
Never push without explicit confirmation.
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
# Pre-commit hook dispatcher. Runs modular checks from tooling/.
|
||||
# Installed via: git config core.hooksPath .config/hooks
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||
ERRORS=0
|
||||
|
||||
run_check() {
|
||||
local script="$1"
|
||||
local label="$2"
|
||||
if [ -x "$REPO_ROOT/$script" ]; then
|
||||
if ! "$REPO_ROOT/$script"; then
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
else
|
||||
echo "pre-commit: WARNING — $label skipped ($script not found or not executable)"
|
||||
echo " Run 'make setup' or check that $script exists and is executable."
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Checks ---
|
||||
run_check "tooling/check-fact-ids" "fact_id validation"
|
||||
|
||||
if [ "$ERRORS" -gt 0 ]; then
|
||||
echo ""
|
||||
echo "pre-commit: $ERRORS check(s) failed. Commit aborted."
|
||||
exit 1
|
||||
fi
|
||||
+23
-7
@@ -1,7 +1,24 @@
|
||||
# Database
|
||||
db/commonwealth.db
|
||||
db/commonwealth.db-wal
|
||||
db/commonwealth.db-shm
|
||||
# Build and cache
|
||||
.cache/
|
||||
server/target/
|
||||
tooling/content-converter/target/
|
||||
tooling/line-previewer/target/
|
||||
tooling/test-client/target/
|
||||
content-ron/
|
||||
|
||||
# Godot client (further ignores managed by client team)
|
||||
client/.godot/
|
||||
client/export/
|
||||
client/reports/
|
||||
|
||||
# Renderer (separate Godot project for sprite pipeline)
|
||||
renderer/.godot/
|
||||
renderer/**/*.import
|
||||
renderer/**/*.uid
|
||||
renderer/output/*.png
|
||||
|
||||
# Database (shared across worktrees at ../settledreach.db, not tracked)
|
||||
db/commonwealth.db*
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
@@ -18,8 +35,7 @@ Thumbs.db
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Claude Code internals (plans, session transcripts, local settings)
|
||||
# Note: .claude/agents/ and .claude/skills/ ARE tracked
|
||||
# Claude Code internals (plans, session transcripts)
|
||||
# Note: .claude/agents/, .claude/skills/, and .claude/settings.json ARE tracked
|
||||
.claude/plans/
|
||||
.claude/projects/
|
||||
.claude/settings.json
|
||||
|
||||
+604
-1
@@ -1,19 +1,622 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to the Commonwealth game project will be documented in this file.
|
||||
All notable changes to The Settled Reach project will be documented in this file.
|
||||
|
||||
Format based on [Keep a Changelog](https://keepachangelog.com/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v0.1.9] — 2026-02-18
|
||||
|
||||
### Fixed
|
||||
- `make game` now builds client before launching — was missing `build-client` dependency, causing class_name registration failures after `make clean`
|
||||
- `build-client` uses `--import --quit` instead of just `--quit` — ensures `.godot/` cache and `global_script_class_cache.cfg` are created from scratch
|
||||
- `make clean` preserves `client/.godot/` directory (clears contents only) to avoid Godot startup issues
|
||||
|
||||
### Added
|
||||
- `--description TEXT` flag for `ticket create` CLI — previously required raw SQL workaround to set ticket descriptions
|
||||
|
||||
### Changed
|
||||
- UI audio assets revised — monologue chimes re-generated (0.8s, insert-tech aesthetic), fog_recognition re-generated (was silent), all 8 assets normalized to 44.1kHz stereo LUFS -16 (#453)
|
||||
- review-pr skill: explicit verdict rules — critical/warning → REQUEST_CHANGES, suggestion-only → APPROVE
|
||||
|
||||
### Added
|
||||
- `make golden-diff` + `make golden-update` targets (#486) — developer workflow for golden file comparison and regeneration; safe restore on cargo failure
|
||||
- Gauntlet checklist YAML schema (#497) — 7 condition types evaluable from ObserverSnapshot, per-room checklists for 3 rooms, `make checklist-validate` and `make checklist-generate` targets, wired into `pre-pr-content` gate
|
||||
- Gauntlet room timer + personal bests (#496) — GauntletHUD shows TIMER: MM:SS (PB: MM:SS), starts on room entry, resets on room change, persists stats to user://dev/gauntlet-stats.json, session summary on disconnect, hidden in non-gauntlet mode
|
||||
- WRONG button F12 MVP (#495) — bug report capture: pause sim, show modal prompt, save snapshot.json + render.txt + description.txt to user://bug-reports/, Esc to cancel
|
||||
- GameState.room_id and gauntlet_mode fields — parsed from ObserverSnapshot, enabling gauntlet UI
|
||||
- BUG_REPORT action in InputMapper (F12 binding) with SimBridge wire guard (client-only)
|
||||
- 24 new anti-tedium tests — GauntletHUD lifecycle (16: timer, PB, visibility, room change, session tracking) + BugReportDialog (8: pause/unpause, wire guard, text render, edge cases)
|
||||
- whatsinagame starter kit — reusable multi-agent team bootstrap for any project (3-tier profiles, 18 skills, 16 agent archetypes, stakeholder personas, ticketing DB, decision tracking)
|
||||
- Domain-action naming convention for skills documented in create-skill guide
|
||||
- `gauntlet` feature flag (default-on) — allows stripping Gauntlet test world from release builds with `--no-default-features`
|
||||
- `EXPECTED_ENTITY_COUNT` and `RESET_PLATE_STABLE_IDS` constants — entity counts derived from StableId ranges instead of hardcoded values
|
||||
- Debounce exact-boundary test for room reset (tick 9 rejected, tick 10 accepted)
|
||||
- Reset plate StableId verification in `stable_id_ranges_match_spec`
|
||||
- Runtime content test (`content_runtime.rs`) separated from structural loading tests
|
||||
- Client P2 tests (#492) — 16 gdUnit4 tests for camera (smoothing, zoom, viewport, follow, no-pan), entity alpha/color (peripheral, forward, NPC color, player constant), UI (monologue, interaction, inventory, dialogue, pause, fog blob, fog z_index)
|
||||
- Client P3 tests (#493) — 12 gdUnit4 tests for z-layer ordering (floor/ysort/fog/UI), entity lerp (snap, converge, LERP_SPEED=12.0), Tyre additions (recognition, facing, delta scaling, blob removal)
|
||||
- Anti-tedium regression tests (#494) — 5 tests: F12 no-crash guard, no queued input, gauntlet UI hidden in default/normal snapshot/multi-tick modes
|
||||
|
||||
### Changed
|
||||
- Room reset API consolidated to `plan_reset` only — `execute_reset` removed (was a maintenance trap; production uses Commands via `plan_reset`)
|
||||
- `room_at()` documented with z-range and corridor overlap assumptions
|
||||
- TCP runtime test now has 10s read timeout and registers player in EntityRegistry
|
||||
|
||||
### Removed
|
||||
- Dead `RoomMember` component from reset.rs (defined but never used)
|
||||
- Protocol v8: dialogue_response field decoding (DialogueResponseEvent with line_id, text, speaker_entity_id) from server #305/D-028
|
||||
- Test client binary scaffolding (#480) — standalone crate at `tooling/test-client/` with CLI (--connect, --replay, --text, --json, --quiet, --golden, --ticks), exit codes (0/1/2), golden file JSON diff, JSONL replay loader
|
||||
- Snapshot text renderer (#481) — `format_snapshot_text(&ObserverSnapshot)` pub-exported from server crate, entity labels as kind:entity_id sorted by distance, room name stub, 10 unit tests
|
||||
- Sprint 9 (Gauntlet) briefing files — server, client, CI, audio, joint — 23 tickets across 4 teams
|
||||
- Weapon aim lock audio (`sfx_weapon_aim_lock.ogg`) — clinical targeting confirmation tone for weapon aim state (#440)
|
||||
- Stance change audio (`sfx_stance_change.ogg`) — subtle mechanical click for stance toggle feedback (#440)
|
||||
|
||||
### Fixed
|
||||
- MessagePack int_64 encoder dead code branch (#516) — `-(1 << 63)` overflowed making int_64 branch unreachable; negative values beyond int_32 now correctly encode as 0xd3 instead of 0xcf
|
||||
- Cross-encoder fixture pipeline hardened — encode failures now exit non-zero instead of writing empty .msgpack files (#475 review)
|
||||
- GDScript fixture test no longer silently skips on missing/empty fixture dir — asserts instead (#475 review)
|
||||
- GDScript fixture generator covers all PlayerAction variants (added MoveSouth, MoveEast, MoveWest, Unpause, ToggleStanceDown, WalkAway)
|
||||
- `make pre-pr` now checks GDScript fixture staleness alongside Rust fixtures
|
||||
- Protocol version bumped from 7 to 8 to match server — fixes 5 test failures from version mismatch
|
||||
- Interact action encoding changed from unit variant to struct variant to match server's PlayerAction::Interact { target_entity_id, verb }
|
||||
- Monologue duplication test (test_monologue_not_duplicated_after_consumption) fixed — was using poll_snapshot() which doesn't consume _last_snapshot in test mode
|
||||
- `make pre-pr` target — full pre-PR verification chain: lint → build → test → content validation → fixture staleness (#460, #465)
|
||||
- Branch-specific pre-PR variants: `make pre-pr-server`, `make pre-pr-client`, `make pre-pr-content`
|
||||
- Content cross-reference validation (9 checks) — canonical_id uniqueness, relationship targets, location slugs, dialogue locations, fact_ids, triangle membership, npc_count, dialogue line_ids, bidirectional relationships (#464)
|
||||
|
||||
### Fixed
|
||||
- Dialogue schema missing `focused` (mood) and `greeting` (situation) values added in Sprint 7-8 content
|
||||
- Speaker wire ID silent fallback — dialogue now warns and skips when target entity missing from registry (was silently using 0)
|
||||
- Cross-plugin system ordering — trigger_recognition_monologue now runs after detect_anomalies (latent determinism bug)
|
||||
- Walk-away ordering — process_walk_away now runs after process_talk_interaction (prevents same-tick race)
|
||||
- ActiveDialogue overwrite — new Talk while in existing dialogue now emits IncompleteInteraction before replacing
|
||||
- Server-side Talk range check — handle_talk now enforces CLOSE_RANGE before setting TalkRequest (was client-only)
|
||||
- ExamineNpc label collision — VerbKind::ExamineNpc now uses "Examine NPC" label (was "Observe", same as generic Observe)
|
||||
- Dead conditional in main.rs collapsed (both branches were identical)
|
||||
- WalkAway variant added to all_player_action_variants_roundtrip serialization test
|
||||
|
||||
### Changed
|
||||
- DialogueCooldownTracker.used changed from Vec to BTreeMap for O(log n) lookup (D-041 compliance)
|
||||
- MonologueState.shown_ids changed from Vec to HashSet for O(1) contains check (was O(n) per tick)
|
||||
- Secret trust tier documented as unreachable with TODO for Phase 2 KG-gated unlock
|
||||
|
||||
### Added
|
||||
- Determinism test: different_seed_produces_different_replay — exercises SimRng via dialogue weighted selection
|
||||
- Dialogue selection pipeline (#305, D-028) — 4-layer filtering engine: access tier from KG relationship, situation derivation from game state, trust tier, weighted topic+mood scoring via SimRng. Full Talk verb → selected line → ObserverSnapshot pipeline with cooldown tracking
|
||||
- ContentSlug component (#452) — stable content identity from YAML (e.g. "kael-davan") independent of runtime Entity handles, for knowledge graph interaction memory across save/load
|
||||
- Walk-away KG recording (#427, D-064) — IncompleteInteraction knowledge events with Talk/Confront type, ActiveDialogue tracking, walk-away detection clears dialogue and records in KG
|
||||
- Anomaly detection for urgent recognition (#450, D-060) — AnomalyMarker flags PersonOfInterest/Contradicted entities for 0.3s cognitive delay instead of 0.6s normal
|
||||
- Recognition monologue during cognitive delay (#451, D-060) — monologue fires at delay START (grey blob phase), not completion. v0.1 fallback lines, anomaly prioritization, cooldown tracking
|
||||
- Server --test-mode, --port, --seed CLI flags (#459) — LISTENING:{port} stdout signal, OS-assigned ports, deterministic seed override, stderr-only tracing
|
||||
- Determinism gauntlet test (#466) — 20-tick replay determinism regression test with movement, stance, pause/unpause exercise
|
||||
- Pause guard test suite (#461-463, #468) — 7 tests covering movement, unpause, roundtrip, stance, interact, batch, tick_rate during pause
|
||||
- EntityRegistry lifecycle tests (#469) — stale mapping, re-register, unknown unregister edge cases
|
||||
- Boundary value encode/roundtrip tests (#471) — 41 values across all MessagePack integer format boundaries
|
||||
- Encoding asymmetry tests (#473) — Rust decoder accepts GDScript-style signed encodings for unsigned fields
|
||||
- Boundary fixture generation (#472) — 14 raw + 5 snapshot fixtures at integer format boundaries
|
||||
- Malformed batch rejection test (#479) — truncated, garbage, and mixed payloads rejected atomically
|
||||
- Per-fix determinism unit tests (#467) — equidistant NPC ordering, visible tile sorting, same-tile mover resolution
|
||||
|
||||
### Fixed
|
||||
- Determinism: visible_ids HashSet → BTreeSet for stable iteration order (#456)
|
||||
- Determinism: visible entities in snapshot sorted by entity_id (#457)
|
||||
- Determinism: movers sorted by Entity bits in validate_movement (#458)
|
||||
- Pause guard blocks all actions except Pause/Unpause while paused (previously only blocked movement)
|
||||
|
||||
### Changed
|
||||
- Protocol version bumped from v7 to v8 (dialogue_response field in ObserverSnapshot)
|
||||
|
||||
### Added
|
||||
- AudioManager autoload (#255, D-068/D-069/D-073) — 5-bus architecture (Music, Ambient, WorldSFX, PlayerActions, UISounds), directory-scan asset registry, spatial/non-spatial playback, audio dip profiles (dialogue, confrontation, listening_focus) with low-pass filter sweep, zone crossfade stub
|
||||
- Dialogue response selection (#435, D-061/D-062) — structured options with response_id, priority sorting, max 3 visible, invisible locked options, RichTextLabel for BBCode support
|
||||
- Walk-away mechanic (#437, D-064) — WASD triggers WalkAway input during dialogue, 300ms fade, dialogue_active flag gates movement, re-show on server interrupt
|
||||
- Confrontation text styling (#436, D-063) — italic first-person options, 1.5s monologue beat with dialogue dim to 70%, audio dip via AudioManager, walk-away cancels in-flight beat
|
||||
- MessagePack boundary value tests (#470) — 41 values (25 positive, 16 negative), encode-only header verification, roundtrip, Rust-style unsigned decode overlap tests
|
||||
- Client P0 regression tests (#477) — monologue carry-forward (Bug #5), camera stability during pause (Bug #2)
|
||||
- Client P1 tests (#478) — fog shader state (4), entity lifecycle (2), pending recognition blob (1)
|
||||
|
||||
### Changed
|
||||
- Fog byte magic numbers replaced with named constants (#476) — VIS_HIDDEN/PERIPHERAL/FORWARD, EXP_UNEXPLORED/EXPLORED/VISIBLE in FogState
|
||||
- Dialogue options now carry structured {text, response_id, priority, confrontation} instead of plain strings
|
||||
- Protocol v7 dialogue decode validates and skips malformed options
|
||||
|
||||
### Added
|
||||
- QA test architecture workshop complete — 3-round, 7-agent workshop producing 60 tickets (epic #455): Gauntlet test world (7 rooms, 48 entities), test client binary (tooling/test-client/), determinism fixes, content validation, make pre-pr pipeline, 38 client tests, anti-tedium features, human tester workflow
|
||||
- Workshop skill updated — agents now write output files to disk instead of sending messages, fixing documenter access
|
||||
- Camera anchor test suite — 10 gdUnit4 tests verifying camera init, smoothing cycle, and player tracking
|
||||
- Entity position lerping — framerate-independent exponential smoothing so entities slide between tiles instead of snapping
|
||||
|
||||
### Fixed
|
||||
- Server never sends snapshots — blocking TCP read in receive_bridge_inputs stalled the entire bevy Update schedule; switched to non-blocking I/O with WouldBlock handling
|
||||
- Camera doesn't center on player at startup — Camera2D smoothed_camera_pos starts at (0,0); now disable smoothing during init, snap to player, re-enable after first anchored frame
|
||||
- Player moves while game is paused — movement commands now discarded when TickRate is Paused (pause/unpause still process)
|
||||
- Spacebar only pauses, doesn't toggle — added UNPAUSE action with toggle logic based on tick_rate state
|
||||
- MessagePack encodes tick 128 as -128 — off-by-one in signed int boundary checks (<=128 instead of <128) across int8/16/32/64 branches
|
||||
- Monologue/dialogue lost on snapshot overwrite — one-shot events now carried forward when a newer snapshot replaces an unconsumed one
|
||||
- Fog shader white screen on load failure — ColorRect defaults to transparent, shader load failure logged instead of crashing
|
||||
- Fog desync during camera smooth pan — fog rect now tracks camera position instead of player position
|
||||
|
||||
### Changed
|
||||
- Server game loop throttled to ~20 ticks/sec (50ms frames) — non-blocking TCP loop no longer spins; remaining frame budget available for NPC AI
|
||||
- Hold-to-move input model — movement polled each frame with stance-based throttle (Sprint=200ms, Walk=400ms, Careful=600ms, Crouch=800ms) and composite diagonals (W+D → northeast)
|
||||
- D-053 updated with client throttle rates and input model documentation
|
||||
|
||||
### Added
|
||||
- Dialogue box UI skeleton (#434, D-061) — bottom screen, max 20% height, ~65% width, NPC speech + max 3 response options, insert-styled colors, WASD walk-away with 300ms fade, no close button, diegetic on InsertOverlay z-layer 6
|
||||
- Fog entity visualization (#431, D-059/D-060) — cognitive delay rendering: sonar-style sound pings (3 concentric rings, 1.5s fade), unrecognized grey blobs with 0.8s breathing pulse, D-033 color transition at 50% recognition progress, ±0.5 tile position drift, FogEntities node at z:950
|
||||
|
||||
### Changed
|
||||
- Client protocol version bumped from 6 to 7 (pending_recognitions decode for cognitive delay)
|
||||
- GameState: current_dialogue and pending_recognitions fields wired from ObserverSnapshot v7
|
||||
- Scene tree: DialogueBox added to InsertOverlay, FogEntities at z:950 between fog shader and InsertOverlay
|
||||
- Test mode: mock dialogue (Kael NPC, 3 options) and mock cognitive delay entity (6-tick recognition cycle)
|
||||
|
||||
### Added
|
||||
- Archetype evidence presentation spec (#443, D-065/D-034/D-033) — detective case file vs smuggler notebook design document: item definitions, knowledge graph presentation, contradiction markers, THE FRIEND arc walkthroughs, systems interaction map, authoring guidelines
|
||||
- Cognitive delay system (#423, D-060) — CognitiveDelay component buffers perception events before emitting KnowledgeEvents (0.6s base / 0.3s urgent at 10 tps), pending_recognitions in ObserverSnapshot v7 for client fog entity visualization, cancellation on entity LOS exit
|
||||
- ListeningFocus eavesdrop system (#426, D-053) — stationary_ticks tracking for eavesdrop positioning bonus, 30-tick threshold (20 for Careful stance), Sprint blocks accumulation, registered after validate_movement
|
||||
- YAML content loader with hot-reload (#326, D-028) — LinePool system parsing dialogue/monologue YAML into BTreeMap-indexed pools, 4-layer query filtering (access > situation > trust > topic+mood), timestamp-polling hot-reload (dev-only), graceful failure preserves previous content
|
||||
- Line pool format specification (#308) — formal spec at docs/architecture/line-pool-format.md defining YAML structure, tag enums, 4-layer filtering pipeline, prerequisite-to-KG mapping, ID format, and Rust loader interface
|
||||
- InteractionMemory KG schema design (#442, D-064) — design doc at docs/architecture/interaction-memory-schema.md extending FactKnowledge with interaction tracking, 5-state InteractionState enum, monologue prerequisite extension, NpcTolerance reconciliation
|
||||
- Audio discussion decisions D-067 through D-074: recognition chime timing, 5-bus architecture, audio dip profiles, confrontation as cognitive vulnerability, monologue chime placeholder strategy, universal conversation murmur, zone crossfade, hybrid audio generation
|
||||
- Asset pipeline documentation system (docs/assets/) with category-based index, sonic palette, and generation templates for audio, visual, and video pipelines
|
||||
- Stable Audio Open connector and post-processing wrappers (audio-generate, audio-health, audio-post) with timeout handling for 11GB VRAM constraint
|
||||
- gen-audio skill with prompt assembly system (sonic palette prefixes + category templates + asset descriptions)
|
||||
- 6 interaction UI audio assets (#440): cursor_hover, weapon_aim, implant_open, fog_recognition, sfx_monologue_chime, sfx_monologue_chime_urgent — generated via SAO, needs duration trimming (#453)
|
||||
- Dialogue/confrontation ambient dip implementation spec with full Godot AudioBus tween code
|
||||
- Synthesis tooling (tooling/synth_ui_sounds.py) for programmatic insert-tech sound generation
|
||||
|
||||
### Changed
|
||||
- Protocol version bumped from 6 to 7 (pending_recognitions field in ObserverSnapshot)
|
||||
- MessagePack fixtures regenerated for protocol v7
|
||||
- Monologue trigger system uses .values() iterator (clippy fix)
|
||||
- Monologue schema: relationship prerequisite now requires target and state fields
|
||||
- 389 tests total (70 new) — cognitive delay pipeline, line pool loader, ListeningFocus, content watching, serialization
|
||||
- Renamed asset-gen skill to gen-image for consistent gen-* naming pattern
|
||||
- Client protocol v6 bridge — decode player_stance (4 variants) and player_inventory from ObserverSnapshot, TOGGLE_STANCE_UP/DOWN input actions, 25 gdUnit4 tests
|
||||
- Three-scope z-layer rendering pipeline (D-049) — world z:0-900 inside CanvasGroup, insert overlay CanvasLayer 10, UI CanvasLayer 20, modal CanvasLayer 30. Y-sort contract enforced, reserved VFX/airborne/lower-floor ranges documented
|
||||
- Cursor state machine (#429, D-056) — 4 states (Default/EntityHover/ObjectHover/WeaponAim), 150ms transitions, insert-styled geometric shapes
|
||||
- Fog shader rebuild (#430, D-059) — 5-layer fragment shader with animated Perlin noise, CanvasGroup compositing, FogState autoload for visibility/exploration textures
|
||||
- Entity interaction list (#432, D-057) — vertical multi-verb menu, insert-styled, sprint suppression, diegetic toggle
|
||||
- World radial menu (#433, D-058) — 2 spokes (Observe + Insert), drag-release and click-click input, 60-degree acceptance zones
|
||||
- Inventory UI (#438, D-065) — 3x3 grid, 40x40px slots, 1-9 hotkey selection
|
||||
- Stance indicator (#439, D-053) — color-coded HUD text, C/X keybinds
|
||||
- Architecture docs: z-layer gap analysis, fog shader spec, flying taxi feasibility analysis
|
||||
|
||||
### Changed
|
||||
- Scene tree restructured: Entities z_index 3->0 (critical y-sort fix), FloorObjects->10, YSortGroup->100, Overhead->300, FogOverlay->900, ModalLayer added
|
||||
- constants.gd rewritten with three-scope z numbering and full reserved range documentation
|
||||
- Fog renderer replaced: TileMapLayer-based fog_renderer.gd deleted, replaced by shader-based fog_shader.gd + fog.gdshader
|
||||
|
||||
### Added
|
||||
- ObserverSnapshot v6 wire protocol (#449) — player_stance (MovementStance) and player_inventory (Vec\<InventoryItem\>) fields with serde defaults for backward compatibility
|
||||
- Stance system (#417) — Sprint/Walk/Careful/Crouch movement stance with tick-based speed (1/2/3/4 ticks per move), monologue rate multipliers (40%/100%/150%/100%), PlayerMoveCooldown component, ToggleStanceUp/Down player actions
|
||||
- TilePresence posture layers (#420) — Standing/Prone/Seated/Fixture occupancy layers enabling same-tile coexistence (e.g. seated NPC + standing player), layer-based collision in validate_movement
|
||||
- ObjectType component (#421) — Readable/Container/Terminal/Door/Pickup/Furniture types with Phase 1 verb sets computed from type + proximity range
|
||||
- Phase 2 verb filter (#422) — KG-gated observer-side verb processing: POI priority flips (D-060), Confront injection at KnowsDetails+ confidence, contradiction marking, archetype-specific label relabeling (Smuggler sees Move/Stash, Detective sees Scan/Flag on containers)
|
||||
- CharacterArchetype component — Smuggler/Detective archetype for Phase 2 verb label differentiation (D-057)
|
||||
- VerbKind::Confront — Phase 2 only verb injected when observer has KnowsDetails+ on an NPC at close range
|
||||
- Smuggler inventory system (#424) — CarriedBy(StableId) component, Take/Place verbs, 9-slot (3x3 grid) capacity, auto-slot assignment, info boundary enforcement (carried items invisible to other observers)
|
||||
- MovementProfile component (#418) — per-archetype default stance (smuggler=Walk, detective=Walk), applied on spawn, factory methods for future archetypes
|
||||
- Sprint interaction buffer suppression (#419, D-055) — sprint stance explicitly clears interaction buffer, no verbs computed or sent during sprint, anomaly monologue pipeline unaffected
|
||||
- Sprint anomaly double-take monologue (#428, D-055) — SprintAnomalyQueue component detects Contradicted entities during sprint, fires delayed retroactive monologue after ~1.5s ("Wait — something wasn't right back there"), first-in-wins queue semantics, 3 hardcoded v0.1 lines
|
||||
|
||||
### Changed
|
||||
- Protocol version bumped from 5 to 6 (stance, inventory, ObjectType, verb system fields)
|
||||
- MessagePack fixtures regenerated for protocol v6
|
||||
- Input processing queries expanded for stance and cooldown components with backward-compatible Option wrapping
|
||||
- Observer pipeline queries expanded for Stance and CharacterArchetype components
|
||||
- NearbyInteraction carries object_type and contradicted fields for Phase 2 context
|
||||
- BridgePlugin system ordering: process_sprint_anomaly_monologue runs after trigger_monologue, compute_observer_snapshot runs after anomaly processing
|
||||
- Player spawn includes MovementProfile, Stance, PlayerMoveCooldown, and SprintAnomalyQueue components
|
||||
- 331 tests total (131 new) — comprehensive QA coverage across stance, occupancy, Phase 2 verbs, sprint suppression, inventory, anomaly monologue, and wire format
|
||||
|
||||
### Added
|
||||
- D-066: Dual-scale grid — 0.5m simulation tiles for stealth granularity, 1m visual tiles for proportional art (2x retina factor). All world geometry 2x2 sim tile minimum so cover/LOS maps 1:1 with visuals. Amends OQ-01.
|
||||
- Sprint CLI (`db/connectors/sprint`) — unified sprint lifecycle management with 5 subcommands: status, start, stop, start-work, prepare. Auto-detects sprint from DB state and team from git branch. Guards prevent activating unplanned sprints.
|
||||
- Shared permission settings in `.claude/settings.json` — git, ticket/sprint CLI, make, tea, and core skills pre-approved across all worktrees. Deny rules block destructive operations.
|
||||
- Decisions D-053 through D-065 from Control & Interaction Workshop — formalized interaction verb system, contextual actions, NPC awareness model, and related design decisions
|
||||
- Sprint 6 Touch briefings for server, client, copy, and joint teams
|
||||
- Control & Interaction Workshop outputs — full workshop notes and outcomes
|
||||
- Smuggler inventory item specs for transit district (#441)
|
||||
- Start-workshop skill for multi-agent design workshops
|
||||
|
||||
### Fixed
|
||||
- Added worktree boundary rules to CLAUDE.md — agents must stay within the git root, no navigating to sibling worktrees or above the repo
|
||||
- Plan-sprint skill now enforces worktree-relative paths in generated briefings
|
||||
- Worktree-update skill now discovers branches dynamically via `git worktree list` instead of relying on hardcoded branch names — fixes missed branches like `planning`
|
||||
|
||||
### Changed
|
||||
- Start-sprint and plan-sprint skills updated to use sprint CLI instead of manual multi-query workflows
|
||||
- Permission syntax migrated from deprecated `:*` suffix to modern space-wildcard format across all worktrees
|
||||
- Internal monologue trigger system (#414) — enter_location fires on first tick, time_idle fires after 100 ticks of no movement, 300-tick cooldown, dedup within session, random line selection from content pools via ChaCha20 RNG
|
||||
- MonologueEvent in ObserverSnapshot v5 — current_monologue field carries id, text, and display duration across the IPC bridge
|
||||
- Client monologue display wiring — protocol v5 decoding, GameState extraction, HUD display pass-through
|
||||
|
||||
### Fixed
|
||||
- NPC spawn missing Interactable component (#413) — NPCs spawned from content and proof room now have Interactable, enabling E-prompt detection
|
||||
- PlayerAction::Interact was a no-op (#415) — changed from unit to struct variant with target_entity_id and verb fields, server logs interaction data
|
||||
|
||||
### Changed
|
||||
- Protocol version bumped from 4 to 5 (MonologueEvent field, Interact variant change)
|
||||
- MessagePack fixtures regenerated for protocol v5
|
||||
|
||||
### Changed
|
||||
- UIStrings YAML parser now handles arbitrary nesting depth and inline comments — adapts to copy team's restructured ui-strings.yaml with multi-level sections (relationship_states, health_values)
|
||||
- HUD uses new YAML keys: `hud.perception_mode_prefix`, `hud.time_prefix`, `hud.health` (empty prefixes display values directly)
|
||||
- Interaction prompt keybind hint ("E") hardcoded instead of loaded from YAML — keybinding is not copywriter text
|
||||
|
||||
### Added
|
||||
- Pre-commit FactId validation hook (#393) — grep-based check validates fact_id references in content YAML against canonical knowledge catalogs; advisory mode when catalogs are stubs, enforcing mode when populated
|
||||
- Pre-commit hook infrastructure — `.config/hooks/` with modular dispatcher, `make setup-hooks` target, `core.hooksPath` config for worktree-safe hook installation
|
||||
- `make check-fact-ids` target for manual fact_id validation
|
||||
- UIStrings autoload with YAML-based UI string loading (#409) — minimal YAML parser, `get_text()` lookup with fallback-to-key
|
||||
- HUD and interaction prompt labels now loaded from `client/data/ui-strings.yaml` instead of hardcoded strings
|
||||
- Character voice speech patterns (#310) — sentence-level execution spec for smuggler and detective covering contractions, punctuation, stress markers, vocabulary, verbal tics, and authoring checklist
|
||||
- NPC authoring style guide (#379) — 954-line handbook: tier budgets, 9 NPC patterns, dialogue/monologue rules, tag taxonomy, dual-lens coordination, Krenn/Sova culture, FRIEND phase mapping, validation checklist
|
||||
- UI microcopy (#409) — 72 YAML strings for client integration: interaction verbs, relationship states, HUD labels, perception modes, notifications, knowledge panel, tutorial prompts
|
||||
- Kael Davan FRIEND pack (#297) — 86 hand-authored lines across 3 locations, 5-phase relationship arc with contradiction scene, dual-lens notes
|
||||
- Sera Venn FRIEND pack (#298) — 75 hand-authored lines at The Last Shift, trust-gated gossip, avoidance contradiction, contaminated trust arc
|
||||
- PC-as-NPC content (#401) — 70 authored items enabling second-playthrough recognition (D-039 wow moment #4)
|
||||
|
||||
### Changed
|
||||
- Moved ui-strings.yaml from content/campaigns/_meta/ to client/data/ for direct Godot client loading
|
||||
|
||||
### Fixed
|
||||
- Fact ID format collision across FRIEND content — normalized 73 flat IDs to dotted category.topic format, fixing broken detective evidence chain from Sera to Kael observations
|
||||
- Entity ref format inconsistency — normalized underscore format to npc:hyphenated across ~15 monologue references
|
||||
- Phase tag inconsistency between FRIEND packs — standardized to phase-N format, added missing phase-2 tags to Kael ring ops lines
|
||||
- Detective monologue gap for Kael — added 7 observation lines (7 → 15 total), added mood tags and bar_evening situation to Kael bar content
|
||||
- Hoshe QA briefing updated with integration test coverage priorities — test harnesses, dedicated client-server test map, edge case focus
|
||||
- Sprint 5 "Live" team briefings — copy (6 tickets), client (2), CI (1), joint coordination for content-at-scale sprint targeting FRIEND packs, voice patterns, NPC style guide, PC-as-NPC authoring, UI microcopy, FactId validation
|
||||
- Live server mode (`make game`) — single command builds server, launches client with TCP connection, auto-kills server on exit; `make stop` helper for manual cleanup
|
||||
- `SR_LIVE=1` environment variable switches SimBridge from test mode to real TCP server connection
|
||||
- TileKind in wire protocol (#412) — server sends Floor/Wall/Door/Object per visible tile, client renders walls and floors in live mode
|
||||
- Input roundtrip integration test (#411) — spawns real server, connects via TCP, validates full movement and interact pipeline
|
||||
- Debug logging for received player inputs on server (visible with `RUST_LOG=debug`)
|
||||
|
||||
### Fixed
|
||||
- Interaction prompt target+verb data now attached to Interact action in game loop (#405) — was TODO stub, server receives `{target_entity_id, verb}` payload
|
||||
- Player entity detection uses `kind.variant == "Player"` instead of hardcoded `entity_id == 1` — fixes "player not found" warnings when connected to real server (which assigns different IDs)
|
||||
- Movement keys now work in live mode — client was sending millisecond timestamps as input tick, server only processes ticks <= current frame counter; now uses server tick from latest snapshot
|
||||
- Entity-to-tile alignment in live mode — server sends tile-center render coords (tile 16 → 16.5), entity renderer now floors to tile index before positioning
|
||||
|
||||
### Added
|
||||
- Content loader Phase 2 (#408) — 2-phase spawn pipeline loads real YAML content into ECS entities: enums, entity attributes, pools, templates, triangles, NPC profiles with Want/Tolerance/Contentment/Personality/Tells/Skills axes plus cross-reference resolution for Secrets, Relationships, Information, and DailyRoutine
|
||||
- Global enum YAML files (#387) — 9 enum definitions (situations, topics, moods, triggers, access-tiers, trust-tiers, activities, patterns, motivations) from D-035 taxonomy
|
||||
- Entity attributes YAML (#388) — 16 canonical knowledge graph attribute keys from D-024 with A7 workshop updates
|
||||
- Seed-time pools (#389) — 5 single-candidate pools for deterministic v0.1 testing
|
||||
- Social site templates (#390) — 3 templates (logistics-hub, bar, smuggling-ring) with role slot definitions per D-025
|
||||
- Triangle YAML files (#391) — 5 v0.1 triangles (3 active fork, 2 passive) per D-024 workshop synthesis
|
||||
- Seed configuration schema design (#394) — design document defining game-start randomization: FRIEND selections, pool draws, template assignments, entanglement config, ChaCha20 RNG protocol
|
||||
- YAML to RON converter tool (#403) — build-time converter in tooling/content-converter/, runs via `make content-ron`
|
||||
- Line previewer CLI (#407) — 4 subcommands (dialogue, monologue, coverage, sequence) for content authors to test line selection without running the full game
|
||||
- Happiness added to WantKind enum — Harek remapped from Safety to Happiness
|
||||
|
||||
### Fixed
|
||||
- Dialogue-pool schema corrected to use arrays for situation/topic/mood per D-035 (were incorrectly single strings)
|
||||
- NPC want.primary changed from narrative strings to WantKind enum keywords — fixes silent Want component drop at spawn time
|
||||
- Schema enum constraint added to npc-profile.schema.json for want.primary validation
|
||||
|
||||
### Added
|
||||
- Sprint 4 "Feel" team briefings — copy (8 tickets), server (9), client (1 carry-over), CI (1), joint coordination
|
||||
- Interaction prompt system (#405) — server-driven "E - Talk" prompt decoding v4 nearby_interactions with nested VerbOption structs, fade animation, extensible get_interaction_target/get_selected_verb interface for future radial verb menu
|
||||
- Art direction & mood board workshop (3 rounds + closing) — 4-agent team establishes visual identity, 16 art direction principles, 9 mood board images, 10 candidate decisions (D-042–D-051)
|
||||
- 3D-to-2D sprite render pipeline (`client/tooling/sprite_renderer/`) — Godot @tool scene renders textured 3D models at "the angle" (-72.5deg ortho) from 4 cardinal directions at 1024/256/64 resolutions with outline applied at working resolution
|
||||
- `/render-sprite` skill — CLI wrapper for the render pipeline with headless import step
|
||||
- Pipeline POC: Era 1 institutional wall + bar green wall textures generated via Nano Banana and rendered through full pipeline
|
||||
- PerceptionQuery trait and ActivePerceptionMode resource — abstraction layer for D-017 perception mode swapping (NaturalVision default implementation)
|
||||
- VisibilityGeometry intermediate resource decoupling FOV computation from entity filtering
|
||||
- Client-side PROTOCOL_VERSION enforcement — snapshot decoder rejects version mismatches with error log
|
||||
- POI verb priority test in observer pipeline — asserts both verb kind and priority values end-to-end
|
||||
|
||||
### Changed
|
||||
- Observer pipeline decomposed into two-stage system: compute_visibility_geometry (geometry) → compute_observer_snapshot (entity filtering + assembly)
|
||||
- POI verb priority adjustment moved from simulation phase (interaction.rs) to perception phase (observer) — fixes D-010 information boundary violation
|
||||
- compute_nearby_interactions no longer reads KnowledgeGraph — determines verb availability by proximity only, verb priority adjusted by observer
|
||||
- compute_nearby_interactions scheduling moved from SimulationPlugin to BridgePlugin for explicit ordering with geometry and observer systems
|
||||
- Client test snapshot updated to v4 format (Protocol.PROTOCOL_VERSION, tick_rate replaces paused)
|
||||
|
||||
### Added
|
||||
- Content validation tooling — `make validate-content` validates campaign YAML files against JSON schemas, maps files by directory context
|
||||
- Entity::to_bits() roundtrip test — guards against bevy version changes silently breaking wire IDs
|
||||
- TickRate switch mid-accumulation test — verifies Half→Full→Paused→Half transitions preserve accumulator state
|
||||
- Tick rate scaling system (#406, D-052) — Full/Half/Paused rates with fractional accumulation, SetTickRate player action, replaces binary pause flag
|
||||
- Proximity detection and interaction verbs (#404, D-060) — compute_nearby_interactions system with Manhattan distance ranges (close ≤2, mid ≤5), context-sensitive verb computation (Talk, Observe, Examine), PersonOfInterest priority flip, NearbyInteraction in ObserverSnapshot v4
|
||||
- Content directory skeleton (#385, D-057) — district-as-atomic-pack layout with Sova Transit first district, 17 NPC stubs, 3 locations, 5 triangles, dialogue/monologue pools, factions, knowledge catalogs, enum definitions
|
||||
- Content schema definitions (#386) — 8 JSON Schema files (draft 2020-12) for district, location, npc-profile, dialogue-pool, monologue-pool, triangle, routine, fact-catalog validation
|
||||
- PROTOCOL_VERSION constant in bridge types — versioning strategy documented (subprocess IPC, serde defaults for field evolution)
|
||||
- Campaign, system, station JSON schemas for hierarchical content validation
|
||||
|
||||
### Fixed
|
||||
- Test suite aligned with server v4 protocol enforcement — all hand-built snapshots include version field, verb priorities 1-indexed, ExamineNpc label corrected to "Observe"
|
||||
- E2E proof tests resilient to entity ordering — player found by kind instead of array position, wall-hides test checks specific NPC position instead of total count, supports 3-NPC proof room layout
|
||||
- Wire entity_id now uses StableId consistently across observer, observation, interpretation, and interaction systems (was Entity::to_bits() in some paths)
|
||||
- Restored system/station/district hierarchical fields in district metadata (incorrectly removed during canonical_id cleanup)
|
||||
- Unregistered entities in observer/interaction now log tracing::error instead of silently falling back to Entity::to_bits()
|
||||
|
||||
### Changed
|
||||
- NearbyInteractionBuffer refactored from global Resource to per-entity Component on PlayerCharacter — multiplayer-ready (D-009)
|
||||
- Observer module split into mod.rs (244 lines) + tests.rs (480 lines) — reduces module complexity
|
||||
- Content directory restructured from flat districts/ to hierarchical campaigns/main/systems/krenn/stations/sova/districts/transit/ — path mirrors canonical IDs, glob-based discovery, multi-campaign/DLC ready
|
||||
- Content manifest (content.yaml) rewritten for glob-based district discovery
|
||||
- District identity fields (system, station, district) now derived from directory path — removed from district.yaml required fields
|
||||
- NPC canonical_id schema accepts district-scoped IDs (npc:transit.kael-davan) for cross-district uniqueness
|
||||
- ObserverSnapshot protocol bumped to v4 — adds nearby_interactions field, tick_rate replaces paused field in GameTime
|
||||
- NearbyInteractionBuffer.interactions is now private with take() accessor (no per-frame clone)
|
||||
- NearbyInteraction.distance changed from f32 to u32 (matches manhattan distance)
|
||||
- Missing PlayerCharacter in input processing now panics instead of silent no-op
|
||||
- Verb sort uses (priority, kind) tuple for deterministic ordering at equal priority
|
||||
- Unregistered entity in knowledge events triggers debug_assert + error (was warn)
|
||||
- District schema: canonical_id is now optional (derived from directory path at load time)
|
||||
- Skills trimmed for CLAUDE.md deduplication — search-docs (-48%), ticket (-17%), review-pr (-35%) now reference CLAUDE.md for basics instead of repeating them
|
||||
- Review-pr reviewer profiles extracted to `references/reviewer-profiles.md` for progressive disclosure
|
||||
|
||||
### Removed
|
||||
- Dead generate_snapshot function in bridge/mod.rs — superseded by compute_observer_snapshot
|
||||
- content/global/regions/ directory — region data absorbed into system.yaml metadata
|
||||
|
||||
### Added
|
||||
- Dual Lens Authoring Guide — 7-chapter reference for writing content that works for both smuggler and detective perspectives (D-027, D-028, D-032, D-034, D-035)
|
||||
- THE MIRROR pattern spec — transparency-as-contrast NPC design with Naia Tamm reference implementation and generator template
|
||||
- Smuggler voice card — register parameters, 5 voice anchors, 4 anti-patterns, paired comparison examples, display constraints, authoring checklist
|
||||
- Smuggler moral arc spec — 4-phase trajectory (Comfort, Doubt, Reckoning, Compromise), FactId gates, monologue trigger rules, Kael intersection mapping
|
||||
- PC-as-NPC unified spec — starting knowledge/relationship graphs, tell inversion, orientation monologue, 9-step conversion checklist, v0.1 smuggler + detective briefs
|
||||
- Triangle 1 Hub Power Volume Escalation fork — 3-path smuggler decision (escalate/stabilize/mediate), ~41 authored dialogue lines, NPC state change tables
|
||||
- Content directory structure design doc — runtime content/ layout, canonical ID format, 8 JSON Schema specs, 3-tier validation pipeline, migration path from wiki
|
||||
- Interaction verb spec — 7 v0.1 verbs (Move, Look, Monologue, Examine Object, Examine NPC, Talk, Overhear), priority resolution, server pipeline architecture
|
||||
- v0.1 wow moments checklist — maps all 6 D-039 moments to content deliverables, tickets, dependencies, completion status
|
||||
- Nils Davan off-stage NPC stub — ring coordinator, GHOST + HANDLER pattern, lattice message design, relationship map
|
||||
- NPC pattern/motivation mapping applied to all 18 NPC wiki pages with composition reads
|
||||
- Structured NPC data model (#86) — replaced stub string/f32 fields with typed enums and integer types for D-010 determinism (WantKind, SecretSeverity, Skill, PersonalityTrait, CombatStyle, RelationshipKind)
|
||||
- Global RelationshipGraph resource (#87) — BTreeMap with tuple key for efficient prefix queries and reverse lookups
|
||||
- A* pathfinding system (#237) — PathRequest/ComputedPath/PathBlocked components with cardinal-neighbor A* and manhattan heuristic
|
||||
- NPC path following system (#238) — MovementSpeed throttling, per-tick path advancement with MoveIntent creation
|
||||
- Daily routine system (#88) — NpcPlugin with PreviousDayPhase resource and check_phase_transition system issuing PathRequests at day-phase boundaries
|
||||
- Multiple NPC spawning (#84) — 3 distinct NPCs (dock worker, field tech, guard) with full component bundles and RelationshipGraph edges
|
||||
- Observation event generator (#239) — RoutineDeviation, Absence, and NewEntity triggers from comparing visible snapshot against NPC routines and knowledge state
|
||||
- Review-pr skill routes reviewers by branch type — server/client get Hoshe+Tyre, copy gets Hoshe+Paula+Miri, visual gets Hoshe+Araminta, audio gets Hoshe+Ozzie
|
||||
- Start-sprint skill spawns team agents from sprint briefings — parses the Agents line, creates a team with tasks from tickets, and launches all listed agents as background teammates
|
||||
|
||||
### Fixed
|
||||
- IPC error handling (#341) — DeserializationWithDump/MutexPoisoned error variants, hex dump logging on deserialization failure, graceful error classification in bridge I/O systems
|
||||
- NpcPlugin system ordering — routine phase transitions now run before pathfinding so PathRequests are picked up same frame
|
||||
- Stale doc comment in observation event generator — system runs before knowledge updates, not after
|
||||
- Clippy warnings from Rust 1.93 — derive Default, is_multiple_of, collapsible if
|
||||
|
||||
### Changed
|
||||
- Hael renamed to Naia Tamm across all wiki files (16 files updated)
|
||||
- Canonical full names applied to 11 single-name NPCs (Voss→Arvo Voss, Devra→Devra Talsen, etc.)
|
||||
- Location shortcodes standardized in monologue guide (hub_m_ → terminal_m_ per D-036)
|
||||
- Entity attributes updated to 16 canonical keys — 4 new role-perspective keys (risk_assessment, loyalty_assessment, position_integrity, moral_weight), secret_held→leverage_held rename
|
||||
- Drin Rosta expanded from Tier 3 to Tier 2 — full 10-axis profile, 6 voice lines, Triangle 2 + Triangle 5 roles
|
||||
- NPC index roster table expanded with Pattern and Motivation columns
|
||||
- Sprint 3 "Know" team briefings regenerated from database — all four files (server, client, joint, copy) now match actual sprint 3 ticket assignments
|
||||
- Ticketing database moved to shared worktree location (`../settledreach.db`) — eliminates binary merge conflicts across branches
|
||||
- All Python connectors use script-relative path resolution instead of `$REPO_ROOT` env var or git rev-parse
|
||||
- `$REPO_ROOT` environment variable removed — all scripts, skills, and docs use relative paths
|
||||
- Merged copy branch — 39 tickets, wiki review + content scoping workshops, game world glossary
|
||||
- Merged maintenance branch — worktree-update skill
|
||||
|
||||
### Added
|
||||
- `make db-backup` / `make db-install` — database backup to `docs/backups/` (main-only) and restore for new clones
|
||||
- Worktree-update skill backs up the shared database after merges on main
|
||||
|
||||
### Removed
|
||||
- `db/commonwealth.db` from git tracking (replaced by shared `../settledreach.db`)
|
||||
- `$REPO_ROOT` env var from all 9 worktree `settings.local.json` files
|
||||
|
||||
### Added
|
||||
- Worktree-update skill — non-destructive branch sync with PR detection and conflict safety
|
||||
- Game world wiki — 45-file glossary covering Sova Transit District: 17 NPCs, 5 triangles, 3 social sites, 7 factions, knowledge vocabulary
|
||||
- Wiki Review workshop (4 rounds + lead interview) — 300-world generator model, cultural ingredients menu, three-system NPC architecture, Sacred/Profane/Middle Kingdom framework
|
||||
- v0.1 Content Scoping workshop (2 rounds + closing) — 16 EntityKnowledge keys, mechanical NPC mapping, YAML content format, 7-verb interaction model, server-authoritative pause, 20 decisions (D-042–D-061)
|
||||
- 39 implementation tickets (#371–#409) from content scoping workshop — copy 21, server 13, client 2, ci 1
|
||||
- Control & Interaction workshop brief (queued)
|
||||
- Large content push team pattern in CLAUDE.md
|
||||
- Inigo sound designer agent — soundscape design, ambient layers, diegetic cues, D-018 audio propagation
|
||||
- Team-agent mapping in sprint planning — each team has defined default agents for briefing assignment
|
||||
- Sprint skills recognize all team branches (server, client, copy, audio, visual, ci)
|
||||
- Sprint 3 "Know" team briefings — server (6 tickets), client (2 tickets), joint (2 split tickets), copy (1 carry-over)
|
||||
- Copy team sprint briefing for Sprint 2 (#368 knowledge vocabulary)
|
||||
- Sprint 2 proof: fog of perception E2E tests (#357) — 3 tests verifying all 7 acceptance criteria through real server pipeline (movement, tiles, fog, wall hiding, corner reveal)
|
||||
- Server proof room — wall at (16,14) between player at (16,16) and NPC at (16,13) for LOS testing
|
||||
- Dynamic test snapshot in SimBridge — tracks player position from queued inputs, Bresenham LOS, Manhattan-distance visibility for standalone demo mode
|
||||
- 4 Bresenham LOS unit tests — clear path, wall blocked, diagonal, same position (PR #13 review)
|
||||
|
||||
### Fixed
|
||||
- Type safety in GameState visible_tiles loop — validates Dictionary with x/y keys before access (PR #11 review)
|
||||
- Consistent reset_test_state() usage across all test files (PR #13 review)
|
||||
- E2E connection loop now detects server process death early (PR #13 review)
|
||||
- Corner reveal test verifies NPC position at (16.5, 13.5) (PR #13 review)
|
||||
- Entity renderer skips redundant modulate.a writes when alpha unchanged (PR #11 review)
|
||||
|
||||
### Added
|
||||
- Observer snapshot knowledge integration (#366) — VisibleEntity carries relationship state (D-033 color) and observation type (Visible/Remembered), remembered entities appear as fog ghosts at last known position
|
||||
- Knowledge graph system (#361, #362, #363, #365) — per-entity KnowledgeGraph component (D-041), StableEntityId + EntityRegistry, KnowledgeEventQueue, decay system, 4-level confidence hierarchy
|
||||
- Direct observation knowledge flow (#364) — perception emits DirectObservation/LeftLOS events to knowledge graph, entities entering/leaving LOS tracked
|
||||
- Protocol v2 decoder — extracts game_time, player_facing, visible_tiles, and per-entity visibility sectors from ObserverSnapshot v2
|
||||
- D-033 entity color palette (#130) — relationship-based colors (teal/green/amber/red), Phase 1 defaults by entity kind
|
||||
- Peripheral vision dimming — entities in peripheral vision rendered at 50% alpha (D-015)
|
||||
- Player facing direction indicator — Polygon2D triangle on player entity showing 8-directional facing
|
||||
- GameState v2 fields — game_time, player_facing, visibility_sectors stored from snapshot data
|
||||
- Test snapshot updated to v2 format with visibility sectors, game_time, and player_facing
|
||||
- Observer visibility query (#112) — replaces unfiltered generate_snapshot with LOS-filtered compute_observer_snapshot combining shadowcasting + vision cone
|
||||
- Vision cone system (#111) — forward/peripheral/blind sectors per D-015, Facing component updated on movement
|
||||
- Symmetric shadowcasting (#110, #359) — Albert Ford algorithm with rational fraction slopes, benchmarked 1.2-10.5x faster than recursive, symmetry guaranteed (D-035)
|
||||
- ObserverSnapshot v2 schema (#358, #25) — version field, GameTime, FacingDirection, VisibleTile, VisibilitySector types, visibility tag on entities
|
||||
- D-035 decision record — symmetric shadowcasting selected over recursive (resolves Q-018)
|
||||
- Tile rendering engine (#129) — programmatic TileSet with floor/wall/door/object placeholders, renders from snapshot tile data
|
||||
- Fog overlay rendering (#131) — three visibility states (visible/fog-edge/hidden) via TileMapLayer overlay
|
||||
- Camera lock to character (#116) — Camera2D smoothing at 2x zoom, locked to player position (D-015)
|
||||
- Test room environment — 8x8 room with corridor and Manhattan-distance visibility for development without server
|
||||
- `ticket team` command and `--team` filter — comma-separated team assignment for tickets (server, client, joint, content)
|
||||
|
||||
### Changed
|
||||
- All instruction files (CLAUDE.md, skills, agent files) now use `$REPO_ROOT` env var instead of `git rev-parse --show-toplevel` — pre-set per worktree via `.claude/settings.local.json`
|
||||
- `start-sprint` skill now requires plan mode — agent must create and get approval for a concrete sprint plan before starting implementation
|
||||
|
||||
### Fixed
|
||||
- Entity renderer protocol field mismatches — "id"→"entity_id", "type"→"kind.variant", "position"→x/y fields now match Protocol.decode_entity() output
|
||||
- Entity centering — entities (24x24) now centered within 32px tiles instead of top-left aligned
|
||||
- `start-sprint` skill uses `git rev-parse --show-toplevel` for worktree-safe absolute paths — fixes "No such file or directory" errors on team branches
|
||||
|
||||
### Changed
|
||||
- Background clear color set to near-black for unexplored areas (was default Godot gray)
|
||||
- Scene render order: Tiles → FogOverlay → Entities (fog covers tiles, entities render on top)
|
||||
- FogOverlay node type changed from Node2D to TileMapLayer for tile-based fog rendering
|
||||
- GameState now stores visible_tiles and visible_positions from snapshots
|
||||
- Test snapshot includes player entity (kind "Player"), second NPC entity, tile data, and visibility data
|
||||
|
||||
### Added
|
||||
- Sprint 2 "See" briefings (server, client, joint) — fog of perception through the bridge
|
||||
- `/plan-sprint` skill — automates sprint planning workflow and briefing file generation
|
||||
- `ticket show` multi-ID support and `--brief` flag for compact human-readable output
|
||||
- Q-018 through Q-023 — 6 open questions from architecture audit (shadowcasting, entity ID stability, collision resolution, tick overflow, pathfinding cache, debug visualization)
|
||||
- 5 architecture spike workshop briefs (knowledge graph, observer pipeline, NPC AI state machines, save/load, map authoring)
|
||||
- 17 tickets from architecture audit (#339-#355) — 7 Sprint 1 tasks, 5 Sprint 2+ tasks, 5 workshop epics
|
||||
- End-to-end connection test (#81) — GDScript test spawning Rust server, connecting via LocalBridge, sending MoveNorth input, verifying player movement in snapshot response (D-030 Layer 3)
|
||||
- Batch input encoding (Vec\<PlayerInput\> wire format) — Protocol.encode_player_inputs() batches all inputs per tick into one framed message matching server expectations
|
||||
- EntityKind::Player fixture — snapshot_player.msgpack for cross-language testing, multi-entity fixture updated to include all 4 entity kinds
|
||||
- 7 new tests (4 batch encoding, 1 framed batch roundtrip, 1 Player fixture decode, 1 E2E connection), 43 total client tests passing
|
||||
- LocalBridge GDScript TCP transport (#79) — 4-byte big-endian length-prefix framing matching Rust server, StreamPeerTCP wrapper with partial read handling
|
||||
- ServerProcess subprocess manager — spawns/stops Rust server via OS.create_process(), auto-cleanup on destruction
|
||||
- SimBridge live transport integration — _process() polling loop for TCP receive/send, connection state machine (DISCONNECTED → CONNECTING → CONNECTED → ERROR)
|
||||
- 8-directional input support — 4 diagonal movement variants (NE, SE, SW, NW) in InputMapper, SimBridge wire mapping, and project.godot input actions
|
||||
- 12 LocalBridge tests (framing roundtrips, cross-layer Protocol+framing, diagonal wire mapping)
|
||||
- 4 diagonal movement cross-language fixtures (Rust → GDScript, D-030 Layer 1)
|
||||
- 33 total client tests passing (up from 20)
|
||||
- TcpBridge transport for Godot client connection — TCP localhost IPC alongside existing Unix socket LocalBridge
|
||||
- Input processing system (process_player_input) — drains InputQueue, converts PlayerActions to MoveIntent components, handles pause/unpause
|
||||
- Snapshot generation system (generate_snapshot) — builds ObserverSnapshot from ECS state with render coordinate conversion
|
||||
- Bridge I/O systems (receive_bridge_inputs, send_bridge_snapshot) — wire bridge to ECS pipeline with graceful disconnect detection
|
||||
- Full game loop in main.rs — TCP accept, tick loop with ServerRunning resource, CLI/env addr config
|
||||
- PlayerCharacter marker component, Player EntityKind variant, SnapshotBuffer resource
|
||||
- E2E game_loop integration test verifying player movement through full pipeline
|
||||
- 8 new tests (3 TCP bridge + 4 input processing + 1 E2E game loop), total 53
|
||||
- Architecture audit framework (docs/audits/) — adversarial two-round review pattern by Tyre + Troblum
|
||||
- Sprint 1 architecture review — full decision + code audit, GREEN architecture, AMBER implementation plan
|
||||
- v0.1 content gap analysis workshop — 6 agents, 2 rounds, 9 content layers, 8 new decisions (D-032 through D-039)
|
||||
- D-032: Separate monologue pools per playable character (hard partition, not filter)
|
||||
- D-033: Entity color represents relationship to player character (asymmetric per character)
|
||||
- D-034: THE FRIEND NPC pattern — production-level emotional centerpiece per character (Kael Davan, Sera Venn)
|
||||
- D-035: Converged tag taxonomy for dialogue/monologue line pools (6 structural + 3 selection tags)
|
||||
- D-036: Sova Transit District / Krenn System as v0.1 setting (first named star system)
|
||||
- D-037: Contraband specification — unlicensed lattice components (moral ambiguity by design)
|
||||
- D-038: Audio in v0.1 scope — 8 AI-generated files via Stable Audio Open
|
||||
- D-039: All 6 wow moments promoted to v0.1 must-have scope
|
||||
- LocalBridge IPC over Unix domain sockets (#78) — length-prefixed MessagePack framing, SimBridge implementation, BridgeResource ECS wrapper
|
||||
- Tile collision system (#236) — TilePosition component, chunk-based WalkabilityMap (D-012), MoveIntent + validate_movement system
|
||||
- TilePosition ↔ f32 render coordinate conversion (to_render_coords, from_render_coords) bridging simulation and wire format
|
||||
- Chunk load/unload support in WalkabilityMap — HashMap<ChunkCoord, ChunkData> with 32x32 tile chunks
|
||||
- 8-directional movement — diagonal PlayerAction variants (MoveNortheast/Northwest/Southeast/Southwest) + TilePosition::all_neighbors()
|
||||
- Entity-entity collision in validate_movement — spatial occupancy check prevents multiple entities on same tile
|
||||
- 25 new tests (4 framing + 2 IPC integration + 17 movement unit + 2 movement integration), total 45
|
||||
- MessagePack serialization for GDScript (ticket #77) — Protocol codec decoding ObserverSnapshot/PlayerInput from Rust wire format, encoding PlayerInput for server
|
||||
- Godot4MessagePack library (pure GDScript) for MessagePack encode/decode
|
||||
- Rust fixture generator (gen_fixtures.rs) producing canonical .msgpack test fixtures with rmp_serde
|
||||
- 8 cross-language protocol tests verifying Rust↔GDScript MessagePack compatibility (D-030 Layer 1)
|
||||
- SimBridge wired to Protocol codec with receive_bytes()/drain_outbound() for transport layer
|
||||
- `db/connectors/ticket` CLI — ergonomic ticket management (list, show, create, assign, sprint, deps, search, epics, children, count) with JSON output
|
||||
- Sprint 1 "Run" created with 8 stories targeting moving character on screen via IPC bridge
|
||||
- Ticket skill rewritten to use ticket CLI instead of raw SQL wrapper scripts
|
||||
- Godot 4 client boilerplate (epic #277) — scenes, autoloads (SimBridge, GameState, InputMapper), rendering stubs, UI shell (HUD, minimap, monologue display), input system with semantic actions
|
||||
- gdUnit4 test framework with 7 tests (2 smoke + 5 D-030 Layer 1 fixture tests for snapshot parsing)
|
||||
- make ci-client pipeline (lint, build, test via gdUnit4 headless runner)
|
||||
- Camera tracking locked to player position (D-015), monologue display wired to snapshot data (D-016)
|
||||
- Gitea tea CLI instructions in CLAUDE.md — non-interactive flags, PR workflow patterns
|
||||
- `/review-pr` skill — dual-agent PR review with Hoshe (code quality) and Tyre (architecture) in parallel, Gitea integration, vendor file exclusion patterns, local merge workflow, heredoc workaround
|
||||
- Automated Rust install via `make setup` (tooling/install-rust script, rustup + clippy + rustfmt)
|
||||
- Automated Godot download/install via `make setup` (tooling/install-godot script, installs to ~/bin/godot4)
|
||||
- InputQueue tick ordering enforcement via debug_assert (determinism guard)
|
||||
- Serialization roundtrip tests for all PlayerAction and EntityKind variants (D-030 Layer 1)
|
||||
- Edge case tests: day wraparound at midnight, day() calculation, out-of-order input rejection
|
||||
- Test runner switched to cargo-nextest (D-030 requirement)
|
||||
- Rust/bevy_ecs simulation server boilerplate (epic 276) — Cargo project, module structure, core ECS types, plugin scaffolding, deterministic simulation resources, test infrastructure
|
||||
- SimulationTime resource with D-031 time system (10 ticks/game-minute, 4 day phases)
|
||||
- SimRng deterministic RNG resource (ChaCha20, seeded for replay)
|
||||
- InputQueue resource for timestamped semantic player actions
|
||||
- ObserverSnapshot and PlayerInput IPC types with MessagePack serialization (D-020)
|
||||
- SimBridge trait abstracting client-server transport
|
||||
- CauseChain production component for information provenance tracking (D-030)
|
||||
- SimulationTier types with LRU eviction support (D-026: Active/Background/StateSaved/Ungenerated)
|
||||
- NPC 10-axis model components (D-024: 7 essential + 3 supporting + CombatCapability)
|
||||
- Server test infrastructure: 11 inline unit tests + 4 integration tests (smoke + serialization round-trips)
|
||||
- make ci-server pipeline verified green (clippy, fmt, build, test)
|
||||
- Round 18 v0.1 gap analysis workshop — 7 agents, 2 rounds, 4 tracks (concept proof, wow factor, missing systems, testability)
|
||||
- D-030: Testability architecture — 8 sub-decisions for ticket #214 (gdUnit4, hybrid Rust testing, CauseChain component, three-layer IPC testing)
|
||||
- D-031: Time system — 10 ticks = 1 game-minute, 4 day phases (Morning/Afternoon/Evening/Night), diegetic clock display
|
||||
- 41 new tickets from gap analysis: 3 epics (Movement & Collision, Observation & Interaction, Game State Management) + 38 stories
|
||||
- 15 priority promotions including 3 tickets to critical (deterministic replay, divergent knowledge, divergent relationships)
|
||||
- 21 new dependency records mapping critical path through collision → pathfinding → NPC movement → routine execution
|
||||
- Workshop directory convention established with per-workshop subdirectories
|
||||
- Project directory scaffold: client/, server/, tooling/, tests/, .config/, .cache/
|
||||
- Top-level Makefile with dev workflow targets (setup, build, run, test, lint, ci, clean)
|
||||
- Whitelistable sqlite-init and sqlite-seed wrapper scripts completing the db/connectors/sqlite-* set
|
||||
- Round 17 content architecture workshop — Full team (8 agents, 3 rounds) defining content pipeline
|
||||
- D-023: Three-tier content model (authored drama modules, templated content, procedural filler) with life-sim substrate
|
||||
- D-024: NPC generation model — 10 axes (7 essential + 3 supporting) with CombatCapability ECS component
|
||||
- D-025: Social site / functional cluster as atomic Tier 2 template unit (4-8 NPCs, 15-40 tiles)
|
||||
- D-026: Simulation tiers with timestamp-based LRU eviction (Active/Background/State-saved/Ungenerated)
|
||||
- D-027: Vertical slice — smuggler + detective two-character proof-of-concept (supersedes D-006)
|
||||
- D-028: Dialogue architecture — tagged line pools with four relational layers (access tiers, history, trust-gating, unprompted disclosure)
|
||||
- D-029: Population entanglement ratio — 30% flat / 50% mundane triangles / 20% intrigue-entangled
|
||||
- Content architecture workshop brief documenting the "life first, drama second" design philosophy
|
||||
- Mellanie (Copywriter) activated from standby for content authoring phase
|
||||
- Round 16 faction development — Full faction framework with lore, political analysis, mechanical grounding (session closed, awaiting team input for v0.1 selection)
|
||||
- 3D reputation system: Trust × Usefulness × Exposure per faction
|
||||
- Faction mechanical grounding: starting loadouts, information asymmetry, blind spots, resource loops
|
||||
- Power dynamics analysis: formal vs. real power distribution across factions
|
||||
- Character drama templates: whistleblower, inspector with conscience, dual-loyalty operative, reluctant conspirator
|
||||
- Betrayal vectors and conspiracy potential for all six factions
|
||||
- "First 30 Minutes" test demonstrating six mechanically distinct faction perspectives
|
||||
- D-021: Official project title "The Settled Reach" confirmed, domain settledreach.com secured
|
||||
- Round 14 worldbuilding — "The Settled Reach" original SF setting foundation with full team reactions
|
||||
- Original terminology established: Settled Reach, Founder Gates, Span Gates, Interstitium, neural lattice, Meridian, imprint, re-embodiment, Perpetuals, the Unbound, Forking, Severance
|
||||
- Four enhancement tiers: Baseline, Augmented, Transcendent, Elevated (plus the Threshold as endgame horizon)
|
||||
- Two-tier death system: soft death (lattice intact) and hard death (lattice destroyed, imprint restore)
|
||||
- Six factions: Concord Assembly, Syndics, Separatists, Guardians of Autonomy/Severance, Veil Institute, Lattice Commission
|
||||
- Infrastructure-as-mystery: Builders inhabiting the Interstitium, Gyre events as leakage
|
||||
- Miri's IP originality guardian role — flags concepts too close to source franchises
|
||||
- Whitelistable wrapper scripts for SQLite and Qdrant connectors (sqlite-query, sqlite-exec, qdrant-search, qdrant-index, qdrant-health, qdrant-count)
|
||||
- Architecture evaluation and risk assessment documents for Godot+Rust bridge approach
|
||||
- Round 13 engine selection discussion — full team debate on engine paradigms
|
||||
- D-020: Engine and architecture selection — Godot 4 client + Rust/bevy_ecs simulation server via subprocess/IPC
|
||||
- Rejected alternatives R-004 through R-010 documented (pure Bevy, pure Godot, GDExtension, C++ GDExtension, Fyrox, custom framework, protobuf)
|
||||
|
||||
### Fixed
|
||||
- create-skill references to nonexistent init_skill.py and package_skill.py scripts
|
||||
- SimBridge wire format: inputs now batch-encoded as Vec\<PlayerInput\> array per server protocol (was sending individual inputs per frame)
|
||||
- SimBridge server args: positional address format ("127.0.0.1:9876") matching server CLI, port default corrected to 9876
|
||||
- Wire format mismatch: LocalBridge now uses rmp_serde::to_vec_named() (named maps) matching client Protocol.gd expectations
|
||||
- EOF on bridge read now returns BridgeError::Transport for disconnect detection instead of empty Vec
|
||||
- WalkabilityMap rewritten from flat Vec to chunk-based HashMap per D-012 architecture (Tyre review)
|
||||
- LocalBridge mutex .unwrap() → .expect() for clearer panic messages (Hoshe review)
|
||||
- Documented 16MB MAX_MESSAGE_SIZE rationale in framing.rs (Hoshe review)
|
||||
- Client input_mapper double-check bug (redundant event.pressed + is_action_pressed)
|
||||
- Bounds validation on snapshot position arrays in game_state.gd and entity_renderer.gd
|
||||
- Deterministic test snapshots (replaced Time.get_ticks_msec() with incrementing counter)
|
||||
- Tween overlap in monologue_display.gd (cancel active tween before creating new one)
|
||||
- SubViewportContainer missing SubViewport child in minimap.tscn
|
||||
- Cargo.toml edition 2024 → 2021 for broader toolchain compatibility
|
||||
- Relationship.target_name: String → target_id: u64 for entity scalability (Tyre review)
|
||||
- DayPhase enum now derives Serialize/Deserialize (consistent with other enums)
|
||||
- Replaced all absolute paths (macOS and Linux) with project-relative paths across round-16 docs
|
||||
- Removed duplicate ROUND-16-STATUS.md from project root (content already in round-16-session-notes.md)
|
||||
- Added relative-path convention to Qatux agent persona for cross-system consistency
|
||||
|
||||
### Removed
|
||||
- dotfiles/tmux.conf (no longer needed)
|
||||
|
||||
### Changed
|
||||
- Q-018 (shadowcasting algorithm selection) resolved via D-035
|
||||
- All 18 agent briefings updated for decisions/ directory split and DEVOPS.md references
|
||||
- Implementation agents (Dudley, Hoshe, Justine, Oscar, Si, Stig, Tyre) now include Development Workflow sections with Makefile targets
|
||||
- Q-009 (time system) resolved via D-031
|
||||
- Ticket catalog expanded from 232 to 273 tickets with sprint sequencing (Run → Feel → Content → Validate)
|
||||
- Ticket skill updated to use wrapper scripts exclusively (no more direct python3 calls)
|
||||
- Miri's role updated from Canon Guardian to Worldbuilder & Setting Designer (original IP pivot)
|
||||
- Project description updated to reflect D-020 engine decision
|
||||
- Connector usage instructions now reference wrapper scripts instead of python3 directly
|
||||
|
||||
@@ -1,41 +1,98 @@
|
||||
# Commonwealth Game Project
|
||||
# The Settled Reach
|
||||
|
||||
A top-down immersive sim — occlusion-based detective game with combat elements, set in an original science fiction universe (Commonwealth-inspired). 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 (D-020).
|
||||
|
||||
**Official Title:** The Settled Reach (D-021)
|
||||
**Repository name:** commonwealth (historical code name, retained for path stability)
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
DECISIONS.md # Confirmed decisions (D-001+), open questions (Q-001+)
|
||||
DISCUSSION.md # Active discussion round only
|
||||
TEAM.md # Team roster and roles
|
||||
client/ # Godot 4 client (D-020)
|
||||
server/ # Rust/bevy_ecs simulation server (D-020)
|
||||
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/ # Archived discussion rounds (historical)
|
||||
discussions/ # Discussion rounds (all rounds archived here per D-022)
|
||||
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/
|
||||
commonwealth.db # SQLite ticketing database
|
||||
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
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
## Agent Instructions
|
||||
|
||||
### Worktree 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.
|
||||
|
||||
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.
|
||||
|
||||
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.**
|
||||
|
||||
- All file paths are relative to the worktree/git root (e.g. `server/src/bridge/types.rs`, `client/scripts/rendering/fog.gd`).
|
||||
- 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
|
||||
|
||||
The ticketing database (`settledreach.db`) lives in the **parent directory** shared across all worktrees — it is not tracked in git. A backup is committed to `docs/backups/settledreach.db.backup` via main only.
|
||||
|
||||
### Before starting work
|
||||
1. Read your briefing at `docs/briefings/{your-name}.md` for current project context
|
||||
2. Read `DECISIONS.md` for confirmed decisions relevant to your work
|
||||
3. Check `DISCUSSION.md` for any active discussion
|
||||
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
|
||||
3. Read the relevant `decisions/*.md` domain file(s) referenced in the briefing
|
||||
4. Background context: `docs/briefings/{your-name}.md`, `docs/discussions/`
|
||||
|
||||
### SQLite access
|
||||
**Never use the `sqlite3` CLI** - it crashes in Claude Code due to a known std::bad_alloc bug.
|
||||
### Ticket and database access
|
||||
**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
|
||||
```
|
||||
|
||||
Use the whitelistable wrapper scripts:
|
||||
### Sprint CLI
|
||||
**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:
|
||||
```bash
|
||||
db/connectors/sqlite-query "SELECT * FROM tickets WHERE status='in_progress'"
|
||||
db/connectors/sqlite-exec "UPDATE tickets SET status='done' WHERE id=1"
|
||||
@@ -49,17 +106,69 @@ db/connectors/qdrant-health
|
||||
db/connectors/qdrant-count
|
||||
```
|
||||
|
||||
### 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)
|
||||
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
|
||||
- Decisions: domain files in `decisions/` (see `decisions/README.md` for index)
|
||||
- Decision IDs: `D-NNN` (confirmed), `Q-NNN` (open questions), `R-NNN` (rejected)
|
||||
- Discussion rounds: numbered sequentially, archived to `docs/discussions/` when complete
|
||||
- Briefings: one per agent, updated after decision-producing rounds
|
||||
- Tickets: managed via `/ticket` skill or `sqlite_connector.py` directly
|
||||
- Tickets: managed via `db/connectors/ticket` CLI or `/ticket` skill
|
||||
|
||||
### Commit conventions
|
||||
Use conventional commits with Commonwealth-specific scopes:
|
||||
`agents`, `skills`, `docs`, `briefings`, `discussions`, `schema`, `db`, `config`, `engine`, `simulation`, `client`, `ui`, `audio`, `assets`
|
||||
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)
|
||||
|
||||
+14
-371
@@ -1,374 +1,17 @@
|
||||
# Commonwealth Game - Decisions Log
|
||||
# Decisions Archive
|
||||
|
||||
This document tracks confirmed decisions, open questions, and rejected alternatives.
|
||||
Decisions have been reorganized by domain. See:
|
||||
- [decisions/README.md](decisions/README.md) for the domain index
|
||||
- Individual domain files in `decisions/`
|
||||
|
||||
---
|
||||
| File | Domain | Count |
|
||||
|------|--------|-------|
|
||||
| [architecture.md](decisions/architecture.md) | Technical foundation | 8 |
|
||||
| [perception.md](decisions/perception.md) | Player observation | 6 |
|
||||
| [content.md](decisions/content.md) | NPC, dialogue, templates | 5 |
|
||||
| [scope.md](decisions/scope.md) | Game concept, prototype | 9 |
|
||||
| [process.md](decisions/process.md) | Team, workflow | 3 |
|
||||
| [questions.md](decisions/questions.md) | Open questions | 11 |
|
||||
| [rejected.md](decisions/rejected.md) | Rejected alternatives | 10 |
|
||||
|
||||
## CONFIRMED DECISIONS
|
||||
|
||||
### D-001: Build a custom game, not a mod
|
||||
- **Date:** 2026-02-08
|
||||
- **Decision:** We are building a standalone game, not a Stellaris mod.
|
||||
- **Rationale:** No existing game provides the right combination of character-driven dynasty play, wormhole-centric space map, and deep internal politics that the Commonwealth universe demands. Stellaris gets the map right but characters wrong. CK3 gets characters right but the map wrong. Neither supports wormhole-as-infrastructure as a core mechanic.
|
||||
- **Raised by:** Team Leader (Jeroen), after team debate across Rounds 1-3.
|
||||
- **Dissent:** None. Team unanimously supports after discussion.
|
||||
|
||||
### D-002: SUPERSEDED by D-005
|
||||
|
||||
### D-003: Commonwealth is the first campaign, not the only possible one
|
||||
- **Date:** 2026-02-08
|
||||
- **Decision:** Build a character-driven space grand strategy *framework/engine*, with the Commonwealth universe as the first campaign/scenario.
|
||||
- **Rationale:** Avoids locking into one IP. The framework has broader value. The Commonwealth provides a rich, opinionated first use case that forces good design decisions.
|
||||
- **Raised by:** Gore (Transhumanist Fan), endorsed by team.
|
||||
|
||||
### D-005: Core concept - single character, first-person, story-generator
|
||||
- **Date:** 2026-02-08
|
||||
- **Decision:** The game is a first-person, single-character experience. You select a character at game start and play from their perspective. The world is a rich simulation experienced through one person's keyhole view.
|
||||
- **Supersedes:** D-002 (dynasty grand strategy concept)
|
||||
- **Elevator pitch:** "Pick a character. Step into the Commonwealth. Figure it out."
|
||||
- **Key pillars:**
|
||||
- **Single character selection** - who you pick determines where you start, what you know, what you can do, and what you care about
|
||||
- **Asymmetric information as core mechanic** - you only know what your character knows. Others lie, withhold, reveal. The same world-state looks completely different from different characters
|
||||
- **Rimworld-style story generator** - an AI storyteller paces events for dramatic tension. Structural randomness (who's compromised, where evidence is) set at game start. Dramatic randomness (when things happen) driven by pacing
|
||||
- **Wormhole network** as traversable infrastructure - you physically move through wormholes between locations
|
||||
- **Crisis events emerge from simulation** - not timers, not scripted, but consequences
|
||||
- **Replayability through perspective** - same conspiracy, different character, completely different game
|
||||
- **Design references:** Rimworld (storyteller/systems), Disco Elysium (character-driven investigation), Sunless Skies (atmosphere/travel), King of Dragon Pass (event-driven decisions)
|
||||
- **Raised by:** Team Leader (Jeroen), evolved through Rounds 4-6 with full team input.
|
||||
- **Dissent:** None. Team unanimously energized by the pivot.
|
||||
|
||||
### D-006: Prototype scenario - Institute / Armstrong City / Guardians
|
||||
- **Date:** 2026-02-08
|
||||
- **Decision:** The first playable prototype focuses on the Starflyer conspiracy arc, scoped to the investigative/political hub: the Institute, Armstrong City, and the Guardians of Selfhood. This tests the core mechanic (asymmetric information in a conspiracy) with controllable scope.
|
||||
- **Rationale:** The conspiracy arc IS asymmetric information. If it's not compelling at this scale, a galaxy won't fix it. If it works, everything else is expansion.
|
||||
- **Raised by:** Team Leader (Jeroen).
|
||||
|
||||
### D-007: Five pillars of game design
|
||||
- **Date:** 2026-02-08
|
||||
- **Decision:** The game rests on five core design pillars. All systems must serve at least one.
|
||||
- **Pillars:**
|
||||
1. **Characters & Information** - who you are, what you know, who you know. Asymmetric information is the master mechanic.
|
||||
2. **The Wormhole Network** - where you go, how you move through the world. Physical traversal of infrastructure.
|
||||
3. **Society & Factions** - the political landscape you navigate from inside, not above.
|
||||
4. **Crisis & Storyteller** - what the world throws at you and when. Rimworld-style AI pacing with structural + dramatic randomness.
|
||||
5. **Action & Spectacle** - what happens when it all goes loud. The punctuation, not the sentence.
|
||||
- **Raised by:** Gestalt (pillars 1-4), Team Leader (pillar 5, citing Rimworld's shooter pedigree).
|
||||
|
||||
### D-008: Action pillar design principles
|
||||
- **Date:** 2026-02-08
|
||||
- **Decision:** The action/combat system follows these principles:
|
||||
- **Rimworld/XCOM hybrid** - simple mechanics, stats-driven. Simple gets complicated fast on its own.
|
||||
- **Z-levels** - floors, verticality. Not 3D rendering, but 3D space. Fights across floors of a building.
|
||||
- **Perception-bounded** - you see/hear what your character can. Rest is abstracted/occluded. Same principle as information asymmetry applied to physical space.
|
||||
- **LOD/occlusion** - simulation reduces outside player view. Both a design principle and performance optimization.
|
||||
- **Large, varied maps** - locations should feel big. Achieved through procedural generation (templates + procedural flesh), trickery, or both. Procedural layouts also feed replayability.
|
||||
- **Multiple maps** - different locations are separate maps. Step through a wormhole, load a new place.
|
||||
- **Wildly asymmetric encounters** - balanced fights are the exception. Power mismatches in both directions are the norm and the source of tension.
|
||||
- **Hubris wall as design principle** - let the player feel powerful, then recontextualize their power level. Not cheap difficulty spikes - genuine "you were playing a smaller game than you thought" moments. The books are not always balanced; the fun comes when a character hits their hubris wall.
|
||||
- **Death = information loss, not game over** - memory cell backup system means death costs you everything since your last backup. Lost knowledge, lost relationships, lost deals. The storyteller knows what you've lost and can exploit it.
|
||||
- **Scales with ascension** - baseline human fights with weapons, Higher fights with biononics, ANA-connected fights with something else entirely. The pillar transforms as the character evolves.
|
||||
- **Design references:** Rimworld (real-time, simple systems, personal stakes), XCOM (tactical asymmetry, pod activation moments), Dwarf Fortress (z-levels, simulation depth)
|
||||
- **Raised by:** Team Leader (Jeroen), with input from full team.
|
||||
|
||||
### D-009: Multiplayer - design for it, build single-player first
|
||||
- **Date:** 2026-02-08
|
||||
- **Decision:** Single-player is the build target. Multiplayer is designed into the architecture from day one so it can be added without rewriting the game.
|
||||
- **Rationale:** Two players experiencing the same conspiracy from different keyholes (Senate insider + Guardian operative) is a killer feature. But building multiplayer too early kills projects. The compromise: architectural decisions now that make multiplayer a "add networking" problem later, not a "rewrite everything" problem. Team Leader flagged that bolting multiplayer on after the fact is one of the hardest things to do - so the architecture must be honest about this from the start.
|
||||
- **Cost:** ~15-20% slower single-player development due to required abstractions. Accepted as cheap insurance.
|
||||
- **Raised by:** Full team discussion. Tyre led technical framing, Team Leader insisted on architectural honesty.
|
||||
|
||||
### D-010: Multiplayer-ready architectural baseline
|
||||
- **Date:** 2026-02-08
|
||||
- **Decision:** Four non-negotiable architectural principles that must be present from the first line of code:
|
||||
1. **Client-server separation** - even in single-player. The game simulation runs as a "server," the player view is a "client." Single-player = local client + local server. This is the single decision that makes or breaks retrofitting multiplayer.
|
||||
2. **Information boundaries as a first-class system** - every piece of game state is tagged with who knows it. Not fog-of-war bolted on - the engine fundamentally thinks in terms of "what does this observer have access to." Required for single-player asymmetric information anyway. Multiplayer just means multiple observers.
|
||||
3. **No baking player identity into the game loop** - the simulation doesn't know there's "the player." It knows there are characters, some of which are player-controlled. Adding a second player-controlled character should be a configuration change, not a rewrite.
|
||||
4. **Deterministic simulation with input events** - game state advances based on timestamped actions, not "whatever the local machine calculated." Enables synchronization later without rewriting the simulation.
|
||||
- **Side benefits (Nigel's observation):** Every one of these makes single-player better too. Information boundaries make NPC AI smarter about what they know. Client-server makes save/load cleaner. Deterministic simulation makes debugging easier. No sacrifice.
|
||||
- **Engine implication:** Client-server friendliness is now a hard requirement on the engine shortlist (see Q-001).
|
||||
- **Raised by:** Tyre (Technical Architect), endorsed by Team Leader as "sound architectural baseline."
|
||||
|
||||
### D-011: Fog of perception is non-negotiable (Pillar 1 infrastructure)
|
||||
- **Date:** 2026-02-09
|
||||
- **Decision:** Fog of perception is not a feature - it's the minimum requirement for information asymmetry to work with a top-down camera. Without it, the player is omniscient within their viewport and the entire information pillar collapses at the local map level. First-person camera gets this for free; top-down must enforce it.
|
||||
- **Implementation:** Line-of-sight based (shadowcasting), not simple radius. Walls block vision, buildings are opaque, corners create blind spots, z-levels interact with sightlines.
|
||||
- **Sound as secondary information channel:** Events outside LOS can be heard - footsteps, conversations, gunfire, alarms. Sound gives partial/directional information, prompting decisions based on incomplete data.
|
||||
- **Applies to ALL entities:** NPCs use the same LOS/perception system as the player. An NPC who can't see you doesn't know you're there. NPCs have memory and inference (saw you enter a building → knows you're inside). Ties to D-010 principle 2: information boundaries are universal, the player's isn't special.
|
||||
- **Fog returns when you leave:** Previously explored areas revert to fog over time. Information about locations decays. What you saw at the docks yesterday may not be true today.
|
||||
- **v0.1 scope:** 2D shadowcast per z-level + sound propagation. Vertical LOS (looking between floors) deferred.
|
||||
- **Rationale:** Team Leader identified overnight that top-down without fog is a fundamental gap in the information asymmetry design.
|
||||
- **Raised by:** Team Leader (Jeroen), with technical framing by Tyre and Gestalt.
|
||||
|
||||
### D-012: Chunk-based map architecture for future borderless generation
|
||||
- **Date:** 2026-02-09
|
||||
- **Decision:** Maps use chunk-based generation and loading from day one. Chunks load/unload around the player. A bounded map is "only generate chunks within this boundary." Removing the boundary later to enable Minecraft-style borderless generation is a configuration change, not a rewrite.
|
||||
- **v0.1:** Bounded ~150x150 per world, 2-3 z-levels, chunk-based internally.
|
||||
- **Future:** Borderless generation. The world generates as you explore. The map can never be "solved" by walking to every corner. New areas develop, existing areas change.
|
||||
- **Rationale:** Same principle as D-010 (multiplayer architecture) - design for the future, build the simpler version now.
|
||||
- **Raised by:** Tyre (Technical Architect), endorsed by Team Leader.
|
||||
|
||||
### D-013: Diegetic insert/POI navigation system
|
||||
- **Date:** 2026-02-09
|
||||
- **Decision:** The player's map interface is diegetic - it IS the character's neural insert (Commonwealth technology). Not a game UI bolted on, but the character literally checking their implant's navigation overlay. Points of interest appear on the map only when learned through gameplay.
|
||||
- **Canon basis (Miri):** Commonwealth citizens have inserts connecting them to the unisphere. Built-in navigation, messaging, data access. Characters in the books constantly check inserts for directions, news, contacts.
|
||||
- **How POIs are learned:**
|
||||
- Character background (starting knowledge based on who you are)
|
||||
- NPC interactions (contacts send locations, tips, "meet me here" pins)
|
||||
- Research (unisphere searches, case files, institutional databases)
|
||||
- Physical discovery (finding something while exploring)
|
||||
- **Different characters see different maps:** A cop sees flagged locations from case files. A Guardian sees safehouses and dead drops. A Senator sees political venues. Same city, different POI layer.
|
||||
- **POIs can be manipulated:** Tips can be traps. Locations can be outdated. Information channels through the insert system carry the same trust/deception dynamics as verbal information.
|
||||
- **Anti-Ubisoft:** No "climb tower to reveal icons." Map fills at the pace of investigation. Early game: sparse, frightening. Late game: dense, and you realize how much you missed.
|
||||
- **Anchoring by desire, not force:** In a borderless world, the POI/insert system gives players reasons to go to specific places without funneling them. Navigation is pulled by player intent, not pushed by map design.
|
||||
- **Multiplayer implication:** Players can share/send POIs via inserts. In co-op: collaborative conspiracy boards. In adversarial: feeding false locations.
|
||||
- **Raised by:** Team Leader (Jeroen) proposed borderless + anchoring concept. Miri confirmed canon basis. Full team contributed mechanics.
|
||||
|
||||
### D-015: Camera locked to character, rotation as future option
|
||||
- **Date:** 2026-02-09
|
||||
- **Decision:** Camera is locked to the character at all times. No panning. Optionally rotates to character facing direction (player option, later version).
|
||||
- **Rationale:** Pannable camera breaks the information model - you become a surveillance drone, not a character. Locked camera reinforces "you ARE this person." Rotation with facing direction restores directional audio mapping (binaural becomes viable again) and creates a natural vision cone (front = detailed, peripheral = reduced, behind = blind).
|
||||
- **Vision cone model:**
|
||||
- Forward: full LOS, full detail
|
||||
- Peripheral: reduced range, dimmer
|
||||
- Behind: fog / blind spot. You can be snuck up on.
|
||||
- **Reference:** Hotline Miami's camera made that game terrifying with the same principle.
|
||||
- **v0.1:** Locked camera, no rotation. Vision cone still works on fixed-north map. Rotation deferred as player option.
|
||||
- **Raised by:** Team Leader (Jeroen).
|
||||
|
||||
### D-019: Top-down confirmed as primary camera, 3D cutscenes for key moments
|
||||
- **Date:** 2026-02-09
|
||||
- **Decision:** Top-down is the gameplay camera. Final. 3D cutscenes can be used for significant narrative moments (wormhole traversal, Dyson barrier opening, first contact, major reveals).
|
||||
- **Rationale after full honest review:**
|
||||
- What first-person would give us (visceral traversal, face-to-face conversations, natural asymmetry, spatial horror) is real but compensated by: vision cone + fog, internal monologue, perception mode overlays, sound model
|
||||
- What top-down gives us that first-person can't: multi-layer information observation, tactical combat clarity, system legibility, NPC simulation visibility, strategic UI coexistence, 5-10x faster prototype
|
||||
- Key insight (Gestalt): the fun is systems interacting - observation → insert check → thermal → monologue → mental note → exploitation. That sequence is BETTER top-down.
|
||||
- Key insight (Paula): monologue interpreting faces/conversations is arguably richer than player reading 3D faces, and more faithful to Hamilton's close-POV writing style
|
||||
- 3D cutscenes recapture the visceral moments without burdening gameplay engineering. They're decoupled, can be added as polish, game ships complete without them.
|
||||
- Camera change to 3D IS the dramatic signal - player knows something significant is happening (Gestalt)
|
||||
- **Architecture note (Tyre):** Client-server separation means the renderer is swappable. A full first-person client is architecturally possible in the future. Top-down now doesn't mean top-down forever.
|
||||
- **v0.1:** Top-down only. No cutscenes. Those are milestone features.
|
||||
- **Raised by:** Team Leader (Jeroen), after full team review of tradeoffs in Round 12.
|
||||
|
||||
### D-016: Internal monologue as core perception/atmosphere system
|
||||
- **Date:** 2026-02-09
|
||||
- **Decision:** The player character has an internal monologue that narrates sensory information the camera can't show, creates atmosphere, provides diegetic hints, and can be an unreliable narrator.
|
||||
- **Functions:**
|
||||
- **Perception bridge:** Translates non-visual senses into character voice. *"Footsteps behind me. Two people, unhurried."*
|
||||
- **Atmosphere:** Character's running commentary on environment, mood, situation.
|
||||
- **Diegetic tutorial:** Character thinks about what they might do. *"That terminal might have access logs."* No "press X" popups.
|
||||
- **Unreliable narrator:** Monologue is character's INTERPRETATION, not ground truth. Can be wrong. *"Seems quiet. Safe to move."* (It wasn't safe.)
|
||||
- **Character voice:** Varies by character background, mood, knowledge. Paranoid Guardian vs confident Senator walking the same street = different monologue = different experience.
|
||||
- **Production note (Nigel):** Cheapest feature on the list - it's text. Thousands of contextual lines, AI-assistable generation, character-specific variants.
|
||||
- **Raised by:** Emerged from team discussion. Tyre proposed text/log as sound option, team recognized broader potential.
|
||||
|
||||
### D-017: Perception modes as character-build system
|
||||
- **Date:** 2026-02-09
|
||||
- **Decision:** The fog/vision system supports multiple perception modes that vary by character build, equipment, and ascension level. Each mode reveals different information with different trust/quality tradeoffs.
|
||||
- **Confirmed modes:**
|
||||
- **Natural vision** - detail, color, identity. Blocked by walls. Everyone has it.
|
||||
- **Thermal** - heat signatures through walls. Body count but no identity. Higher biononic / military gear.
|
||||
- **Camera feeds** - remote visual from fixed positions. Only where cameras exist. Feed can be spoofed/looped. Hacker, institutional access, insert exploit.
|
||||
- **Unisphere tracking** - location pings of known individuals. Only active inserts, can be masked. Law enforcement / intelligence access.
|
||||
- **Audio analysis** - sound signatures, direction, classification. No visual. Insert processing, trainable skill.
|
||||
- **Ascension scaling:**
|
||||
- Baseline: natural vision + carried gear
|
||||
- Enhanced: insert-based modes, camera access, audio processing
|
||||
- Higher: biononic thermal, enhanced spectrum, passive scanning
|
||||
- ANA-touched: pattern recognition across all feeds, predictive awareness
|
||||
- **Playstyle implications (Nigel):** Low-tech Guardian playthrough = survival horror (blind, relying on contacts and paranoia). Senator playthrough = information overload (cameras and tracking but drowning in data). Perception modes are playstyle selectors.
|
||||
- **Canon check (Miri):** All modes exist in the books. Higher biononics include enhanced senses. Unisphere connects everything. Security systems are hackable. Guardians actively spoof and evade. Starflyer agents defeat all sensor modes.
|
||||
- **v0.1:** Natural vision cone + basic audio indicators + insert minimap only. Additional modes are milestone features, each self-contained and modular.
|
||||
- **Engine implication:** Each perception mode is an observer query against the information boundary system (D-010 principle 2). Engine doesn't distinguish between eyes/thermal/camera - all are "given this sensor, what state is visible?"
|
||||
- **Raised by:** Team Leader (Jeroen) proposed thermal and camera hacking. Full team developed into perception mode framework.
|
||||
|
||||
### D-018: Three-range sound model
|
||||
- **Date:** 2026-02-09
|
||||
- **Decision:** Sound information reaches the player through three ranges with decreasing accuracy and trust:
|
||||
|
||||
| Range | Method | Info quality | Trust level |
|
||||
|-------|--------|-------------|-------------|
|
||||
| Close (near/in LOS) | Stereo audio (maps to facing direction with camera rotation) | High accuracy, identity possible | Raw sensory, reliable |
|
||||
| Medium (outside LOS, nearby) | Visual indicators at fog edge + internal monologue | Directional, imprecise, type classification | Sensory impression, reliable but vague |
|
||||
| Long (across map) | Insert notifications, text alerts | Specific but delayed, location data | Network-dependent, spoofable, manipulable |
|
||||
|
||||
- **Key insight:** Each range is a different information QUALITY, not just distance. Close = trustworthy. Long = potentially compromised. The Starflyer's agents would absolutely spoof long-range feeds.
|
||||
- **v0.1:** Screen-space stereo for close + visual fog-edge indicators for medium. Long-range insert alerts as stretch goal.
|
||||
- **Raised by:** Full team discussion.
|
||||
|
||||
### D-014: v0.1 map specification
|
||||
- **Date:** 2026-02-09
|
||||
- **Decision:** First playable tech demo map spec:
|
||||
|
||||
| Layer | Spec |
|
||||
|-------|------|
|
||||
| World map | 3 worlds (Hub/Capital/Fringe), node graph with wormhole connections |
|
||||
| Local maps | 1 per world, ~150x150 tiles, 2-3 z-levels, chunk-based |
|
||||
| Key locations | 5-8 hand-crafted buildings/rooms per world embedded in procedural space |
|
||||
| Ambient fill | Procedural from templates (district types vary per world) |
|
||||
| Wormhole gates | Physical locations on local map, observable/traversable, social chokepoints |
|
||||
| Fog | LOS-based shadowcasting with vision cone (forward/peripheral/blind), fog returns on departure |
|
||||
| Sound | Stereo close-range + visual fog-edge indicators for medium range |
|
||||
| Navigation | Diegetic insert minimap - dots when close, border arrows for known distant POIs |
|
||||
| Camera | Locked to character, no panning, no rotation in v0.1 |
|
||||
| Monologue | Internal character voice for non-visual perception, atmosphere, diegetic hints |
|
||||
| Perception | Natural vision + basic audio only. Thermal/cameras/tracking deferred to milestones |
|
||||
| NPCs | ~15 total across all worlds, on schedules, moving between locations |
|
||||
| Art | Functional boxes with labels. Atmosphere carried by monologue and interaction models. |
|
||||
|
||||
- **Raised by:** Full team across Rounds 8-10.
|
||||
|
||||
### D-020: Engine and architecture selection — Godot client + Rust simulation via subprocess/IPC
|
||||
- **Date:** 2026-02-09
|
||||
- **Decision:** The game uses a split architecture: **Godot 4** (GDScript) as the rendering client, **Rust** with **bevy_ecs standalone** as the simulation server. The two communicate via **subprocess/IPC** (local socket for single-player, TCP for multiplayer). **NOT via GDExtension.**
|
||||
- **Architecture:**
|
||||
- The Rust simulation is a standalone binary with zero Godot dependencies. It runs the ECS world, perception queries, AI, storyteller, combat — all game logic.
|
||||
- The Godot client is a pure renderer: receives `ObserverSnapshot` data, draws tiles/sprites/fog, plays audio, shows UI, captures input. No game logic in GDScript.
|
||||
- Single-player: Godot launches the Rust binary as a child process. Local Unix socket or localhost TCP.
|
||||
- Multiplayer: Godot connects to a remote Rust server. Same protocol. The simulation binary doesn't know the difference.
|
||||
- This IS the D-010 client-server architecture — literally, not simulated.
|
||||
- **Serialization:**
|
||||
- **MessagePack** for all client-facing communication (Rust↔Godot). Dynamic structure supports variable HUD composition driven by perception modes (D-017). Cross-language, debuggable.
|
||||
- **bincode** reserved for future Rust↔Rust server-to-server sync (same binary, hot path, zero overhead).
|
||||
- **protobuf** rejected — solves deployment/versioning problems we don't have, poor GDScript support.
|
||||
- **Why subprocess over GDExtension:**
|
||||
- Eliminates entire risk categories: gdext pre-1.0 API churn, Godot version ABI breakage, FFI thread safety (`Gd<T>` is `!Send`), cross-boundary memory management.
|
||||
- Decouples learning: build and test Rust simulation standalone, build Godot renderer standalone, connect when both work.
|
||||
- Maps directly to D-010 client-server with no simulation — it IS client-server from day one.
|
||||
- Either side can be upgraded, replaced, or scaled independently.
|
||||
- Cost: ~1-5ms serialization latency per tick. Acceptable for a detective/strategy game, not a twitch shooter.
|
||||
- **Key patterns:**
|
||||
- `ObserverSnapshot`: the only data structure crossing the boundary. Contains visible entities, fog state, sound events, monologue triggers, HUD widget data. Variable shape per character build.
|
||||
- `PlayerInput`: semantic actions (MoveNorth, Interact, UsePerceptionMode), not raw key events. Timestamped for deterministic processing.
|
||||
- `SimBridge` trait: abstracts transport. `LocalBridge` (subprocess, channels) and `NetworkBridge` (TCP, MessagePack) implement the same interface.
|
||||
- **Kill switch:** If no working prototype (character + fog + one NPC) exists by week 8 of development, pivot to pure Godot. If bridge/sync code exceeds game logic for 3 consecutive sprints, the architecture tax is too high.
|
||||
- **Development sequence:**
|
||||
1. Build Rust simulation as standalone binary (testable via terminal/logs)
|
||||
2. Build Godot renderer as standalone project (hardcoded test data)
|
||||
3. Connect via MessagePack protocol
|
||||
- **Project structure:**
|
||||
```
|
||||
simulation/ # Pure Rust, bevy_ecs, zero Godot deps
|
||||
client/ # Godot 4 project, GDScript only
|
||||
protocol/ # Shared message definitions (MessagePack schemas)
|
||||
```
|
||||
- **Evaluation reports:** `docs/architecture/eval-godot-rust-bridge.md` (Tyre), `docs/architecture/risk-godot-rust-bridge.md` (Troblum)
|
||||
- **Raised by:** Team Leader (Jeroen) proposed Godot client + Rust backend. Tyre designed architecture. Troblum's risk assessment shifted integration from GDExtension to subprocess/IPC. Full team endorsed.
|
||||
- **Dissent:** None. Troblum's CRITICAL risk flags on GDExtension were accepted; subprocess approach addresses them.
|
||||
|
||||
### D-004: Team composition confirmed
|
||||
- **Date:** 2026-02-08
|
||||
- **Decision:** Team of 8 agents + Team Leader.
|
||||
|
||||
| Agent | Role |
|
||||
|-------|------|
|
||||
| MIRI | Lore Expert & Canon Guardian |
|
||||
| OZZIE | Player Experience / "Wow Factor" Advocate |
|
||||
| PAULA | Narrative & Political Depth |
|
||||
| GORE | Themes & Endgame Design |
|
||||
| GESTALT | Systems Design & Fun Factor |
|
||||
| NIGEL | Sandbox & Replayability |
|
||||
| TYRE | Technical Architecture & Feasibility |
|
||||
| SCRIBE | Documenter |
|
||||
| Jeroen | Team Leader, final decisions |
|
||||
|
||||
---
|
||||
|
||||
## OPEN QUESTIONS
|
||||
|
||||
### Q-001: Game engine selection
|
||||
- **Status:** Resolved → D-020
|
||||
|
||||
### Q-002: Scope of v0.1 playable prototype
|
||||
- **Status:** Map spec resolved (D-014). Remaining: mechanics, characters, interactions for minimum playable build.
|
||||
- **Assigned to:** Full team
|
||||
|
||||
### Q-003: Art direction / presentation style
|
||||
- **Status:** Partially resolved for v0.1 (D-014: boxes with labels, atmosphere via dialog/interaction)
|
||||
- **Remaining:** Long-term art direction beyond tech demo.
|
||||
- **Assigned to:** TBD
|
||||
|
||||
### Q-004: One campaign spanning all eras or separate era scenarios?
|
||||
- **Status:** Not yet discussed
|
||||
- **Context:** Gore raised that Commonwealth Era and Void Era play very differently. Prototype focuses on pre-Starflyer War era.
|
||||
- **Assigned to:** Gore, Miri to lead discussion
|
||||
|
||||
### Q-005: Scale for prototype - locations, characters, factions
|
||||
- **Status:** Partially scoped
|
||||
- **Early signal:** Institute/Armstrong City hub, ~10-20 characters, Guardians + institutional + political factions
|
||||
- **Assigned to:** Gestalt, Tyre, Miri
|
||||
|
||||
### Q-006: Multiplayer or single-player only?
|
||||
- **Status:** Resolved → D-009
|
||||
|
||||
### Q-007: Target platform(s)
|
||||
- **Status:** Not yet discussed
|
||||
- **Context:** Team Leader has Linux background (Fedora). Cross-platform considerations?
|
||||
- **Assigned to:** Tyre
|
||||
|
||||
### Q-008: Licensing / distribution model
|
||||
- **Status:** Not yet discussed
|
||||
- **Question:** Open source? Free? Commercial? This affects engine choice and asset decisions.
|
||||
- **Assigned to:** Team Leader
|
||||
|
||||
### Q-009: Time system
|
||||
- **Status:** Not yet discussed
|
||||
- **Question:** How does time flow? Real-time with pause? Turn-based (days/weeks/months)? How do we handle deep time / decades passing?
|
||||
- **Assigned to:** Gestalt, Gore
|
||||
|
||||
### Q-010: Storyteller AI design
|
||||
- **Status:** Not yet discussed
|
||||
- **Question:** How does the Rimworld-style storyteller work? What are the pacing rules? How much structural randomness vs dramatic randomness?
|
||||
- **Assigned to:** Gestalt, Nigel
|
||||
|
||||
### Q-011: Character selection and playable characters
|
||||
- **Status:** Not yet discussed
|
||||
- **Question:** Which characters are playable in the prototype? How different are their starting positions? Can you play canon characters or only original ones?
|
||||
- **Assigned to:** Miri, Paula
|
||||
|
||||
---
|
||||
|
||||
## REJECTED ALTERNATIVES
|
||||
|
||||
### R-001: Stellaris mod
|
||||
- **Rejected:** 2026-02-08
|
||||
- **Reason:** Character system too shallow, multi-empire assumption conflicts with Commonwealth's single-civilization focus, wormhole-as-infrastructure not achievable within Stellaris modding. Team Leader's experience with Star Trek: New Horizons confirmed that even well-suited IPs struggle with character connection in Stellaris.
|
||||
|
||||
### R-002: CK3 total conversion
|
||||
- **Rejected:** 2026-02-08
|
||||
- **Reason:** Would require building a space map from scratch within CK3's framework - essentially building a game inside a game. The map system fundamentally doesn't support the complexity needed.
|
||||
|
||||
### R-003: Other existing games (Distant Worlds 2, GalCiv IV, Sins of a Solar Empire II, Victoria 3)
|
||||
- **Rejected:** 2026-02-08
|
||||
- **Reason:** Each captures at most 40% of what's needed. Smaller modding communities, less mature tools, and none solve the core CK3+Stellaris hybrid requirement.
|
||||
|
||||
### R-004: Pure Bevy (Rust) — no Godot
|
||||
- **Rejected:** 2026-02-09
|
||||
- **Reason:** No visual editor (level design is code-only). UI framework in flux. API breaks significantly between versions. For a solo developer who needs visual tools for hand-crafted buildings (D-014), Godot's editor is a massive productivity advantage. Bevy's ECS is used — just not its renderer.
|
||||
|
||||
### R-005: Pure Godot (GDScript or C#)
|
||||
- **Rejected:** 2026-02-09
|
||||
- **Reason:** Scene tree paradigm fights ECS-style simulation. Perception queries (D-017) and information boundaries (D-010) map naturally to ECS component queries, not scene tree traversal. Multi-core scaling for expanded content impossible in GDScript. Kept as kill-switch fallback if Rust architecture exceeds time budget.
|
||||
|
||||
### R-006: Godot + Rust via GDExtension
|
||||
- **Rejected:** 2026-02-09
|
||||
- **Reason:** gdext is pre-1.0 with breaking API changes. Godot version upgrades break GDExtension ABI. Thread safety at FFI boundary is CRITICAL risk (`Gd<T>` is `!Send`/`!Sync`). Couples Rust and Godot learning curves. Subprocess/IPC achieves the same architecture without the FFI risk surface, and maps directly to D-010 client-server. See `docs/architecture/risk-godot-rust-bridge.md` for full analysis.
|
||||
|
||||
### R-007: Godot + C++ via GDExtension
|
||||
- **Rejected:** 2026-02-09
|
||||
- **Reason:** More mature bindings than gdext, but trades Rust's safety guarantees for C++ memory unsafety. No advantage over subprocess/IPC approach. Developer not experienced in C++.
|
||||
|
||||
### R-008: Fyrox (pure Rust engine)
|
||||
- **Rejected:** 2026-02-09
|
||||
- **Reason:** Single maintainer. ~1/50th of Godot's community. Less documentation, fewer tutorials, weaker AI training data for Claude Code. Single-maintainer risk unacceptable for multi-year project.
|
||||
|
||||
### R-009: Custom framework (Rust + raylib/macroquad)
|
||||
- **Rejected:** 2026-02-09
|
||||
- **Reason:** Maximum control but you build everything — tilemap rendering, camera, UI, audio, input, asset pipeline. Months before testing a mechanic. The game's hard problems are simulation, not rendering — don't reinvent the rendering wheel.
|
||||
|
||||
### R-010: protobuf for client-server serialization
|
||||
- **Rejected:** 2026-02-09
|
||||
- **Reason:** Schema evolution across independent deployments is a problem we don't have (one developer, client and server ship together). Poor GDScript support. Rigid schema fights dynamic HUD composition driven by perception modes (D-017). MessagePack's schema-optional nature fits better.
|
||||
|
||||
---
|
||||
|
||||
*Document maintained by QATUX. Last updated: 2026-02-09*
|
||||
*Restructured 2026-02-11. Original monolithic format in git history.*
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
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 \
|
||||
decisions-sync decisions-coverage decisions-active decisions-orphan \
|
||||
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-server pre-pr-client pre-pr-content \
|
||||
fixtures-client golden-diff golden-update \
|
||||
checklist-validate checklist-generate
|
||||
|
||||
# --- Configuration ---
|
||||
|
||||
GODOT_VERSION ?= 4.6
|
||||
|
||||
# Default
|
||||
help:
|
||||
@echo "The Settled Reach — Development Commands"
|
||||
@echo ""
|
||||
@echo " make setup Install dev dependencies (Rust, Godot, tooling)"
|
||||
@echo " make build Build client and server"
|
||||
@echo " make game Build and run the full game (server + client)"
|
||||
@echo " make stop Stop any running server instance"
|
||||
@echo " make client Run the Godot client (test mode)"
|
||||
@echo " make server Run the Rust simulation server"
|
||||
@echo " make test Run all tests"
|
||||
@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 clean Remove build artifacts and caches"
|
||||
@echo ""
|
||||
@echo " make db-backup Backup shared database to git (main only)"
|
||||
@echo " make db-install Restore shared database from backup"
|
||||
@echo ""
|
||||
@echo " make decisions-sync Sync decisions/*.md into SQLite"
|
||||
@echo " make decisions-coverage Decision-to-ticket coverage by domain"
|
||||
@echo " make decisions-active List active decisions"
|
||||
@echo " make decisions-orphan Decisions without implementing tickets"
|
||||
@echo " make validate-content Validate content YAML against schemas"
|
||||
@echo " make check-fact-ids Check fact_id references against knowledge catalogs"
|
||||
@echo " make content-ron Convert content YAML to RON (build-time)"
|
||||
@echo " make fixtures-client Generate GDScript->Rust cross-encoder fixtures (#475)"
|
||||
@echo " make golden-diff Show diff if golden file output has changed"
|
||||
@echo " make golden-update Regenerate golden file and stage for commit"
|
||||
@echo " make checklist-validate Validate checklist YAML against schema"
|
||||
@echo " make checklist-generate Validate checklists + print condition summary"
|
||||
@echo ""
|
||||
@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-client Client-scoped pre-PR (lint, build, test)"
|
||||
@echo " make pre-pr-content Content-scoped pre-PR (schema + cross-ref validation)"
|
||||
@echo ""
|
||||
@echo " make setup-hooks Install pre-commit hooks (included in setup)"
|
||||
@echo ""
|
||||
@echo " GODOT_VERSION=4.6 make setup Override Godot version"
|
||||
|
||||
# --- Setup ---
|
||||
|
||||
setup: setup-rust setup-godot setup-tooling setup-hooks decisions-sync
|
||||
@echo "Dev environment ready."
|
||||
|
||||
setup-rust:
|
||||
@tooling/install-rust
|
||||
|
||||
setup-godot:
|
||||
@GODOT_VERSION=$(GODOT_VERSION) tooling/install-godot
|
||||
|
||||
setup-tooling:
|
||||
@echo "Checking tooling dependencies..."
|
||||
@command -v python3 >/dev/null 2>&1 || { echo "Install Python 3"; exit 1; }
|
||||
@command -v curl >/dev/null 2>&1 || { echo "Install curl (required for Godot download)"; exit 1; }
|
||||
@command -v unzip >/dev/null 2>&1 || { echo "Install unzip (required for Godot download)"; exit 1; }
|
||||
@python3 -c "import yaml" 2>/dev/null || { echo "Install PyYAML: pip install pyyaml"; exit 1; }
|
||||
@python3 -c "import jsonschema" 2>/dev/null || { echo "Install jsonschema: pip install jsonschema"; exit 1; }
|
||||
|
||||
setup-hooks:
|
||||
@git config core.hooksPath .config/hooks
|
||||
@echo "Git hooks path set to .config/hooks"
|
||||
|
||||
# --- Build ---
|
||||
|
||||
build: build-server build-client
|
||||
|
||||
build-server:
|
||||
cd server && cargo build
|
||||
|
||||
build-client:
|
||||
@test -n "$(GODOT)" || { echo "Godot not found. Run 'make setup' first."; exit 1; }
|
||||
$(GODOT) --headless --path client --import --quit
|
||||
|
||||
# --- Run ---
|
||||
|
||||
server:
|
||||
cd server && cargo run
|
||||
|
||||
client:
|
||||
@test -n "$(GODOT)" || { echo "Godot not found. Run 'make setup' first."; exit 1; }
|
||||
$(GODOT) --path client
|
||||
|
||||
game: stop build
|
||||
@test -n "$(GODOT)" || { echo "Godot not found. Run 'make setup' first."; exit 1; }
|
||||
@echo "Starting server..."
|
||||
@cd server && cargo run &
|
||||
@sleep 2
|
||||
@echo "Starting client..."
|
||||
@SR_LIVE=1 $(GODOT) --path client
|
||||
@$(MAKE) stop
|
||||
|
||||
stop:
|
||||
@lsof -ti :9876 | xargs -r kill 2>/dev/null || true
|
||||
@echo "Stopped any running server on port 9876"
|
||||
|
||||
# --- Test ---
|
||||
|
||||
test: test-server test-client
|
||||
|
||||
test-server:
|
||||
cd server && cargo nextest run
|
||||
|
||||
fixtures:
|
||||
cd server && cargo test --test gen_fixtures -- --ignored
|
||||
|
||||
fixtures-client:
|
||||
@test -n "$(GODOT)" || { echo "Godot not found. Run 'make setup' first."; exit 1; }
|
||||
@echo "Generating GDScript fixtures for Rust decoder..."
|
||||
$(GODOT) --headless --path client -s res://tests/gen_client_fixtures.gd
|
||||
@echo "Verifying Rust can decode GDScript fixtures..."
|
||||
cd server && cargo test --test serialization gdscript_generated_fixtures
|
||||
@echo "--- Fixtures-client: PASS ---"
|
||||
|
||||
golden-diff:
|
||||
@echo "Regenerating golden output for comparison..."
|
||||
@mkdir -p .cache/golden
|
||||
@cp server/tests/golden/proof_room_tick_10.json .cache/golden/before.json
|
||||
@cd server && UPDATE_GOLDEN=1 cargo test --test golden_suite -- proof_room_tick_10_matches_golden; \
|
||||
rc=$$?; \
|
||||
cp ../server/tests/golden/proof_room_tick_10.json ../.cache/golden/after.json 2>/dev/null; \
|
||||
cp ../.cache/golden/before.json ../server/tests/golden/proof_room_tick_10.json; \
|
||||
if [ $$rc -ne 0 ]; then echo "Cargo test failed (golden file restored)."; exit $$rc; fi
|
||||
@if diff -q .cache/golden/before.json .cache/golden/after.json >/dev/null 2>&1; then \
|
||||
echo "--- Golden file: UP TO DATE ---"; \
|
||||
else \
|
||||
echo "--- Golden file has CHANGED ---"; \
|
||||
echo ""; \
|
||||
diff --color -u .cache/golden/before.json .cache/golden/after.json || true; \
|
||||
echo ""; \
|
||||
echo "Run 'make golden-update' to accept changes."; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
golden-update:
|
||||
@echo "Regenerating golden file..."
|
||||
@cd server && UPDATE_GOLDEN=1 cargo test --test golden_suite -- proof_room_tick_10_matches_golden
|
||||
@git add server/tests/golden/
|
||||
@echo "--- Golden file updated and staged ---"
|
||||
@echo "Review with: git diff --cached -- server/tests/golden/"
|
||||
|
||||
test-client:
|
||||
@test -n "$(GODOT)" || { echo "Godot not found. Run 'make setup' first."; exit 1; }
|
||||
$(GODOT) --headless --path client -s res://addons/gdUnit4/bin/GdUnitCmdTool.gd --ignoreHeadlessMode -a res://tests/
|
||||
|
||||
# --- Lint ---
|
||||
|
||||
lint: lint-server lint-client
|
||||
|
||||
lint-server:
|
||||
cd server && cargo clippy -- -D warnings
|
||||
cd server && cargo fmt --check
|
||||
|
||||
lint-client:
|
||||
@test -n "$(GODOT)" || { echo "Godot not found. Run 'make setup' first."; exit 1; }
|
||||
@echo "Checking GDScript for errors..."
|
||||
@$(GODOT) --headless --path client --quit 2>&1 | grep -i "SCRIPT ERROR" && { echo "GDScript errors found"; exit 1; } || echo "No script errors found"
|
||||
|
||||
# --- Pre-PR verification ---
|
||||
|
||||
pre-pr: pre-pr-lint pre-pr-build pre-pr-test pre-pr-validate pre-pr-fixtures
|
||||
@echo ""
|
||||
@echo "=== PRE-PR: ALL CHECKS PASSED ==="
|
||||
@echo "Safe to create PR."
|
||||
|
||||
pre-pr-lint: lint-server lint-client
|
||||
@echo "--- Lint: PASS ---"
|
||||
|
||||
pre-pr-build: build-server build-client
|
||||
@echo "--- Build: PASS ---"
|
||||
|
||||
pre-pr-test: test-server test-client
|
||||
@echo "--- Tests: PASS ---"
|
||||
|
||||
pre-pr-validate: validate-content check-fact-ids
|
||||
@echo "--- Content validation: PASS ---"
|
||||
|
||||
pre-pr-fixtures:
|
||||
@echo "Checking Rust->GDScript fixture staleness..."
|
||||
@cd server && cargo test --test gen_fixtures -- --ignored
|
||||
@if git diff --quiet client/tests/fixtures/; then \
|
||||
echo "--- Rust fixtures: UP TO DATE ---"; \
|
||||
else \
|
||||
echo ""; \
|
||||
echo "--- RUST FIXTURES STALE ---"; \
|
||||
echo " Protocol changed but fixtures not regenerated."; \
|
||||
echo " Stale fixtures make all client tests FALSE POSITIVES."; \
|
||||
echo ""; \
|
||||
echo " Changed files:"; \
|
||||
git diff --stat client/tests/fixtures/; \
|
||||
echo ""; \
|
||||
echo " Fix: commit the updated fixtures with your protocol change."; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "Checking GDScript->Rust fixture staleness..."
|
||||
@$(MAKE) fixtures-client
|
||||
@if git diff --quiet server/tests/fixtures/gdscript/; then \
|
||||
echo "--- GDScript fixtures: UP TO DATE ---"; \
|
||||
else \
|
||||
echo ""; \
|
||||
echo "--- GDSCRIPT FIXTURES STALE ---"; \
|
||||
echo " Protocol changed but GDScript fixtures not regenerated."; \
|
||||
echo " Stale fixtures make cross-encoder tests FALSE POSITIVES."; \
|
||||
echo ""; \
|
||||
echo " Changed files:"; \
|
||||
git diff --stat server/tests/fixtures/gdscript/; \
|
||||
echo ""; \
|
||||
echo " Fix: commit the updated fixtures with your protocol change."; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
# Branch-specific variants (faster, scope-appropriate)
|
||||
|
||||
pre-pr-server: lint-server build-server test-server pre-pr-fixtures
|
||||
@echo "=== Server pre-PR: PASSED ==="
|
||||
|
||||
pre-pr-client: lint-client build-client test-client
|
||||
@echo "=== Client pre-PR: PASSED ==="
|
||||
|
||||
pre-pr-content: validate-content check-fact-ids checklist-validate
|
||||
@echo "=== Content pre-PR: PASSED ==="
|
||||
|
||||
# --- CI (run locally) ---
|
||||
|
||||
ci: ci-server ci-client
|
||||
|
||||
ci-server: lint-server build-server test-server
|
||||
|
||||
ci-client: lint-client build-client test-client
|
||||
|
||||
# --- Database ---
|
||||
|
||||
db-backup:
|
||||
@tooling/db-backup
|
||||
|
||||
db-install:
|
||||
@tooling/db-install
|
||||
|
||||
# --- Decisions ---
|
||||
|
||||
decisions-sync:
|
||||
@db/connectors/decisions-sync
|
||||
|
||||
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"
|
||||
|
||||
decisions-active:
|
||||
@db/connectors/sqlite-query "SELECT id, domain, title FROM decisions WHERE status='active' AND type='confirmed' ORDER BY domain, id"
|
||||
|
||||
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)"
|
||||
|
||||
# --- Content Validation ---
|
||||
|
||||
validate-content:
|
||||
@tooling/validate-content
|
||||
|
||||
check-fact-ids:
|
||||
@tooling/check-fact-ids
|
||||
|
||||
checklist-validate:
|
||||
@tooling/validate-checklist --check
|
||||
|
||||
checklist-generate:
|
||||
@tooling/validate-checklist
|
||||
|
||||
content-ron:
|
||||
cd tooling/content-converter && cargo build --release
|
||||
tooling/content-converter/target/release/content-converter --input content --output content-ron --verbose
|
||||
|
||||
# --- Clean ---
|
||||
|
||||
clean:
|
||||
cd server && cargo clean || true
|
||||
rm -rf .cache/*
|
||||
rm -rf client/.godot/* client/reports
|
||||
@echo "Clean complete."
|
||||
@@ -0,0 +1,135 @@
|
||||
# The Settled Reach
|
||||
|
||||
**A top-down immersive sim where you only know what you can see, hear, and prove.**
|
||||
|
||||
---
|
||||
|
||||
## You're not the hero. You're a witness with a gun.
|
||||
|
||||
Pick a character. Step into their life. Figure out what's happening before it's too late.
|
||||
|
||||
The Settled Reach is a single-character detective game where information is the real weapon. You play from one perspective in a simulated world full of people who lie, withhold, and reveal on their own schedules. What you know depends on where you are, who you trust, and whether you're asking the right questions.
|
||||
|
||||
---
|
||||
|
||||
## What You Do
|
||||
|
||||
**Investigate.** Follow leads through a city you can't see all at once. Line-of-sight isn't a gameplay abstraction - it's the core mechanic. You see what your character sees. Walls block vision. Corners hide threats. Fog returns when you leave. Information decays.
|
||||
|
||||
**Navigate.** Your neural lattice gives you directions, but only to places you've learned about. Contacts send locations. Research reveals addresses. Physical exploration fills the gaps. The map is sparse and frightening at first. By the end, you realize how much you missed.
|
||||
|
||||
**Decide.** Every conversation is a negotiation over what gets shared. NPCs have their own goals, their own information, and their own reasons to lie. You choose who to trust based on incomplete data. Sometimes you're wrong.
|
||||
|
||||
**Act.** When investigation fails, violence is an option. Fights are tactical, asymmetric, and often a terrible idea. Power mismatches run in both directions. Death costs you everything since your last backup - lost knowledge, lost relationships, lost deals. The world knows what you forgot.
|
||||
|
||||
**Survive.** An AI storyteller paces events for dramatic tension. The conspiracy's structure is set at game start. When things happen is driven by how you're doing. Comfortable? Something breaks. Desperate? A break appears. The game wants a story, not a grind.
|
||||
|
||||
---
|
||||
|
||||
## Core Features
|
||||
|
||||
### Information Asymmetry as Master Mechanic
|
||||
You only know what your character knows. The same world looks completely different from different perspectives. A Senator sees political venues on their map. A Guardian sees safehouses. Same conspiracy, different game.
|
||||
|
||||
### Occlusion-Based Detection
|
||||
Vision is line-of-sight with a directional cone. You see what's in front of you. Peripheral vision is reduced. Behind you is fog. Sound gives partial information outside your view - footsteps, voices, gunfire. You make decisions based on what you can piece together.
|
||||
|
||||
### Internal Monologue
|
||||
Your character interprets what they sense in their own voice. Footsteps behind you. Thermal signatures through walls. A contact's nervous tells. The monologue bridges what the camera can't show and creates atmosphere. It can also be wrong.
|
||||
|
||||
### Perception Modes
|
||||
Different characters access different sensors. Natural vision shows detail but is blocked by walls. Thermal imaging shows heat signatures with no identity. Camera feeds give remote vision but can be spoofed. Unisphere tracking pings known contacts but can be masked. Each mode reveals different information with different trust tradeoffs.
|
||||
|
||||
### Diegetic Interface
|
||||
The map is your character's neural lattice - Commonwealth technology, not a game UI. Points of interest appear when you learn them through gameplay. Tips can be traps. Navigation is pulled by player intent, not pushed by map design.
|
||||
|
||||
### Multiple Playable Characters
|
||||
Every character starts in a different position with different knowledge and different tools. A cop has case files and legal authority. An investigator has contacts and freedom to operate. A politician has institutional access and public constraints. Replayability comes from perspective, not randomness.
|
||||
|
||||
### Rimworld-Style Storyteller
|
||||
Events emerge from simulation, paced by an AI director. No timers. No scripts. Consequences of what's already in motion. The storyteller knows what you've lost and can exploit it.
|
||||
|
||||
### Wildly Asymmetric Combat
|
||||
Balanced fights are the exception. You're outgunned or overpowered, rarely evenly matched. Tactical clarity from the top-down view. Z-levels create verticality - fights across floors of a building. Perception boundaries mean you can be ambushed. Victory often means not fighting at all.
|
||||
|
||||
---
|
||||
|
||||
## The Setting
|
||||
|
||||
Humanity spread across hundreds of star systems by learning to activate alien gates it didn't build. The result is the Settled Reach - a civilization stitched together by infrastructure it doesn't fully understand.
|
||||
|
||||
You live in a society where death is temporary (if you can afford the backup), enhancement is political (who gets augmented and why), and the gates that connect everything were made by someone who hasn't introduced themselves.
|
||||
|
||||
Wormholes aren't fast travel markers. They're physical locations you traverse. Social chokepoints. Surveillance points. The infrastructure you depend on to move between worlds.
|
||||
|
||||
Something is breaking. Or something is waking up. You're in the wrong place at the right time to figure out which.
|
||||
|
||||
---
|
||||
|
||||
## Built For Replayability
|
||||
|
||||
Structural randomness set at game start: who's compromised, where evidence is hidden, which contacts can be trusted. Play the same scenario as different characters and encounter a different game. The conspiracy's skeleton is fixed. The flesh changes every time.
|
||||
|
||||
---
|
||||
|
||||
## Development Philosophy
|
||||
|
||||
Single-character perspective. Asymmetric information as core mechanic. Top-down camera locked to character position. Godot 4 client + Rust simulation server. Client-server architecture from day one, designed for multiplayer even though single-player ships first.
|
||||
|
||||
No fog-of-war as an afterthought. No tutorial popups. No omniscient map reveals. The game is built around what you don't know.
|
||||
|
||||
---
|
||||
|
||||
## [LOREM IPSUM PLACEHOLDER]
|
||||
|
||||
### Screenshots
|
||||
*[Gameplay screenshot: character navigating fog-of-war through urban environment]*
|
||||
*[Gameplay screenshot: thermal vision showing heat signatures through building]*
|
||||
*[Gameplay screenshot: conversation interface with internal monologue overlay]*
|
||||
*[Gameplay screenshot: tactical combat with vision cone and z-level interaction]*
|
||||
|
||||
### Media
|
||||
*[Trailer: Character selection → Investigation → Revelation → Crisis]*
|
||||
*[Dev diary: Information asymmetry as game mechanic]*
|
||||
|
||||
### Awards & Recognition
|
||||
*[To be populated as development progresses]*
|
||||
|
||||
### Community & Updates
|
||||
*[Discord link]*
|
||||
*[Development blog]*
|
||||
*[Steam community hub]*
|
||||
|
||||
---
|
||||
|
||||
## System Requirements
|
||||
|
||||
### Minimum
|
||||
**OS:** [TBD]
|
||||
**Processor:** [TBD]
|
||||
**Memory:** [TBD]
|
||||
**Graphics:** [TBD]
|
||||
**Storage:** [TBD]
|
||||
|
||||
### Recommended
|
||||
**OS:** [TBD]
|
||||
**Processor:** [TBD]
|
||||
**Memory:** [TBD]
|
||||
**Graphics:** [TBD]
|
||||
**Storage:** [TBD]
|
||||
|
||||
---
|
||||
|
||||
## About This Project
|
||||
|
||||
**Official Title:** The Settled Reach (D-021)
|
||||
**Repository:** commonwealth (historical code name)
|
||||
**Engine:** Godot 4 + Rust/bevy_ecs simulation server via subprocess/IPC
|
||||
**Setting:** Original science fiction IP, Commonwealth-inspired
|
||||
**Status:** Pre-alpha development
|
||||
|
||||
For development documentation, see the [decisions/](decisions/) directory and [TEAM.md](TEAM.md).
|
||||
|
||||
---
|
||||
|
||||
**The Settled Reach is in active development. All features, systems, and content are subject to change.**
|
||||
@@ -1,4 +1,4 @@
|
||||
# Commonwealth Game - Team Roster
|
||||
# The Settled Reach - Team Roster
|
||||
|
||||
## Team Leader
|
||||
- **Jeroen** - Final decision maker. 30 years software dev / systems & cloud architect. Claude Code 20x.
|
||||
@@ -38,6 +38,7 @@
|
||||
| **DUDLEY** | Server Developer | Game server, ECS, simulation loop, world state management. | Server implementation phase |
|
||||
| **OSCAR** | Networking Developer | Multiplayer networking, client-server protocol, sync. | Networking implementation phase |
|
||||
| **JUSTINE** | Polish & Deploy | Build pipelines, packaging, performance optimization, release prep. | Pre-release phase |
|
||||
| **INIGO** | Sound Designer | Soundscape design, ambient layers, diegetic audio cues, audio propagation. | Audio implementation phase |
|
||||
| **MELLANIE** | Copywriter | In-game text, UI copy, tooltips, flavor text, lore entries. | Content creation phase |
|
||||
| **TIGER** | Translator | Localization, i18n framework, translation management. | Localization phase |
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"test_root_folder": "res://tests",
|
||||
"report_format": "JSON",
|
||||
"auto_save": true
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Mike Schulze
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env -S godot -s
|
||||
extends SceneTree
|
||||
|
||||
|
||||
var _cli_runner: GdUnitTestCIRunner
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_MINIMIZED)
|
||||
_cli_runner = GdUnitTestCIRunner.new()
|
||||
root.add_child(_cli_runner)
|
||||
|
||||
|
||||
# do not use print statements on _finalize it results in random crashes
|
||||
func _finalize() -> void:
|
||||
queue_delete(_cli_runner)
|
||||
if OS.is_stdout_verbose():
|
||||
prints("Finallize ..")
|
||||
prints("-Orphan nodes report-----------------------")
|
||||
Window.print_orphan_nodes()
|
||||
prints("Finallize .. done")
|
||||
@@ -0,0 +1 @@
|
||||
uid://c3hmp16ojaqd8
|
||||
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env -S godot -s
|
||||
extends MainLoop
|
||||
|
||||
const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd")
|
||||
|
||||
# gdlint: disable=max-line-length
|
||||
const LOG_FRAME_TEMPLATE = """
|
||||
<!DOCTYPE html>
|
||||
<html style="display: inline-grid;">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Godot Logging</title>
|
||||
<link rel="stylesheet" href="css/styles.css">
|
||||
</head>
|
||||
|
||||
<body style="background-color: #eee;">
|
||||
<div class="godot-report-frame"">
|
||||
${content}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
const NO_LOG_MESSAGE = """
|
||||
<h3>No logging available!</h3>
|
||||
</br>
|
||||
<p>In order for logging to take place, you must activate the Activate file logging option in the project settings.</p>
|
||||
<p>You can enable the logging under:
|
||||
<b>Project Settings</b> > <b>Debug</b> > <b>File Logging</b> > <b>Enable File Logging</b> in the project settings.</p>
|
||||
"""
|
||||
|
||||
#warning-ignore-all:return_value_discarded
|
||||
var _cmd_options := CmdOptions.new([
|
||||
CmdOption.new(
|
||||
"-rd, --report-directory",
|
||||
"-rd <directory>",
|
||||
"Specifies the output directory in which the reports are to be written. The default is res://reports/.",
|
||||
TYPE_STRING,
|
||||
true
|
||||
)
|
||||
])
|
||||
|
||||
|
||||
var _report_root_path: String
|
||||
var _current_report_path: String
|
||||
var _debug_cmd_args := PackedStringArray()
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
set_report_directory(GdUnitFileAccess.current_dir() + "reports")
|
||||
set_current_report_path()
|
||||
|
||||
|
||||
func _process(_delta: float) -> bool:
|
||||
# check if reports exists
|
||||
if not reports_available():
|
||||
prints("no reports found")
|
||||
return true
|
||||
|
||||
# only process if godot logging is enabled
|
||||
if not GdUnitSettings.is_log_enabled():
|
||||
write_report(NO_LOG_MESSAGE, "")
|
||||
return true
|
||||
|
||||
# parse possible custom report path,
|
||||
var cmd_parser := CmdArgumentParser.new(_cmd_options, "GdUnitCmdTool.gd")
|
||||
# ignore erros and exit quitly
|
||||
if cmd_parser.parse(get_cmdline_args(), true).is_error():
|
||||
return true
|
||||
CmdCommandHandler.new(_cmd_options).register_cb("-rd", set_report_directory)
|
||||
|
||||
var godot_log_file := scan_latest_godot_log()
|
||||
var result := read_log_file_content(godot_log_file)
|
||||
if result.is_error():
|
||||
write_report(result.error_message(), godot_log_file)
|
||||
return true
|
||||
write_report(result.value_as_string(), godot_log_file)
|
||||
return true
|
||||
|
||||
|
||||
func set_current_report_path() -> void:
|
||||
# scan for latest report directory
|
||||
var iteration := GdUnitFileAccess.find_last_path_index(
|
||||
_report_root_path, GdUnitConstants.REPORT_DIR_PREFIX
|
||||
)
|
||||
_current_report_path = "%s/%s%d" % [_report_root_path, GdUnitConstants.REPORT_DIR_PREFIX, iteration]
|
||||
|
||||
|
||||
func set_report_directory(path: String) -> void:
|
||||
_report_root_path = path
|
||||
|
||||
|
||||
func get_log_report_html() -> String:
|
||||
return _current_report_path + "/godot_report_log.html"
|
||||
|
||||
|
||||
func reports_available() -> bool:
|
||||
return DirAccess.dir_exists_absolute(_report_root_path)
|
||||
|
||||
|
||||
func scan_latest_godot_log() -> String:
|
||||
var path := GdUnitSettings.get_log_path().get_base_dir()
|
||||
var files_sorted := Array()
|
||||
for file in GdUnitFileAccess.scan_dir(path):
|
||||
var file_name := "%s/%s" % [path, file]
|
||||
files_sorted.append(file_name)
|
||||
# sort by name, the name contains the timestamp so we sort at the end by timestamp
|
||||
files_sorted.sort()
|
||||
return files_sorted.back()
|
||||
|
||||
|
||||
func read_log_file_content(log_file: String) -> GdUnitResult:
|
||||
var file := FileAccess.open(log_file, FileAccess.READ)
|
||||
if file == null:
|
||||
return GdUnitResult.error(
|
||||
"Can't find log file '%s'. Error: %s"
|
||||
% [log_file, error_string(FileAccess.get_open_error())]
|
||||
)
|
||||
var content := "<pre>" + file.get_as_text()
|
||||
# patch out console format codes
|
||||
for color_index in range(0, 256):
|
||||
var to_replace := "[38;5;%dm" % color_index
|
||||
content = content.replace(to_replace, "")
|
||||
content += "</pre>"
|
||||
content = content\
|
||||
.replace("[0m", "")\
|
||||
.replace(GdUnitCSIMessageWriter.CSI_BOLD, "")\
|
||||
.replace(GdUnitCSIMessageWriter.CSI_ITALIC, "")\
|
||||
.replace(GdUnitCSIMessageWriter.CSI_UNDERLINE, "")
|
||||
return GdUnitResult.success(content)
|
||||
|
||||
|
||||
func write_report(content: String, godot_log_file: String) -> GdUnitResult:
|
||||
var file := FileAccess.open(get_log_report_html(), FileAccess.WRITE)
|
||||
if file == null:
|
||||
return GdUnitResult.error(
|
||||
"Can't open to write '%s'. Error: %s"
|
||||
% [get_log_report_html(), error_string(FileAccess.get_open_error())]
|
||||
)
|
||||
var report_html := LOG_FRAME_TEMPLATE.replace("${content}", content)
|
||||
file.store_string(report_html)
|
||||
_update_index_html(godot_log_file)
|
||||
return GdUnitResult.success(file)
|
||||
|
||||
|
||||
func _update_index_html(godot_log_file: String) -> void:
|
||||
var index_path := "%s/index.html" % _current_report_path
|
||||
var index_file := FileAccess.open(index_path, FileAccess.READ_WRITE)
|
||||
if index_file == null:
|
||||
push_error(
|
||||
"Can't add log path '%s' to `%s`. Error: %s"
|
||||
% [godot_log_file, index_path, error_string(FileAccess.get_open_error())]
|
||||
)
|
||||
return
|
||||
var content := index_file.get_as_text()\
|
||||
.replace("${log_report}", get_log_report_html())\
|
||||
.replace("${godot_log_file}", godot_log_file)
|
||||
# overide it
|
||||
index_file.seek(0)
|
||||
index_file.store_string(content)
|
||||
|
||||
|
||||
func get_cmdline_args() -> PackedStringArray:
|
||||
if _debug_cmd_args.is_empty():
|
||||
return OS.get_cmdline_args()
|
||||
return _debug_cmd_args
|
||||
@@ -0,0 +1 @@
|
||||
uid://df5cjsxm4mqxl
|
||||
@@ -0,0 +1,7 @@
|
||||
[plugin]
|
||||
|
||||
name="gdUnit4"
|
||||
description="Unit Testing Framework for Godot Scripts"
|
||||
author="Mike Schulze"
|
||||
version="6.1.1"
|
||||
script="plugin.gd"
|
||||
@@ -0,0 +1,101 @@
|
||||
@tool
|
||||
extends EditorPlugin
|
||||
|
||||
var _gd_inspector: Control
|
||||
var _gd_console: Control
|
||||
var _filesystem_context_menu: EditorContextMenuPlugin
|
||||
var _editor_context_menu: EditorContextMenuPlugin
|
||||
var _editor_code_context_menu: EditorContextMenuPlugin
|
||||
|
||||
|
||||
func _enter_tree() -> void:
|
||||
var inferred_declaration: int = ProjectSettings.get_setting("debug/gdscript/warnings/inferred_declaration")
|
||||
|
||||
var is_gdunit_excluded_warnings: bool = false
|
||||
if Engine.get_version_info().hex >= 0x40600:
|
||||
var dirctrory_rules: Dictionary = ProjectSettings.get_setting("debug/gdscript/warnings/directory_rules")
|
||||
if dirctrory_rules.has("res://addons/gdUnit4") and dirctrory_rules["res://addons/gdUnit4"] == 0:
|
||||
is_gdunit_excluded_warnings = true
|
||||
else:
|
||||
is_gdunit_excluded_warnings = ProjectSettings.get_setting("debug/gdscript/warnings/exclude_addons")
|
||||
if !is_gdunit_excluded_warnings and inferred_declaration != 0:
|
||||
printerr("GdUnit4: 'inferred_declaration' is set to Warning/Error!")
|
||||
if Engine.get_version_info().hex >= 0x40600:
|
||||
printerr("GdUnit4 is not 'inferred_declaration' save, you have to excluded the addon (debug/gdscript/warnings/directory_rules)")
|
||||
else:
|
||||
printerr("GdUnit4 is not 'inferred_declaration' save, you have to excluded addons (debug/gdscript/warnings/exclude_addons)")
|
||||
printerr("Loading GdUnit4 Plugin failed.")
|
||||
return
|
||||
|
||||
if check_running_in_test_env():
|
||||
@warning_ignore("return_value_discarded")
|
||||
GdUnitCSIMessageWriter.new().prints_warning("It was recognized that GdUnit4 is running in a test environment, therefore the GdUnit4 plugin will not be executed!")
|
||||
return
|
||||
|
||||
if Engine.get_version_info().hex < 0x40500:
|
||||
prints("This GdUnit4 plugin version '%s' requires Godot version '4.5' or higher to run." % GdUnit4Version.current())
|
||||
return
|
||||
GdUnitSettings.setup()
|
||||
# Install the GdUnit Inspector
|
||||
_gd_inspector = (load("res://addons/gdUnit4/src/ui/GdUnitInspector.tscn") as PackedScene).instantiate()
|
||||
_add_context_menus()
|
||||
add_control_to_dock(EditorPlugin.DOCK_SLOT_LEFT_UR, _gd_inspector)
|
||||
# Install the GdUnit Console
|
||||
_gd_console = (load("res://addons/gdUnit4/src/ui/GdUnitConsole.tscn") as PackedScene).instantiate()
|
||||
var control: Control = add_control_to_bottom_panel(_gd_console, "gdUnitConsole")
|
||||
@warning_ignore("unsafe_method_access")
|
||||
await _gd_console.setup_update_notification(control)
|
||||
if GdUnit4CSharpApiLoader.is_api_loaded():
|
||||
prints("GdUnit4Net version '%s' loaded." % GdUnit4CSharpApiLoader.version())
|
||||
else:
|
||||
prints("No GdUnit4Net found.")
|
||||
# Connect to be notified for script changes to be able to discover new tests
|
||||
GdUnitTestDiscoverGuard.instance()
|
||||
@warning_ignore("return_value_discarded")
|
||||
resource_saved.connect(_on_resource_saved)
|
||||
prints("Loading GdUnit4 Plugin success")
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if check_running_in_test_env():
|
||||
return
|
||||
if is_instance_valid(_gd_inspector):
|
||||
remove_control_from_docks(_gd_inspector)
|
||||
_gd_inspector.free()
|
||||
_remove_context_menus()
|
||||
if is_instance_valid(_gd_console):
|
||||
remove_control_from_bottom_panel(_gd_console)
|
||||
_gd_console.free()
|
||||
var gdUnitTools: GDScript = load("res://addons/gdUnit4/src/core/GdUnitTools.gd")
|
||||
@warning_ignore("unsafe_method_access")
|
||||
gdUnitTools.dispose_all(true)
|
||||
prints("Unload GdUnit4 Plugin success")
|
||||
|
||||
|
||||
func check_running_in_test_env() -> bool:
|
||||
var args: PackedStringArray = OS.get_cmdline_args()
|
||||
args.append_array(OS.get_cmdline_user_args())
|
||||
return DisplayServer.get_name() == "headless" or args.has("--selftest") or args.has("--add") or args.has("-a") or args.has("--quit-after") or args.has("--import")
|
||||
|
||||
|
||||
func _add_context_menus() -> void:
|
||||
_filesystem_context_menu = preload("res://addons/gdUnit4/src/ui/menu/GdUnitEditorFileSystemContextMenuHandler.gd").new()
|
||||
_editor_context_menu = preload("res://addons/gdUnit4/src/ui/menu/GdUnitScriptEditorContextMenuHandler.gd").new()
|
||||
_editor_code_context_menu = preload("res://addons/gdUnit4/src/ui/menu/GdUnitScriptEditorContextMenuHandler.gd").new()
|
||||
add_context_menu_plugin(EditorContextMenuPlugin.CONTEXT_SLOT_FILESYSTEM, _filesystem_context_menu)
|
||||
add_context_menu_plugin(EditorContextMenuPlugin.CONTEXT_SLOT_SCRIPT_EDITOR, _editor_context_menu)
|
||||
add_context_menu_plugin(EditorContextMenuPlugin.CONTEXT_SLOT_SCRIPT_EDITOR_CODE, _editor_code_context_menu)
|
||||
|
||||
|
||||
func _remove_context_menus() -> void:
|
||||
if is_instance_valid(_filesystem_context_menu):
|
||||
remove_context_menu_plugin(_filesystem_context_menu)
|
||||
if is_instance_valid(_editor_context_menu):
|
||||
remove_context_menu_plugin(_editor_context_menu)
|
||||
if is_instance_valid(_editor_code_context_menu):
|
||||
remove_context_menu_plugin(_editor_code_context_menu)
|
||||
|
||||
|
||||
func _on_resource_saved(resource: Resource) -> void:
|
||||
if resource is Script:
|
||||
await GdUnitTestDiscoverGuard.instance().discover(resource as Script)
|
||||
@@ -0,0 +1 @@
|
||||
uid://glrigkb7wtwn
|
||||
@@ -0,0 +1,62 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
:: Initialize variables
|
||||
set "godot_binary="
|
||||
set "filtered_args="
|
||||
|
||||
:: Process all arguments
|
||||
set "i=0"
|
||||
:parse_args
|
||||
if "%~1"=="" goto end_parse_args
|
||||
|
||||
if "%~1"=="--godot_binary" (
|
||||
set "godot_binary=%~2"
|
||||
shift
|
||||
shift
|
||||
) else (
|
||||
set "filtered_args=!filtered_args! %~1"
|
||||
shift
|
||||
)
|
||||
goto parse_args
|
||||
:end_parse_args
|
||||
|
||||
:: If --godot_binary wasn't provided, fallback to environment variable
|
||||
if "!godot_binary!"=="" (
|
||||
set "godot_binary=%GODOT_BIN%"
|
||||
)
|
||||
|
||||
:: Check if we have a godot_binary value from any source
|
||||
if "!godot_binary!"=="" (
|
||||
echo Godot binary path is not specified.
|
||||
echo Please either:
|
||||
echo - Set the environment variable: set GODOT_BIN=C:\path\to\godot.exe
|
||||
echo - Or use the --godot_binary argument: --godot_binary C:\path\to\godot.exe
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
:: Check if the Godot binary exists
|
||||
if not exist "!godot_binary!" (
|
||||
echo Error: The specified Godot binary '!godot_binary!' does not exist.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
:: Get Godot version and check if it's a mono build
|
||||
for /f "tokens=*" %%i in ('"!godot_binary!" --version') do set GODOT_VERSION=%%i
|
||||
echo !GODOT_VERSION! | findstr /I "mono" >nul
|
||||
if !errorlevel! equ 0 (
|
||||
echo Godot .NET detected
|
||||
echo Compiling c# classes ... Please Wait
|
||||
dotnet build --debug
|
||||
echo done !errorlevel!
|
||||
)
|
||||
|
||||
:: Run the tests with the filtered arguments
|
||||
"!godot_binary!" --path . -s -d res://addons/gdUnit4/bin/GdUnitCmdTool.gd !filtered_args!
|
||||
set exit_code=%ERRORLEVEL%
|
||||
echo Run tests ends with %exit_code%
|
||||
|
||||
:: Run the copy log command
|
||||
"!godot_binary!" --headless --path . --quiet -s res://addons/gdUnit4/bin/GdUnitCopyLog.gd !filtered_args! > nul
|
||||
set exit_code2=%ERRORLEVEL%
|
||||
exit /b %exit_code%
|
||||
Executable
+62
@@ -0,0 +1,62 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Check for command-line argument
|
||||
godot_binary=""
|
||||
filtered_args=""
|
||||
|
||||
# Process all arguments with a more compatible approach
|
||||
while [ $# -gt 0 ]; do
|
||||
if [ "$1" = "--godot_binary" ] && [ $# -gt 1 ]; then
|
||||
# Get the next argument as the value
|
||||
godot_binary="$2"
|
||||
shift 2
|
||||
else
|
||||
# Keep non-godot_binary arguments for passing to Godot
|
||||
filtered_args="$filtered_args $1"
|
||||
shift
|
||||
fi
|
||||
done
|
||||
|
||||
# If --godot_binary wasn't provided, fallback to environment variable
|
||||
if [ -z "$godot_binary" ]; then
|
||||
godot_binary="$GODOT_BIN"
|
||||
fi
|
||||
|
||||
# Check if we have a godot_binary value from any source
|
||||
if [ -z "$godot_binary" ]; then
|
||||
echo "Godot binary path is not specified."
|
||||
echo "Please either:"
|
||||
echo " - Set the environment variable: export GODOT_BIN=/path/to/godot"
|
||||
echo " - Or use the --godot_binary argument: --godot_binary /path/to/godot"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if the Godot binary exists and is executable
|
||||
if [ ! -f "$godot_binary" ]; then
|
||||
echo "Error: The specified Godot binary '$godot_binary' does not exist."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -x "$godot_binary" ]; then
|
||||
echo "Error: The specified Godot binary '$godot_binary' is not executable."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get Godot version and check if it's a .NET build
|
||||
GODOT_VERSION=$("$godot_binary" --version)
|
||||
if echo "$GODOT_VERSION" | grep -i "mono" > /dev/null; then
|
||||
echo "Godot .NET detected"
|
||||
echo "Compiling c# classes ... Please Wait"
|
||||
dotnet build --debug
|
||||
echo "done $?"
|
||||
fi
|
||||
|
||||
# Run the tests with the filtered arguments
|
||||
"$godot_binary" --path . -s -d res://addons/gdUnit4/bin/GdUnitCmdTool.gd $filtered_args
|
||||
exit_code=$?
|
||||
echo "Run tests ends with $exit_code"
|
||||
|
||||
# Run the copy log command
|
||||
"$godot_binary" --headless --path . --quiet -s res://addons/gdUnit4/bin/GdUnitCopyLog.gd $filtered_args > /dev/null
|
||||
exit_code2=$?
|
||||
exit $exit_code
|
||||
@@ -0,0 +1,12 @@
|
||||
class_name Comparator
|
||||
extends Resource
|
||||
|
||||
enum {
|
||||
EQUAL,
|
||||
LESS_THAN,
|
||||
LESS_EQUAL,
|
||||
GREATER_THAN,
|
||||
GREATER_EQUAL,
|
||||
BETWEEN_EQUAL,
|
||||
NOT_BETWEEN_EQUAL,
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://ddpip23ul2ci3
|
||||
@@ -0,0 +1,80 @@
|
||||
## Factory class providing convenient static methods to create various fuzzer instances.[br]
|
||||
##
|
||||
## Fuzzers is a utility class that simplifies the creation of different fuzzer types
|
||||
## for testing purposes. It provides static factory methods that create pre-configured
|
||||
## fuzzers with sensible defaults, making it easier to set up fuzz testing in your
|
||||
## test suites without manually instantiating each fuzzer type.[br]
|
||||
##
|
||||
## This class acts as a central access point for all fuzzer types, improving code
|
||||
## readability and reducing boilerplate in test cases.[br]
|
||||
##
|
||||
## @tutorial(Fuzzing Testing): https://en.wikipedia.org/wiki/Fuzzing
|
||||
class_name Fuzzers
|
||||
extends Resource
|
||||
|
||||
|
||||
## Generates random strings with length between [param min_length] and
|
||||
## [param max_length] (inclusive), using characters from [param charset].
|
||||
## See [StringFuzzer] for detailed documentation and examples.
|
||||
static func rand_str(min_length: int, max_length: int, charset := StringFuzzer.DEFAULT_CHARSET) -> StringFuzzer:
|
||||
return StringFuzzer.new(min_length, max_length, charset)
|
||||
|
||||
|
||||
## Creates a [BoolFuzzer] for generating random boolean values.[br]
|
||||
##
|
||||
## See [BoolFuzzer] for detailed documentation and examples.
|
||||
static func boolean() -> BoolFuzzer:
|
||||
return BoolFuzzer.new()
|
||||
|
||||
|
||||
## Creates an [IntFuzzer] for generating random integers within a range.[br]
|
||||
##
|
||||
## Generates random integers between [param from] and [param to] (inclusive)
|
||||
## using [constant IntFuzzer.NORMAL] mode.
|
||||
## See [IntFuzzer] for detailed documentation and examples.
|
||||
static func rangei(from: int, to: int) -> IntFuzzer:
|
||||
return IntFuzzer.new(from, to)
|
||||
|
||||
|
||||
## Creates a [FloatFuzzer] for generating random floats within a range.[br]
|
||||
##
|
||||
## Generates random float values between [param from] and [param to] (inclusive).
|
||||
## See [FloatFuzzer] for detailed documentation and examples.
|
||||
static func rangef(from: float, to: float) -> FloatFuzzer:
|
||||
return FloatFuzzer.new(from, to)
|
||||
|
||||
|
||||
## Creates a [Vector2Fuzzer] for generating random 2D vectors within a range.[br]
|
||||
##
|
||||
## Generates random Vector2 values where each component is bounded by
|
||||
## [param from] and [param to] (inclusive).
|
||||
## See [Vector2Fuzzer] for detailed documentation and examples.
|
||||
static func rangev2(from: Vector2, to: Vector2) -> Vector2Fuzzer:
|
||||
return Vector2Fuzzer.new(from, to)
|
||||
|
||||
|
||||
## Creates a [Vector3Fuzzer] for generating random 3D vectors within a range.[br]
|
||||
##
|
||||
## Generates random Vector3 values where each component is bounded by
|
||||
## [param from] and [param to] (inclusive).
|
||||
## See [Vector3Fuzzer] for detailed documentation and examples.
|
||||
static func rangev3(from: Vector3, to: Vector3) -> Vector3Fuzzer:
|
||||
return Vector3Fuzzer.new(from, to)
|
||||
|
||||
|
||||
## Creates an [IntFuzzer] that generates only even integers.[br]
|
||||
##
|
||||
## Generates random even integers between [param from] and [param to] (inclusive)
|
||||
## using [constant IntFuzzer.EVEN] mode.
|
||||
## See [IntFuzzer] for detailed documentation about even number generation.
|
||||
static func eveni(from: int, to: int) -> IntFuzzer:
|
||||
return IntFuzzer.new(from, to, IntFuzzer.EVEN)
|
||||
|
||||
|
||||
## Creates an [IntFuzzer] that generates only odd integers.[br]
|
||||
##
|
||||
## Generates random odd integers between [param from] and [param to] (inclusive)
|
||||
## using [constant IntFuzzer.ODD] mode.
|
||||
## See [IntFuzzer] for detailed documentation about odd number generation.
|
||||
static func oddi(from: int, to: int) -> IntFuzzer:
|
||||
return IntFuzzer.new(from, to, IntFuzzer.ODD)
|
||||
@@ -0,0 +1 @@
|
||||
uid://b73uueo7qj7yd
|
||||
@@ -0,0 +1,122 @@
|
||||
## An Assertion Tool to verify array values
|
||||
@abstract class_name GdUnitArrayAssert
|
||||
extends GdUnitAssert
|
||||
|
||||
|
||||
## Verifies that the current value is null.
|
||||
@abstract func is_null() -> GdUnitArrayAssert
|
||||
|
||||
|
||||
## Verifies that the current value is not null.
|
||||
@abstract func is_not_null() -> GdUnitArrayAssert
|
||||
|
||||
|
||||
## Verifies that the current Array is equal to the given one.
|
||||
@abstract func is_equal(...expected: Array) -> GdUnitArrayAssert
|
||||
|
||||
|
||||
## Verifies that the current Array is equal to the given one, ignoring case considerations.
|
||||
@abstract func is_equal_ignoring_case(...expected: Array) -> GdUnitArrayAssert
|
||||
|
||||
|
||||
## Verifies that the current Array is not equal to the given one.
|
||||
@abstract func is_not_equal(...expected: Array) -> GdUnitArrayAssert
|
||||
|
||||
|
||||
## Verifies that the current Array is not equal to the given one, ignoring case considerations.
|
||||
@abstract func is_not_equal_ignoring_case(...expected: Array) -> GdUnitArrayAssert
|
||||
|
||||
|
||||
## Overrides the default failure message by given custom message.
|
||||
@abstract func override_failure_message(message: String) -> GdUnitArrayAssert
|
||||
|
||||
|
||||
## Appends a custom message to the failure message.
|
||||
@abstract func append_failure_message(message: String) -> GdUnitArrayAssert
|
||||
|
||||
|
||||
## Verifies that the current Array is empty, it has a size of 0.
|
||||
@abstract func is_empty() -> GdUnitArrayAssert
|
||||
|
||||
|
||||
## Verifies that the current Array is not empty, it has a size of minimum 1.
|
||||
@abstract func is_not_empty() -> GdUnitArrayAssert
|
||||
|
||||
|
||||
## Verifies that the current Array is the same. [br]
|
||||
## Compares the current by object reference equals
|
||||
@abstract func is_same(expected: Variant) -> GdUnitArrayAssert
|
||||
|
||||
|
||||
## Verifies that the current Array is NOT the same. [br]
|
||||
## Compares the current by object reference equals
|
||||
@abstract func is_not_same(expected: Variant) -> GdUnitArrayAssert
|
||||
|
||||
|
||||
## Verifies that the current Array has a size of given value.
|
||||
@abstract func has_size(expectd: int) -> GdUnitArrayAssert
|
||||
|
||||
|
||||
## Verifies that the current Array contains the given values, in any order.[br]
|
||||
## The values are compared by deep parameter comparision, for object reference compare you have to use [method contains_same]
|
||||
@abstract func contains(...expected: Array) -> GdUnitArrayAssert
|
||||
|
||||
|
||||
## Verifies that the current Array contains exactly only the given values and nothing else, in same order.[br]
|
||||
## The values are compared by deep parameter comparision, for object reference compare you have to use [method contains_same_exactly]
|
||||
@abstract func contains_exactly(...expected: Array) -> GdUnitArrayAssert
|
||||
|
||||
|
||||
## Verifies that the current Array contains exactly only the given values and nothing else, in any order.[br]
|
||||
## The values are compared by deep parameter comparision, for object reference compare you have to use [method contains_same_exactly_in_any_order]
|
||||
@abstract func contains_exactly_in_any_order(...expected: Array) -> GdUnitArrayAssert
|
||||
|
||||
|
||||
## Verifies that the current Array contains the given values, in any order.[br]
|
||||
## The values are compared by object reference, for deep parameter comparision use [method contains]
|
||||
@abstract func contains_same(...expected: Array) -> GdUnitArrayAssert
|
||||
|
||||
|
||||
## Verifies that the current Array contains exactly only the given values and nothing else, in same order.[br]
|
||||
## The values are compared by object reference, for deep parameter comparision use [method contains_exactly]
|
||||
@abstract func contains_same_exactly(...expected: Array) -> GdUnitArrayAssert
|
||||
|
||||
|
||||
## Verifies that the current Array contains exactly only the given values and nothing else, in any order.[br]
|
||||
## The values are compared by object reference, for deep parameter comparision use [method contains_exactly_in_any_order]
|
||||
@abstract func contains_same_exactly_in_any_order(...expected: Array) -> GdUnitArrayAssert
|
||||
|
||||
|
||||
## Verifies that the current Array do NOT contains the given values, in any order.[br]
|
||||
## The values are compared by deep parameter comparision, for object reference compare you have to use [method not_contains_same]
|
||||
## [b]Example:[/b]
|
||||
## [codeblock]
|
||||
## # will succeed
|
||||
## assert_array([1, 2, 3, 4, 5]).not_contains(6)
|
||||
## # will fail
|
||||
## assert_array([1, 2, 3, 4, 5]).not_contains(2, 6)
|
||||
## [/codeblock]
|
||||
@abstract func not_contains(...expected: Array) -> GdUnitArrayAssert
|
||||
|
||||
|
||||
## Verifies that the current Array do NOT contains the given values, in any order.[br]
|
||||
## The values are compared by object reference, for deep parameter comparision use [method not_contains]
|
||||
## [b]Example:[/b]
|
||||
## [codeblock]
|
||||
## # will succeed
|
||||
## assert_array([1, 2, 3, 4, 5]).not_contains(6)
|
||||
## # will fail
|
||||
## assert_array([1, 2, 3, 4, 5]).not_contains(2, 6)
|
||||
## [/codeblock]
|
||||
@abstract func not_contains_same(...expected: Array) -> GdUnitArrayAssert
|
||||
|
||||
|
||||
## Extracts all values by given function name and optional arguments into a new ArrayAssert.
|
||||
## If the elements not accessible by `func_name` the value is converted to `"n.a"`, expecting null values
|
||||
@abstract func extract(func_name: String, ...func_args: Array) -> GdUnitArrayAssert
|
||||
|
||||
|
||||
## Extracts all values by given extractor's into a new ArrayAssert.
|
||||
## If the elements not extractable than the value is converted to `"n.a"`, expecting null values
|
||||
## -- The argument type is Array[GdUnitValueExtractor]
|
||||
@abstract func extractv(...extractors: Array) -> GdUnitArrayAssert
|
||||
@@ -0,0 +1 @@
|
||||
uid://b6yh0a5ut11nk
|
||||
@@ -0,0 +1,47 @@
|
||||
## Base interface of all GdUnit asserts
|
||||
@abstract class_name GdUnitAssert
|
||||
extends RefCounted
|
||||
|
||||
|
||||
## Verifies that the current value is null.
|
||||
@abstract func is_null() -> GdUnitAssert
|
||||
|
||||
|
||||
## Verifies that the current value is not null.
|
||||
@abstract func is_not_null() -> GdUnitAssert
|
||||
|
||||
|
||||
## Verifies that the current value is equal to expected one.
|
||||
@abstract func is_equal(expected: Variant) -> GdUnitAssert
|
||||
|
||||
|
||||
## Verifies that the current value is not equal to expected one.
|
||||
@abstract func is_not_equal(expected: Variant) -> GdUnitAssert
|
||||
|
||||
|
||||
## Overrides the default failure message by given custom message.[br]
|
||||
## This function allows you to replace the automatically generated failure message with a more specific
|
||||
## or user-friendly message that better describes the test failure context.[br]
|
||||
## Usage:
|
||||
## [codeblock]
|
||||
## # Override with custom context-specific message
|
||||
## func test_player_inventory():
|
||||
## assert_that(player.get_item_count("sword"))\
|
||||
## .override_failure_message("Player should have exactly one sword")\
|
||||
## .is_equal(1)
|
||||
## [/codeblock]
|
||||
@abstract func override_failure_message(message: String) -> GdUnitAssert
|
||||
|
||||
|
||||
## Appends a custom message to the failure message.[br]
|
||||
## This can be used to add additional information to the generated failure message
|
||||
## while keeping the original assertion details for better debugging context.[br]
|
||||
## Usage:
|
||||
## [codeblock]
|
||||
## # Add context to existing failure message
|
||||
## func test_player_health():
|
||||
## assert_that(player.health)\
|
||||
## .append_failure_message("Player was damaged by: %s" % last_damage_source)\
|
||||
## .is_greater(0)
|
||||
## [/codeblock]
|
||||
@abstract func append_failure_message(message: String) -> GdUnitAssert
|
||||
@@ -0,0 +1 @@
|
||||
uid://cl8koak7jpbil
|
||||
@@ -0,0 +1,72 @@
|
||||
class_name GdUnitAwaiter
|
||||
extends RefCounted
|
||||
|
||||
|
||||
# Waits for a specified signal in an interval of 50ms sent from the <source>, and terminates with an error after the specified timeout has elapsed.
|
||||
# source: the object from which the signal is emitted
|
||||
# signal_name: signal name
|
||||
# args: the expected signal arguments as an array
|
||||
# timeout: the timeout in ms, default is set to 2000ms
|
||||
func await_signal_on(source :Object, signal_name :String, args :Array = [], timeout_millis :int = 2000) -> Variant:
|
||||
# fail fast if the given source instance invalid
|
||||
var assert_that := GdUnitAssertImpl.new(signal_name)
|
||||
var line_number := GdUnitAssertions.get_line_number()
|
||||
if not is_instance_valid(source):
|
||||
@warning_ignore("return_value_discarded")
|
||||
assert_that.report_error(GdAssertMessages.error_await_signal_on_invalid_instance(source, signal_name, args), line_number)
|
||||
return await (Engine.get_main_loop() as SceneTree).process_frame
|
||||
# fail fast if the given source instance invalid
|
||||
if not is_instance_valid(source):
|
||||
@warning_ignore("return_value_discarded")
|
||||
assert_that.report_error(GdAssertMessages.error_await_signal_on_invalid_instance(source, signal_name, args), line_number)
|
||||
return await await_idle_frame()
|
||||
var awaiter := GdUnitSignalAwaiter.new(timeout_millis)
|
||||
var value :Variant = await awaiter.on_signal(source, signal_name, args)
|
||||
if awaiter.is_interrupted():
|
||||
var failure := "await_signal_on(%s, %s) timed out after %sms" % [signal_name, args, timeout_millis]
|
||||
@warning_ignore("return_value_discarded")
|
||||
assert_that.report_error(failure, line_number)
|
||||
return value
|
||||
|
||||
|
||||
# Waits for a specified signal sent from the <source> between idle frames and aborts with an error after the specified timeout has elapsed
|
||||
# source: the object from which the signal is emitted
|
||||
# signal_name: signal name
|
||||
# args: the expected signal arguments as an array
|
||||
# timeout: the timeout in ms, default is set to 2000ms
|
||||
func await_signal_idle_frames(source :Object, signal_name :String, args :Array = [], timeout_millis :int = 2000) -> Variant:
|
||||
var line_number := GdUnitAssertions.get_line_number()
|
||||
# fail fast if the given source instance invalid
|
||||
if not is_instance_valid(source):
|
||||
@warning_ignore("return_value_discarded")
|
||||
GdUnitAssertImpl.new(signal_name)\
|
||||
.report_error(GdAssertMessages.error_await_signal_on_invalid_instance(source, signal_name, args), line_number)
|
||||
return await await_idle_frame()
|
||||
var awaiter := GdUnitSignalAwaiter.new(timeout_millis, true)
|
||||
var value :Variant = await awaiter.on_signal(source, signal_name, args)
|
||||
if awaiter.is_interrupted():
|
||||
var failure := "await_signal_idle_frames(%s, %s) timed out after %sms" % [signal_name, args, timeout_millis]
|
||||
@warning_ignore("return_value_discarded")
|
||||
GdUnitAssertImpl.new(signal_name).report_error(failure, line_number)
|
||||
return value
|
||||
|
||||
|
||||
# Waits for for a given amount of milliseconds
|
||||
# example:
|
||||
# # waits for 100ms
|
||||
# await GdUnitAwaiter.await_millis(myNode, 100).completed
|
||||
# use this waiter and not `await get_tree().create_timer().timeout to prevent errors when a test case is timed out
|
||||
func await_millis(milliSec :int) -> void:
|
||||
var timer :Timer = Timer.new()
|
||||
timer.set_name("gdunit_await_millis_timer_%d" % timer.get_instance_id())
|
||||
(Engine.get_main_loop() as SceneTree).root.add_child(timer)
|
||||
timer.add_to_group("GdUnitTimers")
|
||||
timer.set_one_shot(true)
|
||||
timer.start(milliSec / 1000.0)
|
||||
await timer.timeout
|
||||
timer.queue_free()
|
||||
|
||||
|
||||
# Waits until the next idle frame
|
||||
func await_idle_frame() -> void:
|
||||
await (Engine.get_main_loop() as SceneTree).process_frame
|
||||
@@ -0,0 +1 @@
|
||||
uid://bvgpep6whjff4
|
||||
@@ -0,0 +1,35 @@
|
||||
## An Assertion Tool to verify boolean values
|
||||
@abstract class_name GdUnitBoolAssert
|
||||
extends GdUnitAssert
|
||||
|
||||
|
||||
## Verifies that the current value is null.
|
||||
@abstract func is_null() -> GdUnitBoolAssert
|
||||
|
||||
|
||||
## Verifies that the current value is not null.
|
||||
@abstract func is_not_null() -> GdUnitBoolAssert
|
||||
|
||||
|
||||
## Verifies that the current value is equal to the given one.
|
||||
@abstract func is_equal(expected: Variant) -> GdUnitBoolAssert
|
||||
|
||||
|
||||
## Verifies that the current value is not equal to the given one.
|
||||
@abstract func is_not_equal(expected: Variant) -> GdUnitBoolAssert
|
||||
|
||||
|
||||
## Overrides the default failure message by given custom message.
|
||||
@abstract func override_failure_message(message: String) -> GdUnitBoolAssert
|
||||
|
||||
|
||||
## Appends a custom message to the failure message.
|
||||
@abstract func append_failure_message(message: String) -> GdUnitBoolAssert
|
||||
|
||||
|
||||
## Verifies that the current value is true.
|
||||
@abstract func is_true() -> GdUnitBoolAssert
|
||||
|
||||
|
||||
## Verifies that the current value is false.
|
||||
@abstract func is_false() -> GdUnitBoolAssert
|
||||
@@ -0,0 +1 @@
|
||||
uid://bc1hypk7cebhc
|
||||
@@ -0,0 +1,10 @@
|
||||
class_name GdUnitConstants
|
||||
extends RefCounted
|
||||
|
||||
const NO_ARG :Variant = "<--null-->"
|
||||
|
||||
const EXPECT_ASSERT_REPORT_FAILURES := "expect_assert_report_failures"
|
||||
|
||||
## The maximum number of report history files to store
|
||||
const DEFAULT_REPORT_HISTORY_COUNT = 20
|
||||
const REPORT_DIR_PREFIX = "report_"
|
||||
@@ -0,0 +1 @@
|
||||
uid://cfmea6d6m2w8b
|
||||
@@ -0,0 +1,79 @@
|
||||
## An Assertion Tool to verify dictionary
|
||||
@abstract class_name GdUnitDictionaryAssert
|
||||
extends GdUnitAssert
|
||||
|
||||
|
||||
## Verifies that the current value is null.
|
||||
@abstract func is_null() -> GdUnitDictionaryAssert
|
||||
|
||||
|
||||
## Verifies that the current value is not null.
|
||||
@abstract func is_not_null() -> GdUnitDictionaryAssert
|
||||
|
||||
|
||||
## Verifies that the current dictionary is equal to the given one, ignoring order.
|
||||
@abstract func is_equal(expected: Variant) -> GdUnitDictionaryAssert
|
||||
|
||||
|
||||
## Verifies that the current dictionary is not equal to the given one, ignoring order.
|
||||
@abstract func is_not_equal(expected: Variant) -> GdUnitDictionaryAssert
|
||||
|
||||
|
||||
## Overrides the default failure message by given custom message.
|
||||
@abstract func override_failure_message(message: String) -> GdUnitDictionaryAssert
|
||||
|
||||
|
||||
## Appends a custom message to the failure message.
|
||||
@abstract func append_failure_message(message: String) -> GdUnitDictionaryAssert
|
||||
|
||||
|
||||
## Verifies that the current dictionary is empty, it has a size of 0.
|
||||
@abstract func is_empty() -> GdUnitDictionaryAssert
|
||||
|
||||
|
||||
## Verifies that the current dictionary is not empty, it has a size of minimum 1.
|
||||
@abstract func is_not_empty() -> GdUnitDictionaryAssert
|
||||
|
||||
|
||||
## Verifies that the current dictionary is the same. [br]
|
||||
## Compares the current by object reference equals
|
||||
@abstract func is_same(expected: Variant) -> GdUnitDictionaryAssert
|
||||
|
||||
|
||||
## Verifies that the current dictionary is NOT the same. [br]
|
||||
## Compares the current by object reference equals
|
||||
@abstract func is_not_same(expected: Variant) -> GdUnitDictionaryAssert
|
||||
|
||||
|
||||
## Verifies that the current dictionary has a size of given value.
|
||||
@abstract func has_size(expected: int) -> GdUnitDictionaryAssert
|
||||
|
||||
|
||||
## Verifies that the current dictionary contains the given key(s).[br]
|
||||
## The keys are compared by deep parameter comparision, for object reference compare you have to use [method contains_same_keys]
|
||||
@abstract func contains_keys(...expected: Array) -> GdUnitDictionaryAssert
|
||||
|
||||
|
||||
## Verifies that the current dictionary contains the given key and value.[br]
|
||||
## The key and value are compared by deep parameter comparision, for object reference compare you have to use [method contains_same_key_value]
|
||||
@abstract func contains_key_value(key: Variant, value: Variant) -> GdUnitDictionaryAssert
|
||||
|
||||
|
||||
## Verifies that the current dictionary not contains the given key(s).[br]
|
||||
## The keys are compared by deep parameter comparision, for object reference compare you have to use [method not_contains_same_keys]
|
||||
@abstract func not_contains_keys(...expected: Array) -> GdUnitDictionaryAssert
|
||||
|
||||
|
||||
## Verifies that the current dictionary contains the given key(s).[br]
|
||||
## The keys are compared by object reference, for deep parameter comparision use [method contains_keys]
|
||||
@abstract func contains_same_keys(expected: Array) -> GdUnitDictionaryAssert
|
||||
|
||||
|
||||
## Verifies that the current dictionary contains the given key and value.[br]
|
||||
## The key and value are compared by object reference, for deep parameter comparision use [method contains_key_value]
|
||||
@abstract func contains_same_key_value(key: Variant, value: Variant) -> GdUnitDictionaryAssert
|
||||
|
||||
|
||||
## Verifies that the current dictionary not contains the given key(s).
|
||||
## The keys are compared by object reference, for deep parameter comparision use [method not_contains_keys]
|
||||
@abstract func not_contains_same_keys(...expected: Array) -> GdUnitDictionaryAssert
|
||||
@@ -0,0 +1 @@
|
||||
uid://bc11e0x8ln3sy
|
||||
@@ -0,0 +1,52 @@
|
||||
## An assertion tool to verify GDUnit asserts.
|
||||
## This assert is for internal use only, to verify that failed asserts work as expected.
|
||||
@abstract class_name GdUnitFailureAssert
|
||||
extends GdUnitAssert
|
||||
|
||||
|
||||
## Verifies that the current value is null.
|
||||
@abstract func is_null() -> GdUnitFailureAssert
|
||||
|
||||
|
||||
## Verifies that the current value is not null.
|
||||
@abstract func is_not_null() -> GdUnitFailureAssert
|
||||
|
||||
|
||||
## Verifies that the current value is equal to the given one.
|
||||
@abstract func is_equal(expected: Variant) -> GdUnitFailureAssert
|
||||
|
||||
|
||||
## Verifies that the current value is not equal to expected one.
|
||||
@abstract func is_not_equal(expected: Variant) -> GdUnitFailureAssert
|
||||
|
||||
|
||||
## Overrides the default failure message by given custom message.
|
||||
@abstract func override_failure_message(message: String) -> GdUnitFailureAssert
|
||||
|
||||
|
||||
## Appends a custom message to the failure message.
|
||||
@abstract func append_failure_message(message: String) -> GdUnitFailureAssert
|
||||
|
||||
|
||||
## Verifies if the executed assert was successful
|
||||
@abstract func is_success() -> GdUnitFailureAssert
|
||||
|
||||
|
||||
## Verifies if the executed assert has failed
|
||||
@abstract func is_failed() -> GdUnitFailureAssert
|
||||
|
||||
|
||||
## Verifies the failure line is equal to expected one.
|
||||
@abstract func has_line(expected: int) -> GdUnitFailureAssert
|
||||
|
||||
|
||||
## Verifies the failure message is equal to expected one.
|
||||
@abstract func has_message(expected: String) -> GdUnitFailureAssert
|
||||
|
||||
|
||||
## Verifies that the failure message starts with the expected message.
|
||||
@abstract func starts_with_message(expected: String) -> GdUnitFailureAssert
|
||||
|
||||
|
||||
## Verifies that the failure message contains the expected message.
|
||||
@abstract func contains_message(expected: String) -> GdUnitFailureAssert
|
||||
@@ -0,0 +1 @@
|
||||
uid://bty5fyx14c0l4
|
||||
@@ -0,0 +1,38 @@
|
||||
@abstract class_name GdUnitFileAssert
|
||||
extends GdUnitAssert
|
||||
|
||||
|
||||
## Verifies that the current value is null.
|
||||
@abstract func is_null() -> GdUnitFileAssert
|
||||
|
||||
|
||||
## Verifies that the current value is not null.
|
||||
@abstract func is_not_null() -> GdUnitFileAssert
|
||||
|
||||
|
||||
## Verifies that the current value is equal to the given one.
|
||||
@abstract func is_equal(expected: Variant) -> GdUnitFileAssert
|
||||
|
||||
|
||||
## Verifies that the current value is not equal to expected one.
|
||||
@abstract func is_not_equal(expected: Variant) -> GdUnitFileAssert
|
||||
|
||||
|
||||
## Overrides the default failure message by given custom message.
|
||||
@abstract func override_failure_message(message: String) -> GdUnitFileAssert
|
||||
|
||||
|
||||
## Appends a custom message to the failure message.
|
||||
@abstract func append_failure_message(message: String) -> GdUnitFileAssert
|
||||
|
||||
|
||||
@abstract func is_file() -> GdUnitFileAssert
|
||||
|
||||
|
||||
@abstract func exists() -> GdUnitFileAssert
|
||||
|
||||
|
||||
@abstract func is_script() -> GdUnitFileAssert
|
||||
|
||||
|
||||
@abstract func contains_exactly(expected_rows :Array) -> GdUnitFileAssert
|
||||
@@ -0,0 +1 @@
|
||||
uid://8fd3gw48ntfg
|
||||
@@ -0,0 +1,75 @@
|
||||
## An Assertion Tool to verify float values
|
||||
@abstract class_name GdUnitFloatAssert
|
||||
extends GdUnitAssert
|
||||
|
||||
|
||||
## Verifies that the current value is null.
|
||||
@abstract func is_null() -> GdUnitFloatAssert
|
||||
|
||||
|
||||
## Verifies that the current value is not null.
|
||||
@abstract func is_not_null() -> GdUnitFloatAssert
|
||||
|
||||
|
||||
## Verifies that the current value is equal to the given one.
|
||||
@abstract func is_equal(expected: Variant) -> GdUnitFloatAssert
|
||||
|
||||
|
||||
## Verifies that the current value is not equal to expected one.
|
||||
@abstract func is_not_equal(expected: Variant) -> GdUnitFloatAssert
|
||||
|
||||
|
||||
## Verifies that the current and expected value are approximately equal.
|
||||
@abstract func is_equal_approx(expected: float, approx: float) -> GdUnitFloatAssert
|
||||
|
||||
|
||||
## Overrides the default failure message by given custom message.
|
||||
@abstract func override_failure_message(message: String) -> GdUnitFloatAssert
|
||||
|
||||
|
||||
## Appends a custom message to the failure message.
|
||||
@abstract func append_failure_message(message: String) -> GdUnitFloatAssert
|
||||
|
||||
|
||||
## Verifies that the current value is less than the given one.
|
||||
@abstract func is_less(expected: float) -> GdUnitFloatAssert
|
||||
|
||||
|
||||
## Verifies that the current value is less than or equal the given one.
|
||||
@abstract func is_less_equal(expected: float) -> GdUnitFloatAssert
|
||||
|
||||
|
||||
## Verifies that the current value is greater than the given one.
|
||||
@abstract func is_greater(expected: float) -> GdUnitFloatAssert
|
||||
|
||||
|
||||
## Verifies that the current value is greater than or equal the given one.
|
||||
@abstract func is_greater_equal(expected: float) -> GdUnitFloatAssert
|
||||
|
||||
|
||||
## Verifies that the current value is negative.
|
||||
@abstract func is_negative() -> GdUnitFloatAssert
|
||||
|
||||
|
||||
## Verifies that the current value is not negative.
|
||||
@abstract func is_not_negative() -> GdUnitFloatAssert
|
||||
|
||||
|
||||
## Verifies that the current value is equal to zero.
|
||||
@abstract func is_zero() -> GdUnitFloatAssert
|
||||
|
||||
|
||||
## Verifies that the current value is not equal to zero.
|
||||
@abstract func is_not_zero() -> GdUnitFloatAssert
|
||||
|
||||
|
||||
## Verifies that the current value is in the given set of values.
|
||||
@abstract func is_in(expected: Array) -> GdUnitFloatAssert
|
||||
|
||||
|
||||
## Verifies that the current value is not in the given set of values.
|
||||
@abstract func is_not_in(expected: Array) -> GdUnitFloatAssert
|
||||
|
||||
|
||||
## Verifies that the current value is between the given boundaries (inclusive).
|
||||
@abstract func is_between(from: float, to: float) -> GdUnitFloatAssert
|
||||
@@ -0,0 +1 @@
|
||||
uid://l2bllxln5gbk
|
||||
@@ -0,0 +1,42 @@
|
||||
## An Assertion Tool to verify function callback values
|
||||
@abstract class_name GdUnitFuncAssert
|
||||
extends GdUnitAssert
|
||||
|
||||
|
||||
## Verifies that the current value is null.
|
||||
@abstract func is_null() -> GdUnitFuncAssert
|
||||
|
||||
|
||||
## Verifies that the current value is not null.
|
||||
@abstract func is_not_null() -> GdUnitFuncAssert
|
||||
|
||||
|
||||
## Verifies that the current value is equal to the given one.
|
||||
@abstract func is_equal(expected: Variant) -> GdUnitFuncAssert
|
||||
|
||||
|
||||
## Verifies that the current value is not equal to expected one.
|
||||
@abstract func is_not_equal(expected: Variant) -> GdUnitFuncAssert
|
||||
|
||||
|
||||
## Overrides the default failure message by given custom message.
|
||||
@abstract func override_failure_message(message: String) -> GdUnitFuncAssert
|
||||
|
||||
|
||||
## Appends a custom message to the failure message.
|
||||
@abstract func append_failure_message(message: String) -> GdUnitFuncAssert
|
||||
|
||||
|
||||
## Verifies that the current value is true.
|
||||
@abstract func is_true() -> GdUnitFuncAssert
|
||||
|
||||
|
||||
## Verifies that the current value is false.
|
||||
@abstract func is_false() -> GdUnitFuncAssert
|
||||
|
||||
|
||||
## Sets the timeout in ms to wait the function returnd the expected value, if the time over a failure is emitted.[br]
|
||||
## e.g.[br]
|
||||
## do wait until 5s the function `is_state` is returns 10 [br]
|
||||
## [code]assert_func(instance, "is_state").wait_until(5000).is_equal(10)[/code]
|
||||
@abstract func wait_until(timeout: int) -> GdUnitFuncAssert
|
||||
@@ -0,0 +1 @@
|
||||
uid://6x3tm35nv2jp
|
||||
@@ -0,0 +1,59 @@
|
||||
## An assertion tool to verify for Godot runtime errors like assert() and push notifications like push_error().
|
||||
@abstract class_name GdUnitGodotErrorAssert
|
||||
extends GdUnitAssert
|
||||
|
||||
|
||||
## Verifies that the current value is null.
|
||||
@abstract func is_null() -> GdUnitGodotErrorAssert
|
||||
|
||||
|
||||
## Verifies that the current value is not null.
|
||||
@abstract func is_not_null() -> GdUnitGodotErrorAssert
|
||||
|
||||
|
||||
## Verifies that the current value is equal to the given one.
|
||||
@abstract func is_equal(expected: Variant) -> GdUnitGodotErrorAssert
|
||||
|
||||
|
||||
## Verifies that the current value is not equal to expected one.
|
||||
@abstract func is_not_equal(expected: Variant) -> GdUnitGodotErrorAssert
|
||||
|
||||
|
||||
## Overrides the default failure message by given custom message.
|
||||
@abstract func override_failure_message(message: String) -> GdUnitGodotErrorAssert
|
||||
|
||||
|
||||
## Appends a custom message to the failure message.
|
||||
@abstract func append_failure_message(message: String) -> GdUnitGodotErrorAssert
|
||||
|
||||
|
||||
## Verifies if the executed code runs without any runtime errors
|
||||
## Usage:
|
||||
## [codeblock]
|
||||
## await assert_error(<callable>).is_success()
|
||||
## [/codeblock]
|
||||
@abstract func is_success() -> GdUnitGodotErrorAssert
|
||||
|
||||
|
||||
## Verifies if the executed code runs into a runtime error
|
||||
## Usage:
|
||||
## [codeblock]
|
||||
## await assert_error(<callable>).is_runtime_error(<expected error message>)
|
||||
## [/codeblock]
|
||||
@abstract func is_runtime_error(expected_error: Variant) -> GdUnitGodotErrorAssert
|
||||
|
||||
|
||||
## Verifies if the executed code has a push_warning() used
|
||||
## Usage:
|
||||
## [codeblock]
|
||||
## await assert_error(<callable>).is_push_warning(<expected push warning message>)
|
||||
## [/codeblock]
|
||||
@abstract func is_push_warning(expected_warning: Variant) -> GdUnitGodotErrorAssert
|
||||
|
||||
|
||||
## Verifies if the executed code has a push_error() used
|
||||
## Usage:
|
||||
## [codeblock]
|
||||
## await assert_error(<callable>).is_push_error(<expected push error message>)
|
||||
## [/codeblock]
|
||||
@abstract func is_push_error(expected_error: Variant) -> GdUnitGodotErrorAssert
|
||||
@@ -0,0 +1 @@
|
||||
uid://csta4hbrodnq3
|
||||
@@ -0,0 +1,79 @@
|
||||
## An Assertion Tool to verify integer values
|
||||
@abstract class_name GdUnitIntAssert
|
||||
extends GdUnitAssert
|
||||
|
||||
|
||||
## Verifies that the current value is null.
|
||||
@abstract func is_null() -> GdUnitIntAssert
|
||||
|
||||
|
||||
## Verifies that the current value is not null.
|
||||
@abstract func is_not_null() -> GdUnitIntAssert
|
||||
|
||||
|
||||
## Verifies that the current value is equal to the given one.
|
||||
@abstract func is_equal(expected: Variant) -> GdUnitIntAssert
|
||||
|
||||
|
||||
## Verifies that the current value is not equal to expected one.
|
||||
@abstract func is_not_equal(expected: Variant) -> GdUnitIntAssert
|
||||
|
||||
|
||||
## Overrides the default failure message by given custom message.
|
||||
@abstract func override_failure_message(message: String) -> GdUnitIntAssert
|
||||
|
||||
|
||||
## Appends a custom message to the failure message.
|
||||
@abstract func append_failure_message(message: String) -> GdUnitIntAssert
|
||||
|
||||
|
||||
## Verifies that the current value is less than the given one.
|
||||
@abstract func is_less(expected: int) -> GdUnitIntAssert
|
||||
|
||||
|
||||
## Verifies that the current value is less than or equal the given one.
|
||||
@abstract func is_less_equal(expected: int) -> GdUnitIntAssert
|
||||
|
||||
|
||||
## Verifies that the current value is greater than the given one.
|
||||
@abstract func is_greater(expected: int) -> GdUnitIntAssert
|
||||
|
||||
|
||||
## Verifies that the current value is greater than or equal the given one.
|
||||
@abstract func is_greater_equal(expected: int) -> GdUnitIntAssert
|
||||
|
||||
|
||||
## Verifies that the current value is even.
|
||||
@abstract func is_even() -> GdUnitIntAssert
|
||||
|
||||
|
||||
## Verifies that the current value is odd.
|
||||
@abstract func is_odd() -> GdUnitIntAssert
|
||||
|
||||
|
||||
## Verifies that the current value is negative.
|
||||
@abstract func is_negative() -> GdUnitIntAssert
|
||||
|
||||
|
||||
## Verifies that the current value is not negative.
|
||||
@abstract func is_not_negative() -> GdUnitIntAssert
|
||||
|
||||
|
||||
## Verifies that the current value is equal to zero.
|
||||
@abstract func is_zero() -> GdUnitIntAssert
|
||||
|
||||
|
||||
## Verifies that the current value is not equal to zero.
|
||||
@abstract func is_not_zero() -> GdUnitIntAssert
|
||||
|
||||
|
||||
## Verifies that the current value is in the given set of values.
|
||||
@abstract func is_in(expected: Array) -> GdUnitIntAssert
|
||||
|
||||
|
||||
## Verifies that the current value is not in the given set of values.
|
||||
@abstract func is_not_in(expected: Array) -> GdUnitIntAssert
|
||||
|
||||
|
||||
## Verifies that the current value is between the given boundaries (inclusive).
|
||||
@abstract func is_between(from: int, to: int) -> GdUnitIntAssert
|
||||
@@ -0,0 +1 @@
|
||||
uid://bn8v4t4wxb04u
|
||||
@@ -0,0 +1,51 @@
|
||||
## An Assertion Tool to verify Object values
|
||||
@abstract class_name GdUnitObjectAssert
|
||||
extends GdUnitAssert
|
||||
|
||||
|
||||
## Verifies that the current value is null.
|
||||
@abstract func is_null() -> GdUnitObjectAssert
|
||||
|
||||
|
||||
## Verifies that the current value is not null.
|
||||
@abstract func is_not_null() -> GdUnitObjectAssert
|
||||
|
||||
|
||||
## Verifies that the current value is equal to the given one.
|
||||
@abstract func is_equal(expected: Variant) -> GdUnitObjectAssert
|
||||
|
||||
|
||||
## Verifies that the current value is not equal to expected one.
|
||||
@abstract func is_not_equal(expected: Variant) -> GdUnitObjectAssert
|
||||
|
||||
|
||||
## Overrides the default failure message by given custom message.
|
||||
@abstract func override_failure_message(message: String) -> GdUnitObjectAssert
|
||||
|
||||
|
||||
## Appends a custom message to the failure message.
|
||||
@abstract func append_failure_message(message: String) -> GdUnitObjectAssert
|
||||
|
||||
|
||||
## Verifies that the current object is the same as the given one.
|
||||
@abstract func is_same(expected: Variant) -> GdUnitObjectAssert
|
||||
|
||||
|
||||
## Verifies that the current object is not the same as the given one.
|
||||
@abstract func is_not_same(expected: Variant) -> GdUnitObjectAssert
|
||||
|
||||
|
||||
## Verifies that the current object is an instance of the given type.
|
||||
@abstract func is_instanceof(type: Variant) -> GdUnitObjectAssert
|
||||
|
||||
|
||||
## Verifies that the current object is not an instance of the given type.
|
||||
@abstract func is_not_instanceof(type: Variant) -> GdUnitObjectAssert
|
||||
|
||||
|
||||
## Checks whether the current object inherits from the specified type.
|
||||
@abstract func is_inheriting(type: Variant) -> GdUnitObjectAssert
|
||||
|
||||
|
||||
## Checks whether the current object does NOT inherit from the specified type.
|
||||
@abstract func is_not_inheriting(type: Variant) -> GdUnitObjectAssert
|
||||
@@ -0,0 +1 @@
|
||||
uid://cjv125sos5js0
|
||||
@@ -0,0 +1,51 @@
|
||||
## An Assertion Tool to verify Results
|
||||
@abstract class_name GdUnitResultAssert
|
||||
extends GdUnitAssert
|
||||
|
||||
|
||||
## Verifies that the current value is null.
|
||||
@abstract func is_null() -> GdUnitResultAssert
|
||||
|
||||
|
||||
## Verifies that the current value is not null.
|
||||
@abstract func is_not_null() -> GdUnitResultAssert
|
||||
|
||||
|
||||
## Verifies that the current value is equal to the given one.
|
||||
@abstract func is_equal(expected: Variant) -> GdUnitResultAssert
|
||||
|
||||
|
||||
## Verifies that the current value is not equal to expected one.
|
||||
@abstract func is_not_equal(expected: Variant) -> GdUnitResultAssert
|
||||
|
||||
|
||||
## Overrides the default failure message by given custom message.
|
||||
@abstract func override_failure_message(message: String) -> GdUnitResultAssert
|
||||
|
||||
|
||||
## Appends a custom message to the failure message.
|
||||
@abstract func append_failure_message(message: String) -> GdUnitResultAssert
|
||||
|
||||
|
||||
## Verifies that the result is ends up with empty
|
||||
@abstract func is_empty() -> GdUnitResultAssert
|
||||
|
||||
|
||||
## Verifies that the result is ends up with success
|
||||
@abstract func is_success() -> GdUnitResultAssert
|
||||
|
||||
|
||||
## Verifies that the result is ends up with warning
|
||||
@abstract func is_warning() -> GdUnitResultAssert
|
||||
|
||||
|
||||
## Verifies that the result is ends up with error
|
||||
@abstract func is_error() -> GdUnitResultAssert
|
||||
|
||||
|
||||
## Verifies that the result contains the given message
|
||||
@abstract func contains_message(expected: String) -> GdUnitResultAssert
|
||||
|
||||
|
||||
## Verifies that the result contains the given value
|
||||
@abstract func is_value(expected: Variant) -> GdUnitResultAssert
|
||||
@@ -0,0 +1 @@
|
||||
uid://dt8rg6uisd0ug
|
||||
@@ -0,0 +1,350 @@
|
||||
## The Scene Runner is a tool used for simulating interactions on a scene.
|
||||
## With this tool, you can simulate input events such as keyboard or mouse input and/or simulate scene processing over a certain number of frames.
|
||||
## This tool is typically used for integration testing a scene.
|
||||
@abstract class_name GdUnitSceneRunner
|
||||
extends RefCounted
|
||||
|
||||
|
||||
## Simulates that an action has been pressed.[br]
|
||||
## [member action] : the action e.g. [code]"ui_up"[/code][br]
|
||||
## [member event_index] : [url=https://docs.godotengine.org/en/4.4/classes/class_inputeventaction.html#class-inputeventaction-property-event-index]default=-1[/url][br]
|
||||
@abstract func simulate_action_pressed(action: String, event_index := -1) -> GdUnitSceneRunner
|
||||
|
||||
|
||||
## Simulates that an action is pressed.[br]
|
||||
## [member action] : the action e.g. [code]"ui_up"[/code][br]
|
||||
## [member event_index] : [url=https://docs.godotengine.org/en/4.4/classes/class_inputeventaction.html#class-inputeventaction-property-event-index]default=-1[/url][br]
|
||||
@abstract func simulate_action_press(action: String, event_index := -1) -> GdUnitSceneRunner
|
||||
|
||||
|
||||
## Simulates that an action has been released.[br]
|
||||
## [member action] : the action e.g. [code]"ui_up"[/code][br]
|
||||
## [member event_index] : [url=https://docs.godotengine.org/en/4.4/classes/class_inputeventaction.html#class-inputeventaction-property-event-index]default=-1[/url][br]
|
||||
@abstract func simulate_action_release(action: String, event_index := -1) -> GdUnitSceneRunner
|
||||
|
||||
|
||||
## Simulates that a key has been pressed.[br]
|
||||
## @deprecated: the modifier [b]shift_pressed[/b] and [b]ctrl_pressed[/b] will be removed in v7.0
|
||||
## [member key_code] : the key code e.g. [constant KEY_ENTER][br]
|
||||
## [codeblock]
|
||||
## func test_key_presssed():
|
||||
## var runner = scene_runner("res://scenes/simple_scene.tscn")
|
||||
## await runner.simulate_key_pressed(KEY_SPACE)
|
||||
## [/codeblock]
|
||||
@abstract func simulate_key_pressed(key_code: int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner
|
||||
|
||||
|
||||
## Simulates that a key is pressing.[br]
|
||||
## @deprecated: the modifier [b]shift_pressed[/b] and [b]ctrl_pressed[/b] will be removed in v7.0[br]See `test_key_shift_and_A_presssing` for example using key combinations
|
||||
## [member key_code] : the key code e.g. [constant KEY_ENTER][br]
|
||||
## [codeblock]
|
||||
## # Do simulate key pressing A
|
||||
## func test_key_A_presssing():
|
||||
## var runner = scene_runner("res://scenes/simple_scene.tscn")
|
||||
## await runner.simulate_key_press(KEY_A)
|
||||
##
|
||||
##
|
||||
## # Do simulate keycombination pressing shift+A
|
||||
## func test_key_shift_and_A_presssing():
|
||||
## var runner = scene_runner("res://scenes/simple_scene.tscn")
|
||||
## runner.simulate_key_press(KEY_SHIFT)
|
||||
## runner.simulate_key_press(KEY_A)
|
||||
## await _runner.await_input_processed()
|
||||
## [/codeblock]
|
||||
@abstract func simulate_key_press(key_code: int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner
|
||||
|
||||
|
||||
## Simulates that a key has been released.[br]
|
||||
## [member key_code] : the key code e.g. [constant KEY_ENTER][br]
|
||||
## [codeblock]
|
||||
## # Do simulate releasing key A
|
||||
## func test_key_A_releasing():
|
||||
## var runner = scene_runner("res://scenes/simple_scene.tscn")
|
||||
## await runner.simulate_key_release(KEY_A)
|
||||
##
|
||||
##
|
||||
## # Do simulate keycombination pressing shift+A
|
||||
## func test_key_shift_and_A_releasing(():
|
||||
## var runner = scene_runner("res://scenes/simple_scene.tscn")
|
||||
## runner.simulate_key_release(KEY_SHIFT)
|
||||
## runner.simulate_key_release(KEY_A)
|
||||
## await _runner.await_input_processed()
|
||||
## [/codeblock]
|
||||
## @deprecated: the modifier [b]shift_pressed[/b] and [b]ctrl_pressed[/b] will be removed in v7.0[br]See `test_key_shift_and_A_releasing` for example using key combinations
|
||||
@abstract func simulate_key_release(key_code: int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner
|
||||
|
||||
|
||||
## Sets the mouse position to the specified vector, provided in pixels and relative to an origin at the upper left corner of the currently focused Window Manager game window.[br]
|
||||
## [member position] : The absolute position in pixels as Vector2
|
||||
@abstract func set_mouse_position(position: Vector2) -> GdUnitSceneRunner
|
||||
|
||||
|
||||
## Returns the mouse's position in this Viewport using the coordinate system of this Viewport.
|
||||
@abstract func get_mouse_position() -> Vector2
|
||||
|
||||
|
||||
## Gets the current global mouse position of the current window
|
||||
@abstract func get_global_mouse_position() -> Vector2
|
||||
|
||||
|
||||
## Simulates a mouse moved to final position.[br]
|
||||
## [member position] : The final mouse position
|
||||
@abstract func simulate_mouse_move(position: Vector2) -> GdUnitSceneRunner
|
||||
|
||||
|
||||
## Simulates a mouse move to the relative coordinates (offset).[br]
|
||||
## [color=yellow]You must use [b]await[/b] to wait until the simulated mouse movement is complete.[/color][br]
|
||||
## [br]
|
||||
## [member relative] : The relative position, indicating the mouse position offset.[br]
|
||||
## [member time] : The time to move the mouse by the relative position in seconds (default is 1 second).[br]
|
||||
## [member trans_type] : Sets the type of transition used (default is TRANS_LINEAR).[br]
|
||||
## [codeblock]
|
||||
## func test_move_mouse():
|
||||
## var runner = scene_runner("res://scenes/simple_scene.tscn")
|
||||
## await runner.simulate_mouse_move_relative(Vector2(100,100))
|
||||
## [/codeblock]
|
||||
@abstract func simulate_mouse_move_relative(relative: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner
|
||||
|
||||
|
||||
## Simulates a mouse move to the absolute coordinates.[br]
|
||||
## [color=yellow]You must use [b]await[/b] to wait until the simulated mouse movement is complete.[/color][br]
|
||||
## [br]
|
||||
## [member position] : The final position of the mouse.[br]
|
||||
## [member time] : The time to move the mouse to the final position in seconds (default is 1 second).[br]
|
||||
## [member trans_type] : Sets the type of transition used (default is TRANS_LINEAR).[br]
|
||||
## [codeblock]
|
||||
## func test_move_mouse():
|
||||
## var runner = scene_runner("res://scenes/simple_scene.tscn")
|
||||
## await runner.simulate_mouse_move_absolute(Vector2(100,100))
|
||||
## [/codeblock]
|
||||
@abstract func simulate_mouse_move_absolute(position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner
|
||||
|
||||
|
||||
## Simulates a mouse button pressed.[br]
|
||||
## [member button_index] : The mouse button identifier, one of the [enum MouseButton] or button wheel constants.
|
||||
## [member double_click] : Set to true to simulate a double-click
|
||||
@abstract func simulate_mouse_button_pressed(button_index: MouseButton, double_click := false) -> GdUnitSceneRunner
|
||||
|
||||
|
||||
## Simulates a mouse button press (holding)[br]
|
||||
## [member button_index] : The mouse button identifier, one of the [enum MouseButton] or button wheel constants.
|
||||
## [member double_click] : Set to true to simulate a double-click
|
||||
@abstract func simulate_mouse_button_press(button_index: MouseButton, double_click := false) -> GdUnitSceneRunner
|
||||
|
||||
|
||||
## Simulates a mouse button released.[br]
|
||||
## [member button_index] : The mouse button identifier, one of the [enum MouseButton] or button wheel constants.
|
||||
@abstract func simulate_mouse_button_release(button_index: MouseButton) -> GdUnitSceneRunner
|
||||
|
||||
|
||||
## Simulates a screen touch is pressed.[br]
|
||||
## [member index] : The touch index in the case of a multi-touch event.[br]
|
||||
## [member position] : The position to touch the screen.[br]
|
||||
## [member double_tap] : If true, the touch's state is a double tab.
|
||||
@abstract func simulate_screen_touch_pressed(index: int, position: Vector2, double_tap := false) -> GdUnitSceneRunner
|
||||
|
||||
|
||||
## Simulates a screen touch press without releasing it immediately, effectively simulating a "hold" action.[br]
|
||||
## [member index] : The touch index in the case of a multi-touch event.[br]
|
||||
## [member position] : The position to touch the screen.[br]
|
||||
## [member double_tap] : If true, the touch's state is a double tab.
|
||||
@abstract func simulate_screen_touch_press(index: int, position: Vector2, double_tap := false) -> GdUnitSceneRunner
|
||||
|
||||
|
||||
## Simulates a screen touch is released.[br]
|
||||
## [member index] : The touch index in the case of a multi-touch event.[br]
|
||||
## [member double_tap] : If true, the touch's state is a double tab.
|
||||
@abstract func simulate_screen_touch_release(index: int, double_tap := false) -> GdUnitSceneRunner
|
||||
|
||||
|
||||
## Simulates a touch drag and drop event to a relative position.[br]
|
||||
## [color=yellow]You must use [b]await[/b] to wait until the simulated drag&drop is complete.[/color][br]
|
||||
## [br]
|
||||
## [member index] : The touch index in the case of a multi-touch event.[br]
|
||||
## [member relative] : The relative position, indicating the drag&drop position offset.[br]
|
||||
## [member time] : The time to move to the relative position in seconds (default is 1 second).[br]
|
||||
## [member trans_type] : Sets the type of transition used (default is TRANS_LINEAR).[br]
|
||||
## [codeblock]
|
||||
## func test_touch_drag_drop():
|
||||
## var runner = scene_runner("res://scenes/simple_scene.tscn")
|
||||
## # start drag at position 50,50
|
||||
## runner.simulate_screen_touch_drag_begin(1, Vector2(50, 50))
|
||||
## # and drop it at final at 150,50 relative (50,50 + 100,0)
|
||||
## await runner.simulate_screen_touch_drag_relative(1, Vector2(100,0))
|
||||
## [/codeblock]
|
||||
@abstract func simulate_screen_touch_drag_relative(index: int, relative: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner
|
||||
|
||||
|
||||
## Simulates a touch screen drop to the absolute coordinates (offset).[br]
|
||||
## [color=yellow]You must use [b]await[/b] to wait until the simulated drop is complete.[/color][br]
|
||||
## [br]
|
||||
## [member index] : The touch index in the case of a multi-touch event.[br]
|
||||
## [member position] : The final position, indicating the drop position.[br]
|
||||
## [member time] : The time to move to the final position in seconds (default is 1 second).[br]
|
||||
## [member trans_type] : Sets the type of transition used (default is TRANS_LINEAR).[br]
|
||||
## [codeblock]
|
||||
## func test_touch_drag_drop():
|
||||
## var runner = scene_runner("res://scenes/simple_scene.tscn")
|
||||
## # start drag at position 50,50
|
||||
## runner.simulate_screen_touch_drag_begin(1, Vector2(50, 50))
|
||||
## # and drop it at 100,50
|
||||
## await runner.simulate_screen_touch_drag_absolute(1, Vector2(100,50))
|
||||
## [/codeblock]
|
||||
@abstract func simulate_screen_touch_drag_absolute(index: int, position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner
|
||||
|
||||
|
||||
## Simulates a complete drag and drop event from one position to another.[br]
|
||||
## This is ideal for testing complex drag-and-drop scenarios that require a specific start and end position.[br]
|
||||
## [color=yellow]You must use [b]await[/b] to wait until the simulated drop is complete.[/color][br]
|
||||
## [br]
|
||||
## [member index] : The touch index in the case of a multi-touch event.[br]
|
||||
## [member position] : The drag start position, indicating the drag position.[br]
|
||||
## [member drop_position] : The drop position, indicating the drop position.[br]
|
||||
## [member time] : The time to move to the final position in seconds (default is 1 second).[br]
|
||||
## [member trans_type] : Sets the type of transition used (default is TRANS_LINEAR).[br]
|
||||
## [codeblock]
|
||||
## func test_touch_drag_drop():
|
||||
## var runner = scene_runner("res://scenes/simple_scene.tscn")
|
||||
## # start drag at position 50,50 and drop it at 100,50
|
||||
## await runner.simulate_screen_touch_drag_drop(1, Vector2(50, 50), Vector2(100,50))
|
||||
## [/codeblock]
|
||||
@abstract func simulate_screen_touch_drag_drop(index: int, position: Vector2, drop_position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner
|
||||
|
||||
|
||||
## Simulates a touch screen drag event to given position.[br]
|
||||
## [member index] : The touch index in the case of a multi-touch event.[br]
|
||||
## [member position] : The drag start position, indicating the drag position.[br]
|
||||
@abstract func simulate_screen_touch_drag(index: int, position: Vector2) -> GdUnitSceneRunner
|
||||
|
||||
|
||||
## Returns the actual position of the touchscreen drag position by given index.
|
||||
## [member index] : The touch index in the case of a multi-touch event.[br]
|
||||
@abstract func get_screen_touch_drag_position(index: int) -> Vector2
|
||||
|
||||
|
||||
## Sets how fast or slow the scene simulation is processed (clock ticks versus the real).[br]
|
||||
## It defaults to 1.0. A value of 2.0 means the game moves twice as fast as real life,
|
||||
## whilst a value of 0.5 means the game moves at half the regular speed.
|
||||
## [member time_factor] : A float representing the simulation speed.[br]
|
||||
## - Default is 1.0, meaning the simulation runs at normal speed.[br]
|
||||
## - A value of 2.0 means the simulation runs twice as fast as real time.[br]
|
||||
## - A value of 0.5 means the simulation runs at half the regular speed.[br]
|
||||
@abstract func set_time_factor(time_factor: float = 1.0) -> GdUnitSceneRunner
|
||||
|
||||
|
||||
## Simulates scene processing for a certain number of frames.[br]
|
||||
## [member frames] : amount of frames to process[br]
|
||||
## [member delta_milli] : the time delta between a frame in milliseconds
|
||||
@abstract func simulate_frames(frames: int, delta_milli: int = -1) -> GdUnitSceneRunner
|
||||
|
||||
|
||||
## Simulates scene processing until the given signal is emitted by the scene.[br]
|
||||
## [member signal_name] : the signal to stop the simulation[br]
|
||||
## [member args] : optional signal arguments to be matched for stop[br]
|
||||
@abstract func simulate_until_signal(signal_name: String, ...args: Array) -> GdUnitSceneRunner
|
||||
|
||||
|
||||
## Simulates scene processing until the given signal is emitted by the given object.[br]
|
||||
## [member source] : the object that should emit the signal[br]
|
||||
## [member signal_name] : the signal to stop the simulation[br]
|
||||
## [member args] : optional signal arguments to be matched for stop
|
||||
@abstract func simulate_until_object_signal(source: Object, signal_name: String, ...args: Array) -> GdUnitSceneRunner
|
||||
|
||||
|
||||
## Waits for all input events to be processed by flushing any buffered input events
|
||||
## and then awaiting a full cycle of both the process and physics frames.[br]
|
||||
## [br]
|
||||
## This is typically used to ensure that any simulated or queued inputs are fully
|
||||
## processed before proceeding with the next steps in the scene.[br]
|
||||
## It's essential for reliable input simulation or when synchronizing logic based
|
||||
## on inputs.[br]
|
||||
##
|
||||
## Usage Example:
|
||||
## [codeblock]
|
||||
## await await_input_processed() # Ensure all inputs are processed before continuing
|
||||
## [/codeblock]
|
||||
@abstract func await_input_processed() -> void
|
||||
|
||||
|
||||
## The await_func function pauses execution until a specified function in the scene returns a value.[br]
|
||||
## It returns a [GdUnitFuncAssert], which provides a suite of assertion methods to verify the returned value.[br]
|
||||
## [member func_name] : The name of the function to wait for.[br]
|
||||
## [member args] : Optional function arguments
|
||||
## [br]
|
||||
## Usage Example:
|
||||
## [codeblock]
|
||||
## # Waits for 'calculate_score' function and verifies the result is equal to 100.
|
||||
## await_func("calculate_score").is_equal(100)
|
||||
## [/codeblock]
|
||||
@abstract func await_func(func_name: String, ...args: Array) -> GdUnitFuncAssert
|
||||
|
||||
|
||||
## The await_func_on function extends the functionality of await_func by allowing you to specify a source node within the scene.[br]
|
||||
## It waits for a specified function on that node to return a value and returns a [GdUnitFuncAssert] object for assertions.[br]
|
||||
## [member source] : The object where implements the function.[br]
|
||||
## [member func_name] : The name of the function to wait for.[br]
|
||||
## [member args] : optional function arguments
|
||||
## [br]
|
||||
## Usage Example:
|
||||
## [codeblock]
|
||||
## # Waits for 'calculate_score' function and verifies the result is equal to 100.
|
||||
## var my_instance := ScoreCalculator.new()
|
||||
## await_func(my_instance, "calculate_score").is_equal(100)
|
||||
## [/codeblock]
|
||||
@abstract func await_func_on(source: Object, func_name: String, ...args: Array) -> GdUnitFuncAssert
|
||||
|
||||
|
||||
## Waits for the specified signal to be emitted by the scene. If the signal is not emitted within the given timeout, the operation fails.[br]
|
||||
## [member signal_name] : The name of the signal to wait for[br]
|
||||
## [member args] : The signal arguments as an array[br]
|
||||
## [member timeout] : The maximum duration (in milliseconds) to wait for the signal to be emitted before failing
|
||||
@abstract func await_signal(signal_name: String, args := [], timeout := 2000 ) -> void
|
||||
|
||||
|
||||
## Waits for the specified signal to be emitted by a particular source node. If the signal is not emitted within the given timeout, the operation fails.[br]
|
||||
## [member source] : the object from which the signal is emitted[br]
|
||||
## [member signal_name] : The name of the signal to wait for[br]
|
||||
## [member args] : The signal arguments as an array[br]
|
||||
## [member timeout] : tThe maximum duration (in milliseconds) to wait for the signal to be emitted before failing
|
||||
@abstract func await_signal_on(source: Object, signal_name: String, args := [], timeout := 2000 ) -> void
|
||||
|
||||
|
||||
## Restores the scene window to a windowed mode and brings it to the foreground.[br]
|
||||
## This ensures that the scene is visible and active during testing, making it easier to observe and interact with.
|
||||
@abstract func move_window_to_foreground() -> GdUnitSceneRunner
|
||||
|
||||
|
||||
## Minimizes the scene window to a windowed mode and brings it to the background.[br]
|
||||
## This ensures that the scene is hidden during testing.
|
||||
@abstract func move_window_to_background() -> GdUnitSceneRunner
|
||||
|
||||
|
||||
## Return the current value of the property with the name <name>.[br]
|
||||
## [member name] : name of property[br]
|
||||
## [member return] : the value of the property
|
||||
@abstract func get_property(name: String) -> Variant
|
||||
|
||||
|
||||
## Set the value <value> of the property with the name <name>.[br]
|
||||
## [member name] : name of property[br]
|
||||
## [member value] : value of property[br]
|
||||
## [member return] : true|false depending on valid property name.
|
||||
@abstract func set_property(name: String, value: Variant) -> bool
|
||||
|
||||
|
||||
## executes the function specified by <name> in the scene and returns the result.[br]
|
||||
## [member name] : the name of the function to execute[br]
|
||||
## [member args] : optional function arguments[br]
|
||||
## [member return] : the function result
|
||||
@abstract func invoke(name: String, ...args: Array) -> Variant
|
||||
|
||||
|
||||
## Searches for the specified node with the name in the current scene and returns it, otherwise null.[br]
|
||||
## [member name] : the name of the node to find[br]
|
||||
## [member recursive] : enables/disables seraching recursive[br]
|
||||
## [member return] : the node if find otherwise null
|
||||
@abstract func find_child(name: String, recursive: bool = true, owned: bool = false) -> Node
|
||||
|
||||
|
||||
## Access to current running scene
|
||||
@abstract func scene() -> Node
|
||||
@@ -0,0 +1 @@
|
||||
uid://bpcumf0tehyq
|
||||
@@ -0,0 +1,158 @@
|
||||
## An Assertion Tool to verify for emitted signals until a waiting time
|
||||
@abstract class_name GdUnitSignalAssert
|
||||
extends GdUnitAssert
|
||||
|
||||
|
||||
## Verifies that the current value is null.
|
||||
@abstract func is_null() -> GdUnitSignalAssert
|
||||
|
||||
|
||||
## Verifies that the current value is not null.
|
||||
@abstract func is_not_null() -> GdUnitSignalAssert
|
||||
|
||||
|
||||
## Verifies that the current value is equal to the given one.
|
||||
@abstract func is_equal(expected: Variant) -> GdUnitSignalAssert
|
||||
|
||||
|
||||
## Verifies that the current value is not equal to expected one.
|
||||
@abstract func is_not_equal(expected: Variant) -> GdUnitSignalAssert
|
||||
|
||||
|
||||
## Overrides the default failure message by given custom message.
|
||||
@abstract func override_failure_message(message: String) -> GdUnitSignalAssert
|
||||
|
||||
|
||||
## Appends a custom message to the failure message.
|
||||
@abstract func append_failure_message(message: String) -> GdUnitSignalAssert
|
||||
|
||||
|
||||
## Verifies that the specified signal is emitted with the expected arguments.[br]
|
||||
##
|
||||
## This assertion waits for a signal to be emitted from the object under test and
|
||||
## validates that it was emitted with the correct arguments. The function supports
|
||||
## both typed signals (Signal type) and string-based signal names for flexibility
|
||||
## in different testing scenarios.[br]
|
||||
## [br]
|
||||
## [b]Parameters:[/b][br]
|
||||
## [param signal_name]: The signal to monitor. Can be either:[br]
|
||||
## • A [Signal] reference (recommended for type safety)[br]
|
||||
## • A [String] with the signal name
|
||||
## [param signal_args]: Optional expected signal arguments.[br]
|
||||
## When provided, verifies the signal was emitted with exactly these values.[br]
|
||||
## [br]
|
||||
## [b]Returns:[/b][br]
|
||||
## [GdUnitSignalAssert] - Returns self for method chaining.[br]
|
||||
## [br]
|
||||
## [b]Examples:[/b]
|
||||
## [codeblock]
|
||||
## signal signal_a(value: int)
|
||||
## signal signal_b(name: String, count: int)
|
||||
##
|
||||
## # Wait for signal emission without checking arguments
|
||||
## # Using Signal reference (type-safe)
|
||||
## await assert_signal(instance).is_emitted(signal_a)
|
||||
## # Using string name (dynamic)
|
||||
## await assert_signal(instance).is_emitted("signal_a")
|
||||
##
|
||||
## # Wait for signal emission with specific argument
|
||||
## await assert_signal(instance).is_emitted(signal_a, 10)
|
||||
##
|
||||
## # Wait for signal with multiple arguments
|
||||
## await assert_signal(instance).is_emitted(signal_b, "test", 42)
|
||||
##
|
||||
## # Wait max 500ms for signal with argument 10
|
||||
## await assert_signal(instance).wait_until(500).is_emitted(signal_a, 10)
|
||||
## [/codeblock]
|
||||
## [br]
|
||||
## [b]Note:[/b] This is an async operation - use [code]await[/code] when calling.[br]
|
||||
## The assertion fails if the signal is not emitted within the timeout period.
|
||||
@abstract func is_emitted(signal_name: Variant, ...signal_args: Array) -> GdUnitSignalAssert
|
||||
|
||||
|
||||
## Verifies that the specified signal is NOT emitted with the expected arguments.[br]
|
||||
##
|
||||
## This assertion waits for a specified time period and validates that a signal
|
||||
## was not emitted with the given arguments. Useful for ensuring certain conditions
|
||||
## don't trigger unwanted signals or for verifying signal filtering logic.[br]
|
||||
## [br]
|
||||
## [b]Parameters:[/b][br]
|
||||
## [param signal_name]: The signal to monitor. Can be either:[br]
|
||||
## • A [Signal] reference (recommended for type safety)[br]
|
||||
## • A [String] with the signal name
|
||||
## [param signal_args]: Optional expected signal arguments.[br]
|
||||
## When provided, verifies the signal was not emitted with these specific values.[br]
|
||||
## If omitted, verifies the signal was not emitted at all.[br]
|
||||
## [br]
|
||||
## [b]Returns:[/b][br]
|
||||
## [GdUnitSignalAssert] - Returns self for method chaining.[br]
|
||||
## [br]
|
||||
## [b]Examples:[/b]
|
||||
## [codeblock]
|
||||
## signal signal_a(value: int)
|
||||
## signal signal_b(name: String, count: int)
|
||||
##
|
||||
## # Verify signal is not emitted at all (without checking arguments)
|
||||
## await assert_signal(instance).wait_until(500).is_not_emitted(signal_a)
|
||||
## await assert_signal(instance).wait_until(500).is_not_emitted("signal_a")
|
||||
##
|
||||
## # Verify signal is not emitted with specific argument
|
||||
## await assert_signal(instance).wait_until(500).is_not_emitted(signal_a, 10)
|
||||
##
|
||||
## # Verify signal is not emitted with multiple arguments
|
||||
## await assert_signal(instance).wait_until(500).is_not_emitted(signal_b, "test", 42)
|
||||
##
|
||||
## # Can be emitted with different arguments (this passes)
|
||||
## instance.emit_signal("signal_a", 20) # Emits with 20, not 10
|
||||
## await assert_signal(instance).wait_until(500).is_not_emitted(signal_a, 10)
|
||||
## [/codeblock]
|
||||
## [br]
|
||||
## [b]Note:[/b] This is an async operation - use [code]await[/code] when calling.[br]
|
||||
## The assertion fails if the signal IS emitted with the specified arguments within the timeout period.
|
||||
@abstract func is_not_emitted(signal_name: Variant, ...signal_args: Array) -> GdUnitSignalAssert
|
||||
|
||||
|
||||
## Verifies that the specified signal exists on the emitter object.[br]
|
||||
##
|
||||
## This assertion checks if a signal is defined on the object under test,
|
||||
## regardless of whether it has been emitted. Useful for validating that
|
||||
## objects have the expected signals before testing their emission.[br]
|
||||
## [br]
|
||||
## [b]Parameters:[/b][br]
|
||||
## [param signal_name]: The signal to check. Can be either:[br]
|
||||
## • A [Signal] reference (recommended for type safety)[br]
|
||||
## • A [String] with the signal name
|
||||
## [br]
|
||||
## [b]Returns:[/b][br]
|
||||
## [GdUnitSignalAssert] - Returns self for method chaining.[br]
|
||||
## [br]
|
||||
## [b]Examples:[/b]
|
||||
## [codeblock]
|
||||
## signal my_signal(value: int)
|
||||
## signal another_signal()
|
||||
##
|
||||
## # Verify signal exists using Signal reference
|
||||
## assert_signal(instance).is_signal_exists(my_signal)
|
||||
##
|
||||
## # Verify signal exists using string name
|
||||
## assert_signal(instance).is_signal_exists("my_signal")
|
||||
##
|
||||
## # Chain with other assertions
|
||||
## assert_signal(instance) \
|
||||
## .is_signal_exists(my_signal) \
|
||||
## .is_emitted(my_signal, 42)
|
||||
##
|
||||
## [/codeblock]
|
||||
## [br]
|
||||
## [b]Note:[/b] This only checks signal definition, not emission.[br]
|
||||
## The assertion fails if the signal is not defined on the object.
|
||||
@abstract func is_signal_exists(signal_name: Variant) -> GdUnitSignalAssert
|
||||
|
||||
|
||||
## Sets the assert signal timeout in ms, if the time over a failure is reported.[br]
|
||||
## Example:
|
||||
## [codeblock]
|
||||
## do wait until 5s the instance has emitted the signal `signal_a`[br]
|
||||
## assert_signal(instance).wait_until(5000).is_emitted("signal_a")
|
||||
## [/codeblock]
|
||||
@abstract func wait_until(timeout: int) -> GdUnitSignalAssert
|
||||
@@ -0,0 +1 @@
|
||||
uid://c7787lxncd5du
|
||||
@@ -0,0 +1,71 @@
|
||||
## An Assertion Tool to verify String values
|
||||
@abstract class_name GdUnitStringAssert
|
||||
extends GdUnitAssert
|
||||
|
||||
|
||||
## Verifies that the current value is null.
|
||||
@abstract func is_null() -> GdUnitStringAssert
|
||||
|
||||
|
||||
## Verifies that the current value is not null.
|
||||
@abstract func is_not_null() -> GdUnitStringAssert
|
||||
|
||||
|
||||
## Verifies that the current value is equal to the given one.
|
||||
@abstract func is_equal(expected: Variant) -> GdUnitStringAssert
|
||||
|
||||
|
||||
## Verifies that the current String is equal to the given one, ignoring case considerations.
|
||||
@abstract func is_equal_ignoring_case(expected: Variant) -> GdUnitStringAssert
|
||||
|
||||
|
||||
## Verifies that the current value is not equal to expected one.
|
||||
@abstract func is_not_equal(expected: Variant) -> GdUnitStringAssert
|
||||
|
||||
|
||||
## Verifies that the current String is not equal to the given one, ignoring case considerations.
|
||||
@abstract func is_not_equal_ignoring_case(expected: Variant) -> GdUnitStringAssert
|
||||
|
||||
|
||||
## Overrides the default failure message by given custom message.
|
||||
@abstract func override_failure_message(message: String) -> GdUnitStringAssert
|
||||
|
||||
|
||||
## Appends a custom message to the failure message.
|
||||
@abstract func append_failure_message(message: String) -> GdUnitStringAssert
|
||||
|
||||
|
||||
## Verifies that the current String is empty, it has a length of 0.
|
||||
@abstract func is_empty() -> GdUnitStringAssert
|
||||
|
||||
|
||||
## Verifies that the current String is not empty, it has a length of minimum 1.
|
||||
@abstract func is_not_empty() -> GdUnitStringAssert
|
||||
|
||||
|
||||
## Verifies that the current String contains the given String.
|
||||
@abstract func contains(expected: String) -> GdUnitStringAssert
|
||||
|
||||
|
||||
## Verifies that the current String does not contain the given String.
|
||||
@abstract func not_contains(expected: String) -> GdUnitStringAssert
|
||||
|
||||
|
||||
## Verifies that the current String does not contain the given String, ignoring case considerations.
|
||||
@abstract func contains_ignoring_case(expected: String) -> GdUnitStringAssert
|
||||
|
||||
|
||||
## Verifies that the current String does not contain the given String, ignoring case considerations.
|
||||
@abstract func not_contains_ignoring_case(expected: String) -> GdUnitStringAssert
|
||||
|
||||
|
||||
## Verifies that the current String starts with the given prefix.
|
||||
@abstract func starts_with(expected: String) -> GdUnitStringAssert
|
||||
|
||||
|
||||
## Verifies that the current String ends with the given suffix.
|
||||
@abstract func ends_with(expected: String) -> GdUnitStringAssert
|
||||
|
||||
|
||||
## Verifies that the current String has the expected length by used comparator.
|
||||
@abstract func has_length(length: int, comparator: int = Comparator.EQUAL) -> GdUnitStringAssert
|
||||
@@ -0,0 +1 @@
|
||||
uid://bywja6o2c1cfd
|
||||
@@ -0,0 +1,746 @@
|
||||
## The main class for all GdUnit test suites[br]
|
||||
## This class is the main class to implement your unit tests[br]
|
||||
## You have to extend and implement your test cases as described[br]
|
||||
## e.g MyTests.gd [br]
|
||||
## [codeblock]
|
||||
## extends GdUnitTestSuite
|
||||
## # testcase
|
||||
## func test_case_a():
|
||||
## assert_that("value").is_equal("value")
|
||||
## [/codeblock]
|
||||
## @tutorial: https://mikeschulze.github.io/gdUnit4/faq/test-suite/
|
||||
|
||||
@icon("res://addons/gdUnit4/src/ui/settings/logo.png")
|
||||
class_name GdUnitTestSuite
|
||||
extends Node
|
||||
|
||||
|
||||
### internal runtime variables that must not be overwritten!!!
|
||||
@warning_ignore("unused_private_class_variable")
|
||||
var __is_skipped := false
|
||||
@warning_ignore("unused_private_class_variable")
|
||||
var __skip_reason := "Unknow."
|
||||
var __active_test_case: String
|
||||
var __awaiter := __gdunit_awaiter()
|
||||
|
||||
|
||||
### We now load all used asserts and tool scripts into the cache according to the principle of "lazy loading"
|
||||
### in order to noticeably reduce the loading time of the test suite.
|
||||
# We go this hard way to increase the loading performance to avoid reparsing all the used scripts
|
||||
# for more detailed info -> https://github.com/godotengine/godot/issues/67400
|
||||
func __lazy_load(script_path: String) -> GDScript:
|
||||
return GdUnitAssertions.__lazy_load(script_path)
|
||||
|
||||
|
||||
func __gdunit_assert() -> GDScript:
|
||||
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd")
|
||||
|
||||
|
||||
func __gdunit_tools() -> GDScript:
|
||||
return __lazy_load("res://addons/gdUnit4/src/core/GdUnitTools.gd")
|
||||
|
||||
|
||||
func __gdunit_file_access() -> GDScript:
|
||||
return __lazy_load("res://addons/gdUnit4/src/core/GdUnitFileAccess.gd")
|
||||
|
||||
|
||||
func __gdunit_awaiter() -> Object:
|
||||
return __lazy_load("res://addons/gdUnit4/src/GdUnitAwaiter.gd").new()
|
||||
|
||||
|
||||
func __gdunit_argument_matchers() -> GDScript:
|
||||
return __lazy_load("res://addons/gdUnit4/src/matchers/GdUnitArgumentMatchers.gd")
|
||||
|
||||
|
||||
func __gdunit_object_interactions() -> GDScript:
|
||||
return __lazy_load("res://addons/gdUnit4/src/doubler/GdUnitObjectInteractions.gd")
|
||||
|
||||
|
||||
## This function is called before a test suite starts[br]
|
||||
## You can overwrite to prepare test data or initalizize necessary variables
|
||||
func before() -> void:
|
||||
pass
|
||||
|
||||
|
||||
## This function is called at least when a test suite is finished[br]
|
||||
## You can overwrite to cleanup data created during test running
|
||||
func after() -> void:
|
||||
pass
|
||||
|
||||
|
||||
## This function is called before a test case starts[br]
|
||||
## You can overwrite to prepare test case specific data
|
||||
func before_test() -> void:
|
||||
pass
|
||||
|
||||
|
||||
## This function is called after the test case is finished[br]
|
||||
## You can overwrite to cleanup your test case specific data
|
||||
func after_test() -> void:
|
||||
pass
|
||||
|
||||
|
||||
func is_failure() -> bool:
|
||||
return Engine.get_meta("GD_TEST_FAILURE") if Engine.has_meta("GD_TEST_FAILURE") else false
|
||||
|
||||
|
||||
func set_active_test_case(test_case: String) -> void:
|
||||
__active_test_case = test_case
|
||||
|
||||
|
||||
# === Tools ====================================================================
|
||||
# Mapps Godot error number to a readable error message. See at ERROR
|
||||
# https://docs.godotengine.org/de/stable/classes/class_@globalscope.html#enum-globalscope-error
|
||||
func error_as_string(error_number: int) -> String:
|
||||
return error_string(error_number)
|
||||
|
||||
|
||||
## A litle helper to auto freeing your created objects after test execution
|
||||
func auto_free(obj: Variant) -> Variant:
|
||||
var execution_context := GdUnitThreadManager.get_current_context().get_execution_context()
|
||||
|
||||
assert(execution_context != null, "INTERNAL ERROR: The current execution_context is null! Please report this as bug.")
|
||||
return execution_context.register_auto_free(obj)
|
||||
|
||||
|
||||
@warning_ignore("native_method_override")
|
||||
func add_child(node: Node, force_readable_name := false, internal := Node.INTERNAL_MODE_DISABLED) -> void:
|
||||
super.add_child(node, force_readable_name, internal)
|
||||
var execution_context := GdUnitThreadManager.get_current_context().get_execution_context()
|
||||
if execution_context != null:
|
||||
execution_context.orphan_monitor_start()
|
||||
|
||||
|
||||
## Discard the error message triggered by a timeout (interruption).[br]
|
||||
## By default, an interrupted test is reported as an error.[br]
|
||||
## This function allows you to change the message to Success when an interrupted error is reported.
|
||||
func discard_error_interupted_by_timeout() -> void:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
__gdunit_tools().register_expect_interupted_by_timeout(self, __active_test_case)
|
||||
|
||||
|
||||
## Creates a new directory under the temporary directory *user://tmp*[br]
|
||||
## Useful for storing data during test execution. [br]
|
||||
## The directory is automatically deleted after test suite execution
|
||||
func create_temp_dir(relative_path: String) -> String:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_file_access().create_temp_dir(relative_path)
|
||||
|
||||
|
||||
## Deletes the temporary base directory[br]
|
||||
## Is called automatically after each execution of the test suite
|
||||
func clean_temp_dir() -> void:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
__gdunit_file_access().clear_tmp()
|
||||
|
||||
|
||||
## Creates a new file under the temporary directory *user://tmp* + <relative_path>[br]
|
||||
## with given name <file_name> and given file <mode> (default = File.WRITE)[br]
|
||||
## If success the returned File is automatically closed after the execution of the test suite
|
||||
func create_temp_file(relative_path: String, file_name: String, mode := FileAccess.WRITE) -> FileAccess:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_file_access().create_temp_file(relative_path, file_name, mode)
|
||||
|
||||
|
||||
## Reads a resource by given path <resource_path> into a PackedStringArray.
|
||||
func resource_as_array(resource_path: String) -> PackedStringArray:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_file_access().resource_as_array(resource_path)
|
||||
|
||||
|
||||
## Reads a resource by given path <resource_path> and returned the content as String.
|
||||
func resource_as_string(resource_path: String) -> String:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_file_access().resource_as_string(resource_path)
|
||||
|
||||
|
||||
## Reads a resource by given path <resource_path> and return Variand translated by str_to_var
|
||||
func resource_as_var(resource_path: String) -> Variant:
|
||||
@warning_ignore("unsafe_method_access", "unsafe_cast")
|
||||
return str_to_var(__gdunit_file_access().resource_as_string(resource_path) as String)
|
||||
|
||||
|
||||
## Waits for given signal to be emitted by <source> until a specified timeout to fail[br]
|
||||
## source: the object from which the signal is emitted[br]
|
||||
## signal_name: signal name[br]
|
||||
## args: the expected signal arguments as an array[br]
|
||||
## timeout: the timeout in ms, default is set to 2000ms
|
||||
func await_signal_on(source: Object, signal_name: String, args: Array = [], timeout: int = 2000) -> Variant:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return await __awaiter.await_signal_on(source, signal_name, args, timeout)
|
||||
|
||||
|
||||
## Waits until the next idle frame
|
||||
func await_idle_frame() -> void:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
await __awaiter.await_idle_frame()
|
||||
|
||||
|
||||
## Waits for a given amount of milliseconds[br]
|
||||
## example:[br]
|
||||
## [codeblock]
|
||||
## # waits for 100ms
|
||||
## await await_millis(myNode, 100).completed
|
||||
## [/codeblock][br]
|
||||
## use this waiter and not `await get_tree().create_timer().timeout to prevent errors when a test case is timed out
|
||||
func await_millis(timeout: int) -> void:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
await __awaiter.await_millis(timeout)
|
||||
|
||||
|
||||
## Collects detailed information about orphaned nodes for debugging purposes.[br]
|
||||
##
|
||||
## This function gathers comprehensive details about nodes that remain in memory
|
||||
## after test execution (orphans). It provides debugging information to help
|
||||
## identify the source of memory leaks in tests. Must be manually called in
|
||||
## tests when orphan nodes are detected.[br]
|
||||
## [br]
|
||||
## [b]When to Use:[/b][br]
|
||||
## - When GdUnit4 reports orphan nodes after test execution[br]
|
||||
## - For debugging memory leaks in test scenarios[br]
|
||||
## - To get detailed information about unreleased nodes[br]
|
||||
## [br]
|
||||
## [b]Usage Pattern:[/b][br]
|
||||
## Add this call at the end of tests that are suspected to create orphans,
|
||||
## or when the test runner reports orphan detection.[br]
|
||||
## [br]
|
||||
## [b]Examples:[/b]
|
||||
## [codeblock]
|
||||
## func test_scene_management():
|
||||
## # Test code that might create orphan nodes
|
||||
## var scene = preload("res://TestScene.tscn").instantiate()
|
||||
## add_child(scene)
|
||||
##
|
||||
## # Do test operations
|
||||
## scene.some_method()
|
||||
##
|
||||
## # Clean up (but might miss some nodes)
|
||||
## scene.queue_free()
|
||||
##
|
||||
## # Collect orphan details if any are detected
|
||||
## collect_orphan_node_details()
|
||||
## [/codeblock]
|
||||
## [br]
|
||||
## [b]Note:[/b] This is a debugging utility function that should be removed
|
||||
## or commented out once orphan issues are resolved.
|
||||
func collect_orphan_node_details() -> void:
|
||||
GdUnitThreadManager.get_current_context().get_execution_context().orphan_monitor_collect()
|
||||
|
||||
|
||||
## Creates a new scene runner to allow simulate interactions checked a scene.[br]
|
||||
## The runner will manage the scene instance and release after the runner is released[br]
|
||||
## example:[br]
|
||||
## [codeblock]
|
||||
## # creates a runner by using a instanciated scene
|
||||
## var scene = load("res://foo/my_scne.tscn").instantiate()
|
||||
## var runner := scene_runner(scene)
|
||||
##
|
||||
## # or simply creates a runner by using the scene resource path
|
||||
## var runner := scene_runner("res://foo/my_scne.tscn")
|
||||
## [/codeblock]
|
||||
func scene_runner(scene: Variant, verbose := false) -> GdUnitSceneRunner:
|
||||
return auto_free(__lazy_load("res://addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd").new(scene, verbose))
|
||||
|
||||
|
||||
# === Mocking & Spy ===========================================================
|
||||
|
||||
## do return a default value for primitive types or null
|
||||
const RETURN_DEFAULTS = GdUnitMock.RETURN_DEFAULTS
|
||||
## do call the real implementation
|
||||
const CALL_REAL_FUNC = GdUnitMock.CALL_REAL_FUNC
|
||||
## do return a default value for primitive types and a fully mocked value for Object types
|
||||
## builds full deep mocked object
|
||||
const RETURN_DEEP_STUB = GdUnitMock.RETURN_DEEP_STUB
|
||||
|
||||
|
||||
## Creates a mock for given class name
|
||||
func mock(clazz: Variant, mock_mode := RETURN_DEFAULTS) -> Variant:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __lazy_load("res://addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd").build(clazz, mock_mode)
|
||||
|
||||
|
||||
## Creates a spy checked given object instance
|
||||
func spy(instance: Variant) -> Variant:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __lazy_load("res://addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd").build(instance)
|
||||
|
||||
|
||||
## Configures a return value for the specified function and used arguments.[br]
|
||||
## [b]Example:
|
||||
## [codeblock]
|
||||
## # overrides the return value of myMock.is_selected() to false
|
||||
## do_return(false).on(myMock).is_selected()
|
||||
## [/codeblock]
|
||||
func do_return(value: Variant) -> GdUnitMock:
|
||||
return GdUnitMock.new(value)
|
||||
|
||||
|
||||
## Verifies certain behavior happened at least once or exact number of times
|
||||
func verify(obj: Variant, times := 1) -> Variant:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_object_interactions().verify(obj, times)
|
||||
|
||||
|
||||
## Verifies no interactions is happen checked this mock or spy
|
||||
func verify_no_interactions(obj: Variant) -> GdUnitAssert:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_object_interactions().verify_no_interactions(obj)
|
||||
|
||||
|
||||
## Verifies the given mock or spy has any unverified interaction.
|
||||
func verify_no_more_interactions(obj: Variant) -> GdUnitAssert:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_object_interactions().verify_no_more_interactions(obj)
|
||||
|
||||
|
||||
## Resets the saved function call counters checked a mock or spy
|
||||
func reset(obj: Variant) -> void:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
__gdunit_object_interactions().reset(obj)
|
||||
|
||||
|
||||
## Starts monitoring the specified source to collect all transmitted signals.[br]
|
||||
## The collected signals can then be checked with 'assert_signal'.[br]
|
||||
## By default, the specified source is automatically released when the test ends.
|
||||
## You can control this behavior by setting auto_free to false if you do not want the source to be automatically freed.[br]
|
||||
## Usage:
|
||||
## [codeblock]
|
||||
## var emitter := monitor_signals(MyEmitter.new())
|
||||
## # call the function to send the signal
|
||||
## emitter.do_it()
|
||||
## # verify the signial is emitted
|
||||
## await assert_signal(emitter).is_emitted('my_signal')
|
||||
## [/codeblock]
|
||||
func monitor_signals(source: Object, _auto_free := true) -> Object:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
__lazy_load("res://addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd")\
|
||||
.get_current_context()\
|
||||
.get_signal_collector()\
|
||||
.register_emitter(source, true) # force recreate to start with a fresh monitoring
|
||||
return auto_free(source) if _auto_free else source
|
||||
|
||||
|
||||
# === Argument matchers ========================================================
|
||||
## Argument matcher to match any argument
|
||||
func any() -> GdUnitArgumentMatcher:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_argument_matchers().any()
|
||||
|
||||
|
||||
## Argument matcher to match any boolean value
|
||||
func any_bool() -> GdUnitArgumentMatcher:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_argument_matchers().by_type(TYPE_BOOL)
|
||||
|
||||
|
||||
## Argument matcher to match any integer value
|
||||
func any_int() -> GdUnitArgumentMatcher:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_argument_matchers().by_type(TYPE_INT)
|
||||
|
||||
|
||||
## Argument matcher to match any float value
|
||||
func any_float() -> GdUnitArgumentMatcher:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_argument_matchers().by_type(TYPE_FLOAT)
|
||||
|
||||
|
||||
## Argument matcher to match any String value
|
||||
func any_string() -> GdUnitArgumentMatcher:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_argument_matchers().by_type(TYPE_STRING)
|
||||
|
||||
|
||||
## Argument matcher to match any Color value
|
||||
func any_color() -> GdUnitArgumentMatcher:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_argument_matchers().by_type(TYPE_COLOR)
|
||||
|
||||
|
||||
## Argument matcher to match any Vector typed value
|
||||
func any_vector() -> GdUnitArgumentMatcher:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_argument_matchers().by_types([
|
||||
TYPE_VECTOR2,
|
||||
TYPE_VECTOR2I,
|
||||
TYPE_VECTOR3,
|
||||
TYPE_VECTOR3I,
|
||||
TYPE_VECTOR4,
|
||||
TYPE_VECTOR4I,
|
||||
])
|
||||
|
||||
|
||||
## Argument matcher to match any Vector2 value
|
||||
func any_vector2() -> GdUnitArgumentMatcher:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_argument_matchers().by_type(TYPE_VECTOR2)
|
||||
|
||||
|
||||
## Argument matcher to match any Vector2i value
|
||||
func any_vector2i() -> GdUnitArgumentMatcher:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_argument_matchers().by_type(TYPE_VECTOR2I)
|
||||
|
||||
|
||||
## Argument matcher to match any Vector3 value
|
||||
func any_vector3() -> GdUnitArgumentMatcher:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_argument_matchers().by_type(TYPE_VECTOR3)
|
||||
|
||||
|
||||
## Argument matcher to match any Vector3i value
|
||||
func any_vector3i() -> GdUnitArgumentMatcher:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_argument_matchers().by_type(TYPE_VECTOR3I)
|
||||
|
||||
|
||||
## Argument matcher to match any Vector4 value
|
||||
func any_vector4() -> GdUnitArgumentMatcher:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_argument_matchers().by_type(TYPE_VECTOR4)
|
||||
|
||||
|
||||
## Argument matcher to match any Vector4i value
|
||||
func any_vector4i() -> GdUnitArgumentMatcher:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_argument_matchers().by_type(TYPE_VECTOR4I)
|
||||
|
||||
|
||||
## Argument matcher to match any Rect2 value
|
||||
func any_rect2() -> GdUnitArgumentMatcher:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_argument_matchers().by_type(TYPE_RECT2)
|
||||
|
||||
|
||||
## Argument matcher to match any Plane value
|
||||
func any_plane() -> GdUnitArgumentMatcher:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_argument_matchers().by_type(TYPE_PLANE)
|
||||
|
||||
|
||||
## Argument matcher to match any Quaternion value
|
||||
func any_quat() -> GdUnitArgumentMatcher:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_argument_matchers().by_type(TYPE_QUATERNION)
|
||||
|
||||
|
||||
## Argument matcher to match any AABB value
|
||||
func any_aabb() -> GdUnitArgumentMatcher:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_argument_matchers().by_type(TYPE_AABB)
|
||||
|
||||
|
||||
## Argument matcher to match any Basis value
|
||||
func any_basis() -> GdUnitArgumentMatcher:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_argument_matchers().by_type(TYPE_BASIS)
|
||||
|
||||
|
||||
## Argument matcher to match any Transform2D value
|
||||
func any_transform_2d() -> GdUnitArgumentMatcher:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_argument_matchers().by_type(TYPE_TRANSFORM2D)
|
||||
|
||||
|
||||
## Argument matcher to match any Transform3D value
|
||||
func any_transform_3d() -> GdUnitArgumentMatcher:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_argument_matchers().by_type(TYPE_TRANSFORM3D)
|
||||
|
||||
|
||||
## Argument matcher to match any NodePath value
|
||||
func any_node_path() -> GdUnitArgumentMatcher:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_argument_matchers().by_type(TYPE_NODE_PATH)
|
||||
|
||||
|
||||
## Argument matcher to match any RID value
|
||||
func any_rid() -> GdUnitArgumentMatcher:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_argument_matchers().by_type(TYPE_RID)
|
||||
|
||||
|
||||
## Argument matcher to match any Object value
|
||||
func any_object() -> GdUnitArgumentMatcher:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_argument_matchers().by_type(TYPE_OBJECT)
|
||||
|
||||
|
||||
## Argument matcher to match any Dictionary value
|
||||
func any_dictionary() -> GdUnitArgumentMatcher:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_argument_matchers().by_type(TYPE_DICTIONARY)
|
||||
|
||||
|
||||
## Argument matcher to match any Array value
|
||||
func any_array() -> GdUnitArgumentMatcher:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_argument_matchers().by_type(TYPE_ARRAY)
|
||||
|
||||
|
||||
## Argument matcher to match any PackedByteArray value
|
||||
func any_packed_byte_array() -> GdUnitArgumentMatcher:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_argument_matchers().by_type(TYPE_PACKED_BYTE_ARRAY)
|
||||
|
||||
|
||||
## Argument matcher to match any PackedInt32Array value
|
||||
func any_packed_int32_array() -> GdUnitArgumentMatcher:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_argument_matchers().by_type(TYPE_PACKED_INT32_ARRAY)
|
||||
|
||||
|
||||
## Argument matcher to match any PackedInt64Array value
|
||||
func any_packed_int64_array() -> GdUnitArgumentMatcher:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_argument_matchers().by_type(TYPE_PACKED_INT64_ARRAY)
|
||||
|
||||
|
||||
## Argument matcher to match any PackedFloat32Array value
|
||||
func any_packed_float32_array() -> GdUnitArgumentMatcher:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_argument_matchers().by_type(TYPE_PACKED_FLOAT32_ARRAY)
|
||||
|
||||
|
||||
## Argument matcher to match any PackedFloat64Array value
|
||||
func any_packed_float64_array() -> GdUnitArgumentMatcher:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_argument_matchers().by_type(TYPE_PACKED_FLOAT64_ARRAY)
|
||||
|
||||
|
||||
## Argument matcher to match any PackedStringArray value
|
||||
func any_packed_string_array() -> GdUnitArgumentMatcher:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_argument_matchers().by_type(TYPE_PACKED_STRING_ARRAY)
|
||||
|
||||
|
||||
## Argument matcher to match any PackedVector2Array value
|
||||
func any_packed_vector2_array() -> GdUnitArgumentMatcher:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_argument_matchers().by_type(TYPE_PACKED_VECTOR2_ARRAY)
|
||||
|
||||
|
||||
## Argument matcher to match any PackedVector3Array value
|
||||
func any_packed_vector3_array() -> GdUnitArgumentMatcher:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_argument_matchers().by_type(TYPE_PACKED_VECTOR3_ARRAY)
|
||||
|
||||
|
||||
## Argument matcher to match any PackedColorArray value
|
||||
func any_packed_color_array() -> GdUnitArgumentMatcher:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_argument_matchers().by_type(TYPE_PACKED_COLOR_ARRAY)
|
||||
|
||||
|
||||
## Argument matcher to match any instance of given class
|
||||
func any_class(clazz :Object) -> GdUnitArgumentMatcher:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __gdunit_argument_matchers().any_class(clazz)
|
||||
|
||||
|
||||
# === value extract utils ======================================================
|
||||
## Builds an extractor by given function name and optional arguments
|
||||
func extr(func_name: String, args := Array()) -> GdUnitValueExtractor:
|
||||
return __lazy_load("res://addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd").new(func_name, args)
|
||||
|
||||
|
||||
## Creates a GdUnitTuple from the provided arguments for use in test assertions.
|
||||
## [br]
|
||||
## This is the primary helper function for creating tuples in GdUnit4 tests.
|
||||
## It provides a convenient way to group multiple expected values when using
|
||||
## [method extractv] assertions. The function enforces that tuples must contain
|
||||
## at least two values, as single-value extractions don't require tuple grouping.
|
||||
## [br]
|
||||
## [b]Parameters:[/b] [br]
|
||||
## - [code]...args[/code]: Variable number of arguments (minimum 2) to group into a tuple.
|
||||
## Each argument represents a value to be compared in assertions.
|
||||
## [br]
|
||||
## [b]Returns:[/b] [br]
|
||||
## A [GdUnitTuple] containing the provided values, or an empty tuple if fewer than
|
||||
## 2 arguments are provided (with an error message).
|
||||
## [br]
|
||||
## [b]Error Handling:[/b] [br]
|
||||
## [codeblock]
|
||||
## # This will push an error and return empty tuple
|
||||
## var invalid = tuple("single_value") # Error: requires at least 2 arguments
|
||||
## [br]
|
||||
## # Correct usage - minimum 2 arguments
|
||||
## var valid = tuple("name", "value")
|
||||
## var valid_multi = tuple(1, 2, 3, 4, 5) # Can have many values
|
||||
## [/codeblock]
|
||||
func tuple(...args: Array) -> GdUnitTuple:
|
||||
if args.size() < 2:
|
||||
push_error("Tuple requires at least two arguments.")
|
||||
return GdUnitTuple.new()
|
||||
return GdUnitTuple.new.callv(args)
|
||||
|
||||
|
||||
# === Asserts ==================================================================
|
||||
|
||||
## The common assertion tool to verify values.
|
||||
## It checks the given value by type to fit to the best assert
|
||||
func assert_that(current: Variant) -> GdUnitAssert:
|
||||
match typeof(current):
|
||||
TYPE_BOOL:
|
||||
return assert_bool(current)
|
||||
TYPE_INT:
|
||||
return assert_int(current)
|
||||
TYPE_FLOAT:
|
||||
return assert_float(current)
|
||||
TYPE_STRING:
|
||||
return assert_str(current)
|
||||
TYPE_VECTOR2, TYPE_VECTOR2I, TYPE_VECTOR3, TYPE_VECTOR3I, TYPE_VECTOR4, TYPE_VECTOR4I:
|
||||
return assert_vector(current, false)
|
||||
TYPE_DICTIONARY:
|
||||
return assert_dict(current)
|
||||
TYPE_ARRAY, TYPE_PACKED_BYTE_ARRAY, TYPE_PACKED_INT32_ARRAY, TYPE_PACKED_INT64_ARRAY,\
|
||||
TYPE_PACKED_FLOAT32_ARRAY, TYPE_PACKED_FLOAT64_ARRAY, TYPE_PACKED_STRING_ARRAY,\
|
||||
TYPE_PACKED_VECTOR2_ARRAY, TYPE_PACKED_VECTOR3_ARRAY, TYPE_PACKED_COLOR_ARRAY:
|
||||
return assert_array(current, false)
|
||||
TYPE_OBJECT, TYPE_NIL:
|
||||
return assert_object(current)
|
||||
_:
|
||||
return __gdunit_assert().new(current)
|
||||
|
||||
|
||||
## An assertion tool to verify boolean values.
|
||||
func assert_bool(current: Variant) -> GdUnitBoolAssert:
|
||||
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd").new(current)
|
||||
|
||||
|
||||
## An assertion tool to verify String values.
|
||||
func assert_str(current: Variant) -> GdUnitStringAssert:
|
||||
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd").new(current)
|
||||
|
||||
|
||||
## An assertion tool to verify integer values.
|
||||
func assert_int(current: Variant) -> GdUnitIntAssert:
|
||||
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd").new(current)
|
||||
|
||||
|
||||
## An assertion tool to verify float values.
|
||||
func assert_float(current: Variant) -> GdUnitFloatAssert:
|
||||
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd").new(current)
|
||||
|
||||
|
||||
## An assertion tool to verify Vector values.[br]
|
||||
## This assertion supports all vector types.[br]
|
||||
## Usage:
|
||||
## [codeblock]
|
||||
## assert_vector(Vector2(1.2, 1.000001)).is_equal(Vector2(1.2, 1.000001))
|
||||
## [/codeblock]
|
||||
func assert_vector(current: Variant, type_check := true) -> GdUnitVectorAssert:
|
||||
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd").new(current, type_check)
|
||||
|
||||
|
||||
## An assertion tool to verify arrays.
|
||||
func assert_array(current: Variant, type_check := true) -> GdUnitArrayAssert:
|
||||
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd").new(current, type_check)
|
||||
|
||||
|
||||
## An assertion tool to verify dictionaries.
|
||||
func assert_dict(current: Variant) -> GdUnitDictionaryAssert:
|
||||
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd").new(current)
|
||||
|
||||
|
||||
## An assertion tool to verify FileAccess.
|
||||
func assert_file(current: Variant) -> GdUnitFileAssert:
|
||||
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd").new(current)
|
||||
|
||||
|
||||
## An assertion tool to verify Objects.
|
||||
func assert_object(current: Variant) -> GdUnitObjectAssert:
|
||||
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd").new(current)
|
||||
|
||||
|
||||
func assert_result(current: Variant) -> GdUnitResultAssert:
|
||||
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd").new(current)
|
||||
|
||||
|
||||
## An assertion tool that waits until a certain time for an expected function return value
|
||||
func assert_func(instance: Object, func_name: String, args := Array()) -> GdUnitFuncAssert:
|
||||
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd").new(instance, func_name, args)
|
||||
|
||||
|
||||
## An assertion tool to verify for emitted signals until a certain time.
|
||||
func assert_signal(instance: Object) -> GdUnitSignalAssert:
|
||||
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd").new(instance)
|
||||
|
||||
|
||||
## An assertion tool to test for failing assertions.[br]
|
||||
## This assert is only designed for internal use to verify failing asserts working as expected.[br]
|
||||
## Usage:
|
||||
## [codeblock]
|
||||
## assert_failure(func(): assert_bool(true).is_not_equal(true)) \
|
||||
## .has_message("Expecting:\n 'true'\n not equal to\n 'true'")
|
||||
## [/codeblock]
|
||||
func assert_failure(assertion: Callable) -> GdUnitFailureAssert:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd").new().execute(assertion)
|
||||
|
||||
|
||||
## An assertion tool to test for failing assertions.[br]
|
||||
## This assert is only designed for internal use to verify failing asserts working as expected.[br]
|
||||
## Usage:
|
||||
## [codeblock]
|
||||
## await assert_failure_await(func(): assert_bool(true).is_not_equal(true)) \
|
||||
## .has_message("Expecting:\n 'true'\n not equal to\n 'true'")
|
||||
## [/codeblock]
|
||||
func assert_failure_await(assertion: Callable) -> GdUnitFailureAssert:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return await __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd").new().execute_and_await(assertion)
|
||||
|
||||
|
||||
## An assertion tool to verify Godot errors.[br]
|
||||
## You can use to verify certain Godot errors like failing assertions, push_error, push_warn.[br]
|
||||
## Usage:
|
||||
## [codeblock]
|
||||
## # tests no error occurred during execution of the code
|
||||
## await assert_error(func (): return 0 )\
|
||||
## .is_success()
|
||||
##
|
||||
## # tests a push_error('test error') occured during execution of the code
|
||||
## await assert_error(func (): push_error('test error') )\
|
||||
## .is_push_error('test error')
|
||||
## [/codeblock]
|
||||
func assert_error(current: Callable) -> GdUnitGodotErrorAssert:
|
||||
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd").new(current)
|
||||
|
||||
|
||||
## Explicitly fails the current test indicating that the feature is not yet implemented.[br]
|
||||
## This function is useful during development when you want to write test cases before implementing the actual functionality.[br]
|
||||
## It provides a clear indication that the test failure is expected because the feature is still under development.[br]
|
||||
## Usage:
|
||||
## [codeblock]
|
||||
## # Test for a feature that will be implemented later
|
||||
## func test_advanced_ai_behavior():
|
||||
## assert_not_yet_implemented()
|
||||
##
|
||||
## [/codeblock]
|
||||
func assert_not_yet_implemented() -> void:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
__gdunit_assert().new(null).do_fail()
|
||||
|
||||
|
||||
## Explicitly fails the current test with a custom error message.[br]
|
||||
## This function reports an error but does not terminate test execution automatically.[br]
|
||||
## You must use 'return' after calling fail() to stop the test since GDScript has no exception support.[br]
|
||||
## Useful for complex conditional testing scenarios where standard assertions are insufficient.[br]
|
||||
## Usage:
|
||||
## [codeblock]
|
||||
## # Fail test when conditions are not met
|
||||
## if !custom_check(player):
|
||||
## fail("Player should be alive but has %d health" % player.health)
|
||||
## return
|
||||
##
|
||||
## # Continue with test if conditions pass
|
||||
## assert_that(player.health).is_greater(0)
|
||||
## [/codeblock]
|
||||
func fail(message: String) -> void:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
__gdunit_assert().new(null).report_error(message)
|
||||
|
||||
|
||||
# --- internal stuff do not override!!!
|
||||
func ResourcePath() -> String:
|
||||
return get_script().resource_path
|
||||
@@ -0,0 +1 @@
|
||||
uid://b6n0cw7w6ty5h
|
||||
@@ -0,0 +1,86 @@
|
||||
## A tuple implementation for GdUnit4 test assertions and value extraction.
|
||||
## @tutorial(GdUnit4 Array Assertions): https://mikeschulze.github.io/gdUnit4/latest/testing/assert-array/#extractv
|
||||
## @tutorial(GdUnit4 Testing Framework): https://mikeschulze.github.io/gdUnit4/
|
||||
## [br]
|
||||
## The GdUnitTuple class is a utility container designed specifically for the GdUnit4
|
||||
## testing framework. It enables advanced assertion operations, particularly when
|
||||
## extracting and comparing multiple values from complex test results.
|
||||
## [br]
|
||||
## [b]Primary Use Cases in Testing:[/b] [br]
|
||||
## - Extracting multiple properties from test objects with [method extractv]## [br]
|
||||
## - Grouping related assertion values for comparison## [br]
|
||||
## - Returning multiple values from test helper methods## [br]
|
||||
## - Organizing expected vs actual value pairs in assertions## [br]
|
||||
## [br]
|
||||
## [b]Example Usage in Tests:[/b]
|
||||
## [codeblock]
|
||||
## func test_player_stats_after_level_up():
|
||||
## var player = Player.new()
|
||||
## player.level_up()
|
||||
##
|
||||
## # Extract multiple properties using tuple
|
||||
## assert_array([player]) \
|
||||
## .extractv(extr("name"), extr("level"), extr("hp")) \
|
||||
## .contains(tuple("Hero", 2, 150))
|
||||
##
|
||||
## func test_enemy_spawn_positions():
|
||||
## var enemies: Array = spawn_enemies(3)
|
||||
##
|
||||
## # Verify multiple enemies have correct position data
|
||||
## assert_array(enemies) \
|
||||
## .extractv(extr("position.x"), extr("position.y")) \
|
||||
## .contains_exactly([
|
||||
## tuple(100, 200),
|
||||
## tuple(150, 200),
|
||||
## tuple(200, 200)
|
||||
## ])
|
||||
## [/codeblock]
|
||||
## [br]
|
||||
## [b]Integration with GdUnit4 Assertions:[/b] [br]
|
||||
## Tuples work seamlessly with array assertion methods like: [br]
|
||||
## - [code]contains()[/code] - Check if extracted values contain specific tuples [br]
|
||||
## - [code]contains_exactly()[/code] - Verify exact tuple matches [br]
|
||||
## - [code]is_equal()[/code] - Compare tuple equality [br]
|
||||
## [br]
|
||||
## [b]Note:[/b] This class is part of the GdUnit4 testing framework's internal
|
||||
## utilities and is primarily intended for use within test assertions rather
|
||||
## than production code.
|
||||
class_name GdUnitTuple
|
||||
extends RefCounted
|
||||
|
||||
var _values: Array = []
|
||||
|
||||
|
||||
## Initializes a new GdUnitTuple with test values.
|
||||
## [br]
|
||||
## Creates a tuple to hold multiple values extracted from test objects
|
||||
## or expected values for assertions. Commonly used with the [code]tuple()[/code]
|
||||
## helper function in GdUnit4 tests.
|
||||
## [br]
|
||||
## [b]Parameters:[/b]
|
||||
## - [code]...args[/code]: Variable number of values to store.
|
||||
func _init(...args: Array) -> void:
|
||||
_values = args
|
||||
|
||||
|
||||
## Returns the tuple's values as an array for assertion comparisons.
|
||||
## [br]
|
||||
## Provides access to the stored test values. Used internally by GdUnit4's
|
||||
## assertion system when comparing tuples in test validations.
|
||||
## [br]
|
||||
## [b]Returns:[/b]
|
||||
## An [Array] containing all values stored in the tuple.
|
||||
func values() -> Array:
|
||||
return _values
|
||||
|
||||
|
||||
## Returns a string representation for test output and debugging.
|
||||
## [br]
|
||||
## Formats the tuple for display in test results, error messages, and debug logs.
|
||||
## This method is automatically called by GdUnit4 when displaying assertion
|
||||
## failures involving tuples.
|
||||
## [br]
|
||||
## [b]Returns:[/b]
|
||||
## A [String] in the format "tuple([value1, value2, ...])"
|
||||
func _to_string() -> String:
|
||||
return "tuple(%s)" % str(_values)
|
||||
@@ -0,0 +1 @@
|
||||
uid://cy4hy5r7gsyg0
|
||||
@@ -0,0 +1,9 @@
|
||||
## This is the base interface for value extraction
|
||||
class_name GdUnitValueExtractor
|
||||
extends RefCounted
|
||||
|
||||
|
||||
## Extracts a value by given implementation
|
||||
func extract_value(value :Variant) -> Variant:
|
||||
push_error("Uninplemented func 'extract_value'")
|
||||
return value
|
||||
@@ -0,0 +1 @@
|
||||
uid://ltc0vlwyjuxb
|
||||
@@ -0,0 +1,55 @@
|
||||
## An Assertion Tool to verify Vector values
|
||||
@abstract class_name GdUnitVectorAssert
|
||||
extends GdUnitAssert
|
||||
|
||||
|
||||
## Verifies that the current value is null.
|
||||
@abstract func is_null() -> GdUnitVectorAssert
|
||||
|
||||
|
||||
## Verifies that the current value is not null.
|
||||
@abstract func is_not_null() -> GdUnitVectorAssert
|
||||
|
||||
|
||||
## Verifies that the current value is equal to the given one.
|
||||
@abstract func is_equal(expected: Variant) -> GdUnitVectorAssert
|
||||
|
||||
|
||||
## Verifies that the current value is not equal to expected one.
|
||||
@abstract func is_not_equal(expected: Variant) -> GdUnitVectorAssert
|
||||
|
||||
|
||||
## Verifies that the current and expected value are approximately equal.
|
||||
@abstract func is_equal_approx(expected: Variant, approx: Variant) -> GdUnitVectorAssert
|
||||
|
||||
|
||||
## Overrides the default failure message by given custom message.
|
||||
@abstract func override_failure_message(message: String) -> GdUnitVectorAssert
|
||||
|
||||
|
||||
## Appends a custom message to the failure message.
|
||||
@abstract func append_failure_message(message: String) -> GdUnitVectorAssert
|
||||
|
||||
|
||||
## Verifies that the current value is less than the given one.
|
||||
@abstract func is_less(expected: Variant) -> GdUnitVectorAssert
|
||||
|
||||
|
||||
## Verifies that the current value is less than or equal the given one.
|
||||
@abstract func is_less_equal(expected: Variant) -> GdUnitVectorAssert
|
||||
|
||||
|
||||
## Verifies that the current value is greater than the given one.
|
||||
@abstract func is_greater(expected: Variant) -> GdUnitVectorAssert
|
||||
|
||||
|
||||
## Verifies that the current value is greater than or equal the given one.
|
||||
@abstract func is_greater_equal(expected: Variant) -> GdUnitVectorAssert
|
||||
|
||||
|
||||
## Verifies that the current value is between the given boundaries (inclusive).
|
||||
@abstract func is_between(from: Variant, to: Variant) -> GdUnitVectorAssert
|
||||
|
||||
|
||||
## Verifies that the current value is not between the given boundaries (inclusive).
|
||||
@abstract func is_not_between(from: Variant, to: Variant) -> GdUnitVectorAssert
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user