feat: add BurliKong game to the game pool

- Import BurliKong scene and add it to the Phaser game configuration
- Register BurliKong in the GameManager's display name map
- Include BurliKong in the game pool within game.json
- Bump cache version for game config to ensure fresh load
This commit is contained in:
Brian Fertig 2026-05-09 19:54:57 -06:00
parent d044b1f319
commit afb95c91d1
7 changed files with 566 additions and 3 deletions

Binary file not shown.

View File

@ -12,5 +12,5 @@
"9": { "gameCount": 5 },
"10": { "gameCount": 5 }
},
"gamePool": ["ColoradoDefense", "CodeBugInvaders", "DotDude", "SmashOut", "Vortex"]
"gamePool": ["ColoradoDefense", "CodeBugInvaders", "DotDude", "SmashOut", "Vortex", "BurliKong"]
}

View File

@ -0,0 +1,547 @@
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.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; // seconds remaining for Kong arm-raise animation
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(), // track barrels already scored as jump-overs
};
this.mainGfx = this.add.graphics();
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._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,
});
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];
}
} else {
b.x += b.dir * cfg.barrelSpeed * dt;
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._drawBackground(g);
this._drawPlatforms(g);
this._drawLadders(g);
this._drawKong(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);
}
_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);
}
}
}
_drawKong(g) {
// Kong sits above Kong platform (platform index 4)
const kx = 95, ky = PLATFORMS[4].y - PLATFORM_H;
const armRaised = this.kongArmRaise > 0;
// Body (trapezoid approximated as rect)
g.fillStyle(0x552200);
g.fillRect(kx - 32, ky - 95, 64, 60);
// Head (circle approximation via fillCircle)
g.fillStyle(0x663300);
g.fillCircle(kx, ky - 105, 28);
// Eyes
g.fillStyle(0xffd700);
g.fillCircle(kx - 9, ky - 110, 5);
g.fillCircle(kx + 9, ky - 110, 5);
g.fillStyle(0x000000);
g.fillCircle(kx - 9, ky - 110, 2);
g.fillCircle(kx + 9, ky - 110, 2);
// Arms
g.fillStyle(0x552200);
if (armRaised) {
// Left arm raised
g.fillRect(kx - 60, ky - 145, 30, 18);
// Right arm normal
g.fillRect(kx + 32, ky - 70, 30, 18);
} else {
g.fillRect(kx - 60, ky - 70, 30, 18);
g.fillRect(kx + 32, ky - 70, 30, 18);
}
// Feet
g.fillStyle(0x442200);
g.fillRect(kx - 28, ky - 37, 22, 18);
g.fillRect(kx + 6, ky - 37, 22, 18);
}
_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(20, 16, '', {
fontSize: '26px', fontFamily: 'monospace',
color: '#ff4444', stroke: '#000000', strokeThickness: 4,
}).setDepth(10);
this.barrelsText = this.add.text(800, 16, '', {
fontSize: '26px', fontFamily: '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: '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: '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);
else this._playFx('bkHit', 0.7);
const color = success ? '#00ff44' : '#ff2222';
this.add.text(800, 440, msg, {
fontSize: '80px', fontFamily: '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();
}
}

View File

@ -0,0 +1,14 @@
{
"levels": {
"1": { "playerSpeed": 240, "jumpVelocity": 520, "gravity": 1000, "climbSpeed": 180, "barrelSpeed": 140, "barrelSpawnInterval": 4.0, "maxBarrels": 3, "barrelsToPass": 5, "stompBounce": 320 },
"2": { "playerSpeed": 240, "jumpVelocity": 520, "gravity": 1000, "climbSpeed": 180, "barrelSpeed": 172, "barrelSpawnInterval": 3.5, "maxBarrels": 3, "barrelsToPass": 7, "stompBounce": 320 },
"3": { "playerSpeed": 240, "jumpVelocity": 520, "gravity": 1000, "climbSpeed": 185, "barrelSpeed": 200, "barrelSpawnInterval": 3.1, "maxBarrels": 4, "barrelsToPass": 9, "stompBounce": 320 },
"4": { "playerSpeed": 245, "jumpVelocity": 520, "gravity": 1000, "climbSpeed": 188, "barrelSpeed": 224, "barrelSpawnInterval": 2.8, "maxBarrels": 4, "barrelsToPass": 11, "stompBounce": 320 },
"5": { "playerSpeed": 245, "jumpVelocity": 520, "gravity": 1000, "climbSpeed": 190, "barrelSpeed": 245, "barrelSpawnInterval": 2.5, "maxBarrels": 5, "barrelsToPass": 14, "stompBounce": 320 },
"6": { "playerSpeed": 250, "jumpVelocity": 520, "gravity": 1000, "climbSpeed": 193, "barrelSpeed": 264, "barrelSpawnInterval": 2.2, "maxBarrels": 5, "barrelsToPass": 17, "stompBounce": 340 },
"7": { "playerSpeed": 250, "jumpVelocity": 520, "gravity": 1000, "climbSpeed": 196, "barrelSpeed": 281, "barrelSpawnInterval": 2.0, "maxBarrels": 6, "barrelsToPass": 20, "stompBounce": 340 },
"8": { "playerSpeed": 255, "jumpVelocity": 520, "gravity": 1000, "climbSpeed": 198, "barrelSpeed": 296, "barrelSpawnInterval": 1.8, "maxBarrels": 6, "barrelsToPass": 23, "stompBounce": 340 },
"9": { "playerSpeed": 255, "jumpVelocity": 520, "gravity": 1000, "climbSpeed": 200, "barrelSpeed": 309, "barrelSpawnInterval": 1.7, "maxBarrels": 7, "barrelsToPass": 26, "stompBounce": 360 },
"10": { "playerSpeed": 260, "jumpVelocity": 520, "gravity": 1000, "climbSpeed": 200, "barrelSpeed": 320, "barrelSpawnInterval": 1.6, "maxBarrels": 7, "barrelsToPass": 30, "stompBounce": 360 }
}
}

View File

@ -7,6 +7,7 @@ import CodeBugInvaders from './games/code-bug-invaders/CodeBugInvaders.js';
import DotDude from './games/dot-dude/DotDude.js';
import SmashOut from './games/smash-out/SmashOut.js';
import Vortex from './games/vortex/Vortex.js';
import BurliKong from './games/burli-kong/BurliKong.js';
new Phaser.Game({
type: Phaser.AUTO,
@ -17,5 +18,5 @@ new Phaser.Game({
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH,
},
scene: [MainMenu, GameManager, ColoradoDefense, CodeBugInvaders, DotDude, SmashOut, Vortex, GameOver, LevelComplete],
scene: [MainMenu, GameManager, ColoradoDefense, CodeBugInvaders, DotDude, SmashOut, Vortex, BurliKong, GameOver, LevelComplete],
});

View File

@ -140,6 +140,7 @@ export default class GameManager extends Phaser.Scene {
DotDude: 'DOT DUDE',
SmashOut: 'SMASH OUT',
Vortex: 'VORTEX',
BurliKong: 'BURLI KONG',
};
return names[key] || key;
}

View File

@ -6,7 +6,7 @@ export default class MainMenu extends Phaser.Scene {
}
preload() {
this.load.json('gameConfig', 'config/game.json?v=2');
this.load.json('gameConfig', 'config/game.json?v=3');
this.load.audio('mainTheme', 'assets/music/00-main-theme.mp3');
this.load.image('logo-title', 'assets/images/logo-title.png');
this.load.image('logo-people', 'assets/images/logo-people.png');