- Second week: URA_DAYS (8 harder orders, ramps 1.4-2.0, 18s rush, 7-layer SHOGUN'S TOWER finale), judged by the SHOGUN — new same-seed portrait set (oni mask, gold on blood-red washi; Samurai.who prefixes portraits, plus shogun_bow for an S). Ura is played entirely by moonlight. - New pantry where the SHAPES are the difficulty: nori (96x8 paper-thin), ebi (asymmetric convex 7-gon), daikon (46x64 comically tall). - Flow: KANSEI -> 'the SHOGUN is hungry' -> ura week; ura KANSEI = SHIN-KANSEI (true ending) -> quiet kitchen. Unlock persists; title gets a 表/裏 toggle + red ura stamp column (bests at keys 100+). - Kabuki curtain_red reveal on day advances (procedural fallback; death retries skip it). - Verified: base SSSSSSSS; ura A A S S A S S S (all beatable, honestly harder); full arc kansei->ura->shin-kansei->base in-browser; M1-M3 + disaster regression identical. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
89 lines
4.3 KiB
TypeScript
89 lines
4.3 KiB
TypeScript
import type { Stage } from '../scenes/stage';
|
|
import { centroid, type V } from './geom';
|
|
|
|
/** toastsim-style material params. resistance raises the cut threshold; moisture
|
|
* drives the splat; wobble is jelly-class jiggle (soft, deferred to when a
|
|
* jelly food actually ships). */
|
|
export interface Material {
|
|
resistance: number; // 0..1 skin toughness
|
|
moisture: number; // 0..1 wet food splats
|
|
wobble: number; // 0..1 jiggle
|
|
}
|
|
|
|
export interface FoodDef {
|
|
verts: V[]; // convex, centred on origin
|
|
material: Material;
|
|
color: number;
|
|
face: string; // generated face sprite id (rendered later; gameplay is the silhouette)
|
|
}
|
|
|
|
const circle = (r: number, n: number, squashY = 1): V[] =>
|
|
Array.from({ length: n }, (_, i) => {
|
|
const a = (i / n) * Math.PI * 2;
|
|
return { x: Math.cos(a) * r, y: Math.sin(a) * r * squashY };
|
|
});
|
|
|
|
export const box = (w: number, h: number): V[] => {
|
|
const x = w / 2, y = h / 2, c = Math.min(x, y) * 0.4; // chamfered corners → convex octagon
|
|
return [
|
|
{ x: -x + c, y: -y }, { x: x - c, y: -y }, { x, y: -y + c }, { x, y: y - c },
|
|
{ x: x - c, y }, { x: -x + c, y }, { x: -x, y: y - c }, { x: -x, y: -y + c },
|
|
];
|
|
};
|
|
|
|
export const FOODS: Record<string, FoodDef> = {
|
|
tomato: { verts: circle(34, 12), material: { resistance: 0.25, moisture: 0.85, wobble: 0.15 }, color: 0x151515, face: 'face_tomato' },
|
|
bread: { verts: box(78, 52), material: { resistance: 0.35, moisture: 0.05, wobble: 0.0 }, color: 0x1c150e, face: 'face_bread' },
|
|
cheese: { verts: box(60, 44), material: { resistance: 0.45, moisture: 0.15, wobble: 0.0 }, color: 0x191510, face: 'face_cheese' },
|
|
katsu: { verts: box(86, 46), material: { resistance: 0.55, moisture: 0.1, wobble: 0.0 }, color: 0x171008, face: 'face_katsu' },
|
|
egg: { verts: circle(28, 10, 1.15), material: { resistance: 0.3, moisture: 0.4, wobble: 0.1 }, color: 0x121212, face: 'face_egg' },
|
|
// ponytail: wobble is a material param only — real jelly-cluster jiggle waits
|
|
// until a day needs it to be funny. Tofu cuts easy but splats if pressed.
|
|
tofu: { verts: box(64, 46), material: { resistance: 0.12, moisture: 0.6, wobble: 0.4 }, color: 0x1a1a18, face: 'face_egg' },
|
|
// --- the ura pantry ---
|
|
nori: { verts: box(96, 8), material: { resistance: 0.06, moisture: 0.02, wobble: 0 }, color: 0x0d0f0c, face: 'face_bread' },
|
|
ebi: { verts: [{ x: -36, y: 2 }, { x: -22, y: -12 }, { x: 4, y: -15 }, { x: 27, y: -9 }, { x: 36, y: 3 }, { x: 18, y: 14 }, { x: -12, y: 15 }], material: { resistance: 0.4, moisture: 0.3, wobble: 0.05 }, color: 0x161210, face: 'face_katsu' },
|
|
daikon: { verts: box(46, 64), material: { resistance: 0.3, moisture: 0.5, wobble: 0 }, color: 0x181816, face: 'face_egg' },
|
|
};
|
|
|
|
let NEXT = 1;
|
|
|
|
/** A food body + its material and origin colour. Pieces are Foods too. */
|
|
export class Food {
|
|
readonly id: string;
|
|
constructor(
|
|
readonly body: MatterJS.BodyType,
|
|
readonly material: Material,
|
|
readonly color: number,
|
|
readonly face: string,
|
|
readonly kind: string,
|
|
) {
|
|
this.id = `${kind}${NEXT++}`;
|
|
}
|
|
}
|
|
|
|
/** Build a food body from centred local verts placed at world (x,y). Registers
|
|
* the silhouette on the stage. Shared by spawn and by the cutter's pieces. */
|
|
export function makeFood(stage: Stage, local: V[], x: number, y: number, mat: Material, color: number, face: string, kind: string): Food {
|
|
const body = stage.matter.add.fromVertices(x, y, local, {
|
|
friction: 0.6, frictionAir: 0.01, density: 0.0009, restitution: 0.05,
|
|
}) as MatterJS.BodyType;
|
|
// fromVertices recentres COM; render verts relative to that centre.
|
|
const c = centroid(local);
|
|
const rel = local.map((v) => ({ x: v.x - c.x, y: v.y - c.y }));
|
|
stage.addPoly(body, rel, color);
|
|
// a comical face — whole foods only (pieces are pieces). `p`-suffixed kinds
|
|
// are cut pieces; don't give a severed half a startled face.
|
|
if (!kind.endsWith('p')) {
|
|
const xs = local.map((v) => v.x), ys = local.map((v) => v.y);
|
|
stage.addFace(body, Math.max(...xs) - Math.min(...xs), Math.max(...ys) - Math.min(...ys));
|
|
}
|
|
return new Food(body, mat, color, face, kind);
|
|
}
|
|
|
|
/** Spawn a whole food of `kind` at x, resting just above the floor. */
|
|
export function spawnFood(stage: Stage, kind: string, x: number, y: number): Food {
|
|
const def = FOODS[kind] ?? FOODS.tomato;
|
|
return makeFood(stage, def.verts, x, y, def.material, def.color, def.face, kind);
|
|
}
|