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:
Z. Cliffe Schreuders
2025-11-08 10:26:50 +00:00
parent 4dd2a839f4
commit 14bc9af43e
23 changed files with 450 additions and 165 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

BIN
assets/backgrounds/hq1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

BIN
assets/backgrounds/hq2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

BIN
assets/backgrounds/hq3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1020,3 +1020,5 @@ Room Loading → Container System → Unlock System → Inventory System
**Confidence:** High - architecture already supports this model (see ARCHITECTURE_COMPARISON.md)

View File

@@ -839,3 +839,5 @@ end
This approach balances security, UX, and development effort.

View File

@@ -1971,3 +1971,5 @@ This comprehensive plan provides:
The architecture supports both standalone operation and mounting in host applications, making it flexible and maintainable.

View File

@@ -621,3 +621,5 @@ For questions about this migration plan, contact the development team or file an
**Happy migrating! 🚀**

View File

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

View File

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

View File

@@ -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"
}
}
]