SPB Git forge

spb/spinza

Public
8commits 1branches 0releases
1.6 MBsize
maindefault branch
16 days agolast push
TypeScript 97.6% SQL 1.4% JavaScript 0.5%
4.0 KB · 122 lines typescript
Raw Blame History
1import type { Rng } from "../rng";2import type { ArcadeGameDefinition, ArcadeOutcome } from "./types";34/**5 * ORBIT — objects circle a core on concentric orbits. The player fires an6 * impulse at an angle; on each orbit the impulse can hit an object (angular7 * proximity), collecting its value, deflecting, or spawning a new orbit.8 * Supernova collapses every orbit and turns the remaining objects into9 * multipliers of the collected total.10 */1112export interface OrbitConfig {13  orbits: number;14  objectsPerOrbit: [number, number];15  /** Angular half-width of the impulse in degrees (hit tolerance). */16  beamHalfWidth: number;17  objectTypes: { id: string; label: string; value: number; weight: number; deflect?: number; spawn?: boolean }[];18  supernovaChance: number;19  maxOrbits: number;20}2122export interface OrbitObject {23  id: string;24  type: string;25  angle: number;26  value: number;27}2829export interface OrbitStep {30  orbit: number;31  angle: number;32  hit: OrbitObject | null;33  deflectedTo?: number;34  spawned?: boolean;35  collected: number;36}3738function angleDiff(a: number, b: number): number {39  const d = Math.abs(((a - b) % 360 + 540) % 360 - 180);40  return d;41}4243export function resolveOrbit(def: ArcadeGameDefinition, bet: number, rng: Rng, inputRaw: { angle?: number }): ArcadeOutcome {44  const cfg = def.config as unknown as OrbitConfig;45  let angle = ((inputRaw.angle ?? rng.int(360)) % 360 + 360) % 360;46  const orbits: OrbitObject[][] = [];47  let seq = 0;48  const makeOrbit = (): OrbitObject[] => {49    const n = cfg.objectsPerOrbit[0] + rng.int(cfg.objectsPerOrbit[1] - cfg.objectsPerOrbit[0] + 1);50    const objs: OrbitObject[] = [];51    for (let i = 0; i < n; i++) {52      const t = cfg.objectTypes[rng.weighted(cfg.objectTypes.map((o) => o.weight))];53      objs.push({ id: `o${seq++}`, type: t.id, angle: rng.float() * 360, value: t.value });54    }55    return objs;56  };57  for (let i = 0; i < cfg.orbits; i++) orbits.push(makeOrbit());58  const steps: OrbitStep[] = [];59  const features: string[] = [];60  let collected = 0;61  let i = 0;62  while (i < orbits.length && i < cfg.maxOrbits) {63    const objs = orbits[i];64    let hit: OrbitObject | null = null;65    let best = Infinity;66    for (const o of objs) {67      const d = angleDiff(o.angle, angle);68      if (d <= cfg.beamHalfWidth && d < best) {69        best = d;70        hit = o;71      }72    }73    const step: OrbitStep = { orbit: i, angle, hit, collected };74    if (hit) {75      const t = cfg.objectTypes.find((o) => o.id === hit!.type)!;76      collected += hit.value;77      step.collected = collected;78      if (t.deflect) {79        angle = (angle + (rng.float() - 0.5) * 2 * t.deflect + 360) % 360;80        step.deflectedTo = angle;81        features.push("Deflection");82      }83      if (t.spawn && orbits.length < cfg.maxOrbits) {84        orbits.push(makeOrbit());85        step.spawned = true;86        features.push("New orbit");87      }88      features.push(t.label);89    }90    steps.push(step);91    i++;92  }93  let supernova: { remaining: number; multiplier: number } | null = null;94  const hitIds = new Set(steps.filter((s) => s.hit).map((s) => s.hit!.id));95  if (collected > 0 && rng.chance(cfg.supernovaChance)) {96    const remaining = orbits.flat().filter((o) => !hitIds.has(o.id)).length;97    const mult = 1 + remaining * 0.5;98    supernova = { remaining, multiplier: mult };99    collected *= mult;100    features.push("Supernova");101  }102  const scaled = collected * def.payScale;103  const capped = scaled > def.maxMultiplier;104  const multiplier = Math.min(def.maxMultiplier, scaled);105  const totalWin = Math.round(bet * multiplier);106  return {107    game: def.slug,108    version: def.version,109    bet,110    totalWin,111    multiplier: bet ? totalWin / bet : 0,112    capped,113    features: [...new Set(features)],114    steps,115    summary: { angle: inputRaw.angle ?? null, orbits: orbits.map((o) => o.map((x) => ({ ...x, value: Math.round(x.value * def.payScale * 100) / 100 }))), supernova, hits: hitIds.size },116  };117}118119export function randomOrbitInput(_def: ArcadeGameDefinition, rng: Rng): { angle: number } {120  return { angle: rng.int(360) };121}122