import { ArrowLeft, Loader2 } from 'lucide-react'; import { type CSSProperties, type Dispatch, type PointerEvent, type SetStateAction, useCallback, useEffect, useMemo, useRef, useState, } from 'react'; import jumpHopRuntimeLevelLogo from '../../../media/logo.png'; import type { JumpHopRuntimeRunSnapshotResponse, JumpHopTileFaceAsset, JumpHopTileAsset, JumpHopWorkProfileResponse, } from '../../../packages/shared/src/contracts/jumpHop'; import { useResolvedAssetReadUrl } from '../../hooks/useResolvedAssetReadUrl'; import { isGeneratedLegacyPath, readAssetBytes, } from '../../services/assetReadUrlService'; import type { JumpHopRuntimeRequestOptions } from '../../services/jump-hop/jumpHopClient'; import { buildJumpHopVisiblePlatforms, formatJumpHopDurationLabel, getJumpHopCharacterVisualPosition, getJumpHopJumpFeedbackLabel, getJumpHopBackendDragVector, getJumpHopLandingAssistVisualPosition, getJumpHopPlatformVisualSize, getJumpHopRunDurationMs, getJumpHopStatusLabel, getJumpHopTileTone, type JumpHopCharacterVisualPosition, type JumpHopVisiblePlatform, resolveJumpHopCharacterCanvasPosition, selectJumpHopTileAsset, } from '../../services/jump-hop/jumpHopRuntimeModel'; import { useJumpHopLeaderboard } from '../../services/jump-hop/useJumpHopLeaderboard'; import { PlatformActionButton } from '../common/PlatformActionButton'; import { RuntimeResourcePendingMarker } from '../common/RuntimeResourcePendingMarker'; type JumpHopRuntimeJumpPayload = { dragDistance: number; dragVectorX: number; dragVectorY: number; }; type JumpHopVisualJump = { from: JumpHopCharacterVisualPosition; to: JumpHopCharacterVisualPosition; }; type JumpHopPlatformRenderItem = JumpHopVisiblePlatform & { renderKey: string; advanceState: 'exiting' | 'camera' | 'idle'; }; type JumpHopTilePreloadItem = { textureKey: string; asset: JumpHopTileAsset; }; type JumpHopTileFaceKey = 'top' | 'front' | 'right' | 'back' | 'left' | 'bottom'; type JumpHopTileTextureSource = { imageSrc: string; imageObjectKey?: string; assetObjectId?: string; tileId?: string; }; type JumpHopRuntimeShellProps = { profile?: JumpHopWorkProfileResponse | null; run?: JumpHopRuntimeRunSnapshotResponse | null; snapshot?: JumpHopRuntimeRunSnapshotResponse | null; isBusy?: boolean; error?: string | null; runtimeRequestOptions?: JumpHopRuntimeRequestOptions; onJump: (payload: JumpHopRuntimeJumpPayload) => Promise; onRestart: () => void; onExit?: () => void; onBack?: () => void; }; const MAX_CHARGE_RATIO = 1; const DEFAULT_MAX_DRAG_DISTANCE_PX = 180; const JUMP_HOP_ANIMATION_DURATION_MS = 560; const JUMP_HOP_LANDING_RECOIL_DURATION_MS = 560; const JUMP_HOP_PLATFORM_ADVANCE_DURATION_MS = 1440; const JUMP_HOP_PLATFORM_RETAIN_OFFSCREEN_SCREEN_Y = 122; const JUMP_HOP_CAMERA_ZOOM = 1.3; const JUMP_HOP_TAONIER_CHARACTER_IMAGE_SRC = '/branding/jump-hop-taonier-character.png'; const JUMP_HOP_TILE_PRELOAD_LOOKAHEAD_COUNT = 3; const JUMP_HOP_TILE_FACE_KEYS: JumpHopTileFaceKey[] = [ 'top', 'front', 'right', 'back', 'left', 'bottom', ]; const JUMP_HOP_THREE_CAMERA_PITCH_RAD = Math.PI / 4; const JUMP_HOP_THREE_CAMERA_PITCH_COS = Math.cos( JUMP_HOP_THREE_CAMERA_PITCH_RAD, ); export const JUMP_HOP_THREE_CAMERA_UP_Y = 1; const JUMP_HOP_THREE_CAMERA_DISTANCE_MULTIPLIER = 1.34; function clamp(value: number, min: number, max: number) { return Math.min(max, Math.max(min, value)); } function formatJumpHopCssNumber(value: number) { if (!Number.isFinite(value)) { return '0'; } return value.toFixed(4).replace(/\.?0+$/, ''); } function getRun( run: JumpHopRuntimeRunSnapshotResponse | null | undefined, snapshot: JumpHopRuntimeRunSnapshotResponse | null | undefined, ) { return run ?? snapshot ?? null; } function hasJumpHopRunDisplayChange( current: JumpHopRuntimeRunSnapshotResponse, next: JumpHopRuntimeRunSnapshotResponse, ) { return ( current.currentPlatformIndex !== next.currentPlatformIndex || current.status !== next.status || current.successfulJumpCount !== next.successfulJumpCount || current.durationMs !== next.durationMs || current.score !== next.score || current.combo !== next.combo || current.finishedAtMs !== next.finishedAtMs || current.lastJump?.targetPlatformIndex !== next.lastJump?.targetPlatformIndex || current.lastJump?.result !== next.lastJump?.result || current.lastJump?.chargeMs !== next.lastJump?.chargeMs ); } function shouldAnimateJumpHopPlatformAdvance( current: JumpHopRuntimeRunSnapshotResponse, next: JumpHopRuntimeRunSnapshotResponse, ) { return ( current.runId === next.runId && next.currentPlatformIndex > current.currentPlatformIndex && next.status === 'playing' ); } function buildJumpHopCharacterVisualPositionFromPlatform( platform: JumpHopVisiblePlatform, isMiss = false, ): JumpHopCharacterVisualPosition { if (isMiss) { return { screenX: platform.screenX + 8, screenY: platform.screenY - 2, sceneX: platform.sceneX + 0.7, sceneY: platform.sceneY + 0.48, sceneZ: platform.sceneZ - 0.4, isMiss: true, }; } return { screenX: platform.screenX, screenY: platform.screenY - 3, sceneX: platform.sceneX, sceneY: platform.sceneY + 0.84, sceneZ: platform.sceneZ, isMiss: false, }; } function getJumpHopRunLandingVisualPosition({ run, platforms, stageSize, }: { run: JumpHopRuntimeRunSnapshotResponse; platforms: JumpHopVisiblePlatform[]; stageSize: { width: number; height: number }; }) { const lastJump = run.lastJump; if (!lastJump || stageSize.width <= 0 || stageSize.height <= 0) { return null; } return getJumpHopCharacterVisualPosition(run, platforms, stageSize); } function getJumpHopThreeCubeSide( platform: JumpHopVisiblePlatform['platform'], scale: number, ) { const platformSize = getJumpHopPlatformVisualSize(platform, scale); return Math.max(56, Math.min(platformSize.width, platformSize.height) * 0.86); } export function getJumpHopThreeProjectedY( screenY: number, viewportHeight: number, ) { return ( viewportHeight / 2 + (viewportHeight / 2 - screenY) / JUMP_HOP_THREE_CAMERA_PITCH_COS ); } function IsometricFallbackTile({ platform, }: { platform: JumpHopVisiblePlatform['platform']; }) { const tone = getJumpHopTileTone(platform.tileType); const style = { '--jump-hop-tile-tone': tone, } as CSSProperties; return (