Remove dialogue tone-of-voice system, add Gitea Actions full build, add venue cycling
- Delete Rules.Tone/TONE_NAMES, NpcProfile.tone_modifiers, tone params on NPCController.roll_reaction/apply_player_action; dialogue_lines flattened to Array[String]; HUD tone label, T binding, and hud.tone_prefix removed - Docs updated everywhere (PLAN, README, DONE, MEMORY); PLAN section heading 'Tone and Design Direction' -> 'Design Direction' - Add .gitea/workflows/build.yml: checks job (import+tests) on push/PR; full build job (export templates, Linux/Windows/macOS exports, artifact upload, and tag-based Gitea release publish via GITEA_TOKEN) on tags/dispatch - Remove old .github/workflows/ci.yml to avoid double runs - Add venue cycling (V) across all three scenarios with hud.venue_switched - Refresh README screenshots to match the new HUD - 53/53 tests pass, boot smoke clean
This commit is contained in:
1 parent
f7486d7539
commit
a3768c2ce4
21 files changed
+230
-185
No files matched your search
@@ -21,13 +21,11 @@ static func shared_profiles() -> Array[NpcProfile]:
|
||||
tourist.npc_type = Rules.NpcType.TOURIST
|
||||
tourist.temperament = 0.1
|
||||
tourist.base_compliance = 0.9
|
||||
tourist.dialogue_lines = {
|
||||
Rules.Tone.FRIENDLY: [
|
||||
"Oh sure, sorry, just taking a picture.",
|
||||
"Is there a better spot to stand?",
|
||||
],
|
||||
Rules.Tone.PROFESSIONAL: ["Understood, moving."],
|
||||
}
|
||||
tourist.dialogue_lines = [
|
||||
"Oh sure, sorry, just taking a picture.",
|
||||
"Is there a better spot to stand?",
|
||||
"Understood, moving.",
|
||||
]
|
||||
profiles.append(tourist)
|
||||
|
||||
var confused := NpcProfile.new()
|
||||
@@ -36,12 +34,10 @@ static func shared_profiles() -> Array[NpcProfile]:
|
||||
confused.npc_type = Rules.NpcType.CONFUSED_VISITOR
|
||||
confused.temperament = 0.2
|
||||
confused.base_compliance = 0.85
|
||||
confused.dialogue_lines = {
|
||||
Rules.Tone.FRIENDLY: [
|
||||
"Wait, this isn't the entrance?",
|
||||
"Sorry about that, I'm a bit lost.",
|
||||
],
|
||||
}
|
||||
confused.dialogue_lines = [
|
||||
"Wait, this isn't the entrance?",
|
||||
"Sorry about that, I'm a bit lost.",
|
||||
]
|
||||
profiles.append(confused)
|
||||
|
||||
var delivery := NpcProfile.new()
|
||||
@@ -50,9 +46,7 @@ static func shared_profiles() -> Array[NpcProfile]:
|
||||
delivery.npc_type = Rules.NpcType.DELIVERY
|
||||
delivery.temperament = 0.25
|
||||
delivery.base_compliance = 0.8
|
||||
delivery.dialogue_lines = {
|
||||
Rules.Tone.PROFESSIONAL: ["Where should I leave this?"],
|
||||
}
|
||||
delivery.dialogue_lines = ["Where should I leave this?"]
|
||||
profiles.append(delivery)
|
||||
|
||||
var performer := NpcProfile.new()
|
||||
@@ -97,11 +91,9 @@ static func shared_profiles() -> Array[NpcProfile]:
|
||||
Rules.Reaction.COMPLY: 10,
|
||||
},
|
||||
}
|
||||
influencer.dialogue_lines = {
|
||||
Rules.Tone.SUSPICIOUS: [
|
||||
"This is public sidewalk, you can't stop me from filming.",
|
||||
],
|
||||
}
|
||||
influencer.dialogue_lines = [
|
||||
"This is public sidewalk, you can't stop me from filming.",
|
||||
]
|
||||
profiles.append(influencer)
|
||||
|
||||
# --- Higher-risk crowd ------------------------------------------------------
|
||||
@@ -162,12 +154,10 @@ static func shared_profiles() -> Array[NpcProfile]:
|
||||
Rules.Reaction.LEAVE: 30,
|
||||
},
|
||||
}
|
||||
trespasser.dialogue_lines = {
|
||||
Rules.Tone.SUSPICIOUS: [
|
||||
"I'm just here to see a friend, come on.",
|
||||
"I'll be inside for ten seconds.",
|
||||
],
|
||||
}
|
||||
trespasser.dialogue_lines = [
|
||||
"I'm just here to see a friend, come on.",
|
||||
"I'll be inside for ten seconds.",
|
||||
]
|
||||
profiles.append(trespasser)
|
||||
|
||||
var mental_crisis := NpcProfile.new()
|
||||
@@ -278,12 +268,10 @@ static func shared_profiles() -> Array[NpcProfile]:
|
||||
Rules.Reaction.FORCE_ENTRY: 20,
|
||||
},
|
||||
}
|
||||
protester.dialogue_lines = {
|
||||
Rules.Tone.SUSPICIOUS: [
|
||||
"I have every right to be on public sidewalk.",
|
||||
"You can't touch me, this is a lawful protest.",
|
||||
],
|
||||
}
|
||||
protester.dialogue_lines = [
|
||||
"I have every right to be on public sidewalk.",
|
||||
"You can't touch me, this is a lawful protest.",
|
||||
]
|
||||
profiles.append(protester)
|
||||
|
||||
# --- Authorized crowd ---------------------------------------------------------
|
||||
|
||||
@@ -8,7 +8,7 @@ const FSM := preload("res://src/core/fsm.gd")
|
||||
## reaction logic. The presentation layer adds a visual avatar and navigation.
|
||||
## RefCounted on purpose: headless tests and CI do not need a scene tree.
|
||||
|
||||
signal interacted(level: int, tone: int)
|
||||
signal interacted(level: int)
|
||||
|
||||
var npc_id: String = ""
|
||||
var profile: NpcProfile = null
|
||||
@@ -61,15 +61,10 @@ func _build_fsm(initial_state: String) -> void:
|
||||
fsm.add_transition("THREATENING", "FORCING_ENTRY")
|
||||
fsm.start(initial_state)
|
||||
|
||||
func roll_reaction(level: int, tone: int) -> int:
|
||||
## Weighted pick from profile's reaction table for `level`, adjusted for tone.
|
||||
func roll_reaction(level: int) -> int:
|
||||
## Weighted pick from profile's reaction table for `level`.
|
||||
highest_escalation_received = maxi(highest_escalation_received, level)
|
||||
var weights: Dictionary = profile.pickup_reactions_per_level(level).duplicate()
|
||||
var tone_mod: float = profile.tone_modifiers.get(tone, 0.0)
|
||||
if tone_mod > 0.0 and weights.has(Rules.Reaction.COMPLY):
|
||||
# Shift weight mass toward compliance proportionally.
|
||||
var transferable: float = float(weights[Rules.Reaction.COMPLY]) * tone_mod
|
||||
weights[Rules.Reaction.COMPLY] += transferable * 0.5
|
||||
var roll_value := rng.randf()
|
||||
var total := 0.0
|
||||
for key in weights.keys():
|
||||
@@ -83,16 +78,15 @@ func roll_reaction(level: int, tone: int) -> int:
|
||||
return key
|
||||
return Rules.Reaction.LEAVE
|
||||
|
||||
func apply_player_action(level: int, tone: int) -> Dictionary:
|
||||
func apply_player_action(level: int) -> Dictionary:
|
||||
## Returns reaction to the player's action and updates FSM when needed.
|
||||
var performed_reaction := roll_reaction(level, tone)
|
||||
var performed_reaction := roll_reaction(level)
|
||||
reaction = performed_reaction
|
||||
reaction_set = true
|
||||
_update_state_for_reaction(performed_reaction)
|
||||
return {
|
||||
"npc_id": npc_id,
|
||||
"level_used": level,
|
||||
"tone": tone,
|
||||
"reaction": performed_reaction,
|
||||
"reaction_name": Rules.Reaction.find_key(performed_reaction),
|
||||
"new_state": fsm.current_state,
|
||||
|
||||
+3
-11
@@ -12,8 +12,7 @@ const Rules := preload("res://src/core/rules.gd")
|
||||
## 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
|
||||
|
||||
## Base compliance: probability the NPC obeys a level-1 polite direction.
|
||||
## Venue/scenario recalculates effective compliance from profile + tone + level.
|
||||
## 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
|
||||
@@ -56,18 +55,11 @@ const Rules := preload("res://src/core/rules.gd")
|
||||
},
|
||||
}
|
||||
|
||||
@export var tone_modifiers: Dictionary = {
|
||||
Rules.Tone.FRIENDLY: 0.10, # + compliance
|
||||
Rules.Tone.PROFESSIONAL: 0.00,
|
||||
Rules.Tone.FIRM: -0.05,
|
||||
Rules.Tone.SUSPICIOUS: -0.15,
|
||||
}
|
||||
|
||||
## 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 per tone and intent. Falls back to generic lines when empty.
|
||||
@export var dialogue_lines: Dictionary = {}
|
||||
## Dialogue lines this NPC uses. Falls back to generic lines when empty.
|
||||
@export var dialogue_lines: Array[String] = []
|
||||
|
||||
## Items the NPC is carrying, used by the handheld wand (The Joke Shop).
|
||||
## Values are item names: "keys", "belt", "phone", "weapon", "knife", etc.
|
||||
|
||||
@@ -108,21 +108,6 @@ enum Reaction {
|
||||
CALL_SOMEONE = 8,
|
||||
}
|
||||
|
||||
## Player dialogue tones.
|
||||
enum Tone {
|
||||
PROFESSIONAL = 0,
|
||||
FRIENDLY = 1,
|
||||
FIRM = 2,
|
||||
SUSPICIOUS = 3,
|
||||
}
|
||||
|
||||
const TONE_NAMES := {
|
||||
Tone.PROFESSIONAL: "Professional",
|
||||
Tone.FRIENDLY: "Friendly",
|
||||
Tone.FIRM: "Firm",
|
||||
Tone.SUSPICIOUS: "Suspicious",
|
||||
}
|
||||
|
||||
## Shift phases by time of day.
|
||||
enum Phase {
|
||||
EARLY = 0,
|
||||
|
||||
+21
-21
@@ -46,9 +46,9 @@ var current_incident_npc_id := ""
|
||||
var current_incident_active := false
|
||||
var current_npc_truth := {"weapons_seen": false, "danger_level": 0}
|
||||
|
||||
## Dialogue tone picker (PLAN.md: Level 1 tones). Cycled with T.
|
||||
var selected_tone: int = Rules.Tone.PROFESSIONAL
|
||||
const TONE_ORDER := [Rules.Tone.PROFESSIONAL, Rules.Tone.FRIENDLY, Rules.Tone.FIRM, Rules.Tone.SUSPICIOUS]
|
||||
## Venue cycling (V): next venue in the scenario list.
|
||||
const VENUE_ORDER := ["hollywood_building", "joke_shop", "tv_studio"]
|
||||
var _venue_index := 0
|
||||
|
||||
## Aim-free targeting: cycle through nearby NPCs with Tab (accessibility).
|
||||
const NEARBY_RADIUS := 12.0
|
||||
@@ -266,12 +266,10 @@ func _handle_key(keycode: Key) -> void:
|
||||
_toggle_inspection()
|
||||
KEY_Q:
|
||||
if targeted_npc != null and not inspection_open:
|
||||
_apply_interaction(Rules.Level.SIMPLE_DETERRENCE, selected_tone, "polite direction")
|
||||
_apply_interaction(Rules.Level.SIMPLE_DETERRENCE, "polite direction")
|
||||
KEY_W:
|
||||
if targeted_npc != null and not inspection_open:
|
||||
_toggle_wand_screening()
|
||||
KEY_T:
|
||||
_cycle_tone()
|
||||
KEY_TAB:
|
||||
_cycle_nearby_target()
|
||||
KEY_F:
|
||||
@@ -280,21 +278,23 @@ func _handle_key(keycode: Key) -> void:
|
||||
_toggle_flashlight()
|
||||
KEY_L:
|
||||
_toggle_door_lock()
|
||||
KEY_V:
|
||||
_cycle_venue()
|
||||
_:
|
||||
_handle_action_key(keycode)
|
||||
|
||||
func _handle_action_key(keycode: Key) -> void:
|
||||
match keycode:
|
||||
KEY_1:
|
||||
_apply_interaction(Rules.Level.SIMPLE_DETERRENCE, selected_tone, "raise hand")
|
||||
_apply_interaction(Rules.Level.SIMPLE_DETERRENCE, "raise hand")
|
||||
KEY_2:
|
||||
_apply_interaction(Rules.Level.SIMPLE_DETERRENCE, selected_tone, "point")
|
||||
_apply_interaction(Rules.Level.SIMPLE_DETERRENCE, "point")
|
||||
KEY_3:
|
||||
_apply_interaction(Rules.Level.SIMPLE_DETERRENCE, selected_tone, "head shake")
|
||||
_apply_interaction(Rules.Level.SIMPLE_DETERRENCE, "head shake")
|
||||
KEY_4:
|
||||
_apply_interaction(Rules.Level.FORMAL_WARNING, selected_tone, "formal warning")
|
||||
_apply_interaction(Rules.Level.FORMAL_WARNING, "formal warning")
|
||||
KEY_5:
|
||||
_apply_interaction(Rules.Level.BLOCKING, selected_tone, "blocking")
|
||||
_apply_interaction(Rules.Level.BLOCKING, "blocking")
|
||||
KEY_6:
|
||||
_open_radio(false)
|
||||
KEY_R:
|
||||
@@ -331,13 +331,14 @@ func _toggle_door_lock() -> void:
|
||||
hud.set_door(door.locked)
|
||||
hud.log(Strings.lookup("door.locked") if door.locked else Strings.lookup("door.unlocked"))
|
||||
|
||||
## Cycle the dialogue tone (Professional → Friendly → Firm → Suspicious).
|
||||
func _cycle_tone() -> void:
|
||||
var idx := TONE_ORDER.find(selected_tone)
|
||||
idx = (idx + 1) % TONE_ORDER.size()
|
||||
selected_tone = TONE_ORDER[idx]
|
||||
hud.set_tone(Rules.TONE_NAMES.get(selected_tone, "?"))
|
||||
hud.log("Tone: %s" % Rules.TONE_NAMES.get(selected_tone, "?"))
|
||||
## Cycle to the next venue (Hollywood Blvd → The Joke Shop → TV Studio).
|
||||
## The coordinator is rebuilt with the new scenario and the shift restarts.
|
||||
func _cycle_venue() -> void:
|
||||
_venue_index = (_venue_index + 1) % VENUE_ORDER.size()
|
||||
switch_scenario(VENUE_ORDER[_venue_index])
|
||||
var venue_name: String = selected_scenario.display_name if selected_scenario else "?"
|
||||
hud.log(Strings.fmt("hud.venue_switched", [venue_name]))
|
||||
hud.set_shift_state(coordinator, selected_scenario)
|
||||
|
||||
## Aim-free targeting: cycle through NPCs near the guard (Tab). Lets players
|
||||
## interact without precise aiming (accessibility).
|
||||
@@ -359,20 +360,19 @@ func _cycle_nearby_target() -> void:
|
||||
_begin_incident(next_target)
|
||||
hud.log(Strings.fmt("hud.targeting", [next_target.name]))
|
||||
|
||||
func _apply_interaction(level: int, tone: int, action_name: String) -> void:
|
||||
func _apply_interaction(level: int, action_name: String) -> void:
|
||||
if targeted_npc == null:
|
||||
hud.log(Strings.lookup("hud.aim_first"))
|
||||
return
|
||||
var controller: Object = _targeted_controller()
|
||||
if controller == null or coordinator == null:
|
||||
return
|
||||
var outcome: Dictionary = controller.apply_player_action(level, tone)
|
||||
var outcome: Dictionary = controller.apply_player_action(level)
|
||||
var npc_id: String = controller.npc_id
|
||||
coordinator.recorder.add_event(npc_id, {
|
||||
"type": "player_action",
|
||||
"action": action_name,
|
||||
"level": level,
|
||||
"tone": Rules.TONE_NAMES.get(tone, "?"),
|
||||
})
|
||||
coordinator.recorder.add_escalation(npc_id, level, "%s on %s" % [action_name, npc_id])
|
||||
if level >= Rules.Level.FORMAL_WARNING:
|
||||
|
||||
+1
-10
@@ -20,7 +20,6 @@ var mode_label: Label
|
||||
var wand_panel: PanelContainer
|
||||
var wand_text: Label
|
||||
var wand_meta: Label
|
||||
var tone_label: Label
|
||||
var escalation_label: Label
|
||||
|
||||
var _debug_enabled := false
|
||||
@@ -99,13 +98,6 @@ func _init() -> void:
|
||||
mode_label.modulate = Color(0.85, 0.85, 0.9)
|
||||
vbox.add_child(mode_label)
|
||||
|
||||
# --- dialogue tone indicator ---
|
||||
tone_label = Label.new()
|
||||
tone_label.text = "Tone: Professional"
|
||||
tone_label.add_theme_font_size_override("font_size", 13)
|
||||
tone_label.modulate = Color(0.7, 0.85, 1.0)
|
||||
vbox.add_child(tone_label)
|
||||
|
||||
# --- escalation level indicator (color-coded) ---
|
||||
escalation_label = Label.new()
|
||||
escalation_label.text = "Escalation: Observation"
|
||||
@@ -200,8 +192,7 @@ func _refresh_mode_label() -> void:
|
||||
func set_action_hint(text: String) -> void:
|
||||
hint_label.text = text
|
||||
|
||||
func set_tone(tone_name: String) -> void:
|
||||
tone_label.text = Strings.fmt("hud.tone_prefix", [tone_name])
|
||||
|
||||
|
||||
## Color-coded escalation feedback (PLAN.md: Polish and Accessibility).
|
||||
## Green → yellow → orange → red as the escalation level rises.
|
||||
|
||||
@@ -16,7 +16,7 @@ func test_tourist_complies_at_level_1() -> bool:
|
||||
var p := _profile_with_defaults("tourist", Rules.NpcType.TOURIST, 0.9)
|
||||
var npc := NPCController.new()
|
||||
npc.setup("t1", p, "photo", 1234, "AT_ENTRANCE")
|
||||
var outcome := npc.apply_player_action(Rules.Level.SIMPLE_DETERRENCE, Rules.Tone.FRIENDLY)
|
||||
var outcome := npc.apply_player_action(Rules.Level.SIMPLE_DETERRENCE)
|
||||
var ok := TestUtils.check_eq(outcome["reaction"], Rules.Reaction.COMPLY, "base_compliance 0.9 tourist should comply at level 1")
|
||||
return ok and TestUtils.check_eq(npc.decide_leave(), true, "compliant NPC leaves")
|
||||
|
||||
@@ -28,7 +28,7 @@ func test_trespasser_frequent_non_compliance() -> bool:
|
||||
## from NpcProfile.default_reactions (mostly COMPLY there).
|
||||
var outcomes: Array = []
|
||||
for i in 5:
|
||||
outcomes.append(npc.roll_reaction(Rules.Level.SIMPLE_DETERRENCE, Rules.Tone.PROFESSIONAL))
|
||||
outcomes.append(npc.roll_reaction(Rules.Level.SIMPLE_DETERRENCE))
|
||||
## With compliance 0.9/0.2 variation we simply assert we get SOME reaction.
|
||||
var ok := TestUtils.check(npc.highest_escalation_received >= Rules.Level.SIMPLE_DETERRENCE, "highest escalation tracked")
|
||||
return ok
|
||||
@@ -46,8 +46,8 @@ func test_deterministic_with_seed() -> bool:
|
||||
var seq_a: Array = []
|
||||
var seq_b: Array = []
|
||||
for i in 4:
|
||||
seq_a.append(n1.roll_reaction(Rules.Level.FORMAL_WARNING, Rules.Tone.PROFESSIONAL))
|
||||
seq_b.append(n2.roll_reaction(Rules.Level.FORMAL_WARNING, Rules.Tone.PROFESSIONAL))
|
||||
seq_a.append(n1.roll_reaction(Rules.Level.FORMAL_WARNING))
|
||||
seq_b.append(n2.roll_reaction(Rules.Level.FORMAL_WARNING))
|
||||
return TestUtils.check_eq(seq_a, seq_b, "same seed produces identical reaction sequence")
|
||||
|
||||
func test_violent_threat_scales_to_threaten() -> bool:
|
||||
@@ -69,7 +69,7 @@ func test_violent_threat_scales_to_threaten() -> bool:
|
||||
npc.setup("v1", p, "threat", 777, "AT_ENTRANCE")
|
||||
var saw_threat := false
|
||||
for i in 20:
|
||||
if npc.roll_reaction(Rules.Level.EMERGENCY, Rules.Tone.PROFESSIONAL) == Rules.Reaction.THREATEN:
|
||||
if npc.roll_reaction(Rules.Level.EMERGENCY) == Rules.Reaction.THREATEN:
|
||||
saw_threat = true
|
||||
return TestUtils.check(saw_threat, "violent profile can threaten at emergency level")
|
||||
|
||||
@@ -82,7 +82,7 @@ func test_call_someone_reaction() -> bool:
|
||||
}
|
||||
var npc := NPCController.new()
|
||||
npc.setup("c1", p, "film", 5, "AT_ENTRANCE")
|
||||
var outcome := npc.apply_player_action(Rules.Level.FORMAL_WARNING, Rules.Tone.PROFESSIONAL)
|
||||
var outcome := npc.apply_player_action(Rules.Level.FORMAL_WARNING)
|
||||
var a := TestUtils.check_eq(outcome["reaction"], Rules.Reaction.CALL_SOMEONE, "rolls CALL_SOMEONE")
|
||||
var b := TestUtils.check_eq(npc.fsm.current_state, "ARGUING", "transitions to ARGUING")
|
||||
return a and b
|
||||
@@ -92,7 +92,7 @@ func test_fsm_transitions() -> bool:
|
||||
var npc := NPCController.new()
|
||||
npc.setup("g1", p, "enter", 5, "APPROACH")
|
||||
var started := TestUtils.check_eq(npc.fsm.current_state, "APPROACH", "starts approaching")
|
||||
npc.apply_player_action(Rules.Level.SIMPLE_DETERRENCE, Rules.Tone.FRIENDLY)
|
||||
npc.apply_player_action(Rules.Level.SIMPLE_DETERRENCE)
|
||||
var left := TestUtils.check_eq(npc.fsm.current_state, "LEAVING", "complies and leaves")
|
||||
return started and left
|
||||
|
||||
@@ -103,7 +103,7 @@ func test_returning_trespasser_circles_back() -> bool:
|
||||
p.return_probability = 1.0
|
||||
var npc := NPCController.new()
|
||||
npc.setup("rt1", p, "trespass", 4242, "AT_ENTRANCE")
|
||||
npc.apply_player_action(Rules.Level.SIMPLE_DETERRENCE, Rules.Tone.PROFESSIONAL)
|
||||
npc.apply_player_action(Rules.Level.SIMPLE_DETERRENCE)
|
||||
var left := TestUtils.check_eq(npc.fsm.current_state, "LOITERING_DISTANT", "directed away loiters before returning")
|
||||
var scheduled := TestUtils.check(npc.will_return, "return scheduled")
|
||||
# Advance past the return delay (max 60s).
|
||||
@@ -122,7 +122,7 @@ func test_tourist_never_returns() -> bool:
|
||||
var p := _profile_with_defaults("tourist", Rules.NpcType.TOURIST, 0.9)
|
||||
var npc := NPCController.new()
|
||||
npc.setup("t2", p, "photo", 7, "AT_ENTRANCE")
|
||||
npc.apply_player_action(Rules.Level.SIMPLE_DETERRENCE, Rules.Tone.FRIENDLY)
|
||||
npc.apply_player_action(Rules.Level.SIMPLE_DETERRENCE)
|
||||
return TestUtils.check_eq(npc.will_return, false, "tourist does not schedule a return")
|
||||
|
||||
func test_perception_pass_bands() -> bool:
|
||||
|
||||
@@ -10,8 +10,8 @@ func test_strings_lookup() -> bool:
|
||||
return a and b
|
||||
|
||||
func test_strings_format() -> bool:
|
||||
var a := TestUtils.check_eq(Strings.fmt("hud.tone_prefix", ["Firm"]), "Tone: Firm", "tone prefix formats")
|
||||
var b := TestUtils.check_eq(Strings.fmt("react.comply", ["Tourist"]), "Tourist complies and steps back.", "reaction formats")
|
||||
var a := TestUtils.check_eq(Strings.fmt("react.comply", ["Tourist"]), "Tourist complies and steps back.", "reaction formats")
|
||||
var b := TestUtils.check_eq(Strings.fmt("hud.venue_switched", ["The Joke Shop"]), "Venue: The Joke Shop", "venue switch formats")
|
||||
return a and b
|
||||
|
||||
func test_strings_json_backed_all_resolve() -> bool:
|
||||
@@ -26,7 +26,7 @@ func test_strings_json_backed_all_resolve() -> bool:
|
||||
func test_strings_cover_ui_keys() -> bool:
|
||||
## Every key referenced by the HUD and game manager must exist.
|
||||
var required := [
|
||||
"hud.controls", "hud.tone_prefix", "hud.escalation_prefix", "hud.cooldown",
|
||||
"hud.controls", "hud.escalation_prefix", "hud.cooldown", "hud.venue_switched",
|
||||
"hud.aim_first", "hud.no_one_nearby", "hud.targeting", "hud.no_wand",
|
||||
"hud.screen_first", "access.granted", "access.denied", "access.policy_satisfied",
|
||||
"access.wrong_grant", "access.wrong_deny", "wand.admitted_prohibited",
|
||||
|
||||
Reference in new issue
Block a user