| |
| |
| |
| |
| |
|
|
| (function () { |
| 'use strict'; |
|
|
| |
| const TYPEWRITER_SPEED = 30; |
| const OVERLAY_DISMISS_MS = 5000; |
| const SCENARIO_DISMISS_MS = 6000; |
| const COIN_RAIN_DEFAULT = 10; |
| const INIT_RETRY_DELAY = 2000; |
| const DEBOUNCE_MS = 100; |
|
|
| |
| let _previousHP = null; |
| let _previousMeta = null; |
| let _typewriterActive = false; |
| let _observerSetup = false; |
| let _lastNarrativeText = ''; |
| let _lastChatCount = 0; |
|
|
|
|
| |
| |
| |
|
|
| function applyTypewriter(element) { |
| if (!element) return; |
|
|
| |
| if (element.dataset.typing === 'true') return; |
|
|
| const fullHTML = element.innerHTML; |
| const fullText = element.textContent || ''; |
|
|
| |
| if (fullText.trim().length < 2) return; |
|
|
| |
| if (fullText === element.dataset.lastTyped) return; |
|
|
| element.dataset.typing = 'true'; |
| element.dataset.lastTyped = fullText; |
| _typewriterActive = true; |
|
|
| |
| |
| const wrapper = document.createElement('span'); |
| wrapper.className = 'typewriter-wrapper'; |
| wrapper.innerHTML = fullHTML; |
| wrapper.style.visibility = 'hidden'; |
|
|
| |
| element.innerHTML = ''; |
| element.appendChild(wrapper); |
|
|
| |
| |
| const isRichContent = fullHTML !== fullText; |
|
|
| if (isRichContent) { |
| |
| _typewriterRichReveal(element, fullHTML, fullText); |
| } else { |
| |
| _typewriterPlainReveal(element, fullText); |
| } |
| } |
|
|
| function _typewriterPlainReveal(element, text) { |
| element.innerHTML = ''; |
| let index = 0; |
|
|
| const cursor = document.createElement('span'); |
| cursor.className = 'typewriter-cursor'; |
| cursor.textContent = '|'; |
|
|
| function typeNext() { |
| if (index < text.length) { |
| |
| const chunk = text.substring(index, index + 2); |
| element.insertBefore( |
| document.createTextNode(chunk), |
| cursor |
| ); |
| index += 2; |
| element.appendChild(cursor); |
| element.scrollTop = element.scrollHeight; |
| setTimeout(typeNext, TYPEWRITER_SPEED); |
| } else { |
| |
| element.dataset.typing = 'false'; |
| _typewriterActive = false; |
| |
| setTimeout(() => { |
| if (cursor.parentNode) cursor.remove(); |
| }, 2000); |
| } |
| } |
|
|
| element.appendChild(cursor); |
| typeNext(); |
| } |
|
|
| function _typewriterRichReveal(element, html, plainText) { |
| |
| |
| element.innerHTML = ''; |
|
|
| const container = document.createElement('div'); |
| container.style.cssText = 'overflow:hidden; display:inline;'; |
| container.innerHTML = html; |
| element.appendChild(container); |
|
|
| |
| const textNodes = _getTextNodes(container); |
| let totalRevealed = 0; |
| const totalChars = plainText.length; |
|
|
| |
| textNodes.forEach(node => { |
| const span = document.createElement('span'); |
| span.style.visibility = 'hidden'; |
| span.textContent = node.textContent; |
| span.dataset.fullText = node.textContent; |
| span.dataset.revealed = '0'; |
| node.parentNode.replaceChild(span, node); |
| }); |
|
|
| const hiddenSpans = container.querySelectorAll('span[data-full-text]'); |
| let currentSpanIndex = 0; |
| let currentCharIndex = 0; |
|
|
| const cursor = document.createElement('span'); |
| cursor.className = 'typewriter-cursor'; |
| cursor.textContent = '|'; |
| element.appendChild(cursor); |
|
|
| function revealNext() { |
| if (currentSpanIndex >= hiddenSpans.length) { |
| element.dataset.typing = 'false'; |
| _typewriterActive = false; |
| setTimeout(() => { |
| if (cursor.parentNode) cursor.remove(); |
| }, 2000); |
| return; |
| } |
|
|
| const span = hiddenSpans[currentSpanIndex]; |
| const full = span.dataset.fullText; |
|
|
| currentCharIndex += 2; |
| if (currentCharIndex >= full.length) { |
| span.style.visibility = 'visible'; |
| span.textContent = full; |
| currentSpanIndex++; |
| currentCharIndex = 0; |
| } else { |
| span.style.visibility = 'visible'; |
| span.textContent = full.substring(0, currentCharIndex); |
| } |
|
|
| element.scrollTop = element.scrollHeight; |
| setTimeout(revealNext, TYPEWRITER_SPEED); |
| } |
|
|
| revealNext(); |
| } |
|
|
| function _getTextNodes(element) { |
| const nodes = []; |
| const walker = document.createTreeWalker( |
| element, |
| NodeFilter.SHOW_TEXT, |
| null, |
| false |
| ); |
| let node; |
| while ((node = walker.nextNode())) { |
| if (node.textContent.trim().length > 0) { |
| nodes.push(node); |
| } |
| } |
| return nodes; |
| } |
|
|
|
|
| |
| |
| |
|
|
| function setupMutationObserver() { |
| if (_observerSetup) return; |
|
|
| const config = { childList: true, subtree: true, characterData: true }; |
| let debounceTimer = null; |
|
|
| const observer = new MutationObserver((mutations) => { |
| |
| clearTimeout(debounceTimer); |
| debounceTimer = setTimeout(() => { |
| handleMutations(mutations); |
| }, DEBOUNCE_MS); |
| }); |
|
|
| |
| const appRoot = document.querySelector('gradio-app') || |
| document.querySelector('.gradio-container') || |
| document.body; |
|
|
| observer.observe(appRoot, config); |
| _observerSetup = true; |
| console.log('[Dokkaebi System] MutationObserver active.'); |
| } |
|
|
| function handleMutations(mutations) { |
| |
| const narrative = document.querySelector('.narrative-text'); |
| if (narrative) { |
| const currentText = narrative.textContent || ''; |
| if (currentText !== _lastNarrativeText && currentText.trim().length > 0) { |
| _lastNarrativeText = currentText; |
| applyTypewriter(narrative); |
| } |
| } |
|
|
| |
| const chat = document.querySelector('.constellation-chat'); |
| if (chat) { |
| const messages = chat.querySelectorAll('.chat-message'); |
| if (messages.length > _lastChatCount) { |
| |
| for (let i = _lastChatCount; i < messages.length; i++) { |
| if (!messages[i].classList.contains('slide-in-right')) { |
| messages[i].classList.add('slide-in-right'); |
| } |
| } |
| _lastChatCount = messages.length; |
| scrollStarStream(); |
| } |
| } |
|
|
| |
| const overlay = document.querySelector('.fullscreen-overlay'); |
| if (overlay && overlay.innerHTML.trim().length > 0) { |
| const isVisible = overlay.offsetParent !== null || |
| getComputedStyle(overlay).display !== 'none'; |
| if (isVisible) { |
| |
| if (overlay.querySelector('.scenario-announcement')) { |
| showScenarioAnnouncement(); |
| } |
| |
| else if (overlay.classList.contains('reader-noticed')) { |
| showReaderNoticed(); |
| } |
| |
| else { |
| showOverlay(OVERLAY_DISMISS_MS); |
| } |
| } |
| } |
|
|
| |
| const stateEl = document.getElementById('game-state-data'); |
| if (stateEl) { |
| |
| const stateJson = stateEl.dataset.state; |
| const triggersJson = stateEl.dataset.triggers; |
| if (stateJson && stateJson.trim().startsWith('{')) { |
| try { |
| handleStateEffects(stateJson, triggersJson); |
| } catch (e) { console.warn('[Dokkaebi] State parse error:', e); } |
| } |
| } |
|
|
| |
| if (document.body.classList.contains('shake')) { |
| |
| } |
| } |
|
|
|
|
| |
| |
| |
|
|
| function setupEventDelegation() { |
| |
| document.addEventListener('click', (e) => { |
| const btn = e.target.closest('.suggestion-btn'); |
| if (!btn) return; |
|
|
| e.preventDefault(); |
| e.stopPropagation(); |
|
|
| const actionText = btn.dataset.action || btn.textContent.trim(); |
| if (!actionText) return; |
|
|
| |
| const textarea = document.querySelector('.game-input textarea') || |
| document.querySelector('.game-input input[type="text"]'); |
|
|
| if (textarea) { |
| |
| const descriptor = Object.getOwnPropertyDescriptor( |
| window.HTMLTextAreaElement.prototype, 'value' |
| ) || Object.getOwnPropertyDescriptor( |
| window.HTMLInputElement.prototype, 'value' |
| ); |
|
|
| if (descriptor && descriptor.set) { |
| descriptor.set.call(textarea, actionText); |
| } else { |
| textarea.value = actionText; |
| } |
|
|
| |
| textarea.dispatchEvent(new Event('input', { bubbles: true })); |
| textarea.dispatchEvent(new Event('change', { bubbles: true })); |
|
|
| |
| btn.style.borderColor = '#904858'; |
| btn.style.boxShadow = '0 0 10px rgba(192,104,120,0.25)'; |
| btn.style.color = '#c06878'; |
|
|
| |
| setTimeout(() => { |
| const submitBtn = document.querySelector('.act-button') || |
| document.querySelector('button[type="submit"]'); |
| if (submitBtn) submitBtn.click(); |
|
|
| |
| setTimeout(() => { |
| btn.style.borderColor = ''; |
| btn.style.boxShadow = ''; |
| }, 200); |
| }, 100); |
| } |
| }); |
|
|
| |
| document.addEventListener('click', (e) => { |
| const overlay = e.target.closest('.fullscreen-overlay'); |
| if (overlay && !overlay.classList.contains('reader-noticed')) { |
| dismissOverlay(overlay); |
| } |
| }); |
|
|
| |
| document.addEventListener('animationend', (e) => { |
| if (e.animationName === 'shake') { |
| e.target.classList.remove('shake'); |
| } |
| if (e.animationName === 'damageFlash') { |
| e.target.remove(); |
| } |
| }); |
|
|
| |
| document.addEventListener('keydown', (e) => { |
| if (e.key === 'Enter' && !e.shiftKey) { |
| const textarea = document.querySelector('.game-input textarea'); |
| if (textarea && document.activeElement === textarea) { |
| const value = textarea.value.trim(); |
| if (value.length > 0) { |
| e.preventDefault(); |
| const submitBtn = document.querySelector('.act-button'); |
| if (submitBtn) submitBtn.click(); |
| } |
| } |
| } |
| }); |
|
|
| console.log('[Dokkaebi System] Event delegation active.'); |
| } |
|
|
|
|
| |
| |
| |
|
|
| function showOverlay(duration) { |
| duration = duration || OVERLAY_DISMISS_MS; |
| const overlay = document.querySelector('.fullscreen-overlay'); |
| if (!overlay) return; |
|
|
| |
| overlay.style.display = 'flex'; |
| overlay.classList.add('fade-in'); |
|
|
| |
| setTimeout(() => { |
| dismissOverlay(overlay); |
| }, duration); |
| } |
|
|
| function dismissOverlay(overlay) { |
| overlay = overlay || document.querySelector('.fullscreen-overlay'); |
| if (!overlay) return; |
|
|
| overlay.classList.remove('fade-in'); |
| overlay.classList.add('fade-out'); |
|
|
| setTimeout(() => { |
| overlay.style.setProperty('display', 'none', 'important'); |
| overlay.classList.remove('fade-out'); |
| |
| overlay.classList.remove('scenario-flash'); |
| }, 500); |
| } |
|
|
| function showScenarioAnnouncement() { |
| const overlay = document.querySelector('.fullscreen-overlay'); |
| if (!overlay) return; |
|
|
| |
| overlay.classList.add('scenario-flash'); |
| overlay.style.display = 'flex'; |
|
|
| |
| const announcement = overlay.querySelector('.scenario-announcement'); |
| if (announcement) { |
| announcement.classList.add('scale-up'); |
| } |
|
|
| |
| setTimeout(() => { |
| dismissOverlay(overlay); |
| if (announcement) { |
| announcement.classList.remove('scale-up'); |
| } |
| overlay.classList.remove('scenario-flash'); |
| }, SCENARIO_DISMISS_MS); |
| } |
|
|
| function showReaderNoticed() { |
| |
| document.body.classList.add('glitch-effect', 'static-noise'); |
|
|
| const overlay = document.querySelector('.fullscreen-overlay'); |
| if (overlay) { |
| overlay.style.display = 'flex'; |
| overlay.classList.add('fade-in'); |
| } |
|
|
| |
| triggerScreenShake(); |
|
|
| |
| const content = overlay ? overlay.querySelector('.reader-noticed') : null; |
| if (content) { |
| content.classList.add('glitch-text'); |
| } |
|
|
| console.log('[Dokkaebi System] ⚠ The Reader has noticed you.'); |
| } |
|
|
|
|
| |
| |
| |
|
|
| function scrollStarStream() { |
| const chat = document.querySelector('.constellation-chat'); |
| if (chat) { |
| |
| requestAnimationFrame(() => { |
| chat.scrollTo({ |
| top: chat.scrollHeight, |
| behavior: 'smooth' |
| }); |
| }); |
| } |
| } |
|
|
|
|
| |
| |
| |
|
|
| function spawnCoinRain(container, count) { |
| container = container || document.body; |
| count = count || COIN_RAIN_DEFAULT; |
|
|
| for (let i = 0; i < count; i++) { |
| const coin = document.createElement('div'); |
| coin.className = 'coin-particle'; |
|
|
| |
| const startX = Math.random() * 100; |
| const duration = 1 + Math.random() * 1.5; |
| const delay = Math.random() * 0.5; |
| const size = 4 + Math.random() * 6; |
|
|
| coin.style.cssText = ` |
| --start-x: ${startX}%; |
| --fall-duration: ${duration}s; |
| left: ${startX}%; |
| top: -10px; |
| width: ${size}px; |
| height: ${size}px; |
| animation-delay: ${delay}s; |
| position: absolute; |
| `; |
|
|
| container.style.position = container.style.position || 'relative'; |
| container.appendChild(coin); |
|
|
| |
| setTimeout(() => { |
| if (coin.parentNode) coin.remove(); |
| }, (duration + delay) * 1000 + 200); |
| } |
| } |
|
|
|
|
| |
| |
| |
|
|
| function triggerDamageFlash() { |
| |
| triggerScreenShake(); |
|
|
| |
| const flash = document.createElement('div'); |
| flash.className = 'damage-flash-overlay'; |
| document.body.appendChild(flash); |
|
|
| |
| setTimeout(() => { |
| if (flash.parentNode) flash.remove(); |
| }, 500); |
| } |
|
|
| function triggerScreenShake() { |
| document.body.classList.add('shake'); |
| setTimeout(() => { |
| document.body.classList.remove('shake'); |
| }, 500); |
| } |
|
|
| function triggerGlitchEffect() { |
| document.body.classList.add('glitch-effect', 'static-noise'); |
| |
| } |
|
|
| function clearGlitchEffect() { |
| document.body.classList.remove('glitch-effect', 'static-noise'); |
| } |
|
|
|
|
| |
| |
| |
|
|
| function handleStateEffects(stateJson, triggersJson) { |
| let state, triggers; |
| try { |
| state = typeof stateJson === 'string' ? JSON.parse(stateJson) : stateJson; |
| } catch (e) { |
| return; |
| } |
| try { |
| triggers = triggersJson && typeof triggersJson === 'string' ? JSON.parse(triggersJson) : (triggersJson || {}); |
| } catch (e) { |
| triggers = {}; |
| } |
|
|
| |
| if (typeof state.meta_exposure === 'number') { |
| document.body.classList.remove('meta-warning', 'meta-critical'); |
|
|
| if (state.meta_exposure >= 75) { |
| document.body.classList.add('meta-critical'); |
| |
| const metaBar = document.querySelector('.stat-bar-meta'); |
| if (metaBar) metaBar.classList.add('meter-danger'); |
| } else if (state.meta_exposure >= 50) { |
| document.body.classList.add('meta-warning'); |
| const metaBar = document.querySelector('.stat-bar-meta'); |
| if (metaBar) { |
| metaBar.classList.remove('meter-danger'); |
| metaBar.classList.add('meter-pulse'); |
| setTimeout(() => metaBar.classList.remove('meter-pulse'), 1000); |
| } |
| } else { |
| const metaBar = document.querySelector('.stat-bar-meta'); |
| if (metaBar) { |
| metaBar.classList.remove('meter-danger', 'meter-pulse'); |
| } |
| } |
|
|
| |
| if (_previousMeta !== null && state.meta_exposure > _previousMeta) { |
| const metaFill = document.querySelector('.stat-bar-meta .stat-bar-fill'); |
| if (metaFill) { |
| metaFill.classList.add('meter-pulse'); |
| setTimeout(() => metaFill.classList.remove('meter-pulse'), 1000); |
| } |
| } |
| _previousMeta = state.meta_exposure; |
| } |
|
|
| |
| if (state.current_phase) { |
| document.body.classList.remove('phase-combat', 'phase-safe-zone', 'phase-exploration'); |
| const phaseClass = 'phase-' + state.current_phase.toLowerCase().replace(/\s+/g, '-'); |
| document.body.classList.add(phaseClass); |
| } |
|
|
| |
| if (typeof state.hp === 'number') { |
| const tookDamage = triggers.damage === true || |
| (_previousHP !== null && state.hp < _previousHP); |
| if (tookDamage) { |
| triggerDamageFlash(); |
| const hpBar = document.querySelector('.stat-bar-hp'); |
| if (hpBar) { |
| const pct = state.hp / (state.max_hp || 100); |
| if (pct > 0.6) hpBar.dataset.percent = 'high'; |
| else if (pct > 0.3) hpBar.dataset.percent = 'mid'; |
| else hpBar.dataset.percent = 'low'; |
| } |
| } |
| _previousHP = state.hp; |
| } |
|
|
| |
| if (triggers.coins_gained === true) { |
| const playerPanel = document.querySelector('.player-panel'); |
| spawnCoinRain(playerPanel || document.body, 12); |
| } |
|
|
| |
| if (state.show_rank) { |
| const rankDisplay = document.querySelector('.ranking-display'); |
| if (rankDisplay) { |
| const rank = state.show_rank.toLowerCase(); |
| rankDisplay.classList.add(`rank-${rank}`); |
| } |
| } |
|
|
| |
| if (state.reader_noticed) { |
| triggerGlitchEffect(); |
| } |
| } |
|
|
|
|
| |
| |
| |
|
|
| function animateStatBars() { |
| const fills = document.querySelectorAll('.stat-bar-fill'); |
| fills.forEach(fill => { |
| const targetWidth = fill.style.width; |
| if (targetWidth && !fill.dataset.animated) { |
| |
| fill.style.width = '0%'; |
| fill.offsetHeight; |
| fill.style.width = targetWidth; |
| fill.dataset.animated = 'true'; |
| } |
| }); |
| } |
|
|
|
|
| |
| |
| |
|
|
| function setupStartScreen() { |
| const title = document.querySelector('.game-title'); |
| if (title && !title.classList.contains('floating')) { |
| title.classList.add('floating'); |
| } |
|
|
| const startBtn = document.querySelector('.start-btn'); |
| if (startBtn && !startBtn.classList.contains('glow-pulse')) { |
| startBtn.classList.add('glow-pulse'); |
| } |
| } |
|
|
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| function formatORVText(text) { |
| if (!text) return text; |
|
|
| |
| text = text.replace(/\[([^\]]+)\]/g, (match, inner) => { |
| |
| const formatted = inner.replace(/'([^']+)'/g, |
| '<span class="skill-name">\'$1\'</span>' |
| ); |
| return `<span class="bracket-text">[${formatted}]</span>`; |
| }); |
|
|
| |
| text = text.replace(/(\d+)\s*coins?/gi, |
| '<span class="coins-display">$1</span> coins' |
| ); |
|
|
| return text; |
| } |
|
|
|
|
| |
| |
| |
|
|
| function spawnAmbientParticles() { |
| const container = document.querySelector('.game-area'); |
| if (!container) return; |
|
|
| |
| for (let i = 0; i < 5; i++) { |
| const particle = document.createElement('div'); |
| const x = Math.random() * 100; |
| const y = Math.random() * 100; |
| const size = 1 + Math.random() * 2; |
| const duration = 8 + Math.random() * 12; |
| const delay = Math.random() * 5; |
|
|
| particle.style.cssText = ` |
| position: absolute; |
| left: ${x}%; |
| top: ${y}%; |
| width: ${size}px; |
| height: ${size}px; |
| background: rgba(201, 168, 76, 0.12); |
| border-radius: 50%; |
| pointer-events: none; |
| z-index: 0; |
| animation: float ${duration}s ease-in-out ${delay}s infinite; |
| opacity: 0.3; |
| `; |
|
|
| container.style.position = container.style.position || 'relative'; |
| container.appendChild(particle); |
| } |
| } |
|
|
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| function clickRipple(e) { |
| const ripple = document.createElement('div'); |
| ripple.style.cssText = ` |
| position: fixed; |
| left: ${e.clientX - 15}px; |
| top: ${e.clientY - 15}px; |
| width: 30px; |
| height: 30px; |
| border: 1px solid rgba(74, 158, 255, 0.4); |
| border-radius: 50%; |
| pointer-events: none; |
| z-index: 99999; |
| animation: scaleUp 0.4s ease-out forwards; |
| opacity: 0.6; |
| `; |
| document.body.appendChild(ripple); |
| setTimeout(() => { |
| if (ripple.parentNode) ripple.remove(); |
| }, 400); |
| } |
|
|
|
|
| |
| |
| |
|
|
| function initialize() { |
| console.log('[Dokkaebi System] Initializing Scenario Simulator...'); |
|
|
| setupMutationObserver(); |
| setupEventDelegation(); |
| setupStartScreen(); |
|
|
| |
| setTimeout(animateStatBars, 500); |
|
|
| |
| setTimeout(spawnAmbientParticles, 1000); |
|
|
| console.log('[Dokkaebi System] Scenario Simulator initialized.'); |
| console.log('[Dokkaebi System] ──────────────────────────────────'); |
| console.log('[Dokkaebi System] "The scenario has begun."'); |
| console.log('[Dokkaebi System] ──────────────────────────────────'); |
| } |
|
|
| |
| if (document.readyState === 'loading') { |
| document.addEventListener('DOMContentLoaded', initialize); |
| } else { |
| |
| initialize(); |
| } |
|
|
| |
| |
| setTimeout(() => { |
| if (!_observerSetup) { |
| console.log('[Dokkaebi System] Retry initialization (Gradio late load)...'); |
| initialize(); |
| } |
| |
| setupStartScreen(); |
| |
| animateStatBars(); |
| |
| scrollStarStream(); |
| }, INIT_RETRY_DELAY); |
|
|
| |
| setTimeout(() => { |
| setupStartScreen(); |
| animateStatBars(); |
| scrollStarStream(); |
| }, 4000); |
|
|
|
|
| |
| |
| |
|
|
| |
| window.ORVGame = { |
| typewriter: applyTypewriter, |
| damageFlash: triggerDamageFlash, |
| screenShake: triggerScreenShake, |
| glitch: triggerGlitchEffect, |
| clearGlitch: clearGlitchEffect, |
| coinRain: spawnCoinRain, |
| scrollChat: scrollStarStream, |
| showOverlay: showOverlay, |
| dismissOverlay: dismissOverlay, |
| showScenario: showScenarioAnnouncement, |
| showReaderNoticed: showReaderNoticed, |
| handleState: handleStateEffects, |
| formatText: formatORVText, |
| animateBars: animateStatBars, |
| }; |
|
|
| })(); |
|
|