simuu/lib
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, 8],
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 poisonArrow = createEffect(
id, archer.id, target.id,
{ delay: 0, interval: 1, repeat: 5 },
{
payload: { type: 'damage', value: 8 },
children: [
{
payload: { type: 'slow', mult: 0.5, duration: 2 },
condition: { chance: 0.3 },
},
],
},
)
// game loop
for (const effect of activeEffects) {
for (const fire of tickEffect(effect, dt)) {
applyPayload(fire.payload, fire.target)
}
}
Payloads: damage, heal, splash, slow, construct
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(playerId, unitId, 'archer'),
d: entityRef(enemyId, targetId, '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, count, 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, Queue
Word generator
codename() // "golden-falcon"
word('predicates') // "swift"
word('objects') // "meadow"
word('collections') // "fleet"
word('teams') // "pride"