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
247 changes: 247 additions & 0 deletions spec/RouteEngine.rollbackRenderState.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
import { describe, expect, it, vi } from "vitest";
import createRouteEngine from "../src/RouteEngine.js";
import createEffectsHandler from "../src/createEffectsHandler.js";

const createTicker = () => ({
add: vi.fn(),
remove: vi.fn(),
});

const findElementById = (elements, id) => {
for (const element of elements || []) {
if (element?.id === id) {
return element;
}

const nested = findElementById(element?.children, id);
if (nested) {
return nested;
}
}

return null;
};

const createProjectData = () => ({
screen: {
width: 1920,
height: 1080,
backgroundColor: "#000000",
},
resources: {
layouts: {
revealDialogue: {
mode: "adv",
elements: [
{
id: "dialogue-text",
type: "text-revealing",
content: "${dialogue.content}",
revealEffect: "typewriter",
displaySpeed: 30,
textStyleId: "body",
},
],
},
layeredPanel: {
elements: [
{
id: "panel-text",
type: "text",
content: "Layered panel",
textStyleId: "body",
},
],
transitions: [
{
id: "panel-fade-in",
type: "update",
tween: {
alpha: {
initialValue: 0,
keyframes: [{ duration: 300, value: 1 }],
},
},
},
],
},
},
sounds: {},
images: {},
videos: {},
sprites: {},
characters: {},
variables: {},
transforms: {},
sectionTransitions: {},
animations: {},
fonts: {
bodyFont: {
fileId: "Arial",
},
},
colors: {
bodyColor: {
hex: "#FFFFFF",
},
},
textStyles: {
body: {
fontId: "bodyFont",
colorId: "bodyColor",
fontSize: 24,
fontWeight: "400",
fontStyle: "normal",
lineHeight: 1.2,
},
},
controls: {},
},
story: {
initialSceneId: "scene1",
scenes: {
scene1: {
initialSectionId: "section1",
sections: {
section1: {
lines: [
{
id: "line1",
actions: {
pushLayeredView: {
resourceId: "layeredPanel",
resourceType: "layout",
},
dialogue: {
mode: "adv",
ui: {
resourceId: "revealDialogue",
},
content: [
{
text: "Line 1 should stay fully settled after rollback.",
},
],
},
},
},
{
id: "line2",
actions: {
clearLayeredViews: {},
dialogue: {
mode: "adv",
ui: {
resourceId: "revealDialogue",
},
content: [
{
text: "Line 2 exists so line 1 can be restored by rollback.",
},
],
},
},
},
],
},
},
},
},
},
});

