Meta Description" name="description" />
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Buff Man: The Full Saga</title>
<style>
body {
background-color: #1a1a1a;
color: white;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
min-height: 100vh;
margin: 0;
font-family: 'Courier New', Courier, monospace;
overflow: hidden;
user-select: none;
touch-action: manipulation;
}
h1 { margin: 10px 0; font-size: 18px; color: #aaa; text-transform: uppercase; }
/* HUD */
#hud {
display: flex;
justify-content: space-between;
width: 440px;
padding: 8px;
background: #000;
border: 2px solid #444;
border-bottom: none;
box-sizing: border-box;
font-weight: bold;
visibility: hidden;
}
.lives { color: #ff3333; letter-spacing: 2px; }
.key-slot { color: #555; }
.key-active { color: #ffd700; text-shadow: 0 0 10px #ffd700; }
.gum-active { color: #d000ff; animation: pulse 0.5s infinite alternate; }
@keyframes pulse { from { opacity: 1; } to { opacity: 0.5; } }
/* GAME AREA */
.game-container {
position: relative;
width: 440px;
height: 440px;
}
canvas {
background-color: #000;
border: 4px solid #444;
box-shadow: 0 0 20px rgba(0,0,0,0.8);
image-rendering: pixelated;
display: block;
}
/* OVERLAYS */
#start-screen {
position: absolute; top: 0; left: 0;
width: 100%; height: 100%;
background: rgba(0,0,0,0.9);
display: flex; flex-direction: column;
justify-content: center; align-items: center;
z-index: 20;
border: 4px solid #444; box-sizing: border-box;
}
#start-btn {
padding: 15px 30px; font-size: 24px;
font-family: 'Courier New', Courier, monospace; font-weight: bold;
background: #ff3333; color: white; border: none; border-radius: 5px;
cursor: pointer; text-transform: uppercase; box-shadow: 0 0 10px red;
display: none;
}
#start-btn:hover { background: #ff6666; transform: scale(1.05); }
#loading-txt { font-size: 18px; color: #888; animation: blink 1s infinite; }
@keyframes blink { 50% { opacity: 0.5; } }
#msg {
position: absolute; top: 180px; width: 100%;
text-align: center; font-size: 24px; font-weight: bold;
color: white; text-shadow: 2px 2px 0 #000;
pointer-events: none; opacity: 0; transition: opacity 0.3s;
z-index: 10;
}
/* CONTROLS */
.controls {
margin-top: 15px;
display: grid;
grid-template-columns: repeat(3, 60px);
grid-template-rows: repeat(2, 60px);
gap: 5px; justify-items: center;
}
.btn {
width: 60px; height: 60px;
background: #333; border: 2px solid #555; border-radius: 10px;
color: white; font-size: 24px;
display: flex; justify-content: center; align-items: center;
cursor: pointer;
}
.btn:active { background: #555; transform: scale(0.95); }
.btn-up { grid-column: 2; grid-row: 1; }
.btn-left { grid-column: 1; grid-row: 2; }
.btn-down { grid-column: 2; grid-row: 2; }
.btn-right { grid-column: 3; grid-row: 2; }
</style>
</head>
<body>
<h1>Buff Man Saga</h1>
<div id="hud">
<div class="stat"><span id="lives-txt" class="lives">❤️❤️❤️</span></div>
<div class="stat"><span id="key-icon" class="key-slot">🗝️ NEED KEY</span></div>
<div class="stat"><span id="gum-txt" style="display:none">IMMORTAL: <span id="timer-val">0</span>s</span></div>
</div>
<div class="game-container">
<div id="msg">LOCKED!</div>
<div id="start-screen">
<h2 style="font-size: 30px; margin-bottom: 10px; color:white;">BUFF MAN</h2>
<p style="color:#aaa; margin-bottom: 30px; font-size: 14px;">The Romance & The Twist</p>
<div id="loading-txt">LOADING...</div>
<button id="start-btn" onclick="startGame()">START GAME</button>
</div>
<canvas id="gameCanvas" width="440" height="440"></canvas>
</div>
<div class="controls">
<div class="btn btn-up" onclick="move(0, -1)">▲</div>
<div class="btn btn-left" onclick="move(-1, 0)">◀</div>
<div class="btn btn-down" onclick="move(0, 1)">▼</div>
<div class="btn btn-right" onclick="move(1, 0)">▶</div>
</div>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const ui = {
lives: document.getElementById('lives-txt'),
key: document.getElementById('key-icon'),
gum: document.getElementById('gum-txt'),
timer: document.getElementById('timer-val'),
msg: document.getElementById('msg'),
hud: document.getElementById('hud'),
startScreen: document.getElementById('start-screen'),
startBtn: document.getElementById('start-btn'),
loadTxt: document.getElementById('loading-txt')
};
const TILE = 40;
const GRID_W = 11;
const GRID_H = 11;
const SPEED_NORMAL = 0.2;
const SPEED_BOOST = 0.4;
/** ASSETS **/
const svgHero = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 450" width="${TILE}" height="${TILE}"><path d="M 80 150 C 50 150 50 50 150 50 C 250 50 250 150 220 180 C 200 230 100 230 80 150 Z" stroke="white" stroke-width="20" fill="none"/><path d="M 110 215 L 100 290 Q 150 300 200 290 L 190 215" stroke="white" stroke-width="20" fill="none"/><path d="M 110 220 C 60 220 90 220 40 220 L 40 180" stroke="white" stroke-width="20" fill="none"/><path d="M 190 220 C 240 220 210 220 260 220 L 260 200" stroke="white" stroke-width="20" fill="none"/><path d="M 110 290 L 80 380" stroke="white" stroke-width="20" fill="none"/><path d="M 190 290 L 220 380" stroke="white" stroke-width="20" fill="none"/></svg>`;
// Small Baby (Hero scaled down)
const svgBaby = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 450" width="${TILE}" height="${TILE}"><g transform="scale(0.8, 0.8) translate(50, 50)"><path d="M 150 50 C 100 50 100 150 150 150 C 200 150 200 50 150 50 Z" stroke="white" stroke-width="25" fill="none"/><line x1="150" y1="150" x2="150" y2="250" stroke="white" stroke-width="25"/><line x1="150" y1="250" x2="100" y2="350" stroke="white" stroke-width="25"/><line x1="150" y1="250" x2="200" y2="350" stroke="white" stroke-width="25"/><line x1="80" y1="200" x2="220" y2="200" stroke="white" stroke-width="25"/></g></svg>`;
const svgPrincess = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 450" width="${TILE}" height="${TILE}"><path d="M 150 140 L 50 380 L 250 380 Z" fill="#ff69b4" stroke="white" stroke-width="10"/><circle cx="150" cy="90" r="50" fill="white"/><path d="M 100 50 L 125 20 L 150 50 L 175 20 L 200 50" stroke="gold" stroke-width="10" fill="none"/><path d="M 135 100 L 135 130" stroke="#00ffff" stroke-width="8"/><path d="M 165 100 L 165 130" stroke="#00ffff" stroke-width="8"/></svg>`;
const svgBhanu = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 450" width="${TILE}" height="${TILE}"><circle cx="150" cy="150" r="60" stroke="white" stroke-width="15" fill="none"/><line x1="150" y1="210" x2="150" y2="350" stroke="white" stroke-width="15"/><line x1="150" y1="350" x2="100" y2="440" stroke="white" stroke-width="15"/><line x1="150" y1="350" x2="200" y2="440" stroke="white" stroke-width="15"/><line x1="150" y1="250" x2="80" y2="220" stroke="white" stroke-width="15"/><line x1="150" y1="250" x2="220" y2="220" stroke="white" stroke-width="15"/><text x="150" y="80" font-family="monospace" font-size="60" fill="yellow" text-anchor="middle">BHANU</text></svg>`;
const svgTerrorist = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 450" width="${TILE}" height="${TILE}"><circle cx="150" cy="100" r="60" fill="#333" stroke="red" stroke-width="8"/><rect x="110" y="80" width="80" height="20" fill="white"/><line x1="150" y1="160" x2="150" y2="300" stroke="white" stroke-width="20"/><line x1="150" y1="200" x2="260" y2="200" stroke="white" stroke-width="15"/><rect x="250" y="170" width="50" height="40" fill="#555"/><path d="M 150 300 L 100 400" stroke="white" stroke-width="20"/><path d="M 150 300 L 200 400" stroke="white" stroke-width="20"/></svg>`;
const svgKey = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path d="M 30 50 L 80 50 M 70 50 L 70 70 M 80 50 L 80 70" stroke="gold" stroke-width="10" stroke-linecap="round"/><circle cx="30" cy="50" r="15" stroke="gold" stroke-width="10" fill="none"/></svg>`;
const svgFruit = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="55" r="30" fill="#bd00ff"/><path d="M 50 25 L 50 10" stroke="green" stroke-width="6"/></svg>`;
const imgs = {};
function load(name, svg) {
const i = new Image();
i.src = 'data:image/svg+xml;base64,' + btoa(svg);
imgs[name] = i;
}
load('hero', svgHero);
load('baby', svgBaby);
load('princess', svgPrincess);
load('bhanu', svgBhanu);
load('terrorist', svgTerrorist);
load('key', svgKey);
load('fruit', svgFruit);
/** GAME VARS **/
let level = 1;
let lives = 3;
let hasKey = false;
let gumTimer = 0;
let state = 'MENU';
let map = [];
let items = [];
let ghosts = [];
const p = { x: 1, y: 1, vx: 1, vy: 1, tx: 1, ty: 1, moving: false, face: 1 };
const maps = {
1: [
[1,1,1,1,1,1,1,1,1,1,1],
[1,0,0,0,0,0,1,0,0,0,1],
[1,0,1,1,1,0,1,0,1,0,1],
[1,0,1,0,0,0,0,0,1,0,1],
[1,0,1,0,1,1,1,0,1,0,1],
[1,0,0,0,0,0,0,0,0,0,1],
[1,1,1,0,1,1,1,0,1,1,1],
[1,0,0,0,0,0,1,0,0,0,1],
[1,0,1,1,1,0,2,2,2,0,1],
[1,0,0,0,0,0,2,0,2,0,1],
[1,1,1,1,1,1,1,1,1,1,1]
],
2: [
[1,1,1,1,1,1,1,1,1,1,1],
[1,0,0,0,1,0,0,0,0,0,1],
[1,0,1,0,1,0,1,1,1,0,1],
[1,0,1,0,0,0,0,0,1,0,1],
[1,0,1,1,1,1,1,0,1,0,1],
[1,0,0,0,0,0,0,0,0,0,1],
[1,0,1,1,1,0,1,1,1,0,1],
[1,0,0,0,0,0,0,0,0,0,1],
[1,0,1,1,0,1,0,1,1,0,1],
[1,0,0,0,0,1,0,0,0,0,1],
[1,1,1,1,1,1,1,1,1,1,1]
]
};
function showMsg(text, color="white") {
ui.msg.innerText = text;
ui.msg.style.color = color;
ui.msg.style.opacity = 1;
setTimeout(() => { ui.msg.style.opacity = 0; }, 2000);
}
function startGame() {
ui.startScreen.style.display = 'none';
ui.hud.style.visibility = 'visible';
initLevel(1);
}
function updateHUD() {
let h = ""; for(let i=0; i<lives; i++) h += "❤️";
ui.lives.innerText = h;
if(hasKey) { ui.key.innerText = "🗝️ KEY FOUND"; ui.key.className = "key-slot key-active"; }
else { ui.key.innerText = "🗝️ NEED KEY"; ui.key.className = "key-slot"; }
if(gumTimer > 0) {
ui.gum.style.display = 'inline'; ui.gum.className = "gum-active";
ui.timer.innerText = Math.ceil(gumTimer);
} else {
ui.gum.style.display = 'none';
}
}
function initLevel(lvl) {
level = lvl;
state = 'PLAY';
hasKey = false;
p.x = 1; p.y = 1; p.vx = 1; p.vy = 1; p.tx = 1; p.ty = 1; p.moving = false;
map = JSON.parse(JSON.stringify(maps[lvl]));
items = []; ghosts = [];
if(lvl === 1) {
showMsg("LEVEL 1: SAVE PRINCESS");
items.push({x: 9, y: 1, type: 'key'});
items.push({x: 5, y: 5, type: 'fruit'});
items.push({x: 7, y: 9, type: 'princess'});
ghosts.push({x: 5, y: 3, type: 'ghost', speed: 0.04, dir: {x:0, y:1}, offX:0, offY:0});
ghosts.push({x: 8, y: 6, type: 'ghost', speed: 0.04, dir: {x:-1, y:0}, offX:0, offY:0});
} else if(lvl === 2) {
showMsg("LEVEL 2: SAVE BHANU");
items.push({x: 1, y: 8, type: 'key'});
items.push({x: 9, y: 9, type: 'bhanu'});
ghosts.push({x: 8, y: 2, type: 'terrorist', speed: 0.07, dir: {x:0, y:1}, offX:0, offY:0});
ghosts.push({x: 5, y: 5, type: 'ghost', speed: 0.04, dir: {x:1, y:0}, offX:0, offY:0});
}
updateHUD();
}
function move(dx, dy) {
if(state !== 'PLAY') return;
if(!p.moving) {
const nx = p.x + dx;
const ny = p.y + dy;
if(dx !== 0) p.face = dx;
if(map[ny][nx] !== 1) {
p.tx = nx; p.ty = ny; p.moving = true;
}
}
}
window.addEventListener('keydown', e => {
if(state === 'OVER' && e.key === 'Enter') location.reload();
if(e.key === 'ArrowUp') move(0, -1);
if(e.key === 'ArrowDown') move(0, 1);
if(e.key === 'ArrowLeft') move(-1, 0);
if(e.key === 'ArrowRight') move(1, 0);
});
function gameLoop(dt) {
if(state === 'PLAY') {
if(gumTimer > 0) { gumTimer -= dt; updateHUD(); }
if(p.moving) {
const speed = (gumTimer > 0) ? SPEED_BOOST : SPEED_NORMAL;
const dx = p.tx - p.vx;
const dy = p.ty - p.vy;
if(Math.abs(dx) < speed && Math.abs(dy) < speed) {
p.vx = p.tx; p.vy = p.ty; p.x = p.tx; p.y = p.ty;
p.moving = false; checkCollisions();
} else {
p.vx += Math.sign(dx) * speed; p.vy += Math.sign(dy) * speed;
}
}
ghosts.forEach(g => {
if(g.offX === 0 && g.offY === 0) {
const dirs = [{x:0, y:-1}, {x:0, y:1}, {x:-1, y:0}, {x:1, y:0}];
let valid = [];
dirs.forEach(d => {
if(d.x === -g.dir.x && d.y === -g.dir.y) return;
if(map[g.y + d.y][g.x + d.x] !== 1) valid.push(d);
});
if(valid.length > 0) {
if(g.type === 'terrorist' && Math.random() > 0.2) {
valid.sort((a,b) => (Math.abs((g.x+a.x)-p.x)+Math.abs((g.y+a.y)-p.y)) - (Math.abs((g.x+b.x)-p.x)+Math.abs((g.y+b.y)-p.y)));
g.dir = valid[0];
} else g.dir = valid[Math.floor(Math.random() * valid.length)];
} else g.dir = {x: -g.dir.x, y: -g.dir.y};
}
g.offX += g.dir.x * g.speed; g.offY += g.dir.y * g.speed;
if(Math.abs(g.offX) >= 1 || Math.abs(g.offY) >= 1) {
g.x += g.dir.x; g.y += g.dir.y; g.offX = 0; g.offY = 0;
}
if(Math.sqrt((p.vx-(g.x+g.offX))**2+(p.vy-(g.y+g.offY))**2) < 0.6) {
if(gumTimer <= 0) die();
}
});
}
}
function checkCollisions() {
const idx = items.findIndex(i => i.x === p.x && i.y === p.y);
if(idx !== -1) {
const item = items[idx];
if(item.type === 'fruit') { gumTimer = 300; items.splice(idx, 1); showMsg("IMMORTAL!", "#d000ff"); }
else if(item.type === 'key') { hasKey = true; items.splice(idx, 1); showMsg("GOT KEY!", "gold"); updateHUD(); }
else if(item.type === 'princess') {
if(hasKey) { showMsg("SAVED HER!"); setTimeout(() => initLevel(2), 1500); }
else showMsg("LOCKED!", "red");
}
else if(item.type === 'bhanu') {
if(hasKey) { state = 'END'; playEnding(); }
else showMsg("LOCKED!", "red");
}
}
}
function die() {
lives--; updateHUD();
showMsg("HIT! RESPAWNING...", "red");
p.x = 1; p.y = 1; p.vx = 1; p.vy = 1; p.tx = 1; p.ty = 1; p.moving = false;
if(lives <= 0) state = 'OVER';
}
function draw() {
ctx.fillStyle = "#111"; ctx.fillRect(0, 0, canvas.width, canvas.height);
if(state === 'MENU') return;
for(let y=0; y<GRID_H; y++) {
for(let x=0; x<GRID_W; x++) {
const t = map[y][x]; const px = x*TILE; const py = y*TILE;
if(t === 1) { ctx.fillStyle = "#333"; ctx.fillRect(px, py, TILE, TILE); ctx.strokeStyle = "#444"; ctx.strokeRect(px, py, TILE, TILE); }
}
}
items.forEach(i => {
const px = i.x*TILE; const py = i.y*TILE;
if(i.type === 'fruit') ctx.drawImage(imgs.fruit, px, py, TILE, TILE);
if(i.type === 'key') ctx.drawImage(imgs.key, px, py, TILE, TILE);
if(i.type === 'princess') ctx.drawImage(imgs.princess, px, py, TILE, TILE);
if(i.type === 'bhanu') ctx.drawImage(imgs.bhanu, px, py, TILE, TILE);
});
ctx.save(); ctx.translate(p.vx*TILE + TILE/2, p.vy*TILE + TILE/2);
if(p.face === -1) ctx.scale(-1, 1);
ctx.drawImage(imgs.hero, -TILE/2, -TILE/2, TILE, TILE);
if(gumTimer > 0) { ctx.beginPath(); ctx.arc(0, 0, TILE/1.5, 0, Math.PI*2); ctx.strokeStyle = "#d000ff"; ctx.lineWidth = 3; ctx.stroke(); }
ctx.restore();
if(level === 1) {
for(let y=0; y<GRID_H; y++) {
for(let x=0; x<GRID_W; x++) {
if(map[y][x] === 2) {
const px = x*TILE; const py = y*TILE;
ctx.strokeStyle = "#aaa"; ctx.lineWidth = 4; ctx.beginPath();
ctx.moveTo(px+10, py); ctx.lineTo(px+10, py+TILE);
ctx.moveTo(px+20, py); ctx.lineTo(px+20, py+TILE);
ctx.moveTo(px+30, py); ctx.lineTo(px+30, py+TILE); ctx.stroke();
}
}
}
}
ghosts.forEach(g => {
const px = (g.x + g.offX)*TILE; const py = (g.y + g.offY)*TILE;
if(g.type === 'terrorist') ctx.drawImage(imgs.terrorist, px, py, TILE, TILE);
else {
ctx.fillStyle = "red"; ctx.beginPath(); ctx.arc(px+TILE/2, py+TILE/2, TILE/2-4, Math.PI, 0); ctx.fill();
ctx.fillStyle = "white"; ctx.beginPath(); ctx.arc(px+14, py+16, 4, 0, Math.PI*2); ctx.arc(px+26, py+16, 4, 0, Math.PI*2); ctx.fill();
}
});
if(state === 'OVER') {
ctx.fillStyle = "rgba(0,0,0,0.8)"; ctx.fillRect(0,0, canvas.width, canvas.height);
ctx.fillStyle = "red"; ctx.font = "40px monospace"; ctx.textAlign = "center"; ctx.fillText("GAME OVER", 220, 200);
ctx.fillStyle = "white"; ctx.font = "20px monospace"; ctx.fillText("Press Enter to Retry", 220, 240);
}
}
function playEnding() {
let f = 0;
const cx = 220; const cy = 220;
function anim() {
if(state !== 'END') return;
// Clear
ctx.fillStyle = "black"; ctx.fillRect(0,0, 440, 440);
f++;
if(f < 100) {
// PART 1: The Encounter
ctx.fillStyle = "white"; ctx.font = "16px monospace"; ctx.textAlign = "center";
ctx.fillText("Bhanu: Daddy! You found the key!", cx, 150);
ctx.drawImage(imgs.hero, cx-60, cy, 60, 60);
ctx.drawImage(imgs.bhanu, cx, cy, 60, 60);
} else if (f < 150) {
// PART 2: The Red Flash (Kill)
ctx.fillStyle = "red"; ctx.font = "20px monospace"; ctx.textAlign = "center";
ctx.fillText("...", cx, 150);
ctx.save(); ctx.translate(cx-60, cy);
ctx.globalCompositeOperation = "source-over"; ctx.drawImage(imgs.hero, 0, 0, 60, 60);
ctx.globalCompositeOperation = "source-atop"; ctx.fillStyle = "red"; ctx.fillRect(0,0,60,60);
ctx.restore();
ctx.drawImage(imgs.bhanu, cx, cy, 60, 60);
} else if (f < 200) {
// PART 3: The Slash
ctx.fillStyle = "#300"; ctx.fillRect(0,0,440,440);
ctx.drawImage(imgs.hero, cx-30, cy, 60, 60);
ctx.strokeStyle = "white"; ctx.lineWidth = 5; ctx.beginPath();
ctx.moveTo(cx-20, cy+60); ctx.lineTo(cx+80, cy); ctx.stroke();
} else if (f < 300) {
// PART 4: Transition Text
ctx.fillStyle = "black"; ctx.fillRect(0,0,440,440);
ctx.fillStyle = "white"; ctx.font = "20px monospace"; ctx.textAlign = "center";
ctx.fillText("YEARS LATER...", cx, cy);
} else {
// PART 5: THE HAPPY ENDING DANCE
ctx.fillStyle = "#ffb6c1"; // Pink background
ctx.fillRect(0,0,440,440);
const bounce = Math.sin(f * 0.1) * 10;
// Parents
ctx.drawImage(imgs.hero, cx-60, cy - 20 + bounce, 60, 60);
ctx.drawImage(imgs.princess, cx, cy - 20 + bounce, 60, 60);
// 5 Children
for(let i=0; i<5; i++) {
const childBounce = Math.sin((f + i*20) * 0.2) * 8;
ctx.drawImage(imgs.baby, 60 + (i*70), cy + 60 + childBounce, 40, 40);
}
// Floating Hearts
for(let i=0; i<5; i++) {
const hY = (cy - (f*2 + i*50) % 440);
if(hY > 0) {
ctx.fillStyle = "red"; ctx.font = "30px sans-serif";
ctx.fillText("❤️", 80 + i*60, hY);
}
}
ctx.fillStyle = "#d000ff"; ctx.font = "24px monospace"; ctx.textAlign = "center";
ctx.fillText("HAPPILY EVER AFTER", cx, 100);
ctx.font = "14px monospace"; ctx.fillStyle = "#888";
ctx.fillText("(Don't ask about Bhanu)", cx, 130);
requestAnimationFrame(anim);
return;
}
requestAnimationFrame(anim);
}
anim();
}
let lastTime = 0;
function loop(timestamp) {
const dt = (timestamp - lastTime) / 1000; lastTime = timestamp;
gameLoop(dt);
if(state !== 'END') draw();
requestAnimationFrame(loop);
}
Promise.all(Object.values(imgs).map(i => new Promise(r => i.onload = r))).then(() => {
ui.loadTxt.style.display = 'none'; ui.startBtn.style.display = 'block';
requestAnimationFrame(loop);
});
</script>
</body>
</html>
1
1
25KB
25KB
118.0ms
196.0ms
119.0ms