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 }
|
||||
```
|
||||
+84
@@ -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"
|
||||
+92
@@ -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 |
|
||||
+253
@@ -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
|
||||
+414
@@ -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()
|
||||
@@ -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:
|
||||
1. Use `git diff main...<branch>` (full diff) and filter in the prompt, or
|
||||
2. Read source files directly from the branch:
|
||||
`git show origin/<branch>:<path>`
|
||||
2. Read source files directly from the branch worktree (see below).
|
||||
|
||||
For large diffs (>1000 lines of source), provide **source files** rather than
|
||||
raw diff to reviewers — cleaner context, better reviews. Read files with
|
||||
`git show origin/<branch>:<path>` and include them in the prompt.
|
||||
raw diff to reviewers — cleaner context, better reviews.
|
||||
|
||||
**IMPORTANT — agent tool access:** Not all reviewer agents have Bash access.
|
||||
Agents that CAN read from branches themselves: **Hoshe, Tyre, Araminta**.
|
||||
Agents that CANNOT (no Bash tool): **Paula, Miri, Ozzie, Gestalt, Gore, Nigel**.
|
||||
**IMPORTANT — use worktree paths for ALL agents.** This project uses git
|
||||
worktrees. Each team branch is checked out at:
|
||||
|
||||
For agents without Bash, you MUST read the source files yourself (via
|
||||
`git show origin/<branch>:<path>`) and **paste the file contents directly
|
||||
into the agent prompt**. Do not tell these agents to read files — they can't.
|
||||
For very large PRs, read the key files (new/heavily modified) and include
|
||||
summaries or excerpts of minor changes. Also read and include the relevant
|
||||
`decisions/*.md` files these agents need for context.
|
||||
```
|
||||
/var/home/jeroenschweitzer/Projects/settled-reach/<branch>/
|
||||
```
|
||||
|
||||
For example, the `copy` branch lives at:
|
||||
```
|
||||
/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
|
||||
|
||||
|
||||
@@ -2,11 +2,20 @@
|
||||
|
||||
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`)
|
||||
|
||||
**Hoshe (Code Quality)**
|
||||
- `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
|
||||
- Error handling gaps
|
||||
- Test coverage (are new features tested?)
|
||||
@@ -16,44 +25,43 @@ Use `model: sonnet` for all reviewers — sufficient for review, saves cost.
|
||||
|
||||
**Tyre (Architecture)**
|
||||
- `subagent_type`: `tyre`, `model`: `sonnet`
|
||||
- Prompt: Include source code and commit log. Tell Tyre to read the relevant
|
||||
`decisions/*.md` files first, then review for:
|
||||
- Prompt: Provide the worktree path, list of changed files, and commit log.
|
||||
Tell Tyre to read the relevant `decisions/*.md` files from the worktree
|
||||
first, then review for:
|
||||
- Architectural consistency with project decisions
|
||||
- API/interface design quality
|
||||
- Dependency and coupling concerns
|
||||
- Scalability implications
|
||||
- Whether the change respects non-negotiable baselines (D-010, D-012)
|
||||
- Tyre can read files directly from the branch using `git show origin/<branch>:<path>`
|
||||
|
||||
## Copy reviews (`copy`)
|
||||
|
||||
**Hoshe (QA)**
|
||||
- `subagent_type`: `hoshe`, `model`: `sonnet`
|
||||
- Prompt: Include the changed files and commit log. Ask Hoshe to review for:
|
||||
- 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)
|
||||
- Broken references or links
|
||||
- Spelling and grammar
|
||||
- File organization and structure
|
||||
- Missing or orphaned files
|
||||
|
||||
**Paula (Narrative Depth)** — NO BASH ACCESS
|
||||
**Paula (Narrative Depth)**
|
||||
- `subagent_type`: `paula`, `model`: `sonnet`
|
||||
- Paula cannot read from branches. You must paste file contents and decision
|
||||
files directly into the prompt.
|
||||
- Prompt: Include full text of changed files, commit log, and relevant
|
||||
`decisions/*.md` content. Ask Paula to review for:
|
||||
- Prompt: Provide the worktree path, list of changed files, commit log, and
|
||||
list of relevant `decisions/*.md` files to read. Tell Paula to read all
|
||||
files from the worktree using the Read tool, then review for:
|
||||
- Narrative quality and character voice consistency
|
||||
- Whether dialogue and monologue feel authentic to the characters
|
||||
- Consequences and stakes — do choices carry weight?
|
||||
- Political and interpersonal depth
|
||||
- Emotional resonance — does the text make you feel something?
|
||||
|
||||
**Miri (World Consistency)** — NO BASH ACCESS
|
||||
**Miri (World Consistency)**
|
||||
- `subagent_type`: `miri`, `model`: `sonnet`
|
||||
- Miri cannot read from branches. You must paste file contents and decision
|
||||
files directly into the prompt.
|
||||
- Prompt: Include full text of changed files, commit log, and relevant
|
||||
`decisions/*.md` content. Ask Miri to review for:
|
||||
- Prompt: Provide the worktree path, list of changed files, commit log, and
|
||||
list of relevant `decisions/*.md` files to read. Tell Miri to read all
|
||||
files from the worktree using the Read tool, then review for:
|
||||
- Lore accuracy — do facts match established setting?
|
||||
- Internal consistency across files
|
||||
- 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)**
|
||||
- `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
|
||||
- Asset organization and directory structure
|
||||
- Missing or broken references in scene/resource files
|
||||
- Import settings consistency
|
||||
|
||||
**Araminta (Art Direction)** — HAS BASH ACCESS
|
||||
**Araminta (Art Direction)**
|
||||
- `subagent_type`: `araminta`, `model`: `sonnet`
|
||||
- Prompt: Include the changed files and commit log. Tell Araminta to read
|
||||
the style guide and relevant design docs first, then review for:
|
||||
- Prompt: Provide the worktree path, list of changed files, and commit log.
|
||||
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
|
||||
- Color palette adherence
|
||||
- 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)**
|
||||
- `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
|
||||
- Audio asset organization and directory structure
|
||||
- Missing or broken references
|
||||
- Import/bus configuration consistency
|
||||
|
||||
**Ozzie (Player Experience)** — NO BASH ACCESS
|
||||
**Ozzie (Player Experience)**
|
||||
- `subagent_type`: `ozzie`, `model`: `sonnet`
|
||||
- Ozzie cannot read from branches. You must paste file contents directly
|
||||
into the prompt.
|
||||
- Prompt: Include full text of changed files and commit log. Ask Ozzie to
|
||||
- Prompt: Provide the worktree path, list of changed files, and commit log.
|
||||
Tell Ozzie to read all files from the worktree using the Read tool, then
|
||||
review for:
|
||||
- Emotional impact — does the audio enhance the moment?
|
||||
- Atmosphere and tone — does it feel like the Commonwealth?
|
||||
|
||||
@@ -88,9 +88,10 @@ When all Round N tasks are complete:
|
||||
|
||||
Wrap-up sequence:
|
||||
1. Qatux produces final `workshop-outcomes.md` from accumulated notes
|
||||
2. If SI is present, SI creates tickets from decided items
|
||||
3. Send shutdown_request to all agents (qatux and si last, after they finish their output tasks)
|
||||
4. TeamDelete to clean up
|
||||
2. Qatux creates or updates diagrams (via `/d2-diagram`) for any new D-records produced by the workshop
|
||||
3. If SI is present, SI creates tickets from decided items
|
||||
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
|
||||
|
||||
|
||||
@@ -21,6 +21,9 @@ renderer/output/*.png
|
||||
# Database (shared across worktrees at ../settledreach.db, not tracked)
|
||||
db/commonwealth.db*
|
||||
|
||||
# Frame0 ID mapping files (ephemeral, per-machine)
|
||||
*.idmap.json
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
@@ -6,6 +6,72 @@ Format based on [Keep a Changelog](https://keepachangelog.com/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v0.1.15] — 2026-02-23
|
||||
|
||||
### Added
|
||||
- Sprint 16 "Converse" briefings — 8 tickets across server/client/copy/visual teams
|
||||
- 19 UI wireframes — HUD, dialogue, monologue, popups, menus in v0.1 and v1.0 variants with D-record cross-references
|
||||
- d2-diagram skill — text-to-diagram generation with project defaults (theme 200, dagre, PNG)
|
||||
- frame0-wireframe skill — UI wireframing via Frame0 HTTP API, replaces MCP dependency with bash+curl
|
||||
- 16 decision diagrams — architecture, data-flow, entity, state, and UI categories covering all project decisions
|
||||
|
||||
### Changed
|
||||
- frame0-wireframe skill rewritten — JSON-as-truth workflow with frame0-sync.py, batch export, renderer-only guidance
|
||||
- pr-review skill — all reviewer agents now use worktree paths instead of git show
|
||||
- Dialogue panel is always visible as permanent insert UI element (D-061)
|
||||
- Makefile: check-protocol target verifies server/client protocol versions match before build
|
||||
- D-035 amended: line ID namespace changed from location-scoped to NPC-scoped (Sprint 15)
|
||||
- Tilemap z-layer filtering — FloorTiles renders z=0 only, z=1/z>1 reserved for future layer nodes (#71, D-049)
|
||||
- Entity 24x32 footprint per D-044 visual hierarchy — split ENTITY_SIZE into WIDTH/HEIGHT with separate offsets (#72)
|
||||
- Follow target stub on GameState — `follow_target_id` field ready for server #241 Follow verb
|
||||
- Manual exponential camera smoothing — CAMERA_SMOOTHING_SPEED constant (8.0), same lerp pattern as entity renderer (#117)
|
||||
- 31 new Sprint 15 validation tests — camera smoothing, UI framework z-layers, entity footprint, Sprint 14 regressions
|
||||
- SpatialIndex trait with naive Vec implementation — entities_in_range, entities_at, update methods with Manhattan distance (#340)
|
||||
- NPC generation pipeline — procedural seeding of all 10 D-024 axes via SimRng with constraint validation (#92)
|
||||
- Personality and tell system — 5 tell categories (Nervous, Angry, Friendly, Guarded, RoutineDeviation) derived from NPC axis values each tick (#90)
|
||||
- Tolerance threshold monitoring — ToleranceBreachEvent on stress exceeding per-NPC threshold, mood FSM integration (#105)
|
||||
- Routine deviation detection — RoutineDeviationEvent on wrong location/activity for day phase, absence detection, pathfinding-aware (#243)
|
||||
- Follow mechanic — Follow verb, proximity/LOS tracking, double-frequency observation events, NPC suspicion accumulation, configurable thresholds (#241)
|
||||
- Monologue event triggers — observe_npc, hear_sound, observe_anomaly, witness_interaction, post_conversation with D-035 context tags (#119)
|
||||
- Protocol v13 — tell_state on VisibleEntity, follow_state on ObserverSnapshot, Follow verb
|
||||
|
||||
## [v0.1.14] — 2026-02-21
|
||||
|
||||
### Added
|
||||
- Unified dialogue log — player-NPC and overheard NPC-NPC conversations in one chronological scrolling panel (#535, D-061/D-078)
|
||||
- F3 debug overlay — real-time game state display with tick, FPS, position, entity counts, dialogue/monologue status (#511)
|
||||
- Monologue display — multi-line priority queue with character colours, italic BBCode, stagger animation (#122)
|
||||
- Protocol v9 — conversation_events, conversation_ended, dialogue_response fields with carry-forward logic
|
||||
- Dialogue theme system — configurable NPC name colour palette, entry timing, passive opacity via dialogue-theme.yaml
|
||||
- Monologue display system visual spec — typography, positioning, stacking, priority, fade animation, character color differentiation, 80-char line constraint (#315)
|
||||
- Entity color system spec — D-033 relationship-to-player mapping, transition animations, color blindness assessment (#304)
|
||||
- Text display hierarchy spec — 4 content pipelines (dialogue, monologue, observation, environmental) with z-layers and positioning (#316)
|
||||
- Sound indicator visual design — fog-edge pulse for D-018 three-range sound model with direction encoding and range differentiation (#317)
|
||||
- THE FRIEND visual treatment spec — 3-phase earned visual detail for Kael Davan and Sera Venn (#318)
|
||||
- Environmental text visual standards — signage, terminal, and news ticker rendering with bilingual Concordat/Krenn treatment (#334)
|
||||
- Tell visual/behavioral expression spec — 5 tell categories mapped to 6 Tier 2 behaviors (#251)
|
||||
- Monologue line pool maxLength raised from 160 to 256 chars (soft guidance ≤160)
|
||||
- NPC name masking infrastructure — entity-anchored dialogue log with server-side role labels, retroactive name update on learning, NpcColorIndex for stable color assignment
|
||||
- Dialogue option keyboard selection (1/2/3 number keys) and numbered option labels
|
||||
- Interaction list chrome — background panel, mouse hover highlighting, click-to-interact, pointing hand cursor
|
||||
|
||||
### Changed
|
||||
- D-061 updated to document unified conversation log architecture from Sprint 14
|
||||
- Dialogue options switched from RichTextLabel to Label for reliable VBoxContainer sizing
|
||||
|
||||
### Fixed
|
||||
- Visual grammar dialogue max-width corrected from "~70% screen width" to 640px per D-076
|
||||
- BBCode injection in dialogue log formatting — server-sourced strings now escaped with [lb]
|
||||
- Per-frame dialogue log rebuild replaced with dirty flag (performance)
|
||||
- dialogue_active lifecycle — now cleared after panel fade completes per D-064
|
||||
- PAUSE/UNPAUSE routed through main.gd input recording for bug report replay (#507)
|
||||
- WASD input freeze after filing bug report — LineEdit focus not released before queue_free() across CanvasLayers
|
||||
- WASD not reactivating after Talk — dialogue_active held for entry_lifetime instead of cleared immediately
|
||||
- Recognition chime spam — entity IDs now tracked permanently per room instead of expiring
|
||||
- Audio path warning — res://audio/ corrected to res://assets/audio/ in AudioManager
|
||||
- world_radial.tscn anchors_preset warning — changed from 15 to 0
|
||||
- bug_report_dialog.gd push_warning changed to print for informational message
|
||||
|
||||
## [v0.1.13] — 2026-02-20
|
||||
|
||||
### Added
|
||||
|
||||
@@ -20,6 +20,7 @@ docs/
|
||||
briefings/ # Per-agent context briefings (maintained by Qatux)
|
||||
architecture/ # Technical architecture 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.)
|
||||
workshops/ # Workshop briefs and outputs (per-workshop subdirectories)
|
||||
db/
|
||||
@@ -144,6 +145,7 @@ Key rules:
|
||||
### File conventions
|
||||
- Decisions: domain files in `decisions/` (see `decisions/README.md` for index)
|
||||
- Decision IDs: `D-NNN` (confirmed), `Q-NNN` (open questions), `R-NNN` (rejected)
|
||||
- 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
|
||||
- Briefings: one per agent, updated after decision-producing rounds
|
||||
- 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)
|
||||
|
||||
.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 \
|
||||
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 \
|
||||
@@ -28,6 +28,7 @@ help:
|
||||
@echo " make ci Run full CI pipeline locally"
|
||||
@echo " make ci-client Run client CI checks"
|
||||
@echo " make ci-server Run server CI checks"
|
||||
@echo " make check-protocol Verify server/client protocol versions match"
|
||||
@echo " make clean Remove build artifacts and caches"
|
||||
@echo ""
|
||||
@echo " make db-backup Backup shared database to git (main only)"
|
||||
@@ -82,7 +83,16 @@ setup-hooks:
|
||||
|
||||
# --- 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:
|
||||
cd server && cargo build
|
||||
@@ -94,7 +104,7 @@ build-client:
|
||||
# --- Run ---
|
||||
|
||||
server:
|
||||
cd server && cargo run
|
||||
cd server && cargo run --bin settled-reach-server
|
||||
|
||||
client:
|
||||
@test -n "$(GODOT)" || { echo "Godot not found. Run 'make setup' first."; exit 1; }
|
||||
@@ -103,7 +113,7 @@ client:
|
||||
game: stop build
|
||||
@test -n "$(GODOT)" || { echo "Godot not found. Run 'make setup' first."; exit 1; }
|
||||
@echo "Starting server..."
|
||||
@cd server && cargo run &
|
||||
@cd server && cargo run --bin settled-reach-server &
|
||||
@sleep 2
|
||||
@echo "Starting client..."
|
||||
@SR_LIVE=1 $(GODOT) --path client
|
||||
@@ -290,7 +300,7 @@ perf-baseline:
|
||||
|
||||
debug-schedule:
|
||||
@echo "Dumping bevy_ecs schedule graph..."
|
||||
@cd server && cargo run -- --dump-schedule
|
||||
@cd server && cargo run --bin settled-reach-server -- --dump-schedule
|
||||
|
||||
content-ron:
|
||||
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"
|
||||
type="AudioStreamOggVorbis"
|
||||
uid="uid://caeuztw9ah5h5l"
|
||||
uid="uid://cow7symyvpmal"
|
||||
path="res://.godot/imported/sfx_npc_murmur.ogg-bfd7592cfea1b592d89f107b6cd33838.oggvorbisstr"
|
||||
|
||||
[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={}
|
||||
@@ -0,0 +1,47 @@
|
||||
# Dialogue Log Theme — The Settled Reach
|
||||
#
|
||||
# Ticket: #535 | Sprint: 14
|
||||
# Colors and timing for the unified dialogue log panel.
|
||||
# Loaded by dialogue_box.gd at runtime.
|
||||
|
||||
# ============================================================
|
||||
# PLAYER COLOR
|
||||
# Fixed color for the player's name in dialogue log entries.
|
||||
# ============================================================
|
||||
player_color: "#e0e8ff"
|
||||
|
||||
# ============================================================
|
||||
# NPC COLOR PALETTE
|
||||
# 8 distinct colors for NPC names. Indexed by hash(npc_name) % 8.
|
||||
# Must be readable on a dark semi-transparent panel background.
|
||||
# ============================================================
|
||||
npc_colors:
|
||||
0: "#4a9ebb" # teal
|
||||
1: "#6bc9a6" # green
|
||||
2: "#e8c547" # amber
|
||||
3: "#d49e5d" # warm orange
|
||||
4: "#b586d4" # lavender
|
||||
5: "#d45d5d" # muted red
|
||||
6: "#5daa7d" # forest
|
||||
7: "#7daccc" # sky blue
|
||||
|
||||
# ============================================================
|
||||
# TEXT COLORS
|
||||
# Arrow separator and speech text color.
|
||||
# ============================================================
|
||||
arrow_color: "#8890a0"
|
||||
speech_color: "#c8d0e0"
|
||||
|
||||
# ============================================================
|
||||
# PASSIVE (OVERHEARD) OPACITY
|
||||
# Base opacity multiplier for non-player-centric lines.
|
||||
# 1.0 = full opacity, 0.0 = invisible.
|
||||
# ============================================================
|
||||
passive_opacity: 0.9
|
||||
|
||||
# ============================================================
|
||||
# ENTRY TIMING
|
||||
# All entry types share the same lifetime and fade duration.
|
||||
# ============================================================
|
||||
entry_lifetime_seconds: 45.0
|
||||
entry_fade_seconds: 5.0
|
||||
@@ -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
|
||||
@@ -120,6 +120,11 @@ bug_report={
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194343,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
debug_overlay={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194334,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
teleport_hub={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194317,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
|
||||
+12
-1
@@ -1,4 +1,4 @@
|
||||
[gd_scene load_steps=22 format=3 uid="uid://bswrmh7w8dbgm"]
|
||||
[gd_scene load_steps=23 format=3 uid="uid://bswrmh7w8dbgm"]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/main.gd" id="1_main"]
|
||||
[ext_resource type="Script" path="res://scripts/rendering/world_renderer.gd" id="2_world"]
|
||||
@@ -21,6 +21,7 @@
|
||||
[ext_resource type="PackedScene" path="res://ui/checklist_overlay.tscn" id="18_checklist"]
|
||||
[ext_resource type="PackedScene" path="res://ui/bug_report_dialog.tscn" id="19_bugreport"]
|
||||
[ext_resource type="PackedScene" path="res://ui/settings_dialog.tscn" id="21_settings"]
|
||||
[ext_resource type="Script" path="res://scripts/ui/debug_overlay.gd" id="22_debug"]
|
||||
|
||||
[node name="Game" type="Node2D"]
|
||||
script = ExtResource("1_main")
|
||||
@@ -152,6 +153,16 @@ layer = 20
|
||||
; D-065: Inventory grid — 3x3, bottom-right, 40x40px, 1-9 hotkeys
|
||||
[node name="InventoryGrid" parent="UILayer" instance=ExtResource("12_inv")]
|
||||
|
||||
; #511: F3 debug overlay — real-time game state, toggled by F3
|
||||
[node name="DebugOverlay" type="Control" parent="UILayer"]
|
||||
anchors_preset = 0
|
||||
offset_left = 16
|
||||
offset_top = 120
|
||||
offset_right = 400
|
||||
offset_bottom = 400
|
||||
mouse_filter = 2
|
||||
script = ExtResource("22_debug")
|
||||
|
||||
; D-056: Cursor state machine — insert-styled geometric cursor, topmost in UILayer
|
||||
[node name="CursorRenderer" type="Node2D" parent="UILayer"]
|
||||
script = ExtResource("10_cursor")
|
||||
|
||||
@@ -44,7 +44,7 @@ const FILTER_CUTOFF_DEFAULT := 20500.0
|
||||
# --- D-073: Zone crossfade constants ---
|
||||
const CROSSFADE_DURATION := 1.8 # D-073: 1.5-2s spec, mid-range
|
||||
|
||||
# Maps server zone_id strings to ambient asset keys (filenames in res://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) —
|
||||
# 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
|
||||
@@ -112,15 +112,15 @@ func _setup_buses() -> void:
|
||||
# --- Asset registry (D-068 directory-scan pattern) ---
|
||||
|
||||
func _scan_registry() -> void:
|
||||
_scan_dir("res://audio/")
|
||||
_scan_dir("res://assets/audio/")
|
||||
print("AudioManager: %d assets registered" % _registry.size())
|
||||
|
||||
|
||||
func _scan_dir(path: String) -> void:
|
||||
var dir := DirAccess.open(path)
|
||||
if dir == null:
|
||||
if path == "res://audio/":
|
||||
print("AudioManager: res://audio/ not found — all play methods no-op")
|
||||
if path == "res://assets/audio/":
|
||||
print("AudioManager: res://assets/audio/ not found — all play methods no-op")
|
||||
return
|
||||
dir.list_dir_begin()
|
||||
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) ---
|
||||
# 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).
|
||||
# Missing assets no-op gracefully (D-038 fallback pattern).
|
||||
const SOUND_EVENT_ASSETS: Dictionary = {
|
||||
|
||||
@@ -19,11 +19,20 @@ var visibility_sectors: Dictionary = {} # Vector2i -> "Forward"/"Peripheral"
|
||||
# refined when the server assigns explicit player entity IDs).
|
||||
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)
|
||||
var nearby_interactions: Array = [] # [{entity_id, entity_type, distance, verbs: [{kind, label, priority, available}]}]
|
||||
|
||||
# v5 fields (#414)
|
||||
var current_monologue: Variant = null # {id, text, duration_seconds} or null
|
||||
var current_monologue: Variant = null # {id, text, duration_seconds, priority, is_urgent} or null
|
||||
|
||||
# #122 (D-032): Character lattice profile — selects monologue text colour palette.
|
||||
# "lattice_augmented" = detective, "lattice_baseline" = smuggler.
|
||||
# Server sends this field as part of the player's capability snapshot.
|
||||
var lattice_profile: String = "lattice_baseline"
|
||||
|
||||
# v6 fields (#449, D-053, D-065)
|
||||
var player_stance: String = "Walk" # Sprint/Walk/Careful/Crouch
|
||||
@@ -54,6 +63,13 @@ var rng_seed: Variant = null
|
||||
# v7 fields (#431, D-059/D-060)
|
||||
var pending_recognitions: Array = [] # [{entity_id, x, y, z, remaining_ticks, total_delay_ticks}]
|
||||
|
||||
# v8 fields (#305, D-028): NPC follow-up after player dialogue choice
|
||||
var dialogue_response: Variant = null # {line_id, text, speaker_entity_id}
|
||||
|
||||
# v9 fields (#535, D-078): Overheard NPC-to-NPC conversations
|
||||
var conversation_events: Array = [] # [{speaker_id, target_id, speaker_name, target_name, occluded_line}]
|
||||
var conversation_ended: Array = [] # [{speaker_id, target_id}]
|
||||
|
||||
# #126, D-018: Medium-range sound events for fog-edge directional indicators.
|
||||
# Format: [{x, y, event_type, range_category}] — server sends current medium events per tick.
|
||||
var medium_sound_events: Array = []
|
||||
@@ -138,6 +154,10 @@ func apply_snapshot(snapshot: Dictionary) -> void:
|
||||
else:
|
||||
current_monologue = null
|
||||
|
||||
# #122: lattice_profile — character insert capability level for monologue colour
|
||||
if snapshot.has("lattice_profile") and snapshot.lattice_profile is String:
|
||||
lattice_profile = snapshot.lattice_profile
|
||||
|
||||
# v6: player_stance (#449, D-053)
|
||||
if snapshot.has("player_stance") and snapshot.player_stance is String:
|
||||
player_stance = snapshot.player_stance
|
||||
@@ -160,6 +180,24 @@ func apply_snapshot(snapshot: Dictionary) -> void:
|
||||
else:
|
||||
pending_recognitions = []
|
||||
|
||||
# v9: conversation_events (#535, D-078) — overheard NPC-to-NPC lines
|
||||
if snapshot.has("conversation_events") and snapshot.conversation_events is Array:
|
||||
conversation_events = snapshot.conversation_events
|
||||
else:
|
||||
conversation_events = []
|
||||
|
||||
# v9: conversation_ended (#535, D-078) — pairs whose conversation ended
|
||||
if snapshot.has("conversation_ended") and snapshot.conversation_ended is Array:
|
||||
conversation_ended = snapshot.conversation_ended
|
||||
else:
|
||||
conversation_ended = []
|
||||
|
||||
# v8: dialogue_response (#305, D-028) — NPC follow-up after player choice
|
||||
if snapshot.has("dialogue_response") and snapshot.dialogue_response is Dictionary:
|
||||
dialogue_response = snapshot.dialogue_response
|
||||
else:
|
||||
dialogue_response = null
|
||||
|
||||
# v8: gauntlet mode (#496) — room_id and gauntlet_mode
|
||||
if snapshot.has("gauntlet_mode") and snapshot.gauntlet_mode == true:
|
||||
gauntlet_mode = true
|
||||
|
||||
@@ -241,6 +241,17 @@ func receive_bytes(bytes: PackedByteArray) -> void:
|
||||
snapshot["current_monologue"] = _last_snapshot["current_monologue"]
|
||||
if snapshot.get("current_dialogue") == null and _last_snapshot.get("current_dialogue") != null:
|
||||
snapshot["current_dialogue"] = _last_snapshot["current_dialogue"]
|
||||
# #535: Carry forward one-shot dialogue events (arrays merge, scalar falls through)
|
||||
if snapshot.get("dialogue_response") == null and _last_snapshot.get("dialogue_response") != null:
|
||||
snapshot["dialogue_response"] = _last_snapshot["dialogue_response"]
|
||||
var old_conv_events: Array = _last_snapshot.get("conversation_events", [])
|
||||
if old_conv_events.size() > 0:
|
||||
var new_conv_events: Array = snapshot.get("conversation_events", [])
|
||||
snapshot["conversation_events"] = old_conv_events + new_conv_events
|
||||
var old_conv_ended: Array = _last_snapshot.get("conversation_ended", [])
|
||||
if old_conv_ended.size() > 0:
|
||||
var new_conv_ended: Array = snapshot.get("conversation_ended", [])
|
||||
snapshot["conversation_ended"] = old_conv_ended + new_conv_ended
|
||||
_last_snapshot = snapshot
|
||||
|
||||
# Drain the outbound buffer. Returns raw input entries for batch encoding.
|
||||
@@ -396,6 +407,37 @@ func _test_snapshot() -> Dictionary:
|
||||
"total_delay_ticks": total_delay,
|
||||
})
|
||||
|
||||
# #535: Mock overheard NPC-NPC conversation (D-078)
|
||||
# Two NPCs (Mira and Soren) trade lines every 5 ticks starting at tick 3.
|
||||
# Conversation ends after 6 exchanges (~30 ticks).
|
||||
var conv_events: Array = []
|
||||
var conv_ended: Array = []
|
||||
var conv_start := 3
|
||||
var conv_lines := [
|
||||
{"speaker": "Mira", "target": "Soren", "line": "The cargo manifests don't add up. Three containers unaccounted for."},
|
||||
{"speaker": "Soren", "target": "Mira", "line": "Could be a logging error. Happens every... cycle."},
|
||||
{"speaker": "Mira", "target": "Soren", "line": "Not like this. Someone moved them after... check."},
|
||||
{"speaker": "Soren", "target": "Mira", "line": "You're reading too much into it. The docks are... these days."},
|
||||
{"speaker": "Mira", "target": "Soren", "line": "Then explain the weight discrepancy. Two hundred kilos... just gone."},
|
||||
{"speaker": "Soren", "target": "Mira", "line": "Fine. I'll pull the bay... tonight. But keep this between us."},
|
||||
]
|
||||
var conv_tick_interval := 5
|
||||
var conv_total_ticks := conv_lines.size() * conv_tick_interval
|
||||
if _test_tick >= conv_start and _test_tick < conv_start + conv_total_ticks:
|
||||
var conv_index := (_test_tick - conv_start) / conv_tick_interval
|
||||
var within_tick := (_test_tick - conv_start) % conv_tick_interval
|
||||
if within_tick == 0 and conv_index < conv_lines.size():
|
||||
var cl: Dictionary = conv_lines[conv_index]
|
||||
conv_events.append({
|
||||
"speaker_id": 10,
|
||||
"target_id": 11,
|
||||
"speaker_name": cl.speaker,
|
||||
"target_name": cl.target,
|
||||
"occluded_line": cl.line,
|
||||
})
|
||||
elif _test_tick == conv_start + conv_total_ticks:
|
||||
conv_ended.append({"speaker_id": 10, "target_id": 11})
|
||||
|
||||
return {
|
||||
"tick": _test_tick,
|
||||
"version": Protocol.PROTOCOL_VERSION,
|
||||
@@ -417,6 +459,8 @@ func _test_snapshot() -> Dictionary:
|
||||
"current_dialogue": dialogue,
|
||||
"pending_recognitions": pending_recs,
|
||||
"gauntlet_mode": _test_gauntlet_mode,
|
||||
"conversation_events": conv_events,
|
||||
"conversation_ended": conv_ended,
|
||||
}
|
||||
|
||||
# Generate a small test room: 8x6 room with walls, a door, and floor
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://rqfw0ycyb4c6
|
||||
@@ -91,14 +91,19 @@ const FACING_INDICATOR_SIZE: float = 6.0
|
||||
const FACING_INDICATOR_OFFSET: float = 14.0
|
||||
|
||||
# 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
|
||||
# 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
|
||||
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
|
||||
const IMPLANT_TEXT_COLOR: Color = Color("#E0F7FA") # Cyan-white — primary text
|
||||
const IMPLANT_TEXT_DIM: Color = Color("#9EBFC4") # Dimmed variant — secondary text
|
||||
|
||||
+98
-40
@@ -14,16 +14,19 @@ extends Node2D
|
||||
@onready var cursor_renderer = $UILayer/CursorRenderer # D-056: z-layer 7
|
||||
@onready var gauntlet_hud = $UILayer/GauntletHUD # #496: room timer + personal bests
|
||||
@onready var checklist_overlay = $UILayer/ChecklistOverlay # #503: auto-checklist progress
|
||||
@onready var debug_overlay = $UILayer/DebugOverlay # #511: F3 debug overlay
|
||||
@onready var bug_report_dialog = $ModalLayer/BugReportDialog # #495: F12 WRONG button
|
||||
@onready var settings_dialog = $ModalLayer/SettingsDialog # #528: audio settings (ESC/OPEN_MENU)
|
||||
|
||||
var _last_dialogue_npc_id: int = -1 # D-064: NPC entity_id for WalkAway input
|
||||
var _last_dialogue_npc_name: String = "" # #535: NPC name for dialogue_response attribution
|
||||
var _camera_anchored: bool = false
|
||||
var _last_monologue_tick: int = -1 # Prevent re-consuming monologue when same tick polled twice
|
||||
var _last_monologue_tick: int = -1 # Prevent re-consuming monologue when same tick polled twice
|
||||
var _last_dialogue_tick: int = -1
|
||||
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 _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 _current_zone: String = "" # D-073 (#529): zone tracking for ambient crossfades
|
||||
|
||||
@@ -32,13 +35,10 @@ const LISTENING_FOCUS_TICKS: int = 30 # D-071: stationary ticks before Listenin
|
||||
func _ready() -> void:
|
||||
print("The Settled Reach — client initialized")
|
||||
|
||||
# Disable camera smoothing during init. Camera2D's position_smoothing
|
||||
# lerps an internal smoothed_camera_pos toward global_position each frame.
|
||||
# That smoothed position initializes at (0,0) — the Camera2D's default in
|
||||
# the .tscn. Even after we set global_position to the player coords,
|
||||
# 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.
|
||||
# #117: Manual lerp approach — disable Godot's built-in Camera2D smoothing.
|
||||
# We lerp camera.global_position directly in _process() using CAMERA_SMOOTHING_SPEED,
|
||||
# matching entity_renderer.gd's exponential smoothing pattern. Built-in smoothing
|
||||
# would conflict because we'd be setting global_position to the target every frame.
|
||||
camera.position_smoothing_enabled = false
|
||||
|
||||
# Connect to simulation (test mode sets CONNECTED immediately)
|
||||
@@ -47,7 +47,7 @@ func _ready() -> void:
|
||||
# Camera anchor: snap to player position before the first frame renders.
|
||||
# In test mode poll_snapshot() returns synchronously — position is set
|
||||
# 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()
|
||||
if first_snapshot != null:
|
||||
GameState.apply_snapshot(first_snapshot)
|
||||
@@ -59,13 +59,15 @@ func _ready() -> void:
|
||||
dialogue_box.option_selected.connect(_on_dialogue_option_selected)
|
||||
dialogue_box.dialogue_dismissed.connect(_on_dialogue_dismissed)
|
||||
dialogue_box.confrontation_monologue.connect(_on_confrontation_monologue)
|
||||
dialogue_box.pause_requested.connect(_on_dialogue_pause_requested)
|
||||
dialogue_box.unpause_requested.connect(_on_dialogue_unpause_requested)
|
||||
|
||||
# #496: Print gauntlet session summary on disconnect
|
||||
if gauntlet_hud:
|
||||
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
|
||||
var snapshot: Variant = SimBridge.poll_snapshot()
|
||||
if snapshot != null:
|
||||
@@ -129,6 +131,10 @@ func _process(_delta: float) -> void:
|
||||
if checklist_overlay and checklist_overlay.has_method("update_from_state"):
|
||||
checklist_overlay.update_from_state()
|
||||
|
||||
# #511: Update debug overlay (F3 toggle, dev tool)
|
||||
if debug_overlay and debug_overlay.has_method("update_from_state"):
|
||||
debug_overlay.update_from_state()
|
||||
|
||||
# D-018 #125: Play close-range sound events via positional 2D audio
|
||||
_play_close_sound_events()
|
||||
|
||||
@@ -145,23 +151,23 @@ func _process(_delta: float) -> void:
|
||||
# D-061: Show dialogue if server sent one this tick (#434)
|
||||
_consume_dialogue()
|
||||
|
||||
# Track camera to player position every frame (D-015: locked, no panning)
|
||||
if _camera_anchored:
|
||||
camera.global_position = GameState.player_position * Constants.TILE_SIZE
|
||||
# #535: Consume overheard conversation events and responses
|
||||
_consume_conversation_events()
|
||||
_consume_conversation_ended()
|
||||
_consume_dialogue_response()
|
||||
|
||||
# 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:
|
||||
# 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:
|
||||
var target := GameState.player_position * Constants.TILE_SIZE
|
||||
if _teleport_in_progress:
|
||||
camera.global_position = target
|
||||
_teleport_in_progress = false
|
||||
else:
|
||||
camera.position_smoothing_enabled = true
|
||||
camera.reset_smoothing()
|
||||
var weight := 1.0 - exp(-Constants.CAMERA_SMOOTHING_SPEED * delta)
|
||||
camera.global_position = camera.global_position.lerp(target, weight)
|
||||
|
||||
# Send queued input to simulation
|
||||
# #507: Server-bound inputs are accumulated into _pending_record_inputs across frames.
|
||||
@@ -236,22 +242,16 @@ func _play_close_sound_events() -> void:
|
||||
# D-067: Recognition chime — fires sfx_monologue_chime when a fog entity
|
||||
# enters the cognitive delay recognition queue for the first time.
|
||||
# "The chime marks the character's attention shifting" (D-067).
|
||||
# Entities that complete recognition (leave pending_recognitions) are removed
|
||||
# from _known_recognition_ids so they can chime again if re-encountered.
|
||||
# IDs persist for the session — one chime per entity, no re-trigger on
|
||||
# fog oscillation or server re-send. Cleared on room change (teleport).
|
||||
func _play_recognition_chimes() -> void:
|
||||
var active_ids: Dictionary = {}
|
||||
for rec in GameState.pending_recognitions:
|
||||
if not rec is Dictionary or not rec.has("entity_id"):
|
||||
continue
|
||||
var eid: int = rec.entity_id
|
||||
active_ids[eid] = true
|
||||
if not _known_recognition_ids.has(eid):
|
||||
_known_recognition_ids[eid] = true
|
||||
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
|
||||
@@ -288,7 +288,12 @@ func _consume_monologue() -> void:
|
||||
return
|
||||
_last_monologue_tick = GameState.current_tick
|
||||
var mono: Dictionary = GameState.current_monologue
|
||||
monologue_display.show_monologue(mono.get("text", ""), mono.get("duration_seconds", 5.0))
|
||||
monologue_display.show_monologue(
|
||||
mono.get("text", ""),
|
||||
mono.get("duration_seconds", 5.0),
|
||||
mono.get("priority", 2),
|
||||
mono.get("is_urgent", false)
|
||||
)
|
||||
# #502: Amber flash on room reset
|
||||
var mono_id: String = mono.get("id", "")
|
||||
if mono_id.begins_with("room_reset"):
|
||||
@@ -296,6 +301,7 @@ func _consume_monologue() -> void:
|
||||
GameState.current_monologue = null
|
||||
|
||||
|
||||
|
||||
# Consume-once per tick with ID tracking: show dialogue, then clear.
|
||||
# Tick guard + is_dialogue_active check prevent re-triggering.
|
||||
func _consume_dialogue() -> void:
|
||||
@@ -309,6 +315,7 @@ func _consume_dialogue() -> void:
|
||||
_last_dialogue_tick = GameState.current_tick
|
||||
var dlg: Dictionary = GameState.current_dialogue
|
||||
_last_dialogue_npc_id = dlg.get("npc_entity_id", -1)
|
||||
_last_dialogue_npc_name = dlg.get("npc_name", "")
|
||||
dialogue_box.show_dialogue(
|
||||
dlg.get("npc_name", ""),
|
||||
dlg.get("speech", ""),
|
||||
@@ -317,6 +324,39 @@ func _consume_dialogue() -> void:
|
||||
GameState.current_dialogue = null
|
||||
|
||||
|
||||
# #535: Consume overheard NPC-NPC conversation events (D-078).
|
||||
# Each event carries pre-occluded text — render verbatim in the dialogue log.
|
||||
func _consume_conversation_events() -> void:
|
||||
if not dialogue_box:
|
||||
return
|
||||
for event in GameState.conversation_events:
|
||||
dialogue_box.append_conversation_event(event)
|
||||
GameState.conversation_events = []
|
||||
|
||||
|
||||
# #535: Handle conversation_ended events — notify dialogue box to stop tracking pairs.
|
||||
func _consume_conversation_ended() -> void:
|
||||
if not dialogue_box:
|
||||
return
|
||||
for event in GameState.conversation_ended:
|
||||
dialogue_box.on_conversation_ended(event)
|
||||
GameState.conversation_ended = []
|
||||
|
||||
|
||||
# #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:
|
||||
if GameState.dialogue_response == null or not dialogue_box:
|
||||
return
|
||||
var dr: Dictionary = GameState.dialogue_response
|
||||
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
|
||||
|
||||
|
||||
# D-061: Handle dialogue option selection → send to server
|
||||
func _on_dialogue_option_selected(response_id: String, text: String) -> void:
|
||||
SimBridge.send_input({
|
||||
@@ -331,9 +371,29 @@ func _on_dialogue_option_selected(response_id: String, text: String) -> void:
|
||||
|
||||
|
||||
# D-063: Handle confrontation beat monologue → show on monologue display (layer 7)
|
||||
# Confrontation lines are high-priority (3) and urgent — full opacity, elevated colour.
|
||||
# Tick guard deduplicates if dialogue box emits the signal multiple times in one tick.
|
||||
func _on_confrontation_monologue(text: String, duration: float) -> void:
|
||||
if monologue_display:
|
||||
monologue_display.show_monologue(text, duration)
|
||||
if not monologue_display:
|
||||
return
|
||||
if GameState.current_tick == _last_confrontation_tick:
|
||||
return
|
||||
_last_confrontation_tick = GameState.current_tick
|
||||
monologue_display.show_monologue(text, duration, 3, true)
|
||||
|
||||
|
||||
# D-061: Auto-pause on dialogue open — routed through input recording (#507, Tyre #3)
|
||||
func _on_dialogue_pause_requested() -> void:
|
||||
var input := {"action": InputMapper.Action.PAUSE, "timestamp_msec": Time.get_ticks_msec()}
|
||||
SimBridge.send_input(input)
|
||||
_pending_record_inputs.append(input)
|
||||
|
||||
|
||||
# D-061: Auto-unpause on dialogue close — routed through input recording (#507, Tyre #3)
|
||||
func _on_dialogue_unpause_requested() -> void:
|
||||
var input := {"action": InputMapper.Action.UNPAUSE, "timestamp_msec": Time.get_ticks_msec()}
|
||||
SimBridge.send_input(input)
|
||||
_pending_record_inputs.append(input)
|
||||
|
||||
|
||||
# D-064: Handle walk-away → send WalkAway{npc_id} to server
|
||||
@@ -365,11 +425,8 @@ func _detect_teleport(old_pos: Vector2, new_pos: Vector2) -> bool:
|
||||
# Clears dialogue/monologue/interaction state (server clears its side too).
|
||||
# Scoped to Gauntlet testing only — production fast-travel uses diegetic gates.
|
||||
func _teleport_transition() -> void:
|
||||
# Snap camera: disable smoothing, force re-anchor.
|
||||
# _teleport_in_progress defers smoothing re-enable by one frame so the
|
||||
# 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
|
||||
# Set teleport flag — the camera tracking block in _process() will snap
|
||||
# to the player's new position this frame (no lerp). Flag clears after snap.
|
||||
_camera_anchored = true
|
||||
_teleport_in_progress = true
|
||||
|
||||
@@ -377,6 +434,7 @@ func _teleport_transition() -> void:
|
||||
GameState.current_monologue = null
|
||||
GameState.current_dialogue = null
|
||||
GameState.dialogue_active = false
|
||||
_known_recognition_ids.clear() # D-067: reset chimes for new room
|
||||
if dialogue_box and dialogue_box.is_dialogue_active():
|
||||
dialogue_box.hide_dialogue()
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ class_name Protocol
|
||||
|
||||
## Protocol version — must match server PROTOCOL_VERSION in bridge/types.rs.
|
||||
## Reject snapshots where version != this value.
|
||||
const PROTOCOL_VERSION: int = 8
|
||||
const PROTOCOL_VERSION: int = 13
|
||||
|
||||
|
||||
# -- Decode: bytes from server → GDScript types --------------------------------
|
||||
@@ -180,6 +180,32 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant:
|
||||
"speaker_entity_id": int(raw_dr.get("speaker_entity_id", -1)),
|
||||
}
|
||||
|
||||
# v9: conversation_events (#535, D-078) — overheard NPC-to-NPC dialogue lines.
|
||||
# Each event carries pre-occluded text plus speaker/target attribution.
|
||||
var conversation_events: Array = []
|
||||
var raw_conv_events: Variant = raw.get("conversation_events")
|
||||
if raw_conv_events is Array:
|
||||
for raw_ce in raw_conv_events:
|
||||
if raw_ce is Dictionary and raw_ce.has("occluded_line"):
|
||||
conversation_events.append({
|
||||
"speaker_id": int(raw_ce.get("speaker_id", 0)),
|
||||
"target_id": int(raw_ce.get("target_id", 0)),
|
||||
"speaker_name": str(raw_ce.get("speaker_name", "")),
|
||||
"target_name": str(raw_ce.get("target_name", "")),
|
||||
"occluded_line": str(raw_ce["occluded_line"]),
|
||||
})
|
||||
|
||||
# v9: conversation_ended (#535, D-078) — pairs whose conversation ended this tick.
|
||||
var conversation_ended: Array = []
|
||||
var raw_conv_ended: Variant = raw.get("conversation_ended")
|
||||
if raw_conv_ended is Array:
|
||||
for raw_end in raw_conv_ended:
|
||||
if raw_end is Dictionary:
|
||||
conversation_ended.append({
|
||||
"speaker_id": int(raw_end.get("speaker_id", 0)),
|
||||
"target_id": int(raw_end.get("target_id", 0)),
|
||||
})
|
||||
|
||||
return {
|
||||
"tick": tick,
|
||||
"entities": entities,
|
||||
@@ -195,6 +221,8 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant:
|
||||
"current_dialogue": current_dialogue,
|
||||
"dialogue_response": dialogue_response,
|
||||
"pending_recognitions": pending_recognitions,
|
||||
"conversation_events": conversation_events,
|
||||
"conversation_ended": conversation_ended,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -13,8 +13,11 @@ extends Node2D
|
||||
# color from RelationshipState via the knowledge graph.
|
||||
|
||||
const TILE_SIZE: int = Constants.TILE_SIZE
|
||||
const ENTITY_SIZE: int = 24
|
||||
const ENTITY_OFFSET: float = (TILE_SIZE - ENTITY_SIZE) / 2.0 # center within tile
|
||||
# D-044: 24x32 entity footprint within 32x32 visual tile (64x64 source scaled to 32px runtime)
|
||||
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.
|
||||
# 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:
|
||||
var entity_node = ColorRect.new()
|
||||
entity_node.name = "Entity_" + str(entity_id)
|
||||
entity_node.size = Vector2(ENTITY_SIZE, ENTITY_SIZE)
|
||||
entity_node.pivot_offset = Vector2(ENTITY_SIZE / 2.0, ENTITY_SIZE / 2.0)
|
||||
entity_node.size = Vector2(ENTITY_WIDTH, ENTITY_HEIGHT)
|
||||
entity_node.pivot_offset = Vector2(ENTITY_WIDTH / 2.0, ENTITY_HEIGHT / 2.0)
|
||||
|
||||
# D-033 color by relationship (#521)
|
||||
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)
|
||||
if entity_data.has("x") and entity_data.has("y"):
|
||||
var target := Vector2(
|
||||
floorf(entity_data.x) * TILE_SIZE + ENTITY_OFFSET,
|
||||
floorf(entity_data.y) * TILE_SIZE + ENTITY_OFFSET
|
||||
floorf(entity_data.x) * TILE_SIZE + ENTITY_OFFSET_X,
|
||||
floorf(entity_data.y) * TILE_SIZE + ENTITY_OFFSET_Y
|
||||
)
|
||||
entity_node.position = 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.
|
||||
if entity_data.has("x") and entity_data.has("y"):
|
||||
_entity_targets[entity_id] = Vector2(
|
||||
floorf(entity_data.x) * TILE_SIZE + ENTITY_OFFSET,
|
||||
floorf(entity_data.y) * TILE_SIZE + ENTITY_OFFSET
|
||||
floorf(entity_data.x) * TILE_SIZE + ENTITY_OFFSET_X,
|
||||
floorf(entity_data.y) * TILE_SIZE + ENTITY_OFFSET_Y
|
||||
)
|
||||
|
||||
# #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
|
||||
# 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)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://cb10ir0idsr0g
|
||||
@@ -12,6 +12,7 @@ extends TileMapLayer
|
||||
# (4,0) = reset_plate — amber (#502)
|
||||
|
||||
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 }
|
||||
|
||||
@@ -64,6 +65,10 @@ func _setup_tileset() -> void:
|
||||
|
||||
# Update tiles from snapshot data
|
||||
# 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:
|
||||
if not _initialized:
|
||||
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"):
|
||||
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
|
||||
if not TILE_TYPE_MAP.has(tile_type_str):
|
||||
push_warning("TileRenderer: unknown tile type '%s' at (%d, %d)" % [
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
extends Control
|
||||
# #511: F3 debug overlay — real-time game state display for dev use.
|
||||
|
||||
const HEADER_COLOR := Color("#e8c547")
|
||||
const LABEL_COLOR := Color("#8890a0")
|
||||
const VALUE_COLOR := Color("#c8d0e0")
|
||||
const BG_COLOR := Color(0.08, 0.08, 0.12, 0.85)
|
||||
const FONT_SIZE := 12
|
||||
const LINE_HEIGHT := 16
|
||||
const PADDING := Vector2(10, 8)
|
||||
const COL_GAP := 16 # gap between left and right columns
|
||||
|
||||
var _cached_font: Font = null
|
||||
|
||||
func _ready() -> void:
|
||||
visible = false
|
||||
_cached_font = ThemeDB.fallback_font
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if event.is_action_pressed("debug_overlay"):
|
||||
visible = not visible
|
||||
if visible:
|
||||
queue_redraw()
|
||||
|
||||
func update_from_state() -> void:
|
||||
if not visible:
|
||||
return
|
||||
queue_redraw()
|
||||
|
||||
func _draw() -> void:
|
||||
if not visible:
|
||||
return
|
||||
var font: Font = _cached_font if _cached_font else ThemeDB.fallback_font
|
||||
|
||||
# Build lines as [label, value, label, value] pairs (two columns)
|
||||
var left_lines: Array = []
|
||||
var right_lines: Array = []
|
||||
|
||||
left_lines.append(["tick", str(GameState.current_tick)])
|
||||
right_lines.append(["fps", str(Engine.get_frames_per_second())])
|
||||
|
||||
var pos := GameState.player_position
|
||||
left_lines.append(["pos", "(%d, %d)" % [int(pos.x), int(pos.y)]])
|
||||
right_lines.append(["facing", GameState.player_facing])
|
||||
|
||||
left_lines.append(["stance", GameState.player_stance])
|
||||
right_lines.append(["zone", GameState.current_zone_id if GameState.current_zone_id != "" else "-"])
|
||||
|
||||
left_lines.append(["entities", str(GameState.visible_entities.size())])
|
||||
right_lines.append(["tiles", str(GameState.visible_tiles.size())])
|
||||
|
||||
left_lines.append(["interactions", str(GameState.nearby_interactions.size())])
|
||||
right_lines.append(["recognitions", str(GameState.pending_recognitions.size())])
|
||||
|
||||
var mono_status := "active" if GameState.current_monologue != null else "idle"
|
||||
var dlg_status := "active" if GameState.dialogue_active else "idle"
|
||||
left_lines.append(["monologue", mono_status])
|
||||
right_lines.append(["dialogue", dlg_status])
|
||||
|
||||
left_lines.append(["stationary", str(GameState.stationary_ticks)])
|
||||
right_lines.append(["insert", "ON" if GameState.insert_active else "OFF"])
|
||||
|
||||
var gt := GameState.game_time
|
||||
var time_str := "%s d%s" % [gt.get("day_phase", "-"), str(gt.get("day", "-"))] if gt.size() > 0 else "-"
|
||||
var rate_str: String = gt.get("tick_rate", "-") if gt.size() > 0 else "-"
|
||||
left_lines.append(["time", time_str])
|
||||
right_lines.append(["tick_rate", rate_str])
|
||||
|
||||
var mode_str := "test" if SimBridge.test_mode else "live"
|
||||
var gauntlet_str := "ON" if GameState.gauntlet_mode else "OFF"
|
||||
left_lines.append(["mode", mode_str])
|
||||
right_lines.append(["gauntlet", gauntlet_str])
|
||||
|
||||
# Measure column widths
|
||||
var left_label_w: float = 0.0
|
||||
var left_value_w: float = 0.0
|
||||
var right_label_w: float = 0.0
|
||||
var right_value_w: float = 0.0
|
||||
|
||||
for line in left_lines:
|
||||
left_label_w = max(left_label_w, font.get_string_size(line[0] + ": ", HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE).x)
|
||||
left_value_w = max(left_value_w, font.get_string_size(line[1], HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE).x)
|
||||
for line in right_lines:
|
||||
right_label_w = max(right_label_w, font.get_string_size(line[0] + ": ", HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE).x)
|
||||
right_value_w = max(right_value_w, font.get_string_size(line[1], HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE).x)
|
||||
|
||||
var header_text := "F3 DEBUG"
|
||||
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 box_w: float = max(header_w, content_w) + PADDING.x * 2
|
||||
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
|
||||
|
||||
# Background
|
||||
draw_rect(Rect2(Vector2.ZERO, Vector2(box_w, box_h)), BG_COLOR)
|
||||
|
||||
# Header
|
||||
var y: float = PADDING.y + FONT_SIZE
|
||||
draw_string(font, Vector2(PADDING.x, y), header_text, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE + 1, HEADER_COLOR)
|
||||
y += LINE_HEIGHT
|
||||
|
||||
# Data lines (two columns)
|
||||
var right_x: float = PADDING.x + left_label_w + left_value_w + COL_GAP
|
||||
for i in range(line_count):
|
||||
if i < left_lines.size():
|
||||
var lbl: String = left_lines[i][0] + ": "
|
||||
var val: String = left_lines[i][1]
|
||||
draw_string(font, Vector2(PADDING.x, y), lbl, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE, LABEL_COLOR)
|
||||
draw_string(font, Vector2(PADDING.x + left_label_w, y), val, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE, VALUE_COLOR)
|
||||
if i < right_lines.size():
|
||||
var lbl: String = right_lines[i][0] + ": "
|
||||
var val: String = right_lines[i][1]
|
||||
draw_string(font, Vector2(right_x, y), lbl, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE, LABEL_COLOR)
|
||||
draw_string(font, Vector2(right_x + right_label_w, y), val, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE, VALUE_COLOR)
|
||||
y += LINE_HEIGHT
|
||||
@@ -0,0 +1 @@
|
||||
uid://c55ow14m345vv
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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()
|
||||
|
||||
|
||||
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 ---
|
||||
|
||||
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)
|
||||
|
||||
|
||||
func test_camera_smoothing_reenabled_after_process() -> void:
|
||||
# After the first anchored frame, smoothing should be back on for gameplay.
|
||||
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_true()
|
||||
|
||||
|
||||
func test_camera_follows_player_movement() -> void:
|
||||
func test_camera_lerps_toward_player_movement() -> void:
|
||||
# #117: With manual lerp, camera moves TOWARD player position (not snapping).
|
||||
# After one 16ms frame the camera should be partway between old and new position.
|
||||
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
|
||||
var initial_pos := camera.global_position # anchored at (320, 320)
|
||||
|
||||
# Move player north via SimBridge test mode
|
||||
SimBridge._test_input_queue.append("MoveNorth")
|
||||
_instance._process(0.016)
|
||||
|
||||
# Camera should have moved with the player
|
||||
assert_that(camera.global_position.y < initial_pos.y).is_true()
|
||||
assert_that(camera.global_position).is_equal(
|
||||
GameState.player_position * Constants.TILE_SIZE)
|
||||
var new_target := GameState.player_position * Constants.TILE_SIZE # (320, 288)
|
||||
# Camera should have moved north (lower y) but NOT reached the target yet
|
||||
assert_that(camera.global_position.y).is_less(initial_pos.y)
|
||||
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)
|
||||
var node = renderer.entity_nodes[10]
|
||||
var expected := Vector2(
|
||||
floorf(8.0) * Constants.TILE_SIZE + EntityRenderer.ENTITY_OFFSET,
|
||||
floorf(6.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_Y
|
||||
)
|
||||
assert_that(node.position).override_failure_message(
|
||||
"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)
|
||||
var after_pos: Vector2 = node.position
|
||||
var target := Vector2(
|
||||
floorf(6.0) * Constants.TILE_SIZE + EntityRenderer.ENTITY_OFFSET,
|
||||
floorf(5.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_Y
|
||||
)
|
||||
# Position should have moved toward target (x increased)
|
||||
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}}]
|
||||
renderer.update_entities(entity_moved)
|
||||
var target := Vector2(
|
||||
floorf(7.0) * Constants.TILE_SIZE + EntityRenderer.ENTITY_OFFSET,
|
||||
floorf(5.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_Y
|
||||
)
|
||||
# Simulate 0.3s at 60fps (18 frames × 0.016s ≈ 0.288s)
|
||||
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
|
||||
# Reset position for large delta test
|
||||
small_node.position = Vector2(
|
||||
floorf(5.0) * Constants.TILE_SIZE + EntityRenderer.ENTITY_OFFSET,
|
||||
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_Y
|
||||
)
|
||||
# Large delta step
|
||||
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,490 @@
|
||||
## #122: Monologue display — client tests (Sprint 14)
|
||||
## Covers queue management, priority logic, stagger, colour palette, BBCode output,
|
||||
## no-overwrite contract (P0 #477), and per-slot fade lifecycle.
|
||||
##
|
||||
## API per Tyre architecture review:
|
||||
## show_monologue(text, duration, priority=2, is_urgent=false)
|
||||
## GameState.lattice_profile selects colour palette
|
||||
class_name TestMonologueDisplay
|
||||
extends GdUnitTestSuite
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _make_display() -> Node:
|
||||
if not ResourceLoader.exists("res://ui/monologue_display.tscn"):
|
||||
push_warning("TestMonologueDisplay: scene not found — tests skipped")
|
||||
return null
|
||||
var node = load("res://ui/monologue_display.tscn").instantiate()
|
||||
add_child(node)
|
||||
return node
|
||||
|
||||
|
||||
func _label_text(d: Node) -> String:
|
||||
if d._visible.is_empty():
|
||||
return ""
|
||||
var slot_node: Node = d._visible[0].node
|
||||
return (slot_node.get_child(0) as RichTextLabel).text
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lifecycle — ensure clean GameState between every test
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func before_test() -> void:
|
||||
## Reset GameState fields touched by this suite so tests don't bleed into each other.
|
||||
## lattice_profile: tests that care about colour set it explicitly — default to baseline.
|
||||
## current_monologue: GameState integration tests need null as start state.
|
||||
GameState.current_monologue = null
|
||||
GameState.lattice_profile = "lattice_baseline"
|
||||
|
||||
func after_test() -> void:
|
||||
GameState.current_monologue = null
|
||||
GameState.lattice_profile = "lattice_baseline"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Initial state
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func test_nothing_visible_on_init() -> void:
|
||||
var d = _make_display()
|
||||
if d == null: return
|
||||
assert_int(d._visible.size()).is_equal(0)
|
||||
assert_int(d._queue.size()).is_equal(0)
|
||||
d.queue_free()
|
||||
|
||||
|
||||
func test_stagger_timer_zero_on_init() -> void:
|
||||
var d = _make_display()
|
||||
if d == null: return
|
||||
assert_float(d._next_fade_in_msec).is_equal(0.0)
|
||||
d.queue_free()
|
||||
|
||||
|
||||
func test_show_monologue_with_empty_text_does_not_set_displaying() -> void:
|
||||
# Empty text must be ignored — no visible slot created, no queue entry.
|
||||
# Prevents ghost display nodes and stagger timer contamination.
|
||||
var d = _make_display()
|
||||
if d == null: return
|
||||
d.show_monologue("", 5.0)
|
||||
assert_int(d._visible.size()).is_equal(0)
|
||||
assert_int(d._queue.size()).is_equal(0)
|
||||
d.queue_free()
|
||||
|
||||
|
||||
func test_empty_text_does_not_advance_stagger_timer() -> void:
|
||||
var d = _make_display()
|
||||
if d == null: return
|
||||
d.show_monologue("", 5.0)
|
||||
assert_float(d._next_fade_in_msec).is_equal(0.0)
|
||||
d.queue_free()
|
||||
|
||||
|
||||
func test_empty_text_when_slots_full_does_not_enqueue() -> void:
|
||||
var d = _make_display()
|
||||
if d == null: return
|
||||
d._next_fade_in_msec = 0.0; d.show_monologue("A", 10.0)
|
||||
d._next_fade_in_msec = 0.0; d.show_monologue("B", 10.0)
|
||||
d._next_fade_in_msec = 0.0; d.show_monologue("C", 10.0)
|
||||
d.show_monologue("", 5.0)
|
||||
assert_int(d._queue.size()).is_equal(0)
|
||||
d.queue_free()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Single-line display
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func test_single_line_goes_to_visible() -> void:
|
||||
var d = _make_display()
|
||||
if d == null: return
|
||||
d.show_monologue("One.", 5.0)
|
||||
assert_int(d._visible.size()).is_equal(1)
|
||||
assert_int(d._queue.size()).is_equal(0)
|
||||
d.queue_free()
|
||||
|
||||
|
||||
func test_show_monologue_sets_stagger_timer() -> void:
|
||||
var d = _make_display()
|
||||
if d == null: return
|
||||
var before := float(Time.get_ticks_msec())
|
||||
d.show_monologue("Stagger.", 5.0)
|
||||
assert_float(d._next_fade_in_msec).is_greater(before)
|
||||
d.queue_free()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# MAX_VISIBLE = 3 simultaneous lines
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func test_three_lines_all_visible() -> void:
|
||||
var d = _make_display()
|
||||
if d == null: return
|
||||
d._next_fade_in_msec = 0.0; d.show_monologue("A", 10.0)
|
||||
d._next_fade_in_msec = 0.0; d.show_monologue("B", 10.0)
|
||||
d._next_fade_in_msec = 0.0; d.show_monologue("C", 10.0)
|
||||
assert_int(d._visible.size()).is_equal(3)
|
||||
assert_int(d._queue.size()).is_equal(0)
|
||||
d.queue_free()
|
||||
|
||||
|
||||
func test_fourth_line_queues_when_slots_full() -> void:
|
||||
var d = _make_display()
|
||||
if d == null: return
|
||||
d._next_fade_in_msec = 0.0; d.show_monologue("A", 10.0)
|
||||
d._next_fade_in_msec = 0.0; d.show_monologue("B", 10.0)
|
||||
d._next_fade_in_msec = 0.0; d.show_monologue("C", 10.0)
|
||||
d.show_monologue("D", 5.0)
|
||||
assert_int(d._visible.size()).is_equal(3)
|
||||
assert_int(d._queue.size()).is_equal(1)
|
||||
d.queue_free()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# No-overwrite contract (P0, #477)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func test_new_line_does_not_replace_first_visible_line() -> void:
|
||||
var d = _make_display()
|
||||
if d == null: return
|
||||
d.show_monologue("First line.", 10.0)
|
||||
var first_text := _label_text(d)
|
||||
# Force stagger active; second call must queue, not display
|
||||
d._next_fade_in_msec = float(Time.get_ticks_msec()) + 10000.0
|
||||
d.show_monologue("Second line.", 5.0)
|
||||
assert_that(_label_text(d)).is_equal(first_text)
|
||||
d.queue_free()
|
||||
|
||||
|
||||
func test_two_visible_lines_coexist_without_overwriting() -> void:
|
||||
var d = _make_display()
|
||||
if d == null: return
|
||||
d._next_fade_in_msec = 0.0; d.show_monologue("Alpha", 10.0)
|
||||
d._next_fade_in_msec = 0.0; d.show_monologue("Beta", 10.0)
|
||||
assert_int(d._visible.size()).is_equal(2)
|
||||
d.queue_free()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Priority queue
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func test_queue_sorted_highest_priority_first() -> void:
|
||||
var d = _make_display()
|
||||
if d == null: return
|
||||
d._next_fade_in_msec = 0.0; d.show_monologue("A", 10.0)
|
||||
d._next_fade_in_msec = 0.0; d.show_monologue("B", 10.0)
|
||||
d._next_fade_in_msec = 0.0; d.show_monologue("C", 10.0)
|
||||
d.show_monologue("low", 5.0, 1)
|
||||
d.show_monologue("high", 5.0, 4)
|
||||
d.show_monologue("normal", 5.0, 2)
|
||||
assert_int(d._queue[0].priority).is_equal(4)
|
||||
d.queue_free()
|
||||
|
||||
|
||||
func test_queue_drop_replaces_lowest_when_full() -> void:
|
||||
var d = _make_display()
|
||||
if d == null: return
|
||||
d._next_fade_in_msec = 0.0; d.show_monologue("A", 10.0)
|
||||
d._next_fade_in_msec = 0.0; d.show_monologue("B", 10.0)
|
||||
d._next_fade_in_msec = 0.0; d.show_monologue("C", 10.0)
|
||||
for i in range(d.MAX_QUEUE):
|
||||
d.show_monologue("low_%d" % i, 5.0, 1)
|
||||
d.show_monologue("critical!", 5.0, 9)
|
||||
assert_int(d._queue.size()).is_equal(d.MAX_QUEUE)
|
||||
var has_critical := false
|
||||
for e in d._queue:
|
||||
if e.priority == 9:
|
||||
has_critical = true
|
||||
assert_that(has_critical).is_true()
|
||||
d.queue_free()
|
||||
|
||||
|
||||
func test_queue_ignores_lower_priority_when_full() -> void:
|
||||
var d = _make_display()
|
||||
if d == null: return
|
||||
d._next_fade_in_msec = 0.0; d.show_monologue("A", 10.0)
|
||||
d._next_fade_in_msec = 0.0; d.show_monologue("B", 10.0)
|
||||
d._next_fade_in_msec = 0.0; d.show_monologue("C", 10.0)
|
||||
for i in range(d.MAX_QUEUE):
|
||||
d.show_monologue("hi_%d" % i, 5.0, 5)
|
||||
d.show_monologue("noise", 1.0, 1)
|
||||
assert_int(d._queue.size()).is_equal(d.MAX_QUEUE)
|
||||
for e in d._queue:
|
||||
assert_int(e.priority).is_equal(5)
|
||||
d.queue_free()
|
||||
|
||||
|
||||
func test_queue_never_exceeds_max_depth() -> void:
|
||||
var d = _make_display()
|
||||
if d == null: return
|
||||
d._next_fade_in_msec = 0.0; d.show_monologue("A", 10.0)
|
||||
d._next_fade_in_msec = 0.0; d.show_monologue("B", 10.0)
|
||||
d._next_fade_in_msec = 0.0; d.show_monologue("C", 10.0)
|
||||
for i in range(d.MAX_QUEUE + 20):
|
||||
d.show_monologue("flood_%d" % i, 1.0, 2)
|
||||
assert_int(d._queue.size()).is_less_or_equal(d.MAX_QUEUE)
|
||||
d.queue_free()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Expire and drain
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func test_expired_slot_removed_from_visible() -> void:
|
||||
var d = _make_display()
|
||||
if d == null: return
|
||||
d.show_monologue("Expires.", 1.0)
|
||||
d._visible[0].expire_timer = -0.1
|
||||
d._process(0.0)
|
||||
assert_int(d._visible.size()).is_equal(0)
|
||||
d.queue_free()
|
||||
|
||||
|
||||
func test_queue_drains_when_slot_opens() -> void:
|
||||
var d = _make_display()
|
||||
if d == null: return
|
||||
d._next_fade_in_msec = 0.0; d.show_monologue("A", 10.0)
|
||||
d._next_fade_in_msec = 0.0; d.show_monologue("B", 10.0)
|
||||
d._next_fade_in_msec = 0.0; d.show_monologue("C", 10.0)
|
||||
d.show_monologue("Queued.", 5.0)
|
||||
d._visible[0].expire_timer = -0.1
|
||||
d._next_fade_in_msec = 0.0 # stagger elapsed
|
||||
d._process(0.0)
|
||||
assert_int(d._queue.size()).is_equal(0)
|
||||
assert_int(d._visible.size()).is_equal(3)
|
||||
d.queue_free()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stagger
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func test_second_call_within_stagger_period_queues() -> void:
|
||||
var d = _make_display()
|
||||
if d == null: return
|
||||
d.show_monologue("First.", 5.0)
|
||||
# _next_fade_in_msec is now ~150ms in the future
|
||||
d.show_monologue("Second.", 5.0)
|
||||
assert_int(d._visible.size()).is_equal(1)
|
||||
assert_int(d._queue.size()).is_equal(1)
|
||||
d.queue_free()
|
||||
|
||||
|
||||
func test_stagger_elapsed_allows_second_visible() -> void:
|
||||
var d = _make_display()
|
||||
if d == null: return
|
||||
d.show_monologue("First.", 5.0)
|
||||
d._next_fade_in_msec = 0.0 # manually expire stagger
|
||||
d.show_monologue("Second.", 5.0)
|
||||
assert_int(d._visible.size()).is_equal(2)
|
||||
assert_int(d._queue.size()).is_equal(0)
|
||||
d.queue_free()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# BBCode output
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func test_text_wrapped_in_italic_bbcode() -> void:
|
||||
var d = _make_display()
|
||||
if d == null: return
|
||||
d.show_monologue("Italic line.", 5.0)
|
||||
var txt := _label_text(d)
|
||||
assert_that(txt).contains("[i]")
|
||||
assert_that(txt).contains("[/i]")
|
||||
assert_that(txt).contains("Italic line.")
|
||||
d.queue_free()
|
||||
|
||||
|
||||
func test_text_has_color_bbcode() -> void:
|
||||
var d = _make_display()
|
||||
if d == null: return
|
||||
d.show_monologue("Coloured.", 5.0)
|
||||
var txt := _label_text(d)
|
||||
assert_that(txt).contains("[color=#")
|
||||
assert_that(txt).contains("[/color]")
|
||||
d.queue_free()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lattice colour palette
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func test_augmented_colour_differs_from_baseline() -> void:
|
||||
var d = _make_display()
|
||||
if d == null: return
|
||||
|
||||
GameState.lattice_profile = "lattice_augmented"
|
||||
d.show_monologue("Detective.", 5.0)
|
||||
var aug_txt := _label_text(d)
|
||||
d._visible[0].expire_timer = -0.1; d._process(0.0)
|
||||
d._next_fade_in_msec = 0.0
|
||||
|
||||
GameState.lattice_profile = "lattice_baseline"
|
||||
d.show_monologue("Smuggler.", 5.0)
|
||||
var base_txt := _label_text(d)
|
||||
|
||||
assert_that(aug_txt).is_not_equal(base_txt)
|
||||
GameState.lattice_profile = "lattice_baseline"
|
||||
d.queue_free()
|
||||
|
||||
|
||||
func test_urgent_colour_differs_from_standard() -> void:
|
||||
var d = _make_display()
|
||||
if d == null: return
|
||||
|
||||
GameState.lattice_profile = "lattice_baseline"
|
||||
d.show_monologue("Normal.", 5.0, 2, false)
|
||||
var std_txt := _label_text(d)
|
||||
d._visible[0].expire_timer = -0.1; d._process(0.0)
|
||||
d._next_fade_in_msec = 0.0
|
||||
|
||||
d.show_monologue("Urgent!", 5.0, 3, true)
|
||||
var urg_txt := _label_text(d)
|
||||
|
||||
assert_that(std_txt).is_not_equal(urg_txt)
|
||||
d.queue_free()
|
||||
|
||||
|
||||
func test_unknown_profile_falls_back_without_crash() -> void:
|
||||
var d = _make_display()
|
||||
if d == null: return
|
||||
GameState.lattice_profile = "lattice_hypothetical_tier_x"
|
||||
d.show_monologue("Future proof.", 5.0)
|
||||
var txt := _label_text(d)
|
||||
assert_that(txt).contains("[color=#") # fallback colour applied, no crash
|
||||
GameState.lattice_profile = "lattice_baseline"
|
||||
d.queue_free()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Slot lifecycle
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func test_visible_slot_has_tween() -> void:
|
||||
var d = _make_display()
|
||||
if d == null: return
|
||||
d.show_monologue("Has tween.", 5.0)
|
||||
assert_that(d._visible[0].tween).is_not_null()
|
||||
d.queue_free()
|
||||
|
||||
|
||||
func test_visible_slot_stores_priority() -> void:
|
||||
var d = _make_display()
|
||||
if d == null: return
|
||||
d.show_monologue("Priority 7.", 5.0, 7)
|
||||
assert_int(d._visible[0].priority).is_equal(7)
|
||||
d.queue_free()
|
||||
|
||||
|
||||
func test_expire_timer_decrements_in_process() -> void:
|
||||
var d = _make_display()
|
||||
if d == null: return
|
||||
d.show_monologue("Timer.", 10.0)
|
||||
d._process(1.5)
|
||||
assert_float(d._visible[0].expire_timer).is_less(10.0)
|
||||
d.queue_free()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GameState integration — current_monologue field (v5, #414)
|
||||
# These tests do not instantiate the scene — they verify apply_snapshot()
|
||||
# correctly populates and clears current_monologue so the display layer
|
||||
# receives valid data (or null) every tick.
|
||||
#
|
||||
# Joint test plan (sprint-14/joint.md §Test Plan Alignment):
|
||||
# "Verify current_monologue: None produces no display (no ghost text from previous tick)"
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func test_gamestate_monologue_null_when_snapshot_omits_field() -> void:
|
||||
## D-016: Snapshot without current_monologue must clear the field.
|
||||
## Prevents a stale line from a previous tick persisting as ghost text.
|
||||
GameState.current_monologue = {"id": "stale", "text": "Old.", "duration_seconds": 3.0}
|
||||
GameState.apply_snapshot({"tick": 2})
|
||||
assert_that(GameState.current_monologue).is_null()
|
||||
|
||||
|
||||
func test_gamestate_monologue_set_from_snapshot() -> void:
|
||||
## apply_snapshot populates current_monologue when the field is a dict.
|
||||
GameState.apply_snapshot({
|
||||
"tick": 1,
|
||||
"current_monologue": {"id": "m001", "text": "A thought.", "duration_seconds": 5.0},
|
||||
})
|
||||
assert_that(GameState.current_monologue).is_not_null()
|
||||
assert_that(GameState.current_monologue.get("text")).is_equal("A thought.")
|
||||
|
||||
|
||||
func test_gamestate_monologue_null_when_field_is_non_dict() -> void:
|
||||
## Defensive: a non-dictionary value from the server must be rejected (null).
|
||||
GameState.apply_snapshot({"tick": 1, "current_monologue": "not-a-dict"})
|
||||
assert_that(GameState.current_monologue).is_null()
|
||||
|
||||
|
||||
func test_gamestate_monologue_duration_seconds_survives_round_trip() -> void:
|
||||
## duration_seconds feeds show_monologue()'s duration param — must not be lost.
|
||||
GameState.apply_snapshot({
|
||||
"tick": 1,
|
||||
"current_monologue": {"id": "m1", "text": "Test.", "duration_seconds": 7.5},
|
||||
})
|
||||
var dur: float = float(GameState.current_monologue.get("duration_seconds"))
|
||||
assert_that(dur).is_equal_approx(7.5, 0.001)
|
||||
|
||||
|
||||
func test_gamestate_monologue_id_survives_round_trip() -> void:
|
||||
## The id field is used by SimBridge carry-forward deduplication (#477).
|
||||
GameState.apply_snapshot({
|
||||
"tick": 1,
|
||||
"current_monologue": {"id": "enter_cargo_bay_001", "text": "Hmm.", "duration_seconds": 3.0},
|
||||
})
|
||||
assert_that(GameState.current_monologue.get("id")).is_equal("enter_cargo_bay_001")
|
||||
|
||||
|
||||
func test_gamestate_monologue_replaced_by_next_snapshot() -> void:
|
||||
## Each snapshot with a monologue replaces the previous value — no accumulation.
|
||||
GameState.apply_snapshot({
|
||||
"tick": 1,
|
||||
"current_monologue": {"id": "first", "text": "First.", "duration_seconds": 3.0},
|
||||
})
|
||||
GameState.apply_snapshot({
|
||||
"tick": 2,
|
||||
"current_monologue": {"id": "second", "text": "Second.", "duration_seconds": 3.0},
|
||||
})
|
||||
assert_that(GameState.current_monologue.get("id")).is_equal("second")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# D-049 Z-layer / canvas scope compliance
|
||||
# MonologueDisplay must be parented to UILayer (CanvasLayer, layer=20).
|
||||
# Two tests: (1) constant sanity, (2) scene tree structural verification.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func test_canvas_ui_constant_is_20() -> void:
|
||||
## D-049: CANVAS_UI = 20 is the agreed HUD layer for monologue display.
|
||||
## Sanity check — constant must not drift from the spec.
|
||||
assert_that(Constants.CANVAS_UI).is_equal(20)
|
||||
|
||||
|
||||
func test_monologue_display_parented_to_canvas_layer_20_in_main_scene() -> void:
|
||||
## D-049: Structural verification — MonologueDisplay must be a direct child of
|
||||
## UILayer (CanvasLayer, layer=20) in the live scene tree, not the world layer.
|
||||
## Catches regressions where the node gets accidentally moved to InsertOverlay
|
||||
## (layer=10) or ModalLayer (layer=30), or dropped into the world z-stack.
|
||||
##
|
||||
## Scene path verified: Game/UILayer/MonologueDisplay (main.tscn line 141).
|
||||
if not ResourceLoader.exists("res://scenes/main.tscn"):
|
||||
push_warning("TestMonologueDisplay: main.tscn not found — D-049 scene tree test skipped")
|
||||
return
|
||||
var scene: Node = load("res://scenes/main.tscn").instantiate()
|
||||
auto_free(scene)
|
||||
add_child(scene)
|
||||
|
||||
var mono: Node = scene.get_node_or_null("UILayer/MonologueDisplay")
|
||||
assert_that(mono != null).is_true()
|
||||
|
||||
var parent: Node = mono.get_parent()
|
||||
assert_that(parent is CanvasLayer).is_true()
|
||||
assert_that((parent as CanvasLayer).layer).is_equal(Constants.CANVAS_UI)
|
||||
@@ -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
|
||||
visible = false
|
||||
if _line_edit:
|
||||
_line_edit.release_focus()
|
||||
_line_edit.queue_free()
|
||||
_line_edit = null
|
||||
|
||||
@@ -344,7 +345,7 @@ func _save_report(description: String) -> void:
|
||||
else:
|
||||
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])
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://c5xgtnkp0butl
|
||||
+485
-111
@@ -1,28 +1,59 @@
|
||||
extends Control
|
||||
|
||||
# Dialogue box — D-061: bottom screen, max 20% height, no portraits.
|
||||
# #535: Unified conversation log — player dialogue and overheard NPC-NPC conversations
|
||||
# flow chronologically. Oldest at top, scrolls up. Options at bottom.
|
||||
#
|
||||
# InsertOverlay (CanvasLayer 10, z-layer 6) — diegetic, insert-styled.
|
||||
# NPC speech top, player response options below, left-aligned.
|
||||
# Max 3 visible options. No close button — walk-away (WASD) or option select only.
|
||||
# Auto-pause in single-player when dialogue is open (D-061).
|
||||
# D-063: Confrontation options render italic, trigger monologue beat before send.
|
||||
# D-064: Walk-away via WASD dismisses active conversation (log entries persist).
|
||||
# dialogue_active held until fade completes (D-064 auto-pause spec).
|
||||
# D-078: Overheard NPC-NPC lines prefixed with ┃ glyph + desaturated colours.
|
||||
|
||||
signal option_selected(response_id: String, text: String)
|
||||
signal dialogue_dismissed # Walk-away or conversation end
|
||||
signal confrontation_monologue(text: String, duration: float) # D-063: beat monologue
|
||||
signal pause_requested # D-061: auto-pause — main.gd routes through input recording (#507)
|
||||
signal unpause_requested # D-061: auto-unpause
|
||||
|
||||
@onready var panel: PanelContainer = $PanelContainer
|
||||
@onready var npc_speech: RichTextLabel = $PanelContainer/MarginContainer/VBoxContainer/NpcSpeech
|
||||
@onready var dialogue_log: RichTextLabel = $PanelContainer/MarginContainer/VBoxContainer/DialogueLog
|
||||
@onready var options_container: VBoxContainer = $PanelContainer/MarginContainer/VBoxContainer/OptionsContainer
|
||||
|
||||
var _is_showing: bool = false
|
||||
# -- Log state --
|
||||
# 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).
|
||||
var _log_entries: Array[Dictionary] = []
|
||||
var _in_player_conversation: bool = false
|
||||
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 --
|
||||
var _option_controls: Array[Control] = []
|
||||
var _option_response_ids: Array[String] = []
|
||||
var _option_texts: Array[String] = []
|
||||
var _option_is_confrontation: Array[bool] = []
|
||||
var _npc_name: String = ""
|
||||
|
||||
# -- UI state --
|
||||
var _active_tween: Tween = null
|
||||
var _beat_tween: Tween = null # D-063: confrontation beat delay
|
||||
var _option_controls: Array[Control] = []
|
||||
var _option_response_ids: Array[String] = [] # response_id per option, same index
|
||||
var _option_texts: Array[String] = [] # raw display text per option
|
||||
var _option_is_confrontation: Array[bool] = [] # confrontation flag per option
|
||||
var _npc_name: String = ""
|
||||
|
||||
# -- Theme (loaded from data/dialogue-theme.yaml) --
|
||||
var _player_color: Color = Color("#e0e8ff")
|
||||
var _npc_colors: Array[Color] = []
|
||||
var _arrow_color: Color = Color("#8890a0")
|
||||
var _speech_color: Color = Color("#c8d0e0")
|
||||
var _passive_opacity: float = 0.9
|
||||
var _entry_lifetime: float = 45.0
|
||||
var _entry_fade: float = 5.0
|
||||
|
||||
const THEME_PATH: String = "res://data/dialogue-theme.yaml"
|
||||
|
||||
const FADE_IN: float = 0.2
|
||||
const FADE_OUT: float = 0.3 # D-064: 300ms fade on walk-away
|
||||
@@ -32,6 +63,10 @@ const MAX_WIDTH_PX: float = Constants.DIALOGUE_MAX_WIDTH # D-076: 640px (OQ-29)
|
||||
const CONFRONTATION_BEAT_DURATION: float = 1.5 # D-063: pause before sending
|
||||
const CONFRONTATION_DIM_ALPHA: float = 0.7 # D-063: dialogue box dims during beat
|
||||
const CONFRONTATION_MONOLOGUE_KEY: String = "dialogue.confrontation_beat"
|
||||
const PLAYER_NAME: String = "You"
|
||||
const PASSIVE_GLYPH: String = "\u2503 " # ┃ prefix for overheard lines (Araminta review)
|
||||
const PASSIVE_DESATURATION: float = 0.4 # Desaturate passive name colours by this factor
|
||||
const MIN_CONTRAST_LUMINANCE: float = 0.25 # Floor for name colour brightness against dark BG
|
||||
|
||||
# D-064: movement actions that trigger walk-away
|
||||
const _WALK_AWAY_ACTIONS: Array[StringName] = [
|
||||
@@ -41,59 +76,275 @@ const _WALK_AWAY_ACTIONS: Array[StringName] = [
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
panel.modulate.a = 0.0
|
||||
visible = false
|
||||
_is_showing = false
|
||||
# Panel is always visible as a permanent insert UI element (D-061).
|
||||
# Content fades in/out but the panel frame stays on screen.
|
||||
visible = true
|
||||
panel.modulate.a = 1.0
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_load_theme()
|
||||
_update_layout()
|
||||
get_viewport().size_changed.connect(_update_layout)
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
_expire_entries()
|
||||
if _log_dirty:
|
||||
_log_dirty = false
|
||||
_rebuild_log()
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if not _is_showing:
|
||||
if not _in_player_conversation:
|
||||
return
|
||||
|
||||
# D-064: WASD during dialogue → walk-away, 300ms fade
|
||||
# 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
|
||||
if event is InputEventKey and event.pressed:
|
||||
for action in _WALK_AWAY_ACTIONS:
|
||||
if event.is_action_pressed(action):
|
||||
get_viewport().set_input_as_handled()
|
||||
_cancel_beat()
|
||||
hide_dialogue()
|
||||
_end_player_conversation()
|
||||
dialogue_dismissed.emit()
|
||||
return
|
||||
|
||||
|
||||
# Responsive layout — clamps width to MAX_WIDTH_PX and height to 20% viewport.
|
||||
# -- Theme loading --
|
||||
|
||||
func _load_theme() -> void:
|
||||
# Default NPC palette in case file not found
|
||||
_npc_colors = [
|
||||
Color("#4a9ebb"), Color("#6bc9a6"), Color("#e8c547"), Color("#d49e5d"),
|
||||
Color("#b586d4"), Color("#d45d5d"), Color("#5daa7d"), Color("#7daccc"),
|
||||
]
|
||||
|
||||
if not FileAccess.file_exists(THEME_PATH):
|
||||
push_warning("DialogueBox: theme file not found: %s — using defaults" % THEME_PATH)
|
||||
return
|
||||
|
||||
var file := FileAccess.open(THEME_PATH, FileAccess.READ)
|
||||
if file == null:
|
||||
return
|
||||
var text := file.get_as_text()
|
||||
file.close()
|
||||
|
||||
var strings := UIStrings._parse_yaml(text)
|
||||
|
||||
if strings.has("player_color"):
|
||||
_player_color = Color(strings["player_color"])
|
||||
if strings.has("arrow_color"):
|
||||
_arrow_color = Color(strings["arrow_color"])
|
||||
if strings.has("speech_color"):
|
||||
_speech_color = Color(strings["speech_color"])
|
||||
if strings.has("passive_opacity"):
|
||||
_passive_opacity = float(strings["passive_opacity"])
|
||||
if strings.has("entry_lifetime_seconds"):
|
||||
_entry_lifetime = float(strings["entry_lifetime_seconds"])
|
||||
if strings.has("entry_fade_seconds"):
|
||||
_entry_fade = float(strings["entry_fade_seconds"])
|
||||
|
||||
# Load NPC colors from indexed keys (npc_colors.0 through npc_colors.7)
|
||||
var loaded_colors: Array[Color] = []
|
||||
for i in range(16): # support up to 16 palette entries
|
||||
var key := "npc_colors.%d" % i
|
||||
if strings.has(key):
|
||||
loaded_colors.append(Color(strings[key]))
|
||||
if loaded_colors.size() > 0:
|
||||
_npc_colors = loaded_colors
|
||||
|
||||
|
||||
# -- Responsive layout --
|
||||
|
||||
func _update_layout() -> void:
|
||||
var vp := get_viewport_rect().size
|
||||
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_right = w / 2.0
|
||||
panel.offset_top = -max_h
|
||||
|
||||
|
||||
# Show dialogue with NPC speech and response options.
|
||||
# npc_name: who is speaking (displayed as prefix)
|
||||
# speech: the NPC's dialogue text
|
||||
# options: Array of {text, response_id, priority, confrontation} — sorted by priority, max 3 shown.
|
||||
# D-062: locked options are invisible (server filters before sending).
|
||||
# D-063: confrontation options render italic.
|
||||
# -- Log entry management --
|
||||
|
||||
## Append a dialogue line to the log.
|
||||
## speaker/target: display names. text: the spoken line.
|
||||
## is_passive: true for overheard NPC-NPC (renders with ┃ prefix + desaturated).
|
||||
## Active conversation entries are pinned (no timeout) while _in_player_conversation.
|
||||
func append_line(speaker: String, target: String, text: String, is_passive: bool = false) -> void:
|
||||
var pinned := not is_passive and _in_player_conversation
|
||||
_log_entries.append({
|
||||
"speaker": speaker,
|
||||
"target": target,
|
||||
"text": text,
|
||||
"is_passive": is_passive,
|
||||
"pinned": pinned,
|
||||
"timestamp_msec": Time.get_ticks_msec(),
|
||||
})
|
||||
_log_dirty = true
|
||||
_ensure_visible()
|
||||
|
||||
|
||||
## 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:
|
||||
var text: String = event.get("occluded_line", "")
|
||||
if text.is_empty():
|
||||
return
|
||||
|
||||
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).
|
||||
func on_conversation_ended(_event: Dictionary) -> void:
|
||||
pass
|
||||
|
||||
|
||||
## Append the player's chosen response to the log.
|
||||
func append_player_line(target_npc: String, text: String) -> void:
|
||||
append_line(PLAYER_NAME, target_npc, text, false)
|
||||
|
||||
|
||||
## Append an NPC follow-up line (from dialogue_response).
|
||||
func append_dialogue_response(npc_name: String, text: String) -> void:
|
||||
append_line(npc_name, PLAYER_NAME, text, false)
|
||||
|
||||
|
||||
# -- Active player conversation --
|
||||
|
||||
## Show dialogue with NPC speech and response options.
|
||||
## npc_name: who is speaking. speech: the NPC's line. options: player choices.
|
||||
func show_dialogue(npc_name: String, speech: String, options: Array = []) -> void:
|
||||
_npc_name = npc_name
|
||||
_cancel_beat()
|
||||
_in_player_conversation = true
|
||||
|
||||
# NPC speech — name prefix in bold
|
||||
if npc_name.is_empty():
|
||||
npc_speech.text = speech
|
||||
else:
|
||||
npc_speech.text = "[b]%s:[/b] %s" % [npc_name, speech]
|
||||
# Append NPC's line to the log
|
||||
if not speech.is_empty():
|
||||
append_line(npc_name, PLAYER_NAME, speech, false)
|
||||
|
||||
# Clear old options
|
||||
# Clear old options and show new ones
|
||||
_clear_options()
|
||||
_show_options(options)
|
||||
|
||||
# Sort by priority ascending, cap at MAX_OPTIONS (D-061: max 3 visible)
|
||||
# Show panel
|
||||
_ensure_visible()
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
GameState.dialogue_active = true # D-064: block movement while in conversation
|
||||
|
||||
# D-069: Dialogue dip
|
||||
AudioManager.apply_dip("dialogue")
|
||||
|
||||
# D-061: auto-pause — signal to main.gd for input recording (#507)
|
||||
pause_requested.emit()
|
||||
|
||||
|
||||
## End active player conversation — clears options but preserves log.
|
||||
## D-064: dialogue_active cleared immediately so WASD resumes.
|
||||
## Log entries remain visible and expire via timeout (cosmetic only).
|
||||
func _end_player_conversation() -> void:
|
||||
_in_player_conversation = false
|
||||
_clear_options()
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
|
||||
# Unpin all entries and reset their timestamps so timeout starts now (Araminta review)
|
||||
var now := Time.get_ticks_msec()
|
||||
for entry in _log_entries:
|
||||
if entry.pinned:
|
||||
entry.pinned = false
|
||||
entry.timestamp_msec = now
|
||||
_log_dirty = true
|
||||
|
||||
# D-069: Clear dialogue/confrontation dip
|
||||
AudioManager.clear_dip()
|
||||
|
||||
# D-061: unpause — signal to main.gd for input recording (#507)
|
||||
unpause_requested.emit()
|
||||
|
||||
# D-064: unblock movement immediately — log entries stay visible but don't block input.
|
||||
GameState.dialogue_active = false
|
||||
|
||||
# If no entries remain, hide the panel with fade.
|
||||
if _log_entries.is_empty():
|
||||
hide_dialogue()
|
||||
|
||||
|
||||
## End active dialogue state. Panel stays visible (permanent insert UI element).
|
||||
func hide_dialogue() -> void:
|
||||
if _in_player_conversation:
|
||||
_end_player_conversation()
|
||||
return # _end_player_conversation may call hide_dialogue if log is empty
|
||||
|
||||
GameState.dialogue_active = false
|
||||
|
||||
|
||||
func is_dialogue_active() -> bool:
|
||||
return _in_player_conversation or GameState.dialogue_active
|
||||
|
||||
|
||||
func has_active_entries() -> bool:
|
||||
return _log_entries.size() > 0 or _in_player_conversation
|
||||
|
||||
|
||||
# -- Options --
|
||||
|
||||
func _show_options(options: Array) -> void:
|
||||
var sorted_opts: Array = options.duplicate()
|
||||
sorted_opts.sort_custom(func(a, b): return a.get("priority", 0) < b.get("priority", 0))
|
||||
var count := mini(sorted_opts.size(), MAX_OPTIONS)
|
||||
@@ -103,27 +354,21 @@ func show_dialogue(npc_name: String, speech: String, options: Array = []) -> voi
|
||||
var raw_text: String = opt.get("text", "")
|
||||
var is_confrontation: bool = opt.get("confrontation", false)
|
||||
|
||||
# RichTextLabel for BBCode support (D-063: confrontation italic)
|
||||
var label := RichTextLabel.new()
|
||||
label.bbcode_enabled = true
|
||||
label.fit_content = true
|
||||
label.scroll_active = false
|
||||
var label := Label.new()
|
||||
label.add_theme_font_size_override("font_size", 14)
|
||||
label.add_theme_color_override("font_color", Constants.INSERT_COLOR_TEXT)
|
||||
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
label.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
label.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||
label.add_theme_color_override("default_color", Constants.INSERT_COLOR_TEXT)
|
||||
|
||||
if is_confrontation:
|
||||
label.text = "[i]%s[/i]" % raw_text
|
||||
else:
|
||||
label.text = raw_text
|
||||
var numbered_text := "%d. %s" % [i + 1, raw_text]
|
||||
label.text = numbered_text
|
||||
|
||||
# Click handling
|
||||
var idx := i
|
||||
label.gui_input.connect(func(event: InputEvent):
|
||||
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
_on_option_pressed(idx)
|
||||
)
|
||||
# Hover color
|
||||
label.mouse_entered.connect(_make_hover_on(label))
|
||||
label.mouse_exited.connect(_make_hover_off(label))
|
||||
|
||||
@@ -133,56 +378,6 @@ func show_dialogue(npc_name: String, speech: String, options: Array = []) -> voi
|
||||
_option_texts.append(raw_text)
|
||||
_option_is_confrontation.append(is_confrontation)
|
||||
|
||||
# Show with fade
|
||||
visible = true
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
_is_showing = true
|
||||
GameState.dialogue_active = true # D-064: block movement while dialogue visible/fading
|
||||
|
||||
# D-069: Dialogue dip — reduce ambient noise to foreground conversation.
|
||||
# Confrontation options REPLACE (not nest) this dip via apply_dip("confrontation")
|
||||
# in _start_confrontation_beat(). hide_dialogue()'s clear_dip() restores to base
|
||||
# volumes regardless of which profile was last active — intentional replacement semantics.
|
||||
AudioManager.apply_dip("dialogue")
|
||||
|
||||
# D-061: auto-pause in single-player when dialogue opens
|
||||
SimBridge.send_input({"action": InputMapper.Action.PAUSE, "timestamp_msec": Time.get_ticks_msec()})
|
||||
|
||||
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)
|
||||
|
||||
|
||||
# Hide dialogue with fade (D-064: 300ms)
|
||||
func hide_dialogue() -> void:
|
||||
if not _is_showing:
|
||||
return
|
||||
|
||||
_is_showing = false
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
|
||||
# D-069: Clear dialogue/confrontation dip — restore bus volumes to player slider settings.
|
||||
# Safe to call even if confrontation beat already cleared the dip (no-op when empty).
|
||||
AudioManager.clear_dip()
|
||||
|
||||
# D-061: unpause when dialogue closes
|
||||
SimBridge.send_input({"action": InputMapper.Action.UNPAUSE, "timestamp_msec": Time.get_ticks_msec()})
|
||||
|
||||
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
|
||||
_clear_options()
|
||||
GameState.dialogue_active = false # D-064: unblock movement after fade completes
|
||||
)
|
||||
|
||||
|
||||
func is_dialogue_active() -> bool:
|
||||
return _is_showing or GameState.dialogue_active
|
||||
|
||||
|
||||
func _on_option_pressed(index: int) -> void:
|
||||
if index >= _option_controls.size():
|
||||
@@ -191,46 +386,40 @@ func _on_option_pressed(index: int) -> void:
|
||||
var text: String = _option_texts[index] if index < _option_texts.size() else ""
|
||||
var is_confront: bool = _option_is_confrontation[index] if index < _option_is_confrontation.size() else false
|
||||
|
||||
# Append player's chosen response to the log
|
||||
append_player_line(_npc_name, text)
|
||||
|
||||
if is_confront:
|
||||
_start_confrontation_beat(rid, text)
|
||||
else:
|
||||
option_selected.emit(rid, text)
|
||||
hide_dialogue()
|
||||
_end_player_conversation()
|
||||
|
||||
|
||||
# D-063: Confrontation beat — delay before sending response.
|
||||
# 1. Dim dialogue to 70%, show monologue, dip audio
|
||||
# 2. Wait CONFRONTATION_BEAT_DURATION
|
||||
# 3. Emit option_selected, restore audio, hide dialogue
|
||||
# -- Confrontation beat (D-063) --
|
||||
|
||||
func _start_confrontation_beat(response_id: String, text: String) -> void:
|
||||
# Disable option clicks during beat
|
||||
for ctrl in _option_controls:
|
||||
if is_instance_valid(ctrl):
|
||||
ctrl.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
|
||||
# Dim dialogue box
|
||||
if _active_tween and _active_tween.is_valid():
|
||||
_active_tween.kill()
|
||||
_active_tween = create_tween()
|
||||
_active_tween.tween_property(panel, "modulate:a", CONFRONTATION_DIM_ALPHA, 0.2)
|
||||
|
||||
# D-063: monologue beat — text from ui-strings.yaml (D-042)
|
||||
confrontation_monologue.emit(UIStrings.get_text(CONFRONTATION_MONOLOGUE_KEY), CONFRONTATION_BEAT_DURATION)
|
||||
|
||||
# D-063: audio dip via AudioManager
|
||||
AudioManager.apply_dip("confrontation")
|
||||
|
||||
# Delay, then complete
|
||||
_beat_tween = create_tween()
|
||||
_beat_tween.tween_interval(CONFRONTATION_BEAT_DURATION)
|
||||
_beat_tween.tween_callback(func():
|
||||
AudioManager.clear_dip()
|
||||
option_selected.emit(response_id, text)
|
||||
hide_dialogue()
|
||||
_end_player_conversation()
|
||||
)
|
||||
|
||||
|
||||
# Cancel an in-flight confrontation beat (e.g. player walks away mid-beat).
|
||||
func _cancel_beat() -> void:
|
||||
if _beat_tween and _beat_tween.is_valid():
|
||||
_beat_tween.kill()
|
||||
@@ -238,6 +427,191 @@ func _cancel_beat() -> void:
|
||||
AudioManager.clear_dip()
|
||||
|
||||
|
||||
# -- Log rendering --
|
||||
|
||||
## Rebuild the dialogue log BBCode from all non-expired entries.
|
||||
func _rebuild_log() -> void:
|
||||
if not dialogue_log:
|
||||
return
|
||||
var now := Time.get_ticks_msec()
|
||||
var bbcode := ""
|
||||
for entry in _log_entries:
|
||||
var alpha: float = 1.0
|
||||
if not entry.pinned:
|
||||
var age_sec: float = (now - entry.timestamp_msec) / 1000.0
|
||||
if age_sec > _entry_lifetime:
|
||||
alpha = clampf(1.0 - (age_sec - _entry_lifetime) / _entry_fade, 0.0, 1.0)
|
||||
if entry.is_passive:
|
||||
alpha *= _passive_opacity
|
||||
if alpha <= 0.0:
|
||||
continue
|
||||
var line := _format_entry(entry, alpha)
|
||||
if not bbcode.is_empty():
|
||||
bbcode += "\n"
|
||||
bbcode += line
|
||||
dialogue_log.text = bbcode
|
||||
|
||||
|
||||
## Format a single log entry as BBCode.
|
||||
## Hoshe #2: escape BBCode brackets in server-sourced strings.
|
||||
## Araminta: passive lines get ┃ prefix + desaturated colours.
|
||||
## 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:
|
||||
var speaker: String
|
||||
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 is_passive: bool = entry.is_passive
|
||||
|
||||
# Desaturate passive name colours (Araminta review)
|
||||
if is_passive:
|
||||
speaker_color = _desaturate(speaker_color, PASSIVE_DESATURATION)
|
||||
target_color = _desaturate(target_color, PASSIVE_DESATURATION)
|
||||
|
||||
var sc := _color_with_alpha(speaker_color, alpha)
|
||||
var ac := _color_with_alpha(_arrow_color, alpha)
|
||||
var tc := _color_with_alpha(target_color, alpha)
|
||||
var txc := _color_with_alpha(_speech_color, alpha)
|
||||
|
||||
var prefix := PASSIVE_GLYPH if is_passive else ""
|
||||
|
||||
# Non-blocking: simplify 1-on-1 player dialogue — no arrow for Speaker → You or You → Speaker
|
||||
if involves_player and not is_passive:
|
||||
# Just "Speaker: text" or "You: text"
|
||||
return "%s[color=%s][b]%s[/b][/color][color=%s]: %s[/color]" % [
|
||||
prefix, sc, speaker, txc, text
|
||||
]
|
||||
else:
|
||||
# Full "Speaker → Target: text" for overheard
|
||||
return "%s[color=%s][b]%s[/b][/color][color=%s] \u2192 [/color][color=%s][b]%s[/b][/color][color=%s]: %s[/color]" % [
|
||||
prefix, sc, speaker, ac, tc, target, txc, text
|
||||
]
|
||||
|
||||
|
||||
## Escape BBCode bracket characters in server-sourced text (Hoshe #2).
|
||||
static func _escape_bbcode(text: String) -> String:
|
||||
return text.replace("[", "[lb]")
|
||||
|
||||
|
||||
## Get a stable color for a character name, with contrast floor enforcement.
|
||||
func _color_for_name(char_name: String) -> Color:
|
||||
if char_name == PLAYER_NAME:
|
||||
return _player_color
|
||||
if _npc_colors.is_empty():
|
||||
return _speech_color
|
||||
var idx := absi(char_name.hash()) % _npc_colors.size()
|
||||
var color := _npc_colors[idx]
|
||||
return _enforce_contrast(color)
|
||||
|
||||
|
||||
## Enforce minimum luminance so name colours remain readable against dark BG (Araminta #3).
|
||||
static func _enforce_contrast(color: Color) -> Color:
|
||||
var lum := color.r * 0.299 + color.g * 0.587 + color.b * 0.114
|
||||
if lum < MIN_CONTRAST_LUMINANCE:
|
||||
var boost := MIN_CONTRAST_LUMINANCE / maxf(lum, 0.001)
|
||||
return Color(
|
||||
minf(color.r * boost, 1.0),
|
||||
minf(color.g * boost, 1.0),
|
||||
minf(color.b * boost, 1.0),
|
||||
color.a
|
||||
)
|
||||
return color
|
||||
|
||||
|
||||
## Desaturate a colour by a factor (0.0 = no change, 1.0 = full greyscale).
|
||||
static func _desaturate(color: Color, amount: float) -> Color:
|
||||
var grey := color.r * 0.299 + color.g * 0.587 + color.b * 0.114
|
||||
return Color(
|
||||
lerpf(color.r, grey, amount),
|
||||
lerpf(color.g, grey, amount),
|
||||
lerpf(color.b, grey, amount),
|
||||
color.a
|
||||
)
|
||||
|
||||
|
||||
## Convert a Color to a hex string with alpha baked in.
|
||||
static func _color_with_alpha(base: Color, alpha: float) -> String:
|
||||
return Color(base.r, base.g, base.b, base.a * alpha).to_html(true)
|
||||
|
||||
|
||||
# -- Entry expiry --
|
||||
|
||||
## Remove fully expired entries. Mark dirty if any fading entries exist.
|
||||
## Pinned entries (active conversation) skip expiry entirely (Araminta #2).
|
||||
func _expire_entries() -> void:
|
||||
if _log_entries.is_empty():
|
||||
return
|
||||
var now := Time.get_ticks_msec()
|
||||
var total_lifetime_msec: int = int((_entry_lifetime + _entry_fade) * 1000.0)
|
||||
var removed := false
|
||||
var has_fading := false
|
||||
|
||||
# Remove expired non-pinned entries from the front (oldest first)
|
||||
while _log_entries.size() > 0:
|
||||
var entry: Dictionary = _log_entries[0]
|
||||
if entry.pinned:
|
||||
break # Pinned entries never expire
|
||||
var age: int = now - entry.timestamp_msec
|
||||
if age < total_lifetime_msec:
|
||||
# Check if this entry is in the fading phase
|
||||
if age > int(_entry_lifetime * 1000.0):
|
||||
has_fading = true
|
||||
break
|
||||
_log_entries.remove_at(0)
|
||||
removed = true
|
||||
|
||||
# Check remaining entries for fading state
|
||||
if not has_fading:
|
||||
for entry in _log_entries:
|
||||
if not entry.pinned:
|
||||
var age: int = now - entry.timestamp_msec
|
||||
if age > int(_entry_lifetime * 1000.0):
|
||||
has_fading = true
|
||||
break
|
||||
|
||||
if removed or has_fading:
|
||||
_log_dirty = true
|
||||
|
||||
if removed and _log_entries.is_empty() and not _in_player_conversation:
|
||||
hide_dialogue()
|
||||
|
||||
|
||||
# -- Visibility --
|
||||
|
||||
## No-op — panel is always visible as a permanent insert UI element.
|
||||
func _ensure_visible() -> void:
|
||||
pass
|
||||
|
||||
|
||||
func _clear_options() -> void:
|
||||
for ctrl in _option_controls:
|
||||
if is_instance_valid(ctrl):
|
||||
@@ -248,12 +622,12 @@ func _clear_options() -> void:
|
||||
_option_is_confrontation.clear()
|
||||
|
||||
|
||||
# Hover callbacks — closures that capture the label reference.
|
||||
static func _make_hover_on(label: RichTextLabel) -> Callable:
|
||||
# Hover callbacks
|
||||
static func _make_hover_on(label: Control) -> Callable:
|
||||
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():
|
||||
label.add_theme_color_override("default_color", Constants.INSERT_COLOR_TEXT)
|
||||
label.add_theme_color_override("font_color", Constants.INSERT_COLOR_TEXT)
|
||||
|
||||
@@ -37,12 +37,14 @@ theme_override_constants/margin_bottom = 14
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="NpcSpeech" type="RichTextLabel" parent="PanelContainer/MarginContainer/VBoxContainer"]
|
||||
[node name="DialogueLog" type="RichTextLabel" parent="PanelContainer/MarginContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
bbcode_enabled = true
|
||||
text = ""
|
||||
fit_content = true
|
||||
scroll_active = false
|
||||
fit_content = false
|
||||
scroll_active = true
|
||||
scroll_following = true
|
||||
|
||||
[node name="OptionsContainer" type="VBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
@@ -19,10 +19,12 @@ const LABEL_GAP := 2
|
||||
const INSERT_FG := Constants.IMPLANT_TEXT_COLOR
|
||||
const INSERT_DIM := Constants.IMPLANT_TEXT_DIM
|
||||
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 _insert_active: bool = true
|
||||
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 _selected_index: int = 0
|
||||
var _active_tween: Tween = null
|
||||
@@ -37,6 +39,21 @@ func _ready() -> void:
|
||||
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:
|
||||
# D-055: Sprint stance suppresses interaction list
|
||||
if GameState.player_stance == "Sprint":
|
||||
@@ -70,6 +87,7 @@ func update_from_state() -> void:
|
||||
_current_target_id = entity_id
|
||||
_verb_items = sorted
|
||||
_selected_index = 0
|
||||
_cache_entity_position()
|
||||
_rebuild_labels()
|
||||
_show()
|
||||
|
||||
@@ -84,15 +102,46 @@ func _rebuild_labels() -> void:
|
||||
for i in range(_verb_items.size()):
|
||||
var verb: Dictionary = _verb_items[i]
|
||||
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_color_override("font_color", INSERT_FG if i == _selected_index else INSERT_DIM)
|
||||
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)
|
||||
_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:
|
||||
if _showing:
|
||||
return
|
||||
@@ -161,6 +210,29 @@ func get_verb_labels() -> Array:
|
||||
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:
|
||||
_insert_active = active
|
||||
if not active and _showing:
|
||||
|
||||
+150
-35
@@ -1,48 +1,163 @@
|
||||
extends Control
|
||||
|
||||
# Internal monologue display (per D-015)
|
||||
# Shows character's internal thoughts as text overlay
|
||||
# Internal monologue display — multi-line, priority-queued (per D-016, #122).
|
||||
# Per Tyre architecture review, Sprint 14.
|
||||
#
|
||||
# Up to MAX_VISIBLE lines display simultaneously in a VBoxContainer.
|
||||
# Additional arrivals queue up to MAX_QUEUE depth; lowest-priority entry is
|
||||
# dropped when the queue is full and an equal-or-higher-priority line arrives
|
||||
# (>= tiebreak = FIFO: newest replaces oldest at same priority).
|
||||
#
|
||||
# Stagger: 0.15s minimum gap between consecutive fade-ins (spec §5.4).
|
||||
# Colour: lattice_profile passed in at call time — no autoload access in renderer.
|
||||
# is_urgent=true → opacity 1.0 and elevated colour variant (bloom deferred).
|
||||
|
||||
@onready var text_panel: PanelContainer = $PanelContainer
|
||||
@onready var text_label: RichTextLabel = $PanelContainer/MarginContainer/RichTextLabel
|
||||
const MAX_VISIBLE: int = 3
|
||||
const MAX_QUEUE: int = 5
|
||||
|
||||
const STAGGER_SEC: float = 0.15
|
||||
const FADE_IN_SEC: float = 0.3
|
||||
const FADE_OUT_SEC: float = 0.5
|
||||
const MIN_DURATION: float = FADE_IN_SEC + 0.1 # clamp: line must survive its own fade-in
|
||||
|
||||
# Lattice colour palette — keyed by lattice_profile passed from GameState at show time.
|
||||
# standard opacity = 0.85, urgent opacity = 1.0.
|
||||
# Source: Tyre architecture review, Sprint 14.
|
||||
const _LATTICE_COLORS: Dictionary = {
|
||||
"lattice_augmented": { # detective
|
||||
"standard": Color("#d0d4e0"),
|
||||
"urgent": Color("#e0e8f8"),
|
||||
},
|
||||
"lattice_baseline": { # smuggler
|
||||
"standard": Color("#d8d0c4"),
|
||||
"urgent": Color("#f0e4d4"),
|
||||
},
|
||||
}
|
||||
const _FALLBACK_STANDARD: Color = Color("#c8d0e0")
|
||||
const _FALLBACK_URGENT: Color = Color("#e0e8f8")
|
||||
|
||||
@onready var _vbox: VBoxContainer = $VBoxContainer
|
||||
|
||||
# Visible slot: {node: Control, expire_timer: float, priority: int, tween: Tween}
|
||||
var _visible: Array[Dictionary] = []
|
||||
# Queue entry: {text, duration, priority, is_urgent, lattice_profile}
|
||||
var _queue: Array[Dictionary] = []
|
||||
# Msec timestamp when the next fade-in may begin (stagger enforcement)
|
||||
var _next_fade_in_msec: float = 0.0
|
||||
|
||||
var fade_timer: float = 0.0
|
||||
var fade_duration: float = 5.0 # Display duration before fade
|
||||
var is_visible: bool = false
|
||||
var _active_tween: Tween = null
|
||||
|
||||
func _ready() -> void:
|
||||
print("MonologueDisplay: Initialized")
|
||||
text_panel.modulate.a = 0.0
|
||||
is_visible = false
|
||||
pass
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
# Auto-fade after display
|
||||
if is_visible:
|
||||
fade_timer += delta
|
||||
if fade_timer >= fade_duration:
|
||||
_fade_out()
|
||||
# Expire visible lines
|
||||
for slot in _visible.duplicate():
|
||||
slot.expire_timer -= delta
|
||||
if slot.expire_timer <= 0.0:
|
||||
_retire_slot(slot)
|
||||
|
||||
# Show internal monologue text
|
||||
func show_monologue(text: String, duration: float = 5.0) -> void:
|
||||
text_label.text = text
|
||||
fade_duration = duration
|
||||
fade_timer = 0.0
|
||||
is_visible = true
|
||||
# Drain queue into available visible slots (one per stagger interval)
|
||||
if not _queue.is_empty() and _visible.size() < MAX_VISIBLE:
|
||||
var now := float(Time.get_ticks_msec())
|
||||
if now >= _next_fade_in_msec:
|
||||
var next: Dictionary = _queue.pop_front()
|
||||
_show_line(next.text, next.duration, next.priority, next.is_urgent, next.lattice_profile)
|
||||
|
||||
# Cancel any active tween before starting a new one
|
||||
if _active_tween and _active_tween.is_valid():
|
||||
_active_tween.kill()
|
||||
_active_tween = create_tween()
|
||||
_active_tween.tween_property(text_panel, "modulate:a", 1.0, 0.3)
|
||||
|
||||
# Fade out the monologue
|
||||
func _fade_out() -> void:
|
||||
if not is_visible:
|
||||
# Display a monologue line.
|
||||
# priority: higher number = more important (default 2; urgent beats normal).
|
||||
# is_urgent: visual flag — full opacity + elevated colour. Bloom deferred.
|
||||
# Empty text is silently ignored — no slot created, no queue entry.
|
||||
# lattice_profile is read from GameState here and passed down — renderer stays
|
||||
# decoupled from the autoload (D-020 renderer contract).
|
||||
func show_monologue(text: String, duration: float, priority: int = 2, is_urgent: bool = false) -> void:
|
||||
if text.is_empty():
|
||||
return
|
||||
var profile := GameState.lattice_profile
|
||||
var now := float(Time.get_ticks_msec())
|
||||
if _visible.size() < MAX_VISIBLE and now >= _next_fade_in_msec:
|
||||
_show_line(text, duration, priority, is_urgent, profile)
|
||||
else:
|
||||
_enqueue(text, duration, priority, is_urgent, profile)
|
||||
|
||||
is_visible = false
|
||||
if _active_tween and _active_tween.is_valid():
|
||||
_active_tween.kill()
|
||||
_active_tween = create_tween()
|
||||
_active_tween.tween_property(text_panel, "modulate:a", 0.0, 0.5)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _show_line(text: String, duration: float, priority: int, is_urgent: bool, lattice_profile: String) -> void:
|
||||
var line_node := _build_line_node(text, is_urgent, lattice_profile)
|
||||
_vbox.add_child(line_node)
|
||||
|
||||
var slot := {
|
||||
node = line_node,
|
||||
expire_timer = maxf(duration, MIN_DURATION), # clamp: survives own fade-in
|
||||
priority = priority,
|
||||
tween = null,
|
||||
}
|
||||
_visible.append(slot)
|
||||
_next_fade_in_msec = float(Time.get_ticks_msec()) + STAGGER_SEC * 1000.0
|
||||
|
||||
line_node.modulate.a = 0.0
|
||||
var tween := create_tween()
|
||||
slot.tween = tween
|
||||
var target_opacity := 1.0 if is_urgent else 0.85
|
||||
tween.tween_property(line_node, "modulate:a", target_opacity, FADE_IN_SEC)
|
||||
|
||||
|
||||
func _retire_slot(slot: Dictionary) -> void:
|
||||
_visible.erase(slot)
|
||||
var node: Node = slot.node
|
||||
var t: Tween = slot.tween
|
||||
if t and t.is_valid():
|
||||
t.kill()
|
||||
var tween := create_tween()
|
||||
tween.tween_property(node, "modulate:a", 0.0, FADE_OUT_SEC)
|
||||
tween.tween_callback(node.queue_free)
|
||||
|
||||
|
||||
func _enqueue(text: String, duration: float, priority: int, is_urgent: bool, lattice_profile: String) -> void:
|
||||
if _queue.size() < MAX_QUEUE:
|
||||
_queue.append({text = text, duration = duration, priority = priority, is_urgent = is_urgent, lattice_profile = lattice_profile})
|
||||
_queue.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return a.priority > b.priority)
|
||||
else:
|
||||
# >= tiebreak: newest replaces oldest at equal priority (FIFO for equal ranks)
|
||||
var lowest := _lowest_priority_idx()
|
||||
if priority >= _queue[lowest].priority:
|
||||
_queue[lowest] = {text = text, duration = duration, priority = priority, is_urgent = is_urgent, lattice_profile = lattice_profile}
|
||||
_queue.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return a.priority > b.priority)
|
||||
# else: incoming line is strictly lower priority — silently drop; no sort needed
|
||||
|
||||
|
||||
func _lowest_priority_idx() -> int:
|
||||
var idx := 0
|
||||
for i in range(1, _queue.size()):
|
||||
if _queue[i].priority < _queue[idx].priority:
|
||||
idx = i
|
||||
return idx
|
||||
|
||||
|
||||
func _build_line_node(text: String, is_urgent: bool, lattice_profile: String) -> Control:
|
||||
var palette: Dictionary = _LATTICE_COLORS.get(lattice_profile, {})
|
||||
var color: Color = palette.get("urgent", _FALLBACK_URGENT) if is_urgent \
|
||||
else palette.get("standard", _FALLBACK_STANDARD)
|
||||
|
||||
var container := MarginContainer.new()
|
||||
container.add_theme_constant_override("margin_left", 4)
|
||||
container.add_theme_constant_override("margin_right", 4)
|
||||
container.add_theme_constant_override("margin_top", 2)
|
||||
container.add_theme_constant_override("margin_bottom", 2)
|
||||
|
||||
var label := RichTextLabel.new()
|
||||
label.bbcode_enabled = true
|
||||
label.fit_content = true
|
||||
label.scroll_active = false
|
||||
label.add_theme_font_size_override("normal_font_size", 13)
|
||||
# Escape [ to prevent BBCode injection from server-sourced text.
|
||||
# [lb] is Godot's BBCode entity for a literal left bracket.
|
||||
var safe_text := text.replace("[", "[lb]")
|
||||
label.text = "[i][color=#%s]%s[/color][/i]" % [color.to_html(false), safe_text]
|
||||
|
||||
container.add_child(label)
|
||||
return container
|
||||
|
||||
@@ -2,37 +2,23 @@
|
||||
|
||||
[ext_resource type="Script" path="res://ui/monologue_display.gd" id="1_monologue"]
|
||||
|
||||
; Monologue display area — bottom-left of viewport.
|
||||
; Anchors: 5% left margin, 50% max width, 25% height from bottom (spec §3.1, z-layer 7).
|
||||
; Lines are created dynamically inside VBoxContainer by monologue_display.gd.
|
||||
[node name="MonologueDisplay" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 12
|
||||
anchor_top = 1.0
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_top = -150.0
|
||||
grow_horizontal = 2
|
||||
layout_mode = 1
|
||||
anchor_left = 0.05
|
||||
anchor_top = 0.75
|
||||
anchor_right = 0.55
|
||||
anchor_bottom = 0.98
|
||||
grow_horizontal = 1
|
||||
grow_vertical = 0
|
||||
clip_contents = true
|
||||
mouse_filter = 2
|
||||
script = ExtResource("1_monologue")
|
||||
|
||||
[node name="PanelContainer" type="PanelContainer" parent="."]
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 10
|
||||
anchor_right = 1.0
|
||||
offset_left = 100.0
|
||||
offset_right = -100.0
|
||||
offset_bottom = 120.0
|
||||
grow_horizontal = 2
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="PanelContainer"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_left = 16
|
||||
theme_override_constants/margin_top = 12
|
||||
theme_override_constants/margin_right = 16
|
||||
theme_override_constants/margin_bottom = 12
|
||||
|
||||
[node name="RichTextLabel" type="RichTextLabel" parent="PanelContainer/MarginContainer"]
|
||||
layout_mode = 2
|
||||
bbcode_enabled = true
|
||||
text = "Internal monologue will appear here..."
|
||||
fit_content = true
|
||||
scroll_active = false
|
||||
anchor_bottom = 1.0
|
||||
theme_override_constants/separation = 4
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://cpjq8yfsnpr5m
|
||||
@@ -5,7 +5,7 @@
|
||||
; D-058: World radial menu — right-click, 2 spokes (Observe + Insert)
|
||||
[node name="WorldRadial" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchors_preset = 0
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "dialogue-line.schema.json",
|
||||
"title": "Dialogue Line — canonical D-035 tag taxonomy",
|
||||
"description": "Canonical single-line schema for dialogue and monologue pools. Implements the converged tag taxonomy from D-035 (6 structural + 3 selection + 2 authoring-only tags). Monologue-specific additions (character, trigger, prerequisite) are defined in $defs/monologue_extension. Mood vocabulary renamed Sprint 14 to match voice guide (anxious/frustrated/content/suspicious/warm/hostile/relieved/focused).",
|
||||
"type": "object",
|
||||
"required": ["id", "text", "role", "access", "trust", "situation"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9-]*_(d|m)_[0-9]{3}$",
|
||||
"description": "Stable machine-parseable line ID: {template}_{d|m}_{###}. d = dialogue, m = monologue."
|
||||
},
|
||||
"text": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "The authored line text."
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9-]*$",
|
||||
"description": "Template-defined role slug (e.g. dock-worker, bar-owner, player_character). Not NPC name — NPC assignment is runtime."
|
||||
},
|
||||
"access": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": ["public", "insider", "authority", "peer", "hostile"]
|
||||
},
|
||||
"minItems": 1,
|
||||
"uniqueItems": true,
|
||||
"description": "D-028 Layer 1: access tiers this line is eligible for. List — a line can be eligible for multiple tiers. Hard filter."
|
||||
},
|
||||
"trust": {
|
||||
"type": "string",
|
||||
"enum": ["surface", "real", "secret"],
|
||||
"description": "D-028 Layer 3: minimum trust tier required. Hard filter. Ordering: surface < real < secret."
|
||||
},
|
||||
"situation": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"arrival",
|
||||
"shift_start",
|
||||
"shift_end",
|
||||
"shift_transition",
|
||||
"bar_evening",
|
||||
"night_shift",
|
||||
"investigation",
|
||||
"confrontation",
|
||||
"social",
|
||||
"alone",
|
||||
"emergency",
|
||||
"routine",
|
||||
"observation",
|
||||
"greeting"
|
||||
]
|
||||
},
|
||||
"minItems": 1,
|
||||
"uniqueItems": true,
|
||||
"description": "D-028 Layer 2: situations in which this line can fire. 14 v0.1 values (13 original + greeting added Sprint 8 for PC dialogue initial contact lines). NOTE: 'greeting' is not yet in server/src/content/line_pool.rs — lines using it will be skipped until Rust is updated."
|
||||
},
|
||||
"topic": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"colleague",
|
||||
"routine",
|
||||
"cargo",
|
||||
"money",
|
||||
"trust",
|
||||
"danger",
|
||||
"institution",
|
||||
"personal",
|
||||
"investigation"
|
||||
]
|
||||
},
|
||||
"uniqueItems": true,
|
||||
"description": "D-028 Layer 4: topic tags for weighted selection. 9 v0.1 values. Optional — defaults to empty if omitted. Note: 'crime' deliberately excluded; NPCs think of it as 'cargo' or 'money'."
|
||||
},
|
||||
"mood": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"anxious",
|
||||
"frustrated",
|
||||
"content",
|
||||
"suspicious",
|
||||
"warm",
|
||||
"hostile",
|
||||
"relieved",
|
||||
"focused"
|
||||
]
|
||||
},
|
||||
"uniqueItems": true,
|
||||
"description": "D-028 Layer 4: mood tags for weighted selection. 8 v0.1 values. Neutral mood = omit tag (untagged lines are always eligible). Renamed Sprint 14 to match voice guide vocabulary."
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Freeform escape hatch for author intent not covered by the structured taxonomy. Not consumed by the engine selection pipeline."
|
||||
},
|
||||
"knowledge_grant": {
|
||||
"type": "object",
|
||||
"description": "Knowledge the player gains from hearing this line. Feeds into the knowledge graph (D-041).",
|
||||
"required": ["fact_id", "confidence"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"fact_id": {
|
||||
"type": "string",
|
||||
"description": "FactId from the knowledge vocabulary (#368)."
|
||||
},
|
||||
"confidence": {
|
||||
"type": "string",
|
||||
"enum": ["suspects", "knows_of", "knows_details", "direct"],
|
||||
"description": "D-041 confidence tier granted."
|
||||
}
|
||||
}
|
||||
},
|
||||
"dual_lens": {
|
||||
"type": "object",
|
||||
"description": "Authoring-only: per-character notes for content with different resonance for smuggler vs detective. NOT consumed by the engine.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"smuggler": { "type": "string" },
|
||||
"detective": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"notes": {
|
||||
"type": "string",
|
||||
"description": "Authoring-only: freeform author notes, context, or intent documentation. NOT consumed by the engine."
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"monologue_extension": {
|
||||
"title": "Monologue-specific additions (D-035)",
|
||||
"description": "Additional required fields for monologue lines. Applied ON TOP OF the base dialogue line schema. Pool-level character partitioning (D-032) is enforced at the pool root, not per-line.",
|
||||
"type": "object",
|
||||
"required": ["trigger"],
|
||||
"properties": {
|
||||
"character": {
|
||||
"type": "string",
|
||||
"enum": ["smuggler", "detective"],
|
||||
"description": "D-032: hard partition tag. Which playable character this line belongs to. Must match the parent pool's character field."
|
||||
},
|
||||
"trigger": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"enter_location",
|
||||
"observe_npc",
|
||||
"hear_sound",
|
||||
"observe_anomaly",
|
||||
"post_conversation",
|
||||
"discover_evidence",
|
||||
"witness_interaction",
|
||||
"time_idle",
|
||||
"return_visit"
|
||||
],
|
||||
"description": "What causes this monologue line to fire. 9 v0.1 trigger types."
|
||||
},
|
||||
"prerequisite": {
|
||||
"description": "Knowledge state gate. null = unconditional (fires whenever triggered). Conditions are AND-evaluated. Uses FactIds from the knowledge vocabulary (#368, D-041).",
|
||||
"oneOf": [
|
||||
{ "type": "null" },
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"facts": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/fact_prerequisite" }
|
||||
},
|
||||
"entity_attributes": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/attribute_prerequisite" }
|
||||
},
|
||||
"relationship": {
|
||||
"type": "object",
|
||||
"required": ["target", "state"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"target": { "type": "string" },
|
||||
"state": {
|
||||
"type": "string",
|
||||
"enum": ["unknown", "known", "friendly", "person_of_interest", "hostile"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 10,
|
||||
"default": 5,
|
||||
"description": "Selection priority. Higher = more likely to fire when multiple lines are eligible. Default: 5."
|
||||
},
|
||||
"cooldown": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"default": 0,
|
||||
"description": "Minimum simulation ticks before this line can fire again. Default: 0 (no cooldown)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"fact_prerequisite": {
|
||||
"type": "object",
|
||||
"required": ["fact_id", "min_confidence"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"fact_id": {
|
||||
"type": "string",
|
||||
"description": "FactId from the knowledge vocabulary (#368, D-041)."
|
||||
},
|
||||
"min_confidence": {
|
||||
"type": "string",
|
||||
"enum": ["suspects", "knows_of", "knows_details", "direct"],
|
||||
"description": "Minimum D-041 confidence level required for this fact."
|
||||
}
|
||||
}
|
||||
},
|
||||
"attribute_prerequisite": {
|
||||
"type": "object",
|
||||
"required": ["entity", "key", "value"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"entity": { "type": "string" },
|
||||
"key": { "type": "string" },
|
||||
"value": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"situation_enum": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"arrival",
|
||||
"shift_start",
|
||||
"shift_end",
|
||||
"shift_transition",
|
||||
"bar_evening",
|
||||
"night_shift",
|
||||
"investigation",
|
||||
"confrontation",
|
||||
"social",
|
||||
"alone",
|
||||
"emergency",
|
||||
"routine",
|
||||
"observation",
|
||||
"greeting"
|
||||
],
|
||||
"description": "14 v0.1 situation values (D-035 + Sprint 8 amendment)."
|
||||
},
|
||||
"topic_enum": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"colleague",
|
||||
"routine",
|
||||
"cargo",
|
||||
"money",
|
||||
"trust",
|
||||
"danger",
|
||||
"institution",
|
||||
"personal",
|
||||
"investigation"
|
||||
],
|
||||
"description": "9 v0.1 topic values (D-035)."
|
||||
},
|
||||
"mood_enum": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"anxious",
|
||||
"frustrated",
|
||||
"content",
|
||||
"suspicious",
|
||||
"warm",
|
||||
"hostile",
|
||||
"relieved",
|
||||
"focused"
|
||||
],
|
||||
"description": "8 v0.1 mood values. Renamed Sprint 14 to match voice guide vocabulary. Neutral = untagged."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,13 +90,12 @@
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"fond", "comfortable", "worried", "suspicious",
|
||||
"analytical", "conflicted", "concerned", "relieved",
|
||||
"focused"
|
||||
"anxious", "frustrated", "content", "suspicious",
|
||||
"warm", "hostile", "relieved", "focused"
|
||||
]
|
||||
},
|
||||
"uniqueItems": true,
|
||||
"description": "Mood tags for selection weighting (D-035: list<enum>, v0.1 8 moods + focused added Sprint 8)"
|
||||
"description": "Mood tags for selection weighting (D-035: list<enum>). 8 v0.1 values, renamed Sprint 14 to match voice guide vocabulary. Neutral = untagged."
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
@@ -106,14 +105,28 @@
|
||||
"knowledge_grant": {
|
||||
"type": "object",
|
||||
"description": "Knowledge the player gains from hearing this line",
|
||||
"required": ["fact_id", "confidence"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"fact_id": { "type": "string" },
|
||||
"confidence": {
|
||||
"type": "string",
|
||||
"enum": ["suspects", "knows_of", "knows_details", "direct"]
|
||||
}
|
||||
},
|
||||
"required": ["fact_id", "confidence"]
|
||||
}
|
||||
},
|
||||
"dual_lens": {
|
||||
"type": "object",
|
||||
"description": "Authoring-only: per-character resonance notes (NOT consumed by engine)",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"smuggler": { "type": "string" },
|
||||
"detective": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"notes": {
|
||||
"type": "string",
|
||||
"description": "Authoring-only: freeform author notes (NOT consumed by engine)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "monologue-pool.schema.json",
|
||||
"title": "Monologue Line Pool",
|
||||
"description": "Tagged monologue lines, hard-partitioned by character (D-032, D-035).",
|
||||
"description": "Tagged monologue lines, hard-partitioned by character (D-032, D-035). Monologue lines carry all 6 D-035 structural tags for schema compliance, but role/access/trust are fixed constants for player-character internal voice (role=player_character, access=[public], trust=surface). The engine does not gate monologue on access or trust — these tags exist for taxonomy uniformity only.",
|
||||
"type": "object",
|
||||
"required": ["character", "location", "lines"],
|
||||
"additionalProperties": false,
|
||||
@@ -10,12 +10,12 @@
|
||||
"character": {
|
||||
"type": "string",
|
||||
"enum": ["smuggler", "detective"],
|
||||
"description": "Playable character this pool belongs to (hard partition per D-032)"
|
||||
"description": "Playable character this pool belongs to (hard partition per D-032). All lines in this pool belong to this character."
|
||||
},
|
||||
"location": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9-]*$",
|
||||
"description": "Location slug, or 'general' for location-independent lines"
|
||||
"description": "Location slug, or 'general' for location-independent lines."
|
||||
},
|
||||
"lines": {
|
||||
"type": "array",
|
||||
@@ -26,70 +26,176 @@
|
||||
"$defs": {
|
||||
"monologue_line": {
|
||||
"type": "object",
|
||||
"required": ["id", "text", "trigger"],
|
||||
"required": ["id", "text", "role", "access", "trust", "situation", "trigger"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9-]*_m_[sd]_[0-9]{3}$",
|
||||
"description": "Stable line ID: {location_slug}_m_{s|d}_{###}"
|
||||
"description": "Stable line ID: {location_slug}_m_{s|d}_{###}. s = smuggler, d = detective."
|
||||
},
|
||||
"text": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 160,
|
||||
"description": "Line text — 160 char max to fit monologue display without scrolling"
|
||||
"maxLength": 256,
|
||||
"description": "Line text — 256 char hard cap, aim for ≤160 to avoid line wrap"
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"const": "player_character",
|
||||
"description": "D-035 structural tag — always player_character for monologue. Monologue is the player character's internal voice."
|
||||
},
|
||||
"access": {
|
||||
"type": "array",
|
||||
"items": { "const": "public" },
|
||||
"minItems": 1,
|
||||
"maxItems": 1,
|
||||
"description": "D-035 structural tag — always [public] for monologue. No access gating applies to internal voice."
|
||||
},
|
||||
"trust": {
|
||||
"type": "string",
|
||||
"const": "surface",
|
||||
"description": "D-035 structural tag — always surface for monologue. No trust gating applies to internal voice."
|
||||
},
|
||||
"situation": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"arrival",
|
||||
"shift_start",
|
||||
"shift_end",
|
||||
"shift_transition",
|
||||
"bar_evening",
|
||||
"night_shift",
|
||||
"investigation",
|
||||
"confrontation",
|
||||
"social",
|
||||
"alone",
|
||||
"emergency",
|
||||
"routine",
|
||||
"observation",
|
||||
"greeting"
|
||||
]
|
||||
},
|
||||
"minItems": 1,
|
||||
"uniqueItems": true,
|
||||
"description": "D-035 structural tag: situations in which this monologue line is contextually appropriate. 14 v0.1 values. The engine selects using trigger; situation provides additional authoring context for filtering by the caller. NOTE: 'greeting' is not yet in server/src/content/line_pool.rs Situation enum."
|
||||
},
|
||||
"trigger": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"enter_location", "observe_npc", "hear_sound",
|
||||
"observe_anomaly", "post_conversation", "discover_evidence",
|
||||
"witness_interaction", "time_idle", "return_visit"
|
||||
"enter_location",
|
||||
"observe_npc",
|
||||
"hear_sound",
|
||||
"observe_anomaly",
|
||||
"post_conversation",
|
||||
"discover_evidence",
|
||||
"witness_interaction",
|
||||
"time_idle",
|
||||
"return_visit"
|
||||
],
|
||||
"description": "What causes this line to fire"
|
||||
"description": "What causes this line to fire. 9 v0.1 trigger types (D-035 monologue-specific tag)."
|
||||
},
|
||||
"prerequisites": {
|
||||
"type": "object",
|
||||
"description": "AND-only prerequisite conditions",
|
||||
"properties": {
|
||||
"facts": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/fact_prerequisite" }
|
||||
},
|
||||
"entity_attributes": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/attribute_prerequisite" }
|
||||
},
|
||||
"relationship": {
|
||||
"description": "Knowledge state gate (D-035 monologue-specific tag). null or omitted = unconditional. Conditions are AND-evaluated. Uses FactIds from the knowledge vocabulary (#368, D-041).",
|
||||
"oneOf": [
|
||||
{ "type": "null" },
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["target", "state"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"target": { "type": "string" },
|
||||
"state": {
|
||||
"type": "string",
|
||||
"enum": ["unknown", "known", "friendly", "person_of_interest", "hostile"]
|
||||
"facts": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/fact_prerequisite" }
|
||||
},
|
||||
"entity_attributes": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/attribute_prerequisite" }
|
||||
},
|
||||
"relationship": {
|
||||
"type": "object",
|
||||
"required": ["target", "state"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"target": { "type": "string" },
|
||||
"state": {
|
||||
"type": "string",
|
||||
"enum": ["unknown", "known", "friendly", "person_of_interest", "hostile"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"topic": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"colleague",
|
||||
"routine",
|
||||
"cargo",
|
||||
"money",
|
||||
"trust",
|
||||
"danger",
|
||||
"institution",
|
||||
"personal",
|
||||
"investigation"
|
||||
]
|
||||
},
|
||||
"uniqueItems": true,
|
||||
"description": "D-035 selection tag: topic tags for weighted selection. 9 v0.1 values. Optional."
|
||||
},
|
||||
"mood": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"anxious",
|
||||
"frustrated",
|
||||
"content",
|
||||
"suspicious",
|
||||
"warm",
|
||||
"hostile",
|
||||
"relieved",
|
||||
"focused"
|
||||
]
|
||||
},
|
||||
"uniqueItems": true,
|
||||
"description": "D-035 selection tag: mood tags for weighted selection. 8 v0.1 values, renamed Sprint 14 to match voice guide vocabulary. Neutral = untagged."
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 10,
|
||||
"default": 5,
|
||||
"description": "Selection priority (higher = more likely to fire)"
|
||||
"description": "Selection priority (higher = more likely to fire when multiple lines are eligible). Default: 5."
|
||||
},
|
||||
"cooldown": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Minimum ticks before this line can fire again"
|
||||
"default": 0,
|
||||
"description": "Minimum simulation ticks before this line can fire again. Default: 0."
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
"items": { "type": "string" },
|
||||
"description": "D-035 selection tag: freeform tags. Not consumed by the selection pipeline."
|
||||
},
|
||||
"dual_lens": {
|
||||
"type": "object",
|
||||
"description": "Authoring-only: notes on how this line reads differently for smuggler vs detective. NOT consumed by engine.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"smuggler": { "type": "string" },
|
||||
"detective": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"notes": {
|
||||
"type": "string",
|
||||
"description": "Authoring-only: freeform author notes. NOT consumed by engine."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -98,10 +204,14 @@
|
||||
"required": ["fact_id", "min_confidence"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"fact_id": { "type": "string" },
|
||||
"fact_id": {
|
||||
"type": "string",
|
||||
"description": "FactId from the knowledge vocabulary (#368, D-041)."
|
||||
},
|
||||
"min_confidence": {
|
||||
"type": "string",
|
||||
"enum": ["suspects", "knows_of", "knows_details", "direct"]
|
||||
"enum": ["suspects", "knows_of", "knows_details", "direct"],
|
||||
"description": "Minimum D-041 confidence level required."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
+64
-8
@@ -41,7 +41,7 @@ lines:
|
||||
access: [insider]
|
||||
trust: real
|
||||
situation: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
topic: [cargo]
|
||||
tags: [kael, ring-ops, renn, operational]
|
||||
|
||||
@@ -51,7 +51,7 @@ lines:
|
||||
access: [insider]
|
||||
trust: real
|
||||
situation: [routine]
|
||||
mood: [concerned]
|
||||
mood: [frustrated]
|
||||
topic: [danger, cargo]
|
||||
tags: [kael, ring-ops, operational, caution]
|
||||
|
||||
@@ -66,7 +66,7 @@ lines:
|
||||
access: [insider]
|
||||
trust: surface
|
||||
situation: [confrontation]
|
||||
mood: [concerned]
|
||||
mood: [frustrated]
|
||||
topic: [danger]
|
||||
tags: [kael, contradiction, phase-3]
|
||||
|
||||
@@ -76,7 +76,7 @@ lines:
|
||||
access: [insider]
|
||||
trust: surface
|
||||
situation: [confrontation]
|
||||
mood: [concerned]
|
||||
mood: [frustrated]
|
||||
topic: [routine]
|
||||
tags: [kael, deflection, lying, phase-4]
|
||||
|
||||
@@ -86,7 +86,7 @@ lines:
|
||||
access: [insider]
|
||||
trust: surface
|
||||
situation: [confrontation]
|
||||
mood: [concerned]
|
||||
mood: [frustrated]
|
||||
topic: [trust]
|
||||
tags: [kael, deflection, hostile-path, phase-4]
|
||||
|
||||
@@ -100,7 +100,7 @@ lines:
|
||||
access: [insider]
|
||||
trust: secret
|
||||
situation: [confrontation, alone]
|
||||
mood: [conflicted]
|
||||
mood: [anxious]
|
||||
topic: [personal, trust]
|
||||
tags: [kael, confession, phase-5]
|
||||
|
||||
@@ -110,7 +110,7 @@ lines:
|
||||
access: [insider]
|
||||
trust: secret
|
||||
situation: [confrontation, alone]
|
||||
mood: [worried]
|
||||
mood: [anxious]
|
||||
topic: [danger, trust]
|
||||
tags: [kael, confession, nils, phase-5]
|
||||
|
||||
@@ -120,9 +120,65 @@ lines:
|
||||
access: [insider]
|
||||
trust: secret
|
||||
situation: [alone]
|
||||
mood: [conflicted]
|
||||
mood: [anxious]
|
||||
topic: [personal]
|
||||
tags: [kael, confession, phase-5]
|
||||
knowledge_grant:
|
||||
fact_id: knowledge.kael_exit_plan
|
||||
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: maintenance-corridors_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: maintenance-corridors_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: maintenance-corridors_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: maintenance-corridors_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."
|
||||
|
||||
+20
-20
@@ -28,7 +28,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [ring-operative, surface-cover, b7]
|
||||
dual_lens:
|
||||
smuggler: "Cover phrase. He's not doing maintenance."
|
||||
@@ -42,7 +42,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [ring-operative, surface-cover, bay-four, spatial]
|
||||
dual_lens:
|
||||
smuggler: "He's telling me the route is faster. This is spatial information, not just courtesy."
|
||||
@@ -55,7 +55,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [ring-operative, surface-cover, ambient]
|
||||
dual_lens:
|
||||
smuggler: "Dead air confirmed. Meridian's not picking this up."
|
||||
@@ -68,7 +68,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine, colleague]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [ring-operative, surface-cover, pael, corridor-traffic]
|
||||
dual_lens:
|
||||
smuggler: "Pael was through. Legitimate. Corridor's been active — that means traffic covers us."
|
||||
@@ -81,7 +81,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [ring-operative, surface-cover, c4, spatial]
|
||||
dual_lens:
|
||||
smuggler: "He's giving me the timing. Three minutes to C-4 from this point."
|
||||
@@ -156,7 +156,7 @@ lines:
|
||||
trust: real
|
||||
situation: [routine, confrontation]
|
||||
topic: [danger, colleague]
|
||||
mood: [concerned]
|
||||
mood: [frustrated]
|
||||
tags: [ring-operative, ring-ops, coded, drin-asking, warning]
|
||||
dual_lens:
|
||||
smuggler: "Drin's circling B-7. That's a warning. Push the pickup or hold it."
|
||||
@@ -218,7 +218,7 @@ lines:
|
||||
trust: real
|
||||
situation: [confrontation, investigation]
|
||||
topic: [danger, institution]
|
||||
mood: [concerned]
|
||||
mood: [frustrated]
|
||||
tags: [ring-operative, ring-ops, commission, warning]
|
||||
dual_lens:
|
||||
smuggler: "Detective is visible on the main floor. Corridors are still clear."
|
||||
@@ -231,7 +231,7 @@ lines:
|
||||
trust: real
|
||||
situation: [confrontation]
|
||||
topic: [danger]
|
||||
mood: [concerned]
|
||||
mood: [frustrated]
|
||||
tags: [ring-operative, ring-ops, drin, abort-signal]
|
||||
dual_lens:
|
||||
smuggler: "Stop. Drin's in position. The window's closed."
|
||||
@@ -244,7 +244,7 @@ lines:
|
||||
trust: real
|
||||
situation: [confrontation, routine]
|
||||
topic: [danger, institution]
|
||||
mood: [concerned]
|
||||
mood: [frustrated]
|
||||
tags: [ring-operative, ring-ops, maret, voss, caution]
|
||||
dual_lens:
|
||||
smuggler: "Maret flagged something. Voss is managing it. Stand down tonight."
|
||||
@@ -257,7 +257,7 @@ lines:
|
||||
trust: real
|
||||
situation: [shift_end, routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [ring-operative, ring-ops, run-complete, closing]
|
||||
dual_lens:
|
||||
smuggler: "Run's done. No issues. Tomorrow."
|
||||
@@ -274,7 +274,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [investigation, confrontation]
|
||||
topic: [institution, routine]
|
||||
mood: [wary]
|
||||
mood: [suspicious]
|
||||
tags: [ring-operative, detective-path, access-denial]
|
||||
|
||||
- id: maintenance-corridors_d_118
|
||||
@@ -284,7 +284,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [investigation]
|
||||
topic: [institution, routine]
|
||||
mood: [wary]
|
||||
mood: [suspicious]
|
||||
tags: [ring-operative, detective-path, voss-redirect]
|
||||
dual_lens:
|
||||
smuggler: "Never encounters this."
|
||||
@@ -297,7 +297,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [investigation]
|
||||
topic: [routine]
|
||||
mood: [wary]
|
||||
mood: [suspicious]
|
||||
tags: [ring-operative, detective-path, deflection]
|
||||
|
||||
# ==========================================
|
||||
@@ -312,7 +312,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [ring-operative, surface-cover, generation-pass]
|
||||
|
||||
- id: maintenance-corridors_d_121
|
||||
@@ -322,7 +322,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine, colleague]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [ring-operative, surface-cover, pael, generation-pass]
|
||||
|
||||
- id: maintenance-corridors_d_122
|
||||
@@ -332,7 +332,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [ring-operative, surface-cover, generation-pass]
|
||||
|
||||
- id: maintenance-corridors_d_123
|
||||
@@ -342,7 +342,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [ring-operative, surface-cover, spatial, generation-pass]
|
||||
|
||||
- id: maintenance-corridors_d_124
|
||||
@@ -352,7 +352,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [ring-operative, surface-cover, navigation, generation-pass]
|
||||
dual_lens:
|
||||
smuggler: "He's directing people away from the route. Social traffic management."
|
||||
@@ -365,7 +365,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine, shift_end]
|
||||
topic: [routine]
|
||||
mood: [tired]
|
||||
mood: [frustrated]
|
||||
tags: [ring-operative, surface-cover, generation-pass]
|
||||
|
||||
- id: maintenance-corridors_d_126
|
||||
@@ -375,5 +375,5 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [ring-operative, surface-cover, generation-pass]
|
||||
|
||||
+5
-5
@@ -22,7 +22,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine, observation]
|
||||
topic: [colleague, personal]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [tev, seems-important-but-isnt, waiting, kosse]
|
||||
dual_lens:
|
||||
smuggler: "Someone asking who came through the corridor. Tev. Harmless — checking for a partner."
|
||||
@@ -43,7 +43,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine, cargo]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [tev, atmospheric]
|
||||
|
||||
- id: maintenance-corridors_d_003
|
||||
@@ -53,7 +53,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [tev, incidental]
|
||||
|
||||
- id: maintenance-corridors_d_004
|
||||
@@ -63,7 +63,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine, night_shift]
|
||||
topic: [routine, cargo]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [tev, atmospheric]
|
||||
|
||||
- id: maintenance-corridors_d_005
|
||||
@@ -73,5 +73,5 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine, observation]
|
||||
topic: [personal]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [tev, waiting]
|
||||
|
||||
+30
-30
@@ -20,7 +20,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [arrival]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [lera, greeting, economy-of-words]
|
||||
|
||||
- id: the-last-shift_d_101
|
||||
@@ -30,7 +30,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [arrival]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [lera, greeting]
|
||||
|
||||
- id: the-last-shift_d_102
|
||||
@@ -40,7 +40,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [arrival]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [lera, greeting, house-rules]
|
||||
|
||||
- id: the-last-shift_d_103
|
||||
@@ -50,7 +50,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [arrival, routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [lera, greeting, redirection]
|
||||
|
||||
# ==========================================
|
||||
@@ -64,7 +64,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [confrontation, routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [lera, house-rules, authority]
|
||||
|
||||
- id: the-last-shift_d_105
|
||||
@@ -74,7 +74,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine, shift_end]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [lera, tab, house-rules]
|
||||
|
||||
- id: the-last-shift_d_106
|
||||
@@ -84,7 +84,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine, institution]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [lera, torek, tab-arrangement]
|
||||
dual_lens:
|
||||
smuggler: "Lera chose to give Torek credit. Commission man. She's keeping him visible and comfortable. Smart."
|
||||
@@ -98,7 +98,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine, institution]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [lera, lattice-free, house-rules]
|
||||
notes: "Not legally enforceable — Lera knows this. It's about atmosphere, not compliance."
|
||||
|
||||
@@ -109,7 +109,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [lera, drinks, community]
|
||||
|
||||
# ==========================================
|
||||
@@ -123,7 +123,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [arrival, routine]
|
||||
topic: [colleague, routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [lera, kael, spatial-information]
|
||||
dual_lens:
|
||||
smuggler: "Good. She keeps track. That's what she does."
|
||||
@@ -136,7 +136,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [colleague]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [lera, naia, observation]
|
||||
dual_lens:
|
||||
smuggler: "Lera noticed Naia's off. She notices everything. She's saying it because she wants someone to do something."
|
||||
@@ -149,7 +149,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [colleague]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [lera, ren, drifter]
|
||||
dual_lens:
|
||||
smuggler: "That's Lera's version of a clean assessment. She'd say more if she suspected something."
|
||||
@@ -162,7 +162,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [colleague]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [lera, tev, endorsement]
|
||||
dual_lens:
|
||||
smuggler: "Lera vouching for Tev. She doesn't do that for people she doesn't trust."
|
||||
@@ -193,7 +193,7 @@ lines:
|
||||
trust: real
|
||||
situation: [routine]
|
||||
topic: [colleague]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [lera, kael, loyalty]
|
||||
dual_lens:
|
||||
smuggler: "Lera's loyal to Kael. Not to the ring. There's a difference and it matters."
|
||||
@@ -210,7 +210,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [arrival, investigation]
|
||||
topic: [institution, routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [lera, detective-path, neutral-welcome]
|
||||
|
||||
- id: the-last-shift_d_116
|
||||
@@ -220,7 +220,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [investigation, routine]
|
||||
topic: [institution, routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [lera, detective-path, institutional-independence]
|
||||
|
||||
- id: the-last-shift_d_117
|
||||
@@ -230,7 +230,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [investigation, confrontation]
|
||||
topic: [institution, routine]
|
||||
mood: [wary]
|
||||
mood: [suspicious]
|
||||
tags: [lera, detective-path, authority-assertion]
|
||||
|
||||
- id: the-last-shift_d_118
|
||||
@@ -240,7 +240,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [investigation]
|
||||
topic: [institution, routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [lera, detective-path, careful-language]
|
||||
dual_lens:
|
||||
smuggler: "Never encounters this line."
|
||||
@@ -257,7 +257,7 @@ lines:
|
||||
trust: real
|
||||
situation: [routine, alone]
|
||||
topic: [colleague, danger]
|
||||
mood: [concerned]
|
||||
mood: [frustrated]
|
||||
tags: [lera, naia, threshold-line]
|
||||
dual_lens:
|
||||
smuggler: "Lera's telling me to do something about Naia. She's right."
|
||||
@@ -274,7 +274,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [shift_end]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [lera, closing]
|
||||
|
||||
- id: the-last-shift_d_121
|
||||
@@ -284,7 +284,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [shift_end]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [lera, closing]
|
||||
|
||||
- id: the-last-shift_d_122
|
||||
@@ -294,7 +294,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [shift_end]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [lera, closing]
|
||||
|
||||
# ==========================================
|
||||
@@ -308,7 +308,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [arrival]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [lera, greeting, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_124
|
||||
@@ -318,7 +318,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [arrival]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [lera, greeting, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_125
|
||||
@@ -328,7 +328,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [arrival]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [lera, greeting, newcomer, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_126
|
||||
@@ -338,7 +338,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [arrival, routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [lera, house-rules, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_127
|
||||
@@ -348,7 +348,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [arrival]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [lera, redirect, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_128
|
||||
@@ -358,7 +358,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine, shift_end]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [lera, house-rules, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_129
|
||||
@@ -368,7 +368,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [lera, ambient, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_130
|
||||
@@ -378,5 +378,5 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [lera, ambient, generation-pass]
|
||||
|
||||
+30
-30
@@ -19,7 +19,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [arrival]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [bar-regular, greeting]
|
||||
|
||||
- id: the-last-shift_d_301
|
||||
@@ -29,7 +29,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [arrival]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [bar-regular, greeting]
|
||||
|
||||
- id: the-last-shift_d_302
|
||||
@@ -39,7 +39,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [arrival, routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [bar-regular, greeting, drinks]
|
||||
|
||||
# ==========================================
|
||||
@@ -53,7 +53,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine, cargo]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [bar-regular, terminal, voss]
|
||||
|
||||
- id: the-last-shift_d_304
|
||||
@@ -63,7 +63,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [colleague, cargo]
|
||||
mood: [wary]
|
||||
mood: [suspicious]
|
||||
tags: [bar-regular, maret, voss, discrepancy]
|
||||
dual_lens:
|
||||
smuggler: "They're talking about Maret's flags as general knowledge. That's a wider problem than I thought."
|
||||
@@ -76,7 +76,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [cargo, routine]
|
||||
mood: [wary]
|
||||
mood: [suspicious]
|
||||
tags: [bar-regular, bay-four, observation]
|
||||
dual_lens:
|
||||
smuggler: "They've noticed bay four. It's becoming visible. Not good."
|
||||
@@ -89,7 +89,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [bar-regular, transition-window, complaint]
|
||||
dual_lens:
|
||||
smuggler: "The window is their biggest frustration. And their biggest frustration is the ring's biggest asset."
|
||||
@@ -102,7 +102,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [institution, routine]
|
||||
mood: [wary]
|
||||
mood: [suspicious]
|
||||
tags: [bar-regular, commission, audit]
|
||||
dual_lens:
|
||||
smuggler: "Cycle fifteen. That's the deadline. Get clean before then."
|
||||
@@ -115,7 +115,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [institution, colleague]
|
||||
mood: [wary]
|
||||
mood: [suspicious]
|
||||
tags: [bar-regular, voss, observation]
|
||||
dual_lens:
|
||||
smuggler: "Voss is visible more. Is that heat from above, or is he managing something on the floor?"
|
||||
@@ -132,7 +132,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [bar-regular, sports, drifters]
|
||||
|
||||
- id: the-last-shift_d_310
|
||||
@@ -142,7 +142,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [institution, routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [bar-regular, infrastructure, grievance]
|
||||
|
||||
- id: the-last-shift_d_311
|
||||
@@ -152,7 +152,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [community, routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [bar-regular, vessels-tamm, notice]
|
||||
dual_lens:
|
||||
smuggler: "Vessels Tamm. Naia's surname is Tamm. Don't chase it — but don't ignore it either."
|
||||
@@ -165,7 +165,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [community, routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [bar-regular, clinic, community]
|
||||
|
||||
- id: the-last-shift_d_313
|
||||
@@ -175,7 +175,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [institution, community]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [bar-regular, lattice, economy]
|
||||
dual_lens:
|
||||
smuggler: "That's why the waiting list keeps growing. They're providing a service."
|
||||
@@ -188,7 +188,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [institution, community]
|
||||
mood: [wary]
|
||||
mood: [suspicious]
|
||||
tags: [bar-regular, workers-association, voss, transition-window]
|
||||
dual_lens:
|
||||
smuggler: "Voss is defending the twenty-minute window against Association pressure. He's protecting the gap."
|
||||
@@ -205,7 +205,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [arrival, investigation]
|
||||
topic: [routine]
|
||||
mood: [wary]
|
||||
mood: [suspicious]
|
||||
tags: [bar-regular, detective-path, recognition]
|
||||
|
||||
- id: the-last-shift_d_316
|
||||
@@ -215,7 +215,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [investigation, routine]
|
||||
topic: [institution, routine]
|
||||
mood: [wary]
|
||||
mood: [suspicious]
|
||||
tags: [bar-regular, detective-path, deflection]
|
||||
|
||||
- id: the-last-shift_d_317
|
||||
@@ -225,7 +225,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [investigation]
|
||||
topic: [routine]
|
||||
mood: [wary]
|
||||
mood: [suspicious]
|
||||
tags: [bar-regular, detective-path, lera-redirect]
|
||||
|
||||
# ==========================================
|
||||
@@ -239,7 +239,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [shift_end]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [bar-regular, closing]
|
||||
|
||||
- id: the-last-shift_d_319
|
||||
@@ -249,7 +249,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [shift_end]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [bar-regular, closing, community]
|
||||
|
||||
# ==========================================
|
||||
@@ -263,7 +263,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [arrival]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [bar-regular, greeting, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_321
|
||||
@@ -273,7 +273,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [arrival]
|
||||
topic: [routine, institution]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [bar-regular, greeting, voss, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_322
|
||||
@@ -283,7 +283,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [arrival]
|
||||
topic: [routine]
|
||||
mood: [tired]
|
||||
mood: [frustrated]
|
||||
tags: [bar-regular, greeting, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_323
|
||||
@@ -293,7 +293,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [bar-regular, sports, community, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_324
|
||||
@@ -303,7 +303,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [institution, community]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [bar-regular, infrastructure, grievance, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_325
|
||||
@@ -313,7 +313,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [colleague, routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [bar-regular, new-hire, community, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_326
|
||||
@@ -323,7 +323,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [institution, routine]
|
||||
mood: [wary]
|
||||
mood: [suspicious]
|
||||
tags: [bar-regular, infrastructure, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_327
|
||||
@@ -333,7 +333,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [colleague]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [bar-regular, kael, observation, generation-pass]
|
||||
dual_lens:
|
||||
smuggler: "Kael's absence is visible. Other people notice too."
|
||||
@@ -346,7 +346,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [bar-regular, lera, humor, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_329
|
||||
@@ -356,5 +356,5 @@ lines:
|
||||
trust: surface
|
||||
situation: [shift_end]
|
||||
topic: [routine, institution]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [bar-regular, closing, generation-pass]
|
||||
|
||||
+26
-26
@@ -19,7 +19,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [arrival]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [sess, greeting]
|
||||
|
||||
- id: the-last-shift_d_201
|
||||
@@ -29,7 +29,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [arrival]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [sess, greeting, social]
|
||||
|
||||
- id: the-last-shift_d_202
|
||||
@@ -39,7 +39,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [arrival, routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [sess, greeting, drinks]
|
||||
|
||||
# ==========================================
|
||||
@@ -53,7 +53,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [colleague]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [sess, kael, observation]
|
||||
dual_lens:
|
||||
smuggler: "Sess noticed Kael's off. She's reading it as personal. She doesn't know what's underneath."
|
||||
@@ -66,7 +66,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [institution, routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [sess, torek, observation]
|
||||
dual_lens:
|
||||
smuggler: "Torek's making friends with the staff. Not good."
|
||||
@@ -79,7 +79,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [colleague]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [sess, naia, note, observation]
|
||||
dual_lens:
|
||||
smuggler: "Naia left something. Lera has it. Go to Lera."
|
||||
@@ -92,7 +92,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [colleague]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [sess, ren, drifter]
|
||||
|
||||
- id: the-last-shift_d_207
|
||||
@@ -102,7 +102,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [colleague, routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [sess, tev, door, observation]
|
||||
dual_lens:
|
||||
smuggler: "Tev watches the door for Lera. Not for anyone else. That's the setup."
|
||||
@@ -115,7 +115,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [colleague, institution]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [sess, sera, torek, observation]
|
||||
dual_lens:
|
||||
smuggler: "Commission in, Commission contact out. Makes sense."
|
||||
@@ -132,7 +132,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [sess, tev, sports, community]
|
||||
|
||||
- id: the-last-shift_d_210
|
||||
@@ -142,7 +142,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [institution, routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [sess, workers-association, community]
|
||||
|
||||
- id: the-last-shift_d_211
|
||||
@@ -152,7 +152,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [sess, ambient]
|
||||
|
||||
# ==========================================
|
||||
@@ -166,7 +166,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [arrival, investigation]
|
||||
topic: [institution, routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [sess, detective-path, observation]
|
||||
|
||||
- id: the-last-shift_d_213
|
||||
@@ -176,7 +176,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [investigation, routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [sess, detective-path, open]
|
||||
dual_lens:
|
||||
smuggler: "Never encounters this line."
|
||||
@@ -189,7 +189,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [investigation]
|
||||
topic: [institution, routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [sess, detective-path, lera-redirect]
|
||||
|
||||
# ==========================================
|
||||
@@ -203,7 +203,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [shift_end]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [sess, closing]
|
||||
|
||||
- id: the-last-shift_d_216
|
||||
@@ -213,7 +213,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [shift_end]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [sess, closing]
|
||||
|
||||
# ==========================================
|
||||
@@ -227,7 +227,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [arrival]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [sess, greeting, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_218
|
||||
@@ -237,7 +237,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [arrival, routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [sess, greeting, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_219
|
||||
@@ -247,7 +247,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [arrival]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [sess, greeting, social, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_220
|
||||
@@ -257,7 +257,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine, arrival]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [sess, lera, house-rules, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_221
|
||||
@@ -267,7 +267,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [institution, colleague]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [sess, torek, observation, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_222
|
||||
@@ -277,7 +277,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [colleague]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [sess, ren, observation, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_223
|
||||
@@ -287,7 +287,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [sess, sports, community, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_224
|
||||
@@ -297,7 +297,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [sess, ambient, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_225
|
||||
@@ -307,5 +307,5 @@ lines:
|
||||
trust: surface
|
||||
situation: [shift_end, routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [sess, closing, generation-pass]
|
||||
|
||||
+6
-6
@@ -22,7 +22,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [bar_evening, social]
|
||||
topic: [money, cargo]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [ren, seems-important-but-isnt, job-seeking, night-shift]
|
||||
dual_lens:
|
||||
smuggler: "Someone asking about the night rotation. Probably nothing. Watch them anyway."
|
||||
@@ -43,7 +43,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [social, bar_evening]
|
||||
topic: [routine, colleague]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [ren, job-seeking]
|
||||
|
||||
- id: the-last-shift_d_003
|
||||
@@ -53,7 +53,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [social, bar_evening]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [ren, atmospheric]
|
||||
|
||||
- id: the-last-shift_d_004
|
||||
@@ -63,7 +63,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [social]
|
||||
topic: [personal, routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [ren, drifter]
|
||||
|
||||
- id: the-last-shift_d_005
|
||||
@@ -73,7 +73,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [bar_evening, social]
|
||||
topic: [personal]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [ren, atmospheric]
|
||||
|
||||
- id: the-last-shift_d_006
|
||||
@@ -83,7 +83,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [social, bar_evening]
|
||||
topic: [cargo, routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [ren, seems-important-but-isnt, job-seeking]
|
||||
notes: >
|
||||
Second "seems important" line. Asking about manifest deadlines sounds
|
||||
|
||||
+69
-15
@@ -18,7 +18,7 @@ lines:
|
||||
access: [insider, peer]
|
||||
trust: surface
|
||||
situation: [bar_evening, social, arrival]
|
||||
mood: [fond]
|
||||
mood: [warm]
|
||||
topic: [personal, colleague]
|
||||
tags: [kael, greeting, phase-1]
|
||||
|
||||
@@ -28,7 +28,7 @@ lines:
|
||||
access: [insider, peer]
|
||||
trust: surface
|
||||
situation: [bar_evening, social]
|
||||
mood: [fond]
|
||||
mood: [warm]
|
||||
topic: [personal]
|
||||
tags: [kael, social, phase-1]
|
||||
|
||||
@@ -38,7 +38,7 @@ lines:
|
||||
access: [insider, peer]
|
||||
trust: surface
|
||||
situation: [bar_evening, social]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
topic: [colleague, routine]
|
||||
tags: [kael, humor, voss, lera]
|
||||
|
||||
@@ -48,7 +48,7 @@ lines:
|
||||
access: [insider, peer]
|
||||
trust: surface
|
||||
situation: [bar_evening, social]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
topic: [colleague]
|
||||
tags: [kael, observational, torek]
|
||||
knowledge_grant:
|
||||
@@ -61,7 +61,7 @@ lines:
|
||||
access: [peer]
|
||||
trust: surface
|
||||
situation: [bar_evening, social]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
topic: [personal]
|
||||
tags: [kael, atmospheric]
|
||||
|
||||
@@ -71,7 +71,7 @@ lines:
|
||||
access: [insider, peer]
|
||||
trust: surface
|
||||
situation: [bar_evening, social]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
topic: [personal, routine]
|
||||
tags: [kael, casual, lera]
|
||||
|
||||
@@ -85,7 +85,7 @@ lines:
|
||||
access: [insider, peer]
|
||||
trust: surface
|
||||
situation: [bar_evening, social]
|
||||
mood: [fond]
|
||||
mood: [warm]
|
||||
topic: [personal]
|
||||
tags: [kael, unprompted, naia-reference]
|
||||
|
||||
@@ -95,7 +95,7 @@ lines:
|
||||
access: [insider, peer]
|
||||
trust: surface
|
||||
situation: [bar_evening, social]
|
||||
mood: [fond]
|
||||
mood: [warm]
|
||||
topic: [personal]
|
||||
tags: [kael, unprompted, naia-reference]
|
||||
|
||||
@@ -105,7 +105,7 @@ lines:
|
||||
access: [insider]
|
||||
trust: real
|
||||
situation: [bar_evening, social, alone]
|
||||
mood: [conflicted]
|
||||
mood: [anxious]
|
||||
topic: [personal, trust]
|
||||
tags: [kael, naia-reference, vulnerability]
|
||||
|
||||
@@ -119,7 +119,7 @@ lines:
|
||||
access: [insider]
|
||||
trust: real
|
||||
situation: [bar_evening, social]
|
||||
mood: [concerned]
|
||||
mood: [frustrated]
|
||||
topic: [danger]
|
||||
tags: [kael, ring-ops, nils]
|
||||
|
||||
@@ -129,7 +129,7 @@ lines:
|
||||
access: [insider]
|
||||
trust: real
|
||||
situation: [bar_evening, social]
|
||||
mood: [concerned]
|
||||
mood: [frustrated]
|
||||
topic: [danger, colleague]
|
||||
tags: [kael, ring-ops, torek, caution]
|
||||
|
||||
@@ -139,7 +139,7 @@ lines:
|
||||
access: [insider]
|
||||
trust: real
|
||||
situation: [bar_evening, social, alone]
|
||||
mood: [concerned]
|
||||
mood: [frustrated]
|
||||
topic: [colleague, danger]
|
||||
tags: [kael, ring-ops, lera, caution]
|
||||
knowledge_grant:
|
||||
@@ -156,7 +156,7 @@ lines:
|
||||
access: [insider, peer]
|
||||
trust: surface
|
||||
situation: [bar_evening, social]
|
||||
mood: [conflicted]
|
||||
mood: [anxious]
|
||||
topic: [personal]
|
||||
tags: [kael, contaminated-trust, phase-5]
|
||||
|
||||
@@ -166,7 +166,7 @@ lines:
|
||||
access: [insider]
|
||||
trust: real
|
||||
situation: [bar_evening, social]
|
||||
mood: [conflicted]
|
||||
mood: [anxious]
|
||||
topic: [personal]
|
||||
tags: [kael, contaminated-trust, naia-reference, phase-5]
|
||||
|
||||
@@ -176,6 +176,60 @@ lines:
|
||||
access: [insider]
|
||||
trust: surface
|
||||
situation: [bar_evening, social]
|
||||
mood: [worried]
|
||||
mood: [anxious]
|
||||
topic: [trust, personal]
|
||||
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: the-last-shift_d_016
|
||||
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: the-last-shift_d_017
|
||||
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: the-last-shift_d_018
|
||||
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: the-last-shift_d_019
|
||||
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."
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ lines:
|
||||
access: [public]
|
||||
trust: surface
|
||||
situation: [bar_evening, greeting]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
topic: [personal]
|
||||
tags: [pc-detective, greeting, public-tier]
|
||||
|
||||
|
||||
+2
-2
@@ -15,7 +15,7 @@ lines:
|
||||
access: [public]
|
||||
trust: surface
|
||||
situation: [bar_evening, greeting]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
topic: [personal]
|
||||
tags: [pc-smuggler, greeting, public-tier]
|
||||
|
||||
@@ -25,7 +25,7 @@ lines:
|
||||
access: [public]
|
||||
trust: surface
|
||||
situation: [bar_evening, social]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
topic: [routine]
|
||||
tags: [pc-smuggler, social, public-tier]
|
||||
|
||||
|
||||
+140
-81
@@ -39,23 +39,23 @@ lines:
|
||||
# ============================================================
|
||||
|
||||
- id: the-last-shift_d_101
|
||||
text: "There you are. Grab a seat -- Lera's doing that thing with the grain spirit again. Don't ask, just trust me and order the ale."
|
||||
text: "There you are. Grab a seat — Lera's doing that thing with the grain spirit again. Don't ask, just trust me and order the ale."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
trust: surface
|
||||
situation: [bar_evening, social]
|
||||
topic: [personal]
|
||||
mood: [fond, comfortable]
|
||||
mood: [warm, content]
|
||||
tags: [greeting, phase-1, establishes-warmth]
|
||||
|
||||
- id: the-last-shift_d_102
|
||||
text: "Corner booth's free. I saved it -- force of habit."
|
||||
text: "Corner booth's free. I saved it — force of habit."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
trust: surface
|
||||
situation: [bar_evening, arrival]
|
||||
topic: [personal]
|
||||
mood: [fond]
|
||||
mood: [warm]
|
||||
tags: [greeting, phase-1, spatial-anchor]
|
||||
|
||||
- id: the-last-shift_d_103
|
||||
@@ -65,7 +65,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [bar_evening, social]
|
||||
topic: [personal, routine]
|
||||
mood: [fond, comfortable]
|
||||
mood: [warm, content]
|
||||
tags: [greeting, phase-1-2]
|
||||
|
||||
- id: the-last-shift_d_104
|
||||
@@ -75,27 +75,27 @@ lines:
|
||||
trust: surface
|
||||
situation: [bar_evening, arrival]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [greeting, phase-2-5, repeatable]
|
||||
|
||||
- id: the-last-shift_d_105
|
||||
text: "Good timing. Naia was here earlier -- you just missed her."
|
||||
text: "Good timing. Naia was here earlier — you just missed her."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
trust: surface
|
||||
situation: [bar_evening, social]
|
||||
topic: [colleague, personal]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [greeting, phase-2, naia-reference]
|
||||
|
||||
- id: the-last-shift_d_106
|
||||
text: "Grab a seat. Quiet night so far -- Lera's in a good mood, which means the ale's fresh."
|
||||
text: "Grab a seat. Quiet night so far — Lera's in a good mood, which means the ale's fresh."
|
||||
role: bar-regular
|
||||
access: [peer, public]
|
||||
trust: surface
|
||||
situation: [bar_evening, social]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [greeting, phase-1-3]
|
||||
|
||||
- id: the-last-shift_d_107
|
||||
@@ -105,7 +105,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [bar_evening, arrival]
|
||||
topic: [personal]
|
||||
mood: [concerned]
|
||||
mood: [frustrated]
|
||||
tags: [greeting, phase-3-5, post-pattern, shorter]
|
||||
|
||||
- id: the-last-shift_d_108
|
||||
@@ -115,7 +115,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [bar_evening, arrival]
|
||||
topic: [routine, personal]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [greeting, phase-2-3]
|
||||
|
||||
# ============================================================
|
||||
@@ -130,17 +130,17 @@ lines:
|
||||
trust: surface
|
||||
situation: [bar_evening, social]
|
||||
topic: [routine, colleague]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [orientation, district-background]
|
||||
|
||||
- id: the-last-shift_d_111
|
||||
text: "See the card game in the corner? That's Harek's table. Security officer, off-duty. Harmless -- just loud."
|
||||
text: "See the card game in the corner? That's Harek's table. Security officer, off-duty. Harmless — just loud."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
trust: surface
|
||||
situation: [bar_evening, social]
|
||||
topic: [colleague]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [orientation, npc-introduction]
|
||||
|
||||
- id: the-last-shift_d_112
|
||||
@@ -150,7 +150,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [bar_evening, social]
|
||||
topic: [routine, colleague]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [orientation, district-background]
|
||||
|
||||
- id: the-last-shift_d_113
|
||||
@@ -160,7 +160,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [social]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [setting-atmosphere]
|
||||
|
||||
- id: the-last-shift_d_114
|
||||
@@ -170,7 +170,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [social]
|
||||
topic: [routine, institution]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [self-introduction, institutional-background]
|
||||
|
||||
- id: the-last-shift_d_115
|
||||
@@ -180,27 +180,27 @@ lines:
|
||||
trust: surface
|
||||
situation: [social]
|
||||
topic: [personal, institution]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [backstory, phase-1]
|
||||
|
||||
- id: the-last-shift_d_116
|
||||
text: "Lera remembers every regular's order. Eighteen years of that. Don't ask how she does it -- she'll just pour you one and prove it."
|
||||
text: "Lera remembers every regular's order. Eighteen years of that. Don't ask how she does it — she'll just pour you one and prove it."
|
||||
role: bar-regular
|
||||
access: [peer, public]
|
||||
trust: surface
|
||||
situation: [bar_evening, social]
|
||||
topic: [colleague]
|
||||
mood: [fond]
|
||||
mood: [warm]
|
||||
tags: [community-texture]
|
||||
|
||||
- id: the-last-shift_d_117
|
||||
text: "The shift crowd clears out by 19:00. After that it's just us -- regulars, a few stragglers. The good hours."
|
||||
text: "The shift crowd clears out by 19:00. After that it's just us — regulars, a few stragglers. The good hours."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
trust: surface
|
||||
situation: [bar_evening]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [setting-atmosphere, routine]
|
||||
|
||||
- id: the-last-shift_d_118
|
||||
@@ -210,7 +210,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [social]
|
||||
topic: [personal]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [setting-detail, phase-1]
|
||||
|
||||
- id: the-last-shift_d_119
|
||||
@@ -220,7 +220,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [social, bar_evening]
|
||||
topic: [personal]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [spatial-anchor, character-detail]
|
||||
|
||||
# ============================================================
|
||||
@@ -233,33 +233,33 @@ lines:
|
||||
# --- Surface trust ---
|
||||
|
||||
- id: the-last-shift_d_120
|
||||
text: "The Commission runs maintenance checks quarterly. I handle the field side -- equipment calibration, relay diagnostics. Standard."
|
||||
text: "The Commission runs maintenance checks quarterly. I handle the field side — equipment calibration, relay diagnostics. Standard."
|
||||
role: bar-regular
|
||||
access: [peer, authority]
|
||||
trust: surface
|
||||
situation: [social]
|
||||
topic: [institution, routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [institutional-background]
|
||||
|
||||
- id: the-last-shift_d_121
|
||||
text: "Naia teaches the station kids. Primary education, mostly. She's good at it -- patient. The kids adore her."
|
||||
text: "Naia teaches the station kids. Primary education, mostly. She's good at it — patient. The kids adore her."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
trust: surface
|
||||
situation: [social, bar_evening]
|
||||
topic: [colleague, personal]
|
||||
mood: [fond]
|
||||
mood: [warm]
|
||||
tags: [naia-reference, phase-2]
|
||||
|
||||
- id: the-last-shift_d_122
|
||||
text: "The district runs on freight. Terminal handles most of it -- container processing, manifest filing. Bread and butter."
|
||||
text: "The district runs on freight. Terminal handles most of it — container processing, manifest filing. Bread and butter."
|
||||
role: bar-regular
|
||||
access: [peer, public]
|
||||
trust: surface
|
||||
situation: [social]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [district-context]
|
||||
|
||||
- id: the-last-shift_d_123
|
||||
@@ -269,7 +269,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [social, investigation]
|
||||
topic: [colleague, institution]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [deflection-surface, distance-from-hub]
|
||||
|
||||
- id: the-last-shift_d_124
|
||||
@@ -279,7 +279,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [social]
|
||||
topic: [colleague, personal]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [community-framing]
|
||||
|
||||
- id: the-last-shift_d_125
|
||||
@@ -289,19 +289,19 @@ lines:
|
||||
trust: surface
|
||||
situation: [social]
|
||||
topic: [institution]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [information-offer, phase-2, overcompensation-seed]
|
||||
|
||||
# --- Real trust ---
|
||||
|
||||
- id: the-last-shift_d_130
|
||||
text: "Naia's been worried lately. About Kael -- her partner. He's a dock worker at the Terminal. Late nights, evasive answers. She confided in me."
|
||||
text: "Naia's been worried lately. About Kael — her partner. He's a dock worker at the Terminal. Late nights, evasive answers. She confided in me."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
trust: real
|
||||
situation: [social, bar_evening]
|
||||
topic: [colleague, personal, trust]
|
||||
mood: [concerned]
|
||||
mood: [frustrated]
|
||||
tags: [naia-kael-link, phase-2, critical-information]
|
||||
knowledge_grant:
|
||||
fact_id: "relationship.kael_naia_connection"
|
||||
@@ -314,7 +314,7 @@ lines:
|
||||
trust: real
|
||||
situation: [social]
|
||||
topic: [institution, investigation]
|
||||
mood: [analytical]
|
||||
mood: [focused]
|
||||
tags: [institutional-access, phase-2-3, self-incriminating-hint]
|
||||
|
||||
- id: the-last-shift_d_132
|
||||
@@ -324,7 +324,7 @@ lines:
|
||||
trust: real
|
||||
situation: [social, bar_evening]
|
||||
topic: [personal, trust]
|
||||
mood: [concerned, conflicted]
|
||||
mood: [frustrated, anxious]
|
||||
tags: [motivation-reveal, phase-2-3]
|
||||
|
||||
- id: the-last-shift_d_133
|
||||
@@ -334,7 +334,7 @@ lines:
|
||||
trust: real
|
||||
situation: [social]
|
||||
topic: [institution, investigation]
|
||||
mood: [analytical]
|
||||
mood: [focused]
|
||||
tags: [institutional-capability, overcompensation, phase-3-4]
|
||||
knowledge_grant:
|
||||
fact_id: "investigation.manifest_discrepancy"
|
||||
@@ -347,7 +347,7 @@ lines:
|
||||
trust: real
|
||||
situation: [social]
|
||||
topic: [institution, personal]
|
||||
mood: [analytical]
|
||||
mood: [focused]
|
||||
tags: [self-awareness, capability-reveal]
|
||||
|
||||
- id: the-last-shift_d_135
|
||||
@@ -357,7 +357,7 @@ lines:
|
||||
trust: real
|
||||
situation: [social, investigation]
|
||||
topic: [trust]
|
||||
mood: [conflicted]
|
||||
mood: [anxious]
|
||||
tags: [deflection-real-tier, phase-4, the-lie]
|
||||
|
||||
- id: the-last-shift_d_136
|
||||
@@ -367,19 +367,19 @@ lines:
|
||||
trust: real
|
||||
situation: [bar_evening, social]
|
||||
topic: [money, personal]
|
||||
mood: [conflicted]
|
||||
mood: [anxious]
|
||||
tags: [moral-position, community-loyalty]
|
||||
|
||||
# --- Secret trust ---
|
||||
|
||||
- id: the-last-shift_d_140
|
||||
text: "I ran a compliance check. Unauthorized. Six weeks ago. Naia was so worried about Kael -- I thought I could help. I found manifest discrepancies tied to his shifts."
|
||||
text: "I ran a compliance check. Unauthorized. Six weeks ago. Naia was so worried about Kael — I thought I could help. I found manifest discrepancies tied to his shifts."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
trust: secret
|
||||
situation: [alone, social]
|
||||
topic: [investigation, trust, personal]
|
||||
mood: [conflicted]
|
||||
mood: [anxious]
|
||||
tags: [confession, phase-5, critical-reveal]
|
||||
knowledge_grant:
|
||||
fact_id: "investigation.manifest_discrepancy"
|
||||
@@ -392,30 +392,30 @@ lines:
|
||||
trust: secret
|
||||
situation: [alone, social]
|
||||
topic: [personal, trust, institution]
|
||||
mood: [conflicted]
|
||||
mood: [anxious]
|
||||
tags: [motivation-confession, phase-5]
|
||||
|
||||
- id: the-last-shift_d_142
|
||||
text: "The irregularities correlate with Torek Lintar's inspection shifts. That's why I can't -- I can't be near him. He doesn't know I know."
|
||||
text: "The irregularities correlate with Torek Lintar's inspection shifts. That's why I can't — I can't be near him. He doesn't know I know."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
trust: secret
|
||||
situation: [alone]
|
||||
topic: [investigation, danger]
|
||||
mood: [worried, conflicted]
|
||||
mood: [anxious]
|
||||
tags: [torek-link, avoidance-explanation, phase-5]
|
||||
knowledge_grant:
|
||||
fact_id: "investigation.shift_mismatch"
|
||||
confidence: "knows_details"
|
||||
|
||||
- id: the-last-shift_d_143
|
||||
text: "I don't know what you're looking for. I told you everything I know. If you're asking whether I'm hiding something -- I'm a field tech, not an analyst."
|
||||
text: "I don't know what you're looking for. I told you everything I know. If you're asking whether I'm hiding something — I'm a field tech, not an analyst."
|
||||
role: bar-regular
|
||||
access: [peer, authority]
|
||||
trust: secret
|
||||
situation: [investigation, confrontation]
|
||||
topic: [trust, investigation]
|
||||
mood: [suspicious, conflicted]
|
||||
mood: [suspicious, anxious]
|
||||
tags: [denial-under-pressure, phase-4-5, the-wall]
|
||||
|
||||
- id: the-last-shift_d_144
|
||||
@@ -425,7 +425,7 @@ lines:
|
||||
trust: secret
|
||||
situation: [alone, social]
|
||||
topic: [trust, personal]
|
||||
mood: [conflicted]
|
||||
mood: [anxious]
|
||||
tags: [retrospective-confession, phase-5, emotional-payload]
|
||||
|
||||
# ============================================================
|
||||
@@ -435,36 +435,36 @@ lines:
|
||||
# ============================================================
|
||||
|
||||
- id: the-last-shift_d_150
|
||||
text: "Oh -- before I forget. The dock scheduler, Maret? She's sharp. If you need freight records context, she's the one to ask."
|
||||
text: "Oh — before I forget. The dock scheduler, Maret? She's sharp. If you need freight records context, she's the one to ask."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
trust: surface
|
||||
situation: [social, bar_evening]
|
||||
topic: [colleague, investigation]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [unprompted, genuine-help, phase-1-2]
|
||||
knowledge_grant:
|
||||
fact_id: "relationship.trust_network"
|
||||
confidence: "suspects"
|
||||
|
||||
- id: the-last-shift_d_151
|
||||
text: "I checked the relay calibration logs for the Terminal this morning. Nothing flagged. But the data throughput was higher than standard -- more manifests processed than usual."
|
||||
text: "I checked the relay calibration logs for the Terminal this morning. Nothing flagged. But the data throughput was higher than standard — more manifests processed than usual."
|
||||
role: bar-regular
|
||||
access: [peer, authority]
|
||||
trust: real
|
||||
situation: [social]
|
||||
topic: [institution, cargo]
|
||||
mood: [analytical]
|
||||
mood: [focused]
|
||||
tags: [unprompted, overcompensation, phase-3-4]
|
||||
|
||||
- id: the-last-shift_d_152
|
||||
text: "I asked around -- quietly -- about the shift rotation schedule. Voss has been adjusting it more than usual this cycle. Might mean nothing."
|
||||
text: "I asked around — quietly — about the shift rotation schedule. Voss has been adjusting it more than usual this cycle. Might mean nothing."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
trust: real
|
||||
situation: [social, bar_evening]
|
||||
topic: [routine, investigation]
|
||||
mood: [analytical, concerned]
|
||||
mood: [focused, frustrated]
|
||||
tags: [unprompted, overcompensation, phase-3-4]
|
||||
|
||||
- id: the-last-shift_d_153
|
||||
@@ -474,7 +474,7 @@ lines:
|
||||
trust: real
|
||||
situation: [social]
|
||||
topic: [institution, investigation]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [unprompted, information-offer, overcompensation, phase-3-5]
|
||||
|
||||
- id: the-last-shift_d_154
|
||||
@@ -484,20 +484,20 @@ lines:
|
||||
trust: real
|
||||
situation: [social, bar_evening]
|
||||
topic: [personal, colleague]
|
||||
mood: [concerned]
|
||||
mood: [frustrated]
|
||||
tags: [unprompted, naia-worry, phase-2-3]
|
||||
knowledge_grant:
|
||||
fact_id: "relationship.kael_naia_connection"
|
||||
confidence: "knows_details"
|
||||
|
||||
- id: the-last-shift_d_155
|
||||
text: "You should know -- Lera watches out for her regulars. If people have been asking about you, she'd know. She just won't volunteer it."
|
||||
text: "You should know — Lera watches out for her regulars. If people have been asking about you, she'd know. She just won't volunteer it."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
trust: real
|
||||
situation: [social, bar_evening]
|
||||
topic: [colleague, trust, danger]
|
||||
mood: [concerned]
|
||||
mood: [frustrated]
|
||||
tags: [unprompted, genuine-help, phase-2-3]
|
||||
|
||||
# ============================================================
|
||||
@@ -506,67 +506,67 @@ lines:
|
||||
# ============================================================
|
||||
|
||||
- id: the-last-shift_d_160
|
||||
text: "Actually, I should head out. Early shift tomorrow -- calibration rounds start at 07:30."
|
||||
text: "Actually, I should head out. Early shift tomorrow — calibration rounds start at 07:30."
|
||||
role: bar-regular
|
||||
access: [peer, public]
|
||||
trust: surface
|
||||
situation: [bar_evening]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [avoidance-excuse-1, torek-trigger, phase-3]
|
||||
|
||||
- id: the-last-shift_d_161
|
||||
text: "Sorry -- headache coming on. Station air does that sometimes. I'll catch you tomorrow."
|
||||
text: "Sorry — headache coming on. Station air does that sometimes. I'll catch you tomorrow."
|
||||
role: bar-regular
|
||||
access: [peer, public]
|
||||
trust: surface
|
||||
situation: [bar_evening]
|
||||
topic: [personal]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [avoidance-excuse-2, torek-trigger, phase-3]
|
||||
|
||||
- id: the-last-shift_d_162
|
||||
text: "I need to check something at the Commission kiosk. Forgot to file a diagnostic report. I'll be quick -- or not. You know how the system is."
|
||||
text: "I need to check something at the Commission kiosk. Forgot to file a diagnostic report. I'll be quick — or not. You know how the system is."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
trust: surface
|
||||
situation: [bar_evening]
|
||||
topic: [institution, routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [avoidance-excuse-3, torek-trigger, phase-3]
|
||||
|
||||
- id: the-last-shift_d_163
|
||||
text: "Torek? He's just loud. I don't love loud drunks. Nothing personal -- I just like a quieter evening."
|
||||
text: "Torek? He's just loud. I don't love loud drunks. Nothing personal — I just like a quieter evening."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
trust: surface
|
||||
situation: [social, investigation]
|
||||
topic: [colleague]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [torek-deflection, phase-4, the-deflection]
|
||||
|
||||
- id: the-last-shift_d_164
|
||||
text: "Manifests? I wouldn't know -- I'm on the equipment side, not the freight side. Different department entirely."
|
||||
text: "Manifests? I wouldn't know — I'm on the equipment side, not the freight side. Different department entirely."
|
||||
role: bar-regular
|
||||
access: [peer, authority]
|
||||
trust: surface
|
||||
situation: [investigation, social]
|
||||
topic: [cargo, institution]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [topic-deflection, cargo-avoidance, phase-3-5]
|
||||
|
||||
- id: the-last-shift_d_165
|
||||
text: "Anyway -- did you try the ale? Lera got a new supply run in. Much better than last week's batch."
|
||||
text: "Anyway — did you try the ale? Lera got a new supply run in. Much better than last week's batch."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
trust: surface
|
||||
situation: [social, bar_evening]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [redirect, warmth-recovery, phase-4]
|
||||
|
||||
- id: the-last-shift_d_166
|
||||
text: "I'm just a field tech. I maintain equipment. I don't track people or shipments -- that's your department."
|
||||
text: "I'm just a field tech. I maintain equipment. I don't track people or shipments — that's your department."
|
||||
role: bar-regular
|
||||
access: [peer, authority]
|
||||
trust: surface
|
||||
@@ -576,13 +576,13 @@ lines:
|
||||
tags: [deflection-firm, phase-4-5, the-wall]
|
||||
|
||||
- id: the-last-shift_d_167
|
||||
text: "Let's talk about something else. How's the investigation going -- generally? Making progress?"
|
||||
text: "Let's talk about something else. How's the investigation going — generally? Making progress?"
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
trust: real
|
||||
situation: [social, investigation]
|
||||
topic: [investigation, trust]
|
||||
mood: [conflicted]
|
||||
mood: [anxious]
|
||||
tags: [redirect-to-detective, controlled-recovery, phase-4]
|
||||
|
||||
# ============================================================
|
||||
@@ -598,7 +598,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [bar_evening, arrival]
|
||||
topic: [personal]
|
||||
mood: [fond]
|
||||
mood: [warm]
|
||||
tags: [post-discovery, surface-warmth, phase-5]
|
||||
|
||||
- id: the-last-shift_d_171
|
||||
@@ -608,7 +608,7 @@ lines:
|
||||
trust: real
|
||||
situation: [social]
|
||||
topic: [institution]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [post-discovery, overcompensation, phase-5]
|
||||
|
||||
- id: the-last-shift_d_172
|
||||
@@ -618,7 +618,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [bar_evening, social]
|
||||
topic: [personal, investigation]
|
||||
mood: [concerned]
|
||||
mood: [frustrated]
|
||||
tags: [post-discovery, awareness, phase-5]
|
||||
|
||||
- id: the-last-shift_d_173
|
||||
@@ -628,15 +628,74 @@ lines:
|
||||
trust: real
|
||||
situation: [social]
|
||||
topic: [trust, personal]
|
||||
mood: [fond, conflicted]
|
||||
mood: [warm, anxious]
|
||||
tags: [post-discovery, genuine-but-contaminated, phase-5]
|
||||
|
||||
- id: the-last-shift_d_174
|
||||
text: "If there's anything I can help with -- anything at all -- you just ask. You know that."
|
||||
text: "If there's anything I can help with — anything at all — you just ask. You know that."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
trust: real
|
||||
situation: [social, bar_evening]
|
||||
topic: [trust, personal]
|
||||
mood: [fond, concerned]
|
||||
mood: [warm, frustrated]
|
||||
tags: [post-discovery, overcompensation, phase-5, closing-line]
|
||||
|
||||
# ============================================================
|
||||
# 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.
|
||||
# Sera is THE FRIEND for the detective. Warm, observant, bar anchor.
|
||||
# ============================================================
|
||||
|
||||
# first_meeting (detective path): RelationshipState Unknown → Public/Authority access.
|
||||
# Sera clocks them as Commission immediately. Makes them welcome anyway.
|
||||
- id: the-last-shift_d_175
|
||||
text: "Commission? Don't worry, I don't bite. I'm Sera. Corner booth's better — you can see the whole room."
|
||||
role: bar-regular
|
||||
access: [public, authority]
|
||||
trust: surface
|
||||
situation: [greeting]
|
||||
mood: [warm]
|
||||
topic: [personal, institution]
|
||||
tags: [sera, greeting, first-meeting, detective-path, layer2]
|
||||
notes: "Layer 2 greeting — first meeting, detective path. Sera reads the institutional marking immediately and disarms it. Corner booth tip is genuine — Sera always notices the useful details."
|
||||
|
||||
# established (detective path): count >= 3, RelationshipState: Known/Friendly → Peer access.
|
||||
# Warmer, more personal. Sera has been waiting for them.
|
||||
- id: the-last-shift_d_176
|
||||
text: "I was hoping you'd come in. Saved the corner booth. How'd today go?"
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
trust: surface
|
||||
situation: [greeting]
|
||||
mood: [warm]
|
||||
topic: [personal, investigation]
|
||||
tags: [sera, greeting, repeat-visit, detective-path, layer2]
|
||||
notes: "Layer 2 greeting — established, detective path. Personal, invested. Sera asks about their day the way a friend does — not just small talk."
|
||||
|
||||
# first_meeting (smuggler path): smuggler is dock crew, Sera knows the district crowd.
|
||||
# Peer/insider access from the start — Sera has social network, not institutional distance.
|
||||
- id: the-last-shift_d_177
|
||||
text: "Terminal crew? Grab a seat. Kael usually sits in the back if you're looking for him."
|
||||
role: bar-regular
|
||||
access: [peer, insider]
|
||||
trust: surface
|
||||
situation: [greeting]
|
||||
mood: [content]
|
||||
topic: [colleague, personal]
|
||||
tags: [sera, greeting, first-meeting, smuggler-path, layer2]
|
||||
notes: "Layer 2 greeting — first meeting, smuggler path. Sera recognizes dock crew on sight and extends the social network. Mentioning Kael is casual intel — she knows everyone's connections."
|
||||
|
||||
# post-confrontation: confrontation logged. Sera's surface warmth is present but qualified.
|
||||
# She doesn't know where they stand. The welcome is real but uncertain.
|
||||
- id: the-last-shift_d_178
|
||||
text: "You're back. I wasn't sure you would be."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
trust: surface
|
||||
situation: [greeting]
|
||||
mood: [anxious, suspicious]
|
||||
topic: [personal, trust]
|
||||
tags: [sera, greeting, post-confrontation, layer2]
|
||||
notes: "Layer 2 greeting — post-confrontation. Sera's warmth is genuine but the relief is complicated. She genuinely didn't know if they'd come back. The relationship is still there but it's been stressed."
|
||||
|
||||
+18
-18
@@ -20,7 +20,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [arrival, routine]
|
||||
topic: [routine, cargo]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [renn, greeting, operational]
|
||||
|
||||
- id: the-terminal_d_501
|
||||
@@ -30,7 +30,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [arrival, routine]
|
||||
topic: [routine, cargo]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [renn, greeting, procedural]
|
||||
|
||||
- id: the-terminal_d_502
|
||||
@@ -40,7 +40,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [arrival, shift_start]
|
||||
topic: [routine, cargo]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [renn, greeting, cargo]
|
||||
|
||||
# ==========================================
|
||||
@@ -54,7 +54,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [renn, operational, brief]
|
||||
|
||||
- id: the-terminal_d_504
|
||||
@@ -64,7 +64,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine, investigation]
|
||||
topic: [routine, cargo]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [renn, procedural, documentation]
|
||||
|
||||
- id: the-terminal_d_505
|
||||
@@ -74,7 +74,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine, cargo]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [renn, operational]
|
||||
|
||||
- id: the-terminal_d_506
|
||||
@@ -84,7 +84,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [renn, operational]
|
||||
dual_lens:
|
||||
smuggler: "Clean phrase. Public-safe. He'd say it either way."
|
||||
@@ -154,7 +154,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [investigation, routine]
|
||||
topic: [institution, cargo]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [renn, detective-path, procedural]
|
||||
|
||||
- id: the-terminal_d_512
|
||||
@@ -164,7 +164,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [investigation]
|
||||
topic: [cargo, institution]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [renn, detective-path, deflection]
|
||||
dual_lens:
|
||||
smuggler: "That's the answer. Clean and correct."
|
||||
@@ -177,7 +177,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [investigation, confrontation]
|
||||
topic: [cargo, institution]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [renn, detective-path, authority-assertion]
|
||||
|
||||
# ==========================================
|
||||
@@ -191,7 +191,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [shift_end, routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [renn, closing]
|
||||
|
||||
- id: the-terminal_d_515
|
||||
@@ -201,7 +201,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [shift_end]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [renn, closing]
|
||||
|
||||
# ==========================================
|
||||
@@ -215,7 +215,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [arrival, routine]
|
||||
topic: [routine, cargo]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [renn, greeting, generation-pass]
|
||||
|
||||
- id: the-terminal_d_517
|
||||
@@ -225,7 +225,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [arrival, routine]
|
||||
topic: [routine, cargo]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [renn, greeting, procedural, generation-pass]
|
||||
|
||||
- id: the-terminal_d_518
|
||||
@@ -235,7 +235,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [arrival, routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [renn, delay, generation-pass]
|
||||
|
||||
- id: the-terminal_d_519
|
||||
@@ -245,7 +245,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine, investigation]
|
||||
topic: [routine, cargo]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [renn, procedural, generation-pass]
|
||||
|
||||
- id: the-terminal_d_520
|
||||
@@ -255,7 +255,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [arrival, routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [renn, operational, generation-pass]
|
||||
|
||||
- id: the-terminal_d_521
|
||||
@@ -265,5 +265,5 @@ lines:
|
||||
trust: surface
|
||||
situation: [shift_end, routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [renn, closing, generation-pass]
|
||||
|
||||
+33
-33
@@ -19,7 +19,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [shift_start, arrival]
|
||||
topic: [routine, cargo]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [dock-worker, greeting]
|
||||
|
||||
- id: the-terminal_d_201
|
||||
@@ -29,7 +29,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [arrival, shift_start]
|
||||
topic: [routine]
|
||||
mood: [tired]
|
||||
mood: [frustrated]
|
||||
tags: [dock-worker, greeting]
|
||||
|
||||
- id: the-terminal_d_202
|
||||
@@ -39,7 +39,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [arrival, shift_start]
|
||||
topic: [institution, routine]
|
||||
mood: [wary]
|
||||
mood: [suspicious]
|
||||
tags: [dock-worker, voss, caution]
|
||||
dual_lens:
|
||||
smuggler: "Voss is in early. If the schedule's tight, the window shifts. Check it."
|
||||
@@ -56,7 +56,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [cargo, routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [dock-worker, bay-four, procedural]
|
||||
dual_lens:
|
||||
smuggler: "He doesn't know what's in it. Just annoyed at the backup."
|
||||
@@ -69,7 +69,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine, shift_start]
|
||||
topic: [cargo, routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [dock-worker, manifest]
|
||||
|
||||
- id: the-terminal_d_205
|
||||
@@ -79,7 +79,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [colleague, routine]
|
||||
mood: [wary]
|
||||
mood: [suspicious]
|
||||
tags: [dock-worker, maret, observation]
|
||||
dual_lens:
|
||||
smuggler: "Maret reviewing something. If she flags a discrepancy, Voss'll sit on it. He always does."
|
||||
@@ -92,7 +92,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine, colleague]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [dock-worker, drin, inspection]
|
||||
dual_lens:
|
||||
smuggler: "Drin on six while bay four stays held. Scheduled or arranged?"
|
||||
@@ -105,7 +105,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [shift_transition, routine]
|
||||
topic: [institution, routine]
|
||||
mood: [wary]
|
||||
mood: [suspicious]
|
||||
tags: [dock-worker, commission, transition-window]
|
||||
dual_lens:
|
||||
smuggler: "Commission presence during transition. Window's compromised."
|
||||
@@ -118,7 +118,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine, shift_start]
|
||||
topic: [colleague, routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [dock-worker, pael, community]
|
||||
|
||||
- id: the-terminal_d_209
|
||||
@@ -128,7 +128,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [cargo, colleague]
|
||||
mood: [wary]
|
||||
mood: [suspicious]
|
||||
tags: [dock-worker, maret, voss, discrepancy]
|
||||
dual_lens:
|
||||
smuggler: "Forty kilos. Maret flagged it. Voss buried it. Same as last time."
|
||||
@@ -144,7 +144,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [cargo, routine]
|
||||
mood: [wary]
|
||||
mood: [suspicious]
|
||||
tags: [dock-worker, c4-storage, observation]
|
||||
dual_lens:
|
||||
smuggler: "C-4's getting casual attention. Note it."
|
||||
@@ -161,7 +161,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [shift_end, routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [dock-worker, social, the-last-shift]
|
||||
|
||||
- id: the-terminal_d_212
|
||||
@@ -171,7 +171,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [institution, routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [dock-worker, workers-association, community]
|
||||
|
||||
- id: the-terminal_d_213
|
||||
@@ -181,7 +181,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [dock-worker, social, pael]
|
||||
|
||||
# ==========================================
|
||||
@@ -195,7 +195,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [investigation, routine]
|
||||
topic: [institution, routine]
|
||||
mood: [wary]
|
||||
mood: [suspicious]
|
||||
tags: [dock-worker, detective-path, redirect]
|
||||
|
||||
- id: the-terminal_d_215
|
||||
@@ -205,7 +205,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [investigation, confrontation]
|
||||
topic: [institution, routine]
|
||||
mood: [wary]
|
||||
mood: [suspicious]
|
||||
tags: [dock-worker, detective-path, deflection]
|
||||
|
||||
- id: the-terminal_d_216
|
||||
@@ -215,7 +215,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [investigation]
|
||||
topic: [institution, routine]
|
||||
mood: [wary]
|
||||
mood: [suspicious]
|
||||
tags: [dock-worker, detective-path, voss, redirect]
|
||||
|
||||
# ==========================================
|
||||
@@ -229,7 +229,7 @@ lines:
|
||||
trust: real
|
||||
situation: [routine]
|
||||
topic: [colleague]
|
||||
mood: [wary]
|
||||
mood: [suspicious]
|
||||
tags: [dock-worker, maret, peer-candor]
|
||||
dual_lens:
|
||||
smuggler: "Maret's not going to drop it. That's the problem."
|
||||
@@ -242,7 +242,7 @@ lines:
|
||||
trust: real
|
||||
situation: [routine]
|
||||
topic: [institution, colleague]
|
||||
mood: [wary]
|
||||
mood: [suspicious]
|
||||
tags: [dock-worker, voss, peer-candor]
|
||||
dual_lens:
|
||||
smuggler: "He suspects Voss is shaving something. Doesn't know what. Doesn't want to."
|
||||
@@ -259,7 +259,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [shift_end]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [dock-worker, closing]
|
||||
|
||||
- id: the-terminal_d_220
|
||||
@@ -269,7 +269,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [shift_end, shift_transition]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [dock-worker, closing]
|
||||
|
||||
# ==========================================
|
||||
@@ -283,7 +283,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [arrival, shift_start]
|
||||
topic: [routine, cargo]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [dock-worker, greeting, generation-pass]
|
||||
|
||||
- id: the-terminal_d_222
|
||||
@@ -293,7 +293,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [arrival, shift_start]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [dock-worker, station, generation-pass]
|
||||
|
||||
- id: the-terminal_d_223
|
||||
@@ -303,7 +303,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine, shift_start]
|
||||
topic: [colleague, routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [dock-worker, pael, generation-pass]
|
||||
|
||||
- id: the-terminal_d_224
|
||||
@@ -313,7 +313,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [shift_start, routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [dock-worker, social, generation-pass]
|
||||
|
||||
- id: the-terminal_d_225
|
||||
@@ -323,7 +323,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [cargo, routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [dock-worker, scan-hold, generation-pass]
|
||||
|
||||
- id: the-terminal_d_226
|
||||
@@ -333,7 +333,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [colleague, routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [dock-worker, new-hire, generation-pass]
|
||||
|
||||
- id: the-terminal_d_227
|
||||
@@ -343,7 +343,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [shift_transition]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [dock-worker, handoff, generation-pass]
|
||||
|
||||
- id: the-terminal_d_228
|
||||
@@ -353,7 +353,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine, shift_start]
|
||||
topic: [routine, cargo]
|
||||
mood: [wary]
|
||||
mood: [suspicious]
|
||||
tags: [dock-worker, span-gate, generation-pass]
|
||||
|
||||
- id: the-terminal_d_229
|
||||
@@ -363,7 +363,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [shift_start, routine]
|
||||
topic: [colleague, routine]
|
||||
mood: [tired]
|
||||
mood: [frustrated]
|
||||
tags: [dock-worker, staffing, generation-pass]
|
||||
|
||||
- id: the-terminal_d_230
|
||||
@@ -373,7 +373,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine, colleague]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [dock-worker, pael, humor, generation-pass]
|
||||
|
||||
- id: the-terminal_d_231
|
||||
@@ -383,7 +383,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [dock-worker, overtime, generation-pass]
|
||||
|
||||
- id: the-terminal_d_232
|
||||
@@ -393,5 +393,5 @@ lines:
|
||||
trust: surface
|
||||
situation: [shift_end]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [dock-worker, closing, generation-pass]
|
||||
|
||||
+87
-31
@@ -18,7 +18,7 @@ lines:
|
||||
access: [insider, peer]
|
||||
trust: surface
|
||||
situation: [arrival, shift_start]
|
||||
mood: [fond]
|
||||
mood: [warm]
|
||||
topic: [colleague]
|
||||
tags: [kael, greeting, phase-1]
|
||||
|
||||
@@ -28,7 +28,7 @@ lines:
|
||||
access: [insider, peer]
|
||||
trust: surface
|
||||
situation: [shift_start]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
topic: [routine]
|
||||
tags: [kael, greeting, phase-1]
|
||||
|
||||
@@ -38,7 +38,7 @@ lines:
|
||||
access: [insider, peer]
|
||||
trust: surface
|
||||
situation: [shift_start]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
topic: [colleague, routine]
|
||||
tags: [kael, greeting, humor]
|
||||
|
||||
@@ -48,7 +48,7 @@ lines:
|
||||
access: [insider, peer]
|
||||
trust: surface
|
||||
situation: [shift_start, social]
|
||||
mood: [fond]
|
||||
mood: [warm]
|
||||
topic: [colleague]
|
||||
tags: [kael, greeting, phase-1]
|
||||
|
||||
@@ -58,7 +58,7 @@ lines:
|
||||
access: [peer]
|
||||
trust: surface
|
||||
situation: [arrival, social]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
topic: [personal]
|
||||
tags: [kael, greeting, casual]
|
||||
|
||||
@@ -69,7 +69,7 @@ lines:
|
||||
access: [public, authority]
|
||||
trust: surface
|
||||
situation: [arrival]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
topic: [routine]
|
||||
tags: [kael, greeting, outsider]
|
||||
|
||||
@@ -83,7 +83,7 @@ lines:
|
||||
access: [insider, peer]
|
||||
trust: surface
|
||||
situation: [routine, shift_start]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
topic: [routine, cargo]
|
||||
tags: [kael, operational]
|
||||
|
||||
@@ -93,7 +93,7 @@ lines:
|
||||
access: [public, peer]
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
topic: [cargo, routine]
|
||||
tags: [kael, operational]
|
||||
knowledge_grant:
|
||||
@@ -106,7 +106,7 @@ lines:
|
||||
access: [insider, peer]
|
||||
trust: surface
|
||||
situation: [shift_start]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
topic: [routine]
|
||||
tags: [kael, humor]
|
||||
|
||||
@@ -116,7 +116,7 @@ lines:
|
||||
access: [public, peer]
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
topic: [routine]
|
||||
tags: [kael, environmental]
|
||||
|
||||
@@ -126,7 +126,7 @@ lines:
|
||||
access: [insider, peer]
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
topic: [cargo, colleague]
|
||||
tags: [kael, operational, maret]
|
||||
|
||||
@@ -136,7 +136,7 @@ lines:
|
||||
access: [insider, peer]
|
||||
trust: surface
|
||||
situation: [shift_end, shift_transition]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
topic: [routine]
|
||||
tags: [kael, operational]
|
||||
|
||||
@@ -146,7 +146,7 @@ lines:
|
||||
access: [insider, peer]
|
||||
trust: surface
|
||||
situation: [social, routine]
|
||||
mood: [fond]
|
||||
mood: [warm]
|
||||
topic: [personal]
|
||||
tags: [kael, naia-reference, casual]
|
||||
|
||||
@@ -156,6 +156,7 @@ lines:
|
||||
access: [public, peer]
|
||||
trust: surface
|
||||
situation: [routine, social]
|
||||
mood: [content]
|
||||
topic: [colleague]
|
||||
tags: [kael, observational]
|
||||
|
||||
@@ -199,7 +200,7 @@ lines:
|
||||
access: [insider]
|
||||
trust: real
|
||||
situation: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
topic: [cargo]
|
||||
tags: [kael, ring-ops, maret, phase-2]
|
||||
|
||||
@@ -209,7 +210,7 @@ lines:
|
||||
access: [insider]
|
||||
trust: real
|
||||
situation: [social]
|
||||
mood: [concerned]
|
||||
mood: [frustrated]
|
||||
topic: [danger, trust]
|
||||
tags: [kael, ring-ops, nils, tension, phase-2]
|
||||
|
||||
@@ -219,6 +220,7 @@ lines:
|
||||
access: [insider]
|
||||
trust: real
|
||||
situation: [routine]
|
||||
mood: [content]
|
||||
topic: [colleague]
|
||||
tags: [kael, ring-ops, drin, phase-2]
|
||||
knowledge_grant:
|
||||
@@ -231,7 +233,7 @@ lines:
|
||||
access: [insider]
|
||||
trust: real
|
||||
situation: [routine, social]
|
||||
mood: [worried]
|
||||
mood: [anxious]
|
||||
topic: [colleague, danger]
|
||||
tags: [kael, ring-ops, maret, caution, phase-2]
|
||||
|
||||
@@ -241,7 +243,7 @@ lines:
|
||||
access: [insider]
|
||||
trust: real
|
||||
situation: [shift_transition]
|
||||
mood: [concerned]
|
||||
mood: [frustrated]
|
||||
topic: [cargo, danger]
|
||||
tags: [kael, ring-ops, contraband, phase-2]
|
||||
|
||||
@@ -255,7 +257,7 @@ lines:
|
||||
access: [insider]
|
||||
trust: secret
|
||||
situation: [social, alone]
|
||||
mood: [conflicted]
|
||||
mood: [anxious]
|
||||
topic: [trust, personal]
|
||||
tags: [kael, vulnerability, friend-arc]
|
||||
|
||||
@@ -265,7 +267,7 @@ lines:
|
||||
access: [insider]
|
||||
trust: secret
|
||||
situation: [social, alone]
|
||||
mood: [conflicted]
|
||||
mood: [anxious]
|
||||
topic: [personal, trust]
|
||||
tags: [kael, naia-reference, vulnerability]
|
||||
|
||||
@@ -275,7 +277,7 @@ lines:
|
||||
access: [insider]
|
||||
trust: secret
|
||||
situation: [alone]
|
||||
mood: [worried]
|
||||
mood: [anxious]
|
||||
topic: [personal, trust]
|
||||
tags: [kael, naia-reference, vulnerability, friend-arc]
|
||||
|
||||
@@ -289,7 +291,7 @@ lines:
|
||||
access: [insider, peer]
|
||||
trust: surface
|
||||
situation: [shift_start]
|
||||
mood: [concerned]
|
||||
mood: [frustrated]
|
||||
topic: [colleague, danger]
|
||||
tags: [kael, unprompted, warning]
|
||||
|
||||
@@ -299,7 +301,7 @@ lines:
|
||||
access: [insider, peer]
|
||||
trust: surface
|
||||
situation: [shift_end, social]
|
||||
mood: [fond]
|
||||
mood: [warm]
|
||||
topic: [personal, colleague]
|
||||
tags: [kael, unprompted, social]
|
||||
|
||||
@@ -309,7 +311,7 @@ lines:
|
||||
access: [insider]
|
||||
trust: real
|
||||
situation: [social]
|
||||
mood: [worried]
|
||||
mood: [anxious]
|
||||
topic: [danger, colleague]
|
||||
tags: [kael, unprompted, torek, caution]
|
||||
|
||||
@@ -323,7 +325,7 @@ lines:
|
||||
access: [insider, peer]
|
||||
trust: surface
|
||||
situation: [confrontation, social]
|
||||
mood: [concerned]
|
||||
mood: [frustrated]
|
||||
topic: [personal]
|
||||
tags: [kael, deflection, phase-4]
|
||||
|
||||
@@ -333,7 +335,7 @@ lines:
|
||||
access: [insider, peer]
|
||||
trust: surface
|
||||
situation: [confrontation]
|
||||
mood: [concerned]
|
||||
mood: [frustrated]
|
||||
topic: [personal, trust]
|
||||
tags: [kael, deflection, phase-4]
|
||||
|
||||
@@ -343,7 +345,7 @@ lines:
|
||||
access: [insider, peer]
|
||||
trust: surface
|
||||
situation: [confrontation]
|
||||
mood: [concerned]
|
||||
mood: [frustrated]
|
||||
topic: [routine]
|
||||
tags: [kael, deflection, lying, phase-4]
|
||||
|
||||
@@ -353,7 +355,7 @@ lines:
|
||||
access: [insider, peer]
|
||||
trust: surface
|
||||
situation: [confrontation, shift_start]
|
||||
mood: [concerned]
|
||||
mood: [frustrated]
|
||||
topic: [routine, trust]
|
||||
tags: [kael, deflection, subject-change, phase-4]
|
||||
|
||||
@@ -363,7 +365,7 @@ lines:
|
||||
access: [insider]
|
||||
trust: surface
|
||||
situation: [confrontation]
|
||||
mood: [concerned]
|
||||
mood: [frustrated]
|
||||
topic: [danger]
|
||||
tags: [kael, deflection, phase-4]
|
||||
|
||||
@@ -378,7 +380,7 @@ lines:
|
||||
access: [insider]
|
||||
trust: real
|
||||
situation: [confrontation, alone]
|
||||
mood: [conflicted]
|
||||
mood: [anxious]
|
||||
topic: [trust, personal]
|
||||
tags: [kael, confession-path, phase-5]
|
||||
|
||||
@@ -389,7 +391,7 @@ lines:
|
||||
access: [insider]
|
||||
trust: surface
|
||||
situation: [confrontation]
|
||||
mood: [concerned]
|
||||
mood: [frustrated]
|
||||
topic: [danger, trust]
|
||||
tags: [kael, hostile-path, phase-5]
|
||||
|
||||
@@ -400,7 +402,7 @@ lines:
|
||||
access: [insider]
|
||||
trust: secret
|
||||
situation: [confrontation, alone]
|
||||
mood: [conflicted]
|
||||
mood: [anxious]
|
||||
topic: [personal, trust]
|
||||
tags: [kael, confession, naia-reference, phase-5]
|
||||
|
||||
@@ -422,3 +424,57 @@ lines:
|
||||
situation: [confrontation, investigation]
|
||||
topic: [colleague, routine]
|
||||
tags: [kael, deflection, detective-path, voss]
|
||||
|
||||
# ========================================
|
||||
# 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: the-terminal_d_050
|
||||
text: "This section is dock crew and logistics. If you're Commission or admin, check-in's back that way."
|
||||
role: dock-worker
|
||||
access: [public]
|
||||
trust: surface
|
||||
situation: [greeting]
|
||||
mood: [content]
|
||||
topic: [routine]
|
||||
tags: [kael, greeting, first-meeting, layer2]
|
||||
notes: "Layer 2 greeting — first meeting. Kael is helpful but orienting the stranger away from his work."
|
||||
|
||||
# established — peer/smuggler context: count >= 3, RelationshipState: Known/Friendly → Peer/Insider access.
|
||||
- id: the-terminal_d_051
|
||||
text: "You're back. Good — bay three's been quiet. I'll fill you in."
|
||||
role: dock-worker
|
||||
access: [peer, insider]
|
||||
trust: surface
|
||||
situation: [greeting]
|
||||
mood: [warm]
|
||||
topic: [colleague, routine]
|
||||
tags: [kael, greeting, repeat-visit, peer-tone, layer2]
|
||||
notes: "Layer 2 greeting — established. Warm, practical, skips pleasantries. Treats them as crew."
|
||||
|
||||
# established — authority/detective context: count >= 3, RelationshipState: PersonOfInterest → Authority access.
|
||||
- id: the-terminal_d_052
|
||||
text: "Back again. I'm on shift — if you have questions, make them quick."
|
||||
role: dock-worker
|
||||
access: [authority]
|
||||
trust: surface
|
||||
situation: [greeting]
|
||||
mood: [focused]
|
||||
topic: [routine]
|
||||
tags: [kael, greeting, repeat-visit, authority-tone, layer2]
|
||||
notes: "Layer 2 greeting — repeat with authority figure. Professional, clipped, not hostile. Kael can't refuse but won't volunteer."
|
||||
|
||||
# post-confrontation: confrontation in InteractionMemory.notable_events. NPC mood shifted to suspicious/frustrated.
|
||||
- id: the-terminal_d_053
|
||||
text: "Didn't expect you today."
|
||||
role: dock-worker
|
||||
access: [peer, insider]
|
||||
trust: surface
|
||||
situation: [greeting]
|
||||
mood: [suspicious, frustrated]
|
||||
topic: [personal]
|
||||
tags: [kael, greeting, post-confrontation, layer2]
|
||||
notes: "Layer 2 greeting — post-confrontation. Strained. Four words doing the work of a paragraph. Not hostile but the warmth is gone."
|
||||
|
||||
+6
-6
@@ -21,7 +21,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [pael, seems-important-but-isnt, maintenance, b7]
|
||||
dual_lens:
|
||||
smuggler: "Pael knows the corridor's been used. But Pael knows nothing — this is an HVAC complaint."
|
||||
@@ -42,7 +42,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [pael, atmospheric]
|
||||
|
||||
- id: the-terminal_d_042
|
||||
@@ -52,7 +52,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [pael, atmospheric]
|
||||
|
||||
- id: the-terminal_d_043
|
||||
@@ -62,7 +62,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [pael, warning, helpful]
|
||||
|
||||
- id: the-terminal_d_044
|
||||
@@ -72,7 +72,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine, social]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [pael, response]
|
||||
|
||||
- id: the-terminal_d_045
|
||||
@@ -82,5 +82,5 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [pael, atmospheric]
|
||||
|
||||
+21
-21
@@ -19,7 +19,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [arrival, shift_start]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [new-hire, greeting, orientation]
|
||||
|
||||
- id: the-terminal_d_401
|
||||
@@ -29,7 +29,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [arrival, shift_start]
|
||||
topic: [routine, institution]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [new-hire, greeting, voss]
|
||||
|
||||
- id: the-terminal_d_402
|
||||
@@ -39,7 +39,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [arrival, shift_start]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [new-hire, greeting]
|
||||
|
||||
# ==========================================
|
||||
@@ -53,7 +53,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [cargo, routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [new-hire, bay-four, observation]
|
||||
dual_lens:
|
||||
smuggler: "New eyes on bay four. He doesn't know what he's looking at. But he noticed."
|
||||
@@ -66,7 +66,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [cargo, routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [new-hire, c4-storage, observation, inadvertent-witness]
|
||||
dual_lens:
|
||||
smuggler: "He saw someone in C-4 without a log. Does he know who? This is a problem."
|
||||
@@ -83,7 +83,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine, shift_transition]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [new-hire, transition-window, comparison]
|
||||
dual_lens:
|
||||
smuggler: "He's asking about the window. Doesn't know why it matters. Answer carefully."
|
||||
@@ -96,7 +96,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [cargo, routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [new-hire, bay-four, inadvertent-witness]
|
||||
dual_lens:
|
||||
smuggler: "He saw movement through bay four during the hold. This is a liability."
|
||||
@@ -112,7 +112,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [new-hire, maintenance-corridors, orientation]
|
||||
dual_lens:
|
||||
smuggler: "He found the corridor entrance by accident. Normal for new workers. Noted."
|
||||
@@ -129,7 +129,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [institution, routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [new-hire, commission, curiosity]
|
||||
|
||||
- id: the-terminal_d_409
|
||||
@@ -139,7 +139,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [colleague]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [new-hire, maret, observation]
|
||||
dual_lens:
|
||||
smuggler: "He noticed Maret. Good that he's asking, not reporting. Manage this."
|
||||
@@ -152,7 +152,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [colleague]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [new-hire, voss, curiosity]
|
||||
|
||||
- id: the-terminal_d_411
|
||||
@@ -162,7 +162,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [institution, routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [new-hire, voss, career]
|
||||
|
||||
# ==========================================
|
||||
@@ -176,7 +176,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [shift_end]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [new-hire, closing]
|
||||
|
||||
- id: the-terminal_d_413
|
||||
@@ -186,7 +186,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [shift_end]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [new-hire, closing]
|
||||
|
||||
# ==========================================
|
||||
@@ -200,7 +200,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine, shift_start]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [new-hire, orientation, generation-pass]
|
||||
|
||||
- id: the-terminal_d_415
|
||||
@@ -210,7 +210,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [colleague]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [new-hire, maret, curiosity, generation-pass]
|
||||
|
||||
- id: the-terminal_d_416
|
||||
@@ -220,7 +220,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine, arrival]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [new-hire, orientation, generation-pass]
|
||||
|
||||
- id: the-terminal_d_417
|
||||
@@ -230,7 +230,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [new-hire, social, generation-pass]
|
||||
|
||||
- id: the-terminal_d_418
|
||||
@@ -240,7 +240,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [new-hire, orientation, humor, generation-pass]
|
||||
|
||||
- id: the-terminal_d_419
|
||||
@@ -250,7 +250,7 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [colleague]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [new-hire, drin, curiosity, generation-pass]
|
||||
|
||||
- id: the-terminal_d_420
|
||||
@@ -260,5 +260,5 @@ lines:
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [colleague]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [new-hire, voss, humor, generation-pass]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user