earthbattle / index.html
Jaspior's picture
Add 3 files
62750c6 verified
<!DOCTYPE html>
<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 !important;
border-left-color: #ffd700 !important;
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 !important;
color: #666 !important;
cursor: not-allowed;
border-color: #444 !important;
}
#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 !important;
}
.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>