Something went wrong. Try again.
A Social Deduction / Deception Game
Something went wrong. Try again.
5.3 kB · 118 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119// ---------------------------------------------------------------------------// Cosmetic auto-gates — a door/gate that animates open when a sprite *paths// through* it and closes once it has passed, purely for flavour. It is NOT an// obstacle: the nav grid keeps gate cells permanently passable, so A* paths// never change and nothing desyncs. (If gates ever need to actually block — "the// saboteur left it open" as evidence — that's the server-authoritative,// dynamic-nav escalation; `setObjectSolid` in scene.ts is the runtime collision// hook for that future.)//// Trigger = PATH CROSSING, not proximity. The gate opens only when a supplied// path crosses its doorway segment within a short look-ahead, so:// • a player whose A* route goes through the doorway opens it (just in time),// • a player walking *past* it (a route that never crosses) does NOT,// • NPCs never open it — the caller simply doesn't pass their paths.// Backends that ship A* paths down for interpolation feed those straight in.//// Openness (0 closed → 1 open) is scrubbed across the object's animation frames// (not Pixi play/reverse), so it's fully interruptible. Author the gate object// as a plain forward animation: frame 0 closed, last frame open.// ---------------------------------------------------------------------------
import type { ObjectInstance } from './scene'
interface Point { x: number y: number}
export interface GateOptions { /** the doorway segment a path must cross to open. When omitted, a segment of * length `span` is built across the object's anchor along `axis`. */ threshold?: readonly [Point, Point] /** doorway width in px (only used when `threshold` is omitted). Default 48. */ span?: number /** auto-doorway orientation: `'x'` = a horizontal bar crossed by vertical * movement, `'y'` = a vertical bar. Default `'x'`. */ axis?: 'x' | 'y' /** doorway centre when `threshold` is omitted. Default the object's anchor. */ at?: Point /** open when the crossing is within this much path-distance ahead (px), so the * gate is open by the time the sprite arrives. Default 110. */ lookahead?: number /** full open (or close) sweep time, ms. Default 220. */ sweepMs?: number /** stay open this long after the crossing leaves the path, ms — covers the * sprite's body still clearing the doorway. Default 350. */ lingerMs?: number}
export interface Gate { readonly object: ObjectInstance /** 0 = fully closed (frame 0), 1 = fully open (last frame). */ readonly openness: number /** drive the gate from forward polylines. Pass ONLY the paths that may open it * (player paths) — each polyline is `[current pos, ...remaining waypoints]`, * i.e. build it as `[{ x: e.x, y: e.y }, ...e.path]`. Omit NPC paths and they * never open it. */ update(dtMs: number, paths: readonly (readonly Point[])[]): void}
/** Make a placed animated object behave as a path-triggered auto-gate. The * object stays non-colliding (nav owns passability); this only scrubs frames. */export function createGate(object: ObjectInstance, opts: GateOptions = {}): Gate { const span = opts.span ?? 48 const at = opts.at ?? { x: object.node.pos.x, y: object.node.pos.y } const axis = opts.axis ?? 'x' const threshold: readonly [Point, Point] = opts.threshold ?? (axis === 'x' ? [{ x: at.x - span / 2, y: at.y }, { x: at.x + span / 2, y: at.y }] : [{ x: at.x, y: at.y - span / 2 }, { x: at.x, y: at.y + span / 2 }]) const lookahead = opts.lookahead ?? 110 const sweepMs = Math.max(1, opts.sweepMs ?? 220) const lingerMs = opts.lingerMs ?? 350 const lastFrame = Math.max(0, object.sprite.textures.length - 1)
let openness = 0 let linger = 0 object.sprite.gotoAndStop(0) // start closed; we own the frame from here
return { object, get openness() { return openness }, update(dtMs, paths) { const wantOpen = paths.some((p) => pathCrosses(p, threshold[0], threshold[1], lookahead)) linger = wantOpen ? lingerMs : Math.max(0, linger - dtMs) const target = wantOpen || linger > 0 ? 1 : 0 const delta = dtMs / sweepMs openness = target > openness ? Math.min(target, openness + delta) : Math.max(target, openness - delta) object.sprite.gotoAndStop(Math.round(openness * lastFrame)) }, }}
/** Does the forward polyline cross segment a–b within `lookahead` px of its * start? Walks segments accumulating path-distance and bails once the start of * a segment is already beyond the look-ahead. */function pathCrosses(path: readonly Point[], a: Point, b: Point, lookahead: number): boolean { let dist = 0 for (let i = 0; i + 1 < path.length; i++) { if (dist > lookahead) return false const p = path[i]! const q = path[i + 1]! if (segmentsCross(p, q, a, b)) return true dist += Math.hypot(q.x - p.x, q.y - p.y) } return false}
/** Proper intersection of segments p1p2 and p3p4 (touching/collinear = false). */function segmentsCross(p1: Point, p2: Point, p3: Point, p4: Point): boolean { const side = (a: Point, b: Point, c: Point) => Math.sign((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)) return side(p3, p4, p1) !== side(p3, p4, p2) && side(p1, p2, p3) !== side(p1, p2, p4)}