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
2 changes: 2 additions & 0 deletions docs/SaveLoad.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,8 @@ Current save/load-related selectors are:

`selectSaveSlotPage` is a UI helper for paginated save/load screens. It flattens the current page into slot UI items based on the `loadPage` variable.

An active save/load layout can set `resources.layouts.<layoutId>.paginationSize` to a positive integer. During rendering, the topmost overlay layout that sets this field takes precedence, followed by the current presentation layout. If neither defines a page size, the engine uses six slots. Direct calls to `selectSaveSlotPage({ slotsPerPage })` can still supply an explicit size.

### Effects

The save/load path crosses the store boundary through effects:
Expand Down
63 changes: 63 additions & 0 deletions spec/RouteEngine.saveLoadPagination.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { describe, expect, it } from "vitest";
import createRouteEngine from "../src/RouteEngine.js";

describe("RouteEngine save/load pagination", () => {
it("uses the active layout paginationSize when selecting rendered slots", () => {
const engine = createRouteEngine({ handlePendingEffects: () => {} });

engine.init({
initialState: {
global: {},
projectData: {
screen: { width: 800, height: 600 },
resources: {
layouts: {
saveGrid: {
paginationSize: 4,
elements: [
{
"$for slot in saveSlots": [
{
id: "save-slot-${slot.slotId}",
type: "text",
content: "${slot.slotId}",
},
],
},
],
},
},
},
story: {
initialSceneId: "scene1",
scenes: {
scene1: {
initialSectionId: "section1",
sections: {
section1: {
lines: [{ id: "line1", actions: {} }],
},
},
},
},
},
},
},
});

engine.handleAction("setSaveLoadPagination", { value: 2 });
engine.handleAction("pushOverlay", {
resourceId: "saveGrid",
resourceType: "layout",
});

const overlay = engine
.selectRenderState()
.elements.find((element) => element.id === "overlayStack-0");
const slots = overlay.children.filter((element) =>
element.id?.startsWith("save-slot-"),
);

expect(slots.map(({ content }) => content)).toEqual([5, 6, 7, 8]);
});
});
32 changes: 32 additions & 0 deletions spec/projectDataSchema.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -772,6 +772,38 @@ describe("projectData schema", () => {
expect(validateProjectData.errors).toBeNull();
});

it("requires layout paginationSize to be a positive integer", () => {
expect(
validateProjectData(
createMinimalProjectData({
resources: {
layouts: {
saveGrid: {
paginationSize: 4,
elements: [],
},
},
},
}),
),
).toBe(true);
expect(validateProjectData.errors).toBeNull();

for (const paginationSize of [0, -1, 1.5]) {
expect(
validateProjectData(
createMinimalProjectData({
resources: {
layouts: {
saveGrid: { paginationSize, elements: [] },
},
},
}),
),
).toBe(false);
}
});

it("requires width and height on spritesheet resources", () => {
const projectData = createMinimalProjectData({
resources: {
Expand Down
2 changes: 1 addition & 1 deletion src/generated/l10nPayloadValidators.js

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions src/schemas/projectData/resources.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,10 @@ properties:
type: object
description: Layout configuration. This schema is intentionally permissive because runtime-supported layout fields extend beyond the documented subset.
properties:
paginationSize:
type: integer
minimum: 1
description: Number of save/load slots shown per page when this layout is active
elements:
type: array
description: Array of UI elements authored with textStyleId, imageId, colorId, and inline shader filters
Expand Down
35 changes: 33 additions & 2 deletions src/stores/system.store.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const DEFAULT_NEXT_LINE_CONFIG = {
};

const CURRENT_SAVE_FORMAT_VERSION = 1;
const DEFAULT_SAVE_SLOT_PAGE_SIZE = 6;
const RANDOM_OUTCOME_VERSION = 1;
const CHOICE_INTERACTION_SOURCE = "choice";
const FORM_INTERACTION_SOURCE = "form";
Expand Down Expand Up @@ -3815,7 +3816,10 @@ export const selectPreviousPresentationState = ({ state }) => {
* ]
* }
*/
export const selectSaveSlotPage = ({ state }, { slotsPerPage = 6 } = {}) => {
export const selectSaveSlotPage = (
{ state },
{ slotsPerPage = DEFAULT_SAVE_SLOT_PAGE_SIZE } = {},
) => {
const runtime = selectRuntime({ state });
const saveLoadPagination = runtime.saveLoadPagination ?? 1;
const startSlot = (saveLoadPagination - 1) * slotsPerPage + 1;
Expand Down Expand Up @@ -3857,6 +3861,30 @@ export const shouldSettleCurrentLinePresentation = (state) => {
);
};

const getSaveSlotPageSize = (state, presentationState) => {
const layouts = state.projectData.resources?.layouts ?? {};
const overlayLayoutIds = [...(state.global.overlayStack ?? [])]
.reverse()
.map((overlay) => overlay?.resourceId);
const layoutIds = [...overlayLayoutIds, presentationState.layout?.resourceId];

for (const layoutId of layoutIds) {
if (
typeof layoutId !== "string" ||
!Object.prototype.hasOwnProperty.call(layouts, layoutId)
) {
continue;
}

const paginationSize = layouts[layoutId]?.paginationSize;
if (Number.isInteger(paginationSize) && paginationSize > 0) {
return paginationSize;
}
}

return DEFAULT_SAVE_SLOT_PAGE_SIZE;
};

export const selectRenderState = ({ state }, options = {}) => {
const presentationState = selectPresentationState({ state });
const previousPresentationState = selectPreviousPresentationState({ state });
Expand Down Expand Up @@ -3893,7 +3921,10 @@ export const selectRenderState = ({ state }, options = {}) => {

const allVariables = selectAllVariables({ state });

const { saveSlots } = selectSaveSlotPage({ state });
const { saveSlots } = selectSaveSlotPage(
{ state },
{ slotsPerPage: getSaveSlotPageSize(state, presentationState) },
);
const settleCurrentLinePresentation =
shouldSettleCurrentLinePresentation(state);

Expand Down
Loading