Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>🏠 한국어 가족집 모험 🇰🇷</title> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;700&family=Orbitron:wght@400;700&display=swap'); | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Courier New', monospace; | |
| background: url('assets/images/bukchon-hanok-thumb-scaled.jpg') center/cover fixed; | |
| background-color: #f5e9d9; | |
| color: #3a3226; | |
| min-height: 100vh; | |
| overflow-x: hidden; | |
| position: relative; | |
| } | |
| body::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: url('assets/images/repeat-background.jpg') repeat; | |
| opacity: 0.15; | |
| pointer-events: none; | |
| z-index: -1; | |
| } | |
| /* Animated background elements */ | |
| .bg-particles { | |
| position: fixed; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; | |
| z-index: -1; | |
| overflow: hidden; | |
| } | |
| .particle { | |
| position: absolute; | |
| width: 2px; | |
| height: 2px; | |
| background: rgba(255, 255, 255, 0.3); | |
| animation: float 6s ease-in-out infinite; | |
| } | |
| .flower-petal { | |
| position: absolute; | |
| width: 15px; | |
| height: 15px; | |
| background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="%23ff9ff3" d="M12,2C13.1,2 14,2.9 14,4C14,5.1 13.1,6 12,6C10.9,6 10,5.1 10,4C10,2.9 10.9,2 12,2M15.5,8C16.3,8 17,8.7 17,9.5C17,10.3 16.3,11 15.5,11C14.7,11 14,10.3 14,9.5C14,8.7 14.7,8 15.5,8M8.5,8C9.3,8 10,8.7 10,9.5C10,10.3 9.3,11 8.5,11C7.7,11 7,10.3 7,9.5C7,8.7 7.7,8 8.5,8M12,11C13.1,11 14,11.9 14,13C14,14.1 13.1,15 12,15C10.9,15 10,14.1 10,13C10,11.9 10.9,11 12,11M19,17V19H5V17C5,14.8 8.1,13 12,13C15.9,13 19,14.8 19,17Z" /></svg>'); | |
| background-size: contain; | |
| opacity: 0.7; | |
| animation: petalFall linear infinite; | |
| } | |
| @keyframes float { | |
| 0%, 100% { transform: translateY(0px) rotate(0deg); opacity: 0.3; } | |
| 50% { transform: translateY(-20px) rotate(180deg); opacity: 0.8; } | |
| } | |
| @keyframes petalFall { | |
| 0% { | |
| transform: translate(0, -10vh) rotate(0deg); | |
| opacity: 0; | |
| } | |
| 10% { | |
| opacity: 0.7; | |
| } | |
| 90% { | |
| opacity: 0.7; | |
| } | |
| 100% { | |
| transform: translate(var(--random-x), 100vh) rotate(360deg); | |
| opacity: 0; | |
| } | |
| } | |
| /* Header */ | |
| .game-header { | |
| background: linear-gradient(90deg, #8b6b4a, #6b4f32, #8b6b4a); | |
| padding: 10px 0; | |
| text-align: center; | |
| border-bottom: 3px solid #3a3226; | |
| } | |
| .header-content h1 { | |
| font-family: 'Gungsuh', 'Batang', serif; | |
| font-size: 2.5em; | |
| font-weight: 700; | |
| color: #3a3226; | |
| margin-bottom: 5px; | |
| text-shadow: 2px 2px 4px rgba(0,0,0,0.1); | |
| letter-spacing: 1px; | |
| } | |
| .progress-stats { | |
| display: flex; | |
| gap: 15px; | |
| justify-content: center; | |
| margin-top: 10px; | |
| flex-wrap: wrap; | |
| } | |
| .progress-indicator { | |
| background: rgba(107, 79, 50, 0.7); | |
| border: 2px solid #3a3226; | |
| border-radius: 20px; | |
| padding: 5px 15px; | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 10px; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| } | |
| .progress-indicator:hover { | |
| background: rgba(107, 79, 50, 0.9); | |
| box-shadow: 0 0 10px rgba(107, 79, 50, 0.5); | |
| } | |
| .progress-count { | |
| font-family: 'Courier New', monospace; | |
| font-weight: bold; | |
| color: #f5e9d9; | |
| font-size: 1.2em; | |
| } | |
| .progress-label { | |
| font-size: 0.9em; | |
| color: #f5e9d9; | |
| } | |
| .header-content p { | |
| color: rgba(255,255,255,0.9); | |
| font-size: 1.1em; | |
| } | |
| /* Main game container */ | |
| .game-container { | |
| display: grid; | |
| grid-template-columns: 1fr 300px; | |
| gap: 20px; | |
| padding: 20px; | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| min-height: calc(100vh - 120px); | |
| } | |
| /* Chat area */ | |
| .chat-section { | |
| background: rgba(245, 233, 217, 0.85); | |
| border: 2px solid #8b6b4a; | |
| border-radius: 0; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.2), | |
| inset 0 0 30px rgba(139, 107, 74, 0.3); | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| max-width: 800px; | |
| margin: 0 auto; | |
| position: relative; | |
| } | |
| .chat-section::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: url('assets/images/repeat-background.jpg') repeat; | |
| opacity: 0.05; | |
| pointer-events: none; | |
| z-index: 0; | |
| } | |
| .chat-header { | |
| background: linear-gradient(90deg, #8b6b4a, #6b4f32, #8b6b4a); | |
| padding: 15px 20px; | |
| color: #f5e9d9; | |
| font-weight: 600; | |
| border-bottom: 2px solid #3a3226; | |
| } | |
| .current-npc-info { | |
| display: flex; | |
| align-items: center; | |
| gap: 15px; | |
| } | |
| .npc-avatar { | |
| width: 50px; | |
| height: 50px; | |
| border-radius: 50%; | |
| border: 3px solid white; | |
| object-fit: cover; | |
| } | |
| .npc-details h3 { | |
| font-size: 1.2em; | |
| margin-bottom: 2px; | |
| } | |
| .npc-details p { | |
| font-size: 0.9em; | |
| opacity: 0.9; | |
| } | |
| #game-messages { | |
| flex: 1; | |
| padding: 20px; | |
| overflow-y: auto; | |
| max-height: 400px; | |
| scrollbar-width: thin; | |
| scrollbar-color: #4a5568 #2d3748; | |
| } | |
| #game-messages::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| #game-messages::-webkit-scrollbar-track { | |
| background: #2d3748; | |
| border-radius: 4px; | |
| } | |
| #game-messages::-webkit-scrollbar-thumb { | |
| background: #4a5568; | |
| border-radius: 4px; | |
| } | |
| /* Message styling */ | |
| .game-text { | |
| margin-bottom: 12px; | |
| padding: 8px 12px; | |
| line-height: 1.5; | |
| border-left: 3px solid transparent; | |
| opacity: 0; | |
| animation: fadeIn 0.3s forwards; | |
| } | |
| @keyframes fadeIn { | |
| to { opacity: 1; } | |
| } | |
| .npc-dialogue { | |
| background: rgba(255,255,255,0.7); | |
| color: #3a3226; | |
| border-left: 3px solid #8b6b4a; | |
| padding: 12px 16px; | |
| margin: 10px 0; | |
| position: relative; | |
| transform: translateX(-20px); | |
| animation: slideIn 0.5s forwards; | |
| } | |
| @keyframes slideIn { | |
| to { transform: translateX(0); } | |
| } | |
| .npc-name { | |
| color: #a52a2a; | |
| font-weight: bold; | |
| } | |
| .command-echo { | |
| background: rgba(255,255,255,0.3); | |
| color: #3a3226; | |
| font-style: italic; | |
| border-radius: 15px 15px 5px 15px; | |
| padding: 8px 12px; | |
| text-align: right; | |
| font-weight: 600; | |
| } | |
| .help-content { | |
| background: rgba(245, 233, 217, 0.9); | |
| border: 2px solid #8b6b4a; | |
| border-radius: 0; | |
| padding: 15px; | |
| white-space: pre-line; | |
| font-family: 'Courier New', monospace; | |
| font-size: 0.9em; | |
| color: #3a3226; | |
| margin: 10px 0; | |
| box-shadow: inset 0 0 10px rgba(139, 107, 74, 0.3); | |
| } | |
| .korean-text { | |
| font-size: 1.1em; | |
| line-height: 1.6; | |
| } | |
| .vocab-highlight { | |
| background: linear-gradient(45deg, #ff6b6b, #feca57); | |
| color: white; | |
| padding: 2px 6px; | |
| border-radius: 4px; | |
| font-weight: 600; | |
| } | |
| /* Chat input */ | |
| .chat-input { | |
| background: rgba(45, 55, 72, 0.9); | |
| padding: 15px 20px; | |
| border-top: 1px solid rgba(255,255,255,0.1); | |
| } | |
| .input-group { | |
| display: flex; | |
| gap: 10px; | |
| } | |
| .chat-input input { | |
| flex: 1; | |
| background: rgba(255,255,255,0.7); | |
| border: 2px solid #8b6b4a; | |
| border-radius: 0; | |
| padding: 12px 20px; | |
| color: #3a3226; | |
| font-family: 'Courier New', monospace; | |
| font-size: 1em; | |
| transition: all 0.3s ease; | |
| box-shadow: inset 0 0 10px rgba(139, 107, 74, 0.3); | |
| } | |
| .chat-input input:focus { | |
| outline: none; | |
| border-color: #3a3226; | |
| box-shadow: inset 0 0 15px rgba(139, 107, 74, 0.5); | |
| animation: caretBlink 1s step-end infinite; | |
| } | |
| @keyframes caretBlink { | |
| 50% { border-right: 2px solid #3a3226; } | |
| } | |
| .chat-input input::placeholder { | |
| color: rgba(255,255,255,0.6); | |
| } | |
| .send-button { | |
| background: linear-gradient(45deg, #8b6b4a, #6b4f32); | |
| border: 2px outset #3a3226; | |
| border-radius: 0; | |
| padding: 12px 24px; | |
| color: #f5e9d9; | |
| font-family: 'Courier New', monospace; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| box-shadow: 3px 3px 0 rgba(0,0,0,0.2); | |
| text-shadow: none; | |
| } | |
| .send-button:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6); | |
| } | |
| .send-button:disabled { | |
| opacity: 0.6; | |
| cursor: not-allowed; | |
| transform: none; | |
| } | |
| /* Sidebar */ | |
| .game-sidebar { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 20px; | |
| } | |
| .panel { | |
| background: rgba(15, 20, 25, 0.8); | |
| backdrop-filter: blur(10px); | |
| border-radius: 15px; | |
| border: 1px solid rgba(255,255,255,0.1); | |
| overflow: hidden; | |
| box-shadow: 0 8px 32px rgba(0,0,0,0.3); | |
| } | |
| .panel-header { | |
| background: linear-gradient(90deg, #8b6b4a, #6b4f32, #8b6b4a); | |
| padding: 12px 16px; | |
| font-weight: 600; | |
| color: #f5e9d9; | |
| border-bottom: 2px solid #3a3226; | |
| } | |
| .panel-content { | |
| padding: 16px; | |
| } | |
| /* House map */ | |
| .house-map { | |
| font-family: 'Courier New', monospace; | |
| font-size: 0.8em; | |
| line-height: 1.3; | |
| text-align: center; | |
| white-space: pre-line; | |
| color: #a0aec0; | |
| } | |
| .current-room-highlight { | |
| background: linear-gradient(45deg, #4ecdc4, #44a08d); | |
| color: white; | |
| padding: 2px 6px; | |
| border-radius: 4px; | |
| font-weight: bold; | |
| } | |
| /* Progress stats */ | |
| .stats { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 10px; | |
| margin-bottom: 15px; | |
| } | |
| .stat-item { | |
| background: rgba(255,255,255,0.05); | |
| padding: 12px; | |
| border-radius: 8px; | |
| text-align: center; | |
| border: 1px solid rgba(255,255,255,0.1); | |
| } | |
| .stat-value { | |
| font-size: 1.4em; | |
| font-weight: 700; | |
| color: #4ecdc4; | |
| display: block; | |
| } | |
| .stat-label { | |
| font-size: 0.8em; | |
| color: #a0aec0; | |
| margin-top: 2px; | |
| } | |
| /* Quick actions */ | |
| .quick-actions-container { | |
| display: flex; | |
| justify-content: center; | |
| gap: 10px; | |
| padding: 10px; | |
| background: rgba(107, 79, 50, 0.7); | |
| border-top: 2px solid #3a3226; | |
| border-bottom: 2px solid #3a3226; | |
| } | |
| .quick-actions-container .quick-btn { | |
| flex: 1; | |
| max-width: 120px; | |
| padding: 8px 5px; | |
| font-size: 0.9em; | |
| font-family: 'Courier New', monospace; | |
| border-radius: 0; | |
| background: rgba(245, 233, 217, 0.9); | |
| color: #3a3226; | |
| border: 2px outset #8b6b4a; | |
| font-weight: bold; | |
| text-shadow: none; | |
| box-shadow: none; | |
| } | |
| .quick-actions-container .quick-btn:hover { | |
| background: rgba(245, 233, 217, 0.95); | |
| animation: neonFlicker 0.5s infinite alternate; | |
| } | |
| .quick-btn { | |
| background: #6b4f32; | |
| border: 2px outset #8b6b4a; | |
| border-radius: 0; | |
| padding: 8px 12px; | |
| color: #8e6f46; | |
| font-family: 'Courier New', monospace; | |
| font-size: 0.85em; | |
| cursor: pointer; | |
| position: relative; | |
| text-shadow: 1px 1px 0 #3a3226; | |
| box-shadow: 3px 3px 0 rgba(0,0,0,0.2); | |
| } | |
| .quick-btn:hover { | |
| background: #8b6b4a; | |
| box-shadow: 0 0 10px rgba(107, 79, 50, 0.7); | |
| animation: neonFlicker 0.5s infinite alternate; | |
| } | |
| @keyframes neonFlicker { | |
| 0%, 19%, 21%, 23%, 25%, 54%, 56%, 100% { | |
| box-shadow: 0 0 10px rgba(107, 79, 50, 0.7); | |
| } | |
| 20%, 24%, 55% { | |
| box-shadow: 0 0 15px rgba(107, 79, 50, 0.9); | |
| } | |
| } | |
| /* Room navigation */ | |
| .room-nav { | |
| display: grid; | |
| grid-template-columns: 1fr; | |
| gap: 6px; | |
| } | |
| .room-btn { | |
| background: rgba(245, 233, 217, 0.2); | |
| border: 2px solid #8b6b4a; | |
| border-radius: 0; | |
| padding: 8px 12px; | |
| color: #ecebea; | |
| font-size: 0.85em; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| text-align: left; | |
| margin-bottom: 5px; | |
| } | |
| .room-btn:hover { | |
| background: rgba(139, 107, 74, 0.3); | |
| border-color: #3a3226; | |
| color: #3a3226; | |
| } | |
| .room-btn.current { | |
| background: linear-gradient(90deg, #8b6b4a, #6b4f32); | |
| border-color: #3a3226; | |
| color: #f5e9d9; | |
| font-weight: 600; | |
| } | |
| /* Loading indicator */ | |
| .loading { | |
| color: #8b6b4a; | |
| animation: pulse 1.5s ease-in-out infinite; | |
| font-weight: 600; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.5; } | |
| } | |
| /* Success/Error messages */ | |
| .success { color: #6b4f32; font-weight: 600; } | |
| .error { color: #a52a2a; font-weight: 600; background: rgba(165, 42, 42, 0.1); padding: 8px 12px; border-radius: 5px; } | |
| /* Responsive design */ | |
| @media (max-width: 768px) { | |
| .game-container { | |
| grid-template-columns: 1fr; | |
| padding: 10px; | |
| } | |
| .header-content h1 { | |
| font-size: 1.8em; | |
| } | |
| .quick-actions { | |
| grid-template-columns: 1fr; | |
| } | |
| .stats { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="bg-particles" id="particles"></div> | |
| <header class="game-header"> | |
| <div class="header-content"> | |
| <h1>🏠 한국어 가족집 모험</h1> | |
| <p>Korean Family House Adventure - Learn Korean through immersive conversations!</p> | |
| <div class="progress-stats"> | |
| <div class="progress-indicator" onclick="showVocabulary()"> | |
| <span class="progress-count" id="word-count">0</span> | |
| <span class="progress-label">Korean Words Learned</span> | |
| </div> | |
| <div class="progress-indicator" onclick="showInventory()"> | |
| <span class="progress-count" id="inventory-count">0</span> | |
| <span class="progress-label">Objects Examined</span> | |
| </div> | |
| </div> | |
| </div> | |
| </header> | |
| <div class="game-container"> | |
| <div class="chat-section"> | |
| <div class="chat-header"> | |
| <div class="current-npc-info"> | |
| <img src="assets/images/Family portraits 2ChatGPT Image Aug 17, 2025, 10_48_14 PM.png" alt="NPC Avatar" class="npc-avatar" id="npc-avatar"> | |
| <div class="npc-details"> | |
| <h3 id="current-npc-name">Loading...</h3> | |
| <p id="current-room-name">Getting ready...</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="game-messages"> | |
| <div class="game-text loading">🎮 Starting your Korean adventure...</div> | |
| </div> | |
| <!-- Quick Actions moved here under chat --> | |
| <div class="quick-actions-container"> | |
| <button class="quick-btn" onclick="sendQuickCommand('look')">👀 Look</button> | |
| <button class="quick-btn" onclick="sendQuickCommand('help')">❓ Help</button> | |
| <button class="quick-btn" onclick="showMapDialog()">🗺️ Map</button> | |
| <button class="quick-btn" onclick="showExamineDialog()">🔍 Examine</button> | |
| <button class="quick-btn" onclick="showChatDialog()">💬 Chat</button> | |
| </div> | |
| <div class="chat-input"> | |
| <div class="input-group"> | |
| <input type="text" id="command-input" placeholder="Type commands: help, look, examine [object], go [room], chat [message]..." autofocus> | |
| <button class="send-button" id="submit-button">Send 💬</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="game-sidebar"> | |
| <!-- Quick Actions removed from sidebar - now under chat --> | |
| <!-- House map removed from sidebar --> | |
| <div class="panel"> | |
| <div class="panel-header"> | |
| 🚀 Quick Navigate | |
| </div> | |
| <div class="panel-content"> | |
| <div class="room-nav"> | |
| <button class="room-btn current" onclick="sendQuickCommand('go hall')">🏠 Hall (Central Hub)</button> | |
| <button class="room-btn" onclick="sendQuickCommand('go kitchen')">🍳 Kitchen (Grandma)</button> | |
| <button class="room-btn" onclick="sendQuickCommand('go garden')">🌸 Garden (Grandpa)</button> | |
| <button class="room-btn" onclick="sendQuickCommand('go bedroom')">🎵 Bedroom (Sister)</button> | |
| <button class="room-btn" onclick="sendQuickCommand('go study')">📚 Study (Brother)</button> | |
| <button class="room-btn" onclick="sendQuickCommand('go classroom')">✏️ Classroom (Teacher)</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Create animated background elements | |
| function createParticles() { | |
| const container = document.getElementById('particles'); | |
| // Create regular particles | |
| for (let i = 0; i < 30; i++) { | |
| const particle = document.createElement('div'); | |
| particle.className = 'particle'; | |
| particle.style.left = Math.random() * 100 + '%'; | |
| particle.style.top = Math.random() * 100 + '%'; | |
| particle.style.animationDelay = Math.random() * 6 + 's'; | |
| container.appendChild(particle); | |
| } | |
| // Create flower petals | |
| for (let i = 0; i < 15; i++) { | |
| const petal = document.createElement('div'); | |
| petal.className = 'flower-petal'; | |
| petal.style.left = Math.random() * 100 + '%'; | |
| petal.style.setProperty('--random-x', (Math.random() * 100 - 50) + 'px'); | |
| petal.style.animationDuration = (8 + Math.random() * 10) + 's'; | |
| petal.style.animationDelay = Math.random() * 5 + 's'; | |
| petal.style.width = (10 + Math.random() * 10) + 'px'; | |
| petal.style.height = petal.style.width; | |
| container.appendChild(petal); | |
| } | |
| } | |
| // API base URL | |
| const API_BASE = ''; | |
| // DOM elements | |
| const gameMessages = document.getElementById('game-messages'); | |
| const commandInput = document.getElementById('command-input'); | |
| const submitButton = document.getElementById('submit-button'); | |
| const currentNpcName = document.getElementById('current-npc-name'); | |
| const currentRoomName = document.getElementById('current-room-name'); | |
| const wordCountSpan = document.getElementById('word-count'); | |
| const npcAvatar = document.getElementById('npc-avatar'); | |
| // Check if required elements exist | |
| if (!gameMessages || !commandInput || !submitButton || !wordCountSpan) { | |
| console.error('Required DOM elements not found!'); | |
| } | |
| // Game state | |
| let gameState = { | |
| current_room: '', | |
| discovered_words: [], | |
| isLoading: false, | |
| conversationCount: 0 | |
| }; | |
| // Room and NPC mapping | |
| const roomNames = { | |
| 'hall': 'Hall (대청)', | |
| 'kitchen': 'Kitchen (부엌)', | |
| 'garden': 'Garden (정원)', | |
| 'bedroom': 'Bedroom (침실)', | |
| 'study': 'Study (서재)', | |
| 'classroom': 'Classroom (교실)' | |
| }; | |
| const npcNames = { | |
| 'ahjumma_gpt': 'Grandma Kim Soon-ja (김순자 할머니)', | |
| 'ahjussi_gpt': 'Grandpa Park Chul-min (박철민 할아버지)', | |
| 'unni_gpt': 'Sister Lee Min-ji (이민지 언니)', | |
| 'oppa_gpt': 'Brother Jung Jae-hyun (정재현 오빠)', | |
| 'seonsaengnim_gpt': 'Teacher Choi Soo-jin (최수진 선생님)' | |
| }; | |
| const npcImages = { | |
| 'ahjumma_gpt': 'assets/images/Ahjumma ChatGPT Image Mar 26, 2025, 05_36_15 PM.png', | |
| 'ahjussi_gpt': 'assets/images/Ahjussi ChatGPT Image May 5, 2025, 04_37_07 PM.png', | |
| 'unni_gpt': 'assets/images/K-pop trainee ChatGPT Image May 5, 2025, 04_35_54 PM.png', | |
| 'oppa_gpt': 'assets/images/SunbaeOppa ChatGPT Image May 5, 2025, 04_07_13 PM.png', | |
| 'seonsaengnim_gpt': 'assets/images/SeonsaengnimChatGPT Image Aug 17, 2025, 10_41_39 PM.png' | |
| }; | |
| // Initialize game | |
| async function initGame() { | |
| try { | |
| createParticles(); | |
| addToGame('🎮 Welcome to the Korean Family House Adventure!', 'success korean-text'); | |
| addToGame('🏠 You\'re about to meet a wonderful Korean family who will teach you their language and culture.', 'game-text'); | |
| addToGame(''); | |
| await loadGameState(); | |
| await loadRoomInfo(); | |
| } catch (error) { | |
| addToGame(`❌ Failed to start game: ${error.message}`, 'error'); | |
| console.error('Game initialization failed:', error); | |
| } | |
| } | |
| // Load current game state | |
| async function loadGameState() { | |
| try { | |
| const response = await fetch(`${API_BASE}/api/game/state`); | |
| if (response.ok) { | |
| gameState = await response.json(); | |
| updateUI(); | |
| } | |
| } catch (error) { | |
| console.error('Failed to load game state:', error); | |
| } | |
| } | |
| // Load room information | |
| async function loadRoomInfo() { | |
| try { | |
| const response = await fetch(`${API_BASE}/api/game/room-info`); | |
| const data = await response.json(); | |
| if (data.success) { | |
| addToGame(data.message, 'game-text korean-text'); | |
| if (data.data.npc_info && data.data.npc_info.name) { | |
| updateNpcDisplay(data.data); | |
| } | |
| updateGameState(data.data); | |
| } else { | |
| addToGame(`❌ ${data.message}`, 'error'); | |
| } | |
| } catch (error) { | |
| addToGame(`❌ Failed to load room info: ${error.message}`, 'error'); | |
| } | |
| } | |
| // Update NPC display | |
| function updateNpcDisplay(data) { | |
| try { | |
| if (data.current_room && data.room_mapping) { | |
| const agentName = data.room_mapping[data.current_room]; | |
| if (agentName && npcNames[agentName]) { | |
| if (currentNpcName) { | |
| currentNpcName.textContent = npcNames[agentName]; | |
| } | |
| if (currentRoomName) { | |
| currentRoomName.textContent = roomNames[data.current_room] || data.current_room; | |
| } | |
| // Update avatar | |
| if (npcAvatar && npcImages[agentName]) { | |
| npcAvatar.src = npcImages[agentName]; | |
| npcAvatar.alt = npcNames[agentName]; | |
| } | |
| } | |
| } | |
| } catch (error) { | |
| console.error('Error updating NPC display:', error); | |
| } | |
| } | |
| // Update UI with game state | |
| function updateUI() { | |
| // Update word count in header | |
| if (wordCountSpan) { | |
| wordCountSpan.textContent = gameState.discovered_words.length; | |
| } | |
| // Update inventory count in header | |
| const inventoryCountSpan = document.getElementById('inventory-count'); | |
| if (inventoryCountSpan) { | |
| inventoryCountSpan.textContent = (gameState.player_inventory || []).length; | |
| } | |
| // Note: conversation count was removed from design, so skipping that update | |
| // Update room buttons | |
| document.querySelectorAll('.room-btn').forEach(btn => { | |
| btn.classList.remove('current'); | |
| }); | |
| // Highlight current room | |
| const currentRoomBtn = document.querySelector(`[onclick*="${gameState.current_room}"]`); | |
| if (currentRoomBtn) { | |
| currentRoomBtn.classList.add('current'); | |
| } | |
| } | |
| // Update game state | |
| function updateGameState(data) { | |
| if (data.current_room) { | |
| gameState.current_room = data.current_room; | |
| } | |
| if (data.discovered_words) { | |
| gameState.discovered_words = data.discovered_words; | |
| } | |
| if (data.player_inventory) { | |
| gameState.player_inventory = data.player_inventory; | |
| } | |
| updateUI(); | |
| } | |
| // Add text to game messages with typing animation | |
| function addToGame(text, className = 'game-text') { | |
| try { | |
| if (!gameMessages) { | |
| console.error('gameMessages element not found'); | |
| return; | |
| } | |
| // Convert newlines to HTML breaks for proper formatting | |
| const formattedText = text.replace(/\n/g, '<br>'); | |
| const div = document.createElement('div'); | |
| div.className = className; | |
| gameMessages.appendChild(div); | |
| let i = 0; | |
| const typing = setInterval(() => { | |
| if (i < formattedText.length) { | |
| div.innerHTML = formattedText.substring(0, i+1); | |
| i++; | |
| if (gameMessages) { | |
| gameMessages.scrollTop = gameMessages.scrollHeight; | |
| } | |
| } else { | |
| clearInterval(typing); | |
| } | |
| }, 20); | |
| } catch (error) { | |
| console.error('Error adding text to game:', error); | |
| } | |
| } | |
| // Send command to API | |
| async function sendCommand(command) { | |
| if (gameState.isLoading) return; | |
| gameState.isLoading = true; | |
| submitButton.disabled = true; | |
| // Echo command | |
| addToGame(`> ${command}`, 'command-echo'); | |
| try { | |
| let response; | |
| let data; | |
| if (command.startsWith('go ') || command.startsWith('move ') || command.startsWith('이동 ')) { | |
| // Movement command | |
| response = await fetch(`${API_BASE}/api/game/move`, { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({command: command}) | |
| }); | |
| data = await response.json(); | |
| } else if (command.startsWith('examine ')) { | |
| // Examine object command | |
| response = await fetch(`${API_BASE}/api/game/examine`, { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({command: command}) | |
| }); | |
| data = await response.json(); | |
| } else if (command === 'look' || command === 'l') { | |
| // Look around command | |
| response = await fetch(`${API_BASE}/api/game/command`, { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({command: command}) | |
| }); | |
| data = await response.json(); | |
| } else if (command.startsWith('take ')) { | |
| // Take object command | |
| response = await fetch(`${API_BASE}/api/game/take`, { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({command: command}) | |
| }); | |
| data = await response.json(); | |
| } else if (command.startsWith('chat ') || command.startsWith('talk ') || command.startsWith('say ')) { | |
| // Chat with NPC command | |
| const chatMessage = command.replace(/^(chat|talk|say)\s+/, ''); | |
| response = await fetch(`${API_BASE}/api/game/talk`, { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({message: chatMessage}) | |
| }); | |
| data = await response.json(); | |
| } else if (command === 'help' || command === '?' || command === 'h' || command === 'rooms' || command === 'map') { | |
| // Help and info commands | |
| response = await fetch(`${API_BASE}/api/game/command`, { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({command: command}) | |
| }); | |
| data = await response.json(); | |
| } else { | |
| // Invalid command - show help | |
| addToGame('❌ Invalid command! Use one of these:', 'error'); | |
| addToGame('• look - Look around current room', 'help-content'); | |
| addToGame('• examine [object] - Examine an object', 'help-content'); | |
| addToGame('• go [room] - Move to another room', 'help-content'); | |
| addToGame('• chat [message] - Chat with family member in room', 'help-content'); | |
| addToGame('• help - Show full help menu', 'help-content'); | |
| return; | |
| } | |
| // Display response | |
| if (data.success) { | |
| if (command === 'help' || command === '?' || command === 'h' || command === 'rooms' || command === 'map') { | |
| addToGame(data.message, 'help-content'); | |
| } else if (command === 'look' || command === 'l') { | |
| // Format look command nicely | |
| addToGame('👀 Looking around...', 'game-text'); | |
| addToGame(data.message, 'npc-dialogue korean-text'); | |
| } else if (command.startsWith('examine ')) { | |
| // Format examine command nicely | |
| addToGame('🔍 Examining...', 'game-text'); | |
| addToGame(data.message, 'npc-dialogue korean-text'); | |
| } else if (command.startsWith('chat ') || command.startsWith('talk ') || command.startsWith('say ')) { | |
| // Format chat response nicely | |
| addToGame(data.message, 'npc-dialogue korean-text'); | |
| } else if (command.startsWith('go ') || command.startsWith('move ')) { | |
| // Format movement response | |
| addToGame(data.message, 'game-text'); | |
| // Auto-look after moving (standard MUD behavior) | |
| setTimeout(() => { | |
| sendCommand('look'); | |
| }, 500); | |
| } else { | |
| addToGame(data.message, 'korean-text'); | |
| } | |
| if (data.data) { | |
| updateGameState(data.data); | |
| updateNpcDisplay(data.data); | |
| } | |
| } else { | |
| addToGame(`❌ ${data.message}`, 'error'); | |
| } | |
| } catch (error) { | |
| addToGame(`❌ Network error: ${error.message}`, 'error'); | |
| } finally { | |
| gameState.isLoading = false; | |
| submitButton.disabled = false; | |
| } | |
| } | |
| // Send talk message to NPC | |
| async function sendTalkMessage(message) { | |
| if (gameState.isLoading) return; | |
| gameState.isLoading = true; | |
| submitButton.disabled = true; | |
| addToGame(`💬 "${message}"`, 'command-echo'); | |
| addToGame('🤖 Korean family member is responding...', 'loading korean-text'); | |
| try { | |
| const response = await fetch(`${API_BASE}/api/game/talk`, { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({message: message}) | |
| }); | |
| const data = await response.json(); | |
| // Remove loading message | |
| const lastElement = gameMessages.lastElementChild; | |
| if (lastElement && lastElement.textContent.includes('responding')) { | |
| gameMessages.removeChild(lastElement); | |
| } | |
| if (data.success) { | |
| addToGame(data.message, 'npc-dialogue korean-text'); | |
| gameState.conversationCount++; | |
| if (data.data) { | |
| updateGameState(data.data); | |
| updateNpcDisplay(data.data); | |
| } | |
| } else { | |
| addToGame(`❌ ${data.message}`, 'error'); | |
| } | |
| } catch (error) { | |
| addToGame(`❌ Chat error: ${error.message}`, 'error'); | |
| } finally { | |
| gameState.isLoading = false; | |
| submitButton.disabled = false; | |
| } | |
| } | |
| // Quick command buttons | |
| function sendQuickCommand(command) { | |
| sendCommand(command); | |
| } | |
| // Show talk dialog | |
| function showTalkDialog() { | |
| const message = prompt('💬 What would you like to say to the NPC?\\n(NPC에게 할 말을 입력하세요:)'); | |
| if (message && message.trim()) { | |
| sendTalkMessage(message.trim()); | |
| } | |
| } | |
| // Show examine options | |
| async function showExamineDialog() { | |
| // Show available objects to examine in current room | |
| try { | |
| const response = await fetch(`${API_BASE}/api/game/room-info`); | |
| const data = await response.json(); | |
| if (data.success && data.data.objects && data.data.objects.length > 0) { | |
| let objectText = '🔍 Objects you can examine in this room:\n\n'; | |
| data.data.objects.forEach(obj => { | |
| objectText += `• ${obj.name} - ${obj.description}\n`; | |
| }); | |
| objectText += '\nType "examine [object name]" to interact with them!'; | |
| addToGame(objectText, 'help-content'); | |
| } else { | |
| addToGame('🔍 There\'s nothing to examine in this room. Try moving to a different room!', 'help-content'); | |
| } | |
| } catch (error) { | |
| console.error('Error fetching room objects:', error); | |
| addToGame('🔍 To examine objects, type "examine [object name]" or look around first to see what\'s available!', 'help-content'); | |
| } | |
| } | |
| // Show chat instructions | |
| async function showChatDialog() { | |
| try { | |
| const response = await fetch(`${API_BASE}/api/game/room-info`); | |
| const data = await response.json(); | |
| if (data.success && data.data.npc_info && data.data.npc_info.name) { | |
| const npcName = data.data.npc_info.name; | |
| const chatText = `💬 How to chat with ${npcName}: | |
| 📝 Method 1: Use "chat" command | |
| Type: "chat Hello! How are you?" | |
| 📝 Method 2: Use "talk" command | |
| Type: "talk Can you teach me Korean?" | |
| 📝 Method 3: Use "say" command | |
| Type: "say I want to learn about Korean culture" | |
| 💡 Tips: | |
| • Ask about objects you've examined | |
| • Request Korean lessons | |
| • Learn about Korean culture | |
| • Practice conversation | |
| Try asking ${npcName} about something!`; | |
| addToGame(chatText, 'help-content'); | |
| } else { | |
| addToGame('💬 No family member in this room! Move to a different room to chat with someone.', 'help-content'); | |
| } | |
| } catch (error) { | |
| console.error('Error fetching room info:', error); | |
| addToGame('💬 To chat: "chat [your message]", "talk [your message]", or "say [your message]"', 'help-content'); | |
| } | |
| } | |
| // Show vocabulary learned | |
| function showVocabulary() { | |
| const words = gameState.discovered_words || []; | |
| if (words.length === 0) { | |
| addToGame('📚 You haven\'t learned any Korean words yet! Talk to family members to start learning.', 'help-content'); | |
| } else { | |
| let vocabText = '📚 Korean Words You\'ve Learned:\n\n'; | |
| words.forEach((word, index) => { | |
| vocabText += `${index + 1}. ${word}\n`; | |
| }); | |
| vocabText += '\nKeep talking to family members to learn more! 화이팅! (Fighting!)'; | |
| addToGame(vocabText, 'help-content'); | |
| } | |
| } | |
| // Show inventory (examined objects) | |
| function showInventory() { | |
| const inventory = gameState.player_inventory || []; | |
| if (inventory.length === 0) { | |
| addToGame('🎒 Your inventory is empty! Examine objects around the house to collect them.', 'help-content'); | |
| } else { | |
| let inventoryText = '🎒 Objects You\'ve Examined:\n\n'; | |
| inventory.forEach((item, index) => { | |
| inventoryText += `${index + 1}. ${item}\n`; | |
| }); | |
| inventoryText += '\nThese items represent your cultural discoveries! Keep exploring for more!'; | |
| addToGame(inventoryText, 'help-content'); | |
| } | |
| } | |
| // Show map dialog | |
| function showMapDialog() { | |
| addToGame('🗺️ Opening Korean house map...', 'game-text'); | |
| const mapText = `🏠 KOREAN FAMILY HOUSE MAP 🇰🇷 | |
| 🌸 Garden (정원) | |
| 👴 Grandpa Park | |
| | | |
| 🍳 Kitchen ---- 🏠 Hall ---- 📚 Study | |
| 👵 Grandma Kim 📖 Brother Jung | |
| | | |
| ✏️ Classroom (교실) | |
| 👩🏫 Teacher Choi | |
| | | |
| 🎵 Bedroom (침실) | |
| 👩 Sister Lee | |
| 🚪 Available Rooms: | |
| • 🏠 Hall (대청) - Central hub, family gathering area | |
| • 🍳 Kitchen (부엌) - Grandma Kim teaches honorifics | |
| • 🌸 Garden (정원) - Grandpa Park shares traditional wisdom | |
| • 🎵 Bedroom (침실) - Sister Lee teaches K-pop and slang | |
| • 📚 Study (서재) - Brother Jung explains complex grammar | |
| • ✏️ Classroom (교실) - Teacher Choi provides practical lessons | |
| Use 'go [room]' to move around!`; | |
| addToGame(mapText, 'help-content'); | |
| } | |
| // Process user input | |
| function processInput() { | |
| const input = commandInput.value.trim(); | |
| if (!input || gameState.isLoading) return; | |
| commandInput.value = ''; | |
| // All input goes through strict command parsing | |
| sendCommand(input); | |
| } | |
| // Event listeners | |
| submitButton.addEventListener('click', processInput); | |
| commandInput.addEventListener('keyup', function(e) { | |
| if (e.key === 'Enter') { | |
| processInput(); | |
| } | |
| }); | |
| // Auto-focus input | |
| commandInput.focus(); | |
| // Start the game when DOM is ready | |
| document.addEventListener('DOMContentLoaded', function() { | |
| initGame(); | |
| }); | |
| // Fallback: start game immediately if DOM is already loaded | |
| if (document.readyState === 'loading') { | |
| // DOM still loading | |
| } else { | |
| // DOM already loaded | |
| initGame(); | |
| } | |
| </script> | |
| </body> | |
| </html> |