class_name NpcProfile extends Resource const Rules := preload("res://src/core/rules.gd") ## Authorable behavior profile for one kind of NPC (PLAN.md: NPC Behavior Types). @export var id: String = "" @export var display_name: String = "" @export var npc_type: int = Rules.NpcType.TOURIST ## How difficult temperament is (0 = very calm, 1 = very hot-headed). ## Affects how likely the NPC is to argue, point, or threaten at each level. @export_range(0.0, 1.0) var temperament: float = 0.25 ## Probability the NPC obeys a level-1 polite direction. @export_range(0.0, 1.0) var base_compliance: float = 0.8 ## Probability that an NPC directed away will circle back later in the shift ## (returning-trespasser behavior). 0 = never returns. Trespassers and people ## in crisis are more likely to return than tourists. @export_range(0.0, 1.0) var return_probability: float = 0.0 ## Probability weighting of the NP C's reaction per player escalation level. ## keys: escalation level int -> { ReactionEnum: weight } ## Missing entries fall back to the profile's `default_reactions`. @export var reactions_by_level: Dictionary = {} @export var default_reactions: Dictionary = { Rules.Level.SIMPLE_DETERRENCE: { Rules.Reaction.COMPLY: 80, Rules.Reaction.IGNORE: 12, Rules.Reaction.ARGUE: 8, }, Rules.Level.FORMAL_WARNING: { Rules.Reaction.COMPLY: 70, Rules.Reaction.LEAVE: 10, Rules.Reaction.ARGUE: 12, Rules.Reaction.RECORD: 8, }, Rules.Level.BLOCKING: { Rules.Reaction.COMPLY: 55, Rules.Reaction.LEAVE: 15, Rules.Reaction.ARGUE: 20, Rules.Reaction.RECORD: 10, }, Rules.Level.RADIO_ASSIST: { Rules.Reaction.LEAVE: 65, Rules.Reaction.ARGUE: 15, Rules.Reaction.FORCE_ENTRY: 20, }, Rules.Level.EMERGENCY: { Rules.Reaction.FORCE_ENTRY: 30, Rules.Reaction.THREATEN: 40, Rules.Reaction.LEAVE: 30, }, } ## Delay the NPC waits before reacting to the player (frames/seconds scaled). @export_range(0.1, 5.0) var reaction_delay: float = 0.6 ## Dialogue lines this NPC uses. Falls back to generic lines when empty. @export var dialogue_lines: Array[String] = [] ## Context-specific dialogue: keys such as "reason", "refusal", "deescalation", ## "recognition" and "group". Missing contexts fall back to `dialogue_lines`. @export var context_lines: Dictionary = {} ## Items the NPC is carrying, used by the handheld wand (The Joke Shop). ## Values are item names: "keys", "belt", "phone", "weapon", "knife", etc. @export var carried_items: Array[String] = [] func line_for(context: String, rng: RandomNumberGenerator = null) -> String: var pool: Array = [] if context_lines.has(context): pool = context_lines[context] elif context == "reason": pool = dialogue_lines if pool.is_empty(): return "" var index := 0 if rng != null: index = rng.randi_range(0, pool.size() - 1) return str(pool[index]) func pickup_reactions_per_level(level: int) -> Dictionary: if reactions_by_level.has(level): return reactions_by_level[level] return default_reactions.get(level, {})