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.
/**
* 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']);
