export type ChildMotionWarmupStepId = | 'center_arrive' | 'wave_greeting' | 'warmup_intro' | 'move_left' | 'return_center_1' | 'move_right' | 'return_center_2' | 'wave_left_hand' | 'wave_right_hand' | 'warmup_finish' | 'level_select'; export type ChildMotionWarmupTarget = 'center' | 'left' | 'right'; export type ChildMotionWarmupStepKind = | 'position' | 'gesture' | 'narration' | 'finish' | 'levelSelect'; export type ChildMotionWarmupStep = { id: ChildMotionWarmupStepId; kind: ChildMotionWarmupStepKind; title: string; spokenLines: string[]; target?: ChildMotionWarmupTarget; }; export type ChildMotionPoint = { x: number; y: number; isRaised?: boolean; isArmExtended?: boolean; armAngleDeg?: number; armReach?: number; }; export type ChildMotionHandSpace = { minX: number; maxX: number; minY: number; maxY: number; minAngleDeg: number | null; maxAngleDeg: number | null; maxReach: number | null; }; export type ChildMotionWarmupCalibration = { leftBoundary: number | null; rightBoundary: number | null; leftHandPath: ChildMotionPoint[]; rightHandPath: ChildMotionPoint[]; leftHandSpace: ChildMotionHandSpace | null; rightHandSpace: ChildMotionHandSpace | null; }; export type ChildMotionWarmupCompletion = | { type: 'position'; avatarX: number; } | { type: 'left-hand'; path: ChildMotionPoint[]; } | { type: 'right-hand'; path: ChildMotionPoint[]; } | { type: 'narration'; }; export const CHILD_MOTION_CENTER_X = 0.5; export const CHILD_MOTION_LEFT_X = 0.34; export const CHILD_MOTION_RIGHT_X = 0.66; export const CHILD_MOTION_POSITION_EPSILON = 0.045; export const CHILD_MOTION_HOLD_DURATION_MS = 2000; export const CHILD_MOTION_NARRATION_DURATION_MS = 900; export const CHILD_MOTION_FINISH_DURATION_MS = 1200; export const CHILD_MOTION_WARMUP_STEPS: ChildMotionWarmupStep[] = [ { id: 'center_arrive', kind: 'position', title: '来到圆圈这里', spokenLines: ['欢迎你,小朋友,见到你真开心', '来圆圈这里和我打个招呼吧'], target: 'center', }, { id: 'wave_greeting', kind: 'gesture', title: '打个招呼', spokenLines: ['来圆圈这里和我打个招呼吧'], }, { id: 'warmup_intro', kind: 'narration', title: '准备热身', spokenLines: ['你好呀小朋友,为了你玩的安全和开心,先来和我一起热个身吧'], }, { id: 'move_left', kind: 'position', title: '向左一步', spokenLines: ['向左一步'], target: 'left', }, { id: 'return_center_1', kind: 'position', title: '回到中间来', spokenLines: ['回到中间来'], target: 'center', }, { id: 'move_right', kind: 'position', title: '向右一步', spokenLines: ['向右一步'], target: 'right', }, { id: 'return_center_2', kind: 'position', title: '回到中间来', spokenLines: ['回到中间来'], target: 'center', }, { id: 'wave_left_hand', kind: 'gesture', title: '挥动左手', spokenLines: ['挥动左手'], }, { id: 'wave_right_hand', kind: 'gesture', title: '挥动右手', spokenLines: ['挥动右手'], }, { id: 'warmup_finish', kind: 'finish', title: '热身完成', spokenLines: ['真厉害,你是我见过最聪明的小朋友', '别走开,现在开始我们的游戏吧'], }, { id: 'level_select', kind: 'levelSelect', title: '准备开始', spokenLines: ['现在开始我们的游戏吧'], }, ]; const STEP_BY_ID = new Map( CHILD_MOTION_WARMUP_STEPS.map((step) => [step.id, step]), ); const NEXT_STEP_BY_ID = new Map( CHILD_MOTION_WARMUP_STEPS.slice(0, -1).map((step, index) => [ step.id, CHILD_MOTION_WARMUP_STEPS[index + 1]!.id, ]), ); let childMotionWarmupCompletedInRuntime = false; export function getChildMotionWarmupStep(id: ChildMotionWarmupStepId) { return STEP_BY_ID.get(id) ?? CHILD_MOTION_WARMUP_STEPS[0]!; } export function getChildMotionTargetX(target: ChildMotionWarmupTarget) { if (target === 'left') { return CHILD_MOTION_LEFT_X; } if (target === 'right') { return CHILD_MOTION_RIGHT_X; } return CHILD_MOTION_CENTER_X; } export function isAvatarOnWarmupTarget( step: ChildMotionWarmupStep, avatarX: number, ) { if (step.kind !== 'position' || !step.target) { return false; } return ( Math.abs(avatarX - getChildMotionTargetX(step.target)) <= CHILD_MOTION_POSITION_EPSILON ); } export function resolveNextChildMotionWarmupStep( stepId: ChildMotionWarmupStepId, ) { return NEXT_STEP_BY_ID.get(stepId) ?? stepId; } export function createEmptyChildMotionCalibration(): ChildMotionWarmupCalibration { return { leftBoundary: null, rightBoundary: null, leftHandPath: [], rightHandPath: [], leftHandSpace: null, rightHandSpace: null, }; } function resolveChildMotionHandSpace( path: ChildMotionPoint[], ): ChildMotionHandSpace | null { if (path.length === 0) { return null; } const xValues = path.map((point) => point.x); const yValues = path.map((point) => point.y); const angleValues = path .map((point) => point.armAngleDeg) .filter((angle): angle is number => typeof angle === 'number'); const reachValues = path .map((point) => point.armReach) .filter((reach): reach is number => typeof reach === 'number'); return { minX: Math.min(...xValues), maxX: Math.max(...xValues), minY: Math.min(...yValues), maxY: Math.max(...yValues), minAngleDeg: angleValues.length > 0 ? Math.min(...angleValues) : null, maxAngleDeg: angleValues.length > 0 ? Math.max(...angleValues) : null, maxReach: reachValues.length > 0 ? Math.max(...reachValues) : null, }; } export function applyChildMotionWarmupCompletion( stepId: ChildMotionWarmupStepId, calibration: ChildMotionWarmupCalibration, completion: ChildMotionWarmupCompletion, ): ChildMotionWarmupCalibration { if (stepId === 'move_left' && completion.type === 'position') { return { ...calibration, leftBoundary: Math.max(0, CHILD_MOTION_CENTER_X - completion.avatarX), }; } if (stepId === 'move_right' && completion.type === 'position') { return { ...calibration, rightBoundary: Math.max(0, completion.avatarX - CHILD_MOTION_CENTER_X), }; } if (stepId === 'wave_left_hand' && completion.type === 'left-hand') { return { ...calibration, leftHandPath: completion.path, leftHandSpace: resolveChildMotionHandSpace(completion.path), }; } if (stepId === 'wave_right_hand' && completion.type === 'right-hand') { return { ...calibration, rightHandPath: completion.path, rightHandSpace: resolveChildMotionHandSpace(completion.path), }; } return calibration; } export function hasCompletedChildMotionWarmupInRuntime() { return childMotionWarmupCompletedInRuntime; } export function markChildMotionWarmupCompletedInRuntime() { childMotionWarmupCompletedInRuntime = true; } export function resetChildMotionWarmupRuntimeSession() { childMotionWarmupCompletedInRuntime = false; }