Skip to content

Commit

Permalink
feat: Initial support for multiple scenes and switching players
Browse files Browse the repository at this point in the history
Support for player winning or dying, which reloads the game scene, and
loads a "Game Over" scene when his lives run out. We pass game options
constantly to the scene, which means we now instantiate the players in
the start scene.

This also necessitates improvements to Peter:
- Tracking the player's control configs
- Handling level wins
  • Loading branch information
joshuacurtiss committed Jun 4, 2024
1 parent e6264e9 commit 7e5292a
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 40 deletions.
Binary file added public/sprites/title.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { k } from './kaboom';
import GameOverScene from './scenes/GameOverScene';
import GameScene from './scenes/GameScene';
import StartScene from './scenes/StartScene';

Expand Down Expand Up @@ -47,6 +48,7 @@ loadSprite("peter", "peter.png", {
dead: { from: 17, to: 18, loop: true, speed: 8 }
},
});
loadSprite("title", "title.png");
loadSprite("floor", "floor.png", { sliceX: 2 });
loadSprite("floor-stair-blue", "floor-stair-blue.png", { sliceX: 2 });
loadSprite("floor-stair-green", "floor-stair-green.png", { sliceX: 2 });
Expand All @@ -60,4 +62,5 @@ loadSprite("stair-green", "stair-green.png", { sliceX: 2 });

scene("start", StartScene);
scene("game", GameScene);
go("game");
scene("gameover", GameOverScene);
go("start");
85 changes: 63 additions & 22 deletions src/objects/Peter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
AreaComp,
Comp,
GameObj,
GamepadButton,
Key,
PosComp,
SpriteComp,
Vec2,
Expand All @@ -28,16 +30,38 @@ const {
z,
} = k;

type DieCallbackFn = (player: PeterComp)=>void;
type CallbackFn = (player: PeterComp)=>void;

export interface PeterControls {
keyboard: {
left: Key;
right: Key;
up: Key;
down: Key;
action: Key;
};
gamepad: {
left: GamepadButton;
right: GamepadButton;
up: GamepadButton;
down: GamepadButton;
action: GamepadButton;
}
};

export interface PeterComp extends Comp {
controls: PeterControls;
isInitialized: boolean;
isFrozen: boolean;
isAlive: boolean;
level: number;
lives: number;
freeze: Function;
action: Function;
die: Function;
get lives(): number;
onDie: (fn: DieCallbackFn) => void;
win: Function;
onDie: (fn: CallbackFn) => void;
onWin: (fn: CallbackFn) => void;
setAnim: (dir: Vec2) => void;
}

Expand Down Expand Up @@ -80,30 +104,49 @@ export function addPeter(options: Partial<PeterCompOpt> = {}): PeterObj {

export function peter(options: Partial<PeterCompOpt> = {}): PeterComp {
const opt = Object.assign({}, PeterCompOptDefaults, options);
const dieCallbacks: DieCallbackFn[] = [];
let lives = 4;
const dieCallbacks: CallbackFn[] = [];
const winCallbacks: CallbackFn[] = [];
return {
id: "peter",
require: ["area", "sprite", "can-salt", "can-walk"],
controls: {
keyboard: {
action: "space",
left: "left",
right: "right",
up: "up",
down: "down",
},
gamepad: {
action: "south",
left: "dpad-left",
right: "dpad-right",
up: "dpad-up",
down: "dpad-down",
},
},
isFrozen: false,
isInitialized: false,
isAlive: false,
level: 0,
lives: 4,
add() {
onKeyPress(key=>{
if (key==='space') {
this.throwSalt();
}
});
this.onCollide("enemy", enemy=>{
if (enemy.isStunned) return;
this.die();
});
this.onDirChange(this.setAnim);
this.setObjects(opt.walkableObjects);
},
action() {
this.throwSalt();
},
onDie(fn) {
dieCallbacks.push(fn);
},
onWin(fn) {
winCallbacks.push(fn);
},
setAnim(newdir) {
let anim = 'idle';
let flipX = newdir.x>0;
Expand All @@ -113,24 +156,22 @@ export function peter(options: Partial<PeterCompOpt> = {}): PeterComp {
if (this.curAnim() !== anim) this.play(anim);
this.flipX = flipX;
},
update() {
if (this.isFrozen || !this.isAlive) return;
let dir = vec2(0);
if (isKeyDown("left")) dir = vec2(-1, 0);
else if (isKeyDown("right")) dir = vec2(1, 0);
else if (isKeyDown("up")) dir = vec2(0, -1);
else if (isKeyDown("down")) dir = vec2(0, 1);
this.setIntendedDir(dir);
},
freeze() {
this.isFrozen = true;
},
get lives() {
return lives;
async win() {
this.freeze();
this.level+=1;
this.stop();
for (let i=0 ; i<10 ; i+=1) {
this.play(i % 2 ? 'celebrate' : 'idle');
await wait(0.4);
}
winCallbacks.forEach(fn=>fn(this));
},
async die() {
this.isAlive = false;
lives-=1;
this.lives-=1;
this.stop();
this.frame = 14;
await wait(1);
Expand Down
24 changes: 24 additions & 0 deletions src/scenes/GameOverScene.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { k, BURGERTIME_BLUE } from '../kaboom';
import { GameSceneOpt } from './GameScene';

const {
add,
anchor,
color,
go,
pos,
text,
vec2,
wait,
} = k;

export default function(deadPlayer: number, opt: GameSceneOpt) {
opt.players.forEach(p=>p.pos = vec2(-20));
add([
text(`Player ${deadPlayer+1} Game Over`, { size: 10 }),
pos(k.width()/2, k.height()/2),
color(BURGERTIME_BLUE),
anchor('center'),
]);
wait(4, ()=>go(opt.players.some(p=>p.lives>=0) ? 'game' : 'start', opt));
};
103 changes: 96 additions & 7 deletions src/scenes/GameScene.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
import { LevelOpt } from 'kaboom';
import { k } from '../kaboom';
import { k, BURGERTIME_BLUE } from '../kaboom';
import { waitSpawnPowerup } from '../objects/Powerup';
import { addEnemy } from '../objects/Enemy';
import { addPeter } from '../objects/Peter';
import { PeterObj } from '../objects/Peter';
import { WalkableObj } from '../abilities/Walk';
import LEVELS from '../levels.json';

const {
add,
addLevel,
anchor,
area,
color,
fixed,
go,
isKeyDown,
onKeyPress,
pos,
rect,
sprite,
text,
onUpdate,
vec2,
wait,
z,
} = k;

Expand Down Expand Up @@ -62,11 +73,25 @@ const levelConf: LevelOpt = {
},
};

export default function(levelNumber = 0) {
export interface GameSceneOpt {
currentPlayer: number;
players: PeterObj[]
}

const GameSceneOptDefaults: GameSceneOpt = {
currentPlayer: 0,
players: [],
};

export default function(options: Partial<GameSceneOpt>) {
const opt = Object.assign({}, GameSceneOptDefaults, options);
const player = opt.players[opt.currentPlayer];

// UI Setup
const ui = add([fixed(), z(100)])

// Level setup
const levelNumber = player.level<LEVELS.length ? player.level : 0;
const level = addLevel(LEVELS[levelNumber], levelConf);
const stairs = level.get('stair') as WalkableObj[];
const floors = level.get('floor') as WalkableObj[];
Expand Down Expand Up @@ -95,12 +120,76 @@ export default function(levelNumber = 0) {
stair.use(sprite(spriteName, { frame }));
});

// Player setup
addPeter({ pos: vec2(128, 173), walkableObjects: { floors, stairs, stairtops }});
// Powerups
waitSpawnPowerup();

// Enemy Setup
addEnemy({ type: 'hotdog', pos: vec2(160, 173) });

// Powerups
waitSpawnPowerup();
// Next Scene management (when player dies or wins)
function goNextScene(action: 'win' | 'die') {
let { currentPlayer } = opt;
const { players } = opt,
deadPlayer = currentPlayer,
player = players[currentPlayer],
someoneIsAlive = players.some(p=>p.lives>=0),
scene = player.lives<0 ? 'gameover' : 'game';
if (action==='die' && someoneIsAlive) {
do {
if (++currentPlayer>=players.length) currentPlayer=0;
} while (someoneIsAlive && players[currentPlayer].lives<0);
}
if (scene==='gameover') wait(3, ()=>go(scene, deadPlayer, { ...opt, currentPlayer }));
else wait(3, ()=>go(scene, { ...opt, currentPlayer }));
}

// Player setup
opt.players.forEach(p=>p.pos = vec2(-20));
player.level = levelNumber;
player.pos = vec2(128, 173);
player.setObjects({ floors, stairs, stairtops });
player.setAnim(vec2(0));
player.isAlive = true;
player.isFrozen = true;
if (!player.isInitialized) {
player.onDie(()=>goNextScene('die'));
player.onWin(()=>goNextScene('win'));
}

// Controls
onKeyPress(player.controls.keyboard.action, ()=>player.action());
onUpdate('player', p=>{
if (p.isFrozen || !p.isAlive) return;
const { up, down, left, right } = p.controls.keyboard;
let dir = vec2(0);
if (isKeyDown(left)) dir = vec2(-1, 0);
else if (isKeyDown(right)) dir = vec2(1, 0);
else if (isKeyDown(up)) dir = vec2(0, -1);
else if (isKeyDown(down)) dir = vec2(0, 1);
p.setIntendedDir(dir);
});

// "Player Ready" message and music pause
const dlg = add([
rect(k.width(), k.height()),
pos(k.width()/2, k.height()/2),
color(0, 0, 0),
anchor('center'),
area(),
z(9999),
]);
dlg.add([
text(`Player ${opt.currentPlayer+1} ready!`, { size: 10 }),
color(BURGERTIME_BLUE),
anchor('center'),
]);
wait(5, ()=>{
dlg.destroy();
wait(player.isInitialized ? 0.5 : 5, ()=>player.isFrozen = false);
if (!player.isInitialized) {
// TODO: Play first-time music
player.isInitialized = true;
}
})

}
27 changes: 17 additions & 10 deletions src/scenes/StartScene.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
import { k } from '../kaboom';
import { addMenu } from '../objects/Menu';
import { addMenuButton } from '../objects/MenuButton';
import { addPeter } from '../objects/Peter';

const {
add,
anchor,
color,
go,
onKeyRelease,
pos,
text,
sprite,
vec2,
} = k;

export default function() {
add([
text("Press enter to start", { size: 12 }),
pos(vec2(128, 120)),
anchor("center"),
color(255, 255, 255),
sprite('title'),
anchor('center'),
pos(vec2(128, 64)),
])
addMenu([
addMenuButton({ text: '1 Player', pos: vec2(128, 120), action: ()=>{
const players = [ addPeter() ];
go('game', { players });
} }),
addMenuButton({ text: '2 Players', pos: vec2(128, 145), action: ()=>{
const players = [ addPeter(), addPeter() ];
go('game', { players });
} }),
]);

onKeyRelease("enter", ()=>go("game"));
onKeyRelease("space", ()=>go("game"));
};

0 comments on commit 7e5292a

Please sign in to comment.