isle.pizza/src/core/audio.js
Christian Semmler 1d18779689
Plant editor (#25)
* Plant editor

Add Plants tab to the save editor for browsing and editing all 81 plants.
Click-to-customize based on selected character matches the original game
behavior (Pepper→variant, Mama→sound, Papa→move, Nick→color, Laura→mood).
Includes 3D preview with per-variant display tuning, click animations,
sound playback, and reset to defaults.

* Refactor shared animation code into AnimatedRenderer base class

Extract duplicated animation infrastructure (clock, mixer, animation
caching, raycaster, keyframe interpolation) from ActorRenderer and
PlantRenderer into a new AnimatedRenderer intermediate class. Extract
identical sound player code from both editors into createSoundPlayer()
utility. Fix PlantRenderer interpolateVertex bug where scale keys had
X incorrectly negated. Remove dead PLANT_ANIM_IDS export and redundant
textures.clear() calls.

* Extract shared editor CSS and fix vehicle nav spacing

Move duplicated preview, spinner, navigation, and side-button styles
from VehicleEditor, ActorEditor, and PlantEditor into a shared
editor-common.css. Standardize class names (nav-index, nav-name,
side-btn) and fix VehicleEditor part-info min-width (100px → 150px)
to match the other editors.

* Add carousel tabs and selection-based nav to save editor

Wrap save editor tab buttons in a Carousel to prevent overflow on
desktop. Carousel nav buttons now cycle through the selected item
(save slot or tab) instead of scrolling, with auto-scroll-into-view.
On mobile, tabs reflow with flex-wrap as before.

* Update February changelog with plant editor and carousel navigation
2026-02-14 18:22:28 +01:00

92 lines
2.4 KiB
JavaScript

// Audio utilities
import { soundEnabled } from '../stores.js';
import { fetchSoundAsWav } from './assetLoader.js';
export function getInstallAudio() {
return document.getElementById('install-audio');
}
export function pauseInstallAudio() {
const audio = getInstallAudio();
if (audio) {
audio.pause();
soundEnabled.set(false);
}
}
export function playInstallAudio() {
const audio = getInstallAudio();
if (audio) {
audio.currentTime = 0;
audio.load();
audio.play()
.then(() => {
soundEnabled.set(true);
})
.catch(() => {
soundEnabled.set(false);
});
}
}
export function toggleInstallAudio() {
const audio = getInstallAudio();
if (!audio) return;
if (audio.paused) {
playInstallAudio();
} else {
pauseInstallAudio();
}
}
/**
* Create a reusable sound player for game asset sounds.
* Uses Web Audio API with caching and configurable volume.
* @param {number} volume - Gain value (0-1), default 0.3
* @returns {{ play: (name: string) => Promise<void>, dispose: () => void }}
*/
export function createSoundPlayer(volume = 0.3) {
let audioContext = null;
let gainNode = null;
const cache = new Map();
async function play(name) {
try {
if (!audioContext) {
audioContext = new AudioContext();
gainNode = audioContext.createGain();
gainNode.gain.value = volume;
gainNode.connect(audioContext.destination);
}
if (audioContext.state === 'suspended') {
await audioContext.resume();
}
let audioBuffer = cache.get(name);
if (!audioBuffer) {
const wav = await fetchSoundAsWav(name);
if (!wav) return;
audioBuffer = await audioContext.decodeAudioData(wav);
cache.set(name, audioBuffer);
}
const source = audioContext.createBufferSource();
source.buffer = audioBuffer;
source.connect(gainNode);
source.start();
} catch (e) {
console.error(`Failed to play sound ${name}:`, e);
}
}
function dispose() {
audioContext?.close();
audioContext = null;
gainNode = null;
cache.clear();
}
return { play, dispose };
}