This commit is contained in:
2026-04-25 11:22:03 +08:00
parent 31f350d499
commit a6029639b0
12 changed files with 518 additions and 63 deletions

View File

@@ -1,5 +1,5 @@
import { ArrowLeft, Loader2 } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import { useEffect, useRef, useState, type PointerEvent } from 'react';
import type {
BigFishAssetSlotResponse,
@@ -9,6 +9,12 @@ import type {
} from '../../../packages/shared/src/contracts/bigFish';
import { ResolvedAssetImage } from '../ResolvedAssetImage';
type TouchOrigin = {
pointerId: number;
x: number;
y: number;
};
type BigFishRuntimeShellProps = {
run: BigFishRuntimeSnapshotResponse | null;
assetSlots?: BigFishAssetSlotResponse[];
@@ -34,6 +40,20 @@ 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;
if (Math.hypot(deltaX, deltaY) < deadZone) {
return { x: 0, y: 0 };
}
return normalizeVector(deltaX, deltaY);
}
function projectEntity(
entity: BigFishRuntimeEntityResponse,
run: BigFishRuntimeSnapshotResponse,
@@ -152,7 +172,8 @@ export function BigFishRuntimeShell({
onBack,
onSubmitInput,
}: BigFishRuntimeShellProps) {
const padRef = useRef<HTMLDivElement | null>(null);
const stageRef = useRef<HTMLDivElement | null>(null);
const [touchOrigin, setTouchOrigin] = useState<TouchOrigin | null>(null);
const [stick, setStick] = useState({ x: 0, y: 0 });
const stickRef = useRef(stick);
@@ -163,7 +184,7 @@ export function BigFishRuntimeShell({
useEffect(() => {
const timer = window.setInterval(() => {
const current = stickRef.current;
// 即使摇杆静止也持续回传当前输入,让后端持续推进刷怪、清理与胜负裁决。
// 即使没有方向输入也持续回传当前状态,让后端持续推进刷怪、清理与胜负裁决。
onSubmitInput(current);
}, 220);
@@ -172,20 +193,39 @@ export function BigFishRuntimeShell({
};
}, [onSubmitInput]);
const updateStickFromPointer = (clientX: number, clientY: number) => {
const pad = padRef.current;
if (!pad) {
const submitDirection = (direction: SubmitBigFishInputRequest) => {
setStick(direction);
onSubmitInput(direction);
};
const beginTouchControl = (event: PointerEvent<HTMLDivElement>) => {
if (event.target instanceof HTMLElement && event.target.closest('button')) {
return;
}
const rect = pad.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const vector = normalizeVector(
(clientX - centerX) / (rect.width / 2),
(clientY - centerY) / (rect.height / 2),
event.currentTarget.setPointerCapture(event.pointerId);
setTouchOrigin({
pointerId: event.pointerId,
x: event.clientX,
y: event.clientY,
});
submitDirection({ x: 0, y: 0 });
};
const updateTouchControl = (event: PointerEvent<HTMLDivElement>) => {
if (!touchOrigin || touchOrigin.pointerId !== event.pointerId) {
return;
}
submitDirection(
resolveDirectionFromOrigin(touchOrigin, event.clientX, event.clientY),
);
setStick(vector);
onSubmitInput(vector);
};
const endTouchControl = (event: PointerEvent<HTMLDivElement>) => {
if (!touchOrigin || touchOrigin.pointerId !== event.pointerId) {
return;
}
setTouchOrigin(null);
submitDirection({ x: 0, y: 0 });
};
if (!run) {
@@ -206,7 +246,14 @@ export function BigFishRuntimeShell({
return (
<div className="fixed inset-0 z-[100] flex justify-center bg-slate-950 text-white">
<div className="relative h-full w-full max-w-[430px] overflow-hidden bg-[radial-gradient(circle_at_50%_20%,rgba(34,211,238,0.2),transparent_28%),radial-gradient(circle_at_20%_80%,rgba(16,185,129,0.18),transparent_26%),linear-gradient(180deg,#082f49,#020617)]">
<div
ref={stageRef}
className="relative h-full w-full max-w-[430px] touch-none overflow-hidden bg-[radial-gradient(circle_at_50%_20%,rgba(34,211,238,0.2),transparent_28%),radial-gradient(circle_at_20%_80%,rgba(16,185,129,0.18),transparent_26%),linear-gradient(180deg,#082f49,#020617)]"
onPointerDown={beginTouchControl}
onPointerMove={updateTouchControl}
onPointerUp={endTouchControl}
onPointerCancel={endTouchControl}
>
{backgroundAsset ? (
<ResolvedAssetImage
src={backgroundAsset}
@@ -251,40 +298,7 @@ export function BigFishRuntimeShell({
))}
</div>
<div className="absolute bottom-6 left-4 z-30">
<div
ref={padRef}
role="presentation"
className="relative h-28 w-28 rounded-full border border-white/18 bg-black/24 backdrop-blur"
onPointerDown={(event) => {
event.currentTarget.setPointerCapture(event.pointerId);
updateStickFromPointer(event.clientX, event.clientY);
}}
onPointerMove={(event) => {
if (event.buttons <= 0) {
return;
}
updateStickFromPointer(event.clientX, event.clientY);
}}
onPointerUp={() => {
setStick({ x: 0, y: 0 });
onSubmitInput({ x: 0, y: 0 });
}}
onPointerCancel={() => {
setStick({ x: 0, y: 0 });
onSubmitInput({ x: 0, y: 0 });
}}
>
<div
className="absolute left-1/2 top-1/2 h-11 w-11 -translate-x-1/2 -translate-y-1/2 rounded-full bg-cyan-200 shadow-lg shadow-cyan-950/30"
style={{
transform: `translate(calc(-50% + ${stick.x * 34}px), calc(-50% + ${stick.y * 34}px))`,
}}
/>
</div>
</div>
<div className="absolute bottom-6 right-4 z-30 max-w-[13rem] space-y-2 text-right text-xs text-white/72">
<div className="pointer-events-none absolute bottom-6 right-4 z-30 max-w-[13rem] space-y-2 text-right text-xs text-white/72">
{isBusy ? <div>...</div> : null}
{error ? <div className="text-rose-200">{error}</div> : null}
{run.eventLog.slice(-3).map((event) => (