import { playMusic, stopMusic } from '../../audio.js'; const PLATFORMS = [ { y: 820, xMin: 50, xMax: 1550 }, // 0 = floor { y: 660, xMin: 50, xMax: 1550 }, // 1 { y: 495, xMin: 50, xMax: 1550 }, // 2 { y: 330, xMin: 50, xMax: 1550 }, // 3 { y: 165, xMin: 50, xMax: 1550 }, // 4 = Kong platform (WIN) ]; // Ladders: bottom connects lower platform, top connects upper platform // Zone: player can grab if within ±20px horizontally of center const LADDERS = [ { x: 1150, fromPlatform: 0, yBottom: 820, yTop: 660 }, // Floor → P1 { x: 380, fromPlatform: 1, yBottom: 660, yTop: 495 }, // P1 → P2 { x: 1050, fromPlatform: 2, yBottom: 495, yTop: 330 }, // P2 → P3 { x: 680, fromPlatform: 3, yBottom: 330, yTop: 165 }, // P3 → Kong (WIN lane) ]; const PLATFORM_H = 15; const PLAYER_W = 28; const PLAYER_H = 40; const BARREL_R = 14; const LADDER_W = 36; const LADDER_GRAB_RANGE = 28; // Initial direction for barrels per platform index const BARREL_DIR = [1, -1, 1, -1, 1]; // index = platformIndex export default class BurliKong extends Phaser.Scene { constructor() { super({ key: 'BurliKong' }); } preload() { this.load.json('bkConfig', 'src/games/burli-kong/config.json'); this.load.spritesheet('burliSprite', 'assets/images/burli-sprite.png', { frameWidth: 135, frameHeight: 135 }); this.load.audio('bkMusic', 'assets/music/06-burli-kong.mp3'); this.load.audio('bkComplete', 'assets/fx/complete.mp3'); this.load.audio('bkJump', 'assets/fx/launch.mp3'); this.load.audio('bkStomp', 'assets/fx/destroy.mp3'); this.load.audio('bkHit', 'assets/fx/short_death.mp3'); this.load.audio('bkBarrel', 'assets/fx/fireball.mp3'); } init(data) { this.lives = data.lives ?? 3; this.level = String(data.level ?? 1); } create() { const all = this.cache.json.get('bkConfig'); this.cfg = all.levels[this.level] ?? all.levels['1']; this.gameActive = true; this.barrelsCleared = 0; this.spawnTimer = 0; this.barrels = []; this.kongArmRaise = 0; // Idle animation state this.kongIdleTimer = 0; this.kongIdleInterval = 3.5; this.kongIsIdling = false; this.kongIdleSeq = 0; // incremented to invalidate stale delayed callbacks this.player = { x: 200, y: PLATFORMS[0].y - PLAYER_H, vx: 0, vy: 0, state: 'ground', // 'ground' | 'jumping' | 'ladder' platformIndex: 0, facingDir: 1, ladderIndex: -1, jumpedOverBarrels: new Set(), }; // Background drawn once at depth 0 const bgGfx = this.add.graphics().setDepth(0); this._drawBackground(bgGfx); // Kong sprite at depth 1, bottom-anchored to platform surface this.kongSprite = this.add.sprite(95, PLATFORMS[4].y - PLATFORM_H, 'burliSprite', 0) .setOrigin(0.5, 1) .setDepth(1); // Sprite 5: static, 20px to the right of Kong this.add.image(250, PLATFORMS[4].y - PLATFORM_H, 'burliSprite', 5) .setOrigin(0.5, 1) .setDepth(1); // Game elements (platforms, ladders, barrels, player) at depth 2 this.mainGfx = this.add.graphics().setDepth(2); this.cursors = this.input.keyboard.createCursorKeys(); this.keyA = this.input.keyboard.addKey('A'); this.keyAPressed = false; this._buildHUD(); playMusic(this, 'bkMusic'); } update(time, delta) { if (!this.gameActive) return; const dt = delta / 1000; this._handleInput(dt); this._spawnBarrels(dt); this._updateBarrels(dt); this._checkCollisions(); this._updateKongSprite(dt); this._draw(); this._updateHUD(); } // ─── Input ──────────────────────────────────────────────────────────────── _handleInput(dt) { const p = this.player; const cfg = this.cfg; // Jump (A key, edge-triggered, ground only) if (this.keyA.isDown && !this.keyAPressed) { this.keyAPressed = true; if (p.state === 'ground') { p.state = 'jumping'; p.vy = -cfg.jumpVelocity; this._playFx('bkJump', 0.5); } } if (!this.keyA.isDown) this.keyAPressed = false; if (p.state === 'ground') { p.vx = 0; if (this.cursors.left.isDown) { p.vx = -cfg.playerSpeed; p.facingDir = -1; } if (this.cursors.right.isDown) { p.vx = cfg.playerSpeed; p.facingDir = 1; } p.x += p.vx * dt; p.x = Phaser.Math.Clamp(p.x, PLATFORMS[p.platformIndex].xMin + PLAYER_W / 2, PLATFORMS[p.platformIndex].xMax - PLAYER_W / 2); // Snap y to current platform surface p.y = PLATFORMS[p.platformIndex].y - PLAYER_H; // Start climbing: up arrow near ladder bottom on current platform if (this.cursors.up.isDown) { const li = this._nearLadderBottom(p); if (li >= 0) { p.state = 'ladder'; p.ladderIndex = li; p.x = LADDERS[li].x; p.vx = 0; } } } else if (p.state === 'jumping') { p.vy += cfg.gravity * dt; p.x += p.vx * dt; p.y += p.vy * dt; p.x = Phaser.Math.Clamp(p.x, 55, 1545); // Land on current platform (only if moving downward and crossing platform top) const plat = PLATFORMS[p.platformIndex]; const platTop = plat.y - PLATFORM_H; if (p.vy > 0 && p.y + PLAYER_H >= platTop) { p.y = plat.y - PLAYER_H; p.vy = 0; p.state = 'ground'; } } else if (p.state === 'ladder') { const ladder = LADDERS[p.ladderIndex]; if (this.cursors.up.isDown) { p.y -= cfg.climbSpeed * dt; } else if (this.cursors.down.isDown) { p.y += cfg.climbSpeed * dt; } // Dismount at top: reached upper platform if (p.y <= ladder.yTop - PLAYER_H) { const newPlatformIndex = ladder.fromPlatform + 1; p.y = PLATFORMS[newPlatformIndex].y - PLAYER_H; p.platformIndex = newPlatformIndex; p.state = 'ground'; p.ladderIndex = -1; // Win: reached Kong platform if (p.platformIndex === 4) { this._endGame(true, 'MADE IT!'); return; } } // Dismount at bottom: dropped below ladder bottom if (p.y + PLAYER_H >= ladder.yBottom) { p.y = PLATFORMS[ladder.fromPlatform].y - PLAYER_H; p.platformIndex = ladder.fromPlatform; p.state = 'ground'; p.ladderIndex = -1; } // Dismount sideways: left/right while on ladder (onto current platform level) if (this.cursors.left.isDown || this.cursors.right.isDown) { // Find which platform the player's midpoint is closest to const midY = p.y + PLAYER_H / 2; let bestPlat = ladder.fromPlatform; let bestDist = Infinity; for (let i = ladder.fromPlatform; i <= ladder.fromPlatform + 1; i++) { const d = Math.abs(PLATFORMS[i].y - PLATFORM_H / 2 - midY); if (d < bestDist) { bestDist = d; bestPlat = i; } } p.platformIndex = bestPlat; p.y = PLATFORMS[bestPlat].y - PLAYER_H; p.state = 'ground'; p.facingDir = this.cursors.left.isDown ? -1 : 1; p.ladderIndex = -1; } } } _nearLadderBottom(p) { for (let i = 0; i < LADDERS.length; i++) { const l = LADDERS[i]; if (l.fromPlatform !== p.platformIndex) continue; if (Math.abs(p.x - l.x) <= LADDER_GRAB_RANGE) return i; } return -1; } // ─── Spawn ──────────────────────────────────────────────────────────────── _spawnBarrels(dt) { this.spawnTimer += dt; if (this.spawnTimer < this.cfg.barrelSpawnInterval) return; if (this.barrels.filter(b => b.alive).length >= this.cfg.maxBarrels) return; this.spawnTimer = 0; this.barrels.push({ x: 200, y: PLATFORMS[4].y - BARREL_R - PLATFORM_H, platformIndex: 4, dir: BARREL_DIR[4], falling: false, vy: 0, alive: true, cleared: false, ladderRollsDone: new Set(), }); this.kongArmRaise = 0.6; this._playFx('bkBarrel', 0.5); } // ─── Barrel update ──────────────────────────────────────────────────────── _updateBarrels(dt) { const cfg = this.cfg; for (const b of this.barrels) { if (!b.alive) continue; if (b.falling) { b.vy += cfg.gravity * dt; b.y += b.vy * dt; // Land on next platform down const nextIdx = b.platformIndex - 1; if (nextIdx < 0) { b.alive = false; continue; } const nextPlat = PLATFORMS[nextIdx]; const landY = nextPlat.y - BARREL_R - PLATFORM_H; if (b.y >= landY) { b.y = landY; b.falling = false; b.vy = 0; b.platformIndex = nextIdx; b.dir = BARREL_DIR[nextIdx]; b.ladderRollsDone = new Set(); } } else { b.x += b.dir * cfg.barrelSpeed * dt; // 50% chance to fall down a ladder when rolling over one for (let li = 0; li < LADDERS.length; li++) { const l = LADDERS[li]; if (l.fromPlatform + 1 !== b.platformIndex) continue; if (b.ladderRollsDone.has(li)) continue; if (Math.abs(b.x - l.x) < 32) { b.ladderRollsDone.add(li); if (Math.random() < 0.5) { b.x = l.x; b.falling = true; b.vy = 0; break; } } } const plat = PLATFORMS[b.platformIndex]; // Fall off edge if (b.dir > 0 && b.x > plat.xMax - BARREL_R) { if (b.platformIndex === 0) { b.alive = false; // off the floor } else { b.falling = true; b.vy = 0; b.x = plat.xMax - BARREL_R; } } else if (b.dir < 0 && b.x < plat.xMin + BARREL_R) { if (b.platformIndex === 0) { b.alive = false; } else { b.falling = true; b.vy = 0; b.x = plat.xMin + BARREL_R; } } } } this.barrels = this.barrels.filter(b => b.alive); if (this.kongArmRaise > 0) this.kongArmRaise -= dt; } // ─── Collision ──────────────────────────────────────────────────────────── _checkCollisions() { if (!this.gameActive) return; const p = this.player; const pLeft = p.x - PLAYER_W / 2; const pRight = p.x + PLAYER_W / 2; const pTop = p.y; const pBottom = p.y + PLAYER_H; const pCenterX = p.x; for (const b of this.barrels) { if (!b.alive || b.falling) continue; // Only check barrels on same or adjacent platform if (Math.abs(b.platformIndex - p.platformIndex) > 0) continue; if (b.platformIndex !== p.platformIndex) continue; const dx = pCenterX - b.x; const dy = (p.y + PLAYER_H / 2) - b.y; const dist = Math.hypot(dx, dy); // Stomp: player falling, feet crossing top of barrel if (p.state === 'jumping' && p.vy > 0 && pBottom >= b.y - BARREL_R && pBottom <= b.y + BARREL_R && Math.abs(dx) < BARREL_R + PLAYER_W * 0.5) { b.alive = false; if (!b.cleared) { b.cleared = true; this.barrelsCleared++; } p.vy = -this.cfg.stompBounce; this._playFx('bkStomp', 0.7); this._checkWin(); continue; } // Jump-over: player jumping, feet above barrel center, laterally overlapping if (p.state === 'jumping' && !b.cleared && pBottom < b.y && Math.abs(dx) < BARREL_R + PLAYER_W * 0.6 && !p.jumpedOverBarrels.has(b)) { p.jumpedOverBarrels.add(b); b.cleared = true; this.barrelsCleared++; this._checkWin(); continue; } // Side hit: AABB vs circle overlap (not a stomp or jump-over already processed) if (dist < BARREL_R + Math.min(PLAYER_W, PLAYER_H) * 0.45) { // Don't penalize if player is clearly above (already scored) if (p.state === 'jumping' && pBottom < b.y - BARREL_R * 0.5) continue; this._endGame(false, "BARREL'D!"); return; } } // Clean up jump-over tracking for dead/cleared barrels for (const b of p.jumpedOverBarrels) { if (!b.alive || b.cleared) continue; // keep cleared ones out naturally } } _checkWin() { if (this.barrelsCleared >= this.cfg.barrelsToPass) { this._endGame(true, 'CLEARED!'); } } // ─── Drawing ────────────────────────────────────────────────────────────── _draw() { const g = this.mainGfx; g.clear(); this._drawPlatforms(g); this._drawLadders(g); this._drawBarrels(g); this._drawPlayer(g); } _drawBackground(g) { g.fillStyle(0x00060f); g.fillRect(0, 0, 1600, 900); g.lineStyle(1, 0x0a1a2a, 0.5); for (let x = 0; x < 1600; x += 80) g.lineBetween(x, 0, x, 900); for (let y = 0; y < 900; y += 60) g.lineBetween(0, y, 1600, y); } // ─── Kong sprite ────────────────────────────────────────────────────────── _updateKongSprite(dt) { if (this.kongArmRaise > 0) { // Throwing — invalidate any in-flight idle sequence this.kongIdleSeq++; this.kongIsIdling = false; this.kongIdleTimer = 0; this.kongSprite.setFrame(1); } else { if (!this.kongIsIdling) { this.kongSprite.setFrame(0); this.kongIdleTimer += dt; if (this.kongIdleTimer >= this.kongIdleInterval) { this._playKongIdleAnimation(); } } } } _playKongIdleAnimation() { this.kongIsIdling = true; this.kongIdleTimer = 0; const seq = ++this.kongIdleSeq; this.kongSprite.setFrame(2); this.time.delayedCall(450, () => { if (!this.gameActive || this.kongIdleSeq !== seq) return; this.kongSprite.setFrame(3); this.time.delayedCall(450, () => { if (!this.gameActive || this.kongIdleSeq !== seq) return; this.kongSprite.setFrame(0); this.kongIsIdling = false; }); }); } _drawPlatforms(g) { for (const plat of PLATFORMS) { const w = plat.xMax - plat.xMin; // Platform body g.fillStyle(0x2255bb); g.fillRect(plat.xMin, plat.y - PLATFORM_H, w, PLATFORM_H); // Bright top edge g.lineStyle(3, 0x44aaff, 1.0); g.lineBetween(plat.xMin, plat.y - PLATFORM_H, plat.xMax, plat.y - PLATFORM_H); } } _drawLadders(g) { for (const l of LADDERS) { const lx = l.x - LADDER_W / 2; const rx = l.x + LADDER_W / 2; g.lineStyle(3, 0xffcc44, 0.9); g.lineBetween(lx, l.yBottom - PLATFORM_H, lx, l.yTop); g.lineBetween(rx, l.yBottom - PLATFORM_H, rx, l.yTop); // Rungs g.lineStyle(2, 0xffcc44, 0.7); const rungCount = Math.floor((l.yBottom - l.yTop) / 20); for (let r = 0; r <= rungCount; r++) { const ry = l.yTop + r * ((l.yBottom - PLATFORM_H - l.yTop) / rungCount); g.lineBetween(lx, ry, rx, ry); } } } _drawBarrels(g) { for (const b of this.barrels) { if (!b.alive) continue; const r = BARREL_R; // Barrel body g.fillStyle(0xaa5500); g.fillCircle(b.x, b.y, r); // Cross-lines g.lineStyle(2, 0xffaa44, 0.9); g.lineBetween(b.x - r + 3, b.y, b.x + r - 3, b.y); g.lineBetween(b.x, b.y - r + 3, b.x, b.y + r - 3); // Outline g.lineStyle(1.5, 0xff8800, 0.7); g.strokeCircle(b.x, b.y, r); } } _drawPlayer(g) { const p = this.player; const x = p.x, y = p.y; // Body g.fillStyle(0xdd4411); g.fillRect(x - PLAYER_W / 2, y, PLAYER_W, PLAYER_H * 0.65); // Head g.fillStyle(0xffaa77); g.fillCircle(x, y - 2, PLAYER_W * 0.42); // Facing direction indicator (small triangle on chest) g.fillStyle(0xffff00); const tx = x + p.facingDir * (PLAYER_W * 0.35); const ty = y + PLAYER_H * 0.3; g.fillTriangle( tx, ty, tx - p.facingDir * 8, ty - 6, tx - p.facingDir * 8, ty + 6 ); // Legs g.fillStyle(0x994400); g.fillRect(x - PLAYER_W / 2, y + PLAYER_H * 0.65, PLAYER_W * 0.4, PLAYER_H * 0.35); g.fillRect(x + PLAYER_W * 0.1, y + PLAYER_H * 0.65, PLAYER_W * 0.4, PLAYER_H * 0.35); } // ─── HUD ────────────────────────────────────────────────────────────────── _buildHUD() { this.livesText = this.add.text(340, 16, '', { fontSize: '26px', fontFamily: 'ArcadeClassic, monospace', color: '#ff4444', stroke: '#000000', strokeThickness: 4, }).setDepth(10); this.barrelsText = this.add.text(800, 16, '', { fontSize: '26px', fontFamily: 'ArcadeClassic, monospace', color: '#ffcc44', stroke: '#000000', strokeThickness: 4, }).setOrigin(0.5, 0).setDepth(10); this.levelText = this.add.text(1580, 16, `LEVEL ${this.level}`, { fontSize: '26px', fontFamily: 'ArcadeClassic, monospace', color: '#00ccff', stroke: '#000000', strokeThickness: 4, }).setOrigin(1, 0).setDepth(10); this.hintText = this.add.text(800, 875, 'A: Jump ↑↓: Climb Reach the top or clear barrels!', { fontSize: '18px', fontFamily: 'ArcadeClassic, monospace', color: '#446688', stroke: '#000000', strokeThickness: 3, }).setOrigin(0.5, 1).setDepth(10); } _updateHUD() { this.livesText.setText('♥ '.repeat(this.lives).trim()); this.barrelsText.setText(`BARRELS ${this.barrelsCleared} / ${this.cfg.barrelsToPass}`); } // ─── End game ───────────────────────────────────────────────────────────── _endGame(success, msg) { if (!this.gameActive) return; this.gameActive = false; if (success) { this._playFx('bkComplete', 0.8); this.kongSprite.setFrame(4); } else { this._playFx('bkHit', 0.7); } const color = success ? '#00ff44' : '#ff2222'; this.add.text(800, 440, msg, { fontSize: '80px', fontFamily: 'ArcadeClassic, monospace', color, stroke: '#000000', strokeThickness: 6, }).setOrigin(0.5).setDepth(20); this.time.delayedCall(2200, () => { this.game.events.emit('miniGameResult', { success }); }); } shutdown() { stopMusic(this); } _playFx(key, volume = 0.6) { const snd = this.sound.add(key, { volume }); snd.once('complete', () => snd.destroy()); snd.play(); } }