mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-20 13:50:46 +00:00
Add NPC inventory management and UI enhancements
- Introduced new NPC inventory system allowing NPCs to hold and give items to players. - Updated ContainerMinigame to support NPC mode, displaying NPC avatars and available items. - Enhanced chat and conversation systems to sync NPC item states with Ink variables, improving narrative interactions. - Added event listeners for item changes, ensuring dynamic updates during conversations. - Implemented new methods in NPCGameBridge for item giving and inventory display, streamlining item interactions.
This commit is contained in:
BIN
assets/backgrounds/background1.png
Normal file
BIN
assets/backgrounds/background1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 103 KiB |
BIN
assets/backgrounds/hq1.png
Normal file
BIN
assets/backgrounds/hq1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 174 KiB |
BIN
assets/backgrounds/hq2.png
Normal file
BIN
assets/backgrounds/hq2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 157 KiB |
BIN
assets/backgrounds/hq3.png
Normal file
BIN
assets/backgrounds/hq3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 173 KiB |
@@ -9,8 +9,14 @@ export class ContainerMinigame extends MinigameScene {
|
||||
this.contents = params.contents || [];
|
||||
this.isTakeable = params.isTakeable || false;
|
||||
|
||||
// Auto-detect desktop mode for PC/tablet containers
|
||||
this.desktopMode = params.desktopMode || this.shouldUseDesktopMode();
|
||||
// NPC mode support
|
||||
this.mode = params.mode || 'container'; // 'container', 'pc', or 'npc'
|
||||
this.npcId = params.npcId || null;
|
||||
this.npcDisplayName = params.npcDisplayName || null;
|
||||
this.npcAvatar = params.npcAvatar || null;
|
||||
|
||||
// Auto-detect desktop mode for PC/tablet containers (not used in NPC mode)
|
||||
this.desktopMode = (this.mode !== 'npc') && (params.desktopMode || this.shouldUseDesktopMode());
|
||||
}
|
||||
|
||||
shouldUseDesktopMode() {
|
||||
@@ -58,7 +64,9 @@ export class ContainerMinigame extends MinigameScene {
|
||||
}
|
||||
|
||||
createContainerUI() {
|
||||
if (this.desktopMode) {
|
||||
if (this.mode === 'npc') {
|
||||
this.createNPCUI();
|
||||
} else if (this.desktopMode) {
|
||||
this.createDesktopUI();
|
||||
} else {
|
||||
this.createStandardUI();
|
||||
@@ -71,6 +79,27 @@ export class ContainerMinigame extends MinigameScene {
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
createNPCUI() {
|
||||
// NPC mode - show NPC avatar and offer items
|
||||
let avatarHtml = '';
|
||||
if (this.npcAvatar) {
|
||||
avatarHtml = `<img src="${this.npcAvatar}" alt="${this.npcDisplayName}" class="npc-avatar" style="width: 80px; height: 80px; border-radius: 50%; margin-bottom: 15px; display: block; margin-left: auto; margin-right: auto;">`;
|
||||
}
|
||||
|
||||
this.gameContainer.innerHTML = `
|
||||
<div class="container-minigame npc-mode">
|
||||
${avatarHtml}
|
||||
<h3 style="text-align: center; margin-bottom: 20px;">${this.npcDisplayName || 'NPC'} offers you items</h3>
|
||||
<div class="container-contents-section">
|
||||
<h4 style="text-align: center;">Available Items</h4>
|
||||
<div class="container-contents-grid" id="container-contents-grid">
|
||||
<!-- Contents will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
createStandardUI() {
|
||||
this.gameContainer.innerHTML = `
|
||||
<div class="container-minigame">
|
||||
@@ -455,6 +484,24 @@ export class ContainerMinigame extends MinigameScene {
|
||||
const itemIndex = this.contents.findIndex(content => content === item);
|
||||
if (itemIndex !== -1) {
|
||||
this.contents.splice(itemIndex, 1);
|
||||
|
||||
// If in NPC mode, also remove from NPC's itemsHeld
|
||||
if (this.mode === 'npc' && this.npcId && window.npcManager) {
|
||||
const npc = window.npcManager.getNPC(this.npcId);
|
||||
if (npc && npc.itemsHeld) {
|
||||
const npcItemIndex = npc.itemsHeld.findIndex(i => i === item);
|
||||
if (npcItemIndex !== -1) {
|
||||
npc.itemsHeld.splice(npcItemIndex, 1);
|
||||
|
||||
// Emit event to update Ink variables
|
||||
if (window.eventDispatcher) {
|
||||
window.eventDispatcher.emit('npc_items_changed', {
|
||||
npcId: this.npcId
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show success message
|
||||
|
||||
@@ -79,27 +79,56 @@ export function processGameActionTags(tags, ui) {
|
||||
|
||||
case 'give_item':
|
||||
if (param) {
|
||||
// Parse item properties from param (could be "keycard" or "keycard|CEO Keycard")
|
||||
const [itemType, itemName] = param.split('|').map(s => s.trim());
|
||||
const giveResult = window.NPCGameBridge.giveItem(itemType, {
|
||||
name: itemName || itemType
|
||||
});
|
||||
const [itemType] = param.split('|').map(s => s.trim());
|
||||
const npcId = window.currentConversationNPCId;
|
||||
|
||||
if (!npcId) {
|
||||
result.message = '⚠️ No NPC context available';
|
||||
console.warn(result.message);
|
||||
break;
|
||||
}
|
||||
|
||||
const giveResult = window.NPCGameBridge.giveItem(npcId, itemType);
|
||||
if (giveResult.success) {
|
||||
result.success = true;
|
||||
result.message = `📦 Received: ${itemName || itemType}`;
|
||||
result.message = `📦 Received: ${giveResult.item.name}`;
|
||||
if (ui) ui.showNotification(result.message, 'success');
|
||||
console.log('✅ Item given successfully:', giveResult);
|
||||
} else {
|
||||
result.message = `⚠️ Failed to give item: ${itemType}`;
|
||||
result.message = `⚠️ ${giveResult.error}`;
|
||||
if (ui) ui.showNotification(result.message, 'warning');
|
||||
console.warn('⚠️ Item give failed:', giveResult);
|
||||
}
|
||||
} else {
|
||||
result.message = '⚠️ give_item tag missing item parameter';
|
||||
result.message = '⚠️ give_item requires item type parameter';
|
||||
console.warn(result.message);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'give_npc_inventory_items':
|
||||
const npcId = window.currentConversationNPCId;
|
||||
|
||||
if (!npcId) {
|
||||
result.message = '⚠️ No NPC context available';
|
||||
console.warn(result.message);
|
||||
break;
|
||||
}
|
||||
|
||||
// Parse filter types (comma-separated)
|
||||
const filterTypes = param ? param.split(',').map(s => s.trim()).filter(s => s) : null;
|
||||
|
||||
const showResult = window.NPCGameBridge.showNPCInventory(npcId, filterTypes);
|
||||
if (showResult.success) {
|
||||
result.success = true;
|
||||
result.message = `📦 Opening inventory with ${showResult.itemCount} items`;
|
||||
console.log('✅ NPC inventory opened:', showResult);
|
||||
} else {
|
||||
result.message = `⚠️ ${showResult.error}`;
|
||||
if (ui) ui.showNotification(result.message, 'warning');
|
||||
console.warn('⚠️ Show inventory failed:', showResult);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'set_objective':
|
||||
if (param) {
|
||||
window.NPCGameBridge.setObjective(param);
|
||||
|
||||
@@ -98,6 +98,53 @@ export default class PersonChatConversation {
|
||||
// Set variables in the Ink engine using setVariable instead of bindVariable
|
||||
this.inkEngine.setVariable('last_interaction_type', 'person');
|
||||
this.inkEngine.setVariable('player_name', 'Player');
|
||||
|
||||
// Sync NPC items to Ink variables
|
||||
this.syncItemsToInk();
|
||||
|
||||
// Set up event listener for item changes
|
||||
if (window.eventDispatcher) {
|
||||
this._itemsChangedListener = (data) => {
|
||||
if (data.npcId === this.npc.id) {
|
||||
this.syncItemsToInk();
|
||||
}
|
||||
};
|
||||
window.eventDispatcher.on('npc_items_changed', this._itemsChangedListener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync NPC's held items to Ink variables
|
||||
* Sets has_<type> based on itemsHeld array
|
||||
*/
|
||||
syncItemsToInk() {
|
||||
if (!this.inkEngine || !this.inkEngine.story) return;
|
||||
|
||||
const npc = this.npc;
|
||||
if (!npc || !npc.itemsHeld) return;
|
||||
|
||||
const varState = this.inkEngine.story.variablesState;
|
||||
if (!varState._defaultGlobalVariables) return;
|
||||
|
||||
// Count items by type
|
||||
const itemCounts = {};
|
||||
npc.itemsHeld.forEach(item => {
|
||||
itemCounts[item.type] = (itemCounts[item.type] || 0) + 1;
|
||||
});
|
||||
|
||||
// Set has_<type> variables based on inventory
|
||||
Object.keys(itemCounts).forEach(type => {
|
||||
const varName = `has_${type}`;
|
||||
if (varState._defaultGlobalVariables && varState._defaultGlobalVariables.has(varName)) {
|
||||
const hasItem = itemCounts[type] > 0;
|
||||
try {
|
||||
this.inkEngine.setVariable(varName, hasItem);
|
||||
console.log(`✅ Synced ${varName} = ${hasItem} for NPC ${npc.id}`);
|
||||
} catch (err) {
|
||||
console.warn(`⚠️ Could not sync ${varName}:`, err.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -337,6 +384,11 @@ export default class PersonChatConversation {
|
||||
*/
|
||||
end() {
|
||||
try {
|
||||
// Remove event listener
|
||||
if (window.eventDispatcher && this._itemsChangedListener) {
|
||||
window.eventDispatcher.off('npc_items_changed', this._itemsChangedListener);
|
||||
}
|
||||
|
||||
if (this.inkEngine) {
|
||||
// Don't destroy - keep for history/dual identity
|
||||
this.inkEngine = null;
|
||||
|
||||
@@ -281,6 +281,9 @@ export class PersonChatMinigame extends MinigameScene {
|
||||
|
||||
console.log('🎭 PersonChatMinigame started');
|
||||
|
||||
// Track NPC context for tag processing
|
||||
window.currentConversationNPCId = this.npcId;
|
||||
|
||||
// Start conversation with Ink
|
||||
this.startConversation();
|
||||
}
|
||||
@@ -856,6 +859,9 @@ export class PersonChatMinigame extends MinigameScene {
|
||||
npcConversationStateManager.saveNPCState(this.npcId, this.inkEngine.story);
|
||||
}
|
||||
|
||||
// Clear NPC context
|
||||
window.currentConversationNPCId = null;
|
||||
|
||||
// Call parent cleanup
|
||||
super.cleanup();
|
||||
}
|
||||
|
||||
@@ -417,7 +417,7 @@ export default class PersonChatPortraits {
|
||||
if (!this.canvas || !this.ctx) return;
|
||||
|
||||
try {
|
||||
console.log(`🎨 render() called - useSpriteTalk: ${this.useSpriteTalk}, spriteSheet: ${this.spriteSheet}`);
|
||||
// console.log(`🎨 render() called - useSpriteTalk: ${this.useSpriteTalk}, spriteSheet: ${this.spriteSheet}`);
|
||||
|
||||
// Clear canvas
|
||||
this.ctx.fillStyle = '#000';
|
||||
@@ -425,7 +425,7 @@ export default class PersonChatPortraits {
|
||||
|
||||
// If using spriteTalk image, render that instead
|
||||
if (this.useSpriteTalk) {
|
||||
console.log(`🎨 Rendering spriteTalk image path`);
|
||||
// console.log(`🎨 Rendering spriteTalk image path`);
|
||||
// Calculate sprite scale for spriteTalk
|
||||
const spriteTalkScale = this.calculateSpriteTalkScale();
|
||||
// Draw background with sprite scale if loaded
|
||||
|
||||
@@ -74,6 +74,20 @@ export default class PhoneChatConversation {
|
||||
|
||||
this.storyLoaded = true;
|
||||
this.storyEnded = false;
|
||||
|
||||
// Sync NPC items to Ink variables
|
||||
this.syncItemsToInk();
|
||||
|
||||
// Set up event listener for item changes
|
||||
if (window.eventDispatcher) {
|
||||
this._itemsChangedListener = (data) => {
|
||||
if (data.npcId === this.npcId) {
|
||||
this.syncItemsToInk();
|
||||
}
|
||||
};
|
||||
window.eventDispatcher.on('npc_items_changed', this._itemsChangedListener);
|
||||
}
|
||||
|
||||
console.log(`✅ Story loaded successfully for ${this.npcId}`);
|
||||
|
||||
return true;
|
||||
@@ -117,6 +131,40 @@ export default class PhoneChatConversation {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync NPC's held items to Ink variables
|
||||
* Sets has_<type> based on itemsHeld array
|
||||
*/
|
||||
syncItemsToInk() {
|
||||
if (!this.engine || !this.engine.story) return;
|
||||
|
||||
const npc = this.npcManager.getNPC(this.npcId);
|
||||
if (!npc || !npc.itemsHeld) return;
|
||||
|
||||
const varState = this.engine.story.variablesState;
|
||||
if (!varState._defaultGlobalVariables) return;
|
||||
|
||||
// Count items by type
|
||||
const itemCounts = {};
|
||||
npc.itemsHeld.forEach(item => {
|
||||
itemCounts[item.type] = (itemCounts[item.type] || 0) + 1;
|
||||
});
|
||||
|
||||
// Set has_<type> variables based on inventory
|
||||
Object.keys(itemCounts).forEach(type => {
|
||||
const varName = `has_${type}`;
|
||||
if (varState._defaultGlobalVariables && varState._defaultGlobalVariables.has(varName)) {
|
||||
const hasItem = itemCounts[type] > 0;
|
||||
try {
|
||||
this.engine.setVariable(varName, hasItem);
|
||||
console.log(`✅ Synced ${varName} = ${hasItem} for NPC ${npc.id}`);
|
||||
} catch (err) {
|
||||
console.warn(`⚠️ Could not sync ${varName}:`, err.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Continue the story and get the next text/choices
|
||||
* @returns {Object} Story result { text, choices, tags, canContinue, hasEnded }
|
||||
@@ -331,6 +379,16 @@ export default class PhoneChatConversation {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up resources (event listeners, etc.)
|
||||
*/
|
||||
cleanup() {
|
||||
// Remove event listener
|
||||
if (window.eventDispatcher && this._itemsChangedListener) {
|
||||
window.eventDispatcher.off('npc_items_changed', this._itemsChangedListener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get conversation metadata (variables, state)
|
||||
* @returns {Object} Metadata about the conversation
|
||||
|
||||
@@ -207,6 +207,8 @@ export class PhoneChatMinigame extends MinigameScene {
|
||||
|
||||
// If NPC ID provided, open that conversation directly
|
||||
if (this.currentNPCId) {
|
||||
// Track NPC context for tag processing
|
||||
window.currentConversationNPCId = this.currentNPCId;
|
||||
this.openConversation(this.currentNPCId);
|
||||
} else {
|
||||
// Show contact list for this phone
|
||||
@@ -299,6 +301,9 @@ export class PhoneChatMinigame extends MinigameScene {
|
||||
// Update current NPC
|
||||
this.currentNPCId = npcId;
|
||||
|
||||
// Track NPC context for tag processing
|
||||
window.currentConversationNPCId = npcId;
|
||||
|
||||
// Initialize conversation modules
|
||||
this.history = new PhoneChatHistory(npcId, this.npcManager);
|
||||
this.conversation = new PhoneChatConversation(npcId, this.npcManager, this.inkEngine);
|
||||
@@ -735,6 +740,9 @@ export class PhoneChatMinigame extends MinigameScene {
|
||||
this.conversation = null;
|
||||
this.history = null;
|
||||
|
||||
// Clear NPC context
|
||||
window.currentConversationNPCId = null;
|
||||
|
||||
// Call parent cleanup
|
||||
super.cleanup();
|
||||
}
|
||||
|
||||
@@ -116,7 +116,13 @@ export default class InkEngine {
|
||||
|
||||
setVariable(name, value) {
|
||||
if (!this.story) throw new Error('Story not loaded');
|
||||
// inkjs VariableState.SetGlobal expects a RuntimeObject; it's forgiving for primitives
|
||||
this.story.variablesState.SetGlobal(name, value);
|
||||
|
||||
// Let Ink handle the value type conversion through the indexer
|
||||
// which properly wraps values in Runtime.Value objects
|
||||
try {
|
||||
this.story.variablesState[name] = value;
|
||||
} catch (err) {
|
||||
console.warn(`⚠️ Failed to set variable ${name}:`, err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,14 +35,30 @@ class NPCConversationStateManager {
|
||||
// Always save the variables (favour, items earned, flags, etc.)
|
||||
// These persist across conversations even when story ends
|
||||
if (story.variablesState) {
|
||||
state.variables = { ...story.variablesState };
|
||||
// Filter out has_* variables (derived from itemsHeld, will be re-synced on load)
|
||||
const filteredVariables = {};
|
||||
for (const [key, value] of Object.entries(story.variablesState)) {
|
||||
// Skip dynamically-synced item inventory variables
|
||||
if (!key.startsWith('has_lockpick') &&
|
||||
!key.startsWith('has_workstation') &&
|
||||
!key.startsWith('has_phone') &&
|
||||
!key.startsWith('has_keycard')) {
|
||||
filteredVariables[key] = value;
|
||||
}
|
||||
}
|
||||
state.variables = filteredVariables;
|
||||
console.log(`💾 Saved variables for ${npcId}:`, state.variables);
|
||||
}
|
||||
|
||||
// Only save full story state if story is still active OR if explicitly forced
|
||||
if (!story.state.hasEnded || forceFullState) {
|
||||
state.storyState = story.state.ToJson();
|
||||
console.log(`💾 Saved full story state for ${npcId} (active story)`);
|
||||
try {
|
||||
state.storyState = story.state.ToJson();
|
||||
console.log(`💾 Saved full story state for ${npcId} (active story)`);
|
||||
} catch (serializeError) {
|
||||
// If serialization fails (due to dynamic variables), just save variables
|
||||
console.warn(`⚠️ Could not serialize full story state for ${npcId}, saving variables only:`, serializeError.message);
|
||||
}
|
||||
} else {
|
||||
console.log(`💾 Saved variables only for ${npcId} (story ended - will restart fresh)`);
|
||||
}
|
||||
|
||||
@@ -100,99 +100,142 @@ export class NPCGameBridge {
|
||||
}
|
||||
|
||||
/**
|
||||
* Give an item to the player
|
||||
* @param {string} itemType - Type of item to give
|
||||
* @param {Object} properties - Optional item properties
|
||||
* @returns {Object} Result object with success status
|
||||
* Give an item from NPC's inventory to the player immediately
|
||||
* @param {string} npcId - NPC identifier
|
||||
* @param {string} itemType - Type of item to give (optional - gives first if null)
|
||||
* @returns {Object} Result with success status
|
||||
*/
|
||||
giveItem(itemType, properties = {}) {
|
||||
if (!itemType) {
|
||||
const result = { success: false, error: 'No itemType provided' };
|
||||
this._logAction('giveItem', { itemType, properties }, result);
|
||||
giveItem(npcId, itemType = null) {
|
||||
if (!npcId) {
|
||||
const result = { success: false, error: 'No npcId provided' };
|
||||
this._logAction('giveItem', { npcId, itemType }, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Get NPC from manager
|
||||
const npc = window.npcManager?.getNPC(npcId);
|
||||
if (!npc) {
|
||||
const result = { success: false, error: `NPC ${npcId} not found` };
|
||||
this._logAction('giveItem', { npcId, itemType }, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!npc.itemsHeld || npc.itemsHeld.length === 0) {
|
||||
const result = { success: false, error: `NPC ${npcId} has no items to give` };
|
||||
this._logAction('giveItem', { npcId, itemType }, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Find item in NPC's inventory
|
||||
let itemIndex = -1;
|
||||
if (itemType) {
|
||||
// Find first item matching type
|
||||
itemIndex = npc.itemsHeld.findIndex(item => item.type === itemType);
|
||||
if (itemIndex === -1) {
|
||||
const result = { success: false, error: `NPC ${npcId} doesn't have ${itemType}` };
|
||||
this._logAction('giveItem', { npcId, itemType }, result);
|
||||
return result;
|
||||
}
|
||||
} else {
|
||||
// Give first item
|
||||
itemIndex = 0;
|
||||
}
|
||||
|
||||
const item = npc.itemsHeld[itemIndex];
|
||||
|
||||
if (!window.addToInventory) {
|
||||
const result = { success: false, error: 'Inventory system not available' };
|
||||
this._logAction('giveItem', { itemType, properties }, result);
|
||||
this._logAction('giveItem', { npcId, itemType }, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
// Default names for common items
|
||||
const defaultNames = {
|
||||
'lockpick': 'Lock Pick Kit',
|
||||
'bluetooth_scanner': 'Bluetooth Scanner',
|
||||
'fingerprint_kit': 'Fingerprint Kit',
|
||||
'pin-cracker': 'PIN Cracker',
|
||||
'workstation': 'Crypto Analysis Station',
|
||||
'keycard': 'Access Keycard',
|
||||
'key': 'Key'
|
||||
};
|
||||
|
||||
// Default observations for common items
|
||||
const defaultObservations = {
|
||||
'lockpick': 'A professional lock picking kit with various picks and tension wrenches',
|
||||
'bluetooth_scanner': 'A device for scanning and connecting to nearby Bluetooth devices',
|
||||
'fingerprint_kit': 'A forensic kit for collecting and analyzing fingerprints',
|
||||
'pin-cracker': 'A tool for cracking numeric PIN codes',
|
||||
'workstation': 'A powerful workstation for cryptographic analysis',
|
||||
'keycard': 'An access keycard for secured areas',
|
||||
'key': 'A key that opens a specific lock'
|
||||
};
|
||||
|
||||
// Create a basic item structure
|
||||
const itemName = (properties.name && properties.name !== itemType)
|
||||
? properties.name
|
||||
: (defaultNames[itemType] || itemType);
|
||||
const itemObservations = properties.observations || defaultObservations[itemType] || `A ${itemName} given by an NPC`;
|
||||
|
||||
const item = {
|
||||
type: itemType,
|
||||
name: itemName,
|
||||
takeable: true,
|
||||
observations: itemObservations,
|
||||
scenarioData: {
|
||||
...properties, // Spread properties first
|
||||
type: itemType, // Then override with correct values
|
||||
name: itemName,
|
||||
observations: itemObservations,
|
||||
takeable: true
|
||||
}
|
||||
// Create sprite using container pattern
|
||||
const tempSprite = {
|
||||
scenarioData: item,
|
||||
name: item.type,
|
||||
objectId: `npc_gift_${npcId}_${item.type}_${Date.now()}`,
|
||||
texture: { key: item.type }
|
||||
};
|
||||
|
||||
// Create a pseudo-sprite for the inventory system
|
||||
const sprite = {
|
||||
name: item.name,
|
||||
scenarioData: item.scenarioData,
|
||||
texture: { key: itemType },
|
||||
objectId: `npc_gift_${itemType}_${Date.now()}`
|
||||
};
|
||||
// Add to player inventory
|
||||
window.addToInventory(tempSprite);
|
||||
|
||||
console.log('🎁 NPCGameBridge: Creating item sprite:', {
|
||||
itemType,
|
||||
name: sprite.name,
|
||||
scenarioDataName: sprite.scenarioData.name,
|
||||
scenarioDataType: sprite.scenarioData.type,
|
||||
fullScenarioData: sprite.scenarioData
|
||||
});
|
||||
// Remove from NPC's inventory
|
||||
npc.itemsHeld.splice(itemIndex, 1);
|
||||
|
||||
window.addToInventory(sprite);
|
||||
|
||||
// Emit event
|
||||
// Emit event to update Ink variables
|
||||
if (window.eventDispatcher) {
|
||||
window.eventDispatcher.emit('item_given_by_npc', {
|
||||
itemType,
|
||||
source: 'npc'
|
||||
});
|
||||
window.eventDispatcher.emit('npc_items_changed', { npcId });
|
||||
}
|
||||
|
||||
const result = { success: true, itemType, item };
|
||||
this._logAction('giveItem', { itemType, properties }, result);
|
||||
const result = { success: true, item, npcId };
|
||||
this._logAction('giveItem', { npcId, itemType }, result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const result = { success: false, error: error.message };
|
||||
this._logAction('giveItem', { itemType, properties }, result);
|
||||
this._logAction('giveItem', { npcId, itemType }, result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show NPC's inventory items in container UI
|
||||
* @param {string} npcId - NPC identifier
|
||||
* @param {string[]} filterTypes - Array of item types to show (null = all)
|
||||
* @returns {Object} Result with success status
|
||||
*/
|
||||
showNPCInventory(npcId, filterTypes = null) {
|
||||
if (!npcId) {
|
||||
const result = { success: false, error: 'No npcId provided' };
|
||||
this._logAction('showNPCInventory', { npcId, filterTypes }, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
const npc = window.npcManager?.getNPC(npcId);
|
||||
if (!npc) {
|
||||
const result = { success: false, error: `NPC ${npcId} not found` };
|
||||
this._logAction('showNPCInventory', { npcId, filterTypes }, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!npc.itemsHeld || npc.itemsHeld.length === 0) {
|
||||
const result = { success: false, error: `NPC ${npcId} has no items` };
|
||||
this._logAction('showNPCInventory', { npcId, filterTypes }, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Filter items if types specified
|
||||
let itemsToShow = npc.itemsHeld;
|
||||
if (filterTypes && filterTypes.length > 0) {
|
||||
itemsToShow = npc.itemsHeld.filter(item =>
|
||||
filterTypes.includes(item.type)
|
||||
);
|
||||
}
|
||||
|
||||
if (itemsToShow.length === 0) {
|
||||
const result = { success: false, error: 'No matching items to show' };
|
||||
this._logAction('showNPCInventory', { npcId, filterTypes }, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Open container minigame in NPC mode
|
||||
if (window.startContainerMinigame) {
|
||||
window.startContainerMinigame({
|
||||
name: `${npc.displayName}'s Items`,
|
||||
contents: itemsToShow,
|
||||
mode: 'npc',
|
||||
npcId: npcId,
|
||||
npcDisplayName: npc.displayName,
|
||||
npcAvatar: npc.avatar
|
||||
});
|
||||
|
||||
const result = { success: true, npcId, itemCount: itemsToShow.length };
|
||||
this._logAction('showNPCInventory', { npcId, filterTypes }, result);
|
||||
return result;
|
||||
} else {
|
||||
const result = { success: false, error: 'Container minigame not available' };
|
||||
this._logAction('showNPCInventory', { npcId, filterTypes }, result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -414,7 +457,8 @@ if (typeof window !== 'undefined') {
|
||||
|
||||
// Register convenience methods globally for Ink
|
||||
window.npcUnlockDoor = (roomId) => bridge.unlockDoor(roomId);
|
||||
window.npcGiveItem = (itemType, properties) => bridge.giveItem(itemType, properties);
|
||||
window.npcGiveItem = (npcId, itemType) => bridge.giveItem(npcId, itemType);
|
||||
window.npcShowInventory = (npcId, filterTypes) => bridge.showNPCInventory(npcId, filterTypes);
|
||||
window.npcSetObjective = (text) => bridge.setObjective(text);
|
||||
window.npcRevealSecret = (secretId, data) => bridge.revealSecret(secretId, data);
|
||||
window.npcAddNote = (title, content) => bridge.addNote(title, content);
|
||||
|
||||
@@ -65,7 +65,8 @@ export default class NPCManager {
|
||||
metadata: {},
|
||||
eventMappings: {},
|
||||
phoneId: 'player_phone', // Default to player's phone
|
||||
npcType: 'phone' // Default to phone-based NPC
|
||||
npcType: 'phone', // Default to phone-based NPC
|
||||
itemsHeld: [] // Initialize empty inventory for NPC item giving
|
||||
}, realOpts);
|
||||
|
||||
this.npcs.set(realId, entry);
|
||||
|
||||
@@ -1020,3 +1020,5 @@ Room Loading → Container System → Unlock System → Inventory System
|
||||
**Confidence:** High - architecture already supports this model (see ARCHITECTURE_COMPARISON.md)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -839,3 +839,5 @@ end
|
||||
This approach balances security, UX, and development effort.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1971,3 +1971,5 @@ This comprehensive plan provides:
|
||||
The architecture supports both standalone operation and mounting in host applications, making it flexible and maintainable.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -621,3 +621,5 @@ For questions about this migration plan, contact the development team or file an
|
||||
**Happy migrating! 🚀**
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -13,6 +13,12 @@ VAR asked_about_self = false
|
||||
VAR asked_about_ceo = false
|
||||
VAR asked_for_items = false
|
||||
|
||||
// NPC item inventory variables (synced from itemsHeld array)
|
||||
VAR has_lockpick = false
|
||||
VAR has_workstation = false
|
||||
VAR has_phone = false
|
||||
VAR has_keycard = false
|
||||
|
||||
=== start ===
|
||||
# speaker:npc
|
||||
Hey there! I'm here to help you out if you need it. 👋
|
||||
@@ -40,16 +46,11 @@ What can I do for you?
|
||||
}
|
||||
|
||||
// Items - changes based on state
|
||||
{asked_about_self and not has_given_lockpick:
|
||||
{asked_about_self and (has_lockpick or has_workstation or has_phone or has_keycard):
|
||||
+ [Do you have any items for me?]
|
||||
-> give_items
|
||||
}
|
||||
|
||||
{has_given_lockpick:
|
||||
+ [Got any other items for me?]
|
||||
-> other_items
|
||||
}
|
||||
|
||||
// Feedback option appears after using lockpick
|
||||
{saw_lockpick_used:
|
||||
+ [Thanks for the lockpick! It worked great.]
|
||||
@@ -80,7 +81,7 @@ What would you like to do?
|
||||
{has_unlocked_ceo:
|
||||
I already unlocked the CEO's office for you! Just head on in.
|
||||
-> hub
|
||||
- else:
|
||||
|- else:
|
||||
The CEO's office? That's a tough one...
|
||||
{trust_level >= 1:
|
||||
Alright, I trust you enough. Let me unlock that door for you.
|
||||
@@ -105,38 +106,26 @@ Let me know!
|
||||
|
||||
=== give_items ===
|
||||
# speaker:npc
|
||||
{has_given_lockpick:
|
||||
I already gave you a lockpick set. Check your inventory - it should be there!
|
||||
{not has_lockpick and not has_workstation and not has_phone and not has_keycard:
|
||||
Sorry, I don't have any items to give you right now.
|
||||
-> hub
|
||||
- else:
|
||||
Let me see what I have...
|
||||
|- else:
|
||||
{trust_level >= 2:
|
||||
Here's a lockpick set. Use it to open locked doors and containers! 🔓
|
||||
~ has_given_lockpick = true
|
||||
Let me show you what I have for you!
|
||||
#give_npc_inventory_items
|
||||
~ asked_for_items = true
|
||||
#give_item:lockpick
|
||||
~ trust_level = trust_level + 1
|
||||
Good luck out there!
|
||||
-> hub
|
||||
- else:
|
||||
I need to trust you more before I give you something like that.
|
||||
Build up some trust first - ask me questions or help me out!
|
||||
I have some items, but I need to trust you more first.
|
||||
Build up some trust - ask me questions!
|
||||
-> hub
|
||||
}
|
||||
}
|
||||
|
||||
=== other_items ===
|
||||
# speaker:npc
|
||||
{trust_level >= 4:
|
||||
I've got a keycard for restricted areas. Think you can use it responsibly?
|
||||
#give_item:keycard
|
||||
~ trust_level = trust_level + 1
|
||||
Use it wisely!
|
||||
-> hub
|
||||
- else:
|
||||
That's all I have right now. The lockpick set is your best tool for now.
|
||||
-> hub
|
||||
}
|
||||
I think I gave you most of what I had. Check your inventory!
|
||||
-> hub
|
||||
|
||||
=== lockpick_feedback ===
|
||||
Great! I'm glad it helped you out. That's what I'm here for.
|
||||
@@ -150,8 +139,8 @@ What else do you need?
|
||||
{has_unlocked_ceo:
|
||||
The CEO's office has evidence you're looking for. Search the desk thoroughly.
|
||||
Also, check any computers for sensitive files.
|
||||
- else:
|
||||
{has_given_lockpick:
|
||||
|- else:
|
||||
{has_lockpick:
|
||||
Try using that lockpick set on locked doors and containers around the building.
|
||||
You never know what secrets people hide behind locked doors!
|
||||
- else:
|
||||
@@ -170,29 +159,29 @@ Good luck!
|
||||
|
||||
// Triggered when player picks up the lockpick
|
||||
=== on_lockpick_pickup ===
|
||||
{has_given_lockpick:
|
||||
{has_lockpick:
|
||||
Great! You found the lockpick I gave you. Try it on a locked door or container!
|
||||
- else:
|
||||
Nice find! That lockpick set looks professional. Could be very useful. 🔓
|
||||
|- else:
|
||||
Nice find! That lockpick set looks professional. Could be very useful.
|
||||
}
|
||||
-> hub
|
||||
|
||||
// Triggered when player completes any lockpicking minigame
|
||||
=== on_lockpick_success ===
|
||||
~ saw_lockpick_used = true
|
||||
{has_given_lockpick:
|
||||
Excellent! Glad I could help you get through that. 🎯
|
||||
- else:
|
||||
Nice work getting through that lock! 🔓
|
||||
{has_lockpick:
|
||||
Excellent! Glad I could help you get through that.
|
||||
|- else:
|
||||
Nice work getting through that lock!
|
||||
}
|
||||
-> hub
|
||||
|
||||
// Triggered when player fails a lockpicking attempt
|
||||
=== on_lockpick_failed ===
|
||||
{has_given_lockpick:
|
||||
Don't give up! Lockpicking takes practice. Try adjusting the tension. 🔧
|
||||
{has_lockpick:
|
||||
Don't give up! Lockpicking takes practice. Try adjusting the tension.
|
||||
Want me to help you with anything else?
|
||||
- else:
|
||||
|- else:
|
||||
Tough break. Lockpicking isn't easy without the right tools...
|
||||
I might be able to help with that if you ask.
|
||||
}
|
||||
@@ -202,18 +191,18 @@ Good luck!
|
||||
=== on_door_unlocked ===
|
||||
~ saw_door_unlock = true
|
||||
{has_unlocked_ceo:
|
||||
Another door open! You're making great progress. 🚪✓
|
||||
- else:
|
||||
Another door open! You're making great progress.
|
||||
|- else:
|
||||
Nice! You found a way through that door. Keep going!
|
||||
}
|
||||
-> hub
|
||||
|
||||
// Triggered when player tries a locked door
|
||||
=== on_door_attempt ===
|
||||
That door's locked tight. You'll need to find a way to unlock it. 🔒
|
||||
That door's locked tight. You'll need to find a way to unlock it.
|
||||
{trust_level >= 2:
|
||||
Want me to help you out? Just ask!
|
||||
- else:
|
||||
|- else:
|
||||
{trust_level >= 1:
|
||||
I might be able to help if you get to know me better first.
|
||||
}
|
||||
@@ -223,9 +212,9 @@ That door's locked tight. You'll need to find a way to unlock it. 🔒
|
||||
// Triggered when player interacts with the CEO desk
|
||||
=== on_ceo_desk_interact ===
|
||||
{has_unlocked_ceo:
|
||||
The CEO's desk - you made it! Nice work. 📋
|
||||
The CEO's desk - you made it! Nice work.
|
||||
That's where the important evidence is kept.
|
||||
- else:
|
||||
|- else:
|
||||
Trying to get into the CEO's office? I might be able to help with that...
|
||||
}
|
||||
-> hub
|
||||
@@ -233,20 +222,20 @@ That door's locked tight. You'll need to find a way to unlock it. 🔒
|
||||
// Triggered when player picks up any item
|
||||
=== on_item_found ===
|
||||
{trust_level >= 1:
|
||||
Good find! Every item could be important for your mission. 📦
|
||||
Good find! Every item could be important for your mission.
|
||||
}
|
||||
-> hub
|
||||
|
||||
// Triggered when player enters any room (general progress check)
|
||||
=== on_room_entered ===
|
||||
{has_unlocked_ceo:
|
||||
Keep searching for that evidence! 🔍
|
||||
- else:
|
||||
Keep searching for that evidence!
|
||||
|- else:
|
||||
{trust_level >= 1:
|
||||
You're making progress through the building. 🚶
|
||||
You're making progress through the building.
|
||||
Let me know if you need help with anything.
|
||||
- else:
|
||||
Exploring new areas... 🚶
|
||||
Exploring new areas...
|
||||
}
|
||||
}
|
||||
-> hub
|
||||
@@ -254,13 +243,13 @@ That door's locked tight. You'll need to find a way to unlock it. 🔒
|
||||
// Triggered when player discovers a new room for the first time
|
||||
=== on_room_discovered ===
|
||||
{trust_level >= 2:
|
||||
Great find! This new area might have what we need. 🗺️✨
|
||||
Great find! This new area might have what we need.
|
||||
Search it thoroughly!
|
||||
- else:
|
||||
|- else:
|
||||
{trust_level >= 1:
|
||||
Interesting! You've found a new area. Be careful exploring. 🗺️
|
||||
Interesting! You've found a new area. Be careful exploring.
|
||||
- else:
|
||||
A new room... wonder what's inside. 🚪
|
||||
A new room... wonder what's inside.
|
||||
}
|
||||
}
|
||||
-> hub
|
||||
@@ -268,12 +257,11 @@ That door's locked tight. You'll need to find a way to unlock it. 🔒
|
||||
// Triggered when player enters the CEO office
|
||||
=== on_ceo_office_entered ===
|
||||
{has_unlocked_ceo:
|
||||
You're in! Remember, you're looking for evidence of the data breach. 🕵️
|
||||
You're in! Remember, you're looking for evidence of the data breach.
|
||||
Check the desk, computer, and any drawers.
|
||||
- else:
|
||||
Whoa, you got into the CEO's office! That's impressive! 🎉
|
||||
|- else:
|
||||
Whoa, you got into the CEO's office! That's impressive!
|
||||
~ trust_level = trust_level + 1
|
||||
Maybe I underestimated you. Impressive work!
|
||||
}
|
||||
-> hub
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -10,42 +10,42 @@ Woop! Welcome! This is a group conversation test. Let me introduce you to my col
|
||||
=== group_meeting ===
|
||||
# speaker:npc:test_npc_back
|
||||
Agent, meet my colleague from the back office. BACK
|
||||
+ [Continue] -> colleague_introduction
|
||||
-> colleague_introduction
|
||||
|
||||
=== colleague_introduction ===
|
||||
# speaker:npc:test_npc_front
|
||||
Nice to meet you! I'm the lead technician here. FRONT.
|
||||
+ [Ask about their work] -> player_question
|
||||
-> player_question
|
||||
|
||||
=== player_question ===
|
||||
# speaker:player
|
||||
What kind of work do you both do here?
|
||||
+ [Listen] -> front_npc_explains
|
||||
-> front_npc_explains
|
||||
|
||||
=== front_npc_explains ===
|
||||
# speaker:npc:test_npc_back
|
||||
Well, I handle the front desk operations and guest interactions. But my colleague here...
|
||||
+ [Continue listening] -> colleague_responds
|
||||
-> colleague_responds
|
||||
|
||||
=== colleague_responds ===
|
||||
# speaker:npc:test_npc_front
|
||||
I manage all the backend systems and security infrastructure. Together, we keep everything running smoothly.
|
||||
+ [Respond] -> player_follow_up
|
||||
-> player_follow_up
|
||||
|
||||
=== player_follow_up ===
|
||||
# speaker:player
|
||||
That sounds like a well-coordinated operation!
|
||||
+ [Listen more] -> front_npc_agrees
|
||||
-> front_npc_agrees
|
||||
|
||||
=== front_npc_agrees ===
|
||||
# speaker:npc:test_npc_back
|
||||
It really is! We've been working together for several years now. Communication is key.
|
||||
+ [Hear more] -> colleague_adds
|
||||
-> colleague_adds
|
||||
|
||||
=== colleague_adds ===
|
||||
# speaker:npc:test_npc_front
|
||||
Exactly. And we're always looking for talented people like you to join our team.
|
||||
+ [Respond] -> player_closing
|
||||
-> player_closing
|
||||
|
||||
=== player_closing ===
|
||||
# speaker:player
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"npcs": [
|
||||
{
|
||||
"id": "test_npc_front",
|
||||
"displayName": "Front NPC",
|
||||
"displayName": "Helper NPC",
|
||||
"npcType": "person",
|
||||
"position": { "x": 5, "y": 3 },
|
||||
"spriteSheet": "hacker-red",
|
||||
@@ -32,7 +32,29 @@
|
||||
"idleFrameEnd": 23
|
||||
},
|
||||
"storyPath": "scenarios/ink/helper-npc.json",
|
||||
"currentKnot": "start"
|
||||
"currentKnot": "start",
|
||||
"itemsHeld": [
|
||||
{
|
||||
"type": "phone",
|
||||
"name": "Your Phone",
|
||||
"takeable": true,
|
||||
"phoneId": "player_phone",
|
||||
"npcIds": ["neye_eve", "gossip_girl", "helper_npc"],
|
||||
"observations": "Your personal phone with some interesting contacts"
|
||||
},
|
||||
{
|
||||
"type": "workstation",
|
||||
"name": "Crypto Analysis Station",
|
||||
"takeable": true,
|
||||
"observations": "A powerful workstation for cryptographic analysis"
|
||||
},
|
||||
{
|
||||
"type": "lockpick",
|
||||
"name": "Lock Pick Kit",
|
||||
"takeable": true,
|
||||
"observations": "A professional lock picking kit with various picks and tension wrenches"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "test_npc_back",
|
||||
@@ -47,9 +69,9 @@
|
||||
"storyPath": "scenarios/ink/test2.json",
|
||||
"currentKnot": "hub",
|
||||
"timedConversation": {
|
||||
"delay": 3000,
|
||||
"delay": 100,
|
||||
"targetKnot": "group_meeting",
|
||||
"background": "assets/mini-games/desktop-wallpaper.png"
|
||||
"background": "assets/backgrounds/hq1.png"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user