Files
security-game/src/core/venue_scenario.gd
T
zwitschi a2a0ded527
Full build / Import + headless tests (push) Successful in 35s
Full build / Full desktop build + optional release (push) Successful in 3m26s
Author the missing scenario content from the PLAN lists
Joke Shop
- Reservation list without ID (guest list valid, ID missing) — denied.
- Borrowed/mismatched ID (ID valid but mismatched) — denied.
- Guest who refuses the wand screen: new `refuses_screening` event flag, copied
  onto NPCController, handled in _toggle_wand_screening (logs wand.refused, records
  a wand_refused event, small prop hit, panel stays closed).
- Late arrival during the performance (ID + ticket, "arrive" intent).

Studio
- Filming-boundary event and a later repeat, both using the new `film_boundary`
  intent which maps to `steps_end` so the influencer stands past the posted line.
- Celebrity arrival now pulls in a small crowd via a new `extra_spawns` event field
  (two tourists + an influencer), spawned through the normal path.

Building
- Filming-boundary event whose hint points at the body camera and request-reason
  verbs, plus an unclear-intent event that hints at asking for a reason.

Engine support
- Events may carry `hint` (string key logged on spawn), `extra_spawns`, and
  `refuses_screening`; 9 new localized hint strings.

Tests/docs
- Headless suite 139 -> 149 (test_scenario_content.gd) and a new
  smoke_scenario_content.gd that spawns the refusal and celebrity events through the
  real signal path.
- README, TODO section moved to DONE, MEMORY. Version 0.2.13.
2026-10-31 21:55:24 +01:00

96 lines
3.5 KiB
GDScript

class_name VenueScenario
extends Resource
const Rules := preload("res://src/core/rules.gd")
const AccessPolicy := preload("res://src/core/access_policy.gd")
const NpcProfile := preload("res://src/core/npc_profile.gd")
## Data-driven venue definition (ADR-0001). Provides locations, rules,
## NPC pools, event pools, equipment, and shift phases.
@export var id: String = ""
@export var display_name: String = ""
@export_multiline var description: String = ""
@export var access_policy: AccessPolicy = AccessPolicy.new()
## Equipment available at this venue.
@export var equipment: Array[String] = [] # e.g. "wristband_policy", "id_check", "wand", "flashlight"
## Named spots: "guard_post", "entrance", "steps", "sidewalk", "queue_area".
@export var locations: Dictionary = {}
## NPC archetype definitions that appear in this scenario.
@export var npc_profiles: Array[NpcProfile] = []
## Event pool. Each event: { id, phase: Rules.Phase, kind: "routine"|"scripted",
## npc_type: int, intent: String, weight: float, probability: float,
## time_range_s: Vector2, requires_emergency: bool }
@export var event_pool: Array[Dictionary] = []
## Shift setup: phases map Rules.Phase -> { start_s, end_s, spawn_rate }.
@export var shift_phases: Dictionary = {}
## Default duration of one shift in seconds (PLAN.md: 10-15 minutes).
@export var shift_duration_s: float = 900.0
@export var policy_summary: String = ""
## Door-staff (guard) dialogue lines specific to this venue. Keys are intent
## or situation names; values are Arrays of line strings.
@export var door_staff_lines: Dictionary = {}
func profile_for(npc_type: int) -> NpcProfile:
for p in npc_profiles:
if p.npc_type == npc_type:
return p
return null
func events_in_phase(phase: int) -> Array[Dictionary]:
var result: Array[Dictionary] = []
for event in event_pool:
if event.get("phase", -1) == phase:
result.append(event)
return result
## Map an NPC intent to a named venue location (Vector3). Falls back to the
## entrance when the intent or its location is unknown. Pure and headless-safe.
func waypoint_for_intent(intent: String) -> Vector3:
return _location_or_default(_spot_for_intent(intent), Vector3(0, 0, 1.2))
func _spot_for_intent(intent: String) -> String:
match intent:
"enter", "arrive", "deliver", "trespass", "force_entry", "follow_authorized", "counterfeit_credential", "blend_in", "crisis", "threat":
return "entrance"
"photo", "film", "perform", "wander", "roam":
return "sidewalk"
"sit", "film_boundary":
# film_boundary: an influencer ignoring the posted filming line, so they
# stand at the steps instead of the permitted sidewalk spot.
return "steps_end"
_:
return "sidewalk"
## Pick a random door-staff line for a context key (e.g. "id_check",
## "ticket_check", "wand_check", "refusal", "prohibited_item"). Returns ""
## when the venue has no lines for that context. Deterministic when a seeded
## RNG is injected; otherwise returns the first line.
func door_staff_line(context: String, rng: RandomNumberGenerator = null) -> String:
if not door_staff_lines.has(context):
return ""
var lines: Variant = door_staff_lines[context]
if not (lines is Array):
return ""
var pool: Array = lines
if pool.is_empty():
return ""
if rng != null:
return str(pool[rng.randi_range(0, pool.size() - 1)])
return str(pool[0])
func _location_or_default(spot: String, fallback: Vector3) -> Vector3:
if locations.has(spot):
var v: Variant = locations[spot]
if v is Vector3:
return v
return fallback