Back to Browser Side Quest
SOURCE / PINNED RELEASE

Made of little things.

Browser Side Quest

Release
b9d42f5768e4…
Author-recorded commit
c267690145fa…
License
LICENSE
Author’s source reference
nostr://npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr/wss%3A%2F%2Fgit.shakespeare.diy%2F/browser-side-quest

Archive hash verified: 6d4b90b8a5f328f7…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

src/sim/snapshot.ts
/**
 * Compact world state the shard host broadcasts so a peer can take over when
 * the host leaves. Mob ids are deterministic (area/kind/index), so a fresh
 * World can be brought in line with the departed host's: mobs that were dead
 * are killed again (and respawn on the normal 30 s timer), survivors keep their
 * hit points and position. Items on the ground and chest drops are transient
 * and are not carried over.
 */
import { Mob } from './entities.js';
import type { World } from './world.js';

/** [id, hitPoints, x, y] for every living mob. */
export type WorldSnapshot = { v: 1; mobs: [number, number, number, number][] };

export function takeSnapshot(world: World): WorldSnapshot {
  const mobs: WorldSnapshot['mobs'] = [];
  for (const mob of world.mobs.values()) {
    if (!mob.isDead) mobs.push([mob.id, mob.hitPoints, mob.x, mob.y]);
  }
  return { v: 1, mobs };
}

export function isSnapshot(value: unknown): value is WorldSnapshot {
  const s = value as WorldSnapshot;
  return (
    !!s &&
    s.v === 1 &&
    Array.isArray(s.mobs) &&
    s.mobs.every((m) => Array.isArray(m) && m.length === 4 && m.every((n) => typeof n === 'number' && Number.isFinite(n)))
  );
}

/** Apply a snapshot to a freshly started world (before any player joins). */
export function restoreSnapshot(world: World, snapshot: WorldSnapshot): void {
  const alive = new Map(snapshot.mobs.map(([id, hp, x, y]) => [id, { hp, x, y }]));
  for (const mob of [...world.mobs.values()]) {
    const state = alive.get(mob.id);
    if (!state) {
      killSilently(world, mob);
      continue;
    }
    mob.hitPoints = Math.max(1, Math.min(mob.maxHitPoints, Math.round(state.hp)));
    if (world.isValidPosition(state.x, state.y)) world.moveEntity(mob, state.x, state.y);
  }
}

function killSilently(world: World, mob: Mob): void {
  mob.hitPoints = 0;
  world.removeEntity(mob);
}