From 90dfa195818ec542ea3006c9e7aa70cc54a9151b Mon Sep 17 00:00:00 2001 From: Fernando Gomes Date: Fri, 29 May 2026 16:48:38 -0300 Subject: [PATCH] feat: scream when hedgehog is harshly thrown When hedgehog mode is enabled and the user grabs the hog and flings it sharply upward, play the Wilhelm scream. Detection is purely renderer-side via the hedgehog-mode package's public API: on every pointerup we defer one animation frame so Matter.js has applied the post-release velocity, then inspect each hedgehog's rigidBody.velocity. If a hog is moving up faster than the autonomous jump speed (y < -25, speed > 25), we play the scream at the user's completionVolume. The wilhelm.mp3 asset already shipped as a selectable task-completion sound; this just wires it to throw releases as well via a new one-shot playSoundUrl helper that doesn't interfere with the completion-sound audio chain. Generated-By: PostHog Code Task-Id: a92f24e3-30a1-4e8d-a6e8-5fb508ef9993 --- .../src/renderer/components/HedgehogMode.tsx | 27 +++++++++++++++++++ apps/code/src/renderer/utils/sounds.ts | 10 +++++++ 2 files changed, 37 insertions(+) diff --git a/apps/code/src/renderer/components/HedgehogMode.tsx b/apps/code/src/renderer/components/HedgehogMode.tsx index cc3d795848..ebf0467869 100644 --- a/apps/code/src/renderer/components/HedgehogMode.tsx +++ b/apps/code/src/renderer/components/HedgehogMode.tsx @@ -5,10 +5,15 @@ import type { HedgeHogMode as HedgehogModeGame, } from "@posthog/hedgehog-mode"; import { logger } from "@utils/logger"; +import { playSoundUrl, WILHELM_SOUND_URL } from "@utils/sounds"; import { useEffect, useRef } from "react"; const log = logger.scope("hedgehog-mode"); +// Above the autonomous jump velocity of 15, so jumps never trigger. +const HARSH_THROW_Y_THRESHOLD = 25; +const HARSH_THROW_SPEED_THRESHOLD = 25; + export function HedgehogMode() { const hedgehogMode = useSettingsStore((s) => s.hedgehogMode); const setHedgehogMode = useSettingsStore((s) => s.setHedgehogMode); @@ -30,6 +35,27 @@ export function HedgehogMode() { | HedgehogActorOptions | undefined; + const onPointerUp = () => { + // Defer one frame so Matter.js applies the post-release velocity. + requestAnimationFrame(() => { + if (cancelled || !gameRef.current) return; + for (const hedgehog of gameRef.current.getAllHedgehogs()) { + const v = hedgehog.rigidBody?.velocity; + if (!v) continue; + const speed = Math.hypot(v.x, v.y); + if ( + v.y < -HARSH_THROW_Y_THRESHOLD && + speed > HARSH_THROW_SPEED_THRESHOLD + ) { + const volume = useSettingsStore.getState().completionVolume; + playSoundUrl(WILHELM_SOUND_URL, volume); + break; + } + } + }); + }; + window.addEventListener("pointerup", onPointerUp); + import("@posthog/hedgehog-mode") .then(async ({ HedgeHogMode }) => { if (cancelled) return; @@ -62,6 +88,7 @@ export function HedgehogMode() { return () => { cancelled = true; + window.removeEventListener("pointerup", onPointerUp); }; }, [hedgehogMode, user?.hedgehog_config, setHedgehogMode]); diff --git a/apps/code/src/renderer/utils/sounds.ts b/apps/code/src/renderer/utils/sounds.ts index b0abc7b0f3..ef9359196e 100644 --- a/apps/code/src/renderer/utils/sounds.ts +++ b/apps/code/src/renderer/utils/sounds.ts @@ -13,6 +13,16 @@ import slideUrl from "@renderer/assets/sounds/slide.mp3"; import switchUrl from "@renderer/assets/sounds/switch.mp3"; import wilhelmUrl from "@renderer/assets/sounds/wilhelm.mp3"; +export const WILHELM_SOUND_URL = wilhelmUrl; + +export function playSoundUrl(url: string, volume = 80): void { + const audio = new Audio(url); + audio.volume = Math.max(0, Math.min(100, volume)) / 100; + audio.play().catch(() => { + // Audio play can fail if user hasn't interacted with the page yet + }); +} + const SOUND_URLS: Record, string> = { guitar: guitarUrl, danilo: daniloUrl,