Compare commits
@@ -0,0 +1,16 @@
|
||||
# Git Safety
|
||||
|
||||
## Staging rules
|
||||
|
||||
- **Stage files by name** — never use `git add -A` or `git add .`
|
||||
- Verify no secrets, saves, or binary blobs are staged
|
||||
- Skip files in `.gitignore`
|
||||
- The `.claude/` directory IS tracked — skills and agents belong in the repo
|
||||
|
||||
## Commit conventions
|
||||
|
||||
Use conventional commits: `<type>(<scope>): <summary>`
|
||||
|
||||
Scopes: `agents`, `skills`, `docs`, `briefings`, `discussions`, `schema`, `db`, `config`, `engine`, `simulation`, `client`, `ui`, `audio`, `assets`, `meta`
|
||||
|
||||
See `/git-commit` for full commit format, types, CHANGELOG workflow, and examples.
|
||||
@@ -0,0 +1,8 @@
|
||||
# Local Services
|
||||
|
||||
Endpoints are also preconfigured in `db/connectors/config.json`.
|
||||
|
||||
- **Gitea:** `http://git.schweitz.internal` (login: `schweitz`)
|
||||
- **Qdrant:** `http://tower-of-joy:6333/`
|
||||
- **Ollama:** `http://tower-of-joy:11434/` (nomic-embed-text)
|
||||
- **Collection:** `commonwealth` (768 dimensions, cosine distance)
|
||||
@@ -0,0 +1,40 @@
|
||||
# Project Structure (detailed)
|
||||
|
||||
```
|
||||
client/ # Godot 4 client
|
||||
server/ # Rust/bevy_ecs simulation server
|
||||
tooling/ # Build tools, scripts, asset pipelines
|
||||
tests/ # Integration and end-to-end tests
|
||||
.config/ # Configuration files (linters, formatters, CI)
|
||||
.cache/ # Local caches for testing/linting (gitignored)
|
||||
docs/
|
||||
discussions/ # Discussion rounds (archived here when complete)
|
||||
briefings/ # Per-agent context briefings (maintained by Qatux)
|
||||
architecture/ # Technical architecture documents
|
||||
design/ # Game design documents
|
||||
diagrams/ # d2 source + PNG renders
|
||||
sprints/ # Sprint briefings per team
|
||||
workshops/ # Workshop briefs and outputs
|
||||
db/
|
||||
schema.sql # Database schema
|
||||
connectors/ # Connector scripts for SQLite and Qdrant
|
||||
config.json # Endpoint configuration
|
||||
ticket # Ticket CLI
|
||||
sqlite_connector.py # SQLite mini MCP
|
||||
qdrant_connector.py # Qdrant + ollama mini MCP
|
||||
.claude/
|
||||
agents/ # Agent personality files
|
||||
skills/ # Skill definitions
|
||||
rules/ # Auto-loaded instruction modules
|
||||
decisions/ # Decision domain files (source of truth)
|
||||
README.md # Domain index — use this to find specific D-records
|
||||
architecture.md # Architecture decisions
|
||||
perception.md # Perception and information system decisions
|
||||
content.md # Content and narrative decisions
|
||||
scope.md # Scope and feature decisions
|
||||
process.md # Process and workflow decisions
|
||||
questions.md # Open questions (Q-NNN)
|
||||
rejected.md # Rejected proposals (R-NNN)
|
||||
DECISIONS.md # Redirect to decisions/ directory
|
||||
TEAM.md # Team roster and roles
|
||||
```
|
||||
@@ -0,0 +1,45 @@
|
||||
# Gitea Access (tea CLI)
|
||||
|
||||
**Never access the Gitea API directly** — use the `tea` CLI with all required flags to bypass interactive mode.
|
||||
|
||||
Always pass `--login schweitz --repo jpmschweitzer/settled-reach --output simple` to avoid TTY prompts.
|
||||
|
||||
```bash
|
||||
# List open PRs
|
||||
tea pr list --login schweitz --repo jpmschweitzer/settled-reach --state open --output simple
|
||||
|
||||
# View a PR with comments
|
||||
tea pr --login schweitz --repo jpmschweitzer/settled-reach --comments -o simple <PR_NUMBER>
|
||||
|
||||
# Post a comment on a PR (or issue)
|
||||
tooling/tea-comment <NUMBER> "comment body"
|
||||
|
||||
# Approve a PR
|
||||
tea pr approve --login schweitz --repo jpmschweitzer/settled-reach <PR_NUMBER>
|
||||
|
||||
# List issues
|
||||
tea issue list --login schweitz --repo jpmschweitzer/settled-reach --state open --output simple
|
||||
```
|
||||
|
||||
## Key rules
|
||||
|
||||
- **All flags must be explicit** — omitting `--login` or `--repo` triggers interactive prompts that crash in Claude Code (no TTY)
|
||||
- **Use `--output simple`** for machine-readable output (no table borders)
|
||||
- **For comments, use `tooling/tea-comment <number> "body"`** — handles temp files and cleanup automatically. Works with multi-line strings.
|
||||
- **`tea pr reject` does not work on your own PRs** — use `tea comment` instead
|
||||
- **Never delete protected branches:** `main`, `maintenance`, `server`, `client`, `copy`, `audio`, `visual`, `ci` are protected on Gitea. Do not use `tea pr clean`, `git push --delete`, or `git branch -D` on these branches.
|
||||
|
||||
## Pull requests
|
||||
|
||||
**Use `tea` (Gitea CLI), not `gh` (GitHub CLI).** The remote is Gitea at `git.schweitz.internal`.
|
||||
|
||||
Always provide all required flags to ensure non-interactive execution:
|
||||
```bash
|
||||
tea pr create \
|
||||
--repo jpmschweitzer/settled-reach \
|
||||
--login schweitz \
|
||||
--title "feat(scope): short description" \
|
||||
--description "PR body here" \
|
||||
--base main \
|
||||
--head branch-name
|
||||
```
|
||||
@@ -0,0 +1,33 @@
|
||||
# Team Patterns
|
||||
|
||||
## Model selection
|
||||
|
||||
Default model is Opus 4.6 (200K context). For heavy sessions (workshops,
|
||||
sprint planning, large reviews), switch to extended context on-demand:
|
||||
|
||||
- `/model sonnet[1m]` — Sonnet 4.6 with 1M context window
|
||||
- `/model opus[1m]` — Opus 4.6 with 1M context window
|
||||
- Cost: 2x input + 1.5x output for tokens beyond 200K (Tier 4 required)
|
||||
|
||||
## Large content pushes
|
||||
|
||||
When producing many files (wiki pages, content batches, bulk docs):
|
||||
1. **Lore librarian** agent (read-only): ingests all source material, answers focused context queries from writers, tracks cross-file consistency
|
||||
2. **Multiple writer** agents (parallel, by domain): each gets a task slice, writes directly to disk using the Write tool — one file at a time, write often, no text accumulation
|
||||
3. **Reviewer** agents (blocked until writing done): check voice consistency, attribute uniformity, style
|
||||
|
||||
Key: writers use Write tool directly (no transcription bottleneck), librarian catches contradictions early, split work by domain not volume.
|
||||
|
||||
## Team monitoring (stuck agent detection)
|
||||
|
||||
When leading a team (sprint, workshop, or any multi-agent session):
|
||||
|
||||
**Agent heartbeat rule** — include in every agent spawn prompt:
|
||||
> If you have been working on a single task for more than 15 minutes
|
||||
> without making progress, message the team lead with what is blocking
|
||||
> you. Do not keep retrying the same approach silently.
|
||||
|
||||
**Team lead proactive checks:**
|
||||
- If an agent has not sent a message in ~20 minutes, ping them for a status update.
|
||||
- **Bottleneck detection:** if other agents are idle and waiting on one agent's output, that agent's silence is a red flag — check on them immediately, do not wait for the next natural message.
|
||||
- When checking on a stuck agent, offer to reassign the task or pull in another agent to help.
|
||||
@@ -44,6 +44,7 @@
|
||||
"Bash(make)",
|
||||
|
||||
"Bash(tea *)",
|
||||
"Bash(tooling/tea-comment *)",
|
||||
|
||||
"Bash(chmod *)",
|
||||
"Bash(ls *)",
|
||||
|
||||
@@ -10,9 +10,9 @@ allowed-tools: Bash, Read, Grep, Glob
|
||||
|
||||
# Search Docs Skill
|
||||
|
||||
Semantic search across project documents. Basic commands (`qdrant-search`,
|
||||
`qdrant-index`, `qdrant-health`, `qdrant-count`) and endpoints are documented
|
||||
in CLAUDE.md. This skill covers advanced operations and workflows.
|
||||
Semantic search across project documents. Endpoints are in
|
||||
`.claude/rules/local-services.md`. This skill covers advanced operations
|
||||
and workflows.
|
||||
|
||||
## Advanced Commands
|
||||
|
||||
|
||||
@@ -136,7 +136,5 @@ chore(meta): release v0.1.0
|
||||
|
||||
## Staging Rules
|
||||
|
||||
- Stage files by name — never use `git add -A` or `git add .`
|
||||
- Verify no secrets, saves, or binary blobs are staged
|
||||
- Skip files in `.gitignore`
|
||||
- The `.claude/` directory IS tracked — skills belong in the repo
|
||||
See `.claude/rules/git-safety.md` for staging rules (always-loaded).
|
||||
These apply to ALL git operations, not just this skill.
|
||||
|
||||
@@ -100,15 +100,11 @@ git diff --stat main...<branch>
|
||||
Draft title (`<type>(<scope>): <summary>`, max 70 chars) and description.
|
||||
|
||||
```bash
|
||||
cat > /tmp/pr-body.md << 'EOF'
|
||||
## Summary
|
||||
...
|
||||
EOF
|
||||
tea pr create \
|
||||
--repo jpmschweitzer/settled-reach \
|
||||
--login schweitz \
|
||||
--title "<title>" \
|
||||
--description "$(cat /tmp/pr-body.md)" \
|
||||
--description "## Summary ..." \
|
||||
--base main \
|
||||
--head <branch>
|
||||
```
|
||||
@@ -134,6 +130,14 @@ Report which tickets were moved to review. Skip tickets that are
|
||||
already `done`, `review`, `cancelled`, or `backlog` (only transition
|
||||
`in_progress` → `review`).
|
||||
|
||||
### 9. Next steps
|
||||
|
||||
If a sprint team is active (you are the team lead), do NOT shut down
|
||||
agents after pushing. The team should remain alive for PR review and
|
||||
potential comment fixes.
|
||||
|
||||
Suggest: "PR created/updated. Run `/pr-review` to review before merge."
|
||||
|
||||
## Arguments
|
||||
|
||||
If the user passes arguments (e.g., `/pr-push "my title"`), use them as the
|
||||
|
||||
@@ -16,11 +16,20 @@ on the branch type. All reviewers must approve for a clean review.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Determine the branch
|
||||
### 0. Branch guard — MUST be on `main`
|
||||
|
||||
If the user provided a branch name as argument, use it. Otherwise use the
|
||||
current branch (`git branch --show-current`). If on `main`, ask the user
|
||||
which branch to review.
|
||||
```bash
|
||||
git branch --show-current
|
||||
```
|
||||
|
||||
If the current branch is **not `main`**, stop immediately and tell the user:
|
||||
"PR reviews must be run from the `main` worktree. Switch to `main` first."
|
||||
Do NOT proceed with the review from a team branch.
|
||||
|
||||
### 1. Determine the branch to review
|
||||
|
||||
If the user provided a branch name as argument, use it. Otherwise list open
|
||||
PRs and ask the user which branch to review.
|
||||
|
||||
To list open PRs on Gitea:
|
||||
```bash
|
||||
@@ -174,19 +183,11 @@ After presenting results to the user, post the review as a PR comment.
|
||||
|
||||
Note: `tea pr reject` does not work on your own PRs. Use `tea comment` instead.
|
||||
|
||||
**IMPORTANT — `tea comment` hangs with inline heredocs and multi-line strings.**
|
||||
Always use a two-step approach: write to a temp file first, then pass via `$(cat)`:
|
||||
Post using the `tea-comment` wrapper (handles temp files and cleanup):
|
||||
|
||||
```bash
|
||||
tooling/tea-comment <PR_NUMBER> "review markdown here"
|
||||
```
|
||||
# Step 1: Write review to .tmp/ using the Write tool (no permission prompt)
|
||||
Write(file_path: "<repo_root>/.tmp/review-<branch>.md", content: "...review content...")
|
||||
|
||||
# Step 2: Post to Gitea (separate Bash call)
|
||||
tea comment --login schweitz --repo jpmschweitzer/settled-reach <PR_NUMBER> "$(cat .tmp/review-<branch>.md)"
|
||||
```
|
||||
|
||||
Use the Write tool for step 1 (avoids Bash permission prompts). The `.tmp/`
|
||||
directory is gitignored and exists in the repo root for this purpose.
|
||||
|
||||
## 7. Merging approved PRs
|
||||
|
||||
@@ -203,6 +204,22 @@ tea pr close --login schweitz --repo jpmschweitzer/settled-reach <PR_NUMBER>
|
||||
Gitea does **not** auto-close PRs when you push a local merge — always close
|
||||
manually with `tea pr close` after pushing.
|
||||
|
||||
### 8. Post-review team actions
|
||||
|
||||
If a sprint team is active and you are the team lead, handle the
|
||||
review outcome:
|
||||
|
||||
**CHANGES_REQUESTED:**
|
||||
The sprint-start lifecycle (step 9c) handles dispatching review
|
||||
comments to agents. After presenting results, remind the lead:
|
||||
"Review requested changes. Create tasks from the warnings/critical
|
||||
issues and dispatch to idle agents, then re-push and re-review."
|
||||
|
||||
**APPROVED:**
|
||||
The sprint-start lifecycle (step 9c) handles shutdown. After
|
||||
presenting results, remind the lead: "Review approved. Proceed with
|
||||
team shutdown per sprint-start step 9c."
|
||||
|
||||
## Tips from practice
|
||||
|
||||
- **Vendor code**: Explicitly note vendor code in the prompt so reviewers focus
|
||||
|
||||
@@ -325,3 +325,57 @@ Output to the user:
|
||||
You are now the team lead. Agents work autonomously — monitor via
|
||||
`TaskList`, communicate via `SendMessage`, and handle blockers as
|
||||
they arise.
|
||||
|
||||
**When all tasks complete:** Do NOT shut down agents. The team stays
|
||||
alive through the PR review cycle. Follow step 9 (post-work lifecycle).
|
||||
|
||||
### 9. Post-work lifecycle
|
||||
|
||||
When all tasks are complete (TaskList shows all completed):
|
||||
|
||||
#### 9a. Commit and push
|
||||
|
||||
Run `/git-commit` to commit all changes, then `/pr-push` to create or
|
||||
update the PR. Do NOT shut down agents — the team stays alive for review.
|
||||
|
||||
#### 9b. Review
|
||||
|
||||
Run `/pr-review` to spawn temporary reviewers. Wait for results.
|
||||
|
||||
#### 9c. Handle review outcome
|
||||
|
||||
**If CHANGES_REQUESTED:**
|
||||
|
||||
1. Parse the review comment table (from the Gitea PR comment or the
|
||||
review output). Extract each warning/critical issue with:
|
||||
- File path and approximate line
|
||||
- Severity (critical / warning / suggestion)
|
||||
- Description
|
||||
|
||||
2. Create a task per warning/critical issue:
|
||||
```
|
||||
TaskCreate(
|
||||
subject: "Review: {short description}",
|
||||
description: "{full issue description from review table, including
|
||||
file path, severity, and reviewer name}",
|
||||
activeForm: "Fixing review comment: {short description}"
|
||||
)
|
||||
```
|
||||
Skip suggestion-severity items unless they are trivial (1-line fixes).
|
||||
|
||||
3. Dispatch to idle agents: send each a message via SendMessage telling
|
||||
them to check TaskList for new review-fix tasks. Agents claim and
|
||||
work tasks as usual.
|
||||
|
||||
4. After all review-fix tasks are complete, re-run `/git-commit` then
|
||||
`/pr-push` to update the PR. Then re-run `/pr-review`.
|
||||
|
||||
5. Repeat this loop until review returns APPROVED.
|
||||
|
||||
**If APPROVED:**
|
||||
|
||||
1. Send `shutdown_request` to all sprint agents.
|
||||
2. Wait for all `shutdown_response` confirmations.
|
||||
3. Call `TeamDelete` to clean up.
|
||||
4. Report: "Sprint {N} {team} complete. PR #{X} approved and ready for
|
||||
merge on main."
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
name: sprint-status
|
||||
description: >
|
||||
Sprint health check and cleanup sweep. Lists all tickets in the active
|
||||
sprint grouped by status, detects bookkeeping issues (stale tickets,
|
||||
orphan PRs, done-but-open PRs, unassigned work), and shows open work
|
||||
by team. Use when checking sprint progress, before sprint close, or
|
||||
when housekeeping feels off. Triggers on "sprint status", "cleanup
|
||||
sweep", "what's open", "sprint health".
|
||||
user-invocable: true
|
||||
allowed-tools: Task, Read, Grep, Glob
|
||||
---
|
||||
|
||||
# Sprint Status
|
||||
|
||||
**Delegate this entire skill to a subagent** (general-purpose, model: haiku).
|
||||
|
||||
When this skill is invoked, spawn a subagent using the Task tool:
|
||||
|
||||
```
|
||||
Task(
|
||||
subagent_type: "general-purpose",
|
||||
model: "haiku",
|
||||
prompt: "Run /sprint-status. Read the skill at
|
||||
.claude/skills/sprint-status/SKILL.md for the full workflow
|
||||
(below the --- separator), then execute it.",
|
||||
description: "Sprint status report"
|
||||
)
|
||||
```
|
||||
|
||||
Present the subagent's output to the user verbatim. Do NOT run the
|
||||
workflow yourself.
|
||||
|
||||
---
|
||||
|
||||
The remainder of this file is the subagent's reference for executing
|
||||
the workflow.
|
||||
|
||||
## Step 1 — Gather data
|
||||
|
||||
Run these two commands in parallel:
|
||||
|
||||
```bash
|
||||
db/connectors/sprint sweep
|
||||
```
|
||||
|
||||
```bash
|
||||
tea pr list --login schweitz --repo jpmschweitzer/settled-reach --state open --output simple
|
||||
```
|
||||
|
||||
The `sweep` command returns JSON with:
|
||||
- `sprint` — id, name, goal
|
||||
- `progress` — total, done, pct
|
||||
- `by_status` — tickets grouped into done, review, in_progress, blocked, backlog
|
||||
- `by_team` — per-team counts
|
||||
- `issues` — bookkeeping problems with suggested fix commands
|
||||
|
||||
The `tea pr list` returns open PRs as `#N title` lines.
|
||||
|
||||
## Step 2 — Cross-reference PRs with tickets
|
||||
|
||||
Parse PR head branches from the `tea pr list` output. Known team branches:
|
||||
`server`, `client`, `copy`, `audio`, `visual`, `ci`.
|
||||
|
||||
Detect additional issues:
|
||||
|
||||
- **done_team_open_pr**: A team's tickets are all done but an open PR
|
||||
still exists for that team branch.
|
||||
- **orphan_pr**: An open PR exists on a branch that has no tickets in
|
||||
the active sprint.
|
||||
|
||||
Add these to the issues list from step 1.
|
||||
|
||||
## Step 3 — Format output
|
||||
|
||||
Read `references/output-template.md` for the exact format spec.
|
||||
|
||||
Render the report using data from steps 1-2. Key rules:
|
||||
- Sections ordered: Completed, In Review, In Progress, Blocked, Backlog
|
||||
- Sort tickets within sections by team then ticket ID
|
||||
- Empty sections: show header with "(0)" and "(none)" — no empty table
|
||||
- Bookkeeping Issues: two-column table (Issue, Fix)
|
||||
- Open Work by Team: summary table at the bottom
|
||||
- Issue type labels: `stale_backlog` → "Stale backlog",
|
||||
`unassigned_in_progress` → "Unassigned in_progress",
|
||||
`assigned_but_done` → "Assigned but done",
|
||||
`done_team_open_pr` → "Done team with open PR",
|
||||
`orphan_pr` → "Orphan PR"
|
||||
|
||||
## Step 4 — Suggest actions
|
||||
|
||||
After the formatted report, if there are bookkeeping issues, add a
|
||||
"Suggested fixes" section with the fix command for each issue. Group
|
||||
by issue type for readability.
|
||||
@@ -0,0 +1,54 @@
|
||||
# Sprint Status Output Template
|
||||
|
||||
## Sprint {N}: {Theme} — Status Report
|
||||
|
||||
**Goal:** {goal}
|
||||
**Status:** {status} | {done}/{total} tickets ({pct}%)
|
||||
**Open PRs:** {count} ({branches})
|
||||
|
||||
---
|
||||
|
||||
### Completed ({count})
|
||||
|
||||
| # | Team | Title | Assigned |
|
||||
|---|------|-------|----------|
|
||||
| #{id} | {team} | {title} | {assigned} |
|
||||
|
||||
### In Review ({count})
|
||||
|
||||
| # | Team | Title | PR |
|
||||
|---|------|-------|----|
|
||||
| #{id} | {team} | {title} | #{pr} |
|
||||
|
||||
### In Progress ({count})
|
||||
|
||||
| # | Team | Title | Assigned | Note |
|
||||
|---|------|-------|----------|------|
|
||||
| #{id} | {team} | {title} | {assigned} | |
|
||||
|
||||
### Blocked ({count})
|
||||
|
||||
| # | Team | Title | Blocked by |
|
||||
|---|------|-------|------------|
|
||||
| #{id} | {team} | {title} | #{ids} |
|
||||
|
||||
### Backlog ({count})
|
||||
|
||||
| # | Team | Title | Note |
|
||||
|---|------|-------|----|
|
||||
| #{id} | {team} | {title} | not started |
|
||||
|
||||
---
|
||||
|
||||
### Bookkeeping Issues
|
||||
|
||||
| Issue | Fix |
|
||||
|-------|-----|
|
||||
| {type}: {detail} | `{command}` |
|
||||
|
||||
### Open Work by Team
|
||||
|
||||
| Team | Backlog | In Progress | Review | Blocked | Done |
|
||||
|------|---------|-------------|--------|---------|------|
|
||||
| {team} | {n} | {n} | {n} | {n} | {n} |
|
||||
| **Total** | **{n}** | **{n}** | **{n}** | **{n}** | **{n}** |
|
||||
@@ -10,9 +10,8 @@ allowed-tools: Bash, Read, Grep, Glob
|
||||
|
||||
# Ticket Skill
|
||||
|
||||
Manage the project ticketing database. Basic usage (`ticket list`, `ticket show`,
|
||||
`ticket sprint --active`) and raw SQL wrappers are documented in CLAUDE.md.
|
||||
This skill covers the full command reference.
|
||||
Manage the project ticketing database. Basic usage is in CLAUDE.md's CLI tools
|
||||
section. This skill covers the full command reference.
|
||||
|
||||
## Commands
|
||||
|
||||
|
||||
@@ -6,6 +6,78 @@ Format based on [Keep a Changelog](https://keepachangelog.com/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- D-084: dual-namespace line ID scheme for auto-generated NPCs — role pool (shared, unchanged) + instance override (opt-in, seeded counter). Resolves Q-028 (#544)
|
||||
- Tier 1 drama module schema (`content/schemas/drama_module.schema.yaml`) — entry conditions, NPC requirements, event sequences, outcomes, pool format (#158)
|
||||
- Smuggling ring v0.1 stub module (`content/modules/tier1/smuggling_ring_v0_1.yaml`) — vertical slice Tier 1 module with 6 NPC roles, dual event sequences, 5 outcomes (#158)
|
||||
- Line ID authoring guide (`docs/design/line-id-authoring-guide.md`) — dual-namespace conventions for hand-authored and auto-generated NPC content
|
||||
- Tier 1 module authoring guide (`docs/design/tier1-module-authoring.md`) — field reference, NPC pattern/motivation tables, design principles, pre-submission checklist
|
||||
- Sprint 18: Touch planned — 14 tickets (server 9, client 3, copy 2) covering examine mechanic, NPC awareness, social propagation, minimap, dialogue UI, save state model
|
||||
- `.claude/rules/` directory — modular auto-loaded instructions (tea-cli, git-safety, project-structure, team-patterns, local-services)
|
||||
- KnowledgeGrant untagged enum with Fact and Entity variants, ContentEntityRegistry for NPC spawn-time entity resolution (D-079, #545)
|
||||
- KnowledgeGranted event processing — grants fire at dialogue line selection, runtime NPC KG guardrail (D-079, #546)
|
||||
- ContradictionClaim struct with 600-tick window detection in observe_entity, epistemic neutrality for both sources (D-083, #547)
|
||||
- NPC-to-NPC knowledge transfer system — trust-gated fact exchange, confidence capping at KnowsOf, ToldBy source construction (D-080, #548)
|
||||
- tell_state KG awareness — NPC relationship reads from KG for other-entity state, MVP information boundary (D-082, #549)
|
||||
- Contradiction monologue with pre-resolved entity names, PersonOfInterest relationship shift, THE FRIEND arc event chain (D-083, #550)
|
||||
- Unprompted disclosure system — DisclosureCandidates component, 7 trigger gates, three-layer rate limiting, two-stage trait filter (D-081, #551)
|
||||
- Trait modifier system — Cautious/Gossipy/Loyal/Talkative filter predicates via content-authorable config (D-081, #173)
|
||||
- POI data model and proximity-based discovery system via KnowledgeGranted events (#148, #149)
|
||||
- Protocol versioning tests — version round-trip, mismatch detection, serde_default migration pattern, full variant coverage (#232)
|
||||
- Team monitoring rules — heartbeat rule for stuck agent detection, bottleneck detection pattern
|
||||
- `tooling/tea-comment` — single-command wrapper for posting Gitea PR/issue comments with multi-line bodies
|
||||
- Insert/HUD wireframe and visual spec (#314) — dual character variants (smuggler social network view, detective investigation overlay) with pixel-precise layout, entity markers, time display, border arrows, commission grid, and all interaction states
|
||||
- Contradiction monologue lines — 16 hand-authored lines (8 detective, 8 smuggler) for Sera/Kael FRIEND arc, Phase 2 blindsiding + Phase 3 pattern recognition, cognitive-dissonance-not-accusation tone per D-083 (#552)
|
||||
- Diegetic tutorial monologue — 20 lines (10 per character) teaching movement, fog, sound, NPC interaction, and insert/HUD through character voice, fire-once on first-time events (#330)
|
||||
- Diegetic time display on insert HUD — station local time (HH:MM), day phase with cycle-tinted color, day number on InsertOverlay (#263)
|
||||
- Relationship color accent on E-Talk overlay — 3px left-edge bar using D-033 palette signals NPC relationship at a glance (#537)
|
||||
- `Constants.format_game_time()` helper for converting game-minutes to HH:MM station time
|
||||
- `/sprint-status` cleanup sweep skill — consistent health report with tickets by status, PR cross-reference, bookkeeping issue detection, and open work by team
|
||||
- `sprint sweep` CLI subcommand — structured JSON output for sprint health checks (grouped tickets, per-team summary, issue detection)
|
||||
- Knowledge Flow & NPC Boundaries workshop — 5 D-records (D-079–D-083) covering grant architecture, NPC-to-NPC propagation, unprompted disclosure, NPC information boundaries MVP, contradiction detection pipeline
|
||||
- 7 knowledge graph implementation tickets (#545–#551) with full dependency chain and line estimates
|
||||
- Contradiction monologue content ticket (#552) for Sera/Kael FRIEND arc
|
||||
- Sprint 17 completion proofs: contradiction detection fires, NPC-to-NPC knowledge transfers
|
||||
- Entity renderer migrated from ColorRect placeholders to Sprite2D with D-019 angle sprites — self_modulate for D-033 tinting, 8→4 octant direction mapping, feet-anchored y-sort (#540)
|
||||
|
||||
### Changed
|
||||
- CLAUDE.md compacted from 188 to 67 lines — CLI references, endpoints, and patterns moved to `.claude/rules/`
|
||||
- `/sprint-status` delegates to haiku subagent — keeps sweep JSON, template read, and PR list out of main context window
|
||||
- `sprint sweep` JSON trimmed — removed unused fields (`ok`, `sprint.status`, `priority`, `ticket_id`), shortened issue detail strings
|
||||
- Sprint status output template condensed — rendering rules moved to skill definition, bookkeeping table simplified to 2 columns
|
||||
- Model selection documented in CLAUDE.md — `/model sonnet[1m]` and `/model opus[1m]` for 1M context sessions
|
||||
- Sprint 17 briefings updated with workshop results — server (14 tickets), copy (2 tickets), client (2), visual (1)
|
||||
- Q-024 (gossip timing), Q-025 (KG memory), Q-026 (contradiction detection) closed
|
||||
- Sprint 16 closed (8/8 done)
|
||||
- 3D sprite render pipeline — Camera3D at D-019 angle (-72.5° from horizontal), three-point studio lighting rig, orthographic projection, resolution chain 1024→256→64
|
||||
- Generic NPC capsule model (24×32px footprint per D-044) and structural wall model for pipeline validation
|
||||
- Test sprites: 8 runtime 64px sprites (NPC + wall × 4 directions) deployed to client/assets/sprites/
|
||||
- Pipeline documentation (renderer/README.md) — camera spec, lighting rig, resolution chain, model authoring guide
|
||||
- DialogueResponse verb handler — players pick dialogue options and receive follow-up lines via full D-028 four-layer pipeline (#539)
|
||||
- Trust-gated gossip verification — integration tests confirm Secret/Real/Surface tier gating per D-075 (#171)
|
||||
- Line variety tracker wiring — DialogueCooldownTracker prevents repeat lines within 600-tick window (#338)
|
||||
- DialogueResponse cross-language fixture for GDScript testing
|
||||
- Sprint team lifecycle through PR review — teams stay alive for commit → push → review → fix loop → approve → shutdown
|
||||
- Zone_id extraction in game_state.gd optimized from O(N) tile scan to O(1) dictionary lookup — builds _tile_by_coord from member visible_tiles covering both test and live paths (#543)
|
||||
- Shared run_dialogue_pipeline() helper eliminates ~60 lines of duplication between Talk and DialogueResponse systems
|
||||
- Dialogue and monologue line IDs migrated from location-scoped (the-terminal_d_039) to NPC-scoped (kael-davan_d_001) namespace — each NPC has an independent sequence per D-035 (#542)
|
||||
- DialogueCooldownTracker documented as per-player-global by design (NPC-scoped line IDs per D-035 prevent collision)
|
||||
- CONFRONTATION_LINES marked TODO for migration to D-028/D-035 content pipeline
|
||||
- pr-push and pr-review skills updated with team lifecycle awareness
|
||||
|
||||
### Fixed
|
||||
- PR #59 review: stale mood vocabulary updated in line-pool-format.md, style-guide, and content-directory-structure.md to post-Sprint 14 values
|
||||
- PR #59 review: orphaned location-scoped IDs in maintenance-tech.yaml comments and smuggler-inventory.yaml cross-references updated to NPC-scoped
|
||||
- PR #59 review: Lera Sessik tenure corrected from "twelve years" to "eighteen years", NPC header fixed
|
||||
- PR #59 review: ring-operative.yaml fact_id corrected from `location.surveillance_gaps` to `investigation.surveillance_gaps`
|
||||
- Dialogue systems moved from BridgePlugin to NpcPlugin — game logic registers where it belongs (#538)
|
||||
- Schedule ambiguity: emit_observation_events now has explicit .before(advance_tick) constraint
|
||||
- process_dialogue_response updates ActiveDialogue tick and InteractionMemory on follow-up
|
||||
- DialogueResponse range check added (CLOSE_RANGE, matching Talk/Confront pattern)
|
||||
- Weighted selection fallback replaced with unreachable!() — dead code removed
|
||||
- assert!(false) → panic!() in serialization tests (clippy)
|
||||
- SetFacing and TeleportToHub added to roundtrip test coverage
|
||||
|
||||
## [v0.1.15] — 2026-02-23
|
||||
|
||||
### Added
|
||||
|
||||
@@ -1,51 +1,26 @@
|
||||
# The Settled Reach
|
||||
|
||||
A top-down immersive sim — occlusion-based detective game with combat elements, set in an original science fiction universe. Single-character perspective, asymmetric information as core mechanic, Rimworld-style storyteller. Godot 4 client + Rust/bevy_ecs simulation server via subprocess/IPC (D-020).
|
||||
A top-down immersive sim — occlusion-based detective game with combat elements, set in an original science fiction universe. Single-character perspective, asymmetric information as core mechanic, Rimworld-style storyteller. Godot 4 client + Rust/bevy_ecs simulation server via subprocess/IPC.
|
||||
|
||||
**Official Title:** The Settled Reach (D-021)
|
||||
**Repository name:** settled-reach (formerly commonwealth, renamed for clarity)
|
||||
**Official Title:** The Settled Reach
|
||||
**Repository name:** settled-reach
|
||||
**Version source of truth:** `project.yaml` (root `version` field, scheme: `0.1.{sprint_number}`)
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
client/ # Godot 4 client (D-020)
|
||||
server/ # Rust/bevy_ecs simulation server (D-020)
|
||||
client/ # Godot 4 client
|
||||
server/ # Rust/bevy_ecs simulation server
|
||||
tooling/ # Build tools, scripts, asset pipelines
|
||||
tests/ # Integration and end-to-end tests
|
||||
.config/ # Configuration files (linters, formatters, CI)
|
||||
.cache/ # Local caches for testing/linting (gitignored)
|
||||
docs/
|
||||
discussions/ # Discussion rounds (all rounds archived here per D-022)
|
||||
briefings/ # Per-agent context briefings (maintained by Qatux)
|
||||
architecture/ # Technical architecture documents
|
||||
design/ # Game design documents
|
||||
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/
|
||||
schema.sql # Database schema
|
||||
connectors/ # Connector scripts for SQLite and Qdrant
|
||||
config.json # Endpoint configuration
|
||||
ticket # Ticket CLI (list, show, create, assign, sprint, etc.)
|
||||
sqlite_connector.py # SQLite mini MCP
|
||||
qdrant_connector.py # Qdrant + ollama mini MCP
|
||||
.claude/
|
||||
agents/ # Agent personality files
|
||||
skills/ # Skill definitions
|
||||
decisions/ # Decision domain files (source of truth)
|
||||
README.md # Domain index and query examples
|
||||
architecture.md # D-008, D-009, D-010, D-012, D-020, D-026, D-030, D-031, D-041, D-042, D-054, D-055, D-066
|
||||
perception.md # D-011, D-015, D-016, D-017, D-018, D-019, D-033, D-035, D-043-D-049, D-052, D-056-D-061, D-067, D-069-D-072, D-076-D-078
|
||||
content.md # D-023, D-024, D-025, D-028, D-029, D-032, D-034-D-037, D-050, D-062-D-064
|
||||
scope.md # D-001, D-003, D-005, D-006, D-007, D-013, D-014, D-027, D-038, D-039, D-051, D-053, D-065
|
||||
process.md # D-004, D-021, D-022
|
||||
questions.md # Q-001 through Q-011
|
||||
rejected.md # R-001 through R-010
|
||||
DECISIONS.md # Redirect to decisions/ directory
|
||||
TEAM.md # Team roster and roles
|
||||
docs/ # Architecture, design, briefings, sprints, workshops
|
||||
db/ # Schema + connector scripts (ticket CLI, SQLite, Qdrant)
|
||||
.claude/ # Agents, skills, rules
|
||||
decisions/ # Decision domain files (D-NNN confirmed, Q-NNN open, R-NNN rejected)
|
||||
```
|
||||
|
||||
Full annotated tree: `.claude/rules/project-structure.md`
|
||||
|
||||
## DevOps
|
||||
|
||||
See [docs/DEVOPS.md](docs/DEVOPS.md) for build, test, lint, and CI procedures. All development operations go through the top-level `Makefile` — run `make` for a summary of targets.
|
||||
@@ -54,15 +29,12 @@ See [docs/DEVOPS.md](docs/DEVOPS.md) for build, test, lint, and CI procedures. A
|
||||
|
||||
### Worktree boundaries
|
||||
|
||||
This project uses **git worktrees** in a shared parent directory (`settled-reach/`). Each team branch (`server`, `client`, `copy`, `audio`, `visual`, `ci`) is checked out in its own worktree under that parent. The parent directory also contains shared resources like the ticketing database.
|
||||
This project uses **git worktrees** in a shared parent directory (`settled-reach/`). Each team branch (`server`, `client`, `copy`, `audio`, `visual`, `ci`) has its own worktree. The worktree root IS the git root.
|
||||
|
||||
Each worktree contains the full repository: `server/` (Rust backend), `client/` (Godot client), `docs/`, `decisions/`, etc. The worktree root IS the git root — use `git rev-parse --show-toplevel` if in doubt.
|
||||
|
||||
Unless there is a direct instruction or a functional need (e.g. accessing the shared database in the parent directory), **all work must remain within the scope of the git root Claude is running in.**
|
||||
|
||||
- All file paths are relative to the worktree/git root (e.g. `server/src/bridge/types.rs`, `client/scripts/rendering/fog.gd`).
|
||||
- Do not navigate to or access sibling worktrees in the parent directory (`../client/`, `../copy/`, etc.) unless explicitly instructed.
|
||||
- Do not navigate above the git root unless explicitly instructed.
|
||||
- **All work must remain within the git root** unless explicitly instructed otherwise.
|
||||
- All file paths are relative to the worktree root (e.g. `server/src/bridge/types.rs`).
|
||||
- Do not navigate to or access sibling worktrees (`../client/`, `../copy/`, etc.) unless explicitly instructed.
|
||||
- **Exception — stale git lock files:** Worktree index locks live in the shared `.git` directory (e.g. `main/.git/worktrees/copy/index.lock`). If a `git` command fails with `index.lock: File exists`, you may remove the lock file for **your own worktree only**. Never touch lock files belonging to other worktrees.
|
||||
|
||||
### Database
|
||||
|
||||
@@ -74,73 +46,18 @@ The ticketing database (`settledreach.db`) lives in the **parent directory** sha
|
||||
3. Read the relevant `decisions/*.md` domain file(s) referenced in the briefing
|
||||
4. Background context: `docs/briefings/{your-name}.md`, `docs/discussions/`
|
||||
|
||||
### Ticket and database access
|
||||
**Prefer the ticket CLI over raw SQL.** The CLI handles column names, joins, and output formatting correctly:
|
||||
```bash
|
||||
db/connectors/ticket list --sprint 2 --team server
|
||||
db/connectors/ticket show 78
|
||||
db/connectors/ticket sprint --active
|
||||
```
|
||||
### CLI tools
|
||||
|
||||
### Sprint CLI
|
||||
**Use the sprint CLI for sprint-scoped operations.** It batches ticket queries and formats output for agent consumption:
|
||||
```bash
|
||||
db/connectors/sprint status # Current sprint progress
|
||||
db/connectors/sprint status --team server # Team-scoped view
|
||||
db/connectors/sprint start-work --team client # Full context dump for starting work
|
||||
db/connectors/sprint prepare # Prepare next sprint (candidates + gaps)
|
||||
db/connectors/sprint start # Activate a planned sprint
|
||||
db/connectors/sprint stop # Complete an active sprint
|
||||
```
|
||||
Team is auto-detected from the current git branch (if not `main`). Sprint is auto-detected from DB state.
|
||||
**Prefer CLI wrappers over raw SQL.** Never use the `sqlite3` CLI — it crashes in Claude Code (std::bad_alloc). Use the wrapper scripts instead.
|
||||
|
||||
Only fall back to raw SQL for queries the CLI doesn't support. **Never use the `sqlite3` CLI** — it crashes in Claude Code due to a known std::bad_alloc bug. Use the wrapper scripts instead:
|
||||
```bash
|
||||
db/connectors/sqlite-query "SELECT * FROM tickets WHERE status='in_progress'"
|
||||
db/connectors/sqlite-exec "UPDATE tickets SET status='done' WHERE id=1"
|
||||
```
|
||||
|
||||
### Qdrant / document search
|
||||
```bash
|
||||
db/connectors/qdrant-search "asymmetric information design"
|
||||
db/connectors/qdrant-index docs/briefings/tyre.md
|
||||
db/connectors/qdrant-health
|
||||
db/connectors/qdrant-count
|
||||
```
|
||||
|
||||
### Gitea access (tea CLI)
|
||||
**Never access the Gitea API directly** — use the `tea` CLI with all required flags to bypass interactive mode.
|
||||
|
||||
Always pass `--login schweitz --repo jpmschweitzer/settled-reach --output simple` to avoid TTY prompts.
|
||||
|
||||
```bash
|
||||
# List open PRs
|
||||
tea pr list --login schweitz --repo jpmschweitzer/settled-reach --state open --output simple
|
||||
|
||||
# View a PR with comments
|
||||
tea pr --login schweitz --repo jpmschweitzer/settled-reach --comments -o simple <PR_NUMBER>
|
||||
|
||||
# Post a comment on a PR (or issue)
|
||||
tea comment --login schweitz --repo jpmschweitzer/settled-reach <NUMBER> "comment body"
|
||||
|
||||
# Approve a PR
|
||||
tea pr approve --login schweitz --repo jpmschweitzer/settled-reach <PR_NUMBER>
|
||||
|
||||
# List issues
|
||||
tea issue list --login schweitz --repo jpmschweitzer/settled-reach --state open --output simple
|
||||
```
|
||||
|
||||
Key rules:
|
||||
- **All flags must be explicit** — omitting `--login` or `--repo` triggers interactive prompts that crash in Claude Code (no TTY)
|
||||
- **Use `--output simple`** for machine-readable output (no table borders)
|
||||
- **`tea comment` hangs with inline heredocs and multi-line strings.** Always write the comment body to a temp file first, then pass it via `$(cat)`:
|
||||
```bash
|
||||
# Step 1: Write content to .tmp/ (gitignored) using the Write tool
|
||||
# Step 2: Post via cat
|
||||
tea comment --login schweitz --repo jpmschweitzer/settled-reach <NUMBER> "$(cat .tmp/review-branch.md)"
|
||||
```
|
||||
- **`tea pr reject` does not work on your own PRs** — use `tea comment` instead
|
||||
- **Never delete protected branches:** `main`, `maintenance`, `server`, `client`, `copy`, `audio`, `visual`, `ci` are protected on Gitea. Do not use `tea pr clean`, `git push --delete`, or `git branch -D` on these branches.
|
||||
| Tool | Command | Full reference |
|
||||
|------|---------|----------------|
|
||||
| Tickets | `db/connectors/ticket list`, `show`, `create`, `assign` | `/ticket` skill |
|
||||
| Sprints | `db/connectors/sprint status`, `start-work`, `prepare` | `/sprint-start` skill |
|
||||
| SQL queries | `db/connectors/sqlite-query "SELECT ..."` | — |
|
||||
| SQL writes | `db/connectors/sqlite-exec "UPDATE ..."` | — |
|
||||
| Doc search | `db/connectors/qdrant-search "query"` | `/docs-search` skill |
|
||||
| Doc index | `db/connectors/qdrant-index path/to/file.md` | `/docs-search` skill |
|
||||
|
||||
### File conventions
|
||||
- Decisions: domain files in `decisions/` (see `decisions/README.md` for index)
|
||||
@@ -149,35 +66,3 @@ Key rules:
|
||||
- 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
|
||||
|
||||
### Commit conventions
|
||||
Use conventional commits with project-specific scopes:
|
||||
`agents`, `skills`, `docs`, `briefings`, `discussions`, `schema`, `db`, `config`, `engine`, `simulation`, `client`, `ui`, `audio`, `assets`, `meta`
|
||||
|
||||
### Pull requests
|
||||
**Use `tea` (Gitea CLI), not `gh` (GitHub CLI).** The remote is Gitea at `git.schweitz.internal`.
|
||||
|
||||
Always provide all required flags to ensure non-interactive execution:
|
||||
```bash
|
||||
tea pr create \
|
||||
--repo jpmschweitzer/settled-reach \
|
||||
--login schweitz \
|
||||
--title "feat(scope): short description" \
|
||||
--description "PR body here" \
|
||||
--base main \
|
||||
--head branch-name
|
||||
```
|
||||
|
||||
### Large content pushes (team pattern)
|
||||
When producing many files (wiki pages, content batches, bulk docs):
|
||||
1. **Lore librarian** agent (read-only): ingests all source material, answers focused context queries from writers, tracks cross-file consistency
|
||||
2. **Multiple writer** agents (parallel, by domain): each gets a task slice, writes directly to disk using the Write tool — one file at a time, write often, no text accumulation
|
||||
3. **Reviewer** agents (blocked until writing done): check voice consistency, attribute uniformity, style
|
||||
|
||||
Key: writers use Write tool directly (no transcription bottleneck), librarian catches contradictions early, split work by domain not volume.
|
||||
|
||||
### Local services
|
||||
- Gitea: `http://git.schweitz.internal` (login: `schweitz`)
|
||||
- Qdrant: `http://tower-of-joy:6333/`
|
||||
- Ollama: `http://tower-of-joy:11434/` (nomic-embed-text)
|
||||
- Collection: `commonwealth` (768 dimensions, cosine distance)
|
||||
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
@@ -1,4 +1,4 @@
|
||||
[gd_scene load_steps=23 format=3 uid="uid://bswrmh7w8dbgm"]
|
||||
[gd_scene load_steps=24 format=3 uid="uid://bswrmh7w8dbgm"]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/main.gd" id="1_main"]
|
||||
[ext_resource type="Script" path="res://scripts/rendering/world_renderer.gd" id="2_world"]
|
||||
@@ -22,6 +22,7 @@
|
||||
[ext_resource type="PackedScene" path="res://ui/bug_report_dialog.tscn" id="19_bugreport"]
|
||||
[ext_resource type="PackedScene" path="res://ui/settings_dialog.tscn" id="21_settings"]
|
||||
[ext_resource type="Script" path="res://scripts/ui/debug_overlay.gd" id="22_debug"]
|
||||
[ext_resource type="PackedScene" path="res://ui/time_display.tscn" id="23_tdisplay"]
|
||||
|
||||
[node name="Game" type="Node2D"]
|
||||
script = ExtResource("1_main")
|
||||
@@ -118,6 +119,9 @@ zoom = Vector2(2, 2)
|
||||
[node name="InsertOverlay" type="CanvasLayer" parent="."]
|
||||
layer = 10
|
||||
|
||||
; #263: Time display — diegetic insert clock, top-left placeholder (D-013, D-031)
|
||||
[node name="TimeDisplay" parent="InsertOverlay" instance=ExtResource("23_tdisplay")]
|
||||
|
||||
; InteractionPrompt — v0.1 fallback single-line "E - Talk" display
|
||||
[node name="InteractionPrompt" parent="InsertOverlay" instance=ExtResource("9_prompt")]
|
||||
|
||||
|
||||
@@ -240,16 +240,16 @@ func apply_snapshot(snapshot: Dictionary) -> void:
|
||||
medium_sound_events = []
|
||||
close_sound_events = []
|
||||
|
||||
# D-073 (#529): Extract zone_id from the player's current tile (server-authoritative).
|
||||
# O(1) via visible_positions dict would be ideal, but tiles are arrays without
|
||||
# positional indexing — use the same tile iteration below instead.
|
||||
current_zone_id = ""
|
||||
var _px := int(player_position.x)
|
||||
var _py := int(player_position.y)
|
||||
for _ztile in visible_tiles:
|
||||
if _ztile is Dictionary and _ztile.get("x") == _px and _ztile.get("y") == _py:
|
||||
current_zone_id = _ztile.get("zone_id", "")
|
||||
break
|
||||
# D-073 (#529): O(1) zone_id lookup. Build coord→tile dict from member visible_tiles
|
||||
# (populated above from either "tiles" test-mode key or "visible_tiles" live key).
|
||||
# Must use the member var, not snapshot.visible_tiles, so test mode is covered.
|
||||
var _tile_by_coord: Dictionary = {}
|
||||
for vtile in visible_tiles:
|
||||
if vtile is Dictionary and vtile.has("x") and vtile.has("y"):
|
||||
_tile_by_coord[Vector2i(vtile.x, vtile.y)] = vtile
|
||||
var player_pos_key := Vector2i(int(player_position.x), int(player_position.y))
|
||||
var player_tile = _tile_by_coord.get(player_pos_key, null)
|
||||
current_zone_id = player_tile.get("zone_id", "") if player_tile else ""
|
||||
|
||||
# v2: visible_tiles with visibility sectors
|
||||
# Derives visible_positions when not explicitly provided (real server mode)
|
||||
|
||||
@@ -96,6 +96,11 @@ const FACING_INDICATOR_OFFSET: float = 14.0
|
||||
# two columns of text comfortably, leaves world game visible alongside.
|
||||
const DIALOGUE_MAX_WIDTH: int = 1200
|
||||
|
||||
# D-031: Format game-minutes (0..1439) as station local time string "HH:MM".
|
||||
static func format_game_time(time_of_day: int) -> String:
|
||||
var clamped: int = clampi(time_of_day, 0, 1439)
|
||||
return "%02d:%02d" % [clamped / 60, clamped % 60]
|
||||
|
||||
# Default camera zoom — used as fallback when get_camera_2d() returns null
|
||||
const CAMERA_DEFAULT_ZOOM: Vector2 = Vector2(2.0, 2.0)
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ extends Node2D
|
||||
@onready var cursor_renderer = $UILayer/CursorRenderer # D-056: z-layer 7
|
||||
@onready var gauntlet_hud = $UILayer/GauntletHUD # #496: room timer + personal bests
|
||||
@onready var checklist_overlay = $UILayer/ChecklistOverlay # #503: auto-checklist progress
|
||||
@onready var time_display = $InsertOverlay/TimeDisplay # #263: diegetic time display (D-013, D-031)
|
||||
@onready var debug_overlay = $UILayer/DebugOverlay # #511: F3 debug overlay
|
||||
@onready var bug_report_dialog = $ModalLayer/BugReportDialog # #495: F12 WRONG button
|
||||
@onready var settings_dialog = $ModalLayer/SettingsDialog # #528: audio settings (ESC/OPEN_MENU)
|
||||
@@ -104,7 +105,7 @@ func _process(delta: float) -> void:
|
||||
if interaction_list and interaction_list.has_method("update_from_state"):
|
||||
if dialogue_box and dialogue_box.is_dialogue_active():
|
||||
if interaction_list.is_showing():
|
||||
interaction_list._hide()
|
||||
interaction_list.hide_list()
|
||||
else:
|
||||
interaction_list.update_from_state()
|
||||
|
||||
@@ -131,6 +132,10 @@ func _process(delta: float) -> void:
|
||||
if checklist_overlay and checklist_overlay.has_method("update_from_state"):
|
||||
checklist_overlay.update_from_state()
|
||||
|
||||
# #263: Update time display (D-013, D-031)
|
||||
if time_display and time_display.has_method("update_from_state"):
|
||||
time_display.update_from_state()
|
||||
|
||||
# #511: Update debug overlay (F3 toggle, dev tool)
|
||||
if debug_overlay and debug_overlay.has_method("update_from_state"):
|
||||
debug_overlay.update_from_state()
|
||||
|
||||
@@ -2,7 +2,7 @@ class_name EntityRenderer
|
||||
extends Node2D
|
||||
|
||||
# Entity renderer — manages entity sprites under the Entities node
|
||||
# Creates/updates/removes ColorRect children based on entity data
|
||||
# Creates/updates/removes Sprite2D children based on entity data
|
||||
# Entity format (from Protocol v2): {entity_id, x, y, z, kind: {variant, data}, visibility}
|
||||
#
|
||||
# Position lerping: entity sprites smoothly slide between tiles instead of snapping.
|
||||
@@ -11,13 +11,15 @@ extends Node2D
|
||||
#
|
||||
# D-033 colors: Phase 1 defaults by entity kind. Phase 2 (#361) will derive
|
||||
# color from RelationshipState via the knowledge graph.
|
||||
# #540: Sprites at D-019 angle (-72.5° from horizontal). Textures are neutral greyscale;
|
||||
# self_modulate applies D-033 relationship tinting. modulate.a reserved for D-015 dimming.
|
||||
|
||||
const TILE_SIZE: int = Constants.TILE_SIZE
|
||||
# D-044: 24x32 entity footprint within 32x32 visual tile (64x64 source scaled to 32px runtime)
|
||||
# D-044: 24x32 entity footprint within 32x32 visual tile (64x64 source at 0.5 scale = 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.
|
||||
const ENTITY_OFFSET_X: float = 0.0 # sprite fills tile width at 0.5 scale
|
||||
const ENTITY_OFFSET_Y: float = TILE_SIZE - ENTITY_HEIGHT # feet-anchored for correct y-sort with D-019 tilt
|
||||
|
||||
# Lerp speed — framerate-independent exponential smoothing.
|
||||
# At 12.0: ~70% there after 0.1s, ~95% after 0.25s.
|
||||
@@ -27,7 +29,8 @@ const LERP_SPEED: float = 12.0
|
||||
var entity_nodes: Dictionary = {} # entity_id -> Node2D
|
||||
var _entity_targets: Dictionary = {} # entity_id -> Vector2 (target pixel position)
|
||||
var _entity_relationships: Dictionary = {} # #521: entity_id -> String (last relationship)
|
||||
var _entity_tweens: Dictionary = {} # #521: entity_id -> {target: Color, elapsed: float}
|
||||
var _entity_tweens: Dictionary = {} # #521: entity_id -> {from: Color, target: Color, elapsed: float}
|
||||
var _entity_facing: Dictionary = {} # #540: entity_id -> String ("north"/"east"/"south"/"west")
|
||||
|
||||
# #521: Color transition duration in seconds (D-033: "0.5s fade")
|
||||
const COLOR_FADE_DURATION: float = 0.5
|
||||
@@ -48,7 +51,7 @@ func _process(delta: float) -> void:
|
||||
if not node.position.is_equal_approx(target):
|
||||
node.position = node.position.lerp(target, weight)
|
||||
|
||||
# #521: Advance color transitions (manual lerp, testable without SceneTree)
|
||||
# #521: Advance self_modulate transitions (manual lerp, testable without SceneTree)
|
||||
var finished_ids: Array = []
|
||||
for entity_id in _entity_tweens.keys():
|
||||
if not entity_nodes.has(entity_id):
|
||||
@@ -57,8 +60,9 @@ func _process(delta: float) -> void:
|
||||
var tween_data: Dictionary = _entity_tweens[entity_id]
|
||||
tween_data.elapsed += delta
|
||||
var t := clampf(tween_data.elapsed / COLOR_FADE_DURATION, 0.0, 1.0)
|
||||
var node_c: ColorRect = entity_nodes[entity_id] as ColorRect
|
||||
node_c.color = tween_data.from.lerp(tween_data.target, t)
|
||||
var node_s: Sprite2D = entity_nodes[entity_id] as Sprite2D
|
||||
if node_s:
|
||||
node_s.self_modulate = tween_data.from.lerp(tween_data.target, t)
|
||||
if t >= 1.0:
|
||||
finished_ids.append(entity_id)
|
||||
for eid in finished_ids:
|
||||
@@ -92,15 +96,26 @@ func update_entities(entities: Array) -> void:
|
||||
for entity_id in ids_to_remove:
|
||||
_remove_entity_node(entity_id)
|
||||
|
||||
# Create a new entity node with D-033 color and optional facing indicator
|
||||
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_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)
|
||||
# Create a new entity node with D-033 tint and sprite texture at D-019 angle
|
||||
func _create_entity_node(entity_id: int, entity_data: Dictionary) -> void:
|
||||
var entity_node := Sprite2D.new()
|
||||
entity_node.name = "Entity_" + str(entity_id)
|
||||
# centered=false: top-left origin aligns with tile grid.
|
||||
# scale=0.5: maps 64px source texture to 32px runtime (D-043, 2x camera = 64px on screen).
|
||||
entity_node.centered = false
|
||||
entity_node.scale = Vector2(0.5, 0.5)
|
||||
|
||||
# Load sprite for current facing direction
|
||||
var direction := _entity_direction(entity_id, entity_data)
|
||||
_entity_facing[entity_id] = direction
|
||||
var tex := _load_sprite_texture(direction)
|
||||
if tex == null:
|
||||
push_error("EntityRenderer: no texture for entity %d direction '%s' — entity will be invisible" % [entity_id, direction])
|
||||
entity_node.texture = tex
|
||||
|
||||
# D-033: self_modulate for relationship tinting; modulate.a is reserved for D-015 dimming.
|
||||
entity_node.self_modulate = _color_for_kind(entity_data)
|
||||
|
||||
add_child(entity_node)
|
||||
entity_nodes[entity_id] = entity_node
|
||||
@@ -121,12 +136,13 @@ func _create_entity_node(entity_id: int, entity_data: Dictionary) -> void:
|
||||
|
||||
_update_entity_node(entity_id, entity_data)
|
||||
|
||||
# Update an existing entity node (target position, visibility dimming, facing)
|
||||
|
||||
# Update an existing entity node (target position, sprite direction, visibility dimming, facing)
|
||||
func _update_entity_node(entity_id: int, entity_data: Dictionary) -> void:
|
||||
if not entity_nodes.has(entity_id):
|
||||
return
|
||||
|
||||
var entity_node = entity_nodes[entity_id]
|
||||
var node = entity_nodes[entity_id]
|
||||
|
||||
# Update target position — the lerp in _process() will smoothly move there.
|
||||
# Server sends tile-center coords (tile 16 → 16.5), floor to get tile index.
|
||||
@@ -136,38 +152,42 @@ func _update_entity_node(entity_id: int, entity_data: Dictionary) -> void:
|
||||
floorf(entity_data.y) * TILE_SIZE + ENTITY_OFFSET_Y
|
||||
)
|
||||
|
||||
# #521: Detect relationship change → fade D-033 color (0.5s via _process)
|
||||
# #540: Update sprite texture when facing direction changes
|
||||
var new_dir := _entity_direction(entity_id, entity_data)
|
||||
if new_dir != _entity_facing.get(entity_id, ""):
|
||||
_entity_facing[entity_id] = new_dir
|
||||
var new_tex := _load_sprite_texture(new_dir)
|
||||
if new_tex != null:
|
||||
(node as Sprite2D).texture = new_tex
|
||||
# null: keep previous texture rather than going invisible mid-game
|
||||
|
||||
# #521: Detect relationship change → fade D-033 self_modulate (0.5s via _process)
|
||||
var new_rel: String = entity_data.get("relationship", "Unknown")
|
||||
var old_rel: String = _entity_relationships.get(entity_id, "Unknown")
|
||||
if new_rel != old_rel:
|
||||
if new_rel != _entity_relationships.get(entity_id, "Unknown"):
|
||||
_entity_relationships[entity_id] = new_rel
|
||||
var new_color := _color_for_kind(entity_data)
|
||||
_entity_tweens[entity_id] = {
|
||||
"from": entity_node.color,
|
||||
"target": new_color,
|
||||
"from": (node as Sprite2D).self_modulate,
|
||||
"target": _color_for_kind(entity_data),
|
||||
"elapsed": 0.0,
|
||||
}
|
||||
|
||||
# Note: modulate.a (peripheral dimming below) and color (D-033 tint above)
|
||||
# are compositionally independent — both can change simultaneously without
|
||||
# interference. If alpha tweening is added later, coordinate with color tween.
|
||||
|
||||
# v2: Peripheral vision dimming (D-015)
|
||||
# null visibility (v1 backward compat) defaults to full alpha
|
||||
# D-015: Peripheral vision dimming via modulate.a.
|
||||
# Independent from self_modulate (D-033 tint) — both can change simultaneously.
|
||||
var visibility: Variant = entity_data.get("visibility")
|
||||
var target_alpha := Constants.PERIPHERAL_ALPHA if visibility == "Peripheral" else 1.0
|
||||
if not is_equal_approx(entity_node.modulate.a, target_alpha):
|
||||
entity_node.modulate.a = target_alpha
|
||||
if not is_equal_approx(node.modulate.a, target_alpha):
|
||||
node.modulate.a = target_alpha
|
||||
|
||||
# D-054: Update facing indicator from client-side mouse angle (not server).
|
||||
# InputMapper.facing_angle is a continuous float — smoother than octant snapping.
|
||||
if entity_id == GameState.player_entity_id:
|
||||
var indicator = entity_node.get_node_or_null("FacingIndicator")
|
||||
var indicator = node.get_node_or_null("FacingIndicator")
|
||||
if indicator != null:
|
||||
# facing_angle: 0=East, -PI/2=North. Indicator: 0=North (up).
|
||||
# Rotate from North basis: add PI/2 to convert.
|
||||
indicator.rotation = InputMapper.facing_angle + PI / 2.0
|
||||
|
||||
|
||||
# Remove an entity node
|
||||
func _remove_entity_node(entity_id: int) -> void:
|
||||
if not entity_nodes.has(entity_id):
|
||||
@@ -179,13 +199,49 @@ func _remove_entity_node(entity_id: int) -> void:
|
||||
_entity_targets.erase(entity_id)
|
||||
_entity_relationships.erase(entity_id)
|
||||
_entity_tweens.erase(entity_id)
|
||||
_entity_facing.erase(entity_id)
|
||||
|
||||
|
||||
# D-033 color by entity kind — delegates to Constants.color_for_entity_kind
|
||||
static func _color_for_kind(entity_data: Dictionary) -> Color:
|
||||
return Constants.color_for_entity_kind(entity_data)
|
||||
|
||||
# Add a facing direction indicator triangle to the player entity
|
||||
func _add_facing_indicator(parent_node: Control) -> void:
|
||||
|
||||
# #540: Map entity to current 4-direction sprite key.
|
||||
# Player uses GameState.player_facing (8-octant → 4-cardinal). NPCs default "south".
|
||||
func _entity_direction(entity_id: int, _entity_data: Dictionary) -> String:
|
||||
if entity_id == GameState.player_entity_id:
|
||||
return _octant_to_direction(GameState.player_facing)
|
||||
# NPCs: no facing field in v1 entity format; south is viewer-facing (D-019 angle)
|
||||
return "south"
|
||||
|
||||
|
||||
# Map 8-direction octant string to nearest 4-direction sprite key.
|
||||
# N/NW → north, NE/E → east, SE/S → south, SW/W → west
|
||||
static func _octant_to_direction(octant: String) -> String:
|
||||
match octant:
|
||||
"North", "Northwest": return "north"
|
||||
"Northeast", "East": return "east"
|
||||
"Southeast", "South": return "south"
|
||||
"Southwest", "West": return "west"
|
||||
_:
|
||||
push_warning("EntityRenderer: unrecognised octant '%s' — defaulting to south" % octant)
|
||||
return "south"
|
||||
|
||||
|
||||
# Load the sprite texture for the given 4-direction key.
|
||||
# Falls back to null with a push_warning if the asset is missing.
|
||||
static func _load_sprite_texture(direction: String) -> Texture2D:
|
||||
var path := "res://assets/sprites/npc_generic_%s_64.png" % direction
|
||||
if ResourceLoader.exists(path):
|
||||
return load(path) as Texture2D
|
||||
push_warning("EntityRenderer: sprite not found: %s" % path)
|
||||
return null
|
||||
|
||||
|
||||
# Add a facing direction indicator triangle to the player entity.
|
||||
# Indicator position is in Sprite2D local space (64px texture before 0.5 scale → center at (32,32)).
|
||||
func _add_facing_indicator(parent_node: Node2D) -> void:
|
||||
var indicator := Polygon2D.new()
|
||||
indicator.name = "FacingIndicator"
|
||||
var s := Constants.FACING_INDICATOR_SIZE
|
||||
@@ -197,6 +253,7 @@ func _add_facing_indicator(parent_node: Control) -> void:
|
||||
Vector2(s * 0.6, -offset + s * 0.4),
|
||||
])
|
||||
indicator.color = Constants.ENTITY_COLOR_PLAYER
|
||||
# Position at center of parent ColorRect — rotation around this point
|
||||
indicator.position = Vector2(ENTITY_WIDTH / 2.0, ENTITY_HEIGHT / 2.0)
|
||||
# Sprite2D local space: 64px texture at scale 0.5 → center of visible sprite at (32,32).
|
||||
# Indicator rotates around this point to track player facing direction.
|
||||
indicator.position = Vector2(32.0, 32.0)
|
||||
parent_node.add_child(indicator)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
うtickヘ,ヲaction�DialogueResponseげtarget_entity_id*ォresponse_idーkael-davan_d_001
|
||||
@@ -135,43 +135,43 @@ func test_npc_uses_relationship_color_unknown() -> void:
|
||||
var renderer: Node2D = _make_entity_renderer()
|
||||
var entities: Array = [_make_entity(2, "Npc", "Unknown")]
|
||||
renderer.update_entities(entities)
|
||||
var node: ColorRect = renderer.entity_nodes[2] as ColorRect
|
||||
var node: Sprite2D = renderer.entity_nodes[2] as Sprite2D
|
||||
# Unknown -> teal (both Phase 1 and Phase 2 produce the same result)
|
||||
assert_that(node.color).is_equal(Constants.ENTITY_COLOR_UNKNOWN)
|
||||
assert_that(node.self_modulate).is_equal(Constants.ENTITY_COLOR_UNKNOWN)
|
||||
renderer.queue_free()
|
||||
|
||||
func test_npc_uses_relationship_color_friendly() -> void:
|
||||
var renderer: Node2D = _make_entity_renderer()
|
||||
var entities: Array = [_make_entity(2, "Npc", "Friendly")]
|
||||
renderer.update_entities(entities)
|
||||
var node: ColorRect = renderer.entity_nodes[2] as ColorRect
|
||||
var node: Sprite2D = renderer.entity_nodes[2] as Sprite2D
|
||||
if not _entity_uses_relationship(renderer):
|
||||
push_warning("TestColorShift: entity renderer not yet using relationship for color — awaiting #521")
|
||||
renderer.queue_free()
|
||||
return
|
||||
assert_that(node.color).is_equal(Constants.ENTITY_COLOR_FRIENDLY)
|
||||
assert_that(node.self_modulate).is_equal(Constants.ENTITY_COLOR_FRIENDLY)
|
||||
renderer.queue_free()
|
||||
|
||||
func test_npc_uses_relationship_color_poi() -> void:
|
||||
var renderer: Node2D = _make_entity_renderer()
|
||||
var entities: Array = [_make_entity(2, "Npc", "PersonOfInterest")]
|
||||
renderer.update_entities(entities)
|
||||
var node: ColorRect = renderer.entity_nodes[2] as ColorRect
|
||||
var node: Sprite2D = renderer.entity_nodes[2] as Sprite2D
|
||||
if not _entity_uses_relationship(renderer):
|
||||
renderer.queue_free()
|
||||
return
|
||||
assert_that(node.color).is_equal(Constants.ENTITY_COLOR_POI)
|
||||
assert_that(node.self_modulate).is_equal(Constants.ENTITY_COLOR_POI)
|
||||
renderer.queue_free()
|
||||
|
||||
func test_npc_uses_relationship_color_hostile() -> void:
|
||||
var renderer: Node2D = _make_entity_renderer()
|
||||
var entities: Array = [_make_entity(2, "Npc", "Hostile")]
|
||||
renderer.update_entities(entities)
|
||||
var node: ColorRect = renderer.entity_nodes[2] as ColorRect
|
||||
var node: Sprite2D = renderer.entity_nodes[2] as Sprite2D
|
||||
if not _entity_uses_relationship(renderer):
|
||||
renderer.queue_free()
|
||||
return
|
||||
assert_that(node.color).is_equal(Constants.ENTITY_COLOR_HOSTILE)
|
||||
assert_that(node.self_modulate).is_equal(Constants.ENTITY_COLOR_HOSTILE)
|
||||
renderer.queue_free()
|
||||
|
||||
func test_player_color_ignores_relationship() -> void:
|
||||
@@ -179,8 +179,8 @@ func test_player_color_ignores_relationship() -> void:
|
||||
var renderer: Node2D = _make_entity_renderer()
|
||||
var entities: Array = [_make_entity(1, "Player", "Hostile")]
|
||||
renderer.update_entities(entities)
|
||||
var node: ColorRect = renderer.entity_nodes[1] as ColorRect
|
||||
assert_that(node.color).is_equal(Constants.ENTITY_COLOR_PLAYER)
|
||||
var node: Sprite2D = renderer.entity_nodes[1] as Sprite2D
|
||||
assert_that(node.self_modulate).is_equal(Constants.ENTITY_COLOR_PLAYER)
|
||||
renderer.queue_free()
|
||||
|
||||
func test_object_color_ignores_relationship() -> void:
|
||||
@@ -188,8 +188,8 @@ func test_object_color_ignores_relationship() -> void:
|
||||
var renderer: Node2D = _make_entity_renderer()
|
||||
var entities: Array = [_make_entity(3, "Object", "Friendly")]
|
||||
renderer.update_entities(entities)
|
||||
var node: ColorRect = renderer.entity_nodes[3] as ColorRect
|
||||
assert_that(node.color).is_equal(Constants.ENTITY_COLOR_OBJECT)
|
||||
var node: Sprite2D = renderer.entity_nodes[3] as Sprite2D
|
||||
assert_that(node.self_modulate).is_equal(Constants.ENTITY_COLOR_OBJECT)
|
||||
renderer.queue_free()
|
||||
|
||||
|
||||
@@ -204,20 +204,20 @@ func test_color_shift_not_instant() -> void:
|
||||
var renderer: Node2D = _make_entity_renderer()
|
||||
var entities_before: Array = [_make_entity(2, "Npc", "Friendly")]
|
||||
renderer.update_entities(entities_before)
|
||||
var node: ColorRect = renderer.entity_nodes[2] as ColorRect
|
||||
var node: Sprite2D = renderer.entity_nodes[2] as Sprite2D
|
||||
if not _entity_uses_relationship(renderer):
|
||||
renderer.queue_free()
|
||||
return
|
||||
# Change relationship to Hostile
|
||||
var entities_after: Array = [_make_entity(2, "Npc", "Hostile")]
|
||||
renderer.update_entities(entities_after)
|
||||
# Immediately after update, color should NOT yet be the target
|
||||
var color_after_immediate: Color = node.color
|
||||
# Immediately after update, self_modulate should NOT yet be the target
|
||||
var color_after_immediate: Color = node.self_modulate
|
||||
if not _renderer_has_tween_support(renderer):
|
||||
push_warning("TestColorShift: tween on relationship change not implemented yet — awaiting #521")
|
||||
renderer.queue_free()
|
||||
return
|
||||
# The color should NOT be exactly the target yet (tween in progress)
|
||||
# The self_modulate should NOT be exactly the target yet (tween in progress)
|
||||
assert_that(color_after_immediate != Constants.ENTITY_COLOR_HOSTILE).is_true()
|
||||
renderer.queue_free()
|
||||
|
||||
@@ -236,8 +236,8 @@ func test_color_shift_reaches_target() -> void:
|
||||
while elapsed < 1.5:
|
||||
renderer._process(1.0 / 60.0)
|
||||
elapsed += 1.0 / 60.0
|
||||
var node: ColorRect = renderer.entity_nodes[2] as ColorRect
|
||||
assert_that(node.color.is_equal_approx(Constants.ENTITY_COLOR_HOSTILE)).is_true()
|
||||
var node: Sprite2D = renderer.entity_nodes[2] as Sprite2D
|
||||
assert_that(node.self_modulate.is_equal_approx(Constants.ENTITY_COLOR_HOSTILE)).is_true()
|
||||
renderer.queue_free()
|
||||
|
||||
func test_color_shift_mid_transition_retrigger() -> void:
|
||||
@@ -263,8 +263,8 @@ func test_color_shift_mid_transition_retrigger() -> void:
|
||||
while elapsed < 1.0:
|
||||
renderer._process(1.0 / 60.0)
|
||||
elapsed += 1.0 / 60.0
|
||||
var node: ColorRect = renderer.entity_nodes[2] as ColorRect
|
||||
assert_that(node.color.is_equal_approx(Constants.ENTITY_COLOR_HOSTILE)).is_true()
|
||||
var node: Sprite2D = renderer.entity_nodes[2] as Sprite2D
|
||||
assert_that(node.self_modulate.is_equal_approx(Constants.ENTITY_COLOR_HOSTILE)).is_true()
|
||||
renderer.queue_free()
|
||||
|
||||
|
||||
@@ -275,10 +275,10 @@ func test_color_shift_same_relationship_no_tween() -> void:
|
||||
return
|
||||
var entities: Array = [_make_entity(2, "Npc", "Unknown")]
|
||||
renderer.update_entities(entities)
|
||||
var node: ColorRect = renderer.entity_nodes[2] as ColorRect
|
||||
var color_first: Color = node.color
|
||||
var node: Sprite2D = renderer.entity_nodes[2] as Sprite2D
|
||||
var color_first: Color = node.self_modulate
|
||||
renderer.update_entities(entities)
|
||||
var color_second: Color = node.color
|
||||
var color_second: Color = node.self_modulate
|
||||
assert_that(color_first).is_equal(color_second)
|
||||
renderer.queue_free()
|
||||
|
||||
@@ -354,10 +354,12 @@ func _entity_uses_relationship(renderer: Node2D) -> bool:
|
||||
renderer.update_entities([friendly, hostile])
|
||||
if not renderer.entity_nodes.has(10) or not renderer.entity_nodes.has(11):
|
||||
return false
|
||||
var f_node: ColorRect = renderer.entity_nodes[10] as ColorRect
|
||||
var h_node: ColorRect = renderer.entity_nodes[11] as ColorRect
|
||||
var f_color: Color = f_node.color
|
||||
var h_color: Color = h_node.color
|
||||
var f_node: Sprite2D = renderer.entity_nodes[10] as Sprite2D
|
||||
var h_node: Sprite2D = renderer.entity_nodes[11] as Sprite2D
|
||||
if not f_node or not h_node:
|
||||
return false
|
||||
var f_color: Color = f_node.self_modulate
|
||||
var h_color: Color = h_node.self_modulate
|
||||
var uses_rel: bool = not f_color.is_equal_approx(h_color)
|
||||
renderer.update_entities([])
|
||||
return uses_rel
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
## Sprint 17 — UX: E-Talk overlay improvement (#537) — Phase 1
|
||||
## Tests for relationship color indicator in interaction_list.gd.
|
||||
##
|
||||
## Spec refs:
|
||||
## D-033 (entity color = relationship to player)
|
||||
## D-051 (diegetic insert display — insert-only overlay)
|
||||
## D-057 (interaction list, z-layer 6)
|
||||
##
|
||||
## Phase 1 scope (2026-02-24):
|
||||
## Color bar only — NPC name and dialogue tier deferred to Phase 2 pending
|
||||
## server protocol change (no known_attributes in wire protocol v13).
|
||||
##
|
||||
## Implementation: client/ui/interaction_list.gd
|
||||
## `var _relationship_color: Color = Constants.IMPLANT_TEXT_DIM`
|
||||
## `func _cache_entity_relationship() -> void`
|
||||
## 3px left-edge bar drawn in _draw() at 85% alpha, called from update_from_state()
|
||||
class_name TestETalkOverlaySprint17
|
||||
extends GdUnitTestSuite
|
||||
|
||||
|
||||
func before_test() -> void:
|
||||
SimBridge.reset_test_state()
|
||||
GameState.nearby_interactions = []
|
||||
GameState.visible_entities = []
|
||||
GameState.player_stance = ""
|
||||
|
||||
|
||||
func after_test() -> void:
|
||||
GameState.nearby_interactions = []
|
||||
GameState.visible_entities = []
|
||||
GameState.player_stance = ""
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# D-033: Constants.color_for_relationship() — palette baseline (pure logic)
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
func test_relationship_unknown_maps_to_teal() -> void:
|
||||
assert_that(Constants.color_for_relationship("Unknown")).is_equal(Constants.ENTITY_COLOR_UNKNOWN)
|
||||
|
||||
func test_relationship_friendly_maps_to_green() -> void:
|
||||
assert_that(Constants.color_for_relationship("Friendly")).is_equal(Constants.ENTITY_COLOR_FRIENDLY)
|
||||
|
||||
func test_relationship_poi_maps_to_amber() -> void:
|
||||
assert_that(Constants.color_for_relationship("PersonOfInterest")).is_equal(Constants.ENTITY_COLOR_POI)
|
||||
|
||||
func test_relationship_hostile_maps_to_red() -> void:
|
||||
assert_that(Constants.color_for_relationship("Hostile")).is_equal(Constants.ENTITY_COLOR_HOSTILE)
|
||||
|
||||
func test_relationship_known_falls_back_to_unknown_color() -> void:
|
||||
# "Known" not matched in color_for_relationship() — falls through _ → ENTITY_COLOR_UNKNOWN
|
||||
assert_that(Constants.color_for_relationship("Known")).is_equal(Constants.ENTITY_COLOR_UNKNOWN)
|
||||
|
||||
func test_relationship_unknown_string_falls_back_to_unknown() -> void:
|
||||
assert_that(Constants.color_for_relationship("SomeNewState")).is_equal(Constants.ENTITY_COLOR_UNKNOWN)
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# UIStrings: relationship state labels exist (Phase 2 pre-fixture)
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
func test_ui_strings_has_relationship_unknown_label() -> void:
|
||||
assert_that(UIStrings.has_key("relationship_states.unknown.label")).is_true()
|
||||
|
||||
func test_ui_strings_has_relationship_known_label() -> void:
|
||||
assert_that(UIStrings.has_key("relationship_states.known.label")).is_true()
|
||||
|
||||
func test_ui_strings_has_relationship_friendly_label() -> void:
|
||||
assert_that(UIStrings.has_key("relationship_states.friendly.label")).is_true()
|
||||
|
||||
func test_ui_strings_has_relationship_poi_label() -> void:
|
||||
assert_that(UIStrings.has_key("relationship_states.person_of_interest.label")).is_true()
|
||||
|
||||
func test_ui_strings_has_relationship_hostile_label() -> void:
|
||||
assert_that(UIStrings.has_key("relationship_states.hostile.label")).is_true()
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# _relationship_color initial state
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
func test_relationship_color_default_is_implant_dim() -> void:
|
||||
# Before any update, _relationship_color starts at IMPLANT_TEXT_DIM
|
||||
var list = _make_list()
|
||||
assert_that(list._relationship_color).is_equal(Constants.IMPLANT_TEXT_DIM)
|
||||
list.queue_free()
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# _cache_entity_relationship(): D-033 palette via update_from_state() (#537 core)
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
func test_cache_relationship_unknown_sets_unknown_color() -> void:
|
||||
_setup_npc_interaction(2, "Unknown")
|
||||
var list = _make_list()
|
||||
list.update_from_state()
|
||||
assert_that(list._relationship_color).is_equal(Constants.ENTITY_COLOR_UNKNOWN)
|
||||
list.queue_free()
|
||||
|
||||
func test_cache_relationship_friendly_sets_green() -> void:
|
||||
_setup_npc_interaction(2, "Friendly")
|
||||
var list = _make_list()
|
||||
list.update_from_state()
|
||||
assert_that(list._relationship_color).is_equal(Constants.ENTITY_COLOR_FRIENDLY)
|
||||
list.queue_free()
|
||||
|
||||
func test_cache_relationship_poi_sets_amber() -> void:
|
||||
_setup_npc_interaction(2, "PersonOfInterest")
|
||||
var list = _make_list()
|
||||
list.update_from_state()
|
||||
assert_that(list._relationship_color).is_equal(Constants.ENTITY_COLOR_POI)
|
||||
list.queue_free()
|
||||
|
||||
func test_cache_relationship_hostile_sets_red() -> void:
|
||||
_setup_npc_interaction(2, "Hostile")
|
||||
var list = _make_list()
|
||||
list.update_from_state()
|
||||
assert_that(list._relationship_color).is_equal(Constants.ENTITY_COLOR_HOSTILE)
|
||||
list.queue_free()
|
||||
|
||||
func test_cache_relationship_absent_field_uses_unknown_color() -> void:
|
||||
# Entity present but "relationship" key missing → get("relationship", "Unknown") → Unknown color
|
||||
GameState.visible_entities = [{
|
||||
"entity_id": 2, "x": 12.0, "y": 9.0, "z": 0,
|
||||
"kind": {"variant": "Npc", "data": null},
|
||||
"visibility": "Forward",
|
||||
}]
|
||||
GameState.nearby_interactions = [{
|
||||
"entity_id": 2, "entity_type": "Npc", "distance": 1,
|
||||
"verbs": [{"kind": "Talk", "label": "Talk", "priority": 1, "available": true}],
|
||||
}]
|
||||
var list = _make_list()
|
||||
list.update_from_state()
|
||||
assert_that(list._relationship_color).is_equal(Constants.ENTITY_COLOR_UNKNOWN)
|
||||
list.queue_free()
|
||||
|
||||
func test_cache_relationship_entity_not_in_visible_uses_dim_fallback() -> void:
|
||||
# Entity in nearby_interactions but NOT in visible_entities → IMPLANT_TEXT_DIM fallback
|
||||
GameState.visible_entities = []
|
||||
GameState.nearby_interactions = [{
|
||||
"entity_id": 2, "entity_type": "Npc", "distance": 1,
|
||||
"verbs": [{"kind": "Talk", "label": "Talk", "priority": 1, "available": true}],
|
||||
}]
|
||||
var list = _make_list()
|
||||
list.update_from_state()
|
||||
assert_that(list._relationship_color).is_equal(Constants.IMPLANT_TEXT_DIM)
|
||||
list.queue_free()
|
||||
|
||||
func test_cache_relationship_updates_when_relationship_changes() -> void:
|
||||
# First call: Unknown
|
||||
_setup_npc_interaction(2, "Unknown")
|
||||
var list = _make_list()
|
||||
list.update_from_state()
|
||||
assert_that(list._relationship_color).is_equal(Constants.ENTITY_COLOR_UNKNOWN)
|
||||
|
||||
# Relationship shifts → re-cache picks up new value
|
||||
_setup_npc_interaction(2, "Hostile")
|
||||
list.update_from_state()
|
||||
assert_that(list._relationship_color).is_equal(Constants.ENTITY_COLOR_HOSTILE)
|
||||
list.queue_free()
|
||||
|
||||
func test_cache_relationship_targets_correct_entity_by_id() -> void:
|
||||
# Two entities visible; nearby_interactions[0] is the target (entity 2, Friendly)
|
||||
# Entity 3 (Hostile) must not pollute the color
|
||||
GameState.visible_entities = [
|
||||
{
|
||||
"entity_id": 2, "x": 11.0, "y": 9.0, "z": 0,
|
||||
"kind": {"variant": "Npc", "data": null},
|
||||
"visibility": "Forward", "relationship": "Friendly", "observation": "Visible",
|
||||
},
|
||||
{
|
||||
"entity_id": 3, "x": 12.0, "y": 9.0, "z": 0,
|
||||
"kind": {"variant": "Npc", "data": null},
|
||||
"visibility": "Forward", "relationship": "Hostile", "observation": "Visible",
|
||||
},
|
||||
]
|
||||
GameState.nearby_interactions = [
|
||||
{
|
||||
"entity_id": 2, "entity_type": "Npc", "distance": 1,
|
||||
"verbs": [{"kind": "Talk", "label": "Talk", "priority": 1, "available": true}],
|
||||
},
|
||||
{
|
||||
"entity_id": 3, "entity_type": "Npc", "distance": 2,
|
||||
"verbs": [{"kind": "Talk", "label": "Talk", "priority": 1, "available": true}],
|
||||
},
|
||||
]
|
||||
var list = _make_list()
|
||||
list.update_from_state()
|
||||
# interaction[0] = entity 2 (Friendly) → green bar
|
||||
assert_that(list._relationship_color).is_equal(Constants.ENTITY_COLOR_FRIENDLY)
|
||||
list.queue_free()
|
||||
|
||||
func test_z_layer_is_insert_canvas() -> void:
|
||||
# D-049 / D-057: interaction list lives on InsertOverlay (CanvasLayer 10)
|
||||
var list = _make_list()
|
||||
assert_that(list.get_z_layer()).is_equal(Constants.CANVAS_INSERT)
|
||||
list.queue_free()
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Regression: interaction_list public API unaffected by #537 changes
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
func test_list_hides_when_no_interactions() -> void:
|
||||
GameState.nearby_interactions = []
|
||||
var list = _make_list()
|
||||
list.update_from_state()
|
||||
assert_that(list.is_showing()).is_false()
|
||||
list.queue_free()
|
||||
|
||||
func test_list_shows_on_npc_interaction() -> void:
|
||||
_setup_npc_interaction(2, "Unknown")
|
||||
var list = _make_list()
|
||||
list.update_from_state()
|
||||
assert_that(list.is_showing()).is_true()
|
||||
list.queue_free()
|
||||
|
||||
func test_list_get_selected_verb_returns_first_verb() -> void:
|
||||
GameState.nearby_interactions = [{
|
||||
"entity_id": 2, "entity_type": "Npc", "distance": 1,
|
||||
"verbs": [
|
||||
{"kind": "Talk", "label": "Talk", "priority": 1, "available": true},
|
||||
{"kind": "ExamineNpc", "label": "Look", "priority": 2, "available": true},
|
||||
],
|
||||
}]
|
||||
var list = _make_list()
|
||||
assert_that(list.get_selected_verb()).is_equal("Talk")
|
||||
list.queue_free()
|
||||
|
||||
func test_list_get_interaction_target_returns_entity_id() -> void:
|
||||
GameState.visible_entities = [{
|
||||
"entity_id": 5, "x": 12.0, "y": 9.0, "z": 0,
|
||||
"kind": {"variant": "Npc", "data": null},
|
||||
"visibility": "Forward", "relationship": "Unknown", "observation": "Visible",
|
||||
}]
|
||||
GameState.nearby_interactions = [{
|
||||
"entity_id": 5, "entity_type": "Npc", "distance": 1,
|
||||
"verbs": [{"kind": "Talk", "label": "Talk", "priority": 1, "available": true}],
|
||||
}]
|
||||
var list = _make_list()
|
||||
list.update_from_state()
|
||||
assert_that(list.get_interaction_target()).is_equal(5)
|
||||
list.queue_free()
|
||||
|
||||
func test_list_suppressed_when_insert_inactive() -> void:
|
||||
_setup_npc_interaction(2, "Unknown")
|
||||
var list = _make_list()
|
||||
list.set_insert_active(false)
|
||||
list.update_from_state()
|
||||
assert_that(list.is_showing()).is_false()
|
||||
list.queue_free()
|
||||
|
||||
func test_list_hides_on_empty_verb_list() -> void:
|
||||
GameState.nearby_interactions = [{
|
||||
"entity_id": 2, "entity_type": "Npc", "distance": 1, "verbs": [],
|
||||
}]
|
||||
var list = _make_list()
|
||||
list.update_from_state()
|
||||
assert_that(list.is_showing()).is_false()
|
||||
list.queue_free()
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Phase 2 placeholders — deferred pending server protocol change (no known_attributes in v13)
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
func skip_test_npc_name_displayed_when_known() -> void:
|
||||
pass
|
||||
|
||||
func skip_test_dialogue_tier_context_hint_for_friendly() -> void:
|
||||
pass
|
||||
|
||||
func skip_test_dialogue_tier_context_hint_for_hostile() -> void:
|
||||
pass
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
func _make_list() -> Control:
|
||||
var scene = load("res://ui/interaction_list.tscn")
|
||||
var list = scene.instantiate()
|
||||
add_child(list) # _ready() fires here — @onready var _vbox resolves
|
||||
return list
|
||||
|
||||
|
||||
## Set up GameState with a single NPC entity + matching interaction for tests.
|
||||
func _setup_npc_interaction(entity_id: int, relationship: String) -> void:
|
||||
GameState.visible_entities = [{
|
||||
"entity_id": entity_id, "x": 12.0, "y": 9.0, "z": 0,
|
||||
"kind": {"variant": "Npc", "data": null},
|
||||
"visibility": "Forward", "relationship": relationship, "observation": "Visible",
|
||||
}]
|
||||
GameState.nearby_interactions = [{
|
||||
"entity_id": entity_id, "entity_type": "Npc", "distance": 1,
|
||||
"verbs": [{"kind": "Talk", "label": "Talk", "priority": 1, "available": true}],
|
||||
}]
|
||||
@@ -0,0 +1,211 @@
|
||||
## Sprint 16 #543: GameState.current_zone_id population tests.
|
||||
## Verifies that apply_snapshot() correctly extracts zone_id from the player's
|
||||
## current tile after the O(N) → O(1) refactor. Behavior must be identical
|
||||
## before and after the change (net-zero behavioral change per ticket spec).
|
||||
## Spec refs: D-073, #543, #529.
|
||||
class_name TestSnapshotZoneId
|
||||
extends GdUnitTestSuite
|
||||
|
||||
|
||||
func before_test() -> void:
|
||||
GameState.current_zone_id = ""
|
||||
GameState.player_position = Vector2.ZERO
|
||||
GameState.visible_tiles = []
|
||||
GameState.visible_positions = {}
|
||||
GameState.visibility_sectors = {}
|
||||
|
||||
|
||||
# -- Happy path (S16-Z01) ------------------------------------------------------
|
||||
|
||||
func test_zone_id_populated_when_player_on_zone_tile() -> void:
|
||||
# S16-Z01: Player at (5,5), visible_tiles has zone_id "zone_alpha" at (5,5).
|
||||
# D-073: current_zone_id must be set to the server-authoritative zone_id.
|
||||
GameState.apply_snapshot({
|
||||
"tick": 1,
|
||||
"entities": [
|
||||
{"entity_id": 1, "x": 5.0, "y": 5.0, "z": 0, "kind": {"variant": "Player", "data": null}},
|
||||
],
|
||||
"visible_tiles": [
|
||||
{"x": 3, "y": 3, "z": 0, "type": "floor", "zone_id": "zone_foyer"},
|
||||
{"x": 5, "y": 5, "z": 0, "type": "floor", "zone_id": "zone_alpha"},
|
||||
{"x": 7, "y": 7, "z": 0, "type": "floor", "zone_id": "zone_beta"},
|
||||
],
|
||||
})
|
||||
assert_that(GameState.current_zone_id).override_failure_message(
|
||||
"current_zone_id must match zone_id at player tile (5,5)"
|
||||
).is_equal("zone_alpha")
|
||||
assert_that(GameState.current_zone_id.is_empty()).override_failure_message(
|
||||
"current_zone_id must be non-empty when player is on a zone-tagged tile"
|
||||
).is_false()
|
||||
|
||||
|
||||
# -- Missing zone_id field (S16-Z02) -------------------------------------------
|
||||
|
||||
func test_zone_id_empty_when_tile_lacks_zone_field() -> void:
|
||||
# S16-Z02: Player's tile exists but has no zone_id key → empty string.
|
||||
# Server may send tiles without zone_id when tile is unzoned.
|
||||
GameState.apply_snapshot({
|
||||
"tick": 1,
|
||||
"entities": [
|
||||
{"entity_id": 1, "x": 5.0, "y": 5.0, "z": 0, "kind": {"variant": "Player", "data": null}},
|
||||
],
|
||||
"visible_tiles": [
|
||||
{"x": 5, "y": 5, "z": 0, "type": "floor"},
|
||||
],
|
||||
})
|
||||
assert_that(GameState.current_zone_id).override_failure_message(
|
||||
"Tile without zone_id field → current_zone_id must default to empty string"
|
||||
).is_equal("")
|
||||
|
||||
|
||||
# -- Player off-tile (S16-Z03) -------------------------------------------------
|
||||
|
||||
func test_zone_id_empty_when_player_not_on_any_tile() -> void:
|
||||
# S16-Z03: Player at (99,99) but no tile at that position → empty string.
|
||||
GameState.apply_snapshot({
|
||||
"tick": 1,
|
||||
"entities": [
|
||||
{"entity_id": 1, "x": 99.0, "y": 99.0, "z": 0, "kind": {"variant": "Player", "data": null}},
|
||||
],
|
||||
"visible_tiles": [
|
||||
{"x": 5, "y": 5, "z": 0, "type": "floor", "zone_id": "zone_alpha"},
|
||||
],
|
||||
})
|
||||
assert_that(GameState.current_zone_id).override_failure_message(
|
||||
"Player at (99,99) with no matching tile → current_zone_id must be empty"
|
||||
).is_equal("")
|
||||
|
||||
|
||||
# -- Empty tiles (S16-Z04) -----------------------------------------------------
|
||||
|
||||
func test_zone_id_empty_on_empty_visible_tiles() -> void:
|
||||
# S16-Z04: No visible tiles at all → empty string, no crash.
|
||||
GameState.apply_snapshot({
|
||||
"tick": 1,
|
||||
"entities": [
|
||||
{"entity_id": 1, "x": 5.0, "y": 5.0, "z": 0, "kind": {"variant": "Player", "data": null}},
|
||||
],
|
||||
"visible_tiles": [],
|
||||
})
|
||||
assert_that(GameState.current_zone_id).override_failure_message(
|
||||
"Empty visible_tiles → current_zone_id must be empty string (no crash)"
|
||||
).is_equal("")
|
||||
|
||||
|
||||
# -- Correct tile selected among many (S16-Z05) --------------------------------
|
||||
|
||||
func test_zone_id_selects_correct_tile_among_many() -> void:
|
||||
# S16-Z05: 10x10 tile grid, player at (6,4). Only zone_6_4 must be selected.
|
||||
# Tests that the lookup doesn't accidentally match a neighboring tile.
|
||||
var tiles: Array = []
|
||||
for tx in range(10):
|
||||
for ty in range(10):
|
||||
tiles.append({"x": tx, "y": ty, "z": 0, "type": "floor",
|
||||
"zone_id": "zone_%d_%d" % [tx, ty]})
|
||||
GameState.apply_snapshot({
|
||||
"tick": 1,
|
||||
"entities": [
|
||||
{"entity_id": 1, "x": 6.0, "y": 4.0, "z": 0, "kind": {"variant": "Player", "data": null}},
|
||||
],
|
||||
"visible_tiles": tiles,
|
||||
})
|
||||
assert_that(GameState.current_zone_id).override_failure_message(
|
||||
"Player at (6,4) must get zone_6_4, not a neighboring tile"
|
||||
).is_equal("zone_6_4")
|
||||
|
||||
|
||||
# -- Zone transition (S16-Z06) -------------------------------------------------
|
||||
|
||||
func test_zone_id_updates_on_zone_transition() -> void:
|
||||
# S16-Z06: Player moves from zone_alpha (5,5) to zone_beta (6,5).
|
||||
# current_zone_id must update on each snapshot.
|
||||
GameState.apply_snapshot({
|
||||
"tick": 1,
|
||||
"entities": [
|
||||
{"entity_id": 1, "x": 5.0, "y": 5.0, "z": 0, "kind": {"variant": "Player", "data": null}},
|
||||
],
|
||||
"visible_tiles": [
|
||||
{"x": 5, "y": 5, "z": 0, "type": "floor", "zone_id": "zone_alpha"},
|
||||
{"x": 6, "y": 5, "z": 0, "type": "floor", "zone_id": "zone_beta"},
|
||||
],
|
||||
})
|
||||
assert_that(GameState.current_zone_id).is_equal("zone_alpha")
|
||||
|
||||
GameState.apply_snapshot({
|
||||
"tick": 2,
|
||||
"entities": [
|
||||
{"entity_id": 1, "x": 6.0, "y": 5.0, "z": 0, "kind": {"variant": "Player", "data": null}},
|
||||
],
|
||||
"visible_tiles": [
|
||||
{"x": 5, "y": 5, "z": 0, "type": "floor", "zone_id": "zone_alpha"},
|
||||
{"x": 6, "y": 5, "z": 0, "type": "floor", "zone_id": "zone_beta"},
|
||||
],
|
||||
})
|
||||
assert_that(GameState.current_zone_id).override_failure_message(
|
||||
"After moving to (6,5), current_zone_id must update to zone_beta"
|
||||
).is_equal("zone_beta")
|
||||
|
||||
|
||||
# -- Regression: visible_positions unaffected (S16-Z07) -----------------------
|
||||
|
||||
func test_visible_positions_unaffected_by_zone_id_refactor() -> void:
|
||||
# S16-Z07: The O(1) refactor must not break visible_positions population.
|
||||
# Both zone_id and visible_positions derive from the same visible_tiles loop —
|
||||
# verify both are correctly populated after a single apply_snapshot().
|
||||
GameState.apply_snapshot({
|
||||
"tick": 1,
|
||||
"entities": [
|
||||
{"entity_id": 1, "x": 5.0, "y": 5.0, "z": 0, "kind": {"variant": "Player", "data": null}},
|
||||
],
|
||||
"visible_tiles": [
|
||||
{"x": 5, "y": 5, "z": 0, "type": "floor", "zone_id": "zone_alpha", "visibility": "Forward"},
|
||||
{"x": 6, "y": 5, "z": 0, "type": "floor", "zone_id": "zone_beta", "visibility": "Peripheral"},
|
||||
],
|
||||
})
|
||||
assert_that(GameState.visible_positions.has(Vector2i(5, 5))).override_failure_message(
|
||||
"visible_positions must still contain (5,5) after zone_id refactor"
|
||||
).is_true()
|
||||
assert_that(GameState.visible_positions.has(Vector2i(6, 5))).is_true()
|
||||
assert_that(GameState.current_zone_id).override_failure_message(
|
||||
"current_zone_id must be zone_alpha after same apply_snapshot call"
|
||||
).is_equal("zone_alpha")
|
||||
|
||||
|
||||
# -- Fractional player position (S16-Z08) --------------------------------------
|
||||
|
||||
func test_zone_id_uses_int_truncation_of_player_position() -> void:
|
||||
# S16-Z08: Player at (5.7, 5.9) → int(5.7)=5, int(5.9)=5 → matches tile (5,5).
|
||||
# Server sends player coords as floats; zone lookup must truncate to tile index.
|
||||
GameState.apply_snapshot({
|
||||
"tick": 1,
|
||||
"entities": [
|
||||
{"entity_id": 1, "x": 5.7, "y": 5.9, "z": 0, "kind": {"variant": "Player", "data": null}},
|
||||
],
|
||||
"visible_tiles": [
|
||||
{"x": 5, "y": 5, "z": 0, "type": "floor", "zone_id": "zone_alpha"},
|
||||
],
|
||||
})
|
||||
assert_that(GameState.current_zone_id).override_failure_message(
|
||||
"Player at (5.7,5.9) must match tile (5,5) — int() truncation applies"
|
||||
).is_equal("zone_alpha")
|
||||
|
||||
|
||||
# -- Test-mode tiles key (S16-Z09) --------------------------------------------
|
||||
|
||||
func test_zone_id_works_with_test_mode_tiles_key() -> void:
|
||||
# S16-Z09: Test mode sends "tiles" key, not "visible_tiles".
|
||||
# The O(1) refactor uses member visible_tiles (covers both paths).
|
||||
# Regression guard: if _tile_by_coord is moved into the snapshot.visible_tiles
|
||||
# block only, this test fails — catching the exact regression Tyre flagged.
|
||||
GameState.apply_snapshot({
|
||||
"tick": 1,
|
||||
"entities": [
|
||||
{"entity_id": 1, "x": 5.0, "y": 5.0, "z": 0, "kind": {"variant": "Player", "data": null}},
|
||||
],
|
||||
"tiles": [
|
||||
{"x": 5, "y": 5, "z": 0, "type": "floor", "zone_id": "zone_alpha"},
|
||||
],
|
||||
})
|
||||
assert_that(GameState.current_zone_id).override_failure_message(
|
||||
"Test-mode 'tiles' key must populate zone_id via member visible_tiles"
|
||||
).is_equal("zone_alpha")
|
||||
@@ -0,0 +1,292 @@
|
||||
## Sprint 16 #540: Sprite integration tests.
|
||||
## Tests z-sorting with real sprites, 24x32 D-044 footprint within D-066 64x64
|
||||
## bounding box, sprite asset existence from #541, and fog shader independence.
|
||||
## Spec refs: D-019, D-043, D-044, D-049, D-066, #540, #541.
|
||||
class_name TestSpriteIntegration
|
||||
extends GdUnitTestSuite
|
||||
|
||||
var EntityRendererScript = load("res://scripts/rendering/entity_renderer.gd")
|
||||
|
||||
|
||||
func before_test() -> void:
|
||||
GameState.player_entity_id = 1
|
||||
GameState.player_position = Vector2.ZERO
|
||||
GameState.visible_entities = []
|
||||
|
||||
|
||||
# -- Helpers -------------------------------------------------------------------
|
||||
|
||||
func _make_entity_renderer() -> Node2D:
|
||||
var renderer = Node2D.new()
|
||||
renderer.set_script(EntityRendererScript)
|
||||
add_child(renderer)
|
||||
return renderer
|
||||
|
||||
|
||||
# -- Footprint constants: D-044 spec (S16-S01, S16-S02) -----------------------
|
||||
|
||||
func test_entity_footprint_matches_d044_spec() -> void:
|
||||
# S16-S01: D-044 specifies 24x32 entity footprint within 32x32 visual tile.
|
||||
# (64x64 source sprite scaled to 32px runtime at 2x retina per D-066).
|
||||
assert_that(EntityRenderer.ENTITY_WIDTH).override_failure_message(
|
||||
"D-044: ENTITY_WIDTH must be 24px"
|
||||
).is_equal(24)
|
||||
assert_that(EntityRenderer.ENTITY_HEIGHT).override_failure_message(
|
||||
"D-044: ENTITY_HEIGHT must be 32px"
|
||||
).is_equal(32)
|
||||
|
||||
|
||||
func test_entity_footprint_within_d066_2x2_sim_tile_bounding_box() -> void:
|
||||
# S16-S02: D-066 requires entity sprite footprint contained within 2x2 sim tile
|
||||
# bounding box. At 32px/tile → 64x64px max. Entity must fit to keep interaction
|
||||
# range (2 sim tiles) accurate with the tilted perspective.
|
||||
var tile_2x: int = Constants.TILE_SIZE * 2
|
||||
assert_that(EntityRenderer.ENTITY_WIDTH <= tile_2x).override_failure_message(
|
||||
"D-066: ENTITY_WIDTH %d must fit within 2x tile width %dpx" % [
|
||||
EntityRenderer.ENTITY_WIDTH, tile_2x]
|
||||
).is_true()
|
||||
assert_that(EntityRenderer.ENTITY_HEIGHT <= tile_2x).override_failure_message(
|
||||
"D-066: ENTITY_HEIGHT %d must fit within 2x tile height %dpx" % [
|
||||
EntityRenderer.ENTITY_HEIGHT, tile_2x]
|
||||
).is_true()
|
||||
|
||||
|
||||
func test_entity_width_fits_within_single_tile() -> void:
|
||||
# S16-S03: Entity width (24) < TILE_SIZE (32) → centered within tile.
|
||||
# Ensures horizontal centering offset is positive and entity doesn't overflow.
|
||||
assert_that(EntityRenderer.ENTITY_WIDTH < Constants.TILE_SIZE).override_failure_message(
|
||||
"Entity width must be less than TILE_SIZE for centered layout"
|
||||
).is_true()
|
||||
assert_that(EntityRenderer.ENTITY_OFFSET_X >= 0.0).override_failure_message(
|
||||
"ENTITY_OFFSET_X must be non-negative for horizontal centering"
|
||||
).is_true()
|
||||
|
||||
|
||||
# -- Pixel position (S16-S04) -------------------------------------------------
|
||||
|
||||
func test_entity_pixel_position_at_tile_3_7() -> void:
|
||||
# S16-S04: Entity at tile (3.0, 7.0) → pixel position must be
|
||||
# (3 * TILE_SIZE + ENTITY_OFFSET_X, 7 * TILE_SIZE + ENTITY_OFFSET_Y).
|
||||
var renderer := _make_entity_renderer()
|
||||
var entity := [{"entity_id": 10, "x": 3.0, "y": 7.0, "z": 0,
|
||||
"kind": {"variant": "Npc", "data": null}}]
|
||||
renderer.update_entities(entity)
|
||||
var node = renderer.entity_nodes[10]
|
||||
var expected_x := 3.0 * Constants.TILE_SIZE + EntityRenderer.ENTITY_OFFSET_X
|
||||
var expected_y := 7.0 * Constants.TILE_SIZE + EntityRenderer.ENTITY_OFFSET_Y
|
||||
assert_that(node.position.x).override_failure_message(
|
||||
"Entity x must be tile_x * TILE_SIZE + ENTITY_OFFSET_X"
|
||||
).is_equal_approx(expected_x, 0.1)
|
||||
assert_that(node.position.y).override_failure_message(
|
||||
"Entity y must be tile_y * TILE_SIZE + ENTITY_OFFSET_Y"
|
||||
).is_equal_approx(expected_y, 0.1)
|
||||
renderer.queue_free()
|
||||
|
||||
|
||||
# -- Z-sort ordering: D-049 y-based (S16-S05, S16-S06) -----------------------
|
||||
|
||||
func test_z_sort_south_entity_has_higher_pixel_y() -> void:
|
||||
# S16-S05: D-049 y-sort — entity at y=8 (south) must have higher pixel.y
|
||||
# than entity at y=4 (north). Godot y-sort renders higher-y on top.
|
||||
# With tilted sprites, south-facing entity must visually overlap northern.
|
||||
var renderer := _make_entity_renderer()
|
||||
var entities := [
|
||||
{"entity_id": 20, "x": 5.0, "y": 4.0, "z": 0, "kind": {"variant": "Npc", "data": null}},
|
||||
{"entity_id": 21, "x": 5.0, "y": 8.0, "z": 0, "kind": {"variant": "Npc", "data": null}},
|
||||
]
|
||||
renderer.update_entities(entities)
|
||||
var north_node = renderer.entity_nodes[20]
|
||||
var south_node = renderer.entity_nodes[21]
|
||||
assert_that(south_node.position.y > north_node.position.y).override_failure_message(
|
||||
"Entity at y=8 must have higher pixel.y than entity at y=4 for y-sort"
|
||||
).is_true()
|
||||
renderer.queue_free()
|
||||
|
||||
|
||||
func test_z_sort_y_position_difference_equals_tile_size() -> void:
|
||||
# S16-S06: Two entities one tile apart in y → pixel y difference = TILE_SIZE.
|
||||
# Verifies position calculation is consistent for adjacent tiles.
|
||||
var renderer := _make_entity_renderer()
|
||||
var entities := [
|
||||
{"entity_id": 30, "x": 5.0, "y": 3.0, "z": 0, "kind": {"variant": "Npc", "data": null}},
|
||||
{"entity_id": 31, "x": 5.0, "y": 4.0, "z": 0, "kind": {"variant": "Npc", "data": null}},
|
||||
]
|
||||
renderer.update_entities(entities)
|
||||
var node3 = renderer.entity_nodes[30]
|
||||
var node4 = renderer.entity_nodes[31]
|
||||
var delta_y := node4.position.y - node3.position.y
|
||||
assert_that(delta_y).override_failure_message(
|
||||
"Adjacent tiles must differ by exactly TILE_SIZE (%dpx) in y" % Constants.TILE_SIZE
|
||||
).is_equal_approx(float(Constants.TILE_SIZE), 0.1)
|
||||
renderer.queue_free()
|
||||
|
||||
|
||||
func test_z_sort_same_y_different_x_no_y_difference() -> void:
|
||||
# S16-S07: Two entities at same y but different x → same pixel.y.
|
||||
# Horizontal position must not affect y-sort order.
|
||||
var renderer := _make_entity_renderer()
|
||||
var entities := [
|
||||
{"entity_id": 40, "x": 2.0, "y": 5.0, "z": 0, "kind": {"variant": "Npc", "data": null}},
|
||||
{"entity_id": 41, "x": 8.0, "y": 5.0, "z": 0, "kind": {"variant": "Npc", "data": null}},
|
||||
]
|
||||
renderer.update_entities(entities)
|
||||
var left_node = renderer.entity_nodes[40]
|
||||
var right_node = renderer.entity_nodes[41]
|
||||
assert_that(left_node.position.y).override_failure_message(
|
||||
"Entities at same y-tile must have same pixel.y regardless of x"
|
||||
).is_equal_approx(right_node.position.y, 0.1)
|
||||
renderer.queue_free()
|
||||
|
||||
|
||||
# -- Sprite assets from #541 (S16-S08, S16-S09) --------------------------------
|
||||
|
||||
func test_npc_sprite_assets_exist_for_all_cardinal_directions() -> void:
|
||||
# S16-S08: #541 delivers 64px NPC sprites for all four cardinal directions.
|
||||
# entity_renderer.gd must be able to load these paths.
|
||||
for direction in ["north", "east", "south", "west"]:
|
||||
var path := "res://assets/sprites/npc_generic_%s_64.png" % direction
|
||||
assert_that(ResourceLoader.exists(path)).override_failure_message(
|
||||
"NPC sprite missing: %s" % path
|
||||
).is_true()
|
||||
|
||||
|
||||
func test_wall_sprite_assets_exist_for_all_cardinal_directions() -> void:
|
||||
# S16-S09: #541 delivers 64px wall sprites for all four cardinal directions.
|
||||
for direction in ["north", "east", "south", "west"]:
|
||||
var path := "res://assets/sprites/wall_structural_%s_64.png" % direction
|
||||
assert_that(ResourceLoader.exists(path)).override_failure_message(
|
||||
"Wall sprite missing: %s" % path
|
||||
).is_true()
|
||||
|
||||
|
||||
# -- Fog shader independence: D-019 (S16-S10, S16-S11) -----------------------
|
||||
|
||||
func test_fog_shader_script_and_gdshader_load_correctly() -> void:
|
||||
# S16-S10: fog_shader.gd and fog.gdshader must remain intact after sprite
|
||||
# changes. D-019: "fog vision cone math remains pure 2D" — unaffected by
|
||||
# the art-direction tilt baked into sprites.
|
||||
assert_that(ResourceLoader.exists("res://scripts/rendering/fog_shader.gd")).override_failure_message(
|
||||
"fog_shader.gd must load correctly — must not be affected by sprite changes"
|
||||
).is_true()
|
||||
assert_that(ResourceLoader.exists("res://shaders/fog.gdshader")).override_failure_message(
|
||||
"fog.gdshader must exist — fog is screen-space and sprite-independent"
|
||||
).is_true()
|
||||
|
||||
|
||||
func test_fog_update_runs_independently_of_entity_renderer_state() -> void:
|
||||
# S16-S11: FogState.update_from_state() must succeed with no entity renderer
|
||||
# active. D-019: fog driven by LOS mask (visible_positions), not sprites.
|
||||
var fog = get_node_or_null("/root/FogState")
|
||||
if fog == null:
|
||||
push_warning("TestSpriteIntegration: FogState not available — fog independence test skipped")
|
||||
return
|
||||
# Provide visibility data but no entity renderer context
|
||||
GameState.visible_positions = {Vector2i(5, 5): true, Vector2i(6, 5): true}
|
||||
GameState.visibility_sectors = {
|
||||
Vector2i(5, 5): "Forward",
|
||||
Vector2i(6, 5): "Peripheral",
|
||||
}
|
||||
if fog.has_method("update_from_state"):
|
||||
fog.update_from_state()
|
||||
assert_that(fog.visibility_texture).override_failure_message(
|
||||
"FogState visibility_texture must be populated independently of sprite state"
|
||||
).is_not_null()
|
||||
GameState.visible_positions.clear()
|
||||
GameState.visibility_sectors.clear()
|
||||
|
||||
|
||||
# -- Direction mapping: _octant_to_direction (S16-S12 through S16-S21) --------
|
||||
|
||||
func test_octant_north_maps_to_north() -> void:
|
||||
# S16-S12: "North" → "north"
|
||||
assert_that(EntityRenderer._octant_to_direction("North")).is_equal("north")
|
||||
|
||||
func test_octant_northwest_maps_to_north() -> void:
|
||||
# S16-S13: "Northwest" → "north" (grouped with North per mapping spec)
|
||||
assert_that(EntityRenderer._octant_to_direction("Northwest")).is_equal("north")
|
||||
|
||||
func test_octant_northeast_maps_to_east() -> void:
|
||||
# S16-S14: "Northeast" → "east"
|
||||
assert_that(EntityRenderer._octant_to_direction("Northeast")).is_equal("east")
|
||||
|
||||
func test_octant_east_maps_to_east() -> void:
|
||||
# S16-S15: "East" → "east"
|
||||
assert_that(EntityRenderer._octant_to_direction("East")).is_equal("east")
|
||||
|
||||
func test_octant_southeast_maps_to_south() -> void:
|
||||
# S16-S16: "Southeast" → "south"
|
||||
assert_that(EntityRenderer._octant_to_direction("Southeast")).is_equal("south")
|
||||
|
||||
func test_octant_south_maps_to_south() -> void:
|
||||
# S16-S17: "South" → "south"
|
||||
assert_that(EntityRenderer._octant_to_direction("South")).is_equal("south")
|
||||
|
||||
func test_octant_southwest_maps_to_west() -> void:
|
||||
# S16-S18: "Southwest" → "west"
|
||||
assert_that(EntityRenderer._octant_to_direction("Southwest")).is_equal("west")
|
||||
|
||||
func test_octant_west_maps_to_west() -> void:
|
||||
# S16-S19: "West" → "west"
|
||||
assert_that(EntityRenderer._octant_to_direction("West")).is_equal("west")
|
||||
|
||||
func test_octant_unknown_string_falls_back_to_south() -> void:
|
||||
# S16-S20: Unknown string → "south" fallback (safe default — viewer-facing per D-019)
|
||||
assert_that(EntityRenderer._octant_to_direction("Unknown")).is_equal("south")
|
||||
assert_that(EntityRenderer._octant_to_direction("invalid")).is_equal("south")
|
||||
|
||||
func test_octant_empty_string_falls_back_to_south() -> void:
|
||||
# S16-S21: Empty string → "south" fallback
|
||||
assert_that(EntityRenderer._octant_to_direction("")).is_equal("south")
|
||||
|
||||
|
||||
# -- Direction mapping: _entity_direction (S16-S22 through S16-S25) ----------
|
||||
|
||||
func test_entity_direction_npc_always_south() -> void:
|
||||
# S16-S22: NPC entity → always "south" regardless of any data field.
|
||||
# NPCs have no facing in v1 entity format; south is viewer-facing (D-019 angle).
|
||||
var renderer := _make_entity_renderer()
|
||||
GameState.player_entity_id = 1
|
||||
# entity_id 99 is not the player
|
||||
var dir := renderer._entity_direction(99, {"entity_id": 99,
|
||||
"kind": {"variant": "Npc", "data": null}})
|
||||
assert_that(dir).override_failure_message(
|
||||
"NPC entity must always return 'south'"
|
||||
).is_equal("south")
|
||||
renderer.queue_free()
|
||||
|
||||
func test_entity_direction_player_uses_player_facing() -> void:
|
||||
# S16-S23: Player entity → uses GameState.player_facing via _octant_to_direction.
|
||||
var renderer := _make_entity_renderer()
|
||||
GameState.player_entity_id = 1
|
||||
GameState.player_facing = "North"
|
||||
var dir := renderer._entity_direction(1, {"entity_id": 1,
|
||||
"kind": {"variant": "Player", "data": null}})
|
||||
assert_that(dir).override_failure_message(
|
||||
"Player entity with player_facing='North' must return 'north'"
|
||||
).is_equal("north")
|
||||
renderer.queue_free()
|
||||
|
||||
func test_entity_direction_player_facing_east() -> void:
|
||||
# S16-S24: Player facing "East" → "east"
|
||||
var renderer := _make_entity_renderer()
|
||||
GameState.player_entity_id = 1
|
||||
GameState.player_facing = "East"
|
||||
var dir := renderer._entity_direction(1, {"entity_id": 1,
|
||||
"kind": {"variant": "Player", "data": null}})
|
||||
assert_that(dir).override_failure_message(
|
||||
"Player entity with player_facing='East' must return 'east'"
|
||||
).is_equal("east")
|
||||
renderer.queue_free()
|
||||
|
||||
func test_entity_direction_player_facing_diagonal_uses_nearest_cardinal() -> void:
|
||||
# S16-S25: Player facing "Northwest" → "north" (nearest cardinal mapping).
|
||||
# Diagonal octants map to one of the four cardinal sprite sets.
|
||||
var renderer := _make_entity_renderer()
|
||||
GameState.player_entity_id = 1
|
||||
GameState.player_facing = "Northwest"
|
||||
var dir := renderer._entity_direction(1, {"entity_id": 1,
|
||||
"kind": {"variant": "Player", "data": null}})
|
||||
assert_that(dir).override_failure_message(
|
||||
"Player entity with player_facing='Northwest' must return 'north'"
|
||||
).is_equal("north")
|
||||
renderer.queue_free()
|
||||
@@ -0,0 +1,318 @@
|
||||
## Sprint 17 — Time display on insert HUD (#263)
|
||||
## Tests for Constants.format_game_time(), InsertClock wiring, and GameState integration.
|
||||
##
|
||||
## Spec refs:
|
||||
## D-031 (game time: 10 ticks = 1 game-minute, 1440 min/day, HH:MM display)
|
||||
## D-051 (diegetic insert display)
|
||||
##
|
||||
## Implementation: client/ui/time_display.gd — draw-based Control at InsertOverlay/TimeDisplay.
|
||||
## Format function: Constants.format_game_time(time_of_day: int) -> String (extracted for
|
||||
## testability from time_display.gd:46 inline Constants.format_game_time(tod)).
|
||||
##
|
||||
## Private state access: Tests read _time_str, _phase_str, _day_str, _has_data directly
|
||||
## because time_display.gd is draw-based (no Label nodes to inspect). This is an accepted
|
||||
## test pattern for draw-based UI — the private vars ARE the rendered output contract.
|
||||
## If the rendering approach changes (e.g. to Label nodes), these tests should switch to
|
||||
## reading Label.text via public node paths instead.
|
||||
class_name TestTimeDisplaySprint17
|
||||
extends GdUnitTestSuite
|
||||
|
||||
var _clock: Control = null
|
||||
|
||||
|
||||
func before_test() -> void:
|
||||
SimBridge.reset_test_state()
|
||||
GameState.game_time = {}
|
||||
var ClockScript = load("res://ui/time_display.gd")
|
||||
_clock = Control.new()
|
||||
_clock.set_script(ClockScript)
|
||||
add_child(_clock)
|
||||
|
||||
|
||||
func after_test() -> void:
|
||||
if _clock and is_instance_valid(_clock):
|
||||
_clock.queue_free()
|
||||
_clock = null
|
||||
GameState.game_time = {}
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Constants.format_game_time() — pure logic, D-031
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
func test_format_midnight() -> void:
|
||||
assert_that(Constants.format_game_time(0)).is_equal("00:00")
|
||||
|
||||
func test_format_morning_start() -> void:
|
||||
# 360 game-minutes = 6 h exactly (Morning phase boundary, D-031)
|
||||
assert_that(Constants.format_game_time(360)).is_equal("06:00")
|
||||
|
||||
func test_format_noon() -> void:
|
||||
assert_that(Constants.format_game_time(720)).is_equal("12:00")
|
||||
|
||||
func test_format_evening_start() -> void:
|
||||
assert_that(Constants.format_game_time(1080)).is_equal("18:00")
|
||||
|
||||
func test_format_end_of_day() -> void:
|
||||
# Last valid minute — must not wrap or overflow
|
||||
assert_that(Constants.format_game_time(1439)).is_equal("23:59")
|
||||
|
||||
func test_format_pads_single_digit_hour() -> void:
|
||||
# 30 min = 00:30
|
||||
assert_that(Constants.format_game_time(30)).is_equal("00:30")
|
||||
|
||||
func test_format_pads_single_digit_minute() -> void:
|
||||
# 121 min = 02:01
|
||||
assert_that(Constants.format_game_time(121)).is_equal("02:01")
|
||||
|
||||
func test_format_half_past_hour() -> void:
|
||||
assert_that(Constants.format_game_time(90)).is_equal("01:30")
|
||||
|
||||
func test_format_arbitrary_midday() -> void:
|
||||
# 835 min = 13:55
|
||||
assert_that(Constants.format_game_time(835)).is_equal("13:55")
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# InsertClock initial state
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
func test_insert_clock_initial_time_str_is_placeholder() -> void:
|
||||
# Before any snapshot, _time_str must be "--:--" (not shown by _draw)
|
||||
assert_that(_clock._time_str).is_equal("--:--")
|
||||
|
||||
func test_insert_clock_initial_phase_str_is_empty() -> void:
|
||||
assert_that(_clock._phase_str).is_equal("")
|
||||
|
||||
func test_insert_clock_initial_day_str_is_empty() -> void:
|
||||
assert_that(_clock._day_str).is_equal("")
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# InsertClock.update_from_state() — reads GameState.game_time
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
func test_update_from_state_formats_time_str() -> void:
|
||||
GameState.game_time = {
|
||||
"day": 0, "time_of_day": 720, "day_phase": "Afternoon", "tick_rate": "Full",
|
||||
}
|
||||
_clock.update_from_state()
|
||||
assert_that(_clock._time_str).is_equal("12:00")
|
||||
|
||||
func test_update_from_state_sets_phase_str() -> void:
|
||||
GameState.game_time = {
|
||||
"day": 0, "time_of_day": 1080, "day_phase": "Evening", "tick_rate": "Full",
|
||||
}
|
||||
_clock.update_from_state()
|
||||
assert_that(_clock._phase_str).is_equal("Evening")
|
||||
|
||||
func test_update_from_state_sets_day_str_one_indexed() -> void:
|
||||
# Day 0 from server → "D1" display (1-indexed)
|
||||
GameState.game_time = {
|
||||
"day": 0, "time_of_day": 100, "day_phase": "Morning", "tick_rate": "Full",
|
||||
}
|
||||
_clock.update_from_state()
|
||||
assert_that(_clock._day_str).is_equal("D1")
|
||||
|
||||
func test_update_from_state_day_2() -> void:
|
||||
GameState.game_time = {
|
||||
"day": 1, "time_of_day": 50, "day_phase": "Morning", "tick_rate": "Full",
|
||||
}
|
||||
_clock.update_from_state()
|
||||
assert_that(_clock._day_str).is_equal("D2")
|
||||
|
||||
func test_update_from_state_midnight() -> void:
|
||||
GameState.game_time = {
|
||||
"day": 0, "time_of_day": 0, "day_phase": "Night", "tick_rate": "Full",
|
||||
}
|
||||
_clock.update_from_state()
|
||||
assert_that(_clock._time_str).is_equal("00:00")
|
||||
|
||||
func test_update_from_state_end_of_day() -> void:
|
||||
GameState.game_time = {
|
||||
"day": 0, "time_of_day": 1439, "day_phase": "Night", "tick_rate": "Full",
|
||||
}
|
||||
_clock.update_from_state()
|
||||
assert_that(_clock._time_str).is_equal("23:59")
|
||||
|
||||
func test_update_from_state_skips_empty_game_time() -> void:
|
||||
# Empty game_time must not overwrite --:-- (guard in update_from_state)
|
||||
GameState.game_time = {}
|
||||
_clock.update_from_state()
|
||||
assert_that(_clock._time_str).is_equal("--:--")
|
||||
|
||||
func test_update_from_state_deduplicates_same_tick() -> void:
|
||||
# Calling twice with identical data must produce same result (signature cache)
|
||||
GameState.game_time = {
|
||||
"day": 0, "time_of_day": 360, "day_phase": "Morning", "tick_rate": "Full",
|
||||
}
|
||||
_clock.update_from_state()
|
||||
assert_that(_clock._time_str).is_equal("06:00")
|
||||
# Call again — result unchanged, no crash
|
||||
_clock.update_from_state()
|
||||
assert_that(_clock._time_str).is_equal("06:00")
|
||||
|
||||
func test_update_from_state_updates_on_new_tick() -> void:
|
||||
# time_of_day changes → signature changes → _time_str updates
|
||||
GameState.game_time = {
|
||||
"day": 0, "time_of_day": 60, "day_phase": "Morning", "tick_rate": "Full",
|
||||
}
|
||||
_clock.update_from_state()
|
||||
assert_that(_clock._time_str).is_equal("01:00")
|
||||
|
||||
GameState.game_time = {
|
||||
"day": 0, "time_of_day": 120, "day_phase": "Morning", "tick_rate": "Full",
|
||||
}
|
||||
_clock.update_from_state()
|
||||
assert_that(_clock._time_str).is_equal("02:00")
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# InsertClock.PHASE_COLORS — all four D-031 phases have colors
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
func test_phase_colors_has_morning() -> void:
|
||||
assert_that(_clock.PHASE_COLORS.has("Morning")).is_true()
|
||||
|
||||
func test_phase_colors_has_afternoon() -> void:
|
||||
assert_that(_clock.PHASE_COLORS.has("Afternoon")).is_true()
|
||||
|
||||
func test_phase_colors_has_evening() -> void:
|
||||
assert_that(_clock.PHASE_COLORS.has("Evening")).is_true()
|
||||
|
||||
func test_phase_colors_has_night() -> void:
|
||||
assert_that(_clock.PHASE_COLORS.has("Night")).is_true()
|
||||
|
||||
func test_phase_color_applied_after_update() -> void:
|
||||
GameState.game_time = {
|
||||
"day": 0, "time_of_day": 100, "day_phase": "Morning", "tick_rate": "Full",
|
||||
}
|
||||
_clock.update_from_state()
|
||||
assert_that(_clock._phase_color).is_equal(_clock.PHASE_COLORS["Morning"])
|
||||
|
||||
func test_phase_color_unknown_phase_uses_dim_fallback() -> void:
|
||||
# Unknown phase string → Constants.IMPLANT_TEXT_DIM
|
||||
GameState.game_time = {
|
||||
"day": 0, "time_of_day": 100, "day_phase": "Twilight", "tick_rate": "Full",
|
||||
}
|
||||
_clock.update_from_state()
|
||||
assert_that(_clock._phase_color).is_equal(Constants.IMPLANT_TEXT_DIM)
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# GameState: game_time field parsing (confirms apply_snapshot wiring)
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
func test_game_time_populated_from_snapshot() -> void:
|
||||
GameState.apply_snapshot({
|
||||
"tick": 5, "entities": [],
|
||||
"game_time": {
|
||||
"day": 0, "time_of_day": 720, "day_phase": "Afternoon", "tick_rate": "Full",
|
||||
},
|
||||
})
|
||||
assert_that(GameState.game_time.get("time_of_day")).is_equal(720)
|
||||
|
||||
func test_game_time_all_four_phases_store_correctly() -> void:
|
||||
for phase in ["Morning", "Afternoon", "Evening", "Night"]:
|
||||
GameState.apply_snapshot({
|
||||
"tick": 1, "entities": [],
|
||||
"game_time": {
|
||||
"day": 0, "time_of_day": 100, "day_phase": phase, "tick_rate": "Full",
|
||||
},
|
||||
})
|
||||
assert_that(GameState.game_time.get("day_phase")).is_equal(phase)
|
||||
|
||||
func test_game_time_missing_from_snapshot_preserves_previous() -> void:
|
||||
GameState.game_time = {
|
||||
"day": 0, "time_of_day": 360, "day_phase": "Morning", "tick_rate": "Full",
|
||||
}
|
||||
GameState.apply_snapshot({"tick": 2, "entities": []})
|
||||
assert_that(GameState.game_time.get("time_of_day")).is_equal(360)
|
||||
|
||||
func test_game_time_zero_time_of_day_stored() -> void:
|
||||
# time_of_day = 0 (midnight) must not be treated as falsy/missing
|
||||
GameState.apply_snapshot({
|
||||
"tick": 1, "entities": [],
|
||||
"game_time": {"day": 0, "time_of_day": 0, "day_phase": "Night", "tick_rate": "Full"},
|
||||
})
|
||||
assert_that(GameState.game_time.get("time_of_day")).is_equal(0)
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# SimBridge test mode: game_time fields are valid
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
func test_sim_bridge_snapshot_has_game_time() -> void:
|
||||
var snap = SimBridge._test_snapshot()
|
||||
assert_that(snap.has("game_time")).is_true()
|
||||
assert_that(snap.game_time is Dictionary).is_true()
|
||||
|
||||
func test_sim_bridge_game_time_has_required_fields() -> void:
|
||||
var snap = SimBridge._test_snapshot()
|
||||
var gt: Dictionary = snap.game_time
|
||||
assert_that(gt.has("day")).is_true()
|
||||
assert_that(gt.has("time_of_day")).is_true()
|
||||
assert_that(gt.has("day_phase")).is_true()
|
||||
assert_that(gt.has("tick_rate")).is_true()
|
||||
|
||||
func test_sim_bridge_time_of_day_is_non_negative() -> void:
|
||||
var snap = SimBridge._test_snapshot()
|
||||
assert_that(snap.game_time.get("time_of_day", -1) as int).is_greater_equal(0)
|
||||
|
||||
func test_sim_bridge_time_of_day_within_day_bounds() -> void:
|
||||
# D-031: 1440 game-minutes per day, valid range 0..1439
|
||||
var snap = SimBridge._test_snapshot()
|
||||
assert_that(snap.game_time.get("time_of_day", 0) as int).is_less_equal(1439)
|
||||
|
||||
func test_sim_bridge_day_phase_is_valid() -> void:
|
||||
var snap = SimBridge._test_snapshot()
|
||||
var phase: String = snap.game_time.get("day_phase", "")
|
||||
assert_that(["Morning", "Afternoon", "Evening", "Night"].has(phase)).is_true()
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Scene: InsertClock node at InsertOverlay/TimeDisplay
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
func test_insert_clock_exists_in_ui_layer() -> void:
|
||||
var scene := load("res://scenes/main.tscn")
|
||||
var instance = scene.instantiate()
|
||||
auto_free(instance)
|
||||
add_child(instance)
|
||||
|
||||
assert_that(instance.get_node_or_null("InsertOverlay/TimeDisplay")).is_not_null()
|
||||
|
||||
func test_insert_clock_time_str_updates_after_process() -> void:
|
||||
var scene := load("res://scenes/main.tscn")
|
||||
var instance = scene.instantiate()
|
||||
auto_free(instance)
|
||||
add_child(instance)
|
||||
|
||||
GameState.apply_snapshot({
|
||||
"tick": 1, "version": Protocol.PROTOCOL_VERSION, "entities": [],
|
||||
"game_time": {"day": 0, "time_of_day": 720, "day_phase": "Afternoon", "tick_rate": "Full"},
|
||||
})
|
||||
instance._process(0.016)
|
||||
|
||||
var clock = instance.get_node_or_null("InsertOverlay/TimeDisplay")
|
||||
assert_that(clock).is_not_null()
|
||||
assert_that(clock._time_str).is_equal("12:00")
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Regression: debug_overlay still reads game_time correctly (#511)
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
func test_debug_overlay_reads_game_time_day_phase() -> void:
|
||||
GameState.apply_snapshot({
|
||||
"tick": 1, "entities": [],
|
||||
"game_time": {"day": 0, "time_of_day": 100, "day_phase": "Morning", "tick_rate": "Full"},
|
||||
})
|
||||
assert_that(GameState.game_time.get("day_phase")).is_equal("Morning")
|
||||
|
||||
func test_debug_overlay_reads_game_time_tick_rate() -> void:
|
||||
GameState.apply_snapshot({
|
||||
"tick": 1, "entities": [],
|
||||
"game_time": {"day": 0, "time_of_day": 100, "day_phase": "Morning", "tick_rate": "Half"},
|
||||
})
|
||||
assert_that(GameState.game_time.get("tick_rate")).is_equal("Half")
|
||||
@@ -29,6 +29,8 @@ var _verb_items: Array = [] # sorted [{kind, label, priority, available}]
|
||||
var _selected_index: int = 0
|
||||
var _active_tween: Tween = null
|
||||
var _verb_labels: Array[Label] = []
|
||||
# #537: D-033 relationship color — cached per target, drawn as left-edge accent bar
|
||||
var _relationship_color: Color = Constants.IMPLANT_TEXT_DIM
|
||||
|
||||
@onready var _vbox: VBoxContainer = $VBox
|
||||
|
||||
@@ -39,10 +41,14 @@ func _ready() -> void:
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
|
||||
|
||||
var _last_screen_pos: Vector2 = Vector2.ZERO
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if _showing:
|
||||
_update_screen_position()
|
||||
queue_redraw()
|
||||
if position != _last_screen_pos:
|
||||
_last_screen_pos = position
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
@@ -52,6 +58,9 @@ func _draw() -> void:
|
||||
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)
|
||||
# #537: D-033 relationship color accent — 3px left-edge bar signals NPC relationship
|
||||
var bar_rect := Rect2(-pad, -pad, 3.0, bg_rect.size.y)
|
||||
draw_rect(bar_rect, _relationship_color * Color(1, 1, 1, 0.85))
|
||||
|
||||
|
||||
func update_from_state() -> void:
|
||||
@@ -88,6 +97,7 @@ func update_from_state() -> void:
|
||||
_verb_items = sorted
|
||||
_selected_index = 0
|
||||
_cache_entity_position()
|
||||
_cache_entity_relationship()
|
||||
_rebuild_labels()
|
||||
_show()
|
||||
|
||||
@@ -127,6 +137,17 @@ func _cache_entity_position() -> void:
|
||||
return
|
||||
|
||||
|
||||
## #537: Cache relationship color for the target entity (D-033 palette).
|
||||
## Falls back to IMPLANT_TEXT_DIM for non-NPC or unknown entities.
|
||||
func _cache_entity_relationship() -> void:
|
||||
for entity in GameState.visible_entities:
|
||||
if entity.get("entity_id") == _current_target_id:
|
||||
var rel: String = str(entity.get("relationship", "Unknown"))
|
||||
_relationship_color = Constants.color_for_relationship(rel)
|
||||
return
|
||||
_relationship_color = Constants.IMPLANT_TEXT_DIM
|
||||
|
||||
|
||||
## 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:
|
||||
@@ -187,6 +208,10 @@ func get_visible_verb_count() -> int:
|
||||
return _verb_items.size()
|
||||
|
||||
|
||||
func hide_list() -> void:
|
||||
_hide()
|
||||
|
||||
|
||||
func is_showing() -> bool:
|
||||
return _showing
|
||||
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
extends Control
|
||||
|
||||
## #263: Time display — diegetic time readout on the player's neural insert (D-013, D-031).
|
||||
## Shows station local time (HH:MM), day phase, and day number.
|
||||
## Lives on InsertOverlay (CanvasLayer 10) per D-051 diegetic insert principle.
|
||||
## Draw-based for implant visual aesthetic. Updated via update_from_state() from main.gd.
|
||||
##
|
||||
## Placeholder layout — position and style will be refined when #314 wireframe lands.
|
||||
|
||||
const FONT_SIZE_TIME: int = 15
|
||||
const FONT_SIZE_META: int = 10
|
||||
const PADDING := Vector2(10, 7)
|
||||
const BG_COLOR := Color(0.04, 0.05, 0.08, 0.70)
|
||||
const BORDER_COLOR := Color(0.10, 0.20, 0.26, 0.65)
|
||||
|
||||
# Day phase colors — station lighting cycle (D-031)
|
||||
const PHASE_COLORS := {
|
||||
"Morning": Color("#aed6dc"), # pale cyan-blue — early light
|
||||
"Afternoon": Color("#E0F7FA"), # bright cyan-white — full day
|
||||
"Evening": Color("#9EBFC4"), # dimmed — dusk transition
|
||||
"Night": Color("#4a7080"), # dark teal — station nightwatch
|
||||
}
|
||||
|
||||
var _time_str: String = "--:--"
|
||||
var _phase_str: String = ""
|
||||
var _day_str: String = ""
|
||||
var _day_text: String = ""
|
||||
var _phase_color: Color = Constants.IMPLANT_TEXT_DIM
|
||||
var _last_signature: String = ""
|
||||
var _has_data: bool = false
|
||||
|
||||
# Cached geometry — recomputed in update_from_state(), used in _draw()
|
||||
var _time_size: Vector2 = Vector2.ZERO
|
||||
var _phase_size: Vector2 = Vector2.ZERO
|
||||
var _day_size: Vector2 = Vector2.ZERO
|
||||
var _meta_h: float = 0.0
|
||||
var _box_w: float = 0.0
|
||||
var _box_h: float = 0.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
|
||||
|
||||
func update_from_state() -> void:
|
||||
var gt: Dictionary = GameState.game_time
|
||||
if gt.is_empty():
|
||||
return
|
||||
var tod: int = int(gt.get("time_of_day", 0))
|
||||
var day: int = int(gt.get("day", 0))
|
||||
var phase: String = str(gt.get("day_phase", ""))
|
||||
var sig: String = "%d:%d:%s" % [tod, day, phase]
|
||||
if sig == _last_signature:
|
||||
return
|
||||
_last_signature = sig
|
||||
_time_str = Constants.format_game_time(tod)
|
||||
_phase_str = phase
|
||||
_day_str = "D%d" % (day + 1)
|
||||
_day_text = " " + _day_str
|
||||
_phase_color = PHASE_COLORS.get(phase, Constants.IMPLANT_TEXT_DIM)
|
||||
_has_data = true
|
||||
_cache_geometry()
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _cache_geometry() -> void:
|
||||
var font := ThemeDB.fallback_font
|
||||
_time_size = font.get_string_size(_time_str, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE_TIME)
|
||||
_phase_size = font.get_string_size(_phase_str, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE_META)
|
||||
_day_size = font.get_string_size(_day_text, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE_META)
|
||||
_meta_h = font.get_string_size("A", HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE_META).y
|
||||
var meta_w := _phase_size.x + _day_size.x
|
||||
var content_w := max(_time_size.x, meta_w)
|
||||
_box_w = content_w + PADDING.x * 2
|
||||
_box_h = PADDING.y * 2 + _time_size.y + 3 + _meta_h
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
if not _has_data:
|
||||
return
|
||||
|
||||
# Background
|
||||
draw_rect(Rect2(Vector2.ZERO, Vector2(_box_w, _box_h)), BG_COLOR)
|
||||
draw_rect(Rect2(Vector2.ZERO, Vector2(_box_w, _box_h)), BORDER_COLOR, false, 1.0)
|
||||
|
||||
var font := ThemeDB.fallback_font
|
||||
|
||||
# HH:MM (primary, full brightness)
|
||||
draw_string(font, Vector2(PADDING.x, PADDING.y + _time_size.y),
|
||||
_time_str, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE_TIME, Constants.IMPLANT_TEXT_COLOR)
|
||||
|
||||
# Phase + day number (secondary, dimmed + phase-tinted)
|
||||
var meta_y := PADDING.y + _time_size.y + 3 + _meta_h
|
||||
draw_string(font, Vector2(PADDING.x, meta_y),
|
||||
_phase_str, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE_META, _phase_color)
|
||||
draw_string(font, Vector2(PADDING.x + _phase_size.x, meta_y),
|
||||
_day_text, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE_META, Constants.IMPLANT_TEXT_DIM)
|
||||
@@ -0,0 +1,22 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://b4timedisplay1"]
|
||||
|
||||
[ext_resource type="Script" path="res://ui/time_display.gd" id="1_tdisplay"]
|
||||
|
||||
; #263: Time display — top-left placeholder per D-013/D-051.
|
||||
; Position and size will be refined when #314 wireframe lands.
|
||||
; NOTE: draw-based content manages its own layout; Control rect is a
|
||||
; minimum bounding box, not a clip rect. Increase if content grows.
|
||||
[node name="TimeDisplay" type="Control"]
|
||||
anchors_preset = 0
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
offset_left = 16.0
|
||||
offset_top = 16.0
|
||||
offset_right = 200.0
|
||||
offset_bottom = 70.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
script = ExtResource("1_tdisplay")
|
||||
@@ -253,7 +253,7 @@ Every dialogue line requires 6 structural tags + up to 3 selection tags.
|
||||
|
||||
| Tag | Type | Format | Description |
|
||||
|-----|------|--------|-------------|
|
||||
| `id` | string | `{location}_{d}_{###}` | Stable line ID. Machine-parseable. Never reused. |
|
||||
| `id` | string | `{npc-slug}_d_{###}` | Stable line ID. Machine-parseable. Never reused. NPC-scoped per D-035 Sprint 15 amendment. |
|
||||
| `text` | string | The line | The authored dialogue text |
|
||||
| `role` | enum | lowercase-slug | Template role, not NPC name. NPCs fill roles at runtime. |
|
||||
| `access` | list | `[public, insider, ...]` | D-028 Layer 1 hard filter. Who can hear this. |
|
||||
@@ -265,7 +265,7 @@ Every dialogue line requires 6 structural tags + up to 3 selection tags.
|
||||
| Tag | Type | Format | Description |
|
||||
|-----|------|--------|-------------|
|
||||
| `topic` | list | `[colleague, cargo, ...]` | D-028 Layer 4 weighted selection. Defaults to `[routine]`. |
|
||||
| `mood` | list | `[comfortable, worried, ...]` | D-028 Layer 4 weighted selection. Defaults to `[comfortable]`. |
|
||||
| `mood` | list | `[content, anxious, ...]` | D-028 Layer 4 weighted selection. Defaults to untagged (neutral). |
|
||||
| `tags` | list | freeform strings | Escape hatch for author intent. |
|
||||
|
||||
#### Authoring-Only Tags (Not Consumed by Engine)
|
||||
@@ -352,7 +352,7 @@ Same trigger, different pool. That's how mirror moments work.
|
||||
|
||||
| Tag | Type | Description |
|
||||
|-----|------|-------------|
|
||||
| `id` | string | `{location}_m_{s\|d}_{###}` — `s` for smuggler, `d` for detective |
|
||||
| `id` | string | `{npc-slug}_m_{s\|d}_{###}` — `s` for smuggler, `d` for detective. NPC-scoped per D-035 Sprint 15 amendment. |
|
||||
| `text` | string | The monologue line (160 char max) |
|
||||
| `character` | enum | `smuggler` or `detective` |
|
||||
| `trigger` | enum | What causes this line to fire (see trigger types below) |
|
||||
@@ -600,9 +600,9 @@ See `content/global/enums/topics.yaml`. Note: `crime` is deliberately excluded.
|
||||
|
||||
### Moods (8 values)
|
||||
|
||||
`fond`, `comfortable`, `worried`, `suspicious`, `analytical`, `conflicted`, `concerned`, `relieved`
|
||||
`anxious`, `frustrated`, `content`, `suspicious`, `warm`, `hostile`, `relieved`, `focused`
|
||||
|
||||
See `content/global/enums/moods.yaml`.
|
||||
See `content/global/enums/moods.yaml`. (Updated Sprint 14 amendment to D-035.)
|
||||
|
||||
### Monologue Triggers (9 values)
|
||||
|
||||
@@ -630,7 +630,7 @@ Before submitting any content, run through these checks. Items marked **HARD** w
|
||||
|
||||
### Dialogue Lines
|
||||
|
||||
- [ ] **HARD:** `id` matches pattern `{location}_d_{###}` (lowercase, underscore-separated)
|
||||
- [ ] **HARD:** `id` matches pattern `{npc-slug}_d_{###}` (e.g. `kael-davan_d_001`) — NPC-scoped per D-035 Sprint 15 amendment
|
||||
- [ ] **HARD:** `role` is a valid lowercase slug matching a template role
|
||||
- [ ] **HARD:** `access` is a non-empty list of valid access tier enums
|
||||
- [ ] **HARD:** `trust` is one of `surface`, `real`, `secret`
|
||||
@@ -643,7 +643,7 @@ Before submitting any content, run through these checks. Items marked **HARD** w
|
||||
|
||||
### Monologue Lines
|
||||
|
||||
- [ ] **HARD:** `id` matches pattern `{location}_m_{s|d}_{###}`
|
||||
- [ ] **HARD:** `id` matches pattern `{npc-slug}_m_{s|d}_{###}` (e.g. `pc-smuggler_m_s_001`) — NPC-scoped per D-035 Sprint 15 amendment
|
||||
- [ ] **HARD:** `text` is 160 characters or fewer
|
||||
- [ ] **HARD:** `trigger` is a valid trigger enum
|
||||
- [ ] **HARD:** Character partition: smuggler lines in smuggler files, detective in detective files
|
||||
@@ -755,9 +755,9 @@ All YAML content files must validate against their JSON Schema:
|
||||
|
||||
| Content Type | Pattern | Example |
|
||||
|-------------|---------|---------|
|
||||
| Dialogue | `{location}_d_{###}` | `transit_d_044` |
|
||||
| Monologue (smuggler) | `{location}_m_s_{###}` | `transit_m_s_011` |
|
||||
| Monologue (detective) | `{location}_m_d_{###}` | `transit_m_d_042` |
|
||||
| Dialogue | `{npc-slug}_d_{###}` | `kael-davan_d_001` |
|
||||
| Monologue (smuggler) | `{npc-slug}_m_s_{###}` | `pc-smuggler_m_s_001` |
|
||||
| Monologue (detective) | `{npc-slug}_m_d_{###}` | `pc-detective_m_d_001` |
|
||||
|
||||
IDs are stable identifiers. Once assigned, never reused or reassigned. Number gaps are acceptable.
|
||||
|
||||
@@ -865,26 +865,26 @@ Every generated line must pass:
|
||||
### Step 2: Assign Tags
|
||||
|
||||
```yaml
|
||||
- id: transit_d_071
|
||||
- id: kael-davan_d_076
|
||||
text: "Morning. Voss kept the rotation thin -- we've got a clear window after 14:00."
|
||||
role: dock-worker
|
||||
access: [insider]
|
||||
trust: real
|
||||
situation: [shift_start]
|
||||
topic: [cargo, routine]
|
||||
mood: [comfortable]
|
||||
mood: [content]
|
||||
tags: [ring-operational]
|
||||
```
|
||||
|
||||
### Step 3: Validate
|
||||
|
||||
- `id`: `transit_d_071` -- valid pattern.
|
||||
- `id`: `kael-davan_d_076` -- valid pattern (NPC-scoped per D-035 Sprint 15 amendment). Continues from kael-davan's terminal sequence (_034-_075).
|
||||
- `role`: `dock-worker` -- valid slug.
|
||||
- `access: [insider]` -- only ring members hear this. Detective never sees it. Correct.
|
||||
- `trust: real` -- requires earned trust. Not surface-level small talk. Correct for operational ring dialogue.
|
||||
- `situation: [shift_start]` -- morning shift context. Correct.
|
||||
- `topic: [cargo, routine]` -- covers both operational and schedule content. Correct.
|
||||
- `mood: [comfortable]` -- Phase 1, baseline warm. Correct.
|
||||
- `mood: [content]` -- Phase 1, baseline warm. Correct.
|
||||
|
||||
### Step 4: Dual-Lens Check
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9-]*_(d|m)_[0-9]{3}$",
|
||||
"description": "Stable machine-parseable line ID: {template}_{d|m}_{###}. d = dialogue, m = monologue."
|
||||
"description": "Stable machine-parseable line ID: {npc-slug}_{d|m}_{###} (e.g. kael-davan_d_001, pc-detective_m_d_001). d = dialogue, m = monologue. NPC-scoped per D-035 Sprint 15 amendment — each NPC has an independent sequence starting at _001."
|
||||
},
|
||||
"text": {
|
||||
"type": "string",
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9-]*_d_[0-9]{3}$",
|
||||
"description": "Stable line ID: {location_slug}_d_{###}"
|
||||
"description": "Stable line ID: {npc-slug}_d_{###} (e.g. kael-davan_d_001). NPC-scoped per D-035 Sprint 15 amendment — each NPC has an independent sequence starting at _001."
|
||||
},
|
||||
"text": {
|
||||
"type": "string",
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9-]*_m_[sd]_[0-9]{3}$",
|
||||
"description": "Stable line ID: {location_slug}_m_{s|d}_{###}. s = smuggler, d = detective."
|
||||
"description": "Stable line ID: {npc-slug}_m_{s|d}_{###} (e.g. pc-smuggler_m_s_001, pc-detective_m_d_001). s = smuggler, d = detective. NPC-scoped per D-035 Sprint 15 amendment — each NPC has an independent sequence starting at _001."
|
||||
},
|
||||
"text": {
|
||||
"type": "string",
|
||||
|
||||
@@ -13,7 +13,7 @@ lines:
|
||||
# RING OPERATIONS — corridor handoffs
|
||||
# ========================================
|
||||
|
||||
- id: maintenance-corridors_d_001
|
||||
- id: kael-davan_d_001
|
||||
text: "Package is in the junction locker. Renn picks it up in ten."
|
||||
role: dock-worker
|
||||
access: [insider]
|
||||
@@ -23,7 +23,7 @@ lines:
|
||||
topic: [cargo]
|
||||
tags: [kael, ring-ops, renn, operational]
|
||||
|
||||
- id: maintenance-corridors_d_002
|
||||
- id: kael-davan_d_002
|
||||
text: "B-7 is clear. Camera cycles in forty seconds — move fast."
|
||||
role: dock-worker
|
||||
access: [insider]
|
||||
@@ -35,7 +35,7 @@ lines:
|
||||
fact_id: investigation.surveillance_gaps
|
||||
confidence: knows_details
|
||||
|
||||
- id: maintenance-corridors_d_003
|
||||
- id: kael-davan_d_003
|
||||
text: "Chalk marks are fresh. Renn came through already. We're on schedule."
|
||||
role: dock-worker
|
||||
access: [insider]
|
||||
@@ -45,7 +45,7 @@ lines:
|
||||
topic: [cargo]
|
||||
tags: [kael, ring-ops, renn, operational]
|
||||
|
||||
- id: maintenance-corridors_d_004
|
||||
- id: kael-davan_d_004
|
||||
text: "Don't use the C-section hatch tonight. Maintenance crew's working late."
|
||||
role: dock-worker
|
||||
access: [insider]
|
||||
@@ -60,7 +60,7 @@ lines:
|
||||
# When the smuggler finds Kael with the unknown contact
|
||||
# ========================================
|
||||
|
||||
- id: maintenance-corridors_d_005
|
||||
- id: kael-davan_d_005
|
||||
text: "What are you — wait. How long have you been standing there?"
|
||||
role: dock-worker
|
||||
access: [insider]
|
||||
@@ -70,7 +70,7 @@ lines:
|
||||
topic: [danger]
|
||||
tags: [kael, contradiction, phase-3]
|
||||
|
||||
- id: maintenance-corridors_d_006
|
||||
- id: kael-davan_d_006
|
||||
text: "That was nobody. Maintenance contact. It's handled."
|
||||
role: dock-worker
|
||||
access: [insider]
|
||||
@@ -80,7 +80,7 @@ lines:
|
||||
topic: [routine]
|
||||
tags: [kael, deflection, lying, phase-4]
|
||||
|
||||
- id: maintenance-corridors_d_007
|
||||
- id: kael-davan_d_007
|
||||
text: "I don't have to explain every conversation I have. Drop it."
|
||||
role: dock-worker
|
||||
access: [insider]
|
||||
@@ -94,7 +94,7 @@ lines:
|
||||
# SECRET — corridor confessions
|
||||
# ========================================
|
||||
|
||||
- id: maintenance-corridors_d_008
|
||||
- id: kael-davan_d_008
|
||||
text: "I'm trying to get out. That's all. I just want out."
|
||||
role: dock-worker
|
||||
access: [insider]
|
||||
@@ -104,7 +104,7 @@ lines:
|
||||
topic: [personal, trust]
|
||||
tags: [kael, confession, phase-5]
|
||||
|
||||
- id: maintenance-corridors_d_009
|
||||
- id: kael-davan_d_009
|
||||
text: "Nils will kill me if he finds out. You know that. You know him."
|
||||
role: dock-worker
|
||||
access: [insider]
|
||||
@@ -114,7 +114,7 @@ lines:
|
||||
topic: [danger, trust]
|
||||
tags: [kael, confession, nils, phase-5]
|
||||
|
||||
- id: maintenance-corridors_d_010
|
||||
- id: kael-davan_d_010
|
||||
text: "I found someone who can get me new papers. Off-station. Clean break."
|
||||
role: dock-worker
|
||||
access: [insider]
|
||||
@@ -136,7 +136,7 @@ lines:
|
||||
# ========================================
|
||||
|
||||
# first_meeting: InteractionMemory.count == 0. Any stranger in restricted corridors is a problem.
|
||||
- id: maintenance-corridors_d_011
|
||||
- id: kael-davan_d_011
|
||||
text: "This is a restricted section. Who authorized you back here?"
|
||||
role: dock-worker
|
||||
access: [public]
|
||||
@@ -148,7 +148,7 @@ lines:
|
||||
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
|
||||
- id: kael-davan_d_012
|
||||
text: "You're on time. Clear all the way to the junction."
|
||||
role: dock-worker
|
||||
access: [peer, insider]
|
||||
@@ -160,7 +160,7 @@ lines:
|
||||
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
|
||||
- id: kael-davan_d_013
|
||||
text: "You keep showing up back here. You have clearance for this section?"
|
||||
role: dock-worker
|
||||
access: [authority]
|
||||
@@ -172,7 +172,7 @@ lines:
|
||||
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
|
||||
- id: kael-davan_d_014
|
||||
text: "You followed me here."
|
||||
role: dock-worker
|
||||
access: [peer, insider]
|
||||
|
||||
@@ -21,7 +21,7 @@ lines:
|
||||
# SURFACE COVER — sounds like maintenance work
|
||||
# ==========================================
|
||||
|
||||
- id: maintenance-corridors_d_100
|
||||
- id: ring-operative_d_001
|
||||
text: "Running maintenance on the B-7 conduit. Could be a while."
|
||||
role: ring-operative
|
||||
access: [public, peer]
|
||||
@@ -35,7 +35,7 @@ lines:
|
||||
detective: "Maintenance worker. Conduit repair. Routine."
|
||||
notes: "'Running maintenance' is the canonical ring cover phrase."
|
||||
|
||||
- id: maintenance-corridors_d_101
|
||||
- id: ring-operative_d_002
|
||||
text: "Bay four access from here is faster than going around. If you're in a hurry."
|
||||
role: ring-operative
|
||||
access: [public, peer]
|
||||
@@ -48,7 +48,7 @@ lines:
|
||||
smuggler: "He's telling me the route is faster. This is spatial information, not just courtesy."
|
||||
detective: "Ring operative casually establishes corridor-to-bay-four spatial relationship. Note it."
|
||||
|
||||
- id: maintenance-corridors_d_102
|
||||
- id: ring-operative_d_003
|
||||
text: "Quiet in here tonight. Good working conditions."
|
||||
role: ring-operative
|
||||
access: [public, peer]
|
||||
@@ -61,7 +61,7 @@ lines:
|
||||
smuggler: "Dead air confirmed. Meridian's not picking this up."
|
||||
detective: "Routine corridor comment."
|
||||
|
||||
- id: maintenance-corridors_d_103
|
||||
- id: ring-operative_d_004
|
||||
text: "Pael was through here earlier. Seal replacement on the ventilation unit."
|
||||
role: ring-operative
|
||||
access: [public, peer]
|
||||
@@ -74,7 +74,7 @@ lines:
|
||||
smuggler: "Pael was through. Legitimate. Corridor's been active — that means traffic covers us."
|
||||
detective: "References Pael's B-7 presence without suspicion. Maintenance workers know each other's routes."
|
||||
|
||||
- id: maintenance-corridors_d_104
|
||||
- id: ring-operative_d_005
|
||||
text: "C-4 access is at the far end. About three minutes from here."
|
||||
role: ring-operative
|
||||
access: [public, peer]
|
||||
@@ -94,7 +94,7 @@ lines:
|
||||
# OPERATIONAL COORDINATION — insider/real only
|
||||
# ==========================================
|
||||
|
||||
- id: maintenance-corridors_d_105
|
||||
- id: ring-operative_d_006
|
||||
text: "Cans are ready. Bay four, per the schedule."
|
||||
role: ring-operative
|
||||
access: [insider]
|
||||
@@ -107,7 +107,7 @@ lines:
|
||||
smuggler: "Containers cleared. Bay four. We're on."
|
||||
detective: "Never hears this line."
|
||||
|
||||
- id: maintenance-corridors_d_106
|
||||
- id: ring-operative_d_007
|
||||
text: "Manifest is clean. Window opens in eighteen minutes."
|
||||
role: ring-operative
|
||||
access: [insider]
|
||||
@@ -120,7 +120,7 @@ lines:
|
||||
smuggler: "Paperwork's set. Eighteen minutes. That's earlier than planned — adjust."
|
||||
detective: "Never hears this line."
|
||||
|
||||
- id: maintenance-corridors_d_107
|
||||
- id: ring-operative_d_008
|
||||
text: "Usual pickup. Route's the same."
|
||||
role: ring-operative
|
||||
access: [insider]
|
||||
@@ -133,7 +133,7 @@ lines:
|
||||
smuggler: "Same route, same handoff. No variations."
|
||||
detective: "Never hears this line."
|
||||
|
||||
- id: maintenance-corridors_d_108
|
||||
- id: ring-operative_d_009
|
||||
text: "The gap's clear. Scan's done at 14:20. Next window's yours."
|
||||
role: ring-operative
|
||||
access: [insider]
|
||||
@@ -146,10 +146,10 @@ lines:
|
||||
smuggler: "14:20 scan, then the window opens. Twenty-five minutes clear."
|
||||
detective: "Never hears this line."
|
||||
knowledge_grant:
|
||||
fact_id: location.surveillance_gaps
|
||||
fact_id: investigation.surveillance_gaps
|
||||
confidence: knows_details
|
||||
|
||||
- id: maintenance-corridors_d_109
|
||||
- id: ring-operative_d_010
|
||||
text: "Drin was asking about B-7. Twice today."
|
||||
role: ring-operative
|
||||
access: [insider]
|
||||
@@ -165,7 +165,7 @@ lines:
|
||||
fact_id: investigation.drin_inspection_pattern
|
||||
confidence: suspects
|
||||
|
||||
- id: maintenance-corridors_d_110
|
||||
- id: ring-operative_d_011
|
||||
text: "Dead air through here to the C-4 junction. No coverage."
|
||||
role: ring-operative
|
||||
access: [insider]
|
||||
@@ -178,10 +178,10 @@ lines:
|
||||
smuggler: "Full confirmation. Meridian doesn't reach this section."
|
||||
detective: "Never hears this line."
|
||||
knowledge_grant:
|
||||
fact_id: location.surveillance_gaps
|
||||
fact_id: investigation.surveillance_gaps
|
||||
confidence: knows_details
|
||||
|
||||
- id: maintenance-corridors_d_111
|
||||
- id: ring-operative_d_012
|
||||
text: "Voss confirmed the hold clears at 1400. He'll run the bay himself."
|
||||
role: ring-operative
|
||||
access: [insider]
|
||||
@@ -194,7 +194,7 @@ lines:
|
||||
smuggler: "Voss is personally running bay four at 1400. That's the signal the window is clear."
|
||||
detective: "Never hears this line."
|
||||
|
||||
- id: maintenance-corridors_d_112
|
||||
- id: ring-operative_d_013
|
||||
text: "Kael's on dock. He knows."
|
||||
role: ring-operative
|
||||
access: [insider]
|
||||
@@ -211,7 +211,7 @@ lines:
|
||||
# PRESSURE / WARNING SIGNALS
|
||||
# ==========================================
|
||||
|
||||
- id: maintenance-corridors_d_113
|
||||
- id: ring-operative_d_014
|
||||
text: "Commission eyes on the terminal floor. Not here yet."
|
||||
role: ring-operative
|
||||
access: [insider]
|
||||
@@ -224,7 +224,7 @@ lines:
|
||||
smuggler: "Detective is visible on the main floor. Corridors are still clear."
|
||||
detective: "Never hears this line."
|
||||
|
||||
- id: maintenance-corridors_d_114
|
||||
- id: ring-operative_d_015
|
||||
text: "Hold the run. Drin's in the corridor section."
|
||||
role: ring-operative
|
||||
access: [insider]
|
||||
@@ -237,7 +237,7 @@ lines:
|
||||
smuggler: "Stop. Drin's in position. The window's closed."
|
||||
detective: "Never hears this line."
|
||||
|
||||
- id: maintenance-corridors_d_115
|
||||
- id: ring-operative_d_016
|
||||
text: "Maret filed again. Voss has it. Don't move anything tonight."
|
||||
role: ring-operative
|
||||
access: [insider]
|
||||
@@ -250,7 +250,7 @@ lines:
|
||||
smuggler: "Maret flagged something. Voss is managing it. Stand down tonight."
|
||||
detective: "Never hears this line."
|
||||
|
||||
- id: maintenance-corridors_d_116
|
||||
- id: ring-operative_d_017
|
||||
text: "We're clean. Same time tomorrow."
|
||||
role: ring-operative
|
||||
access: [insider]
|
||||
@@ -267,7 +267,7 @@ lines:
|
||||
# IF DETECTIVE SOMEHOW ENTERS — surface only
|
||||
# ==========================================
|
||||
|
||||
- id: maintenance-corridors_d_117
|
||||
- id: ring-operative_d_018
|
||||
text: "Restricted access back here. Service personnel only."
|
||||
role: ring-operative
|
||||
access: [authority, public]
|
||||
@@ -277,7 +277,7 @@ lines:
|
||||
mood: [suspicious]
|
||||
tags: [ring-operative, detective-path, access-denial]
|
||||
|
||||
- id: maintenance-corridors_d_118
|
||||
- id: ring-operative_d_019
|
||||
text: "I'm running maintenance. If there's a Commission access request, file it through Voss."
|
||||
role: ring-operative
|
||||
access: [authority]
|
||||
@@ -290,7 +290,7 @@ lines:
|
||||
smuggler: "Never encounters this."
|
||||
detective: "Operative redirects to Voss. Everyone redirects to Voss. That's structural, not coincidence."
|
||||
|
||||
- id: maintenance-corridors_d_119
|
||||
- id: ring-operative_d_020
|
||||
text: "Nothing unusual back here. Conduit work. Same every cycle."
|
||||
role: ring-operative
|
||||
access: [authority, public]
|
||||
@@ -305,7 +305,7 @@ lines:
|
||||
# Surface-only: covers routine corridor presence without operational tells.
|
||||
# ==========================================
|
||||
|
||||
- id: maintenance-corridors_d_120
|
||||
- id: ring-operative_d_021
|
||||
text: "Power relay's been cycling weird. I'm watching it."
|
||||
role: ring-operative
|
||||
access: [public, peer]
|
||||
@@ -315,7 +315,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [ring-operative, surface-cover, generation-pass]
|
||||
|
||||
- id: maintenance-corridors_d_121
|
||||
- id: ring-operative_d_022
|
||||
text: "Pael was supposed to handle B-7. He didn't show."
|
||||
role: ring-operative
|
||||
access: [public, peer]
|
||||
@@ -325,7 +325,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [ring-operative, surface-cover, pael, generation-pass]
|
||||
|
||||
- id: maintenance-corridors_d_122
|
||||
- id: ring-operative_d_023
|
||||
text: "Watch the floor here. Drain panel's been loose three cycles."
|
||||
role: ring-operative
|
||||
access: [public, peer]
|
||||
@@ -335,7 +335,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [ring-operative, surface-cover, generation-pass]
|
||||
|
||||
- id: maintenance-corridors_d_123
|
||||
- id: ring-operative_d_024
|
||||
text: "Airflow's better down the B-section. C-section's got the recycler issue."
|
||||
role: ring-operative
|
||||
access: [public, peer]
|
||||
@@ -345,7 +345,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [ring-operative, surface-cover, spatial, generation-pass]
|
||||
|
||||
- id: maintenance-corridors_d_124
|
||||
- id: ring-operative_d_025
|
||||
text: "You want the service bay, it's left at the junction. Don't go right — that's the restricted access."
|
||||
role: ring-operative
|
||||
access: [public, peer]
|
||||
@@ -358,7 +358,7 @@ lines:
|
||||
smuggler: "He's directing people away from the route. Social traffic management."
|
||||
detective: "Operative redirects me away from C-4 access junction instinctively."
|
||||
|
||||
- id: maintenance-corridors_d_125
|
||||
- id: ring-operative_d_026
|
||||
text: "Long shift. These corridors look the same after a while."
|
||||
role: ring-operative
|
||||
access: [public, peer]
|
||||
@@ -368,7 +368,7 @@ lines:
|
||||
mood: [frustrated]
|
||||
tags: [ring-operative, surface-cover, generation-pass]
|
||||
|
||||
- id: maintenance-corridors_d_126
|
||||
- id: ring-operative_d_027
|
||||
text: "Scan's clear. I checked it an hour ago."
|
||||
role: ring-operative
|
||||
access: [public, peer]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Dialogue: transit-worker at Maintenance Corridors
|
||||
# NPC: Tev Osel (Tier 3, NOBODY/CIVILIAN)
|
||||
# Voice: patient, mildly distracted, confused why anyone is asking him anything.
|
||||
# KEY LINE: maintenance-corridors_d_001 — sounds like surveillance positioning inquiry
|
||||
# KEY LINE: transit-worker_d_001 — sounds like surveillance positioning inquiry
|
||||
# Actually: Tev missed Kosse at the corridor junction and is looking for them
|
||||
# Ticket: #307 | Sprint: 12
|
||||
|
||||
@@ -15,7 +15,7 @@ lines:
|
||||
# Actually: Tev missed his partner at the regular meeting spot.
|
||||
# ==========================================
|
||||
|
||||
- id: maintenance-corridors_d_001
|
||||
- id: transit-worker_d_001
|
||||
text: "You see which way Kosse went? Should've been through here twenty minutes ago."
|
||||
role: transit-worker
|
||||
access: [public]
|
||||
@@ -36,7 +36,7 @@ lines:
|
||||
# ROUTINE LINES
|
||||
# ==========================================
|
||||
|
||||
- id: maintenance-corridors_d_002
|
||||
- id: transit-worker_d_002
|
||||
text: "They shifted the freight route again. Always forget which junction."
|
||||
role: transit-worker
|
||||
access: [public]
|
||||
@@ -46,7 +46,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [tev, atmospheric]
|
||||
|
||||
- id: maintenance-corridors_d_003
|
||||
- id: transit-worker_d_003
|
||||
text: "Just on break. Out of your way in a minute."
|
||||
role: transit-worker
|
||||
access: [public]
|
||||
@@ -56,7 +56,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [tev, incidental]
|
||||
|
||||
- id: maintenance-corridors_d_004
|
||||
- id: transit-worker_d_004
|
||||
text: "Late freight route runs long. Can't do anything about it."
|
||||
role: transit-worker
|
||||
access: [public]
|
||||
@@ -66,7 +66,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [tev, atmospheric]
|
||||
|
||||
- id: maintenance-corridors_d_005
|
||||
- id: transit-worker_d_005
|
||||
text: "Waiting for someone. Shouldn't be long."
|
||||
role: transit-worker
|
||||
access: [public]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Dialogue: bar-owner at The Last Shift
|
||||
# NPC: Lera Osk (Tier 2, SYSTEM/OPERATOR)
|
||||
# NPC: Lera Sessik (Tier 2, ANCHOR/OPERATOR)
|
||||
# Voice: direct, dry. Runs her bar like a territory. Not political — protective.
|
||||
# Ring-aware: knows it exists in her bar. Doesn't ask. Doesn't interfere. Not complicit.
|
||||
# Torek arrangement: she gives him a tab specifically — Commission visibility is useful.
|
||||
@@ -13,7 +13,7 @@ lines:
|
||||
# GREETINGS — territorial welcome
|
||||
# ==========================================
|
||||
|
||||
- id: the-last-shift_d_100
|
||||
- id: bar-owner_d_001
|
||||
text: "Usual?"
|
||||
role: bar-owner
|
||||
access: [public, peer, insider]
|
||||
@@ -23,7 +23,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [lera, greeting, economy-of-words]
|
||||
|
||||
- id: the-last-shift_d_101
|
||||
- id: bar-owner_d_002
|
||||
text: "Long shift?"
|
||||
role: bar-owner
|
||||
access: [public, peer]
|
||||
@@ -33,7 +33,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [lera, greeting]
|
||||
|
||||
- id: the-last-shift_d_102
|
||||
- id: bar-owner_d_003
|
||||
text: "Bar's clean, drink's cold. That's the deal."
|
||||
role: bar-owner
|
||||
access: [public]
|
||||
@@ -43,7 +43,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [lera, greeting, house-rules]
|
||||
|
||||
- id: the-last-shift_d_103
|
||||
- id: bar-owner_d_004
|
||||
text: "Sess'll get you. I've got something in the back."
|
||||
role: bar-owner
|
||||
access: [public, peer]
|
||||
@@ -57,7 +57,7 @@ lines:
|
||||
# BAR MANAGEMENT — house rules, operations
|
||||
# ==========================================
|
||||
|
||||
- id: the-last-shift_d_104
|
||||
- id: bar-owner_d_005
|
||||
text: "Fights go outside. First and last warning."
|
||||
role: bar-owner
|
||||
access: [public, peer, insider]
|
||||
@@ -67,7 +67,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [lera, house-rules, authority]
|
||||
|
||||
- id: the-last-shift_d_105
|
||||
- id: bar-owner_d_006
|
||||
text: "Tab closes at end of cycle. Not at end of your memory."
|
||||
role: bar-owner
|
||||
access: [public, peer]
|
||||
@@ -77,7 +77,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [lera, tab, house-rules]
|
||||
|
||||
- id: the-last-shift_d_106
|
||||
- id: bar-owner_d_007
|
||||
text: "Torek's running a tab. That's fine. Don't ask me why."
|
||||
role: bar-owner
|
||||
access: [peer]
|
||||
@@ -91,7 +91,7 @@ lines:
|
||||
detective: "Bar owner specifically maintains a tab for a Commission officer. Useful relationship or leverage?"
|
||||
notes: "Lera's Torek arrangement: she keeps him comfortable because knowing where the Commission officer is drinking is better than not knowing."
|
||||
|
||||
- id: the-last-shift_d_107
|
||||
- id: bar-owner_d_008
|
||||
text: "Lattice stays off at the bar. Sign's on the wall."
|
||||
role: bar-owner
|
||||
access: [public, peer, insider]
|
||||
@@ -102,7 +102,7 @@ lines:
|
||||
tags: [lera, lattice-free, house-rules]
|
||||
notes: "Not legally enforceable — Lera knows this. It's about atmosphere, not compliance."
|
||||
|
||||
- id: the-last-shift_d_108
|
||||
- id: bar-owner_d_009
|
||||
text: "Grain spirit's back. Limited run. Don't make it last two nights."
|
||||
role: bar-owner
|
||||
access: [public, peer, insider]
|
||||
@@ -116,7 +116,7 @@ lines:
|
||||
# PEOPLE — Lera knows everyone
|
||||
# ==========================================
|
||||
|
||||
- id: the-last-shift_d_109
|
||||
- id: bar-owner_d_010
|
||||
text: "Kael's in the back. If you're looking."
|
||||
role: bar-owner
|
||||
access: [peer, insider]
|
||||
@@ -129,7 +129,7 @@ lines:
|
||||
smuggler: "Good. She keeps track. That's what she does."
|
||||
detective: "Lera knows where Kael Davan is without being asked. She tracks her regulars."
|
||||
|
||||
- id: the-last-shift_d_110
|
||||
- id: bar-owner_d_011
|
||||
text: "Naia's been in early the last few nights. Doesn't say much."
|
||||
role: bar-owner
|
||||
access: [peer]
|
||||
@@ -142,7 +142,7 @@ lines:
|
||||
smuggler: "Lera noticed Naia's off. She notices everything. She's saying it because she wants someone to do something."
|
||||
detective: "Bar owner volunteers behavioral change in Naia Tamm. Lera observes her regulars carefully."
|
||||
|
||||
- id: the-last-shift_d_111
|
||||
- id: bar-owner_d_012
|
||||
text: "Ren? Been here two months. Quiet, pays their tab. That's all I can tell you."
|
||||
role: bar-owner
|
||||
access: [public, peer]
|
||||
@@ -155,7 +155,7 @@ lines:
|
||||
smuggler: "That's Lera's version of a clean assessment. She'd say more if she suspected something."
|
||||
detective: "Lera's read on Ren: quiet, no trouble. If Lera's not worried, the baseline is probably accurate."
|
||||
|
||||
- id: the-last-shift_d_112
|
||||
- id: bar-owner_d_013
|
||||
text: "Tev? Good worker. Three years. Minds their own."
|
||||
role: bar-owner
|
||||
access: [public, peer]
|
||||
@@ -172,7 +172,7 @@ lines:
|
||||
# RING-ADJACENT — Lera's knowledge edge
|
||||
# ==========================================
|
||||
|
||||
- id: the-last-shift_d_113
|
||||
- id: bar-owner_d_014
|
||||
text: "Whatever's happening in the back corner isn't my business. Don't make it my business."
|
||||
role: bar-owner
|
||||
access: [insider]
|
||||
@@ -186,7 +186,7 @@ lines:
|
||||
detective: "Never hears this line."
|
||||
notes: "Lera's arrangement: she doesn't ask, she doesn't see, and she keeps things stable. Pragmatic, not complicit."
|
||||
|
||||
- id: the-last-shift_d_114
|
||||
- id: bar-owner_d_015
|
||||
text: "Kael's got my number. He's been a regular long enough that that means something."
|
||||
role: bar-owner
|
||||
access: [peer]
|
||||
@@ -203,7 +203,7 @@ lines:
|
||||
# DETECTIVE ENCOUNTER — neutral, unwelcoming
|
||||
# ==========================================
|
||||
|
||||
- id: the-last-shift_d_115
|
||||
- id: bar-owner_d_016
|
||||
text: "Commission's welcome. Like everyone else. Bar rules apply."
|
||||
role: bar-owner
|
||||
access: [authority, public]
|
||||
@@ -213,7 +213,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [lera, detective-path, neutral-welcome]
|
||||
|
||||
- id: the-last-shift_d_116
|
||||
- id: bar-owner_d_017
|
||||
text: "If someone in my bar has something to say to Commission, they'll say it. I don't push that."
|
||||
role: bar-owner
|
||||
access: [authority]
|
||||
@@ -223,8 +223,8 @@ lines:
|
||||
mood: [content]
|
||||
tags: [lera, detective-path, institutional-independence]
|
||||
|
||||
- id: the-last-shift_d_117
|
||||
text: "I've run this bar twelve years without a Commission complaint. I'd like to keep it that way."
|
||||
- id: bar-owner_d_018
|
||||
text: "I've run this bar eighteen years without a Commission complaint. I'd like to keep it that way."
|
||||
role: bar-owner
|
||||
access: [authority]
|
||||
trust: surface
|
||||
@@ -233,7 +233,7 @@ lines:
|
||||
mood: [suspicious]
|
||||
tags: [lera, detective-path, authority-assertion]
|
||||
|
||||
- id: the-last-shift_d_118
|
||||
- id: bar-owner_d_019
|
||||
text: "I see a lot. I don't always remember all of it."
|
||||
role: bar-owner
|
||||
access: [authority]
|
||||
@@ -250,7 +250,7 @@ lines:
|
||||
# ELEVATED TRUST — what Lera actually says
|
||||
# ==========================================
|
||||
|
||||
- id: the-last-shift_d_119
|
||||
- id: bar-owner_d_020
|
||||
text: "Naia's been asking questions I can't answer. Someone should."
|
||||
role: bar-owner
|
||||
access: [peer, insider]
|
||||
@@ -267,7 +267,7 @@ lines:
|
||||
# SIGN-OFF / CLOSING TIME
|
||||
# ==========================================
|
||||
|
||||
- id: the-last-shift_d_120
|
||||
- id: bar-owner_d_021
|
||||
text: "Last call. Finish up."
|
||||
role: bar-owner
|
||||
access: [public, peer, insider]
|
||||
@@ -277,7 +277,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [lera, closing]
|
||||
|
||||
- id: the-last-shift_d_121
|
||||
- id: bar-owner_d_022
|
||||
text: "Bar's closed. Come back next cycle."
|
||||
role: bar-owner
|
||||
access: [public, peer, insider]
|
||||
@@ -287,7 +287,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [lera, closing]
|
||||
|
||||
- id: the-last-shift_d_122
|
||||
- id: bar-owner_d_023
|
||||
text: "Good night. Stay out of trouble."
|
||||
role: bar-owner
|
||||
access: [public, peer, insider]
|
||||
@@ -301,7 +301,7 @@ lines:
|
||||
# GENERATION PASS — ambient variants
|
||||
# ==========================================
|
||||
|
||||
- id: the-last-shift_d_123
|
||||
- id: bar-owner_d_024
|
||||
text: "You look tired. Sit."
|
||||
role: bar-owner
|
||||
access: [peer, insider]
|
||||
@@ -311,7 +311,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [lera, greeting, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_124
|
||||
- id: bar-owner_d_025
|
||||
text: "Same as yesterday?"
|
||||
role: bar-owner
|
||||
access: [peer, insider]
|
||||
@@ -321,7 +321,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [lera, greeting, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_125
|
||||
- id: bar-owner_d_026
|
||||
text: "New face. Bar rules are on the wall."
|
||||
role: bar-owner
|
||||
access: [public]
|
||||
@@ -331,7 +331,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [lera, greeting, newcomer, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_126
|
||||
- id: bar-owner_d_027
|
||||
text: "No trouble tonight. I mean it."
|
||||
role: bar-owner
|
||||
access: [public, peer, insider]
|
||||
@@ -341,7 +341,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [lera, house-rules, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_127
|
||||
- id: bar-owner_d_028
|
||||
text: "Sess'll get you settled. I'm in the back."
|
||||
role: bar-owner
|
||||
access: [public, peer]
|
||||
@@ -351,7 +351,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [lera, redirect, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_128
|
||||
- id: bar-owner_d_029
|
||||
text: "We close at 2300. Don't make me say it twice."
|
||||
role: bar-owner
|
||||
access: [public, peer]
|
||||
@@ -361,7 +361,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [lera, house-rules, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_129
|
||||
- id: bar-owner_d_030
|
||||
text: "Good crowd tonight. Quiet enough."
|
||||
role: bar-owner
|
||||
access: [peer, insider]
|
||||
@@ -371,7 +371,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [lera, ambient, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_130
|
||||
- id: bar-owner_d_031
|
||||
text: "Bar's yours. I've got accounts."
|
||||
role: bar-owner
|
||||
access: [public, peer, insider]
|
||||
|
||||
@@ -12,7 +12,7 @@ lines:
|
||||
# GREETINGS — familiar, community register
|
||||
# ==========================================
|
||||
|
||||
- id: the-last-shift_d_300
|
||||
- id: bar-regular_d_001
|
||||
text: "Long one today?"
|
||||
role: bar-regular
|
||||
access: [public, peer]
|
||||
@@ -22,7 +22,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [bar-regular, greeting]
|
||||
|
||||
- id: the-last-shift_d_301
|
||||
- id: bar-regular_d_002
|
||||
text: "You're in early. Good shift or bad shift?"
|
||||
role: bar-regular
|
||||
access: [public, peer]
|
||||
@@ -32,7 +32,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [bar-regular, greeting]
|
||||
|
||||
- id: the-last-shift_d_302
|
||||
- id: bar-regular_d_003
|
||||
text: "Grain spirit's back. I already had two. Don't judge."
|
||||
role: bar-regular
|
||||
access: [public, peer]
|
||||
@@ -46,7 +46,7 @@ lines:
|
||||
# TERMINAL FLOOR — off-duty opinions
|
||||
# ==========================================
|
||||
|
||||
- id: the-last-shift_d_303
|
||||
- id: bar-regular_d_004
|
||||
text: "Push today. Bay six was backed up until 2000. Voss wasn't happy."
|
||||
role: bar-regular
|
||||
access: [public, peer]
|
||||
@@ -56,7 +56,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [bar-regular, terminal, voss]
|
||||
|
||||
- id: the-last-shift_d_304
|
||||
- id: bar-regular_d_005
|
||||
text: "Maret flagged another one. Nobody knows what Voss does with them."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
@@ -69,7 +69,7 @@ lines:
|
||||
smuggler: "They're talking about Maret's flags as general knowledge. That's a wider problem than I thought."
|
||||
detective: "Maret's pattern is common knowledge on the floor. Multiple witnesses, informal. That can be confirmed."
|
||||
|
||||
- id: the-last-shift_d_305
|
||||
- id: bar-regular_d_006
|
||||
text: "Bay four's been held all week. Three cycles. Nobody's said what's in it."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
@@ -82,7 +82,7 @@ lines:
|
||||
smuggler: "They've noticed bay four. It's becoming visible. Not good."
|
||||
detective: "Bay four hold is common knowledge at worker level. That means it predates any routine explanation."
|
||||
|
||||
- id: the-last-shift_d_306
|
||||
- id: bar-regular_d_007
|
||||
text: "Transition window's the worst part of the job. Twenty minutes to hand off and nothing ever lines up."
|
||||
role: bar-regular
|
||||
access: [public, peer]
|
||||
@@ -95,7 +95,7 @@ lines:
|
||||
smuggler: "The window is their biggest frustration. And their biggest frustration is the ring's biggest asset."
|
||||
detective: "Workers discuss the transition window as a systematic failure. The gap is known."
|
||||
|
||||
- id: the-last-shift_d_307
|
||||
- id: bar-regular_d_008
|
||||
text: "Commission audit's coming cycle fifteen. Everyone's running around double-checking everything."
|
||||
role: bar-regular
|
||||
access: [public, peer]
|
||||
@@ -108,7 +108,7 @@ lines:
|
||||
smuggler: "Cycle fifteen. That's the deadline. Get clean before then."
|
||||
detective: "Workers are responding to the audit announcement. Behavioral change in the workforce. Note who isn't anxious."
|
||||
|
||||
- id: the-last-shift_d_308
|
||||
- id: bar-regular_d_009
|
||||
text: "Voss has been on the floor more than usual this cycle. Watching everything."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
@@ -125,7 +125,7 @@ lines:
|
||||
# COMMUNITY — the full life of this district
|
||||
# ==========================================
|
||||
|
||||
- id: the-last-shift_d_309
|
||||
- id: bar-regular_d_010
|
||||
text: "Drifters lost again. Four-nil this time. I'm done watching until they rebuild."
|
||||
role: bar-regular
|
||||
access: [public, peer]
|
||||
@@ -135,7 +135,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [bar-regular, sports, drifters]
|
||||
|
||||
- id: the-last-shift_d_310
|
||||
- id: bar-regular_d_011
|
||||
text: "Ventilation's still the same in Sector 3. Seven years of 'under discussion.' I've stopped expecting anything."
|
||||
role: bar-regular
|
||||
access: [public, peer]
|
||||
@@ -145,49 +145,49 @@ lines:
|
||||
mood: [content]
|
||||
tags: [bar-regular, infrastructure, grievance]
|
||||
|
||||
- id: the-last-shift_d_311
|
||||
- id: bar-regular_d_012
|
||||
text: "You see the missing person notice on the board? Vessels Tamm. I don't know a Vessels Tamm."
|
||||
role: bar-regular
|
||||
access: [public, peer]
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [community, routine]
|
||||
topic: [personal, routine]
|
||||
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."
|
||||
detective: "Vessels Tamm notice is circulating casually. People haven't connected it to Naia Tamm yet."
|
||||
|
||||
- id: the-last-shift_d_312
|
||||
- id: bar-regular_d_013
|
||||
text: "Re-em clinic's backed up four cycles for non-urgent. Main station if it's serious, apparently."
|
||||
role: bar-regular
|
||||
access: [public, peer]
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [community, routine]
|
||||
topic: [personal, routine]
|
||||
mood: [content]
|
||||
tags: [bar-regular, clinic, community]
|
||||
|
||||
- id: the-last-shift_d_313
|
||||
- id: bar-regular_d_014
|
||||
text: "Lattice upgrades are up eighteen percent this cycle. That's more than my raise."
|
||||
role: bar-regular
|
||||
access: [public, peer]
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [institution, community]
|
||||
topic: [institution, personal]
|
||||
mood: [content]
|
||||
tags: [bar-regular, lattice, economy]
|
||||
dual_lens:
|
||||
smuggler: "That's why the waiting list keeps growing. They're providing a service."
|
||||
detective: "Economic grievance expressed casually. Price pressure is general knowledge. Confirms motive context."
|
||||
|
||||
- id: the-last-shift_d_314
|
||||
- id: bar-regular_d_015
|
||||
text: "Workers' Association is pushing the handoff protocol again. Voss won't budge."
|
||||
role: bar-regular
|
||||
access: [public, peer]
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [institution, community]
|
||||
topic: [institution, personal]
|
||||
mood: [suspicious]
|
||||
tags: [bar-regular, workers-association, voss, transition-window]
|
||||
dual_lens:
|
||||
@@ -198,7 +198,7 @@ lines:
|
||||
# DETECTIVE ENCOUNTER — community wariness
|
||||
# ==========================================
|
||||
|
||||
- id: the-last-shift_d_315
|
||||
- id: bar-regular_d_016
|
||||
text: "You new? Haven't seen you before."
|
||||
role: bar-regular
|
||||
access: [public]
|
||||
@@ -208,7 +208,7 @@ lines:
|
||||
mood: [suspicious]
|
||||
tags: [bar-regular, detective-path, recognition]
|
||||
|
||||
- id: the-last-shift_d_316
|
||||
- id: bar-regular_d_017
|
||||
text: "Commission? I just drink here. Whatever you're after, I probably can't help."
|
||||
role: bar-regular
|
||||
access: [authority]
|
||||
@@ -218,7 +218,7 @@ lines:
|
||||
mood: [suspicious]
|
||||
tags: [bar-regular, detective-path, deflection]
|
||||
|
||||
- id: the-last-shift_d_317
|
||||
- id: bar-regular_d_018
|
||||
text: "Ask Lera. She knows what happens in this bar better than anyone."
|
||||
role: bar-regular
|
||||
access: [authority, public]
|
||||
@@ -232,7 +232,7 @@ lines:
|
||||
# SIGN-OFF
|
||||
# ==========================================
|
||||
|
||||
- id: the-last-shift_d_318
|
||||
- id: bar-regular_d_019
|
||||
text: "Another cycle done. Same tomorrow."
|
||||
role: bar-regular
|
||||
access: [public, peer]
|
||||
@@ -242,7 +242,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [bar-regular, closing]
|
||||
|
||||
- id: the-last-shift_d_319
|
||||
- id: bar-regular_d_020
|
||||
text: "Safe home. Watch the corridors after 2200."
|
||||
role: bar-regular
|
||||
access: [public, peer]
|
||||
@@ -256,7 +256,7 @@ lines:
|
||||
# GENERATION PASS — ambient variants
|
||||
# ==========================================
|
||||
|
||||
- id: the-last-shift_d_320
|
||||
- id: bar-regular_d_021
|
||||
text: "Pull up a chair. Floor's full otherwise."
|
||||
role: bar-regular
|
||||
access: [public, peer]
|
||||
@@ -266,7 +266,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [bar-regular, greeting, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_321
|
||||
- id: bar-regular_d_022
|
||||
text: "You're in late. Voss kept you?"
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
@@ -276,7 +276,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [bar-regular, greeting, voss, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_322
|
||||
- id: bar-regular_d_023
|
||||
text: "Just got off. Bay six all night. My back knows it."
|
||||
role: bar-regular
|
||||
access: [public, peer]
|
||||
@@ -286,7 +286,7 @@ lines:
|
||||
mood: [frustrated]
|
||||
tags: [bar-regular, greeting, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_323
|
||||
- id: bar-regular_d_024
|
||||
text: "Drifters are playing Mirren next cycle. Home game. Might actually go."
|
||||
role: bar-regular
|
||||
access: [public, peer]
|
||||
@@ -296,17 +296,17 @@ lines:
|
||||
mood: [content]
|
||||
tags: [bar-regular, sports, community, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_324
|
||||
- id: bar-regular_d_025
|
||||
text: "Sector 3 meeting went nowhere. Same as last time. Same as the time before that."
|
||||
role: bar-regular
|
||||
access: [public, peer]
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
topic: [institution, community]
|
||||
topic: [institution, personal]
|
||||
mood: [content]
|
||||
tags: [bar-regular, infrastructure, grievance, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_325
|
||||
- id: bar-regular_d_026
|
||||
text: "New hire's asking questions on the floor again. Good. Someone should."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
@@ -316,7 +316,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [bar-regular, new-hire, community, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_326
|
||||
- id: bar-regular_d_027
|
||||
text: "Span gate was loud today. Three cycles now. That's not normal."
|
||||
role: bar-regular
|
||||
access: [public, peer]
|
||||
@@ -326,7 +326,7 @@ lines:
|
||||
mood: [suspicious]
|
||||
tags: [bar-regular, infrastructure, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_327
|
||||
- id: bar-regular_d_028
|
||||
text: "Kael's not in tonight. Unusual."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
@@ -339,7 +339,7 @@ lines:
|
||||
smuggler: "Kael's absence is visible. Other people notice too."
|
||||
detective: "Kael Davan's routine absence is noted socially. Behavioral change is general knowledge."
|
||||
|
||||
- id: the-last-shift_d_328
|
||||
- id: bar-regular_d_029
|
||||
text: "Lera keeps the grain spirit back until she's sure you're a regular. You must've made the grade."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
@@ -349,7 +349,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [bar-regular, lera, humor, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_329
|
||||
- id: bar-regular_d_030
|
||||
text: "Good night. Don't let the Commission ruin your morning."
|
||||
role: bar-regular
|
||||
access: [public, peer]
|
||||
|
||||
@@ -12,7 +12,7 @@ lines:
|
||||
# GREETINGS — service register
|
||||
# ==========================================
|
||||
|
||||
- id: the-last-shift_d_200
|
||||
- id: bartender_d_001
|
||||
text: "What'll it be?"
|
||||
role: bartender
|
||||
access: [public, peer, insider]
|
||||
@@ -22,7 +22,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [sess, greeting]
|
||||
|
||||
- id: the-last-shift_d_201
|
||||
- id: bartender_d_002
|
||||
text: "Back early tonight. Long shift?"
|
||||
role: bartender
|
||||
access: [public, peer]
|
||||
@@ -32,7 +32,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [sess, greeting, social]
|
||||
|
||||
- id: the-last-shift_d_202
|
||||
- id: bartender_d_003
|
||||
text: "Grain spirit's back. Lera's running it at cycle price for two more days."
|
||||
role: bartender
|
||||
access: [public, peer, insider]
|
||||
@@ -46,7 +46,7 @@ lines:
|
||||
# BAR OBSERVATIONS — social intelligence
|
||||
# ==========================================
|
||||
|
||||
- id: the-last-shift_d_203
|
||||
- id: bartender_d_004
|
||||
text: "Kael's been in most nights this week. Quiet though. Not his usual."
|
||||
role: bartender
|
||||
access: [peer]
|
||||
@@ -59,7 +59,7 @@ lines:
|
||||
smuggler: "Sess noticed Kael's off. She's reading it as personal. She doesn't know what's underneath."
|
||||
detective: "Bartender flagging behavioral change in Kael Davan. Consistent with other observations."
|
||||
|
||||
- id: the-last-shift_d_204
|
||||
- id: bartender_d_005
|
||||
text: "Torek tipped well again tonight. Always does. Nicer than you'd expect for Commission."
|
||||
role: bartender
|
||||
access: [peer, public]
|
||||
@@ -72,7 +72,7 @@ lines:
|
||||
smuggler: "Torek's making friends with the staff. Not good."
|
||||
detective: "Bartender views Torek favorably. Torek is cultivating the service staff deliberately."
|
||||
|
||||
- id: the-last-shift_d_205
|
||||
- id: bartender_d_006
|
||||
text: "Naia was in earlier. Didn't stay. Left something on the table — looked like a note. Lera has it."
|
||||
role: bartender
|
||||
access: [peer]
|
||||
@@ -85,7 +85,7 @@ lines:
|
||||
smuggler: "Naia left something. Lera has it. Go to Lera."
|
||||
detective: "Naia Tamm left written material at the bar. Lera is holding it. That's a material lead."
|
||||
|
||||
- id: the-last-shift_d_206
|
||||
- id: bartender_d_007
|
||||
text: "Ren? Yeah, they're always in the morning crowd. Don't say much. Good for the morning."
|
||||
role: bartender
|
||||
access: [public, peer]
|
||||
@@ -95,7 +95,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [sess, ren, drifter]
|
||||
|
||||
- id: the-last-shift_d_207
|
||||
- id: bartender_d_008
|
||||
text: "Tev watches the door. Every shift. Lera likes it that way."
|
||||
role: bartender
|
||||
access: [peer]
|
||||
@@ -108,7 +108,7 @@ lines:
|
||||
smuggler: "Tev watches the door for Lera. Not for anyone else. That's the setup."
|
||||
detective: "Tev's entrance-monitoring is confirmed as Lera's request. Not operational surveillance."
|
||||
|
||||
- id: the-last-shift_d_208
|
||||
- id: bartender_d_009
|
||||
text: "Sera was in earlier. She usually stays later. She left when Torek came in."
|
||||
role: bartender
|
||||
access: [peer]
|
||||
@@ -125,7 +125,7 @@ lines:
|
||||
# SOCIAL / COMMUNITY
|
||||
# ==========================================
|
||||
|
||||
- id: the-last-shift_d_209
|
||||
- id: bartender_d_010
|
||||
text: "Drifters lost again. Tev took it hard. Drank two too many."
|
||||
role: bartender
|
||||
access: [public, peer]
|
||||
@@ -135,7 +135,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [sess, tev, sports, community]
|
||||
|
||||
- id: the-last-shift_d_210
|
||||
- id: bartender_d_011
|
||||
text: "Workers' Association meeting's Thursday. Half the regulars will be here Friday in a mood either way."
|
||||
role: bartender
|
||||
access: [public, peer]
|
||||
@@ -145,7 +145,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [sess, workers-association, community]
|
||||
|
||||
- id: the-last-shift_d_211
|
||||
- id: bartender_d_012
|
||||
text: "Quiet night. Good for everyone."
|
||||
role: bartender
|
||||
access: [public, peer, insider]
|
||||
@@ -159,7 +159,7 @@ lines:
|
||||
# DETECTIVE ENCOUNTER — helpful, unfocused
|
||||
# ==========================================
|
||||
|
||||
- id: the-last-shift_d_212
|
||||
- id: bartender_d_013
|
||||
text: "Commission comes in sometimes. Not usually this early in the cycle."
|
||||
role: bartender
|
||||
access: [authority, public]
|
||||
@@ -169,7 +169,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [sess, detective-path, observation]
|
||||
|
||||
- id: the-last-shift_d_213
|
||||
- id: bartender_d_014
|
||||
text: "I know most of the regulars. Ask me anything. I probably know something."
|
||||
role: bartender
|
||||
access: [authority, public]
|
||||
@@ -182,7 +182,7 @@ lines:
|
||||
smuggler: "Never encounters this line."
|
||||
detective: "Bartender is cooperative. Useful — but she'll answer questions she doesn't know are questions."
|
||||
|
||||
- id: the-last-shift_d_214
|
||||
- id: bartender_d_015
|
||||
text: "Lera's the one who knows the real history of this place. I've only been here four cycles."
|
||||
role: bartender
|
||||
access: [authority, public]
|
||||
@@ -196,7 +196,7 @@ lines:
|
||||
# SIGN-OFF
|
||||
# ==========================================
|
||||
|
||||
- id: the-last-shift_d_215
|
||||
- id: bartender_d_016
|
||||
text: "Last round's on the board. After that, Lera calls it."
|
||||
role: bartender
|
||||
access: [public, peer, insider]
|
||||
@@ -206,7 +206,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [sess, closing]
|
||||
|
||||
- id: the-last-shift_d_216
|
||||
- id: bartender_d_017
|
||||
text: "Good night. Safe walk."
|
||||
role: bartender
|
||||
access: [public, peer, insider]
|
||||
@@ -220,7 +220,7 @@ lines:
|
||||
# GENERATION PASS — ambient variants
|
||||
# ==========================================
|
||||
|
||||
- id: the-last-shift_d_217
|
||||
- id: bartender_d_018
|
||||
text: "The usual? Or trying something different tonight?"
|
||||
role: bartender
|
||||
access: [peer, insider]
|
||||
@@ -230,7 +230,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [sess, greeting, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_218
|
||||
- id: bartender_d_019
|
||||
text: "Busy one tonight. Give me a second."
|
||||
role: bartender
|
||||
access: [public, peer, insider]
|
||||
@@ -240,7 +240,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [sess, greeting, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_219
|
||||
- id: bartender_d_020
|
||||
text: "Good shift or bad shift? Your face says one thing, your order'll confirm it."
|
||||
role: bartender
|
||||
access: [peer]
|
||||
@@ -250,7 +250,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [sess, greeting, social, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_220
|
||||
- id: bartender_d_021
|
||||
text: "Lera says the specials board is final. Don't ask me to substitute."
|
||||
role: bartender
|
||||
access: [public, peer]
|
||||
@@ -260,7 +260,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [sess, lera, house-rules, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_221
|
||||
- id: bartender_d_022
|
||||
text: "Torek's in his usual spot. Corner table. Second drink already."
|
||||
role: bartender
|
||||
access: [peer]
|
||||
@@ -270,7 +270,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [sess, torek, observation, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_222
|
||||
- id: bartender_d_023
|
||||
text: "Ren's been here since this morning. Still on the same drink."
|
||||
role: bartender
|
||||
access: [peer]
|
||||
@@ -280,7 +280,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [sess, ren, observation, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_223
|
||||
- id: bartender_d_024
|
||||
text: "Drifters game is on the Meridian relay tonight. If anyone's got a feed, that'll be the crowd."
|
||||
role: bartender
|
||||
access: [public, peer]
|
||||
@@ -290,7 +290,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [sess, sports, community, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_224
|
||||
- id: bartender_d_025
|
||||
text: "Slow night for a change. Enjoy it."
|
||||
role: bartender
|
||||
access: [public, peer, insider]
|
||||
@@ -300,7 +300,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [sess, ambient, generation-pass]
|
||||
|
||||
- id: the-last-shift_d_225
|
||||
- id: bartender_d_026
|
||||
text: "Tab's cleared. Lera'll be pleased."
|
||||
role: bartender
|
||||
access: [peer]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Dialogue: day-worker at The Last Shift
|
||||
# NPC: Ren Tosse (Tier 3, NOBODY/CIVILIAN)
|
||||
# Voice: casual-observant, asks questions without social friction. Just drifting.
|
||||
# KEY LINE: the-last-shift_d_001 — sounds like intel gathering on night operations
|
||||
# KEY LINE: day-worker_d_001 — sounds like intel gathering on night operations
|
||||
# Actually: asking about after-hours freight work availability
|
||||
# Ticket: #307 | Sprint: 12
|
||||
|
||||
@@ -15,7 +15,7 @@ lines:
|
||||
# Actually: asking if there's paid after-hours freight work available.
|
||||
# ==========================================
|
||||
|
||||
- id: the-last-shift_d_001
|
||||
- id: day-worker_d_001
|
||||
text: "Night rotation still short-handed? Heard the after-hours freight pays decent."
|
||||
role: day-worker
|
||||
access: [public]
|
||||
@@ -36,7 +36,7 @@ lines:
|
||||
# ROUTINE LINES
|
||||
# ==========================================
|
||||
|
||||
- id: the-last-shift_d_002
|
||||
- id: day-worker_d_002
|
||||
text: "Bay seven any better this cycle? The super was a problem last time."
|
||||
role: day-worker
|
||||
access: [public]
|
||||
@@ -46,7 +46,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [ren, job-seeking]
|
||||
|
||||
- id: the-last-shift_d_003
|
||||
- id: day-worker_d_003
|
||||
text: "Sova's decent. Station layout makes sense once you learn it."
|
||||
role: day-worker
|
||||
access: [public]
|
||||
@@ -56,7 +56,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [ren, atmospheric]
|
||||
|
||||
- id: the-last-shift_d_004
|
||||
- id: day-worker_d_004
|
||||
text: "Just passing through. Maybe not. Depends on the work."
|
||||
role: day-worker
|
||||
access: [public]
|
||||
@@ -66,7 +66,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [ren, drifter]
|
||||
|
||||
- id: the-last-shift_d_005
|
||||
- id: day-worker_d_005
|
||||
text: "Cheap drink here's better than most stations I've stopped at."
|
||||
role: day-worker
|
||||
access: [public]
|
||||
@@ -76,7 +76,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [ren, atmospheric]
|
||||
|
||||
- id: the-last-shift_d_006
|
||||
- id: day-worker_d_006
|
||||
text: "Any idea when the manifest deadline runs? Trying to time when the work window opens."
|
||||
role: day-worker
|
||||
access: [public]
|
||||
|
||||
@@ -12,7 +12,7 @@ lines:
|
||||
# SOCIAL — off-shift casual
|
||||
# ========================================
|
||||
|
||||
- id: the-last-shift_d_001
|
||||
- id: kael-davan_d_015
|
||||
text: "Saved you a seat. Lera's got the spiced rice tonight."
|
||||
role: dock-worker
|
||||
access: [insider, peer]
|
||||
@@ -22,7 +22,7 @@ lines:
|
||||
topic: [personal, colleague]
|
||||
tags: [kael, greeting, phase-1]
|
||||
|
||||
- id: the-last-shift_d_002
|
||||
- id: kael-davan_d_016
|
||||
text: "First round's mine. Don't argue — you covered me last week."
|
||||
role: dock-worker
|
||||
access: [insider, peer]
|
||||
@@ -32,7 +32,7 @@ lines:
|
||||
topic: [personal]
|
||||
tags: [kael, social, phase-1]
|
||||
|
||||
- id: the-last-shift_d_003
|
||||
- id: kael-davan_d_017
|
||||
text: "Voss tried to dock my overtime again. Lera told him off. Wish I'd seen it."
|
||||
role: dock-worker
|
||||
access: [insider, peer]
|
||||
@@ -42,7 +42,7 @@ lines:
|
||||
topic: [colleague, routine]
|
||||
tags: [kael, humor, voss, lera]
|
||||
|
||||
- id: the-last-shift_d_004
|
||||
- id: kael-davan_d_018
|
||||
text: "Torek's holding court at the bar again. How does he afford to drink like that?"
|
||||
role: dock-worker
|
||||
access: [insider, peer]
|
||||
@@ -55,7 +55,7 @@ lines:
|
||||
fact_id: investigation.torek_spending_pattern
|
||||
confidence: suspects
|
||||
|
||||
- id: the-last-shift_d_005
|
||||
- id: kael-davan_d_019
|
||||
text: "Quiet night. I like these ones. No drama, just people winding down."
|
||||
role: dock-worker
|
||||
access: [peer]
|
||||
@@ -65,7 +65,7 @@ lines:
|
||||
topic: [personal]
|
||||
tags: [kael, atmospheric]
|
||||
|
||||
- id: the-last-shift_d_006
|
||||
- id: kael-davan_d_020
|
||||
text: "Lera asked about my overtime schedule. Told her I'm trying to cut back."
|
||||
role: dock-worker
|
||||
access: [insider, peer]
|
||||
@@ -79,7 +79,7 @@ lines:
|
||||
# NAIA REFERENCES — personal, unprompted
|
||||
# ========================================
|
||||
|
||||
- id: the-last-shift_d_007
|
||||
- id: kael-davan_d_021
|
||||
text: "Naia's meeting me here after her shift. Try not to embarrass me."
|
||||
role: dock-worker
|
||||
access: [insider, peer]
|
||||
@@ -89,7 +89,7 @@ lines:
|
||||
topic: [personal]
|
||||
tags: [kael, unprompted, naia-reference]
|
||||
|
||||
- id: the-last-shift_d_008
|
||||
- id: kael-davan_d_022
|
||||
text: "Naia wants to plan a trip. Off-station. Somewhere with actual sky. Sounds expensive."
|
||||
role: dock-worker
|
||||
access: [insider, peer]
|
||||
@@ -99,7 +99,7 @@ lines:
|
||||
topic: [personal]
|
||||
tags: [kael, unprompted, naia-reference]
|
||||
|
||||
- id: the-last-shift_d_009
|
||||
- id: kael-davan_d_023
|
||||
text: "She worries. I tell her it's just dock work. She doesn't buy it anymore."
|
||||
role: dock-worker
|
||||
access: [insider]
|
||||
@@ -113,7 +113,7 @@ lines:
|
||||
# RING OPS — bar context, trust-gated
|
||||
# ========================================
|
||||
|
||||
- id: the-last-shift_d_010
|
||||
- id: kael-davan_d_024
|
||||
text: "Nils wants to talk. Tomorrow, bay side. Said it's about volume."
|
||||
role: dock-worker
|
||||
access: [insider]
|
||||
@@ -123,7 +123,7 @@ lines:
|
||||
topic: [danger]
|
||||
tags: [kael, ring-ops, nils]
|
||||
|
||||
- id: the-last-shift_d_011
|
||||
- id: kael-davan_d_025
|
||||
text: "Keep it light tonight. Torek's listening and he doesn't know when to stop."
|
||||
role: dock-worker
|
||||
access: [insider]
|
||||
@@ -133,7 +133,7 @@ lines:
|
||||
topic: [danger, colleague]
|
||||
tags: [kael, ring-ops, torek, caution]
|
||||
|
||||
- id: the-last-shift_d_012
|
||||
- id: kael-davan_d_026
|
||||
text: "Lera knows more than she lets on. She won't say anything — but don't test it."
|
||||
role: dock-worker
|
||||
access: [insider]
|
||||
@@ -150,7 +150,7 @@ lines:
|
||||
# POST-CONTRADICTION — at the bar, damaged trust
|
||||
# ========================================
|
||||
|
||||
- id: the-last-shift_d_013
|
||||
- id: kael-davan_d_027
|
||||
text: "I saved your seat. ...Force of habit."
|
||||
role: dock-worker
|
||||
access: [insider, peer]
|
||||
@@ -160,7 +160,7 @@ lines:
|
||||
topic: [personal]
|
||||
tags: [kael, contaminated-trust, phase-5]
|
||||
|
||||
- id: the-last-shift_d_014
|
||||
- id: kael-davan_d_028
|
||||
text: "Naia keeps asking why I'm quiet. I told her work stress. Not wrong."
|
||||
role: dock-worker
|
||||
access: [insider]
|
||||
@@ -170,7 +170,7 @@ lines:
|
||||
topic: [personal]
|
||||
tags: [kael, contaminated-trust, naia-reference, phase-5]
|
||||
|
||||
- id: the-last-shift_d_015
|
||||
- id: kael-davan_d_029
|
||||
text: "We're good, right? You and me. We're still good."
|
||||
role: dock-worker
|
||||
access: [insider]
|
||||
@@ -187,7 +187,7 @@ lines:
|
||||
# ========================================
|
||||
|
||||
# first_meeting: InteractionMemory.count == 0. RelationshipState: Unknown → Public access only.
|
||||
- id: the-last-shift_d_016
|
||||
- id: kael-davan_d_030
|
||||
text: "Evening. Friend of someone's, or first time at Lera's?"
|
||||
role: dock-worker
|
||||
access: [public]
|
||||
@@ -199,7 +199,7 @@ lines:
|
||||
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
|
||||
- id: kael-davan_d_031
|
||||
text: "You made it. I was starting to think something came up."
|
||||
role: dock-worker
|
||||
access: [peer, insider]
|
||||
@@ -211,7 +211,7 @@ lines:
|
||||
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
|
||||
- id: kael-davan_d_032
|
||||
text: "Still in the district. Thought you'd be done by now."
|
||||
role: dock-worker
|
||||
access: [authority]
|
||||
@@ -223,7 +223,7 @@ lines:
|
||||
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
|
||||
- id: kael-davan_d_033
|
||||
text: "You came back."
|
||||
role: dock-worker
|
||||
access: [peer, insider]
|
||||
|
||||
@@ -9,7 +9,7 @@ location: the-last-shift
|
||||
role: commission-liaison
|
||||
lines:
|
||||
|
||||
- id: the-last-shift_d_204
|
||||
- id: pc-detective_d_001
|
||||
text: "Good evening. Off the clock, more or less. Enjoying the local hospitality."
|
||||
role: commission-liaison
|
||||
access: [public]
|
||||
@@ -19,7 +19,7 @@ lines:
|
||||
topic: [personal]
|
||||
tags: [pc-detective, greeting, public-tier]
|
||||
|
||||
- id: the-last-shift_d_205
|
||||
- id: pc-detective_d_002
|
||||
text: "Ms. Venn recommended the ale. She was right. This district has its charms."
|
||||
role: commission-liaison
|
||||
access: [public]
|
||||
@@ -28,7 +28,7 @@ lines:
|
||||
topic: [routine, colleague]
|
||||
tags: [pc-detective, social, public-tier, sera]
|
||||
|
||||
- id: the-last-shift_d_206
|
||||
- id: pc-detective_d_003
|
||||
text: "Just unwinding. The review can wait until morning. Have a good evening."
|
||||
role: commission-liaison
|
||||
access: [public]
|
||||
|
||||
@@ -9,7 +9,7 @@ location: the-last-shift
|
||||
role: dock-worker
|
||||
lines:
|
||||
|
||||
- id: the-last-shift_d_201
|
||||
- id: pc-smuggler_d_001
|
||||
text: "Evening. Just having a drink with friends. Can I help you?"
|
||||
role: dock-worker
|
||||
access: [public]
|
||||
@@ -19,7 +19,7 @@ lines:
|
||||
topic: [personal]
|
||||
tags: [pc-smuggler, greeting, public-tier]
|
||||
|
||||
- id: the-last-shift_d_202
|
||||
- id: pc-smuggler_d_002
|
||||
text: "Lera's ale is the one good thing about a ten-hour shift. Don't let anyone tell you different."
|
||||
role: dock-worker
|
||||
access: [public]
|
||||
@@ -29,7 +29,7 @@ lines:
|
||||
topic: [routine]
|
||||
tags: [pc-smuggler, social, public-tier]
|
||||
|
||||
- id: the-last-shift_d_203
|
||||
- id: pc-smuggler_d_003
|
||||
text: "Off the clock, thanks. Whatever it is, it can wait until morning."
|
||||
role: dock-worker
|
||||
access: [authority]
|
||||
|
||||
@@ -38,7 +38,7 @@ lines:
|
||||
# Phase 3-5: same warmth, but the detective is now watching
|
||||
# ============================================================
|
||||
|
||||
- id: the-last-shift_d_101
|
||||
- id: sera-venn_d_001
|
||||
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]
|
||||
@@ -48,7 +48,7 @@ lines:
|
||||
mood: [warm, content]
|
||||
tags: [greeting, phase-1, establishes-warmth]
|
||||
|
||||
- id: the-last-shift_d_102
|
||||
- id: sera-venn_d_002
|
||||
text: "Corner booth's free. I saved it — force of habit."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
@@ -58,7 +58,7 @@ lines:
|
||||
mood: [warm]
|
||||
tags: [greeting, phase-1, spatial-anchor]
|
||||
|
||||
- id: the-last-shift_d_103
|
||||
- id: sera-venn_d_003
|
||||
text: "You look like you've been reading manifests all day. Sit. I'll get you something that isn't recycled air."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
@@ -68,7 +68,7 @@ lines:
|
||||
mood: [warm, content]
|
||||
tags: [greeting, phase-1-2]
|
||||
|
||||
- id: the-last-shift_d_104
|
||||
- id: sera-venn_d_004
|
||||
text: "Evening. Same booth, same drink?"
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
@@ -78,7 +78,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [greeting, phase-2-5, repeatable]
|
||||
|
||||
- id: the-last-shift_d_105
|
||||
- id: sera-venn_d_005
|
||||
text: "Good timing. Naia was here earlier — you just missed her."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
@@ -88,7 +88,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [greeting, phase-2, naia-reference]
|
||||
|
||||
- id: the-last-shift_d_106
|
||||
- id: sera-venn_d_006
|
||||
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]
|
||||
@@ -98,7 +98,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [greeting, phase-1-3]
|
||||
|
||||
- id: the-last-shift_d_107
|
||||
- id: sera-venn_d_007
|
||||
text: "Hey. Long day?"
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
@@ -108,7 +108,7 @@ lines:
|
||||
mood: [frustrated]
|
||||
tags: [greeting, phase-3-5, post-pattern, shorter]
|
||||
|
||||
- id: the-last-shift_d_108
|
||||
- id: sera-venn_d_008
|
||||
text: "Same time, same place. You're becoming a regular."
|
||||
role: bar-regular
|
||||
access: [peer, public]
|
||||
@@ -123,7 +123,7 @@ lines:
|
||||
# What Sera says during normal bar evenings
|
||||
# ============================================================
|
||||
|
||||
- id: the-last-shift_d_110
|
||||
- id: sera-venn_d_009
|
||||
text: "The Last Shift. Official name. Everyone calls it Lera's, though. Eighteen years behind that counter."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
@@ -133,7 +133,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [orientation, district-background]
|
||||
|
||||
- id: the-last-shift_d_111
|
||||
- id: sera-venn_d_010
|
||||
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]
|
||||
@@ -143,7 +143,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [orientation, npc-introduction]
|
||||
|
||||
- id: the-last-shift_d_112
|
||||
- id: sera-venn_d_011
|
||||
text: "Most of the after-shift crowd comes from the Terminal. Dock workers, scheduler types. They drink hard and leave early."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
@@ -153,7 +153,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [orientation, district-background]
|
||||
|
||||
- id: the-last-shift_d_113
|
||||
- id: sera-venn_d_012
|
||||
text: "Sova's not bad, once you know the rhythms. Shift change is when things move. Between shifts is when you can breathe."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
@@ -163,7 +163,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [setting-atmosphere]
|
||||
|
||||
- id: the-last-shift_d_114
|
||||
- id: sera-venn_d_013
|
||||
text: "I do lattice calibration, mostly. Commission-standard equipment checks. Relay nodes, terminal diagnostics. Exciting stuff."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
@@ -173,7 +173,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [self-introduction, institutional-background]
|
||||
|
||||
- id: the-last-shift_d_115
|
||||
- id: sera-venn_d_014
|
||||
text: "I transferred here fourteen months ago. Border-system relay posting before this. Sova was supposed to be the quiet one."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
@@ -183,7 +183,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [backstory, phase-1]
|
||||
|
||||
- id: the-last-shift_d_116
|
||||
- id: sera-venn_d_015
|
||||
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]
|
||||
@@ -193,7 +193,7 @@ lines:
|
||||
mood: [warm]
|
||||
tags: [community-texture]
|
||||
|
||||
- id: the-last-shift_d_117
|
||||
- id: sera-venn_d_016
|
||||
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]
|
||||
@@ -203,7 +203,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [setting-atmosphere, routine]
|
||||
|
||||
- id: the-last-shift_d_118
|
||||
- id: sera-venn_d_017
|
||||
text: "You adapting to station gravity yet? Point-nine-three standard takes a week to stop noticing."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
@@ -213,7 +213,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [setting-detail, phase-1]
|
||||
|
||||
- id: the-last-shift_d_119
|
||||
- id: sera-venn_d_018
|
||||
text: "I like this booth. Corner. You can see the door and the bar. Force of institutional habit, I suppose."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
@@ -232,7 +232,7 @@ lines:
|
||||
|
||||
# --- Surface trust ---
|
||||
|
||||
- id: the-last-shift_d_120
|
||||
- id: sera-venn_d_019
|
||||
text: "The Commission runs maintenance checks quarterly. I handle the field side — equipment calibration, relay diagnostics. Standard."
|
||||
role: bar-regular
|
||||
access: [peer, authority]
|
||||
@@ -242,7 +242,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [institutional-background]
|
||||
|
||||
- id: the-last-shift_d_121
|
||||
- id: sera-venn_d_020
|
||||
text: "Naia teaches the station kids. Primary education, mostly. She's good at it — patient. The kids adore her."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
@@ -252,7 +252,7 @@ lines:
|
||||
mood: [warm]
|
||||
tags: [naia-reference, phase-2]
|
||||
|
||||
- id: the-last-shift_d_122
|
||||
- id: sera-venn_d_021
|
||||
text: "The district runs on freight. Terminal handles most of it — container processing, manifest filing. Bread and butter."
|
||||
role: bar-regular
|
||||
access: [peer, public]
|
||||
@@ -262,7 +262,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [district-context]
|
||||
|
||||
- id: the-last-shift_d_123
|
||||
- id: sera-venn_d_022
|
||||
text: "I don't know the dock workers well. I'm Commission, they're logistics. Different worlds, same station."
|
||||
role: bar-regular
|
||||
access: [peer, authority]
|
||||
@@ -272,7 +272,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [deflection-surface, distance-from-hub]
|
||||
|
||||
- id: the-last-shift_d_124
|
||||
- id: sera-venn_d_023
|
||||
text: "Most people here are decent. Working shifts, paying bills, coming to Lera's. It's a community."
|
||||
role: bar-regular
|
||||
access: [peer, public]
|
||||
@@ -282,7 +282,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [community-framing]
|
||||
|
||||
- id: the-last-shift_d_125
|
||||
- id: sera-venn_d_024
|
||||
text: "I can look things up if you need. Commission terminals access compliance records, shift logs, equipment histories. Part of the job."
|
||||
role: bar-regular
|
||||
access: [peer, authority]
|
||||
@@ -294,7 +294,7 @@ lines:
|
||||
|
||||
# --- Real trust ---
|
||||
|
||||
- id: the-last-shift_d_130
|
||||
- id: sera-venn_d_025
|
||||
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]
|
||||
@@ -307,7 +307,7 @@ lines:
|
||||
fact_id: "relationship.kael_naia_connection"
|
||||
confidence: "knows_of"
|
||||
|
||||
- id: the-last-shift_d_131
|
||||
- id: sera-venn_d_026
|
||||
text: "The Commission kiosk on Level 3 logs every compliance check. If someone ran an unauthorized scan, it would show up there. Hypothetically."
|
||||
role: bar-regular
|
||||
access: [peer, authority]
|
||||
@@ -317,7 +317,7 @@ lines:
|
||||
mood: [focused]
|
||||
tags: [institutional-access, phase-2-3, self-incriminating-hint]
|
||||
|
||||
- id: the-last-shift_d_132
|
||||
- id: sera-venn_d_027
|
||||
text: "I worry about Naia. She doesn't deserve whatever's happening. She just wants Kael to come home at a reasonable hour."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
@@ -327,7 +327,7 @@ lines:
|
||||
mood: [frustrated, anxious]
|
||||
tags: [motivation-reveal, phase-2-3]
|
||||
|
||||
- id: the-last-shift_d_133
|
||||
- id: sera-venn_d_028
|
||||
text: "Compliance data goes back eighteen months on the Commission systems. Anyone with field-tech access could cross-reference shift records with manifest filings."
|
||||
role: bar-regular
|
||||
access: [peer, authority]
|
||||
@@ -340,7 +340,7 @@ lines:
|
||||
fact_id: "investigation.manifest_discrepancy"
|
||||
confidence: "suspects"
|
||||
|
||||
- id: the-last-shift_d_134
|
||||
- id: sera-venn_d_029
|
||||
text: "I'm not an investigator. I calibrate equipment and file reports. But I notice things. Patterns in the data, mostly."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
@@ -350,7 +350,7 @@ lines:
|
||||
mood: [focused]
|
||||
tags: [self-awareness, capability-reveal]
|
||||
|
||||
- id: the-last-shift_d_135
|
||||
- id: sera-venn_d_030
|
||||
text: "You know me. I wouldn't sit on something if I thought it mattered. But not everything that looks irregular IS irregular."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
@@ -360,7 +360,7 @@ lines:
|
||||
mood: [anxious]
|
||||
tags: [deflection-real-tier, phase-4, the-lie]
|
||||
|
||||
- id: the-last-shift_d_136
|
||||
- id: sera-venn_d_031
|
||||
text: "Some of the regulars here have hard lives. Grey economy, tight margins. I'm not judging. I'm just a field tech who likes the ale."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
@@ -372,7 +372,7 @@ lines:
|
||||
|
||||
# --- Secret trust ---
|
||||
|
||||
- id: the-last-shift_d_140
|
||||
- id: sera-venn_d_032
|
||||
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]
|
||||
@@ -385,7 +385,7 @@ lines:
|
||||
fact_id: "investigation.manifest_discrepancy"
|
||||
confidence: "knows_details"
|
||||
|
||||
- id: the-last-shift_d_141
|
||||
- id: sera-venn_d_033
|
||||
text: "If I report it, Naia's life falls apart. Kael gets investigated. Maybe arrested. She didn't ask me to find this. I did it on my own."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
@@ -395,7 +395,7 @@ lines:
|
||||
mood: [anxious]
|
||||
tags: [motivation-confession, phase-5]
|
||||
|
||||
- id: the-last-shift_d_142
|
||||
- id: sera-venn_d_034
|
||||
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]
|
||||
@@ -408,7 +408,7 @@ lines:
|
||||
fact_id: "investigation.shift_mismatch"
|
||||
confidence: "knows_details"
|
||||
|
||||
- id: the-last-shift_d_143
|
||||
- id: sera-venn_d_035
|
||||
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]
|
||||
@@ -418,7 +418,7 @@ lines:
|
||||
mood: [suspicious, anxious]
|
||||
tags: [denial-under-pressure, phase-4-5, the-wall]
|
||||
|
||||
- id: the-last-shift_d_144
|
||||
- id: sera-venn_d_036
|
||||
text: "Every tip I gave you was real. The district background, the shift patterns, the regulars. I didn't steer you wrong. I just... didn't give you everything."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
@@ -434,7 +434,7 @@ lines:
|
||||
# Phase 1-2: genuinely helpful. Phase 3-5: overcompensation.
|
||||
# ============================================================
|
||||
|
||||
- id: the-last-shift_d_150
|
||||
- id: sera-venn_d_037
|
||||
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]
|
||||
@@ -447,7 +447,7 @@ lines:
|
||||
fact_id: "relationship.trust_network"
|
||||
confidence: "suspects"
|
||||
|
||||
- id: the-last-shift_d_151
|
||||
- id: sera-venn_d_038
|
||||
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]
|
||||
@@ -457,7 +457,7 @@ lines:
|
||||
mood: [focused]
|
||||
tags: [unprompted, overcompensation, phase-3-4]
|
||||
|
||||
- id: the-last-shift_d_152
|
||||
- id: sera-venn_d_039
|
||||
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]
|
||||
@@ -467,7 +467,7 @@ lines:
|
||||
mood: [focused, frustrated]
|
||||
tags: [unprompted, overcompensation, phase-3-4]
|
||||
|
||||
- id: the-last-shift_d_153
|
||||
- id: sera-venn_d_040
|
||||
text: "The maintenance corridor access logs are Commission-maintained. I can pull them if you need. Just say the word."
|
||||
role: bar-regular
|
||||
access: [peer, authority]
|
||||
@@ -477,7 +477,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [unprompted, information-offer, overcompensation, phase-3-5]
|
||||
|
||||
- id: the-last-shift_d_154
|
||||
- id: sera-venn_d_041
|
||||
text: "Naia mentioned Kael came home late again last night. Third time this week. She's trying not to make it a thing."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
@@ -490,7 +490,7 @@ lines:
|
||||
fact_id: "relationship.kael_naia_connection"
|
||||
confidence: "knows_details"
|
||||
|
||||
- id: the-last-shift_d_155
|
||||
- id: sera-venn_d_042
|
||||
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]
|
||||
@@ -505,7 +505,7 @@ lines:
|
||||
# Phase 3-4: avoidance excuses, topic deflection, controlled evasion
|
||||
# ============================================================
|
||||
|
||||
- id: the-last-shift_d_160
|
||||
- id: sera-venn_d_043
|
||||
text: "Actually, I should head out. Early shift tomorrow — calibration rounds start at 07:30."
|
||||
role: bar-regular
|
||||
access: [peer, public]
|
||||
@@ -515,7 +515,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [avoidance-excuse-1, torek-trigger, phase-3]
|
||||
|
||||
- id: the-last-shift_d_161
|
||||
- id: sera-venn_d_044
|
||||
text: "Sorry — headache coming on. Station air does that sometimes. I'll catch you tomorrow."
|
||||
role: bar-regular
|
||||
access: [peer, public]
|
||||
@@ -525,7 +525,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [avoidance-excuse-2, torek-trigger, phase-3]
|
||||
|
||||
- id: the-last-shift_d_162
|
||||
- id: sera-venn_d_045
|
||||
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]
|
||||
@@ -535,7 +535,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [avoidance-excuse-3, torek-trigger, phase-3]
|
||||
|
||||
- id: the-last-shift_d_163
|
||||
- id: sera-venn_d_046
|
||||
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]
|
||||
@@ -545,7 +545,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [torek-deflection, phase-4, the-deflection]
|
||||
|
||||
- id: the-last-shift_d_164
|
||||
- id: sera-venn_d_047
|
||||
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]
|
||||
@@ -555,7 +555,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [topic-deflection, cargo-avoidance, phase-3-5]
|
||||
|
||||
- id: the-last-shift_d_165
|
||||
- id: sera-venn_d_048
|
||||
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]
|
||||
@@ -565,7 +565,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [redirect, warmth-recovery, phase-4]
|
||||
|
||||
- id: the-last-shift_d_166
|
||||
- id: sera-venn_d_049
|
||||
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]
|
||||
@@ -575,7 +575,7 @@ lines:
|
||||
mood: [suspicious]
|
||||
tags: [deflection-firm, phase-4-5, the-wall]
|
||||
|
||||
- id: the-last-shift_d_167
|
||||
- id: sera-venn_d_050
|
||||
text: "Let's talk about something else. How's the investigation going — generally? Making progress?"
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
@@ -591,7 +591,7 @@ lines:
|
||||
# Same surface warmth, but calibrated. Trust is contaminated.
|
||||
# ============================================================
|
||||
|
||||
- id: the-last-shift_d_170
|
||||
- id: sera-venn_d_051
|
||||
text: "Same booth. Drink's on me tonight."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
@@ -601,7 +601,7 @@ lines:
|
||||
mood: [warm]
|
||||
tags: [post-discovery, surface-warmth, phase-5]
|
||||
|
||||
- id: the-last-shift_d_171
|
||||
- id: sera-venn_d_052
|
||||
text: "I pulled the relay diagnostic data you asked about. Clean. No anomalies in the terminal equipment this quarter."
|
||||
role: bar-regular
|
||||
access: [peer, authority]
|
||||
@@ -611,7 +611,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [post-discovery, overcompensation, phase-5]
|
||||
|
||||
- id: the-last-shift_d_172
|
||||
- id: sera-venn_d_053
|
||||
text: "You've been quiet tonight. Everything all right with the case?"
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
@@ -621,7 +621,7 @@ lines:
|
||||
mood: [frustrated]
|
||||
tags: [post-discovery, awareness, phase-5]
|
||||
|
||||
- id: the-last-shift_d_173
|
||||
- id: sera-venn_d_054
|
||||
text: "I know you're good at what you do. That's why I'm glad you're here. This district needed someone thorough."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
@@ -631,7 +631,7 @@ lines:
|
||||
mood: [warm, anxious]
|
||||
tags: [post-discovery, genuine-but-contaminated, phase-5]
|
||||
|
||||
- id: the-last-shift_d_174
|
||||
- id: sera-venn_d_055
|
||||
text: "If there's anything I can help with — anything at all — you just ask. You know that."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
@@ -650,7 +650,7 @@ lines:
|
||||
|
||||
# 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
|
||||
- id: sera-venn_d_056
|
||||
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]
|
||||
@@ -663,7 +663,7 @@ lines:
|
||||
|
||||
# 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
|
||||
- id: sera-venn_d_057
|
||||
text: "I was hoping you'd come in. Saved the corner booth. How'd today go?"
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
@@ -676,7 +676,7 @@ lines:
|
||||
|
||||
# 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
|
||||
- id: sera-venn_d_058
|
||||
text: "Terminal crew? Grab a seat. Kael usually sits in the back if you're looking for him."
|
||||
role: bar-regular
|
||||
access: [peer, insider]
|
||||
@@ -689,7 +689,7 @@ lines:
|
||||
|
||||
# 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
|
||||
- id: sera-venn_d_059
|
||||
text: "You're back. I wasn't sure you would be."
|
||||
role: bar-regular
|
||||
access: [peer]
|
||||
|
||||
@@ -13,7 +13,7 @@ lines:
|
||||
# ARRIVALS — checking in
|
||||
# ==========================================
|
||||
|
||||
- id: the-terminal_d_500
|
||||
- id: courier_d_001
|
||||
text: "Route's clear. Moving on schedule."
|
||||
role: courier
|
||||
access: [public, peer]
|
||||
@@ -23,7 +23,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [renn, greeting, operational]
|
||||
|
||||
- id: the-terminal_d_501
|
||||
- id: courier_d_002
|
||||
text: "Manifest for bay three. Needs a scheduler stamp."
|
||||
role: courier
|
||||
access: [public, peer]
|
||||
@@ -33,7 +33,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [renn, greeting, procedural]
|
||||
|
||||
- id: the-terminal_d_502
|
||||
- id: courier_d_003
|
||||
text: "Inbound from Havara. Three cans, all matched."
|
||||
role: courier
|
||||
access: [public, peer]
|
||||
@@ -47,7 +47,7 @@ lines:
|
||||
# SURFACE ROUTINE — low-information, on-script
|
||||
# ==========================================
|
||||
|
||||
- id: the-terminal_d_503
|
||||
- id: courier_d_004
|
||||
text: "Not staying. Just the drop."
|
||||
role: courier
|
||||
access: [public, peer]
|
||||
@@ -57,7 +57,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [renn, operational, brief]
|
||||
|
||||
- id: the-terminal_d_504
|
||||
- id: courier_d_005
|
||||
text: "Paperwork's clean. You can check."
|
||||
role: courier
|
||||
access: [public, peer, authority]
|
||||
@@ -67,7 +67,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [renn, procedural, documentation]
|
||||
|
||||
- id: the-terminal_d_505
|
||||
- id: courier_d_006
|
||||
text: "Pickup's scheduled at 1600. I'll be back."
|
||||
role: courier
|
||||
access: [public, peer]
|
||||
@@ -77,7 +77,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [renn, operational]
|
||||
|
||||
- id: the-terminal_d_506
|
||||
- id: courier_d_007
|
||||
text: "Corridor's clear from my end. No delays."
|
||||
role: courier
|
||||
access: [public, peer]
|
||||
@@ -94,7 +94,7 @@ lines:
|
||||
# RING-ADJACENT — insider/real trust only
|
||||
# ==========================================
|
||||
|
||||
- id: the-terminal_d_507
|
||||
- id: courier_d_008
|
||||
text: "Documentation's handled. Bay four, per the note."
|
||||
role: courier
|
||||
access: [insider]
|
||||
@@ -107,7 +107,7 @@ lines:
|
||||
smuggler: "Renn's confirming the paperwork trail. Bay four, as arranged."
|
||||
detective: "Never hears this line."
|
||||
|
||||
- id: the-terminal_d_508
|
||||
- id: courier_d_009
|
||||
text: "Transit stamp's current. Nothing flagged at the gate."
|
||||
role: courier
|
||||
access: [insider]
|
||||
@@ -117,7 +117,7 @@ lines:
|
||||
mood: [focused]
|
||||
tags: [renn, ring-ops, documentation]
|
||||
|
||||
- id: the-terminal_d_509
|
||||
- id: courier_d_010
|
||||
text: "Pickup window is the same as last cycle. Twenty minutes."
|
||||
role: courier
|
||||
access: [insider]
|
||||
@@ -130,7 +130,7 @@ lines:
|
||||
smuggler: "Twenty minutes. Transition window confirmation."
|
||||
detective: "Never hears this line."
|
||||
|
||||
- id: the-terminal_d_510
|
||||
- id: courier_d_011
|
||||
text: "Tell Voss the hold note clears at 1400."
|
||||
role: courier
|
||||
access: [insider]
|
||||
@@ -147,7 +147,7 @@ lines:
|
||||
# DETECTIVE ENCOUNTER — surface cooperation
|
||||
# ==========================================
|
||||
|
||||
- id: the-terminal_d_511
|
||||
- id: courier_d_012
|
||||
text: "Licensed freight courier. Manifest's available on request."
|
||||
role: courier
|
||||
access: [authority]
|
||||
@@ -157,7 +157,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [renn, detective-path, procedural]
|
||||
|
||||
- id: the-terminal_d_512
|
||||
- id: courier_d_013
|
||||
text: "I drop documentation. What's inside the containers isn't my business."
|
||||
role: courier
|
||||
access: [authority]
|
||||
@@ -170,7 +170,7 @@ lines:
|
||||
smuggler: "That's the answer. Clean and correct."
|
||||
detective: "Classic courier deflection. Legally accurate. Operationally suspicious."
|
||||
|
||||
- id: the-terminal_d_513
|
||||
- id: courier_d_014
|
||||
text: "I've been running this route for two years. Nothing unusual."
|
||||
role: courier
|
||||
access: [authority, public]
|
||||
@@ -184,7 +184,7 @@ lines:
|
||||
# DEPARTURES
|
||||
# ==========================================
|
||||
|
||||
- id: the-terminal_d_514
|
||||
- id: courier_d_015
|
||||
text: "That's the drop. I'm out."
|
||||
role: courier
|
||||
access: [public, peer]
|
||||
@@ -194,7 +194,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [renn, closing]
|
||||
|
||||
- id: the-terminal_d_515
|
||||
- id: courier_d_016
|
||||
text: "Next run's tomorrow. Early."
|
||||
role: courier
|
||||
access: [public, peer]
|
||||
@@ -208,7 +208,7 @@ lines:
|
||||
# GENERATION PASS — ambient variants
|
||||
# ==========================================
|
||||
|
||||
- id: the-terminal_d_516
|
||||
- id: courier_d_017
|
||||
text: "Two stops today. First one's done."
|
||||
role: courier
|
||||
access: [public, peer]
|
||||
@@ -218,7 +218,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [renn, greeting, generation-pass]
|
||||
|
||||
- id: the-terminal_d_517
|
||||
- id: courier_d_018
|
||||
text: "Outbound for Havara. Needs a bay assignment."
|
||||
role: courier
|
||||
access: [public, peer]
|
||||
@@ -228,7 +228,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [renn, greeting, procedural, generation-pass]
|
||||
|
||||
- id: the-terminal_d_518
|
||||
- id: courier_d_019
|
||||
text: "Gate's backed up. Running twenty minutes late."
|
||||
role: courier
|
||||
access: [public, peer]
|
||||
@@ -238,7 +238,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [renn, delay, generation-pass]
|
||||
|
||||
- id: the-terminal_d_519
|
||||
- id: courier_d_020
|
||||
text: "Paperwork's in order. Same as always."
|
||||
role: courier
|
||||
access: [public, peer, authority]
|
||||
@@ -248,7 +248,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [renn, procedural, generation-pass]
|
||||
|
||||
- id: the-terminal_d_520
|
||||
- id: courier_d_021
|
||||
text: "Quick turnaround today. Back out by 1300."
|
||||
role: courier
|
||||
access: [public, peer]
|
||||
@@ -258,7 +258,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [renn, operational, generation-pass]
|
||||
|
||||
- id: the-terminal_d_521
|
||||
- id: courier_d_022
|
||||
text: "Signed off on bay three. That's me done here."
|
||||
role: courier
|
||||
access: [public, peer]
|
||||
|
||||
@@ -12,7 +12,7 @@ lines:
|
||||
# GREETINGS — shift arrival
|
||||
# ==========================================
|
||||
|
||||
- id: the-terminal_d_200
|
||||
- id: dock-worker_d_001
|
||||
text: "Push today. Bay six's backed up."
|
||||
role: dock-worker
|
||||
access: [public, peer]
|
||||
@@ -22,7 +22,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [dock-worker, greeting]
|
||||
|
||||
- id: the-terminal_d_201
|
||||
- id: dock-worker_d_002
|
||||
text: "Late night last cycle. Bay eight ran until 2300."
|
||||
role: dock-worker
|
||||
access: [public, peer]
|
||||
@@ -32,7 +32,7 @@ lines:
|
||||
mood: [frustrated]
|
||||
tags: [dock-worker, greeting]
|
||||
|
||||
- id: the-terminal_d_202
|
||||
- id: dock-worker_d_003
|
||||
text: "Voss is on already. Floor's locked down tight."
|
||||
role: dock-worker
|
||||
access: [peer]
|
||||
@@ -49,7 +49,7 @@ lines:
|
||||
# FLOOR TALK — routine, cargo, observation
|
||||
# ==========================================
|
||||
|
||||
- id: the-terminal_d_203
|
||||
- id: dock-worker_d_004
|
||||
text: "Bay four's been on hold three days. Nobody says why."
|
||||
role: dock-worker
|
||||
access: [public, peer]
|
||||
@@ -62,7 +62,7 @@ lines:
|
||||
smuggler: "He doesn't know what's in it. Just annoyed at the backup."
|
||||
detective: "Worker notices Bay 4 hold as unusual. Not suspicious — just a worker tracking the floor."
|
||||
|
||||
- id: the-terminal_d_204
|
||||
- id: dock-worker_d_005
|
||||
text: "Manifest looks clean. Three inbounds, two outbounds. Light day."
|
||||
role: dock-worker
|
||||
access: [public, peer]
|
||||
@@ -72,7 +72,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [dock-worker, manifest]
|
||||
|
||||
- id: the-terminal_d_205
|
||||
- id: dock-worker_d_006
|
||||
text: "Maret's been at that terminal all morning. Never good."
|
||||
role: dock-worker
|
||||
access: [peer]
|
||||
@@ -85,7 +85,7 @@ lines:
|
||||
smuggler: "Maret reviewing something. If she flags a discrepancy, Voss'll sit on it. He always does."
|
||||
detective: "Workers have noticed Maret's behavior. Whatever she found, it's not small."
|
||||
|
||||
- id: the-terminal_d_206
|
||||
- id: dock-worker_d_007
|
||||
text: "Drin ran bay six again last cycle. Third inspection this week."
|
||||
role: dock-worker
|
||||
access: [public, peer]
|
||||
@@ -98,7 +98,7 @@ lines:
|
||||
smuggler: "Drin on six while bay four stays held. Scheduled or arranged?"
|
||||
detective: "Workers notice inspection distribution. Drin on six, bay four untouched."
|
||||
|
||||
- id: the-terminal_d_207
|
||||
- id: dock-worker_d_008
|
||||
text: "Transition window's twice as long with Commission eyes on the floor."
|
||||
role: dock-worker
|
||||
access: [peer]
|
||||
@@ -111,7 +111,7 @@ lines:
|
||||
smuggler: "Commission presence during transition. Window's compromised."
|
||||
detective: "Workers slow down when Commission is visible. Normal — or trained."
|
||||
|
||||
- id: the-terminal_d_208
|
||||
- id: dock-worker_d_009
|
||||
text: "Good crew this cycle. Linn and Pael are both on."
|
||||
role: dock-worker
|
||||
access: [public, peer]
|
||||
@@ -121,7 +121,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [dock-worker, pael, community]
|
||||
|
||||
- id: the-terminal_d_209
|
||||
- id: dock-worker_d_010
|
||||
text: "Cargo mark's off by forty, according to Maret. Voss logged it and moved on."
|
||||
role: dock-worker
|
||||
access: [peer]
|
||||
@@ -137,7 +137,7 @@ lines:
|
||||
fact_id: investigation.oversight_gap_pattern
|
||||
confidence: suspects
|
||||
|
||||
- id: the-terminal_d_210
|
||||
- id: dock-worker_d_011
|
||||
text: "C-4 containers haven't been checked in three days. Not our problem, apparently."
|
||||
role: dock-worker
|
||||
access: [peer]
|
||||
@@ -154,7 +154,7 @@ lines:
|
||||
# COMMUNITY / CASUAL
|
||||
# ==========================================
|
||||
|
||||
- id: the-terminal_d_211
|
||||
- id: dock-worker_d_012
|
||||
text: "You heading to the Last Shift after? Lera's got the grain spirit back."
|
||||
role: dock-worker
|
||||
access: [public, peer]
|
||||
@@ -164,7 +164,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [dock-worker, social, the-last-shift]
|
||||
|
||||
- id: the-terminal_d_212
|
||||
- id: dock-worker_d_013
|
||||
text: "Workers' Association meeting's cycle twenty-two. Handoff protocol's back on the agenda."
|
||||
role: dock-worker
|
||||
access: [public, peer]
|
||||
@@ -174,7 +174,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [dock-worker, workers-association, community]
|
||||
|
||||
- id: the-terminal_d_213
|
||||
- id: dock-worker_d_014
|
||||
text: "Drifters lost again. Four-three. Pael seemed to take it personally."
|
||||
role: dock-worker
|
||||
access: [public, peer]
|
||||
@@ -188,7 +188,7 @@ lines:
|
||||
# DETECTIVE ENCOUNTER — deflection, redirect
|
||||
# ==========================================
|
||||
|
||||
- id: the-terminal_d_214
|
||||
- id: dock-worker_d_015
|
||||
text: "Manifest questions go to the scheduler. I'm floor-side."
|
||||
role: dock-worker
|
||||
access: [authority, public]
|
||||
@@ -198,7 +198,7 @@ lines:
|
||||
mood: [suspicious]
|
||||
tags: [dock-worker, detective-path, redirect]
|
||||
|
||||
- id: the-terminal_d_215
|
||||
- id: dock-worker_d_016
|
||||
text: "Commission's asked before. Same answers. We run a clean floor."
|
||||
role: dock-worker
|
||||
access: [authority]
|
||||
@@ -208,7 +208,7 @@ lines:
|
||||
mood: [suspicious]
|
||||
tags: [dock-worker, detective-path, deflection]
|
||||
|
||||
- id: the-terminal_d_216
|
||||
- id: dock-worker_d_017
|
||||
text: "Voss handles discrepancies. That's above my level."
|
||||
role: dock-worker
|
||||
access: [authority, peer]
|
||||
@@ -222,7 +222,7 @@ lines:
|
||||
# PEER CANDOR — elevated trust
|
||||
# ==========================================
|
||||
|
||||
- id: the-terminal_d_217
|
||||
- id: dock-worker_d_018
|
||||
text: "Maret's going to push it. She doesn't leave a number sitting."
|
||||
role: dock-worker
|
||||
access: [peer]
|
||||
@@ -235,7 +235,7 @@ lines:
|
||||
smuggler: "Maret's not going to drop it. That's the problem."
|
||||
detective: "Workers think Maret will force the issue. She may be the break."
|
||||
|
||||
- id: the-terminal_d_218
|
||||
- id: dock-worker_d_019
|
||||
text: "Voss runs a tight floor. Maybe too tight, if you take my meaning."
|
||||
role: dock-worker
|
||||
access: [peer]
|
||||
@@ -252,7 +252,7 @@ lines:
|
||||
# SIGN-OFF
|
||||
# ==========================================
|
||||
|
||||
- id: the-terminal_d_219
|
||||
- id: dock-worker_d_020
|
||||
text: "Bay's clear. Good shift."
|
||||
role: dock-worker
|
||||
access: [public, peer]
|
||||
@@ -262,7 +262,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [dock-worker, closing]
|
||||
|
||||
- id: the-terminal_d_220
|
||||
- id: dock-worker_d_021
|
||||
text: "Night crew's here. We're done."
|
||||
role: dock-worker
|
||||
access: [public, peer]
|
||||
@@ -276,7 +276,7 @@ lines:
|
||||
# GENERATION PASS — ambient variants
|
||||
# ==========================================
|
||||
|
||||
- id: the-terminal_d_221
|
||||
- id: dock-worker_d_022
|
||||
text: "Early start. Bay two's already loaded."
|
||||
role: dock-worker
|
||||
access: [public, peer]
|
||||
@@ -286,7 +286,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [dock-worker, greeting, generation-pass]
|
||||
|
||||
- id: the-terminal_d_222
|
||||
- id: dock-worker_d_023
|
||||
text: "Air recyclers are loud this morning. Must be the outer ring issue again."
|
||||
role: dock-worker
|
||||
access: [public, peer]
|
||||
@@ -296,7 +296,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [dock-worker, station, generation-pass]
|
||||
|
||||
- id: the-terminal_d_223
|
||||
- id: dock-worker_d_024
|
||||
text: "Pael's in early. Something must be broken in B-7 again."
|
||||
role: dock-worker
|
||||
access: [peer]
|
||||
@@ -306,7 +306,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [dock-worker, pael, generation-pass]
|
||||
|
||||
- id: the-terminal_d_224
|
||||
- id: dock-worker_d_025
|
||||
text: "Running light today. Maybe I'll actually make it to the Last Shift before close."
|
||||
role: dock-worker
|
||||
access: [public, peer]
|
||||
@@ -316,7 +316,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [dock-worker, social, generation-pass]
|
||||
|
||||
- id: the-terminal_d_225
|
||||
- id: dock-worker_d_026
|
||||
text: "Bay three's got a scan hold. Not my problem. Not my bay."
|
||||
role: dock-worker
|
||||
access: [public, peer]
|
||||
@@ -326,7 +326,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [dock-worker, scan-hold, generation-pass]
|
||||
|
||||
- id: the-terminal_d_226
|
||||
- id: dock-worker_d_027
|
||||
text: "New hire's back. Asking questions again. Good kid."
|
||||
role: dock-worker
|
||||
access: [peer]
|
||||
@@ -336,7 +336,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [dock-worker, new-hire, generation-pass]
|
||||
|
||||
- id: the-terminal_d_227
|
||||
- id: dock-worker_d_028
|
||||
text: "Handoff's in ten. Don't disappear on me."
|
||||
role: dock-worker
|
||||
access: [peer]
|
||||
@@ -346,7 +346,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [dock-worker, handoff, generation-pass]
|
||||
|
||||
- id: the-terminal_d_228
|
||||
- id: dock-worker_d_029
|
||||
text: "Span gate cycled early. All inbounds are going to be late. Plan around it."
|
||||
role: dock-worker
|
||||
access: [public, peer]
|
||||
@@ -356,7 +356,7 @@ lines:
|
||||
mood: [suspicious]
|
||||
tags: [dock-worker, span-gate, generation-pass]
|
||||
|
||||
- id: the-terminal_d_229
|
||||
- id: dock-worker_d_030
|
||||
text: "Linn's not in today. We're running two short on bay six."
|
||||
role: dock-worker
|
||||
access: [public, peer]
|
||||
@@ -366,7 +366,7 @@ lines:
|
||||
mood: [frustrated]
|
||||
tags: [dock-worker, staffing, generation-pass]
|
||||
|
||||
- id: the-terminal_d_230
|
||||
- id: dock-worker_d_031
|
||||
text: "Lunch is in the break room. Don't touch the one with Pael's name on it. He will notice."
|
||||
role: dock-worker
|
||||
access: [public, peer]
|
||||
@@ -376,7 +376,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [dock-worker, pael, humor, generation-pass]
|
||||
|
||||
- id: the-terminal_d_231
|
||||
- id: dock-worker_d_032
|
||||
text: "Overtime's posted if you want it. Bay eight, 1800 to 2200."
|
||||
role: dock-worker
|
||||
access: [public, peer]
|
||||
@@ -386,7 +386,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [dock-worker, overtime, generation-pass]
|
||||
|
||||
- id: the-terminal_d_232
|
||||
- id: dock-worker_d_033
|
||||
text: "Off on time today. First time this cycle."
|
||||
role: dock-worker
|
||||
access: [public, peer]
|
||||
|
||||
@@ -12,7 +12,7 @@ lines:
|
||||
# GREETINGS — Phase 1: Comfort (0-10 min)
|
||||
# ========================================
|
||||
|
||||
- id: the-terminal_d_001
|
||||
- id: kael-davan_d_034
|
||||
text: "There you are. Good — I was starting to wonder."
|
||||
role: dock-worker
|
||||
access: [insider, peer]
|
||||
@@ -22,7 +22,7 @@ lines:
|
||||
topic: [colleague]
|
||||
tags: [kael, greeting, phase-1]
|
||||
|
||||
- id: the-terminal_d_002
|
||||
- id: kael-davan_d_035
|
||||
text: "Morning. Freight's stacked clean today. Should be a smooth one."
|
||||
role: dock-worker
|
||||
access: [insider, peer]
|
||||
@@ -32,7 +32,7 @@ lines:
|
||||
topic: [routine]
|
||||
tags: [kael, greeting, phase-1]
|
||||
|
||||
- id: the-terminal_d_003
|
||||
- id: kael-davan_d_036
|
||||
text: "Voss has the schedule posted already. Keen today. That's suspicious."
|
||||
role: dock-worker
|
||||
access: [insider, peer]
|
||||
@@ -42,7 +42,7 @@ lines:
|
||||
topic: [colleague, routine]
|
||||
tags: [kael, greeting, humor]
|
||||
|
||||
- id: the-terminal_d_004
|
||||
- id: kael-davan_d_037
|
||||
text: "Hey. Grab a coffee before Voss finds something to complain about."
|
||||
role: dock-worker
|
||||
access: [insider, peer]
|
||||
@@ -52,7 +52,7 @@ lines:
|
||||
topic: [colleague]
|
||||
tags: [kael, greeting, phase-1]
|
||||
|
||||
- id: the-terminal_d_005
|
||||
- id: kael-davan_d_038
|
||||
text: "You look like you slept about as well as I did."
|
||||
role: dock-worker
|
||||
access: [peer]
|
||||
@@ -63,7 +63,7 @@ lines:
|
||||
tags: [kael, greeting, casual]
|
||||
|
||||
# Public greeting for detective / outsiders
|
||||
- id: the-terminal_d_006
|
||||
- id: kael-davan_d_039
|
||||
text: "Can I help you? If you're looking for the supervisor, Voss is by the schedule board."
|
||||
role: dock-worker
|
||||
access: [public, authority]
|
||||
@@ -77,7 +77,7 @@ lines:
|
||||
# SHIFT TALK — routine dialogue
|
||||
# ========================================
|
||||
|
||||
- id: the-terminal_d_007
|
||||
- id: kael-davan_d_040
|
||||
text: "Bay three's running behind. If we don't clear it by shift end, Voss is going to lose it."
|
||||
role: dock-worker
|
||||
access: [insider, peer]
|
||||
@@ -87,7 +87,7 @@ lines:
|
||||
topic: [routine, cargo]
|
||||
tags: [kael, operational]
|
||||
|
||||
- id: the-terminal_d_008
|
||||
- id: kael-davan_d_041
|
||||
text: "Manifest says this one's forty kilos over. Logging error, probably."
|
||||
role: dock-worker
|
||||
access: [public, peer]
|
||||
@@ -100,7 +100,7 @@ lines:
|
||||
fact_id: investigation.manifest_discrepancy
|
||||
confidence: suspects
|
||||
|
||||
- id: the-terminal_d_009
|
||||
- id: kael-davan_d_042
|
||||
text: "Night shift left the dock in decent shape for once. Miracles happen."
|
||||
role: dock-worker
|
||||
access: [insider, peer]
|
||||
@@ -110,7 +110,7 @@ lines:
|
||||
topic: [routine]
|
||||
tags: [kael, humor]
|
||||
|
||||
- id: the-terminal_d_010
|
||||
- id: kael-davan_d_043
|
||||
text: "Loading arm three's been grinding all week. Someone should file a maintenance ticket."
|
||||
role: dock-worker
|
||||
access: [public, peer]
|
||||
@@ -120,7 +120,7 @@ lines:
|
||||
topic: [routine]
|
||||
tags: [kael, environmental]
|
||||
|
||||
- id: the-terminal_d_011
|
||||
- id: kael-davan_d_044
|
||||
text: "Maret rerouted the bay four queue. Tight schedule today."
|
||||
role: dock-worker
|
||||
access: [insider, peer]
|
||||
@@ -130,7 +130,7 @@ lines:
|
||||
topic: [cargo, colleague]
|
||||
tags: [kael, operational, maret]
|
||||
|
||||
- id: the-terminal_d_012
|
||||
- id: kael-davan_d_045
|
||||
text: "Shift transition in twenty. After that it's Drin's problem."
|
||||
role: dock-worker
|
||||
access: [insider, peer]
|
||||
@@ -140,7 +140,7 @@ lines:
|
||||
topic: [routine]
|
||||
tags: [kael, operational]
|
||||
|
||||
- id: the-terminal_d_013
|
||||
- id: kael-davan_d_046
|
||||
text: "Naia's got a school event tonight. I'm heading home straight after shift."
|
||||
role: dock-worker
|
||||
access: [insider, peer]
|
||||
@@ -150,7 +150,7 @@ lines:
|
||||
topic: [personal]
|
||||
tags: [kael, naia-reference, casual]
|
||||
|
||||
- id: the-terminal_d_014
|
||||
- id: kael-davan_d_047
|
||||
text: "New hire started on bay seven. Keen. Reminds me of my first week."
|
||||
role: dock-worker
|
||||
access: [public, peer]
|
||||
@@ -164,7 +164,7 @@ lines:
|
||||
# RING COORDINATION — insider, real trust
|
||||
# ========================================
|
||||
|
||||
- id: the-terminal_d_015
|
||||
- id: kael-davan_d_048
|
||||
text: "Container 4471 is flagged. I'll reroute it during shift transition."
|
||||
role: dock-worker
|
||||
access: [insider]
|
||||
@@ -174,7 +174,7 @@ lines:
|
||||
topic: [cargo]
|
||||
tags: [kael, ring-ops, operational, phase-2]
|
||||
|
||||
- id: the-terminal_d_016
|
||||
- id: kael-davan_d_049
|
||||
text: "Voss kept the window clear. Twenty minutes, maybe twenty-five."
|
||||
role: dock-worker
|
||||
access: [insider]
|
||||
@@ -184,7 +184,7 @@ lines:
|
||||
topic: [cargo, routine]
|
||||
tags: [kael, ring-ops, operational, phase-2]
|
||||
|
||||
- id: the-terminal_d_017
|
||||
- id: kael-davan_d_050
|
||||
text: "Renn's on the corridor side. Package moves when I give the signal."
|
||||
role: dock-worker
|
||||
access: [insider]
|
||||
@@ -194,7 +194,7 @@ lines:
|
||||
topic: [cargo]
|
||||
tags: [kael, ring-ops, renn, phase-2]
|
||||
|
||||
- id: the-terminal_d_018
|
||||
- id: kael-davan_d_051
|
||||
text: "Manifest's clean on my end. Maret processed it without questions."
|
||||
role: dock-worker
|
||||
access: [insider]
|
||||
@@ -204,7 +204,7 @@ lines:
|
||||
topic: [cargo]
|
||||
tags: [kael, ring-ops, maret, phase-2]
|
||||
|
||||
- id: the-terminal_d_019
|
||||
- id: kael-davan_d_052
|
||||
text: "Nils wants higher volume next cycle. I told him we're already tight."
|
||||
role: dock-worker
|
||||
access: [insider]
|
||||
@@ -214,7 +214,7 @@ lines:
|
||||
topic: [danger, trust]
|
||||
tags: [kael, ring-ops, nils, tension, phase-2]
|
||||
|
||||
- id: the-terminal_d_020
|
||||
- id: kael-davan_d_053
|
||||
text: "Drin waved through bay four without checking. At least he's reliable."
|
||||
role: dock-worker
|
||||
access: [insider]
|
||||
@@ -227,7 +227,7 @@ lines:
|
||||
fact_id: investigation.drin_inspection_pattern
|
||||
confidence: knows_details
|
||||
|
||||
- id: the-terminal_d_021
|
||||
- id: kael-davan_d_054
|
||||
text: "Keep an eye on Maret. She's been looking at the manifests longer than usual."
|
||||
role: dock-worker
|
||||
access: [insider]
|
||||
@@ -237,7 +237,7 @@ lines:
|
||||
topic: [colleague, danger]
|
||||
tags: [kael, ring-ops, maret, caution, phase-2]
|
||||
|
||||
- id: the-terminal_d_022
|
||||
- id: kael-davan_d_055
|
||||
text: "There's a sealed one in the batch tonight. Nils says components. Don't open it."
|
||||
role: dock-worker
|
||||
access: [insider]
|
||||
@@ -251,7 +251,7 @@ lines:
|
||||
# TRUST-GATED — deep trust, secret tier
|
||||
# ========================================
|
||||
|
||||
- id: the-terminal_d_023
|
||||
- id: kael-davan_d_056
|
||||
text: "Some days I wonder how long we can keep this up."
|
||||
role: dock-worker
|
||||
access: [insider]
|
||||
@@ -261,7 +261,7 @@ lines:
|
||||
topic: [trust, personal]
|
||||
tags: [kael, vulnerability, friend-arc]
|
||||
|
||||
- id: the-terminal_d_024
|
||||
- id: kael-davan_d_057
|
||||
text: "Naia asked me again last night. Where I go. Why I'm late. I hate lying to her."
|
||||
role: dock-worker
|
||||
access: [insider]
|
||||
@@ -271,7 +271,7 @@ lines:
|
||||
topic: [personal, trust]
|
||||
tags: [kael, naia-reference, vulnerability]
|
||||
|
||||
- id: the-terminal_d_025
|
||||
- id: kael-davan_d_058
|
||||
text: "If something happens to me — look out for Naia. Promise me that."
|
||||
role: dock-worker
|
||||
access: [insider]
|
||||
@@ -285,7 +285,7 @@ lines:
|
||||
# UNPROMPTED — Kael volunteers or warns
|
||||
# ========================================
|
||||
|
||||
- id: the-terminal_d_026
|
||||
- id: kael-davan_d_059
|
||||
text: "Heads up — Voss is in a mood. Keep your head down this shift."
|
||||
role: dock-worker
|
||||
access: [insider, peer]
|
||||
@@ -295,7 +295,7 @@ lines:
|
||||
topic: [colleague, danger]
|
||||
tags: [kael, unprompted, warning]
|
||||
|
||||
- id: the-terminal_d_027
|
||||
- id: kael-davan_d_060
|
||||
text: "I'm buying at Lera's after shift. You're coming. No arguments."
|
||||
role: dock-worker
|
||||
access: [insider, peer]
|
||||
@@ -305,7 +305,7 @@ lines:
|
||||
topic: [personal, colleague]
|
||||
tags: [kael, unprompted, social]
|
||||
|
||||
- id: the-terminal_d_028
|
||||
- id: kael-davan_d_061
|
||||
text: "Torek's been running his mouth at the bar. You might want to have a word."
|
||||
role: dock-worker
|
||||
access: [insider]
|
||||
@@ -319,7 +319,7 @@ lines:
|
||||
# DEFLECTION — Phase 2-4: Post-contradiction
|
||||
# ========================================
|
||||
|
||||
- id: the-terminal_d_029
|
||||
- id: kael-davan_d_062
|
||||
text: "It's nothing. Just work stuff."
|
||||
role: dock-worker
|
||||
access: [insider, peer]
|
||||
@@ -329,7 +329,7 @@ lines:
|
||||
topic: [personal]
|
||||
tags: [kael, deflection, phase-4]
|
||||
|
||||
- id: the-terminal_d_030
|
||||
- id: kael-davan_d_063
|
||||
text: "Don't worry about it. I've got it handled."
|
||||
role: dock-worker
|
||||
access: [insider, peer]
|
||||
@@ -339,7 +339,7 @@ lines:
|
||||
topic: [personal, trust]
|
||||
tags: [kael, deflection, phase-4]
|
||||
|
||||
- id: the-terminal_d_031
|
||||
- id: kael-davan_d_064
|
||||
text: "I was checking on a maintenance issue. That's all."
|
||||
role: dock-worker
|
||||
access: [insider, peer]
|
||||
@@ -349,7 +349,7 @@ lines:
|
||||
topic: [routine]
|
||||
tags: [kael, deflection, lying, phase-4]
|
||||
|
||||
- id: the-terminal_d_032
|
||||
- id: kael-davan_d_065
|
||||
text: "You're reading too much into it. Come on — shift's starting."
|
||||
role: dock-worker
|
||||
access: [insider, peer]
|
||||
@@ -359,7 +359,7 @@ lines:
|
||||
topic: [routine, trust]
|
||||
tags: [kael, deflection, subject-change, phase-4]
|
||||
|
||||
- id: the-terminal_d_033
|
||||
- id: kael-davan_d_066
|
||||
text: "Can we not do this here? Voss is watching."
|
||||
role: dock-worker
|
||||
access: [insider]
|
||||
@@ -374,7 +374,7 @@ lines:
|
||||
# ========================================
|
||||
|
||||
# Path A: Gentle confrontation
|
||||
- id: the-terminal_d_034
|
||||
- id: kael-davan_d_067
|
||||
text: "I... yeah. I need to tell you something. But not here."
|
||||
role: dock-worker
|
||||
access: [insider]
|
||||
@@ -385,7 +385,7 @@ lines:
|
||||
tags: [kael, confession-path, phase-5]
|
||||
|
||||
# Path B: Harsh pressure
|
||||
- id: the-terminal_d_035
|
||||
- id: kael-davan_d_068
|
||||
text: "You want to report me? Go ahead. See what Nils does to both of us."
|
||||
role: dock-worker
|
||||
access: [insider]
|
||||
@@ -396,7 +396,7 @@ lines:
|
||||
tags: [kael, hostile-path, phase-5]
|
||||
|
||||
# If smuggler pushes further (gentle)
|
||||
- id: the-terminal_d_036
|
||||
- id: kael-davan_d_069
|
||||
text: "Naia can't keep living like this. Neither can I. I'm looking for a way out."
|
||||
role: dock-worker
|
||||
access: [insider]
|
||||
@@ -407,7 +407,7 @@ lines:
|
||||
tags: [kael, confession, naia-reference, phase-5]
|
||||
|
||||
# If detective confronts (authority path)
|
||||
- id: the-terminal_d_037
|
||||
- id: kael-davan_d_070
|
||||
text: "I don't know what you're talking about. I load containers. That's my job."
|
||||
role: dock-worker
|
||||
access: [public, authority]
|
||||
@@ -416,7 +416,7 @@ lines:
|
||||
topic: [routine]
|
||||
tags: [kael, deflection, detective-path]
|
||||
|
||||
- id: the-terminal_d_038
|
||||
- id: kael-davan_d_071
|
||||
text: "You want to ask questions, talk to Voss. He runs the schedule."
|
||||
role: dock-worker
|
||||
access: [public, authority]
|
||||
@@ -432,7 +432,7 @@ lines:
|
||||
# ========================================
|
||||
|
||||
# first_meeting: InteractionMemory.count == 0. RelationshipState: Unknown → Public access only.
|
||||
- id: the-terminal_d_050
|
||||
- id: kael-davan_d_072
|
||||
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]
|
||||
@@ -444,7 +444,7 @@ lines:
|
||||
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
|
||||
- id: kael-davan_d_073
|
||||
text: "You're back. Good — bay three's been quiet. I'll fill you in."
|
||||
role: dock-worker
|
||||
access: [peer, insider]
|
||||
@@ -456,7 +456,7 @@ lines:
|
||||
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
|
||||
- id: kael-davan_d_074
|
||||
text: "Back again. I'm on shift — if you have questions, make them quick."
|
||||
role: dock-worker
|
||||
access: [authority]
|
||||
@@ -468,7 +468,7 @@ lines:
|
||||
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
|
||||
- id: kael-davan_d_075
|
||||
text: "Didn't expect you today."
|
||||
role: dock-worker
|
||||
access: [peer, insider]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Dialogue: maintenance-tech at The Terminal
|
||||
# NPC: Pael Varren (Tier 3, NOBODY/CIVILIAN)
|
||||
# Voice: tired-competent, minimal, dry. Knows the infrastructure. Knows nothing else.
|
||||
# KEY LINE: the-terminal_d_040 — sounds like surveillance knowledge, is a maintenance complaint
|
||||
# KEY LINE: maintenance-tech_d_001 — sounds like surveillance knowledge, is a maintenance complaint
|
||||
# Ticket: #307 | Sprint: 12
|
||||
|
||||
location: the-terminal
|
||||
@@ -14,7 +14,7 @@ lines:
|
||||
# Actually: the B-7 seal fails when people use the corridor too much. Pael notices load.
|
||||
# ==========================================
|
||||
|
||||
- id: the-terminal_d_040
|
||||
- id: maintenance-tech_d_001
|
||||
text: "B-7's had three seal failures this cycle. Someone's been through there more than they should."
|
||||
role: maintenance-tech
|
||||
access: [public]
|
||||
@@ -35,7 +35,7 @@ lines:
|
||||
# ROUTINE LINES
|
||||
# ==========================================
|
||||
|
||||
- id: the-terminal_d_041
|
||||
- id: maintenance-tech_d_002
|
||||
text: "Air recycler's running hot again. Third deferred ticket this week."
|
||||
role: maintenance-tech
|
||||
access: [public]
|
||||
@@ -45,7 +45,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [pael, atmospheric]
|
||||
|
||||
- id: the-terminal_d_042
|
||||
- id: maintenance-tech_d_003
|
||||
text: "Loading arm three's been on my list for a month. Management keeps deferring it."
|
||||
role: maintenance-tech
|
||||
access: [public]
|
||||
@@ -55,7 +55,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [pael, atmospheric]
|
||||
|
||||
- id: the-terminal_d_043
|
||||
- id: maintenance-tech_d_004
|
||||
text: "If the gate tone shifts, stay clear of bay six. She needs recalibration."
|
||||
role: maintenance-tech
|
||||
access: [public]
|
||||
@@ -65,7 +65,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [pael, warning, helpful]
|
||||
|
||||
- id: the-terminal_d_044
|
||||
- id: maintenance-tech_d_005
|
||||
text: "Not me. I just fix them."
|
||||
role: maintenance-tech
|
||||
access: [public]
|
||||
@@ -75,7 +75,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [pael, response]
|
||||
|
||||
- id: the-terminal_d_045
|
||||
- id: maintenance-tech_d_006
|
||||
text: "Gate hum's been three cycles off-spec. Nobody cares until it stops completely."
|
||||
role: maintenance-tech
|
||||
access: [public]
|
||||
|
||||
@@ -12,7 +12,7 @@ lines:
|
||||
# GREETINGS — early days
|
||||
# ==========================================
|
||||
|
||||
- id: the-terminal_d_400
|
||||
- id: new-hire_d_001
|
||||
text: "Still figuring out the bay numbering. Six and eight are in the wrong order."
|
||||
role: new-hire
|
||||
access: [public, peer]
|
||||
@@ -22,7 +22,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [new-hire, greeting, orientation]
|
||||
|
||||
- id: the-terminal_d_401
|
||||
- id: new-hire_d_002
|
||||
text: "Voss told me to check in with you. Is that the standard thing?"
|
||||
role: new-hire
|
||||
access: [public, peer]
|
||||
@@ -32,7 +32,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [new-hire, greeting, voss]
|
||||
|
||||
- id: the-terminal_d_402
|
||||
- id: new-hire_d_003
|
||||
text: "First full cycle on the floor. Trying to stay out of the way."
|
||||
role: new-hire
|
||||
access: [public, peer]
|
||||
@@ -46,7 +46,7 @@ lines:
|
||||
# FRESH OBSERVATIONS — the dangerous innocence
|
||||
# ==========================================
|
||||
|
||||
- id: the-terminal_d_403
|
||||
- id: new-hire_d_004
|
||||
text: "Does bay four usually get held that long? The other bays turned over twice already."
|
||||
role: new-hire
|
||||
access: [public, peer]
|
||||
@@ -59,7 +59,7 @@ lines:
|
||||
smuggler: "New eyes on bay four. He doesn't know what he's looking at. But he noticed."
|
||||
detective: "New hire asking exactly the right question without knowing it."
|
||||
|
||||
- id: the-terminal_d_404
|
||||
- id: new-hire_d_005
|
||||
text: "I saw someone go into C-4 last night without a scan log. Is that normal?"
|
||||
role: new-hire
|
||||
access: [public, peer]
|
||||
@@ -76,7 +76,7 @@ lines:
|
||||
confidence: suspects
|
||||
notes: "High-value inadvertent witness line. Operationally dangerous for the ring."
|
||||
|
||||
- id: the-terminal_d_405
|
||||
- id: new-hire_d_006
|
||||
text: "Why does the transition window only get twenty minutes? The stations I trained at ran thirty."
|
||||
role: new-hire
|
||||
access: [public, peer]
|
||||
@@ -89,7 +89,7 @@ lines:
|
||||
smuggler: "He's asking about the window. Doesn't know why it matters. Answer carefully."
|
||||
detective: "New hire confirms the window length is unusually short. File that."
|
||||
|
||||
- id: the-terminal_d_406
|
||||
- id: new-hire_d_007
|
||||
text: "Do containers usually get rerouted mid-hold? One went out through bay four while I thought it was still held."
|
||||
role: new-hire
|
||||
access: [public, peer]
|
||||
@@ -105,7 +105,7 @@ lines:
|
||||
fact_id: investigation.manifest_discrepancy
|
||||
confidence: suspects
|
||||
|
||||
- id: the-terminal_d_407
|
||||
- id: new-hire_d_008
|
||||
text: "Maintenance corridor access is just off C-4, right? I took the wrong turn twice."
|
||||
role: new-hire
|
||||
access: [public, peer]
|
||||
@@ -122,7 +122,7 @@ lines:
|
||||
# LEARNING / QUESTIONS
|
||||
# ==========================================
|
||||
|
||||
- id: the-terminal_d_408
|
||||
- id: new-hire_d_009
|
||||
text: "What's the Commission kiosk actually for? Nobody seems to use it."
|
||||
role: new-hire
|
||||
access: [public, peer]
|
||||
@@ -132,7 +132,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [new-hire, commission, curiosity]
|
||||
|
||||
- id: the-terminal_d_409
|
||||
- id: new-hire_d_010
|
||||
text: "Maret seems stressed. Is that always the case or is something going on?"
|
||||
role: new-hire
|
||||
access: [peer]
|
||||
@@ -145,7 +145,7 @@ lines:
|
||||
smuggler: "He noticed Maret. Good that he's asking, not reporting. Manage this."
|
||||
detective: "New hire reads Maret's stress without context. Confirms it's visible."
|
||||
|
||||
- id: the-terminal_d_410
|
||||
- id: new-hire_d_011
|
||||
text: "Voss seems really serious. Is he always like that?"
|
||||
role: new-hire
|
||||
access: [public, peer]
|
||||
@@ -155,7 +155,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [new-hire, voss, curiosity]
|
||||
|
||||
- id: the-terminal_d_411
|
||||
- id: new-hire_d_012
|
||||
text: "How long before Voss trusts you to run a bay on your own?"
|
||||
role: new-hire
|
||||
access: [peer]
|
||||
@@ -169,7 +169,7 @@ lines:
|
||||
# SIGN-OFF
|
||||
# ==========================================
|
||||
|
||||
- id: the-terminal_d_412
|
||||
- id: new-hire_d_013
|
||||
text: "That was a full shift. I'll have the bay numbers down by next cycle."
|
||||
role: new-hire
|
||||
access: [public, peer]
|
||||
@@ -179,7 +179,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [new-hire, closing]
|
||||
|
||||
- id: the-terminal_d_413
|
||||
- id: new-hire_d_014
|
||||
text: "Good day. Lot to learn. But good."
|
||||
role: new-hire
|
||||
access: [public, peer]
|
||||
@@ -193,7 +193,7 @@ lines:
|
||||
# GENERATION PASS — ambient variants
|
||||
# ==========================================
|
||||
|
||||
- id: the-terminal_d_414
|
||||
- id: new-hire_d_015
|
||||
text: "Is there a trick to the manifest terminal or does it always lag like this?"
|
||||
role: new-hire
|
||||
access: [public, peer]
|
||||
@@ -203,7 +203,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [new-hire, orientation, generation-pass]
|
||||
|
||||
- id: the-terminal_d_415
|
||||
- id: new-hire_d_016
|
||||
text: "How long has Maret been here? She seems like she knows everything."
|
||||
role: new-hire
|
||||
access: [peer]
|
||||
@@ -213,7 +213,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [new-hire, maret, curiosity, generation-pass]
|
||||
|
||||
- id: the-terminal_d_416
|
||||
- id: new-hire_d_017
|
||||
text: "Nobody told me about the span gate frequency. I kept thinking something was wrong."
|
||||
role: new-hire
|
||||
access: [public, peer]
|
||||
@@ -223,7 +223,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [new-hire, orientation, generation-pass]
|
||||
|
||||
- id: the-terminal_d_417
|
||||
- id: new-hire_d_018
|
||||
text: "The Last Shift — is that actually a bar? I haven't been yet."
|
||||
role: new-hire
|
||||
access: [public, peer]
|
||||
@@ -233,7 +233,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [new-hire, social, generation-pass]
|
||||
|
||||
- id: the-terminal_d_418
|
||||
- id: new-hire_d_019
|
||||
text: "I thought the scanner on bay three was broken. Turns out you have to hold the badge longer."
|
||||
role: new-hire
|
||||
access: [public, peer]
|
||||
@@ -243,7 +243,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [new-hire, orientation, humor, generation-pass]
|
||||
|
||||
- id: the-terminal_d_419
|
||||
- id: new-hire_d_020
|
||||
text: "Who's Drin? I keep hearing the name but haven't met them."
|
||||
role: new-hire
|
||||
access: [public, peer]
|
||||
@@ -253,7 +253,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [new-hire, drin, curiosity, generation-pass]
|
||||
|
||||
- id: the-terminal_d_420
|
||||
- id: new-hire_d_021
|
||||
text: "Three cycles in and I still can't tell when Voss is satisfied. Is there a tell?"
|
||||
role: new-hire
|
||||
access: [peer]
|
||||
|
||||
@@ -17,7 +17,7 @@ lines:
|
||||
# Second playthrough: "That's me."
|
||||
# ========================================
|
||||
|
||||
- id: the-terminal_d_044
|
||||
- id: pc-detective_d_004
|
||||
text: "Good morning. Commission Liaison Office. I'm conducting a routine compliance review."
|
||||
role: commission-liaison
|
||||
access: [public]
|
||||
@@ -27,7 +27,7 @@ lines:
|
||||
topic: [routine]
|
||||
tags: [pc-detective, greeting, public-tier]
|
||||
|
||||
- id: the-terminal_d_045
|
||||
- id: pc-detective_d_005
|
||||
text: "Standard review. Manifest reconciliation, scheduling compliance. Nothing unusual."
|
||||
role: commission-liaison
|
||||
access: [public]
|
||||
@@ -36,7 +36,7 @@ lines:
|
||||
topic: [routine]
|
||||
tags: [pc-detective, purpose-question, public-tier]
|
||||
|
||||
- id: the-terminal_d_046
|
||||
- id: pc-detective_d_006
|
||||
text: "Only been here a few days. Seems like a well-run operation."
|
||||
role: commission-liaison
|
||||
access: [public]
|
||||
@@ -45,7 +45,7 @@ lines:
|
||||
topic: [routine]
|
||||
tags: [pc-detective, district-question, public-tier]
|
||||
|
||||
- id: the-terminal_d_047
|
||||
- id: pc-detective_d_007
|
||||
text: "Ms. Venn? Colleague. We've worked in the same division."
|
||||
role: commission-liaison
|
||||
access: [public]
|
||||
@@ -54,7 +54,7 @@ lines:
|
||||
topic: [colleague]
|
||||
tags: [pc-detective, sera-question, public-tier]
|
||||
|
||||
- id: the-terminal_d_048
|
||||
- id: pc-detective_d_008
|
||||
text: "I appreciate the interest, but I'm sure you have cargo to route. I won't keep you."
|
||||
role: commission-liaison
|
||||
access: [public, authority]
|
||||
|
||||
@@ -17,7 +17,7 @@ lines:
|
||||
# Second playthrough: "That's me."
|
||||
# ========================================
|
||||
|
||||
- id: the-terminal_d_039
|
||||
- id: pc-smuggler_d_004
|
||||
text: "Morning. Can I help you with something?"
|
||||
role: dock-worker
|
||||
access: [public]
|
||||
@@ -27,7 +27,7 @@ lines:
|
||||
topic: [routine]
|
||||
tags: [pc-smuggler, greeting, public-tier]
|
||||
|
||||
- id: the-terminal_d_040
|
||||
- id: pc-smuggler_d_005
|
||||
text: "Cargo routing. Containers come in, I process them out. Standard stuff."
|
||||
role: dock-worker
|
||||
access: [public, authority]
|
||||
@@ -36,7 +36,7 @@ lines:
|
||||
topic: [routine, cargo]
|
||||
tags: [pc-smuggler, work-question, public-tier]
|
||||
|
||||
- id: the-terminal_d_041
|
||||
- id: pc-smuggler_d_006
|
||||
text: "It's a freight hub. Busy during shifts, quiet after. Same as anywhere."
|
||||
role: dock-worker
|
||||
access: [public]
|
||||
@@ -45,7 +45,7 @@ lines:
|
||||
topic: [routine]
|
||||
tags: [pc-smuggler, hub-question, public-tier]
|
||||
|
||||
- id: the-terminal_d_042
|
||||
- id: pc-smuggler_d_007
|
||||
text: "Good crew. We do our shifts, go home. Nothing exciting."
|
||||
role: dock-worker
|
||||
access: [public, authority]
|
||||
@@ -54,7 +54,7 @@ lines:
|
||||
topic: [colleague, routine]
|
||||
tags: [pc-smuggler, colleagues-question, public-tier]
|
||||
|
||||
- id: the-terminal_d_043
|
||||
- id: pc-smuggler_d_008
|
||||
text: "Look, I just move containers. You'd want to talk to Voss about scheduling."
|
||||
role: dock-worker
|
||||
access: [authority]
|
||||
|
||||
@@ -13,7 +13,7 @@ lines:
|
||||
# GREETINGS — scheduler's opening read
|
||||
# ==========================================
|
||||
|
||||
- id: the-terminal_d_300
|
||||
- id: scheduler_d_001
|
||||
text: "Manifest's current. Bay assignments are posted."
|
||||
role: scheduler
|
||||
access: [public, peer]
|
||||
@@ -23,7 +23,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [maret, procedural, greeting]
|
||||
|
||||
- id: the-terminal_d_301
|
||||
- id: scheduler_d_002
|
||||
text: "Container 4471 is still in C-4. Scheduled for bay six by 1400. Check with Voss if the hold's lifted."
|
||||
role: scheduler
|
||||
access: [public, peer]
|
||||
@@ -39,7 +39,7 @@ lines:
|
||||
fact_id: investigation.manifest_discrepancy
|
||||
confidence: suspects
|
||||
|
||||
- id: the-terminal_d_302
|
||||
- id: scheduler_d_003
|
||||
text: "Three inbounds today. All verified against the manifest. No gaps so far."
|
||||
role: scheduler
|
||||
access: [public, peer]
|
||||
@@ -53,7 +53,7 @@ lines:
|
||||
# DISCREPANCY PATTERN — the core arc
|
||||
# ==========================================
|
||||
|
||||
- id: the-terminal_d_303
|
||||
- id: scheduler_d_004
|
||||
text: "Container 4474 shows a forty-seven kilo discrepancy. I've flagged it twice."
|
||||
role: scheduler
|
||||
access: [public, peer]
|
||||
@@ -69,7 +69,7 @@ lines:
|
||||
fact_id: investigation.oversight_gap_pattern
|
||||
confidence: suspects
|
||||
|
||||
- id: the-terminal_d_304
|
||||
- id: scheduler_d_005
|
||||
text: "It was logged as administrative. I don't know what that means for a weight discrepancy."
|
||||
role: scheduler
|
||||
access: [public, peer]
|
||||
@@ -82,7 +82,7 @@ lines:
|
||||
smuggler: "She doesn't know what 'administrative' means either. Voss's answer covered nothing."
|
||||
detective: "Maret knows the answer she got wasn't a real answer. She's still sitting with that."
|
||||
|
||||
- id: the-terminal_d_305
|
||||
- id: scheduler_d_006
|
||||
text: "The transition window anomalies cluster at shift change. I've run the numbers four times. It's not random."
|
||||
role: scheduler
|
||||
access: [peer]
|
||||
@@ -98,7 +98,7 @@ lines:
|
||||
fact_id: investigation.manifest_discrepancy
|
||||
confidence: knows_details
|
||||
|
||||
- id: the-terminal_d_306
|
||||
- id: scheduler_d_007
|
||||
text: "Three separate manifest entries show weight variations under ten kilos. Within rounding, technically."
|
||||
role: scheduler
|
||||
access: [peer]
|
||||
@@ -111,7 +111,7 @@ lines:
|
||||
smuggler: "She's tracking the small ones too. But 'within rounding' is what Voss told her. She's not satisfied."
|
||||
detective: "Sub-threshold variations. Small enough to dismiss individually. Maret's running aggregate analysis."
|
||||
|
||||
- id: the-terminal_d_307
|
||||
- id: scheduler_d_008
|
||||
text: "I filed a formal notice with Voss. He reviewed it and told me it was resolved. It's not resolved."
|
||||
role: scheduler
|
||||
access: [peer]
|
||||
@@ -131,7 +131,7 @@ lines:
|
||||
# ROUTINE SCHEDULING — grounded work
|
||||
# ==========================================
|
||||
|
||||
- id: the-terminal_d_308
|
||||
- id: scheduler_d_009
|
||||
text: "Bay eight's running on reduced capacity until 1700. Route inbounds through six."
|
||||
role: scheduler
|
||||
access: [public, peer]
|
||||
@@ -141,7 +141,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [maret, operational]
|
||||
|
||||
- id: the-terminal_d_309
|
||||
- id: scheduler_d_010
|
||||
text: "Drin's running the inspection on bay six this cycle. Bay four's on hold per Voss's directive."
|
||||
role: scheduler
|
||||
access: [public, peer]
|
||||
@@ -154,7 +154,7 @@ lines:
|
||||
smuggler: "Maret confirmed it: bay four is Voss's directive. Not a system hold."
|
||||
detective: "The bay four hold comes directly from Voss, not from the system. Discretionary hold."
|
||||
|
||||
- id: the-terminal_d_310
|
||||
- id: scheduler_d_011
|
||||
text: "Overtime requests go through Meridian. I can't process them on paper anymore."
|
||||
role: scheduler
|
||||
access: [public, peer]
|
||||
@@ -164,7 +164,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [maret, administrative]
|
||||
|
||||
- id: the-terminal_d_311
|
||||
- id: scheduler_d_012
|
||||
text: "Manifest update at 1300. If your containers aren't posted by then, they don't make the cycle."
|
||||
role: scheduler
|
||||
access: [public, peer]
|
||||
@@ -178,7 +178,7 @@ lines:
|
||||
# DETECTIVE ENCOUNTER — access and resistance
|
||||
# ==========================================
|
||||
|
||||
- id: the-terminal_d_312
|
||||
- id: scheduler_d_013
|
||||
text: "Commission manifest access requires a formal request through Voss. I can tell you what's on the public record."
|
||||
role: scheduler
|
||||
access: [authority]
|
||||
@@ -188,7 +188,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [maret, detective-path, procedural]
|
||||
|
||||
- id: the-terminal_d_313
|
||||
- id: scheduler_d_014
|
||||
text: "The flagged entries are in the system. They were logged as reviewed and closed. That's what I can confirm."
|
||||
role: scheduler
|
||||
access: [authority]
|
||||
@@ -201,7 +201,7 @@ lines:
|
||||
smuggler: "Never encounters this line."
|
||||
detective: "Maret confirms the flags exist in the system. And she's using careful language: 'what I can confirm.' There's more."
|
||||
|
||||
- id: the-terminal_d_314
|
||||
- id: scheduler_d_015
|
||||
text: "I'm not in a position to speak to why certain reviews were closed the way they were."
|
||||
role: scheduler
|
||||
access: [authority]
|
||||
@@ -218,7 +218,7 @@ lines:
|
||||
# ELEVATED TRUST — Maret breaks professional distance
|
||||
# ==========================================
|
||||
|
||||
- id: the-terminal_d_315
|
||||
- id: scheduler_d_016
|
||||
text: "The pattern I'm seeing doesn't make sense for equipment failure. It's too consistent."
|
||||
role: scheduler
|
||||
access: [peer, insider]
|
||||
@@ -234,7 +234,7 @@ lines:
|
||||
fact_id: investigation.manifest_discrepancy
|
||||
confidence: knows_details
|
||||
|
||||
- id: the-terminal_d_316
|
||||
- id: scheduler_d_017
|
||||
text: "I filed a second formal notice two cycles ago. It's been under review since then. By Voss."
|
||||
role: scheduler
|
||||
access: [peer]
|
||||
@@ -250,7 +250,7 @@ lines:
|
||||
fact_id: behavioral.voss_awareness
|
||||
confidence: knows_details
|
||||
|
||||
- id: the-terminal_d_317
|
||||
- id: scheduler_d_018
|
||||
text: "I don't think I should be the one telling you this."
|
||||
role: scheduler
|
||||
access: [insider]
|
||||
@@ -265,7 +265,7 @@ lines:
|
||||
# SIGN-OFF
|
||||
# ==========================================
|
||||
|
||||
- id: the-terminal_d_318
|
||||
- id: scheduler_d_019
|
||||
text: "Manifest posted for the evening shift. I'm off at 1500."
|
||||
role: scheduler
|
||||
access: [public, peer]
|
||||
@@ -275,7 +275,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [maret, closing]
|
||||
|
||||
- id: the-terminal_d_319
|
||||
- id: scheduler_d_020
|
||||
text: "Numbers don't lie. People lie about numbers."
|
||||
role: scheduler
|
||||
access: [peer]
|
||||
@@ -290,7 +290,7 @@ lines:
|
||||
# GENERATION PASS — ambient variants
|
||||
# ==========================================
|
||||
|
||||
- id: the-terminal_d_320
|
||||
- id: scheduler_d_021
|
||||
text: "Bay assignments updated as of 0900. Check before you touch anything."
|
||||
role: scheduler
|
||||
access: [public, peer]
|
||||
@@ -300,7 +300,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [maret, procedural, generation-pass]
|
||||
|
||||
- id: the-terminal_d_321
|
||||
- id: scheduler_d_022
|
||||
text: "Inbound from the Havara corridor is delayed six hours. Adjust your bay queue."
|
||||
role: scheduler
|
||||
access: [public, peer]
|
||||
@@ -310,7 +310,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [maret, operational, generation-pass]
|
||||
|
||||
- id: the-terminal_d_322
|
||||
- id: scheduler_d_023
|
||||
text: "Scan log for bay six is still open from last cycle. Close it before end of shift."
|
||||
role: scheduler
|
||||
access: [public, peer]
|
||||
@@ -320,7 +320,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [maret, procedural, generation-pass]
|
||||
|
||||
- id: the-terminal_d_323
|
||||
- id: scheduler_d_024
|
||||
text: "Overtime requests need scheduler approval now. Voss changed the process."
|
||||
role: scheduler
|
||||
access: [public, peer]
|
||||
@@ -330,7 +330,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [maret, voss, administrative, generation-pass]
|
||||
|
||||
- id: the-terminal_d_324
|
||||
- id: scheduler_d_025
|
||||
text: "If a container's not logged by 1300, it doesn't make the cycle. That's the rule."
|
||||
role: scheduler
|
||||
access: [public, peer]
|
||||
@@ -340,7 +340,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [maret, procedural, generation-pass]
|
||||
|
||||
- id: the-terminal_d_325
|
||||
- id: scheduler_d_026
|
||||
text: "End-of-cycle reconciliation is at 1430. Be available."
|
||||
role: scheduler
|
||||
access: [public, peer]
|
||||
@@ -350,7 +350,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [maret, procedural, generation-pass]
|
||||
|
||||
- id: the-terminal_d_326
|
||||
- id: scheduler_d_027
|
||||
text: "Weight tolerance is plus or minus five kilos. Anything outside that gets a flag."
|
||||
role: scheduler
|
||||
access: [public, peer]
|
||||
@@ -360,7 +360,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [maret, procedural, manifest, generation-pass]
|
||||
|
||||
- id: the-terminal_d_327
|
||||
- id: scheduler_d_028
|
||||
text: "Commission audit prep starts next cycle. Get your documentation current."
|
||||
role: scheduler
|
||||
access: [public, peer]
|
||||
|
||||
@@ -12,7 +12,7 @@ lines:
|
||||
# GREETINGS — shift arrival
|
||||
# ==========================================
|
||||
|
||||
- id: the-terminal_d_100
|
||||
- id: shift-supervisor_d_001
|
||||
text: "You're late. Don't be late."
|
||||
role: shift-supervisor
|
||||
access: [public, peer, insider]
|
||||
@@ -22,7 +22,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [voss, greeting, authority]
|
||||
|
||||
- id: the-terminal_d_101
|
||||
- id: shift-supervisor_d_002
|
||||
text: "Manifest's posted. Bay assignments are final — don't renegotiate them with me."
|
||||
role: shift-supervisor
|
||||
access: [public, peer]
|
||||
@@ -32,7 +32,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [voss, procedural]
|
||||
|
||||
- id: the-terminal_d_102
|
||||
- id: shift-supervisor_d_003
|
||||
text: "Bay 4's running a review hold. Nobody touches that freight until I say."
|
||||
role: shift-supervisor
|
||||
access: [public, peer]
|
||||
@@ -45,7 +45,7 @@ lines:
|
||||
smuggler: "Review hold on Bay 4. Voss knows the container's in the queue. Is he covering it or flagging it?"
|
||||
detective: "Supervisor flagging Bay 4 himself. Either due diligence or containment."
|
||||
|
||||
- id: the-terminal_d_103
|
||||
- id: shift-supervisor_d_004
|
||||
text: "Commission liaison's in the district this cycle. Keep the floor clean."
|
||||
role: shift-supervisor
|
||||
access: [public, peer]
|
||||
@@ -62,7 +62,7 @@ lines:
|
||||
# SHIFT AUTHORITY — routine management
|
||||
# ==========================================
|
||||
|
||||
- id: the-terminal_d_104
|
||||
- id: shift-supervisor_d_005
|
||||
text: "Drin was supposed to run the bay six inspection. Tell him I'm asking."
|
||||
role: shift-supervisor
|
||||
access: [public, peer]
|
||||
@@ -72,7 +72,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [voss, drin, authority]
|
||||
|
||||
- id: the-terminal_d_105
|
||||
- id: shift-supervisor_d_006
|
||||
text: "Transition window's twenty minutes. Not twenty-five, not thirty. Twenty."
|
||||
role: shift-supervisor
|
||||
access: [public, peer, insider]
|
||||
@@ -82,7 +82,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [voss, procedural, transition-window]
|
||||
|
||||
- id: the-terminal_d_106
|
||||
- id: shift-supervisor_d_007
|
||||
text: "Overtime requests go through the Meridian form. Don't bring them to me."
|
||||
role: shift-supervisor
|
||||
access: [public, peer]
|
||||
@@ -92,7 +92,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [voss, procedural]
|
||||
|
||||
- id: the-terminal_d_107
|
||||
- id: shift-supervisor_d_008
|
||||
text: "Bay eight's going to downtime at 1300. Plan around it."
|
||||
role: shift-supervisor
|
||||
access: [public, peer]
|
||||
@@ -102,7 +102,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [voss, operational]
|
||||
|
||||
- id: the-terminal_d_108
|
||||
- id: shift-supervisor_d_009
|
||||
text: "Maret flagged a discrepancy. I've reviewed it. It's logged. Move on."
|
||||
role: shift-supervisor
|
||||
access: [public, peer]
|
||||
@@ -118,7 +118,7 @@ lines:
|
||||
fact_id: investigation.oversight_gap_pattern
|
||||
confidence: suspects
|
||||
|
||||
- id: the-terminal_d_109
|
||||
- id: shift-supervisor_d_010
|
||||
text: "Freight's moving. That's what matters. Questions slow things down."
|
||||
role: shift-supervisor
|
||||
access: [public, peer]
|
||||
@@ -132,7 +132,7 @@ lines:
|
||||
# SCHEDULING COVER — insider, real trust
|
||||
# ==========================================
|
||||
|
||||
- id: the-terminal_d_110
|
||||
- id: shift-supervisor_d_011
|
||||
text: "Bay four is clear for transition. I've adjusted the inspection schedule."
|
||||
role: shift-supervisor
|
||||
access: [insider]
|
||||
@@ -145,7 +145,7 @@ lines:
|
||||
smuggler: "He adjusted the schedule. The window is actually clear. Voss is part of this."
|
||||
detective: "Never hears this line."
|
||||
|
||||
- id: the-terminal_d_111
|
||||
- id: shift-supervisor_d_012
|
||||
text: "Drin's on bay six. Bay four's mine tonight."
|
||||
role: shift-supervisor
|
||||
access: [insider]
|
||||
@@ -155,7 +155,7 @@ lines:
|
||||
mood: [focused]
|
||||
tags: [voss, ring-ops, scheduling-cover, drin]
|
||||
|
||||
- id: the-terminal_d_112
|
||||
- id: shift-supervisor_d_013
|
||||
text: "The hold note is administrative. Don't treat it as anything else."
|
||||
role: shift-supervisor
|
||||
access: [insider]
|
||||
@@ -170,7 +170,7 @@ lines:
|
||||
# DETECTIVE ENCOUNTER — authority tier
|
||||
# ==========================================
|
||||
|
||||
- id: the-terminal_d_113
|
||||
- id: shift-supervisor_d_014
|
||||
text: "You'll need to submit a formal access request through Commission channels for any restricted data."
|
||||
role: shift-supervisor
|
||||
access: [authority]
|
||||
@@ -180,7 +180,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [voss, detective-path, institutional-resistance]
|
||||
|
||||
- id: the-terminal_d_114
|
||||
- id: shift-supervisor_d_015
|
||||
text: "The manifest's available to any Commission officer. Ask the scheduler."
|
||||
role: shift-supervisor
|
||||
access: [authority]
|
||||
@@ -190,7 +190,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [voss, detective-path, redirect]
|
||||
|
||||
- id: the-terminal_d_115
|
||||
- id: shift-supervisor_d_016
|
||||
text: "My workers perform their duties. If there's an irregularity, it'll show in the log."
|
||||
role: shift-supervisor
|
||||
access: [authority]
|
||||
@@ -200,7 +200,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [voss, detective-path, deflection]
|
||||
|
||||
- id: the-terminal_d_116
|
||||
- id: shift-supervisor_d_017
|
||||
text: "I've been running this floor for six years. The numbers work out."
|
||||
role: shift-supervisor
|
||||
access: [authority, public]
|
||||
@@ -214,7 +214,7 @@ lines:
|
||||
# PRESSURE / CONFRONTATION
|
||||
# ==========================================
|
||||
|
||||
- id: the-terminal_d_117
|
||||
- id: shift-supervisor_d_018
|
||||
text: "Close the door."
|
||||
role: shift-supervisor
|
||||
access: [insider, peer]
|
||||
@@ -224,7 +224,7 @@ lines:
|
||||
mood: [frustrated]
|
||||
tags: [voss, confrontation, private]
|
||||
|
||||
- id: the-terminal_d_118
|
||||
- id: shift-supervisor_d_019
|
||||
text: "Maret keeps flagging the discrepancies. That needs to stop being her problem."
|
||||
role: shift-supervisor
|
||||
access: [insider]
|
||||
@@ -237,7 +237,7 @@ lines:
|
||||
fact_id: behavioral.voss_awareness
|
||||
confidence: knows_of
|
||||
|
||||
- id: the-terminal_d_119
|
||||
- id: shift-supervisor_d_020
|
||||
text: "If you've got a problem with how I run this floor, that's a conversation for another time."
|
||||
role: shift-supervisor
|
||||
access: [public, peer]
|
||||
@@ -251,7 +251,7 @@ lines:
|
||||
# SIGN-OFF / END OF SHIFT
|
||||
# ==========================================
|
||||
|
||||
- id: the-terminal_d_120
|
||||
- id: shift-supervisor_d_021
|
||||
text: "Sign-off is at 1400. Be done."
|
||||
role: shift-supervisor
|
||||
access: [public, peer]
|
||||
@@ -261,7 +261,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [voss, procedural]
|
||||
|
||||
- id: the-terminal_d_121
|
||||
- id: shift-supervisor_d_022
|
||||
text: "Night shift takes over at 2200. I don't want to hear about the handoff tomorrow."
|
||||
role: shift-supervisor
|
||||
access: [public, peer]
|
||||
@@ -271,7 +271,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [voss, procedural]
|
||||
|
||||
- id: the-terminal_d_122
|
||||
- id: shift-supervisor_d_023
|
||||
text: "Good shift. Tomorrow at 0600."
|
||||
role: shift-supervisor
|
||||
access: [public, peer, insider]
|
||||
@@ -285,7 +285,7 @@ lines:
|
||||
# GENERATION PASS — ambient variants
|
||||
# ==========================================
|
||||
|
||||
- id: the-terminal_d_123
|
||||
- id: shift-supervisor_d_024
|
||||
text: "Assignments don't change once they're posted. Read the board."
|
||||
role: shift-supervisor
|
||||
access: [public, peer]
|
||||
@@ -295,7 +295,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [voss, procedural, generation-pass]
|
||||
|
||||
- id: the-terminal_d_124
|
||||
- id: shift-supervisor_d_025
|
||||
text: "If there's a discrepancy, it goes to the scheduler. Not to me directly."
|
||||
role: shift-supervisor
|
||||
access: [public, peer]
|
||||
@@ -305,7 +305,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [voss, procedural, maret, generation-pass]
|
||||
|
||||
- id: the-terminal_d_125
|
||||
- id: shift-supervisor_d_026
|
||||
text: "Span gate's cycling at 1100. Clear the bay six access before then."
|
||||
role: shift-supervisor
|
||||
access: [public, peer]
|
||||
@@ -315,7 +315,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [voss, operational, generation-pass]
|
||||
|
||||
- id: the-terminal_d_126
|
||||
- id: shift-supervisor_d_027
|
||||
text: "I don't repeat myself on safety calls. If you missed it, ask someone who was listening."
|
||||
role: shift-supervisor
|
||||
access: [public, peer]
|
||||
@@ -325,7 +325,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [voss, authority, generation-pass]
|
||||
|
||||
- id: the-terminal_d_127
|
||||
- id: shift-supervisor_d_028
|
||||
text: "End-of-day log goes to Meridian before you leave. No exceptions."
|
||||
role: shift-supervisor
|
||||
access: [public, peer]
|
||||
@@ -335,7 +335,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [voss, procedural, generation-pass]
|
||||
|
||||
- id: the-terminal_d_128
|
||||
- id: shift-supervisor_d_029
|
||||
text: "Inbounds from Havara are on the revised schedule. Check the update."
|
||||
role: shift-supervisor
|
||||
access: [public, peer]
|
||||
@@ -345,7 +345,7 @@ lines:
|
||||
mood: [content]
|
||||
tags: [voss, operational, generation-pass]
|
||||
|
||||
- id: the-terminal_d_129
|
||||
- id: shift-supervisor_d_030
|
||||
text: "Clean floor, clean manifest. That's the job."
|
||||
role: shift-supervisor
|
||||
access: [public, peer, insider]
|
||||
|
||||
@@ -47,8 +47,8 @@ items:
|
||||
notes: >
|
||||
Physical proof of cargo routing irregularities. The manifest shows
|
||||
three containers rerouted during shift transition with weights that
|
||||
don't reconcile. Cross-ref: Kael dialogue the-terminal_d_008
|
||||
("Manifest says this one's forty kilos over") and the-terminal_d_018
|
||||
don't reconcile. Cross-ref: Kael dialogue kael-davan_d_041
|
||||
("Manifest says this one's forty kilos over") and kael-davan_d_051
|
||||
("Manifest's clean on my end").
|
||||
- fact_id: "investigation.cargo_anomaly"
|
||||
relationship: "corroborates"
|
||||
|
||||
@@ -24,7 +24,7 @@ lines:
|
||||
|
||||
# --- Arrival ---
|
||||
|
||||
- id: commission-kiosk_m_d_001
|
||||
- id: pc-detective_m_d_001
|
||||
text: "Commission terminal. Authorized access, full import logs. Let's see what the system knows."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -33,7 +33,7 @@ lines:
|
||||
situation: [arrival, routine]
|
||||
tags: [arrival, tutorial, orientation]
|
||||
|
||||
- id: commission-kiosk_m_d_002
|
||||
- id: pc-detective_m_d_002
|
||||
text: "Standard regulation spec. Commission requisition 4-C. They didn't upgrade this terminal in three years."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -42,7 +42,7 @@ lines:
|
||||
situation: [arrival, routine]
|
||||
tags: [arrival, atmospheric]
|
||||
|
||||
- id: commission-kiosk_m_d_003
|
||||
- id: pc-detective_m_d_003
|
||||
text: "Filtered air. A Commission smell — sterile, precise, faintly antiseptic. Familiar."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -51,7 +51,7 @@ lines:
|
||||
situation: [arrival, routine]
|
||||
tags: [arrival, atmospheric, sensory]
|
||||
|
||||
- id: commission-kiosk_m_d_004
|
||||
- id: pc-detective_m_d_004
|
||||
text: "Back at the kiosk. The terminal logged my last access thirty-two minutes ago."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -60,7 +60,7 @@ lines:
|
||||
situation: [arrival]
|
||||
tags: [arrival, orientation]
|
||||
|
||||
- id: commission-kiosk_m_d_005
|
||||
- id: pc-detective_m_d_005
|
||||
text: "Nobody uses this terminal except Commission staff. Which means whoever was here before me has credentials."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -71,7 +71,7 @@ lines:
|
||||
|
||||
# --- Perception / Sound ---
|
||||
|
||||
- id: commission-kiosk_m_d_006
|
||||
- id: pc-detective_m_d_006
|
||||
text: "The terminal hum is different from the freight equipment. Cleaner. Higher frequency."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -80,7 +80,7 @@ lines:
|
||||
situation: [routine]
|
||||
tags: [sensory, environmental]
|
||||
|
||||
- id: commission-kiosk_m_d_007
|
||||
- id: pc-detective_m_d_007
|
||||
text: "Footsteps at the transit corridor entrance. Not Commission — wrong pace."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -89,7 +89,7 @@ lines:
|
||||
situation: [routine, observation]
|
||||
tags: [sensory, caution]
|
||||
|
||||
- id: commission-kiosk_m_d_008
|
||||
- id: pc-detective_m_d_008
|
||||
text: "Someone's running queries at the main manifest board. Loud keystrokes — frustrated."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -100,7 +100,7 @@ lines:
|
||||
|
||||
# --- Tutorial / Evidence ---
|
||||
|
||||
- id: commission-kiosk_m_d_009
|
||||
- id: pc-detective_m_d_009
|
||||
text: "Import logs go back forty days. Weight discrepancies flagged automatically — unless someone cleared the flag."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -109,7 +109,7 @@ lines:
|
||||
situation: [routine, investigation]
|
||||
tags: [tutorial, investigation, orientation]
|
||||
|
||||
- id: commission-kiosk_m_d_010
|
||||
- id: pc-detective_m_d_010
|
||||
text: "Personnel movement log. Every authorized access to every restricted space, timestamped."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -120,7 +120,7 @@ lines:
|
||||
|
||||
# --- Time Idle / Ruminative ---
|
||||
|
||||
- id: commission-kiosk_m_d_011
|
||||
- id: pc-detective_m_d_011
|
||||
text: "Pattern is forming. Three manifests, same routing anomaly, different filing dates. That's deliberate."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -129,7 +129,7 @@ lines:
|
||||
situation: [routine, investigation]
|
||||
tags: [investigation, analytical]
|
||||
|
||||
- id: commission-kiosk_m_d_012
|
||||
- id: pc-detective_m_d_012
|
||||
text: "The kiosk shows what the system knows. The system doesn't know what it hasn't been told."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -138,7 +138,7 @@ lines:
|
||||
situation: [routine]
|
||||
tags: [atmospheric, investigation]
|
||||
|
||||
- id: commission-kiosk_m_d_013
|
||||
- id: pc-detective_m_d_013
|
||||
text: "Commission protocols say: document everything, infer nothing. The gap between those two is where cases live."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -147,7 +147,7 @@ lines:
|
||||
situation: [routine]
|
||||
tags: [atmospheric, analytical]
|
||||
|
||||
- id: commission-kiosk_m_d_014
|
||||
- id: pc-detective_m_d_014
|
||||
text: "Twelve access events in the manifest terminal in the last six days. Elevated for a district this size."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -156,7 +156,7 @@ lines:
|
||||
situation: [routine, investigation]
|
||||
tags: [investigation, analytical]
|
||||
|
||||
- id: commission-kiosk_m_d_015
|
||||
- id: pc-detective_m_d_015
|
||||
text: "Evidence is what the system logged. Inference is what it means. Right now I have plenty of both."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -167,7 +167,7 @@ lines:
|
||||
|
||||
# --- Anomaly ---
|
||||
|
||||
- id: commission-kiosk_m_d_016
|
||||
- id: pc-detective_m_d_016
|
||||
text: "Access log shows a non-Commission credential used at 0340. That terminal should have rejected it."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -177,7 +177,7 @@ lines:
|
||||
priority: 7
|
||||
tags: [investigation, caution, analytical]
|
||||
|
||||
- id: commission-kiosk_m_d_017
|
||||
- id: pc-detective_m_d_017
|
||||
text: "Container 4471 has three separate manifest entries with different timestamps. One of them is false."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -191,7 +191,7 @@ lines:
|
||||
priority: 8
|
||||
tags: [investigation, contraband, analytical]
|
||||
|
||||
- id: commission-kiosk_m_d_018
|
||||
- id: pc-detective_m_d_018
|
||||
text: "Standard access log would show calibration events. This one shows something cleared three days ago. Manually."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -202,7 +202,7 @@ lines:
|
||||
|
||||
# --- Knowledge-Gated: Sera Arc ---
|
||||
|
||||
- id: commission-kiosk_m_d_019
|
||||
- id: pc-detective_m_d_019
|
||||
text: "Venn, S. — last calibration event logged here: 0615 this morning. Standard. Her schedule is precise."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -216,7 +216,7 @@ lines:
|
||||
priority: 5
|
||||
tags: [npc, sera, routine, friend-arc]
|
||||
|
||||
- id: commission-kiosk_m_d_020
|
||||
- id: pc-detective_m_d_020
|
||||
text: "Venn's credentials are in the access log. Three times today. She's thorough, or she's looking for something."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -230,7 +230,7 @@ lines:
|
||||
priority: 6
|
||||
tags: [npc, sera, investigation, friend-arc, dual-lens]
|
||||
|
||||
- id: commission-kiosk_m_d_021
|
||||
- id: pc-detective_m_d_021
|
||||
text: "She runs calibration checks on the Commission terminal. That's her job. But the access log shows she's also running manifest queries. That's not."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -244,7 +244,7 @@ lines:
|
||||
priority: 7
|
||||
tags: [npc, sera, tell, investigation, friend-arc]
|
||||
|
||||
- id: commission-kiosk_m_d_022
|
||||
- id: pc-detective_m_d_022
|
||||
text: "Venn cleared a flag on container 4471 six days ago. Routine override — authorized. But 4471 is in my manifest discrepancy list."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -260,7 +260,7 @@ lines:
|
||||
priority: 9
|
||||
tags: [npc, sera, investigation, contraband, friend-arc, contaminated-trust]
|
||||
|
||||
- id: commission-kiosk_m_d_023
|
||||
- id: pc-detective_m_d_023
|
||||
text: "She knows. Either she found it and cleared it deliberately, or she was directed to. Both are worse than I want to believe."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -278,7 +278,7 @@ lines:
|
||||
|
||||
# --- Post-Conversation ---
|
||||
|
||||
- id: commission-kiosk_m_d_024
|
||||
- id: pc-detective_m_d_024
|
||||
text: "She said she hadn't touched the manifest terminal. The log says otherwise. Either she forgot or she lied."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -292,7 +292,7 @@ lines:
|
||||
priority: 9
|
||||
tags: [npc, sera, post-conversation, tell, friend-arc, contaminated-trust]
|
||||
|
||||
- id: commission-kiosk_m_d_025
|
||||
- id: pc-detective_m_d_025
|
||||
text: "Davan said he runs a clean dock. The manifest data disagrees on three specific points. Filed."
|
||||
role: player_character
|
||||
access: [public]
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
character: detective
|
||||
location: contradiction
|
||||
lines:
|
||||
# Contradiction Monologue — Ticket #552 — Sera/Kael FRIEND Arc
|
||||
# Authored per D-083 (contradiction detection pipeline) and Paula Round 2 Mellanie spec
|
||||
# (docs/workshops/knowledge-flow-npc-boundaries/paula-round2.md, lines 117-158).
|
||||
#
|
||||
# Trigger: contradiction_detected fires when the detective observes Kael in Corridor B-7
|
||||
# while the KG holds Sera's claim that Kael was at dock intake during second shift.
|
||||
# Event payload: source_display_name = "Sera Venn", subject_display_name = "Kael Davan"
|
||||
#
|
||||
# FRIEND-pattern NPCs (Sera, Kael) always use hand-authored lines — generic template fallback
|
||||
# does not fire for this arc.
|
||||
#
|
||||
# Authoring principle: cognitive dissonance, not accusation. The detective doesn't know
|
||||
# who or what is wrong. Uncertainty first, suspicion second, accusation never.
|
||||
#
|
||||
# Phase 2 lines: trust established but not deep — the blindsiding.
|
||||
# Prerequisite: KG entry for Kael has state: Contradicted, source: ToldBy(Sera Venn)
|
||||
# Phase 3 lines: additional context accumulated — Sera's avoidance of Torek observed 3x.
|
||||
# Prerequisite: Phase 2 + Sera's avoidance pattern has fired (three departures logged).
|
||||
#
|
||||
# ID discriminator: _con_ marks contradiction lines per Q-028 pending resolution.
|
||||
# cooldown: omitted — fire-once enforced by trigger semantics (contradiction_detected
|
||||
# fires once per KG state change), not by a cooldown value.
|
||||
#
|
||||
# Note: contradiction_detected is a new trigger type — requires server-side implementation
|
||||
# in monologue.rs (ticket #550). Secondary beat lines use lower priority for sequencing;
|
||||
# server team should implement delay_after_trigger_ticks for proper 3-5 second gap.
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# PHASE 2 — Primary beat (fires on contradiction_detected, immediate)
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
- id: pc-detective_m_d_con_001
|
||||
text: "Sera said Kael was at the dock intake during second shift. I'm looking at him in corridor B-7 right now."
|
||||
role: player_character
|
||||
access: [public]
|
||||
trust: surface
|
||||
situation: [investigation, observation]
|
||||
trigger: contradiction_detected
|
||||
mood: [focused, anxious]
|
||||
priority: 10
|
||||
|
||||
prerequisites:
|
||||
entities:
|
||||
- id: "npc:kael-davan"
|
||||
state: Contradicted
|
||||
- id: "npc:sera-venn"
|
||||
state: Contradicted
|
||||
tags: [contradiction, friend-arc, sera, kael, phase-2, analytical]
|
||||
notes: >
|
||||
Paula's canonical primary line — preserved verbatim. Flat factual statement: source named,
|
||||
subject named, specific location claim vs. direct observation. The 'I'm' contraction is
|
||||
natural in a surprise moment even for the analytical detective — the observation breaks
|
||||
through the professional register. No interpretation, no accusation.
|
||||
|
||||
- id: pc-detective_m_d_con_002
|
||||
text: "Venn said Kael was at dock intake through second shift. Kael is in B-7. Those aren't compatible."
|
||||
role: player_character
|
||||
access: [public]
|
||||
trust: surface
|
||||
situation: [investigation, observation]
|
||||
trigger: contradiction_detected
|
||||
mood: [focused]
|
||||
priority: 9
|
||||
|
||||
prerequisites:
|
||||
entities:
|
||||
- id: "npc:kael-davan"
|
||||
state: Contradicted
|
||||
- id: "npc:sera-venn"
|
||||
state: Contradicted
|
||||
tags: [contradiction, friend-arc, sera, kael, phase-2, analytical]
|
||||
notes: >
|
||||
Alternate primary — fully analytical register: surname-initial for both (institutional
|
||||
reflex), no contractions, plain declarative close. 'Those aren't compatible' is the
|
||||
detective's version of stating contradiction — clinical, process-language. Fires as
|
||||
alternate to con_001 for variety on replay.
|
||||
|
||||
- id: pc-detective_m_d_con_003
|
||||
text: "She placed Kael at the dock. He's not at the dock."
|
||||
role: player_character
|
||||
access: [public]
|
||||
trust: surface
|
||||
situation: [investigation, observation]
|
||||
trigger: contradiction_detected
|
||||
mood: [focused, anxious]
|
||||
priority: 9
|
||||
|
||||
prerequisites:
|
||||
entities:
|
||||
- id: "npc:kael-davan"
|
||||
state: Contradicted
|
||||
- id: "npc:sera-venn"
|
||||
state: Contradicted
|
||||
tags: [contradiction, friend-arc, sera, kael, phase-2]
|
||||
notes: >
|
||||
Stripped-down alternate. 'She placed' is the informal of 'Venn stated' — personal register
|
||||
triggered by Sera being THE FRIEND. 'He's not at the dock' — the barest possible statement
|
||||
of contradiction. The brevity is the weight: six words and the investigation changes shape.
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# PHASE 2 — Secondary beat (fires after short delay, cognitive processing)
|
||||
# Server team (#550): implement delay_after_trigger_ticks: 90 for 3-5s gap.
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
- id: pc-detective_m_d_con_004
|
||||
text: "One of them is wrong. Sera, or what I'm seeing. Or I'm missing something I don't have yet."
|
||||
role: player_character
|
||||
access: [public]
|
||||
trust: surface
|
||||
situation: [investigation, alone]
|
||||
trigger: contradiction_detected
|
||||
mood: [anxious, focused]
|
||||
priority: 7
|
||||
|
||||
prerequisites:
|
||||
entities:
|
||||
- id: "npc:kael-davan"
|
||||
state: Contradicted
|
||||
- id: "npc:sera-venn"
|
||||
state: Contradicted
|
||||
tags: [contradiction, friend-arc, sera, kael, phase-2, cognitive-dissonance, secondary-beat]
|
||||
notes: >
|
||||
Paula's canonical secondary beat — fires 3-5 seconds after the primary beat.
|
||||
Epistemic neutrality held across three options: Sera is wrong, observation is wrong, or
|
||||
context is missing. No option is dismissed. 'I'm missing something I don't have yet' is
|
||||
the key line — it holds accusation off even against evidence. The detective files it open.
|
||||
Lower priority (7) ensures primary lines fire first; server team should implement
|
||||
delay_after_trigger_ticks for proper sequencing.
|
||||
|
||||
- id: pc-detective_m_d_con_005
|
||||
text: "File it. Don't close it. Either Venn's information was wrong when she gave it, or it was wrong on purpose. That's a different question."
|
||||
role: player_character
|
||||
access: [public]
|
||||
trust: surface
|
||||
situation: [investigation, alone]
|
||||
trigger: contradiction_detected
|
||||
mood: [focused]
|
||||
priority: 6
|
||||
|
||||
prerequisites:
|
||||
entities:
|
||||
- id: "npc:kael-davan"
|
||||
state: Contradicted
|
||||
- id: "npc:sera-venn"
|
||||
state: Contradicted
|
||||
tags: [contradiction, friend-arc, sera, kael, phase-2, analytical, cognitive-dissonance, secondary-beat]
|
||||
notes: >
|
||||
Alternate secondary beat. 'File it, don't close it' is the detective's internal procedure
|
||||
for unresolved data. The critical distinction: wrong information vs. deliberately wrong
|
||||
information — the detective separates these without assuming either. 'That's a different
|
||||
question' closes the processing loop, marking it unresolved, not dismissed.
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# PHASE 3 — With accumulated context (Sera's avoidance pattern observed)
|
||||
# Requires: three Sera departures correlated with Torek Lintar arrivals observed.
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
- id: pc-detective_m_d_con_006
|
||||
text: "Sera told me Kael doesn't make mistakes. He's not where she said he'd be. And she's been avoiding Lintar for three weeks."
|
||||
role: player_character
|
||||
access: [public]
|
||||
trust: surface
|
||||
situation: [investigation, observation]
|
||||
trigger: contradiction_detected
|
||||
mood: [focused, anxious]
|
||||
priority: 10
|
||||
|
||||
prerequisites:
|
||||
entities:
|
||||
- id: "npc:kael-davan"
|
||||
state: Contradicted
|
||||
- id: "npc:sera-venn"
|
||||
state: Contradicted
|
||||
- id: "npc:torek-lintar"
|
||||
relationship: PersonOfInterest
|
||||
tags: [contradiction, friend-arc, sera, kael, phase-3, pattern-recognition]
|
||||
notes: >
|
||||
Paula's canonical Phase 3 line — preserved verbatim. Three facts in sequence, no explicit
|
||||
connection drawn between them: Sera's testimony about Kael ('doesn't make mistakes'),
|
||||
Kael's actual location, Sera's avoidance behavior. The detective is connecting dots but
|
||||
not announcing the conclusion. The emotional weight is in 'Sera told me' — personal register
|
||||
for the friend relationship — vs. the clinical observation about Lintar.
|
||||
|
||||
- id: pc-detective_m_d_con_007
|
||||
text: "Two facts: Venn placed Kael at dock intake, second shift. Kael is in B-7. One new observation: Venn leaves every time Lintar enters the bar. Worth seeing if those connect."
|
||||
role: player_character
|
||||
access: [public]
|
||||
trust: surface
|
||||
situation: [investigation, observation]
|
||||
trigger: contradiction_detected
|
||||
mood: [focused]
|
||||
priority: 9
|
||||
|
||||
prerequisites:
|
||||
entities:
|
||||
- id: "npc:kael-davan"
|
||||
state: Contradicted
|
||||
- id: "npc:sera-venn"
|
||||
state: Contradicted
|
||||
- id: "npc:torek-lintar"
|
||||
relationship: PersonOfInterest
|
||||
tags: [contradiction, friend-arc, sera, kael, phase-3, analytical, pattern-recognition]
|
||||
notes: >
|
||||
Alternate Phase 3 — analytical register throughout, no personal register even for Sera
|
||||
(surname only). 'Worth seeing if those connect' is the detective's verbal tic of flagging
|
||||
without concluding. The numbered format ('Two facts... One new observation') is the
|
||||
detective's internal case-building style. Fully non-accusatory: the connection is not
|
||||
stated, only the intent to look for it.
|
||||
|
||||
- id: pc-detective_m_d_con_008
|
||||
text: "Venn's been managing her exits around Lintar. And she put Kael somewhere he isn't. Let's see what sits at the intersection of those two."
|
||||
role: player_character
|
||||
access: [public]
|
||||
trust: surface
|
||||
situation: [investigation, alone]
|
||||
trigger: contradiction_detected
|
||||
mood: [focused]
|
||||
priority: 8
|
||||
|
||||
prerequisites:
|
||||
entities:
|
||||
- id: "npc:kael-davan"
|
||||
state: Contradicted
|
||||
- id: "npc:sera-venn"
|
||||
state: Contradicted
|
||||
- id: "npc:torek-lintar"
|
||||
relationship: PersonOfInterest
|
||||
tags: [contradiction, friend-arc, sera, kael, phase-3, analytical]
|
||||
notes: >
|
||||
Alternate Phase 3. 'Let's see what' verbal tic, no accusation. 'Managing her exits' is
|
||||
the detective naming Sera's avoidance behavior — behavioral description, not motive
|
||||
attribution. 'Put Kael somewhere he isn't' — plain contradiction framing. The intersection
|
||||
remains unstated. Detective is following the thread, not announcing what's on it.
|
||||
@@ -1,7 +1,7 @@
|
||||
character: detective
|
||||
location: general
|
||||
lines:
|
||||
- id: general_m_d_001
|
||||
- id: pc-detective_m_d_001
|
||||
text: "Sova Transit District. Population twelve thousand and change. Let's narrow that down."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -10,7 +10,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [arrival, analytical]
|
||||
|
||||
- id: general_m_d_002
|
||||
- id: pc-detective_m_d_002
|
||||
text: "The span gate resonance is constant here. Background radiation of a freight hub."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -19,7 +19,7 @@ lines:
|
||||
trigger: hear_sound
|
||||
tags: [environmental]
|
||||
|
||||
- id: general_m_d_003
|
||||
- id: pc-detective_m_d_003
|
||||
text: "First week on station. The case file is thin. The manifest discrepancies are not."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -28,7 +28,7 @@ lines:
|
||||
trigger: time_idle
|
||||
tags: [investigation, orientation]
|
||||
|
||||
- id: general_m_d_004
|
||||
- id: pc-detective_m_d_004
|
||||
text: "Everyone knows I'm Commission. The conversations stop when I walk in. Noted."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -37,7 +37,7 @@ lines:
|
||||
trigger: time_idle
|
||||
tags: [investigation, social]
|
||||
|
||||
- id: general_m_d_005
|
||||
- id: pc-detective_m_d_005
|
||||
text: "Station gravity's point-nine-three standard. Enough to notice after a long shift."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -46,7 +46,7 @@ lines:
|
||||
trigger: time_idle
|
||||
tags: [environmental, personal]
|
||||
|
||||
- id: general_m_d_006
|
||||
- id: pc-detective_m_d_006
|
||||
text: "The previous liaison rotated out three months ago. That gap matters."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -55,7 +55,7 @@ lines:
|
||||
trigger: time_idle
|
||||
tags: [investigation, analytical]
|
||||
|
||||
- id: general_m_d_007
|
||||
- id: pc-detective_m_d_007
|
||||
text: "Back in the district. Let's see what changed while I wasn't watching."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -64,7 +64,7 @@ lines:
|
||||
trigger: return_visit
|
||||
tags: [arrival, analytical]
|
||||
|
||||
- id: general_m_d_008
|
||||
- id: pc-detective_m_d_008
|
||||
text: "Recycled air, industrial residue, and that constant gate hum. Sova in three notes."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -73,7 +73,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [environmental, atmospheric]
|
||||
|
||||
- id: general_m_d_009
|
||||
- id: pc-detective_m_d_009
|
||||
text: "Communities like this close ranks around outsiders. Patient work."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -82,7 +82,7 @@ lines:
|
||||
trigger: time_idle
|
||||
tags: [reflection, social]
|
||||
|
||||
- id: general_m_d_010
|
||||
- id: pc-detective_m_d_010
|
||||
text: "The manifest data goes back eighteen months. The pattern starts around month six."
|
||||
role: player_character
|
||||
access: [public]
|
||||
|
||||
@@ -3,7 +3,7 @@ location: maintenance-corridors
|
||||
lines:
|
||||
# --- Arrival + Environmental Flavor ---
|
||||
|
||||
- id: maintenance-corridors_m_d_001
|
||||
- id: pc-detective_m_d_001
|
||||
text: "Maintenance level. Low lighting, exposed conduit, restricted access nobody enforces."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -12,7 +12,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [arrival, environmental]
|
||||
|
||||
- id: maintenance-corridors_m_d_002
|
||||
- id: pc-detective_m_d_002
|
||||
text: "The air's different down here. Cooler. Damper. Less filtered."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -21,7 +21,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [environmental, atmospheric]
|
||||
|
||||
- id: maintenance-corridors_m_d_003
|
||||
- id: pc-detective_m_d_003
|
||||
text: "Service corridor B-section. Not on the public transit map. Interesting routing options."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -30,7 +30,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [arrival, investigation]
|
||||
|
||||
- id: maintenance-corridors_m_d_004
|
||||
- id: pc-detective_m_d_004
|
||||
text: "Water recycling conduits. The station's circulatory system, hidden from view."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -39,7 +39,7 @@ lines:
|
||||
trigger: hear_sound
|
||||
tags: [environmental]
|
||||
|
||||
- id: maintenance-corridors_m_d_005
|
||||
- id: pc-detective_m_d_005
|
||||
text: "Pipe network branching three directions. Good for maintenance. Better for privacy."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -48,7 +48,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [environmental, analytical]
|
||||
|
||||
- id: maintenance-corridors_m_d_006
|
||||
- id: pc-detective_m_d_006
|
||||
text: "Emergency lighting only. Visibility reduced, but so is surveillance coverage."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -57,7 +57,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [environmental, investigation]
|
||||
|
||||
- id: maintenance-corridors_m_d_007
|
||||
- id: pc-detective_m_d_007
|
||||
text: "The rust smell is stronger near junction B-7. Older section. Less maintenance."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -66,7 +66,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [environmental, atmospheric]
|
||||
|
||||
- id: maintenance-corridors_m_d_008
|
||||
- id: pc-detective_m_d_008
|
||||
text: "Back in the corridors. Different route this time — mapping the alternatives."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -77,7 +77,7 @@ lines:
|
||||
|
||||
# --- Investigation Observations ---
|
||||
|
||||
- id: maintenance-corridors_m_d_009
|
||||
- id: pc-detective_m_d_009
|
||||
text: "Scuff marks on the floor. Recent, heavy. Cargo weight, not foot traffic."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -86,7 +86,7 @@ lines:
|
||||
trigger: observe_anomaly
|
||||
tags: [investigation, evidence]
|
||||
|
||||
- id: maintenance-corridors_m_d_010
|
||||
- id: pc-detective_m_d_010
|
||||
text: "The service hatch has been used recently. Oil on the handle, dust disturbed."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -95,7 +95,7 @@ lines:
|
||||
trigger: observe_anomaly
|
||||
tags: [investigation, evidence]
|
||||
|
||||
- id: maintenance-corridors_m_d_011
|
||||
- id: pc-detective_m_d_011
|
||||
text: "Restricted access, but the lock plate shows wear. Regular use by someone with clearance."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -104,7 +104,7 @@ lines:
|
||||
trigger: observe_anomaly
|
||||
tags: [investigation, evidence]
|
||||
|
||||
- id: maintenance-corridors_m_d_012
|
||||
- id: pc-detective_m_d_012
|
||||
text: "Sound carries differently down here. Voices from two junctions away, if you're patient."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -113,7 +113,7 @@ lines:
|
||||
trigger: hear_sound
|
||||
tags: [environmental, investigation]
|
||||
|
||||
- id: maintenance-corridors_m_d_013
|
||||
- id: pc-detective_m_d_013
|
||||
text: "Chalk marks on the wall. Low, deliberate. Not maintenance notation — wrong color."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -122,7 +122,7 @@ lines:
|
||||
trigger: observe_anomaly
|
||||
tags: [investigation, evidence]
|
||||
|
||||
- id: maintenance-corridors_m_d_014
|
||||
- id: pc-detective_m_d_014
|
||||
text: "This corridor connects bay four to temp storage. Bypasses the main freight floor entirely."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -131,7 +131,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [investigation, operational]
|
||||
|
||||
- id: maintenance-corridors_m_d_015
|
||||
- id: pc-detective_m_d_015
|
||||
text: "Footsteps ahead. Single set, moving fast. Someone who knows where they're going."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -140,7 +140,7 @@ lines:
|
||||
trigger: hear_sound
|
||||
tags: [investigation, caution]
|
||||
|
||||
- id: maintenance-corridors_m_d_016
|
||||
- id: pc-detective_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]
|
||||
@@ -151,7 +151,7 @@ lines:
|
||||
|
||||
# --- Knowledge-Gated Lines ---
|
||||
|
||||
- id: maintenance-corridors_m_d_017
|
||||
- id: pc-detective_m_d_017
|
||||
text: "The cargo scuff pattern matches bay four containers. Dock to corridor to storage — the route."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -165,7 +165,7 @@ lines:
|
||||
priority: 8
|
||||
tags: [investigation, evidence, breakthrough]
|
||||
|
||||
- id: maintenance-corridors_m_d_018
|
||||
- id: pc-detective_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]
|
||||
@@ -179,7 +179,7 @@ lines:
|
||||
priority: 7
|
||||
tags: [npc, renn, investigation, evidence]
|
||||
|
||||
- id: maintenance-corridors_m_d_019
|
||||
- id: pc-detective_m_d_019
|
||||
text: "Camera blind spot at junction C-2. Coincidence, or someone mapped the coverage?"
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -192,7 +192,7 @@ lines:
|
||||
min_confidence: suspects
|
||||
tags: [investigation, operational]
|
||||
|
||||
- id: maintenance-corridors_m_d_020
|
||||
- id: pc-detective_m_d_020
|
||||
text: "Standard container, no manifest checkpoint along this route. That's the gap."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -206,7 +206,7 @@ lines:
|
||||
priority: 9
|
||||
tags: [investigation, breakthrough]
|
||||
|
||||
- id: maintenance-corridors_m_d_021
|
||||
- id: pc-detective_m_d_021
|
||||
text: "Hatch lubricant is fresh. Someone's maintaining this access point. Not station maintenance."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -219,7 +219,7 @@ lines:
|
||||
min_confidence: knows_of
|
||||
tags: [investigation, evidence]
|
||||
|
||||
- id: maintenance-corridors_m_d_022
|
||||
- id: pc-detective_m_d_022
|
||||
text: "Davan was seen near B-7 during off-shift. Cross-reference with container movements."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -233,7 +233,7 @@ lines:
|
||||
priority: 7
|
||||
tags: [npc, kael, investigation]
|
||||
|
||||
- id: maintenance-corridors_m_d_023
|
||||
- id: pc-detective_m_d_023
|
||||
text: "Two access routes, one manifest checkpoint. Someone designed this gap. Or discovered it."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -246,7 +246,7 @@ lines:
|
||||
min_confidence: knows_details
|
||||
tags: [investigation, analytical]
|
||||
|
||||
- id: maintenance-corridors_m_d_024
|
||||
- id: pc-detective_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]
|
||||
@@ -260,7 +260,7 @@ lines:
|
||||
priority: 6
|
||||
tags: [npc, drin, investigation, moral]
|
||||
|
||||
- id: maintenance-corridors_m_d_025
|
||||
- id: pc-detective_m_d_025
|
||||
text: "Evidence is accumulating. Names, routes, timing. But I still can't see the full picture."
|
||||
role: player_character
|
||||
access: [public]
|
||||
|
||||
@@ -10,7 +10,7 @@ lines:
|
||||
# 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
|
||||
- id: pc-detective_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]
|
||||
@@ -23,7 +23,7 @@ lines:
|
||||
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
|
||||
- id: pc-detective_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]
|
||||
@@ -36,7 +36,7 @@ lines:
|
||||
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
|
||||
- id: pc-detective_m_d_003
|
||||
text: "Lattice overlay initializing. Four identity flags in immediate range — cross-referencing against Commission case file."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -49,7 +49,7 @@ lines:
|
||||
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
|
||||
- id: pc-detective_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]
|
||||
@@ -62,7 +62,7 @@ lines:
|
||||
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
|
||||
- id: pc-detective_m_d_005
|
||||
text: "Visual range terminates at bay nine. Approximately fifteen meters. Consistent with district atmo settings."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -74,7 +74,7 @@ lines:
|
||||
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
|
||||
- id: pc-detective_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]
|
||||
@@ -87,7 +87,7 @@ lines:
|
||||
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
|
||||
- id: pc-detective_m_d_007
|
||||
text: "Venn, S. at the far terminal. She said she'd come in early."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -100,7 +100,7 @@ lines:
|
||||
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
|
||||
- id: pc-detective_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]
|
||||
@@ -113,7 +113,7 @@ lines:
|
||||
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
|
||||
- id: pc-detective_m_d_009
|
||||
text: "Central manifest board: real-time container routing, weight declarations, bay assignments. Everything routes through here."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -126,7 +126,7 @@ lines:
|
||||
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
|
||||
- id: pc-detective_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]
|
||||
@@ -139,7 +139,7 @@ lines:
|
||||
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
|
||||
- id: pc-detective_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]
|
||||
@@ -152,7 +152,7 @@ lines:
|
||||
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
|
||||
- id: pc-detective_m_d_012
|
||||
text: "Lattice stable. Nine identity flags and counting. Let's see what this shift tells me."
|
||||
role: player_character
|
||||
access: [public]
|
||||
|
||||
@@ -12,7 +12,7 @@ lines:
|
||||
|
||||
# --- Tell Observations (5 core tells from pc-as-npc-spec.md section 8) ---
|
||||
|
||||
- id: general_m_d_011
|
||||
- id: pc-detective_m_d_001
|
||||
text: "That dock worker checks the corridor before turning. Habitual. Military? Or just careful."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -25,7 +25,7 @@ lines:
|
||||
state: known
|
||||
tags: [npc, pc-smuggler, tell, corridor-check]
|
||||
|
||||
- id: general_m_d_012
|
||||
- id: pc-detective_m_d_002
|
||||
text: "Movement pattern changed at shift change. More direct. Less idle. Interesting."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -38,7 +38,7 @@ lines:
|
||||
state: known
|
||||
tags: [npc, pc-smuggler, tell, shift-transition]
|
||||
|
||||
- id: general_m_d_013
|
||||
- id: pc-detective_m_d_003
|
||||
text: "Slight acceleration past the checkpoint. Most people do it. But this one does it every time."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -51,7 +51,7 @@ lines:
|
||||
state: known
|
||||
tags: [npc, pc-smuggler, tell, security-proximity]
|
||||
|
||||
- id: general_m_d_014
|
||||
- id: pc-detective_m_d_004
|
||||
text: "Same three people at the same table. Every shift. Consistent social group."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -64,7 +64,7 @@ lines:
|
||||
state: known
|
||||
tags: [npc, pc-smuggler, tell, social-clustering]
|
||||
|
||||
- id: general_m_d_015
|
||||
- id: pc-detective_m_d_005
|
||||
text: "That worker checked container 4471 twice. The same container that's in my manifest discrepancy file."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -83,7 +83,7 @@ lines:
|
||||
|
||||
# --- Accumulated Observation (gates on multiple tell sightings) ---
|
||||
|
||||
- id: general_m_d_016
|
||||
- id: pc-detective_m_d_006
|
||||
text: "Three behavioral flags on one dock worker. Corridor awareness, shift-transition alertness, cargo fixation. That's a pattern."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -98,7 +98,7 @@ lines:
|
||||
priority: 7
|
||||
tags: [npc, pc-smuggler, investigation, analytical, pattern-recognition]
|
||||
|
||||
- id: general_m_d_017
|
||||
- id: pc-detective_m_d_007
|
||||
text: "Cross-referencing the dock worker's shift schedule with manifest anomalies. Correlation: moderate."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -115,7 +115,7 @@ lines:
|
||||
priority: 8
|
||||
tags: [npc, pc-smuggler, investigation, evidence]
|
||||
|
||||
- id: general_m_d_018
|
||||
- id: pc-detective_m_d_008
|
||||
text: "The dock worker and Davan eat together every shift. Same courier joins them. Tight operational cluster."
|
||||
role: player_character
|
||||
access: [public]
|
||||
|
||||
@@ -3,7 +3,7 @@ location: the-last-shift
|
||||
lines:
|
||||
# --- Arrival + Environmental Flavor ---
|
||||
|
||||
- id: the-last-shift_m_d_001
|
||||
- id: pc-detective_m_d_001
|
||||
text: "The Last Shift. Only place in this district that doesn't smell like freight lubricant."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -12,7 +12,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [arrival, atmospheric]
|
||||
|
||||
- id: the-last-shift_m_d_002
|
||||
- id: pc-detective_m_d_002
|
||||
text: "Grain spirit and low conversation. The social nexus of Sova Transit."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -21,7 +21,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [arrival, analytical]
|
||||
|
||||
- id: the-last-shift_m_d_003
|
||||
- id: pc-detective_m_d_003
|
||||
text: "Bar's busy tonight. Good — crowds talk. Bad — they notice outsiders."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -30,7 +30,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [arrival, investigation]
|
||||
|
||||
- id: the-last-shift_m_d_004
|
||||
- id: pc-detective_m_d_004
|
||||
text: "Standard district bar. Prefab fixtures, aftermarket lighting, a menu board nobody reads."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -39,7 +39,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [arrival, environmental]
|
||||
|
||||
- id: the-last-shift_m_d_005
|
||||
- id: pc-detective_m_d_005
|
||||
text: "The Meridian feed on the wall display. Local news, freight indices, Assembly coverage."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -48,7 +48,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [environmental]
|
||||
|
||||
- id: the-last-shift_m_d_006
|
||||
- id: pc-detective_m_d_006
|
||||
text: "The overhead vent rattles every thirty seconds. Like clockwork. Nobody else notices."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -57,7 +57,7 @@ lines:
|
||||
trigger: hear_sound
|
||||
tags: [environmental]
|
||||
|
||||
- id: the-last-shift_m_d_007
|
||||
- id: pc-detective_m_d_007
|
||||
text: "The conversation volume dropped when I walked in. Recovering now. They're adjusting."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -66,7 +66,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [social, investigation]
|
||||
|
||||
- id: the-last-shift_m_d_008
|
||||
- id: pc-detective_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]
|
||||
@@ -75,7 +75,7 @@ lines:
|
||||
trigger: return_visit
|
||||
tags: [arrival, analytical]
|
||||
|
||||
- id: the-last-shift_m_d_009
|
||||
- id: pc-detective_m_d_009
|
||||
text: "Kitchen's running. Fried protein and something spiced. The bar's other currency: hot food."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -84,7 +84,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [environmental, atmospheric]
|
||||
|
||||
- id: the-last-shift_m_d_010
|
||||
- id: pc-detective_m_d_010
|
||||
text: "Someone carved initials into this table. Old. A history I don't have."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -95,7 +95,7 @@ lines:
|
||||
|
||||
# --- NPC Commentary ---
|
||||
|
||||
- id: the-last-shift_m_d_011
|
||||
- id: pc-detective_m_d_011
|
||||
text: "The bartender — Sessik, L. Knows everyone. Watches everything. Gives away nothing."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -104,7 +104,7 @@ lines:
|
||||
trigger: observe_npc
|
||||
tags: [npc, lera, analytical]
|
||||
|
||||
- id: the-last-shift_m_d_012
|
||||
- id: pc-detective_m_d_012
|
||||
text: "Good to see Sera. At least someone in this district speaks my language."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -113,7 +113,7 @@ lines:
|
||||
trigger: observe_npc
|
||||
tags: [npc, sera, warm]
|
||||
|
||||
- id: the-last-shift_m_d_013
|
||||
- id: pc-detective_m_d_013
|
||||
text: "Lintar, T. — the loud one at the bar. Spends freely. Where's the money from?"
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -122,7 +122,7 @@ lines:
|
||||
trigger: observe_npc
|
||||
tags: [npc, torek, investigation]
|
||||
|
||||
- id: the-last-shift_m_d_014
|
||||
- id: pc-detective_m_d_014
|
||||
text: "A woman at the corner table — Tamm, N., according to Sera. Davan's partner."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -131,7 +131,7 @@ lines:
|
||||
trigger: observe_npc
|
||||
tags: [npc, naia, analytical]
|
||||
|
||||
- id: the-last-shift_m_d_015
|
||||
- id: pc-detective_m_d_015
|
||||
text: "The regulars have assigned seating. Unofficial, but enforced. I don't have a spot yet."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -140,7 +140,7 @@ lines:
|
||||
trigger: observe_npc
|
||||
tags: [social, atmospheric]
|
||||
|
||||
- id: the-last-shift_m_d_016
|
||||
- id: pc-detective_m_d_016
|
||||
text: "Sessik greeted three people by name since I sat down. I got a nod. Progress."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -149,7 +149,7 @@ lines:
|
||||
trigger: witness_interaction
|
||||
tags: [npc, lera, social]
|
||||
|
||||
- id: the-last-shift_m_d_017
|
||||
- id: pc-detective_m_d_017
|
||||
text: "Two dock workers in the corner. Heads close, voices low. Not unusual for after-shift."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -158,7 +158,7 @@ lines:
|
||||
trigger: witness_interaction
|
||||
tags: [social, investigation]
|
||||
|
||||
- id: the-last-shift_m_d_018
|
||||
- id: pc-detective_m_d_018
|
||||
text: "Sera introduced me to Tamm. Brief, polite, guarded. Expected."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -167,7 +167,7 @@ lines:
|
||||
trigger: post_conversation
|
||||
tags: [npc, sera, naia, social]
|
||||
|
||||
- id: the-last-shift_m_d_019
|
||||
- id: pc-detective_m_d_019
|
||||
text: "Sessik poured my drink without asking. She remembered. Small thing, but noted."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -176,7 +176,7 @@ lines:
|
||||
trigger: observe_npc
|
||||
tags: [npc, lera, social, warm]
|
||||
|
||||
- id: the-last-shift_m_d_020
|
||||
- id: pc-detective_m_d_020
|
||||
text: "The old man by the window hasn't spoken to anyone all evening. Watching, though."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -187,7 +187,7 @@ lines:
|
||||
|
||||
# --- Knowledge-Gated Lines ---
|
||||
|
||||
- id: the-last-shift_m_d_021
|
||||
- id: pc-detective_m_d_021
|
||||
text: "Sera left when Torek arrived. Second time. Different excuse. Same result."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -201,7 +201,7 @@ lines:
|
||||
priority: 7
|
||||
tags: [npc, sera, torek, tell, friend-arc]
|
||||
|
||||
- id: the-last-shift_m_d_022
|
||||
- id: pc-detective_m_d_022
|
||||
text: "That's the third time. Three excuses, one result. That's data."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -215,7 +215,7 @@ lines:
|
||||
priority: 9
|
||||
tags: [npc, sera, torek, tell, friend-arc, contradiction]
|
||||
|
||||
- id: the-last-shift_m_d_023
|
||||
- id: pc-detective_m_d_023
|
||||
text: "Lintar's spending doesn't match a dock worker's salary. Source unknown."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -228,7 +228,7 @@ lines:
|
||||
min_confidence: knows_of
|
||||
tags: [npc, torek, investigation]
|
||||
|
||||
- id: the-last-shift_m_d_024
|
||||
- id: pc-detective_m_d_024
|
||||
text: "Sessik's eyes tracked me when I sat near the back room. Protective, not hostile."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -241,7 +241,7 @@ lines:
|
||||
min_confidence: suspects
|
||||
tags: [npc, lera, investigation]
|
||||
|
||||
- id: the-last-shift_m_d_025
|
||||
- id: pc-detective_m_d_025
|
||||
text: "Sera's being extra helpful tonight. Names, backgrounds, context. Compensating?"
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -255,7 +255,7 @@ lines:
|
||||
priority: 7
|
||||
tags: [npc, sera, friend-arc, behavioral]
|
||||
|
||||
- id: the-last-shift_m_d_026
|
||||
- id: pc-detective_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]
|
||||
@@ -269,7 +269,7 @@ lines:
|
||||
priority: 8
|
||||
tags: [npc, sera, contaminated-trust, friend-arc]
|
||||
|
||||
- id: the-last-shift_m_d_027
|
||||
- id: pc-detective_m_d_027
|
||||
text: "Tamm seems distressed tonight. Not her baseline. Something at home, or something she heard?"
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -282,7 +282,7 @@ lines:
|
||||
state: known
|
||||
tags: [npc, naia, behavioral]
|
||||
|
||||
- id: the-last-shift_m_d_028
|
||||
- id: pc-detective_m_d_028
|
||||
text: "Sessik just redirected a conversation away from freight schedules. Protective reflex?"
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -295,7 +295,7 @@ lines:
|
||||
min_confidence: suspects
|
||||
tags: [npc, lera, investigation]
|
||||
|
||||
- id: the-last-shift_m_d_029
|
||||
- id: pc-detective_m_d_029
|
||||
text: "Davan and his partner at the bar. She's talking. He's not listening. Noted."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -308,7 +308,7 @@ lines:
|
||||
state: known
|
||||
tags: [npc, kael, naia, behavioral]
|
||||
|
||||
- id: the-last-shift_m_d_030
|
||||
- id: pc-detective_m_d_030
|
||||
text: "The regulars are warming up. Three visits and I'm almost furniture. Almost."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -324,7 +324,7 @@ lines:
|
||||
# --- Kael Davan — Additional Bar Observations ---
|
||||
# Ticket: #297 review fix | Filling detective monologue gap
|
||||
|
||||
- id: the-last-shift_m_d_043
|
||||
- id: pc-detective_m_d_031
|
||||
text: "Davan is different here. Looser. The dock worker persona drops and something warmer replaces it."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -337,7 +337,7 @@ lines:
|
||||
state: known
|
||||
tags: [npc, kael, behavioral, dual-context]
|
||||
|
||||
- id: the-last-shift_m_d_044
|
||||
- id: pc-detective_m_d_032
|
||||
text: "Davan checks the door every time it opens. Even here. Even off-shift. That is not relaxation."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -357,7 +357,7 @@ lines:
|
||||
# 5-phase FRIEND arc: trust > background data > pattern > question >
|
||||
# contaminated trust. Contraction shifts mark objectivity loss.
|
||||
|
||||
- id: the-last-shift_m_d_031
|
||||
- id: pc-detective_m_d_033
|
||||
text: "Sera changed the subject when I mentioned manifests. Smooth. Practised, maybe."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -371,7 +371,7 @@ lines:
|
||||
priority: 6
|
||||
tags: [npc, sera, tell, behavioral, friend-arc]
|
||||
|
||||
- id: the-last-shift_m_d_032
|
||||
- id: pc-detective_m_d_034
|
||||
text: "She volunteers names, routines, context. Gives everything I ask for -- except what I did not."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -385,7 +385,7 @@ lines:
|
||||
priority: 6
|
||||
tags: [npc, sera, tell, overcompensation, friend-arc]
|
||||
|
||||
- id: the-last-shift_m_d_033
|
||||
- id: pc-detective_m_d_035
|
||||
text: "Sera at the Commission kiosk. Third pass this shift. That is not standard recalibration."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -399,7 +399,7 @@ lines:
|
||||
priority: 7
|
||||
tags: [npc, sera, tell, peripheral, friend-arc]
|
||||
|
||||
- id: the-last-shift_m_d_034
|
||||
- id: pc-detective_m_d_036
|
||||
text: "Everything Sera told me checks out. Layout, schedules, regulars. Accurate. Thorough. Selective?"
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -413,7 +413,7 @@ lines:
|
||||
priority: 5
|
||||
tags: [npc, sera, analytical, phase-2-3, friend-arc]
|
||||
|
||||
- id: the-last-shift_m_d_035
|
||||
- id: pc-detective_m_d_037
|
||||
text: "She mentioned Naia's worry about Kael early -- before I had context. Priming, or concern?"
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -427,7 +427,7 @@ lines:
|
||||
priority: 7
|
||||
tags: [npc, sera, naia, retrospective, friend-arc]
|
||||
|
||||
- id: the-last-shift_m_d_036
|
||||
- id: pc-detective_m_d_038
|
||||
text: "Sera's institutional access goes further than field calibration. She could pull records I cannot."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -440,7 +440,7 @@ lines:
|
||||
state: friendly
|
||||
tags: [npc, sera, analytical, capability, friend-arc]
|
||||
|
||||
- id: the-last-shift_m_d_037
|
||||
- id: pc-detective_m_d_039
|
||||
text: "Assessment: Venn, S. Cooperative. Embedded. Competent. Green -- with reservations now."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -454,7 +454,7 @@ lines:
|
||||
priority: 7
|
||||
tags: [npc, sera, phase-transition, analytical, friend-arc]
|
||||
|
||||
- id: the-last-shift_m_d_038
|
||||
- id: pc-detective_m_d_040
|
||||
text: "Correction: amber. The avoidance pattern is behavioural, not preferential. Data, not taste."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -468,7 +468,7 @@ lines:
|
||||
priority: 9
|
||||
tags: [npc, sera, phase-transition, tell, contradiction, friend-arc]
|
||||
|
||||
- id: the-last-shift_m_d_039
|
||||
- id: pc-detective_m_d_041
|
||||
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]
|
||||
@@ -482,7 +482,7 @@ lines:
|
||||
priority: 8
|
||||
tags: [npc, sera, emotional, phase-4-5, friend-arc]
|
||||
|
||||
- id: the-last-shift_m_d_040
|
||||
- id: pc-detective_m_d_042
|
||||
text: "Those early tips. The district layout, the introductions. Were those help -- or steering?"
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -496,7 +496,7 @@ lines:
|
||||
priority: 7
|
||||
tags: [npc, sera, contaminated-trust, retrospective, friend-arc]
|
||||
|
||||
- id: the-last-shift_m_d_041
|
||||
- id: pc-detective_m_d_043
|
||||
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]
|
||||
@@ -510,7 +510,7 @@ lines:
|
||||
priority: 8
|
||||
tags: [npc, sera, contaminated-trust, phase-4-5, friend-arc]
|
||||
|
||||
- id: the-last-shift_m_d_042
|
||||
- id: pc-detective_m_d_044
|
||||
text: "Trust: compromised. The question is not what she told me. It is what she did not."
|
||||
role: player_character
|
||||
access: [public]
|
||||
|
||||
@@ -3,7 +3,7 @@ location: the-terminal
|
||||
lines:
|
||||
# --- Arrival + Environmental Flavor ---
|
||||
|
||||
- id: the-terminal_m_d_001
|
||||
- id: pc-detective_m_d_001
|
||||
text: "Logistics hub. Standard prefab, heavy foot traffic. Let's see what the shift change tells me."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -12,7 +12,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [arrival, analytical]
|
||||
|
||||
- id: the-terminal_m_d_002
|
||||
- id: pc-detective_m_d_002
|
||||
text: "Sixteen bays, four loading arms, one manifest board. Everything routes through here."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -21,7 +21,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [arrival, orientation]
|
||||
|
||||
- id: the-terminal_m_d_003
|
||||
- id: pc-detective_m_d_003
|
||||
text: "Cargo lubricant and recycled air. The smell of honest work. Or a convincing imitation."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -30,7 +30,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [arrival, atmospheric]
|
||||
|
||||
- id: the-terminal_m_d_004
|
||||
- id: pc-detective_m_d_004
|
||||
text: "Loading arm three has a grind in its cycle. Maintenance deferred — budget, or negligence?"
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -39,7 +39,7 @@ lines:
|
||||
trigger: hear_sound
|
||||
tags: [environmental, analytical]
|
||||
|
||||
- id: the-terminal_m_d_005
|
||||
- id: pc-detective_m_d_005
|
||||
text: "Shift change in twenty minutes. The handover period is where oversight gets thin."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -48,7 +48,7 @@ lines:
|
||||
trigger: time_idle
|
||||
tags: [investigation, operational]
|
||||
|
||||
- id: the-terminal_m_d_006
|
||||
- id: pc-detective_m_d_006
|
||||
text: "The manifest board updates in real-time. Container movement, weight, destination. All logged."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -57,7 +57,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [environmental, orientation]
|
||||
|
||||
- id: the-terminal_m_d_007
|
||||
- id: pc-detective_m_d_007
|
||||
text: "Heavy foot traffic at shift change. Good — observation is easier in a crowd."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -66,7 +66,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [arrival, investigation]
|
||||
|
||||
- id: the-terminal_m_d_008
|
||||
- id: pc-detective_m_d_008
|
||||
text: "Containers stacked to regulation height in most bays. Bay four is the exception."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -75,7 +75,7 @@ lines:
|
||||
trigger: observe_anomaly
|
||||
tags: [environmental, investigation]
|
||||
|
||||
- id: the-terminal_m_d_009
|
||||
- id: pc-detective_m_d_009
|
||||
text: "Back at the hub. Different shift, different faces. Same manifest board."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -84,7 +84,7 @@ lines:
|
||||
trigger: return_visit
|
||||
tags: [arrival]
|
||||
|
||||
- id: the-terminal_m_d_010
|
||||
- id: pc-detective_m_d_010
|
||||
text: "The dock lights are running half-cycle. Saves power. Also reduces visibility."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -93,7 +93,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [environmental, analytical]
|
||||
|
||||
- id: the-terminal_m_d_011
|
||||
- id: pc-detective_m_d_011
|
||||
text: "Something in the air beyond the lubricant. Chemical, faint. Not standard freight residue."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -102,7 +102,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [environmental, investigation]
|
||||
|
||||
- id: the-terminal_m_d_012
|
||||
- id: pc-detective_m_d_012
|
||||
text: "Overhead conveyor running at capacity. The manifest says otherwise. Interesting."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -113,7 +113,7 @@ lines:
|
||||
|
||||
# --- NPC Commentary ---
|
||||
|
||||
- id: the-terminal_m_d_013
|
||||
- id: pc-detective_m_d_013
|
||||
text: "Shift supervisor Voss. Tense posture, checking the dock more than the schedule."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -122,7 +122,7 @@ lines:
|
||||
trigger: observe_npc
|
||||
tags: [npc, voss, behavioral]
|
||||
|
||||
- id: the-terminal_m_d_014
|
||||
- id: pc-detective_m_d_014
|
||||
text: "Korr, M. — freight scheduler. Efficient, focused. Either very good or very careful."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -131,7 +131,7 @@ lines:
|
||||
trigger: observe_npc
|
||||
tags: [npc, maret, analytical]
|
||||
|
||||
- id: the-terminal_m_d_015
|
||||
- id: pc-detective_m_d_015
|
||||
text: "Rosta, D. — dock inspector. Quick checks, minimal documentation. That's a pattern."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -140,7 +140,7 @@ lines:
|
||||
trigger: observe_npc
|
||||
tags: [npc, drin, investigation]
|
||||
|
||||
- id: the-terminal_m_d_016
|
||||
- id: pc-detective_m_d_016
|
||||
text: "Dock worker. Davan, K. Unremarkable on paper. But he knows the bay layout cold."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -149,7 +149,7 @@ lines:
|
||||
trigger: observe_npc
|
||||
tags: [npc, kael]
|
||||
|
||||
- id: the-terminal_m_d_017
|
||||
- id: pc-detective_m_d_017
|
||||
text: "Conversations stop when I pass. Standard response to Commission presence. Expected."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -158,7 +158,7 @@ lines:
|
||||
trigger: witness_interaction
|
||||
tags: [social, investigation]
|
||||
|
||||
- id: the-terminal_m_d_018
|
||||
- id: pc-detective_m_d_018
|
||||
text: "Two workers talking by bay six. One sees me, taps the other. Both go quiet."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -167,7 +167,7 @@ lines:
|
||||
trigger: witness_interaction
|
||||
tags: [social, investigation]
|
||||
|
||||
- id: the-terminal_m_d_019
|
||||
- id: pc-detective_m_d_019
|
||||
text: "Voss gave Davan a look across the bay. Brief. Loaded. Filing it."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -176,7 +176,7 @@ lines:
|
||||
trigger: witness_interaction
|
||||
tags: [npc, voss, kael, behavioral]
|
||||
|
||||
- id: the-terminal_m_d_020
|
||||
- id: pc-detective_m_d_020
|
||||
text: "The courier — Renn — moves fast. Efficient routing. Knows these corridors well."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -185,7 +185,7 @@ lines:
|
||||
trigger: observe_npc
|
||||
tags: [npc, renn, analytical]
|
||||
|
||||
- id: the-terminal_m_d_021
|
||||
- id: pc-detective_m_d_021
|
||||
text: "New shift starting. Different bodies, same avoidance radius around me."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -194,7 +194,7 @@ lines:
|
||||
trigger: observe_npc
|
||||
tags: [social, atmospheric]
|
||||
|
||||
- id: the-terminal_m_d_022
|
||||
- id: pc-detective_m_d_022
|
||||
text: "Rosta's inspection of bay four took forty seconds. The others: three minutes each."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -203,7 +203,7 @@ lines:
|
||||
trigger: observe_npc
|
||||
tags: [npc, drin, investigation, behavioral]
|
||||
|
||||
- id: the-terminal_m_d_023
|
||||
- id: pc-detective_m_d_023
|
||||
text: "Voss deflected when I asked about overnight containers. Smooth, but I caught it."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -212,7 +212,7 @@ lines:
|
||||
trigger: post_conversation
|
||||
tags: [npc, voss, investigation]
|
||||
|
||||
- id: the-terminal_m_d_024
|
||||
- id: pc-detective_m_d_024
|
||||
text: "Korr won't make eye contact. Not hostility — something else. Anxiety, maybe."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -223,7 +223,7 @@ lines:
|
||||
|
||||
# --- Knowledge-Gated Lines ---
|
||||
|
||||
- id: the-terminal_m_d_025
|
||||
- id: pc-detective_m_d_025
|
||||
text: "Davan's lattice activity spiked. Expecting a message? Or checking for surveillance?"
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -236,7 +236,7 @@ lines:
|
||||
state: known
|
||||
tags: [npc, kael, behavioral, tell]
|
||||
|
||||
- id: the-terminal_m_d_026
|
||||
- id: pc-detective_m_d_026
|
||||
text: "Container 4471. Logged for temp storage overnight. Turnaround should be sub-eight hours."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -250,7 +250,7 @@ lines:
|
||||
priority: 7
|
||||
tags: [investigation, evidence]
|
||||
|
||||
- id: the-terminal_m_d_027
|
||||
- id: pc-detective_m_d_027
|
||||
text: "Weight discrepancy on the bay four manifest. Point-three kilos over declared. Noted."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -263,7 +263,7 @@ lines:
|
||||
min_confidence: suspects
|
||||
tags: [investigation, evidence]
|
||||
|
||||
- id: the-terminal_m_d_028
|
||||
- id: pc-detective_m_d_028
|
||||
text: "Rosta's gambling debts are in the case file. Leverage, if I need it. Do I need it?"
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -276,7 +276,7 @@ lines:
|
||||
min_confidence: knows_details
|
||||
tags: [npc, drin, investigation, moral]
|
||||
|
||||
- id: the-terminal_m_d_029
|
||||
- id: pc-detective_m_d_029
|
||||
text: "Three names keep appearing near the anomalous manifests: Voss, Davan, Renn. Coincidence?"
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -290,7 +290,7 @@ lines:
|
||||
priority: 7
|
||||
tags: [investigation, analytical]
|
||||
|
||||
- id: the-terminal_m_d_030
|
||||
- id: pc-detective_m_d_030
|
||||
text: "The shift transition creates a twenty-minute gap in active oversight. Every cycle."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -304,7 +304,7 @@ lines:
|
||||
priority: 7
|
||||
tags: [investigation, operational]
|
||||
|
||||
- id: the-terminal_m_d_031
|
||||
- id: pc-detective_m_d_031
|
||||
text: "Korr changed the schedule after our conversation. Covering tracks, or just doing her job?"
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -318,7 +318,7 @@ lines:
|
||||
value: schedule_modification
|
||||
tags: [npc, maret, investigation]
|
||||
|
||||
- id: the-terminal_m_d_032
|
||||
- id: pc-detective_m_d_032
|
||||
text: "Rosta inspects everything else by the book. Bay four is the exception. That's the tell."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -332,7 +332,7 @@ lines:
|
||||
priority: 8
|
||||
tags: [npc, drin, investigation, behavioral]
|
||||
|
||||
- id: the-terminal_m_d_033
|
||||
- id: pc-detective_m_d_033
|
||||
text: "Davan's at an unusual terminal. Not his assigned bay. Why the move?"
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -345,7 +345,7 @@ lines:
|
||||
state: known
|
||||
tags: [npc, kael, investigation]
|
||||
|
||||
- id: the-terminal_m_d_034
|
||||
- id: pc-detective_m_d_034
|
||||
text: "Voss is watching me watch his people. He knows what I'm here for."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -359,7 +359,7 @@ lines:
|
||||
priority: 6
|
||||
tags: [npc, voss, investigation]
|
||||
|
||||
- id: the-terminal_m_d_035
|
||||
- id: pc-detective_m_d_035
|
||||
text: "The routing logs and the physical movement don't match. Someone's hand-editing."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -376,7 +376,7 @@ lines:
|
||||
# --- Kael Davan FRIEND Arc — Additional Detective Observations ---
|
||||
# Ticket: #297 review fix | Filling detective monologue gap (7 → 12 Kael lines)
|
||||
|
||||
- id: the-terminal_m_d_036
|
||||
- id: pc-detective_m_d_036
|
||||
text: "Davan's shift pattern is clean. Too clean. Same bays, same hours, same rotation. Nothing out of place."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -389,7 +389,7 @@ lines:
|
||||
state: known
|
||||
tags: [npc, kael, analytical, behavioral]
|
||||
|
||||
- id: the-terminal_m_d_037
|
||||
- id: pc-detective_m_d_037
|
||||
text: "Davan eats with the same group every shift. Social anchor, or keeping tabs?"
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -402,7 +402,7 @@ lines:
|
||||
state: known
|
||||
tags: [npc, kael, social, behavioral]
|
||||
|
||||
- id: the-terminal_m_d_038
|
||||
- id: pc-detective_m_d_038
|
||||
text: "Davan handled that container routing without checking the manifest board. He knows the schedule by heart."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -416,7 +416,7 @@ lines:
|
||||
priority: 6
|
||||
tags: [npc, kael, behavioral, tell, competence]
|
||||
|
||||
- id: the-terminal_m_d_039
|
||||
- id: pc-detective_m_d_039
|
||||
text: "Davan's posture changed when I approached bay four. Subtle. Filing it."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -430,7 +430,7 @@ lines:
|
||||
priority: 7
|
||||
tags: [npc, kael, tell, behavioral, friend-arc]
|
||||
|
||||
- id: the-terminal_m_d_040
|
||||
- id: pc-detective_m_d_040
|
||||
text: "Three manifests with weight discrepancies. All during Davan's shifts. Correlation is not causation. But noted."
|
||||
role: player_character
|
||||
access: [public]
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
character: detective
|
||||
location: tutorial
|
||||
lines:
|
||||
# Diegetic Tutorial Monologue — Ticket #330 — Detective
|
||||
# Authored per sprint-17 copy briefing and D-016 (internal monologue as core system).
|
||||
#
|
||||
# These lines teach mechanics through character voice — not UI instructions.
|
||||
# Distinct from opening.yaml (which covers the first 5 minutes and sets voice/motivation).
|
||||
# These fire on first-time events throughout gameplay, wherever they occur.
|
||||
#
|
||||
# Coverage:
|
||||
# Movement / exploration — first_move, first_new_section
|
||||
# Fog of perception — first_fog_encounter, first_fog_edge
|
||||
# Sound model — first_sound_heard, first_off_screen_sound
|
||||
# NPC interaction — first_npc_proximity, first_npc_face_read
|
||||
# Insert / HUD — first_insert_open, first_insert_contact_flag
|
||||
#
|
||||
# Voice: analytical, procedure-oriented, careful. No contractions in analytical mode.
|
||||
# Colon usage for categorization. 'Let's see what...' and 'Worth flagging' verbal tics.
|
||||
# ID discriminator: _tut_ per sprint briefing.
|
||||
# cooldown: omitted — fire-once enforced by trigger semantics (first_* triggers
|
||||
# fire once per game), not by a cooldown value.
|
||||
# priority: 8 (primary, one per trigger type), 6 (secondary variant, mood-weighted alternate).
|
||||
#
|
||||
# Schema corrections (Gestalt review, Sprint 17):
|
||||
# priority: string "tutorial" → integer. All other monologue files use integer priority;
|
||||
# string value would fail engine parse in monologue.rs.
|
||||
# situation: removed invalid values (movement, exploration) not in D-035 13-situation
|
||||
# enum. Replaced with arrival, routine per actual trigger context.
|
||||
#
|
||||
# Trigger types (first_move, first_new_section, first_fog_encounter, first_fog_edge,
|
||||
# first_sound_heard, first_off_screen_sound, first_npc_proximity, first_npc_face_read,
|
||||
# first_insert_open, first_insert_contact_flag) are extended trigger enum values not in
|
||||
# D-035 v0.1 set — require server-side implementation in monologue.rs.
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# MOVEMENT / EXPLORATION
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
- id: pc-detective_m_d_tut_001
|
||||
text: "Signage: section designators on the corridor wall. Commission-standard markings. Worth learning before the district learns me."
|
||||
role: player_character
|
||||
access: [public]
|
||||
trust: surface
|
||||
situation: [arrival, routine]
|
||||
trigger: first_move
|
||||
mood: [focused]
|
||||
priority: 8
|
||||
|
||||
tags: [tutorial, movement, navigation, analytical]
|
||||
notes: >
|
||||
Detective version of the 'check the signage' tutorial. Colon usage for the signage
|
||||
label (section 2.2 verbal pattern). 'Commission-standard markings' — institutional
|
||||
framing; he reads the environment through his professional lens first. 'Worth learning
|
||||
before the district learns me' — dry awareness of his asymmetric visibility in this
|
||||
community. Same teaching as the smuggler version, different cognitive frame.
|
||||
|
||||
- id: pc-detective_m_d_tut_002
|
||||
text: "Three sections in range from the main terminal. Commission briefing had the layout. Let's confirm it against the floor."
|
||||
role: player_character
|
||||
access: [public]
|
||||
trust: surface
|
||||
situation: [arrival, investigation]
|
||||
trigger: first_new_section
|
||||
mood: [focused]
|
||||
priority: 6
|
||||
|
||||
tags: [tutorial, movement, navigation, analytical]
|
||||
notes: >
|
||||
Fires on entering a new section for the first time. 'Commission briefing had the
|
||||
layout' — he arrived with a map, now he's verifying it against reality. 'Let's
|
||||
confirm it against the floor' — 'Let's' verbal tic, investigative method stated as
|
||||
principle: documentation is a starting point, not a substitute for observation.
|
||||
Teaches: the district has distinct sections to explore.
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# FOG OF PERCEPTION
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
- id: pc-detective_m_d_tut_003
|
||||
text: "Visual range terminates at the corridor junction. Consistent with district atmo settings. Anything past that point: unconfirmed."
|
||||
role: player_character
|
||||
access: [public]
|
||||
trust: surface
|
||||
situation: [observation]
|
||||
trigger: first_fog_encounter
|
||||
mood: [focused]
|
||||
priority: 8
|
||||
|
||||
tags: [tutorial, fog, perception, analytical]
|
||||
notes: >
|
||||
Detective fog tutorial — analytical register throughout. 'Visual range terminates at'
|
||||
instead of 'Can't see past.' 'Consistent with atmo settings' — he already knows this
|
||||
is an environmental property, not an anomaly. 'Anything past that point: unconfirmed'
|
||||
— colon usage, precise epistemic status. Teaches: perception boundary exists, and
|
||||
the detective categorizes it as an information state, not a barrier. Compare to
|
||||
smuggler's 'Might be worth checking' — same fog, different response.
|
||||
|
||||
- id: pc-detective_m_d_tut_004
|
||||
text: "Can't clear the far bay from here. Going around is the procedure — not a limitation, a working condition."
|
||||
role: player_character
|
||||
access: [public]
|
||||
trust: surface
|
||||
situation: [observation]
|
||||
trigger: first_fog_edge
|
||||
mood: [focused]
|
||||
priority: 6
|
||||
|
||||
tags: [tutorial, fog, perception, analytical]
|
||||
notes: >
|
||||
Second fog encounter line — fires at a fog zone boundary. 'Going around is the
|
||||
procedure' — detective frames the limitation as methodology. The second clause
|
||||
('not a limitation, a working condition') is self-instruction: he's telling himself
|
||||
not to treat fog as frustrating but as standard operating environment. Teaches:
|
||||
perception range is persistent throughout gameplay, and the correct response is
|
||||
movement, not waiting.
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# SOUND MODEL
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
- id: pc-detective_m_d_tut_005
|
||||
text: "Voices. Down the corridor. Source count indeterminate from here — distance degrades clarity."
|
||||
role: player_character
|
||||
access: [public]
|
||||
trust: surface
|
||||
situation: [observation]
|
||||
trigger: first_sound_heard
|
||||
mood: [focused]
|
||||
priority: 8
|
||||
|
||||
tags: [tutorial, sound, perception, analytical]
|
||||
notes: >
|
||||
Detective sound tutorial — analytical framing of the briefing's example. 'Source count
|
||||
indeterminate' is the precise version of 'can't tell how many.' 'Distance degrades
|
||||
clarity' — technical, causal. He hears, notes what he can establish (voices, direction)
|
||||
and what he can't (count, content). No contractions. Teaches: sound channel is active
|
||||
and distance-limited. Compare to smuggler's 'Can't make out the words' — same acoustic
|
||||
reality, systematized differently.
|
||||
|
||||
- id: pc-detective_m_d_tut_006
|
||||
text: "Footsteps. Metal-grated surface. Two sets minimum — moving away. Service access corridor, by the acoustic signature."
|
||||
role: player_character
|
||||
access: [public]
|
||||
trust: surface
|
||||
situation: [observation]
|
||||
trigger: first_off_screen_sound
|
||||
mood: [focused]
|
||||
priority: 6
|
||||
|
||||
tags: [tutorial, sound, perception, analytical]
|
||||
notes: >
|
||||
Second sound tutorial — fires on an off-screen audio source. The detective parses
|
||||
the sound through multiple analytical dimensions: surface type, count, direction,
|
||||
room type. 'Two sets minimum' — precise but not overclaiming. 'By the acoustic
|
||||
signature' is technical terminology applied to diegetic audio interpretation.
|
||||
Teaches: sound carries more than presence — floor type, distance, count, direction
|
||||
are all extractable. The detective extracts all of them automatically.
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# NPC INTERACTION
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
- id: pc-detective_m_d_tut_007
|
||||
text: "Civilians in range. Standard approach: observe before engaging. Let them surface what's relevant before directing the conversation."
|
||||
role: player_character
|
||||
access: [public]
|
||||
trust: surface
|
||||
situation: [social, observation]
|
||||
trigger: first_npc_proximity
|
||||
mood: [focused]
|
||||
priority: 8
|
||||
|
||||
tags: [tutorial, npc, interaction, analytical]
|
||||
notes: >
|
||||
Detective NPC tutorial — procedural, method-as-principle. 'Civilians in range' is
|
||||
his categorization (Commission-trained: everyone is a witness category until proven
|
||||
otherwise). 'Standard approach' — he has a protocol. 'Let them surface what's relevant'
|
||||
is the investigative principle: don't prime witnesses, let them self-select information.
|
||||
No contractions throughout. Teaches: NPC interaction is player-initiated and
|
||||
approach-sensitive. Different method than the smuggler ('give them reason to') —
|
||||
same outcome, different social model.
|
||||
|
||||
- id: pc-detective_m_d_tut_008
|
||||
text: "Commission credentials flag on approach. They know who I am before I speak. Worth noting who adjusts their behavior."
|
||||
role: player_character
|
||||
access: [public]
|
||||
trust: surface
|
||||
situation: [social, investigation]
|
||||
trigger: first_npc_face_read
|
||||
mood: [focused]
|
||||
priority: 6
|
||||
|
||||
tags: [tutorial, npc, observation, analytical, authority]
|
||||
notes: >
|
||||
Fires when the player observes an NPC at close range. The detective's asymmetry:
|
||||
his authority access tier makes him visible in ways the smuggler is not. NPCs react
|
||||
to his presence before interaction begins. 'Worth noting who adjusts their behavior'
|
||||
— 'Worth' verbal tic, sets up behavioral observation as investigation method. Teaches:
|
||||
the detective's Commission credentials affect NPC behavior, and that behavioral
|
||||
response is data.
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# INSERT / HUD
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
- id: pc-detective_m_d_tut_009
|
||||
text: "Lattice overlay active: time, contact positions, flagged entities. Commission-linked. Standard procedure to keep it running."
|
||||
role: player_character
|
||||
access: [public]
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
trigger: first_insert_open
|
||||
mood: [focused]
|
||||
priority: 8
|
||||
|
||||
tags: [tutorial, insert, hud, analytical]
|
||||
notes: >
|
||||
Detective insert tutorial. Colon usage: catalogs what the overlay contains. 'Commission-
|
||||
linked' — institutional framing, establishes why the detective's HUD is denser than
|
||||
the smuggler's. 'Standard procedure' — not a discovery, a protocol. Teaches: the
|
||||
insert is an active investigation tool, not optional. Compare to smuggler's 'Should
|
||||
show the time and nearby contacts' — same overlay, presented as procedural requirement
|
||||
vs. operational check.
|
||||
|
||||
- id: pc-detective_m_d_tut_010
|
||||
text: "Nine identity flags in range. Cross-referencing case file now. Let's see what matches."
|
||||
role: player_character
|
||||
access: [public]
|
||||
trust: surface
|
||||
situation: [investigation, routine]
|
||||
trigger: first_insert_contact_flag
|
||||
mood: [focused]
|
||||
priority: 6
|
||||
|
||||
tags: [tutorial, insert, hud, lattice, investigation]
|
||||
notes: >
|
||||
Fires when the insert first populates with flagged entities. Count given precisely
|
||||
('Nine identity flags'). 'Case file' — the detective's overlay cross-references a
|
||||
pre-loaded investigation file, not just a general registry. 'Let's see what matches'
|
||||
— 'Let's see what' verbal tic closing the tutorial sequence. Teaches: the detective's
|
||||
insert is investigation-keyed, matching observed faces against case file entries.
|
||||
This is why his overlay is analytically denser than the smuggler's.
|
||||
@@ -0,0 +1,228 @@
|
||||
character: smuggler
|
||||
location: contradiction
|
||||
lines:
|
||||
# Contradiction Monologue — Ticket #552 — Sera/Kael FRIEND Arc (Smuggler perspective)
|
||||
# Authored per D-083 (contradiction detection pipeline) and Paula Round 2 Mellanie spec
|
||||
# (docs/workshops/knowledge-flow-npc-boundaries/paula-round2.md, lines 117-158).
|
||||
#
|
||||
# Trigger: contradiction_detected fires when the smuggler observes Kael in Corridor B-7
|
||||
# with an unrecognized contact — contradicting Kael's stated location and ring protocol.
|
||||
# Event payload: source_display_name = "Kael Davan", subject_display_name = "Kael Davan"
|
||||
# (Kael is both source and subject: he placed himself elsewhere, smuggler sees him here.)
|
||||
#
|
||||
# FRIEND-pattern NPCs always use hand-authored lines — generic template fallback
|
||||
# does not fire for this arc.
|
||||
#
|
||||
# Authoring principle: cognitive dissonance, not accusation. The smuggler reads social
|
||||
# signals instinctively, but doesn't conclude — she contains the alarm. Street-smart
|
||||
# gut reaction, not analytical procedure.
|
||||
#
|
||||
# Phase 2 lines: first observation — Kael in B-7 with unknown contact.
|
||||
# Prerequisite: KG entry for Kael has state: Contradicted, source: ToldBy(Kael Davan)
|
||||
# Phase 3 lines: behavioral pattern already accumulating — increased lattice checking,
|
||||
# shortened interactions, early exits from break room.
|
||||
# Prerequisite: Phase 2 + Kael's behavioral anomalies observed (count >= 2).
|
||||
#
|
||||
# ID discriminator: _con_ marks contradiction lines per Q-028 pending resolution.
|
||||
# cooldown: omitted — fire-once enforced by trigger semantics (contradiction_detected
|
||||
# fires once per KG state change), not by a cooldown value.
|
||||
#
|
||||
# Note: contradiction_detected is a new trigger type — requires server-side implementation
|
||||
# in monologue.rs (ticket #550). Secondary beat lines use lower priority for sequencing;
|
||||
# server team should implement delay_after_trigger_ticks for proper 3-5 second gap.
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# PHASE 2 — Primary beat (fires on contradiction_detected, immediate)
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
- id: pc-smuggler_m_s_con_001
|
||||
text: "Kael said second shift was clear. He's in B-7. With someone I don't know."
|
||||
role: player_character
|
||||
access: [public]
|
||||
trust: surface
|
||||
situation: [investigation, observation]
|
||||
trigger: contradiction_detected
|
||||
mood: [focused, anxious]
|
||||
priority: 10
|
||||
|
||||
prerequisites:
|
||||
entities:
|
||||
- id: "npc:kael-davan"
|
||||
state: Contradicted
|
||||
tags: [contradiction, friend-arc, kael, phase-2, operational]
|
||||
notes: >
|
||||
Primary line — three beats, three facts. 'Kael said' names the source (friend register,
|
||||
first name). 'He's in B-7' is the observation. 'With someone I don't know' is the alarm:
|
||||
not just wrong location, but outside the ring. Fragment structure throughout — the smuggler
|
||||
clocks a scene in pieces. No accusation, no conclusion: just inventory of what's wrong.
|
||||
|
||||
- id: pc-smuggler_m_s_con_002
|
||||
text: "That's Kael. Restricted corridor. Unrecognized contact. Kael doesn't come to B-7."
|
||||
role: player_character
|
||||
access: [public]
|
||||
trust: surface
|
||||
situation: [investigation, observation]
|
||||
trigger: contradiction_detected
|
||||
mood: [focused]
|
||||
priority: 9
|
||||
|
||||
prerequisites:
|
||||
entities:
|
||||
- id: "npc:kael-davan"
|
||||
state: Contradicted
|
||||
tags: [contradiction, friend-arc, kael, phase-2, operational]
|
||||
notes: >
|
||||
Alternate primary — catalog format. 'That's Kael' opens with identification, not alarm:
|
||||
recognition first, then the wrong details fall into place. Each fragment adds one layer
|
||||
of what's wrong. Close: 'Kael doesn't come to B-7' — negative statement of prior
|
||||
expectation, which is now violated. No contraction in the closing declarative (emphasis).
|
||||
|
||||
- id: pc-smuggler_m_s_con_003
|
||||
text: "He's in B-7. He told me he'd be heading home after shift. One of those isn't the case."
|
||||
role: player_character
|
||||
access: [public]
|
||||
trust: surface
|
||||
situation: [investigation, observation]
|
||||
trigger: contradiction_detected
|
||||
mood: [focused, anxious]
|
||||
priority: 9
|
||||
|
||||
prerequisites:
|
||||
entities:
|
||||
- id: "npc:kael-davan"
|
||||
state: Contradicted
|
||||
tags: [contradiction, friend-arc, kael, phase-2]
|
||||
notes: >
|
||||
Alternate primary — echoes the Paula canonical structure (observation / prior claim /
|
||||
contradiction stated) but in the smuggler's voice. 'He told me' is personal register
|
||||
(friend, not colleague). 'One of those isn't the case' — the smuggler's understated
|
||||
version of flagging a contradiction: not 'one of them is a lie' but 'isn't the case.'
|
||||
Epistemic containment in plain language.
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# PHASE 2 — Secondary beat (gut-level recalibration, operational containment)
|
||||
# Server team (#550): implement delay_after_trigger_ticks: 90 for 3-5s gap.
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
- id: pc-smuggler_m_s_con_004
|
||||
text: "Maybe there's a reason. There's always a reason. But Kael knows better than to run a meeting in a maintenance corridor without telling me."
|
||||
role: player_character
|
||||
access: [public]
|
||||
trust: surface
|
||||
situation: [investigation, alone]
|
||||
trigger: contradiction_detected
|
||||
mood: [focused, anxious]
|
||||
priority: 7
|
||||
|
||||
prerequisites:
|
||||
entities:
|
||||
- id: "npc:kael-davan"
|
||||
state: Contradicted
|
||||
tags: [contradiction, friend-arc, kael, phase-2, cognitive-dissonance, secondary-beat]
|
||||
notes: >
|
||||
Secondary beat — fires 3-5 seconds after primary. Self-challenge first: the smuggler
|
||||
gives Kael benefit of the doubt, then immediately walks it back with operational
|
||||
logic. 'There's always a reason' is the street-smart equivalent of epistemic
|
||||
containment — she's seen strange things before. The closing line is where the alarm
|
||||
lives: this isn't strange, it's a protocol violation.
|
||||
Lower priority (7) ensures primary lines fire first; server team should implement
|
||||
delay_after_trigger_ticks for proper sequencing.
|
||||
|
||||
- id: pc-smuggler_m_s_con_005
|
||||
text: "Don't assume. Could be he's running something for Nils I wasn't told about. Could be something else. The second thing is the problem."
|
||||
role: player_character
|
||||
access: [public]
|
||||
trust: surface
|
||||
situation: [investigation, alone]
|
||||
trigger: contradiction_detected
|
||||
mood: [focused]
|
||||
priority: 6
|
||||
|
||||
prerequisites:
|
||||
entities:
|
||||
- id: "npc:kael-davan"
|
||||
state: Contradicted
|
||||
tags: [contradiction, friend-arc, kael, phase-2, operational, cognitive-dissonance, secondary-beat]
|
||||
notes: >
|
||||
Alternate secondary beat. 'Don't assume' is the smuggler's self-instruction (operational
|
||||
discipline). Two possibilities held open: ring-authorized (names Nils — the coordinator)
|
||||
vs. outside the ring. 'The second thing is the problem' — the alarm, stated as dry
|
||||
understatement. Operational logic applied to an emotional moment: the smuggler
|
||||
keeps the processing clean even when the stakes are personal.
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# PHASE 3 — Behavioral pattern accumulated (increased lattice checking, early exits)
|
||||
# Requires: Kael behavioral anomalies observed prior to the B-7 contradiction.
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
- id: pc-smuggler_m_s_con_006
|
||||
text: "He's been distracted for three days. Lattice checks every few minutes, leaving lunch early. Now a B-7 meeting with someone outside the ring. Kael's working an angle he hasn't told me about."
|
||||
role: player_character
|
||||
access: [public]
|
||||
trust: surface
|
||||
situation: [investigation, observation]
|
||||
trigger: contradiction_detected
|
||||
mood: [focused, anxious]
|
||||
priority: 10
|
||||
|
||||
prerequisites:
|
||||
entities:
|
||||
- id: "npc:kael-davan"
|
||||
state: Contradicted
|
||||
tells_observed: 2
|
||||
tags: [contradiction, friend-arc, kael, phase-3, pattern-recognition, operational]
|
||||
notes: >
|
||||
Phase 3 primary — connects all accumulated behavioral data to the contradiction. Fragment
|
||||
catalog builds the picture: three days of signals, then this. Closing line names the
|
||||
conclusion without naming its implication: 'working an angle' is street-smart language
|
||||
for running something unauthorized, but the smuggler doesn't say 'exit attempt' —
|
||||
she doesn't know that yet.
|
||||
Note: tells_observed is a proposed prerequisite field — server team (#550) to implement
|
||||
or simplify to a different check.
|
||||
|
||||
- id: pc-smuggler_m_s_con_007
|
||||
text: "Kael doesn't run his own contacts. Devra handles external. So either Devra knows about this, or Kael's outside protocol. Neither's good."
|
||||
role: player_character
|
||||
access: [public]
|
||||
trust: surface
|
||||
situation: [investigation, observation]
|
||||
trigger: contradiction_detected
|
||||
mood: [focused]
|
||||
priority: 9
|
||||
|
||||
prerequisites:
|
||||
entities:
|
||||
- id: "npc:kael-davan"
|
||||
state: Contradicted
|
||||
tells_observed: 2
|
||||
tags: [contradiction, friend-arc, kael, phase-3, operational]
|
||||
notes: >
|
||||
Alternate Phase 3 — operational knowledge applied. The smuggler knows ring structure:
|
||||
Devra manages external contacts, Kael handles cargo logistics. An unrecognized contact
|
||||
means either Devra is running something Kael's involved in (plausible, alarming) or
|
||||
Kael went outside protocol (worse). Both possibilities named, neither closed. 'Neither's
|
||||
good' — the smuggler's understated close for a significant threat read.
|
||||
|
||||
- id: pc-smuggler_m_s_con_008
|
||||
text: "He's been somewhere else in his head all week. Now I know why. Whoever that was — not ring. Kael's looking for a way out."
|
||||
role: player_character
|
||||
access: [public]
|
||||
trust: surface
|
||||
situation: [investigation, observation]
|
||||
trigger: contradiction_detected
|
||||
mood: [anxious]
|
||||
priority: 8
|
||||
|
||||
prerequisites:
|
||||
entities:
|
||||
- id: "npc:kael-davan"
|
||||
state: Contradicted
|
||||
tells_observed: 2
|
||||
tags: [contradiction, friend-arc, kael, phase-3, intuition]
|
||||
notes: >
|
||||
Alternate Phase 3 — gut read, highest emotional weight. 'Somewhere else in his head'
|
||||
describes the behavioral changes without cataloging them. 'Now I know why' — the click
|
||||
of understanding arriving. 'Not ring' — short, certain, two words. 'Looking for a way
|
||||
out' is street-smart language for exit attempt: the smuggler doesn't know the details
|
||||
but the read is right. This line is the closest the smuggler comes to naming what's
|
||||
happening — and she still doesn't say it explicitly.
|
||||
@@ -1,7 +1,7 @@
|
||||
character: smuggler
|
||||
location: general
|
||||
lines:
|
||||
- id: general_m_s_001
|
||||
- id: pc-smuggler_m_s_001
|
||||
text: "Another day on Sova. Same air, same hum, same faces."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -10,7 +10,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [arrival, atmospheric]
|
||||
|
||||
- id: general_m_s_002
|
||||
- id: pc-smuggler_m_s_002
|
||||
text: "The span gate hum's louder today. Maintenance hasn't touched it in weeks."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -19,7 +19,7 @@ lines:
|
||||
trigger: hear_sound
|
||||
tags: [environmental]
|
||||
|
||||
- id: general_m_s_003
|
||||
- id: pc-smuggler_m_s_003
|
||||
text: "Twenty minutes between shifts. That's the window. Always has been."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -28,7 +28,7 @@ lines:
|
||||
trigger: time_idle
|
||||
tags: [operational, orientation]
|
||||
|
||||
- id: general_m_s_004
|
||||
- id: pc-smuggler_m_s_004
|
||||
text: "People talk about leaving Sova. Nobody actually does."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -37,7 +37,7 @@ lines:
|
||||
trigger: time_idle
|
||||
tags: [reflection]
|
||||
|
||||
- id: general_m_s_005
|
||||
- id: pc-smuggler_m_s_005
|
||||
text: "Two years of this. Still not used to the gate vibration in my teeth."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -46,7 +46,7 @@ lines:
|
||||
trigger: hear_sound
|
||||
tags: [environmental, personal]
|
||||
|
||||
- id: general_m_s_006
|
||||
- id: pc-smuggler_m_s_006
|
||||
text: "The money's good. The risk's manageable. Keep telling yourself that."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -55,7 +55,7 @@ lines:
|
||||
trigger: time_idle
|
||||
tags: [reflection, operational]
|
||||
|
||||
- id: general_m_s_007
|
||||
- id: pc-smuggler_m_s_007
|
||||
text: "Back again. Same corridors, same shortcuts, same reasons."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -64,7 +64,7 @@ lines:
|
||||
trigger: return_visit
|
||||
tags: [arrival]
|
||||
|
||||
- id: general_m_s_008
|
||||
- id: pc-smuggler_m_s_008
|
||||
text: "Commission patrols are thinner this cycle. Good or bad — hard to say."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -73,7 +73,7 @@ lines:
|
||||
trigger: observe_anomaly
|
||||
tags: [operational, caution]
|
||||
|
||||
- id: general_m_s_009
|
||||
- id: pc-smuggler_m_s_009
|
||||
text: "If anyone asks, I'm clocking overtime. That's always the story."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -82,7 +82,7 @@ lines:
|
||||
trigger: time_idle
|
||||
tags: [operational, cover]
|
||||
|
||||
- id: general_m_s_010
|
||||
- id: pc-smuggler_m_s_010
|
||||
text: "Recycled air tastes different near the outer ring. Sharper. More machine."
|
||||
role: player_character
|
||||
access: [public]
|
||||
|
||||
@@ -3,7 +3,7 @@ location: maintenance-corridors
|
||||
lines:
|
||||
# --- Arrival + Environmental Flavor ---
|
||||
|
||||
- id: maintenance-corridors_m_s_001
|
||||
- id: pc-smuggler_m_s_001
|
||||
text: "Service corridor. Dim lights, damp walls, and the constant drip of condensation."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -12,7 +12,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [arrival, atmospheric]
|
||||
|
||||
- id: maintenance-corridors_m_s_002
|
||||
- id: pc-smuggler_m_s_002
|
||||
text: "Cooler down here. The ventilation doesn't reach this far."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -21,7 +21,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [environmental, atmospheric]
|
||||
|
||||
- id: maintenance-corridors_m_s_003
|
||||
- id: pc-smuggler_m_s_003
|
||||
text: "The access panel's still loose. Good — our route's intact."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -30,7 +30,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [arrival, operational]
|
||||
|
||||
- id: maintenance-corridors_m_s_004
|
||||
- id: pc-smuggler_m_s_004
|
||||
text: "Pipes groaning overhead. Station's old. These corridors are older."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -39,7 +39,7 @@ lines:
|
||||
trigger: hear_sound
|
||||
tags: [environmental, atmospheric]
|
||||
|
||||
- id: maintenance-corridors_m_s_005
|
||||
- id: pc-smuggler_m_s_005
|
||||
text: "Water recycling rumbles through the walls down here. Constant."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -48,7 +48,7 @@ lines:
|
||||
trigger: hear_sound
|
||||
tags: [environmental]
|
||||
|
||||
- id: maintenance-corridors_m_s_006
|
||||
- id: pc-smuggler_m_s_006
|
||||
text: "Back in the maintenance tunnels. Rust and coolant."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -57,7 +57,7 @@ lines:
|
||||
trigger: return_visit
|
||||
tags: [arrival, atmospheric]
|
||||
|
||||
- id: maintenance-corridors_m_s_007
|
||||
- id: pc-smuggler_m_s_007
|
||||
text: "Emergency lighting only. Just enough to see where you're going."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -66,7 +66,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [environmental]
|
||||
|
||||
- id: maintenance-corridors_m_s_008
|
||||
- id: pc-smuggler_m_s_008
|
||||
text: "Footsteps echo different down here. You learn to listen."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -77,7 +77,7 @@ lines:
|
||||
|
||||
# --- NPC + Operational ---
|
||||
|
||||
- id: maintenance-corridors_m_s_009
|
||||
- id: pc-smuggler_m_s_009
|
||||
text: "Renn's chalk marks on the wall. Route's been used recently."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -86,7 +86,7 @@ lines:
|
||||
trigger: observe_anomaly
|
||||
tags: [operational, renn]
|
||||
|
||||
- id: maintenance-corridors_m_s_010
|
||||
- id: pc-smuggler_m_s_010
|
||||
text: "Someone left a crate at junction B-4. Not one of ours."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -95,7 +95,7 @@ lines:
|
||||
trigger: observe_anomaly
|
||||
tags: [operational, caution]
|
||||
|
||||
- id: maintenance-corridors_m_s_011
|
||||
- id: pc-smuggler_m_s_011
|
||||
text: "The temp storage hatch is warm. Cargo cycled through recently."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -104,7 +104,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [operational]
|
||||
|
||||
- id: maintenance-corridors_m_s_012
|
||||
- id: pc-smuggler_m_s_012
|
||||
text: "Scratches on the lock plate. Someone's been forcing this access point."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -113,7 +113,7 @@ lines:
|
||||
trigger: observe_anomaly
|
||||
tags: [environmental, caution]
|
||||
|
||||
- id: maintenance-corridors_m_s_013
|
||||
- id: pc-smuggler_m_s_013
|
||||
text: "Timer says twelve minutes until the oversight gap closes. Plenty."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -122,7 +122,7 @@ lines:
|
||||
trigger: time_idle
|
||||
tags: [operational]
|
||||
|
||||
- id: maintenance-corridors_m_s_014
|
||||
- id: pc-smuggler_m_s_014
|
||||
text: "That's Renn's jacket on the pipe junction. He was here."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -131,7 +131,7 @@ lines:
|
||||
trigger: observe_anomaly
|
||||
tags: [npc, renn, operational]
|
||||
|
||||
- id: maintenance-corridors_m_s_015
|
||||
- id: pc-smuggler_m_s_015
|
||||
text: "Voices echoing from the junction. Can't make out words yet."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -140,7 +140,7 @@ lines:
|
||||
trigger: hear_sound
|
||||
tags: [caution]
|
||||
|
||||
- id: maintenance-corridors_m_s_016
|
||||
- id: pc-smuggler_m_s_016
|
||||
text: "The route through B-section is clear. Voss kept his word."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -151,7 +151,7 @@ lines:
|
||||
|
||||
# --- Knowledge-Gated Lines ---
|
||||
|
||||
- id: maintenance-corridors_m_s_017
|
||||
- id: pc-smuggler_m_s_017
|
||||
text: "Corridor B-7. This is where I saw Kael. With someone I don't know."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -165,7 +165,7 @@ lines:
|
||||
priority: 8
|
||||
tags: [npc, kael, contradiction, friend-arc]
|
||||
|
||||
- id: maintenance-corridors_m_s_018
|
||||
- id: pc-smuggler_m_s_018
|
||||
text: "Fresh boot prints in the dust. Two sets. Different sizes."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -178,7 +178,7 @@ lines:
|
||||
min_confidence: suspects
|
||||
tags: [evidence, kael, friend-arc]
|
||||
|
||||
- id: maintenance-corridors_m_s_019
|
||||
- id: pc-smuggler_m_s_019
|
||||
text: "New scratch marks on the service hatch. Someone's using our route."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -192,7 +192,7 @@ lines:
|
||||
priority: 7
|
||||
tags: [operational, caution]
|
||||
|
||||
- id: maintenance-corridors_m_s_020
|
||||
- id: pc-smuggler_m_s_020
|
||||
text: "The container in temp was supposed to move last shift. It didn't. Why?"
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -205,7 +205,7 @@ lines:
|
||||
min_confidence: knows_of
|
||||
tags: [operational, contraband]
|
||||
|
||||
- id: maintenance-corridors_m_s_021
|
||||
- id: pc-smuggler_m_s_021
|
||||
text: "Camera's been repositioned at junction C-2. Who authorized that?"
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -219,7 +219,7 @@ lines:
|
||||
priority: 7
|
||||
tags: [investigation, caution]
|
||||
|
||||
- id: maintenance-corridors_m_s_022
|
||||
- id: pc-smuggler_m_s_022
|
||||
text: "If Kael's meeting people down here without telling us, the whole route's exposed."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -233,7 +233,7 @@ lines:
|
||||
priority: 8
|
||||
tags: [npc, kael, operational, friend-arc]
|
||||
|
||||
- id: maintenance-corridors_m_s_023
|
||||
- id: pc-smuggler_m_s_023
|
||||
text: "Sealed containers don't end up in temp storage by accident. Someone rerouted."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -246,7 +246,7 @@ lines:
|
||||
min_confidence: knows_details
|
||||
tags: [operational, contraband]
|
||||
|
||||
- id: maintenance-corridors_m_s_024
|
||||
- id: pc-smuggler_m_s_024
|
||||
text: "Kael's not answering his lattice. He was supposed to meet me here."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -260,7 +260,7 @@ lines:
|
||||
priority: 7
|
||||
tags: [npc, kael, concern, friend-arc]
|
||||
|
||||
- id: maintenance-corridors_m_s_025
|
||||
- id: pc-smuggler_m_s_025
|
||||
text: "Someone wiped the dust off this hatch. Recently. Carefully."
|
||||
role: player_character
|
||||
access: [public]
|
||||
|
||||
@@ -8,7 +8,7 @@ lines:
|
||||
# Arc: station hum → spatial count → fog boundary → thin crowd → Kael sighting ×2 → news ticker → sound ping → time countdown → settle ×2.
|
||||
# All lines fully schema-compliant per D-035 (role, access, trust, situation present).
|
||||
|
||||
- id: opening_m_s_001
|
||||
- id: pc-smuggler_m_s_001
|
||||
text: "Station hum. B-flat, steady — freight's moving."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -21,7 +21,7 @@ lines:
|
||||
tags: [opening-hook, tutorial-arc, atmospheric]
|
||||
notes: "Opening sensation. Station audio registers first — the hum tells her freight is running. Teaches: audio environment carries operational information (D-074)."
|
||||
|
||||
- id: opening_m_s_002
|
||||
- id: pc-smuggler_m_s_002
|
||||
text: "Sixteen bays, three arms running. Standard morning."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -34,7 +34,7 @@ lines:
|
||||
tags: [opening-hook, tutorial-arc, orientation]
|
||||
notes: "Spatial orientation. She clocks the floor in two beats — count of bays, count of running arms. Everything in fragments. Teaches: the terminal layout."
|
||||
|
||||
- id: opening_m_s_003
|
||||
- id: pc-smuggler_m_s_003
|
||||
text: "Haze past bay eight today. Can't see the far dock from here."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -47,7 +47,7 @@ lines:
|
||||
tags: [opening-hook, tutorial-arc, fog, perception]
|
||||
notes: "Fog boundary. She notices the edge of her vision as a fact of the floor, not a mechanic. Teaches: perception range has limits (D-015, D-046)."
|
||||
|
||||
- id: opening_m_s_004
|
||||
- id: pc-smuggler_m_s_004
|
||||
text: "Morning crew's spread thin. Good. Room to move."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -60,7 +60,7 @@ lines:
|
||||
tags: [opening-hook, tutorial-arc]
|
||||
notes: "Light crowd = operational comfort. 'Good.' standalone verbal tic (section 1.7). Teaches: NPC density affects movement options."
|
||||
|
||||
- id: opening_m_s_005
|
||||
- id: pc-smuggler_m_s_005
|
||||
text: "Kael's at dock three. Right where he should be."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -73,7 +73,7 @@ lines:
|
||||
tags: [opening-hook, tutorial-arc, kael, friend-arc]
|
||||
notes: "First Kael sighting. Warm, immediate — she locates him before she finishes reading the floor. Teaches: named NPCs are identifiable via insert overlay (D-033 green entity)."
|
||||
|
||||
- id: opening_m_s_006
|
||||
- id: pc-smuggler_m_s_006
|
||||
text: "Already has his clipboard. Day's organized before it starts. Two years, same routine."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -86,7 +86,7 @@ lines:
|
||||
tags: [opening-hook, tutorial-arc, kael, friend-arc]
|
||||
notes: "Kael warmth through behavior-reading, not labeling. Three beats: observation → interpretation → familiarity. 'Two years, same routine' says she knows his patterns intimately. Establishes: Kael is trusted, competent, reliable — the baseline before the friend arc erodes it."
|
||||
|
||||
- id: opening_m_s_007
|
||||
- id: pc-smuggler_m_s_007
|
||||
text: "Bulletin board's got a new Commission notice. Lattice registration updates. Not today's problem."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -99,7 +99,7 @@ lines:
|
||||
tags: [opening-hook, tutorial-arc, news-ticker]
|
||||
notes: "News ticker glance. She reads and dismisses it. The Commission exists as background noise — relevant only when it stops being ignorable. Teaches: environmental text exists, player can engage or skip. D-037 irony: lattice registration is exactly what the ring circumvents."
|
||||
|
||||
- id: opening_m_s_008
|
||||
- id: pc-smuggler_m_s_008
|
||||
text: "Span gate echo from dock fourteen. Something inbound, running heavy by the sound of it."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -112,7 +112,7 @@ lines:
|
||||
tags: [opening-hook, tutorial-arc, sound, perception]
|
||||
notes: "First sound ping at the edge of range. She interprets pitch instantly — the echo tells her load weight. Teaches: sound carries operational information (D-018, D-074 sound model). Sound channel is meaningful."
|
||||
|
||||
- id: opening_m_s_009
|
||||
- id: pc-smuggler_m_s_009
|
||||
text: "Shift board says light cargo today. Fifty minutes before Voss does his rounds."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -125,7 +125,7 @@ lines:
|
||||
tags: [opening-hook, tutorial-arc, operational]
|
||||
notes: "'Fifty minutes' = time-as-countdown verbal tic (section 1.7). She's already calculating windows. Teaches: time and NPC patrol rhythms are trackable."
|
||||
|
||||
- id: opening_m_s_010
|
||||
- id: pc-smuggler_m_s_010
|
||||
text: "Same floor, same faces, same hum. Could be worse."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -138,7 +138,7 @@ lines:
|
||||
tags: [opening-hook, tutorial-arc, atmospheric]
|
||||
notes: "Settling line. Baseline contentment. Parallel three-beat structure then understatement. 'Could be worse' is her comfort language — not enthusiastic, just settled. Voice anchor for confidence register (section 1.8)."
|
||||
|
||||
- id: opening_m_s_011
|
||||
- id: pc-smuggler_m_s_011
|
||||
text: "Dock's clean this morning. Good start."
|
||||
role: player_character
|
||||
access: [public]
|
||||
|
||||
@@ -12,7 +12,7 @@ lines:
|
||||
|
||||
# --- Tell Observations (5 core tells from pc-as-npc-spec.md section 8) ---
|
||||
|
||||
- id: general_m_s_011
|
||||
- id: pc-smuggler_m_s_001
|
||||
text: "Commission officer just stood there after talking to Voss. Making notes. Not good."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -25,7 +25,7 @@ lines:
|
||||
state: known
|
||||
tags: [npc, pc-detective, tell, note-taking, caution]
|
||||
|
||||
- id: general_m_s_012
|
||||
- id: pc-smuggler_m_s_002
|
||||
text: "That investigator is working the hub section by section. Not browsing. Searching."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -38,7 +38,7 @@ lines:
|
||||
state: known
|
||||
tags: [npc, pc-detective, tell, systematic-movement, caution]
|
||||
|
||||
- id: general_m_s_013
|
||||
- id: pc-smuggler_m_s_003
|
||||
text: "Commission's parked at the manifest terminal again. Third time today."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -51,7 +51,7 @@ lines:
|
||||
state: known
|
||||
tags: [npc, pc-detective, tell, manifest-fixation, caution]
|
||||
|
||||
- id: general_m_s_014
|
||||
- id: pc-smuggler_m_s_004
|
||||
text: "They're watching. From the corner, where they can see the whole dock floor. Professional."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -64,7 +64,7 @@ lines:
|
||||
state: known
|
||||
tags: [npc, pc-detective, tell, observation-positioning, caution]
|
||||
|
||||
- id: general_m_s_015
|
||||
- id: pc-smuggler_m_s_005
|
||||
text: "The investigator and Sera again. Old friends? Or is Sera reporting to them?"
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -82,7 +82,7 @@ lines:
|
||||
|
||||
# --- Operational Awareness (smuggler's threat assessment) ---
|
||||
|
||||
- id: general_m_s_016
|
||||
- id: pc-smuggler_m_s_006
|
||||
text: "New Commission face at the hub. Keep your head down. Don't give them anything."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -96,7 +96,7 @@ lines:
|
||||
priority: 8
|
||||
tags: [npc, pc-detective, operational, caution, first-sighting]
|
||||
|
||||
- id: general_m_s_017
|
||||
- id: pc-smuggler_m_s_007
|
||||
text: "That investigator's been here all morning. Nobody stays that long for a routine check."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -110,7 +110,7 @@ lines:
|
||||
priority: 6
|
||||
tags: [npc, pc-detective, suspicion, operational]
|
||||
|
||||
- id: general_m_s_018
|
||||
- id: pc-smuggler_m_s_008
|
||||
text: "Commission's at the bar. Sitting with Sera. Watching the room like they own it."
|
||||
role: player_character
|
||||
access: [public]
|
||||
|
||||
@@ -23,7 +23,7 @@ lines:
|
||||
|
||||
# --- Arrival ---
|
||||
|
||||
- id: smuggling-hold_m_s_001
|
||||
- id: pc-smuggler_m_s_001
|
||||
text: "Sub-level. The kind of space nobody finds unless you know where to look."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -32,7 +32,7 @@ lines:
|
||||
situation: [arrival, routine]
|
||||
tags: [arrival, atmospheric, operational]
|
||||
|
||||
- id: smuggling-hold_m_s_002
|
||||
- id: pc-smuggler_m_s_002
|
||||
text: "Coolant smell. No ventilation down here — just recycled air and waiting."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -41,7 +41,7 @@ lines:
|
||||
situation: [arrival, routine]
|
||||
tags: [arrival, atmospheric, sensory]
|
||||
|
||||
- id: smuggling-hold_m_s_003
|
||||
- id: pc-smuggler_m_s_003
|
||||
text: "Access hatch sealed from the inside. Good. Route's still ours."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -50,7 +50,7 @@ lines:
|
||||
situation: [arrival, routine]
|
||||
tags: [arrival, operational]
|
||||
|
||||
- id: smuggling-hold_m_s_004
|
||||
- id: pc-smuggler_m_s_004
|
||||
text: "Three containers in temp. Right where they should be."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -63,7 +63,7 @@ lines:
|
||||
min_confidence: knows_of
|
||||
tags: [arrival, operational, contraband]
|
||||
|
||||
- id: smuggling-hold_m_s_005
|
||||
- id: pc-smuggler_m_s_005
|
||||
text: "Back in the hold. Dust on the floor shows the last two paths in. Both mine."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -72,7 +72,7 @@ lines:
|
||||
situation: [arrival, routine]
|
||||
tags: [arrival, operational]
|
||||
|
||||
- id: smuggling-hold_m_s_006
|
||||
- id: pc-smuggler_m_s_006
|
||||
text: "Nobody's been here. Good. That's how it should feel."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -83,7 +83,7 @@ lines:
|
||||
|
||||
# --- Perception / Sound ---
|
||||
|
||||
- id: smuggling-hold_m_s_007
|
||||
- id: pc-smuggler_m_s_007
|
||||
text: "The water recycler's on the other side of that wall. Loud enough to cover conversation."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -92,7 +92,7 @@ lines:
|
||||
situation: [routine]
|
||||
tags: [sensory, environmental, operational]
|
||||
|
||||
- id: smuggling-hold_m_s_008
|
||||
- id: pc-smuggler_m_s_008
|
||||
text: "Footsteps above. Maintenance crew — they stay up top. Always."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -101,7 +101,7 @@ lines:
|
||||
situation: [routine]
|
||||
tags: [sensory, environmental, caution]
|
||||
|
||||
- id: smuggling-hold_m_s_009
|
||||
- id: pc-smuggler_m_s_009
|
||||
text: "That sound. Pressure shift. Someone opened the main hatch."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -111,7 +111,7 @@ lines:
|
||||
priority: 7
|
||||
tags: [sensory, caution]
|
||||
|
||||
- id: smuggling-hold_m_s_010
|
||||
- id: pc-smuggler_m_s_010
|
||||
text: "Pipe drone changes pitch when cargo shifts weight in the upper tier. That's cargo moving."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -122,7 +122,7 @@ lines:
|
||||
|
||||
# --- Time Idle / Ruminative ---
|
||||
|
||||
- id: smuggling-hold_m_s_011
|
||||
- id: pc-smuggler_m_s_011
|
||||
text: "Fifteen minutes until the oversight window closes. Plenty of time. Probably."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -131,7 +131,7 @@ lines:
|
||||
situation: [shift_transition]
|
||||
tags: [operational, atmospheric]
|
||||
|
||||
- id: smuggling-hold_m_s_012
|
||||
- id: pc-smuggler_m_s_012
|
||||
text: "Medical-grade lattice components. People need these. That's still the reason."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -140,7 +140,7 @@ lines:
|
||||
situation: [routine]
|
||||
tags: [atmospheric, contraband]
|
||||
|
||||
- id: smuggling-hold_m_s_013
|
||||
- id: pc-smuggler_m_s_013
|
||||
text: "Twelve minutes of reduced oversight. Stop counting and do something useful."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -149,7 +149,7 @@ lines:
|
||||
situation: [shift_transition]
|
||||
tags: [operational]
|
||||
|
||||
- id: smuggling-hold_m_s_014
|
||||
- id: pc-smuggler_m_s_014
|
||||
text: "How many times have I been in this room telling myself it's almost done?"
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -158,7 +158,7 @@ lines:
|
||||
situation: [routine]
|
||||
tags: [atmospheric, personal]
|
||||
|
||||
- id: smuggling-hold_m_s_015
|
||||
- id: pc-smuggler_m_s_015
|
||||
text: "Quiet. The right kind. Not the wrong kind."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -169,7 +169,7 @@ lines:
|
||||
|
||||
# --- Anomaly / Investigation ---
|
||||
|
||||
- id: smuggling-hold_m_s_016
|
||||
- id: pc-smuggler_m_s_016
|
||||
text: "Container 4471's been opened. Not by me. Not by anyone I authorized."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -179,7 +179,7 @@ lines:
|
||||
priority: 8
|
||||
tags: [operational, caution, contraband]
|
||||
|
||||
- id: smuggling-hold_m_s_017
|
||||
- id: pc-smuggler_m_s_017
|
||||
text: "Dust disturbed at the secondary hatch. Recent. Someone's been using the back route."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -188,7 +188,7 @@ lines:
|
||||
situation: [routine]
|
||||
tags: [caution, operational]
|
||||
|
||||
- id: smuggling-hold_m_s_018
|
||||
- id: pc-smuggler_m_s_018
|
||||
text: "Scratches on the access panel. New ones, over the old ones. Different tool."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -197,7 +197,7 @@ lines:
|
||||
situation: [routine]
|
||||
tags: [caution, environmental]
|
||||
|
||||
- id: smuggling-hold_m_s_019
|
||||
- id: pc-smuggler_m_s_019
|
||||
text: "The temp unit's running warm. Someone moved cargo through here fast."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -208,7 +208,7 @@ lines:
|
||||
|
||||
# --- Knowledge-Gated: Kael Arc ---
|
||||
|
||||
- id: smuggling-hold_m_s_020
|
||||
- id: pc-smuggler_m_s_020
|
||||
text: "Kael used to wait for me here. The one place where we could actually talk."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -222,7 +222,7 @@ lines:
|
||||
priority: 6
|
||||
tags: [npc, kael, atmospheric, friend-arc]
|
||||
|
||||
- id: smuggling-hold_m_s_021
|
||||
- id: pc-smuggler_m_s_021
|
||||
text: "Kael's not answering. Should be here by now. Should have been here ten minutes ago."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -236,7 +236,7 @@ lines:
|
||||
priority: 7
|
||||
tags: [npc, kael, concern, friend-arc]
|
||||
|
||||
- id: smuggling-hold_m_s_022
|
||||
- id: pc-smuggler_m_s_022
|
||||
text: "If Kael talked to someone in that corridor — someone outside the ring — and then this container got touched... that's not coincidence."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -252,7 +252,7 @@ lines:
|
||||
priority: 9
|
||||
tags: [npc, kael, investigation, contraband, friend-arc]
|
||||
|
||||
- id: smuggling-hold_m_s_023
|
||||
- id: pc-smuggler_m_s_023
|
||||
text: "The manifest Kael helped me falsify is in that container. If he talked, they already know."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -268,7 +268,7 @@ lines:
|
||||
priority: 9
|
||||
tags: [npc, kael, operational, contraband, friend-arc, contaminated-trust]
|
||||
|
||||
- id: smuggling-hold_m_s_024
|
||||
- id: pc-smuggler_m_s_024
|
||||
text: "Renn's mark is here. The route's still active. So Kael hasn't burned everything. Yet."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -284,7 +284,7 @@ lines:
|
||||
|
||||
# --- Post-Conversation ---
|
||||
|
||||
- id: smuggling-hold_m_s_025
|
||||
- id: pc-smuggler_m_s_025
|
||||
text: "Renn didn't ask questions. That's either loyalty or he already knows."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -293,7 +293,7 @@ lines:
|
||||
situation: [routine]
|
||||
tags: [npc, renn, operational]
|
||||
|
||||
- id: smuggling-hold_m_s_026
|
||||
- id: pc-smuggler_m_s_026
|
||||
text: "One more run. That's what I keep telling myself. Has been for eight months."
|
||||
role: player_character
|
||||
access: [public]
|
||||
|
||||
@@ -3,7 +3,7 @@ location: the-last-shift
|
||||
lines:
|
||||
# --- Arrival + Environmental Flavor ---
|
||||
|
||||
- id: the-last-shift_m_s_001
|
||||
- id: pc-smuggler_m_s_001
|
||||
text: "Lera's. Grain spirit and noise. Could use both right now."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -12,7 +12,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [arrival, atmospheric]
|
||||
|
||||
- id: the-last-shift_m_s_002
|
||||
- id: pc-smuggler_m_s_002
|
||||
text: "Bar's half full. The after-shift crowd's starting to filter in."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -21,7 +21,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [arrival]
|
||||
|
||||
- id: the-last-shift_m_s_003
|
||||
- id: pc-smuggler_m_s_003
|
||||
text: "Lera's got the Meridian feed running. Same headlines, different day."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -30,7 +30,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [arrival, environmental]
|
||||
|
||||
- id: the-last-shift_m_s_004
|
||||
- id: pc-smuggler_m_s_004
|
||||
text: "Smells like fried protein and cheap grain spirit. Perfect."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -39,7 +39,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [environmental, atmospheric]
|
||||
|
||||
- id: the-last-shift_m_s_005
|
||||
- id: pc-smuggler_m_s_005
|
||||
text: "My booth's free. Small victories."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -48,7 +48,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [arrival, personal]
|
||||
|
||||
- id: the-last-shift_m_s_006
|
||||
- id: pc-smuggler_m_s_006
|
||||
text: "The overhead light's still buzzing. Lera says it adds character."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -57,7 +57,7 @@ lines:
|
||||
trigger: hear_sound
|
||||
tags: [environmental]
|
||||
|
||||
- id: the-last-shift_m_s_007
|
||||
- id: pc-smuggler_m_s_007
|
||||
text: "Music's louder tonight. Lera's in a good mood, or drowning something out."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -66,7 +66,7 @@ lines:
|
||||
trigger: hear_sound
|
||||
tags: [environmental, atmospheric]
|
||||
|
||||
- id: the-last-shift_m_s_008
|
||||
- id: pc-smuggler_m_s_008
|
||||
text: "Back at Lera's. Same booth, same drink, same crowd."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -75,7 +75,7 @@ lines:
|
||||
trigger: return_visit
|
||||
tags: [arrival]
|
||||
|
||||
- id: the-last-shift_m_s_009
|
||||
- id: pc-smuggler_m_s_009
|
||||
text: "The ventilation rattles when the kitchen's on. You learn to talk over it."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -84,7 +84,7 @@ lines:
|
||||
trigger: hear_sound
|
||||
tags: [environmental]
|
||||
|
||||
- id: the-last-shift_m_s_010
|
||||
- id: pc-smuggler_m_s_010
|
||||
text: "Quiet night at Lera's. Fewer people means fewer ears."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -95,7 +95,7 @@ lines:
|
||||
|
||||
# --- NPC Commentary ---
|
||||
|
||||
- id: the-last-shift_m_s_011
|
||||
- id: pc-smuggler_m_s_011
|
||||
text: "Lera's behind the bar. Good — she keeps things steady."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -104,7 +104,7 @@ lines:
|
||||
trigger: observe_npc
|
||||
tags: [npc, lera]
|
||||
|
||||
- id: the-last-shift_m_s_012
|
||||
- id: pc-smuggler_m_s_012
|
||||
text: "Kael's at the corner table. Looks relaxed. That's something."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -113,7 +113,7 @@ lines:
|
||||
trigger: observe_npc
|
||||
tags: [npc, kael]
|
||||
|
||||
- id: the-last-shift_m_s_013
|
||||
- id: pc-smuggler_m_s_013
|
||||
text: "Torek's buying rounds again. That man doesn't know the word 'discreet.'"
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -122,7 +122,7 @@ lines:
|
||||
trigger: observe_npc
|
||||
tags: [npc, torek, caution]
|
||||
|
||||
- id: the-last-shift_m_s_014
|
||||
- id: pc-smuggler_m_s_014
|
||||
text: "Sera's here. Commission field tech. She keeps to herself — mostly."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -131,7 +131,7 @@ lines:
|
||||
trigger: observe_npc
|
||||
tags: [npc, sera, commission]
|
||||
|
||||
- id: the-last-shift_m_s_015
|
||||
- id: pc-smuggler_m_s_015
|
||||
text: "Naia's laughing at something Lera said. Nice to see her relaxed."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -140,7 +140,7 @@ lines:
|
||||
trigger: observe_npc
|
||||
tags: [npc, naia]
|
||||
|
||||
- id: the-last-shift_m_s_016
|
||||
- id: pc-smuggler_m_s_016
|
||||
text: "Old Pael's in his usual spot. Hasn't moved in three hours. Impressive."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -149,7 +149,7 @@ lines:
|
||||
trigger: observe_npc
|
||||
tags: [npc, atmospheric]
|
||||
|
||||
- id: the-last-shift_m_s_017
|
||||
- id: pc-smuggler_m_s_017
|
||||
text: "Lera just cut someone off. Smooth about it, but firm."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -158,7 +158,7 @@ lines:
|
||||
trigger: witness_interaction
|
||||
tags: [npc, lera]
|
||||
|
||||
- id: the-last-shift_m_s_018
|
||||
- id: pc-smuggler_m_s_018
|
||||
text: "Kael and Naia at the bar. Leaning into each other. They look good together."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -167,7 +167,7 @@ lines:
|
||||
trigger: witness_interaction
|
||||
tags: [npc, kael, naia]
|
||||
|
||||
- id: the-last-shift_m_s_019
|
||||
- id: pc-smuggler_m_s_019
|
||||
text: "Voss is drinking alone. Not his usual pace."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -176,7 +176,7 @@ lines:
|
||||
trigger: observe_npc
|
||||
tags: [npc, voss]
|
||||
|
||||
- id: the-last-shift_m_s_020
|
||||
- id: pc-smuggler_m_s_020
|
||||
text: "That new face in the corner — been nursing the same drink for an hour."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -187,7 +187,7 @@ lines:
|
||||
|
||||
# --- Knowledge-Gated Lines ---
|
||||
|
||||
- id: the-last-shift_m_s_021
|
||||
- id: pc-smuggler_m_s_021
|
||||
text: "Sera left when Torek walked in. Coincidence? Maybe."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -200,7 +200,7 @@ lines:
|
||||
min_confidence: suspects
|
||||
tags: [npc, sera, torek, pattern]
|
||||
|
||||
- id: the-last-shift_m_s_022
|
||||
- id: pc-smuggler_m_s_022
|
||||
text: "Torek just paid cash credit, not station account. Sloppy."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -213,7 +213,7 @@ lines:
|
||||
min_confidence: knows_of
|
||||
tags: [npc, torek, operational]
|
||||
|
||||
- id: the-last-shift_m_s_023
|
||||
- id: pc-smuggler_m_s_023
|
||||
text: "Lera gave me that look. The one that says 'not here, not now.'"
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -226,7 +226,7 @@ lines:
|
||||
min_confidence: knows_details
|
||||
tags: [npc, lera, operational]
|
||||
|
||||
- id: the-last-shift_m_s_024
|
||||
- id: pc-smuggler_m_s_024
|
||||
text: "Naia looks tired. Has Kael been coming home late again?"
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -239,7 +239,7 @@ lines:
|
||||
state: known
|
||||
tags: [npc, naia, kael, concern]
|
||||
|
||||
- id: the-last-shift_m_s_025
|
||||
- id: pc-smuggler_m_s_025
|
||||
text: "The detective is here. At the bar. My bar."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -253,7 +253,7 @@ lines:
|
||||
priority: 8
|
||||
tags: [investigation, caution]
|
||||
|
||||
- id: the-last-shift_m_s_026
|
||||
- id: pc-smuggler_m_s_026
|
||||
text: "Sera and the detective sharing a booth. Commission people stick together."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -266,7 +266,7 @@ lines:
|
||||
min_confidence: knows_of
|
||||
tags: [npc, sera, investigation]
|
||||
|
||||
- id: the-last-shift_m_s_027
|
||||
- id: pc-smuggler_m_s_027
|
||||
text: "Kael didn't save my seat tonight. First time in months."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -280,7 +280,7 @@ lines:
|
||||
priority: 8
|
||||
tags: [npc, kael, contaminated-trust, friend-arc]
|
||||
|
||||
- id: the-last-shift_m_s_028
|
||||
- id: pc-smuggler_m_s_028
|
||||
text: "The back room light's on. Nils is here, then."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -293,7 +293,7 @@ lines:
|
||||
min_confidence: knows_details
|
||||
tags: [operational, nils]
|
||||
|
||||
- id: the-last-shift_m_s_029
|
||||
- id: pc-smuggler_m_s_029
|
||||
text: "Kael bought Naia a drink but barely looked at her. That's new."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -306,7 +306,7 @@ lines:
|
||||
min_confidence: suspects
|
||||
tags: [npc, kael, naia, tell, friend-arc]
|
||||
|
||||
- id: the-last-shift_m_s_030
|
||||
- id: pc-smuggler_m_s_030
|
||||
text: "Everyone's a little too quiet tonight. Something happened. I can feel it."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -326,7 +326,7 @@ lines:
|
||||
# perspective. On replay, these lines gain new meaning when the player
|
||||
# understands Sera's concealment arc from the detective playthrough.
|
||||
|
||||
- id: the-last-shift_m_s_031
|
||||
- id: pc-smuggler_m_s_031
|
||||
text: "Sera's at the Commission kiosk. Third time this shift. Nobody recalibrates that much."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -340,7 +340,7 @@ lines:
|
||||
priority: 6
|
||||
tags: [npc, sera, commission, pattern, dual-lens]
|
||||
|
||||
- id: the-last-shift_m_s_032
|
||||
- id: pc-smuggler_m_s_032
|
||||
text: "Naia trusts Sera. More than most people here. That says something."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -349,7 +349,7 @@ lines:
|
||||
trigger: witness_interaction
|
||||
tags: [npc, sera, naia, social, dual-lens]
|
||||
|
||||
- id: the-last-shift_m_s_033
|
||||
- id: pc-smuggler_m_s_033
|
||||
text: "Sera was studying the freight display. For a calibration tech, that's a lot of manifest reading."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -363,7 +363,7 @@ lines:
|
||||
priority: 6
|
||||
tags: [npc, sera, tell, cargo, dual-lens]
|
||||
|
||||
- id: the-last-shift_m_s_034
|
||||
- id: pc-smuggler_m_s_034
|
||||
text: "That's three times Sera's bailed when Torek showed up. Maybe she owes him money."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -377,7 +377,7 @@ lines:
|
||||
priority: 7
|
||||
tags: [npc, sera, torek, pattern, dual-lens, replay-payload]
|
||||
|
||||
- id: the-last-shift_m_s_035
|
||||
- id: pc-smuggler_m_s_035
|
||||
text: "Sera offered the detective compliance records. Helpful -- or professional courtesy?"
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -390,7 +390,7 @@ lines:
|
||||
min_confidence: knows_of
|
||||
tags: [npc, sera, investigation, caution, dual-lens]
|
||||
|
||||
- id: the-last-shift_m_s_036
|
||||
- id: pc-smuggler_m_s_036
|
||||
text: "Something about Sera tonight. Same smile, different eyes."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -403,7 +403,7 @@ lines:
|
||||
state: known
|
||||
tags: [npc, sera, behavioral, dual-lens]
|
||||
|
||||
- id: the-last-shift_m_s_037
|
||||
- id: pc-smuggler_m_s_037
|
||||
text: "Commission tech who drinks at a dock bar. Either lonely or embedded. Either way -- careful."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -412,7 +412,7 @@ lines:
|
||||
trigger: observe_npc
|
||||
tags: [npc, sera, commission, assessment, dual-lens]
|
||||
|
||||
- id: the-last-shift_m_s_038
|
||||
- id: pc-smuggler_m_s_038
|
||||
text: "Sera knows the shift rotations better than Voss. That's not calibration. That's attention."
|
||||
role: player_character
|
||||
access: [public]
|
||||
|
||||
@@ -3,7 +3,7 @@ location: the-terminal
|
||||
lines:
|
||||
# --- Arrival + Environmental Flavor ---
|
||||
|
||||
- id: the-terminal_m_s_001
|
||||
- id: pc-smuggler_m_s_001
|
||||
text: "Morning shift. Recycled air and cargo lubricant. Home sweet home."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -12,7 +12,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [arrival, atmospheric]
|
||||
|
||||
- id: the-terminal_m_s_002
|
||||
- id: pc-smuggler_m_s_002
|
||||
text: "The loading arms are cycling. Freight's on schedule — for once."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -21,7 +21,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [arrival, operational]
|
||||
|
||||
- id: the-terminal_m_s_003
|
||||
- id: pc-smuggler_m_s_003
|
||||
text: "Shift board's already full. Voss has everyone running double today."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -30,7 +30,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [arrival, operational]
|
||||
|
||||
- id: the-terminal_m_s_004
|
||||
- id: pc-smuggler_m_s_004
|
||||
text: "Cargo lubricant and ozone. You'd think I'd stop noticing."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -39,7 +39,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [environmental, atmospheric]
|
||||
|
||||
- id: the-terminal_m_s_005
|
||||
- id: pc-smuggler_m_s_005
|
||||
text: "Loading arm three is grinding again. Someone should file that."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -48,7 +48,7 @@ lines:
|
||||
trigger: hear_sound
|
||||
tags: [environmental]
|
||||
|
||||
- id: the-terminal_m_s_006
|
||||
- id: pc-smuggler_m_s_006
|
||||
text: "Containers stacked six high in bay four. That's new."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -57,7 +57,7 @@ lines:
|
||||
trigger: observe_anomaly
|
||||
tags: [environmental, operational]
|
||||
|
||||
- id: the-terminal_m_s_007
|
||||
- id: pc-smuggler_m_s_007
|
||||
text: "The freight manifest board's flickering. Always flickering."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -66,7 +66,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [environmental]
|
||||
|
||||
- id: the-terminal_m_s_008
|
||||
- id: pc-smuggler_m_s_008
|
||||
text: "Dock lights are on half-cycle. Budget cuts again."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -75,7 +75,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [environmental, atmospheric]
|
||||
|
||||
- id: the-terminal_m_s_009
|
||||
- id: pc-smuggler_m_s_009
|
||||
text: "Back at the hub. Shift transition in fifty minutes."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -84,7 +84,7 @@ lines:
|
||||
trigger: return_visit
|
||||
tags: [arrival, operational]
|
||||
|
||||
- id: the-terminal_m_s_010
|
||||
- id: pc-smuggler_m_s_010
|
||||
text: "Quiet morning. Containers moving, nobody talking. I like it this way."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -93,7 +93,7 @@ lines:
|
||||
trigger: enter_location
|
||||
tags: [arrival, atmospheric]
|
||||
|
||||
- id: the-terminal_m_s_011
|
||||
- id: pc-smuggler_m_s_011
|
||||
text: "Bay three's been sealed off. Inspection, or something else?"
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -102,7 +102,7 @@ lines:
|
||||
trigger: observe_anomaly
|
||||
tags: [environmental, caution]
|
||||
|
||||
- id: the-terminal_m_s_012
|
||||
- id: pc-smuggler_m_s_012
|
||||
text: "The conveyor hums at a different pitch when it's loaded heavy. This one's heavy."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -113,7 +113,7 @@ lines:
|
||||
|
||||
# --- NPC Commentary ---
|
||||
|
||||
- id: the-terminal_m_s_013
|
||||
- id: pc-smuggler_m_s_013
|
||||
text: "Kael's already at the dock. Good. The day's better when he's on shift."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -122,7 +122,7 @@ lines:
|
||||
trigger: observe_npc
|
||||
tags: [npc, kael, warm]
|
||||
|
||||
- id: the-terminal_m_s_014
|
||||
- id: pc-smuggler_m_s_014
|
||||
text: "Voss is pacing. Never a good sign."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -131,7 +131,7 @@ lines:
|
||||
trigger: observe_npc
|
||||
tags: [npc, voss, caution]
|
||||
|
||||
- id: the-terminal_m_s_015
|
||||
- id: pc-smuggler_m_s_015
|
||||
text: "Maret's got her scheduling face on. Tight lips, fast hands on the terminal."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -140,7 +140,7 @@ lines:
|
||||
trigger: observe_npc
|
||||
tags: [npc, maret]
|
||||
|
||||
- id: the-terminal_m_s_016
|
||||
- id: pc-smuggler_m_s_016
|
||||
text: "New face at dock seven. Temp worker, or someone I should know about?"
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -149,7 +149,7 @@ lines:
|
||||
trigger: observe_npc
|
||||
tags: [npc, caution]
|
||||
|
||||
- id: the-terminal_m_s_017
|
||||
- id: pc-smuggler_m_s_017
|
||||
text: "Drin's at inspection again. Going through the motions, same as always."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -158,7 +158,7 @@ lines:
|
||||
trigger: observe_npc
|
||||
tags: [npc, drin]
|
||||
|
||||
- id: the-terminal_m_s_018
|
||||
- id: pc-smuggler_m_s_018
|
||||
text: "Renn's running late. Third time this week."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -167,7 +167,7 @@ lines:
|
||||
trigger: observe_npc
|
||||
tags: [npc, renn, operational]
|
||||
|
||||
- id: the-terminal_m_s_019
|
||||
- id: pc-smuggler_m_s_019
|
||||
text: "Voss and Maret talking by the schedule board. Both look tense."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -176,7 +176,7 @@ lines:
|
||||
trigger: witness_interaction
|
||||
tags: [npc, voss, maret]
|
||||
|
||||
- id: the-terminal_m_s_020
|
||||
- id: pc-smuggler_m_s_020
|
||||
text: "Kael nodded at me across the bay. Business as usual."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -185,7 +185,7 @@ lines:
|
||||
trigger: observe_npc
|
||||
tags: [npc, kael]
|
||||
|
||||
- id: the-terminal_m_s_021
|
||||
- id: pc-smuggler_m_s_021
|
||||
text: "Someone I don't recognize talking to Drin. Drin's smiling. Drin never smiles."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -194,7 +194,7 @@ lines:
|
||||
trigger: witness_interaction
|
||||
tags: [npc, drin, anomaly]
|
||||
|
||||
- id: the-terminal_m_s_022
|
||||
- id: pc-smuggler_m_s_022
|
||||
text: "Torek's on the dock. Shouldn't be — his shift ended two hours ago."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -203,7 +203,7 @@ lines:
|
||||
trigger: observe_anomaly
|
||||
tags: [npc, torek, caution]
|
||||
|
||||
- id: the-terminal_m_s_023
|
||||
- id: pc-smuggler_m_s_023
|
||||
text: "Nils isn't here yet. That's unusual."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -212,7 +212,7 @@ lines:
|
||||
trigger: observe_anomaly
|
||||
tags: [npc, nils, operational]
|
||||
|
||||
- id: the-terminal_m_s_024
|
||||
- id: pc-smuggler_m_s_024
|
||||
text: "Kael's eating alone today. He always eats with me."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -223,7 +223,7 @@ lines:
|
||||
|
||||
# --- Knowledge-Gated Lines ---
|
||||
|
||||
- id: the-terminal_m_s_025
|
||||
- id: pc-smuggler_m_s_025
|
||||
text: "Kael keeps checking his lattice. Waiting for a message? Not like him to be jumpy."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -237,7 +237,7 @@ lines:
|
||||
priority: 7
|
||||
tags: [npc, kael, tell, friend-arc]
|
||||
|
||||
- id: the-terminal_m_s_026
|
||||
- id: pc-smuggler_m_s_026
|
||||
text: "Container 4471's been in temp since yesterday. That's not the normal routing."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -250,7 +250,7 @@ lines:
|
||||
min_confidence: knows_details
|
||||
tags: [operational, contraband]
|
||||
|
||||
- id: the-terminal_m_s_027
|
||||
- id: pc-smuggler_m_s_027
|
||||
text: "Voss changed the rotation again. Third time this month. Covering something."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -263,7 +263,7 @@ lines:
|
||||
min_confidence: knows_of
|
||||
tags: [npc, voss, operational]
|
||||
|
||||
- id: the-terminal_m_s_028
|
||||
- id: pc-smuggler_m_s_028
|
||||
text: "That detective's back. Asking questions near bay four. Stay calm."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -277,7 +277,7 @@ lines:
|
||||
priority: 8
|
||||
tags: [investigation, caution]
|
||||
|
||||
- id: the-terminal_m_s_029
|
||||
- id: pc-smuggler_m_s_029
|
||||
text: "Maret hasn't looked at me all shift. She noticed something. I know she did."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -292,7 +292,7 @@ lines:
|
||||
priority: 6
|
||||
tags: [npc, maret, paranoia]
|
||||
|
||||
- id: the-terminal_m_s_030
|
||||
- id: pc-smuggler_m_s_030
|
||||
text: "Shift transition in ten. If the route's clean, we move tonight."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -305,7 +305,7 @@ lines:
|
||||
min_confidence: knows_details
|
||||
tags: [operational, contraband]
|
||||
|
||||
- id: the-terminal_m_s_031
|
||||
- id: pc-smuggler_m_s_031
|
||||
text: "Kael was in corridor B-7 last night. Off-shift. Off-route. That's not nothing."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -319,7 +319,7 @@ lines:
|
||||
priority: 8
|
||||
tags: [npc, kael, contradiction, friend-arc]
|
||||
|
||||
- id: the-terminal_m_s_032
|
||||
- id: pc-smuggler_m_s_032
|
||||
text: "Who was Kael talking to? Not anyone from our rotation. I'd know the face."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -333,7 +333,7 @@ lines:
|
||||
priority: 9
|
||||
tags: [npc, kael, contradiction, friend-arc]
|
||||
|
||||
- id: the-terminal_m_s_033
|
||||
- id: pc-smuggler_m_s_033
|
||||
text: "He looked left before answering. He always looks left when he's lying."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -347,7 +347,7 @@ lines:
|
||||
priority: 9
|
||||
tags: [npc, kael, tell, friend-arc]
|
||||
|
||||
- id: the-terminal_m_s_034
|
||||
- id: pc-smuggler_m_s_034
|
||||
text: "Torek's spending too much at Lera's. Someone's going to notice."
|
||||
role: player_character
|
||||
access: [public]
|
||||
@@ -360,7 +360,7 @@ lines:
|
||||
min_confidence: knows_of
|
||||
tags: [npc, torek, operational, caution]
|
||||
|
||||
- id: the-terminal_m_s_035
|
||||
- id: pc-smuggler_m_s_035
|
||||
text: "Same old Kael. Same jokes, same routine. ...What are you not telling me?"
|
||||
role: player_character
|
||||
access: [public]
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
character: smuggler
|
||||
location: tutorial
|
||||
lines:
|
||||
# Diegetic Tutorial Monologue — Ticket #330 — Smuggler
|
||||
# Authored per sprint-17 copy briefing and D-016 (internal monologue as core system).
|
||||
#
|
||||
# These lines teach mechanics through character voice — not UI instructions.
|
||||
# Distinct from opening.yaml (which covers the first 5 minutes and sets voice/motivation).
|
||||
# These fire on first-time events throughout gameplay, wherever they occur.
|
||||
#
|
||||
# Coverage:
|
||||
# Movement / exploration — first_move, first_new_section
|
||||
# Fog of perception — first_fog_encounter, first_fog_edge
|
||||
# Sound model — first_sound_heard, first_off_screen_sound
|
||||
# NPC interaction — first_npc_proximity, first_npc_face_read
|
||||
# Insert / HUD — first_insert_open, first_insert_contact_flag
|
||||
#
|
||||
# Voice: observational, street-smart, practical. Fragments. Standalone "Good." as tic.
|
||||
# No analytical distance — the smuggler reads, reacts, files, moves on.
|
||||
# ID discriminator: _tut_ per sprint briefing.
|
||||
# cooldown: omitted — fire-once enforced by trigger semantics (first_* triggers
|
||||
# fire once per game), not by a cooldown value.
|
||||
# priority: 8 (primary, one per trigger type), 6 (secondary variant, mood-weighted alternate).
|
||||
#
|
||||
# Schema corrections (Gestalt review, Sprint 17):
|
||||
# priority: string "tutorial" → integer. All other monologue files use integer priority;
|
||||
# string value would fail engine parse in monologue.rs.
|
||||
# situation: removed invalid values (movement, exploration) not in D-035 13-situation
|
||||
# enum. Replaced with arrival, routine, observation per actual trigger context.
|
||||
# mood: [focused] retained where set; valid per D-035 Sprint 14 amendment.
|
||||
#
|
||||
# Trigger types (first_move, first_new_section, first_fog_encounter, first_fog_edge,
|
||||
# first_sound_heard, first_off_screen_sound, first_npc_proximity, first_npc_face_read,
|
||||
# first_insert_open, first_insert_contact_flag) are extended trigger enum values not in
|
||||
# D-035 v0.1 set — require server-side implementation in monologue.rs.
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# MOVEMENT / EXPLORATION
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
- id: pc-smuggler_m_s_tut_001
|
||||
text: "These corridors all look the same. Mental note: check the signage."
|
||||
role: player_character
|
||||
access: [public]
|
||||
trust: surface
|
||||
situation: [arrival, routine]
|
||||
trigger: first_move
|
||||
mood: [focused]
|
||||
priority: 8
|
||||
|
||||
tags: [tutorial, movement, navigation]
|
||||
notes: >
|
||||
Directly from the sprint briefing example. First move teaches navigation: the district
|
||||
looks homogeneous, signage is the tool. 'Mental note' is the smuggler's internal
|
||||
flag for things to remember — operational housekeeping, not analysis. Establishes
|
||||
early: this environment requires active spatial tracking.
|
||||
|
||||
- id: pc-smuggler_m_s_tut_002
|
||||
text: "Two lefts past the junction, right at the supply hatch. That's dock four. Route's in the muscle now."
|
||||
role: player_character
|
||||
access: [public]
|
||||
trust: surface
|
||||
situation: [arrival, routine]
|
||||
trigger: first_new_section
|
||||
mood: [content]
|
||||
priority: 6
|
||||
|
||||
tags: [tutorial, movement, navigation, operational]
|
||||
notes: >
|
||||
Fires on entering a new section for the first time. 'Route's in the muscle' — the
|
||||
smuggler doesn't memorize spatially, she moves until it's automatic. Practical,
|
||||
self-sufficient. Teaches: the district has multiple sections, each requires its own
|
||||
orientation. 'Dock four' is a landmark. Forward movement is rewarded with familiarity.
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# FOG OF PERCEPTION
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
- id: pc-smuggler_m_s_tut_003
|
||||
text: "Can't see past that corner. Might be worth checking."
|
||||
role: player_character
|
||||
access: [public]
|
||||
trust: surface
|
||||
situation: [observation]
|
||||
trigger: first_fog_encounter
|
||||
mood: [focused]
|
||||
priority: 8
|
||||
|
||||
tags: [tutorial, fog, perception]
|
||||
notes: >
|
||||
Directly from the sprint briefing example. First fog boundary. Restated as habit:
|
||||
the smuggler doesn't treat the fog as a limitation, she treats every unknown corner
|
||||
as a question worth answering. 'Might be worth' — not paranoia, just operational
|
||||
instinct. Teaches: the perception boundary is navigable, not fixed.
|
||||
|
||||
- id: pc-smuggler_m_s_tut_004
|
||||
text: "Haze cuts off past the junction. Anything in it — can't say."
|
||||
role: player_character
|
||||
access: [public]
|
||||
trust: surface
|
||||
situation: [observation]
|
||||
trigger: first_fog_edge
|
||||
mood: [focused]
|
||||
priority: 6
|
||||
|
||||
tags: [tutorial, fog, perception]
|
||||
notes: >
|
||||
Second fog encounter line — fires when the player reaches the boundary of a fog zone
|
||||
(the edge of visible range). 'Can't say' — plain statement of unknowing, not anxiety.
|
||||
The smuggler accepts incomplete information as a working condition, not a failure state.
|
||||
Teaches: perception range is an ongoing factor, not a one-time obstacle.
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# SOUND MODEL
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
- id: pc-smuggler_m_s_tut_005
|
||||
text: "Voices down the hall. Can't make out the words from here."
|
||||
role: player_character
|
||||
access: [public]
|
||||
trust: surface
|
||||
situation: [observation]
|
||||
trigger: first_sound_heard
|
||||
mood: [focused]
|
||||
priority: 8
|
||||
|
||||
tags: [tutorial, sound, perception]
|
||||
notes: >
|
||||
Directly from the sprint briefing example. First sound heard that's not immediately
|
||||
visible. Teaching: sound carries information, but range and occlusion limit it.
|
||||
'Can't make out the words' establishes both the value of the sound channel (voices =
|
||||
people = relevant) and its limitation (unclear at range). Moving toward source is
|
||||
the implied next step.
|
||||
|
||||
- id: pc-smuggler_m_s_tut_006
|
||||
text: "Footsteps. Two sets. Moving away — good."
|
||||
role: player_character
|
||||
access: [public]
|
||||
trust: surface
|
||||
situation: [observation]
|
||||
trigger: first_off_screen_sound
|
||||
mood: [content]
|
||||
priority: 6
|
||||
|
||||
tags: [tutorial, sound, perception, operational]
|
||||
notes: >
|
||||
Second sound line — fires when audio from an off-screen source is detected. The
|
||||
smuggler parses footsteps as count and direction instinctively. 'Moving away — good.'
|
||||
— the standalone 'Good.' tic applied to tactical assessment. Teaches: sound carries
|
||||
directional and movement data, not just presence. Two sets = two people, which is
|
||||
specific enough to be operationally useful.
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# NPC INTERACTION
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
- id: pc-smuggler_m_s_tut_007
|
||||
text: "Could ask around. People talk if you give them reason to."
|
||||
role: player_character
|
||||
access: [public]
|
||||
trust: surface
|
||||
situation: [social, observation]
|
||||
trigger: first_npc_proximity
|
||||
mood: [content]
|
||||
priority: 8
|
||||
|
||||
tags: [tutorial, npc, interaction, social]
|
||||
notes: >
|
||||
Directly from the sprint briefing example. First time in range of an NPC. The smuggler
|
||||
doesn't approach directly — she flags the option. 'Give them reason to' is street-smart
|
||||
social knowledge: people don't volunteer freely, they need a hook. Teaches: NPC
|
||||
interaction is player-initiated, and approach matters. No mechanics text — just the
|
||||
character's own method stated as personal principle.
|
||||
|
||||
- id: pc-smuggler_m_s_tut_008
|
||||
text: "Know the face. Don't need the name."
|
||||
role: player_character
|
||||
access: [public]
|
||||
trust: surface
|
||||
situation: [social, observation]
|
||||
trigger: first_npc_face_read
|
||||
mood: [content]
|
||||
priority: 6
|
||||
|
||||
tags: [tutorial, npc, observation, social]
|
||||
notes: >
|
||||
Fires when the player observes an NPC at close range (face visible). The smuggler
|
||||
reads people by face, not by name — the name comes later if relevant. Teaches: NPCs
|
||||
are identifiable by observation, and the insert overlay will fill in names when
|
||||
available. The smuggler's social intelligence is pre-lattice, instinctive.
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# INSERT / HUD
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
- id: pc-smuggler_m_s_tut_009
|
||||
text: "Check the overlay. Should show the time and nearby contacts."
|
||||
role: player_character
|
||||
access: [public]
|
||||
trust: surface
|
||||
situation: [routine]
|
||||
trigger: first_insert_open
|
||||
mood: [content]
|
||||
priority: 8
|
||||
|
||||
tags: [tutorial, insert, hud, operational]
|
||||
notes: >
|
||||
Directly from the sprint briefing example. First insert/HUD open. The smuggler's
|
||||
overlay shows time and nearby contacts — both operationally critical. 'Should show'
|
||||
— practical, not certain. Teaches: the insert is the primary tool for time-tracking
|
||||
and NPC awareness. Diegetic: she checks it the same way she checks a shift board.
|
||||
|
||||
- id: pc-smuggler_m_s_tut_010
|
||||
text: "Overlay marks Kael two sections over. Good."
|
||||
role: player_character
|
||||
access: [public]
|
||||
trust: surface
|
||||
situation: [routine, social]
|
||||
trigger: first_insert_contact_flag
|
||||
mood: [content, warm]
|
||||
priority: 6
|
||||
|
||||
tags: [tutorial, insert, hud, kael, friend-arc]
|
||||
notes: >
|
||||
Fires when the insert first flags a known contact (here: Kael, the FRIEND NPC).
|
||||
'Two sections over' — spatial data from the overlay. 'Good.' standalone tic —
|
||||
warm, settling. Teaches: the insert shows known NPC positions in real-time.
|
||||
Also seeds the Kael relationship: the smuggler checks his location as a habit,
|
||||
not a task. His position is always relevant to her.
|
||||
@@ -0,0 +1,106 @@
|
||||
# Fact catalog: ring
|
||||
# Smuggling ring module event trail — observable evidence produced by module events.
|
||||
# These facts are made discoverable by specific events in smuggling_ring_v0_1.yaml.
|
||||
# Stub entries — description and confidence level confirmed; progression text TBD.
|
||||
# Ticket: #158 | Sprint 18
|
||||
|
||||
facts:
|
||||
|
||||
- fact_id: ring.cargo_discrepancy_pattern
|
||||
description: Cargo manifests at The Terminal show small, systematic discrepancies — weight variances, unverified entries, containers with no return log
|
||||
typical_confidence: suspects
|
||||
characters: [smuggler, detective]
|
||||
notes: >
|
||||
Detective's analytical lattice may flag this automatically during Terminal walkthrough.
|
||||
Smuggler encounters it during operational paperwork checks. Pattern becomes clearer over
|
||||
multiple observations (once: false event — repeats). Starting entry point for both
|
||||
investigation arcs.
|
||||
|
||||
- fact_id: ring.kael_missed_verification
|
||||
description: Kael Davan was absent from a scheduled cargo verification — another dock worker covered without explanation
|
||||
typical_confidence: suspects
|
||||
characters: [smuggler, detective]
|
||||
notes: >
|
||||
Discoverable via cargo manifest review (Terminal office) or analytical lattice flag
|
||||
(detective). Smuggler notices the irregularity in their own paperwork. First concrete
|
||||
evidence that Kael is deviating from his normal operational reliability.
|
||||
|
||||
- fact_id: ring.kael_unauthorized_corridor_access
|
||||
description: Kael was observed in restricted maintenance corridor B-7 during off-shift hours with an unknown contact
|
||||
typical_confidence: knows_of
|
||||
characters: [smuggler, detective]
|
||||
notes: >
|
||||
Core FRIEND contradiction observable (D-034). Player must be in visual range of B-7
|
||||
to discover this, OR examine the corridor door access log (investigative action).
|
||||
Both characters can discover this fact — interpreted differently (smuggler: security
|
||||
breach / detective: suspicious contact). Upgrades from suspects to knows_of when
|
||||
contact identity confirmed, knows_details when purpose confirmed.
|
||||
|
||||
- fact_id: ring.kael_unusual_meridian_activity
|
||||
description: Encrypted Meridian packets sent from cargo bay terminals — frequent, patterned, not from personal devices
|
||||
typical_confidence: suspects
|
||||
characters: [detective]
|
||||
notes: >
|
||||
Detective-only. Requires analytical lattice to flag the outgoing packet pattern.
|
||||
Player must be in or adjacent to The Terminal cargo bay. Content not accessible —
|
||||
only the existence and frequency of the encrypted messages. Hints at Kael's
|
||||
off-district contact without revealing who.
|
||||
|
||||
- fact_id: ring.voss_kael_private_meeting
|
||||
description: Voss called Kael into the supervisor's office — closed door, raised voices audible from adjacent position
|
||||
typical_confidence: suspects
|
||||
characters: [smuggler, detective]
|
||||
notes: >
|
||||
Observable via spatial positioning (supervisor's office area). Smuggler can witness
|
||||
the approach and departure without hearing content. Detective observes Kael's visible
|
||||
strain immediately after if present in The Terminal. Upgrade to knows_of via
|
||||
trust-gated dialogue with Kael ("Are you alright?" option unlocks post-meeting).
|
||||
|
||||
- fact_id: ring.voss_accelerating_timeline
|
||||
description: Voss is pushing to close ring operations faster than normal — someone is watching
|
||||
typical_confidence: knows_of
|
||||
characters: [smuggler, detective]
|
||||
notes: >
|
||||
Smuggler: Voss mentions "the schedule moving up" in guarded conversation (trust-gated,
|
||||
ring-insider access required). Detective: observe Voss and Nils in two exchanges within
|
||||
the same shift and note behavioral change. Not directly stated — inferred from context.
|
||||
|
||||
- fact_id: ring.commission_cargo_flag
|
||||
description: A Commission internal note flags The Terminal's cargo variance rate as statistically unusual
|
||||
typical_confidence: knows_of
|
||||
characters: [detective]
|
||||
notes: >
|
||||
Detective-only. Accessible via institutional Commission query (authority access tier,
|
||||
Terminal records). Not a formal investigation — just an internal flag from Maret Korr.
|
||||
Confirms the detective's suspicions with institutional weight. Smuggler has no way to
|
||||
know this exists.
|
||||
|
||||
- fact_id: ring.final_shipment_scheduled
|
||||
description: The ring has scheduled a major final drop — after it clears, operations go dark
|
||||
typical_confidence: knows_of
|
||||
characters: [smuggler, detective]
|
||||
notes: >
|
||||
Smuggler: direct notification from Voss (ring-insider access). Detective: cargo manifest
|
||||
shows an unusually large entry scheduled 3 days out — no obvious legitimate reason for
|
||||
the volume. This fact creates the closing window — both characters know time is running
|
||||
out, for different reasons.
|
||||
|
||||
- fact_id: ring.sera_avoidance_behavior
|
||||
description: Sera Venn takes longer routes through The Last Shift to avoid standing near Torek Lintar
|
||||
typical_confidence: suspects
|
||||
characters: [detective]
|
||||
notes: >
|
||||
Detective-only observable. Player must observe Sera's movement pattern on two separate
|
||||
occasions — requires forward vision cone and awareness of her baseline route. The
|
||||
avoidance is visible but not explained. Points toward Sera holding information about
|
||||
Torek's investigation without having acted on it.
|
||||
|
||||
- fact_id: ring.nils_unlogged_cargo
|
||||
description: Nils makes regular supply runs to maintenance corridor C-7 with containers that never appear in return logs
|
||||
typical_confidence: suspects
|
||||
characters: [smuggler, detective]
|
||||
notes: >
|
||||
Discoverable by watching Nils's cargo handling pattern over two shifts, or by examining
|
||||
the maintenance corridor C-7 access log directly. The missing return entry is the tell —
|
||||
the container went in but never came back. Both characters can find this; smuggler may
|
||||
already suspect given operational context.
|
||||
@@ -0,0 +1,692 @@
|
||||
# yaml-language-server: $schema=../../schemas/drama_module.schema.yaml
|
||||
#
|
||||
# Tier 1 Drama Module: The Smuggling Ring (v0.1)
|
||||
# The vertical slice Tier 1 module for D-027.
|
||||
#
|
||||
# NARRATIVE CORE:
|
||||
# A logistics worker (the smuggler PC, if played) is embedded in a small ring
|
||||
# smuggling unlicensed lattice components through Sova Transit District (D-037).
|
||||
# The ring is led by Voss from The Terminal. Kael Davan — a ring member and the
|
||||
# smuggler's FRIEND — is quietly trying to exit to protect his partner Naia Tamm.
|
||||
# Sera Venn (the detective's FRIEND) has noticed Kael's manifest discrepancies
|
||||
# but hasn't reported them, protecting Naia by proxy.
|
||||
#
|
||||
# DUAL-LENS EXPERIENCE:
|
||||
# Smuggler plays INSIDE the ring: manage drops, cover tracks, notice Kael going cold.
|
||||
# Detective plays OUTSIDE: cargo anomalies → follow Kael → witness secret meeting →
|
||||
# confront or protect.
|
||||
#
|
||||
# SUCCESS CRITERIA (D-027):
|
||||
# #1: 30 minutes of daily-life play before the ring activates (min_play_ticks: 2100)
|
||||
# #3: Player names Kael as an NPC they felt conflicted about
|
||||
# #4: observe→notice→follow→discover emerges from systems, not scripts
|
||||
|
||||
module_id: smuggling_ring_v0_1
|
||||
display_name: "The Smuggling Ring"
|
||||
version: "0.1"
|
||||
tier: 1
|
||||
description: >
|
||||
A small ring of logistics workers smuggling unlicensed lattice components through
|
||||
Sova Transit District. The ring's weakest link — Kael Davan — is trying to exit
|
||||
to protect his partner. The detective investigates cargo anomalies. The smuggler
|
||||
manages ring operations and navigates Kael's loyalty crisis. Neither character
|
||||
knows the other's full picture until confrontation forces it.
|
||||
|
||||
notes: >
|
||||
This module IS the vertical slice (D-027). It exercises every system at full depth:
|
||||
dual-lens NPC observation, tell progression, trust-gated dialogue, knowledge graph
|
||||
confidence accumulation, confrontation weight (D-063), walk-away consequences (D-064),
|
||||
and THE FRIEND contradiction arc (D-034). All outcome paths must feel earned.
|
||||
No outcome is "the right answer" — Kael's situation has no clean resolution.
|
||||
|
||||
dual_lens:
|
||||
smuggler: >
|
||||
You're inside the ring. Voss manages operations; you handle logistics cover.
|
||||
Kael used to be reliable. Lately he's absent, distracted, making excuses.
|
||||
The drop schedule is at risk. Do you pressure him, cover for him, or cut him?
|
||||
You don't know he's trying to get out. He doesn't know you've noticed.
|
||||
detective: >
|
||||
Cargo manifest discrepancies in The Terminal. Small, systematic, deniable.
|
||||
Your analytical lattice flags them before your conscious mind does.
|
||||
Follow the thread: discrepancy → dock worker with odd schedule → Kael Davan →
|
||||
maintenance corridors → someone he shouldn't be meeting. And then what?
|
||||
Arrest a man trying to leave a ring he never wanted to join?
|
||||
|
||||
pool:
|
||||
weight: 8
|
||||
compatible_districts:
|
||||
- sova-transit
|
||||
max_concurrent: 1
|
||||
|
||||
# ── ENTRY CONDITIONS ─────────────────────────────────────────────────────────
|
||||
# Ring activity begins after player has had time to establish routine (D-027 #1).
|
||||
# The ring is already running at game start — the module activates when the
|
||||
# storyteller decides the tension has built enough to surface.
|
||||
|
||||
entry_conditions:
|
||||
world_state:
|
||||
- type: npc_present
|
||||
role: ring-leader
|
||||
- type: npc_present
|
||||
role: ring-member-exiting
|
||||
- type: location_accessible
|
||||
location: the-terminal
|
||||
- type: location_accessible
|
||||
location: maintenance-corridors
|
||||
|
||||
activation:
|
||||
trigger: storyteller_push
|
||||
min_play_ticks: 2100 # ~35 minutes at 1 tick/second — D-027 criterion #1
|
||||
# The storyteller pushes activation when player has established presence
|
||||
# in The Terminal or The Last Shift through routine interaction.
|
||||
# Proximity trigger (maintenance-corridors) is a secondary activation path
|
||||
# if the player wanders there early.
|
||||
|
||||
# ── NPC REQUIREMENTS ─────────────────────────────────────────────────────────
|
||||
# All core roles are named (hand-authored NPCs from the vertical slice).
|
||||
# No generated NPC slots in v0.1 — the smuggling ring uses the 15 authored NPCs.
|
||||
|
||||
npc_requirements:
|
||||
- role: ring-leader
|
||||
display_hint: >
|
||||
Runs the ring from The Terminal. Logistics authority = cover.
|
||||
Never handles contraband directly. Pressure source for Kael.
|
||||
binding: named
|
||||
named_npc: "npc:voss"
|
||||
must_have_motivation: HANDLER
|
||||
|
||||
- role: ring-member-exiting
|
||||
display_hint: >
|
||||
Kael Davan. Dock worker, ring member, smuggler's FRIEND.
|
||||
Trying to exit quietly to protect Naia. This is THE FRIEND contradiction.
|
||||
Every event sequence runs through this role.
|
||||
binding: named
|
||||
named_npc: "npc:kael-davan"
|
||||
must_have_pattern: FRIEND
|
||||
must_have_motivation: TURNCOAT
|
||||
|
||||
- role: partner-uninvolved
|
||||
display_hint: >
|
||||
Naia Tamm. Kael's partner. Does not know about the ring.
|
||||
Her safety is Kael's motivation for exiting. Her ignorance is the moral weight.
|
||||
Discovery of her connection to Kael is a late-investigation revelation.
|
||||
binding: named
|
||||
named_npc: "npc:naia-tamm"
|
||||
must_have_motivation: CIVILIAN
|
||||
|
||||
- role: evidence-holder
|
||||
display_hint: >
|
||||
Sera Venn. Detective's FRIEND. Commission field tech.
|
||||
She has noticed Kael's manifest discrepancies but hasn't reported them —
|
||||
she knows Naia, and filing means Kael's arrest and Naia's exposure.
|
||||
Her silence IS the detective's investigation blocker in phase 1.
|
||||
binding: named
|
||||
named_npc: "npc:sera-venn"
|
||||
must_have_pattern: FRIEND
|
||||
must_have_motivation: WITNESS
|
||||
|
||||
- role: ring-operative
|
||||
display_hint: >
|
||||
The ring's operational member in maintenance corridors.
|
||||
Handles physical drops. Not a speaking character — observable behavior only.
|
||||
Can be the anonymous contact Kael meets.
|
||||
binding: named
|
||||
named_npc: "npc:nils-davan"
|
||||
is_optional: false
|
||||
|
||||
- role: institutional-watcher
|
||||
display_hint: >
|
||||
Maret Korr. A Commission observer embedded at The Terminal.
|
||||
Her growing attention is the external pressure that accelerates the timeline.
|
||||
She doesn't know about the ring specifically — she's tracking cargo patterns.
|
||||
binding: named
|
||||
named_npc: "npc:maret-korr"
|
||||
must_have_motivation: OPERATOR
|
||||
is_optional: true # Module runs without Maret, but with degraded tension arc
|
||||
|
||||
# ── EVENTS ───────────────────────────────────────────────────────────────────
|
||||
# Two sequences + one pool.
|
||||
# Sequence A: Kael's exit arc (the FRIEND contradiction backbone)
|
||||
# Sequence B: Investigation pressure arc (escalating discovery opportunities)
|
||||
# Pool: ambient ring activity (fires opportunistically throughout the module)
|
||||
|
||||
events:
|
||||
|
||||
sequences:
|
||||
|
||||
# SEQUENCE A: Kael's Exit Arc
|
||||
# The narrative spine. Each step makes Kael's contradiction more visible.
|
||||
# Observable to both characters, interpreted differently.
|
||||
|
||||
- sequence_id: kael_exit_arc
|
||||
label: "Kael's Exit Arc"
|
||||
description: >
|
||||
Kael Davan's progressive attempt to leave the ring.
|
||||
Tells intensify. Routine deviations appear. The secret meeting is the
|
||||
pivot point — after it fires, both characters' understanding shifts.
|
||||
steps:
|
||||
|
||||
- event_id: kael_goes_cold
|
||||
label: "Kael Goes Cold"
|
||||
description: >
|
||||
Kael starts missing social patterns he'd normally keep — fewer bar visits,
|
||||
shorter responses at The Terminal, leaving early. His tell system activates:
|
||||
the shoulder-check behavior appears. Nothing dramatic. Just absence where
|
||||
there was presence. The smuggler notices because they work together.
|
||||
The detective might notice if they've been tracking Kael's baseline.
|
||||
triggers:
|
||||
- type: ticks_since_activation
|
||||
ticks: 300 # ~5 minutes after module activates
|
||||
effects:
|
||||
- type: npc_routine_deviation
|
||||
npc_role: ring-member-exiting
|
||||
description: >
|
||||
Kael skips his usual post-shift drink at The Last Shift.
|
||||
Leaves the terminal 15 minutes early. No explanation.
|
||||
- type: tell_intensify
|
||||
npc_role: ring-member-exiting
|
||||
description: >
|
||||
Kael's shoulder-check behavior activates at The Terminal.
|
||||
Visible to any character with forward vision cone in his direction.
|
||||
sets_flag: kael_behavior_changed
|
||||
|
||||
- event_id: drop_happens_without_kael
|
||||
label: "Scheduled Drop — Kael Absent"
|
||||
description: >
|
||||
A ring drop occurs in maintenance corridor C-7. Kael was supposed
|
||||
to verify the cargo. He wasn't there. Nils covered it.
|
||||
The smuggler notices the irregularity in the paperwork.
|
||||
The detective — if watching cargo patterns — sees a manifest entry
|
||||
with no verifying signature where one is normally present.
|
||||
triggers:
|
||||
- type: ticks_since_event
|
||||
after_event: kael_goes_cold
|
||||
ticks: 450 # ~7.5 minutes after goes-cold
|
||||
effects:
|
||||
- type: fact_becomes_discoverable
|
||||
fact_id: "ring.kael_missed_verification"
|
||||
discoverable_by: any
|
||||
discovery_method: >
|
||||
Smuggler: check the cargo manifest in The Terminal office.
|
||||
Detective: analytical lattice flags unsigned verification entry.
|
||||
- type: location_state
|
||||
location: maintenance-corridors
|
||||
description: "An unsigned cargo verification entry exists in corridor C-7's log."
|
||||
sets_flag: kael_missed_drop
|
||||
|
||||
- event_id: kael_secret_meeting
|
||||
label: "Kael's Secret Meeting"
|
||||
description: >
|
||||
Kael meets an off-district contact in maintenance corridor B-7.
|
||||
This is the observable contradiction (D-034): Kael, in a restricted
|
||||
area he has no logged reason to be in, talking to someone who's
|
||||
not in any district NPC roster. His body language is tense.
|
||||
If the player is in visual range: this is the pivot moment.
|
||||
If not: the meeting happens anyway — the world doesn't wait.
|
||||
triggers:
|
||||
- type: ticks_since_event
|
||||
after_event: drop_happens_without_kael
|
||||
ticks: 600 # ~10 minutes after the dropped verification
|
||||
- type: player_proximity
|
||||
target_type: location
|
||||
target: maintenance-corridors
|
||||
radius_tiles: 12 # Player wandering near triggers the meeting early
|
||||
effects:
|
||||
- type: npc_routine_deviation
|
||||
npc_role: ring-member-exiting
|
||||
description: >
|
||||
Kael enters maintenance corridor B-7. Locked door to restricted
|
||||
supply closet. Emerges with the ring-operative 8 minutes later.
|
||||
Neither acknowledges the encounter publicly.
|
||||
- type: fact_becomes_discoverable
|
||||
fact_id: "ring.kael_unauthorized_corridor_access"
|
||||
discoverable_by: any
|
||||
discovery_method: >
|
||||
Player must be in visual range of corridor B-7.
|
||||
Or examine the corridor door access log (investigative action).
|
||||
- type: tell_intensify
|
||||
npc_role: ring-member-exiting
|
||||
description: >
|
||||
After the meeting, Kael's shoulder-check frequency doubles.
|
||||
Also: he avoids eye contact with the smuggler at The Terminal.
|
||||
sets_flag: secret_meeting_occurred
|
||||
|
||||
- event_id: kael_sends_message
|
||||
label: "Kael Sends the Message"
|
||||
description: >
|
||||
Kael sends an encrypted Meridian message to an off-district contact.
|
||||
The detective's analytical lattice can detect an anomalous outgoing
|
||||
packet from the district node — not the content, just the pattern
|
||||
(frequent, encrypted, sent from cargo bay terminals, not personal devices).
|
||||
The smuggler won't see this unless they're specifically watching Kael.
|
||||
triggers:
|
||||
- type: ticks_since_event
|
||||
after_event: kael_secret_meeting
|
||||
ticks: 200
|
||||
effects:
|
||||
- type: fact_becomes_discoverable
|
||||
fact_id: "ring.kael_unusual_meridian_activity"
|
||||
discoverable_by: detective
|
||||
discovery_method: >
|
||||
Detective's analytical lattice flags the outgoing packet pattern.
|
||||
Requires player to be in or adjacent to The Terminal cargo bay.
|
||||
sets_flag: kael_message_sent
|
||||
|
||||
- event_id: ring_leader_confronts_kael
|
||||
label: "Voss Confronts Kael"
|
||||
description: >
|
||||
Voss calls Kael into The Terminal supervisor's office.
|
||||
Closed door. Raised voices (audible only from adjacent room/position).
|
||||
Kael emerges pale. Voss emerges neutral. The smuggler can witness
|
||||
the approach/departure without hearing content. The detective can
|
||||
observe Kael's state immediately after if in The Terminal.
|
||||
This is Voss applying pressure. Kael is now visibly under strain.
|
||||
triggers:
|
||||
- type: flag_set
|
||||
flag: kael_message_sent
|
||||
- type: ticks_since_event
|
||||
after_event: kael_message_sent
|
||||
ticks: 400
|
||||
effects:
|
||||
- type: npc_routine_deviation
|
||||
npc_role: ring-leader
|
||||
description: "Voss calls Kael into the supervisor's office. Door closed."
|
||||
- type: npc_routine_deviation
|
||||
npc_role: ring-member-exiting
|
||||
description: >
|
||||
Kael emerges from the meeting looking strained. His shoulder-check
|
||||
is now constant. He takes an unscheduled break outside, alone.
|
||||
- type: tell_intensify
|
||||
npc_role: ring-member-exiting
|
||||
description: >
|
||||
Kael's contentment hits lowest observed level. He now actively avoids
|
||||
the ring-operative (Nils) in public. The disconnection is visible.
|
||||
- type: fact_becomes_discoverable
|
||||
fact_id: "ring.voss_kael_private_meeting"
|
||||
discoverable_by: any
|
||||
discovery_method: >
|
||||
Observe the meeting room door (spatial). Or ask Kael directly
|
||||
after (trust-gated dialogue unlocks "Are you alright?" option).
|
||||
sets_flag: voss_pressure_applied
|
||||
|
||||
# SEQUENCE B: Investigation Pressure Arc
|
||||
# External pressure that escalates the timeline.
|
||||
# Fires in parallel with Sequence A.
|
||||
|
||||
- sequence_id: investigation_pressure
|
||||
label: "Investigation Pressure Arc"
|
||||
description: >
|
||||
Maret Korr's institutional attention creates a closing window.
|
||||
Her growing interest is the reason the module can't stay in equilibrium forever.
|
||||
She doesn't know about the ring — she's a pattern-watcher. But patterns
|
||||
are what the detective investigates too. Their paths converge.
|
||||
steps:
|
||||
|
||||
- event_id: maret_flags_anomaly
|
||||
label: "Maret Flags the Cargo Anomaly"
|
||||
description: >
|
||||
Maret Korr files an internal Commission note flagging The Terminal's
|
||||
cargo variance rate as statistically unusual. Not an investigation —
|
||||
just a flag. The detective's institutional access can pull this note.
|
||||
The smuggler has no way to know it exists (unless the detective tells them).
|
||||
triggers:
|
||||
- type: ticks_since_activation
|
||||
ticks: 900 # ~15 minutes after activation
|
||||
effects:
|
||||
- type: fact_becomes_discoverable
|
||||
fact_id: "ring.commission_cargo_flag"
|
||||
discoverable_by: detective
|
||||
discovery_method: >
|
||||
Detective queries Commission data via institutional access
|
||||
(authority access tier, Terminal records).
|
||||
sets_flag: commission_flag_exists
|
||||
|
||||
- event_id: maret_increases_presence
|
||||
label: "Maret Increases Her Presence"
|
||||
description: >
|
||||
Maret starts spending more time in The Terminal. More frequent
|
||||
walkthroughs during shift changes. Her attention to the cargo bay
|
||||
area is noticeable to anyone watching. Ring members are unnerved.
|
||||
Voss starts accelerating the timeline to close operations before
|
||||
institutional attention becomes formal investigation.
|
||||
triggers:
|
||||
- type: ticks_since_event
|
||||
after_event: maret_flags_anomaly
|
||||
ticks: 600
|
||||
- type: player_action
|
||||
action: examine
|
||||
target_role: institutional-watcher
|
||||
effects:
|
||||
- type: npc_routine_deviation
|
||||
npc_role: institutional-watcher
|
||||
description: >
|
||||
Maret adds two extra Terminal walkthroughs per shift cycle.
|
||||
Spends 15 minutes studying the cargo bay manifest terminals.
|
||||
- type: tell_intensify
|
||||
npc_role: ring-leader
|
||||
description: >
|
||||
Voss becomes quieter, more deliberate. Less casual conversation.
|
||||
His tell — the stillness before speaking — becomes more frequent.
|
||||
- type: fact_becomes_discoverable
|
||||
fact_id: "ring.voss_accelerating_timeline"
|
||||
discoverable_by: any
|
||||
discovery_method: >
|
||||
Smuggler: Voss mentions "the schedule moving up" in a guarded
|
||||
conversation (trust-gated, ring-insider access required).
|
||||
Detective: observe Voss and Nils in two exchanges within same shift.
|
||||
sets_flag: timeline_accelerating
|
||||
|
||||
- event_id: final_shipment_scheduled
|
||||
label: "The Final Shipment Is Scheduled"
|
||||
description: >
|
||||
The ring schedules the last major drop — after this, they go dark.
|
||||
This is the closing window. If the detective hasn't uncovered enough
|
||||
by the time this fires, the ring disperses and the operation closes
|
||||
without exposure (escaped outcome). If they have, confrontation
|
||||
becomes unavoidable. The smuggler knows about this drop. Kael doesn't
|
||||
want to participate. Voss insists.
|
||||
triggers:
|
||||
- type: flag_set
|
||||
flag: timeline_accelerating
|
||||
- type: ticks_since_event
|
||||
after_event: maret_increases_presence
|
||||
ticks: 800
|
||||
effects:
|
||||
- type: fact_becomes_discoverable
|
||||
fact_id: "ring.final_shipment_scheduled"
|
||||
discoverable_by: any
|
||||
discovery_method: >
|
||||
Smuggler: direct notification from Voss.
|
||||
Detective: cargo manifest shows an unusual large entry for 3 days out.
|
||||
- type: npc_routine_deviation
|
||||
npc_role: ring-member-exiting
|
||||
description: >
|
||||
Kael's schedule changes: he's assigned to the cargo bay
|
||||
during the drop window. He doesn't want to be there.
|
||||
sets_flag: final_shipment_known
|
||||
|
||||
pools:
|
||||
|
||||
# POOL: Ambient ring activity — opportunistic events that add texture
|
||||
- pool_id: ambient_ring_activity
|
||||
label: "Ambient Ring Activity"
|
||||
description: >
|
||||
Low-level ring business that happens throughout the module regardless of
|
||||
player engagement. Creates the sense that the ring exists independently.
|
||||
Players who look closely will find more; players who don't still feel the world moving.
|
||||
events:
|
||||
- event_id: cargo_discrepancy_appears
|
||||
label: "Small Cargo Discrepancy Appears"
|
||||
description: >
|
||||
A minor manifest irregularity appears in The Terminal records.
|
||||
Small enough to be deniable. Systematic enough to be a pattern.
|
||||
The detective's analytical lattice may flag it. The smuggler can
|
||||
correct it if they notice it — covering tracks is part of their role.
|
||||
triggers:
|
||||
- type: ticks_since_activation
|
||||
ticks: 150 # Fires early and repeats
|
||||
effects:
|
||||
- type: fact_becomes_discoverable
|
||||
fact_id: "ring.cargo_discrepancy_pattern"
|
||||
discoverable_by: any
|
||||
discovery_method: >
|
||||
Detective: analytical lattice flags during Terminal walkthrough.
|
||||
Smuggler: check manifest terminals (or get flagged by the discrepancy
|
||||
in their own work).
|
||||
once: false # Repeats — pattern builds over time
|
||||
|
||||
- event_id: sera_avoids_torek
|
||||
label: "Sera Avoids Torek at The Bar"
|
||||
description: >
|
||||
Sera Venn reroutes her usual path through The Last Shift to avoid
|
||||
standing near Torek Lintar (the Commission enforcement officer).
|
||||
Anyone watching Sera's normal pattern would notice.
|
||||
This is the detective's first clue that Sera's behavior is odd.
|
||||
triggers:
|
||||
- type: ticks_since_activation
|
||||
ticks: 500
|
||||
effects:
|
||||
- type: npc_routine_deviation
|
||||
npc_role: evidence-holder
|
||||
description: >
|
||||
Sera takes a longer route to her usual seat, passing through
|
||||
the back of the bar to avoid Torek's sightline.
|
||||
- type: fact_becomes_discoverable
|
||||
fact_id: "ring.sera_avoidance_behavior"
|
||||
discoverable_by: detective
|
||||
discovery_method: >
|
||||
Observe Sera's path through the bar on two separate occasions.
|
||||
Requires forward vision cone and awareness of her baseline route.
|
||||
once: false
|
||||
|
||||
- event_id: nils_makes_supply_run
|
||||
label: "Nils Makes an Unscheduled Supply Run"
|
||||
description: >
|
||||
The ring-operative (Nils) enters the maintenance corridors with a
|
||||
small container logged as "calibration tools". The container isn't
|
||||
logged for return. Someone paying attention to cargo flow would notice.
|
||||
triggers:
|
||||
- type: ticks_since_activation
|
||||
ticks: 700
|
||||
effects:
|
||||
- type: npc_routine_deviation
|
||||
npc_role: ring-operative
|
||||
description: "Nils takes a container to maintenance corridor C-7."
|
||||
- type: fact_becomes_discoverable
|
||||
fact_id: "ring.nils_unlogged_cargo"
|
||||
discoverable_by: any
|
||||
discovery_method: >
|
||||
Watch Nils's cargo handling pattern over two shifts.
|
||||
Or examine maintenance corridor C-7 access log.
|
||||
once: false
|
||||
|
||||
# ── OUTCOMES ─────────────────────────────────────────────────────────────────
|
||||
# Five resolution states. Checked each tick after the first sequence step fires.
|
||||
# Order matters — the storyteller applies the first matching outcome.
|
||||
# is_terminal: true ends the module.
|
||||
|
||||
outcomes:
|
||||
|
||||
# 1. RING EXPOSED
|
||||
# Detective successfully uncovers the operation.
|
||||
# Commission becomes involved. Arrests/flight follow.
|
||||
- outcome_id: ring_exposed
|
||||
label: "Ring Exposed"
|
||||
is_terminal: true
|
||||
description: >
|
||||
The detective accumulates enough evidence to trigger a formal Commission
|
||||
inquiry. The ring collapses: arrests, flight, or both. Voss is detained.
|
||||
Kael's situation is now public. The smuggler (if played) faces consequences.
|
||||
Naia learns what Kael was doing — and why he was trying to leave.
|
||||
No clean endings. The right outcome for the detective who goes all the way.
|
||||
conditions:
|
||||
facts_known:
|
||||
- "ring.cargo_discrepancy_pattern"
|
||||
- "ring.kael_unauthorized_corridor_access"
|
||||
- "ring.voss_kael_private_meeting"
|
||||
flags_set:
|
||||
- secret_meeting_occurred # set by kael_secret_meeting event
|
||||
- commission_flag_exists # Commission was watching before exposure
|
||||
effects:
|
||||
- type: npc_disposition
|
||||
npc_role: ring-leader
|
||||
shift: hostile
|
||||
description: "Voss is detained or flees. Commission inquiry opens."
|
||||
- type: npc_disposition
|
||||
npc_role: ring-member-exiting
|
||||
shift: hostile
|
||||
description: >
|
||||
Kael is arrested or disappears. His exit attempt is now moot.
|
||||
His relationship with Naia is exposed.
|
||||
- type: faction_reaction
|
||||
faction: lattice-commission
|
||||
reaction: grateful
|
||||
description: "Commission credits the detective's investigation."
|
||||
- type: npc_exit
|
||||
npc_role: ring-leader
|
||||
description: "Voss leaves the district — detained, fled, or both."
|
||||
|
||||
# 2. KAEL ESCAPES THE RING
|
||||
# Unique path. Requires the player to engage with Kael directly
|
||||
# and choose to help him rather than expose the ring wholesale.
|
||||
- outcome_id: kael_escapes
|
||||
label: "Kael Escapes the Ring"
|
||||
is_terminal: true
|
||||
description: >
|
||||
Through the player's choices — helping Kael cover his exit, or warning him,
|
||||
or simply choosing not to act on what they know — Kael successfully leaves
|
||||
the ring before the final shipment. He and Naia leave the district quietly.
|
||||
The ring continues without him, smaller and more cautious.
|
||||
This outcome requires discovering Kael's secret AND choosing restraint.
|
||||
The smuggler can engineer this by covering for Kael with Voss.
|
||||
The detective can achieve this by confronting Kael privately rather than
|
||||
filing a report. The most morally complicated path.
|
||||
conditions:
|
||||
facts_known:
|
||||
- "ring.kael_unauthorized_corridor_access"
|
||||
flags_set:
|
||||
- kael_behavior_changed # set by kael_goes_cold — his exit arc begins here
|
||||
- secret_meeting_occurred # set by kael_secret_meeting — the pivot moment
|
||||
- voss_pressure_applied # set by ring_leader_confronts_kael — pressure applied
|
||||
# ring_exposed is checked first in the outcomes list and is terminal,
|
||||
# so kael_escapes only evaluates if ring_exposed hasn't fired.
|
||||
# No flags_not_set needed here — outcome ordering handles priority.
|
||||
effects:
|
||||
- type: npc_disposition
|
||||
npc_role: ring-member-exiting
|
||||
shift: friendly
|
||||
description: "Kael remembers whoever helped him. He's gone, but grateful."
|
||||
- type: npc_exit
|
||||
npc_role: ring-member-exiting
|
||||
description: "Kael and Naia leave Sova Transit District."
|
||||
- type: faction_reaction
|
||||
faction: the-ring
|
||||
reaction: suspicious
|
||||
description: "The ring is destabilized by Kael's exit. Voss is alert to further leaks."
|
||||
|
||||
# 3. RING COMPLETES OPERATION
|
||||
# The ring finishes the final shipment and goes dark before discovery.
|
||||
# Default path if the detective doesn't move fast enough.
|
||||
- outcome_id: ring_completes
|
||||
label: "Ring Completes the Operation"
|
||||
is_terminal: true
|
||||
description: >
|
||||
The final shipment clears. The ring disperses. Voss transfers. Nils goes quiet.
|
||||
Kael stays — he's now out by default, the ring having dissolved around him.
|
||||
The evidence trail goes cold. The detective closes the case as inconclusive.
|
||||
The smuggler completes their last run and waits to see if there's another.
|
||||
Unsatisfying only if you expected a tidy resolution. The world moved on.
|
||||
conditions:
|
||||
flags_set:
|
||||
- final_shipment_known # set by final_shipment_scheduled event
|
||||
- timeline_accelerating # set by maret_increases_presence — Maret forced their hand
|
||||
facts_not_known:
|
||||
- "ring.cargo_discrepancy_pattern" # detective never found the basic pattern — no investigation
|
||||
ticks_since_activation: 3600 # Module ran for ~60 minutes without full exposure
|
||||
# kael_message_sent was previously gated here but auto-fires at tick ~1550,
|
||||
# making this outcome permanently unreachable. Replaced with player-action fact gate.
|
||||
effects:
|
||||
- type: faction_reaction
|
||||
faction: the-ring
|
||||
reaction: neutral
|
||||
description: "The ring successfully completed this operation. They'll be back."
|
||||
- type: npc_exit
|
||||
npc_role: ring-leader
|
||||
description: "Voss transfers to another station for 'career development'."
|
||||
- type: location_access_change
|
||||
location: maintenance-corridors
|
||||
change: open
|
||||
description: "The restricted supply closet is now empty. Access log shows it cleared."
|
||||
|
||||
# 4. RING SPLINTERS
|
||||
# Partial discovery. The ring fractures but doesn't fully collapse.
|
||||
# An incomplete ending that leaves threads for future investigation.
|
||||
- outcome_id: ring_splinters
|
||||
label: "Ring Splinters"
|
||||
is_terminal: false # Not terminal — splinter state can evolve
|
||||
description: >
|
||||
Enough evidence surfaces that the ring knows it's been partially seen.
|
||||
Voss shuts down active operations. Nils disappears. Kael stays — now the
|
||||
one person in the district who knows what happened and has no one to tell.
|
||||
The formal investigation stalls for lack of a clear chain of evidence.
|
||||
The detective has facts but not the complete picture. The smuggler
|
||||
faces an awkward return to normalcy. Both know the ring isn't gone — just quiet.
|
||||
conditions:
|
||||
facts_known:
|
||||
- "ring.cargo_discrepancy_pattern" # detective found some evidence — ring responds
|
||||
events_fired:
|
||||
- kael_goes_cold # event ID — Kael's behavioral shift fired
|
||||
flags_set:
|
||||
- kael_missed_drop # set by drop_happens_without_kael — ring destabilized
|
||||
ticks_since_activation: 2400
|
||||
# Mutually exclusive with ring_completes via facts_known/facts_not_known on
|
||||
# ring.cargo_discrepancy_pattern. No auto-flag gate needed.
|
||||
effects:
|
||||
- type: npc_disposition
|
||||
npc_role: ring-leader
|
||||
shift: suspicious
|
||||
description: "Voss goes quiet. He's watching to see who knows what."
|
||||
- type: npc_exit
|
||||
npc_role: ring-operative
|
||||
description: "Nils stops appearing at The Terminal. Transferred, officially."
|
||||
- type: faction_reaction
|
||||
faction: the-ring
|
||||
reaction: suspicious
|
||||
description: "The ring is alerted to exposure risk. Future operations will be more careful."
|
||||
|
||||
# 5. INVESTIGATION STALLS (post-splinter exit)
|
||||
# The ring splinters but the detective never breaks through to the pivot evidence.
|
||||
# Explicit terminal exit for the non-terminal ring_splinters state.
|
||||
- outcome_id: ring_stalemate
|
||||
label: "Investigation Stalls"
|
||||
is_terminal: true
|
||||
description: >
|
||||
The ring went dark after the splinter. The detective has the cargo discrepancy
|
||||
on record — enough to flag, not enough to pursue. The case stays open but cold.
|
||||
No arrests. No answers. Kael stays in the district, the only person who knows
|
||||
the full shape of what happened, with no one left to tell it to.
|
||||
The ring will reconstitute elsewhere. It always does.
|
||||
conditions:
|
||||
facts_known:
|
||||
- "ring.cargo_discrepancy_pattern" # ring_splinters already fired (same gate)
|
||||
facts_not_known:
|
||||
- "ring.kael_unauthorized_corridor_access" # detective never reached the pivot evidence
|
||||
flags_set:
|
||||
- kael_missed_drop
|
||||
- final_shipment_known # ring finished while investigation stalled
|
||||
ticks_since_activation: 4500 # 2100 ticks after ring_splinters window — investigation ran cold
|
||||
effects:
|
||||
- type: faction_reaction
|
||||
faction: lattice-commission
|
||||
reaction: neutral
|
||||
description: "The discrepancy flag stays in Maret's file. No follow-up action."
|
||||
- type: npc_exit
|
||||
npc_role: ring-leader
|
||||
description: "Voss quietly transfers. No announcement, no incident report."
|
||||
|
||||
# 6. MODULE EXPIRY (quiet exit)
|
||||
# Player never engaged at all. Module times out without drama.
|
||||
# NOTE (Gestalt, Sprint 18): Condition uses facts_not_known, not flags_not_set.
|
||||
# kael_behavior_changed fires automatically at tick 300 (time-triggered), making
|
||||
# flags_not_set: [kael_behavior_changed] permanently false after tick 300.
|
||||
# Gate expiry on player-action-required facts instead.
|
||||
- outcome_id: module_abandoned
|
||||
label: "Module Abandoned"
|
||||
is_terminal: true
|
||||
is_expiry: true
|
||||
description: >
|
||||
The player never engaged with the ring's signals. The final shipment
|
||||
completed without incident. The ring disperses on its own schedule.
|
||||
Kael stays. The world is unchanged. This is not failure — it's the game
|
||||
acknowledging that not every conspiracy needs a protagonist.
|
||||
The 70% mundane majority (D-029) plays out: life continued.
|
||||
conditions:
|
||||
facts_not_known:
|
||||
- "ring.cargo_discrepancy_pattern" # Only known via player examination of terminal
|
||||
- "ring.kael_unauthorized_corridor_access" # Only known via player observing Kael in B-7
|
||||
ticks_since_activation: 5400 # ~90 minutes with zero player investigation
|
||||
effects:
|
||||
- type: faction_reaction
|
||||
faction: the-ring
|
||||
reaction: neutral
|
||||
description: "The ring closed operations without incident. No record of compromise."
|
||||
@@ -0,0 +1,718 @@
|
||||
# Drama Module Schema — Tier 1 Content (D-023)
|
||||
# YAML expression of JSON Schema 2020-12
|
||||
# Validated against this schema: content/modules/tier1/*.yaml
|
||||
#
|
||||
# Ownership:
|
||||
# Dramatic structure (this file): Paula
|
||||
# YAML validation tooling / serde structs: Gestalt / Tyre
|
||||
# Authoring ergonomics review: Mellanie
|
||||
#
|
||||
# See: docs/design/tier1-module-authoring.md for field-by-field guide.
|
||||
|
||||
$schema: "https://json-schema.org/draft/2020-12/schema"
|
||||
$id: "drama_module.schema.yaml"
|
||||
title: "Tier 1 Drama Module"
|
||||
description: >
|
||||
A hand-authored drama module drawn from the pool at game start.
|
||||
The storyteller activates one or more modules per playthrough based on
|
||||
entry conditions, then fires events and detects outcomes. Tier 1 modules
|
||||
are the conspiracy layer of D-023 — authored, optional, relocatable.
|
||||
type: object
|
||||
required:
|
||||
- module_id
|
||||
- display_name
|
||||
- version
|
||||
- tier
|
||||
- pool
|
||||
- entry_conditions
|
||||
- npc_requirements
|
||||
- events
|
||||
- outcomes
|
||||
additionalProperties: false
|
||||
|
||||
properties:
|
||||
|
||||
# ── IDENTITY ────────────────────────────────────────────────────────────────
|
||||
|
||||
module_id:
|
||||
type: string
|
||||
pattern: "^[a-z][a-z0-9-]*_v[0-9]+_[0-9]+$"
|
||||
description: >
|
||||
Stable unique slug. Format: {name}_v{major}_{minor}.
|
||||
Never reuse IDs. Increment version on breaking structural changes.
|
||||
Example: "smuggling_ring_v0_1"
|
||||
|
||||
display_name:
|
||||
type: string
|
||||
minLength: 1
|
||||
description: "Human-readable title shown in dev/debug tooling."
|
||||
|
||||
version:
|
||||
type: string
|
||||
pattern: "^[0-9]+\\.[0-9]+$"
|
||||
description: "Authoring version. Semantic: major.minor."
|
||||
|
||||
tier:
|
||||
type: integer
|
||||
const: 1
|
||||
description: "Always 1 for Tier 1 drama modules."
|
||||
|
||||
description:
|
||||
type: string
|
||||
description: "One-paragraph authoring summary. Not shown in-game."
|
||||
|
||||
# ── POOL METADATA ─────────────────────────────────────────────────────────
|
||||
# Controls how the storyteller includes this module in the per-playthrough pool.
|
||||
|
||||
pool:
|
||||
type: object
|
||||
required:
|
||||
- weight
|
||||
additionalProperties: false
|
||||
description: "How the storyteller samples this module from the pool."
|
||||
properties:
|
||||
weight:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 10
|
||||
description: >
|
||||
Relative selection probability (1–10). Higher = more likely to be
|
||||
included in a given playthrough's active module set. Default: 5.
|
||||
compatible_districts:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: >
|
||||
District slugs where this module can activate, or omit for "any".
|
||||
Example: ["sova-transit"]
|
||||
incompatible_with:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
pattern: "^[a-z][a-z0-9-]*_v[0-9]+_[0-9]+$"
|
||||
description: >
|
||||
Module IDs that cannot run concurrently with this one.
|
||||
The storyteller will not activate both in the same playthrough.
|
||||
max_concurrent:
|
||||
type: integer
|
||||
minimum: 1
|
||||
default: 1
|
||||
description: >
|
||||
Maximum simultaneous active instances. Almost always 1.
|
||||
Set to 2+ only for modules designed to stack (rare).
|
||||
|
||||
# ── ENTRY CONDITIONS ──────────────────────────────────────────────────────
|
||||
# All listed conditions must be true for the module to become activatable.
|
||||
# The storyteller checks these each tick after min_play_ticks.
|
||||
|
||||
entry_conditions:
|
||||
type: object
|
||||
required:
|
||||
- activation
|
||||
additionalProperties: false
|
||||
description: >
|
||||
World-state prerequisites. The storyteller activates the module when
|
||||
ALL conditions are satisfied AND the activation trigger fires.
|
||||
properties:
|
||||
world_state:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/$defs/world_state_condition"
|
||||
description: "World-state conditions checked each tick."
|
||||
player:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/$defs/player_condition"
|
||||
description: >
|
||||
Optional player-state conditions. Module can activate without
|
||||
player engagement — these gate on player-specific world state,
|
||||
not on player noticing the module.
|
||||
activation:
|
||||
type: object
|
||||
required:
|
||||
- trigger
|
||||
additionalProperties: false
|
||||
description: "How and when activation is evaluated."
|
||||
properties:
|
||||
trigger:
|
||||
type: string
|
||||
enum:
|
||||
- proximity # Player comes within range of a key NPC/location
|
||||
- storyteller_push # Storyteller activates on its own schedule
|
||||
- player_action # Player performs a specific action
|
||||
description: "What pushes the module from 'eligible' to 'active'."
|
||||
min_play_ticks:
|
||||
type: integer
|
||||
minimum: 0
|
||||
description: >
|
||||
Minimum ticks of game time before this module can activate.
|
||||
Enforces D-027 success criterion #1: 30 minutes of daily-life
|
||||
breathing room. At 1 tick/second, 30 minutes ≈ 1800 ticks.
|
||||
proximity_location:
|
||||
type: string
|
||||
description: >
|
||||
Required when trigger = proximity. Location slug the player
|
||||
must enter or approach. Example: "maintenance-corridors"
|
||||
proximity_radius_tiles:
|
||||
type: integer
|
||||
minimum: 1
|
||||
description: >
|
||||
Required when trigger = proximity. Tile radius around the
|
||||
location's anchor point.
|
||||
player_action_required:
|
||||
type: string
|
||||
description: >
|
||||
Required when trigger = player_action. The action that fires
|
||||
activation. Example: "examine:cargo-manifest"
|
||||
|
||||
# ── NPC REQUIREMENTS ──────────────────────────────────────────────────────
|
||||
# NPC slots this module requires. Each slot is filled at module load time.
|
||||
# Named bindings resolve to specific authored NPCs; generated bindings
|
||||
# are filled from the district's generated NPC pool.
|
||||
|
||||
npc_requirements:
|
||||
type: array
|
||||
minItems: 1
|
||||
items:
|
||||
$ref: "#/$defs/npc_slot"
|
||||
description: >
|
||||
Module-internal NPC role slots. Roles are referenced by slug throughout
|
||||
the rest of this document. Hand-authored NPCs use named bindings.
|
||||
Generated NPCs use constraint-based bindings.
|
||||
|
||||
# ── EVENTS ────────────────────────────────────────────────────────────────
|
||||
# Ordered sequences and unordered event pools the storyteller can fire.
|
||||
# Sequences are narrative beats in a defined order.
|
||||
# Pools are events the storyteller can fire in any order when conditions are met.
|
||||
|
||||
events:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
description: "Event sequences and pools the storyteller manages."
|
||||
properties:
|
||||
sequences:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/$defs/event_sequence"
|
||||
description: >
|
||||
Ordered event sequences. Steps fire in order; the next step
|
||||
becomes eligible only after the previous one fires.
|
||||
pools:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/$defs/event_pool"
|
||||
description: >
|
||||
Unordered event pools. The storyteller may fire any eligible
|
||||
event in the pool when its trigger conditions are met.
|
||||
|
||||
# ── OUTCOMES ──────────────────────────────────────────────────────────────
|
||||
# Resolution states the module can reach. The storyteller checks outcome
|
||||
# conditions each tick. First matching outcome wins.
|
||||
# Every module MUST include an expiry outcome.
|
||||
|
||||
outcomes:
|
||||
type: array
|
||||
minItems: 1
|
||||
items:
|
||||
$ref: "#/$defs/outcome"
|
||||
description: >
|
||||
Terminal and transitional resolution states. The storyteller checks
|
||||
these each tick and applies the first matching outcome.
|
||||
|
||||
# ── AUTHORING NOTES ───────────────────────────────────────────────────────
|
||||
|
||||
notes:
|
||||
type: string
|
||||
description: "Authoring-only field. Design rationale, cross-references. Ignored at load time."
|
||||
|
||||
dual_lens:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
description: "Authoring-only. How smuggler vs detective experience this module."
|
||||
properties:
|
||||
smuggler: { type: string }
|
||||
detective: { type: string }
|
||||
|
||||
# ── SHARED DEFINITIONS ────────────────────────────────────────────────────────
|
||||
|
||||
$defs:
|
||||
|
||||
# World-state condition types
|
||||
|
||||
world_state_condition:
|
||||
type: object
|
||||
required:
|
||||
- type
|
||||
description: "A single world-state prerequisite for module activation."
|
||||
oneOf:
|
||||
- # NPC with the given module role is present in the district
|
||||
properties:
|
||||
type: { type: string, const: "npc_present" }
|
||||
role: { type: string, description: "Module-internal NPC role slug." }
|
||||
required: [type, role]
|
||||
additionalProperties: false
|
||||
|
||||
- # A specific location is accessible to the player
|
||||
properties:
|
||||
type: { type: string, const: "location_accessible" }
|
||||
location: { type: string, description: "Location slug." }
|
||||
required: [type, location]
|
||||
additionalProperties: false
|
||||
|
||||
- # Player has NOT yet discovered a specific fact
|
||||
properties:
|
||||
type: { type: string, const: "fact_not_known" }
|
||||
fact_id: { type: string, description: "Fact ID from global/knowledge/." }
|
||||
required: [type, fact_id]
|
||||
additionalProperties: false
|
||||
|
||||
- # No other Tier 1 module of the given ID is currently active
|
||||
properties:
|
||||
type: { type: string, const: "no_active_module" }
|
||||
module_id: { type: string }
|
||||
required: [type, module_id]
|
||||
additionalProperties: false
|
||||
|
||||
- # A named fact IS known (module requires precondition awareness)
|
||||
properties:
|
||||
type: { type: string, const: "fact_known" }
|
||||
fact_id: { type: string }
|
||||
known_by: { type: string, enum: [smuggler, detective, any] }
|
||||
required: [type, fact_id]
|
||||
additionalProperties: false
|
||||
|
||||
# Player-state condition types
|
||||
|
||||
player_condition:
|
||||
type: object
|
||||
required:
|
||||
- type
|
||||
description: "A player-state prerequisite."
|
||||
oneOf:
|
||||
- # Player has reached minimum relationship threshold with an NPC
|
||||
properties:
|
||||
type: { type: string, const: "relationship_threshold" }
|
||||
npc_role: { type: string, description: "Module-internal NPC role." }
|
||||
min_state:
|
||||
type: string
|
||||
enum: [stranger, known, friendly]
|
||||
description: "Minimum RelationshipState required."
|
||||
required: [type, npc_role, min_state]
|
||||
additionalProperties: false
|
||||
|
||||
- # Minimum game ticks elapsed
|
||||
properties:
|
||||
type: { type: string, const: "min_ticks" }
|
||||
ticks: { type: integer, minimum: 0 }
|
||||
required: [type, ticks]
|
||||
additionalProperties: false
|
||||
|
||||
# NPC slot definition
|
||||
|
||||
npc_slot:
|
||||
type: object
|
||||
required:
|
||||
- role
|
||||
- binding
|
||||
additionalProperties: false
|
||||
description: >
|
||||
One NPC slot in the module. Named binding = specific authored NPC.
|
||||
Generated binding = constraint-matched NPC from district pool.
|
||||
properties:
|
||||
role:
|
||||
type: string
|
||||
pattern: "^[a-z][a-z0-9-]*$"
|
||||
description: >
|
||||
Module-internal role slug. Referenced in events, outcomes, and
|
||||
triggers. Example: "ring-leader", "ring-member-exiting", "witness"
|
||||
display_hint:
|
||||
type: string
|
||||
description: "Authoring note. What this role is narratively."
|
||||
binding:
|
||||
type: string
|
||||
enum: [named, generated]
|
||||
description: >
|
||||
named = resolves to a specific authored NPC (use named_npc).
|
||||
generated = any district NPC matching the axis constraints.
|
||||
named_npc:
|
||||
type: string
|
||||
pattern: "^npc:[a-z][a-z0-9-]*$"
|
||||
description: >
|
||||
Required when binding = named. Short-form NPC canonical ID.
|
||||
Example: "npc:kael-davan"
|
||||
axes:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/$defs/axis_constraint"
|
||||
description: >
|
||||
Required when binding = generated. The NPC must satisfy all
|
||||
listed axis constraints to fill this slot.
|
||||
must_have_pattern:
|
||||
type: string
|
||||
enum: [FRIEND, MIRROR, ANCHOR, GHOST, CATALYST, THRESHOLD, REMNANT, SYSTEM, NOBODY]
|
||||
description: "Optional: NPC must have this pattern (D-024)."
|
||||
must_have_motivation:
|
||||
type: string
|
||||
enum: [HANDLER, WITNESS, TURNCOAT, CIVILIAN, OPERATOR, SKEPTIC]
|
||||
description: "Optional: NPC must have this motivation (D-024)."
|
||||
is_optional:
|
||||
type: boolean
|
||||
default: false
|
||||
description: >
|
||||
If true, the module can activate without this slot filled.
|
||||
Optional slots produce degraded but valid module runs.
|
||||
|
||||
# NPC axis constraint (used in generated bindings)
|
||||
|
||||
axis_constraint:
|
||||
type: object
|
||||
required:
|
||||
- axis
|
||||
- constraint
|
||||
additionalProperties: false
|
||||
properties:
|
||||
axis:
|
||||
type: string
|
||||
enum: [want, secret, relationships, tolerance, routine, information, contentment, personality, tells, skills]
|
||||
description: "Which NPC axis to constrain (D-024)."
|
||||
constraint:
|
||||
type: string
|
||||
description: >
|
||||
Constraint expression. Freeform string interpreted by the storyteller.
|
||||
Convention: "has_{value}", "min_{N}", "not_{value}".
|
||||
Examples: "has_major_secret", "min_contentment_-3", "not_combat_trained"
|
||||
|
||||
# Event sequence
|
||||
|
||||
event_sequence:
|
||||
type: object
|
||||
required:
|
||||
- sequence_id
|
||||
- steps
|
||||
additionalProperties: false
|
||||
description: "An ordered sequence of narrative events."
|
||||
properties:
|
||||
sequence_id:
|
||||
type: string
|
||||
pattern: "^[a-z][a-z0-9_-]*$"
|
||||
label:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
steps:
|
||||
type: array
|
||||
minItems: 1
|
||||
items:
|
||||
$ref: "#/$defs/event_step"
|
||||
|
||||
# Unordered event pool
|
||||
|
||||
event_pool:
|
||||
type: object
|
||||
required:
|
||||
- pool_id
|
||||
- events
|
||||
additionalProperties: false
|
||||
properties:
|
||||
pool_id:
|
||||
type: string
|
||||
pattern: "^[a-z][a-z0-9_-]*$"
|
||||
label:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
events:
|
||||
type: array
|
||||
minItems: 1
|
||||
items:
|
||||
$ref: "#/$defs/event_step"
|
||||
|
||||
# Individual event step
|
||||
|
||||
event_step:
|
||||
type: object
|
||||
required:
|
||||
- event_id
|
||||
- triggers
|
||||
additionalProperties: false
|
||||
description: "A single storyteller-managed event with triggers and effects."
|
||||
properties:
|
||||
event_id:
|
||||
type: string
|
||||
pattern: "^[a-z][a-z0-9_-]*$"
|
||||
description: "Unique within this module. Used in outcome conditions."
|
||||
label:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
description: "What happens narratively when this event fires."
|
||||
triggers:
|
||||
type: array
|
||||
minItems: 1
|
||||
items:
|
||||
$ref: "#/$defs/event_trigger"
|
||||
description: "ANY trigger being true fires this event."
|
||||
effects:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/$defs/event_effect"
|
||||
description: "What changes in the world when this event fires."
|
||||
once:
|
||||
type: boolean
|
||||
default: true
|
||||
description: "If true, fires only once. If false, may repeat when conditions reset."
|
||||
sets_flag:
|
||||
type: string
|
||||
pattern: "^[a-z][a-z0-9_-]*$"
|
||||
description: "Module-internal flag set when this event fires. Queryable in outcomes."
|
||||
|
||||
# Event trigger conditions
|
||||
|
||||
event_trigger:
|
||||
type: object
|
||||
required:
|
||||
- type
|
||||
description: "A condition that causes an event to fire."
|
||||
oneOf:
|
||||
- # Ticks elapsed since module activation
|
||||
properties:
|
||||
type: { type: string, const: "ticks_since_activation" }
|
||||
ticks: { type: integer, minimum: 1 }
|
||||
required: [type, ticks]
|
||||
additionalProperties: false
|
||||
|
||||
- # Ticks elapsed since a previous event fired
|
||||
properties:
|
||||
type: { type: string, const: "ticks_since_event" }
|
||||
after_event: { type: string }
|
||||
ticks: { type: integer, minimum: 1 }
|
||||
required: [type, after_event, ticks]
|
||||
additionalProperties: false
|
||||
|
||||
- # Player enters a location or comes within range of NPC
|
||||
properties:
|
||||
type: { type: string, const: "player_proximity" }
|
||||
target_type: { type: string, enum: [location, npc_role] }
|
||||
target: { type: string }
|
||||
radius_tiles: { type: integer, minimum: 1 }
|
||||
required: [type, target_type, target]
|
||||
additionalProperties: false
|
||||
|
||||
- # Player performs an interaction
|
||||
properties:
|
||||
type: { type: string, const: "player_action" }
|
||||
action:
|
||||
type: string
|
||||
enum: [talk, examine, confront, follow, observe]
|
||||
target_role: { type: string, description: "Module NPC role or location slug." }
|
||||
required: [type, action, target_role]
|
||||
additionalProperties: false
|
||||
|
||||
- # Player has discovered a specific fact
|
||||
properties:
|
||||
type: { type: string, const: "fact_known_by_player" }
|
||||
fact_id: { type: string }
|
||||
required: [type, fact_id]
|
||||
additionalProperties: false
|
||||
|
||||
- # A module flag has been set
|
||||
properties:
|
||||
type: { type: string, const: "flag_set" }
|
||||
flag: { type: string }
|
||||
required: [type, flag]
|
||||
additionalProperties: false
|
||||
|
||||
- # NPC enters a specific mood state
|
||||
properties:
|
||||
type: { type: string, const: "npc_mood" }
|
||||
npc_role: { type: string }
|
||||
mood:
|
||||
type: string
|
||||
enum: [anxious, frustrated, content, suspicious, warm, hostile, relieved, focused]
|
||||
required: [type, npc_role, mood]
|
||||
additionalProperties: false
|
||||
|
||||
# Event effects
|
||||
|
||||
event_effect:
|
||||
type: object
|
||||
required:
|
||||
- type
|
||||
description: "A world change triggered by an event."
|
||||
oneOf:
|
||||
- # NPC deviates from their normal routine
|
||||
properties:
|
||||
type: { type: string, const: "npc_routine_deviation" }
|
||||
npc_role: { type: string }
|
||||
description: { type: string, description: "What the deviation looks like." }
|
||||
duration_ticks: { type: integer }
|
||||
required: [type, npc_role, description]
|
||||
additionalProperties: false
|
||||
|
||||
- # A fact becomes discoverable (moves to Rumoured confidence)
|
||||
properties:
|
||||
type: { type: string, const: "fact_becomes_discoverable" }
|
||||
fact_id: { type: string }
|
||||
discoverable_by:
|
||||
type: string
|
||||
enum: [smuggler, detective, any]
|
||||
discovery_method:
|
||||
type: string
|
||||
description: "How the player can discover this. Authoring note."
|
||||
required: [type, fact_id, discoverable_by]
|
||||
additionalProperties: false
|
||||
|
||||
- # NPC tell behavior becomes more pronounced
|
||||
properties:
|
||||
type: { type: string, const: "tell_intensify" }
|
||||
npc_role: { type: string }
|
||||
description: { type: string }
|
||||
required: [type, npc_role]
|
||||
additionalProperties: false
|
||||
|
||||
- # A module-internal flag is set
|
||||
properties:
|
||||
type: { type: string, const: "flag_set" }
|
||||
flag: { type: string, pattern: "^[a-z][a-z0-9_-]*$" }
|
||||
required: [type, flag]
|
||||
additionalProperties: false
|
||||
|
||||
- # Something changes about a location
|
||||
properties:
|
||||
type: { type: string, const: "location_state" }
|
||||
location: { type: string }
|
||||
description: { type: string }
|
||||
required: [type, location, description]
|
||||
additionalProperties: false
|
||||
|
||||
- # NPC's access to information changes
|
||||
properties:
|
||||
type: { type: string, const: "npc_knowledge_update" }
|
||||
npc_role: { type: string }
|
||||
fact_id: { type: string }
|
||||
description: { type: string }
|
||||
required: [type, npc_role, fact_id]
|
||||
additionalProperties: false
|
||||
|
||||
# Module outcome definition
|
||||
|
||||
outcome:
|
||||
type: object
|
||||
required:
|
||||
- outcome_id
|
||||
- label
|
||||
- is_terminal
|
||||
additionalProperties: false
|
||||
description: >
|
||||
A resolution state the module can reach. Conditions are checked each tick.
|
||||
The first matching outcome is applied. is_terminal = true ends the module.
|
||||
properties:
|
||||
outcome_id:
|
||||
type: string
|
||||
pattern: "^[a-z][a-z0-9_-]*$"
|
||||
label:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
description: "What this outcome means narratively."
|
||||
is_terminal:
|
||||
type: boolean
|
||||
description: "If true, this outcome ends the module permanently."
|
||||
is_expiry:
|
||||
type: boolean
|
||||
default: false
|
||||
description: >
|
||||
If true, this is the quiet-exit outcome when the player never engages.
|
||||
Every module must include exactly one expiry outcome.
|
||||
conditions:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
description: "ALL conditions must be true to reach this outcome."
|
||||
properties:
|
||||
facts_known:
|
||||
type: array
|
||||
items: { type: string }
|
||||
description: "Player must know all these facts."
|
||||
facts_not_known:
|
||||
type: array
|
||||
items: { type: string }
|
||||
description: "Player must NOT know any of these facts."
|
||||
flags_set:
|
||||
type: array
|
||||
items: { type: string }
|
||||
description: "All these module flags must be set."
|
||||
flags_not_set:
|
||||
type: array
|
||||
items: { type: string }
|
||||
description: "None of these module flags may be set."
|
||||
events_fired:
|
||||
type: array
|
||||
items: { type: string }
|
||||
description: "All these events must have fired."
|
||||
ticks_since_activation:
|
||||
type: integer
|
||||
description: "Module has been active for at least this many ticks."
|
||||
effects:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/$defs/outcome_effect"
|
||||
description: "Effects applied when this outcome is reached."
|
||||
|
||||
# Outcome-level effects (broader scope than event effects)
|
||||
|
||||
outcome_effect:
|
||||
type: object
|
||||
required:
|
||||
- type
|
||||
oneOf:
|
||||
- # NPC disposition toward player changes
|
||||
properties:
|
||||
type: { type: string, const: "npc_disposition" }
|
||||
npc_role: { type: string }
|
||||
shift:
|
||||
type: string
|
||||
enum: [hostile, suspicious, neutral, friendly]
|
||||
description: { type: string }
|
||||
required: [type, npc_role, shift]
|
||||
additionalProperties: false
|
||||
|
||||
- # Faction reaction
|
||||
properties:
|
||||
type: { type: string, const: "faction_reaction" }
|
||||
faction: { type: string }
|
||||
reaction:
|
||||
type: string
|
||||
enum: [hostile, suspicious, neutral, friendly, grateful]
|
||||
description: { type: string }
|
||||
required: [type, faction, reaction]
|
||||
additionalProperties: false
|
||||
|
||||
- # Location becomes restricted or opens up
|
||||
properties:
|
||||
type: { type: string, const: "location_access_change" }
|
||||
location: { type: string }
|
||||
change:
|
||||
type: string
|
||||
enum: [restricted, locked, open]
|
||||
description: { type: string }
|
||||
required: [type, location, change]
|
||||
additionalProperties: false
|
||||
|
||||
- # A fact is now permanently known/unknown
|
||||
properties:
|
||||
type: { type: string, const: "fact_state" }
|
||||
fact_id: { type: string }
|
||||
state:
|
||||
type: string
|
||||
enum: [known, hidden, destroyed]
|
||||
description: { type: string }
|
||||
required: [type, fact_id, state]
|
||||
additionalProperties: false
|
||||
|
||||
- # NPC leaves the district or changes role
|
||||
properties:
|
||||
type: { type: string, const: "npc_exit" }
|
||||
npc_role: { type: string }
|
||||
description: { type: string }
|
||||
required: [type, npc_role]
|
||||
additionalProperties: false
|
||||
@@ -7,6 +7,7 @@ Direct DB access only for sprint lifecycle mutations.
|
||||
|
||||
Usage:
|
||||
sprint status [--sprint N] [--team T] Sprint progress and ticket overview
|
||||
sprint sweep [--sprint N] Health check: grouped tickets, issues, team summary (JSON)
|
||||
sprint start [--sprint N] Activate a planned sprint
|
||||
sprint stop [--sprint N] Complete an active sprint
|
||||
sprint start-work [--sprint N] [--team T] Full context dump for starting work
|
||||
@@ -503,6 +504,95 @@ def cmd_start_work(args):
|
||||
print(REMINDER)
|
||||
|
||||
|
||||
def cmd_sweep(args):
|
||||
"""Health check: grouped tickets, bookkeeping issues, team summary (JSON)."""
|
||||
flags, _ = parse_flags(args, ["sprint"])
|
||||
sprint = detect_sprint(flags, prefer_status="active")
|
||||
|
||||
tickets = get_tickets_for_sprint(sprint["id"])
|
||||
|
||||
# Build dependency map for blocked detection
|
||||
blocked_by_map = {} # ticket_id -> [blocker_id, ...]
|
||||
for t in tickets:
|
||||
if t["status"] == "done":
|
||||
continue
|
||||
deps = get_ticket_deps(t["id"])
|
||||
open_blockers = [b["id"] for b in deps.get("blocked_by", []) if b["status"] != "done"]
|
||||
if open_blockers:
|
||||
blocked_by_map[t["id"]] = open_blockers
|
||||
|
||||
# Group by status
|
||||
by_status = {"done": [], "review": [], "in_progress": [], "blocked": [], "backlog": []}
|
||||
for t in tickets:
|
||||
entry = {
|
||||
"id": t["id"],
|
||||
"title": t["title"],
|
||||
"team": t.get("team") or "unassigned",
|
||||
"assigned_to": t.get("assigned_to"),
|
||||
}
|
||||
if t["status"] == "done":
|
||||
by_status["done"].append(entry)
|
||||
elif t["id"] in blocked_by_map:
|
||||
entry["blocked_by"] = blocked_by_map[t["id"]]
|
||||
by_status["blocked"].append(entry)
|
||||
elif t["status"] == "review":
|
||||
by_status["review"].append(entry)
|
||||
elif t["status"] == "in_progress":
|
||||
by_status["in_progress"].append(entry)
|
||||
else:
|
||||
by_status["backlog"].append(entry)
|
||||
|
||||
# Per-team summary
|
||||
by_team = {}
|
||||
for status, items in by_status.items():
|
||||
for item in items:
|
||||
team = item["team"]
|
||||
if team not in by_team:
|
||||
by_team[team] = {"backlog": 0, "in_progress": 0, "review": 0, "blocked": 0, "done": 0, "total": 0}
|
||||
by_team[team][status] = by_team[team].get(status, 0) + 1
|
||||
by_team[team]["total"] += 1
|
||||
|
||||
# Bookkeeping issues
|
||||
issues = []
|
||||
for t in tickets:
|
||||
if t["status"] in ("in_progress", "review") and not t.get("assigned_to"):
|
||||
issues.append({
|
||||
"type": "unassigned_in_progress",
|
||||
"detail": f"#{t['id']} unassigned {t['status']}",
|
||||
"fix": f"db/connectors/ticket assign {t['id']} <agent>",
|
||||
})
|
||||
if t["status"] == "backlog" and sprint["status"] == "active" and t["id"] not in blocked_by_map:
|
||||
issues.append({
|
||||
"type": "stale_backlog",
|
||||
"detail": f"#{t['id']} stale backlog",
|
||||
"fix": f"db/connectors/ticket status {t['id']} in_progress",
|
||||
})
|
||||
if t["status"] == "done" and t.get("assigned_to"):
|
||||
issues.append({
|
||||
"type": "assigned_but_done",
|
||||
"detail": f"#{t['id']} done, still assigned",
|
||||
"fix": f"db/connectors/ticket unassign {t['id']}",
|
||||
})
|
||||
|
||||
# Progress
|
||||
total = len(tickets)
|
||||
done = len(by_status["done"])
|
||||
pct = int(done / total * 100) if total > 0 else 0
|
||||
|
||||
result = {
|
||||
"sprint": {
|
||||
"id": sprint["id"],
|
||||
"name": sprint.get("name", f"Sprint {sprint['id']}"),
|
||||
"goal": sprint.get("goal", ""),
|
||||
},
|
||||
"progress": {"total": total, "done": done, "pct": pct},
|
||||
"by_status": by_status,
|
||||
"by_team": by_team,
|
||||
"issues": issues,
|
||||
}
|
||||
print(json.dumps(result, indent=2))
|
||||
|
||||
|
||||
def cmd_prepare(args):
|
||||
flags, _ = parse_flags(args, ["sprint", "team"])
|
||||
sprint = detect_sprint_for_prepare(flags)
|
||||
@@ -606,16 +696,17 @@ HELP = """sprint \u2014 sprint lifecycle and context for agents
|
||||
|
||||
Usage:
|
||||
sprint status [--sprint N] [--team T] Sprint progress and ticket overview
|
||||
sprint sweep [--sprint N] Health check: grouped tickets, issues, team summary (JSON)
|
||||
sprint start [--sprint N] Activate a planned sprint
|
||||
sprint stop [--sprint N] Complete an active sprint
|
||||
sprint start-work [--sprint N] [--team T] Full context dump for starting work
|
||||
sprint prepare [--sprint N] [--team T] Prepare next sprint (candidates + gaps)
|
||||
|
||||
Sprint auto-detection:
|
||||
status/start-work prefer the active sprint
|
||||
start prefer the planning sprint
|
||||
stop prefer the active sprint
|
||||
prepare target next sprint (max id + 1)
|
||||
status/start-work/sweep prefer the active sprint
|
||||
start prefer the planning sprint
|
||||
stop prefer the active sprint
|
||||
prepare target next sprint (max id + 1)
|
||||
|
||||
Team auto-detection:
|
||||
If --team is omitted, uses the current git branch name (unless on main).
|
||||
@@ -632,6 +723,7 @@ def main():
|
||||
|
||||
commands = {
|
||||
"status": cmd_status,
|
||||
"sweep": cmd_sweep,
|
||||
"start": cmd_start,
|
||||
"stop": cmd_stop,
|
||||
"start-work": cmd_start_work,
|
||||
|
||||
@@ -12,10 +12,10 @@ Cross-domain decisions live in one file with cross-reference notes in related fi
|
||||
|------|--------|-----------|
|
||||
| [architecture.md](architecture.md) | Technical foundation | D-008, D-009, D-010, D-012, D-020, D-026, D-030, D-031, D-041, D-042, D-054, D-055, D-066, D-068, D-073 |
|
||||
| [perception.md](perception.md) | Player observation | D-011, D-015, D-016, D-017, D-018, D-019, D-033, D-035, D-043, D-044, D-045, D-046, D-047, D-048, D-049, D-052, D-056, D-057, D-058, D-059, D-060, D-061, D-067, D-069, D-070, D-071, D-072, D-076, D-077, D-078 |
|
||||
| [content.md](content.md) | NPC, dialogue, templates | D-023, D-024, D-025, D-028, D-029, D-032, D-034, D-035, D-036, D-037, D-050, D-062, D-063, D-064, D-074 |
|
||||
| [content.md](content.md) | NPC, dialogue, templates | D-023, D-024, D-025, D-028, D-029, D-032, D-034, D-035, D-036, D-037, D-050, D-062, D-063, D-064, D-074, D-075, D-084 |
|
||||
| [scope.md](scope.md) | Game concept, prototype | D-001, D-003, D-005, D-006, D-007, D-013, D-014, D-027, D-038, D-039, D-051, D-053, D-065 |
|
||||
| [process.md](process.md) | Team, workflow | D-004, D-021, D-022, D-040 |
|
||||
| [questions.md](questions.md) | Open questions | Q-001 through Q-026 |
|
||||
| [questions.md](questions.md) | Open questions | Q-001 through Q-028 |
|
||||
| [rejected.md](rejected.md) | Rejected alternatives | R-001 through R-010 |
|
||||
|
||||
## Querying Decisions
|
||||
|
||||
@@ -182,6 +182,28 @@ How narrative, NPCs, and world content are created: content tiers, NPC generatio
|
||||
- **Raised by:** Tyre (technical analysis, architecture synthesis)
|
||||
- **Dissent:** Gestalt endorses D-075 (reviewed 2026-02-19). The emergent archetype distinction is sufficient: access tier tags already encode "authority figure lines" vs "insider lines" in content; starting knowledge differentials produce different dialogue gate timings per character; adding an archetype filter would create per-character content maintenance burden and dilute the "two keyholes on the same world" experience (D-027). Knowledge vocabulary doc (#368) confirms this works in practice — same fact IDs, different starting confidence levels, different gate-open timing per character. *Nigel's input still pending.*
|
||||
|
||||
### D-084: Dual-namespace line ID scheme — role pool + instance override
|
||||
- **Date:** 2026-02-25
|
||||
- **Decision:** Line IDs for auto-generated NPC content use a dual-namespace approach that eliminates the Q-028 collision problem without altering the existing ID format.
|
||||
- **Role namespace (unchanged, primary):** `{role-slug}_{d|m|e}_{###}` — e.g., `dock-worker_d_001`. These are shared lines delivered by any instance of the role. One file, one ID sequence per role-at-location. No per-instance authoring, no collision possible. All existing authored content is unaffected.
|
||||
- **Instance namespace (new, opt-in):** `{role-slug}-{zero-padded counter}_{d|m|e}_{###}` — e.g., `dock-worker-07_d_001`. Counter is the generation-order rank within the role group for that district, starting at 01, assigned deterministically from the world seed. Used only when a specific generated NPC needs authored content that differs from the role pool.
|
||||
- **Key design choices:**
|
||||
- **Why the collision problem is mostly already solved:** The Q-028 collision framing assumed NPC-scoped IDs require per-instance ID sequences. They don't. D-028 tagged line pools are role-scoped: `dock-worker_d_001` is content that any dock worker can deliver. Forty dock workers all drawing from `dock-worker_d_###` is correct behavior, not a collision. A collision would only occur if two *distinct authored lines* shared the same ID — which the role namespace prevents by definition (one file, one sequence).
|
||||
- **Instance namespace scope:** Opt-in only. Tier 3 (flat wallpaper) and Tier 2 (mundane triangles) auto-generated NPCs use the role pool exclusively. Instance pools are authored only when a specific generated NPC needs content variation the role pool cannot supply (e.g., a generated NPC flagged as a triangle member with unique tell lines).
|
||||
- **History log disambiguation:** The speaker of a line is identified by `StableId`, not by line ID. Line ID identifies content; `StableId` identifies the speaker. `(StableId: 42, line_id: "dock-worker_d_001")` and `(StableId: 43, line_id: "dock-worker_d_001")` are two different log entries for the same content line — no collision in the log.
|
||||
- **Counter stability:** Generation order within a role group is seeded from the world seed. Same seed → same order → same counter assignments. Counter is recorded in the district's NPC roster at world-gen time. The counter survives save/load because it is part of the generated NPC's profile, not recomputed at runtime.
|
||||
- **Schema compatibility:** The existing ID regex `^[a-z][a-z0-9-]*_[dme]_\d{3}$` already accepts `dock-worker-07_d_001`. No regex change required. No content migration required.
|
||||
- **Rejected alternatives:**
|
||||
- **StableId prefix (`npc-00042_d_001`):** StableId is assigned at load time from sorted canonical IDs. Authors cannot know it before writing files. A generate-then-bake pipeline would break the content/generation separation principle. Rejected.
|
||||
- **UUID suffix (`dock-worker-a3f2_d_001`):** UUIDs are stable per seed but change across seeds, orphaning any authored instance content on replay. Rejected.
|
||||
- **Slug registry with collision resolution (`dock-worker`, `dock-worker-2`, ...):** First instance gets a privileged non-suffixed slug while all others get a counter, creating asymmetry with no upside. Rejected.
|
||||
- **Implementation requirement:** The content registry (`server/src/knowledge/registry.rs` or a new `server/src/content/npc_slug.rs`) tracks a `RoleCounter: BTreeMap<String, u32>` per district. Incremented when a generated NPC claims an instance namespace slot. Stored in the district manifest. Provides `generate_instance_slug(role_slug) -> String` returning `{role-slug}-{counter:02}`.
|
||||
- **Hand-authored NPCs:** Unchanged. `kael-davan`, `sera-venn`, and all named authored NPCs keep their current slugs and ID sequences. No migration.
|
||||
- **Resolves:** Q-028
|
||||
- **Cross-reference:** Line ID scheme ([D-035](#d-035-converged-tag-taxonomy-for-dialogue-and-monologue-line-pools)), population model ([D-029](#d-029-population-entanglement-ratio--305020)), NPC generation ([D-024](#d-024-npc-generation-model--10-axes--combat-component))
|
||||
- **Raised by:** Gestalt (Sprint 18, #544). Endorsed by Tyre pending implementation review.
|
||||
- **Dissent:** None.
|
||||
|
||||
---
|
||||
|
||||
*16 decisions. Last updated: 2026-02-19 (D-075 dissent updated)*
|
||||
*17 decisions. Last updated: 2026-02-25 (D-084 added — Q-028 resolution)*
|
||||
|
||||
@@ -398,6 +398,54 @@ How the player observes and interacts with the world: camera, fog, line-of-sight
|
||||
- **Raised by:** Lead (passive panel directive, occlusion filter, monologue separation)
|
||||
- **Dissent:** None
|
||||
|
||||
### D-079: Knowledge Grant Architecture
|
||||
- **Date:** 2026-02-24
|
||||
- **Decision:** All knowledge input (dialogue testimony, physical evidence, POI discovery, NPC-authored initial knowledge) flows through a single `KnowledgeEventType::KnowledgeGranted` event type. No separate event types for different grant sources. The `KnowledgeGrant` YAML schema is extended to an untagged enum supporting `Fact { fact_id, confidence }` and `Entity { entity_ref, attributes, confidence }` variants. The source field (`KnowledgeSource`) distinguishes provenance: `ToldBy { source_id: StableId, tick }` for NPC testimony, `DirectObservation { tick }` for physical evidence, `Heard { tick, range }` for overheard conversations. Grants fire at line selection time in `process_talk_interaction`, server-side, pushed to `KnowledgeEventQueue` and processed on the same tick. A `ContentEntityRegistry` resource (`BTreeMap<String, StableId>`, populated at NPC spawn time) resolves `entity_ref` strings to `StableId`s at grant processing time. Content-load validation enforces: confidence strings parse to valid `KnowledgeConfidence` variants; fact_ids conform to `"category.topic"` format; entity_refs resolve to registered StableIds. Runtime guardrail: if the granting NPC's KG does not contain the fact being granted, the grant is dropped with `tracing::warn!`.
|
||||
- **Scope note — `Compound` grant variant:** `Compound { grants: Vec<KnowledgeGrant> }` (multiple grants from a single line) deferred to Sprint 18. Sprint 17 ships `Fact` and `Entity` variants only. Sprint 18 `EntityGrant { attributes: BTreeMap }` full variant is committed scope, not aspirational (Paula condition, 2026-02-24).
|
||||
- **Rationale:** Unified event type keeps the knowledge system composable. Grant timing at line selection (not client display) ensures D-010 determinism — tick-stamped and server-authoritative. Entity grants are required for contradiction detection: testimony must create `EntityKnowledge` with `ToldBy` source so that a subsequent `DirectObservation` can detect a discrepancy. Physical evidence uses the same mechanism with `DirectObservation` source, which is treated as higher-confidence and cannot be contradicted by the NPC-denial path.
|
||||
- **Raised by:** Knowledge Flow & NPC Information Boundaries Workshop — unanimous on grant timing and unified event type. Entity grants in Sprint 17 per team lead decision (2026-02-24), overriding Paula's Round 2 acceptance of Dudley's FactId workaround.
|
||||
- **Dissent:** Paula (Round 2) accepted the FactId workaround with 3 binding conditions; team lead overrode in favour of entity grants path championed by Tyre, Gestalt, and Dudley. Paula's conditions honoured where applicable: (1) `ToldBy` source flows through `ContradictionDetected` event payload — satisfied by D-083; (2) Sprint 18 entity grants are committed scope; (3) formal record of this commitment.
|
||||
- **Implements:** Tickets #545 (schema), #546 (event + handler)
|
||||
- **Cross-reference:** [D-041](architecture.md#d-041-knowledge-graph-data-model), [D-010](architecture.md#d-010-multiplayer-ready-architectural-baseline), [D-083](#d-083-contradiction-detection-pipeline)
|
||||
|
||||
### D-080: NPC-to-NPC Knowledge Propagation
|
||||
- **Date:** 2026-02-24
|
||||
- **Decision:** NPC-to-NPC knowledge transfer is implemented via a `transfer_npc_knowledge` Bevy system that runs `after(run_npc_conversations)`. The system uses `kg_query.get_many_mut([entity_a, entity_b])` for dual-mutable KG access. Knowledge transfer occurs once per conversation at conversation start, not per-line. Rate: flat `rng.random_range(1..=3)` facts per conversation, drawn from eligible entries sorted by `last_updated_tick` descending (most recent first). Trust-gated filtering: `RelationshipEdge.trust` value determines eligible entries (None tier < 0: no transfer; Surface 0–3: Active facts at KnowsOf+ only; Real 3–7: facts at any confidence + entity observations; Secret 7–10: all Active entries). Confidence cap: `transferred_confidence = min(source_confidence, KnowledgeConfidence::KnowsOf)`. Facts with `disclosure_blocked: true` are never transferred regardless of trust tier. Source construction: `KnowledgeSource::ToldBy { source_id: speaker_sid, tick: time.tick }`. Player overhearing: when the player is within `VOICE_RANGE_TILES` (8) of a knowledge-transferring NPC pair, the player gains entity-level KG entries at `Suspects` confidence with `KnowledgeSource::Heard { tick, range: Medium }`. Specific overheard fact transfer to player deferred to Sprint 18.
|
||||
- **Rationale:** Reuses `run_npc_conversations` proximity detection, deterministic StableId-sorted pairing, and conversation lifecycle — avoids building a separate propagation system. Confidence capping at `KnowsOf` prevents gossip chains from amplifying information. `ToldBy` source construction is the prerequisite for contradiction detection. The `disclosure_blocked` flag models secrets whose sharing is existentially dangerous regardless of trust (D-034 NPC design requirement).
|
||||
- **Resolves:** Q-024
|
||||
- **Raised by:** Workshop — unanimous
|
||||
- **Dissent:** None
|
||||
- **Implements:** Ticket #548
|
||||
- **Cross-reference:** [D-041](architecture.md#d-041-knowledge-graph-data-model), [D-083](#d-083-contradiction-detection-pipeline), [D-078](#d-078-overheard-npc-conversation--passive-dialogue-panel-with-occlusion-filter)
|
||||
|
||||
### D-081: Unprompted Disclosure Design
|
||||
- **Date:** 2026-02-24
|
||||
- **Decision:** Unprompted disclosure is implemented as a filtered KG query producing a `DisclosureCandidates` component, consumed by Layer 4 of the dialogue pipeline. The system runs as `derive_disclosure_candidates` (Active-tier NPCs within player range only; recomputed every 30 ticks via `computed_tick` freshness check). NPCs check their own KG only — no cross-entity KG queries (D-010 principle 2). Candidate selection filters: Active state only; confidence ≥ KnowsOf (Cautious trait raises threshold); not in `per_fact_history` (not already disclosed this cooldown period); `disclosure_blocked != true`. Trait two-stage filter: Stage 1 (what) filters candidate pool by trait-based predicates (Cautious raises confidence floor, Gossipy lowers it, Loyal suppresses facts about protected entities, Talkative overrides witness inhibition). Stage 2 (how) biases line pool scoring in Layer 4. Trigger gates (all must pass): trust tier ≥ Surface toward player; mood not Hostile; contentment ≥ −10; candidates not empty; witness inhibition (no non-trusted NPCs within 5 tiles, OR NPC-player trust = Secret tier, OR Talkative trait override); location privacy (`disclosure_context` tag compatible with current zone type: `private`/`semi_private`/`any`); per-NPC cooldown (300 ticks / 30 game-minutes) not active; global rate limit not exceeded. Global rate limit: 1 disclosure per 10 ticks maximum across all NPCs; when multiple candidates ready in same window, select by ascending StableId (deterministic, D-010). Rate limiting layers: (1) per-fact per-NPC `per_fact_history` — primary; (2) per-NPC 300-tick cooldown — secondary; (3) global 1/10-tick cap — tertiary.
|
||||
- **Rationale:** NPCs checking only their own KG (Option A) is both diegetically correct and mechanically richer: NPCs can say things the player already knows, which creates dramatic irony. Option B (NPC checks player's KG) would collapse this asymmetry at exactly the moments their speech would be most dramatically charged. Separate `DisclosureCandidates` component prevents DerivedTellState serialization bloat. The location privacy gate creates learnable spatial behavior patterns. The per-fact `per_fact_history` is the primary narrative quality gate.
|
||||
- **Raised by:** Workshop — unanimous on architecture; Paula and Gestalt on trigger gates; Tyre and Dudley on implementation structure
|
||||
- **Dissent:** Gestalt (Round 1) opposed global rate limit as creating invisible NPC competition. Resolved in Round 2 via deterministic StableId selection. Included.
|
||||
- **Implements:** Tickets #551, #172, #173
|
||||
- **Cross-reference:** [D-041](architecture.md#d-041-knowledge-graph-data-model), [D-082](#d-082-npc-information-boundaries--mvp-scope), [D-034](content.md#d-034-the-friend-npc-archetype)
|
||||
|
||||
### D-082: NPC Information Boundaries — MVP Scope
|
||||
- **Date:** 2026-02-24
|
||||
- **Decision:** Sprint 17 MVP information boundary scope: (1) `tell_state.rs` reads NPC relationship state from KG for OTHER-entity state (replaces direct `relationships.entries` read for Friendly tell derivation); self-axis components (Secret, Contentment, Tolerance, Mood) continue as ground-truth reads. (2) Unprompted disclosure (D-081) uses KG for candidate selection. No other system retrofits in Sprint 17. `derive_tell_state` adds `Option<&KnowledgeGraph>` to query; `None` falls through to current axis-based behavior (existing tests continue passing). All KG-driven behavior scoped to `With<ActiveSim>` — Background-tier NPCs receive no KG-based boundary changes. Deferred: conversation partner KG-awareness check (Sprint 18 candidate, `kg.knows_entity(&partner_sid)` in pairing loop); routine KG awareness (v0.2+); pathfinding KG integration (not planned — failure mode has no safe recovery). Fallback on missing KG entry: fall through to ground truth with `tracing::debug!` structured logging.
|
||||
- **Rationale:** The minimum viable boundary produces maximum gameplay-visible difference for minimum risk. Pathfinding from KG has an unresolvable failure mode (NPC forgets path nodes = stuck NPCs = undefined movement behavior) and must not be implemented. Self-knowledge always comes from axis components because the NPC is the continuous observer of its own state. The `Option<&KnowledgeGraph>` addition is backward-compatible.
|
||||
- **Raised by:** Workshop — unanimous
|
||||
- **Dissent:** None. Paula adds that `tell_state` should eventually incorporate KG-derived secret-exposure intensity — deferred to a future sprint as enhancement.
|
||||
- **Implements:** Tickets #549 (step 1), #551 (step 2), ticket #142
|
||||
- **Cross-reference:** [D-041](architecture.md#d-041-knowledge-graph-data-model), [D-081](#d-081-unprompted-disclosure-design), [D-026](architecture.md#d-026-npc-simulation-tier-system)
|
||||
|
||||
### D-083: Contradiction Detection Pipeline
|
||||
- **Date:** 2026-02-24
|
||||
- **Decision:** Contradiction detection is event-driven at KG write time — fires inside `observe_entity()` (`server/src/knowledge/graph.rs`) before overwriting an existing entry. Detection algorithm: if the existing entry has `source: ToldBy { tick: told_tick }` AND `last_known_position` differs from incoming position AND `|current_tick - told_tick| < CONTRADICTION_WINDOW_TICKS` (default 600; configurable per contradiction type), then: (1) `entry.state = KnowledgeState::Contradicted`; (2) `entry.contradicted_claim = Some(ContradictionClaim { source: entry.source.clone(), claimed_position: entry.last_known_position, detected_at_tick: current_tick })`; (3) proceed to write new observation. A `ContradictionDetected { observer, entity_sid, source_display_name: Option<String>, subject_display_name: Option<String> }` event is pushed; display names are resolved at detection time via `EntityRegistry + NpcName` so the monologue system is a pure string consumer. `EntityKnowledge` gains one new field: `pub contradicted_claim: Option<ContradictionClaim>` with `#[serde(skip_serializing_if = "Option::is_none")]` and `#[serde(default)]` — backward-compatible, zero migration required. Downstream event chain: `ContradictionDetected` → monologue system (named contradiction line) → relationship shift (`ToldBy` source entity → `PersonOfInterest`) → D-033 amber via existing pipeline → `AnomalyMarker` via `detect_anomalies` (already tested). Both the `ToldBy` entry and the `DirectObservation` entry receive `Contradicted` state (epistemic neutrality). Location contradiction is automatic (Sprint 17). Attribute contradiction uses content-authored YAML pairs (Sprint 18). Fact contradiction uses content-authored `contradicts` field on KnowledgeGrant entries (Sprint 18). `CONTRADICTION_WINDOW_TICKS = 600` (1 game-hour) as default constant, with content-authored override per claim type.
|
||||
- **Rationale:** Event-driven detection avoids per-tick KG scan overhead (99% of ticks have no new information). The `ContradictionClaim` struct (typed) was chosen over string encoding in `known_attributes`: the struct is accessed with direct field reads (no string parsing in the detection hot path); the monologue system needs a typed `KnowledgeSource` to resolve the source entity's name; string encoding in `known_attributes` mixes semantic metadata with internal bookkeeping; one optional struct field is additive (no breaking changes, no migration). Steps 2–5 of the downstream chain (anomaly marker, D-033 color, relationship shift, monologue selection) are already implemented and passing tests. Contradiction detection is the missing link that enables THE FRIEND arc.
|
||||
- **Resolves:** Q-026
|
||||
- **Raised by:** Workshop — unanimous on architecture and downstream chain; Tyre and Gestalt on struct approach; Dudley conceded Option B (string encoding).
|
||||
- **Dissent:** Dudley (Round 1): Option B string encoding as Sprint 17 workaround. Conceded in Round 2. No standing dissent.
|
||||
- **Implements:** Tickets #547 (struct + detection), #550 (monologue + event chain)
|
||||
- **Cross-reference:** [D-034](content.md#d-034-the-friend-npc-archetype), [D-033](perception.md#d-033-entity-color--relationship-to-player), [D-041](architecture.md#d-041-knowledge-graph-data-model), [D-079](#d-079-knowledge-grant-architecture), [D-080](#d-080-npc-to-npc-knowledge-propagation)
|
||||
|
||||
---
|
||||
|
||||
*32 decisions. Last updated: 2026-02-20 (D-061: amended with unified conversation log architecture from Sprint 14 #535)*
|
||||
*37 decisions. Last updated: 2026-02-24 (D-079–D-083: Knowledge Flow & NPC Information Boundaries Workshop)*
|
||||
|
||||
@@ -131,26 +131,23 @@ Tracked questions awaiting discussion or resolution.
|
||||
- **Source:** Architecture Review Audit 2026-02-11
|
||||
|
||||
### Q-024: Gossip propagation timing
|
||||
- **Status:** Open (preferred direction: queued)
|
||||
- **Question:** When does NPC-to-NPC knowledge transfer occur? Two options: (1) Immediate during conversation — knowledge transfers instantly when two NPCs talk, more reactive but harder to predict. (2) Queued for routine intersection — knowledge transfers at scheduled meeting points (bar visits, shift changes), more predictable and player can exploit timing windows.
|
||||
- **Preferred direction:** Queued approach aligns better with core gameplay. If gossip propagates at routine intersections, the player can observe NPCs meeting and predict information flow, time actions between intersection points to exploit windows of ignorance, and strategically attend/skip routine events to control what they overhear. Creates the "I need to be at the bar before shift change to see who talks to whom" mechanic. Immediate propagation makes gossip invisible and unpredictable; queued propagation makes it observable and exploitable, which is the design goal (D-010 principle 2, D-028 Layer 4).
|
||||
- **Context:** Deferred to Sprint 3. Making this decision now without implementation experience would be premature. Gossip propagation is not in Sprint 2 scope (D-041).
|
||||
- **Assigned to:** Gestalt, Tyre
|
||||
- **Source:** Knowledge Graph & Information Boundaries Workshop (Gestalt Round 1)
|
||||
- **Status:** Resolved → [D-080](perception.md#d-080-npc-to-npc-knowledge-propagation)
|
||||
- **Resolution:** Knowledge transfer occurs via a separate `transfer_npc_knowledge` Bevy system running `after(run_npc_conversations)`. Transfer fires once per conversation at conversation start (immediate during conversation, not queued). Rate: 1–3 facts drawn by recency. Trust-tier gated. See D-080 for full specification.
|
||||
- **Closed by:** Knowledge Flow & NPC Information Boundaries Workshop — unanimous. 2026-02-24.
|
||||
- **Source:** Knowledge Graph & Information Boundaries Workshop (Gestalt Round 1); resolved in Knowledge Flow & NPC Information Boundaries Workshop Round 2.
|
||||
|
||||
### Q-025: Knowledge graph cap and eviction strategy
|
||||
- **Status:** Open
|
||||
- **Question:** At what point does an NPC's knowledge graph need entry eviction? What is the eviction policy? Options: MAX_ENTITY_KNOWLEDGE constant (e.g., 100 entities), LRU by `last_observed_tick`, lowest confidence first, hybrid approach. How are evicted entries handled — complete removal, or archival to "forgotten" state that can be refreshed?
|
||||
- **Context:** Memory budget analysis shows ~14 KB per NPC with 50 entities + 20 facts. At 80 Active NPCs this is ~1.1 MB, well within budget. However, long-running sessions or NPCs with high interaction rates could accumulate entries. Eviction policy affects gameplay: forgetting low-confidence rumors creates natural information decay; forgetting old observations simulates memory limits.
|
||||
- **Assigned to:** Tyre, Dudley
|
||||
- **Source:** Knowledge Graph & Information Boundaries Workshop (Dudley Round 1, section 8.3)
|
||||
- **Status:** Resolved — no cap or eviction needed for v0.1/v0.2. Re-evaluation trigger: Active NPC count > 200 OR KG memory exceeds 50 MB.
|
||||
- **Question:** At what point does an NPC's knowledge graph need entry eviction? What is the eviction policy?
|
||||
- **Resolution (2026-02-24, confirmed by Knowledge Flow workshop):** Current analysis: ~14 KB per Active NPC KG (50 entities + 20 facts, D-041 budget). 80 Active NPCs = ~1.1 MB. 2,000 Background NPCs at 10 entries = ~5 MB. Total ~6 MB. With gossip propagation shipping in Sprint 17 (D-080, 1–3 facts per conversation): estimated ~12 MB peak at current NPC counts. Neither re-evaluation condition expected before v0.3. The existing decay system (`decay_knowledge` in `knowledge/events.rs`) downgrades confidence and marks entries Stale but does not remove them — correct behavior (preserves "I used to know X" for narrative). If eviction becomes necessary, simplest policy: on each decay pass, if entities.len() > MAX_ENTITIES, remove Stale entries with lowest last_updated_tick. BTreeMap makes this O(N).
|
||||
- **Closed by:** Knowledge Flow & NPC Information Boundaries Workshop — Tyre, Gestalt, Dudley confirmed; Paula non-objection noted. 2026-02-24.
|
||||
- **Source:** Knowledge Graph & Information Boundaries Workshop (Dudley Round 1, section 8.3). Architecture audit 2026-02-23. Workshop confirmation 2026-02-24.
|
||||
|
||||
### Q-026: Contradiction detection algorithm
|
||||
- **Status:** Open
|
||||
- **Question:** How exactly does the system detect that two knowledge entries contradict each other? Options: (1) Content-authored contradiction pairs (explicit markup in content files: "fact A contradicts fact B"), (2) Automatic same-subject different-value detection (algorithmic comparison of EntityKnowledge fields), (3) Hybrid (author-marked contradictions plus automatic location/state conflicts). How granular should contradiction detection be? Entity location only, or also attributes, relationships, facts?
|
||||
- **Context:** THE FRIEND arc (D-034, D-039 wow moment #3) requires contradiction detection for emotional impact. The canonical example: Sera tells detective "Kael was at the dock during second shift" (ToldBy source), then detective observes Kael in corridor B-7 at that time (DirectObservation source). System must detect location contradiction, set both entries to Contradicted state, emit monologue event, and shift relationship to PersonOfInterest.
|
||||
- **Assigned to:** Gestalt, Paula, Dudley
|
||||
- **Source:** Knowledge Graph & Information Boundaries Workshop (Gestalt/Paula Round 1)
|
||||
- **Status:** Resolved → [D-083](perception.md#d-083-contradiction-detection-pipeline)
|
||||
- **Resolution:** Event-driven detection at KG write time in `observe_entity()`, using `ContradictionClaim` struct. Location contradiction is automatic (Sprint 17): position comparison + time window (CONTRADICTION_WINDOW_TICKS = 600). Attribute and fact contradiction are content-authored (Sprint 18). Both ToldBy and DirectObservation entries receive Contradicted state (epistemic neutrality). `ContradictionDetected` event → monologue with resolved display names → relationship shift → AnomalyMarker via existing pipeline.
|
||||
- **Closed by:** Knowledge Flow & NPC Information Boundaries Workshop — unanimous on architecture. 2026-02-24.
|
||||
- **Source:** Knowledge Graph & Information Boundaries Workshop (Gestalt/Paula Round 1); resolved in Knowledge Flow & NPC Information Boundaries Workshop Round 2.
|
||||
|
||||
### Q-027: Fast-travel system design
|
||||
- **Status:** Open
|
||||
@@ -163,6 +160,14 @@ Tracked questions awaiting discussion or resolution.
|
||||
- **Assigned to:** Gestalt, Paula, Tyre
|
||||
- **Source:** Sprint 10 PR review discussion (2026-02-19)
|
||||
|
||||
### Q-028: Collision-resistant line IDs for auto-generated NPCs
|
||||
- **Status:** Resolved → [D-084](content.md#d-084-dual-namespace-line-id-scheme--role-pool--instance-override)
|
||||
- **Resolution:** The collision problem is mostly already solved by the role-pool architecture: `dock-worker_d_###` lines are shared content for all instances of the role, not per-instance IDs. A true collision (two distinct authored lines sharing the same ID) cannot occur with one file per role. For the edge case of authored instance-specific content, a role-slug + zero-padded generation counter suffix produces `dock-worker-07_d_001`. Counter is seeded-deterministic. No schema change, no migration. Hand-authored NPCs unchanged.
|
||||
- **Closed by:** Gestalt (Sprint 18, #544). 2026-02-25.
|
||||
- **Ticket:** #544
|
||||
- **Assigned to:** Gestalt, Tyre
|
||||
- **Source:** Sprint 16 PR #59 review discussion (2026-02-23)
|
||||
|
||||
---
|
||||
|
||||
*27 questions (3 resolved, 2 partially resolved, 22 open). Last updated: 2026-02-19*
|
||||
*28 questions (7 resolved, 1 partially resolved, 20 open). Last updated: 2026-02-25 (Q-028 resolved → D-084)*
|
||||
|
||||
@@ -113,7 +113,7 @@ lines:
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `topic` | list\<enum\> | no | `[]` | Topic tags for Layer 4 weighted selection. Lines without topic tags are eligible for any topic context. Values: `colleague`, `routine`, `cargo`, `money`, `trust`, `danger`, `institution`, `personal`, `investigation`. |
|
||||
| `mood` | list\<enum\> | no | `[]` | Mood tags for Layer 4 weighted selection. Lines without mood tags are eligible for any mood context. Values: `fond`, `comfortable`, `worried`, `suspicious`, `analytical`, `conflicted`, `concerned`, `relieved`. |
|
||||
| `mood` | list\<enum\> | no | `[]` | Mood tags for Layer 4 weighted selection. Lines without mood tags are eligible for any mood context. Values: `anxious`, `frustrated`, `content`, `suspicious`, `warm`, `hostile`, `relieved`, `focused`. |
|
||||
| `tags` | list\<string\> | no | `[]` | Freeform escape hatch. Not consumed by the filtering engine — used for author organization, content queries, and the line previewer. No validation on values. |
|
||||
|
||||
#### Knowledge grant (optional)
|
||||
@@ -157,33 +157,33 @@ All lines in pool
|
||||
location: the-last-shift
|
||||
role: dock-worker
|
||||
lines:
|
||||
- id: the-last-shift_d_001
|
||||
- id: kael-davan_d_015
|
||||
text: "Saved you a seat. Lera's got the spiced rice tonight."
|
||||
role: dock-worker
|
||||
access: [insider, peer]
|
||||
trust: surface
|
||||
situation: [bar_evening, social, arrival]
|
||||
mood: [fond]
|
||||
mood: [warm]
|
||||
topic: [personal, colleague]
|
||||
tags: [kael, greeting, phase-1]
|
||||
|
||||
- id: the-last-shift_d_010
|
||||
- id: kael-davan_d_024
|
||||
text: "Nils wants to talk. Tomorrow, bay side. Said it's about volume."
|
||||
role: dock-worker
|
||||
access: [insider]
|
||||
trust: real
|
||||
situation: [bar_evening, social]
|
||||
mood: [concerned]
|
||||
mood: [anxious]
|
||||
topic: [danger]
|
||||
tags: [kael, ring-ops, nils]
|
||||
|
||||
- id: the-last-shift_d_012
|
||||
- id: kael-davan_d_026
|
||||
text: "Lera knows more than she lets on. She won't say anything — but don't test it."
|
||||
role: dock-worker
|
||||
access: [insider]
|
||||
trust: real
|
||||
situation: [bar_evening, social, alone]
|
||||
mood: [concerned]
|
||||
mood: [anxious]
|
||||
topic: [colleague, danger]
|
||||
tags: [kael, ring-ops, lera, caution]
|
||||
knowledge_grant:
|
||||
@@ -291,13 +291,13 @@ character: detective
|
||||
location: the-last-shift
|
||||
lines:
|
||||
# Basic atmospheric line — no prerequisites, any visit
|
||||
- id: the-last-shift_m_d_001
|
||||
- id: pc-detective_m_d_001
|
||||
text: "The Last Shift. Only place in this district that doesn't smell like freight lubricant."
|
||||
trigger: enter_location
|
||||
tags: [arrival, atmospheric]
|
||||
|
||||
# Knowledge-gated observation — requires prior suspicion
|
||||
- id: the-last-shift_m_d_021
|
||||
- id: pc-detective_m_d_021
|
||||
text: "Sera left when Torek arrived. Second time. Different excuse. Same result."
|
||||
trigger: observe_anomaly
|
||||
prerequisites:
|
||||
@@ -308,7 +308,7 @@ lines:
|
||||
tags: [npc, sera, torek, tell, friend-arc]
|
||||
|
||||
# Relationship-gated line — requires person_of_interest status
|
||||
- id: the-last-shift_m_d_026
|
||||
- id: pc-detective_m_d_026
|
||||
text: "Same booth. Same warm smile. Same offer to buy me a drink. Everything except the truth."
|
||||
trigger: observe_npc
|
||||
prerequisites:
|
||||
@@ -326,15 +326,15 @@ lines:
|
||||
### 5.1 Pattern
|
||||
|
||||
```
|
||||
{location-slug}_{type}_{character?}_{sequence}
|
||||
{npc-slug}_{type}_{character?}_{sequence}
|
||||
```
|
||||
|
||||
| Segment | Format | Values | Example |
|
||||
|---------|--------|--------|---------|
|
||||
| `location-slug` | kebab-case | Matches location directory name, or `general` | `the-last-shift`, `general` |
|
||||
| `npc-slug` | kebab-case | NPC name in kebab-case. Each NPC has an independent sequence. | `kael-davan`, `dock-worker`, `pc-detective` |
|
||||
| `type` | single char | `d` = dialogue, `m` = monologue, `e` = environmental (future) | `d`, `m` |
|
||||
| `character` | single char | `s` = smuggler, `d` = detective. **Monologue only.** | `s`, `d` |
|
||||
| `sequence` | 3-digit zero-padded | `001`–`999` | `001`, `042` |
|
||||
| `sequence` | 3-digit zero-padded | `001`–`999` per NPC | `001`, `042` |
|
||||
|
||||
**General validation regex** (matches both dialogue and monologue IDs):
|
||||
|
||||
@@ -342,26 +342,26 @@ lines:
|
||||
^[a-z0-9-]+_(d|m)(_[a-z])?_\d{3}$
|
||||
```
|
||||
|
||||
- `[a-z0-9-]+` — location slug (kebab-case, at least one character)
|
||||
- `[a-z0-9-]+` — npc slug (kebab-case, at least one character)
|
||||
- `(d|m)` — pool type: `d` for dialogue, `m` for monologue
|
||||
- `(_[a-z])?` — optional character segment (monologue only): `_s` or `_d`
|
||||
- `\d{3}` — three-digit zero-padded sequence number
|
||||
|
||||
Use the pool-specific regexes in [Section 5.2](#52-regex-patterns) for strict per-type validation. This general regex is useful for quick format checks that accept either type.
|
||||
|
||||
> **D-035 deviation note:** D-035 specifies `{template}_{d|m|e}_{###}`. This spec refines that to `{location-slug}_{d|m}_{###}` (dialogue) and `{location-slug}_m_{s|d}_{###}` (monologue) for two reasons: (1) location-slug is more precise than template name and matches the directory hierarchy, and (2) monologue IDs include a character segment (`s`/`d`) to ensure uniqueness across the hard character partition (D-032). All existing authored content already uses this refined format. D-035 should be updated to reflect the implemented convention.
|
||||
> **D-035 Sprint 15 amendment:** IDs are NPC-scoped, not location-scoped. The old `{location-slug}_{d|m}_{###}` format caused collisions when the same NPC appeared at multiple locations (e.g. `the-terminal_d_039` appeared in multiple NPC files). The new format `{npc-slug}_{d|m}_{###}` gives each NPC an independent 999-line ceiling. For multi-location NPCs, sequences are globally continuous across files (e.g. kael-davan uses _001-_014 at maintenance-corridors, _015-_033 at the-last-shift, _034-_075 at the-terminal). Single-location NPCs start at _001. Monologue uses per-file restart with composite key (file_path + line_id).
|
||||
|
||||
### 5.2 Regex patterns
|
||||
|
||||
| Pool type | Regex | Example |
|
||||
|-----------|-------|---------|
|
||||
| Dialogue | `^[a-z][a-z0-9-]*_d_[0-9]{3}$` | `the-last-shift_d_001` |
|
||||
| Monologue | `^[a-z][a-z0-9-]*_m_[sd]_[0-9]{3}$` | `the-last-shift_m_d_021`, `general_m_s_003` |
|
||||
| Dialogue | `^[a-z][a-z0-9-]*_d_[0-9]{3}$` | `kael-davan_d_001` |
|
||||
| Monologue | `^[a-z][a-z0-9-]*_m_[sd]_[0-9]{3}$` | `pc-detective_m_d_021`, `pc-smuggler_m_s_003` |
|
||||
|
||||
### 5.3 Uniqueness scope
|
||||
|
||||
- IDs must be unique **within a single YAML file**.
|
||||
- IDs are **not required to be globally unique** — location slug + file path provides global uniqueness. The engine uses `(file_path, line_id)` as the composite key.
|
||||
- IDs are **not required to be globally unique** — npc-slug + file path provides global uniqueness. The engine uses `(file_path, line_id)` as the composite key.
|
||||
- Sequence numbers need not be contiguous. Gaps are expected when lines are removed or reordered.
|
||||
|
||||
### 5.4 ID stability
|
||||
@@ -406,9 +406,9 @@ All enum values are defined in `content/global/enums/` and validated by the JSON
|
||||
|
||||
### 6.5 Moods (D-028 Layer 4)
|
||||
|
||||
`fond`, `comfortable`, `worried`, `suspicious`, `analytical`, `conflicted`, `concerned`, `relieved`
|
||||
`anxious`, `frustrated`, `content`, `suspicious`, `warm`, `hostile`, `relieved`, `focused`
|
||||
|
||||
8 values for v0.1.
|
||||
8 values for v0.1. (Updated Sprint 14 amendment to D-035.)
|
||||
|
||||
### 6.6 Monologue triggers
|
||||
|
||||
|
||||
@@ -234,10 +234,10 @@ All schema files live in `content/_schema/`. They use JSON Schema draft 2020-12.
|
||||
|
||||
| Content Type | ID Pattern | Example |
|
||||
|---|---|---|
|
||||
| Dialogue | `{location_slug}_{d}_{###}` | `the-terminal_d_001` |
|
||||
| Monologue (smuggler) | `{location_slug}_m_s_{###}` | `the-terminal_m_s_001` |
|
||||
| Monologue (detective) | `{location_slug}_m_d_{###}` | `the-terminal_m_d_001` |
|
||||
| Examine | `{location_slug}_{e}_{###}` | `the-terminal_e_001` |
|
||||
| Dialogue | `{npc-slug}_d_{###}` | `kael-davan_d_001` |
|
||||
| Monologue (smuggler) | `{npc-slug}_m_s_{###}` | `pc-smuggler_m_s_001` |
|
||||
| Monologue (detective) | `{npc-slug}_m_d_{###}` | `pc-detective_m_d_001` |
|
||||
| Examine | `{npc-slug}_{e}_{###}` | `kael-davan_e_001` |
|
||||
|
||||
Line IDs are stable. They are never reused, even if a line is deleted. Numbering gaps are expected and acceptable.
|
||||
|
||||
@@ -586,7 +586,7 @@ For quick reference during authoring. Authoritative source: `content/global/enum
|
||||
|
||||
**Topics (9):** `colleague`, `routine`, `cargo`, `money`, `trust`, `danger`, `institution`, `personal`, `investigation`
|
||||
|
||||
**Moods (8):** `fond`, `comfortable`, `worried`, `suspicious`, `analytical`, `conflicted`, `concerned`, `relieved`
|
||||
**Moods (8):** `anxious`, `frustrated`, `content`, `suspicious`, `warm`, `hostile`, `relieved`, `focused`
|
||||
|
||||
**Access Tiers (5):** `public`, `insider`, `authority`, `peer`, `hostile`
|
||||
|
||||
|
||||
@@ -0,0 +1,606 @@
|
||||
# Insert/HUD Wireframe and Visual Specification — v0.1
|
||||
## Dual Character Variants: Smuggler + Detective
|
||||
|
||||
**Ticket:** #314
|
||||
**Author:** Araminta (Visual Designer)
|
||||
**Date:** 2026-02-24
|
||||
**Status:** Active — feeds #263 (time display client implementation) and #151 (minimap rendering)
|
||||
**Visual grammar reference:** `docs/design/visual-grammar-v01.md` (§7 Neural Insert Overlay)
|
||||
|
||||
---
|
||||
|
||||
## 1. System Architecture
|
||||
|
||||
The insert HUD is not a UI panel bolted onto the game. It is the player character's **neural lattice** — Settled Reach technology that overlays computational data onto the character's perception. Every element on z-layer 6 is diegetic: the character is experiencing this information through their implant, not reading a game interface.
|
||||
|
||||
This has direct design consequences:
|
||||
|
||||
- The insert does not appear/disappear — it activates and deactivates. The character's lattice is always there; they choose when to engage it.
|
||||
- Labels use the character's vocabulary, not the game's. The smuggler's insert calls it "The Last Shift." The detective's insert calls it "Licensed Establishment, District 7."
|
||||
- The same underlying `ObserverSnapshot` feeds both variants. One parameterized system: `lattice_profile` on the snapshot determines which visual mode renders. Stig builds one HUD framework, not two. The variants are rendering configurations, not separate systems.
|
||||
|
||||
### 1.1 Technical Parameters
|
||||
|
||||
| Parameter | Smuggler | Detective |
|
||||
|-----------|----------|-----------|
|
||||
| `lattice_profile` | `lattice_baseline` | `lattice_augmented` |
|
||||
| Line weight | 1px | 1–2px |
|
||||
| Annotation density | Sparse — essential markers only | Dense — more markers, connection lines |
|
||||
| Passive opacity | 60% | 80% |
|
||||
| Active opacity | 85% | 100% |
|
||||
| Bloom | 2–3px gaussian at 40% blend | 2–3px gaussian at 40% blend |
|
||||
| Chrome color | `#c8d0e0` | `#c8d0e0` |
|
||||
|
||||
### 1.2 Z-Layer Position
|
||||
|
||||
Insert HUD lives entirely on **z-layer 6**. It renders above fog (z-layer 5) and below monologue/UI (z-layer 7). The fog shader does not mask it — insert data is computational, not perceptual. Entity markers can appear in fogged areas if the lattice has position data.
|
||||
|
||||
---
|
||||
|
||||
## 2. Screen Layout
|
||||
|
||||
**Base resolution:** 1920×1080px
|
||||
|
||||
The HUD has four zones:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ [STANCE] [PERCEPTION] [TIME] │ ← HUD chrome band (y: 0–80px)
|
||||
│ │
|
||||
│ │
|
||||
│ WORLD VIEW │
|
||||
│ (insert entity markers rendered here, │
|
||||
│ positioned at entity world locations) │
|
||||
│ │
|
||||
│ ↑ ↑ ↑ ↑ border arrows appear here │
|
||||
│ ← (for off-screen known POIs) → │
|
||||
│ ↓ ↓ ↓ ↓ │
|
||||
│ │
|
||||
│ [INVENTORY] [INVENTORY] │ ← inventory band (y: 816–864px)
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ DIALOGUE BOX (z-layer 7, when active) │ ← bottom 20% (y: 864–1080px)
|
||||
│ [NPC speech] │
|
||||
│ [response options] │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Coordinate system:** Origin (0,0) is top-left. X increases rightward, Y increases downward. All pixel values at 1920×1080 base; Godot 4 DPI scaling handles other resolutions.
|
||||
|
||||
**Dialogue box:** The bottom 20% (216px) is reserved when dialogue is active. Insert overlay remains visible at 60% of its normal opacity when dialogue is open — the lattice keeps running, the character keeps perceiving. The insert does not suppress during dialogue.
|
||||
|
||||
---
|
||||
|
||||
## 3. Time Display
|
||||
|
||||
**Feeds:** Ticket #263 (client time display implementation)
|
||||
|
||||
### 3.1 Position and Dimensions
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Container x | 1720px → 1904px (right-inset 16px, 184px wide) |
|
||||
| Container y | 16px → 64px (48px tall) |
|
||||
| Background | `#0e1218` at 40% opacity — barely-there panel behind text |
|
||||
| Typeface | Michroma Regular 400 |
|
||||
| Alignment | Right-aligned text, right-inset 8px within container |
|
||||
|
||||
This position is intentionally unremarkable. The player should find it without hunting for it.
|
||||
|
||||
### 3.2 Smuggler Variant
|
||||
|
||||
Single line: the time. Informal, just the clock.
|
||||
|
||||
```
|
||||
09:42
|
||||
```
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Text | `{HH}:{MM}` — 24-hour format, always two digits each |
|
||||
| Font size | 24px |
|
||||
| Color | `#c8d0e0` (insert chrome) |
|
||||
| Opacity | 80% passive, 100% when insert active and entity hover |
|
||||
| Y position | Center within container, ~y: 32px |
|
||||
|
||||
No annotation. The smuggler's lattice gives them the time. That's it.
|
||||
|
||||
### 3.3 Detective Variant
|
||||
|
||||
Two lines: clock on top, shift indicator below. The detective thinks in shifts.
|
||||
|
||||
```
|
||||
09:42
|
||||
SHIFT A ·
|
||||
```
|
||||
|
||||
| Element | Value |
|
||||
|---------|-------|
|
||||
| Time text | `{HH}:{MM}` — Michroma 20px, `#c8d0e0` at 80% passive, y: 26px in container |
|
||||
| Phase label | `SHIFT {X}` — Michroma 11px, `#c8d0e0` at 60%, y: 50px in container |
|
||||
| Phase dot | 4px circle, phase-color (see below), same y as phase label |
|
||||
|
||||
**Day phase to shift mapping:**
|
||||
|
||||
| Game time | Shift label | Dot color |
|
||||
|-----------|-------------|-----------|
|
||||
| 05:00–12:00 | `SHIFT A` | `#c8d0e0` (chrome, neutral) |
|
||||
| 12:00–18:00 | `SHIFT B` | `#c8d0e0` (chrome, neutral) |
|
||||
| 18:00–22:00 | `SHIFT C` | `#e8c547` (amber — social hours, higher risk of contacts) |
|
||||
| 22:00–05:00 | `SHIFT OFF` | `#4a9ebb` (teal — night, lower routine activity) |
|
||||
|
||||
The dot is a subtle visual cue, not a state machine icon. It gives the detective's lattice a slightly more annotated feel than the smuggler's plain clock.
|
||||
|
||||
### 3.4 Insert-Off Behavior
|
||||
|
||||
When `insert_active == false`:
|
||||
- Time display dims to 50% of passive opacity
|
||||
- Background panel disappears entirely
|
||||
- Text remains — the character always knows what time it is, even without active lattice engagement. Time is ambient lattice output, not targeting output.
|
||||
- Phase label (detective) also dims to 50%
|
||||
|
||||
The time display is the one persistent insert element. Everything else goes dark on insert deactivation.
|
||||
|
||||
---
|
||||
|
||||
## 4. Entity Markers
|
||||
|
||||
Entity markers appear at each entity's world position — rendered in world space on z-layer 6. They are not minimap icons in a panel; they annotate the world the player is looking at.
|
||||
|
||||
### 4.1 Smuggler Variant — Social Network View
|
||||
|
||||
The smuggler sees their world as a social network. People are dots. Labels are familiar names.
|
||||
|
||||
**Dot specification:**
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Shape | Filled circle |
|
||||
| Diameter | 6px |
|
||||
| Color | D-033 relationship color for this NPC |
|
||||
| Bloom | 2–3px gaussian at 40% blend |
|
||||
| Passive state | 60% opacity |
|
||||
| Active state (hover) | 100% opacity + bloom brightens |
|
||||
| All non-hovered on hover | 40% opacity (focus effect) |
|
||||
|
||||
The dot sits at the entity's sprite anchor point (center-bottom of the 64×64 canvas). It does not float; it attaches to the entity.
|
||||
|
||||
**Label specification:**
|
||||
|
||||
Named NPCs (Kael, Lera, Sera, etc.) always show a casual label. Generic NPCs show no label unless hovered.
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Typeface | Michroma 11px |
|
||||
| Color | `#c8d0e0` at 70% passive, 100% on hover |
|
||||
| Position | 8px above the dot, horizontally centered |
|
||||
| Format | First name only for Known NPCs: `Kael` |
|
||||
| Format | Location nickname for POIs: `The Last Shift`, `Kael's usual spot` |
|
||||
| Background | None — label is direct on world view. Keep it legible against zone palettes. |
|
||||
|
||||
**Relationship colors applied:**
|
||||
|
||||
| NPC state | Dot color | Label format |
|
||||
|-----------|-----------|--------------|
|
||||
| Unknown | `#4a9ebb` teal | No label (generic) |
|
||||
| Known/Friendly | `#6bc9a6` green | First name |
|
||||
| Person of Interest | `#e8c547` amber | First name + `?` suffix: `Torek?` |
|
||||
| Hostile | `#d45d5d` red | First name |
|
||||
|
||||
The smuggler does not see "Person of Interest." To them, amber means "something's off about this person" — not a case file designation.
|
||||
|
||||
### 4.2 Detective Variant — Investigation Overlay
|
||||
|
||||
The detective sees their world as a case map. Known individuals are flagged. Locations have institutional labels. The overlay is denser and more annotated.
|
||||
|
||||
**Named NPC marker specification:**
|
||||
|
||||
| NPC type | Shape | Size | Color |
|
||||
|----------|-------|------|-------|
|
||||
| Person of Interest | Diamond (rotated square) | 10px × 10px | `#e8c547` amber |
|
||||
| Known/Friendly | Filled circle | 6px | `#6bc9a6` green |
|
||||
| Unknown | Filled circle | 6px | `#4a9ebb` teal |
|
||||
| Hostile / Suspect | Diamond | 10px × 10px | `#d45d5d` red |
|
||||
|
||||
The diamond shape is the detective's POI designation — it communicates "flagged" without text. The smuggler's variant uses only circles because their lattice doesn't have case-file designation logic.
|
||||
|
||||
**Label specification:**
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Typeface | Michroma 11px |
|
||||
| Color | `#c8d0e0` at 80% passive, 100% on hover |
|
||||
| Position | 10px above the marker, right-aligned to marker center |
|
||||
| NPC label format | `POI: K. Davan` (formal: last name initial + surname) |
|
||||
| NPC label format | `K. Davan` for Known/non-POI |
|
||||
| Location label | See §5 |
|
||||
|
||||
**Evidence annotation:**
|
||||
|
||||
When the detective has logged evidence associated with an entity, a small square appears adjacent to their marker:
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Shape | Filled square |
|
||||
| Size | 4px × 4px |
|
||||
| Position | 4px right of the primary marker |
|
||||
| Color | `#e8c547` amber |
|
||||
| Opacity | 60% passive, 100% on hover |
|
||||
|
||||
This is the insert's way of flagging "you have something on this person." It doesn't say what.
|
||||
|
||||
**Commission grid overlay (Detective only):**
|
||||
|
||||
The detective's augmented lattice renders a very faint reference grid across the viewport. This is the "commission-style" aesthetic — the world read as a site map.
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Grid lines | Horizontal + vertical, every 5 visual tiles (~320px) |
|
||||
| Line color | `#333340` at 20% opacity |
|
||||
| Line weight | 1px |
|
||||
| Behavior | Always visible when insert active, disappears on insert-off |
|
||||
|
||||
The grid does not animate, pulse, or react. It is a passive reference overlay. Five lines across the width, five down the height, making a 5×5 grid of reference squares. It is barely visible — felt as structure, not seen as decoration.
|
||||
|
||||
---
|
||||
|
||||
## 5. Location Labels
|
||||
|
||||
Location labels appear at POI anchor points in world space — the entrance tile or center tile of a known location.
|
||||
|
||||
### 5.1 Smuggler Variant
|
||||
|
||||
Casual labels, written in the smuggler's vocabulary. These are their personal pins.
|
||||
|
||||
| Location | Label text |
|
||||
|----------|-----------|
|
||||
| The Last Shift (bar) | `The Last Shift` |
|
||||
| The Terminal (logistics hub) | `The Terminal` |
|
||||
| Maintenance corridor (smuggling space) | `B-7 drop` (or specific pin label) |
|
||||
| Kael's regular position | `Kael's usual spot` |
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Typeface | Michroma 12px |
|
||||
| Color | `#c8d0e0` at 50% — location labels are backgrounded, not foregrounded |
|
||||
| Position | Centered on location anchor, 16px above entity-level (below any entity markers at that location) |
|
||||
| Visibility | Always visible for known locations, fades in on discovery |
|
||||
|
||||
### 5.2 Detective Variant
|
||||
|
||||
Institutional labels, written in Commission case file format.
|
||||
|
||||
| Location | Label text |
|
||||
|----------|-----------|
|
||||
| The Last Shift (bar) | `Licensed Establishment · District 7` |
|
||||
| The Terminal (logistics hub) | `Freight Operations Hub · Level 2` |
|
||||
| Maintenance corridor | `Restricted Access — Corridor B-7` |
|
||||
| Kael's regular position | `K. Davan – Last Sighted` |
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Typeface | Michroma 11px |
|
||||
| Color | `#c8d0e0` at 70% — slightly more prominent than smuggler's labels |
|
||||
| Position | Same as smuggler, centered on anchor |
|
||||
| Secondary text | Dot separator `·` between primary name and category/district |
|
||||
| Visibility | Always visible for known/case-file locations |
|
||||
|
||||
---
|
||||
|
||||
## 6. Border Arrows (Off-Screen Navigation)
|
||||
|
||||
When a known POI or tracked entity is outside the current viewport, a border arrow appears at the screen edge pointing toward them.
|
||||
|
||||
**The camera is locked to the player character (D-014).** Border arrows are the only navigation affordance for off-screen known locations. There is no minimap panel.
|
||||
|
||||
### 6.1 Arrow Specification
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Shape | Equilateral triangle, apex pointing toward screen edge |
|
||||
| Size | 12px × 10px |
|
||||
| Inset from edge | 8px |
|
||||
| Color | Matches the POI's relationship color (D-033), or `#c8d0e0` chrome for neutral known locations |
|
||||
| Opacity | 60% passive, 100% on hover |
|
||||
| Label on hover | Location name (using appropriate variant's label format), Michroma 10px, appears inside-screen adjacent to arrow |
|
||||
|
||||
**Clustering:** When multiple POIs are off-screen in similar directions, arrows stack with 4px gap. Maximum 6 arrows per edge before clustering into a count indicator: `+3` (Michroma 10px, chrome color).
|
||||
|
||||
**Decay:** If the character has only `Suspects` confidence about a location (not `KnowsOf`), the border arrow renders at 30% opacity with a dashed border (1px alternating 2px on / 2px off). "Something might be there."
|
||||
|
||||
### 6.2 Smuggler vs Detective
|
||||
|
||||
- **Smuggler:** Border arrows appear for Known/Friendly NPCs and personal pins. The arrow color is the NPC's relationship color. The label is casual. Unknown NPCs do not generate border arrows.
|
||||
- **Detective:** Border arrows appear for all case-file entries, POIs, and known locations regardless of relationship state. Evidence-flagged entities show the amber evidence square (4px) adjacent to the arrow.
|
||||
|
||||
---
|
||||
|
||||
## 7. Inventory Indicators (Smuggler Only)
|
||||
|
||||
Per D-065: the smuggler can carry up to 3–4 physical items. Icons appear in the lower-right corner, above the dialogue band.
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Position | x: 1904px (right inset 16px), y: 816px → 856px |
|
||||
| Icon size | 40×40px |
|
||||
| Gap between icons | 8px horizontal, right-to-left stacking |
|
||||
| Opacity | 80% passive, 100% on hover |
|
||||
| Empty slots | Not displayed — icons only appear when items are carried |
|
||||
| Z-layer | 6 (insert layer), but icons are physical objects, not lattice data |
|
||||
|
||||
**Item icons (placeholder descriptions for #314 — actual sprites are a separate ticket):**
|
||||
- Manifest copy: document silhouette
|
||||
- Corridor access token: hexagonal tag silhouette
|
||||
- Personal comm log: communication device silhouette
|
||||
|
||||
Icons do not disappear when insert is off — they are physical items, not lattice annotations. The character still has pockets whether or not their insert is active.
|
||||
|
||||
**Detective inventory:** The detective has 2 inventory slots but they are knowledge-only in v0.1 (D-065). No inventory icons display for the detective in v0.1.
|
||||
|
||||
---
|
||||
|
||||
## 8. Stance Indicator
|
||||
|
||||
Both variants show the current movement stance. This is ambient information — the player set it, but may have forgotten.
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Position | x: 16px → 180px, y: 826px → 856px (lower-left, above dialogue band) |
|
||||
| Typeface | Michroma 11px |
|
||||
| Color | `#c8d0e0` at 50% — intentionally quiet |
|
||||
| Content | Current stance label: `WALK` / `CAREFUL` / `SPRINT` / `CROUCH` |
|
||||
| Behavior | Fades to 30% when dialogue is active |
|
||||
|
||||
The stance indicator never hides; it just dims. It is the quietest element in the HUD.
|
||||
|
||||
---
|
||||
|
||||
## 9. Interaction States
|
||||
|
||||
### 9.1 Insert Active — Default State
|
||||
|
||||
Full overlay as described in §3–§8. Entity markers, location labels, border arrows, time display, stance indicator, inventory icons all render at their passive opacity values.
|
||||
|
||||
### 9.2 Insert Inactive (D-056/D-057)
|
||||
|
||||
When `insert_active == false`:
|
||||
|
||||
| Element | State |
|
||||
|---------|-------|
|
||||
| Entity markers | Hidden |
|
||||
| Location labels | Hidden |
|
||||
| Border arrows | Hidden |
|
||||
| Commission grid (detective) | Hidden |
|
||||
| Evidence annotations | Hidden |
|
||||
| Bloom | Off — no gaussian pass |
|
||||
| Time display | Dims to 50% of passive opacity, background panel disappears |
|
||||
| Stance indicator | Remains — 30% opacity |
|
||||
| Inventory icons | Remain — physical, not insert data |
|
||||
| Cursor | Shape still changes on entity proximity (subconscious physical response per D-056) |
|
||||
|
||||
The world goes quiet. The character still moves, still perceives physically, still has their items. But the lattice stops annotating.
|
||||
|
||||
### 9.3 Entity Hover (Insert Active)
|
||||
|
||||
When the player's cursor enters an entity's interaction radius:
|
||||
|
||||
1. Hovered entity's marker brightens to 100% opacity + bloom intensifies to 60%
|
||||
2. All other entity markers dim to 40% opacity
|
||||
3. Interaction verb appears: Michroma 13px, D-033 color of the target, positioned 16px above the entity marker
|
||||
4. Entity label brightens to 100%
|
||||
|
||||
The interaction verb is the primary affordance for engagement. The verb appears on the insert layer — it disappears when insert is off, and is unaffected by fog (since it's z-layer 6).
|
||||
|
||||
Verb examples: `Talk`, `Observe`, `Follow`, `Confront`, `Examine`
|
||||
|
||||
### 9.4 Dialogue Active
|
||||
|
||||
When a dialogue interaction is in progress:
|
||||
|
||||
| Element | Change |
|
||||
|---------|--------|
|
||||
| Dialogue box | Appears at bottom 20% (y: 864–1080), z-layer 7 |
|
||||
| Monologue (if active) | Floats above dialogue box at y: ~800–860, z-layer 7 |
|
||||
| Insert overlay | Reduces to 60% of active opacity — still visible, lattice still running |
|
||||
| Entity markers | Remain visible but dimmed 40% |
|
||||
| Hovered entity marker | Stays bright — the conversation target stays highlighted |
|
||||
| Inventory icons | Remain visible |
|
||||
| Time display | Remains |
|
||||
| Stance indicator | Dims to 30% |
|
||||
|
||||
The insert doesn't close during dialogue. The character is still aware of their surroundings.
|
||||
|
||||
### 9.5 High Perception Load (D-059)
|
||||
|
||||
When multiple perception modes are active simultaneously, the insert shows diegetic interference:
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Effect | Horizontal scan-line distortion |
|
||||
| Intensity | 5% — subtle, peripheral |
|
||||
| Behavior | Periodic (not constant), short bursts, random intervals 2–8 seconds |
|
||||
| Applies to | All insert elements simultaneously |
|
||||
| Does NOT apply to | UI z-layer 7 (monologue, dialogue) |
|
||||
|
||||
This is a visual warning, not a mechanical penalty. The player reads it as "the lattice is working hard." It should not be alarming; it should be ambient.
|
||||
|
||||
---
|
||||
|
||||
## 10. Wireframe Mockups
|
||||
|
||||
### 10.1 Smuggler Variant — The Last Shift, Evening
|
||||
|
||||
The smuggler is in the bar. Kael is nearby (green — Known/Friendly). An unknown dock worker is in the corner (teal). The bar is a known location.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ CAREFUL 09:42 │
|
||||
│ │
|
||||
│ ↑ The Last Shift (bar: border direction) │
|
||||
│ │
|
||||
│ │
|
||||
│ ● ● │
|
||||
│ Kael [player] ● │
|
||||
│ (green) (cream) (teal) │
|
||||
│ │
|
||||
│ The Last Shift │
|
||||
│ (chrome label, centered) │
|
||||
│ │
|
||||
│ │
|
||||
│ [manifest][token]│
|
||||
├─────────────────────────────────────────────────────────────────────────┤
|
||||
│ [dialogue box — inactive] │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
Legend:
|
||||
● = entity dot marker (6px) Kael = label (Michroma 11px, chrome)
|
||||
The Last Shift = location label (Michroma 12px, 50% chrome)
|
||||
[manifest][token] = inventory icons (40×40px, lower-right)
|
||||
09:42 = time display (Michroma 24px, upper-right)
|
||||
CAREFUL = stance (Michroma 11px, lower-left, 50%)
|
||||
```
|
||||
|
||||
### 10.2 Detective Variant — Same Location, Same Time
|
||||
|
||||
The detective is outside the bar, watching the entrance. Kael is inside, behind a wall — the lattice shows his last known position as a dim marker. A POI is approaching from the corridor (off-screen right).
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ WALK [ANALYT] 09:42 │
|
||||
│ SHIFT C · │
|
||||
│ +·····+·····+·····+·····+·····+·····+·····+·····+·····+·····+ │
|
||||
│ · ·│
|
||||
│ · ◆ [player] → ·│
|
||||
│ · K. Davan (blue-wh) [POI: arrow, amber, off-screen]·│
|
||||
│ · (amber diamond) ·│
|
||||
│ · ·│
|
||||
│ · Licensed Establishment · District 7 ·│
|
||||
│ +·····+·····+·····+·····+·····+·····+·····+·····+·····+·····+ │
|
||||
│ · ·│
|
||||
│ · ·│
|
||||
│ │
|
||||
│ WALK │
|
||||
├─────────────────────────────────────────────────────────────────────────┤
|
||||
│ [dialogue box — inactive] │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
Legend:
|
||||
◆ = POI diamond marker (10×10px) K. Davan = formal label (Michroma 11px)
|
||||
+·····+ = commission grid overlay (Michroma thin, 20% opacity)
|
||||
→ = border arrow (off-screen POI, amber — K. Davan connection)
|
||||
Licensed Establishment · District 7 = institutional location label
|
||||
09:42 / SHIFT C · = time display + shift with amber phase dot
|
||||
[ANALYT] = perception mode indicator (placeholder position, upper-center-right)
|
||||
```
|
||||
|
||||
### 10.3 Entity Hover State — Detective, POI Hover
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ WALK [ANALYT] 09:42 │
|
||||
│ SHIFT C · │
|
||||
│ │
|
||||
│ Talk │
|
||||
│ Observe │
|
||||
│ ◆ Confront ←─ interaction verbs (amber, 13px) │
|
||||
│ K. Davan │
|
||||
│ [■]◆ (brightened, 100%, bloom 60%) │
|
||||
│ │
|
||||
│ all other markers at 40% opacity ─────────────────────────→ │
|
||||
│ ● │
|
||||
│ (40%) │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
[■] = evidence annotation (4×4px amber square, adjacent to diamond)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Color Reference Summary
|
||||
|
||||
All colors from `visual-grammar-v01.md` §1.5 and §3.2. Reproduced here for implementation reference.
|
||||
|
||||
| Use | Hex | Notes |
|
||||
|-----|-----|-------|
|
||||
| Insert chrome (default) | `#c8d0e0` | Time display, labels, stance, grid |
|
||||
| Entity — Unknown | `#4a9ebb` | Cool teal. Default NPC state. |
|
||||
| Entity — Known/Friendly | `#6bc9a6` | Soft green. Trusted network. |
|
||||
| Entity — POI | `#e8c547` | Warm amber. Flagged. |
|
||||
| Entity — Hostile | `#d45d5d` | Muted red. Subjective danger. |
|
||||
| Player — Detective | `#e0e8ff` | Cool blue-white. |
|
||||
| Player — Smuggler | `#e8e0d0` | Warm cream. |
|
||||
| Unexplored wireframe | `#333340` | Not used in HUD chrome directly — world layer use |
|
||||
| Time panel background | `#0e1218` at 40% | Barely-there. Matches zone ambient palette. |
|
||||
|
||||
**Saturation rule:** Entity D-033 colors must remain the most saturated elements on screen at all times. Nothing in the insert chrome may compete with entity color saturation (D-044, §3.6 of visual grammar). The chrome is cool grey-white — it frames entity colors, never rivals them.
|
||||
|
||||
---
|
||||
|
||||
## 12. Typography Reference
|
||||
|
||||
All text is Michroma Regular 400 (Google Fonts). No exceptions (visual-grammar-v01.md §5.4).
|
||||
|
||||
| Role | Size | Color | Opacity (passive / active) | Z-layer |
|
||||
|------|------|-------|---------------------------|---------|
|
||||
| Time display | 24px (smuggler) / 20px (detective) | `#c8d0e0` | 80% / 100% | 6 |
|
||||
| Phase label (detective) | 11px | `#c8d0e0` | 60% / 80% | 6 |
|
||||
| Entity label (hover) | 11px | `#c8d0e0` | 70% / 100% | 6 |
|
||||
| Location label (smuggler) | 12px | `#c8d0e0` | 50% / 70% | 6 |
|
||||
| Location label (detective) | 11px | `#c8d0e0` | 70% / 90% | 6 |
|
||||
| Interaction verb | 13px | D-033 target color | 95% (always active) | 6 |
|
||||
| Stance indicator | 11px | `#c8d0e0` | 50% / 60% | 6 |
|
||||
| Border arrow label (hover) | 10px | `#c8d0e0` | 0% / 100% (hover-only) | 6 |
|
||||
|
||||
---
|
||||
|
||||
## 13. What This Document Does Not Cover
|
||||
|
||||
These are deliberately out of scope for #314. Separate tickets own them.
|
||||
|
||||
- **Perception mode overlay visual design** — thermal, camera feed, etc. (#315 area, depends on D-017/D-048 full spec)
|
||||
- **Monologue display system** — typography animation, stacking (#315)
|
||||
- **Sound indicator visual design** — fog-edge ping pulse (#317)
|
||||
- **THE FRIEND visual treatment** — special relationship visual affordances (#318)
|
||||
- **Minimap panel** (if a panel approach is ever adopted — currently border arrows handle navigation, no panel exists in v0.1)
|
||||
- **World map / inter-location navigation** — a different system entirely
|
||||
- **Cursor states** — specified in D-056, not repeated here
|
||||
|
||||
---
|
||||
|
||||
## Appendix A — Integration Notes for Stig (#263)
|
||||
|
||||
**For the time display implementation (#263):**
|
||||
|
||||
1. The time display container is at **x: 1720–1904, y: 16–64** (right-inset 16px from 1920px width). These are your anchor coordinates.
|
||||
2. The `ObserverSnapshot` needs to expose current game time (hours, minutes) and current day phase. Consult D-031 for the tick-to-time mapping (10 ticks = 1 game-minute).
|
||||
3. Time display text must be **right-aligned** within the container. Don't center it — it should feel like a readout in the upper-right corner.
|
||||
4. The background panel (`#0e1218` at 40%) should be a simple NinePatchRect or ColorRect — minimal, no rounded corners.
|
||||
5. When `insert_active == false`: set time display `modulate.a = 0.5 * passive_opacity` and hide the background panel. The text stays; the container chrome disappears.
|
||||
6. For the detective's phase label: drive the shift string from a simple range lookup on the game hour (see §3.3 table above). The dot color is a color constant mapped from phase — not dynamic.
|
||||
7. The phase label is visible only on `lattice_profile == lattice_augmented`. Single boolean check in the time display node.
|
||||
|
||||
**For the entity marker system (future tickets):**
|
||||
|
||||
- One marker scene, parametrized by shape (circle vs diamond), size, color, bloom intensity
|
||||
- The `lattice_profile` drives shape selection: baseline = circles always, augmented = diamonds for POI state, circles for other states
|
||||
- Evidence annotation (`■`) is a separate child node, shown/hidden based on KnowledgeGraph query for evidence entries on this entity
|
||||
- Commission grid is a separate CanvasLayer node, only visible when `lattice_profile == lattice_augmented` and `insert_active == true`
|
||||
|
||||
---
|
||||
|
||||
## Appendix B — Decision Cross-References
|
||||
|
||||
| Decision | Relevance |
|
||||
|----------|-----------|
|
||||
| D-013 | Diegetic insert/POI navigation. The HUD IS the neural insert. |
|
||||
| D-014 | Minimap spec: "dots when close, border arrows for known distant POIs" |
|
||||
| D-019 | Camera tilt — sprites authored for shallow tilt. Markers sit above entity anchor. |
|
||||
| D-031 | Time system — tick-to-time mapping, day phases. Feeds §3. |
|
||||
| D-033 | Entity color = relationship to player. All marker colors source from this. |
|
||||
| D-048 | Neural insert visual design. Bloom spec, character variants. |
|
||||
| D-049 | Z-layer 6 = insert overlay. Z-layer 7 = UI/monologue. |
|
||||
| D-051 | "Settling is placement." Object density as meaning. Insert labels reflect this. |
|
||||
| D-056 | Cursor states persist when insert-off. |
|
||||
| D-057 | Entity interaction labels suppressed when insert-off. |
|
||||
| D-059 | Perception load → scan-line interference at 5%. |
|
||||
| D-065 | Inventory: 40×40px icons, smuggler-only in v0.1. |
|
||||
| D-066 | Dual-scale grid. Entity markers anchor to visual tile positions. |
|
||||
@@ -307,7 +307,7 @@ Social positioning in a specific context.
|
||||
### Full YAML format (engine canonical)
|
||||
|
||||
```yaml
|
||||
- id: transit_m_s_025
|
||||
- id: pc-smuggler_m_s_025
|
||||
character: smuggler
|
||||
text: "Kael keeps checking his lattice. Waiting for a message? Not like him to be jumpy."
|
||||
trigger: observe_anomaly
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
# Line ID Authoring Guide
|
||||
|
||||
**Decision:** D-084 (dual-namespace line ID scheme)
|
||||
**Resolves:** Q-028 (collision-resistant IDs for auto-generated NPCs)
|
||||
**Ticket:** #544
|
||||
|
||||
---
|
||||
|
||||
## The Short Version
|
||||
|
||||
- **Role pool lines:** Use `{role-slug}_d_{###}` — e.g., `dock-worker_d_001`. These lines are shared by all NPCs with that role. This is the default for all auto-generated NPC content.
|
||||
- **Named NPC lines:** Use `{npc-slug}_d_{###}` — e.g., `kael-davan_d_001`. Unchanged from current practice.
|
||||
- **Instance-specific lines (rare):** Use `{role-slug}-{counter}_d_{###}` — e.g., `dock-worker-07_d_001`. Only needed when a specific generated NPC needs content different from the role pool.
|
||||
|
||||
---
|
||||
|
||||
## How Line IDs Work
|
||||
|
||||
A line ID identifies **content**, not speaker. The speaker is identified by their `StableId` in the history log. So `dock-worker_d_001` being said by 40 different dock workers is correct: the log records `(StableId: 12, dock-worker_d_001)`, `(StableId: 37, dock-worker_d_001)`, etc. No collision.
|
||||
|
||||
This means the role pool approach already handles most cases — the "collision problem" is mainly a concern for the rare case where you want a specific generated NPC to say something *different* from others of the same role.
|
||||
|
||||
---
|
||||
|
||||
## Namespace Reference
|
||||
|
||||
### Named NPC lines (Tier 1 and Tier 2 authored NPCs)
|
||||
|
||||
```
|
||||
Format: {npc-slug}_{content-type}_{###}
|
||||
Example: kael-davan_d_001 (Kael's dialogue line 1)
|
||||
sera-venn_d_015 (Sera's dialogue line 15)
|
||||
pc-smuggler_m_s_001 (Smuggler monologue line 1)
|
||||
```
|
||||
|
||||
File location: One file per NPC (e.g., `dialogue/maintenance-corridors/kael-davan.yaml`)
|
||||
|
||||
Numbering: Sequential within the file. Gaps are acceptable (deleted lines leave permanent gaps). Never reuse a number.
|
||||
|
||||
---
|
||||
|
||||
### Role pool lines (auto-generated NPCs, Tier 3 flat, Tier 2 mundane)
|
||||
|
||||
```
|
||||
Format: {role-slug}_{content-type}_{###}
|
||||
Example: dock-worker_d_001 (any dock worker, dialogue line 1)
|
||||
bar-regular_d_008 (any bar regular, dialogue line 8)
|
||||
transit-worker_d_003 (any transit worker, dialogue line 3)
|
||||
```
|
||||
|
||||
File location: One file per role-at-location (e.g., `dialogue/the-terminal/dock-worker.yaml`)
|
||||
|
||||
These lines are shared by **all instances** of the role. Write them to suit any dock worker, not a specific one.
|
||||
|
||||
---
|
||||
|
||||
### Instance-specific lines (opt-in, rare)
|
||||
|
||||
Use only when the generation system has flagged a specific NPC as needing content that differs from the role pool. Examples: a generated dock worker who is also a triangle member with a specific tell; a generated bar regular who witnessed a specific event.
|
||||
|
||||
```
|
||||
Format: {role-slug}-{zero-padded counter}_{content-type}_{###}
|
||||
Example: dock-worker-07_d_001 (instance 7 of dock-worker role, line 1)
|
||||
bar-regular-02_d_005 (instance 2 of bar-regular role, line 5)
|
||||
```
|
||||
|
||||
The counter (01, 02, ... N) is assigned by the generation system in world-seed-deterministic order. The NPC's generated profile file will tell you which counter to use.
|
||||
|
||||
File location: Same directory as the role pool file, separate file with instance slug as name (e.g., `dialogue/the-terminal/dock-worker-07.yaml`)
|
||||
|
||||
---
|
||||
|
||||
## Quick Decision Guide
|
||||
|
||||
| Situation | ID format to use |
|
||||
|-----------|------------------|
|
||||
| Named authored NPC (Kael, Sera, Voss...) | `{npc-slug}_d_{###}` |
|
||||
| Lines any dock worker can say | `dock-worker_d_{###}` |
|
||||
| Lines any bar regular can say | `bar-regular_d_{###}` |
|
||||
| Generated NPC with specific triangle role | `{role-slug}-{counter}_d_{###}` |
|
||||
| Generated NPC who's just background | `{role-slug}_d_{###}` — no instance ID needed |
|
||||
|
||||
---
|
||||
|
||||
## Schema Compatibility
|
||||
|
||||
The existing ID regex `^[a-z][a-z0-9-]*_[dme]_\d{3}$` accepts all three formats. No schema change is required. The content validator (`make validate-content`) checks for duplicate IDs across all files in a district.
|
||||
|
||||
---
|
||||
|
||||
## Numbering Rules
|
||||
|
||||
1. Start at `001`, increment by 1 for each new line.
|
||||
2. Never reuse a number, even if a line is deleted. Gaps are fine.
|
||||
3. Lines within a single file have a contiguous prefix — `dock-worker_d_001` through `dock-worker_d_042`, etc.
|
||||
4. Cross-file: `kael-davan.yaml` at the terminal and `kael-davan.yaml` at maintenance corridors both use the `kael-davan_d_###` namespace. Continue numbering from where the other file left off (check the existing files first, use a fresh sequence if the NPC is new to a location).
|
||||
|
||||
---
|
||||
|
||||
*D-084 — authored by Gestalt, Sprint 18*
|
||||
@@ -226,7 +226,7 @@ Add `tags: [mirror_moment]` to both sides of every mirror pair so they can be id
|
||||
|
||||
```yaml
|
||||
# Smuggler side — monologue-smuggler.yaml
|
||||
- id: transit_m_s_044
|
||||
- id: pc-smuggler_m_s_044
|
||||
character: smuggler
|
||||
trigger: observe_npc
|
||||
prerequisite:
|
||||
@@ -239,7 +239,7 @@ Add `tags: [mirror_moment]` to both sides of every mirror pair so they can be id
|
||||
tags: [mirror_moment, mirror_worried_partner]
|
||||
|
||||
# Detective side — monologue-detective.yaml
|
||||
- id: transit_m_d_044
|
||||
- id: pc-detective_m_d_044
|
||||
character: detective
|
||||
trigger: observe_npc
|
||||
prerequisite:
|
||||
|
||||
@@ -359,7 +359,7 @@ skills:
|
||||
```yaml
|
||||
# File: districts/{location}/dialogue/{role}-mirror.yaml
|
||||
|
||||
- id: "{location}_d_001"
|
||||
- id: "{npc-slug}_d_001"
|
||||
text: "Greeting — warm, open, no guardedness"
|
||||
role: "{role}"
|
||||
access: ["public"]
|
||||
@@ -367,7 +367,7 @@ skills:
|
||||
situation: ["greeting"]
|
||||
mood: ["warm"]
|
||||
|
||||
- id: "{location}_d_002"
|
||||
- id: "{npc-slug}_d_002"
|
||||
text: "Sharing worry — direct, no evasion"
|
||||
role: "{role}"
|
||||
access: ["public", "insider"]
|
||||
@@ -375,7 +375,7 @@ skills:
|
||||
situation: ["worried_inquiry"]
|
||||
mood: ["anxious"]
|
||||
|
||||
- id: "{location}_d_010"
|
||||
- id: "{npc-slug}_d_010"
|
||||
text: "Deep emotional vulnerability — peer tier"
|
||||
role: "{role}"
|
||||
access: ["peer"]
|
||||
@@ -385,8 +385,8 @@ skills:
|
||||
|
||||
# Monologue content (separate per character, per D-032)
|
||||
|
||||
# File: districts/{location}/monologue-smuggler.yaml
|
||||
- id: "{location}_m_s_001"
|
||||
# File: districts/{location}/monologue/smuggler/{location}.yaml
|
||||
- id: "pc-smuggler_m_s_001"
|
||||
text: "Smuggler feels guilt observing MIRROR's distress"
|
||||
character: "smuggler"
|
||||
trigger: "observe_npc"
|
||||
@@ -394,8 +394,8 @@ skills:
|
||||
target.known_attributes.name: "{Name}"
|
||||
target.known_attributes.emotional_state: "worried"
|
||||
|
||||
# File: districts/{location}/monologue-detective.yaml
|
||||
- id: "{location}_m_d_001"
|
||||
# File: districts/{location}/monologue/detective/{location}.yaml
|
||||
- id: "pc-detective_m_d_001"
|
||||
text: "Detective recognizes MIRROR as honest witness"
|
||||
character: "detective"
|
||||
trigger: "post_conversation"
|
||||
|
||||
@@ -32,7 +32,7 @@ All smuggler openings share:
|
||||
These lines fire when the smuggler enters The Terminal at session start. 3-4 lines available; engine selects one based on seed variant.
|
||||
|
||||
```yaml
|
||||
- id: transit_m_s_open_001
|
||||
- id: pc-smuggler_m_s_001
|
||||
character: smuggler
|
||||
trigger: enter_location
|
||||
location: the-terminal
|
||||
@@ -40,7 +40,7 @@ These lines fire when the smuggler enters The Terminal at session start. 3-4 lin
|
||||
mood: [content, routine]
|
||||
tags: [opening_hook, canonical_anchor]
|
||||
|
||||
- id: transit_m_s_open_002
|
||||
- id: pc-smuggler_m_s_002
|
||||
character: smuggler
|
||||
trigger: enter_location
|
||||
location: the-terminal
|
||||
@@ -48,7 +48,7 @@ These lines fire when the smuggler enters The Terminal at session start. 3-4 lin
|
||||
mood: [routine, aware]
|
||||
tags: [opening_hook]
|
||||
|
||||
- id: transit_m_s_open_003
|
||||
- id: pc-smuggler_m_s_003
|
||||
character: smuggler
|
||||
trigger: enter_location
|
||||
location: the-terminal
|
||||
@@ -56,7 +56,7 @@ These lines fire when the smuggler enters The Terminal at session start. 3-4 lin
|
||||
mood: [routine, observant]
|
||||
tags: [opening_hook]
|
||||
|
||||
- id: transit_m_s_open_004
|
||||
- id: pc-smuggler_m_s_004
|
||||
character: smuggler
|
||||
trigger: enter_location
|
||||
location: the-terminal
|
||||
@@ -70,7 +70,7 @@ These lines fire when the smuggler enters The Terminal at session start. 3-4 lin
|
||||
These fire when the smuggler sees Kael on the dock floor. Critical FRIEND establishment lines.
|
||||
|
||||
```yaml
|
||||
- id: transit_m_s_open_010
|
||||
- id: pc-smuggler_m_s_010
|
||||
character: smuggler
|
||||
trigger: observe_npc
|
||||
target: npc:kael-davan
|
||||
@@ -82,7 +82,7 @@ These fire when the smuggler sees Kael on the dock floor. Critical FRIEND establ
|
||||
mood: [content, fond]
|
||||
tags: [opening_hook, friend_phase_1, canonical_anchor]
|
||||
|
||||
- id: transit_m_s_open_011
|
||||
- id: pc-smuggler_m_s_011
|
||||
character: smuggler
|
||||
trigger: observe_npc
|
||||
target: npc:kael-davan
|
||||
@@ -104,7 +104,7 @@ These fire when the smuggler sees Kael on the dock floor. Critical FRIEND establ
|
||||
**Operational context line (observe_anomaly — schedule board):**
|
||||
|
||||
```yaml
|
||||
- id: transit_m_s_open_020
|
||||
- id: pc-smuggler_m_s_020
|
||||
character: smuggler
|
||||
trigger: observe_anomaly
|
||||
location: the-terminal
|
||||
@@ -131,7 +131,7 @@ Kael greets the smuggler warmly. Asks about the weekend. Makes a dry comment abo
|
||||
**Absence lines (observe_npc, failed — Kael's station empty):**
|
||||
|
||||
```yaml
|
||||
- id: transit_m_s_open_030
|
||||
- id: pc-smuggler_m_s_030
|
||||
character: smuggler
|
||||
trigger: observe_anomaly
|
||||
location: the-terminal
|
||||
@@ -140,7 +140,7 @@ Kael greets the smuggler warmly. Asks about the weekend. Makes a dry comment abo
|
||||
tags: [opening_hook, variant_b]
|
||||
notes: "Phase 1 tell: Kael's routine is so established that absence is noted before alarm."
|
||||
|
||||
- id: transit_m_s_open_031
|
||||
- id: pc-smuggler_m_s_031
|
||||
character: smuggler
|
||||
trigger: time_idle
|
||||
location: the-terminal
|
||||
@@ -169,7 +169,7 @@ Kael arrives slightly disheveled, slightly out of breath. Brief explanation —
|
||||
**Environmental read lines:**
|
||||
|
||||
```yaml
|
||||
- id: transit_m_s_open_040
|
||||
- id: pc-smuggler_m_s_040
|
||||
character: smuggler
|
||||
trigger: observe_npc
|
||||
target: npc:voss
|
||||
@@ -177,7 +177,7 @@ Kael arrives slightly disheveled, slightly out of breath. Brief explanation —
|
||||
mood: [aware, cautious]
|
||||
tags: [opening_hook, variant_c]
|
||||
|
||||
- id: transit_m_s_open_041
|
||||
- id: pc-smuggler_m_s_041
|
||||
character: smuggler
|
||||
trigger: observe_npc
|
||||
target: npc:voss
|
||||
@@ -204,7 +204,7 @@ Kael arrives slightly disheveled, slightly out of breath. Brief explanation —
|
||||
**Container flag lines:**
|
||||
|
||||
```yaml
|
||||
- id: transit_m_s_open_050
|
||||
- id: pc-smuggler_m_s_050
|
||||
character: smuggler
|
||||
trigger: observe_anomaly
|
||||
location: the-terminal
|
||||
@@ -212,7 +212,7 @@ Kael arrives slightly disheveled, slightly out of breath. Brief explanation —
|
||||
mood: [operational, concerned]
|
||||
tags: [opening_hook, variant_d]
|
||||
|
||||
- id: transit_m_s_open_051
|
||||
- id: pc-smuggler_m_s_051
|
||||
character: smuggler
|
||||
trigger: observe_anomaly
|
||||
location: the-terminal
|
||||
@@ -245,7 +245,7 @@ All detective openings share:
|
||||
Lines that fire when the detective enters their starting location. Selection based on seed variant.
|
||||
|
||||
```yaml
|
||||
- id: transit_m_d_open_001
|
||||
- id: pc-detective_m_d_001
|
||||
character: detective
|
||||
trigger: enter_location
|
||||
location: the-terminal
|
||||
@@ -253,7 +253,7 @@ Lines that fire when the detective enters their starting location. Selection bas
|
||||
mood: [professional, observant]
|
||||
tags: [opening_hook, canonical_anchor]
|
||||
|
||||
- id: transit_m_d_open_002
|
||||
- id: pc-detective_m_d_002
|
||||
character: detective
|
||||
trigger: enter_location
|
||||
location: the-terminal
|
||||
@@ -262,7 +262,7 @@ Lines that fire when the detective enters their starting location. Selection bas
|
||||
tags: [opening_hook, canonical_anchor]
|
||||
notes: "Companion line to open_001 — these fire in sequence on Variant A/B/C."
|
||||
|
||||
- id: transit_m_d_open_003
|
||||
- id: pc-detective_m_d_003
|
||||
character: detective
|
||||
trigger: enter_location
|
||||
location: the-last-shift
|
||||
@@ -270,7 +270,7 @@ Lines that fire when the detective enters their starting location. Selection bas
|
||||
mood: [professional, curious]
|
||||
tags: [opening_hook, variant_a_bar]
|
||||
|
||||
- id: transit_m_d_open_004
|
||||
- id: pc-detective_m_d_004
|
||||
character: detective
|
||||
trigger: enter_location
|
||||
location: the-terminal
|
||||
@@ -284,7 +284,7 @@ Lines that fire when the detective enters their starting location. Selection bas
|
||||
Critical FRIEND establishment for the detective.
|
||||
|
||||
```yaml
|
||||
- id: transit_m_d_open_010
|
||||
- id: pc-detective_m_d_010
|
||||
character: detective
|
||||
trigger: observe_npc
|
||||
target: npc:sera-venn
|
||||
@@ -296,7 +296,7 @@ Critical FRIEND establishment for the detective.
|
||||
mood: [relieved, warm]
|
||||
tags: [opening_hook, friend_phase_1, canonical_anchor]
|
||||
|
||||
- id: transit_m_d_open_011
|
||||
- id: pc-detective_m_d_011
|
||||
character: detective
|
||||
trigger: observe_npc
|
||||
target: npc:sera-venn
|
||||
@@ -322,7 +322,7 @@ Critical FRIEND establishment for the detective.
|
||||
**Bar arrival line:**
|
||||
|
||||
```yaml
|
||||
- id: transit_m_d_open_020
|
||||
- id: pc-detective_m_d_020
|
||||
character: detective
|
||||
trigger: enter_location
|
||||
location: the-last-shift
|
||||
@@ -334,7 +334,7 @@ Critical FRIEND establishment for the detective.
|
||||
**Lera read (public NPC, establishing social texture):**
|
||||
|
||||
```yaml
|
||||
- id: transit_m_d_open_021
|
||||
- id: pc-detective_m_d_021
|
||||
character: detective
|
||||
trigger: observe_npc
|
||||
target: npc:lera-sessik
|
||||
@@ -363,7 +363,7 @@ Sera gives a brief district overview — who the regulars are, what the Terminal
|
||||
**Terminal read line:**
|
||||
|
||||
```yaml
|
||||
- id: transit_m_d_open_030
|
||||
- id: pc-detective_m_d_030
|
||||
character: detective
|
||||
trigger: enter_location
|
||||
location: the-terminal
|
||||
@@ -375,7 +375,7 @@ Sera gives a brief district overview — who the regulars are, what the Terminal
|
||||
**Voss approach (first adversarial NPC):**
|
||||
|
||||
```yaml
|
||||
- id: transit_m_d_open_031
|
||||
- id: pc-detective_m_d_031
|
||||
character: detective
|
||||
trigger: observe_npc
|
||||
target: npc:voss
|
||||
@@ -406,7 +406,7 @@ Standard question options: "Thank you, I'll need those." / "Tell me about your s
|
||||
**Investigative focus line:**
|
||||
|
||||
```yaml
|
||||
- id: transit_m_d_open_040
|
||||
- id: pc-detective_m_d_040
|
||||
character: detective
|
||||
trigger: enter_location
|
||||
location: the-terminal
|
||||
@@ -414,7 +414,7 @@ Standard question options: "Thank you, I'll need those." / "Tell me about your s
|
||||
mood: [analytical, focused]
|
||||
tags: [opening_hook, variant_c]
|
||||
|
||||
- id: transit_m_d_open_041
|
||||
- id: pc-detective_m_d_041
|
||||
character: detective
|
||||
trigger: observe_anomaly
|
||||
location: the-terminal
|
||||
@@ -436,7 +436,7 @@ Standard question options: "Thank you, I'll need those." / "Tell me about your s
|
||||
**Escort read lines:**
|
||||
|
||||
```yaml
|
||||
- id: transit_m_d_open_050
|
||||
- id: pc-detective_m_d_050
|
||||
character: detective
|
||||
trigger: observe_npc
|
||||
target: npc:torek-lintar
|
||||
@@ -444,7 +444,7 @@ Standard question options: "Thank you, I'll need those." / "Tell me about your s
|
||||
mood: [professional, curious]
|
||||
tags: [opening_hook, variant_d]
|
||||
|
||||
- id: transit_m_d_open_051
|
||||
- id: pc-detective_m_d_051
|
||||
character: detective
|
||||
trigger: observe_npc
|
||||
target: npc:torek-lintar
|
||||
|
||||
@@ -0,0 +1,450 @@
|
||||
# Tier 1 Drama Module — Authoring Guide
|
||||
|
||||
**Schema:** `content/schemas/drama_module.schema.yaml`
|
||||
**Module pool:** `content/modules/tier1/*.yaml`
|
||||
**Decisions:** D-023 (three-tier model), D-027 (vertical slice), D-029 (30/50/20 population), D-034 (FRIEND pattern)
|
||||
**Vertical slice reference:** `content/modules/tier1/smuggling_ring_v0_1.yaml`
|
||||
|
||||
---
|
||||
|
||||
## What Is a Tier 1 Drama Module?
|
||||
|
||||
Tier 1 is the authored conspiracy layer of D-023. Drama modules are the things that can go wrong — or go very right, or simply happen — beneath the surface of daily life in Sova Transit. They are:
|
||||
|
||||
- **Hand-authored.** Every event sequence, every NPC role, every outcome was written by a person.
|
||||
- **Pool-based.** Multiple modules exist. The storyteller draws from the pool at game start and activates a subset based on the district and the storyteller's pacing decisions.
|
||||
- **Optional from the player's perspective.** The player can play 60 minutes without engaging the ring. The ring happens anyway. D-027 criterion #4: the observe→notice→follow→discover sequence must emerge from *systems*, not *scripts*.
|
||||
- **Dual-lens.** Every module must be experienced differently by the smuggler and detective characters. Same world, different keyholes.
|
||||
|
||||
What they are **not:**
|
||||
- Not quests with markers or objectives.
|
||||
- Not scripted cutscenes.
|
||||
- Not balanced challenge encounters.
|
||||
|
||||
The storyteller uses the module as a *schedule* — a series of world events it will fire, and conditions it monitors to determine how the world resolves. The player is a witness and agent in a world that moves with or without them.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
content/
|
||||
schemas/
|
||||
drama_module.schema.yaml ← Schema reference (this file validates against it)
|
||||
modules/
|
||||
tier1/
|
||||
smuggling_ring_v0_1.yaml ← The v0.1 vertical slice module
|
||||
future_module_v0_1.yaml ← Future modules go here
|
||||
```
|
||||
|
||||
One `.yaml` file per drama module. The storyteller's content loader scans `content/modules/tier1/` at startup and adds all valid modules to the pool.
|
||||
|
||||
---
|
||||
|
||||
## Field Reference
|
||||
|
||||
### Identity Fields
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `module_id` | Yes | Stable slug: `{name}_v{major}_{minor}`. Never reuse. Increment on breaking structural change. |
|
||||
| `display_name` | Yes | Human-readable title for dev tooling. Not shown in-game. |
|
||||
| `version` | Yes | Authoring version: `{major}.{minor}`. |
|
||||
| `tier` | Yes | Always `1`. |
|
||||
| `description` | No | One-paragraph design summary. Authoring-only. |
|
||||
| `notes` | No | Design rationale, cross-references. Ignored at load time. |
|
||||
| `dual_lens` | No | How smuggler vs detective experience this module. Authoring-only. **Write this first** — it disciplines the design. |
|
||||
|
||||
---
|
||||
|
||||
### Pool Metadata
|
||||
|
||||
Controls how the storyteller samples this module.
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `pool.weight` | Yes | Selection probability 1–10. Higher = more likely per playthrough. Default 5. |
|
||||
| `pool.compatible_districts` | No | District slugs. Omit for "any". |
|
||||
| `pool.incompatible_with` | No | Module IDs that can't run concurrently. |
|
||||
| `pool.max_concurrent` | No | Almost always 1. |
|
||||
|
||||
**Design note on weight:** Use weight to tune narrative variety, not difficulty. A weight-1 module is a rare playthrough surprise. A weight-8 module like the smuggling ring is "this is usually what's happening in Sova Transit."
|
||||
|
||||
---
|
||||
|
||||
### Entry Conditions
|
||||
|
||||
Defines when the module becomes eligible for activation. ALL world-state conditions must be true. The activation trigger determines *how* it fires.
|
||||
|
||||
#### World-State Condition Types
|
||||
|
||||
| Type | Required Fields | Use When |
|
||||
|------|----------------|----------|
|
||||
| `npc_present` | `role` | The module requires a specific NPC to be in the district. |
|
||||
| `location_accessible` | `location` | The module requires a location the player can physically reach. |
|
||||
| `fact_not_known` | `fact_id` | Module shouldn't activate if a precondition has already been discovered. |
|
||||
| `no_active_module` | `module_id` | Prevents two incompatible modules running at once. |
|
||||
| `fact_known` | `fact_id`, `known_by` | Module requires prior knowledge to make sense. |
|
||||
|
||||
#### Player Conditions (Optional)
|
||||
|
||||
Player conditions are *optional* — modules can and should activate without player engagement as a prerequisite. Use player conditions sparingly, only when the module literally cannot function without a minimum relationship state.
|
||||
|
||||
#### Activation Triggers
|
||||
|
||||
| Trigger | When to Use |
|
||||
|---------|-------------|
|
||||
| `storyteller_push` | Default. Storyteller activates on its own pacing. Most Tier 1 modules. |
|
||||
| `proximity` | Module activates when player wanders near a key location. Useful for "stumble-upon" conspiracies. |
|
||||
| `player_action` | Reserved for modules that require player initiation. Use rarely. |
|
||||
|
||||
**The `min_play_ticks` field is load-bearing for D-027 criterion #1.** At approximately 1 tick/second, 30 minutes of play ≈ 1800 ticks. Set `min_play_ticks` to at least 1800. The vertical slice uses 2100 to give extra breathing room.
|
||||
|
||||
---
|
||||
|
||||
### NPC Requirements
|
||||
|
||||
Each module specifies its NPC slots. Roles are internal slugs used throughout the rest of the document.
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `role` | Yes | Module-internal slug. Kebab-case. Used in event triggers and outcome conditions. |
|
||||
| `display_hint` | No | Authoring note: who this role is narratively. |
|
||||
| `binding` | Yes | `named` (specific authored NPC) or `generated` (any matching NPC). |
|
||||
| `named_npc` | Conditional | Required when `binding: named`. Short-form canonical ID: `npc:{slug}`. |
|
||||
| `axes` | Conditional | Required when `binding: generated`. Axis constraints the NPC must satisfy. |
|
||||
| `must_have_pattern` | No | Optional NPC pattern (D-024 System A). |
|
||||
| `must_have_motivation` | No | Optional NPC motivation (D-024 System B). |
|
||||
| `is_optional` | No | Default false. If true, module runs without this slot filled (degraded experience). |
|
||||
|
||||
#### Named vs. Generated Bindings
|
||||
|
||||
**Named bindings** reference specific hand-authored NPCs from the district. All v0.1 roles are named. This is the right choice for:
|
||||
- THE FRIEND NPCs (D-034) — they have authored arcs, not generic behavior
|
||||
- NPCs with unique relationships in the 5-triangle web
|
||||
- Roles where voice, history, and moral weight matter
|
||||
|
||||
**Generated bindings** are for future modules set in different districts or using procedurally generated NPCs. They use axis constraints:
|
||||
|
||||
```yaml
|
||||
axes:
|
||||
- axis: secret
|
||||
constraint: has_major_secret
|
||||
- axis: contentment
|
||||
constraint: min_contentment_-3 # Discontented, susceptible to opportunity
|
||||
```
|
||||
|
||||
Constraint conventions: `has_{value}`, `min_{N}`, `not_{value}`. The server's NPC filter system interprets these.
|
||||
|
||||
#### What "Roles" Are Not
|
||||
|
||||
NPC roles in a drama module are **not** the same as NPC patterns (FRIEND, MIRROR, etc.) or motivations (HANDLER, WITNESS, etc.). Module roles are:
|
||||
- Functional slots within the module's narrative (ring-leader, witness, evidence-holder)
|
||||
- Module-local: "ring-leader" in the smuggling ring module ≠ "ring-leader" in any other module
|
||||
- Used to reference the same NPC across events and outcomes without hardcoding the NPC slug
|
||||
|
||||
#### NPC Pattern and Motivation Reference
|
||||
|
||||
Patterns (System A, `must_have_pattern`) encode the NPC's thematic function in the player's experience:
|
||||
|
||||
| Pattern | What It Means |
|
||||
|---------|---------------|
|
||||
| `FRIEND` | Emotionally complex anchor; the contradiction arc lives here (D-034) |
|
||||
| `MIRROR` | Reflects the player character's own path back at them |
|
||||
| `ANCHOR` | Reliable presence; stability the player can always return to |
|
||||
| `GHOST` | Presence felt more than seen; past hangs over current events |
|
||||
| `CATALYST` | Actions cause cascading effects on other NPCs |
|
||||
| `THRESHOLD` | Gatekeeper; controls access to deeper information or relationships |
|
||||
| `REMNANT` | Survivor of a prior event; carries knowledge others want buried |
|
||||
| `SYSTEM` | Embodies an institution or faction rather than personal stakes |
|
||||
| `NOBODY` | Genuinely flat; texture and atmosphere, no arc |
|
||||
|
||||
Motivations (System B, `must_have_motivation`) encode why the NPC acts within the module's conspiracy:
|
||||
|
||||
| Motivation | What It Means |
|
||||
|------------|---------------|
|
||||
| `HANDLER` | Organizes or directs others; the operational center |
|
||||
| `WITNESS` | Knows something they haven't decided to act on |
|
||||
| `TURNCOAT` | Wants out, or has already switched allegiance |
|
||||
| `CIVILIAN` | No conspiracy involvement; proximity creates moral weight |
|
||||
| `OPERATOR` | Executes tasks; functional cog in the system |
|
||||
| `SKEPTIC` | Doubts the conspiracy exists; useful foil for investigation |
|
||||
|
||||
**Full definitions and canonical usage:** `decisions/content.md` D-024.
|
||||
|
||||
---
|
||||
|
||||
### Events
|
||||
|
||||
Events are world-state changes the storyteller fires. They are not scripted player experiences — they happen in the world, and the player may or may not observe them.
|
||||
|
||||
#### Sequences vs. Pools
|
||||
|
||||
| Structure | Use For |
|
||||
|-----------|---------|
|
||||
| **Sequence** | Ordered narrative beats. Step N+1 becomes eligible only after step N fires. Use for character arcs. |
|
||||
| **Pool** | Unordered ambient activity. The storyteller fires any eligible event at any time. Use for texture and background. |
|
||||
|
||||
The vertical slice uses:
|
||||
- `kael_exit_arc` (sequence) — Kael's ordered character arc
|
||||
- `investigation_pressure` (sequence) — Parallel pressure escalation
|
||||
- `ambient_ring_activity` (pool) — Background ring business that runs throughout
|
||||
|
||||
Most modules should have 1-2 sequences plus 1 pool.
|
||||
|
||||
#### Event Step Fields
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `event_id` | Yes | Unique within module. Used in outcome conditions and `ticks_since_event` triggers. |
|
||||
| `label` | No | Short human-readable label for dev tooling. |
|
||||
| `description` | No | What happens narratively. Write this first — events should have a clear observable presence. |
|
||||
| `triggers` | Yes | ANY trigger being true fires the event. Multiple triggers = OR logic. |
|
||||
| `effects` | No | What changes in the world. |
|
||||
| `once` | No | Default `true`. Set `false` for repeating events (ambient discrepancies, etc.). |
|
||||
| `sets_flag` | No | Module-internal flag set when event fires. Used in outcome conditions. |
|
||||
|
||||
#### Trigger Types
|
||||
|
||||
| Type | Fires When | Key Fields |
|
||||
|------|-----------|------------|
|
||||
| `ticks_since_activation` | N ticks after module activated | `ticks` |
|
||||
| `ticks_since_event` | N ticks after a previous event fired | `after_event`, `ticks` |
|
||||
| `player_proximity` | Player near NPC/location | `target_type`, `target`, `radius_tiles` |
|
||||
| `player_action` | Player interacts with target | `action`, `target_role` |
|
||||
| `fact_known_by_player` | Player has discovered a fact | `fact_id` |
|
||||
| `flag_set` | A module flag has been set | `flag` |
|
||||
| `npc_mood` | NPC enters a mood state | `npc_role`, `mood` |
|
||||
|
||||
**Design principle: events should fire without the player.** Every event must have at least one tick-based trigger (`ticks_since_activation` or `ticks_since_event`). Proximity and action triggers are secondary paths that fire the event *earlier* if the player engages. The world moves at its own pace; the player accelerates or delays, not controls.
|
||||
|
||||
#### Effect Types
|
||||
|
||||
| Type | Use For |
|
||||
|------|---------|
|
||||
| `npc_routine_deviation` | Visible NPC behavior change. Write this descriptively — it's what the player sees. |
|
||||
| `fact_becomes_discoverable` | Gates a fact into the knowledge graph at Rumoured confidence. |
|
||||
| `tell_intensify` | NPC's tell behavior becomes more frequent/pronounced. |
|
||||
| `flag_set` | Internal state tracking. Not visible to player. |
|
||||
| `location_state` | Something visible changes in a location. |
|
||||
| `npc_knowledge_update` | An NPC learns something new. |
|
||||
|
||||
**On `fact_becomes_discoverable`:** This makes a fact discoverable, not known. The player still has to find it — through proximity, examination, dialogue, or observation. The `discovery_method` field is an authoring note for how: be specific enough that a Mellanie can write the dialogue or monologue that surfaces it, and a Gestalt can define the trigger condition in the fact catalog.
|
||||
|
||||
**Fact ID convention:** Use `{module-slug}.{fact_name}` — e.g., `ring.kael_unauthorized_corridor_access`. The module slug prefix namespaces the fact to avoid collisions across modules. Before creating a new fact ID, check `content/global/knowledge/` to see if an equivalent fact already exists; reuse it rather than creating a duplicate.
|
||||
|
||||
**Mapping `discovery_method` to D-035 trigger types:** The `discovery_method` note should describe exactly how the player triggers fact discovery. This maps directly to the D-035 monologue trigger taxonomy (full list in `decisions/content.md` D-035 and `content/global/enums/triggers.yaml`):
|
||||
|
||||
| If discovery happens via… | D-035 trigger type | What to author |
|
||||
|--------------------------|-------------------|----------------|
|
||||
| Player enters the location where something is visible | `enter_location` | Monologue line flagging the anomaly on arrival |
|
||||
| Player watches an NPC doing something unusual | `observe_npc` | Monologue line on NPC observation; dialogue option unlocks |
|
||||
| Player examines an object or terminal | `observe_anomaly` | Examine verb interaction; monologue on result |
|
||||
| Player witnesses two NPCs interacting | `witness_interaction` | Monologue line; trust-gated gossip unlock |
|
||||
| Player finishes a conversation with the relevant NPC | `post_conversation` | Monologue beat after talking to the NPC |
|
||||
| Player discovers a physical object (cargo, message) | `discover_evidence` | Examine verb; monologue on discovery |
|
||||
| Player returns to a location they've been before | `return_visit` | Monologue on changed state vs. prior visit |
|
||||
|
||||
Write the `discovery_method` note to specify which of these applies — ideally two methods for redundancy (e.g., `enter_location` plus `observe_anomaly`) so players aren't funneled into a single approach.
|
||||
|
||||
---
|
||||
|
||||
### Outcomes
|
||||
|
||||
Outcomes are resolution states. The storyteller checks all outcome conditions each tick after the module activates. The first matching outcome is applied.
|
||||
|
||||
**Every module must include:**
|
||||
- At least one terminal outcome that represents "the investigation succeeded"
|
||||
- At least one terminal outcome that represents "the conspiracy ran its course"
|
||||
- Exactly one expiry outcome (`is_expiry: true`) for quiet player non-engagement
|
||||
|
||||
#### Outcome Fields
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `outcome_id` | Yes | Unique slug. |
|
||||
| `label` | Yes | Short label. |
|
||||
| `is_terminal` | Yes | `true` = module ends. `false` = transitional state (module can continue evolving). |
|
||||
| `is_expiry` | No | `true` = this is the quiet-exit outcome. One per module. |
|
||||
| `conditions` | No | ALL conditions must be true. See below. |
|
||||
| `effects` | No | World changes when outcome is reached. |
|
||||
|
||||
**On `is_terminal: false`:** A non-terminal outcome fires its effects and applies its label, but the module remains active — the storyteller keeps checking for the next matching outcome. Use this for intermediate states where the world has visibly shifted but the situation hasn't resolved: the `ring_splinters` outcome in the vertical slice is non-terminal because the ring going quiet is a change of state, not a conclusion. A module with only non-terminal outcomes will run forever; always ensure there is a reachable terminal outcome (or expiry) downstream.
|
||||
|
||||
#### Outcome Conditions
|
||||
|
||||
| Condition | Description |
|
||||
|-----------|-------------|
|
||||
| `facts_known` | Player must know all listed facts. |
|
||||
| `facts_not_known` | Player must NOT know any listed facts. |
|
||||
| `flags_set` | All listed module flags must be set. |
|
||||
| `flags_not_set` | None of listed flags may be set. |
|
||||
| `events_fired` | All listed events must have fired. |
|
||||
| `ticks_since_activation` | Module has been running for at least N ticks. |
|
||||
|
||||
#### Outcome Effects
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `npc_disposition` | NPC's relationship state with player shifts. |
|
||||
| `faction_reaction` | Faction reputation change. |
|
||||
| `location_access_change` | Location becomes restricted, locked, or open. |
|
||||
| `fact_state` | Fact is permanently known, hidden, or destroyed. |
|
||||
| `npc_exit` | NPC leaves the district or becomes inaccessible. |
|
||||
|
||||
---
|
||||
|
||||
## Design Principles for Tier 1 Modules
|
||||
|
||||
### 1. The World Moves First
|
||||
|
||||
Events happen on a tick schedule. The player is a witness who can accelerate, delay, or redirect — not a trigger. If your module can only function if the player takes specific actions, it's a quest, not a drama module.
|
||||
|
||||
### 2. Both Characters Must Have a Story
|
||||
|
||||
Every event and outcome must mean something different to the smuggler and the detective. Write the `dual_lens` authoring field first. If you can't write both lenses, the module is character-agnostic filler — not Tier 1.
|
||||
|
||||
### 3. No Clean Resolutions
|
||||
|
||||
D-034 and D-027 both require moral ambiguity. The smuggling ring doesn't have a "good" ending. The detective arresting Kael is not obviously better than letting him go. Every outcome must have a cost. If one outcome is obviously correct, you've failed the design.
|
||||
|
||||
### 4. THE FRIEND Contradiction Is the Pivot
|
||||
|
||||
If your module involves a FRIEND-pattern NPC, the observable contradiction (D-034) must be:
|
||||
- **Observable from spatial positioning** — not from dialogue, not from menus
|
||||
- **Ambiguous before context** — the player sees the behavior before they understand what it means
|
||||
- **Irreversible once witnessed** — seeing changes the relationship, even if the player does nothing
|
||||
|
||||
The secret meeting in corridor B-7 is the canonical example. After witnessing it, neither character can pretend they don't know what they saw.
|
||||
|
||||
### 5. Expiry Is Not Failure
|
||||
|
||||
The `module_abandoned` expiry outcome should feel like a natural ending, not a penalty. The world closes around this conspiracy without the player. That's the 70% mundane reality (D-029): most conspiracies don't get protagonists. Write the expiry description to feel melancholy but not punitive.
|
||||
|
||||
### 6. Facts, Not Flags, Drive Investigation
|
||||
|
||||
Facts (from `global/knowledge/`) are the player's knowledge graph. Flags are the storyteller's internal state tracking. The key design question: "Is this something the player knows, or is this something the storyteller tracks?" If the player knows it, it's a fact. If the storyteller tracks it, it's a flag.
|
||||
|
||||
Facts should be discoverable through multiple methods (observation, dialogue, examination, proximity). Never require a single specific action to surface a critical fact.
|
||||
|
||||
---
|
||||
|
||||
## Validation and Format Rules (Gestalt)
|
||||
|
||||
These rules cover the schema's format constraints and the validation gaps that JSON Schema cannot enforce. All of these are also caught by Tier 2 build-time validation (`make validate-content`), but catching them during authoring saves a pipeline run.
|
||||
|
||||
### ID and Slug Formats
|
||||
|
||||
| Field | Regex | Example |
|
||||
|-------|-------|---------|
|
||||
| `module_id` | `^[a-z][a-z0-9-]*_v[0-9]+_[0-9]+$` | `smuggling_ring_v0_1` |
|
||||
| `sequence_id`, `pool_id` | `^[a-z][a-z0-9_-]*$` | `kael_exit_arc` |
|
||||
| `event_id` | `^[a-z][a-z0-9_-]*$` | `kael_goes_cold` |
|
||||
| `outcome_id` | `^[a-z][a-z0-9_-]*$` | `ring_exposed` |
|
||||
| `sets_flag` / flag references | `^[a-z][a-z0-9_-]*$` | `kael_behavior_changed` |
|
||||
| `role` (npc slot) | `^[a-z][a-z0-9-]*$` | `ring-member-exiting` |
|
||||
| `named_npc` | `^npc:[a-z][a-z0-9-]*$` | `npc:kael-davan` |
|
||||
| `version` | `^[0-9]+\\.[0-9]+$` | `0.1` |
|
||||
|
||||
Note the difference: `event_id`, `outcome_id`, `sequence_id`, and flags use underscores and hyphens (`[a-z0-9_-]*`). NPC `role` slugs use hyphens only (`[a-z0-9-]*`). Mixing them in wrong fields will fail schema validation.
|
||||
|
||||
### Flag Naming Convention
|
||||
|
||||
Flags are module-internal state. Every flag name that appears in `sets_flag` on an event **must** also appear in at least one outcome's `flags_set` or `flags_not_set` condition — or the flag serves no purpose. Convention:
|
||||
|
||||
- Use `snake_case` with underscores: `kael_behavior_changed`, `voss_pressure_applied`
|
||||
- Name by what happened, not what it enables: `handler_pressure_applied` not `kael_ready_to_flee`
|
||||
- Flags set by events accumulate — they are never automatically cleared
|
||||
- A flag set by a time-triggered event (not player-triggered) cannot be used as an expiry gate (see "Common Mistakes" below)
|
||||
|
||||
### Axis Constraint Syntax (Generated NPC Bindings)
|
||||
|
||||
The `constraint` field in `axes` is a freeform string. The storyteller's NPC filter interprets it. Convention (author responsibility — schema does not enforce):
|
||||
|
||||
| Prefix | Example | Meaning |
|
||||
|--------|---------|---------|
|
||||
| `has_` | `has_major_secret` | NPC axis value includes this descriptor |
|
||||
| `min_contentment_` | `min_contentment_-3` | Contentment axis value ≤ N (more discontented) |
|
||||
| `not_` | `not_combat_trained` | Axis value does NOT include this descriptor |
|
||||
| `is_` | `is_ring_member` | Boolean flag set on NPC profile |
|
||||
|
||||
### What JSON Schema Cannot Validate (Tier 2 Catches These)
|
||||
|
||||
| Issue | Where to Look | Impact |
|
||||
|-------|--------------|--------|
|
||||
| `fact_id` not defined in `global/knowledge/` | Effect `fact_becomes_discoverable`, outcome `facts_known` | Fact silently never becomes discoverable |
|
||||
| `sets_flag` name not referenced in any outcome condition | Event `sets_flag` | Flag is set but never meaningful |
|
||||
| `flags_set`/`flags_not_set` reference flag never set by any event | Outcome conditions | Condition permanently true or false |
|
||||
| `ticks_since_event.after_event` references unknown event_id | Event trigger | Trigger never fires |
|
||||
| `named_npc` ID doesn't exist in district NPC profiles | NPC requirements | Load-time failure |
|
||||
| Multiple outcomes have `is_expiry: true` | Outcomes list | Undefined storyteller behavior |
|
||||
| `faction` in outcome effects not in `global/factions/` | Outcome effects | Effect silently ignored |
|
||||
|
||||
### The Expiry Condition Pitfall
|
||||
|
||||
This is the most common authoring mistake for expiry outcomes. **The expiry condition must use `facts_not_known`, not `flags_not_set`.** Reason:
|
||||
|
||||
Events with `ticks_since_activation` triggers fire automatically without player engagement. If an auto-firing event sets a flag, and your expiry checks `flags_not_set: [that_flag]`, the expiry condition becomes permanently false after the event fires — the module can never expire quietly.
|
||||
|
||||
**Wrong:**
|
||||
```yaml
|
||||
# kael_goes_cold fires automatically at tick 300, sets kael_behavior_changed
|
||||
# This expiry can never fire after tick 300
|
||||
- outcome_id: module_abandoned
|
||||
is_expiry: true
|
||||
conditions:
|
||||
flags_not_set:
|
||||
- kael_behavior_changed # This flag is always set by tick 300
|
||||
ticks_since_activation: 5400
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
```yaml
|
||||
# facts_not_known gates on player investigative action, not auto-fired events
|
||||
- outcome_id: module_abandoned
|
||||
is_expiry: true
|
||||
conditions:
|
||||
facts_not_known:
|
||||
- "ring.cargo_discrepancy_pattern" # Only known if player examined terminal
|
||||
- "ring.kael_unauthorized_corridor_access" # Only known if player observed Kael
|
||||
ticks_since_activation: 5400
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checklist Before Submitting a New Module
|
||||
|
||||
- [ ] `module_id` uses correct format and doesn't collide with existing modules
|
||||
- [ ] `dual_lens` is written and shows clearly different experiences per character
|
||||
- [ ] `min_play_ticks` ≥ 1800 (30 minutes at 1 tick/second)
|
||||
- [ ] Every event sequence step has at least one tick-based trigger
|
||||
- [ ] Every `fact_becomes_discoverable` effect has a `discovery_method` note
|
||||
- [ ] The module includes at least one named FRIEND-pattern NPC (for v0.1 modules)
|
||||
- [ ] Expiry outcome is present (`is_expiry: true`) with conditions gated on `facts_not_known`, NOT `flags_not_set`
|
||||
- [ ] All outcomes have been reviewed for moral ambiguity — no "obviously correct" resolution
|
||||
- [ ] `npc_requirements` covers every role referenced in events and outcomes
|
||||
- [ ] All fact IDs used in effects/conditions exist in `global/knowledge/`
|
||||
- [ ] All `sets_flag` names appear in at least one outcome condition
|
||||
- [ ] All `flags_set`/`flags_not_set` names are set by at least one event's `sets_flag`
|
||||
- [ ] `make validate-content` passes
|
||||
|
||||
---
|
||||
|
||||
## Cross-References
|
||||
|
||||
| Topic | Location |
|
||||
|-------|----------|
|
||||
| Three-tier content model | `decisions/content.md` D-023 |
|
||||
| NPC 10-axis model | `decisions/content.md` D-024 |
|
||||
| Vertical slice scope | `decisions/scope.md` D-027 |
|
||||
| Population ratios | `decisions/content.md` D-029 |
|
||||
| THE FRIEND pattern | `decisions/content.md` D-034 |
|
||||
| Smuggling ring module | `content/modules/tier1/smuggling_ring_v0_1.yaml` |
|
||||
| Drama module schema | `content/schemas/drama_module.schema.yaml` |
|
||||
| Fact catalog | `content/global/knowledge/` |
|
||||
| NPC profiles (v0.1) | `content/campaigns/main/systems/krenn/` |
|
||||
| Storyteller stub | `server/src/storyteller/mod.rs` |
|
||||
|
||||
---
|
||||
|
||||
*Ticket #158 — Tier 1 drama module schema. Paula (dramatic structure), Gestalt (schema format), Mellanie (authoring review).*
|
||||
@@ -0,0 +1,60 @@
|
||||
# Sprint 17: Tell — Client Tasks
|
||||
|
||||
**Goal:** NPCs volunteer information unprompted and express personality through delivery; the player can read time and orientation at a glance from the insert HUD; POI infrastructure lands on the server.
|
||||
|
||||
**Branch:** `client`
|
||||
**Agents:** Stig (UI), Tyre (arch), Hoshe (QA)
|
||||
|
||||
## Carry-over from Sprint 16
|
||||
|
||||
None. Sprint 16 client tickets (#540, #543) will close before Sprint 17.
|
||||
|
||||
## New Tickets
|
||||
|
||||
| # | Title | Blocked by |
|
||||
|---|-------|------------|
|
||||
| #263 | Time display on insert HUD | — |
|
||||
| #537 | UX: E-Talk overlay improvement | — |
|
||||
|
||||
Use `db/connectors/ticket show <id>` for full details.
|
||||
|
||||
## Key Decisions
|
||||
|
||||
- `decisions/perception.md` — D-019 (camera angle), D-033 (entity color)
|
||||
- `decisions/architecture.md` — D-020 (Godot client + Rust server via IPC)
|
||||
- `decisions/scope.md` — D-051 (diegetic insert/minimap)
|
||||
|
||||
## Notes
|
||||
|
||||
### #263 — Time display on insert HUD
|
||||
|
||||
**What exists:** The client has `client/scripts/ui/debug_overlay.gd` for debug display. The insert HUD concept is defined in D-051 but no insert UI code exists yet. `client/scripts/rendering/world_renderer.gd` handles the main rendering pipeline. The server sends `SimulationTime` data in `ObserverSnapshot` (see `server/src/bridge/types.rs`).
|
||||
|
||||
**What to deliver:** A diegetic time display on the player's insert/HUD. The player needs to know the current game time for routine-based investigation (NPC schedules, shift changes). Should show time in the station's local format, not abstract tick counts.
|
||||
|
||||
**Design dependency:** #314 (visual, this sprint) produces the insert/HUD wireframe. Coordinate with Araminta's output for placement and visual style. Can start with a placeholder layout and refine after the wireframe lands.
|
||||
|
||||
**Integration:** Reads `SimulationTime` from the snapshot. No new server work required — time data is already in the protocol.
|
||||
|
||||
### #537 — UX: E-Talk overlay improvement
|
||||
|
||||
**What exists:** The E-Talk interaction overlay appears when adjacent to an NPC. Playtester feedback says it doesn't communicate enough value — the overlay appears but doesn't convey what talking will accomplish or what access tier is available.
|
||||
|
||||
**What to deliver:** Improve the overlay to show: NPC name (if known from KG), relationship state color (from `RelationshipState` in snapshot), and a hint of available dialogue tier (surface/real/secret based on trust level). The goal is to make the player feel informed before committing to a conversation.
|
||||
|
||||
**Integration:** All data needed is already in `ObserverSnapshot` — the `VisibleEntity` includes `relationship` state and `known_attributes`. No server changes needed.
|
||||
|
||||
## Dependency Chain
|
||||
|
||||
```
|
||||
#263 (Time display) → standalone
|
||||
#537 (E-Talk UX) → standalone
|
||||
Both can run in parallel.
|
||||
```
|
||||
|
||||
## PR Workflow
|
||||
|
||||
When ready to submit, create a PR with `tea` CLI:
|
||||
```bash
|
||||
tea pr create --repo jpmschweitzer/settled-reach --login schweitz --title "feat(client): sprint 17 description" --description "body" --base main --head client
|
||||
```
|
||||