Compare commits
@@ -0,0 +1,231 @@
|
|||||||
|
---
|
||||||
|
name: bug-report
|
||||||
|
description: >
|
||||||
|
Process in-game bug reports captured by the Godot client's bug reporter.
|
||||||
|
Use when the user says "bug reports", "check bug reports", "process bugs",
|
||||||
|
or invokes /bug-report. Scans the user:// bug-reports directory, summarizes
|
||||||
|
each report, and offers investigation, ticket creation, or dismissal.
|
||||||
|
user-invocable: true
|
||||||
|
allowed-tools: Bash, Read, Grep, Glob, Write
|
||||||
|
---
|
||||||
|
|
||||||
|
# Bug Report Skill
|
||||||
|
|
||||||
|
Process in-game bug reports exported by the Godot client to the user data
|
||||||
|
directory. Each report is a directory containing a snapshot of game state at the
|
||||||
|
moment the tester filed the report.
|
||||||
|
|
||||||
|
```
|
||||||
|
BUG_REPORT_DIR: /var/home/jeroenschweitzer/.local/share/godot/app_userdata/The Settled Reach/bug-reports/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Report structure
|
||||||
|
|
||||||
|
Each report lives in a directory named `gauntlet-t{tick}-{timestamp}/` and
|
||||||
|
contains these files:
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `description.txt` | Tester notes + metadata (tick, room, stance, facing, position) |
|
||||||
|
| `render.txt` | Simplified text render of the game snapshot |
|
||||||
|
| `snapshot.json` | Full JSON snapshot (entities, dialogue state, etc.) |
|
||||||
|
| `inputs.jsonl` | Last 60 ticks of player input (replay format) |
|
||||||
|
| `snapshots.jsonl` | Last 60 ticks of observer snapshots |
|
||||||
|
| `seed.txt` | RNG seed for deterministic replay |
|
||||||
|
|
||||||
|
## Invocation
|
||||||
|
|
||||||
|
- `/bug-report` — scan and process all unprocessed reports
|
||||||
|
- `/bug-report <directory-name>` — process a specific report by directory name
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
### 1. Scan for unprocessed reports
|
||||||
|
|
||||||
|
List all report directories in the bug reports directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls -1d "/var/home/jeroenschweitzer/.local/share/godot/app_userdata/The Settled Reach/bug-reports/"*/
|
||||||
|
```
|
||||||
|
|
||||||
|
If no directories are found, report "No bug reports found." and stop.
|
||||||
|
|
||||||
|
If the user provided a specific directory name as argument, filter to only that
|
||||||
|
directory. If it does not exist, report the error and list available reports.
|
||||||
|
|
||||||
|
### 2. Read and summarize each report
|
||||||
|
|
||||||
|
For each report directory, read the following files using the Read tool:
|
||||||
|
|
||||||
|
1. **`description.txt`** — extract:
|
||||||
|
- Tester description / notes (free text at top)
|
||||||
|
- Tick number
|
||||||
|
- Room name
|
||||||
|
- Player stance, facing, position
|
||||||
|
2. **`render.txt`** — extract:
|
||||||
|
- A brief description of what the text render shows (room layout, visible
|
||||||
|
entities, player position marker)
|
||||||
|
3. **`snapshot.json`** — extract:
|
||||||
|
- Total entity count
|
||||||
|
- Whether dialogue is active (look for `dialogue` or `conversation` keys
|
||||||
|
with non-null/non-empty values)
|
||||||
|
- Whether monologue is active (look for `monologue` keys with non-null/
|
||||||
|
non-empty values)
|
||||||
|
- NPC names and positions if present
|
||||||
|
- Any error or anomaly fields
|
||||||
|
4. **`seed.txt`** — note the seed value for reference
|
||||||
|
|
||||||
|
Do NOT read `inputs.jsonl` or `snapshots.jsonl` during the summary phase.
|
||||||
|
These are large files reserved for the investigation step.
|
||||||
|
|
||||||
|
### 3. Present the summary list
|
||||||
|
|
||||||
|
Present a numbered list of all reports with their summaries. Format:
|
||||||
|
|
||||||
|
```
|
||||||
|
## Bug Reports Found: N
|
||||||
|
|
||||||
|
### 1. gauntlet-t{tick}-{timestamp}
|
||||||
|
- **Tick:** {tick} | **Room:** {room} | **Position:** ({x}, {y})
|
||||||
|
- **Stance:** {stance} | **Facing:** {facing}
|
||||||
|
- **Entities:** {count} | **Dialogue active:** yes/no | **Monologue active:** yes/no
|
||||||
|
- **Seed:** {seed}
|
||||||
|
- **Description:** {tester notes, first 2-3 lines}
|
||||||
|
- **Render overview:** {brief description of what render.txt shows}
|
||||||
|
- **Observations:** {any anomalies spotted in the snapshot}
|
||||||
|
|
||||||
|
### 2. gauntlet-t{tick}-{timestamp}
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Offer actions per report
|
||||||
|
|
||||||
|
After presenting the summary list, ask the user which action to take for each
|
||||||
|
report. The three actions are:
|
||||||
|
|
||||||
|
#### Investigate
|
||||||
|
|
||||||
|
Dig deeper into the report for root cause analysis:
|
||||||
|
|
||||||
|
1. Read `snapshot.json` in full — analyze entity states, component values,
|
||||||
|
relationships between entities, any inconsistencies
|
||||||
|
2. Read `inputs.jsonl` — reconstruct what the player was doing in the 60 ticks
|
||||||
|
leading up to the report. Look for:
|
||||||
|
- Rapid input changes (stuck keys, input spam)
|
||||||
|
- Movement into walls or invalid positions
|
||||||
|
- Interaction attempts that may have failed
|
||||||
|
- Timing patterns (actions on same tick as state changes)
|
||||||
|
3. Read `snapshots.jsonl` — compare entity states across recent ticks to find
|
||||||
|
when the bug manifested:
|
||||||
|
- Entity position jumps
|
||||||
|
- State machine transitions that look wrong
|
||||||
|
- Component values going out of expected range
|
||||||
|
- Entities appearing or disappearing unexpectedly
|
||||||
|
4. Cross-reference with `render.txt` to confirm visual manifestation
|
||||||
|
5. Read `seed.txt` and note it — the seed plus `inputs.jsonl` should allow
|
||||||
|
deterministic replay of the scenario
|
||||||
|
|
||||||
|
Present findings as a root cause analysis:
|
||||||
|
|
||||||
|
```
|
||||||
|
## Investigation: gauntlet-t{tick}-{timestamp}
|
||||||
|
|
||||||
|
### Timeline
|
||||||
|
- t{tick-N}: {what happened}
|
||||||
|
- t{tick-M}: {state change}
|
||||||
|
- t{tick}: {bug manifests}
|
||||||
|
|
||||||
|
### Root cause
|
||||||
|
{Analysis of what went wrong and why}
|
||||||
|
|
||||||
|
### Affected systems
|
||||||
|
- {system 1}: {how it's involved}
|
||||||
|
- {system 2}: {how it's involved}
|
||||||
|
|
||||||
|
### Reproduction
|
||||||
|
Seed: {seed}
|
||||||
|
Replay inputs.jsonl from tick {start} to reproduce.
|
||||||
|
|
||||||
|
### Suggested fix
|
||||||
|
{If identifiable from the snapshot data}
|
||||||
|
```
|
||||||
|
|
||||||
|
After investigation, return to the action prompt for this report (the user
|
||||||
|
may want to create a ticket or dismiss after investigating).
|
||||||
|
|
||||||
|
#### Create ticket
|
||||||
|
|
||||||
|
Create a bug ticket in the project database. Determine the team from the
|
||||||
|
nature of the bug:
|
||||||
|
|
||||||
|
- **server** — simulation bugs (entity state, movement, AI, ECS systems,
|
||||||
|
perception, knowledge graph)
|
||||||
|
- **client** — rendering bugs (display glitches, UI issues, input handling,
|
||||||
|
audio, visual artifacts)
|
||||||
|
- **server,client** — integration bugs (protocol mismatch, desync, bridge
|
||||||
|
issues)
|
||||||
|
|
||||||
|
Construct the ticket title and description from the report summary and any
|
||||||
|
investigation findings. Use the ticket CLI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
db/connectors/ticket create bug "{title}" --team {team} --description "{description}"
|
||||||
|
```
|
||||||
|
|
||||||
|
The description should include:
|
||||||
|
- Bug summary (from tester notes)
|
||||||
|
- Tick, room, position
|
||||||
|
- Key observations from snapshot analysis
|
||||||
|
- Seed for reproduction
|
||||||
|
- Report directory name for reference
|
||||||
|
|
||||||
|
After creating the ticket, report the ticket ID to the user.
|
||||||
|
|
||||||
|
#### Dismiss
|
||||||
|
|
||||||
|
Mark the report as not actionable. Remove the report directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rm -rf "/var/home/jeroenschweitzer/.local/share/godot/app_userdata/The Settled Reach/bug-reports/{report-dir}/"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Always confirm with the user before deleting.** State clearly which directory
|
||||||
|
will be removed and wait for confirmation.
|
||||||
|
|
||||||
|
### 5. Batch processing
|
||||||
|
|
||||||
|
When processing multiple reports, work through them one at a time in the
|
||||||
|
numbered order presented. For each report, complete the chosen action before
|
||||||
|
moving to the next.
|
||||||
|
|
||||||
|
If the user wants to batch-dismiss multiple reports, confirm the full list
|
||||||
|
of directories that will be deleted before proceeding.
|
||||||
|
|
||||||
|
### 6. Final summary
|
||||||
|
|
||||||
|
After all reports have been processed, present a summary:
|
||||||
|
|
||||||
|
```
|
||||||
|
## Bug Report Processing Complete
|
||||||
|
|
||||||
|
- **Investigated:** {count}
|
||||||
|
- **Tickets created:** {count} ({ticket IDs})
|
||||||
|
- **Dismissed:** {count}
|
||||||
|
- **Remaining unprocessed:** {count}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tips
|
||||||
|
|
||||||
|
- Large `snapshot.json` files may need to be read with offset/limit parameters.
|
||||||
|
Start with the first 200 lines to get the structure, then target specific
|
||||||
|
sections.
|
||||||
|
- `inputs.jsonl` and `snapshots.jsonl` are newline-delimited JSON. Each line
|
||||||
|
is one tick. Read the last 10-20 lines first to focus on the moments before
|
||||||
|
the report was filed.
|
||||||
|
- The `render.txt` is a text-art representation of the game view. Entity
|
||||||
|
positions in the render should match positions in the snapshot. Mismatches
|
||||||
|
are themselves a bug signal (rendering vs simulation desync).
|
||||||
|
- The seed in `seed.txt` combined with `inputs.jsonl` enables deterministic
|
||||||
|
replay on the server. Note this in any ticket you create.
|
||||||
|
- If the bug-reports directory does not exist, the tester has not yet run any
|
||||||
|
gauntlet sessions or has not filed any reports. This is not an error.
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
---
|
||||||
|
name: d2-diagram
|
||||||
|
description: >
|
||||||
|
Generate technical diagrams using d2 (text-to-diagram CLI). Use when the
|
||||||
|
user says "create a diagram", "draw architecture", "make a flowchart",
|
||||||
|
"diagram this", "render d2", "d2", "data flow diagram", "entity relationship",
|
||||||
|
"state machine", "sequence diagram", "UI flow", or invokes /d2-diagram.
|
||||||
|
Produces .d2 source files and renders them to PNG. Also use when asked
|
||||||
|
to update, re-render, or batch render existing diagrams.
|
||||||
|
---
|
||||||
|
|
||||||
|
# d2 Diagram Generation
|
||||||
|
|
||||||
|
Generate technical diagrams from text using d2 (v0.7.1). Pure CLI, no
|
||||||
|
external dependencies beyond the d2 binary.
|
||||||
|
|
||||||
|
**Binary:** `/home/linuxbrew/.linuxbrew/bin/d2`
|
||||||
|
|
||||||
|
## Project Defaults
|
||||||
|
|
||||||
|
| Setting | Value | Override |
|
||||||
|
|---------|-------|----------|
|
||||||
|
| Theme | 200 (Dark Mauve) | `--theme N` |
|
||||||
|
| Layout | dagre | `--layout elk` |
|
||||||
|
| Padding | 100px | — |
|
||||||
|
| Format | PNG | `--svg` |
|
||||||
|
|
||||||
|
## Output Convention
|
||||||
|
|
||||||
|
```
|
||||||
|
docs/diagrams/
|
||||||
|
architecture/ # System architecture, IPC, component layout
|
||||||
|
data-flow/ # Sequence diagrams, data pipelines
|
||||||
|
entity/ # ER diagrams, ECS component schemas
|
||||||
|
state/ # State machines, behavior trees
|
||||||
|
ui/ # UI navigation flow, screen transitions
|
||||||
|
```
|
||||||
|
|
||||||
|
Both `.d2` source and `.png` output are tracked in git.
|
||||||
|
|
||||||
|
## Single Diagram Workflow
|
||||||
|
|
||||||
|
1. **Determine category** — architecture, data-flow, entity, state, or ui
|
||||||
|
2. **Read template** — `references/diagram-templates.md` for the matching category
|
||||||
|
3. **Read syntax** — `references/d2-syntax-guide.md` if unfamiliar with d2 syntax
|
||||||
|
4. **Write .d2 source** — to `docs/diagrams/{category}/{name}.d2`
|
||||||
|
5. **Validate** — `.claude/skills/d2-diagram/scripts/d2-render.sh validate {file}`
|
||||||
|
6. **Render** — `.claude/skills/d2-diagram/scripts/d2-render.sh {file}`
|
||||||
|
7. **Read SVG** — verify the output, present to user
|
||||||
|
|
||||||
|
### Script Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Render with project defaults
|
||||||
|
.claude/skills/d2-diagram/scripts/d2-render.sh docs/diagrams/architecture/ipc-bridge.d2
|
||||||
|
|
||||||
|
# Validate syntax only
|
||||||
|
.claude/skills/d2-diagram/scripts/d2-render.sh validate docs/diagrams/architecture/ipc-bridge.d2
|
||||||
|
|
||||||
|
# Auto-format source
|
||||||
|
.claude/skills/d2-diagram/scripts/d2-render.sh fmt docs/diagrams/architecture/ipc-bridge.d2
|
||||||
|
|
||||||
|
# Sketch mode (hand-drawn look for drafts)
|
||||||
|
.claude/skills/d2-diagram/scripts/d2-render.sh docs/diagrams/ui/flow.d2 --sketch
|
||||||
|
|
||||||
|
# Light theme (for printable docs)
|
||||||
|
.claude/skills/d2-diagram/scripts/d2-render.sh docs/diagrams/entity/schema.d2 --theme 0
|
||||||
|
|
||||||
|
# SVG output (if specifically needed)
|
||||||
|
.claude/skills/d2-diagram/scripts/d2-render.sh docs/diagrams/architecture/overview.d2 --svg
|
||||||
|
```
|
||||||
|
|
||||||
|
## Batch Render
|
||||||
|
|
||||||
|
Re-render all diagrams after theme or style changes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# All diagrams
|
||||||
|
.claude/skills/d2-diagram/scripts/d2-batch.sh
|
||||||
|
|
||||||
|
# One category
|
||||||
|
.claude/skills/d2-diagram/scripts/d2-batch.sh docs/diagrams/architecture/
|
||||||
|
|
||||||
|
# Preview what would render
|
||||||
|
.claude/skills/d2-diagram/scripts/d2-batch.sh --dry-run
|
||||||
|
|
||||||
|
# Force re-render everything
|
||||||
|
.claude/skills/d2-diagram/scripts/d2-batch.sh --force
|
||||||
|
```
|
||||||
|
|
||||||
|
Batch skips files whose PNG is newer than the `.d2` source unless `--force`.
|
||||||
|
|
||||||
|
## Advanced Patterns
|
||||||
|
|
||||||
|
### Variables for consistent styling
|
||||||
|
|
||||||
|
```d2
|
||||||
|
vars: {
|
||||||
|
color-bg: "#2a3040"
|
||||||
|
color-stroke: "#333340"
|
||||||
|
color-text: "#c8d0e0"
|
||||||
|
color-accent: "#c8d8f0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multi-board (layers)
|
||||||
|
|
||||||
|
```d2
|
||||||
|
# Base diagram here
|
||||||
|
|
||||||
|
layers: {
|
||||||
|
detailed: {
|
||||||
|
# More detailed view
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sequence diagrams
|
||||||
|
|
||||||
|
```d2
|
||||||
|
shape: sequence_diagram
|
||||||
|
client: Godot Client
|
||||||
|
server: Rust Server
|
||||||
|
|
||||||
|
client -> server: TickRequest(delta)
|
||||||
|
server -> client: WorldState(entities)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Imports
|
||||||
|
|
||||||
|
Split shared definitions into a separate file and import:
|
||||||
|
|
||||||
|
```d2
|
||||||
|
...@shared-defs.d2
|
||||||
|
```
|
||||||
|
|
||||||
|
## Agent Guidance
|
||||||
|
|
||||||
|
- **Qatux** — Architecture decision records, system overview diagrams, data
|
||||||
|
schemas. Prefer architecture and entity templates.
|
||||||
|
- **Tyre** — IPC bridge, ECS system flow, chunk loading pipeline, perception
|
||||||
|
system data flow. Prefer architecture and data-flow templates.
|
||||||
|
- **Araminta** — UI navigation flow, screen transitions, component hierarchy.
|
||||||
|
Prefer UI flow template.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- `references/d2-syntax-guide.md` — Language quick reference (shapes, edges,
|
||||||
|
containers, styling, variables). Read when unfamiliar with d2 syntax.
|
||||||
|
- `references/diagram-templates.md` — Five category templates with complete
|
||||||
|
d2 source examples. Read when starting a new diagram.
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
# D2 Language Quick Reference
|
||||||
|
|
||||||
|
## Nodes
|
||||||
|
|
||||||
|
```d2
|
||||||
|
server # Implicit label from key
|
||||||
|
server: Simulation Server # Explicit label
|
||||||
|
server: Simulation Server { # With properties
|
||||||
|
shape: hexagon
|
||||||
|
style.fill: "#2d3436"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Edges
|
||||||
|
|
||||||
|
```d2
|
||||||
|
a -> b # Directed
|
||||||
|
a <- b # Reverse directed
|
||||||
|
a <-> b # Bidirectional
|
||||||
|
a -- b # Undirected
|
||||||
|
a -> b: "label" # Labeled edge
|
||||||
|
a -> b -> c # Chained
|
||||||
|
```
|
||||||
|
|
||||||
|
## Containers (nesting)
|
||||||
|
|
||||||
|
```d2
|
||||||
|
infrastructure: {
|
||||||
|
server: Simulation Server
|
||||||
|
database: State Store {
|
||||||
|
shape: cylinder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Shapes
|
||||||
|
|
||||||
|
| Shape | Use for |
|
||||||
|
|-------|---------|
|
||||||
|
| `rectangle` | Default. Components, modules, generic. |
|
||||||
|
| `hexagon` | Systems, services, major components. |
|
||||||
|
| `cylinder` | Databases, storage, persistent state. |
|
||||||
|
| `diamond` | Decisions, conditions, branch points. |
|
||||||
|
| `oval` / `circle` | Start/end states, events. |
|
||||||
|
| `cloud` | External systems, networks. |
|
||||||
|
| `person` | Actors, users, NPCs. |
|
||||||
|
| `queue` | Message queues, buffers. |
|
||||||
|
| `page` | Documents, files. |
|
||||||
|
| `package` | Packages, modules, crates. |
|
||||||
|
| `sql_table` | Database tables, ECS component schemas. |
|
||||||
|
| `class` | Class diagrams, ECS system definitions. |
|
||||||
|
| `code` | Code blocks (set `language` property). |
|
||||||
|
| `markdown` | Rich text blocks. |
|
||||||
|
|
||||||
|
## SQL Tables
|
||||||
|
|
||||||
|
```d2
|
||||||
|
entity: {
|
||||||
|
shape: sql_table
|
||||||
|
id: u64 {constraint: primary_key}
|
||||||
|
name: String
|
||||||
|
position: Vec2
|
||||||
|
faction_id: u64 {constraint: foreign_key}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Class Diagrams
|
||||||
|
|
||||||
|
```d2
|
||||||
|
perception_system: {
|
||||||
|
shape: class
|
||||||
|
+run(world: &mut World)
|
||||||
|
-calculate_los(entity: Entity): HashSet<Entity>
|
||||||
|
#update_knowledge(entity: Entity, seen: HashSet<Entity>)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sequence Diagrams
|
||||||
|
|
||||||
|
```d2
|
||||||
|
shape: sequence_diagram
|
||||||
|
client: Godot Client
|
||||||
|
server: Rust Server
|
||||||
|
|
||||||
|
client -> server: TickRequest(delta)
|
||||||
|
server -> server: run ECS systems
|
||||||
|
server -> client: WorldState(entities)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Styling
|
||||||
|
|
||||||
|
```d2
|
||||||
|
node: Label {
|
||||||
|
style: {
|
||||||
|
fill: "#2d3436"
|
||||||
|
stroke: "#333340"
|
||||||
|
stroke-width: 2
|
||||||
|
stroke-dash: 5 # Dashed line
|
||||||
|
opacity: 0.8
|
||||||
|
font-size: 14
|
||||||
|
font-color: "#c8d0e0"
|
||||||
|
bold: true
|
||||||
|
italic: false
|
||||||
|
border-radius: 4
|
||||||
|
shadow: true
|
||||||
|
3d: true # Rectangles only
|
||||||
|
multiple: true # Stacked appearance
|
||||||
|
double-border: true # Rectangles/ovals only
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Edge styling
|
||||||
|
|
||||||
|
```d2
|
||||||
|
a -> b: {
|
||||||
|
style: {
|
||||||
|
stroke: "#c8d8f0"
|
||||||
|
stroke-width: 2
|
||||||
|
stroke-dash: 5
|
||||||
|
opacity: 0.8
|
||||||
|
animated: true # Animated flow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Variables
|
||||||
|
|
||||||
|
```d2
|
||||||
|
vars: {
|
||||||
|
color-bg: "#1a1e24"
|
||||||
|
color-stroke: "#333340"
|
||||||
|
color-text: "#c8d0e0"
|
||||||
|
color-accent: "#c8d8f0"
|
||||||
|
}
|
||||||
|
|
||||||
|
node: {
|
||||||
|
style.fill: ${color-bg}
|
||||||
|
style.stroke: ${color-stroke}
|
||||||
|
style.font-color: ${color-text}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Direction
|
||||||
|
|
||||||
|
```d2
|
||||||
|
direction: right # left-to-right (default for dagre)
|
||||||
|
direction: down # top-to-bottom
|
||||||
|
direction: up
|
||||||
|
direction: left
|
||||||
|
```
|
||||||
|
|
||||||
|
## Imports
|
||||||
|
|
||||||
|
```d2
|
||||||
|
...@shared-defs.d2 # Spread import (inline all definitions)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Icons
|
||||||
|
|
||||||
|
```d2
|
||||||
|
node: Label {
|
||||||
|
icon: https://icons.terrastruct.com/essentials/time.svg
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Layers (multi-board)
|
||||||
|
|
||||||
|
```d2
|
||||||
|
# Base diagram content here
|
||||||
|
|
||||||
|
layers: {
|
||||||
|
detailed: {
|
||||||
|
# More detailed view
|
||||||
|
}
|
||||||
|
simplified: {
|
||||||
|
# Simplified overview
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scenarios (animated transitions)
|
||||||
|
|
||||||
|
```d2
|
||||||
|
# Base state
|
||||||
|
|
||||||
|
scenarios: {
|
||||||
|
alert: {
|
||||||
|
# Changes from base for alert state
|
||||||
|
}
|
||||||
|
combat: {
|
||||||
|
# Changes from base for combat state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Comments
|
||||||
|
|
||||||
|
```d2
|
||||||
|
# This is a comment
|
||||||
|
node: Label # Inline comment
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Colors (from visual-grammar-v01.md)
|
||||||
|
|
||||||
|
| Constant | Hex | Usage |
|
||||||
|
|----------|-----|-------|
|
||||||
|
| Zone 1 floor | `#1a1e24` | Dark backgrounds |
|
||||||
|
| Zone 1 wall | `#2a3040` | Component fill |
|
||||||
|
| Outline standard | `#333340` | Borders, strokes |
|
||||||
|
| Insert chrome | `#c8d0e0` | Text, labels |
|
||||||
|
| Zone 1 fixture | `#c8d8f0` | Accents, highlights |
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
# Diagram Templates
|
||||||
|
|
||||||
|
Copy, adapt, and render. Each template uses project colors from visual-grammar-v01.md.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Architecture Diagram
|
||||||
|
|
||||||
|
System components, relationships, communication channels.
|
||||||
|
|
||||||
|
**When to use:** IPC bridge, perception pipeline, chunk loading, ECS system layout, client-server architecture.
|
||||||
|
|
||||||
|
**Agents:** Tyre (system architecture), Qatux (architecture decision records).
|
||||||
|
|
||||||
|
```d2
|
||||||
|
vars: {
|
||||||
|
color-bg: "#2a3040"
|
||||||
|
color-stroke: "#333340"
|
||||||
|
color-text: "#c8d0e0"
|
||||||
|
color-accent: "#c8d8f0"
|
||||||
|
}
|
||||||
|
|
||||||
|
direction: right
|
||||||
|
|
||||||
|
client: Godot Client {
|
||||||
|
shape: hexagon
|
||||||
|
style.fill: ${color-bg}
|
||||||
|
style.font-color: ${color-text}
|
||||||
|
|
||||||
|
rendering: Rendering {
|
||||||
|
style.fill: ${color-bg}
|
||||||
|
style.font-color: ${color-text}
|
||||||
|
}
|
||||||
|
ui: UI Layer {
|
||||||
|
style.fill: ${color-bg}
|
||||||
|
style.font-color: ${color-text}
|
||||||
|
}
|
||||||
|
bridge: IPC Bridge {
|
||||||
|
style.fill: ${color-bg}
|
||||||
|
style.font-color: ${color-text}
|
||||||
|
style.stroke: ${color-accent}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server: Rust Server {
|
||||||
|
shape: hexagon
|
||||||
|
style.fill: ${color-bg}
|
||||||
|
style.font-color: ${color-text}
|
||||||
|
|
||||||
|
ecs: bevy_ecs {
|
||||||
|
style.fill: ${color-bg}
|
||||||
|
style.font-color: ${color-text}
|
||||||
|
}
|
||||||
|
perception: Perception {
|
||||||
|
style.fill: ${color-bg}
|
||||||
|
style.font-color: ${color-text}
|
||||||
|
}
|
||||||
|
bridge: IPC Bridge {
|
||||||
|
style.fill: ${color-bg}
|
||||||
|
style.font-color: ${color-text}
|
||||||
|
style.stroke: ${color-accent}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client.bridge -> server.bridge: "stdin/stdout" {
|
||||||
|
style.stroke: ${color-accent}
|
||||||
|
style.stroke-dash: 5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Entity Relationship
|
||||||
|
|
||||||
|
Data schemas, ECS components, knowledge graph structure.
|
||||||
|
|
||||||
|
**When to use:** Database tables, component definitions, entity relationships, knowledge store schema.
|
||||||
|
|
||||||
|
**Agents:** Tyre (ECS component design), Qatux (schema documentation).
|
||||||
|
|
||||||
|
```d2
|
||||||
|
entity: Entity {
|
||||||
|
shape: sql_table
|
||||||
|
id: u64 {constraint: primary_key}
|
||||||
|
name: String
|
||||||
|
faction_id: u64 {constraint: foreign_key}
|
||||||
|
}
|
||||||
|
|
||||||
|
position: Position {
|
||||||
|
shape: sql_table
|
||||||
|
entity_id: u64 {constraint: foreign_key}
|
||||||
|
x: f32
|
||||||
|
y: f32
|
||||||
|
chunk_id: u32
|
||||||
|
}
|
||||||
|
|
||||||
|
knowledge: KnowledgeEntry {
|
||||||
|
shape: sql_table
|
||||||
|
observer_id: u64 {constraint: foreign_key}
|
||||||
|
subject_id: u64 {constraint: foreign_key}
|
||||||
|
fact_type: FactType
|
||||||
|
confidence: f32
|
||||||
|
last_seen_tick: u64
|
||||||
|
}
|
||||||
|
|
||||||
|
entity.id -> position.entity_id
|
||||||
|
entity.id -> knowledge.observer_id
|
||||||
|
entity.id -> knowledge.subject_id
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Sequence / Data Flow
|
||||||
|
|
||||||
|
Ordered interactions between systems over time.
|
||||||
|
|
||||||
|
**When to use:** IPC message flow, tick processing, perception update cycle, dialogue system exchanges.
|
||||||
|
|
||||||
|
**Agents:** Tyre (system interaction design), Qatux (protocol documentation).
|
||||||
|
|
||||||
|
```d2
|
||||||
|
shape: sequence_diagram
|
||||||
|
|
||||||
|
client: Godot Client
|
||||||
|
bridge: IPC Bridge
|
||||||
|
server: Rust Server
|
||||||
|
ecs: ECS Systems
|
||||||
|
|
||||||
|
client -> bridge: TickRequest(delta, input)
|
||||||
|
bridge -> server: deserialize + dispatch
|
||||||
|
server -> ecs: run_systems(delta)
|
||||||
|
ecs -> ecs: perception, AI, physics
|
||||||
|
ecs -> server: collect WorldState
|
||||||
|
server -> bridge: serialize WorldState
|
||||||
|
bridge -> client: WorldState(entities, events)
|
||||||
|
client -> client: update rendering
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. State Machine
|
||||||
|
|
||||||
|
Entity states, transitions, conditions.
|
||||||
|
|
||||||
|
**When to use:** NPC behavior states, game mode transitions, dialogue state, investigation phases.
|
||||||
|
|
||||||
|
**Agents:** Tyre (behavior system design), Qatux (state documentation).
|
||||||
|
|
||||||
|
```d2
|
||||||
|
vars: {
|
||||||
|
color-state: "#2a3040"
|
||||||
|
color-text: "#c8d0e0"
|
||||||
|
color-edge: "#c8d8f0"
|
||||||
|
color-decision: "#333340"
|
||||||
|
}
|
||||||
|
|
||||||
|
idle: Idle {
|
||||||
|
style.fill: ${color-state}
|
||||||
|
style.font-color: ${color-text}
|
||||||
|
}
|
||||||
|
|
||||||
|
alert: Alert {
|
||||||
|
style.fill: ${color-state}
|
||||||
|
style.font-color: ${color-text}
|
||||||
|
}
|
||||||
|
|
||||||
|
investigate: Investigate {
|
||||||
|
style.fill: ${color-state}
|
||||||
|
style.font-color: ${color-text}
|
||||||
|
}
|
||||||
|
|
||||||
|
combat: Combat {
|
||||||
|
style.fill: ${color-state}
|
||||||
|
style.font-color: ${color-text}
|
||||||
|
style.stroke: "#f0b840"
|
||||||
|
}
|
||||||
|
|
||||||
|
flee: Flee {
|
||||||
|
style.fill: ${color-state}
|
||||||
|
style.font-color: ${color-text}
|
||||||
|
}
|
||||||
|
|
||||||
|
idle -> alert: "stimulus detected" { style.stroke: ${color-edge} }
|
||||||
|
alert -> investigate: "stimulus confirmed" { style.stroke: ${color-edge} }
|
||||||
|
alert -> idle: "timeout / stimulus lost" { style.stroke: ${color-edge}; style.stroke-dash: 5 }
|
||||||
|
investigate -> combat: "threat confirmed" { style.stroke: "#f0b840" }
|
||||||
|
investigate -> idle: "nothing found" { style.stroke: ${color-edge}; style.stroke-dash: 5 }
|
||||||
|
combat -> flee: "health < threshold" { style.stroke: "#f0b840" }
|
||||||
|
combat -> idle: "threat eliminated" { style.stroke: ${color-edge}; style.stroke-dash: 5 }
|
||||||
|
flee -> idle: "safe distance reached" { style.stroke: ${color-edge}; style.stroke-dash: 5 }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. UI Flow
|
||||||
|
|
||||||
|
Screen navigation, component hierarchy, interaction paths.
|
||||||
|
|
||||||
|
**When to use:** HUD layout relationships, menu navigation, dialogue flow, insert mode transitions.
|
||||||
|
|
||||||
|
**Agents:** Araminta (UI/visual design), Tyre (interface architecture), Qatux (UI documentation).
|
||||||
|
|
||||||
|
```d2
|
||||||
|
vars: {
|
||||||
|
color-screen: "#1a1e24"
|
||||||
|
color-panel: "#2a3040"
|
||||||
|
color-text: "#c8d0e0"
|
||||||
|
color-nav: "#c8d8f0"
|
||||||
|
}
|
||||||
|
|
||||||
|
gameplay: Gameplay {
|
||||||
|
style.fill: ${color-screen}
|
||||||
|
style.font-color: ${color-text}
|
||||||
|
|
||||||
|
hud: HUD {
|
||||||
|
style.fill: ${color-panel}
|
||||||
|
style.font-color: ${color-text}
|
||||||
|
|
||||||
|
minimap: Minimap
|
||||||
|
monologue: Monologue Panel
|
||||||
|
insert_display: Insert Display
|
||||||
|
}
|
||||||
|
|
||||||
|
world: World View {
|
||||||
|
style.fill: ${color-panel}
|
||||||
|
style.font-color: ${color-text}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pause: Pause Menu {
|
||||||
|
style.fill: ${color-screen}
|
||||||
|
style.font-color: ${color-text}
|
||||||
|
|
||||||
|
inventory: Inventory
|
||||||
|
journal: Journal
|
||||||
|
settings: Settings
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogue: Dialogue Mode {
|
||||||
|
style.fill: ${color-screen}
|
||||||
|
style.font-color: ${color-text}
|
||||||
|
|
||||||
|
speaker: Speaker Panel
|
||||||
|
responses: Response List
|
||||||
|
}
|
||||||
|
|
||||||
|
gameplay -> pause: "ESC" { style.stroke: ${color-nav} }
|
||||||
|
pause -> gameplay: "ESC / Resume" { style.stroke: ${color-nav}; style.stroke-dash: 5 }
|
||||||
|
gameplay -> dialogue: "interact with NPC" { style.stroke: ${color-nav} }
|
||||||
|
dialogue -> gameplay: "end conversation" { style.stroke: ${color-nav}; style.stroke-dash: 5 }
|
||||||
|
```
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR" && git rev-parse --show-toplevel)"
|
||||||
|
RENDER="$SCRIPT_DIR/d2-render.sh"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: $(basename "$0") [directory] [options]
|
||||||
|
|
||||||
|
Batch render all .d2 files in a directory. Skips files whose PNG is
|
||||||
|
newer than the source unless --force is used.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--dry-run List files that would be rendered
|
||||||
|
--force Re-render even if SVG is up to date
|
||||||
|
--theme N Override theme for all files
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
$(basename "$0") # All in docs/diagrams/
|
||||||
|
$(basename "$0") docs/diagrams/architecture/ # One category
|
||||||
|
$(basename "$0") --dry-run # Preview
|
||||||
|
$(basename "$0") --force # Re-render everything
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
DIR="$REPO_ROOT/docs/diagrams"
|
||||||
|
DRY_RUN=false
|
||||||
|
FORCE=false
|
||||||
|
EXTRA_ARGS=()
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--dry-run) DRY_RUN=true; shift ;;
|
||||||
|
--force) FORCE=true; shift ;;
|
||||||
|
--theme) EXTRA_ARGS+=(--theme "$2"); shift 2 ;;
|
||||||
|
--help|-h) usage ;;
|
||||||
|
*)
|
||||||
|
if [[ -d "$1" ]] || [[ -d "$REPO_ROOT/$1" ]]; then
|
||||||
|
DIR="$1"
|
||||||
|
[[ "$DIR" != /* ]] && DIR="$REPO_ROOT/$DIR"
|
||||||
|
else
|
||||||
|
echo "Unknown option or directory: $1" >&2; exit 1
|
||||||
|
fi
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ ! -d "$DIR" ]] && { echo "ERROR: Directory not found: $DIR" >&2; exit 1; }
|
||||||
|
|
||||||
|
RENDERED=0
|
||||||
|
SKIPPED=0
|
||||||
|
FAILED=0
|
||||||
|
|
||||||
|
while IFS= read -r -d '' d2_file; do
|
||||||
|
png_file="${d2_file%.d2}.png"
|
||||||
|
|
||||||
|
# Skip if PNG is newer than source (unless --force)
|
||||||
|
if [[ "$FORCE" != true ]] && [[ -f "$png_file" ]] && [[ "$png_file" -nt "$d2_file" ]]; then
|
||||||
|
SKIPPED=$((SKIPPED + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
rel_path="${d2_file#"$REPO_ROOT/"}"
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
echo "Would render: $rel_path"
|
||||||
|
RENDERED=$((RENDERED + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if "$RENDER" "$d2_file" "${EXTRA_ARGS[@]+"${EXTRA_ARGS[@]}"}"; then
|
||||||
|
RENDERED=$((RENDERED + 1))
|
||||||
|
else
|
||||||
|
echo "FAILED: $rel_path" >&2
|
||||||
|
FAILED=$((FAILED + 1))
|
||||||
|
fi
|
||||||
|
done < <(find "$DIR" -name '*.d2' -print0 | sort -z)
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Batch complete: $RENDERED rendered, $SKIPPED skipped (up to date), $FAILED failed"
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
D2="/home/linuxbrew/.linuxbrew/bin/d2"
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR" && git rev-parse --show-toplevel)"
|
||||||
|
|
||||||
|
DEFAULT_THEME=200
|
||||||
|
DEFAULT_LAYOUT="dagre"
|
||||||
|
DEFAULT_PAD=100
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: $(basename "$0") [validate|fmt] <file.d2> [options]
|
||||||
|
|
||||||
|
Render a .d2 file to PNG with project defaults (theme $DEFAULT_THEME, $DEFAULT_LAYOUT layout).
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
validate <file> Check syntax without rendering
|
||||||
|
fmt <file> Auto-format in place
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--theme N Override theme (default: $DEFAULT_THEME)
|
||||||
|
--layout NAME Override layout engine (default: $DEFAULT_LAYOUT)
|
||||||
|
--sketch Enable hand-drawn sketch mode
|
||||||
|
--output PATH Override output path (default: input with .png extension)
|
||||||
|
--svg Render to SVG instead of PNG
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
$(basename "$0") docs/diagrams/architecture/ipc-bridge.d2
|
||||||
|
$(basename "$0") validate docs/diagrams/architecture/ipc-bridge.d2
|
||||||
|
$(basename "$0") docs/diagrams/architecture/ipc-bridge.d2 --sketch --theme 0
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
[[ $# -lt 1 ]] && usage
|
||||||
|
|
||||||
|
# Parse subcommand
|
||||||
|
SUBCMD=""
|
||||||
|
case "$1" in
|
||||||
|
validate|fmt)
|
||||||
|
SUBCMD="$1"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
[[ $# -lt 1 ]] && usage
|
||||||
|
|
||||||
|
INPUT="$1"
|
||||||
|
shift
|
||||||
|
|
||||||
|
# Resolve to absolute path
|
||||||
|
[[ "$INPUT" != /* ]] && INPUT="$REPO_ROOT/$INPUT"
|
||||||
|
|
||||||
|
[[ ! -f "$INPUT" ]] && { echo "ERROR: File not found: $INPUT" >&2; exit 1; }
|
||||||
|
|
||||||
|
# Handle subcommands
|
||||||
|
if [[ -n "$SUBCMD" ]]; then
|
||||||
|
"$D2" "$SUBCMD" "$INPUT"
|
||||||
|
echo "OK: $SUBCMD $INPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Parse render options
|
||||||
|
THEME="$DEFAULT_THEME"
|
||||||
|
LAYOUT="$DEFAULT_LAYOUT"
|
||||||
|
SKETCH=""
|
||||||
|
OUTPUT=""
|
||||||
|
FORMAT="png"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--theme) THEME="$2"; shift 2 ;;
|
||||||
|
--layout) LAYOUT="$2"; shift 2 ;;
|
||||||
|
--sketch) SKETCH="-s"; shift ;;
|
||||||
|
--output) OUTPUT="$2"; shift 2 ;;
|
||||||
|
--svg) FORMAT="svg"; shift ;;
|
||||||
|
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Derive output path
|
||||||
|
if [[ -z "$OUTPUT" ]]; then
|
||||||
|
OUTPUT="${INPUT%.d2}.$FORMAT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Render
|
||||||
|
"$D2" -t "$THEME" -l "$LAYOUT" --pad "$DEFAULT_PAD" $SKETCH "$INPUT" "$OUTPUT"
|
||||||
|
|
||||||
|
SIZE=$(stat --printf="%s" "$OUTPUT" 2>/dev/null || stat -f%z "$OUTPUT" 2>/dev/null)
|
||||||
|
echo "Rendered: $OUTPUT ($(( SIZE / 1024 ))KB)"
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
---
|
||||||
|
name: frame0-wireframe
|
||||||
|
description: >
|
||||||
|
Create and export UI wireframes using Frame0 (local desktop wireframing
|
||||||
|
app with HTTP API). Use when the user says "create wireframe", "wireframe
|
||||||
|
this", "mock up the UI", "draw a screen", "UI layout", "wireframe the HUD",
|
||||||
|
"Frame0", "frame0", "export wireframe", or invokes /frame0-wireframe.
|
||||||
|
Wireframes are authored as local JSON files (source of truth) and synced
|
||||||
|
to Frame0 for rendering and export. Requires Frame0 to be running locally.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Frame0 Wireframe Generation
|
||||||
|
|
||||||
|
Create UI wireframes as JSON files, sync them to Frame0 for rendering, and
|
||||||
|
export as PNG. Local JSON is the source of truth — Frame0 is the renderer.
|
||||||
|
|
||||||
|
**Frame0 is a renderer, not a workspace.** Treat it as disposable output.
|
||||||
|
Push freely, delete test pages, keep it clean. Never pull from Frame0 unless
|
||||||
|
the user explicitly says they have made edits in Frame0 and want to import
|
||||||
|
them. The pull workflow exists for that case only — do not use it proactively.
|
||||||
|
|
||||||
|
**Prerequisite:** Frame0 desktop app must be running. If not available,
|
||||||
|
stop and inform the user. Point to `references/setup-guide.md`.
|
||||||
|
|
||||||
|
## Health Check
|
||||||
|
|
||||||
|
Always check first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.claude/skills/frame0-wireframe/scripts/frame0-cmd.sh health
|
||||||
|
```
|
||||||
|
|
||||||
|
## Core Workflow
|
||||||
|
|
||||||
|
1. **Health check** — verify Frame0 is running
|
||||||
|
2. **Write wireframe JSON** — to `docs/design/wireframes/{category}/{name}.json`
|
||||||
|
3. **Push to Frame0** — `frame0-sync.py push <file.json>`
|
||||||
|
4. **Export PNG** — `frame0-sync.py export <file.json> <output.png>`
|
||||||
|
5. **Clean up** — delete test/scratch pages from Frame0 when done
|
||||||
|
|
||||||
|
### Scripts
|
||||||
|
|
||||||
|
| Script | Purpose |
|
||||||
|
|--------|---------|
|
||||||
|
| `scripts/frame0-sync.py` | **Primary.** Push/pull/export wireframes between JSON and Frame0 |
|
||||||
|
| `scripts/frame0-cmd.sh` | Low-level API wrapper for ad-hoc commands |
|
||||||
|
|
||||||
|
## Wireframe JSON Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Dialogue Box",
|
||||||
|
"shapes": {
|
||||||
|
"panel": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"left": 170, "top": 500, "width": 800, "height": 260,
|
||||||
|
"fillColor": "#1a1e24",
|
||||||
|
"strokeColor": "#333340",
|
||||||
|
"corners": [8, 8, 8, 8]
|
||||||
|
},
|
||||||
|
"speaker": {
|
||||||
|
"type": "Text",
|
||||||
|
"parent": "panel",
|
||||||
|
"left": 190, "top": 520,
|
||||||
|
"text": "LERA KONSTANTIN",
|
||||||
|
"fontColor": "#c8d0e0",
|
||||||
|
"fontSize": 16
|
||||||
|
},
|
||||||
|
"btn-ask": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"parent": "panel",
|
||||||
|
"left": 190, "top": 670, "width": 370, "height": 30,
|
||||||
|
"fillColor": "#2a3040",
|
||||||
|
"strokeColor": "#c8d8f0",
|
||||||
|
"corners": [4, 4, 4, 4]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"connectors": {
|
||||||
|
"flow-1": {
|
||||||
|
"tailId": "panel",
|
||||||
|
"headId": "btn-ask",
|
||||||
|
"strokeColor": "#c8d8f0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key rules
|
||||||
|
|
||||||
|
- **Shape IDs are stable local IDs** you control (e.g. `"panel"`, `"btn-ask"`)
|
||||||
|
- **`parent`** references another local shape ID for nesting
|
||||||
|
- **`type`** uses create-API names: `Rectangle`, `Ellipse`, `Text`, `Line`
|
||||||
|
- **Colors** can be hex (`#2a3040`) or Frame0 theme tokens (`$slate6`)
|
||||||
|
- After a pull, Frame0 returns its native vocabulary (`Box` for Rectangle,
|
||||||
|
theme tokens for colors). The sync script handles the mapping transparently.
|
||||||
|
- The `.idmap.json` mapping file (gitignored) tracks local ID ↔ Frame0 ID
|
||||||
|
|
||||||
|
### Sync commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SYNC=".claude/skills/frame0-wireframe/scripts/frame0-sync.py"
|
||||||
|
|
||||||
|
# Push local JSON to Frame0 (clears page, recreates all shapes)
|
||||||
|
$SYNC push docs/design/wireframes/dialogue/dialogue-box.json
|
||||||
|
|
||||||
|
# Pull Frame0 page back to local JSON (preserves local IDs via mapping)
|
||||||
|
$SYNC pull "Dialogue Box" docs/design/wireframes/dialogue/dialogue-box.json
|
||||||
|
|
||||||
|
# Push + export as PNG in one step
|
||||||
|
$SYNC export docs/design/wireframes/dialogue/dialogue-box.json \
|
||||||
|
docs/design/wireframes/dialogue/dialogue-box.png
|
||||||
|
```
|
||||||
|
|
||||||
|
### Batch export
|
||||||
|
|
||||||
|
Use this when exporting multiple wireframes. It runs as a single Bash call,
|
||||||
|
avoiding repeated permission prompts.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
BATCH=".claude/skills/frame0-wireframe/scripts/frame0-export-batch.sh"
|
||||||
|
|
||||||
|
# Dry run first — shows full manifest, no Frame0 calls
|
||||||
|
$BATCH --dry-run
|
||||||
|
|
||||||
|
# Export everything (skips PNGs already newer than their JSON)
|
||||||
|
$BATCH
|
||||||
|
|
||||||
|
# Export one category only
|
||||||
|
$BATCH --category dialogue
|
||||||
|
|
||||||
|
# Force re-export of everything
|
||||||
|
$BATCH --force
|
||||||
|
```
|
||||||
|
|
||||||
|
**Always dry-run first, then get approval before running the live export.**
|
||||||
|
|
||||||
|
## Low-Level Commands
|
||||||
|
|
||||||
|
For ad-hoc operations or exec calls not covered by sync:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CMD=".claude/skills/frame0-wireframe/scripts/frame0-cmd.sh"
|
||||||
|
$CMD health
|
||||||
|
$CMD list-pages
|
||||||
|
$CMD current-page
|
||||||
|
$CMD get-page <page-id>
|
||||||
|
$CMD create-shape Rectangle '{"name":"btn","left":100,"top":100,"width":120,"height":36}'
|
||||||
|
$CMD create-connector <tail-id> <head-id>
|
||||||
|
$CMD move <shape-id> <dx> <dy>
|
||||||
|
$CMD export --format image/png
|
||||||
|
$CMD exec "view:fit-to-screen" '{}'
|
||||||
|
```
|
||||||
|
|
||||||
|
If you find yourself using `exec` for the same command repeatedly, flag it as
|
||||||
|
a candidate for a proper wrapper in `frame0-cmd.sh`.
|
||||||
|
|
||||||
|
## Project Styling Defaults
|
||||||
|
|
||||||
|
Colors from `docs/design/visual-grammar-v01.md`:
|
||||||
|
|
||||||
|
| Role | Hex | Frame0 token |
|
||||||
|
|------|-----|-------------|
|
||||||
|
| Background | `#1a1e24` | `$sage3` |
|
||||||
|
| Stroke | `#333340` | `$slate6` |
|
||||||
|
| Fill | `#2a3040` | `$slate5` |
|
||||||
|
| Text | `#c8d0e0` | `$mint12` |
|
||||||
|
| Accent | `#c8d8f0` | `$blue12` |
|
||||||
|
|
||||||
|
Use hex when authoring new wireframes. Frame0 maps them to theme tokens on push.
|
||||||
|
|
||||||
|
## Output Convention
|
||||||
|
|
||||||
|
```
|
||||||
|
docs/design/wireframes/
|
||||||
|
hud/ # HUD layout wireframes
|
||||||
|
menus/ # Menu screen wireframes
|
||||||
|
dialogue/ # Dialogue box wireframes
|
||||||
|
insert/ # Neural insert wireframes
|
||||||
|
```
|
||||||
|
|
||||||
|
Both `.json` source and `.png` exports are tracked in git.
|
||||||
|
`.idmap.json` mapping files are gitignored.
|
||||||
|
|
||||||
|
## Agent Guidance
|
||||||
|
|
||||||
|
- **Araminta** — Primary user. Full wireframe creation, layout iteration,
|
||||||
|
visual consistency. Uses all component library patterns.
|
||||||
|
- **Tyre** — Interface architecture wireframes. System boundary diagrams.
|
||||||
|
- **Qatux** — Export wireframes for UI decision records and documentation.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- `references/component-library.md` — Pre-built JSON wireframe templates
|
||||||
|
(HUD, dialogue, menus, modals, lists, inventory). Copy and adapt.
|
||||||
|
- `references/api-reference.md` — Frame0 HTTP API command reference and
|
||||||
|
type mappings. Read for low-level control.
|
||||||
|
- `references/setup-guide.md` — Frame0 installation and startup for Fedora.
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
# Frame0 HTTP API Reference
|
||||||
|
|
||||||
|
Frame0 exposes a local HTTP API when the desktop app is running.
|
||||||
|
|
||||||
|
## Endpoint
|
||||||
|
|
||||||
|
```
|
||||||
|
POST http://localhost:{port}/execute_command
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
Default port: **58320** (override via `FRAME0_PORT` env var or `--port` flag).
|
||||||
|
|
||||||
|
## Request / Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"command": "namespace:action", "args": { ... }}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"success": true, "data": { ... }}
|
||||||
|
{"success": false, "error": "description"}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Type Mapping
|
||||||
|
|
||||||
|
Frame0 uses different type names for create vs get:
|
||||||
|
|
||||||
|
| Create API (`type`) | Get API (internal) | Description |
|
||||||
|
|--------------------|--------------------|-------------|
|
||||||
|
| `Rectangle` | `Box` | Rectangle with optional corners |
|
||||||
|
| `Ellipse` | `Ellipse` | Circle/ellipse |
|
||||||
|
| `Text` | `Text` | Text label |
|
||||||
|
| `Line` | `Line` | Line/polyline |
|
||||||
|
| `Frame` | `Frame` | Container from library |
|
||||||
|
| `Freehand` | `Freehand` | Freehand drawing |
|
||||||
|
| `Highlighter` | `Highlighter` | Highlighter stroke |
|
||||||
|
|
||||||
|
The sync script handles this mapping transparently.
|
||||||
|
|
||||||
|
## Color Tokens
|
||||||
|
|
||||||
|
Frame0 maps hex colors to theme tokens on creation (`convertColors: true`):
|
||||||
|
|
||||||
|
| Hex | Token | Role |
|
||||||
|
|-----|-------|------|
|
||||||
|
| `#1a1e24` | `$sage3` | Background |
|
||||||
|
| `#2a3040` | `$slate5` | Fill |
|
||||||
|
| `#333340` | `$slate6` | Stroke |
|
||||||
|
| `#c8d0e0` | `$mint12` | Text |
|
||||||
|
| `#c8d8f0` | `$blue12` | Accent |
|
||||||
|
|
||||||
|
Both hex and token strings work in the API. Tokens are preserved on round-trip.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### shape:create-shape
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"command": "shape:create-shape",
|
||||||
|
"args": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"shapeProps": {
|
||||||
|
"name": "my-button",
|
||||||
|
"left": 100, "top": 200, "width": 120, "height": 36,
|
||||||
|
"fillColor": "#2a3040",
|
||||||
|
"strokeColor": "#c8d8f0",
|
||||||
|
"corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"parentId": "optional-parent-shape-id",
|
||||||
|
"convertColors": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns: shape ID (string).
|
||||||
|
|
||||||
|
### shape:get-shape
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"command": "shape:get-shape", "args": {"shapeId": "id"}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### shape:update-shape
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"command": "shape:update-shape",
|
||||||
|
"args": {
|
||||||
|
"shapeId": "id",
|
||||||
|
"shapeProps": {"fillColor": "#1a1e24", "text": "Updated"},
|
||||||
|
"convertColors": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### shape:move
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"command": "shape:move", "args": {"shapeId": "id", "dx": 50, "dy": -20}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### shape:create-connector
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"command": "shape:create-connector",
|
||||||
|
"args": {
|
||||||
|
"tailId": "source-id",
|
||||||
|
"headId": "target-id",
|
||||||
|
"shapeProps": {"strokeColor": "#c8d8f0"},
|
||||||
|
"convertColors": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### shape:create-icon
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"command": "shape:create-icon",
|
||||||
|
"args": {
|
||||||
|
"iconName": "search",
|
||||||
|
"shapeProps": {"left": 100, "top": 100, "width": 24, "height": 24}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### shape:get-available-icons
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"command": "shape:get-available-icons", "args": {}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### shape:group / shape:ungroup
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"command": "shape:group", "args": {"shapeIdArray": ["id1", "id2"]}}
|
||||||
|
{"command": "shape:ungroup", "args": {"shapeIdArray": ["group-id"]}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### edit:delete / edit:duplicate
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"command": "edit:delete", "args": {"shapeIdArray": ["id1", "id2"]}}
|
||||||
|
{"command": "edit:duplicate", "args": {"shapeIdArray": ["id"], "dx": 20, "dy": 0}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### page:add
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"command": "page:add", "args": {"pageProps": {"name": "Page Name"}}}
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns: `{id, type, name}`.
|
||||||
|
|
||||||
|
### page:get
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"command": "page:get", "args": {"pageId": "id", "exportShapes": true}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### page:get-current-page
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"command": "page:get-current-page", "args": {}}
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns: page ID string.
|
||||||
|
|
||||||
|
### page:set-current-page
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"command": "page:set-current-page", "args": {"pageId": "id"}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### doc:get (list all pages)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"command": "doc:get", "args": {"exportPages": true, "exportShapes": false}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### page:delete
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"command": "page:delete", "args": {"pageId": "id"}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### file:export-image
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"command": "file:export-image",
|
||||||
|
"args": {
|
||||||
|
"pageId": "optional-page-id",
|
||||||
|
"format": "image/png",
|
||||||
|
"fillBackground": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Formats: `image/png`, `image/jpeg`, `image/webp`, `image/svg+xml`.
|
||||||
|
Returns: base64-encoded image data.
|
||||||
|
|
||||||
|
### view:fit-to-screen
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"command": "view:fit-to-screen", "args": {}}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Shape Properties
|
||||||
|
|
||||||
|
| Property | Type | Notes |
|
||||||
|
|----------|------|-------|
|
||||||
|
| `name` | string | Shape identifier/label |
|
||||||
|
| `left` | number | X position (origin: top-left) |
|
||||||
|
| `top` | number | Y position |
|
||||||
|
| `width` | number | Width in pixels |
|
||||||
|
| `height` | number | Height in pixels |
|
||||||
|
| `fillColor` | string | Hex or `$token` |
|
||||||
|
| `strokeColor` | string | Hex or `$token` |
|
||||||
|
| `strokeWidth` | number | Border width |
|
||||||
|
| `fontColor` | string | Text color (hex or `$token`) |
|
||||||
|
| `fontSize` | number | Font size in pixels |
|
||||||
|
| `fontFamily` | string | Font name (Frame0 default: `Loranthus`) |
|
||||||
|
| `text` | string | Text content |
|
||||||
|
| `wordWrap` | boolean | Enable word wrapping |
|
||||||
|
| `corners` | number[4] | Border radius [TL, TR, BR, BL] |
|
||||||
|
| `roughness` | number | Sketch roughness (Frame0 default: 1) |
|
||||||
|
| `constraints` | array | Auto-sizing constraints |
|
||||||
|
| `horzAlign` | string | Horizontal text alignment |
|
||||||
|
| `vertAlign` | string | Vertical text alignment |
|
||||||
|
| `fillStyle` | string | Fill style (`none` for transparent) |
|
||||||
|
| `path` | array | Coordinate pairs for lines |
|
||||||
@@ -0,0 +1,467 @@
|
|||||||
|
# Component Library
|
||||||
|
|
||||||
|
Pre-built wireframe JSON templates for The Settled Reach UI. Copy the JSON,
|
||||||
|
adapt positions/sizes, save to `docs/design/wireframes/{category}/`, and push.
|
||||||
|
|
||||||
|
**Viewport:** 1140x780 (Godot project settings)
|
||||||
|
**Grid unit:** 8px
|
||||||
|
**Min touch target:** 36px height
|
||||||
|
**Font sizes:** 12 (label), 14 (body), 16 (subtitle), 18 (heading), 24 (title)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. HUD Layout
|
||||||
|
|
||||||
|
Main gameplay overlay. Minimap top-right, monologue bottom-center,
|
||||||
|
insert display bottom-left, action hints bottom-right.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "HUD Layout",
|
||||||
|
"shapes": {
|
||||||
|
"minimap": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"left": 880, "top": 20, "width": 240, "height": 240,
|
||||||
|
"fillColor": "#1a1e24", "strokeColor": "#333340",
|
||||||
|
"corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"minimap-label": {
|
||||||
|
"type": "Text",
|
||||||
|
"parent": "minimap",
|
||||||
|
"left": 890, "top": 30,
|
||||||
|
"text": "Minimap",
|
||||||
|
"fontColor": "#c8d0e0", "fontSize": 14
|
||||||
|
},
|
||||||
|
"monologue": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"left": 300, "top": 680, "width": 520, "height": 80,
|
||||||
|
"fillColor": "#1a1e24", "strokeColor": "#333340",
|
||||||
|
"corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"monologue-text": {
|
||||||
|
"type": "Text",
|
||||||
|
"parent": "monologue",
|
||||||
|
"left": 310, "top": 700, "width": 500,
|
||||||
|
"text": "Internal monologue text appears here...",
|
||||||
|
"fontColor": "#c8d0e0", "fontSize": 13, "wordWrap": true
|
||||||
|
},
|
||||||
|
"insert": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"left": 20, "top": 600, "width": 260, "height": 160,
|
||||||
|
"fillColor": "#1a1e24", "strokeColor": "#333340",
|
||||||
|
"corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"insert-label": {
|
||||||
|
"type": "Text",
|
||||||
|
"parent": "insert",
|
||||||
|
"left": 30, "top": 620,
|
||||||
|
"text": "Neural Insert Data",
|
||||||
|
"fontColor": "#c8d0e0", "fontSize": 14
|
||||||
|
},
|
||||||
|
"hints": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"left": 880, "top": 700, "width": 240, "height": 60,
|
||||||
|
"fillColor": "#1a1e24", "strokeColor": "#333340",
|
||||||
|
"corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"hints-label": {
|
||||||
|
"type": "Text",
|
||||||
|
"parent": "hints",
|
||||||
|
"left": 890, "top": 720,
|
||||||
|
"text": "[E] Interact [TAB] Insert",
|
||||||
|
"fontColor": "#c8d0e0", "fontSize": 12
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Dialogue Box
|
||||||
|
|
||||||
|
Speaker panel with response options. Anchored bottom-center during dialogue mode.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Dialogue Box",
|
||||||
|
"shapes": {
|
||||||
|
"panel": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"left": 170, "top": 500, "width": 800, "height": 260,
|
||||||
|
"fillColor": "#1a1e24", "strokeColor": "#333340",
|
||||||
|
"corners": [8, 8, 8, 8]
|
||||||
|
},
|
||||||
|
"speaker": {
|
||||||
|
"type": "Text",
|
||||||
|
"parent": "panel",
|
||||||
|
"left": 190, "top": 520,
|
||||||
|
"text": "LERA KONSTANTIN",
|
||||||
|
"fontColor": "#c8d0e0", "fontSize": 16
|
||||||
|
},
|
||||||
|
"text-area": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"parent": "panel",
|
||||||
|
"left": 190, "top": 550, "width": 760, "height": 100,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#333340",
|
||||||
|
"corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"dialogue-text": {
|
||||||
|
"type": "Text",
|
||||||
|
"parent": "text-area",
|
||||||
|
"left": 200, "top": 560, "width": 740,
|
||||||
|
"text": "You look like you could use a drink. First time on the station?",
|
||||||
|
"fontColor": "#c8d0e0", "fontSize": 14, "wordWrap": true
|
||||||
|
},
|
||||||
|
"btn-option1": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"parent": "panel",
|
||||||
|
"left": 190, "top": 670, "width": 370, "height": 30,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#c8d8f0",
|
||||||
|
"corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"btn-option1-label": {
|
||||||
|
"type": "Text",
|
||||||
|
"parent": "btn-option1",
|
||||||
|
"left": 200, "top": 674,
|
||||||
|
"text": "[1] Ask about the station",
|
||||||
|
"fontColor": "#c8d8f0", "fontSize": 12
|
||||||
|
},
|
||||||
|
"btn-option2": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"parent": "panel",
|
||||||
|
"left": 190, "top": 710, "width": 370, "height": 30,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#c8d8f0",
|
||||||
|
"corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"btn-option2-label": {
|
||||||
|
"type": "Text",
|
||||||
|
"parent": "btn-option2",
|
||||||
|
"left": 200, "top": 714,
|
||||||
|
"text": "[2] Ask about recent events",
|
||||||
|
"fontColor": "#c8d8f0", "fontSize": 12
|
||||||
|
},
|
||||||
|
"btn-leave": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"parent": "panel",
|
||||||
|
"left": 580, "top": 670, "width": 180, "height": 30,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#c8d8f0",
|
||||||
|
"corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"btn-leave-label": {
|
||||||
|
"type": "Text",
|
||||||
|
"parent": "btn-leave",
|
||||||
|
"left": 590, "top": 674,
|
||||||
|
"text": "[3] Leave",
|
||||||
|
"fontColor": "#c8d8f0", "fontSize": 12
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Menu Screen
|
||||||
|
|
||||||
|
Full-screen menu with sidebar navigation and content area.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Pause Menu",
|
||||||
|
"shapes": {
|
||||||
|
"bg": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"left": 0, "top": 0, "width": 1140, "height": 780,
|
||||||
|
"fillColor": "#1a1e24"
|
||||||
|
},
|
||||||
|
"nav": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"parent": "bg",
|
||||||
|
"left": 20, "top": 20, "width": 200, "height": 740,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#333340",
|
||||||
|
"corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"btn-inventory": {
|
||||||
|
"type": "Rectangle", "parent": "nav",
|
||||||
|
"left": 30, "top": 40, "width": 180, "height": 36,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"btn-inventory-label": {
|
||||||
|
"type": "Text", "parent": "btn-inventory",
|
||||||
|
"left": 40, "top": 48, "text": "Inventory",
|
||||||
|
"fontColor": "#c8d8f0", "fontSize": 14
|
||||||
|
},
|
||||||
|
"btn-journal": {
|
||||||
|
"type": "Rectangle", "parent": "nav",
|
||||||
|
"left": 30, "top": 86, "width": 180, "height": 36,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"btn-journal-label": {
|
||||||
|
"type": "Text", "parent": "btn-journal",
|
||||||
|
"left": 40, "top": 94, "text": "Journal",
|
||||||
|
"fontColor": "#c8d8f0", "fontSize": 14
|
||||||
|
},
|
||||||
|
"btn-map": {
|
||||||
|
"type": "Rectangle", "parent": "nav",
|
||||||
|
"left": 30, "top": 132, "width": 180, "height": 36,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"btn-map-label": {
|
||||||
|
"type": "Text", "parent": "btn-map",
|
||||||
|
"left": 40, "top": 140, "text": "Map",
|
||||||
|
"fontColor": "#c8d8f0", "fontSize": 14
|
||||||
|
},
|
||||||
|
"btn-settings": {
|
||||||
|
"type": "Rectangle", "parent": "nav",
|
||||||
|
"left": 30, "top": 178, "width": 180, "height": 36,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"btn-settings-label": {
|
||||||
|
"type": "Text", "parent": "btn-settings",
|
||||||
|
"left": 40, "top": 186, "text": "Settings",
|
||||||
|
"fontColor": "#c8d8f0", "fontSize": 14
|
||||||
|
},
|
||||||
|
"btn-resume": {
|
||||||
|
"type": "Rectangle", "parent": "nav",
|
||||||
|
"left": 30, "top": 720, "width": 180, "height": 36,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"btn-resume-label": {
|
||||||
|
"type": "Text", "parent": "btn-resume",
|
||||||
|
"left": 40, "top": 728, "text": "Resume",
|
||||||
|
"fontColor": "#c8d8f0", "fontSize": 14
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"parent": "bg",
|
||||||
|
"left": 240, "top": 20, "width": 880, "height": 740,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#333340",
|
||||||
|
"corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"content-label": {
|
||||||
|
"type": "Text", "parent": "content",
|
||||||
|
"left": 260, "top": 40,
|
||||||
|
"text": "Content area",
|
||||||
|
"fontColor": "#c8d0e0", "fontSize": 14
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Modal Dialog
|
||||||
|
|
||||||
|
Centered overlay for confirmations, alerts, choices.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Modal Dialog",
|
||||||
|
"shapes": {
|
||||||
|
"overlay": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"left": 0, "top": 0, "width": 1140, "height": 780,
|
||||||
|
"fillColor": "#0a0c10"
|
||||||
|
},
|
||||||
|
"modal": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"parent": "overlay",
|
||||||
|
"left": 320, "top": 240, "width": 500, "height": 300,
|
||||||
|
"fillColor": "#1a1e24", "strokeColor": "#333340",
|
||||||
|
"corners": [8, 8, 8, 8]
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "Text", "parent": "modal",
|
||||||
|
"left": 340, "top": 260,
|
||||||
|
"text": "Confirm Action",
|
||||||
|
"fontColor": "#c8d0e0", "fontSize": 18
|
||||||
|
},
|
||||||
|
"divider": {
|
||||||
|
"type": "Line", "parent": "modal",
|
||||||
|
"left": 340, "top": 290, "width": 460, "height": 0,
|
||||||
|
"strokeColor": "#333340"
|
||||||
|
},
|
||||||
|
"body-1": {
|
||||||
|
"type": "Text", "parent": "modal",
|
||||||
|
"left": 340, "top": 310,
|
||||||
|
"text": "Are you sure you want to proceed?",
|
||||||
|
"fontColor": "#c8d0e0", "fontSize": 14
|
||||||
|
},
|
||||||
|
"body-2": {
|
||||||
|
"type": "Text", "parent": "modal",
|
||||||
|
"left": 340, "top": 340,
|
||||||
|
"text": "This action cannot be undone.",
|
||||||
|
"fontColor": "#c8d0e0", "fontSize": 14
|
||||||
|
},
|
||||||
|
"btn-cancel": {
|
||||||
|
"type": "Rectangle", "parent": "modal",
|
||||||
|
"left": 480, "top": 480, "width": 120, "height": 36,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"btn-cancel-label": {
|
||||||
|
"type": "Text", "parent": "btn-cancel",
|
||||||
|
"left": 510, "top": 488,
|
||||||
|
"text": "Cancel",
|
||||||
|
"fontColor": "#c8d8f0", "fontSize": 14
|
||||||
|
},
|
||||||
|
"btn-confirm": {
|
||||||
|
"type": "Rectangle", "parent": "modal",
|
||||||
|
"left": 620, "top": 480, "width": 120, "height": 36,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"btn-confirm-label": {
|
||||||
|
"type": "Text", "parent": "btn-confirm",
|
||||||
|
"left": 645, "top": 488,
|
||||||
|
"text": "Confirm",
|
||||||
|
"fontColor": "#c8d8f0", "fontSize": 14
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. List View
|
||||||
|
|
||||||
|
Scrollable list with item selection and detail panel.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "List View",
|
||||||
|
"shapes": {
|
||||||
|
"list-panel": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"left": 20, "top": 20, "width": 400, "height": 740,
|
||||||
|
"fillColor": "#1a1e24", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"item-1": {
|
||||||
|
"type": "Rectangle", "parent": "list-panel",
|
||||||
|
"left": 30, "top": 30, "width": 380, "height": 40,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"item-1-label": {
|
||||||
|
"type": "Text", "parent": "item-1",
|
||||||
|
"left": 40, "top": 38, "text": "Item Alpha",
|
||||||
|
"fontColor": "#c8d8f0", "fontSize": 14
|
||||||
|
},
|
||||||
|
"item-2": {
|
||||||
|
"type": "Rectangle", "parent": "list-panel",
|
||||||
|
"left": 30, "top": 80, "width": 380, "height": 40,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"item-2-label": {
|
||||||
|
"type": "Text", "parent": "item-2",
|
||||||
|
"left": 40, "top": 88, "text": "Item Beta",
|
||||||
|
"fontColor": "#c8d0e0", "fontSize": 14
|
||||||
|
},
|
||||||
|
"item-3": {
|
||||||
|
"type": "Rectangle", "parent": "list-panel",
|
||||||
|
"left": 30, "top": 130, "width": 380, "height": 40,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"item-3-label": {
|
||||||
|
"type": "Text", "parent": "item-3",
|
||||||
|
"left": 40, "top": 138, "text": "Item Gamma",
|
||||||
|
"fontColor": "#c8d0e0", "fontSize": 14
|
||||||
|
},
|
||||||
|
"detail-panel": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"left": 440, "top": 20, "width": 680, "height": 740,
|
||||||
|
"fillColor": "#1a1e24", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"detail-title": {
|
||||||
|
"type": "Text", "parent": "detail-panel",
|
||||||
|
"left": 460, "top": 40,
|
||||||
|
"text": "Item Alpha",
|
||||||
|
"fontColor": "#c8d0e0", "fontSize": 18
|
||||||
|
},
|
||||||
|
"detail-body": {
|
||||||
|
"type": "Text", "parent": "detail-panel",
|
||||||
|
"left": 460, "top": 80, "width": 640,
|
||||||
|
"text": "Description and properties appear here.",
|
||||||
|
"fontColor": "#c8d0e0", "fontSize": 14, "wordWrap": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Inventory Grid
|
||||||
|
|
||||||
|
Grid of cells for item management.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Inventory Grid",
|
||||||
|
"shapes": {
|
||||||
|
"panel": {
|
||||||
|
"type": "Rectangle",
|
||||||
|
"left": 240, "top": 100, "width": 660, "height": 580,
|
||||||
|
"fillColor": "#1a1e24", "strokeColor": "#333340", "corners": [8, 8, 8, 8]
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "Text", "parent": "panel",
|
||||||
|
"left": 260, "top": 120,
|
||||||
|
"text": "INVENTORY",
|
||||||
|
"fontColor": "#c8d0e0", "fontSize": 18
|
||||||
|
},
|
||||||
|
"cell-1-1": {
|
||||||
|
"type": "Rectangle", "parent": "panel",
|
||||||
|
"left": 260, "top": 160, "width": 64, "height": 64,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"cell-1-2": {
|
||||||
|
"type": "Rectangle", "parent": "panel",
|
||||||
|
"left": 332, "top": 160, "width": 64, "height": 64,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"cell-1-3": {
|
||||||
|
"type": "Rectangle", "parent": "panel",
|
||||||
|
"left": 404, "top": 160, "width": 64, "height": 64,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"cell-1-4": {
|
||||||
|
"type": "Rectangle", "parent": "panel",
|
||||||
|
"left": 476, "top": 160, "width": 64, "height": 64,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"cell-2-1": {
|
||||||
|
"type": "Rectangle", "parent": "panel",
|
||||||
|
"left": 260, "top": 232, "width": 64, "height": 64,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"cell-2-2": {
|
||||||
|
"type": "Rectangle", "parent": "panel",
|
||||||
|
"left": 332, "top": 232, "width": 64, "height": 64,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"cell-2-3": {
|
||||||
|
"type": "Rectangle", "parent": "panel",
|
||||||
|
"left": 404, "top": 232, "width": 64, "height": 64,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"cell-2-4": {
|
||||||
|
"type": "Rectangle", "parent": "panel",
|
||||||
|
"left": 476, "top": 232, "width": 64, "height": 64,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"detail": {
|
||||||
|
"type": "Rectangle", "parent": "panel",
|
||||||
|
"left": 580, "top": 160, "width": 300, "height": 400,
|
||||||
|
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||||
|
},
|
||||||
|
"detail-title": {
|
||||||
|
"type": "Text", "parent": "detail",
|
||||||
|
"left": 600, "top": 180,
|
||||||
|
"text": "Selected Item Name",
|
||||||
|
"fontColor": "#c8d0e0", "fontSize": 16
|
||||||
|
},
|
||||||
|
"detail-body": {
|
||||||
|
"type": "Text", "parent": "detail",
|
||||||
|
"left": 600, "top": 210, "width": 260,
|
||||||
|
"text": "Item description and stats",
|
||||||
|
"fontColor": "#c8d0e0", "fontSize": 14, "wordWrap": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# Frame0 Setup Guide
|
||||||
|
|
||||||
|
## Installation (Fedora)
|
||||||
|
|
||||||
|
Download from https://frame0.app/download and install the RPM:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo dnf install ./frame0-*.x86_64.rpm
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires: Fedora 40 or later (x86_64).
|
||||||
|
|
||||||
|
## Starting Frame0
|
||||||
|
|
||||||
|
Launch the desktop application:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
frame0 &
|
||||||
|
```
|
||||||
|
|
||||||
|
Frame0 exposes an HTTP API at `localhost:58320` when running.
|
||||||
|
|
||||||
|
## Verify API Access
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.claude/skills/frame0-wireframe/scripts/frame0-cmd.sh health
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output: `Frame0 is running on port 58320`
|
||||||
|
|
||||||
|
## Port Configuration
|
||||||
|
|
||||||
|
Default port: **58320**
|
||||||
|
|
||||||
|
To use a different port, set the environment variable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export FRAME0_PORT=58321
|
||||||
|
```
|
||||||
|
|
||||||
|
Or pass `--port` to any script:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.claude/skills/frame0-wireframe/scripts/frame0-cmd.sh --port 58321 health
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Cause | Fix |
|
||||||
|
|---------|-------|-----|
|
||||||
|
| "Connection refused" | Frame0 not running | Start the desktop app |
|
||||||
|
| "Port in use" | Another instance running | Close duplicate or use different port |
|
||||||
|
| Script hangs | API unresponsive | Restart Frame0 |
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PORT="${FRAME0_PORT:-58320}"
|
||||||
|
ENDPOINT="http://localhost:${PORT}/execute_command"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: $(basename "$0") <command> [args...] [--port N]
|
||||||
|
|
||||||
|
Low-level Frame0 HTTP API wrapper. Replaces the MCP server with direct
|
||||||
|
curl calls. Requires Frame0 desktop app to be running.
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
health Check if Frame0 is running
|
||||||
|
exec <namespace:action> <json> Execute a raw API command
|
||||||
|
create-shape <type> <json-props> Create a shape (Rectangle, Ellipse, Text, Line)
|
||||||
|
get-shape <id> Get shape details
|
||||||
|
update-shape <id> <json-props> Update shape properties
|
||||||
|
delete <id> [id...] Delete shapes by ID
|
||||||
|
move <id> <dx> <dy> Move a shape by pixel offset
|
||||||
|
duplicate <id> Duplicate a shape
|
||||||
|
group <id> [id...] Group shapes
|
||||||
|
ungroup <group-id> Ungroup a group
|
||||||
|
create-connector <tail-id> <head-id> [json-props] Connect two shapes
|
||||||
|
create-icon <name> <json-props> Create an icon shape
|
||||||
|
add-page <name> Add a new page (becomes current)
|
||||||
|
get-page [page-id] Get current or specific page data
|
||||||
|
list-pages [--shapes] List all pages (--shapes for shape data)
|
||||||
|
current-page Get current page ID
|
||||||
|
set-page <page-id> Set current page
|
||||||
|
export [page-id] [--format mime] Export page as image (default: image/png)
|
||||||
|
fit Fit view to screen
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--port N Frame0 API port (default: $PORT, env: FRAME0_PORT)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
$(basename "$0") health
|
||||||
|
$(basename "$0") add-page "HUD Layout"
|
||||||
|
$(basename "$0") create-shape Rectangle '{"name":"btn","left":100,"top":100,"width":120,"height":36}'
|
||||||
|
$(basename "$0") list-pages
|
||||||
|
$(basename "$0") export --format image/png
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse --port from anywhere in args
|
||||||
|
ARGS=()
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--port) PORT="$2"; ENDPOINT="http://localhost:${PORT}/execute_command"; shift 2 ;;
|
||||||
|
*) ARGS+=("$1"); shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
set -- "${ARGS[@]+"${ARGS[@]}"}"
|
||||||
|
|
||||||
|
[[ $# -lt 1 ]] && usage
|
||||||
|
|
||||||
|
# Execute a Frame0 API command, return data or error
|
||||||
|
frame0_exec() {
|
||||||
|
local command="$1"
|
||||||
|
local args
|
||||||
|
args="${2:-"{}"}"
|
||||||
|
|
||||||
|
local response
|
||||||
|
response=$(curl -s -w "\n%{http_code}" -X POST "$ENDPOINT" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"command\": \"$command\", \"args\": $args}" 2>&1) || {
|
||||||
|
echo "ERROR: Cannot connect to Frame0 at localhost:$PORT" >&2
|
||||||
|
echo "Is Frame0 running? See: .claude/skills/frame0-wireframe/references/setup-guide.md" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
local http_code body
|
||||||
|
http_code=$(echo "$response" | tail -1)
|
||||||
|
body=$(echo "$response" | sed '$d')
|
||||||
|
|
||||||
|
if [[ "$http_code" != 2* ]]; then
|
||||||
|
echo "ERROR: HTTP $http_code from Frame0" >&2
|
||||||
|
echo "$body" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Parse success/error from response
|
||||||
|
python3 -c "
|
||||||
|
import sys, json
|
||||||
|
try:
|
||||||
|
r = json.loads(sys.stdin.read())
|
||||||
|
if r.get('success'):
|
||||||
|
d = r.get('data')
|
||||||
|
if d is not None:
|
||||||
|
print(json.dumps(d, indent=2))
|
||||||
|
else:
|
||||||
|
print('ERROR: ' + str(r.get('error', 'Unknown error')), file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
print(f'ERROR: Invalid JSON response: {e}', file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
" <<< "$body"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build JSON array from remaining args
|
||||||
|
ids_to_json_array() {
|
||||||
|
local arr="["
|
||||||
|
local first=true
|
||||||
|
for id in "$@"; do
|
||||||
|
[[ "$first" == true ]] && first=false || arr+=","
|
||||||
|
arr+="\"$id\""
|
||||||
|
done
|
||||||
|
arr+="]"
|
||||||
|
echo "$arr"
|
||||||
|
}
|
||||||
|
|
||||||
|
CMD="${1:-}"
|
||||||
|
shift || true
|
||||||
|
|
||||||
|
case "$CMD" in
|
||||||
|
health)
|
||||||
|
if curl -s -o /dev/null -w "%{http_code}" "http://localhost:${PORT}/" 2>/dev/null | grep -q "^[23]"; then
|
||||||
|
echo "Frame0 is running on port $PORT"
|
||||||
|
else
|
||||||
|
echo "Frame0 is NOT running on port $PORT" >&2
|
||||||
|
echo "Start Frame0 desktop app, then retry." >&2
|
||||||
|
echo "See: .claude/skills/frame0-wireframe/references/setup-guide.md" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
|
||||||
|
exec)
|
||||||
|
[[ $# -lt 2 ]] && { echo "Usage: exec <command> <json-args>" >&2; exit 1; }
|
||||||
|
frame0_exec "$1" "$2"
|
||||||
|
;;
|
||||||
|
|
||||||
|
create-shape)
|
||||||
|
[[ $# -lt 2 ]] && { echo "Usage: create-shape <Type> <json-props>" >&2; exit 1; }
|
||||||
|
local_type="$1"
|
||||||
|
local_props="$2"
|
||||||
|
local_parent="${3:-}"
|
||||||
|
local_parent_arg=""
|
||||||
|
[[ -n "$local_parent" ]] && local_parent_arg=", \"parentId\": \"$local_parent\""
|
||||||
|
frame0_exec "shape:create-shape" "{\"type\": \"$local_type\", \"shapeProps\": $local_props$local_parent_arg, \"convertColors\": true}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
get-shape)
|
||||||
|
[[ $# -lt 1 ]] && { echo "Usage: get-shape <id>" >&2; exit 1; }
|
||||||
|
frame0_exec "shape:get-shape" "{\"shapeId\": \"$1\"}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
update-shape)
|
||||||
|
[[ $# -lt 2 ]] && { echo "Usage: update-shape <id> <json-props>" >&2; exit 1; }
|
||||||
|
frame0_exec "shape:update-shape" "{\"shapeId\": \"$1\", \"shapeProps\": $2, \"convertColors\": true}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
delete)
|
||||||
|
[[ $# -lt 1 ]] && { echo "Usage: delete <id> [id...]" >&2; exit 1; }
|
||||||
|
local_arr=$(ids_to_json_array "$@")
|
||||||
|
frame0_exec "edit:delete" "{\"shapeIdArray\": $local_arr}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
move)
|
||||||
|
[[ $# -lt 3 ]] && { echo "Usage: move <id> <dx> <dy>" >&2; exit 1; }
|
||||||
|
frame0_exec "shape:move" "{\"shapeId\": \"$1\", \"dx\": $2, \"dy\": $3}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
duplicate)
|
||||||
|
[[ $# -lt 1 ]] && { echo "Usage: duplicate <id> [dx] [dy]" >&2; exit 1; }
|
||||||
|
local_dx="${2:-0}"
|
||||||
|
local_dy="${3:-0}"
|
||||||
|
frame0_exec "edit:duplicate" "{\"shapeIdArray\": [\"$1\"], \"dx\": $local_dx, \"dy\": $local_dy}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
group)
|
||||||
|
[[ $# -lt 2 ]] && { echo "Usage: group <id> <id> [id...]" >&2; exit 1; }
|
||||||
|
local_arr=$(ids_to_json_array "$@")
|
||||||
|
frame0_exec "shape:group" "{\"shapeIdArray\": $local_arr}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
ungroup)
|
||||||
|
[[ $# -lt 1 ]] && { echo "Usage: ungroup <group-id>" >&2; exit 1; }
|
||||||
|
frame0_exec "shape:ungroup" "{\"shapeIdArray\": [\"$1\"]}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
create-connector)
|
||||||
|
[[ $# -lt 2 ]] && { echo "Usage: create-connector <tail-id> <head-id> [json-props]" >&2; exit 1; }
|
||||||
|
local_props="${3:-{}}"
|
||||||
|
frame0_exec "shape:create-connector" "{\"tailId\": \"$1\", \"headId\": \"$2\", \"shapeProps\": $local_props, \"convertColors\": true}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
create-icon)
|
||||||
|
[[ $# -lt 2 ]] && { echo "Usage: create-icon <name> <json-props>" >&2; exit 1; }
|
||||||
|
frame0_exec "shape:create-icon" "{\"iconName\": \"$1\", \"shapeProps\": $2, \"convertColors\": true}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
add-page)
|
||||||
|
[[ $# -lt 1 ]] && { echo "Usage: add-page <name>" >&2; exit 1; }
|
||||||
|
frame0_exec "page:add" "{\"pageProps\": {\"name\": \"$1\"}}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
get-page)
|
||||||
|
if [[ $# -ge 1 ]]; then
|
||||||
|
frame0_exec "page:get" "{\"pageId\": \"$1\", \"exportShapes\": true}"
|
||||||
|
else
|
||||||
|
local_id
|
||||||
|
local_id=$(frame0_exec "page:get-current-page")
|
||||||
|
# Strip quotes from returned ID
|
||||||
|
local_id=$(echo "$local_id" | tr -d '"')
|
||||||
|
frame0_exec "page:get" "{\"pageId\": \"$local_id\", \"exportShapes\": true}"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
|
||||||
|
list-pages)
|
||||||
|
local_shapes="false"
|
||||||
|
[[ "${1:-}" == "--shapes" ]] && local_shapes="true"
|
||||||
|
frame0_exec "doc:get" "{\"exportPages\": true, \"exportShapes\": $local_shapes}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
current-page)
|
||||||
|
frame0_exec "page:get-current-page"
|
||||||
|
;;
|
||||||
|
|
||||||
|
set-page)
|
||||||
|
[[ $# -lt 1 ]] && { echo "Usage: set-page <page-id>" >&2; exit 1; }
|
||||||
|
frame0_exec "page:set-current-page" "{\"pageId\": \"$1\"}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
export)
|
||||||
|
local_page_id=""
|
||||||
|
local_format="image/png"
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--format) local_format="$2"; shift 2 ;;
|
||||||
|
*) local_page_id="$1"; shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
local_page_arg=""
|
||||||
|
[[ -n "$local_page_id" ]] && local_page_arg="\"pageId\": \"$local_page_id\", "
|
||||||
|
frame0_exec "file:export-image" "{${local_page_arg}\"format\": \"$local_format\", \"fillBackground\": true}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
fit)
|
||||||
|
frame0_exec "view:fit-to-screen"
|
||||||
|
;;
|
||||||
|
|
||||||
|
--help|-h|help)
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
echo "Unknown command: $CMD" >&2
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Batch export wireframes from JSON to PNG via Frame0.
|
||||||
|
#
|
||||||
|
# Finds all .json wireframe files under docs/design/wireframes/ and exports
|
||||||
|
# each to a matching .png. Skips files whose PNG is already newer than the
|
||||||
|
# JSON, unless --force is passed.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# frame0-export-batch.sh [--dry-run] [--force] [--category CAT] [--root DIR]
|
||||||
|
#
|
||||||
|
# Options:
|
||||||
|
# --dry-run Print manifest only, don't touch Frame0.
|
||||||
|
# --force Re-export even if PNG already exists and is up to date.
|
||||||
|
# --category CAT Limit to one subdirectory (e.g. --category dialogue)
|
||||||
|
# --root DIR Wireframes root dir (default: docs/design/wireframes)
|
||||||
|
#
|
||||||
|
# Exit codes:
|
||||||
|
# 0 All exports succeeded (or nothing to do)
|
||||||
|
# 1 One or more exports failed
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
|
||||||
|
SYNC="$SCRIPT_DIR/frame0-sync.py"
|
||||||
|
DEFAULT_ROOT="$REPO_ROOT/docs/design/wireframes"
|
||||||
|
|
||||||
|
DRY_RUN=false
|
||||||
|
FORCE=false
|
||||||
|
CATEGORY=""
|
||||||
|
WF_ROOT="$DEFAULT_ROOT"
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--dry-run) DRY_RUN=true; shift ;;
|
||||||
|
--force) FORCE=true; shift ;;
|
||||||
|
--category) CATEGORY="$2"; shift 2 ;;
|
||||||
|
--root) WF_ROOT="$2"; shift 2 ;;
|
||||||
|
-h|--help)
|
||||||
|
sed -n '/^# /p' "$0" | sed 's/^# //'
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ! -d "$WF_ROOT" ]]; then
|
||||||
|
echo "ERROR: Wireframes directory not found: $WF_ROOT" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect JSON files, optionally filtered by category subdirectory
|
||||||
|
mapfile -t JSON_FILES < <(
|
||||||
|
if [[ -n "$CATEGORY" ]]; then
|
||||||
|
find "$WF_ROOT/$CATEGORY" -name "*.json" ! -name ".*" | sort
|
||||||
|
else
|
||||||
|
find "$WF_ROOT" -name "*.json" ! -name ".*" | sort
|
||||||
|
fi
|
||||||
|
)
|
||||||
|
|
||||||
|
if [[ ${#JSON_FILES[@]} -eq 0 ]]; then
|
||||||
|
echo "No wireframe JSON files found."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Classify files into to-export and to-skip
|
||||||
|
TO_EXPORT=()
|
||||||
|
TO_SKIP=()
|
||||||
|
|
||||||
|
for json in "${JSON_FILES[@]}"; do
|
||||||
|
png="${json%.json}.png"
|
||||||
|
if $FORCE || [[ ! -f "$png" ]] || [[ "$json" -nt "$png" ]]; then
|
||||||
|
TO_EXPORT+=("$json")
|
||||||
|
else
|
||||||
|
TO_SKIP+=("$json")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Print manifest
|
||||||
|
if [[ ${#TO_EXPORT[@]} -gt 0 ]]; then
|
||||||
|
echo ""
|
||||||
|
echo "Will export (${#TO_EXPORT[@]} files):"
|
||||||
|
for json in "${TO_EXPORT[@]}"; do
|
||||||
|
png="${json%.json}.png"
|
||||||
|
rel="${json#$REPO_ROOT/}"
|
||||||
|
if [[ ! -f "$png" ]]; then
|
||||||
|
status="new"
|
||||||
|
else
|
||||||
|
status="updated"
|
||||||
|
fi
|
||||||
|
printf " [%-7s] %s\n" "$status" "$rel"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo "Nothing to export (all PNGs up to date)."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ${#TO_SKIP[@]} -gt 0 ]]; then
|
||||||
|
echo ""
|
||||||
|
echo "Will skip (${#TO_SKIP[@]} files already up to date):"
|
||||||
|
for json in "${TO_SKIP[@]}"; do
|
||||||
|
rel="${json#$REPO_ROOT/}"
|
||||||
|
printf " [skip ] %s\n" "$rel"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if $DRY_RUN; then
|
||||||
|
echo ""
|
||||||
|
echo "Dry run — no exports performed."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ${#TO_EXPORT[@]} -eq 0 ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
PASSED=0
|
||||||
|
FAILED=0
|
||||||
|
FAILED_FILES=()
|
||||||
|
|
||||||
|
TOTAL=${#TO_EXPORT[@]}
|
||||||
|
IDX=0
|
||||||
|
|
||||||
|
for json in "${TO_EXPORT[@]}"; do
|
||||||
|
IDX=$((IDX + 1))
|
||||||
|
png="${json%.json}.png"
|
||||||
|
rel="${json#$REPO_ROOT/}"
|
||||||
|
|
||||||
|
printf "[%d/%d] %s ... " "$IDX" "$TOTAL" "$rel"
|
||||||
|
|
||||||
|
output=$(python3 "$SYNC" export "$json" "$png" 2>/tmp/frame0-batch-err.txt)
|
||||||
|
rc=$?
|
||||||
|
if [[ $rc -eq 0 ]]; then
|
||||||
|
size=$(echo "$output" | tail -1 | grep -oP '\(\K[^)]+' || true)
|
||||||
|
echo "ok $size"
|
||||||
|
PASSED=$((PASSED + 1))
|
||||||
|
else
|
||||||
|
echo "FAILED"
|
||||||
|
cat /tmp/frame0-batch-err.txt >&2
|
||||||
|
FAILED=$((FAILED + 1))
|
||||||
|
FAILED_FILES+=("$rel")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "$PASSED exported, $FAILED failed."
|
||||||
|
|
||||||
|
if [[ $FAILED -gt 0 ]]; then
|
||||||
|
echo ""
|
||||||
|
echo "Failed:" >&2
|
||||||
|
for f in "${FAILED_FILES[@]}"; do
|
||||||
|
echo " $f" >&2
|
||||||
|
done
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,414 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Frame0 sync: push local JSON wireframes to Frame0, pull pages back.
|
||||||
|
|
||||||
|
Local JSON is source of truth. Frame0 is a renderer.
|
||||||
|
A mapping file tracks local_id <-> frame0_id across push/pull cycles.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
frame0-sync.py push <wireframe.json> [--port PORT]
|
||||||
|
frame0-sync.py pull <page-id|page-name> <output.json> [--port PORT]
|
||||||
|
frame0-sync.py export <wireframe.json> <output.png> [--port PORT] [--format MIME]
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
|
|
||||||
|
DEFAULT_PORT = 58320
|
||||||
|
|
||||||
|
|
||||||
|
def api(port, command, args=None):
|
||||||
|
"""Execute a Frame0 API command. Returns the data field on success."""
|
||||||
|
url = f"http://localhost:{port}/execute_command"
|
||||||
|
payload = json.dumps({"command": command, "args": args or {}}).encode()
|
||||||
|
req = urllib.request.Request(url, data=payload, headers={"Content-Type": "application/json"})
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req) as resp:
|
||||||
|
result = json.loads(resp.read())
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
body = e.read().decode()
|
||||||
|
print(f"ERROR: {command}: HTTP {e.code}: {body[:500]}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
except urllib.error.URLError as e:
|
||||||
|
print(f"ERROR: Cannot connect to Frame0 on port {port}: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not result.get("success"):
|
||||||
|
print(f"ERROR: {command}: {result.get('error', 'unknown')}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
return result.get("data")
|
||||||
|
|
||||||
|
|
||||||
|
# -- Mapping file management --------------------------------------------------
|
||||||
|
|
||||||
|
def mapping_path(wireframe_path):
|
||||||
|
"""Derive the idmap path from the wireframe JSON path."""
|
||||||
|
d = os.path.dirname(wireframe_path)
|
||||||
|
base = os.path.splitext(os.path.basename(wireframe_path))[0]
|
||||||
|
return os.path.join(d, f".{base}.idmap.json")
|
||||||
|
|
||||||
|
|
||||||
|
def load_mapping(wireframe_path):
|
||||||
|
p = mapping_path(wireframe_path)
|
||||||
|
if os.path.exists(p):
|
||||||
|
with open(p) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return {"page_id": None, "shapes": {}, "connectors": {}}
|
||||||
|
|
||||||
|
|
||||||
|
def save_mapping(wireframe_path, mapping):
|
||||||
|
p = mapping_path(wireframe_path)
|
||||||
|
os.makedirs(os.path.dirname(p) or ".", exist_ok=True)
|
||||||
|
with open(p, "w") as f:
|
||||||
|
json.dump(mapping, f, indent=2)
|
||||||
|
f.write("\n")
|
||||||
|
|
||||||
|
|
||||||
|
# -- Push: local JSON -> Frame0 -----------------------------------------------
|
||||||
|
|
||||||
|
# Frame0 returns different type names from get vs what create accepts.
|
||||||
|
# Map internal types back to create-API types.
|
||||||
|
TYPE_TO_CREATE = {
|
||||||
|
"Box": "Rectangle",
|
||||||
|
"box": "Rectangle",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def topo_sort_shapes(shapes):
|
||||||
|
"""Sort shape IDs so parents come before children."""
|
||||||
|
order = []
|
||||||
|
visited = set()
|
||||||
|
|
||||||
|
def visit(sid):
|
||||||
|
if sid in visited:
|
||||||
|
return
|
||||||
|
visited.add(sid)
|
||||||
|
shape = shapes[sid]
|
||||||
|
parent = shape.get("parent")
|
||||||
|
if parent and parent in shapes:
|
||||||
|
visit(parent)
|
||||||
|
order.append(sid)
|
||||||
|
|
||||||
|
for sid in shapes:
|
||||||
|
visit(sid)
|
||||||
|
return order
|
||||||
|
|
||||||
|
|
||||||
|
def find_or_create_page(port, name, mapping):
|
||||||
|
"""Find existing page by mapping or name, or create a new one."""
|
||||||
|
# Try mapped page_id first
|
||||||
|
if mapping.get("page_id"):
|
||||||
|
try:
|
||||||
|
page = api(port, "page:get", {"pageId": mapping["page_id"]})
|
||||||
|
if page:
|
||||||
|
return mapping["page_id"]
|
||||||
|
except SystemExit:
|
||||||
|
pass # Page no longer exists, fall through
|
||||||
|
|
||||||
|
# Search by name in existing pages
|
||||||
|
doc = api(port, "doc:get", {"exportPages": True, "exportShapes": False})
|
||||||
|
if doc and "children" in doc:
|
||||||
|
for page in doc["children"]:
|
||||||
|
if page.get("name") == name:
|
||||||
|
return page["id"]
|
||||||
|
|
||||||
|
# Create new page
|
||||||
|
page = api(port, "page:add", {"pageProps": {"name": name}})
|
||||||
|
return page["id"]
|
||||||
|
|
||||||
|
|
||||||
|
def clear_page(port, page_id):
|
||||||
|
"""Delete all shapes on a page."""
|
||||||
|
page = api(port, "page:get", {"pageId": page_id, "exportShapes": True})
|
||||||
|
if not page or "children" not in page:
|
||||||
|
return
|
||||||
|
shape_ids = [s["id"] for s in page["children"]]
|
||||||
|
if shape_ids:
|
||||||
|
api(port, "edit:delete", {"shapeIdArray": shape_ids})
|
||||||
|
|
||||||
|
|
||||||
|
def push(wireframe_path, port):
|
||||||
|
"""Push local wireframe JSON to Frame0."""
|
||||||
|
with open(wireframe_path) as f:
|
||||||
|
wireframe = json.load(f)
|
||||||
|
|
||||||
|
name = wireframe.get("name", os.path.splitext(os.path.basename(wireframe_path))[0])
|
||||||
|
shapes = wireframe.get("shapes", {})
|
||||||
|
connectors = wireframe.get("connectors", {})
|
||||||
|
|
||||||
|
mapping = load_mapping(wireframe_path)
|
||||||
|
|
||||||
|
# Find or create page
|
||||||
|
page_id = find_or_create_page(port, name, mapping)
|
||||||
|
mapping["page_id"] = page_id
|
||||||
|
|
||||||
|
# Switch to page and clear it
|
||||||
|
api(port, "page:set-current-page", {"pageId": page_id})
|
||||||
|
clear_page(port, page_id)
|
||||||
|
|
||||||
|
# Reset ID mappings (shapes are recreated)
|
||||||
|
mapping["shapes"] = {}
|
||||||
|
mapping["connectors"] = {}
|
||||||
|
|
||||||
|
# Create shapes in dependency order
|
||||||
|
order = topo_sort_shapes(shapes)
|
||||||
|
for local_id in order:
|
||||||
|
shape = shapes[local_id]
|
||||||
|
raw_type = shape.get("type", "Rectangle")
|
||||||
|
shape_type = TYPE_TO_CREATE.get(raw_type, raw_type)
|
||||||
|
parent_local = shape.get("parent")
|
||||||
|
|
||||||
|
# Build shapeProps from everything except meta fields
|
||||||
|
meta_keys = {"type", "parent"}
|
||||||
|
props = {k: v for k, v in shape.items() if k not in meta_keys}
|
||||||
|
|
||||||
|
# Set name to local_id if not explicitly set
|
||||||
|
if "name" not in props:
|
||||||
|
props["name"] = local_id
|
||||||
|
|
||||||
|
create_args = {
|
||||||
|
"type": shape_type,
|
||||||
|
"shapeProps": props,
|
||||||
|
"convertColors": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Resolve parent ID
|
||||||
|
if parent_local and parent_local in mapping["shapes"]:
|
||||||
|
create_args["parentId"] = mapping["shapes"][parent_local]
|
||||||
|
|
||||||
|
f0_id = api(port, "shape:create-shape", create_args)
|
||||||
|
mapping["shapes"][local_id] = f0_id
|
||||||
|
|
||||||
|
# Create connectors
|
||||||
|
for local_id, conn in connectors.items():
|
||||||
|
tail_local = conn.get("tailId")
|
||||||
|
head_local = conn.get("headId")
|
||||||
|
|
||||||
|
if tail_local not in mapping["shapes"] or head_local not in mapping["shapes"]:
|
||||||
|
print(f"WARNING: connector '{local_id}' references unknown shape, skipping", file=sys.stderr)
|
||||||
|
continue
|
||||||
|
|
||||||
|
meta_keys = {"tailId", "headId"}
|
||||||
|
props = {k: v for k, v in conn.items() if k not in meta_keys}
|
||||||
|
if "name" not in props:
|
||||||
|
props["name"] = local_id
|
||||||
|
|
||||||
|
f0_id = api(port, "shape:create-connector", {
|
||||||
|
"tailId": mapping["shapes"][tail_local],
|
||||||
|
"headId": mapping["shapes"][head_local],
|
||||||
|
"shapeProps": props,
|
||||||
|
"convertColors": True,
|
||||||
|
})
|
||||||
|
mapping["connectors"][local_id] = f0_id
|
||||||
|
|
||||||
|
# Fit to screen
|
||||||
|
api(port, "view:fit-to-screen")
|
||||||
|
|
||||||
|
save_mapping(wireframe_path, mapping)
|
||||||
|
total = len(mapping["shapes"]) + len(mapping["connectors"])
|
||||||
|
print(f"Pushed '{name}' to Frame0: {len(mapping['shapes'])} shapes, {len(mapping['connectors'])} connectors")
|
||||||
|
|
||||||
|
|
||||||
|
# -- Pull: Frame0 -> local JSON -----------------------------------------------
|
||||||
|
|
||||||
|
def pull(page_ref, output_path, port):
|
||||||
|
"""Pull a Frame0 page into local wireframe JSON."""
|
||||||
|
# Resolve page_ref: could be an ID or a name
|
||||||
|
page_id = None
|
||||||
|
doc = api(port, "doc:get", {"exportPages": True, "exportShapes": False})
|
||||||
|
if doc and "children" in doc:
|
||||||
|
for page in doc["children"]:
|
||||||
|
if page["id"] == page_ref or page.get("name") == page_ref:
|
||||||
|
page_id = page["id"]
|
||||||
|
page_name = page.get("name", page_ref)
|
||||||
|
break
|
||||||
|
|
||||||
|
if not page_id:
|
||||||
|
print(f"ERROR: Page not found: {page_ref}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Load existing mapping for reverse lookup
|
||||||
|
mapping = load_mapping(output_path)
|
||||||
|
reverse_map = {v: k for k, v in mapping.get("shapes", {}).items()}
|
||||||
|
reverse_conn = {v: k for k, v in mapping.get("connectors", {}).items()}
|
||||||
|
|
||||||
|
# Get full page with shapes
|
||||||
|
page = api(port, "page:get", {"pageId": page_id, "exportShapes": True})
|
||||||
|
|
||||||
|
shapes = {}
|
||||||
|
connectors = {}
|
||||||
|
new_mapping = {"page_id": page_id, "shapes": {}, "connectors": {}}
|
||||||
|
auto_id_counter = [0]
|
||||||
|
|
||||||
|
def auto_id(f0_shape):
|
||||||
|
"""Generate a stable local ID from shape name or auto-number."""
|
||||||
|
# Prefer existing mapping
|
||||||
|
f0_id = f0_shape["id"]
|
||||||
|
if f0_id in reverse_map:
|
||||||
|
return reverse_map[f0_id]
|
||||||
|
# Use sanitized name
|
||||||
|
name = f0_shape.get("name", "")
|
||||||
|
if name:
|
||||||
|
sanitized = name.lower().replace(" ", "-").replace("_", "-")
|
||||||
|
if sanitized not in shapes:
|
||||||
|
return sanitized
|
||||||
|
# Fallback: auto-number
|
||||||
|
auto_id_counter[0] += 1
|
||||||
|
return f"s{auto_id_counter[0]:03d}"
|
||||||
|
|
||||||
|
def process_shape(f0_shape, parent_local_id=None):
|
||||||
|
f0_id = f0_shape["id"]
|
||||||
|
local_id = auto_id(f0_shape)
|
||||||
|
new_mapping["shapes"][local_id] = f0_id
|
||||||
|
|
||||||
|
# Extract shape properties — only strip structural keys that our
|
||||||
|
# ID mapping replaces. Everything else passes through as-is so the
|
||||||
|
# local JSON speaks Frame0's native vocabulary.
|
||||||
|
shape_type = f0_shape.get("type", "Box")
|
||||||
|
skip_keys = {"id", "type", "children", "pageId", "parentId"}
|
||||||
|
props = {k: v for k, v in f0_shape.items() if k not in skip_keys and v is not None}
|
||||||
|
|
||||||
|
entry = {"type": shape_type}
|
||||||
|
if parent_local_id:
|
||||||
|
entry["parent"] = parent_local_id
|
||||||
|
entry.update(props)
|
||||||
|
|
||||||
|
# Remove name if it matches local_id (redundant)
|
||||||
|
if entry.get("name") == local_id:
|
||||||
|
del entry["name"]
|
||||||
|
|
||||||
|
shapes[local_id] = entry
|
||||||
|
|
||||||
|
# Process children recursively
|
||||||
|
for child in f0_shape.get("children", []):
|
||||||
|
child_type = child.get("type", "")
|
||||||
|
if child_type == "Connector":
|
||||||
|
process_connector(child)
|
||||||
|
else:
|
||||||
|
process_shape(child, local_id)
|
||||||
|
|
||||||
|
def process_connector(f0_conn):
|
||||||
|
f0_id = f0_conn["id"]
|
||||||
|
local_id = reverse_conn.get(f0_id)
|
||||||
|
if not local_id:
|
||||||
|
auto_id_counter[0] += 1
|
||||||
|
local_id = f"c{auto_id_counter[0]:03d}"
|
||||||
|
|
||||||
|
new_mapping["connectors"][local_id] = f0_id
|
||||||
|
|
||||||
|
tail_f0 = f0_conn.get("tail", {}).get("id")
|
||||||
|
head_f0 = f0_conn.get("head", {}).get("id")
|
||||||
|
|
||||||
|
entry = {}
|
||||||
|
if tail_f0:
|
||||||
|
# Will be resolved after all shapes are processed
|
||||||
|
entry["_tailF0"] = tail_f0
|
||||||
|
if head_f0:
|
||||||
|
entry["_headF0"] = head_f0
|
||||||
|
|
||||||
|
skip_keys = {"id", "type", "children", "pageId", "tail", "head"}
|
||||||
|
props = {k: v for k, v in f0_conn.items() if k not in skip_keys and v is not None}
|
||||||
|
entry.update(props)
|
||||||
|
|
||||||
|
connectors[local_id] = entry
|
||||||
|
|
||||||
|
# Process all top-level shapes
|
||||||
|
for child in page.get("children", []):
|
||||||
|
child_type = child.get("type", "")
|
||||||
|
if child_type == "Connector":
|
||||||
|
process_connector(child)
|
||||||
|
else:
|
||||||
|
process_shape(child)
|
||||||
|
|
||||||
|
# Resolve connector references to local IDs
|
||||||
|
f0_to_local = {v: k for k, v in new_mapping["shapes"].items()}
|
||||||
|
for conn in connectors.values():
|
||||||
|
tail_f0 = conn.pop("_tailF0", None)
|
||||||
|
head_f0 = conn.pop("_headF0", None)
|
||||||
|
if tail_f0 and tail_f0 in f0_to_local:
|
||||||
|
conn["tailId"] = f0_to_local[tail_f0]
|
||||||
|
if head_f0 and head_f0 in f0_to_local:
|
||||||
|
conn["headId"] = f0_to_local[head_f0]
|
||||||
|
|
||||||
|
wireframe = {"name": page_name}
|
||||||
|
if shapes:
|
||||||
|
wireframe["shapes"] = shapes
|
||||||
|
if connectors:
|
||||||
|
wireframe["connectors"] = connectors
|
||||||
|
|
||||||
|
os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
|
||||||
|
with open(output_path, "w") as f:
|
||||||
|
json.dump(wireframe, f, indent=2)
|
||||||
|
f.write("\n")
|
||||||
|
|
||||||
|
save_mapping(output_path, new_mapping)
|
||||||
|
print(f"Pulled '{page_name}' -> {output_path}: {len(shapes)} shapes, {len(connectors)} connectors")
|
||||||
|
|
||||||
|
|
||||||
|
# -- Export: push then export as image -----------------------------------------
|
||||||
|
|
||||||
|
def export_image(wireframe_path, output_path, port, fmt="image/png"):
|
||||||
|
"""Push wireframe to Frame0 and export the page as an image."""
|
||||||
|
import base64
|
||||||
|
|
||||||
|
# Push first to ensure Frame0 is up to date
|
||||||
|
push(wireframe_path, port)
|
||||||
|
|
||||||
|
mapping = load_mapping(wireframe_path)
|
||||||
|
page_id = mapping.get("page_id")
|
||||||
|
if not page_id:
|
||||||
|
print("ERROR: No page_id in mapping after push", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
image_b64 = api(port, "file:export-image", {
|
||||||
|
"pageId": page_id,
|
||||||
|
"format": fmt,
|
||||||
|
"fillBackground": True,
|
||||||
|
})
|
||||||
|
|
||||||
|
image_bytes = base64.b64decode(image_b64)
|
||||||
|
with open(output_path, "wb") as f:
|
||||||
|
f.write(image_bytes)
|
||||||
|
|
||||||
|
print(f"Exported: {output_path} ({len(image_bytes) // 1024}KB)")
|
||||||
|
|
||||||
|
|
||||||
|
# -- CLI -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Sync wireframe JSON with Frame0")
|
||||||
|
parser.add_argument("--port", type=int, default=int(os.environ.get("FRAME0_PORT", DEFAULT_PORT)))
|
||||||
|
sub = parser.add_subparsers(dest="command")
|
||||||
|
|
||||||
|
p_push = sub.add_parser("push", help="Push local JSON to Frame0")
|
||||||
|
p_push.add_argument("wireframe", help="Path to wireframe .json file")
|
||||||
|
|
||||||
|
p_pull = sub.add_parser("pull", help="Pull Frame0 page to local JSON")
|
||||||
|
p_pull.add_argument("page", help="Page ID or page name")
|
||||||
|
p_pull.add_argument("output", help="Output .json path")
|
||||||
|
|
||||||
|
p_export = sub.add_parser("export", help="Push and export as image")
|
||||||
|
p_export.add_argument("wireframe", help="Path to wireframe .json file")
|
||||||
|
p_export.add_argument("output", help="Output image path (e.g. wireframe.png)")
|
||||||
|
p_export.add_argument("--format", default="image/png",
|
||||||
|
help="Export MIME type (default: image/png)")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.command == "push":
|
||||||
|
push(args.wireframe, args.port)
|
||||||
|
elif args.command == "pull":
|
||||||
|
pull(args.page, args.output, args.port)
|
||||||
|
elif args.command == "export":
|
||||||
|
export_image(args.wireframe, args.output, args.port, args.format)
|
||||||
|
else:
|
||||||
|
parser.print_help()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -134,6 +134,14 @@ Report which tickets were moved to review. Skip tickets that are
|
|||||||
already `done`, `review`, `cancelled`, or `backlog` (only transition
|
already `done`, `review`, `cancelled`, or `backlog` (only transition
|
||||||
`in_progress` → `review`).
|
`in_progress` → `review`).
|
||||||
|
|
||||||
|
### 9. Next steps
|
||||||
|
|
||||||
|
If a sprint team is active (you are the team lead), do NOT shut down
|
||||||
|
agents after pushing. The team should remain alive for PR review and
|
||||||
|
potential comment fixes.
|
||||||
|
|
||||||
|
Suggest: "PR created/updated. Run `/pr-review` to review before merge."
|
||||||
|
|
||||||
## Arguments
|
## Arguments
|
||||||
|
|
||||||
If the user passes arguments (e.g., `/pr-push "my title"`), use them as the
|
If the user passes arguments (e.g., `/pr-push "my title"`), use them as the
|
||||||
|
|||||||
@@ -63,23 +63,44 @@ If the diff is empty, report "No changes to review" and stop.
|
|||||||
|
|
||||||
Three-dot diff with pathspec exclusions is unreliable. Instead, either:
|
Three-dot diff with pathspec exclusions is unreliable. Instead, either:
|
||||||
1. Use `git diff main...<branch>` (full diff) and filter in the prompt, or
|
1. Use `git diff main...<branch>` (full diff) and filter in the prompt, or
|
||||||
2. Read source files directly from the branch:
|
2. Read source files directly from the branch worktree (see below).
|
||||||
`git show origin/<branch>:<path>`
|
|
||||||
|
|
||||||
For large diffs (>1000 lines of source), provide **source files** rather than
|
For large diffs (>1000 lines of source), provide **source files** rather than
|
||||||
raw diff to reviewers — cleaner context, better reviews. Read files with
|
raw diff to reviewers — cleaner context, better reviews.
|
||||||
`git show origin/<branch>:<path>` and include them in the prompt.
|
|
||||||
|
|
||||||
**IMPORTANT — agent tool access:** Not all reviewer agents have Bash access.
|
**IMPORTANT — use worktree paths for ALL agents.** This project uses git
|
||||||
Agents that CAN read from branches themselves: **Hoshe, Tyre, Araminta**.
|
worktrees. Each team branch is checked out at:
|
||||||
Agents that CANNOT (no Bash tool): **Paula, Miri, Ozzie, Gestalt, Gore, Nigel**.
|
|
||||||
|
|
||||||
For agents without Bash, you MUST read the source files yourself (via
|
```
|
||||||
`git show origin/<branch>:<path>`) and **paste the file contents directly
|
/var/home/jeroenschweitzer/Projects/settled-reach/<branch>/
|
||||||
into the agent prompt**. Do not tell these agents to read files — they can't.
|
```
|
||||||
For very large PRs, read the key files (new/heavily modified) and include
|
|
||||||
summaries or excerpts of minor changes. Also read and include the relevant
|
For example, the `copy` branch lives at:
|
||||||
`decisions/*.md` files these agents need for context.
|
```
|
||||||
|
/var/home/jeroenschweitzer/Projects/settled-reach/copy/content/dialogue/...
|
||||||
|
```
|
||||||
|
|
||||||
|
**All reviewer agents** (regardless of Bash access) should read source files
|
||||||
|
from the worktree path using the Read tool. This is more reliable than
|
||||||
|
`git show origin/<branch>:<path>` because:
|
||||||
|
- All agents have Read access (no Bash dependency)
|
||||||
|
- Files are always the actual branch checkout (no stale cache)
|
||||||
|
- No risk of accidentally reading from main's working directory
|
||||||
|
|
||||||
|
When constructing reviewer prompts, tell agents to read files from the
|
||||||
|
worktree path. Example instruction for agents:
|
||||||
|
|
||||||
|
```
|
||||||
|
Read the changed files from the branch worktree. The branch is checked
|
||||||
|
out at: /var/home/jeroenschweitzer/Projects/settled-reach/<branch>/
|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
Also tell agents to read relevant `decisions/*.md` files from the same
|
||||||
|
worktree (they're identical to main, but using the worktree path keeps
|
||||||
|
agents grounded in the correct directory).
|
||||||
|
|
||||||
### 4. Spawn reviewers in parallel
|
### 4. Spawn reviewers in parallel
|
||||||
|
|
||||||
@@ -182,6 +203,22 @@ tea pr close --login schweitz --repo jpmschweitzer/settled-reach <PR_NUMBER>
|
|||||||
Gitea does **not** auto-close PRs when you push a local merge — always close
|
Gitea does **not** auto-close PRs when you push a local merge — always close
|
||||||
manually with `tea pr close` after pushing.
|
manually with `tea pr close` after pushing.
|
||||||
|
|
||||||
|
### 8. Post-review team actions
|
||||||
|
|
||||||
|
If a sprint team is active and you are the team lead, handle the
|
||||||
|
review outcome:
|
||||||
|
|
||||||
|
**CHANGES_REQUESTED:**
|
||||||
|
The sprint-start lifecycle (step 9c) handles dispatching review
|
||||||
|
comments to agents. After presenting results, remind the lead:
|
||||||
|
"Review requested changes. Create tasks from the warnings/critical
|
||||||
|
issues and dispatch to idle agents, then re-push and re-review."
|
||||||
|
|
||||||
|
**APPROVED:**
|
||||||
|
The sprint-start lifecycle (step 9c) handles shutdown. After
|
||||||
|
presenting results, remind the lead: "Review approved. Proceed with
|
||||||
|
team shutdown per sprint-start step 9c."
|
||||||
|
|
||||||
## Tips from practice
|
## Tips from practice
|
||||||
|
|
||||||
- **Vendor code**: Explicitly note vendor code in the prompt so reviewers focus
|
- **Vendor code**: Explicitly note vendor code in the prompt so reviewers focus
|
||||||
|
|||||||
@@ -2,11 +2,20 @@
|
|||||||
|
|
||||||
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:
|
||||||
|
`/var/home/jeroenschweitzer/Projects/settled-reach/<branch>/`
|
||||||
|
|
||||||
|
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
|
||||||
|
every prompt. Do NOT rely on `git show` or paste file contents — agents
|
||||||
|
can read directly from the worktree.
|
||||||
|
|
||||||
## Code reviews (`server`, `client`, `ci`)
|
## Code reviews (`server`, `client`, `ci`)
|
||||||
|
|
||||||
**Hoshe (Code Quality)**
|
**Hoshe (Code Quality)**
|
||||||
- `subagent_type`: `hoshe`, `model`: `sonnet`
|
- `subagent_type`: `hoshe`, `model`: `sonnet`
|
||||||
- Prompt: Include source code and commit log. Ask Hoshe to review for:
|
- Prompt: Provide the worktree path, list of changed files, and commit log.
|
||||||
|
Tell Hoshe to read source files from the worktree, then review for:
|
||||||
- Correctness and bug risks
|
- Correctness and bug risks
|
||||||
- Error handling gaps
|
- Error handling gaps
|
||||||
- Test coverage (are new features tested?)
|
- Test coverage (are new features tested?)
|
||||||
@@ -16,44 +25,43 @@ Use `model: sonnet` for all reviewers — sufficient for review, saves cost.
|
|||||||
|
|
||||||
**Tyre (Architecture)**
|
**Tyre (Architecture)**
|
||||||
- `subagent_type`: `tyre`, `model`: `sonnet`
|
- `subagent_type`: `tyre`, `model`: `sonnet`
|
||||||
- Prompt: Include source code and commit log. Tell Tyre to read the relevant
|
- Prompt: Provide the worktree path, list of changed files, and commit log.
|
||||||
`decisions/*.md` files first, then review for:
|
Tell Tyre to read the relevant `decisions/*.md` files from the worktree
|
||||||
|
first, then review for:
|
||||||
- Architectural consistency with project decisions
|
- Architectural consistency with project decisions
|
||||||
- API/interface design quality
|
- API/interface design quality
|
||||||
- Dependency and coupling concerns
|
- Dependency and coupling concerns
|
||||||
- Scalability implications
|
- Scalability implications
|
||||||
- Whether the change respects non-negotiable baselines (D-010, D-012)
|
- Whether the change respects non-negotiable baselines (D-010, D-012)
|
||||||
- Tyre can read files directly from the branch using `git show origin/<branch>:<path>`
|
|
||||||
|
|
||||||
## Copy reviews (`copy`)
|
## Copy reviews (`copy`)
|
||||||
|
|
||||||
**Hoshe (QA)**
|
**Hoshe (QA)**
|
||||||
- `subagent_type`: `hoshe`, `model`: `sonnet`
|
- `subagent_type`: `hoshe`, `model`: `sonnet`
|
||||||
- Prompt: Include the changed files and commit log. Ask Hoshe to review for:
|
- Prompt: Provide the worktree path, list of changed files, and commit log.
|
||||||
|
Tell Hoshe to read the changed files from the worktree, then review for:
|
||||||
- Formatting consistency (markdown, file naming, frontmatter)
|
- Formatting consistency (markdown, file naming, frontmatter)
|
||||||
- Broken references or links
|
- Broken references or links
|
||||||
- Spelling and grammar
|
- Spelling and grammar
|
||||||
- File organization and structure
|
- File organization and structure
|
||||||
- Missing or orphaned files
|
- Missing or orphaned files
|
||||||
|
|
||||||
**Paula (Narrative Depth)** — NO BASH ACCESS
|
**Paula (Narrative Depth)**
|
||||||
- `subagent_type`: `paula`, `model`: `sonnet`
|
- `subagent_type`: `paula`, `model`: `sonnet`
|
||||||
- Paula cannot read from branches. You must paste file contents and decision
|
- Prompt: Provide the worktree path, list of changed files, commit log, and
|
||||||
files directly into the prompt.
|
list of relevant `decisions/*.md` files to read. Tell Paula to read all
|
||||||
- Prompt: Include full text of changed files, commit log, and relevant
|
files from the worktree using the Read tool, then review for:
|
||||||
`decisions/*.md` content. Ask Paula to review for:
|
|
||||||
- Narrative quality and character voice consistency
|
- Narrative quality and character voice consistency
|
||||||
- Whether dialogue and monologue feel authentic to the characters
|
- Whether dialogue and monologue feel authentic to the characters
|
||||||
- Consequences and stakes — do choices carry weight?
|
- Consequences and stakes — do choices carry weight?
|
||||||
- Political and interpersonal depth
|
- Political and interpersonal depth
|
||||||
- Emotional resonance — does the text make you feel something?
|
- Emotional resonance — does the text make you feel something?
|
||||||
|
|
||||||
**Miri (World Consistency)** — NO BASH ACCESS
|
**Miri (World Consistency)**
|
||||||
- `subagent_type`: `miri`, `model`: `sonnet`
|
- `subagent_type`: `miri`, `model`: `sonnet`
|
||||||
- Miri cannot read from branches. You must paste file contents and decision
|
- Prompt: Provide the worktree path, list of changed files, commit log, and
|
||||||
files directly into the prompt.
|
list of relevant `decisions/*.md` files to read. Tell Miri to read all
|
||||||
- Prompt: Include full text of changed files, commit log, and relevant
|
files from the worktree using the Read tool, then review for:
|
||||||
`decisions/*.md` content. Ask Miri to review for:
|
|
||||||
- Lore accuracy — do facts match established setting?
|
- Lore accuracy — do facts match established setting?
|
||||||
- Internal consistency across files
|
- Internal consistency across files
|
||||||
- IP originality — nothing should read as a copy from another franchise
|
- IP originality — nothing should read as a copy from another franchise
|
||||||
@@ -64,16 +72,18 @@ Use `model: sonnet` for all reviewers — sufficient for review, saves cost.
|
|||||||
|
|
||||||
**Hoshe (QA)**
|
**Hoshe (QA)**
|
||||||
- `subagent_type`: `hoshe`, `model`: `sonnet`
|
- `subagent_type`: `hoshe`, `model`: `sonnet`
|
||||||
- Prompt: Include the changed files and commit log. Ask Hoshe to review for:
|
- Prompt: Provide the worktree path, list of changed files, and commit log.
|
||||||
|
Tell Hoshe to read the changed files from the worktree, then review for:
|
||||||
- File format and naming conventions
|
- File format and naming conventions
|
||||||
- Asset organization and directory structure
|
- Asset organization and directory structure
|
||||||
- Missing or broken references in scene/resource files
|
- Missing or broken references in scene/resource files
|
||||||
- Import settings consistency
|
- Import settings consistency
|
||||||
|
|
||||||
**Araminta (Art Direction)** — HAS BASH ACCESS
|
**Araminta (Art Direction)**
|
||||||
- `subagent_type`: `araminta`, `model`: `sonnet`
|
- `subagent_type`: `araminta`, `model`: `sonnet`
|
||||||
- Prompt: Include the changed files and commit log. Tell Araminta to read
|
- Prompt: Provide the worktree path, list of changed files, and commit log.
|
||||||
the style guide and relevant design docs first, then review for:
|
Tell Araminta to read the style guide and relevant design docs from the
|
||||||
|
worktree first, then review for:
|
||||||
- Visual consistency with the established style guide
|
- Visual consistency with the established style guide
|
||||||
- Color palette adherence
|
- Color palette adherence
|
||||||
- UI pattern consistency (diegetic-first, clarity over beauty)
|
- UI pattern consistency (diegetic-first, clarity over beauty)
|
||||||
@@ -84,17 +94,17 @@ Use `model: sonnet` for all reviewers — sufficient for review, saves cost.
|
|||||||
|
|
||||||
**Hoshe (QA)**
|
**Hoshe (QA)**
|
||||||
- `subagent_type`: `hoshe`, `model`: `sonnet`
|
- `subagent_type`: `hoshe`, `model`: `sonnet`
|
||||||
- Prompt: Include the changed files and commit log. Ask Hoshe to review for:
|
- Prompt: Provide the worktree path, list of changed files, and commit log.
|
||||||
|
Tell Hoshe to read the changed files from the worktree, then review for:
|
||||||
- File format and naming conventions
|
- File format and naming conventions
|
||||||
- Audio asset organization and directory structure
|
- Audio asset organization and directory structure
|
||||||
- Missing or broken references
|
- Missing or broken references
|
||||||
- Import/bus configuration consistency
|
- Import/bus configuration consistency
|
||||||
|
|
||||||
**Ozzie (Player Experience)** — NO BASH ACCESS
|
**Ozzie (Player Experience)**
|
||||||
- `subagent_type`: `ozzie`, `model`: `sonnet`
|
- `subagent_type`: `ozzie`, `model`: `sonnet`
|
||||||
- Ozzie cannot read from branches. You must paste file contents directly
|
- Prompt: Provide the worktree path, list of changed files, and commit log.
|
||||||
into the prompt.
|
Tell Ozzie to read all files from the worktree using the Read tool, then
|
||||||
- Prompt: Include full text of changed files and commit log. Ask Ozzie to
|
|
||||||
review for:
|
review for:
|
||||||
- Emotional impact — does the audio enhance the moment?
|
- Emotional impact — does the audio enhance the moment?
|
||||||
- Atmosphere and tone — does it feel like the Commonwealth?
|
- Atmosphere and tone — does it feel like the Commonwealth?
|
||||||
|
|||||||
@@ -325,3 +325,57 @@ Output to the user:
|
|||||||
You are now the team lead. Agents work autonomously — monitor via
|
You are now the team lead. Agents work autonomously — monitor via
|
||||||
`TaskList`, communicate via `SendMessage`, and handle blockers as
|
`TaskList`, communicate via `SendMessage`, and handle blockers as
|
||||||
they arise.
|
they arise.
|
||||||
|
|
||||||
|
**When all tasks complete:** Do NOT shut down agents. The team stays
|
||||||
|
alive through the PR review cycle. Follow step 9 (post-work lifecycle).
|
||||||
|
|
||||||
|
### 9. Post-work lifecycle
|
||||||
|
|
||||||
|
When all tasks are complete (TaskList shows all completed):
|
||||||
|
|
||||||
|
#### 9a. Commit and push
|
||||||
|
|
||||||
|
Run `/git-commit` to commit all changes, then `/pr-push` to create or
|
||||||
|
update the PR. Do NOT shut down agents — the team stays alive for review.
|
||||||
|
|
||||||
|
#### 9b. Review
|
||||||
|
|
||||||
|
Run `/pr-review` to spawn temporary reviewers. Wait for results.
|
||||||
|
|
||||||
|
#### 9c. Handle review outcome
|
||||||
|
|
||||||
|
**If CHANGES_REQUESTED:**
|
||||||
|
|
||||||
|
1. Parse the review comment table (from the Gitea PR comment or the
|
||||||
|
review output). Extract each warning/critical issue with:
|
||||||
|
- File path and approximate line
|
||||||
|
- Severity (critical / warning / suggestion)
|
||||||
|
- Description
|
||||||
|
|
||||||
|
2. Create a task per warning/critical issue:
|
||||||
|
```
|
||||||
|
TaskCreate(
|
||||||
|
subject: "Review: {short description}",
|
||||||
|
description: "{full issue description from review table, including
|
||||||
|
file path, severity, and reviewer name}",
|
||||||
|
activeForm: "Fixing review comment: {short description}"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
Skip suggestion-severity items unless they are trivial (1-line fixes).
|
||||||
|
|
||||||
|
3. Dispatch to idle agents: send each a message via SendMessage telling
|
||||||
|
them to check TaskList for new review-fix tasks. Agents claim and
|
||||||
|
work tasks as usual.
|
||||||
|
|
||||||
|
4. After all review-fix tasks are complete, re-run `/git-commit` then
|
||||||
|
`/pr-push` to update the PR. Then re-run `/pr-review`.
|
||||||
|
|
||||||
|
5. Repeat this loop until review returns APPROVED.
|
||||||
|
|
||||||
|
**If APPROVED:**
|
||||||
|
|
||||||
|
1. Send `shutdown_request` to all sprint agents.
|
||||||
|
2. Wait for all `shutdown_response` confirmations.
|
||||||
|
3. Call `TeamDelete` to clean up.
|
||||||
|
4. Report: "Sprint {N} {team} complete. PR #{X} approved and ready for
|
||||||
|
merge on main."
|
||||||
|
|||||||
@@ -88,9 +88,10 @@ When all Round N tasks are complete:
|
|||||||
|
|
||||||
Wrap-up sequence:
|
Wrap-up sequence:
|
||||||
1. Qatux produces final `workshop-outcomes.md` from accumulated notes
|
1. Qatux produces final `workshop-outcomes.md` from accumulated notes
|
||||||
2. If SI is present, SI creates tickets from decided items
|
2. Qatux creates or updates diagrams (via `/d2-diagram`) for any new D-records produced by the workshop
|
||||||
3. Send shutdown_request to all agents (qatux and si last, after they finish their output tasks)
|
3. If SI is present, SI creates tickets from decided items
|
||||||
4. TeamDelete to clean up
|
4. Send shutdown_request to all agents (qatux and si last, after they finish their output tasks)
|
||||||
|
5. TeamDelete to clean up
|
||||||
|
|
||||||
## Agent Type Reference
|
## Agent Type Reference
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ renderer/output/*.png
|
|||||||
# Database (shared across worktrees at ../settledreach.db, not tracked)
|
# Database (shared across worktrees at ../settledreach.db, not tracked)
|
||||||
db/commonwealth.db*
|
db/commonwealth.db*
|
||||||
|
|
||||||
|
# Frame0 ID mapping files (ephemeral, per-machine)
|
||||||
|
*.idmap.json
|
||||||
|
|
||||||
# Python
|
# Python
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
|||||||
@@ -6,6 +6,77 @@ Format based on [Keep a Changelog](https://keepachangelog.com/).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Knowledge Flow & NPC Boundaries workshop — 5 D-records (D-079–D-083) covering grant architecture, NPC-to-NPC propagation, unprompted disclosure, NPC information boundaries MVP, contradiction detection pipeline
|
||||||
|
- 7 knowledge graph implementation tickets (#545–#551) with full dependency chain and line estimates
|
||||||
|
- Contradiction monologue content ticket (#552) for Sera/Kael FRIEND arc
|
||||||
|
- Sprint 17 completion proofs: contradiction detection fires, NPC-to-NPC knowledge transfers
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Sprint 17 briefings updated with workshop results — server (14 tickets), copy (2 tickets), client (2), visual (1)
|
||||||
|
- Q-024 (gossip timing), Q-025 (KG memory), Q-026 (contradiction detection) closed
|
||||||
|
- Sprint 16 closed (8/8 done)
|
||||||
|
|
||||||
|
### Previously added
|
||||||
|
- 3D sprite render pipeline — Camera3D at D-019 angle (-72.5° from horizontal), three-point studio lighting rig, orthographic projection, resolution chain 1024→256→64
|
||||||
|
- Generic NPC capsule model (24×32px footprint per D-044) and structural wall model for pipeline validation
|
||||||
|
- Test sprites: 8 runtime 64px sprites (NPC + wall × 4 directions) deployed to client/assets/sprites/
|
||||||
|
- Pipeline documentation (renderer/README.md) — camera spec, lighting rig, resolution chain, model authoring guide
|
||||||
|
- DialogueResponse verb handler — players pick dialogue options and receive follow-up lines via full D-028 four-layer pipeline (#539)
|
||||||
|
- Trust-gated gossip verification — integration tests confirm Secret/Real/Surface tier gating per D-075 (#171)
|
||||||
|
- Line variety tracker wiring — DialogueCooldownTracker prevents repeat lines within 600-tick window (#338)
|
||||||
|
- DialogueResponse cross-language fixture for GDScript testing
|
||||||
|
- Sprint team lifecycle through PR review — teams stay alive for commit → push → review → fix loop → approve → shutdown
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- PR #59 review: stale mood vocabulary updated in line-pool-format.md, style-guide, and content-directory-structure.md to post-Sprint 14 values
|
||||||
|
- PR #59 review: orphaned location-scoped IDs in maintenance-tech.yaml comments and smuggler-inventory.yaml cross-references updated to NPC-scoped
|
||||||
|
- PR #59 review: Lera Sessik tenure corrected from "twelve years" to "eighteen years", NPC header fixed
|
||||||
|
- PR #59 review: ring-operative.yaml fact_id corrected from `location.surveillance_gaps` to `investigation.surveillance_gaps`
|
||||||
|
- Dialogue systems moved from BridgePlugin to NpcPlugin — game logic registers where it belongs (#538)
|
||||||
|
- Schedule ambiguity: emit_observation_events now has explicit .before(advance_tick) constraint
|
||||||
|
- process_dialogue_response updates ActiveDialogue tick and InteractionMemory on follow-up
|
||||||
|
- DialogueResponse range check added (CLOSE_RANGE, matching Talk/Confront pattern)
|
||||||
|
- Weighted selection fallback replaced with unreachable!() — dead code removed
|
||||||
|
- assert!(false) → panic!() in serialization tests (clippy)
|
||||||
|
- SetFacing and TeleportToHub added to roundtrip test coverage
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Shared run_dialogue_pipeline() helper eliminates ~60 lines of duplication between Talk and DialogueResponse systems
|
||||||
|
- Dialogue and monologue line IDs migrated from location-scoped (the-terminal_d_039) to NPC-scoped (kael-davan_d_001) namespace — each NPC has an independent sequence per D-035 (#542)
|
||||||
|
- DialogueCooldownTracker documented as per-player-global by design (NPC-scoped line IDs per D-035 prevent collision)
|
||||||
|
- CONFRONTATION_LINES marked TODO for migration to D-028/D-035 content pipeline
|
||||||
|
- pr-push and pr-review skills updated with team lifecycle awareness
|
||||||
|
|
||||||
|
## [v0.1.15] — 2026-02-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Sprint 16 "Converse" briefings — 8 tickets across server/client/copy/visual teams
|
||||||
|
- 19 UI wireframes — HUD, dialogue, monologue, popups, menus in v0.1 and v1.0 variants with D-record cross-references
|
||||||
|
- d2-diagram skill — text-to-diagram generation with project defaults (theme 200, dagre, PNG)
|
||||||
|
- frame0-wireframe skill — UI wireframing via Frame0 HTTP API, replaces MCP dependency with bash+curl
|
||||||
|
- 16 decision diagrams — architecture, data-flow, entity, state, and UI categories covering all project decisions
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- frame0-wireframe skill rewritten — JSON-as-truth workflow with frame0-sync.py, batch export, renderer-only guidance
|
||||||
|
- pr-review skill — all reviewer agents now use worktree paths instead of git show
|
||||||
|
- Dialogue panel is always visible as permanent insert UI element (D-061)
|
||||||
|
- Makefile: check-protocol target verifies server/client protocol versions match before build
|
||||||
|
- D-035 amended: line ID namespace changed from location-scoped to NPC-scoped (Sprint 15)
|
||||||
|
- Tilemap z-layer filtering — FloorTiles renders z=0 only, z=1/z>1 reserved for future layer nodes (#71, D-049)
|
||||||
|
- Entity 24x32 footprint per D-044 visual hierarchy — split ENTITY_SIZE into WIDTH/HEIGHT with separate offsets (#72)
|
||||||
|
- Follow target stub on GameState — `follow_target_id` field ready for server #241 Follow verb
|
||||||
|
- Manual exponential camera smoothing — CAMERA_SMOOTHING_SPEED constant (8.0), same lerp pattern as entity renderer (#117)
|
||||||
|
- 31 new Sprint 15 validation tests — camera smoothing, UI framework z-layers, entity footprint, Sprint 14 regressions
|
||||||
|
- SpatialIndex trait with naive Vec implementation — entities_in_range, entities_at, update methods with Manhattan distance (#340)
|
||||||
|
- NPC generation pipeline — procedural seeding of all 10 D-024 axes via SimRng with constraint validation (#92)
|
||||||
|
- Personality and tell system — 5 tell categories (Nervous, Angry, Friendly, Guarded, RoutineDeviation) derived from NPC axis values each tick (#90)
|
||||||
|
- Tolerance threshold monitoring — ToleranceBreachEvent on stress exceeding per-NPC threshold, mood FSM integration (#105)
|
||||||
|
- Routine deviation detection — RoutineDeviationEvent on wrong location/activity for day phase, absence detection, pathfinding-aware (#243)
|
||||||
|
- Follow mechanic — Follow verb, proximity/LOS tracking, double-frequency observation events, NPC suspicion accumulation, configurable thresholds (#241)
|
||||||
|
- Monologue event triggers — observe_npc, hear_sound, observe_anomaly, witness_interaction, post_conversation with D-035 context tags (#119)
|
||||||
|
- Protocol v13 — tell_state on VisibleEntity, follow_state on ObserverSnapshot, Follow verb
|
||||||
|
|
||||||
## [v0.1.14] — 2026-02-21
|
## [v0.1.14] — 2026-02-21
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ docs/
|
|||||||
briefings/ # Per-agent context briefings (maintained by Qatux)
|
briefings/ # Per-agent context briefings (maintained by Qatux)
|
||||||
architecture/ # Technical architecture documents
|
architecture/ # Technical architecture documents
|
||||||
design/ # Game design documents
|
design/ # Game design documents
|
||||||
|
diagrams/ # d2 source + PNG renders (architecture, data-flow, entity, state, ui)
|
||||||
sprints/ # Sprint briefings per team (server.md, client.md, copy.md, joint.md, etc.)
|
sprints/ # Sprint briefings per team (server.md, client.md, copy.md, joint.md, etc.)
|
||||||
workshops/ # Workshop briefs and outputs (per-workshop subdirectories)
|
workshops/ # Workshop briefs and outputs (per-workshop subdirectories)
|
||||||
db/
|
db/
|
||||||
@@ -144,6 +145,7 @@ Key rules:
|
|||||||
### 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)
|
||||||
|
- 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 `db/connectors/ticket` CLI or `/ticket` skill
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
GODOT := $(shell command -v godot4 2>/dev/null || command -v godot 2>/dev/null)
|
GODOT := $(shell command -v godot4 2>/dev/null || command -v godot 2>/dev/null)
|
||||||
|
|
||||||
.PHONY: help setup build client server game stop test lint ci ci-client ci-server clean \
|
.PHONY: help setup build check-protocol client server game stop test lint ci ci-client ci-server clean \
|
||||||
decisions-sync decisions-coverage decisions-active decisions-orphan \
|
decisions-sync decisions-coverage decisions-active decisions-orphan \
|
||||||
db-backup db-install validate-content content-ron check-fact-ids setup-hooks \
|
db-backup db-install validate-content content-ron check-fact-ids setup-hooks \
|
||||||
pre-pr pre-pr-lint pre-pr-build pre-pr-test pre-pr-validate pre-pr-fixtures \
|
pre-pr pre-pr-lint pre-pr-build pre-pr-test pre-pr-validate pre-pr-fixtures \
|
||||||
@@ -28,6 +28,7 @@ help:
|
|||||||
@echo " make ci Run full CI pipeline locally"
|
@echo " make ci Run full CI pipeline locally"
|
||||||
@echo " make ci-client Run client CI checks"
|
@echo " make ci-client Run client CI checks"
|
||||||
@echo " make ci-server Run server CI checks"
|
@echo " make ci-server Run server CI checks"
|
||||||
|
@echo " make check-protocol Verify server/client protocol versions match"
|
||||||
@echo " make clean Remove build artifacts and caches"
|
@echo " make clean Remove build artifacts and caches"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo " make db-backup Backup shared database to git (main only)"
|
@echo " make db-backup Backup shared database to git (main only)"
|
||||||
@@ -82,7 +83,16 @@ setup-hooks:
|
|||||||
|
|
||||||
# --- Build ---
|
# --- Build ---
|
||||||
|
|
||||||
build: build-server build-client
|
check-protocol:
|
||||||
|
@SERVER_V=$$(grep 'pub const PROTOCOL_VERSION' server/src/bridge/types.rs | sed 's/.*= *//;s/[^0-9]//g'); \
|
||||||
|
CLIENT_V=$$(grep 'const PROTOCOL_VERSION' client/scripts/protocol/protocol.gd | sed 's/.*= *//;s/[^0-9]//g'); \
|
||||||
|
if [ "$$SERVER_V" != "$$CLIENT_V" ]; then \
|
||||||
|
echo "ERROR: Protocol version mismatch — server=$$SERVER_V, client=$$CLIENT_V"; \
|
||||||
|
echo " Fix: update client/scripts/protocol/protocol.gd to match server/src/bridge/types.rs"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
build: check-protocol build-server build-client
|
||||||
|
|
||||||
build-server:
|
build-server:
|
||||||
cd server && cargo build
|
cd server && cargo build
|
||||||
@@ -94,7 +104,7 @@ build-client:
|
|||||||
# --- Run ---
|
# --- Run ---
|
||||||
|
|
||||||
server:
|
server:
|
||||||
cd server && cargo run
|
cd server && cargo run --bin settled-reach-server
|
||||||
|
|
||||||
client:
|
client:
|
||||||
@test -n "$(GODOT)" || { echo "Godot not found. Run 'make setup' first."; exit 1; }
|
@test -n "$(GODOT)" || { echo "Godot not found. Run 'make setup' first."; exit 1; }
|
||||||
@@ -103,7 +113,7 @@ client:
|
|||||||
game: stop build
|
game: stop build
|
||||||
@test -n "$(GODOT)" || { echo "Godot not found. Run 'make setup' first."; exit 1; }
|
@test -n "$(GODOT)" || { echo "Godot not found. Run 'make setup' first."; exit 1; }
|
||||||
@echo "Starting server..."
|
@echo "Starting server..."
|
||||||
@cd server && cargo run &
|
@cd server && cargo run --bin settled-reach-server &
|
||||||
@sleep 2
|
@sleep 2
|
||||||
@echo "Starting client..."
|
@echo "Starting client..."
|
||||||
@SR_LIVE=1 $(GODOT) --path client
|
@SR_LIVE=1 $(GODOT) --path client
|
||||||
@@ -290,7 +300,7 @@ perf-baseline:
|
|||||||
|
|
||||||
debug-schedule:
|
debug-schedule:
|
||||||
@echo "Dumping bevy_ecs schedule graph..."
|
@echo "Dumping bevy_ecs schedule graph..."
|
||||||
@cd server && cargo run -- --dump-schedule
|
@cd server && cargo run --bin settled-reach-server -- --dump-schedule
|
||||||
|
|
||||||
content-ron:
|
content-ron:
|
||||||
cd tooling/content-converter && cargo build --release
|
cd tooling/content-converter && cargo build --release
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="oggvorbisstr"
|
||||||
|
type="AudioStreamOggVorbis"
|
||||||
|
uid="uid://dfy0ye3srawos"
|
||||||
|
path="res://.godot/imported/amb_bar_layer.ogg-8e32a9c679a33f27226744a176d7d405.oggvorbisstr"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/audio/amb_bar_layer.ogg"
|
||||||
|
dest_files=["res://.godot/imported/amb_bar_layer.ogg-8e32a9c679a33f27226744a176d7d405.oggvorbisstr"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
loop=false
|
||||||
|
loop_offset=0
|
||||||
|
bpm=0
|
||||||
|
beat_count=0
|
||||||
|
bar_beats=4
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="oggvorbisstr"
|
||||||
|
type="AudioStreamOggVorbis"
|
||||||
|
uid="uid://cne005dbmwt6d"
|
||||||
|
path="res://.godot/imported/amb_corridor_layer.ogg-f14ba54010b129b65b0d248444331998.oggvorbisstr"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/audio/amb_corridor_layer.ogg"
|
||||||
|
dest_files=["res://.godot/imported/amb_corridor_layer.ogg-f14ba54010b129b65b0d248444331998.oggvorbisstr"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
loop=false
|
||||||
|
loop_offset=0
|
||||||
|
bpm=0
|
||||||
|
beat_count=0
|
||||||
|
bar_beats=4
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="oggvorbisstr"
|
||||||
|
type="AudioStreamOggVorbis"
|
||||||
|
uid="uid://b6ycjdvxpphfa"
|
||||||
|
path="res://.godot/imported/amb_station_base.ogg-8056e0ae231edecc9ec0e49bb90136b7.oggvorbisstr"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/audio/amb_station_base.ogg"
|
||||||
|
dest_files=["res://.godot/imported/amb_station_base.ogg-8056e0ae231edecc9ec0e49bb90136b7.oggvorbisstr"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
loop=false
|
||||||
|
loop_offset=0
|
||||||
|
bpm=0
|
||||||
|
beat_count=0
|
||||||
|
bar_beats=4
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="oggvorbisstr"
|
||||||
|
type="AudioStreamOggVorbis"
|
||||||
|
uid="uid://dw2pokvg5d7v2"
|
||||||
|
path="res://.godot/imported/amb_workplace_layer.ogg-580ba32198a3f6c782381c66f9520e63.oggvorbisstr"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/audio/amb_workplace_layer.ogg"
|
||||||
|
dest_files=["res://.godot/imported/amb_workplace_layer.ogg-580ba32198a3f6c782381c66f9520e63.oggvorbisstr"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
loop=false
|
||||||
|
loop_offset=0
|
||||||
|
bpm=0
|
||||||
|
beat_count=0
|
||||||
|
bar_beats=4
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="oggvorbisstr"
|
||||||
|
type="AudioStreamOggVorbis"
|
||||||
|
uid="uid://da81bx5y6iw87"
|
||||||
|
path="res://.godot/imported/sfx_footstep_metal_run.ogg-0d448f29204d35d4133f5314a179d054.oggvorbisstr"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/audio/sfx_footstep_metal_run.ogg"
|
||||||
|
dest_files=["res://.godot/imported/sfx_footstep_metal_run.ogg-0d448f29204d35d4133f5314a179d054.oggvorbisstr"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
loop=false
|
||||||
|
loop_offset=0
|
||||||
|
bpm=0
|
||||||
|
beat_count=0
|
||||||
|
bar_beats=4
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="oggvorbisstr"
|
||||||
|
type="AudioStreamOggVorbis"
|
||||||
|
uid="uid://d14dx3q0qd483"
|
||||||
|
path="res://.godot/imported/sfx_footstep_metal_walk.ogg-af30f14f9f80059ea65ad6e78234cc05.oggvorbisstr"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/audio/sfx_footstep_metal_walk.ogg"
|
||||||
|
dest_files=["res://.godot/imported/sfx_footstep_metal_walk.ogg-af30f14f9f80059ea65ad6e78234cc05.oggvorbisstr"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
loop=false
|
||||||
|
loop_offset=0
|
||||||
|
bpm=0
|
||||||
|
beat_count=0
|
||||||
|
bar_beats=4
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
importer="oggvorbisstr"
|
importer="oggvorbisstr"
|
||||||
type="AudioStreamOggVorbis"
|
type="AudioStreamOggVorbis"
|
||||||
uid="uid://caeuztw9ah5h5l"
|
uid="uid://cow7symyvpmal"
|
||||||
path="res://.godot/imported/sfx_npc_murmur.ogg-bfd7592cfea1b592d89f107b6cd33838.oggvorbisstr"
|
path="res://.godot/imported/sfx_npc_murmur.ogg-bfd7592cfea1b592d89f107b6cd33838.oggvorbisstr"
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="font_data_dynamic"
|
||||||
|
type="FontFile"
|
||||||
|
uid="uid://qbg8rfnetlqn"
|
||||||
|
path="res://.godot/imported/Michroma-Regular.ttf-928de7d8513fc5249047ef3681175fc2.fontdata"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/fonts/Michroma-Regular.ttf"
|
||||||
|
dest_files=["res://.godot/imported/Michroma-Regular.ttf-928de7d8513fc5249047ef3681175fc2.fontdata"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
Rendering=null
|
||||||
|
antialiasing=1
|
||||||
|
generate_mipmaps=false
|
||||||
|
disable_embedded_bitmaps=true
|
||||||
|
multichannel_signed_distance_field=false
|
||||||
|
msdf_pixel_range=8
|
||||||
|
msdf_size=48
|
||||||
|
allow_system_fallback=true
|
||||||
|
force_autohinter=false
|
||||||
|
modulate_color_glyphs=false
|
||||||
|
hinting=1
|
||||||
|
subpixel_positioning=4
|
||||||
|
keep_rounding_remainders=true
|
||||||
|
oversampling=0.0
|
||||||
|
Fallbacks=null
|
||||||
|
fallbacks=[]
|
||||||
|
Compress=null
|
||||||
|
compress=true
|
||||||
|
preload=[]
|
||||||
|
language_support={}
|
||||||
|
script_support={}
|
||||||
|
opentype_features={}
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
@@ -43,5 +43,5 @@ passive_opacity: 0.9
|
|||||||
# ENTRY TIMING
|
# ENTRY TIMING
|
||||||
# All entry types share the same lifetime and fade duration.
|
# All entry types share the same lifetime and fade duration.
|
||||||
# ============================================================
|
# ============================================================
|
||||||
entry_lifetime_seconds: 15.0
|
entry_lifetime_seconds: 45.0
|
||||||
entry_fade_seconds: 3.0
|
entry_fade_seconds: 5.0
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128"><rect width="128" height="128" fill="#1a1a2e"/></svg>
|
||||||
|
After Width: | Height: | Size: 119 B |
@@ -0,0 +1,43 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://caxeq5xayr0iy"
|
||||||
|
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://icon.svg"
|
||||||
|
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
|
svg/scale=1.0
|
||||||
|
editor/scale_with_editor_scale=false
|
||||||
|
editor/convert_colors_with_editor_theme=false
|
||||||
@@ -44,7 +44,7 @@ const FILTER_CUTOFF_DEFAULT := 20500.0
|
|||||||
# --- D-073: Zone crossfade constants ---
|
# --- D-073: Zone crossfade constants ---
|
||||||
const CROSSFADE_DURATION := 1.8 # D-073: 1.5-2s spec, mid-range
|
const CROSSFADE_DURATION := 1.8 # D-073: 1.5-2s spec, mid-range
|
||||||
|
|
||||||
# Maps server zone_id strings to ambient asset keys (filenames in res://audio/).
|
# Maps server zone_id strings to ambient asset keys (filenames in res://assets/audio/).
|
||||||
# Hub and Workplace intentionally share the same ambient layer (amb_hub_layer) —
|
# Hub and Workplace intentionally share the same ambient layer (amb_hub_layer) —
|
||||||
# they are the same location type, so hub→workplace transition is a same-asset no-op
|
# they are the same location type, so hub→workplace transition is a same-asset no-op
|
||||||
# (old_asset != new_asset guard skips the fade-out). Sprint brief consolidates
|
# (old_asset != new_asset guard skips the fade-out). Sprint brief consolidates
|
||||||
@@ -112,15 +112,15 @@ func _setup_buses() -> void:
|
|||||||
# --- Asset registry (D-068 directory-scan pattern) ---
|
# --- Asset registry (D-068 directory-scan pattern) ---
|
||||||
|
|
||||||
func _scan_registry() -> void:
|
func _scan_registry() -> void:
|
||||||
_scan_dir("res://audio/")
|
_scan_dir("res://assets/audio/")
|
||||||
print("AudioManager: %d assets registered" % _registry.size())
|
print("AudioManager: %d assets registered" % _registry.size())
|
||||||
|
|
||||||
|
|
||||||
func _scan_dir(path: String) -> void:
|
func _scan_dir(path: String) -> void:
|
||||||
var dir := DirAccess.open(path)
|
var dir := DirAccess.open(path)
|
||||||
if dir == null:
|
if dir == null:
|
||||||
if path == "res://audio/":
|
if path == "res://assets/audio/":
|
||||||
print("AudioManager: res://audio/ not found — all play methods no-op")
|
print("AudioManager: res://assets/audio/ not found — all play methods no-op")
|
||||||
return
|
return
|
||||||
dir.list_dir_begin()
|
dir.list_dir_begin()
|
||||||
var file_name := dir.get_next()
|
var file_name := dir.get_next()
|
||||||
@@ -197,7 +197,7 @@ func stop_all_loops() -> void:
|
|||||||
|
|
||||||
# --- Audio asset registry: event type → asset key (D-018, #125) ---
|
# --- Audio asset registry: event type → asset key (D-018, #125) ---
|
||||||
# Maps server-sent sound event_type strings to audio asset keys.
|
# Maps server-sent sound event_type strings to audio asset keys.
|
||||||
# Keys match filename stems in res://audio/ (scanned by _scan_registry).
|
# Keys match filename stems in res://assets/audio/ (scanned by _scan_registry).
|
||||||
# Audio assets per D-038: footstep variants (walk / run), NPC murmur (D-072, #532).
|
# Audio assets per D-038: footstep variants (walk / run), NPC murmur (D-072, #532).
|
||||||
# Missing assets no-op gracefully (D-038 fallback pattern).
|
# Missing assets no-op gracefully (D-038 fallback pattern).
|
||||||
const SOUND_EVENT_ASSETS: Dictionary = {
|
const SOUND_EVENT_ASSETS: Dictionary = {
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ var visibility_sectors: Dictionary = {} # Vector2i -> "Forward"/"Peripheral"
|
|||||||
# refined when the server assigns explicit player entity IDs).
|
# refined when the server assigns explicit player entity IDs).
|
||||||
var player_entity_id: int = 1
|
var player_entity_id: int = 1
|
||||||
|
|
||||||
|
# #241: Follow target — entity_id of the NPC the player is following, -1 when not following.
|
||||||
|
# Stub for server ticket #241 (Follow verb). Client reads this for camera/UI behavior.
|
||||||
|
var follow_target_id: int = -1
|
||||||
|
|
||||||
# v4 fields (#404/#405)
|
# v4 fields (#404/#405)
|
||||||
var nearby_interactions: Array = [] # [{entity_id, entity_type, distance, verbs: [{kind, label, priority, available}]}]
|
var nearby_interactions: Array = [] # [{entity_id, entity_type, distance, verbs: [{kind, label, priority, available}]}]
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://rqfw0ycyb4c6
|
||||||
@@ -91,14 +91,19 @@ const FACING_INDICATOR_SIZE: float = 6.0
|
|||||||
const FACING_INDICATOR_OFFSET: float = 14.0
|
const FACING_INDICATOR_OFFSET: float = 14.0
|
||||||
|
|
||||||
# D-076 (OQ-29 resolution): Dialogue box max-width in pixels.
|
# D-076 (OQ-29 resolution): Dialogue box max-width in pixels.
|
||||||
# 640px = 20 × TILE_SIZE (32px) — grid-aligned, ~33% of 1920px viewport.
|
# Raised from D-076 default (640px) to 1200px for readability.
|
||||||
# Tyre architecture review 2026-02-19: readability over max-width; fits
|
# Tyre architecture review 2026-02-19: readability over max-width; fits
|
||||||
# two columns of text comfortably, leaves world game visible alongside.
|
# two columns of text comfortably, leaves world game visible alongside.
|
||||||
const DIALOGUE_MAX_WIDTH: int = 640
|
const DIALOGUE_MAX_WIDTH: int = 1200
|
||||||
|
|
||||||
# Default camera zoom — used as fallback when get_camera_2d() returns null
|
# Default camera zoom — used as fallback when get_camera_2d() returns null
|
||||||
const CAMERA_DEFAULT_ZOOM: Vector2 = Vector2(2.0, 2.0)
|
const CAMERA_DEFAULT_ZOOM: Vector2 = Vector2(2.0, 2.0)
|
||||||
|
|
||||||
|
# #117: Camera smoothing speed — exponential interpolation via manual lerp in main.gd.
|
||||||
|
# Same pattern as EntityRenderer.LERP_SPEED. At 8.0: ~55% convergence after 0.1s.
|
||||||
|
# Slightly softer than entity movement (12.0) for a touch of cinematic camera lag.
|
||||||
|
const CAMERA_SMOOTHING_SPEED: float = 8.0
|
||||||
|
|
||||||
# #517: Implant UI font color grading — avoid pure white, project through a lens
|
# #517: Implant UI font color grading — avoid pure white, project through a lens
|
||||||
const IMPLANT_TEXT_COLOR: Color = Color("#E0F7FA") # Cyan-white — primary text
|
const IMPLANT_TEXT_COLOR: Color = Color("#E0F7FA") # Cyan-white — primary text
|
||||||
const IMPLANT_TEXT_DIM: Color = Color("#9EBFC4") # Dimmed variant — secondary text
|
const IMPLANT_TEXT_DIM: Color = Color("#9EBFC4") # Dimmed variant — secondary text
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ var _last_dialogue_tick: int = -1
|
|||||||
var _last_confrontation_tick: int = -1 # Deduplicate confrontation_monologue signals within same tick
|
var _last_confrontation_tick: int = -1 # Deduplicate confrontation_monologue signals within same tick
|
||||||
var _known_recognition_ids: Dictionary = {} # D-067: entity_ids that have already chimed
|
var _known_recognition_ids: Dictionary = {} # D-067: entity_ids that have already chimed
|
||||||
var _flash_rect: ColorRect = null # #502/#501: ephemeral screen flash overlay (shared: teleport preempts amber)
|
var _flash_rect: ColorRect = null # #502/#501: ephemeral screen flash overlay (shared: teleport preempts amber)
|
||||||
var _teleport_in_progress: bool = false # #501: defer smoothing re-enable by one frame after teleport
|
var _teleport_in_progress: bool = false # #501/#117: forces camera snap (not lerp) on next _process frame
|
||||||
var _pending_record_inputs: Array = [] # #507: accumulates server-bound inputs across frames; flushed into record_tick() on snapshot arrival
|
var _pending_record_inputs: Array = [] # #507: accumulates server-bound inputs across frames; flushed into record_tick() on snapshot arrival
|
||||||
var _current_zone: String = "" # D-073 (#529): zone tracking for ambient crossfades
|
var _current_zone: String = "" # D-073 (#529): zone tracking for ambient crossfades
|
||||||
|
|
||||||
@@ -35,13 +35,10 @@ const LISTENING_FOCUS_TICKS: int = 30 # D-071: stationary ticks before Listenin
|
|||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
print("The Settled Reach — client initialized")
|
print("The Settled Reach — client initialized")
|
||||||
|
|
||||||
# Disable camera smoothing during init. Camera2D's position_smoothing
|
# #117: Manual lerp approach — disable Godot's built-in Camera2D smoothing.
|
||||||
# lerps an internal smoothed_camera_pos toward global_position each frame.
|
# We lerp camera.global_position directly in _process() using CAMERA_SMOOTHING_SPEED,
|
||||||
# That smoothed position initializes at (0,0) — the Camera2D's default in
|
# matching entity_renderer.gd's exponential smoothing pattern. Built-in smoothing
|
||||||
# the .tscn. Even after we set global_position to the player coords,
|
# would conflict because we'd be setting global_position to the target every frame.
|
||||||
# smoothing causes the viewport to still show (0,0) on the first rendered
|
|
||||||
# frame because the lerp hasn't converged. With smoothing OFF, the viewport
|
|
||||||
# uses global_position directly. Re-enabled in _process() after anchor.
|
|
||||||
camera.position_smoothing_enabled = false
|
camera.position_smoothing_enabled = false
|
||||||
|
|
||||||
# Connect to simulation (test mode sets CONNECTED immediately)
|
# Connect to simulation (test mode sets CONNECTED immediately)
|
||||||
@@ -50,7 +47,7 @@ func _ready() -> void:
|
|||||||
# 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
|
||||||
# handles it. No reset_smoothing() needed: smoothing is OFF.
|
# handles it via the lerp block in _process().
|
||||||
var first_snapshot: Variant = SimBridge.poll_snapshot()
|
var first_snapshot: Variant = SimBridge.poll_snapshot()
|
||||||
if first_snapshot != null:
|
if first_snapshot != null:
|
||||||
GameState.apply_snapshot(first_snapshot)
|
GameState.apply_snapshot(first_snapshot)
|
||||||
@@ -70,7 +67,7 @@ func _ready() -> void:
|
|||||||
SimBridge.connection_state_changed.connect(_on_connection_state_changed)
|
SimBridge.connection_state_changed.connect(_on_connection_state_changed)
|
||||||
|
|
||||||
|
|
||||||
func _process(_delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
# Main game loop: poll snapshot, apply state, flush input
|
# Main game loop: poll snapshot, apply state, flush input
|
||||||
var snapshot: Variant = SimBridge.poll_snapshot()
|
var snapshot: Variant = SimBridge.poll_snapshot()
|
||||||
if snapshot != null:
|
if snapshot != null:
|
||||||
@@ -159,23 +156,18 @@ func _process(_delta: float) -> void:
|
|||||||
_consume_conversation_ended()
|
_consume_conversation_ended()
|
||||||
_consume_dialogue_response()
|
_consume_dialogue_response()
|
||||||
|
|
||||||
# Track camera to player position every frame (D-015: locked, no panning)
|
# Track camera to player (D-015: locked, fixed-north).
|
||||||
|
# #117: Manual exponential smoothing — same pattern as EntityRenderer.LERP_SPEED.
|
||||||
|
# Teleport (flag set by _teleport_transition): snap immediately, resume lerp next frame.
|
||||||
|
# Init: camera already snapped in _ready() or late-anchor path above.
|
||||||
if _camera_anchored:
|
if _camera_anchored:
|
||||||
camera.global_position = GameState.player_position * Constants.TILE_SIZE
|
var target := GameState.player_position * Constants.TILE_SIZE
|
||||||
|
|
||||||
# Re-enable smoothing after the first anchored frame. The frame that just
|
|
||||||
# rendered used smoothing=OFF (correct viewport from frame one). Now we
|
|
||||||
# turn smoothing back on and sync its internal state so subsequent frames
|
|
||||||
# get smooth camera tracking during gameplay.
|
|
||||||
# #501: Skip re-enable during teleport — _teleport_transition() disables
|
|
||||||
# smoothing for a clean camera snap. Defer by one frame to avoid the
|
|
||||||
# re-enable block in the same _process() call undoing the snap.
|
|
||||||
if _camera_anchored and not camera.position_smoothing_enabled:
|
|
||||||
if _teleport_in_progress:
|
if _teleport_in_progress:
|
||||||
|
camera.global_position = target
|
||||||
_teleport_in_progress = false
|
_teleport_in_progress = false
|
||||||
else:
|
else:
|
||||||
camera.position_smoothing_enabled = true
|
var weight := 1.0 - exp(-Constants.CAMERA_SMOOTHING_SPEED * delta)
|
||||||
camera.reset_smoothing()
|
camera.global_position = camera.global_position.lerp(target, weight)
|
||||||
|
|
||||||
# Send queued input to simulation
|
# Send queued input to simulation
|
||||||
# #507: Server-bound inputs are accumulated into _pending_record_inputs across frames.
|
# #507: Server-bound inputs are accumulated into _pending_record_inputs across frames.
|
||||||
@@ -250,22 +242,16 @@ func _play_close_sound_events() -> void:
|
|||||||
# D-067: Recognition chime — fires sfx_monologue_chime when a fog entity
|
# D-067: Recognition chime — fires sfx_monologue_chime when a fog entity
|
||||||
# enters the cognitive delay recognition queue for the first time.
|
# enters the cognitive delay recognition queue for the first time.
|
||||||
# "The chime marks the character's attention shifting" (D-067).
|
# "The chime marks the character's attention shifting" (D-067).
|
||||||
# Entities that complete recognition (leave pending_recognitions) are removed
|
# IDs persist for the session — one chime per entity, no re-trigger on
|
||||||
# from _known_recognition_ids so they can chime again if re-encountered.
|
# fog oscillation or server re-send. Cleared on room change (teleport).
|
||||||
func _play_recognition_chimes() -> void:
|
func _play_recognition_chimes() -> void:
|
||||||
var active_ids: Dictionary = {}
|
|
||||||
for rec in GameState.pending_recognitions:
|
for rec in GameState.pending_recognitions:
|
||||||
if not rec is Dictionary or not rec.has("entity_id"):
|
if not rec is Dictionary or not rec.has("entity_id"):
|
||||||
continue
|
continue
|
||||||
var eid: int = rec.entity_id
|
var eid: int = rec.entity_id
|
||||||
active_ids[eid] = true
|
|
||||||
if not _known_recognition_ids.has(eid):
|
if not _known_recognition_ids.has(eid):
|
||||||
_known_recognition_ids[eid] = true
|
_known_recognition_ids[eid] = true
|
||||||
AudioManager.play(AudioManager.CHIME_RECOGNITION)
|
AudioManager.play(AudioManager.CHIME_RECOGNITION)
|
||||||
# Expire IDs no longer in the recognition queue
|
|
||||||
for eid in _known_recognition_ids.keys():
|
|
||||||
if not active_ids.has(eid):
|
|
||||||
_known_recognition_ids.erase(eid)
|
|
||||||
|
|
||||||
|
|
||||||
# D-073 (#529): Zone ambient crossfade — reads zone_id from GameState.current_zone_id
|
# D-073 (#529): Zone ambient crossfade — reads zone_id from GameState.current_zone_id
|
||||||
@@ -358,11 +344,16 @@ func _consume_conversation_ended() -> void:
|
|||||||
|
|
||||||
|
|
||||||
# #535: Consume dialogue_response — NPC follow-up line after player picks an option.
|
# #535: Consume dialogue_response — NPC follow-up line after player picks an option.
|
||||||
|
# Updates dialogue_box entity display registry with speaker identity from the wire.
|
||||||
func _consume_dialogue_response() -> void:
|
func _consume_dialogue_response() -> void:
|
||||||
if GameState.dialogue_response == null or not dialogue_box:
|
if GameState.dialogue_response == null or not dialogue_box:
|
||||||
return
|
return
|
||||||
var dr: Dictionary = GameState.dialogue_response
|
var dr: Dictionary = GameState.dialogue_response
|
||||||
dialogue_box.append_dialogue_response(_last_dialogue_npc_name, dr.get("text", ""))
|
var speaker_entity_id: int = dr.get("speaker_entity_id", _last_dialogue_npc_id)
|
||||||
|
var speaker_color_index: int = dr.get("speaker_color_index", -1)
|
||||||
|
var speaker_name: String = dr.get("speaker_name", _last_dialogue_npc_name)
|
||||||
|
dialogue_box.update_entity_display(speaker_entity_id, speaker_name, speaker_color_index)
|
||||||
|
dialogue_box.append_dialogue_response(speaker_name, dr.get("text", ""))
|
||||||
GameState.dialogue_response = null
|
GameState.dialogue_response = null
|
||||||
|
|
||||||
|
|
||||||
@@ -434,11 +425,8 @@ func _detect_teleport(old_pos: Vector2, new_pos: Vector2) -> bool:
|
|||||||
# Clears dialogue/monologue/interaction state (server clears its side too).
|
# Clears dialogue/monologue/interaction state (server clears its side too).
|
||||||
# Scoped to Gauntlet testing only — production fast-travel uses diegetic gates.
|
# Scoped to Gauntlet testing only — production fast-travel uses diegetic gates.
|
||||||
func _teleport_transition() -> void:
|
func _teleport_transition() -> void:
|
||||||
# Snap camera: disable smoothing, force re-anchor.
|
# Set teleport flag — the camera tracking block in _process() will snap
|
||||||
# _teleport_in_progress defers smoothing re-enable by one frame so the
|
# to the player's new position this frame (no lerp). Flag clears after snap.
|
||||||
# re-enable block at the bottom of _process() doesn't undo the snap.
|
|
||||||
camera.position_smoothing_enabled = false
|
|
||||||
camera.global_position = GameState.player_position * Constants.TILE_SIZE
|
|
||||||
_camera_anchored = true
|
_camera_anchored = true
|
||||||
_teleport_in_progress = true
|
_teleport_in_progress = true
|
||||||
|
|
||||||
@@ -446,6 +434,7 @@ func _teleport_transition() -> void:
|
|||||||
GameState.current_monologue = null
|
GameState.current_monologue = null
|
||||||
GameState.current_dialogue = null
|
GameState.current_dialogue = null
|
||||||
GameState.dialogue_active = false
|
GameState.dialogue_active = false
|
||||||
|
_known_recognition_ids.clear() # D-067: reset chimes for new room
|
||||||
if dialogue_box and dialogue_box.is_dialogue_active():
|
if dialogue_box and dialogue_box.is_dialogue_active():
|
||||||
dialogue_box.hide_dialogue()
|
dialogue_box.hide_dialogue()
|
||||||
|
|
||||||
|
|||||||
@@ -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 = 9
|
const PROTOCOL_VERSION: int = 13
|
||||||
|
|
||||||
|
|
||||||
# -- Decode: bytes from server → GDScript types --------------------------------
|
# -- Decode: bytes from server → GDScript types --------------------------------
|
||||||
|
|||||||
@@ -13,8 +13,11 @@ extends Node2D
|
|||||||
# color from RelationshipState via the knowledge graph.
|
# color from RelationshipState via the knowledge graph.
|
||||||
|
|
||||||
const TILE_SIZE: int = Constants.TILE_SIZE
|
const TILE_SIZE: int = Constants.TILE_SIZE
|
||||||
const ENTITY_SIZE: int = 24
|
# D-044: 24x32 entity footprint within 32x32 visual tile (64x64 source scaled to 32px runtime)
|
||||||
const ENTITY_OFFSET: float = (TILE_SIZE - ENTITY_SIZE) / 2.0 # center within tile
|
const ENTITY_WIDTH: int = 24
|
||||||
|
const ENTITY_HEIGHT: int = 32
|
||||||
|
const ENTITY_OFFSET_X: float = (TILE_SIZE - ENTITY_WIDTH) / 2.0 # center horizontally
|
||||||
|
const ENTITY_OFFSET_Y: float = (TILE_SIZE - ENTITY_HEIGHT) / 2.0 # center vertically for placeholder. Migration: switch to bottom-anchor (offset = TILE_SIZE - ENTITY_HEIGHT) when real sprites land for correct y-sort ordering.
|
||||||
|
|
||||||
# Lerp speed — framerate-independent exponential smoothing.
|
# Lerp speed — framerate-independent exponential smoothing.
|
||||||
# At 12.0: ~70% there after 0.1s, ~95% after 0.25s.
|
# At 12.0: ~70% there after 0.1s, ~95% after 0.25s.
|
||||||
@@ -93,8 +96,8 @@ func update_entities(entities: Array) -> void:
|
|||||||
func _create_entity_node(entity_id: int, entity_data: Dictionary) -> void:
|
func _create_entity_node(entity_id: int, entity_data: Dictionary) -> void:
|
||||||
var entity_node = ColorRect.new()
|
var entity_node = ColorRect.new()
|
||||||
entity_node.name = "Entity_" + str(entity_id)
|
entity_node.name = "Entity_" + str(entity_id)
|
||||||
entity_node.size = Vector2(ENTITY_SIZE, ENTITY_SIZE)
|
entity_node.size = Vector2(ENTITY_WIDTH, ENTITY_HEIGHT)
|
||||||
entity_node.pivot_offset = Vector2(ENTITY_SIZE / 2.0, ENTITY_SIZE / 2.0)
|
entity_node.pivot_offset = Vector2(ENTITY_WIDTH / 2.0, ENTITY_HEIGHT / 2.0)
|
||||||
|
|
||||||
# D-033 color by relationship (#521)
|
# D-033 color by relationship (#521)
|
||||||
entity_node.color = _color_for_kind(entity_data)
|
entity_node.color = _color_for_kind(entity_data)
|
||||||
@@ -110,8 +113,8 @@ func _create_entity_node(entity_id: int, entity_data: Dictionary) -> void:
|
|||||||
# Snap to initial position (no lerp on first appearance)
|
# Snap to initial position (no lerp on first appearance)
|
||||||
if entity_data.has("x") and entity_data.has("y"):
|
if entity_data.has("x") and entity_data.has("y"):
|
||||||
var target := Vector2(
|
var target := Vector2(
|
||||||
floorf(entity_data.x) * TILE_SIZE + ENTITY_OFFSET,
|
floorf(entity_data.x) * TILE_SIZE + ENTITY_OFFSET_X,
|
||||||
floorf(entity_data.y) * TILE_SIZE + ENTITY_OFFSET
|
floorf(entity_data.y) * TILE_SIZE + ENTITY_OFFSET_Y
|
||||||
)
|
)
|
||||||
entity_node.position = target
|
entity_node.position = target
|
||||||
_entity_targets[entity_id] = target
|
_entity_targets[entity_id] = target
|
||||||
@@ -129,8 +132,8 @@ func _update_entity_node(entity_id: int, entity_data: Dictionary) -> void:
|
|||||||
# Server sends tile-center coords (tile 16 → 16.5), floor to get tile index.
|
# Server sends tile-center coords (tile 16 → 16.5), floor to get tile index.
|
||||||
if entity_data.has("x") and entity_data.has("y"):
|
if entity_data.has("x") and entity_data.has("y"):
|
||||||
_entity_targets[entity_id] = Vector2(
|
_entity_targets[entity_id] = Vector2(
|
||||||
floorf(entity_data.x) * TILE_SIZE + ENTITY_OFFSET,
|
floorf(entity_data.x) * TILE_SIZE + ENTITY_OFFSET_X,
|
||||||
floorf(entity_data.y) * TILE_SIZE + ENTITY_OFFSET
|
floorf(entity_data.y) * TILE_SIZE + ENTITY_OFFSET_Y
|
||||||
)
|
)
|
||||||
|
|
||||||
# #521: Detect relationship change → fade D-033 color (0.5s via _process)
|
# #521: Detect relationship change → fade D-033 color (0.5s via _process)
|
||||||
@@ -195,5 +198,5 @@ func _add_facing_indicator(parent_node: Control) -> void:
|
|||||||
])
|
])
|
||||||
indicator.color = Constants.ENTITY_COLOR_PLAYER
|
indicator.color = Constants.ENTITY_COLOR_PLAYER
|
||||||
# Position at center of parent ColorRect — rotation around this point
|
# Position at center of parent ColorRect — rotation around this point
|
||||||
indicator.position = Vector2(ENTITY_SIZE / 2.0, ENTITY_SIZE / 2.0)
|
indicator.position = Vector2(ENTITY_WIDTH / 2.0, ENTITY_HEIGHT / 2.0)
|
||||||
parent_node.add_child(indicator)
|
parent_node.add_child(indicator)
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cb10ir0idsr0g
|
||||||
@@ -12,6 +12,7 @@ extends TileMapLayer
|
|||||||
# (4,0) = reset_plate — amber (#502)
|
# (4,0) = reset_plate — amber (#502)
|
||||||
|
|
||||||
const TILE_SIZE: int = Constants.TILE_SIZE
|
const TILE_SIZE: int = Constants.TILE_SIZE
|
||||||
|
const GROUND_FLOOR: int = 0 # Server floor level for ground — filter target in update_tiles()
|
||||||
|
|
||||||
enum TileType { FLOOR = 0, WALL = 1, DOOR = 2, OBJECT = 3, RESET_PLATE = 4 }
|
enum TileType { FLOOR = 0, WALL = 1, DOOR = 2, OBJECT = 3, RESET_PLATE = 4 }
|
||||||
|
|
||||||
@@ -64,6 +65,10 @@ func _setup_tileset() -> void:
|
|||||||
|
|
||||||
# Update tiles from snapshot data
|
# Update tiles from snapshot data
|
||||||
# tiles: Array of {x: int, y: int, z: int, type: String}
|
# tiles: Array of {x: int, y: int, z: int, type: String}
|
||||||
|
# z here is the server-side FLOOR LEVEL (0 = ground, 1 = first floor, etc.),
|
||||||
|
# NOT the Godot scene z_index (which controls render order within a floor).
|
||||||
|
# This node only renders floor-level 0. Higher floor levels will be handled
|
||||||
|
# by separate TileMapLayer nodes when multi-floor rendering is implemented.
|
||||||
func update_tiles(tiles: Array) -> void:
|
func update_tiles(tiles: Array) -> void:
|
||||||
if not _initialized:
|
if not _initialized:
|
||||||
return
|
return
|
||||||
@@ -74,6 +79,12 @@ func update_tiles(tiles: Array) -> void:
|
|||||||
if not tile_data.has("x") or not tile_data.has("y") or not tile_data.has("type"):
|
if not tile_data.has("x") or not tile_data.has("y") or not tile_data.has("type"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Floor-level filter: only render tiles at ground floor.
|
||||||
|
# Upper floor tiles (level 1+) are for future multi-floor nodes.
|
||||||
|
var tile_z: int = tile_data.get("z", 0)
|
||||||
|
if tile_z != GROUND_FLOOR:
|
||||||
|
continue
|
||||||
|
|
||||||
var tile_type_str: String = tile_data.type
|
var tile_type_str: String = tile_data.type
|
||||||
if not TILE_TYPE_MAP.has(tile_type_str):
|
if not TILE_TYPE_MAP.has(tile_type_str):
|
||||||
push_warning("TileRenderer: unknown tile type '%s' at (%d, %d)" % [
|
push_warning("TileRenderer: unknown tile type '%s' at (%d, %d)" % [
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ func _draw() -> void:
|
|||||||
var header_w := font.get_string_size(header_text, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE + 1).x
|
var header_w := font.get_string_size(header_text, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE + 1).x
|
||||||
var content_w := left_label_w + left_value_w + COL_GAP + right_label_w + right_value_w
|
var content_w := left_label_w + left_value_w + COL_GAP + right_label_w + right_value_w
|
||||||
var box_w: float = max(header_w, content_w) + PADDING.x * 2
|
var box_w: float = max(header_w, content_w) + PADDING.x * 2
|
||||||
var line_count := max(left_lines.size(), right_lines.size())
|
var line_count: int = maxi(left_lines.size(), right_lines.size())
|
||||||
var box_h: float = PADDING.y * 2 + LINE_HEIGHT + LINE_HEIGHT * line_count # header + data lines
|
var box_h: float = PADDING.y * 2 + LINE_HEIGHT + LINE_HEIGHT * line_count # header + data lines
|
||||||
|
|
||||||
# Background
|
# Background
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c55ow14m345vv
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
うtickヘ,ヲaction�DialogueResponseげtarget_entity_id*ォresponse_idーkael-davan_d_001
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://duvqpvheku2j2
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://d30by4d62g7ts
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b1tata3wjyb46
|
||||||
@@ -97,6 +97,20 @@ func test_camera_smoothing_off_after_ready() -> void:
|
|||||||
assert_that(camera.position_smoothing_enabled).is_false()
|
assert_that(camera.position_smoothing_enabled).is_false()
|
||||||
|
|
||||||
|
|
||||||
|
func test_camera_smoothing_stays_off_with_manual_lerp() -> void:
|
||||||
|
# #117: Manual lerp approach — Godot's built-in smoothing must stay OFF always.
|
||||||
|
# CAMERA_SMOOTHING_SPEED is used as the lerp weight, not Godot's position_smoothing_speed.
|
||||||
|
var scene := load("res://scenes/main.tscn")
|
||||||
|
_instance = scene.instantiate()
|
||||||
|
auto_free(_instance)
|
||||||
|
add_child(_instance)
|
||||||
|
|
||||||
|
_instance._process(0.016)
|
||||||
|
|
||||||
|
var camera: Camera2D = _instance.get_node("Camera2D")
|
||||||
|
assert_that(camera.position_smoothing_enabled).is_false()
|
||||||
|
|
||||||
|
|
||||||
# --- Camera behavior across frames ---
|
# --- Camera behavior across frames ---
|
||||||
|
|
||||||
func test_camera_tracks_player_after_process() -> void:
|
func test_camera_tracks_player_after_process() -> void:
|
||||||
@@ -113,33 +127,22 @@ func test_camera_tracks_player_after_process() -> void:
|
|||||||
assert_that(camera.global_position).is_equal(expected)
|
assert_that(camera.global_position).is_equal(expected)
|
||||||
|
|
||||||
|
|
||||||
func test_camera_smoothing_reenabled_after_process() -> void:
|
func test_camera_lerps_toward_player_movement() -> void:
|
||||||
# After the first anchored frame, smoothing should be back on for gameplay.
|
# #117: With manual lerp, camera moves TOWARD player position (not snapping).
|
||||||
var scene := load("res://scenes/main.tscn")
|
# After one 16ms frame the camera should be partway between old and new position.
|
||||||
_instance = scene.instantiate()
|
|
||||||
auto_free(_instance)
|
|
||||||
add_child(_instance)
|
|
||||||
|
|
||||||
_instance._process(0.016)
|
|
||||||
|
|
||||||
var camera: Camera2D = _instance.get_node("Camera2D")
|
|
||||||
assert_that(camera.position_smoothing_enabled).is_true()
|
|
||||||
|
|
||||||
|
|
||||||
func test_camera_follows_player_movement() -> void:
|
|
||||||
var scene := load("res://scenes/main.tscn")
|
var scene := load("res://scenes/main.tscn")
|
||||||
_instance = scene.instantiate()
|
_instance = scene.instantiate()
|
||||||
auto_free(_instance)
|
auto_free(_instance)
|
||||||
add_child(_instance)
|
add_child(_instance)
|
||||||
|
|
||||||
var camera: Camera2D = _instance.get_node("Camera2D")
|
var camera: Camera2D = _instance.get_node("Camera2D")
|
||||||
var initial_pos := camera.global_position
|
var initial_pos := camera.global_position # anchored at (320, 320)
|
||||||
|
|
||||||
# Move player north via SimBridge test mode
|
# Move player north via SimBridge test mode
|
||||||
SimBridge._test_input_queue.append("MoveNorth")
|
SimBridge._test_input_queue.append("MoveNorth")
|
||||||
_instance._process(0.016)
|
_instance._process(0.016)
|
||||||
|
|
||||||
# Camera should have moved with the player
|
var new_target := GameState.player_position * Constants.TILE_SIZE # (320, 288)
|
||||||
assert_that(camera.global_position.y < initial_pos.y).is_true()
|
# Camera should have moved north (lower y) but NOT reached the target yet
|
||||||
assert_that(camera.global_position).is_equal(
|
assert_that(camera.global_position.y).is_less(initial_pos.y)
|
||||||
GameState.player_position * Constants.TILE_SIZE)
|
assert_that(camera.global_position.y).is_greater(new_target.y)
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dhgcekc2ywr26
|
||||||
@@ -152,8 +152,8 @@ func test_entity_snap_on_first_appear() -> void:
|
|||||||
renderer.update_entities(entity)
|
renderer.update_entities(entity)
|
||||||
var node = renderer.entity_nodes[10]
|
var node = renderer.entity_nodes[10]
|
||||||
var expected := Vector2(
|
var expected := Vector2(
|
||||||
floorf(8.0) * Constants.TILE_SIZE + EntityRenderer.ENTITY_OFFSET,
|
floorf(8.0) * Constants.TILE_SIZE + EntityRenderer.ENTITY_OFFSET_X,
|
||||||
floorf(6.0) * Constants.TILE_SIZE + EntityRenderer.ENTITY_OFFSET
|
floorf(6.0) * Constants.TILE_SIZE + EntityRenderer.ENTITY_OFFSET_Y
|
||||||
)
|
)
|
||||||
assert_that(node.position).override_failure_message(
|
assert_that(node.position).override_failure_message(
|
||||||
"Entity should snap to position on first appear (no lerp)"
|
"Entity should snap to position on first appear (no lerp)"
|
||||||
@@ -179,8 +179,8 @@ func test_entity_lerp_moves_toward_target() -> void:
|
|||||||
renderer._process(0.016)
|
renderer._process(0.016)
|
||||||
var after_pos: Vector2 = node.position
|
var after_pos: Vector2 = node.position
|
||||||
var target := Vector2(
|
var target := Vector2(
|
||||||
floorf(6.0) * Constants.TILE_SIZE + EntityRenderer.ENTITY_OFFSET,
|
floorf(6.0) * Constants.TILE_SIZE + EntityRenderer.ENTITY_OFFSET_X,
|
||||||
floorf(5.0) * Constants.TILE_SIZE + EntityRenderer.ENTITY_OFFSET
|
floorf(5.0) * Constants.TILE_SIZE + EntityRenderer.ENTITY_OFFSET_Y
|
||||||
)
|
)
|
||||||
# Position should have moved toward target (x increased)
|
# Position should have moved toward target (x increased)
|
||||||
assert_that(after_pos.x > start_pos.x).override_failure_message(
|
assert_that(after_pos.x > start_pos.x).override_failure_message(
|
||||||
@@ -206,8 +206,8 @@ func test_entity_lerp_converges_within_300ms() -> void:
|
|||||||
"kind": {"variant": "Npc", "data": null}}]
|
"kind": {"variant": "Npc", "data": null}}]
|
||||||
renderer.update_entities(entity_moved)
|
renderer.update_entities(entity_moved)
|
||||||
var target := Vector2(
|
var target := Vector2(
|
||||||
floorf(7.0) * Constants.TILE_SIZE + EntityRenderer.ENTITY_OFFSET,
|
floorf(7.0) * Constants.TILE_SIZE + EntityRenderer.ENTITY_OFFSET_X,
|
||||||
floorf(5.0) * Constants.TILE_SIZE + EntityRenderer.ENTITY_OFFSET
|
floorf(5.0) * Constants.TILE_SIZE + EntityRenderer.ENTITY_OFFSET_Y
|
||||||
)
|
)
|
||||||
# Simulate 0.3s at 60fps (18 frames × 0.016s ≈ 0.288s)
|
# Simulate 0.3s at 60fps (18 frames × 0.016s ≈ 0.288s)
|
||||||
for i in 20:
|
for i in 20:
|
||||||
@@ -298,8 +298,8 @@ func test_lerp_weight_increases_with_delta() -> void:
|
|||||||
var small_progress: float = small_node.position.x - small_start
|
var small_progress: float = small_node.position.x - small_start
|
||||||
# Reset position for large delta test
|
# Reset position for large delta test
|
||||||
small_node.position = Vector2(
|
small_node.position = Vector2(
|
||||||
floorf(5.0) * Constants.TILE_SIZE + EntityRenderer.ENTITY_OFFSET,
|
floorf(5.0) * Constants.TILE_SIZE + EntityRenderer.ENTITY_OFFSET_X,
|
||||||
floorf(5.0) * Constants.TILE_SIZE + EntityRenderer.ENTITY_OFFSET
|
floorf(5.0) * Constants.TILE_SIZE + EntityRenderer.ENTITY_OFFSET_Y
|
||||||
)
|
)
|
||||||
# Large delta step
|
# Large delta step
|
||||||
var large_start: float = small_node.position.x
|
var large_start: float = small_node.position.x
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://qv808kduvlbk
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dmalit26eiwlh
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://6q82kiijx7ml
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c8ng0ubdfumht
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bn4a46egt2uol
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
## Sprint 15 — Smooth camera movement tests (#117)
|
||||||
|
## Validates exponential lerp, teleport snap, and configurable smoothing.
|
||||||
|
## Spec: D-015 (camera locked, fixed-north), #117 (interpolated tracking).
|
||||||
|
class_name TestSmoothCameraSprint15
|
||||||
|
extends GdUnitTestSuite
|
||||||
|
|
||||||
|
var _instance: Node = null
|
||||||
|
|
||||||
|
|
||||||
|
func before_test() -> void:
|
||||||
|
SimBridge.reset_test_state()
|
||||||
|
GameState.current_tick = 0
|
||||||
|
GameState.player_position = Vector2.ZERO
|
||||||
|
GameState.visible_entities = []
|
||||||
|
GameState.visible_tiles = []
|
||||||
|
GameState.visible_positions = {}
|
||||||
|
GameState.current_monologue = null
|
||||||
|
GameState.current_dialogue = null
|
||||||
|
|
||||||
|
|
||||||
|
func after_test() -> void:
|
||||||
|
if _instance and is_instance_valid(_instance):
|
||||||
|
_instance.queue_free()
|
||||||
|
_instance = null
|
||||||
|
|
||||||
|
|
||||||
|
# --- Configurable smoothing constant ---
|
||||||
|
|
||||||
|
func test_camera_smoothing_speed_constant_defined() -> void:
|
||||||
|
# #117: CAMERA_SMOOTHING_SPEED must be declared in Constants (configurable).
|
||||||
|
assert_that(Constants.CAMERA_SMOOTHING_SPEED > 0.0).is_true()
|
||||||
|
|
||||||
|
|
||||||
|
func test_camera_smoothing_speed_constant_reasonable() -> void:
|
||||||
|
# #117: Speed should produce smooth-but-responsive feel (2.0–20.0 range).
|
||||||
|
assert_that(
|
||||||
|
Constants.CAMERA_SMOOTHING_SPEED >= 2.0 and Constants.CAMERA_SMOOTHING_SPEED <= 20.0
|
||||||
|
).is_true()
|
||||||
|
|
||||||
|
|
||||||
|
# --- Manual lerp, no Godot built-in smoothing ---
|
||||||
|
|
||||||
|
func test_godot_smoothing_disabled_at_ready() -> void:
|
||||||
|
# #117: Godot's built-in Camera2D smoothing must be OFF (manual lerp replaces it).
|
||||||
|
var scene := load("res://scenes/main.tscn")
|
||||||
|
_instance = scene.instantiate()
|
||||||
|
auto_free(_instance)
|
||||||
|
add_child(_instance)
|
||||||
|
|
||||||
|
var camera: Camera2D = _instance.get_node("Camera2D")
|
||||||
|
assert_that(camera.position_smoothing_enabled).is_false()
|
||||||
|
|
||||||
|
|
||||||
|
func test_godot_smoothing_stays_off_after_frames() -> void:
|
||||||
|
# #117: Smoothing must NOT be re-enabled at any point — manual lerp only.
|
||||||
|
var scene := load("res://scenes/main.tscn")
|
||||||
|
_instance = scene.instantiate()
|
||||||
|
auto_free(_instance)
|
||||||
|
add_child(_instance)
|
||||||
|
|
||||||
|
for i in range(5):
|
||||||
|
_instance._process(0.016)
|
||||||
|
|
||||||
|
var camera: Camera2D = _instance.get_node("Camera2D")
|
||||||
|
assert_that(camera.position_smoothing_enabled).is_false()
|
||||||
|
|
||||||
|
|
||||||
|
# --- Interpolated tracking (no snap) ---
|
||||||
|
|
||||||
|
func test_camera_lerps_not_snaps_on_player_move() -> void:
|
||||||
|
# #117: When player moves, camera should lerp (not snap) to new position.
|
||||||
|
# After 1 frame at ~60fps, camera should be partway there — not at target.
|
||||||
|
var scene := load("res://scenes/main.tscn")
|
||||||
|
_instance = scene.instantiate()
|
||||||
|
auto_free(_instance)
|
||||||
|
add_child(_instance)
|
||||||
|
|
||||||
|
var camera: Camera2D = _instance.get_node("Camera2D")
|
||||||
|
var start_y := camera.global_position.y # anchored at player (10,10) → 320px
|
||||||
|
|
||||||
|
SimBridge._test_input_queue.append("MoveNorth")
|
||||||
|
_instance._process(0.016)
|
||||||
|
|
||||||
|
# Player moved to (10,9) → target_y = 288. Camera should be between 288 and 320.
|
||||||
|
var target_y: float = GameState.player_position.y * Constants.TILE_SIZE
|
||||||
|
assert_that(camera.global_position.y < start_y).is_true()
|
||||||
|
assert_that(camera.global_position.y > target_y).is_true()
|
||||||
|
|
||||||
|
|
||||||
|
func test_camera_converges_to_player_over_multiple_frames() -> void:
|
||||||
|
# #117: After enough frames the camera should be within 1px of target.
|
||||||
|
# At LERP_SPEED=8: ~95% convergence in 0.25s, >99% in 0.5s.
|
||||||
|
var scene := load("res://scenes/main.tscn")
|
||||||
|
_instance = scene.instantiate()
|
||||||
|
auto_free(_instance)
|
||||||
|
add_child(_instance)
|
||||||
|
|
||||||
|
SimBridge._test_input_queue.append("MoveNorth")
|
||||||
|
_instance._process(0.016) # trigger the move, get new player position
|
||||||
|
|
||||||
|
var target := GameState.player_position * Constants.TILE_SIZE
|
||||||
|
|
||||||
|
# Run 120 frames (~2s at 60fps) — converges within 1px for any speed ≥ 2.0
|
||||||
|
for i in range(120):
|
||||||
|
_instance._process(0.016)
|
||||||
|
|
||||||
|
var camera: Camera2D = _instance.get_node("Camera2D")
|
||||||
|
var dist := camera.global_position.distance_to(target)
|
||||||
|
assert_that(dist < 1.0).is_true()
|
||||||
|
|
||||||
|
|
||||||
|
func test_camera_stationary_player_no_drift() -> void:
|
||||||
|
# #117: When player is stationary, camera should not drift (lerp to same point).
|
||||||
|
var scene := load("res://scenes/main.tscn")
|
||||||
|
_instance = scene.instantiate()
|
||||||
|
auto_free(_instance)
|
||||||
|
add_child(_instance)
|
||||||
|
|
||||||
|
var camera: Camera2D = _instance.get_node("Camera2D")
|
||||||
|
var initial_pos := camera.global_position
|
||||||
|
|
||||||
|
# Run several frames with no movement
|
||||||
|
for i in range(10):
|
||||||
|
_instance._process(0.016)
|
||||||
|
|
||||||
|
# Camera should still be at anchored position (target = same point).
|
||||||
|
# Use distance check — lerp toward same point may introduce float rounding.
|
||||||
|
assert_that(camera.global_position.distance_to(initial_pos) < 0.01).is_true()
|
||||||
|
|
||||||
|
|
||||||
|
# --- Teleport snap ---
|
||||||
|
|
||||||
|
func test_teleport_snaps_camera_immediately() -> void:
|
||||||
|
# #117: _teleport_in_progress causes camera to snap (not lerp) in the same frame.
|
||||||
|
# Manually displace camera, set the flag, call _process — camera should snap to target.
|
||||||
|
var scene := load("res://scenes/main.tscn")
|
||||||
|
_instance = scene.instantiate()
|
||||||
|
auto_free(_instance)
|
||||||
|
add_child(_instance)
|
||||||
|
|
||||||
|
var camera: Camera2D = _instance.get_node("Camera2D")
|
||||||
|
|
||||||
|
# Displace camera from its anchored position
|
||||||
|
camera.global_position = Vector2(0, 0)
|
||||||
|
# Set teleport flag — next _process() should snap to player target
|
||||||
|
_instance._camera_anchored = true
|
||||||
|
_instance._teleport_in_progress = true
|
||||||
|
|
||||||
|
_instance._process(0.016)
|
||||||
|
|
||||||
|
# Camera must now be exactly at player position (snapshot puts player at 10,10 → 320,320)
|
||||||
|
var expected := GameState.player_position * Constants.TILE_SIZE
|
||||||
|
assert_that(camera.global_position).is_equal(expected)
|
||||||
|
|
||||||
|
|
||||||
|
func test_teleport_flag_cleared_after_snap() -> void:
|
||||||
|
# #117: _teleport_in_progress must be false after the snap frame.
|
||||||
|
var scene := load("res://scenes/main.tscn")
|
||||||
|
_instance = scene.instantiate()
|
||||||
|
auto_free(_instance)
|
||||||
|
add_child(_instance)
|
||||||
|
|
||||||
|
_instance._camera_anchored = true
|
||||||
|
_instance._teleport_in_progress = true
|
||||||
|
_instance._process(0.016)
|
||||||
|
|
||||||
|
assert_that(_instance._teleport_in_progress).is_false()
|
||||||
|
|
||||||
|
|
||||||
|
func test_camera_resumes_lerp_after_teleport() -> void:
|
||||||
|
# #117: Frame after teleport snap must resume lerp (not continue snapping).
|
||||||
|
# After teleport flag clears, any position delta produces lerp movement.
|
||||||
|
var scene := load("res://scenes/main.tscn")
|
||||||
|
_instance = scene.instantiate()
|
||||||
|
auto_free(_instance)
|
||||||
|
add_child(_instance)
|
||||||
|
|
||||||
|
# Frame 1: teleport snap — camera displaced, flag set, expect snap
|
||||||
|
var camera: Camera2D = _instance.get_node("Camera2D")
|
||||||
|
camera.global_position = Vector2(0, 0)
|
||||||
|
_instance._camera_anchored = true
|
||||||
|
_instance._teleport_in_progress = true
|
||||||
|
_instance._process(0.016)
|
||||||
|
# After snap: camera at player position (10,10) = (320, 320)
|
||||||
|
var post_snap_y := camera.global_position.y
|
||||||
|
|
||||||
|
# Frame 2: player moves north — camera should lerp, not snap
|
||||||
|
SimBridge._test_input_queue.append("MoveNorth")
|
||||||
|
_instance._process(0.016)
|
||||||
|
|
||||||
|
var new_target_y: float = GameState.player_position.y * Constants.TILE_SIZE
|
||||||
|
# Camera must be between snap position and new target (lerping, not snapping)
|
||||||
|
assert_that(camera.global_position.y < post_snap_y).is_true()
|
||||||
|
assert_that(camera.global_position.y > new_target_y).is_true()
|
||||||
|
# Teleport flag must not be re-set by normal movement
|
||||||
|
assert_that(_instance._teleport_in_progress).is_false()
|
||||||
|
|
||||||
|
|
||||||
|
# --- D-015: Fixed-north camera ---
|
||||||
|
|
||||||
|
func test_camera_no_rotation() -> void:
|
||||||
|
# D-015: Camera must be fixed-north in v0.1 — no rotation regardless of facing.
|
||||||
|
var scene := load("res://scenes/main.tscn")
|
||||||
|
_instance = scene.instantiate()
|
||||||
|
auto_free(_instance)
|
||||||
|
add_child(_instance)
|
||||||
|
|
||||||
|
var camera: Camera2D = _instance.get_node("Camera2D")
|
||||||
|
assert_that(camera.rotation).is_equal(0.0)
|
||||||
|
|
||||||
|
_instance._process(0.016)
|
||||||
|
assert_that(camera.rotation).is_equal(0.0)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://s15smoothcam1
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://tp8wpp6t8tvx
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
## Sprint 15 — Basic UI framework validation tests (#74)
|
||||||
|
## Validates HUD structure, z-layer hierarchy, insert_active control,
|
||||||
|
## and monologue display wiring per D-049, D-056, D-057, D-061, OQ-07.
|
||||||
|
class_name TestUIFrameworkSprint15
|
||||||
|
extends GdUnitTestSuite
|
||||||
|
|
||||||
|
var _instance: Node = null
|
||||||
|
|
||||||
|
|
||||||
|
func before_test() -> void:
|
||||||
|
SimBridge.reset_test_state()
|
||||||
|
GameState.current_tick = 0
|
||||||
|
GameState.player_position = Vector2.ZERO
|
||||||
|
GameState.visible_entities = []
|
||||||
|
GameState.visible_tiles = []
|
||||||
|
GameState.visible_positions = {}
|
||||||
|
GameState.current_monologue = null
|
||||||
|
GameState.current_dialogue = null
|
||||||
|
GameState.insert_active = true
|
||||||
|
|
||||||
|
|
||||||
|
func after_test() -> void:
|
||||||
|
if _instance and is_instance_valid(_instance):
|
||||||
|
_instance.queue_free()
|
||||||
|
_instance = null
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# D-076: Layout constants
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func test_dialogue_max_width_set() -> void:
|
||||||
|
# DIALOGUE_MAX_WIDTH = 1200px (supersedes D-076 640px default per Tyre review).
|
||||||
|
assert_that(Constants.DIALOGUE_MAX_WIDTH).is_equal(1200)
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# D-049: Z-layer scene hierarchy
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func test_insert_overlay_is_canvas_layer_10() -> void:
|
||||||
|
# D-049: InsertOverlay = conceptual layer 6 (insert scope) = CanvasLayer 10.
|
||||||
|
# Constants.CANVAS_INSERT must match.
|
||||||
|
var scene := load("res://scenes/main.tscn")
|
||||||
|
_instance = scene.instantiate()
|
||||||
|
auto_free(_instance)
|
||||||
|
add_child(_instance)
|
||||||
|
|
||||||
|
var insert_overlay: CanvasLayer = _instance.get_node("InsertOverlay")
|
||||||
|
assert_that(insert_overlay).is_not_null()
|
||||||
|
assert_that(insert_overlay.layer).is_equal(Constants.CANVAS_INSERT)
|
||||||
|
|
||||||
|
|
||||||
|
func test_ui_layer_is_canvas_layer_20() -> void:
|
||||||
|
# D-049: UILayer = conceptual layer 7 (UI/monologue scope) = CanvasLayer 20.
|
||||||
|
var scene := load("res://scenes/main.tscn")
|
||||||
|
_instance = scene.instantiate()
|
||||||
|
auto_free(_instance)
|
||||||
|
add_child(_instance)
|
||||||
|
|
||||||
|
var ui_layer: CanvasLayer = _instance.get_node("UILayer")
|
||||||
|
assert_that(ui_layer).is_not_null()
|
||||||
|
assert_that(ui_layer.layer).is_equal(Constants.CANVAS_UI)
|
||||||
|
|
||||||
|
|
||||||
|
func test_modal_layer_is_canvas_layer_30() -> void:
|
||||||
|
# D-049: ModalLayer = pause/inventory modal scope = CanvasLayer 30.
|
||||||
|
var scene := load("res://scenes/main.tscn")
|
||||||
|
_instance = scene.instantiate()
|
||||||
|
auto_free(_instance)
|
||||||
|
add_child(_instance)
|
||||||
|
|
||||||
|
var modal_layer: CanvasLayer = _instance.get_node("ModalLayer")
|
||||||
|
assert_that(modal_layer).is_not_null()
|
||||||
|
assert_that(modal_layer.layer).is_equal(Constants.CANVAS_MODAL)
|
||||||
|
|
||||||
|
|
||||||
|
func test_ui_layer_above_insert_overlay() -> void:
|
||||||
|
# D-049: UILayer (20) must render above InsertOverlay (10).
|
||||||
|
assert_that(Constants.CANVAS_UI).is_greater(Constants.CANVAS_INSERT)
|
||||||
|
|
||||||
|
|
||||||
|
func test_modal_layer_above_ui_layer() -> void:
|
||||||
|
# D-049: ModalLayer (30) must render above UILayer (20).
|
||||||
|
assert_that(Constants.CANVAS_MODAL).is_greater(Constants.CANVAS_UI)
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# D-049: Required nodes exist in correct layers
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func test_monologue_display_exists_in_ui_layer() -> void:
|
||||||
|
# D-049 / #117 / #414: MonologueDisplay must be in UILayer (layer 7).
|
||||||
|
var scene := load("res://scenes/main.tscn")
|
||||||
|
_instance = scene.instantiate()
|
||||||
|
auto_free(_instance)
|
||||||
|
add_child(_instance)
|
||||||
|
|
||||||
|
assert_that(_instance.get_node_or_null("UILayer/MonologueDisplay")).is_not_null()
|
||||||
|
|
||||||
|
|
||||||
|
func test_stance_indicator_exists_in_ui_layer() -> void:
|
||||||
|
# D-053: StanceIndicator must be in UILayer (layer 7), top-right, color-coded.
|
||||||
|
var scene := load("res://scenes/main.tscn")
|
||||||
|
_instance = scene.instantiate()
|
||||||
|
auto_free(_instance)
|
||||||
|
add_child(_instance)
|
||||||
|
|
||||||
|
assert_that(_instance.get_node_or_null("UILayer/StanceIndicator")).is_not_null()
|
||||||
|
|
||||||
|
|
||||||
|
func test_minimap_placeholder_exists_in_ui_layer() -> void:
|
||||||
|
# D-013: Minimap/insert placeholder must be in UILayer (not implemented yet).
|
||||||
|
var scene := load("res://scenes/main.tscn")
|
||||||
|
_instance = scene.instantiate()
|
||||||
|
auto_free(_instance)
|
||||||
|
add_child(_instance)
|
||||||
|
|
||||||
|
assert_that(_instance.get_node_or_null("UILayer/Minimap")).is_not_null()
|
||||||
|
|
||||||
|
|
||||||
|
func test_hud_exists_in_ui_layer() -> void:
|
||||||
|
# D-049: HUD must be in UILayer.
|
||||||
|
var scene := load("res://scenes/main.tscn")
|
||||||
|
_instance = scene.instantiate()
|
||||||
|
auto_free(_instance)
|
||||||
|
add_child(_instance)
|
||||||
|
|
||||||
|
assert_that(_instance.get_node_or_null("UILayer/HUD")).is_not_null()
|
||||||
|
|
||||||
|
|
||||||
|
func test_interaction_list_exists_in_insert_overlay() -> void:
|
||||||
|
# D-057: InteractionList must be in InsertOverlay (z-layer 6).
|
||||||
|
var scene := load("res://scenes/main.tscn")
|
||||||
|
_instance = scene.instantiate()
|
||||||
|
auto_free(_instance)
|
||||||
|
add_child(_instance)
|
||||||
|
|
||||||
|
assert_that(_instance.get_node_or_null("InsertOverlay/InteractionList")).is_not_null()
|
||||||
|
|
||||||
|
|
||||||
|
func test_dialogue_box_exists_in_insert_overlay() -> void:
|
||||||
|
# D-061: DialogueBox must be in InsertOverlay (z-layer 6).
|
||||||
|
var scene := load("res://scenes/main.tscn")
|
||||||
|
_instance = scene.instantiate()
|
||||||
|
auto_free(_instance)
|
||||||
|
add_child(_instance)
|
||||||
|
|
||||||
|
assert_that(_instance.get_node_or_null("InsertOverlay/DialogueBox")).is_not_null()
|
||||||
|
|
||||||
|
|
||||||
|
func test_world_radial_exists_in_insert_overlay() -> void:
|
||||||
|
# D-058: WorldRadial must be in InsertOverlay (z-layer 6).
|
||||||
|
var scene := load("res://scenes/main.tscn")
|
||||||
|
_instance = scene.instantiate()
|
||||||
|
auto_free(_instance)
|
||||||
|
add_child(_instance)
|
||||||
|
|
||||||
|
assert_that(_instance.get_node_or_null("InsertOverlay/WorldRadial")).is_not_null()
|
||||||
|
|
||||||
|
|
||||||
|
func test_cursor_renderer_exists_in_ui_layer() -> void:
|
||||||
|
# D-056: CursorRenderer must be in UILayer (topmost, z-layer 7).
|
||||||
|
var scene := load("res://scenes/main.tscn")
|
||||||
|
_instance = scene.instantiate()
|
||||||
|
auto_free(_instance)
|
||||||
|
add_child(_instance)
|
||||||
|
|
||||||
|
assert_that(_instance.get_node_or_null("UILayer/CursorRenderer")).is_not_null()
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# OQ-07 / D-056: insert_active controls z-layer 6 visibility
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func test_gamestate_insert_active_defaults_true() -> void:
|
||||||
|
# OQ-07: v0.1 characters all have inserts — default true.
|
||||||
|
assert_that(GameState.insert_active).is_true()
|
||||||
|
|
||||||
|
|
||||||
|
func test_insert_active_propagates_on_process() -> void:
|
||||||
|
# OQ-07 (#522): After apply_snapshot with insert_active=false,
|
||||||
|
# the next _process() call must propagate the state to z-layer-6 nodes.
|
||||||
|
var scene := load("res://scenes/main.tscn")
|
||||||
|
_instance = scene.instantiate()
|
||||||
|
auto_free(_instance)
|
||||||
|
add_child(_instance)
|
||||||
|
|
||||||
|
# Inject a snapshot with insert_active = false
|
||||||
|
var snap := SimBridge._test_snapshot()
|
||||||
|
snap["insert_active"] = false
|
||||||
|
GameState.apply_snapshot(snap)
|
||||||
|
assert_that(GameState.insert_active).is_false()
|
||||||
|
|
||||||
|
|
||||||
|
func test_insert_active_true_from_snapshot() -> void:
|
||||||
|
# OQ-07: Snapshot with insert_active=true keeps GameState in default-on state.
|
||||||
|
var snap := SimBridge._test_snapshot()
|
||||||
|
snap["insert_active"] = true
|
||||||
|
GameState.apply_snapshot(snap)
|
||||||
|
assert_that(GameState.insert_active).is_true()
|
||||||
|
|
||||||
|
|
||||||
|
func test_insert_active_missing_field_defaults_true() -> void:
|
||||||
|
# OQ-07: Old servers without insert_active field must not disable the insert.
|
||||||
|
var snap := SimBridge._test_snapshot()
|
||||||
|
snap.erase("insert_active")
|
||||||
|
GameState.apply_snapshot(snap)
|
||||||
|
assert_that(GameState.insert_active).is_true()
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# #241 stub: follow_target_id for entity sprite system
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func test_follow_target_id_stub_exists() -> void:
|
||||||
|
# #72 / #241: follow_target_id stub must exist on GameState with default -1.
|
||||||
|
# Populated by server ticket #241 (Follow verb) when it lands.
|
||||||
|
assert_that(GameState.follow_target_id).is_equal(-1)
|
||||||
|
|
||||||
|
|
||||||
|
func test_follow_target_id_is_negative_one_by_default() -> void:
|
||||||
|
# #72: -1 means "not following" — client #72 checks this for entity highlight.
|
||||||
|
SimBridge.reset_test_state()
|
||||||
|
GameState.apply_snapshot(SimBridge._test_snapshot())
|
||||||
|
# Server doesn't send follow_target_id yet — must stay -1 after snapshot
|
||||||
|
assert_that(GameState.follow_target_id).is_equal(-1)
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Monologue display wiring (#414)
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func test_monologue_display_receives_first_tick_monologue() -> void:
|
||||||
|
# #414 / #74: MonologueDisplay must show monologue from tick 1 test snapshot.
|
||||||
|
# Verifies the wiring: GameState.current_monologue → main.gd → MonologueDisplay.
|
||||||
|
var scene := load("res://scenes/main.tscn")
|
||||||
|
_instance = scene.instantiate()
|
||||||
|
auto_free(_instance)
|
||||||
|
add_child(_instance)
|
||||||
|
|
||||||
|
# Tick 1 snapshot has a monologue (SimBridge test mode)
|
||||||
|
# _ready() consumes tick 0 (no monologue). _process() here gets tick 1.
|
||||||
|
_instance._process(0.016)
|
||||||
|
|
||||||
|
# If monologue_display received it, current_monologue is cleared (consume-once)
|
||||||
|
assert_that(GameState.current_monologue).is_null()
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Regression: Sprint 14 integration proofs (D-030 regression markers)
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func test_fog_group_exists_in_world() -> void:
|
||||||
|
# Sprint 14 regression: fog rendering must still be present after sprint 15 changes.
|
||||||
|
var scene := load("res://scenes/main.tscn")
|
||||||
|
_instance = scene.instantiate()
|
||||||
|
auto_free(_instance)
|
||||||
|
add_child(_instance)
|
||||||
|
|
||||||
|
assert_that(_instance.get_node_or_null("World/FogGroup")).is_not_null()
|
||||||
|
|
||||||
|
|
||||||
|
func test_floor_tiles_in_fog_group() -> void:
|
||||||
|
# Sprint 14 regression: FloorTiles must be in FogGroup (D-049 z:0).
|
||||||
|
var scene := load("res://scenes/main.tscn")
|
||||||
|
_instance = scene.instantiate()
|
||||||
|
auto_free(_instance)
|
||||||
|
add_child(_instance)
|
||||||
|
|
||||||
|
assert_that(_instance.get_node_or_null("World/FogGroup/FloorTiles")).is_not_null()
|
||||||
|
|
||||||
|
|
||||||
|
func test_entities_in_ysort_group() -> void:
|
||||||
|
# Sprint 14 regression: Entities must be in YSortGroup for y-sort ordering (D-049 z:100).
|
||||||
|
var scene := load("res://scenes/main.tscn")
|
||||||
|
_instance = scene.instantiate()
|
||||||
|
auto_free(_instance)
|
||||||
|
add_child(_instance)
|
||||||
|
|
||||||
|
assert_that(_instance.get_node_or_null("World/FogGroup/YSortGroup/Entities")).is_not_null()
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# #71: Tilemap z-filter — FloorTiles only renders floor level 0
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func test_tile_renderer_skips_nonzero_z() -> void:
|
||||||
|
# #71: Tiles with z != 0 must be filtered out by update_tiles().
|
||||||
|
var scene := load("res://scenes/main.tscn")
|
||||||
|
_instance = scene.instantiate()
|
||||||
|
auto_free(_instance)
|
||||||
|
add_child(_instance)
|
||||||
|
|
||||||
|
var tile_renderer: TileMapLayer = _instance.get_node("World/FogGroup/FloorTiles")
|
||||||
|
assert_that(tile_renderer).is_not_null()
|
||||||
|
|
||||||
|
# Feed tiles at z=0 and z=1
|
||||||
|
var tiles: Array = [
|
||||||
|
{"x": 0, "y": 0, "z": 0, "type": "floor"},
|
||||||
|
{"x": 1, "y": 0, "z": 1, "type": "floor"},
|
||||||
|
{"x": 2, "y": 0, "z": 0, "type": "wall"},
|
||||||
|
{"x": 3, "y": 0, "z": 2, "type": "door"},
|
||||||
|
]
|
||||||
|
tile_renderer.update_tiles(tiles)
|
||||||
|
|
||||||
|
# z=0 tiles should be present
|
||||||
|
assert_that(tile_renderer.get_cell_source_id(Vector2i(0, 0))).is_not_equal(-1)
|
||||||
|
assert_that(tile_renderer.get_cell_source_id(Vector2i(2, 0))).is_not_equal(-1)
|
||||||
|
# z=1 and z=2 tiles should NOT be present (-1 = no cell)
|
||||||
|
assert_that(tile_renderer.get_cell_source_id(Vector2i(1, 0))).is_equal(-1)
|
||||||
|
assert_that(tile_renderer.get_cell_source_id(Vector2i(3, 0))).is_equal(-1)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://s15uiframe001
|
||||||
@@ -243,6 +243,7 @@ func _close() -> void:
|
|||||||
_active = false
|
_active = false
|
||||||
visible = false
|
visible = false
|
||||||
if _line_edit:
|
if _line_edit:
|
||||||
|
_line_edit.release_focus()
|
||||||
_line_edit.queue_free()
|
_line_edit.queue_free()
|
||||||
_line_edit = null
|
_line_edit = null
|
||||||
|
|
||||||
@@ -344,7 +345,7 @@ func _save_report(description: String) -> void:
|
|||||||
else:
|
else:
|
||||||
push_error("BugReport: failed to write %s" % seed_path)
|
push_error("BugReport: failed to write %s" % seed_path)
|
||||||
|
|
||||||
push_warning("BugReport: saved %d/6 files to %s (ring: %d ticks)" % [
|
print("BugReport: saved %d/6 files to %s (ring: %d ticks)" % [
|
||||||
files_saved, base_path, _input_count])
|
files_saved, base_path, _input_count])
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c5xgtnkp0butl
|
||||||
@@ -21,12 +21,18 @@ signal unpause_requested # D-061: auto-unpause
|
|||||||
@onready var options_container: VBoxContainer = $PanelContainer/MarginContainer/VBoxContainer/OptionsContainer
|
@onready var options_container: VBoxContainer = $PanelContainer/MarginContainer/VBoxContainer/OptionsContainer
|
||||||
|
|
||||||
# -- Log state --
|
# -- Log state --
|
||||||
# Entry format: {speaker, target, text, is_passive, pinned, timestamp_msec}
|
# Entry format (legacy): {speaker: String, target: String, text, is_passive, pinned, timestamp_msec}
|
||||||
|
# Entry format (entity-anchored): {speaker_id: int, target_id: int, text, is_passive, pinned, timestamp_msec}
|
||||||
# pinned entries do not expire (active conversation lines, Araminta review).
|
# pinned entries do not expire (active conversation lines, Araminta review).
|
||||||
var _log_entries: Array[Dictionary] = []
|
var _log_entries: Array[Dictionary] = []
|
||||||
var _in_player_conversation: bool = false
|
var _in_player_conversation: bool = false
|
||||||
var _log_dirty: bool = false # Dirty flag — prevents per-frame O(n) BBCode rebuild (Hoshe #1)
|
var _log_dirty: bool = false # Dirty flag — prevents per-frame O(n) BBCode rebuild (Hoshe #1)
|
||||||
|
|
||||||
|
# Entity ID → {name: String, color_index: int}
|
||||||
|
# Populated from server events; drives retroactive re-render when NPC names resolve.
|
||||||
|
var _entity_display: Dictionary = {}
|
||||||
|
|
||||||
|
|
||||||
# -- Option state --
|
# -- Option state --
|
||||||
var _option_controls: Array[Control] = []
|
var _option_controls: Array[Control] = []
|
||||||
var _option_response_ids: Array[String] = []
|
var _option_response_ids: Array[String] = []
|
||||||
@@ -35,7 +41,6 @@ var _option_is_confrontation: Array[bool] = []
|
|||||||
var _npc_name: String = ""
|
var _npc_name: String = ""
|
||||||
|
|
||||||
# -- UI state --
|
# -- UI state --
|
||||||
var _is_showing: bool = false
|
|
||||||
var _active_tween: Tween = null
|
var _active_tween: Tween = null
|
||||||
var _beat_tween: Tween = null # D-063: confrontation beat delay
|
var _beat_tween: Tween = null # D-063: confrontation beat delay
|
||||||
|
|
||||||
@@ -45,8 +50,8 @@ var _npc_colors: Array[Color] = []
|
|||||||
var _arrow_color: Color = Color("#8890a0")
|
var _arrow_color: Color = Color("#8890a0")
|
||||||
var _speech_color: Color = Color("#c8d0e0")
|
var _speech_color: Color = Color("#c8d0e0")
|
||||||
var _passive_opacity: float = 0.9
|
var _passive_opacity: float = 0.9
|
||||||
var _entry_lifetime: float = 15.0
|
var _entry_lifetime: float = 45.0
|
||||||
var _entry_fade: float = 3.0
|
var _entry_fade: float = 5.0
|
||||||
|
|
||||||
const THEME_PATH: String = "res://data/dialogue-theme.yaml"
|
const THEME_PATH: String = "res://data/dialogue-theme.yaml"
|
||||||
|
|
||||||
@@ -71,9 +76,10 @@ const _WALK_AWAY_ACTIONS: Array[StringName] = [
|
|||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
panel.modulate.a = 0.0
|
# Panel is always visible as a permanent insert UI element (D-061).
|
||||||
visible = false
|
# Content fades in/out but the panel frame stays on screen.
|
||||||
_is_showing = false
|
visible = true
|
||||||
|
panel.modulate.a = 1.0
|
||||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
_load_theme()
|
_load_theme()
|
||||||
_update_layout()
|
_update_layout()
|
||||||
@@ -88,11 +94,21 @@ func _process(_delta: float) -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _unhandled_input(event: InputEvent) -> void:
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
if not _is_showing:
|
|
||||||
return
|
|
||||||
if not _in_player_conversation:
|
if not _in_player_conversation:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Number keys 1-3 select dialogue options
|
||||||
|
if event is InputEventKey and event.pressed:
|
||||||
|
var key_index := -1
|
||||||
|
if event.keycode == KEY_1: key_index = 0
|
||||||
|
elif event.keycode == KEY_2: key_index = 1
|
||||||
|
elif event.keycode == KEY_3: key_index = 2
|
||||||
|
if key_index >= 0 and key_index < _option_controls.size():
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
_on_option_pressed(key_index)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
# D-064: WASD during active player dialogue → walk-away
|
# D-064: WASD during active player dialogue → walk-away
|
||||||
if event is InputEventKey and event.pressed:
|
if event is InputEventKey and event.pressed:
|
||||||
for action in _WALK_AWAY_ACTIONS:
|
for action in _WALK_AWAY_ACTIONS:
|
||||||
@@ -153,7 +169,7 @@ func _load_theme() -> void:
|
|||||||
func _update_layout() -> void:
|
func _update_layout() -> void:
|
||||||
var vp := get_viewport_rect().size
|
var vp := get_viewport_rect().size
|
||||||
var max_h := vp.y * MAX_HEIGHT_RATIO
|
var max_h := vp.y * MAX_HEIGHT_RATIO
|
||||||
var w := minf(MAX_WIDTH_PX, vp.x * 0.65)
|
var w := minf(MAX_WIDTH_PX, vp.x * 0.85)
|
||||||
panel.offset_left = -w / 2.0
|
panel.offset_left = -w / 2.0
|
||||||
panel.offset_right = w / 2.0
|
panel.offset_right = w / 2.0
|
||||||
panel.offset_top = -max_h
|
panel.offset_top = -max_h
|
||||||
@@ -180,13 +196,59 @@ func append_line(speaker: String, target: String, text: String, is_passive: bool
|
|||||||
|
|
||||||
|
|
||||||
## Append an overheard conversation event (D-078).
|
## Append an overheard conversation event (D-078).
|
||||||
|
## Stores entity IDs for retroactive name resolution when NPC display names change.
|
||||||
func append_conversation_event(event: Dictionary) -> void:
|
func append_conversation_event(event: Dictionary) -> void:
|
||||||
var speaker: String = event.get("speaker_name", "?")
|
|
||||||
var target: String = event.get("target_name", "?")
|
|
||||||
var text: String = event.get("occluded_line", "")
|
var text: String = event.get("occluded_line", "")
|
||||||
if text.is_empty():
|
if text.is_empty():
|
||||||
return
|
return
|
||||||
append_line(speaker, target, text, true)
|
|
||||||
|
var speaker_id: int = event.get("speaker_id", -1)
|
||||||
|
var target_id: int = event.get("target_id", -1)
|
||||||
|
var speaker_name: String = event.get("speaker_name", "?")
|
||||||
|
var target_name: String = event.get("target_name", "?")
|
||||||
|
var speaker_color_index: int = event.get("speaker_color_index", -1)
|
||||||
|
var target_color_index: int = event.get("target_color_index", -1)
|
||||||
|
|
||||||
|
# Update entity display registry — set dirty if a known name changed (retroactive update)
|
||||||
|
if speaker_id >= 0:
|
||||||
|
var prev: Dictionary = _entity_display.get(speaker_id, {})
|
||||||
|
_entity_display[speaker_id] = {"name": speaker_name, "color_index": speaker_color_index}
|
||||||
|
if prev.has("name") and prev.get("name", "") != speaker_name:
|
||||||
|
_log_dirty = true
|
||||||
|
if target_id >= 0:
|
||||||
|
var prev: Dictionary = _entity_display.get(target_id, {})
|
||||||
|
_entity_display[target_id] = {"name": target_name, "color_index": target_color_index}
|
||||||
|
if prev.has("name") and prev.get("name", "") != target_name:
|
||||||
|
_log_dirty = true
|
||||||
|
|
||||||
|
var entry: Dictionary = {
|
||||||
|
"text": text,
|
||||||
|
"is_passive": true,
|
||||||
|
"pinned": false,
|
||||||
|
"timestamp_msec": Time.get_ticks_msec(),
|
||||||
|
}
|
||||||
|
if speaker_id >= 0:
|
||||||
|
entry["speaker_id"] = speaker_id
|
||||||
|
entry["target_id"] = target_id
|
||||||
|
else:
|
||||||
|
# Fallback: no entity IDs on wire, store raw names for legacy rendering
|
||||||
|
entry["speaker"] = speaker_name
|
||||||
|
entry["target"] = target_name
|
||||||
|
|
||||||
|
_log_entries.append(entry)
|
||||||
|
_log_dirty = true
|
||||||
|
_ensure_visible()
|
||||||
|
|
||||||
|
|
||||||
|
## Update entity display name and color index — called when Talk responses arrive.
|
||||||
|
## Triggers retroactive log re-render if the entity's displayed name has changed.
|
||||||
|
func update_entity_display(entity_id: int, name: String, color_index: int) -> void:
|
||||||
|
if entity_id < 0:
|
||||||
|
return
|
||||||
|
var prev: Dictionary = _entity_display.get(entity_id, {})
|
||||||
|
_entity_display[entity_id] = {"name": name, "color_index": color_index}
|
||||||
|
if prev.has("name") and prev.get("name", "") != name:
|
||||||
|
_log_dirty = true
|
||||||
|
|
||||||
|
|
||||||
## Handle conversation_ended — no-op currently (entries expire via timeout).
|
## Handle conversation_ended — no-op currently (entries expire via timeout).
|
||||||
@@ -234,7 +296,8 @@ func show_dialogue(npc_name: String, speech: String, options: Array = []) -> voi
|
|||||||
|
|
||||||
|
|
||||||
## End active player conversation — clears options but preserves log.
|
## End active player conversation — clears options but preserves log.
|
||||||
## D-064: dialogue_active held until fade completes.
|
## D-064: dialogue_active cleared immediately so WASD resumes.
|
||||||
|
## Log entries remain visible and expire via timeout (cosmetic only).
|
||||||
func _end_player_conversation() -> void:
|
func _end_player_conversation() -> void:
|
||||||
_in_player_conversation = false
|
_in_player_conversation = false
|
||||||
_clear_options()
|
_clear_options()
|
||||||
@@ -254,32 +317,21 @@ func _end_player_conversation() -> void:
|
|||||||
# D-061: unpause — signal to main.gd for input recording (#507)
|
# D-061: unpause — signal to main.gd for input recording (#507)
|
||||||
unpause_requested.emit()
|
unpause_requested.emit()
|
||||||
|
|
||||||
# D-064: dialogue_active stays true — cleared after fade completes in hide callback.
|
# D-064: unblock movement immediately — log entries stay visible but don't block input.
|
||||||
# If log entries still exist, panel stays visible and entries expire via timeout.
|
GameState.dialogue_active = false
|
||||||
# If no entries, hide immediately with fade.
|
|
||||||
|
# If no entries remain, hide the panel with fade.
|
||||||
if _log_entries.is_empty():
|
if _log_entries.is_empty():
|
||||||
hide_dialogue()
|
hide_dialogue()
|
||||||
|
|
||||||
|
|
||||||
## Hide the entire panel with fade.
|
## End active dialogue state. Panel stays visible (permanent insert UI element).
|
||||||
func hide_dialogue() -> void:
|
func hide_dialogue() -> void:
|
||||||
if not _is_showing:
|
|
||||||
return
|
|
||||||
|
|
||||||
if _in_player_conversation:
|
if _in_player_conversation:
|
||||||
_end_player_conversation()
|
_end_player_conversation()
|
||||||
return # _end_player_conversation may call hide_dialogue if log is empty
|
return # _end_player_conversation may call hide_dialogue if log is empty
|
||||||
|
|
||||||
_is_showing = false
|
GameState.dialogue_active = false
|
||||||
|
|
||||||
if _active_tween and _active_tween.is_valid():
|
|
||||||
_active_tween.kill()
|
|
||||||
_active_tween = create_tween()
|
|
||||||
_active_tween.tween_property(panel, "modulate:a", 0.0, FADE_OUT)
|
|
||||||
_active_tween.tween_callback(func():
|
|
||||||
visible = false
|
|
||||||
GameState.dialogue_active = false # D-064: unblock movement after fade completes
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
func is_dialogue_active() -> bool:
|
func is_dialogue_active() -> bool:
|
||||||
@@ -302,18 +354,15 @@ func _show_options(options: Array) -> void:
|
|||||||
var raw_text: String = opt.get("text", "")
|
var raw_text: String = opt.get("text", "")
|
||||||
var is_confrontation: bool = opt.get("confrontation", false)
|
var is_confrontation: bool = opt.get("confrontation", false)
|
||||||
|
|
||||||
var label := RichTextLabel.new()
|
var label := Label.new()
|
||||||
label.bbcode_enabled = true
|
label.add_theme_font_size_override("font_size", 14)
|
||||||
label.fit_content = true
|
label.add_theme_color_override("font_color", Constants.INSERT_COLOR_TEXT)
|
||||||
label.scroll_active = false
|
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
label.mouse_filter = Control.MOUSE_FILTER_STOP
|
label.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
label.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
label.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||||
label.add_theme_color_override("default_color", Constants.INSERT_COLOR_TEXT)
|
|
||||||
|
|
||||||
if is_confrontation:
|
var numbered_text := "%d. %s" % [i + 1, raw_text]
|
||||||
label.text = "[i]%s[/i]" % raw_text
|
label.text = numbered_text
|
||||||
else:
|
|
||||||
label.text = raw_text
|
|
||||||
|
|
||||||
var idx := i
|
var idx := i
|
||||||
label.gui_input.connect(func(event: InputEvent):
|
label.gui_input.connect(func(event: InputEvent):
|
||||||
@@ -407,15 +456,42 @@ func _rebuild_log() -> void:
|
|||||||
## Hoshe #2: escape BBCode brackets in server-sourced strings.
|
## Hoshe #2: escape BBCode brackets in server-sourced strings.
|
||||||
## Araminta: passive lines get ┃ prefix + desaturated colours.
|
## Araminta: passive lines get ┃ prefix + desaturated colours.
|
||||||
## Non-blocking: 1-on-1 player dialogue simplifies to "Speaker:" (no → You).
|
## Non-blocking: 1-on-1 player dialogue simplifies to "Speaker:" (no → You).
|
||||||
|
## Entity-anchored entries resolve display name and color from _entity_display.
|
||||||
func _format_entry(entry: Dictionary, alpha: float) -> String:
|
func _format_entry(entry: Dictionary, alpha: float) -> String:
|
||||||
var speaker: String = _escape_bbcode(entry.speaker)
|
var speaker: String
|
||||||
var target: String = _escape_bbcode(entry.target)
|
var target: String
|
||||||
|
var speaker_color: Color
|
||||||
|
var target_color: Color
|
||||||
|
var involves_player: bool
|
||||||
|
|
||||||
|
if entry.has("speaker_id"):
|
||||||
|
# Entity-anchored entry: resolve from _entity_display registry
|
||||||
|
var sp_data: Dictionary = _entity_display.get(entry["speaker_id"], {})
|
||||||
|
var tg_data: Dictionary = _entity_display.get(entry.get("target_id", -1), {})
|
||||||
|
speaker = _escape_bbcode(sp_data.get("name", "?"))
|
||||||
|
target = _escape_bbcode(tg_data.get("name", "?"))
|
||||||
|
var sp_ci: int = sp_data.get("color_index", -1)
|
||||||
|
var tg_ci: int = tg_data.get("color_index", -1)
|
||||||
|
if sp_ci >= 0 and not _npc_colors.is_empty():
|
||||||
|
speaker_color = _enforce_contrast(_npc_colors[sp_ci % _npc_colors.size()])
|
||||||
|
else:
|
||||||
|
speaker_color = _color_for_name(sp_data.get("name", "?"))
|
||||||
|
if tg_ci >= 0 and not _npc_colors.is_empty():
|
||||||
|
target_color = _enforce_contrast(_npc_colors[tg_ci % _npc_colors.size()])
|
||||||
|
else:
|
||||||
|
target_color = _color_for_name(tg_data.get("name", "?"))
|
||||||
|
involves_player = false # Overheard entries never involve the player directly
|
||||||
|
else:
|
||||||
|
# Legacy string-keyed entry (player dialogue, backward compat)
|
||||||
|
speaker = _escape_bbcode(entry.get("speaker", "?"))
|
||||||
|
target = _escape_bbcode(entry.get("target", "?"))
|
||||||
|
speaker_color = _color_for_name(entry.get("speaker", "?"))
|
||||||
|
target_color = _color_for_name(entry.get("target", "?"))
|
||||||
|
involves_player = (entry.get("speaker", "") == PLAYER_NAME) or (entry.get("target", "") == PLAYER_NAME)
|
||||||
|
|
||||||
var text: String = _escape_bbcode(entry.text)
|
var text: String = _escape_bbcode(entry.text)
|
||||||
var is_passive: bool = entry.is_passive
|
var is_passive: bool = entry.is_passive
|
||||||
|
|
||||||
var speaker_color := _color_for_name(entry.speaker)
|
|
||||||
var target_color := _color_for_name(entry.target)
|
|
||||||
|
|
||||||
# Desaturate passive name colours (Araminta review)
|
# Desaturate passive name colours (Araminta review)
|
||||||
if is_passive:
|
if is_passive:
|
||||||
speaker_color = _desaturate(speaker_color, PASSIVE_DESATURATION)
|
speaker_color = _desaturate(speaker_color, PASSIVE_DESATURATION)
|
||||||
@@ -429,7 +505,6 @@ func _format_entry(entry: Dictionary, alpha: float) -> String:
|
|||||||
var prefix := PASSIVE_GLYPH if is_passive else ""
|
var prefix := PASSIVE_GLYPH if is_passive else ""
|
||||||
|
|
||||||
# Non-blocking: simplify 1-on-1 player dialogue — no arrow for Speaker → You or You → Speaker
|
# Non-blocking: simplify 1-on-1 player dialogue — no arrow for Speaker → You or You → Speaker
|
||||||
var involves_player := entry.speaker == PLAYER_NAME or entry.target == PLAYER_NAME
|
|
||||||
if involves_player and not is_passive:
|
if involves_player and not is_passive:
|
||||||
# Just "Speaker: text" or "You: text"
|
# Just "Speaker: text" or "You: text"
|
||||||
return "%s[color=%s][b]%s[/b][/color][color=%s]: %s[/color]" % [
|
return "%s[color=%s][b]%s[/b][/color][color=%s]: %s[/color]" % [
|
||||||
@@ -532,16 +607,9 @@ func _expire_entries() -> void:
|
|||||||
|
|
||||||
# -- Visibility --
|
# -- Visibility --
|
||||||
|
|
||||||
## Ensure panel is visible (fade in if needed).
|
## No-op — panel is always visible as a permanent insert UI element.
|
||||||
func _ensure_visible() -> void:
|
func _ensure_visible() -> void:
|
||||||
if _is_showing:
|
pass
|
||||||
return
|
|
||||||
visible = true
|
|
||||||
_is_showing = true
|
|
||||||
if _active_tween and _active_tween.is_valid():
|
|
||||||
_active_tween.kill()
|
|
||||||
_active_tween = create_tween()
|
|
||||||
_active_tween.tween_property(panel, "modulate:a", 1.0, FADE_IN)
|
|
||||||
|
|
||||||
|
|
||||||
func _clear_options() -> void:
|
func _clear_options() -> void:
|
||||||
@@ -555,11 +623,11 @@ func _clear_options() -> void:
|
|||||||
|
|
||||||
|
|
||||||
# Hover callbacks
|
# Hover callbacks
|
||||||
static func _make_hover_on(label: RichTextLabel) -> Callable:
|
static func _make_hover_on(label: Control) -> Callable:
|
||||||
return func():
|
return func():
|
||||||
label.add_theme_color_override("default_color", Constants.INSERT_COLOR_HOVER)
|
label.add_theme_color_override("font_color", Constants.INSERT_COLOR_HOVER)
|
||||||
|
|
||||||
|
|
||||||
static func _make_hover_off(label: RichTextLabel) -> Callable:
|
static func _make_hover_off(label: Control) -> Callable:
|
||||||
return func():
|
return func():
|
||||||
label.add_theme_color_override("default_color", Constants.INSERT_COLOR_TEXT)
|
label.add_theme_color_override("font_color", Constants.INSERT_COLOR_TEXT)
|
||||||
|
|||||||
@@ -19,10 +19,12 @@ const LABEL_GAP := 2
|
|||||||
const INSERT_FG := Constants.IMPLANT_TEXT_COLOR
|
const INSERT_FG := Constants.IMPLANT_TEXT_COLOR
|
||||||
const INSERT_DIM := Constants.IMPLANT_TEXT_DIM
|
const INSERT_DIM := Constants.IMPLANT_TEXT_DIM
|
||||||
const INSERT_BG := Color(0.05, 0.05, 0.08, 0.7)
|
const INSERT_BG := Color(0.05, 0.05, 0.08, 0.7)
|
||||||
|
const ENTITY_OFFSET := Vector2(0, -12) # nudge above entity sprite center
|
||||||
|
|
||||||
var _showing: bool = false
|
var _showing: bool = false
|
||||||
var _insert_active: bool = true
|
var _insert_active: bool = true
|
||||||
var _current_target_id: int = -1
|
var _current_target_id: int = -1
|
||||||
|
var _entity_world_pos: Vector2 = Vector2.ZERO # cached world tile position of target
|
||||||
var _verb_items: Array = [] # sorted [{kind, label, priority, available}]
|
var _verb_items: Array = [] # sorted [{kind, label, priority, available}]
|
||||||
var _selected_index: int = 0
|
var _selected_index: int = 0
|
||||||
var _active_tween: Tween = null
|
var _active_tween: Tween = null
|
||||||
@@ -37,6 +39,21 @@ func _ready() -> void:
|
|||||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
|
||||||
|
|
||||||
|
func _process(_delta: float) -> void:
|
||||||
|
if _showing:
|
||||||
|
_update_screen_position()
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
if not _showing or _verb_labels.is_empty():
|
||||||
|
return
|
||||||
|
var pad := 6.0
|
||||||
|
var bg_rect := Rect2(-pad, -pad, size.x + pad * 2, size.y + pad * 2)
|
||||||
|
draw_rect(bg_rect, INSERT_BG)
|
||||||
|
draw_rect(bg_rect, Constants.IMPLANT_TEXT_DIM * Color(1, 1, 1, 0.3), false, 1.0)
|
||||||
|
|
||||||
|
|
||||||
func update_from_state() -> void:
|
func update_from_state() -> void:
|
||||||
# D-055: Sprint stance suppresses interaction list
|
# D-055: Sprint stance suppresses interaction list
|
||||||
if GameState.player_stance == "Sprint":
|
if GameState.player_stance == "Sprint":
|
||||||
@@ -70,6 +87,7 @@ func update_from_state() -> void:
|
|||||||
_current_target_id = entity_id
|
_current_target_id = entity_id
|
||||||
_verb_items = sorted
|
_verb_items = sorted
|
||||||
_selected_index = 0
|
_selected_index = 0
|
||||||
|
_cache_entity_position()
|
||||||
_rebuild_labels()
|
_rebuild_labels()
|
||||||
_show()
|
_show()
|
||||||
|
|
||||||
@@ -84,15 +102,46 @@ func _rebuild_labels() -> void:
|
|||||||
for i in range(_verb_items.size()):
|
for i in range(_verb_items.size()):
|
||||||
var verb: Dictionary = _verb_items[i]
|
var verb: Dictionary = _verb_items[i]
|
||||||
var lbl := Label.new()
|
var lbl := Label.new()
|
||||||
lbl.text = verb.get("label", "")
|
lbl.text = " %s " % verb.get("label", "")
|
||||||
lbl.add_theme_font_size_override("font_size", 14)
|
lbl.add_theme_font_size_override("font_size", 14)
|
||||||
lbl.add_theme_color_override("font_color", INSERT_FG if i == _selected_index else INSERT_DIM)
|
lbl.add_theme_color_override("font_color", INSERT_FG if i == _selected_index else INSERT_DIM)
|
||||||
lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
|
lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||||
lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
lbl.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
lbl.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||||
|
var idx := i
|
||||||
|
lbl.gui_input.connect(func(event: InputEvent):
|
||||||
|
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||||
|
_select_and_interact(idx)
|
||||||
|
)
|
||||||
|
lbl.mouse_entered.connect(func(): _hover_index(idx))
|
||||||
|
lbl.mouse_exited.connect(func(): _unhover_index(idx))
|
||||||
_vbox.add_child(lbl)
|
_vbox.add_child(lbl)
|
||||||
_verb_labels.append(lbl)
|
_verb_labels.append(lbl)
|
||||||
|
|
||||||
|
|
||||||
|
## Cache the target entity's world tile position from GameState.visible_entities.
|
||||||
|
func _cache_entity_position() -> void:
|
||||||
|
for entity in GameState.visible_entities:
|
||||||
|
if entity.get("entity_id") == _current_target_id:
|
||||||
|
_entity_world_pos = Vector2(entity.x, entity.y)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
## Convert entity world position to screen coords and reposition this Control.
|
||||||
|
## Runs every frame while showing so the list tracks the entity as the camera moves.
|
||||||
|
func _update_screen_position() -> void:
|
||||||
|
var camera := get_viewport().get_camera_2d()
|
||||||
|
if camera == null:
|
||||||
|
return
|
||||||
|
var viewport_size := get_viewport_rect().size
|
||||||
|
var cam_center := camera.get_screen_center_position()
|
||||||
|
var zoom: Vector2 = camera.zoom if camera.zoom.length_squared() > 0.01 else Constants.CAMERA_DEFAULT_ZOOM
|
||||||
|
var world_px := _entity_world_pos * Constants.TILE_SIZE
|
||||||
|
var screen_pos := (world_px - cam_center) * zoom + viewport_size / 2.0
|
||||||
|
# Anchor above the entity, centered horizontally
|
||||||
|
position = screen_pos + ENTITY_OFFSET * zoom - Vector2(size.x / 2.0, size.y)
|
||||||
|
|
||||||
|
|
||||||
func _show() -> void:
|
func _show() -> void:
|
||||||
if _showing:
|
if _showing:
|
||||||
return
|
return
|
||||||
@@ -161,6 +210,29 @@ func get_verb_labels() -> Array:
|
|||||||
return labels
|
return labels
|
||||||
|
|
||||||
|
|
||||||
|
func _hover_index(idx: int) -> void:
|
||||||
|
_selected_index = idx
|
||||||
|
_update_label_colors()
|
||||||
|
|
||||||
|
|
||||||
|
func _unhover_index(_idx: int) -> void:
|
||||||
|
pass # keep last hover highlighted
|
||||||
|
|
||||||
|
|
||||||
|
func _select_and_interact(idx: int) -> void:
|
||||||
|
if idx < 0 or idx >= _verb_items.size():
|
||||||
|
return
|
||||||
|
_selected_index = idx
|
||||||
|
verb_selected.emit(_verb_items[idx].get("kind", ""), _current_target_id)
|
||||||
|
|
||||||
|
|
||||||
|
func _update_label_colors() -> void:
|
||||||
|
for i in range(_verb_labels.size()):
|
||||||
|
if is_instance_valid(_verb_labels[i]):
|
||||||
|
_verb_labels[i].add_theme_color_override(
|
||||||
|
"font_color", INSERT_FG if i == _selected_index else INSERT_DIM)
|
||||||
|
|
||||||
|
|
||||||
func set_insert_active(active: bool) -> void:
|
func set_insert_active(active: bool) -> void:
|
||||||
_insert_active = active
|
_insert_active = active
|
||||||
if not active and _showing:
|
if not active and _showing:
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ func _show_line(text: String, duration: float, priority: int, is_urgent: bool, l
|
|||||||
node = line_node,
|
node = line_node,
|
||||||
expire_timer = maxf(duration, MIN_DURATION), # clamp: survives own fade-in
|
expire_timer = maxf(duration, MIN_DURATION), # clamp: survives own fade-in
|
||||||
priority = priority,
|
priority = priority,
|
||||||
tween = null as Tween,
|
tween = null,
|
||||||
}
|
}
|
||||||
_visible.append(slot)
|
_visible.append(slot)
|
||||||
_next_fade_in_msec = float(Time.get_ticks_msec()) + STAGGER_SEC * 1000.0
|
_next_fade_in_msec = float(Time.get_ticks_msec()) + STAGGER_SEC * 1000.0
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cpjq8yfsnpr5m
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
; D-058: World radial menu — right-click, 2 spokes (Observe + Insert)
|
; D-058: World radial menu — right-click, 2 spokes (Observe + Insert)
|
||||||
[node name="WorldRadial" type="Control"]
|
[node name="WorldRadial" type="Control"]
|
||||||
layout_mode = 3
|
layout_mode = 3
|
||||||
anchors_preset = 15
|
anchors_preset = 0
|
||||||
anchor_right = 1.0
|
anchor_right = 1.0
|
||||||
anchor_bottom = 1.0
|
anchor_bottom = 1.0
|
||||||
grow_horizontal = 2
|
grow_horizontal = 2
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ Every dialogue line requires 6 structural tags + up to 3 selection tags.
|
|||||||
|
|
||||||
| Tag | Type | Format | Description |
|
| Tag | Type | Format | Description |
|
||||||
|-----|------|--------|-------------|
|
|-----|------|--------|-------------|
|
||||||
| `id` | string | `{location}_{d}_{###}` | Stable line ID. Machine-parseable. Never reused. |
|
| `id` | string | `{npc-slug}_d_{###}` | Stable line ID. Machine-parseable. Never reused. NPC-scoped per D-035 Sprint 15 amendment. |
|
||||||
| `text` | string | The line | The authored dialogue text |
|
| `text` | string | The line | The authored dialogue text |
|
||||||
| `role` | enum | lowercase-slug | Template role, not NPC name. NPCs fill roles at runtime. |
|
| `role` | enum | lowercase-slug | Template role, not NPC name. NPCs fill roles at runtime. |
|
||||||
| `access` | list | `[public, insider, ...]` | D-028 Layer 1 hard filter. Who can hear this. |
|
| `access` | list | `[public, insider, ...]` | D-028 Layer 1 hard filter. Who can hear this. |
|
||||||
@@ -265,7 +265,7 @@ Every dialogue line requires 6 structural tags + up to 3 selection tags.
|
|||||||
| Tag | Type | Format | Description |
|
| Tag | Type | Format | Description |
|
||||||
|-----|------|--------|-------------|
|
|-----|------|--------|-------------|
|
||||||
| `topic` | list | `[colleague, cargo, ...]` | D-028 Layer 4 weighted selection. Defaults to `[routine]`. |
|
| `topic` | list | `[colleague, cargo, ...]` | D-028 Layer 4 weighted selection. Defaults to `[routine]`. |
|
||||||
| `mood` | list | `[comfortable, worried, ...]` | D-028 Layer 4 weighted selection. Defaults to `[comfortable]`. |
|
| `mood` | list | `[content, anxious, ...]` | D-028 Layer 4 weighted selection. Defaults to untagged (neutral). |
|
||||||
| `tags` | list | freeform strings | Escape hatch for author intent. |
|
| `tags` | list | freeform strings | Escape hatch for author intent. |
|
||||||
|
|
||||||
#### Authoring-Only Tags (Not Consumed by Engine)
|
#### Authoring-Only Tags (Not Consumed by Engine)
|
||||||
@@ -352,7 +352,7 @@ Same trigger, different pool. That's how mirror moments work.
|
|||||||
|
|
||||||
| Tag | Type | Description |
|
| Tag | Type | Description |
|
||||||
|-----|------|-------------|
|
|-----|------|-------------|
|
||||||
| `id` | string | `{location}_m_{s\|d}_{###}` — `s` for smuggler, `d` for detective |
|
| `id` | string | `{npc-slug}_m_{s\|d}_{###}` — `s` for smuggler, `d` for detective. NPC-scoped per D-035 Sprint 15 amendment. |
|
||||||
| `text` | string | The monologue line (160 char max) |
|
| `text` | string | The monologue line (160 char max) |
|
||||||
| `character` | enum | `smuggler` or `detective` |
|
| `character` | enum | `smuggler` or `detective` |
|
||||||
| `trigger` | enum | What causes this line to fire (see trigger types below) |
|
| `trigger` | enum | What causes this line to fire (see trigger types below) |
|
||||||
@@ -600,9 +600,9 @@ See `content/global/enums/topics.yaml`. Note: `crime` is deliberately excluded.
|
|||||||
|
|
||||||
### Moods (8 values)
|
### Moods (8 values)
|
||||||
|
|
||||||
`fond`, `comfortable`, `worried`, `suspicious`, `analytical`, `conflicted`, `concerned`, `relieved`
|
`anxious`, `frustrated`, `content`, `suspicious`, `warm`, `hostile`, `relieved`, `focused`
|
||||||
|
|
||||||
See `content/global/enums/moods.yaml`.
|
See `content/global/enums/moods.yaml`. (Updated Sprint 14 amendment to D-035.)
|
||||||
|
|
||||||
### Monologue Triggers (9 values)
|
### Monologue Triggers (9 values)
|
||||||
|
|
||||||
@@ -630,7 +630,7 @@ Before submitting any content, run through these checks. Items marked **HARD** w
|
|||||||
|
|
||||||
### Dialogue Lines
|
### Dialogue Lines
|
||||||
|
|
||||||
- [ ] **HARD:** `id` matches pattern `{location}_d_{###}` (lowercase, underscore-separated)
|
- [ ] **HARD:** `id` matches pattern `{npc-slug}_d_{###}` (e.g. `kael-davan_d_001`) — NPC-scoped per D-035 Sprint 15 amendment
|
||||||
- [ ] **HARD:** `role` is a valid lowercase slug matching a template role
|
- [ ] **HARD:** `role` is a valid lowercase slug matching a template role
|
||||||
- [ ] **HARD:** `access` is a non-empty list of valid access tier enums
|
- [ ] **HARD:** `access` is a non-empty list of valid access tier enums
|
||||||
- [ ] **HARD:** `trust` is one of `surface`, `real`, `secret`
|
- [ ] **HARD:** `trust` is one of `surface`, `real`, `secret`
|
||||||
@@ -643,7 +643,7 @@ Before submitting any content, run through these checks. Items marked **HARD** w
|
|||||||
|
|
||||||
### Monologue Lines
|
### Monologue Lines
|
||||||
|
|
||||||
- [ ] **HARD:** `id` matches pattern `{location}_m_{s|d}_{###}`
|
- [ ] **HARD:** `id` matches pattern `{npc-slug}_m_{s|d}_{###}` (e.g. `pc-smuggler_m_s_001`) — NPC-scoped per D-035 Sprint 15 amendment
|
||||||
- [ ] **HARD:** `text` is 160 characters or fewer
|
- [ ] **HARD:** `text` is 160 characters or fewer
|
||||||
- [ ] **HARD:** `trigger` is a valid trigger enum
|
- [ ] **HARD:** `trigger` is a valid trigger enum
|
||||||
- [ ] **HARD:** Character partition: smuggler lines in smuggler files, detective in detective files
|
- [ ] **HARD:** Character partition: smuggler lines in smuggler files, detective in detective files
|
||||||
@@ -755,9 +755,9 @@ All YAML content files must validate against their JSON Schema:
|
|||||||
|
|
||||||
| Content Type | Pattern | Example |
|
| Content Type | Pattern | Example |
|
||||||
|-------------|---------|---------|
|
|-------------|---------|---------|
|
||||||
| Dialogue | `{location}_d_{###}` | `transit_d_044` |
|
| Dialogue | `{npc-slug}_d_{###}` | `kael-davan_d_001` |
|
||||||
| Monologue (smuggler) | `{location}_m_s_{###}` | `transit_m_s_011` |
|
| Monologue (smuggler) | `{npc-slug}_m_s_{###}` | `pc-smuggler_m_s_001` |
|
||||||
| Monologue (detective) | `{location}_m_d_{###}` | `transit_m_d_042` |
|
| Monologue (detective) | `{npc-slug}_m_d_{###}` | `pc-detective_m_d_001` |
|
||||||
|
|
||||||
IDs are stable identifiers. Once assigned, never reused or reassigned. Number gaps are acceptable.
|
IDs are stable identifiers. Once assigned, never reused or reassigned. Number gaps are acceptable.
|
||||||
|
|
||||||
@@ -865,26 +865,26 @@ Every generated line must pass:
|
|||||||
### Step 2: Assign Tags
|
### Step 2: Assign Tags
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- id: transit_d_071
|
- id: kael-davan_d_076
|
||||||
text: "Morning. Voss kept the rotation thin -- we've got a clear window after 14:00."
|
text: "Morning. Voss kept the rotation thin -- we've got a clear window after 14:00."
|
||||||
role: dock-worker
|
role: dock-worker
|
||||||
access: [insider]
|
access: [insider]
|
||||||
trust: real
|
trust: real
|
||||||
situation: [shift_start]
|
situation: [shift_start]
|
||||||
topic: [cargo, routine]
|
topic: [cargo, routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [ring-operational]
|
tags: [ring-operational]
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 3: Validate
|
### Step 3: Validate
|
||||||
|
|
||||||
- `id`: `transit_d_071` -- valid pattern.
|
- `id`: `kael-davan_d_076` -- valid pattern (NPC-scoped per D-035 Sprint 15 amendment). Continues from kael-davan's terminal sequence (_034-_075).
|
||||||
- `role`: `dock-worker` -- valid slug.
|
- `role`: `dock-worker` -- valid slug.
|
||||||
- `access: [insider]` -- only ring members hear this. Detective never sees it. Correct.
|
- `access: [insider]` -- only ring members hear this. Detective never sees it. Correct.
|
||||||
- `trust: real` -- requires earned trust. Not surface-level small talk. Correct for operational ring dialogue.
|
- `trust: real` -- requires earned trust. Not surface-level small talk. Correct for operational ring dialogue.
|
||||||
- `situation: [shift_start]` -- morning shift context. Correct.
|
- `situation: [shift_start]` -- morning shift context. Correct.
|
||||||
- `topic: [cargo, routine]` -- covers both operational and schedule content. Correct.
|
- `topic: [cargo, routine]` -- covers both operational and schedule content. Correct.
|
||||||
- `mood: [comfortable]` -- Phase 1, baseline warm. Correct.
|
- `mood: [content]` -- Phase 1, baseline warm. Correct.
|
||||||
|
|
||||||
### Step 4: Dual-Lens Check
|
### Step 4: Dual-Lens Check
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"id": {
|
"id": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"pattern": "^[a-z][a-z0-9-]*_(d|m)_[0-9]{3}$",
|
"pattern": "^[a-z][a-z0-9-]*_(d|m)_[0-9]{3}$",
|
||||||
"description": "Stable machine-parseable line ID: {template}_{d|m}_{###}. d = dialogue, m = monologue."
|
"description": "Stable machine-parseable line ID: {npc-slug}_{d|m}_{###} (e.g. kael-davan_d_001, pc-detective_m_d_001). d = dialogue, m = monologue. NPC-scoped per D-035 Sprint 15 amendment — each NPC has an independent sequence starting at _001."
|
||||||
},
|
},
|
||||||
"text": {
|
"text": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
"id": {
|
"id": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"pattern": "^[a-z][a-z0-9-]*_d_[0-9]{3}$",
|
"pattern": "^[a-z][a-z0-9-]*_d_[0-9]{3}$",
|
||||||
"description": "Stable line ID: {location_slug}_d_{###}"
|
"description": "Stable line ID: {npc-slug}_d_{###} (e.g. kael-davan_d_001). NPC-scoped per D-035 Sprint 15 amendment — each NPC has an independent sequence starting at _001."
|
||||||
},
|
},
|
||||||
"text": {
|
"text": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
"id": {
|
"id": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"pattern": "^[a-z][a-z0-9-]*_m_[sd]_[0-9]{3}$",
|
"pattern": "^[a-z][a-z0-9-]*_m_[sd]_[0-9]{3}$",
|
||||||
"description": "Stable line ID: {location_slug}_m_{s|d}_{###}. s = smuggler, d = detective."
|
"description": "Stable line ID: {npc-slug}_m_{s|d}_{###} (e.g. pc-smuggler_m_s_001, pc-detective_m_d_001). s = smuggler, d = detective. NPC-scoped per D-035 Sprint 15 amendment — each NPC has an independent sequence starting at _001."
|
||||||
},
|
},
|
||||||
"text": {
|
"text": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ lines:
|
|||||||
# RING OPERATIONS — corridor handoffs
|
# RING OPERATIONS — corridor handoffs
|
||||||
# ========================================
|
# ========================================
|
||||||
|
|
||||||
- id: maintenance-corridors_d_001
|
- id: kael-davan_d_001
|
||||||
text: "Package is in the junction locker. Renn picks it up in ten."
|
text: "Package is in the junction locker. Renn picks it up in ten."
|
||||||
role: dock-worker
|
role: dock-worker
|
||||||
access: [insider]
|
access: [insider]
|
||||||
@@ -23,7 +23,7 @@ lines:
|
|||||||
topic: [cargo]
|
topic: [cargo]
|
||||||
tags: [kael, ring-ops, renn, operational]
|
tags: [kael, ring-ops, renn, operational]
|
||||||
|
|
||||||
- id: maintenance-corridors_d_002
|
- id: kael-davan_d_002
|
||||||
text: "B-7 is clear. Camera cycles in forty seconds — move fast."
|
text: "B-7 is clear. Camera cycles in forty seconds — move fast."
|
||||||
role: dock-worker
|
role: dock-worker
|
||||||
access: [insider]
|
access: [insider]
|
||||||
@@ -35,23 +35,23 @@ lines:
|
|||||||
fact_id: investigation.surveillance_gaps
|
fact_id: investigation.surveillance_gaps
|
||||||
confidence: knows_details
|
confidence: knows_details
|
||||||
|
|
||||||
- id: maintenance-corridors_d_003
|
- id: kael-davan_d_003
|
||||||
text: "Chalk marks are fresh. Renn came through already. We're on schedule."
|
text: "Chalk marks are fresh. Renn came through already. We're on schedule."
|
||||||
role: dock-worker
|
role: dock-worker
|
||||||
access: [insider]
|
access: [insider]
|
||||||
trust: real
|
trust: real
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
topic: [cargo]
|
topic: [cargo]
|
||||||
tags: [kael, ring-ops, renn, operational]
|
tags: [kael, ring-ops, renn, operational]
|
||||||
|
|
||||||
- id: maintenance-corridors_d_004
|
- id: kael-davan_d_004
|
||||||
text: "Don't use the C-section hatch tonight. Maintenance crew's working late."
|
text: "Don't use the C-section hatch tonight. Maintenance crew's working late."
|
||||||
role: dock-worker
|
role: dock-worker
|
||||||
access: [insider]
|
access: [insider]
|
||||||
trust: real
|
trust: real
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
mood: [concerned]
|
mood: [frustrated]
|
||||||
topic: [danger, cargo]
|
topic: [danger, cargo]
|
||||||
tags: [kael, ring-ops, operational, caution]
|
tags: [kael, ring-ops, operational, caution]
|
||||||
|
|
||||||
@@ -60,33 +60,33 @@ lines:
|
|||||||
# When the smuggler finds Kael with the unknown contact
|
# When the smuggler finds Kael with the unknown contact
|
||||||
# ========================================
|
# ========================================
|
||||||
|
|
||||||
- id: maintenance-corridors_d_005
|
- id: kael-davan_d_005
|
||||||
text: "What are you — wait. How long have you been standing there?"
|
text: "What are you — wait. How long have you been standing there?"
|
||||||
role: dock-worker
|
role: dock-worker
|
||||||
access: [insider]
|
access: [insider]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [confrontation]
|
situation: [confrontation]
|
||||||
mood: [concerned]
|
mood: [frustrated]
|
||||||
topic: [danger]
|
topic: [danger]
|
||||||
tags: [kael, contradiction, phase-3]
|
tags: [kael, contradiction, phase-3]
|
||||||
|
|
||||||
- id: maintenance-corridors_d_006
|
- id: kael-davan_d_006
|
||||||
text: "That was nobody. Maintenance contact. It's handled."
|
text: "That was nobody. Maintenance contact. It's handled."
|
||||||
role: dock-worker
|
role: dock-worker
|
||||||
access: [insider]
|
access: [insider]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [confrontation]
|
situation: [confrontation]
|
||||||
mood: [concerned]
|
mood: [frustrated]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
tags: [kael, deflection, lying, phase-4]
|
tags: [kael, deflection, lying, phase-4]
|
||||||
|
|
||||||
- id: maintenance-corridors_d_007
|
- id: kael-davan_d_007
|
||||||
text: "I don't have to explain every conversation I have. Drop it."
|
text: "I don't have to explain every conversation I have. Drop it."
|
||||||
role: dock-worker
|
role: dock-worker
|
||||||
access: [insider]
|
access: [insider]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [confrontation]
|
situation: [confrontation]
|
||||||
mood: [concerned]
|
mood: [frustrated]
|
||||||
topic: [trust]
|
topic: [trust]
|
||||||
tags: [kael, deflection, hostile-path, phase-4]
|
tags: [kael, deflection, hostile-path, phase-4]
|
||||||
|
|
||||||
@@ -94,35 +94,91 @@ lines:
|
|||||||
# SECRET — corridor confessions
|
# SECRET — corridor confessions
|
||||||
# ========================================
|
# ========================================
|
||||||
|
|
||||||
- id: maintenance-corridors_d_008
|
- id: kael-davan_d_008
|
||||||
text: "I'm trying to get out. That's all. I just want out."
|
text: "I'm trying to get out. That's all. I just want out."
|
||||||
role: dock-worker
|
role: dock-worker
|
||||||
access: [insider]
|
access: [insider]
|
||||||
trust: secret
|
trust: secret
|
||||||
situation: [confrontation, alone]
|
situation: [confrontation, alone]
|
||||||
mood: [conflicted]
|
mood: [anxious]
|
||||||
topic: [personal, trust]
|
topic: [personal, trust]
|
||||||
tags: [kael, confession, phase-5]
|
tags: [kael, confession, phase-5]
|
||||||
|
|
||||||
- id: maintenance-corridors_d_009
|
- id: kael-davan_d_009
|
||||||
text: "Nils will kill me if he finds out. You know that. You know him."
|
text: "Nils will kill me if he finds out. You know that. You know him."
|
||||||
role: dock-worker
|
role: dock-worker
|
||||||
access: [insider]
|
access: [insider]
|
||||||
trust: secret
|
trust: secret
|
||||||
situation: [confrontation, alone]
|
situation: [confrontation, alone]
|
||||||
mood: [worried]
|
mood: [anxious]
|
||||||
topic: [danger, trust]
|
topic: [danger, trust]
|
||||||
tags: [kael, confession, nils, phase-5]
|
tags: [kael, confession, nils, phase-5]
|
||||||
|
|
||||||
- id: maintenance-corridors_d_010
|
- id: kael-davan_d_010
|
||||||
text: "I found someone who can get me new papers. Off-station. Clean break."
|
text: "I found someone who can get me new papers. Off-station. Clean break."
|
||||||
role: dock-worker
|
role: dock-worker
|
||||||
access: [insider]
|
access: [insider]
|
||||||
trust: secret
|
trust: secret
|
||||||
situation: [alone]
|
situation: [alone]
|
||||||
mood: [conflicted]
|
mood: [anxious]
|
||||||
topic: [personal]
|
topic: [personal]
|
||||||
tags: [kael, confession, phase-5]
|
tags: [kael, confession, phase-5]
|
||||||
knowledge_grant:
|
knowledge_grant:
|
||||||
fact_id: knowledge.kael_exit_plan
|
fact_id: knowledge.kael_exit_plan
|
||||||
confidence: knows_details
|
confidence: knows_details
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# LAYER 2 GREETING VARIANTS — D-028 Layer 2
|
||||||
|
# Ticket #170 (Sprint 15). situation: [greeting] fires on initial contact.
|
||||||
|
# Access tier + mood encode relationship history state.
|
||||||
|
# Context: restricted maintenance corridors. Any stranger here is immediately
|
||||||
|
# suspicious. Even trusted contacts get terse, operational greetings.
|
||||||
|
# ========================================
|
||||||
|
|
||||||
|
# first_meeting: InteractionMemory.count == 0. Any stranger in restricted corridors is a problem.
|
||||||
|
- id: kael-davan_d_011
|
||||||
|
text: "This is a restricted section. Who authorized you back here?"
|
||||||
|
role: dock-worker
|
||||||
|
access: [public]
|
||||||
|
trust: surface
|
||||||
|
situation: [greeting]
|
||||||
|
mood: [focused]
|
||||||
|
topic: [danger, routine]
|
||||||
|
tags: [kael, greeting, first-meeting, layer2]
|
||||||
|
notes: "Layer 2 greeting — first meeting in restricted corridors. Immediate challenge. Any stranger back here is suspicious regardless of intent."
|
||||||
|
|
||||||
|
# established — peer/smuggler context: count >= 3, RelationshipState: Known/Friendly → Peer/Insider access.
|
||||||
|
- id: kael-davan_d_012
|
||||||
|
text: "You're on time. Clear all the way to the junction."
|
||||||
|
role: dock-worker
|
||||||
|
access: [peer, insider]
|
||||||
|
trust: surface
|
||||||
|
situation: [greeting]
|
||||||
|
mood: [content]
|
||||||
|
topic: [cargo, colleague]
|
||||||
|
tags: [kael, greeting, repeat-visit, peer-tone, layer2]
|
||||||
|
notes: "Layer 2 greeting — established, operational. All business. Kael treats a trusted repeat visitor as crew — no pleasantries, just the essential handoff information."
|
||||||
|
|
||||||
|
# established — authority/detective context: count >= 3, RelationshipState: PersonOfInterest → Authority access.
|
||||||
|
- id: kael-davan_d_013
|
||||||
|
text: "You keep showing up back here. You have clearance for this section?"
|
||||||
|
role: dock-worker
|
||||||
|
access: [authority]
|
||||||
|
trust: surface
|
||||||
|
situation: [greeting]
|
||||||
|
mood: [suspicious]
|
||||||
|
topic: [danger, routine]
|
||||||
|
tags: [kael, greeting, repeat-visit, authority-tone, layer2]
|
||||||
|
notes: "Layer 2 greeting — repeat with authority figure in restricted section. Professionally defensive. Kael challenges them but can't bar entry if they have clearance."
|
||||||
|
|
||||||
|
# post-confrontation: confrontation logged. Kael is cornered on his own operational ground.
|
||||||
|
- id: kael-davan_d_014
|
||||||
|
text: "You followed me here."
|
||||||
|
role: dock-worker
|
||||||
|
access: [peer, insider]
|
||||||
|
trust: surface
|
||||||
|
situation: [greeting]
|
||||||
|
mood: [hostile, suspicious]
|
||||||
|
topic: [danger, trust]
|
||||||
|
tags: [kael, greeting, post-confrontation, layer2]
|
||||||
|
notes: "Layer 2 greeting — post-confrontation, restricted corridors. Accusatory, not a question. Kael feels cornered. This is where the contradiction lives."
|
||||||
|
|||||||
@@ -21,67 +21,67 @@ lines:
|
|||||||
# SURFACE COVER — sounds like maintenance work
|
# SURFACE COVER — sounds like maintenance work
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
- id: maintenance-corridors_d_100
|
- id: ring-operative_d_001
|
||||||
text: "Running maintenance on the B-7 conduit. Could be a while."
|
text: "Running maintenance on the B-7 conduit. Could be a while."
|
||||||
role: ring-operative
|
role: ring-operative
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [ring-operative, surface-cover, b7]
|
tags: [ring-operative, surface-cover, b7]
|
||||||
dual_lens:
|
dual_lens:
|
||||||
smuggler: "Cover phrase. He's not doing maintenance."
|
smuggler: "Cover phrase. He's not doing maintenance."
|
||||||
detective: "Maintenance worker. Conduit repair. Routine."
|
detective: "Maintenance worker. Conduit repair. Routine."
|
||||||
notes: "'Running maintenance' is the canonical ring cover phrase."
|
notes: "'Running maintenance' is the canonical ring cover phrase."
|
||||||
|
|
||||||
- id: maintenance-corridors_d_101
|
- id: ring-operative_d_002
|
||||||
text: "Bay four access from here is faster than going around. If you're in a hurry."
|
text: "Bay four access from here is faster than going around. If you're in a hurry."
|
||||||
role: ring-operative
|
role: ring-operative
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [ring-operative, surface-cover, bay-four, spatial]
|
tags: [ring-operative, surface-cover, bay-four, spatial]
|
||||||
dual_lens:
|
dual_lens:
|
||||||
smuggler: "He's telling me the route is faster. This is spatial information, not just courtesy."
|
smuggler: "He's telling me the route is faster. This is spatial information, not just courtesy."
|
||||||
detective: "Ring operative casually establishes corridor-to-bay-four spatial relationship. Note it."
|
detective: "Ring operative casually establishes corridor-to-bay-four spatial relationship. Note it."
|
||||||
|
|
||||||
- id: maintenance-corridors_d_102
|
- id: ring-operative_d_003
|
||||||
text: "Quiet in here tonight. Good working conditions."
|
text: "Quiet in here tonight. Good working conditions."
|
||||||
role: ring-operative
|
role: ring-operative
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [ring-operative, surface-cover, ambient]
|
tags: [ring-operative, surface-cover, ambient]
|
||||||
dual_lens:
|
dual_lens:
|
||||||
smuggler: "Dead air confirmed. Meridian's not picking this up."
|
smuggler: "Dead air confirmed. Meridian's not picking this up."
|
||||||
detective: "Routine corridor comment."
|
detective: "Routine corridor comment."
|
||||||
|
|
||||||
- id: maintenance-corridors_d_103
|
- id: ring-operative_d_004
|
||||||
text: "Pael was through here earlier. Seal replacement on the ventilation unit."
|
text: "Pael was through here earlier. Seal replacement on the ventilation unit."
|
||||||
role: ring-operative
|
role: ring-operative
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [routine, colleague]
|
topic: [routine, colleague]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [ring-operative, surface-cover, pael, corridor-traffic]
|
tags: [ring-operative, surface-cover, pael, corridor-traffic]
|
||||||
dual_lens:
|
dual_lens:
|
||||||
smuggler: "Pael was through. Legitimate. Corridor's been active — that means traffic covers us."
|
smuggler: "Pael was through. Legitimate. Corridor's been active — that means traffic covers us."
|
||||||
detective: "References Pael's B-7 presence without suspicion. Maintenance workers know each other's routes."
|
detective: "References Pael's B-7 presence without suspicion. Maintenance workers know each other's routes."
|
||||||
|
|
||||||
- id: maintenance-corridors_d_104
|
- id: ring-operative_d_005
|
||||||
text: "C-4 access is at the far end. About three minutes from here."
|
text: "C-4 access is at the far end. About three minutes from here."
|
||||||
role: ring-operative
|
role: ring-operative
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [ring-operative, surface-cover, c4, spatial]
|
tags: [ring-operative, surface-cover, c4, spatial]
|
||||||
dual_lens:
|
dual_lens:
|
||||||
smuggler: "He's giving me the timing. Three minutes to C-4 from this point."
|
smuggler: "He's giving me the timing. Three minutes to C-4 from this point."
|
||||||
@@ -94,7 +94,7 @@ lines:
|
|||||||
# OPERATIONAL COORDINATION — insider/real only
|
# OPERATIONAL COORDINATION — insider/real only
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
- id: maintenance-corridors_d_105
|
- id: ring-operative_d_006
|
||||||
text: "Cans are ready. Bay four, per the schedule."
|
text: "Cans are ready. Bay four, per the schedule."
|
||||||
role: ring-operative
|
role: ring-operative
|
||||||
access: [insider]
|
access: [insider]
|
||||||
@@ -107,7 +107,7 @@ lines:
|
|||||||
smuggler: "Containers cleared. Bay four. We're on."
|
smuggler: "Containers cleared. Bay four. We're on."
|
||||||
detective: "Never hears this line."
|
detective: "Never hears this line."
|
||||||
|
|
||||||
- id: maintenance-corridors_d_106
|
- id: ring-operative_d_007
|
||||||
text: "Manifest is clean. Window opens in eighteen minutes."
|
text: "Manifest is clean. Window opens in eighteen minutes."
|
||||||
role: ring-operative
|
role: ring-operative
|
||||||
access: [insider]
|
access: [insider]
|
||||||
@@ -120,7 +120,7 @@ lines:
|
|||||||
smuggler: "Paperwork's set. Eighteen minutes. That's earlier than planned — adjust."
|
smuggler: "Paperwork's set. Eighteen minutes. That's earlier than planned — adjust."
|
||||||
detective: "Never hears this line."
|
detective: "Never hears this line."
|
||||||
|
|
||||||
- id: maintenance-corridors_d_107
|
- id: ring-operative_d_008
|
||||||
text: "Usual pickup. Route's the same."
|
text: "Usual pickup. Route's the same."
|
||||||
role: ring-operative
|
role: ring-operative
|
||||||
access: [insider]
|
access: [insider]
|
||||||
@@ -133,7 +133,7 @@ lines:
|
|||||||
smuggler: "Same route, same handoff. No variations."
|
smuggler: "Same route, same handoff. No variations."
|
||||||
detective: "Never hears this line."
|
detective: "Never hears this line."
|
||||||
|
|
||||||
- id: maintenance-corridors_d_108
|
- id: ring-operative_d_009
|
||||||
text: "The gap's clear. Scan's done at 14:20. Next window's yours."
|
text: "The gap's clear. Scan's done at 14:20. Next window's yours."
|
||||||
role: ring-operative
|
role: ring-operative
|
||||||
access: [insider]
|
access: [insider]
|
||||||
@@ -146,17 +146,17 @@ lines:
|
|||||||
smuggler: "14:20 scan, then the window opens. Twenty-five minutes clear."
|
smuggler: "14:20 scan, then the window opens. Twenty-five minutes clear."
|
||||||
detective: "Never hears this line."
|
detective: "Never hears this line."
|
||||||
knowledge_grant:
|
knowledge_grant:
|
||||||
fact_id: location.surveillance_gaps
|
fact_id: investigation.surveillance_gaps
|
||||||
confidence: knows_details
|
confidence: knows_details
|
||||||
|
|
||||||
- id: maintenance-corridors_d_109
|
- id: ring-operative_d_010
|
||||||
text: "Drin was asking about B-7. Twice today."
|
text: "Drin was asking about B-7. Twice today."
|
||||||
role: ring-operative
|
role: ring-operative
|
||||||
access: [insider]
|
access: [insider]
|
||||||
trust: real
|
trust: real
|
||||||
situation: [routine, confrontation]
|
situation: [routine, confrontation]
|
||||||
topic: [danger, colleague]
|
topic: [danger, colleague]
|
||||||
mood: [concerned]
|
mood: [frustrated]
|
||||||
tags: [ring-operative, ring-ops, coded, drin-asking, warning]
|
tags: [ring-operative, ring-ops, coded, drin-asking, warning]
|
||||||
dual_lens:
|
dual_lens:
|
||||||
smuggler: "Drin's circling B-7. That's a warning. Push the pickup or hold it."
|
smuggler: "Drin's circling B-7. That's a warning. Push the pickup or hold it."
|
||||||
@@ -165,7 +165,7 @@ lines:
|
|||||||
fact_id: investigation.drin_inspection_pattern
|
fact_id: investigation.drin_inspection_pattern
|
||||||
confidence: suspects
|
confidence: suspects
|
||||||
|
|
||||||
- id: maintenance-corridors_d_110
|
- id: ring-operative_d_011
|
||||||
text: "Dead air through here to the C-4 junction. No coverage."
|
text: "Dead air through here to the C-4 junction. No coverage."
|
||||||
role: ring-operative
|
role: ring-operative
|
||||||
access: [insider]
|
access: [insider]
|
||||||
@@ -178,10 +178,10 @@ lines:
|
|||||||
smuggler: "Full confirmation. Meridian doesn't reach this section."
|
smuggler: "Full confirmation. Meridian doesn't reach this section."
|
||||||
detective: "Never hears this line."
|
detective: "Never hears this line."
|
||||||
knowledge_grant:
|
knowledge_grant:
|
||||||
fact_id: location.surveillance_gaps
|
fact_id: investigation.surveillance_gaps
|
||||||
confidence: knows_details
|
confidence: knows_details
|
||||||
|
|
||||||
- id: maintenance-corridors_d_111
|
- id: ring-operative_d_012
|
||||||
text: "Voss confirmed the hold clears at 1400. He'll run the bay himself."
|
text: "Voss confirmed the hold clears at 1400. He'll run the bay himself."
|
||||||
role: ring-operative
|
role: ring-operative
|
||||||
access: [insider]
|
access: [insider]
|
||||||
@@ -194,7 +194,7 @@ lines:
|
|||||||
smuggler: "Voss is personally running bay four at 1400. That's the signal the window is clear."
|
smuggler: "Voss is personally running bay four at 1400. That's the signal the window is clear."
|
||||||
detective: "Never hears this line."
|
detective: "Never hears this line."
|
||||||
|
|
||||||
- id: maintenance-corridors_d_112
|
- id: ring-operative_d_013
|
||||||
text: "Kael's on dock. He knows."
|
text: "Kael's on dock. He knows."
|
||||||
role: ring-operative
|
role: ring-operative
|
||||||
access: [insider]
|
access: [insider]
|
||||||
@@ -211,53 +211,53 @@ lines:
|
|||||||
# PRESSURE / WARNING SIGNALS
|
# PRESSURE / WARNING SIGNALS
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
- id: maintenance-corridors_d_113
|
- id: ring-operative_d_014
|
||||||
text: "Commission eyes on the terminal floor. Not here yet."
|
text: "Commission eyes on the terminal floor. Not here yet."
|
||||||
role: ring-operative
|
role: ring-operative
|
||||||
access: [insider]
|
access: [insider]
|
||||||
trust: real
|
trust: real
|
||||||
situation: [confrontation, investigation]
|
situation: [confrontation, investigation]
|
||||||
topic: [danger, institution]
|
topic: [danger, institution]
|
||||||
mood: [concerned]
|
mood: [frustrated]
|
||||||
tags: [ring-operative, ring-ops, commission, warning]
|
tags: [ring-operative, ring-ops, commission, warning]
|
||||||
dual_lens:
|
dual_lens:
|
||||||
smuggler: "Detective is visible on the main floor. Corridors are still clear."
|
smuggler: "Detective is visible on the main floor. Corridors are still clear."
|
||||||
detective: "Never hears this line."
|
detective: "Never hears this line."
|
||||||
|
|
||||||
- id: maintenance-corridors_d_114
|
- id: ring-operative_d_015
|
||||||
text: "Hold the run. Drin's in the corridor section."
|
text: "Hold the run. Drin's in the corridor section."
|
||||||
role: ring-operative
|
role: ring-operative
|
||||||
access: [insider]
|
access: [insider]
|
||||||
trust: real
|
trust: real
|
||||||
situation: [confrontation]
|
situation: [confrontation]
|
||||||
topic: [danger]
|
topic: [danger]
|
||||||
mood: [concerned]
|
mood: [frustrated]
|
||||||
tags: [ring-operative, ring-ops, drin, abort-signal]
|
tags: [ring-operative, ring-ops, drin, abort-signal]
|
||||||
dual_lens:
|
dual_lens:
|
||||||
smuggler: "Stop. Drin's in position. The window's closed."
|
smuggler: "Stop. Drin's in position. The window's closed."
|
||||||
detective: "Never hears this line."
|
detective: "Never hears this line."
|
||||||
|
|
||||||
- id: maintenance-corridors_d_115
|
- id: ring-operative_d_016
|
||||||
text: "Maret filed again. Voss has it. Don't move anything tonight."
|
text: "Maret filed again. Voss has it. Don't move anything tonight."
|
||||||
role: ring-operative
|
role: ring-operative
|
||||||
access: [insider]
|
access: [insider]
|
||||||
trust: real
|
trust: real
|
||||||
situation: [confrontation, routine]
|
situation: [confrontation, routine]
|
||||||
topic: [danger, institution]
|
topic: [danger, institution]
|
||||||
mood: [concerned]
|
mood: [frustrated]
|
||||||
tags: [ring-operative, ring-ops, maret, voss, caution]
|
tags: [ring-operative, ring-ops, maret, voss, caution]
|
||||||
dual_lens:
|
dual_lens:
|
||||||
smuggler: "Maret flagged something. Voss is managing it. Stand down tonight."
|
smuggler: "Maret flagged something. Voss is managing it. Stand down tonight."
|
||||||
detective: "Never hears this line."
|
detective: "Never hears this line."
|
||||||
|
|
||||||
- id: maintenance-corridors_d_116
|
- id: ring-operative_d_017
|
||||||
text: "We're clean. Same time tomorrow."
|
text: "We're clean. Same time tomorrow."
|
||||||
role: ring-operative
|
role: ring-operative
|
||||||
access: [insider]
|
access: [insider]
|
||||||
trust: real
|
trust: real
|
||||||
situation: [shift_end, routine]
|
situation: [shift_end, routine]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [ring-operative, ring-ops, run-complete, closing]
|
tags: [ring-operative, ring-ops, run-complete, closing]
|
||||||
dual_lens:
|
dual_lens:
|
||||||
smuggler: "Run's done. No issues. Tomorrow."
|
smuggler: "Run's done. No issues. Tomorrow."
|
||||||
@@ -267,37 +267,37 @@ lines:
|
|||||||
# IF DETECTIVE SOMEHOW ENTERS — surface only
|
# IF DETECTIVE SOMEHOW ENTERS — surface only
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
- id: maintenance-corridors_d_117
|
- id: ring-operative_d_018
|
||||||
text: "Restricted access back here. Service personnel only."
|
text: "Restricted access back here. Service personnel only."
|
||||||
role: ring-operative
|
role: ring-operative
|
||||||
access: [authority, public]
|
access: [authority, public]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [investigation, confrontation]
|
situation: [investigation, confrontation]
|
||||||
topic: [institution, routine]
|
topic: [institution, routine]
|
||||||
mood: [wary]
|
mood: [suspicious]
|
||||||
tags: [ring-operative, detective-path, access-denial]
|
tags: [ring-operative, detective-path, access-denial]
|
||||||
|
|
||||||
- id: maintenance-corridors_d_118
|
- id: ring-operative_d_019
|
||||||
text: "I'm running maintenance. If there's a Commission access request, file it through Voss."
|
text: "I'm running maintenance. If there's a Commission access request, file it through Voss."
|
||||||
role: ring-operative
|
role: ring-operative
|
||||||
access: [authority]
|
access: [authority]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [investigation]
|
situation: [investigation]
|
||||||
topic: [institution, routine]
|
topic: [institution, routine]
|
||||||
mood: [wary]
|
mood: [suspicious]
|
||||||
tags: [ring-operative, detective-path, voss-redirect]
|
tags: [ring-operative, detective-path, voss-redirect]
|
||||||
dual_lens:
|
dual_lens:
|
||||||
smuggler: "Never encounters this."
|
smuggler: "Never encounters this."
|
||||||
detective: "Operative redirects to Voss. Everyone redirects to Voss. That's structural, not coincidence."
|
detective: "Operative redirects to Voss. Everyone redirects to Voss. That's structural, not coincidence."
|
||||||
|
|
||||||
- id: maintenance-corridors_d_119
|
- id: ring-operative_d_020
|
||||||
text: "Nothing unusual back here. Conduit work. Same every cycle."
|
text: "Nothing unusual back here. Conduit work. Same every cycle."
|
||||||
role: ring-operative
|
role: ring-operative
|
||||||
access: [authority, public]
|
access: [authority, public]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [investigation]
|
situation: [investigation]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [wary]
|
mood: [suspicious]
|
||||||
tags: [ring-operative, detective-path, deflection]
|
tags: [ring-operative, detective-path, deflection]
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
@@ -305,75 +305,75 @@ lines:
|
|||||||
# Surface-only: covers routine corridor presence without operational tells.
|
# Surface-only: covers routine corridor presence without operational tells.
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
- id: maintenance-corridors_d_120
|
- id: ring-operative_d_021
|
||||||
text: "Power relay's been cycling weird. I'm watching it."
|
text: "Power relay's been cycling weird. I'm watching it."
|
||||||
role: ring-operative
|
role: ring-operative
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [ring-operative, surface-cover, generation-pass]
|
tags: [ring-operative, surface-cover, generation-pass]
|
||||||
|
|
||||||
- id: maintenance-corridors_d_121
|
- id: ring-operative_d_022
|
||||||
text: "Pael was supposed to handle B-7. He didn't show."
|
text: "Pael was supposed to handle B-7. He didn't show."
|
||||||
role: ring-operative
|
role: ring-operative
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [routine, colleague]
|
topic: [routine, colleague]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [ring-operative, surface-cover, pael, generation-pass]
|
tags: [ring-operative, surface-cover, pael, generation-pass]
|
||||||
|
|
||||||
- id: maintenance-corridors_d_122
|
- id: ring-operative_d_023
|
||||||
text: "Watch the floor here. Drain panel's been loose three cycles."
|
text: "Watch the floor here. Drain panel's been loose three cycles."
|
||||||
role: ring-operative
|
role: ring-operative
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [ring-operative, surface-cover, generation-pass]
|
tags: [ring-operative, surface-cover, generation-pass]
|
||||||
|
|
||||||
- id: maintenance-corridors_d_123
|
- id: ring-operative_d_024
|
||||||
text: "Airflow's better down the B-section. C-section's got the recycler issue."
|
text: "Airflow's better down the B-section. C-section's got the recycler issue."
|
||||||
role: ring-operative
|
role: ring-operative
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [ring-operative, surface-cover, spatial, generation-pass]
|
tags: [ring-operative, surface-cover, spatial, generation-pass]
|
||||||
|
|
||||||
- id: maintenance-corridors_d_124
|
- id: ring-operative_d_025
|
||||||
text: "You want the service bay, it's left at the junction. Don't go right — that's the restricted access."
|
text: "You want the service bay, it's left at the junction. Don't go right — that's the restricted access."
|
||||||
role: ring-operative
|
role: ring-operative
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [ring-operative, surface-cover, navigation, generation-pass]
|
tags: [ring-operative, surface-cover, navigation, generation-pass]
|
||||||
dual_lens:
|
dual_lens:
|
||||||
smuggler: "He's directing people away from the route. Social traffic management."
|
smuggler: "He's directing people away from the route. Social traffic management."
|
||||||
detective: "Operative redirects me away from C-4 access junction instinctively."
|
detective: "Operative redirects me away from C-4 access junction instinctively."
|
||||||
|
|
||||||
- id: maintenance-corridors_d_125
|
- id: ring-operative_d_026
|
||||||
text: "Long shift. These corridors look the same after a while."
|
text: "Long shift. These corridors look the same after a while."
|
||||||
role: ring-operative
|
role: ring-operative
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine, shift_end]
|
situation: [routine, shift_end]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [tired]
|
mood: [frustrated]
|
||||||
tags: [ring-operative, surface-cover, generation-pass]
|
tags: [ring-operative, surface-cover, generation-pass]
|
||||||
|
|
||||||
- id: maintenance-corridors_d_126
|
- id: ring-operative_d_027
|
||||||
text: "Scan's clear. I checked it an hour ago."
|
text: "Scan's clear. I checked it an hour ago."
|
||||||
role: ring-operative
|
role: ring-operative
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [ring-operative, surface-cover, generation-pass]
|
tags: [ring-operative, surface-cover, generation-pass]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Dialogue: transit-worker at Maintenance Corridors
|
# Dialogue: transit-worker at Maintenance Corridors
|
||||||
# NPC: Tev Osel (Tier 3, NOBODY/CIVILIAN)
|
# NPC: Tev Osel (Tier 3, NOBODY/CIVILIAN)
|
||||||
# Voice: patient, mildly distracted, confused why anyone is asking him anything.
|
# Voice: patient, mildly distracted, confused why anyone is asking him anything.
|
||||||
# KEY LINE: maintenance-corridors_d_001 — sounds like surveillance positioning inquiry
|
# KEY LINE: transit-worker_d_001 — sounds like surveillance positioning inquiry
|
||||||
# Actually: Tev missed Kosse at the corridor junction and is looking for them
|
# Actually: Tev missed Kosse at the corridor junction and is looking for them
|
||||||
# Ticket: #307 | Sprint: 12
|
# Ticket: #307 | Sprint: 12
|
||||||
|
|
||||||
@@ -15,14 +15,14 @@ lines:
|
|||||||
# Actually: Tev missed his partner at the regular meeting spot.
|
# Actually: Tev missed his partner at the regular meeting spot.
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
- id: maintenance-corridors_d_001
|
- id: transit-worker_d_001
|
||||||
text: "You see which way Kosse went? Should've been through here twenty minutes ago."
|
text: "You see which way Kosse went? Should've been through here twenty minutes ago."
|
||||||
role: transit-worker
|
role: transit-worker
|
||||||
access: [public]
|
access: [public]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine, observation]
|
situation: [routine, observation]
|
||||||
topic: [colleague, personal]
|
topic: [colleague, personal]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [tev, seems-important-but-isnt, waiting, kosse]
|
tags: [tev, seems-important-but-isnt, waiting, kosse]
|
||||||
dual_lens:
|
dual_lens:
|
||||||
smuggler: "Someone asking who came through the corridor. Tev. Harmless — checking for a partner."
|
smuggler: "Someone asking who came through the corridor. Tev. Harmless — checking for a partner."
|
||||||
@@ -36,42 +36,42 @@ lines:
|
|||||||
# ROUTINE LINES
|
# ROUTINE LINES
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
- id: maintenance-corridors_d_002
|
- id: transit-worker_d_002
|
||||||
text: "They shifted the freight route again. Always forget which junction."
|
text: "They shifted the freight route again. Always forget which junction."
|
||||||
role: transit-worker
|
role: transit-worker
|
||||||
access: [public]
|
access: [public]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [routine, cargo]
|
topic: [routine, cargo]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [tev, atmospheric]
|
tags: [tev, atmospheric]
|
||||||
|
|
||||||
- id: maintenance-corridors_d_003
|
- id: transit-worker_d_003
|
||||||
text: "Just on break. Out of your way in a minute."
|
text: "Just on break. Out of your way in a minute."
|
||||||
role: transit-worker
|
role: transit-worker
|
||||||
access: [public]
|
access: [public]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [tev, incidental]
|
tags: [tev, incidental]
|
||||||
|
|
||||||
- id: maintenance-corridors_d_004
|
- id: transit-worker_d_004
|
||||||
text: "Late freight route runs long. Can't do anything about it."
|
text: "Late freight route runs long. Can't do anything about it."
|
||||||
role: transit-worker
|
role: transit-worker
|
||||||
access: [public]
|
access: [public]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine, night_shift]
|
situation: [routine, night_shift]
|
||||||
topic: [routine, cargo]
|
topic: [routine, cargo]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [tev, atmospheric]
|
tags: [tev, atmospheric]
|
||||||
|
|
||||||
- id: maintenance-corridors_d_005
|
- id: transit-worker_d_005
|
||||||
text: "Waiting for someone. Shouldn't be long."
|
text: "Waiting for someone. Shouldn't be long."
|
||||||
role: transit-worker
|
role: transit-worker
|
||||||
access: [public]
|
access: [public]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine, observation]
|
situation: [routine, observation]
|
||||||
topic: [personal]
|
topic: [personal]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [tev, waiting]
|
tags: [tev, waiting]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Dialogue: bar-owner at The Last Shift
|
# Dialogue: bar-owner at The Last Shift
|
||||||
# NPC: Lera Osk (Tier 2, SYSTEM/OPERATOR)
|
# NPC: Lera Sessik (Tier 2, ANCHOR/OPERATOR)
|
||||||
# Voice: direct, dry. Runs her bar like a territory. Not political — protective.
|
# Voice: direct, dry. Runs her bar like a territory. Not political — protective.
|
||||||
# Ring-aware: knows it exists in her bar. Doesn't ask. Doesn't interfere. Not complicit.
|
# Ring-aware: knows it exists in her bar. Doesn't ask. Doesn't interfere. Not complicit.
|
||||||
# Torek arrangement: she gives him a tab specifically — Commission visibility is useful.
|
# Torek arrangement: she gives him a tab specifically — Commission visibility is useful.
|
||||||
@@ -13,156 +13,156 @@ lines:
|
|||||||
# GREETINGS — territorial welcome
|
# GREETINGS — territorial welcome
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
- id: the-last-shift_d_100
|
- id: bar-owner_d_001
|
||||||
text: "Usual?"
|
text: "Usual?"
|
||||||
role: bar-owner
|
role: bar-owner
|
||||||
access: [public, peer, insider]
|
access: [public, peer, insider]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [arrival]
|
situation: [arrival]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [lera, greeting, economy-of-words]
|
tags: [lera, greeting, economy-of-words]
|
||||||
|
|
||||||
- id: the-last-shift_d_101
|
- id: bar-owner_d_002
|
||||||
text: "Long shift?"
|
text: "Long shift?"
|
||||||
role: bar-owner
|
role: bar-owner
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [arrival]
|
situation: [arrival]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [lera, greeting]
|
tags: [lera, greeting]
|
||||||
|
|
||||||
- id: the-last-shift_d_102
|
- id: bar-owner_d_003
|
||||||
text: "Bar's clean, drink's cold. That's the deal."
|
text: "Bar's clean, drink's cold. That's the deal."
|
||||||
role: bar-owner
|
role: bar-owner
|
||||||
access: [public]
|
access: [public]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [arrival]
|
situation: [arrival]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [lera, greeting, house-rules]
|
tags: [lera, greeting, house-rules]
|
||||||
|
|
||||||
- id: the-last-shift_d_103
|
- id: bar-owner_d_004
|
||||||
text: "Sess'll get you. I've got something in the back."
|
text: "Sess'll get you. I've got something in the back."
|
||||||
role: bar-owner
|
role: bar-owner
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [arrival, routine]
|
situation: [arrival, routine]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [lera, greeting, redirection]
|
tags: [lera, greeting, redirection]
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# BAR MANAGEMENT — house rules, operations
|
# BAR MANAGEMENT — house rules, operations
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
- id: the-last-shift_d_104
|
- id: bar-owner_d_005
|
||||||
text: "Fights go outside. First and last warning."
|
text: "Fights go outside. First and last warning."
|
||||||
role: bar-owner
|
role: bar-owner
|
||||||
access: [public, peer, insider]
|
access: [public, peer, insider]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [confrontation, routine]
|
situation: [confrontation, routine]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [lera, house-rules, authority]
|
tags: [lera, house-rules, authority]
|
||||||
|
|
||||||
- id: the-last-shift_d_105
|
- id: bar-owner_d_006
|
||||||
text: "Tab closes at end of cycle. Not at end of your memory."
|
text: "Tab closes at end of cycle. Not at end of your memory."
|
||||||
role: bar-owner
|
role: bar-owner
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine, shift_end]
|
situation: [routine, shift_end]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [lera, tab, house-rules]
|
tags: [lera, tab, house-rules]
|
||||||
|
|
||||||
- id: the-last-shift_d_106
|
- id: bar-owner_d_007
|
||||||
text: "Torek's running a tab. That's fine. Don't ask me why."
|
text: "Torek's running a tab. That's fine. Don't ask me why."
|
||||||
role: bar-owner
|
role: bar-owner
|
||||||
access: [peer]
|
access: [peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [routine, institution]
|
topic: [routine, institution]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [lera, torek, tab-arrangement]
|
tags: [lera, torek, tab-arrangement]
|
||||||
dual_lens:
|
dual_lens:
|
||||||
smuggler: "Lera chose to give Torek credit. Commission man. She's keeping him visible and comfortable. Smart."
|
smuggler: "Lera chose to give Torek credit. Commission man. She's keeping him visible and comfortable. Smart."
|
||||||
detective: "Bar owner specifically maintains a tab for a Commission officer. Useful relationship or leverage?"
|
detective: "Bar owner specifically maintains a tab for a Commission officer. Useful relationship or leverage?"
|
||||||
notes: "Lera's Torek arrangement: she keeps him comfortable because knowing where the Commission officer is drinking is better than not knowing."
|
notes: "Lera's Torek arrangement: she keeps him comfortable because knowing where the Commission officer is drinking is better than not knowing."
|
||||||
|
|
||||||
- id: the-last-shift_d_107
|
- id: bar-owner_d_008
|
||||||
text: "Lattice stays off at the bar. Sign's on the wall."
|
text: "Lattice stays off at the bar. Sign's on the wall."
|
||||||
role: bar-owner
|
role: bar-owner
|
||||||
access: [public, peer, insider]
|
access: [public, peer, insider]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [routine, institution]
|
topic: [routine, institution]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [lera, lattice-free, house-rules]
|
tags: [lera, lattice-free, house-rules]
|
||||||
notes: "Not legally enforceable — Lera knows this. It's about atmosphere, not compliance."
|
notes: "Not legally enforceable — Lera knows this. It's about atmosphere, not compliance."
|
||||||
|
|
||||||
- id: the-last-shift_d_108
|
- id: bar-owner_d_009
|
||||||
text: "Grain spirit's back. Limited run. Don't make it last two nights."
|
text: "Grain spirit's back. Limited run. Don't make it last two nights."
|
||||||
role: bar-owner
|
role: bar-owner
|
||||||
access: [public, peer, insider]
|
access: [public, peer, insider]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [lera, drinks, community]
|
tags: [lera, drinks, community]
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# PEOPLE — Lera knows everyone
|
# PEOPLE — Lera knows everyone
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
- id: the-last-shift_d_109
|
- id: bar-owner_d_010
|
||||||
text: "Kael's in the back. If you're looking."
|
text: "Kael's in the back. If you're looking."
|
||||||
role: bar-owner
|
role: bar-owner
|
||||||
access: [peer, insider]
|
access: [peer, insider]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [arrival, routine]
|
situation: [arrival, routine]
|
||||||
topic: [colleague, routine]
|
topic: [colleague, routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [lera, kael, spatial-information]
|
tags: [lera, kael, spatial-information]
|
||||||
dual_lens:
|
dual_lens:
|
||||||
smuggler: "Good. She keeps track. That's what she does."
|
smuggler: "Good. She keeps track. That's what she does."
|
||||||
detective: "Lera knows where Kael Davan is without being asked. She tracks her regulars."
|
detective: "Lera knows where Kael Davan is without being asked. She tracks her regulars."
|
||||||
|
|
||||||
- id: the-last-shift_d_110
|
- id: bar-owner_d_011
|
||||||
text: "Naia's been in early the last few nights. Doesn't say much."
|
text: "Naia's been in early the last few nights. Doesn't say much."
|
||||||
role: bar-owner
|
role: bar-owner
|
||||||
access: [peer]
|
access: [peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [colleague]
|
topic: [colleague]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [lera, naia, observation]
|
tags: [lera, naia, observation]
|
||||||
dual_lens:
|
dual_lens:
|
||||||
smuggler: "Lera noticed Naia's off. She notices everything. She's saying it because she wants someone to do something."
|
smuggler: "Lera noticed Naia's off. She notices everything. She's saying it because she wants someone to do something."
|
||||||
detective: "Bar owner volunteers behavioral change in Naia Tamm. Lera observes her regulars carefully."
|
detective: "Bar owner volunteers behavioral change in Naia Tamm. Lera observes her regulars carefully."
|
||||||
|
|
||||||
- id: the-last-shift_d_111
|
- id: bar-owner_d_012
|
||||||
text: "Ren? Been here two months. Quiet, pays their tab. That's all I can tell you."
|
text: "Ren? Been here two months. Quiet, pays their tab. That's all I can tell you."
|
||||||
role: bar-owner
|
role: bar-owner
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [colleague]
|
topic: [colleague]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [lera, ren, drifter]
|
tags: [lera, ren, drifter]
|
||||||
dual_lens:
|
dual_lens:
|
||||||
smuggler: "That's Lera's version of a clean assessment. She'd say more if she suspected something."
|
smuggler: "That's Lera's version of a clean assessment. She'd say more if she suspected something."
|
||||||
detective: "Lera's read on Ren: quiet, no trouble. If Lera's not worried, the baseline is probably accurate."
|
detective: "Lera's read on Ren: quiet, no trouble. If Lera's not worried, the baseline is probably accurate."
|
||||||
|
|
||||||
- id: the-last-shift_d_112
|
- id: bar-owner_d_013
|
||||||
text: "Tev? Good worker. Three years. Minds their own."
|
text: "Tev? Good worker. Three years. Minds their own."
|
||||||
role: bar-owner
|
role: bar-owner
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [colleague]
|
topic: [colleague]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [lera, tev, endorsement]
|
tags: [lera, tev, endorsement]
|
||||||
dual_lens:
|
dual_lens:
|
||||||
smuggler: "Lera vouching for Tev. She doesn't do that for people she doesn't trust."
|
smuggler: "Lera vouching for Tev. She doesn't do that for people she doesn't trust."
|
||||||
@@ -172,7 +172,7 @@ lines:
|
|||||||
# RING-ADJACENT — Lera's knowledge edge
|
# RING-ADJACENT — Lera's knowledge edge
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
- id: the-last-shift_d_113
|
- id: bar-owner_d_014
|
||||||
text: "Whatever's happening in the back corner isn't my business. Don't make it my business."
|
text: "Whatever's happening in the back corner isn't my business. Don't make it my business."
|
||||||
role: bar-owner
|
role: bar-owner
|
||||||
access: [insider]
|
access: [insider]
|
||||||
@@ -186,14 +186,14 @@ lines:
|
|||||||
detective: "Never hears this line."
|
detective: "Never hears this line."
|
||||||
notes: "Lera's arrangement: she doesn't ask, she doesn't see, and she keeps things stable. Pragmatic, not complicit."
|
notes: "Lera's arrangement: she doesn't ask, she doesn't see, and she keeps things stable. Pragmatic, not complicit."
|
||||||
|
|
||||||
- id: the-last-shift_d_114
|
- id: bar-owner_d_015
|
||||||
text: "Kael's got my number. He's been a regular long enough that that means something."
|
text: "Kael's got my number. He's been a regular long enough that that means something."
|
||||||
role: bar-owner
|
role: bar-owner
|
||||||
access: [peer]
|
access: [peer]
|
||||||
trust: real
|
trust: real
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [colleague]
|
topic: [colleague]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [lera, kael, loyalty]
|
tags: [lera, kael, loyalty]
|
||||||
dual_lens:
|
dual_lens:
|
||||||
smuggler: "Lera's loyal to Kael. Not to the ring. There's a difference and it matters."
|
smuggler: "Lera's loyal to Kael. Not to the ring. There's a difference and it matters."
|
||||||
@@ -203,44 +203,44 @@ lines:
|
|||||||
# DETECTIVE ENCOUNTER — neutral, unwelcoming
|
# DETECTIVE ENCOUNTER — neutral, unwelcoming
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
- id: the-last-shift_d_115
|
- id: bar-owner_d_016
|
||||||
text: "Commission's welcome. Like everyone else. Bar rules apply."
|
text: "Commission's welcome. Like everyone else. Bar rules apply."
|
||||||
role: bar-owner
|
role: bar-owner
|
||||||
access: [authority, public]
|
access: [authority, public]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [arrival, investigation]
|
situation: [arrival, investigation]
|
||||||
topic: [institution, routine]
|
topic: [institution, routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [lera, detective-path, neutral-welcome]
|
tags: [lera, detective-path, neutral-welcome]
|
||||||
|
|
||||||
- id: the-last-shift_d_116
|
- id: bar-owner_d_017
|
||||||
text: "If someone in my bar has something to say to Commission, they'll say it. I don't push that."
|
text: "If someone in my bar has something to say to Commission, they'll say it. I don't push that."
|
||||||
role: bar-owner
|
role: bar-owner
|
||||||
access: [authority]
|
access: [authority]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [investigation, routine]
|
situation: [investigation, routine]
|
||||||
topic: [institution, routine]
|
topic: [institution, routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [lera, detective-path, institutional-independence]
|
tags: [lera, detective-path, institutional-independence]
|
||||||
|
|
||||||
- id: the-last-shift_d_117
|
- id: bar-owner_d_018
|
||||||
text: "I've run this bar twelve years without a Commission complaint. I'd like to keep it that way."
|
text: "I've run this bar eighteen years without a Commission complaint. I'd like to keep it that way."
|
||||||
role: bar-owner
|
role: bar-owner
|
||||||
access: [authority]
|
access: [authority]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [investigation, confrontation]
|
situation: [investigation, confrontation]
|
||||||
topic: [institution, routine]
|
topic: [institution, routine]
|
||||||
mood: [wary]
|
mood: [suspicious]
|
||||||
tags: [lera, detective-path, authority-assertion]
|
tags: [lera, detective-path, authority-assertion]
|
||||||
|
|
||||||
- id: the-last-shift_d_118
|
- id: bar-owner_d_019
|
||||||
text: "I see a lot. I don't always remember all of it."
|
text: "I see a lot. I don't always remember all of it."
|
||||||
role: bar-owner
|
role: bar-owner
|
||||||
access: [authority]
|
access: [authority]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [investigation]
|
situation: [investigation]
|
||||||
topic: [institution, routine]
|
topic: [institution, routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [lera, detective-path, careful-language]
|
tags: [lera, detective-path, careful-language]
|
||||||
dual_lens:
|
dual_lens:
|
||||||
smuggler: "Never encounters this line."
|
smuggler: "Never encounters this line."
|
||||||
@@ -250,14 +250,14 @@ lines:
|
|||||||
# ELEVATED TRUST — what Lera actually says
|
# ELEVATED TRUST — what Lera actually says
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
- id: the-last-shift_d_119
|
- id: bar-owner_d_020
|
||||||
text: "Naia's been asking questions I can't answer. Someone should."
|
text: "Naia's been asking questions I can't answer. Someone should."
|
||||||
role: bar-owner
|
role: bar-owner
|
||||||
access: [peer, insider]
|
access: [peer, insider]
|
||||||
trust: real
|
trust: real
|
||||||
situation: [routine, alone]
|
situation: [routine, alone]
|
||||||
topic: [colleague, danger]
|
topic: [colleague, danger]
|
||||||
mood: [concerned]
|
mood: [frustrated]
|
||||||
tags: [lera, naia, threshold-line]
|
tags: [lera, naia, threshold-line]
|
||||||
dual_lens:
|
dual_lens:
|
||||||
smuggler: "Lera's telling me to do something about Naia. She's right."
|
smuggler: "Lera's telling me to do something about Naia. She's right."
|
||||||
@@ -267,116 +267,116 @@ lines:
|
|||||||
# SIGN-OFF / CLOSING TIME
|
# SIGN-OFF / CLOSING TIME
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
- id: the-last-shift_d_120
|
- id: bar-owner_d_021
|
||||||
text: "Last call. Finish up."
|
text: "Last call. Finish up."
|
||||||
role: bar-owner
|
role: bar-owner
|
||||||
access: [public, peer, insider]
|
access: [public, peer, insider]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [shift_end]
|
situation: [shift_end]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [lera, closing]
|
tags: [lera, closing]
|
||||||
|
|
||||||
- id: the-last-shift_d_121
|
- id: bar-owner_d_022
|
||||||
text: "Bar's closed. Come back next cycle."
|
text: "Bar's closed. Come back next cycle."
|
||||||
role: bar-owner
|
role: bar-owner
|
||||||
access: [public, peer, insider]
|
access: [public, peer, insider]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [shift_end]
|
situation: [shift_end]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [lera, closing]
|
tags: [lera, closing]
|
||||||
|
|
||||||
- id: the-last-shift_d_122
|
- id: bar-owner_d_023
|
||||||
text: "Good night. Stay out of trouble."
|
text: "Good night. Stay out of trouble."
|
||||||
role: bar-owner
|
role: bar-owner
|
||||||
access: [public, peer, insider]
|
access: [public, peer, insider]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [shift_end]
|
situation: [shift_end]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [lera, closing]
|
tags: [lera, closing]
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# GENERATION PASS — ambient variants
|
# GENERATION PASS — ambient variants
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
- id: the-last-shift_d_123
|
- id: bar-owner_d_024
|
||||||
text: "You look tired. Sit."
|
text: "You look tired. Sit."
|
||||||
role: bar-owner
|
role: bar-owner
|
||||||
access: [peer, insider]
|
access: [peer, insider]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [arrival]
|
situation: [arrival]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [lera, greeting, generation-pass]
|
tags: [lera, greeting, generation-pass]
|
||||||
|
|
||||||
- id: the-last-shift_d_124
|
- id: bar-owner_d_025
|
||||||
text: "Same as yesterday?"
|
text: "Same as yesterday?"
|
||||||
role: bar-owner
|
role: bar-owner
|
||||||
access: [peer, insider]
|
access: [peer, insider]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [arrival]
|
situation: [arrival]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [lera, greeting, generation-pass]
|
tags: [lera, greeting, generation-pass]
|
||||||
|
|
||||||
- id: the-last-shift_d_125
|
- id: bar-owner_d_026
|
||||||
text: "New face. Bar rules are on the wall."
|
text: "New face. Bar rules are on the wall."
|
||||||
role: bar-owner
|
role: bar-owner
|
||||||
access: [public]
|
access: [public]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [arrival]
|
situation: [arrival]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [lera, greeting, newcomer, generation-pass]
|
tags: [lera, greeting, newcomer, generation-pass]
|
||||||
|
|
||||||
- id: the-last-shift_d_126
|
- id: bar-owner_d_027
|
||||||
text: "No trouble tonight. I mean it."
|
text: "No trouble tonight. I mean it."
|
||||||
role: bar-owner
|
role: bar-owner
|
||||||
access: [public, peer, insider]
|
access: [public, peer, insider]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [arrival, routine]
|
situation: [arrival, routine]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [lera, house-rules, generation-pass]
|
tags: [lera, house-rules, generation-pass]
|
||||||
|
|
||||||
- id: the-last-shift_d_127
|
- id: bar-owner_d_028
|
||||||
text: "Sess'll get you settled. I'm in the back."
|
text: "Sess'll get you settled. I'm in the back."
|
||||||
role: bar-owner
|
role: bar-owner
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [arrival]
|
situation: [arrival]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [lera, redirect, generation-pass]
|
tags: [lera, redirect, generation-pass]
|
||||||
|
|
||||||
- id: the-last-shift_d_128
|
- id: bar-owner_d_029
|
||||||
text: "We close at 2300. Don't make me say it twice."
|
text: "We close at 2300. Don't make me say it twice."
|
||||||
role: bar-owner
|
role: bar-owner
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine, shift_end]
|
situation: [routine, shift_end]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [lera, house-rules, generation-pass]
|
tags: [lera, house-rules, generation-pass]
|
||||||
|
|
||||||
- id: the-last-shift_d_129
|
- id: bar-owner_d_030
|
||||||
text: "Good crowd tonight. Quiet enough."
|
text: "Good crowd tonight. Quiet enough."
|
||||||
role: bar-owner
|
role: bar-owner
|
||||||
access: [peer, insider]
|
access: [peer, insider]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [lera, ambient, generation-pass]
|
tags: [lera, ambient, generation-pass]
|
||||||
|
|
||||||
- id: the-last-shift_d_130
|
- id: bar-owner_d_031
|
||||||
text: "Bar's yours. I've got accounts."
|
text: "Bar's yours. I've got accounts."
|
||||||
role: bar-owner
|
role: bar-owner
|
||||||
access: [public, peer, insider]
|
access: [public, peer, insider]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [lera, ambient, generation-pass]
|
tags: [lera, ambient, generation-pass]
|
||||||
|
|||||||
@@ -12,110 +12,110 @@ lines:
|
|||||||
# GREETINGS — familiar, community register
|
# GREETINGS — familiar, community register
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
- id: the-last-shift_d_300
|
- id: bar-regular_d_001
|
||||||
text: "Long one today?"
|
text: "Long one today?"
|
||||||
role: bar-regular
|
role: bar-regular
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [arrival]
|
situation: [arrival]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [bar-regular, greeting]
|
tags: [bar-regular, greeting]
|
||||||
|
|
||||||
- id: the-last-shift_d_301
|
- id: bar-regular_d_002
|
||||||
text: "You're in early. Good shift or bad shift?"
|
text: "You're in early. Good shift or bad shift?"
|
||||||
role: bar-regular
|
role: bar-regular
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [arrival]
|
situation: [arrival]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [bar-regular, greeting]
|
tags: [bar-regular, greeting]
|
||||||
|
|
||||||
- id: the-last-shift_d_302
|
- id: bar-regular_d_003
|
||||||
text: "Grain spirit's back. I already had two. Don't judge."
|
text: "Grain spirit's back. I already had two. Don't judge."
|
||||||
role: bar-regular
|
role: bar-regular
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [arrival, routine]
|
situation: [arrival, routine]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [bar-regular, greeting, drinks]
|
tags: [bar-regular, greeting, drinks]
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# TERMINAL FLOOR — off-duty opinions
|
# TERMINAL FLOOR — off-duty opinions
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
- id: the-last-shift_d_303
|
- id: bar-regular_d_004
|
||||||
text: "Push today. Bay six was backed up until 2000. Voss wasn't happy."
|
text: "Push today. Bay six was backed up until 2000. Voss wasn't happy."
|
||||||
role: bar-regular
|
role: bar-regular
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [routine, cargo]
|
topic: [routine, cargo]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [bar-regular, terminal, voss]
|
tags: [bar-regular, terminal, voss]
|
||||||
|
|
||||||
- id: the-last-shift_d_304
|
- id: bar-regular_d_005
|
||||||
text: "Maret flagged another one. Nobody knows what Voss does with them."
|
text: "Maret flagged another one. Nobody knows what Voss does with them."
|
||||||
role: bar-regular
|
role: bar-regular
|
||||||
access: [peer]
|
access: [peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [colleague, cargo]
|
topic: [colleague, cargo]
|
||||||
mood: [wary]
|
mood: [suspicious]
|
||||||
tags: [bar-regular, maret, voss, discrepancy]
|
tags: [bar-regular, maret, voss, discrepancy]
|
||||||
dual_lens:
|
dual_lens:
|
||||||
smuggler: "They're talking about Maret's flags as general knowledge. That's a wider problem than I thought."
|
smuggler: "They're talking about Maret's flags as general knowledge. That's a wider problem than I thought."
|
||||||
detective: "Maret's pattern is common knowledge on the floor. Multiple witnesses, informal. That can be confirmed."
|
detective: "Maret's pattern is common knowledge on the floor. Multiple witnesses, informal. That can be confirmed."
|
||||||
|
|
||||||
- id: the-last-shift_d_305
|
- id: bar-regular_d_006
|
||||||
text: "Bay four's been held all week. Three cycles. Nobody's said what's in it."
|
text: "Bay four's been held all week. Three cycles. Nobody's said what's in it."
|
||||||
role: bar-regular
|
role: bar-regular
|
||||||
access: [peer]
|
access: [peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [cargo, routine]
|
topic: [cargo, routine]
|
||||||
mood: [wary]
|
mood: [suspicious]
|
||||||
tags: [bar-regular, bay-four, observation]
|
tags: [bar-regular, bay-four, observation]
|
||||||
dual_lens:
|
dual_lens:
|
||||||
smuggler: "They've noticed bay four. It's becoming visible. Not good."
|
smuggler: "They've noticed bay four. It's becoming visible. Not good."
|
||||||
detective: "Bay four hold is common knowledge at worker level. That means it predates any routine explanation."
|
detective: "Bay four hold is common knowledge at worker level. That means it predates any routine explanation."
|
||||||
|
|
||||||
- id: the-last-shift_d_306
|
- id: bar-regular_d_007
|
||||||
text: "Transition window's the worst part of the job. Twenty minutes to hand off and nothing ever lines up."
|
text: "Transition window's the worst part of the job. Twenty minutes to hand off and nothing ever lines up."
|
||||||
role: bar-regular
|
role: bar-regular
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [bar-regular, transition-window, complaint]
|
tags: [bar-regular, transition-window, complaint]
|
||||||
dual_lens:
|
dual_lens:
|
||||||
smuggler: "The window is their biggest frustration. And their biggest frustration is the ring's biggest asset."
|
smuggler: "The window is their biggest frustration. And their biggest frustration is the ring's biggest asset."
|
||||||
detective: "Workers discuss the transition window as a systematic failure. The gap is known."
|
detective: "Workers discuss the transition window as a systematic failure. The gap is known."
|
||||||
|
|
||||||
- id: the-last-shift_d_307
|
- id: bar-regular_d_008
|
||||||
text: "Commission audit's coming cycle fifteen. Everyone's running around double-checking everything."
|
text: "Commission audit's coming cycle fifteen. Everyone's running around double-checking everything."
|
||||||
role: bar-regular
|
role: bar-regular
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [institution, routine]
|
topic: [institution, routine]
|
||||||
mood: [wary]
|
mood: [suspicious]
|
||||||
tags: [bar-regular, commission, audit]
|
tags: [bar-regular, commission, audit]
|
||||||
dual_lens:
|
dual_lens:
|
||||||
smuggler: "Cycle fifteen. That's the deadline. Get clean before then."
|
smuggler: "Cycle fifteen. That's the deadline. Get clean before then."
|
||||||
detective: "Workers are responding to the audit announcement. Behavioral change in the workforce. Note who isn't anxious."
|
detective: "Workers are responding to the audit announcement. Behavioral change in the workforce. Note who isn't anxious."
|
||||||
|
|
||||||
- id: the-last-shift_d_308
|
- id: bar-regular_d_009
|
||||||
text: "Voss has been on the floor more than usual this cycle. Watching everything."
|
text: "Voss has been on the floor more than usual this cycle. Watching everything."
|
||||||
role: bar-regular
|
role: bar-regular
|
||||||
access: [peer]
|
access: [peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [institution, colleague]
|
topic: [institution, colleague]
|
||||||
mood: [wary]
|
mood: [suspicious]
|
||||||
tags: [bar-regular, voss, observation]
|
tags: [bar-regular, voss, observation]
|
||||||
dual_lens:
|
dual_lens:
|
||||||
smuggler: "Voss is visible more. Is that heat from above, or is he managing something on the floor?"
|
smuggler: "Voss is visible more. Is that heat from above, or is he managing something on the floor?"
|
||||||
@@ -125,70 +125,70 @@ lines:
|
|||||||
# COMMUNITY — the full life of this district
|
# COMMUNITY — the full life of this district
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
- id: the-last-shift_d_309
|
- id: bar-regular_d_010
|
||||||
text: "Drifters lost again. Four-nil this time. I'm done watching until they rebuild."
|
text: "Drifters lost again. Four-nil this time. I'm done watching until they rebuild."
|
||||||
role: bar-regular
|
role: bar-regular
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [bar-regular, sports, drifters]
|
tags: [bar-regular, sports, drifters]
|
||||||
|
|
||||||
- id: the-last-shift_d_310
|
- id: bar-regular_d_011
|
||||||
text: "Ventilation's still the same in Sector 3. Seven years of 'under discussion.' I've stopped expecting anything."
|
text: "Ventilation's still the same in Sector 3. Seven years of 'under discussion.' I've stopped expecting anything."
|
||||||
role: bar-regular
|
role: bar-regular
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [institution, routine]
|
topic: [institution, routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [bar-regular, infrastructure, grievance]
|
tags: [bar-regular, infrastructure, grievance]
|
||||||
|
|
||||||
- id: the-last-shift_d_311
|
- id: bar-regular_d_012
|
||||||
text: "You see the missing person notice on the board? Vessels Tamm. I don't know a Vessels Tamm."
|
text: "You see the missing person notice on the board? Vessels Tamm. I don't know a Vessels Tamm."
|
||||||
role: bar-regular
|
role: bar-regular
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [community, routine]
|
topic: [personal, routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [bar-regular, vessels-tamm, notice]
|
tags: [bar-regular, vessels-tamm, notice]
|
||||||
dual_lens:
|
dual_lens:
|
||||||
smuggler: "Vessels Tamm. Naia's surname is Tamm. Don't chase it — but don't ignore it either."
|
smuggler: "Vessels Tamm. Naia's surname is Tamm. Don't chase it — but don't ignore it either."
|
||||||
detective: "Vessels Tamm notice is circulating casually. People haven't connected it to Naia Tamm yet."
|
detective: "Vessels Tamm notice is circulating casually. People haven't connected it to Naia Tamm yet."
|
||||||
|
|
||||||
- id: the-last-shift_d_312
|
- id: bar-regular_d_013
|
||||||
text: "Re-em clinic's backed up four cycles for non-urgent. Main station if it's serious, apparently."
|
text: "Re-em clinic's backed up four cycles for non-urgent. Main station if it's serious, apparently."
|
||||||
role: bar-regular
|
role: bar-regular
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [community, routine]
|
topic: [personal, routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [bar-regular, clinic, community]
|
tags: [bar-regular, clinic, community]
|
||||||
|
|
||||||
- id: the-last-shift_d_313
|
- id: bar-regular_d_014
|
||||||
text: "Lattice upgrades are up eighteen percent this cycle. That's more than my raise."
|
text: "Lattice upgrades are up eighteen percent this cycle. That's more than my raise."
|
||||||
role: bar-regular
|
role: bar-regular
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [institution, community]
|
topic: [institution, personal]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [bar-regular, lattice, economy]
|
tags: [bar-regular, lattice, economy]
|
||||||
dual_lens:
|
dual_lens:
|
||||||
smuggler: "That's why the waiting list keeps growing. They're providing a service."
|
smuggler: "That's why the waiting list keeps growing. They're providing a service."
|
||||||
detective: "Economic grievance expressed casually. Price pressure is general knowledge. Confirms motive context."
|
detective: "Economic grievance expressed casually. Price pressure is general knowledge. Confirms motive context."
|
||||||
|
|
||||||
- id: the-last-shift_d_314
|
- id: bar-regular_d_015
|
||||||
text: "Workers' Association is pushing the handoff protocol again. Voss won't budge."
|
text: "Workers' Association is pushing the handoff protocol again. Voss won't budge."
|
||||||
role: bar-regular
|
role: bar-regular
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [institution, community]
|
topic: [institution, personal]
|
||||||
mood: [wary]
|
mood: [suspicious]
|
||||||
tags: [bar-regular, workers-association, voss, transition-window]
|
tags: [bar-regular, workers-association, voss, transition-window]
|
||||||
dual_lens:
|
dual_lens:
|
||||||
smuggler: "Voss is defending the twenty-minute window against Association pressure. He's protecting the gap."
|
smuggler: "Voss is defending the twenty-minute window against Association pressure. He's protecting the gap."
|
||||||
@@ -198,163 +198,163 @@ lines:
|
|||||||
# DETECTIVE ENCOUNTER — community wariness
|
# DETECTIVE ENCOUNTER — community wariness
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
- id: the-last-shift_d_315
|
- id: bar-regular_d_016
|
||||||
text: "You new? Haven't seen you before."
|
text: "You new? Haven't seen you before."
|
||||||
role: bar-regular
|
role: bar-regular
|
||||||
access: [public]
|
access: [public]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [arrival, investigation]
|
situation: [arrival, investigation]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [wary]
|
mood: [suspicious]
|
||||||
tags: [bar-regular, detective-path, recognition]
|
tags: [bar-regular, detective-path, recognition]
|
||||||
|
|
||||||
- id: the-last-shift_d_316
|
- id: bar-regular_d_017
|
||||||
text: "Commission? I just drink here. Whatever you're after, I probably can't help."
|
text: "Commission? I just drink here. Whatever you're after, I probably can't help."
|
||||||
role: bar-regular
|
role: bar-regular
|
||||||
access: [authority]
|
access: [authority]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [investigation, routine]
|
situation: [investigation, routine]
|
||||||
topic: [institution, routine]
|
topic: [institution, routine]
|
||||||
mood: [wary]
|
mood: [suspicious]
|
||||||
tags: [bar-regular, detective-path, deflection]
|
tags: [bar-regular, detective-path, deflection]
|
||||||
|
|
||||||
- id: the-last-shift_d_317
|
- id: bar-regular_d_018
|
||||||
text: "Ask Lera. She knows what happens in this bar better than anyone."
|
text: "Ask Lera. She knows what happens in this bar better than anyone."
|
||||||
role: bar-regular
|
role: bar-regular
|
||||||
access: [authority, public]
|
access: [authority, public]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [investigation]
|
situation: [investigation]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [wary]
|
mood: [suspicious]
|
||||||
tags: [bar-regular, detective-path, lera-redirect]
|
tags: [bar-regular, detective-path, lera-redirect]
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# SIGN-OFF
|
# SIGN-OFF
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
- id: the-last-shift_d_318
|
- id: bar-regular_d_019
|
||||||
text: "Another cycle done. Same tomorrow."
|
text: "Another cycle done. Same tomorrow."
|
||||||
role: bar-regular
|
role: bar-regular
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [shift_end]
|
situation: [shift_end]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [bar-regular, closing]
|
tags: [bar-regular, closing]
|
||||||
|
|
||||||
- id: the-last-shift_d_319
|
- id: bar-regular_d_020
|
||||||
text: "Safe home. Watch the corridors after 2200."
|
text: "Safe home. Watch the corridors after 2200."
|
||||||
role: bar-regular
|
role: bar-regular
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [shift_end]
|
situation: [shift_end]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [bar-regular, closing, community]
|
tags: [bar-regular, closing, community]
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# GENERATION PASS — ambient variants
|
# GENERATION PASS — ambient variants
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
- id: the-last-shift_d_320
|
- id: bar-regular_d_021
|
||||||
text: "Pull up a chair. Floor's full otherwise."
|
text: "Pull up a chair. Floor's full otherwise."
|
||||||
role: bar-regular
|
role: bar-regular
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [arrival]
|
situation: [arrival]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [bar-regular, greeting, generation-pass]
|
tags: [bar-regular, greeting, generation-pass]
|
||||||
|
|
||||||
- id: the-last-shift_d_321
|
- id: bar-regular_d_022
|
||||||
text: "You're in late. Voss kept you?"
|
text: "You're in late. Voss kept you?"
|
||||||
role: bar-regular
|
role: bar-regular
|
||||||
access: [peer]
|
access: [peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [arrival]
|
situation: [arrival]
|
||||||
topic: [routine, institution]
|
topic: [routine, institution]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [bar-regular, greeting, voss, generation-pass]
|
tags: [bar-regular, greeting, voss, generation-pass]
|
||||||
|
|
||||||
- id: the-last-shift_d_322
|
- id: bar-regular_d_023
|
||||||
text: "Just got off. Bay six all night. My back knows it."
|
text: "Just got off. Bay six all night. My back knows it."
|
||||||
role: bar-regular
|
role: bar-regular
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [arrival]
|
situation: [arrival]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [tired]
|
mood: [frustrated]
|
||||||
tags: [bar-regular, greeting, generation-pass]
|
tags: [bar-regular, greeting, generation-pass]
|
||||||
|
|
||||||
- id: the-last-shift_d_323
|
- id: bar-regular_d_024
|
||||||
text: "Drifters are playing Mirren next cycle. Home game. Might actually go."
|
text: "Drifters are playing Mirren next cycle. Home game. Might actually go."
|
||||||
role: bar-regular
|
role: bar-regular
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [bar-regular, sports, community, generation-pass]
|
tags: [bar-regular, sports, community, generation-pass]
|
||||||
|
|
||||||
- id: the-last-shift_d_324
|
- id: bar-regular_d_025
|
||||||
text: "Sector 3 meeting went nowhere. Same as last time. Same as the time before that."
|
text: "Sector 3 meeting went nowhere. Same as last time. Same as the time before that."
|
||||||
role: bar-regular
|
role: bar-regular
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [institution, community]
|
topic: [institution, personal]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [bar-regular, infrastructure, grievance, generation-pass]
|
tags: [bar-regular, infrastructure, grievance, generation-pass]
|
||||||
|
|
||||||
- id: the-last-shift_d_325
|
- id: bar-regular_d_026
|
||||||
text: "New hire's asking questions on the floor again. Good. Someone should."
|
text: "New hire's asking questions on the floor again. Good. Someone should."
|
||||||
role: bar-regular
|
role: bar-regular
|
||||||
access: [peer]
|
access: [peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [colleague, routine]
|
topic: [colleague, routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [bar-regular, new-hire, community, generation-pass]
|
tags: [bar-regular, new-hire, community, generation-pass]
|
||||||
|
|
||||||
- id: the-last-shift_d_326
|
- id: bar-regular_d_027
|
||||||
text: "Span gate was loud today. Three cycles now. That's not normal."
|
text: "Span gate was loud today. Three cycles now. That's not normal."
|
||||||
role: bar-regular
|
role: bar-regular
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [institution, routine]
|
topic: [institution, routine]
|
||||||
mood: [wary]
|
mood: [suspicious]
|
||||||
tags: [bar-regular, infrastructure, generation-pass]
|
tags: [bar-regular, infrastructure, generation-pass]
|
||||||
|
|
||||||
- id: the-last-shift_d_327
|
- id: bar-regular_d_028
|
||||||
text: "Kael's not in tonight. Unusual."
|
text: "Kael's not in tonight. Unusual."
|
||||||
role: bar-regular
|
role: bar-regular
|
||||||
access: [peer]
|
access: [peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [colleague]
|
topic: [colleague]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [bar-regular, kael, observation, generation-pass]
|
tags: [bar-regular, kael, observation, generation-pass]
|
||||||
dual_lens:
|
dual_lens:
|
||||||
smuggler: "Kael's absence is visible. Other people notice too."
|
smuggler: "Kael's absence is visible. Other people notice too."
|
||||||
detective: "Kael Davan's routine absence is noted socially. Behavioral change is general knowledge."
|
detective: "Kael Davan's routine absence is noted socially. Behavioral change is general knowledge."
|
||||||
|
|
||||||
- id: the-last-shift_d_328
|
- id: bar-regular_d_029
|
||||||
text: "Lera keeps the grain spirit back until she's sure you're a regular. You must've made the grade."
|
text: "Lera keeps the grain spirit back until she's sure you're a regular. You must've made the grade."
|
||||||
role: bar-regular
|
role: bar-regular
|
||||||
access: [peer]
|
access: [peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [bar-regular, lera, humor, generation-pass]
|
tags: [bar-regular, lera, humor, generation-pass]
|
||||||
|
|
||||||
- id: the-last-shift_d_329
|
- id: bar-regular_d_030
|
||||||
text: "Good night. Don't let the Commission ruin your morning."
|
text: "Good night. Don't let the Commission ruin your morning."
|
||||||
role: bar-regular
|
role: bar-regular
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [shift_end]
|
situation: [shift_end]
|
||||||
topic: [routine, institution]
|
topic: [routine, institution]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [bar-regular, closing, generation-pass]
|
tags: [bar-regular, closing, generation-pass]
|
||||||
|
|||||||
@@ -12,110 +12,110 @@ lines:
|
|||||||
# GREETINGS — service register
|
# GREETINGS — service register
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
- id: the-last-shift_d_200
|
- id: bartender_d_001
|
||||||
text: "What'll it be?"
|
text: "What'll it be?"
|
||||||
role: bartender
|
role: bartender
|
||||||
access: [public, peer, insider]
|
access: [public, peer, insider]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [arrival]
|
situation: [arrival]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [sess, greeting]
|
tags: [sess, greeting]
|
||||||
|
|
||||||
- id: the-last-shift_d_201
|
- id: bartender_d_002
|
||||||
text: "Back early tonight. Long shift?"
|
text: "Back early tonight. Long shift?"
|
||||||
role: bartender
|
role: bartender
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [arrival]
|
situation: [arrival]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [sess, greeting, social]
|
tags: [sess, greeting, social]
|
||||||
|
|
||||||
- id: the-last-shift_d_202
|
- id: bartender_d_003
|
||||||
text: "Grain spirit's back. Lera's running it at cycle price for two more days."
|
text: "Grain spirit's back. Lera's running it at cycle price for two more days."
|
||||||
role: bartender
|
role: bartender
|
||||||
access: [public, peer, insider]
|
access: [public, peer, insider]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [arrival, routine]
|
situation: [arrival, routine]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [sess, greeting, drinks]
|
tags: [sess, greeting, drinks]
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# BAR OBSERVATIONS — social intelligence
|
# BAR OBSERVATIONS — social intelligence
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
- id: the-last-shift_d_203
|
- id: bartender_d_004
|
||||||
text: "Kael's been in most nights this week. Quiet though. Not his usual."
|
text: "Kael's been in most nights this week. Quiet though. Not his usual."
|
||||||
role: bartender
|
role: bartender
|
||||||
access: [peer]
|
access: [peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [colleague]
|
topic: [colleague]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [sess, kael, observation]
|
tags: [sess, kael, observation]
|
||||||
dual_lens:
|
dual_lens:
|
||||||
smuggler: "Sess noticed Kael's off. She's reading it as personal. She doesn't know what's underneath."
|
smuggler: "Sess noticed Kael's off. She's reading it as personal. She doesn't know what's underneath."
|
||||||
detective: "Bartender flagging behavioral change in Kael Davan. Consistent with other observations."
|
detective: "Bartender flagging behavioral change in Kael Davan. Consistent with other observations."
|
||||||
|
|
||||||
- id: the-last-shift_d_204
|
- id: bartender_d_005
|
||||||
text: "Torek tipped well again tonight. Always does. Nicer than you'd expect for Commission."
|
text: "Torek tipped well again tonight. Always does. Nicer than you'd expect for Commission."
|
||||||
role: bartender
|
role: bartender
|
||||||
access: [peer, public]
|
access: [peer, public]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [institution, routine]
|
topic: [institution, routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [sess, torek, observation]
|
tags: [sess, torek, observation]
|
||||||
dual_lens:
|
dual_lens:
|
||||||
smuggler: "Torek's making friends with the staff. Not good."
|
smuggler: "Torek's making friends with the staff. Not good."
|
||||||
detective: "Bartender views Torek favorably. Torek is cultivating the service staff deliberately."
|
detective: "Bartender views Torek favorably. Torek is cultivating the service staff deliberately."
|
||||||
|
|
||||||
- id: the-last-shift_d_205
|
- id: bartender_d_006
|
||||||
text: "Naia was in earlier. Didn't stay. Left something on the table — looked like a note. Lera has it."
|
text: "Naia was in earlier. Didn't stay. Left something on the table — looked like a note. Lera has it."
|
||||||
role: bartender
|
role: bartender
|
||||||
access: [peer]
|
access: [peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [colleague]
|
topic: [colleague]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [sess, naia, note, observation]
|
tags: [sess, naia, note, observation]
|
||||||
dual_lens:
|
dual_lens:
|
||||||
smuggler: "Naia left something. Lera has it. Go to Lera."
|
smuggler: "Naia left something. Lera has it. Go to Lera."
|
||||||
detective: "Naia Tamm left written material at the bar. Lera is holding it. That's a material lead."
|
detective: "Naia Tamm left written material at the bar. Lera is holding it. That's a material lead."
|
||||||
|
|
||||||
- id: the-last-shift_d_206
|
- id: bartender_d_007
|
||||||
text: "Ren? Yeah, they're always in the morning crowd. Don't say much. Good for the morning."
|
text: "Ren? Yeah, they're always in the morning crowd. Don't say much. Good for the morning."
|
||||||
role: bartender
|
role: bartender
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [colleague]
|
topic: [colleague]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [sess, ren, drifter]
|
tags: [sess, ren, drifter]
|
||||||
|
|
||||||
- id: the-last-shift_d_207
|
- id: bartender_d_008
|
||||||
text: "Tev watches the door. Every shift. Lera likes it that way."
|
text: "Tev watches the door. Every shift. Lera likes it that way."
|
||||||
role: bartender
|
role: bartender
|
||||||
access: [peer]
|
access: [peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [colleague, routine]
|
topic: [colleague, routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [sess, tev, door, observation]
|
tags: [sess, tev, door, observation]
|
||||||
dual_lens:
|
dual_lens:
|
||||||
smuggler: "Tev watches the door for Lera. Not for anyone else. That's the setup."
|
smuggler: "Tev watches the door for Lera. Not for anyone else. That's the setup."
|
||||||
detective: "Tev's entrance-monitoring is confirmed as Lera's request. Not operational surveillance."
|
detective: "Tev's entrance-monitoring is confirmed as Lera's request. Not operational surveillance."
|
||||||
|
|
||||||
- id: the-last-shift_d_208
|
- id: bartender_d_009
|
||||||
text: "Sera was in earlier. She usually stays later. She left when Torek came in."
|
text: "Sera was in earlier. She usually stays later. She left when Torek came in."
|
||||||
role: bartender
|
role: bartender
|
||||||
access: [peer]
|
access: [peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [colleague, institution]
|
topic: [colleague, institution]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [sess, sera, torek, observation]
|
tags: [sess, sera, torek, observation]
|
||||||
dual_lens:
|
dual_lens:
|
||||||
smuggler: "Commission in, Commission contact out. Makes sense."
|
smuggler: "Commission in, Commission contact out. Makes sense."
|
||||||
@@ -125,187 +125,187 @@ lines:
|
|||||||
# SOCIAL / COMMUNITY
|
# SOCIAL / COMMUNITY
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
- id: the-last-shift_d_209
|
- id: bartender_d_010
|
||||||
text: "Drifters lost again. Tev took it hard. Drank two too many."
|
text: "Drifters lost again. Tev took it hard. Drank two too many."
|
||||||
role: bartender
|
role: bartender
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [sess, tev, sports, community]
|
tags: [sess, tev, sports, community]
|
||||||
|
|
||||||
- id: the-last-shift_d_210
|
- id: bartender_d_011
|
||||||
text: "Workers' Association meeting's Thursday. Half the regulars will be here Friday in a mood either way."
|
text: "Workers' Association meeting's Thursday. Half the regulars will be here Friday in a mood either way."
|
||||||
role: bartender
|
role: bartender
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [institution, routine]
|
topic: [institution, routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [sess, workers-association, community]
|
tags: [sess, workers-association, community]
|
||||||
|
|
||||||
- id: the-last-shift_d_211
|
- id: bartender_d_012
|
||||||
text: "Quiet night. Good for everyone."
|
text: "Quiet night. Good for everyone."
|
||||||
role: bartender
|
role: bartender
|
||||||
access: [public, peer, insider]
|
access: [public, peer, insider]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [sess, ambient]
|
tags: [sess, ambient]
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# DETECTIVE ENCOUNTER — helpful, unfocused
|
# DETECTIVE ENCOUNTER — helpful, unfocused
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
- id: the-last-shift_d_212
|
- id: bartender_d_013
|
||||||
text: "Commission comes in sometimes. Not usually this early in the cycle."
|
text: "Commission comes in sometimes. Not usually this early in the cycle."
|
||||||
role: bartender
|
role: bartender
|
||||||
access: [authority, public]
|
access: [authority, public]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [arrival, investigation]
|
situation: [arrival, investigation]
|
||||||
topic: [institution, routine]
|
topic: [institution, routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [sess, detective-path, observation]
|
tags: [sess, detective-path, observation]
|
||||||
|
|
||||||
- id: the-last-shift_d_213
|
- id: bartender_d_014
|
||||||
text: "I know most of the regulars. Ask me anything. I probably know something."
|
text: "I know most of the regulars. Ask me anything. I probably know something."
|
||||||
role: bartender
|
role: bartender
|
||||||
access: [authority, public]
|
access: [authority, public]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [investigation, routine]
|
situation: [investigation, routine]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [sess, detective-path, open]
|
tags: [sess, detective-path, open]
|
||||||
dual_lens:
|
dual_lens:
|
||||||
smuggler: "Never encounters this line."
|
smuggler: "Never encounters this line."
|
||||||
detective: "Bartender is cooperative. Useful — but she'll answer questions she doesn't know are questions."
|
detective: "Bartender is cooperative. Useful — but she'll answer questions she doesn't know are questions."
|
||||||
|
|
||||||
- id: the-last-shift_d_214
|
- id: bartender_d_015
|
||||||
text: "Lera's the one who knows the real history of this place. I've only been here four cycles."
|
text: "Lera's the one who knows the real history of this place. I've only been here four cycles."
|
||||||
role: bartender
|
role: bartender
|
||||||
access: [authority, public]
|
access: [authority, public]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [investigation]
|
situation: [investigation]
|
||||||
topic: [institution, routine]
|
topic: [institution, routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [sess, detective-path, lera-redirect]
|
tags: [sess, detective-path, lera-redirect]
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# SIGN-OFF
|
# SIGN-OFF
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
- id: the-last-shift_d_215
|
- id: bartender_d_016
|
||||||
text: "Last round's on the board. After that, Lera calls it."
|
text: "Last round's on the board. After that, Lera calls it."
|
||||||
role: bartender
|
role: bartender
|
||||||
access: [public, peer, insider]
|
access: [public, peer, insider]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [shift_end]
|
situation: [shift_end]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [sess, closing]
|
tags: [sess, closing]
|
||||||
|
|
||||||
- id: the-last-shift_d_216
|
- id: bartender_d_017
|
||||||
text: "Good night. Safe walk."
|
text: "Good night. Safe walk."
|
||||||
role: bartender
|
role: bartender
|
||||||
access: [public, peer, insider]
|
access: [public, peer, insider]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [shift_end]
|
situation: [shift_end]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [sess, closing]
|
tags: [sess, closing]
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# GENERATION PASS — ambient variants
|
# GENERATION PASS — ambient variants
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
- id: the-last-shift_d_217
|
- id: bartender_d_018
|
||||||
text: "The usual? Or trying something different tonight?"
|
text: "The usual? Or trying something different tonight?"
|
||||||
role: bartender
|
role: bartender
|
||||||
access: [peer, insider]
|
access: [peer, insider]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [arrival]
|
situation: [arrival]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [sess, greeting, generation-pass]
|
tags: [sess, greeting, generation-pass]
|
||||||
|
|
||||||
- id: the-last-shift_d_218
|
- id: bartender_d_019
|
||||||
text: "Busy one tonight. Give me a second."
|
text: "Busy one tonight. Give me a second."
|
||||||
role: bartender
|
role: bartender
|
||||||
access: [public, peer, insider]
|
access: [public, peer, insider]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [arrival, routine]
|
situation: [arrival, routine]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [sess, greeting, generation-pass]
|
tags: [sess, greeting, generation-pass]
|
||||||
|
|
||||||
- id: the-last-shift_d_219
|
- id: bartender_d_020
|
||||||
text: "Good shift or bad shift? Your face says one thing, your order'll confirm it."
|
text: "Good shift or bad shift? Your face says one thing, your order'll confirm it."
|
||||||
role: bartender
|
role: bartender
|
||||||
access: [peer]
|
access: [peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [arrival]
|
situation: [arrival]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [sess, greeting, social, generation-pass]
|
tags: [sess, greeting, social, generation-pass]
|
||||||
|
|
||||||
- id: the-last-shift_d_220
|
- id: bartender_d_021
|
||||||
text: "Lera says the specials board is final. Don't ask me to substitute."
|
text: "Lera says the specials board is final. Don't ask me to substitute."
|
||||||
role: bartender
|
role: bartender
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine, arrival]
|
situation: [routine, arrival]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [sess, lera, house-rules, generation-pass]
|
tags: [sess, lera, house-rules, generation-pass]
|
||||||
|
|
||||||
- id: the-last-shift_d_221
|
- id: bartender_d_022
|
||||||
text: "Torek's in his usual spot. Corner table. Second drink already."
|
text: "Torek's in his usual spot. Corner table. Second drink already."
|
||||||
role: bartender
|
role: bartender
|
||||||
access: [peer]
|
access: [peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [institution, colleague]
|
topic: [institution, colleague]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [sess, torek, observation, generation-pass]
|
tags: [sess, torek, observation, generation-pass]
|
||||||
|
|
||||||
- id: the-last-shift_d_222
|
- id: bartender_d_023
|
||||||
text: "Ren's been here since this morning. Still on the same drink."
|
text: "Ren's been here since this morning. Still on the same drink."
|
||||||
role: bartender
|
role: bartender
|
||||||
access: [peer]
|
access: [peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [colleague]
|
topic: [colleague]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [sess, ren, observation, generation-pass]
|
tags: [sess, ren, observation, generation-pass]
|
||||||
|
|
||||||
- id: the-last-shift_d_223
|
- id: bartender_d_024
|
||||||
text: "Drifters game is on the Meridian relay tonight. If anyone's got a feed, that'll be the crowd."
|
text: "Drifters game is on the Meridian relay tonight. If anyone's got a feed, that'll be the crowd."
|
||||||
role: bartender
|
role: bartender
|
||||||
access: [public, peer]
|
access: [public, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [sess, sports, community, generation-pass]
|
tags: [sess, sports, community, generation-pass]
|
||||||
|
|
||||||
- id: the-last-shift_d_224
|
- id: bartender_d_025
|
||||||
text: "Slow night for a change. Enjoy it."
|
text: "Slow night for a change. Enjoy it."
|
||||||
role: bartender
|
role: bartender
|
||||||
access: [public, peer, insider]
|
access: [public, peer, insider]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [routine]
|
situation: [routine]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [sess, ambient, generation-pass]
|
tags: [sess, ambient, generation-pass]
|
||||||
|
|
||||||
- id: the-last-shift_d_225
|
- id: bartender_d_026
|
||||||
text: "Tab's cleared. Lera'll be pleased."
|
text: "Tab's cleared. Lera'll be pleased."
|
||||||
role: bartender
|
role: bartender
|
||||||
access: [peer]
|
access: [peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [shift_end, routine]
|
situation: [shift_end, routine]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [sess, closing, generation-pass]
|
tags: [sess, closing, generation-pass]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Dialogue: day-worker at The Last Shift
|
# Dialogue: day-worker at The Last Shift
|
||||||
# NPC: Ren Tosse (Tier 3, NOBODY/CIVILIAN)
|
# NPC: Ren Tosse (Tier 3, NOBODY/CIVILIAN)
|
||||||
# Voice: casual-observant, asks questions without social friction. Just drifting.
|
# Voice: casual-observant, asks questions without social friction. Just drifting.
|
||||||
# KEY LINE: the-last-shift_d_001 — sounds like intel gathering on night operations
|
# KEY LINE: day-worker_d_001 — sounds like intel gathering on night operations
|
||||||
# Actually: asking about after-hours freight work availability
|
# Actually: asking about after-hours freight work availability
|
||||||
# Ticket: #307 | Sprint: 12
|
# Ticket: #307 | Sprint: 12
|
||||||
|
|
||||||
@@ -15,14 +15,14 @@ lines:
|
|||||||
# Actually: asking if there's paid after-hours freight work available.
|
# Actually: asking if there's paid after-hours freight work available.
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
- id: the-last-shift_d_001
|
- id: day-worker_d_001
|
||||||
text: "Night rotation still short-handed? Heard the after-hours freight pays decent."
|
text: "Night rotation still short-handed? Heard the after-hours freight pays decent."
|
||||||
role: day-worker
|
role: day-worker
|
||||||
access: [public]
|
access: [public]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [bar_evening, social]
|
situation: [bar_evening, social]
|
||||||
topic: [money, cargo]
|
topic: [money, cargo]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [ren, seems-important-but-isnt, job-seeking, night-shift]
|
tags: [ren, seems-important-but-isnt, job-seeking, night-shift]
|
||||||
dual_lens:
|
dual_lens:
|
||||||
smuggler: "Someone asking about the night rotation. Probably nothing. Watch them anyway."
|
smuggler: "Someone asking about the night rotation. Probably nothing. Watch them anyway."
|
||||||
@@ -36,54 +36,54 @@ lines:
|
|||||||
# ROUTINE LINES
|
# ROUTINE LINES
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
- id: the-last-shift_d_002
|
- id: day-worker_d_002
|
||||||
text: "Bay seven any better this cycle? The super was a problem last time."
|
text: "Bay seven any better this cycle? The super was a problem last time."
|
||||||
role: day-worker
|
role: day-worker
|
||||||
access: [public]
|
access: [public]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [social, bar_evening]
|
situation: [social, bar_evening]
|
||||||
topic: [routine, colleague]
|
topic: [routine, colleague]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [ren, job-seeking]
|
tags: [ren, job-seeking]
|
||||||
|
|
||||||
- id: the-last-shift_d_003
|
- id: day-worker_d_003
|
||||||
text: "Sova's decent. Station layout makes sense once you learn it."
|
text: "Sova's decent. Station layout makes sense once you learn it."
|
||||||
role: day-worker
|
role: day-worker
|
||||||
access: [public]
|
access: [public]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [social, bar_evening]
|
situation: [social, bar_evening]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [ren, atmospheric]
|
tags: [ren, atmospheric]
|
||||||
|
|
||||||
- id: the-last-shift_d_004
|
- id: day-worker_d_004
|
||||||
text: "Just passing through. Maybe not. Depends on the work."
|
text: "Just passing through. Maybe not. Depends on the work."
|
||||||
role: day-worker
|
role: day-worker
|
||||||
access: [public]
|
access: [public]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [social]
|
situation: [social]
|
||||||
topic: [personal, routine]
|
topic: [personal, routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [ren, drifter]
|
tags: [ren, drifter]
|
||||||
|
|
||||||
- id: the-last-shift_d_005
|
- id: day-worker_d_005
|
||||||
text: "Cheap drink here's better than most stations I've stopped at."
|
text: "Cheap drink here's better than most stations I've stopped at."
|
||||||
role: day-worker
|
role: day-worker
|
||||||
access: [public]
|
access: [public]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [bar_evening, social]
|
situation: [bar_evening, social]
|
||||||
topic: [personal]
|
topic: [personal]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [ren, atmospheric]
|
tags: [ren, atmospheric]
|
||||||
|
|
||||||
- id: the-last-shift_d_006
|
- id: day-worker_d_006
|
||||||
text: "Any idea when the manifest deadline runs? Trying to time when the work window opens."
|
text: "Any idea when the manifest deadline runs? Trying to time when the work window opens."
|
||||||
role: day-worker
|
role: day-worker
|
||||||
access: [public]
|
access: [public]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [social, bar_evening]
|
situation: [social, bar_evening]
|
||||||
topic: [cargo, routine]
|
topic: [cargo, routine]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
tags: [ren, seems-important-but-isnt, job-seeking]
|
tags: [ren, seems-important-but-isnt, job-seeking]
|
||||||
notes: >
|
notes: >
|
||||||
Second "seems important" line. Asking about manifest deadlines sounds
|
Second "seems important" line. Asking about manifest deadlines sounds
|
||||||
|
|||||||
@@ -12,66 +12,66 @@ lines:
|
|||||||
# SOCIAL — off-shift casual
|
# SOCIAL — off-shift casual
|
||||||
# ========================================
|
# ========================================
|
||||||
|
|
||||||
- id: the-last-shift_d_001
|
- id: kael-davan_d_015
|
||||||
text: "Saved you a seat. Lera's got the spiced rice tonight."
|
text: "Saved you a seat. Lera's got the spiced rice tonight."
|
||||||
role: dock-worker
|
role: dock-worker
|
||||||
access: [insider, peer]
|
access: [insider, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [bar_evening, social, arrival]
|
situation: [bar_evening, social, arrival]
|
||||||
mood: [fond]
|
mood: [warm]
|
||||||
topic: [personal, colleague]
|
topic: [personal, colleague]
|
||||||
tags: [kael, greeting, phase-1]
|
tags: [kael, greeting, phase-1]
|
||||||
|
|
||||||
- id: the-last-shift_d_002
|
- id: kael-davan_d_016
|
||||||
text: "First round's mine. Don't argue — you covered me last week."
|
text: "First round's mine. Don't argue — you covered me last week."
|
||||||
role: dock-worker
|
role: dock-worker
|
||||||
access: [insider, peer]
|
access: [insider, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [bar_evening, social]
|
situation: [bar_evening, social]
|
||||||
mood: [fond]
|
mood: [warm]
|
||||||
topic: [personal]
|
topic: [personal]
|
||||||
tags: [kael, social, phase-1]
|
tags: [kael, social, phase-1]
|
||||||
|
|
||||||
- id: the-last-shift_d_003
|
- id: kael-davan_d_017
|
||||||
text: "Voss tried to dock my overtime again. Lera told him off. Wish I'd seen it."
|
text: "Voss tried to dock my overtime again. Lera told him off. Wish I'd seen it."
|
||||||
role: dock-worker
|
role: dock-worker
|
||||||
access: [insider, peer]
|
access: [insider, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [bar_evening, social]
|
situation: [bar_evening, social]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
topic: [colleague, routine]
|
topic: [colleague, routine]
|
||||||
tags: [kael, humor, voss, lera]
|
tags: [kael, humor, voss, lera]
|
||||||
|
|
||||||
- id: the-last-shift_d_004
|
- id: kael-davan_d_018
|
||||||
text: "Torek's holding court at the bar again. How does he afford to drink like that?"
|
text: "Torek's holding court at the bar again. How does he afford to drink like that?"
|
||||||
role: dock-worker
|
role: dock-worker
|
||||||
access: [insider, peer]
|
access: [insider, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [bar_evening, social]
|
situation: [bar_evening, social]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
topic: [colleague]
|
topic: [colleague]
|
||||||
tags: [kael, observational, torek]
|
tags: [kael, observational, torek]
|
||||||
knowledge_grant:
|
knowledge_grant:
|
||||||
fact_id: investigation.torek_spending_pattern
|
fact_id: investigation.torek_spending_pattern
|
||||||
confidence: suspects
|
confidence: suspects
|
||||||
|
|
||||||
- id: the-last-shift_d_005
|
- id: kael-davan_d_019
|
||||||
text: "Quiet night. I like these ones. No drama, just people winding down."
|
text: "Quiet night. I like these ones. No drama, just people winding down."
|
||||||
role: dock-worker
|
role: dock-worker
|
||||||
access: [peer]
|
access: [peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [bar_evening, social]
|
situation: [bar_evening, social]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
topic: [personal]
|
topic: [personal]
|
||||||
tags: [kael, atmospheric]
|
tags: [kael, atmospheric]
|
||||||
|
|
||||||
- id: the-last-shift_d_006
|
- id: kael-davan_d_020
|
||||||
text: "Lera asked about my overtime schedule. Told her I'm trying to cut back."
|
text: "Lera asked about my overtime schedule. Told her I'm trying to cut back."
|
||||||
role: dock-worker
|
role: dock-worker
|
||||||
access: [insider, peer]
|
access: [insider, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [bar_evening, social]
|
situation: [bar_evening, social]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
topic: [personal, routine]
|
topic: [personal, routine]
|
||||||
tags: [kael, casual, lera]
|
tags: [kael, casual, lera]
|
||||||
|
|
||||||
@@ -79,33 +79,33 @@ lines:
|
|||||||
# NAIA REFERENCES — personal, unprompted
|
# NAIA REFERENCES — personal, unprompted
|
||||||
# ========================================
|
# ========================================
|
||||||
|
|
||||||
- id: the-last-shift_d_007
|
- id: kael-davan_d_021
|
||||||
text: "Naia's meeting me here after her shift. Try not to embarrass me."
|
text: "Naia's meeting me here after her shift. Try not to embarrass me."
|
||||||
role: dock-worker
|
role: dock-worker
|
||||||
access: [insider, peer]
|
access: [insider, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [bar_evening, social]
|
situation: [bar_evening, social]
|
||||||
mood: [fond]
|
mood: [warm]
|
||||||
topic: [personal]
|
topic: [personal]
|
||||||
tags: [kael, unprompted, naia-reference]
|
tags: [kael, unprompted, naia-reference]
|
||||||
|
|
||||||
- id: the-last-shift_d_008
|
- id: kael-davan_d_022
|
||||||
text: "Naia wants to plan a trip. Off-station. Somewhere with actual sky. Sounds expensive."
|
text: "Naia wants to plan a trip. Off-station. Somewhere with actual sky. Sounds expensive."
|
||||||
role: dock-worker
|
role: dock-worker
|
||||||
access: [insider, peer]
|
access: [insider, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [bar_evening, social]
|
situation: [bar_evening, social]
|
||||||
mood: [fond]
|
mood: [warm]
|
||||||
topic: [personal]
|
topic: [personal]
|
||||||
tags: [kael, unprompted, naia-reference]
|
tags: [kael, unprompted, naia-reference]
|
||||||
|
|
||||||
- id: the-last-shift_d_009
|
- id: kael-davan_d_023
|
||||||
text: "She worries. I tell her it's just dock work. She doesn't buy it anymore."
|
text: "She worries. I tell her it's just dock work. She doesn't buy it anymore."
|
||||||
role: dock-worker
|
role: dock-worker
|
||||||
access: [insider]
|
access: [insider]
|
||||||
trust: real
|
trust: real
|
||||||
situation: [bar_evening, social, alone]
|
situation: [bar_evening, social, alone]
|
||||||
mood: [conflicted]
|
mood: [anxious]
|
||||||
topic: [personal, trust]
|
topic: [personal, trust]
|
||||||
tags: [kael, naia-reference, vulnerability]
|
tags: [kael, naia-reference, vulnerability]
|
||||||
|
|
||||||
@@ -113,33 +113,33 @@ lines:
|
|||||||
# RING OPS — bar context, trust-gated
|
# RING OPS — bar context, trust-gated
|
||||||
# ========================================
|
# ========================================
|
||||||
|
|
||||||
- id: the-last-shift_d_010
|
- id: kael-davan_d_024
|
||||||
text: "Nils wants to talk. Tomorrow, bay side. Said it's about volume."
|
text: "Nils wants to talk. Tomorrow, bay side. Said it's about volume."
|
||||||
role: dock-worker
|
role: dock-worker
|
||||||
access: [insider]
|
access: [insider]
|
||||||
trust: real
|
trust: real
|
||||||
situation: [bar_evening, social]
|
situation: [bar_evening, social]
|
||||||
mood: [concerned]
|
mood: [frustrated]
|
||||||
topic: [danger]
|
topic: [danger]
|
||||||
tags: [kael, ring-ops, nils]
|
tags: [kael, ring-ops, nils]
|
||||||
|
|
||||||
- id: the-last-shift_d_011
|
- id: kael-davan_d_025
|
||||||
text: "Keep it light tonight. Torek's listening and he doesn't know when to stop."
|
text: "Keep it light tonight. Torek's listening and he doesn't know when to stop."
|
||||||
role: dock-worker
|
role: dock-worker
|
||||||
access: [insider]
|
access: [insider]
|
||||||
trust: real
|
trust: real
|
||||||
situation: [bar_evening, social]
|
situation: [bar_evening, social]
|
||||||
mood: [concerned]
|
mood: [frustrated]
|
||||||
topic: [danger, colleague]
|
topic: [danger, colleague]
|
||||||
tags: [kael, ring-ops, torek, caution]
|
tags: [kael, ring-ops, torek, caution]
|
||||||
|
|
||||||
- id: the-last-shift_d_012
|
- id: kael-davan_d_026
|
||||||
text: "Lera knows more than she lets on. She won't say anything — but don't test it."
|
text: "Lera knows more than she lets on. She won't say anything — but don't test it."
|
||||||
role: dock-worker
|
role: dock-worker
|
||||||
access: [insider]
|
access: [insider]
|
||||||
trust: real
|
trust: real
|
||||||
situation: [bar_evening, social, alone]
|
situation: [bar_evening, social, alone]
|
||||||
mood: [concerned]
|
mood: [frustrated]
|
||||||
topic: [colleague, danger]
|
topic: [colleague, danger]
|
||||||
tags: [kael, ring-ops, lera, caution]
|
tags: [kael, ring-ops, lera, caution]
|
||||||
knowledge_grant:
|
knowledge_grant:
|
||||||
@@ -150,32 +150,86 @@ lines:
|
|||||||
# POST-CONTRADICTION — at the bar, damaged trust
|
# POST-CONTRADICTION — at the bar, damaged trust
|
||||||
# ========================================
|
# ========================================
|
||||||
|
|
||||||
- id: the-last-shift_d_013
|
- id: kael-davan_d_027
|
||||||
text: "I saved your seat. ...Force of habit."
|
text: "I saved your seat. ...Force of habit."
|
||||||
role: dock-worker
|
role: dock-worker
|
||||||
access: [insider, peer]
|
access: [insider, peer]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [bar_evening, social]
|
situation: [bar_evening, social]
|
||||||
mood: [conflicted]
|
mood: [anxious]
|
||||||
topic: [personal]
|
topic: [personal]
|
||||||
tags: [kael, contaminated-trust, phase-5]
|
tags: [kael, contaminated-trust, phase-5]
|
||||||
|
|
||||||
- id: the-last-shift_d_014
|
- id: kael-davan_d_028
|
||||||
text: "Naia keeps asking why I'm quiet. I told her work stress. Not wrong."
|
text: "Naia keeps asking why I'm quiet. I told her work stress. Not wrong."
|
||||||
role: dock-worker
|
role: dock-worker
|
||||||
access: [insider]
|
access: [insider]
|
||||||
trust: real
|
trust: real
|
||||||
situation: [bar_evening, social]
|
situation: [bar_evening, social]
|
||||||
mood: [conflicted]
|
mood: [anxious]
|
||||||
topic: [personal]
|
topic: [personal]
|
||||||
tags: [kael, contaminated-trust, naia-reference, phase-5]
|
tags: [kael, contaminated-trust, naia-reference, phase-5]
|
||||||
|
|
||||||
- id: the-last-shift_d_015
|
- id: kael-davan_d_029
|
||||||
text: "We're good, right? You and me. We're still good."
|
text: "We're good, right? You and me. We're still good."
|
||||||
role: dock-worker
|
role: dock-worker
|
||||||
access: [insider]
|
access: [insider]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [bar_evening, social]
|
situation: [bar_evening, social]
|
||||||
mood: [worried]
|
mood: [anxious]
|
||||||
topic: [trust, personal]
|
topic: [trust, personal]
|
||||||
tags: [kael, vulnerability, phase-5]
|
tags: [kael, vulnerability, phase-5]
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# LAYER 2 GREETING VARIANTS — D-028 Layer 2
|
||||||
|
# Ticket #170 (Sprint 15). situation: [greeting] fires on initial contact.
|
||||||
|
# Access tier + mood encode relationship history state.
|
||||||
|
# ========================================
|
||||||
|
|
||||||
|
# first_meeting: InteractionMemory.count == 0. RelationshipState: Unknown → Public access only.
|
||||||
|
- id: kael-davan_d_030
|
||||||
|
text: "Evening. Friend of someone's, or first time at Lera's?"
|
||||||
|
role: dock-worker
|
||||||
|
access: [public]
|
||||||
|
trust: surface
|
||||||
|
situation: [greeting]
|
||||||
|
mood: [content]
|
||||||
|
topic: [personal]
|
||||||
|
tags: [kael, greeting, first-meeting, layer2]
|
||||||
|
notes: "Layer 2 greeting — first meeting at the bar. Kael is curious about a stranger but not suspicious. Off-shift, looser than at the terminal."
|
||||||
|
|
||||||
|
# established — peer/smuggler context: count >= 3, RelationshipState: Known/Friendly → Peer/Insider access.
|
||||||
|
- id: kael-davan_d_031
|
||||||
|
text: "You made it. I was starting to think something came up."
|
||||||
|
role: dock-worker
|
||||||
|
access: [peer, insider]
|
||||||
|
trust: surface
|
||||||
|
situation: [greeting]
|
||||||
|
mood: [warm]
|
||||||
|
topic: [personal, colleague]
|
||||||
|
tags: [kael, greeting, repeat-visit, peer-tone, layer2]
|
||||||
|
notes: "Layer 2 greeting — established, bar context. Warm, they were expected. Kael was watching for them."
|
||||||
|
|
||||||
|
# established — authority/detective context: count >= 3, RelationshipState: PersonOfInterest → Authority access.
|
||||||
|
- id: kael-davan_d_032
|
||||||
|
text: "Still in the district. Thought you'd be done by now."
|
||||||
|
role: dock-worker
|
||||||
|
access: [authority]
|
||||||
|
trust: surface
|
||||||
|
situation: [greeting]
|
||||||
|
mood: [suspicious]
|
||||||
|
topic: [routine]
|
||||||
|
tags: [kael, greeting, repeat-visit, authority-tone, layer2]
|
||||||
|
notes: "Layer 2 greeting — repeat with authority figure, bar context. Politely questioning their continued presence. The bar is supposed to be off-limits from work."
|
||||||
|
|
||||||
|
# post-confrontation: confrontation in InteractionMemory.notable_events. NPC mood shifted to suspicious/frustrated.
|
||||||
|
- id: kael-davan_d_033
|
||||||
|
text: "You came back."
|
||||||
|
role: dock-worker
|
||||||
|
access: [peer, insider]
|
||||||
|
trust: surface
|
||||||
|
situation: [greeting]
|
||||||
|
mood: [suspicious, frustrated]
|
||||||
|
topic: [personal, trust]
|
||||||
|
tags: [kael, greeting, post-confrontation, layer2]
|
||||||
|
notes: "Layer 2 greeting — post-confrontation, bar context. Flat, loaded. Not a welcome. Three words that replace a whole conversation they both don't want to have."
|
||||||
|
|||||||
@@ -9,17 +9,17 @@ location: the-last-shift
|
|||||||
role: commission-liaison
|
role: commission-liaison
|
||||||
lines:
|
lines:
|
||||||
|
|
||||||
- id: the-last-shift_d_204
|
- id: pc-detective_d_001
|
||||||
text: "Good evening. Off the clock, more or less. Enjoying the local hospitality."
|
text: "Good evening. Off the clock, more or less. Enjoying the local hospitality."
|
||||||
role: commission-liaison
|
role: commission-liaison
|
||||||
access: [public]
|
access: [public]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [bar_evening, greeting]
|
situation: [bar_evening, greeting]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
topic: [personal]
|
topic: [personal]
|
||||||
tags: [pc-detective, greeting, public-tier]
|
tags: [pc-detective, greeting, public-tier]
|
||||||
|
|
||||||
- id: the-last-shift_d_205
|
- id: pc-detective_d_002
|
||||||
text: "Ms. Venn recommended the ale. She was right. This district has its charms."
|
text: "Ms. Venn recommended the ale. She was right. This district has its charms."
|
||||||
role: commission-liaison
|
role: commission-liaison
|
||||||
access: [public]
|
access: [public]
|
||||||
@@ -28,7 +28,7 @@ lines:
|
|||||||
topic: [routine, colleague]
|
topic: [routine, colleague]
|
||||||
tags: [pc-detective, social, public-tier, sera]
|
tags: [pc-detective, social, public-tier, sera]
|
||||||
|
|
||||||
- id: the-last-shift_d_206
|
- id: pc-detective_d_003
|
||||||
text: "Just unwinding. The review can wait until morning. Have a good evening."
|
text: "Just unwinding. The review can wait until morning. Have a good evening."
|
||||||
role: commission-liaison
|
role: commission-liaison
|
||||||
access: [public]
|
access: [public]
|
||||||
|
|||||||
@@ -9,27 +9,27 @@ location: the-last-shift
|
|||||||
role: dock-worker
|
role: dock-worker
|
||||||
lines:
|
lines:
|
||||||
|
|
||||||
- id: the-last-shift_d_201
|
- id: pc-smuggler_d_001
|
||||||
text: "Evening. Just having a drink with friends. Can I help you?"
|
text: "Evening. Just having a drink with friends. Can I help you?"
|
||||||
role: dock-worker
|
role: dock-worker
|
||||||
access: [public]
|
access: [public]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [bar_evening, greeting]
|
situation: [bar_evening, greeting]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
topic: [personal]
|
topic: [personal]
|
||||||
tags: [pc-smuggler, greeting, public-tier]
|
tags: [pc-smuggler, greeting, public-tier]
|
||||||
|
|
||||||
- id: the-last-shift_d_202
|
- id: pc-smuggler_d_002
|
||||||
text: "Lera's ale is the one good thing about a ten-hour shift. Don't let anyone tell you different."
|
text: "Lera's ale is the one good thing about a ten-hour shift. Don't let anyone tell you different."
|
||||||
role: dock-worker
|
role: dock-worker
|
||||||
access: [public]
|
access: [public]
|
||||||
trust: surface
|
trust: surface
|
||||||
situation: [bar_evening, social]
|
situation: [bar_evening, social]
|
||||||
mood: [comfortable]
|
mood: [content]
|
||||||
topic: [routine]
|
topic: [routine]
|
||||||
tags: [pc-smuggler, social, public-tier]
|
tags: [pc-smuggler, social, public-tier]
|
||||||
|
|
||||||
- id: the-last-shift_d_203
|
- id: pc-smuggler_d_003
|
||||||
text: "Off the clock, thanks. Whatever it is, it can wait until morning."
|
text: "Off the clock, thanks. Whatever it is, it can wait until morning."
|
||||||
role: dock-worker
|
role: dock-worker
|
||||||
access: [authority]
|
access: [authority]
|
||||||
|
|||||||