diff --git a/app/controllers/break_escape/games_controller.rb b/app/controllers/break_escape/games_controller.rb
index bb31d2b..5f2218e 100644
--- a/app/controllers/break_escape/games_controller.rb
+++ b/app/controllers/break_escape/games_controller.rb
@@ -111,6 +111,12 @@ module BreakEscape
filtered['submittedFlags'] = @game.player_state['submitted_flags']
end
+ # Include current inventory from player_state for page reload recovery
+ # This allows the client to restore inventory state on reload
+ if @game.player_state['inventory'].present? && @game.player_state['inventory'].is_a?(Array)
+ filtered['playerInventory'] = @game.player_state['inventory']
+ end
+
render json: filtered
rescue => e
Rails.logger.error "[BreakEscape] scenario error: #{e.message}\n#{e.backtrace.first(5).join("\n")}"
@@ -226,6 +232,9 @@ module BreakEscape
def container
authorize @game if defined?(Pundit)
+ # Reload game to get latest player_state (in case inventory was updated)
+ @game.reload
+
container_id = params[:container_id]
return render_error('Missing container_id parameter', :bad_request) unless container_id.present?
@@ -243,7 +252,8 @@ module BreakEscape
# Return filtered contents
contents = filter_container_contents(container_data)
- Rails.logger.debug "[BreakEscape] Serving container contents for: #{container_id}"
+ Rails.logger.info "[BreakEscape] Serving container contents for: #{container_id} - returning #{contents.length} items"
+ Rails.logger.debug "[BreakEscape] Container contents: #{contents.map { |c| "#{c['type']}/#{c['id']}/#{c['name']}" }.join(', ')}"
render json: {
container_id: container_id,
@@ -658,7 +668,69 @@ module BreakEscape
item_copy
end || []
- contents
+ # Filter out items that are already in the player's inventory
+ inventory = @game.player_state['inventory'] || []
+ Rails.logger.debug "[BreakEscape] Filtering container contents. Inventory has #{inventory.length} items"
+ Rails.logger.debug "[BreakEscape] Container has #{contents.length} items before filtering"
+
+ filtered_contents = contents.reject do |item|
+ in_inventory = item_in_inventory?(item, inventory)
+ if in_inventory
+ Rails.logger.debug "[BreakEscape] Filtering out item: #{item['type']} / #{item['id']} / #{item['name']} (already in inventory)"
+ end
+ in_inventory
+ end
+
+ Rails.logger.debug "[BreakEscape] Container has #{filtered_contents.length} items after filtering"
+ filtered_contents
+ end
+
+ # Check if an item is already in the player's inventory
+ # Matches by type, id, or name (similar to validation logic)
+ def item_in_inventory?(item, inventory)
+ return false if inventory.blank? || item.blank?
+
+ # Normalize item data (handle both string and symbol keys)
+ item_type = item['type'] || item[:type]
+ item_id = item['key_id'] || item[:key_id] || item['id'] || item[:id]
+ item_name = item['name'] || item[:name]
+
+ Rails.logger.debug "[BreakEscape] Checking if item in inventory: type=#{item_type}, id=#{item_id}, name=#{item_name}"
+
+ inventory.any? do |inv_item|
+ # Inventory items are stored as flat objects (not nested in scenarioData)
+ # Handle both string and symbol keys
+ inv_type = inv_item['type'] || inv_item[:type]
+ inv_id = inv_item['key_id'] || inv_item[:key_id] || inv_item['id'] || inv_item[:id]
+ inv_name = inv_item['name'] || inv_item[:name]
+
+ Rails.logger.debug "[BreakEscape] Comparing with inventory item: type=#{inv_type}, id=#{inv_id}, name=#{inv_name}"
+
+ # Must match type
+ next false unless inv_type == item_type
+
+ # If both have IDs, match by ID (most specific)
+ if item_id.present? && inv_id.present?
+ match = inv_id.to_s == item_id.to_s
+ Rails.logger.debug "[BreakEscape] ID match: #{match} (#{item_id} == #{inv_id})"
+ return true if match
+ end
+
+ # If both have names, match by name (fallback if no ID match)
+ if item_name.present? && inv_name.present?
+ match = inv_name.to_s == item_name.to_s
+ Rails.logger.debug "[BreakEscape] Name match: #{match} (#{item_name} == #{inv_name})"
+ return true if match
+ end
+
+ # If item has no ID or name, match by type only (less specific, but works for generic items)
+ if item_id.blank? && item_name.blank?
+ Rails.logger.debug "[BreakEscape] Type-only match (no ID/name)"
+ return true
+ end
+
+ false
+ end
end
# Items that are always allowed in inventory (core game mechanics)
diff --git a/app/models/break_escape/game.rb b/app/models/break_escape/game.rb
index 91658d9..2130389 100644
--- a/app/models/break_escape/game.rb
+++ b/app/models/break_escape/game.rb
@@ -480,13 +480,40 @@ module BreakEscape
end
# Validate collection tasks
+ # Supports both type-based matching (targetItems) and ID-based matching (targetItemIds)
def validate_collection(task)
inventory = player_state['inventory'] || []
- target_items = Array(task['targetItems'])
+ target_items = Array(task['targetItems'] || [])
+ target_item_ids = Array(task['targetItemIds'] || [])
+
count = inventory.count do |item|
item_type = item['type'] || item.dig('scenarioData', 'type')
- target_items.include?(item_type)
+ item_id = item['id'] || item.dig('scenarioData', 'id')
+ item_name = item['name'] || item.dig('scenarioData', 'name')
+ identifier = item_id || item_name
+
+ matches = false
+
+ # Type-based matching
+ if target_items.any?
+ matches = target_items.include?(item_type)
+ end
+
+ # ID-based matching (more specific)
+ if target_item_ids.any?
+ matches = target_item_ids.include?(identifier)
+ end
+
+ # If both specified, match either
+ if target_items.any? && target_item_ids.any?
+ type_match = target_items.include?(item_type)
+ id_match = target_item_ids.include?(identifier)
+ matches = type_match || id_match
+ end
+
+ matches
end
+
count >= (task['targetCount'] || 1)
end
diff --git a/public/break_escape/js/minigames/container/container-minigame.js b/public/break_escape/js/minigames/container/container-minigame.js
index ed56a07..88d36ba 100644
--- a/public/break_escape/js/minigames/container/container-minigame.js
+++ b/public/break_escape/js/minigames/container/container-minigame.js
@@ -6,7 +6,9 @@ export class ContainerMinigame extends MinigameScene {
constructor(container, params) {
super(container, params);
this.containerItem = params.containerItem;
- this.contents = params.contents || [];
+ // Don't set contents here - let init() load from server if available
+ // Only use passed contents as fallback for locked containers or local games
+ this.contents = [];
this.isTakeable = params.isTakeable || false;
// NPC mode support
@@ -38,17 +40,18 @@ export class ContainerMinigame extends MinigameScene {
}
async loadContainerContents() {
- const gameId = window.gameId;
+ // Try multiple sources for gameId
+ const gameId = window.gameId || window.breakEscapeConfig?.gameId;
const containerId = this.containerItem.scenarioData.id ||
this.containerItem.scenarioData.name ||
this.containerItem.objectId;
if (!gameId) {
- console.error('No gameId available for container loading');
+ console.error('No gameId available for container loading. Checked window.gameId and window.breakEscapeConfig?.gameId');
return [];
}
- console.log(`Loading contents for container: ${containerId}`);
+ console.log(`Loading contents for container: ${containerId} (gameId: ${gameId})`);
try {
const response = await fetch(`/break_escape/games/${gameId}/container/${containerId}`, {
@@ -67,7 +70,7 @@ export class ContainerMinigame extends MinigameScene {
}
const data = await response.json();
- console.log(`Loaded ${data.contents?.length || 0} items from container`);
+ console.log(`Loaded ${data.contents?.length || 0} items from container ${containerId}:`, data.contents);
return data.contents || [];
} catch (error) {
console.error('Failed to load container contents:', error);
@@ -103,11 +106,18 @@ export class ContainerMinigame extends MinigameScene {
// Show loading state
this.gameContainer.innerHTML = '
Loading contents...
';
- // Load contents from server (if gameId exists and container is not locked)
- if (window.gameId && this.containerItem.scenarioData.locked === false) {
+ // Always load contents from server if gameId exists and container is unlocked
+ // This ensures we get the latest contents (with items already in inventory filtered out)
+ // Even if contents were passed in params, reload from server to get accurate state
+ const gameId = window.gameId || window.breakEscapeConfig?.gameId;
+ if (gameId && this.containerItem.scenarioData.locked === false) {
+ console.log('Reloading container contents from server to get latest state');
this.contents = await this.loadContainerContents();
+ } else if (this.params.contents && this.params.contents.length > 0) {
+ // Only use passed contents if server loading isn't available (locked container or local game)
+ console.log('Using passed contents (container locked or no server)');
+ this.contents = this.params.contents;
}
- // Otherwise use contents passed in (for unlocked containers or local game)
// Create the container minigame UI
this.createContainerUI();
@@ -736,6 +746,25 @@ export function returnToContainerAfterNotes() {
if (containerState.itemToTake) {
console.log('Removing notes item after notes minigame:', containerState.itemToTake);
+ // If the item is takeable, add it to inventory so objectives system can track it
+ if (containerState.itemToTake.takeable && window.addToInventory) {
+ console.log('Adding takeable notes item to inventory for objectives tracking');
+
+ // Create a temporary sprite-like object for the inventory system
+ const tempSprite = {
+ scenarioData: containerState.itemToTake,
+ name: containerState.itemToTake.type,
+ objectId: `temp_${Date.now()}`,
+ setVisible: function(visible) {
+ // Mock setVisible method for inventory compatibility
+ console.log(`Mock setVisible(${visible}) called on temp sprite`);
+ }
+ };
+
+ // Add to inventory - this will emit the item_picked_up event
+ window.addToInventory(tempSprite);
+ }
+
// Remove from container display
if (containerState.itemElement && containerState.itemElement.parentElement) {
containerState.itemElement.parentElement.remove();
@@ -750,10 +779,11 @@ export function returnToContainerAfterNotes() {
window.gameAlert(`${containerState.itemToTake.name} has been noted`, 'success', 'Added to Notes', 2000);
}
- // Start the container minigame with the stored state
+ // Start the container minigame - don't pass contents, let it reload from server
+ // This ensures items already in inventory are filtered out
startContainerMinigame(
containerState.containerItem,
- containerState.contents,
+ null, // Don't pass contents - let it reload from server
containerState.isTakeable,
null, // desktopMode - let it auto-detect or use npcOptions
containerState.npcOptions // Restore NPC context if it was saved
diff --git a/public/break_escape/js/systems/inventory.js b/public/break_escape/js/systems/inventory.js
index 8057f92..6858906 100644
--- a/public/break_escape/js/systems/inventory.js
+++ b/public/break_escape/js/systems/inventory.js
@@ -128,7 +128,23 @@ export function processInitialInventoryItems() {
return;
}
- // Check for startItemsInInventory array in scenario
+ // Priority 1: Use server-side inventory if available (for page reload recovery)
+ if (window.gameScenario.playerInventory && Array.isArray(window.gameScenario.playerInventory)) {
+ console.log(`Processing ${window.gameScenario.playerInventory.length} items from server inventory`);
+
+ window.gameScenario.playerInventory.forEach(itemData => {
+ console.log(`Adding ${itemData.name} to inventory from server playerInventory`);
+
+ // Create inventory sprite for this object
+ const inventoryItem = createInventorySprite(itemData);
+ if (inventoryItem) {
+ addToInventory(inventoryItem);
+ }
+ });
+ return; // Don't process startItemsInInventory if we loaded from server
+ }
+
+ // Priority 2: Fall back to startItemsInInventory from scenario (for new games)
if (window.gameScenario.startItemsInInventory && Array.isArray(window.gameScenario.startItemsInInventory)) {
console.log(`Processing ${window.gameScenario.startItemsInInventory.length} starting inventory items`);
@@ -370,6 +386,7 @@ export async function addToInventory(sprite) {
window.eventDispatcher.emit(`item_picked_up:${sprite.scenarioData.type}`, {
itemType: sprite.scenarioData.type,
itemName: sprite.scenarioData.name,
+ itemId: sprite.scenarioData.id,
roomId: window.currentPlayerRoom
});
}
@@ -445,6 +462,7 @@ function addKeyToInventory(sprite) {
window.eventDispatcher.emit(`item_picked_up:key`, {
itemType: 'key',
itemName: sprite.scenarioData?.name || 'Unknown Key',
+ itemId: sprite.scenarioData?.id || keyId,
keyId: keyId,
roomId: window.currentPlayerRoom
});
diff --git a/public/break_escape/js/systems/objectives-manager.js b/public/break_escape/js/systems/objectives-manager.js
index b21529c..3ed64f5 100644
--- a/public/break_escape/js/systems/objectives-manager.js
+++ b/public/break_escape/js/systems/objectives-manager.js
@@ -149,15 +149,65 @@ export class ObjectivesManager {
case 'collect_items':
const matchingItems = inventoryItems.filter(item => {
const itemType = item.scenarioData?.type || item.getAttribute?.('data-type');
- return task.targetItems.includes(itemType);
+ const itemId = item.scenarioData?.id;
+ const itemName = item.scenarioData?.name;
+
+ let matches = false;
+
+ // Type-based matching
+ if (task.targetItems && task.targetItems.length > 0) {
+ matches = task.targetItems.includes(itemType);
+ }
+
+ // ID-based matching (more specific)
+ if (task.targetItemIds && task.targetItemIds.length > 0) {
+ const identifier = itemId || itemName;
+ matches = task.targetItemIds.includes(identifier);
+ }
+
+ // If both specified, match either
+ if (task.targetItems && task.targetItems.length > 0 &&
+ task.targetItemIds && task.targetItemIds.length > 0) {
+ const typeMatch = task.targetItems.includes(itemType);
+ const identifier = itemId || itemName;
+ const idMatch = task.targetItemIds.includes(identifier);
+ matches = typeMatch || idMatch;
+ }
+
+ return matches;
});
// Also count keys from keyRing
const keyRingItems = window.inventory?.keyRing?.keys || [];
- const matchingKeys = keyRingItems.filter(key =>
- task.targetItems.includes(key.scenarioData?.type) ||
- task.targetItems.includes('key')
- );
+ const matchingKeys = keyRingItems.filter(key => {
+ const keyType = key.scenarioData?.type;
+ const keyId = key.scenarioData?.key_id || key.scenarioData?.id;
+ const keyName = key.scenarioData?.name;
+
+ let matches = false;
+
+ // Type-based matching
+ if (task.targetItems && task.targetItems.length > 0) {
+ matches = task.targetItems.includes(keyType) || task.targetItems.includes('key');
+ }
+
+ // ID-based matching
+ if (task.targetItemIds && task.targetItemIds.length > 0) {
+ const identifier = keyId || keyName;
+ matches = task.targetItemIds.includes(identifier);
+ }
+
+ // If both specified, match either
+ if (task.targetItems && task.targetItems.length > 0 &&
+ task.targetItemIds && task.targetItemIds.length > 0) {
+ const typeMatch = task.targetItems.includes(keyType) || task.targetItems.includes('key');
+ const identifier = keyId || keyName;
+ const idMatch = task.targetItemIds.includes(identifier);
+ matches = typeMatch || idMatch;
+ }
+
+ return matches;
+ });
const totalCount = matchingItems.length + matchingKeys.length;
@@ -245,17 +295,45 @@ export class ObjectivesManager {
/**
* Handle item pickup - check collect_items tasks
+ * Supports both type-based matching (targetItems) and ID-based matching (targetItemIds)
*/
handleItemPickup(data) {
if (!this.initialized) return;
const itemType = data.itemType;
+ const itemId = data.itemId;
+ const itemName = data.itemName;
- // Find all active collect_items tasks that target this item type
+ // Find all active collect_items tasks that target this item
Object.values(this.taskIndex).forEach(task => {
if (task.type !== 'collect_items') return;
if (task.status !== 'active') return;
- if (!task.targetItems.includes(itemType)) return;
+
+ // Check if item matches task criteria
+ let matches = false;
+
+ // Type-based matching (targetItems array)
+ if (task.targetItems && task.targetItems.length > 0) {
+ matches = task.targetItems.includes(itemType);
+ }
+
+ // ID-based matching (targetItemIds array) - more specific, overrides type matching
+ if (task.targetItemIds && task.targetItemIds.length > 0) {
+ // Match by ID if available, fall back to name
+ const identifier = itemId || itemName;
+ matches = task.targetItemIds.includes(identifier);
+ }
+
+ // If both are specified, item must match at least one
+ if (task.targetItems && task.targetItems.length > 0 &&
+ task.targetItemIds && task.targetItemIds.length > 0) {
+ const typeMatch = task.targetItems.includes(itemType);
+ const identifier = itemId || itemName;
+ const idMatch = task.targetItemIds.includes(identifier);
+ matches = typeMatch || idMatch;
+ }
+
+ if (!matches) return;
// Increment progress
task.currentCount = (task.currentCount || 0) + 1;
diff --git a/scenarios/lab_intro_linux/ink/locksmith.ink b/scenarios/lab_intro_linux/ink/locksmith.ink
index 8106fc7..e6ff1aa 100644
--- a/scenarios/lab_intro_linux/ink/locksmith.ink
+++ b/scenarios/lab_intro_linux/ink/locksmith.ink
@@ -21,12 +21,11 @@ Welcome to the lockpicking practice room. I'm here to teach you the fundamentals
#give_item:lockpick
#complete_task:talk_to_locksmith
#unlock_task:pick_all_locks
- Now let me explain how to use it.
- else:
- I see you already have a lockpick set. Let me give you a quick refresher on the basics.
+ I see you already have a lockpick set.
}
--> lockpicking_tutorial
+-> hub
// ===========================================
// MAIN HUB
@@ -36,18 +35,19 @@ Welcome to the lockpicking practice room. I'm here to teach you the fundamentals
What would you like to know?
{not lockpicking_tutorial_given:
- * [Teach me about lockpicking]
+ * [Can you teach me about lockpicking?]
-> lockpicking_tutorial
}
-{not all_locks_picked:
+{lockpicking_tutorial_given and not all_locks_picked:
+ [I'm working on picking the locks]
You'll find five locked containers in this room. Each one contains a document fragment. Pick all five to complete the practice exercise.
-> hub
}
-+ [That's all I need]
- -> end_conversation
++ [That's all I need] #exit_conversation
+ Good luck with your practice. Come back if you need any tips!
+ -> hub
// ===========================================
// LOCKPICKING TUTORIAL
@@ -58,21 +58,13 @@ What would you like to know?
Lockpicking is a physical security skill that's essential for field operations. Here's how it works:
-The basic principle: Most locks use pin tumblers. Each pin has two parts - a driver pin and a key pin. When the correct key is inserted, the pins align at the shear line, allowing the lock to turn.
+Most locks use pin tumblers. Each pin has two parts - a driver pin and a key pin. When the correct key is inserted, the pins align at the shear line, allowing the lock to turn.
-When picking a lock, you need two tools:
-1. A tension wrench - applies rotational pressure to the lock cylinder
-2. A pick - manipulates the pins one by one
+When picking a lock, you need two tools: a tension wrench that applies rotational pressure to the lock cylinder, and a pick that manipulates the pins one by one.
-The technique:
-- Apply light tension with the wrench in the direction the lock turns
-- Use the pick to push each pin up until you feel it "bind" (stop moving)
-- Pins bind in a specific order - work through them systematically
-- When all pins are set at the shear line, the lock will turn
+The technique involves applying light tension with the wrench in the direction the lock turns, then using the pick to push each pin up until you feel it "bind" (stop moving). Pins bind in a specific order, so you work through them systematically. When all pins are set at the shear line, the lock will turn.
-Practice makes perfect. Start with the containers in this room - they have different difficulty levels.
-
-Each container has a different lock configuration. Start with the easier ones and work your way up. When you've picked all five locks and collected all the documents, come back and I'll congratulate you on completing the practice.
+Practice makes perfect. Start with the containers in this room - they have different difficulty levels. Each container has a different lock configuration. Start with the easier ones and work your way up. When you've picked all five locks and collected all the documents, come back and I'll congratulate you on completing the practice.
Good luck!
@@ -88,25 +80,10 @@ Good luck!
Congratulations! You've successfully picked all five locks and recovered all the lost documents.
-You've demonstrated:
-- Understanding of lock mechanics
-- Ability to apply proper tension
-- Skill in identifying binding order
-- Patience and precision
-
-These skills will serve you well in the field. Lockpicking is often the difference between mission success and failure when you need access without leaving evidence of forced entry.
+You've demonstrated understanding of lock mechanics, ability to apply proper tension, skill in identifying binding order, and patience and precision. These skills will serve you well in the field. Lockpicking is often the difference between mission success and failure when you need access without leaving evidence of forced entry.
You're ready for real-world operations. Well done, Agent.
-> hub
-// ===========================================
-// END CONVERSATION
-// ===========================================
-
-=== end_conversation ===
-Good luck with your practice. Come back if you need any tips!
-
-#exit_conversation
--> hub
diff --git a/scenarios/lab_intro_linux/ink/locksmith.json b/scenarios/lab_intro_linux/ink/locksmith.json
index 9079d4c..e91e7d2 100644
--- a/scenarios/lab_intro_linux/ink/locksmith.json
+++ b/scenarios/lab_intro_linux/ink/locksmith.json
@@ -1 +1 @@
-{"inkVersion":21,"root":[[["done",{"#n":"g-0"}],null],"done",{"start":["^Welcome to the lockpicking practice room. I'm here to teach you the fundamentals of lockpicking.","\n","ev",{"VAR?":"has_lockpick"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^Here's a professional lockpick set to get you started.","\n","#","^give_item:lockpick","/#","#","^complete_task:talk_to_locksmith","/#","#","^unlock_task:pick_all_locks","/#","^Now let me explain how to use it.","\n",{"->":"start.7"},null]}],[{"->":".^.b"},{"b":["\n","^I see you already have a lockpick set. Let me give you a quick refresher on the basics.","\n",{"->":"start.7"},null]}],"nop","\n",{"->":"lockpicking_tutorial"},null],"hub":[["^What would you like to know?","\n","ev",{"VAR?":"lockpicking_tutorial_given"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","ev","str","^Teach me about lockpicking","/str","/ev",{"*":".^.c-0","flg":20},{"->":"hub.0.7"},{"c-0":["\n",{"->":"lockpicking_tutorial"},{"#f":5}]}]}],"nop","\n","ev",{"VAR?":"all_locks_picked"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","ev","str","^I'm working on picking the locks","/str","/ev",{"*":".^.c-0","flg":4},{"->":"hub.0.14"},{"c-0":["\n","^You'll find five locked containers in this room. Each one contains a document fragment. Pick all five to complete the practice exercise.","\n",{"->":"hub"},null]}]}],"nop","\n","ev","str","^That's all I need","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["\n",{"->":"end_conversation"},null]}],null],"lockpicking_tutorial":[["ev",true,"/ev",{"VAR=":"lockpicking_tutorial_given","re":true},"^Lockpicking is a physical security skill that's essential for field operations. Here's how it works:","\n","^The basic principle: Most locks use pin tumblers. Each pin has two parts - a driver pin and a key pin. When the correct key is inserted, the pins align at the shear line, allowing the lock to turn.","\n","^When picking a lock, you need two tools:","\n","^1. A tension wrench - applies rotational pressure to the lock cylinder","\n","^2. A pick - manipulates the pins one by one","\n","^The technique:","\n",["^Apply light tension with the wrench in the direction the lock turns","\n",["^Use the pick to push each pin up until you feel it \"bind\" (stop moving)","\n",["^Pins bind in a specific order - work through them systematically","\n",["^When all pins are set at the shear line, the lock will turn","\n","^Practice makes perfect. Start with the containers in this room - they have different difficulty levels.","\n","^Each container has a different lock configuration. Start with the easier ones and work your way up. When you've picked all five locks and collected all the documents, come back and I'll congratulate you on completing the practice.","\n","^Good luck!","\n",{"->":"hub"},{"#n":"g-3"}],{"#n":"g-2"}],{"#n":"g-1"}],{"#n":"g-0"}],null],null],"lockpicking_complete":[["ev",true,"/ev",{"VAR=":"all_locks_picked","re":true},"^Congratulations! You've successfully picked all five locks and recovered all the lost documents.","\n","^You've demonstrated:","\n",["^Understanding of lock mechanics","\n",["^Ability to apply proper tension","\n",["^Skill in identifying binding order","\n",["^Patience and precision","\n","^These skills will serve you well in the field. Lockpicking is often the difference between mission success and failure when you need access without leaving evidence of forced entry.","\n","^You're ready for real-world operations. Well done, Agent.","\n",{"->":"hub"},{"#n":"g-3"}],{"#n":"g-2"}],{"#n":"g-1"}],{"#n":"g-0"}],null],null],"end_conversation":["^Good luck with your practice. Come back if you need any tips!","\n","#","^exit_conversation","/#",{"->":"hub"},null],"global decl":["ev",false,{"VAR=":"has_lockpick"},false,{"VAR=":"lockpicking_tutorial_given"},false,{"VAR=":"all_locks_picked"},"/ev","end",null]}],"listDefs":{}}
\ No newline at end of file
+{"inkVersion":21,"root":[[["done",{"#n":"g-0"}],null],"done",{"start":["^Welcome to the lockpicking practice room. I'm here to teach you the fundamentals of lockpicking.","\n","ev",{"VAR?":"has_lockpick"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^Here's a professional lockpick set to get you started.","\n","#","^give_item:lockpick","/#","#","^complete_task:talk_to_locksmith","/#","#","^unlock_task:pick_all_locks","/#",{"->":"start.7"},null]}],[{"->":".^.b"},{"b":["\n","^I see you already have a lockpick set.","\n",{"->":"start.7"},null]}],"nop","\n",{"->":"hub"},null],"hub":[["^What would you like to know?","\n","ev",{"VAR?":"lockpicking_tutorial_given"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","ev","str","^Can you teach me about lockpicking?","/str","/ev",{"*":".^.c-0","flg":20},{"->":"hub.0.7"},{"c-0":["\n",{"->":"lockpicking_tutorial"},{"#f":5}]}]}],"nop","\n","ev",{"VAR?":"lockpicking_tutorial_given"},{"VAR?":"all_locks_picked"},"!","&&","/ev",[{"->":".^.b","c":true},{"b":["\n","ev","str","^I'm working on picking the locks","/str","/ev",{"*":".^.c-0","flg":4},{"->":"hub.0.16"},{"c-0":["\n","^You'll find five locked containers in this room. Each one contains a document fragment. Pick all five to complete the practice exercise.","\n",{"->":"hub"},null]}]}],"nop","\n","ev","str","^That's all I need","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ","#","^exit_conversation","/#","\n","^Good luck with your practice. Come back if you need any tips!","\n",{"->":"hub"},null]}],null],"lockpicking_tutorial":["ev",true,"/ev",{"VAR=":"lockpicking_tutorial_given","re":true},"^Lockpicking is a physical security skill that's essential for field operations. Here's how it works:","\n","^Most locks use pin tumblers. Each pin has two parts - a driver pin and a key pin. When the correct key is inserted, the pins align at the shear line, allowing the lock to turn.","\n","^When picking a lock, you need two tools: a tension wrench that applies rotational pressure to the lock cylinder, and a pick that manipulates the pins one by one.","\n","^The technique involves applying light tension with the wrench in the direction the lock turns, then using the pick to push each pin up until you feel it \"bind\" (stop moving). Pins bind in a specific order, so you work through them systematically. When all pins are set at the shear line, the lock will turn.","\n","^Practice makes perfect. Start with the containers in this room - they have different difficulty levels. Each container has a different lock configuration. Start with the easier ones and work your way up. When you've picked all five locks and collected all the documents, come back and I'll congratulate you on completing the practice.","\n","^Good luck!","\n",{"->":"hub"},null],"lockpicking_complete":["ev",true,"/ev",{"VAR=":"all_locks_picked","re":true},"^Congratulations! You've successfully picked all five locks and recovered all the lost documents.","\n","^You've demonstrated understanding of lock mechanics, ability to apply proper tension, skill in identifying binding order, and patience and precision. These skills will serve you well in the field. Lockpicking is often the difference between mission success and failure when you need access without leaving evidence of forced entry.","\n","^You're ready for real-world operations. Well done, Agent.","\n",{"->":"hub"},null],"global decl":["ev",false,{"VAR=":"has_lockpick"},false,{"VAR=":"lockpicking_tutorial_given"},false,{"VAR=":"all_locks_picked"},"/ev","end",null]}],"listDefs":{}}
\ No newline at end of file
diff --git a/scenarios/lab_intro_linux/scenario.json.erb b/scenarios/lab_intro_linux/scenario.json.erb
index db492fa..0c9d541 100644
--- a/scenarios/lab_intro_linux/scenario.json.erb
+++ b/scenarios/lab_intro_linux/scenario.json.erb
@@ -59,7 +59,7 @@
"taskId": "pick_all_locks",
"title": "Pick locks to retrieve lost documents",
"type": "collect_items",
- "targetItems": ["notes"],
+ "targetItemIds": ["document_fragment_1", "document_fragment_2", "document_fragment_3", "document_fragment_4", "document_fragment_5"],
"targetCount": 5,
"currentCount": 0,
"showProgress": true,
@@ -260,6 +260,7 @@
"contents": [
{
"type": "notes",
+ "id": "document_fragment_1",
"name": "Document Fragment 1",
"takeable": true,
"readable": true,
@@ -282,6 +283,7 @@
"contents": [
{
"type": "notes",
+ "id": "document_fragment_2",
"name": "Document Fragment 2",
"takeable": true,
"readable": true,
@@ -304,6 +306,7 @@
"contents": [
{
"type": "notes",
+ "id": "document_fragment_3",
"name": "Document Fragment 3",
"takeable": true,
"readable": true,
@@ -326,6 +329,7 @@
"contents": [
{
"type": "notes",
+ "id": "document_fragment_4",
"name": "Document Fragment 4",
"takeable": true,
"readable": true,
@@ -348,6 +352,7 @@
"contents": [
{
"type": "notes",
+ "id": "document_fragment_5",
"name": "Document Fragment 5",
"takeable": true,
"readable": true,