@routevn/creator-model 1.7.2 → 1.8.0

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/model.js +638 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@routevn/creator-model",
3
- "version": "1.7.2",
3
+ "version": "1.8.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/model.js CHANGED
@@ -392,7 +392,7 @@ const LAYOUT_ELEMENT_BASE_TYPES = [
392
392
  "container-ref-confirm-dialog-ok",
393
393
  "container-ref-confirm-dialog-cancel",
394
394
  ];
395
- export const SCHEMA_VERSION = 7;
395
+ export const SCHEMA_VERSION = 8;
396
396
  const LAYOUT_CONTAINER_ELEMENT_TYPES = [
397
397
  "folder",
398
398
  "container",
@@ -404,6 +404,18 @@ const LAYOUT_CONTAINER_ELEMENT_TYPES = [
404
404
  "container-ref-confirm-dialog-ok",
405
405
  "container-ref-confirm-dialog-cancel",
406
406
  ];
407
+ const LAYOUT_TEXT_CONTENT_ELEMENT_TYPES = [
408
+ "text",
409
+ "text-revealing",
410
+ "text-ref-character-name",
411
+ "text-revealing-ref-dialogue-content",
412
+ "text-ref-choice-item-content",
413
+ "text-ref-save-load-slot-date",
414
+ "text-ref-dialogue-line-character-name",
415
+ "text-ref-dialogue-line-content",
416
+ "text-ref-history-line-character-name",
417
+ "text-ref-history-line-content",
418
+ ];
407
419
  const DOMAIN_ERROR_KIND_BY_NAME = {
408
420
  PayloadValidationError: "payload",
409
421
  PreconditionValidationError: "precondition",
@@ -496,6 +508,15 @@ const isVariableReferenceTarget = (state, variableId) => {
496
508
  return isPlainObject(variable) && variable.type !== "folder";
497
509
  };
498
510
 
511
+ const isStringVariableReferenceTarget = (state, variableId) => {
512
+ const variable = state.variables.items[variableId];
513
+ return (
514
+ isPlainObject(variable) &&
515
+ variable.type === "variable" &&
516
+ variable.variableType === "string"
517
+ );
518
+ };
519
+
499
520
  const LAYOUT_ITEM_TARGET_SET = new Set(["item.savedAt"]);
500
521
  const LAYOUT_DIALOGUE_TARGET_SET = new Set(["dialogue.characterId"]);
501
522
  const RUNTIME_TARGET_DOT_PATTERN = /^runtime\.([A-Za-z_$][A-Za-z0-9_$]*)$/;
@@ -3410,6 +3431,251 @@ const validateLayoutElementBlur = ({ blur, path, errorFactory }) => {
3410
3431
  }
3411
3432
  };
3412
3433
 
3434
+ const LAYOUT_TEXT_REVEAL_INDICATOR_ELEMENT_TYPES = [
3435
+ "text-revealing",
3436
+ "text-revealing-ref-dialogue-content",
3437
+ ];
3438
+
3439
+ const validateLayoutTextRevealIndicatorVisual = ({
3440
+ visual,
3441
+ path,
3442
+ errorFactory,
3443
+ }) => {
3444
+ if (!isPlainObject(visual)) {
3445
+ return invalidFromErrorFactory(
3446
+ errorFactory,
3447
+ `${path} must be an object when provided`,
3448
+ );
3449
+ }
3450
+
3451
+ {
3452
+ const result = validateAllowedKeys({
3453
+ value: visual,
3454
+ allowedKeys: [
3455
+ "kind",
3456
+ "imageId",
3457
+ "resourceId",
3458
+ "animationName",
3459
+ "width",
3460
+ "height",
3461
+ "offsetX",
3462
+ "offsetY",
3463
+ ],
3464
+ path,
3465
+ errorFactory,
3466
+ });
3467
+ if (result?.valid === false) {
3468
+ return result;
3469
+ }
3470
+ }
3471
+
3472
+ const kind =
3473
+ visual.kind === "spritesheet" ||
3474
+ visual.resourceId !== undefined ||
3475
+ visual.animationName !== undefined
3476
+ ? "spritesheet"
3477
+ : "image";
3478
+
3479
+ if (
3480
+ visual.kind !== undefined &&
3481
+ visual.kind !== "image" &&
3482
+ visual.kind !== "spritesheet"
3483
+ ) {
3484
+ return invalidFromErrorFactory(
3485
+ errorFactory,
3486
+ `${path}.kind must be 'image' or 'spritesheet' when provided`,
3487
+ );
3488
+ }
3489
+
3490
+ if (kind === "image" && !isNonEmptyString(visual.imageId)) {
3491
+ return invalidFromErrorFactory(
3492
+ errorFactory,
3493
+ `${path}.imageId must be a non-empty string`,
3494
+ );
3495
+ }
3496
+
3497
+ if (kind === "spritesheet") {
3498
+ if (!isNonEmptyString(visual.resourceId)) {
3499
+ return invalidFromErrorFactory(
3500
+ errorFactory,
3501
+ `${path}.resourceId must be a non-empty string`,
3502
+ );
3503
+ }
3504
+
3505
+ if (!isNonEmptyString(visual.animationName)) {
3506
+ return invalidFromErrorFactory(
3507
+ errorFactory,
3508
+ `${path}.animationName must be a non-empty string`,
3509
+ );
3510
+ }
3511
+ }
3512
+
3513
+ for (const key of ["width", "height"]) {
3514
+ if (
3515
+ visual[key] !== undefined &&
3516
+ (!isFiniteNumber(visual[key]) || visual[key] <= 0)
3517
+ ) {
3518
+ return invalidFromErrorFactory(
3519
+ errorFactory,
3520
+ `${path}.${key} must be a finite number greater than 0 when provided`,
3521
+ );
3522
+ }
3523
+ }
3524
+
3525
+ for (const key of ["offsetX", "offsetY"]) {
3526
+ if (visual[key] !== undefined && !isFiniteNumber(visual[key])) {
3527
+ return invalidFromErrorFactory(
3528
+ errorFactory,
3529
+ `${path}.${key} must be a finite number when provided`,
3530
+ );
3531
+ }
3532
+ }
3533
+ };
3534
+
3535
+ const validateLayoutTextRevealIndicator = ({
3536
+ indicator,
3537
+ path,
3538
+ errorFactory,
3539
+ }) => {
3540
+ if (!isPlainObject(indicator)) {
3541
+ return invalidFromErrorFactory(
3542
+ errorFactory,
3543
+ `${path} must be an object when provided`,
3544
+ );
3545
+ }
3546
+
3547
+ {
3548
+ const result = validateAllowedKeys({
3549
+ value: indicator,
3550
+ allowedKeys: ["revealing", "complete", "offsetX", "offsetY"],
3551
+ path,
3552
+ errorFactory,
3553
+ });
3554
+ if (result?.valid === false) {
3555
+ return result;
3556
+ }
3557
+ }
3558
+
3559
+ for (const key of ["revealing", "complete"]) {
3560
+ if (indicator[key] !== undefined) {
3561
+ const result = validateLayoutTextRevealIndicatorVisual({
3562
+ visual: indicator[key],
3563
+ path: `${path}.${key}`,
3564
+ errorFactory,
3565
+ });
3566
+ if (result?.valid === false) {
3567
+ return result;
3568
+ }
3569
+ }
3570
+ }
3571
+
3572
+ for (const key of ["offsetX", "offsetY"]) {
3573
+ if (indicator[key] !== undefined && !isFiniteNumber(indicator[key])) {
3574
+ return invalidFromErrorFactory(
3575
+ errorFactory,
3576
+ `${path}.${key} must be a finite number when provided`,
3577
+ );
3578
+ }
3579
+ }
3580
+ };
3581
+
3582
+ const validateLayoutTextContent = ({ content, path, errorFactory }) => {
3583
+ if (!Array.isArray(content)) {
3584
+ return invalidFromErrorFactory(
3585
+ errorFactory,
3586
+ `${path} must be an array when provided`,
3587
+ );
3588
+ }
3589
+
3590
+ for (let index = 0; index < content.length; index += 1) {
3591
+ const item = content[index];
3592
+ const itemPath = `${path}[${index}]`;
3593
+
3594
+ if (!isPlainObject(item)) {
3595
+ return invalidFromErrorFactory(
3596
+ errorFactory,
3597
+ `${itemPath} must be an object`,
3598
+ );
3599
+ }
3600
+
3601
+ {
3602
+ const result = validateAllowedKeys({
3603
+ value: item,
3604
+ allowedKeys: ["text", "reference"],
3605
+ path: itemPath,
3606
+ errorFactory,
3607
+ });
3608
+ if (result?.valid === false) {
3609
+ return result;
3610
+ }
3611
+ }
3612
+
3613
+ const hasText = Object.hasOwn(item, "text");
3614
+ const hasReference = Object.hasOwn(item, "reference");
3615
+
3616
+ if (hasText === hasReference) {
3617
+ return invalidFromErrorFactory(
3618
+ errorFactory,
3619
+ `${itemPath} must include exactly one of text or reference`,
3620
+ );
3621
+ }
3622
+
3623
+ if (hasText) {
3624
+ if (!isString(item.text)) {
3625
+ return invalidFromErrorFactory(
3626
+ errorFactory,
3627
+ `${itemPath}.text must be a string`,
3628
+ );
3629
+ }
3630
+ continue;
3631
+ }
3632
+
3633
+ if (!isPlainObject(item.reference)) {
3634
+ return invalidFromErrorFactory(
3635
+ errorFactory,
3636
+ `${itemPath}.reference must be an object`,
3637
+ );
3638
+ }
3639
+
3640
+ {
3641
+ const result = validateAllowedKeys({
3642
+ value: item.reference,
3643
+ allowedKeys: ["resourceId"],
3644
+ path: `${itemPath}.reference`,
3645
+ errorFactory,
3646
+ });
3647
+ if (result?.valid === false) {
3648
+ return result;
3649
+ }
3650
+ }
3651
+
3652
+ if (!isNonEmptyString(item.reference.resourceId)) {
3653
+ return invalidFromErrorFactory(
3654
+ errorFactory,
3655
+ `${itemPath}.reference.resourceId must be a non-empty string`,
3656
+ );
3657
+ }
3658
+ }
3659
+ };
3660
+
3661
+ const getLayoutTextContentReferenceEntries = (content) => {
3662
+ if (!Array.isArray(content)) {
3663
+ return [];
3664
+ }
3665
+
3666
+ const entries = [];
3667
+ for (let index = 0; index < content.length; index += 1) {
3668
+ const resourceId = content[index]?.reference?.resourceId;
3669
+ if (resourceId !== undefined) {
3670
+ entries.push({
3671
+ index,
3672
+ resourceId,
3673
+ });
3674
+ }
3675
+ }
3676
+ return entries;
3677
+ };
3678
+
3413
3679
  const validateLayoutElementData = ({
3414
3680
  data,
3415
3681
  path,
@@ -3438,9 +3704,11 @@ const validateLayoutElementData = ({
3438
3704
  "fill",
3439
3705
  "border",
3440
3706
  "text",
3707
+ "content",
3441
3708
  "textStyle",
3442
3709
  "displaySpeed",
3443
3710
  "revealEffect",
3711
+ "indicator",
3444
3712
  "resourceId",
3445
3713
  "animationName",
3446
3714
  "imageId",
@@ -3619,6 +3887,52 @@ const validateLayoutElementData = ({
3619
3887
  }
3620
3888
  }
3621
3889
 
3890
+ if (data.content !== undefined) {
3891
+ {
3892
+ const result = validateLayoutTextContent({
3893
+ content: data.content,
3894
+ path: `${path}.content`,
3895
+ errorFactory,
3896
+ });
3897
+ if (result?.valid === false) {
3898
+ return result;
3899
+ }
3900
+ }
3901
+
3902
+ if (
3903
+ data.type !== undefined &&
3904
+ !LAYOUT_TEXT_CONTENT_ELEMENT_TYPES.includes(data.type)
3905
+ ) {
3906
+ return invalidFromErrorFactory(
3907
+ errorFactory,
3908
+ `${path}.content can only be provided for text elements`,
3909
+ );
3910
+ }
3911
+ }
3912
+
3913
+ if (data.indicator !== undefined) {
3914
+ {
3915
+ const result = validateLayoutTextRevealIndicator({
3916
+ indicator: data.indicator,
3917
+ path: `${path}.indicator`,
3918
+ errorFactory,
3919
+ });
3920
+ if (result?.valid === false) {
3921
+ return result;
3922
+ }
3923
+ }
3924
+
3925
+ if (
3926
+ data.type !== undefined &&
3927
+ !LAYOUT_TEXT_REVEAL_INDICATOR_ELEMENT_TYPES.includes(data.type)
3928
+ ) {
3929
+ return invalidFromErrorFactory(
3930
+ errorFactory,
3931
+ `${path}.indicator can only be provided for text revealing elements`,
3932
+ );
3933
+ }
3934
+ }
3935
+
3622
3936
  for (const key of [
3623
3937
  "text",
3624
3938
  "resourceId",
@@ -4134,9 +4448,11 @@ const validateLayoutElementItems = ({ items, path, errorFactory }) => {
4134
4448
  "fill",
4135
4449
  "border",
4136
4450
  "text",
4451
+ "content",
4137
4452
  "textStyle",
4138
4453
  "displaySpeed",
4139
4454
  "revealEffect",
4455
+ "indicator",
4140
4456
  "resourceId",
4141
4457
  "animationName",
4142
4458
  "particleId",
@@ -4254,6 +4570,7 @@ const validateCharacterItems = ({ items, path, errorFactory }) => {
4254
4570
  "tagIds",
4255
4571
  "spriteGroups",
4256
4572
  "shortcut",
4573
+ "nameVariableId",
4257
4574
  "fileId",
4258
4575
  "sprites",
4259
4576
  ],
@@ -4326,6 +4643,16 @@ const validateCharacterItems = ({ items, path, errorFactory }) => {
4326
4643
  );
4327
4644
  }
4328
4645
 
4646
+ if (
4647
+ item.nameVariableId !== undefined &&
4648
+ !isNonEmptyString(item.nameVariableId)
4649
+ ) {
4650
+ return invalidFromErrorFactory(
4651
+ errorFactory,
4652
+ `${itemPath}.nameVariableId must be a non-empty string when provided`,
4653
+ );
4654
+ }
4655
+
4329
4656
  if (item.fileId !== undefined && !isNonEmptyString(item.fileId)) {
4330
4657
  return invalidFromErrorFactory(
4331
4658
  errorFactory,
@@ -4781,6 +5108,13 @@ const applyCharacterUpdate = ({ currentItem, data }) => {
4781
5108
  }
4782
5109
  }
4783
5110
 
5111
+ if (
5112
+ Object.hasOwn(data, "nameVariableId") &&
5113
+ (data.nameVariableId === undefined || data.nameVariableId === "")
5114
+ ) {
5115
+ delete nextItem.nameVariableId;
5116
+ }
5117
+
4784
5118
  return nextItem;
4785
5119
  };
4786
5120
 
@@ -6495,6 +6829,28 @@ const validateTransformPreviewImageReferences = ({
6495
6829
  return VALID_RESULT;
6496
6830
  };
6497
6831
 
6832
+ const validateStringVariableReference = ({
6833
+ state,
6834
+ variableId,
6835
+ path,
6836
+ details = {},
6837
+ errorFactory = createPreconditionValidationError,
6838
+ }) => {
6839
+ if (variableId === undefined || variableId === null || variableId === "") {
6840
+ return VALID_RESULT;
6841
+ }
6842
+
6843
+ if (!isStringVariableReferenceTarget(state, variableId)) {
6844
+ return invalidFromErrorFactory(
6845
+ errorFactory,
6846
+ `${path} must reference an existing string variable`,
6847
+ details,
6848
+ );
6849
+ }
6850
+
6851
+ return VALID_RESULT;
6852
+ };
6853
+
6498
6854
  export const assertInvariants = ({ state }) => {
6499
6855
  if (!isPlainObject(state)) {
6500
6856
  return invalidInvariant("state must be an object");
@@ -6909,6 +7265,19 @@ export const assertInvariants = ({ state }) => {
6909
7265
  }
6910
7266
  }
6911
7267
 
7268
+ if (character.nameVariableId !== undefined) {
7269
+ const result = validateStringVariableReference({
7270
+ state,
7271
+ variableId: character.nameVariableId,
7272
+ path: "character.nameVariableId",
7273
+ details: { characterId, variableId: character.nameVariableId },
7274
+ errorFactory: createInvariantValidationError,
7275
+ });
7276
+ if (!result.valid) {
7277
+ return result;
7278
+ }
7279
+ }
7280
+
6912
7281
  {
6913
7282
  const result = validateTagIdsAgainstScope({
6914
7283
  state,
@@ -7358,15 +7727,16 @@ export const assertInvariants = ({ state }) => {
7358
7727
  elementId,
7359
7728
  targetId,
7360
7729
  animationName,
7730
+ fieldPrefix = "",
7361
7731
  }) => {
7362
7732
  const spritesheet = state.spritesheets?.items?.[targetId];
7363
7733
  if (!isPlainObject(spritesheet) || spritesheet.type === "folder") {
7364
7734
  return invalidInvariant(
7365
- `${ownerLabel} element resourceId must reference an existing non-folder spritesheet`,
7735
+ `${ownerLabel} element ${fieldPrefix}resourceId must reference an existing non-folder spritesheet`,
7366
7736
  {
7367
7737
  [ownerIdField]: ownerId,
7368
7738
  elementId,
7369
- field: "resourceId",
7739
+ field: `${fieldPrefix}resourceId`,
7370
7740
  targetId,
7371
7741
  },
7372
7742
  );
@@ -7377,11 +7747,11 @@ export const assertInvariants = ({ state }) => {
7377
7747
  !isPlainObject(spritesheet.animations?.[animationName])
7378
7748
  ) {
7379
7749
  return invalidInvariant(
7380
- `${ownerLabel} element animationName must reference an existing spritesheet animation`,
7750
+ `${ownerLabel} element ${fieldPrefix}animationName must reference an existing spritesheet animation`,
7381
7751
  {
7382
7752
  [ownerIdField]: ownerId,
7383
7753
  elementId,
7384
- field: "animationName",
7754
+ field: `${fieldPrefix}animationName`,
7385
7755
  targetId: animationName,
7386
7756
  },
7387
7757
  );
@@ -7480,6 +7850,43 @@ export const assertInvariants = ({ state }) => {
7480
7850
  }
7481
7851
  }
7482
7852
 
7853
+ for (const stateName of ["revealing", "complete"]) {
7854
+ const visual = element.indicator?.[stateName];
7855
+ if (
7856
+ visual?.kind === "spritesheet" ||
7857
+ visual?.resourceId !== undefined ||
7858
+ visual?.animationName !== undefined
7859
+ ) {
7860
+ const result = assertSpritesheetAnimationReference({
7861
+ ownerIdField,
7862
+ ownerId,
7863
+ ownerLabel,
7864
+ elementId,
7865
+ targetId: visual.resourceId,
7866
+ animationName: visual.animationName,
7867
+ fieldPrefix: `indicator.${stateName}.`,
7868
+ });
7869
+ if (!result.valid) {
7870
+ return result;
7871
+ }
7872
+ continue;
7873
+ }
7874
+
7875
+ if (visual?.imageId !== undefined) {
7876
+ const result = assertImageReference({
7877
+ ownerIdField,
7878
+ ownerId,
7879
+ ownerLabel,
7880
+ elementId,
7881
+ field: `indicator.${stateName}.imageId`,
7882
+ targetId: visual.imageId,
7883
+ });
7884
+ if (!result.valid) {
7885
+ return result;
7886
+ }
7887
+ }
7888
+ }
7889
+
7483
7890
  for (const field of ["hoverSoundId", "clickSoundId"]) {
7484
7891
  if (element[field] !== undefined) {
7485
7892
  const result = assertSoundReference({
@@ -7577,6 +7984,23 @@ export const assertInvariants = ({ state }) => {
7577
7984
  }
7578
7985
  }
7579
7986
 
7987
+ for (const {
7988
+ index,
7989
+ resourceId,
7990
+ } of getLayoutTextContentReferenceEntries(element.content)) {
7991
+ if (!isVariableReferenceTarget(state, resourceId)) {
7992
+ return invalidInvariant(
7993
+ `${ownerLabel} element content.${index}.reference.resourceId must reference an existing non-folder variable`,
7994
+ {
7995
+ [ownerIdField]: ownerId,
7996
+ elementId,
7997
+ field: `content.${index}.reference.resourceId`,
7998
+ targetId: resourceId,
7999
+ },
8000
+ );
8001
+ }
8002
+ }
8003
+
7580
8004
  if (element.variableId !== undefined) {
7581
8005
  const result = assertVariableReference({
7582
8006
  ownerIdField,
@@ -10639,6 +11063,7 @@ const validateCharacterCreateData = ({ data, errorFactory }) => {
10639
11063
  "tagIds",
10640
11064
  "spriteGroups",
10641
11065
  "shortcut",
11066
+ "nameVariableId",
10642
11067
  "fileId",
10643
11068
  "sprites",
10644
11069
  ],
@@ -10694,6 +11119,16 @@ const validateCharacterCreateData = ({ data, errorFactory }) => {
10694
11119
  );
10695
11120
  }
10696
11121
 
11122
+ if (
11123
+ data.nameVariableId !== undefined &&
11124
+ !isNonEmptyString(data.nameVariableId)
11125
+ ) {
11126
+ return invalidFromErrorFactory(
11127
+ errorFactory,
11128
+ "payload.data.nameVariableId must be a non-empty string when provided",
11129
+ );
11130
+ }
11131
+
10697
11132
  if (data.fileId !== undefined && !isNonEmptyString(data.fileId)) {
10698
11133
  return invalidFromErrorFactory(
10699
11134
  errorFactory,
@@ -10735,6 +11170,7 @@ const validateCharacterUpdateData = ({ data, errorFactory }) => {
10735
11170
  "tagIds",
10736
11171
  "spriteGroups",
10737
11172
  "shortcut",
11173
+ "nameVariableId",
10738
11174
  "fileId",
10739
11175
  ],
10740
11176
  path: "payload.data",
@@ -10773,6 +11209,17 @@ const validateCharacterUpdateData = ({ data, errorFactory }) => {
10773
11209
  );
10774
11210
  }
10775
11211
 
11212
+ if (
11213
+ data.nameVariableId !== undefined &&
11214
+ data.nameVariableId !== "" &&
11215
+ !isNonEmptyString(data.nameVariableId)
11216
+ ) {
11217
+ return invalidFromErrorFactory(
11218
+ errorFactory,
11219
+ "payload.data.nameVariableId must be a non-empty string when provided",
11220
+ );
11221
+ }
11222
+
10776
11223
  {
10777
11224
  const result = validateOptionalUniqueIdArray({
10778
11225
  value: data.tagIds,
@@ -11298,6 +11745,36 @@ const validateVisualElementReferenceTargets = ({
11298
11745
  );
11299
11746
  }
11300
11747
 
11748
+ if (
11749
+ data.indicator !== undefined &&
11750
+ !LAYOUT_TEXT_REVEAL_INDICATOR_ELEMENT_TYPES.includes(data.type)
11751
+ ) {
11752
+ return invalidFromErrorFactory(
11753
+ errorFactory,
11754
+ `${ownerLabel} element indicator can only be provided for text revealing elements`,
11755
+ {
11756
+ [ownerIdField]: ownerId,
11757
+ elementId,
11758
+ field: "indicator",
11759
+ },
11760
+ );
11761
+ }
11762
+
11763
+ if (
11764
+ data.content !== undefined &&
11765
+ !LAYOUT_TEXT_CONTENT_ELEMENT_TYPES.includes(data.type)
11766
+ ) {
11767
+ return invalidFromErrorFactory(
11768
+ errorFactory,
11769
+ `${ownerLabel} element content can only be provided for text elements`,
11770
+ {
11771
+ [ownerIdField]: ownerId,
11772
+ elementId,
11773
+ field: "content",
11774
+ },
11775
+ );
11776
+ }
11777
+
11301
11778
  if (
11302
11779
  data.type === "spritesheet-animation" ||
11303
11780
  data.resourceId !== undefined ||
@@ -11398,6 +11875,66 @@ const validateVisualElementReferenceTargets = ({
11398
11875
  }
11399
11876
  }
11400
11877
 
11878
+ for (const stateName of ["revealing", "complete"]) {
11879
+ const visual = data.indicator?.[stateName];
11880
+ if (
11881
+ visual?.kind === "spritesheet" ||
11882
+ visual?.resourceId !== undefined ||
11883
+ visual?.animationName !== undefined
11884
+ ) {
11885
+ const spritesheet = state.spritesheets?.items?.[visual.resourceId];
11886
+ if (!isPlainObject(spritesheet) || spritesheet.type === "folder") {
11887
+ return invalidFromErrorFactory(
11888
+ errorFactory,
11889
+ `${ownerLabel} element indicator.${stateName}.resourceId must reference an existing non-folder spritesheet`,
11890
+ {
11891
+ [ownerIdField]: ownerId,
11892
+ elementId,
11893
+ field: `indicator.${stateName}.resourceId`,
11894
+ targetId: visual.resourceId,
11895
+ },
11896
+ );
11897
+ }
11898
+
11899
+ if (
11900
+ !isNonEmptyString(visual.animationName) ||
11901
+ !isPlainObject(spritesheet.animations?.[visual.animationName])
11902
+ ) {
11903
+ return invalidFromErrorFactory(
11904
+ errorFactory,
11905
+ `${ownerLabel} element indicator.${stateName}.animationName must reference an existing spritesheet animation`,
11906
+ {
11907
+ [ownerIdField]: ownerId,
11908
+ elementId,
11909
+ field: `indicator.${stateName}.animationName`,
11910
+ targetId: visual.animationName,
11911
+ },
11912
+ );
11913
+ }
11914
+
11915
+ continue;
11916
+ }
11917
+
11918
+ const imageId = visual?.imageId;
11919
+ if (imageId === undefined) {
11920
+ continue;
11921
+ }
11922
+
11923
+ const image = state.images.items[imageId];
11924
+ if (!isPlainObject(image) || image.type === "folder") {
11925
+ return invalidFromErrorFactory(
11926
+ errorFactory,
11927
+ `${ownerLabel} element indicator.${stateName}.imageId must reference an existing non-folder image`,
11928
+ {
11929
+ [ownerIdField]: ownerId,
11930
+ elementId,
11931
+ field: `indicator.${stateName}.imageId`,
11932
+ targetId: imageId,
11933
+ },
11934
+ );
11935
+ }
11936
+ }
11937
+
11401
11938
  if (data.hoverSoundId !== undefined) {
11402
11939
  const sound = state.sounds.items[data.hoverSoundId];
11403
11940
  if (!isPlainObject(sound) || sound.type === "folder") {
@@ -11625,6 +12162,23 @@ const validateVisualElementReferenceTargets = ({
11625
12162
  }
11626
12163
  }
11627
12164
 
12165
+ for (const { index, resourceId } of getLayoutTextContentReferenceEntries(
12166
+ data.content,
12167
+ )) {
12168
+ if (!isVariableReferenceTarget(state, resourceId)) {
12169
+ return invalidFromErrorFactory(
12170
+ errorFactory,
12171
+ `${ownerLabel} element content.${index}.reference.resourceId must reference an existing non-folder variable`,
12172
+ {
12173
+ [ownerIdField]: ownerId,
12174
+ elementId,
12175
+ field: `content.${index}.reference.resourceId`,
12176
+ targetId: resourceId,
12177
+ },
12178
+ );
12179
+ }
12180
+ }
12181
+
11628
12182
  if (data.variableId !== undefined) {
11629
12183
  if (!isVariableReferenceTarget(state, data.variableId)) {
11630
12184
  return invalidFromErrorFactory(
@@ -17072,6 +17626,10 @@ const COMMAND_DEFINITIONS = [
17072
17626
  item.fileId = payload.data.fileId;
17073
17627
  }
17074
17628
 
17629
+ if (payload.data.nameVariableId !== undefined) {
17630
+ item.nameVariableId = payload.data.nameVariableId;
17631
+ }
17632
+
17075
17633
  assignOptionalTagIds({
17076
17634
  target: item,
17077
17635
  tagIds: payload.data.tagIds,
@@ -17112,6 +17670,21 @@ const COMMAND_DEFINITIONS = [
17112
17670
  }
17113
17671
  }
17114
17672
 
17673
+ {
17674
+ const result = validateStringVariableReference({
17675
+ state,
17676
+ variableId: payload.data.nameVariableId,
17677
+ path: "payload.data.nameVariableId",
17678
+ details: {
17679
+ characterId: payload.characterId,
17680
+ variableId: payload.data.nameVariableId,
17681
+ },
17682
+ });
17683
+ if (!result.valid) {
17684
+ return result;
17685
+ }
17686
+ }
17687
+
17115
17688
  {
17116
17689
  const result = validateTagIdsAgainstScope({
17117
17690
  state,
@@ -17217,6 +17790,21 @@ const COMMAND_DEFINITIONS = [
17217
17790
  return result;
17218
17791
  }
17219
17792
 
17793
+ if (Object.hasOwn(payload.data, "nameVariableId")) {
17794
+ const variableResult = validateStringVariableReference({
17795
+ state,
17796
+ variableId: payload.data.nameVariableId,
17797
+ path: "payload.data.nameVariableId",
17798
+ details: {
17799
+ characterId: payload.characterId,
17800
+ variableId: payload.data.nameVariableId,
17801
+ },
17802
+ });
17803
+ if (!variableResult.valid) {
17804
+ return variableResult;
17805
+ }
17806
+ }
17807
+
17220
17808
  {
17221
17809
  const tagResult = validateTagIdsAgainstScope({
17222
17810
  state,
@@ -17617,6 +18205,16 @@ const COMMAND_DEFINITIONS = [
17617
18205
  "payload.parentId must reference a folder sprite item",
17618
18206
  );
17619
18207
  }
18208
+
18209
+ const parentNode = findTreeNode({
18210
+ nodes: collection.tree,
18211
+ nodeId: parentId,
18212
+ });
18213
+ if (!parentNode) {
18214
+ return invalidPrecondition(
18215
+ "payload.parentId must reference a folder sprite item in the tree",
18216
+ );
18217
+ }
17620
18218
  }
17621
18219
 
17622
18220
  if (payload.positionTargetId !== undefined) {
@@ -17626,6 +18224,16 @@ const COMMAND_DEFINITIONS = [
17626
18224
  );
17627
18225
  }
17628
18226
 
18227
+ const targetNode = findTreeNode({
18228
+ nodes: collection.tree,
18229
+ nodeId: payload.positionTargetId,
18230
+ });
18231
+ if (!targetNode) {
18232
+ return invalidPrecondition(
18233
+ "payload.positionTargetId must reference an existing sprite item in the tree",
18234
+ );
18235
+ }
18236
+
17629
18237
  const targetParentId = getNodeParentId({
17630
18238
  tree: collection.tree,
17631
18239
  nodeId: payload.positionTargetId,
@@ -17957,6 +18565,11 @@ const COMMAND_DEFINITIONS = [
17957
18565
  nodes: collection.tree,
17958
18566
  nodeId: payload.spriteId,
17959
18567
  });
18568
+ if (!currentNode) {
18569
+ return invalidPrecondition(
18570
+ "payload.spriteId must reference an existing sprite item in the tree",
18571
+ );
18572
+ }
17960
18573
 
17961
18574
  if (payload.parentId !== undefined && payload.parentId !== null) {
17962
18575
  const parentItem = collection.items[payload.parentId];
@@ -17966,6 +18579,16 @@ const COMMAND_DEFINITIONS = [
17966
18579
  );
17967
18580
  }
17968
18581
 
18582
+ const parentNode = findTreeNode({
18583
+ nodes: collection.tree,
18584
+ nodeId: payload.parentId,
18585
+ });
18586
+ if (!parentNode) {
18587
+ return invalidPrecondition(
18588
+ "payload.parentId must reference a folder sprite item in the tree",
18589
+ );
18590
+ }
18591
+
17969
18592
  const descendantIds = new Set(
17970
18593
  collectTreeDescendantIds({
17971
18594
  node: currentNode,
@@ -17992,6 +18615,16 @@ const COMMAND_DEFINITIONS = [
17992
18615
  );
17993
18616
  }
17994
18617
 
18618
+ const targetNode = findTreeNode({
18619
+ nodes: collection.tree,
18620
+ nodeId: payload.positionTargetId,
18621
+ });
18622
+ if (!targetNode) {
18623
+ return invalidPrecondition(
18624
+ "payload.positionTargetId must reference an existing sprite item in the tree",
18625
+ );
18626
+ }
18627
+
17995
18628
  const targetParentId = getNodeParentId({
17996
18629
  tree: collection.tree,
17997
18630
  nodeId: payload.positionTargetId,