Spaces:
Running
Running
| <html lang="pt-br"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Earthbattle</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <style> | |
| @font-face { | |
| font-family: 'Press Start 2P'; | |
| src: url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); | |
| } | |
| body { | |
| font-family: 'Press Start 2P', cursive; | |
| background-color: #111; | |
| color: #fff; | |
| margin: 0; | |
| padding: 0; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| min-height: 100vh; | |
| overflow: hidden; | |
| } | |
| #game-container { | |
| border: 3px solid #fff; | |
| padding: 20px; | |
| background-color: rgba(0, 0, 0, 0.75); | |
| box-shadow: 0 0 15px #fff; | |
| position: relative; | |
| z-index: 1; | |
| width: 90%; | |
| max-width: 800px; | |
| } | |
| #shader-canvas { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| z-index: 0; | |
| } | |
| .character-status { | |
| border: 2px solid #888; | |
| padding: 10px; | |
| margin-bottom: 10px; | |
| background-color: rgba(30, 30, 30, 0.85); | |
| } | |
| .hp-meter { | |
| font-family: 'Courier New', Courier, monospace; | |
| font-size: 1.1em; | |
| font-weight: bold; | |
| color: #ff4136; | |
| background-color: #333; | |
| padding: 2px 5px; | |
| border-radius: 3px; | |
| min-width: 120px; | |
| display: inline-block; | |
| text-align: right; | |
| border: 1px solid #555; | |
| } | |
| .hp-compromised-text { | |
| font-size: 0.8em; | |
| color: #FF851B; | |
| } | |
| .mp-meter { | |
| color: #0074d9; | |
| font-family: 'Courier New', Courier, monospace; | |
| font-size: 1.2em; | |
| font-weight: bold; | |
| } | |
| .player.active { | |
| border-left-width: 5px ; | |
| border-left-color: #ffd700 ; | |
| box-shadow: 0 0 10px #ffd700; | |
| } | |
| .defeated { | |
| opacity: 0.5; | |
| text-decoration: line-through; | |
| } | |
| .defeated .hp-meter { | |
| color: #555; | |
| animation: none; | |
| } | |
| #actions button, .modal button { | |
| font-family: inherit; | |
| padding: 10px 15px; | |
| margin: 5px; | |
| cursor: pointer; | |
| font-size: 1em; | |
| border-width: 2px; | |
| } | |
| #actions button:disabled, .modal button:disabled { | |
| background-color: #333 ; | |
| color: #666 ; | |
| cursor: not-allowed; | |
| border-color: #444 ; | |
| } | |
| #message-log { | |
| border: 1px dashed #666; | |
| padding: 10px; | |
| margin-top: 15px; | |
| height: 100px; | |
| overflow-y: auto; | |
| background-color: rgba(0,0,0,0.6); | |
| font-size: 0.9em; | |
| } | |
| #message-log p { | |
| margin: 2px 0; | |
| border-bottom: 1px dotted #444; | |
| padding-bottom: 2px; | |
| } | |
| #message-log p:last-child { | |
| border-bottom: none; | |
| } | |
| .modal { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-color: rgba(0,0,0,0.9); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 10; | |
| } | |
| .modal-content { | |
| background-color: #1a1a1a; | |
| padding: 20px; | |
| border: 3px solid #ccc; | |
| box-shadow: 0 0 25px #fff; | |
| text-align: center; | |
| max-width: 90%; | |
| width: 600px; | |
| max-height: 85vh; | |
| overflow-y: auto; | |
| } | |
| .modal h2 { | |
| margin-top: 0; | |
| margin-bottom: 1rem; | |
| color: #ffd700; | |
| } | |
| .hidden { | |
| display: none ; | |
| } | |
| .modal button { | |
| border-color: #888; | |
| } | |
| .modal button:hover { | |
| filter: brightness(1.2); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <canvas id="shader-canvas"></canvas> | |
| <div id="game-container"> | |
| <h1 class="text-center text-3xl mb-6 text-yellow-400">Earthbattle 🌀</h1> | |
| <div id="party-display" class="grid grid-cols-1 md:grid-cols-3 gap-4"></div> | |
| <div id="enemy-display" class="my-6"></div> | |
| <div id="actions" class="flex flex-wrap justify-center gap-2"> | |
| <button id="attack-btn" class="text-white bg-red-600 hover:bg-red-700 border-red-800">Atacar ⚔️</button> | |
| <button id="item-btn" class="text-white bg-blue-600 hover:bg-blue-700 border-blue-800">Item 🎒</button> | |
| <button id="special-btn" class="text-white bg-purple-600 hover:bg-purple-700 border-purple-800">Especial ✨</button> | |
| </div> | |
| <div id="message-log" class="mt-4"></div> | |
| <button id="restart-btn" class="hidden mt-6 w-full bg-green-600 hover:bg-green-700 text-white py-3 border-green-700">Reiniciar Partida 🔄</button> | |
| </div> | |
| <div id="item-modal" class="modal hidden"> | |
| <div class="modal-content"> | |
| <h2>Inventário</h2> | |
| <div id="item-list" class="grid grid-cols-1 sm:grid-cols-2 gap-3 my-4"></div> | |
| <p class="mt-4 text-lg">Usar item em quem?</p> | |
| <div id="item-target-list" class="grid grid-cols-1 sm:grid-cols-3 gap-3 mt-2"></div> | |
| <button id="cancel-item-btn" class="mt-6 bg-gray-600 hover:bg-gray-700 text-white border-gray-700">Cancelar</button> | |
| </div> | |
| </div> | |
| <div id="special-modal" class="modal hidden"> | |
| <div class="modal-content"> | |
| <h2>Especiais</h2> | |
| <p id="special-description" class="my-4 text-lg"></p> | |
| <p>Usar em quem? (Se aplicável)</p> | |
| <div id="special-target-list" class="grid grid-cols-1 sm:grid-cols-3 gap-3 mt-2"></div> | |
| <div class="mt-6 flex justify-center gap-4"> | |
| <button id="confirm-special-btn" class="bg-green-600 hover:bg-green-700 text-white border-green-700">Confirmar</button> | |
| <button id="cancel-special-btn" class="bg-gray-600 hover:bg-gray-700 text-white border-gray-700">Cancelar</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="powerup-modal" class="modal hidden"> | |
| <div class="modal-content"> | |
| <h2>✨ Power-up! ✨</h2> | |
| <p class="my-4 text-lg">Escolha uma melhoria para a equipe:</p> | |
| <div id="powerup-options" class="grid grid-cols-1 gap-3"></div> | |
| </div> | |
| </div> | |
| <div id="shop-modal" class="modal hidden"> | |
| <div class="modal-content"> | |
| <h2>🏪 Loja 🏪</h2> | |
| <p class="my-4 text-lg">Créditos: <span id="shop-credits" class="text-yellow-400 font-bold">0</span></p> | |
| <div id="shop-items" class="grid grid-cols-1 sm:grid-cols-2 gap-3 my-4"></div> | |
| <p class="mt-4 text-lg">Comprar para quem?</p> | |
| <div id="shop-buyer-list" class="grid grid-cols-1 sm:grid-cols-3 gap-3 mt-2"></div> | |
| <button id="close-shop-btn" class="mt-6 bg-blue-600 hover:bg-blue-700 text-white border-blue-700">Sair da Loja</button> | |
| </div> | |
| </div> | |
| <script> | |
| // --- Shader Code --- | |
| const shaderCanvas = document.getElementById('shader-canvas'); | |
| let gl = null; | |
| let shaderProgram; | |
| let timeUniformLocation; | |
| let resolutionUniformLocation; | |
| let startTime = Date.now(); | |
| const vsSource = `attribute vec4 aVertexPosition; void main(void) { gl_Position = aVertexPosition; }`; | |
| const fsSource = `precision mediump float; uniform vec2 u_resolution; uniform float u_time; | |
| float random(vec2 st) { return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123); } | |
| void main(void) { vec2 st = gl_FragCoord.xy/u_resolution.xy; st.x *= u_resolution.x/u_resolution.y; | |
| float color = 0.0; vec2 pos = st * vec2(5.0,10.0); pos.x += sin(pos.y+u_time*0.5)*0.3; pos.y += cos(pos.x+u_time*0.3)*0.2; | |
| color = fract(pos.x*pos.y*sin(u_time*0.1)*0.5 + pos.x*0.2 + pos.y*0.3); | |
| gl_FragColor = vec4(0.5+0.5*sin(u_time*0.2+color*5.0+0.0), 0.5+0.5*sin(u_time*0.3+color*6.0+2.0), 0.5+0.5*sin(u_time*0.4+color*7.0+4.0),1.0);}`; | |
| function initShaders() { | |
| gl = shaderCanvas.getContext('webgl'); | |
| if (!gl) { | |
| console.error("WebGL não suportado!"); | |
| shaderCanvas.style.display = 'none'; | |
| return false; | |
| } | |
| const vS = loadShader(gl, gl.VERTEX_SHADER, vsSource); | |
| const fS = loadShader(gl, gl.FRAGMENT_SHADER, fsSource); | |
| shaderProgram = gl.createProgram(); | |
| gl.attachShader(shaderProgram, vS); | |
| gl.attachShader(shaderProgram, fS); | |
| gl.linkProgram(shaderProgram); | |
| if(!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { | |
| console.error('Shader link err: '+gl.getProgramInfoLog(shaderProgram)); | |
| return false; | |
| } | |
| gl.useProgram(shaderProgram); | |
| const pB = gl.createBuffer(); | |
| gl.bindBuffer(gl.ARRAY_BUFFER, pB); | |
| const pos = [-1, -1, 1, -1, -1, 1, 1, 1]; | |
| gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(pos), gl.STATIC_DRAW); | |
| const vPos = gl.getAttribLocation(shaderProgram, 'aVertexPosition'); | |
| gl.vertexAttribPointer(vPos, 2, gl.FLOAT, false, 0, 0); | |
| gl.enableVertexAttribArray(vPos); | |
| timeUniformLocation = gl.getUniformLocation(shaderProgram, 'u_time'); | |
| resolutionUniformLocation = gl.getUniformLocation(shaderProgram, 'u_resolution'); | |
| return true; | |
| } | |
| function loadShader(glCtx, type, src) { | |
| const s = glCtx.createShader(type); | |
| glCtx.shaderSource(s, src); | |
| glCtx.compileShader(s); | |
| if(!glCtx.getShaderParameter(s, glCtx.COMPILE_STATUS)) { | |
| console.error('Shader compile err: '+glCtx.getShaderInfoLog(s)); | |
| glCtx.deleteShader(s); | |
| return null; | |
| } | |
| return s; | |
| } | |
| function renderShader() { | |
| if(!gl || !shaderProgram) return; | |
| shaderCanvas.width = window.innerWidth; | |
| shaderCanvas.height = window.innerHeight; | |
| gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); | |
| gl.uniform1f(timeUniformLocation, (Date.now()-startTime)/1000.0); | |
| gl.uniform2f(resolutionUniformLocation, gl.canvas.width, gl.canvas.height); | |
| gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); | |
| requestAnimationFrame(renderShader); | |
| } | |
| // Game Constants | |
| const ENEMY_TARGET_COUNT = 10; | |
| const HEAL_POST_BATTLE_PERCENT = 0.25; | |
| const BASE_SHOP_CREDITS = 50; | |
| const REVIVE_HP_PERCENT_ITEM = 0.25; | |
| const REVIVE_HP_PERCENT_VICTORY = 0.15; | |
| const DEFAULT_DAMAGE_SPLIT = {immediate:0.5, compromised:0.5}; | |
| const STRONG_ENEMY_DAMAGE_SPLIT = {immediate:0.8, compromised:0.2}; | |
| // DOM Elements | |
| const partyDisplay = document.getElementById('party-display'); | |
| const enemyDisplay = document.getElementById('enemy-display'); | |
| const attackBtn = document.getElementById('attack-btn'); | |
| const itemBtn = document.getElementById('item-btn'); | |
| const specialBtn = document.getElementById('special-btn'); | |
| const messageLog = document.getElementById('message-log'); | |
| const restartBtn = document.getElementById('restart-btn'); | |
| const itemModal = document.getElementById('item-modal'); | |
| const itemListDiv = document.getElementById('item-list'); | |
| const itemTargetListDiv = document.getElementById('item-target-list'); | |
| const cancelItemBtn = document.getElementById('cancel-item-btn'); | |
| const specialModal = document.getElementById('special-modal'); | |
| const specialDescriptionP = document.getElementById('special-description'); | |
| const specialTargetListDiv = document.getElementById('special-target-list'); | |
| const confirmSpecialBtn = document.getElementById('confirm-special-btn'); | |
| const cancelSpecialBtn = document.getElementById('cancel-special-btn'); | |
| const powerupModal = document.getElementById('powerup-modal'); | |
| const powerupOptionsDiv = document.getElementById('powerup-options'); | |
| const shopModal = document.getElementById('shop-modal'); | |
| const shopCreditsSpan = document.getElementById('shop-credits'); | |
| const shopItemsDiv = document.getElementById('shop-items'); | |
| const shopBuyerListDiv = document.getElementById('shop-buyer-list'); | |
| const closeShopBtn = document.getElementById('close-shop-btn'); | |
| // Game State | |
| let party = []; | |
| let currentEnemy = null; | |
| let currentPlayerIndex = 0; | |
| let enemiesDefeatedCount = 0; | |
| let currentEnemyLevel = 1; | |
| let shopCredits = BASE_SHOP_CREDITS; | |
| let shopStock = {}; | |
| let gameActive = false; | |
| let turnActionsDisabled = false; | |
| // Improved Enemy Scaling System | |
| const ENEMY_SCALING = { | |
| baseHp: 30, | |
| hpPerLevel: 15, | |
| baseDamage: 5, | |
| damagePerLevel: 2.5, | |
| levelCap: 20, | |
| scalingCurve: (level) => { | |
| // Quadratic scaling up to level 10, then linear | |
| if (level <= 10) return level * level * 0.1; | |
| return 10 + (level - 10) * 0.5; | |
| }, | |
| getEnemyStats: (level) => { | |
| const curveFactor = ENEMY_SCALING.scalingCurve(level); | |
| return { | |
| hp: Math.floor(ENEMY_SCALING.baseHp + (ENEMY_SCALING.hpPerLevel * curveFactor)), | |
| damage: Math.floor(ENEMY_SCALING.baseDamage + (ENEMY_SCALING.damagePerLevel * curveFactor)), | |
| level: Math.min(level, ENEMY_SCALING.levelCap) | |
| }; | |
| } | |
| }; | |
| // Character Class | |
| class Character { | |
| constructor(id, name, hp, mana, baseDamage, specialName, specialCost, specialEffect, specialRequiresTarget = true) { | |
| this.id = id; | |
| this.name = name; | |
| this.maxHp = hp; | |
| this.currentHp = hp; | |
| this.compromisedDamage = 0; | |
| this.damageReductionPercent = 0; | |
| this.maxMana = mana; | |
| this.currentMana = mana; | |
| this.baseDamage = baseDamage; | |
| this.specialName = specialName; | |
| this.specialCost = specialCost; | |
| this.specialEffect = specialEffect; | |
| this.specialRequiresTarget = specialRequiresTarget; | |
| this.inventory = {"Poção de HP":2, "Poção de Mana":1}; | |
| this.isDefeated = false; | |
| this.domElement = null; | |
| } | |
| updateDOM() { | |
| if(!this.domElement) { | |
| this.domElement = document.getElementById(`player-${this.id}`); | |
| if(!this.domElement) return; | |
| } | |
| const hpEl = this.domElement.querySelector('.hp-meter'); | |
| const mpEl = this.domElement.querySelector('.mp-meter'); | |
| const nameEl = this.domElement.querySelector('.player-name'); | |
| const invEl = this.domElement.querySelector('.inventory-count'); | |
| let compText = this.compromisedDamage > 0 ? ` <span class="hp-compromised-text">(-${Math.ceil(this.compromisedDamage)})</span>` : ""; | |
| if(hpEl) hpEl.innerHTML = `${Math.ceil(this.currentHp)}/${this.maxHp}${compText}`; | |
| if(mpEl) mpEl.textContent = `${this.currentMana}/${this.maxMana}`; | |
| if(nameEl) nameEl.textContent = this.name; | |
| if(invEl) invEl.textContent = Object.values(this.inventory).reduce((a,b) => a+b, 0); | |
| this.domElement.classList.toggle('defeated', this.isDefeated); | |
| } | |
| applyCompromisedDamage() { | |
| if(this.isDefeated || this.compromisedDamage <= 0) { | |
| this.compromisedDamage = 0; | |
| return false; | |
| } | |
| const dmgToApply = Math.ceil(this.compromisedDamage); | |
| this.currentHp -= dmgToApply; | |
| logMessage(`🩸 ${this.name} sofreu ${dmgToApply} HP de dano comprometido!`); | |
| this.compromisedDamage = 0; | |
| if(this.currentHp <= 0) { | |
| this.currentHp = 0; | |
| this.isDefeated = true; | |
| logMessage(`💀 ${this.name} foi derrotado pelo dano comprometido!`); | |
| } | |
| this.updateDOM(); | |
| return this.isDefeated; | |
| } | |
| takeDamage(totalDamage, damageSplitRatio = DEFAULT_DAMAGE_SPLIT) { | |
| if(this.isDefeated) return; | |
| let actualDmg = totalDamage * (1 - this.damageReductionPercent); | |
| actualDmg = Math.max(0, Math.floor(actualDmg)); | |
| const immediateDmg = Math.ceil(actualDmg * damageSplitRatio.immediate); | |
| const newCompromisedDmg = Math.floor(actualDmg * damageSplitRatio.compromised); | |
| this.currentHp -= immediateDmg; | |
| logMessage(`💥 ${this.name} perde ${immediateDmg} HP.`); | |
| if(newCompromisedDmg > 0) { | |
| this.compromisedDamage += newCompromisedDmg; | |
| logMessage(`🩸 ${newCompromisedDmg} HP de ${this.name} comprometido.`); | |
| } | |
| if(this.currentHp <= 0) { | |
| this.currentHp = 0; | |
| this.isDefeated = true; | |
| logMessage(`💀 ${this.name} derrotado pelo dano imediato!`); | |
| } | |
| this.updateDOM(); | |
| } | |
| heal(amount) { | |
| if(this.isDefeated) { | |
| logMessage(`${this.name} derrotado, não pode curar.`); | |
| return; | |
| } | |
| this.currentHp = Math.min(this.maxHp, this.currentHp + amount); | |
| logMessage(`💖 ${this.name} curou ${Math.round(amount)} HP! HP: ${Math.ceil(this.currentHp)}/${this.maxHp}`); | |
| this.updateDOM(); | |
| } | |
| revive(hpPercent) { | |
| if(this.isDefeated) { | |
| this.isDefeated = false; | |
| this.currentHp = Math.floor(this.maxHp * hpPercent); | |
| this.compromisedDamage = 0; | |
| logMessage(`🌟 ${this.name} revivido com ${Math.ceil(this.currentHp)} HP!`); | |
| this.updateDOM(); | |
| return true; | |
| } | |
| return false; | |
| } | |
| restoreMana(amount) { | |
| this.currentMana = Math.min(this.maxMana, this.currentMana + amount); | |
| logMessage(`💧 ${this.name} restaurou ${amount} MP!`); | |
| this.updateDOM(); | |
| } | |
| } | |
| // Enemy Class with improved scaling | |
| class Enemy { | |
| constructor(name, level, attackType = 'default') { | |
| const stats = ENEMY_SCALING.getEnemyStats(level); | |
| this.name = `${name} Nv.${level} 👺`; | |
| this.maxHp = stats.hp; | |
| this.currentHp = this.maxHp; | |
| this.baseDamage = stats.damage; | |
| this.attackType = attackType; | |
| this.level = stats.level; | |
| this.domElement = null; | |
| // Special abilities for higher level enemies | |
| if (level >= 8) { | |
| this.specialAbility = Math.random() > 0.5 ? 'doubleAttack' : 'heal'; | |
| } else { | |
| this.specialAbility = null; | |
| } | |
| } | |
| updateDOM() { | |
| if(!this.domElement) { | |
| this.domElement = document.getElementById('enemy-status'); | |
| if(!this.domElement) return; | |
| } | |
| this.domElement.innerHTML = ` | |
| <h3 class="text-xl text-red-400">${this.name}</h3> | |
| <p>HP: <span class="hp-meter">${this.currentHp}/${this.maxHp}</span></p> | |
| ${this.specialAbility ? `<p class="text-xs text-purple-400 mt-1">Habilidade: ${this.getSpecialAbilityName()}</p>` : ''} | |
| `; | |
| this.domElement.classList.toggle('defeated', this.currentHp <= 0); | |
| } | |
| getSpecialAbilityName() { | |
| switch(this.specialAbility) { | |
| case 'doubleAttack': return 'Ataque Duplo'; | |
| case 'heal': return 'Auto-Cura'; | |
| default: return ''; | |
| } | |
| } | |
| attack(targetParty) { | |
| const alive = targetParty.filter(p => !p.isDefeated); | |
| if(alive.length === 0) return; | |
| const target = alive[Math.floor(Math.random() * alive.length)]; | |
| const split = this.attackType === 'strong' ? STRONG_ENEMY_DAMAGE_SPLIT : DEFAULT_DAMAGE_SPLIT; | |
| // Base attack | |
| logMessage(`⚔️ ${this.name} (${this.attackType === 'strong' ? 'Ataque Forte!' : 'Ataque'}) ataca ${target.name}!`); | |
| target.takeDamage(this.baseDamage, split); | |
| // Special abilities | |
| if (this.specialAbility === 'doubleAttack' && this.currentHp > 0) { | |
| logMessage(`🌀 ${this.name} usa Ataque Duplo!`); | |
| setTimeout(() => { | |
| target.takeDamage(Math.floor(this.baseDamage * 0.7), split); | |
| }, 500); | |
| } else if (this.specialAbility === 'heal' && this.currentHp < this.maxHp * 0.5) { | |
| const healAmount = Math.floor(this.maxHp * 0.2); | |
| this.currentHp = Math.min(this.maxHp, this.currentHp + healAmount); | |
| logMessage(`💚 ${this.name} se cura em ${healAmount} HP!`); | |
| this.updateDOM(); | |
| } | |
| } | |
| takeDamage(amount) { | |
| this.currentHp -= amount; | |
| logMessage(`💥 ${this.name} recebeu ${amount} de dano!`); | |
| if(this.currentHp <= 0) { | |
| this.currentHp = 0; | |
| logMessage(`🎉 ${this.name} foi derrotado!`); | |
| } | |
| this.updateDOM(); | |
| return this.currentHp <= 0; | |
| } | |
| } | |
| // Special Effects | |
| const specialEffects = { | |
| megaHeal: (caster, targetParty) => { | |
| targetParty.forEach(member => { | |
| if(!member.isDefeated) { | |
| member.heal(Math.floor(member.maxHp * 0.6)); | |
| } | |
| }); | |
| }, | |
| powerStrike: (caster, targetEnemy) => { | |
| const damage = Math.floor(caster.baseDamage * 2.5); | |
| logMessage(`💥 Golpe Poderoso: ${damage} dano em ${targetEnemy.name}!`); | |
| if(targetEnemy.takeDamage(damage)) { | |
| handleEnemyDefeated(); | |
| } | |
| }, | |
| psiShield: (caster) => { | |
| logMessage(`🛡️ ${caster.name} cria escudo psíquico! Defesa aumentada!`); | |
| party.forEach(member => { | |
| if(!member.isDefeated) { | |
| member.damageReductionPercent = Math.min(0.5, (member.damageReductionPercent || 0) + 0.1); | |
| logMessage(` ${member.name}: ${Math.round(member.damageReductionPercent * 100)}% Red. Dano.`); | |
| member.updateDOM(); | |
| } | |
| }); | |
| } | |
| }; | |
| // Game Functions | |
| function logMessage(msg) { | |
| const p = document.createElement('p'); | |
| p.textContent = msg; | |
| messageLog.appendChild(p); | |
| messageLog.scrollTop = messageLog.scrollHeight; | |
| } | |
| function createPartyDOM() { | |
| partyDisplay.innerHTML = ''; | |
| party.forEach(p => { | |
| const div = document.createElement('div'); | |
| div.id = `player-${p.id}`; | |
| div.className = 'character-status player p-3 rounded-lg shadow-md'; | |
| div.innerHTML = ` | |
| <h4 class="text-lg font-semibold mb-1"><span class="player-name">${p.name}</span></h4> | |
| <p class="text-sm">HP: <span class="hp-meter">0/0</span></p> | |
| <p class="text-sm">MP: <span class="mp-meter">0/0</span></p> | |
| <p class="text-xs mt-1">Itens: <span class="inventory-count">0</span></p> | |
| `; | |
| partyDisplay.appendChild(div); | |
| p.domElement = div; | |
| p.updateDOM(); | |
| }); | |
| } | |
| function createEnemyDOM() { | |
| enemyDisplay.innerHTML = `<div id="enemy-status" class="character-status p-3 rounded-lg shadow-md"></div>`; | |
| if(currentEnemy) { | |
| currentEnemy.domElement = document.getElementById('enemy-status'); | |
| currentEnemy.updateDOM(); | |
| } | |
| } | |
| function updateAllDOM() { | |
| party.forEach(p => { | |
| if(p.domElement) p.updateDOM(); | |
| }); | |
| if(currentEnemy && currentEnemy.domElement) currentEnemy.updateDOM(); | |
| updateActionButtonStates(); | |
| } | |
| function updateActionButtonStates() { | |
| if(!party[currentPlayerIndex] || !gameActive) { | |
| [attackBtn, itemBtn, specialBtn].forEach(btn => btn.disabled = true); | |
| return; | |
| } | |
| const player = party[currentPlayerIndex]; | |
| attackBtn.disabled = turnActionsDisabled || player.isDefeated; | |
| itemBtn.disabled = turnActionsDisabled || player.isDefeated || Object.values(player.inventory).every(count => count === 0); | |
| specialBtn.disabled = turnActionsDisabled || player.isDefeated || player.currentMana < player.specialCost; | |
| // Update active player highlight | |
| party.forEach(p => { | |
| if(p.domElement) { | |
| p.domElement.classList.remove('active', 'border-yellow-400', 'shadow-yellow-500/50'); | |
| } | |
| }); | |
| if(!player.isDefeated && player.domElement) { | |
| player.domElement.classList.add('active', 'border-yellow-400', 'shadow-yellow-500/50'); | |
| } | |
| } | |
| function disableTurnActions(disable = true) { | |
| turnActionsDisabled = disable; | |
| updateActionButtonStates(); | |
| } | |
| // Game Initialization | |
| function initGame() { | |
| messageLog.innerHTML = '<p>Bem-vindo! Prepare-se para a batalha!</p>'; | |
| gameActive = true; | |
| turnActionsDisabled = false; | |
| party = [ | |
| new Character('p1', "Herói 💪", 100, 50, 15, "Mega Cura", 25, specialEffects.megaHeal, 'player'), | |
| new Character('p2', "Maga 🧙", 80, 80, 10, "Golpe Poderoso", 20, specialEffects.powerStrike, true), | |
| new Character('p3', "Guardião 🛡️", 120, 30, 12, "Escudo Psíquico", 15, specialEffects.psiShield, false) | |
| ]; | |
| currentPlayerIndex = 0; | |
| enemiesDefeatedCount = 0; | |
| currentEnemyLevel = 1; | |
| shopCredits = BASE_SHOP_CREDITS; | |
| shopStock = { | |
| "Poção de HP": 5, | |
| "Poção de Mana": 4, | |
| "Bomba Pequena": 3, | |
| "Pena de Fênix": 2 | |
| }; | |
| createPartyDOM(); | |
| spawnNewEnemy(); | |
| updateAllDOM(); | |
| logMessage(`É a vez de ${party[currentPlayerIndex].name}.`); | |
| restartBtn.classList.add('hidden'); | |
| disableTurnActions(party[currentPlayerIndex].isDefeated); | |
| } | |
| function spawnNewEnemy() { | |
| const names = ["Goblin Astuto", "Ogro Zonzo", "Morcego Sombrio", "Slime Ácido", "Esqueleto Brutal"]; | |
| const enemyName = names[Math.floor(Math.random() * names.length)]; | |
| const attackType = enemyName === "Esqueleto Brutal" ? 'strong' : 'default'; | |
| // Scale enemy level based on progress | |
| const levelScale = Math.min( | |
| currentEnemyLevel + Math.floor(enemiesDefeatedCount / 3), | |
| ENEMY_SCALING.levelCap | |
| ); | |
| currentEnemy = new Enemy(enemyName, levelScale, attackType); | |
| createEnemyDOM(); | |
| let specialMsg = ''; | |
| if (currentEnemy.specialAbility) { | |
| specialMsg = ` (Habilidade: ${currentEnemy.getSpecialAbilityName()})`; | |
| } | |
| logMessage(`Novo inimigo: ${currentEnemy.name}!${attackType === 'strong' ? ' (Ataque Forte!)' : ''}${specialMsg}`); | |
| } | |
| // Player Action Core Logic | |
| async function corePlayerAttack(player) { | |
| logMessage(`${player.name} ataca ${currentEnemy.name}!`); | |
| await delay(500); | |
| return currentEnemy.takeDamage(player.baseDamage); | |
| } | |
| async function coreUseItem(player, itemName, target) { | |
| logMessage(`${player.name} usa ${itemName} em ${target.name || 'si mesmo'}.`); | |
| if (itemName !== "Pena de Fênix" && target.isDefeated) { | |
| logMessage(`${target.name} derrotado.`); | |
| return false; | |
| } | |
| if (player.inventory[itemName] <= 0) { | |
| logMessage(`Faltou ${itemName}!`); | |
| return false; | |
| } | |
| player.inventory[itemName]--; | |
| if (player.inventory[itemName] <= 0) { | |
| delete player.inventory[itemName]; | |
| } | |
| await delay(500); | |
| let enemyDefeated = false; | |
| if (itemName === "Pena de Fênix") { | |
| if (target.isDefeated) { | |
| target.revive(REVIVE_HP_PERCENT_ITEM); | |
| } else { | |
| logMessage(`${target.name} não precisa.`); | |
| player.inventory[itemName] = (player.inventory[itemName] || 0) + 1; | |
| } | |
| } | |
| else if (itemName === "Poção de HP") { | |
| target.heal(25 + Math.floor(player.maxHp * 0.2)); | |
| } | |
| else if (itemName === "Poção de Mana") { | |
| target.restoreMana(20 + Math.floor(player.maxMana * 0.2)); | |
| } | |
| else if (itemName === "Bomba Pequena" && target && typeof target.takeDamage === 'function') { | |
| if(target.takeDamage(30 + player.baseDamage)) { | |
| enemyDefeated = true; | |
| } | |
| } | |
| player.updateDOM(); | |
| return enemyDefeated; | |
| } | |
| async function coreExecuteSpecial(player, target) { | |
| logMessage(`${player.name} usa ${player.specialName}!`); | |
| player.currentMana -= player.specialCost; | |
| player.updateDOM(); | |
| await delay(500); | |
| let enemyDefeatedBySpecial = false; | |
| if (player.specialEffect === specialEffects.powerStrike) { | |
| player.specialEffect(player, target || currentEnemy, party); | |
| enemyDefeatedBySpecial = currentEnemy.currentHp <= 0; | |
| } else { | |
| player.specialEffect(player, target || currentEnemy, party); | |
| } | |
| return enemyDefeatedBySpecial; | |
| } | |
| // Player Action Orchestrator | |
| async function handlePlayerAction(actionCoreFunction, ...args) { | |
| if (turnActionsDisabled || !gameActive || party[currentPlayerIndex].isDefeated) return; | |
| disableTurnActions(true); | |
| const player = party[currentPlayerIndex]; | |
| let enemyDefeatedByPrimaryAction = false; | |
| if (actionCoreFunction) { | |
| enemyDefeatedByPrimaryAction = await actionCoreFunction(player, ...args); | |
| } | |
| // Apply compromised damage AFTER primary action | |
| let playerDefeatedByCompromised = false; | |
| if (!player.isDefeated && player.compromisedDamage > 0) { | |
| logMessage(`--- ${player.name} processando dano comprometido (-${Math.ceil(player.compromisedDamage)} HP)... ---`); | |
| await delay(600); | |
| playerDefeatedByCompromised = player.applyCompromisedDamage(); | |
| } | |
| updateAllDOM(); | |
| if (checkGameOver()) return; | |
| // Decide next step | |
| if (enemyDefeatedByPrimaryAction && currentEnemy.currentHp <= 0) { | |
| handleEnemyDefeated(); | |
| } else if (playerDefeatedByCompromised && allPlayersDefeated()) { | |
| // Already covered by checkGameOver() | |
| } else { | |
| nextTurn(); | |
| } | |
| } | |
| // Event Listeners for main action buttons | |
| attackBtn.addEventListener('click', () => handlePlayerAction(corePlayerAttack)); | |
| itemBtn.addEventListener('click', openItemModal); | |
| specialBtn.addEventListener('click', openSpecialModal); | |
| function openItemModal() { | |
| if(turnActionsDisabled || !gameActive || party[currentPlayerIndex].isDefeated) return; | |
| const player = party[currentPlayerIndex]; | |
| const hasItems = Object.values(player.inventory).some(count => count > 0); | |
| if(!hasItems) { | |
| logMessage("Inventário vazio!"); | |
| return; | |
| } | |
| itemListDiv.innerHTML = ''; | |
| Object.entries(player.inventory).forEach(([itemName, count]) => { | |
| if(count > 0) { | |
| const btn = document.createElement('button'); | |
| btn.className = 'w-full text-left p-2 border border-gray-600 rounded hover:bg-gray-700'; | |
| btn.textContent = `${itemName} (x${count})`; | |
| btn.onclick = () => selectItem(itemName, player); | |
| itemListDiv.appendChild(btn); | |
| } | |
| }); | |
| itemTargetListDiv.innerHTML = ''; | |
| itemModal.classList.remove('hidden'); | |
| } | |
| function selectItem(itemName, caster) { | |
| itemTargetListDiv.innerHTML = ''; | |
| const createTargetBtn = (target, actionFn, isDefeatedTarget = false) => { | |
| const btn = document.createElement('button'); | |
| btn.className = `w-full p-2 border rounded text-white ${ | |
| isDefeatedTarget ? 'bg-gray-500 hover:bg-gray-600 border-gray-700' : 'bg-blue-600 hover:bg-blue-700 border-blue-500' | |
| }`; | |
| btn.textContent = target.name + (isDefeatedTarget ? " (Caído)" : ""); | |
| btn.onclick = actionFn; | |
| itemTargetListDiv.appendChild(btn); | |
| }; | |
| if(itemName === "Pena de Fênix") { | |
| party.forEach(p => { | |
| if(p.isDefeated) { | |
| createTargetBtn(p, () => { | |
| handlePlayerAction(coreUseItem, itemName, p); | |
| itemModal.classList.add('hidden'); | |
| }, true); | |
| } | |
| }); | |
| if(!itemTargetListDiv.hasChildNodes()) { | |
| itemTargetListDiv.innerHTML = '<p class="text-gray-400">Ninguém para reviver!</p>'; | |
| } | |
| } | |
| else if(itemName.includes("Poção")) { | |
| party.forEach(p => { | |
| if(!p.isDefeated) { | |
| createTargetBtn(p, () => { | |
| handlePlayerAction(coreUseItem, itemName, p); | |
| itemModal.classList.add('hidden'); | |
| }); | |
| } | |
| }); | |
| } | |
| else if(itemName.includes("Bomba")) { | |
| createTargetBtn(currentEnemy, () => { | |
| handlePlayerAction(coreUseItem, itemName, currentEnemy); | |
| itemModal.classList.add('hidden'); | |
| }); | |
| } | |
| else { | |
| handlePlayerAction(coreUseItem, itemName, caster); | |
| itemModal.classList.add('hidden'); | |
| } | |
| } | |
| function openSpecialModal() { | |
| if(turnActionsDisabled || !gameActive || party[currentPlayerIndex].isDefeated) return; | |
| const player = party[currentPlayerIndex]; | |
| if(player.currentMana < player.specialCost) { | |
| logMessage("Mana insuficiente!"); | |
| return; | |
| } | |
| specialDescriptionP.textContent = `${player.specialName} (Custo: ${player.specialCost} MP)`; | |
| specialTargetListDiv.innerHTML = ''; | |
| confirmSpecialBtn.onclick = () => { | |
| let target = player.specialRequiresTarget === true ? currentEnemy : | |
| (player.specialRequiresTarget === false ? null : undefined); | |
| if(player.specialRequiresTarget !== 'player') { | |
| handlePlayerAction(coreExecuteSpecial, target); | |
| specialModal.classList.add('hidden'); | |
| } | |
| }; | |
| if(player.specialRequiresTarget === 'player') { | |
| party.forEach(p => { | |
| if(!p.isDefeated) { | |
| const btn = document.createElement('button'); | |
| btn.className = 'w-full p-2 border border-purple-500 rounded bg-purple-600 hover:bg-purple-700 text-white'; | |
| btn.textContent = p.name; | |
| btn.onclick = () => { | |
| handlePlayerAction(coreExecuteSpecial, p); | |
| specialModal.classList.add('hidden'); | |
| }; | |
| specialTargetListDiv.appendChild(btn); | |
| } | |
| }); | |
| confirmSpecialBtn.classList.add('hidden'); | |
| } else { | |
| confirmSpecialBtn.classList.remove('hidden'); | |
| } | |
| specialModal.classList.remove('hidden'); | |
| } | |
| // Turn Management | |
| async function nextTurn() { | |
| if (!gameActive) return; | |
| if (checkGameOver()) return; | |
| currentPlayerIndex = (currentPlayerIndex + 1) % party.length; | |
| let nextPlayer = party[currentPlayerIndex]; | |
| let attempts = 0; | |
| while (nextPlayer.isDefeated && attempts < party.length) { | |
| currentPlayerIndex = (currentPlayerIndex + 1) % party.length; | |
| nextPlayer = party[currentPlayerIndex]; | |
| attempts++; | |
| } | |
| updateAllDOM(); | |
| if (allPlayersDefeated()) { | |
| gameOver(false); | |
| return; | |
| } | |
| if (currentPlayerIndex === 0 && attempts < party.length) { | |
| disableTurnActions(true); | |
| setTimeout(enemyTurn, 1000); | |
| } | |
| else if (attempts < party.length) { | |
| logMessage(`É a vez de ${nextPlayer.name}.`); | |
| disableTurnActions(false); | |
| } | |
| else { | |
| gameOver(false); | |
| } | |
| } | |
| async function enemyTurn() { | |
| if(!gameActive || (currentEnemy && currentEnemy.currentHp <= 0)) { | |
| if(gameActive && !allPlayersDefeated()) { | |
| currentPlayerIndex = party.findIndex(p => !p.isDefeated); | |
| if(currentPlayerIndex !== -1) { | |
| logMessage(`É a vez de ${party[currentPlayerIndex].name}.`); | |
| disableTurnActions(false); | |
| } else { | |
| gameOver(false); | |
| } | |
| updateAllDOM(); | |
| } | |
| return; | |
| } | |
| logMessage(`--- Vez de ${currentEnemy.name} ---`); | |
| disableTurnActions(true); | |
| await delay(1000); | |
| currentEnemy.attack(party); | |
| await delay(1000); | |
| if(checkGameOver()) return; | |
| nextTurn(); | |
| } | |
| function handleEnemyDefeated() { | |
| enemiesDefeatedCount++; | |
| logMessage(`Inimigos derrotados: ${enemiesDefeatedCount}/${ENEMY_TARGET_COUNT}`); | |
| disableTurnActions(true); | |
| if(enemiesDefeatedCount >= ENEMY_TARGET_COUNT) { | |
| party.forEach(p => { | |
| if(p.currentHp <= 0 || p.isDefeated) { | |
| p.revive(REVIVE_HP_PERCENT_VICTORY); | |
| } | |
| }); | |
| updateAllDOM(); | |
| gameOver(true); | |
| } else { | |
| setTimeout(postBattlePhase, 1000); | |
| } | |
| } | |
| function postBattlePhase() { | |
| logMessage("🎉 VITÓRIA NA BATALHA! 🎉"); | |
| // Heal party | |
| party.forEach(p => { | |
| if(p.currentHp <= 0 || p.isDefeated) { | |
| p.revive(REVIVE_HP_PERCENT_VICTORY); | |
| } | |
| if(!p.isDefeated) { | |
| p.heal(Math.floor(p.maxHp * HEAL_POST_BATTLE_PERCENT)); | |
| p.restoreMana(Math.floor(p.maxMana * 0.15)); | |
| p.damageReductionPercent = 0; | |
| } | |
| }); | |
| updateAllDOM(); | |
| setTimeout(offerPowerUp, 1500); | |
| } | |
| function offerPowerUp() { | |
| powerupOptionsDiv.innerHTML = ''; | |
| const POWER_UPS = [ | |
| {name: "💪 +20 HP Máx", effect: p => { p.maxHp += 20; p.heal(20); }}, | |
| {name: "✨ +15 Mana Máx", effect: p => { p.maxMana += 15; p.restoreMana(15); }}, | |
| {name: "🛡️ Defesa Extra (Reduz 10% dano)", effect: p => p.damageReductionPercent = Math.min(0.5, (p.damageReductionPercent || 0) + 0.10)}, | |
| {name: "💥 +3 Dano Base", effect: p => p.baseDamage += 3}, | |
| {name: "💰 2 Poções HP", effect: p => p.inventory["Poção de HP"] = (p.inventory["Poção de HP"] || 0) + 2}, | |
| ]; | |
| // Get 3 random power-ups | |
| const shuffled = [...POWER_UPS].sort(() => 0.5 - Math.random()); | |
| const selectedPowerUps = shuffled.slice(0, 3); | |
| selectedPowerUps.forEach(powerUp => { | |
| const btn = document.createElement('button'); | |
| btn.className = 'w-full p-3 border border-yellow-500 rounded bg-yellow-600 hover:bg-yellow-700 text-white'; | |
| btn.textContent = powerUp.name; | |
| btn.onclick = () => { | |
| logMessage(`Power-up: ${powerUp.name}`); | |
| party.forEach(p => { | |
| if(!p.isDefeated) { | |
| powerUp.effect(p); | |
| } | |
| }); | |
| updateAllDOM(); | |
| powerupModal.classList.add('hidden'); | |
| setTimeout(offerShop, 1000); | |
| }; | |
| powerupOptionsDiv.appendChild(btn); | |
| }); | |
| powerupModal.classList.remove('hidden'); | |
| } | |
| function offerShop() { | |
| shopItemsDiv.innerHTML = ''; | |
| shopBuyerListDiv.innerHTML = ''; | |
| shopCreditsSpan.textContent = shopCredits; | |
| const ITEMS_SALE = { | |
| "Poção de HP": { price: 15, desc: "Cura HP." }, | |
| "Poção de Mana": { price: 10, desc: "Restaura MP." }, | |
| "Bomba Pequena": { price: 25, desc: "Dano." }, | |
| "Pena de Fênix": { price: 75, desc: `Revive ${REVIVE_HP_PERCENT_ITEM * 100}%HP` } | |
| }; | |
| Object.entries(ITEMS_SALE).forEach(([itemName, itemData]) => { | |
| const stock = shopStock[itemName] || 0; | |
| const entry = document.createElement('div'); | |
| entry.className = 'shop-item p-3 border border-gray-700 rounded bg-gray-800'; | |
| entry.innerHTML = ` | |
| <p class="font-semibold">${itemName} (x${stock}) - ${itemData.price}💰</p> | |
| <p class="text-xs text-gray-400">${itemData.desc}</p> | |
| `; | |
| const btn = document.createElement('button'); | |
| btn.className = 'mt-2 w-full p-2 border border-green-500 rounded bg-green-600 hover:bg-green-700 text-white disabled:bg-gray-500 disabled:border-gray-600'; | |
| btn.textContent = "Comprar"; | |
| btn.disabled = stock <= 0 || shopCredits < itemData.price; | |
| btn.onclick = () => buyItem(itemName, itemData.price, null); | |
| entry.appendChild(btn); | |
| shopItemsDiv.appendChild(entry); | |
| }); | |
| party.forEach(p => { | |
| const btn = document.createElement('button'); | |
| btn.className = `w-full p-2 border rounded text-white ${ | |
| p.isDefeated ? 'bg-gray-500 hover:bg-gray-600 border-gray-700' : 'bg-blue-600 hover:bg-blue-700 border-blue-500' | |
| }`; | |
| btn.textContent = p.name + (p.isDefeated ? " (Caído)" : ""); | |
| btn.onclick = () => { | |
| shopItemsDiv.querySelectorAll('.shop-item button').forEach(buyBtn => { | |
| if(buyBtn.textContent === "Comprar") { | |
| const itemDiv = buyBtn.closest('.shop-item'); | |
| const nameDOM = itemDiv.querySelector('p.font-semibold').textContent.split(' (')[0]; | |
| const priceDOM = parseInt(itemDiv.querySelector('p.font-semibold').textContent.match(/- (\d+)💰/)[1]); | |
| if(nameDOM === "Pena de Fênix") { | |
| buyBtn.disabled = !p.isDefeated || shopStock[nameDOM] <= 0 || shopCredits < priceDOM; | |
| } else { | |
| buyBtn.disabled = p.isDefeated || shopStock[nameDOM] <= 0 || shopCredits < priceDOM; | |
| } | |
| buyBtn.onclick = () => buyItem(nameDOM, priceDOM, p); | |
| } | |
| }); | |
| logMessage(`Selecionado ${p.name}.`); | |
| shopBuyerListDiv.querySelectorAll('button').forEach(b => { | |
| b.style.borderColor = b.classList.contains('bg-gray-500') ? '#4B5563' : '#3B82F6'; | |
| }); | |
| btn.style.borderColor = "yellow"; | |
| }; | |
| shopBuyerListDiv.appendChild(btn); | |
| }); | |
| shopModal.classList.remove('hidden'); | |
| } | |
| function buyItem(itemName, price, buyer) { | |
| if(!buyer) { | |
| logMessage("Selecione comprador!"); | |
| if(shopBuyerListDiv) { | |
| shopBuyerListDiv.style.outline = "2px dashed red"; | |
| setTimeout(() => { | |
| if(shopBuyerListDiv) shopBuyerListDiv.style.outline = "none"; | |
| }, 2000); | |
| } | |
| return; | |
| } | |
| if(itemName === "Pena de Fênix" && !buyer.isDefeated) { | |
| logMessage(`${buyer.name} não está caído.`); | |
| return; | |
| } | |
| if(itemName !== "Pena de Fênix" && buyer.isDefeated) { | |
| logMessage(`${buyer.name} está caído.`); | |
| return; | |
| } | |
| if(shopCredits >= price && (shopStock[itemName] || 0) > 0) { | |
| shopCredits -= price; | |
| shopStock[itemName]--; | |
| if(itemName === "Pena de Fênix") { | |
| buyer.revive(REVIVE_HP_PERCENT_ITEM); | |
| } else { | |
| buyer.inventory[itemName] = (buyer.inventory[itemName] || 0) + 1; | |
| } | |
| logMessage(`${itemName} p/ ${buyer.name}! Créditos: ${shopCredits}`); | |
| buyer.updateDOM(); | |
| offerShop(); | |
| } else { | |
| logMessage("Sem créditos/estoque!"); | |
| } | |
| } | |
| // Utility Functions | |
| function delay(ms) { | |
| return new Promise(resolve => setTimeout(resolve, ms)); | |
| } | |
| function checkGameOver() { | |
| if(allPlayersDefeated()) { | |
| gameOver(false); | |
| return true; | |
| } | |
| return false; | |
| } | |
| function allPlayersDefeated() { | |
| return party.every(p => p.isDefeated); | |
| } | |
| function gameOver(isWin) { | |
| if(!gameActive) return; | |
| gameActive = false; | |
| disableTurnActions(true); | |
| if(isWin) { | |
| logMessage("🏆🎉 VITÓRIA FINAL! 🎉🏆"); | |
| } else { | |
| logMessage("--- 💀 GAME OVER 💀 ---"); | |
| } | |
| restartBtn.classList.remove('hidden'); | |
| } | |
| // Event Listeners | |
| cancelItemBtn.addEventListener('click', () => { | |
| itemModal.classList.add('hidden'); | |
| disableTurnActions(false); | |
| }); | |
| cancelSpecialBtn.addEventListener('click', () => { | |
| specialModal.classList.add('hidden'); | |
| disableTurnActions(false); | |
| }); | |
| closeShopBtn.addEventListener('click', () => { | |
| shopModal.classList.add('hidden'); | |
| currentEnemyLevel++; | |
| spawnNewEnemy(); | |
| currentPlayerIndex = party.findIndex(p => !p.isDefeated); | |
| if(currentPlayerIndex === -1) { | |
| gameOver(false); | |
| return; | |
| } | |
| logMessage(`É ${party[currentPlayerIndex].name}.`); | |
| disableTurnActions(false); | |
| updateAllDOM(); | |
| }); | |
| restartBtn.addEventListener('click', initGame); | |
| // Initialize | |
| document.addEventListener('DOMContentLoaded', () => { | |
| if(initShaders()) { | |
| renderShader(); | |
| } else { | |
| console.warn("Shader falhou."); | |
| } | |
| initGame(); | |
| }); | |
| window.addEventListener('resize', () => { | |
| // renderShader handles resizing | |
| }); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Jaspior/earthbattle" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |