Something went wrong. Try again.
A Social Deduction / Deception Game
Something went wrong. Try again.
7.2 kB · 139 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140// ---------------------------------------------------------------------------// Property readers — render-time interpretation of a node's `properties` bag.//// These are pure functions over `Property[]`; the engine uses them to decide// object animation playback. They are the engine's own self-contained copies of// the reserved-key readers that also live (for the editor) in `lexicon/map.ts`// and `store/editorStore.ts`. The reserved keys are part of the data contract,// so a separate game repo carries this file with the engine. The lexicon JSON// (`lexicons/world.tilemap.map.json`) remains the wire source of truth for the// key names.// ---------------------------------------------------------------------------
import type { Property } from './contract'
/** An object's animation play mode. `'loop'` cycles 0→n→0; `'pingpong'` plays * forward then backward without repeating the endpoints. */export type AnimationPlayMode = 'loop' | 'pingpong'
/** An object's animation play mode (on a definition's property bag). */export const OBJECT_ANIMATION_MODE_PROPERTY = 'world.tilemap.animationMode'
/** Per-instance animation state, carried on a placed object's property bag so * the object node itself stays a pure transform. */export const ANIMATION_PLAYING_PROPERTY = 'world.tilemap.animationPlaying'export const ANIMATION_FRAME_PROPERTY = 'world.tilemap.animationFrame'export const ANIMATION_ONCE_PROPERTY = 'world.tilemap.animationOnce'export const ANIMATION_REVERSE_PROPERTY = 'world.tilemap.animationReverse'
/** Marks a placed object as a cosmetic auto-door / auto-gate: it animates open * when a player paths through it and closes once they pass (see * `render/gate.ts`). Set on the instance's property bag so the same object def * can be placed as plain decoration elsewhere. Absent = not a gate. A * game-logic concern, so it lives in the `town.deception.*` namespace rather * than the generic `world.tilemap.*` one. */export const OBJECT_AUTO_GATE_PROPERTY = 'town.deception.autoGate'
/** Marks a node (a point marker, or an AREA for a scattered crowd) as a decorative * NPC spawn (PLAN §9): set dressing the client animates per-viewer, never on the * wire. The value is an NPC account **DID** (its repo holds `actor.rpg.sprite/self` * + `town.deception.npc/self`), optionally followed by a whitespace-separated * integer **weight** = exact count, e.g. `did:plc:abc 3`. A node may carry MORE * THAN ONE such property (one per critter) to define a distribution: an area then * spawns `sum(weights)` NPCs in those proportions; a marker spawns a single NPC * (weighted pick). Absent = a plain node. Game-logic-adjacent presentation, so it * lives in the `town.deception.*` namespace like the auto-gate flag rather than * the generic `world.tilemap.*` one. */export const NPC_TYPE_PROPERTY = 'town.deception.npc'
/** Optional per-marker override of an NPC's wander leash radius (px), letting one * placement roam wider/tighter than its account default. Ignored for area crowds * (those leash to the area's bounds). */export const NPC_WANDER_PROPERTY = 'town.deception.npcWander'
/** First value for `key` in a property list, or undefined when absent. */function getProperty(props: Property[] | undefined, key: string): string | undefined { return props?.find((p) => p.key === key)?.value}
/** Every value for `key` in a property list (a node may repeat a key). */function getAllProperties(props: Property[] | undefined, key: string): string[] { return props?.filter((p) => p.key === key).map((p) => p.value) ?? []}
/** One member of a node's NPC distribution: an account DID and how many to place. */export interface NpcEntry { did: string /** exact count for an area crowd; the relative weight for a marker's single pick. */ weight: number}
/** An object definition's animation play mode: the {@link OBJECT_ANIMATION_MODE_PROPERTY} * value, defaulting to `'loop'` for any unset/unrecognised value. */export function objectAnimationModeOf(props: Property[] | undefined): AnimationPlayMode { return getProperty(props, OBJECT_ANIMATION_MODE_PROPERTY) === 'pingpong' ? 'pingpong' : 'loop'}
/** Whether a placed object plays its animation; stopped only when an explicit * `'0'`/`'false'` flag is present (so absent = playing). */export function objectAnimationPlayingOf(props: Property[] | undefined): boolean { const v = getProperty(props, ANIMATION_PLAYING_PROPERTY) return v !== '0' && v !== 'false'}
/** The frame a stopped object holds: the {@link ANIMATION_FRAME_PROPERTY} value * as a non-negative integer, defaulting to 0 when unset/unparseable. */export function objectAnimationFrameOf(props: Property[] | undefined): number { const raw = getProperty(props, ANIMATION_FRAME_PROPERTY) if (raw === undefined) return 0 const n = Number(raw) return Number.isFinite(n) ? Math.max(0, Math.round(n)) : 0}
/** Whether a placed object plays its cycle once then holds (vs. loops forever). * True only when an explicit `'1'`/`'true'` flag is present (absent = looping). */export function objectAnimationOnceOf(props: Property[] | undefined): boolean { const v = getProperty(props, ANIMATION_ONCE_PROPERTY) return v === '1' || v === 'true'}
/** Whether a placed object plays its cycle backwards (vs. forwards). True only * when an explicit `'1'`/`'true'` flag is present (absent = forwards). */export function objectAnimationReverseOf(props: Property[] | undefined): boolean { const v = getProperty(props, ANIMATION_REVERSE_PROPERTY) return v === '1' || v === 'true'}
/** Whether a placed object is an auto-door / auto-gate: true only when an * explicit `'1'`/`'true'` {@link OBJECT_AUTO_GATE_PROPERTY} flag is present * (absent = a plain object). */export function objectAutoGateOf(props: Property[] | undefined): boolean { const v = getProperty(props, OBJECT_AUTO_GATE_PROPERTY) return v === '1' || v === 'true'}
/** A node's NPC distribution: each {@link NPC_TYPE_PROPERTY} value parsed into an * account DID + weight (`<did>` or `<did> <weight>`; weight defaults to 1, floored * at 1). Entries with a non-DID first token are dropped. Empty when the node is not * an NPC node. The client resolves each DID to its sprite + behaviour; the render * engine itself never spawns NPCs. */export function npcEntriesOf(props: Property[] | undefined): NpcEntry[] { const out: NpcEntry[] = [] for (const raw of getAllProperties(props, NPC_TYPE_PROPERTY)) { const [did, weightTok] = raw.trim().split(/\s+/, 2) if (!did || !did.startsWith('did:')) continue const n = weightTok === undefined ? 1 : Number(weightTok) const weight = Number.isFinite(n) && n >= 1 ? Math.round(n) : 1 out.push({ did, weight }) } return out}
/** A marker's per-placement wander-radius override (px), or undefined when unset/ * unparseable — in which case the NPC type's catalog default applies. */export function npcWanderRadiusOf(props: Property[] | undefined): number | undefined { const raw = getProperty(props, NPC_WANDER_PROPERTY) if (raw === undefined) return undefined const n = Number(raw) return Number.isFinite(n) && n >= 0 ? n : undefined}