describe("RouteEngine rollback render state", () => {
it("restores rollbacked lines directly in their settled end state", () => {
const routeGraphics = {
render: vi.fn(),
};

let engine;
const effectsHandler = createEffectsHandler({
getEngine: () => engine,
routeGraphics,
ticker: createTicker(),
});

engine = createRouteEngine({
handlePendingEffects: effectsHandler,
});

engine.init({
initialState: {
projectData: createProjectData(),
},
});

engine.handleActions({
nextLine: {},
});
engine.handleActions({
nextLine: {},
});
engine.handleAction("rollbackByOffset", { offset: -1 });

const rollbackRender = routeGraphics.render.mock.calls.at(-1)?.[0];

expect(engine.selectSystemState().contexts.at(-1).pointers.read.lineId).toBe(
"line1",
);
expect(findElementById(rollbackRender.elements, "dialogue-text")).toMatchObject(
{
type: "text-revealing",
revealEffect: "none",
},
);
expect(findElementById(rollbackRender.elements, "panel-text")).toMatchObject(
{
type: "text",
content: "Layered panel",
},
);
expect(rollbackRender.animations).toEqual([]);
});

it("keeps layered view transitions when pushed after line completion", () => {
const routeGraphics = {
render: vi.fn(),
};

let engine;
const effectsHandler = createEffectsHandler({
getEngine: () => engine,
routeGraphics,
ticker: createTicker(),
});

engine = createRouteEngine({
handlePendingEffects: effectsHandler,
});

engine.init({
initialState: {
projectData: createProjectData(),
},
});

engine.handleAction("markLineCompleted", {});
engine.handleAction("clearLayeredViews", {});
engine.handleAction("pushLayeredView", {
resourceId: "layeredPanel",
resourceType: "layout",
});

const overlayRender = routeGraphics.render.mock.calls.at(-1)?.[0];

expect(engine.selectSystemState().global.isLineCompleted).toBe(true);
expect(findElementById(overlayRender.elements, "panel-text")).toMatchObject({
type: "text",
content: "Layered panel",
});
expect(overlayRender.animations).toEqual([
expect.objectContaining({
id: "panel-fade-in",
targetId: "layeredView-0",
}),
]);
});
});
70 changes: 70 additions & 0 deletions spec/system/renderState/addLayeredViews.spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,76 @@ out:
easing: "linear"
relative: true
---
case: keep layered view transitions when pushed after line completion
in:
- elements:
- id: "story"
type: "container"
x: 0
y: 0
children: []
animations: []
- resources:
layouts:
animatedLayout:
elements:
- type: "text"
content: "Animated"
transitions:
- id: "animated-layout-fade-in"
type: "update"
tween:
alpha:
initialValue: 0
keyframes:
- duration: 500
value: 1
variables: {}
autoMode: false
skipMode: false
isLineCompleted: true
skipTransitionsAndAnimations: false
screen:
width: 1920
height: 1080
layeredViews:
- resourceId: animatedLayout
resourceType: layout
out:
elements:
- id: "story"
type: "container"
x: 0
y: 0
children: []
- id: "layeredView-0"
type: "container"
x: 0
y: 0
children:
- id: "layeredView-0-blocker"
type: "rect"
fill: "transparent"
width: 1920
height: 1080
x: 0
y: 0
click:
payload:
actions: {}
- type: "text"
content: "Animated"
animations:
- id: "animated-layout-fade-in"
type: "update"
targetId: "layeredView-0"
tween:
alpha:
initialValue: 0
keyframes:
- duration: 500
value: 1
---
case: handle layeredView with nested children
in:
- elements:
Expand Down
14 changes: 8 additions & 6 deletions src/stores/constructRenderState.js
Original file line number Diff line number Diff line change
Expand Up @@ -2065,12 +2065,14 @@ export const addLayeredViews = (
}

if (Array.isArray(layout.transitions)) {
pushNormalizedLayoutTransitions({
animations,
transitions: layout.transitions,
defaultTargetId: `layeredView-${index}`,
idPrefix: `layeredView-${index}`,
});
if (!skipTransitionsAndAnimations) {
pushNormalizedLayoutTransitions({
animations,
transitions: layout.transitions,
defaultTargetId: `layeredView-${index}`,
idPrefix: `layeredView-${index}`,
});
}
}

// Create a container for this layeredView
Expand Down
23 changes: 22 additions & 1 deletion src/stores/system.store.js
Original file line number Diff line number Diff line change
Expand Up @@ -953,6 +953,23 @@ export const selectCurrentPageSlots = (
return { saveSlots: slots };
};

const shouldSettleCurrentLinePresentation = (state) => {
const lastContext = state.contexts?.[state.contexts.length - 1];
const rollback = lastContext?.rollback;
if (
!rollback ||
!Array.isArray(rollback.timeline) ||
typeof rollback.currentIndex !== "number"
) {
return false;
}

return (
rollback.currentIndex >= 0 &&
rollback.currentIndex < rollback.timeline.length - 1
);
};

export const selectRenderState = ({ state }) => {
const presentationState = selectPresentationState({ state });
const previousPresentationState = selectPreviousPresentationState({ state });
Expand All @@ -963,6 +980,8 @@ export const selectRenderState = ({ state }) => {
};

const { saveSlots } = selectCurrentPageSlots({ state });
const settleCurrentLinePresentation =
shouldSettleCurrentLinePresentation(state);

const renderState = constructRenderState({
presentationState,
Expand All @@ -975,7 +994,9 @@ export const selectRenderState = ({ state }) => {
canRollback: selectCanRollback({ state }),
skipOnlyViewedLines: !allVariables._skipUnseenText,
isLineCompleted: state.global.isLineCompleted,
skipTransitionsAndAnimations: !!allVariables._skipTransitionsAndAnimations,
skipTransitionsAndAnimations:
!!allVariables._skipTransitionsAndAnimations ||
settleCurrentLinePresentation,
layeredViews: state.global.layeredViews,
dialogueHistory: selectDialogueHistory({ state }),
saveSlots,
Expand Down
3 changes: 3 additions & 0 deletions vt/reference/rollback/reveal-complete--capture-01.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading