This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||
import { afterEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import type { BigFishRuntimeSnapshotResponse } from '../../../packages/shared/src/contracts/bigFish';
|
||||
import { BigFishRuntimeShell } from './BigFishRuntimeShell';
|
||||
@@ -38,7 +38,21 @@ function createRun(
|
||||
};
|
||||
}
|
||||
|
||||
function dispatchPointerEvent(
|
||||
target: HTMLElement,
|
||||
type: string,
|
||||
options: { pointerId: number; clientX: number; clientY: number },
|
||||
) {
|
||||
const event = new Event(type, { bubbles: true, cancelable: true });
|
||||
Object.assign(event, options);
|
||||
target.dispatchEvent(event);
|
||||
}
|
||||
|
||||
describe('BigFishRuntimeShell', () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test('renders restart and exit actions after a failed run', () => {
|
||||
const onBack = vi.fn();
|
||||
const onRestart = vi.fn();
|
||||
@@ -96,4 +110,51 @@ describe('BigFishRuntimeShell', () => {
|
||||
|
||||
expect(screen.queryByRole('dialog', { name: '玩法规则' })).toBeNull();
|
||||
});
|
||||
|
||||
test('keeps moving in the last sampled direction after drag ends', () => {
|
||||
vi.useFakeTimers();
|
||||
const onSubmitInput = vi.fn();
|
||||
|
||||
const { container } = render(
|
||||
<BigFishRuntimeShell
|
||||
run={createRun('running')}
|
||||
onBack={() => {}}
|
||||
onSubmitInput={onSubmitInput}
|
||||
/>,
|
||||
);
|
||||
const stage = container.querySelector('.touch-none');
|
||||
if (!(stage instanceof HTMLElement)) {
|
||||
throw new Error('Missing big fish stage');
|
||||
}
|
||||
|
||||
act(() => {
|
||||
dispatchPointerEvent(stage, 'pointerdown', {
|
||||
pointerId: 1,
|
||||
clientX: 100,
|
||||
clientY: 100,
|
||||
});
|
||||
});
|
||||
act(() => {
|
||||
dispatchPointerEvent(stage, 'pointermove', {
|
||||
pointerId: 1,
|
||||
clientX: 140,
|
||||
clientY: 100,
|
||||
});
|
||||
});
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(100);
|
||||
});
|
||||
act(() => {
|
||||
dispatchPointerEvent(stage, 'pointerup', {
|
||||
pointerId: 1,
|
||||
clientX: 140,
|
||||
clientY: 100,
|
||||
});
|
||||
});
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(220);
|
||||
});
|
||||
|
||||
expect(onSubmitInput).toHaveBeenLastCalledWith({ x: 1, y: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,8 @@ type TouchOrigin = {
|
||||
y: number;
|
||||
};
|
||||
|
||||
type TouchSample = TouchOrigin;
|
||||
|
||||
type BigFishRuntimeShellProps = {
|
||||
run: BigFishRuntimeSnapshotResponse | null;
|
||||
assetSlots?: BigFishAssetSlotResponse[];
|
||||
@@ -42,14 +44,13 @@ function normalizeVector(x: number, y: number) {
|
||||
};
|
||||
}
|
||||
|
||||
function resolveDirectionFromOrigin(
|
||||
origin: TouchOrigin,
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
) {
|
||||
const deadZone = 12;
|
||||
const deltaX = clientX - origin.x;
|
||||
const deltaY = clientY - origin.y;
|
||||
function resolveDirectionFromSample(previous: TouchSample, current: TouchSample) {
|
||||
const deadZone = 4;
|
||||
const deltaX = current.x - previous.x;
|
||||
const deltaY = current.y - previous.y;
|
||||
if (!Number.isFinite(deltaX) || !Number.isFinite(deltaY)) {
|
||||
return { x: 0, y: 0 };
|
||||
}
|
||||
if (Math.hypot(deltaX, deltaY) < deadZone) {
|
||||
return { x: 0, y: 0 };
|
||||
}
|
||||
@@ -147,7 +148,7 @@ function BigFishRuleModal({
|
||||
>
|
||||
<div className="space-y-3 text-sm leading-6 text-[var(--platform-text-base)]">
|
||||
<div className="rounded-2xl bg-cyan-50 px-4 py-3 text-cyan-950">
|
||||
拖动屏幕控制方向,角色会按固定速度移动。
|
||||
拖动屏幕改变方向,角色会按固定速度移动。
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<div>低级或同级野生实体会被收编。</div>
|
||||
@@ -178,15 +179,15 @@ function BigFishEntityDot({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`absolute -translate-x-1/2 -translate-y-1/2 overflow-hidden rounded-full border shadow-lg transition-all ${
|
||||
className={`absolute -translate-x-1/2 -translate-y-1/2 transition-all ${
|
||||
entityImageSrc
|
||||
? owned
|
||||
? isLeader
|
||||
? 'border-cyan-50 shadow-cyan-950/40'
|
||||
: 'border-cyan-100/80 shadow-cyan-950/28'
|
||||
? 'drop-shadow-[0_10px_16px_rgba(8,47,73,0.45)]'
|
||||
: 'drop-shadow-[0_8px_12px_rgba(8,47,73,0.32)]'
|
||||
: entity.level > run.playerLevel
|
||||
? 'border-rose-100/80 shadow-rose-950/28'
|
||||
: 'border-emerald-100/80 shadow-emerald-950/24'
|
||||
? 'drop-shadow-[0_8px_12px_rgba(127,29,29,0.36)]'
|
||||
: 'drop-shadow-[0_8px_12px_rgba(6,78,59,0.3)]'
|
||||
: owned
|
||||
? isLeader
|
||||
? 'border-cyan-100 bg-cyan-300 shadow-cyan-950/30'
|
||||
@@ -202,11 +203,10 @@ function BigFishEntityDot({
|
||||
<ResolvedAssetImage
|
||||
src={entityImageSrc}
|
||||
alt={`Lv.${entity.level} 实体`}
|
||||
className={`h-full w-full object-cover ${
|
||||
className={`h-full w-full object-contain ${
|
||||
owned && isLeader ? 'scale-110' : ''
|
||||
}`}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_35%,transparent_32%,rgba(2,6,23,0.18)_72%,rgba(2,6,23,0.36)_100%)]" />
|
||||
</>
|
||||
) : null}
|
||||
<span className="absolute inset-0 flex items-center justify-center text-[0.62rem] font-black text-white [text-shadow:0_1px_2px_rgba(2,6,23,0.9)]">
|
||||
@@ -227,6 +227,8 @@ export function BigFishRuntimeShell({
|
||||
}: BigFishRuntimeShellProps) {
|
||||
const stageRef = useRef<HTMLDivElement | null>(null);
|
||||
const [touchOrigin, setTouchOrigin] = useState<TouchOrigin | null>(null);
|
||||
const currentTouchRef = useRef<TouchSample | null>(null);
|
||||
const lastTouchSampleRef = useRef<TouchSample | null>(null);
|
||||
const [isRuleModalOpen, setIsRuleModalOpen] = useState(false);
|
||||
const [stick, setStick] = useState({ x: 0, y: 0 });
|
||||
const stickRef = useRef(stick);
|
||||
@@ -251,6 +253,31 @@ export function BigFishRuntimeShell({
|
||||
};
|
||||
}, [onSubmitInput, run?.status]);
|
||||
|
||||
useEffect(() => {
|
||||
if (run?.status !== 'running' || !touchOrigin) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const timer = window.setInterval(() => {
|
||||
const current = currentTouchRef.current;
|
||||
const previous = lastTouchSampleRef.current;
|
||||
if (!current || !previous || current.pointerId !== previous.pointerId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sampledDirection = resolveDirectionFromSample(previous, current);
|
||||
lastTouchSampleRef.current = { ...current };
|
||||
if (sampledDirection.x === 0 && sampledDirection.y === 0) {
|
||||
return;
|
||||
}
|
||||
submitDirection(sampledDirection);
|
||||
}, 100);
|
||||
|
||||
return () => {
|
||||
window.clearInterval(timer);
|
||||
};
|
||||
}, [run?.status, touchOrigin]);
|
||||
|
||||
const submitDirection = (direction: SubmitBigFishInputRequest) => {
|
||||
setStick(direction);
|
||||
onSubmitInput(direction);
|
||||
@@ -260,22 +287,35 @@ export function BigFishRuntimeShell({
|
||||
if (event.target instanceof HTMLElement && event.target.closest('button')) {
|
||||
return;
|
||||
}
|
||||
event.currentTarget.setPointerCapture(event.pointerId);
|
||||
if (!Number.isFinite(event.clientX) || !Number.isFinite(event.clientY)) {
|
||||
return;
|
||||
}
|
||||
event.currentTarget.setPointerCapture?.(event.pointerId);
|
||||
setTouchOrigin({
|
||||
pointerId: event.pointerId,
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
});
|
||||
submitDirection({ x: 0, y: 0 });
|
||||
currentTouchRef.current = {
|
||||
pointerId: event.pointerId,
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
};
|
||||
lastTouchSampleRef.current = { ...currentTouchRef.current };
|
||||
};
|
||||
|
||||
const updateTouchControl = (event: PointerEvent<HTMLDivElement>) => {
|
||||
if (!touchOrigin || touchOrigin.pointerId !== event.pointerId) {
|
||||
return;
|
||||
}
|
||||
submitDirection(
|
||||
resolveDirectionFromOrigin(touchOrigin, event.clientX, event.clientY),
|
||||
);
|
||||
if (!Number.isFinite(event.clientX) || !Number.isFinite(event.clientY)) {
|
||||
return;
|
||||
}
|
||||
currentTouchRef.current = {
|
||||
pointerId: event.pointerId,
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
};
|
||||
};
|
||||
|
||||
const endTouchControl = (event: PointerEvent<HTMLDivElement>) => {
|
||||
@@ -283,7 +323,8 @@ export function BigFishRuntimeShell({
|
||||
return;
|
||||
}
|
||||
setTouchOrigin(null);
|
||||
submitDirection({ x: 0, y: 0 });
|
||||
currentTouchRef.current = null;
|
||||
lastTouchSampleRef.current = null;
|
||||
};
|
||||
|
||||
if (!run) {
|
||||
|
||||
Reference in New Issue
Block a user