Back to Outbreak: Soycoon City
SOURCE / PINNED RELEASE

Made of little things.

Outbreak: Soycoon City

Release
db7750b1d009…
Author-recorded commit
3ee3f5fd27a6…
License
LICENSE
Author’s source reference
nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-b87c04571a9

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

src/controller.ts
import { createGamepadInput, type ControllerFrame, type GamepadSample, type GamepadBindings } from '../docs/examples/gamepad.js';
import type { Controls } from './game.js';
export const GAMEPAD_BINDINGS: GamepadBindings = {
  turn:[{axis:0,scale:-1},{button:14},{button:15,scale:-1}],
  forward:[{axis:1,scale:-1},{button:12},{button:13,scale:-1}],
  aim:[{button:6}],aimUp:[{axis:3,scale:-1}],shoot:[{button:7}],reload:[{button:2}],interact:[{button:0}],run:[{button:1}],pause:[{button:9}]
};
const neutral=():Controls=>({forward:0,turn:0,run:false,aim:false});
export class GameController {
  input:ReturnType<typeof createGamepadInput>;slot:number|null=null;frame:ControllerFrame|null=null;controls=neutral();
  constructor(options:{read?:()=>ArrayLike<GamepadSample|null>;active?:()=>boolean}={}) {this.input=createGamepadInput(GAMEPAD_BINDINGS,{deadZone:.2,...options});}
  poll(enabled:boolean,onAction:(action:string)=>void):void {
    this.frame=this.input.poll();this.controls=neutral();
    if(this.slot!==null&&this.frame.disconnected.includes(this.slot)){this.slot=null;onAction('controllerLost');return;}
    const available=this.frame.players.filter(p=>p.mapped);
    if(this.slot===null)this.slot=available[0]?.index??null;
    const p=available.find(p=>p.index===this.slot);if(!p||!enabled)return;
    const a=p.actions;this.controls={forward:a.forward.value,turn:a.turn.value,aim:a.aim.down,aimUp:a.aim.down&&a.aimUp.value>.4,run:a.run.down};
    for(const action of ['shoot','reload','interact','pause'])if(a[action].pressed)onAction(action);
  }
  get status():string {
    const frame=this.frame;if(!frame)return 'Controller · press a button to connect';
    const p=frame.players.find(p=>p.index===this.slot);
    if(p)return `Controller ${p.index+1} · ${frame.status==='inactive'?'click game to focus':'stick / D-pad · LT aim · RT fire'}`;
    if(frame.players.some(p=>!p.mapped))return 'Controller layout unknown · keyboard/touch available';
    if(frame.status==='blocked'||frame.status==='unavailable')return 'Controller API unavailable · keyboard/touch available';
    return 'Controller · press a button to connect';
  }
  reset():void{this.controls=neutral();this.input.reset();}
  dispose():void{this.input.dispose();this.controls=neutral();}
}
export function mergeControls(a:Controls,b:Controls):Controls {
  const strongest=(x:number,y:number)=>Math.abs(x)>=Math.abs(y)?x:y;
  return {forward:strongest(a.forward,b.forward),turn:strongest(a.turn,b.turn),run:a.run||b.run,aim:a.aim||b.aim,aimUp:Boolean(a.aimUp||b.aimUp)};
}