diff --git a/docs/NPC_ITEM_GIVING_EXAMPLES.md b/docs/NPC_ITEM_GIVING_EXAMPLES.md new file mode 100644 index 0000000..ea0b3a5 --- /dev/null +++ b/docs/NPC_ITEM_GIVING_EXAMPLES.md @@ -0,0 +1,271 @@ +# NPC Item Giving System - Usage Examples + +This document demonstrates how to use the NPC item giving system with both immediate and container-based approaches. + +## Setup + +NPCs must declare `itemsHeld` array in the scenario JSON: + +```json +{ + "id": "equipment_officer", + "displayName": "Equipment Officer", + "itemsHeld": [ + { + "type": "lockpick", + "name": "Lock Pick Kit", + "takeable": true, + "observations": "Professional lock picking set" + }, + { + "type": "workstation", + "name": "Analysis Workstation", + "takeable": true, + "observations": "Portable workstation" + } + ] +} +``` + +## Ink Variables + +Items are automatically synced to Ink variables: + +```ink +VAR has_lockpick = false +VAR has_workstation = false +VAR has_phone = false +VAR has_keycard = false +``` + +Declare only the `has_*` variables you need for your NPC. + +## Usage Pattern 1: Immediate Single Item Transfer + +**Tag:** `#give_item:type` + +**Use Case:** Give one specific item immediately without opening UI + +**Ink Example:** +```ink +=== give_basic_item === +Here's a lockpick set! +#give_item:lockpick +Good luck! +-> hub +``` + +**How it works:** +1. Player reads dialogue +2. Tag is processed +3. First `lockpick` from NPC's `itemsHeld` is added to inventory +4. Item is removed from NPC's inventory +5. Conversation continues + +**Notes:** +- Only gives the first matching item type +- No UI overlay +- Fast and clean for single items + +--- + +## Usage Pattern 2: Container UI - All Items + +**Tag:** `#give_npc_inventory_items` + +**Use Case:** Show all items the NPC is holding for player to choose from + +**Ink Example:** +```ink +=== give_all_items === +# speaker:npc +Here are all the tools I have available. Take what you need! +#give_npc_inventory_items +What else can I help with? +-> hub +``` + +**How it works:** +1. Tag is processed +2. Container minigame opens in "NPC mode" +3. Shows NPC's portrait and all items in `itemsHeld` +4. Player can take items from container +5. Each taken item is removed from NPC's inventory +6. Variables updated automatically +7. Conversation continues + +**Container UI Features:** +- NPC portrait/avatar displayed +- "Equipment Officer offers you items" header +- Grid of available items +- Player can examine each item before taking + +--- + +## Usage Pattern 3: Container UI - Filtered Items + +**Tag:** `#give_npc_inventory_items:type1,type2` + +**Use Case:** Show only specific item types from NPC's inventory + +**Ink Example:** +```ink +=== give_tools_only === +# speaker:npc +Here are the specialized tools we have. Choose what you need for the job. +#give_npc_inventory_items:lockpick,workstation +Let me know if you need anything else! +-> hub +``` + +**Comma-separated types:** +```ink +// Show lockpicks and keycards only +#give_npc_inventory_items:lockpick,keycard + +// Show workstations only +#give_npc_inventory_items:workstation +``` + +**How it works:** +1. Tag parsed for filter types +2. Container UI opens showing only matching items +3. If NPC has 2 lockpicks, 1 workstation, 1 keycard: + - With filter `lockpick,keycard`: shows 2 lockpicks + 1 keycard (not workstation) +4. Player can take from filtered list +5. Variables updated + +--- + +## Advanced Example: Conditional Item Giving + +**Ink Example:** +```ink +=== equipment_hub === +What tools do you need? + +{has_lockpick and has_workstation and has_keycard: + + [I have all the tools I need] + -> thanks_npc +} + +{has_lockpick or has_workstation or has_keycard: + + [Show me everything else you have] + #give_npc_inventory_items + -> equipment_hub +} + ++ [I need specialized tools] + -> show_tools + ++ [I need security access] + -> show_keycards + ++ [I'm good for now] + -> goodbye + +=== show_tools === +Here are our specialized tools: +#give_npc_inventory_items:lockpick,workstation +-> equipment_hub + +=== show_keycards === +Here are the access devices: +#give_npc_inventory_items:keycard +-> equipment_hub + +=== thanks_npc === +Perfect! Anything else? +-> equipment_hub + +=== goodbye === +Come back if you need anything! +-> END +``` + +--- + +## Complete Scenario Example + +From `npc-sprite-test2.json`: + +### Helper NPC (Immediate Item Transfer) +- Position: (5, 3) +- Story: `helper-npc.ink` +- Items: phone, workstation, lockpick +- Pattern: Uses `#give_item:type` for single items +- Demonstrates: Conditional dialogue that adapts to available items + +### Equipment Officer (Container-Based UI) +- Position: (8, 5) +- Story: `equipment-officer.ink` +- Items: 2 lockpicks (Basic & Advanced), 1 workstation, 1 keycard +- Pattern: Uses `#give_npc_inventory_items` to show container minigame +- Demonstrates: + - Container UI with NPC portrait + - Multiple items of same type + - All items displayed in interactive grid + - Items removed from NPC inventory as player takes them + - Conversation continues after item selection + +## Container Minigame UI Features (NPC Mode) + +When `#give_npc_inventory_items` is used, the container minigame opens with: + +1. **NPC Portrait/Avatar** - Displayed at top if available +2. **Custom Header** - "Equipment Officer offers you items" +3. **Item Grid** - Interactive grid showing all held items (or filtered items) +4. **Item Details** - Click items to see observations/descriptions +5. **Take Items** - Player can take any/all items shown +6. **Automatic Updates** - NPC inventory decreases as items taken +7. **Variable Sync** - `has_*` variables update in real-time + +Example dialogue flow: +``` +Player: "Show me what you have available" +→ Container minigame opens with NPC's items +→ Player takes "Advanced Lock Pick Kit" +→ NPC inventory now has 1 lockpick instead of 2 +→ has_lockpick variable remains true (still has items) +→ Conversation continues with "What else can I help with?" +``` + +--- + +## Event System + +When items are given, the `npc_items_changed` event is emitted: + +```javascript +window.eventDispatcher.emit('npc_items_changed', { npcId }); +``` + +This automatically triggers: +1. Variables re-sync in Ink +2. Conditions re-evaluated +3. Conversation state updated + +--- + +## Best Practices + +1. **Declare all `has_*` variables** you'll check in Ink at the top of your story +2. **Use immediate giving** (`#give_item`) for story-critical single items +3. **Use container UI** when offering multiple items or choices +4. **Use filtered container UI** for specialized equipment categories +5. **Check variables** in conditions to adapt dialogue based on what's available +6. **Remove items as they're taken** - they're automatically removed from NPC inventory + +--- + +## Testing + +To test the NPC item giving system: + +1. Load `npc-sprite-test2.json` scenario +2. Talk to "Helper NPC" (5, 3) - demonstrates immediate giving +3. Talk to "Equipment Officer" (8, 5) - demonstrates container UI +4. Try different dialogue paths to see variable updates +5. Verify items appear in player inventory +6. Check that NPC inventory decreases as items are taken + diff --git a/js/minigames/container/container-minigame.js b/js/minigames/container/container-minigame.js index 2121c12..de0aa83 100644 --- a/js/minigames/container/container-minigame.js +++ b/js/minigames/container/container-minigame.js @@ -559,13 +559,13 @@ export class ContainerMinigame extends MinigameScene { } // Function to start the container minigame -export function startContainerMinigame(containerItem, contents, isTakeable = false, desktopMode = null) { +export function startContainerMinigame(containerItem, contents, isTakeable = false, desktopMode = null, npcOptions = null) { // Auto-detect desktop mode if not explicitly set if (desktopMode === null) { desktopMode = shouldUseDesktopModeForContainer(containerItem); } - console.log('Starting container minigame', { containerItem, contents, isTakeable, desktopMode }); + console.log('Starting container minigame', { containerItem, contents, isTakeable, desktopMode, npcOptions }); // Initialize the minigame framework if not already done if (!window.MinigameFramework) { @@ -586,6 +586,12 @@ export function startContainerMinigame(containerItem, contents, isTakeable = fal desktopMode: desktopMode, cancelText: 'Close', showCancel: true, + ...(npcOptions && { + mode: npcOptions.mode || 'container', + npcId: npcOptions.npcId, + npcDisplayName: npcOptions.npcDisplayName, + npcAvatar: npcOptions.npcAvatar + }), onComplete: (success, result) => { console.log('Container minigame completed', { success, result }); } diff --git a/js/systems/npc-game-bridge.js b/js/systems/npc-game-bridge.js index 4a87584..7e74dea 100644 --- a/js/systems/npc-game-bridge.js +++ b/js/systems/npc-game-bridge.js @@ -179,66 +179,78 @@ export class NPCGameBridge { } } - /** - * 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; - } + /** + * 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; - } + 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; - } + 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) - ); - } + // 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; - } + 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 - }); + // Open container minigame in NPC mode + if (window.startContainerMinigame) { + // Create a pseudo-container item for the minigame + // The minigame expects a containerItem with scenarioData + const containerItem = { + scenarioData: { + name: `${npc.displayName}'s Items`, + type: 'npc_inventory', + observations: `Equipment and items held by ${npc.displayName}` + }, + name: 'NPC Inventory', + texture: { key: 'generic' }, + objectId: `npc_container_${npcId}` + }; - 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; + // Pass additional NPC context through the minigame + window.startContainerMinigame(containerItem, itemsToShow, false, null, { + 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; + } } - } /** * Set the current objective text diff --git a/scenarios/ink/equipment-officer.ink b/scenarios/ink/equipment-officer.ink new file mode 100644 index 0000000..068a404 --- /dev/null +++ b/scenarios/ink/equipment-officer.ink @@ -0,0 +1,55 @@ +// equipment-officer.ink +// NPC that demonstrates container-based item giving +// Shows all held items through the container minigame UI + +VAR trust_level = 0 +VAR has_lockpick = false +VAR has_workstation = false +VAR has_keycard = false + +=== start === +# speaker:npc +Welcome to equipment supply. I have various tools available. +What can I help you with? +~ trust_level = trust_level + 1 +-> hub + +=== hub === +{trust_level >= 1: + * [Show me what you have available] + -> show_inventory +} + +* [Tell me about your equipment] + -> about_equipment + +* [I'll come back later] + -> goodbye + +=== show_inventory === +# speaker:npc +Here's everything we have in stock. Take what you need! +#give_npc_inventory_items +What else can I help with? +-> hub + +=== show_inventory_filtered === +# speaker:npc +Here are the specialist tools: +#give_npc_inventory_items:lockpick,workstation +Let me know if you need access devices too! +-> hub + +=== about_equipment === +We supply equipment for fieldwork - lockpicking kits for access, workstations for analysis, and keycards for security. All essential tools for the job. +~ trust_level = trust_level + 1 ++ [Show me what you have] + -> show_inventory ++ [Never mind] + -> hub + +=== goodbye === +# speaker:npc +Come back when you need something! +-> END + diff --git a/scenarios/ink/equipment-officer.json b/scenarios/ink/equipment-officer.json new file mode 100644 index 0000000..49dab61 --- /dev/null +++ b/scenarios/ink/equipment-officer.json @@ -0,0 +1 @@ +{"inkVersion":21,"root":[[["done",{"#n":"g-0"}],null],"done",{"start":["#","^speaker:npc","/#","^Welcome to equipment supply. I have various tools available.","\n","^What can I help you with?","\n","ev",{"VAR?":"trust_level"},1,"+","/ev",{"VAR=":"trust_level","re":true},{"->":"hub"},null],"hub":[["ev",{"VAR?":"trust_level"},1,">=","/ev",[{"->":".^.b","c":true},{"b":["\n","ev","str","^Show me what you have available","/str","/ev",{"*":".^.c-0","flg":20},{"->":"hub.0.6"},{"c-0":["\n",{"->":"show_inventory"},{"#f":5}]}]}],"nop","\n","ev","str","^Tell me about your equipment","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^I'll come back later","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n",{"->":"about_equipment"},{"#f":5}],"c-1":["\n",{"->":"goodbye"},{"#f":5}]}],null],"show_inventory":["#","^speaker:npc","/#","^Here's everything we have in stock. Take what you need!","\n","#","^give_npc_inventory_items","/#","^What else can I help with?","\n",{"->":"hub"},null],"show_inventory_filtered":["#","^speaker:npc","/#","^Here are the specialist tools:","\n","#","^give_npc_inventory_items:lockpick,workstation","/#","^Let me know if you need access devices too!","\n",{"->":"hub"},null],"about_equipment":[["^We supply equipment for fieldwork - lockpicking kits for access, workstations for analysis, and keycards for security. All essential tools for the job.","\n","ev",{"VAR?":"trust_level"},1,"+","/ev",{"VAR=":"trust_level","re":true},"ev","str","^Show me what you have","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Never mind","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"show_inventory"},null],"c-1":["\n",{"->":"hub"},null]}],null],"goodbye":["#","^speaker:npc","/#","^Come back when you need something!","\n","end",null],"global decl":["ev",0,{"VAR=":"trust_level"},false,{"VAR=":"has_lockpick"},false,{"VAR=":"has_workstation"},false,{"VAR=":"has_keycard"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/scenarios/npc-sprite-test2.json b/scenarios/npc-sprite-test2.json index 3488321..ac60f13 100644 --- a/scenarios/npc-sprite-test2.json +++ b/scenarios/npc-sprite-test2.json @@ -73,6 +73,46 @@ "targetKnot": "group_meeting", "background": "assets/backgrounds/hq1.png" } + }, + { + "id": "container_test_npc", + "displayName": "Equipment Officer", + "npcType": "person", + "position": { "x": 8, "y": 5 }, + "spriteSheet": "hacker-red", + "spriteTalk": "assets/characters/hacker-red-talk.png", + "spriteConfig": { + "idleFrameStart": 20, + "idleFrameEnd": 23 + }, + "storyPath": "scenarios/ink/equipment-officer.json", + "currentKnot": "start", + "itemsHeld": [ + { + "type": "lockpick", + "name": "Basic Lock Pick Kit", + "takeable": true, + "observations": "A basic set of lock picking tools" + }, + { + "type": "lockpick", + "name": "Advanced Lock Pick Kit", + "takeable": true, + "observations": "An advanced set with precision tools" + }, + { + "type": "workstation", + "name": "Analysis Workstation", + "takeable": true, + "observations": "Portable analysis workstation" + }, + { + "type": "keycard", + "name": "Security Keycard", + "takeable": true, + "observations": "Electronic access keycard" + } + ] } ] }