Compare commits
@@ -44,7 +44,7 @@ Named after Qatux, the Raiel with perfect memory who helped Paula Myo by recalli
|
|||||||
- **Work in dedicated round files:** All new rounds happen in `docs/discussions/round-NN-topic.md` from the start. DISCUSSION.md is retired for new content.
|
- **Work in dedicated round files:** All new rounds happen in `docs/discussions/round-NN-topic.md` from the start. DISCUSSION.md is retired for new content.
|
||||||
- **Update the discussion index ONLY when closing:** After a round is formally closed, update `docs/discussions/README.md` with the round entry (number, topic, decisions produced, file link).
|
- **Update the discussion index ONLY when closing:** After a round is formally closed, update `docs/discussions/README.md` with the round entry (number, topic, decisions produced, file link).
|
||||||
- **Update briefings:** After a round produces new decisions, update the relevant agent briefing files in `docs/briefings/`.
|
- **Update briefings:** After a round produces new decisions, update the relevant agent briefing files in `docs/briefings/`.
|
||||||
- **Re-index documents:** After archiving or updating documents, re-index them in Qdrant via `db/connectors/qdrant-index <path>`.
|
- **Re-index documents:** After archiving or updating documents, re-index them in Qdrant via `tooling/db/qdrant-index <path>`.
|
||||||
|
|
||||||
## Team workflow (mandatory)
|
## Team workflow (mandatory)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Local Services
|
# Local Services
|
||||||
|
|
||||||
Endpoints are also preconfigured in `db/connectors/config.json`.
|
Endpoints are also preconfigured in `tooling/db/config.json`.
|
||||||
|
|
||||||
- **Gitea:** `http://git.schweitz.internal` (login: `schweitz`)
|
- **Gitea:** `http://git.schweitz.internal` (login: `schweitz`)
|
||||||
- **Qdrant:** `http://tower-of-joy:6333/`
|
- **Qdrant:** `http://tower-of-joy:6333/`
|
||||||
|
|||||||
@@ -17,11 +17,14 @@ docs/
|
|||||||
workshops/ # Workshop briefs and outputs
|
workshops/ # Workshop briefs and outputs
|
||||||
db/
|
db/
|
||||||
schema.sql # Database schema
|
schema.sql # Database schema
|
||||||
connectors/ # Connector scripts for SQLite and Qdrant
|
tooling/
|
||||||
|
db/ # Connector scripts for SQLite, Qdrant, and audio
|
||||||
config.json # Endpoint configuration
|
config.json # Endpoint configuration
|
||||||
ticket # Ticket CLI
|
ticket # Ticket CLI
|
||||||
|
sprint # Sprint lifecycle CLI
|
||||||
sqlite_connector.py # SQLite mini MCP
|
sqlite_connector.py # SQLite mini MCP
|
||||||
qdrant_connector.py # Qdrant + ollama mini MCP
|
qdrant_connector.py # Qdrant + ollama mini MCP
|
||||||
|
audio_connector.py # Stable Audio Open connector
|
||||||
.claude/
|
.claude/
|
||||||
agents/ # Agent personality files
|
agents/ # Agent personality files
|
||||||
skills/ # Skill definitions
|
skills/ # Skill definitions
|
||||||
|
|||||||
@@ -25,20 +25,22 @@
|
|||||||
"Bash(git ls-tree *)",
|
"Bash(git ls-tree *)",
|
||||||
"Bash(git rev-parse --show-toplevel)",
|
"Bash(git rev-parse --show-toplevel)",
|
||||||
|
|
||||||
"Bash(db/connectors/ticket *)",
|
"Bash(tooling/db/ticket *)",
|
||||||
"Bash(db/connectors/sprint *)",
|
"Bash(tooling/db/sprint *)",
|
||||||
"Bash(db/connectors/sqlite-query *)",
|
"Bash(tooling/db/sqlite-query *)",
|
||||||
"Bash(db/connectors/sqlite-exec *)",
|
"Bash(tooling/db/sqlite-exec *)",
|
||||||
"Bash(db/connectors/qdrant-search *)",
|
"Bash(tooling/db/qdrant-search *)",
|
||||||
"Bash(db/connectors/qdrant-index *)",
|
"Bash(tooling/db/qdrant-index *)",
|
||||||
"Bash(db/connectors/qdrant-health)",
|
"Bash(tooling/db/qdrant-health)",
|
||||||
"Bash(db/connectors/qdrant-count)",
|
"Bash(tooling/db/qdrant-count)",
|
||||||
"Bash(db/connectors/sqlite-init)",
|
"Bash(tooling/db/sqlite-init)",
|
||||||
"Bash(db/connectors/decisions-sync)",
|
"Bash(tooling/db/decisions-sync)",
|
||||||
|
"Bash(tooling/db/decision *)",
|
||||||
|
|
||||||
"Bash(db/connectors/audio-generate *)",
|
"Bash(tooling/db/audio-generate *)",
|
||||||
"Bash(db/connectors/audio-health)",
|
"Bash(tooling/db/audio-health)",
|
||||||
"Bash(db/connectors/audio-post *)",
|
"Bash(tooling/db/audio-post *)",
|
||||||
|
"Bash(tooling/db/audio-batch *)",
|
||||||
|
|
||||||
"Bash(make *)",
|
"Bash(make *)",
|
||||||
"Bash(make)",
|
"Bash(make)",
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ description: >
|
|||||||
# Audio Generation — The Settled Reach
|
# Audio Generation — The Settled Reach
|
||||||
|
|
||||||
Generate sonically consistent audio assets using the Stable Audio Open API via
|
Generate sonically consistent audio assets using the Stable Audio Open API via
|
||||||
wrapper scripts at `db/connectors/audio-*`.
|
wrapper scripts at `tooling/db/audio-*`.
|
||||||
|
|
||||||
Asset descriptions, filenames, bus routing, and design intent are documented in
|
Asset descriptions, filenames, bus routing, and design intent are documented in
|
||||||
`docs/assets/audio/`. This skill provides the prompt system, generation
|
`docs/assets/audio/`. This skill provides the prompt system, generation
|
||||||
@@ -25,21 +25,21 @@ workflow, and quality validation.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Check API health
|
# Check API health
|
||||||
db/connectors/audio-health
|
tooling/db/audio-health
|
||||||
|
|
||||||
# Generate a single asset (WAV only)
|
# Generate a single asset (WAV only)
|
||||||
db/connectors/audio-generate "prompt text" \
|
tooling/db/audio-generate "prompt text" \
|
||||||
--duration 10 --steps 100 --cfg 7 \
|
--duration 10 --steps 100 --cfg 7 \
|
||||||
--output path/to/output.wav
|
--output path/to/output.wav
|
||||||
|
|
||||||
# Generate + post-process in one command (WAV → trim → normalize → OGG)
|
# Generate + post-process in one command (WAV → trim → normalize → OGG)
|
||||||
db/connectors/audio-generate "prompt text" \
|
tooling/db/audio-generate "prompt text" \
|
||||||
--duration 10 --steps 100 --cfg 7 \
|
--duration 10 --steps 100 --cfg 7 \
|
||||||
--output path/to/gen/intermediate.wav \
|
--output path/to/gen/intermediate.wav \
|
||||||
--output-ogg client/assets/audio/final.ogg
|
--output-ogg client/assets/audio/final.ogg
|
||||||
|
|
||||||
# Batch-generate from a manifest (preferred for multiple assets)
|
# Batch-generate from a manifest (preferred for multiple assets)
|
||||||
db/connectors/audio-batch docs/assets/audio/batch-s10-327.json
|
tooling/db/audio-batch docs/assets/audio/batch-s10-327.json
|
||||||
```
|
```
|
||||||
|
|
||||||
### Parameters
|
### Parameters
|
||||||
@@ -138,16 +138,16 @@ AMB-001, SFX-002, UI-005). This couples the manifest to the asset inventory.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Full run
|
# Full run
|
||||||
db/connectors/audio-batch docs/assets/audio/batch-s10-327.json
|
tooling/db/audio-batch docs/assets/audio/batch-s10-327.json
|
||||||
|
|
||||||
# Dry run — preview what would be generated
|
# Dry run — preview what would be generated
|
||||||
db/connectors/audio-batch docs/assets/audio/batch-s10-327.json --dry-run
|
tooling/db/audio-batch docs/assets/audio/batch-s10-327.json --dry-run
|
||||||
|
|
||||||
# Generate only specific assets
|
# Generate only specific assets
|
||||||
db/connectors/audio-batch docs/assets/audio/batch-s10-327.json --only AMB-001,AMB-002
|
tooling/db/audio-batch docs/assets/audio/batch-s10-327.json --only AMB-001,AMB-002
|
||||||
|
|
||||||
# Skip assets that already have OGG files
|
# Skip assets that already have OGG files
|
||||||
db/connectors/audio-batch docs/assets/audio/batch-s10-327.json --skip-existing
|
tooling/db/audio-batch docs/assets/audio/batch-s10-327.json --skip-existing
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Update asset docs with prompts
|
### 3. Update asset docs with prompts
|
||||||
@@ -190,8 +190,8 @@ For one-off generation or iteration on a specific asset:
|
|||||||
2. Read `references/sonic-palette.md` for the sonic family prefix.
|
2. Read `references/sonic-palette.md` for the sonic family prefix.
|
||||||
3. Read `references/category-templates.md` for the matching template.
|
3. Read `references/category-templates.md` for the matching template.
|
||||||
4. Assemble the full prompt.
|
4. Assemble the full prompt.
|
||||||
5. Run `db/connectors/audio-health` to verify the API is up.
|
5. Run `tooling/db/audio-health` to verify the API is up.
|
||||||
6. Run `db/connectors/audio-generate` with `--post` or `--output-ogg` to
|
6. Run `tooling/db/audio-generate` with `--post` or `--output-ogg` to
|
||||||
generate and post-process in one step.
|
generate and post-process in one step.
|
||||||
7. Verify the output (file size, duration).
|
7. Verify the output (file size, duration).
|
||||||
8. Update the asset status and prompt in `docs/assets/audio/{category}.md`.
|
8. Update the asset status and prompt in `docs/assets/audio/{category}.md`.
|
||||||
@@ -218,12 +218,12 @@ If you need to post-process separately (e.g., re-normalizing an existing file):
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Full pipeline: trim → normalize → convert
|
# Full pipeline: trim → normalize → convert
|
||||||
db/connectors/audio-post pipeline input.wav --output output.ogg
|
tooling/db/audio-post pipeline input.wav --output output.ogg
|
||||||
|
|
||||||
# Individual steps
|
# Individual steps
|
||||||
db/connectors/audio-post trim input.wav
|
tooling/db/audio-post trim input.wav
|
||||||
db/connectors/audio-post normalize input.wav --lufs -16
|
tooling/db/audio-post normalize input.wav --lufs -16
|
||||||
db/connectors/audio-post convert input.wav --output output.ogg
|
tooling/db/audio-post convert input.wav --output output.ogg
|
||||||
```
|
```
|
||||||
|
|
||||||
## Manual Synthesis (Insert-Tech Sounds)
|
## Manual Synthesis (Insert-Tech Sounds)
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ Construct the ticket title and description from the report summary and any
|
|||||||
investigation findings. Use the ticket CLI:
|
investigation findings. Use the ticket CLI:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
db/connectors/ticket create bug "{title}" --team {team} --description "{description}"
|
tooling/db/ticket create bug "{title}" --team {team} --description "{description}"
|
||||||
```
|
```
|
||||||
|
|
||||||
The description should include:
|
The description should include:
|
||||||
|
|||||||
@@ -20,14 +20,14 @@ and workflows.
|
|||||||
|
|
||||||
For precise indexing of specific content:
|
For precise indexing of specific content:
|
||||||
```bash
|
```bash
|
||||||
python3 db/connectors/qdrant_connector.py index "unique-id" "Text content to index" --metadata source=manual heading="Custom heading"
|
python3 tooling/db/qdrant_connector.py index "unique-id" "Text content to index" --metadata source=manual heading="Custom heading"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Create collection
|
### Create collection
|
||||||
|
|
||||||
Initialize the Qdrant collection (run once during setup):
|
Initialize the Qdrant collection (run once during setup):
|
||||||
```bash
|
```bash
|
||||||
python3 db/connectors/qdrant_connector.py create-collection
|
python3 tooling/db/qdrant_connector.py create-collection
|
||||||
```
|
```
|
||||||
|
|
||||||
## Bulk Indexing
|
## Bulk Indexing
|
||||||
@@ -35,7 +35,7 @@ python3 db/connectors/qdrant_connector.py create-collection
|
|||||||
Index all project documents at once:
|
Index all project documents at once:
|
||||||
```bash
|
```bash
|
||||||
for f in decisions/*.md DISCUSSION.md TEAM.md docs/discussions/*.md docs/briefings/*.md; do
|
for f in decisions/*.md DISCUSSION.md TEAM.md docs/discussions/*.md docs/briefings/*.md; do
|
||||||
db/connectors/qdrant-index "$f"
|
tooling/db/qdrant-index "$f"
|
||||||
done
|
done
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ Extract ticket IDs from `#NNN` patterns. For each ticket that is
|
|||||||
currently `in_progress`, update it to `review`:
|
currently `in_progress`, update it to `review`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
db/connectors/ticket status <id> review
|
tooling/db/ticket status <id> review
|
||||||
```
|
```
|
||||||
|
|
||||||
Report which tickets were moved to review. Skip tickets that are
|
Report which tickets were moved to review. Skip tickets that are
|
||||||
|
|||||||
@@ -83,12 +83,12 @@ raw diff to reviewers — cleaner context, better reviews.
|
|||||||
worktrees. Each team branch is checked out at:
|
worktrees. Each team branch is checked out at:
|
||||||
|
|
||||||
```
|
```
|
||||||
/var/home/jeroenschweitzer/Projects/settled-reach/<branch>/
|
/var/mnt/data/projects/settled-reach/<branch>/
|
||||||
```
|
```
|
||||||
|
|
||||||
For example, the `copy` branch lives at:
|
For example, the `copy` branch lives at:
|
||||||
```
|
```
|
||||||
/var/home/jeroenschweitzer/Projects/settled-reach/copy/content/dialogue/...
|
/var/mnt/data/projects/settled-reach/copy/content/dialogue/...
|
||||||
```
|
```
|
||||||
|
|
||||||
**All reviewer agents** (regardless of Bash access) should read source files
|
**All reviewer agents** (regardless of Bash access) should read source files
|
||||||
@@ -103,10 +103,10 @@ worktree path. Example instruction for agents:
|
|||||||
|
|
||||||
```
|
```
|
||||||
Read the changed files from the branch worktree. The branch is checked
|
Read the changed files from the branch worktree. The branch is checked
|
||||||
out at: /var/home/jeroenschweitzer/Projects/settled-reach/<branch>/
|
out at: /var/mnt/data/projects/settled-reach/<branch>/
|
||||||
|
|
||||||
For example, to read `content/dialogue/the-terminal/kael-davan.yaml`,
|
For example, to read `content/dialogue/the-terminal/kael-davan.yaml`,
|
||||||
use: /var/home/jeroenschweitzer/Projects/settled-reach/<branch>/content/dialogue/the-terminal/kael-davan.yaml
|
use: /var/mnt/data/projects/settled-reach/<branch>/content/dialogue/the-terminal/kael-davan.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
Also tell agents to read relevant `decisions/*.md` files from the same
|
Also tell agents to read relevant `decisions/*.md` files from the same
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
Use `model: sonnet` for all reviewers — sufficient for review, saves cost.
|
Use `model: sonnet` for all reviewers — sufficient for review, saves cost.
|
||||||
|
|
||||||
**All agents read from worktree paths.** Each branch is checked out at:
|
**All agents read from worktree paths.** Each branch is checked out at:
|
||||||
`/var/home/jeroenschweitzer/Projects/settled-reach/<branch>/`
|
`/var/mnt/data/projects/settled-reach/<branch>/`
|
||||||
|
|
||||||
Tell every reviewer agent to read source files from the worktree using the
|
Tell every reviewer agent to read source files from the worktree using the
|
||||||
Read tool. Include the worktree base path and a list of changed files in
|
Read tool. Include the worktree base path and a list of changed files in
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ the user and a purpose-assembled agent panel.
|
|||||||
Get carry-overs, backlog candidates, and decision gaps in one shot:
|
Get carry-overs, backlog candidates, and decision gaps in one shot:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
db/connectors/sprint prepare
|
tooling/db/sprint prepare
|
||||||
```
|
```
|
||||||
|
|
||||||
This auto-detects the next sprint number (max ID + 1), creates the sprint
|
This auto-detects the next sprint number (max ID + 1), creates the sprint
|
||||||
@@ -103,10 +103,10 @@ record in `planning` status if needed, and outputs:
|
|||||||
For critical epics, check their children for granular candidates:
|
For critical epics, check their children for granular candidates:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
db/connectors/ticket children <epic_id>
|
tooling/db/ticket children <epic_id>
|
||||||
```
|
```
|
||||||
|
|
||||||
Use `db/connectors/ticket show --brief <id> [<id>...]` to quickly scan multiple tickets.
|
Use `tooling/db/ticket show --brief <id> [<id>...]` to quickly scan multiple tickets.
|
||||||
|
|
||||||
### 3. Read existing code state
|
### 3. Read existing code state
|
||||||
|
|
||||||
@@ -184,14 +184,14 @@ Update it with the theme and goal, then assign tickets:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Update the sprint with theme and goal
|
# Update the sprint with theme and goal
|
||||||
db/connectors/sqlite-exec "UPDATE sprints SET name='Sprint N: Theme', goal='goal' WHERE id=N"
|
tooling/db/sqlite-exec "UPDATE sprints SET name='Sprint N: Theme', goal='goal' WHERE id=N"
|
||||||
|
|
||||||
# Assign tickets
|
# Assign tickets
|
||||||
db/connectors/ticket sprint assign <ticket_id> <sprint_id>
|
tooling/db/ticket sprint assign <ticket_id> <sprint_id>
|
||||||
```
|
```
|
||||||
|
|
||||||
The sprint stays in `planning` status until explicitly activated via
|
The sprint stays in `planning` status until explicitly activated via
|
||||||
`db/connectors/sprint start`. This prevents starting an unplanned sprint.
|
`tooling/db/sprint start`. This prevents starting an unplanned sprint.
|
||||||
|
|
||||||
### 8. Present summary
|
### 8. Present summary
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ Each team gets one briefing file at `docs/sprints/sprint-N/<team>.md`.
|
|||||||
|---|-------|------------|
|
|---|-------|------------|
|
||||||
| #ID | Title | #dependency or — |
|
| #ID | Title | #dependency or — |
|
||||||
|
|
||||||
Use `db/connectors/ticket show <id>` for full details.
|
Use `tooling/db/ticket show <id>` for full details.
|
||||||
|
|
||||||
## Key Decisions
|
## Key Decisions
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ When `/sprint-start` is run on `main`, assess the current sprint state
|
|||||||
and do the next right thing. Query the database to determine the state:
|
and do the next right thing. Query the database to determine the state:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
db/connectors/sqlite-query "SELECT id, name, status FROM sprints ORDER BY id DESC LIMIT 3"
|
tooling/db/sqlite-query "SELECT id, name, status FROM sprints ORDER BY id DESC LIMIT 3"
|
||||||
```
|
```
|
||||||
|
|
||||||
Then follow the **first matching case**:
|
Then follow the **first matching case**:
|
||||||
@@ -48,7 +48,7 @@ Then follow the **first matching case**:
|
|||||||
First, check whether the sprint's work is actually done:
|
First, check whether the sprint's work is actually done:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
db/connectors/sprint status
|
tooling/db/sprint status
|
||||||
```
|
```
|
||||||
|
|
||||||
This shows ticket counts by status (done, in_progress, backlog).
|
This shows ticket counts by status (done, in_progress, backlog).
|
||||||
@@ -81,7 +81,7 @@ explicitly chooses to close.
|
|||||||
#### A1. Close the active sprint
|
#### A1. Close the active sprint
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
db/connectors/sprint stop
|
tooling/db/sprint stop
|
||||||
```
|
```
|
||||||
|
|
||||||
This marks the active sprint as completed and lists carry-over candidates.
|
This marks the active sprint as completed and lists carry-over candidates.
|
||||||
@@ -142,7 +142,7 @@ A sprint is ready to activate. Verify it looks complete:
|
|||||||
```
|
```
|
||||||
2. Check the ticket count:
|
2. Check the ticket count:
|
||||||
```bash
|
```bash
|
||||||
db/connectors/sprint status --sprint N
|
tooling/db/sprint status --sprint N
|
||||||
```
|
```
|
||||||
|
|
||||||
If briefings are missing or the sprint has 0 tickets, report the gap
|
If briefings are missing or the sprint has 0 tickets, report the gap
|
||||||
@@ -151,7 +151,7 @@ and suggest running `/sprint-plan` to complete planning.
|
|||||||
If everything looks ready, activate the sprint:
|
If everything looks ready, activate the sprint:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
db/connectors/sprint start
|
tooling/db/sprint start
|
||||||
```
|
```
|
||||||
|
|
||||||
Then report:
|
Then report:
|
||||||
@@ -184,7 +184,7 @@ If the merge has conflicts, report them and stop — do not force-resolve.
|
|||||||
Run the sprint CLI to get the full context dump in one shot:
|
Run the sprint CLI to get the full context dump in one shot:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
db/connectors/sprint start-work
|
tooling/db/sprint start-work
|
||||||
```
|
```
|
||||||
|
|
||||||
This auto-detects the active sprint and current team from the branch.
|
This auto-detects the active sprint and current team from the branch.
|
||||||
@@ -204,7 +204,7 @@ If no matching briefing exists for the team, suggest running
|
|||||||
|
|
||||||
For tickets that need more detail than the `start-work` summary provides:
|
For tickets that need more detail than the `start-work` summary provides:
|
||||||
```bash
|
```bash
|
||||||
db/connectors/ticket show <id>
|
tooling/db/ticket show <id>
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6. Read key decisions
|
### 6. Read key decisions
|
||||||
@@ -218,7 +218,7 @@ Mark all actionable (unblocked, non-done) tickets in the sprint as
|
|||||||
`in_progress`:
|
`in_progress`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
db/connectors/ticket status <id> in_progress
|
tooling/db/ticket status <id> in_progress
|
||||||
```
|
```
|
||||||
|
|
||||||
Then output a summary:
|
Then output a summary:
|
||||||
@@ -296,8 +296,8 @@ Task(
|
|||||||
|
|
||||||
2. DB SCRIPTS: When calling ticket/sprint/sqlite scripts, use
|
2. DB SCRIPTS: When calling ticket/sprint/sqlite scripts, use
|
||||||
the exact command with no wrappers or chaining. Examples:
|
the exact command with no wrappers or chaining. Examples:
|
||||||
db/connectors/ticket show 528
|
tooling/db/ticket show 528
|
||||||
db/connectors/ticket list --sprint {N}
|
tooling/db/ticket list --sprint {N}
|
||||||
Do NOT prepend python3, do NOT chain with && or ;, do NOT
|
Do NOT prepend python3, do NOT chain with && or ;, do NOT
|
||||||
add cleanup commands. Just the bare command.
|
add cleanup commands. Just the bare command.
|
||||||
|
|
||||||
@@ -346,7 +346,7 @@ Task(
|
|||||||
7. If no tasks remain, message the team lead. Do NOT shut down
|
7. If no tasks remain, message the team lead. Do NOT shut down
|
||||||
on your own.
|
on your own.
|
||||||
|
|
||||||
Use `db/connectors/ticket show <id>` for full ticket specs.",
|
Use `tooling/db/ticket show <id>` for full ticket specs.",
|
||||||
description: "Sprint {N} {team}: {name}",
|
description: "Sprint {N} {team}: {name}",
|
||||||
run_in_background: true
|
run_in_background: true
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ the workflow.
|
|||||||
Run these two commands in parallel:
|
Run these two commands in parallel:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
db/connectors/sprint sweep
|
tooling/db/sprint sweep
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -17,60 +17,60 @@ section. This skill covers the full command reference.
|
|||||||
|
|
||||||
### List tickets (full flags)
|
### List tickets (full flags)
|
||||||
```bash
|
```bash
|
||||||
db/connectors/ticket list [--status S] [--priority P] [--epic N] [--sprint N] [--assigned A] [--team T]
|
tooling/db/ticket list [--status S] [--priority P] [--epic N] [--sprint N] [--assigned A] [--team T]
|
||||||
```
|
```
|
||||||
|
|
||||||
### Create ticket
|
### Create ticket
|
||||||
```bash
|
```bash
|
||||||
db/connectors/ticket create <type> <title> [--parent N] [--priority P] [--decision D] [--team T]
|
tooling/db/ticket create <type> <title> [--parent N] [--priority P] [--decision D] [--team T]
|
||||||
```
|
```
|
||||||
Types: `initiative`, `epic`, `story`, `task`, `bug`
|
Types: `initiative`, `epic`, `story`, `task`, `bug`
|
||||||
Priorities: `critical`, `high`, `medium`, `low`
|
Priorities: `critical`, `high`, `medium`, `low`
|
||||||
|
|
||||||
### Update status
|
### Update status
|
||||||
```bash
|
```bash
|
||||||
db/connectors/ticket status <id> <new_status>
|
tooling/db/ticket status <id> <new_status>
|
||||||
db/connectors/ticket done <id> [<id> ...]
|
tooling/db/ticket done <id> [<id> ...]
|
||||||
```
|
```
|
||||||
Statuses: `backlog`, `ready`, `in_progress`, `review`, `done`, `cancelled`
|
Statuses: `backlog`, `ready`, `in_progress`, `review`, `done`, `cancelled`
|
||||||
|
|
||||||
### Assignment
|
### Assignment
|
||||||
```bash
|
```bash
|
||||||
db/connectors/ticket assign <id> <agent>
|
tooling/db/ticket assign <id> <agent>
|
||||||
db/connectors/ticket unassign <id>
|
tooling/db/ticket unassign <id>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Team assignment
|
### Team assignment
|
||||||
```bash
|
```bash
|
||||||
db/connectors/ticket team <id> <teams>
|
tooling/db/ticket team <id> <teams>
|
||||||
```
|
```
|
||||||
Teams are comma-separated, e.g. `server`, `client`, `server,client`.
|
Teams are comma-separated, e.g. `server`, `client`, `server,client`.
|
||||||
|
|
||||||
### Sprint management
|
### Sprint management
|
||||||
```bash
|
```bash
|
||||||
db/connectors/ticket sprint [--active]
|
tooling/db/ticket sprint [--active]
|
||||||
db/connectors/ticket sprint assign <id> <sprint_id>
|
tooling/db/ticket sprint assign <id> <sprint_id>
|
||||||
```
|
```
|
||||||
|
|
||||||
For sprint-scoped operations (status overview, context dumps, lifecycle),
|
For sprint-scoped operations (status overview, context dumps, lifecycle),
|
||||||
use the dedicated sprint CLI instead: `db/connectors/sprint --help`
|
use the dedicated sprint CLI instead: `tooling/db/sprint --help`
|
||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
```bash
|
```bash
|
||||||
db/connectors/ticket deps <id>
|
tooling/db/ticket deps <id>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Search and browse
|
### Search and browse
|
||||||
```bash
|
```bash
|
||||||
db/connectors/ticket search <keyword>
|
tooling/db/ticket search <keyword>
|
||||||
db/connectors/ticket epics [--status S]
|
tooling/db/ticket epics [--status S]
|
||||||
db/connectors/ticket children <id>
|
tooling/db/ticket children <id>
|
||||||
db/connectors/ticket count [--status S]
|
tooling/db/ticket count [--status S]
|
||||||
```
|
```
|
||||||
|
|
||||||
### Batch show
|
### Batch show
|
||||||
```bash
|
```bash
|
||||||
db/connectors/ticket show --brief <id> [<id>...]
|
tooling/db/ticket show --brief <id> [<id>...]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|||||||
@@ -6,6 +6,44 @@ Format based on [Keep a Changelog](https://keepachangelog.com/).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [v0.1.22] — 2026-03-03
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Visual test harness — `make screenshot`, `make test-visual`, `make visual-update` for automated visual regression testing with golden PNGs across 11 scenarios (fog, HUD, dialogue, minimap)
|
||||||
|
- Visual movie mode — `make visual-movie` captures interaction flows as frame sequences with contact sheet generation
|
||||||
|
- World seed protocol — StartupMessage carries world_seed from client to server after handshake, enabling deterministic NPC population seeding (D-010, D-029)
|
||||||
|
- EntanglementConfig — per-seed NPC population ratios (flat/mundane/intrigue) sampled from seeded RNG with D-029 bounds, ensuring same seed = same world (#175, #178)
|
||||||
|
- Fog debug mode — toggle FogState.debug_exploration to render raw exploration texture as colored overlay for diagnostic use
|
||||||
|
- D-110 through D-112: z-level addressing, subterranean architecture, no instancing decisions
|
||||||
|
- Q-051: speech bubble indicator over speaking NPCs
|
||||||
|
- Sprint 22 "Wire" briefings (server, client, visual, CI, planning, joint)
|
||||||
|
|
||||||
|
### Added (server)
|
||||||
|
- Production NPC pool generation — 23 authored Sova NPCs spawn with EntanglementTag (Flat/Intrigue) based on triangle membership (#176, D-029)
|
||||||
|
- Authored triangle instantiation — 5 Sova triangles (3 active forks, 2 passive tensions) loaded from content YAML with deterministic IDs (#188, D-087)
|
||||||
|
- Contamination activation mechanic — timer-based storyteller fires after 30 game-minutes, pressures active triangles, emits ContaminationEvent (#254)
|
||||||
|
- Modifications data model stub — Vec<Modification> on chunk entities, round-trips through save/load for future construction DLC (#567, D-112)
|
||||||
|
- Zone Gate gauntlet room — two-zone test room with door boundary, zone crossing detection system (#512)
|
||||||
|
- Fuzzy map tests — 50-seed randomized testing of procedural maps against 4 structural invariants (#509)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fog shader: silent compilation failure in OpenGL3 compat mode — removed `return` statements from fragment() which are not supported, causing fog overlay to render as no-op (root cause of Sprint 22 fog regression)
|
||||||
|
- Fog system: blocky stair-stepped edges at vision cone boundary — doubled Gaussian blur step size for D-066 compliant 6-8 tile smooth gradient (#569)
|
||||||
|
- Fog system: zero visibility in explored areas — switched bounds calculation from visible_tiles (empty in live server mode) to visible_positions, and removed shader guard that cut off gradient bleed into unexplored tiles (#569)
|
||||||
|
- Fog shader alpha tuned to D-059 spec: light fog 0.25-0.35 (was 0.25-0.55), deep fog 0.55-0.70 (was 0.78-0.90) — world content now visible through fog instead of hidden behind it (#563)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Fog shader now distinguishes light fog (near cone, neutral dark) from deep fog (far from cone, zone temperature tint) with separate Perlin noise breathing cycles (8-10s / 15-20s)
|
||||||
|
- Zone temperature tint populated per-tile from server zone_id: bar=warm amber-dark, hub=cool blue-dark, corridor=neutral dark (D-059/D-046/D-077)
|
||||||
|
- Simplified vision cone from 3-sector (forward/peripheral/blind) to forward-only 120° arc — server sends only forward-cone tiles, client renders explored tiles behind the player with light fog overlay
|
||||||
|
- Simplified fog shader from 5-layer to 3-layer model (clear, explored, unexplored)
|
||||||
|
- Fog texture resize now preserves exploration data — tiles behind the player stay as light fog instead of reverting to unexplored black
|
||||||
|
- Updated D-015/D-017 perception decisions to reflect simplified cone model
|
||||||
|
- Moved connector scripts from db/connectors/ to tooling/db/ (#274) — backwards-compat symlink removed in #568
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- db/connectors symlink — all references now use tooling/db/ directly (#568)
|
||||||
|
|
||||||
## [v0.1.20] — 2026-02-25
|
## [v0.1.20] — 2026-02-25
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ server/ # Rust/bevy_ecs simulation server
|
|||||||
tooling/ # Build tools, scripts, asset pipelines
|
tooling/ # Build tools, scripts, asset pipelines
|
||||||
tests/ # Integration and end-to-end tests
|
tests/ # Integration and end-to-end tests
|
||||||
docs/ # Architecture, design, briefings, sprints, workshops
|
docs/ # Architecture, design, briefings, sprints, workshops
|
||||||
db/ # Schema + connector scripts (ticket CLI, SQLite, Qdrant)
|
db/ # Schema + seed data (connectors moved to tooling/db/)
|
||||||
.claude/ # Agents, skills, rules
|
.claude/ # Agents, skills, rules
|
||||||
decisions/ # Decision domain files (D-NNN confirmed, Q-NNN open, R-NNN rejected)
|
decisions/ # Decision domain files (D-NNN confirmed, Q-NNN open, R-NNN rejected)
|
||||||
```
|
```
|
||||||
@@ -42,7 +42,7 @@ The ticketing database (`settledreach.db`) lives in the **parent directory** sha
|
|||||||
|
|
||||||
### Before starting work
|
### Before starting work
|
||||||
1. Read your sprint briefing at `docs/sprints/sprint-N/{team}.md` for current tasks
|
1. Read your sprint briefing at `docs/sprints/sprint-N/{team}.md` for current tasks
|
||||||
2. Use `db/connectors/ticket show <id>` for full ticket details
|
2. Use `tooling/db/ticket show <id>` for full ticket details
|
||||||
3. Read the relevant `decisions/*.md` domain file(s) referenced in the briefing
|
3. Read the relevant `decisions/*.md` domain file(s) referenced in the briefing
|
||||||
4. Background context: `docs/briefings/{your-name}.md`, `docs/discussions/`
|
4. Background context: `docs/briefings/{your-name}.md`, `docs/discussions/`
|
||||||
|
|
||||||
@@ -52,19 +52,19 @@ The ticketing database (`settledreach.db`) lives in the **parent directory** sha
|
|||||||
|
|
||||||
| Tool | Command | Full reference |
|
| Tool | Command | Full reference |
|
||||||
|------|---------|----------------|
|
|------|---------|----------------|
|
||||||
| Tickets | `db/connectors/ticket list`, `show`, `create`, `assign` | `/ticket` skill |
|
| Tickets | `tooling/db/ticket list`, `show`, `create`, `assign` | `/ticket` skill |
|
||||||
| Sprints | `db/connectors/sprint status`, `start-work`, `prepare` | `/sprint-start` skill |
|
| Sprints | `tooling/db/sprint status`, `start-work`, `prepare` | `/sprint-start` skill |
|
||||||
| SQL queries | `db/connectors/sqlite-query "SELECT ..."` | — |
|
| SQL queries | `tooling/db/sqlite-query "SELECT ..."` | — |
|
||||||
| SQL writes | `db/connectors/sqlite-exec "UPDATE ..."` | — |
|
| SQL writes | `tooling/db/sqlite-exec "UPDATE ..."` | — |
|
||||||
| Decisions | `db/connectors/decision next`, `claim`, `check-dupes` | — |
|
| Decisions | `tooling/db/decision next`, `claim`, `check-dupes` | — |
|
||||||
| Doc search | `db/connectors/qdrant-search "query"` | `/docs-search` skill |
|
| Doc search | `tooling/db/qdrant-search "query"` | `/docs-search` skill |
|
||||||
| Doc index | `db/connectors/qdrant-index path/to/file.md` | `/docs-search` skill |
|
| Doc index | `tooling/db/qdrant-index path/to/file.md` | `/docs-search` skill |
|
||||||
|
|
||||||
### File conventions
|
### File conventions
|
||||||
- Decisions: domain files in `decisions/` (see `decisions/README.md` for index)
|
- Decisions: domain files in `decisions/` (see `decisions/README.md` for index)
|
||||||
- Decision IDs: `D-NNN` (confirmed), `Q-NNN` (open questions), `R-NNN` (rejected)
|
- Decision IDs: `D-NNN` (confirmed), `Q-NNN` (open questions), `R-NNN` (rejected)
|
||||||
- **Claim IDs before writing:** `db/connectors/decision claim D <domain> "title"` — prevents ID collisions across worktrees
|
- **Claim IDs before writing:** `tooling/db/decision claim D <domain> "title"` — prevents ID collisions across worktrees
|
||||||
- Diagrams: `.d2` source + `.png` renders in `docs/diagrams/{category}/`. Create or update diagrams via `/d2-diagram` when D-records are added or modified.
|
- Diagrams: `.d2` source + `.png` renders in `docs/diagrams/{category}/`. Create or update diagrams via `/d2-diagram` when D-records are added or modified.
|
||||||
- Discussion rounds: numbered sequentially, archived to `docs/discussions/` when complete
|
- Discussion rounds: numbered sequentially, archived to `docs/discussions/` when complete
|
||||||
- Briefings: one per agent, updated after decision-producing rounds
|
- Briefings: one per agent, updated after decision-producing rounds
|
||||||
- Tickets: managed via `db/connectors/ticket` CLI or `/ticket` skill
|
- Tickets: managed via `tooling/db/ticket` CLI or `/ticket` skill
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ GODOT := $(shell command -v godot4 2>/dev/null || command -v godot 2>/dev/null)
|
|||||||
fixtures-client golden-diff golden-update \
|
fixtures-client golden-diff golden-update \
|
||||||
checklist-validate checklist-generate \
|
checklist-validate checklist-generate \
|
||||||
perf-baseline debug-schedule \
|
perf-baseline debug-schedule \
|
||||||
test-ipc-fixtures test-ipc-protocol test-ipc-integration test-ipc-benchmark
|
test-ipc-fixtures test-ipc-protocol test-ipc-integration test-ipc-benchmark \
|
||||||
|
screenshot visual-movie test-visual visual-update
|
||||||
|
|
||||||
# --- Configuration ---
|
# --- Configuration ---
|
||||||
|
|
||||||
@@ -53,6 +54,11 @@ help:
|
|||||||
@echo " make checklist-generate Validate checklists + print condition summary"
|
@echo " make checklist-generate Validate checklists + print condition summary"
|
||||||
@echo " make perf-baseline Run performance benchmarks and save baseline"
|
@echo " make perf-baseline Run performance benchmarks and save baseline"
|
||||||
@echo ""
|
@echo ""
|
||||||
|
@echo " make screenshot Ad-hoc visual capture (SCENARIO=name, default: fog_3state)"
|
||||||
|
@echo " make visual-movie Flow capture with contact sheet (FLOW=name)"
|
||||||
|
@echo " make test-visual Run visual golden regression tests"
|
||||||
|
@echo " make visual-update Regenerate visual goldens and stage for commit"
|
||||||
|
@echo ""
|
||||||
@echo " make pre-pr Run all pre-PR checks (lint, build, test, validate, fixtures)"
|
@echo " make pre-pr Run all pre-PR checks (lint, build, test, validate, fixtures)"
|
||||||
@echo " make pre-pr-server Server-scoped pre-PR (lint, build, test, fixtures)"
|
@echo " make pre-pr-server Server-scoped pre-PR (lint, build, test, fixtures)"
|
||||||
@echo " make pre-pr-client Client-scoped pre-PR (lint, build, test)"
|
@echo " make pre-pr-client Client-scoped pre-PR (lint, build, test)"
|
||||||
@@ -284,16 +290,16 @@ db-install:
|
|||||||
# --- Decisions ---
|
# --- Decisions ---
|
||||||
|
|
||||||
decisions-sync:
|
decisions-sync:
|
||||||
@db/connectors/decisions-sync
|
@tooling/db/decisions-sync
|
||||||
|
|
||||||
decisions-coverage:
|
decisions-coverage:
|
||||||
@db/connectors/sqlite-query "SELECT d.domain, COUNT(DISTINCT d.id) as decisions, COUNT(DISTINCT t.decision_ref) as with_tickets FROM decisions d LEFT JOIN tickets t ON d.id = t.decision_ref WHERE d.status='active' AND d.type='confirmed' GROUP BY d.domain"
|
@tooling/db/sqlite-query "SELECT d.domain, COUNT(DISTINCT d.id) as decisions, COUNT(DISTINCT t.decision_ref) as with_tickets FROM decisions d LEFT JOIN tickets t ON d.id = t.decision_ref WHERE d.status='active' AND d.type='confirmed' GROUP BY d.domain"
|
||||||
|
|
||||||
decisions-active:
|
decisions-active:
|
||||||
@db/connectors/sqlite-query "SELECT id, domain, title FROM decisions WHERE status='active' AND type='confirmed' ORDER BY domain, id"
|
@tooling/db/sqlite-query "SELECT id, domain, title FROM decisions WHERE status='active' AND type='confirmed' ORDER BY domain, id"
|
||||||
|
|
||||||
decisions-orphan:
|
decisions-orphan:
|
||||||
@db/connectors/sqlite-query "SELECT id, title FROM decisions WHERE type='confirmed' AND status='active' AND id NOT IN (SELECT DISTINCT decision_ref FROM tickets WHERE decision_ref IS NOT NULL)"
|
@tooling/db/sqlite-query "SELECT id, title FROM decisions WHERE type='confirmed' AND status='active' AND id NOT IN (SELECT DISTINCT decision_ref FROM tickets WHERE decision_ref IS NOT NULL)"
|
||||||
|
|
||||||
# --- Content Validation ---
|
# --- Content Validation ---
|
||||||
|
|
||||||
@@ -318,6 +324,20 @@ debug-schedule:
|
|||||||
@echo "Dumping bevy_ecs schedule graph..."
|
@echo "Dumping bevy_ecs schedule graph..."
|
||||||
@cd server && cargo run --bin settled-reach-server -- --dump-schedule
|
@cd server && cargo run --bin settled-reach-server -- --dump-schedule
|
||||||
|
|
||||||
|
# --- Visual test harness ---
|
||||||
|
|
||||||
|
screenshot:
|
||||||
|
@tests/run-visual --screenshot $(SCENARIO)
|
||||||
|
|
||||||
|
visual-movie:
|
||||||
|
@tests/run-visual --movie $(FLOW)
|
||||||
|
|
||||||
|
test-visual:
|
||||||
|
@tests/run-visual
|
||||||
|
|
||||||
|
visual-update:
|
||||||
|
@tests/run-visual --update
|
||||||
|
|
||||||
content-ron:
|
content-ron:
|
||||||
cd tooling/content-converter && cargo build --release
|
cd tooling/content-converter && cargo build --release
|
||||||
tooling/content-converter/target/release/content-converter --input content --output content-ron --verbose
|
tooling/content-converter/target/release/content-converter --input content --output content-ron --verbose
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://850hx6cd5kx7"
|
||||||
|
path="res://.godot/imported/npc_generic_east_64.png-bf22e76d70922112e99bf747d85a0a04.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/sprites/npc_generic_east_64.png"
|
||||||
|
dest_files=["res://.godot/imported/npc_generic_east_64.png-bf22e76d70922112e99bf747d85a0a04.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://bx6s0yglmpt2l"
|
||||||
|
path="res://.godot/imported/npc_generic_north_64.png-296e233a60c8b9efed025a82a69614df.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/sprites/npc_generic_north_64.png"
|
||||||
|
dest_files=["res://.godot/imported/npc_generic_north_64.png-296e233a60c8b9efed025a82a69614df.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dpsuq148mksls"
|
||||||
|
path="res://.godot/imported/npc_generic_south_64.png-c121e02d806f6dcc3ed440484827c258.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/sprites/npc_generic_south_64.png"
|
||||||
|
dest_files=["res://.godot/imported/npc_generic_south_64.png-c121e02d806f6dcc3ed440484827c258.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dgt84hobbdumx"
|
||||||
|
path="res://.godot/imported/npc_generic_west_64.png-9ee237384537f7357d37802b6cfae559.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/sprites/npc_generic_west_64.png"
|
||||||
|
dest_files=["res://.godot/imported/npc_generic_west_64.png-9ee237384537f7357d37802b6cfae559.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://bcmer1bsugk8b"
|
||||||
|
path="res://.godot/imported/wall_structural_east_64.png-f868b267dcc1824e2b0fe213e49b4996.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/sprites/wall_structural_east_64.png"
|
||||||
|
dest_files=["res://.godot/imported/wall_structural_east_64.png-f868b267dcc1824e2b0fe213e49b4996.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dd5j0q6cu674m"
|
||||||
|
path="res://.godot/imported/wall_structural_north_64.png-f7eceb6d561e7e07b3bac2e28c659628.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/sprites/wall_structural_north_64.png"
|
||||||
|
dest_files=["res://.godot/imported/wall_structural_north_64.png-f7eceb6d561e7e07b3bac2e28c659628.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://cctlhvaolom3x"
|
||||||
|
path="res://.godot/imported/wall_structural_south_64.png-c34925f9a5a3f6b5968e73e51953367e.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/sprites/wall_structural_south_64.png"
|
||||||
|
dest_files=["res://.godot/imported/wall_structural_south_64.png-c34925f9a5a3f6b5968e73e51953367e.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://ughjt5s8la2p"
|
||||||
|
path="res://.godot/imported/wall_structural_west_64.png-7c39e0c692d32348190875c4ad99b227.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/sprites/wall_structural_west_64.png"
|
||||||
|
dest_files=["res://.godot/imported/wall_structural_west_64.png-7c39e0c692d32348190875c4ad99b227.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
@@ -109,6 +109,7 @@ notifications:
|
|||||||
load_failed: "Load failed."
|
load_failed: "Load failed."
|
||||||
connection_lost: "Signal interrupted."
|
connection_lost: "Signal interrupted."
|
||||||
connection_restored: "Signal restored."
|
connection_restored: "Signal restored."
|
||||||
|
loading: "Resuming..."
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# KNOWLEDGE PANEL LABELS
|
# KNOWLEDGE PANEL LABELS
|
||||||
@@ -183,6 +184,9 @@ menu:
|
|||||||
confirm_quit: "Unsaved progress will be lost."
|
confirm_quit: "Unsaved progress will be lost."
|
||||||
confirm_yes: "Yes"
|
confirm_yes: "Yes"
|
||||||
confirm_no: "No"
|
confirm_no: "No"
|
||||||
|
load_game_browse: "LOAD GAME"
|
||||||
|
load_game_back: "BACK"
|
||||||
|
load_game_empty: "No saves found."
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
audio_volume: "Volume"
|
audio_volume: "Volume"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
[gd_scene load_steps=26 format=3 uid="uid://bswrmh7w8dbgm"]
|
[gd_scene load_steps=27 format=3 uid="uid://bswrmh7w8dbgm"]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://scripts/main.gd" id="1_main"]
|
[ext_resource type="Script" path="res://scripts/main.gd" id="1_main"]
|
||||||
[ext_resource type="Script" path="res://scripts/rendering/world_renderer.gd" id="2_world"]
|
[ext_resource type="Script" path="res://scripts/rendering/world_renderer.gd" id="2_world"]
|
||||||
@@ -21,10 +21,11 @@
|
|||||||
[ext_resource type="PackedScene" path="res://ui/checklist_overlay.tscn" id="18_checklist"]
|
[ext_resource type="PackedScene" path="res://ui/checklist_overlay.tscn" id="18_checklist"]
|
||||||
[ext_resource type="PackedScene" path="res://ui/bug_report_dialog.tscn" id="19_bugreport"]
|
[ext_resource type="PackedScene" path="res://ui/bug_report_dialog.tscn" id="19_bugreport"]
|
||||||
[ext_resource type="PackedScene" path="res://ui/settings_dialog.tscn" id="21_settings"]
|
[ext_resource type="PackedScene" path="res://ui/settings_dialog.tscn" id="21_settings"]
|
||||||
[ext_resource type="Script" path="res://scripts/ui/debug_overlay.gd" id="22_debug"]
|
[ext_resource type="Script" path="res://ui/debug_overlay.gd" id="22_debug"]
|
||||||
[ext_resource type="PackedScene" path="res://ui/time_display.tscn" id="23_tdisplay"]
|
[ext_resource type="PackedScene" path="res://ui/time_display.tscn" id="23_tdisplay"]
|
||||||
[ext_resource type="PackedScene" path="res://ui/examine_display.tscn" id="24_examine"]
|
[ext_resource type="PackedScene" path="res://ui/examine_display.tscn" id="24_examine"]
|
||||||
[ext_resource type="PackedScene" path="res://ui/journal_panel.tscn" id="25_journal"]
|
[ext_resource type="PackedScene" path="res://ui/journal_panel.tscn" id="25_journal"]
|
||||||
|
[ext_resource type="PackedScene" path="res://ui/loading_screen.tscn" id="26_loading"]
|
||||||
|
|
||||||
[node name="Game" type="Node2D"]
|
[node name="Game" type="Node2D"]
|
||||||
script = ExtResource("1_main")
|
script = ExtResource("1_main")
|
||||||
@@ -190,3 +191,6 @@ layer = 30
|
|||||||
|
|
||||||
; #528: Audio settings dialog — 5-bus volume sliders, ESC/OPEN_MENU to toggle
|
; #528: Audio settings dialog — 5-bus volume sliders, ESC/OPEN_MENU to toggle
|
||||||
[node name="SettingsDialog" parent="ModalLayer" instance=ExtResource("21_settings")]
|
[node name="SettingsDialog" parent="ModalLayer" instance=ExtResource("21_settings")]
|
||||||
|
|
||||||
|
; #257: Loading screen — full-screen overlay during save/load round-trip
|
||||||
|
[node name="LoadingScreen" parent="ModalLayer" instance=ExtResource("26_loading")]
|
||||||
|
|||||||
@@ -59,8 +59,66 @@ text = "CONTINUE"
|
|||||||
theme_override_font_sizes/font_size = 15
|
theme_override_font_sizes/font_size = 15
|
||||||
theme_override_colors/font_color = Color(0.906, 0.773, 0.278, 1.0)
|
theme_override_colors/font_color = Color(0.906, 0.773, 0.278, 1.0)
|
||||||
|
|
||||||
|
[node name="LoadGameBtn" type="Button" parent="VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "LOAD GAME"
|
||||||
|
theme_override_font_sizes/font_size = 15
|
||||||
|
theme_override_colors/font_color = Color(0.906, 0.773, 0.278, 1.0)
|
||||||
|
|
||||||
[node name="QuitBtn" type="Button" parent="VBox"]
|
[node name="QuitBtn" type="Button" parent="VBox"]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
text = "QUIT"
|
text = "QUIT"
|
||||||
theme_override_font_sizes/font_size = 15
|
theme_override_font_sizes/font_size = 15
|
||||||
theme_override_colors/font_color = Color(0.533, 0.565, 0.627, 1.0)
|
theme_override_colors/font_color = Color(0.533, 0.565, 0.627, 1.0)
|
||||||
|
|
||||||
|
[node name="LoadGamePanel" type="Control" parent="."]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
[node name="PanelBg" type="ColorRect" parent="LoadGamePanel"]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
color = Color(0.05, 0.05, 0.08, 0.96)
|
||||||
|
mouse_filter = 2
|
||||||
|
|
||||||
|
[node name="VBox" type="VBoxContainer" parent="LoadGamePanel"]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 8
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_top = 0.5
|
||||||
|
anchor_right = 0.5
|
||||||
|
anchor_bottom = 0.5
|
||||||
|
offset_left = -160.0
|
||||||
|
offset_top = -180.0
|
||||||
|
offset_right = 160.0
|
||||||
|
offset_bottom = 180.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
theme_override_constants/separation = 12
|
||||||
|
|
||||||
|
[node name="TitleLabel" type="Label" parent="LoadGamePanel/VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "LOAD GAME"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
theme_override_font_sizes/font_size = 20
|
||||||
|
theme_override_colors/font_color = Color(0.784, 0.816, 0.878, 1.0)
|
||||||
|
|
||||||
|
[node name="SavesScroll" type="ScrollContainer" parent="LoadGamePanel/VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
custom_minimum_size = Vector2(320, 240)
|
||||||
|
|
||||||
|
[node name="SavesList" type="VBoxContainer" parent="LoadGamePanel/VBox/SavesScroll"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
theme_override_constants/separation = 8
|
||||||
|
|
||||||
|
[node name="BackBtn" type="Button" parent="LoadGamePanel/VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "BACK"
|
||||||
|
theme_override_font_sizes/font_size = 14
|
||||||
|
theme_override_colors/font_color = Color(0.533, 0.565, 0.627, 1.0)
|
||||||
|
|||||||
@@ -8,13 +8,29 @@ extends Node
|
|||||||
# Used by fog shader to distinguish visual treatment per tile.
|
# Used by fog shader to distinguish visual treatment per tile.
|
||||||
# Test assertions reference these: assert_that(byte).is_equal(FogState.VIS_FORWARD)
|
# Test assertions reference these: assert_that(byte).is_equal(FogState.VIS_FORWARD)
|
||||||
const VIS_HIDDEN: int = 0 # Not in LOS — fully fogged
|
const VIS_HIDDEN: int = 0 # Not in LOS — fully fogged
|
||||||
const VIS_PERIPHERAL: int = 180 # In LOS, peripheral sector — light fog dimming
|
const VIS_PERIPHERAL: int = 180 # DEPRECATED: peripheral sector removed in Sprint 22 (#569). Retained — tests still reference it.
|
||||||
const VIS_FORWARD: int = 255 # In LOS, forward sector — clear vision
|
const VIS_FORWARD: int = 255 # In LOS, forward sector — clear vision
|
||||||
|
|
||||||
const EXP_UNEXPLORED: int = 0 # Never seen — total darkness
|
const EXP_UNEXPLORED: int = 0 # Never seen — total darkness
|
||||||
const EXP_EXPLORED: int = 128 # Previously seen, now out of LOS — deep fog
|
const EXP_EXPLORED: int = 128 # Previously seen, now out of LOS — deep fog
|
||||||
const EXP_VISIBLE: int = 255 # Currently in LOS — clear (written each frame)
|
const EXP_VISIBLE: int = 255 # Currently in LOS — clear (written each frame)
|
||||||
|
|
||||||
|
# Zone temperature tints (D-059 + D-046, Sprint 22) — keyed by zone_id string from server.
|
||||||
|
# Matches audio_manager.gd ZONE_ASSETS zone_id strings for consistent zone semantics.
|
||||||
|
# Low saturation is intentional (D-046): tints are subtle — distinguishable as warm/cool/neutral
|
||||||
|
# in side-by-side comparison, not garish. The "Hopper test" validates this.
|
||||||
|
# Colors are dark tints used as the fog overlay in the deep fog zone:
|
||||||
|
# hub/workplace: #1a1f2e (cool blue-dark — terminal, institutional)
|
||||||
|
# bar: #2a1f15 (warm amber-dark — social, inhabited)
|
||||||
|
# corridor: #1a1a1a (neutral dark — transitional, maintenance)
|
||||||
|
const ZONE_TINTS: Dictionary = {
|
||||||
|
"hub": Color(0.102, 0.122, 0.180), # #1a1f2e — cool blue-dark
|
||||||
|
"workplace": Color(0.102, 0.122, 0.180), # same as hub
|
||||||
|
"bar": Color(0.165, 0.122, 0.082), # #2a1f15 — warm amber-dark
|
||||||
|
"corridor": Color(0.102, 0.102, 0.102), # #1a1a1a — neutral dark
|
||||||
|
}
|
||||||
|
const ZONE_TINT_DEFAULT: Color = Color(0.102, 0.102, 0.102) # #1a1a1a neutral
|
||||||
|
|
||||||
var map_bounds: Rect2i = Rect2i(0, 0, 1, 1)
|
var map_bounds: Rect2i = Rect2i(0, 0, 1, 1)
|
||||||
var visibility_texture: ImageTexture
|
var visibility_texture: ImageTexture
|
||||||
var exploration_texture: ImageTexture
|
var exploration_texture: ImageTexture
|
||||||
@@ -25,16 +41,34 @@ var _exp_bytes: PackedByteArray
|
|||||||
var _vis_image: Image
|
var _vis_image: Image
|
||||||
var _exp_image: Image
|
var _exp_image: Image
|
||||||
var _tint_image: Image
|
var _tint_image: Image
|
||||||
|
# Zone tint stored as 3-channel RGB bytes (R, G, B per pixel) for preservation across resizes
|
||||||
|
var _tint_bytes: PackedByteArray
|
||||||
var _width: int = 1
|
var _width: int = 1
|
||||||
var _height: int = 1
|
var _height: int = 1
|
||||||
var _prev_visible: Dictionary = {} # Tiles visible last frame (for incremental decay)
|
var _prev_visible: Dictionary = {} # Tiles visible last frame (for incremental decay)
|
||||||
|
|
||||||
|
## Debug flag — when true, fog.gdshader renders raw exploration texture
|
||||||
|
## as colored overlay (green=visible, blue=explored, red=unexplored).
|
||||||
|
## Toggle via FogState.debug_exploration = true in the console.
|
||||||
|
var debug_exploration: bool = false
|
||||||
|
|
||||||
|
## Deterministic shader time for visual test captures.
|
||||||
|
## When >= 0, fog_shader.gd uses this instead of Time.get_ticks_msec().
|
||||||
|
## Set before settle frames so noise phase is reproducible across runs.
|
||||||
|
var override_time: float = -1.0
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
_resize(Rect2i(0, 0, 64, 64))
|
_resize(Rect2i(0, 0, 64, 64))
|
||||||
|
|
||||||
|
|
||||||
func _resize(bounds: Rect2i) -> void:
|
func _resize(bounds: Rect2i) -> void:
|
||||||
|
var old_bounds := map_bounds
|
||||||
|
var old_exp := _exp_bytes
|
||||||
|
var old_tint := _tint_bytes # empty on first call (_ready); guard at line 104 skips copy
|
||||||
|
var old_w := _width
|
||||||
|
var old_h := _height
|
||||||
|
|
||||||
map_bounds = bounds
|
map_bounds = bounds
|
||||||
_width = maxi(bounds.size.x, 1)
|
_width = maxi(bounds.size.x, 1)
|
||||||
_height = maxi(bounds.size.y, 1)
|
_height = maxi(bounds.size.y, 1)
|
||||||
@@ -49,41 +83,84 @@ func _resize(bounds: Rect2i) -> void:
|
|||||||
_exp_bytes = PackedByteArray()
|
_exp_bytes = PackedByteArray()
|
||||||
_exp_bytes.resize(sz)
|
_exp_bytes.resize(sz)
|
||||||
_exp_bytes.fill(EXP_UNEXPLORED)
|
_exp_bytes.fill(EXP_UNEXPLORED)
|
||||||
|
# Preserve exploration data from old bounds into new bounds
|
||||||
|
if old_exp.size() > 0 and old_w > 0 and old_h > 0:
|
||||||
|
var dx: int = old_bounds.position.x - bounds.position.x
|
||||||
|
var dy: int = old_bounds.position.y - bounds.position.y
|
||||||
|
for oy in range(old_h):
|
||||||
|
var ny: int = oy + dy
|
||||||
|
if ny < 0 or ny >= _height:
|
||||||
|
continue
|
||||||
|
for ox in range(old_w):
|
||||||
|
var nx: int = ox + dx
|
||||||
|
if nx < 0 or nx >= _width:
|
||||||
|
continue
|
||||||
|
var old_val: int = old_exp[oy * old_w + ox]
|
||||||
|
if old_val > EXP_UNEXPLORED:
|
||||||
|
_exp_bytes[ny * _width + nx] = old_val
|
||||||
_exp_image = Image.create_from_data(_width, _height, false, Image.FORMAT_R8, _exp_bytes)
|
_exp_image = Image.create_from_data(_width, _height, false, Image.FORMAT_R8, _exp_bytes)
|
||||||
exploration_texture = ImageTexture.create_from_image(_exp_image)
|
exploration_texture = ImageTexture.create_from_image(_exp_image)
|
||||||
|
|
||||||
# Zone tint — neutral dark for Sprint 6 (zone metadata deferred)
|
# Zone tint — 3 bytes per pixel (RGB), default neutral dark
|
||||||
_tint_image = Image.create(_width, _height, false, Image.FORMAT_RGB8)
|
var tint_sz := sz * 3
|
||||||
_tint_image.fill(Color(0.05, 0.05, 0.08))
|
_tint_bytes = PackedByteArray()
|
||||||
|
_tint_bytes.resize(tint_sz)
|
||||||
|
var default_r := int(ZONE_TINT_DEFAULT.r * 255.0)
|
||||||
|
var default_g := int(ZONE_TINT_DEFAULT.g * 255.0)
|
||||||
|
var default_b := int(ZONE_TINT_DEFAULT.b * 255.0)
|
||||||
|
for i in range(sz):
|
||||||
|
_tint_bytes[i * 3 + 0] = default_r
|
||||||
|
_tint_bytes[i * 3 + 1] = default_g
|
||||||
|
_tint_bytes[i * 3 + 2] = default_b
|
||||||
|
# Preserve zone tint data from old bounds (zone tints are stable — tile zone never changes)
|
||||||
|
if old_tint.size() > 0 and old_w > 0 and old_h > 0:
|
||||||
|
var dx: int = old_bounds.position.x - bounds.position.x
|
||||||
|
var dy: int = old_bounds.position.y - bounds.position.y
|
||||||
|
for oy in range(old_h):
|
||||||
|
var ny: int = oy + dy
|
||||||
|
if ny < 0 or ny >= _height:
|
||||||
|
continue
|
||||||
|
for ox in range(old_w):
|
||||||
|
var nx: int = ox + dx
|
||||||
|
if nx < 0 or nx >= _width:
|
||||||
|
continue
|
||||||
|
var old_idx := (oy * old_w + ox) * 3
|
||||||
|
var new_idx := (ny * _width + nx) * 3
|
||||||
|
_tint_bytes[new_idx + 0] = old_tint[old_idx + 0]
|
||||||
|
_tint_bytes[new_idx + 1] = old_tint[old_idx + 1]
|
||||||
|
_tint_bytes[new_idx + 2] = old_tint[old_idx + 2]
|
||||||
|
_tint_image = Image.create_from_data(_width, _height, false, Image.FORMAT_RGB8, _tint_bytes)
|
||||||
zone_tint_texture = ImageTexture.create_from_image(_tint_image)
|
zone_tint_texture = ImageTexture.create_from_image(_tint_image)
|
||||||
|
|
||||||
_prev_visible.clear()
|
_prev_visible.clear()
|
||||||
|
|
||||||
|
|
||||||
func update_from_state() -> void:
|
func update_from_state() -> void:
|
||||||
# Resize if map bounds changed
|
# Grow bounds to include newly visible tiles — never shrink, so explored
|
||||||
var tiles := GameState.visible_tiles
|
# tiles behind the player stay in the texture and render as deep fog
|
||||||
if tiles.size() > 0:
|
# instead of black. Exploration data is preserved across resizes.
|
||||||
var new_bounds := _compute_bounds(tiles)
|
# Use visible_positions (always populated from server snapshots) instead of
|
||||||
|
# visible_tiles, which stays empty in live server mode because the server
|
||||||
|
# sends tile_kind but game_state.gd's population check expects "type".
|
||||||
|
var positions: Dictionary = GameState.visible_positions
|
||||||
|
if positions.size() > 0:
|
||||||
|
var new_bounds := _grow_bounds_from_positions(positions)
|
||||||
if new_bounds != map_bounds:
|
if new_bounds != map_bounds:
|
||||||
_resize(new_bounds)
|
_resize(new_bounds)
|
||||||
|
|
||||||
var ox: int = map_bounds.position.x
|
var ox: int = map_bounds.position.x
|
||||||
var oy: int = map_bounds.position.y
|
var oy: int = map_bounds.position.y
|
||||||
var positions: Dictionary = GameState.visible_positions
|
|
||||||
var sectors: Dictionary = GameState.visibility_sectors
|
|
||||||
|
|
||||||
# TODO(v0.2): gradual decay over game-time instead of immediate EXP_VISIBLE→EXP_EXPLORED
|
# TODO(v0.2): gradual decay over game-time instead of immediate EXP_VISIBLE→EXP_EXPLORED
|
||||||
|
|
||||||
# 1. Clear visibility, then write current LOS
|
# 1. Clear visibility, then write current LOS (all tiles are Forward)
|
||||||
_vis_bytes.fill(VIS_HIDDEN)
|
_vis_bytes.fill(VIS_HIDDEN)
|
||||||
for pos in positions:
|
for pos in positions:
|
||||||
var px: int = pos.x - ox
|
var px: int = pos.x - ox
|
||||||
var py: int = pos.y - oy
|
var py: int = pos.y - oy
|
||||||
if px < 0 or py < 0 or px >= _width or py >= _height:
|
if px < 0 or py < 0 or px >= _width or py >= _height:
|
||||||
continue
|
continue
|
||||||
var sector: String = sectors.get(pos, "Forward")
|
_vis_bytes[py * _width + px] = VIS_FORWARD
|
||||||
_vis_bytes[py * _width + px] = VIS_FORWARD if sector == "Forward" else VIS_PERIPHERAL
|
|
||||||
_vis_image.set_data(_width, _height, false, Image.FORMAT_R8, _vis_bytes)
|
_vis_image.set_data(_width, _height, false, Image.FORMAT_R8, _vis_bytes)
|
||||||
visibility_texture.update(_vis_image)
|
visibility_texture.update(_vis_image)
|
||||||
|
|
||||||
@@ -106,24 +183,58 @@ func update_from_state() -> void:
|
|||||||
_exp_image.set_data(_width, _height, false, Image.FORMAT_R8, _exp_bytes)
|
_exp_image.set_data(_width, _height, false, Image.FORMAT_R8, _exp_bytes)
|
||||||
exploration_texture.update(_exp_image)
|
exploration_texture.update(_exp_image)
|
||||||
|
|
||||||
|
# 3. Zone tint: write zone temperature color for currently visible tiles.
|
||||||
|
# Zone data is stable (tile zone never changes) so we only write on first sight.
|
||||||
|
# Data persists in _tint_bytes across frames and across resizes.
|
||||||
|
# visible_tiles carries zone_id per tile (populated from snapshot "tiles" or
|
||||||
|
# "visible_tiles" with type field — see game_state.gd apply_snapshot).
|
||||||
|
var tiles := GameState.visible_tiles
|
||||||
|
var tint_dirty := false
|
||||||
|
for tile in tiles:
|
||||||
|
if not tile is Dictionary or not tile.has("x") or not tile.has("y"):
|
||||||
|
continue
|
||||||
|
var zone_id: String = str(tile.get("zone_id", ""))
|
||||||
|
if zone_id.is_empty():
|
||||||
|
continue
|
||||||
|
var tint_color: Color = ZONE_TINTS.get(zone_id, ZONE_TINT_DEFAULT)
|
||||||
|
var px: int = int(tile.x) - ox
|
||||||
|
var py: int = int(tile.y) - oy
|
||||||
|
if px < 0 or py < 0 or px >= _width or py >= _height:
|
||||||
|
continue
|
||||||
|
var tint_idx := (py * _width + px) * 3
|
||||||
|
var new_r := int(tint_color.r * 255.0)
|
||||||
|
var new_g := int(tint_color.g * 255.0)
|
||||||
|
var new_b := int(tint_color.b * 255.0)
|
||||||
|
# Only update if different from current (avoid spurious texture uploads)
|
||||||
|
if _tint_bytes[tint_idx] != new_r or _tint_bytes[tint_idx + 1] != new_g or _tint_bytes[tint_idx + 2] != new_b:
|
||||||
|
_tint_bytes[tint_idx + 0] = new_r
|
||||||
|
_tint_bytes[tint_idx + 1] = new_g
|
||||||
|
_tint_bytes[tint_idx + 2] = new_b
|
||||||
|
tint_dirty = true
|
||||||
|
if tint_dirty:
|
||||||
|
_tint_image.set_data(_width, _height, false, Image.FORMAT_RGB8, _tint_bytes)
|
||||||
|
zone_tint_texture.update(_tint_image)
|
||||||
|
|
||||||
# Shallow copy — correct for Dictionary<Vector2i, bool/String> values
|
# Shallow copy — correct for Dictionary<Vector2i, bool/String> values
|
||||||
_prev_visible = positions.duplicate()
|
_prev_visible = positions.duplicate()
|
||||||
|
|
||||||
|
|
||||||
func _compute_bounds(tiles: Array) -> Rect2i:
|
func _grow_bounds_from_positions(positions: Dictionary) -> Rect2i:
|
||||||
|
## Compute bounds from visible_positions (Dictionary[Vector2i, bool]).
|
||||||
var min_x := 999999
|
var min_x := 999999
|
||||||
var min_y := 999999
|
var min_y := 999999
|
||||||
var max_x := -999999
|
var max_x := -999999
|
||||||
var max_y := -999999
|
var max_y := -999999
|
||||||
for tile in tiles:
|
for pos in positions:
|
||||||
if not tile is Dictionary or not tile.has("x") or not tile.has("y"):
|
min_x = mini(min_x, pos.x)
|
||||||
continue
|
min_y = mini(min_y, pos.y)
|
||||||
min_x = mini(min_x, int(tile.x))
|
max_x = maxi(max_x, pos.x)
|
||||||
min_y = mini(min_y, int(tile.y))
|
max_y = maxi(max_y, pos.y)
|
||||||
max_x = maxi(max_x, int(tile.x))
|
|
||||||
max_y = maxi(max_y, int(tile.y))
|
|
||||||
# Guard: all tiles invalid (no x/y) — sentinels would produce negative Rect2i
|
|
||||||
if min_x > max_x:
|
if min_x > max_x:
|
||||||
return Rect2i(0, 0, 1, 1)
|
return map_bounds
|
||||||
# Margin for fog gradient bleed at edges
|
var tile_bounds := Rect2i(min_x - 8, min_y - 8, max_x - min_x + 17, max_y - min_y + 17)
|
||||||
return Rect2i(min_x - 4, min_y - 4, max_x - min_x + 9, max_y - min_y + 9)
|
if map_bounds.size.x <= 1 and map_bounds.size.y <= 1:
|
||||||
|
return tile_bounds
|
||||||
|
return map_bounds.merge(tile_bounds)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,12 @@ var gauntlet_mode: bool = false # true when snapshot includes gauntlet_mode fla
|
|||||||
# the server's "insert_active" snapshot field, disabling all z-layer-6 UI.
|
# the server's "insert_active" snapshot field, disabling all z-layer-6 UI.
|
||||||
var insert_active: bool = true
|
var insert_active: bool = true
|
||||||
|
|
||||||
|
# #175: World seed for deterministic simulation (D-010, D-029).
|
||||||
|
# Set by SessionManager.new_game(), sent to server via StartupMessage in SimBridge.
|
||||||
|
# Same seed → same EntanglementConfig → same NPC population across playthroughs.
|
||||||
|
# Persists for the session lifetime; not overwritten by apply_snapshot().
|
||||||
|
var world_seed: int = 0
|
||||||
|
|
||||||
# #507: RNG seed for replay determinism — populated from snapshot "rng_seed" field.
|
# #507: RNG seed for replay determinism — populated from snapshot "rng_seed" field.
|
||||||
# Null in v0.1 (server does not yet send this field; protocol change required).
|
# Null in v0.1 (server does not yet send this field; protocol change required).
|
||||||
var rng_seed: Variant = null
|
var rng_seed: Variant = null
|
||||||
@@ -75,6 +81,11 @@ var rng_seed: Variant = null
|
|||||||
# One-shot: consumed by main.gd after display, then set back to null.
|
# One-shot: consumed by main.gd after display, then set back to null.
|
||||||
var save_result: Variant = null
|
var save_result: Variant = null
|
||||||
|
|
||||||
|
# #257: Pending load path — set by main menu "Load Game" selection.
|
||||||
|
# main.gd sends LOAD_GAME on startup if non-empty, then clears this field.
|
||||||
|
# Format: user://saves/<game-id>/<filename>.sav or "" if no pending load.
|
||||||
|
var pending_load_path: String = ""
|
||||||
|
|
||||||
# v7 fields (#431, D-059/D-060)
|
# v7 fields (#431, D-059/D-060)
|
||||||
var pending_recognitions: Array = [] # [{entity_id, x, y, z, remaining_ticks, total_delay_ticks}]
|
var pending_recognitions: Array = [] # [{entity_id, x, y, z, remaining_ticks, total_delay_ticks}]
|
||||||
|
|
||||||
|
|||||||
@@ -31,12 +31,27 @@ func new_game() -> String:
|
|||||||
save_path, error_string(err)])
|
save_path, error_string(err)])
|
||||||
return ""
|
return ""
|
||||||
GameState.current_game_id = game_id
|
GameState.current_game_id = game_id
|
||||||
|
|
||||||
|
# #175: Generate world_seed for deterministic simulation (D-010, D-029).
|
||||||
|
# Combines two randi() calls (u32 each) into 63-bit entropy range.
|
||||||
|
# Mask bit 31 of the upper word before shifting to prevent signed overflow:
|
||||||
|
# GDScript int is i64 — if bit 63 is set, MessagePack encodes as negative,
|
||||||
|
# and Rust rmp_serde rejects negative values when deserializing as u64.
|
||||||
|
GameState.world_seed = ((rng.randi() & 0x7FFFFFFF) << 32) | rng.randi()
|
||||||
|
|
||||||
|
# Persist world_seed to save directory so resume_game() can restore it.
|
||||||
|
# Without this, loaded sessions would send seed=0, breaking D-010 determinism.
|
||||||
|
_write_seed_file(save_path, GameState.world_seed)
|
||||||
|
|
||||||
return game_id
|
return game_id
|
||||||
|
|
||||||
|
|
||||||
## Resume an existing game session by setting the active game-id.
|
## Resume an existing game session by setting the active game-id.
|
||||||
|
## Restores world_seed from the save directory for D-010 deterministic replay.
|
||||||
func resume_game(game_id: String) -> void:
|
func resume_game(game_id: String) -> void:
|
||||||
GameState.current_game_id = game_id
|
GameState.current_game_id = game_id
|
||||||
|
var save_path := SAVES_DIR + game_id + "/"
|
||||||
|
GameState.world_seed = _read_seed_file(save_path)
|
||||||
|
|
||||||
|
|
||||||
## List all game directories under user://saves/ sorted by last-modified (most recent first).
|
## List all game directories under user://saves/ sorted by last-modified (most recent first).
|
||||||
@@ -111,6 +126,26 @@ func _cleanup_quit_dialog() -> void:
|
|||||||
_quit_dialog = null
|
_quit_dialog = null
|
||||||
|
|
||||||
|
|
||||||
|
## Write world_seed to a file in the save directory for session persistence.
|
||||||
|
func _write_seed_file(save_path: String, seed: int) -> void:
|
||||||
|
var file := FileAccess.open(save_path + "world_seed", FileAccess.WRITE)
|
||||||
|
if file == null:
|
||||||
|
push_error("SessionManager: failed to write seed file: %s" % error_string(FileAccess.get_open_error()))
|
||||||
|
return
|
||||||
|
file.store_64(seed)
|
||||||
|
|
||||||
|
|
||||||
|
## Read world_seed from save directory. Returns 0 if file missing (legacy saves).
|
||||||
|
## Masks the sign bit on read: save files written before the signed-overflow fix
|
||||||
|
## may contain negative i64 values that Rust rmp_serde rejects as u64.
|
||||||
|
func _read_seed_file(save_path: String) -> int:
|
||||||
|
var file := FileAccess.open(save_path + "world_seed", FileAccess.READ)
|
||||||
|
if file == null:
|
||||||
|
push_warning("SessionManager: no seed file in %s — using seed=0 (legacy save)" % save_path)
|
||||||
|
return 0
|
||||||
|
return file.get_64() & 0x7FFFFFFFFFFFFFFF
|
||||||
|
|
||||||
|
|
||||||
func _find_newest_save(dir_path: String) -> String:
|
func _find_newest_save(dir_path: String) -> String:
|
||||||
var dir := DirAccess.open(dir_path)
|
var dir := DirAccess.open(dir_path)
|
||||||
if dir == null:
|
if dir == null:
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b357ok64jc8vp
|
||||||
@@ -231,6 +231,26 @@ func _process(delta: float) -> void:
|
|||||||
_set_state(ConnectionState.ERROR)
|
_set_state(ConnectionState.ERROR)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Send startup message with world_seed (#175, D-010/D-029).
|
||||||
|
# Server blocks waiting for this before entering the tick loop.
|
||||||
|
var startup_bytes := Protocol.encode_startup_message(GameState.world_seed)
|
||||||
|
if startup_bytes.size() > 0:
|
||||||
|
var send_err := _bridge.send_message(startup_bytes)
|
||||||
|
if send_err != OK:
|
||||||
|
var reason := "Failed to send startup message: %s" % error_string(send_err)
|
||||||
|
push_error("SimBridge: %s" % reason)
|
||||||
|
handshake_failed.emit(reason)
|
||||||
|
_bridge.disconnect_from_server()
|
||||||
|
_set_state(ConnectionState.ERROR)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
var reason := "Failed to encode startup message"
|
||||||
|
push_error("SimBridge: %s" % reason)
|
||||||
|
handshake_failed.emit(reason)
|
||||||
|
_bridge.disconnect_from_server()
|
||||||
|
_set_state(ConnectionState.ERROR)
|
||||||
|
return
|
||||||
|
|
||||||
handshake_complete.emit(server_version)
|
handshake_complete.emit(server_version)
|
||||||
_set_state(ConnectionState.CONNECTED)
|
_set_state(ConnectionState.CONNECTED)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ extends Node2D
|
|||||||
@onready var debug_overlay = $UILayer/DebugOverlay # #511: F3 debug overlay
|
@onready var debug_overlay = $UILayer/DebugOverlay # #511: F3 debug overlay
|
||||||
@onready var bug_report_dialog = $ModalLayer/BugReportDialog # #495: F12 WRONG button
|
@onready var bug_report_dialog = $ModalLayer/BugReportDialog # #495: F12 WRONG button
|
||||||
@onready var settings_dialog = $ModalLayer/SettingsDialog # #528: audio settings (ESC/OPEN_MENU)
|
@onready var settings_dialog = $ModalLayer/SettingsDialog # #528: audio settings (ESC/OPEN_MENU)
|
||||||
|
@onready var loading_screen = $ModalLayer/LoadingScreen # #257: blocking overlay during load
|
||||||
|
|
||||||
var _last_dialogue_npc_id: int = -1 # D-064: NPC entity_id for WalkAway input
|
var _last_dialogue_npc_id: int = -1 # D-064: NPC entity_id for WalkAway input
|
||||||
var _last_dialogue_npc_name: String = "" # #535: NPC name for dialogue_response attribution
|
var _last_dialogue_npc_name: String = "" # #535: NPC name for dialogue_response attribution
|
||||||
@@ -49,6 +50,17 @@ func _ready() -> void:
|
|||||||
# Connect to simulation (test mode sets CONNECTED immediately)
|
# Connect to simulation (test mode sets CONNECTED immediately)
|
||||||
SimBridge.connect_to_sim()
|
SimBridge.connect_to_sim()
|
||||||
|
|
||||||
|
# #257: If returning from main menu "Load Game" selection, defer dispatch until connected.
|
||||||
|
# In test mode, connect_to_sim() sets CONNECTED synchronously — dispatch fires immediately.
|
||||||
|
# In live mode, state is CONNECTING — signal handler dispatches once connected.
|
||||||
|
if not GameState.pending_load_path.is_empty():
|
||||||
|
if loading_screen:
|
||||||
|
loading_screen.show_loading()
|
||||||
|
if SimBridge.state == SimBridge.ConnectionState.CONNECTED:
|
||||||
|
_dispatch_pending_load()
|
||||||
|
else:
|
||||||
|
SimBridge.connection_state_changed.connect(_on_sim_connected_for_load)
|
||||||
|
|
||||||
# Camera anchor: snap to player position before the first frame renders.
|
# Camera anchor: snap to player position before the first frame renders.
|
||||||
# In test mode poll_snapshot() returns synchronously — position is set
|
# In test mode poll_snapshot() returns synchronously — position is set
|
||||||
# immediately. In live mode the snapshot isn't available yet — _process
|
# immediately. In live mode the snapshot isn't available yet — _process
|
||||||
@@ -161,6 +173,15 @@ func _process(delta: float) -> void:
|
|||||||
if input.action == InputMapper.Action.OPEN_JOURNAL:
|
if input.action == InputMapper.Action.OPEN_JOURNAL:
|
||||||
_toggle_journal()
|
_toggle_journal()
|
||||||
continue
|
continue
|
||||||
|
# #257: LOAD_GAME — send first, then show loading screen (avoids stuck overlay if send fails)
|
||||||
|
if input.action == InputMapper.Action.LOAD_GAME:
|
||||||
|
var err := SimBridge.send_input(input)
|
||||||
|
_pending_record_inputs.append(input)
|
||||||
|
if err == OK and loading_screen:
|
||||||
|
loading_screen.show_loading()
|
||||||
|
elif err != OK:
|
||||||
|
push_error("main.gd: LOAD_GAME send_input failed: %s" % error_string(err))
|
||||||
|
continue
|
||||||
# #528: ESC/OPEN_MENU — client-only, toggle audio settings dialog
|
# #528: ESC/OPEN_MENU — client-only, toggle audio settings dialog
|
||||||
if input.action == InputMapper.Action.OPEN_MENU:
|
if input.action == InputMapper.Action.OPEN_MENU:
|
||||||
if settings_dialog:
|
if settings_dialog:
|
||||||
@@ -368,12 +389,15 @@ func _consume_dialogue_response() -> void:
|
|||||||
GameState.dialogue_response = null
|
GameState.dialogue_response = null
|
||||||
|
|
||||||
|
|
||||||
# #554: Show save/load result notification from server response.
|
# #554/#257: Show save/load result notification; hide loading screen on load complete.
|
||||||
func _consume_save_result() -> void:
|
func _consume_save_result() -> void:
|
||||||
if GameState.save_result == null:
|
if GameState.save_result == null:
|
||||||
return
|
return
|
||||||
var result: Dictionary = GameState.save_result
|
var result: Dictionary = GameState.save_result
|
||||||
GameState.save_result = null # consume once
|
GameState.save_result = null # consume once
|
||||||
|
# #257: Dismiss loading screen regardless of success/failure
|
||||||
|
if loading_screen:
|
||||||
|
loading_screen.hide_loading()
|
||||||
var msg: String
|
var msg: String
|
||||||
if result.get("success", false):
|
if result.get("success", false):
|
||||||
if result.get("kind", "") == "save":
|
if result.get("kind", "") == "save":
|
||||||
@@ -462,6 +486,32 @@ func _on_connection_state_changed(old_state: SimBridge.ConnectionState, new_stat
|
|||||||
gauntlet_hud.finalize()
|
gauntlet_hud.finalize()
|
||||||
|
|
||||||
|
|
||||||
|
# #257: Deferred LOAD_GAME dispatch — fires once when SimBridge reaches CONNECTED.
|
||||||
|
# pending_load_path is set by main_menu.gd before scene change.
|
||||||
|
func _on_sim_connected_for_load(_old_state: SimBridge.ConnectionState, new_state: SimBridge.ConnectionState) -> void:
|
||||||
|
if new_state != SimBridge.ConnectionState.CONNECTED:
|
||||||
|
return
|
||||||
|
if SimBridge.connection_state_changed.is_connected(_on_sim_connected_for_load):
|
||||||
|
SimBridge.connection_state_changed.disconnect(_on_sim_connected_for_load)
|
||||||
|
_dispatch_pending_load()
|
||||||
|
|
||||||
|
|
||||||
|
func _dispatch_pending_load() -> void:
|
||||||
|
var load_path := GameState.pending_load_path
|
||||||
|
if load_path.is_empty():
|
||||||
|
return
|
||||||
|
GameState.pending_load_path = ""
|
||||||
|
var err := SimBridge.send_input({
|
||||||
|
"action": InputMapper.Action.LOAD_GAME,
|
||||||
|
"timestamp_msec": Time.get_ticks_msec(),
|
||||||
|
"action_data": {"path": load_path},
|
||||||
|
})
|
||||||
|
if err != OK:
|
||||||
|
push_error("main.gd: failed to send LOAD_GAME after connection — %s" % error_string(err))
|
||||||
|
if loading_screen:
|
||||||
|
loading_screen.hide_loading(false)
|
||||||
|
|
||||||
|
|
||||||
# #501: Detect large position jump indicating a teleport (not normal movement).
|
# #501: Detect large position jump indicating a teleport (not normal movement).
|
||||||
const TELEPORT_DISTANCE_THRESHOLD: float = 5.0
|
const TELEPORT_DISTANCE_THRESHOLD: float = 5.0
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class_name Protocol
|
|||||||
|
|
||||||
## Protocol version — must match server PROTOCOL_VERSION in bridge/types.rs.
|
## Protocol version — must match server PROTOCOL_VERSION in bridge/types.rs.
|
||||||
## Reject snapshots where version != this value.
|
## Reject snapshots where version != this value.
|
||||||
const PROTOCOL_VERSION: int = 15
|
const PROTOCOL_VERSION: int = 17
|
||||||
|
|
||||||
|
|
||||||
# -- Decode: bytes from server → GDScript types --------------------------------
|
# -- Decode: bytes from server → GDScript types --------------------------------
|
||||||
@@ -427,6 +427,18 @@ static func _decode_enum_variant(raw) -> Dictionary:
|
|||||||
|
|
||||||
# -- Encode: GDScript types → bytes to server ----------------------------------
|
# -- Encode: GDScript types → bytes to server ----------------------------------
|
||||||
|
|
||||||
|
## Encode a StartupMessage to MessagePack bytes (#175).
|
||||||
|
## Sent by the client immediately after handshake validation.
|
||||||
|
## Server reads this to initialize SimRng with the world seed (D-010, D-029).
|
||||||
|
static func encode_startup_message(world_seed: int) -> PackedByteArray:
|
||||||
|
var msg := {"world_seed": world_seed}
|
||||||
|
var result = Messagepack.encode(msg)
|
||||||
|
if result.status != null:
|
||||||
|
push_error("Protocol: startup message encode failed: %s" % result.status)
|
||||||
|
return PackedByteArray()
|
||||||
|
return result.value
|
||||||
|
|
||||||
|
|
||||||
## Encode a PlayerInput to MessagePack bytes.
|
## Encode a PlayerInput to MessagePack bytes.
|
||||||
## action_name: one of "MoveNorth", "MoveSouth", "MoveEast", "MoveWest",
|
## action_name: one of "MoveNorth", "MoveSouth", "MoveEast", "MoveWest",
|
||||||
## "Interact", "UsePerceptionMode", "Pause", "Unpause"
|
## "Interact", "UsePerceptionMode", "Pause", "Unpause"
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://2p33ypi2y2kv
|
||||||
@@ -4,6 +4,9 @@ extends Node2D
|
|||||||
## Reads textures from FogState autoload, positions rect to cover viewport.
|
## Reads textures from FogState autoload, positions rect to cover viewport.
|
||||||
## Architecture: docs/architecture/fog-shader-spec.md
|
## Architecture: docs/architecture/fog-shader-spec.md
|
||||||
|
|
||||||
|
signal fog_noise_ready
|
||||||
|
var _noise_ready: bool = false
|
||||||
|
|
||||||
var _fog_rect: ColorRect
|
var _fog_rect: ColorRect
|
||||||
var _shader_mat: ShaderMaterial
|
var _shader_mat: ShaderMaterial
|
||||||
|
|
||||||
@@ -36,10 +39,11 @@ func _ready() -> void:
|
|||||||
noise_tex.width = 256
|
noise_tex.width = 256
|
||||||
noise_tex.height = 256
|
noise_tex.height = 256
|
||||||
noise_tex.seamless = true
|
noise_tex.seamless = true
|
||||||
|
noise_tex.changed.connect(func(): _noise_ready = true; fog_noise_ready.emit())
|
||||||
_shader_mat.set_shader_parameter("noise_tex", noise_tex)
|
_shader_mat.set_shader_parameter("noise_tex", noise_tex)
|
||||||
_shader_mat.set_shader_parameter("tile_size", TILE_SIZE)
|
_shader_mat.set_shader_parameter("tile_size", TILE_SIZE)
|
||||||
|
|
||||||
print("FogShader: Initialized (D-059 5-layer)")
|
print("FogShader: Initialized (D-059 3-state)")
|
||||||
|
|
||||||
|
|
||||||
func update_fog() -> void:
|
func update_fog() -> void:
|
||||||
@@ -68,4 +72,6 @@ func update_fog() -> void:
|
|||||||
_shader_mat.set_shader_parameter("rect_sz", _fog_rect.size)
|
_shader_mat.set_shader_parameter("rect_sz", _fog_rect.size)
|
||||||
_shader_mat.set_shader_parameter("map_offset", Vector2(FogState.map_bounds.position))
|
_shader_mat.set_shader_parameter("map_offset", Vector2(FogState.map_bounds.position))
|
||||||
_shader_mat.set_shader_parameter("map_size", Vector2(FogState.map_bounds.size))
|
_shader_mat.set_shader_parameter("map_size", Vector2(FogState.map_bounds.size))
|
||||||
_shader_mat.set_shader_parameter("time", Time.get_ticks_msec() / 1000.0)
|
var t: float = FogState.override_time if FogState.override_time >= 0.0 else Time.get_ticks_msec() / 1000.0
|
||||||
|
_shader_mat.set_shader_parameter("time", t)
|
||||||
|
_shader_mat.set_shader_parameter("debug_exploration", FogState.debug_exploration)
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://drppri4b46v80
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cix55xks85vl8
|
||||||
@@ -1,15 +1,17 @@
|
|||||||
shader_type canvas_item;
|
shader_type canvas_item;
|
||||||
|
|
||||||
// D-059: 5-layer fog shader. Composites over world content (layers 0-4).
|
// D-059/D-015: 3-state fog shader (simplified from 5-layer by #569).
|
||||||
// Layer 1: Clear (vision cone) — transparent, soft gradient edge
|
// State 1: Clear (forward cone) — transparent, soft Gaussian gradient edge (3-4 tile radius)
|
||||||
// Layer 2: Light fog (peripheral) — desaturated + dim + animated noise, 8-10s cycle
|
// State 2: Explored (out of cone) — light fog overlay, alpha 0.25-0.35, zone temperature tint,
|
||||||
// Layer 3: Deep fog (explored) — near-monochrome + zone tint + breathing, 15-20s cycle
|
// 8-10s Perlin breathe. Art and information preserved, just "not fresh" (D-015).
|
||||||
// Layer 4: Unexplored + maps — wireframe (Sprint 6: deferred, treated as Layer 5)
|
// State 3: Unexplored — solid near-black #12141a
|
||||||
// Layer 5: Unexplored, no maps — solid near-black #12141a
|
// D-033: Entity colors are NOT affected — they render above the fog overlay (z-layer 5).
|
||||||
|
// D-046: Zone temperature tint from zone_tint_tex — warm=bar, cool=hub, neutral=corridor.
|
||||||
|
// D-077: zone_tint_tex populated per-tile from server zone_id via fog_state.gd.
|
||||||
|
|
||||||
uniform sampler2D visibility_tex : filter_linear, repeat_disable;
|
uniform sampler2D visibility_tex : filter_linear, repeat_disable;
|
||||||
uniform sampler2D exploration_tex : filter_linear, repeat_disable;
|
uniform sampler2D exploration_tex : filter_linear, repeat_disable;
|
||||||
uniform sampler2D zone_tint_tex : filter_nearest, repeat_disable;
|
uniform sampler2D zone_tint_tex : filter_nearest, repeat_disable; // nearest: zones have hard boundaries (D-073)
|
||||||
uniform sampler2D noise_tex : filter_linear, repeat_enable;
|
uniform sampler2D noise_tex : filter_linear, repeat_enable;
|
||||||
uniform vec2 rect_pos; // World-space position of the ColorRect (pixels)
|
uniform vec2 rect_pos; // World-space position of the ColorRect (pixels)
|
||||||
uniform vec2 rect_sz; // World-space size of the ColorRect (pixels)
|
uniform vec2 rect_sz; // World-space size of the ColorRect (pixels)
|
||||||
@@ -17,57 +19,99 @@ uniform vec2 map_offset; // map_bounds.position (tiles)
|
|||||||
uniform vec2 map_size; // map_bounds.size (tiles)
|
uniform vec2 map_size; // map_bounds.size (tiles)
|
||||||
uniform float tile_size; // Pixels per sim tile
|
uniform float tile_size; // Pixels per sim tile
|
||||||
uniform float time; // Seconds since start
|
uniform float time; // Seconds since start
|
||||||
|
uniform bool debug_exploration = false; // When true, render raw exploration texture
|
||||||
|
|
||||||
// D-059 fog layer colors
|
|
||||||
const vec3 UNEXPLORED_COLOR = vec3(0.071, 0.078, 0.102); // #12141a
|
const vec3 UNEXPLORED_COLOR = vec3(0.071, 0.078, 0.102); // #12141a
|
||||||
const vec3 DARK_OVERLAY = vec3(0.02, 0.02, 0.05);
|
|
||||||
|
|
||||||
// D-059 thresholds (after bilinear filtering)
|
// Soft gradient via 7x7 Gaussian blur on visibility (sigma 2.0).
|
||||||
// Forward tiles = 1.0, Peripheral = 0.706 (180/255), not-visible = 0.0
|
// Spreads the cone boundary into a 3-4 tile radius gradient — no hard tile-stepped edges.
|
||||||
const float CLEAR_THRESHOLD = 0.85; // Above this: fully clear
|
float sample_visibility(vec2 uv) {
|
||||||
const float PERIPHERAL_LOW = 0.55; // Below this: transition to deep/unexplored
|
vec2 t = 2.0 / map_size;
|
||||||
|
float sum = 0.0;
|
||||||
|
float weight = 0.0;
|
||||||
|
for (float dy = -3.0; dy <= 3.0; dy += 1.0) {
|
||||||
|
for (float dx = -3.0; dx <= 3.0; dx += 1.0) {
|
||||||
|
float w = exp(-(dx * dx + dy * dy) / 8.0);
|
||||||
|
vec2 sample_uv = clamp(uv + vec2(dx, dy) * t, vec2(0.0), vec2(1.0));
|
||||||
|
sum += texture(visibility_tex, sample_uv).r * w;
|
||||||
|
weight += w;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sum / weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Soft gradient on exploration boundary (5x5, sigma 1.5).
|
||||||
|
// Prevents hard tile-stepped staircase at explored/unexplored edge.
|
||||||
|
float sample_exploration(vec2 uv) {
|
||||||
|
vec2 t = 1.0 / map_size;
|
||||||
|
float sum = 0.0;
|
||||||
|
float weight = 0.0;
|
||||||
|
for (float dy = -2.0; dy <= 2.0; dy += 1.0) {
|
||||||
|
for (float dx = -2.0; dx <= 2.0; dx += 1.0) {
|
||||||
|
float w = exp(-(dx * dx + dy * dy) / 4.5);
|
||||||
|
vec2 sample_uv = clamp(uv + vec2(dx, dy) * t, vec2(0.0), vec2(1.0));
|
||||||
|
sum += texture(exploration_tex, sample_uv).r * w;
|
||||||
|
weight += w;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sum / weight;
|
||||||
|
}
|
||||||
|
|
||||||
void fragment() {
|
void fragment() {
|
||||||
// Map UV (0-1 across ColorRect) to world pixels, then to tile coordinates
|
|
||||||
vec2 world_px = rect_pos + UV * rect_sz;
|
vec2 world_px = rect_pos + UV * rect_sz;
|
||||||
vec2 tile = world_px / tile_size;
|
vec2 tile = world_px / tile_size;
|
||||||
|
|
||||||
// Map tile coordinate to texture UV
|
|
||||||
vec2 tex_uv = (tile - map_offset) / map_size;
|
vec2 tex_uv = (tile - map_offset) / map_size;
|
||||||
|
|
||||||
// Outside known map → unexplored
|
// Outside known map -> unexplored
|
||||||
|
// Note: no early return — fragment() in OpenGL3 compat doesn't support return.
|
||||||
if (tex_uv.x < 0.0 || tex_uv.x > 1.0 || tex_uv.y < 0.0 || tex_uv.y > 1.0) {
|
if (tex_uv.x < 0.0 || tex_uv.x > 1.0 || tex_uv.y < 0.0 || tex_uv.y > 1.0) {
|
||||||
COLOR = vec4(UNEXPLORED_COLOR, 1.0);
|
COLOR = vec4(UNEXPLORED_COLOR, 1.0);
|
||||||
} else {
|
|
||||||
float vis = texture(visibility_tex, tex_uv).r;
|
|
||||||
float explored = texture(exploration_tex, tex_uv).r;
|
|
||||||
|
|
||||||
if (vis > PERIPHERAL_LOW) {
|
// Debug mode: render raw exploration texture (bypass fog rendering).
|
||||||
// In or near vision cone
|
// Green = EXP_VISIBLE (255), blue = EXP_EXPLORED (128), red = EXP_UNEXPLORED (0).
|
||||||
if (vis > CLEAR_THRESHOLD) {
|
} else if (debug_exploration) {
|
||||||
// Layer 1: Clear — soft edge gradient
|
float explored_dbg = texture(exploration_tex, tex_uv).r;
|
||||||
float edge = smoothstep(CLEAR_THRESHOLD, 1.0, vis);
|
if (explored_dbg > 0.9) {
|
||||||
COLOR = vec4(0.0, 0.0, 0.0, 1.0 - edge);
|
COLOR = vec4(0.0, explored_dbg, 0.0, 0.8); // Green: currently visible
|
||||||
} else {
|
} else if (explored_dbg > 0.1) {
|
||||||
// Layer 2: Light fog (peripheral + forward edge)
|
COLOR = vec4(0.0, 0.0, explored_dbg * 2.0, 0.8); // Blue: explored
|
||||||
// D-059: animated Perlin noise, 8-10s cycle
|
|
||||||
float noise_val = texture(noise_tex, tile * 0.03 + vec2(time * 0.11, time * 0.07)).r;
|
|
||||||
float coverage = smoothstep(PERIPHERAL_LOW, CLEAR_THRESHOLD, vis);
|
|
||||||
// Blend from heavy fog (alpha ~0.55) to lighter fog near clear edge
|
|
||||||
float alpha = mix(0.55, 0.25, coverage) + noise_val * 0.1;
|
|
||||||
COLOR = vec4(DARK_OVERLAY, alpha);
|
|
||||||
}
|
|
||||||
} else if (explored > 0.3) {
|
|
||||||
// Layer 3: Deep fog (previously explored, no longer in LOS)
|
|
||||||
// D-059: near-monochrome, ~10% zone temperature tint, 15-20s breathing cycle
|
|
||||||
vec3 zone_tint = texture(zone_tint_tex, tex_uv).rgb;
|
|
||||||
float noise_val = texture(noise_tex, tile * 0.015 + vec2(time * 0.045, time * 0.03)).r;
|
|
||||||
vec3 tint_color = mix(vec3(0.04), zone_tint, 0.1);
|
|
||||||
float alpha = mix(0.78, 0.90, noise_val); // Fog breathes
|
|
||||||
COLOR = vec4(tint_color, alpha);
|
|
||||||
} else {
|
} else {
|
||||||
// Layer 5: Unexplored, no maps — information zero
|
COLOR = vec4(0.5, 0.0, 0.0, 0.8); // Red: unexplored
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
float vis_raw = texture(visibility_tex, tex_uv).r;
|
||||||
|
float vis = sample_visibility(tex_uv);
|
||||||
|
float explored_raw = texture(exploration_tex, tex_uv).r;
|
||||||
|
float explored = sample_exploration(tex_uv);
|
||||||
|
|
||||||
|
// Prevent gradient bleed into never-explored tiles (use raw, unblurred value)
|
||||||
|
if (explored_raw < 0.01 && vis_raw < 0.01) {
|
||||||
|
vis = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (explored < 0.01 && vis < 0.01) {
|
||||||
|
// Unexplored: solid near-black — information zero
|
||||||
COLOR = vec4(UNEXPLORED_COLOR, 1.0);
|
COLOR = vec4(UNEXPLORED_COLOR, 1.0);
|
||||||
|
} else {
|
||||||
|
// Fog noise — 8-10s breathe cycle, ±0.05 symmetric around baseline
|
||||||
|
float noise_val = texture(noise_tex, tile * 0.03 + vec2(time * 0.11, time * 0.07)).r;
|
||||||
|
float fog_alpha = 0.30 + (noise_val * 2.0 - 1.0) * 0.05; // 0.25-0.35
|
||||||
|
|
||||||
|
// Zone temperature tint (D-046/D-077): subtle warm/cool/neutral per zone
|
||||||
|
vec3 zone_tint = texture(zone_tint_tex, tex_uv).rgb;
|
||||||
|
|
||||||
|
// Clarity ramp: transparent inside cone, light fog at edges and beyond
|
||||||
|
float clarity = smoothstep(0.0, 0.85, vis);
|
||||||
|
float alpha = mix(fog_alpha, 0.0, clarity);
|
||||||
|
vec3 color = mix(zone_tint, vec3(0.0), clarity);
|
||||||
|
|
||||||
|
// Soft edge between explored and unexplored (blurred to avoid staircase)
|
||||||
|
float exp_fade = smoothstep(0.0, 0.3, explored);
|
||||||
|
alpha = mix(1.0, alpha, exp_fade);
|
||||||
|
color = mix(UNEXPLORED_COLOR, color, exp_fade);
|
||||||
|
|
||||||
|
COLOR = vec4(color, alpha);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
After Width: | Height: | Size: 50 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://qlxaaip3fqic"
|
||||||
|
path="res://.godot/imported/cursor_menu.png-03cbe8a063e08efcd4283ce76f668ea1.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://tests/golden/visual/cursor_menu.png"
|
||||||
|
dest_files=["res://.godot/imported/cursor_menu.png-03cbe8a063e08efcd4283ce76f668ea1.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
|
After Width: | Height: | Size: 50 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://oqbx8gwq1xta"
|
||||||
|
path="res://.godot/imported/dialogue_open.png-2e5a2dd99abe2817e4bb7ede8f83b0bf.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://tests/golden/visual/dialogue_open.png"
|
||||||
|
dest_files=["res://.godot/imported/dialogue_open.png-2e5a2dd99abe2817e4bb7ede8f83b0bf.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
|
After Width: | Height: | Size: 50 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://b2hctv70ar7al"
|
||||||
|
path="res://.godot/imported/dialogue_with_monologue.png-5193b3824e2195b1c7ca00bc005f9026.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://tests/golden/visual/dialogue_with_monologue.png"
|
||||||
|
dest_files=["res://.godot/imported/dialogue_with_monologue.png-5193b3824e2195b1c7ca00bc005f9026.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
|
After Width: | Height: | Size: 42 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://bw4kxsno66sv3"
|
||||||
|
path="res://.godot/imported/fog_3state.png-b0193a736de6756c27053a3db24f9a12.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://tests/golden/visual/fog_3state.png"
|
||||||
|
dest_files=["res://.godot/imported/fog_3state.png-b0193a736de6756c27053a3db24f9a12.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
|
After Width: | Height: | Size: 46 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://7ol3rcrpienc"
|
||||||
|
path="res://.godot/imported/fog_boundary.png-7da06d227319f9c43ef7017ee082732a.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://tests/golden/visual/fog_boundary.png"
|
||||||
|
dest_files=["res://.godot/imported/fog_boundary.png-7da06d227319f9c43ef7017ee082732a.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
|
After Width: | Height: | Size: 42 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://fpncie7mnyay"
|
||||||
|
path="res://.godot/imported/fog_boundary_replay.png-3e1acf3ef35450d1df230400b3003250.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://tests/golden/visual/fog_boundary_replay.png"
|
||||||
|
dest_files=["res://.godot/imported/fog_boundary_replay.png-3e1acf3ef35450d1df230400b3003250.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
|
After Width: | Height: | Size: 100 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://71e1yuyjdxn7"
|
||||||
|
path="res://.godot/imported/fog_debug.png-7d563b17499f08fbb82ec57bcdcc0e68.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://tests/golden/visual/fog_debug.png"
|
||||||
|
dest_files=["res://.godot/imported/fog_debug.png-7d563b17499f08fbb82ec57bcdcc0e68.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
|
After Width: | Height: | Size: 42 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://2owpogcjnewr"
|
||||||
|
path="res://.godot/imported/fog_diagonal.png-06416ee4adae921b884280dc73761ed3.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://tests/golden/visual/fog_diagonal.png"
|
||||||
|
dest_files=["res://.godot/imported/fog_diagonal.png-06416ee4adae921b884280dc73761ed3.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
|
After Width: | Height: | Size: 34 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dufvsap028rfw"
|
||||||
|
path="res://.godot/imported/fog_live_hub.png-a2756ff6cb45af238cc0a32ba70a705a.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://tests/golden/visual/fog_live_hub.png"
|
||||||
|
dest_files=["res://.godot/imported/fog_live_hub.png-a2756ff6cb45af238cc0a32ba70a705a.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
|
After Width: | Height: | Size: 42 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dmqmnfrjrk1u"
|
||||||
|
path="res://.godot/imported/fog_live_replay.png-4604646d390fbfac6491f513f667c5c6.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://tests/golden/visual/fog_live_replay.png"
|
||||||
|
dest_files=["res://.godot/imported/fog_live_replay.png-4604646d390fbfac6491f513f667c5c6.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
|
After Width: | Height: | Size: 42 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dgdt2rmql6hbb"
|
||||||
|
path="res://.godot/imported/fog_theater_replay.png-ef10fe62d9da045193422127e48853ba.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://tests/golden/visual/fog_theater_replay.png"
|
||||||
|
dest_files=["res://.godot/imported/fog_theater_replay.png-ef10fe62d9da045193422127e48853ba.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
|
After Width: | Height: | Size: 42 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://b8weq3wp6tybx"
|
||||||
|
path="res://.godot/imported/fog_zone_tint.png-5a5cd0f27eaf991e1b39709de687dcf4.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://tests/golden/visual/fog_zone_tint.png"
|
||||||
|
dest_files=["res://.godot/imported/fog_zone_tint.png-5a5cd0f27eaf991e1b39709de687dcf4.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
|
After Width: | Height: | Size: 42 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://c5r4tk4yafj2r"
|
||||||
|
path="res://.godot/imported/hud_default.png-d3286d0b35f8bfaeab8436d9645c0c0d.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://tests/golden/visual/hud_default.png"
|
||||||
|
dest_files=["res://.godot/imported/hud_default.png-d3286d0b35f8bfaeab8436d9645c0c0d.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
|
After Width: | Height: | Size: 42 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://0pym2n4nq12a"
|
||||||
|
path="res://.godot/imported/minimap_stance.png-abbfd0259479cd17651bee48cdc7f7b8.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://tests/golden/visual/minimap_stance.png"
|
||||||
|
dest_files=["res://.godot/imported/minimap_stance.png-abbfd0259479cd17651bee48cdc7f7b8.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
|
After Width: | Height: | Size: 42 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://cc5nmdpumq6t"
|
||||||
|
path="res://.godot/imported/npc_in_fog.png-e8a24e029e5c640a7daee7d3f2838bdc.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://tests/golden/visual/npc_in_fog.png"
|
||||||
|
dest_files=["res://.godot/imported/npc_in_fog.png-e8a24e029e5c640a7daee7d3f2838bdc.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://ypniv2crdp8c
|
||||||
@@ -250,7 +250,7 @@ func test_gauntlet_ui_stays_hidden_after_multiple_snapshots() -> void:
|
|||||||
|
|
||||||
# -- GauntletHUD Feature Tests (#496) ----------------------------------------
|
# -- GauntletHUD Feature Tests (#496) ----------------------------------------
|
||||||
# Timer lifecycle, personal bests, room change, visibility.
|
# Timer lifecycle, personal bests, room change, visibility.
|
||||||
# Spec: sprint-9/client.md #496. Ticket: db/connectors/ticket show 496.
|
# Spec: sprint-9/client.md #496. Ticket: tooling/db/ticket show 496.
|
||||||
|
|
||||||
func test_format_time_zero() -> void:
|
func test_format_time_zero() -> void:
|
||||||
# Static utility: 0 seconds → "00:00"
|
# Static utility: 0 seconds → "00:00"
|
||||||
@@ -476,7 +476,7 @@ func test_gauntlet_snapshot_roundtrip_via_apply() -> void:
|
|||||||
|
|
||||||
# -- BugReportDialog Feature Tests (#495) ------------------------------------
|
# -- BugReportDialog Feature Tests (#495) ------------------------------------
|
||||||
# Pause/unpause lifecycle, wire guard, text render, state machine.
|
# Pause/unpause lifecycle, wire guard, text render, state machine.
|
||||||
# Spec: sprint-9/client.md #495. Ticket: db/connectors/ticket show 495.
|
# Spec: sprint-9/client.md #495. Ticket: tooling/db/ticket show 495.
|
||||||
|
|
||||||
func test_bug_report_sends_pause_on_open() -> void:
|
func test_bug_report_sends_pause_on_open() -> void:
|
||||||
# start_capture() must send Pause to the server.
|
# start_capture() must send Pause to the server.
|
||||||
|
|||||||
@@ -55,7 +55,9 @@ func test_fog_visibility_forward_tile() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func test_fog_visibility_peripheral_tile() -> void:
|
func test_fog_visibility_peripheral_tile() -> void:
|
||||||
# P1 #4: Peripheral-sector tile writes VIS_PERIPHERAL (180) to _vis_bytes.
|
# P1 #4: Server simplified to forward-only (Sprint 22, #569). All visible
|
||||||
|
# tiles are now written as VIS_FORWARD regardless of visibility_sectors value.
|
||||||
|
# Peripheral sector is no longer a distinct visual state.
|
||||||
var fog = _get_fog_state()
|
var fog = _get_fog_state()
|
||||||
if fog == null:
|
if fog == null:
|
||||||
return
|
return
|
||||||
@@ -65,8 +67,8 @@ func test_fog_visibility_peripheral_tile() -> void:
|
|||||||
GameState.visibility_sectors = {Vector2i(10, 8): "Peripheral"}
|
GameState.visibility_sectors = {Vector2i(10, 8): "Peripheral"}
|
||||||
fog.update_from_state()
|
fog.update_from_state()
|
||||||
assert_that(fog._vis_bytes[8 * 64 + 10]).override_failure_message(
|
assert_that(fog._vis_bytes[8 * 64 + 10]).override_failure_message(
|
||||||
"Peripheral tile at (10,8) should be VIS_PERIPHERAL=%d" % FogState.VIS_PERIPHERAL
|
"All visible tiles write VIS_FORWARD after forward-only simplification"
|
||||||
).is_equal(FogState.VIS_PERIPHERAL)
|
).is_equal(FogState.VIS_FORWARD)
|
||||||
GameState.visible_positions.clear()
|
GameState.visible_positions.clear()
|
||||||
GameState.visibility_sectors.clear()
|
GameState.visibility_sectors.clear()
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ extends GdUnitTestSuite
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const DEBUG_SCENE_PATH: String = "res://scenes/main.tscn"
|
const DEBUG_SCENE_PATH: String = "res://scenes/main.tscn"
|
||||||
const DEBUG_SCRIPT_PATH: String = "res://scripts/ui/debug_overlay.gd"
|
const DEBUG_SCRIPT_PATH: String = "res://ui/debug_overlay.gd"
|
||||||
|
|
||||||
|
|
||||||
func _make_overlay() -> Control:
|
func _make_overlay() -> Control:
|
||||||
@@ -51,7 +51,7 @@ func after_test() -> void:
|
|||||||
|
|
||||||
func test_debug_overlay_script_exists() -> void:
|
func test_debug_overlay_script_exists() -> void:
|
||||||
assert_bool(ResourceLoader.exists(DEBUG_SCRIPT_PATH)).override_failure_message(
|
assert_bool(ResourceLoader.exists(DEBUG_SCRIPT_PATH)).override_failure_message(
|
||||||
"debug_overlay.gd must exist at res://scripts/ui/debug_overlay.gd (#348)"
|
"debug_overlay.gd must exist at res://ui/debug_overlay.gd (#348)"
|
||||||
).is_true()
|
).is_true()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bdsgybncfyu52
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b46mtkralmq14
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bh8tiio43gveq
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
## Sprint 22 — Entanglement ratio configuration acceptance tests (#175, #178)
|
||||||
|
##
|
||||||
|
## Test-first stubs for the client-side surface of the world_seed feature.
|
||||||
|
## These tests will warn-and-skip until the implementation lands (Tyre, #175).
|
||||||
|
##
|
||||||
|
## Client-side acceptance criteria (#175):
|
||||||
|
## - GameState carries a world_seed field (stores the seed for this session)
|
||||||
|
## - SessionManager.new_game() generates and stores a world_seed
|
||||||
|
## - The IPC startup payload carries world_seed so the server can seed SimRng
|
||||||
|
##
|
||||||
|
## Server-side acceptance criteria (#178) are in:
|
||||||
|
## - server/src/content/entanglement.rs (Rust unit tests)
|
||||||
|
##
|
||||||
|
## Spec: D-029 (30/50/20 entanglement ratio, variable per seed), D-010 (deterministic sim)
|
||||||
|
## Tickets: #175, #178
|
||||||
|
class_name TestEntanglementSprint22
|
||||||
|
extends GdUnitTestSuite
|
||||||
|
|
||||||
|
|
||||||
|
# -- Client-side: GameState.world_seed field (#175) ---------------------------
|
||||||
|
|
||||||
|
func test_game_state_has_world_seed_field() -> void:
|
||||||
|
# #175 client-side: GameState must store the world_seed for this session.
|
||||||
|
# The seed is set by SessionManager.new_game() and read by SimBridge to
|
||||||
|
# carry it in the session startup IPC message.
|
||||||
|
if not "world_seed" in GameState:
|
||||||
|
push_warning("TestEntanglementSprint22: GameState.world_seed not found — test-first stub (awaiting #175)")
|
||||||
|
return
|
||||||
|
# Field exists — verify it is numeric (int or null are both acceptable initial states)
|
||||||
|
var seed_val = GameState.get("world_seed")
|
||||||
|
assert_bool(seed_val == null or seed_val is int).override_failure_message(
|
||||||
|
"GameState.world_seed must be int or null"
|
||||||
|
).is_true()
|
||||||
|
|
||||||
|
|
||||||
|
func test_game_state_world_seed_can_be_set_and_read() -> void:
|
||||||
|
if not "world_seed" in GameState:
|
||||||
|
push_warning("TestEntanglementSprint22: GameState.world_seed missing — skipped (#175 not yet implemented)")
|
||||||
|
return
|
||||||
|
var orig = GameState.get("world_seed")
|
||||||
|
GameState.world_seed = 0xDEADBEEF
|
||||||
|
assert_int(GameState.world_seed).is_equal(0xDEADBEEF)
|
||||||
|
# Restore
|
||||||
|
GameState.world_seed = orig
|
||||||
|
|
||||||
|
|
||||||
|
func test_game_state_world_seed_default_is_null_or_zero() -> void:
|
||||||
|
# Before a session starts, world_seed should be null (no session) or 0 (unset).
|
||||||
|
if not "world_seed" in GameState:
|
||||||
|
push_warning("TestEntanglementSprint22: GameState.world_seed missing — skipped")
|
||||||
|
return
|
||||||
|
var seed_val = GameState.get("world_seed")
|
||||||
|
assert_bool(seed_val == null or seed_val == 0).override_failure_message(
|
||||||
|
"GameState.world_seed should be null or 0 before any session starts"
|
||||||
|
).is_true()
|
||||||
|
|
||||||
|
|
||||||
|
# -- Client-side: SessionManager seed generation (#175) -----------------------
|
||||||
|
|
||||||
|
func test_session_manager_exists() -> void:
|
||||||
|
var sm = get_node_or_null("/root/SessionManager")
|
||||||
|
if sm == null:
|
||||||
|
push_warning("TestEntanglementSprint22: SessionManager autoload not found — skipped")
|
||||||
|
return
|
||||||
|
assert_that(sm).is_not_null()
|
||||||
|
|
||||||
|
|
||||||
|
func test_session_manager_new_game_generates_world_seed() -> void:
|
||||||
|
# #175: new_game() must generate and store world_seed in GameState.
|
||||||
|
# The seed is a non-zero u64 that will be sent to the server on startup.
|
||||||
|
var sm = get_node_or_null("/root/SessionManager")
|
||||||
|
if sm == null:
|
||||||
|
push_warning("TestEntanglementSprint22: SessionManager not found — skipped")
|
||||||
|
return
|
||||||
|
if not "world_seed" in GameState:
|
||||||
|
push_warning("TestEntanglementSprint22: GameState.world_seed missing — test-first stub")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Call new_game() (will create a save dir — acceptable in test environment)
|
||||||
|
var orig_seed = GameState.get("world_seed")
|
||||||
|
var orig_game_id: String = GameState.current_game_id
|
||||||
|
sm.new_game()
|
||||||
|
var generated_seed = GameState.get("world_seed")
|
||||||
|
|
||||||
|
# world_seed must have been set to a non-null, non-zero value
|
||||||
|
assert_bool(generated_seed != null).override_failure_message(
|
||||||
|
"SessionManager.new_game() must set GameState.world_seed (#175)"
|
||||||
|
).is_true()
|
||||||
|
if generated_seed != null:
|
||||||
|
assert_bool(generated_seed != 0).override_failure_message(
|
||||||
|
"Generated world_seed must be non-zero"
|
||||||
|
).is_true()
|
||||||
|
|
||||||
|
# Restore state
|
||||||
|
GameState.current_game_id = orig_game_id
|
||||||
|
GameState.world_seed = orig_seed
|
||||||
|
|
||||||
|
|
||||||
|
func test_session_manager_same_game_id_has_same_seed() -> void:
|
||||||
|
# Resuming a session must restore the original world_seed (not generate a new one).
|
||||||
|
# This ensures deterministic replays work correctly (D-010).
|
||||||
|
var sm = get_node_or_null("/root/SessionManager")
|
||||||
|
if sm == null:
|
||||||
|
push_warning("TestEntanglementSprint22: SessionManager not found — skipped")
|
||||||
|
return
|
||||||
|
if not sm.has_method("resume_game"):
|
||||||
|
push_warning("TestEntanglementSprint22: resume_game() missing — skipped")
|
||||||
|
return
|
||||||
|
if not "world_seed" in GameState:
|
||||||
|
push_warning("TestEntanglementSprint22: GameState.world_seed missing — test-first stub")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Set a known seed and game_id, then resume — seed must not be clobbered
|
||||||
|
GameState.world_seed = 12345678
|
||||||
|
var orig_game_id: String = GameState.current_game_id
|
||||||
|
sm.resume_game("20260228-120000-abc123")
|
||||||
|
# resume_game() must NOT overwrite world_seed
|
||||||
|
assert_int(GameState.world_seed).override_failure_message(
|
||||||
|
"resume_game() must not overwrite world_seed — seed is loaded from the save, not regenerated"
|
||||||
|
).is_equal(12345678)
|
||||||
|
GameState.current_game_id = orig_game_id
|
||||||
|
|
||||||
|
|
||||||
|
# -- IPC startup message: world_seed field (#175) ----------------------------
|
||||||
|
|
||||||
|
func test_protocol_encode_startup_message_has_world_seed_field() -> void:
|
||||||
|
# #175 acceptance: startup IPC message must carry "world_seed" key.
|
||||||
|
# Verifies Protocol.encode_startup_message encodes the seed so the server
|
||||||
|
# can deserialize it as StartupMessage { world_seed: u64 }.
|
||||||
|
var seed: int = 0xDEADBEEF # 3735928559 — fits in u32, safely maps to Rust u64
|
||||||
|
var bytes: PackedByteArray = Protocol.encode_startup_message(seed)
|
||||||
|
assert_bool(bytes.size() > 0).override_failure_message(
|
||||||
|
"Protocol.encode_startup_message must return non-empty bytes"
|
||||||
|
).is_true()
|
||||||
|
var decoded = Messagepack.decode(bytes)
|
||||||
|
assert_that(decoded.status).override_failure_message(
|
||||||
|
"encode_startup_message output must be valid msgpack: %s" % str(decoded.status)
|
||||||
|
).is_null()
|
||||||
|
var msg = decoded.value
|
||||||
|
assert_bool(msg is Dictionary and msg.has("world_seed")).override_failure_message(
|
||||||
|
"StartupMessage wire payload must contain 'world_seed' key, got: %s" % str(msg)
|
||||||
|
).is_true()
|
||||||
|
assert_int(msg["world_seed"]).override_failure_message(
|
||||||
|
"world_seed must round-trip through msgpack unchanged"
|
||||||
|
).is_equal(seed)
|
||||||
|
|
||||||
|
|
||||||
|
func test_protocol_encode_startup_message_zero_seed() -> void:
|
||||||
|
# Edge case: seed=0 must still encode a valid payload (world_seed: 0).
|
||||||
|
var bytes: PackedByteArray = Protocol.encode_startup_message(0)
|
||||||
|
assert_bool(bytes.size() > 0).is_true()
|
||||||
|
var decoded = Messagepack.decode(bytes)
|
||||||
|
assert_that(decoded.status).is_null()
|
||||||
|
assert_int(decoded.value["world_seed"]).is_equal(0)
|
||||||
|
|
||||||
|
|
||||||
|
func test_sim_bridge_can_send_world_seed_in_startup() -> void:
|
||||||
|
# #175 acceptance: "startup IPC message carries a world_seed field"
|
||||||
|
# The client must be able to include world_seed in the session startup payload.
|
||||||
|
# Test-first: verify the API exists (method or field), else warn-and-skip.
|
||||||
|
var sim_bridge = get_node_or_null("/root/SimBridge")
|
||||||
|
if sim_bridge == null:
|
||||||
|
push_warning("TestEntanglementSprint22: SimBridge not found — skipped")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Option A: SimBridge has a world_seed property that is sent during startup
|
||||||
|
if "world_seed" in sim_bridge:
|
||||||
|
sim_bridge.world_seed = 99999
|
||||||
|
assert_int(sim_bridge.world_seed).is_equal(99999)
|
||||||
|
sim_bridge.world_seed = 0
|
||||||
|
return
|
||||||
|
|
||||||
|
# Option B: SimBridge has a set_world_seed() method
|
||||||
|
if sim_bridge.has_method("set_world_seed"):
|
||||||
|
# Method exists — this is the expected API
|
||||||
|
sim_bridge.set_world_seed(99999)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Neither found — test-first stub
|
||||||
|
push_warning(
|
||||||
|
"TestEntanglementSprint22: SimBridge has no world_seed field or set_world_seed() — " +
|
||||||
|
"test-first stub awaiting #175 implementation"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# -- Protocol: world_seed flows from client to server (#175) ------------------
|
||||||
|
|
||||||
|
func test_apply_snapshot_does_not_clobber_world_seed() -> void:
|
||||||
|
# world_seed is set at session start and must persist across all subsequent snapshots.
|
||||||
|
# Snapshots must not overwrite or clear the world_seed that was set at startup.
|
||||||
|
if not "world_seed" in GameState:
|
||||||
|
push_warning("TestEntanglementSprint22: GameState.world_seed missing — test-first stub")
|
||||||
|
return
|
||||||
|
GameState.world_seed = 42000
|
||||||
|
GameState.apply_snapshot({"tick": 5, "visible_tiles": []})
|
||||||
|
assert_int(GameState.world_seed).override_failure_message(
|
||||||
|
"apply_snapshot() must not clear or overwrite world_seed — seed is set once at session start"
|
||||||
|
).is_equal(42000)
|
||||||
|
GameState.world_seed = null
|
||||||
|
|
||||||
|
|
||||||
|
# -- Seed variation property (#178, informational — full test is Rust-side) ---
|
||||||
|
|
||||||
|
func test_different_seeds_produce_different_configs_informational() -> void:
|
||||||
|
# D-029: "entanglement rate varies per seed to prevent metagaming calibration"
|
||||||
|
# The definitive acceptance test for this is Rust-side (server/src/content/entanglement.rs):
|
||||||
|
# - EntanglementConfig::from_rng(seed_A) == EntanglementConfig::from_rng(seed_A) [deterministic]
|
||||||
|
# - EntanglementConfig::from_rng(seed_A) != EntanglementConfig::from_rng(seed_B) [variable, >=90%]
|
||||||
|
#
|
||||||
|
# This test only verifies the client side: world_seed is a u64 large enough to
|
||||||
|
# have sufficient entropy. A 24-bit game_id hex component alone has 16M combinations;
|
||||||
|
# the full u64 seed provides 2^64 possibilities.
|
||||||
|
#
|
||||||
|
# We verify that two calls to new_game() produce different seeds.
|
||||||
|
var sm = get_node_or_null("/root/SessionManager")
|
||||||
|
if sm == null:
|
||||||
|
push_warning("TestEntanglementSprint22: SessionManager not found — skipped")
|
||||||
|
return
|
||||||
|
if not "world_seed" in GameState:
|
||||||
|
push_warning("TestEntanglementSprint22: GameState.world_seed missing — test-first stub")
|
||||||
|
return
|
||||||
|
|
||||||
|
var orig_game_id: String = GameState.current_game_id
|
||||||
|
sm.new_game()
|
||||||
|
var seed_a = GameState.get("world_seed")
|
||||||
|
sm.new_game()
|
||||||
|
var seed_b = GameState.get("world_seed")
|
||||||
|
|
||||||
|
if seed_a == null or seed_b == null:
|
||||||
|
push_warning("TestEntanglementSprint22: new_game() did not set world_seed — test-first stub")
|
||||||
|
GameState.current_game_id = orig_game_id
|
||||||
|
return
|
||||||
|
|
||||||
|
# Two different sessions should produce different seeds
|
||||||
|
assert_bool(seed_a != seed_b).override_failure_message(
|
||||||
|
"Two calls to new_game() must produce different world_seeds (D-029 anti-metagaming)"
|
||||||
|
).is_true()
|
||||||
|
GameState.current_game_id = orig_game_id
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://hdoafqdfr6sq
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c8e2watwphu41
|
||||||
@@ -0,0 +1,517 @@
|
|||||||
|
## Sprint 22 — Fog system acceptance tests (#569)
|
||||||
|
##
|
||||||
|
## Validates FogState data management against the Sprint 22 acceptance criteria:
|
||||||
|
## - Explored tiles never revert to unexplored black (EXP_EXPLORED persistence)
|
||||||
|
## - Bounds grow-only invariant (explored tiles behind player stay in texture)
|
||||||
|
## - All visible tiles written as Forward (server simplified to Forward-only)
|
||||||
|
## - Exploration data survives texture resize (grow-only bounds copy)
|
||||||
|
## - Shader file present with correct fog_alpha constant
|
||||||
|
##
|
||||||
|
## Spec: D-059 (fog shader), D-015 (vision cone), D-066 (dual-scale grid, 6-8 tile gradient)
|
||||||
|
## Ticket: #569
|
||||||
|
class_name TestFogSprint22
|
||||||
|
extends GdUnitTestSuite
|
||||||
|
|
||||||
|
|
||||||
|
func _get_fog_state() -> Node:
|
||||||
|
var node = get_node_or_null("/root/FogState")
|
||||||
|
if node == null:
|
||||||
|
push_warning("TestFogSprint22: FogState autoload not found — test skipped (awaiting #569)")
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
func before_test() -> void:
|
||||||
|
GameState.visible_positions.clear()
|
||||||
|
GameState.visible_tiles.clear()
|
||||||
|
GameState.visibility_sectors.clear()
|
||||||
|
|
||||||
|
|
||||||
|
func after_test() -> void:
|
||||||
|
GameState.visible_positions.clear()
|
||||||
|
GameState.visible_tiles.clear()
|
||||||
|
GameState.visibility_sectors.clear()
|
||||||
|
|
||||||
|
|
||||||
|
# -- Spec constants (D-059) ---------------------------------------------------
|
||||||
|
|
||||||
|
func test_exp_explored_constant_is_128() -> void:
|
||||||
|
# EXP_EXPLORED = 128 → shader reads this as ~0.502.
|
||||||
|
# smoothstep(0.0, 0.2, 0.502) = 1.0 → exp_fade fully applied.
|
||||||
|
# If EXP_EXPLORED were 0, explored tiles would render as solid unexplored black.
|
||||||
|
var fog_state = _get_fog_state()
|
||||||
|
if fog_state == null:
|
||||||
|
return
|
||||||
|
assert_int(fog_state.EXP_EXPLORED).override_failure_message(
|
||||||
|
"EXP_EXPLORED must be 128 — shader exp_fade requires explored value > 0.2 to avoid unexplored-black rendering"
|
||||||
|
).is_equal(128)
|
||||||
|
|
||||||
|
|
||||||
|
func test_exp_unexplored_constant_is_0() -> void:
|
||||||
|
var fog_state = _get_fog_state()
|
||||||
|
if fog_state == null:
|
||||||
|
return
|
||||||
|
assert_int(fog_state.EXP_UNEXPLORED).is_equal(0)
|
||||||
|
|
||||||
|
|
||||||
|
func test_exp_visible_constant_is_255() -> void:
|
||||||
|
# EXP_VISIBLE = 255 → shader reads 1.0, full art visibility (currently in LOS)
|
||||||
|
var fog_state = _get_fog_state()
|
||||||
|
if fog_state == null:
|
||||||
|
return
|
||||||
|
assert_int(fog_state.EXP_VISIBLE).is_equal(255)
|
||||||
|
|
||||||
|
|
||||||
|
func test_vis_forward_constant_is_255() -> void:
|
||||||
|
# D-059: VIS_FORWARD = 255 → clear vision, nearly transparent fog overlay
|
||||||
|
var fog_state = _get_fog_state()
|
||||||
|
if fog_state == null:
|
||||||
|
return
|
||||||
|
assert_int(fog_state.VIS_FORWARD).is_equal(255)
|
||||||
|
|
||||||
|
|
||||||
|
func test_vis_hidden_constant_is_0() -> void:
|
||||||
|
# D-059: VIS_HIDDEN = 0 → no vision, fog fully opaque
|
||||||
|
var fog_state = _get_fog_state()
|
||||||
|
if fog_state == null:
|
||||||
|
return
|
||||||
|
assert_int(fog_state.VIS_HIDDEN).is_equal(0)
|
||||||
|
|
||||||
|
|
||||||
|
func test_unexplored_color_spec_value() -> void:
|
||||||
|
# D-059: Unexplored = solid near-black #12141a
|
||||||
|
# Verify the hex value decodes to the expected channel values.
|
||||||
|
var c := Color("#12141a")
|
||||||
|
assert_float(c.r).is_equal_approx(18.0 / 255.0, 0.003)
|
||||||
|
assert_float(c.g).is_equal_approx(20.0 / 255.0, 0.003)
|
||||||
|
assert_float(c.b).is_equal_approx(26.0 / 255.0, 0.003)
|
||||||
|
# Sanity: it IS very dark (all channels < 0.12)
|
||||||
|
assert_float(c.r).is_less(0.12)
|
||||||
|
assert_float(c.g).is_less(0.12)
|
||||||
|
assert_float(c.b).is_less(0.12)
|
||||||
|
|
||||||
|
|
||||||
|
# -- Acceptance: explored tiles persist after leaving LOS (criterion 3) ------
|
||||||
|
|
||||||
|
func test_explored_tile_becomes_exp_explored_after_leaving_los() -> void:
|
||||||
|
# ACCEPTANCE CRITERION: "Explored tiles never revert to unexplored black"
|
||||||
|
# When tile (5,5) was in LOS (frame 1) and then leaves LOS (frame 2),
|
||||||
|
# its exploration byte must be EXP_EXPLORED (128), not EXP_UNEXPLORED (0).
|
||||||
|
var fog_state = _get_fog_state()
|
||||||
|
if fog_state == null:
|
||||||
|
return
|
||||||
|
if not fog_state.has_method("update_from_state"):
|
||||||
|
push_warning("TestFogSprint22: update_from_state missing — skipped")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Frame 1: tile (5,5) is visible
|
||||||
|
GameState.visible_positions = {Vector2i(5, 5): true}
|
||||||
|
GameState.visible_tiles = [{"x": 5, "y": 5, "z": 0, "visibility": "Forward"}]
|
||||||
|
fog_state.update_from_state()
|
||||||
|
|
||||||
|
# Frame 2: tile (5,5) leaves LOS
|
||||||
|
GameState.visible_positions.clear()
|
||||||
|
GameState.visible_tiles = []
|
||||||
|
fog_state.update_from_state()
|
||||||
|
|
||||||
|
# Internal state check: _exp_bytes[tile(5,5)] must be EXP_EXPLORED (128)
|
||||||
|
var exp_bytes = fog_state.get("_exp_bytes")
|
||||||
|
if exp_bytes == null:
|
||||||
|
push_warning("TestFogSprint22: _exp_bytes not accessible — data path untestable headlessly")
|
||||||
|
return
|
||||||
|
var ox: int = fog_state.map_bounds.position.x
|
||||||
|
var oy: int = fog_state.map_bounds.position.y
|
||||||
|
var w: int = fog_state.get("_width") if fog_state.get("_width") != null else -1
|
||||||
|
if w <= 0:
|
||||||
|
push_warning("TestFogSprint22: _width inaccessible — data path untestable")
|
||||||
|
return
|
||||||
|
var px := 5 - ox
|
||||||
|
var py := 5 - oy
|
||||||
|
if px < 0 or py < 0 or px >= w:
|
||||||
|
push_warning("TestFogSprint22: tile (5,5) out of bounds after update — check grow_bounds margin")
|
||||||
|
return
|
||||||
|
var idx := py * w + px
|
||||||
|
if idx < 0 or idx >= exp_bytes.size():
|
||||||
|
push_warning("TestFogSprint22: idx %d out of exp_bytes range %d" % [idx, exp_bytes.size()])
|
||||||
|
return
|
||||||
|
assert_int(exp_bytes[idx]).override_failure_message(
|
||||||
|
"Tile (5,5) must be EXP_EXPLORED=128 after leaving LOS — not EXP_UNEXPLORED=0 (#569 regression)"
|
||||||
|
).is_equal(fog_state.EXP_EXPLORED)
|
||||||
|
|
||||||
|
|
||||||
|
func test_explored_tile_is_exp_visible_while_in_los() -> void:
|
||||||
|
# While in LOS, tile exploration byte must be EXP_VISIBLE (255)
|
||||||
|
var fog_state = _get_fog_state()
|
||||||
|
if fog_state == null:
|
||||||
|
return
|
||||||
|
if not fog_state.has_method("update_from_state"):
|
||||||
|
return
|
||||||
|
|
||||||
|
GameState.visible_positions = {Vector2i(3, 3): true}
|
||||||
|
GameState.visible_tiles = [{"x": 3, "y": 3, "z": 0, "visibility": "Forward"}]
|
||||||
|
fog_state.update_from_state()
|
||||||
|
|
||||||
|
var exp_bytes = fog_state.get("_exp_bytes")
|
||||||
|
if exp_bytes == null:
|
||||||
|
return
|
||||||
|
var ox: int = fog_state.map_bounds.position.x
|
||||||
|
var oy: int = fog_state.map_bounds.position.y
|
||||||
|
var w: int = fog_state.get("_width") if fog_state.get("_width") != null else -1
|
||||||
|
if w <= 0:
|
||||||
|
return
|
||||||
|
var px := 3 - ox
|
||||||
|
var py := 3 - oy
|
||||||
|
if px < 0 or py < 0 or px >= w:
|
||||||
|
return
|
||||||
|
var idx := py * w + px
|
||||||
|
if idx >= 0 and idx < exp_bytes.size():
|
||||||
|
assert_int(exp_bytes[idx]).is_equal(fog_state.EXP_VISIBLE)
|
||||||
|
|
||||||
|
|
||||||
|
func test_unexplored_tile_stays_exp_unexplored() -> void:
|
||||||
|
# Tile (7, 8) was never seen — must remain EXP_UNEXPLORED (0)
|
||||||
|
var fog_state = _get_fog_state()
|
||||||
|
if fog_state == null:
|
||||||
|
return
|
||||||
|
if not fog_state.has_method("update_from_state"):
|
||||||
|
return
|
||||||
|
|
||||||
|
# See only (5, 5) — tile (7, 8) is not in LOS
|
||||||
|
GameState.visible_positions = {Vector2i(5, 5): true}
|
||||||
|
GameState.visible_tiles = [{"x": 5, "y": 5, "z": 0, "visibility": "Forward"}]
|
||||||
|
fog_state.update_from_state()
|
||||||
|
|
||||||
|
var exp_bytes = fog_state.get("_exp_bytes")
|
||||||
|
if exp_bytes == null:
|
||||||
|
return
|
||||||
|
var ox: int = fog_state.map_bounds.position.x
|
||||||
|
var oy: int = fog_state.map_bounds.position.y
|
||||||
|
var w: int = fog_state.get("_width") if fog_state.get("_width") != null else -1
|
||||||
|
if w <= 0:
|
||||||
|
return
|
||||||
|
var px := 7 - ox
|
||||||
|
var py := 8 - oy
|
||||||
|
if px < 0 or py < 0 or px >= w:
|
||||||
|
return
|
||||||
|
var idx := py * w + px
|
||||||
|
if idx >= 0 and idx < exp_bytes.size():
|
||||||
|
assert_int(exp_bytes[idx]).is_equal(fog_state.EXP_UNEXPLORED)
|
||||||
|
|
||||||
|
|
||||||
|
# -- Acceptance: bounds grow-only invariant ------------------------------------
|
||||||
|
|
||||||
|
func test_bounds_never_shrink() -> void:
|
||||||
|
# ACCEPTANCE CRITERION: "Explored tiles never revert to unexplored black"
|
||||||
|
# Requires grow-only bounds: once a tile is in the texture, it stays there.
|
||||||
|
var fog_state = _get_fog_state()
|
||||||
|
if fog_state == null:
|
||||||
|
return
|
||||||
|
if not fog_state.has_method("update_from_state"):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Frame 1: see (10, 10) → establishes initial bounds
|
||||||
|
GameState.visible_positions = {Vector2i(10, 10): true}
|
||||||
|
GameState.visible_tiles = [{"x": 10, "y": 10, "z": 0, "visibility": "Forward"}]
|
||||||
|
fog_state.update_from_state()
|
||||||
|
var b1: Rect2i = fog_state.map_bounds
|
||||||
|
|
||||||
|
# Frame 2: see (30, 30) → bounds must expand to include both
|
||||||
|
GameState.visible_positions = {Vector2i(30, 30): true}
|
||||||
|
GameState.visible_tiles = [{"x": 30, "y": 30, "z": 0, "visibility": "Forward"}]
|
||||||
|
fog_state.update_from_state()
|
||||||
|
var b2: Rect2i = fog_state.map_bounds
|
||||||
|
|
||||||
|
# Frame 3: back to (10, 10) → bounds must NOT shrink
|
||||||
|
GameState.visible_positions = {Vector2i(10, 10): true}
|
||||||
|
GameState.visible_tiles = [{"x": 10, "y": 10, "z": 0, "visibility": "Forward"}]
|
||||||
|
fog_state.update_from_state()
|
||||||
|
var b3: Rect2i = fog_state.map_bounds
|
||||||
|
|
||||||
|
assert_bool(b2.size.x >= b1.size.x).override_failure_message(
|
||||||
|
"Bounds must grow when player moves to larger region"
|
||||||
|
).is_true()
|
||||||
|
assert_bool(b2.size.y >= b1.size.y).is_true()
|
||||||
|
assert_bool(b3.size.x >= b2.size.x).override_failure_message(
|
||||||
|
"Bounds must not shrink when player returns to previous position (grow-only invariant)"
|
||||||
|
).is_true()
|
||||||
|
assert_bool(b3.size.y >= b2.size.y).is_true()
|
||||||
|
|
||||||
|
|
||||||
|
func test_bounds_include_margin_for_gradient_bleed() -> void:
|
||||||
|
# D-066: 6-8 tile gradient at cone edge requires texture margin.
|
||||||
|
# _grow_bounds adds 8-tile margin on each side (accommodates 7x7 Gaussian
|
||||||
|
# kernel at 2-texel intervals = ±6 tile reach). After seeing (10,10),
|
||||||
|
# bounds should extend at least 4 tiles beyond the visible tile.
|
||||||
|
var fog_state = _get_fog_state()
|
||||||
|
if fog_state == null:
|
||||||
|
return
|
||||||
|
if not fog_state.has_method("update_from_state"):
|
||||||
|
return
|
||||||
|
|
||||||
|
GameState.visible_positions = {Vector2i(10, 10): true}
|
||||||
|
GameState.visible_tiles = [{"x": 10, "y": 10, "z": 0, "visibility": "Forward"}]
|
||||||
|
fog_state.update_from_state()
|
||||||
|
|
||||||
|
var b: Rect2i = fog_state.map_bounds
|
||||||
|
# With 4-tile margin: bounds.position.x <= 10 - 4 = 6
|
||||||
|
assert_bool(b.position.x <= 6).override_failure_message(
|
||||||
|
"FogState bounds must include 4-tile margin for gradient bleed (D-066 gradient spec)"
|
||||||
|
).is_true()
|
||||||
|
assert_bool(b.position.y <= 6).is_true()
|
||||||
|
|
||||||
|
|
||||||
|
# -- Acceptance: Forward-only visibility (Sprint 22 server simplification) ----
|
||||||
|
|
||||||
|
func test_visible_tiles_written_as_vis_forward() -> void:
|
||||||
|
# Sprint 22: server sends only Forward tiles (Peripheral sector removed).
|
||||||
|
# FogState writes VIS_FORWARD (255) for all tiles in visible_positions.
|
||||||
|
var fog_state = _get_fog_state()
|
||||||
|
if fog_state == null:
|
||||||
|
return
|
||||||
|
if not fog_state.has_method("update_from_state"):
|
||||||
|
return
|
||||||
|
|
||||||
|
GameState.visible_positions = {Vector2i(5, 5): true, Vector2i(6, 5): true}
|
||||||
|
GameState.visible_tiles = [
|
||||||
|
{"x": 5, "y": 5, "z": 0, "visibility": "Forward"},
|
||||||
|
{"x": 6, "y": 5, "z": 0, "visibility": "Forward"},
|
||||||
|
]
|
||||||
|
fog_state.update_from_state()
|
||||||
|
|
||||||
|
var vis_bytes = fog_state.get("_vis_bytes")
|
||||||
|
if vis_bytes == null:
|
||||||
|
return
|
||||||
|
var ox: int = fog_state.map_bounds.position.x
|
||||||
|
var oy: int = fog_state.map_bounds.position.y
|
||||||
|
var w: int = fog_state.get("_width") if fog_state.get("_width") != null else -1
|
||||||
|
if w <= 0:
|
||||||
|
return
|
||||||
|
for pos in [Vector2i(5, 5), Vector2i(6, 5)]:
|
||||||
|
var px := pos.x - ox
|
||||||
|
var py := pos.y - oy
|
||||||
|
if px < 0 or py < 0 or px >= w:
|
||||||
|
continue
|
||||||
|
var idx := py * w + px
|
||||||
|
if idx >= 0 and idx < vis_bytes.size():
|
||||||
|
assert_int(vis_bytes[idx]).override_failure_message(
|
||||||
|
"All visible tiles should be VIS_FORWARD=255 — server is Forward-only in Sprint 22"
|
||||||
|
).is_equal(fog_state.VIS_FORWARD)
|
||||||
|
|
||||||
|
|
||||||
|
func test_tiles_outside_los_written_as_vis_hidden() -> void:
|
||||||
|
# Tiles in bounds but not in visible_positions must be VIS_HIDDEN (0)
|
||||||
|
var fog_state = _get_fog_state()
|
||||||
|
if fog_state == null:
|
||||||
|
return
|
||||||
|
if not fog_state.has_method("update_from_state"):
|
||||||
|
return
|
||||||
|
|
||||||
|
GameState.visible_positions = {Vector2i(5, 5): true}
|
||||||
|
GameState.visible_tiles = [{"x": 5, "y": 5, "z": 0, "visibility": "Forward"}]
|
||||||
|
fog_state.update_from_state()
|
||||||
|
|
||||||
|
# (5, 7) is inside the padded bounds but not visible — must be VIS_HIDDEN
|
||||||
|
var vis_bytes = fog_state.get("_vis_bytes")
|
||||||
|
if vis_bytes == null:
|
||||||
|
return
|
||||||
|
var ox: int = fog_state.map_bounds.position.x
|
||||||
|
var oy: int = fog_state.map_bounds.position.y
|
||||||
|
var w: int = fog_state.get("_width") if fog_state.get("_width") != null else -1
|
||||||
|
if w <= 0:
|
||||||
|
return
|
||||||
|
var px := 5 - ox
|
||||||
|
var py := 7 - oy
|
||||||
|
if px >= 0 and py >= 0 and px < w:
|
||||||
|
var idx := py * w + px
|
||||||
|
if idx >= 0 and idx < vis_bytes.size():
|
||||||
|
assert_int(vis_bytes[idx]).is_equal(fog_state.VIS_HIDDEN)
|
||||||
|
|
||||||
|
|
||||||
|
# -- Acceptance: exploration survives texture resize --------------------------
|
||||||
|
|
||||||
|
func test_exploration_data_preserved_across_bounds_growth() -> void:
|
||||||
|
# D-059: Texture resize must copy old exploration bytes into new texture.
|
||||||
|
# Without this, tiles seen before a resize appear as EXP_UNEXPLORED (black).
|
||||||
|
var fog_state = _get_fog_state()
|
||||||
|
if fog_state == null:
|
||||||
|
return
|
||||||
|
if not fog_state.has_method("update_from_state"):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Frame 1: see (5, 5), then leave
|
||||||
|
GameState.visible_positions = {Vector2i(5, 5): true}
|
||||||
|
GameState.visible_tiles = [{"x": 5, "y": 5, "z": 0, "visibility": "Forward"}]
|
||||||
|
fog_state.update_from_state()
|
||||||
|
GameState.visible_positions.clear()
|
||||||
|
GameState.visible_tiles = []
|
||||||
|
fog_state.update_from_state() # (5,5) → EXP_EXPLORED
|
||||||
|
|
||||||
|
# Frame 2: move far away — forces bounds growth (resize)
|
||||||
|
GameState.visible_positions = {Vector2i(80, 80): true}
|
||||||
|
GameState.visible_tiles = [{"x": 80, "y": 80, "z": 0, "visibility": "Forward"}]
|
||||||
|
fog_state.update_from_state()
|
||||||
|
|
||||||
|
# (5,5) must still be EXP_EXPLORED after the resize
|
||||||
|
var exp_bytes = fog_state.get("_exp_bytes")
|
||||||
|
if exp_bytes == null:
|
||||||
|
return
|
||||||
|
var ox: int = fog_state.map_bounds.position.x
|
||||||
|
var oy: int = fog_state.map_bounds.position.y
|
||||||
|
var w: int = fog_state.get("_width") if fog_state.get("_width") != null else -1
|
||||||
|
if w <= 0:
|
||||||
|
return
|
||||||
|
var px := 5 - ox
|
||||||
|
var py := 5 - oy
|
||||||
|
if px < 0 or py < 0 or px >= w:
|
||||||
|
push_warning("TestFogSprint22: (5,5) not in bounds after resize — is copy-on-resize working?")
|
||||||
|
return
|
||||||
|
var idx := py * w + px
|
||||||
|
if idx >= 0 and idx < exp_bytes.size():
|
||||||
|
assert_int(exp_bytes[idx]).override_failure_message(
|
||||||
|
"Exploration data at (5,5) must survive bounds growth — EXP_EXPLORED (128) expected after resize"
|
||||||
|
).is_greater_equal(fog_state.EXP_EXPLORED)
|
||||||
|
|
||||||
|
|
||||||
|
# -- Shader file checks (D-059) -----------------------------------------------
|
||||||
|
|
||||||
|
func test_fog_gdshader_exists() -> void:
|
||||||
|
assert_bool(ResourceLoader.exists("res://shaders/fog.gdshader")).override_failure_message(
|
||||||
|
"fog.gdshader must exist — fog rendering requires this shader file (#569)"
|
||||||
|
).is_true()
|
||||||
|
|
||||||
|
|
||||||
|
func test_fog_shader_defines_fog_alpha() -> void:
|
||||||
|
# D-059: explored fog overlay must be ~25-30% opacity.
|
||||||
|
# fog_alpha constant controls this. Verify the shader defines it.
|
||||||
|
if not ResourceLoader.exists("res://shaders/fog.gdshader"):
|
||||||
|
push_warning("TestFogSprint22: fog.gdshader not found — shader check skipped")
|
||||||
|
return
|
||||||
|
var source := FileAccess.get_file_as_string("res://shaders/fog.gdshader")
|
||||||
|
if source.is_empty():
|
||||||
|
push_warning("TestFogSprint22: fog.gdshader is empty or unreadable")
|
||||||
|
return
|
||||||
|
assert_bool(source.contains("fog_alpha")).override_failure_message(
|
||||||
|
"fog.gdshader must define fog_alpha for the 25-30%% explored-tile overlay (D-059)"
|
||||||
|
).is_true()
|
||||||
|
|
||||||
|
|
||||||
|
func test_fog_shader_defines_smoothstep_clarity_ramp() -> void:
|
||||||
|
# D-059/D-066: smooth gradient requires a clarity ramp (smoothstep).
|
||||||
|
# The blurred visibility → clarity ramp must use smoothstep for smooth gradients.
|
||||||
|
if not ResourceLoader.exists("res://shaders/fog.gdshader"):
|
||||||
|
push_warning("TestFogSprint22: fog.gdshader not found — skipped")
|
||||||
|
return
|
||||||
|
var source := FileAccess.get_file_as_string("res://shaders/fog.gdshader")
|
||||||
|
if source.is_empty():
|
||||||
|
return
|
||||||
|
assert_bool(source.contains("smoothstep")).override_failure_message(
|
||||||
|
"fog.gdshader must use smoothstep for the clarity ramp — hard steps violate D-066 gradient spec"
|
||||||
|
).is_true()
|
||||||
|
|
||||||
|
|
||||||
|
func test_fog_shader_defines_unexplored_color() -> void:
|
||||||
|
# D-059: unexplored = solid near-black #12141a.
|
||||||
|
if not ResourceLoader.exists("res://shaders/fog.gdshader"):
|
||||||
|
push_warning("TestFogSprint22: fog.gdshader not found — skipped")
|
||||||
|
return
|
||||||
|
var source := FileAccess.get_file_as_string("res://shaders/fog.gdshader")
|
||||||
|
if source.is_empty():
|
||||||
|
return
|
||||||
|
assert_bool(source.contains("UNEXPLORED_COLOR")).override_failure_message(
|
||||||
|
"fog.gdshader must define UNEXPLORED_COLOR constant (D-059 #12141a spec)"
|
||||||
|
).is_true()
|
||||||
|
|
||||||
|
|
||||||
|
func test_fog_shader_uses_gaussian_blur_for_gradient() -> void:
|
||||||
|
# D-066: 6-8 tile soft gradient requires Gaussian blur on visibility texture.
|
||||||
|
# Current implementation: 7x7 kernel at 2-texel intervals (±6 tiles), sigma 2.0
|
||||||
|
# in kernel space = 4.0 tiles effective. At 2-sigma (8 tiles), weight drops to 0.14.
|
||||||
|
# This covers the D-066 "6-8 tile" gradient spec.
|
||||||
|
if not ResourceLoader.exists("res://shaders/fog.gdshader"):
|
||||||
|
push_warning("TestFogSprint22: fog.gdshader not found — skipped")
|
||||||
|
return
|
||||||
|
var source := FileAccess.get_file_as_string("res://shaders/fog.gdshader")
|
||||||
|
if source.is_empty():
|
||||||
|
return
|
||||||
|
# 7x7 Gaussian uses dy from -3 to 3
|
||||||
|
assert_bool(source.contains("sample_visibility")).override_failure_message(
|
||||||
|
"fog.gdshader must call sample_visibility() for Gaussian-blurred visibility (D-066 gradient)"
|
||||||
|
).is_true()
|
||||||
|
assert_bool(source.contains("-3.0")).override_failure_message(
|
||||||
|
"fog.gdshader sample_visibility must use 7x7 kernel (±3 tiles) for 6-tile gradient coverage (D-066)"
|
||||||
|
).is_true()
|
||||||
|
|
||||||
|
|
||||||
|
# -- Regression: GameState visible_positions (existing contract) ---------------
|
||||||
|
|
||||||
|
func test_visible_positions_derived_from_visible_tiles_in_server_mode() -> void:
|
||||||
|
# D-020: In real server mode, visible_positions derives from visible_tiles.
|
||||||
|
# Fog rendering depends on this derivation being correct.
|
||||||
|
GameState.apply_snapshot({
|
||||||
|
"tick": 10,
|
||||||
|
"visible_tiles": [
|
||||||
|
{"x": 7, "y": 7, "z": 0, "visibility": "Forward"},
|
||||||
|
{"x": 8, "y": 7, "z": 0, "visibility": "Forward"},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
assert_bool(GameState.visible_positions.has(Vector2i(7, 7))).override_failure_message(
|
||||||
|
"visible_positions must be derived from visible_tiles when no explicit visible_positions key"
|
||||||
|
).is_true()
|
||||||
|
assert_bool(GameState.visible_positions.has(Vector2i(8, 7))).is_true()
|
||||||
|
|
||||||
|
|
||||||
|
func test_visibility_sectors_populated_forward_only() -> void:
|
||||||
|
# D-015: visibility_sectors must be populated from visible_tiles.
|
||||||
|
# In Forward-only mode, all sectors are "Forward".
|
||||||
|
GameState.apply_snapshot({
|
||||||
|
"tick": 11,
|
||||||
|
"visible_tiles": [
|
||||||
|
{"x": 4, "y": 4, "z": 0, "visibility": "Forward"},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
assert_bool(GameState.visibility_sectors.has(Vector2i(4, 4))).is_true()
|
||||||
|
assert_str(GameState.visibility_sectors[Vector2i(4, 4)]).is_equal("Forward")
|
||||||
|
|
||||||
|
|
||||||
|
func test_visible_positions_cleared_on_new_snapshot() -> void:
|
||||||
|
# Old positions from tick N must not persist to tick N+1
|
||||||
|
GameState.apply_snapshot({
|
||||||
|
"tick": 1,
|
||||||
|
"visible_tiles": [{"x": 5, "y": 5, "z": 0, "visibility": "Forward"}],
|
||||||
|
})
|
||||||
|
assert_int(GameState.visible_positions.size()).is_equal(1)
|
||||||
|
GameState.apply_snapshot({
|
||||||
|
"tick": 2,
|
||||||
|
"visible_tiles": [{"x": 10, "y": 10, "z": 0, "visibility": "Forward"}],
|
||||||
|
})
|
||||||
|
assert_bool(GameState.visible_positions.has(Vector2i(5, 5))).override_failure_message(
|
||||||
|
"Old visible positions must be cleared when new visible_tiles arrive"
|
||||||
|
).is_false()
|
||||||
|
assert_bool(GameState.visible_positions.has(Vector2i(10, 10))).is_true()
|
||||||
|
|
||||||
|
|
||||||
|
# -- Performance (D-059) -------------------------------------------------------
|
||||||
|
|
||||||
|
func test_fog_state_update_under_2ms_for_400_tiles() -> void:
|
||||||
|
# D-059: <1ms/frame CPU budget for fog update. Allow 2x margin for test env.
|
||||||
|
var fog_state = _get_fog_state()
|
||||||
|
if fog_state == null:
|
||||||
|
return
|
||||||
|
if not fog_state.has_method("update_from_state"):
|
||||||
|
return
|
||||||
|
|
||||||
|
var positions: Dictionary = {}
|
||||||
|
var tiles: Array = []
|
||||||
|
for x in range(20):
|
||||||
|
for y in range(20):
|
||||||
|
positions[Vector2i(x, y)] = true
|
||||||
|
tiles.append({"x": x, "y": y, "z": 0, "visibility": "Forward"})
|
||||||
|
GameState.visible_positions = positions
|
||||||
|
GameState.visible_tiles = tiles
|
||||||
|
|
||||||
|
var start := Time.get_ticks_usec()
|
||||||
|
fog_state.update_from_state()
|
||||||
|
var elapsed_ms := (Time.get_ticks_usec() - start) / 1000.0
|
||||||
|
|
||||||
|
assert_float(elapsed_ms).override_failure_message(
|
||||||
|
"FogState.update_from_state() must complete in <2ms for 400 tiles (spec: <1ms D-059)"
|
||||||
|
).is_less(2.0)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dvt7ytxff0id
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c6lle6hfc1cf6
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cp6n6uo6pfnee
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cao5jf5h36img
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dm46ip672i3jc
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
## Sprint 21 — Save/load game flow (#257)
|
||||||
|
## Tests: LoadingScreen overlay, pending_load_path field, deferred dispatch guards.
|
||||||
|
class_name TestSaveLoadFlowSprint21
|
||||||
|
extends GdUnitTestSuite
|
||||||
|
|
||||||
|
|
||||||
|
const LOADING_SCREEN_SCENE_PATH: String = "res://ui/loading_screen.tscn"
|
||||||
|
|
||||||
|
|
||||||
|
func _make_loading_screen() -> Control:
|
||||||
|
if not ResourceLoader.exists(LOADING_SCREEN_SCENE_PATH):
|
||||||
|
push_warning("TestSaveLoadFlowSprint21: loading_screen.tscn not found — skip")
|
||||||
|
return null
|
||||||
|
var node: Control = load(LOADING_SCREEN_SCENE_PATH).instantiate()
|
||||||
|
add_child(node)
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
func before_test() -> void:
|
||||||
|
GameState.pending_load_path = ""
|
||||||
|
|
||||||
|
|
||||||
|
func after_test() -> void:
|
||||||
|
GameState.pending_load_path = ""
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# LoadingScreen scene
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func test_loading_screen_scene_exists() -> void:
|
||||||
|
assert_bool(ResourceLoader.exists(LOADING_SCREEN_SCENE_PATH)).override_failure_message(
|
||||||
|
"LoadingScreen scene must exist at res://ui/loading_screen.tscn (#257)"
|
||||||
|
).is_true()
|
||||||
|
|
||||||
|
|
||||||
|
func test_loading_screen_hidden_on_ready() -> void:
|
||||||
|
var ls := _make_loading_screen()
|
||||||
|
if ls == null: return
|
||||||
|
assert_bool(ls.visible).override_failure_message(
|
||||||
|
"LoadingScreen must be hidden on _ready (#257)"
|
||||||
|
).is_false()
|
||||||
|
ls.queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
func test_show_loading_makes_visible() -> void:
|
||||||
|
var ls := _make_loading_screen()
|
||||||
|
if ls == null: return
|
||||||
|
ls.show_loading()
|
||||||
|
assert_bool(ls.visible).override_failure_message(
|
||||||
|
"show_loading() must make LoadingScreen visible"
|
||||||
|
).is_true()
|
||||||
|
ls.queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
func test_hide_loading_makes_invisible() -> void:
|
||||||
|
var ls := _make_loading_screen()
|
||||||
|
if ls == null: return
|
||||||
|
ls.show_loading()
|
||||||
|
ls.hide_loading()
|
||||||
|
assert_bool(ls.visible).override_failure_message(
|
||||||
|
"hide_loading() must hide LoadingScreen"
|
||||||
|
).is_false()
|
||||||
|
ls.queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
func test_hide_loading_accepts_success_param() -> void:
|
||||||
|
var ls := _make_loading_screen()
|
||||||
|
if ls == null: return
|
||||||
|
ls.show_loading()
|
||||||
|
ls.hide_loading(true)
|
||||||
|
assert_bool(ls.visible).is_false()
|
||||||
|
ls.show_loading()
|
||||||
|
ls.hide_loading(false)
|
||||||
|
assert_bool(ls.visible).is_false()
|
||||||
|
ls.queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
func test_hide_loading_default_param_is_true() -> void:
|
||||||
|
var ls := _make_loading_screen()
|
||||||
|
if ls == null: return
|
||||||
|
ls.show_loading()
|
||||||
|
ls.hide_loading() # no argument — default success=true
|
||||||
|
assert_bool(ls.visible).is_false()
|
||||||
|
ls.queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# GameState.pending_load_path field
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func test_pending_load_path_default_is_empty() -> void:
|
||||||
|
GameState.pending_load_path = ""
|
||||||
|
assert_str(GameState.pending_load_path).override_failure_message(
|
||||||
|
"GameState.pending_load_path default must be empty string"
|
||||||
|
).is_empty()
|
||||||
|
|
||||||
|
|
||||||
|
func test_pending_load_path_can_be_set_and_read() -> void:
|
||||||
|
var path := "user://saves/20260225-143022-a7b3f1/quicksave.sav"
|
||||||
|
GameState.pending_load_path = path
|
||||||
|
assert_str(GameState.pending_load_path).override_failure_message(
|
||||||
|
"GameState.pending_load_path must persist the value set"
|
||||||
|
).is_equal(path)
|
||||||
|
|
||||||
|
|
||||||
|
func test_pending_load_path_can_be_cleared() -> void:
|
||||||
|
GameState.pending_load_path = "user://saves/test/quicksave.sav"
|
||||||
|
GameState.pending_load_path = ""
|
||||||
|
assert_str(GameState.pending_load_path).is_empty()
|
||||||