A Pen by mode-mercury on CodePen.
Created
October 18, 2025 00:44
-
-
Save mode-mercury/744ab92e9ac82ca047407d255023ca05 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>Nexus Chess - Cyberpunk Tower Defense</title> | |
| <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { | |
| background: linear-gradient(135deg, #0a0a1a 0%, #151530 50%, #0a0a1a 100%); | |
| color: white; | |
| font-family: 'Orbitron', 'Courier New', monospace; | |
| min-height: 100vh; | |
| overflow-x: hidden; | |
| } | |
| :root { | |
| --cyber-blue: #00f0ff; | |
| --cyber-pink: #ff00aa; | |
| --cyber-purple: #aa00ff; | |
| --cyber-gold: #ffd700; | |
| --cyber-green: #00ff88; | |
| --cyber-red: #ff0044; | |
| --cyber-darkblue: #1e3a8a; | |
| --cyber-teal: #00ffcc; | |
| --cyber-orange: #ff7700; | |
| --cyber-violet: #cc00ff; | |
| } | |
| .particles { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; | |
| z-index: -1; | |
| } | |
| .particle { | |
| position: absolute; | |
| width: 2px; | |
| height: 2px; | |
| background: var(--cyber-blue); | |
| animation: float 12s infinite linear; | |
| } | |
| .particle.pink { background: var(--cyber-pink); } | |
| .particle.green { background: var(--cyber-green); } | |
| .particle.purple { background: var(--cyber-purple); } | |
| .particle.teal { background: var(--cyber-teal); } | |
| .particle.orange { background: var(--cyber-orange); } | |
| .particle.violet { background: var(--cyber-violet); } | |
| @keyframes float { | |
| 0% { transform: translateY(100vh) translateX(0px) rotate(0deg); opacity: 0; } | |
| 10% { opacity: 1; } | |
| 90% { opacity: 1; } | |
| 100% { transform: translateY(-10vh) translateX(100px) rotate(180deg); opacity: 0; } | |
| } | |
| .game-container { | |
| background: rgba(10, 10, 30, 0.4); | |
| backdrop-filter: blur(15px); | |
| border: 1px solid rgba(0, 240, 255, 0.4); | |
| border-radius: 20px; | |
| min-height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .card { | |
| width: 100px; | |
| height: 160px; | |
| cursor: pointer; | |
| position: relative; | |
| user-select: none; | |
| transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); | |
| transform-origin: center; | |
| } | |
| @media (min-width: 768px) { | |
| .card { | |
| width: 110px; | |
| height: 176px; | |
| } | |
| } | |
| .card:hover { | |
| transform: translateY(-8px) scale(1.05); | |
| z-index: 10; | |
| } | |
| .card-disabled { | |
| opacity: 0.5; | |
| filter: grayscale(50%); | |
| pointer-events: none; | |
| } | |
| @keyframes legendary-glow { | |
| 0%, 100% { filter: drop-shadow(0 0 10px #ffd700); } | |
| 50% { filter: drop-shadow(0 0 20px #ffd700) drop-shadow(0 0 30px #ff6b35); } | |
| } | |
| @keyframes epic-glow { | |
| 0%, 100% { filter: drop-shadow(0 0 10px #ff7700); } | |
| 50% { filter: drop-shadow(0 0 15px #ff7700) drop-shadow(0 0 25px #ff4400); } | |
| } | |
| @keyframes rare-glow { | |
| 0%, 100% { filter: drop-shadow(0 0 8px #c0c0c0); } | |
| 50% { filter: drop-shadow(0 0 12px #c0c0c0) drop-shadow(0 0 20px #ffffff); } | |
| } | |
| .card-legendary { | |
| animation: legendary-glow 3s ease-in-out infinite; | |
| } | |
| .card-epic { | |
| animation: epic-glow 3s ease-in-out infinite; | |
| } | |
| .card-rare { | |
| animation: rare-glow 3s ease-in-out infinite; | |
| } | |
| /* 3D Game Board Styles */ | |
| #game-canvas { | |
| width: 100%; | |
| height: 500px; | |
| border-radius: 12px; | |
| overflow: hidden; | |
| background: linear-gradient(135deg, rgba(5, 5, 15, 0.95) 0%, rgba(20, 10, 40, 0.95) 50%, rgba(5, 5, 15, 0.95) 100%); | |
| box-shadow: | |
| 0 0 40px rgba(170, 0, 255, 0.5), | |
| inset 0 0 30px rgba(170, 0, 255, 0.3), | |
| 0 0 80px rgba(0, 255, 204, 0.4); | |
| } | |
| /* Piece Menu */ | |
| .piece-menu { | |
| position: fixed; | |
| background: linear-gradient(135deg, rgba(0, 0, 0, 0.95) 0%, rgba(30, 30, 30, 0.9) 100%); | |
| border: 2px solid #00f0ff; | |
| border-radius: 8px; | |
| padding: 8px; | |
| min-width: 200px; | |
| z-index: 100; | |
| backdrop-filter: blur(10px); | |
| box-shadow: 0 0 20px rgba(0, 240, 255, 0.4); | |
| transform: translateX(-50%); | |
| left: 50%; | |
| bottom: 20px; | |
| } | |
| .piece-menu button { | |
| display: block; | |
| width: 100%; | |
| padding: 6px 10px; | |
| margin: 2px 0; | |
| background: rgba(0, 240, 255, 0.2); | |
| border: 1px solid #00f0ff; | |
| color: #00f0ff; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-size: 12px; | |
| transition: all 0.3s ease; | |
| } | |
| .piece-menu button:hover { | |
| background: rgba(0, 240, 255, 0.4); | |
| box-shadow: 0 0 10px rgba(0, 240, 255, 0.6); | |
| } | |
| .piece-menu button:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| } | |
| .modal-enter { | |
| animation: modalEnter 0.3s ease-out; | |
| } | |
| @keyframes modalEnter { | |
| from { | |
| opacity: 0; | |
| transform: scale(0.9) translateY(-20px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: scale(1) translateY(0); | |
| } | |
| } | |
| .difficulty-btn { | |
| transition: all 0.3s ease; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .difficulty-btn::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: -100%; | |
| width: 100%; | |
| height: 100%; | |
| background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent); | |
| transition: left 0.5s; | |
| } | |
| .difficulty-btn:hover::before { | |
| left: 100%; | |
| } | |
| .floating { | |
| animation: float 3s ease-in-out infinite; | |
| } | |
| @keyframes float { | |
| 0%, 100% { | |
| transform: translateY(0px); | |
| } | |
| 50% { | |
| transform: translateY(-10px); | |
| } | |
| } | |
| .compact-section { | |
| background: linear-gradient(135deg, rgba(20, 25, 45, 0.8) 0%, rgba(10, 15, 30, 0.9) 100%); | |
| border: 1px solid rgba(80, 90, 120, 0.6); | |
| backdrop-filter: blur(10px); | |
| } | |
| .card-detail-modal { | |
| backdrop-filter: blur(20px); | |
| } | |
| .logo-text { | |
| background: linear-gradient(45deg, #00f0ff, #aa00ff, #ff00aa, #00ff88); | |
| background-size: 300% 300%; | |
| animation: gradientShift 4s ease infinite; | |
| -webkit-background-clip: text; | |
| background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| text-shadow: 0 0 20px rgba(0, 240, 255, 0.5); | |
| } | |
| @keyframes gradientShift { | |
| 0% { | |
| background-position: 0% 50%; | |
| } | |
| 50% { | |
| background-position: 100% 50%; | |
| } | |
| 100% { | |
| background-position: 0% 50%; | |
| } | |
| } | |
| .profile-icon { | |
| width: 32px; | |
| height: 32px; | |
| background-color: #111827; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| border: 2px solid; | |
| } | |
| .profile-icon svg { | |
| width: 18px; | |
| height: 18px; | |
| } | |
| #player-info .profile-icon { | |
| border-color: #00f0ff; | |
| } | |
| #opponent-info .profile-icon { | |
| border-color: #ff00aa; | |
| } | |
| .board-container { | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| flex: 1; | |
| padding: 1rem; | |
| position: relative; | |
| } | |
| .hand-fan { | |
| display: flex; | |
| justify-content: center; | |
| align-items: flex-end; | |
| min-height: 180px; | |
| perspective: 1000px; | |
| position: relative; | |
| } | |
| .card-fan-wrapper { | |
| position: relative; | |
| transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); | |
| margin: 0 -15px; | |
| } | |
| .card-fan-wrapper:hover { | |
| transform: translateY(-20px) scale(1.05) !important; | |
| z-index: 20; | |
| } | |
| #battle-log::-webkit-scrollbar { | |
| width: 4px; | |
| } | |
| #battle-log::-webkit-scrollbar-thumb { | |
| background-color: rgba(0, 240, 255, 0.4); | |
| border-radius: 2px; | |
| } | |
| .stats-compact { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| font-size: 0.75rem; | |
| flex-wrap: wrap; | |
| max-width: 180px; | |
| } | |
| .health-bar { | |
| background: linear-gradient(90deg, #ff0044 0%, #ff7700 50%, #00ff88 100%); | |
| height: 4px; | |
| border-radius: 2px; | |
| transition: all 0.3s ease; | |
| } | |
| .exp-bar { | |
| background: linear-gradient(90deg, #aa00ff 0%, #ff00aa 50%, #00f0ff 100%); | |
| height: 6px; | |
| border-radius: 3px; | |
| transition: all 0.3s ease; | |
| } | |
| /* Player info layout fixes */ | |
| #player-info, #opponent-info { | |
| max-width: 180px; | |
| overflow: hidden; | |
| } | |
| #player-info .stats-compact, #opponent-info .stats-compact { | |
| justify-content: flex-start; | |
| } | |
| #opponent-info .stats-compact { | |
| justify-content: flex-end; | |
| } | |
| @media (max-width: 768px) { | |
| #player-info, #opponent-info { | |
| max-width: 140px; | |
| } | |
| .stats-compact { | |
| font-size: 0.7rem; | |
| gap: 4px; | |
| } | |
| } | |
| /* Chest animation */ | |
| @keyframes chest-glow { | |
| 0%, 100% { | |
| transform: translateY(0) rotateY(0deg); | |
| box-shadow: 0 0 10px currentColor; | |
| } | |
| 50% { | |
| transform: translateY(-5px) rotateY(10deg); | |
| box-shadow: 0 0 20px currentColor; | |
| } | |
| } | |
| .chest-legendary { | |
| animation: chest-glow 3s ease-in-out infinite; | |
| color: #ffd700; | |
| } | |
| .chest-epic { | |
| animation: chest-glow 3s ease-inout infinite; | |
| color: #ff7700; | |
| } | |
| .chest-rare { | |
| animation: chest-glow 3s ease-in-out infinite; | |
| color: #c0c0c0; | |
| } | |
| /* Enhanced mode selection */ | |
| .mode-selection-bg { | |
| background: radial-gradient(circle at center, #0a0a1a 0%, #050510 100%); | |
| } | |
| .nexus-glow { | |
| box-shadow: 0 0 60px rgba(0, 240, 255, 0.4), 0 0 100px rgba(170, 0, 255, 0.3); | |
| } | |
| .selection-card { | |
| background: linear-gradient(135deg, rgba(20, 20, 40, 0.9) 0%, rgba(10, 10, 25, 0.9) 100%); | |
| border: 1px solid rgba(0, 240, 255, 0.3); | |
| backdrop-filter: blur(10px); | |
| } | |
| .selection-card:hover { | |
| border-color: rgba(0, 240, 255, 0.6); | |
| box-shadow: 0 0 30px rgba(0, 240, 255, 0.3); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="particles" id="particles"></div> | |
| <div class="game-container container mx-auto p-2 max-w-7xl"> | |
| <!-- Header --> | |
| <div class="text-center mb-2"> | |
| <h1 class="text-xl md:text-2xl font-bold logo-text floating">⚡ NEXUS CHESS ⚡</h1> | |
| <p class="text-gray-300 text-xs">Cyberpunk Tower Defense</p> | |
| </div> | |
| <!-- Enhanced Cyberpunk Mode Selection Modal --> | |
| <div id="mode-selection" class="fixed inset-0 mode-selection-bg flex items-center justify-center z-50 card-detail-modal"> | |
| <div class="selection-card rounded-2xl p-8 max-w-2xl w-full mx-4 border-2 border-cyan-400 shadow-2xl modal-enter backdrop-blur-xl nexus-glow"> | |
| <div class="text-center mb-8"> | |
| <div class="text-6xl mb-4 floating">🔮</div> | |
| <h2 class="text-4xl font-bold mb-2 logo-text">NEXUS REALMS</h2> | |
| <p class="text-cyan-300 text-sm">Choose your digital dominion</p> | |
| <div class="w-32 h-1 bg-gradient-to-r from-cyan-500 to-purple-500 mx-auto mt-2 rounded-full"></div> | |
| </div> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| <div class="text-center"> | |
| <h3 class="text-xl font-bold mb-4 text-cyan-300">PLAYER BATTLE</h3> | |
| <button onclick="startPvPMode()" class="w-full difficulty-btn bg-gradient-to-r from-cyan-600 via-blue-600 to-purple-600 hover:from-cyan-500 hover:via-blue-500 hover:to-purple-500 text-white py-6 px-8 rounded-xl font-bold transition-all transform hover:scale-105 border-2 border-cyan-400 shadow-lg nexus-glow"> | |
| <span class="text-3xl mr-3">⚔️</span> | |
| <div class="text-lg">NEURAL DUEL</div> | |
| <div class="text-xs opacity-75 mt-1">Player vs Player Combat</div> | |
| </button> | |
| </div> | |
| <div class="text-center"> | |
| <h3 class="text-xl font-bold mb-4 text-purple-300">AI NEXUS</h3> | |
| <div class="space-y-4"> | |
| <button onclick="startGame('easy')" class="w-full difficulty-btn bg-gradient-to-r from-green-600 to-emerald-700 hover:from-green-500 hover:to-emerald-600 text-white py-4 px-6 rounded-xl font-bold transition-all transform hover:scale-105 border-2 border-green-400 shadow-lg" style="box-shadow: 0 0 20px rgba(0, 255, 136, 0.3);"> | |
| <span class="text-xl mr-2">🟢</span>SCRIPT KIDDIE | |
| <div class="text-xs opacity-75">Basic AI Protocols</div> | |
| </button> | |
| <button onclick="startGame('medium')" class="w-full difficulty-btn bg-gradient-to-r from-yellow-600 to-orange-700 hover:from-yellow-500 hover:to-orange-600 text-white py-4 px-6 rounded-xl font-bold transition-all transform hover:scale-105 border-2 border-yellow-400 shadow-lg" style="box-shadow: 0 0 20px rgba(255, 119, 0, 0.3);"> | |
| <span class="text-xl mr-2">🟡</span>CYBER OPERATIVE | |
| <div class="text-xs opacity-75">Advanced AI Systems</div> | |
| </button> | |
| <button onclick="startGame('hard')" class="w-full difficulty-btn bg-gradient-to-r from-red-600 to-pink-700 hover:from-red-500 hover:to-pink-600 text-white py-4 px-6 rounded-xl font-bold transition-all transform hover:scale-105 border-2 border-red-400 shadow-lg" style="box-shadow: 0 0 20px rgba(255, 0, 68, 0.3);"> | |
| <span class="text-xl mr-2">🔴</span>NEXUS OVERLORD | |
| <div class="text-xs opacity-75">Expert AI Dominion</div> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="text-center mt-8"> | |
| <p class="text-gray-400 text-xs">Enter the grid and command your digital forces</p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Player Info Header --> | |
| <div class="flex justify-between items-center mb-2 compact-section rounded-lg p-2"> | |
| <div id="player-info" class="flex items-center space-x-2 bg-cyan-900/20 rounded-lg p-2"> | |
| <div id="player-profile-icon" class="profile-icon"> | |
| <svg class="text-cyan-400" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-2-9h4v2h-2v2h2v2h-4v-2h2v-2h-2v-2zm-2-2h-2V7h2v2zm6 0V7h2v2h-2z"/></svg> | |
| </div> | |
| <div class="text-cyan-400"> | |
| <div class="text-xs font-bold">PLAYER 1</div> | |
| <div class="stats-compact"> | |
| <span id="p1-health" class="text-sm font-bold">10</span> | |
| <div class="w-16 bg-gray-700 rounded h-1"><div id="p1-health-bar" class="health-bar w-full h-full"></div></div> | |
| <span class="text-xs">⚡<span id="p1-mana" class="text-cyan-400 font-bold">3</span></span> | |
| <span class="text-xs">✨<span id="p1-exp" class="text-purple-400 font-bold">0</span></span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="text-center"> | |
| <div class="text-xs text-gray-300">TURN: <span id="current-turn" class="text-white font-bold">Player 1</span></div> | |
| <div class="text-xs text-gray-300">TURN #<span id="turn-counter" class="text-white font-bold">1</span></div> | |
| <div class="flex items-center gap-2 mt-1"> | |
| <button id="end-turn-btn" class="bg-purple-600 hover:bg-purple-700 px-3 py-1 rounded text-xs font-bold transition-colors border border-purple-400">END TURN</button> | |
| <button id="music-toggle" class="bg-gray-600 hover:bg-gray-700 px-2 py-1 rounded text-xs transition-colors border border-gray-500">🔊</button> | |
| </div> | |
| </div> | |
| <div id="opponent-info" class="flex items-center space-x-2 bg-pink-900/20 rounded-lg p-2"> | |
| <div class="text-pink-400 text-right"> | |
| <div class="text-xs font-bold">PLAYER 2</div> | |
| <div class="stats-compact"> | |
| <span class="text-xs">✨<span id="p2-exp" class="text-purple-400 font-bold">0</span></span> | |
| <span class="text-xs">⚡<span id="p2-mana" class="text-pink-400 font-bold">3</span></span> | |
| <div class="w-16 bg-gray-700 rounded h-1"><div id="p2-health-bar" class="health-bar w-full h-full"></div></div> | |
| <span id="p2-health" class="text-sm font-bold">10</span> | |
| </div> | |
| </div> | |
| <div id="opponent-profile-icon" class="profile-icon"> | |
| <svg class="text-pink-400" fill="currentColor" viewBox="0 0 24 24"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Main Game Area --> | |
| <div class="flex flex-col flex-1"> | |
| <!-- 3D Game Board --> | |
| <div class="board-container flex-1 relative"> | |
| <canvas id="game-canvas"></canvas> | |
| <!-- UI Overlays --> | |
| <div id="nexus-info" style="position: absolute; top: 10px; left: 10px; background: rgba(0,0,0,0.8); padding: 8px; border-radius: 4px; font-size: 12px; border: 1px solid #00f0ff;"> | |
| <div>Enemy Nexus HP: <span id="enemy-nexus-health">10</span>/10</div> | |
| </div> | |
| <!-- Chest Info --> | |
| <div id="chest-info" style="position: absolute; top: 10px; right: 10px; background: rgba(0,0,0,0.8); padding: 8px; border-radius: 4px; font-size: 12px; border: 1px solid #ffd700;"> | |
| <div>Chests: <span id="chest-count">0</span> remaining</div> | |
| </div> | |
| </div> | |
| <!-- Battle Log --> | |
| <div class="mb-2"> | |
| <h4 class="text-xs font-bold text-cyan-300 mb-1">📜 BATTLE LOG</h4> | |
| <div id="battle-log" class="bg-gray-900 rounded p-2 h-12 overflow-y-auto text-xs text-gray-300 border border-gray-700"></div> | |
| </div> | |
| <!-- Player Hand - Fanned Display --> | |
| <div class="hand-fan"> | |
| <div id="p1-hand" class="flex justify-center items-end"></div> | |
| </div> | |
| <!-- Deck Info --> | |
| <div class="text-center text-xs text-cyan-300 compact-section rounded p-1 mt-2 border border-cyan-800"> | |
| 📚: <span id="p1-deck-count">50</span> | 📚: <span id="p2-deck-count">50</span> (Opponent) | |
| </div> | |
| </div> | |
| <!-- Card Detail Modal --> | |
| <div id="card-detail-modal" class="fixed inset-0 bg-black bg-opacity-70 hidden flex items-center justify-center z-50 card-detail-modal"> | |
| <div class="bg-gradient-to-br from-gray-800 to-gray-900 rounded-lg p-6 max-w-sm w-full mx-4 border-2 border-cyan-400 modal-enter" style="max-height: 90vh; overflow-y: auto;"> | |
| <div id="card-detail-content"></div> | |
| <div class="flex space-x-3 mt-4"> | |
| <button id="play-card-btn" class="flex-1 bg-cyan-600 hover:bg-cyan-700 px-4 py-2 rounded-lg font-bold transition-colors border border-cyan-400">🎯 Play Card</button> | |
| <button onclick="hideCardDetailModal()" class="flex-1 bg-gray-600 hover:bg-gray-700 px-4 py-2 rounded-lg transition-colors border border-gray-500">❌ Cancel</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Piece Menu --> | |
| <div id="piece-menu" class="piece-menu" style="display: none;"></div> | |
| <!-- Chest Loot Modal --> | |
| <div id="chest-loot-modal" class="fixed inset-0 bg-black bg-opacity-70 hidden flex items-center justify-center z-50 card-detail-modal"> | |
| <div class="bg-gradient-to-br from-gray-800 to-gray-900 rounded-lg p-6 max-w-sm w-full mx-4 border-2 border-yellow-500 modal-enter" style="max-height: 90vh; overflow-y: auto;"> | |
| <div id="chest-loot-content"></div> | |
| <div class="flex space-x-3 mt-4"> | |
| <button onclick="hideChestLootModal()" class="flex-1 bg-yellow-600 hover:bg-yellow-700 px-4 py-2 rounded-lg font-bold transition-colors text-black border border-yellow-400">🎁 Take Loot</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Game state and configuration | |
| let gameState = {}; | |
| let soundEnabled = true; | |
| let selectedCardIndex = null; | |
| let selectedCardPlayer = null; | |
| let currentOscillators = []; | |
| let musicPlaying = false; | |
| let audioContext; | |
| let selectedPieceForMenu = null; | |
| // Three.js variables | |
| let scene, camera, renderer, raycaster, mouse; | |
| let boardSquares = []; | |
| let pieces3D = []; | |
| let nexusCores = []; | |
| let chests3D = []; | |
| let defenseTowers = []; | |
| let walls3D = []; | |
| let boardGroup; | |
| let selectedPiece3D = null; | |
| let validMoveSquares = []; | |
| // Expanded board size with stairway areas | |
| const BOARD_ROWS = 14; // 10 main board + 2 stairway rows per player | |
| const BOARD_COLS = 10; | |
| const SQUARE_SIZE = 1; | |
| const SQUARE_SPACING = 1.1; | |
| // Defense tower positions (outside main board) | |
| const TOWER_POSITIONS = [ | |
| { row: -1, col: 3, player: 1 }, { row: -1, col: 6, player: 1 }, | |
| { row: BOARD_ROWS, col: 3, player: 2 }, { row: BOARD_ROWS, col: 6, player: 2 } | |
| ]; | |
| const CARD_TYPES = { PIECE: 'piece', INSTANT: 'instant', SUMMON: 'summon', ENHANCEMENT: 'enhancement', HEALING: 'healing', EXP_BOOST: 'exp_boost', GEAR: 'gear' }; | |
| // Chest types | |
| const CHEST_TYPES = { | |
| LEGENDARY: { name: 'Legendary Chest', color: 0xffd700, glow: 0xffaa00, rarity: 'legendary', minItems: 2, maxItems: 3 }, | |
| EPIC: { name: 'Epic Chest', color: 0xff7700, glow: 0xff4400, rarity: 'epic', minItems: 2, maxItems: 3 }, | |
| RARE: { name: 'Rare Chest', color: 0xc0c0c0, glow: 0xffffff, rarity: 'rare', minItems: 1, maxItems: 2 } | |
| }; | |
| // Gear items | |
| const GEAR_ITEMS = { | |
| WEAPONS: [ | |
| { name: 'Quantum Blade', type: 'weapon', attack: 3, rarity: 'legendary', description: 'A blade that cuts through dimensions', flavor: '"Reality is just another layer to slice through."' }, | |
| { name: 'Plasma Saber', type: 'weapon', attack: 2, rarity: 'epic', description: 'High-energy plasma weapon', flavor: '"Hot enough to melt through any defense."' }, | |
| { name: 'Nano Sword', type: 'weapon', attack: 1, rarity: 'rare', description: 'Molecular-edged blade', flavor: '"Sharpened to the atomic level."' }, | |
| { name: 'Data Dagger', type: 'weapon', attack: 1, rarity: 'rare', description: 'Quick digital strike weapon', flavor: '"Information is the deadliest weapon."' } | |
| ], | |
| ARMOR: [ | |
| { name: 'Quantum Shield', type: 'armor', defense: 3, rarity: 'legendary', description: 'Shield that exists in multiple dimensions', flavor: '"Can block attacks from any reality."' }, | |
| { name: 'Plasma Barrier', type: 'armor', defense: 2, rarity: 'epic', description: 'Energy-based defensive field', flavor: '"Nothing gets through this wall of pure energy."' }, | |
| { name: 'Nano Mesh', type: 'armor', defense: 1, rarity: 'rare', description: 'Self-repairing protective layer', flavor: '"Adapts to any threat automatically."' }, | |
| { name: 'Data Cloak', type: 'armor', defense: 1, rarity: 'rare', description: 'Information-scrambling defense', flavor: '"Confuses enemy targeting systems."' } | |
| ] | |
| }; | |
| // Special chest-only cards | |
| const CHEST_CARDS = [ | |
| { name: '⚡ Time Warp', cost: 0, rarity: 'legendary', description: '🕒 Take an extra turn immediately.', flavor: '"Time bends to your will."', effect_desc: 'Take an additional turn after this one' }, | |
| { name: '🌀 Black Hole', cost: 0, rarity: 'legendary', description: '🕳️ Remove all enemy pieces from a 3x3 area.', flavor: '"The ultimate reset button."', effect_desc: 'Destroy all pieces in target area' }, | |
| { name: '✨ Phoenix Protocol', cost: 0, rarity: 'epic', description: '🔥 Resurrect your last destroyed piece at full health.', flavor: '"From the ashes, we rise again."', effect_desc: 'Revive last destroyed friendly piece' }, | |
| { name: '🛡️ Impervious Matrix', cost: 0, rarity: 'epic', description: '💎 Make your Nexus invulnerable for 2 turns.', flavor: '"Absolute protection."', effect_desc: 'Nexus takes no damage for 2 turns' }, | |
| { name: '⚙️ Overclock', cost: 0, rarity: 'rare', description: '🚀 Double the attack of all your pieces for one turn.', flavor: '"Pushing systems beyond their limits."', effect_desc: 'Double attack for all friendly pieces' }, | |
| { name: '🔋 Power Core', cost: 0, rarity: 'rare', description: '💡 Gain 5 permanent maximum mana.', flavor: '"Unlimited power!"', effect_desc: 'Increase max mana by 5' } | |
| ]; | |
| const PIECE_TYPES = { | |
| NEXUS_CORE: { name: '🏛️ Nexus Core', symbol: '♔', attack: 0, defense: 10, cost: 0, moves: 'none', rarity: 'legendary', description: '💎 The heart of your digital empire. Its destruction means total system failure.', flavor: '"In the grid, all paths lead to the Core."' }, | |
| DATA_EMPRESS: { name: '👑 Data Empress', symbol: '♕', attack: 9, defense: 9, cost: 9, moves: 'queen', rarity: 'legendary', description: '✨ Supreme digital sovereign with unlimited network access and devastating power.', flavor: '"She commands the datastreams like a symphony."' }, | |
| FORTRESS_NODE: { name: '🏰 Fortress Node', symbol: '♖', attack: 5, defense: 5, cost: 5, moves: 'rook', rarity: 'rare', description: '🛡️ Heavily fortified server cluster. Moves in straight lines, crushing everything in its path.', flavor: '"Impenetrable walls in cyberspace."' }, | |
| SIGNAL_HACKER: { name: '🔮 Signal Hacker', symbol: '♗', attack: 3, defense: 3, cost: 3, moves: 'bishop', rarity: 'common', description: '📡 Expert in diagonal data manipulation. Slips through network defenses at odd angles.', flavor: '"The shortest path is rarely straight."' }, | |
| CYBER_STALKER: { name: '🐴 Cyber Stalker', symbol: '♘', attack: 3, defense: 3, cost: 3, moves: 'knight', rarity: 'common', description: '⚡ Unpredictable digital hunter. Its erratic movement patterns confuse enemy algorithms.', flavor: '"Two steps forward, one step sideways."' }, | |
| BYTE_PAWN: { name: '🟦 Byte Pawn', symbol: '♙', attack: 1, defense: 1, cost: 1, moves: 'pawn', rarity: 'common', description: '🧩 Basic data packet soldier. Moves forward and attacks diagonally up to 2 spaces.', flavor: '"Small but essential."' }, | |
| STASIS_GUARD: { name: '🔒 Stasis Guard', symbol: '⬟', attack: 0, defense: 1, cost: 0, moves: 'pawn', rarity: 'special', description: '🛡️ Temporary defense matrix. Dissolves after first impact or temporal decay.', flavor: '"A barrier of frozen time."' }, | |
| DEFENSE_TOWER: { name: '🗼 Defense Tower', symbol: '🗼', attack: 2, defense: 4, cost: 3, moves: 'none', rarity: 'rare', description: '🎯 Automated defense system. Attacks adjacent enemy pieces each turn.', flavor: '"Always watching, always ready."' } | |
| }; | |
| const INSTANT_CARDS = [ | |
| { name: '🛡️ Firewall Boost', cost: 2, effect: 'shield', rarity: 'common', description: '🛡️ Temporarily increase defense of a piece.', flavor: '"Stronger walls for a short time."', effect_desc: 'Add +2 defense to target piece this turn' }, | |
| { name: '💥 Data Spike', cost: 3, effect: 'damage', rarity: 'common', description: '⚡ Deliver a sudden burst of damage to an enemy piece.', flavor: '"A sharp jab to their system."', effect_desc: 'Deal 3 damage to target piece' }, | |
| { name: '🔄 Control Override', cost: 5, effect: 'control', rarity: 'legendary', description: '🕹️ Take control of an enemy piece for one turn.', flavor: '"Your pawn becomes mine."', effect_desc: 'Control target enemy piece for 1 turn' }, | |
| { name: '🚀 Speed Hack', cost: 3, effect: 'buff', rarity: 'rare', description: '⚡ Increase movement for a piece.', flavor: '"Faster than the blink of an eye."', effect_desc: 'Target piece can move twice this turn' }, | |
| { name: '🔍 Security Breach', cost: 1, effect: 'draw', rarity: 'common', description: '📄 Access restricted data archives for additional resources.', flavor: '"Knowledge is the ultimate weapon."', effect_desc: 'Draw 2 additional cards from your deck' }, | |
| { name: '⚡ Power Surge', cost: 3, effect: 'mana', rarity: 'common', description: '🔋 Redirect energy from auxiliary systems.', flavor: '"More power to the processing cores!"', effect_desc: 'Gain +2 mana at start of next turn' }, | |
| { name: '💀 System Crash', cost: 4, effect: 'destroy', rarity: 'rare', description: '🔥 Cause catastrophic system failure in target piece.', flavor: '"Error 404: Piece not found."', effect_desc: 'Destroy target piece with defense 3 or less' }, | |
| { name: '🔗 Neural Link', cost: 2, effect: 'bond', rarity: 'rare', description: '🧠 Create psychic connection between two pieces.', flavor: '"Minds think as one."', effect_desc: 'Two target pieces share damage taken' } | |
| ]; | |
| const SUMMON_CARDS = [ | |
| { name: '📡 Deploy Protocol', cost: 1, rarity: 'common', description: '🚀 Initialize rapid deployment sequence for lightweight units.', flavor: '"Small programs, big impact."', effect_desc: 'Summon any piece costing 3 mana or less for free' }, | |
| { name: '♻️ Emergency Backup', cost: 2, rarity: 'rare', description: '💾 Restore deleted programs from backup archives.', flavor: '"Nothing is ever truly lost in the digital realm."', effect_desc: 'Return any destroyed piece from discard pile to hand' }, | |
| { name: '⚡ Resource Allocation', cost: 0, rarity: 'common', description: '🔄 Optimize system resources for maximum efficiency.', flavor: '"Work smarter, not harder."', effect_desc: 'Gain +2 mana immediately this turn' }, | |
| { name: '🧠 Neural Link', cost: 1, rarity: 'common', description: '📡 Establish direct interface with data repositories.', flavor: '"Direct download to consciousness."', effect_desc: 'Draw 3 cards from your deck' }, | |
| { name: '🏰 Digital Fortress', cost: 2, rarity: 'common', description: '🛡️ Reinforce network security across all nodes.', flavor: '"Strong walls make strong empires."', effect_desc: 'All friendly pieces gain +1 defense permanently' }, | |
| { name: '🌀 Quantum Surge', cost: 4, rarity: 'legendary', description: '⚛️ Harness quantum fluctuations for massive power boost.', flavor: '"Reality bends to digital will."', effect_desc: 'All friendly pieces gain +2 attack and +1 defense this turn' } | |
| ]; | |
| const ENHANCEMENT_CARDS = [ | |
| { name: '⚔️ Combat Protocol', cost: 2, rarity: 'common', description: '🗡️ Upload advanced combat algorithms to target piece.', flavor: '"Precision strikes through superior code."', effect_desc: 'Target piece gains +2 attack permanently' }, | |
| { name: '🛡️ Armor Plating', cost: 2, rarity: 'common', description: '🔧 Install additional defensive layers.', flavor: '"Better safe than sorry."', effect_desc: 'Target piece gains +2 defense permanently' }, | |
| { name: '🔄 Adaptive Core', cost: 3, rarity: 'rare', description: '🧬 Grant piece ability to evolve and adapt.', flavor: '"Evolution through digital selection."', effect_desc: 'Target piece gains +1 attack and +1 defense each turn' }, | |
| { name: '👁️ Omniscience Net', cost: 4, rarity: 'legendary', description: '🌐 Connect piece to global surveillance network.', flavor: '"All seeing, all knowing."', effect_desc: 'Target piece can see and attack any enemy piece on board' } | |
| ]; | |
| const HEALING_CARDS = [ | |
| { name: '💚 Nano Repair', cost: 2, rarity: 'common', description: '🔧 Deploy microscopic repair bots to fix damaged systems.', flavor: '"Healing from the inside out."', effect_desc: 'Restore 3 health to target piece' }, | |
| { name: '✨ System Restore', cost: 4, rarity: 'rare', description: '💽 Restore piece to previous backup state.', flavor: '"Like it never happened."', effect_desc: 'Fully heal target piece and remove debuffs' }, | |
| { name: '🌟 Mass Regeneration', cost: 6, rarity: 'legendary', description: '🔄 Network-wide healing protocol.', flavor: '"All systems nominal."', effect_desc: 'Heal all friendly pieces by 2 health' }, | |
| { name: '⚕️ Emergency Medical', cost: 1, rarity: 'common', description: '🚑 Quick field repairs for critical damage.', flavor: '"Patching the holes."', effect_desc: 'Heal target piece by 1 and give +1 defense this turn' } | |
| ]; | |
| const EXP_BOOST_CARDS = [ | |
| { name: '📈 Data Mining', cost: 2, rarity: 'common', description: '⛏️ Extract valuable experience from system operations.', flavor: '"Knowledge is power."', effect_desc: 'Gain 10 experience points immediately' }, | |
| { name: '🎓 Training Protocol', cost: 3, rarity: 'rare', description: '📚 Accelerated learning subroutines.', flavor: '"Practice makes perfect."', effect_desc: 'Gain 15 experience and draw 1 card' }, | |
| { name: '💎 Master Code', cost: 5, rarity: 'legendary', description: '🧠 Access to advanced programming techniques.', flavor: '"Transcendent understanding."', effect_desc: 'Gain 25 experience and upgrade target piece level' }, | |
| { name: '⚡ Quick Study', cost: 1, rarity: 'common', description: '📖 Rapid information absorption.', flavor: '"Learning at light speed."', effect_desc: 'Gain 5 experience points' } | |
| ]; | |
| // Audio system | |
| function initAudio() { | |
| if (!audioContext) { | |
| audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
| } | |
| } | |
| function playBackgroundMusic() { | |
| if (!soundEnabled || musicPlaying) return; | |
| initAudio(); | |
| musicPlaying = true; | |
| // Create layered ambient cyberpunk music | |
| const createOscillatorLayer = (freq, type, gain, delay = 0) => { | |
| setTimeout(() => { | |
| if (!musicPlaying) return; | |
| const osc = audioContext.createOscillator(); | |
| const gainNode = audioContext.createGain(); | |
| const filter = audioContext.createBiquadFilter(); | |
| osc.connect(filter); | |
| filter.connect(gainNode); | |
| gainNode.connect(audioContext.destination); | |
| osc.frequency.value = freq; | |
| osc.type = type; | |
| filter.type = 'lowpass'; | |
| filter.frequency.value = 800; | |
| gainNode.gain.setValueAtTime(0, audioContext.currentTime); | |
| gainNode.gain.linearRampToValueAtTime(gain, audioContext.currentTime + 2); | |
| // Add subtle frequency modulation | |
| const lfo = audioContext.createOscillator(); | |
| const lfoGain = audioContext.createGain(); | |
| lfo.connect(lfoGain); | |
| lfoGain.connect(osc.frequency); | |
| lfo.frequency.value = 0.2; | |
| lfoGain.gain.value = 5; | |
| osc.start(); | |
| lfo.start(); | |
| currentOscillators.push({ osc, lfo, gainNode }); | |
| }, delay); | |
| }; | |
| // Create multiple layers for rich ambient sound | |
| createOscillatorLayer(110, 'sine', 0.02, 0); // Bass drone | |
| createOscillatorLayer(220, 'triangle', 0.015, 1000); // Mid layer | |
| createOscillatorLayer(440, 'sawtooth', 0.008, 2000); // High layer | |
| createOscillatorLayer(330, 'sine', 0.012, 3000); // Harmony | |
| // Restart the music loop | |
| setTimeout(() => { | |
| if (musicPlaying) { | |
| stopCurrentOscillators(); | |
| playBackgroundMusic(); | |
| } | |
| }, 12000); | |
| } | |
| function stopCurrentOscillators() { | |
| currentOscillators.forEach(({ osc, lfo, gainNode }) => { | |
| try { | |
| gainNode.gain.linearRampToValueAtTime(0, audioContext.currentTime + 1); | |
| setTimeout(() => { | |
| osc.stop(); | |
| lfo.stop(); | |
| }, 1000); | |
| } catch (e) {} | |
| }); | |
| currentOscillators = []; | |
| } | |
| function stopBackgroundMusic() { | |
| musicPlaying = false; | |
| stopCurrentOscillators(); | |
| } | |
| function toggleMusic() { | |
| soundEnabled = !soundEnabled; | |
| const btn = document.getElementById('music-toggle'); | |
| btn.textContent = soundEnabled ? '🔊' : '🔇'; | |
| if (soundEnabled) { | |
| playBackgroundMusic(); | |
| } else { | |
| stopBackgroundMusic(); | |
| } | |
| } | |
| function playSound(soundId) { | |
| if (!soundEnabled) return; | |
| initAudio(); | |
| const oscillator = audioContext.createOscillator(); | |
| const gainNode = audioContext.createGain(); | |
| oscillator.connect(gainNode); | |
| gainNode.connect(audioContext.destination); | |
| oscillator.frequency.value = soundId === 'cardPlay' ? 800 : soundId === 'pieceMove' ? 600 : soundId === 'battle' ? 400 : soundId === 'chest' ? 1000 : 500; | |
| oscillator.type = 'sine'; | |
| gainNode.gain.setValueAtTime(0.1, audioContext.currentTime); | |
| gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.2); | |
| oscillator.start(audioContext.currentTime); | |
| oscillator.stop(audioContext.currentTime + 0.2); | |
| } | |
| // Enhanced particle system | |
| function createParticles() { | |
| const container = document.getElementById('particles'); | |
| const colors = ['', 'pink', 'green', 'purple', 'teal', 'orange', 'violet']; | |
| for (let i = 0; i < 40; i++) { | |
| const p = document.createElement('div'); | |
| const colorClass = colors[Math.floor(Math.random() * colors.length)]; | |
| p.className = `particle ${colorClass}`; | |
| p.style.left = Math.random() * 100 + '%'; | |
| p.style.top = Math.random() * 100 + '%'; | |
| p.style.animationDelay = Math.random() * 12 + 's'; | |
| p.style.animationDuration = (12 + Math.random() * 6) + 's'; | |
| container.appendChild(p); | |
| } | |
| } | |
| // Three.js setup | |
| function init3D() { | |
| const canvas = document.getElementById('game-canvas'); | |
| const container = canvas.parentElement; | |
| // Scene setup | |
| scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0x050515); | |
| // Camera setup | |
| camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000); | |
| camera.position.set(0, 10, 10); | |
| camera.lookAt(0, 0, 0); | |
| // Renderer setup | |
| renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true }); | |
| renderer.setSize(container.clientWidth, container.clientHeight); | |
| renderer.shadowMap.enabled = true; | |
| renderer.shadowMap.type = THREE.PCFSoftShadowMap; | |
| // Raycaster for mouse interaction | |
| raycaster = new THREE.Raycaster(); | |
| mouse = new THREE.Vector2(); | |
| // Lighting - more vibrant colors | |
| const ambientLight = new THREE.AmbientLight(0x404040, 0.6); | |
| scene.add(ambientLight); | |
| const directionalLight = new THREE.DirectionalLight(0xcc00ff, 0.9); | |
| directionalLight.position.set(5, 12, 5); | |
| directionalLight.castShadow = true; | |
| directionalLight.shadow.mapSize.width = 2048; | |
| directionalLight.shadow.mapSize.height = 2048; | |
| scene.add(directionalLight); | |
| const pointLight = new THREE.PointLight(0x00ffcc, 0.7, 25); | |
| pointLight.position.set(-5, 8, -5); | |
| scene.add(pointLight); | |
| // Create board | |
| createBoard3D(); | |
| // Mouse event handlers | |
| canvas.addEventListener('mousemove', onMouseMove, false); | |
| canvas.addEventListener('click', onMouseClick, false); | |
| // Handle window resize | |
| window.addEventListener('resize', onWindowResize, false); | |
| // Start animation loop | |
| animate(); | |
| } | |
| function createBoard3D() { | |
| boardGroup = new THREE.Group(); | |
| scene.add(boardGroup); | |
| // Create board squares with stairway effect | |
| for (let row = 0; row < BOARD_ROWS; row++) { | |
| boardSquares[row] = []; | |
| for (let col = 0; col < BOARD_COLS; col++) { | |
| const square = createBoardSquare(row, col); | |
| boardSquares[row][col] = square; | |
| boardGroup.add(square); | |
| } | |
| } | |
| // Create nexus cores | |
| createNexusCores(); | |
| // Create defense towers | |
| createDefenseTowers(); | |
| // Create random walls | |
| createRandomWalls(); | |
| } | |
| function createBoardSquare(row, col) { | |
| // Hexagonal board square | |
| const geometry = new THREE.CylinderGeometry(0.4, 0.4, 0.1, 6); | |
| // More vibrant alternating colors with stairway effect | |
| const isLight = (row + col) % 2 === 0; | |
| let color, yPos, emissive; | |
| // Stairway effect for player bases | |
| if (row < 2) { // Player 1 stairway | |
| color = isLight ? 0x1a1a4a : 0x0f0f3f; | |
| yPos = row === 0 ? 0.3 : 0.15; // Higher for row 0 | |
| emissive = isLight ? 0x0a0a33 : 0x050522; | |
| } else if (row >= BOARD_ROWS - 2) { // Player 2 stairway | |
| color = isLight ? 0x4a1a1a : 0x3f0f0f; | |
| yPos = row === BOARD_ROWS - 1 ? 0.3 : 0.15; // Higher for last row | |
| emissive = isLight ? 0x330a0a : 0x220505; | |
| } else { // Main board | |
| color = isLight ? 0x2a1a4a : 0x1a0f3f; | |
| yPos = 0; | |
| emissive = isLight ? 0x1a0033 : 0x0d0022; | |
| } | |
| const material = new THREE.MeshPhongMaterial({ | |
| color: color, | |
| emissive: emissive, | |
| transparent: true, | |
| opacity: 0.9 | |
| }); | |
| const square = new THREE.Mesh(geometry, material); | |
| square.position.set( | |
| (col - BOARD_COLS / 2 + 0.5) * SQUARE_SPACING, | |
| yPos, | |
| (row - BOARD_ROWS / 2 + 0.5) * SQUARE_SPACING | |
| ); | |
| square.receiveShadow = true; | |
| // Store board coordinates | |
| square.userData = { row, col, type: 'square' }; | |
| return square; | |
| } | |
| function createNexusCores() { | |
| // Player 1 Nexus (bottom) - positioned at the end of the stairway | |
| const nexus1 = createNexus(1); | |
| nexus1.position.set(0, 0.5, (BOARD_ROWS / 2 + 1) * SQUARE_SPACING); | |
| nexusCores[1] = nexus1; | |
| scene.add(nexus1); | |
| // Player 2 Nexus (top) - positioned at the end of the stairway | |
| const nexus2 = createNexus(2); | |
| nexus2.position.set(0, 0.5, -(BOARD_ROWS / 2 + 1) * SQUARE_SPACING); | |
| nexusCores[2] = nexus2; | |
| scene.add(nexus2); | |
| } | |
| function createNexus(player) { | |
| const group = new THREE.Group(); | |
| // Main nexus structure with more refined design | |
| const geometry = new THREE.OctahedronGeometry(0.8); | |
| const color = player === 1 ? 0x00ffcc : 0xff00aa; | |
| const material = new THREE.MeshPhongMaterial({ | |
| color: color, | |
| emissive: color, | |
| emissiveIntensity: 0.6, | |
| transparent: true, | |
| opacity: 0.9 | |
| }); | |
| const nexus = new THREE.Mesh(geometry, material); | |
| nexus.position.y = 0.75; | |
| nexus.castShadow = true; | |
| nexus.userData = { type: 'nexus', player: player }; | |
| group.add(nexus); | |
| // Add pulsing glow effect | |
| const glowGeometry = new THREE.SphereGeometry(1.2, 16, 16); | |
| const glowMaterial = new THREE.MeshBasicMaterial({ | |
| color: color, | |
| transparent: true, | |
| opacity: 0.2 | |
| }); | |
| const glow = new THREE.Mesh(glowGeometry, glowMaterial); | |
| glow.position.y = 0.75; | |
| group.add(glow); | |
| // Add energy core | |
| const coreGeometry = new THREE.SphereGeometry(0.3, 12, 12); | |
| const coreMaterial = new THREE.MeshBasicMaterial({ | |
| color: 0xffffff, | |
| emissive: 0xffffff, | |
| emissiveIntensity: 0.8 | |
| }); | |
| const core = new THREE.Mesh(coreGeometry, coreMaterial); | |
| core.position.y = 0.75; | |
| group.add(core); | |
| return group; | |
| } | |
| function createDefenseTowers() { | |
| TOWER_POSITIONS.forEach(towerPos => { | |
| const tower = createTower3D(towerPos.player, towerPos.row, towerPos.col); | |
| defenseTowers.push(tower); | |
| scene.add(tower); | |
| }); | |
| } | |
| function createTower3D(player, row, col) { | |
| const group = new THREE.Group(); | |
| const towerColor = player === 1 ? 0x00ffcc : 0xff00aa; | |
| // Tower base | |
| const baseGeometry = new THREE.CylinderGeometry(0.4, 0.5, 0.3, 8); | |
| const baseMaterial = new THREE.MeshPhongMaterial({ | |
| color: towerColor, | |
| emissive: towerColor, | |
| emissiveIntensity: 0.3 | |
| }); | |
| const base = new THREE.Mesh(baseGeometry, baseMaterial); | |
| base.position.y = 0.15; | |
| base.castShadow = true; | |
| group.add(base); | |
| // Tower body | |
| const bodyGeometry = new THREE.CylinderGeometry(0.3, 0.4, 1.5, 8); | |
| const bodyMaterial = new THREE.MeshPhongMaterial({ | |
| color: towerColor, | |
| emissive: towerColor, | |
| emissiveIntensity: 0.2 | |
| }); | |
| const body = new THREE.Mesh(bodyGeometry, bodyMaterial); | |
| body.position.y = 1.0; | |
| body.castShadow = true; | |
| group.add(body); | |
| // Tower top with cannon | |
| const topGeometry = new THREE.CylinderGeometry(0.2, 0.3, 0.5, 6); | |
| const top = new THREE.Mesh(topGeometry, bodyMaterial); | |
| top.position.y = 1.8; | |
| group.add(top); | |
| // Cannon barrel | |
| const barrelGeometry = new THREE.CylinderGeometry(0.08, 0.08, 0.8, 6); | |
| const barrelMaterial = new THREE.MeshPhongMaterial({ | |
| color: 0x333333, | |
| emissive: 0x111111 | |
| }); | |
| const barrel = new THREE.Mesh(barrelGeometry, barrelMaterial); | |
| barrel.position.y = 1.8; | |
| barrel.position.z = player === 1 ? 0.5 : -0.5; | |
| barrel.rotation.x = Math.PI / 2; | |
| group.add(barrel); | |
| // Position tower | |
| group.position.set( | |
| (col - BOARD_COLS / 2 + 0.5) * SQUARE_SPACING, | |
| 0, | |
| (row - BOARD_ROWS / 2 + 0.5) * SQUARE_SPACING | |
| ); | |
| // Store tower data | |
| group.userData = { | |
| type: 'defense_tower', | |
| player: player, | |
| row: row, | |
| col: col, | |
| attack: () => Math.floor(Math.random() * 3) + 1 // 1-3 damage | |
| }; | |
| return group; | |
| } | |
| function createRandomWalls() { | |
| const wallCount = 8; // Number of random walls to generate | |
| for (let i = 0; i < wallCount; i++) { | |
| let row, col; | |
| let attempts = 0; | |
| // Find an empty spot for the wall (avoid edges and player bases) | |
| do { | |
| row = Math.floor(Math.random() * (BOARD_ROWS - 4)) + 2; | |
| col = Math.floor(Math.random() * (BOARD_COLS - 2)) + 1; | |
| attempts++; | |
| } while ((gameState.board[row][col] !== null || | |
| isNearNexus(row, col) || | |
| walls3D.some(w => w.userData.row === row && w.userData.col === col)) && | |
| attempts < 100); | |
| if (attempts < 100) { | |
| const wall = createWall3D(row, col); | |
| walls3D.push(wall); | |
| scene.add(wall); | |
| // Add wall to game state | |
| gameState.board[row][col] = { | |
| type: 'wall', | |
| owner: 0, // Neutral | |
| health: 5, | |
| isWall: true, | |
| id: Math.random().toString(36).substr(2, 9) | |
| }; | |
| } | |
| } | |
| } | |
| function isNearNexus(row, col) { | |
| // Check if position is too close to nexus (avoid blocking paths) | |
| const player1NexusRow = 0; | |
| const player2NexusRow = BOARD_ROWS - 1; | |
| const nexusCol = BOARD_COLS / 2; | |
| return (Math.abs(row - player1NexusRow) < 3 && Math.abs(col - nexusCol) < 3) || | |
| (Math.abs(row - player2NexusRow) < 3 && Math.abs(col - nexusCol) < 3); | |
| } | |
| function createWall3D(row, col) { | |
| const group = new THREE.Group(); | |
| // Wall base | |
| const wallGeometry = new THREE.BoxGeometry(0.8, 0.8, 0.8); | |
| const wallMaterial = new THREE.MeshPhongMaterial({ | |
| color: 0x555555, | |
| emissive: 0x222222, | |
| emissiveIntensity: 0.1 | |
| }); | |
| const wall = new THREE.Mesh(wallGeometry, wallMaterial); | |
| wall.position.y = 0.4; | |
| wall.castShadow = true; | |
| group.add(wall); | |
| // Wall details | |
| const detailGeometry = new THREE.BoxGeometry(0.6, 0.1, 0.6); | |
| const detailMaterial = new THREE.MeshPhongMaterial({ | |
| color: 0x777777, | |
| emissive: 0x333333 | |
| }); | |
| for (let i = 0; i < 3; i++) { | |
| const detail = new THREE.Mesh(detailGeometry, detailMaterial); | |
| detail.position.y = 0.2 + i * 0.2; | |
| group.add(detail); | |
| } | |
| // Position wall | |
| group.position.set( | |
| (col - BOARD_COLS / 2 + 0.5) * SQUARE_SPACING, | |
| 0, | |
| (row - BOARD_ROWS / 2 + 0.5) * SQUARE_SPACING | |
| ); | |
| // Store wall data | |
| group.userData = { | |
| type: 'wall', | |
| row: row, | |
| col: col | |
| }; | |
| return group; | |
| } | |
| function createChest3D(chestData, row, col) { | |
| const group = new THREE.Group(); | |
| // Chest base | |
| const baseGeometry = new THREE.BoxGeometry(0.5, 0.3, 0.4); | |
| const baseMaterial = new THREE.MeshPhongMaterial({ | |
| color: chestData.type.color, | |
| emissive: chestData.type.color, | |
| emissiveIntensity: 0.4 | |
| }); | |
| const base = new THREE.Mesh(baseGeometry, baseMaterial); | |
| base.position.y = 0.15; | |
| base.castShadow = true; | |
| group.add(base); | |
| // Chest lid | |
| const lidGeometry = new THREE.BoxGeometry(0.5, 0.1, 0.4); | |
| const lid = new THREE.Mesh(lidGeometry, baseMaterial); | |
| lid.position.y = 0.35; | |
| lid.castShadow = true; | |
| group.add(lid); | |
| // Chest lock | |
| const lockGeometry = new THREE.CylinderGeometry(0.05, 0.05, 0.05, 8); | |
| const lockMaterial = new THREE.MeshPhongMaterial({ | |
| color: 0x000000, | |
| emissive: chestData.type.glow, | |
| emissiveIntensity: 0.7 | |
| }); | |
| const lock = new THREE.Mesh(lockGeometry, lockMaterial); | |
| lock.position.y = 0.4; | |
| lock.position.z = 0.15; | |
| group.add(lock); | |
| // Position on board | |
| group.position.set( | |
| (col - BOARD_COLS / 2 + 0.5) * SQUARE_SPACING, | |
| 0, | |
| (row - BOARD_ROWS / 2 + 0.5) * SQUARE_SPACING | |
| ); | |
| // Store chest data | |
| group.userData = { | |
| type: 'chest', | |
| chestData: chestData, | |
| row: row, | |
| col: col | |
| }; | |
| return group; | |
| } | |
| function createPiece3D(pieceData, row, col) { | |
| const group = new THREE.Group(); | |
| // Base with more vibrant colors | |
| const baseGeometry = new THREE.CylinderGeometry(0.3, 0.3, 0.1, 8); | |
| const playerColor = pieceData.owner === 1 ? 0x00ffcc : 0xff00aa; | |
| const baseMaterial = new THREE.MeshPhongMaterial({ | |
| color: playerColor, | |
| emissive: playerColor, | |
| emissiveIntensity: 0.4 | |
| }); | |
| const base = new THREE.Mesh(baseGeometry, baseMaterial); | |
| base.position.y = 0.05; | |
| base.castShadow = true; | |
| group.add(base); | |
| // Chess-like piece body based on piece type with more refined designs | |
| let bodyGeometry; | |
| const pieceHeight = 0.8; | |
| switch (pieceData.pieceType) { | |
| case 'DATA_EMPRESS': // Queen | |
| // Create queen with refined crown design | |
| const queenGroup = new THREE.Group(); | |
| const queenBase = new THREE.CylinderGeometry(0.2, 0.25, 0.6, 10); | |
| const queenMaterial = new THREE.MeshPhongMaterial({ | |
| color: playerColor, | |
| emissive: playerColor, | |
| emissiveIntensity: 0.3, | |
| transparent: true, | |
| opacity: 0.9 | |
| }); | |
| const queenMesh = new THREE.Mesh(queenBase, queenMaterial); | |
| queenMesh.position.y = 0.3; | |
| // Crown details with more refinement | |
| const crownGeometry = new THREE.ConeGeometry(0.12, 0.25, 5); | |
| for (let i = 0; i < 5; i++) { | |
| const crownSpike = new THREE.Mesh(crownGeometry, queenMaterial); | |
| crownSpike.position.y = 0.6; | |
| crownSpike.rotation.y = (i * Math.PI * 2) / 5; | |
| crownSpike.position.x = 0.15 * Math.cos((i * Math.PI * 2) / 5); | |
| crownSpike.position.z = 0.15 * Math.sin((i * Math.PI * 2) / 5); | |
| queenGroup.add(crownSpike); | |
| } | |
| // Central gem | |
| const gemGeometry = new THREE.SphereGeometry(0.08, 8, 6); | |
| const gemMaterial = new THREE.MeshPhongMaterial({ | |
| color: 0xffffff, | |
| emissive: 0xffffff, | |
| emissiveIntensity: 0.5 | |
| }); | |
| const gem = new THREE.Mesh(gemGeometry, gemMaterial); | |
| gem.position.y = 0.7; | |
| queenGroup.add(gem); | |
| queenGroup.add(queenMesh); | |
| queenGroup.position.y = 0.4; | |
| group.add(queenGroup); | |
| break; | |
| case 'FORTRESS_NODE': // Rook | |
| // Create rook with more refined castle design | |
| const rookGeometry = new THREE.CylinderGeometry(0.22, 0.25, 0.7, 10); | |
| const rookMaterial = new THREE.MeshPhongMaterial({ | |
| color: playerColor, | |
| emissive: playerColor, | |
| emissiveIntensity: 0.3, | |
| transparent: true, | |
| opacity: 0.9 | |
| }); | |
| const rook = new THREE.Mesh(rookGeometry, rookMaterial); | |
| rook.position.y = 0.35; | |
| // Battlements with more detail | |
| const battlementGeometry = new THREE.BoxGeometry(0.12, 0.12, 0.12); | |
| for (let i = 0; i < 4; i++) { | |
| const battlement = new THREE.Mesh(battlementGeometry, rookMaterial); | |
| battlement.position.y = 0.7; | |
| battlement.position.x = 0.18 * Math.cos((i * Math.PI) / 2); | |
| battlement.position.z = 0.18 * Math.sin((i * Math.PI) / 2); | |
| group.add(battlement); | |
| } | |
| group.add(rook); | |
| break; | |
| case 'SIGNAL_HACKER': // Bishop | |
| // Create bishop with more refined mitre design | |
| const bishopGroup = new THREE.Group(); | |
| const bishopBase = new THREE.CylinderGeometry(0.15, 0.2, 0.4, 10); | |
| const bishopMaterial = new THREE.MeshPhongMaterial({ | |
| color: playerColor, | |
| emissive: playerColor, | |
| emissiveIntensity: 0.3, | |
| transparent: true, | |
| opacity: 0.9 | |
| }); | |
| const bishopBaseMesh = new THREE.Mesh(bishopBase, bishopMaterial); | |
| bishopBaseMesh.position.y = 0.2; | |
| // Bishop head with more refined shape | |
| const bishopHead = new THREE.ConeGeometry(0.1, 0.35, 8); | |
| const bishopHeadMesh = new THREE.Mesh(bishopHead, bishopMaterial); | |
| bishopHeadMesh.position.y = 0.55; | |
| // Slit in bishop head | |
| const slitGeometry = new THREE.BoxGeometry(0.06, 0.18, 0.02); | |
| const slitMaterial = new THREE.MeshPhongMaterial({ color: 0x000000 }); | |
| const slit = new THREE.Mesh(slitGeometry, slitMaterial); | |
| slit.position.y = 0.55; | |
| slit.position.z = 0.1; | |
| bishopGroup.add(bishopBaseMesh); | |
| bishopGroup.add(bishopHeadMesh); | |
| bishopGroup.add(slit); | |
| bishopGroup.position.y = 0.4; | |
| group.add(bishopGroup); | |
| break; | |
| case 'CYBER_STALKER': // Knight | |
| // Create knight with more refined horse design | |
| const knightGroup = new THREE.Group(); | |
| const knightBase = new THREE.CylinderGeometry(0.15, 0.18, 0.5, 10); | |
| const knightMaterial = new THREE.MeshPhongMaterial({ | |
| color: playerColor, | |
| emissive: playerColor, | |
| emissiveIntensity: 0.3, | |
| transparent: true, | |
| opacity: 0.9 | |
| }); | |
| const knightBaseMesh = new THREE.Mesh(knightBase, knightMaterial); | |
| knightBaseMesh.position.y = 0.25; | |
| // Horse head with more detail | |
| const headGeometry = new THREE.SphereGeometry(0.1, 10, 8); | |
| const headMesh = new THREE.Mesh(headGeometry, knightMaterial); | |
| headMesh.position.set(0.12, 0.6, 0.05); | |
| headMesh.scale.set(1.3, 1.1, 0.9); | |
| // Horse ears | |
| const earGeometry = new THREE.ConeGeometry(0.035, 0.1, 6); | |
| const leftEar = new THREE.Mesh(earGeometry, knightMaterial); | |
| leftEar.position.set(0.06, 0.7, 0.05); | |
| leftEar.rotation.x = -0.5; | |
| const rightEar = new THREE.Mesh(earGeometry, knightMaterial); | |
| rightEar.position.set(0.18, 0.7, 0.05); | |
| rightEar.rotation.x = -0.5; | |
| // Horse mane | |
| const maneGeometry = new THREE.BoxGeometry(0.08, 0.05, 0.1); | |
| const mane = new THREE.Mesh(maneGeometry, knightMaterial); | |
| mane.position.set(0.12, 0.65, -0.05); | |
| knightGroup.add(knightBaseMesh); | |
| knightGroup.add(headMesh); | |
| knightGroup.add(leftEar); | |
| knightGroup.add(rightEar); | |
| knightGroup.add(mane); | |
| knightGroup.position.y = 0.4; | |
| group.add(knightGroup); | |
| break; | |
| case 'DEFENSE_TOWER': | |
| // Tower with more refined cyberpunk elements | |
| const towerGeometry = new THREE.CylinderGeometry(0.15, 0.2, 1.0, 10); | |
| const towerMaterial = new THREE.MeshPhongMaterial({ | |
| color: playerColor, | |
| emissive: playerColor, | |
| emissiveIntensity: 0.3, | |
| transparent: true, | |
| opacity: 0.9 | |
| }); | |
| const tower = new THREE.Mesh(towerGeometry, towerMaterial); | |
| tower.position.y = 0.5; | |
| // Tower top with more detail | |
| const topGeometry = new THREE.CylinderGeometry(0.1, 0.15, 0.3, 8); | |
| const top = new THREE.Mesh(topGeometry, towerMaterial); | |
| top.position.y = 0.9; | |
| // Antenna with glow | |
| const antennaGeometry = new THREE.CylinderGeometry(0.02, 0.02, 0.3, 6); | |
| const antennaMaterial = new THREE.MeshPhongMaterial({ | |
| color: playerColor, | |
| emissive: playerColor, | |
| emissiveIntensity: 0.8 | |
| }); | |
| const antenna = new THREE.Mesh(antennaGeometry, antennaMaterial); | |
| antenna.position.y = 1.15; | |
| // Radar dish | |
| const dishGeometry = new THREE.SphereGeometry(0.08, 8, 4); | |
| const dish = new THREE.Mesh(dishGeometry, antennaMaterial); | |
| dish.position.y = 1.3; | |
| dish.scale.z = 0.3; | |
| group.add(tower); | |
| group.add(top); | |
| group.add(antenna); | |
| group.add(dish); | |
| break; | |
| default: // BYTE_PAWN and others - Standard pawn shape with refinement | |
| const pawnGroup = new THREE.Group(); | |
| const pawnBase = new THREE.CylinderGeometry(0.15, 0.18, 0.3, 10); | |
| const pawnMaterial = new THREE.MeshPhongMaterial({ | |
| color: playerColor, | |
| emissive: playerColor, | |
| emissiveIntensity: 0.3, | |
| transparent: true, | |
| opacity: 0.9 | |
| }); | |
| const pawnBaseMesh = new THREE.Mesh(pawnBase, pawnMaterial); | |
| pawnBaseMesh.position.y = 0.15; | |
| const pawnHead = new THREE.SphereGeometry(0.12, 10, 8); | |
| const pawnHeadMesh = new THREE.Mesh(pawnHead, pawnMaterial); | |
| pawnHeadMesh.position.y = 0.45; | |
| // Pawn collar | |
| const collarGeometry = new THREE.TorusGeometry(0.15, 0.02, 8, 12); | |
| const collar = new THREE.Mesh(collarGeometry, pawnMaterial); | |
| collar.position.y = 0.3; | |
| collar.rotation.x = Math.PI / 2; | |
| pawnGroup.add(pawnBaseMesh); | |
| pawnGroup.add(pawnHeadMesh); | |
| pawnGroup.add(collar); | |
| pawnGroup.position.y = 0.4; | |
| group.add(pawnGroup); | |
| } | |
| // Position on board with stairway adjustment | |
| let yOffset = 0; | |
| if (row < 2) yOffset = row === 0 ? 0.3 : 0.15; | |
| else if (row >= BOARD_ROWS - 2) yOffset = row === BOARD_ROWS - 1 ? 0.3 : 0.15; | |
| group.position.set( | |
| (col - BOARD_COLS / 2 + 0.5) * SQUARE_SPACING, | |
| yOffset, | |
| (row - BOARD_ROWS / 2 + 0.5) * SQUARE_SPACING | |
| ); | |
| // Store piece data | |
| group.userData = { | |
| type: 'piece', | |
| pieceData: pieceData, | |
| row: row, | |
| col: col | |
| }; | |
| return group; | |
| } | |
| function onMouseMove(event) { | |
| const rect = renderer.domElement.getBoundingClientRect(); | |
| mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; | |
| mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; | |
| // Highlight hovered objects | |
| raycaster.setFromCamera(mouse, camera); | |
| const intersects = raycaster.intersectObjects(scene.children, true); | |
| // Reset all materials | |
| resetSquareColors(); | |
| if (intersects.length > 0) { | |
| const object = intersects[0].object; | |
| const userData = object.userData || object.parent.userData; | |
| if (userData && userData.type === 'square') { | |
| // Highlight square | |
| object.material.emissive.setHex(0x333366); | |
| } else if (userData && userData.type === 'piece') { | |
| // Highlight piece | |
| if (object.material && object.material.emissive) { | |
| object.material.emissive.setHex(0x666666); | |
| } | |
| } else if (userData && userData.type === 'chest') { | |
| // Highlight chest | |
| if (object.material && object.material.emissive) { | |
| object.material.emissive.setHex(0x888888); | |
| } | |
| } | |
| } | |
| } | |
| function onMouseClick(event) { | |
| raycaster.setFromCamera(mouse, camera); | |
| const intersects = raycaster.intersectObjects(scene.children, true); | |
| if (intersects.length > 0) { | |
| const object = intersects[0].object; | |
| const userData = object.userData || object.parent.userData; | |
| if (userData && userData.type === 'square') { | |
| handleSquareClick(userData.row, userData.col); | |
| } else if (userData && userData.type === 'piece') { | |
| handlePieceClick(userData.pieceData, userData.row, userData.col); | |
| } else if (userData && userData.type === 'nexus') { | |
| handleNexusClick(userData.player); | |
| } else if (userData && userData.type === 'chest') { | |
| handleChestClick(userData.chestData, userData.row, userData.col); | |
| } | |
| } | |
| } | |
| function resetSquareColors() { | |
| for (let row = 0; row < BOARD_ROWS; row++) { | |
| for (let col = 0; col < BOARD_COLS; col++) { | |
| const square = boardSquares[row][col]; | |
| const isLight = (row + col) % 2 === 0; | |
| let baseColor, emissiveColor; | |
| if (row < 2) { // Player 1 stairway | |
| baseColor = isLight ? 0x1a1a4a : 0x0f0f3f; | |
| emissiveColor = isLight ? 0x0a0a33 : 0x050522; | |
| } else if (row >= BOARD_ROWS - 2) { // Player 2 stairway | |
| baseColor = isLight ? 0x4a1a1a : 0x3f0f0f; | |
| emissiveColor = isLight ? 0x330a0a : 0x220505; | |
| } else { // Main board | |
| baseColor = isLight ? 0x2a1a4a : 0x1a0f3f; | |
| emissiveColor = isLight ? 0x1a0033 : 0x0d0022; | |
| } | |
| square.material.color.setHex(baseColor); | |
| square.material.emissive.setHex(emissiveColor); | |
| } | |
| } | |
| } | |
| function showValidMoves3D(selectedPiece) { | |
| clearValidMoves3D(); | |
| for (let r = 0; r < BOARD_ROWS; r++) { | |
| for (let c = 0; c < BOARD_COLS; c++) { | |
| if (canMovePiece(selectedPiece, r, c)) { | |
| const square = boardSquares[r][c]; | |
| square.material.color.setHex(0x00ff00); | |
| square.material.emissive.setHex(0x004400); | |
| validMoveSquares.push(square); | |
| } | |
| } | |
| } | |
| } | |
| function showValidAttacks3D(selectedPiece) { | |
| clearValidMoves3D(); | |
| for (let r = 0; r < BOARD_ROWS; r++) { | |
| for (let c = 0; c < BOARD_COLS; c++) { | |
| if (canAttackSquare(selectedPiece, r, c)) { | |
| const square = boardSquares[r][c]; | |
| square.material.color.setHex(0xff0000); | |
| square.material.emissive.setHex(0x440000); | |
| validMoveSquares.push(square); | |
| } | |
| } | |
| } | |
| } | |
| function clearValidMoves3D() { | |
| validMoveSquares.forEach(square => { | |
| const row = square.userData.row; | |
| const col = square.userData.col; | |
| const isLight = (row + col) % 2 === 0; | |
| let baseColor, emissiveColor; | |
| if (row < 2) { // Player 1 stairway | |
| baseColor = isLight ? 0x1a1a4a : 0x0f0f3f; | |
| emissiveColor = isLight ? 0x0a0a33 : 0x050522; | |
| } else if (row >= BOARD_ROWS - 2) { // Player 2 stairway | |
| baseColor = isLight ? 0x4a1a1a : 0x3f0f0f; | |
| emissiveColor = isLight ? 0x330a0a : 0x220505; | |
| } else { // Main board | |
| baseColor = isLight ? 0x2a1a4a : 0x1a0f3f; | |
| emissiveColor = isLight ? 0x1a0033 : 0x0d0022; | |
| } | |
| square.material.color.setHex(baseColor); | |
| square.material.emissive.setHex(emissiveColor); | |
| }); | |
| validMoveSquares = []; | |
| } | |
| function update3DBoard() { | |
| // Remove all pieces from scene | |
| pieces3D.forEach(piece => { | |
| if (piece.parent) { | |
| piece.parent.remove(piece); | |
| } | |
| }); | |
| pieces3D = []; | |
| // Add current pieces | |
| for (let row = 0; row < BOARD_ROWS; row++) { | |
| for (let col = 0; col < BOARD_COLS; col++) { | |
| const piece = gameState.board[row][col]; | |
| if (piece && !piece.isWall) { | |
| const piece3D = createPiece3D(piece, row, col); | |
| scene.add(piece3D); | |
| pieces3D.push(piece3D); | |
| } | |
| } | |
| } | |
| } | |
| function onWindowResize() { | |
| const container = document.getElementById('game-canvas').parentElement; | |
| camera.aspect = container.clientWidth / container.clientHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(container.clientWidth, container.clientHeight); | |
| } | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| // Animate nexus cores | |
| nexusCores.forEach(nexus => { | |
| if (nexus) { | |
| nexus.rotation.y += 0.005; | |
| // Pulsing effect | |
| const scale = 1 + Math.sin(Date.now() * 0.002) * 0.05; | |
| nexus.scale.set(scale, scale, scale); | |
| } | |
| }); | |
| // Animate pieces | |
| pieces3D.forEach(piece => { | |
| if (piece.userData.pieceData.justSummoned) { | |
| piece.rotation.y += 0.02; | |
| } else { | |
| piece.rotation.y += 0.001; | |
| } | |
| // Idle floating animation | |
| const time = Date.now() * 0.001; | |
| const yOffset = piece.userData.row < 2 ? (piece.userData.row === 0 ? 0.3 : 0.15) : | |
| piece.userData.row >= BOARD_ROWS - 2 ? (piece.userData.row === BOARD_ROWS - 1 ? 0.3 : 0.15) : 0; | |
| piece.position.y = yOffset + 0.05 + Math.sin(time + piece.userData.row + piece.userData.col) * 0.02; | |
| }); | |
| // Animate chests | |
| chests3D.forEach(chest => { | |
| chest.rotation.y += 0.01; | |
| const time = Date.now() * 0.001; | |
| chest.position.y = 0.15 + Math.sin(time + chest.userData.row + chest.userData.col) * 0.05; | |
| }); | |
| // Animate defense towers | |
| defenseTowers.forEach(tower => { | |
| tower.rotation.y += 0.005; | |
| const time = Date.now() * 0.001; | |
| tower.position.y = 0.05 + Math.sin(time + tower.userData.row + tower.userData.col) * 0.02; | |
| }); | |
| renderer.render(scene, camera); | |
| } | |
| // Defense tower attack system | |
| function executeTowerAttacks() { | |
| defenseTowers.forEach(tower => { | |
| const towerData = tower.userData; | |
| if (towerData.player === gameState.currentPlayer) { | |
| const damage = towerData.attack(); | |
| const enemyPieces = []; | |
| // Find all enemy pieces | |
| for (let r = 0; r < BOARD_ROWS; r++) { | |
| for (let c = 0; c < BOARD_COLS; c++) { | |
| const piece = gameState.board[r][c]; | |
| if (piece && piece.owner !== towerData.player && !piece.isWall) { | |
| enemyPieces.push({ piece, row: r, col: c }); | |
| } | |
| } | |
| } | |
| // Attack a random enemy piece | |
| if (enemyPieces.length > 0) { | |
| const target = enemyPieces[Math.floor(Math.random() * enemyPieces.length)]; | |
| target.piece.health -= damage; | |
| log(`🗼 Defense Tower attacks ${target.piece.name} for ${damage} damage!`); | |
| awardExperience(towerData.player, 1); | |
| if (target.piece.health <= 0) { | |
| log(`💥 ${target.piece.name} destroyed by tower fire!`); | |
| gameState.board[target.row][target.col] = null; | |
| awardExperience(towerData.player, 2); | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| // Chest system | |
| function generateChests(state) { | |
| const chests = []; | |
| const chestCount = 6; // Number of chests to generate | |
| for (let i = 0; i < chestCount; i++) { | |
| let row, col; | |
| let attempts = 0; | |
| // Find an empty spot for the chest (avoid stairway areas) | |
| do { | |
| row = Math.floor(Math.random() * (BOARD_ROWS - 6)) + 3; | |
| col = Math.floor(Math.random() * (BOARD_COLS - 2)) + 1; | |
| attempts++; | |
| } while ((state.board[row][col] !== null || | |
| chests.some(c => c.row === row && c.col === col) || | |
| state.board[row][col]?.isWall) && | |
| attempts < 100); | |
| if (attempts < 100) { | |
| // Determine chest type based on rarity | |
| const rand = Math.random(); | |
| let chestType; | |
| if (rand < 0.1) { // 10% legendary | |
| chestType = CHEST_TYPES.LEGENDARY; | |
| } else if (rand < 0.3) { // 20% epic | |
| chestType = CHEST_TYPES.EPIC; | |
| } else { // 70% rare | |
| chestType = CHEST_TYPES.RARE; | |
| } | |
| const chest = { | |
| type: chestType, | |
| row: row, | |
| col: col, | |
| id: Math.random().toString(36).substr(2, 9) | |
| }; | |
| chests.push(chest); | |
| // Create 3D chest | |
| const chest3D = createChest3D(chest, row, col); | |
| scene.add(chest3D); | |
| chests3D.push(chest3D); | |
| } | |
| } | |
| return chests; | |
| } | |
| function handleChestClick(chestData, row, col) { | |
| // Only allow current player to open chests | |
| if (gameState.currentPlayer !== 1) return; | |
| // Check if there's a piece on this square that belongs to current player | |
| const piece = gameState.board[row][col]; | |
| if (!piece || piece.owner !== gameState.currentPlayer) { | |
| log("🚫 You need a piece on this square to open the chest!"); | |
| return; | |
| } | |
| openChest(chestData, piece); | |
| } | |
| function openChest(chestData, piece) { | |
| playSound('chest'); | |
| // Remove chest from board | |
| gameState.chests = gameState.chests.filter(c => c.id !== chestData.id); | |
| // Remove 3D chest | |
| const chestIndex = chests3D.findIndex(c => c.userData.chestData.id === chestData.id); | |
| if (chestIndex !== -1) { | |
| scene.remove(chests3D[chestIndex]); | |
| chests3D.splice(chestIndex, 1); | |
| } | |
| // Generate loot based on chest type | |
| const loot = generateChestLoot(chestData.type); | |
| // Show loot modal | |
| showChestLootModal(loot, chestData.type); | |
| log(`🎁 ${piece.name} opened a ${chestData.type.name}!`); | |
| updateChestCount(); | |
| } | |
| function generateChestLoot(chestType) { | |
| const loot = []; | |
| const itemCount = Math.floor(Math.random() * (chestType.maxItems - chestType.minItems + 1)) + chestType.minItems; | |
| for (let i = 0; i < itemCount; i++) { | |
| const rand = Math.random(); | |
| if (rand < 0.4) { // 40% chance for gear | |
| const gearType = Math.random() < 0.5 ? 'WEAPONS' : 'ARMOR'; | |
| const availableGear = GEAR_ITEMS[gearType].filter(item => | |
| item.rarity === chestType.rarity || | |
| (chestType.rarity === 'legendary' && item.rarity === 'epic') || // Legendary chests can contain epic gear | |
| (chestType.rarity === 'epic' && item.rarity === 'rare') // Epic chests can contain rare gear | |
| ); | |
| if (availableGear.length > 0) { | |
| const gear = {...availableGear[Math.floor(Math.random() * availableGear.length)]}; | |
| gear.id = Math.random().toString(36).substr(2, 9); | |
| loot.push({ type: 'gear', item: gear }); | |
| } | |
| } else { // 60% chance for special card | |
| const availableCards = CHEST_CARDS.filter(card => | |
| card.rarity === chestType.rarity || | |
| (chestType.rarity === 'legendary' && card.rarity === 'epic') || // Legendary chests can contain epic cards | |
| (chestType.rarity === 'epic' && card.rarity === 'rare') // Epic chests can contain rare cards | |
| ); | |
| if (availableCards.length > 0) { | |
| const card = {...availableCards[Math.floor(Math.random() * availableCards.length)]}; | |
| card.id = Math.random().toString(36).substr(2, 9); | |
| card.type = CARD_TYPES.INSTANT; // Mark as instant for playability | |
| loot.push({ type: 'card', item: card }); | |
| } | |
| } | |
| } | |
| return loot; | |
| } | |
| function showChestLootModal(loot, chestType) { | |
| const modal = document.getElementById('chest-loot-modal'); | |
| const content = document.getElementById('chest-loot-content'); | |
| let lootHTML = ` | |
| <div class="text-center"> | |
| <h2 class="text-xl font-bold mb-2" style="color: ${chestType.rarity === 'legendary' ? '#ffd700' : chestType.rarity === 'epic' ? '#ff7700' : '#c0c0c0'}">${chestType.name} Loot!</h2> | |
| <div class="text-4xl mb-4">${chestType.rarity === 'legendary' ? '💰' : chestType.rarity === 'epic' ? '🎁' : '📦'}</div> | |
| <p class="text-cyan-300 mb-4">You found:</p> | |
| `; | |
| loot.forEach(item => { | |
| if (item.type === 'gear') { | |
| const gear = item.item; | |
| lootHTML += ` | |
| <div class="mb-3 p-2 rounded border ${gear.rarity === 'legendary' ? 'border-yellow-500 bg-yellow-900/20' : gear.rarity === 'epic' ? 'border-orange-500 bg-orange-900/20' : 'border-gray-400 bg-gray-800/20'}"> | |
| <div class="font-bold ${gear.rarity === 'legendary' ? 'text-yellow-400' : gear.rarity === 'epic' ? 'text-orange-400' : 'text-gray-300'}">${gear.name}</div> | |
| <div class="text-sm">${gear.type === 'weapon' ? '⚔️ Weapon' : '🛡️ Armor'}</div> | |
| <div class="text-xs">${gear.type === 'weapon' ? `+${gear.attack} Attack` : `+${gear.defense} Defense`}</div> | |
| <div class="text-xs text-cyan-300 mt-1">${gear.description}</div> | |
| <div class="text-xs italic text-gray-400">"${gear.flavor}"</div> | |
| </div> | |
| `; | |
| } else if (item.type === 'card') { | |
| const card = item.item; | |
| lootHTML += ` | |
| <div class="mb-3 p-2 rounded border ${card.rarity === 'legendary' ? 'border-yellow-500 bg-yellow-900/20' : card.rarity === 'epic' ? 'border-orange-500 bg-orange-900/20' : 'border-gray-400 bg-gray-800/20'}"> | |
| <div class="font-bold ${card.rarity === 'legendary' ? 'text-yellow-400' : card.rarity === 'epic' ? 'text-orange-400' : 'text-gray-300'}">${card.name}</div> | |
| <div class="text-sm">Special Card</div> | |
| <div class="text-xs text-cyan-300 mt-1">${card.description}</div> | |
| <div class="text-xs italic text-gray-400">"${card.flavor}"</div> | |
| </div> | |
| `; | |
| } | |
| }); | |
| lootHTML += `</div>`; | |
| content.innerHTML = lootHTML; | |
| modal.classList.remove('hidden'); | |
| // Store loot for claiming | |
| gameState.pendingLoot = loot; | |
| } | |
| function hideChestLootModal() { | |
| const modal = document.getElementById('chest-loot-modal'); | |
| modal.classList.add('hidden'); | |
| // Add loot to player's inventory | |
| if (gameState.pendingLoot) { | |
| gameState.pendingLoot.forEach(item => { | |
| if (item.type === 'card') { | |
| // Add to player's hand | |
| gameState.players[1].hand.push(item.item); | |
| } else if (item.type === 'gear') { | |
| // Add to player's gear inventory | |
| if (!gameState.players[1].gear) gameState.players[1].gear = []; | |
| gameState.players[1].gear.push(item.item); | |
| } | |
| }); | |
| log(`🎉 Loot added to your inventory!`); | |
| gameState.pendingLoot = null; | |
| updateUI(); | |
| } | |
| } | |
| function updateChestCount() { | |
| const chestCount = gameState.chests ? gameState.chests.length : 0; | |
| document.getElementById('chest-count').textContent = chestCount; | |
| } | |
| // Game initialization | |
| function getInitialGameState() { | |
| const state = { | |
| gameMode: 'pvp', | |
| currentPlayer: 1, | |
| turnCounter: 1, | |
| players: { | |
| 1: { health: 10, mana: 3, hand: [], deck: [], movedPieces: [], isAI: false, nexusHealth: 10, experience: 0 }, | |
| 2: { health: 10, mana: 3, hand: [], deck: [], movedPieces: [], isAI: false, nexusHealth: 10, experience: 0 } | |
| }, | |
| board: Array(BOARD_ROWS).fill().map(() => Array(BOARD_COLS).fill(null)), | |
| selectedPiece: null, | |
| gameOver: false, | |
| log: ["🎮 Game started. Player 1's turn."] | |
| }; | |
| createDecks(state); | |
| drawInitialHands(state); | |
| setupInitialBoard(state); | |
| // Generate chests | |
| state.chests = generateChests(state); | |
| return state; | |
| } | |
| function startPvPMode() { | |
| gameState = getInitialGameState(); | |
| gameState.gameMode = 'pvp'; | |
| $('#mode-selection').fadeOut(300); | |
| initGame(); | |
| } | |
| function startGame(difficulty) { | |
| gameState = getInitialGameState(); | |
| gameState.gameMode = difficulty; | |
| gameState.players[2].isAI = true; | |
| $('#mode-selection').fadeOut(300); | |
| initGame(); | |
| } | |
| function initGame() { | |
| init3D(); | |
| updateUI(); | |
| updateChestCount(); | |
| playBackgroundMusic(); | |
| } | |
| function setupInitialBoard(state) { | |
| // Place defense towers on the board - adjusted for expanded board | |
| const towerPositions1 = [[2,2], [2,7]]; | |
| const towerPositions2 = [[BOARD_ROWS-3,2], [BOARD_ROWS-3,7]]; | |
| towerPositions1.forEach(([r, c]) => { | |
| if (r >= 0 && r < BOARD_ROWS && c >= 0 && c < BOARD_COLS) { | |
| state.board[r][c] = { | |
| ...PIECE_TYPES.DEFENSE_TOWER, | |
| owner: 1, | |
| health: PIECE_TYPES.DEFENSE_TOWER.defense, | |
| isTower: true, | |
| level: 1, | |
| id: Math.random().toString(36).substr(2, 9) | |
| }; | |
| } | |
| }); | |
| towerPositions2.forEach(([r, c]) => { | |
| if (r >= 0 && r < BOARD_ROWS && c >= 0 && c < BOARD_COLS) { | |
| state.board[r][c] = { | |
| ...PIECE_TYPES.DEFENSE_TOWER, | |
| owner: 2, | |
| health: PIECE_TYPES.DEFENSE_TOWER.defense, | |
| isTower: true, | |
| level: 1, | |
| id: Math.random().toString(36).substr(2, 9) | |
| }; | |
| } | |
| }); | |
| // Place stasis guards - adjusted for expanded board | |
| const stasisPositions1 = [[1,1], [1,2], [1,3], [1,4], [1,5], [1,6], [1,7], [1,8]]; | |
| const stasisPositions2 = [[BOARD_ROWS-2,1], [BOARD_ROWS-2,2], [BOARD_ROWS-2,3], [BOARD_ROWS-2,4], [BOARD_ROWS-2,5], [BOARD_ROWS-2,6], [BOARD_ROWS-2,7], [BOARD_ROWS-2,8]]; | |
| stasisPositions1.forEach(([r, c]) => { | |
| if (r >= 0 && r < BOARD_ROWS && c >= 0 && c < BOARD_COLS) { | |
| state.board[r][c] = { | |
| ...PIECE_TYPES.STASIS_GUARD, | |
| owner: 1, | |
| health: PIECE_TYPES.STASIS_GUARD.defense, | |
| isStasis: true, | |
| level: 1, | |
| id: Math.random().toString(36).substr(2, 9) | |
| }; | |
| } | |
| }); | |
| stasisPositions2.forEach(([r, c]) => { | |
| if (r >= 0 && r < BOARD_ROWS && c >= 0 && c < BOARD_COLS) { | |
| state.board[r][c] = { | |
| ...PIECE_TYPES.STASIS_GUARD, | |
| owner: 2, | |
| health: PIECE_TYPES.STASIS_GUARD.defense, | |
| isStasis: true, | |
| level: 1, | |
| id: Math.random().toString(36).substr(2, 9) | |
| }; | |
| } | |
| }); | |
| } | |
| function createDecks(state) { | |
| Object.keys(state.players).forEach(player => { | |
| const deck = []; | |
| // Add pieces with increased quantities | |
| Object.entries(PIECE_TYPES).forEach(([type, piece]) => { | |
| if (type === 'NEXUS_CORE' || type === 'STASIS_GUARD') return; // Skip these | |
| let count = 1; | |
| if (type === 'DATA_EMPRESS') count = 1; | |
| else if (type === 'FORTRESS_NODE') count = 4; | |
| else if (type === 'SIGNAL_HACKER') count = 4; | |
| else if (type === 'CYBER_STALKER') count = 4; | |
| else if (type === 'BYTE_PAWN') count = 6; | |
| else if (type === 'DEFENSE_TOWER') count = 3; | |
| for (let i = 0; i < count; i++) { | |
| deck.push({ | |
| type: CARD_TYPES.PIECE, | |
| pieceType: type, | |
| ...piece, | |
| id: Math.random().toString(36).substr(2, 9) | |
| }); | |
| } | |
| }); | |
| // Add other card types | |
| INSTANT_CARDS.forEach(card => { | |
| for (let i = 0; i < 2; i++) { | |
| deck.push({ | |
| type: CARD_TYPES.INSTANT, | |
| ...card, | |
| id: Math.random().toString(36).substr(2, 9) | |
| }); | |
| } | |
| }); | |
| SUMMON_CARDS.forEach(card => { | |
| for (let i = 0; i < 2; i++) { | |
| deck.push({ | |
| type: CARD_TYPES.SUMMON, | |
| ...card, | |
| id: Math.random().toString(36).substr(2, 9) | |
| }); | |
| } | |
| }); | |
| ENHANCEMENT_CARDS.forEach(card => { | |
| deck.push({ | |
| type: CARD_TYPES.ENHANCEMENT, | |
| ...card, | |
| id: Math.random().toString(36).substr(2, 9) | |
| }); | |
| }); | |
| // Add healing cards | |
| HEALING_CARDS.forEach(card => { | |
| for (let i = 0; i < 2; i++) { | |
| deck.push({ | |
| type: CARD_TYPES.HEALING, | |
| ...card, | |
| id: Math.random().toString(36).substr(2, 9) | |
| }); | |
| } | |
| }); | |
| // Add experience boost cards | |
| EXP_BOOST_CARDS.forEach(card => { | |
| for (let i = 0; i < 2; i++) { | |
| deck.push({ | |
| type: CARD_TYPES.EXP_BOOST, | |
| ...card, | |
| id: Math.random().toString(36).substr(2, 9) | |
| }); | |
| } | |
| }); | |
| // Shuffle deck | |
| for (let i = deck.length - 1; i > 0; i--) { | |
| const j = Math.floor(Math.random() * (i + 1)); | |
| [deck[i], deck[j]] = [deck[j], deck[i]]; | |
| } | |
| state.players[player].deck = deck; | |
| }); | |
| } | |
| // ... (rest of the game logic functions remain similar but adjusted for the expanded board) | |
| // Make sure to update the handleSquareClick function to handle chests and walls | |
| function handleSquareClick(row, col) { | |
| // Check if clicking on a wall | |
| if (gameState.board[row][col]?.isWall) { | |
| log("🚧 This square is blocked by a wall!"); | |
| return; | |
| } | |
| // Check if clicking on a chest | |
| const chest = gameState.chests ? gameState.chests.find(c => c.row === row && c.col === col) : null; | |
| if (chest) { | |
| handleChestClick(chest, row, col); | |
| return; | |
| } | |
| // Check if clicking on nexus attack zones (now at row 0 and BOARD_ROWS-1) | |
| if ((row === 0 && gameState.currentPlayer === 1) || (row === BOARD_ROWS - 1 && gameState.currentPlayer === 2)) { | |
| if (gameState.selectedPiece) { | |
| attackNexusCore(gameState.selectedPiece, gameState.currentPlayer === 1 ? 2 : 1); | |
| clearSelection(); | |
| hidePieceMenu(); | |
| updateUI(); | |
| return; | |
| } | |
| } | |
| const piece = gameState.board[row][col]; | |
| if (gameState.selectedPiece) { | |
| // Handle move or attack | |
| if (canMovePiece(gameState.selectedPiece, row, col)) { | |
| movePiece(gameState.selectedPiece, row, col); | |
| } else if (canAttackSquare(gameState.selectedPiece, row, col)) { | |
| attackPiece(gameState.selectedPiece, row, col); | |
| } | |
| clearSelection(); | |
| hidePieceMenu(); | |
| clearValidMoves3D(); | |
| } else { | |
| hidePieceMenu(); | |
| } | |
| updateUI(); | |
| } | |
| // Update movement logic to account for walls | |
| function canMovePiece(selectedPiece, targetRow, targetCol) { | |
| const piece = selectedPiece.piece; | |
| const fromRow = selectedPiece.row; | |
| const fromCol = selectedPiece.col; | |
| const targetPiece = gameState.board[targetRow][targetCol]; | |
| // Check if target is a wall | |
| if (targetPiece && targetPiece.isWall) return false; | |
| if (targetPiece && targetPiece.owner === piece.owner) return false; | |
| const rowDiff = Math.abs(targetRow - fromRow); | |
| const colDiff = Math.abs(targetCol - fromCol); | |
| switch (piece.moves) { | |
| case 'king': return rowDiff <= 1 && colDiff <= 1; | |
| case 'queen': return rowDiff === 0 || colDiff === 0 || rowDiff === colDiff; | |
| case 'rook': return rowDiff === 0 || colDiff === 0; | |
| case 'bishop': return rowDiff === colDiff; | |
| case 'knight': return (rowDiff === 2 && colDiff === 1) || (rowDiff === 1 && colDiff === 2); | |
| case 'pawn': | |
| const dir = piece.owner === 1 ? -1 : 1; | |
| if (targetPiece !== null && !targetPiece.isWall) { | |
| return false; | |
| } else { | |
| return (targetRow - fromRow === dir) && (colDiff === 0); | |
| } | |
| case 'none': return false; | |
| default: return false; | |
| } | |
| } | |
| // Update turn management to include tower attacks | |
| function endTurn() { | |
| if (gameState.gameOver) return; | |
| hidePieceMenu(); | |
| clearValidMoves3D(); | |
| // Execute tower attacks and board tower attacks | |
| executeTowerAttacks(); | |
| executeBoardTowerAttacks(); | |
| gameState.players[gameState.currentPlayer].movedPieces = []; | |
| for (let r = 0; r < BOARD_ROWS; r++) { | |
| for (let c = 0; c < BOARD_COLS; c++) { | |
| const p = gameState.board[r][c]; | |
| if (p && p.owner === gameState.currentPlayer) { | |
| p.justSummoned = false; | |
| p.tempAttackBoost = 0; | |
| p.tempDefenseBoost = 0; | |
| } | |
| } | |
| } | |
| if (gameState.turnCounter >= 6) { | |
| removeStasisPieces(); | |
| } | |
| gameState.currentPlayer = gameState.currentPlayer === 1 ? 2 : 1; | |
| if (gameState.currentPlayer === 1) { | |
| gameState.turnCounter++; | |
| } | |
| gameState.players[gameState.currentPlayer].mana = Math.min(10, (gameState.players[gameState.currentPlayer].mana || 0) + 1); | |
| drawCard(gameState.currentPlayer); | |
| clearSelection(); | |
| awardExperience(gameState.currentPlayer === 1 ? 2 : 1, 1); | |
| log(`🎮 Turn ${gameState.turnCounter} - Player ${gameState.currentPlayer}'s turn`); | |
| updateUI(); | |
| if (gameState.players[gameState.currentPlayer].isAI) { | |
| setTimeout(makeAIMove, 1500); | |
| } | |
| } | |
| // Board tower attacks (existing tower logic) | |
| function executeBoardTowerAttacks() { | |
| for (let r = 0; r < BOARD_ROWS; r++) { | |
| for (let c = 0; c < BOARD_COLS; c++) { | |
| const piece = gameState.board[r][c]; | |
| if (piece && piece.isTower && piece.owner === gameState.currentPlayer) { | |
| attackAdjacentSquares(r, c, piece); | |
| } | |
| } | |
| } | |
| } | |
| function attackAdjacentSquares(towerRow, towerCol, tower) { | |
| const directions = [[-1,-1], [-1,0], [-1,1], [0,-1], [0,1], [1,-1], [1,0], [1,1]]; | |
| let attacked = false; | |
| directions.forEach(([dr, dc]) => { | |
| const targetRow = towerRow + dr; | |
| const targetCol = towerCol + dc; | |
| if (targetRow >= 0 && targetRow < BOARD_ROWS && targetCol >= 0 && targetCol < BOARD_COLS) { | |
| const target = gameState.board[targetRow][targetCol]; | |
| if (target && target.owner !== tower.owner && !target.isWall) { | |
| const damage = tower.attack; | |
| target.health = (target.health || target.defense) - damage; | |
| log(`🗼 ${tower.name} attacks ${target.name} for ${damage} damage!`); | |
| attacked = true; | |
| awardExperience(tower.owner, 2); | |
| if (target.health <= 0) { | |
| log(`💥 ${target.name} destroyed by tower fire!`); | |
| gameState.board[targetRow][targetCol] = null; | |
| awardExperience(tower.owner, 3); | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| // Update the UI function to show chest count | |
| function updateUI() { | |
| if (!gameState || !gameState.players) return; | |
| const player1 = gameState.players[1]; | |
| const player2 = gameState.players[2]; | |
| $('#p1-health').text(player1.health); | |
| $('#p2-health').text(player2.health); | |
| $('#p1-health-bar').css('width', `${(player1.health / 10) * 100}%`); | |
| $('#p2-health-bar').css('width', `${(player2.health / 10) * 100}%`); | |
| $('#current-turn').text(`Player ${gameState.currentPlayer}`); | |
| $('#turn-counter').text(gameState.turnCounter); | |
| $('#p1-mana').text(player1.mana); | |
| $('#p2-mana').text(player2.mana); | |
| $('#p1-exp').text(player1.experience); | |
| $('#p2-exp').text(player2.experience); | |
| $('#p1-deck-count').text(player1.deck?.length || 0); | |
| $('#p2-deck-count').text(player2.deck?.length || 0); | |
| // Update enemy nexus health display | |
| const enemyPlayer = gameState.currentPlayer === 1 ? 2 : 1; | |
| $('#enemy-nexus-health').text(gameState.players[enemyPlayer].nexusHealth); | |
| updatePlayerHand(player1.hand || [], 1); | |
| update3DBoard(); | |
| updateLog(); | |
| updateChestCount(); | |
| } | |
| // The rest of the existing functions (drawCard, awardExperience, etc.) remain similar | |
| // Initialize game | |
| $(document).ready(() => { | |
| createParticles(); | |
| $('#mode-selection').show(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment