Getting Started with MelonJS
Published on Monday, December 15, 2025
LittleJS Example
And here is a simple example of how to use LittleJS to create a game.
LittleJS example
javascript
import * as littlejsengine from 'https://esm.run/littlejsengine';
const {
setCameraScale,
setCameraPos,
Color,
engineInit,
vec2,
tile,
drawRect,
drawTile,
drawCircle,
drawText,
setCanvasFixedSize,
keyDirection,
keyWasPressed,
mouseWasPressed,
gamepadStick,
Timer,
rand,
time,
Sound,
isTouchDevice,
setTouchGamepadEnable,
setTouchGamepadAnalog,
setTouchGamepadSize,
setTouchGamepadButtonCount,
} = littlejsengine;
// Enable touch gamepad for mobile devices
setTouchGamepadEnable(true);
setTouchGamepadAnalog(false); // Use 8-way dpad instead of analog
setTouchGamepadSize(80);
setTouchGamepadButtonCount(0); // No right-side buttons needed
// =============================================================================
// SOUND EFFECTS (using ZzFX via Sound class for better browser compatibility)
// Sounds are initialized in gameInit after engine starts
// Design sounds at: https://killedbyapixel.github.io/ZzFX/
// =============================================================================
const Sounds = {
coin: null,
combo: null,
dash: null,
bounce: null,
};
function initSounds() {
// Initialize audio primer first
initAudioPrimer();
// Coin collect - brighter chime (about 3/4 pitch of red flag sound)
// biome-ignore lint/suspicious/noSparseArray: ZzFX format
Sounds.coin = new Sound([
2.5,
,
580,
0.01,
0.08,
0.2,
1,
1.8,
,
,
100,
0.03,
,
,
,
,
,
0.65,
0.02,
]);
// Combo collect - higher pitched, more exciting
// biome-ignore lint/suspicious/noSparseArray: ZzFX format
Sounds.combo = new Sound([
2.5,
,
698,
0.02,
0.1,
0.3,
1,
2,
,
,
200,
0.05,
,
,
,
,
,
0.7,
0.02,
]);
// Dash - whoosh sound
// biome-ignore lint/suspicious/noSparseArray: ZzFX format
Sounds.dash = new Sound([
1,
,
200,
0.01,
0.02,
0.1,
4,
0.5,
-10,
,
,
,
,
,
,
,
0.3,
0.01,
]);
// Wall bounce - soft thud
// biome-ignore lint/suspicious/noSparseArray: ZzFX format
Sounds.bounce = new Sound([
0.8,
,
150,
0.01,
0.02,
0.05,
4,
0.3,
,
,
,
,
,
,
,
,
,
0.2,
0.01,
]);
}
// Flag to track if audio has been unlocked
let audioPrimed = false;
let silentPrimer = null;
function initAudioPrimer() {
// Create a very short, nearly silent sound just to unlock audio context
// biome-ignore lint/suspicious/noSparseArray: ZzFX format
silentPrimer = new Sound([0.01, , 1, , 0.001, 0.001]);
}
function primeAudio() {
if (audioPrimed) return;
audioPrimed = true;
// Play nearly inaudible sound to unlock audio context
silentPrimer?.play();
}
// =============================================================================
// LOCAL STORAGE HELPERS (safe for iframes)
// =============================================================================
function saveHighScore(score) {
try {
localStorage.setItem('slalomHighScore', score.toString());
} catch (e) {
// localStorage may be blocked in iframes
}
}
function loadHighScore() {
try {
const saved = localStorage.getItem('slalomHighScore');
return saved ? parseInt(saved, 10) : 0;
} catch (e) {
return 0;
}
}
// =============================================================================
// GAME CONFIGURATION
// =============================================================================
const GameConfig = {
center: 0,
height: 28,
size: 0,
tiles: ['/images/tilemap.png'],
tileSize: 16,
tilePadding: 0.45,
width: 20,
};
// =============================================================================
// GAME STATE
// =============================================================================
const GameState = {
player: null,
coins: [],
particles: [],
decorations: [],
score: 0,
highScore: 0,
spawnTimer: null,
comboTimer: null,
combo: 0,
screenShake: 0,
gameTimer: null,
gameTime: 30, // 30 second rounds
gameOver: false,
gameStarted: false, // Wait for input before starting
flagsSpawned: 0, // Counter for deterministic red flag spawning
};
// =============================================================================
// PLAYER
// =============================================================================
function createPlayer() {
return {
pos: vec2(0, -8),
vel: vec2(0, 0),
size: vec2(2, 2),
baseSpeed: 0.08,
friction: 0.92,
isMoving: false,
squash: 1,
stretch: 1,
facingRight: true,
trail: [],
};
}
function updatePlayer(player) {
// Don't update if game is over
if (GameState.gameOver) return;
// Get input from arrow keys, WASD, or touch gamepad
const keyInput = keyDirection();
const touchInput = gamepadStick(0);
const input = keyInput.length() > 0 ? keyInput : touchInput;
// Prime audio on first input
if (!audioPrimed && (input.x !== 0 || input.y !== 0)) {
primeAudio();
}
// Apply acceleration based on input
player.vel.x += input.x * player.baseSpeed;
player.vel.y += input.y * player.baseSpeed;
// Apply friction
player.vel = player.vel.scale(player.friction);
// Update position
player.pos = player.pos.add(player.vel);
// Track facing direction
if (Math.abs(player.vel.x) > 0.01) {
player.facingRight = player.vel.x > 0;
}
// Add trail particles when moving fast
if (player.vel.length() > 0.15) {
player.trail.push({
pos: player.pos.copy(),
life: 1,
size: 0.8,
});
}
// Update trail
player.trail = player.trail.filter((t) => {
t.life -= 0.08;
t.size *= 0.92;
return t.life > 0;
});
// Bounce off walls with juice!
const halfWidth = GameConfig.width / 2 - 1;
const halfHeight = GameConfig.height / 2 - 2;
if (player.pos.x < -halfWidth) {
player.pos.x = -halfWidth;
player.vel.x *= -0.7;
player.squash = 0.6;
GameState.screenShake = 0.4;
spawnParticles(
vec2(-halfWidth, player.pos.y),
8,
new Color(0.9, 0.95, 1, 1),
);
Sounds.bounce?.play();
// Penalty for hitting wall
GameState.score = Math.max(0, GameState.score - 1);
spawnFloatingText(player.pos, '-1', new Color(1, 0.3, 0.3, 1));
}
if (player.pos.x > halfWidth) {
player.pos.x = halfWidth;
player.vel.x *= -0.7;
player.squash = 0.6;
GameState.screenShake = 0.4;
spawnParticles(
vec2(halfWidth, player.pos.y),
8,
new Color(0.9, 0.95, 1, 1),
);
Sounds.bounce?.play();
// Penalty for hitting wall
GameState.score = Math.max(0, GameState.score - 1);
spawnFloatingText(player.pos, '-1', new Color(1, 0.3, 0.3, 1));
}
if (player.pos.y < -halfHeight) {
player.pos.y = -halfHeight;
player.vel.y *= -0.7;
player.stretch = 0.6;
GameState.screenShake = 0.3;
spawnParticles(
vec2(player.pos.x, -halfHeight),
8,
new Color(0.9, 0.95, 1, 1),
);
Sounds.bounce?.play();
// Penalty for hitting wall
GameState.score = Math.max(0, GameState.score - 1);
spawnFloatingText(player.pos, '-1', new Color(1, 0.3, 0.3, 1));
}
if (player.pos.y > halfHeight) {
player.pos.y = halfHeight;
player.vel.y *= -0.7;
player.stretch = 0.6;
GameState.screenShake = 0.4;
Sounds.bounce?.play();
// Penalty for hitting wall
GameState.score = Math.max(0, GameState.score - 1);
spawnFloatingText(player.pos, '-1', new Color(1, 0.3, 0.3, 1));
}
// Animate squash and stretch back to normal
player.squash += (1 - player.squash) * 0.15;
player.stretch += (1 - player.stretch) * 0.15;
// Track if player is moving (for animation state)
player.isMoving = input.x !== 0 || input.y !== 0 || player.vel.length() > 0.1;
}
function renderPlayer(player) {
// Render trail first (snowy/icy trail)
for (const t of player.trail) {
const alpha = t.life * 0.4;
drawCircle(t.pos, t.size, new Color(0.8, 0.9, 1, alpha));
}
const drawPos = vec2(player.pos.x, player.pos.y);
// Shadow on snow
drawCircle(
vec2(player.pos.x, player.pos.y - 0.7),
1.4,
new Color(0.4, 0.5, 0.6, 0.2),
);
// Skier character - tile 70 when idle, tile 71 when moving
const playerTile = player.isMoving ? 71 : 70;
const size = vec2(
player.size.x * player.squash * (player.facingRight ? 1 : -1),
player.size.y * player.stretch,
);
drawTile(
drawPos,
size,
tile(playerTile, GameConfig.tileSize, 0, GameConfig.tilePadding),
);
}
// =============================================================================
// FLAGS (Slalom gates to collect!)
// =============================================================================
function createFlag(pos) {
const halfW = GameConfig.width / 2 - 2;
const halfH = GameConfig.height / 2 - 3;
// Every 7th flag is red (deterministic, fair for all players)
GameState.flagsSpawned++;
const isRed = GameState.flagsSpawned % 7 === 0;
return {
pos: pos || vec2(rand(-halfW, halfW), rand(-halfH + 2, halfH)),
size: vec2(1.4, 1.4),
spawnPhase: rand(0, Math.PI * 2),
collected: false,
spawnTime: time,
scale: 0,
isRed: isRed,
};
}
function spawnFlags(count) {
for (let i = 0; i < count; i++) {
GameState.coins.push(createFlag());
}
}
function updateFlags() {
// Don't update if game is over
if (GameState.gameOver) return;
for (const flag of GameState.coins) {
// Spawn animation only
if (flag.scale < 1) {
flag.scale += 0.1;
if (flag.scale > 1) flag.scale = 1;
}
// Check collision with player
const dist = flag.pos.distance(GameState.player.pos);
if (dist < 1.3 && !flag.collected) {
flag.collected = true;
// Simple scoring: blue = 1, red = 3
const points = flag.isRed ? 3 : 1;
GameState.score += points;
// Spawn floating score text
const textColor = flag.isRed
? new Color(1, 0.4, 0.4, 1) // Red for red flags
: new Color(0.4, 0.6, 1, 1); // Blue for blue flags
spawnFloatingText(flag.pos.add(vec2(0, 0.5)), `+${points}`, textColor);
// Spawn celebration particles (snow burst!)
const particleCount = flag.isRed ? 15 : 10;
const particleColor = flag.isRed
? new Color(1, 0.5, 0.5, 1)
: new Color(0.5, 0.7, 1, 1);
spawnParticles(flag.pos, particleCount, particleColor);
// Red flags get the more exciting sound
if (flag.isRed) {
Sounds.combo?.play();
} else {
Sounds.coin?.play();
}
// Update high score
if (GameState.score > GameState.highScore) {
GameState.highScore = GameState.score;
saveHighScore(GameState.highScore);
}
}
}
// Remove collected flags
GameState.coins = GameState.coins.filter((c) => !c.collected);
// Spawn new flags - keep between 1 and 6 on screen
// Always maintain at least 1 flag so fast players always see one
if (GameState.coins.length < 1) {
spawnFlags(1);
} else if (GameState.coins.length < 6) {
// Spawn rate increases with score - fast players get more flags
const baseRate = 0.015;
const scoreBonus = Math.min(GameState.score * 0.001, 0.025);
if (Math.random() < baseRate + scoreBonus) {
spawnFlags(1);
}
}
}
function renderFlags() {
for (const flag of GameState.coins) {
const drawPos = vec2(flag.pos.x, flag.pos.y);
// Flag shadow on snow
drawCircle(
vec2(flag.pos.x, flag.pos.y - 0.4),
0.6 * flag.scale,
new Color(0.4, 0.5, 0.6, 0.15),
);
// Subtle glow effect (red or blue based on flag type)
const glowColor = flag.isRed
? new Color(1, 0.4, 0.4, 0.2)
: new Color(0.4, 0.6, 1, 0.2);
drawCircle(drawPos, 1.0 * flag.scale, glowColor);
// Flag sprite (red=20, blue=21) - large slalom flags
const flagTile = flag.isRed ? 20 : 21;
const size = vec2(flag.size.x * flag.scale, flag.size.y * flag.scale);
drawTile(
drawPos,
size,
tile(flagTile, GameConfig.tileSize, 0, GameConfig.tilePadding),
);
}
}
// =============================================================================
// FLOATING TEXT (score popups)
// =============================================================================
function spawnFloatingText(pos, text, color) {
GameState.floatingTexts.push({
pos: pos.copy(),
vel: vec2(rand(-0.02, 0.02), 0.08),
text: text,
color: color,
life: 1,
scale: 0.5,
});
}
function updateFloatingTexts() {
for (const ft of GameState.floatingTexts) {
ft.pos = ft.pos.add(ft.vel);
ft.vel = ft.vel.scale(0.98);
ft.life -= 0.025;
// Scale up quickly then stay
if (ft.scale < 1) {
ft.scale += 0.15;
if (ft.scale > 1) ft.scale = 1;
}
}
// Remove faded texts
GameState.floatingTexts = GameState.floatingTexts.filter((ft) => ft.life > 0);
}
function renderFloatingTexts() {
for (const ft of GameState.floatingTexts) {
const alpha = ft.life * ft.life; // Ease out
const color = new Color(ft.color.r, ft.color.g, ft.color.b, alpha);
const shadowColor = new Color(0, 0, 0, alpha * 0.5);
const size = 0.6 * ft.scale;
// Shadow
drawText(ft.text, ft.pos.add(vec2(0.05, -0.05)), size, shadowColor);
// Main text
drawText(ft.text, ft.pos, size, color);
}
}
// =============================================================================
// PARTICLES
// =============================================================================
function spawnParticles(pos, count, color) {
for (let i = 0; i < count; i++) {
const angle = rand(0, Math.PI * 2);
const speed = rand(0.08, 0.2);
GameState.particles.push({
pos: pos.copy(),
vel: vec2(Math.cos(angle) * speed, Math.sin(angle) * speed + 0.1),
size: rand(0.15, 0.4),
color: new Color(
color.r + rand(-0.1, 0.1),
color.g + rand(-0.1, 0.1),
color.b + rand(-0.1, 0.1),
1,
),
life: 1,
decay: rand(0.015, 0.035),
gravity: rand(0.002, 0.006),
});
}
}
function updateParticles() {
for (const p of GameState.particles) {
p.pos = p.pos.add(p.vel);
p.vel = p.vel.scale(0.97);
p.vel.y -= p.gravity;
p.life -= p.decay;
p.size *= 0.97;
}
// Remove dead particles
GameState.particles = GameState.particles.filter((p) => p.life > 0);
}
function renderParticles() {
for (const p of GameState.particles) {
const alpha = p.life * p.life; // Ease out
const color = new Color(p.color.r, p.color.g, p.color.b, alpha);
drawCircle(p.pos, p.size, color);
}
}
// =============================================================================
// DECORATIONS (snowy background stuff)
// =============================================================================
function createDecorations() {
const decorations = [];
const baseY = -13.5; // At the very bottom of the screen
// Mix of snowy pines (6/18), bare trees (7/19) - 10 trees with organic spacing
const treePositions = [-9.2, -6.5, -4.8, -2, 0.5, 2.8, 5.2, 6.8, 8.5, 9.8];
for (let i = 0; i < treePositions.length; i++) {
const xPos = treePositions[i];
const treeX = xPos + rand(-0.8, 0.8); // Much more horizontal variation
const isBare = rand() > 0.65; // 35% chance of bare tree
const topTile = isBare ? 7 : 6;
const trunkTile = isBare ? 19 : 18;
const treeSize = rand(1.1, 1.6);
// Trunk
decorations.push({
pos: vec2(treeX, baseY),
tile: trunkTile,
size: vec2(treeSize, treeSize),
isTree: true,
});
// Top
decorations.push({
pos: vec2(treeX, baseY + treeSize * 0.85),
tile: topTile,
size: vec2(treeSize, treeSize),
isTree: true,
});
}
// Add lots of bushes scattered between and in front of trees (24 bushes)
for (let i = 0; i < 24; i++) {
const bushX = rand(-9, 9);
const bushY = baseY + rand(0.1, 1.2);
const isSnowy = rand() > 0.4; // 60% snowy, 40% bare
decorations.push({
pos: vec2(bushX, bushY),
tile: isSnowy ? 30 : 31,
size: vec2(rand(0.5, 0.9), rand(0.5, 0.9)),
isBush: true, // Mark as bush for render ordering
});
}
// Add a big snowman between trees
decorations.push({
pos: vec2(rand(-2, 2), baseY + rand(0.5, 1.0)),
tile: 69,
size: vec2(2.0, 2.0),
isBush: true, // Render with bushes (behind trees)
});
// Add some ski tracks on the ground
for (let i = 0; i < 8; i++) {
decorations.push({
pos: vec2(rand(-8, 8), rand(-11, -8)),
tile: 41,
size: vec2(1, 1),
isTrack: true,
});
}
// Add some snow clouds
for (let i = 0; i < 5; i++) {
decorations.push({
pos: vec2(rand(-10, 10), rand(9, 12)),
tile: -1, // Use circles for clouds
size: vec2(rand(2, 4), rand(1, 2)),
phase: rand(0, Math.PI * 2),
speed: rand(0.008, 0.02),
isCloud: true,
});
}
// Add falling snowflakes
for (let i = 0; i < 20; i++) {
decorations.push({
pos: vec2(rand(-10, 10), rand(-12, 14)),
size: vec2(rand(0.1, 0.25)),
phase: rand(0, Math.PI * 2),
speed: rand(0.02, 0.05),
isSnowflake: true,
});
}
return decorations;
}
function updateDecorations() {
for (const d of GameState.decorations) {
if (d.isCloud) {
d.pos.x += d.speed;
if (d.pos.x > 14) d.pos.x = -14;
}
if (d.isSnowflake) {
d.phase += 0.05;
d.pos.y -= d.speed;
d.pos.x += Math.sin(d.phase) * 0.02;
// Reset snowflake to top when it falls off screen
if (d.pos.y < -14) {
d.pos.y = 14;
d.pos.x = rand(-10, 10);
}
}
}
}
function renderDecorations() {
// Render clouds first (behind everything)
for (const d of GameState.decorations) {
if (d.isCloud) {
const wobble = Math.sin(d.phase) * 0.1;
drawCircle(d.pos, d.size.x, new Color(0.9, 0.93, 0.98, 0.8));
drawCircle(
d.pos.add(vec2(-0.8, 0.2)),
d.size.x * 0.7,
new Color(0.95, 0.97, 1, 0.8),
);
drawCircle(
d.pos.add(vec2(0.8, 0.1 + wobble)),
d.size.x * 0.6,
new Color(0.92, 0.95, 1, 0.8),
);
}
// Render falling snowflakes
if (d.isSnowflake) {
const twinkle = 0.6 + Math.sin(d.phase * 3) * 0.4;
drawCircle(d.pos, d.size.x, new Color(1, 1, 1, twinkle));
}
}
}
function renderForegroundDecorations() {
// Render bushes and snowman first (behind trees)
for (const d of GameState.decorations) {
if (d.isBush && d.tile) {
drawTile(
d.pos,
d.size,
tile(d.tile, GameConfig.tileSize, 0, GameConfig.tilePadding),
);
}
}
// Then render trees and tracks on top
for (const d of GameState.decorations) {
if ((d.isTree || d.isTrack) && d.tile) {
drawTile(
d.pos,
d.size,
tile(d.tile, GameConfig.tileSize, 0, GameConfig.tilePadding),
);
}
}
}
// =============================================================================
// BACKGROUND & UI
// =============================================================================
function renderBackground() {
// Winter sky gradient (pale blue to white)
drawRect(
vec2(0, 0),
vec2(GameConfig.width, GameConfig.height),
new Color(0.7, 0.82, 0.92, 1),
);
drawRect(
vec2(0, -5),
vec2(GameConfig.width, 12),
new Color(0.82, 0.88, 0.95, 1),
);
// Snow/ice ground
drawRect(
vec2(0, -12),
vec2(GameConfig.width, 5),
new Color(0.95, 0.97, 1, 1),
);
// Ice rink surface
drawRect(
vec2(0, -2),
vec2(GameConfig.width - 2, GameConfig.height - 6),
new Color(0.85, 0.92, 0.98, 1),
);
// Snow bank edges
drawRect(
vec2(0, -13),
vec2(GameConfig.width, 2),
new Color(0.88, 0.91, 0.96, 1),
);
}
function renderUI() {
// Dark bar behind top UI for readability
drawRect(
vec2(0, 13),
vec2(GameConfig.width, 3),
new Color(0.1, 0.15, 0.25, 0.7),
);
// Score display
const scoreText = `Flags: ${GameState.score}`;
drawText(scoreText, vec2(-7, 12.5), 0.7, new Color(0, 0, 0, 0.5));
drawText(scoreText, vec2(-7, 12.7), 0.7, new Color(1, 1, 1, 1));
// High score
const highText = `Best: ${GameState.highScore}`;
drawText(highText, vec2(7, 12.5), 0.6, new Color(0, 0, 0, 0.5));
drawText(highText, vec2(7, 12.7), 0.6, new Color(1, 1, 1, 1));
// Timer display
const timeLeft = Math.max(0, Math.ceil(-(GameState.gameTimer?.get() || 0)));
const timerColor =
timeLeft <= 5 ? new Color(1, 0.3, 0.3, 1) : new Color(1, 1, 1, 1);
drawText(`Time: ${timeLeft}`, vec2(0, 12.7), 0.7, timerColor);
// Start screen
if (!GameState.gameStarted) {
drawRect(
vec2(0, 0),
vec2(GameConfig.width, GameConfig.height),
new Color(0, 0, 0, 0.5),
);
drawText('SLALOM', vec2(0, 4), 1.5, new Color(1, 1, 1, 1));
drawText(
'Ski through the flags!',
vec2(0, 1.5),
0.5,
new Color(0.8, 0.9, 1, 0.9),
);
const controlText = isTouchDevice
? 'Use d-pad to move'
: 'Arrow keys to move';
drawText(controlText, vec2(0, 0.5), 0.45, new Color(0.8, 0.9, 1, 0.8));
const pulse = 0.7 + Math.sin(time * 4) * 0.15;
const startText = isTouchDevice ? 'Tap to start' : 'Press SPACE to start';
drawText(startText, vec2(0, -2), 0.55 * pulse, new Color(0.7, 0.85, 1, 1));
return;
}
// Game over display
if (GameState.gameOver) {
drawRect(
vec2(0, 0),
vec2(GameConfig.width, GameConfig.height),
new Color(0, 0, 0, 0.5),
);
drawText('TIME UP!', vec2(0, 3), 1.2, new Color(1, 1, 1, 1));
drawText(
`Final Score: ${GameState.score}`,
vec2(0, 0.5),
0.8,
new Color(0.7, 0.85, 1, 1),
);
const restartText = isTouchDevice
? 'Tap to play again'
: 'Press SPACE to play again';
drawText(restartText, vec2(0, -2), 0.5, new Color(0.8, 0.9, 1, 0.8));
}
// Instructions (fade out after game starts)
if (!GameState.gameOver) {
const timeSinceStart = -(GameState.gameTimer?.get() || 0);
const instructionAlpha = Math.max(0, 1 - timeSinceStart * 0.2);
if (instructionAlpha > 0) {
drawText(
'Avoid walls!',
vec2(0, 0),
0.45,
new Color(0.3, 0.4, 0.6, instructionAlpha * 0.8),
);
}
}
}
// =============================================================================
// MAIN GAME FUNCTIONS
// =============================================================================
function gameInit() {
const gameSize = vec2(
GameConfig.width * GameConfig.tileSize,
GameConfig.height * GameConfig.tileSize,
);
setCanvasFixedSize(gameSize);
GameConfig.size = vec2(GameConfig.width, GameConfig.height);
GameConfig.center = vec2(0);
setCameraScale(GameConfig.tileSize);
// Initialize sounds (must be done after engine init)
initSounds();
// Load high score from localStorage
GameState.highScore = loadHighScore();
// Initialize game state
GameState.player = createPlayer();
GameState.coins = [];
GameState.particles = [];
GameState.floatingTexts = [];
GameState.decorations = createDecorations();
GameState.score = 0;
GameState.spawnTimer = new Timer();
GameState.spawnTimer.set(2); // Start spawn timer
GameState.comboTimer = null;
GameState.combo = 0;
GameState.gameTimer = null; // Don't start timer until player presses SPACE
GameState.gameOver = false;
GameState.gameStarted = false;
GameState.flagsSpawned = 0;
// Spawn initial flags
spawnFlags(5);
}
function gameUpdate() {
// Check for game over
// Wait for player to start the game
if (!GameState.gameStarted) {
if (keyWasPressed('Space') || mouseWasPressed(0)) {
GameState.gameStarted = true;
GameState.gameTimer = new Timer(GameState.gameTime);
primeAudio();
}
updateDecorations(); // Keep snowflakes moving
return;
}
if (GameState.gameTimer?.elapsed() && !GameState.gameOver) {
GameState.gameOver = true;
// Update high score at end of game and save to localStorage
if (GameState.score > GameState.highScore) {
GameState.highScore = GameState.score;
saveHighScore(GameState.highScore);
}
}
// Restart game on space or tap when game over
if (GameState.gameOver && (keyWasPressed('Space') || mouseWasPressed(0))) {
GameState.player = createPlayer();
GameState.coins = [];
GameState.score = 0;
GameState.combo = 0;
GameState.gameTimer = new Timer(GameState.gameTime);
GameState.gameOver = false;
GameState.flagsSpawned = 0;
spawnFlags(5);
}
// Update screen shake
GameState.screenShake *= 0.9;
updatePlayer(GameState.player);
updateFlags();
updateParticles();
updateFloatingTexts();
updateDecorations();
}
function gameRender() {
// Apply screen shake by offsetting camera
const shakeX = rand(-1, 1) * GameState.screenShake;
const shakeY = rand(-1, 1) * GameState.screenShake;
setCameraPos(vec2(shakeX, shakeY));
renderBackground();
renderDecorations();
renderFlags();
renderParticles();
renderPlayer(GameState.player);
renderFloatingTexts();
renderForegroundDecorations();
}
function gameUpdatePost() {}
function gameRenderPost() {
renderUI();
}
// Fire up LittleJS!
engineInit(
gameInit,
gameUpdate,
gameUpdatePost,
gameRender,
gameRenderPost,
GameConfig.tiles,
document.querySelector('#demo-one-example'),
);