Tidebreaker: Pixel Depths
by LaserBuilder681459 lines40.7 KB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<meta name="theme-color" content="#05080f">
<title>Tidebreaker: Pixel Depths</title>
<style>
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent;}
html,body{
margin:0;padding:0;height:100%;width:100%;
background:#05080f;overflow:hidden;
font-family:'Courier New',monospace;
user-select:none;-webkit-user-select:none;
touch-action:none;overscroll-behavior:none;
}
#game{position:fixed;inset:0;display:flex;flex-direction:column;}
#stage{flex:1;display:flex;align-items:center;justify-content:center;overflow:hidden;min-height:0;}
canvas{
image-rendering:pixelated;
image-rendering:-moz-crisp-edges;
image-rendering:crisp-edges;
display:block;
touch-action:none;
}
#controls{
display:none;
flex:0 0 auto;
height:min(150px,26vh);
padding:8px 14px calc(12px + env(safe-area-inset-bottom));
justify-content:space-between;align-items:flex-end;
background:linear-gradient(to top,rgba(5,8,15,.9),rgba(5,8,15,0));
pointer-events:none;
}
body.touch #controls{display:flex;}
#controls .side{display:flex;gap:12px;align-items:flex-end;pointer-events:none;}
#controls .btn{pointer-events:auto;}
.side{display:flex;gap:12px;align-items:flex-end;}
.btn{
width:62px;height:62px;border-radius:50%;
display:flex;align-items:center;justify-content:center;
font-size:25px;line-height:1;
color:#bfe9f5;
background:rgba(30,70,100,.45);
border:2px solid rgba(126,232,250,.35);
box-shadow:0 0 14px rgba(0,0,0,.4) inset;
transition:transform .05s,background .05s;
touch-action:none;
}
.btn.on{background:rgba(126,232,250,.45);transform:scale(.92);color:#fff;}
.btn.jmp{background:rgba(40,120,90,.45);border-color:rgba(120,255,200,.4);color:#c8ffe6;}
.btn.jmp.on{background:rgba(120,255,200,.45);}
.btn.atk{background:rgba(130,50,70,.45);border-color:rgba(255,140,170,.4);color:#ffd7e0;}
.btn.atk.on{background:rgba(255,140,170,.5);}
.side.right .btn:first-child{width:56px;height:56px;font-size:21px;}
</style>
</head>
<body>
<div id="game">
<div id="stage">
<canvas id="c" width="480" height="270"></canvas>
</div>
<div id="controls">
<div class="side left">
<div class="btn" id="bLeft">◀</div>
<div class="btn" id="bRight">▶</div>
</div>
<div class="side right">
<div class="btn atk" id="bAtk">⚔</div>
<div class="btn jmp" id="bJump">▲</div>
</div>
</div>
</div>
<script>
(() => {
"use strict";
/* ============================================================
CONSTANTS
============================================================ */
const VW = 480, VH = 270;
const GRAVITY = 780;
const JUMP_VEL = -350;
const MAX_FALL = 340;
const RUN_SPEED = 128;
const RUN_ACCEL = 950;
const WATER_SPEED = 74;
const WATER_ACCEL = 430;
const WATER_GRAV = 200;
const WATER_LIFT = 520;
const WATER_JUMP = -340;
/* ============================================================
CANVAS / SCALING
============================================================ */
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = false;
const stage = document.getElementById('stage');
function resize(){
const r = stage.getBoundingClientRect();
if(r.width < 4 || r.height < 4) return;
const s = Math.min(r.width / VW, r.height / VH);
canvas.style.width = Math.max(1, Math.floor(VW * s)) + 'px';
canvas.style.height = Math.max(1, Math.floor(VH * s)) + 'px';
}
window.addEventListener('resize', resize);
window.addEventListener('orientationchange', () => setTimeout(resize, 150));
window.addEventListener('gesturestart', e => e.preventDefault());
/* ============================================================
INPUT
============================================================ */
const input = {
left:0, right:0,
jumpHeld:false, jumpBuf:0,
attackHeld:false, attackBuf:0
};
function setAction(a, down){
if(a === 'left') input.left = down ? 1 : 0;
if(a === 'right') input.right = down ? 1 : 0;
if(a === 'jump'){
input.jumpHeld = down;
if(down) input.jumpBuf = 0.15;
}
if(a === 'attack'){
input.attackHeld = down;
if(down) input.attackBuf = 0.15;
}
}
function clearInput(){
input.left = 0; input.right = 0;
input.jumpHeld = false; input.jumpBuf = 0;
input.attackHeld = false; input.attackBuf = 0;
}
const keyMap = {
ArrowLeft:'left', KeyA:'left',
ArrowRight:'right', KeyD:'right',
Space:'jump', ArrowUp:'jump', KeyW:'jump', KeyZ:'jump',
KeyJ:'attack', KeyX:'attack', KeyK:'attack'
};
window.addEventListener('keydown', e => {
if(e.code === 'Enter' || e.code === 'Escape'){
e.preventDefault();
confirmPress();
return;
}
const a = keyMap[e.code];
if(a){ e.preventDefault(); setAction(a, true); }
}, {passive:false});
window.addEventListener('keyup', e => {
const a = keyMap[e.code];
if(a){ e.preventDefault(); setAction(a, false); }
}, {passive:false});
function bindButton(id, action){
const el = document.getElementById(id);
const down = e => { e.preventDefault(); el.classList.add('on'); setAction(action, true); };
const up = e => { e.preventDefault(); el.classList.remove('on'); setAction(action, false); };
el.addEventListener('pointerdown', down);
el.addEventListener('pointerup', up);
el.addEventListener('pointercancel', up);
el.addEventListener('pointerleave', up);
el.addEventListener('contextmenu', e => e.preventDefault());
}
bindButton('bLeft','left');
bindButton('bRight','right');
bindButton('bJump','jump');
bindButton('bAtk','attack');
const isTouch = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0) ||
window.matchMedia('(pointer: coarse)').matches;
if(isTouch) document.body.classList.add('touch');
requestAnimationFrame(resize);
setTimeout(resize, 60);
canvas.addEventListener('pointerdown', e => {
e.preventDefault();
resumeAudio();
if(state !== 'play') confirmPress();
});
window.addEventListener('pointerdown', resumeAudio, {passive:true});
/* ============================================================
AUDIO
============================================================ */
let actx = null;
function resumeAudio(){
try{
if(!actx){
const AC = window.AudioContext || window.webkitAudioContext;
if(!AC) return;
actx = new AC();
}
if(actx.state === 'suspended') actx.resume();
}catch(e){ actx = null; }
}
function beep(freq, dur, type, vol){
if(!actx) return;
try{
const t = actx.currentTime;
const o = actx.createOscillator();
const g = actx.createGain();
o.type = type || 'square';
o.frequency.setValueAtTime(freq, t);
g.gain.setValueAtTime(vol || 0.05, t);
g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
o.connect(g); g.connect(actx.destination);
o.start(t); o.stop(t + dur + 0.03);
}catch(e){}
}
function slide(f1, f2, dur, type, vol){
if(!actx) return;
try{
const t = actx.currentTime;
const o = actx.createOscillator();
const g = actx.createGain();
o.type = type || 'square';
o.frequency.setValueAtTime(f1, t);
o.frequency.exponentialRampToValueAtTime(Math.max(30, f2), t + dur);
g.gain.setValueAtTime(vol || 0.05, t);
g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
o.connect(g); g.connect(actx.destination);
o.start(t); o.stop(t + dur + 0.03);
}catch(e){}
}
/* ============================================================
HELPERS
============================================================ */
const clamp = (v,a,b) => v < a ? a : (v > b ? b : v);
const rectHit = (a,b) => 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 coinRow(x, y, n, gap){
const a = [];
for(let i = 0; i < n; i++){
a.push({x: x + i*gap, y: y, r: 5, taken:false, t: Math.random()*6});
}
return a;
}
/* ============================================================
LEVELS (platform heights tuned for a ~78px single jump
and ~157px double jump from a ground top of y=230)
============================================================ */
const LEVELS = [
/* ---------------- LEVEL 1 ---------------- */
{
name: 'Saltwind Shore',
hint: 'Tap JUMP again in mid-air to double jump',
w: 1700, h: 270,
spawn: {x: 40, y: 190},
waterLow: 400, waterHigh: 170,
platforms: [
{x:0, y:230, w:400, h:40},
{x:500, y:230, w:380, h:40},
{x:1000, y:230, w:700, h:40},
{x:620, y:170, w:120, h:14, oneway:true},
{x:860, y:140, w:100, h:14, oneway:true}
],
crystals: [
{x:330, y:206, w:14, h:24, type:'raise'},
{x:1300, y:206, w:14, h:24, type:'lower'}
],
enemies: [
{type:'crab', x:560, y:200, w:16, h:16, minX:520, maxX:850, speed:36, dir: 1, hp:2},
{type:'bat', x:1050, y:150, w:16, h:12, minX:1010, maxX:1300, speed:46, dir: 1, baseY:150, hp:1},
{type:'crab', x:1250, y:200, w:16, h:16, minX:1050, maxX:1420, speed:42, dir:-1, hp:2}
],
coins: [
...coinRow(120, 200, 4, 22),
...coinRow(650, 148, 4, 22),
...coinRow(1090, 200, 5, 22),
...coinRow(1500, 200, 3, 22)
],
goal: {x:1620, y:198, w:18, h:32}
},
/* ---------------- LEVEL 2 ---------------- */
{
name: 'Coral Ruins',
hint: 'Raise the tide to swim up to high ledges',
w: 2100, h: 270,
spawn: {x: 40, y: 190},
waterLow: 400, waterHigh: 140,
platforms: [
{x:0, y:230, w:340, h:40},
{x:510, y:230, w:290, h:40},
{x:970, y:230, w:290, h:40},
{x:1430, y:230, w:670, h:40},
{x:400, y:170, w:110, h:14, oneway:true},
{x:700, y:140, w:110, h:14, oneway:true},
{x:1150, y:110, w:110, h:14, oneway:true},
{x:1650, y:140, w:200, h:14, oneway:true}
],
crystals: [
{x:240, y:206, w:14, h:24, type:'raise'},
{x:1030, y:206, w:14, h:24, type:'raise'},
{x:1550, y:206, w:14, h:24, type:'lower'}
],
enemies: [
{type:'crab', x:560, y:200, w:16, h:16, minX:520, maxX:780, speed:40, dir: 1, hp:2},
{type:'bat', x:900, y:170, w:16, h:12, minX:860, maxX:1180, speed:52, dir: 1, baseY:170, hp:1},
{type:'crab', x:1050, y:200, w:16, h:16, minX:990, maxX:1240, speed:44, dir:-1, hp:2},
{type:'bat', x:1500, y:150, w:16, h:12, minX:1460, maxX:1900, speed:56, dir:-1, baseY:150, hp:1},
{type:'crab', x:1750, y:200, w:16, h:16, minX:1480, maxX:2050, speed:48, dir: 1, hp:2}
],
coins: [
...coinRow(80, 200, 5, 22),
...coinRow(420, 148, 4, 22),
...coinRow(720, 118, 4, 22),
...coinRow(1170, 88, 4, 22),
...coinRow(1700, 118, 5, 22)
],
goal: {x:1780, y:108, w:18, h:32}
},
/* ---------------- LEVEL 3 ---------------- */
{
name: 'Drowned City',
hint: 'Strike the Coral Golem, then reach the flag',
w: 1900, h: 270,
spawn: {x: 40, y: 190},
waterLow: 400, waterHigh: 140,
platforms: [
{x:0, y:230, w:1900, h:40},
{x:300, y:170, w:120, h:14, oneway:true},
{x:600, y:140, w:120, h:14, oneway:true},
{x:900, y:170, w:120, h:14, oneway:true},
{x:1200, y:140, w:120, h:14, oneway:true},
{x:1560, y:170, w:220, h:14, oneway:true}
],
crystals: [
{x:180, y:206, w:14, h:24, type:'raise'},
{x:1050, y:206, w:14, h:24, type:'raise'},
{x:1400, y:206, w:14, h:24, type:'lower'}
],
enemies: [
{type:'crab', x:420, y:200, w:16, h:16, minX:220, maxX:700, speed:52, dir: 1, hp:3},
{type:'bat', x:700, y:150, w:16, h:12, minX:620, maxX:980, speed:60, dir: 1, baseY:150, hp:1},
{type:'bat', x:1080, y:100, w:16, h:12, minX:960, maxX:1300, speed:60, dir:-1, baseY:100, hp:1},
{type:'crab', x:1180, y:200, w:16, h:16, minX:960, maxX:1380, speed:56, dir:-1, hp:3},
{type:'golem',x:1450, y:170, w:30, h:36, hp:6, speed:52, dir:-1}
],
coins: [
...coinRow(80, 200, 4, 22),
...coinRow(320, 148, 4, 22),
...coinRow(620, 118, 4, 22),
...coinRow(920, 148, 4, 22),
...coinRow(1220, 118, 4, 22),
...coinRow(1600, 148, 5, 22)
],
goal: {x:1810, y:198, w:18, h:32}
}
];
/* ============================================================
GAME STATE
============================================================ */
let state = 'title';
let stateTimer = 0;
let levelIndex = 0;
let level = null;
let camX = 0;
let time = 0;
let shake = 0;
let hintTimer = 0;
let particles = [];
let totalCoins = 0;
const player = {
x:0, y:0, py:0, w:10, h:14,
vx:0, vy:0,
onGround:false, coyote:0,
airJumps:1,
facing:1,
hp:3, maxHp:3, invuln:0,
attackTimer:0, attackCd:0, stun:0,
coins:0, inWater:false, splashCd:0
};
/* ============================================================
LEVEL LOAD
============================================================ */
function loadLevel(i){
const L = LEVELS[i];
level = {
name: L.name,
hint: L.hint,
w: L.w, h: L.h,
spawn: {x: L.spawn.x, y: L.spawn.y},
waterLow: L.waterLow,
waterHigh: L.waterHigh,
waterY: L.waterLow,
waterTarget: L.waterLow,
platforms: L.platforms.map(p => Object.assign({}, p)),
crystals: L.crystals.map(c => Object.assign({}, c, {flash:0})),
enemies: L.enemies.map(e => Object.assign({}, e, {
t: Math.random()*2,
vy:0, py:0, onGround:false,
hurt:0, dead:false
})),
coins: L.coins.map(c => Object.assign({}, c, {taken:false, t:Math.random()*6})),
goal: Object.assign({}, L.goal)
};
player.x = L.spawn.x;
player.y = L.spawn.y;
player.py = player.y;
player.vx = 0;
player.vy = 0;
player.hp = player.maxHp;
player.invuln = 0;
player.attackTimer = 0;
player.attackCd = 0;
player.stun = 0;
player.facing = 1;
player.inWater = false;
player.splashCd = 0;
player.onGround = false;
player.coyote = 0;
player.airJumps = 1;
camX = 0;
shake = 0;
particles.length = 0;
hintTimer = 4.5;
clearInput();
}
function restartLevel(){
const coins = player.coins;
loadLevel(levelIndex);
player.coins = coins;
state = 'play';
stateTimer = 0;
}
function startGame(){
levelIndex = 0;
player.coins = 0;
totalCoins = 0;
loadLevel(0);
state = 'play';
stateTimer = 0;
resumeAudio();
}
function nextLevel(){
levelIndex++;
if(levelIndex >= LEVELS.length){
state = 'win';
stateTimer = 0;
clearInput();
} else {
loadLevel(levelIndex);
state = 'play';
stateTimer = 0;
}
}
function confirmPress(){
if(state === 'title'){
startGame();
} else if(state === 'win'){
state = 'title';
stateTimer = 0;
loadLevel(0);
}
}
/* ============================================================
PARTICLES
============================================================ */
function burst(x, y, color, count, speed){
for(let i = 0; i < count; i++){
if(particles.length > 220) return;
const a = Math.random() * Math.PI * 2;
const s = speed * (0.3 + Math.random() * 0.9);
particles.push({
x, y,
vx: Math.cos(a) * s,
vy: Math.sin(a) * s - 30,
life: 0.5 + Math.random() * 0.5,
max: 1,
color,
size: 1 + (Math.random() < 0.5 ? 0 : 1)
});
}
}
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.vy += 260 * dt;
p.vx *= 0.98;
p.x += p.vx * dt;
p.y += p.vy * dt;
}
}
/* ============================================================
PHYSICS
============================================================ */
function collideX(o, plats){
for(let i = 0; i < plats.length; i++){
const q = plats[i];
if(q.oneway) continue;
if(!rectHit(o,q)) continue;
if(o.vx > 0) o.x = q.x - o.w;
else if(o.vx < 0) o.x = q.x + q.w;
o.vx = 0;
}
}
function collideY(o, plats){
o.onGround = false;
for(let i = 0; i < plats.length; i++){
const q = plats[i];
if(!rectHit(o,q)) continue;
if(q.oneway){
// only land when falling and previously above the surface
if(o.vy >= 0 && o.py + o.h <= q.y + 5){
o.y = q.y - o.h;
o.vy = 0;
o.onGround = true;
}
} else {
if(o.vy >= 0 && o.py + o.h <= q.y + 8){
o.y = q.y - o.h;
o.vy = 0;
o.onGround = true;
} else if(o.vy < 0 && o.py >= q.y + q.h - 8){
o.y = q.y + q.h;
o.vy = 0;
}
}
}
}
/* ============================================================
PLAYER
============================================================ */
function updatePlayer(dt){
const p = player;
const L = level;
if(p.invuln > 0) p.invuln -= dt;
if(p.stun > 0) p.stun -= dt;
if(p.attackCd > 0) p.attackCd -= dt;
if(p.attackTimer > 0) p.attackTimer -= dt;
if(p.splashCd > 0) p.splashCd -= dt;
/* ---- water detection ---- */
const wasInWater = p.inWater;
p.inWater = (L.waterY < L.h) && ((p.y + p.h * 0.7) > L.waterY);
if(p.inWater && !wasInWater && p.splashCd <= 0){
burst(p.x + p.w/2, L.waterY, '#9fe8ff', 10, 70);
beep(700, 0.08, 'sine', 0.03);
p.splashCd = 0.3;
}
/* ---- attack ---- */
if((input.attackBuf > 0 || input.attackHeld) && p.attackCd <= 0 && p.stun <= 0){
input.attackBuf = 0;
p.attackCd = 0.30;
p.attackTimer = 0.18;
doAttack();
}
/* ---- horizontal ---- */
const dir = (input.right ? 1 : 0) - (input.left ? 1 : 0);
const maxSpd = p.inWater ? WATER_SPEED : RUN_SPEED;
const accel = p.inWater ? WATER_ACCEL : RUN_ACCEL;
if(p.stun <= 0){
if(dir !== 0){
p.vx += dir * accel * dt;
if(Math.abs(p.vx) > maxSpd) p.vx = dir * maxSpd;
p.facing = dir;
} else {
const fr = (p.onGround ? 1500 : 650) * dt;
if(Math.abs(p.vx) <= fr) p.vx = 0;
else p.vx -= Math.sign(p.vx) * fr;
}
} else {
p.vx *= Math.max(0, 1 - 3 * dt);
}
/* ---- vertical ---- */
if(p.inWater){
p.vy += WATER_GRAV * dt;
if(input.jumpHeld) p.vy -= WATER_LIFT * dt;
p.vy = clamp(p.vy, -125, 75);
} else {
p.vy += GRAVITY * dt;
if(p.vy > MAX_FALL) p.vy = MAX_FALL;
// variable jump height
if(!input.jumpHeld && p.vy < 0) p.vy += 1500 * dt;
}
/* ---- coyote time & air jumps ---- */
if(p.onGround){
p.coyote = 0.10;
p.airJumps = 1;
} else if(p.coyote > 0){
p.coyote -= dt;
}
/* ---- jump ---- */
if(input.jumpBuf > 0){
if(p.inWater){
p.vy = WATER_JUMP;
input.jumpBuf = 0;
p.splashCd = 0;
burst(p.x + p.w/2, p.y + p.h, '#bff2ff', 9, 95);
beep(520, 0.10, 'square', 0.04);
} else if(p.onGround || p.coyote > 0){
p.vy = JUMP_VEL;
p.onGround = false;
p.coyote = 0;
p.airJumps = 1;
input.jumpBuf = 0;
beep(430, 0.08, 'square', 0.04);
burst(p.x + p.w/2, p.y + p.h, '#8fd0e0', 5, 55);
} else if(p.airJumps > 0){
p.airJumps--;
p.vy = JUMP_VEL * 0.92;
input.jumpBuf = 0;
beep(620, 0.09, 'triangle', 0.04);
burst(p.x + p.w/2, p.y + p.h, '#bff2ff', 10, 80);
}
}
/* ---- integrate ---- */
p.x += p.vx * dt;
collideX(p, L.platforms);
p.x = clamp(p.x, 0, L.w - p.w);
p.py = p.y;
p.y += p.vy * dt;
collideY(p, L.platforms);
/* ---- out of world ---- */
if(p.y > L.h + 40){
p.hp = 0;
die();
}
}
/* ---------- attack ---------- */
function doAttack(){
const p = player;
beep(660, 0.06, 'square', 0.035);
slide(880, 320, 0.12, 'square', 0.03);
const hb = {
x: p.facing > 0 ? p.x + p.w - 2 : p.x - 16,
y: p.y + 1,
w: 18, h: 13
};
/* enemies */
for(const e of level.enemies){
if(e.dead || e.hurt > 0) continue;
if(!rectHit(hb, e)) continue;
e.hp--;
e.hurt = 0.16;
burst(e.x + e.w/2, e.y + e.h/2, '#ffd166', 9, 100);
shake = Math.max(shake, 0.12);
if(e.hp <= 0){
e.dead = true;
burst(e.x + e.w/2, e.y + e.h/2, '#ff8fa3', 18, 140);
beep(200, 0.18, 'sawtooth', 0.05);
shake = Math.max(shake, 0.28);
} else {
beep(300, 0.07, 'square', 0.04);
}
}
/* tide crystals */
for(const c of level.crystals){
if(!rectHit(hb, c)) continue;
c.flash = 0.35;
const raising = c.type === 'raise';
level.waterTarget = raising ? level.waterHigh : level.waterLow;
burst(c.x + c.w/2, c.y + c.h/2,
raising ? '#7ee8fa' : '#ffb37e', 14, 90);
beep(raising ? 720 : 380, 0.22, 'sine', 0.05);
slide(raising ? 400 : 700, raising ? 900 : 260, 0.4, 'sine', 0.04);
shake = Math.max(shake, 0.2);
}
}
/* ---------- damage / death ---------- */
function damagePlayer(n, fromX){
const p = player;
if(p.invuln > 0 || state !== 'play') return;
p.hp -= n;
p.invuln = 1.3;
p.stun = 0.22;
p.vy = -170;
p.vx = (p.x + p.w/2 < fromX ? -1 : 1) * 150;
shake = Math.max(shake, 0.35);
burst(p.x + p.w/2, p.y + p.h/2, '#ff5470', 12, 130);
beep(150, 0.18, 'sawtooth', 0.06);
if(p.hp <= 0) die();
}
function die(){
if(state !== 'play') return;
state = 'dead';
stateTimer = 0;
clearInput();
burst(player.x + 5, player.y + 7, '#2ec4b6', 26, 160);
burst(player.x + 5, player.y + 7, '#ffffff', 12, 110);
slide(400, 80, 0.6, 'sawtooth', 0.06);
shake = 0.5;
}
function clearLevel(){
if(state !== 'play') return;
state = 'clear';
stateTimer = 0;
clearInput();
burst(level.goal.x + 9, level.goal.y + 10, '#ffe066', 26, 150);
slide(400, 1000, 0.4, 'sine', 0.06);
}
/* ============================================================
ENEMIES
============================================================ */
function updateEnemies(dt){
const L = level;
for(const e of L.enemies){
if(e.dead) continue;
if(e.hurt > 0) e.hurt -= dt;
e.t += dt;
if(e.type === 'crab'){
e.x += e.dir * e.speed * dt;
if(e.x < e.minX){ e.x = e.minX; e.dir = 1; }
if(e.x + e.w > e.maxX){ e.x = e.maxX - e.w; e.dir = -1; }
e.vy += GRAVITY * dt;
if(e.vy > MAX_FALL) e.vy = MAX_FALL;
e.py = e.y;
e.y += e.vy * dt;
collideY(e, L.platforms);
} else if(e.type === 'bat'){
e.x += e.dir * e.speed * dt;
if(e.x < e.minX){ e.x = e.minX; e.dir = 1; }
if(e.x + e.w > e.maxX){ e.x = e.maxX - e.w; e.dir = -1; }
e.y = e.baseY + Math.sin(e.t * 2.4) * 22;
} else if(e.type === 'golem'){
const dx = (player.x + player.w/2) - (e.x + e.w/2);
e.dir = dx >= 0 ? 1 : -1;
e.vy += GRAVITY * dt;
if(e.vy > MAX_FALL) e.vy = MAX_FALL;
if(e.onGround && Math.abs(dx) > 34){
e.x += e.dir * e.speed * dt;
}
if(e.onGround && e.t > 1.6){
e.vy = -330;
e.t = 0;
beep(110, 0.15, 'sawtooth', 0.05);
burst(e.x + e.w/2, e.y + e.h, '#8899aa', 10, 90);
}
e.py = e.y;
e.y += e.vy * dt;
collideY(e, L.platforms);
e.x = clamp(e.x, 0, L.w - e.w);
}
/* contact damage */
if(state === 'play' && player.invuln <= 0 && rectHit(player, e)){
damagePlayer(1, e.x + e.w/2);
}
}
/* cleanup */
for(let i = L.enemies.length - 1; i >= 0; i--){
const e = L.enemies[i];
if(e.dead || (e.type !== 'golem' && e.y > L.h + 80)){
L.enemies.splice(i,1);
}
}
}
/* ============================================================
WORLD
============================================================ */
function updateWorld(dt){
const L = level;
/* tide animation */
if(L.waterY !== L.waterTarget){
const diff = L.waterTarget - L.waterY;
const step = 78 * dt;
if(Math.abs(diff) <= step) L.waterY = L.waterTarget;
else L.waterY += Math.sign(diff) * step;
}
for(const c of L.crystals) if(c.flash > 0) c.flash -= dt;
if(hintTimer > 0) hintTimer -= dt;
/* coins */
for(const c of L.coins){
if(c.taken) continue;
c.t += dt;
const box = {x:c.x - c.r, y:c.y - c.r, w:c.r*2, h:c.r*2};
if(rectHit(player, box)){
c.taken = true;
player.coins++;
totalCoins++;
burst(c.x, c.y, '#ffe066', 8, 80);
beep(900 + Math.random()*200, 0.08, 'square', 0.035);
}
}
updatePlayer(dt);
updateEnemies(dt);
/* goal — locked until the golem is gone */
const bossAlive = L.enemies.some(e => e.type === 'golem');
if(!bossAlive && rectHit(player, L.goal)) clearLevel();
}
/* ============================================================
RENDER
============================================================ */
const skyGrad = (() => {
const g = ctx.createLinearGradient(0, 0, 0, VH);
g.addColorStop(0, '#061024');
g.addColorStop(0.55, '#0e3350');
g.addColorStop(1, '#1b5f7d');
return g;
})();
function drawBackground(){
ctx.fillStyle = skyGrad;
ctx.fillRect(0, 0, VW, VH);
/* moon */
ctx.fillStyle = 'rgba(230,245,255,0.85)';
ctx.fillRect(392, 30, 16, 16);
ctx.fillStyle = '#0e3350';
ctx.fillRect(396, 26, 14, 14);
/* far islands */
ctx.fillStyle = '#0a2337';
const p1 = -camX * 0.15;
const span1 = VW + 480;
for(let i = 0; i < 10; i++){
let bx = (i * 240 + p1) % span1;
if(bx < 0) bx += span1;
bx -= 240;
ctx.beginPath();
ctx.moveTo(bx, 210);
ctx.lineTo(bx + 70, 140);
ctx.lineTo(bx + 140, 210);
ctx.closePath();
ctx.fill();
}
/* near islands */
ctx.fillStyle = '#0d2c44';
const p2 = -camX * 0.35;
const span2 = VW + 640;
for(let i = 0; i < 10; i++){
let bx = (i * 320 + p2) % span2;
if(bx < 0) bx += span2;
bx -= 320;
ctx.beginPath();
ctx.moveTo(bx, 230);
ctx.lineTo(bx + 95, 168);
ctx.lineTo(bx + 190, 230);
ctx.closePath();
ctx.fill();
}
}
function drawWater(){
const L = level;
if(L.waterY >= L.h) return;
const wTop = L.waterY;
ctx.fillStyle = 'rgba(38,132,196,0.42)';
ctx.fillRect(0, wTop, L.w, L.h - wTop);
ctx.fillStyle = 'rgba(12,60,110,0.35)';
const deep = wTop + 60;
if(deep < L.h) ctx.fillRect(0, deep, L.w, L.h - deep);
/* surface line */
ctx.fillStyle = 'rgba(150,240,255,0.75)';
const startX = Math.floor(camX / 6) * 6;
for(let x = startX; x < camX + VW + 6; x += 6){
const yy = wTop + Math.sin(x * 0.06 + time * 3) * 1.6;
ctx.fillRect(x, Math.round(yy), 6, 1);
}
/* shimmer */
ctx.fillStyle = 'rgba(190,250,255,0.16)';
for(let i = 0; i < 14; i++){
const sx = ((i * 137 + time * 20) % (L.w + 200)) - 100;
const sy = wTop + 12 + ((i * 53) % 80);
if(sy < L.h) ctx.fillRect(sx, sy, 10, 1);
}
}
function drawPlatforms(){
const L = level;
for(const p of L.platforms){
if(p.x + p.w < camX - 20 || p.x > camX + VW + 20) continue;
if(p.oneway){
ctx.fillStyle = '#5b7a8c';
ctx.fillRect(p.x, p.y, p.w, p.h);
ctx.fillStyle = '#8fb4c4';
ctx.fillRect(p.x, p.y, p.w, 3);
ctx.fillStyle = 'rgba(0,0,0,0.25)';
ctx.fillRect(p.x, p.y + p.h - 3, p.w, 3);
ctx.fillStyle = '#e08a7a';
for(let i = 6; i < p.w - 6; i += 26){
ctx.fillRect(p.x + i, p.y + 5, 2, 2);
}
} else {
ctx.fillStyle = '#3d5566';
ctx.fillRect(p.x, p.y, p.w, p.h);
ctx.fillStyle = '#7a9c5a';
ctx.fillRect(p.x, p.y, p.w, 4);
ctx.fillStyle = '#5d7d42';
ctx.fillRect(p.x, p.y + 4, p.w, 2);
ctx.fillStyle = 'rgba(0,0,0,0.22)';
for(let i = 3; i < p.w - 8; i += 16){
ctx.fillRect(p.x + i, p.y + 9, 5, 3);
}
}
}
}
function drawCrystals(){
for(const c of level.crystals){
if(c.x + c.w < camX - 24 || c.x > camX + VW + 24) continue;
const cx = c.x + c.w/2;
const cy = c.y + c.h/2;
const pulse = 0.6 + Math.sin(time * 4 + c.x) * 0.4;
const raise = c.type === 'raise';
const base = raise ? '#2ea8d8' : '#e08a4a';
const bright = raise ? '#9ff0ff' : '#ffd0a0';
const flash = c.flash > 0;
ctx.fillStyle = raise
? 'rgba(126,232,250,' + (0.10 + pulse * 0.14) + ')'
: 'rgba(255,180,120,' + (0.10 + pulse * 0.14) + ')';
ctx.fillRect(cx - 16, cy - 18, 32, 36);
ctx.fillStyle = '#2b3f52';
ctx.fillRect(c.x - 2, c.y + c.h - 4, c.w + 4, 5);
ctx.fillStyle = flash ? '#ffffff' : base;
for(let i = 0; i < 7; i++){
const ww = (i < 4) ? (i + 1) * 1.6 : (7 - i) * 1.6;
ctx.fillRect(Math.round(cx - ww/2), Math.round(c.y + i * 3.2),
Math.round(ww), 4);
}
ctx.fillStyle = flash ? '#ffffff' : bright;
ctx.fillRect(Math.round(cx - 1), Math.round(c.y + 5), 2, 6);
if(raise){
ctx.fillRect(cx - 1, c.y - 10, 2, 6);
ctx.fillRect(cx - 3, c.y - 7, 6, 2);
} else {
ctx.fillRect(cx - 1, c.y - 4, 2, 6);
ctx.fillRect(cx - 3, c.y - 4, 6, 2);
}
}
}
function drawCoins(){
for(const c of level.coins){
if(c.taken) continue;
if(c.x < camX - 20 || c.x > camX + VW + 20) continue;
const bob = Math.sin(c.t * 3) * 2;
const y = c.y + bob;
const w = Math.abs(Math.cos(c.t * 2.5)) * 4 + 1;
ctx.fillStyle = '#e8b23a';
ctx.fillRect(Math.round(c.x - w), Math.round(y - 5), Math.round(w*2), 10);
ctx.fillStyle = '#ffe98a';
ctx.fillRect(Math.round(c.x - w), Math.round(y - 5), Math.round(w*2), 3);
ctx.fillStyle = '#a9772a';
ctx.fillRect(Math.round(c.x - 1), Math.round(y - 2), 2, 4);
}
}
function drawEnemies(){
for(const e of level.enemies){
if(e.x + e.w < camX - 30 || e.x > camX + VW + 30) continue;
const x = Math.round(e.x);
const y = Math.round(e.y);
if(e.hurt > 0) ctx.globalAlpha = 0.55;
if(e.type === 'crab'){
ctx.fillStyle = 'rgba(0,0,0,0.25)';
ctx.fillRect(x + 1, y + e.h - 1, e.w - 2, 2);
const lg = Math.sin(e.t * 14) * 1.5;
ctx.fillStyle = '#9c3b22';
ctx.fillRect(x + 1, y + 12 + lg, 3, 4);
ctx.fillRect(x + 12, y + 12 - lg, 3, 4);
ctx.fillStyle = '#d9532f';
ctx.fillRect(x + 1, y + 6, 14, 8);
ctx.fillStyle = '#ef7a4d';
ctx.fillRect(x + 2, y + 5, 12, 4);
ctx.fillStyle = '#b8431f';
ctx.fillRect(x - 3, y + 7, 4, 5);
ctx.fillRect(x + 15, y + 7, 4, 5);
ctx.fillStyle = '#ffffff';
ctx.fillRect(x + 4, y + 3, 3, 3);
ctx.fillRect(x + 9, y + 3, 3, 3);
ctx.fillStyle = '#1a1a1a';
ctx.fillRect(x + 5, y + 4, 1, 2);
ctx.fillRect(x + 10, y + 4, 1, 2);
} else if(e.type === 'bat'){
const wing = Math.sin(e.t * 16) * 4;
ctx.fillStyle = '#6c4bd6';
ctx.fillRect(x - 3, y + 2 + wing, 5, 3);
ctx.fillRect(x + 14, y + 2 - wing, 5, 3);
ctx.fillStyle = '#8a63ff';
ctx.fillRect(x + 3, y + 2, 10, 8);
ctx.fillStyle = '#5a3cb0';
ctx.fillRect(x + 3, y + 8, 10, 2);
ctx.fillRect(x + 3, y, 2, 3);
ctx.fillRect(x + 11, y, 2, 3);
ctx.fillStyle = '#ff4d6d';
ctx.fillRect(x + 5, y + 4, 2, 2);
ctx.fillRect(x + 9, y + 4, 2, 2);
} else if(e.type === 'golem'){
ctx.fillStyle = 'rgba(0,0,0,0.3)';
ctx.fillRect(x + 2, y + e.h - 2, e.w - 4, 3);
ctx.fillStyle = '#4c5f70';
ctx.fillRect(x + 4, y + 24, 8, 12);
ctx.fillRect(x + 18, y + 24, 8, 12);
ctx.fillStyle = '#63798c';
ctx.fillRect(x + 2, y + 8, e.w - 4, 20);
ctx.fillStyle = '#7d94a8';
ctx.fillRect(x + 4, y + 10, e.w - 8, 6);
ctx.fillStyle = '#526678';
ctx.fillRect(x - 4, y + 12, 6, 14);
ctx.fillRect(x + e.w - 2, y + 12, 6, 14);
ctx.fillStyle = '#6e8598';
ctx.fillRect(x + 6, y, e.w - 12, 9);
const coreGlow = 0.6 + Math.sin(time * 6) * 0.4;
ctx.fillStyle = 'rgba(255,90,120,' + coreGlow + ')';
ctx.fillRect(x + 10, y + 15, 10, 8);
ctx.fillStyle = '#ffe0e6';
ctx.fillRect(x + 13, y + 17, 4, 4);
ctx.fillStyle = '#ff6b8a';
ctx.fillRect(x + 8, y + 3, 4, 3);
ctx.fillRect(x + 18, y + 3, 4, 3);
}
ctx.globalAlpha = 1;
}
}
function drawPlayer(){
const p = player;
if(p.invuln > 0 && Math.floor(time * 22) % 2 === 0) return;
const x = Math.round(p.x);
const y = Math.round(p.y);
const f = p.facing;
ctx.fillStyle = 'rgba(0,0,0,0.25)';
ctx.fillRect(x + 1, y + p.h - 1, p.w - 2, 2);
const walking = p.onGround && Math.abs(p.vx) > 12;
const phase = walking ? Math.floor(time * 12) % 4 : 0;
const legOff = [0, 1, 0, -1][phase];
ctx.fillStyle = '#24384f';
ctx.fillRect(x + 1, y + 10, 3, 4 + legOff);
ctx.fillRect(x + 6, y + 10, 3, 4 - legOff);
ctx.fillStyle = '#16222f';
ctx.fillRect(x + 1, y + 13 + legOff, 3, 1);
ctx.fillRect(x + 6, y + 13 - legOff, 3, 1);
ctx.fillStyle = '#2ec4b6';
ctx.fillRect(x, y + 5, 10, 6);
ctx.fillStyle = '#1d8f86';
ctx.fillRect(x, y + 9, 10, 2);
ctx.fillStyle = '#d9a441';
ctx.fillRect(x, y + 9, 10, 1);
ctx.fillStyle = '#ffd9a0';
ctx.fillRect(x + 1, y, 8, 6);
ctx.fillStyle = '#1b4965';
ctx.fillRect(x, y - 2, 10, 3);
ctx.fillRect(x - 1, y + 1, 12, 1);
ctx.fillStyle = '#12263a';
ctx.fillRect(x + (f > 0 ? 6 : 2), y + 2, 2, 2);
if(p.attackTimer > 0){
const t = 1 - (p.attackTimer / 0.18);
const sw = 17;
const sx = f > 0 ? x + 7 : x - sw + 3;
const swing = Math.sin(t * Math.PI) * 8;
const sy = y + 5 - swing;
ctx.fillStyle = 'rgba(200,250,255,0.35)';
ctx.fillRect(sx, sy, sw, 6);
ctx.fillStyle = '#eaf7fa';
ctx.fillRect(sx, sy + 2, sw, 3);
ctx.fillStyle = '#8fd4e0';
ctx.fillRect(sx, sy + 5, sw, 1);
}
}
function drawGoal(){
const g = level.goal;
const x = Math.round(g.x);
const y = Math.round(g.y);
const bossAlive = level.enemies.some(e => e.type === 'golem');
ctx.fillStyle = '#8b6b4a';
ctx.fillRect(x + 8, y, 3, g.h);
ctx.fillStyle = '#5d7d42';
ctx.fillRect(x + 2, y + g.h - 4, 15, 4);
const wave = Math.sin(time * 5) * 3;
ctx.fillStyle = bossAlive ? '#7a3b4a' : '#ffd166';
ctx.beginPath();
ctx.moveTo(x + 11, y + 2);
ctx.lineTo(x + 33, y + 7 + wave);
ctx.lineTo(x + 33, y + 15 - wave);
ctx.lineTo(x + 11, y + 18);
ctx.closePath();
ctx.fill();
if(!bossAlive){
ctx.fillStyle = 'rgba(255,230,120,' + (0.10 + Math.sin(time*4)*0.07) + ')';
ctx.fillRect(x - 8, y - 8, 46, g.h + 16);
}
}
function drawParticles(){
for(const p of particles){
ctx.globalAlpha = clamp(p.life / p.max, 0, 1);
ctx.fillStyle = p.color;
ctx.fillRect(Math.round(p.x), Math.round(p.y), p.size, p.size);
}
ctx.globalAlpha = 1;
}
/* ---------- HUD ---------- */
function drawHeart(cx, cy, filled){
ctx.fillStyle = filled ? '#ff5470' : 'rgba(255,255,255,0.14)';
ctx.fillRect(cx + 1, cy, 2, 1);
ctx.fillRect(cx + 4, cy, 2, 1);
ctx.fillRect(cx, cy + 1, 7, 3);
ctx.fillRect(cx + 1, cy + 4, 5, 1);
ctx.fillRect(cx + 2, cy + 5, 3, 1);
ctx.fillRect(cx + 3, cy + 6, 1, 1);
}
function drawHUD(){
ctx.textBaseline = 'top';
for(let i = 0; i < player.maxHp; i++){
drawHeart(8 + i * 11, 8, i < player.hp);
}
ctx.fillStyle = '#e8b23a';
ctx.fillRect(8, 22, 6, 6);
ctx.fillStyle = '#ffe98a';
ctx.fillRect(8, 22, 6, 2);
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 9px "Courier New", monospace';
ctx.textAlign = 'left';
ctx.fillText('x' + player.coins, 18, 22);
ctx.textAlign = 'center';
ctx.fillStyle = 'rgba(255,255,255,0.85)';
ctx.fillText('LV ' + (levelIndex + 1) + ' ' + level.name, VW/2, 8);
/* tide meter */
const mx = VW - 60, my = 8;
const range = level.waterLow - level.waterHigh;
const t = range > 0
? clamp((level.waterLow - level.waterY) / range, 0, 1)
: 0;
ctx.fillStyle = 'rgba(0,0,0,0.35)';
ctx.fillRect(mx, my, 52, 8);
ctx.fillStyle = 'rgba(126,232,250,0.8)';
ctx.fillRect(mx + 1, my + 1, Math.round(50 * t), 6);
ctx.strokeStyle = 'rgba(255,255,255,0.35)';
ctx.lineWidth = 1;
ctx.strokeRect(mx + 0.5, my + 0.5, 51, 7);
ctx.textAlign = 'right';
ctx.fillStyle = 'rgba(190,235,250,0.75)';
ctx.font = 'bold 7px "Courier New", monospace';
ctx.fillText('TIDE', mx - 4, my + 1);
/* boss bar */
const boss = level.enemies.find(e => e.type === 'golem');
if(boss){
const bw = 140;
const bx = (VW - bw) / 2;
const by = 24;
ctx.fillStyle = 'rgba(0,0,0,0.45)';
ctx.fillRect(bx - 1, by - 1, bw + 2, 9);
ctx.fillStyle = '#ff5470';
ctx.fillRect(bx, by, Math.round(bw * (boss.hp / 6)), 7);
ctx.fillStyle = 'rgba(255,255,255,0.85)';
ctx.font = 'bold 8px "Courier New", monospace';
ctx.textAlign = 'center';
ctx.fillText('CORAL GOLEM', VW/2, by + 12);
}
/* level hint */
if(hintTimer > 0 && state === 'play'){
const a = clamp(hintTimer, 0, 1);
ctx.globalAlpha = a;
ctx.textAlign = 'center';
ctx.font = 'bold 8px "Courier New", monospace';
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(VW/2 - 118, VH - 30, 236, 15);
ctx.fillStyle = '#bff2ff';
ctx.fillText(level.hint, VW/2, VH - 26);
ctx.globalAlpha = 1;
}
}
/* ---------- Overlays ---------- */
function drawOverlay(){
if(state === 'play') return;
ctx.fillStyle = 'rgba(4,10,20,0.72)';
ctx.fillRect(0, 0, VW, VH);
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
if(state === 'title'){
const bob = Math.sin(time * 2) * 3;
ctx.fillStyle = '#7ee8fa';
ctx.font = 'bold 26px "Courier New", monospace';
ctx.fillText('TIDEBREAKER', VW/2, 74 + bob);
ctx.fillStyle = '#ffe066';
ctx.font = 'bold 12px "Courier New", monospace';
ctx.fillText('PIXEL DEPTHS', VW/2, 96 + bob);
ctx.fillStyle = 'rgba(200,235,255,0.85)';
ctx.font = 'bold 9px "Courier New", monospace';
ctx.fillText('◀ ▶ move ▲ jump (x2) ⚔ attack', VW/2, 142);
ctx.fillText('Smash tide crystals to raise & lower the sea', VW/2, 158);
ctx.fillText('Survive three shores and free the ocean', VW/2, 174);
if(Math.sin(time * 4) > -0.2){
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 11px "Courier New", monospace';
ctx.fillText('TAP or PRESS ENTER', VW/2, 214);
}
}
if(state === 'dead'){
ctx.fillStyle = '#ff5470';
ctx.font = 'bold 22px "Courier New", monospace';
ctx.fillText('SWEPT AWAY', VW/2, VH/2 - 12);
ctx.fillStyle = 'rgba(220,240,255,0.8)';
ctx.font = 'bold 10px "Courier New", monospace';
ctx.fillText('the tide reclaims you...', VW/2, VH/2 + 14);
}
if(state === 'clear'){
ctx.fillStyle = '#ffe066';
ctx.font = 'bold 20px "Courier New", monospace';
ctx.fillText('TIDE TURNED!', VW/2, VH/2 - 20);
ctx.fillStyle = 'rgba(220,240,255,0.9)';
ctx.font = 'bold 10px "Courier New", monospace';
ctx.fillText(level.name + ' restored', VW/2, VH/2 + 2);
ctx.fillText('coins: ' + player.coins, VW/2, VH/2 + 18);
}
if(state === 'win'){
ctx.fillStyle = '#7ee8fa';
ctx.font = 'bold 22px "Courier New", monospace';
ctx.fillText('THE SEA IS FREE', VW/2, 86);
ctx.fillStyle = '#ffe066';
ctx.font = 'bold 11px "Courier New", monospace';
ctx.fillText('You broke the Quiet.', VW/2, 116);
ctx.fillStyle = 'rgba(220,240,255,0.9)';
ctx.font = 'bold 10px "Courier New", monospace';
ctx.fillText('Total coins: ' + totalCoins, VW/2, 146);
if(Math.sin(time * 4) > -0.2){
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 10px "Courier New", monospace';
ctx.fillText('tap to return to title', VW/2, 200);
}
}
}
/* ============================================================
RENDER
============================================================ */
function render(){
let sx = 0, sy = 0;
if(shake > 0){
sx = (Math.random() - 0.5) * shake * 8;
sy = (Math.random() - 0.5) * shake * 8;
}
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, VW, VH);
if(!level){
drawBackground();
drawOverlay();
return;
}
drawBackground();
ctx.save();
ctx.translate(Math.round(-camX + sx), Math.round(sy));
drawWater();
drawPlatforms();
drawCoins();
drawCrystals();
drawGoal();
drawEnemies();
drawPlayer();
drawParticles();
ctx.restore();
ctx.setTransform(1, 0, 0, 1, 0, 0);
if(state === 'play' || state === 'dead' || state === 'clear'){
drawHUD();
}
drawOverlay();
}
/* ============================================================
MAIN LOOP
============================================================ */
let last = performance.now();
function frame(now){
let dt = (now - last) / 1000;
last = now;
if(!isFinite(dt) || dt < 0) dt = 0;
if(dt > 0.033) dt = 0.033; // clamp for stable physics
time += dt;
if(shake > 0) shake = Math.max(0, shake - dt * 2.4);
input.jumpBuf = Math.max(0, input.jumpBuf - dt);
input.attackBuf = Math.max(0, input.attackBuf - dt);
if(state === 'play'){
updateWorld(dt);
updateParticles(dt);
const target = clamp(
player.x + player.w/2 - VW/2,
0,
Math.max(0, level.w - VW)
);
camX += (target - camX) * Math.min(1, dt * 9);
if(Math.abs(target - camX) < 0.4) camX = target;
} else if(state === 'dead'){
stateTimer += dt;
updateParticles(dt);
if(stateTimer > 1.4) restartLevel();
} else if(state === 'clear'){
stateTimer += dt;
updateParticles(dt);
if(stateTimer > 1.8) nextLevel();
}
render();
requestAnimationFrame(frame);
}
/* ============================================================
BOOT
============================================================ */
loadLevel(0);
state = 'title';
resize();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>Game Source: Tidebreaker: Pixel Depths
Creator: LaserBuilder68
Libraries: none
Complexity: complex (1459 lines, 40.7 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: tidebreaker-pixel-depths-laserbuilder68" to link back to the original. Then publish at arcadelab.ai/publish.