simuu/lib
Some shared primitives for making games.
Time control
Pause, timescale, and frame stepping for game loops.
const time = createTimeControl({
scales: [0.25, 0.5, 1, 2, 4],
initialScale: 1,
})
// game loop
function update(rawDelta) {
const dt = time.delta(rawDelta)
if (dt === 0) return // paused or waiting for step
// update game with dt
}
// controls
time.toggle() // pause/unpause
time.faster() // next timescale
time.slower() // previous timescale
time.step() // advance one frame (enters step mode)
time.play() // exit step mode, resume
State: time.scale, time.paused, time.stepping
Camera
2D camera with coordinate conversion, zoom, bounds, and target following.
import { Camera } from '../lib'
const cam = new Camera(800, 600)
cam.setBounds(0, 0, worldWidth, worldHeight)
cam.centerOn(player.x, player.y)
cam.update(deltaMs)
cam.applyTo(pixiContainer)
const { x, y } = cam.screenToWorld(mouseX, mouseY)
Smooth follow with lerp:
cam.startFollow(player, 0.1)
Zoom toward cursor:
canvas.addEventListener('wheel', (e) => {
const world = cam.screenToWorld(e.clientX, e.clientY)
cam.zoomAt(e.deltaY > 0 ? -0.1 : 0.1, world.x, world.y)
})
Effects
Tree-structured effects for ability timing. Parent fires on schedule, children fire with it (optionally gated by conditions). Keeps ability logic declarative instead of scattered setTimeout calls.
const attack = createEffect(
id,
archer.id,
target.id,
{ delay: 0.5, interval: 1, repeat: Infinity },
{
payload: { type: 'damage', value: 25 },
children: [
{
payload: { type: 'heal', value: 5 }, // lifesteal
condition: { every: 3 }, // every 3rd hit
},
],
},
)
// game loop
for (const effect of activeEffects) {
for (const fire of tickEffect(effect, dt)) {
applyPayload(fire.payload, fire.target)
}
}
Payloads: damage, heal, construct, pulse
Conditions: { always: true }, { every: N }, { chance: 0-1 }
Combat log
Event types and query utilities. Storage lives in game layer.
import { byType, bySourceOwner, sumValues, entityRef } from '../lib'
log({
t: 5.2,
e: 'SPELL_DAMAGE',
s: entityRef(unitId, 1, 'archer'),
d: entityRef(targetId, 2, 'grunt'),
v: 25,
})
const myDamage = sumValues(
events.filter(byType('SPELL_DAMAGE')).filter(bySourceOwner(playerId)),
)
Events: SPELL_DAMAGE, SPELL_HEAL, SPELL_CAST_START, SPELL_CAST_SUCCESS, SPELL_CAST_FAILED, SPELL_SUMMON, UNIT_DIED, GAME_START, GAME_END, RESOURCE_SPEND, RESOURCE_GAIN
Filters: byType, bySourceOwner, byDestOwner, byOwner, bySourceType, byDestType, byAbility, byTimeRange, byEntityId
Aggregations: sumValues, totalDamageByOwner, totalHealingByOwner, killsByOwner, deathsByOwner, spawnsByOwner, spentByOwner, getLifespan, avgLifespanByUnitType
Utils
clamp(x, 0, 100)
lerp(a, b, 0.5)
naturalize(100, 0.1) // 100 ± 10%
inverseLerp(0, 100, 50) // 0.5
remap(50, 0, 100, 0, 1)
pick(['a', 'b', 'c'])
shuffle(array)
sample(array, 3)
random(10, 20)
randomInt(1, 6)
formatDuration(125) // "2:05"
formatDurationPrecise(125.7)
formatTime(timestamp)
timeSince(timestamp) // "5 minutes ago"
throttle(fn, 100)
debounce(fn, 200)
Also: roundOne, toPercent, range, remove, distance, distanceSq
Word generator
codename() // "golden-falcon"
word('predicates') // "swift"
word('objects') // "meadow"
word('collections') // "fleet"
word('teams') // "pride"
Sounds
Sound system with pooled samples, file samples, and procedural generation. Built on @pixi/sound.
import { initSoundRegistry, playSound, type SoundRegistry } from 'simuu/sounds'
const registry: SoundRegistry = {
pools: [
{
name: 'click',
paths: Array.from({ length: 11 }, (_, i) => `/sfx/ui/button-${i + 1}.webm`),
volume: 0.3,
},
],
files: [
{ name: 'victory', url: '/sfx/victory.webm', volume: 0.7 },
],
generated: [
{ name: 'beep', buffer: generateBeep(), volume: 0.4 },
],
defs: {
click: { volume: 0.3, throttleMs: 50 },
victory: { volume: 0.7 },
beep: { volume: 0.4 },
},
}
await initSoundRegistry(registry)
playSound('click', registry) // picks random from pool
playSound('victory', registry)
For procedural audio, use createWavBuffer:
import { createWavBuffer } from 'simuu/sounds'
function generateBeep(): ArrayBuffer {
const { samples, writeSample, buffer } = createWavBuffer(0.1)
for (let i = 0; i < samples; i++) {
const t = i / 44100
writeSample(i, Math.sin(2 * Math.PI * 440 * t) * 0.5)
}
return buffer
}
Global controls:
import { soundConfig, toggleSound, setMasterVolume } from 'simuu/sounds'
toggleSound() // on/off
setMasterVolume(0.5)
soundConfig.enabled // current state