import { preparedFillHeight } from './JellyFillPile'; import type { FillKind } from './JellyFillKinds'; import type { FillContainerKind } from './JellyFillContainers'; import type { Stars } from './Stars'; export { FILL_LAYOUT } from './JellyFillPile'; export interface JellyFillDefinition { target: number; interval: number; fall: number; containers?: number; fillWindow?: number; portions?: readonly number[]; kinds?: readonly FillKind[]; containerShape?: FillContainerKind; containerShapes?: readonly FillContainerKind[] } export type FillSignal = 'idle' | 'near' | 'target' | 'over' | 'missed'; export const JELLY_FILL: Readonly = { target: 12, interval: 0.14, fall: 0.36, fillWindow: 4.1 }; export const FILL_SPILL_DURATION = 0.6; export const fillBodyLimit = (definition: JellyFillDefinition): number => definition.target + 24; /** 목표 개수의 ±2개까지 통과한다. 혼합 크기도 실제 젤리 개수로 여유를 준다. */ export function fillStars(count: number, target: number): Stars { if (count === 0) return 0; const difference = Math.abs(count - target); return difference === 0 ? 3 : difference === 1 ? 2 : difference === 2 ? 1 : 0; } export const FILL_HANDOFF_SECONDS = 0.7; /** 양끝의 이동 속도를 보존해 대기 통을 멈춤 없이 현재 통으로 인계한다. */ export function fillConveyorPositions(beltOffset: number, departure: number, hasNext: boolean, handoff: boolean, speed = 1.8 / 4.46) { const progress = Math.min(1, Math.max(0, (beltOffset + 0.9) / 1.8)); const waiting = -2.9 + 1.6 * progress; const k = Math.min(1, Math.max(0, departure)), duration = handoff ? FILL_HANDOFF_SECONDS : 0.4; const move = (a: number, b: number, v0: number, v1: number) => (2*k*k*k-3*k*k+1)*a + (k*k*k-2*k*k+k)*v0*duration + (-2*k*k*k+3*k*k)*b + (k*k*k-k*k)*v1*duration; return { current: move(beltOffset, beltOffset + 3.8, speed, speed), next: !hasNext ? null : handoff ? move(waiting, -0.9, speed*1.6/1.8, speed) : waiting }; } /** 누름을 유지할수록 투입구가 열린다. 목표량에 따른 자동 감속은 하지 않는다. */ export function fillInterval(definition: JellyFillDefinition, heldFor: number): number { // 기본 정의에서는 약한 투입0.5초 ↔ 강한 투입0.1초로5배 차이를 낸다. return definition.interval / 1.4 * (5 - 4 * Math.min(1, Math.max(0, heldFor) / 0.9)); } /** 벨트·기둥·투입구의 외곽 표본. 카메라와 기능 검사가 같은 범위를 사용한다. */ export const FILL_FRAME_POINTS: readonly (readonly [number, number, number])[] = [ ...[-2.9, 2.9].flatMap(x => [-1.18, 1.18].map(z => [x, 0, z] as const)), ...[-1.65, 1.65].map(x => [x, 3.42, -1.15] as const), ...[-0.66, 0.66].flatMap(x => [-0.66, 0.66].map(z => [x, 3.58, z] as const)), ]; /** 목표선은 완성 더미 기준에 맞추되 진행 높이는 양에 비례해 단조 증가한다. */ export function fillHeight(amount: number, target = JELLY_FILL.target, portions?: readonly number[], kinds?: readonly FillKind[], containerShape?: FillContainerKind): number { // 준비 중의 HUD는 0%다. 준비를 끝내기 전에는 높이 조회로 무거운 외곽 계산을 하지 않는다. const targetHeight = preparedFillHeight(target, portions, kinds, containerShape) ?? 1; return amount / target * targetHeight; }