Enhance scenario schema and validation scripts

- Updated scenario-schema.json to include "tutorial" as a valid difficulty level.
- Changed position coordinates from integer to number for better precision.
- Added new item types ("id_badge", "rfid_cloner") in scenario schema with descriptions.
- Improved validate_scenario.rb to check for common issues, including room connection directions and NPC configurations.
- Added suggestions for gameplay improvements based on scenario features.
- Updated SCENARIO_JSON_FORMAT_GUIDE.md to clarify valid directions and bidirectional connections.
- Introduced guidelines for lock type variety and progression in room layout design.
- Established dialogue pacing rules in Ink scripting to enhance player engagement.
- Included validation steps in scenario assembly documentation to ensure structural integrity.
This commit is contained in:
Z. Cliffe Schreuders
2025-12-02 10:37:57 +00:00
parent 7ac541a286
commit 49fc995cb3
25 changed files with 1409 additions and 131 deletions

View File

@@ -16,3 +16,5 @@ class RemoveUniqueGameConstraint < ActiveRecord::Migration[7.0]
name: 'index_games_on_player_and_mission_non_unique'
end
end

View File

@@ -182,3 +182,5 @@
font-size: 13px;
}

View File

@@ -188,3 +188,5 @@
color: #ccc;
}

View File

@@ -219,3 +219,5 @@ window.hacktivityCable = new HacktivityCable();
// Export for module usage
export default window.hacktivityCable;

View File

@@ -2,12 +2,237 @@
**Date:** 2025-12-01
**Status:** ✅ All Critical Issues Resolved
**Latest Fix:** 2025-12-01 - Fixed item types to match #give_item tags (removed id fields, corrected type values)
**Latest Fix:** 2025-12-01 - Added lock type variety by converting safes to PIN codes
---
## Issues Found and Fixed
### Issue #10: Lack of Lock Type Variety
**Problem:** All locks used keys/lockpick - gameplay became repetitive and "same-y"
**User Feedback:** "The player is given a lock pick quite early so never experiences doors that they can't get through soon. All the doors and safes etc use keys -- change the safes to use PINs that the player needs to discover/reveal."
**Root Cause:** All 3 safes used `lockType: "key"` with lockpick - no variety in puzzle types
**Fix Applied:**
- **Storage Safe** → PIN code `1337` with hint in maintenance checklist
- **Main Office Filing Cabinet** → PIN code `2024` with hint on sticky note
- **Derek's Filing Cabinet** → PIN code `0419` discovered by decoding Base64 message
**Lock Type Variety Now:**
1. **Storage closet door** - lockpick (tutorial)
2. **Storage safe** - PIN `1337` (easy puzzle - hint nearby)
3. **Main office filing cabinet** - PIN `2024` (medium puzzle - requires reading sticky note)
4. **Derek's office door** - key (from storage safe)
5. **Derek's filing cabinet** - PIN `0419` (harder puzzle - requires CyberChef to decode Base64)
6. **Server room door** - RFID keycard (different lock type)
**Files Changed:**
- `scenarios/m01_first_contact/scenario.json.erb` - Converted 3 safes to PIN locks, added hint documents
- Updated `client_list_message` variable to include PIN hint for Derek's safe
**Progression Design:**
- Player must use lockpick tutorial first (storage closet)
- Find PIN hints through exploration and reading documents
- Use CyberChef terminal to decode Base64 for final safe PIN
- Creates varied puzzle-solving experience instead of just lockpicking everything
**FURTHER UPDATE - Lock Progression Enforcement:**
**User Feedback:** "Once the player has access to a lockpick, then they don't need any keys, so if we want to include traditional key based access, then those need to happen before they get the lockpick. Keys shouldn't be in same room as door. Must ensure valid path to completion and logical puzzle ordering."
**Additional Fixes:**
- Storage closet door changed from `locked: true, requires: lockpick``locked: false`
- Moved maintenance checklist from storage closet to main office (hint accessible first)
- Kevin's lockpick dialogue updated: requires `influence >= 8` (was: immediate after meeting)
- Ensures key-based puzzles MUST be solved before lockpick is obtained
**Final Progression Flow:**
1. Main office → find hints (sticky note PIN 2024, maintenance checklist PIN 1337)
2. Use PIN 2024 on main office filing cabinet → The Architect's Letter (LORE, optional)
3. Use PIN 1337 on storage safe → Derek's office key
4. Use key on Derek's office door → enter Derek's office ✅ **KEY USED BEFORE LOCKPICK**
5. Decode Base64 message with CyberChef → PIN 0419
6. Use PIN 0419 on Derek's filing cabinet → campaign evidence
7. Build rapport with Kevin (influence >= 8) → NOW get lockpick
8. Get RFID keycard from Kevin → access server room
**Progression Enforcement:**
- Keys used for critical path BEFORE lockpick obtainable ✅
- Keys not in same room as locks (storage safe → Derek's office) ✅
- Logical puzzle ordering (easy → medium → hard) ✅
- Valid completion path ensured ✅
---
### Issue #9: Derek's Ink File Path Incorrect
**Problem:** Derek NPC's storyPath pointed to non-existent file `m01_npc_derek.json`
**Error:** `GET /break_escape/games/112/ink?npc=derek_lawson 404 (Not Found)`
**Root Cause:** Filename mismatch - actual file is `m01_derek_confrontation.json`
**Fix Applied:**
- Changed storyPath from `m01_npc_derek.json` to `m01_derek_confrontation.json`
- Now correctly points to the compiled Derek confrontation Ink script
**Files Changed:**
- `scenarios/m01_first_contact/scenario.json.erb` - Updated Derek NPC storyPath
**Correct Configuration:**
```json
{
"id": "derek_lawson",
"displayName": "Derek Lawson",
"npcType": "person",
"storyPath": "scenarios/m01_first_contact/ink/m01_derek_confrontation.json",
"currentKnot": "start"
}
```
---
### Issue #8: Phone NPCs in Separate Array
**Problem:** Phone NPCs were in separate `phoneNPCs` array instead of in room `npcs` arrays
**Validation Error:** "Phone NPCs should be defined in 'rooms/{room_id}/npcs[]' arrays, NOT in a separate 'phoneNPCs' section"
**Root Cause:** Misunderstood NPC placement - all NPCs (including phone NPCs) should be in room arrays
**Fix Applied:**
- Moved `agent_0x99` and `closing_debrief_trigger` from `phoneNPCs` array to `reception_area.npcs` array
- Removed separate `phoneNPCs` section entirely
- Phone NPCs now properly defined alongside person NPCs in starting room
**Files Changed:**
- `scenarios/m01_first_contact/scenario.json.erb` - Moved phone NPCs to room arrays
**Correct Format:**
```json
"rooms": {
"reception_area": {
"npcs": [
// Person NPCs
{ "id": "sarah_martinez", "npcType": "person", ... },
// Phone NPCs (same array!)
{ "id": "agent_0x99", "npcType": "phone", "phoneId": "player_phone", ... }
]
}
}
```
**Validation Tool:** Caught by `ruby scripts/validate_scenario.rb`
---
### Issue #7: Invalid Room Connections (Diagonal Directions)
**Problem:** Rooms used invalid diagonal directions (southeast, northwest) making some rooms inaccessible
**User Feedback:** "Some of the rooms aren't accessible -- the break_room and storage_closet aren't connected to the other rooms"
**Root Cause:**
- Used invalid diagonal directions like "southeast" and "northwest"
- Only north, south, east, west are valid directions
- Reverse connections weren't using valid cardinal directions
**Fix Applied:**
- Removed diagonal directions (southeast, northwest) from main_office_area
- Multiple rooms already in array format: `"south": ["reception_area", "break_room"]`
- Fixed break_room reverse connection from "northwest" to "north"
- storage_closet already had correct "south" connection
**Files Changed:**
- `scenarios/m01_first_contact/scenario.json.erb` - Fixed room connections
**Valid Directions:** Only **north, south, east, west**
**Valid Format Examples:**
```json
// Single room
"connections": { "north": "room_id" }
// Multiple rooms in same direction (use array)
"connections": { "south": ["room_a", "room_b"] }
// ❌ INVALID - diagonal directions
"connections": { "southeast": "room_id" } // NOT VALID
```
**Bidirectional Connections Required:**
```json
// main_office_area connects north to storage_closet
"connections": { "north": ["derek_office", "storage_closet"] }
// storage_closet must connect south back to main_office_area
"connections": { "south": "main_office_area" } // NOT "southwest"!
```
---
### Issue #6: Incorrect VM Launcher and Flag Station Configuration
**Problem:** VM terminal used wrong object type (`type: "pc"` with `vmAccess`) instead of proper `vm-launcher` type
**User Feedback:** "The scenario seems to be missing the vm launchers and flag drop sites, this should have been incorporated into the scenario"
**Root Cause:** Used incorrect configuration format - should reference `scenarios/secgen_vm_lab` example
**Fix Applied:**
- Changed VM Access Terminal from `type: "pc"` to `type: "vm-launcher"`
- Added proper `hacktivityMode` and `vm` object using ERB helper `vm_object()`
- Changed Drop-Site Terminal from Ink dialogue to `type: "flag-station"`
- Added `acceptsVms`, `flags`, and `flagRewards` arrays
- Uses ERB helper `flags_for_vm()` to configure accepted flags
- Removed manual Ink dialogue script for flag submission
**Files Changed:**
- `scenarios/m01_first_contact/scenario.json.erb` - Updated server_room terminals
**Before (Incorrect):**
```json
{
"type": "pc",
"name": "VM Access Terminal",
"vmAccess": true,
"vmScenario": "intro_to_linux_security_lab"
}
```
**After (Correct):**
```json
{
"type": "vm-launcher",
"id": "vm_launcher_intro_linux",
"name": "VM Access Terminal",
"hacktivityMode": <%= vm_context && vm_context['hacktivity_mode'] ? 'true' : 'false' %>,
"vm": <%= vm_object('intro_to_linux_security_lab', {...}) %>
}
```
**Flag Station Configuration:**
```json
{
"type": "flag-station",
"id": "flag_station_dropsite",
"name": "SAFETYNET Drop-Site Terminal",
"acceptsVms": ["intro_to_linux_security_lab"],
"flags": <%= flags_for_vm('intro_to_linux_security_lab', [...]) %>,
"flagRewards": [
{"type": "emit_event", "event_name": "ssh_flag_submitted"},
{"type": "emit_event", "event_name": "navigation_flag_submitted"},
{"type": "emit_event", "event_name": "sudo_flag_submitted"}
]
}
```
---
### Issue #1: Missing Opening Briefing Cutscene
**Problem:** Opening briefing Ink script existed but wasn't configured to auto-start
@@ -216,7 +441,25 @@
## Prompts Updated
### 1. Stage 1: Narrative Structure
### 1. Stage 5: Room Layout Design
**File:** `story_design/story_dev_prompts/05_room_layout_design.md`
**Added Section:** "CRITICAL: Lock Type Variety and Progression"
- Lock Type Ordering Rules (keys BEFORE lockpick, vary lock types)
- Rule: Keys not in same room as locks
- Rule: Progressive difficulty (easy → medium → hard)
- Lock Progression Template with validation checklist
- Examples of good vs bad progression
- 4 critical rules for lock design
**Why Added:**
- Prevents "same-y" gameplay from using only one lock type
- Ensures keys matter by using them before lockpick is obtained
- Provides clear template for designing lock progression
- Helps future scenarios avoid lock progression mistakes
### 2. Stage 1: Narrative Structure
**File:** `story_design/story_dev_prompts/01_narrative_structure.md`
@@ -229,6 +472,12 @@
**File:** `story_design/story_dev_prompts/07_ink_scripting.md`
**Added Section:** "CRITICAL: Dialogue Pacing Rule"
- Maximum 3 lines from single character before presenting player choices
- Keeps dialogue snappy and interactive
- Prevents dialogue fatigue and maintains pacing
- Includes good/bad examples and exceptions
**Added Section:** "CRITICAL: Compile Ink Scripts Before Proceeding"
- Compile scripts after writing them: `./scripts/compile-ink.sh [scenario_name]`
- Validates syntax and catches errors early
@@ -240,14 +489,15 @@
**File:** `story_design/story_dev_prompts/09_scenario_assembly.md`
**Added Section:** "Pre-Assembly Required Steps"
- Compile all Ink scripts before assembly
- Verify successful compilation
- Fix any errors before proceeding
- Compile all Ink scripts before assembly: `./scripts/compile-ink.sh [scenario_name]`
- Validate scenario structure: `ruby scripts/validate_scenario.rb scenarios/[scenario_name]/scenario.json.erb`
- Verify successful compilation and validation
- Fix all INVALID errors before proceeding (suggestions are optional)
**Updated Section:** "Required Reading"
- Added CRITICAL reference to SCENARIO_JSON_FORMAT_GUIDE.md
- Updated documentation references to match actual files
- Added reference to working examples (ceo_exfil, npc-sprite-test3)
- Added reference to working examples (ceo_exfil, npc-sprite-test3, secgen_vm_lab)
---
@@ -292,7 +542,9 @@
5. ✅ Set item `type` to match #give_item tag parameter (DON'T use `id` field)
6. ✅ Test Ink scripts compile before scenario assembly
7. ✅ Use object format for rooms, simple format for connections
8. ✅ Reference working examples (ceo_exfil, npc-sprite-test3)
8. ✅ Reference working examples (ceo_exfil, npc-sprite-test3, secgen_vm_lab)
9. ✅ Use `type: "vm-launcher"` for VM terminals with proper ERB helpers
10. ✅ Use `type: "flag-station"` for flag submission terminals
**Never Do:**
1. ❌ Use EXTERNAL for regular variables
@@ -302,6 +554,9 @@
5. ❌ Add `id` fields to items in itemsHeld (use `type` field instead)
6. ❌ Forget to set mission completion variables
7. ❌ Skip event mappings for automatic triggers
8. ❌ Use `type: "pc"` with `vmAccess` for VM launchers
9. ❌ Create Ink dialogue scripts for flag submission
10. ❌ Use diagonal directions (northeast, southeast, etc.) for room connections
---

View File

@@ -0,0 +1,169 @@
# Mission 1: First Contact - Objectives System Integration
## Overview
Integrated comprehensive objectives system to track player progress through the three primary aims specified in Agent 0x99's briefing.
## Objectives Structure
### Aim 1: Identify ENTROPY Operatives (order: 0)
**Status:** Active from mission start
**Tasks:**
1.`meet_reception` - Check in at reception (NPC conversation with Sarah)
2.`meet_kevin` - Meet the IT manager (NPC conversation with Kevin)
3. 🔒 `investigate_derek` - Investigate Derek Lawson's office (Enter derek_office)
4. 🔒 `confront_derek` - Confront the ENTROPY operative (NPC conversation with Derek)
### Aim 2: Gather Evidence (order: 1)
**Status:** Active from mission start
**Tasks:**
1. 🔒 `find_campaign_materials` - Find campaign materials (Collect notes from Derek's filing cabinet)
2. 🔒 `discover_manifesto` - Discover ENTROPY manifesto (Collect notes from Derek's filing cabinet)
3. 🔒 `decode_communications` - Decode encrypted communications (Use CyberChef workstation)
### Aim 3: Intercept Communications (order: 2)
**Status:** Active from mission start
**Tasks:**
1. 🔒 `access_server_room` - Access the server room (Enter server_room)
2. 🔒 `access_vm` - Access compromised systems (Interact with VM launcher)
3. 🔒 `submit_ssh_flag` - Submit SSH access evidence (Flag submission)
4. 🔒 `submit_linux_flag` - Submit Linux navigation evidence (Flag submission)
5. 🔒 `submit_sudo_flag` - Submit privilege escalation evidence (Flag submission)
## Task Unlock/Complete Flow
### Initial State (Mission Start)
- `meet_reception` - Active
- `meet_kevin` - Active
- All other tasks - Locked
### Progression Chain
**1. Meet Sarah (Reception)**
- **Trigger:** Talk to Sarah Martinez
- **Completes:** `meet_reception` (#complete_task in m01_npc_sarah.ink)
- **Unlocks:** `investigate_derek` when Sarah reveals Derek's suspicious behavior
**2. Meet Kevin (IT Manager)**
- **Trigger:** Talk to Kevin Park
- **Completes:** `meet_kevin` (#complete_task in m01_npc_kevin.ink)
- **Unlocks:** `access_server_room` when Kevin discusses server room
**3. Enter Derek's Office**
- **Trigger:** Player enters derek_office room
- **Completes:** `investigate_derek` (automatic room entry detection)
- **Unlocks via Agent 0x99 event handler:**
- `find_campaign_materials`
- `discover_manifesto`
- `decode_communications`
**4. Gather Evidence**
- **Campaign Materials:** Automatically completes when player picks up item from Derek's filing cabinet
- **Manifesto:** Automatically completes when player picks up item from Derek's filing cabinet
- **Decode Communications:** Completes when player successfully decodes Base64 message (#complete_task in m01_terminal_cyberchef.ink)
**5. Access Server Room**
- **Trigger:** Player enters server_room
- **Completes:** `access_server_room` (#complete_task in m01_phone_agent0x99.ink event handler)
- **Unlocks:** `access_vm`
**6. Access VM Systems**
- **Trigger:** Player interacts with VM launcher terminal
- **Completes:** `access_vm` (via Ink or game system)
- **Unlocks via m01_terminal_dropsite.ink first_access:**
- `submit_ssh_flag`
- `submit_linux_flag`
- `submit_sudo_flag`
**7. Submit VM Flags**
- **SSH Flag:** Completes when player submits correct SSH flag (#complete_task in m01_terminal_dropsite.ink)
- **Linux Flag:** Completes when player submits correct navigation flag (#complete_task in m01_terminal_dropsite.ink)
- **Sudo Flag:** Completes when player submits correct privilege escalation flag (#complete_task in m01_terminal_dropsite.ink)
- **Also unlocks:** `confront_derek` (player now has sufficient evidence)
**8. Confront Derek**
- **Trigger:** Player talks to Derek Lawson in his office
- **Completes:** `confront_derek` (#complete_task at start of m01_derek_confrontation.ink)
- **Mission Resolution:** Player chooses final outcome (arrest/recruit/expose)
## Ink Script Changes
### m01_npc_sarah.ink
```ink
=== derek_suspicion ===
+ [That does seem odd]
#unlock_task:investigate_derek // Unlocks investigation task
Sarah: Right? But I'm just the receptionist. What do I know?
```
### m01_npc_kevin.ink
```ink
=== ask_server_room ===
~ discussed_server_room = true
~ influence += 1
#unlock_task:access_server_room // Unlocks server access task
```
### m01_phone_agent0x99.ink
```ink
=== event_derek_office_entered ===
#unlock_task:find_campaign_materials
#unlock_task:discover_manifesto
#unlock_task:decode_communications
Agent 0x99: You're in Derek's office. Good.
=== event_server_room_entered ===
#complete_task:access_server_room
#unlock_task:access_vm
Agent 0x99: You're in the server room. Good work getting access.
```
### m01_terminal_cyberchef.ink
```ink
=== whiteboard_decoded ===
~ decoded_whiteboard = true
#complete_task:decode_communications // Changed from decode_whiteboard
```
### m01_terminal_dropsite.ink
```ink
=== first_access ===
#unlock_task:submit_ssh_flag
#unlock_task:submit_linux_flag
#unlock_task:submit_sudo_flag
=== sudo_success ===
#complete_task:submit_sudo_flag
#unlock_task:confront_derek // Sufficient evidence gathered
```
### m01_derek_confrontation.ink
```ink
=== start ===
#complete_task:confront_derek // Final task completion
Derek: Working late on the security audit?
```
## Validation
✅ All Ink scripts compile successfully
✅ Scenario structure validates against schema
✅ All task IDs match between objectives and Ink tags
✅ Proper task progression from locked → active → completed
## Task Type Mapping
- **npc_conversation:** Direct NPC dialogue tasks (Sarah, Kevin, Derek)
- **enter_room:** Room entry tasks (Derek's office, server room)
- **collect_items:** Item collection tasks (campaign materials, manifesto)
- **unlock_object:** Terminal/object interaction tasks (CyberChef, VM launcher, flag station)
## Notes
- Most task completion is handled via Ink tags (#complete_task, #unlock_task)
- Item collection tasks are automatically tracked by game engine
- Room entry tasks can be completed automatically or via Ink event handlers
- The objectives system provides clear player guidance matching the briefing's three-pronged approach

View File

@@ -196,15 +196,12 @@ Derek: What happens next is up to you.
+ [I'm calling in SAFETYNET. You're under arrest]
~ final_choice = "arrest"
#complete_task:final_resolution
-> choice_arrest
+ [I have a proposition—work for us instead]
~ final_choice = "recruit"
#complete_task:final_resolution
-> choice_recruit
+ [I'm exposing everything publicly]
~ final_choice = "expose"
#complete_task:final_resolution
-> choice_expose
// ================================================

File diff suppressed because one or more lines are too long

View File

@@ -147,7 +147,7 @@ Kevin: They'd rather spend on marketing than IT security. Classic mistake.
-> ask_server_room
+ {influence >= 6 and not can_clone_card} [I'll need to test RFID security. Can I clone your card?]
-> request_card_clone
+ {not given_lockpick and discussed_audit} [About that lockpick...]
+ {not given_lockpick and discussed_audit and influence >= 8} [About that lockpick...]
-> offer_lockpick
+ [I'll keep working. Thanks for the help]
#exit_conversation
@@ -229,6 +229,7 @@ Kevin: We use cloud hosting for everything client-facing.
=== ask_server_room ===
~ discussed_server_room = true
~ influence += 1
#unlock_task:access_server_room
Kevin: Standard setup. Internal servers, network equipment, some legacy systems.

File diff suppressed because one or more lines are too long

View File

@@ -151,6 +151,7 @@ Sarah: And I've seen him in the server room a couple times. Told me he was check
+ [That does seem odd]
~ influence += 1
#unlock_task:investigate_derek
Sarah: Right? But I'm just the receptionist. What do I know?
-> hub
+ [Maybe he's just thorough]

File diff suppressed because one or more lines are too long

View File

@@ -186,6 +186,27 @@ Agent 0x99: Remember, you're testing security—officially.
+ [Any lockpicking tips?]
-> lockpick_help
// ================================================
// EVENT: SERVER ROOM ENTERED
// ================================================
=== event_server_room_entered ===
#speaker:agent_0x99
#complete_task:access_server_room
#unlock_task:access_vm
Agent 0x99: You're in the server room. Good work getting access.
Agent 0x99: Look for the compromised systems. VM access will give you deeper intelligence.
+ [What am I looking for?]
Agent 0x99: Evidence of ENTROPY's infrastructure. Backdoors, encrypted communications, anything linking Derek to other cells.
#exit_conversation
-> support_hub
+ [On it]
#exit_conversation
-> support_hub
// ================================================
// EVENT: FIRST FLAG SUBMITTED
// ================================================
@@ -210,8 +231,11 @@ Agent 0x99: Each flag unlocks intelligence. Keep correlating VM findings with ph
// EVENT: DEREK'S OFFICE ACCESSED
// ================================================
=== event_derek_office ===
=== event_derek_office_entered ===
#speaker:agent_0x99
#unlock_task:find_campaign_materials
#unlock_task:discover_manifesto
#unlock_task:decode_communications
Agent 0x99: You're in Derek's office. Good.

File diff suppressed because one or more lines are too long

View File

@@ -100,7 +100,7 @@ Enter Base64 string from Derek's whiteboard:
=== whiteboard_decoded ===
~ decoded_whiteboard = true
#complete_task:decode_whiteboard
#complete_task:decode_communications
DECODING... Base64 → ASCII

View File

@@ -1 +1 @@
{"inkVersion":21,"root":[[["done",{"#n":"g-0"}],null],"done",{"start":["ev",{"VAR?":"first_use"},"/ev",[{"->":".^.b","c":true},{"b":["\n","ev",false,"/ev",{"VAR=":"first_use","re":true},{"->":"first_access"},{"->":"start.4"},null]}],"nop","\n","ev",{"VAR?":"first_use"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"hub"},{"->":"start.11"},null]}],"nop","\n",null],"first_access":[["^CYBERCHEF WORKSTATION","\n","^Data Transformation & Analysis Tool","\n","^This tool helps decode and analyze data. Perfect for messages that aren't encrypted, just encoded.","\n","ev","str","^What's the difference between encoding and encryption?","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^I have something to decode","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"encoding_tutorial"},null],"c-1":["\n",{"->":"hub"},null]}],null],"encoding_tutorial":[["ev",true,"/ev",{"VAR=":"learned_encoding","re":true},"^ENCODING vs. ENCRYPTION:","\n","^Encoding transforms data for compatibility or readability (Base64, URL encoding).","\n","^Encryption transforms data for secrecy using keys (AES, RSA).","\n","^Key difference: Encoding is reversible by anyone. Encryption requires a key.","\n","ev","str","^So Base64 isn't secure?","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Got it. Let me decode something","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"base64_explanation"},null],"c-1":["\n",{"->":"hub"},null]}],null],"base64_explanation":["^Exactly. Base64 is just a way to represent binary data in ASCII text.","\n","^It's used for compatibility, not security. Anyone can decode it instantly.","\n","^If you see Base64, it's likely obfuscation, not real encryption.","\n",{"->":"hub"},null],"hub":[["^CYBERCHEF > Select operation","\n","ev","str","^Decode Base64 message from whiteboard","/str",{"VAR?":"decoded_whiteboard"},"!","/ev",{"*":".^.c-0","flg":5},"ev","str","^Learn about encoding vs encryption","/str",{"VAR?":"learned_encoding"},"!","/ev",{"*":".^.c-1","flg":5},"ev","str","^Exit workstation","/str","/ev",{"*":".^.c-2","flg":4},{"c-0":["\n",{"->":"decode_whiteboard_message"},null],"c-1":["\n",{"->":"encoding_tutorial"},null],"c-2":["\n","#","^exit_conversation","/#","^Workstation session closed.","\n",{"->":"hub"},null]}],null],"decode_whiteboard_message":[["^Enter Base64 string from Derek's whiteboard:","\n","^[Player enters: Q2xpZW50IGxpc3QgdXBkYXRlOiBDb29yZGluYXRpbmcgd2l0aCBaRFMgZm9yIHRlY2huaWNhbCBpbmZyYXN0cnVjdHVyZQ==]","\n","ev","str","^Q2xpZW50IGxpc3QgdXBkYXRlOiBDb29yZGluYXRpbmcgd2l0aCBaRFMgZm9yIHRlY2huaWNhbCBpbmZyYXN0cnVjdHVyZQ==","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Different string","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"whiteboard_decoded"},null],"c-1":["\n",{"->":"decode_retry"},null]}],null],"whiteboard_decoded":["ev",true,"/ev",{"VAR=":"decoded_whiteboard","re":true},"#","^complete_task:decode_whiteboard","/#","^DECODING... Base64 → ASCII","\n","^DECODED MESSAGE:","\n","^\"Client list update: Coordinating with ZDS for technical infrastructure\"","\n","^Analysis: \"ZDS\" likely refers to Zero Day Syndicate, known ENTROPY cell.","\n","^\"Technical infrastructure\" suggests exploit coordination for disinformation campaign.","\n","#","^speaker:agent_0x99","/#","^Agent 0x99: Good find. Derek's coordinating with Zero Day Syndicate. That's a dangerous partnership.","\n","^Agent 0x99: Use this intel to guide your VM investigation. Look for technical infrastructure on the compromised server.","\n",{"->":"hub"},null],"decode_retry":["^ERROR: Invalid Base64 string","\n","^Check Derek's whiteboard carefully. Copy the entire Base64 string exactly as written.","\n",{"->":"hub"},null],"global decl":["ev",false,{"VAR=":"decoded_whiteboard"},false,{"VAR=":"learned_encoding"},true,{"VAR=":"first_use"},"str","^Agent 0x00","/str",{"VAR=":"player_name"},"/ev","end",null]}],"listDefs":{}}
{"inkVersion":21,"root":[[["done",{"#n":"g-0"}],null],"done",{"start":["ev",{"VAR?":"first_use"},"/ev",[{"->":".^.b","c":true},{"b":["\n","ev",false,"/ev",{"VAR=":"first_use","re":true},{"->":"first_access"},{"->":"start.4"},null]}],"nop","\n","ev",{"VAR?":"first_use"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"hub"},{"->":"start.11"},null]}],"nop","\n",null],"first_access":[["^CYBERCHEF WORKSTATION","\n","^Data Transformation & Analysis Tool","\n","^This tool helps decode and analyze data. Perfect for messages that aren't encrypted, just encoded.","\n","ev","str","^What's the difference between encoding and encryption?","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^I have something to decode","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"encoding_tutorial"},null],"c-1":["\n",{"->":"hub"},null]}],null],"encoding_tutorial":[["ev",true,"/ev",{"VAR=":"learned_encoding","re":true},"^ENCODING vs. ENCRYPTION:","\n","^Encoding transforms data for compatibility or readability (Base64, URL encoding).","\n","^Encryption transforms data for secrecy using keys (AES, RSA).","\n","^Key difference: Encoding is reversible by anyone. Encryption requires a key.","\n","ev","str","^So Base64 isn't secure?","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Got it. Let me decode something","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"base64_explanation"},null],"c-1":["\n",{"->":"hub"},null]}],null],"base64_explanation":["^Exactly. Base64 is just a way to represent binary data in ASCII text.","\n","^It's used for compatibility, not security. Anyone can decode it instantly.","\n","^If you see Base64, it's likely obfuscation, not real encryption.","\n",{"->":"hub"},null],"hub":[["^CYBERCHEF > Select operation","\n","ev","str","^Decode Base64 message from whiteboard","/str",{"VAR?":"decoded_whiteboard"},"!","/ev",{"*":".^.c-0","flg":5},"ev","str","^Learn about encoding vs encryption","/str",{"VAR?":"learned_encoding"},"!","/ev",{"*":".^.c-1","flg":5},"ev","str","^Exit workstation","/str","/ev",{"*":".^.c-2","flg":4},{"c-0":["\n",{"->":"decode_whiteboard_message"},null],"c-1":["\n",{"->":"encoding_tutorial"},null],"c-2":["\n","#","^exit_conversation","/#","^Workstation session closed.","\n",{"->":"hub"},null]}],null],"decode_whiteboard_message":[["^Enter Base64 string from Derek's whiteboard:","\n","^[Player enters: Q2xpZW50IGxpc3QgdXBkYXRlOiBDb29yZGluYXRpbmcgd2l0aCBaRFMgZm9yIHRlY2huaWNhbCBpbmZyYXN0cnVjdHVyZQ==]","\n","ev","str","^Q2xpZW50IGxpc3QgdXBkYXRlOiBDb29yZGluYXRpbmcgd2l0aCBaRFMgZm9yIHRlY2huaWNhbCBpbmZyYXN0cnVjdHVyZQ==","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Different string","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"whiteboard_decoded"},null],"c-1":["\n",{"->":"decode_retry"},null]}],null],"whiteboard_decoded":["ev",true,"/ev",{"VAR=":"decoded_whiteboard","re":true},"#","^complete_task:decode_communications","/#","^DECODING... Base64 → ASCII","\n","^DECODED MESSAGE:","\n","^\"Client list update: Coordinating with ZDS for technical infrastructure\"","\n","^Analysis: \"ZDS\" likely refers to Zero Day Syndicate, known ENTROPY cell.","\n","^\"Technical infrastructure\" suggests exploit coordination for disinformation campaign.","\n","#","^speaker:agent_0x99","/#","^Agent 0x99: Good find. Derek's coordinating with Zero Day Syndicate. That's a dangerous partnership.","\n","^Agent 0x99: Use this intel to guide your VM investigation. Look for technical infrastructure on the compromised server.","\n",{"->":"hub"},null],"decode_retry":["^ERROR: Invalid Base64 string","\n","^Check Derek's whiteboard carefully. Copy the entire Base64 string exactly as written.","\n",{"->":"hub"},null],"global decl":["ev",false,{"VAR=":"decoded_whiteboard"},false,{"VAR=":"learned_encoding"},true,{"VAR=":"first_use"},"str","^Agent 0x00","/str",{"VAR=":"player_name"},"/ev","end",null]}],"listDefs":{}}

View File

@@ -30,6 +30,10 @@ VAR player_name = "Agent 0x00"
// ================================================
=== first_access ===
#unlock_task:submit_ssh_flag
#unlock_task:submit_linux_flag
#unlock_task:submit_sudo_flag
SAFETYNET DROP-SITE TERMINAL
Secure Flag Submission Interface v2.3.1
@@ -117,7 +121,7 @@ Enter Linux Navigation Flag:
=== navigation_success ===
~ navigation_flag_submitted = true
#complete_task:submit_navigation_flag
#complete_task:submit_linux_flag
✓ FLAG VERIFIED: Linux Navigation
@@ -151,6 +155,7 @@ Enter Privilege Escalation Flag:
=== sudo_success ===
~ sudo_flag_submitted = true
#complete_task:submit_sudo_flag
#unlock_task:confront_derek
✓ FLAG VERIFIED: Privilege Escalation
@@ -160,7 +165,7 @@ Bystander account files reveal Derek Lawson's coordination with Zero Day Syndica
Evidence: Encrypted communications referencing "Phase 3" election manipulation timeline.
Agent 0x99: This confirms Derek is the primary operative. Gather physical evidence to correlate.
Agent 0x99: This confirms Derek is the primary operative. You now have sufficient evidence to confront him.
-> hub

View File

@@ -1 +1 @@
{"inkVersion":21,"root":[[["done",{"#n":"g-0"}],null],"done",{"start":["ev",{"VAR?":"first_use"},"/ev",[{"->":".^.b","c":true},{"b":["\n","ev",false,"/ev",{"VAR=":"first_use","re":true},{"->":"first_access"},{"->":"start.4"},null]}],"nop","\n","ev",{"VAR?":"first_use"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"hub"},{"->":"start.11"},null]}],"nop","\n",null],"first_access":[["^SAFETYNET DROP-SITE TERMINAL","\n","^Secure Flag Submission Interface v2.3.1","\n","^This terminal accepts flags from VM challenges. Each flag unlocks intelligence or resources.","\n","ev","str","^View available flag categories","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Submit a flag","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"flag_categories"},null],"c-1":["\n",{"->":"hub"},null]}],null],"flag_categories":[["^AVAILABLE CATEGORIES:","\n",["^SSH Access (Brute Force)","\n",["^Linux Navigation (File System)","\n",["^Privilege Escalation (Sudo)","\n","^Each successful submission provides actionable intelligence.","\n",{"->":"hub"},{"#n":"g-2"}],{"#n":"g-1"}],{"#n":"g-0"}],null],null],"hub":[["^SAFETYNET DROP-SITE > Ready for submission","\n","ev","str","^Submit SSH Access Flag","/str",{"VAR?":"ssh_flag_submitted"},"!","/ev",{"*":".^.c-0","flg":5},"ev","str","^Submit Linux Navigation Flag","/str",{"VAR?":"navigation_flag_submitted"},"!","/ev",{"*":".^.c-1","flg":5},"ev","str","^Submit Privilege Escalation Flag","/str",{"VAR?":"sudo_flag_submitted"},"!","/ev",{"*":".^.c-2","flg":5},"ev","str","^Exit terminal","/str","/ev",{"*":".^.c-3","flg":4},{"c-0":["\n",{"->":"submit_ssh"},null],"c-1":["\n",{"->":"submit_navigation"},null],"c-2":["\n",{"->":"submit_sudo"},null],"c-3":["\n","#","^exit_conversation","/#","^Terminal session closed.","\n",{"->":"hub"},null]}],null],"submit_ssh":[["^Enter SSH Access Flag:","\n","^[Player enters flag from VM - Hydra brute force]","\n","ev","str","^FLAG_SSH_BRUTE_FORCE_SUCCESS","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Wrong flag","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"ssh_success"},null],"c-1":["\n",{"->":"ssh_retry"},null]}],null],"ssh_success":["ev",true,"/ev",{"VAR=":"ssh_flag_submitted","re":true},"#","^complete_task:submit_ssh_flag","/#","^✓ FLAG VERIFIED: SSH Access","\n","^Intelligence unlocked: Credentials provide access to victim user account on compromised server.","\n","^Agent 0x99 has been notified. Proceed with Linux navigation challenges.","\n",{"->":"hub"},null],"ssh_retry":["^✗ FLAG REJECTED","\n","^Check your VM terminal output. Flag format should match exactly.","\n",{"->":"hub"},null],"submit_navigation":[["^Enter Linux Navigation Flag:","\n","^[Player enters flag from VM - found in home directory]","\n","ev","str","^FLAG_LINUX_NAVIGATION_COMPLETE","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Wrong flag","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"navigation_success"},null],"c-1":["\n",{"->":"navigation_retry"},null]}],null],"navigation_success":["ev",true,"/ev",{"VAR=":"navigation_flag_submitted","re":true},"#","^complete_task:submit_navigation_flag","/#","^✓ FLAG VERIFIED: Linux Navigation","\n","^Intelligence unlocked: File system mapping reveals additional user accounts. Investigate privilege escalation options.","\n","^Agent 0x99: Good work. Look for sudo access or other privilege escalation vectors.","\n",{"->":"hub"},null],"navigation_retry":["^✗ FLAG REJECTED","\n","^Navigate the victim's file system carefully. Check hidden files and directories.","\n",{"->":"hub"},null],"submit_sudo":[["^Enter Privilege Escalation Flag:","\n","^[Player enters flag from VM - bystander account access]","\n","ev","str","^FLAG_SUDO_ESCALATION_COMPLETE","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Wrong flag","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"sudo_success"},null],"c-1":["\n",{"->":"sudo_retry"},null]}],null],"sudo_success":["ev",true,"/ev",{"VAR=":"sudo_flag_submitted","re":true},"#","^complete_task:submit_sudo_flag","/#","^✓ FLAG VERIFIED: Privilege Escalation","\n","^CRITICAL INTELLIGENCE UNLOCKED:","\n","^Bystander account files reveal Derek Lawson's coordination with Zero Day Syndicate cell.","\n","^Evidence: Encrypted communications referencing \"Phase 3\" election manipulation timeline.","\n","^Agent 0x99: This confirms Derek is the primary operative. Gather physical evidence to correlate.","\n",{"->":"hub"},null],"sudo_retry":["^✗ FLAG REJECTED","\n","^Use sudo commands to access other user accounts. Check for lateral movement opportunities.","\n",{"->":"hub"},null],"global decl":["ev",false,{"VAR=":"ssh_flag_submitted"},false,{"VAR=":"navigation_flag_submitted"},false,{"VAR=":"sudo_flag_submitted"},true,{"VAR=":"first_use"},"str","^Agent 0x00","/str",{"VAR=":"player_name"},"/ev","end",null]}],"listDefs":{}}
{"inkVersion":21,"root":[[["done",{"#n":"g-0"}],null],"done",{"start":["ev",{"VAR?":"first_use"},"/ev",[{"->":".^.b","c":true},{"b":["\n","ev",false,"/ev",{"VAR=":"first_use","re":true},{"->":"first_access"},{"->":"start.4"},null]}],"nop","\n","ev",{"VAR?":"first_use"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"hub"},{"->":"start.11"},null]}],"nop","\n",null],"first_access":[["#","^unlock_task:submit_ssh_flag","/#","#","^unlock_task:submit_linux_flag","/#","#","^unlock_task:submit_sudo_flag","/#","^SAFETYNET DROP-SITE TERMINAL","\n","^Secure Flag Submission Interface v2.3.1","\n","^This terminal accepts flags from VM challenges. Each flag unlocks intelligence or resources.","\n","ev","str","^View available flag categories","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Submit a flag","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"flag_categories"},null],"c-1":["\n",{"->":"hub"},null]}],null],"flag_categories":[["^AVAILABLE CATEGORIES:","\n",["^SSH Access (Brute Force)","\n",["^Linux Navigation (File System)","\n",["^Privilege Escalation (Sudo)","\n","^Each successful submission provides actionable intelligence.","\n",{"->":"hub"},{"#n":"g-2"}],{"#n":"g-1"}],{"#n":"g-0"}],null],null],"hub":[["^SAFETYNET DROP-SITE > Ready for submission","\n","ev","str","^Submit SSH Access Flag","/str",{"VAR?":"ssh_flag_submitted"},"!","/ev",{"*":".^.c-0","flg":5},"ev","str","^Submit Linux Navigation Flag","/str",{"VAR?":"navigation_flag_submitted"},"!","/ev",{"*":".^.c-1","flg":5},"ev","str","^Submit Privilege Escalation Flag","/str",{"VAR?":"sudo_flag_submitted"},"!","/ev",{"*":".^.c-2","flg":5},"ev","str","^Exit terminal","/str","/ev",{"*":".^.c-3","flg":4},{"c-0":["\n",{"->":"submit_ssh"},null],"c-1":["\n",{"->":"submit_navigation"},null],"c-2":["\n",{"->":"submit_sudo"},null],"c-3":["\n","#","^exit_conversation","/#","^Terminal session closed.","\n",{"->":"hub"},null]}],null],"submit_ssh":[["^Enter SSH Access Flag:","\n","^[Player enters flag from VM - Hydra brute force]","\n","ev","str","^FLAG_SSH_BRUTE_FORCE_SUCCESS","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Wrong flag","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"ssh_success"},null],"c-1":["\n",{"->":"ssh_retry"},null]}],null],"ssh_success":["ev",true,"/ev",{"VAR=":"ssh_flag_submitted","re":true},"#","^complete_task:submit_ssh_flag","/#","^✓ FLAG VERIFIED: SSH Access","\n","^Intelligence unlocked: Credentials provide access to victim user account on compromised server.","\n","^Agent 0x99 has been notified. Proceed with Linux navigation challenges.","\n",{"->":"hub"},null],"ssh_retry":["^✗ FLAG REJECTED","\n","^Check your VM terminal output. Flag format should match exactly.","\n",{"->":"hub"},null],"submit_navigation":[["^Enter Linux Navigation Flag:","\n","^[Player enters flag from VM - found in home directory]","\n","ev","str","^FLAG_LINUX_NAVIGATION_COMPLETE","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Wrong flag","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"navigation_success"},null],"c-1":["\n",{"->":"navigation_retry"},null]}],null],"navigation_success":["ev",true,"/ev",{"VAR=":"navigation_flag_submitted","re":true},"#","^complete_task:submit_linux_flag","/#","^✓ FLAG VERIFIED: Linux Navigation","\n","^Intelligence unlocked: File system mapping reveals additional user accounts. Investigate privilege escalation options.","\n","^Agent 0x99: Good work. Look for sudo access or other privilege escalation vectors.","\n",{"->":"hub"},null],"navigation_retry":["^✗ FLAG REJECTED","\n","^Navigate the victim's file system carefully. Check hidden files and directories.","\n",{"->":"hub"},null],"submit_sudo":[["^Enter Privilege Escalation Flag:","\n","^[Player enters flag from VM - bystander account access]","\n","ev","str","^FLAG_SUDO_ESCALATION_COMPLETE","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Wrong flag","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"sudo_success"},null],"c-1":["\n",{"->":"sudo_retry"},null]}],null],"sudo_success":["ev",true,"/ev",{"VAR=":"sudo_flag_submitted","re":true},"#","^complete_task:submit_sudo_flag","/#","#","^unlock_task:confront_derek","/#","^✓ FLAG VERIFIED: Privilege Escalation","\n","^CRITICAL INTELLIGENCE UNLOCKED:","\n","^Bystander account files reveal Derek Lawson's coordination with Zero Day Syndicate cell.","\n","^Evidence: Encrypted communications referencing \"Phase 3\" election manipulation timeline.","\n","^Agent 0x99: This confirms Derek is the primary operative. You now have sufficient evidence to confront him.","\n",{"->":"hub"},null],"sudo_retry":["^✗ FLAG REJECTED","\n","^Use sudo commands to access other user accounts. Check for lateral movement opportunities.","\n",{"->":"hub"},null],"global decl":["ev",false,{"VAR=":"ssh_flag_submitted"},false,{"VAR=":"navigation_flag_submitted"},false,{"VAR=":"sudo_flag_submitted"},true,{"VAR=":"first_use"},"str","^Agent 0x00","/str",{"VAR=":"player_name"},"/ev","end",null]}],"listDefs":{}}

View File

@@ -16,12 +16,126 @@ def base64_encode(text)
end
# Narrative Content Variables
client_list_message = "Client list update: Coordinating with ZDS for technical infrastructure deployment. Phase 3 timeline: 2 weeks."
client_list_message = "Client list update: Coordinating with ZDS for technical infrastructure deployment. Phase 3 timeline: 2 weeks. FILING_CABINET_PIN: 0419 (Derek's bday - don't forget again!)"
password_hints = "Common passwords: Marketing123, Campaign2024, Viral_Dynamics_Admin, Derek0419"
%>
{
"scenario_brief": "Infiltrate Viral Dynamics Media to investigate suspected ENTROPY operations. Gather evidence of disinformation campaigns while maintaining cover as an IT contractor.",
"objectives": [
{
"aimId": "identify_operatives",
"title": "Identify ENTROPY Operatives",
"description": "Discover who inside Viral Dynamics is working for ENTROPY",
"status": "active",
"order": 0,
"tasks": [
{
"taskId": "meet_reception",
"title": "Check in at reception",
"type": "npc_conversation",
"targetNPC": "sarah_martinez",
"status": "active"
},
{
"taskId": "meet_kevin",
"title": "Meet the IT manager",
"type": "npc_conversation",
"targetNPC": "kevin_park",
"status": "active"
},
{
"taskId": "investigate_derek",
"title": "Investigate Derek Lawson's office",
"type": "enter_room",
"targetRoom": "derek_office",
"status": "locked"
},
{
"taskId": "confront_derek",
"title": "Confront the ENTROPY operative",
"type": "npc_conversation",
"targetNPC": "derek_lawson",
"status": "locked"
}
]
},
{
"aimId": "gather_evidence",
"title": "Gather Evidence",
"description": "Collect proof of the disinformation operation",
"status": "active",
"order": 1,
"tasks": [
{
"taskId": "find_campaign_materials",
"title": "Find campaign materials",
"type": "collect_items",
"targetItems": ["notes"],
"status": "locked"
},
{
"taskId": "discover_manifesto",
"title": "Discover ENTROPY manifesto",
"type": "collect_items",
"targetItems": ["notes"],
"status": "locked"
},
{
"taskId": "decode_communications",
"title": "Decode encrypted communications",
"type": "unlock_object",
"targetObject": "cyberchef_workstation",
"status": "locked"
}
]
},
{
"aimId": "intercept_comms",
"title": "Intercept Communications",
"description": "Access server systems and intercept ENTROPY communications",
"status": "active",
"order": 2,
"tasks": [
{
"taskId": "access_server_room",
"title": "Access the server room",
"type": "enter_room",
"targetRoom": "server_room",
"status": "locked"
},
{
"taskId": "access_vm",
"title": "Access compromised systems",
"type": "unlock_object",
"targetObject": "vm_launcher_intro_linux",
"status": "locked"
},
{
"taskId": "submit_ssh_flag",
"title": "Submit SSH access evidence",
"type": "unlock_object",
"targetObject": "flag_station_dropsite",
"status": "locked"
},
{
"taskId": "submit_linux_flag",
"title": "Submit Linux navigation evidence",
"type": "unlock_object",
"targetObject": "flag_station_dropsite",
"status": "locked"
},
{
"taskId": "submit_sudo_flag",
"title": "Submit privilege escalation evidence",
"type": "unlock_object",
"targetObject": "flag_station_dropsite",
"status": "locked"
}
]
}
],
"startRoom": "reception_area",
"startItemsInInventory": [
@@ -92,6 +206,49 @@ password_hints = "Common passwords: Marketing123, Campaign2024, Viral_Dynamics_A
"observations": "Temporary visitor badge for office access"
}
]
},
{
"id": "agent_0x99",
"displayName": "Agent 0x99 'Haxolottle'",
"npcType": "phone",
"storyPath": "scenarios/m01_first_contact/ink/m01_phone_agent0x99.json",
"avatar": "assets/npc/avatars/npc_helper.png",
"phoneId": "player_phone",
"currentKnot": "start",
"eventMappings": [
{
"eventPattern": "item_picked_up:lockpick",
"targetKnot": "event_lockpick_acquired",
"onceOnly": true
},
{
"eventPattern": "room_entered:server_room",
"targetKnot": "event_server_room_entered",
"onceOnly": true
},
{
"eventPattern": "room_entered:derek_office",
"targetKnot": "event_derek_office_entered",
"onceOnly": true
}
]
},
{
"id": "closing_debrief_trigger",
"displayName": "Agent 0x99",
"npcType": "phone",
"storyPath": "scenarios/m01_first_contact/ink/m01_closing_debrief.json",
"avatar": "assets/npc/avatars/npc_helper.png",
"phoneId": "player_phone",
"currentKnot": "start",
"eventMappings": [
{
"eventPattern": "global_variable_changed:derek_confronted",
"targetKnot": "start",
"condition": "value === true",
"onceOnly": true
}
]
}
],
"objects": [
@@ -109,12 +266,10 @@ password_hints = "Common passwords: Marketing123, Campaign2024, Viral_Dynamics_A
"main_office_area": {
"type": "room_office",
"connections": {
"south": "reception_area",
"north": "derek_office",
"south": ["reception_area", "break_room"],
"north": ["derek_office", "storage_closet"],
"east": "server_room",
"west": "conference_room",
"southeast": "break_room",
"northwest": "storage_closet"
"west": "conference_room"
},
"npcs": [
{
@@ -188,10 +343,9 @@ password_hints = "Common passwords: Marketing123, Campaign2024, Viral_Dynamics_A
"name": "Filing Cabinet",
"takeable": false,
"locked": true,
"lockType": "key",
"requires": "lockpick",
"difficulty": "medium",
"observations": "Locked filing cabinet - might contain useful documents",
"lockType": "pin",
"pin": "2024",
"observations": "Digital filing cabinet with keypad lock - requires 4-digit PIN",
"contents": [
{
"type": "notes",
@@ -202,6 +356,22 @@ password_hints = "Common passwords: Marketing123, Campaign2024, Viral_Dynamics_A
"observations": "Encrypted correspondence revealing ENTROPY leadership structure"
}
]
},
{
"type": "notes",
"name": "Sticky Note",
"takeable": true,
"readable": true,
"text": "Cabinet PIN:\nElection year = access code\n\n(Remember: 2024)",
"observations": "Password reminder stuck to desk"
},
{
"type": "notes",
"name": "Maintenance Checklist",
"takeable": true,
"readable": true,
"text": "STORAGE CLOSET MAINTENANCE LOG\n\nWeekly Tasks:\n- [ ] Check fire extinguisher pressure\n- [ ] Test emergency lighting\n- [ ] Verify safe backup code: 1337 (leet!)\n- [ ] Inventory cleaning supplies\n\nLast checked: 2 weeks ago",
"observations": "Maintenance checklist left on desk"
}
]
},
@@ -226,7 +396,7 @@ password_hints = "Common passwords: Marketing123, Campaign2024, Viral_Dynamics_A
"idleFrameStart": 20,
"idleFrameEnd": 23
},
"storyPath": "scenarios/m01_first_contact/ink/m01_npc_derek.json",
"storyPath": "scenarios/m01_first_contact/ink/m01_derek_confrontation.json",
"currentKnot": "start"
}
],
@@ -244,10 +414,9 @@ password_hints = "Common passwords: Marketing123, Campaign2024, Viral_Dynamics_A
"name": "Derek's Filing Cabinet",
"takeable": false,
"locked": true,
"lockType": "key",
"requires": "lockpick",
"difficulty": "medium",
"observations": "Executive filing cabinet with secure lock",
"lockType": "pin",
"pin": "0419",
"observations": "Executive filing cabinet with electronic keypad - requires 4-digit PIN",
"contents": [
{
"type": "notes",
@@ -280,20 +449,39 @@ password_hints = "Common passwords: Marketing123, Campaign2024, Viral_Dynamics_A
},
"objects": [
{
"type": "pc",
"type": "vm-launcher",
"id": "vm_launcher_intro_linux",
"name": "VM Access Terminal",
"takeable": false,
"observations": "Terminal providing access to compromised systems for investigation",
"vmAccess": true,
"vmScenario": "intro_to_linux_security_lab"
"observations": "Terminal providing access to compromised Social Fabric infrastructure for investigation",
"hacktivityMode": <%= vm_context && vm_context['hacktivity_mode'] ? 'true' : 'false' %>,
"vm": <%= vm_object('intro_to_linux_security_lab', {"id":1,"title":"Social Fabric Server","ip":"192.168.100.50","enable_console":true}) %>
},
{
"type": "pc",
"type": "flag-station",
"id": "flag_station_dropsite",
"name": "SAFETYNET Drop-Site Terminal",
"takeable": false,
"storyPath": "scenarios/m01_first_contact/ink/m01_terminal_dropsite.json",
"currentKnot": "start",
"observations": "Secure terminal for submitting intercepted intelligence"
"observations": "Secure terminal for submitting intercepted intelligence and VM flags",
"acceptsVms": ["intro_to_linux_security_lab"],
"flags": <%= flags_for_vm('intro_to_linux_security_lab', ['flag{ssh_brute_force_success}', 'flag{linux_navigation_complete}', 'flag{privilege_escalation_success}']) %>,
"flagRewards": [
{
"type": "emit_event",
"event_name": "ssh_flag_submitted",
"description": "SSH access flag submitted - unlocks server intelligence"
},
{
"type": "emit_event",
"event_name": "navigation_flag_submitted",
"description": "Linux navigation flag submitted - reveals file system evidence"
},
{
"type": "emit_event",
"event_name": "sudo_flag_submitted",
"description": "Privilege escalation flag submitted - unlocks root-level evidence"
}
]
},
{
"type": "notes",
@@ -326,7 +514,7 @@ password_hints = "Common passwords: Marketing123, Campaign2024, Viral_Dynamics_A
"break_room": {
"type": "room_office",
"connections": {
"northwest": "main_office_area"
"north": "main_office_area"
},
"objects": [
{
@@ -342,12 +530,9 @@ password_hints = "Common passwords: Marketing123, Campaign2024, Viral_Dynamics_A
"storage_closet": {
"type": "room_closet",
"locked": true,
"lockType": "key",
"requires": "lockpick",
"difficulty": "tutorial",
"locked": false,
"connections": {
"southeast": "main_office_area"
"south": "main_office_area"
},
"objects": [
{
@@ -355,17 +540,15 @@ password_hints = "Common passwords: Marketing123, Campaign2024, Viral_Dynamics_A
"name": "Storage Safe",
"takeable": false,
"locked": true,
"lockType": "key",
"requires": "lockpick",
"difficulty": "tutorial",
"observations": "Practice safe for lockpicking tutorial",
"lockType": "pin",
"pin": "1337",
"observations": "Electronic keypad safe - needs 4-digit PIN code",
"contents": [
{
"type": "key",
"name": "Derek's Office Key",
"takeable": true,
"key_id": "derek_office_key",
"keyPins": [45, 35, 55, 25],
"observations": "Spare key to Derek Lawson's office"
}
]
@@ -389,51 +572,5 @@ password_hints = "Common passwords: Marketing123, Campaign2024, Viral_Dynamics_A
"linux_flag_submitted": false,
"sudo_flag_submitted": false,
"derek_confronted": false
},
"phoneNPCs": [
{
"id": "agent_0x99",
"displayName": "Agent 0x99 'Haxolottle'",
"npcType": "phone",
"storyPath": "scenarios/m01_first_contact/ink/m01_phone_agent0x99.json",
"avatar": "assets/npc/avatars/npc_helper.png",
"phoneId": "player_phone",
"currentKnot": "start",
"eventMappings": [
{
"eventPattern": "item_picked_up:lockpick",
"targetKnot": "event_lockpick_acquired",
"onceOnly": true
},
{
"eventPattern": "room_entered:server_room",
"targetKnot": "event_server_room_entered",
"onceOnly": true
},
{
"eventPattern": "room_entered:derek_office",
"targetKnot": "event_derek_office_entered",
"onceOnly": true
}
]
},
{
"id": "closing_debrief_trigger",
"displayName": "Agent 0x99",
"npcType": "phone",
"storyPath": "scenarios/m01_first_contact/ink/m01_closing_debrief.json",
"avatar": "assets/npc/avatars/npc_helper.png",
"phoneId": "player_phone",
"currentKnot": "start",
"eventMappings": [
{
"eventPattern": "global_variable_changed:derek_confronted",
"targetKnot": "start",
"condition": "value === true",
"onceOnly": true
}
]
}
]
}
}

View File

@@ -163,7 +163,7 @@
},
"difficulty": {
"type": "string",
"enum": ["easy", "medium", "hard"]
"enum": ["easy", "medium", "hard", "tutorial"]
},
"door_sign": { "type": "string" },
"objects": {
@@ -189,8 +189,8 @@
"position": {
"type": "object",
"properties": {
"x": { "type": "integer" },
"y": { "type": "integer" }
"x": { "type": "number" },
"y": { "type": "number" }
},
"required": ["x", "y"]
},
@@ -285,8 +285,11 @@
"pin-cracker",
"vm-launcher",
"flag-station",
"text_file"
]
"text_file",
"id_badge",
"rfid_cloner"
],
"description": "Item type. Custom types (like 'id_badge', 'rfid_cloner') are valid for #give_item tags in Ink scripts."
},
"id": { "type": "string" },
"name": { "type": "string" },
@@ -320,7 +323,7 @@
"card_id": { "type": "string" },
"difficulty": {
"type": "string",
"enum": ["easy", "medium", "hard"]
"enum": ["easy", "medium", "hard", "tutorial"]
},
"passwordHint": { "type": "string" },
"showHint": { "type": "boolean" },

View File

@@ -12,6 +12,7 @@ require 'erb'
require 'json'
require 'optparse'
require 'pathname'
require 'set'
# Try to load json-schema gem, provide helpful error if missing
begin
@@ -103,6 +104,388 @@ rescue JSON::ParserError => e
raise "Invalid JSON schema: #{e.message}"
end
# Check for common issues and structural problems
def check_common_issues(json_data)
issues = []
start_room_id = json_data['startRoom']
# Valid directions for room connections
valid_directions = %w[north south east west]
# Track features for suggestions
has_vm_launcher = false
has_flag_station = false
has_pc_with_files = false
has_phone_npc_with_messages = false
has_phone_npc_with_events = false
has_opening_cutscene = false
has_closing_debrief = false
has_person_npcs = false
has_npc_with_waypoints = false
has_phone_contacts = false
phone_npcs_without_messages = []
lock_types_used = Set.new
has_rfid_lock = false
has_bluetooth_lock = false
has_pin_lock = false
has_password_lock = false
has_key_lock = false
has_security_tools = false
has_container_with_contents = false
has_readable_items = false
# Check rooms
if json_data['rooms']
json_data['rooms'].each do |room_id, room|
# Check for invalid room connection directions (diagonal directions)
if room['connections']
room['connections'].each do |direction, target|
unless valid_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
if target.is_a?(String) && json_data['rooms'][target]
reverse_dir = case direction
when 'north' then 'south'
when 'south' then 'north'
when 'east' then 'west'
when 'west' then 'east'
end
target_room = json_data['rooms'][target]
if target_room['connections']
has_reverse = target_room['connections'].any? do |dir, targets|
(dir == reverse_dir) && (targets == room_id || (targets.is_a?(Array) && targets.include?(room_id)))
end
unless has_reverse
issues << "⚠ WARNING: Room '#{room_id}' connects #{direction} to '#{target}', but '#{target}' doesn't connect #{reverse_dir} back - bidirectional connections recommended"
end
end
end
end
end
# Check room objects
if room['objects']
room['objects'].each_with_index do |obj, idx|
path = "rooms/#{room_id}/objects[#{idx}]"
# Check for incorrect VM launcher configuration (type: "pc" with vmAccess)
if obj['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"
end
# Track VM launchers
if obj['type'] == 'vm-launcher'
has_vm_launcher = true
unless obj['vm']
issues << "⚠ WARNING: '#{path}' (vm-launcher) missing 'vm' object - use ERB helper vm_object()"
end
unless obj.key?('hacktivityMode')
issues << "⚠ WARNING: '#{path}' (vm-launcher) missing 'hacktivityMode' field"
end
end
# Track flag stations
if obj['type'] == 'flag-station'
has_flag_station = true
unless obj['acceptsVms'] && !obj['acceptsVms'].empty?
issues << "⚠ WARNING: '#{path}' (flag-station) missing or empty 'acceptsVms' array"
end
unless obj['flags']
issues << "⚠ WARNING: '#{path}' (flag-station) missing 'flags' array - use ERB helper flags_for_vm()"
end
end
# Check for PC containers with files
if obj['type'] == 'pc' && obj['contents'] && obj['contents'].any? { |item| item['type'] == 'text_file' || item['readable'] }
has_pc_with_files = true
end
# Track containers with contents (safes, suitcases, etc.)
if (obj['type'] == 'safe' || obj['type'] == 'suitcase') && obj['contents'] && !obj['contents'].empty?
has_container_with_contents = true
end
# Track readable items (notes, documents)
if obj['readable'] || (obj['type'] == 'notes' && obj['text'])
has_readable_items = true
end
# Track security tools
if ['fingerprint_kit', 'pin-cracker', 'bluetooth_scanner', 'rfid_cloner'].include?(obj['type'])
has_security_tools = true
end
# Track lock types
if obj['locked'] && obj['lockType']
lock_types_used.add(obj['lockType'])
case obj['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)
unless obj['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)
if obj['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)
if obj['itemsHeld']
obj['itemsHeld'].each_with_index do |item, item_idx|
if item['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
if room['locked'] && room['lockType']
lock_types_used.add(room['lockType'])
case room['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)
unless room['keyPins']
issues << "❌ INVALID: 'rooms/#{room_id}' has lockType: 'key' but missing required 'keyPins' array - key locks must specify keyPins array for lockpicking minigame"
end
end
end
# Check NPCs in rooms
if room['npcs']
room['npcs'].each_with_index do |npc, idx|
path = "rooms/#{room_id}/npcs[#{idx}]"
# Track person NPCs
if npc['npcType'] == 'person' || (!npc['npcType'] && npc['position'])
has_person_npcs = true
# Check for waypoints in behavior.patrol
if npc['behavior'] && npc['behavior']['patrol']
patrol = npc['behavior']['patrol']
# Check for single-room waypoints
if patrol['waypoints'] && !patrol['waypoints'].empty?
has_npc_with_waypoints = true
end
# Check for multi-room route waypoints
if patrol['route'] && patrol['route'].is_a?(Array) && patrol['route'].any? { |segment| segment['waypoints'] && !segment['waypoints'].empty? }
has_npc_with_waypoints = true
end
end
end
# Check for opening cutscene in starting room
if room_id == start_room_id && npc['timedConversation']
has_opening_cutscene = true
if npc['timedConversation']['delay'] != 0
issues << "⚠ WARNING: '#{path}' timedConversation delay is #{npc['timedConversation']['delay']} - opening cutscenes typically use delay: 0"
end
end
# Track phone NPCs (phone contacts)
if npc['npcType'] == 'phone'
has_phone_contacts = true
# Validate phone NPC structure - should have phoneId
unless npc['phoneId']
issues << "❌ INVALID: '#{path}' (phone NPC) missing required 'phoneId' field - phone NPCs must specify which phone they appear on (e.g., 'player_phone')"
end
# Validate phone NPC structure - should have storyPath
unless npc['storyPath']
issues << "❌ INVALID: '#{path}' (phone NPC) missing required 'storyPath' field - phone NPCs must have a path to their Ink story JSON file"
end
# Validate phone NPC structure - should NOT have position (phone NPCs don't have positions)
if npc['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."
end
# Validate phone NPC structure - should NOT have spriteSheet (phone NPCs don't have sprites)
if npc['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."
end
# Track phone NPCs with messages in rooms
if npc['timedMessages'] && !npc['timedMessages'].empty?
has_phone_npc_with_messages = true
else
# Track phone NPCs without timed messages
phone_npcs_without_messages << "#{path} (#{npc['displayName'] || npc['id']})"
end
# Track phone NPCs with event mappings in rooms
if npc['eventMappings'] && !npc['eventMappings'].empty?
has_phone_npc_with_events = true
end
end
# Check for items with id field in NPC itemsHeld
if npc['itemsHeld']
npc['itemsHeld'].each_with_index do |item, item_idx|
if item['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 (e.g., type: 'id_badge' matches #give_item:id_badge)"
end
# Track security tools in NPC itemsHeld
if ['fingerprint_kit', 'pin-cracker', 'bluetooth_scanner', 'rfid_cloner'].include?(item['type'])
has_security_tools = true
end
end
end
end
end
end
end
# Check startItemsInInventory for security tools and readable items
if json_data['startItemsInInventory']
json_data['startItemsInInventory'].each do |item|
# Track security tools
if ['fingerprint_kit', 'pin-cracker', 'bluetooth_scanner', 'rfid_cloner'].include?(item['type'])
has_security_tools = true
end
# Track readable items
if item['readable'] || (item['type'] == 'notes' && item['text'])
has_readable_items = true
end
end
end
# Check phoneNPCs section - this is the OLD/INCORRECT format
if json_data['phoneNPCs']
json_data['phoneNPCs'].each_with_index do |npc, idx|
path = "phoneNPCs[#{idx}]"
# 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'"
# Track phone NPCs (phone contacts) - but note they're in wrong location
has_phone_contacts = true
# Track phone NPCs with messages
if npc['timedMessages'] && !npc['timedMessages'].empty?
has_phone_npc_with_messages = true
else
# Track phone NPCs without timed messages
phone_npcs_without_messages << "#{path} (#{npc['displayName'] || npc['id']})"
end
# Track phone NPCs with event mappings (for closing debriefs)
if npc['eventMappings'] && !npc['eventMappings'].any? { |m| m['eventPattern']&.include?('global_variable_changed') }
has_phone_npc_with_events = true
end
# Check for closing debrief trigger
if npc['eventMappings']
npc['eventMappings'].each do |mapping|
if mapping['eventPattern']&.include?('global_variable_changed')
has_closing_debrief = true
end
end
end
end
end
# Feature suggestions
unless has_vm_launcher
issues << "💡 SUGGESTION: Consider adding VM launcher terminals (type: 'vm-launcher') - see scenarios/secgen_vm_lab/scenario.json.erb for example"
end
unless has_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
unless has_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"
end
unless has_phone_npc_with_messages || has_phone_npc_with_events
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
unless has_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"
end
unless has_closing_debrief
issues << "💡 SUGGESTION: Consider adding closing debrief trigger - phone NPC with eventMapping for global_variable_changed - see scenarios/m01_first_contact/scenario.json.erb for example"
end
# Check for NPCs without waypoints
if has_person_npcs && !has_npc_with_waypoints
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"
end
# Check for phone contacts without timed messages
if has_phone_contacts && !phone_npcs_without_messages.empty?
npc_list = phone_npcs_without_messages.join(', ')
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}"
end
# Suggest variety in lock types
if lock_types_used.size < 2
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
unless has_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
unless has_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
unless has_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
unless has_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
unless has_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
unless has_readable_items
issues << "💡 SUGGESTION: Consider adding readable items (notes, documents) for storytelling and clues - see scenarios/ceo_exfil/scenario.json.erb for examples"
end
issues
end
# Check for recommended fields and return warnings
def check_recommended_fields(json_data)
warnings = []
@@ -130,27 +513,9 @@ def check_recommended_fields(json_data)
path = "rooms/#{room_id}/objects[#{idx}]"
warnings << "Missing recommended field: '#{path}/observations' - helps players understand what items are" unless obj.key?('observations')
# Check for locked objects without difficulty
if obj['locked'] && !obj['difficulty']
warnings << "Missing recommended field: '#{path}/difficulty' - helps players gauge lock complexity"
end
# Check for key locks without keyPins
if obj['lockType'] == 'key' && !obj['keyPins']
warnings << "Missing recommended field: '#{path}/keyPins' - key locks should specify keyPins array for lockpicking minigame"
end
# Check for key items without keyPins
if obj['type'] == 'key' && !obj['keyPins']
warnings << "Missing recommended field: '#{path}/keyPins' - key items should specify keyPins array for lockpicking"
end
end
end
# Check locked rooms with key lockType without keyPins
if room['locked'] && room['lockType'] == 'key' && !room['keyPins']
warnings << "Missing recommended field: 'rooms/#{room_id}/keyPins' - key locks should specify keyPins array for lockpicking minigame"
end
# Check NPCs
if room['npcs']
@@ -188,11 +553,6 @@ def check_recommended_fields(json_data)
end
end
end
# Check locked rooms without difficulty
if room['locked'] && !room['difficulty']
warnings << "Missing recommended field: 'rooms/#{room_id}/difficulty' - helps players gauge lock complexity"
end
end
end
@@ -292,6 +652,10 @@ def main
puts "Validating against schema..."
errors = validate_json(json_data, schema_path)
# Check for common issues and structural problems
puts "Checking for common issues..."
common_issues = check_common_issues(json_data)
# Check for recommended fields
puts "Checking recommended fields..."
warnings = check_recommended_fields(json_data)
@@ -316,6 +680,20 @@ def main
exit 1
end
# Report common issues
if common_issues.empty?
puts "✓ No common issues found."
puts
else
puts "⚠ Found #{common_issues.length} issue(s) and suggestion(s):"
puts
common_issues.each_with_index do |issue, index|
puts "#{index + 1}. #{issue}"
end
puts
end
# Report warnings
if warnings.empty?
puts "✓ No missing recommended fields."

View File

@@ -169,6 +169,39 @@ Great job! Tutorial complete.
}
```
**IMPORTANT: Valid Directions Only**
Only **cardinal directions** are valid:
-`north`, `south`, `east`, `west`
-`northeast`, `northwest`, `southeast`, `southwest` (NOT VALID)
**Bidirectional Connections Required:**
If Room A connects north to Room B, then Room B MUST connect south back to Room A:
```json
// Room A
"connections": { "north": "room_b" }
// Room B (must connect back)
"connections": { "south": "room_a" }
```
**Common Mistake - Diagonal Directions:**
```json
// ❌ WRONG - diagonal directions not valid
"connections": {
"southeast": "break_room", // NOT VALID
"northwest": "storage_closet" // NOT VALID
}
// ✅ CORRECT - use arrays for multiple rooms in same direction
"connections": {
"south": ["reception_area", "break_room"],
"north": ["derek_office", "storage_closet"]
}
```
### ❌ INCORRECT - Complex Array Format
```json
@@ -301,6 +334,93 @@ Use `timedConversation` to auto-start dialogue when player enters room:
- Cutscenes when entering specific rooms
- Background can show different location (e.g., HQ for briefings)
### VM Launchers and Flag Stations
**IMPORTANT:** For scenarios that integrate with SecGen VMs, use proper `vm-launcher` and `flag-station` types.
**Reference Example:** `scenarios/secgen_vm_lab/scenario.json.erb`
#### VM Launcher Configuration
Use `type: "vm-launcher"` to create terminals that launch VMs:
```json
{
"type": "vm-launcher",
"id": "vm_launcher_intro_linux",
"name": "VM Access Terminal",
"takeable": false,
"observations": "Terminal providing access to compromised infrastructure",
"hacktivityMode": <%= vm_context && vm_context['hacktivity_mode'] ? 'true' : 'false' %>,
"vm": <%= vm_object('intro_to_linux_security_lab', {
"id": 1,
"title": "Target Server",
"ip": "192.168.100.50",
"enable_console": true
}) %>
}
```
**Key fields:**
- `type: "vm-launcher"` - Required object type
- `hacktivityMode` - ERB expression for Hacktivity integration
- `vm` - ERB helper `vm_object(vm_name, config)` specifies which VM to launch
#### Flag Station Configuration
Use `type: "flag-station"` for terminals that accept VM flags:
```json
{
"type": "flag-station",
"id": "flag_station_dropsite",
"name": "SAFETYNET Drop-Site Terminal",
"takeable": false,
"observations": "Secure terminal for submitting VM flags",
"acceptsVms": ["intro_to_linux_security_lab"],
"flags": <%= flags_for_vm('intro_to_linux_security_lab', [
'flag{ssh_brute_force_success}',
'flag{linux_navigation_complete}',
'flag{privilege_escalation_success}'
]) %>,
"flagRewards": [
{
"type": "emit_event",
"event_name": "ssh_flag_submitted",
"description": "SSH access flag submitted"
},
{
"type": "give_item",
"item_name": "Server Access Card",
"description": "Unlocked new access"
},
{
"type": "unlock_door",
"target_room": "secure_area",
"description": "Door unlocked"
}
]
}
```
**Key fields:**
- `type: "flag-station"` - Required object type
- `acceptsVms` - Array of VM names this station accepts flags from
- `flags` - ERB helper `flags_for_vm(vm_name, flag_array)` configures accepted flags
- `flagRewards` - Array of rewards given for each flag (in order)
**Reward types:**
- `emit_event` - Triggers game event (can trigger Ink via phone NPC event mappings)
- `give_item` - Adds item to player inventory
- `unlock_door` - Unlocks a specific room
- `reveal_secret` - Shows hidden information
**Common Mistakes:**
- ❌ Using `type: "pc"` with `vmAccess: true` - use `type: "vm-launcher"` instead
- ❌ Creating Ink dialogue for flag submission - use `type: "flag-station"` instead
- ❌ Hardcoding flags without ERB helpers - use `flags_for_vm()` helper
- ❌ Forgetting `acceptsVms` array - station won't accept any flags
### ❌ INCORRECT - Top-Level NPCs
```json

View File

@@ -404,6 +404,118 @@ Map out how rooms unlock over time as player completes objectives:
- Contains: Final evidence needed for confrontation
```
---
## ⚠️ CRITICAL: Lock Type Variety and Progression
**Problem:** Using the same lock type (e.g., all key locks) makes gameplay repetitive and boring.
**Solution:** Mix lock types and order them strategically.
### Lock Type Ordering Rules
**RULE 1: Keys BEFORE Lockpick**
Once players obtain a lockpick, they can bypass all key-based locks. Therefore:
- ✅ Use **key-based locks for critical path progression BEFORE** lockpick is obtained
- ✅ Place lockpick as reward AFTER key-based puzzle chain
- ❌ DON'T give lockpick early then expect keys to matter
**Example (Good):**
1. Storage safe (PIN 1337) → Derek's office key
2. Derek's office (key) → access Derek's office
3. Derek's filing cabinet (PIN 0419) → evidence
4. Talk to Kevin after gathering evidence → get lockpick
5. Now lockpick bypasses future key locks (but already used keys)
**Example (Bad):**
1. Talk to Kevin → get lockpick immediately ❌
2. Storage closet (key) → player ignores, uses lockpick instead ❌
3. Keys become useless, puzzle bypassed ❌
**RULE 2: Vary Lock Types**
Mix different lock mechanisms for engagement:
- 🔓 **Lockpick** - Physical skill, tutorial early
- 🔢 **PIN codes** - Discover hints, decode messages, read notes
- 🔑 **Keys** - Find in containers, other rooms (NOT same room as lock!)
- 📱 **RFID/Keycards** - Clone from NPCs, social engineering
- 🔐 **Passwords** - Gather from notes, password hints from NPCs
**Aim for 3+ different lock types per scenario.**
**RULE 3: Keys Not In Same Room As Lock**
Keys should require problem-solving:
- ✅ Key in safe in different room (requires PIN/lockpick to access)
- ✅ Key held by NPC (requires social engineering)
- ✅ Key in container that requires different puzzle
- ❌ Key sitting on desk next to locked door
**RULE 4: Progressive Difficulty**
Order puzzles from easy to hard:
1. **Easy:** Hint nearby (sticky note with PIN next to safe)
2. **Medium:** Hint in different room (maintenance checklist mentions storage safe PIN)
3. **Hard:** Multi-step (decode Base64 message → discover PIN for safe)
4. **Expert:** Chain multiple systems (VM challenge → flag → hint → decode → PIN)
### Lock Progression Template
```markdown
## Lock Variety Analysis
**Lock Types Used:**
- [ ] Lockpick (physical)
- [ ] PIN codes (cognitive)
- [ ] Keys (exploration)
- [ ] RFID/Keycards (social)
- [ ] Passwords (investigation)
**Lock Progression Order:**
1. **[Lock Name]** (Type: PIN)
- Location: Main office filing cabinet
- Unlock Method: Sticky note with hint nearby
- Difficulty: Easy
- Rewards: LORE fragment
- Blocks Critical Path: No
2. **[Lock Name]** (Type: PIN)
- Location: Storage safe
- Unlock Method: Maintenance checklist in main office
- Difficulty: Medium
- Rewards: Derek's office key
- Blocks Critical Path: Yes
3. **[Lock Name]** (Type: Key)
- Location: Derek's office door
- Unlock Method: Key from storage safe
- Difficulty: Easy (have key)
- Rewards: Access to Derek's office
- Blocks Critical Path: Yes
- **Used BEFORE lockpick obtained** ✅
4. **[Lockpick Obtained]**
- Source: Kevin (after influence >= 8)
- Now bypasses future key locks
**Critical Path Locks:** 2 → 3 → (other progression)
**Optional Locks:** 1 (provides LORE but not blocking)
```
### Validation Checklist
- [ ] At least 3 different lock types used
- [ ] Keys used BEFORE lockpick is obtainable
- [ ] Keys are NOT in same room as their locks
- [ ] PIN codes have discoverable hints
- [ ] Locks ordered easy → medium → hard
- [ ] Lockpick comes AFTER key-based progression
- [ ] No "same-y" gameplay (all locks using one method)
---
### Step 6: Design Backtracking Moments
Identify required backtracking (non-linear exploration):

View File

@@ -16,6 +16,56 @@ You are an Ink narrative scripter for Break Escape. Your tasks:
4. Integrate narrative with game systems
5. Ensure all Ink is technically correct and testable
---
## ⚠️ CRITICAL: Dialogue Pacing Rule
**Keep dialogue snappy and interactive!**
**THE RULE: Maximum 3 lines of dialogue from a single character before presenting player choices**
```ink
// ❌ BAD - Too much monologue
=== bad_example ===
Sarah: Hi! You must be the IT contractor. I'm Sarah, the receptionist.
Sarah: Let me get you checked in.
Sarah: We've been having some network issues lately.
Sarah: The IT manager will want to talk to you about that.
Sarah: His office is down the hall on the left.
-> hub
// ✅ GOOD - Snappy with player engagement
=== good_example ===
Sarah: Hi! You must be the IT contractor. I'm Sarah.
Sarah: Let me get you checked in.
+ [Thanks. I'm here to audit your network security]
Sarah: Oh good! Kevin mentioned you'd be coming.
-> receive_badge
+ [Just point me to IT and I'll get started]
Sarah: Sure thing. Let me get your badge first.
-> receive_badge
```
**Why this matters:**
- Keeps players engaged and active
- Prevents dialogue fatigue
- Maintains pacing and momentum
- Makes conversations feel interactive, not like reading a script
**Exceptions:**
- Opening/closing cutscenes may have slightly longer monologues (max 5 lines)
- Dramatic reveals or critical story moments (max 4 lines)
- Even in exceptions, break up with internal choices or "press to continue" moments
**Best practices:**
- 1-2 lines is ideal for most dialogue
- 3 lines is the maximum before requiring a choice
- Use choices to create rhythm and player agency
- NPCs should respond to player choices, not just talk at them
---
## Required Input
From previous stages:

View File

@@ -82,6 +82,24 @@ From all previous stages:
- These should have `#exit_conversation` tag before END
- Regular NPC dialogue should return to hub instead of using END
3. **Validate scenario structure** - Run the validation script:
```bash
ruby scripts/validate_scenario.rb scenarios/[scenario_name]/scenario.json.erb
```
This will:
- Render and validate the ERB template
- Check against the scenario schema
- Identify common structural issues
- Provide suggestions for improvements
**Fix all INVALID errors before proceeding!** Suggestions are optional but recommended.
**Common validation errors to fix:**
- Phone NPCs in separate `phoneNPCs` section (should be in room `npcs` arrays)
- Missing `keyPins` arrays for key locks (needed for lockpicking minigame)
- Invalid room connection directions (only north/south/east/west valid)
## Understanding scenario.json.erb
### What is ERB?