Compare commits

...
54 Commits
Author SHA1 Message Date
jpmschweitzerandClaude Opus 4.6 cc43f14c39 chore(meta): release v0.1.15
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 16:24:23 +01:00
jpmschweitzerandClaude Opus 4.6 0bf5616fcd chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 15:56:20 +01:00
jpmschweitzerandClaude Opus 4.6 e1cbcc243b docs(sprints): add sprint 16 "Converse" briefings
Plan 8 tickets across server (4), client (2), copy (1), visual (1).
Focus: two-way dialogue exchange, trust-gated gossip, sprite pipeline,
and housekeeping fixes. No carry-overs from sprint 15.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 15:56:10 +01:00
jpmschweitzerandClaude Opus 4.6 07199c8ddc chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 15:36:45 +01:00
jpmschweitzerandClaude Opus 4.6 b97f65e8ab docs(decisions): D-035 amendment — NPC-scoped line ID namespace
Sprint 15 amendment: line ID namespace changed from location-scoped
to NPC-scoped. Each NPC's IDs are independent, eliminating cross-file
coordination and collision risk at scale.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 15:36:29 +01:00
jpmschweitzerandClaude Opus 4.6 fe71b11315 chore(skills): update pr-review to use worktree paths for all agents
All reviewer agents now read source files from worktree paths using
the Read tool instead of git show. Removes Bash dependency for
non-code reviewers and ensures files are always the actual branch
checkout.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 15:36:24 +01:00
jpmschweitzerandClaude Opus 4.6 0880e8d987 feat(client): permanent dialogue panel + protocol v13 + build check
- dialogue_box.gd: panel is always visible as permanent insert UI
  element per D-061 — content fades but frame stays on screen
- protocol.gd: bump PROTOCOL_VERSION to 13
- Makefile: add check-protocol target that verifies server/client
  protocol versions match, runs automatically before build

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 15:36:18 +01:00
jpmschweitzerandClaude Opus 4.6 eebc151b9f docs(briefings): add diagram responsibilities to Qatux and project docs
- CLAUDE.md: add docs/diagrams/ to project structure, add diagram
  file conventions for D-record changes
- qatux.md: add priority 5 for diagram creation/updates
- workshop-start SKILL.md: add diagram step in wrap-up sequence

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 15:36:12 +01:00
jpmschweitzerandClaude Opus 4.6 996a000146 docs(ui): add 19 UI wireframes across 5 categories (v0.1 + v1.0)
First wireframe set covering all major UI surfaces:
- HUD: basic insert (v0.1), full insert with perception modes (v1.0)
- Dialogue: speaking, choosing, overheard states (v0.1), full with
  relationship indicators (v1.0)
- Monologue: idle, reaction (v0.1), thought threading (v1.0)
- Popups: modal, tooltip, interaction list (v0.1), context menu,
  notification stack (v1.0)
- Menus: main, pause, save/load (v0.1), main menu, options (v1.0)

README.md cross-references each wireframe to governing D-records.
JSON source + PNG exports tracked in git.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 15:36:06 +01:00
jpmschweitzerandClaude Opus 4.6 30a21e9a08 chore(skills): rewrite frame0-wireframe skill with JSON sync workflow
Replace individual API call workflow with JSON-as-truth architecture.
Local wireframe JSON files are the source of truth; Frame0 is treated
purely as a renderer. New frame0-sync.py handles push/pull/export with
stable local ID <-> ephemeral Frame0 ID mapping via .idmap.json files.

Key changes:
- frame0-sync.py: push/pull/export with topo-sorted shape creation
- frame0-export-batch.sh: batch export with staleness check
- frame0-cmd.sh: fixed command names, list-pages JSON bug, added commands
- Removed frame0-wireframe.sh (superseded by JSON workflow)
- SKILL.md: renderer-only guidance, simplified workflow
- api-reference.md: corrected commands, type/color token mapping
- component-library.md: all 6 patterns rewritten as JSON templates
- .gitignore: added *.idmap.json

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 15:35:57 +01:00
jpmschweitzerandClaude Opus 4.6 18b852fae6 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 11:57:20 +01:00
jpmschweitzerandClaude Opus 4.6 1e556ccca2 docs(diagrams): add decision diagrams across all domains
16 d2 diagrams covering architecture (IPC bridge, system tiers,
testability, audio bus), data flow (perception, dialogue, eavesdrop
pipelines), entity (knowledge graph, NPC generation, dialogue tags),
state (simulation tiers, movement stances, knowledge confidence), and
UI (HUD layout, interaction flow, fog layers). PNG output for universal
viewer compatibility. Removes test SVG from font debugging.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 11:57:07 +01:00
jpmschweitzerandClaude Opus 4.6 08dfadb863 chore(skills): add frame0-wireframe skill for UI wireframing
Replaces Frame0 MCP server with portable bash+curl scripts that call
Frame0's local HTTP API directly. Includes low-level API wrapper,
high-level composition helpers with project styling, component library
patterns, and setup guide. Requires Frame0 desktop app running locally.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 11:57:00 +01:00
jpmschweitzerandClaude Opus 4.6 1d4b9cf299 chore(skills): add d2-diagram skill for text-to-diagram generation
Wraps d2 CLI (v0.7.1) with project defaults: theme 200 (Dark Mauve),
dagre layout, PNG output. Includes render/validate/batch scripts,
syntax guide, and five category templates (architecture, entity,
data-flow, state, UI flow).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 11:56:53 +01:00
jpmschweitzerandClaude Opus 4.6 d643ab7322 test: SVG with stripped embedded fonts for Gitea inline render check
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 11:41:53 +01:00
jpmschweitzer 4d7d77dc3a merge: resolve CHANGELOG.md conflict from server branch 2026-02-21 15:13:29 +01:00
jpmschweitzer 687beba3ce Merge remote-tracking branch 'origin/copy' 2026-02-21 15:13:10 +01:00
jpmschweitzerandClaude Opus 4.6 e166409674 fix(copy): PR #53 review round 3 — span gate terminology, mood tags, em-dash consistency
Fix "span gate threshold" → "span gate hum" (D-036 canonical term) in
detective opening, add missing mood tags on opening_m_d_002 and
the-terminal_d_020, normalize sera-venn double-dashes to em-dashes,
fix situations.yaml header count, document greeting fallback behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 15:03:06 +01:00
jpmschweitzerandClaude Opus 4.6 67cadddaf2 fix(simulation): address PR #55 review — stale comment, range, duplication, docs
- Fix protocol version comment (12 → 13) in ObserverSnapshot doc
- Widen Want intensity range from 3..=9 to 1..=10 to match spec and test
- Replace duplicated pool selection in trigger_recognition_monologue with
  call to select_pool_line helper (~50 lines removed)
- Document NaiveSpatialIndex migration cost for grid/quadtree swap
- Remove misleading Default derive from TellCategory (Nervous is not a
  sensible default for neutral NPCs)
- Add caller invariant doc on generate_npc (no TilePosition spawned)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 14:55:21 +01:00
jpmschweitzerandClaude Opus 4.6 4be9cde3fa fix(copy): PR #53 review round 2 — ID collision, wary/tired moods, greeting enum
- Renumber Kael greeting IDs _d_046-049 → _d_050-053 (pc-detective.yaml holds
  046-048, Hoshe R2-H1)
- Replace pre-existing invalid mood values: wary→suspicious (22 lines),
  tired→frustrated (4 lines) across 5 dialogue files (Hoshe R2-H2)
