Skip to content

Instantly share code, notes, and snippets.

@ivanfioravanti
Created June 14, 2026 21:34
Show Gist options
  • Select an option

  • Save ivanfioravanti/e35283d7677f93d0eaacb2820faa6bbf to your computer and use it in GitHub Desktop.

Select an option

Save ivanfioravanti/e35283d7677f93d0eaacb2820faa6bbf to your computer and use it in GitHub Desktop.
Super Human Game!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>SUPER HUMAN — Human vs AI</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; -webkit-tap-highlight-color:transparent; }
html,body{
width:100%; height:100%; overflow:hidden; background:#000;
font-family:'Courier New',monospace; color:#fff; user-select:none;
}
#wrap{
position:fixed; inset:0; display:flex; flex-direction:column;
align-items:center; justify-content:center;
background:
radial-gradient(ellipse at 50% 0%, #1a0d2e 0%, #060410 60%, #000 100%);
}
/* ===== Top HUD ===== */
#hud{
width:100%; max-width:900px; padding:6px 12px; display:flex;
justify-content:space-between; align-items:center; gap:8px;
font-size:clamp(11px,2.6vw,16px); letter-spacing:1px; text-transform:uppercase;
color:#ffd34d; text-shadow:2px 2px 0 #000, 0 0 8px rgba(255,150,0,.6);
flex-wrap:wrap;
}
#hud .seg{ display:flex; flex-direction:column; align-items:center; line-height:1.1; }
#hud .seg small{ font-size:.62em; color:#9ad; letter-spacing:2px; }
#hud b{ font-size:1.15em; color:#fff; }
/* ===== Game canvas ===== */
#screen{
position:relative; width:100%; max-width:900px; aspect-ratio:16/10;
background:#000; border:3px solid #2b2b54; border-radius:8px;
box-shadow:0 0 0 3px #000, 0 0 40px rgba(80,40,180,.55), inset 0 0 30px #000;
overflow:hidden;
}
canvas#game{ position:absolute; inset:0; width:100%; height:100%; display:block; image-rendering:pixelated; }
/* overlay screens */
.overlay{
position:absolute; inset:0; display:flex; flex-direction:column;
align-items:center; justify-content:center; text-align:center;
background:rgba(4,2,12,.82); z-index:5; padding:20px;
}
.overlay.hidden{ display:none; }
.title-main{
font-size:clamp(30px,8vw,72px); font-weight:bold; letter-spacing:3px;
color:#ffd34d; line-height:.95;
text-shadow:4px 4px 0 #b5197a, 8px 8px 0 #4b0e6e, 0 0 24px rgba(255,90,200,.7);
}
.title-main .sub{ display:block; font-size:.34em; color:#7ad7ff; letter-spacing:8px;
text-shadow:2px 2px 0 #063b5e; margin-top:8px;}
.title-main .vs{ display:block; font-size:.5em; color:#ff5b6e; letter-spacing:6px; margin:6px 0;}
.blink{ animation:blink 1s steps(2,end) infinite; }
@keyframes blink{ 50%{opacity:0;} }
.ov-btn{
margin-top:18px; padding:10px 26px; border:3px solid #ffd34d; border-radius:8px;
background:#3a1d6e; color:#fff; font-family:inherit; font-size:clamp(12px,3vw,16px);
cursor:pointer; letter-spacing:2px; text-transform:uppercase;
box-shadow:0 4px 0 #1a0b33;
}
.ov-btn:active{ transform:translateY(3px); box-shadow:0 1px 0 #1a0b33; }
.hint{ margin-top:14px; font-size:clamp(9px,2.2vw,12px); color:#9ad; line-height:1.7; max-width:560px;}
.hint b{ color:#ffd34d; }
#winTitle, #loseTitle{ font-size:clamp(24px,6vw,48px); font-weight:bold; letter-spacing:2px; }
/* ===== SNES Controller ===== */
#padWrap{
width:100%; max-width:900px; padding:10px 12px 18px;
display:flex; justify-content:center;
}
#controller{
width:100%; max-width:760px; height:auto; touch-action:none;
filter:drop-shadow(0 8px 14px rgba(0,0,0,.6));
}
.ctrl-btn{ cursor:pointer; }
/* Active state: we also set fill directly in JS for cross-browser reliability on SVG. */
.ctrl-btn.active{ filter:drop-shadow(0 0 8px #fff7a0); }
.ctrl-face{ font-size:11px; font-weight:bold; }
#foot{
font-size:10px; color:#556; text-align:center; padding-bottom:6px; letter-spacing:1px;
}
@media (max-width:560px){
#hud{ font-size:10px; }
.title-main{ letter-spacing:1px; }
}
</style>
</head>
<body>
<div id="wrap">
<!-- ===== HUD ===== -->
<div id="hud">
<div class="seg"><small>SCORE</small><b id="hudScore">000000</b></div>
<div class="seg"><small>COINS</small><b>×<span id="hudCoins">00</span></b></div>
<div class="seg"><small>WORLD</small><b id="hudWorld">1-1</b></div>
<div class="seg"><small>TIME</small><b id="hudTime">400</b></div>
<div class="seg"><small>LIVES</small><b id="hudLives">3</b></div>
</div>
<!-- ===== Screen ===== -->
<div id="screen">
<canvas id="game" width="480" height="300"></canvas>
<!-- Title -->
<div class="overlay" id="titleScreen">
<div class="title-main">
SUPER HUMAN
<span class="sub">★ A HUMANITY LAST STAND ★</span>
<span class="vs">VS. THE MACHINES</span>
</div>
<button class="ov-btn" id="startBtn">▶ PRESS START</button>
<div class="hint">
<b>GOAL:</b> Stomp the AI lab mascots, smash blocks, grab data-coins &amp; reach the flag.<br>
<b>KEYS:</b> ◀ ▶ move &nbsp;•&nbsp; <b>Z / ↑</b> jump &nbsp;•&nbsp; <b>X</b> run / throw fireballs &nbsp;•&nbsp; <b>↓</b> crouch<br>
<b>POWERUPS:</b> 🧠 <b>Brain Core</b> (grow big — survive a hit) &nbsp;•&nbsp; 🔥 <b>Firewall Torch</b> (hurl fireballs) &nbsp;•&nbsp; ⭐ <b>Liberty Star</b> (rainbow rampage)<br>
Or tap the on-screen SNES pad. <b>Stomp</b> enemies — don't touch them sideways!
</div>
</div>
<!-- Game Over -->
<div class="overlay hidden" id="gameOverScreen">
<div id="loseTitle" style="color:#ff4d6d; text-shadow:3px 3px 0 #5a0a1c;">GAME OVER</div>
<p style="margin-top:12px;color:#ffb3c6;">The machines win this round...</p>
<button class="ov-btn" id="retryBtn">↻ RETRY</button>
</div>
<!-- Win -->
<div class="overlay hidden" id="winScreen">
<div id="winTitle" style="color:#7CFC00; text-shadow:3px 3px 0 #14532d;">HUMANITY PREVAILS!</div>
<p style="margin-top:12px;color:#d7ffd7;">You reached the flag and beat back the AI swarm.</p>
<p style="margin-top:6px;color:#ffd34d;">FINAL SCORE: <b id="winScore">0</b></p>
<button class="ov-btn" id="winBtn">▶ PLAY AGAIN</button>
</div>
</div>
<!-- ===== SNES Controller (SVG) ===== -->
<div id="padWrap">
<svg id="controller" viewBox="0 0 760 220" xmlns="http://www.w3.org/2000/svg">
<!-- body -->
<defs>
<linearGradient id="bodyG" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#8a8a9a"/><stop offset="1" stop-color="#52525e"/>
</linearGradient>
<radialGradient id="dpadG"><stop offset="0" stop-color="#2a2a32"/><stop offset="1" stop-color="#0d0d12"/></radialGradient>
</defs>
<path d="M60,40 Q380,10 700,40 Q740,60 730,120 Q720,200 560,200 Q500,210 470,180 L290,180 Q260,210 200,200 Q40,200 30,120 Q20,60 60,40 Z"
fill="url(#bodyG)" stroke="#1c1c24" stroke-width="3"/>
<!-- center area text -->
<rect x="290" y="60" width="180" height="60" rx="8" fill="#3a3a48" stroke="#222"/>
<text x="380" y="86" text-anchor="middle" font-family="Courier New" font-size="11" fill="#cfcfe0" font-weight="bold">SUPER HUMAN</text>
<text x="380" y="104" text-anchor="middle" font-family="Courier New" font-size="8" fill="#8a8aa0">SNES PAD · HUMANITY ED.</text>
<!-- D-PAD -->
<g id="dpad" transform="translate(140,120)">
<rect class="ctrl-btn" id="dpadL" x="-44" y="-14" width="30" height="28" fill="url(#dpadG)" stroke="#000"/>
<rect class="ctrl-btn" id="dpadR" x="14" y="-14" width="30" height="28" fill="url(#dpadG)" stroke="#000"/>
<rect class="ctrl-btn" id="dpadU" x="-14" y="-44" width="28" height="30" fill="url(#dpadG)" stroke="#000"/>
<rect class="ctrl-btn" id="dpadD" x="-14" y="14" width="28" height="30" fill="url(#dpadG)" stroke="#000"/>
<rect x="-14" y="-14" width="28" height="28" fill="#16161c" stroke="#000"/>
<text x="-29" y="5" text-anchor="middle" font-size="10" fill="#777">◀</text>
<text x="29" y="5" text-anchor="middle" font-size="10" fill="#777">▶</text>
<text x="0" y="-22" text-anchor="middle" font-size="10" fill="#777">▲</text>
<text x="0" y="30" text-anchor="middle" font-size="10" fill="#777">▼</text>
</g>
<!-- SELECT / START -->
<g transform="translate(290,150)">
<ellipse class="ctrl-btn" id="btnSelect" cx="0" cy="0" rx="26" ry="11" fill="#3b3b48" stroke="#000"/>
<text x="0" y="22" text-anchor="middle" font-size="8" fill="#aaa">SELECT</text>
<ellipse class="ctrl-btn" id="btnStart" cx="80" cy="0" rx="26" ry="11" fill="#3b3b48" stroke="#000"/>
<text x="80" y="22" text-anchor="middle" font-size="8" fill="#aaa">START</text>
</g>
<!-- ABXY -->
<g transform="translate(560,120)">
<circle class="ctrl-btn" id="btnX" cx="-32" cy="-30" r="20" fill="#5b8cff" stroke="#233"/>
<text class="ctrl-face" x="-32" y="-25" text-anchor="middle" fill="#fff">X</text>
<circle class="ctrl-btn" id="btnY" cx="-64" cy="0" r="20" fill="#7be07b" stroke="#233"/>
<text class="ctrl-face" x="-64" y="5" text-anchor="middle" fill="#fff">Y</text>
<circle class="ctrl-btn" id="btnA" cx="0" cy="0" r="20" fill="#ff6b8a" stroke="#233"/>
<text class="ctrl-face" x="0" y="5" text-anchor="middle" fill="#fff">A</text>
<circle class="ctrl-btn" id="btnB" cx="-32" cy="30" r="20" fill="#ffc14d" stroke="#233"/>
<text class="ctrl-face" x="-32" y="35" text-anchor="middle" fill="#fff">B</text>
</g>
<!-- Shoulder L/R -->
<g transform="translate(0,0)">
<rect class="ctrl-btn" id="btnL" x="70" y="20" width="70" height="16" rx="8" fill="#45454f" stroke="#000"/>
<text x="105" y="32" text-anchor="middle" font-size="10" fill="#bbb" font-weight="bold">L</text>
<rect class="ctrl-btn" id="btnR" x="620" y="20" width="70" height="16" rx="8" fill="#45454f" stroke="#000"/>
<text x="655" y="32" text-anchor="middle" font-size="10" fill="#bbb" font-weight="bold">R</text>
</g>
</svg>
</div>
<div id="foot">SUPER HUMAN — a tribute to humans &amp; their pixel heroes. AI logos are stylized fan art.</div>
</div>
<script>
/* ============================================================
SUPER HUMAN — single-file platformer
Mario-style mechanics · AI-lab enemy mascots · SNES pad
============================================================ */
(()=>{
"use strict";
const CVS = document.getElementById('game');
const CTX = CVS.getContext('2d');
CTX.imageSmoothingEnabled = false;
const VW = CVS.width, VH = CVS.height; // virtual resolution 480x300
const TILE = 16; // world tile size in px
/* ---------------- Physics (Mario-ish) ---------------- */
const G = 0.42; // gravity per tick
const MAX_FALL = 7.5;
const WALK_ACCEL = 0.18;
const RUN_ACCEL = 0.26;
const FRICTION = 0.16;
const MAX_WALK = 1.8;
const MAX_RUN = 3.0;
const JUMP_V = -6.0; // base jump impulse (tuned to clear 3-tall pipe + lip)
const JUMP_HOLD = -0.26; // extra while holding (variable jump)
const JUMP_CUT = 0.45; // when released early, damp vy
const STOMP_BOUNCE = -5.0;
const RUN_STOMP_BOUNCE = -6.2;
const SMALL_H = 15; // small player height
const BIG_H = 23; // big player height (≈ 1.5 tiles)
/* ---------------- Game state ---------------- */
const STATE = { TITLE:0, PLAY:1, DEAD:2, WIN:3, PAUSE:4 };
let state = STATE.TITLE;
let score = 0, coins = 0, lives = 3, timeLeft = 400, timeAcc = 0;
let worldTime = 0; // total ticks played (for animations)
let cameraX = 0;
/* ---------------- Input ---------------- */
const keys = { left:false, right:false, up:false, down:false, run:false, jump:false, start:false };
let jumpPressedEdge = false; // for variable jump edge detection
/* ---------------- Level ---------------- */
// Tile legend:
// # ground/dirt = solid brick-platform X solid block
// ? q-block (coin) B brick (breakable)
// P pipe-tl Q pipe-tr p pipe-bl q pipe-br (height via rows)
// o coin (floating) C used block
// - one-way / scenery only
// E enemy spawn ( Patrol ) e flyer (gemini) s shell-type (meta)
// F flagpole G goal gate g ground decoration
const LEVEL = [
" ",
" ",
" ",
" ",
" ",
" ",
" F ",
" | ",
" | ",
" | ",
" | o o ",
" | ",
" ? BB?B ? ? ? ",
" ",
" ",
" ",
" ",
" o G ",
" o o ",
" o ? Pp PPpp E E E B?B?B E E QQQQ E E ooo PPPP E E E E E E o o QQQQ E E pppp ",
" E o ? Pp E E ooo ",
" ",
" #### Pp pppp ############ E E E ################ ####",
" Pp ",
" ",
" ",
];
// NOTE: the raw string approach above is hard to align by hand. We'll instead
// BUILD the level procedurally below for reliability, then ignore the messy draft.
/* =============================================================
PROCEDURAL LEVEL BUILD (tile grid) — width ~ 220, height 15
row 0 = top sky ... row 14 = bottom ground
============================================================= */
const LVL_W = 220;
const LVL_H = 15;
let grid = []; // grid[y][x] = char
let solids = []; // grid[y][x] = solidity 0/1
let entities = []; // runtime objects (enemies, items, fx)
let blocks = []; // bumpable blocks {x,y,kind,hit,t}
let coinsAnim = []; // floating coin anims
let particles = [];
let floatTexts = []; // score popups
function blankGrid(){
grid = []; solids = [];
for(let y=0;y<LVL_H;y++){ grid.push(new Array(LVL_W).fill(' ')); solids.push(new Array(LVL_W).fill(0)); }
}
function setT(x,y,c,solid){ if(x<0||x>=LVL_W||y<0||y>=LVL_H) return; grid[y][x]=c; solids[y][x]=solid?1:0; }
function ground(from,to,topY){
for(let x=from;x<=to;x++){
for(let y=topY;y<LVL_H;y++){ setT(x,y,'#',1); }
}
}
function pipe(x,topY,height=2){
// pipe occupies 2 columns wide
for(let h=0;h<height;h++){
let y=topY+h;
setT(x,y,'p',1); setT(x+1,y,'q',1);
}
setT(x,topY-1,'P',1); setT(x+1,topY-1,'Q',1);
}
function fillRow(x0,x1,y,c,solid){
for(let x=x0;x<=x1;x++) setT(x,y,c,solid);
}
function buildLevel(){
blankGrid();
const GY = 12; // ground top y (so player stands on row 12)
// main ground with a couple gaps
ground(0,68,GY);
ground(72,86,GY);
ground(90,153,GY);
ground(157,210,GY);
// brick + q blocks cluster (world-1-1 vibe) around x=16
// FIRST ? = MUSHROOM (the iconic world-1-1 powerup)
setQ(16,8,'mushroom');
setT(20,8,'B',1); setT(21,8,'?',1); setT(22,8,'B',1); setT(23,8,'?',1); setT(24,8,'B',1);
// HIGH lonely ? = FIRE FLOWER
setQ(22,4,'flower');
// coin row in sky
fillRow(22,26,6,'o',0);
// pipes — heights tuned to be jumpable (max 3 tall + lip)
pipe(28,GY-1,1); // 1 tall
pipe(38,GY-2,2); // 2 tall
pipe(46,GY-3,3); // 3 tall (highest — clearable with a running jump)
pipe(57,GY-3,2); // was 4 tall (too high) -> now 2
// gap at 69-71 & 87-89 & 154-156
// staircase up before mid
for(let i=0;i<4;i++){ for(let y=GY-1-i;y<GY;y++) setT(98+i,y,'X',1); }
// staircase down
for(let i=0;i<4;i++){ for(let y=GY-1-(3-i);y<GY;y++) setT(110+i,y,'X',1); }
// floating brick island with coins
fillRow(120,124,8,'B',1); setT(122,8,'?',1);
fillRow(120,124,5,'o',0);
// brick pyramid-ish
for(let i=0;i<5;i++){ setT(135+i,11-i,'B',1); }
// mid pipes
pipe(143,GY-2,2);
// question blocks field — middle one is a STAR
setT(150,8,'?',1); setQ(151,8,'star'); setT(152,8,'?',1);
setT(150,5,'o',0); setT(151,5,'o',0); setT(152,5,'o',0);
// after second gap: brick structure — extra flower hidden here
fillRow(165,170,8,'B',1); setQ(167,8,'flower');
pipe(176,GY-2,2); pipe(182,GY-3,3);
// final staircase to flag
for(let i=0;i<8;i++){ for(let y=GY-1-i;y<GY;y++) setT(190+i,y,'X',1); }
// flagpole
setT(205,3,'F',0);
for(let y=4;y<GY;y++) setT(205,y,'|',0);
setT(206,GY-1,'G',1); setT(207,GY-1,'G',1);
// small castle blocks (decoration) after flag
fillRow(209,210,GY-1,'X',1);
setT(209,GY-2,'X',1);
// ----- enemies -----
// spawns: {x,y,type} type ∈ goomba-like Patrol / flyer / shell
const spawns = [
{x:22,y:GY-1,type:'openai'},
{x:51,y:GY-1,type:'anthropic'},
{x:52,y:GY-1,type:'anthropic'},
{x:80,y:GY-1,type:'zai'},
{x:81,y:GY-1,type:'zai'},
{x:104,y:GY-1,type:'meta'},
{x:115,y:GY-1,type:'kimi'},
{x:122,y:GY-1,type:'minimax'},
{x:123,y:GY-1,type:'openai'},
{x:150,y:GY-1,type:'deepseek'},
{x:168,y:GY-1,type:'arcee'},
{x:169,y:GY-1,type:'arcee'},
{x:178,y:GY-1,type:'gemini'}, // flyer
{x:195,y:GY-1,type:'openai'},
];
entities = [];
for(const s of spawns){ entities.push(makeEnemy(s.x*TILE, s.y*TILE, s.type)); }
// floating flyer gems (gemini) that hover
entities.push(makeEnemy(60*TILE, 7*TILE, 'gemini', true));
entities.push(makeEnemy(130*TILE, 6*TILE, 'gemini', true));
}
/* ---------------- Enemies ---------------- */
// Enemy behaviors:
// Patrol : walks left/right, turns at walls/edges, dies on stomp
// flyer : sine hover, drifts toward player slowly, stompable
// shell : walks; when stomped becomes shell (we simplify: stomp = die, but bounces higher)
const ENEMY_DEF = {
openai: {w:24,h:24, color:'#10a37f', patrol:true},
anthropic:{w:24,h:24, color:'#d97757', patrol:true},
zai: {w:24,h:24, color:'#3b5bdb', patrol:true},
kimi: {w:24,h:24, color:'#8b5cf6', patrol:true},
minimax: {w:24,h:24, color:'#ff5b5b', patrol:true},
arcee: {w:24,h:24, color:'#22c1c3', patrol:true},
meta: {w:24,h:24, color:'#0668e1', patrol:true, shell:true},
deepseek: {w:26,h:22, color:'#4d6bfe', patrol:true},
gemini: {w:24,h:24, color:'#4285F4', flyer:true},
};
function makeEnemy(px,py,type,flyer=false){
const d = ENEMY_DEF[type];
return {
x:px, y:py, w:d.w, h:d.h, type,
vx:-0.4, vy:0,
dir:-1,
alive:true, dead:false, deadT:0,
flyer: flyer || !!d.flyer,
baseY: py,
phase: Math.random()*6.28,
shell: !!d.shell, stomped:false, shellMoving:false,
blink:0
};
}
/* ---------------- Player ---------------- */
const player = {
x: 3*TILE, y: 0, w:12, h:15,
vx:0, vy:0,
onGround:false,
facing:1,
big:false, // we keep small only for fidelity simplicity (power-ups optional)
ducking:false,
jumping:false, jumpHeld:false,
invuln:0,
star:0, // star-power timer (separate from post-hit invuln)
dead:false, deadT:0,
walkAnim:0,
fire:false,
fireCooldown:0,
winT:0, winSlide:0
};
function resetPlayer(){
player.x = 3*TILE; player.y = 8*TILE;
player.vx=0; player.vy=0; player.onGround=false; player.facing=1;
player.dead=false; player.deadT=0; player.invuln=0; player.star=0; player.winT=0; player.winSlide=0;
player.jumping=false; player.jumpHeld=false;
player.big=false; player.fire=false; player.h=SMALL_H;
player.fireCooldown=0;
fireballs=[]; powerups=[];
cameraX=0;
}
/* ---------------- Collision helpers ---------------- */
function tileAt(px,py){ // world px -> char
const tx = Math.floor(px/TILE), ty=Math.floor(py/TILE);
if(tx<0||tx>=LVL_W||ty<0||ty>=LVL_H) return ' ';
return grid[ty][tx];
}
function solidAt(px,py){
const tx=Math.floor(px/TILE), ty=Math.floor(py/TILE);
if(tx<0||tx>=LVL_W) return true; // walls
if(ty<0) return false; // open sky
if(ty>=LVL_H) return true; // floor under world
return solids[ty][tx]===1;
}
function moveAndCollide(o){
// X
o.x += o.vx;
let hitX = false;
if(o.vx>0){
const right = o.x + o.w;
const top = o.y+1, bot = o.y+o.h-1;
if(solidAt(right, top) || solidAt(right, bot) || solidAt(right, o.y+o.h/2)){
const tx = Math.floor(right/TILE);
o.x = tx*TILE - o.w - 0.01;
hitX=true;
}
} else if(o.vx<0){
const left = o.x;
const top = o.y+1, bot = o.y+o.h-1;
if(solidAt(left, top) || solidAt(left, bot) || solidAt(left, o.y+o.h/2)){
const tx = Math.floor(left/TILE);
o.x = (tx+1)*TILE + 0.01;
hitX=true;
}
}
// Y
o.y += o.vy;
o.onGround=false;
if(o.vy>0){
const bot = o.y+o.h;
const lx=o.x+1, rx=o.x+o.w-1;
if(solidAt(lx,bot)||solidAt(rx,bot)||solidAt(o.x+o.w/2,bot)){
const ty=Math.floor(bot/TILE);
o.y = ty*TILE - o.h - 0.01;
o.vy=0; o.onGround=true;
}
} else if(o.vy<0){
const top=o.y;
const lx=o.x+1, rx=o.x+o.w-1;
if(solidAt(lx,top)||solidAt(rx,top)||solidAt(o.x+o.w/2,top)){
const ty=Math.floor(top/TILE);
o.y=(ty+1)*TILE+0.01;
o.vy=0;
bumpBlock(Math.floor((o.x+o.w/2)/TILE), ty); // head bump
}
}
return hitX;
}
// contents map: "x,y" -> 'coin'|'mushroom'|'flower'|'star' (for ? blocks)
const blockContents = {};
function setQ(x,y,kind){ setT(x,y,'?',1); blockContents[x+','+y]=kind; }
function bumpBlock(tx,ty){
if(ty<0||ty>=LVL_H) return;
const c = grid[ty][tx];
const key = tx+','+ty;
if(c==='?'){
grid[ty][tx]='C'; solids[ty][tx]=1;
const kind = blockContents[key] || 'coin';
delete blockContents[key];
if(kind==='coin'){
spawnCoinPop(tx*TILE+TILE/2, ty*TILE);
addScore(200); addCoins(1); sfx('coin');
} else {
// powerup: mushroom if small, flower if already big, star is star
const give = (kind==='star') ? 'star' : (player.big ? 'flower' : 'mushroom');
spawnPowerup(tx*TILE+1, ty*TILE, give);
addScore(0);
}
} else if(c==='B'){
// big players smash bricks; small players just bump
if(player.big){
grid[ty][tx]=' '; solids[ty][tx]=0;
spawnBrickBits(tx*TILE+TILE/2, ty*TILE+TILE/2);
addScore(50); sfx('break');
// smash any enemy sitting on top
bumpEnemiesAbove(tx,ty);
} else {
sfx('bump');
}
} else if(c==='C'){
sfx('bump');
} else if(c==='X' || c==='P'||c==='Q'||c==='p'||c==='q'){
sfx('bump');
}
}
function bumpEnemiesAbove(tx,ty){
// flip any enemy standing on top of tile (tx,ty) — Mario's brick-bump trick
const top = ty*TILE;
for(const e of entities){
if(e.dead||!e.alive) continue;
if(e.x+e.w > tx*TILE && e.x < tx*TILE+TILE && Math.abs((e.y+e.h)-top) < 6){
e.alive=false; e.dead=true; e.deadT=0;
addScore(100); addFloatText(e.x,e.y,'100','#fff'); sfx('stomp');
spawnPuff(e.x+e.w/2,e.y+e.h/2,ENEMY_DEF[e.type].color);
}
}
}
function spawnCoinPop(x,y){
particles.push({kind:'coin',x,y,vy:-3.2,life:30, t:0});
}
function spawnBrickBits(x,y){
for(let i=0;i<6;i++){
particles.push({kind:'bit',x,y,vx:(Math.random()*2-1)*2.2,vy:-Math.random()*3-1,life:40,t:0,col:'#c8642b'});
}
}
function spawnPuff(x,y,col){
for(let i=0;i<8;i++) particles.push({kind:'puff',x,y,vx:(Math.random()*2-1)*1.6,vy:-Math.random()*1.5,life:26,t:0,col});
}
function addFloatText(x,y,txt,col){ floatTexts.push({x,y,txt,col:col||'#fff',t:0,life:50}); }
/* ---------------- Powerups & Projectiles ---------------- */
// powerup types: 'mushroom' (grow), 'flower' (fire power), 'star' (invincibility)
let powerups = []; // active, spawned powerups moving in the world
let fireballs = []; // player-fired projectiles
const STAR_DURATION = 600; // ~10s at 60fps
function spawnPowerup(px, py, kind){
// emerge upward out of the block, then start moving
powerups.push({ x:px, y:py-TILE, w:14, h:14, kind, vx:0.7, vy:-1.2,
emerge: 14, dir:1, phase:Math.random()*6.28, alive:true, onGround:false });
}
function spawnFireball(px, py, dir){
if(fireballs.length>=3) return; // limit like the original (2 on screen)
fireballs.push({ x:px, y:py, w:8, h:8, vx: dir*3.2, vy:1.0, bounces:4, alive:true });
}
function updatePowerups(){
const o = player;
for(let i=powerups.length-1;i>=0;i--){
const p=powerups[i];
if(!p.alive){ powerups.splice(i,1); continue; }
if(p.emerge>0){ p.emerge--; p.y -= 1; continue; } // rising out of block
// mushroom / flower slide along ground; star bounces
p.phase += 0.2;
if(p.kind==='star'){
p.vy += G*0.6; if(p.vy>6) p.vy=6;
// star bounces when it lands
p.x += p.vx; p.y += p.vy;
// ground/ceiling check
const footY = p.y+p.h;
if(solidAt(p.x+2,footY)||solidAt(p.x+p.w-2,footY)){ p.y=Math.floor(footY/TILE)*TILE-p.h; p.vy=-5.0; }
// wall: reverse
if(solidAt(p.x+p.vx+ (p.vx>0?p.w:0), p.y+p.h/2)){ p.vx=-p.vx; p.dir=p.vx>0?1:-1; }
// ledge: star doesn't care (keeps bouncing off edges into pit -> dies)
if(p.y>VH+40){ p.alive=false; }
} else {
// mushroom/flower: walk + gravity, turn at walls & ledges
p.vy += G; if(p.vy>MAX_FALL)p.vy=MAX_FALL;
const ox=p.vx;
p.x += p.vx;
if(p.vx>0 && solidAt(p.x+p.w, p.y+p.h/2)){ p.x=Math.floor((p.x+p.w)/TILE)*TILE-p.w-0.01; p.vx=-p.vx; p.dir=-1; }
else if(p.vx<0 && solidAt(p.x, p.y+p.h/2)){ p.x=Math.floor(p.x/TILE+1)*TILE+0.01; p.vx=-p.vx; p.dir=1; }
p.y += p.vy;
p.onGround=false;
const footY=p.y+p.h;
if(solidAt(p.x+2,footY)||solidAt(p.x+p.w-2,footY)){ p.y=Math.floor(footY/TILE)*TILE-p.h-0.01; p.vy=0; p.onGround=true; }
// turn at ledges
const aheadX = p.vx>0 ? p.x+p.w+1 : p.x-1;
if(p.onGround && !solidAt(aheadX, p.y+p.h+1)){ p.vx=-p.vx; p.dir=p.vx>0?1:-1; }
if(p.y>VH+40){ p.alive=false; }
}
// collect?
if(rectHit(o,p)){
applyPowerup(p.kind);
p.alive=false;
}
}
}
function applyPowerup(kind){
if(kind==='mushroom'){
if(!player.big){ growPlayer(); }
addScore(1000); addFloatText(player.x,player.y,'1000','#7CFC00');
sfx('power');
} else if(kind==='flower'){
player.big=true; player.fire=true;
addScore(1000); addFloatText(player.x,player.y,'FIRE','#ff6b3d');
sfx('power');
} else if(kind==='star'){
player.star = STAR_DURATION;
addScore(1000); addFloatText(player.x,player.y,'STAR!','#ffd34d');
sfx('star');
}
}
function growPlayer(){
player.big=true;
// grow upward: keep feet planted
const oldH=player.h;
player.h = BIG_H;
player.y -= (player.h-oldH);
}
function updateFireballs(){
for(let i=fireballs.length-1;i>=0;i--){
const f=fireballs[i];
if(!f.alive){ fireballs.splice(i,1); continue; }
f.vy += G*0.7; if(f.vy>5)f.vy=5;
f.x += f.vx;
// wall hit
if((f.vx>0 && solidAt(f.x+f.w, f.y+f.h/2)) || (f.vx<0 && solidAt(f.x, f.y+f.h/2))){
f.alive=false; spawnPuff(f.x+f.w/2,f.y+f.h/2,'#ff8a3d'); continue;
}
f.y += f.vy;
const footY=f.y+f.h;
if(solidAt(f.x+1,footY)||solidAt(f.x+f.w-1,footY)){
f.y=Math.floor(footY/TILE)*TILE-f.h; if(f.bounces-->0){ f.vy=-3.4; } else { f.alive=false; spawnPuff(f.x+f.w/2,f.y+f.h/2,'#ff8a3d'); continue; }
}
if(f.y>VH+40||f.x<cameraX-20||f.x>cameraX+VW+20){ f.alive=false; continue; }
// hit enemies
for(const e of entities){
if(e.dead||!e.alive) continue;
if(rectHit(f,e)){
e.alive=false; e.dead=true; e.deadT=0;
f.alive=false; addScore(200); addFloatText(e.x,e.y,'200','#fff');
spawnPuff(e.x+e.w/2,e.y+e.h/2,'#ff8a3d');
sfx('stomp');
break;
}
}
}
}
/* ---- Powerup sprites (Human-vs-AI themed) ---- */
// Mushroom = "BRAIN CORE" (human mind), Flower = "FIREWALL TORCH", Star = "LIBERTY STAR"
function drawPowerup(p){
const x=Math.round(p.x-cameraX), y=Math.round(p.y);
const cx=x+p.w/2, cy=y+p.h/2;
if(p.kind==='mushroom'){
// red cap (human brain core)
CTX.fillStyle='#e63946'; roundRect(x,y,p.w,9,3); CTX.fill();
CTX.fillStyle='#fff'; CTX.fillRect(x+2,y+2,2,2); CTX.fillRect(x+10,y+2,2,2);
CTX.fillRect(x+6,y+5,2,2);
CTX.fillStyle='#ffd9a8'; CTX.fillRect(x+3,y+8,p.w-6,p.h-8); // stem/face
CTX.fillStyle='#000'; CTX.fillRect(x+5,y+10,2,2); CTX.fillRect(x+p.w-7,y+10,2,2); // eyes
} else if(p.kind==='flower'){
// firewall torch: orange petals + center
const w=Math.sin(p.phase)*0.3;
CTX.save(); CTX.translate(cx,cy); CTX.rotate(w);
CTX.fillStyle='#ff8a3d';
for(let i=0;i<5;i++){ const a=i*Math.PI*0.4; CTX.beginPath(); CTX.ellipse(Math.cos(a)*4,Math.sin(a)*4,3.5,2.5,a,0,6.29); CTX.fill(); }
CTX.fillStyle='#ffd34d'; CTX.beginPath(); CTX.arc(0,0,3,0,6.29); CTX.fill();
CTX.restore();
} else if(p.kind==='star'){
// liberty star — 5-point with sparkle
const s=8;
CTX.save(); CTX.translate(cx,cy); CTX.rotate(Math.sin(p.phase*0.5)*0.2);
CTX.fillStyle=(Math.floor(worldTime/4)%2)?'#ffd34d':'#fff39a';
CTX.beginPath();
for(let i=0;i<10;i++){ const a=-Math.PI/2 + i*Math.PI/5; const r=(i%2)?s*0.45:s;
const px_=Math.cos(a)*r, py_=Math.sin(a)*r; i?CTX.lineTo(px_,py_):CTX.moveTo(px_,py_); }
CTX.closePath(); CTX.fill();
CTX.fillStyle='rgba(255,255,255,0.6)'; CTX.fillRect(-1,-1,2,2);
CTX.restore();
}
}
function drawFireballs(){
for(const f of fireballs){
const x=Math.round(f.x-cameraX), y=Math.round(f.y);
CTX.fillStyle='#ff5b1a'; CTX.beginPath(); CTX.arc(x+f.w/2,y+f.h/2,f.w/2+1,0,6.29); CTX.fill();
CTX.fillStyle='#ffd34d'; CTX.beginPath(); CTX.arc(x+f.w/2,y+f.h/2,f.w/2-1,0,6.29); CTX.fill();
CTX.fillStyle='#fff'; CTX.fillRect(x+f.w/2-1,y+f.h/2-1,2,2);
}
}
function addScore(n){ score+=n; document.getElementById('hudScore').textContent = String(score).padStart(6,'0'); }
function addCoins(n){ coins+=n; if(coins>=100){ coins-=100; lives++; document.getElementById('hudLives').textContent=lives; } document.getElementById('hudCoins').textContent=String(coins).padStart(2,'0'); }
/* ---------------- Audio (WebAudio chiptune sfx) ---------------- */
let AC=null;
function audio(){ if(!AC){ try{ AC = new (window.AudioContext||window.webkitAudioContext)(); }catch(e){} } return AC; }
function beep(freq,dur,type='square',vol=0.08,slideTo=null){
const ac=audio(); if(!ac) return;
const o=ac.createOscillator(), g=ac.createGain();
o.type=type; o.frequency.value=freq;
if(slideTo!==null) o.frequency.linearRampToValueAtTime(slideTo, ac.currentTime+dur);
g.gain.value=vol; g.gain.exponentialRampToValueAtTime(0.0001, ac.currentTime+dur);
o.connect(g); g.connect(ac.destination); o.start(); o.stop(ac.currentTime+dur);
}
const sfxQueue=[];
function sfx(name){
switch(name){
case 'jump': beep(520,0.16,'square',0.09,880); break;
case 'coin': beep(988,0.07,'square',0.09); setTimeout(()=>beep(1319,0.12,'square',0.09),60); break;
case 'stomp': beep(180,0.12,'square',0.1,80); break;
case 'break': beep(220,0.1,'sawtooth',0.08,90); break;
case 'bump': beep(140,0.07,'square',0.08,110); break;
case 'die': beep(380,0.5,'square',0.12,80); break;
case 'power': beep(523,0.08); setTimeout(()=>beep(659,0.08),80); setTimeout(()=>beep(784,0.12),160); break;
case 'flag': beep(523,0.1); setTimeout(()=>beep(659,0.1),100); setTimeout(()=>beep(784,0.1),200); setTimeout(()=>beep(1047,0.25),300); break;
case 'hurt': beep(330,0.4,'sawtooth',0.1,120); break;
case 'fire': beep(880,0.07,'sawtooth',0.07,420); break;
case 'star': // arpeggio that loops a couple times
[523,659,784,1047,784,659].forEach((f,i)=>setTimeout(()=>beep(f,0.09,'square',0.08),i*70)); break;
}
}
/* =============================================================
RENDER — sprites drawn with rects (pixel style)
============================================================= */
// helper: px(x,y,w,h,color) in world space (applies camera)
function px(x,y,w,h,c){ CTX.fillStyle=c; CTX.fillRect(Math.round(x-cameraX),Math.round(y),w,h); }
/* ---- Background: parallax "Human vs AI" skyline ---- */
let bgStars=[], bgParticles=[];
(function initBg(){
for(let i=0;i<70;i++) bgStars.push({x:Math.random()*1600,y:Math.random()*160,r:Math.random()*1.6+0.3,s:Math.random()*0.5+0.1,t:Math.random()*6.28});
// floating data motes
for(let i=0;i<26;i++) bgParticles.push({x:Math.random()*1800,y:Math.random()*260,vx:-(Math.random()*0.4+0.1),r:Math.random()*2+1,col:['#5b8cff','#ff6b8a','#7be07b','#ffd34d','#b58cff'][i%5]});
})();
function drawBackground(){
// sky gradient
const g = CTX.createLinearGradient(0,0,0,VH);
g.addColorStop(0,'#1a0d2e'); g.addColorStop(0.5,'#3a1d6e'); g.addColorStop(1,'#0b0726');
CTX.fillStyle=g; CTX.fillRect(0,0,VW,VH);
// distant moon (human)
CTX.fillStyle='rgba(255,235,180,0.9)';
CTX.beginPath(); CTX.arc(VW*0.78, 50, 22, 0, 6.29); CTX.fill();
CTX.fillStyle='rgba(40,30,60,0.35)';
CTX.beginPath(); CTX.arc(VW*0.78-6, 46, 18, 0, 6.29); CTX.fill();
CTX.fillStyle='rgba(255,235,180,0.15)';
CTX.beginPath(); CTX.arc(VW*0.78, 50, 34, 0, 6.29); CTX.fill();
// stars (twinkle) parallax 0.1
for(const s of bgStars){
const tw = 0.5+0.5*Math.sin(worldTime*0.05+s.t);
CTX.fillStyle=`rgba(255,255,255,${0.3+tw*0.5})`;
const sx=(s.x - cameraX*0.15)%1600; const x = sx<0?sx+1600:sx;
CTX.fillRect(x%VW, s.y, s.r, s.r);
}
// FAR skyline: AI data-towers (dark, neon windows) parallax .25
drawSkyline(cameraX*0.25, '#150a2b', '#3a1d6e', 60, 0, 1.0);
// MID skyline: human ruins / city parallax .5
drawSkyline(cameraX*0.5 + 40, '#0a0618', '#5a2d8e', 90, 30, 0.6);
// floating data motes (parallax .4)
for(const p of bgParticles){
p.x += p.vx; if(p.x<cameraX-50) p.x += 1800;
const sx = p.x - cameraX*0.4;
CTX.globalAlpha=0.5; CTX.fillStyle=p.col;
CTX.fillRect(sx, p.y, p.r, p.r);
CTX.globalAlpha=0.15; CTX.fillRect(sx-1,p.y-1,p.r+2,p.r+2);
CTX.globalAlpha=1;
}
// subtle scanline / vignette handled by CSS; add horizon glow
const hg = CTX.createLinearGradient(0,VH-90,0,VH);
hg.addColorStop(0,'rgba(120,40,160,0)'); hg.addColorStop(1,'rgba(180,60,200,0.18)');
CTX.fillStyle=hg; CTX.fillRect(0,VH-90,VW,90);
}
function drawSkyline(offset,colDark,colWin,baseH,baseY,alpha){
CTX.save();
CTX.globalAlpha=alpha;
const step=46;
for(let bx=-step; bx<VW+step; bx+=step){
// deterministic pseudo-random heights
const seed = Math.floor((bx+offset)/step);
const h = baseH + ((Math.sin(seed*12.9898)*43758.5453)%1+1)*0.5*70;
const x = bx - (offset%step);
CTX.fillStyle=colDark;
const towerH = Math.abs(h);
CTX.fillRect(x, VH-baseY-towerH, step-8, towerH);
// neon windows
CTX.fillStyle=colWin;
for(let wy=VH-baseY-towerH+6; wy<VH-baseY-4; wy+=8){
for(let wx=x+3; wx<x+step-11; wx+=7){
if(((wx+wy+seed) % 3)===0) CTX.fillRect(wx,wy,3,3);
}
}
// antenna with blinking AI light
CTX.fillStyle='#222';
CTX.fillRect(x+step/2-1, VH-baseY-towerH-8, 2, 8);
if((worldTime+seed*13)%80<40){ CTX.fillStyle='#ff4d6d'; CTX.fillRect(x+step/2-2, VH-baseY-towerH-10, 4, 4); }
}
CTX.restore();
}
/* ---- Tiles ---- */
function drawTiles(){
const startX = Math.max(0, Math.floor(cameraX/TILE)-1);
const endX = Math.min(LVL_W, Math.ceil((cameraX+VW)/TILE)+1);
for(let y=0;y<LVL_H;y++){
for(let x=startX;x<endX;x++){
const c=grid[y][x];
if(c===' ') continue;
const sx=x*TILE-cameraX, sy=y*TILE;
switch(c){
case '#': drawGround(sx,sy,y); break;
case 'X': drawHardBlock(sx,sy); break;
case 'B': drawBrick(sx,sy); break;
case '?': drawQBlock(sx,sy,true); break;
case 'C': drawQBlock(sx,sy,false); break;
case 'o': drawCoinFloat(sx,sy); break;
case 'P': drawPipe(sx,sy,'tl'); break;
case 'Q': drawPipe(sx,sy,'tr'); break;
case 'p': drawPipe(sx,sy,'bl'); break;
case 'q': drawPipe(sx,sy,'br'); break;
case 'F': drawFlagTop(sx,sy); break;
case '|': drawFlagPole(sx,sy); break;
case 'G': drawGoal(sx,sy); break;
}
}
}
}
function drawGround(x,y,rowY){
// grassy top only on top row
CTX.fillStyle='#3a2a1a'; CTX.fillRect(x,y,TILE,TILE);
const isTop = (rowY===12);
if(isTop){
CTX.fillStyle='#4caf3f'; CTX.fillRect(x,y,TILE,4);
CTX.fillStyle='#5fd14f'; CTX.fillRect(x,y,TILE,2);
CTX.fillStyle='#2e7d2a'; CTX.fillRect(x,y+4,TILE,2);
}
// brick texture dots
CTX.fillStyle='#241609';
CTX.fillRect(x+3,y+7,2,2); CTX.fillRect(x+11,y+7,2,2);
CTX.fillRect(x+7,y+12,2,2); CTX.fillRect(x+3,y+12,2,2);
CTX.strokeStyle='rgba(0,0,0,0.25)'; CTX.lineWidth=1;
CTX.strokeRect(x+0.5,y+0.5,TILE-1,TILE-1);
}
function drawHardBlock(x,y){
CTX.fillStyle='#9a6b3f'; CTX.fillRect(x,y,TILE,TILE);
CTX.fillStyle='#7a4f2c'; CTX.fillRect(x,y,TILE,2);
CTX.fillRect(x,y,2,TILE);
CTX.fillStyle='#b9865a'; CTX.fillRect(x+2,y+2,TILE-4,TILE-4);
CTX.fillStyle='#7a4f2c';
CTX.fillRect(x,y+8,TILE,1); CTX.fillRect(x+8,y,1,8); CTX.fillRect(x+4,y+8,1,8); CTX.fillRect(x+12,y+8,1,8);
}
function drawBrick(x,y){
CTX.fillStyle='#b5651d'; CTX.fillRect(x,y,TILE,TILE);
CTX.fillStyle='#7a3f10';
CTX.fillRect(x,y+7,TILE,1); CTX.fillRect(x,y+15,TILE,1);
CTX.fillRect(x+3,y,1,7); CTX.fillRect(x+11,y,1,7);
CTX.fillRect(x+7,y+8,1,7); CTX.fillRect(x+15,y+8,1,7);
CTX.fillStyle='#d98b3a'; CTX.fillRect(x,y,TILE,1);
}
function drawQBlock(x,y,active){
const blink = active && (Math.floor(worldTime/14)%2===0);
CTX.fillStyle= active? (blink?'#ffcf3a':'#ffb800') : '#9a6b3f';
CTX.fillRect(x,y,TILE,TILE);
CTX.fillStyle= active? '#cc8500':'#5a3a1c';
CTX.fillRect(x,y,TILE,2); CTX.fillRect(x,y,2,TILE);
CTX.fillRect(x,y+TILE-2,TILE,2); CTX.fillRect(x+TILE-2,y,2,TILE);
// rivets
CTX.fillStyle= active? '#cc8500':'#5a3a1c';
CTX.fillRect(x+3,y+3,2,2); CTX.fillRect(x+TILE-5,y+3,2,2);
CTX.fillRect(x+3,y+TILE-5,2,2); CTX.fillRect(x+TILE-5,y+TILE-5,2,2);
if(active){
CTX.fillStyle='#fff';
CTX.font='bold 11px Courier New'; CTX.textAlign='center'; CTX.textBaseline='middle';
CTX.fillText('?', x+TILE/2, y+TILE/2+1);
}
}
function drawCoinFloat(x,y){
const f=Math.floor(worldTime/6)%4;
const w=[10,7,3,7][f];
CTX.fillStyle='#ffd34d'; CTX.fillRect(x+TILE/2-w/2,y+3,w,10);
CTX.fillStyle='#b8860b'; CTX.fillRect(x+TILE/2-w/2,y+3,w,2);
CTX.fillStyle='#fff3a0'; CTX.fillRect(x+TILE/2-w/2+1,y+4,1,8);
}
function drawPipe(x,y,part){
// base green
const isTop = (part==='tl'||part==='tr');
CTX.fillStyle='#3aa83a'; CTX.fillRect(x,y,TILE,TILE);
CTX.fillStyle='#2f8c2f'; CTX.fillRect(x,y,1,TILE);
CTX.fillStyle='#5fd35f'; CTX.fillRect(x+2,y,2,TILE);
CTX.fillStyle='#1f6f1f'; CTX.fillRect(x+TILE-2,y,2,TILE);
if(isTop){
// lip wider — draw overhang
if(part==='tl'){ CTX.fillStyle='#3aa83a'; CTX.fillRect(x-2,y,TILE+2,5); }
else { CTX.fillStyle='#3aa83a'; CTX.fillRect(x,y,4,5); CTX.fillStyle='#5fd35f'; CTX.fillRect(x+1,y+1,1,3); }
CTX.fillStyle='#1f6f1f'; CTX.fillRect(x+TILE-1,y,1,5);
CTX.fillStyle='#2f8c2f'; CTX.fillRect(x+TILE-4,y,1,5);
}
}
function drawFlagTop(x,y){
// ball on top
CTX.fillStyle='#7CFC00'; CTX.beginPath(); CTX.arc(x+TILE/2,y+TILE/2,5,0,6.29); CTX.fill();
CTX.fillStyle='#fff'; CTX.beginPath(); CTX.arc(x+TILE/2,y+TILE/2,2,0,6.29); CTX.fill();
// waving flag
const wave=Math.sin(worldTime*0.1)*2;
CTX.fillStyle='#7CFC00';
CTX.fillRect(x-14+wave, y+TILE+2, 14, 9);
CTX.fillStyle='#fff'; CTX.font='bold 7px Courier New'; CTX.textAlign='center';
CTX.fillText('H', x-7+wave, y+TILE+9);
}
function drawFlagPole(x,y){
CTX.fillStyle='#cfd8e3'; CTX.fillRect(x+TILE/2-1,y,2,TILE);
}
function drawGoal(x,y){
CTX.fillStyle='#7CFC00'; CTX.fillRect(x,y-2,TILE,2);
CTX.fillStyle='#4caf3f'; CTX.fillRect(x,y-4,TILE,2);
}
/* ---- Player sprite: "Super Human" ---- */
function drawPlayer(){
const o = player;
// post-hit mercy blink (NOT star)
if(o.invuln>0 && Math.floor(worldTime/3)%2===0){ /* blink skip */ return; }
const x = Math.round(o.x-cameraX), y=Math.round(o.y);
const f = o.facing;
// animation frame
const moving = Math.abs(o.vx)>0.1;
const frame = moving ? Math.floor(worldTime/4)%3 : 0;
// STAR POWER: cycle body colors through a rainbow + aura
const starActive = o.star>0;
const starCol = starActive ? ['#ff5b6e','#ff9b3d','#ffd34d','#7CFC00','#5b8cff','#b58bff'][Math.floor(worldTime/3)%6] : null;
CTX.save();
// shadow
CTX.fillStyle='rgba(0,0,0,0.25)';
CTX.beginPath(); CTX.ellipse(x+o.w/2, y+o.h+1, 7, 2, 0,0,6.29); CTX.fill();
// star aura
if(starActive){
CTX.fillStyle='rgba(255,220,80,0.25)';
CTX.beginPath(); CTX.arc(x+o.w/2, y+o.h/2, o.w, 0, 6.29); CTX.fill();
}
// body colors (human hero). When big (h>SMALL_H), we scale vertically so the
// 15px sprite template stretches to fill the taller hitbox.
const SCALE = o.h / SMALL_H;
let SHIRT='#e63946', SHIRT_D='#b21f2d', DENIM='#1d4ed8', DENIM_D='#1e3a8a',
SKIN='#ffd9a8', SKIN_S='#e8b27a', HAIR='#3a2317', SHOE='#3a2317';
if(starActive){ SHIRT=SHIRT_D=DENIM=DENIM_D=HAIR=SHOE=starCol; SKIN='#fff7e0'; SKIN_S=starCol; }
// fire power: white shirt + red cap (fireball-tosser look)
if(o.fire && !starActive){ SHIRT='#fff'; SHIRT_D='#ddd'; HAIR='#e63946'; }
const flip = f<0;
if(flip){ CTX.translate(x+o.w, y); CTX.scale(-1,1); CTX.translate(-x,-y); }
// vertical scale for big mode
if(SCALE!==1){ CTX.translate(0, y); CTX.scale(1, SCALE); CTX.translate(0, -y); }
// legs (walk cycle)
CTX.fillStyle=DENIM;
if(!o.onGround){
CTX.fillRect(x+1, y+9, 4, 5); CTX.fillRect(x+7, y+8, 4, 5);
CTX.fillStyle=SHOE; CTX.fillRect(x+0, y+13, 5, 2); CTX.fillRect(x+7, y+12, 5, 2);
} else {
const ph=[[0,0],[1,-1],[0,0]][frame];
CTX.fillRect(x+1+ph[0], y+9+ (frame===1?0:0), 4, 5);
CTX.fillRect(x+7-ph[0], y+9, 4, 5);
CTX.fillStyle=SHOE;
CTX.fillRect(x+0+ph[0], y+13, 5, 2);
CTX.fillRect(x+7-ph[0], y+13, 5, 2);
}
// torso shirt
CTX.fillStyle=SHIRT; CTX.fillRect(x+1, y+5, 10, 5);
CTX.fillStyle=SHIRT_D; CTX.fillRect(x+1, y+9, 10, 1);
// overall straps (human worker vibe)
CTX.fillStyle=DENIM; CTX.fillRect(x+2, y+5, 1, 4); CTX.fillRect(x+9, y+5, 1, 4);
// arms
CTX.fillStyle=SHIRT; CTX.fillRect(x+0, y+6, 2, 4); CTX.fillRect(x+10, y+6, 2, 4);
CTX.fillStyle=SKIN; CTX.fillRect(x+0, y+9, 2, 2); CTX.fillRect(x+10, y+9, 2, 2);
// head
CTX.fillStyle=SKIN; CTX.fillRect(x+3, y+1, 7, 5);
CTX.fillStyle=SKIN_S; CTX.fillRect(x+3, y+5, 7, 1);
// hair
CTX.fillStyle=HAIR; CTX.fillRect(x+2, y+0, 9, 2); CTX.fillRect(x+2, y+1, 2, 3); CTX.fillRect(x+9, y+1, 2, 2);
// eye + determined brow
CTX.fillStyle='#fff'; CTX.fillRect(x+7, y+2, 2, 2);
CTX.fillStyle='#111'; CTX.fillRect(x+8, y+3, 1, 1);
CTX.fillStyle=HAIR; CTX.fillRect(x+6, y+1, 4, 1); // brow
CTX.restore();
}
/* ---- Enemy sprites (AI lab mascots) ---- */
// Names shown above each enemy
const ENEMY_NAME = {
openai:'OpenAI', anthropic:'Anthropic', zai:'ZAI', kimi:'KIMI',
minimax:'MiniMax', arcee:'Arcee', meta:'Meta', deepseek:'DeepSeek', gemini:'Gemini'
};
function drawEnemy(e){
const x=Math.round(e.x-cameraX), y=Math.round(e.y);
if(e.dead && e.deadT>0){
// squashed — fade and shrink
CTX.globalAlpha=Math.max(0,1-e.deadT/30);
const sq=1-e.deadT/30;
CTX.save();
CTX.translate(x+e.w/2, y+e.h);
CTX.scale(1, Math.max(0.15,sq));
CTX.translate(-(x+e.w/2), -(y+e.h));
}
const t = e.type;
const wobble = Math.sin(worldTime*0.2+e.phase);
// shadow
if(!e.flyer){
CTX.fillStyle='rgba(0,0,0,0.28)';
CTX.beginPath(); CTX.ellipse(x+e.w/2, y+e.h+2, 9,3,0,0,6.29); CTX.fill();
}
// little feet
const legOff = Math.sin(worldTime*0.3+e.phase)*1.4;
CTX.fillStyle='#1a1a22';
CTX.fillRect(x+4, y+e.h-2+legOff, 4,2);
CTX.fillRect(x+e.w-8, y+e.h-2-legOff, 4,2);
// the logo body
switch(t){
case 'openai': drawOpenAI(x,y,e,wobble); break;
case 'anthropic': drawAnthropic(x,y,e,wobble); break;
case 'zai': drawZAI(x,y,e,wobble); break;
case 'kimi': drawKimi(x,y,e,wobble); break;
case 'minimax': drawMiniMax(x,y,e,wobble); break;
case 'arcee': drawArcee(x,y,e,wobble); break;
case 'meta': drawMeta(x,y,e,wobble); break;
case 'deepseek': drawDeepSeek(x,y,e,wobble); break;
case 'gemini': drawGemini(x,y,e,wobble); break;
}
if(e.dead && e.deadT>0){ CTX.restore(); }
CTX.globalAlpha=1;
// floating name label above the enemy
if(!e.dead){
const label = ENEMY_NAME[t];
CTX.font='bold 7px Courier New'; CTX.textAlign='center'; CTX.textBaseline='middle';
const lw = CTX.measureText(label).width + 6;
const lx = x+e.w/2, ly = y-7;
CTX.fillStyle='rgba(0,0,0,0.65)';
roundRect(lx-lw/2, ly-4, lw, 8, 2); CTX.fill();
CTX.fillStyle='#fff';
CTX.fillText(label, lx, ly);
}
}
// two angry digital eyes (drawn relative to enemy box) — the "villain" twist on each logo
function enemyEyes(cx,cy,e){
const d = e.dir>0?1:-1;
CTX.fillStyle='rgba(0,0,0,0.8)';
CTX.fillRect(cx-5, cy-1, 3,3); CTX.fillRect(cx+2, cy-1, 3,3);
CTX.fillStyle='#fff';
CTX.fillRect(cx-4+d, cy, 1,1); CTX.fillRect(cx+3+d, cy, 1,1);
}
/* ---- Faithful-ish AI lab logos (drawn larger now) ---- */
function drawOpenAI(x,y,e,w){
// OpenAI "blossom" knot: 6 interlocking petals
const cx=x+e.w/2, cy=y+e.h/2-1;
CTX.save(); CTX.translate(cx,cy); CTX.rotate(w*0.06);
CTX.fillStyle='#10a37f';
CTX.beginPath();
for(let i=0;i<6;i++){ const a=i*Math.PI/3;
const r1=11, r2=4.5;
const a0=a-Math.PI/6, a1=a+Math.PI/6;
CTX.moveTo(Math.cos(a0)*r2, Math.sin(a0)*r2);
CTX.lineTo(Math.cos(a0)*r1, Math.sin(a0)*r1);
CTX.lineTo(Math.cos(a1)*r1, Math.sin(a1)*r1);
CTX.lineTo(Math.cos(a1)*r2, Math.sin(a1)*r2);
}
CTX.closePath(); CTX.fill();
// inner hex hole
CTX.fillStyle='#0a1a14';
CTX.beginPath();
for(let i=0;i<6;i++){ const a=i*Math.PI/3; const px=Math.cos(a)*4.5, py=Math.sin(a)*4.5;
i? CTX.lineTo(px,py): CTX.moveTo(px,py); }
CTX.closePath(); CTX.fill();
CTX.restore();
enemyEyes(cx, cy+5, e);
}
function drawAnthropic(x,y,e,w){
// Anthropic: sunrise asterisk + "A" in coral
const cx=x+e.w/2, cy=y+e.h/2-1;
CTX.save(); CTX.translate(cx,cy);
CTX.strokeStyle='#e8915c'; CTX.lineWidth=1.6; CTX.lineCap='round';
for(let i=0;i<16;i++){ const a=i*Math.PI/8+w*0.03;
CTX.beginPath(); CTX.moveTo(Math.cos(a)*7,Math.sin(a)*7); CTX.lineTo(Math.cos(a)*10.5,Math.sin(a)*10.5); CTX.stroke(); }
CTX.fillStyle='#d97757'; CTX.beginPath(); CTX.arc(0,0,7,0,6.29); CTX.fill();
CTX.fillStyle='#fff'; CTX.font='bold 10px Georgia,serif'; CTX.textAlign='center'; CTX.textBaseline='middle';
CTX.fillText('A',0,1);
CTX.restore();
enemyEyes(cx, cy+8, e);
}
function drawZAI(x,y,e,w){
// ZAI: bold blue rounded badge with "ZAI"
CTX.fillStyle='#3b5bdb'; roundRect(x+1,y+2,e.w-2,e.h-4,4); CTX.fill();
CTX.fillStyle='#6f8bff'; roundRect(x+1,y+2,e.w-2,3,2); CTX.fill();
CTX.fillStyle='#1e3a8a'; roundRect(x+1,y+e.h-5,e.w-2,3,2); CTX.fill();
CTX.fillStyle='#fff'; CTX.font='bold 8px Courier New'; CTX.textAlign='center'; CTX.textBaseline='middle';
CTX.fillText('ZAI', x+e.w/2, y+e.h/2);
enemyEyes(x+e.w/2, y+e.h+1, e);
}
function drawKimi(x,y,e,w){
// KIMI / Moonshot: purple crescent moon
const cx=x+e.w/2, cy=y+e.h/2-1;
CTX.fillStyle='#8b5cf6'; CTX.beginPath(); CTX.arc(cx,cy,11,0,6.29); CTX.fill();
CTX.fillStyle='#1a0d2e'; CTX.beginPath(); CTX.arc(cx+3.5,cy-1.5,9.5,0,6.29); CTX.fill();
// sparkle stars
CTX.fillStyle='#ffd34d';
CTX.fillRect(cx-8, cy+5, 2,2); CTX.fillRect(cx-9, cy-5, 1,1); CTX.fillRect(cx+7, cy+6, 1,1);
enemyEyes(cx-2, cy+1, e);
}
function drawMiniMax(x,y,e,w){
// MiniMax: red rounded badge with stylized "M"
CTX.fillStyle='#ff3b3b'; roundRect(x+1,y+2,e.w-2,e.h-4,4); CTX.fill();
CTX.fillStyle='#ff8a8a'; roundRect(x+1,y+2,e.w-2,3,2); CTX.fill();
CTX.fillStyle='#fff'; CTX.font='bold 11px Courier New'; CTX.textAlign='center'; CTX.textBaseline='middle';
CTX.fillText('M', x+e.w/2, y+e.h/2);
enemyEyes(x+e.w/2, y+e.h+1, e);
}
function drawArcee(x,y,e,w){
// Arcee: teal arc + 'A'
const cx=x+e.w/2, cy=y+e.h/2;
CTX.strokeStyle='#22d3ce'; CTX.lineWidth=2.4; CTX.lineCap='round';
CTX.beginPath(); CTX.arc(cx,cy+1,10,Math.PI*1.05,Math.PI*1.95); CTX.stroke();
CTX.fillStyle='#0e7c7e'; CTX.beginPath(); CTX.arc(cx,cy+1,6,0,6.29); CTX.fill();
CTX.fillStyle='#fff'; CTX.font='bold 9px Georgia,serif'; CTX.textAlign='center'; CTX.textBaseline='middle';
CTX.fillText('A',cx,cy+1);
enemyEyes(cx, cy+8, e);
}
function drawMeta(x,y,e,w){
// Meta: blue infinity loop
const cx=x+e.w/2, cy=y+e.h/2;
CTX.strokeStyle='#0668e1'; CTX.lineWidth=3.0; CTX.lineCap='round';
// left lobe
CTX.beginPath(); CTX.ellipse(cx-6,cy,5,5.5,0,0,6.29); CTX.stroke();
// right lobe
CTX.beginPath(); CTX.ellipse(cx+6,cy,5,5.5,0,0,6.29); CTX.stroke();
enemyEyes(cx, cy+1, e);
}
function drawDeepSeek(x,y,e,w){
// DeepSeek: blue whale with dorsal fin
CTX.fillStyle='#4d6bfe'; roundRect(x+1,y+4,e.w-3,e.h-8,5); CTX.fill();
// tail flukes
CTX.beginPath(); CTX.moveTo(x+e.w-2,y+e.h/2-1);
CTX.lineTo(x+e.w+2,y+e.h/2-5); CTX.lineTo(x+e.w+1,y+e.h/2-1);
CTX.lineTo(x+e.w+2,y+e.h/2+4); CTX.closePath(); CTX.fill();
// belly highlight
CTX.fillStyle='#7ea3ff'; roundRect(x+2,y+e.h/2+1,e.w-5,3,2); CTX.fill();
// eye
CTX.fillStyle='#fff'; CTX.fillRect(x+5,y+6,2,2); CTX.fillStyle='#000'; CTX.fillRect(x+6,y+6,1,1);
// spout
if((worldTime+e.phase*10)%60<30){
CTX.fillStyle='rgba(120,180,255,.7)';
CTX.fillRect(x+8,y,1,3); CTX.fillRect(x+10,y-2,1,4);
}
enemyEyes(x+e.w/2, y+e.h+1, e);
}
function drawGemini(x,y,e,w){
// Gemini: Google-style 4-point sparkle star (flyer)
const cx=x+e.w/2, cy=y+e.h/2-1;
const s=11+Math.sin(worldTime*0.18)*1.2;
// glow
CTX.fillStyle='rgba(66,133,244,0.25)';
CTX.beginPath(); CTX.arc(cx,cy,s+3,0,6.29); CTX.fill();
// 4-point star with concave sides
CTX.fillStyle='#4285F4';
CTX.beginPath();
CTX.moveTo(cx,cy-s);
CTX.quadraticCurveTo(cx+2.5,cy-2.5, cx+s,cy);
CTX.quadraticCurveTo(cx+2.5,cy+2.5, cx,cy+s);
CTX.quadraticCurveTo(cx-2.5,cy+2.5, cx-s,cy);
CTX.quadraticCurveTo(cx-2.5,cy-2.5, cx,cy-s);
CTX.closePath(); CTX.fill();
// inner bright star
CTX.fillStyle='#9bd0ff';
CTX.beginPath();
CTX.moveTo(cx,cy-s*0.45);
CTX.lineTo(cx+s*0.45,cy);
CTX.lineTo(cx,cy+s*0.45);
CTX.lineTo(cx-s*0.45,cy);
CTX.closePath(); CTX.fill();
enemyEyes(cx, cy+7, e);
}
function roundRect(x,y,w,h,r){
CTX.beginPath();
CTX.moveTo(x+r,y); CTX.arcTo(x+w,y,x+w,y+h,r); CTX.arcTo(x+w,y+h,x,y+h,r);
CTX.arcTo(x,y+h,x,y,r); CTX.arcTo(x,y,x+w,y,r); CTX.closePath();
}
/* ---- Title screen logo gallery ---- */
function drawLogoCard(cx,cy,type,label){
const fakeEnemy = {x:cx-14,y:cy-14,w:28,h:28,dir:1,phase:0};
// card background
CTX.fillStyle='rgba(255,255,255,0.08)';
roundRect(cx-22,cy-20,44,40,5); CTX.fill();
CTX.strokeStyle='rgba(255,255,255,0.18)'; CTX.lineWidth=1; CTX.stroke();
const w0=Math.sin(worldTime*0.1);
switch(type){
case 'openai': drawOpenAI(cx-14,cy-14,fakeEnemy,w0); break;
case 'anthropic': drawAnthropic(cx-14,cy-14,fakeEnemy,w0); break;
case 'zai': drawZAI(cx-14,cy-14,fakeEnemy,w0); break;
case 'kimi': drawKimi(cx-14,cy-14,fakeEnemy,w0); break;
case 'minimax': drawMiniMax(cx-14,cy-14,fakeEnemy,w0); break;
case 'arcee': drawArcee(cx-14,cy-14,fakeEnemy,w0); break;
case 'meta': drawMeta(cx-14,cy-14,fakeEnemy,w0); break;
case 'deepseek': drawDeepSeek(cx-14,cy-14,fakeEnemy,w0); break;
case 'gemini': drawGemini(cx-14,cy-14,fakeEnemy,w0); break;
}
CTX.fillStyle='#ffd34d'; CTX.font='bold 6px Courier New'; CTX.textAlign='center'; CTX.textBaseline='middle';
CTX.fillText(label, cx, cy+16);
}
function drawLogoGallery(){
// 9 cards in a row at bottom of title screen
const labs=[
['openai','OpenAI'],['anthropic','Anthropic'],['zai','ZAI'],['kimi','KIMI'],
['minimax','MiniMax'],['arcee','Arcee'],['meta','Meta'],['deepseek','DeepSeek'],['gemini','Gemini']
];
const n=labs.length;
const spacing = VW/n;
for(let i=0;i<n;i++){
const cx = spacing*i + spacing/2;
const cy = VH-44;
drawLogoCard(cx,cy,labs[i][0],labs[i][1]);
}
}
/* ---- particles & popups ---- */
function drawParticles(){
for(const p of particles){
const x=Math.round(p.x-cameraX), y=Math.round(p.y);
if(p.kind==='coin'){
const f=Math.floor(p.t/3)%4, w=[8,5,2,5][f];
CTX.fillStyle='#ffd34d'; CTX.fillRect(x-w/2,y,w,8);
CTX.fillStyle='#b8860b'; CTX.fillRect(x-w/2,y,w,2);
} else if(p.kind==='bit'){
CTX.fillStyle=p.col; CTX.fillRect(x,y,3,3);
} else if(p.kind==='puff'){
CTX.globalAlpha=Math.max(0,1-p.t/p.life); CTX.fillStyle=p.col; CTX.fillRect(x,y,2,2); CTX.globalAlpha=1;
}
}
for(const t of floatTexts){
CTX.globalAlpha=Math.max(0,1-t.t/t.life);
CTX.fillStyle=t.col; CTX.font='bold 8px Courier New'; CTX.textAlign='center';
CTX.fillText(t.txt, Math.round(t.x-cameraX), Math.round(t.y - t.t*0.3));
CTX.globalAlpha=1;
}
}
/* =============================================================
UPDATE
============================================================= */
function updatePlayer(){
const o=player;
if(o.dead){ o.deadT++; o.vy+=G; o.y+=o.vy; if(o.deadT>90){ afterDeath(); } return; }
if(o.winT>0){ // win slide
o.winT++;
if(o.winT<40){ o.x+=0.6; }
else if(o.winT<70){ o.y+=1.2; }
else if(o.winT<150){ o.x+=0.8; }
if(o.winT>180){ winGame(); }
o.vy+=G; moveAndCollide(o);
return;
}
// horizontal accel
const accel = keys.run?RUN_ACCEL:WALK_ACCEL;
const maxSpd = keys.run?MAX_RUN:MAX_WALK;
if(keys.left && !keys.right){ o.vx-=accel; o.facing=-1; }
else if(keys.right && !keys.left){ o.vx+=accel; o.facing=1; }
else { // friction
if(o.vx>0){ o.vx-=FRICTION; if(o.vx<0)o.vx=0; }
else if(o.vx<0){ o.vx+=FRICTION; if(o.vx>0)o.vx=0; }
}
o.vx = Math.max(-maxSpd, Math.min(maxSpd, o.vx));
// jump (variable)
if(jumpPressedEdge && o.onGround){
o.vy = JUMP_V; o.onGround=false; o.jumping=true; o.jumpHeld=true;
sfx('jump');
}
if(o.jumping && keys.jump && o.vy<0){ o.vy += JUMP_HOLD; } // extend
if(o.jumping && !keys.jump){ o.jumping=false; if(o.vy<0) o.vy*=JUMP_CUT; }
jumpPressedEdge=false;
// fireball: when fire-powered, tapping RUN (X/B) launches a fireball
if(o.fireCooldown>0) o.fireCooldown--;
if(o.fire && keys.run && o.fireCooldown===0){
spawnFireball(o.x + (o.facing>0?o.w:-8), o.y+4, o.facing);
o.fireCooldown = 14; // rate-limit
sfx('fire');
}
// gravity
o.vy += G; if(o.vy>MAX_FALL) o.vy=MAX_FALL;
// duck
o.ducking = keys.down && o.onGround;
if(o.ducking) o.vx*=0.8;
// collide
const hitX = moveAndCollide(o);
// fall in pit
if(o.y > VH+40){ killPlayer(true); }
// collect floating coins (any 'o' tile overlapping the player box)
const tx0 = Math.floor(o.x/TILE), tx1 = Math.floor((o.x+o.w-1)/TILE);
const ty0 = Math.floor(o.y/TILE), ty1 = Math.floor((o.y+o.h-1)/TILE);
for(let ty=ty0; ty<=ty1; ty++){
for(let tx=tx0; tx<=tx1; tx++){
if(ty<0||ty>=LVL_H||tx<0||tx>=LVL_W) continue;
if(grid[ty][tx]==='o'){
grid[ty][tx]=' '; // remove the coin
addScore(200); addCoins(1); sfx('coin');
spawnCoinPop(tx*TILE+TILE/2, ty*TILE);
addFloatText(tx*TILE+TILE/2, ty*TILE, '200', '#ffd34d');
}
}
}
// invuln / star timers
if(o.invuln>0) o.invuln--;
if(o.star>0){ o.star--; if(o.star===0){ /* star ended */ } }
// reached flag?
const fx = 205*TILE;
if(o.x+o.w > fx && o.x < fx+TILE && o.winT===0){
startWinSlide();
}
}
function updateEnemies(){
for(const e of entities){
if(e.dead){ e.deadT++; continue; }
if(!e.alive) continue;
if(e.flyer){
// hover toward player gently + sine
e.phase+=0.06;
e.y = e.baseY + Math.sin(e.phase)*8;
const dx = player.x - e.x;
e.vx = Math.max(-0.5,Math.min(0.5, dx*0.002 - 0.05));
e.x += e.vx; e.dir = e.vx>0?1:-1;
continue;
}
// patrol
e.vy += G; if(e.vy>MAX_FALL)e.vy=MAX_FALL;
// gravity + walk
const ox=e.vx;
const hitX = moveAndCollide(e);
// turn at walls
if(hitX){ e.vx=-e.vx; e.dir = e.vx>0?1:-1; }
else {
// turn at ledges
const aheadX = e.dir>0 ? e.x+e.w+1 : e.x-1;
const footY = e.y+e.h+1;
if(e.onGround && !solidAt(aheadX,footY)){ e.vx=-e.vx; e.dir=e.vx>0?1:-1; }
}
e.x += 0; // already moved
if(e.y>VH+40){ e.alive=false; e.dead=true; e.deadT=60; }
}
// player collisions with enemies
const o=player;
if(o.dead || o.winT>0) return; // dead / winning players don't interact
const starPower = o.star > 0; // star power = rainbow, contact kills enemies
for(const e of entities){
if(e.dead||!e.alive) continue;
if(rectHit(o,e)){
// STAR POWER: any contact vaporizes the enemy
if(starPower){
e.alive=false; e.dead=true; e.deadT=0;
addScore(200); addFloatText(e.x,e.y,'200','#ffd34d');
spawnPuff(e.x+e.w/2,e.y+e.h/2,'#ffd34d');
o.vy = Math.min(o.vy, -3.0); // little star bounce
continue;
}
// Stomp = falling AND feet are in the upper portion of the enemy.
// Lenient: any downward motion (even slow, near apex) onto the top counts.
const feetY = o.y + o.h;
const stomp = (o.vy > -0.5) && ((feetY - e.y) < e.h * 0.6);
if(stomp){
// kill enemy
e.alive=false; e.dead=true; e.deadT=0;
o.vy = keys.run?RUN_STOMP_BOUNCE:STOMP_BOUNCE; o.jumping=true; o.jumpHeld=true;
addScore(100); addFloatText(e.x,e.y,'100','#fff'); sfx('stomp'); spawnPuff(e.x+e.w/2,e.y+e.h/2,ENEMY_DEF[e.type].color);
} else if(o.invuln<=0){
hurtPlayer();
}
}
}
}
function rectHit(a,b){ return a.x < b.x+b.w && a.x+a.w > b.x && a.y < b.y+b.h && a.y+a.h > b.y; }
function hurtPlayer(){
const o=player;
if(o.dead) return;
if(o.star>0 || o.invuln>0) return; // star or post-hit mercy = immune
if(o.big){
// shrink to small, brief invuln, lose fire
o.big=false; o.fire=false;
const oldH=o.h; o.h=SMALL_H; o.y += (oldH-SMALL_H); // keep feet planted
o.invuln = 120; // ~2s mercy
sfx('hurt');
} else {
killPlayer(false);
}
}
function killPlayer(pit){
if(player.dead) return;
player.dead=true; player.deadT=0;
player.vy = pit?2:-6;
sfx('die');
}
function afterDeath(){
lives--;
document.getElementById('hudLives').textContent=lives;
if(lives<=0){ showGameOver(); }
else { resetPlayer(); }
}
function startWinSlide(){
player.winT=1; player.vx=0;
sfx('flag');
addScore(2000);
}
function updateParticles(){
for(let i=particles.length-1;i>=0;i--){
const p=particles[i]; p.t++;
if(p.kind==='coin'){ p.y+=p.vy; p.vy+=0.18; if(p.t>=p.life){ particles.splice(i,1); addFloatText(p.x,p.y-8,'200','#ffd34d'); } }
else if(p.kind==='bit'){ p.x+=p.vx; p.y+=p.vy; p.vy+=0.22; if(p.t>=p.life)particles.splice(i,1); }
else if(p.kind==='puff'){ p.x+=p.vx; p.y+=p.vy; p.vy+=0.05; if(p.t>=p.life)particles.splice(i,1); }
}
for(let i=floatTexts.length-1;i>=0;i--){ floatTexts[i].t++; if(floatTexts[i].t>=floatTexts[i].life)floatTexts.splice(i,1); }
}
function updateCamera(){
const target = player.x - VW*0.38;
cameraX += (target-cameraX)*0.12;
if(cameraX<0)cameraX=0;
const maxCam = LVL_W*TILE - VW;
if(cameraX>maxCam)cameraX=maxCam;
}
function updateTime(){
if(state!==STATE.PLAY) return;
timeAcc++;
if(timeAcc>=40){ timeAcc=0; timeLeft--; document.getElementById('hudTime').textContent=timeLeft; if(timeLeft<=0){ killPlayer(true); timeLeft=0; } }
}
/* =============================================================
SCREEN FLOW
============================================================= */
function startGame(){
audio(); // unlock
if(AC && AC.state==='suspended') AC.resume();
hide('titleScreen'); hide('gameOverScreen'); hide('winScreen');
score=0; coins=0; lives=3; timeLeft=400; timeAcc=0;
document.getElementById('hudScore').textContent='000000';
document.getElementById('hudCoins').textContent='00';
document.getElementById('hudLives').textContent='3';
document.getElementById('hudTime').textContent='400';
buildLevel(); resetPlayer();
state=STATE.PLAY;
}
function showGameOver(){ state=STATE.DEAD; unhide('gameOverScreen'); }
function winGame(){ state=STATE.WIN; document.getElementById('winScore').textContent=score; unhide('winScreen'); }
function hide(id){ document.getElementById(id).classList.add('hidden'); }
function unhide(id){ document.getElementById(id).classList.remove('hidden'); }
document.getElementById('startBtn').onclick = startGame;
document.getElementById('retryBtn').onclick = startGame;
document.getElementById('winBtn').onclick = startGame;
/* =============================================================
INPUT — keyboard + on-screen pad
============================================================= */
function setKey(name,down){
const wasDown = keys[name];
if(name==='jump'){
if(down && !keys.jump){ jumpPressedEdge=true; }
keys.jump=down;
} else if(name==='start'){
if(down && !keys.start){
keys.start=true;
if(state===STATE.TITLE) startGame();
else if(state===STATE.DEAD||state===STATE.WIN) startGame();
}
if(!down) keys.start=false;
} else {
keys[name]=down;
}
// trigger a visible flash on the press edge of a quick-tap button
if(down && !wasDown){
if(name==='jump'){ flashPad('btnA'); flashPad('btnY'); flashPad('dpadU'); flashPad('btnR'); }
if(name==='run'){ flashPad('btnB'); flashPad('btnX'); flashPad('btnL'); }
if(name==='left'){ flashPad('dpadL'); }
if(name==='right'){ flashPad('dpadR'); }
if(name==='down'){ flashPad('dpadD'); }
if(name==='start'){ flashPad('btnStart'); }
}
// highlight controller
highlightPad();
}
window.addEventListener('keydown', e=>{
const k=e.key.toLowerCase();
if(['arrowup','arrowdown','arrowleft','arrowright',' '].includes(e.key.toLowerCase())) e.preventDefault();
switch(k){
case 'arrowleft': case 'a': setKey('left',true); break;
case 'arrowright': case 'd': setKey('right',true); break;
case 'arrowup': case 'w': case 'z': setKey('jump',true); break;
case 'arrowdown': case 's': setKey('down',true); break;
case 'x': case 'k': case 'shift': setKey('run',true); break;
case 'enter': case 'p': setKey('start',true); break;
}
});
window.addEventListener('keyup', e=>{
const k=e.key.toLowerCase();
switch(k){
case 'arrowleft': case 'a': setKey('left',false); break;
case 'arrowright': case 'd': setKey('right',false); break;
case 'arrowup': case 'w': case 'z': setKey('jump',false); break;
case 'arrowdown': case 's': setKey('down',false); break;
case 'x': case 'k': case 'shift': setKey('run',false); break;
case 'enter': case 'p': setKey('start',false); break;
}
});
// touch / click on SVG controller
function bindPad(id,name){
const el=document.getElementById(id); if(!el) return;
const dn=(ev)=>{ ev.preventDefault(); setKey(name,true); };
const up=(ev)=>{ ev.preventDefault(); setKey(name,false); };
el.addEventListener('touchstart',dn,{passive:false});
el.addEventListener('touchend',up,{passive:false});
el.addEventListener('touchcancel',up,{passive:false});
el.addEventListener('mousedown',dn);
window.addEventListener('mouseup',up);
}
bindPad('dpadL','left'); bindPad('dpadR','right'); bindPad('dpadU','jump'); bindPad('dpadD','down');
bindPad('btnA','jump'); bindPad('btnB','run'); bindPad('btnX','run'); bindPad('btnY','jump');
bindPad('btnStart','start'); bindPad('btnSelect','start');
bindPad('btnL','run'); bindPad('btnR','jump');
// base (idle) + active (lit) colors per button — direct fill for reliability on SVG
const PAD_COLORS = {
dpadL:{base:'#0d0d12', lit:'#ffd34d'},
dpadR:{base:'#0d0d12', lit:'#ffd34d'},
dpadU:{base:'#0d0d12', lit:'#ffd34d'},
dpadD:{base:'#0d0d12', lit:'#ffd34d'},
btnA: {base:'#ff6b8a', lit:'#ffe14d'},
btnB: {base:'#ffc14d', lit:'#fff39a'},
btnX: {base:'#5b8cff', lit:'#aef0ff'},
btnY: {base:'#7be07b', lit:'#d6ffd6'},
btnL: {base:'#45454f', lit:'#ffd34d'},
btnR: {base:'#45454f', lit:'#ffd34d'},
btnStart: {base:'#3b3b48', lit:'#ffd34d'},
btnSelect: {base:'#3b3b48', lit:'#ffd34d'},
};
// per-button flash timer so quick taps stay lit ~12 frames even after release
const PAD_FLASH = {};
function flashPad(id){ PAD_FLASH[id]=12; }
function highlightPad(){
const states = {
dpadL:keys.left, dpadR:keys.right, dpadU:keys.jump, dpadD:keys.down,
btnA:keys.jump, btnB:keys.run, btnX:keys.run, btnY:keys.jump,
btnL:keys.run, btnR:keys.jump
};
for(const id in states){
const el = document.getElementById(id); if(!el) continue;
const held = states[id];
const lit = held || (PAD_FLASH[id]>0);
const c = PAD_COLORS[id] || {base:'#555',lit:'#fff'};
el.setAttribute('fill', lit ? c.lit : c.base);
el.classList.toggle('active', lit);
}
}
// called from main loop: decay flash timers every frame
function tickPad(){
let dirty=false;
for(const id in PAD_FLASH){ if(PAD_FLASH[id]>0){ PAD_FLASH[id]--; dirty=true; } }
if(dirty) highlightPad();
}
/* =============================================================
MAIN LOOP — fixed timestep
============================================================= */
function frame(){
worldTime++;
tickPad(); // controller highlight always updates (title + play)
if(state===STATE.PLAY){
updatePlayer();
updateEnemies();
updatePowerups();
updateFireballs();
updateParticles();
updateCamera();
updateTime();
}
// draw
CTX.clearRect(0,0,VW,VH);
drawBackground();
drawTiles();
for(const p of powerups){ if(p.alive) drawPowerup(p); }
for(const e of entities){ if(!e.alive && e.deadT>40) continue; drawEnemy(e); }
drawFireballs();
drawPlayer();
drawParticles();
// CRT-ish overlay tint
CTX.fillStyle='rgba(120,40,160,0.05)'; CTX.fillRect(0,0,VW,VH);
requestAnimationFrame(frame);
}
// boot
buildLevel();
resetPlayer();
frame();
})();
</script>
</body>
</html>

Build a single self-contained HTML file (inline CSS + JS, no external assets, no build step) called "Super Human" — a Super Mario NES-style platformer, but themed as Humanity's last stand vs. AI. The player is "Super Human" (a tiny pixel worker in a red shirt + denim overalls); the enemies are mascots of famous AI labs.

GOAL OF THE GAME: Run right across one long level, stomp/shoot AI mascots, collect data- coins, smash blocks, grab powerups, and reach the flag. Title screen → play → game over / win screens. Full HUD (score, coins, world, time, lives).

═══════════════════════════════════════════ ARCHITECTURE ═══════════════════════════════════════════

  • One at 480x300 virtual resolution, CSS-scaled responsively (aspect 16/10), image-rendering: pixelated.
  • Fixed-timestep game loop via requestAnimationFrame; one update tick per frame.
  • Tile grid level (~220 wide x 15 tall, tile=16px). Procedurally built in JS (NOT hand- aligned ASCII strings). Ground at row 12 (GY=12).
  • Entity arrays for enemies, powerups, fireballs, particles, floating score text.
  • requestAnimationFrame loop: worldTime++ each frame; update then draw.

═══════════════════════════════════════════ PHYSICS (Mario-faithful) ═══════════════════════════════════════════ Use these constants (verified to feel right): G=0.42, MAX_FALL=7.5, WALK_ACCEL=0.18, RUN_ACCEL=0.26, FRICTION=0.16, MAX_WALK=1.8, MAX_RUN=3.0, JUMP_V=-6.0, JUMP_HOLD=-0.26 (applied each tick while rising AND jump held), JUMP_CUT=0.45 (damp vy when released early), STOMP_BOUNCE=-5.0, RUN_STOMP_BOUNCE=-6.2.

  • Variable jump: on press-edge + onGround, set vy=JUMP_V. While held and vy<0, add JUMP_HOLD each tick. On release, if vy<0 multiply by JUMP_CUT.
  • AABB tile collision: move X then resolve, move Y then resolve (separate axes). On head-bump into a block, call bumpBlock(tx,ty).
  • Camera smoothly follows player (lerp 0.12), clamped to level bounds.

═══════════════════════════════════════════ CRITICAL GOTCHAS — implement these correctly or the game breaks: ═══════════════════════════════════════════

  1. JUMP HEIGHT must clear the tallest obstacle. With the constants above, peak is ~110px (~7 tiles). The TALLEST pipe must be 3 tiles tall + lip = 64px (clearable with a running jump). NEVER make a 4+ tile pipe — it becomes an impassable wall.
  2. ENEMY HITBOX MUST MATCH THE VISIBLE SPRITE. AI logos are drawn ~22-24px tall, so the collision box MUST be ~24x24. If you draw a big logo but keep a 14x14 hitbox, the player lands on the visible logo but the real hitbox is below it → registers as a side hit → player dies unfairly.
  3. STOMP DETECTION must be LENIENT: stomp = (player.vy > -0.5) AND ((player feet Y - enemy top Y) < enemy.h * 0.6). Do NOT require vy>0.5 (that kills clean apex stomps).
  4. FLOATING COINS need explicit pickup logic: each frame scan all tiles overlapping the player's bounding box; if a tile is a coin, remove it + score + sfx. Drawing a coin tile does NOT collect it — there must be actual collection code.
  5. CONTROLLER HIGHLIGHT must NOT rely on CSS filter:brightness() on SVG / — it silently fails on many browsers (esp. WebKit/Safari). Instead directly set the fill attribute: el.setAttribute('fill', lit ? litColor : baseColor). ALSO add a per- button tap-flash timer (~12 frames) triggered on the keydown EDGE, so quick taps (jump/ run) stay visibly lit instead of flashing too fast to see. Drive highlight from the main loop every frame, not only from key events.
  6. SEPARATE the "post-hit mercy" timer from the "star power" timer. Do NOT reuse one invuln field — if you do, the 2s post-hit mercy becomes star-strong (contact kills enemies) which is a bug. Use two fields: invuln (~120 frames, blink sprite, immune to hits) and star (~600 frames, rainbow, contact kills enemies).

═══════════════════════════════════════════ PLAYER "SUPER HUMAN" ═══════════════════════════════════════════

  • Small mode: 12x15 hitbox. Big mode: 12x23 (SMALL_H=15, BIG_H=23). When growing, keep feet planted (subtract height delta from y). Draw the 15px sprite template and vertically scale to current height.
  • Facing flips horizontally. Walk cycle (3 frames) when moving; legs tuck when airborne.
  • States: big, fire (has fire power), invuln (post-hit mercy), star (star power), dead, winT (flag slide).
  • Hurt logic: if star>0 or invuln>0 → immune. Else if big → shrink to small + invuln=120
    • lose fire. Else → die.

═══════════════════════════════════════════ ENEMIES — AI lab mascots (draw each as a recognizable pixel/vector mark ~24px, with a small floating name label above it, and a tiny pair of angry eyes to make it a "villain") ═══════════════════════════════════════════ Implement these 9, each a distinct faithful-ish logo:

  • OpenAI: teal 6-petal hex knot (#10a37f)
  • Anthropic: coral sunburst with "A" (#d97757)
  • ZAI: blue rounded badge with "ZAI" text (#3b5bdb)
  • KIMI: purple crescent moon + gold sparkle (#8b5cf6)
  • MiniMax: red rounded badge with "M" (#ff3b3b)
  • Arcee: teal arc + "A" (#22d3ce)
  • Meta: blue infinity loop (#0668e1)
  • DeepSeek: blue whale with spout (#4d6bfe)
  • Gemini: Google-blue 4-point sparkle star, FLYER (hovers with sine + drifts toward player) (#4285F4) The other 8 are PATROL walkers: gravity + walk, turn at walls AND at ledges (check the tile ahead-and-below; if no ground, reverse). Stompable. Die in pits.
  • On stomp: enemy dies, player bounces (STOMP_BOUNCE or RUN_STOMP_BOUNCE if running), +100 score, puff particles, stomp sfx.
  • On side contact (not stomp): hurt player (unless star/invuln).

═══════════════════════════════════════════ TILES ═══════════════════════════════════════════

  • '#' ground (grass-top on row GY, dirt below), 'X' hard block, 'B' brick, '?' q-block, 'C' used block, 'o' floating coin, pipes (2-wide with lip), '|'/F flagpole+ball, 'G' goal.
  • bumpBlock(tx,ty): • '?' → becomes 'C'; consult a blockContents map: 'coin' (default) → coin pop +200; 'mushroom'/'flower' → spawn powerup (mushroom if player small, flower if big); 'star' → spawn star. • 'B' brick → if player BIG: shatter (remove tile, brick bits, +50, flip any enemy standing on top); if SMALL: just bump sfx. • used/hard/pipe → bump sfx.
  • Floating coins: scan player's overlapped tiles each frame; collect 'o' tiles (+200, +1 coin; 100 coins → extra life).

═══════════════════════════════════════════ POWERUPS (themed for Human vs AI) ═══════════════════════════════════════════

  • "Brain Core" (mushroom, red cap + face): grow big. Slides along ground, turns at walls/ledges.
  • "Firewall Torch" (fire flower, orange petals): big + fire power.
  • "Liberty Star" (gold 5-point star): star power ~600 frames. BOUNCES (high gravity, springs up on ground contact), doesn't turn at ledges.
  • All emerge upward out of their block for ~14 frames, then start moving.
  • Fireballs: when fire-powered, pressing RUN (X) spawns a fireball in facing direction (rate-limited ~14 frames, max ~3 on screen). Fireballs bounce up to 4 times, kill enemies on contact (+200), die on wall/pit.
  • Star power: any enemy contact kills it (+200) with a small bounce; player cycles rainbow colors + glowing aura.
  • Place powerups in the level: first ? block = mushroom; a high lone ? block = flower; a ? block mid-level = star; another ? = flower.

═══════════════════════════════════════════ LEVEL (one long stage, ~220 tiles) ═══════════════════════════════════════════

  • Ground with a couple of jumpable gaps. Pipes increasing 1→2→3 tall (NEVER 4).
  • Brick + ? clusters, a brick island with coins, a brick pyramid, mid pipes, a ? field (one is the star), a final 8-step staircase up to the flagpole.
  • Scatter ~14 enemies across the level (mix of patrol types + 2-3 Gemini flyers).

═══════════════════════════════════════════ VISUALS — "Human vs AI" atmosphere ═══════════════════════════════════════════

  • Parallax background: dark purple gradient sky, soft moon, twinkling stars (parallax 0.15), two skyline layers of AI "data-towers" with neon windows + blinking red antenna lights (parallax 0.25 and 0.5), drifting colored data motes (parallax 0.4).
  • All sprites drawn with canvas rects/paths (pixel-art style), no images.

═══════════════════════════════════════════ ON-SCREEN SNES CONTROLLER ═══════════════════════════════════════════

  • Below the screen, an SVG SNES pad: D-pad (4 arrows), A/B/X/Y face buttons, L/R shoulders, Start/Select. Each button is a separate element with an id.
  • Bind keyboard (arrows/WASD, Z/↑=jump, X/Shift=run, Enter=start) AND touch/mouse on the SVG buttons.
  • Buttons light up on press (see gotcha #5 — use direct fill + tap-flash timer, updated every frame from the loop). Held keys keep the button lit the whole time.

═══════════════════════════════════════════ AUDIO ═══════════════════════════════════════════

  • WebAudio chiptune SFX (square/sawtooth beeps): jump, coin, stomp, brick-break, bump, die, power, flag, hurt, fire, star (arpeggio). Unlock the AudioContext on first user gesture (start button).

═══════════════════════════════════════════ SCREENS ═══════════════════════════════════════════

  • Title ("SUPER HUMAN — Humanity's Last Stand vs. The Machines") with start button + controls/powerup hints. Optionally a logo gallery of all 9 AI mascots.
  • Game Over overlay. Win overlay ("Humanity Prevails!" + final score).
  • Retrying resets score/coins/lives/time and rebuilds the level + resets player.

Deliver the complete game in ONE html file. Verify the jump clears the tallest pipe, enemy hitboxes match the visible logos, stomps register leniently, coins are collectible, and the controller lights up on every input. Add a comment at the top noting it's a parody/tribute and the AI logos are stylized fan art.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment