feat(edutainment): refresh baby object match flow
This commit is contained in:
@@ -157,6 +157,7 @@ function dispatchPointerEvent(
|
||||
options: {
|
||||
pointerId: number;
|
||||
button?: number;
|
||||
buttons?: number;
|
||||
clientX: number;
|
||||
clientY: number;
|
||||
},
|
||||
@@ -164,9 +165,10 @@ function dispatchPointerEvent(
|
||||
const event = new Event(type, { bubbles: true, cancelable: true });
|
||||
Object.assign(event, options);
|
||||
target.dispatchEvent(event);
|
||||
return event;
|
||||
}
|
||||
|
||||
function dragHand(stage: HTMLElement, button: 0 | 2) {
|
||||
function setStageRect(stage: HTMLElement) {
|
||||
Object.defineProperty(stage, 'getBoundingClientRect', {
|
||||
configurable: true,
|
||||
value: () => ({
|
||||
@@ -181,34 +183,67 @@ function dragHand(stage: HTMLElement, button: 0 | 2) {
|
||||
toJSON: () => ({}),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function dragItemWithHand(stage: HTMLElement, button: 0 | 2, targetX: number) {
|
||||
setStageRect(stage);
|
||||
|
||||
act(() => {
|
||||
dispatchPointerEvent(stage, 'pointerdown', {
|
||||
pointerId: button + 1,
|
||||
button,
|
||||
clientX: 20,
|
||||
clientY: 140,
|
||||
buttons: button === 2 ? 2 : 1,
|
||||
clientX: 160,
|
||||
clientY: 89,
|
||||
});
|
||||
});
|
||||
act(() => {
|
||||
dispatchPointerEvent(stage, 'pointermove', {
|
||||
pointerId: button + 1,
|
||||
button,
|
||||
clientX: 120,
|
||||
clientY: 140,
|
||||
buttons: button === 2 ? 2 : 1,
|
||||
clientX: targetX,
|
||||
clientY: 190,
|
||||
});
|
||||
});
|
||||
act(() => {
|
||||
dispatchPointerEvent(stage, 'pointerup', {
|
||||
pointerId: button + 1,
|
||||
button,
|
||||
clientX: 120,
|
||||
clientY: 140,
|
||||
buttons: 0,
|
||||
clientX: targetX,
|
||||
clientY: 190,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function advanceRoundIntro() {
|
||||
await advanceInitialTargetPreview();
|
||||
await advanceGiftIntro();
|
||||
}
|
||||
|
||||
async function advanceInitialTargetPreview() {
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(2000);
|
||||
});
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(720);
|
||||
});
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
});
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(2000);
|
||||
});
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(720);
|
||||
});
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
});
|
||||
}
|
||||
|
||||
async function advanceGiftIntro() {
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(620);
|
||||
});
|
||||
@@ -236,6 +271,7 @@ test('shows the first gift item after gift and item animations', async () => {
|
||||
);
|
||||
|
||||
expect(screen.getByText('将物品放入对应的篮子里')).toBeTruthy();
|
||||
expect(screen.getByTestId('baby-object-intro-item')).toBeTruthy();
|
||||
expect(screen.getByTestId('baby-object-current-item').textContent).toBe('');
|
||||
|
||||
await advanceRoundIntro();
|
||||
@@ -246,6 +282,56 @@ test('shows the first gift item after gift and item animations', async () => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test('previews both target items before the first gift box round', async () => {
|
||||
vi.useFakeTimers();
|
||||
render(
|
||||
<BabyObjectMatchRuntimeShell
|
||||
draft={createDraft()}
|
||||
random={createRandomSequence([0])}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('baby-object-intro-item')).toBeTruthy();
|
||||
expect(
|
||||
within(screen.getByTestId('baby-object-intro-item')).getByAltText('苹果'),
|
||||
).toBeTruthy();
|
||||
expect(screen.queryByLabelText('礼物盒')).toBeNull();
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(2000);
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('baby-object-intro-item').className).toContain(
|
||||
'baby-object-runtime__intro-item--flying',
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(720);
|
||||
});
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('baby-object-intro-item')).toBeTruthy();
|
||||
expect(
|
||||
within(screen.getByTestId('baby-object-intro-item')).getByAltText('香蕉'),
|
||||
).toBeTruthy();
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(2000);
|
||||
});
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(720);
|
||||
});
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId('baby-object-intro-item')).toBeNull();
|
||||
expect(screen.getByLabelText('礼物盒')).toBeTruthy();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test('applies generated visual package to stage, gift box, baskets, smoke and hud', async () => {
|
||||
vi.useFakeTimers();
|
||||
const { container } = render(
|
||||
@@ -270,6 +356,8 @@ test('applies generated visual package to stage, gift box, baskets, smoke and hu
|
||||
expect(stage.style.getPropertyValue('--baby-object-smoke-image')).toContain(
|
||||
'smoke',
|
||||
);
|
||||
await advanceInitialTargetPreview();
|
||||
|
||||
expect(screen.getByAltText('礼物盒')).toBeTruthy();
|
||||
expect(
|
||||
container.querySelector('.baby-object-runtime__basket-shell-image'),
|
||||
@@ -283,6 +371,80 @@ test('applies generated visual package to stage, gift box, baskets, smoke and hu
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test('uses default runtime hand indicators instead of per-draft generated hand assets', async () => {
|
||||
vi.useFakeTimers();
|
||||
const random = createRandomSequence([0, 0]);
|
||||
const draftWithLegacyHandAssets: BabyObjectMatchDraft = {
|
||||
...createVisualPackageDraft(),
|
||||
visualPackage: {
|
||||
...createVisualPackageDraft().visualPackage!,
|
||||
assets: [
|
||||
...createVisualPackageDraft().visualPackage!.assets,
|
||||
{
|
||||
assetId: 'legacy-left-hand',
|
||||
assetKind: 'left-hand',
|
||||
imageSrc: 'data:image/png;base64,legacy-left-hand',
|
||||
assetObjectId: null,
|
||||
generationProvider: 'vector-engine-gpt-image-2',
|
||||
prompt: '旧左手',
|
||||
},
|
||||
{
|
||||
assetId: 'legacy-right-hand',
|
||||
assetKind: 'right-hand',
|
||||
imageSrc: 'data:image/png;base64,legacy-right-hand',
|
||||
assetObjectId: null,
|
||||
generationProvider: 'vector-engine-gpt-image-2',
|
||||
prompt: '旧右手',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const { container, rerender } = render(
|
||||
<BabyObjectMatchRuntimeShell
|
||||
draft={draftWithLegacyHandAssets}
|
||||
random={random}
|
||||
mocapInput={createMocapInput()}
|
||||
/>,
|
||||
);
|
||||
|
||||
await advanceRoundIntro();
|
||||
|
||||
rerender(
|
||||
<BabyObjectMatchRuntimeShell
|
||||
draft={draftWithLegacyHandAssets}
|
||||
random={random}
|
||||
mocapInput={createMocapInput({
|
||||
latestCommand: {
|
||||
actions: [],
|
||||
hands: [{ x: 0.5, y: 0.37, state: 'open_palm', side: 'right' }],
|
||||
primaryHand: { x: 0.5, y: 0.37, state: 'open_palm', side: 'right' },
|
||||
leftHand: null,
|
||||
rightHand: { x: 0.5, y: 0.37, state: 'open_palm', side: 'right' },
|
||||
},
|
||||
rawPacketPreview: {
|
||||
text: 'show-default-hand',
|
||||
receivedAtMs: 1,
|
||||
},
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('baby-object-left-hand')).toBeTruthy();
|
||||
const stage = container.querySelector('.baby-object-runtime__stage');
|
||||
expect(stage).toBeInstanceOf(HTMLElement);
|
||||
expect(
|
||||
(stage as HTMLElement).style.getPropertyValue(
|
||||
'--baby-object-left-hand-image',
|
||||
),
|
||||
).toBe('');
|
||||
expect(
|
||||
(stage as HTMLElement).style.getPropertyValue(
|
||||
'--baby-object-right-hand-image',
|
||||
),
|
||||
).toBe('');
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test('removes the gift box after smoke releases the current item', async () => {
|
||||
vi.useFakeTimers();
|
||||
render(
|
||||
@@ -292,6 +454,8 @@ test('removes the gift box after smoke releases the current item', async () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
await advanceInitialTargetPreview();
|
||||
|
||||
expect(screen.getByLabelText('礼物盒')).toBeTruthy();
|
||||
|
||||
await act(async () => {
|
||||
@@ -335,10 +499,16 @@ test('keeps left and right baskets fixed while only the gift item is random', as
|
||||
).toBeTruthy();
|
||||
expect(screen.getByLabelText('左侧篮子 苹果')).toBeTruthy();
|
||||
expect(screen.getByLabelText('右侧篮子 香蕉')).toBeTruthy();
|
||||
expect(
|
||||
within(screen.getByLabelText('左侧篮子 苹果')).getByText('苹果'),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
within(screen.getByLabelText('右侧篮子 香蕉')).getByText('香蕉'),
|
||||
).toBeTruthy();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test('mocap camera-right hand movement sends the player left hand item into the left basket', async () => {
|
||||
test('mocap hand must touch the current item before dropping it into a basket', async () => {
|
||||
vi.useFakeTimers();
|
||||
const random = createRandomSequence([0, 0]);
|
||||
const { rerender } = render(
|
||||
@@ -358,13 +528,211 @@ test('mocap camera-right hand movement sends the player left hand item into the
|
||||
mocapInput={createMocapInput({
|
||||
latestCommand: {
|
||||
actions: [],
|
||||
hands: [{ x: 0.22, y: 0.45, state: 'open_palm', side: 'right' }],
|
||||
primaryHand: { x: 0.22, y: 0.45, state: 'open_palm', side: 'right' },
|
||||
hands: [{ x: 0.24, y: 0.72, state: 'open_palm', side: 'right' }],
|
||||
primaryHand: { x: 0.24, y: 0.72, state: 'open_palm', side: 'right' },
|
||||
leftHand: null,
|
||||
rightHand: { x: 0.22, y: 0.45, state: 'open_palm', side: 'right' },
|
||||
rightHand: { x: 0.24, y: 0.72, state: 'open_palm', side: 'right' },
|
||||
},
|
||||
rawPacketPreview: {
|
||||
text: 'camera-right-horizontal-1',
|
||||
text: 'drop-without-grab',
|
||||
receivedAtMs: 1,
|
||||
},
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText('真棒')).toBeNull();
|
||||
expect(screen.queryByText('再想一想吧')).toBeNull();
|
||||
|
||||
rerender(
|
||||
<BabyObjectMatchRuntimeShell
|
||||
draft={createDraft()}
|
||||
random={random}
|
||||
mocapInput={createMocapInput({
|
||||
latestCommand: {
|
||||
actions: [],
|
||||
hands: [{ x: 0.5, y: 0.37, state: 'open_palm', side: 'right' }],
|
||||
primaryHand: { x: 0.5, y: 0.37, state: 'open_palm', side: 'right' },
|
||||
leftHand: null,
|
||||
rightHand: { x: 0.5, y: 0.37, state: 'open_palm', side: 'right' },
|
||||
},
|
||||
rawPacketPreview: {
|
||||
text: 'touch-current-item',
|
||||
receivedAtMs: 2,
|
||||
},
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('baby-object-left-hand')).toBeTruthy();
|
||||
expect(screen.queryByText('真棒')).toBeNull();
|
||||
|
||||
rerender(
|
||||
<BabyObjectMatchRuntimeShell
|
||||
draft={createDraft()}
|
||||
random={random}
|
||||
mocapInput={createMocapInput({
|
||||
latestCommand: {
|
||||
actions: [],
|
||||
hands: [{ x: 0.22, y: 0.78, state: 'open_palm', side: 'right' }],
|
||||
primaryHand: { x: 0.22, y: 0.78, state: 'open_palm', side: 'right' },
|
||||
leftHand: null,
|
||||
rightHand: { x: 0.22, y: 0.78, state: 'open_palm', side: 'right' },
|
||||
},
|
||||
rawPacketPreview: {
|
||||
text: 'drop-left-basket',
|
||||
receivedAtMs: 3,
|
||||
},
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('真棒')).toBeTruthy();
|
||||
expect(screen.getByLabelText('成功次数').textContent).toBe('1/20');
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test('mocap hand uses skeleton wrist before hand landmark points in baby object runtime', async () => {
|
||||
vi.useFakeTimers();
|
||||
const random = createRandomSequence([0, 0]);
|
||||
const { rerender } = render(
|
||||
<BabyObjectMatchRuntimeShell
|
||||
draft={createDraft()}
|
||||
random={random}
|
||||
mocapInput={createMocapInput()}
|
||||
/>,
|
||||
);
|
||||
|
||||
await advanceRoundIntro();
|
||||
|
||||
rerender(
|
||||
<BabyObjectMatchRuntimeShell
|
||||
draft={createDraft()}
|
||||
random={random}
|
||||
mocapInput={createMocapInput({
|
||||
latestCommand: {
|
||||
actions: [],
|
||||
hands: [
|
||||
{
|
||||
x: 0.5,
|
||||
y: 0.37,
|
||||
state: 'open_palm',
|
||||
side: 'right',
|
||||
source: 'palm_center',
|
||||
wrist: { x: 0.62, y: 0.37 },
|
||||
},
|
||||
],
|
||||
primaryHand: {
|
||||
x: 0.5,
|
||||
y: 0.37,
|
||||
state: 'open_palm',
|
||||
side: 'right',
|
||||
source: 'palm_center',
|
||||
wrist: { x: 0.62, y: 0.37 },
|
||||
},
|
||||
leftHand: null,
|
||||
rightHand: {
|
||||
x: 0.5,
|
||||
y: 0.37,
|
||||
state: 'open_palm',
|
||||
side: 'right',
|
||||
source: 'palm_center',
|
||||
wrist: { x: 0.62, y: 0.37 },
|
||||
},
|
||||
bodyJoints: {
|
||||
rightWrist: { x: 0.64, y: 0.37 },
|
||||
},
|
||||
},
|
||||
rawPacketPreview: {
|
||||
text: 'hand-points-over-item-skeleton-wrist-away',
|
||||
receivedAtMs: 1,
|
||||
},
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('baby-object-left-hand')).toBeTruthy();
|
||||
expect(screen.queryByText('真棒')).toBeNull();
|
||||
|
||||
rerender(
|
||||
<BabyObjectMatchRuntimeShell
|
||||
draft={createDraft()}
|
||||
random={random}
|
||||
mocapInput={createMocapInput({
|
||||
latestCommand: {
|
||||
actions: [],
|
||||
hands: [
|
||||
{
|
||||
x: 0.62,
|
||||
y: 0.37,
|
||||
state: 'open_palm',
|
||||
side: 'right',
|
||||
source: 'palm_center',
|
||||
wrist: { x: 0.5, y: 0.37 },
|
||||
},
|
||||
],
|
||||
primaryHand: {
|
||||
x: 0.62,
|
||||
y: 0.37,
|
||||
state: 'open_palm',
|
||||
side: 'right',
|
||||
source: 'palm_center',
|
||||
wrist: { x: 0.5, y: 0.37 },
|
||||
},
|
||||
leftHand: null,
|
||||
rightHand: {
|
||||
x: 0.62,
|
||||
y: 0.37,
|
||||
state: 'open_palm',
|
||||
side: 'right',
|
||||
source: 'palm_center',
|
||||
wrist: { x: 0.62, y: 0.37 },
|
||||
},
|
||||
bodyJoints: {
|
||||
rightWrist: { x: 0.5, y: 0.37 },
|
||||
},
|
||||
},
|
||||
rawPacketPreview: {
|
||||
text: 'skeleton-wrist-over-item-hand-points-away',
|
||||
receivedAtMs: 2,
|
||||
},
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('baby-object-left-hand').className).toContain(
|
||||
'baby-object-runtime__hand--holding-left-corner',
|
||||
);
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test('basket judgement accepts the enlarged basket edge while keeping center gap safe', async () => {
|
||||
vi.useFakeTimers();
|
||||
const random = createRandomSequence([0, 0]);
|
||||
const { rerender } = render(
|
||||
<BabyObjectMatchRuntimeShell
|
||||
draft={createDraft()}
|
||||
random={random}
|
||||
mocapInput={createMocapInput()}
|
||||
/>,
|
||||
);
|
||||
|
||||
await advanceRoundIntro();
|
||||
|
||||
rerender(
|
||||
<BabyObjectMatchRuntimeShell
|
||||
draft={createDraft()}
|
||||
random={random}
|
||||
mocapInput={createMocapInput({
|
||||
latestCommand: {
|
||||
actions: [],
|
||||
hands: [{ x: 0.5, y: 0.37, state: 'open_palm', side: 'right' }],
|
||||
primaryHand: { x: 0.5, y: 0.37, state: 'open_palm', side: 'right' },
|
||||
leftHand: null,
|
||||
rightHand: { x: 0.5, y: 0.37, state: 'open_palm', side: 'right' },
|
||||
},
|
||||
rawPacketPreview: {
|
||||
text: 'touch-current-item-before-narrow-zone',
|
||||
receivedAtMs: 1,
|
||||
},
|
||||
})}
|
||||
@@ -378,40 +746,22 @@ test('mocap camera-right hand movement sends the player left hand item into the
|
||||
mocapInput={createMocapInput({
|
||||
latestCommand: {
|
||||
actions: [],
|
||||
hands: [{ x: 0.24, y: 0.45, state: 'open_palm', side: 'right' }],
|
||||
primaryHand: { x: 0.24, y: 0.45, state: 'open_palm', side: 'right' },
|
||||
hands: [{ x: 0.37, y: 0.82, state: 'open_palm', side: 'right' }],
|
||||
primaryHand: { x: 0.37, y: 0.82, state: 'open_palm', side: 'right' },
|
||||
leftHand: null,
|
||||
rightHand: { x: 0.24, y: 0.45, state: 'open_palm', side: 'right' },
|
||||
rightHand: { x: 0.37, y: 0.82, state: 'open_palm', side: 'right' },
|
||||
},
|
||||
rawPacketPreview: {
|
||||
text: 'camera-right-horizontal-2',
|
||||
text: 'outside-enlarged-left-hitbox-center-gap',
|
||||
receivedAtMs: 2,
|
||||
},
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
|
||||
rerender(
|
||||
<BabyObjectMatchRuntimeShell
|
||||
draft={createDraft()}
|
||||
random={random}
|
||||
mocapInput={createMocapInput({
|
||||
latestCommand: {
|
||||
actions: [],
|
||||
hands: [{ x: 0.22, y: 0.45, state: 'open_palm', side: 'right' }],
|
||||
primaryHand: { x: 0.22, y: 0.45, state: 'open_palm', side: 'right' },
|
||||
leftHand: null,
|
||||
rightHand: { x: 0.22, y: 0.45, state: 'open_palm', side: 'right' },
|
||||
},
|
||||
rawPacketPreview: {
|
||||
text: 'camera-right-horizontal-3',
|
||||
receivedAtMs: 3,
|
||||
},
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText('真棒')).toBeNull();
|
||||
expect(screen.queryByText('再想一想吧')).toBeNull();
|
||||
expect(screen.getByLabelText('成功次数').textContent).toBe('0/20');
|
||||
|
||||
rerender(
|
||||
<BabyObjectMatchRuntimeShell
|
||||
@@ -420,14 +770,14 @@ test('mocap camera-right hand movement sends the player left hand item into the
|
||||
mocapInput={createMocapInput({
|
||||
latestCommand: {
|
||||
actions: [],
|
||||
hands: [{ x: 0.31, y: 0.45, state: 'open_palm', side: 'right' }],
|
||||
primaryHand: { x: 0.31, y: 0.45, state: 'open_palm', side: 'right' },
|
||||
hands: [{ x: 0.36, y: 0.62, state: 'open_palm', side: 'right' }],
|
||||
primaryHand: { x: 0.36, y: 0.62, state: 'open_palm', side: 'right' },
|
||||
leftHand: null,
|
||||
rightHand: { x: 0.31, y: 0.45, state: 'open_palm', side: 'right' },
|
||||
rightHand: { x: 0.36, y: 0.62, state: 'open_palm', side: 'right' },
|
||||
},
|
||||
rawPacketPreview: {
|
||||
text: 'camera-right-horizontal-4',
|
||||
receivedAtMs: 4,
|
||||
text: 'enlarged-left-hitbox-edge',
|
||||
receivedAtMs: 3,
|
||||
},
|
||||
})}
|
||||
/>,
|
||||
@@ -438,7 +788,7 @@ test('mocap camera-right hand movement sends the player left hand item into the
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test('mocap camera-left hand movement sends the player right hand item into the right basket', async () => {
|
||||
test('either mocap hand can drag the current item into either basket', async () => {
|
||||
vi.useFakeTimers();
|
||||
const random = createRandomSequence([0, 0]);
|
||||
const { rerender } = render(
|
||||
@@ -458,12 +808,12 @@ test('mocap camera-left hand movement sends the player right hand item into the
|
||||
mocapInput={createMocapInput({
|
||||
latestCommand: {
|
||||
actions: [],
|
||||
hands: [{ x: 0.82, y: 0.45, state: 'open_palm', side: 'left' }],
|
||||
primaryHand: { x: 0.82, y: 0.45, state: 'open_palm', side: 'left' },
|
||||
leftHand: { x: 0.82, y: 0.45, state: 'open_palm', side: 'left' },
|
||||
hands: [{ x: 0.5, y: 0.37, state: 'open_palm', side: 'left' }],
|
||||
primaryHand: { x: 0.5, y: 0.37, state: 'open_palm', side: 'left' },
|
||||
leftHand: { x: 0.5, y: 0.37, state: 'open_palm', side: 'left' },
|
||||
rightHand: null,
|
||||
},
|
||||
rawPacketPreview: { text: 'camera-left-horizontal-1', receivedAtMs: 1 },
|
||||
rawPacketPreview: { text: 'right-hand-touch-item', receivedAtMs: 1 },
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
@@ -475,48 +825,12 @@ test('mocap camera-left hand movement sends the player right hand item into the
|
||||
mocapInput={createMocapInput({
|
||||
latestCommand: {
|
||||
actions: [],
|
||||
hands: [{ x: 0.8, y: 0.45, state: 'open_palm', side: 'left' }],
|
||||
primaryHand: { x: 0.8, y: 0.45, state: 'open_palm', side: 'left' },
|
||||
leftHand: { x: 0.8, y: 0.45, state: 'open_palm', side: 'left' },
|
||||
hands: [{ x: 0.78, y: 0.78, state: 'open_palm', side: 'left' }],
|
||||
primaryHand: { x: 0.78, y: 0.78, state: 'open_palm', side: 'left' },
|
||||
leftHand: { x: 0.78, y: 0.78, state: 'open_palm', side: 'left' },
|
||||
rightHand: null,
|
||||
},
|
||||
rawPacketPreview: { text: 'camera-left-horizontal-2', receivedAtMs: 2 },
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
|
||||
rerender(
|
||||
<BabyObjectMatchRuntimeShell
|
||||
draft={createDraft()}
|
||||
random={random}
|
||||
mocapInput={createMocapInput({
|
||||
latestCommand: {
|
||||
actions: [],
|
||||
hands: [{ x: 0.82, y: 0.45, state: 'open_palm', side: 'left' }],
|
||||
primaryHand: { x: 0.82, y: 0.45, state: 'open_palm', side: 'left' },
|
||||
leftHand: { x: 0.82, y: 0.45, state: 'open_palm', side: 'left' },
|
||||
rightHand: null,
|
||||
},
|
||||
rawPacketPreview: { text: 'camera-left-horizontal-3', receivedAtMs: 3 },
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText('再想一想吧')).toBeNull();
|
||||
|
||||
rerender(
|
||||
<BabyObjectMatchRuntimeShell
|
||||
draft={createDraft()}
|
||||
random={random}
|
||||
mocapInput={createMocapInput({
|
||||
latestCommand: {
|
||||
actions: [],
|
||||
hands: [{ x: 0.73, y: 0.45, state: 'open_palm', side: 'left' }],
|
||||
primaryHand: { x: 0.73, y: 0.45, state: 'open_palm', side: 'left' },
|
||||
leftHand: { x: 0.73, y: 0.45, state: 'open_palm', side: 'left' },
|
||||
rightHand: null,
|
||||
},
|
||||
rawPacketPreview: { text: 'camera-left-horizontal-4', receivedAtMs: 4 },
|
||||
rawPacketPreview: { text: 'right-hand-drop-right', receivedAtMs: 2 },
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
@@ -526,7 +840,84 @@ test('mocap camera-left hand movement sends the player right hand item into the
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test('mocap action names do not select a basket without horizontal hand movement', async () => {
|
||||
test('holding hand indicator anchors to the lower item corner by hand side', async () => {
|
||||
vi.useFakeTimers();
|
||||
const leftHandRandom = createRandomSequence([0, 0]);
|
||||
const leftHandRuntime = render(
|
||||
<BabyObjectMatchRuntimeShell
|
||||
draft={createDraft()}
|
||||
random={leftHandRandom}
|
||||
mocapInput={createMocapInput()}
|
||||
/>,
|
||||
);
|
||||
|
||||
await advanceRoundIntro();
|
||||
|
||||
leftHandRuntime.rerender(
|
||||
<BabyObjectMatchRuntimeShell
|
||||
draft={createDraft()}
|
||||
random={leftHandRandom}
|
||||
mocapInput={createMocapInput({
|
||||
latestCommand: {
|
||||
actions: [],
|
||||
hands: [{ x: 0.5, y: 0.37, state: 'open_palm', side: 'right' }],
|
||||
primaryHand: { x: 0.5, y: 0.37, state: 'open_palm', side: 'right' },
|
||||
leftHand: null,
|
||||
rightHand: { x: 0.5, y: 0.37, state: 'open_palm', side: 'right' },
|
||||
},
|
||||
rawPacketPreview: {
|
||||
text: 'left-hand-holding-corner',
|
||||
receivedAtMs: 1,
|
||||
},
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('baby-object-left-hand').className).toContain(
|
||||
'baby-object-runtime__hand--holding-left-corner',
|
||||
);
|
||||
|
||||
leftHandRuntime.unmount();
|
||||
|
||||
const rightHandRandom = createRandomSequence([0, 0]);
|
||||
const rightHandRuntime = render(
|
||||
<BabyObjectMatchRuntimeShell
|
||||
draft={createDraft()}
|
||||
random={rightHandRandom}
|
||||
mocapInput={createMocapInput()}
|
||||
/>,
|
||||
);
|
||||
|
||||
await advanceRoundIntro();
|
||||
|
||||
rightHandRuntime.rerender(
|
||||
<BabyObjectMatchRuntimeShell
|
||||
draft={createDraft()}
|
||||
random={rightHandRandom}
|
||||
mocapInput={createMocapInput({
|
||||
latestCommand: {
|
||||
actions: [],
|
||||
hands: [{ x: 0.5, y: 0.37, state: 'open_palm', side: 'left' }],
|
||||
primaryHand: { x: 0.5, y: 0.37, state: 'open_palm', side: 'left' },
|
||||
leftHand: { x: 0.5, y: 0.37, state: 'open_palm', side: 'left' },
|
||||
rightHand: null,
|
||||
},
|
||||
rawPacketPreview: {
|
||||
text: 'right-hand-holding-corner',
|
||||
receivedAtMs: 2,
|
||||
},
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('baby-object-right-hand').className).toContain(
|
||||
'baby-object-runtime__hand--holding-right-corner',
|
||||
);
|
||||
rightHandRuntime.unmount();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test('mocap action names do not select a basket without touching and dragging item', async () => {
|
||||
vi.useFakeTimers();
|
||||
const random = createRandomSequence([0, 0]);
|
||||
const { rerender } = render(
|
||||
@@ -564,7 +955,7 @@ test('mocap action names do not select a basket without horizontal hand movement
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test('mocap unknown hand horizontal movement does not select a basket', async () => {
|
||||
test('mocap unknown hand movement does not grab or select a basket', async () => {
|
||||
vi.useFakeTimers();
|
||||
const random = createRandomSequence([0, 0]);
|
||||
const { rerender } = render(
|
||||
@@ -578,7 +969,8 @@ test('mocap unknown hand horizontal movement does not select a basket', async ()
|
||||
await advanceRoundIntro();
|
||||
|
||||
for (let index = 0; index < 4; index += 1) {
|
||||
const x = [0.22, 0.24, 0.22, 0.31][index] ?? 0.22;
|
||||
const x = [0.5, 0.5, 0.22, 0.22][index] ?? 0.5;
|
||||
const y = [0.37, 0.78, 0.78, 0.37][index] ?? 0.37;
|
||||
rerender(
|
||||
<BabyObjectMatchRuntimeShell
|
||||
draft={createDraft()}
|
||||
@@ -586,13 +978,13 @@ test('mocap unknown hand horizontal movement does not select a basket', async ()
|
||||
mocapInput={createMocapInput({
|
||||
latestCommand: {
|
||||
actions: [],
|
||||
hands: [{ x, y: 0.45, state: 'open_palm', side: 'unknown' }],
|
||||
primaryHand: { x, y: 0.45, state: 'open_palm', side: 'unknown' },
|
||||
hands: [{ x, y, state: 'open_palm', side: 'unknown' }],
|
||||
primaryHand: { x, y, state: 'open_palm', side: 'unknown' },
|
||||
leftHand: null,
|
||||
rightHand: null,
|
||||
},
|
||||
rawPacketPreview: {
|
||||
text: `unknown-horizontal-${index + 1}`,
|
||||
text: `unknown-drag-${index + 1}`,
|
||||
receivedAtMs: index + 1,
|
||||
},
|
||||
})}
|
||||
@@ -608,7 +1000,7 @@ test('mocap unknown hand horizontal movement does not select a basket', async ()
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test('left hand horizontal drag sends a correct item into the left basket', async () => {
|
||||
test('left mouse hand drags a correct item into the left basket', async () => {
|
||||
vi.useFakeTimers();
|
||||
const { container } = render(
|
||||
<BabyObjectMatchRuntimeShell
|
||||
@@ -622,7 +1014,7 @@ test('left hand horizontal drag sends a correct item into the left basket', asyn
|
||||
}
|
||||
|
||||
await advanceRoundIntro();
|
||||
dragHand(stage, 0);
|
||||
dragItemWithHand(stage, 0, 70);
|
||||
|
||||
expect(screen.getByText('真棒')).toBeTruthy();
|
||||
expect(screen.getByLabelText('成功次数').textContent).toBe('1/20');
|
||||
@@ -657,19 +1049,55 @@ test('ignores drag input until the item animation finishes', async () => {
|
||||
throw new Error('Missing baby object runtime stage');
|
||||
}
|
||||
|
||||
dragHand(stage, 0);
|
||||
dragItemWithHand(stage, 0, 70);
|
||||
|
||||
expect(screen.queryByText('真棒')).toBeNull();
|
||||
expect(screen.getByLabelText('成功次数').textContent).toBe('0/20');
|
||||
|
||||
await advanceRoundIntro();
|
||||
dragHand(stage, 0);
|
||||
dragItemWithHand(stage, 0, 70);
|
||||
|
||||
expect(screen.getByText('真棒')).toBeTruthy();
|
||||
expect(screen.getByLabelText('成功次数').textContent).toBe('1/20');
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test('keeps the back button outside active gameplay pointer input', async () => {
|
||||
vi.useFakeTimers();
|
||||
const onBack = vi.fn();
|
||||
render(
|
||||
<BabyObjectMatchRuntimeShell
|
||||
draft={createDraft()}
|
||||
random={createRandomSequence([0, 0])}
|
||||
onBack={onBack}
|
||||
/>,
|
||||
);
|
||||
|
||||
await advanceRoundIntro();
|
||||
|
||||
const backButton = screen.getByRole('button', { name: '返回' });
|
||||
let pointerDownEvent!: Event;
|
||||
act(() => {
|
||||
pointerDownEvent = dispatchPointerEvent(backButton, 'pointerdown', {
|
||||
pointerId: 9,
|
||||
button: 0,
|
||||
buttons: 1,
|
||||
clientX: 16,
|
||||
clientY: 16,
|
||||
});
|
||||
});
|
||||
|
||||
expect(pointerDownEvent.defaultPrevented).toBe(false);
|
||||
expect(screen.queryByTestId('baby-object-left-hand')).toBeNull();
|
||||
|
||||
act(() => {
|
||||
backButton.click();
|
||||
});
|
||||
|
||||
expect(onBack).toHaveBeenCalledTimes(1);
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test('correct placement automatically shows the next gift item', async () => {
|
||||
vi.useFakeTimers();
|
||||
const { container } = render(
|
||||
@@ -691,7 +1119,7 @@ test('correct placement automatically shows the next gift item', async () => {
|
||||
within(screen.getByTestId('baby-object-current-item')).getByAltText('苹果'),
|
||||
).toBeTruthy();
|
||||
|
||||
dragHand(stage, 0);
|
||||
dragItemWithHand(stage, 0, 70);
|
||||
|
||||
expect(screen.getByText('真棒')).toBeTruthy();
|
||||
|
||||
@@ -722,7 +1150,7 @@ test('wrong basket keeps the item active after feedback', async () => {
|
||||
}
|
||||
|
||||
await advanceRoundIntro();
|
||||
dragHand(stage, 2);
|
||||
dragItemWithHand(stage, 2, 250);
|
||||
|
||||
expect(screen.getByText('再想一想吧')).toBeTruthy();
|
||||
expect(screen.getByLabelText('成功次数').textContent).toBe('0/20');
|
||||
@@ -752,7 +1180,7 @@ test('twenty correct placements completes the level', async () => {
|
||||
|
||||
for (let index = 0; index < 20; index += 1) {
|
||||
await advanceRoundIntro();
|
||||
dragHand(stage, 0);
|
||||
dragItemWithHand(stage, 0, 70);
|
||||
await advanceFeedback();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user