|
<!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 & reach the flag.<br> |
|
<b>KEYS:</b> ◀ ▶ move • <b>Z / ↑</b> jump • <b>X</b> run / throw fireballs • <b>↓</b> crouch<br> |
|
<b>POWERUPS:</b> 🧠 <b>Brain Core</b> (grow big — survive a hit) • 🔥 <b>Firewall Torch</b> (hurl fireballs) • ⭐ <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 & 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> |