indiecake-engine
v0.2.0
Published
Lightweight game engine for Indie Cake platform with 2D graphics, input handling, and audio support
Maintainers
Readme
Indie Cake Engine v0.2.0
A lightweight 2D runtime for Indie Cake microgames, with platform integration, TypeScript types, and CDN-ready builds.
Highlights
- Small production runtime (
dist/indiecake-engine.min.js, ~12 KB gzipped) - Multiple output formats (ESM, CJS, IIFE)
- Built-in systems for graphics, input, audio, assets, game state, timing, platform events, and difficulty
- Engine-managed game loop with fixed-step updates and catch-up
- Type definitions generated in
dist/index.d.ts - Release-ready scripts for bundle-size checks and Subresource Integrity (SRI) hashes
Install
npm install indiecake-engineQuick Start
ESM (recommended)
import { IndieCake } from 'indiecake-engine';
IndieCake.init({
canvas: 'gameCanvas',
width: 320,
height: 240,
});
IndieCake.game.ready();
IndieCake.game.start();
IndieCake.loop.start({
update: () => {
if (IndieCake.input.isKeyPressed('Space')) {
IndieCake.platform.addScore(10);
}
},
render: () => {
IndieCake.graphics.clear('#000');
IndieCake.graphics.drawRect(100, 100, 50, 50, { color: '#ff0000' });
},
});CommonJS
const { IndieCake } = require('indiecake-engine');
IndieCake.init({ canvas: 'gameCanvas' });Browser via jsDelivr
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/indiecake-engine.min.js"></script>
<script>
window.IndieCake.init({ canvas: 'gameCanvas' });
</script>CDN URLs
- Latest:
https://cdn.jsdelivr.net/npm/indiecake-engine@latest/dist/indiecake-engine.min.js - Pinned:
https://cdn.jsdelivr.net/npm/[email protected]/dist/indiecake-engine.min.js - Fallback:
https://unpkg.com/[email protected]/dist/indiecake-engine.min.js
Build Scripts
npm run build: production build (clean -> rollup -> size checks)npm run build:rollup: non-production rollup buildnpm run build:rollup:prod: production rollup buildnpm run dev: watch modenpm run check:sizes: gzip size validationnpm run generate:sri: generate SRI hashes and writedist/sri.json
Publishing Releases
The Release Engine to npm workflow runs when a GitHub release with an
engine-v<version> tag is published. The version must match this package's
package.json.
Publishing uses npm Trusted Publishing and GitHub Actions OIDC. No long-lived
npm token or NPM_TOKEN repository secret is required.
Before the first release, configure a GitHub Actions trusted publisher in the
indiecake-engine package settings on npmjs.com with:
- GitHub owner:
indie-cake - Repository:
indiecake - Workflow filename:
publish-engine.yml - Environment: leave blank
The repository, workflow filename, and package repository.url must match the
trusted-publisher configuration exactly. The workflow builds the package,
publishes it to npm, uploads the bundles, and adds CDN URLs to the GitHub release
notes. Because this source repository is private, npm does not generate a public
provenance attestation for the package.
Build Outputs
dist/indiecake-engine.min.js: IIFE, minified (primary CDN artifact)dist/indiecake-engine.iife.js: IIFE, non-minifieddist/indiecake-engine.esm.js: ESM, non-minifieddist/indiecake-engine.esm.min.js: ESM, minifieddist/indiecake-engine.cjs: CommonJS, non-minifieddist/indiecake-engine.cjs.min.cjs: CommonJS, minifieddist/index.d.ts: Type declarationsdist/*.map: source maps
Using SRI Hashes
Generate hashes:
npm run generate:sriThen use the generated value in script tags:
<script
src="https://cdn.jsdelivr.net/npm/[email protected]/dist/indiecake-engine.min.js"
integrity="sha384-..."
crossorigin="anonymous"
></script>Monorepo Notes
- In this repository, server build copies
dist/indiecake-engine.esm.jstoapps/server/public/uploads. - Root turbo build now enforces
indiecake-engine#buildbefore@indiecake/server#build. - The server copy script also has a fallback that builds the engine if the artifact is missing.
Engine Systems
Graphics
- Canvas rendering
- Rect/circle/text/image primitives
- Basic transforms
Input
- Keyboard/mouse/touch support
- Press/state checks and event hooks
Audio
- Sound/music playback through Web Audio API
- Platform-uploaded audio loading by title alias or content hash
- Master volume and mute that apply to already-playing sounds
Assets
- Async loading and retrieval for image/audio/json/text
- Platform-uploaded image loading by alias or content hash
Time and Loop
- Delta time, elapsed time, and FPS tracking
- Pausable timers (
setTimeout/setInterval/wait) tied to engine time - Engine-managed
requestAnimationFrameloop with fixed-step updates
Platform
- Completion events
- Score integration
- Platform-friendly helpers
Difficulty
- Difficulty level setters/getters
- Scaled game behavior helpers
API Reference
Core Engine
IndieCake.init(config); // Initialize all systems
IndieCake.destroy(); // Cleanup all systems
IndieCake.isInitialized(); // Engine initialized state
IndieCake.complete(success); // One-shot game completion
IndieCake.win(); // Alias for complete(true)
IndieCake.lose(); // Alias for complete(false)
IndieCake.random(min, max); // Utility random number
IndieCake.clamp(value, min, max); // Utility clamp
IndieCake.lerp(a, b, t); // Utility interpolation
IndieCake.distance(x1, y1, x2, y2);Subsystems: IndieCake.graphics, .input, .audio, .assets, .game,
.platform, .difficulty, .time, .loop.
Classes: IndieCake.GameObject, .GameScene, .Vector2D, .Rectangle, .Color.
Loop (IndieCake.loop)
loop.start({ fps, update, render, renderWhilePaused, maxCatchUpSteps });
loop.stop(); // Cancel the scheduled frame
loop.isRunning(); // Loop running stateupdate(deltaTime)runs on a fixed step derived fromfps(default 60).render()runs once per animation frame, and keeps running while the game is paused unlessrenderWhilePausedisfalse.maxCatchUpSteps(default 5) caps how many update steps a single frame may run after the tab has been backgrounded.
Graphics (IndieCake.graphics)
graphics.clear(color?); // Clear frame or fill background color
graphics.save(); // Save canvas state (styles/transforms)
graphics.restore(); // Restore last saved canvas state
graphics.drawRect(x, y, width, height, options?); // Draw rectangle
graphics.drawCircle(x, y, radius, options?); // Draw circle
graphics.drawLine(x1, y1, x2, y2, options?); // Draw line segment
graphics.drawPath(points, options?); // Draw a path through a list of points
graphics.drawText(text, x, y, options?); // Draw text label
graphics.measureText(text, options?); // Measure text without drawing it
graphics.drawImage(image, x, y, width?, height?, options?); // Draw sprite/image
graphics.drawImageRegion(image, sx, sy, sw, sh, dx, dy, dw, dh, options?); // Draw a sprite-sheet region
graphics.translate(x, y); // Offset subsequent drawing
graphics.rotate(angle); // Rotate subsequent drawing (radians)
graphics.scale(x, y); // Scale subsequent drawing
graphics.resize(width, height); // Resize render surface
graphics.getCanvas(); // Get underlying canvas element
graphics.getContext(); // Get 2D context for custom drawing
graphics.getWidth(); // Current logical canvas width
graphics.getHeight(); // Current logical canvas heightInput (IndieCake.input)
input.isKeyPressed(key); // True while key is held
input.isKeyDown(key); // Alias of isKeyPressed
input.isMousePressed(button?); // True while mouse button is held
input.getMousePosition(); // Current pointer position { x, y }
input.getTouches(); // Active touch points [{ x, y, id }]
input.isTouching(); // Any active touches present
input.addEventListener((event) => { ... }); // Subscribe to keyboard/mouse/touch events
input.removeEventListener(callback); // Unsubscribe input listenerAssets (IndieCake.assets)
await assets.loadAsset(id, url, type); // Load one asset (image|audio|json|text)
await assets.loadAssets(assetList); // Load multiple assets in parallel
await assets.loadAssetsWithProgress(assetList, onProgress); // Batch load with progress callback
assets.getAsset(id); // Get raw cached asset data by id
assets.getImage(id); // Get image asset as HTMLImageElement
assets.getAudio(id); // Get audio asset as HTMLAudioElement
assets.getJson(id); // Get parsed JSON asset
assets.getText(id); // Get text asset contents
assets.getByHash(hash); // Read cached asset by hash
assets.unloadAsset(id); // Remove one cached asset
assets.clearAll(); // Unload every cached asset
assets.isLoaded(id); // Check if asset exists and is ready
assets.isLoading(id); // Check if asset is still loading
assets.getLoadedAssets(); // Ids of all ready assets
assets.getAssetInfo(id); // { type, loaded, error? } or null
assets.setBaseUrl(baseUrl); // Prefix used for relative asset URLs
assets.getBaseUrl(); // Read current base URL
// Platform-aware helpers (registry only, never arbitrary URLs)
await assets.loadImage(filenameOrAlias); // Load uploaded image by alias/filename
await assets.loadImageByHash(hash); // Load uploaded image by content hash
await assets.preloadPlatformAssets(); // Warm cache with platform registry images
assets.setPlatformAssetRegistry(registry); // Replace the platform registry at runtime
assets.getPlatformAssetRegistry(); // Read the current platform registry
assets.getAvailablePlatformAssets(); // Hashes available in the registryAudio (IndieCake.audio)
await audio.loadAudio(id, url?); // Load by platform alias/hash, or from a URL
await audio.loadAudioByHash(hash); // Load uploaded audio by content hash
await audio.preloadPlatformAudio(); // Preload every audio asset in the registry
audio.unloadAudio(id); // Remove audio from cache
audio.isLoaded(id); // Check audio readiness
audio.play(id, { volume, loop, delay }); // Play sound with options
audio.pause(id); // Pause one track
audio.stop(id); // Stop and reset one track
audio.setMasterVolume(volume); // Set global output volume (0..1)
audio.getMasterVolume(); // Read current global volume
audio.setMuted(boolean); // Mute/unmute all audio
audio.isMuted(); // Read mute state
audio.getLoadedAssets(); // Ids of all ready audio assets
audio.getAssetInfo(id); // { loaded, error?, duration? } or null
audio.getAudioContext(); // Underlying AudioContext, if available
audio.createGainNode(); // GainNode for custom audio graphs
await audio.resumeAudioContext(); // Resume after a user gestureWhen url is omitted, loadAudio(id) resolves the id against the platform audio
registry - first by alias (the title the audio was uploaded with), then by hash.
Master volume and mute apply to sounds that are already playing, and the host
page can seed both through audioSettings in the engine config.
Game State (IndieCake.game)
game.ready(); // Enter ready state
game.start(); // Enter running state
game.stop(); // Enter stopped state
game.pause(); // Pause updates
game.resume(); // Resume from paused state
game.complete(); // Mark game as completed
game.isRunning(); // True when running
game.isPaused(); // True when paused
game.isStopped(); // True when stopped
game.isCompleted(); // True when completed
game.getState(); // Current state string
game.hasGameStarted(); // Check game has started
game.createScene(idOrScene); // Create scene container
game.setActiveScene(id); // Switch active scene
game.getScene(id); // Get scene by id
game.removeScene(id); // Remove scene and objects
game.createObject(template?); // Create game object
game.getObject(id); // Get object by id
game.removeObject(id); // Remove object by id
game.addObjectToScene(objectId, sceneId); // Move object into scene
game.checkCollision(objA, objB); // AABB collision test
game.getObjectsAt(x, y); // Objects under point
game.getAllObjects(); // All objects across scenes
game.getActiveObjects(); // Objects with active=true
game.getVisibleObjects(); // Objects with visible=true
game.setLifecycleEventHandler(handler); // Observe ready/start/pause/resume/completeClasses
class Player extends IndieCake.GameObject {
constructor(x, y) {
super({ x, y, width: 24, height: 24 });
}
update(deltaTime) {
this.x += 50 * deltaTime;
}
render(graphics) {
IndieCake.graphics.drawRect(
this.x,
this.y,
this.width,
this.height,
'#3a86ff'
);
}
}
class Level extends IndieCake.GameScene {
init() {}
update(dt) {}
}
const position = new IndieCake.Vector2D(10, 20);
position.add(other);
position.sub(other);
position.scale(2);
position.dot(other);
position.length();
position.distanceTo(other);
position.normalize();
position.clone();
const bounds = new IndieCake.Rectangle(0, 0, 32, 32);
bounds.contains(x, y);
bounds.intersects(other);
bounds.center();
const color = new IndieCake.Color(255, 0, 110, 1);
color.toCSS();
color.toHex();
IndieCake.Color.fromHex('#ff006e');Platform (IndieCake.platform)
platform.complete({ success, score?, timeElapsed?, metadata? }); // Send completion payload to host
platform.isGameCompleted(); // Prevent duplicate completion submissions
platform.setScore(score); // Set current score
platform.addScore(points); // Increment score
platform.getScore(); // Read current score
platform.getElapsedTime(); // Seconds of play, excluding paused time
platform.resetGameTime(); // Reset elapsed timer for new round
platform.pause(); // Freeze elapsed/remaining time
platform.resume(); // Resume elapsed/remaining time
platform.setTimeLimit(seconds); // Round time budget (null to clear)
platform.getTimeLimit(); // Read round time budget
platform.getTimeRemaining(); // Seconds left, or null without a limit
platform.getGameInfo(); // Host-provided game metadata
platform.getGameTitle(); // Current game title
platform.getGameObjective(); // Player-facing objective
platform.getGameDescription(); // Game description
platform.getThemeInfo(); // Host theme context
platform.getThemeName(); // e.g. 'default' | 'gameboy' | 'arcade'
platform.getThemeMode(); // e.g. 'light' | 'dark'
platform.isDarkMode(); // Convenience dark-mode check
platform.getUserInfo(); // Host user context
platform.getUserId(); // Current user id
platform.getUsername(); // Current username
platform.getDisplayName(); // Current display name
platform.setCallbacks({ onGameComplete, onScoreUpdate }); // Register host/game callbacks
platform.getPlatformInfo(); // UA/device capability info
platform.saveData(key, value); // Persist JSON in localStorage
platform.loadData(key); // Read JSON from localStorage
platform.removeData(key); // Delete persisted keyGame, theme, and user context are supplied by the host page at init, so the
getters return null when a game runs outside the platform.
Difficulty (IndieCake.difficulty)
difficulty.setLevel(level); // Set level (clamped to 1..5)
difficulty.getLevel(); // Read current level
difficulty.defineLevel(level, settings); // Override/define a level's settings
difficulty.getSettings(level?); // Full settings for specific/current level
difficulty.getCurrentSettings(); // Full settings for active level
difficulty.getSpeedMultiplier(); // Numeric speed scaling factor
difficulty.getTimeMultiplier(); // Numeric time scaling factor
difficulty.getSpeedScale(baseSpeed); // Scale a speed value by difficulty
difficulty.getTimeScale(baseTime); // Scale a time value by difficulty
difficulty.getScalingFactor(); // General 1.0-1.8 scaling factor
difficulty.onLevelChange(callback); // Subscribe to level changes
difficulty.removeCallback(callback); // Unsubscribe callback
difficulty.isEasy(); // Level 1-2
difficulty.isMedium(); // Level 3
difficulty.isHard(); // Level 4
difficulty.isExpert(); // Level 5
difficulty.isCasual(); // Level 1
difficulty.isStandard(); // Level 2
difficulty.isChallenging(); // Level 3
difficulty.isLegendary(); // Level 5Time (IndieCake.time)
time.update(deltaTime?); // Update timing from custom delta or the clock
time.pause(); // Pause timing and all engine timers
time.resume(); // Resume timing and all engine timers
await time.wait(ms, options?); // Promise that resolves after a delay
time.setTimeout(callback, ms, options?); // Pausable timeout, returns an id
time.setInterval(callback, ms, options?); // Pausable interval, returns an id
time.clearTimeout(timeoutId); // Cancel one timer
time.clearTimer(timeoutId); // Alias of clearTimeout
time.clearAllCountdowns(); // Cancel every timer
time.clearAll(); // Alias of clearAllCountdowns
time.getCountdowns(); // Active countdown handles
time.deltaTime; // Seconds since last frame
time.totalTime; // Total seconds since init
time.currentFPS; // Estimated FPS (updated ~once per second)
time.now; // Current timestamp in milliseconds
time.getDelta(); // Alias for deltaTime
time.getElapsed(); // Alias for totalTime
time.getFPS(); // Alias for currentFPSTimer options accept { countsDown } to keep remaining time updated while the
timer runs, so paused rounds do not drain a countdown.
Common Usage Patterns
1) Signal lifecycle and run the engine loop
// Tell the platform the game is ready, then that play has begun.
IndieCake.game.ready();
IndieCake.game.start();
IndieCake.loop.start({
fps: 60,
update: (deltaTime) => {
player.x += speed * deltaTime;
},
render: () => {
IndieCake.graphics.clear('#000');
IndieCake.graphics.drawRect(player.x, player.y, 16, 16, { color: '#fff' });
},
});2) Complete a game safely once
// IndieCake.complete() is one-shot per session
if (playerWon) {
IndieCake.win();
} else {
IndieCake.lose();
}3) Load platform assets
// Images uploaded through the creator tools, loaded by alt text or hash
IndieCake.assets.loadImage('player-sprite').then((image) => {
IndieCake.graphics.drawImage(image, 100, 100);
});
// Audio uploaded through the creator tools, loaded by its title
IndieCake.audio.loadAudio('jump').then(() => {
IndieCake.audio.play('jump', { volume: 0.8 });
});
// Non-platform assets (e.g. level config) still load by URL
await IndieCake.assets.loadAssetsWithProgress(
[{ id: 'level', url: '/data/level-1.json', type: 'json' }],
(p) =>
console.log(`Loading ${p.loaded}/${p.total} (${p.percentage.toFixed(0)}%)`)
);4) Tune gameplay by difficulty level
const level = IndieCake.difficulty.getLevel();
// Option A: Direct multiplier-based scaling
const enemySpeed = 140 * IndieCake.difficulty.getSpeedMultiplier();
const timeLimit = 20 * IndieCake.difficulty.getTimeMultiplier();
// Option B: Use built-in scaling helpers
const spawnIntervalMs = IndieCake.difficulty.getTimeScale(1200);
const projectileSpeed = IndieCake.difficulty.getSpeedScale(220);
// Optional named checks for branching behavior
if (IndieCake.difficulty.isLegendary()) {
// Add advanced patterns, extra enemies, etc.
}
console.log('Difficulty level:', level);Difficulty cheat sheet
| Level | Label | Good for | Helpful checks |
| ----- | ----------- | ------------------------------------------- | ------------------------------- |
| 1 | Casual | New players, generous timing, slower pacing | isCasual(), isEasy() |
| 2 | Standard | Default balance for most games | isStandard(), isEasy() |
| 3 | Challenging | Tighten timing and speed a bit | isChallenging(), isMedium() |
| 4 | Expert | Faster reactions, less forgiveness | isHard() |
| 5 | Legendary | Fastest pace and shortest time windows | isLegendary(), isExpert() |
For scaling, a good default pattern is:
- Use
getTimeScale(baseTime)when you want harder modes to reduce or stretch timing in a consistent way. - Use
getSpeedScale(baseSpeed)when you want movement or enemy speed to respond to the selected level. - Use
getCurrentSettings()when you need the full label, description, color, and multiplier set.
Configuration
IndieCake.init({
canvas: 'gameCanvas', // Canvas element or element id
width: 320, // Logical canvas width
height: 240, // Logical canvas height
backgroundColor: '#000000',
fps: 60,
debug: false,
baseUrl: '', // Prefix for relative asset URLs
});When a game runs on the platform, the host page also injects the following at init, so games rarely set them by hand:
| Option | Purpose |
| ------------------- | ---------------------------------------------------------- |
| platformAssets | Image and audio registries backing loadImage/loadAudio |
| platformAssetsUrl | Base URL for hash-based asset lookups |
| audioSettings | The player's saved { volume, muted } |
| gameInfo | Title, objective, description, tags, categories, author |
| themeInfo | Host theme name and light/dark mode |
| userInfo | Id, username, and display name of the signed-in player |
Release Flow (Repository)
- Bump version in
package.json - Run
npm run build - Create a GitHub release tag (
engine-v0.2.0style) - GitHub Actions publishes package to npm
- jsDelivr serves the new version automatically
License
MIT. See LICENSE.
