diff --git a/client/public/version.js b/client/public/version.js index ec8a531..4a848ff 100644 --- a/client/public/version.js +++ b/client/public/version.js @@ -1,5 +1,5 @@ // Maintainer-controlled web client version. // Format: YYYY.MM.DD Rn (example: 2026.02.20 R2) -window.CHGRID_WEB_VERSION = "2026.02.25 R229"; +window.CHGRID_WEB_VERSION = "2026.02.25 R230"; // Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid. window.CHGRID_TIME_ZONE = "America/Detroit"; diff --git a/client/src/audio/pianoSynth.ts b/client/src/audio/pianoSynth.ts index 9ded5d3..b45c300 100644 --- a/client/src/audio/pianoSynth.ts +++ b/client/src/audio/pianoSynth.ts @@ -15,6 +15,22 @@ export const PIANO_INSTRUMENT_OPTIONS = [ export type PianoInstrumentId = (typeof PIANO_INSTRUMENT_OPTIONS)[number]; +/** Returns whether a runtime string is one of the supported piano instrument ids. */ +export function isPianoInstrumentId(value: string): value is PianoInstrumentId { + return ( + value === 'piano' || + value === 'electric_piano' || + value === 'guitar' || + value === 'organ' || + value === 'bass' || + value === 'violin' || + value === 'synth_lead' || + value === 'brass' || + value === 'nintendo' || + value === 'drum_kit' + ); +} + type VoiceRuntime = { gain: GainNode; panner: StereoPannerNode | null; diff --git a/client/src/items/types/piano/runtime.ts b/client/src/items/types/piano/runtime.ts index 2720586..deafb62 100644 --- a/client/src/items/types/piano/runtime.ts +++ b/client/src/items/types/piano/runtime.ts @@ -1,11 +1,12 @@ import { DEFAULT_PIANO_SETTINGS_BY_INSTRUMENT, - PIANO_INSTRUMENT_OPTIONS, PianoSynth, + isPianoInstrumentId, type PianoInstrumentId, } from '../../../audio/pianoSynth'; import { type IncomingMessage, type OutgoingMessage } from '../../../network/protocol'; import { type GameMode, type WorldItem } from '../../../state/gameState'; +import { getItemPropertyOptionValues } from '../../itemRegistry'; const PIANO_WHITE_KEY_MIDI_BY_CODE: Record = { KeyA: 60, @@ -321,8 +322,9 @@ export class PianoController { if (code.startsWith('Digit')) { const digit = Number(code.slice(5)); const instrumentIndex = digit === 0 ? 9 : digit - 1; - if (Number.isInteger(instrumentIndex) && instrumentIndex >= 0 && instrumentIndex < PIANO_INSTRUMENT_OPTIONS.length) { - const instrument = PIANO_INSTRUMENT_OPTIONS[instrumentIndex]; + const shortcutInstruments = this.getShortcutInstruments(); + if (Number.isInteger(instrumentIndex) && instrumentIndex >= 0 && instrumentIndex < shortcutInstruments.length) { + const instrument = shortcutInstruments[instrumentIndex]; if (instrument) { const defaults = DEFAULT_PIANO_SETTINGS_BY_INSTRUMENT[instrument]; const voiceMode = this.defaultsVoiceModeForInstrument(instrument); @@ -646,18 +648,24 @@ export class PianoController { private normalizePianoInstrument(value: unknown): PianoInstrumentId { const raw = String(value ?? 'piano').trim().toLowerCase(); - if (raw === 'electric_piano') return 'electric_piano'; - if (raw === 'guitar') return 'guitar'; - if (raw === 'organ') return 'organ'; - if (raw === 'bass') return 'bass'; - if (raw === 'violin') return 'violin'; - if (raw === 'synth_lead') return 'synth_lead'; - if (raw === 'brass') return 'brass'; - if (raw === 'nintendo') return 'nintendo'; - if (raw === 'drum_kit') return 'drum_kit'; + if (isPianoInstrumentId(raw)) return raw; return 'piano'; } + private getShortcutInstruments(): PianoInstrumentId[] { + const options = getItemPropertyOptionValues('piano', 'instrument') ?? []; + const normalized: PianoInstrumentId[] = []; + const seen = new Set(); + for (const option of options) { + const raw = option.trim().toLowerCase(); + if (!isPianoInstrumentId(raw) || seen.has(raw)) continue; + seen.add(raw); + normalized.push(raw); + if (normalized.length >= 10) break; + } + return normalized; + } + private getPianoMidiForCode(code: string): number | null { if (code in PIANO_WHITE_KEY_MIDI_BY_CODE) { return PIANO_WHITE_KEY_MIDI_BY_CODE[code]!;