mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-21 11:18:08 +00:00
feat(person-chat): Implement multi-character support and enhance dialogue handling
This commit is contained in:
@@ -60,18 +60,67 @@ export class PersonChatMinigame extends MinigameScene {
|
||||
spriteSheet: 'hacker'
|
||||
};
|
||||
|
||||
// Build character index for multi-character support
|
||||
this.characters = this.buildCharacterIndex();
|
||||
|
||||
// Modules
|
||||
this.ui = null;
|
||||
this.conversation = null;
|
||||
|
||||
// State
|
||||
this.isConversationActive = false;
|
||||
this.currentSpeaker = null; // Track current speaker ('npc', 'player', or NPC id)
|
||||
this.currentSpeaker = null; // Track current speaker ID ('player' or NPC id)
|
||||
this.lastResult = null; // Store last continue() result for choice handling
|
||||
|
||||
console.log(`🎭 PersonChatMinigame created for NPC: ${this.npcId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build index of all available characters (player + NPCs)
|
||||
* @returns {Object} Map of character ID to character data
|
||||
*/
|
||||
buildCharacterIndex() {
|
||||
const characters = {};
|
||||
|
||||
// Add player
|
||||
characters['player'] = this.playerData;
|
||||
|
||||
// Add main NPC
|
||||
characters[this.npc.id] = this.npc;
|
||||
|
||||
// Add other NPCs from scenario if available
|
||||
if (this.scenario.npcs && Array.isArray(this.scenario.npcs)) {
|
||||
this.scenario.npcs.forEach(npc => {
|
||||
if (npc.id !== this.npc.id) {
|
||||
characters[npc.id] = npc;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`👥 Built character index with ${Object.keys(characters).length} characters:`, Object.keys(characters));
|
||||
return characters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get character data by ID
|
||||
* @param {string} characterId - Character ID (player, npc_id, etc.)
|
||||
* @returns {Object} Character data
|
||||
*/
|
||||
getCharacterById(characterId) {
|
||||
if (!characterId) return this.npc; // Fallback to main NPC
|
||||
|
||||
// Handle legacy speaker values
|
||||
if (characterId === 'npc') {
|
||||
return this.npc;
|
||||
}
|
||||
if (characterId === 'player') {
|
||||
return this.playerData;
|
||||
}
|
||||
|
||||
// Look up by ID
|
||||
return this.characters[characterId] || this.npc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the minigame UI and components
|
||||
*/
|
||||
@@ -93,7 +142,8 @@ export class PersonChatMinigame extends MinigameScene {
|
||||
game: this.game,
|
||||
npc: this.npc,
|
||||
playerSprite: this.player,
|
||||
playerData: this.playerData
|
||||
playerData: this.playerData,
|
||||
characters: this.characters // Pass multi-character support
|
||||
}, this.npcManager);
|
||||
|
||||
this.ui.render();
|
||||
@@ -224,13 +274,51 @@ export class PersonChatMinigame extends MinigameScene {
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine who is speaking based on Ink tags or content
|
||||
* 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)
|
||||
*
|
||||
* @param {Object} result - Result from conversation.continue()
|
||||
* @returns {string} Speaker ('npc' or 'player')
|
||||
* @returns {string} Character ID of speaker (player, npc_id, or main NPC id)
|
||||
*/
|
||||
determineSpeaker(result) {
|
||||
// Use the shared helper function from chat-helpers
|
||||
return determineSpeakerFromTags(result.tags, 'npc');
|
||||
if (!result.tags || result.tags.length === 0) {
|
||||
return this.npc.id; // Default to main NPC
|
||||
}
|
||||
|
||||
// Check tags in reverse order to find the last speaker tag (current speaker)
|
||||
for (let i = result.tags.length - 1; i >= 0; i--) {
|
||||
const tag = result.tags[i].trim().toLowerCase();
|
||||
|
||||
// Handle multi-part speaker tags like "speaker:npc:test_npc_back"
|
||||
if (tag.startsWith('speaker:')) {
|
||||
const parts = tag.split(':');
|
||||
|
||||
if (parts.length === 2) {
|
||||
// Simple speaker tag: speaker:player or speaker:npc
|
||||
const speaker = parts[1];
|
||||
if (speaker === 'player') return 'player';
|
||||
if (speaker === 'npc') return this.npc.id; // Default NPC
|
||||
} else if (parts.length === 3) {
|
||||
// Specific character tag: speaker:npc:character_id
|
||||
const characterId = parts[2];
|
||||
return this.characters[characterId] ? characterId : this.npc.id;
|
||||
} else if (parts.length > 3) {
|
||||
// Handle IDs with colons like speaker:npc:test_npc_back
|
||||
const characterId = parts.slice(2).join(':');
|
||||
return this.characters[characterId] ? characterId : this.npc.id;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for non-speaker: tags
|
||||
if (tag === 'player') return 'player';
|
||||
if (tag === 'npc') return this.npc.id;
|
||||
}
|
||||
|
||||
// No speaker tag found - default to main NPC
|
||||
return this.npc.id;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,6 +334,9 @@ export class PersonChatMinigame extends MinigameScene {
|
||||
// Get the choice text from lastResult before making the choice
|
||||
const choiceText = this.lastResult.choices[choiceIndex]?.text || '';
|
||||
|
||||
// Clear choice buttons immediately
|
||||
this.ui.hideChoices();
|
||||
|
||||
// Make choice in conversation (this also calls continue() internally)
|
||||
const result = this.conversation.makeChoice(choiceIndex);
|
||||
|
||||
@@ -254,42 +345,10 @@ export class PersonChatMinigame extends MinigameScene {
|
||||
this.ui.showDialogue(choiceText, 'player');
|
||||
}
|
||||
|
||||
// Then display the result (NPC response) after a small delay
|
||||
// Then display the result (dialogue blocks) after a small delay
|
||||
setTimeout(() => {
|
||||
// Extract NPC-only content from the accumulated result
|
||||
// Split by speaker tag to separate player and NPC dialogue
|
||||
let npcText = '';
|
||||
let npcTags = [];
|
||||
|
||||
// Find the section with speaker:npc tag
|
||||
if (result.tags && result.tags.includes('speaker:npc')) {
|
||||
// Split the text by lines and reconstruct based on tags
|
||||
const lines = result.text.split('\n').filter(line => line.trim());
|
||||
const tagIndex = result.tags.indexOf('speaker:npc');
|
||||
|
||||
// Get number of player lines before NPC (based on speaker:player tag position)
|
||||
const playerTagIndex = result.tags.indexOf('speaker:player');
|
||||
let playerLineCount = playerTagIndex >= 0 ? 1 : 0;
|
||||
|
||||
// Skip player lines and collect NPC lines
|
||||
npcText = lines.slice(playerLineCount).join('\n').trim();
|
||||
npcTags = result.tags.filter((tag, idx) => idx >= tagIndex);
|
||||
|
||||
console.log(`📄 Extracted NPC text: "${npcText.substring(0, 50)}..."`);
|
||||
} else {
|
||||
// Fallback: use full result if no speaker tag
|
||||
npcText = result.text;
|
||||
npcTags = result.tags;
|
||||
}
|
||||
|
||||
// Create a new result with only the NPC's dialogue
|
||||
const npcOnlyResult = {
|
||||
...result,
|
||||
text: npcText,
|
||||
tags: npcTags
|
||||
};
|
||||
|
||||
this.displayDialogueResult(npcOnlyResult);
|
||||
// Process accumulated dialogue by splitting into individual speaker blocks
|
||||
this.displayAccumulatedDialogue(result);
|
||||
}, 1500);
|
||||
} catch (error) {
|
||||
console.error('❌ Error handling choice:', error);
|
||||
@@ -297,6 +356,128 @@ export class PersonChatMinigame extends MinigameScene {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display accumulated dialogue by splitting into individual speaker blocks
|
||||
* @param {Object} result - Result with potentially multiple lines and tags
|
||||
*/
|
||||
displayAccumulatedDialogue(result) {
|
||||
if (!result.text || !result.tags) {
|
||||
// No content to display
|
||||
if (result.hasEnded) {
|
||||
this.endConversation();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Split text into lines
|
||||
const lines = result.text.split('\n').filter(line => line.trim());
|
||||
|
||||
// We have lines and tags - pair them up
|
||||
// Each tag corresponds to a line (or group of lines before the next tag)
|
||||
if (lines.length === 0) {
|
||||
if (result.hasEnded) {
|
||||
this.endConversation();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Create dialogue blocks: each block is one or more consecutive lines with the same speaker
|
||||
const dialogueBlocks = this.createDialogueBlocks(lines, result.tags);
|
||||
|
||||
// Display blocks sequentially with delays
|
||||
this.displayDialogueBlocksSequentially(dialogueBlocks, result, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create dialogue blocks from lines and speaker tags
|
||||
* @param {Array<string>} lines - Text lines
|
||||
* @param {Array<string>} tags - Speaker tags
|
||||
* @returns {Array<Object>} Array of {speaker, text} blocks
|
||||
*/
|
||||
createDialogueBlocks(lines, tags) {
|
||||
const blocks = [];
|
||||
let blockIndex = 0;
|
||||
|
||||
// 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
|
||||
if (tag.includes('speaker:player')) {
|
||||
speaker = 'player';
|
||||
} else if (tag.includes('speaker:npc:')) {
|
||||
// Extract character ID from speaker:npc:character_id format
|
||||
const match = tag.match(/speaker:npc:(\S+)/);
|
||||
if (match && match[1]) {
|
||||
speaker = match[1];
|
||||
}
|
||||
} else if (tag === 'player' || tag.includes('player')) {
|
||||
speaker = 'player';
|
||||
}
|
||||
|
||||
// Find how many lines belong to this speaker (until next tag or end)
|
||||
const nextTagIdx = tagIdx + 1;
|
||||
const startLineIdx = blockIndex;
|
||||
|
||||
// Count lines for this speaker - lines between this tag and the next
|
||||
let endLineIdx = lines.length;
|
||||
if (nextTagIdx < tags.length) {
|
||||
// There's another tag coming, but we need to figure out how many lines
|
||||
// For now, assume 1 line per tag (common case)
|
||||
endLineIdx = startLineIdx + 1;
|
||||
}
|
||||
|
||||
// Collect the text for this speaker
|
||||
const blockText = lines.slice(startLineIdx, endLineIdx).join('\n').trim();
|
||||
if (blockText) {
|
||||
blocks.push({ speaker, text: blockText, tag });
|
||||
}
|
||||
|
||||
blockIndex = endLineIdx;
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display dialogue blocks sequentially
|
||||
* @param {Array<Object>} blocks - Array of dialogue blocks
|
||||
* @param {Object} originalResult - Original result from Ink
|
||||
* @param {number} blockIndex - Current block index
|
||||
*/
|
||||
displayDialogueBlocksSequentially(blocks, originalResult, blockIndex) {
|
||||
if (blockIndex >= blocks.length) {
|
||||
// All blocks displayed, check if story has ended
|
||||
if (originalResult.hasEnded) {
|
||||
setTimeout(() => this.endConversation(), 1000);
|
||||
} else {
|
||||
// Try to continue for more dialogue
|
||||
console.log('⏸️ Blocks finished, checking for more dialogue...');
|
||||
setTimeout(() => {
|
||||
const nextLine = this.conversation.continue();
|
||||
if (nextLine.text && nextLine.text.trim()) {
|
||||
this.displayAccumulatedDialogue(nextLine);
|
||||
} else if (nextLine.hasEnded) {
|
||||
this.endConversation();
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Display current block
|
||||
const block = blocks[blockIndex];
|
||||
console.log(`📋 Displaying block ${blockIndex + 1}/${blocks.length}: ${block.speaker}`);
|
||||
|
||||
this.ui.showDialogue(block.text, block.speaker);
|
||||
|
||||
// Display next block after delay
|
||||
setTimeout(() => {
|
||||
this.displayDialogueBlocksSequentially(blocks, originalResult, blockIndex + 1);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display dialogue from a result object (without calling continue() again)
|
||||
* @param {Object} result - Story result from conversation.continue()
|
||||
@@ -340,9 +521,23 @@ export class PersonChatMinigame extends MinigameScene {
|
||||
console.log('⏳ Auto-continuing in 2 seconds...');
|
||||
setTimeout(() => this.showCurrentDialogue(), 2000);
|
||||
} else {
|
||||
// No choices and can't continue - story will end
|
||||
console.log('✓ Waiting for story to end...');
|
||||
setTimeout(() => this.endConversation(), 1000);
|
||||
// No choices and can't continue - check if there's more content
|
||||
// Try to continue anyway (for linear scripted conversations)
|
||||
console.log('⏸️ No more choices, attempting to continue for next line...');
|
||||
setTimeout(() => {
|
||||
const nextLine = this.conversation.continue();
|
||||
if (nextLine.text && nextLine.text.trim()) {
|
||||
// There's more dialogue to show
|
||||
this.displayDialogueResult(nextLine);
|
||||
} else if (nextLine.hasEnded) {
|
||||
// Story has truly ended
|
||||
this.endConversation();
|
||||
} else {
|
||||
// No text but story isn't ended - wait a bit and end
|
||||
console.log('✓ No more dialogue - ending conversation');
|
||||
setTimeout(() => this.endConversation(), 1000);
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Error displaying dialogue:', error);
|
||||
|
||||
@@ -17,7 +17,7 @@ export default class PersonChatUI {
|
||||
/**
|
||||
* Create UI component
|
||||
* @param {HTMLElement} container - Container for UI
|
||||
* @param {Object} params - Configuration (game, npc, playerSprite)
|
||||
* @param {Object} params - Configuration (game, npc, playerSprite, characters)
|
||||
* @param {NPCManager} npcManager - NPC manager for sprite access
|
||||
*/
|
||||
constructor(container, params, npcManager) {
|
||||
@@ -28,6 +28,7 @@ export default class PersonChatUI {
|
||||
this.npc = params.npc;
|
||||
this.playerSprite = params.playerSprite;
|
||||
this.playerData = params.playerData || {};
|
||||
this.characters = params.characters || {}; // Multi-character support
|
||||
|
||||
// UI elements
|
||||
this.elements = {
|
||||
@@ -48,7 +49,7 @@ export default class PersonChatUI {
|
||||
this.portraitRenderer = null;
|
||||
|
||||
// State
|
||||
this.currentSpeaker = null; // 'npc' or 'player'
|
||||
this.currentSpeaker = null; // Character ID
|
||||
this.hasContinued = false; // Track if user has clicked continue
|
||||
|
||||
console.log('📱 PersonChatUI created');
|
||||
@@ -173,25 +174,36 @@ export default class PersonChatUI {
|
||||
/**
|
||||
* Display dialogue text with speaker
|
||||
* @param {string} text - Dialogue text to display
|
||||
* @param {string} speaker - Speaker name ('npc' or 'player')
|
||||
* @param {string} characterId - Character ID ('player', 'npc', or specific NPC ID)
|
||||
*/
|
||||
showDialogue(text, speaker = 'npc') {
|
||||
this.currentSpeaker = speaker;
|
||||
showDialogue(text, characterId = 'npc') {
|
||||
this.currentSpeaker = characterId;
|
||||
|
||||
console.log(`📝 showDialogue called with speaker: ${speaker}, text length: ${text?.length || 0}`);
|
||||
console.log(`📝 dialogueText element:`, this.elements.dialogueText);
|
||||
console.log(`📝 speakerName element:`, this.elements.speakerName);
|
||||
console.log(`📝 showDialogue called with character: ${characterId}, text length: ${text?.length || 0}`);
|
||||
|
||||
// Get character data
|
||||
let character = this.characters[characterId];
|
||||
if (!character) {
|
||||
// Fallback for legacy speaker values
|
||||
if (characterId === 'player') {
|
||||
character = this.playerData;
|
||||
} else if (characterId === 'npc' || !characterId) {
|
||||
character = this.npc;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine display name
|
||||
const displayName = character?.displayName || (characterId === 'player' ? 'You' : 'NPC');
|
||||
const speakerType = characterId === 'player' ? 'player' : 'npc';
|
||||
|
||||
// Update speaker name and label
|
||||
const displayName = speaker === 'npc' ? (this.npc?.displayName || 'NPC') : 'You';
|
||||
this.elements.portraitLabel.textContent = displayName;
|
||||
this.elements.speakerName.textContent = displayName;
|
||||
|
||||
console.log(`📝 Set speaker name to: ${displayName}`);
|
||||
|
||||
// Update speaker styling
|
||||
this.elements.portraitSection.className = `person-chat-portrait-section speaker-${speaker}`;
|
||||
this.elements.speakerName.className = `person-chat-speaker-name ${speaker}-speaker`;
|
||||
this.elements.portraitSection.className = `person-chat-portrait-section speaker-${speakerType}`;
|
||||
this.elements.speakerName.className = `person-chat-speaker-name ${speakerType}-speaker`;
|
||||
|
||||
// Update dialogue text
|
||||
this.elements.dialogueText.textContent = text;
|
||||
@@ -199,7 +211,7 @@ export default class PersonChatUI {
|
||||
console.log(`📝 Set dialogue text, element content: "${this.elements.dialogueText.textContent}"`);
|
||||
|
||||
// Reset portrait for new speaker
|
||||
this.updatePortraitForSpeaker(speaker);
|
||||
this.updatePortraitForSpeaker(characterId, character);
|
||||
|
||||
// Reset continue button state
|
||||
this.hasContinued = false;
|
||||
@@ -207,33 +219,32 @@ export default class PersonChatUI {
|
||||
|
||||
/**
|
||||
* Update portrait for the current speaker
|
||||
* @param {string} speaker - 'npc' or 'player'
|
||||
* @param {string} characterId - Character ID
|
||||
* @param {Object} character - Character data
|
||||
*/
|
||||
updatePortraitForSpeaker(speaker) {
|
||||
updatePortraitForSpeaker(characterId, character) {
|
||||
try {
|
||||
if (!this.portraitRenderer) {
|
||||
if (!this.portraitRenderer || !character) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update sprite data for current speaker
|
||||
if (speaker === 'npc' && this.npc) {
|
||||
// Use the actual NPC object to preserve all properties (including spriteTalk)
|
||||
this.portraitRenderer.npc = this.npc;
|
||||
this.portraitRenderer.setupSpriteInfo();
|
||||
this.portraitRenderer.render();
|
||||
} else if (speaker === 'player') {
|
||||
// Create player NPC object from playerData with spriteTalk
|
||||
if (characterId === 'player' || character.id === 'player') {
|
||||
// Create player object for portrait rendering
|
||||
this.portraitRenderer.npc = {
|
||||
id: 'player',
|
||||
displayName: this.playerData.displayName || 'Agent 0x00',
|
||||
spriteSheet: this.playerData.spriteSheet || 'hacker',
|
||||
spriteTalk: this.playerData.spriteTalk || 'assets/characters/hacker-talk.png',
|
||||
spriteConfig: this.playerData.spriteConfig || {},
|
||||
_sprite: this.playerSprite
|
||||
displayName: character.displayName || 'Agent 0x00',
|
||||
spriteSheet: character.spriteSheet || 'hacker',
|
||||
spriteTalk: character.spriteTalk || 'assets/characters/hacker-talk.png',
|
||||
spriteConfig: character.spriteConfig || {}
|
||||
};
|
||||
this.portraitRenderer.setupSpriteInfo();
|
||||
this.portraitRenderer.render();
|
||||
} else {
|
||||
// Use NPC character object
|
||||
this.portraitRenderer.npc = character;
|
||||
}
|
||||
|
||||
this.portraitRenderer.setupSpriteInfo();
|
||||
this.portraitRenderer.render();
|
||||
} catch (error) {
|
||||
console.error('❌ Error updating portrait:', error);
|
||||
}
|
||||
|
||||
@@ -1,49 +1,61 @@
|
||||
// Test Ink script for development
|
||||
VAR test_counter = 0
|
||||
VAR player_visited_room = false
|
||||
|
||||
=== start ===
|
||||
# speaker: TestNPC
|
||||
# type: bark
|
||||
Hello! This is a test message from Ink.
|
||||
~ test_counter++
|
||||
-> END
|
||||
|
||||
=== test_room_reception ===
|
||||
# speaker: TestNPC
|
||||
# type: bark
|
||||
{player_visited_room:
|
||||
You're back in reception.
|
||||
- else:
|
||||
Welcome to reception! This is your first time here.
|
||||
~ player_visited_room = true
|
||||
}
|
||||
-> END
|
||||
|
||||
=== test_item_lockpick ===
|
||||
# speaker: TestNPC
|
||||
# type: bark
|
||||
You picked up a lockpick! Nice find.
|
||||
~ test_counter++
|
||||
-> END
|
||||
// Test Ink script - Multi-character conversation with camera focus
|
||||
// Demonstrates player, Front NPC (test_npc_front), and Back NPC (test_npc_back) in dialogue
|
||||
VAR conversation_started = false
|
||||
|
||||
=== hub ===
|
||||
# speaker: TestNPC
|
||||
# type: conversation
|
||||
What would you like to test?
|
||||
Counter: {test_counter}
|
||||
+ [Test choice 1] -> test_1
|
||||
+ [Test choice 2] -> test_2
|
||||
+ [Exit] -> END
|
||||
# speaker:npc:test_npc_back
|
||||
Welcome! This is a group conversation test. Let me introduce you to my colleague.
|
||||
+ [Listen in on the introduction] -> group_meeting
|
||||
|
||||
=== group_meeting ===
|
||||
# speaker:npc:test_npc_back
|
||||
Agent, meet my colleague from the back office. BACK
|
||||
-> colleague_introduction
|
||||
|
||||
=== colleague_introduction ===
|
||||
# speaker:npc:test_npc_front
|
||||
Nice to meet you! I'm the lead technician here. FRONT.
|
||||
-> player_question
|
||||
|
||||
=== player_question ===
|
||||
# speaker:player
|
||||
What kind of work do you both do here?
|
||||
-> front_npc_explains
|
||||
|
||||
=== front_npc_explains ===
|
||||
# speaker:npc:test_npc_back
|
||||
Well, I handle the front desk operations and guest interactions. But my colleague here...
|
||||
-> colleague_responds
|
||||
|
||||
=== colleague_responds ===
|
||||
# speaker:npc:test_npc_front
|
||||
I manage all the backend systems and security infrastructure. Together, we keep everything running smoothly.
|
||||
-> player_follow_up
|
||||
|
||||
=== player_follow_up ===
|
||||
# speaker:player
|
||||
That sounds like a well-coordinated operation!
|
||||
-> front_npc_agrees
|
||||
|
||||
=== front_npc_agrees ===
|
||||
# speaker:npc:test_npc_back
|
||||
It really is! We've been working together for several years now. Communication is key.
|
||||
-> colleague_adds
|
||||
|
||||
=== colleague_adds ===
|
||||
# speaker:npc:test_npc_front
|
||||
Exactly. And we're always looking for talented people like you to join our team.
|
||||
-> player_closing
|
||||
|
||||
=== player_closing ===
|
||||
# speaker:player
|
||||
I appreciate the opportunity. I'll definitely consider it.
|
||||
-> conversation_end
|
||||
|
||||
=== conversation_end ===
|
||||
# speaker:npc:test_npc_back
|
||||
Great! Feel free to explore and let us know if you have any questions.
|
||||
-> END
|
||||
|
||||
|
||||
=== test_1 ===
|
||||
# speaker: TestNPC
|
||||
You selected test choice 1!
|
||||
Counter: {test_counter}
|
||||
-> hub
|
||||
|
||||
=== test_2 ===
|
||||
# speaker: TestNPC
|
||||
You selected test choice 2!
|
||||
~ test_counter++
|
||||
-> hub
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{"inkVersion":21,"root":[[["done",{"#n":"g-0"}],null],"done",{"start":["#","^speaker: TestNPC","/#","#","^type: bark","/#","^Hello! This is a test message from Ink.","\n","ev",{"VAR?":"test_counter"},1,"+",{"VAR=":"test_counter","re":true},"/ev","end",null],"test_room_reception":["#","^speaker: TestNPC","/#","#","^type: bark","/#","ev",{"VAR?":"player_visited_room"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^You're back in reception.","\n",{"->":".^.^.^.11"},null]}],[{"->":".^.b"},{"b":["\n","^Welcome to reception! This is your first time here.","\n","ev",true,"/ev",{"VAR=":"player_visited_room","re":true},{"->":".^.^.^.11"},null]}],"nop","\n","end",null],"test_item_lockpick":["#","^speaker: TestNPC","/#","#","^type: bark","/#","^You picked up a lockpick! Nice find.","\n","ev",{"VAR?":"test_counter"},1,"+",{"VAR=":"test_counter","re":true},"/ev","end",null],"hub":[["#","^speaker: TestNPC","/#","#","^type: conversation","/#","^What would you like to test?","\n","^Counter: ","ev",{"VAR?":"test_counter"},"out","/ev","\n","ev","str","^Test choice 1","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Test choice 2","/str","/ev",{"*":".^.c-1","flg":4},"ev","str","^Exit","/str","/ev",{"*":".^.c-2","flg":4},{"c-0":["^ ",{"->":"test_1"},"\n",null],"c-1":["^ ",{"->":"test_2"},"\n",null],"c-2":["^ ","end","\n",null]}],null],"test_1":["#","^speaker: TestNPC","/#","^You selected test choice 1!","\n","^Counter: ","ev",{"VAR?":"test_counter"},"out","/ev","\n",{"->":"hub"},null],"test_2":["#","^speaker: TestNPC","/#","^You selected test choice 2!","\n","ev",{"VAR?":"test_counter"},1,"+",{"VAR=":"test_counter","re":true},"/ev",{"->":"hub"},null],"global decl":["ev",0,{"VAR=":"test_counter"},false,{"VAR=":"player_visited_room"},"/ev","end",null]}],"listDefs":{}}
|
||||
61
scenarios/ink/test2.ink
Normal file
61
scenarios/ink/test2.ink
Normal file
@@ -0,0 +1,61 @@
|
||||
// Test Ink script - Multi-character conversation with camera focus
|
||||
// Demonstrates player, Front NPC (test_npc_front), and Back NPC (test_npc_back) in dialogue
|
||||
VAR conversation_started = false
|
||||
|
||||
=== hub ===
|
||||
# speaker:npc:test_npc_back
|
||||
Welcome! This is a group conversation test. Let me introduce you to my colleague.
|
||||
+ [Listen in on the introduction] -> group_meeting
|
||||
|
||||
=== group_meeting ===
|
||||
# speaker:npc:test_npc_back
|
||||
Agent, meet my colleague from the back office. BACK
|
||||
-> colleague_introduction
|
||||
|
||||
=== colleague_introduction ===
|
||||
# speaker:npc:test_npc_front
|
||||
Nice to meet you! I'm the lead technician here. FRONT.
|
||||
-> player_question
|
||||
|
||||
=== player_question ===
|
||||
# speaker:player
|
||||
What kind of work do you both do here?
|
||||
-> front_npc_explains
|
||||
|
||||
=== front_npc_explains ===
|
||||
# speaker:npc:test_npc_back
|
||||
Well, I handle the front desk operations and guest interactions. But my colleague here...
|
||||
-> colleague_responds
|
||||
|
||||
=== colleague_responds ===
|
||||
# speaker:npc:test_npc_front
|
||||
I manage all the backend systems and security infrastructure. Together, we keep everything running smoothly.
|
||||
-> player_follow_up
|
||||
|
||||
=== player_follow_up ===
|
||||
# speaker:player
|
||||
That sounds like a well-coordinated operation!
|
||||
-> front_npc_agrees
|
||||
|
||||
=== front_npc_agrees ===
|
||||
# speaker:npc:test_npc_back
|
||||
It really is! We've been working together for several years now. Communication is key.
|
||||
-> colleague_adds
|
||||
|
||||
=== colleague_adds ===
|
||||
# speaker:npc:test_npc_front
|
||||
Exactly. And we're always looking for talented people like you to join our team.
|
||||
-> player_closing
|
||||
|
||||
=== player_closing ===
|
||||
# speaker:player
|
||||
I appreciate the opportunity. I'll definitely consider it.
|
||||
-> conversation_end
|
||||
|
||||
=== conversation_end ===
|
||||
# speaker:npc:test_npc_back
|
||||
Great! Feel free to explore and let us know if you have any questions.
|
||||
-> END
|
||||
|
||||
|
||||
|
||||
1
scenarios/ink/test2.json
Normal file
1
scenarios/ink/test2.json
Normal file
@@ -0,0 +1 @@
|
||||
{"inkVersion":21,"root":[[["done",{"#n":"g-0"}],null],"done",{"hub":[["#","^speaker:npc:test_npc_back","/#","^Welcome! This is a group conversation test. Let me introduce you to my colleague.","\n","ev","str","^Listen in on the introduction","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ",{"->":"group_meeting"},"\n",null]}],null],"group_meeting":["#","^speaker:npc:test_npc_back","/#","^Agent, meet my colleague from the back office. BACK","\n",{"->":"colleague_introduction"},null],"colleague_introduction":["#","^speaker:npc:test_npc_front","/#","^Nice to meet you! I'm the lead technician here. FRONT.","\n",{"->":"player_question"},null],"player_question":["#","^speaker:player","/#","^What kind of work do you both do here?","\n",{"->":"front_npc_explains"},null],"front_npc_explains":["#","^speaker:npc:test_npc_back","/#","^Well, I handle the front desk operations and guest interactions. But my colleague here...","\n",{"->":"colleague_responds"},null],"colleague_responds":["#","^speaker:npc:test_npc_front","/#","^I manage all the backend systems and security infrastructure. Together, we keep everything running smoothly.","\n",{"->":"player_follow_up"},null],"player_follow_up":["#","^speaker:player","/#","^That sounds like a well-coordinated operation!","\n",{"->":"front_npc_agrees"},null],"front_npc_agrees":["#","^speaker:npc:test_npc_back","/#","^It really is! We've been working together for several years now. Communication is key.","\n",{"->":"colleague_adds"},null],"colleague_adds":["#","^speaker:npc:test_npc_front","/#","^Exactly. And we're always looking for talented people like you to join our team.","\n",{"->":"player_closing"},null],"player_closing":["#","^speaker:player","/#","^I appreciate the opportunity. I'll definitely consider it.","\n",{"->":"conversation_end"},null],"conversation_end":["#","^speaker:npc:test_npc_back","/#","^Great! Feel free to explore and let us know if you have any questions.","\n","end",null],"global decl":["ev",false,{"VAR=":"conversation_started"},"/ev","end",null]}],"listDefs":{}}
|
||||
@@ -42,7 +42,7 @@
|
||||
"idleFrameStart": 20,
|
||||
"idleFrameEnd": 23
|
||||
},
|
||||
"storyPath": "scenarios/ink/test.ink.json",
|
||||
"storyPath": "scenarios/ink/test2.json",
|
||||
"currentKnot": "hub"
|
||||
}
|
||||
],
|
||||
Reference in New Issue
Block a user