mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-21 19:28:03 +00:00
Add helper NPC dialogue and interactions for mission assistance
- Created a new JSON file for the helper NPC with various dialogue options and interactions. - Implemented responses for asking about the NPC, unlocking the CEO's office, and providing items. - Enhanced trust-building mechanics with the NPC, allowing for item exchanges and hints based on player actions. - Updated existing NPC dialogue to integrate new features and improve player guidance throughout the mission.
This commit is contained in:
@@ -18,7 +18,6 @@
|
||||
padding: 0;
|
||||
background-color: #000;
|
||||
color: #fff;
|
||||
font-family: 'Arial', sans-serif;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -169,7 +168,6 @@
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: all 0.1s ease;
|
||||
font-family: 'Arial', sans-serif;
|
||||
}
|
||||
|
||||
.person-chat-choice-button:hover {
|
||||
@@ -201,7 +199,6 @@
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
transition: all 0.1s ease;
|
||||
font-family: 'Arial', sans-serif;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
@@ -252,7 +249,7 @@
|
||||
border: 2px solid #ff0000;
|
||||
color: #ff6b6b;
|
||||
padding: 10px;
|
||||
font-size: 13px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
/* Scrollbar styling for dialogue box - not needed with transparent background */
|
||||
@@ -275,15 +272,15 @@
|
||||
}
|
||||
|
||||
.person-chat-speaker-name {
|
||||
font-size: 12px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.person-chat-dialogue-text {
|
||||
font-size: 14px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.person-chat-choice-button {
|
||||
font-size: 12px;
|
||||
font-size: 18px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
618
docs/INK_BEST_PRACTICES.md
Normal file
618
docs/INK_BEST_PRACTICES.md
Normal file
@@ -0,0 +1,618 @@
|
||||
# Ink Story Writing Best Practices for Break Escape
|
||||
|
||||
## Speaker Tags - Critical for Dialogue Attribution
|
||||
|
||||
Every dialogue block in Break Escape Ink stories must include a speaker tag comment. This tells the game engine WHO is speaking so it displays the correct character portrait and name.
|
||||
|
||||
### Speaker Tag Format
|
||||
|
||||
```ink
|
||||
=== dialogue_block ===
|
||||
# speaker:npc
|
||||
This dialogue comes from the main NPC being talked to
|
||||
```
|
||||
|
||||
### Tag Types
|
||||
|
||||
| Tag Format | Usage | Example |
|
||||
|-----------|-------|---------|
|
||||
| `# speaker:npc` | Main NPC in single-NPC conversation | `# speaker:npc` |
|
||||
| `# speaker:player` | Player speaking | `# speaker:player` |
|
||||
| `# speaker:npc:sprite_id` | Specific character in multi-NPC scene | `# speaker:npc:test_npc_back` |
|
||||
|
||||
### Missing Tags? They Default Correctly!
|
||||
|
||||
**Important**: If a speaker tag is MISSING, the dialogue automatically attributes to the main NPC. This means:
|
||||
|
||||
- **Single-NPC conversations** can omit tags (simpler Ink)
|
||||
- **Multi-character conversations** MUST include tags to show who's speaking
|
||||
- **Player dialogue** can be explicitly tagged or inferred
|
||||
|
||||
### Examples
|
||||
|
||||
#### Single-Character (Tags Optional)
|
||||
```ink
|
||||
=== hub ===
|
||||
I'm here to help you progress.
|
||||
What can I do for you?
|
||||
-> hub
|
||||
```
|
||||
↑ Both lines default to the main NPC (this.npc.id)
|
||||
|
||||
#### Single-Character (Tags Explicit)
|
||||
```ink
|
||||
=== hub ===
|
||||
# speaker:npc
|
||||
I'm here to help you progress.
|
||||
# speaker:npc
|
||||
What can I do for you?
|
||||
-> hub
|
||||
```
|
||||
↑ Same result, but more explicit
|
||||
|
||||
#### Multi-Character (Tags Required!)
|
||||
```ink
|
||||
=== meeting ===
|
||||
# speaker:npc:test_npc_back
|
||||
Hey, meet my colleague from the back office.
|
||||
|
||||
# speaker:npc:test_npc_front
|
||||
Nice to meet you! I manage the backend systems.
|
||||
|
||||
# speaker:player
|
||||
That sounds interesting.
|
||||
|
||||
# speaker:npc:test_npc_back
|
||||
We work great together!
|
||||
```
|
||||
↑ Tags MUST be present so the correct portraits appear
|
||||
|
||||
### Technical Implementation
|
||||
|
||||
The game engine uses these tags to:
|
||||
|
||||
1. **Determine which character portrait to show** - Main NPC or secondary character
|
||||
2. **Set the speaker label** - Shows character name above dialogue
|
||||
3. **Style the dialogue bubble** - NPC vs Player styling
|
||||
4. **Track multi-character conversations** - Knows who said what when
|
||||
|
||||
**Code location**: `js/minigames/person-chat/person-chat-minigame.js` → `determineSpeaker()` and `createDialogueBlocks()`
|
||||
|
||||
---
|
||||
|
||||
## Core Design Pattern: Hub-Based Conversations
|
||||
|
||||
Break Escape conversations follow a **hub-based loop** pattern where NPCs provide repeatable interactions without hard endings.
|
||||
|
||||
### Why Hub-Based?
|
||||
|
||||
1. **State Persistence** - Variables (favour, items earned, flags) accumulate naturally across multiple interactions
|
||||
2. **Dynamic Content** - Use Ink conditionals to show different options based on player progress
|
||||
3. **Continuous Evolution** - NPCs can "remember" conversations and respond differently
|
||||
4. **Educational Flow** - Mirrors real learning where concepts build on each other
|
||||
|
||||
## Standard Ink Structure
|
||||
|
||||
### Template
|
||||
|
||||
```ink
|
||||
VAR npc_name = "NPC"
|
||||
VAR favour = 0
|
||||
VAR has_learned_about_passwords = false
|
||||
|
||||
=== start ===
|
||||
# speaker:npc
|
||||
~ favour += 1
|
||||
{npc_name}: Hello! What would you like to know?
|
||||
-> hub
|
||||
|
||||
=== hub ===
|
||||
* [Ask about passwords]
|
||||
~ has_learned_about_passwords = true
|
||||
~ favour += 1
|
||||
-> ask_passwords
|
||||
* [Make small talk]
|
||||
-> small_talk
|
||||
* [Leave] #exit_conversation
|
||||
# speaker:npc
|
||||
{npc_name}: See you around!
|
||||
-> hub
|
||||
|
||||
=== ask_passwords ===
|
||||
# speaker:npc
|
||||
{npc_name}: Passwords should be...
|
||||
-> hub
|
||||
|
||||
=== small_talk ===
|
||||
# speaker:npc
|
||||
{npc_name}: Nice weather we're having.
|
||||
-> hub
|
||||
```
|
||||
|
||||
### Key Points
|
||||
|
||||
1. **Hub Section**: Central "choice point" that always loops back
|
||||
2. **Exit Choice**: Include a "Leave" option with `#exit_conversation` tag
|
||||
3. **Variables**: Increment favour/flags on meaningful choices
|
||||
4. **No Hard END**: Avoid `-> END` for loop-based conversations
|
||||
|
||||
## Exit Strategy: `#exit_conversation` Tag
|
||||
|
||||
### What It Does
|
||||
|
||||
When a player selects a choice tagged with `#exit_conversation`:
|
||||
1. The dialogue plays normally
|
||||
2. After the NPC response, the minigame closes automatically
|
||||
3. All conversation state (variables, progress) is saved
|
||||
4. Player returns to the game world
|
||||
|
||||
### Usage
|
||||
|
||||
```ink
|
||||
+ [I need to go] #exit_conversation
|
||||
{npc_name}: Okay, come back anytime!
|
||||
-> hub
|
||||
```
|
||||
|
||||
### Important
|
||||
|
||||
- The NPC still responds to the choice
|
||||
- Variables continue to accumulate
|
||||
- Story state is saved with all progression
|
||||
- On next conversation, story picks up from where it left off
|
||||
|
||||
## Handling Repeated Interactions
|
||||
|
||||
Break Escape uses Ink's built-in features to manage menu options across multiple conversations.
|
||||
|
||||
### Pattern 1: Remove Option After First Visit (`once`)
|
||||
|
||||
Use `once { }` to show a choice only the first time:
|
||||
|
||||
```ink
|
||||
=== hub ===
|
||||
once {
|
||||
* [Introduce yourself]
|
||||
-> introduction
|
||||
}
|
||||
+ [Leave] #exit_conversation
|
||||
-> hub
|
||||
```
|
||||
|
||||
**Result:**
|
||||
- 1st visit: "Introduce yourself" appears
|
||||
- 2nd+ visits: "Introduce yourself" is hidden
|
||||
|
||||
### Pattern 2: Change Menu Text on Repeat (`sticky`)
|
||||
|
||||
Use `sticky { }` with conditionals to show different options:
|
||||
|
||||
```ink
|
||||
VAR asked_question = false
|
||||
|
||||
=== hub ===
|
||||
sticky {
|
||||
+ {asked_question: [Remind me about that question]}
|
||||
-> question_reminder
|
||||
+ {not asked_question: [Ask a question]}
|
||||
-> question
|
||||
}
|
||||
+ [Leave] #exit_conversation -> hub
|
||||
|
||||
=== question ===
|
||||
~ asked_question = true
|
||||
NPC: Here's the answer...
|
||||
-> hub
|
||||
|
||||
=== question_reminder ===
|
||||
NPC: As I said before...
|
||||
-> hub
|
||||
```
|
||||
|
||||
**Result:**
|
||||
- 1st visit: "Ask a question"
|
||||
- 2nd+ visits: "Remind me about that question"
|
||||
|
||||
### Pattern 3: Show Different Content Based on Progress
|
||||
|
||||
Use variable conditionals anywhere:
|
||||
|
||||
```ink
|
||||
VAR favour = 0
|
||||
VAR has_learned_x = false
|
||||
|
||||
=== hub ===
|
||||
+ {favour < 5: [Ask politely]}
|
||||
~ favour += 1
|
||||
-> polite_ask
|
||||
+ {favour >= 5: [Ask as a friend]}
|
||||
~ favour += 2
|
||||
-> friend_ask
|
||||
+ [Leave] #exit_conversation -> hub
|
||||
```
|
||||
|
||||
### Combining Patterns
|
||||
|
||||
```ink
|
||||
VAR asked_quest = false
|
||||
VAR quest_complete = false
|
||||
|
||||
=== hub ===
|
||||
// This option appears only once
|
||||
once {
|
||||
* [You mentioned a quest?]
|
||||
~ asked_quest = true
|
||||
-> quest_explanation
|
||||
}
|
||||
|
||||
// These options change based on state
|
||||
sticky {
|
||||
+ {asked_quest and not quest_complete: [Any progress on that quest?]}
|
||||
-> quest_progress
|
||||
+ {quest_complete: [Quest complete! Any rewards?]}
|
||||
-> quest_rewards
|
||||
}
|
||||
|
||||
+ [Leave] #exit_conversation -> hub
|
||||
```
|
||||
|
||||
### How Variables Persist
|
||||
|
||||
Variables are automatically saved and restored:
|
||||
|
||||
```ink
|
||||
VAR conversation_count = 0
|
||||
|
||||
=== start ===
|
||||
~ conversation_count += 1
|
||||
NPC: This is conversation #{conversation_count}
|
||||
-> hub
|
||||
```
|
||||
|
||||
**Session 1:** conversation_count = 1
|
||||
**Session 2:** conversation_count = 2 (starts at 1, increments to 2)
|
||||
**Session 3:** conversation_count = 3
|
||||
|
||||
The variable keeps incrementing across all conversations!
|
||||
|
||||
## State Saving Strategy
|
||||
|
||||
### Automatic Saving
|
||||
|
||||
- State saves **immediately after each choice** is made
|
||||
- Variables persist across multiple conversations
|
||||
- No manual save required
|
||||
|
||||
### What Gets Saved
|
||||
|
||||
```javascript
|
||||
{
|
||||
storyState: "...", // Full Ink state (for resuming mid-conversation)
|
||||
variables: { favour: 5 }, // Extracted variables (used when restarting)
|
||||
timestamp: 1699207400000 // When it was saved
|
||||
}
|
||||
```
|
||||
|
||||
### Resumption Behavior
|
||||
|
||||
1. **Mid-Conversation Resume** (has `storyState`)
|
||||
- Story picks up exactly where it left off
|
||||
- Full narrative context preserved
|
||||
|
||||
2. **After Hard END** (only `variables`)
|
||||
- Story restarts from `=== start ===`
|
||||
- Variables are pre-loaded
|
||||
- Conditionals can show different options based on prior interactions
|
||||
|
||||
## Advanced Patterns
|
||||
|
||||
### Favour/Reputation System
|
||||
|
||||
```ink
|
||||
VAR favour = 0
|
||||
|
||||
=== hub ===
|
||||
{favour >= 5:
|
||||
+ [You seem to like me...]
|
||||
~ favour += 1
|
||||
-> compliment_response
|
||||
}
|
||||
+ [What's up?]
|
||||
~ favour += 1
|
||||
-> greeting
|
||||
+ [Leave] #exit_conversation
|
||||
-> hub
|
||||
```
|
||||
|
||||
### Unlocking Questlines
|
||||
|
||||
```ink
|
||||
VAR has_quest = false
|
||||
VAR quest_complete = false
|
||||
|
||||
=== hub ===
|
||||
{not has_quest:
|
||||
+ [Do you need help?]
|
||||
~ has_quest = true
|
||||
-> offer_quest
|
||||
}
|
||||
{has_quest and not quest_complete:
|
||||
+ [Is the quest done?]
|
||||
-> check_quest
|
||||
}
|
||||
* [Leave] #exit_conversation
|
||||
-> hub
|
||||
```
|
||||
|
||||
### Dialogue That Changes Based on Progress
|
||||
|
||||
```ink
|
||||
=== greet ===
|
||||
{conversation_count == 1:
|
||||
{npc_name}: Oh, a new face! I'm {npc_name}.
|
||||
}
|
||||
{conversation_count == 2:
|
||||
{npc_name}: Oh, you're back! Nice to see you again.
|
||||
}
|
||||
{conversation_count > 2:
|
||||
{npc_name}: Welcome back, my friend! How are you doing?
|
||||
}
|
||||
-> hub
|
||||
```
|
||||
|
||||
## Anti-Patterns (Avoid These)
|
||||
|
||||
❌ **Hard Endings Without Hub**
|
||||
```ink
|
||||
=== conversation ===
|
||||
{npc_name}: That's all I have to say.
|
||||
-> END
|
||||
```
|
||||
*Problem: Player can't interact again, variables become stuck*
|
||||
|
||||
❌ **Showing Same Option Repeatedly**
|
||||
```ink
|
||||
=== hub ===
|
||||
+ [Learn about X] -> learn_x
|
||||
+ [Learn about X] -> learn_x // This appears EVERY time!
|
||||
```
|
||||
*Better: Use `once { }` or `sticky { }` with conditionals*
|
||||
|
||||
❌ **Forgetting to Mark Topics as Visited**
|
||||
```ink
|
||||
=== hub ===
|
||||
+ [Ask about passwords]
|
||||
-> ask_passwords
|
||||
|
||||
=== ask_passwords ===
|
||||
NPC: Passwords should be strong...
|
||||
-> hub
|
||||
```
|
||||
*Problem: Player sees "Ask about passwords" every time*
|
||||
|
||||
*Better: Track it with a variable*
|
||||
```ink
|
||||
VAR asked_passwords = false
|
||||
|
||||
=== ask_passwords ===
|
||||
~ asked_passwords = true
|
||||
NPC: Passwords should be strong...
|
||||
-> hub
|
||||
```
|
||||
|
||||
❌ **Mixing Exit and END**
|
||||
```ink
|
||||
=== hub ===
|
||||
+ [Leave] #exit_conversation
|
||||
-> END
|
||||
```
|
||||
*Problem: Confused state logic. Use `#exit_conversation` OR `-> END`, not both*
|
||||
|
||||
❌ **Conditional Without Variable**
|
||||
```ink
|
||||
=== hub ===
|
||||
+ {talked_before: [Remind me]} // 'talked_before' undefined!
|
||||
-> reminder
|
||||
```
|
||||
*Better: Define the variable first*
|
||||
```ink
|
||||
VAR talked_before = false
|
||||
|
||||
=== ask_something ===
|
||||
~ talked_before = true
|
||||
-> hub
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
### Check Saved State
|
||||
|
||||
```javascript
|
||||
// In browser console
|
||||
window.npcConversationStateManager.getNPCState('npc_id')
|
||||
```
|
||||
|
||||
### Clear State (Testing)
|
||||
|
||||
```javascript
|
||||
window.npcConversationStateManager.clearNPCState('npc_id')
|
||||
```
|
||||
|
||||
### View All NPCs with Saved State
|
||||
|
||||
```javascript
|
||||
window.npcConversationStateManager.getSavedNPCs()
|
||||
```
|
||||
|
||||
## Testing Your Ink Story
|
||||
|
||||
1. **First Interaction**: Variables should start at defaults
|
||||
2. **Make a Choice**: Favour/flags should increment
|
||||
3. **Exit**: Should save all variables
|
||||
4. **Return**: Should have same favour, new options may appear
|
||||
5. **Hard END (if used)**: Should only save variables, restart fresh
|
||||
|
||||
## Real-World Example: Security Expert NPC
|
||||
|
||||
Here's a complete example showing all techniques combined:
|
||||
|
||||
```ink
|
||||
VAR expert_name = "Security Expert"
|
||||
VAR favour = 0
|
||||
|
||||
VAR learned_passwords = false
|
||||
VAR learned_phishing = false
|
||||
VAR learned_mfa = false
|
||||
|
||||
VAR task_given = false
|
||||
VAR task_complete = false
|
||||
|
||||
=== start ===
|
||||
~ favour += 1
|
||||
{expert_name}: Welcome back! Good to see you again.
|
||||
-> hub
|
||||
|
||||
=== hub ===
|
||||
// Introduction - appears only once
|
||||
once {
|
||||
* [I'd like to learn about cybersecurity]
|
||||
-> introduction
|
||||
}
|
||||
|
||||
// Password topic - changes on repeat
|
||||
sticky {
|
||||
+ {learned_passwords: [Tell me more about password security]}
|
||||
-> passwords_advanced
|
||||
+ {not learned_passwords: [How do I create strong passwords?]}
|
||||
-> learn_passwords
|
||||
}
|
||||
|
||||
// Phishing topic - only shows after passwords are learned
|
||||
{learned_passwords:
|
||||
sticky {
|
||||
+ {learned_phishing: [Any new phishing threats?]}
|
||||
-> phishing_update
|
||||
+ {not learned_phishing: [What about phishing attacks?]}
|
||||
-> learn_phishing
|
||||
}
|
||||
}
|
||||
|
||||
// MFA topic - conditional unlock
|
||||
{learned_passwords and learned_phishing:
|
||||
sticky {
|
||||
+ {learned_mfa: [More about multi-factor authentication?]}
|
||||
-> mfa_advanced
|
||||
+ {not learned_mfa: [I've heard about multi-factor authentication...]}
|
||||
-> learn_mfa
|
||||
}
|
||||
}
|
||||
|
||||
// Tasks appear based on what they've learned
|
||||
{learned_passwords and learned_phishing and not task_given:
|
||||
+ [Do you have any tasks for me?]
|
||||
~ task_given = true
|
||||
-> task_offer
|
||||
}
|
||||
|
||||
{task_given and not task_complete:
|
||||
+ [I completed that task]
|
||||
~ task_complete = true
|
||||
~ favour += 5
|
||||
-> task_complete_response
|
||||
}
|
||||
|
||||
{favour >= 20:
|
||||
+ [You seem to trust me now...]
|
||||
~ favour += 2
|
||||
-> friendship_response
|
||||
}
|
||||
|
||||
+ [Leave] #exit_conversation
|
||||
{expert_name}: Great work! Keep learning.
|
||||
-> hub
|
||||
|
||||
=== introduction ===
|
||||
{expert_name}: Cybersecurity is all about protecting data and systems.
|
||||
{expert_name}: I can teach you the fundamentals, starting with passwords.
|
||||
-> hub
|
||||
|
||||
=== learn_passwords ===
|
||||
~ learned_passwords = true
|
||||
~ favour += 1
|
||||
{expert_name}: Strong passwords are your first line of defense.
|
||||
{expert_name}: Use at least 12 characters, mixed case, numbers, and symbols.
|
||||
-> hub
|
||||
|
||||
=== passwords_advanced ===
|
||||
{expert_name}: Consider using a password manager like Bitwarden or 1Password.
|
||||
{expert_name}: This way you don't have to remember complex passwords.
|
||||
-> hub
|
||||
|
||||
=== learn_phishing ===
|
||||
~ learned_phishing = true
|
||||
~ favour += 1
|
||||
{expert_name}: Phishing emails trick people into revealing sensitive data.
|
||||
{expert_name}: Always verify sender email addresses and never click suspicious links.
|
||||
-> hub
|
||||
|
||||
=== phishing_update ===
|
||||
{expert_name}: New phishing techniques are emerging every day.
|
||||
{expert_name}: Stay vigilant and report suspicious emails to your IT team.
|
||||
-> hub
|
||||
|
||||
=== learn_mfa ===
|
||||
~ learned_mfa = true
|
||||
~ favour += 1
|
||||
{expert_name}: Multi-factor authentication adds an extra security layer.
|
||||
{expert_name}: Even if someone has your password, they can't log in without the second factor.
|
||||
-> hub
|
||||
|
||||
=== mfa_advanced ===
|
||||
{expert_name}: The most secure setup uses a hardware security key like YubiKey.
|
||||
{expert_name}: SMS codes work too, but authenticator apps are better.
|
||||
-> hub
|
||||
|
||||
=== task_offer ===
|
||||
{expert_name}: I need you to audit our password policies.
|
||||
{expert_name}: Can you check if our employees are following best practices?
|
||||
-> hub
|
||||
|
||||
=== task_complete_response ===
|
||||
{expert_name}: Excellent work! Your audit found several issues we need to fix.
|
||||
{expert_name}: You're becoming quite the security expert yourself!
|
||||
-> hub
|
||||
|
||||
=== friendship_response ===
|
||||
{expert_name}: You've learned so much, and I can see your dedication.
|
||||
{expert_name}: I'd like to bring you into our security team permanently.
|
||||
-> hub
|
||||
```
|
||||
|
||||
**Key Features Demonstrated:**
|
||||
- ✅ `once { }` for one-time intro
|
||||
- ✅ `sticky { }` for "tell me more" options
|
||||
- ✅ Conditionals for unlocking content
|
||||
- ✅ Variable tracking (learned_X, favour)
|
||||
- ✅ Task progression system
|
||||
- ✅ Friendship levels based on favour
|
||||
- ✅ Proper hub structure
|
||||
|
||||
## Common Questions
|
||||
|
||||
**Q: Should I use `-> END` or hub loop?**
|
||||
A: Use hub loop for NPCs that should be repeatable. Use `-> END` only for one-time narrative moments.
|
||||
|
||||
**Q: How do I show different dialogue on repeat conversations?**
|
||||
A: Use Ink conditionals with variables like `{conversation_count > 1:` or `{favour >= 5:`
|
||||
|
||||
**Q: Can I have both choices and auto-advance?**
|
||||
A: Yes! After showing choices, the hub is reached. Use `-> hub` to loop.
|
||||
|
||||
**Q: What if I need to end a conversation for story reasons?**
|
||||
A: Use a choice with dialogue that feels like an ending, then loop back to hub. Or use `#exit_conversation` to close the minigame while keeping state.
|
||||
|
||||
**Q: What's the difference between `once` and `sticky`?**
|
||||
A: `once` shows content only once then hides it. `sticky` shows different content based on conditions. Use `once` for introductions, use `sticky` to change menu text.
|
||||
|
||||
**Q: Can I have unlimited options in a hub?**
|
||||
A: Yes! But for good UX, keep it to 3-5 main options plus "Leave". Use conditionals to show/hide options based on player progress.
|
||||
@@ -107,7 +107,7 @@
|
||||
<div class="popup-overlay"></div>
|
||||
|
||||
<!-- Main Game JavaScript Module -->
|
||||
<script type="module" src="js/main.js?v=41"></script>
|
||||
<script type="module" src="js/main.js?v=45"></script>
|
||||
|
||||
<!-- Mobile touch handling -->
|
||||
<script>
|
||||
|
||||
@@ -10,7 +10,7 @@ export { BluetoothScannerMinigame, startBluetoothScannerMinigame } from './bluet
|
||||
export { BiometricsMinigame, startBiometricsMinigame } from './biometrics/biometrics-minigame.js';
|
||||
export { ContainerMinigame, startContainerMinigame, returnToContainerAfterNotes } from './container/container-minigame.js';
|
||||
export { PhoneChatMinigame, returnToPhoneAfterNotes } from './phone-chat/phone-chat-minigame.js';
|
||||
export { PersonChatMinigame } from './person-chat/person-chat-minigame.js?v=8';
|
||||
export { PersonChatMinigame } from './person-chat/person-chat-minigame.js?v=11';
|
||||
export { PinMinigame, startPinMinigame } from './pin/pin-minigame.js';
|
||||
export { PasswordMinigame } from './password/password-minigame.js';
|
||||
export { TextFileMinigame, returnToTextFileAfterNotes } from './text-file/text-file-minigame.js';
|
||||
@@ -58,7 +58,7 @@ import { ContainerMinigame, startContainerMinigame, returnToContainerAfterNotes
|
||||
import { PhoneChatMinigame, returnToPhoneAfterNotes } from './phone-chat/phone-chat-minigame.js';
|
||||
|
||||
// Import the person chat minigame (In-person NPC conversations)
|
||||
import { PersonChatMinigame } from './person-chat/person-chat-minigame.js?v=8';
|
||||
import { PersonChatMinigame } from './person-chat/person-chat-minigame.js?v=10';
|
||||
|
||||
// Import the PIN minigame
|
||||
import { PinMinigame, startPinMinigame } from './pin/pin-minigame.js';
|
||||
|
||||
@@ -299,7 +299,13 @@ export class PersonChatMinigame extends MinigameScene {
|
||||
|
||||
// Check if story has ended
|
||||
if (result.hasEnded) {
|
||||
this.endConversation();
|
||||
// Story reached an END - save state and show message
|
||||
// Player should press ESC to exit and return to hub
|
||||
if (this.inkEngine && this.inkEngine.story) {
|
||||
npcConversationStateManager.saveNPCState(this.npcId, this.inkEngine.story);
|
||||
}
|
||||
this.ui.showDialogue('(End of conversation - press ESC to exit)', 'system');
|
||||
console.log('🏁 Story has reached an end point');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -351,10 +357,14 @@ export class PersonChatMinigame extends MinigameScene {
|
||||
|
||||
/**
|
||||
* Determine who is speaking based on Ink tags
|
||||
* Supports speaker tags like:
|
||||
* - # speaker:player
|
||||
* - # speaker:npc (defaults to main NPC)
|
||||
* - # speaker:npc_id:character_id (specific character)
|
||||
*
|
||||
* SPEAKER TAG FORMATS:
|
||||
* - # speaker:player → Player is speaking
|
||||
* - # speaker:npc → Main NPC being talked to
|
||||
* - # speaker:npc:sprite_id → Specific character (multi-character conversations)
|
||||
*
|
||||
* If no speaker tag is present, dialogue DEFAULTS to the main NPC
|
||||
* This allows simple single-NPC conversations to omit the tag
|
||||
*
|
||||
* @param {Object} result - Result from conversation.continue()
|
||||
* @returns {string} Character ID of speaker (player, npc_id, or main NPC id)
|
||||
@@ -407,8 +417,12 @@ export class PersonChatMinigame extends MinigameScene {
|
||||
try {
|
||||
console.log(`📝 Choice selected: ${choiceIndex}`);
|
||||
|
||||
// Get the choice text from lastResult before making the choice
|
||||
const choiceText = this.lastResult.choices[choiceIndex]?.text || '';
|
||||
// Get the choice object to check for tags
|
||||
const choice = this.lastResult.choices[choiceIndex];
|
||||
const choiceText = choice?.text || '';
|
||||
|
||||
// Check if this choice has the exit_conversation tag
|
||||
const shouldExit = choice?.tags?.some(tag => tag.includes('exit_conversation'));
|
||||
|
||||
// Clear choice buttons immediately
|
||||
this.ui.hideChoices();
|
||||
@@ -416,16 +430,37 @@ export class PersonChatMinigame extends MinigameScene {
|
||||
// Make choice in conversation (this also calls continue() internally)
|
||||
const result = this.conversation.makeChoice(choiceIndex);
|
||||
|
||||
// Save state immediately after making a choice
|
||||
// This ensures variables (favour, items earned, etc.) are persisted
|
||||
if (this.inkEngine && this.inkEngine.story) {
|
||||
npcConversationStateManager.saveNPCState(this.npcId, this.inkEngine.story);
|
||||
}
|
||||
|
||||
// First, display the player's choice as dialogue
|
||||
if (choiceText) {
|
||||
this.ui.showDialogue(choiceText, 'player');
|
||||
}
|
||||
|
||||
// If this was an exit choice, close the minigame after showing the final response
|
||||
if (shouldExit) {
|
||||
console.log('🚪 Exit conversation tag detected - closing minigame');
|
||||
// Save state one final time and close
|
||||
if (this.inkEngine && this.inkEngine.story) {
|
||||
npcConversationStateManager.saveNPCState(this.npcId, this.inkEngine.story);
|
||||
}
|
||||
// Close minigame with a small delay to show player's choice
|
||||
this.scheduleDialogueAdvance(() => {
|
||||
this.complete(true);
|
||||
}, 1500);
|
||||
return;
|
||||
}
|
||||
|
||||
// Then display the result (dialogue blocks) after a small delay
|
||||
this.scheduleDialogueAdvance(() => {
|
||||
// Process accumulated dialogue by splitting into individual speaker blocks
|
||||
this.displayAccumulatedDialogue(result);
|
||||
}, 1500);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error handling choice:', error);
|
||||
this.showError('An error occurred when processing your choice');
|
||||
@@ -440,7 +475,12 @@ export class PersonChatMinigame extends MinigameScene {
|
||||
if (!result.text || !result.tags) {
|
||||
// No content to display
|
||||
if (result.hasEnded) {
|
||||
this.endConversation();
|
||||
// Story ended - save state and show message
|
||||
if (this.inkEngine && this.inkEngine.story) {
|
||||
npcConversationStateManager.saveNPCState(this.npcId, this.inkEngine.story);
|
||||
}
|
||||
this.ui.showDialogue('(Conversation ended - press ESC to close)', 'system');
|
||||
console.log('🏁 Story has reached an end point');
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -458,7 +498,12 @@ export class PersonChatMinigame extends MinigameScene {
|
||||
// Each tag corresponds to a line (or group of lines before the next tag)
|
||||
if (lines.length === 0) {
|
||||
if (result.hasEnded) {
|
||||
this.endConversation();
|
||||
// Story ended - save state and show message
|
||||
if (this.inkEngine && this.inkEngine.story) {
|
||||
npcConversationStateManager.saveNPCState(this.npcId, this.inkEngine.story);
|
||||
}
|
||||
this.ui.showDialogue('(Conversation ended - press ESC to close)', 'system');
|
||||
console.log('🏁 Story has reached an end point');
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -472,6 +517,8 @@ export class PersonChatMinigame extends MinigameScene {
|
||||
|
||||
/**
|
||||
* Create dialogue blocks from lines and speaker tags
|
||||
* When speaker tags are missing, dialogue defaults to the main NPC being talked to
|
||||
*
|
||||
* @param {Array<string>} lines - Text lines
|
||||
* @param {Array<string>} tags - Speaker tags
|
||||
* @returns {Array<Object>} Array of {speaker, text} blocks
|
||||
@@ -480,12 +527,24 @@ export class PersonChatMinigame extends MinigameScene {
|
||||
const blocks = [];
|
||||
let blockIndex = 0;
|
||||
|
||||
// Special case: NO tags at all - all lines belong to main NPC
|
||||
if (!tags || tags.length === 0) {
|
||||
if (lines.length > 0) {
|
||||
const allText = lines.join('\n').trim();
|
||||
if (allText) {
|
||||
blocks.push({ speaker: this.npc.id, text: allText, tag: null });
|
||||
}
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
// Group lines by speaker based on tags
|
||||
for (let tagIdx = 0; tagIdx < tags.length; tagIdx++) {
|
||||
const tag = tags[tagIdx];
|
||||
|
||||
// Determine speaker from tag - support multiple formats
|
||||
let speaker = 'npc'; // default
|
||||
// Default to main NPC if no speaker tag found
|
||||
let speaker = this.npc.id;
|
||||
if (tag.includes('speaker:player')) {
|
||||
speaker = 'player';
|
||||
} else if (tag.includes('speaker:npc:')) {
|
||||
@@ -532,7 +591,14 @@ export class PersonChatMinigame extends MinigameScene {
|
||||
if (blockIndex >= blocks.length) {
|
||||
// All blocks displayed, check if story has ended
|
||||
if (originalResult.hasEnded) {
|
||||
this.scheduleDialogueAdvance(() => this.endConversation(), 1000);
|
||||
// Story ended - save state and show message
|
||||
this.scheduleDialogueAdvance(() => {
|
||||
if (this.inkEngine && this.inkEngine.story) {
|
||||
npcConversationStateManager.saveNPCState(this.npcId, this.inkEngine.story);
|
||||
}
|
||||
this.ui.showDialogue('(Conversation ended - press ESC to close)', 'system');
|
||||
console.log('🏁 Story has reached an end point');
|
||||
}, 1000);
|
||||
} else {
|
||||
// Try to continue for more dialogue
|
||||
console.log('⏸️ Blocks finished, checking for more dialogue...');
|
||||
@@ -549,7 +615,12 @@ export class PersonChatMinigame extends MinigameScene {
|
||||
console.log(`📋 Back to choices: ${nextLine.choices.length} options available`);
|
||||
this.ui.showChoices(nextLine.choices);
|
||||
} else if (nextLine.hasEnded) {
|
||||
this.endConversation();
|
||||
// Story ended - save state and show message
|
||||
if (this.inkEngine && this.inkEngine.story) {
|
||||
npcConversationStateManager.saveNPCState(this.npcId, this.inkEngine.story);
|
||||
}
|
||||
this.ui.showDialogue('(Conversation ended - press ESC to close)', 'system');
|
||||
console.log('🏁 Story has reached an end point');
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
@@ -576,7 +647,12 @@ export class PersonChatMinigame extends MinigameScene {
|
||||
try {
|
||||
// Check if story has ended
|
||||
if (result.hasEnded) {
|
||||
this.endConversation();
|
||||
// Story ended - save state and show message
|
||||
if (this.inkEngine && this.inkEngine.story) {
|
||||
npcConversationStateManager.saveNPCState(this.npcId, this.inkEngine.story);
|
||||
}
|
||||
this.ui.showDialogue('(Conversation ended - press ESC to close)', 'system');
|
||||
console.log('🏁 Story has reached an end point');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -620,12 +696,19 @@ export class PersonChatMinigame extends MinigameScene {
|
||||
// There's more dialogue to show
|
||||
this.displayDialogueResult(nextLine);
|
||||
} else if (nextLine.hasEnded) {
|
||||
// Story has truly ended
|
||||
this.endConversation();
|
||||
// Story reached an end - save state and show message
|
||||
if (this.inkEngine && this.inkEngine.story) {
|
||||
npcConversationStateManager.saveNPCState(this.npcId, this.inkEngine.story);
|
||||
}
|
||||
this.ui.showDialogue('(Conversation ended - press ESC to close)', 'system');
|
||||
console.log('🏁 Story has reached an end point');
|
||||
} else {
|
||||
// No text but story isn't ended - wait a bit and end
|
||||
console.log('✓ No more dialogue - ending conversation');
|
||||
this.scheduleDialogueAdvance(() => this.endConversation(), 1000);
|
||||
// No text but story isn't ended - wait a bit and show message
|
||||
console.log('✓ No more dialogue - conversation paused');
|
||||
if (this.inkEngine && this.inkEngine.story) {
|
||||
npcConversationStateManager.saveNPCState(this.npcId, this.inkEngine.story);
|
||||
}
|
||||
this.ui.showDialogue('(No more dialogue available - press ESC to close)', 'system');
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
@@ -644,6 +727,9 @@ export class PersonChatMinigame extends MinigameScene {
|
||||
this.isConversationActive = false;
|
||||
|
||||
// Save the conversation state before ending
|
||||
// The state manager intelligently saves:
|
||||
// - Full state if conversation is still active
|
||||
// - Variables only if story has ended (so next conversation restarts fresh)
|
||||
if (this.inkEngine && this.inkEngine.story) {
|
||||
npcConversationStateManager.saveNPCState(this.npcId, this.inkEngine.story);
|
||||
}
|
||||
@@ -668,6 +754,9 @@ export class PersonChatMinigame extends MinigameScene {
|
||||
*/
|
||||
cleanup() {
|
||||
// Save conversation state before cleanup
|
||||
// The state manager intelligently handles:
|
||||
// - Saving full state for in-progress conversations
|
||||
// - Saving variables only for ended conversations
|
||||
if (this.isConversationActive && this.inkEngine && this.inkEngine.story) {
|
||||
console.log(`💾 Saving NPC state on cleanup for ${this.npcId}`);
|
||||
npcConversationStateManager.saveNPCState(this.npcId, this.inkEngine.story);
|
||||
|
||||
@@ -194,11 +194,14 @@ export default class PersonChatUI {
|
||||
// Get character data
|
||||
let character = this.characters[characterId];
|
||||
if (!character) {
|
||||
// Fallback for legacy speaker values
|
||||
// Fallback for legacy speaker values or main NPC ID
|
||||
if (characterId === 'player') {
|
||||
character = this.playerData;
|
||||
} else if (characterId === 'npc' || !characterId) {
|
||||
character = this.npc;
|
||||
} else if (characterId === this.npc?.id) {
|
||||
// Main NPC passed by ID - use main NPC data
|
||||
character = this.npc;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,13 +53,15 @@ export default class InkEngine {
|
||||
console.log('🏷️ InkEngine.continue() - accumulated tags:', tags);
|
||||
console.log('🔍 InkEngine.continue() - canContinue after:', this.story.canContinue);
|
||||
console.log('🔍 InkEngine.continue() - currentChoices after:', this.story.currentChoices?.length);
|
||||
console.log('🔍 InkEngine.continue() - hasEnded:', this.story.hasEnded);
|
||||
|
||||
// Return structured result with text, choices, tags, and continue state
|
||||
return {
|
||||
text: text,
|
||||
choices: (this.story.currentChoices || []).map((c, i) => ({ text: c.text, index: i })),
|
||||
tags: tags,
|
||||
canContinue: this.story.canContinue
|
||||
canContinue: this.story.canContinue,
|
||||
hasEnded: this.story.hasEnded
|
||||
};
|
||||
} catch (e) {
|
||||
// inkjs uses Continue() and throws for errors; rethrow with nicer message
|
||||
|
||||
@@ -15,25 +15,44 @@ class NPCConversationStateManager {
|
||||
|
||||
/**
|
||||
* Save the current state of an NPC's conversation
|
||||
*
|
||||
* Important: When story has ended, we save ONLY the variables (not the story state/progress).
|
||||
* This preserves character relationships and earned rewards while allowing the story to restart fresh.
|
||||
*
|
||||
* @param {string} npcId - NPC identifier
|
||||
* @param {Object} story - The Ink story object
|
||||
* @param {boolean} forceFullState - If true, save full state even if story has ended (for in-progress saves)
|
||||
*/
|
||||
saveNPCState(npcId, story) {
|
||||
saveNPCState(npcId, story, forceFullState = false) {
|
||||
if (!npcId || !story) return;
|
||||
|
||||
try {
|
||||
// Serialize the story state (includes all variables and progress)
|
||||
// Use uppercase ToJson as per inkjs API
|
||||
const storyState = story.state.ToJson();
|
||||
|
||||
const state = {
|
||||
storyState: storyState,
|
||||
timestamp: Date.now()
|
||||
timestamp: Date.now(),
|
||||
hasEnded: story.state.hasEnded
|
||||
};
|
||||
|
||||
// Always save the variables (favour, items earned, flags, etc.)
|
||||
// These persist across conversations even when story ends
|
||||
if (story.variablesState) {
|
||||
state.variables = { ...story.variablesState };
|
||||
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)`);
|
||||
} else {
|
||||
console.log(`💾 Saved variables only for ${npcId} (story ended - will restart fresh)`);
|
||||
}
|
||||
|
||||
this.conversationStates.set(npcId, state);
|
||||
console.log(`💾 Saved conversation state for NPC: ${npcId}`, {
|
||||
timestamp: new Date(state.timestamp).toLocaleTimeString()
|
||||
console.log(`✅ NPC state persisted for: ${npcId}`, {
|
||||
timestamp: new Date(state.timestamp).toLocaleTimeString(),
|
||||
hasEnded: state.hasEnded,
|
||||
hasVariables: !!state.variables,
|
||||
hasStoryState: !!state.storyState
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`❌ Error saving NPC state for ${npcId}:`, error);
|
||||
@@ -42,6 +61,11 @@ class NPCConversationStateManager {
|
||||
|
||||
/**
|
||||
* Restore the state of an NPC's conversation
|
||||
*
|
||||
* Strategy:
|
||||
* - If full story state exists (story was mid-conversation): restore it completely
|
||||
* - If only variables exist (story had ended): load variables but let story start fresh
|
||||
*
|
||||
* @param {string} npcId - NPC identifier
|
||||
* @param {Object} story - The Ink story object to restore into
|
||||
* @returns {boolean} True if state was restored
|
||||
@@ -56,15 +80,32 @@ class NPCConversationStateManager {
|
||||
}
|
||||
|
||||
try {
|
||||
// Restore the serialized story state
|
||||
// Use uppercase LoadJson as per inkjs API
|
||||
story.state.LoadJson(state.storyState);
|
||||
|
||||
console.log(`✅ Restored conversation state for NPC: ${npcId}`, {
|
||||
savedAt: new Date(state.timestamp).toLocaleTimeString()
|
||||
});
|
||||
|
||||
return true;
|
||||
// If we have saved story state, restore it completely (mid-conversation state)
|
||||
if (state.storyState) {
|
||||
story.state.LoadJson(state.storyState);
|
||||
console.log(`✅ Restored full story state for NPC: ${npcId}`, {
|
||||
savedAt: new Date(state.timestamp).toLocaleTimeString(),
|
||||
reason: 'In-progress conversation'
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we only have variables (story ended), restore just the variables
|
||||
if (state.variables) {
|
||||
// Load variables into the story
|
||||
for (const [key, value] of Object.entries(state.variables)) {
|
||||
story.variablesState[key] = value;
|
||||
}
|
||||
console.log(`✅ Restored variables for NPC: ${npcId}`, {
|
||||
savedAt: new Date(state.timestamp).toLocaleTimeString(),
|
||||
reason: 'Story ended - restarting fresh with saved variables',
|
||||
variables: state.variables
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log(`ℹ️ No saveable data for NPC: ${npcId}`);
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error(`❌ Error restoring NPC state for ${npcId}:`, error);
|
||||
return false;
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
// Generic NPC Story - Can be used for any NPC
|
||||
// The game should set the npc_name variable before starting
|
||||
// Demonstrates Ink's built-in features for handling repeated interactions
|
||||
//
|
||||
// IMPORTANT: Use #exit_conversation tag on the choice that should close the minigame
|
||||
// This allows proper state saving without triggering story END
|
||||
|
||||
VAR npc_name = "NPC"
|
||||
VAR conversation_count = 0
|
||||
VAR asked_question = false
|
||||
VAR asked_about_passwords = false
|
||||
|
||||
=== start ===
|
||||
~ conversation_count += 1
|
||||
@@ -11,12 +16,59 @@ VAR conversation_count = 0
|
||||
-> hub
|
||||
|
||||
=== hub ===
|
||||
+ [Ask a question]
|
||||
-> question
|
||||
// Options that appear only ONCE using Ink's 'once' feature
|
||||
{not asked_question:
|
||||
* once [Introduce yourself]
|
||||
~ npc_name = "Nice to meet you!"
|
||||
-> introduction
|
||||
}
|
||||
|
||||
// Options that CHANGE after first visit using conditionals
|
||||
{asked_question:
|
||||
+ [Remind me about that question]
|
||||
-> question_reminder
|
||||
- else:
|
||||
+ [Ask a question]
|
||||
-> question
|
||||
}
|
||||
|
||||
{asked_about_passwords:
|
||||
+ [Tell me more about passwords]
|
||||
-> passwords_advanced
|
||||
- else:
|
||||
+ [Ask about password security]
|
||||
-> ask_passwords
|
||||
}
|
||||
|
||||
// Regular options that always appear
|
||||
+ [Say hello]
|
||||
-> greeting
|
||||
+ [Say goodbye]
|
||||
-> goodbye
|
||||
|
||||
// Exit choice
|
||||
+ [Leave] #exit_conversation
|
||||
{npc_name}: See you later!
|
||||
-> hub
|
||||
|
||||
=== introduction ===
|
||||
{npc_name}: Nice to meet you too! I'm {npc_name}.
|
||||
{npc_name}: Feel free to ask me anything.
|
||||
-> hub
|
||||
|
||||
=== ask_passwords ===
|
||||
~ asked_about_passwords = true
|
||||
{npc_name}: Passwords should be long and complex...
|
||||
{npc_name}: Use at least 12 characters with mixed case and numbers.
|
||||
-> hub
|
||||
|
||||
=== question_reminder ===
|
||||
{npc_name}: As I said before, passwords should be strong and unique.
|
||||
{npc_name}: Anything else?
|
||||
-> hub
|
||||
|
||||
=== passwords_advanced ===
|
||||
{npc_name}: For advanced security, use a password manager to generate unique passwords for each site.
|
||||
{npc_name}: Never reuse passwords across different services.
|
||||
-> hub
|
||||
|
||||
=== question ===
|
||||
{npc_name}: That's a good question. Let me think about it...
|
||||
@@ -26,7 +78,3 @@ VAR conversation_count = 0
|
||||
=== greeting ===
|
||||
{npc_name}: Hello to you too! Nice to chat with you.
|
||||
-> hub
|
||||
|
||||
=== goodbye ===
|
||||
{npc_name}: Alright, see you later! Let me know if you need anything else.
|
||||
-> END
|
||||
|
||||
1
scenarios/ink/generic-npc.ink.json
Normal file
1
scenarios/ink/generic-npc.ink.json
Normal file
@@ -0,0 +1 @@
|
||||
{"inkVersion":21,"root":[[["done",{"#n":"g-0"}],null],"done",{"start":["ev",{"VAR?":"conversation_count"},1,"+",{"VAR=":"conversation_count","re":true},"/ev","ev",{"VAR?":"npc_name"},"out","/ev","^: Hey there! This is conversation ","#","ev",{"VAR?":"conversation_count"},"out","/ev","^.","/#","\n","ev",{"VAR?":"npc_name"},"out","/ev","^: What can I help you with?","\n",{"->":"hub"},null],"hub":[["ev",{"VAR?":"asked_question"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n",["ev",{"^->":"hub.0.4.b.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^Introduce yourself","/str","/ev",{"*":".^.^.c-0","flg":22},{"s":["^once ",{"->":"$r","var":true},null]}],{"->":"hub.0.5"},{"c-0":["ev",{"^->":"hub.0.4.b.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"\n","ev","str","^Nice to meet you!","/str","/ev",{"VAR=":"npc_name","re":true},{"->":"introduction"},{"#f":5}]}]}],"nop","\n","ev",{"VAR?":"asked_question"},"/ev",[{"->":".^.b","c":true},{"b":["\n","ev","str","^Remind me about that question","/str","/ev",{"*":".^.c-0","flg":4},{"->":"hub.0.12"},{"c-0":["\n",{"->":"question_reminder"},null]}]}],[{"->":".^.b"},{"b":["\n","ev","str","^Ask a question","/str","/ev",{"*":".^.c-0","flg":4},{"->":"hub.0.12"},{"c-0":["\n",{"->":"question"},null]}]}],"nop","\n","ev",{"VAR?":"asked_about_passwords"},"/ev",[{"->":".^.b","c":true},{"b":["\n","ev","str","^Tell me more about passwords","/str","/ev",{"*":".^.c-0","flg":4},{"->":"hub.0.19"},{"c-0":["\n",{"->":"passwords_advanced"},null]}]}],[{"->":".^.b"},{"b":["\n","ev","str","^Ask about password security","/str","/ev",{"*":".^.c-0","flg":4},{"->":"hub.0.19"},{"c-0":["\n",{"->":"ask_passwords"},null]}]}],"nop","\n","ev","str","^Say hello","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Leave","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"greeting"},null],"c-1":["^ ","#","^exit_conversation","/#","\n","ev",{"VAR?":"npc_name"},"out","/ev","^: See you later!","\n",{"->":"hub"},null]}],null],"introduction":["ev",{"VAR?":"npc_name"},"out","/ev","^: Nice to meet you too! I'm ","ev",{"VAR?":"npc_name"},"out","/ev","^.","\n","ev",{"VAR?":"npc_name"},"out","/ev","^: Feel free to ask me anything.","\n",{"->":"hub"},null],"ask_passwords":["ev",true,"/ev",{"VAR=":"asked_about_passwords","re":true},"ev",{"VAR?":"npc_name"},"out","/ev","^: Passwords should be long and complex...","\n","ev",{"VAR?":"npc_name"},"out","/ev","^: Use at least 12 characters with mixed case and numbers.","\n",{"->":"hub"},null],"question_reminder":["ev",{"VAR?":"npc_name"},"out","/ev","^: As I said before, passwords should be strong and unique.","\n","ev",{"VAR?":"npc_name"},"out","/ev","^: Anything else?","\n",{"->":"hub"},null],"passwords_advanced":["ev",{"VAR?":"npc_name"},"out","/ev","^: For advanced security, use a password manager to generate unique passwords for each site.","\n","ev",{"VAR?":"npc_name"},"out","/ev","^: Never reuse passwords across different services.","\n",{"->":"hub"},null],"question":["ev",{"VAR?":"npc_name"},"out","/ev","^: That's a good question. Let me think about it...","\n","ev",{"VAR?":"npc_name"},"out","/ev","^: I'm not sure I have all the answers right now.","\n",{"->":"hub"},null],"greeting":["ev",{"VAR?":"npc_name"},"out","/ev","^: Hello to you too! Nice to chat with you.","\n",{"->":"hub"},null],"global decl":["ev","str","^NPC","/str",{"VAR=":"npc_name"},0,{"VAR=":"conversation_count"},false,{"VAR=":"asked_question"},false,{"VAR=":"asked_about_passwords"},"/ev","end",null]}],"listDefs":{}}
|
||||
1
scenarios/ink/generic-npc.json
Normal file
1
scenarios/ink/generic-npc.json
Normal file
@@ -0,0 +1 @@
|
||||
{"inkVersion":21,"root":[[["done",{"#n":"g-0"}],null],"done",{"start":["ev",{"VAR?":"conversation_count"},1,"+",{"VAR=":"conversation_count","re":true},"/ev","ev",{"VAR?":"npc_name"},"out","/ev","^: Hey there! This is conversation ","#","ev",{"VAR?":"conversation_count"},"out","/ev","^.","/#","\n","ev",{"VAR?":"npc_name"},"out","/ev","^: What can I help you with?","\n",{"->":"hub"},null],"hub":[["ev",{"VAR?":"asked_question"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n",["ev",{"^->":"hub.0.4.b.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^Introduce yourself","/str","/ev",{"*":".^.^.c-0","flg":22},{"s":["^once ",{"->":"$r","var":true},null]}],{"->":"hub.0.5"},{"c-0":["ev",{"^->":"hub.0.4.b.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"\n","ev","str","^Nice to meet you!","/str","/ev",{"VAR=":"npc_name","re":true},{"->":"introduction"},{"#f":5}]}]}],"nop","\n","ev",{"VAR?":"asked_question"},"/ev",[{"->":".^.b","c":true},{"b":["\n","ev","str","^Remind me about that question","/str","/ev",{"*":".^.c-0","flg":4},{"->":"hub.0.12"},{"c-0":["\n",{"->":"question_reminder"},null]}]}],[{"->":".^.b"},{"b":["\n","ev","str","^Ask a question","/str","/ev",{"*":".^.c-0","flg":4},{"->":"hub.0.12"},{"c-0":["\n",{"->":"question"},null]}]}],"nop","\n","ev",{"VAR?":"asked_about_passwords"},"/ev",[{"->":".^.b","c":true},{"b":["\n","ev","str","^Tell me more about passwords","/str","/ev",{"*":".^.c-0","flg":4},{"->":"hub.0.19"},{"c-0":["\n",{"->":"passwords_advanced"},null]}]}],[{"->":".^.b"},{"b":["\n","ev","str","^Ask about password security","/str","/ev",{"*":".^.c-0","flg":4},{"->":"hub.0.19"},{"c-0":["\n",{"->":"ask_passwords"},null]}]}],"nop","\n","ev","str","^Say hello","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Leave","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"greeting"},null],"c-1":["^ ","#","^exit_conversation","/#","\n","ev",{"VAR?":"npc_name"},"out","/ev","^: See you later!","\n",{"->":"hub"},null]}],null],"introduction":["ev",{"VAR?":"npc_name"},"out","/ev","^: Nice to meet you too! I'm ","ev",{"VAR?":"npc_name"},"out","/ev","^.","\n","ev",{"VAR?":"npc_name"},"out","/ev","^: Feel free to ask me anything.","\n",{"->":"hub"},null],"ask_passwords":["ev",true,"/ev",{"VAR=":"asked_about_passwords","re":true},"ev",{"VAR?":"npc_name"},"out","/ev","^: Passwords should be long and complex...","\n","ev",{"VAR?":"npc_name"},"out","/ev","^: Use at least 12 characters with mixed case and numbers.","\n",{"->":"hub"},null],"question_reminder":["ev",{"VAR?":"npc_name"},"out","/ev","^: As I said before, passwords should be strong and unique.","\n","ev",{"VAR?":"npc_name"},"out","/ev","^: Anything else?","\n",{"->":"hub"},null],"passwords_advanced":["ev",{"VAR?":"npc_name"},"out","/ev","^: For advanced security, use a password manager to generate unique passwords for each site.","\n","ev",{"VAR?":"npc_name"},"out","/ev","^: Never reuse passwords across different services.","\n",{"->":"hub"},null],"question":["ev",{"VAR?":"npc_name"},"out","/ev","^: That's a good question. Let me think about it...","\n","ev",{"VAR?":"npc_name"},"out","/ev","^: I'm not sure I have all the answers right now.","\n",{"->":"hub"},null],"greeting":["ev",{"VAR?":"npc_name"},"out","/ev","^: Hello to you too! Nice to chat with you.","\n",{"->":"hub"},null],"global decl":["ev","str","^NPC","/str",{"VAR=":"npc_name"},0,{"VAR=":"conversation_count"},false,{"VAR=":"asked_question"},false,{"VAR=":"asked_about_passwords"},"/ev","end",null]}],"listDefs":{}}
|
||||
@@ -1,5 +1,6 @@
|
||||
// helper-npc.ink
|
||||
// An NPC that helps the player by unlocking doors and giving hints
|
||||
// Uses hub-based conversation pattern with once/sticky for smart menu management
|
||||
// Includes event-triggered reactions using auto-mapping
|
||||
|
||||
VAR trust_level = 0
|
||||
@@ -8,198 +9,270 @@ VAR has_given_lockpick = false
|
||||
VAR saw_lockpick_used = false
|
||||
VAR saw_door_unlock = false
|
||||
VAR has_greeted = false
|
||||
VAR asked_about_self = false
|
||||
VAR asked_about_ceo = false
|
||||
VAR asked_for_items = false
|
||||
|
||||
=== start ===
|
||||
{ has_greeted:
|
||||
-> main_menu
|
||||
- else:
|
||||
Hey there! I'm here to help you out if you need it. 👋
|
||||
What can I do for you?
|
||||
~ has_greeted = true
|
||||
-> main_menu
|
||||
# speaker:npc
|
||||
Hey there! I'm here to help you out if you need it. 👋
|
||||
What can I do for you?
|
||||
~ has_greeted = true
|
||||
-> hub
|
||||
|
||||
=== hub ===
|
||||
// One-time introduction option
|
||||
{not asked_about_self:
|
||||
* [Who are you?]
|
||||
~ asked_about_self = true
|
||||
-> who_are_you
|
||||
}
|
||||
|
||||
=== main_menu ===
|
||||
+ [Who are you?]
|
||||
# speaker:player
|
||||
Who are you?
|
||||
-> who_are_you
|
||||
+ [Can you help me get into the CEO's office?]
|
||||
# speaker:player
|
||||
Can you help me get into the CEO's office?
|
||||
// CEO office help - changes based on state
|
||||
{asked_about_self and not has_unlocked_ceo:
|
||||
+ [Can you help me get into the CEO's office?]
|
||||
-> help_ceo_office
|
||||
+ [Do you have any items for me?]
|
||||
# speaker:player
|
||||
Do you have any items for me?
|
||||
}
|
||||
|
||||
{has_unlocked_ceo:
|
||||
+ [Any other doors you need help with?]
|
||||
-> other_doors
|
||||
}
|
||||
|
||||
// Items - changes based on state
|
||||
{asked_about_self and not has_given_lockpick:
|
||||
+ [Do you have any items for me?]
|
||||
-> give_items
|
||||
+ {saw_lockpick_used} [Thanks for the lockpick! It worked great.]
|
||||
# speaker:player
|
||||
Thanks for the lockpick! It worked great.
|
||||
}
|
||||
|
||||
{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.]
|
||||
-> lockpick_feedback
|
||||
+ [Thanks, I'm good for now.]
|
||||
# speaker:player
|
||||
Thanks, I'm good for now.
|
||||
-> goodbye
|
||||
}
|
||||
|
||||
// Trust-based advanced options
|
||||
{trust_level >= 3:
|
||||
+ [What hints do you have for me?]
|
||||
-> give_hints
|
||||
}
|
||||
|
||||
// Exit conversation
|
||||
+ [Thanks, I'm good for now.] #exit_conversation
|
||||
Alright then. Let me know if you need anything else!
|
||||
-> hub
|
||||
|
||||
=== who_are_you ===
|
||||
# speaker:npc
|
||||
I'm a friendly NPC who can help you progress through the mission.
|
||||
I can unlock doors, give you items, and provide hints.
|
||||
I can unlock doors, give you items, and provide hints when you need them.
|
||||
~ trust_level = trust_level + 1
|
||||
-> main_menu
|
||||
What would you like to do?
|
||||
-> hub
|
||||
|
||||
=== help_ceo_office ===
|
||||
# speaker:npc
|
||||
{ has_unlocked_ceo:
|
||||
I already unlocked the CEO's office for you! Just head on in.
|
||||
-> main_menu
|
||||
{has_unlocked_ceo:
|
||||
I already unlocked the CEO's office for you! Just head on in.
|
||||
-> hub
|
||||
- else:
|
||||
The CEO's office? That's a tough one...
|
||||
{ trust_level >= 1:
|
||||
Alright, I trust you. Let me unlock that door for you.
|
||||
~ has_unlocked_ceo = true
|
||||
There you go! The door to the CEO's office is now unlocked. # unlock_door:ceo
|
||||
~ trust_level = trust_level + 2
|
||||
-> main_menu
|
||||
- else:
|
||||
I don't know you well enough yet. Ask me something else first.
|
||||
-> main_menu
|
||||
}
|
||||
The CEO's office? That's a tough one...
|
||||
{trust_level >= 1:
|
||||
Alright, I trust you enough. Let me unlock that door for you.
|
||||
~ has_unlocked_ceo = true
|
||||
~ asked_about_ceo = true
|
||||
There you go! The door to the CEO's office is now unlocked. #unlock_door:ceo
|
||||
~ trust_level = trust_level + 2
|
||||
What else can I help with?
|
||||
-> hub
|
||||
- else:
|
||||
I don't know you well enough yet. Ask me some questions first and we can build some trust.
|
||||
-> hub
|
||||
}
|
||||
}
|
||||
|
||||
=== other_doors ===
|
||||
# speaker:npc
|
||||
What other doors do you need help with? I can try to unlock them if you tell me which ones.
|
||||
~ trust_level = trust_level + 1
|
||||
Let me know!
|
||||
-> hub
|
||||
|
||||
=== give_items ===
|
||||
# speaker:npc
|
||||
{ has_given_lockpick:
|
||||
I already gave you a lockpick set! Check your inventory.
|
||||
-> main_menu
|
||||
{has_given_lockpick:
|
||||
I already gave you a lockpick set. Check your inventory - it should be there!
|
||||
-> hub
|
||||
- else:
|
||||
Let me see what I have...
|
||||
{ trust_level >= 2:
|
||||
Here's a lockpick set. Use it wisely! 🔓
|
||||
~ has_given_lockpick = true
|
||||
# give_item:lockpick
|
||||
-> main_menu
|
||||
- else:
|
||||
I need to trust you more before I give you something like that.
|
||||
-> main_menu
|
||||
}
|
||||
Let me see what I have...
|
||||
{trust_level >= 2:
|
||||
Here's a lockpick set. Use it to open locked doors and containers! 🔓
|
||||
~ has_given_lockpick = true
|
||||
~ 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!
|
||||
-> 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
|
||||
}
|
||||
|
||||
=== lockpick_feedback ===
|
||||
# speaker:npc
|
||||
Great! I'm glad it helped you out. That's what I'm here for.
|
||||
You're doing excellent work on this mission.
|
||||
~ trust_level = trust_level + 1
|
||||
-> main_menu
|
||||
~ saw_lockpick_used = false
|
||||
What else do you need?
|
||||
-> hub
|
||||
|
||||
=== goodbye ===
|
||||
# speaker:player
|
||||
Thanks, I'm good for now.
|
||||
# speaker:npc
|
||||
No problem! Let me know if you need anything.
|
||||
-> END
|
||||
=== give_hints ===
|
||||
{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:
|
||||
Try using that lockpick set on locked doors and containers around the building.
|
||||
You never know what secrets people hide behind locked doors!
|
||||
- else:
|
||||
Explore every room carefully. Items are often hidden in places you'd least expect.
|
||||
}
|
||||
}
|
||||
Good luck!
|
||||
-> hub
|
||||
|
||||
// ==========================================
|
||||
// EVENT-TRIGGERED BARKS (Auto-mapped to game events)
|
||||
// These knots are triggered automatically by the NPC system
|
||||
// when specific game events occur.
|
||||
// Note: These redirect to 'main_menu' so clicking the bark opens full conversation without repeating intro
|
||||
// Note: These redirect to 'hub' so clicking opens full conversation
|
||||
// ==========================================
|
||||
|
||||
// Triggered when player picks up the lockpick
|
||||
=== on_lockpick_pickup ===
|
||||
{ has_given_lockpick:
|
||||
Great! You found the lockpick I gave you. Try it on a locked door or container!
|
||||
{has_given_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. 🔓
|
||||
Nice find! That lockpick set looks professional. Could be very useful. 🔓
|
||||
}
|
||||
-> main_menu
|
||||
-> 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. 🎯
|
||||
{has_given_lockpick:
|
||||
Excellent! Glad I could help you get through that. 🎯
|
||||
- else:
|
||||
Nice work getting through that lock! 🔓
|
||||
Nice work getting through that lock! 🔓
|
||||
}
|
||||
-> main_menu
|
||||
-> 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_given_lockpick:
|
||||
Don't give up! Lockpicking takes practice. Try adjusting the tension. 🔧
|
||||
Want me to help you with anything else?
|
||||
- else:
|
||||
Tough break. Lockpicking isn't easy without the right tools...
|
||||
Tough break. Lockpicking isn't easy without the right tools...
|
||||
I might be able to help with that if you ask.
|
||||
}
|
||||
-> main_menu
|
||||
-> hub
|
||||
|
||||
// Triggered when any door is unlocked
|
||||
=== on_door_unlocked ===
|
||||
~ saw_door_unlock = true
|
||||
{ has_unlocked_ceo:
|
||||
Another door open! You're making great progress. 🚪✓
|
||||
{has_unlocked_ceo:
|
||||
Another door open! You're making great progress. 🚪✓
|
||||
- else:
|
||||
Nice! You found a way through that door. Keep going!
|
||||
Nice! You found a way through that door. Keep going!
|
||||
}
|
||||
-> main_menu
|
||||
-> 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. 🔒
|
||||
{ trust_level >= 2:
|
||||
Want me to help you out? Just ask!
|
||||
{trust_level >= 2:
|
||||
Want me to help you out? Just ask!
|
||||
- else:
|
||||
{trust_level >= 1:
|
||||
I might be able to help if you get to know me better first.
|
||||
}
|
||||
}
|
||||
-> main_menu
|
||||
-> hub
|
||||
|
||||
// Triggered when player interacts with the CEO desk
|
||||
=== on_ceo_desk_interact ===
|
||||
{ has_unlocked_ceo:
|
||||
The CEO's desk - you made it! Nice work. 📋
|
||||
{has_unlocked_ceo:
|
||||
The CEO's desk - you made it! Nice work. 📋
|
||||
That's where the important evidence is kept.
|
||||
- else:
|
||||
Trying to get into the CEO's office? I might be able to help with that...
|
||||
Trying to get into the CEO's office? I might be able to help with that...
|
||||
}
|
||||
-> main_menu
|
||||
-> hub
|
||||
|
||||
// Triggered when player picks up any item
|
||||
=== on_item_found ===
|
||||
{ trust_level >= 1:
|
||||
Good find! Every item could be important for your mission. 📦
|
||||
{trust_level >= 1:
|
||||
Good find! Every item could be important for your mission. 📦
|
||||
}
|
||||
-> main_menu
|
||||
-> hub
|
||||
|
||||
// Triggered when player enters any room (general progress check)
|
||||
=== on_room_entered ===
|
||||
{ has_unlocked_ceo:
|
||||
Keep searching for that evidence! 🔍
|
||||
{has_unlocked_ceo:
|
||||
Keep searching for that evidence! 🔍
|
||||
- else:
|
||||
{ trust_level >= 1:
|
||||
You're making progress through the building. 🚶
|
||||
- else:
|
||||
Exploring new areas... 🚶
|
||||
}
|
||||
{trust_level >= 1:
|
||||
You're making progress through the building. 🚶
|
||||
Let me know if you need help with anything.
|
||||
- else:
|
||||
Exploring new areas... 🚶
|
||||
}
|
||||
}
|
||||
-> main_menu
|
||||
-> hub
|
||||
|
||||
// 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. 🗺️✨
|
||||
{trust_level >= 2:
|
||||
Great find! This new area might have what we need. 🗺️✨
|
||||
Search it thoroughly!
|
||||
- else:
|
||||
{ trust_level >= 1:
|
||||
Interesting! You've found a new area. Be careful exploring. 🗺️
|
||||
- else:
|
||||
A new room... wonder what's inside. 🚪
|
||||
}
|
||||
{trust_level >= 1:
|
||||
Interesting! You've found a new area. Be careful exploring. 🗺️
|
||||
- else:
|
||||
A new room... wonder what's inside. 🚪
|
||||
}
|
||||
}
|
||||
-> main_menu
|
||||
-> hub
|
||||
|
||||
// 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. 🕵️
|
||||
{has_unlocked_ceo:
|
||||
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! 🎉
|
||||
~ trust_level = trust_level + 1
|
||||
Whoa, you got into the CEO's office! That's impressive! 🎉
|
||||
~ trust_level = trust_level + 1
|
||||
Maybe I underestimated you. Impressive work!
|
||||
}
|
||||
-> main_menu
|
||||
-> hub
|
||||
|
||||
|
||||
1
scenarios/ink/helper-npc.ink.json
Normal file
1
scenarios/ink/helper-npc.ink.json
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user