TypeScript 97.6%
SQL 1.4%
JavaScript 0.5%
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