lib

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"