import { ArrowLeft, Hand, Loader2, RotateCcw } from 'lucide-react'; import { type CSSProperties, type PointerEvent, useEffect, useMemo, useRef, useState, } from 'react'; import type { JumpHopPlatform, JumpHopRuntimeRunSnapshotResponse, JumpHopTileAsset, JumpHopWorkProfileResponse, } from '../../../packages/shared/src/contracts/jumpHop'; type JumpHopRuntimeShellProps = { profile?: JumpHopWorkProfileResponse | null; run?: JumpHopRuntimeRunSnapshotResponse | null; snapshot?: JumpHopRuntimeRunSnapshotResponse | null; isBusy?: boolean; error?: string | null; onJump: (payload: { chargeMs: number }) => Promise; onRestart: () => void; onExit?: () => void; onBack?: () => void; }; type VisiblePlatform = { platform: JumpHopPlatform; index: number; screenX: number; screenY: number; scale: number; asset: JumpHopTileAsset | null; }; const MAX_CHARGE_RATIO = 1; const DEFAULT_MAX_CHARGE_MS = 1800; const VISIBLE_FORWARD_COUNT = 6; const tileToneByType: Record = { accent: '#e0f2fe', bonus: '#fef3c7', finish: '#dcfce7', normal: '#f8fafc', start: '#e0f2fe', target: '#fee2e2', }; function clamp(value: number, min: number, max: number) { return Math.min(max, Math.max(min, value)); } function getRun( run: JumpHopRuntimeRunSnapshotResponse | null | undefined, snapshot: JumpHopRuntimeRunSnapshotResponse | null | undefined, ) { return run ?? snapshot ?? null; } function buildTileAssetMap(tileAssets: JumpHopTileAsset[] | undefined) { const map = new Map(); for (const asset of tileAssets ?? []) { if (!map.has(asset.tileType)) { map.set(asset.tileType, asset); } } return map; } function getStatusLabel( status: JumpHopRuntimeRunSnapshotResponse['status'] | undefined, ) { if (status === 'cleared') { return '通关'; } if (status === 'failed') { return '失败'; } return '进行中'; } function getJumpFeedback(run: JumpHopRuntimeRunSnapshotResponse | null) { const result = run?.lastJump?.result; if (result === 'perfect') { return 'Perfect'; } if (result === 'finish') { return 'Finish'; } if (result === 'hit') { return 'Hit'; } if (result === 'miss') { return 'Miss'; } return null; } function projectPlatformPath( platforms: JumpHopPlatform[], currentIndex: number, tileAssetMap: Map, ) { const current = platforms[currentIndex] ?? platforms[0]; if (!current) { return []; } const start = Math.max(0, currentIndex - 1); const end = Math.min(platforms.length, currentIndex + VISIBLE_FORWARD_COUNT); const visible = platforms.slice(start, end); const worldScale = 0.86; return visible.map((platform, offset): VisiblePlatform => { const index = start + offset; const dx = platform.x - current.x; const dy = platform.y - current.y; const isoX = (dx - dy) * worldScale; const isoY = (dx + dy) * 0.46 * worldScale; const depth = index - currentIndex; return { platform, index, screenX: 50 + isoX, screenY: 58 + isoY - depth * 0.8, scale: clamp(1 - Math.max(0, depth) * 0.035, 0.78, 1.08), asset: tileAssetMap.get(platform.tileType) ?? tileAssetMap.get('normal') ?? tileAssetMap.get('start') ?? null, }; }); } function getCharacterPosition( run: JumpHopRuntimeRunSnapshotResponse | null, platforms: VisiblePlatform[], ) { if (!run) { return null; } const landedPlatform = platforms.find( (item) => item.index === run.currentPlatformIndex, ); if (landedPlatform) { return { x: landedPlatform.screenX, y: landedPlatform.screenY - 8, isMiss: false, }; } const lastJump = run.lastJump; if (lastJump && run.status === 'failed') { const targetPlatform = platforms.find( (item) => item.index === lastJump.targetPlatformIndex, ); if (targetPlatform) { return { x: targetPlatform.screenX + 8, y: targetPlatform.screenY - 2, isMiss: true, }; } } return null; } function IsometricFallbackTile({ platform }: { platform: JumpHopPlatform }) { const tone = tileToneByType[platform.tileType] ?? tileToneByType.normal; const style = { '--jump-hop-tile-tone': tone, } as CSSProperties; return (