@routevn/creator-model 1.1.4 → 1.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@routevn/creator-model",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/model.js CHANGED
@@ -38,6 +38,7 @@ const createEmptyCollectionState = () => ({
38
38
  items: {},
39
39
  tree: [],
40
40
  });
41
+
41
42
  const normalizeStateCollections = (state) => {
42
43
  if (!isPlainObject(state)) {
43
44
  return state;
@@ -108,7 +109,14 @@ const ANIMATION_EASING_KEYS = [
108
109
  ];
109
110
  const VARIABLE_SCOPE_KEYS = ["context", "global-device", "global-account"];
110
111
  const VARIABLE_TYPE_KEYS = ["string", "number", "boolean"];
111
- const LAYOUT_TYPE_KEYS = ["normal", "dialogue", "nvl", "choice"];
112
+ const LAYOUT_TYPE_KEYS = [
113
+ "normal",
114
+ "save-load",
115
+ "confirmDialog",
116
+ "dialogue",
117
+ "nvl",
118
+ "choice",
119
+ ];
112
120
  const LAYOUT_ELEMENT_TEXT_STYLE_ALIGN_KEYS = ["left", "center", "right"];
113
121
  const LAYOUT_TEXT_REVEAL_EFFECT_KEYS = ["typewriter", "softWipe", "none"];
114
122
  const LAYOUT_ELEMENT_BASE_TYPES = [
@@ -122,17 +130,26 @@ const LAYOUT_ELEMENT_BASE_TYPES = [
122
130
  "text-ref-character-name",
123
131
  "text-revealing-ref-dialogue-content",
124
132
  "text-ref-choice-item-content",
133
+ "text-ref-save-load-slot-date",
125
134
  "text-ref-dialogue-line-character-name",
126
135
  "text-ref-dialogue-line-content",
136
+ "sprite-ref-save-load-slot-image",
137
+ "fragment-ref",
127
138
  "container-ref-choice-item",
139
+ "container-ref-save-load-slot",
128
140
  "container-ref-dialogue-line",
141
+ "container-ref-confirm-dialog-ok",
142
+ "container-ref-confirm-dialog-cancel",
129
143
  ];
130
144
  export const SCHEMA_VERSION = 2;
131
145
  const LAYOUT_CONTAINER_ELEMENT_TYPES = [
132
146
  "folder",
133
147
  "container",
134
148
  "container-ref-choice-item",
149
+ "container-ref-save-load-slot",
135
150
  "container-ref-dialogue-line",
151
+ "container-ref-confirm-dialog-ok",
152
+ "container-ref-confirm-dialog-cancel",
136
153
  ];
137
154
  const DOMAIN_ERROR_KIND_BY_NAME = {
138
155
  PayloadValidationError: "payload",
@@ -230,6 +247,81 @@ const isVariableReferenceTarget = (state, variableId) => {
230
247
  return isPlainObject(variable) && variable.type !== "folder";
231
248
  };
232
249
 
250
+ const LAYOUT_CONDITION_TARGET_SET = new Set([
251
+ "item.savedAt",
252
+ "isLineCompleted",
253
+ "autoMode",
254
+ "skipMode",
255
+ ]);
256
+ const VARIABLE_TARGET_DOT_PATTERN = /^variables\.([A-Za-z_$][A-Za-z0-9_$]*)$/;
257
+ const VARIABLE_TARGET_BRACKET_PATTERN = /^variables\[(.+)\]$/;
258
+
259
+ const parseLayoutConditionTarget = (target) => {
260
+ if (!isNonEmptyString(target)) {
261
+ return undefined;
262
+ }
263
+
264
+ if (LAYOUT_CONDITION_TARGET_SET.has(target)) {
265
+ return {
266
+ kind: "runtime",
267
+ target,
268
+ };
269
+ }
270
+
271
+ const dotMatch = target.match(VARIABLE_TARGET_DOT_PATTERN);
272
+ if (dotMatch) {
273
+ return {
274
+ kind: "variable",
275
+ target,
276
+ variableId: dotMatch[1],
277
+ };
278
+ }
279
+
280
+ const bracketMatch = target.match(VARIABLE_TARGET_BRACKET_PATTERN);
281
+ if (!bracketMatch) {
282
+ return undefined;
283
+ }
284
+
285
+ const rawValue = bracketMatch[1].trim();
286
+
287
+ try {
288
+ const variableId = JSON.parse(rawValue);
289
+ if (isNonEmptyString(variableId)) {
290
+ return {
291
+ kind: "variable",
292
+ target,
293
+ variableId,
294
+ };
295
+ }
296
+ } catch {}
297
+
298
+ if (rawValue.startsWith("'") && rawValue.endsWith("'")) {
299
+ const variableId = rawValue.slice(1, -1);
300
+ if (isNonEmptyString(variableId)) {
301
+ return {
302
+ kind: "variable",
303
+ target,
304
+ variableId,
305
+ };
306
+ }
307
+ }
308
+
309
+ return undefined;
310
+ };
311
+
312
+ const isLayoutConditionTarget = (state, target) => {
313
+ const parsedTarget = parseLayoutConditionTarget(target);
314
+ if (!parsedTarget) {
315
+ return false;
316
+ }
317
+
318
+ if (parsedTarget.kind === "runtime") {
319
+ return true;
320
+ }
321
+
322
+ return isVariableReferenceTarget(state, parsedTarget.variableId);
323
+ };
324
+
233
325
  const toDomainErrorDetails = (publicError) => {
234
326
  const details = isPlainObject(publicError?.details)
235
327
  ? { ...publicError.details }
@@ -2181,6 +2273,29 @@ const validateLayoutElementBorder = ({ border, path, errorFactory }) => {
2181
2273
  }
2182
2274
  };
2183
2275
 
2276
+ const validateLayoutElementInteraction = ({
2277
+ interaction,
2278
+ path,
2279
+ errorFactory,
2280
+ }) => {
2281
+ if (!isPlainObject(interaction)) {
2282
+ return invalidFromErrorFactory(
2283
+ errorFactory,
2284
+ `${path} must be an object when provided`,
2285
+ );
2286
+ }
2287
+
2288
+ if (
2289
+ interaction.inheritToChildren !== undefined &&
2290
+ typeof interaction.inheritToChildren !== "boolean"
2291
+ ) {
2292
+ return invalidFromErrorFactory(
2293
+ errorFactory,
2294
+ `${path}.inheritToChildren must be a boolean when provided`,
2295
+ );
2296
+ }
2297
+ };
2298
+
2184
2299
  const validateLayoutElementData = ({
2185
2300
  data,
2186
2301
  path,
@@ -2216,10 +2331,14 @@ const validateLayoutElementData = ({
2216
2331
  "textStyleId",
2217
2332
  "hoverTextStyleId",
2218
2333
  "clickTextStyleId",
2334
+ "conditionalOverrides",
2219
2335
  "direction",
2220
2336
  "gap",
2221
2337
  "containerType",
2222
2338
  "scroll",
2339
+ "hover",
2340
+ "click",
2341
+ "rightClick",
2223
2342
  "anchorToBottom",
2224
2343
  "thumbImageId",
2225
2344
  "barImageId",
@@ -2230,9 +2349,11 @@ const validateLayoutElementData = ({
2230
2349
  "step",
2231
2350
  "initialValue",
2232
2351
  "variableId",
2352
+ "fragmentLayoutId",
2353
+ "paginationMode",
2354
+ "paginationVariableId",
2355
+ "paginationSize",
2233
2356
  "$when",
2234
- "click",
2235
- "rightClick",
2236
2357
  "change",
2237
2358
  ];
2238
2359
 
@@ -2281,6 +2402,7 @@ const validateLayoutElementData = ({
2281
2402
  "min",
2282
2403
  "max",
2283
2404
  "step",
2405
+ "paginationSize",
2284
2406
  "opacity",
2285
2407
  ]) {
2286
2408
  if (data[key] !== undefined && !isFiniteNumber(data[key])) {
@@ -2322,6 +2444,9 @@ const validateLayoutElementData = ({
2322
2444
  "clickTextStyleId",
2323
2445
  "containerType",
2324
2446
  "variableId",
2447
+ "fragmentLayoutId",
2448
+ "paginationMode",
2449
+ "paginationVariableId",
2325
2450
  "revealEffect",
2326
2451
  "thumbImageId",
2327
2452
  "barImageId",
@@ -2337,6 +2462,191 @@ const validateLayoutElementData = ({
2337
2462
  }
2338
2463
  }
2339
2464
 
2465
+ if (data.conditionalOverrides !== undefined) {
2466
+ if (!Array.isArray(data.conditionalOverrides)) {
2467
+ return invalidFromErrorFactory(
2468
+ errorFactory,
2469
+ `${path}.conditionalOverrides must be an array when provided`,
2470
+ );
2471
+ }
2472
+
2473
+ for (let index = 0; index < data.conditionalOverrides.length; index += 1) {
2474
+ const rule = data.conditionalOverrides[index];
2475
+ const rulePath = `${path}.conditionalOverrides.${index}`;
2476
+
2477
+ if (!isPlainObject(rule)) {
2478
+ return invalidFromErrorFactory(
2479
+ errorFactory,
2480
+ `${rulePath} must be an object`,
2481
+ );
2482
+ }
2483
+
2484
+ {
2485
+ const result = validateAllowedKeys({
2486
+ value: rule,
2487
+ allowedKeys: ["when", "set"],
2488
+ path: rulePath,
2489
+ errorFactory,
2490
+ });
2491
+ if (result?.valid === false) {
2492
+ return result;
2493
+ }
2494
+ }
2495
+
2496
+ if (!isPlainObject(rule.when)) {
2497
+ return invalidFromErrorFactory(
2498
+ errorFactory,
2499
+ `${rulePath}.when must be an object`,
2500
+ );
2501
+ }
2502
+
2503
+ {
2504
+ const result = validateAllowedKeys({
2505
+ value: rule.when,
2506
+ allowedKeys: ["target", "op", "value"],
2507
+ path: `${rulePath}.when`,
2508
+ errorFactory,
2509
+ });
2510
+ if (result?.valid === false) {
2511
+ return result;
2512
+ }
2513
+ }
2514
+
2515
+ if (!isNonEmptyString(rule.when.target)) {
2516
+ return invalidFromErrorFactory(
2517
+ errorFactory,
2518
+ `${rulePath}.when.target must be a non-empty string`,
2519
+ );
2520
+ }
2521
+
2522
+ if (!parseLayoutConditionTarget(rule.when.target)) {
2523
+ return invalidFromErrorFactory(
2524
+ errorFactory,
2525
+ `${rulePath}.when.target must be a supported layout condition target`,
2526
+ );
2527
+ }
2528
+
2529
+ if (
2530
+ !isString(rule.when.value) &&
2531
+ typeof rule.when.value !== "boolean" &&
2532
+ !isFiniteNumber(rule.when.value)
2533
+ ) {
2534
+ return invalidFromErrorFactory(
2535
+ errorFactory,
2536
+ `${rulePath}.when.value must be a string, boolean, or finite number`,
2537
+ );
2538
+ }
2539
+
2540
+ if (rule.when.op !== "eq") {
2541
+ return invalidFromErrorFactory(
2542
+ errorFactory,
2543
+ `${rulePath}.when.op must be "eq"`,
2544
+ );
2545
+ }
2546
+
2547
+ if (!isPlainObject(rule.set)) {
2548
+ return invalidFromErrorFactory(
2549
+ errorFactory,
2550
+ `${rulePath}.set must be an object`,
2551
+ );
2552
+ }
2553
+
2554
+ {
2555
+ const result = validateAllowedKeys({
2556
+ value: rule.set,
2557
+ allowedKeys: [
2558
+ "textStyleId",
2559
+ "hoverTextStyleId",
2560
+ "clickTextStyleId",
2561
+ "imageId",
2562
+ "hoverImageId",
2563
+ "clickImageId",
2564
+ "opacity",
2565
+ "anchorX",
2566
+ "anchorY",
2567
+ "visible",
2568
+ "textStyle",
2569
+ ],
2570
+ path: `${rulePath}.set`,
2571
+ errorFactory,
2572
+ });
2573
+ if (result?.valid === false) {
2574
+ return result;
2575
+ }
2576
+ }
2577
+
2578
+ for (const field of [
2579
+ "textStyleId",
2580
+ "hoverTextStyleId",
2581
+ "clickTextStyleId",
2582
+ "imageId",
2583
+ "hoverImageId",
2584
+ "clickImageId",
2585
+ ]) {
2586
+ if (
2587
+ rule.set[field] !== undefined &&
2588
+ !isNonEmptyString(rule.set[field])
2589
+ ) {
2590
+ return invalidFromErrorFactory(
2591
+ errorFactory,
2592
+ `${rulePath}.set.${field} must be a non-empty string when provided`,
2593
+ );
2594
+ }
2595
+ }
2596
+
2597
+ if (
2598
+ rule.set.opacity !== undefined &&
2599
+ (!isFiniteNumber(rule.set.opacity) ||
2600
+ rule.set.opacity < 0 ||
2601
+ rule.set.opacity > 1)
2602
+ ) {
2603
+ return invalidFromErrorFactory(
2604
+ errorFactory,
2605
+ `${rulePath}.set.opacity must be a finite number between 0 and 1 when provided`,
2606
+ );
2607
+ }
2608
+
2609
+ for (const field of ["anchorX", "anchorY"]) {
2610
+ if (rule.set[field] !== undefined && !isFiniteNumber(rule.set[field])) {
2611
+ return invalidFromErrorFactory(
2612
+ errorFactory,
2613
+ `${rulePath}.set.${field} must be a finite number when provided`,
2614
+ );
2615
+ }
2616
+ }
2617
+
2618
+ if (
2619
+ rule.set.visible !== undefined &&
2620
+ typeof rule.set.visible !== "boolean"
2621
+ ) {
2622
+ return invalidFromErrorFactory(
2623
+ errorFactory,
2624
+ `${rulePath}.set.visible must be a boolean when provided`,
2625
+ );
2626
+ }
2627
+
2628
+ if (rule.set.textStyle !== undefined) {
2629
+ if (!isPlainObject(rule.set.textStyle)) {
2630
+ return invalidFromErrorFactory(
2631
+ errorFactory,
2632
+ `${rulePath}.set.textStyle must be an object when provided`,
2633
+ );
2634
+ }
2635
+
2636
+ {
2637
+ const result = validateLayoutElementTextStyle({
2638
+ textStyle: rule.set.textStyle,
2639
+ path: `${rulePath}.set.textStyle`,
2640
+ errorFactory,
2641
+ });
2642
+ if (result?.valid === false) {
2643
+ return result;
2644
+ }
2645
+ }
2646
+ }
2647
+ }
2648
+ }
2649
+
2340
2650
  if (data.fill !== undefined && !isString(data.fill)) {
2341
2651
  return invalidFromErrorFactory(
2342
2652
  errorFactory,
@@ -2365,6 +2675,17 @@ const validateLayoutElementData = ({
2365
2675
  );
2366
2676
  }
2367
2677
 
2678
+ if (
2679
+ data.paginationMode !== undefined &&
2680
+ data.paginationMode !== "continuous" &&
2681
+ data.paginationMode !== "paginated"
2682
+ ) {
2683
+ return invalidFromErrorFactory(
2684
+ errorFactory,
2685
+ `${path}.paginationMode must be 'continuous' or 'paginated' when provided`,
2686
+ );
2687
+ }
2688
+
2368
2689
  if (data.scroll !== undefined && typeof data.scroll !== "boolean") {
2369
2690
  return invalidFromErrorFactory(
2370
2691
  errorFactory,
@@ -2372,6 +2693,45 @@ const validateLayoutElementData = ({
2372
2693
  );
2373
2694
  }
2374
2695
 
2696
+ if (data.hover !== undefined) {
2697
+ {
2698
+ const result = validateLayoutElementInteraction({
2699
+ interaction: data.hover,
2700
+ path: `${path}.hover`,
2701
+ errorFactory,
2702
+ });
2703
+ if (result?.valid === false) {
2704
+ return result;
2705
+ }
2706
+ }
2707
+ }
2708
+
2709
+ if (data.click !== undefined) {
2710
+ {
2711
+ const result = validateLayoutElementInteraction({
2712
+ interaction: data.click,
2713
+ path: `${path}.click`,
2714
+ errorFactory,
2715
+ });
2716
+ if (result?.valid === false) {
2717
+ return result;
2718
+ }
2719
+ }
2720
+ }
2721
+
2722
+ if (data.rightClick !== undefined) {
2723
+ {
2724
+ const result = validateLayoutElementInteraction({
2725
+ interaction: data.rightClick,
2726
+ path: `${path}.rightClick`,
2727
+ errorFactory,
2728
+ });
2729
+ if (result?.valid === false) {
2730
+ return result;
2731
+ }
2732
+ }
2733
+ }
2734
+
2375
2735
  if (
2376
2736
  data.anchorToBottom !== undefined &&
2377
2737
  typeof data.anchorToBottom !== "boolean"
@@ -2415,20 +2775,6 @@ const validateLayoutElementData = ({
2415
2775
  }
2416
2776
  }
2417
2777
 
2418
- if (data.click !== undefined && !isPlainObject(data.click)) {
2419
- return invalidFromErrorFactory(
2420
- errorFactory,
2421
- `${path}.click must be an object when provided`,
2422
- );
2423
- }
2424
-
2425
- if (data.rightClick !== undefined && !isPlainObject(data.rightClick)) {
2426
- return invalidFromErrorFactory(
2427
- errorFactory,
2428
- `${path}.rightClick must be an object when provided`,
2429
- );
2430
- }
2431
-
2432
2778
  if (data.change !== undefined && !isPlainObject(data.change)) {
2433
2779
  return invalidFromErrorFactory(
2434
2780
  errorFactory,
@@ -2470,10 +2816,14 @@ const validateLayoutElementItems = ({ items, path, errorFactory }) => {
2470
2816
  "textStyleId",
2471
2817
  "hoverTextStyleId",
2472
2818
  "clickTextStyleId",
2819
+ "conditionalOverrides",
2473
2820
  "direction",
2474
2821
  "gap",
2475
2822
  "containerType",
2476
2823
  "scroll",
2824
+ "hover",
2825
+ "click",
2826
+ "rightClick",
2477
2827
  "anchorToBottom",
2478
2828
  "thumbImageId",
2479
2829
  "barImageId",
@@ -2484,9 +2834,11 @@ const validateLayoutElementItems = ({ items, path, errorFactory }) => {
2484
2834
  "step",
2485
2835
  "initialValue",
2486
2836
  "variableId",
2837
+ "fragmentLayoutId",
2838
+ "paginationMode",
2839
+ "paginationVariableId",
2840
+ "paginationSize",
2487
2841
  "$when",
2488
- "click",
2489
- "rightClick",
2490
2842
  "change",
2491
2843
  ],
2492
2844
  path: itemPath,
@@ -2684,7 +3036,15 @@ const validateLayoutItems = ({ items, path, errorFactory }) => {
2684
3036
  allowedKeys:
2685
3037
  item.type === "folder"
2686
3038
  ? ["id", "type", "name"]
2687
- : ["id", "type", "name", "layoutType", "elements"],
3039
+ : [
3040
+ "id",
3041
+ "type",
3042
+ "name",
3043
+ "description",
3044
+ "layoutType",
3045
+ "isFragment",
3046
+ "elements",
3047
+ ],
2688
3048
  path: itemPath,
2689
3049
  errorFactory,
2690
3050
  });
@@ -2714,11 +3074,28 @@ const validateLayoutItems = ({ items, path, errorFactory }) => {
2714
3074
  );
2715
3075
  }
2716
3076
 
3077
+ if (item.description !== undefined && !isString(item.description)) {
3078
+ return invalidFromErrorFactory(
3079
+ errorFactory,
3080
+ `${itemPath}.description must be a string when provided`,
3081
+ );
3082
+ }
3083
+
2717
3084
  if (item.type === "layout") {
2718
3085
  if (!LAYOUT_TYPE_KEYS.includes(item.layoutType)) {
2719
3086
  return invalidFromErrorFactory(
2720
3087
  errorFactory,
2721
- `${itemPath}.layoutType must be 'normal', 'dialogue', 'nvl', or 'choice'`,
3088
+ `${itemPath}.layoutType must be 'normal', 'save-load', 'confirmDialog', 'dialogue', 'nvl', or 'choice'`,
3089
+ );
3090
+ }
3091
+
3092
+ if (
3093
+ item.isFragment !== undefined &&
3094
+ typeof item.isFragment !== "boolean"
3095
+ ) {
3096
+ return invalidFromErrorFactory(
3097
+ errorFactory,
3098
+ `${itemPath}.isFragment must be a boolean when provided`,
2722
3099
  );
2723
3100
  }
2724
3101
 
@@ -3923,6 +4300,32 @@ export const assertInvariants = ({ state }) => {
3923
4300
  return VALID_RESULT;
3924
4301
  };
3925
4302
 
4303
+ const assertFragmentLayoutReference = ({
4304
+ ownerIdField,
4305
+ ownerId,
4306
+ ownerLabel,
4307
+ elementId,
4308
+ targetId,
4309
+ }) => {
4310
+ const layout = state.layouts.items[targetId];
4311
+ if (
4312
+ !isPlainObject(layout) ||
4313
+ layout.type !== "layout" ||
4314
+ layout.isFragment !== true
4315
+ ) {
4316
+ return invalidInvariant(
4317
+ `${ownerLabel} element fragmentLayoutId must reference an existing fragment layout`,
4318
+ {
4319
+ [ownerIdField]: ownerId,
4320
+ elementId,
4321
+ fragmentLayoutId: targetId,
4322
+ },
4323
+ );
4324
+ }
4325
+
4326
+ return VALID_RESULT;
4327
+ };
4328
+
3926
4329
  const assertElementReferencesForCollection = ({
3927
4330
  items,
3928
4331
  ownerIdField,
@@ -3979,6 +4382,51 @@ export const assertInvariants = ({ state }) => {
3979
4382
  }
3980
4383
  }
3981
4384
 
4385
+ if (Array.isArray(element.conditionalOverrides)) {
4386
+ for (
4387
+ let index = 0;
4388
+ index < element.conditionalOverrides.length;
4389
+ index += 1
4390
+ ) {
4391
+ const rule = element.conditionalOverrides[index];
4392
+
4393
+ for (const field of [
4394
+ "textStyleId",
4395
+ "hoverTextStyleId",
4396
+ "clickTextStyleId",
4397
+ ]) {
4398
+ if (rule?.set?.[field] !== undefined) {
4399
+ const result = assertTextStyleReference({
4400
+ ownerIdField,
4401
+ ownerId,
4402
+ ownerLabel,
4403
+ elementId,
4404
+ field: `conditionalOverrides.${index}.set.${field}`,
4405
+ targetId: rule.set[field],
4406
+ });
4407
+ if (!result.valid) {
4408
+ return result;
4409
+ }
4410
+ }
4411
+ }
4412
+
4413
+ if (
4414
+ rule?.when?.target !== undefined &&
4415
+ !isLayoutConditionTarget(state, rule.when.target)
4416
+ ) {
4417
+ return invalidInvariant(
4418
+ `${ownerLabel} element conditionalOverrides when target must reference an existing variable or supported runtime condition`,
4419
+ {
4420
+ [ownerIdField]: ownerId,
4421
+ elementId,
4422
+ field: `conditionalOverrides.${index}.when.target`,
4423
+ targetId: rule.when.target,
4424
+ },
4425
+ );
4426
+ }
4427
+ }
4428
+ }
4429
+
3982
4430
  if (element.variableId !== undefined) {
3983
4431
  const result = assertVariableReference({
3984
4432
  ownerIdField,
@@ -3991,6 +4439,19 @@ export const assertInvariants = ({ state }) => {
3991
4439
  return result;
3992
4440
  }
3993
4441
  }
4442
+
4443
+ if (element.fragmentLayoutId !== undefined) {
4444
+ const result = assertFragmentLayoutReference({
4445
+ ownerIdField,
4446
+ ownerId,
4447
+ ownerLabel,
4448
+ elementId,
4449
+ targetId: element.fragmentLayoutId,
4450
+ });
4451
+ if (!result.valid) {
4452
+ return result;
4453
+ }
4454
+ }
3994
4455
  }
3995
4456
  }
3996
4457
 
@@ -6136,7 +6597,14 @@ const validateLayoutCreateData = ({ data, errorFactory }) => {
6136
6597
  allowedKeys:
6137
6598
  data.type === "folder"
6138
6599
  ? ["type", "name"]
6139
- : ["type", "name", "layoutType", "elements"],
6600
+ : [
6601
+ "type",
6602
+ "name",
6603
+ "description",
6604
+ "layoutType",
6605
+ "isFragment",
6606
+ "elements",
6607
+ ],
6140
6608
  path: "payload.data",
6141
6609
  errorFactory,
6142
6610
  });
@@ -6152,11 +6620,25 @@ const validateLayoutCreateData = ({ data, errorFactory }) => {
6152
6620
  );
6153
6621
  }
6154
6622
 
6623
+ if (data.description !== undefined && !isString(data.description)) {
6624
+ return invalidFromErrorFactory(
6625
+ errorFactory,
6626
+ "payload.data.description must be a string when provided",
6627
+ );
6628
+ }
6629
+
6155
6630
  if (data.type === "layout") {
6156
6631
  if (!LAYOUT_TYPE_KEYS.includes(data.layoutType)) {
6157
6632
  return invalidFromErrorFactory(
6158
6633
  errorFactory,
6159
- "payload.data.layoutType must be 'normal', 'dialogue', 'nvl', or 'choice'",
6634
+ "payload.data.layoutType must be 'normal', 'save-load', 'confirmDialog', 'dialogue', 'nvl', or 'choice'",
6635
+ );
6636
+ }
6637
+
6638
+ if (data.isFragment !== undefined && typeof data.isFragment !== "boolean") {
6639
+ return invalidFromErrorFactory(
6640
+ errorFactory,
6641
+ "payload.data.isFragment must be a boolean when provided",
6160
6642
  );
6161
6643
  }
6162
6644
 
@@ -6179,7 +6661,7 @@ const validateLayoutUpdateData = ({ data, errorFactory }) => {
6179
6661
  {
6180
6662
  const result = validateAllowedKeys({
6181
6663
  value: data,
6182
- allowedKeys: ["name", "layoutType"],
6664
+ allowedKeys: ["name", "description", "layoutType", "isFragment"],
6183
6665
  path: "payload.data",
6184
6666
  errorFactory,
6185
6667
  });
@@ -6202,13 +6684,27 @@ const validateLayoutUpdateData = ({ data, errorFactory }) => {
6202
6684
  );
6203
6685
  }
6204
6686
 
6687
+ if (data.description !== undefined && !isString(data.description)) {
6688
+ return invalidFromErrorFactory(
6689
+ errorFactory,
6690
+ "payload.data.description must be a string when provided",
6691
+ );
6692
+ }
6693
+
6205
6694
  if (
6206
6695
  data.layoutType !== undefined &&
6207
6696
  !LAYOUT_TYPE_KEYS.includes(data.layoutType)
6208
6697
  ) {
6209
6698
  return invalidFromErrorFactory(
6210
6699
  errorFactory,
6211
- "payload.data.layoutType must be 'normal', 'dialogue', 'nvl', or 'choice' when provided",
6700
+ "payload.data.layoutType must be 'normal', 'save-load', 'confirmDialog', 'dialogue', 'nvl', or 'choice' when provided",
6701
+ );
6702
+ }
6703
+
6704
+ if (data.isFragment !== undefined && typeof data.isFragment !== "boolean") {
6705
+ return invalidFromErrorFactory(
6706
+ errorFactory,
6707
+ "payload.data.isFragment must be a boolean when provided",
6212
6708
  );
6213
6709
  }
6214
6710
  };
@@ -6519,6 +7015,69 @@ const validateVisualElementReferenceTargets = ({
6519
7015
  }
6520
7016
  }
6521
7017
 
7018
+ if (Array.isArray(data.conditionalOverrides)) {
7019
+ for (let index = 0; index < data.conditionalOverrides.length; index += 1) {
7020
+ const rule = data.conditionalOverrides[index];
7021
+
7022
+ for (const field of [
7023
+ "textStyleId",
7024
+ "hoverTextStyleId",
7025
+ "clickTextStyleId",
7026
+ ]) {
7027
+ if (rule?.set?.[field] === undefined) {
7028
+ continue;
7029
+ }
7030
+
7031
+ const textStyle = state.textStyles.items[rule.set[field]];
7032
+ if (!isPlainObject(textStyle) || textStyle.type === "folder") {
7033
+ return invalidFromErrorFactory(
7034
+ errorFactory,
7035
+ `${ownerLabel} element conditionalOverrides.${index}.set.${field} must reference an existing non-folder text style`,
7036
+ {
7037
+ [ownerIdField]: ownerId,
7038
+ elementId,
7039
+ field: `conditionalOverrides.${index}.set.${field}`,
7040
+ targetId: rule.set[field],
7041
+ },
7042
+ );
7043
+ }
7044
+ }
7045
+
7046
+ for (const field of ["imageId", "hoverImageId", "clickImageId"]) {
7047
+ if (rule?.set?.[field] === undefined) {
7048
+ continue;
7049
+ }
7050
+
7051
+ const image = state.images.items[rule.set[field]];
7052
+ if (!isPlainObject(image) || image.type === "folder") {
7053
+ return invalidFromErrorFactory(
7054
+ errorFactory,
7055
+ `${ownerLabel} element conditionalOverrides.${index}.set.${field} must reference an existing non-folder image`,
7056
+ {
7057
+ [ownerIdField]: ownerId,
7058
+ elementId,
7059
+ field: `conditionalOverrides.${index}.set.${field}`,
7060
+ targetId: rule.set[field],
7061
+ },
7062
+ );
7063
+ }
7064
+ }
7065
+
7066
+ if (!isLayoutConditionTarget(state, rule?.when?.target)) {
7067
+ return invalidFromErrorFactory(
7068
+ errorFactory,
7069
+ `${ownerLabel} element conditionalOverrides.${index}.when.target must reference an existing variable or supported runtime condition`,
7070
+ {
7071
+ [ownerIdField]: ownerId,
7072
+ elementId,
7073
+ field: `conditionalOverrides.${index}.when.target`,
7074
+ targetId: rule?.when?.target,
7075
+ },
7076
+ );
7077
+ }
7078
+ }
7079
+ }
7080
+
6522
7081
  if (data.variableId !== undefined) {
6523
7082
  if (!isVariableReferenceTarget(state, data.variableId)) {
6524
7083
  return invalidFromErrorFactory(
@@ -6532,6 +7091,25 @@ const validateVisualElementReferenceTargets = ({
6532
7091
  );
6533
7092
  }
6534
7093
  }
7094
+
7095
+ if (data.fragmentLayoutId !== undefined) {
7096
+ const fragmentLayout = state.layouts.items[data.fragmentLayoutId];
7097
+ if (
7098
+ !isPlainObject(fragmentLayout) ||
7099
+ fragmentLayout.type !== "layout" ||
7100
+ fragmentLayout.isFragment !== true
7101
+ ) {
7102
+ return invalidFromErrorFactory(
7103
+ errorFactory,
7104
+ `${ownerLabel} element fragmentLayoutId must reference an existing fragment layout`,
7105
+ {
7106
+ [ownerIdField]: ownerId,
7107
+ elementId,
7108
+ fragmentLayoutId: data.fragmentLayoutId,
7109
+ },
7110
+ );
7111
+ }
7112
+ }
6535
7113
  };
6536
7114
 
6537
7115
  const getTreeIdsInOrder = ({ nodes }) => {
@@ -11198,7 +11776,9 @@ const COMMAND_DEFINITIONS = [
11198
11776
  name: payload.data.name,
11199
11777
  ...(payload.data.type === "layout"
11200
11778
  ? {
11779
+ description: payload.data.description,
11201
11780
  layoutType: payload.data.layoutType,
11781
+ isFragment: payload.data.isFragment,
11202
11782
  elements: structuredClone(payload.data.elements),
11203
11783
  }
11204
11784
  : {}),
@@ -21,6 +21,15 @@ export const SYSTEM_VARIABLE_GROUPS = Object.freeze([
21
21
  description:
22
22
  "Controls the default dialogue text speed stored for this device.",
23
23
  },
24
+ {
25
+ id: "_currentSaveLoadPagination",
26
+ name: "Current Save/Load Pagination",
27
+ scope: "runtime-screen",
28
+ type: "number",
29
+ default: 0,
30
+ description:
31
+ "The current save/load pagination index. Resolves to 0, 1, 2, 3, and so on for the active save/load page.",
32
+ },
24
33
  ]),
25
34
  },
26
35
  ]);