- Add greeting to situations.yaml enum with server-pending note (Hoshe R2-H3)
- Fix double-dash → em-dash on Kael greeting lines (Hoshe R2-H5)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 14:48:03 +01:00
jpmschweitzerandClaude Opus 4.6 3f799ad8ba docs(decisions): update D-076 dialogue max-width from 640px to 1200px
Reflects the intentional override — 1200px chosen for readability
after playtest feedback. Updates decision text, derivation, and
amendment history.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 14:46:02 +01:00
jpmschweitzerandClaude Opus 4.6 062ae88803 fix(client): revert DIALOGUE_MAX_WIDTH to 1200px (intentional override of D-076)
The 1200px value was a deliberate readability decision, not a spec
violation. Reverts the incorrect 640px change from round 1. Updates
comment and regression test to match.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 14:44:34 +01:00
jpmschweitzerandClaude Opus 4.6 78c8bb97de fix(client): address PR #54 round 2 — stale comments, D-076 test, GROUND_FLOOR const
Remove stale smoothing re-enable comments from main.gd (Hoshe #1).
Add DIALOGUE_MAX_WIDTH=640 regression test (Hoshe #2).
Extract GROUND_FLOOR const in tile_renderer (Tyre #3).
Clean up entity_renderer migration comment (Hoshe #3).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 14:42:25 +01:00
jpmschweitzerandClaude Opus 4.6 c0fb316a0a chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 14:40:38 +01:00
jpmschweitzerandClaude Opus 4.6 46cf744e6a data(client): update msgpack fixtures and golden files for protocol v13
Regenerate all msgpack test fixtures with tell_state and follow_state
fields. Update golden proof_room snapshot. Adjust serialization and
bridge tests for new ObserverSnapshot fields.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 14:40:18 +01:00
jpmschweitzerandClaude Opus 4.6 8538f916af feat(simulation): wire Sprint 15 systems + protocol v13
Register all new systems in NpcPlugin and SimulationPlugin with correct
ordering constraints. Protocol bumped to v13: tell_state on VisibleEntity,
follow_state and PostConversationQueue on ObserverSnapshot, Follow verb
on VerbKind. Observer snapshot populates tell state from DerivedTellState
and follow state from FollowTarget. System ordering: tolerance after
mood, deviation after activity, tell after mood+deviation, follow after
visibility geometry, event monologue after conversations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 14:40:09 +01:00
jpmschweitzerandClaude Opus 4.6 517318c8a7 feat(simulation): extend monologue event generation with new triggers (#119)
Add observe_npc, hear_sound, observe_anomaly, witness_interaction, and
post_conversation triggers to monologue system. trigger_event_monologue
system fires after sound collection, NPC conversations, and walk-away.
Context tags (location, situation, character_state) populated for D-035
content pool matching. COOLDOWN_TICKS=300 anti-spam guard respected.
witness_interaction fires after overheard NPC-to-NPC conversation per
D-078. Voice sounds and ambient sounds correctly excluded from
hear_sound trigger.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 14:39:58 +01:00
jpmschweitzerandClaude Opus 4.6 0af86b1f0b feat(simulation): add follow mechanic (#241)
Follow verb on interaction dispatcher. FollowTarget component tracks
target entity, proximity ticks, and LOS-lost ticks. update_follow_state
system: observation events fire at double frequency while following,
NPC suspicion increases via stress when player within 2 tiles for 60+
ticks (configurable). Follow ends on LOS lost timeout, suspicion
threshold crossed, or player issues different action. FollowStateWire
emitted in ObserverSnapshot for client HUD.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 14:39:47 +01:00
jpmschweitzerandClaude Opus 4.6 f31be6f866 feat(simulation): add routine deviation detection (#243)
RoutineDeviationEvent emitted when NPC breaks daily routine: wrong
location for day phase or wrong activity at correct location.
CurrentlyDeviating marker prevents duplicate events per episode.
Respects pathfinding-in-progress (no false positives). Feeds
observation event generator for observe_anomaly monologue triggers.
Primary detective mechanic per D-027 criterion 4.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 14:39:37 +01:00
jpmschweitzerandClaude Opus 4.6 029337a801 feat(simulation): add personality and tell system (#90)
TellCategory enum (Nervous, Angry, Friendly, Guarded, RoutineDeviation)
with DerivedTellState component. derive_tell_state system runs after
update_mood and detect_routine_deviation. Tell state derived from NPC
axis values per D-024: Secret+low Tolerance→Nervous, low Contentment+
Hostile→Angry, high Contentment+Friendly→Friendly, high Secret→Guarded.
v0.1 renderer is monologue text, not visual animation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 14:39:27 +01:00
jpmschweitzerandClaude Opus 4.6 78c43ab802 feat(simulation): add NPC generation pipeline (#92)
RoleDefinition struct + generate_npc() seeds all 10 D-024 axes via
SimRng for deterministic generation. Constraint validation: no duplicate
relationship targets, stress < threshold, one routine entry per phase,
no contradictory personality trait pairs. Spawns fully-populated NPC
entity with 2-3 personality traits for tell system (#90).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 14:39:17 +01:00
jpmschweitzerandClaude Opus 4.6 9a20e4dea9 feat(simulation): add tolerance threshold monitoring system (#105)
ToleranceBreachEvent emitted when NPC stress exceeds per-seed threshold.
ToleranceBreached marker prevents duplicate events per episode, cleared
on recovery. check_tolerance_threshold system runs after update_mood,
integrates with mood FSM to push toward Hostile/Anxious. Background-tier
NPCs excluded. Unblocks #250 (triangle escalation).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 14:39:08 +01:00
jpmschweitzerandClaude Opus 4.6 836c8e98cd feat(simulation): add SpatialIndex trait with naive Vec implementation (#340)
Define SpatialIndex trait with entities_in_range, entities_at, and
update methods. NaiveSpatialIndex uses Vec backend with Manhattan
distance. sync_spatial_index system auto-updates from Changed<TilePosition>.
Registered as Bevy resource. Trait abstraction allows grid/quadtree
replacement later without touching callers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 14:38:59 +01:00
jpmschweitzerandClaude Opus 4.6 9e33a82e1d fix(copy): PR #53 review — ID collision, moods.yaml enum, voice/lore fixes
- Renumber Kael greeting variants _d_039-042 → _d_046-049 (collision with
  pc-smuggler.yaml, Hoshe #1)
- Update moods.yaml enum to Sprint 14 vocabulary (Paula #1, Miri #1)
- Add missing mood field on the-terminal_d_014 (Hoshe #3)
- Fix "wormhole threshold" → "span gate threshold" in detective opening (Miri #2)
- Fix "We've worked this district before" — detective is newly arrived (Miri #5)
- Fix smuggler opening "That's Kael" label → behavior-read (Paula #5)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 14:22:34 +01:00
jpmschweitzerandClaude Opus 4.6 c347f28cbd fix(client): address PR #54 review — 7 items across Hoshe and Tyre
Critical: DIALOGUE_MAX_WIDTH 1200 → 640 to match D-076 spec.
tile_renderer: clarify z = server floor level, not scene z_index.
Add z-filter unit test (tiles at z!=0 must be skipped).
Camera test: is_equal → distance check for float safety, convergence
test frames 40 → 120 for robustness at lower smoothing speeds.
Teleport: remove redundant first snap in _teleport_transition (the
camera block in _process handles it via _teleport_in_progress flag).
entity_renderer: document y-sort bottom-anchor migration path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 14:20:27 +01:00
jpmschweitzerandClaude Opus 4.6 519483a0cc chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 14:10:20 +01:00
jpmschweitzerandClaude Opus 4.6 e616af6eca test(client): sprint 15 validation tests — camera, UI framework, entities
Add test_smooth_camera_sprint15.gd (11 tests): lerp convergence,
teleport snap, D-015 fixed-north, constant range validation.
Add test_ui_framework_sprint15.gd (20 tests): D-049 z-layer hierarchy,
OQ-07 insert_active, #241 follow stub, Sprint 14 regression checks.
Update test_camera_anchor.gd and test_client_p3.gd for ENTITY_OFFSET_X/Y
split and manual lerp camera behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 14:10:06 +01:00
jpmschweitzerandClaude Opus 4.6 176052dfd6 feat(client): manual exponential camera smoothing (#117)
Replace Godot built-in Camera2D smoothing with manual lerp using
CAMERA_SMOOTHING_SPEED (8.0) in constants.gd — same exponential
smoothing pattern as entity_renderer.gd. Teleport snap preserved
via _teleport_in_progress flag. D-015 fixed-north camera lock intact.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 14:09:58 +01:00
jpmschweitzerandClaude Opus 4.6 7ddee15f06 feat(client): tilemap z-filtering, entity 24x32 footprint, follow stub (#71, #72)
tile_renderer: only render z=0 tiles on FloorTiles layer (D-049 z-stack).
entity_renderer: fix footprint from 24x24 to 24x32 per D-044, split
ENTITY_SIZE into ENTITY_WIDTH/ENTITY_HEIGHT with separate offsets.
game_state: add follow_target_id stub for server ticket #241.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 14:09:53 +01:00
jpmschweitzerandClaude Opus 4.6 378c4945e7 feat(copy): #170 Layer 2 relationship history — greeting variants + situation mapping
New: docs/design/layer2-situation-mapping.md — full InteractionMemory→Layer 2
pipeline documentation, 5 relationship history states, authoring checklist.

Greeting variants added:
- Kael Davan: 12 lines (4 per location × 3 locations)
- Sera Venn: 4 lines at The Last Shift
All use situation: [greeting], correct Sprint 14 mood vocabulary.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 14:05:40 +01:00
jpmschweitzerandClaude Opus 4.6 458e50eee0 fix(copy): #169 access tier audit — mood vocab rename + monologue structural tags
Part A: Sprint 14 mood vocabulary applied across 20 dialogue files (334 fixes):
  fond→warm, comfortable→content, worried→anxious, concerned→frustrated,
  analytical→focused, conflicted→anxious/suspicious per context.

Part B: All 12 monologue pool files now carry required D-035 structural tags
  (role, access, trust, situation) — 223 lines fixed.

Part C: Full access tag audit passed — all dialogue lines have valid tags.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 14:05:29 +01:00
jpmschweitzerandClaude Opus 4.6 6dc3a123e6 feat(copy): #299 #300 opening hook monologue — smuggler + detective first 5 minutes
Smuggler: 11-line diegetic tutorial arc (station hum → Kael → fog → sound ping)
Detective: 12-line analytical tutorial arc (lattice overlay → Sera → manifest anomaly)

Both sequences: priority 8-10, cooldown 9999 (fire once), prerequisites null,
fully D-035 schema-compliant. Voice-checked against docs/design/voice-patterns.md.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 14:05:17 +01:00
jpmschweitzer eb6b5cc337 Merge remote-tracking branch 'origin/main' into client
# Conflicts:
#	client/data/dialogue-theme.yaml
#	client/scripts/main.gd
#	client/scripts/protocol/protocol.gd
#	client/scripts/ui/debug_overlay.gd
#	client/ui/dialogue_box.gd
2026-02-21 13:48:08 +01:00
jpmschweitzerandClaude Opus 4.6 c01ad011ed docs(sprints): add Sprint 15 React briefings
Server (7), client (5), copy (2) tickets. Goal: player acts and
world reacts — follow/examine, dialogue access layers, server-side
monologue events, personality and tell system.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 13:43:49 +01:00
jpmschweitzerandClaude Opus 4.6 aa97ef988a chore(client): track Godot-generated uid and import files
New .uid files from Godot 4 and updated .import metadata for
audio assets and fonts added during Sprint 14.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 13:43:43 +01:00
jpmschweitzerandClaude Opus 4.6 e1e9f3479a chore(skills): add bug-report processing skill
Skill for triaging in-game bug reports from the Godot client's
user://bug-reports/ directory. Reads snapshot, render, inputs,
and description files; presents summaries; offers investigate,
create ticket, or dismiss actions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 13:43:36 +01:00
jpmschweitzerandClaude Opus 4.6 136d21ac8b chore(config): specify --bin in Makefile cargo run targets
Disambiguates binary when multiple bin targets exist in the
server crate.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 13:43:29 +01:00
jpmschweitzerandClaude Opus 4.6 038f97f07b data(client): update msgpack fixtures for protocol v12
Regenerated all cross-language test fixtures to include new
conversation_events wire fields (speaker/target name, color index).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 13:43:24 +01:00
jpmschweitzerandClaude Opus 4.6 2c69e557c2 fix(client): resolve WASD freezes, warnings, and chime spam
- bug_report_dialog: release_focus() before queue_free() prevents
  stale focus on ModalLayer blocking input to lower layers
- dialogue_box: clear dialogue_active immediately on conversation
  end instead of holding for entry_lifetime
- main.gd: permanent entity ID tracking per room stops recognition
  chime re-triggering when NPCs oscillate in/out of fog queue
- audio_manager: correct path res://audio/ to res://assets/audio/
- world_radial.tscn: anchors_preset 15 to 0 (script sets size)
- bug_report_dialog: push_warning to print (informational)
- debug_overlay: max() to maxi() for integer line count
- monologue_display: remove unnecessary Tween cast

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 13:43:18 +01:00
jpmschweitzerandClaude Opus 4.6 03ff3c5fef feat(ui): entity-anchored dialogue log with keyboard selection
Log entries store entity IDs and resolve display names at render
time from _entity_display lookup — enables retroactive name update
when player learns an NPC's real name. Color index from server
replaces name-hash coloring for stable NPC colors.

Dialogue options: switched RichTextLabel to Label (fixes stacking
bug), added 1/2/3 number key selection, numbered option labels.

Interaction list: added background panel, mouse hover highlighting,
click-to-interact, pointing hand cursor.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 13:43:08 +01:00
jpmschweitzerandClaude Opus 4.6 28e7e6d9ad feat(simulation): add NPC name masking with role labels and color index
Server-side infrastructure for information asymmetry in dialogue.
NPCs display role labels (Worker, Supervisor, Patron) instead of
real names until the player explicitly learns them via KG lookup.
NpcColorIndex (stable_id % 8) persists across name reveal.

ConversationEvent and DialogueResponseEvent carry display names +
color indices on the wire with serde(default) for compat.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 13:42:59 +01:00
jpmschweitzerandClaude Opus 4.6 e19887c5a9 fix(ui): address PR #52 review — 10 items across Hoshe, Tyre, Araminta (#535)
Critical: bump PROTOCOL_VERSION 8→9 for conversation_events/ended fields.

Hoshe review:
- Dirty flag (_log_dirty) prevents per-frame O(n) BBCode rebuild
- BBCode injection: _escape_bbcode() replaces [ with [lb] on server text
- D-064 regression: dialogue_active cleared in fade callback, not before
- YAML quoting: remove unnecessary quotes from numeric values

Tyre review:
- Carry-forward for dialogue_response, conversation_events, conversation_ended
  in receive_bytes() — arrays merge, scalar falls through
- pause_requested/unpause_requested signals route through main.gd input
  recording (_pending_record_inputs) for #507 replay determinism
- Fix version comments: dialogue_response is v8 (#305), not v9
- Remove dead _active_overheard dictionary

Araminta review:
- Passive lines: ┃ glyph prefix + _desaturate() for name colours
- Active conversation entries pinned (no timeout), unpinned with timestamp
  reset on conversation end
- _enforce_contrast(): minimum luminance floor for name colour readability
- Simplified 1-on-1 attribution: "Speaker:" instead of "Speaker → You:"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 20:39:12 +01:00
jpmschweitzerandClaude Opus 4.6 1d0ba5f7d8 feat(ui): unified dialogue log with overheard NPC conversations (#535)
Refactors dialogue box into a scrolling conversation log. All dialogue
(player-NPC and overheard NPC-NPC) flows chronologically, oldest at top.
Player response options at the bottom during active conversations.

- Entries expire after configurable timeout (equal for all message types)
- Walk-away clears options but preserves log entries (fair information)
- Per-character name colors from dialogue-theme.yaml (hash-indexed palette)
- Overheard lines render at 90% opacity (D-078)
- Protocol decode for conversation_events + conversation_ended
- GameState fields for conversation_events, conversation_ended, dialogue_response
- Mock Mira/Soren NPC-NPC conversation in test snapshot
- Also wires #511 debug overlay into main.gd and main.tscn

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 20:10:40 +01:00
jpmschweitzerandClaude Opus 4.6 9b53096b0b feat(ui): F3 debug overlay — real-time game state display (#511)
Toggle with F3. Shows tick, fps, position, facing, stance, zone,
entity/tile counts, interaction/recognition counts, dialogue status,
stationary ticks, insert state, time/tick_rate, mode, gauntlet.
Two-column layout, click-through, hidden by default.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 20:10:30 +01:00
225 changed files with 18687 additions and 742 deletions
+231
View File
@@ -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.
+151
View File
@@ -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
View File
@@ -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
View File
@@ -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)"
+197
View File
@@ -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
View File
@@ -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
View File
@@ -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()
+34 -13
View File
@@ -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?
+4 -3
View File
@@ -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
+3
View File
@@ -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
+29
View File
@@ -6,6 +6,35 @@ 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
+2
View File
@@ -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
+15 -5
View File
@@ -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={}
+2 -2
View File
@@ -43,5 +43,5 @@ passive_opacity: 0.9
# ENTRY TIMING
# All entry types share the same lifetime and fade duration.
# ============================================================
entry_lifetime_seconds: 15.0
entry_fade_seconds: 3.0
entry_lifetime_seconds: 45.0
entry_fade_seconds: 5.0
+1
View File
@@ -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

+43
View File
@@ -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
+5 -5
View File
@@ -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 = {
+4
View File
@@ -19,6 +19,10 @@ 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}]}]
@@ -0,0 +1 @@
uid://rqfw0ycyb4c6
+7 -2
View File
@@ -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
+26 -37
View File
@@ -26,7 +26,7 @@ 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
@@ -35,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)
@@ -50,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)
@@ -70,7 +67,7 @@ func _ready() -> void:
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:
@@ -159,23 +156,18 @@ func _process(_delta: float) -> void:
_consume_conversation_ended()
_consume_dialogue_response()
# Track camera to player position every frame (D-015: locked, no panning)
# Track camera to player (D-015: locked, fixed-north).
# #117: Manual exponential smoothing — same pattern as EntityRenderer.LERP_SPEED.
# Teleport (flag set by _teleport_transition): snap immediately, resume lerp next frame.
# Init: camera already snapped in _ready() or late-anchor path above.
if _camera_anchored:
camera.global_position = GameState.player_position * Constants.TILE_SIZE
# Re-enable smoothing after the first anchored frame. The frame that just
# rendered used smoothing=OFF (correct viewport from frame one). Now we
# turn smoothing back on and sync its internal state so subsequent frames
# get smooth camera tracking during gameplay.
# #501: Skip re-enable during teleport — _teleport_transition() disables
# smoothing for a clean camera snap. Defer by one frame to avoid the
# re-enable block in the same _process() call undoing the snap.
if _camera_anchored and not camera.position_smoothing_enabled:
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.
@@ -250,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
@@ -358,11 +344,16 @@ func _consume_conversation_ended() -> void:
# #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
dialogue_box.append_dialogue_response(_last_dialogue_npc_name, dr.get("text", ""))
var speaker_entity_id: int = dr.get("speaker_entity_id", _last_dialogue_npc_id)
var speaker_color_index: int = dr.get("speaker_color_index", -1)
var speaker_name: String = dr.get("speaker_name", _last_dialogue_npc_name)
dialogue_box.update_entity_display(speaker_entity_id, speaker_name, speaker_color_index)
dialogue_box.append_dialogue_response(speaker_name, dr.get("text", ""))
GameState.dialogue_response = null
@@ -434,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
@@ -446,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()
+1 -1
View File
@@ -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 = 9
const PROTOCOL_VERSION: int = 13
# -- Decode: bytes from server → GDScript types --------------------------------
+12 -9
View File
@@ -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
+11
View File
@@ -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)" % [
+1 -1
View File
@@ -88,7 +88,7 @@ func _draw() -> void:
var header_w := font.get_string_size(header_text, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE + 1).x
var 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 := max(left_lines.size(), right_lines.size())
var line_count: int = maxi(left_lines.size(), right_lines.size())
var box_h: float = PADDING.y * 2 + LINE_HEIGHT + LINE_HEIGHT * line_count # header + data lines
# Background
+1
View File
@@ -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.
@@ -0,0 +1 @@
uid://duvqpvheku2j2
+1
View File
@@ -0,0 +1 @@
uid://d30by4d62g7ts
@@ -0,0 +1 @@
uid://b1tata3wjyb46
+22 -19
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://dhgcekc2ywr26
+8 -8
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
uid://qv808kduvlbk
+1
View File
@@ -0,0 +1 @@
uid://dmalit26eiwlh
@@ -0,0 +1 @@
uid://6q82kiijx7ml
@@ -0,0 +1 @@
uid://c8ng0ubdfumht
@@ -0,0 +1 @@
uid://bn4a46egt2uol
+212
View File
@@ -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.020.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
+312
View File
@@ -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
+2 -1
View File
@@ -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])
+1
View File
@@ -0,0 +1 @@
uid://c5xgtnkp0butl
+127 -59
View File
@@ -21,12 +21,18 @@ signal unpause_requested # D-061: auto-unpause
@onready var options_container: VBoxContainer = $PanelContainer/MarginContainer/VBoxContainer/OptionsContainer
# -- Log state --
# Entry format: {speaker, target, text, is_passive, pinned, timestamp_msec}
# Entry format (legacy): {speaker: String, target: String, text, is_passive, pinned, timestamp_msec}
# Entry format (entity-anchored): {speaker_id: int, target_id: int, text, is_passive, pinned, timestamp_msec}
# pinned entries do not expire (active conversation lines, Araminta review).
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] = []
@@ -35,7 +41,6 @@ var _option_is_confrontation: Array[bool] = []
var _npc_name: String = ""
# -- UI state --
var _is_showing: bool = false
var _active_tween: Tween = null
var _beat_tween: Tween = null # D-063: confrontation beat delay
@@ -45,8 +50,8 @@ var _npc_colors: Array[Color] = []
var _arrow_color: Color = Color("#8890a0")
var _speech_color: Color = Color("#c8d0e0")
var _passive_opacity: float = 0.9
var _entry_lifetime: float = 15.0
var _entry_fade: float = 3.0
var _entry_lifetime: float = 45.0
var _entry_fade: float = 5.0
const THEME_PATH: String = "res://data/dialogue-theme.yaml"
@@ -71,9 +76,10 @@ 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()
@@ -88,11 +94,21 @@ func _process(_delta: float) -> void:
func _unhandled_input(event: InputEvent) -> void:
if not _is_showing:
return
if not _in_player_conversation:
return
# Number keys 1-3 select dialogue options
if event is InputEventKey and event.pressed:
var key_index := -1
if event.keycode == KEY_1: key_index = 0
elif event.keycode == KEY_2: key_index = 1
elif event.keycode == KEY_3: key_index = 2
if key_index >= 0 and key_index < _option_controls.size():
get_viewport().set_input_as_handled()
_on_option_pressed(key_index)
return
# D-064: WASD during active player dialogue → walk-away
if event is InputEventKey and event.pressed:
for action in _WALK_AWAY_ACTIONS:
@@ -153,7 +169,7 @@ func _load_theme() -> void:
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
@@ -180,13 +196,59 @@ func append_line(speaker: String, target: String, text: String, is_passive: bool
## 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 speaker: String = event.get("speaker_name", "?")
var target: String = event.get("target_name", "?")
var text: String = event.get("occluded_line", "")
if text.is_empty():
return
append_line(speaker, target, text, true)
var speaker_id: int = event.get("speaker_id", -1)
var target_id: int = event.get("target_id", -1)
var speaker_name: String = event.get("speaker_name", "?")
var target_name: String = event.get("target_name", "?")
var speaker_color_index: int = event.get("speaker_color_index", -1)
var target_color_index: int = event.get("target_color_index", -1)
# Update entity display registry — set dirty if a known name changed (retroactive update)
if speaker_id >= 0:
var prev: Dictionary = _entity_display.get(speaker_id, {})
_entity_display[speaker_id] = {"name": speaker_name, "color_index": speaker_color_index}
if prev.has("name") and prev.get("name", "") != speaker_name:
_log_dirty = true
if target_id >= 0:
var prev: Dictionary = _entity_display.get(target_id, {})
_entity_display[target_id] = {"name": target_name, "color_index": target_color_index}
if prev.has("name") and prev.get("name", "") != target_name:
_log_dirty = true
var entry: Dictionary = {
"text": text,
"is_passive": true,
"pinned": false,
"timestamp_msec": Time.get_ticks_msec(),
}
if speaker_id >= 0:
entry["speaker_id"] = speaker_id
entry["target_id"] = target_id
else:
# Fallback: no entity IDs on wire, store raw names for legacy rendering
entry["speaker"] = speaker_name
entry["target"] = target_name
_log_entries.append(entry)
_log_dirty = true
_ensure_visible()
## Update entity display name and color index — called when Talk responses arrive.
## Triggers retroactive log re-render if the entity's displayed name has changed.
func update_entity_display(entity_id: int, name: String, color_index: int) -> void:
if entity_id < 0:
return
var prev: Dictionary = _entity_display.get(entity_id, {})
_entity_display[entity_id] = {"name": name, "color_index": color_index}
if prev.has("name") and prev.get("name", "") != name:
_log_dirty = true
## Handle conversation_ended — no-op currently (entries expire via timeout).
@@ -234,7 +296,8 @@ func show_dialogue(npc_name: String, speech: String, options: Array = []) -> voi
## End active player conversation — clears options but preserves log.
## D-064: dialogue_active held until fade completes.
## D-064: dialogue_active cleared immediately so WASD resumes.
## Log entries remain visible and expire via timeout (cosmetic only).
func _end_player_conversation() -> void:
_in_player_conversation = false
_clear_options()
@@ -254,32 +317,21 @@ func _end_player_conversation() -> void:
# D-061: unpause — signal to main.gd for input recording (#507)
unpause_requested.emit()
# D-064: dialogue_active stays true — cleared after fade completes in hide callback.
# If log entries still exist, panel stays visible and entries expire via timeout.
# If no entries, hide immediately with fade.
# 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()
## Hide the entire panel with fade.
## End active dialogue state. Panel stays visible (permanent insert UI element).
func hide_dialogue() -> void:
if not _is_showing:
return
if _in_player_conversation:
_end_player_conversation()
return # _end_player_conversation may call hide_dialogue if log is empty
_is_showing = false
if _active_tween and _active_tween.is_valid():
_active_tween.kill()
_active_tween = create_tween()
_active_tween.tween_property(panel, "modulate:a", 0.0, FADE_OUT)
_active_tween.tween_callback(func():
visible = false
GameState.dialogue_active = false # D-064: unblock movement after fade completes
)
GameState.dialogue_active = false
func is_dialogue_active() -> bool:
@@ -302,18 +354,15 @@ func _show_options(options: Array) -> void:
var raw_text: String = opt.get("text", "")
var is_confrontation: bool = opt.get("confrontation", false)
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
var idx := i
label.gui_input.connect(func(event: InputEvent):
@@ -407,15 +456,42 @@ func _rebuild_log() -> void:
## 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 = _escape_bbcode(entry.speaker)
var target: String = _escape_bbcode(entry.target)
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
var speaker_color := _color_for_name(entry.speaker)
var target_color := _color_for_name(entry.target)
# Desaturate passive name colours (Araminta review)
if is_passive:
speaker_color = _desaturate(speaker_color, PASSIVE_DESATURATION)
@@ -429,7 +505,6 @@ func _format_entry(entry: Dictionary, alpha: float) -> String:
var prefix := PASSIVE_GLYPH if is_passive else ""
# Non-blocking: simplify 1-on-1 player dialogue — no arrow for Speaker → You or You → Speaker
var involves_player := entry.speaker == PLAYER_NAME or entry.target == PLAYER_NAME
if involves_player and not is_passive:
# Just "Speaker: text" or "You: text"
return "%s[color=%s][b]%s[/b][/color][color=%s]: %s[/color]" % [
@@ -532,16 +607,9 @@ func _expire_entries() -> void:
# -- Visibility --
## Ensure panel is visible (fade in if needed).
## No-op — panel is always visible as a permanent insert UI element.
func _ensure_visible() -> void:
if _is_showing:
return
visible = true
_is_showing = true
if _active_tween and _active_tween.is_valid():
_active_tween.kill()
_active_tween = create_tween()
_active_tween.tween_property(panel, "modulate:a", 1.0, FADE_IN)
pass
func _clear_options() -> void:
@@ -555,11 +623,11 @@ func _clear_options() -> void:
# Hover callbacks
static func _make_hover_on(label: RichTextLabel) -> Callable:
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)
+74 -2
View File
@@ -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:
+1 -1
View File
@@ -94,7 +94,7 @@ func _show_line(text: String, duration: float, priority: int, is_urgent: bool, l
node = line_node,
expire_timer = maxf(duration, MIN_DURATION), # clamp: survives own fade-in
priority = priority,
tween = null as Tween,
tween = null,
}
_visible.append(slot)
_next_fade_in_msec = float(Time.get_ticks_msec()) + STAGGER_SEC * 1000.0
+1
View File
@@ -0,0 +1 @@
uid://cpjq8yfsnpr5m
+1 -1
View File
@@ -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
@@ -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."
@@ -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]
@@ -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]
@@ -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]
@@ -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]
@@ -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]
@@ -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
@@ -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."
@@ -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]
@@ -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]
@@ -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."
@@ -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]
@@ -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]
@@ -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."
@@ -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]
@@ -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]
@@ -23,7 +23,7 @@ lines:
access: [public]
trust: surface
situation: [arrival, greeting]
mood: [comfortable]
mood: [content]
topic: [routine]
tags: [pc-detective, greeting, public-tier]
@@ -23,7 +23,7 @@ lines:
access: [public]
trust: surface
situation: [arrival, greeting]
mood: [comfortable]
mood: [content]
topic: [routine]
tags: [pc-smuggler, greeting, public-tier]
@@ -20,7 +20,7 @@ lines:
trust: surface
situation: [shift_start, arrival]
topic: [routine, cargo]
mood: [comfortable]
mood: [content]
tags: [maret, procedural, greeting]
- id: the-terminal_d_301
@@ -30,7 +30,7 @@ lines:
trust: surface
situation: [shift_start, routine]
topic: [cargo, routine]
mood: [comfortable]
mood: [content]
tags: [maret, container-4471, procedural]
dual_lens:
smuggler: "She's tracking 4471 as a scheduling issue. Not flagging it as a discrepancy yet."
@@ -46,7 +46,7 @@ lines:
trust: surface
situation: [shift_start, routine]
topic: [cargo, routine]
mood: [comfortable]
mood: [content]
tags: [maret, procedural, manifest]
# ==========================================
@@ -60,7 +60,7 @@ lines:
trust: surface
situation: [routine, investigation]
topic: [cargo, routine]
mood: [concerned]
mood: [frustrated]
tags: [maret, discrepancy, flagging]
dual_lens:
smuggler: "Which container? Not 4471. She's flagging a different one. Two flags, no escalation."
@@ -76,7 +76,7 @@ lines:
trust: surface
situation: [routine]
topic: [cargo, routine]
mood: [concerned]
mood: [frustrated]
tags: [maret, discrepancy, institutional-response]
dual_lens:
smuggler: "She doesn't know what 'administrative' means either. Voss's answer covered nothing."
@@ -89,7 +89,7 @@ lines:
trust: surface
situation: [routine, investigation]
topic: [cargo, routine]
mood: [concerned]
mood: [frustrated]
tags: [maret, pattern, transition-window]
dual_lens:
smuggler: "She's found the pattern. Not the cause. Not yet."
@@ -105,7 +105,7 @@ lines:
trust: surface
situation: [routine]
topic: [cargo, routine]
mood: [concerned]
mood: [frustrated]
tags: [maret, pattern, manifest]
dual_lens:
smuggler: "She's tracking the small ones too. But 'within rounding' is what Voss told her. She's not satisfied."
@@ -118,7 +118,7 @@ lines:
trust: surface
situation: [routine, confrontation]
topic: [cargo, institution]
mood: [concerned]
mood: [frustrated]
tags: [maret, voss, suppression, frustration]
dual_lens:
smuggler: "She filed a formal notice. Voss sat on it. She knows. This is the pressure point."
@@ -138,7 +138,7 @@ lines:
trust: surface
situation: [routine, shift_start]
topic: [routine, cargo]
mood: [comfortable]
mood: [content]
tags: [maret, operational]
- id: the-terminal_d_309
@@ -148,7 +148,7 @@ lines:
trust: surface
situation: [routine]
topic: [routine, cargo]
mood: [comfortable]
mood: [content]
tags: [maret, drin, bay-four, scheduling]
dual_lens:
smuggler: "Maret confirmed it: bay four is Voss's directive. Not a system hold."
@@ -161,7 +161,7 @@ lines:
trust: surface
situation: [routine]
topic: [routine, institution]
mood: [comfortable]
mood: [content]
tags: [maret, administrative]
- id: the-terminal_d_311
@@ -171,7 +171,7 @@ lines:
trust: surface
situation: [routine, shift_start]
topic: [routine, cargo]
mood: [comfortable]
mood: [content]
tags: [maret, procedural, manifest]
# ==========================================
@@ -185,7 +185,7 @@ lines:
trust: surface
situation: [investigation, routine]
topic: [institution, cargo]
mood: [comfortable]
mood: [content]
tags: [maret, detective-path, procedural]
- id: the-terminal_d_313
@@ -195,7 +195,7 @@ lines:
trust: surface
situation: [investigation]
topic: [cargo, institution]
mood: [concerned]
mood: [frustrated]
tags: [maret, detective-path, discrepancy-disclosure]
dual_lens:
smuggler: "Never encounters this line."
@@ -208,7 +208,7 @@ lines:
trust: surface
situation: [investigation, confrontation]
topic: [institution, cargo]
mood: [concerned]
mood: [frustrated]
tags: [maret, detective-path, careful-language]
dual_lens:
smuggler: "Never encounters this line."
@@ -225,7 +225,7 @@ lines:
trust: real
situation: [routine, investigation]
topic: [cargo, investigation]
mood: [concerned]
mood: [frustrated]
tags: [maret, peer-candor, pattern]
dual_lens:
smuggler: "She's figured it out. She hasn't said what 'it' is. But she knows the numbers aren't random."
@@ -241,7 +241,7 @@ lines:
trust: real
situation: [routine, confrontation]
topic: [cargo, institution]
mood: [concerned]
mood: [frustrated]
tags: [maret, voss, suppression, peer-candor]
dual_lens:
smuggler: "Two formal notices. Both sitting with Voss. He's managing her output directly."
@@ -257,7 +257,7 @@ lines:
trust: real
situation: [confrontation, alone]
topic: [investigation]
mood: [concerned]
mood: [frustrated]
tags: [maret, detective-path, threshold-line]
notes: "Gate line. Opens when detective has built enough trust. What follows depends on the conversation state."
@@ -272,7 +272,7 @@ lines:
trust: surface
situation: [shift_end]
topic: [routine]
mood: [comfortable]
mood: [content]
tags: [maret, closing]
- id: the-terminal_d_319
@@ -282,7 +282,7 @@ lines:
trust: real
situation: [shift_end, routine]
topic: [routine]
mood: [concerned]
mood: [frustrated]
tags: [maret, closing, character-line]
notes: "Low-access line. Maret's professional philosophy. Can fire as ambient or as a closing beat."
@@ -297,7 +297,7 @@ lines:
trust: surface
situation: [shift_start, routine]
topic: [routine, cargo]
mood: [comfortable]
mood: [content]
tags: [maret, procedural, generation-pass]
- id: the-terminal_d_321
@@ -307,7 +307,7 @@ lines:
trust: surface
situation: [routine]
topic: [cargo, routine]
mood: [comfortable]
mood: [content]
tags: [maret, operational, generation-pass]
- id: the-terminal_d_322
@@ -317,7 +317,7 @@ lines:
trust: surface
situation: [routine]
topic: [routine, cargo]
mood: [comfortable]
mood: [content]
tags: [maret, procedural, generation-pass]
- id: the-terminal_d_323
@@ -327,7 +327,7 @@ lines:
trust: surface
situation: [routine]
topic: [routine, institution]
mood: [comfortable]
mood: [content]
tags: [maret, voss, administrative, generation-pass]
- id: the-terminal_d_324
@@ -337,7 +337,7 @@ lines:
trust: surface
situation: [routine, shift_start]
topic: [routine, cargo]
mood: [comfortable]
mood: [content]
tags: [maret, procedural, generation-pass]
- id: the-terminal_d_325
@@ -347,7 +347,7 @@ lines:
trust: surface
situation: [routine, shift_end]
topic: [routine]
mood: [comfortable]
mood: [content]
tags: [maret, procedural, generation-pass]
- id: the-terminal_d_326
@@ -357,7 +357,7 @@ lines:
trust: surface
situation: [routine]
topic: [cargo, routine]
mood: [comfortable]
mood: [content]
tags: [maret, procedural, manifest, generation-pass]
- id: the-terminal_d_327
@@ -367,5 +367,5 @@ lines:
trust: surface
situation: [routine]
topic: [institution, routine]
mood: [wary]
mood: [suspicious]
tags: [maret, commission, audit, generation-pass]
@@ -19,7 +19,7 @@ lines:
trust: surface
situation: [arrival, shift_start]
topic: [routine]
mood: [comfortable]
mood: [content]
tags: [voss, greeting, authority]
- id: the-terminal_d_101
@@ -29,7 +29,7 @@ lines:
trust: surface
situation: [shift_start]
topic: [routine, cargo]
mood: [comfortable]
mood: [content]
tags: [voss, procedural]
- id: the-terminal_d_102
@@ -39,7 +39,7 @@ lines:
trust: surface
situation: [shift_start, routine]
topic: [cargo, routine]
mood: [comfortable]
mood: [content]
tags: [voss, authority]
dual_lens:
smuggler: "Review hold on Bay 4. Voss knows the container's in the queue. Is he covering it or flagging it?"
@@ -52,7 +52,7 @@ lines:
trust: surface
situation: [shift_start, routine]
topic: [institution, routine]
mood: [concerned]
mood: [frustrated]
tags: [voss, warning, commission]
dual_lens:
smuggler: "Voss knows the detective's here. That warning's not just about tidiness."
@@ -69,7 +69,7 @@ lines:
trust: surface
situation: [routine]
topic: [routine, colleague]
mood: [comfortable]
mood: [content]
tags: [voss, drin, authority]
- id: the-terminal_d_105
@@ -79,7 +79,7 @@ lines:
trust: surface
situation: [shift_transition]
topic: [routine]
mood: [comfortable]
mood: [content]
tags: [voss, procedural, transition-window]
- id: the-terminal_d_106
@@ -89,7 +89,7 @@ lines:
trust: surface
situation: [routine, shift_end]
topic: [routine]
mood: [comfortable]
mood: [content]
tags: [voss, procedural]
- id: the-terminal_d_107
@@ -99,7 +99,7 @@ lines:
trust: surface
situation: [routine, shift_start]
topic: [routine]
mood: [comfortable]
mood: [content]
tags: [voss, operational]
- id: the-terminal_d_108
@@ -109,7 +109,7 @@ lines:
trust: surface
situation: [routine, investigation]
topic: [cargo, routine]
mood: [comfortable]
mood: [content]
tags: [voss, maret, discrepancy, cover]
dual_lens:
smuggler: "Voss reviewed it and logged it. Not escalating. He's managing it."
@@ -125,7 +125,7 @@ lines:
trust: surface
situation: [routine]
topic: [cargo, routine]
mood: [comfortable]
mood: [content]
tags: [voss, deflection, philosophy]
# ==========================================
@@ -177,7 +177,7 @@ lines:
trust: surface
situation: [investigation, routine]
topic: [institution, routine]
mood: [comfortable]
mood: [content]
tags: [voss, detective-path, institutional-resistance]
- id: the-terminal_d_114
@@ -187,7 +187,7 @@ lines:
trust: surface
situation: [investigation]
topic: [cargo, institution]
mood: [comfortable]
mood: [content]
tags: [voss, detective-path, redirect]
- id: the-terminal_d_115
@@ -197,7 +197,7 @@ lines:
trust: surface
situation: [investigation, confrontation]
topic: [routine, institution]
mood: [comfortable]
mood: [content]
tags: [voss, detective-path, deflection]
- id: the-terminal_d_116
@@ -207,7 +207,7 @@ lines:
trust: surface
situation: [investigation, confrontation]
topic: [routine, institution]
mood: [comfortable]
mood: [content]
tags: [voss, detective-path, authority-assertion]
# ==========================================
@@ -221,7 +221,7 @@ lines:
trust: real
situation: [confrontation, alone]
topic: [danger]
mood: [concerned]
mood: [frustrated]
tags: [voss, confrontation, private]
- id: the-terminal_d_118
@@ -231,7 +231,7 @@ lines:
trust: real
situation: [confrontation, alone]
topic: [colleague, danger]
mood: [concerned]
mood: [frustrated]
tags: [voss, ring-ops, maret, caution]
knowledge_grant:
fact_id: behavioral.voss_awareness
@@ -244,7 +244,7 @@ lines:
trust: surface
situation: [confrontation]
topic: [routine]
mood: [concerned]
mood: [frustrated]
tags: [voss, deflection, shutdown]
# ==========================================
@@ -258,7 +258,7 @@ lines:
trust: surface
situation: [shift_end]
topic: [routine]
mood: [comfortable]
mood: [content]
tags: [voss, procedural]
- id: the-terminal_d_121
@@ -268,7 +268,7 @@ lines:
trust: surface
situation: [shift_transition, shift_end]
topic: [routine]
mood: [comfortable]
mood: [content]
tags: [voss, procedural]
- id: the-terminal_d_122
@@ -278,7 +278,7 @@ lines:
trust: surface
situation: [shift_end]
topic: [routine]
mood: [comfortable]
mood: [content]
tags: [voss, closing]
# ==========================================
@@ -292,7 +292,7 @@ lines:
trust: surface
situation: [shift_start, routine]
topic: [routine]
mood: [comfortable]
mood: [content]
tags: [voss, procedural, generation-pass]
- id: the-terminal_d_124
@@ -302,7 +302,7 @@ lines:
trust: surface
situation: [routine]
topic: [routine, cargo]
mood: [comfortable]
mood: [content]
tags: [voss, procedural, maret, generation-pass]
- id: the-terminal_d_125
@@ -312,7 +312,7 @@ lines:
trust: surface
situation: [routine, shift_start]
topic: [routine, cargo]
mood: [comfortable]
mood: [content]
tags: [voss, operational, generation-pass]
- id: the-terminal_d_126
@@ -322,7 +322,7 @@ lines:
trust: surface
situation: [routine]
topic: [routine]
mood: [comfortable]
mood: [content]
tags: [voss, authority, generation-pass]
- id: the-terminal_d_127
@@ -332,7 +332,7 @@ lines:
trust: surface
situation: [shift_end, routine]
topic: [routine, institution]
mood: [comfortable]
mood: [content]
tags: [voss, procedural, generation-pass]
- id: the-terminal_d_128
@@ -342,7 +342,7 @@ lines:
trust: surface
situation: [routine, shift_start]
topic: [routine, cargo]
mood: [comfortable]
mood: [content]
tags: [voss, operational, generation-pass]
- id: the-terminal_d_129
@@ -352,5 +352,5 @@ lines:
trust: surface
situation: [routine]
topic: [routine]
mood: [comfortable]
mood: [content]
tags: [voss, philosophy, generation-pass]
@@ -3,50 +3,90 @@ location: general
lines:
- id: general_m_d_001
text: "Sova Transit District. Population twelve thousand and change. Let's narrow that down."
role: player_character
access: [public]
trust: surface
situation: [arrival]
trigger: enter_location
tags: [arrival, analytical]
- id: general_m_d_002
text: "The span gate resonance is constant here. Background radiation of a freight hub."
role: player_character
access: [public]
trust: surface
situation: [routine]
trigger: hear_sound
tags: [environmental]
- id: general_m_d_003
text: "First week on station. The case file is thin. The manifest discrepancies are not."
role: player_character
access: [public]
trust: surface
situation: [investigation]
trigger: time_idle
tags: [investigation, orientation]
- id: general_m_d_004
text: "Everyone knows I'm Commission. The conversations stop when I walk in. Noted."
role: player_character
access: [public]
trust: surface
situation: [investigation]
trigger: time_idle
tags: [investigation, social]
- id: general_m_d_005
text: "Station gravity's point-nine-three standard. Enough to notice after a long shift."
role: player_character
access: [public]
trust: surface
situation: [alone]
trigger: time_idle
tags: [environmental, personal]
- id: general_m_d_006
text: "The previous liaison rotated out three months ago. That gap matters."
role: player_character
access: [public]
trust: surface
situation: [investigation]
trigger: time_idle
tags: [investigation, analytical]
- id: general_m_d_007
text: "Back in the district. Let's see what changed while I wasn't watching."
role: player_character
access: [public]
trust: surface
situation: [arrival]
trigger: return_visit
tags: [arrival, analytical]
- id: general_m_d_008
text: "Recycled air, industrial residue, and that constant gate hum. Sova in three notes."
role: player_character
access: [public]
trust: surface
situation: [arrival]
trigger: enter_location
tags: [environmental, atmospheric]
- id: general_m_d_009
text: "Communities like this close ranks around outsiders. Patient work."
role: player_character
access: [public]
trust: surface
situation: [observation]
trigger: time_idle
tags: [reflection, social]
- id: general_m_d_010
text: "The manifest data goes back eighteen months. The pattern starts around month six."
role: player_character
access: [public]
trust: surface
situation: [investigation]
trigger: time_idle
tags: [investigation, analytical]
@@ -5,41 +5,73 @@ lines:
- id: maintenance-corridors_m_d_001
text: "Maintenance level. Low lighting, exposed conduit, restricted access nobody enforces."
role: player_character
access: [public]
trust: surface
situation: [arrival]
trigger: enter_location
tags: [arrival, environmental]
- id: maintenance-corridors_m_d_002
text: "The air's different down here. Cooler. Damper. Less filtered."
role: player_character
access: [public]
trust: surface
situation: [arrival]
trigger: enter_location
tags: [environmental, atmospheric]
- id: maintenance-corridors_m_d_003
text: "Service corridor B-section. Not on the public transit map. Interesting routing options."
role: player_character
access: [public]
trust: surface
situation: [arrival, investigation]
trigger: enter_location
tags: [arrival, investigation]
- id: maintenance-corridors_m_d_004
text: "Water recycling conduits. The station's circulatory system, hidden from view."
role: player_character
access: [public]
trust: surface
situation: [routine]
trigger: hear_sound
tags: [environmental]
- id: maintenance-corridors_m_d_005
text: "Pipe network branching three directions. Good for maintenance. Better for privacy."
role: player_character
access: [public]
trust: surface
situation: [arrival]
trigger: enter_location
tags: [environmental, analytical]
- id: maintenance-corridors_m_d_006
text: "Emergency lighting only. Visibility reduced, but so is surveillance coverage."
role: player_character
access: [public]
trust: surface
situation: [arrival, investigation]
trigger: enter_location
tags: [environmental, investigation]
- id: maintenance-corridors_m_d_007
text: "The rust smell is stronger near junction B-7. Older section. Less maintenance."
role: player_character
access: [public]
trust: surface
situation: [arrival]
trigger: enter_location
tags: [environmental, atmospheric]
- id: maintenance-corridors_m_d_008
text: "Back in the corridors. Different route this time — mapping the alternatives."
role: player_character
access: [public]
trust: surface
situation: [arrival, investigation]
trigger: return_visit
tags: [arrival, investigation]
@@ -47,41 +79,73 @@ lines:
- id: maintenance-corridors_m_d_009
text: "Scuff marks on the floor. Recent, heavy. Cargo weight, not foot traffic."
role: player_character
access: [public]
trust: surface
situation: [investigation, observation]
trigger: observe_anomaly
tags: [investigation, evidence]
- id: maintenance-corridors_m_d_010
text: "The service hatch has been used recently. Oil on the handle, dust disturbed."
role: player_character
access: [public]
trust: surface
situation: [investigation, observation]
trigger: observe_anomaly
tags: [investigation, evidence]
- id: maintenance-corridors_m_d_011
text: "Restricted access, but the lock plate shows wear. Regular use by someone with clearance."
role: player_character
access: [public]
trust: surface
situation: [investigation, observation]
trigger: observe_anomaly
tags: [investigation, evidence]
- id: maintenance-corridors_m_d_012
text: "Sound carries differently down here. Voices from two junctions away, if you're patient."
role: player_character
access: [public]
trust: surface
situation: [investigation, routine]
trigger: hear_sound
tags: [environmental, investigation]
- id: maintenance-corridors_m_d_013
text: "Chalk marks on the wall. Low, deliberate. Not maintenance notation — wrong color."
role: player_character
access: [public]
trust: surface
situation: [investigation, observation]
trigger: observe_anomaly
tags: [investigation, evidence]
- id: maintenance-corridors_m_d_014
text: "This corridor connects bay four to temp storage. Bypasses the main freight floor entirely."
role: player_character
access: [public]
trust: surface
situation: [arrival, investigation, routine]
trigger: enter_location
tags: [investigation, operational]
- id: maintenance-corridors_m_d_015
text: "Footsteps ahead. Single set, moving fast. Someone who knows where they're going."
role: player_character
access: [public]
trust: surface
situation: [investigation, routine]
trigger: hear_sound
tags: [investigation, caution]
- id: maintenance-corridors_m_d_016
text: "The temp storage access from this side isn't logged on the manifest system. Blind spot."
role: player_character
access: [public]
trust: surface
situation: [investigation, observation]
trigger: observe_anomaly
tags: [investigation, evidence]
@@ -89,6 +153,10 @@ lines:
- id: maintenance-corridors_m_d_017
text: "The cargo scuff pattern matches bay four containers. Dock to corridor to storage — the route."
role: player_character
access: [public]
trust: surface
situation: [investigation, observation]
trigger: observe_anomaly
prerequisites:
facts:
@@ -99,6 +167,10 @@ lines:
- id: maintenance-corridors_m_d_018
text: "Those chalk marks match the courier's timing. Renn's route markers, if I had to guess."
role: player_character
access: [public]
trust: surface
situation: [investigation, observation]
trigger: observe_anomaly
prerequisites:
facts:
@@ -109,6 +181,10 @@ lines:
- id: maintenance-corridors_m_d_019
text: "Camera blind spot at junction C-2. Coincidence, or someone mapped the coverage?"
role: player_character
access: [public]
trust: surface
situation: [investigation, observation, routine]
trigger: observe_anomaly
prerequisites:
facts:
@@ -118,6 +194,10 @@ lines:
- id: maintenance-corridors_m_d_020
text: "Standard container, no manifest checkpoint along this route. That's the gap."
role: player_character
access: [public]
trust: surface
situation: [alone, investigation]
trigger: time_idle
prerequisites:
facts:
@@ -128,6 +208,10 @@ lines:
- id: maintenance-corridors_m_d_021
text: "Hatch lubricant is fresh. Someone's maintaining this access point. Not station maintenance."
role: player_character
access: [public]
trust: surface
situation: [investigation, observation]
trigger: observe_anomaly
prerequisites:
facts:
@@ -137,6 +221,10 @@ lines:
- id: maintenance-corridors_m_d_022
text: "Davan was seen near B-7 during off-shift. Cross-reference with container movements."
role: player_character
access: [public]
trust: surface
situation: [alone, investigation]
trigger: time_idle
prerequisites:
facts:
@@ -147,6 +235,10 @@ lines:
- id: maintenance-corridors_m_d_023
text: "Two access routes, one manifest checkpoint. Someone designed this gap. Or discovered it."
role: player_character
access: [public]
trust: surface
situation: [alone, investigation]
trigger: time_idle
prerequisites:
facts:
@@ -156,6 +248,10 @@ lines:
- id: maintenance-corridors_m_d_024
text: "Rosta's gambling debt gives me leverage. But using it makes me the authority they resent."
role: player_character
access: [public]
trust: surface
situation: [alone, investigation]
trigger: time_idle
prerequisites:
facts:
@@ -166,6 +262,10 @@ lines:
- id: maintenance-corridors_m_d_025
text: "Evidence is accumulating. Names, routes, timing. But I still can't see the full picture."
role: player_character
access: [public]
trust: surface
situation: [alone, investigation]
trigger: time_idle
prerequisites:
facts:
@@ -0,0 +1,166 @@
character: detective
location: opening
lines:
# Opening Hook Sequence — Ticket #300 — Detective Diegetic Tutorial Arc / First 5 Minutes
# 12 lines. Prerequisites null on all — fires before any knowledge graph state exists.
# Priority 8-10 ensures these fire before the general terminal pool lines.
# cooldown: 9999 — each line fires once, does not repeat.
# Arc: institutional orientation → sensory/analytical → lattice overlay → Commission kiosk → fog boundary →
# NPC lattice scan → Sera sighting → Sera warmth → manifest board → anomaly → flag it → operational settle.
# Contraction discipline: analytical lines = uncontracted; personal lines (Sera) = contracted.
# All lines fully schema-compliant per D-035 (role, access, trust, situation present).
- id: opening_m_d_001
text: "Sova Transit. Mid-morning shift, dock lighting at half-cycle. Consistent with the Commission pre-arrival briefing."
role: player_character
access: [public]
trust: surface
situation: [arrival, shift_start]
trigger: enter_location
mood: [focused]
priority: 10
cooldown: 9999
tags: [opening-hook, tutorial-arc, analytical, orientation]
notes: "Opening line. Reality matches the briefing — he arrived prepared. Establishes: detective as institutional outsider sent here. Cool-lit corridors per D-046 expressed through 'half-cycle' lighting observation. No contractions (analytical)."
- id: opening_m_d_002
text: "Cargo lubricant, recycled atmosphere. Same mixture on every freight hub from here to the span gate hum. First observation: consistent with Commission briefing profile."
role: player_character
access: [public]
trust: surface
situation: [arrival, shift_start]
trigger: enter_location
mood: [focused]
priority: 10
cooldown: 9999
tags: [opening-hook, tutorial-arc, atmospheric, analytical]
notes: "Environmental sensory in analytical register. Colon usage (detective verbal pattern, section 2.2). No contractions. 'span gate' is canonical terminology (D-036); 'hum' is sensory detail. Contrast with smuggler's 'Home sweet home' — same smell, opposite emotional valence. He's working, not home."
- id: opening_m_d_003
text: "Lattice overlay initializing. Four identity flags in immediate range — cross-referencing against Commission case file."
role: player_character
access: [public]
trust: surface
situation: [arrival, shift_start]
trigger: enter_location
mood: [focused]
priority: 10
cooldown: 9999
tags: [opening-hook, tutorial-arc, lattice, perception]
notes: "Teaches insert channel. The analytical lattice is active on entry — it flags and cross-references automatically. Teaches: detective's insert HUD is denser and diegetically active (D-048). Players learn the lattice is a tool, not decoration."
- id: opening_m_d_004
text: "Commission kiosk confirmed my credentials on entry. Authority access logged. The floor knows someone's here."
role: player_character
access: [public]
trust: surface
situation: [arrival, shift_start]
trigger: enter_location
mood: [focused]
priority: 9
cooldown: 9999
tags: [opening-hook, tutorial-arc, institutional, authority]
notes: "Commission kiosk visible, institutional standing established. Sardonic close — dry awareness of his effect on the social environment. Confidence register: wry humor (section 2.8). Teaches: Authority access tier, institutional role is visible to NPCs."
- id: opening_m_d_005
text: "Visual range terminates at bay nine. Approximately fifteen meters. Consistent with district atmo settings."
role: player_character
access: [public]
trust: surface
situation: [arrival, shift_start, observation]
trigger: enter_location
priority: 9
cooldown: 9999
tags: [opening-hook, tutorial-arc, fog, perception]
notes: "Fog boundary in analytical register. He clocks the measurement, notes 'approximately' — precision with honest calibration. Contrast with smuggler's 'Can't see the far dock' — same fog, different framing: limitation vs. measurement."
- id: opening_m_d_006
text: "Dock workers distributed across nine visible bays. Lattice flagging each on contact. Names from the case file, faces from the floor."
role: player_character
access: [public]
trust: surface
situation: [arrival, shift_start, observation]
trigger: enter_location
mood: [focused]
priority: 9
cooldown: 9999
tags: [opening-hook, tutorial-arc, lattice, orientation]
notes: "Spatial overview with lattice overlay tutorial. Explains how the insert channel works: lattice matches observed faces against the pre-loaded case file. Parallel structure ('Names from... faces from...'). Teaches: NPC identification comes through the lattice."
- id: opening_m_d_007
text: "Venn, S. at the far terminal. She said she'd come in early."
role: player_character
access: [public]
trust: surface
situation: [arrival, shift_start, social]
trigger: observe_npc
mood: [warm]
priority: 10
cooldown: 9999
tags: [opening-hook, tutorial-arc, sera, friend-arc]
notes: "First Sera sighting. Surname-initial filing first (institutional reflex) — then breaks to personal contracted voice ('she'd') in the same beat. Teaches contrast: case-file subjects get 'Davan, K.'; Sera gets a different register. D-033 Unknown/Neutral teal entity."
- id: opening_m_d_008
text: "She's got the manifest logs open already. Field tech who knows how to pull records — she knows what I need before I ask."
role: player_character
access: [public]
trust: surface
situation: [arrival, shift_start, social]
trigger: observe_npc
mood: [warm]
priority: 9
cooldown: 9999
tags: [opening-hook, tutorial-arc, sera, friend-arc]
notes: "Sera warmth. Fully contracted throughout ('She's', 'she knows') — personal mode, no analytical distance. Em-dash. Professional respect established without implying prior district visits. Contrast with how he processes everyone else. Teaches: some NPCs are in a different relational category (THE FRIEND pattern, D-034)."
- id: opening_m_d_009
text: "Central manifest board: real-time container routing, weight declarations, bay assignments. Everything routes through here."
role: player_character
access: [public]
trust: surface
situation: [arrival, shift_start, investigation]
trigger: enter_location
mood: [focused]
priority: 8
cooldown: 9999
tags: [opening-hook, tutorial-arc, manifest, investigation]
notes: "Manifest board labeled and cataloged. Colon usage (section 2.2). No contractions (analytical). Teaches: the manifest board is the information hub — and therefore the investigation's starting point."
- id: opening_m_d_010
text: "Container 4471. Temp storage flag since yesterday. Standard turnaround is sub-eight hours — that's not standard."
role: player_character
access: [public]
trust: surface
situation: [arrival, observation, investigation]
trigger: observe_anomaly
mood: [focused, suspicious]
priority: 9
cooldown: 9999
tags: [opening-hook, tutorial-arc, manifest, investigation, anomaly]
notes: "First manifest anomaly. Specific container ID from case file. 'That's not standard' — only contraction in an analytical line, marking it as personal reaction to the finding. Sets the investigation hook: something is wrong before he asks a single question."
- id: opening_m_d_011
text: "Worth flagging. Let's see what the shift logs say about that container's overnight routing."
role: player_character
access: [public]
trust: surface
situation: [observation, investigation]
trigger: observe_anomaly
mood: [focused]
priority: 8
cooldown: 9999
tags: [opening-hook, tutorial-arc, investigation, next-step]
notes: "'Worth flagging' + 'Let's see what' — both detective verbal tics in sequence (section 2.7). Frames the next investigative step. Teaches: investigation is incremental — observe, flag, investigate deeper."
- id: opening_m_d_012
text: "Lattice stable. Nine identity flags and counting. Let's see what this shift tells me."
role: player_character
access: [public]
trust: surface
situation: [arrival, shift_start]
trigger: enter_location
mood: [focused]
priority: 7
cooldown: 9999
tags: [opening-hook, tutorial-arc, lattice, operational]
notes: "Operational settle — closes the opening arc. Lattice confirmed active, identity count growing. 'Let's see what...' verbal tic as the closing beat. The whole opening hook lands here: credentials logged, floor scanned, first anomaly flagged. Investigation begins."
@@ -14,6 +14,10 @@ lines:
- id: general_m_d_011
text: "That dock worker checks the corridor before turning. Habitual. Military? Or just careful."
role: player_character
access: [public]
trust: surface
situation: [observation]
trigger: observe_npc
prerequisites:
relationship:
@@ -23,6 +27,10 @@ lines:
- id: general_m_d_012
text: "Movement pattern changed at shift change. More direct. Less idle. Interesting."
role: player_character
access: [public]
trust: surface
situation: [observation]
trigger: observe_anomaly
prerequisites:
relationship:
@@ -32,6 +40,10 @@ lines:
- id: general_m_d_013
text: "Slight acceleration past the checkpoint. Most people do it. But this one does it every time."
role: player_character
access: [public]
trust: surface
situation: [observation]
trigger: observe_anomaly
prerequisites:
relationship:
@@ -41,6 +53,10 @@ lines:
- id: general_m_d_014
text: "Same three people at the same table. Every shift. Consistent social group."
role: player_character
access: [public]
trust: surface
situation: [observation]
trigger: observe_npc
prerequisites:
relationship:
@@ -50,6 +66,10 @@ lines:
- id: general_m_d_015
text: "That worker checked container 4471 twice. The same container that's in my manifest discrepancy file."
role: player_character
access: [public]
trust: surface
situation: [investigation, observation]
trigger: observe_anomaly
prerequisites:
facts:
@@ -65,6 +85,10 @@ lines:
- id: general_m_d_016
text: "Three behavioral flags on one dock worker. Corridor awareness, shift-transition alertness, cargo fixation. That's a pattern."
role: player_character
access: [public]
trust: surface
situation: [alone, investigation]
trigger: time_idle
prerequisites:
entity_attributes:
@@ -76,6 +100,10 @@ lines:
- id: general_m_d_017
text: "Cross-referencing the dock worker's shift schedule with manifest anomalies. Correlation: moderate."
role: player_character
access: [public]
trust: surface
situation: [alone, investigation]
trigger: time_idle
prerequisites:
facts:
@@ -89,6 +117,10 @@ lines:
- id: general_m_d_018
text: "The dock worker and Davan eat together every shift. Same courier joins them. Tight operational cluster."
role: player_character
access: [public]
trust: surface
situation: [investigation, observation]
trigger: observe_npc
prerequisites:
relationship:
@@ -5,51 +5,91 @@ lines:
- id: the-last-shift_m_d_001
text: "The Last Shift. Only place in this district that doesn't smell like freight lubricant."
role: player_character
access: [public]
trust: surface
situation: [arrival]
trigger: enter_location
tags: [arrival, atmospheric]
- id: the-last-shift_m_d_002
text: "Grain spirit and low conversation. The social nexus of Sova Transit."
role: player_character
access: [public]
trust: surface
situation: [arrival]
trigger: enter_location
tags: [arrival, analytical]
- id: the-last-shift_m_d_003
text: "Bar's busy tonight. Good — crowds talk. Bad — they notice outsiders."
role: player_character
access: [public]
trust: surface
situation: [arrival, investigation]
trigger: enter_location
tags: [arrival, investigation]
- id: the-last-shift_m_d_004
text: "Standard district bar. Prefab fixtures, aftermarket lighting, a menu board nobody reads."
role: player_character
access: [public]
trust: surface
situation: [arrival]
trigger: enter_location
tags: [arrival, environmental]
- id: the-last-shift_m_d_005
text: "The Meridian feed on the wall display. Local news, freight indices, Assembly coverage."
role: player_character
access: [public]
trust: surface
situation: [arrival]
trigger: enter_location
tags: [environmental]
- id: the-last-shift_m_d_006
text: "The overhead vent rattles every thirty seconds. Like clockwork. Nobody else notices."
role: player_character
access: [public]
trust: surface
situation: [routine]
trigger: hear_sound
tags: [environmental]
- id: the-last-shift_m_d_007
text: "The conversation volume dropped when I walked in. Recovering now. They're adjusting."
role: player_character
access: [public]
trust: surface
situation: [arrival, investigation]
trigger: enter_location
tags: [social, investigation]
- id: the-last-shift_m_d_008
text: "Back at the bar. Same booth. Let's see who's here and who isn't."
role: player_character
access: [public]
trust: surface
situation: [arrival]
trigger: return_visit
tags: [arrival, analytical]
- id: the-last-shift_m_d_009
text: "Kitchen's running. Fried protein and something spiced. The bar's other currency: hot food."
role: player_character
access: [public]
trust: surface
situation: [arrival]
trigger: enter_location
tags: [environmental, atmospheric]
- id: the-last-shift_m_d_010
text: "Someone carved initials into this table. Old. A history I don't have."
role: player_character
access: [public]
trust: surface
situation: [alone]
trigger: time_idle
tags: [environmental, reflection]
@@ -57,51 +97,91 @@ lines:
- id: the-last-shift_m_d_011
text: "The bartender — Sessik, L. Knows everyone. Watches everything. Gives away nothing."
role: player_character
access: [public]
trust: surface
situation: [observation]
trigger: observe_npc
tags: [npc, lera, analytical]
- id: the-last-shift_m_d_012
text: "Good to see Sera. At least someone in this district speaks my language."
role: player_character
access: [public]
trust: surface
situation: [observation]
trigger: observe_npc
tags: [npc, sera, warm]
- id: the-last-shift_m_d_013
text: "Lintar, T. — the loud one at the bar. Spends freely. Where's the money from?"
role: player_character
access: [public]
trust: surface
situation: [investigation, observation]
trigger: observe_npc
tags: [npc, torek, investigation]
- id: the-last-shift_m_d_014
text: "A woman at the corner table — Tamm, N., according to Sera. Davan's partner."
role: player_character
access: [public]
trust: surface
situation: [observation]
trigger: observe_npc
tags: [npc, naia, analytical]
- id: the-last-shift_m_d_015
text: "The regulars have assigned seating. Unofficial, but enforced. I don't have a spot yet."
role: player_character
access: [public]
trust: surface
situation: [observation]
trigger: observe_npc
tags: [social, atmospheric]
- id: the-last-shift_m_d_016
text: "Sessik greeted three people by name since I sat down. I got a nod. Progress."
role: player_character
access: [public]
trust: surface
situation: [observation]
trigger: witness_interaction
tags: [npc, lera, social]
- id: the-last-shift_m_d_017
text: "Two dock workers in the corner. Heads close, voices low. Not unusual for after-shift."
role: player_character
access: [public]
trust: surface
situation: [investigation, observation]
trigger: witness_interaction
tags: [social, investigation]
- id: the-last-shift_m_d_018
text: "Sera introduced me to Tamm. Brief, polite, guarded. Expected."
role: player_character
access: [public]
trust: surface
situation: [social]
trigger: post_conversation
tags: [npc, sera, naia, social]
- id: the-last-shift_m_d_019
text: "Sessik poured my drink without asking. She remembered. Small thing, but noted."
role: player_character
access: [public]
trust: surface
situation: [observation]
trigger: observe_npc
tags: [npc, lera, social, warm]
- id: the-last-shift_m_d_020
text: "The old man by the window hasn't spoken to anyone all evening. Watching, though."
role: player_character
access: [public]
trust: surface
situation: [observation]
trigger: observe_npc
tags: [social, atmospheric]
@@ -109,6 +189,10 @@ lines:
- id: the-last-shift_m_d_021
text: "Sera left when Torek arrived. Second time. Different excuse. Same result."
role: player_character
access: [public]
trust: surface
situation: [investigation, observation]
trigger: observe_anomaly
prerequisites:
facts:
@@ -119,6 +203,10 @@ lines:
- id: the-last-shift_m_d_022
text: "That's the third time. Three excuses, one result. That's data."
role: player_character
access: [public]
trust: surface
situation: [investigation, observation]
trigger: observe_anomaly
prerequisites:
facts:
@@ -129,6 +217,10 @@ lines:
- id: the-last-shift_m_d_023
text: "Lintar's spending doesn't match a dock worker's salary. Source unknown."
role: player_character
access: [public]
trust: surface
situation: [investigation, observation]
trigger: observe_npc
prerequisites:
facts:
@@ -138,6 +230,10 @@ lines:
- id: the-last-shift_m_d_024
text: "Sessik's eyes tracked me when I sat near the back room. Protective, not hostile."
role: player_character
access: [public]
trust: surface
situation: [investigation, observation]
trigger: observe_npc
prerequisites:
facts:
@@ -147,6 +243,10 @@ lines:
- id: the-last-shift_m_d_025
text: "Sera's being extra helpful tonight. Names, backgrounds, context. Compensating?"
role: player_character
access: [public]
trust: surface
situation: [investigation, social]
trigger: post_conversation
prerequisites:
relationship:
@@ -157,6 +257,10 @@ lines:
- id: the-last-shift_m_d_026
text: "Same booth. Same warm smile. Same offer to buy me a drink. Everything except the truth."
role: player_character
access: [public]
trust: surface
situation: [investigation, observation]
trigger: observe_npc
prerequisites:
relationship:
@@ -167,6 +271,10 @@ lines:
- id: the-last-shift_m_d_027
text: "Tamm seems distressed tonight. Not her baseline. Something at home, or something she heard?"
role: player_character
access: [public]
trust: surface
situation: [observation]
trigger: observe_npc
prerequisites:
relationship:
@@ -176,6 +284,10 @@ lines:
- id: the-last-shift_m_d_028
text: "Sessik just redirected a conversation away from freight schedules. Protective reflex?"
role: player_character
access: [public]
trust: surface
situation: [investigation, observation]
trigger: witness_interaction
prerequisites:
facts:
@@ -185,6 +297,10 @@ lines:
- id: the-last-shift_m_d_029
text: "Davan and his partner at the bar. She's talking. He's not listening. Noted."
role: player_character
access: [public]
trust: surface
situation: [observation]
trigger: witness_interaction
prerequisites:
relationship:
@@ -194,6 +310,10 @@ lines:
- id: the-last-shift_m_d_030
text: "The regulars are warming up. Three visits and I'm almost furniture. Almost."
role: player_character
access: [public]
trust: surface
situation: [arrival]
trigger: return_visit
prerequisites:
facts:
@@ -206,6 +326,10 @@ lines:
- id: the-last-shift_m_d_043
text: "Davan is different here. Looser. The dock worker persona drops and something warmer replaces it."
role: player_character
access: [public]
trust: surface
situation: [observation]
trigger: observe_npc
prerequisites:
relationship:
@@ -215,6 +339,10 @@ lines:
- id: the-last-shift_m_d_044
text: "Davan checks the door every time it opens. Even here. Even off-shift. That is not relaxation."
role: player_character
access: [public]
trust: surface
situation: [investigation, observation]
trigger: observe_npc
prerequisites:
relationship:
@@ -231,6 +359,10 @@ lines:
- id: the-last-shift_m_d_031
text: "Sera changed the subject when I mentioned manifests. Smooth. Practised, maybe."
role: player_character
access: [public]
trust: surface
situation: [investigation, social]
trigger: post_conversation
prerequisites:
facts:
@@ -241,6 +373,10 @@ lines:
- id: the-last-shift_m_d_032
text: "She volunteers names, routines, context. Gives everything I ask for -- except what I did not."
role: player_character
access: [public]
trust: surface
situation: [investigation, social]
trigger: post_conversation
prerequisites:
relationship:
@@ -251,6 +387,10 @@ lines:
- id: the-last-shift_m_d_033
text: "Sera at the Commission kiosk. Third pass this shift. That is not standard recalibration."
role: player_character
access: [public]
trust: surface
situation: [investigation, observation]
trigger: observe_npc
prerequisites:
facts:
@@ -261,6 +401,10 @@ lines:
- id: the-last-shift_m_d_034
text: "Everything Sera told me checks out. Layout, schedules, regulars. Accurate. Thorough. Selective?"
role: player_character
access: [public]
trust: surface
situation: [investigation, social]
trigger: post_conversation
prerequisites:
relationship:
@@ -271,6 +415,10 @@ lines:
- id: the-last-shift_m_d_035
text: "She mentioned Naia's worry about Kael early -- before I had context. Priming, or concern?"
role: player_character
access: [public]
trust: surface
situation: [investigation, social]
trigger: post_conversation
prerequisites:
facts:
@@ -281,6 +429,10 @@ lines:
- id: the-last-shift_m_d_036
text: "Sera's institutional access goes further than field calibration. She could pull records I cannot."
role: player_character
access: [public]
trust: surface
situation: [alone, investigation]
trigger: time_idle
prerequisites:
relationship:
@@ -290,6 +442,10 @@ lines:
- id: the-last-shift_m_d_037
text: "Assessment: Venn, S. Cooperative. Embedded. Competent. Green -- with reservations now."
role: player_character
access: [public]
trust: surface
situation: [alone, investigation]
trigger: time_idle
prerequisites:
facts:
@@ -300,6 +456,10 @@ lines:
- id: the-last-shift_m_d_038
text: "Correction: amber. The avoidance pattern is behavioural, not preferential. Data, not taste."
role: player_character
access: [public]
trust: surface
situation: [investigation, observation]
trigger: observe_anomaly
prerequisites:
facts:
@@ -310,6 +470,10 @@ lines:
- id: the-last-shift_m_d_039
text: "She's the first person who made this place feel less foreign. I need that to not matter right now."
role: player_character
access: [public]
trust: surface
situation: [alone, investigation]
trigger: time_idle
prerequisites:
relationship:
@@ -320,6 +484,10 @@ lines:
- id: the-last-shift_m_d_040
text: "Those early tips. The district layout, the introductions. Were those help -- or steering?"
role: player_character
access: [public]
trust: surface
situation: [alone, investigation]
trigger: time_idle
prerequisites:
relationship:
@@ -330,6 +498,10 @@ lines:
- id: the-last-shift_m_d_041
text: "She's not a suspect. She's a friend with access to everything I need. That is the problem."
role: player_character
access: [public]
trust: surface
situation: [alone, investigation]
trigger: time_idle
prerequisites:
facts:
@@ -340,6 +512,10 @@ lines:
- id: the-last-shift_m_d_042
text: "Trust: compromised. The question is not what she told me. It is what she did not."
role: player_character
access: [public]
trust: surface
situation: [investigation, social]
trigger: post_conversation
prerequisites:
relationship:

Some files were not shown because too many files have changed in this diff Show More