Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@ public/sfx*
public/mfx*
public/maps
public/data
public/jbox/*
!public/jbox/.gitkeep
public/soundfonts
dist
.DS_Store
10 changes: 9 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="mobile-web-app-capable" content="yes">
<title>EO Web Client</title>
<link rel="preload" href="/soundfonts/GeneralUser-GS.sf2" as="fetch" crossorigin>
<link rel="preload" href="/spessasynth_processor.min.js" as="script">
</head>

<body>
Expand Down Expand Up @@ -347,6 +349,12 @@
<button class="img-btn" type="button" data-id="ok"></button>
</div>

<div id="jukebox-dialog" class="hidden">
<span class="title"></span>
<div class="item-list"></div>
<button class="img-btn" type="button" data-id="cancel"></button>
</div>

<div id="locker" class="hidden">
<span class="title"></span>
<div class="locker-items"></div>
Expand Down Expand Up @@ -593,4 +601,4 @@
<script type="module" src="/src/main.ts"></script>
</body>

</html>
</html>
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"idb": "^8.0.3",
"mitt": "^3.0.1",
"notyf": "^3.10.0",
"pixi.js": "^8.17.1"
"pixi.js": "^8.17.1",
"spessasynth_lib": "^4.2.9"
},
"packageManager": "pnpm@10.33.0"
}
15 changes: 15 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Empty file added public/jbox/.gitkeep
Empty file.
4 changes: 4 additions & 0 deletions src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
GuildController,
InventoryController,
ItemProtectionController,
JukeboxController,
KeyboardController,
LockerController,
MapController,
Expand Down Expand Up @@ -170,6 +171,7 @@ export class Client {
commandController: CommandController;
inventoryController: InventoryController;
lockerController: LockerController;
jukeboxController: JukeboxController;
mapController: MapController;
mouseController: MouseController;
npcController: NpcController;
Expand Down Expand Up @@ -272,6 +274,7 @@ export class Client {
this.drunkController = new DrunkController(this);
this.inventoryController = new InventoryController(this);
this.itemProtectionController = new ItemProtectionController();
this.jukeboxController = new JukeboxController(this);
this.lockerController = new LockerController(this);
this.mapController = new MapController(this);
this.mouseController = new MouseController(this);
Expand Down Expand Up @@ -580,6 +583,7 @@ export class Client {
playSfxById(SfxId.EnterPkMap);
}

this.audioController.stopJukeboxTrack();
this.audioController.stopAmbientSound();

if (this.map.ambientSoundId) {
Expand Down
2 changes: 2 additions & 0 deletions src/client/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ export type ClientEvents = {
details: CharacterDetails;
questNames: string[];
};
jukeboxOpened: { requestedBy: string | null };
jukeboxUpdated: { requestedBy: string | null };
chestOpened: {
items: ThreeItem[];
};
Expand Down
121 changes: 115 additions & 6 deletions src/controllers/audio-controller.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,74 @@
import { MapTileSpec } from 'eolib';
import { Sequencer, WorkletSynthesizer } from 'spessasynth_lib';
import WORKLET_URL from 'spessasynth_lib/dist/spessasynth_processor.min.js?url';
import type { Client } from '@/client';
import { getCachedAsset, saveCachedAsset } from '@/db';
import { getDistance, getVolumeFromDistance, padWithZeros } from '@/utils';
import type { Vector2 } from '@/vector';

const SOUNDFONT_URL = '/soundfonts/GeneralUser-GS.sf2';
const SOUNDFONT_CACHE_KEY = 'sf2:GeneralUser-GS';

export class AudioController {
private client: Client;
ambientSound: AudioBufferSourceNode | null = null;
ambientVolume: GainNode | null = null;
private ambientContext: AudioContext | null = null;
private sequencer: Sequencer | null = null;
private jukeboxLoadId = 0;
private synthReady: Promise<WorkletSynthesizer> | null = null;
private sfBuffer: Promise<ArrayBuffer>;

constructor(client: Client) {
this.client = client;
this.sfBuffer = this.loadSf2();

const unlock = () => {
void this.getSynth();
};
window.addEventListener('pointerdown', unlock, { once: true });
window.addEventListener('keydown', unlock, { once: true });
}

private async loadSf2(): Promise<ArrayBuffer> {
const cached = await getCachedAsset(SOUNDFONT_CACHE_KEY);
if (cached) return cached;

const resp = await fetch(SOUNDFONT_URL);
if (!resp.ok) {
throw new Error(`Failed to load soundfont: HTTP ${resp.status}`);
}
const buffer = await resp.arrayBuffer();
saveCachedAsset(SOUNDFONT_CACHE_KEY, buffer);
return buffer;
}

private async getSynth(): Promise<WorkletSynthesizer> {
if (!this.synthReady) {
this.synthReady = (async () => {
const ctx = new AudioContext();
const [sfBuffer] = await Promise.all([
this.sfBuffer,
ctx.audioWorklet.addModule(WORKLET_URL),
]);
const synth = new WorkletSynthesizer(ctx);
await synth.soundBankManager.addSoundBank(sfBuffer, 'main');
await synth.isReady;
synth.connect(ctx.destination);
return synth;
})().catch((error) => {
this.synthReady = null;
throw error;
});
}
return this.synthReady;
}

private stopCurrentSequencer(): void {
if (this.sequencer) {
this.sequencer.pause();
this.sequencer = null;
}
}

setAmbientVolume(): void {
Expand All @@ -19,7 +78,7 @@ export class AudioController {

const playerAt = this.client.getPlayerCoords();
const sources: { coords: Vector2; distance: number }[] = [];
for (const row of this.client.map!.tileSpecRows) {
for (const row of this.client.map.tileSpecRows) {
for (const tile of row.tiles) {
if (tile.tileSpec === MapTileSpec.AmbientSource) {
const coords = { x: tile.x, y: row.y };
Expand All @@ -31,9 +90,9 @@ export class AudioController {

sources.sort((a, b) => a.distance - b.distance);
if (sources.length) {
const distance = sources[0].distance;
const volume = getVolumeFromDistance(distance);
this.ambientVolume!.gain.value = volume;
this.ambientVolume!.gain.value = getVolumeFromDistance(
sources[0]!.distance,
);
}
}

Expand All @@ -44,15 +103,18 @@ export class AudioController {
this.ambientVolume.disconnect();
this.ambientVolume = null;
}
void this.ambientContext?.close();
this.ambientContext = null;
}

startAmbientSound(): void {
if (!this.client.map!.ambientSoundId) {
if (!this.client.map?.ambientSoundId) {
return;
}

const context = new AudioContext();
fetch(`/sfx/sfx${padWithZeros(this.client.map!.ambientSoundId, 3)}.wav`)
this.ambientContext = context;
fetch(`/sfx/sfx${padWithZeros(this.client.map.ambientSoundId, 3)}.wav`)
.then((response) => response.arrayBuffer())
.then((data) => context.decodeAudioData(data))
.then((buffer) => {
Expand All @@ -66,4 +128,51 @@ export class AudioController {
this.ambientSound.start(0);
});
}

stopJukeboxTrack(): void {
this.jukeboxLoadId += 1;
this.stopCurrentSequencer();
}

async playJukeboxTrack(trackId: number): Promise<void> {
if (!trackId) {
return;
}

const loadId = ++this.jukeboxLoadId;

try {
this.stopCurrentSequencer();

const [synth, midiResp] = await Promise.all([
this.getSynth(),
fetch(`/jbox/jbox${padWithZeros(trackId, 3)}.mid`),
]);

if (this.jukeboxLoadId !== loadId) {
return;
}

if (!midiResp.ok) {
throw new Error(
`Failed to load jukebox track: HTTP ${midiResp.status}`,
);
}

const midiBuffer = await midiResp.arrayBuffer();

if (this.jukeboxLoadId !== loadId) {
return;
}

synth.stopAll(true);
const seq = new Sequencer(synth);
seq.loadNewSongList([{ binary: midiBuffer }]);
seq.loopCount = -1;
this.sequencer = seq;
seq.play();
} catch (error) {
console.error(`Failed to play jukebox track ${trackId}`, error);
}
}
}
1 change: 1 addition & 0 deletions src/controllers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export { DrunkController } from './drunk-controller';
export { GuildController } from './guild-controller';
export { InventoryController } from './inventory-controller';
export { ItemProtectionController } from './item-protection-controller';
export { JukeboxController } from './jukebox-controller';
export { KeyboardController } from './keyboard-controller';
export { LockerController } from './locker-controller';
export { MapController } from './map-controller';
Expand Down
25 changes: 25 additions & 0 deletions src/controllers/jukebox-controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Coords, JukeboxMsgClientPacket, JukeboxOpenClientPacket } from 'eolib';
import type { Client } from '@/client';
import type { Vector2 } from '@/vector';

export class JukeboxController {
private client: Client;

constructor(client: Client) {
this.client = client;
}

open(coords: Vector2): void {
const packet = new JukeboxOpenClientPacket();
packet.coords = new Coords();
packet.coords.x = coords.x;
packet.coords.y = coords.y;
this.client.bus!.send(packet);
}

requestSong(trackId: number): void {
const packet = new JukeboxMsgClientPacket();
packet.trackId = trackId;
this.client.bus!.send(packet);
}
}
5 changes: 5 additions & 0 deletions src/controllers/mouse-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,11 @@ export class MouseController {
return;
}

if (tileSpec === MapTileSpec.Jukebox) {
this.client.jukeboxController.open(this.client.mouseCoords);
return;
}

if (
this.client.mapController.isFacingChairAt(this.client.mouseCoords) &&
!this.client.mapController.occupied(this.client.mouseCoords)
Expand Down
Loading
Loading