mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-20 13:50:46 +00:00
- Created a new JSON scenario file for testing NPC sprite functionality. - Implemented a simple HTTP server with caching headers for development purposes. - Added an HTML page for testing NPC interactions, including system checks and game controls. - Introduced a separate HTML page for testing item delivery through person chat interactions.
22 KiB
22 KiB
Person-Chat Minigame Design
Overview
A cinematic conversation interface that shows zoomed character portraits during face-to-face dialogue with in-person NPCs. Similar to phone-chat but with visual emphasis on the characters speaking.
Visual Design Philosophy
Cinematic Presentation
- Large character portraits: 4x zoomed sprites showing character faces
- Side-by-side layout: NPC on left, player on right
- Subtitle-style dialogue: Text overlays the portrait area or appears below
- Minimal UI chrome: Focus on characters and conversation
- Pixel-art aesthetic: Maintain sharp edges, no border-radius, 2px borders
Differences from Phone-Chat
| Aspect | Phone-Chat | Person-Chat |
|---|---|---|
| Context | Remote messaging | Face-to-face |
| Visuals | Phone UI with avatar icons | Zoomed sprite portraits |
| Layout | Single column messages | Side-by-side characters |
| Atmosphere | Asynchronous | Immediate/present |
| Contact list | Multiple contacts | Single conversation |
UI Layout
Full Interface Mockup
╔═══════════════════════════════════════════════════════════════╗
║ In Conversation - Alex the Sysadmin [X] ║
╠═══════════════════════════════════════════════════════════════╣
║ ║
║ ┏━━━━━━━━━━━━━━━┓ ┏━━━━━━━━━━━━━━━┓ ║
║ ┃ ┃ ┃ ┃ ║
║ ┃ ┃ ┃ ┃ ║
║ ┃ NPC Face ┃ ┃ Player Face ┃ ║
║ ┃ (Zoomed ┃ ┃ (Zoomed ┃ ║
║ ┃ 4x) ┃ ┃ 4x) ┃ ║
║ ┃ ┃ ┃ ┃ ║
║ ┃ ┃ ┃ ┃ ║
║ ┗━━━━━━━━━━━━━━━┛ ┗━━━━━━━━━━━━━━━┛ ║
║ Alex You ║
║ ║
║ ┌─────────────────────────────────────────────────────────┐ ║
║ │ "I've been monitoring the security logs all day. There │ ║
║ │ are some really suspicious access patterns coming from │ ║
║ │ the CEO's office. Want me to show you?" │ ║
║ └─────────────────────────────────────────────────────────┘ ║
║ ║
║ ┌─────────────────────────────────────────────────────────┐ ║
║ │ [1] Yes, please show me the logs │ ║
║ └─────────────────────────────────────────────────────────┘ ║
║ ┌─────────────────────────────────────────────────────────┐ ║
║ │ [2] Can you give me access to the server room? │ ║
║ └─────────────────────────────────────────────────────────┘ ║
║ ┌─────────────────────────────────────────────────────────┐ ║
║ │ [3] I'll come back later when I have more questions │ ║
║ └─────────────────────────────────────────────────────────┘ ║
║ ║
╠═══════════════════════════════════════════════════════════════╣
║ [Add to Notepad] [Close] ║
╚═══════════════════════════════════════════════════════════════╝
Layout Zones
- Header (60px): Title and close button
- Portrait Area (300px): Character faces side-by-side
- Dialogue Area (150px): Text box showing current speech
- Choices Area (flexible): Choice buttons stacked
- Footer (60px): Notebook and close buttons
Portrait Rendering System
Approach: Canvas Screenshot of Game Viewport (SIMPLIFIED)
Strategy:
- Capture current game canvas when conversation starts
- Scale and zoom on specific sprite positions
- Use CSS transform for visual zoom effect
- Simple, performant, no complex texture management
Pros:
- Minimal code complexity
- Reuses existing game rendering
- Works with any sprite instantly
- No special texture management
- Easy to zoom in with CSS transform
Cons:
- Static portrait (doesn't update with animations during conversation)
- Need to center on sprite when zooming
Implementation:
class SpritePortrait {
constructor(gameCanvas, sprite, scale = 4) {
this.gameCanvas = gameCanvas;
this.sprite = sprite;
this.scale = scale; // Zoom level (4x)
}
captureAsDataURL() {
// Get canvas image data
return this.gameCanvas.toDataURL();
}
getZoomViewBox() {
// Calculate viewport to show zoomed sprite
const spriteX = this.sprite.x;
const spriteY = this.sprite.y;
// At 4x zoom, we want 256x256 area centered on sprite
// Original area before zoom: 64x64
const viewWidth = 256 / this.scale; // 64
const viewHeight = 256 / this.scale; // 64
return {
x: spriteX - viewWidth / 2,
y: spriteY - viewHeight / 2,
width: viewWidth,
height: viewHeight,
scale: this.scale
};
}
}
CSS Zoom Effect:
.person-chat-portrait-canvas {
width: 256px;
height: 256px;
image-rendering: pixelated;
object-fit: cover;
object-position: center;
}
Why This Works
- Simplicity: One screenshot, scale via CSS
- Reuses game rendering: No duplicate rendering
- Pixel-perfect: Maintains game's pixel art style
- Performance: Single capture, CSS transform is GPU accelerated
- Flexibility: Works with any sprite, any animation state
Person-Chat Minigame Module Structure
File Organization
js/minigames/person-chat/
├── person-chat-minigame.js # Main controller (extends MinigameScene)
├── person-chat-ui.js # UI rendering
├── person-chat-portraits.js # Portrait rendering system
└── person-chat-conversation.js # Conversation flow logic
Module: person-chat-minigame.js
Main controller extending MinigameScene.
/**
* PersonChatMinigame - Face-to-face conversation interface
*
* Extends MinigameScene to provide cinematic character portraits
* during in-person NPC conversations.
*/
import { MinigameScene } from '../framework/base-minigame.js';
import PersonChatUI from './person-chat-ui.js';
import PersonChatPortraits from './person-chat-portraits.js';
import PersonChatConversation from './person-chat-conversation.js';
import InkEngine from '../../systems/ink/ink-engine.js';
export class PersonChatMinigame extends MinigameScene {
constructor(container, params) {
super(container, params);
// Validate required params
if (!params.npcId) {
throw new Error('PersonChatMinigame requires npcId');
}
// Get managers
this.npcManager = window.npcManager;
this.inkEngine = new InkEngine();
// Initialize modules
this.ui = null;
this.portraits = null;
this.conversation = null;
// State
this.currentNPCId = params.npcId;
this.npc = this.npcManager.getNPC(this.currentNPCId);
console.log('🎭 PersonChatMinigame created for', this.npc.displayName);
}
init() {
// Set up base minigame structure
super.init();
// Customize header
this.headerElement.innerHTML = `
<h3>In Conversation - ${this.npc.displayName}</h3>
`;
// Initialize portrait rendering system
this.portraits = new PersonChatPortraits(
window.game,
this.npc._sprite,
window.player
);
// Initialize UI
this.ui = new PersonChatUI(
this.gameContainer,
this.params,
this.portraits
);
this.ui.render();
// Initialize conversation system
this.conversation = new PersonChatConversation(
this.npcManager,
this.inkEngine,
this.currentNPCId
);
// Set up event listeners
this.setupEventListeners();
// Start conversation
this.startConversation();
}
startConversation() {
console.log('🎭 Starting conversation with', this.npc.displayName);
// Trigger talking animation on NPC sprite
if (this.npc._sprite) {
const talkAnim = `npc-${this.npc.id}-talk`;
if (this.npc._sprite.anims.exists(talkAnim)) {
this.npc._sprite.play(talkAnim, true);
}
}
// Load Ink story and show initial dialogue
this.conversation.start().then(() => {
this.showCurrentDialogue();
});
}
showCurrentDialogue() {
const dialogue = this.conversation.getCurrentText();
const choices = this.conversation.getChoices();
// Update UI
this.ui.showDialogue(dialogue);
this.ui.showChoices(choices);
// Update portraits
this.portraits.update();
}
setupEventListeners() {
// Choice button clicks
this.addEventListener(this.ui.elements.choicesContainer, 'click', (e) => {
const choiceButton = e.target.closest('.choice-button');
if (choiceButton) {
const choiceIndex = parseInt(choiceButton.dataset.index);
this.selectChoice(choiceIndex);
}
});
// Notebook button
const notebookBtn = document.getElementById('minigame-notebook');
if (notebookBtn) {
this.addEventListener(notebookBtn, 'click', () => {
this.saveConversationToNotepad();
});
}
}
selectChoice(choiceIndex) {
// Process choice through Ink
this.conversation.selectChoice(choiceIndex).then(() => {
// Handle action tags (unlock doors, give items, etc.)
this.handleActionTags();
// Update dialogue
if (this.conversation.canContinue()) {
this.showCurrentDialogue();
} else {
// Conversation ended
this.endConversation();
}
});
}
handleActionTags() {
const tags = this.conversation.getCurrentTags();
tags.forEach(tag => {
if (tag.startsWith('unlock_door:')) {
const doorId = tag.split(':')[1];
window.unlockDoor(doorId);
} else if (tag.startsWith('give_item:')) {
const itemType = tag.split(':')[1];
window.giveItemToPlayer(itemType);
}
});
}
endConversation() {
console.log('🎭 Conversation ended');
// Return NPC to idle animation
if (this.npc._sprite) {
const idleAnim = `npc-${this.npc.id}-idle`;
this.npc._sprite.play(idleAnim, true);
}
// Close minigame
this.close();
}
saveConversationToNotepad() {
const history = this.npcManager.getConversationHistory(this.currentNPCId);
const text = this.formatConversationHistory(history);
if (window.notebookManager) {
window.notebookManager.addEntry({
title: `Conversation: ${this.npc.displayName}`,
content: text,
category: 'conversations'
});
}
}
formatConversationHistory(history) {
return history.map(entry => {
const speaker = entry.type === 'npc' ? this.npc.displayName : 'You';
return `${speaker}: ${entry.text}`;
}).join('\n\n');
}
cleanup() {
// Clean up portraits
if (this.portraits) {
this.portraits.destroy();
}
super.cleanup();
}
}
Module: person-chat-portraits.js
Handles portrait rendering using RenderTexture.
/**
* PersonChatPortraits - Portrait Rendering System
*
* Manages 4x zoomed character portraits for person-chat interface.
* Uses RenderTexture to capture and scale sprite faces.
*/
export default class PersonChatPortraits {
constructor(scene, npcSprite, playerSprite) {
this.scene = scene;
this.npcSprite = npcSprite;
this.playerSprite = playerSprite;
// Portrait dimensions (256x256 @ 4x zoom of 64x64 sprite)
this.portraitSize = 256;
this.cropHeight = 40; // Upper portion for face
// Create render textures
this.npcPortrait = this.createPortraitTexture('npc');
this.playerPortrait = this.createPortraitTexture('player');
// Initial render
this.update();
}
createPortraitTexture(id) {
const texture = this.scene.add.renderTexture(
0, 0,
this.portraitSize,
this.portraitSize
);
texture.setOrigin(0.5, 0.5);
texture.name = `portrait_${id}`;
return texture;
}
update() {
// Update NPC portrait
this.renderPortrait(
this.npcPortrait,
this.npcSprite
);
// Update player portrait
this.renderPortrait(
this.playerPortrait,
this.playerSprite
);
}
renderPortrait(renderTexture, sprite) {
if (!sprite || !renderTexture) return;
// Clear previous render
renderTexture.clear();
// Create temp sprite with current frame
const tempSprite = this.scene.add.sprite(0, 0, sprite.texture.key);
tempSprite.setFrame(sprite.frame.name);
// Crop to face area (top portion of sprite)
tempSprite.setCrop(0, 0, 64, this.cropHeight);
// Scale up 4x
tempSprite.setScale(4);
// Center in render texture
const centerX = this.portraitSize / 2;
const centerY = this.portraitSize / 2;
// Draw to texture
renderTexture.draw(tempSprite, centerX, centerY);
// Clean up
tempSprite.destroy();
}
getNPCPortraitDataURL() {
return this.npcPortrait.canvas.toDataURL();
}
getPlayerPortraitDataURL() {
return this.playerPortrait.canvas.toDataURL();
}
destroy() {
if (this.npcPortrait) {
this.npcPortrait.destroy();
}
if (this.playerPortrait) {
this.playerPortrait.destroy();
}
}
}
Module: person-chat-ui.js
Renders UI elements and integrates portraits.
/**
* PersonChatUI - UI Rendering
*
* Creates and manages HTML UI for person-chat interface.
*/
export default class PersonChatUI {
constructor(container, params, portraits) {
this.container = container;
this.params = params;
this.portraits = portraits;
this.elements = {};
}
render() {
// Create main UI structure
const html = `
<div class="person-chat-container">
<!-- Portraits Section -->
<div class="person-chat-portraits">
<div class="portrait-wrapper portrait-npc">
<canvas id="npc-portrait-canvas"></canvas>
<div class="portrait-label">${this.params.npcName}</div>
</div>
<div class="portrait-wrapper portrait-player">
<canvas id="player-portrait-canvas"></canvas>
<div class="portrait-label">You</div>
</div>
</div>
<!-- Dialogue Section -->
<div class="person-chat-dialogue">
<div class="dialogue-text" id="dialogue-text"></div>
</div>
<!-- Choices Section -->
<div class="person-chat-choices" id="choices-container">
<!-- Dynamically filled -->
</div>
</div>
`;
this.container.innerHTML = html;
// Store element references
this.elements = {
portraitNPC: document.getElementById('npc-portrait-canvas'),
portraitPlayer: document.getElementById('player-portrait-canvas'),
dialogueText: document.getElementById('dialogue-text'),
choicesContainer: document.getElementById('choices-container')
};
// Render portraits to canvases
this.updatePortraitCanvases();
}
updatePortraitCanvases() {
// Draw NPC portrait
const npcCanvas = this.elements.portraitNPC;
const npcCtx = npcCanvas.getContext('2d');
npcCanvas.width = 256;
npcCanvas.height = 256;
const npcImage = new Image();
npcImage.src = this.portraits.getNPCPortraitDataURL();
npcImage.onload = () => {
npcCtx.drawImage(npcImage, 0, 0);
};
// Draw player portrait
const playerCanvas = this.elements.portraitPlayer;
const playerCtx = playerCanvas.getContext('2d');
playerCanvas.width = 256;
playerCanvas.height = 256;
const playerImage = new Image();
playerImage.src = this.portraits.getPlayerPortraitDataURL();
playerImage.onload = () => {
playerCtx.drawImage(playerImage, 0, 0);
};
}
showDialogue(text) {
this.elements.dialogueText.textContent = text;
}
showChoices(choices) {
this.elements.choicesContainer.innerHTML = '';
choices.forEach((choice, index) => {
const button = document.createElement('button');
button.className = 'choice-button person-chat-choice';
button.dataset.index = index;
button.textContent = `[${index + 1}] ${choice.text}`;
this.elements.choicesContainer.appendChild(button);
});
}
}
CSS Styling
File: css/person-chat-minigame.css
/* Person-Chat Minigame Styles */
.person-chat-container {
display: flex;
flex-direction: column;
gap: 20px;
padding: 20px;
max-width: 900px;
margin: 0 auto;
}
/* Portraits Section */
.person-chat-portraits {
display: flex;
justify-content: space-around;
gap: 40px;
padding: 20px;
}
.portrait-wrapper {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.portrait-wrapper canvas {
width: 256px;
height: 256px;
border: 2px solid #000;
image-rendering: pixelated;
image-rendering: crisp-edges;
}
.portrait-label {
font-size: 18px;
font-weight: bold;
text-align: center;
}
/* Dialogue Section */
.person-chat-dialogue {
background-color: #f0f0f0;
border: 2px solid #000;
padding: 20px;
min-height: 100px;
}
.dialogue-text {
font-size: 16px;
line-height: 1.5;
white-space: pre-wrap;
}
/* Choices Section */
.person-chat-choices {
display: flex;
flex-direction: column;
gap: 10px;
}
.person-chat-choice {
background-color: #fff;
border: 2px solid #000;
padding: 15px;
font-size: 16px;
text-align: left;
cursor: pointer;
transition: background-color 0.2s;
}
.person-chat-choice:hover {
background-color: #e0e0e0;
}
.person-chat-choice:active {
background-color: #d0d0d0;
}
Integration with Interaction System
Triggering Person-Chat
// In js/systems/interactions.js
function handleNPCInteraction(npc) {
console.log('💬 Starting conversation with', npc.displayName);
// Start person-chat minigame
window.MinigameFramework.startMinigame('person-chat', {
npcId: npc.id,
npcName: npc.displayName,
title: `Talking to ${npc.displayName}`,
onComplete: (result) => {
console.log('Conversation complete:', result);
// Emit event
if (window.eventDispatcher) {
window.eventDispatcher.emit('npc_conversation_ended', {
npcId: npc.id,
npcName: npc.displayName
});
}
}
});
}
Animation Synchronization
During Conversation
- NPC speaking: Show NPC's current animation frame in portrait
- Player choosing: Subtle highlight on player portrait
- Action performed: Brief flash or effect on relevant portrait
Update Timing
// In person-chat-minigame.js update loop
update() {
// Update portraits to match current sprite frames
if (this.portraits) {
this.portraits.update();
this.ui.updatePortraitCanvases();
}
}
Next Steps
- Implement PersonChatMinigame class
- Create portrait rendering system
- Style UI with person-chat-minigame.css
- Integrate with interaction system
- Test with sample NPC