UNPKG

yukinovel

Version:

Yukinovel is a simple web visual novel engine.

1,324 lines (1,321 loc) 54.1 kB
export class UIRenderer { constructor(game) { this.originalCharacterSprites = {}; this.isLogVisible = false; // Typewriter effect properties this.isTyping = false; this.currentTypewriterTimeout = null; this.typewriterSpeed = 20; this.currentDialogueText = ''; this.currentCharacterName = ''; this.currentCharacterColor = '#fff'; this.justSkippedTyping = false; this.game = game; this.backgroundVideo = null; this.exitConfirmModal = null; } detectBackgroundType(url) { const extension = url.toLowerCase().split('.').pop(); if (extension === 'mp4' || extension === 'webm' || extension === 'ogg') { return 'video'; } else if (extension === 'gif') { return 'gif'; } else { return 'image'; } } setupBackgroundVideo(url) { const video = document.createElement('video'); video.src = url; video.autoplay = true; video.loop = true; video.muted = true; video.playsInline = true; return video; } setupMainMenuBackground(config) { let backgroundStyle = 'background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);'; if (config.backgroundColor) { backgroundStyle = `background: ${config.backgroundColor};`; } const backgroundUrl = config.backgroundVideo || config.background; if (backgroundUrl) { const backgroundType = this.detectBackgroundType(backgroundUrl); if (backgroundType === 'video') { const video = this.setupBackgroundVideo(backgroundUrl); video.style.position = 'absolute'; video.style.top = '0'; video.style.left = '0'; video.style.width = '100%'; video.style.height = '100%'; video.style.objectFit = 'cover'; video.style.zIndex = '0'; this.mainMenuContainer.appendChild(video); backgroundStyle = 'background: transparent;'; } else { backgroundStyle = ` background-image: url('${backgroundUrl}'); background-size: cover; background-position: center; background-repeat: no-repeat; `; } } if (config.backgroundOverlay) { backgroundStyle += ` position: relative; `; } this.mainMenuContainer.style.cssText = ` position: absolute; top: 0; left: 0; width: 100%; height: 100%; ${backgroundStyle} display: flex; pointer-events: auto; `; } // Render initial UI structure render(container) { this.container = container; this.container.id = 'container'; this.createUIStructure(); this.attachEventListeners(); } createUIStructure() { // Main Menu container this.mainMenuContainer = document.createElement('div'); this.mainMenuContainer.id = 'container-menu'; this.mainMenuContainer.style.cssText = ` position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: none; pointer-events: auto; `; this.container.appendChild(this.mainMenuContainer); // Game container this.gameContainer = document.createElement('div'); this.gameContainer.id = 'container-game'; this.gameContainer.style.cssText = ` position: absolute; top: 0; left: 0; width: 100%; height: 100%; `; this.container.appendChild(this.gameContainer); // Background layer this.backgroundElement = document.createElement('div'); this.backgroundElement.id = 'background-game'; this.backgroundElement.style.cssText = ` position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-size: cover; background-position: center; background-repeat: no-repeat; transition: opacity 0.5s ease; `; this.gameContainer.appendChild(this.backgroundElement); // Character layer this.characterContainer = document.createElement('div'); this.characterContainer.id = 'container-character'; this.characterContainer.style.cssText = ` position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; `; this.gameContainer.appendChild(this.characterContainer); // UI layer this.uiContainer = document.createElement('div'); this.uiContainer.style.cssText = ` position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; `; this.gameContainer.appendChild(this.uiContainer); // Dialogue container this.dialogueContainer = document.createElement('div'); this.dialogueContainer.style.cssText = ` position: absolute; bottom: 0; left: 0; width: 100%; height: 200px; background: linear-gradient(to bottom, rgba(0,0,0,0.8), rgba(0,0,0,0.9)); color: white; padding: 20px; box-sizing: border-box; pointer-events: auto; cursor: pointer; transition: opacity 0.3s ease; `; this.uiContainer.appendChild(this.dialogueContainer); // Choices container this.choicesContainer = document.createElement('div'); this.choicesContainer.style.cssText = ` position: absolute; bottom: 220px; left: 50%; transform: translateX(-50%); width: 80%; max-width: 600px; pointer-events: auto; display: none; `; this.uiContainer.appendChild(this.choicesContainer); // Controls container this.controlsContainer = document.createElement('div'); this.controlsContainer.style.cssText = ` position: absolute; bottom: 10px; right: 10px; color: white; background-color: rgba(0,0,0,0.2); display: flex; align-items: center; gap: 15px; font-size: 12px; padding: 8px 12px; border-radius: 8px; backdrop-filter: blur(5px); pointer-events: auto; opacity: 0.8; transition: opacity 0.3s ease; `; this.controlsContainer.onmouseover = () => this.controlsContainer.style.opacity = '1'; this.controlsContainer.onmouseout = () => this.controlsContainer.style.opacity = '0.8'; this.uiContainer.appendChild(this.controlsContainer); this.createControlButtons(); // Log container this.logContainer = document.createElement('div'); this.logContainer.style.cssText = ` position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.95); color: white; padding: 20px; box-sizing: border-box; overflow-y: auto; display: none; pointer-events: auto; `; this.uiContainer.appendChild(this.logContainer); } // Main Menu methods showMainMenu() { this.createMainMenu(); this.mainMenuContainer.style.display = 'block'; this.gameContainer.style.display = 'none'; } hideMainMenu() { this.mainMenuContainer.style.display = 'none'; this.gameContainer.style.display = 'block'; } createMainMenu() { this.mainMenuContainer.innerHTML = ''; const config = this.game.getScript().settings?.mainMenu || {}; const langManager = this.game.getLanguageManager(); this.setupMainMenuBackground(config); if (config.backgroundOverlay) { const overlay = document.createElement('div'); overlay.style.cssText = ` position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: ${config.backgroundOverlay}; z-index: 1; `; this.mainMenuContainer.appendChild(overlay); } // Main content container const contentContainer = document.createElement('div'); const layout = config.layout || {}; const alignment = layout.alignment || 'center'; const padding = layout.padding || 50; contentContainer.style.cssText = ` position: relative; z-index: 2; width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: ${layout.titlePosition === 'top' ? 'flex-start' : layout.titlePosition === 'bottom' ? 'flex-end' : 'center'}; align-items: ${alignment === 'left' ? 'flex-start' : alignment === 'right' ? 'flex-end' : 'center'}; padding: ${padding}px; box-sizing: border-box; `; // Title const titleConfig = config.title || {}; const titleDiv = document.createElement('div'); const titleText = langManager.getLocalizedText(titleConfig.text || this.game.getScript().title || 'Visual Novel'); const titleColor = titleConfig.color || '#ffffff'; const titleSize = titleConfig.fontSize || 48; const titleFont = titleConfig.fontFamily || 'Arial, sans-serif'; let titleStyle = ` font-size: ${titleSize}px; font-weight: bold; color: ${titleColor}; font-family: ${titleFont}; margin-bottom: 20px; text-align: ${alignment}; `; if (titleConfig.shadow) { titleStyle += 'text-shadow: 2px 2px 4px rgba(0,0,0,0.5);'; } if (titleConfig.gradient) { titleStyle += ` background: ${titleConfig.gradient}; -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; `; } titleDiv.style.cssText = titleStyle; titleDiv.textContent = titleText; // Animation if (titleConfig.animation === 'fade') { titleDiv.style.animation = 'fadeIn 2s ease-in-out'; } else if (titleConfig.animation === 'slide') { titleDiv.style.animation = 'slideIn 1s ease-out'; } else if (titleConfig.animation === 'glow') { titleDiv.style.animation = 'glow 2s ease-in-out infinite alternate'; } contentContainer.appendChild(titleDiv); // Subtitle const subtitleConfig = config.subtitle || {}; if (subtitleConfig.show !== false) { const subtitleDiv = document.createElement('div'); const subtitleText = langManager.getLocalizedText(subtitleConfig.text || '') || langManager.getSubtitleText(); subtitleDiv.style.cssText = ` font-size: ${subtitleConfig.fontSize || 16}px; color: ${subtitleConfig.color || '#cccccc'}; margin-bottom: 50px; text-align: ${alignment}; `; subtitleDiv.textContent = subtitleText; contentContainer.appendChild(subtitleDiv); } // Buttons container const buttonsContainer = document.createElement('div'); const buttonConfig = config.buttons || {}; const buttonSpacing = buttonConfig.spacing || 15; const buttonWidth = buttonConfig.width || 300; buttonsContainer.style.cssText = ` display: flex; flex-direction: column; gap: ${buttonSpacing}px; width: ${buttonWidth}px; `; // Menu buttons const menuButtons = [ { text: langManager.getText('menu.start'), action: () => this.game.startNewGame(), icon: '' }, { text: langManager.getText('menu.continue'), action: () => this.game.continueGame(), icon: '' }, { text: langManager.getText('menu.load'), action: () => this.game.loadGame(), icon: '' }, { text: langManager.getText('menu.settings'), action: () => this.showSettings(buttonsContainer), icon: '' }, { text: langManager.getText('menu.credits'), action: () => this.showCredits(), icon: '' }, { text: langManager.getText('menu.exit'), action: () => window.close(), icon: '' } ]; menuButtons.forEach(button => { const buttonEl = this.createMenuButton(button.text, button.action, button.icon, config); buttonsContainer.appendChild(buttonEl); }); contentContainer.appendChild(buttonsContainer); this.mainMenuContainer.appendChild(contentContainer); this.addMainMenuAnimations(); } createMenuButton(text, action, icon, config) { const buttonConfig = config.buttons || {}; const button = document.createElement('button'); const style = buttonConfig.style || 'modern'; const color = buttonConfig.color || '#4A90E2'; const hoverColor = buttonConfig.hoverColor || '#357ABD'; const textColor = buttonConfig.textColor || '#ffffff'; const fontSize = buttonConfig.fontSize || 18; const borderRadius = buttonConfig.borderRadius || 8; let buttonStyle = ` background: ${color}; color: ${textColor}; border: none; padding: 15px 30px; font-size: ${fontSize}px; border-radius: ${borderRadius}px; cursor: pointer; transition: all 0.3s ease; font-weight: bold; display: flex; align-items: center; justify-content: flex-start; gap: 15px; width: 100%; box-sizing: border-box; `; // Style variations if (style === 'glass') { buttonStyle += ` background: rgba(255,255,255,0.1); backdrop-filter: blur(10px); border: 1px solid rgba(255,255,255,0.2); `; } else if (style === 'minimal') { buttonStyle += ` background: transparent; border: 2px solid ${color}; color: ${color}; `; } else if (style === 'classic') { buttonStyle += ` background: linear-gradient(45deg, ${color}, ${hoverColor}); box-shadow: 0 4px 8px rgba(0,0,0,0.2); `; } button.style.cssText = buttonStyle; button.innerHTML = `<span style="font-size: 20px;">${icon}</span><span>${text}</span>`; // Hover effects button.onmouseover = () => { if (style === 'minimal') { button.style.background = color; button.style.color = textColor; } else { button.style.background = hoverColor; } if (buttonConfig.animation === 'bounce') { button.style.transform = 'scale(1.05)'; } else if (buttonConfig.animation === 'slide') { button.style.transform = 'translateX(10px)'; } }; button.onmouseout = () => { if (style === 'minimal') { button.style.background = 'transparent'; button.style.color = color; } else { button.style.background = color; } button.style.transform = 'none'; }; button.onclick = action; return button; } addMainMenuAnimations() { const style = document.createElement('style'); style.innerHTML = ` @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes slideIn { from { transform: translateY(-50px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } @keyframes glow { from { text-shadow: 0 0 10px rgba(74,144,226,0.5); } to { text-shadow: 0 0 20px rgba(74,144,226,1), 0 0 30px rgba(74,144,226,0.8); } } `; document.head.appendChild(style); } showCredits() { console.log('Credits modal'); } createControlButtons() { const langManager = this.game.getLanguageManager(); const controls = [ { key: 'Enter', action: langManager.getText('ui.next'), onClick: () => this.handleNext() }, { key: 'Esc', action: langManager.getText('exit.title', 'Thoát'), onClick: () => this.showExitConfirm() }, { key: 'S', action: langManager.getText('ui.save'), onClick: () => this.game.saveGame() }, { key: 'L', action: langManager.getText('ui.load'), onClick: () => this.game.loadGame() }, { key: 'H', action: langManager.getText('ui.history'), onClick: () => this.toggleLog() } ]; controls.forEach(control => { const controlDiv = document.createElement('div'); controlDiv.style.cssText = ` display: flex; align-items: center; gap: 4px; cursor: pointer; transition: all 0.2s ease; padding: 4px 6px; border-radius: 4px; opacity: 0.8; `; controlDiv.onmouseover = () => { controlDiv.style.opacity = '1'; controlDiv.style.backgroundColor = 'rgba(255,255,255,0.1)'; }; controlDiv.onmouseout = () => { controlDiv.style.opacity = '0.8'; controlDiv.style.backgroundColor = 'transparent'; }; controlDiv.onclick = control.onClick; const keyDiv = document.createElement('div'); keyDiv.textContent = control.key; keyDiv.style.cssText = ` background: rgba(255,255,255,0.2); padding: 2px 6px; border-radius: 3px; font-weight: bold; font-size: 10px; min-width: 20px; text-align: center; `; const actionDiv = document.createElement('div'); actionDiv.textContent = control.action; actionDiv.style.cssText = ` font-size: 10px; white-space: nowrap; `; controlDiv.appendChild(keyDiv); controlDiv.appendChild(actionDiv); this.controlsContainer.appendChild(controlDiv); }); } handleNext() { if (this.isTyping) { this.skipTyping(); } else if (!this.justSkippedTyping) { this.game.next(); } } attachEventListeners() { this.dialogueContainer.addEventListener('click', () => { this.handleNext(); }); const gameContainer = document.querySelector('#container-game'); document.addEventListener('keydown', (e) => { if (gameContainer.style.display === 'none') return; if (e.code === 'Space' || e.code === 'Enter') { e.preventDefault(); this.handleNext(); } else if (e.code === 'Escape') { e.preventDefault(); this.showExitConfirm(); } else if (e.code === 'Backspace') { e.preventDefault(); this.game.back(); } else if (e.code === 'KeyS') { e.preventDefault(); this.game.saveGame(); } else if (e.code === 'KeyL') { e.preventDefault(); this.game.loadGame(); } else if (e.code === 'KeyH') { e.preventDefault(); this.toggleLog(); } }); } toggleLog() { const isCurrentlyVisible = this.logContainer.style.display === 'block'; if (isCurrentlyVisible) { this.hideLog(); } else { this.showLog(); } } showLog() { const langManager = this.game.getLanguageManager(); const globalHistory = this.game.getGlobalDialogueHistory(); let logHtml = `<div style="font-size: 24px; font-weight: bold; margin-bottom: 20px; text-align: center;">${langManager.getText('history.title')}</div>`; if (globalHistory.length === 0) { logHtml += `<div style="text-align: center; color: #ccc; margin: 50px 0;">${langManager.getText('history.empty')}</div>`; } else { globalHistory.forEach((entry, index) => { const { dialogue, sceneId } = entry; const characterName = dialogue.character; const character = characterName ? this.game.getScript().characters[characterName] : null; const scene = this.game.getSceneById(sceneId); const isFirstDialogueOfScene = index === 0 || globalHistory[index - 1].sceneId !== sceneId; if (isFirstDialogueOfScene && scene) { logHtml += `<div style=" margin: 20px 0 10px 0; padding: 8px 12px; border-radius: 5px; font-weight: bold; font-size: 14px; text-align: center; border-left: 4px solid #4A90E2; ">${scene.id.toUpperCase()}</div>`; } logHtml += `<div style="margin-bottom: 12px; padding: 8px 12px; background: rgba(255,255,255,0.08); border-radius: 5px;">`; if (character) { logHtml += `<div style="color: ${character.color || '#fff'}; font-weight: bold; margin-bottom: 4px; font-size: 13px;">${character.name}</div>`; } const dialogueText = langManager.getLocalizedText(dialogue.text); logHtml += `<div style="line-height: 1.4; font-size: 14px;">${dialogueText}</div>`; logHtml += `</div>`; }); } const closeButton = document.createElement('div'); closeButton.style.cssText = ` text-align: center; margin-top: 20px; position: sticky; bottom: 20px; `; closeButton.innerHTML = ` <button style=" background: #007bff; color: white; border: none; padding: 12px 24px; border-radius: 6px; cursor: pointer; font-size: 16px; box-shadow: 0 2px 8px rgba(0,123,255,0.3); transition: all 0.2s ease; " onmouseover="this.style.background='#0056b3'; this.style.transform='translateY(-1px)'" onmouseout="this.style.background='#007bff'; this.style.transform='translateY(0)'">${langManager.getText('ui.close')} (H)</button> `; const button = closeButton.querySelector('button'); if (button) { button.onclick = () => this.hideLog(); } this.logContainer.innerHTML = logHtml; this.logContainer.appendChild(closeButton); this.logContainer.style.display = 'block'; this.isLogVisible = true; } hideLog() { this.logContainer.style.display = 'none'; this.isLogVisible = false; } showExitConfirm() { if (this.exitConfirmModal) return; const langManager = this.game.getLanguageManager(); this.exitConfirmModal = document.createElement('div'); this.exitConfirmModal.style.cssText = ` position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0, 0, 0, 0.8); display: flex; justify-content: center; align-items: center; z-index: 10000; opacity: 0; transition: opacity 0.3s ease; `; const modalContent = document.createElement('div'); modalContent.style.cssText = ` background: rgba(30, 30, 30, 0.95); border: 2px solid rgba(255, 255, 255, 0.3); border-radius: 10px; padding: 30px; text-align: center; max-width: 400px; width: 90%; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); transform: scale(0.9); transition: transform 0.3s ease; `; const title = document.createElement('h3'); title.textContent = langManager.getText('exit.title', 'Xác nhận thoát'); title.style.cssText = ` color: #fff; margin: 0 0 15px 0; font-size: 20px; font-weight: normal; `; const message = document.createElement('p'); message.textContent = langManager.getText('exit.message', 'Bạn có chắc chắn muốn thoát về menu chính không?'); message.style.cssText = ` color: #ccc; margin: 0 0 25px 0; font-size: 14px; line-height: 1.4; `; const buttonsContainer = document.createElement('div'); buttonsContainer.style.cssText = ` display: flex; gap: 15px; justify-content: center; `; const yesButton = document.createElement('button'); yesButton.textContent = langManager.getText('ui.yes', 'Có'); yesButton.style.cssText = ` padding: 10px 20px; background: #e74c3c; color: white; border: 1px solid #c0392b; border-radius: 5px; font-size: 14px; cursor: pointer; transition: background 0.2s ease; min-width: 80px; `; const noButton = document.createElement('button'); noButton.textContent = langManager.getText('ui.no', 'Không'); noButton.id = 'exit-no-button'; noButton.style.cssText = ` padding: 10px 20px; background: #27ae60; color: white; border: 1px solid #229954; border-radius: 5px; font-size: 14px; cursor: pointer; transition: all 0.2s ease; min-width: 80px; position: relative; `; yesButton.onmouseover = () => yesButton.style.background = '#c0392b'; yesButton.onmouseout = () => yesButton.style.background = '#e74c3c'; noButton.onmouseover = () => noButton.style.background = '#229954'; noButton.onmouseout = () => noButton.style.background = '#27ae60'; yesButton.onclick = () => { this.hideExitConfirm(); this.game.showMainMenu(); }; noButton.onclick = () => { this.hideExitConfirm(); }; const handleKeyPress = (e) => { if (e.key === 'Escape') { e.preventDefault(); this.hideExitConfirm(); } else if (e.key === 'Enter') { e.preventDefault(); noButton.click(); } }; document.addEventListener('keydown', handleKeyPress); this.exitConfirmModal.keyHandler = handleKeyPress; buttonsContainer.appendChild(yesButton); buttonsContainer.appendChild(noButton); modalContent.appendChild(title); modalContent.appendChild(message); modalContent.appendChild(buttonsContainer); this.exitConfirmModal.appendChild(modalContent); document.body.appendChild(this.exitConfirmModal); requestAnimationFrame(() => { this.exitConfirmModal.style.opacity = '1'; modalContent.style.transform = 'scale(1)'; }); } hideExitConfirm() { if (!this.exitConfirmModal) return; const keyHandler = this.exitConfirmModal.keyHandler; if (keyHandler) { document.removeEventListener('keydown', keyHandler); } this.exitConfirmModal.style.opacity = '0'; const modalContent = this.exitConfirmModal.querySelector('div'); if (modalContent) { modalContent.style.transform = 'scale(0.9)'; } setTimeout(() => { if (this.exitConfirmModal) { document.body.removeChild(this.exitConfirmModal); this.exitConfirmModal = null; } }, 300); } // Update scene updateScene(scene) { this.updateSceneBackground(scene); this.updateCharacters(scene.characters || []); if (scene.characters) { scene.characters.forEach(character => { this.originalCharacterSprites[character.name] = character.image; }); } } updateSceneBackground(scene) { if (this.backgroundVideo) { this.backgroundVideo.pause(); this.backgroundVideo.remove(); this.backgroundVideo = null; } this.backgroundElement.style.backgroundImage = ''; this.backgroundElement.innerHTML = ''; const backgroundUrl = scene.backgroundVideo || scene.background; if (!backgroundUrl) return; let backgroundType = scene.backgroundType || 'auto'; if (backgroundType === 'auto') { backgroundType = this.detectBackgroundType(backgroundUrl); } switch (backgroundType) { case 'video': this.backgroundVideo = this.setupBackgroundVideo(backgroundUrl); this.backgroundVideo.style.position = 'absolute'; this.backgroundVideo.style.top = '0'; this.backgroundVideo.style.left = '0'; this.backgroundVideo.style.width = '100%'; this.backgroundVideo.style.height = '100%'; this.backgroundVideo.style.objectFit = 'cover'; this.backgroundVideo.style.zIndex = '0'; this.backgroundElement.appendChild(this.backgroundVideo); break; case 'gif': case 'image': default: this.backgroundElement.style.backgroundImage = `url(${backgroundUrl})`; break; } } // Update dialogue updateDialogue(dialogue) { const characterName = dialogue.character; const character = characterName ? this.game.getScript().characters[characterName] : null; this.stopTyping(); const localizedText = this.game.getLanguageManager().getLocalizedText(dialogue.text); this.currentDialogueText = localizedText; this.currentCharacterName = character ? character.name : ''; this.currentCharacterColor = character ? character.color || '#fff' : '#fff'; this.startTypewriter(); this.choicesContainer.style.display = 'none'; if (character && dialogue.emotion && character.emotions) { this.updateCharacterEmotion(characterName, dialogue.emotion); } if (character && dialogue.sprite) { this.updateCharacterSprite(characterName, dialogue.sprite); } if (dialogue.characterSprite) { Object.keys(dialogue.characterSprite).forEach(charName => { this.updateCharacterSprite(charName, dialogue.characterSprite[charName]); }); } this.restoreUnspecifiedCharacterSprites(dialogue); } // Typewriter effect methods startTypewriter() { this.isTyping = true; this.justSkippedTyping = false; this.dialogueContainer.style.display = 'block'; this.typeText('', 0); } typeText(currentText, index) { if (index >= this.currentDialogueText.length) { this.isTyping = false; return; } const nextChar = this.currentDialogueText[index]; const newText = currentText + nextChar; let html = ''; if (this.currentCharacterName) { html += `<div style="color: ${this.currentCharacterColor}; font-weight: bold; margin-bottom: 10px;">${this.currentCharacterName}</div>`; } html += `<div style="font-size: 18px; line-height: 1.4;">${newText}</div>`; this.dialogueContainer.innerHTML = html; // đệ quy this.currentTypewriterTimeout = window.setTimeout(() => { this.typeText(newText, index + 1); }, this.typewriterSpeed); } stopTyping() { if (this.currentTypewriterTimeout) { clearTimeout(this.currentTypewriterTimeout); this.currentTypewriterTimeout = null; } } skipTyping() { if (this.isTyping) { this.stopTyping(); this.isTyping = false; this.justSkippedTyping = true; let html = ''; if (this.currentCharacterName) { html += `<div style="color: ${this.currentCharacterColor}; font-weight: bold; margin-bottom: 10px;">${this.currentCharacterName}</div>`; } html += `<div style="font-size: 18px; line-height: 1.4;">${this.currentDialogueText}</div>`; this.dialogueContainer.innerHTML = html; setTimeout(() => { this.justSkippedTyping = false; }, 100); } } // Show choices showChoices(choices) { const langManager = this.game.getLanguageManager(); this.dialogueContainer.style.display = 'none'; this.choicesContainer.style.display = 'block'; let html = ''; choices.forEach((choice, index) => { const choiceText = langManager.getLocalizedText(choice.text); html += ` <button class="choice-button" data-index="${index}" style=" display: block; width: 100%; margin: 10px 0; padding: 15px 20px; background: rgba(255,255,255,0.9); border: none; border-radius: 5px; font-size: 16px; cursor: pointer; transition: background 0.3s ease; " onmouseover="this.style.background='rgba(255,255,255,1)'" onmouseout="this.style.background='rgba(255,255,255,0.9)'" > ${choiceText} </button> `; }); this.choicesContainer.innerHTML = html; this.choicesContainer.querySelectorAll('.choice-button').forEach((button, index) => { button.addEventListener('click', () => { this.game.makeChoice(choices[index]); }); }); } // Update characters updateCharacters(characters) { this.characterContainer.innerHTML = ''; characters.forEach((character, index) => { const charElement = document.createElement('div'); const position = character.position || {}; const x = position.x !== undefined ? position.x : (20 + index * 200); const y = position.y !== undefined ? position.y : 0; const width = position.width || 300; const height = position.height || 400; const scale = position.scale || 1; const xValue = typeof x === 'string' ? x : `${x}px`; const yValue = typeof y === 'string' ? y : `${y}px`; charElement.style.cssText = ` position: absolute; bottom: ${yValue}; left: ${xValue}; width: ${width}px; height: ${height}px; background-image: url(${character.image || ''}); background-size: contain; background-position: bottom; background-repeat: no-repeat; transition: opacity 0.5s ease; transform: scale(${scale}); transform-origin: bottom center; `; charElement.id = `character-${character.name}`; this.characterContainer.appendChild(charElement); }); } updateCharacterEmotion(characterName, emotion) { const character = this.game.getScript().characters[characterName]; if (character && character.emotions && character.emotions[emotion]) { const charElement = document.getElementById(`character-${characterName}`); if (charElement) { charElement.style.backgroundImage = `url(${character.emotions[emotion]})`; } } } updateCharacterSprite(characterName, sprite) { const charElement = document.getElementById(`character-${characterName}`); if (charElement) { charElement.style.backgroundImage = `url(${sprite || ''})`; } } restoreUnspecifiedCharacterSprites(dialogue) { const specifiedCharacters = new Set(); if (dialogue.character && dialogue.sprite) { specifiedCharacters.add(dialogue.character); } if (dialogue.characterSprite) { Object.keys(dialogue.characterSprite).forEach(charName => { specifiedCharacters.add(charName); }); } Object.keys(this.originalCharacterSprites).forEach(charName => { if (!specifiedCharacters.has(charName)) { const charElement = document.getElementById(`character-${charName}`); if (charElement) { charElement.style.backgroundImage = `url(${this.originalCharacterSprites[charName] || ''})`; } } }); } showSettings(menuOverlay) { const langManager = this.game.getLanguageManager(); const config = this.game.getScript().settings?.mainMenu || {}; const settingsContainer = document.createElement('div'); settingsContainer.id = 'settings-container'; this.setupSettingsBackground(settingsContainer, config); let backgroundStyle = 'background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);'; if (config.backgroundColor) { backgroundStyle = `background: ${config.backgroundColor};`; } if (config.background) { backgroundStyle = ` background-image: url('${config.background}'); background-size: cover; background-position: center; background-repeat: no-repeat; `; } settingsContainer.style.cssText = ` position: absolute; top: 0; left: 0; width: 100%; height: 100%; ${backgroundStyle} color: white; overflow-y: auto; pointer-events: auto; z-index: 1000; `; if (config.backgroundOverlay) { const overlay = document.createElement('div'); overlay.style.cssText = ` position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: ${config.backgroundOverlay}; z-index: 1; `; settingsContainer.appendChild(overlay); } // Main content container const contentContainer = document.createElement('div'); contentContainer.style.cssText = ` position: relative; z-index: 2; max-width: 800px; margin: 0 auto; padding: 60px 40px; min-height: 100vh; display: flex; flex-direction: column; `; // Header section const headerSection = document.createElement('div'); headerSection.style.cssText = ` text-align: center; margin-bottom: 60px; `; const settingsTitle = document.createElement('h1'); settingsTitle.textContent = langManager.getText('menu.settings'); settingsTitle.style.cssText = ` font-size: 42px; font-weight: bold; margin: 0 0 15px 0; text-shadow: 2px 2px 4px rgba(0,0,0,0.7); color: white; `; const settingsSubtitle = document.createElement('p'); settingsSubtitle.textContent = langManager.getText('settings.subtitle', 'Tùy chỉnh trải nghiệm game của bạn'); settingsSubtitle.style.cssText = ` font-size: 16px; margin: 0; opacity: 0.9; color: #f0f0f0; `; headerSection.appendChild(settingsTitle); headerSection.appendChild(settingsSubtitle); contentContainer.appendChild(headerSection); // Settings sections container const sectionsContainer = document.createElement('div'); sectionsContainer.style.cssText = ` display: flex; flex-direction: column; gap: 30px; flex: 1; margin-bottom: 40px; `; // Language Settings Section const languageSection = this.createSimpleSettingsSection(langManager.getText('settings.language'), langManager.getText('settings.language.desc'), 'lang'); const languageSelect = document.createElement('select'); languageSelect.style.cssText = ` width: 100%; padding: 12px 16px; font-size: 16px; border: 2px solid rgba(255,255,255,0.3); border-radius: 8px; background: rgba(0,0,0,0.3); color: white; cursor: pointer; transition: all 0.3s ease; `; // Language options const availableLanguages = this.game.getAvailableLanguages(); const currentLanguage = this.game.getCurrentLanguage(); availableLanguages.forEach(lang => { const option = document.createElement('option'); option.value = lang.code; option.textContent = lang.name; option.style.cssText = ` background: #333; color: white; `; if (lang.code === currentLanguage) { option.selected = true; } languageSelect.appendChild(option); }); languageSelect.addEventListener('change', (e) => { const target = e.target; this.game.setLanguage(target.value); }); languageSelect.addEventListener('focus', () => { languageSelect.style.borderColor = 'rgba(255,255,255,0.6)'; languageSelect.style.background = 'rgba(0,0,0,0.5)'; }); languageSelect.addEventListener('blur', () => { languageSelect.style.borderColor = 'rgba(255,255,255,0.3)'; languageSelect.style.background = 'rgba(0,0,0,0.3)'; }); languageSection.appendChild(languageSelect); sectionsContainer.appendChild(languageSection); // Text Speed Settings Section const textSpeedSection = this.createSimpleSettingsSection(langManager.getText('settings.textSpeed'), langManager.getText('settings.textSpeed.desc'), 'speed'); const speedContainer = document.createElement('div'); speedContainer.style.cssText = ` display: flex; align-items: center; gap: 20px; margin-bottom: 20px; `; const speedSlider = document.createElement('input'); speedSlider.type = 'range'; speedSlider.min = '10'; speedSlider.max = '100'; speedSlider.value = this.typewriterSpeed.toString(); speedSlider.style.cssText = ` flex: 1; height: 6px; border-radius: 3px; background: rgba(255,255,255,0.3); outline: none; cursor: pointer; -webkit-appearance: none; `; const speedValue = document.createElement('span'); speedValue.textContent = this.typewriterSpeed + 'ms'; speedValue.style.cssText = ` min-width: 60px; text-align: center; font-weight: bold; color: white; background: rgba(0,0,0,0.3); padding: 8px 12px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.3); `; speedValue.addEventListener('change', (e) => { const target = e.target; this.typewriterSpeed = Number.parseInt(target.value); }); speedSlider.addEventListener('input', (e) => { const target = e.target; this.typewriterSpeed = parseInt(target.value); speedValue.textContent = target.value + 'ms'; }); speedContainer.appendChild(speedSlider); speedContainer.appendChild(speedValue); // Speed presets const speedPresets = document.createElement('div'); speedPresets.style.cssText = ` display: flex; gap: 15px; flex-wrap: wrap; `; const presets = [ { label: langManager.getText('ui.slow'), value: 60 }, { label: langManager.getText('ui.normal'), value: 30 }, { label: langManager.getText('ui.fast'), value: 15 }, { label: langManager.getText('ui.veryfast'), value: 5 } ]; presets.forEach(preset => { const presetButton = document.createElement('button'); presetButton.textContent = preset.label; presetButton.style.cssText = ` padding: 8px 16px; border: 1px solid rgba(255,255,255,0.4); border-radius: 6px; background: rgba(0,0,0,0.3); color: white; cursor: pointer; transition: all 0.3s ease; font-size: 14px; `; presetButton.addEventListener('click', () => { this.typewriterSpeed = preset.value; speedSlider.value = preset.value.toString(); speedValue.textContent = preset.value + 'ms'; }); presetButton.addEventListener('mouseenter', () => { presetButton.style.background = 'rgba(255,255,255,0.2)'; presetButton.style.borderColor = 'rgba(255,255,255,0.6)'; }); presetButton.addEventListener('mouseleave', () => { presetButton.style.background = 'rgba(0,0,0,0.3)'; presetButton.style.borderColor = 'rgba(255,255,255,0.4)'; }); speedPresets.appendChild(presetButton); }); textSpeedSection.appendChild(speedContainer); textSpeedSection.appendChild(speedPresets); sectionsContainer.appendChild(textSpeedSection); // Keyboard Shortcuts Section const shortcutsSection = this.createSimpleSettingsSection(langManager.getText('settings.shortcuts', 'Phím tắt'), langManager.getText('settings.shortcuts.desc'), 'keys'); const shortcutsList = document.createElement('div'); shortcutsList.style.cssText = ` display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 12px; `; const shortcuts = [ { key: 'Space / Enter', action: langManager.getText('ui.next') }, { key: 'Esc', action: langManager.getText('ui.home') }, { key: 'S', action: langManager.getText('ui.save') }, { key: 'L', action: langManager.getText('ui.load') }, { key: 'H', action: langManager.getText('ui.history') }, { key: 'Esc', action: langManager.getText('ui.menu', 'Menu') } ]; shortcuts.forEach(shortcut => { const shortcutItem = document.createElement('div'); shortcutItem.style.cssText = ` display: flex; align-items: center; gap: 12px; padding: 10px 12px; background: rgba(0,0,0,0.3); border-radius: 6px; border: 1px solid rgba(255,255,255,0.2); transition: all 0.3s ease; `; const keyElement = document.createElement('span'); keyElement.textContent = shortcut.key; keyElement.style.cssText = ` background: rgba(255,255,255,0.2); padding: 4px 8px; border-radius: 4px; font-family: 'Courier New', monospace; font-weight: bold; min-width: 80px; text-align: center; font-size: 13px; border: 1px solid rgba(255,255,255,0.3); `; const actionElement = document.createElement('span'); actionElement.textContent = shortcut.action; actionElement.style.cssText = ` flex: 1; font-size: 14px; color: #f0f0f0; `; shortcutItem.appendChild(keyElement); shortcutItem.appendChild(actionElement); shortcutItem.addEventListener('mouseenter', () => { shortcutItem.style.background = 'rgba(255,255,255,0.1)'; shortcutItem.style.borderColor = 'rgba(255,255,255,0.4)'; }); shortcutItem.addEventListener('mouseleave', () => { shortcutItem.style.background = 'rgba(0,0,0,0.3)'; shortcutItem.style.borderColor = 'rgba(255,255,255,0.2)'; }); shortcutsList.appendChild(shortcutItem); }); shortcutsSection.appendChild(shortcutsList); sectionsContainer.appendChild(shortcutsSection); contentContainer.appendChild(sectionsContainer); const footerSection = document.createElement('div'); footerSection.style.cssText = ` text-align: center; margin-top: auto; padding-top: 20px; `; const closeButton = document.createElement('button'); closeButton.className = 'back-menu'; closeButton.textContent = `← ${langManager.getText('ui.back')}`; closeButton.style.cssText = ` padding: 12px 30px; font-size: 16px; font-weight: bold; border: 2px solid rgba(255,255,255,0.4); border-radius: 8px; background: rgba(0,0,0,0.3); color: white; cursor: pointer; transition: all 0.3s ease; min-width: 120px; `; closeButton.addEventListener('mouseenter', () => { closeButton.style.background = 'rgba(255,255,255,0.2)'; closeButton.style.borderColor = 'rgba(255,255,255,0.6)'; }); closeButton.addEventListener('mouseleave', () => { closeButton.style.background = 'rgba(0,0,0,0.3)'; closeButton.style.borderColor = 'rgba(255,255,255,0.4)'; }); closeButton.onclick = () => { menuOverlay.remove(); this.showMainMenu(); }; footerSection.appendChild(closeButton); contentContainer.appendChild(footerSection); settingsContainer.appendChild(contentContainer); menuOverlay.innerHTML = ''; menuOverlay.appendChild(settingsContainer); } createSimpleSettingsSection(title, description, iconType) { const section = document.createElement('div'); section.style.cssText = ` background: rgba(0,0,0,0.4); border-radius: 10px; padding: 25px; border: 1px solid rgba(255,255,255,0.2); transition: all 0.3s ease; `; const header = document.createElement('div'); header.style.cssText = ` display: flex; align-items: center; gap: 15px; margin-bottom: 12px; `; const iconElement = document.createElement('div'); iconElement.style.cssText = ` width: 32px; height: 32px; border-radius: 6px; background: rgba(255,255,255,0.2); border: 1px solid rgba(255,255,255,0.3);