Enhance inventory and container management for improved gameplay experience

- Added functionality to include current player inventory in game state for page reload recovery, allowing players to restore their inventory seamlessly.
- Implemented filtering of container contents to exclude items already in the player's inventory, enhancing user experience and gameplay clarity.
- Updated game mechanics to support both type-based and ID-based matching for inventory items, improving task validation and objectives tracking.
- Enhanced logging for better visibility into inventory processing and container content loading, aiding in debugging and game state management.
- Updated scenarios to reflect changes in item identification and task requirements, ensuring consistency across gameplay elements.
This commit is contained in:
Z. Cliffe Schreuders
2025-12-04 15:42:01 +00:00
parent 5a89ce945c
commit 629aa229b3
8 changed files with 266 additions and 59 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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 = '<div class="loading" style="text-align: center; padding: 20px;">Loading contents...</div>';
// 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

View File

@@ -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
});

View File

@@ -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;

View File

@@ -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

View File

@@ -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":{}}
{"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":{}}

View File

@@ -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,