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.
96 lines
3.5 KiB
GDScript
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
|