fix: 优化跳一跳运行态与地块资源
This commit is contained in:
@@ -6,28 +6,28 @@ import type {
|
||||
} from '../../../packages/shared/src/contracts/jumpHop';
|
||||
import {
|
||||
buildJumpHopVisiblePlatforms,
|
||||
getJumpHopBackendDragVector,
|
||||
getJumpHopCharacterVisualPosition,
|
||||
getJumpHopJumpFeedbackLabel,
|
||||
getJumpHopLandingAssistVisualPosition,
|
||||
getJumpHopPlatformVisualSize,
|
||||
getJumpHopStatusLabel,
|
||||
isJumpHopLandingInsidePlatformFootprint,
|
||||
resolveJumpHopCharacterCanvasPosition,
|
||||
selectJumpHopTileAsset,
|
||||
} from './jumpHopRuntimeModel';
|
||||
|
||||
test('跳一跳地块池按平台编号从 25 个素材中抽取而不是按类型压扁', () => {
|
||||
const tileAssets = Array.from({ length: 25 }, (_, index) => ({
|
||||
test('跳一跳地块池按平台编号从 18 个素材中抽取而不是按类型压扁', () => {
|
||||
const tileAssets = Array.from({ length: 18 }, (_, index) => ({
|
||||
tileType: 'normal',
|
||||
tileId: `tile-${String(index + 1).padStart(2, '0')}`,
|
||||
imageSrc: `asset-${index + 1}`,
|
||||
imageObjectKey: `key-${index + 1}`,
|
||||
assetObjectId: `object-${index + 1}`,
|
||||
sourceAtlasCell: `row-1-col-${index + 1}`,
|
||||
atlasRow: 1,
|
||||
atlasCol: index + 1,
|
||||
sourceAtlasCell: `row-${Math.floor(index / 3) + 1}-col-${(index % 3) + 1}`,
|
||||
atlasRow: Math.floor(index / 3) + 1,
|
||||
atlasCol: (index % 3) + 1,
|
||||
visualWidth: 256,
|
||||
visualHeight: 192,
|
||||
visualHeight: 256,
|
||||
topSurfaceRadius: 42,
|
||||
landingRadius: 34,
|
||||
})) satisfies JumpHopTileAsset[];
|
||||
@@ -59,15 +59,17 @@ test('跳一跳可见平台窗口固定为 3 个并携带选中的地块素材',
|
||||
platform(0.8, 5.1, 'normal'),
|
||||
],
|
||||
};
|
||||
const tileAssets = Array.from({ length: 25 }, (_, index) => ({
|
||||
const tileAssets = Array.from({ length: 18 }, (_, index) => ({
|
||||
tileType: 'normal',
|
||||
tileId: `tile-${String(index + 1).padStart(2, '0')}`,
|
||||
imageSrc: `asset-${index + 1}`,
|
||||
imageObjectKey: `key-${index + 1}`,
|
||||
assetObjectId: `object-${index + 1}`,
|
||||
sourceAtlasCell: `row-1-col-${index + 1}`,
|
||||
sourceAtlasCell: `row-${Math.floor(index / 3) + 1}-col-${(index % 3) + 1}`,
|
||||
atlasRow: Math.floor(index / 3) + 1,
|
||||
atlasCol: (index % 3) + 1,
|
||||
visualWidth: 256,
|
||||
visualHeight: 192,
|
||||
visualHeight: 256,
|
||||
topSurfaceRadius: 42,
|
||||
landingRadius: 34,
|
||||
})) satisfies JumpHopTileAsset[];
|
||||
@@ -119,12 +121,12 @@ test('跳一跳三块可见地块按下方中部上方展开且角色落在当
|
||||
visible,
|
||||
);
|
||||
|
||||
expect(visible[0]?.screenY).toBeGreaterThanOrEqual(68);
|
||||
expect(visible[0]?.screenY).toBeLessThanOrEqual(80);
|
||||
expect(visible[0]?.screenY).toBeGreaterThanOrEqual(60);
|
||||
expect(visible[0]?.screenY).toBeLessThanOrEqual(66);
|
||||
expect(visible[1]?.screenY).toBeGreaterThanOrEqual(40);
|
||||
expect(visible[1]?.screenY).toBeLessThan(visible[0]?.screenY ?? 0);
|
||||
expect(visible[2]?.screenY).toBeLessThan(visible[1]?.screenY ?? 0);
|
||||
expect(visible[2]?.screenY).toBeLessThanOrEqual(26);
|
||||
expect(visible[2]?.screenY).toBeLessThanOrEqual(32);
|
||||
expect(Math.abs((visible[1]?.screenX ?? 0) - (visible[0]?.screenX ?? 0))).toBeGreaterThan(5);
|
||||
expect(Math.abs((visible[2]?.screenX ?? 0) - (visible[1]?.screenX ?? 0))).toBeGreaterThan(5);
|
||||
expect(character?.screenX).toBeCloseTo(visible[0]?.screenX ?? 0, 1);
|
||||
@@ -216,8 +218,8 @@ test('跳一跳三维角色画布坐标与屏幕坐标同向映射到下方起
|
||||
|
||||
expect(canvasPosition?.x).toBeGreaterThan(140);
|
||||
expect(canvasPosition?.x).toBeLessThan(180);
|
||||
expect(canvasPosition?.y).toBeGreaterThan(380);
|
||||
expect(canvasPosition?.y).toBeLessThan(450);
|
||||
expect(canvasPosition?.y).toBeGreaterThan(330);
|
||||
expect(canvasPosition?.y).toBeLessThan(370);
|
||||
});
|
||||
|
||||
test('跳一跳运行态当前地块视觉尺寸按原调参结果放大一倍', () => {
|
||||
@@ -227,7 +229,7 @@ test('跳一跳运行态当前地块视觉尺寸按原调参结果放大一倍',
|
||||
expect(size.height).toBeCloseTo(103.68, 2);
|
||||
});
|
||||
|
||||
test('跳一跳落点辅助标识按后端落点规则随拖拽方向和距离投影', () => {
|
||||
test('跳一跳落点预测按蓄力值沿下一地块中心方向投影', () => {
|
||||
const path: JumpHopPath = {
|
||||
seed: 'forest-tea',
|
||||
difficulty: 'standard',
|
||||
@@ -265,22 +267,12 @@ test('跳一跳落点辅助标识按后端落点规则随拖拽方向和距离
|
||||
const current = visible[0]!;
|
||||
const target = visible[1]!;
|
||||
const stageSize = { width: 320, height: 568 };
|
||||
const currentCanvasPosition = {
|
||||
x: (current.screenX / 100) * stageSize.width,
|
||||
y: (current.screenY / 100) * stageSize.height,
|
||||
};
|
||||
const targetCanvasPosition = {
|
||||
x: (target.screenX / 100) * stageSize.width,
|
||||
y: (target.screenY / 100) * stageSize.height,
|
||||
};
|
||||
const targetWorldDistance = Math.hypot(
|
||||
target.platform.x - current.platform.x,
|
||||
target.platform.y - current.platform.y,
|
||||
);
|
||||
const fullDragDistance =
|
||||
targetWorldDistance / path.scoring.chargeToDistanceRatio;
|
||||
const dragVectorX = -(targetCanvasPosition.x - currentCanvasPosition.x);
|
||||
const dragVectorY = -(targetCanvasPosition.y - currentCanvasPosition.y);
|
||||
|
||||
const fullAssist = getJumpHopLandingAssistVisualPosition(
|
||||
run,
|
||||
@@ -288,8 +280,6 @@ test('跳一跳落点辅助标识按后端落点规则随拖拽方向和距离
|
||||
character,
|
||||
stageSize,
|
||||
fullDragDistance,
|
||||
dragVectorX,
|
||||
dragVectorY,
|
||||
);
|
||||
const halfAssist = getJumpHopLandingAssistVisualPosition(
|
||||
run,
|
||||
@@ -297,23 +287,21 @@ test('跳一跳落点辅助标识按后端落点规则随拖拽方向和距离
|
||||
character,
|
||||
stageSize,
|
||||
fullDragDistance / 2,
|
||||
dragVectorX,
|
||||
dragVectorY,
|
||||
);
|
||||
|
||||
expect(fullAssist?.screenX).toBeCloseTo(target.screenX, 1);
|
||||
expect(fullAssist?.screenY).toBeCloseTo(target.screenY, 1);
|
||||
expect(fullAssist?.screenY).toBeCloseTo(target.screenY - 3, 1);
|
||||
expect(halfAssist?.screenX).toBeCloseTo(
|
||||
current.screenX + (target.screenX - current.screenX) / 2,
|
||||
1,
|
||||
);
|
||||
expect(halfAssist?.screenY).toBeCloseTo(
|
||||
current.screenY + (target.screenY - current.screenY) / 2,
|
||||
current.screenY + (target.screenY - current.screenY) / 2 - 3,
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
test('跳一跳落点辅助标识使用屏幕坐标向后拖拽并投向上方目标地块', () => {
|
||||
test('跳一跳落点预测忽略旧客户端拖拽方向', () => {
|
||||
const path: JumpHopPath = {
|
||||
seed: 'forest-tea',
|
||||
difficulty: 'standard',
|
||||
@@ -351,16 +339,6 @@ test('跳一跳落点辅助标识使用屏幕坐标向后拖拽并投向上方
|
||||
const current = visible[0]!;
|
||||
const target = visible[1]!;
|
||||
const stageSize = { width: 320, height: 568 };
|
||||
const currentCanvasPosition = {
|
||||
x: (current.screenX / 100) * stageSize.width,
|
||||
y: (current.screenY / 100) * stageSize.height,
|
||||
};
|
||||
const targetCanvasPosition = {
|
||||
x: (target.screenX / 100) * stageSize.width,
|
||||
y: (target.screenY / 100) * stageSize.height,
|
||||
};
|
||||
const dragVectorX = -(targetCanvasPosition.x - currentCanvasPosition.x);
|
||||
const dragVectorY = currentCanvasPosition.y - targetCanvasPosition.y;
|
||||
const targetWorldDistance = Math.hypot(
|
||||
target.platform.x - current.platform.x,
|
||||
target.platform.y - current.platform.y,
|
||||
@@ -374,16 +352,29 @@ test('跳一跳落点辅助标识使用屏幕坐标向后拖拽并投向上方
|
||||
character,
|
||||
stageSize,
|
||||
fullDragDistance,
|
||||
dragVectorX,
|
||||
dragVectorY,
|
||||
-999,
|
||||
-999,
|
||||
);
|
||||
|
||||
expect(dragVectorY).toBeGreaterThan(0);
|
||||
expect(assist?.screenX).toBeCloseTo(target.screenX, 1);
|
||||
expect(assist?.screenY).toBeCloseTo(target.screenY, 1);
|
||||
expect(assist?.screenY).toBeCloseTo(target.screenY - 3, 1);
|
||||
expect(assist?.isOnTargetPlatform).toBe(true);
|
||||
});
|
||||
|
||||
test('跳一跳后端落点向量会把屏幕拖拽换算为世界尺度一致的反向弹射', () => {
|
||||
test('跳一跳落点预测用收缩后的视觉顶面 footprint 判断命中', () => {
|
||||
const target = {
|
||||
...platform(1, 0, 'normal'),
|
||||
width: 2,
|
||||
height: 0.6,
|
||||
landingRadius: 0.2,
|
||||
};
|
||||
|
||||
expect(isJumpHopLandingInsidePlatformFootprint(target, 1.6, 0)).toBe(true);
|
||||
expect(isJumpHopLandingInsidePlatformFootprint(target, 1.8, 0)).toBe(false);
|
||||
expect(isJumpHopLandingInsidePlatformFootprint(target, 1, 0.18)).toBe(false);
|
||||
});
|
||||
|
||||
test('跳一跳成功落地后保留真实落点偏移而不是吸附到地块中心', () => {
|
||||
const path: JumpHopPath = {
|
||||
seed: 'forest-tea',
|
||||
difficulty: 'standard',
|
||||
@@ -406,41 +397,34 @@ test('跳一跳后端落点向量会把屏幕拖拽换算为世界尺度一致
|
||||
profileId: 'profile-1',
|
||||
ownerUserId: 'user-1',
|
||||
status: 'playing',
|
||||
currentPlatformIndex: 0,
|
||||
successfulJumpCount: 0,
|
||||
currentPlatformIndex: 1,
|
||||
successfulJumpCount: 1,
|
||||
durationMs: 0,
|
||||
score: 0,
|
||||
score: 1,
|
||||
combo: 0,
|
||||
path,
|
||||
lastJump: null,
|
||||
lastJump: {
|
||||
chargeMs: 300,
|
||||
jumpDistance: 1.0,
|
||||
targetPlatformIndex: 1,
|
||||
landedX: 0.52,
|
||||
landedY: 0.78,
|
||||
result: 'hit',
|
||||
},
|
||||
startedAtMs: 1000,
|
||||
finishedAtMs: null,
|
||||
} as const;
|
||||
const visible = buildJumpHopVisiblePlatforms(path, 0, []);
|
||||
const current = visible[0]!;
|
||||
const target = visible[1]!;
|
||||
const stageSize = { width: 320, height: 568 };
|
||||
const currentCanvasPosition = {
|
||||
x: (current.screenX / 100) * stageSize.width,
|
||||
y: (current.screenY / 100) * stageSize.height,
|
||||
};
|
||||
const targetCanvasPosition = {
|
||||
x: (target.screenX / 100) * stageSize.width,
|
||||
y: (target.screenY / 100) * stageSize.height,
|
||||
};
|
||||
const dragVectorX = -(targetCanvasPosition.x - currentCanvasPosition.x);
|
||||
const dragVectorY = currentCanvasPosition.y - targetCanvasPosition.y;
|
||||
const backendVector = getJumpHopBackendDragVector(
|
||||
run,
|
||||
visible,
|
||||
stageSize,
|
||||
dragVectorX,
|
||||
dragVectorY,
|
||||
);
|
||||
const visible = buildJumpHopVisiblePlatforms(path, 1, []);
|
||||
const character = getJumpHopCharacterVisualPosition(run, visible, {
|
||||
width: 320,
|
||||
height: 568,
|
||||
});
|
||||
const currentCenter = visible[0]!;
|
||||
|
||||
expect(backendVector.dragVectorX).toBeLessThan(0);
|
||||
expect(backendVector.dragVectorY).toBeGreaterThan(0);
|
||||
expect(Math.abs(backendVector.dragVectorY)).toBeLessThan(Math.abs(dragVectorY));
|
||||
expect(character?.screenX).not.toBeCloseTo(currentCenter.screenX, 1);
|
||||
expect(character?.screenY).not.toBeCloseTo(currentCenter.screenY - 3, 1);
|
||||
expect(character?.screenX).toBeLessThan(currentCenter.screenX);
|
||||
expect(character?.screenY).toBeGreaterThan(currentCenter.screenY - 3);
|
||||
});
|
||||
|
||||
test('跳一跳运行态公开反馈不再展示旧 perfect 和通关语义', () => {
|
||||
|
||||
@@ -42,6 +42,7 @@ export type JumpHopLandingAssistVisualPosition = {
|
||||
screenX: number;
|
||||
screenY: number;
|
||||
targetPlatformIndex: number;
|
||||
isOnTargetPlatform: boolean;
|
||||
};
|
||||
|
||||
export type JumpHopBackendDragVector = {
|
||||
@@ -49,12 +50,19 @@ export type JumpHopBackendDragVector = {
|
||||
dragVectorY: number;
|
||||
};
|
||||
|
||||
const JUMP_HOP_DEFAULT_CHARGE_TO_DISTANCE_RATIO = 0.004;
|
||||
const JUMP_HOP_DEFAULT_STAGE_SIZE: JumpHopCanvasSize = {
|
||||
width: 320,
|
||||
height: 568,
|
||||
};
|
||||
const VISIBLE_PLATFORM_COUNT = 3;
|
||||
const JUMP_HOP_STAGE_WORLD_SCALE = 4.2;
|
||||
const JUMP_HOP_STAGE_FORWARD_SCALE = 3;
|
||||
const JUMP_HOP_VISIBLE_PLATFORM_SCREEN_Y = [78, 50, 22] as const;
|
||||
const JUMP_HOP_VISIBLE_PLATFORM_SCREEN_Y = [64, 47, 30] as const;
|
||||
const JUMP_HOP_PLATFORM_VISUAL_SIZE_MULTIPLIER = 2;
|
||||
const JUMP_HOP_SCREEN_X_WORLD_PERCENT = 16 * 0.96;
|
||||
const JUMP_HOP_SCREEN_X_WORLD_PERCENT = 11.2;
|
||||
const JUMP_HOP_TOP_FACE_HITBOX_WIDTH_RATIO = 0.72;
|
||||
const JUMP_HOP_TOP_FACE_HITBOX_HEIGHT_RATIO = 0.52;
|
||||
|
||||
const tileToneByType: Record<JumpHopTileType, string> = {
|
||||
accent: '#e0f2fe',
|
||||
@@ -128,7 +136,7 @@ export function buildJumpHopVisiblePlatforms(
|
||||
: depth === 1
|
||||
? JUMP_HOP_VISIBLE_PLATFORM_SCREEN_Y[1]
|
||||
: JUMP_HOP_VISIBLE_PLATFORM_SCREEN_Y[2];
|
||||
const screenX = clamp(50 + dx * 16 * worldScale, 14, 86);
|
||||
const screenX = clamp(50 + dx * JUMP_HOP_SCREEN_X_WORLD_PERCENT, 14, 86);
|
||||
|
||||
return {
|
||||
platform,
|
||||
@@ -198,6 +206,31 @@ function getJumpHopCanvasPosition(
|
||||
};
|
||||
}
|
||||
|
||||
function getJumpHopCharacterVisualPositionFromPlatform(
|
||||
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 getJumpHopScreenWorldScales(
|
||||
currentPlatform: JumpHopVisiblePlatform,
|
||||
targetPlatform: JumpHopVisiblePlatform,
|
||||
@@ -257,6 +290,155 @@ function getJumpHopScreenWorldScales(
|
||||
};
|
||||
}
|
||||
|
||||
export function getJumpHopWorldLandingVisualPosition(
|
||||
originPlatform: JumpHopVisiblePlatform | null | undefined,
|
||||
scalePlatform: JumpHopVisiblePlatform | null | undefined,
|
||||
stageSize: JumpHopCanvasSize,
|
||||
landedX: number,
|
||||
landedY: number,
|
||||
isMiss = false,
|
||||
): JumpHopCharacterVisualPosition | null {
|
||||
if (
|
||||
!originPlatform ||
|
||||
!scalePlatform ||
|
||||
stageSize.width <= 0 ||
|
||||
stageSize.height <= 0 ||
|
||||
!Number.isFinite(landedX) ||
|
||||
!Number.isFinite(landedY)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const scales = getJumpHopScreenWorldScales(
|
||||
originPlatform,
|
||||
scalePlatform,
|
||||
stageSize,
|
||||
);
|
||||
const worldDeltaX = landedX - originPlatform.platform.x;
|
||||
const worldDeltaY = landedY - originPlatform.platform.y;
|
||||
const landedPixelX =
|
||||
scales.currentCanvasPosition.x +
|
||||
worldDeltaX * scales.signedXScreenPerWorld;
|
||||
const landedPixelY =
|
||||
scales.currentCanvasPosition.y +
|
||||
worldDeltaY * scales.signedYScreenPerWorld;
|
||||
const sceneDeltaX =
|
||||
(landedX - originPlatform.platform.x) * JUMP_HOP_STAGE_WORLD_SCALE;
|
||||
const sceneDeltaZ =
|
||||
(landedY - originPlatform.platform.y) * JUMP_HOP_STAGE_FORWARD_SCALE;
|
||||
|
||||
return {
|
||||
screenX: clamp((landedPixelX / stageSize.width) * 100, 6, 94),
|
||||
screenY: clamp((landedPixelY / stageSize.height) * 100 - 3, 10, 92),
|
||||
sceneX: originPlatform.sceneX + sceneDeltaX,
|
||||
sceneY: originPlatform.sceneY + (isMiss ? 0.48 : 0.84),
|
||||
sceneZ: originPlatform.sceneZ + sceneDeltaZ,
|
||||
isMiss,
|
||||
};
|
||||
}
|
||||
|
||||
export function isJumpHopLandingInsidePlatformFootprint(
|
||||
platform: JumpHopPlatform | null | undefined,
|
||||
landedX: number,
|
||||
landedY: number,
|
||||
) {
|
||||
if (
|
||||
!platform ||
|
||||
!Number.isFinite(landedX) ||
|
||||
!Number.isFinite(landedY)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const halfWidth = Math.max(
|
||||
0,
|
||||
platform.width * 0.5 * JUMP_HOP_TOP_FACE_HITBOX_WIDTH_RATIO,
|
||||
);
|
||||
const halfHeight = Math.max(
|
||||
0,
|
||||
platform.height * 0.5 * JUMP_HOP_TOP_FACE_HITBOX_HEIGHT_RATIO,
|
||||
);
|
||||
return (
|
||||
Math.abs(landedX - platform.x) <= halfWidth &&
|
||||
Math.abs(landedY - platform.y) <= halfHeight
|
||||
);
|
||||
}
|
||||
|
||||
function getJumpHopSuccessfulLandingVisualPosition(
|
||||
run: JumpHopRuntimeRunSnapshotResponse,
|
||||
platforms: JumpHopVisiblePlatform[],
|
||||
stageSize: JumpHopCanvasSize,
|
||||
) {
|
||||
const lastJump = run.lastJump;
|
||||
if (!lastJump) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const landedPlatform =
|
||||
platforms.find((item) => item.index === run.currentPlatformIndex) ??
|
||||
platforms.find((item) => item.index === lastJump.targetPlatformIndex) ??
|
||||
null;
|
||||
if (!landedPlatform) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const previousPlatformIndex = Math.max(0, lastJump.targetPlatformIndex - 1);
|
||||
const previousWindowPlatforms = buildJumpHopVisiblePlatforms(
|
||||
run.path,
|
||||
previousPlatformIndex,
|
||||
[],
|
||||
);
|
||||
const previousPlatform =
|
||||
previousWindowPlatforms.find(
|
||||
(item) => item.index === previousPlatformIndex,
|
||||
) ?? null;
|
||||
const targetPlatformInPreviousWindow =
|
||||
previousWindowPlatforms.find(
|
||||
(item) => item.index === lastJump.targetPlatformIndex,
|
||||
) ?? null;
|
||||
const landingInPreviousWindow = getJumpHopWorldLandingVisualPosition(
|
||||
previousPlatform,
|
||||
targetPlatformInPreviousWindow,
|
||||
stageSize,
|
||||
lastJump.landedX,
|
||||
lastJump.landedY,
|
||||
false,
|
||||
);
|
||||
if (!landingInPreviousWindow || !targetPlatformInPreviousWindow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const targetCenterInPreviousWindow =
|
||||
getJumpHopCharacterVisualPositionFromPlatform(
|
||||
targetPlatformInPreviousWindow,
|
||||
);
|
||||
const landedPlatformCenter =
|
||||
getJumpHopCharacterVisualPositionFromPlatform(landedPlatform);
|
||||
const worldDeltaX = lastJump.landedX - landedPlatform.platform.x;
|
||||
const worldDeltaY = lastJump.landedY - landedPlatform.platform.y;
|
||||
|
||||
return {
|
||||
screenX: clamp(
|
||||
landedPlatformCenter.screenX +
|
||||
landingInPreviousWindow.screenX -
|
||||
targetCenterInPreviousWindow.screenX,
|
||||
6,
|
||||
94,
|
||||
),
|
||||
screenY: clamp(
|
||||
landedPlatformCenter.screenY +
|
||||
landingInPreviousWindow.screenY -
|
||||
targetCenterInPreviousWindow.screenY,
|
||||
10,
|
||||
92,
|
||||
),
|
||||
sceneX: landedPlatform.sceneX + worldDeltaX * JUMP_HOP_STAGE_WORLD_SCALE,
|
||||
sceneY: landedPlatform.sceneY + 0.84,
|
||||
sceneZ: landedPlatform.sceneZ + worldDeltaY * JUMP_HOP_STAGE_FORWARD_SCALE,
|
||||
isMiss: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function getJumpHopBackendDragVector(
|
||||
run: JumpHopRuntimeRunSnapshotResponse | null,
|
||||
platforms: JumpHopVisiblePlatform[],
|
||||
@@ -290,8 +472,8 @@ export function getJumpHopLandingAssistVisualPosition(
|
||||
characterPosition: JumpHopCharacterVisualPosition | null,
|
||||
stageSize: JumpHopCanvasSize,
|
||||
dragDistance: number,
|
||||
dragVectorX: number | null,
|
||||
dragVectorY: number | null,
|
||||
_dragVectorX?: number | null,
|
||||
_dragVectorY?: number | null,
|
||||
) {
|
||||
if (
|
||||
!run ||
|
||||
@@ -310,27 +492,13 @@ export function getJumpHopLandingAssistVisualPosition(
|
||||
}
|
||||
const { currentPlatform, targetPlatform } = pair;
|
||||
|
||||
const dragX = dragVectorX ?? 0;
|
||||
const dragY = dragVectorY ?? 0;
|
||||
const dragLength = Math.hypot(dragX, dragY);
|
||||
if (dragLength < 0.0001) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const scales = getJumpHopScreenWorldScales(
|
||||
currentPlatform,
|
||||
targetPlatform,
|
||||
stageSize,
|
||||
);
|
||||
const backendDragVector = getJumpHopBackendDragVector(
|
||||
run,
|
||||
platforms,
|
||||
stageSize,
|
||||
dragX,
|
||||
dragY,
|
||||
);
|
||||
const jumpWorldX = -backendDragVector.dragVectorX;
|
||||
const jumpWorldY = backendDragVector.dragVectorY;
|
||||
const jumpWorldX = targetPlatform.platform.x - currentPlatform.platform.x;
|
||||
const jumpWorldY = targetPlatform.platform.y - currentPlatform.platform.y;
|
||||
const jumpWorldLength = Math.hypot(jumpWorldX, jumpWorldY);
|
||||
if (jumpWorldLength < 0.0001) {
|
||||
return null;
|
||||
@@ -341,13 +509,15 @@ export function getJumpHopLandingAssistVisualPosition(
|
||||
const chargeToDistanceRatio =
|
||||
run.path.scoring.chargeToDistanceRatio > 0
|
||||
? run.path.scoring.chargeToDistanceRatio
|
||||
: 0.008;
|
||||
: JUMP_HOP_DEFAULT_CHARGE_TO_DISTANCE_RATIO;
|
||||
const projectedWorldDistance =
|
||||
clamp(dragDistance, 0, maxDragDistance) * chargeToDistanceRatio;
|
||||
const landedWorldDeltaX =
|
||||
(jumpWorldX / jumpWorldLength) * projectedWorldDistance;
|
||||
const landedWorldDeltaY =
|
||||
(jumpWorldY / jumpWorldLength) * projectedWorldDistance;
|
||||
const landedWorldX = currentPlatform.platform.x + landedWorldDeltaX;
|
||||
const landedWorldY = currentPlatform.platform.y + landedWorldDeltaY;
|
||||
const landedPixelX =
|
||||
scales.currentCanvasPosition.x +
|
||||
landedWorldDeltaX * scales.signedXScreenPerWorld;
|
||||
@@ -357,8 +527,13 @@ export function getJumpHopLandingAssistVisualPosition(
|
||||
|
||||
return {
|
||||
screenX: clamp((landedPixelX / stageSize.width) * 100, 6, 94),
|
||||
screenY: clamp((landedPixelY / stageSize.height) * 100, 10, 92),
|
||||
screenY: clamp((landedPixelY / stageSize.height) * 100 - 3, 10, 92),
|
||||
targetPlatformIndex: targetPlatform.index,
|
||||
isOnTargetPlatform: isJumpHopLandingInsidePlatformFootprint(
|
||||
targetPlatform.platform,
|
||||
landedWorldX,
|
||||
landedWorldY,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -379,39 +554,64 @@ export function resolveJumpHopCharacterCanvasPosition(
|
||||
export function getJumpHopCharacterVisualPosition(
|
||||
run: JumpHopRuntimeRunSnapshotResponse | null,
|
||||
platforms: JumpHopVisiblePlatform[],
|
||||
stageSize: JumpHopCanvasSize = JUMP_HOP_DEFAULT_STAGE_SIZE,
|
||||
) {
|
||||
if (!run) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lastJump = run.lastJump;
|
||||
if (lastJump) {
|
||||
const isMiss = lastJump.result === 'miss';
|
||||
if (!isMiss) {
|
||||
const landedPosition = getJumpHopSuccessfulLandingVisualPosition(
|
||||
run,
|
||||
platforms,
|
||||
stageSize,
|
||||
);
|
||||
if (landedPosition) {
|
||||
return landedPosition;
|
||||
}
|
||||
}
|
||||
|
||||
const originPlatform =
|
||||
platforms.find((item) => item.index === run.currentPlatformIndex) ??
|
||||
platforms[0] ??
|
||||
null;
|
||||
const scalePlatform =
|
||||
platforms.find((item) =>
|
||||
isMiss
|
||||
? item.index === lastJump.targetPlatformIndex
|
||||
: item.index === run.currentPlatformIndex + 1,
|
||||
) ??
|
||||
platforms.find((item) => item.index === lastJump.targetPlatformIndex) ??
|
||||
originPlatform;
|
||||
const landedPosition = getJumpHopWorldLandingVisualPosition(
|
||||
originPlatform,
|
||||
scalePlatform,
|
||||
stageSize,
|
||||
lastJump.landedX,
|
||||
lastJump.landedY,
|
||||
isMiss,
|
||||
);
|
||||
if (landedPosition) {
|
||||
return landedPosition;
|
||||
}
|
||||
}
|
||||
|
||||
const landedPlatform = platforms.find(
|
||||
(item) => item.index === run.currentPlatformIndex,
|
||||
);
|
||||
if (landedPlatform) {
|
||||
return {
|
||||
screenX: landedPlatform.screenX,
|
||||
screenY: landedPlatform.screenY - 3,
|
||||
sceneX: landedPlatform.sceneX,
|
||||
sceneY: landedPlatform.sceneY + 0.84,
|
||||
sceneZ: landedPlatform.sceneZ,
|
||||
isMiss: false,
|
||||
};
|
||||
return getJumpHopCharacterVisualPositionFromPlatform(landedPlatform);
|
||||
}
|
||||
|
||||
const lastJump = run.lastJump;
|
||||
if (lastJump && run.status === 'failed') {
|
||||
const targetPlatform = platforms.find(
|
||||
(item) => item.index === lastJump.targetPlatformIndex,
|
||||
);
|
||||
if (targetPlatform) {
|
||||
return {
|
||||
screenX: targetPlatform.screenX + 8,
|
||||
screenY: targetPlatform.screenY - 2,
|
||||
sceneX: targetPlatform.sceneX + 0.7,
|
||||
sceneY: targetPlatform.sceneY + 0.48,
|
||||
sceneZ: targetPlatform.sceneZ - 0.4,
|
||||
isMiss: true,
|
||||
};
|
||||
return getJumpHopCharacterVisualPositionFromPlatform(targetPlatform, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -505,7 +505,7 @@ describe('miniGameDraftGenerationProgress', () => {
|
||||
'jump-hop-write-draft',
|
||||
]);
|
||||
expect(progress?.phaseId).toBe('jump-hop-tile-atlas');
|
||||
expect(progress?.phaseLabel).toBe('生成 5x5 地块图集');
|
||||
expect(progress?.phaseLabel).toBe('生成 UV 贴图图集');
|
||||
expect(progress?.estimatedRemainingMs).toBe(265_000);
|
||||
});
|
||||
|
||||
@@ -513,7 +513,7 @@ describe('miniGameDraftGenerationProgress', () => {
|
||||
const entries = buildJumpHopGenerationAnchorEntries(null, {
|
||||
themeText: '云端糖果塔',
|
||||
templateId: 'jump-hop',
|
||||
tilePrompt: '云端糖果塔主题的正面30度视角主题物体图集,物体本身作为跳跃落点',
|
||||
tilePrompt: '云端糖果塔主题的3D立方体主题身份方块包装图集',
|
||||
});
|
||||
|
||||
expect(entries).toEqual([
|
||||
@@ -524,8 +524,8 @@ describe('miniGameDraftGenerationProgress', () => {
|
||||
},
|
||||
{
|
||||
id: 'jump-hop-tile-style',
|
||||
label: '地块图集',
|
||||
value: '云端糖果塔主题的正面30度视角主题物体图集,物体本身作为跳跃落点',
|
||||
label: '地块贴图',
|
||||
value: '云端糖果塔主题的3D立方体主题身份方块包装图集',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -408,20 +408,20 @@ const JUMP_HOP_STEPS = [
|
||||
},
|
||||
{
|
||||
id: 'jump-hop-tile-atlas',
|
||||
label: '生成 5x5 地块图集',
|
||||
detail: '调用 image2 生成 25 个主题地块素材。',
|
||||
label: '生成 UV 贴图图集',
|
||||
detail: '调用 image2 一次生成 18 个立方体六面展开包装。',
|
||||
weight: 54,
|
||||
},
|
||||
{
|
||||
id: 'jump-hop-slice-tiles',
|
||||
label: '切分 25 个地块',
|
||||
detail: '按 5 行 5 列切分透明地块 PNG。',
|
||||
label: '切分六面贴图',
|
||||
detail: '按 3 列 6 行与 4x3 UV 网切分 108 张面贴图 PNG。',
|
||||
weight: 24,
|
||||
},
|
||||
{
|
||||
id: 'jump-hop-write-draft',
|
||||
label: '写入正式草稿',
|
||||
detail: '保存地块池、无限路径缓冲和运行态配置。',
|
||||
detail: '保存地板贴图池、无限路径缓冲和运行态配置。',
|
||||
weight: 10,
|
||||
},
|
||||
] as const satisfies ReadonlyArray<MiniGameStepDefinition>;
|
||||
@@ -1183,7 +1183,7 @@ export function buildJumpHopGenerationAnchorEntries(
|
||||
},
|
||||
{
|
||||
key: 'jump-hop-tile-style',
|
||||
label: '地块图集',
|
||||
label: '地块贴图',
|
||||
value:
|
||||
formPayload?.tilePrompt?.trim() ||
|
||||
config?.tilePrompt?.trim() ||
|
||||
|
||||
Reference in New Issue
Block a user