Skip to content
Merged
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "route-engine-js",
"version": "1.43.0",
"version": "1.45.0",
"description": "A lightweight Visual Novel engine built in JavaScript for creating interactive narrative games with branching storylines",
"repository": {
"type": "git",
Expand Down
49 changes: 49 additions & 0 deletions spec/RouteEngine.audioEffects.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -593,4 +593,53 @@ describe("RouteEngine audioEffects occurrences", () => {
'story.scenes["actual-scene"].sections["section"].lines["old"].actions.bgm.audioEffects.resourceId',
);
});

it("compiles beginEffect and endEffect onto the rendered sound", () => {
const projectData = createProjectData();
projectData.story.scenes.scene.sections.section.lines = [
{
id: "sound-effects",
actions: {
bgm: {
loop: true,
sounds: [
{
id: "lead:%main",
resourceId: "old",
volume: 80,
beginEffect: {
resourceId: "smooth",
playback: { speed: 2 },
},
endEffect: { resourceId: "smooth" },
},
],
},
},
},
];
const engine = createEngine({ projectData });

expect(engine.selectRenderState().audioEffects).toBeUndefined();
expect(engine.selectRenderState().audio[0].children[0]).toMatchObject({
id: "bgm:lead%3A%25main",
loop: false,
beginEffect: {
volume: {
keyframes: [
expect.objectContaining({ value: 50, duration: 200 }),
expect.objectContaining({ value: 30, duration: 300 }),
],
},
},
endEffect: {
volume: {
keyframes: [
expect.objectContaining({ value: 50, duration: 400 }),
expect.objectContaining({ value: 30, duration: 600 }),
],
},
},
});
});
});
23 changes: 23 additions & 0 deletions spec/projectDataSchema.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1945,6 +1945,29 @@ describe("projectData schema", () => {
expect(validatePresentationActions.errors).toBeNull();
});

it("accepts begin and end update-effect selections on channel sounds", () => {
const effectSound = {
id: "theme",
resourceId: "music_1",
beginEffect: { resourceId: "fade-in", playback: { speed: 1.5 } },
endEffect: { resourceId: "fade-out" },
};

for (const actions of [
{ bgm: { sounds: [effectSound] } },
{ sfx: { items: [effectSound] } },
{
sfx: {
channels: [{ id: "music", sounds: [effectSound] }],
},
},
{ voice: { sounds: [effectSound] } },
]) {
expect(validatePresentationActions(actions)).toBe(true);
expect(validatePresentationActions.errors).toBeNull();
}
});

it("rejects an invalid SFX channel apply mode", () => {
expect(
validatePresentationActions({
Expand Down
125 changes: 125 additions & 0 deletions spec/resolveAudioEffects.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { describe, expect, it } from "vitest";
import {
applyAudioEffectUpdateEndpoints,
resolveAudioEffect,
resolveAudioEffects,
resolveSoundBoundaryEffect,
} from "../src/resolveAudioEffects.js";

const oldChannel = {
Expand Down Expand Up @@ -673,4 +675,127 @@ describe("resolveAudioEffect", () => {
nextChannel,
}).toEqual(snapshots);
});

it("keeps per-sound boundary presets out of top-level audioEffects", () => {
expect(
resolveAudioEffects({
occurrence: { ...occurrence, selection: null },
resources: { audioEffects: { smooth: updateResource } },
previousChannel: null,
nextChannel: newChannel,
}),
).toEqual([]);
});
});

describe("resolveSoundBoundaryEffect", () => {
it("compiles update properties and playback speed", () => {
expect(
resolveSoundBoundaryEffect({
selection: { resourceId: "smooth", playback: { speed: 2 } },
selectionPath: 'bgm.sounds["main"].beginEffect',
resources: {
audioEffects: {
smooth: {
type: "update",
tween: {
volume: {
keyframes: [
{ startValue: 0, value: 50, duration: 200 },
{ value: 100, delay: 40, duration: 400 },
],
},
pan: {
keyframes: [{ value: 0.5, duration: 100 }],
},
playbackRate: {
keyframes: [{ value: 1.25, duration: 300 }],
},
},
},
},
},
}),
).toEqual({
volume: {
keyframes: [
{
startValue: 0,
value: 50,
delay: 0,
duration: 100,
easing: "linear",
},
{
value: 100,
delay: 20,
duration: 200,
easing: "linear",
},
],
},
pan: {
keyframes: [{ value: 0.5, delay: 0, duration: 50, easing: "linear" }],
},
playbackRate: {
keyframes: [{ value: 1.25, delay: 0, duration: 150, easing: "linear" }],
},
});
});

it("returns undefined without a selection", () => {
expect(
resolveSoundBoundaryEffect({
selection: undefined,
selectionPath: 'bgm.sounds["main"].endEffect',
resources: {},
}),
).toBeUndefined();
});

it("rejects missing, transition, invalid endpoint, and invalid speed inputs", () => {
const selectionPath = 'bgm.sounds["main"].beginEffect';
expect(() =>
resolveSoundBoundaryEffect({
selection: { resourceId: "missing" },
selectionPath,
resources: {},
}),
).toThrow('Unknown audio effect resource "missing"');

expect(() =>
resolveSoundBoundaryEffect({
selection: { resourceId: "crossfade" },
selectionPath,
resources: { audioEffects: { crossfade: transitionResource } },
}),
).toThrow('require an audio effect resource with type "update"');

expect(() =>
resolveSoundBoundaryEffect({
selection: { resourceId: "bad" },
selectionPath,
resources: {
audioEffects: {
bad: {
type: "update",
tween: {
volume: {
keyframes: [{ value: 10, duration: 100, relative: true }],
},
},
},
},
},
}),
).toThrow("final keyframe must use an absolute finite numeric value");

expect(() =>
resolveSoundBoundaryEffect({
selection: { resourceId: "smooth", playback: { speed: 0 } },
selectionPath,
resources: { audioEffects: { smooth: updateResource } },
}),
).toThrow("must be a finite number greater than 0");
});
});
Loading
Loading