Tanner's Super Fun Math Game
by EpicFlare191443 lines39.8 KB
<!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>Tanner's Super Fun Math Game</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #0a0a2e;
overflow: hidden;
touch-action: none;
font-family: 'Segoe UI', sans-serif;
user-select: none;
-webkit-user-select: none;
}
canvas {
display: block;
margin: 0 auto;
}
#mathOverlay {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.75);
z-index: 100;
justify-content: center;
align-items: center;
flex-direction: column;
}
#mathOverlay.active { display: flex; }
#mathBox {
background: linear-gradient(135deg, #1a1a4e, #2a2a6e);
border: 3px solid #ffcc00;
border-radius: 16px;
padding: 30px 40px;
text-align: center;
max-width: 90vw;
width: 420px;
box-shadow: 0 0 40px rgba(255,204,0,0.3);
}
#mathBox h2 {
color: #ffcc00;
font-size: 18px;
margin-bottom: 10px;
text-transform: uppercase;
letter-spacing: 2px;
}
#mathQuestion {
color: #fff;
font-size: 28px;
font-weight: bold;
margin: 15px 0 20px;
min-height: 40px;
}
#mathAnswers {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.mathBtn {
background: linear-gradient(135deg, #2255aa, #3366cc);
color: #fff;
border: 2px solid #4488ee;
border-radius: 10px;
padding: 14px 10px;
font-size: 20px;
font-weight: bold;
cursor: pointer;
transition: all 0.15s;
}
.mathBtn:hover, .mathBtn:active {
background: linear-gradient(135deg, #3366cc, #4488ee);
transform: scale(1.05);
border-color: #ffcc00;
}
#mathTimer {
color: #ff6666;
font-size: 16px;
margin-top: 12px;
font-weight: bold;
}
#mathResult {
color: #fff;
font-size: 22px;
font-weight: bold;
margin-top: 12px;
min-height: 30px;
}
#touchControls {
display: none;
position: fixed;
bottom: 0; left: 0; right: 0;
height: 180px;
z-index: 50;
pointer-events: none;
-webkit-tap-highlight-color: transparent;
}
#touchControls.active { display: block; }
/* === D-PAD === */
#dpad {
position: absolute;
left: 16px;
bottom: 20px;
width: 144px;
height: 144px;
pointer-events: none;
}
.dpad-btn {
position: absolute;
width: 48px;
height: 48px;
background: rgba(40, 40, 60, 0.85);
border: 2px solid rgba(180, 180, 220, 0.5);
color: rgba(220, 220, 255, 0.8);
font-size: 20px;
display: flex;
align-items: center;
justify-content: center;
pointer-events: auto;
touch-action: none;
-webkit-tap-highlight-color: transparent;
transition: background 0.05s;
}
.dpad-btn.pressed {
background: rgba(80, 120, 255, 0.7);
border-color: rgba(130, 180, 255, 0.9);
color: #fff;
}
#dUp { top: 0; left: 48px; border-radius: 8px 8px 0 0; }
#dDown { bottom: 0; left: 48px; border-radius: 0 0 8px 8px; }
#dLeft { top: 48px; left: 0; border-radius: 8px 0 0 8px; }
#dRight { top: 48px; right: 0; border-radius: 0 8px 8px 0; }
#dCenter {
position: absolute;
top: 48px; left: 48px;
width: 48px; height: 48px;
background: rgba(30, 30, 50, 0.9);
border: 2px solid rgba(120, 120, 180, 0.3);
pointer-events: none;
}
/* === ACTION BUTTONS === */
#actionBtns {
position: absolute;
right: 16px;
bottom: 24px;
width: 160px;
height: 150px;
pointer-events: none;
}
.action-btn {
position: absolute;
width: 58px;
height: 58px;
border-radius: 50%;
border: 2.5px solid;
font-size: 13px;
font-weight: bold;
letter-spacing: 0.5px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
pointer-events: auto;
touch-action: none;
-webkit-tap-highlight-color: transparent;
transition: background 0.05s, transform 0.05s;
box-shadow: 0 3px 6px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.15);
}
.action-btn.pressed { transform: scale(0.92); }
.action-btn span.label { font-size: 9px; opacity: 0.6; margin-top: 1px; }
#btnA {
right: 0; bottom: 40px;
background: rgba(200, 50, 50, 0.75);
border-color: rgba(255, 100, 100, 0.7);
color: #ffcccc;
}
#btnA.pressed { background: rgba(255, 80, 80, 0.9); }
#btnB {
right: 68px; bottom: 0;
background: rgba(50, 140, 50, 0.75);
border-color: rgba(100, 220, 100, 0.7);
color: #ccffcc;
}
#btnB.pressed { background: rgba(80, 200, 80, 0.9); }
#btnX {
right: 68px; bottom: 80px;
background: rgba(50, 80, 200, 0.75);
border-color: rgba(100, 130, 255, 0.7);
color: #ccddff;
}
#btnX.pressed { background: rgba(80, 120, 255, 0.9); }
#btnY {
right: 0; bottom: 115px;
width: 46px; height: 46px;
background: rgba(180, 150, 30, 0.75);
border-color: rgba(255, 220, 60, 0.7);
color: #fff5cc;
font-size: 16px;
}
#btnY.pressed { background: rgba(255, 210, 50, 0.9); }
</style>
</head>
<body>
<canvas id="game"></canvas>
<div id="mathOverlay">
<div id="mathBox">
<h2>⚡ MATH ATTACK! ⚡</h2>
<div id="mathQuestion"></div>
<div id="mathAnswers"></div>
<div id="mathTimer"></div>
<div id="mathResult"></div>
</div>
</div>
<div id="touchControls">
<!-- D-Pad -->
<div id="dpad">
<div class="dpad-btn" id="dUp">▲</div>
<div class="dpad-btn" id="dDown">▼</div>
<div class="dpad-btn" id="dLeft">◀</div>
<div class="dpad-btn" id="dRight">▶</div>
<div id="dCenter"></div>
</div>
<!-- Action Buttons -->
<div id="actionBtns">
<div class="action-btn" id="btnA">A<span class="label">ATK</span></div>
<div class="action-btn" id="btnB">B<span class="label">JUMP</span></div>
<div class="action-btn" id="btnX">X<span class="label">JUMP</span></div>
<div class="action-btn" id="btnY">★</div>
</div>
</div>
<script>
// ============================================
// TANNER'S SUPER FUN MATH GAME
// A Smash Bros-style platform fighter
// ============================================
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
// --- SCREEN SETUP ---
let W, H, SCALE;
function resize() {
W = window.innerWidth;
H = window.innerHeight;
canvas.width = W;
canvas.height = H;
SCALE = Math.min(W / 800, H / 600);
}
resize();
window.addEventListener('resize', resize);
// Touch detection — always show on touch devices
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
if (isTouchDevice) {
document.getElementById('touchControls').classList.add('active');
}
// --- GAME STATE ---
const STATE = { MENU: 0, PLAYING: 1, MATH: 2, KO_PAUSE: 3, GAME_OVER: 4 };
let gameState = STATE.MENU;
let gameTime = 0;
let lastMathTime = 0;
const MATH_INTERVAL = 12000; // ms between math questions
let shakeTimer = 0;
let shakeIntensity = 0;
let particles = [];
let announceText = '';
let announceTimer = 0;
let koFreezeTimer = 0;
// --- INPUT ---
const keys = {};
const touchState = { left: false, right: false, jump: false, attack: false, special: false };
window.addEventListener('keydown', e => { keys[e.code] = true; e.preventDefault(); });
window.addEventListener('keyup', e => { keys[e.code] = false; e.preventDefault(); });
// Robust touch controls — track active touches per button
const activeTouches = {};
function bindTouch(elementId, onDown, onUp) {
const el = document.getElementById(elementId);
if (!el) return;
el.addEventListener('touchstart', e => {
e.preventDefault();
e.stopPropagation();
for (const t of e.changedTouches) {
activeTouches[t.identifier] = elementId;
}
el.classList.add('pressed');
onDown();
}, { passive: false });
el.addEventListener('touchend', e => {
e.preventDefault();
e.stopPropagation();
for (const t of e.changedTouches) {
delete activeTouches[t.identifier];
}
// Only release if no other touches on this element
let stillPressed = false;
for (const id in activeTouches) {
if (activeTouches[id] === elementId) stillPressed = true;
}
if (!stillPressed) {
el.classList.remove('pressed');
onUp();
}
}, { passive: false });
el.addEventListener('touchcancel', e => {
e.preventDefault();
for (const t of e.changedTouches) {
delete activeTouches[t.identifier];
}
el.classList.remove('pressed');
onUp();
}, { passive: false });
}
// D-Pad
bindTouch('dLeft', () => { touchState.left = true; }, () => { touchState.left = false; });
bindTouch('dRight', () => { touchState.right = true; }, () => { touchState.right = false; });
bindTouch('dUp', () => { touchState.jump = true; }, () => { touchState.jump = false; });
bindTouch('dDown', () => { /* crouch/fastfall someday */ }, () => {});
// Action buttons: A = attack, B = jump, X = jump, Y/★ = super
bindTouch('btnA', () => { touchState.attack = true; }, () => { touchState.attack = false; });
bindTouch('btnB', () => { touchState.jump = true; }, () => { touchState.jump = false; });
bindTouch('btnX', () => { touchState.jump = true; }, () => { touchState.jump = false; });
bindTouch('btnY', () => { touchState.special = true; }, () => { touchState.special = false; });
function isDown(action) {
switch(action) {
case 'left': return keys['ArrowLeft'] || keys['KeyA'] || touchState.left;
case 'right': return keys['ArrowRight'] || keys['KeyD'] || touchState.right;
case 'jump': return keys['ArrowUp'] || keys['KeyW'] || keys['Space'] || touchState.jump;
case 'attack': return keys['KeyZ'] || keys['KeyJ'] || touchState.attack;
case 'special': return keys['KeyX'] || keys['KeyK'] || touchState.special;
}
}
let prevKeys = {};
function justPressed(action) {
const down = isDown(action);
const key = 'jp_' + action;
const just = down && !prevKeys[key];
prevKeys[key] = down;
return just;
}
// --- PLATFORMS ---
function getPlatforms() {
const cx = W / 2, cy = H / 2;
const s = SCALE;
return [
// Main stage
{ x: cx - 220 * s, y: cy + 100 * s, w: 440 * s, h: 20 * s, main: true },
// Floating platforms
{ x: cx - 280 * s, y: cy - 10 * s, w: 120 * s, h: 14 * s },
{ x: cx + 160 * s, y: cy - 10 * s, w: 120 * s, h: 14 * s },
{ x: cx - 60 * s, y: cy - 100 * s, w: 120 * s, h: 14 * s },
];
}
// --- FIGHTER CLASS ---
class Fighter {
constructor(name, color, accentColor, x, isPlayer) {
this.name = name;
this.color = color;
this.accent = accentColor;
this.isPlayer = isPlayer;
this.stocks = 3;
this.damage = 0;
this.superCharge = 0; // 0-100
this.reset(x);
}
reset(x) {
const s = SCALE;
this.x = x || W / 2;
this.y = H / 2 - 100 * s;
this.vx = 0;
this.vy = 0;
this.w = 30 * s;
this.h = 44 * s;
this.grounded = false;
this.jumps = 0;
this.maxJumps = 2;
this.facing = this.isPlayer ? 1 : -1;
this.attacking = false;
this.attackTimer = 0;
this.attackCooldown = 0;
this.hitlag = 0;
this.stunTimer = 0;
this.invincible = 60;
this.animFrame = 0;
this.animTimer = 0;
this.superAttacking = false;
this.superTimer = 0;
this.hasHit = false;
}
get cx() { return this.x + this.w / 2; }
get cy() { return this.y + this.h / 2; }
getAttackBox() {
const s = SCALE;
const range = this.superAttacking ? 70 * s : 40 * s;
return {
x: this.facing > 0 ? this.x + this.w : this.x - range,
y: this.y - 5 * s,
w: range,
h: this.h + 10 * s
};
}
update(dt, platforms, opponent) {
const s = SCALE;
const gravity = 1800 * s;
const speed = 350 * s;
const jumpForce = -580 * s;
const friction = 0.85;
if (this.invincible > 0) this.invincible--;
if (this.hitlag > 0) { this.hitlag--; return; }
if (this.stunTimer > 0) { this.stunTimer -= dt; }
if (this.attackCooldown > 0) this.attackCooldown -= dt;
// AI or player control
if (this.stunTimer <= 0) {
if (this.isPlayer) {
this.playerControl(speed, jumpForce, dt);
} else {
this.aiControl(speed, jumpForce, dt, opponent);
}
}
// Physics
this.vy += gravity * dt;
this.x += this.vx * dt;
this.y += this.vy * dt;
// Platform collision
this.grounded = false;
for (const p of platforms) {
if (this.vy >= 0 &&
this.x + this.w > p.x && this.x < p.x + p.w &&
this.y + this.h > p.y && this.y + this.h < p.y + p.h + this.vy * dt + 5 * s) {
this.y = p.y - this.h;
this.vy = 0;
this.grounded = true;
this.jumps = 0;
}
}
// Air friction
if (!this.grounded) {
this.vx *= 0.995;
}
// Attack timer
if (this.attacking) {
this.attackTimer -= dt;
if (this.attackTimer <= 0) {
this.attacking = false;
this.superAttacking = false;
}
}
if (this.superAttacking) {
this.superTimer -= dt;
}
// Animation
this.animTimer += dt;
if (this.animTimer > 0.15) {
this.animTimer = 0;
this.animFrame = (this.animFrame + 1) % 4;
}
// Check blast zone
if (this.x < -100 * s || this.x > W + 100 * s ||
this.y < -200 * s || this.y > H + 100 * s) {
return 'blastzone';
}
return null;
}
playerControl(speed, jumpForce, dt) {
if (isDown('left')) {
this.vx = -speed;
this.facing = -1;
} else if (isDown('right')) {
this.vx = speed;
this.facing = 1;
} else {
this.vx *= 0.8;
}
if (justPressed('jump') && this.jumps < this.maxJumps) {
this.vy = jumpForce;
this.jumps++;
spawnParticles(this.cx, this.y + this.h, 5, '#ffffff', 'burst');
}
if (justPressed('attack') && this.attackCooldown <= 0 && !this.attacking) {
this.attacking = true;
this.attackTimer = 0.2;
this.attackCooldown = 0.35;
this.hasHit = false;
}
if (justPressed('special') && this.superCharge >= 100 && !this.attacking) {
this.attacking = true;
this.superAttacking = true;
this.attackTimer = 0.5;
this.superTimer = 0.5;
this.attackCooldown = 0.6;
this.superCharge = 0;
this.hasHit = false;
shakeTimer = 0.3;
shakeIntensity = 8;
spawnParticles(this.cx, this.cy, 20, '#ffcc00', 'explosion');
}
}
aiControl(speed, jumpForce, dt, player) {
const dx = player.cx - this.cx;
const dy = player.cy - this.cy;
const dist = Math.sqrt(dx * dx + dy * dy);
const s = SCALE;
// Face player
this.facing = dx > 0 ? 1 : -1;
// Move toward player but keep fighting distance (not overlapping!)
const idealDist = 55 * s;
if (Math.abs(dx) > idealDist + 20 * s) {
// Too far — approach
this.vx = (dx > 0 ? 1 : -1) * speed * 0.65;
} else if (Math.abs(dx) < idealDist - 10 * s) {
// Too close — back off
this.vx = (dx > 0 ? -1 : 1) * speed * 0.4;
} else {
this.vx *= 0.8;
}
// Jump if player is above or to recover
if ((dy < -50 * s || this.y > H / 2 + 80 * s) && this.jumps < this.maxJumps && Math.random() < 0.03) {
this.vy = jumpForce;
this.jumps++;
}
// Attack when in range (not too close)
if (dist < 90 * s && dist > 25 * s && this.attackCooldown <= 0 && !this.attacking && Math.random() < 0.04) {
this.attacking = true;
this.attackTimer = 0.2;
this.attackCooldown = 0.6;
this.hasHit = false;
}
// Use super if charged
if (this.superCharge >= 100 && dist < 100 * s && dist > 25 * s && !this.attacking && Math.random() < 0.015) {
this.attacking = true;
this.superAttacking = true;
this.attackTimer = 0.5;
this.superTimer = 0.5;
this.attackCooldown = 0.7;
this.superCharge = 0;
this.hasHit = false;
shakeTimer = 0.3;
shakeIntensity = 8;
spawnParticles(this.cx, this.cy, 20, '#ff4444', 'explosion');
}
}
draw() {
const s = SCALE;
ctx.save();
// Invincibility flash
if (this.invincible > 0 && Math.floor(this.invincible / 3) % 2) {
ctx.globalAlpha = 0.4;
}
// Stun visual
if (this.stunTimer > 0) {
ctx.globalAlpha = 0.7 + Math.sin(Date.now() * 0.02) * 0.3;
}
const cx = this.x + this.w / 2;
const cy = this.y + this.h / 2;
// Super glow
if (this.superCharge >= 100) {
ctx.shadowColor = this.isPlayer ? '#ffcc00' : '#ff4444';
ctx.shadowBlur = 15 + Math.sin(Date.now() * 0.005) * 8;
}
if (this.superAttacking) {
ctx.shadowColor = this.isPlayer ? '#ffcc00' : '#ff4444';
ctx.shadowBlur = 30;
}
// Body
const bob = this.grounded ? Math.sin(this.animTimer * 15) * 2 * s : 0;
ctx.fillStyle = this.color;
roundRect(this.x + 2 * s, this.y + bob + 12 * s, this.w - 4 * s, this.h - 12 * s, 6 * s);
ctx.fill();
// Head
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(cx, this.y + bob + 10 * s, 14 * s, 0, Math.PI * 2);
ctx.fill();
// Eyes
const ex = this.facing * 4 * s;
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(cx + ex - 4 * s, this.y + bob + 8 * s, 4 * s, 0, Math.PI * 2);
ctx.arc(cx + ex + 4 * s, this.y + bob + 8 * s, 4 * s, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#111';
ctx.beginPath();
ctx.arc(cx + ex - 3 * s + this.facing * s, this.y + bob + 8 * s, 2 * s, 0, Math.PI * 2);
ctx.arc(cx + ex + 5 * s + this.facing * s, this.y + bob + 8 * s, 2 * s, 0, Math.PI * 2);
ctx.fill();
// Mouth
if (this.attacking) {
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(cx + ex, this.y + bob + 15 * s, 3 * s, 0, Math.PI);
ctx.fill();
} else {
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1.5 * s;
ctx.beginPath();
ctx.arc(cx + ex, this.y + bob + 14 * s, 3 * s, 0.1, Math.PI - 0.1);
ctx.stroke();
}
// Attack arm
if (this.attacking) {
ctx.fillStyle = this.accent;
const armX = this.facing > 0 ? this.x + this.w : this.x;
const armLen = this.superAttacking ? 45 * s : 25 * s;
ctx.fillRect(armX, this.y + bob + 20 * s, this.facing * armLen, 8 * s);
// Attack effect
if (this.superAttacking) {
ctx.fillStyle = this.isPlayer ? 'rgba(255,204,0,0.6)' : 'rgba(255,68,68,0.6)';
const ex2 = armX + this.facing * armLen;
ctx.beginPath();
ctx.arc(ex2, this.y + bob + 24 * s, 20 * s + Math.sin(Date.now() * 0.01) * 5 * s, 0, Math.PI * 2);
ctx.fill();
} else {
ctx.fillStyle = 'rgba(255,255,255,0.5)';
const ex2 = armX + this.facing * armLen;
for (let i = 0; i < 3; i++) {
const a = (Date.now() * 0.01 + i * 2) % 6.28;
ctx.fillRect(ex2 + Math.cos(a) * 10 * s, this.y + bob + 18 * s + Math.sin(a) * 10 * s, 4 * s, 4 * s);
}
}
}
// Name tag
ctx.shadowBlur = 0;
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.font = `bold ${11 * s}px sans-serif`;
ctx.textAlign = 'center';
const nameW = ctx.measureText(this.name).width;
ctx.fillRect(cx - nameW / 2 - 4 * s, this.y - 18 * s, nameW + 8 * s, 14 * s);
ctx.fillStyle = '#fff';
ctx.fillText(this.name, cx, this.y - 7 * s);
ctx.restore();
}
}
// --- PLAYERS ---
let player, cpu;
function initFighters() {
const s = SCALE;
player = new Fighter('TANNER', '#2277ff', '#66aaff', W / 2 - 100 * s, true);
cpu = new Fighter('MATH BOT', '#ee3344', '#ff7788', W / 2 + 100 * s, false);
}
initFighters();
// --- MATH SYSTEM ---
function genMathQuestion() {
const types = [
'orderOps', 'orderOps', 'fraction', 'fraction',
'algebra', 'algebra', 'percent', 'exponent', 'negative'
];
const type = types[Math.floor(Math.random() * types.length)];
let question, answer, choices;
switch (type) {
case 'orderOps': {
const templates = [
() => {
const a = randInt(2, 9), b = randInt(2, 9), c = randInt(1, 9);
return { q: `${a} + ${b} × ${c}`, a: a + b * c };
},
() => {
const a = randInt(10, 30), b = randInt(2, 5), c = randInt(1, 5);
return { q: `${a} - ${b} × ${c}`, a: a - b * c };
},
() => {
const a = randInt(2, 6), b = randInt(2, 6), c = randInt(1, 9);
return { q: `(${a} + ${b}) × ${c}`, a: (a + b) * c };
},
() => {
const a = randInt(2, 8), b = randInt(2, 8), c = randInt(2, 5), d = randInt(1, 5);
return { q: `${a} + ${b} × ${c} - ${d}`, a: a + b * c - d };
},
];
const t = templates[Math.floor(Math.random() * templates.length)]();
question = t.q;
answer = t.a;
break;
}
case 'fraction': {
const templates = [
() => {
const pairs = [[2,3],[3,4],[2,5],[3,5],[4,5],[2,7],[3,7],[5,6]];
const [a, b] = pairs[Math.floor(Math.random() * pairs.length)];
const [c, d] = pairs[Math.floor(Math.random() * pairs.length)];
const num = a * d + c * b;
const den = b * d;
const g = gcd(num, den);
return { q: `${a}/${b} + ${c}/${d}`, a: `${num/g}/${den/g}` };
},
() => {
const a = randInt(1, 5), b = randInt(2, 6);
const c = randInt(1, 5), d = randInt(2, 6);
const num = a * c, den = b * d;
const g = gcd(num, den);
const ans = den / g === 1 ? `${num/g}` : `${num/g}/${den/g}`;
return { q: `${a}/${b} × ${c}/${d}`, a: ans };
},
];
const t = templates[Math.floor(Math.random() * templates.length)]();
question = t.q;
answer = t.a;
// Generate wrong fraction answers
choices = generateFractionChoices(answer);
break;
}
case 'algebra': {
const templates = [
() => {
const x = randInt(1, 12);
const a = randInt(2, 6);
const b = randInt(1, 20);
return { q: `${a}x + ${b} = ${a * x + b}, x = ?`, a: x };
},
() => {
const x = randInt(1, 10);
const a = randInt(2, 5);
const b = randInt(1, 10);
return { q: `${a}x - ${b} = ${a * x - b}, x = ?`, a: x };
},
() => {
const x = randInt(2, 8);
return { q: `x² = ${x * x}, x = ?`, a: x };
},
];
const t = templates[Math.floor(Math.random() * templates.length)]();
question = t.q;
answer = t.a;
break;
}
case 'percent': {
const percents = [10, 15, 20, 25, 30, 40, 50, 75];
const p = percents[Math.floor(Math.random() * percents.length)];
const base = [40, 60, 80, 100, 120, 150, 200, 250, 300][Math.floor(Math.random() * 9)];
question = `${p}% of ${base}`;
answer = p * base / 100;
break;
}
case 'exponent': {
const base = randInt(2, 6);
const exp = randInt(2, 4);
question = `${base}${toSuperscript(exp)}`;
answer = Math.pow(base, exp);
break;
}
case 'negative': {
const a = randInt(-15, -1);
const b = randInt(-10, 15);
const op = Math.random() > 0.5;
if (op) {
question = `(${a}) + ${b >= 0 ? b : '(' + b + ')'}`;
answer = a + b;
} else {
question = `(${a}) × ${b >= 0 ? b : '(' + b + ')'}`;
answer = a * b;
}
break;
}
}
// Generate choices if not already set
if (!choices) {
choices = generateNumericChoices(answer);
}
return { question: `${question} = ?`, answer: String(answer), choices };
}
function generateNumericChoices(correct) {
const c = [String(correct)];
const offsets = shuffle([-3, -2, -1, 1, 2, 3, 5, -5, 4, -4]);
let i = 0;
while (c.length < 4 && i < offsets.length) {
const wrong = String(correct + offsets[i]);
if (!c.includes(wrong) && wrong !== String(correct)) c.push(wrong);
i++;
}
while (c.length < 4) c.push(String(correct + c.length * 7));
return shuffle(c);
}
function generateFractionChoices(correct) {
const c = [correct];
// Parse correct
const parts = String(correct).split('/');
let num = parseInt(parts[0]);
let den = parts.length > 1 ? parseInt(parts[1]) : 1;
const wrongs = [
`${num + 1}/${den}`,
`${num}/${den + 1}`,
`${num - 1}/${den}`,
`${num + 1}/${den + 1}`,
`${num * 2}/${den * 2 + 1}`,
`${num + 2}/${den}`,
];
for (const w of shuffle(wrongs)) {
if (!c.includes(w) && c.length < 4) c.push(w);
}
while (c.length < 4) c.push(`${num + c.length}/${den + 1}`);
return shuffle(c);
}
function randInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }
function gcd(a, b) { a = Math.abs(a); b = Math.abs(b); while (b) { [a, b] = [b, a % b]; } return a; }
function shuffle(arr) { for (let i = arr.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [arr[i], arr[j]] = [arr[j], arr[i]]; } return arr; }
function toSuperscript(n) { return String(n).split('').map(d => '⁰¹²³⁴⁵⁶⁷⁸⁹'[d]).join(''); }
// --- MATH OVERLAY ---
let mathActive = false;
let mathTimeLeft = 0;
let mathInterval = null;
let mathCorrectAnswer = '';
function showMathQuestion() {
if (mathActive || gameState !== STATE.PLAYING) return;
const q = genMathQuestion();
gameState = STATE.MATH;
mathActive = true;
mathCorrectAnswer = q.answer;
mathTimeLeft = 10;
const overlay = document.getElementById('mathOverlay');
overlay.classList.add('active');
document.getElementById('mathQuestion').textContent = q.question;
document.getElementById('mathResult').textContent = '';
document.getElementById('mathTimer').textContent = `⏱ ${mathTimeLeft}s`;
const answersDiv = document.getElementById('mathAnswers');
answersDiv.innerHTML = '';
q.choices.forEach(choice => {
const btn = document.createElement('button');
btn.className = 'mathBtn';
btn.textContent = choice;
btn.onclick = () => answerMath(choice);
answersDiv.appendChild(btn);
});
mathInterval = setInterval(() => {
mathTimeLeft--;
document.getElementById('mathTimer').textContent = `⏱ ${mathTimeLeft}s`;
if (mathTimeLeft <= 0) {
answerMath(null);
}
}, 1000);
}
function answerMath(choice) {
clearInterval(mathInterval);
const resultEl = document.getElementById('mathResult');
const answersDiv = document.getElementById('mathAnswers');
// Highlight correct answer
Array.from(answersDiv.children).forEach(btn => {
btn.disabled = true;
if (btn.textContent === mathCorrectAnswer) {
btn.style.background = 'linear-gradient(135deg, #22aa44, #44cc66)';
btn.style.borderColor = '#44cc66';
} else if (btn.textContent === choice) {
btn.style.background = 'linear-gradient(135deg, #aa2222, #cc4444)';
btn.style.borderColor = '#cc4444';
}
});
if (choice === mathCorrectAnswer) {
resultEl.textContent = '✅ CORRECT! +30 SUPER CHARGE!';
resultEl.style.color = '#44ff44';
player.superCharge = Math.min(100, player.superCharge + 30);
spawnParticles(player.cx, player.cy, 15, '#ffcc00', 'explosion');
} else {
resultEl.textContent = choice === null ?
`⏰ TIME'S UP! Answer: ${mathCorrectAnswer}` :
`❌ WRONG! Answer: ${mathCorrectAnswer}`;
resultEl.style.color = '#ff6666';
cpu.damage = Math.max(0, cpu.damage - 15);
cpu.superCharge = Math.min(100, cpu.superCharge + 20);
}
setTimeout(() => {
document.getElementById('mathOverlay').classList.remove('active');
mathActive = false;
gameState = STATE.PLAYING;
lastMathTime = gameTime;
}, 1800);
}
// --- PARTICLES ---
function spawnParticles(x, y, count, color, type) {
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = type === 'explosion' ? (80 + Math.random() * 200) * SCALE : (50 + Math.random() * 100) * SCALE;
particles.push({
x, y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed - (type === 'burst' ? 100 * SCALE : 0),
life: 0.4 + Math.random() * 0.4,
maxLife: 0.4 + Math.random() * 0.4,
color,
size: (2 + Math.random() * 4) * SCALE,
});
}
}
// --- COLLISION ---
function boxOverlap(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 checkAttackHit(attacker, defender) {
if (!attacker.attacking || attacker.hasHit) return;
if (defender.invincible > 0) return;
const atk = attacker.getAttackBox();
const def = { x: defender.x, y: defender.y, w: defender.w, h: defender.h };
if (boxOverlap(atk, def)) {
attacker.hasHit = true; // Only one hit per swing!
const isSuper = attacker.superAttacking;
const dmg = isSuper ? 25 + Math.random() * 10 : 8 + Math.random() * 5;
defender.damage += dmg;
// Knockback based on damage %
const kb = (defender.damage / 100 + 0.5) * (isSuper ? 900 : 400) * SCALE;
const angle = isSuper ? -0.3 : -0.5;
defender.vx = attacker.facing * kb * Math.cos(angle);
defender.vy = kb * Math.sin(angle) - 200 * SCALE;
defender.stunTimer = isSuper ? 0.5 : 0.2;
// Hitlag
attacker.hitlag = isSuper ? 6 : 3;
defender.hitlag = isSuper ? 6 : 3;
// Effects
shakeTimer = isSuper ? 0.25 : 0.1;
shakeIntensity = isSuper ? 10 : 4;
spawnParticles(
(attacker.cx + defender.cx) / 2,
(attacker.cy + defender.cy) / 2,
isSuper ? 15 : 6,
isSuper ? '#ffcc00' : '#ffffff',
isSuper ? 'explosion' : 'burst'
);
// Charge super from hitting
attacker.superCharge = Math.min(100, attacker.superCharge + (isSuper ? 0 : 5));
}
}
// --- DRAWING HELPERS ---
function roundRect(x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
}
function drawBackground() {
// Sky gradient
const grad = ctx.createLinearGradient(0, 0, 0, H);
grad.addColorStop(0, '#0a0a2e');
grad.addColorStop(0.5, '#1a1a4e');
grad.addColorStop(1, '#0d0d35');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, W, H);
// Stars
const seed = 42;
for (let i = 0; i < 60; i++) {
const sx = ((seed * (i + 1) * 7.3) % W);
const sy = ((seed * (i + 1) * 3.7) % (H * 0.6));
const ss = 0.5 + ((i * 13) % 3) * 0.5;
const bright = 0.3 + Math.sin(Date.now() * 0.001 + i) * 0.2;
ctx.fillStyle = `rgba(255,255,255,${bright})`;
ctx.fillRect(sx, sy, ss * SCALE, ss * SCALE);
}
}
function drawPlatforms() {
const platforms = getPlatforms();
for (const p of platforms) {
// Platform shadow
ctx.fillStyle = 'rgba(0,0,0,0.3)';
ctx.fillRect(p.x + 3 * SCALE, p.y + 3 * SCALE, p.w, p.h);
// Platform body
const grad = ctx.createLinearGradient(p.x, p.y, p.x, p.y + p.h);
if (p.main) {
grad.addColorStop(0, '#4a6fa5');
grad.addColorStop(1, '#2a4a7a');
} else {
grad.addColorStop(0, '#5a7fb5');
grad.addColorStop(1, '#3a5a8a');
}
ctx.fillStyle = grad;
roundRect(p.x, p.y, p.w, p.h, 4 * SCALE);
ctx.fill();
// Top edge highlight
ctx.fillStyle = 'rgba(255,255,255,0.2)';
ctx.fillRect(p.x + 4 * SCALE, p.y, p.w - 8 * SCALE, 2 * SCALE);
}
}
function drawHUD() {
const s = SCALE;
// Player HUD (left)
drawFighterHUD(player, 20 * s, 15 * s, '#2277ff');
// CPU HUD (right)
drawFighterHUD(cpu, W - 220 * s, 15 * s, '#ee3344');
// Announcement
if (announceTimer > 0) {
ctx.save();
ctx.globalAlpha = Math.min(1, announceTimer * 2);
ctx.fillStyle = '#fff';
ctx.font = `bold ${36 * s}px sans-serif`;
ctx.textAlign = 'center';
ctx.shadowColor = '#000';
ctx.shadowBlur = 10;
ctx.fillText(announceText, W / 2, H / 2 - 40 * s);
ctx.restore();
}
}
function drawFighterHUD(fighter, x, y, color) {
const s = SCALE;
const w = 200 * s;
// Background
ctx.fillStyle = 'rgba(0,0,0,0.6)';
roundRect(x - 5 * s, y - 5 * s, w + 10 * s, 70 * s, 8 * s);
ctx.fill();
// Name
ctx.fillStyle = color;
ctx.font = `bold ${14 * s}px sans-serif`;
ctx.textAlign = 'left';
ctx.fillText(fighter.name, x + 5 * s, y + 14 * s);
// Damage %
const dmgColor = fighter.damage < 50 ? '#44ff44' :
fighter.damage < 100 ? '#ffcc00' :
fighter.damage < 150 ? '#ff8800' : '#ff3333';
ctx.fillStyle = dmgColor;
ctx.font = `bold ${22 * s}px sans-serif`;
ctx.fillText(`${Math.floor(fighter.damage)}%`, x + 5 * s, y + 40 * s);
// Stocks
for (let i = 0; i < 3; i++) {
ctx.fillStyle = i < fighter.stocks ? color : 'rgba(255,255,255,0.15)';
ctx.beginPath();
ctx.arc(x + 140 * s + i * 22 * s, y + 14 * s, 7 * s, 0, Math.PI * 2);
ctx.fill();
}
// Super meter
ctx.fillStyle = 'rgba(255,255,255,0.15)';
ctx.fillRect(x + 5 * s, y + 50 * s, w - 10 * s, 8 * s);
const superColor = fighter.superCharge >= 100 ?
`hsl(${(Date.now() * 0.2) % 360}, 100%, 60%)` :
fighter.isPlayer ? '#ffcc00' : '#ff6666';
ctx.fillStyle = superColor;
ctx.fillRect(x + 5 * s, y + 50 * s, (w - 10 * s) * (fighter.superCharge / 100), 8 * s);
// Super label
ctx.fillStyle = fighter.superCharge >= 100 ? '#fff' : 'rgba(255,255,255,0.5)';
ctx.font = `${9 * s}px sans-serif`;
ctx.fillText(fighter.superCharge >= 100 ? '★ SUPER READY!' : 'SUPER', x + 8 * s, y + 57 * s);
}
function drawControls() {
if (isTouchDevice) return;
const s = SCALE;
ctx.save();
ctx.globalAlpha = 0.4;
ctx.fillStyle = '#fff';
ctx.font = `${10 * s}px sans-serif`;
ctx.textAlign = 'center';
ctx.fillText('← → Move | ↑/W Jump | Z Attack | X Super (when meter full)', W / 2, H - 12 * s);
ctx.restore();
}
// --- MENU ---
function drawMenu() {
drawBackground();
const s = SCALE;
// Title
ctx.save();
ctx.textAlign = 'center';
// Glow
ctx.shadowColor = '#ffcc00';
ctx.shadowBlur = 20 + Math.sin(Date.now() * 0.003) * 10;
ctx.fillStyle = '#ffcc00';
ctx.font = `bold ${42 * s}px sans-serif`;
ctx.fillText("TANNER'S", W / 2, H / 2 - 80 * s);
ctx.font = `bold ${28 * s}px sans-serif`;
ctx.fillText('SUPER FUN MATH GAME', W / 2, H / 2 - 40 * s);
ctx.shadowBlur = 0;
// Subtitle
ctx.fillStyle = '#aaccff';
ctx.font = `${16 * s}px sans-serif`;
ctx.fillText('⚔️ Smash Bros × Math = Epic! ⚔️', W / 2, H / 2);
// Characters preview
ctx.fillStyle = '#2277ff';
ctx.beginPath();
ctx.arc(W / 2 - 60 * s, H / 2 + 50 * s, 20 * s, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#fff';
ctx.font = `bold ${10 * s}px sans-serif`;
ctx.fillText('TANNER', W / 2 - 60 * s, H / 2 + 80 * s);
ctx.fillStyle = '#ee3344';
ctx.beginPath();
ctx.arc(W / 2 + 60 * s, H / 2 + 50 * s, 20 * s, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#fff';
ctx.fillText('MATH BOT', W / 2 + 60 * s, H / 2 + 80 * s);
ctx.fillStyle = '#fff';
ctx.font = `${12 * s}px sans-serif`;
ctx.fillText('VS', W / 2, H / 2 + 55 * s);
// Start prompt
const pulse = 0.5 + Math.sin(Date.now() * 0.004) * 0.5;
ctx.globalAlpha = 0.6 + pulse * 0.4;
ctx.fillStyle = '#fff';
ctx.font = `bold ${18 * s}px sans-serif`;
ctx.fillText(isTouchDevice ? 'TAP TO START' : 'PRESS ENTER TO START', W / 2, H / 2 + 130 * s);
ctx.restore();
}
function drawGameOver() {
const s = SCALE;
ctx.save();
ctx.fillStyle = 'rgba(0,0,0,0.7)';
ctx.fillRect(0, 0, W, H);
ctx.textAlign = 'center';
const winner = player.stocks > 0 ? 'TANNER' : 'MATH BOT';
const winColor = player.stocks > 0 ? '#2277ff' : '#ee3344';
ctx.shadowColor = winColor;
ctx.shadowBlur = 20;
ctx.fillStyle = winColor;
ctx.font = `bold ${48 * s}px sans-serif`;
ctx.fillText(`${winner} WINS!`, W / 2, H / 2 - 20 * s);
ctx.shadowBlur = 0;
ctx.fillStyle = '#fff';
ctx.font = `${16 * s}px sans-serif`;
if (player.stocks > 0) {
ctx.fillText('🧮 Math power prevails! 🧮', W / 2, H / 2 + 20 * s);
} else {
ctx.fillText('Study more math to get stronger!', W / 2, H / 2 + 20 * s);
}
const pulse = 0.5 + Math.sin(Date.now() * 0.004) * 0.5;
ctx.globalAlpha = 0.6 + pulse * 0.4;
ctx.font = `bold ${14 * s}px sans-serif`;
ctx.fillText(isTouchDevice ? 'TAP TO PLAY AGAIN' : 'PRESS ENTER TO PLAY AGAIN', W / 2, H / 2 + 70 * s);
ctx.restore();
}
// --- MAIN LOOP ---
let lastTime = performance.now();
function gameLoop(now) {
const rawDt = (now - lastTime) / 1000;
const dt = Math.min(rawDt, 0.05);
lastTime = now;
// Clear
ctx.clearRect(0, 0, W, H);
// Camera shake
if (shakeTimer > 0) {
shakeTimer -= dt;
const sx = (Math.random() - 0.5) * shakeIntensity * SCALE;
const sy = (Math.random() - 0.5) * shakeIntensity * SCALE;
ctx.save();
ctx.translate(sx, sy);
}
switch (gameState) {
case STATE.MENU:
drawMenu();
if (keys['Enter'] || keys['Space']) {
startGame();
}
break;
case STATE.PLAYING:
gameTime += dt * 1000;
updatePlaying(dt);
drawPlaying();
break;
case STATE.MATH:
drawPlaying();
break;
case STATE.KO_PAUSE:
koFreezeTimer -= dt;
drawPlaying();
if (koFreezeTimer <= 0) {
if (player.stocks <= 0 || cpu.stocks <= 0) {
gameState = STATE.GAME_OVER;
} else {
respawnFighters();
gameState = STATE.PLAYING;
}
}
break;
case STATE.GAME_OVER:
drawPlaying();
drawGameOver();
if (keys['Enter'] || keys['Space']) {
startGame();
}
break;
}
// Update particles (always)
updateParticles(dt);
drawParticles();
// Announcement
if (announceTimer > 0) announceTimer -= dt;
if (shakeTimer > 0 || shakeTimer + dt > 0) {
ctx.restore();
}
// Reset justPressed tracking
// (handled in justPressed function)
requestAnimationFrame(gameLoop);
}
function startGame() {
gameState = STATE.PLAYING;
gameTime = 0;
lastMathTime = 0;
particles = [];
initFighters();
announceText = 'FIGHT!';
announceTimer = 1.5;
}
function updatePlaying(dt) {
const platforms = getPlatforms();
// Check for math question trigger
if (gameTime - lastMathTime > MATH_INTERVAL) {
showMathQuestion();
return;
}
// Update fighters
const pResult = player.update(dt, platforms, cpu);
const cResult = cpu.update(dt, platforms, player);
// Body-to-body separation (prevent overlapping)
const overlap = (player.x + player.w) - cpu.x;
const overlap2 = (cpu.x + cpu.w) - player.x;
// Check if they're actually overlapping vertically too
if (player.y + player.h > cpu.y && player.y < cpu.y + cpu.h) {
if (player.cx < cpu.cx) {
// Player is left of CPU
const ox = (player.x + player.w) - cpu.x;
if (ox > 0) {
player.x -= ox / 2;
cpu.x += ox / 2;
}
} else {
// CPU is left of player
const ox = (cpu.x + cpu.w) - player.x;
if (ox > 0) {
cpu.x -= ox / 2;
player.x += ox / 2;
}
}
}
// Check hits
checkAttackHit(player, cpu);
checkAttackHit(cpu, player);
// Check KOs
if (pResult === 'blastzone') {
player.stocks--;
player.damage = 0;
handleKO('TANNER', '#2277ff');
}
if (cResult === 'blastzone') {
cpu.stocks--;
cpu.damage = 0;
handleKO('MATH BOT', '#ee3344');
}
}
function handleKO(name, color) {
gameState = STATE.KO_PAUSE;
koFreezeTimer = 1.5;
announceText = `💥 ${name} KO'd!`;
announceTimer = 1.5;
shakeTimer = 0.4;
shakeIntensity = 12;
spawnParticles(W / 2, H / 2, 30, color, 'explosion');
}
function respawnFighters() {
const s = SCALE;
if (player.stocks > 0) player.reset(W / 2 - 100 * s);
if (cpu.stocks > 0) cpu.reset(W / 2 + 100 * s);
announceText = 'GO!';
announceTimer = 1;
}
function drawPlaying() {
drawBackground();
drawPlatforms();
player.draw();
cpu.draw();
drawHUD();
drawControls();
}
function updateParticles(dt) {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.life -= dt;
if (p.life <= 0) { particles.splice(i, 1); continue; }
p.x += p.vx * dt;
p.y += p.vy * dt;
p.vy += 400 * SCALE * dt;
p.vx *= 0.98;
}
}
function drawParticles() {
for (const p of particles) {
ctx.globalAlpha = p.life / p.maxLife;
ctx.fillStyle = p.color;
ctx.fillRect(p.x - p.size / 2, p.y - p.size / 2, p.size, p.size);
}
ctx.globalAlpha = 1;
}
// Start game via tap or click (only on menu/gameover screens)
canvas.addEventListener('click', () => {
if (gameState === STATE.MENU || gameState === STATE.GAME_OVER) startGame();
});
canvas.addEventListener('touchend', e => {
if (gameState === STATE.MENU || gameState === STATE.GAME_OVER) {
e.preventDefault();
startGame();
}
// During gameplay, do NOT preventDefault — let events flow naturally
}, { passive: false });
// GO
requestAnimationFrame(gameLoop);
</script>
</body>
</html>Game Source: Tanner's Super Fun Math Game
Creator: EpicFlare19
Libraries: none
Complexity: complex (1443 lines, 39.8 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: tanner-s-super-fun-math-game-epicflare19" to link back to the original. Then publish at arcadelab.ai/publish.