Merge remote-tracking branch 'origin/server'
# Conflicts: # CHANGELOG.md
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
---
|
||||
name: push-pr
|
||||
description: >
|
||||
Push commits and create or update a pull request. Use when the user says
|
||||
"push pr", "push and create pr", "update pr", "create a pr", "open a pr",
|
||||
or invokes /push-pr. NOT triggered by plain "push" (that's just git push).
|
||||
Pushes the current branch, creates a PR if none exists, or confirms the
|
||||
existing PR was updated. NEVER merges the PR into main — this skill only
|
||||
pushes to the branch and manages the PR lifecycle.
|
||||
user-invocable: true
|
||||
allowed-tools: Bash, Read, Grep, Glob, AskUserQuestion
|
||||
---
|
||||
|
||||
# Push PR Skill
|
||||
|
||||
Push commits to remote and create or update a PR. Operates exclusively on the
|
||||
current branch — never touches main.
|
||||
|
||||
## Safety Rules (NON-NEGOTIABLE)
|
||||
|
||||
- **NEVER merge a PR into main.** No `tea pr merge`, no `git merge` into main.
|
||||
- **NEVER checkout or push to main.**
|
||||
- **NEVER force-push** unless the user explicitly requests it.
|
||||
- **NEVER use `--no-verify` or skip hooks.**
|
||||
- Only push to the current working branch.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Validate branch
|
||||
|
||||
```bash
|
||||
git branch --show-current
|
||||
```
|
||||
|
||||
If on `main`, stop: "You're on main. Switch to a team branch first."
|
||||
|
||||
### 2. Check for unpushed commits
|
||||
|
||||
```bash
|
||||
git fetch --all
|
||||
git status
|
||||
git log --oneline origin/<branch>..<branch>
|
||||
```
|
||||
|
||||
If no unpushed commits, skip to step 4 (PR check).
|
||||
|
||||
### 3. Check for conflicts with main
|
||||
|
||||
```bash
|
||||
git merge-tree --write-tree origin/main HEAD 2>&1
|
||||
```
|
||||
|
||||
If conflicts reported, merge main into current branch:
|
||||
|
||||
```bash
|
||||
git merge origin/main --no-edit
|
||||
```
|
||||
|
||||
If merge conflicts, **stop and report** — let the user resolve.
|
||||
If clean, continue.
|
||||
|
||||
### 4. Push
|
||||
|
||||
```bash
|
||||
git push origin <branch>
|
||||
```
|
||||
|
||||
If push fails, stop and report. Never force-push without explicit request.
|
||||
|
||||
### 5. Check for existing PR
|
||||
|
||||
```bash
|
||||
tea pr list --login schweitz --repo jpmschweitzer/settled-reach --state open --output simple
|
||||
```
|
||||
|
||||
Match current branch name in PR list.
|
||||
|
||||
- **PR exists**: Report "Pushed N commits to `<branch>`. PR #X updated." Done.
|
||||
- **No PR**: Continue to step 6.
|
||||
|
||||
### 6. Create a new PR
|
||||
|
||||
```bash
|
||||
git log --oneline main..<branch>
|
||||
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)" \
|
||||
--base main \
|
||||
--head <branch>
|
||||
```
|
||||
|
||||
Report PR URL when done.
|
||||
|
||||
## Arguments
|
||||
|
||||
If the user passes arguments (e.g., `/push-pr "my title"`), use them as the
|
||||
PR title instead of generating one.
|
||||
@@ -11,11 +11,55 @@ Format based on [Keep a Changelog](https://keepachangelog.com/).
|
||||
- 3D-to-2D sprite render pipeline (`client/tooling/sprite_renderer/`) — Godot @tool scene renders textured 3D models at "the angle" (-72.5deg ortho) from 4 cardinal directions at 1024/256/64 resolutions with outline applied at working resolution
|
||||
- `/render-sprite` skill — CLI wrapper for the render pipeline with headless import step
|
||||
- Pipeline POC: Era 1 institutional wall + bar green wall textures generated via Nano Banana and rendered through full pipeline
|
||||
- PerceptionQuery trait and ActivePerceptionMode resource — abstraction layer for D-017 perception mode swapping (NaturalVision default implementation)
|
||||
- VisibilityGeometry intermediate resource decoupling FOV computation from entity filtering
|
||||
- Client-side PROTOCOL_VERSION enforcement — snapshot decoder rejects version mismatches with error log
|
||||
- POI verb priority test in observer pipeline — asserts both verb kind and priority values end-to-end
|
||||
|
||||
### Changed
|
||||
- Observer pipeline decomposed into two-stage system: compute_visibility_geometry (geometry) → compute_observer_snapshot (entity filtering + assembly)
|
||||
- POI verb priority adjustment moved from simulation phase (interaction.rs) to perception phase (observer) — fixes D-010 information boundary violation
|
||||
- compute_nearby_interactions no longer reads KnowledgeGraph — determines verb availability by proximity only, verb priority adjusted by observer
|
||||
- compute_nearby_interactions scheduling moved from SimulationPlugin to BridgePlugin for explicit ordering with geometry and observer systems
|
||||
- Client test snapshot updated to v4 format (Protocol.PROTOCOL_VERSION, tick_rate replaces paused)
|
||||
|
||||
### Added
|
||||
- Content validation tooling — `make validate-content` validates campaign YAML files against JSON schemas, maps files by directory context
|
||||
- Entity::to_bits() roundtrip test — guards against bevy version changes silently breaking wire IDs
|
||||
- TickRate switch mid-accumulation test — verifies Half→Full→Paused→Half transitions preserve accumulator state
|
||||
- Tick rate scaling system (#406, D-052) — Full/Half/Paused rates with fractional accumulation, SetTickRate player action, replaces binary pause flag
|
||||
- Proximity detection and interaction verbs (#404, D-060) — compute_nearby_interactions system with Manhattan distance ranges (close ≤2, mid ≤5), context-sensitive verb computation (Talk, Observe, Examine), PersonOfInterest priority flip, NearbyInteraction in ObserverSnapshot v4
|
||||
- Content directory skeleton (#385, D-057) — district-as-atomic-pack layout with Sova Transit first district, 17 NPC stubs, 3 locations, 5 triangles, dialogue/monologue pools, factions, knowledge catalogs, enum definitions
|
||||
- Content schema definitions (#386) — 8 JSON Schema files (draft 2020-12) for district, location, npc-profile, dialogue-pool, monologue-pool, triangle, routine, fact-catalog validation
|
||||
- PROTOCOL_VERSION constant in bridge types — versioning strategy documented (subprocess IPC, serde defaults for field evolution)
|
||||
- Campaign, system, station JSON schemas for hierarchical content validation
|
||||
|
||||
### Fixed
|
||||
- Wire entity_id now uses StableId consistently across observer, observation, interpretation, and interaction systems (was Entity::to_bits() in some paths)
|
||||
- Restored system/station/district hierarchical fields in district metadata (incorrectly removed during canonical_id cleanup)
|
||||
- Unregistered entities in observer/interaction now log tracing::error instead of silently falling back to Entity::to_bits()
|
||||
|
||||
### Changed
|
||||
- NearbyInteractionBuffer refactored from global Resource to per-entity Component on PlayerCharacter — multiplayer-ready (D-009)
|
||||
- Observer module split into mod.rs (244 lines) + tests.rs (480 lines) — reduces module complexity
|
||||
- Content directory restructured from flat districts/ to hierarchical campaigns/main/systems/krenn/stations/sova/districts/transit/ — path mirrors canonical IDs, glob-based discovery, multi-campaign/DLC ready
|
||||
- Content manifest (content.yaml) rewritten for glob-based district discovery
|
||||
- District identity fields (system, station, district) now derived from directory path — removed from district.yaml required fields
|
||||
- NPC canonical_id schema accepts district-scoped IDs (npc:transit.kael-davan) for cross-district uniqueness
|
||||
- ObserverSnapshot protocol bumped to v4 — adds nearby_interactions field, tick_rate replaces paused field in GameTime
|
||||
- NearbyInteractionBuffer.interactions is now private with take() accessor (no per-frame clone)
|
||||
- NearbyInteraction.distance changed from f32 to u32 (matches manhattan distance)
|
||||
- Missing PlayerCharacter in input processing now panics instead of silent no-op
|
||||
- Verb sort uses (priority, kind) tuple for deterministic ordering at equal priority
|
||||
- Unregistered entity in knowledge events triggers debug_assert + error (was warn)
|
||||
- District schema: canonical_id is now optional (derived from directory path at load time)
|
||||
- Skills trimmed for CLAUDE.md deduplication — search-docs (-48%), ticket (-17%), review-pr (-35%) now reference CLAUDE.md for basics instead of repeating them
|
||||
- Review-pr reviewer profiles extracted to `references/reviewer-profiles.md` for progressive disclosure
|
||||
|
||||
### Removed
|
||||
- Dead generate_snapshot function in bridge/mod.rs — superseded by compute_observer_snapshot
|
||||
- content/global/regions/ directory — region data absorbed into system.yaml metadata
|
||||
|
||||
### Added
|
||||
- Dual Lens Authoring Guide — 7-chapter reference for writing content that works for both smuggler and detective perspectives (D-027, D-028, D-032, D-034, D-035)
|
||||
- THE MIRROR pattern spec — transparency-as-contrast NPC design with Naia Tamm reference implementation and generator template
|
||||
|
||||
@@ -2,7 +2,7 @@ GODOT := $(shell command -v godot4 2>/dev/null || command -v godot 2>/dev/null)
|
||||
|
||||
.PHONY: help setup build client server test lint ci ci-client ci-server clean \
|
||||
decisions-sync decisions-coverage decisions-active decisions-orphan \
|
||||
db-backup db-install
|
||||
db-backup db-install validate-content
|
||||
|
||||
# --- Configuration ---
|
||||
|
||||
@@ -30,6 +30,7 @@ help:
|
||||
@echo " make decisions-coverage Decision-to-ticket coverage by domain"
|
||||
@echo " make decisions-active List active decisions"
|
||||
@echo " make decisions-orphan Decisions without implementing tickets"
|
||||
@echo " make validate-content Validate content YAML against schemas"
|
||||
@echo ""
|
||||
@echo " GODOT_VERSION=4.6 make setup Override Godot version"
|
||||
|
||||
@@ -127,6 +128,11 @@ decisions-active:
|
||||
decisions-orphan:
|
||||
@db/connectors/sqlite-query "SELECT id, title FROM decisions WHERE type='confirmed' AND status='active' AND id NOT IN (SELECT DISTINCT decision_ref FROM tickets WHERE decision_ref IS NOT NULL)"
|
||||
|
||||
# --- Content Validation ---
|
||||
|
||||
validate-content:
|
||||
@tooling/validate-content
|
||||
|
||||
# --- Clean ---
|
||||
|
||||
clean:
|
||||
|
||||
@@ -296,12 +296,12 @@ func _test_snapshot() -> Dictionary:
|
||||
|
||||
return {
|
||||
"tick": _test_tick,
|
||||
"version": 2,
|
||||
"version": Protocol.PROTOCOL_VERSION,
|
||||
"game_time": {
|
||||
"day": 0,
|
||||
"time_of_day": _test_tick * 10,
|
||||
"day_phase": "Morning",
|
||||
"paused": false,
|
||||
"tick_rate": "Full",
|
||||
},
|
||||
"player_facing": _test_facing,
|
||||
"entities": entities,
|
||||
|
||||
@@ -9,6 +9,10 @@ class_name Protocol
|
||||
## Unit enum variants (no data) → bare strings ("MoveNorth", "Npc")
|
||||
## Data enum variants → single-element maps ({"UsePerceptionMode": "thermal"})
|
||||
|
||||
## Protocol version — must match server PROTOCOL_VERSION in bridge/types.rs.
|
||||
## Reject snapshots where version != this value.
|
||||
const PROTOCOL_VERSION: int = 4
|
||||
|
||||
|
||||
# -- Decode: bytes from server → GDScript types --------------------------------
|
||||
|
||||
@@ -27,6 +31,12 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant:
|
||||
push_error("Protocol: snapshot missing required fields")
|
||||
return null
|
||||
|
||||
# Version check: reject snapshots from incompatible server
|
||||
var version: Variant = raw.get("version")
|
||||
if version != PROTOCOL_VERSION:
|
||||
push_error("Protocol: version mismatch (got %s, expected %s). Server and client are out of sync." % [version, PROTOCOL_VERSION])
|
||||
return null
|
||||
|
||||
var entities: Array[Dictionary] = []
|
||||
var raw_entities: Array = raw["entities"]
|
||||
var dropped := 0
|
||||
@@ -44,8 +54,7 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant:
|
||||
# in any realistic scenario (would require ~29 billion years at 10 ticks/game-minute per D-031).
|
||||
var tick: int = raw["tick"]
|
||||
|
||||
# v2 fields — optional for backward compatibility
|
||||
var version: Variant = raw.get("version")
|
||||
# version already checked above; game_time for HUD display
|
||||
var game_time: Variant = raw.get("game_time")
|
||||
|
||||
# player_facing: FacingDirection is a unit enum → bare string in rmp_serde
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,50 @@
|
||||
# Content Infrastructure
|
||||
|
||||
Directories prefixed with `_` are infrastructure, not game content. The server content loader skips directories starting with `_` when scanning for content files.
|
||||
|
||||
- `_meta/` — Infrastructure metadata (this directory)
|
||||
- `_schema/` — JSON Schema validation files (draft 2020-12)
|
||||
|
||||
## Directory Hierarchy
|
||||
|
||||
Content is organized hierarchically to match the game universe:
|
||||
|
||||
```
|
||||
content/
|
||||
content.yaml # Manifest: campaign list, glob discovery patterns
|
||||
global/ # Cross-campaign shared content (factions, enums, knowledge)
|
||||
campaigns/
|
||||
{campaign}/ # e.g., "main"
|
||||
campaign.yaml
|
||||
systems/
|
||||
{system}/ # e.g., "krenn"
|
||||
system.yaml
|
||||
stations/
|
||||
{station}/ # e.g., "sova"
|
||||
station.yaml
|
||||
districts/
|
||||
{district}/ # e.g., "transit"
|
||||
district.yaml
|
||||
npcs/
|
||||
locations/
|
||||
triangles/
|
||||
dialogue/
|
||||
monologue/
|
||||
routines/
|
||||
templates/
|
||||
```
|
||||
|
||||
## Canonical ID Derivation
|
||||
|
||||
Identity is derived from directory path at load time — no redundant ID fields in YAML.
|
||||
|
||||
- **District:** `{system}.{station}.{district}` (e.g., `krenn.sova.transit`)
|
||||
- **NPC (within district):** `npc:{slug}` (e.g., `npc:kael-davan`)
|
||||
- **NPC (cross-district):** `npc:{district}.{slug}` (e.g., `npc:transit.kael-davan`)
|
||||
- **Location:** `{system}.{station}.{district}.location.{slug}`
|
||||
|
||||
## Content Discovery
|
||||
|
||||
The loader reads `content.yaml` for enabled campaigns and their glob patterns.
|
||||
District discovery uses `systems/**/districts/*/district.yaml` — no per-district
|
||||
manifest entry needed. Adding a district = creating a directory with district.yaml.
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "campaign.schema.json",
|
||||
"title": "Campaign Metadata",
|
||||
"description": "Campaign definition file — one per campaign directory (D-003).",
|
||||
"type": "object",
|
||||
"required": ["display_name", "description"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"display_name": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "dialogue-pool.schema.json",
|
||||
"title": "Dialogue Line Pool",
|
||||
"description": "Tagged dialogue lines scoped by location + role (D-028, D-035).",
|
||||
"type": "object",
|
||||
"required": ["location", "role", "lines"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"location": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9-]*$",
|
||||
"description": "Location slug this dialogue pool belongs to"
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9-]*$",
|
||||
"description": "Template role slug (e.g. dock-worker, bar-owner)"
|
||||
},
|
||||
"lines": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/dialogue_line" },
|
||||
"minItems": 1
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"dialogue_line": {
|
||||
"type": "object",
|
||||
"required": ["id", "text", "role", "access", "trust", "situation"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9-]*_d_[0-9]{3}$",
|
||||
"description": "Stable line ID: {location_slug}_d_{###}"
|
||||
},
|
||||
"text": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9-]*$",
|
||||
"description": "Template role this line belongs to"
|
||||
},
|
||||
"access": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": ["public", "insider", "authority", "peer", "hostile"]
|
||||
},
|
||||
"minItems": 1,
|
||||
"uniqueItems": true,
|
||||
"description": "Access tiers this line is available at (multi-tier eligibility)"
|
||||
},
|
||||
"trust": {
|
||||
"type": "string",
|
||||
"enum": ["surface", "real", "secret"],
|
||||
"description": "Minimum trust level required"
|
||||
},
|
||||
"situation": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"arrival", "shift_start", "shift_end", "shift_transition",
|
||||
"bar_evening", "night_shift", "investigation", "confrontation",
|
||||
"social", "alone", "emergency", "routine", "observation"
|
||||
],
|
||||
"description": "Situation context when this line can fire"
|
||||
},
|
||||
"topic": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"colleague", "routine", "cargo", "money", "trust",
|
||||
"danger", "institution", "personal", "investigation"
|
||||
],
|
||||
"description": "Topic tag for selection weighting"
|
||||
},
|
||||
"mood": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"fond", "comfortable", "worried", "suspicious",
|
||||
"analytical", "conflicted", "concerned", "relieved"
|
||||
],
|
||||
"description": "Mood tag for selection weighting"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Freeform tags for additional filtering"
|
||||
},
|
||||
"knowledge_grant": {
|
||||
"type": "object",
|
||||
"description": "Knowledge the player gains from hearing this line",
|
||||
"properties": {
|
||||
"fact_id": { "type": "string" },
|
||||
"confidence": {
|
||||
"type": "string",
|
||||
"enum": ["suspects", "knows_of", "knows_details", "direct"]
|
||||
}
|
||||
},
|
||||
"required": ["fact_id", "confidence"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "district.schema.json",
|
||||
"title": "District Metadata",
|
||||
"description": "District definition file — one per district directory (D-036). Identity derived from directory path.",
|
||||
"type": "object",
|
||||
"required": ["display_name", "description", "locations", "npc_count"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"canonical_id": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z]+\\.[a-z]+\\.[a-z-]+$",
|
||||
"description": "Deprecated — derived from directory path at load time. If present, must match {system}.{station}.{district}."
|
||||
},
|
||||
"display_name": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"system": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z]+$",
|
||||
"description": "Deprecated — derived from directory path."
|
||||
},
|
||||
"station": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z]+$",
|
||||
"description": "Deprecated — derived from directory path."
|
||||
},
|
||||
"district": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z-]+$",
|
||||
"description": "Deprecated — derived from directory path."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"locations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9-]*$"
|
||||
},
|
||||
"minItems": 1,
|
||||
"uniqueItems": true
|
||||
},
|
||||
"npc_count": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "fact-catalog.schema.json",
|
||||
"title": "Fact Catalog",
|
||||
"description": "Fact definitions — one file per fact category in global/knowledge/ (D-035).",
|
||||
"type": "object",
|
||||
"required": ["category", "facts"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"category": {
|
||||
"type": "string",
|
||||
"description": "Fact category matching the filename"
|
||||
},
|
||||
"facts": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/fact" },
|
||||
"minItems": 1
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"fact": {
|
||||
"type": "object",
|
||||
"required": ["fact_id", "description", "discoverable_by"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"fact_id": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9_-]*$",
|
||||
"description": "Stable unique fact identifier"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"discoverable_by": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": ["smuggler", "detective"]
|
||||
},
|
||||
"minItems": 1,
|
||||
"uniqueItems": true,
|
||||
"description": "Which playable characters can discover this fact"
|
||||
},
|
||||
"abstract": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "If true, fact cannot reach Direct confidence (only inferred)"
|
||||
},
|
||||
"progression": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/confidence_level" },
|
||||
"description": "Confidence level descriptions in ascending order"
|
||||
}
|
||||
}
|
||||
},
|
||||
"confidence_level": {
|
||||
"type": "object",
|
||||
"required": ["confidence", "text"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"confidence": {
|
||||
"type": "string",
|
||||
"enum": ["suspects", "knows_of", "knows_details", "direct"]
|
||||
},
|
||||
"text": {
|
||||
"type": "string",
|
||||
"description": "Player-facing description at this confidence level"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "location.schema.json",
|
||||
"title": "Location Definition",
|
||||
"description": "Location metadata — one file per location (D-025, D-036).",
|
||||
"type": "object",
|
||||
"required": ["canonical_id", "display_name"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"canonical_id": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z]+\\.[a-z]+\\.[a-z-]+\\.location\\.[a-z][a-z0-9-]*$",
|
||||
"description": "Full canonical ID: {system}.{station}.{district}.location.{slug}"
|
||||
},
|
||||
"display_name": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"tile_bounds": {
|
||||
"type": "object",
|
||||
"description": "Rectangular tile bounds for this location",
|
||||
"properties": {
|
||||
"x_min": { "type": "integer" },
|
||||
"y_min": { "type": "integer" },
|
||||
"x_max": { "type": "integer" },
|
||||
"y_max": { "type": "integer" },
|
||||
"z": { "type": "integer" }
|
||||
},
|
||||
"required": ["x_min", "y_min", "x_max", "y_max", "z"]
|
||||
},
|
||||
"sightlines": {
|
||||
"type": "object",
|
||||
"description": "Sightline properties for LOS computation",
|
||||
"properties": {
|
||||
"open": {
|
||||
"type": "boolean",
|
||||
"description": "True if the location is open-plan (no internal walls)"
|
||||
},
|
||||
"notes": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"ambient_sound": {
|
||||
"type": "string",
|
||||
"description": "Reference to ambient sound asset"
|
||||
},
|
||||
"social_site": {
|
||||
"type": "string",
|
||||
"description": "Social site template this location belongs to"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "monologue-pool.schema.json",
|
||||
"title": "Monologue Line Pool",
|
||||
"description": "Tagged monologue lines, hard-partitioned by character (D-032, D-035).",
|
||||
"type": "object",
|
||||
"required": ["character", "location", "lines"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"character": {
|
||||
"type": "string",
|
||||
"enum": ["smuggler", "detective"],
|
||||
"description": "Playable character this pool belongs to (hard partition per D-032)"
|
||||
},
|
||||
"location": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9-]*$",
|
||||
"description": "Location slug, or 'general' for location-independent lines"
|
||||
},
|
||||
"lines": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/monologue_line" },
|
||||
"minItems": 1
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"monologue_line": {
|
||||
"type": "object",
|
||||
"required": ["id", "text", "trigger"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9-]*_m_[sd]_[0-9]{3}$",
|
||||
"description": "Stable line ID: {location_slug}_m_{s|d}_{###}"
|
||||
},
|
||||
"text": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 160,
|
||||
"description": "Line text — 160 char max per D-059"
|
||||
},
|
||||
"trigger": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"enter_location", "observe_npc", "hear_sound",
|
||||
"observe_anomaly", "post_conversation", "discover_evidence",
|
||||
"witness_interaction", "time_idle", "return_visit"
|
||||
],
|
||||
"description": "What causes this line to fire"
|
||||
},
|
||||
"prerequisites": {
|
||||
"type": "object",
|
||||
"description": "AND-only prerequisite conditions",
|
||||
"properties": {
|
||||
"facts": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/fact_prerequisite" }
|
||||
},
|
||||
"entity_attributes": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/attribute_prerequisite" }
|
||||
},
|
||||
"relationship": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"target": { "type": "string" },
|
||||
"state": {
|
||||
"type": "string",
|
||||
"enum": ["unknown", "known", "friendly", "person_of_interest", "hostile"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 10,
|
||||
"default": 5,
|
||||
"description": "Selection priority (higher = more likely to fire)"
|
||||
},
|
||||
"cooldown": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Minimum ticks before this line can fire again"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"fact_prerequisite": {
|
||||
"type": "object",
|
||||
"required": ["fact_id", "min_confidence"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"fact_id": { "type": "string" },
|
||||
"min_confidence": {
|
||||
"type": "string",
|
||||
"enum": ["suspects", "knows_of", "knows_details", "direct"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"attribute_prerequisite": {
|
||||
"type": "object",
|
||||
"required": ["entity", "key", "value"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"entity": { "type": "string" },
|
||||
"key": { "type": "string" },
|
||||
"value": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "npc-profile.schema.json",
|
||||
"title": "NPC Profile",
|
||||
"description": "10-axis NPC model profile — one file per NPC (D-024, D-034).",
|
||||
"type": "object",
|
||||
"required": ["canonical_id", "display_name", "tier", "pattern", "motivation"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"canonical_id": {
|
||||
"type": "string",
|
||||
"pattern": "^npc:([a-z][a-z0-9-]+\\.)?[a-z][a-z0-9-]*$",
|
||||
"description": "Canonical ID: npc:{slug} (within district) or npc:{district}.{slug} (cross-district)"
|
||||
},
|
||||
"display_name": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"tier": {
|
||||
"type": "integer",
|
||||
"enum": [1, 2, 3],
|
||||
"description": "NPC tier: 1 = conspiracy (full depth), 2 = template (social context), 3 = filler (atmosphere)"
|
||||
},
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"enum": ["FRIEND", "MIRROR", "ANCHOR", "GHOST", "CATALYST", "THRESHOLD", "REMNANT", "SYSTEM", "NOBODY"],
|
||||
"description": "Thematic pattern — System A (D-050)"
|
||||
},
|
||||
"motivation": {
|
||||
"type": "string",
|
||||
"enum": ["HANDLER", "WITNESS", "TURNCOAT", "CIVILIAN", "OPERATOR", "SKEPTIC"],
|
||||
"description": "Functional motivation — System B (D-050)"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"want": {
|
||||
"type": "object",
|
||||
"description": "Primary want/need driving this NPC",
|
||||
"properties": {
|
||||
"primary": { "type": "string" },
|
||||
"intensity": { "type": "integer", "minimum": 0, "maximum": 10 },
|
||||
"description": { "type": "string" }
|
||||
},
|
||||
"required": ["primary"]
|
||||
},
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The NPC's hidden truth — what they don't want the player to know"
|
||||
},
|
||||
"relationships": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/relationship" },
|
||||
"description": "Named relationships to other NPCs"
|
||||
},
|
||||
"tolerance": {
|
||||
"type": "object",
|
||||
"description": "Stress tolerance threshold",
|
||||
"properties": {
|
||||
"threshold": { "type": "integer" },
|
||||
"description": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"routine": {
|
||||
"type": "object",
|
||||
"description": "Summary of daily routine (full schedule in routines/schedules.yaml)",
|
||||
"properties": {
|
||||
"summary": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"information": {
|
||||
"type": "object",
|
||||
"description": "What this NPC knows",
|
||||
"properties": {
|
||||
"knows": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Fact IDs this NPC knows"
|
||||
},
|
||||
"access_tier": {
|
||||
"type": "string",
|
||||
"enum": ["public", "insider", "authority", "peer", "hostile"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"contentment": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level": { "type": "integer", "minimum": -10, "maximum": 10 },
|
||||
"description": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"personality": {
|
||||
"type": "object",
|
||||
"description": "Personality traits and behavioral tendencies",
|
||||
"additionalProperties": { "type": "string" }
|
||||
},
|
||||
"tells": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/tell" },
|
||||
"description": "Observable behavioral tells (D-024)"
|
||||
},
|
||||
"skills": {
|
||||
"type": "object",
|
||||
"description": "Skill set and combat capability",
|
||||
"properties": {
|
||||
"combat_trained": { "type": "boolean" },
|
||||
"skills": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "integer", "minimum": 0, "maximum": 10 }
|
||||
}
|
||||
}
|
||||
},
|
||||
"triangle_membership": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Triangle slugs this NPC participates in"
|
||||
},
|
||||
"trust_levels": {
|
||||
"type": "object",
|
||||
"description": "What the NPC reveals at each trust level",
|
||||
"properties": {
|
||||
"surface": { "type": "string" },
|
||||
"real": { "type": "string" },
|
||||
"secret": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"friend_arc": {
|
||||
"type": "object",
|
||||
"description": "FRIEND arc data — only valid on FRIEND-pattern Tier 1 NPCs (D-034)",
|
||||
"properties": {
|
||||
"bonded_character": {
|
||||
"type": "string",
|
||||
"enum": ["smuggler", "detective"]
|
||||
},
|
||||
"phases": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/friend_phase" }
|
||||
}
|
||||
},
|
||||
"required": ["bonded_character", "phases"]
|
||||
},
|
||||
"dual_lens": {
|
||||
"type": "object",
|
||||
"description": "Authoring-only: how smuggler vs detective perceives this NPC",
|
||||
"properties": {
|
||||
"smuggler": { "type": "string" },
|
||||
"detective": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"notes": {
|
||||
"type": "string",
|
||||
"description": "Authoring-only: design notes"
|
||||
}
|
||||
},
|
||||
"if": {
|
||||
"properties": { "pattern": { "const": "FRIEND" } }
|
||||
},
|
||||
"then": {
|
||||
"required": ["friend_arc"]
|
||||
},
|
||||
"$defs": {
|
||||
"relationship": {
|
||||
"type": "object",
|
||||
"required": ["target", "kind"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"target": {
|
||||
"type": "string",
|
||||
"pattern": "^npc:([a-z][a-z0-9-]+\\.)?[a-z][a-z0-9-]*$"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": ["colleague", "friend", "rival", "romantic", "family", "superior", "subordinate"]
|
||||
},
|
||||
"trust": {
|
||||
"type": "integer",
|
||||
"minimum": -10,
|
||||
"maximum": 10
|
||||
},
|
||||
"notes": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"tell": {
|
||||
"type": "object",
|
||||
"required": ["trigger", "behavior"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"trigger": { "type": "string" },
|
||||
"behavior": { "type": "string" },
|
||||
"visible_to": {
|
||||
"type": "string",
|
||||
"enum": ["forward", "peripheral", "any"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"friend_phase": {
|
||||
"type": "object",
|
||||
"required": ["phase", "description"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"phase": { "type": "integer", "minimum": 1, "maximum": 5 },
|
||||
"description": { "type": "string" },
|
||||
"trigger": { "type": "string" },
|
||||
"routine_deviation": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "routine.schema.json",
|
||||
"title": "NPC Routine Schedules",
|
||||
"description": "Daily routine schedules — all NPCs in one file per district for cross-NPC validation (D-034).",
|
||||
"type": "object",
|
||||
"required": ["district", "schedules"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"district": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9-]*$",
|
||||
"description": "District slug this routine file belongs to"
|
||||
},
|
||||
"schedules": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/npc_schedule" },
|
||||
"minItems": 1
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"npc_schedule": {
|
||||
"type": "object",
|
||||
"required": ["npc", "entries"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"npc": {
|
||||
"type": "string",
|
||||
"pattern": "^npc:[a-z][a-z0-9-]*$",
|
||||
"description": "NPC short-form canonical ID"
|
||||
},
|
||||
"entries": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/routine_entry" },
|
||||
"minItems": 1
|
||||
},
|
||||
"deviations": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/deviation" },
|
||||
"description": "Conditional schedule overrides (FRIEND arc staging, etc.)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"routine_entry": {
|
||||
"type": "object",
|
||||
"required": ["phase", "location"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"phase": {
|
||||
"type": "string",
|
||||
"enum": ["morning", "afternoon", "evening", "night"],
|
||||
"description": "Day phase for this entry"
|
||||
},
|
||||
"location": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9-]*$",
|
||||
"description": "Location slug"
|
||||
},
|
||||
"tile": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": { "type": "integer" },
|
||||
"y": { "type": "integer" }
|
||||
},
|
||||
"required": ["x", "y"],
|
||||
"description": "Specific tile coordinates within the location"
|
||||
},
|
||||
"activity": {
|
||||
"type": "string",
|
||||
"description": "What the NPC is doing at this location/time"
|
||||
}
|
||||
}
|
||||
},
|
||||
"deviation": {
|
||||
"type": "object",
|
||||
"required": ["trigger", "location"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"trigger": {
|
||||
"type": "string",
|
||||
"description": "Condition that activates this deviation"
|
||||
},
|
||||
"phase": {
|
||||
"type": "string",
|
||||
"enum": ["morning", "afternoon", "evening", "night"]
|
||||
},
|
||||
"location": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9-]*$"
|
||||
},
|
||||
"tile": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": { "type": "integer" },
|
||||
"y": { "type": "integer" }
|
||||
},
|
||||
"required": ["x", "y"]
|
||||
},
|
||||
"activity": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "station.schema.json",
|
||||
"title": "Station Metadata",
|
||||
"description": "Station definition file — one per station directory (D-036).",
|
||||
"type": "object",
|
||||
"required": ["display_name"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"display_name": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"districts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9-]*$"
|
||||
},
|
||||
"uniqueItems": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "system.schema.json",
|
||||
"title": "Star System Metadata",
|
||||
"description": "System definition file — one per system directory (D-036).",
|
||||
"type": "object",
|
||||
"required": ["display_name"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"display_name": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"stations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9-]*$"
|
||||
},
|
||||
"uniqueItems": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "triangle.schema.json",
|
||||
"title": "Triangle Definition",
|
||||
"description": "3-NPC relationship triangle — self-contained forks, no cross-triangle cascade in v0.1 (D-024, D-047).",
|
||||
"type": "object",
|
||||
"required": ["canonical_id", "display_name", "members"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"canonical_id": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9-]*$",
|
||||
"description": "Triangle slug"
|
||||
},
|
||||
"display_name": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"members": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/member" },
|
||||
"minItems": 3,
|
||||
"maxItems": 3,
|
||||
"description": "Exactly 3 NPC members"
|
||||
},
|
||||
"forks": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/fork" },
|
||||
"description": "Possible fork points in this triangle"
|
||||
},
|
||||
"resolution_states": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/resolution" },
|
||||
"description": "Terminal states this triangle can reach"
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"member": {
|
||||
"type": "object",
|
||||
"required": ["npc", "role"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"npc": {
|
||||
"type": "string",
|
||||
"pattern": "^npc:[a-z][a-z0-9-]*$",
|
||||
"description": "NPC short-form canonical ID"
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"description": "This NPC's role within the triangle"
|
||||
}
|
||||
}
|
||||
},
|
||||
"fork": {
|
||||
"type": "object",
|
||||
"required": ["id", "condition"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9_-]*$"
|
||||
},
|
||||
"condition": {
|
||||
"type": "string",
|
||||
"description": "What triggers this fork"
|
||||
},
|
||||
"outcomes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "string" },
|
||||
"description": { "type": "string" },
|
||||
"effects": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"resolution": {
|
||||
"type": "object",
|
||||
"required": ["id", "description"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9_-]*$"
|
||||
},
|
||||
"description": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
# Main Campaign metadata (D-003)
|
||||
display_name: "The Settled Reach"
|
||||
description: >
|
||||
The core campaign. Set in a universe of wormhole-connected star systems,
|
||||
neural lattice technology, and faction-driven politics. Occlusion-based
|
||||
detective game with combat elements.
|
||||
version: "0.1.0"
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# TODO: Revise structure when automatic dialogue/monologue generators mature (D-028 Tier 2).
|
||||
# Dialogue: ring-operative at Maintenance Corridors
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# TODO: Revise structure when automatic dialogue/monologue generators mature (D-028 Tier 2).
|
||||
# Dialogue: bar-owner at The Last Shift
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# TODO: Revise structure when automatic dialogue/monologue generators mature (D-028 Tier 2).
|
||||
# Dialogue: bar-regular at The Last Shift
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# TODO: Revise structure when automatic dialogue/monologue generators mature (D-028 Tier 2).
|
||||
# Dialogue: bartender at The Last Shift
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# TODO: Revise structure when automatic dialogue/monologue generators mature (D-028 Tier 2).
|
||||
# Dialogue: courier at The Terminal
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# TODO: Revise structure when automatic dialogue/monologue generators mature (D-028 Tier 2).
|
||||
# Dialogue: dock-worker at The Terminal
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# TODO: Revise structure when automatic dialogue/monologue generators mature (D-028 Tier 2).
|
||||
# Dialogue: new-hire at The Terminal
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# TODO: Revise structure when automatic dialogue/monologue generators mature (D-028 Tier 2).
|
||||
# Dialogue: scheduler at The Terminal
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# TODO: Revise structure when automatic dialogue/monologue generators mature (D-028 Tier 2).
|
||||
# Dialogue: shift-supervisor at The Terminal
|
||||
@@ -0,0 +1,13 @@
|
||||
# Sova Transit District metadata (D-036)
|
||||
# Identity derived from directory path: campaigns/main/systems/krenn/stations/sova/districts/transit/
|
||||
# canonical_id: krenn.sova.transit (derived at load time by ContentValidator)
|
||||
display_name: "Sova Transit District"
|
||||
description: >
|
||||
A 40-year-old prefab-modular-retrofitted freight logistics hub on Station Sova.
|
||||
Three social sites: The Terminal (logistics hub), The Last Shift (bar),
|
||||
and maintenance corridors.
|
||||
locations:
|
||||
- "the-terminal"
|
||||
- "the-last-shift"
|
||||
- "maintenance-corridors"
|
||||
npc_count: 17
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# Location: Maintenance Corridors (smuggling spaces)
|
||||
# canonical_id: krenn.sova.transit.location.maintenance-corridors
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# Location: The Last Shift (bar)
|
||||
# canonical_id: krenn.sova.transit.location.the-last-shift
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# Location: The Terminal (logistics hub)
|
||||
# canonical_id: krenn.sova.transit.location.the-terminal
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# TODO: Revise structure when automatic dialogue/monologue generators mature (D-028 Tier 2).
|
||||
# Monologue: detective at general
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# TODO: Revise structure when automatic dialogue/monologue generators mature (D-028 Tier 2).
|
||||
# Monologue: detective at maintenance-corridors
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# TODO: Revise structure when automatic dialogue/monologue generators mature (D-028 Tier 2).
|
||||
# Monologue: detective at the-last-shift
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# TODO: Revise structure when automatic dialogue/monologue generators mature (D-028 Tier 2).
|
||||
# Monologue: detective at the-terminal
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# TODO: Revise structure when automatic dialogue/monologue generators mature (D-028 Tier 2).
|
||||
# Monologue: smuggler at general
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# TODO: Revise structure when automatic dialogue/monologue generators mature (D-028 Tier 2).
|
||||
# Monologue: smuggler at maintenance-corridors
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# TODO: Revise structure when automatic dialogue/monologue generators mature (D-028 Tier 2).
|
||||
# Monologue: smuggler at the-last-shift
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# TODO: Revise structure when automatic dialogue/monologue generators mature (D-028 Tier 2).
|
||||
# Monologue: smuggler at the-terminal
|
||||
@@ -0,0 +1,2 @@
|
||||
# NPC Profile: devra
|
||||
# canonical_id: krenn.sova.transit.npc.devra
|
||||
@@ -0,0 +1,2 @@
|
||||
# NPC Profile: drin
|
||||
# canonical_id: krenn.sova.transit.npc.drin
|
||||
@@ -0,0 +1,2 @@
|
||||
# NPC Profile: harek
|
||||
# canonical_id: krenn.sova.transit.npc.harek
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# NPC Profile: kael-davan
|
||||
# canonical_id: krenn.sova.transit.npc.kael-davan
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# NPC Profile: lera-sessik
|
||||
# canonical_id: krenn.sova.transit.npc.lera-sessik
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# NPC Profile: maret-korr
|
||||
# canonical_id: krenn.sova.transit.npc.maret-korr
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# NPC Profile: naia-tamm
|
||||
# canonical_id: krenn.sova.transit.npc.naia-tamm
|
||||
@@ -0,0 +1,2 @@
|
||||
# NPC Profile: olin
|
||||
# canonical_id: krenn.sova.transit.npc.olin
|
||||
@@ -0,0 +1,2 @@
|
||||
# NPC Profile: pell
|
||||
# canonical_id: krenn.sova.transit.npc.pell
|
||||
@@ -0,0 +1,2 @@
|
||||
# NPC Profile: renn
|
||||
# canonical_id: krenn.sova.transit.npc.renn
|
||||
@@ -0,0 +1,2 @@
|
||||
# NPC Profile: resha
|
||||
# canonical_id: krenn.sova.transit.npc.resha
|
||||
@@ -0,0 +1,2 @@
|
||||
# NPC Profile: sabel
|
||||
# canonical_id: krenn.sova.transit.npc.sabel
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# NPC Profile: sera-venn
|
||||
# canonical_id: krenn.sova.transit.npc.sera-venn
|
||||
@@ -0,0 +1,2 @@
|
||||
# NPC Profile: sess
|
||||
# canonical_id: krenn.sova.transit.npc.sess
|
||||
@@ -0,0 +1,2 @@
|
||||
# NPC Profile: tav
|
||||
# canonical_id: krenn.sova.transit.npc.tav
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# NPC Profile: torek-lintar
|
||||
# canonical_id: krenn.sova.transit.npc.torek-lintar
|
||||
@@ -0,0 +1,2 @@
|
||||
# NPC Profile: voss
|
||||
# canonical_id: krenn.sova.transit.npc.voss
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# NPC daily routine schedules — Sova Transit District
|
||||
# All NPC schedules in one file for cross-NPC scheduling validation
|
||||
+1
@@ -0,0 +1 @@
|
||||
# Triangle: bar-tensions
|
||||
+1
@@ -0,0 +1 @@
|
||||
# Triangle: hub-power
|
||||
+1
@@ -0,0 +1 @@
|
||||
# Triangle: informant-question
|
||||
+1
@@ -0,0 +1 @@
|
||||
# Triangle: worried-knowledge
|
||||
+1
@@ -0,0 +1 @@
|
||||
# Triangle: worried-partner
|
||||
@@ -0,0 +1,7 @@
|
||||
# Station Sova metadata (D-036)
|
||||
display_name: "Station Sova"
|
||||
description: >
|
||||
A freight logistics station in the Krenn system. 40-year-old prefab-modular
|
||||
facility serving the span gate's cargo throughput. ~12,000 population.
|
||||
districts:
|
||||
- "transit"
|
||||
@@ -0,0 +1,8 @@
|
||||
# Krenn System metadata (D-036)
|
||||
display_name: "Krenn System"
|
||||
description: >
|
||||
A mid-tier system connected via the Sova span gate. Industrial economy
|
||||
centered on freight logistics and lattice component trade. ~2.4M population,
|
||||
~180 years settled.
|
||||
stations:
|
||||
- "sova"
|
||||
@@ -0,0 +1,11 @@
|
||||
# Content manifest — The Settled Reach v0.1
|
||||
# The server reads this first to discover campaigns and content layout.
|
||||
# District discovery uses glob patterns — no per-district listing needed.
|
||||
version: "0.1.0"
|
||||
|
||||
campaigns:
|
||||
- id: "main"
|
||||
path: "campaigns/main"
|
||||
enabled: true
|
||||
discovery:
|
||||
districts: "systems/**/districts/*/district.yaml"
|
||||
@@ -0,0 +1 @@
|
||||
# Access tiers: public, insider, authority, peer, hostile
|
||||
@@ -0,0 +1 @@
|
||||
# 8 mood values
|
||||
@@ -0,0 +1 @@
|
||||
# 6 functional motivations (System B)
|
||||
@@ -0,0 +1 @@
|
||||
# 9 thematic patterns (System A)
|
||||
@@ -0,0 +1 @@
|
||||
# 13 situation values (D-035)
|
||||
@@ -0,0 +1 @@
|
||||
# 9 topic values
|
||||
@@ -0,0 +1 @@
|
||||
# 9 monologue trigger types
|
||||
@@ -0,0 +1 @@
|
||||
# Trust tiers: surface, real, secret
|
||||
@@ -0,0 +1 @@
|
||||
# Faction: concord-assembly
|
||||
@@ -0,0 +1 @@
|
||||
# Faction: guardians-of-autonomy
|
||||
@@ -0,0 +1 @@
|
||||
# Faction: lattice-commission
|
||||
@@ -0,0 +1 @@
|
||||
# Faction: syndics
|
||||
@@ -0,0 +1 @@
|
||||
# Faction: the-ring
|
||||
@@ -0,0 +1 @@
|
||||
# Faction: the-unbound
|
||||
@@ -0,0 +1 @@
|
||||
# Faction: veil-institute
|
||||
@@ -0,0 +1 @@
|
||||
# Fact catalog: contraband
|
||||
@@ -0,0 +1 @@
|
||||
# Entity attributes — 16 canonical EntityKnowledge keys (D-055)
|
||||
@@ -0,0 +1 @@
|
||||
# Fact catalog: investigation
|
||||
@@ -0,0 +1 @@
|
||||
# Fact catalog: location
|
||||
@@ -0,0 +1 @@
|
||||
# Fact catalog: progress
|
||||
@@ -0,0 +1 @@
|
||||
# Fact catalog: relationship
|
||||
@@ -0,0 +1 @@
|
||||
# Fact catalog: world
|
||||
+10
-59
@@ -63,63 +63,6 @@ impl BridgeResource {
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate ObserverSnapshot v2 from ECS state.
|
||||
/// Pre-visibility version: sends ALL entities (no LOS filtering yet).
|
||||
/// Will be replaced by perception::observer::compute_observer_snapshot in #112.
|
||||
pub fn generate_snapshot(
|
||||
time: Res<crate::simulation::time::SimulationTime>,
|
||||
entities: Query<(
|
||||
Entity,
|
||||
&crate::simulation::movement::TilePosition,
|
||||
Option<&crate::simulation::movement::PlayerCharacter>,
|
||||
Option<&crate::npc::Npc>,
|
||||
)>,
|
||||
mut buffer: ResMut<SnapshotBuffer>,
|
||||
) {
|
||||
let mut visible = Vec::new();
|
||||
for (entity, pos, is_player, is_npc) in entities.iter() {
|
||||
let (x, y, z) = pos.to_render_coords();
|
||||
let kind = if is_player.is_some() {
|
||||
EntityKind::Player
|
||||
} else if is_npc.is_some() {
|
||||
EntityKind::Npc
|
||||
} else {
|
||||
EntityKind::Object
|
||||
};
|
||||
visible.push(VisibleEntity {
|
||||
entity_id: entity.to_bits(),
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
kind,
|
||||
visibility: VisibilitySector::Forward,
|
||||
relationship: RelationshipState::Unknown,
|
||||
observation: EntityVisibility::Visible,
|
||||
});
|
||||
}
|
||||
|
||||
let game_time = GameTime {
|
||||
day: time.day(),
|
||||
time_of_day: time.time_of_day_minutes(),
|
||||
day_phase: time.day_phase(),
|
||||
paused: time.paused,
|
||||
};
|
||||
|
||||
tracing::trace!(
|
||||
"generate_snapshot: tick={}, entities={}",
|
||||
time.tick,
|
||||
visible.len()
|
||||
);
|
||||
buffer.snapshot = Some(ObserverSnapshot {
|
||||
version: 3,
|
||||
tick: time.tick,
|
||||
game_time,
|
||||
player_facing: FacingDirection::default(),
|
||||
entities: visible,
|
||||
visible_tiles: Vec::new(), // Empty until #112 adds LOS filtering
|
||||
});
|
||||
}
|
||||
|
||||
/// Receive inputs from bridge and push to InputQueue
|
||||
pub fn receive_bridge_inputs(
|
||||
bridge: Option<Res<BridgeResource>>,
|
||||
@@ -202,12 +145,20 @@ impl Plugin for BridgePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.init_resource::<SnapshotBuffer>()
|
||||
.init_resource::<ServerRunning>()
|
||||
.init_resource::<crate::perception::query::VisibilityGeometry>()
|
||||
.init_resource::<crate::perception::query::ActivePerceptionMode>()
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
receive_bridge_inputs.before(crate::simulation::input::process_player_input),
|
||||
receive_bridge_inputs
|
||||
.before(crate::simulation::input::process_player_input),
|
||||
crate::perception::observer::compute_visibility_geometry
|
||||
.after(crate::simulation::movement::validate_movement),
|
||||
crate::simulation::interaction::compute_nearby_interactions
|
||||
.after(crate::simulation::movement::validate_movement),
|
||||
crate::perception::observer::compute_observer_snapshot
|
||||
.after(crate::simulation::movement::validate_movement)
|
||||
.after(crate::perception::observer::compute_visibility_geometry)
|
||||
.after(crate::simulation::interaction::compute_nearby_interactions)
|
||||
.before(crate::simulation::time::advance_tick),
|
||||
crate::perception::observation::emit_observation_events
|
||||
.after(crate::perception::observer::compute_observer_snapshot),
|
||||
|
||||
@@ -6,18 +6,28 @@ use bevy_ecs::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub use crate::knowledge::types::{EntityVisibility, KnowledgeConfidence, RelationshipState};
|
||||
pub use crate::simulation::time::DayPhase;
|
||||
pub use crate::simulation::time::{DayPhase, TickRate};
|
||||
|
||||
/// Wire protocol version for ObserverSnapshot.
|
||||
///
|
||||
/// Versioning strategy: flat struct + serde defaults for field evolution.
|
||||
/// Client and server are co-versioned (subprocess IPC per D-020), so protocol
|
||||
/// negotiation is unnecessary. Client should reject snapshots with version !=
|
||||
/// PROTOCOL_VERSION. New fields use #[serde(default)] only during the migration
|
||||
/// period, then the default is removed once both sides are updated.
|
||||
pub const PROTOCOL_VERSION: u8 = 4;
|
||||
|
||||
/// The ONLY data structure crossing the client-server boundary (D-020)
|
||||
/// Contains all information visible to the observer at a given tick.
|
||||
///
|
||||
/// v2 adds: game_time, player_facing, visible_tiles, visibility sectors.
|
||||
/// v3 adds: relationship (D-033 entity color), observation (Visible/Remembered).
|
||||
/// v4 adds: nearby_interactions (D-060, #404 proximity + verbs[]).
|
||||
/// Future fields: ambient sound events, internal monologue triggers,
|
||||
/// HUD state (D-020 expansion).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ObserverSnapshot {
|
||||
/// Protocol version for forward compatibility. Current: 3.
|
||||
/// Protocol version for forward compatibility. Current: 4.
|
||||
pub version: u8,
|
||||
/// Simulation tick when this snapshot was produced
|
||||
pub tick: u64,
|
||||
@@ -29,6 +39,9 @@ pub struct ObserverSnapshot {
|
||||
pub entities: Vec<VisibleEntity>,
|
||||
/// Tiles visible to the observer for fog rendering
|
||||
pub visible_tiles: Vec<VisibleTile>,
|
||||
/// Entities within interaction range with available verbs (D-060, #404).
|
||||
/// Sorted by distance (nearest first). v0.1 client reads verbs[0] on the nearest entity.
|
||||
pub nearby_interactions: Vec<NearbyInteraction>,
|
||||
}
|
||||
|
||||
/// Game time data for client display (D-031)
|
||||
@@ -41,8 +54,8 @@ pub struct GameTime {
|
||||
pub time_of_day: u64,
|
||||
/// Current day phase (Morning/Afternoon/Evening/Night)
|
||||
pub day_phase: DayPhase,
|
||||
/// Whether simulation is paused
|
||||
pub paused: bool,
|
||||
/// Current tick rate state (D-052). Client derives paused from TickRate::Paused.
|
||||
pub tick_rate: TickRate,
|
||||
}
|
||||
|
||||
/// 8-directional facing direction, matching movement system.
|
||||
@@ -136,6 +149,45 @@ pub enum PlayerAction {
|
||||
UsePerceptionMode(String),
|
||||
Pause,
|
||||
Unpause,
|
||||
/// Set tick rate: Full (1.0), Half (0.5), or Paused (0.0) per D-052
|
||||
SetTickRate(TickRate),
|
||||
}
|
||||
|
||||
/// Available interaction verbs for a nearby entity (D-060, #404)
|
||||
/// Embedded in ObserverSnapshot.nearby_interactions.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NearbyInteraction {
|
||||
/// Wire-format entity identifier
|
||||
pub entity_id: u64,
|
||||
/// Entity type for client-side verb display
|
||||
pub entity_type: EntityKind,
|
||||
/// Manhattan distance from player (integer tiles)
|
||||
pub distance: u32,
|
||||
/// Available verbs sorted by priority (index 0 = highest priority)
|
||||
pub verbs: Vec<VerbOption>,
|
||||
}
|
||||
|
||||
/// A single available verb on a nearby entity
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct VerbOption {
|
||||
/// Verb type
|
||||
pub kind: VerbKind,
|
||||
/// Display label for the context prompt (e.g. "Talk", "Observe", "Examine")
|
||||
pub label: String,
|
||||
/// Priority rank (lower = higher priority). v0.1 client reads only priority 1.
|
||||
pub priority: u8,
|
||||
/// Whether this verb is currently available (false = greyed out in v0.2)
|
||||
pub available: bool,
|
||||
}
|
||||
|
||||
/// Verb types for the interaction system (D-060)
|
||||
/// Only active verbs appear in verbs[]. Passive (Look, Overhear) and
|
||||
/// reactive (Monologue) verbs fire independently.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum VerbKind {
|
||||
ExamineObject,
|
||||
ExamineNpc,
|
||||
Talk,
|
||||
}
|
||||
|
||||
/// Snapshot buffer resource for staging outgoing ObserverSnapshots
|
||||
|
||||
@@ -79,14 +79,16 @@ pub fn process_knowledge_events(
|
||||
if let Some(stable_id) = registry.to_stable(target) {
|
||||
observer_kg.observe_entity(stable_id, position, event.tick);
|
||||
} else {
|
||||
tracing::warn!("DirectObservation target {:?} not in EntityRegistry", target);
|
||||
debug_assert!(false, "DirectObservation target {:?} not in EntityRegistry", target);
|
||||
tracing::error!("DirectObservation target {:?} not in EntityRegistry", target);
|
||||
}
|
||||
}
|
||||
KnowledgeEventType::LeftLOS { target } => {
|
||||
if let Some(stable_id) = registry.to_stable(target) {
|
||||
observer_kg.observe_entity_leaving_los(&stable_id, event.tick);
|
||||
} else {
|
||||
tracing::warn!("LeftLOS target {:?} not in EntityRegistry", target);
|
||||
debug_assert!(false, "LeftLOS target {:?} not in EntityRegistry", target);
|
||||
tracing::error!("LeftLOS target {:?} not in EntityRegistry", target);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -260,7 +262,7 @@ mod tests {
|
||||
world.insert_resource(thresholds);
|
||||
|
||||
// Tick 7: not a multiple of 10, decay should NOT run
|
||||
world.insert_resource(SimulationTime { tick: 7, paused: false });
|
||||
world.insert_resource({ let mut t = SimulationTime::default(); t.tick = 7; t });
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(decay_knowledge);
|
||||
schedule.run(&mut world);
|
||||
@@ -273,7 +275,7 @@ mod tests {
|
||||
);
|
||||
|
||||
// Tick 10: multiple of 10, decay SHOULD run (age = 10 > decay_after = 5)
|
||||
world.insert_resource(SimulationTime { tick: 10, paused: false });
|
||||
world.insert_resource({ let mut t = SimulationTime::default(); t.tick = 10; t });
|
||||
let mut schedule2 = bevy_ecs::schedule::Schedule::default();
|
||||
schedule2.add_systems(decay_knowledge);
|
||||
schedule2.run(&mut world);
|
||||
|
||||
@@ -14,6 +14,7 @@ use settled_reach_server::npc::{
|
||||
Want, WantKind,
|
||||
};
|
||||
use settled_reach_server::perception::vision_cone::Facing;
|
||||
use settled_reach_server::simulation::interaction::NearbyInteractionBuffer;
|
||||
use settled_reach_server::simulation::movement::{PlayerCharacter, TilePosition, WalkabilityMap};
|
||||
use settled_reach_server::simulation::path_follow::MovementSpeed;
|
||||
use settled_reach_server::simulation::time::DayPhase;
|
||||
@@ -68,6 +69,7 @@ fn main() {
|
||||
TilePosition::new(16, 16, 0),
|
||||
Facing::default(),
|
||||
KnowledgeGraph::new(),
|
||||
NearbyInteractionBuffer::default(),
|
||||
))
|
||||
.id();
|
||||
registry.register(player);
|
||||
|
||||
@@ -110,39 +110,39 @@ pub fn generate_observation_events(
|
||||
continue;
|
||||
}
|
||||
|
||||
let entity = Entity::from_bits(visible.entity_id);
|
||||
// Convert wire StableId back to bevy Entity via registry
|
||||
let stable_id = StableId(visible.entity_id);
|
||||
let Some(entity) = registry.to_entity(&stable_id) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Check if this is a new entity (not in observer's knowledge graph)
|
||||
if let Some(stable_id) = registry.to_stable(entity) {
|
||||
if !observer_kg.knows_entity(&stable_id) {
|
||||
// Reconstruct tile position from render coords
|
||||
let tile_pos = TilePosition::from_render_coords(visible.x, visible.y, visible.z);
|
||||
event_queue.push(ObservationEvent {
|
||||
tick: time.tick,
|
||||
trigger: ObservationTrigger::NewEntity {
|
||||
entity: stable_id,
|
||||
location: tile_pos,
|
||||
},
|
||||
observer: observer_entity,
|
||||
});
|
||||
}
|
||||
if !observer_kg.knows_entity(&stable_id) {
|
||||
// Reconstruct tile position from render coords
|
||||
let tile_pos = TilePosition::from_render_coords(visible.x, visible.y, visible.z);
|
||||
event_queue.push(ObservationEvent {
|
||||
tick: time.tick,
|
||||
trigger: ObservationTrigger::NewEntity {
|
||||
entity: stable_id,
|
||||
location: tile_pos,
|
||||
},
|
||||
observer: observer_entity,
|
||||
});
|
||||
}
|
||||
|
||||
// Check routine deviation: visible NPC not at expected location
|
||||
if let Ok((actual_pos, routine)) = npc_query.get(entity) {
|
||||
if let Some(expected_pos) = routine.expected_location(current_phase) {
|
||||
if *actual_pos != expected_pos {
|
||||
if let Some(stable_id) = registry.to_stable(entity) {
|
||||
event_queue.push(ObservationEvent {
|
||||
tick: time.tick,
|
||||
trigger: ObservationTrigger::RoutineDeviation {
|
||||
npc: stable_id,
|
||||
expected: expected_pos,
|
||||
actual: *actual_pos,
|
||||
},
|
||||
observer: observer_entity,
|
||||
});
|
||||
}
|
||||
event_queue.push(ObservationEvent {
|
||||
tick: time.tick,
|
||||
trigger: ObservationTrigger::RoutineDeviation {
|
||||
npc: stable_id,
|
||||
expected: expected_pos,
|
||||
actual: *actual_pos,
|
||||
},
|
||||
observer: observer_entity,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -157,8 +157,8 @@ pub fn generate_observation_events(
|
||||
continue;
|
||||
};
|
||||
|
||||
// Skip if currently visible
|
||||
if visible_npc_bits.contains(&entity.to_bits()) {
|
||||
// Skip if currently visible (visible_npc_bits contains wire StableId values)
|
||||
if visible_npc_bits.contains(&stable_id.0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -186,7 +186,8 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::knowledge::registry::EntityRegistry;
|
||||
use crate::npc::RoutineEntry;
|
||||
use crate::perception::observer::compute_observer_snapshot;
|
||||
use crate::perception::observer::{compute_observer_snapshot, compute_visibility_geometry};
|
||||
use crate::perception::query::{ActivePerceptionMode, VisibilityGeometry};
|
||||
use crate::perception::vision_cone::Facing;
|
||||
use crate::simulation::movement::WalkabilityMap;
|
||||
use crate::simulation::time::{DayPhase, MINUTES_PER_PHASE, TICKS_PER_GAME_MINUTE};
|
||||
@@ -199,16 +200,18 @@ mod tests {
|
||||
world.init_resource::<crate::knowledge::KnowledgeEventQueue>();
|
||||
world.init_resource::<EntityRegistry>();
|
||||
world.init_resource::<ObservationEventQueue>();
|
||||
world.init_resource::<VisibilityGeometry>();
|
||||
world.init_resource::<ActivePerceptionMode>();
|
||||
world
|
||||
}
|
||||
|
||||
/// Run the observation pipeline: snapshot -> emit -> interpret -> knowledge update.
|
||||
/// Interpretation runs BEFORE knowledge updates so it can detect new entities
|
||||
/// and compare against the PREVIOUS tick's knowledge state.
|
||||
/// Run the observation pipeline: geometry -> snapshot -> emit -> interpret -> knowledge.
|
||||
fn run_pipeline(world: &mut World) {
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems((
|
||||
compute_observer_snapshot,
|
||||
compute_visibility_geometry,
|
||||
compute_observer_snapshot
|
||||
.after(compute_visibility_geometry),
|
||||
crate::perception::observation::emit_observation_events
|
||||
.after(compute_observer_snapshot),
|
||||
generate_observation_events
|
||||
@@ -234,6 +237,7 @@ mod tests {
|
||||
TilePosition::new(16, 16, 0),
|
||||
Facing(FacingDirection::North),
|
||||
KnowledgeGraph::new(),
|
||||
crate::simulation::interaction::NearbyInteractionBuffer::default(),
|
||||
))
|
||||
.id();
|
||||
registry.register(player);
|
||||
@@ -284,6 +288,7 @@ mod tests {
|
||||
TilePosition::new(16, 16, 0),
|
||||
Facing(FacingDirection::North),
|
||||
KnowledgeGraph::new(),
|
||||
crate::simulation::interaction::NearbyInteractionBuffer::default(),
|
||||
))
|
||||
.id();
|
||||
registry.register(player);
|
||||
@@ -354,6 +359,7 @@ mod tests {
|
||||
TilePosition::new(16, 16, 0),
|
||||
Facing(FacingDirection::North),
|
||||
kg,
|
||||
crate::simulation::interaction::NearbyInteractionBuffer::default(),
|
||||
))
|
||||
.id();
|
||||
registry.register(player);
|
||||
@@ -386,6 +392,7 @@ mod tests {
|
||||
TilePosition::new(16, 16, 0),
|
||||
Facing(FacingDirection::North),
|
||||
KnowledgeGraph::new(), // Empty — never seen anyone
|
||||
crate::simulation::interaction::NearbyInteractionBuffer::default(),
|
||||
))
|
||||
.id();
|
||||
registry.register(player);
|
||||
@@ -432,6 +439,7 @@ mod tests {
|
||||
TilePosition::new(16, 16, 0),
|
||||
Facing(FacingDirection::North),
|
||||
kg,
|
||||
crate::simulation::interaction::NearbyInteractionBuffer::default(),
|
||||
))
|
||||
.id();
|
||||
registry.register(player);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user