Tiny-Healer / index.html
Nymbo's picture
Update index.html
422a259 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tiny Healer - RPG Healer Combat Game</title>
<style>
/* Basic body styling */
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background: linear-gradient(to bottom right, #1e3c72, #2a5298);
color: #ffd100;
}
/* Main game container */
#game-container {
width: 800px;
height: 600px;
border: none;
padding: 20px;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.9), rgba(50, 50, 50, 0.9));
position: relative;
box-shadow: 0 0 30px rgba(0, 0, 0, 0.7);
border-radius: 15px;
display: none; /* Hidden by default, shown after Start is clicked */
}
/* Welcome screen */
#welcome-screen {
width: 800px;
height: 600px;
border: none;
padding: 20px;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.9), rgba(50, 50, 50, 0.9));
position: relative;
box-shadow: 0 0 30px rgba(0, 0, 0, 0.7);
border-radius: 15px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
#welcome-screen h1 {
font-size: 48px;
margin-bottom: 20px;
}
#start-button {
font-size: 24px;
padding: 10px 20px;
background: linear-gradient(to bottom, #ffd100, #ffb800);
color: #000;
border: none;
cursor: pointer;
border-radius: 10px;
transition: all 0.3s ease;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.7);
}
#start-button:hover {
background: linear-gradient(to bottom, #ffea00, #ffdd00);
}
/* Combat window containing allies and boss */
#combat-window {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
}
/* Allies section */
#allies {
width: 45%;
}
/* Boss section */
#boss {
width: 45%;
}
/* Health bar styling */
.health-bar {
height: 30px;
background: linear-gradient(to right, #232526, #414345);
margin-bottom: 10px;
position: relative;
cursor: pointer;
border-radius: 10px;
overflow: hidden;
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.7);
}
.health-bar::before {
content: '';
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
background: linear-gradient(to right, #32cd32, #228b22);
transition: width 0.3s ease;
border-radius: 10px;
}
/* Selected ally health bar */
.health-bar.selected::before {
background: linear-gradient(to right, #00bfff, #1e90ff);
}
/* Dead ally health bar */
.health-bar.dead::before {
background: linear-gradient(to right, #696969, #808080);
}
/* Bleeding ally health bar */
.health-bar.bleeding::before {
background-image: linear-gradient(45deg, #32cd32 25%, #ff4500 25%, #ff4500 50%, #32cd32 50%, #32cd32 75%, #ff4500 75%, #ff4500 100%);
background-size: 50px 50px;
animation: bleed-animation 1s linear infinite;
}
@keyframes bleed-animation {
0% {
background-position: 0 0;
}
100% {
background-position: 50px 0;
}
}
/* Health bar text */
.health-bar-text {
position: absolute;
width: 100%;
text-align: center;
line-height: 30px;
z-index: 1;
text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.8);
color: #fff;
}
/* Boss health bar */
#boss-health-bar {
height: 50px;
}
#boss-health-bar::before {
background: linear-gradient(to right, #ff4500, #b22222);
}
/* Spell buttons container */
#spell-buttons {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
/* Individual spell button */
.spell-button {
width: 18%;
height: 50px;
background: linear-gradient(to bottom, #4a4a4a, #2e2e2e);
border: 1px solid #ffd100;
color: #ffd100;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 14px;
margin: 4px 2px;
cursor: pointer;
position: relative;
overflow: hidden;
border-radius: 10px;
transition: all 0.3s ease;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);
}
.spell-button:hover {
background: linear-gradient(to bottom, #666, #444);
}
/* Disabled spell button */
.spell-button:disabled {
background: linear-gradient(to bottom, #333, #111);
border-color: #666;
color: #666;
cursor: not-allowed;
}
/* Cooldown progress bar on spell buttons */
.cooldown-progress {
position: absolute;
top: 0;
right: 0;
height: 100%;
width: 0;
background-color: rgba(0, 0, 0, 0.5);
transition: width 0.1s linear;
}
/* Mana bar container */
#mana-bar {
width: 100%;
height: 20px;
background: linear-gradient(to right, #141414, #282828);
position: relative;
border-radius: 10px;
overflow: hidden;
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.7);
}
/* Mana bar fill */
#mana-bar-fill {
height: 100%;
width: 100%;
background: linear-gradient(to right, #1e90ff, #4169e1);
transition: width 0.5s;
border-radius: 10px;
}
/* Mana text */
#mana-text {
position: absolute;
width: 100%;
text-align: center;
line-height: 20px;
color: #fff;
text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.8);
}
/* Reset button */
#reset-button {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24px;
padding: 10px 20px;
background: linear-gradient(to bottom, #ffd100, #ffb800);
color: #000;
border: none;
cursor: pointer;
display: none;
border-radius: 10px;
transition: all 0.3s ease;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.7);
}
#reset-button:hover {
background: linear-gradient(to bottom, #ffea00, #ffdd00);
}
/* Game over and victory messages */
#game-over, #victory {
position: absolute;
top: 40%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 36px;
display: none;
text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.8);
}
#game-over {
color: #ff4500;
}
#victory {
color: #32cd32;
}
/* Accordion styles */
.accordion {
background: linear-gradient(to right, #333, #444);
color: #ffd100;
cursor: pointer;
padding: 18px;
width: 100%;
text-align: left;
border: none;
outline: none;
transition: 0.4s;
font-size: 16px;
border-radius: 10px;
margin-top: 10px;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);
}
.accordion:hover {
background: linear-gradient(to right, #444, #555);
}
.panel {
padding: 0 18px;
background: linear-gradient(to bottom, #1a1a1a, #222);
max-height: 0;
overflow: hidden;
transition: max-height 0.2s ease-out;
border-radius: 0 0 10px 10px;
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.7);
}
/* Markdown styles */
.markdown {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #ccc;
}
.markdown h1, .markdown h2, .markdown h3 {
color: #ffd100;
}
.markdown code {
background-color: #333;
padding: 2px 4px;
border-radius: 4px;
}
.markdown pre {
background-color: #333;
padding: 10px;
border-radius: 4px;
overflow-x: auto;
}
</style>
</head>
<body>
<!-- Welcome screen -->
<div id="welcome-screen">
<h1>Welcome to Tiny Healer</h1>
<button id="start-button">Start Game</button>
</div>
<!-- Main game container -->
<div id="game-container">
<!-- Combat window containing allies and boss -->
<div id="combat-window">
<!-- Allies section -->
<div id="allies"></div>
<!-- Boss section -->
<div id="boss">
<!-- Boss health bar -->
<div id="boss-health-bar" class="health-bar">
<div class="health-bar-text">Boss HP: 5000/5000</div>
</div>
</div>
</div>
<!-- Spell buttons container -->
<div id="spell-buttons"></div>
<!-- Mana bar -->
<div id="mana-bar">
<div id="mana-bar-fill"></div>
<div id="mana-text">Mana: 100/100</div>
</div>
<!-- Game over message -->
<div id="game-over">GAME OVER</div>
<!-- Victory message -->
<div id="victory">VICTORY!</div>
<!-- Reset button -->
<button id="reset-button">RESET</button>
<!-- Spellbook Accordion -->
<button class="accordion">Spellbook</button>
<div class="panel">
<div id="spellbook"></div>
</div>
<!-- Info Accordion -->
<button class="accordion">Info</button>
<div class="panel">
<div id="info" class="markdown">
<h1>Tiny Healer</h1>
<h2>TL;DR</h2>
<p>Tiny Healer is made with the gameplay loop of a healer in World of Warcraft in mind. Healing in a dungeon minus the game.</p>
<h2>Rules</h2>
<ul>
<li>You are a healer, you cannot die yourself.</li>
<li>Target your allies and cast spells to heal them (keybinds are 1-5).</li>
<li>Allies automatically attack the boss.</li>
<li>Game Over when all allies are dead.</li>
<li>Victory when the boss is dead.</li>
</ul>
<h2>Mechanics</h2>
<ul>
<li>The boss deals damage to a random ally every second.</li>
<li>The boss periodically applies a Bleed to a random ally.</li>
<li>You regenerate Mana passively.</li>
<li>The Global Cooldown is 1.5 sec.</li>
</ul>
<h2>Usage</h2>
<p>It's only a few hundred lines of Pygame! The idea is to have a structure to tweak/tune mechanics and create your own boss encounters.</p>
</div>
</div>
<script>
// Array to store ally objects
const allies = [];
// Boss object
let boss = { hp: 5000, maxHp: 5000 };
// Currently selected ally
let selectedAlly = null;
// Global cooldown flag
let globalCooldown = false;
// Game active flag
let gameActive = true;
// Player's current mana
let playerMana = 100;
// Maximum mana
const maxMana = 100;
// Boss bleed ability cooldown
let bossBleedCooldown = 0;
// Spell definitions
const spells = [
{ name: "Quick Heal", cooldown: 0, heal: 5, manaCost: 3, description: "A fast, low-cost heal that restores 5 HP." },
{ name: "Strong Heal", cooldown: 15, heal: 30, manaCost: 5, description: "A powerful heal that restores 30 HP, but has a longer cooldown." },
{ name: "Healing Stream", cooldown: 45, heal: 3, duration: 10, interval: 0.5, manaCost: 6, description: "Heals the target for 3 HP every 0.5 seconds for 10 seconds." },
{ name: "Chain Heal", cooldown: 15, heal: 10, targets: 3, manaCost: 10, description: "Heals 3 random allies for 10 HP each." },
{ name: "Team Heal", cooldown: 120, heal: 30, manaCost: 20, description: "Heals all allies for 30 HP, but has a very long cooldown." }
];
// Function to create an ally object
function createAlly(id) {
const allyTypes = [
{ type: "Tank", maxHp: 300 }, // First ally is a Tank
{ type: "Rogue", maxHp: 100 }, // Second ally is a Rogue
{ type: "Rogue", maxHp: 100 }, // Third ally is a Rogue
{ type: "Mage", maxHp: 100 }, // Fourth ally is a Mage
{ type: "Mage", maxHp: 100 }, // Fifth ally is a Mage
];
const ally = allyTypes[id];
return { id, type: ally.type, hp: ally.maxHp, maxHp: ally.maxHp, alive: true, bleeding: false };
}
// Function to update health bars
function updateHealthBar(entity, barElement) {
const percentage = (entity.hp / entity.maxHp) * 100;
barElement.style.width = `${percentage}%`;
barElement.querySelector('.health-bar-text').textContent = `${entity.type} HP: ${entity.hp}/${entity.maxHp}`;
if (entity.hp <= 0) {
barElement.classList.add('dead');
}
if (entity.bleeding) {
barElement.classList.add('bleeding');
} else {
barElement.classList.remove('bleeding');
}
}
// Function to update the mana bar
function updateManaBar() {
const manaBarFill = document.getElementById('mana-bar-fill');
const manaText = document.getElementById('mana-text');
const percentage = (playerMana / maxMana) * 100;
manaBarFill.style.width = `${percentage}%`;
manaText.textContent = `Mana: ${playerMana}/${maxMana}`;
}
// Function to create ally health bars
function createAllies() {
const alliesContainer = document.getElementById('allies');
alliesContainer.innerHTML = '';
allies.length = 0;
for (let i = 0; i < 5; i++) {
const ally = createAlly(i);
allies.push(ally);
const healthBar = document.createElement('div');
healthBar.className = 'health-bar';
healthBar.innerHTML = `<div class="health-bar-text">${ally.type} HP: ${ally.hp}/${ally.maxHp}</div>`;
healthBar.onclick = () => selectAlly(i);
alliesContainer.appendChild(healthBar);
updateHealthBar(ally, healthBar);
}
}
// Function to select an ally
function selectAlly(index) {
if (!allies[index].alive) return;
const healthBars = document.querySelectorAll('#allies .health-bar');
healthBars.forEach(bar => bar.classList.remove('selected'));
healthBars[index].classList.add('selected');
selectedAlly = allies[index];
updateSpellButtons();
}
// Function to create spell buttons
function createSpellButtons() {
const spellButtonsContainer = document.getElementById('spell-buttons');
spellButtonsContainer.innerHTML = '';
spells.forEach((spell, index) => {
const button = document.createElement('button');
button.className = 'spell-button';
button.innerHTML = `${spell.name}<br>(${spell.manaCost} Mana)<div class="cooldown-progress"></div>`;
button.onclick = () => castSpell(index);
button.disabled = true;
spellButtonsContainer.appendChild(button);
});
}
// Function to update spell button states
function updateSpellButtons() {
const spellButtons = document.querySelectorAll('.spell-button');
spellButtons.forEach((button, index) => {
button.disabled = !selectedAlly || !selectedAlly.alive || globalCooldown || button.classList.contains('on-cooldown') || !gameActive || playerMana < spells[index].manaCost;
});
}
// Function to cast a spell
function castSpell(spellIndex) {
const spell = spells[spellIndex];
if (selectedAlly && selectedAlly.alive && !globalCooldown && !document.querySelectorAll('.spell-button')[spellIndex].classList.contains('on-cooldown') && gameActive && playerMana >= spell.manaCost) {
playerMana -= spell.manaCost;
updateManaBar();
switch (spellIndex) {
case 0:
case 1:
healAlly(selectedAlly, spell.heal);
break;
case 2:
startHealingStream(selectedAlly, spell);
break;
case 3:
chainHeal(spell);
break;
case 4:
teamHeal(spell);
break;
}
startSpellCooldown(spellIndex);
startGlobalCooldown();
}
}
// Function to heal an ally
function healAlly(ally, amount) {
if (ally.alive) {
ally.hp = Math.min(ally.hp + amount, ally.maxHp);
updateHealthBar(ally, document.querySelectorAll('.health-bar')[ally.id]);
}
}
// Function to start a healing stream
function startHealingStream(ally, spell) {
let ticks = spell.duration / spell.interval;
function tick() {
if (ticks > 0 && ally.alive && gameActive) {
healAlly(ally, spell.heal);
ticks--;
setTimeout(tick, spell.interval * 1000);
}
}
tick();
}
// Function to perform chain heal
function chainHeal(spell) {
const livingAllies = allies.filter(ally => ally.alive);
const targets = livingAllies.sort(() => 0.5 - Math.random()).slice(0, spell.targets);
targets.forEach(ally => healAlly(ally, spell.heal));
}
// Function to heal all allies
function teamHeal(spell) {
allies.forEach(ally => healAlly(ally, spell.heal));
}
// Function to start spell cooldown
function startSpellCooldown(spellIndex) {
const button = document.querySelectorAll('.spell-button')[spellIndex];
const progressBar = button.querySelector('.cooldown-progress');
button.classList.add('on-cooldown');
progressBar.style.transition = 'width 0.1s linear';
progressBar.style.width = '100%';
setTimeout(() => {
progressBar.style.transition = `width ${spells[spellIndex].cooldown}s linear`;
progressBar.style.width = '0';
}, 100);
setTimeout(() => {
button.classList.remove('on-cooldown');
updateSpellButtons();
}, spells[spellIndex].cooldown * 1000);
}
// Function to start global cooldown
function startGlobalCooldown() {
globalCooldown = true;
updateSpellButtons();
setTimeout(() => {
globalCooldown = false;
updateSpellButtons();
}, 750); // GCD is 0.75 seconds
}
// Function for boss to damage allies
function bossDamage() {
if (!gameActive) return;
const damageAmount = 5;
const livingAllies = allies.filter(ally => ally.alive);
if (livingAllies.length === 0) {
endGame();
return;
}
const targetAlly = livingAllies[Math.floor(Math.random() * livingAllies.length)];
targetAlly.hp = Math.max(targetAlly.hp - damageAmount, 0);
if (targetAlly.hp === 0) {
targetAlly.alive = false;
}
updateHealthBar(targetAlly, document.querySelectorAll('.health-bar')[targetAlly.id]);
if (allies.every(ally => !ally.alive)) {
endGame();
}
}
// Function for boss bleed ability
function bossBleedAbility() {
if (!gameActive) return;
bossBleedCooldown--;
if (bossBleedCooldown <= 0) {
const livingAllies = allies.filter(ally => ally.alive);
if (livingAllies.length > 0) {
const targetAlly = livingAllies[Math.floor(Math.random() * livingAllies.length)];
targetAlly.bleeding = true;
updateHealthBar(targetAlly, document.querySelectorAll('.health-bar')[targetAlly.id]);
let bleedTicks = 4;
function bleedTick() {
if (bleedTicks > 0 && targetAlly.alive && gameActive) {
targetAlly.hp = Math.max(targetAlly.hp - 10, 0);
updateHealthBar(targetAlly, document.querySelectorAll('.health-bar')[targetAlly.id]);
bleedTicks--;
if (bleedTicks === 0) {
targetAlly.bleeding = false;
updateHealthBar(targetAlly, document.querySelectorAll('.health-bar')[targetAlly.id]);
} else {
setTimeout(bleedTick, 1000);
}
}
}
bleedTick();
}
bossBleedCooldown = 15;
}
}
// Function for allies to attack boss
function alliesAttack() {
if (!gameActive) return;
const damagePerAlly = 5;
const livingAllies = allies.filter(ally => ally.alive);
const totalDamage = livingAllies.length * damagePerAlly;
boss.hp = Math.max(boss.hp - totalDamage, 0);
updateHealthBar(boss, document.getElementById('boss-health-bar'));
if (boss.hp <= 0) {
victory();
}
}
// Function to regenerate mana
function regenerateMana() {
if (gameActive && playerMana < maxMana) {
playerMana = Math.min(playerMana + 1, maxMana);
updateManaBar();
}
}
// Function to end the game (loss)
function endGame() {
gameActive = false;
document.getElementById('game-over').style.display = 'block';
document.getElementById('reset-button').style.display = 'block';
updateSpellButtons();
}
// Function to end the game (victory)
function victory() {
gameActive = false;
document.getElementById('victory').style.display = 'block';
document.getElementById('reset-button').style.display = 'block';
updateSpellButtons();
}
// Function to reset the game and return to the welcome screen
function resetGame() {
document.getElementById('game-container').style.display = 'none';
document.getElementById('welcome-screen').style.display = 'flex';
gameActive = true;
document.getElementById('game-over').style.display = 'none';
document.getElementById('victory').style.display = 'none';
document.getElementById('reset-button').style.display = 'none';
createAllies();
createSpellButtons();
boss = { hp: 5000, maxHp: 5000 };
updateHealthBar(boss, document.getElementById('boss-health-bar'));
selectedAlly = null;
globalCooldown = false;
playerMana = maxMana;
bossBleedCooldown = 15;
updateManaBar();
updateSpellButtons();
}
// Main game loop
function gameLoop() {
if (gameActive) {
bossDamage();
bossBleedAbility();
alliesAttack();
setTimeout(gameLoop, 1000);
}
}
// Function to populate the spellbook
function populateSpellbook() {
const spellbookContainer = document.getElementById('spellbook');
spellbookContainer.innerHTML = '';
spells.forEach(spell => {
const spellInfo = document.createElement('div');
spellInfo.innerHTML = `<h3>${spell.name}</h3>
<p>${spell.description}</p>
<p>Mana Cost: ${spell.manaCost}</p>
<p>Cooldown: ${spell.cooldown} seconds</p>`;
spellbookContainer.appendChild(spellInfo);
});
}
// Set up mana regeneration interval
setInterval(regenerateMana, 2000);
// Event listener for keyboard input
document.addEventListener('keydown', (event) => {
const key = event.key;
if (key >= '1' && key <= '5') {
castSpell(parseInt(key) - 1);
}
});
// Event listener for reset button
document.getElementById('reset-button').addEventListener('click', resetGame);
// Event listener for start button
document.getElementById('start-button').addEventListener('click', () => {
document.getElementById('welcome-screen').style.display = 'none';
document.getElementById('game-container').style.display = 'block';
gameLoop(); // Start the game loop
});
// Event listeners for accordions
const accordions = document.getElementsByClassName("accordion");
for (let i = 0; i < accordions.length; i++) {
accordions[i].addEventListener("click", function() {
this.classList.toggle("active");
const panel = this.nextElementSibling;
if (panel.style.maxHeight) {
panel.style.maxHeight = null;
} else {
panel.style.maxHeight = panel.scrollHeight + "px";
}
});
}
// Initial game setup
createAllies();
createSpellButtons();
updateHealthBar(boss, document.getElementById('boss-health-bar'));
updateManaBar();
populateSpellbook();
</script>
</body>
</html>