Add the campaign shell, pause-safe checkpoint resume, post-shift campaign continue, confirmed campaign reset, difficulty presets, searchable Help, content-matrix validation, recovery events, contextual dialogue, policy/debrief context and eligible-event previews. Extend persistence with atomic separate files and add campaign/checkpoint/tutorial smokes. Headless 247/247, Python 94/94, tutorial/campaign/session/scenario smokes green.
89 lines
2.9 KiB
GDScript
89 lines
2.9 KiB
GDScript
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, {})
|