# Check for invalid room connection directions (diagonal directions)
ifroom['connections']
room['connections'].eachdo|direction,target|
unlessvalid_directions.include?(direction)
issues<<"❌ INVALID: Room '#{room_id}' uses invalid direction '#{direction}' - only north, south, east, west are valid (not northeast, southeast, etc.)"
end
# Check reverse connections if target is a single room
# Check for incorrect VM launcher configuration (type: "pc" with vmAccess)
ifobj['type']=='pc'&&obj['vmAccess']
issues<<"❌ INVALID: '#{path}' uses type: 'pc' with vmAccess - should use type: 'vm-launcher' instead. See scenarios/secgen_vm_lab/scenario.json.erb for example"
issues<<"❌ INVALID: '#{path}' is a container with contents but missing required 'locked' field - must be explicitly true or false for server-side validation"
# Check for key locks without keyPins (REQUIRED, not recommended)
unlessobj['keyPins']
issues<<"❌ INVALID: '#{path}' has lockType: 'key' but missing required 'keyPins' array - key locks must specify keyPins array for lockpicking minigame"
end
end
end
# Check for key items without keyPins (REQUIRED, not recommended)
ifobj['type']=='key'&&!obj['keyPins']
issues<<"❌ INVALID: '#{path}' (key item) missing required 'keyPins' array - key items must specify keyPins array for lockpicking"
end
# Check for items with id field (should use type field for #give_item tags)
ifobj['itemsHeld']
obj['itemsHeld'].each_with_indexdo|item,item_idx|
ifitem['id']
issues<<"❌ INVALID: '#{path}/itemsHeld[#{item_idx}]' has 'id' field - items should NOT have 'id' field. Use 'type' field to match #give_item tag parameter"
end
end
end
end
end
# Track room lock types
ifroom['locked']&&room['lockType']
lock_types_used.add(room['lockType'])
caseroom['lockType']
when'rfid'
has_rfid_lock=true
when'bluetooth'
has_bluetooth_lock=true
when'pin'
has_pin_lock=true
when'password'
has_password_lock=true
when'key'
has_key_lock=true
# Check for key locks without keyPins (REQUIRED, not recommended)
unlessroom['keyPins']
issues<<"❌ INVALID: 'rooms/#{room_id}' has lockType: 'key' but missing required 'keyPins' array - key locks must specify keyPins array for lockpicking minigame"
# Validate eventMapping vs eventMappings (parameter name mismatch)
ifnpc['eventMapping']&&!npc['eventMappings']
issues<<"❌ INVALID: '#{path}' uses 'eventMapping' (singular) - should use 'eventMappings' (plural). The NPCManager expects 'eventMappings' and won't register event listeners with 'eventMapping'"
end
# Validate eventMappings structure
ifnpc['eventMappings']
# Check if it's an array
unlessnpc['eventMappings'].is_a?(Array)
issues<<"❌ INVALID: '#{path}' eventMappings is not an array - must be an array of event mapping objects"
# Check for incorrect property name (knot vs targetKnot)
ifmapping['knot']&&!mapping['targetKnot']
issues<<"❌ INVALID: '#{mapping_path}' uses 'knot' property - should use 'targetKnot' instead. Change 'knot' to 'targetKnot'"
end
# Check for missing eventPattern
unlessmapping['eventPattern']
issues<<"❌ INVALID: '#{mapping_path}' missing required 'eventPattern' property - must specify the event pattern to listen for (e.g., 'global_variable_changed:varName')"
end
# Check for missing conversationMode when targetKnot is present
issues<<"⚠ WARNING: '#{mapping_path}' has targetKnot but no conversationMode - should specify 'phone-chat' or 'person-chat' to indicate which UI to use"
end
# Check for missing background when conversationMode is person-chat
issues<<"⚠ WARNING: '#{mapping_path}' has conversationMode: 'person-chat' but no background - person-chat cutscenes typically need a background image (e.g., 'assets/backgrounds/hq1.png')"
# Validate phone NPC structure - should have phoneId
unlessnpc['phoneId']
issues<<"❌ INVALID: '#{path}' (phone NPC) missing required 'phoneId' field - phone NPCs must specify which phone they appear on (e.g., 'player_phone')"
# Validate phone NPC structure - should NOT have position (phone NPCs don't have positions)
ifnpc['position']
issues<<"⚠ WARNING: '#{path}' (phone NPC) has 'position' field - phone NPCs should NOT have position (they're not in-world sprites). Remove the position field."
# Validate phone NPC structure - should NOT have spriteSheet (phone NPCs don't have sprites)
ifnpc['spriteSheet']
issues<<"⚠ WARNING: '#{path}' (phone NPC) has 'spriteSheet' field - phone NPCs should NOT have spriteSheet (they're not in-world sprites). Remove the spriteSheet field."
issues<<"❌ INVALID: '#{path}/itemsHeld[#{item_idx}]' has 'id' field - items should NOT have 'id' field. Use 'type' field to match #give_item tag parameter (e.g., type: 'id_badge' matches #give_item:id_badge)"
# Flag incorrect structure - phone NPCs should be in rooms, not phoneNPCs section
issues<<"❌ INVALID: '#{path}' - Phone NPCs should be defined in 'rooms/{room_id}/npcs[]' arrays, NOT in a separate 'phoneNPCs' section. See scenarios/npc-sprite-test3/scenario.json.erb for correct format. Phone NPCs should be in the starting room (or room where phone is accessible) with npcType: 'phone'"
issues<<"❌ INVALID: '#{mapping_path}' references global variable '#{var_name}' in eventPattern, but it's not defined in scenario.globalVariables. Add '#{var_name}' with an initial value (typically false) to globalVariables"
end
end
# Check for missing spriteTalk when using non-numeric frame sprites
if!npc['spriteTalk']&&npc['spriteSheet']
# Sprites with named frames (not numeric indices) need spriteTalk
issues<<"⚠ WARNING: '#{path}' uses spriteSheet '#{npc['spriteSheet']}' which has named frames, but no 'spriteTalk' property. Person-chat cutscenes will show frame errors. Add 'spriteTalk' property pointing to a headshot image (e.g., 'assets/characters/#{npc['spriteSheet']}_headshot.png')"
end
end
# Validate background for person-chat cutscenes
unlessmapping['background']
issues<<"⚠ WARNING: '#{mapping_path}' is a person-chat cutscene but has no 'background' property. Person-chat cutscenes should have a background image for better visual presentation (e.g., 'assets/backgrounds/hq1.png')"
end
# Check for onceOnly to prevent repeated cutscenes
unlessmapping['onceOnly']
issues<<"⚠ WARNING: '#{mapping_path}' is a person-chat cutscene without 'onceOnly: true'. Cutscenes typically should only trigger once. Add 'onceOnly: true' unless you want the cutscene to repeat"
end
end
end
end
# Check for phone NPCs setting global variables in their stories
ifnpc['npcType']=='phone'&&npc['storyPath']
# Note: We can't easily check the Ink story content from Ruby, but we can suggest best practices
ifnpc['eventMappings']
# This phone NPC has both a story and event mappings, which suggests it might be setting up a cutscene
issues<<"💡 BEST PRACTICE: '#{path}' appears to be a mission-ending phone NPC with sendTimedMessage. Consider using event-driven cutscene architecture instead: 1) Add #set_global:variable_name:true tag in Ink story, 2) Add #exit_conversation tag to close phone, 3) Create separate person NPC with eventMapping listening for global_variable_changed:variable_name. See scenarios/m01_first_contact/scenario.json.erb for reference implementation"
issues<<"❌ INVALID: Global variable '#{var_name}' is referenced in eventPatterns but not defined in scenario.globalVariables. Add '#{var_name}' to globalVariables with an initial value (typically false for cutscene triggers)"
end
# Provide best practice guidance for event-driven cutscenes
ifperson_npcs_with_event_cutscenes.any?
issues<<"✅ GOOD PRACTICE: Scenario uses event-driven cutscene architecture with #{person_npcs_with_event_cutscenes.size} person-chat cutscene(s). Ensure corresponding phone NPCs use #set_global tags to trigger these cutscenes"
issues<<"💡 SUGGESTION: Consider adding VM launcher terminals (type: 'vm-launcher') - see scenarios/secgen_vm_lab/scenario.json.erb for example"
end
unlesshas_flag_station
issues<<"💡 SUGGESTION: Consider adding flag station terminals (type: 'flag-station') for VM flag submission - see scenarios/secgen_vm_lab/scenario.json.erb for example"
end
unlesshas_pc_with_files
issues<<"💡 SUGGESTION: Consider adding at least one PC container (type: 'pc') with files in 'contents' array and optional post-it notes - see scenarios/ceo_exfil/scenario.json.erb for example"
issues<<"💡 SUGGESTION: Consider adding at least one phone NPC (in rooms or phoneNPCs section) with timedMessages or eventMappings - see scenarios/ceo_exfil/scenario.json.erb for example"
end
unlesshas_opening_cutscene
issues<<"💡 SUGGESTION: Consider adding opening briefing cutscene - NPC with timedConversation (delay: 0) in starting room - see scenarios/m01_first_contact/scenario.json.erb for example"
issues<<"💡 SUGGESTION: Consider adding waypoints to at least one person NPC for more dynamic patrol behavior - see scenarios/test-npc-waypoints/scenario.json.erb for example. Add 'behavior.patrol.waypoints' array with {x, y} coordinates"
issues<<"💡 SUGGESTION: Consider adding timedMessages to phone contacts for more engaging interactions - see scenarios/npc-sprite-test3/scenario.json.erb for example. Phone NPCs without timed messages: #{npc_list}"
issues<<"💡 SUGGESTION: Consider adding variety in lock types - scenarios typically use 2+ different lock mechanisms (key, pin, rfid, password). Currently using: #{lock_types_used.to_a.join(', ')||'none'}. See scenarios/ceo_exfil/scenario.json.erb for examples"
end
# Suggest RFID locks
unlesshas_rfid_lock
issues<<"💡 SUGGESTION: Consider adding RFID locks for modern security scenarios - see scenarios/test-rfid/scenario.json.erb for examples"
end
# Suggest PIN locks
unlesshas_pin_lock
issues<<"💡 SUGGESTION: Consider adding PIN locks for numeric code challenges - see scenarios/ceo_exfil/scenario.json.erb for examples"
end
# Suggest password locks
unlesshas_password_lock
issues<<"💡 SUGGESTION: Consider adding password locks for computer/device access - see scenarios/ceo_exfil/scenario.json.erb for examples"
end
# Suggest security tools
unlesshas_security_tools
issues<<"💡 SUGGESTION: Consider adding security tools (fingerprint_kit, pin-cracker, bluetooth_scanner, rfid_cloner) for more interactive gameplay - see scenarios/ceo_exfil/scenario.json.erb for examples"
end
# Suggest containers with contents
unlesshas_container_with_contents
issues<<"💡 SUGGESTION: Consider adding containers (safes, suitcases) with contents for hidden items and rewards - see scenarios/ceo_exfil/scenario.json.erb for examples"
end
# Suggest readable items
unlesshas_readable_items
issues<<"💡 SUGGESTION: Consider adding readable items (notes, documents) for storytelling and clues - see scenarios/ceo_exfil/scenario.json.erb for examples"