Add NPC item giving examples and enhance container UI functionality

- Created a new documentation file detailing usage examples for the NPC item giving system, including immediate and container-based item transfers.
- Updated the ContainerMinigame to support additional NPC context, enhancing the user experience when interacting with NPC inventories.
- Implemented new NPC configurations in the scenario JSON to demonstrate item giving mechanics and container UI features.
- Added an Ink script for the Equipment Officer NPC, showcasing how to present items through the container UI and manage player interactions.
This commit is contained in:
Z. Cliffe Schreuders
2025-11-08 10:44:34 +00:00
parent 14bc9af43e
commit 14e2600e5c
6 changed files with 440 additions and 55 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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