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