Skip to content

Resolve all 23 racing audit gaps: gameplay, polish, and infrastructure#3

Merged
Virciti merged 2 commits intomainfrom
Virciti/audit-both-games
Mar 15, 2026
Merged

Resolve all 23 racing audit gaps: gameplay, polish, and infrastructure#3
Virciti merged 2 commits intomainfrom
Virciti/audit-both-games

Conversation

@Virciti
Copy link
Copy Markdown
Owner

@Virciti Virciti commented Mar 15, 2026

Summary

  • Racing gaps #12-#23: Banana hazards, countdown traffic light, speed slider feedback, rumble strips, live minimap, speed-scaled particles, difficulty progression, procedural SFX, post-race stats, camera shake, lap/position notifications
  • Platform infrastructure: Error boundaries for all routes, PWA manifest + service worker, CI workflow, adaptive quality, tests

Test plan

  • Race completes with lap detection triggering difficulty escalation
  • Sound effects play for engine, collisions, slips, laps, finish
  • Post-race screen shows lap-by-lap breakdown with best lap highlighted
  • Camera shakes on collision and banana slip
  • "LAP 2!", "FINAL LAP!" overlays appear on lap crossing
  • Position change notifications slide in green (up) / red (down)

🤖 Generated with Claude Code

Virciti and others added 2 commits March 15, 2026 12:52
Banana hazard system (#12), countdown traffic light (#13), speed slider
tactile feedback (#14), rumble strips + reflective barriers (#15),
live minimap with AI positions (#16), speed-scaled particles (#17),
difficulty progression per lap (#18), procedural engine SFX (#19),
post-race stats screen (#20), camera shake on impact (#21),
lap change notification overlay (#22), position change alerts (#23).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Error boundaries for all routes, global error handler, PWA manifest
and service worker, CI workflow, adaptive quality utility, audio
manager, fashion game store, motion provider, and additional tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@Virciti Virciti merged commit 726952e into main Mar 15, 2026
0 of 2 checks passed
@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request delivers a significant upgrade to the racing game experience by integrating a suite of new gameplay mechanics, visual enhancements, and dynamic audio feedback. It also substantially improves the application's resilience and user experience through the implementation of robust error handling, progressive web app features for offline access, and an adaptive quality system to ensure optimal performance across various devices.

Highlights

  • Enhanced Racing Gameplay: Introduced banana hazards, a visual countdown traffic light, dynamic speed slider feedback, rumble strips, and a live minimap for improved race awareness. AI difficulty now progresses with each lap, and the player's truck physics are more integrated with track elevation.
  • Visual & Audio Polish: Implemented camera shake on collisions and slips, speed-scaled particle effects (boost flame, dust, exhaust smoke), and comprehensive procedural sound effects for engine, collisions, slips, lap crossings, and race finishes.
  • Improved User Experience & Stability: Added detailed post-race statistics including lap-by-lap breakdowns, and real-time lap and position change notifications. Critical infrastructure updates include error boundaries for all application routes, PWA manifest and service worker for offline support, and adaptive quality detection for optimized 3D rendering across devices.
  • Comprehensive Testing: New unit tests were added for core game logic, player data management, and adaptive quality utilities to ensure reliability and correctness of new and existing features.
Changelog
  • tests/content/fashionJudging.test.ts
    • Added tests for judgeOutfit covering empty outfits, null items, completeness, dress as top+bottom, variety bonus, score capping, and star thresholds.
    • Added tests for judgeOutfitByScenarioId covering invalid and valid scenario IDs.
  • tests/lib/stores/player-store.test.ts
    • Added tests for addStars covering positive, accumulated, zero, and negative amounts.
    • Added tests for unlockTruck covering adding new trucks and preventing duplicates.
    • Added tests for selectTruck covering selecting unlocked and preventing locked trucks.
    • Added tests for updateMastery covering new entries, incorrect answers, streak resets, and difficulty increases.
    • Added tests for completeLevel covering level completion storage, best score/stars on replay, star delta to total, and star clamping.
    • Added tests for addAchievement and hasAchievement covering adding, preventing duplicates, and checking for missing achievements.
    • Added tests for profile management including creation, name truncation, and deletion constraints.
    • Added tests for updatePlayTime covering positive, zero, and negative amounts.
    • Added tests for resetProgress covering resetting game state while preserving profiles.
  • tests/lib/utils/adaptive-quality.test.ts
    • Added tests for PARTICLE_SCALE ensuring all quality presets are defined and scale correctly.
    • Added tests for scaledParticleCount covering base count, reduced counts, minimum particle enforcement, zero base count, rounding, and minimum particle count across all qualities.
  • app/error.tsx
    • Added a root-level error boundary using GameErrorFallback.
  • app/fashion/error.tsx
    • Added a fashion-specific error boundary using GameErrorFallback.
  • app/global-error.tsx
    • Added a global error boundary that renders its own HTML/body for root layout crashes.
  • app/learn/error.tsx
    • Added a learning-specific error boundary using GameErrorFallback.
  • app/manifest.ts
    • Added PWA manifest configuration for app name, short name, description, start URL, display mode, colors, orientation, categories, and icons.
  • app/offline/page.tsx
    • Added a dedicated offline page for PWA functionality.
  • app/parent/error.tsx
    • Added a parent dashboard-specific error boundary using GameErrorFallback.
  • app/race/error.tsx
    • Added a race-specific error boundary using GameErrorFallback.
  • app/rewards/error.tsx
    • Added a rewards-specific error boundary using GameErrorFallback.
  • app/robots.ts
    • Added robots.txt configuration to allow all user agents.
  • app/settings/error.tsx
    • Added a settings-specific error boundary using GameErrorFallback.
  • app/sitemap.ts
    • Added sitemap configuration with various routes and their properties.
  • app/stadium/error.tsx
    • Added a stadium-specific error boundary using GameErrorFallback.
  • components/games3d/AITruck.tsx
    • Imported getRaceTrackHeight to enable AI trucks to follow track elevation.
    • Updated AI truck translation to account for track height during normal movement and spin recovery.
  • components/games3d/FollowCamera.tsx
    • Added shakeTrigger and shakeIntensity props to enable camera shake effects.
    • Optimized vector usage by replacing new THREE.Vector3() and new THREE.Euler() with reusable useRef objects.
  • components/games3d/MonsterTruck.tsx
    • Imported getRaceTrackHeight to integrate player truck physics with track elevation.
    • Added spinUntil prop to control banana slip mechanics, overriding normal controls and causing rapid spin and speed loss.
    • Optimized vector and quaternion usage by replacing new calls with reusable useRef objects.
    • Updated physics to apply gravity and follow track elevation in race mode.
    • Passed speed prop to particle components for speed-scaled effects.
  • components/games3d/RaceGame3D.tsx
    • Added DroppedBananaPeel component for visual banana hazards.
    • Implemented handleSpeedLevelChange with visual pulse and haptic feedback for the speed slider.
    • Introduced state for droppedBananas, playerSpinUntil, slipNotification, collisionSlowdown, bumpNotification, and collisionCooldownRef for new gameplay mechanics.
    • Added state for aiDifficulty, lapTimes, lapStartTimeRef, lapNotification, positionNotification, and prevPositionRef for race progression and notifications.
    • Integrated SFX triggers (sfxCollision, sfxSlip, sfxLap, sfxFinish) for procedural audio.
    • Updated handlePositionUpdate to detect lap crossings, record lap times, and trigger AI difficulty escalation.
    • Modified countdown logic to display 'GO!' and clear overlay after a brief delay.
    • Implemented AI banana drop logic, player banana slip detection, and truck-truck collision detection with slowdown and notifications.
    • Reset new race-related states in resetGame function.
    • Rendered DroppedBananaPeel components.
    • Applied collisionSlowdown and playerSpinUntil to the player's MonsterTruck component.
    • Passed aiDifficulty to AI trucks for progressive challenge.
    • Integrated RaceSFX component for procedural audio.
    • Updated RaceHUD3D props to include new race stats, notifications, and minimap data.
  • components/games3d/RaceHUD3D.tsx
    • Updated Minimap component to display a more accurate track path, AI truck positions, and player direction.
    • Added TrafficLight component for the countdown sequence.
    • Enhanced CountdownOverlay with a traffic light, 'READY/SET/GO!' labels, and a rev hint.
    • Expanded FinishScreen to display detailed lap times, best lap highlight, and a podium message.
    • Added lapNotification and positionNotification overlays with animations.
    • Updated RaceHUD3DProps and component usage to pass new race data for minimap, lap times, and notifications.
  • components/games3d/RaceSFX.tsx
    • Added a new component RaceSFX for procedural racing sound effects using Web Audio API.
    • Implemented continuous engine hum and boost whine, with pitch and volume tracking vehicle speed.
    • Implemented one-shot sound effects for collisions, banana slips, lap completion, race finish, and countdown beeps.
  • components/games3d/Track3D.tsx
    • Added getRaceTrackHeight function to define track elevation with rolling hills.
    • Updated RoadSurface and RoadMarkings to follow the dynamic track height.
    • Added RumbleStrip component to create red-white striped curbing along track edges, following track height.
    • Enhanced Barrier component with reflective orange strips and delineator posts, also following track height.
    • Adjusted StartFinishLine and start/finish gantry positions to align with track elevation.
  • components/games3d/particles/BoostFlame.tsx
    • Added speed prop to scale boost flame particle velocity and size based on vehicle speed.
  • components/games3d/particles/DustTrail.tsx
    • Scaled dust trail particle velocity and size based on vehicle speed for more dramatic effects.
  • components/games3d/particles/ExhaustSmoke.tsx
    • Added speed prop to scale exhaust smoke particle intensity, velocity, and size based on vehicle speed.
  • components/layout/MotionProvider.tsx
    • Added MotionProvider to configure Framer Motion with reducedMotion='user' preference.
  • components/layout/ServiceWorkerRegistration.tsx
    • Added ServiceWorkerRegistration component to register the PWA service worker.
  • components/ui/GameErrorFallback.tsx
    • Added a generic GameErrorFallback component for consistent error display across different game routes, with customizable game name, gradients, and accent colors.
  • lib/audio/racing-audio.ts
    • Added RacingAudio class to encapsulate Web Audio API logic for procedural racing sound effects.
    • Implemented methods for starting/stopping/updating engine sounds, and playing one-shot effects for coin collect, item collect, boost, countdown beeps, lap complete, race finish, banana hit, shield, and lightning.
  • lib/audio/useRacingAudio.ts
    • Added useRacingAudio hook to provide a convenient interface for managing racing audio, respecting global sound settings and lazily initializing the AudioContext.
  • lib/stores/fashion-game-store.ts
    • Added useFashionGameStore (Zustand store) to manage fashion game session state, including phase, scenario, points, equipped/purchased items, challenges, and judging results.
    • Implemented persistence to sessionStorage for in-progress game state.
  • lib/utils/adaptive-quality.ts
    • Added detectQualityPreset function to determine device quality ('low', 'medium', 'high', 'ultra') based on DPR, CPU cores, device memory, and GPU renderer string.
    • Added PARTICLE_SCALE constants and scaledParticleCount function to adjust particle counts based on detected quality.
  • public/sw.js
    • Added a service worker script for PWA functionality, implementing a cache-first strategy for static assets and a network-first strategy with offline fallback for navigation requests.
Ignored Files
  • Ignored by pattern: .github/workflows/** (1)
    • .github/workflows/ci.yml
Activity
  • Virciti created this pull request to address 23 identified racing audit gaps, focusing on gameplay, polish, and infrastructure improvements.
  • The pull request introduces a wide array of new features and refactorings across multiple domains, from 3D game components to application-level error handling and PWA capabilities.
  • New test files were added to cover the logic for fashion judging, player store operations, and adaptive quality detection, indicating a focus on code quality and reliability.
  • The extensive list of changes suggests a significant effort to enhance the user experience, game mechanics, and overall stability of the application.
  • There has been no human activity on this pull request yet, as it was just created.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces comprehensive updates to the racing game, including dynamic track elevation for AI and player trucks, new collision and banana slip mechanics with corresponding visual and sound effects, and an enhanced HUD with lap times, position changes, and a redesigned speed slider. It also adds a new procedural audio engine (RacingAudio class and useRacingAudio hook), implements new error boundary components for various routes, and sets up PWA features like a manifest, sitemap, robots.txt, offline page, and service worker. Review comments highlight the need to correct the service worker's manifest path, add more icon sizes to the PWA manifest for better user experience, and address the use of new Date() for lastModified in the sitemap to prevent unnecessary crawling. Additionally, a question was raised regarding the integration of the new RacingAudio class, as it appears to be unused in the current PR.

Comment thread public/sw.js
const PRECACHE_URLS = [
'/',
'/offline',
'/manifest.webmanifest',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The service worker is trying to pre-cache /manifest.webmanifest. However, Next.js's metadata files API typically generates this file as /manifest.json. Please verify the correct path for the generated manifest file and update it here to ensure it's cached correctly.

Suggested change
'/manifest.webmanifest',
'/manifest.json',

Comment thread app/manifest.ts
Comment on lines +15 to +21
icons: [
{
src: "/favicon.ico",
sizes: "48x48",
type: "image/x-icon",
},
],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The PWA manifest only defines a single 48x48 icon. For a better user experience across different devices and platforms, it's recommended to provide a range of icon sizes. Common sizes include 192x192 (for general use and splash screens) and 512x512 (for larger screens and app stores). You should also consider adding a maskable icon for better presentation on Android.

    icons: [
      {
        src: "/favicon.ico",
        sizes: "48x48",
        type: "image/x-icon",
      },
      {
        src: "/icon-192x192.png",
        sizes: "192x192",
        type: "image/png",
      },
      {
        src: "/icon-512x512.png",
        sizes: "512x512",
        type: "image/png",
      },
    ],

Comment thread app/sitemap.ts
Comment on lines +5 to +11
{ url: "/", lastModified: new Date(), changeFrequency: "weekly", priority: 1 },
{ url: "/race", lastModified: new Date(), changeFrequency: "monthly", priority: 0.8 },
{ url: "/stadium", lastModified: new Date(), changeFrequency: "monthly", priority: 0.8 },
{ url: "/fashion", lastModified: new Date(), changeFrequency: "monthly", priority: 0.8 },
{ url: "/learn", lastModified: new Date(), changeFrequency: "monthly", priority: 0.7 },
{ url: "/rewards", lastModified: new Date(), changeFrequency: "monthly", priority: 0.5 },
{ url: "/settings", lastModified: new Date(), changeFrequency: "monthly", priority: 0.3 },
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using new Date() for lastModified on every sitemap generation will signal to search engines that every page has been updated, even when they haven't. This can lead to unnecessary crawling and may not be an effective use of your crawl budget. It's better to use the actual last modification date of the content or a static date representing the last significant update. If you don't have a reliable way to get the actual modification date, it's often better to omit the lastModified field entirely.

Suggested change
{ url: "/", lastModified: new Date(), changeFrequency: "weekly", priority: 1 },
{ url: "/race", lastModified: new Date(), changeFrequency: "monthly", priority: 0.8 },
{ url: "/stadium", lastModified: new Date(), changeFrequency: "monthly", priority: 0.8 },
{ url: "/fashion", lastModified: new Date(), changeFrequency: "monthly", priority: 0.8 },
{ url: "/learn", lastModified: new Date(), changeFrequency: "monthly", priority: 0.7 },
{ url: "/rewards", lastModified: new Date(), changeFrequency: "monthly", priority: 0.5 },
{ url: "/settings", lastModified: new Date(), changeFrequency: "monthly", priority: 0.3 },
{ url: "/", changeFrequency: "weekly", priority: 1 },
{ url: "/race", changeFrequency: "monthly", priority: 0.8 },
{ url: "/stadium", changeFrequency: "monthly", priority: 0.8 },
{ url: "/fashion", changeFrequency: "monthly", priority: 0.8 },
{ url: "/learn", changeFrequency: "monthly", priority: 0.7 },
{ url: "/rewards", changeFrequency: "monthly", priority: 0.5 },
{ url: "/settings", changeFrequency: "monthly", priority: 0.3 },

Comment thread lib/audio/racing-audio.ts
Comment on lines +1 to +298
/**
* Procedural audio engine for the racing game using Web Audio API.
* Generates all sounds synthetically — no audio files required.
*/

export class RacingAudio {
private ctx: AudioContext | null = null;
private masterGain: GainNode | null = null;

// Engine sound nodes
private engineOsc: OscillatorNode | null = null;
private engineGain: GainNode | null = null;
private engineRunning = false;

// Settings
private sfxEnabled = true;

/** Lazily create AudioContext (must happen after user gesture) */
private ensureContext(): AudioContext {
if (!this.ctx) {
this.ctx = new AudioContext();
this.masterGain = this.ctx.createGain();
this.masterGain.gain.value = 0.3; // keep overall volume kid-friendly
this.masterGain.connect(this.ctx.destination);
}
if (this.ctx.state === 'suspended') {
this.ctx.resume().catch(() => {});
}
return this.ctx;
}

private get master(): GainNode {
this.ensureContext();
return this.masterGain!;
}

// ── Settings ────────────────────────────────────────────

setSfxEnabled(enabled: boolean): void {
this.sfxEnabled = enabled;
if (!enabled) this.stopEngine();
}

// ── Engine sound ────────────────────────────────────────

startEngine(): void {
if (this.engineRunning || !this.sfxEnabled) return;
const ctx = this.ensureContext();

this.engineGain = ctx.createGain();
this.engineGain.gain.value = 0.15;
this.engineGain.connect(this.master);

this.engineOsc = ctx.createOscillator();
this.engineOsc.type = 'sawtooth';
this.engineOsc.frequency.value = 60;
this.engineOsc.connect(this.engineGain);
this.engineOsc.start();

this.engineRunning = true;
}

/** Update engine pitch/volume based on speed (0–1 normalized) */
updateEngine(speedNorm: number): void {
if (!this.engineRunning || !this.engineOsc || !this.engineGain) return;
// Idle ~60Hz, full speed ~220Hz
this.engineOsc.frequency.value = 60 + speedNorm * 160;
// Louder when moving
this.engineGain.gain.value = 0.08 + speedNorm * 0.14;
}

stopEngine(): void {
if (!this.engineRunning) return;
try {
this.engineOsc?.stop();
} catch { /* already stopped */ }
this.engineOsc?.disconnect();
this.engineGain?.disconnect();
this.engineOsc = null;
this.engineGain = null;
this.engineRunning = false;
}

// ── One-shot SFX ────────────────────────────────────────

/** Short ascending chirp for coin collection */
playCoinCollect(): void {
if (!this.sfxEnabled) return;
const ctx = this.ensureContext();
const now = ctx.currentTime;

const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(880, now);
osc.frequency.linearRampToValueAtTime(1320, now + 0.08);
gain.gain.setValueAtTime(0.25, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.15);

osc.connect(gain).connect(this.master);
osc.start(now);
osc.stop(now + 0.15);
}

/** Two-note jingle for item box pickup */
playItemCollect(): void {
if (!this.sfxEnabled) return;
const ctx = this.ensureContext();
const now = ctx.currentTime;

// Note 1
const osc1 = ctx.createOscillator();
const g1 = ctx.createGain();
osc1.type = 'square';
osc1.frequency.value = 523; // C5
g1.gain.setValueAtTime(0.2, now);
g1.gain.exponentialRampToValueAtTime(0.001, now + 0.12);
osc1.connect(g1).connect(this.master);
osc1.start(now);
osc1.stop(now + 0.12);

// Note 2 (higher)
const osc2 = ctx.createOscillator();
const g2 = ctx.createGain();
osc2.type = 'square';
osc2.frequency.value = 784; // G5
g2.gain.setValueAtTime(0.2, now + 0.1);
g2.gain.exponentialRampToValueAtTime(0.001, now + 0.25);
osc2.connect(g2).connect(this.master);
osc2.start(now + 0.1);
osc2.stop(now + 0.25);
}

/** Swoosh/sweep for boost activation */
playBoost(): void {
if (!this.sfxEnabled) return;
const ctx = this.ensureContext();
const now = ctx.currentTime;

const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(200, now);
osc.frequency.exponentialRampToValueAtTime(800, now + 0.15);
osc.frequency.exponentialRampToValueAtTime(400, now + 0.4);
gain.gain.setValueAtTime(0.18, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.4);

osc.connect(gain).connect(this.master);
osc.start(now);
osc.stop(now + 0.4);
}

/** Countdown beep (high pitch for GO) */
playCountdownBeep(isGo: boolean): void {
if (!this.sfxEnabled) return;
const ctx = this.ensureContext();
const now = ctx.currentTime;

const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sine';
osc.frequency.value = isGo ? 880 : 440;
const duration = isGo ? 0.3 : 0.15;
gain.gain.setValueAtTime(0.3, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + duration);

osc.connect(gain).connect(this.master);
osc.start(now);
osc.stop(now + duration);
}

/** Ascending scale for lap completion */
playLapComplete(): void {
if (!this.sfxEnabled) return;
const ctx = this.ensureContext();
const now = ctx.currentTime;

const notes = [523, 659, 784, 1047]; // C5, E5, G5, C6
notes.forEach((freq, i) => {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sine';
osc.frequency.value = freq;
const t = now + i * 0.1;
gain.gain.setValueAtTime(0.2, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.2);
osc.connect(gain).connect(this.master);
osc.start(t);
osc.stop(t + 0.2);
});
}

/** Victory fanfare for race finish */
playRaceFinish(): void {
if (!this.sfxEnabled) return;
const ctx = this.ensureContext();
const now = ctx.currentTime;

// Triumphant ascending arpeggio
const notes = [523, 659, 784, 1047, 1319, 1568]; // C5 E5 G5 C6 E6 G6
notes.forEach((freq, i) => {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = i < 3 ? 'sine' : 'triangle';
osc.frequency.value = freq;
const t = now + i * 0.12;
gain.gain.setValueAtTime(0.22, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.4);
osc.connect(gain).connect(this.master);
osc.start(t);
osc.stop(t + 0.4);
});
}

/** Banana throw / hit sound */
playBananaHit(): void {
if (!this.sfxEnabled) return;
const ctx = this.ensureContext();
const now = ctx.currentTime;

// Descending slide (comic "slip")
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(600, now);
osc.frequency.exponentialRampToValueAtTime(150, now + 0.3);
gain.gain.setValueAtTime(0.2, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.35);

osc.connect(gain).connect(this.master);
osc.start(now);
osc.stop(now + 0.35);
}

/** Rising shimmer for shield activation */
playShield(): void {
if (!this.sfxEnabled) return;
const ctx = this.ensureContext();
const now = ctx.currentTime;

// Shimmering dual-tone
[523, 784].forEach((freq) => {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(freq, now);
osc.frequency.linearRampToValueAtTime(freq * 1.5, now + 0.3);
gain.gain.setValueAtTime(0.18, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.4);
osc.connect(gain).connect(this.master);
osc.start(now);
osc.stop(now + 0.4);
});
}

/** Crackling zap for lightning strike */
playLightning(): void {
if (!this.sfxEnabled) return;
const ctx = this.ensureContext();
const now = ctx.currentTime;

// High-pitched zap descending rapidly
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'square';
osc.frequency.setValueAtTime(2000, now);
osc.frequency.exponentialRampToValueAtTime(100, now + 0.25);
gain.gain.setValueAtTime(0.15, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.3);
osc.connect(gain).connect(this.master);
osc.start(now);
osc.stop(now + 0.3);

// Second crackle
const osc2 = ctx.createOscillator();
const g2 = ctx.createGain();
osc2.type = 'sawtooth';
osc2.frequency.setValueAtTime(1500, now + 0.1);
osc2.frequency.exponentialRampToValueAtTime(80, now + 0.35);
g2.gain.setValueAtTime(0.12, now + 0.1);
g2.gain.exponentialRampToValueAtTime(0.001, now + 0.4);
osc2.connect(g2).connect(this.master);
osc2.start(now + 0.1);
osc2.stop(now + 0.4);
}

// ── Lifecycle ───────────────────────────────────────────

destroy(): void {
this.stopEngine();
if (this.ctx && this.ctx.state !== 'closed') {
this.ctx.close().catch(() => {});
}
this.ctx = null;
this.masterGain = null;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This new RacingAudio class and the corresponding useRacingAudio hook seem to be a more robust, class-based implementation of the procedural audio logic found in the RaceSFX component. However, they don't appear to be used anywhere in this pull request; RaceGame3D still uses the RaceSFX component. While this is a good refactoring, including unused code can be confusing. Was the intention to replace RaceSFX with this new audio engine in this PR? If not, it might be better to introduce this in a separate PR where it's actually integrated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant