Integrate unfinished server-rs refactor worklists
This commit is contained in:
@@ -1,224 +1,9 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import type {
|
||||
BigFishAssetSlotResponse,
|
||||
BigFishRuntimeEntityResponse,
|
||||
BigFishRuntimeSnapshotResponse,
|
||||
SubmitBigFishInputRequest,
|
||||
} from '../packages/shared/src/contracts/bigFish';
|
||||
import { BigFishRuntimeShell } from './components/big-fish-runtime/BigFishRuntimeShell';
|
||||
|
||||
const BIG_FISH_BACKGROUND_IMAGE =
|
||||
'data:image/svg+xml;utf8,' +
|
||||
encodeURIComponent(`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 1280">
|
||||
<defs>
|
||||
<linearGradient id="water" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#38bdf8" />
|
||||
<stop offset="0.52" stop-color="#0f766e" />
|
||||
<stop offset="1" stop-color="#020617" />
|
||||
</linearGradient>
|
||||
<radialGradient id="light" cx="50%" cy="12%" r="52%">
|
||||
<stop offset="0" stop-color="#ecfeff" stop-opacity="0.72" />
|
||||
<stop offset="1" stop-color="#ecfeff" stop-opacity="0" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<rect width="720" height="1280" fill="url(#water)" />
|
||||
<rect width="720" height="1280" fill="url(#light)" />
|
||||
<path d="M70 324 C164 268 256 384 362 320 C492 242 582 330 656 282" fill="none" stroke="#a7f3d0" stroke-width="16" stroke-linecap="round" opacity="0.28" />
|
||||
<path d="M34 760 C156 700 238 806 372 724 C520 634 606 746 704 682" fill="none" stroke="#bae6fd" stroke-width="18" stroke-linecap="round" opacity="0.18" />
|
||||
<circle cx="120" cy="210" r="18" fill="#ecfeff" opacity="0.36" />
|
||||
<circle cx="548" cy="410" r="12" fill="#ecfeff" opacity="0.28" />
|
||||
<circle cx="304" cy="590" r="10" fill="#ecfeff" opacity="0.24" />
|
||||
<path d="M0 1060 C128 1010 244 1096 366 1030 C492 962 612 1026 720 976 V1280 H0 Z" fill="#022c22" opacity="0.62" />
|
||||
</svg>`);
|
||||
|
||||
const WORLD_MIN_X = 60;
|
||||
const WORLD_MAX_X = 780;
|
||||
const WORLD_MIN_Y = 80;
|
||||
const WORLD_MAX_Y = 1240;
|
||||
const PLAYER_SPEED = 20;
|
||||
|
||||
function clamp(value: number, min: number, max: number) {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
function buildEntity(
|
||||
entityId: string,
|
||||
level: number,
|
||||
x: number,
|
||||
y: number,
|
||||
): BigFishRuntimeEntityResponse {
|
||||
return {
|
||||
entityId,
|
||||
level,
|
||||
position: { x, y },
|
||||
radius: 12 + level * 5,
|
||||
offscreenSeconds: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function buildInitialRun(): BigFishRuntimeSnapshotResponse {
|
||||
const leader = buildEntity('player-leader', 1, 360, 640);
|
||||
return {
|
||||
runId: `local-big-fish-run-${Date.now()}`,
|
||||
sessionId: 'local-big-fish-session',
|
||||
status: 'running',
|
||||
tick: 0,
|
||||
playerLevel: 1,
|
||||
winLevel: 5,
|
||||
leaderEntityId: leader.entityId,
|
||||
ownedEntities: [leader],
|
||||
wildEntities: [
|
||||
buildEntity('wild-small-1', 1, 250, 560),
|
||||
buildEntity('wild-small-2', 1, 470, 760),
|
||||
buildEntity('wild-mid-1', 2, 560, 520),
|
||||
buildEntity('wild-mid-2', 3, 210, 820),
|
||||
buildEntity('wild-boss-1', 5, 610, 930),
|
||||
],
|
||||
cameraCenter: { ...leader.position },
|
||||
lastInput: { x: 0, y: 0 },
|
||||
eventLog: ['按住屏幕任意位置,再拖动控制方向。'],
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
function distanceBetween(
|
||||
first: BigFishRuntimeEntityResponse,
|
||||
second: BigFishRuntimeEntityResponse,
|
||||
) {
|
||||
return Math.hypot(
|
||||
first.position.x - second.position.x,
|
||||
first.position.y - second.position.y,
|
||||
);
|
||||
}
|
||||
|
||||
function respawnWildEntity(entity: BigFishRuntimeEntityResponse, tick: number) {
|
||||
const offset = tick * 37 + entity.level * 53;
|
||||
return {
|
||||
...entity,
|
||||
position: {
|
||||
x: WORLD_MIN_X + (offset % Math.floor(WORLD_MAX_X - WORLD_MIN_X)),
|
||||
y: WORLD_MIN_Y + ((offset * 7) % Math.floor(WORLD_MAX_Y - WORLD_MIN_Y)),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function moveWildEntity(entity: BigFishRuntimeEntityResponse, tick: number) {
|
||||
const phase = tick * 0.32 + entity.level * 1.7;
|
||||
const speed = 6 + entity.level * 0.8;
|
||||
const nextX = entity.position.x + Math.cos(phase) * speed;
|
||||
const nextY = entity.position.y + Math.sin(phase * 0.73) * speed;
|
||||
return {
|
||||
...entity,
|
||||
position: {
|
||||
x: clamp(nextX, WORLD_MIN_X, WORLD_MAX_X),
|
||||
y: clamp(nextY, WORLD_MIN_Y, WORLD_MAX_Y),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function applyLocalInput(
|
||||
run: BigFishRuntimeSnapshotResponse,
|
||||
input: SubmitBigFishInputRequest,
|
||||
): BigFishRuntimeSnapshotResponse {
|
||||
if (run.status !== 'running') {
|
||||
return run;
|
||||
}
|
||||
|
||||
const leader = run.ownedEntities.find(
|
||||
(entity) => entity.entityId === run.leaderEntityId,
|
||||
);
|
||||
if (!leader) {
|
||||
return run;
|
||||
}
|
||||
|
||||
const nextLeader = {
|
||||
...leader,
|
||||
position: {
|
||||
x: clamp(leader.position.x + input.x * PLAYER_SPEED, WORLD_MIN_X, WORLD_MAX_X),
|
||||
y: clamp(leader.position.y + input.y * PLAYER_SPEED, WORLD_MIN_Y, WORLD_MAX_Y),
|
||||
},
|
||||
};
|
||||
|
||||
let nextPlayerLevel = run.playerLevel;
|
||||
const nextEvents = [...run.eventLog];
|
||||
const nextWildEntities = run.wildEntities.map((entity) => {
|
||||
const movedEntity = moveWildEntity(entity, run.tick + 1);
|
||||
const touched = distanceBetween(nextLeader, movedEntity) <= nextLeader.radius + movedEntity.radius;
|
||||
if (!touched) {
|
||||
return movedEntity;
|
||||
}
|
||||
|
||||
if (movedEntity.level <= nextPlayerLevel) {
|
||||
nextPlayerLevel = Math.min(run.winLevel, nextPlayerLevel + 1);
|
||||
nextEvents.push(`吞噬 Lv.${movedEntity.level},成长到 Lv.${nextPlayerLevel}`);
|
||||
return respawnWildEntity(movedEntity, run.tick + nextPlayerLevel);
|
||||
}
|
||||
|
||||
nextEvents.push(`撞上 Lv.${movedEntity.level},暂时避开更大的鱼。`);
|
||||
return movedEntity;
|
||||
});
|
||||
|
||||
const scaledLeader = {
|
||||
...nextLeader,
|
||||
level: nextPlayerLevel,
|
||||
radius: 12 + nextPlayerLevel * 5,
|
||||
};
|
||||
const status = nextPlayerLevel >= run.winLevel ? 'won' : 'running';
|
||||
if (status === 'won' && run.status !== 'won') {
|
||||
nextEvents.push('已经成长为海域霸主。');
|
||||
}
|
||||
|
||||
return {
|
||||
...run,
|
||||
status,
|
||||
tick: run.tick + 1,
|
||||
playerLevel: nextPlayerLevel,
|
||||
ownedEntities: [scaledLeader],
|
||||
wildEntities: nextWildEntities,
|
||||
cameraCenter: { ...scaledLeader.position },
|
||||
lastInput: input,
|
||||
eventLog: nextEvents.slice(-5),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function BigFishPlaygroundApp() {
|
||||
const [run, setRun] = useState(buildInitialRun);
|
||||
const assetSlots = useMemo<BigFishAssetSlotResponse[]>(
|
||||
() => [
|
||||
{
|
||||
slotId: 'local-big-fish-background',
|
||||
assetKind: 'stage_background',
|
||||
status: 'ready',
|
||||
assetUrl: BIG_FISH_BACKGROUND_IMAGE,
|
||||
promptSnapshot: '本地直达入口占位海域背景',
|
||||
updatedAt: new Date(0).toISOString(),
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
const handleSubmitInput = useCallback((payload: SubmitBigFishInputRequest) => {
|
||||
setRun((currentRun) => applyLocalInput(currentRun, payload));
|
||||
}, []);
|
||||
|
||||
const handleRestart = useCallback(() => {
|
||||
setRun(buildInitialRun());
|
||||
}, []);
|
||||
|
||||
const handleExit = useCallback(() => {
|
||||
useEffect(() => {
|
||||
window.location.assign('/');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<BigFishRuntimeShell
|
||||
run={run}
|
||||
assetSlots={assetSlots}
|
||||
onBack={handleExit}
|
||||
onRestart={handleRestart}
|
||||
onSubmitInput={handleSubmitInput}
|
||||
/>
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user