Back to Mini Samurai
SOURCE / PINNED RELEASE

Made of little things.

Mini Samurai

Release
a7beb798adbc…
Author-recorded commit
4f97be1515b8…
License
LICENSE
Author’s source reference
nostr://npub12lm3rxsva8kfempyjkxupt09e02n3fezurkyadyqfejdc5e0r2kscqty6r/wss%3A%2F%2Fgit.napplet.soy%2F/n-3b28f0efc5b

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

src/game/controls.ts
/**
 * Control bindings: the keyboard (player 1), one button layout shared by every
 * controller, and whether player 1 also plays with a controller. Controllers join as
 * players 2-4 by pressing any button. Everything here can be changed on the Controls screen.
 */

export const ACTIONS = [
  'up',
  'down',
  'left',
  'right',
  'jump',
  'run',
  'crouch',
  'light',
  'heavy',
  'block',
  'grab',
  'roll',
  'turnLeft',
  'turnRight',
] as const;
export type Action = (typeof ACTIONS)[number];

/** Gamepad actions that use buttons (movement and camera are on the sticks and D-pad). */
export const PAD_ACTIONS = ['jump', 'light', 'heavy', 'block', 'grab', 'roll', 'run', 'crouch'] as const;
export type PadAction = (typeof PAD_ACTIONS)[number];

export const ACTION_LABELS: Record<Action, string> = {
  up: 'Move forward / climb',
  down: 'Move back / drop',
  left: 'Move left',
  right: 'Move right',
  jump: 'Jump / climb',
  run: 'Run',
  crouch: 'Crouch',
  light: 'Attack / throw',
  heavy: 'Heavy attack',
  block: 'Block / parry',
  grab: 'Pick up / put down',
  roll: 'Roll',
  turnLeft: 'Turn camera left',
  turnRight: 'Turn camera right',
};

export type KeyLayout = Record<Action, string[]>;
export type PadLayout = Record<PadAction, number[]>;

export interface ControlSettings {
  /** Player 1's keyboard keys (player 1 also has the mouse). */
  keys: KeyLayout;
  /** Buttons shared by every controller. */
  pad: PadLayout;
  /** The first controller to join plays as player 1 too (for playing alone with a controller). */
  p1Pad: boolean;
}

export function defaultControls(): ControlSettings {
  return {
    keys: {
      up: ['KeyW'],
      down: ['KeyS'],
      left: ['KeyA'],
      right: ['KeyD'],
      jump: ['Space'],
      run: ['ShiftLeft'],
      crouch: ['ControlLeft', 'KeyC'],
      // Attack and heavy are also the left and right mouse buttons.
      light: ['KeyJ'],
      heavy: ['KeyK'],
      block: ['KeyF', 'KeyL'],
      grab: ['KeyE'],
      roll: ['KeyR'],
      turnLeft: ['KeyZ'],
      turnRight: ['KeyX'],
    },
    // Standard (Xbox-style) layout: A jump, X attack, Y heavy, RB block, B pick up,
    // RT roll, LB run, LT crouch. Left stick/D-pad move, right stick turns and tilts the camera.
    pad: {
      jump: [0],
      light: [2],
      heavy: [3],
      block: [5],
      grab: [1],
      roll: [7, 11],
      run: [4, 10],
      crouch: [6],
    },
    p1Pad: false,
  };
}

/** Accept saved settings only if they have the right shape; fill anything missing from the defaults. */
export function parseControls(json: string | null): ControlSettings {
  const base = defaultControls();
  if (!json) return base;
  try {
    const saved = JSON.parse(json) as Record<string, unknown>;
    const strings = (v: unknown): v is string[] => Array.isArray(v) && v.every((s) => typeof s === 'string');
    const numbers = (v: unknown): v is number[] => Array.isArray(v) && v.every((n) => Number.isInteger(n) && n >= 0 && n < 32);
    // Older saves kept two keyboard layouts; only player 1's (the first) is used now.
    const keys = (Array.isArray(saved.keys) ? saved.keys[0] : saved.keys) as Record<string, unknown> | undefined;
    if (keys) for (const a of ACTIONS) if (strings(keys[a])) base.keys[a] = keys[a];
    const pad = saved.pad as Record<string, unknown> | undefined;
    if (pad) for (const a of PAD_ACTIONS) if (numbers(pad[a])) base.pad[a] = pad[a];
    if (typeof saved.p1Pad === 'boolean') base.p1Pad = saved.p1Pad;
  } catch {
    // Corrupt settings: defaults.
  }
  return base;
}

const KEY_NAMES: Record<string, string> = {
  Space: 'Space',
  Enter: 'Enter',
  ShiftLeft: 'Left Shift',
  ShiftRight: 'Right Shift',
  ControlLeft: 'Left Ctrl',
  ControlRight: 'Right Ctrl',
  AltLeft: 'Left Alt',
  AltRight: 'Right Alt',
  ArrowUp: '↑',
  ArrowDown: '↓',
  ArrowLeft: '←',
  ArrowRight: '→',
  Period: '.',
  Comma: ',',
  Slash: '/',
  Backslash: '\\',
  Semicolon: ';',
  Quote: "'",
  BracketLeft: '[',
  BracketRight: ']',
  Minus: '-',
  Equal: '=',
  Backquote: '`',
  Tab: 'Tab',
  Backspace: 'Backspace',
  CapsLock: 'Caps Lock',
  NumpadDecimal: 'Num .',
  NumpadEnter: 'Num Enter',
  NumpadAdd: 'Num +',
  NumpadSubtract: 'Num -',
  NumpadMultiply: 'Num *',
  NumpadDivide: 'Num /',
};

export function keyName(code: string): string {
  if (KEY_NAMES[code]) return KEY_NAMES[code]!;
  if (code.startsWith('Key')) return code.slice(3);
  if (code.startsWith('Digit')) return code.slice(5);
  if (code.startsWith('Numpad')) return `Num ${code.slice(6)}`;
  return code;
}

const PAD_NAMES = ['A', 'B', 'X', 'Y', 'LB', 'RB', 'LT', 'RT', 'View', 'Menu', 'L-stick click', 'R-stick click', 'D-pad ↑', 'D-pad ↓', 'D-pad ←', 'D-pad →', 'Home'];

export function padButtonName(index: number): string {
  return PAD_NAMES[index] ?? `Button ${index}`;
}

/** Keys the game never lets you bind (they pause / leave rebinding). */
export const RESERVED_KEYS = new Set(['Escape', 'KeyP']);