294 lines
7.1 KiB
TypeScript
294 lines
7.1 KiB
TypeScript
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<ChildMotionWarmupStepId, ChildMotionWarmupStepId>(
|
|
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;
|
|
}
|