// Animal Crossing-style dialog system for dougk // Typewriter text effect with character portraits import { playDialogBlip } from '../sounds.js' let dialogOverlay = null let currentDialog = null let currentLineIndex = 0 let currentCharIndex = 0 let isTyping = false let typeInterval = null let onCompleteCallback = null const TYPE_SPEED = 35 // ms per character const FAST_TYPE_SPEED = 10 // when holding/clicking during type const STYLES = ` .dialog-overlay { position: fixed; bottom: 0; left: 0; right: 0; display: flex; justify-content: center; padding: 20px; z-index: 1500; pointer-events: none; } .dialog-box { background: linear-gradient(180deg, #2a2218 0%, #1a1510 100%); border: 4px solid #8b6914; border-radius: 16px; padding: 16px 20px; max-width: 600px; width: 90%; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6); pointer-events: auto; cursor: pointer; position: relative; } .dialog-header { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; } .dialog-portrait { width: 48px; height: 48px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 28px; flex-shrink: 0; } .dialog-portrait.donny { background: linear-gradient(135deg, #7a9eb8 0%, #5a7e98 100%); border: 3px solid #d4af37; } .dialog-portrait.ollie { background: linear-gradient(135deg, #7b4b94 0%, #5b2b74 100%); border: 3px solid #d4af37; } .dialog-name { font-family: 'Courier New', monospace; font-size: 18px; font-weight: bold; color: #ffd700; text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5); } .dialog-text { font-family: 'Courier New', monospace; font-size: 16px; color: #e8d8c8; line-height: 1.5; min-height: 48px; } .dialog-continue { position: absolute; bottom: 12px; right: 16px; color: #ffd700; font-size: 14px; opacity: 0; transition: opacity 0.3s; animation: bounce 0.6s ease-in-out infinite; } .dialog-continue.visible { opacity: 1; } @keyframes bounce { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-4px); } } .dialog-box.shake { animation: dialogShake 0.1s ease-in-out; } @keyframes dialogShake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-2px); } 75% { transform: translateX(2px); } } ` const CHARACTER_INFO = { donny: { name: 'Donny', emoji: '🦄', portrait: 'donny' }, ollie: { name: 'Ollie', emoji: '🐙', portrait: 'ollie' } } function injectStyles() { if (document.getElementById('dialog-styles')) return const style = document.createElement('style') style.id = 'dialog-styles' style.textContent = STYLES document.head.appendChild(style) } function createDialogOverlay(character) { injectStyles() const charInfo = CHARACTER_INFO[character] const overlay = document.createElement('div') overlay.className = 'dialog-overlay' overlay.innerHTML = `
${charInfo.emoji}
${charInfo.name}
` const box = overlay.querySelector('.dialog-box') box.addEventListener('click', handleAdvance) // Also handle keyboard document.addEventListener('keydown', handleKeyDown) return overlay } function handleKeyDown(e) { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault() handleAdvance() } } function handleAdvance() { if (!currentDialog) return if (isTyping) { // Skip to end of current line finishCurrentLine() } else { // Advance to next line advanceDialog() } } function finishCurrentLine() { if (typeInterval) { clearInterval(typeInterval) typeInterval = null } const textEl = dialogOverlay.querySelector('.dialog-text') textEl.textContent = currentDialog.lines[currentLineIndex] isTyping = false // Show continue indicator const continueEl = dialogOverlay.querySelector('.dialog-continue') continueEl.classList.add('visible') } function advanceDialog() { currentLineIndex++ if (currentLineIndex >= currentDialog.lines.length) { // Dialog complete - save callback before closing (closeDialog clears it) const callback = onCompleteCallback closeDialog() if (callback) { callback() } return } // Hide continue indicator const continueEl = dialogOverlay.querySelector('.dialog-continue') continueEl.classList.remove('visible') // Start typing next line startTypingLine() } function startTypingLine() { const textEl = dialogOverlay.querySelector('.dialog-text') const line = currentDialog.lines[currentLineIndex] textEl.textContent = '' currentCharIndex = 0 isTyping = true typeInterval = setInterval(() => { if (currentCharIndex < line.length) { const char = line[currentCharIndex] textEl.textContent += char currentCharIndex++ // Play blip sound for letters (skip spaces/punctuation, play every 2 chars) if (char.match(/[a-zA-Z]/) && currentCharIndex % 2 === 0) { playDialogBlip() } // Add slight shake on punctuation for emphasis if (['.', '!', '?', '...'].some(p => line.substring(0, currentCharIndex).endsWith(p))) { const box = dialogOverlay.querySelector('.dialog-box') box.classList.add('shake') setTimeout(() => box.classList.remove('shake'), 100) } } else { // Line complete clearInterval(typeInterval) typeInterval = null isTyping = false // Show continue indicator const continueEl = dialogOverlay.querySelector('.dialog-continue') continueEl.classList.add('visible') } }, TYPE_SPEED) } export function showDialog(character, lines, container, onComplete) { if (dialogOverlay) { closeDialog() } currentDialog = { character, lines } currentLineIndex = 0 currentCharIndex = 0 onCompleteCallback = onComplete dialogOverlay = createDialogOverlay(character) container.appendChild(dialogOverlay) // Start typing first line startTypingLine() } export function closeDialog() { if (typeInterval) { clearInterval(typeInterval) typeInterval = null } document.removeEventListener('keydown', handleKeyDown) if (dialogOverlay) { dialogOverlay.remove() dialogOverlay = null } currentDialog = null currentLineIndex = 0 currentCharIndex = 0 isTyping = false onCompleteCallback = null } export function isDialogOpen() { return dialogOverlay !== null }