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.
import * as A from './anims.js';
import type { Clip } from './pose.js';
import type { WeaponDef } from './types.js';
export type AttackKind = 'light' | 'heavy' | 'dash' | 'low' | 'air' | 'airLand';
export interface AttackDef {
kind: AttackKind;
clip: Clip;
/** Hit window in clip seconds. */
active: [number, number];
damage: number;
/** Horizontal knockback speed on hit. */
knock: number;
/** Forward burst when the strike starts. */
lunge: number;
/** Freeze frames on hit, in seconds. */
hitstop: number;
/** From this clip time a buffered light/heavy press chains into the next attack. */
chain: number;
/** From this clip time movement/block may interrupt the recovery. */
recover: number;
knockdown?: boolean;
guardBreak?: boolean;
/** Chance that a clean hit knocks the victim's weapon away. */
disarm?: number;
/** Strike reaches with a foot or fist instead of the held item. */
limb?: 'fistL' | 'fistR' | 'footR';
/** Radial shockwave instead of a blade test. */
radius?: number;
}
export interface MoveSet {
grip: A.Grip;
light: AttackDef[];
heavy: AttackDef;
dash: AttackDef;
low: AttackDef;
air: AttackDef;
airLand: AttackDef;
}
function bladeMoves(clips: A.AttackClips, grip: A.Grip): MoveSet {
const [l1, l2, l3] = clips.light as [Clip, Clip, Clip];
return {
grip,
light: [
{ kind: 'light', clip: l1, active: [0.14, 0.27], damage: 7, knock: 1.6, lunge: 1.8, hitstop: 0.07, chain: 0.27, recover: 0.46 },
{ kind: 'light', clip: l2, active: [0.09, 0.23], damage: 7, knock: 1.6, lunge: 1.6, hitstop: 0.07, chain: 0.24, recover: 0.42 },
{ kind: 'light', clip: l3, active: [0.16, 0.3], damage: 11, knock: 4.2, lunge: 4.2, hitstop: 0.11, chain: 0.5, recover: 0.5 },
],
heavy: { kind: 'heavy', clip: clips.heavy, active: [0.4, 0.52], damage: 21, knock: 5.5, lunge: 2.4, hitstop: 0.16, chain: 0.9, recover: 0.86, knockdown: true, guardBreak: true, disarm: 0.4 },
dash: { kind: 'dash', clip: clips.dash, active: [0.08, 0.22], damage: 12, knock: 4, lunge: 3.5, hitstop: 0.1, chain: 0.6, recover: 0.48, disarm: 0.15 },
low: { kind: 'low', clip: clips.low, active: [0.1, 0.25], damage: 7, knock: 1.2, lunge: 1.2, hitstop: 0.08, chain: 0.5, recover: 0.48, knockdown: true },
air: { kind: 'air', clip: clips.air, active: [0.1, 99], damage: 15, knock: 3, lunge: 0, hitstop: 0.12, chain: 99, recover: 99, knockdown: true, disarm: 0.25 },
airLand: { kind: 'airLand', clip: clips.airLand, active: [0, 0.1], damage: 6, knock: 3.5, lunge: 0, hitstop: 0.08, chain: 0.6, recover: 0.42, radius: 0.95, knockdown: true },
};
}
function fistMoves(): MoveSet {
const c = A.FIST_CLIPS;
const [jab, cross, kick] = c.light as [Clip, Clip, Clip];
return {
grip: 'fists',
light: [
{ kind: 'light', clip: jab, active: [0.07, 0.15], damage: 4, knock: 1, lunge: 1.4, hitstop: 0.05, chain: 0.15, recover: 0.3, limb: 'fistL' },
{ kind: 'light', clip: cross, active: [0.08, 0.17], damage: 5, knock: 1.4, lunge: 1.6, hitstop: 0.06, chain: 0.18, recover: 0.34, limb: 'fistR' },
{ kind: 'light', clip: kick, active: [0.13, 0.25], damage: 8, knock: 4, lunge: 1.8, hitstop: 0.1, chain: 0.4, recover: 0.4, limb: 'footR', disarm: 0.2 },
],
heavy: { kind: 'heavy', clip: c.heavy, active: [0.22, 0.31], damage: 11, knock: 6.5, lunge: 2, hitstop: 0.13, chain: 0.6, recover: 0.5, limb: 'footR', guardBreak: true, knockdown: true, disarm: 0.35 },
dash: { kind: 'dash', clip: c.dash, active: [0.1, 0.19], damage: 7, knock: 3.5, lunge: 3, hitstop: 0.08, chain: 0.45, recover: 0.4, limb: 'fistR' },
low: { kind: 'low', clip: c.low, active: [0.1, 0.21], damage: 5, knock: 1, lunge: 0.8, hitstop: 0.07, chain: 0.45, recover: 0.4, limb: 'footR', knockdown: true },
air: { kind: 'air', clip: c.air, active: [0.06, 99], damage: 8, knock: 3.5, lunge: 0, hitstop: 0.09, chain: 99, recover: 99, limb: 'footR', knockdown: true },
airLand: { kind: 'airLand', clip: c.airLand, active: [0, 0], damage: 0, knock: 0, lunge: 0, hitstop: 0, chain: 0.2, recover: 0.15 },
};
}
/** Nunchaku: quick whipping sweeps that knock back hard, a twirling overhead smash. */
function flailMoves(): MoveSet {
const set = bladeMoves(A.FLAIL_CLIPS, 'one');
set.light = set.light.map((m, i) => ({ ...m, active: (i === 2 ? [0.17, 0.3] : [0.12, 0.24]) as [number, number], knock: m.knock * 1.3, chain: i === 2 ? 0.45 : 0.26 }));
set.heavy = { ...set.heavy, active: [0.44, 0.58], chain: 0.85, recover: 0.82 };
set.dash = { ...set.dash, active: [0.09, 0.2] };
set.low = { ...set.low, active: [0.12, 0.24] };
set.air = { ...set.air, active: [0.1, 99] };
return set;
}
const KATANA_SET = bladeMoves(A.KATANA_CLIPS, 'katana');
const FLAIL_SET = flailMoves();
const ONE_SET = bladeMoves(A.ONE_CLIPS, 'one');
const STAB_TWO_SET = bladeMoves(A.STAB_TWO, 'katana');
const STAB_ONE_SET = bladeMoves(A.STAB_ONE, 'one');
export const FIST_SET = fistMoves();
// Thrust-first sets need the thrust's timing on their first and last light attacks.
for (const set of [STAB_TWO_SET, STAB_ONE_SET]) {
const thrust = (i: number): AttackDef => ({ ...set.light[i]!, active: [0.16, 0.3], damage: 9, knock: 3, lunge: 3.6, chain: 0.4, recover: 0.5 });
set.light[0] = thrust(0);
set.light[2] = thrust(2);
}
export function movesFor(def: WeaponDef | null): MoveSet {
if (!def) return FIST_SET;
if (def.flail) return FLAIL_SET;
if (def.style === 'stab') return def.twoHanded ? STAB_TWO_SET : STAB_ONE_SET;
if (def.style === 'katana' || def.twoHanded) return KATANA_SET;
return ONE_SET;
}
