A Pen by mode-mercury on CodePen.
Created
December 27, 2025 01:11
-
-
Save mode-mercury/1c49d75308f8e8bf63a9a74ee17c2c00 to your computer and use it in GitHub Desktop.
Untitled
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Fractal Realms - Enhanced 8-Bit RPG</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| font-family: 'Courier New', monospace; | |
| image-rendering: pixelated; | |
| -webkit-tap-highlight-color: transparent; | |
| -webkit-user-select: none; | |
| user-select: none; | |
| } | |
| body { | |
| background: #0a0a1a; | |
| color: #e0e0ff; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| min-height: 100vh; | |
| overflow: hidden; | |
| background: | |
| radial-gradient(circle at 20% 30%, #1a1a3a 0%, #0a0a1a 70%), | |
| radial-gradient(circle at 80% 70%, #2a1a3a 0%, #0a0a1a 70%); | |
| touch-action: none; | |
| } | |
| #game-container { | |
| width: 800px; | |
| height: 600px; | |
| position: relative; | |
| border: 4px solid #4a3a8d; | |
| border-radius: 8px; | |
| overflow: hidden; | |
| box-shadow: | |
| 0 0 0 2px #2a2a5a, | |
| 0 0 30px rgba(100, 70, 200, 0.5), | |
| inset 0 0 20px rgba(0, 0, 0, 0.5); | |
| background: #151530; | |
| } | |
| #game-screen { | |
| width: 100%; | |
| height: 100%; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| #world-map { | |
| width: 1600px; | |
| height: 1600px; | |
| position: absolute; | |
| top: -500px; | |
| left: -500px; | |
| background: | |
| linear-gradient(45deg, #0f0f25 25%, #1a1a35 25%, #1a1a35 50%, #0f0f25 50%, #0f0f25 75%, #1a1a35 75%), | |
| linear-gradient(45deg, #0f0f25 25%, #1a1a35 25%, #1a1a35 50%, #0f0f25 50%, #0f0f25 75%, #1a1a35 75%); | |
| background-size: 100px 100px, 100px 100px; | |
| background-position: 0 0, 50px 50px; | |
| } | |
| #player { | |
| position: absolute; | |
| width: 32px; | |
| height: 32px; | |
| z-index: 10; | |
| transition: all 0.15s ease; | |
| filter: drop-shadow(0 0 5px rgba(100, 255, 100, 0.7)); | |
| } | |
| .player-sprite { | |
| width: 100%; | |
| height: 100%; | |
| background: #70c070; | |
| position: relative; | |
| border-radius: 4px; | |
| box-shadow: | |
| inset 0 -6px 0 #508050, | |
| inset 0 6px 0 #90e090, | |
| 0 0 0 2px #406040; | |
| animation: player-idle 2s infinite ease-in-out; | |
| } | |
| .player-sprite::before { | |
| content: ''; | |
| position: absolute; | |
| top: 6px; | |
| left: 6px; | |
| width: 20px; | |
| height: 20px; | |
| background: #406040; | |
| border-radius: 2px; | |
| box-shadow: | |
| inset 0 -3px 0 #304030, | |
| inset 0 3px 0 #508050; | |
| } | |
| .player-sprite::after { | |
| content: ''; | |
| position: absolute; | |
| top: 10px; | |
| left: 10px; | |
| width: 12px; | |
| height: 12px; | |
| background: #a0f0a0; | |
| border-radius: 1px; | |
| box-shadow: 0 0 5px #a0f0a0; | |
| animation: player-glow 2s infinite alternate; | |
| } | |
| .enemy { | |
| position: absolute; | |
| width: 32px; | |
| height: 32px; | |
| z-index: 9; | |
| animation: float 3s infinite ease-in-out; | |
| filter: drop-shadow(0 0 5px rgba(255, 100, 100, 0.7)); | |
| } | |
| .enemy-sprite { | |
| width: 100%; | |
| height: 100%; | |
| background: #c07070; | |
| position: relative; | |
| border-radius: 50%; | |
| box-shadow: | |
| inset 0 -6px 0 #805050, | |
| inset 0 6px 0 #e09090, | |
| 0 0 0 2px #604040; | |
| } | |
| .enemy-sprite::before { | |
| content: ''; | |
| position: absolute; | |
| top: 8px; | |
| left: 8px; | |
| width: 16px; | |
| height: 16px; | |
| background: #604040; | |
| border-radius: 50%; | |
| box-shadow: | |
| inset 0 -3px 0 #503030, | |
| inset 0 3px 0 #705050; | |
| } | |
| .enemy-sprite::after { | |
| content: ''; | |
| position: absolute; | |
| top: 10px; | |
| left: 10px; | |
| width: 12px; | |
| height: 12px; | |
| background: #f0a0a0; | |
| border-radius: 50%; | |
| box-shadow: 0 0 5px #f0a0a0; | |
| animation: enemy-glow 2s infinite alternate; | |
| } | |
| .portal { | |
| position: absolute; | |
| width: 48px; | |
| height: 48px; | |
| z-index: 8; | |
| animation: portal-spin 4s infinite linear; | |
| filter: drop-shadow(0 0 10px rgba(200, 100, 255, 0.8)); | |
| } | |
| .portal-inner { | |
| width: 100%; | |
| height: 100%; | |
| background: | |
| radial-gradient(circle at 30% 30%, rgba(180, 100, 255, 0.9) 0%, transparent 50%), | |
| radial-gradient(circle at 70% 70%, rgba(120, 80, 220, 0.9) 0%, transparent 50%); | |
| border-radius: 50%; | |
| position: relative; | |
| box-shadow: | |
| inset 0 0 15px rgba(150, 80, 255, 0.8), | |
| 0 0 15px rgba(150, 80, 255, 0.8); | |
| } | |
| .portal-inner::before { | |
| content: ''; | |
| position: absolute; | |
| top: 6px; | |
| left: 6px; | |
| right: 6px; | |
| bottom: 6px; | |
| border: 2px dashed rgba(200, 150, 255, 0.7); | |
| border-radius: 50%; | |
| animation: portal-pulse 2s infinite alternate; | |
| } | |
| .npc { | |
| position: absolute; | |
| width: 32px; | |
| height: 32px; | |
| z-index: 8; | |
| animation: npc-bob 3s infinite ease-in-out; | |
| filter: drop-shadow(0 0 5px rgba(100, 150, 255, 0.7)); | |
| } | |
| .npc-sprite { | |
| width: 100%; | |
| height: 100%; | |
| background: #7070c0; | |
| position: relative; | |
| border-radius: 4px; | |
| box-shadow: | |
| inset 0 -6px 0 #505080, | |
| inset 0 6px 0 #9090e0, | |
| 0 0 0 2px #404060; | |
| } | |
| .npc-sprite::before { | |
| content: ''; | |
| position: absolute; | |
| top: 6px; | |
| left: 6px; | |
| width: 20px; | |
| height: 20px; | |
| background: #404060; | |
| border-radius: 2px; | |
| box-shadow: | |
| inset 0 -3px 0 #303050, | |
| inset 0 3px 0 #505080; | |
| } | |
| .npc-sprite::after { | |
| content: '?'; | |
| position: absolute; | |
| top: 8px; | |
| left: 8px; | |
| width: 16px; | |
| height: 16px; | |
| color: #b0b0ff; | |
| font-size: 10px; | |
| font-weight: bold; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .environment { | |
| position: absolute; | |
| z-index: 7; | |
| } | |
| .tree { | |
| width: 32px; | |
| height: 48px; | |
| background: #406040; | |
| border-radius: 2px 2px 8px 8px; | |
| box-shadow: | |
| inset 0 -8px 0 #305030, | |
| 0 0 0 2px #304030; | |
| } | |
| .tree::before { | |
| content: ''; | |
| position: absolute; | |
| top: -20px; | |
| left: -8px; | |
| width: 48px; | |
| height: 32px; | |
| background: #308030; | |
| border-radius: 50%; | |
| box-shadow: | |
| inset 0 -6px 0 #206020, | |
| 0 0 0 2px #205020; | |
| } | |
| .rock { | |
| width: 24px; | |
| height: 24px; | |
| background: #606060; | |
| border-radius: 4px; | |
| box-shadow: | |
| inset 0 -4px 0 #505050, | |
| 0 0 0 2px #404040; | |
| } | |
| .building { | |
| width: 64px; | |
| height: 48px; | |
| background: #705050; | |
| border-radius: 4px; | |
| box-shadow: | |
| inset 0 -8px 0 #604040, | |
| 0 0 0 2px #503030; | |
| } | |
| .building::before { | |
| content: ''; | |
| position: absolute; | |
| top: 8px; | |
| left: 8px; | |
| width: 16px; | |
| height: 24px; | |
| background: #403020; | |
| border-radius: 2px; | |
| } | |
| .building::after { | |
| content: ''; | |
| position: absolute; | |
| top: 8px; | |
| right: 8px; | |
| width: 16px; | |
| height: 24px; | |
| background: #403020; | |
| border-radius: 2px; | |
| } | |
| #ui-container { | |
| position: absolute; | |
| bottom: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 120px; | |
| background: rgba(10, 10, 30, 0.85); | |
| border-top: 3px solid #4a3a8d; | |
| display: flex; | |
| z-index: 20; | |
| box-shadow: 0 -5px 15px rgba(0, 0, 0, 0.7); | |
| } | |
| #text-box { | |
| flex: 3; | |
| padding: 12px; | |
| font-size: 14px; | |
| line-height: 1.4; | |
| overflow-y: auto; | |
| border-right: 2px solid #3a2a7d; | |
| } | |
| #stats-panel { | |
| flex: 1; | |
| padding: 12px; | |
| background: rgba(20, 15, 40, 0.8); | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: space-between; | |
| } | |
| .stat-bar { | |
| height: 16px; | |
| background: rgba(40, 40, 80, 0.7); | |
| border-radius: 8px; | |
| overflow: hidden; | |
| margin-bottom: 6px; | |
| box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.5); | |
| } | |
| .stat-fill { | |
| height: 100%; | |
| border-radius: 8px; | |
| transition: width 0.5s ease; | |
| } | |
| .hp-fill { | |
| background: linear-gradient(90deg, #c05050, #ff7070); | |
| box-shadow: 0 0 5px #ff7070; | |
| } | |
| .mp-fill { | |
| background: linear-gradient(90deg, #5050c0, #7070ff); | |
| box-shadow: 0 0 5px #7070ff; | |
| } | |
| .stat-label { | |
| font-size: 10px; | |
| margin-bottom: 2px; | |
| color: #b0b0ff; | |
| } | |
| #timeline-display { | |
| padding: 6px; | |
| background: rgba(30, 20, 60, 0.8); | |
| border-radius: 4px; | |
| text-align: center; | |
| font-size: 12px; | |
| margin-top: 8px; | |
| box-shadow: 0 0 5px rgba(100, 70, 200, 0.5); | |
| } | |
| #combat-menu { | |
| position: absolute; | |
| bottom: 130px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| display: none; | |
| background: rgba(20, 15, 40, 0.95); | |
| border: 2px solid #5a4a9d; | |
| border-radius: 6px; | |
| padding: 8px; | |
| z-index: 25; | |
| box-shadow: 0 0 15px rgba(100, 70, 200, 0.7); | |
| } | |
| .combat-option { | |
| padding: 10px 16px; | |
| margin: 6px; | |
| background: linear-gradient(135deg, #4a3a8d, #3a2a7d); | |
| border: 2px solid #6a5aad; | |
| border-radius: 4px; | |
| color: #e0e0ff; | |
| font-size: 14px; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| text-align: center; | |
| min-width: 100px; | |
| box-shadow: 0 3px 0 #2a1a5d; | |
| } | |
| .combat-option:hover { | |
| background: linear-gradient(135deg, #5a4a9d, #4a3a8d); | |
| transform: translateY(2px); | |
| box-shadow: 0 1px 0 #2a1a5d; | |
| } | |
| .combat-option:active { | |
| transform: translateY(3px); | |
| box-shadow: 0 0 0 #2a1a5d; | |
| } | |
| /* Mobile Controls */ | |
| #mobile-controls { | |
| position: absolute; | |
| bottom: 130px; | |
| left: 20px; | |
| display: none; | |
| z-index: 30; | |
| } | |
| #d-pad { | |
| display: grid; | |
| grid-template-areas: | |
| ". up ." | |
| "left center right" | |
| ". down ."; | |
| gap: 8px; | |
| background: rgba(30, 20, 60, 0.7); | |
| border-radius: 50%; | |
| padding: 15px; | |
| box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); | |
| } | |
| .d-pad-btn { | |
| width: 40px; | |
| height: 40px; | |
| background: linear-gradient(135deg, #4a3a8d, #3a2a7d); | |
| border: 2px solid #6a5aad; | |
| border-radius: 8px; | |
| color: #e0e0ff; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 18px; | |
| font-weight: bold; | |
| box-shadow: 0 3px 0 #2a1a5d; | |
| touch-action: manipulation; | |
| } | |
| .d-pad-btn:active { | |
| transform: translateY(3px); | |
| box-shadow: 0 0 0 #2a1a5d; | |
| } | |
| #btn-up { grid-area: up; } | |
| #btn-left { grid-area: left; } | |
| #btn-right { grid-area: right; } | |
| #btn-down { grid-area: down; } | |
| #action-buttons { | |
| position: absolute; | |
| bottom: 130px; | |
| right: 20px; | |
| display: none; | |
| flex-direction: column; | |
| gap: 10px; | |
| z-index: 30; | |
| } | |
| .action-btn { | |
| width: 60px; | |
| height: 60px; | |
| background: linear-gradient(135deg, #4a3a8d, #3a2a7d); | |
| border: 2px solid #6a5aad; | |
| border-radius: 50%; | |
| color: #e0e0ff; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 12px; | |
| font-weight: bold; | |
| box-shadow: 0 4px 0 #2a1a5d; | |
| touch-action: manipulation; | |
| text-align: center; | |
| line-height: 1.2; | |
| } | |
| .action-btn:active { | |
| transform: translateY(4px); | |
| box-shadow: 0 0 0 #2a1a5d; | |
| } | |
| #btn-interact::after { | |
| content: "INTERACT"; | |
| } | |
| #btn-menu::after { | |
| content: "MENU"; | |
| } | |
| #main-menu { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: | |
| radial-gradient(circle at 30% 40%, rgba(40, 20, 80, 0.9) 0%, rgba(10, 5, 30, 0.95) 70%); | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 30; | |
| } | |
| .menu-title { | |
| font-size: 42px; | |
| color: #d0b0ff; | |
| text-shadow: | |
| 0 0 8px #a080ff, | |
| 0 0 16px #8060ff, | |
| 0 0 24px #6040ff; | |
| margin-bottom: 30px; | |
| letter-spacing: 3px; | |
| } | |
| .menu-option { | |
| padding: 12px 24px; | |
| margin: 12px; | |
| background: linear-gradient(135deg, #4a3a8d, #3a2a7d); | |
| border: 2px solid #6a5aad; | |
| border-radius: 6px; | |
| color: #e0e0ff; | |
| font-size: 18px; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| min-width: 180px; | |
| text-align: center; | |
| box-shadow: 0 4px 0 #2a1a5d; | |
| } | |
| .menu-option:hover { | |
| background: linear-gradient(135deg, #5a4a9d, #4a3a8d); | |
| transform: translateY(2px); | |
| box-shadow: 0 2px 0 #2a1a5d; | |
| } | |
| .menu-option:active { | |
| transform: translateY(4px); | |
| box-shadow: 0 0 0 #2a1a5d; | |
| } | |
| .damage-popup { | |
| position: absolute; | |
| color: #ff6060; | |
| font-size: 16px; | |
| font-weight: bold; | |
| text-shadow: 0 0 5px #ff0000; | |
| z-index: 15; | |
| animation: popup 1.5s ease-out forwards; | |
| } | |
| .heal-popup { | |
| position: absolute; | |
| color: #60ff60; | |
| font-size: 16px; | |
| font-weight: bold; | |
| text-shadow: 0 0 5px #00ff00; | |
| z-index: 15; | |
| animation: popup 1.5s ease-out forwards; | |
| } | |
| #mini-map { | |
| position: absolute; | |
| top: 10px; | |
| right: 10px; | |
| width: 120px; | |
| height: 120px; | |
| background: rgba(10, 10, 30, 0.7); | |
| border: 2px solid #4a3a8d; | |
| border-radius: 4px; | |
| z-index: 15; | |
| box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); | |
| } | |
| .scanlines { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: linear-gradient( | |
| to bottom, | |
| transparent 50%, | |
| rgba(0, 0, 0, 0.1) 50% | |
| ); | |
| background-size: 100% 4px; | |
| z-index: 5; | |
| pointer-events: none; | |
| } | |
| .vignette { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| box-shadow: inset 0 0 80px rgba(0, 0, 0, 0.6); | |
| z-index: 6; | |
| pointer-events: none; | |
| } | |
| .path-marker { | |
| position: absolute; | |
| width: 8px; | |
| height: 8px; | |
| background: rgba(100, 255, 100, 0.5); | |
| border-radius: 50%; | |
| z-index: 4; | |
| pointer-events: none; | |
| } | |
| @keyframes player-idle { | |
| 0%, 100% { transform: translateY(0); } | |
| 50% { transform: translateY(-2px); } | |
| } | |
| @keyframes player-glow { | |
| 0% { opacity: 0.7; } | |
| 100% { opacity: 1; } | |
| } | |
| @keyframes enemy-glow { | |
| 0% { opacity: 0.7; } | |
| 100% { opacity: 1; } | |
| } | |
| @keyframes float { | |
| 0%, 100% { transform: translateY(0); } | |
| 50% { transform: translateY(-8px); } | |
| } | |
| @keyframes portal-spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| @keyframes portal-pulse { | |
| 0% { transform: scale(1); opacity: 0.7; } | |
| 100% { transform: scale(1.1); opacity: 1; } | |
| } | |
| @keyframes npc-bob { | |
| 0%, 100% { transform: translateY(0); } | |
| 50% { transform: translateY(-4px); } | |
| } | |
| @keyframes popup { | |
| 0% { transform: translateY(0); opacity: 1; } | |
| 100% { transform: translateY(-40px); opacity: 0; } | |
| } | |
| /* Mobile detection */ | |
| @media (max-width: 768px), (pointer: coarse) { | |
| #mobile-controls, #action-buttons { | |
| display: flex; | |
| } | |
| #game-container { | |
| width: 100vw; | |
| height: 100vh; | |
| border: none; | |
| border-radius: 0; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="game-container"> | |
| <div id="game-screen"> | |
| <div id="world-map"></div> | |
| <div id="player"> | |
| <div class="player-sprite"></div> | |
| </div> | |
| <div id="mini-map"></div> | |
| <div class="scanlines"></div> | |
| <div class="vignette"></div> | |
| </div> | |
| <!-- Mobile Controls --> | |
| <div id="mobile-controls"> | |
| <div id="d-pad"> | |
| <div class="d-pad-btn" id="btn-up">↑</div> | |
| <div class="d-pad-btn" id="btn-left">←</div> | |
| <div class="d-pad-btn" id="btn-right">→</div> | |
| <div class="d-pad-btn" id="btn-down">↓</div> | |
| </div> | |
| </div> | |
| <div id="action-buttons"> | |
| <div class="action-btn" id="btn-interact"></div> | |
| <div class="action-btn" id="btn-menu"></div> | |
| </div> | |
| <div id="ui-container"> | |
| <div id="text-box"> | |
| <p>FRACTAL REALMS - Reality is collapsing...</p> | |
| <p>Find the Codex Crystals to stabilize the timelines!</p> | |
| </div> | |
| <div id="stats-panel"> | |
| <div> | |
| <div class="stat-label">HP</div> | |
| <div class="stat-bar"> | |
| <div class="stat-fill hp-fill" style="width: 100%"></div> | |
| </div> | |
| <div class="stat-label">MP</div> | |
| <div class="stat-bar"> | |
| <div class="stat-fill mp-fill" style="width: 100%"></div> | |
| </div> | |
| </div> | |
| <div id="timeline-display">ATLAS TIMELINE</div> | |
| </div> | |
| </div> | |
| <div id="combat-menu"> | |
| <div class="combat-option" onclick="playerAttack()">ATTACK</div> | |
| <div class="combat-option" onclick="playerSpecial()">SPECIAL</div> | |
| <div class="combat-option" onclick="playerItem()">ITEM</div> | |
| <div class="combat-option" onclick="playerFlee()">FLEE</div> | |
| </div> | |
| <div id="main-menu"> | |
| <h1 class="menu-title">FRACTAL REALMS</h1> | |
| <div class="menu-option" onclick="startGame()">NEW GAME</div> | |
| <div class="menu-option" onclick="loadGame()">CONTINUE</div> | |
| <div class="menu-option" onclick="showControls()">CONTROLS</div> | |
| </div> | |
| </div> | |
| <script> | |
| // Game State | |
| const gameState = { | |
| scene: "menu", | |
| player: { | |
| x: 400, | |
| y: 300, | |
| hp: 100, | |
| maxHp: 100, | |
| mp: 50, | |
| maxMp: 50, | |
| atk: 10, | |
| def: 5, | |
| level: 1, | |
| exp: 0, | |
| items: ["Potion", "Ether", "Codex Crystal"], | |
| timeline: "Atlas", | |
| quests: [ | |
| { id: 1, name: "Find the First Crystal", completed: false }, | |
| { id: 2, name: "Defeat 5 Spore Creatures", completed: false, progress: 0, target: 5 } | |
| ] | |
| }, | |
| enemies: [], | |
| portals: [], | |
| npcs: [], | |
| environment: [], | |
| inCombat: false, | |
| currentEnemy: null, | |
| timelines: [ | |
| { name: "Atlas", color: "#7050ff" }, | |
| { name: "Alkali", color: "#50ff70" }, | |
| { name: "Aether", color: "#ff9050" }, | |
| { name: "Deep Space", color: "#9050ff" } | |
| ], | |
| currentTimeline: 0, | |
| mapOffset: { x: 0, y: 0 }, | |
| isMoving: false, | |
| moveTarget: null, | |
| pathMarkers: [] | |
| }; | |
| // DOM Elements | |
| const gameScreen = document.getElementById("game-screen"); | |
| const worldMap = document.getElementById("world-map"); | |
| const player = document.getElementById("player"); | |
| const textBox = document.getElementById("text-box"); | |
| const combatMenu = document.getElementById("combat-menu"); | |
| const mainMenu = document.getElementById("main-menu"); | |
| const hpFill = document.querySelector(".hp-fill"); | |
| const mpFill = document.querySelector(".mp-fill"); | |
| const timelineDisplay = document.getElementById("timeline-display"); | |
| const miniMap = document.getElementById("mini-map"); | |
| // Mobile control elements | |
| const mobileControls = document.getElementById("mobile-controls"); | |
| const actionButtons = document.getElementById("action-buttons"); | |
| const btnUp = document.getElementById("btn-up"); | |
| const btnLeft = document.getElementById("btn-left"); | |
| const btnRight = document.getElementById("btn-right"); | |
| const btnDown = document.getElementById("btn-down"); | |
| const btnInteract = document.getElementById("btn-interact"); | |
| const btnMenu = document.getElementById("btn-menu"); | |
| // Initialize Game | |
| function initGame() { | |
| mainMenu.style.display = "flex"; | |
| gameState.scene = "menu"; | |
| generateWorld(); | |
| setupMobileControls(); | |
| // Check if mobile device | |
| if ('ontouchstart' in window || navigator.maxTouchPoints) { | |
| mobileControls.style.display = "flex"; | |
| actionButtons.style.display = "flex"; | |
| } | |
| } | |
| // Setup Mobile Controls | |
| function setupMobileControls() { | |
| // D-pad controls | |
| btnUp.addEventListener('touchstart', () => movePlayer(0, -1)); | |
| btnUp.addEventListener('touchend', stopMovement); | |
| btnLeft.addEventListener('touchstart', () => movePlayer(-1, 0)); | |
| btnLeft.addEventListener('touchend', stopMovement); | |
| btnRight.addEventListener('touchstart', () => movePlayer(1, 0)); | |
| btnRight.addEventListener('touchend', stopMovement); | |
| btnDown.addEventListener('touchstart', () => movePlayer(0, 1)); | |
| btnDown.addEventListener('touchend', stopMovement); | |
| // Action buttons | |
| btnInteract.addEventListener('touchstart', handleInteract); | |
| btnMenu.addEventListener('touchstart', toggleMenu); | |
| // Click to move | |
| gameScreen.addEventListener('click', handleClickToMove); | |
| } | |
| // Generate World | |
| function generateWorld() { | |
| // Clear existing entities | |
| gameState.enemies = []; | |
| gameState.portals = []; | |
| gameState.npcs = []; | |
| gameState.environment = []; | |
| // Remove existing entities from DOM | |
| document.querySelectorAll('.enemy, .portal, .npc, .environment, .path-marker').forEach(el => el.remove()); | |
| // Add environment objects | |
| for (let i = 0; i < 20; i++) { | |
| spawnEnvironment(); | |
| } | |
| // Add enemies | |
| for (let i = 0; i < 8; i++) { | |
| spawnEnemy(); | |
| } | |
| // Add portals | |
| for (let i = 0; i < 4; i++) { | |
| spawnPortal(); | |
| } | |
| // Add NPCs | |
| for (let i = 0; i < 3; i++) { | |
| spawnNPC(); | |
| } | |
| updateMiniMap(); | |
| } | |
| // Spawn Environment Objects | |
| function spawnEnvironment() { | |
| const types = ['tree', 'rock', 'building']; | |
| const type = types[Math.floor(Math.random() * types.length)]; | |
| const env = document.createElement("div"); | |
| env.className = `environment ${type}`; | |
| const x = Math.floor(Math.random() * 1400) + 100; | |
| const y = Math.floor(Math.random() * 1400) + 100; | |
| env.style.left = `${x}px`; | |
| env.style.top = `${y}px`; | |
| const envSprite = document.createElement("div"); | |
| envSprite.className = type; | |
| env.appendChild(envSprite); | |
| gameScreen.appendChild(env); | |
| gameState.environment.push({ | |
| element: env, | |
| x: x, | |
| y: y, | |
| type: type | |
| }); | |
| } | |
| // Spawn Enemy | |
| function spawnEnemy() { | |
| const enemy = document.createElement("div"); | |
| enemy.className = "enemy"; | |
| const x = Math.floor(Math.random() * 1400) + 100; | |
| const y = Math.floor(Math.random() * 1400) + 100; | |
| enemy.style.left = `${x}px`; | |
| enemy.style.top = `${y}px`; | |
| const enemySprite = document.createElement("div"); | |
| enemySprite.className = "enemy-sprite"; | |
| enemy.appendChild(enemySprite); | |
| gameScreen.appendChild(enemy); | |
| gameState.enemies.push({ | |
| element: enemy, | |
| x: x, | |
| y: y, | |
| hp: 30, | |
| maxHp: 30, | |
| type: "Spore" | |
| }); | |
| } | |
| // Spawn Portal | |
| function spawnPortal() { | |
| const portal = document.createElement("div"); | |
| portal.className = "portal"; | |
| const x = Math.floor(Math.random() * 1400) + 100; | |
| const y = Math.floor(Math.random() * 1400) + 100; | |
| portal.style.left = `${x}px`; | |
| portal.style.top = `${y}px`; | |
| const portalInner = document.createElement("div"); | |
| portalInner.className = "portal-inner"; | |
| portal.appendChild(portalInner); | |
| gameScreen.appendChild(portal); | |
| gameState.portals.push({ | |
| element: portal, | |
| x: x, | |
| y: y | |
| }); | |
| } | |
| // Spawn NPC | |
| function spawnNPC() { | |
| const npc = document.createElement("div"); | |
| npc.className = "npc"; | |
| const x = Math.floor(Math.random() * 1400) + 100; | |
| const y = Math.floor(Math.random() * 1400) + 100; | |
| npc.style.left = `${x}px`; | |
| npc.style.top = `${y}px`; | |
| const npcSprite = document.createElement("div"); | |
| npcSprite.className = "npc-sprite"; | |
| npc.appendChild(npcSprite); | |
| gameScreen.appendChild(npc); | |
| gameState.npcs.push({ | |
| element: npc, | |
| x: x, | |
| y: y, | |
| name: ["Mason", "Nyx", "Check"][Math.floor(Math.random() * 3)], | |
| dialog: [ | |
| "The timelines are collapsing! Find the Codex Crystals!", | |
| "Beware of the Conglomerate - they feed on stories!", | |
| "I've seen reality glitch. It's happening more often." | |
| ][Math.floor(Math.random() * 3)], | |
| quest: Math.random() > 0.5 ? { | |
| id: 3, | |
| name: "Find the Lost Artifact", | |
| completed: false | |
| } : null | |
| }); | |
| } | |
| // Start Game | |
| function startGame() { | |
| mainMenu.style.display = "none"; | |
| gameState.scene = "explore"; | |
| updatePlayerPosition(); | |
| textBox.innerHTML = "<p>You awaken in the Atlas timeline. Reality feels... thin.</p><p>Find portals to travel between dimensions!</p>"; | |
| // Start game loop for smooth movement | |
| requestAnimationFrame(gameLoop); | |
| } | |
| // Game Loop for Smooth Movement | |
| function gameLoop() { | |
| if (gameState.scene === "explore" && !gameState.inCombat) { | |
| // Handle continuous movement for mobile | |
| if (gameState.isMoving) { | |
| movePlayer(gameState.moveTarget.x, gameState.moveTarget.y); | |
| } | |
| // Handle pathfinding movement | |
| if (gameState.pathMarkers.length > 0) { | |
| followPath(); | |
| } | |
| } | |
| requestAnimationFrame(gameLoop); | |
| } | |
| // Mobile Movement | |
| function movePlayer(dx, dy) { | |
| if (gameState.inCombat) return; | |
| const speed = 5; | |
| gameState.player.x = Math.max(20, Math.min(1580, gameState.player.x + dx * speed)); | |
| gameState.player.y = Math.max(20, Math.min(1580, gameState.player.y + dy * speed)); | |
| updatePlayerPosition(); | |
| checkInteractions(); | |
| } | |
| // Stop Movement | |
| function stopMovement() { | |
| gameState.isMoving = false; | |
| gameState.moveTarget = null; | |
| } | |
| // Click to Move with Pathfinding | |
| function handleClickToMove(e) { | |
| if (gameState.inCombat) return; | |
| const rect = gameScreen.getBoundingClientRect(); | |
| const clickX = e.clientX - rect.left; | |
| const clickY = e.clientY - rect.top; | |
| // Convert screen coordinates to game world coordinates | |
| const worldX = clickX - gameState.mapOffset.x; | |
| const worldY = clickY - gameState.mapOffset.y; | |
| // Simple pathfinding - create a direct path with intermediate points | |
| createPath(gameState.player.x, gameState.player.y, worldX, worldY); | |
| } | |
| // Create Path with Intermediate Points | |
| function createPath(startX, startY, endX, endY) { | |
| // Clear existing path | |
| gameState.pathMarkers.forEach(marker => { | |
| if (marker.element && marker.element.parentNode) { | |
| marker.element.parentNode.removeChild(marker.element); | |
| } | |
| }); | |
| gameState.pathMarkers = []; | |
| const steps = 10; | |
| for (let i = 1; i <= steps; i++) { | |
| const t = i / steps; | |
| const x = startX + (endX - startX) * t; | |
| const y = startY + (endY - startY) * t; | |
| const marker = document.createElement("div"); | |
| marker.className = "path-marker"; | |
| marker.style.left = `${x}px`; | |
| marker.style.top = `${y}px`; | |
| gameScreen.appendChild(marker); | |
| gameState.pathMarkers.push({ | |
| element: marker, | |
| x: x, | |
| y: y | |
| }); | |
| } | |
| } | |
| // Follow Path | |
| function followPath() { | |
| if (gameState.pathMarkers.length === 0) return; | |
| const nextPoint = gameState.pathMarkers[0]; | |
| const dx = nextPoint.x - gameState.player.x; | |
| const dy = nextPoint.y - gameState.player.y; | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| if (distance < 10) { | |
| // Reached point, remove it | |
| if (nextPoint.element && nextPoint.element.parentNode) { | |
| nextPoint.element.parentNode.removeChild(nextPoint.element); | |
| } | |
| gameState.pathMarkers.shift(); | |
| } else { | |
| // Move toward point | |
| const speed = 5; | |
| gameState.player.x += (dx / distance) * speed; | |
| gameState.player.y += (dy / distance) * speed; | |
| updatePlayerPosition(); | |
| checkInteractions(); | |
| } | |
| } | |
| // Handle Interaction | |
| function handleInteract() { | |
| checkInteractions(true); | |
| } | |
| // Check for Interactions | |
| function checkInteractions(forceInteract = false) { | |
| // Check for enemy collision | |
| for (const enemy of gameState.enemies) { | |
| const dx = Math.abs(gameState.player.x - enemy.x); | |
| const dy = Math.abs(gameState.player.y - enemy.y); | |
| if (dx < 30 && dy < 30) { | |
| startCombat(enemy); | |
| return; | |
| } | |
| } | |
| // Check for portal collision | |
| for (const portal of gameState.portals) { | |
| const dx = Math.abs(gameState.player.x - portal.x); | |
| const dy = Math.abs(gameState.player.y - portal.y); | |
| if (dx < 40 && dy < 40) { | |
| enterPortal(portal); | |
| return; | |
| } | |
| } | |
| // Check for NPC interaction | |
| for (const npc of gameState.npcs) { | |
| const dx = Math.abs(gameState.player.x - npc.x); | |
| const dy = Math.abs(gameState.player.y - npc.y); | |
| if ((dx < 40 && dy < 40) || forceInteract) { | |
| talkToNPC(npc); | |
| return; | |
| } | |
| } | |
| } | |
| // Toggle Menu | |
| function toggleMenu() { | |
| if (gameState.scene === "explore") { | |
| textBox.innerHTML = ` | |
| <p>GAME MENU</p> | |
| <p>Quests:</p> | |
| <ul> | |
| ${gameState.player.quests.map(q => | |
| `<li>${q.name} ${q.completed ? '✓' : q.progress ? `(${q.progress}/${q.target})` : ''}</li>` | |
| ).join('')} | |
| </ul> | |
| <p>Inventory: ${gameState.player.items.join(', ')}</p> | |
| `; | |
| } | |
| } | |
| // Update Player Position | |
| function updatePlayerPosition() { | |
| player.style.left = `${gameState.player.x - 16}px`; | |
| player.style.top = `${gameState.player.y - 16}px`; | |
| // Update map position (simple parallax) | |
| const mapX = -gameState.player.x + 400; | |
| const mapY = -gameState.player.y + 300; | |
| worldMap.style.transform = `translate(${mapX}px, ${mapY}px)`; | |
| updateMiniMap(); | |
| } | |
| // Update Mini Map | |
| function updateMiniMap() { | |
| miniMap.innerHTML = ''; | |
| // Draw player on mini map | |
| const playerDot = document.createElement("div"); | |
| playerDot.style.position = "absolute"; | |
| playerDot.style.width = "4px"; | |
| playerDot.style.height = "4px"; | |
| playerDot.style.background = "#70c070"; | |
| playerDot.style.borderRadius = "50%"; | |
| playerDot.style.left = "50%"; | |
| playerDot.style.top = "50%"; | |
| playerDot.style.transform = "translate(-50%, -50%)"; | |
| miniMap.appendChild(playerDot); | |
| // Draw enemies on mini map | |
| gameState.enemies.forEach(enemy => { | |
| const enemyDot = document.createElement("div"); | |
| enemyDot.style.position = "absolute"; | |
| enemyDot.style.width = "3px"; | |
| enemyDot.style.height = "3px"; | |
| enemyDot.style.background = "#c07070"; | |
| enemyDot.style.borderRadius = "50%"; | |
| const relX = (enemy.x - gameState.player.x) / 16 + 60; | |
| const relY = (enemy.y - gameState.player.y) / 16 + 60; | |
| enemyDot.style.left = `${relX}px`; | |
| enemyDot.style.top = `${relY}px`; | |
| miniMap.appendChild(enemyDot); | |
| }); | |
| // Draw portals on mini map | |
| gameState.portals.forEach(portal => { | |
| const portalDot = document.createElement("div"); | |
| portalDot.style.position = "absolute"; | |
| portalDot.style.width = "4px"; | |
| portalDot.style.height = "4px"; | |
| portalDot.style.background = "#a070ff"; | |
| portalDot.style.borderRadius = "50%"; | |
| const relX = (portal.x - gameState.player.x) / 16 + 60; | |
| const relY = (portal.y - gameState.player.y) / 16 + 60; | |
| portalDot.style.left = `${relX}px`; | |
| portalDot.style.top = `${relY}px`; | |
| miniMap.appendChild(portalDot); | |
| }); | |
| } | |
| // Enter Portal | |
| function enterPortal(portal) { | |
| gameState.currentTimeline = (gameState.currentTimeline + 1) % gameState.timelines.length; | |
| const newTimeline = gameState.timelines[gameState.currentTimeline]; | |
| gameState.player.timeline = newTimeline.name; | |
| timelineDisplay.textContent = `${newTimeline.name.toUpperCase()} TIMELINE`; | |
| timelineDisplay.style.boxShadow = `0 0 8px ${newTimeline.color}`; | |
| textBox.innerHTML = `<p>TIMESHIFT: You enter the ${newTimeline.name} timeline. Reality shimmers around you.</p>`; | |
| // Clear path when changing timelines | |
| gameState.pathMarkers.forEach(marker => { | |
| if (marker.element && marker.element.parentNode) { | |
| marker.element.parentNode.removeChild(marker.element); | |
| } | |
| }); | |
| gameState.pathMarkers = []; | |
| // Remove the used portal | |
| portal.element.style.display = "none"; | |
| gameState.portals = gameState.portals.filter(p => p !== portal); | |
| // Add new portal after a delay | |
| setTimeout(() => { | |
| spawnPortal(); | |
| }, 3000); | |
| } | |
| // Talk to NPC | |
| function talkToNPC(npc) { | |
| textBox.innerHTML = `<p>${npc.name}: "${npc.dialog}"</p>`; | |
| // Handle quests | |
| if (npc.quest && !npc.quest.completed) { | |
| const existingQuest = gameState.player.quests.find(q => q.id === npc.quest.id); | |
| if (!existingQuest) { | |
| gameState.player.quests.push({...npc.quest}); | |
| textBox.innerHTML += `<p>New Quest: ${npc.quest.name}</p>`; | |
| } | |
| } | |
| // Sometimes NPCs give items | |
| if (Math.random() > 0.7 && gameState.player.items.length < 5) { | |
| const newItem = ["Potion", "Ether"][Math.floor(Math.random() * 2)]; | |
| gameState.player.items.push(newItem); | |
| textBox.innerHTML += `<p>You received a ${newItem}!</p>`; | |
| } | |
| } | |
| // Start Combat | |
| function startCombat(enemy) { | |
| gameState.inCombat = true; | |
| gameState.currentEnemy = enemy; | |
| combatMenu.style.display = "flex"; | |
| textBox.innerHTML = `<p>A ${enemy.type} attacks! Prepare for battle!</p>`; | |
| // Clear path when combat starts | |
| gameState.pathMarkers.forEach(marker => { | |
| if (marker.element && marker.element.parentNode) { | |
| marker.element.parentNode.removeChild(marker.element); | |
| } | |
| }); | |
| gameState.pathMarkers = []; | |
| } | |
| // Player Attack | |
| function playerAttack() { | |
| const damage = Math.floor(Math.random() * gameState.player.atk) + 5; | |
| gameState.currentEnemy.hp -= damage; | |
| // Create damage popup | |
| const popup = document.createElement("div"); | |
| popup.className = "damage-popup"; | |
| popup.textContent = `-${damage}`; | |
| popup.style.left = `${gameState.currentEnemy.x}px`; | |
| popup.style.top = `${gameState.currentEnemy.y}px`; | |
| gameScreen.appendChild(popup); | |
| textBox.innerHTML = `<p>You attack for ${damage} damage!</p>`; | |
| setTimeout(() => { | |
| if (gameState.currentEnemy.hp <= 0) { | |
| textBox.innerHTML += `<p>Enemy defeated! +10 EXP</p>`; | |
| gameState.player.exp += 10; | |
| // Update quest progress | |
| const defeatQuest = gameState.player.quests.find(q => q.id === 2); | |
| if (defeatQuest && !defeatQuest.completed) { | |
| defeatQuest.progress++; | |
| if (defeatQuest.progress >= defeatQuest.target) { | |
| defeatQuest.completed = true; | |
| textBox.innerHTML += `<p>Quest Complete: ${defeatQuest.name}!</p>`; | |
| } | |
| } | |
| // Remove enemy | |
| gameScreen.removeChild(gameState.currentEnemy.element); | |
| gameState.enemies = gameState.enemies.filter(e => e !== gameState.currentEnemy); | |
| // Check for level up | |
| if (gameState.player.exp >= gameState.player.level * 20) { | |
| levelUp(); | |
| } | |
| gameState.inCombat = false; | |
| combatMenu.style.display = "none"; | |
| gameState.currentEnemy = null; | |
| // Spawn new enemy after a delay | |
| setTimeout(spawnEnemy, 2000); | |
| } else { | |
| // Enemy attacks back | |
| const enemyDamage = Math.floor(Math.random() * 8) + 2; | |
| gameState.player.hp -= enemyDamage; | |
| updateHPBar(); | |
| // Create damage popup on player | |
| const playerPopup = document.createElement("div"); | |
| playerPopup.className = "damage-popup"; | |
| playerPopup.textContent = `-${enemyDamage}`; | |
| playerPopup.style.left = `${gameState.player.x}px`; | |
| playerPopup.style.top = `${gameState.player.y}px`; | |
| gameScreen.appendChild(playerPopup); | |
| textBox.innerHTML += `<p>The enemy retaliates! You take ${enemyDamage} damage!</p>`; | |
| if (gameState.player.hp <= 0) { | |
| textBox.innerHTML += `<p>YOU HAVE FALLEN IN BATTLE.</p>`; | |
| setTimeout(resetGame, 2000); | |
| } | |
| } | |
| }, 1000); | |
| } | |
| // Player Special | |
| function playerSpecial() { | |
| if (gameState.player.mp < 10) { | |
| textBox.innerHTML = "<p>Not enough MP!</p>"; | |
| return; | |
| } | |
| gameState.player.mp -= 10; | |
| updateMPBar(); | |
| const damage = Math.floor(Math.random() * 15) + 10; | |
| gameState.currentEnemy.hp -= damage; | |
| // Create damage popup | |
| const popup = document.createElement("div"); | |
| popup.className = "damage-popup"; | |
| popup.textContent = `-${damage}`; | |
| popup.style.left = `${gameState.currentEnemy.x}px`; | |
| popup.style.top = `${gameState.currentEnemy.y}px`; | |
| popup.style.color = "#70a0ff"; | |
| gameScreen.appendChild(popup); | |
| textBox.innerHTML = `<p>You use CODE BLAST for ${damage} damage!</p>`; | |
| setTimeout(() => { | |
| textBox.innerHTML += `<p>Enemy defeated! +15 EXP</p>`; | |
| gameState.player.exp += 15; | |
| // Update quest progress | |
| const defeatQuest = gameState.player.quests.find(q => q.id === 2); | |
| if (defeatQuest && !defeatQuest.completed) { | |
| defeatQuest.progress++; | |
| if (defeatQuest.progress >= defeatQuest.target) { | |
| defeatQuest.completed = true; | |
| textBox.innerHTML += `<p>Quest Complete: ${defeatQuest.name}!</p>`; | |
| } | |
| } | |
| // Remove enemy | |
| gameScreen.removeChild(gameState.currentEnemy.element); | |
| gameState.enemies = gameState.enemies.filter(e => e !== gameState.currentEnemy); | |
| // Check for level up | |
| if (gameState.player.exp >= gameState.player.level * 20) { | |
| levelUp(); | |
| } | |
| gameState.inCombat = false; | |
| combatMenu.style.display = "none"; | |
| gameState.currentEnemy = null; | |
| // Spawn new enemy after a delay | |
| setTimeout(spawnEnemy, 2000); | |
| }, 1000); | |
| } | |
| // Player Item | |
| function playerItem() { | |
| if (gameState.player.items.length === 0) { | |
| textBox.innerHTML = "<p>No items left!</p>"; | |
| return; | |
| } | |
| const item = gameState.player.items.pop(); | |
| if (item === "Potion") { | |
| gameState.player.hp = Math.min(gameState.player.hp + 30, gameState.player.maxHp); | |
| updateHPBar(); | |
| // Create heal popup | |
| const popup = document.createElement("div"); | |
| popup.className = "heal-popup"; | |
| popup.textContent = "+30 HP"; | |
| popup.style.left = `${gameState.player.x}px`; | |
| popup.style.top = `${gameState.player.y}px`; | |
| gameScreen.appendChild(popup); | |
| textBox.innerHTML = `<p>Used POTION! HP: ${gameState.player.hp}/${gameState.player.maxHp}</p>`; | |
| } else if (item === "Ether") { | |
| gameState.player.mp = Math.min(gameState.player.mp + 20, gameState.player.maxMp); | |
| updateMPBar(); | |
| // Create heal popup | |
| const popup = document.createElement("div"); | |
| popup.className = "heal-popup"; | |
| popup.textContent = "+20 MP"; | |
| popup.style.left = `${gameState.player.x}px`; | |
| popup.style.top = `${gameState.player.y}px`; | |
| gameScreen.appendChild(popup); | |
| textBox.innerHTML = `<p>Used ETHER! MP: ${gameState.player.mp}/${gameState.player.maxMp}</p>`; | |
| } else if (item === "Codex Crystal") { | |
| gameState.player.hp = gameState.player.maxHp; | |
| gameState.player.mp = gameState.player.maxMp; | |
| updateHPBar(); | |
| updateMPBar(); | |
| // Create heal popup | |
| const popup = document.createElement("div"); | |
| popup.className = "heal-popup"; | |
| popup.textContent = "FULL RESTORE!"; | |
| popup.style.left = `${gameState.player.x}px`; | |
| popup.style.top = `${gameState.player.y}px`; | |
| gameScreen.appendChild(popup); | |
| textBox.innerHTML = `<p>Used CODEX CRYSTAL! HP and MP fully restored!</p>`; | |
| } | |
| } | |
| // Player Flee | |
| function playerFlee() { | |
| if (Math.random() > 0.5) { | |
| textBox.innerHTML = "<p>You successfully escaped!</p>"; | |
| gameState.inCombat = false; | |
| combatMenu.style.display = "none"; | |
| gameState.currentEnemy = null; | |
| } else { | |
| textBox.innerHTML = "<p>Escape failed!</p>"; | |
| const enemyDamage = Math.floor(Math.random() * 8) + 2; | |
| gameState.player.hp -= enemyDamage; | |
| updateHPBar(); | |
| // Create damage popup on player | |
| const playerPopup = document.createElement("div"); | |
| playerPopup.className = "damage-popup"; | |
| playerPopup.textContent = `-${enemyDamage}`; | |
| playerPopup.style.left = `${gameState.player.x}px`; | |
| playerPopup.style.top = `${gameState.player.y}px`; | |
| gameScreen.appendChild(playerPopup); | |
| textBox.innerHTML += `<p>You take ${enemyDamage} damage!</p>`; | |
| if (gameState.player.hp <= 0) { | |
| textBox.innerHTML += `<p>YOU HAVE FALLEN IN BATTLE.</p>`; | |
| setTimeout(resetGame, 2000); | |
| } | |
| } | |
| } | |
| // Level Up | |
| function levelUp() { | |
| gameState.player.level++; | |
| gameState.player.maxHp += 20; | |
| gameState.player.maxMp += 10; | |
| gameState.player.atk += 3; | |
| gameState.player.def += 2; | |
| gameState.player.hp = gameState.player.maxHp; | |
| gameState.player.mp = gameState.player.maxMp; | |
| updateHPBar(); | |
| updateMPBar(); | |
| textBox.innerHTML = `<p>LEVEL UP! You are now Level ${gameState.player.level}!</p>`; | |
| } | |
| // Update HP Bar | |
| function updateHPBar() { | |
| const hpPercent = (gameState.player.hp / gameState.player.maxHp) * 100; | |
| hpFill.style.width = `${hpPercent}%`; | |
| } | |
| // Update MP Bar | |
| function updateMPBar() { | |
| const mpPercent = (gameState.player.mp / gameState.player.maxMp) * 100; | |
| mpFill.style.width = `${mpPercent}%`; | |
| } | |
| // Reset Game | |
| function resetGame() { | |
| gameState.player.hp = gameState.player.maxHp; | |
| gameState.player.mp = gameState.player.maxMp; | |
| gameState.inCombat = false; | |
| combatMenu.style.display = "none"; | |
| if (gameState.currentEnemy) { | |
| gameScreen.removeChild(gameState.currentEnemy.element); | |
| gameState.currentEnemy = null; | |
| } | |
| updateHPBar(); | |
| updateMPBar(); | |
| generateWorld(); | |
| } | |
| // Show Controls | |
| function showControls() { | |
| textBox.innerHTML = ` | |
| <p><strong>CONTROLS:</strong></p> | |
| <p>Arrow Keys - Move (Desktop)</p> | |
| <p>D-Pad - Move (Mobile)</p> | |
| <p>Click/Tap - Move to location</p> | |
| <p>Space/Interact Button - Interact with objects/NPCs</p> | |
| <p>Find portals to travel between timelines!</p> | |
| `; | |
| } | |
| // Keyboard Controls (for desktop) | |
| document.addEventListener("keydown", (e) => { | |
| if (gameState.scene !== "explore" || gameState.inCombat) return; | |
| const speed = 8; | |
| let moved = false; | |
| switch (e.key) { | |
| case "ArrowUp": | |
| gameState.player.y = Math.max(20, gameState.player.y - speed); | |
| moved = true; | |
| break; | |
| case "ArrowDown": | |
| gameState.player.y = Math.min(1580, gameState.player.y + speed); | |
| moved = true; | |
| break; | |
| case "ArrowLeft": | |
| gameState.player.x = Math.max(20, gameState.player.x - speed); | |
| moved = true; | |
| break; | |
| case "ArrowRight": | |
| gameState.player.x = Math.min(1580, gameState.player.x + speed); | |
| moved = true; | |
| break; | |
| case " ": | |
| checkInteractions(true); | |
| break; | |
| case "Escape": | |
| toggleMenu(); | |
| break; | |
| } | |
| if (moved) { | |
| updatePlayerPosition(); | |
| checkInteractions(); | |
| } | |
| }); | |
| // Start Game | |
| window.onload = initGame; | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment