feat: add baby object match edutainment flow
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
@@ -4,12 +4,19 @@ import type {
|
||||
} from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject';
|
||||
import {
|
||||
BABY_OBJECT_MATCH_EDUTAINMENT_TAG,
|
||||
BABY_OBJECT_MATCH_TEMPLATE_ID,
|
||||
BABY_OBJECT_MATCH_TEMPLATE_NAME,
|
||||
} from '../../../packages/shared/src/contracts/edutainmentBabyObject';
|
||||
import type {
|
||||
MocapConnectionStatus,
|
||||
MocapHandInput,
|
||||
MocapInputCommand,
|
||||
} from '../../services/useMocapInput';
|
||||
import { useMocapInput } from '../../services/useMocapInput';
|
||||
import { BabyObjectMatchRuntimeShell } from '../edutainment-runtime/BabyObjectMatchRuntimeShell';
|
||||
import {
|
||||
applyChildMotionWarmupCompletion,
|
||||
CHILD_MOTION_CENTER_X,
|
||||
@@ -33,6 +40,41 @@ type CameraAccessState = 'idle' | 'requesting' | 'ready' | 'blocked';
|
||||
type MotionSourceState = 'connecting' | 'ready' | 'waiting' | 'offline';
|
||||
type WarmupMocapGestureIntent = 'greeting' | 'left-hand' | 'right-hand' | 'jump';
|
||||
|
||||
const CHILD_MOTION_BABY_OBJECT_DEMO_DRAFT: BabyObjectMatchDraft = {
|
||||
draftId: 'child-motion-demo-baby-object-draft',
|
||||
profileId: 'child-motion-demo-baby-object-profile',
|
||||
templateId: BABY_OBJECT_MATCH_TEMPLATE_ID,
|
||||
templateName: BABY_OBJECT_MATCH_TEMPLATE_NAME,
|
||||
workTitle: '宝贝识物',
|
||||
workDescription: '苹果和香蕉识物分类',
|
||||
itemNames: ['苹果', '香蕉'],
|
||||
itemAssets: [
|
||||
{
|
||||
itemId: 'child-motion-demo-baby-object-apple',
|
||||
itemName: '苹果',
|
||||
imageSrc:
|
||||
'data:image/svg+xml;utf8,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 512 512%22%3E%3Crect width=%22512%22 height=%22512%22 rx=%2296%22 fill=%22%23fff1d6%22/%3E%3Ccircle cx=%22256%22 cy=%22266%22 r=%22122%22 fill=%22%23ef5b5b%22/%3E%3Cpath d=%22M250 148c20-50 58-66 102-54-18 45-52 70-102 54Z%22 fill=%22%2351a45f%22/%3E%3Cpath d=%22M256 150c-8-34 2-62 28-84%22 stroke=%22%23734822%22 stroke-width=%2218%22 stroke-linecap=%22round%22 fill=%22none%22/%3E%3Ccircle cx=%22216%22 cy=%22226%22 r=%2218%22 fill=%22%23fff%22 opacity=%22.65%22/%3E%3C/svg%3E',
|
||||
assetObjectId: null,
|
||||
generationProvider: 'placeholder',
|
||||
prompt: '苹果',
|
||||
},
|
||||
{
|
||||
itemId: 'child-motion-demo-baby-object-banana',
|
||||
itemName: '香蕉',
|
||||
imageSrc:
|
||||
'data:image/svg+xml;utf8,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 512 512%22%3E%3Crect width=%22512%22 height=%22512%22 rx=%2296%22 fill=%22%23e9f7ff%22/%3E%3Cpath d=%22M142 302c128 74 228 38 278-122 14 144-84 244-226 220-52-9-84-38-52-98Z%22 fill=%22%23ffd75d%22/%3E%3Cpath d=%22M406 180c6-20 18-34 38-44%22 stroke=%22%238b5b22%22 stroke-width=%2218%22 stroke-linecap=%22round%22/%3E%3Cpath d=%22M158 310c70 40 152 42 218-38%22 stroke=%22%23fff2a7%22 stroke-width=%2220%22 stroke-linecap=%22round%22 fill=%22none%22 opacity=%22.72%22/%3E%3C/svg%3E',
|
||||
assetObjectId: null,
|
||||
generationProvider: 'placeholder',
|
||||
prompt: '香蕉',
|
||||
},
|
||||
],
|
||||
themeTags: [BABY_OBJECT_MATCH_EDUTAINMENT_TAG, '宝贝识物'],
|
||||
publicationStatus: 'published',
|
||||
createdAt: '2026-05-11T00:00:00.000Z',
|
||||
updatedAt: '2026-05-11T00:00:00.000Z',
|
||||
publishedAt: '2026-05-11T00:00:00.000Z',
|
||||
};
|
||||
|
||||
const WARMUP_MOCAP_WAVE_MIN_POINTS = 3;
|
||||
const WARMUP_MOCAP_WAVE_MIN_X_RANGE = 0.055;
|
||||
|
||||
@@ -246,7 +288,6 @@ function getStepIndex(stepId: ChildMotionWarmupStepId) {
|
||||
'jump_once',
|
||||
'warmup_finish',
|
||||
'level_select',
|
||||
'play_placeholder',
|
||||
];
|
||||
return Math.max(0, order.indexOf(stepId));
|
||||
}
|
||||
@@ -377,6 +418,7 @@ export function ChildMotionWarmupDemo() {
|
||||
const [stepId, setStepId] = useState<ChildMotionWarmupStepId>(() =>
|
||||
hasCompletedChildMotionWarmupInRuntime() ? 'level_select' : 'center_arrive',
|
||||
);
|
||||
const [isBabyObjectRuntimeOpen, setIsBabyObjectRuntimeOpen] = useState(false);
|
||||
const [avatarX, setAvatarX] = useState(CHILD_MOTION_CENTER_X);
|
||||
const [calibration, setCalibration] = useState(
|
||||
createEmptyChildMotionCalibration,
|
||||
@@ -778,16 +820,24 @@ export function ChildMotionWarmupDemo() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleStartPlaceholderLevel = () => {
|
||||
setStepId('play_placeholder');
|
||||
};
|
||||
|
||||
const handleReturnToStart = () => {
|
||||
setStepId('level_select');
|
||||
const handleStartBabyObjectLevel = () => {
|
||||
setIsBabyObjectRuntimeOpen(true);
|
||||
};
|
||||
|
||||
const lineText = useMemo(() => step.spokenLines.join(','), [step.spokenLines]);
|
||||
|
||||
if (isBabyObjectRuntimeOpen) {
|
||||
return (
|
||||
<BabyObjectMatchRuntimeShell
|
||||
draft={CHILD_MOTION_BABY_OBJECT_DEMO_DRAFT}
|
||||
onBack={() => {
|
||||
setIsBabyObjectRuntimeOpen(false);
|
||||
setStepId('level_select');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="child-motion-demo" data-testid="child-motion-demo">
|
||||
<div className="child-motion-orientation-tip" role="status">
|
||||
@@ -846,21 +896,12 @@ export function ChildMotionWarmupDemo() {
|
||||
|
||||
{step.kind === 'levelSelect' ? (
|
||||
<div className="child-motion-start-panel">
|
||||
<button type="button" onClick={handleStartPlaceholderLevel}>
|
||||
<button type="button" onClick={handleStartBabyObjectLevel}>
|
||||
开始游戏
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{step.kind === 'placeholder' ? (
|
||||
<div className="child-motion-start-panel">
|
||||
<span>下一关正在设计中</span>
|
||||
<button type="button" onClick={handleReturnToStart}>
|
||||
回到开始
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<ChildMotionCalibrationPanel calibration={calibration} />
|
||||
</section>
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user