diff --git a/src/renderer/effects/platformerScroll.test.ts b/src/renderer/effects/platformerScroll.test.ts index 325859b9..523ea4b8 100644 --- a/src/renderer/effects/platformerScroll.test.ts +++ b/src/renderer/effects/platformerScroll.test.ts @@ -7,7 +7,8 @@ import { PlatformerScrollEffect, platformAt, runnerJumpOffset, - supportTopY + runnerTraversalY, + supportTopY, } from "./platformerScroll"; const createGradient = () => ({ addColorStop: vi.fn() }); @@ -87,6 +88,27 @@ describe("platformerScroll helpers", () => { expect(down).toBe(0); }); + it("runnerTraversalY adds a jump arc for upward steps", () => { + const start = runnerTraversalY(220, 188, 0, 0.2); + const preTakeoff = runnerTraversalY(220, 188, 0.04, 0.2); + const peak = runnerTraversalY(220, 188, 0.5, 0.2); + const end = runnerTraversalY(220, 188, 1, 0.2); + + expect(start).toBe(220); + expect(preTakeoff).toBe(220); + expect(peak).toBeLessThan(194); + expect(end).toBe(188); + }); + + it("runnerTraversalY keeps footing briefly before dropping down", () => { + const early = runnerTraversalY(188, 220, 0.1, 0.2); + const late = runnerTraversalY(188, 220, 0.9, 0.2); + + expect(early).toBe(188); + expect(late).toBeGreaterThan(210); + expect(late).toBeLessThanOrEqual(220); + }); + it("buildRunnerSprite creates a colorful mascot silhouette with animated limbs", () => { const earlyFrame = buildRunnerSprite(100, 160, 16, 0, 0.2); const laterFrame = buildRunnerSprite(100, 160, 16, 0.2, 0.2); diff --git a/src/renderer/effects/platformerScroll.ts b/src/renderer/effects/platformerScroll.ts index f383004c..d7b78b59 100644 --- a/src/renderer/effects/platformerScroll.ts +++ b/src/renderer/effects/platformerScroll.ts @@ -116,6 +116,28 @@ export function runnerJumpOffset(prevSupportY: number, currentSupportY: number, return Math.floor(arc * (lift * 0.85 + extra)); } +export function runnerTraversalY(prevSupportY: number, currentSupportY: number, colProgress: number, audioAmount: number): number { + const progress = clamp(colProgress, 0, 1); + const heightDelta = prevSupportY - currentSupportY; + + if (heightDelta > 0) { + const jumpWindowStart = 0.08; + const jumpWindowEnd = 0.96; + const jumpProgress = smoothstep((progress - jumpWindowStart) / (jumpWindowEnd - jumpWindowStart)); + const arcLift = Math.sin(jumpProgress * Math.PI) * Math.max(6, heightDelta * 0.9 + audioAmount * 4); + const baseY = prevSupportY + (currentSupportY - prevSupportY) * jumpProgress; + return baseY - arcLift; + } + + if (heightDelta < 0) { + const ledgeHold = 0.22; + const fallProgress = progress <= ledgeHold ? 0 : smoothstep((progress - ledgeHold) / (1 - ledgeHold)); + return prevSupportY + (currentSupportY - prevSupportY) * fallProgress; + } + + return prevSupportY; +} + export function buildRunnerSprite(baseX: number, footY: number, tileSize: number, time: number, audioAmount: number): RunnerSprite { const unit = Math.max(1, Math.round(tileSize / 16)); const x = baseX - 4 * unit; @@ -406,10 +428,9 @@ export class PlatformerScrollEffect implements Effect { frontPulse ); - const supportY = Math.floor(prevSupportY + (currentSupportY - prevSupportY) * smoothstep(colProgress)); - const hop = runnerJumpOffset(prevSupportY, currentSupportY, colProgress, audioAmount); + const supportY = runnerTraversalY(prevSupportY, currentSupportY, colProgress, audioAmount); const runBob = Math.floor((Math.sin(time * 14) * 1.5 + audioAmount * 1.5) * 0.5); - const runnerFootY = supportY - hop - runBob - 1; + const runnerFootY = Math.floor(supportY) - runBob - 1; const runnerSprite = buildRunnerSprite(runnerBaseX, runnerFootY, tileSize, time, audioAmount); ctx.fillStyle = runnerSprite.shadow.color;