Sheets via neuer KeyboardAwareSheet-Composable (in Modal pattern, auto-grow mit Tastatur, paddingBottom-Lift): EditMail, AddDomain, CreateRoom, ConnectMail. GameOverScreen behält Spring-Slide-In, nutzt RN Keyboard.addListener für Lift. - KeyboardAwareSheet.tsx — universal modal with sheet-grow + keyboard-padding - react-native-keyboard-controller installiert + KeyboardProvider in Root - Snake: time + ScoreProgressBar + useSnakeSounds (haptic, audio TODO) - Tetris: title weg, Buttons zentriert, kein Pressable mit style-fn - DPad-Buttons 60→48, more bg, no scale - useMe: pub-sub listener pattern für app-weite avatar/nickname-Updates - dm.tsx: resolveAvatar wrap (iron.png-Warning) - Mail-error-humanizer + locales Recovery-Doc-Update in docs/internal/RECOVERY_LOG_2026-05-10.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
79 lines
3.0 KiB
TypeScript
79 lines
3.0 KiB
TypeScript
import { useEffect, useRef } from 'react';
|
|
import * as Haptics from 'expo-haptics';
|
|
|
|
/**
|
|
* Snake-Sound + Haptic-Helper.
|
|
*
|
|
* Aktuell: nur Haptics (Apple Taptic-Engine, Android-Vibrator-Falls-Available).
|
|
* Funktioniert SOFORT ohne weitere Setup-Schritte.
|
|
*
|
|
* UPGRADE-PFAD zu echtem 8-Bit-Retro-Sound:
|
|
*
|
|
* 1. 4 kurze Audio-Files in `apps/rebreak-native/assets/sounds/` ablegen:
|
|
* - `snake-eat.mp3` — Apple-Pickup, ~80ms, tonale "blip"-Töne (chiptune)
|
|
* - `snake-move.mp3` — Optional, Tick-Sound bei jeder Bewegung, ~30ms, dezent
|
|
* - `snake-gameover.mp3` — Death, ~400ms, abfallende Töne
|
|
* - `snake-record.mp3` — New-Record, ~600ms, aufsteigender Chime
|
|
*
|
|
* Free-Quellen (CC0): freesound.org, opengameart.org/content/8-bit-sound-pack,
|
|
* sfxr.me (in-browser-Generator für klassische 8-Bit-Sounds).
|
|
*
|
|
* 2. `expo-av` (oder `expo-audio` nach SDK-54-Migration) installieren falls nicht da:
|
|
* `pnpm add expo-av` (im rebreak-native-Workspace)
|
|
*
|
|
* 3. In dieser Datei oben einfügen:
|
|
* ```ts
|
|
* import { Audio } from 'expo-av';
|
|
* const eatSrc = require('../assets/sounds/snake-eat.mp3');
|
|
* const moveSrc = require('../assets/sounds/snake-move.mp3');
|
|
* const gameoverSrc = require('../assets/sounds/snake-gameover.mp3');
|
|
* const recordSrc = require('../assets/sounds/snake-record.mp3');
|
|
* ```
|
|
*
|
|
* 4. Im Hook-useEffect die Sounds preloaden:
|
|
* ```ts
|
|
* Audio.Sound.createAsync(eatSrc, { volume: 0.5 }).then((r) => (eatRef.current = r.sound));
|
|
* // … analog für alle drei
|
|
* ```
|
|
*
|
|
* 5. In den `play*`-Funktionen `await ref.current?.replayAsync()` aufrufen.
|
|
*
|
|
* Wenn die Files fehlen aber expo-av da ist: keine Crashes — die createAsync-Calls
|
|
* fangen den Error und der Hook läuft im Haptic-only-Mode weiter.
|
|
*/
|
|
export function useSnakeSounds(enabled: boolean = true) {
|
|
const enabledRef = useRef(enabled);
|
|
useEffect(() => {
|
|
enabledRef.current = enabled;
|
|
}, [enabled]);
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
// Cleanup: bei späterer Audio-Integration unloadAsync() für alle Sounds.
|
|
};
|
|
}, []);
|
|
|
|
return {
|
|
playEat: () => {
|
|
if (!enabledRef.current) return;
|
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {});
|
|
// TODO Audio: eatRef.current?.replayAsync().catch(() => {});
|
|
},
|
|
playMove: () => {
|
|
// Bewusst leer — sonst zu viel Vibration bei jedem Tick.
|
|
// Nur via Audio (subtiler als Haptic).
|
|
// TODO Audio: moveRef.current?.replayAsync().catch(() => {});
|
|
},
|
|
playGameOver: () => {
|
|
if (!enabledRef.current) return;
|
|
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {});
|
|
// TODO Audio: gameoverRef.current?.replayAsync().catch(() => {});
|
|
},
|
|
playNewRecord: () => {
|
|
if (!enabledRef.current) return;
|
|
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {});
|
|
// TODO Audio: recordRef.current?.replayAsync().catch(() => {});
|
|
},
|
|
};
|
|
}
|