diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml new file mode 100644 index 000000000..e96aef683 --- /dev/null +++ b/.github/workflows/typecheck.yml @@ -0,0 +1,42 @@ +name: TypeScript Check + +on: + pull_request: + branches: [main] + +jobs: + typecheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Setup env files + run: | + for pkg in packages/*/; do + if [ -f "$pkg.env.example" ]; then + cp "$pkg.env.example" "$pkg.env" + fi + done + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build contracts + run: pnpm --filter 'contracts' run build + + - name: Build libs + run: pnpm build:libs && pnpm --filter 'query-server' run build + + - name: TypeScript check + # claim-frontend, auction-frontend: deprecated, events over + run: pnpm --filter '!claim-frontend' --filter '!auction-frontend' check diff --git a/packages/client/src/lib/components/Admin/helpers.ts b/packages/client/src/lib/components/Admin/helpers.ts index 6e6903129..e5deae7be 100644 --- a/packages/client/src/lib/components/Admin/helpers.ts +++ b/packages/client/src/lib/components/Admin/helpers.ts @@ -59,7 +59,7 @@ export function calculateProfitLossForTrip( const previousOutcome = tripOutcomes[i - 1] const outcomeTime = new Date(outcome._createdAt).getTime() - const previousOutcomeValue = previousOutcome?.tripValue || Number(trip.tripCreationCost) // Value from the previous trip, and if it's the first outcome, creation cost of the trip + const previousOutcomeValue = previousOutcome?.tripValue ?? outcome.oldTripValue ?? Number(trip.tripCreationCost) // Value from the previous trip, or oldTripValue (accounts for boosts), or creation cost const currentTripValue = outcome.tripValue || 0 const valueChange = currentTripValue - previousOutcomeValue diff --git a/packages/client/src/lib/components/GameRun/TripProcessing/TripProcessing.svelte b/packages/client/src/lib/components/GameRun/TripProcessing/TripProcessing.svelte index 25852c453..c3427408f 100644 --- a/packages/client/src/lib/components/GameRun/TripProcessing/TripProcessing.svelte +++ b/packages/client/src/lib/components/GameRun/TripProcessing/TripProcessing.svelte @@ -4,6 +4,7 @@ import { playSound } from "$lib/modules/sound" import { backgroundMusic } from "$lib/modules/sound/stores" import { shaderManager } from "$lib/modules/webgl/shaders/index.svelte" + import { singleFrameRender } from "$lib/modules/ui/state.svelte" import TripProcessingPopUp from "./TripProcessingPopUp.svelte" const { @@ -45,9 +46,11 @@ shaderManager.enableContinuousRendering() // Set the trip processing shader with custom uniforms + // u_quality: 0.0 on mobile (skip madness layer), 1.0 on desktop (full quality) shaderManager.setShader("tripProcessing", false, { u_seed1: { type: "float", value: seed1 }, - u_seed2: { type: "float", value: seed2 } + u_seed2: { type: "float", value: seed2 }, + u_quality: { type: "float", value: $singleFrameRender ? 0.0 : 1.0 } }) // Start timer diff --git a/packages/webgl/src/ShaderManager.ts b/packages/webgl/src/ShaderManager.ts index 72cd45fe5..48482be8d 100644 --- a/packages/webgl/src/ShaderManager.ts +++ b/packages/webgl/src/ShaderManager.ts @@ -2,6 +2,7 @@ import { WebGLGeneralRenderer } from "./WebGLGeneralRenderer" import { shaders } from "./shaders" import { ShaderInitializationError } from "@ratfun/common/error-handling" import type { UniformType, UniformDefinition } from "./types" +import { logger } from "./logger" export type ErrorHandler = (error: Error, context?: string) => void @@ -11,6 +12,8 @@ export interface ShaderManagerOptions { singleFrameRender: () => boolean /** Preserve drawing buffer for canvas capture (enables toDataURL) - impacts performance */ preserveDrawingBuffer?: boolean + /** Target FPS (default: 60 desktop, 30 mobile) */ + targetFPS?: number } export class ShaderManager { @@ -31,6 +34,7 @@ export class ShaderManager { private singleFrameRender: () => boolean private singleFramePauseRafId: number | null = null private preserveDrawingBuffer: boolean + private targetFPS: number currentShaderKey: string | null = null private shaderChangeListeners: Array<(shaderKey: string | null) => void> = [] private shaderChangeTimeout: ReturnType | null = null @@ -44,6 +48,8 @@ export class ShaderManager { this.errorHandler = options.errorHandler this.singleFrameRender = options.singleFrameRender this.preserveDrawingBuffer = options.preserveDrawingBuffer ?? false + // 30fps on mobile, 60fps on desktop + this.targetFPS = options.targetFPS ?? (options.singleFrameRender() ? 30 : 60) } /** @@ -239,7 +245,7 @@ export class ShaderManager { } } catch (error) { // Error already logged to Sentry in initializeRenderer - console.error(`Failed to initialize shader "${pendingKey}":`, error) + logger.error(`shader init failed: ${pendingKey}`) // Mark context as exhausted and hide canvas to show CSS fallback this._contextExhausted = true @@ -312,7 +318,8 @@ export class ShaderManager { shader: shaderSource, uniforms: initialUniforms, onError: this.errorHandler, - preserveDrawingBuffer: this.preserveDrawingBuffer + preserveDrawingBuffer: this.preserveDrawingBuffer, + targetFPS: this.targetFPS }) this._renderer.render() @@ -321,7 +328,7 @@ export class ShaderManager { window.removeEventListener("resize", this.handleResize) window.addEventListener("resize", this.handleResize) } catch (error) { - console.error("Failed to initialize WebGL renderer:", error) + logger.error("renderer init failed") this._renderer = null // Report to Sentry @@ -398,7 +405,7 @@ export class ShaderManager { } } catch (error) { // Still failing, try again - console.warn(`Recovery attempt ${this.recoveryAttempts} failed, will retry...`) + logger.warn(`recovery attempt ${this.recoveryAttempts} failed, retrying`) this.scheduleContextRecovery(shaderKey) } } diff --git a/packages/webgl/src/WebGLGeneralRenderer.ts b/packages/webgl/src/WebGLGeneralRenderer.ts index f8920772a..d2f38c52c 100644 --- a/packages/webgl/src/WebGLGeneralRenderer.ts +++ b/packages/webgl/src/WebGLGeneralRenderer.ts @@ -6,6 +6,7 @@ import { WebGLError, UniformLocationError } from "@ratfun/common/error-handling" +import { logger } from "./logger" /** * A general-purpose WebGL renderer that can render shaders to a canvas. @@ -53,7 +54,7 @@ export class WebGLGeneralRenderer implements WebGLRenderer { this.startTime = performance.now() this.uniforms = options.uniforms || {} this.autoRender = options.autoRender ?? true - this.frameInterval = 1000 / 60 + this.frameInterval = 1000 / (options.targetFPS ?? 60) this.shaderSource = options.shader this.onError = options.onError this.preserveDrawingBuffer = options.preserveDrawingBuffer ?? false @@ -239,14 +240,14 @@ export class WebGLGeneralRenderer implements WebGLRenderer { this.animationId = undefined } - console.warn("WebGL context lost") + logger.warn("context lost") } /** * Handles WebGL context restore event. */ private handleContextRestored = (): void => { - console.log("WebGL context restored, reinitializing...") + logger.log("context restored, reinitializing") this.contextLost = false try { @@ -255,7 +256,7 @@ export class WebGLGeneralRenderer implements WebGLRenderer { this.render() } } catch (error) { - console.error("Failed to restore WebGL context:", error) + logger.error("failed to restore context") // Error already reported in initWebGL } } @@ -520,7 +521,7 @@ export class WebGLGeneralRenderer implements WebGLRenderer { } } catch (error) { // WebGL context may be lost or invalid, ignore cleanup errors - console.warn("WebGL cleanup failed (context may be lost):", error) + logger.warn("cleanup failed, context may be lost") } finally { this.uniformLocations.clear() // Clear references to allow garbage collection diff --git a/packages/webgl/src/index.ts b/packages/webgl/src/index.ts index cf9c3a904..9c273714a 100644 --- a/packages/webgl/src/index.ts +++ b/packages/webgl/src/index.ts @@ -1,6 +1,7 @@ // Main exports export { WebGLGeneralRenderer, createWebGLRenderer } from "./WebGLGeneralRenderer" export { ShaderManager, createShaderManager } from "./ShaderManager" +export { setWebGLLoggingEnabled, isWebGLLoggingEnabled } from "./logger" // Type exports export type { diff --git a/packages/webgl/src/logger.ts b/packages/webgl/src/logger.ts new file mode 100644 index 000000000..a3f2ccfe0 --- /dev/null +++ b/packages/webgl/src/logger.ts @@ -0,0 +1,29 @@ +// webgl package logger - disabled by default to prevent memory leaks +// enable via setWebGLLoggingEnabled(true) during development + +let loggingEnabled = false + +export function setWebGLLoggingEnabled(enabled: boolean): void { + loggingEnabled = enabled +} + +export function isWebGLLoggingEnabled(): boolean { + return loggingEnabled +} + +export const logger = { + log: (...args: unknown[]): void => { + if (loggingEnabled) { + console.log("[WebGL]", ...args) + } + }, + warn: (...args: unknown[]): void => { + if (loggingEnabled) { + console.warn("[WebGL]", ...args) + } + }, + error: (...args: unknown[]): void => { + // errors always logged regardless of loggingEnabled + console.error("[WebGL]", ...args) + } +} diff --git a/packages/webgl/src/shaders/trip-processing/fragment.glsl b/packages/webgl/src/shaders/trip-processing/fragment.glsl index 1dfe16a76..c0e9797a8 100644 --- a/packages/webgl/src/shaders/trip-processing/fragment.glsl +++ b/packages/webgl/src/shaders/trip-processing/fragment.glsl @@ -45,6 +45,12 @@ precision highp float; #define WARP3_MULTIPLIER 3.33 // Warp3 intensity multiplier #define WARP3_STRENGTH 1.5 // Warp3 displacement strength +// LOD time thresholds (must match complexity thresholds to avoid negative amplitude) +// complexityFactor = u_time / 10.0, so threshold 0.6 = 6 seconds +#define LOD_TIME_TURB 6.0 // Enable turbulence at t=6s (complexity=0.6) +#define LOD_TIME_WARP3 7.0 // Enable warp3 at t=7s (complexity=0.7) +#define LOD_TIME_MADNESS 8.0 // Enable madness at t=8s (complexity=0.8) + // Turbulence parameters #define TURB_THRESHOLD 0.6 // Complexity threshold for turbulence #define TURB_MULTIPLIER 2.5 // Turbulence pattern multiplier @@ -126,6 +132,7 @@ uniform vec2 u_resolution; // Screen resolution (width, height) uniform bool u_invert; // Color inversion toggle uniform float u_seed1; // First seed value (0-1) uniform float u_seed2; // Second seed value (0-1) +uniform float u_quality; // Quality tier (0.0=mobile, 1.0=desktop) // ============================================================================ // HASH FUNCTIONS @@ -315,7 +322,8 @@ void main() { warpedUV += vec2(sin(warp2 * TWO_PI), cos(warp2 * TWO_PI)) * (WARP2_STRENGTH_BASE + chaosIntensity * WARP2_STRENGTH_CHAOS); // Coordinate warping - layer 3 (extreme chaos) - if (complexityFactor > WARP3_THRESHOLD) { + // LOD: uniform-based branch allows GPU to skip entirely before 7s + if (u_time > LOD_TIME_WARP3) { float warp3a = rotatedNoise(warpedUV * (10.0 + seedOffset1 * 0.1), noiseRotation2, smoothness1); float warp3b = ridgedNoise(warpedUV * (8.0 + seedOffset2 * 0.1), smoothness2); float warp3 = mix(warp3a, warp3b, combinedSeed) * (complexityFactor - WARP3_THRESHOLD) * WARP3_MULTIPLIER; @@ -359,7 +367,8 @@ void main() { pattern += noisePattern2 * chaosIntensity * LAYER5_CHAOS_WEIGHT; // Layer 6: Extreme turbulence (conditional) - if (complexityFactor > TURB_THRESHOLD) { + // LOD: uniform-based branch allows GPU to skip entirely before 6s + if (u_time > LOD_TIME_TURB) { vec2 turbCoord = warpedUV * (12.0 + combinedSeed * 20.0); vec2 spiralVec = vec2(spiralTime); @@ -374,7 +383,8 @@ void main() { } // Layer 7: Complete madness (conditional) - if (complexityFactor > MAD_THRESHOLD) { + // LOD: skip on mobile (u_quality < 1.0) and before 5s + if (u_time > LOD_TIME_MADNESS && u_quality > 0.5) { float madness = (complexityFactor - MAD_THRESHOLD) * MAD_MULTIPLIER; vec2 madCoord = warpedUV * (20.0 + seedOffset1 * HALF) + vec2(spiralTime * TWO, -spiralTime * 1.5);