@makefully/platypus 4.1.8 → 4.1.10

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": "@makefully/platypus",
3
- "version": "4.1.8",
3
+ "version": "4.1.10",
4
4
  "description": "2D tile-based HTML5 game engine with a component-based entity model",
5
5
  "license": "MIT",
6
6
  "contributors": [
@@ -26,6 +26,9 @@ const
26
26
  'src', path,
27
27
  'data', asset.data ?? null
28
28
  );
29
+ },
30
+ isRegisteredWithPixi = function (alias, src) {
31
+ return Assets.resolver.hasKey(alias) || (src && src !== alias && Assets.resolver.hasKey(src));
29
32
  };
30
33
 
31
34
  export default class AssetManager {
@@ -71,7 +74,18 @@ export default class AssetManager {
71
74
  * @return {Object} Returns the asset if defined.
72
75
  */
73
76
  get (alias) {
74
- return this.assets.get(alias);
77
+ if (this.assets.has(alias)) {
78
+ return this.assets.get(alias);
79
+ }
80
+
81
+ const
82
+ fileId = this.getFileId(alias);
83
+
84
+ if (fileId !== alias && this.assets.has(fileId)) {
85
+ return this.assets.get(fileId);
86
+ }
87
+
88
+ return undefined;
75
89
  }
76
90
 
77
91
  /**
@@ -131,6 +145,7 @@ export default class AssetManager {
131
145
  const
132
146
  counts = this.counts,
133
147
  needsLoading = arrayCache.setUp(),
148
+ needsRelink = arrayCache.setUp(),
134
149
  adds = Data.setUp();
135
150
 
136
151
  if (platypus.game?.options?.images ?? platypus.game?.options?.audio) {
@@ -154,13 +169,39 @@ export default class AssetManager {
154
169
  adds[alias] += 1;
155
170
  } else {
156
171
  adds[alias] = 1;
172
+ if (isRegisteredWithPixi(alias, item.src)) {
173
+ needsRelink.push(item);
174
+ } else {
175
+ needsLoading.push(item);
176
+ }
177
+ }
178
+ }
179
+
180
+ for (let i = needsRelink.length - 1; i >= 0; i--) {
181
+ const
182
+ item = needsRelink[i],
183
+ alias = item.alias[0],
184
+ asset = Assets.get(alias) ?? Assets.get(item.src);
185
+
186
+ if (asset) {
187
+ this.set(alias, asset, adds[alias]);
188
+ needsRelink.splice(i, 1);
189
+ } else {
157
190
  needsLoading.push(item);
191
+ needsRelink.splice(i, 1);
158
192
  }
159
193
  }
160
194
 
161
195
  if (needsLoading.length) {
162
196
  // Do this first to pass `data` property if needed
163
- needsLoading.forEach((asset) => Assets.add(asset));
197
+ needsLoading.forEach((asset) => {
198
+ const
199
+ alias = asset.alias[0];
200
+
201
+ if (!isRegisteredWithPixi(alias, asset.src)) {
202
+ Assets.add(asset);
203
+ }
204
+ });
164
205
 
165
206
  const
166
207
  aliases = needsLoading.map((asset) => asset.alias[0]),
@@ -188,6 +229,7 @@ export default class AssetManager {
188
229
  }
189
230
 
190
231
  arrayCache.recycle(needsLoading);
232
+ arrayCache.recycle(needsRelink);
191
233
  }
192
234
 
193
235
  /**
package/src/VOPlayer.js CHANGED
@@ -662,7 +662,7 @@ class VOPlayer extends Messenger {
662
662
  * @public
663
663
  */
664
664
  unloadSound (sound) {
665
- this.assetCache.delete(sound);
665
+ this.assetCache.delete(this.assetCache.getFileId(sound));
666
666
  }
667
667
 
668
668
  /**
@@ -1,5 +1,6 @@
1
1
  /* global atob, platypus */
2
2
  import {arrayCache, greenSlice, greenSplice, union} from '../utils/array.js';
3
+ import {cloneDefinition} from '../utils/clone.js';
3
4
  import createComponentClass from '../factory.js';
4
5
  import {inflate} from 'pako';
5
6
  import TiledLoader from './TiledLoader.js';
@@ -31,6 +32,21 @@ const
31
32
  }
32
33
  return layer;
33
34
  },
35
+ cloneLevelDefinition = cloneDefinition,
36
+ copySegmentProperty = function (target, source, key) {
37
+ if (target[key]) {
38
+ return;
39
+ }
40
+
41
+ const
42
+ value = source[key];
43
+
44
+ if (value != null && typeof value === 'object') {
45
+ target[key] = cloneDefinition(value);
46
+ } else {
47
+ target[key] = value;
48
+ }
49
+ },
34
50
  mergeData = function (levelData, levelMergeAxisLength, segmentData, segmentMergeAxisLength, nonMergeAxisLength, mergeAxis) {
35
51
  const
36
52
  combined = greenSlice(levelData);
@@ -54,9 +70,7 @@ const
54
70
 
55
71
  for (let i = 0; i < obj2s.length; i++) {
56
72
  const
57
- obj = {
58
- ...obj2s[i]
59
- };
73
+ obj = cloneLevelDefinition(obj2s[i]);
60
74
 
61
75
  if (mergeAxis === 'horizontal') {
62
76
  obj.x += mergeAxisLength;
@@ -68,6 +82,8 @@ const
68
82
  return list;
69
83
  },
70
84
  mergeSegment = function (level, segment, mergeAxis) {
85
+ segment = cloneLevelDefinition(segment);
86
+
71
87
  if (!level.tilewidth && !level.tileheight) {
72
88
  //set level tile size data if it's not already set.
73
89
  level.tilewidth = segment.tilewidth;
@@ -96,9 +112,7 @@ const
96
112
  decodeLayer(segment.layers[i]);
97
113
 
98
114
  const
99
- layer = level.layers[i] = {
100
- ...segment.layers[i]
101
- };
115
+ layer = level.layers[i] = cloneLevelDefinition(segment.layers[i]);
102
116
 
103
117
  // If we're adding objects, make sure that they're offset correctly.
104
118
  if (layer.objects) {
@@ -149,9 +163,7 @@ const
149
163
  const
150
164
  key = keys[i];
151
165
 
152
- if (!level[key]) {
153
- level[key] = segment[key];
154
- }
166
+ copySegmentProperty(level, segment, key);
155
167
  }
156
168
  }
157
169
  },
@@ -200,6 +212,8 @@ const
200
212
  return level;
201
213
  },
202
214
  mirrorSegment = function (segment) {
215
+ segment = cloneLevelDefinition(segment);
216
+
203
217
  const
204
218
  newSegment = {
205
219
  layers: []
@@ -211,9 +225,7 @@ const
211
225
 
212
226
  const
213
227
  fromLayer = segment.layers[i],
214
- toLayer = newSegment.layers[i] = {
215
- ...fromLayer
216
- };
228
+ toLayer = newSegment.layers[i] = cloneLevelDefinition(fromLayer);
217
229
 
218
230
  if (fromLayer.data) {
219
231
  const
@@ -237,9 +249,7 @@ const
237
249
  for (let j = 0; j < fromObjects.length; j++) {
238
250
  const
239
251
  fromObject = fromObjects[j],
240
- toObject = toObjects[j] = {
241
- ...fromObject
242
- };
252
+ toObject = toObjects[j] = cloneLevelDefinition(fromObject);
243
253
 
244
254
  toObject.x = width - fromObject.x - (fromObject.width || 0); // subtract object width since its top-left corner is the origin.
245
255
  if (fromObject.rotation) {
@@ -265,9 +275,7 @@ const
265
275
  const
266
276
  key = keys[i];
267
277
 
268
- if (!newSegment[key]) {
269
- newSegment[key] = segment[key];
270
- }
278
+ copySegmentProperty(newSegment, segment, key);
271
279
  }
272
280
  }
273
281
 
@@ -351,6 +359,10 @@ export default createComponentClass(/** @lends platypus.components.LevelBuilder.
351
359
 
352
360
  events: {
353
361
  "layer-loaded": function (data) {
362
+ if (data?.level && typeof data.level === 'object') {
363
+ return;
364
+ }
365
+
354
366
  const
355
367
  piecesToCopy = data?.levelPieces ?? this.levelPieces;
356
368
 
@@ -417,7 +429,10 @@ export default createComponentClass(/** @lends platypus.components.LevelBuilder.
417
429
  * @param data.persistentData.levelPieces {Object} An object of key/value pairs listing the pieces that map to an id in the level template.
418
430
  * @param data.persistentData.useUniques {Boolean} If true, no single map piece is used twice in the creation of the combined map.
419
431
  */
420
- this.owner.triggerEvent('created-level', this.levelMessage);
432
+ this.owner.triggerEvent('created-level', {
433
+ level: cloneLevelDefinition(this.levelMessage.level),
434
+ persistentData: this.levelMessage.persistentData
435
+ });
421
436
 
422
437
  /**
423
438
  * Dispatched when the scene has loaded and the level has been composited so TileLoader can begin loading the level.
@@ -462,6 +477,9 @@ export default createComponentClass(/** @lends platypus.components.LevelBuilder.
462
477
  throw new Error(`Level Builder: There are no level pieces of type: ${type}`);
463
478
  }
464
479
  },
480
+ cloneLevel: function (level) {
481
+ return cloneLevelDefinition(level);
482
+ },
465
483
  destroy: function () {
466
484
  this.levelMessage.level = null;
467
485
  this.levelMessage.persistentData = null;
@@ -98,8 +98,18 @@ export default createComponentClass(/** @lends platypus.components.RenderSprite.
98
98
  initialize () {
99
99
  const
100
100
  {image, images, localRotation, localScaleX, localScaleY, offsetX, offsetY, offsetZ, options, owner, spriteType} = this,
101
+ resolveTexture = (path) => {
102
+ const
103
+ texture = platypus.assetCache.get(path);
104
+
105
+ if (!texture) {
106
+ platypus.debug.warn(`Render: Image is not a loaded asset: ${path}`);
107
+ }
108
+
109
+ return texture;
110
+ },
101
111
  sprite = this.sprite = new spriteTypes[spriteType === 'Sprite' && images && !images ? 'AnimatedSprite' : spriteType]({
102
- ...(image ? {texture: platypus.assetCache.get(image)} : {textures: images.map((image) => platypus.assetCache.get(image))}),
112
+ ...(image ? {texture: resolveTexture(image)} : {textures: images.map((path) => resolveTexture(path))}),
103
113
  ...options ?? {}
104
114
  }),
105
115
  matchEntity = this.matchEntity ? {
@@ -437,14 +437,7 @@ export default createComponentClass(/** @lends platypus.components.RenderTiles.p
437
437
 
438
438
  if (map) {
439
439
  this.updateTile(tile, map[z || 0], x, y);
440
- // Only flag a re-render if the changed tile is within the currently
441
- // cached region. Changes outside the cache will be drawn correctly
442
- // the next time the camera pans to include them.
443
- if (!this.cache.empty &&
444
- x >= this.cache.left && x <= this.cache.right &&
445
- y >= this.cache.top && y <= this.cache.bottom) {
446
- this.updateCache = true;
447
- }
440
+ this.updateCache = true;
448
441
  }
449
442
  },
450
443
 
@@ -458,70 +451,13 @@ export default createComponentClass(/** @lends platypus.components.RenderTiles.p
458
451
  },
459
452
 
460
453
  /**
461
- * Forces an immediate full re-render centered on the current camera
462
- * position. Call this after bulk tile changes to ensure the display
463
- * is up to date without requiring a camera pan.
464
- *
465
- * For the panning cache, we invalidate cachePixels and re-run the full
466
- * camera-update recache logic using the last known camera, which correctly
467
- * handles texture swapping, tile population, and sprite positioning.
454
+ * Forces an immediate full re-render of all cached tile textures.
455
+ * Call this after bulk tile changes to ensure the display is up to date
456
+ * without requiring a camera pan.
468
457
  */
469
458
  "force-render": function () {
470
- if (this.cacheAll) {
471
- // Single/full cache — re-render entire layer.
472
- this.populateTiles(this.cache, null);
473
- this.renderCache(this.cache, this.cacheTexture, this.mapContainer, this.mapContainerWrapper);
474
- } else if (this.cacheTexture) {
475
- // Panning cache — invalidate cachePixels so the recache branch
476
- // runs, then execute the full camera-update recache inline.
477
- const
478
- {viewport: vp} = this.owner.parent.worldCamera,
479
- {cache, cachePixels} = this,
480
- tempC = tempCache,
481
- ctw = this.cacheTilesWidth - 1,
482
- cth = this.cacheTilesHeight - 1,
483
- ctw2 = ctw / 2,
484
- cth2 = cth / 2;
485
-
486
- this.convertCamera(vp);
487
-
488
- // Force the recache branch to run regardless of camera position.
489
- cachePixels.empty = true;
490
-
491
- tempC.setAll(
492
- Math.round((this.laxCam.x - this.left) / this.tileWidth - ctw2) + ctw2,
493
- Math.round((this.laxCam.y - this.top) / this.tileHeight - cth2) + cth2,
494
- ctw, cth
495
- );
496
- if (tempC.left < 0) {
497
- tempC.moveX(tempC.halfWidth);
498
- } else if (tempC.right > this.tilesWidth - 1) {
499
- tempC.moveX(this.tilesWidth - 1 - tempC.halfWidth);
500
- }
501
- if (tempC.top < 0) {
502
- tempC.moveY(tempC.halfHeight);
503
- } else if (tempC.bottom > this.tilesHeight - 1) {
504
- tempC.moveY(this.tilesHeight - 1 - tempC.halfHeight);
505
- }
506
-
507
- this.tilesSpriteCache.texture = this.cacheTexture;
508
- this.cacheTexture = this.cacheTexture.alternate;
509
- this.tilesSprite.texture = this.cacheTexture;
510
-
511
- this.populateTiles(tempC, cache);
512
- this.renderCache(tempC, this.cacheTexture, this.mapContainer, this.mapContainerWrapper, this.tilesSpriteCache);
513
- cache.set(tempC);
514
-
515
- cachePixels.setAll(
516
- (cache.x + 0.5) * this.tileWidth + this.left,
517
- (cache.y + 0.5) * this.tileHeight + this.top,
518
- (cache.width + 1) * this.tileWidth,
519
- (cache.height + 1) * this.tileHeight
520
- );
521
-
522
- this.tilesSprite.x = vp.left - this.laxCam.left + cache.left * this.tileWidth + this.left;
523
- this.tilesSprite.y = vp.top - this.laxCam.top + cache.top * this.tileHeight + this.top;
524
- }
459
+ this.refreshAllCaches();
460
+ this.updateCache = false;
525
461
  }
526
462
  },
527
463
 
@@ -571,8 +507,7 @@ export default createComponentClass(/** @lends platypus.components.RenderTiles.p
571
507
  'handle-render': function () {
572
508
  if (this.updateCache) {
573
509
  this.updateCache = false;
574
- this.populateTiles(this.cache, null);
575
- this.renderCache(this.cache, this.cacheTexture, this.mapContainer, this.mapContainerWrapper);
510
+ this.refreshAllCaches();
576
511
  }
577
512
  }
578
513
  });
@@ -632,21 +567,8 @@ export default createComponentClass(/** @lends platypus.components.RenderTiles.p
632
567
  },
633
568
  'handle-render': function () {
634
569
  if (this.updateCache) {
635
- const
636
- {cache, cacheGrid, cacheTilesHeight, cacheTilesWidth, tilesHeight, tilesWidth} = this,
637
- tsh = tilesHeight - 1,
638
- tsw = tilesWidth - 1;
639
-
640
570
  this.updateCache = false;
641
-
642
- for (let x = 0; x < cacheGrid.length; x++) {
643
- for (let y = 0; y < cacheGrid[x].length; y++) {
644
- cache.setBounds(x * cacheTilesWidth, y * cacheTilesHeight, Math.min((x + 1) * cacheTilesWidth, tsw), Math.min((y + 1) * cacheTilesHeight, tsh));
645
-
646
- this.populateTiles(cache, null);
647
- this.renderCacheWithExtrusion(cache, cacheGrid[x][y].texture, this.mapContainer, this.mapContainerWrapper);
648
- }
649
- }
571
+ this.refreshAllCaches();
650
572
  }
651
573
  }
652
574
  });
@@ -699,24 +621,7 @@ export default createComponentClass(/** @lends platypus.components.RenderTiles.p
699
621
  this.updateBufferRegion(laxCam);
700
622
  }
701
623
 
702
- const
703
- ctw = this.cacheTilesWidth - 1,
704
- cth = this.cacheTilesHeight - 1,
705
- ctw2 = ctw / 2,
706
- cth2 = cth / 2;
707
-
708
- //only attempt to draw children that are relevant
709
- tempC.setAll(Math.round((laxCam.x - this.left) / this.tileWidth - ctw2) + ctw2, Math.round((laxCam.y - this.top) / this.tileHeight - cth2) + cth2, ctw, cth);
710
- if (tempC.left < 0) {
711
- tempC.moveX(tempC.halfWidth);
712
- } else if (tempC.right > this.tilesWidth - 1) {
713
- tempC.moveX(this.tilesWidth - 1 - tempC.halfWidth);
714
- }
715
- if (tempC.top < 0) {
716
- tempC.moveY(tempC.halfHeight);
717
- } else if (tempC.bottom > this.tilesHeight - 1) {
718
- tempC.moveY(this.tilesHeight - 1 - tempC.halfHeight);
719
- }
624
+ this.alignCacheToCamera(tempC, vp);
720
625
 
721
626
  this.tilesSpriteCache.texture = this.cacheTexture;
722
627
  this.cacheTexture = this.cacheTexture.alternate;
@@ -734,17 +639,9 @@ export default createComponentClass(/** @lends platypus.components.RenderTiles.p
734
639
  this.tilesSprite.y = vp.top - laxCam.top + cache.top * this.tileHeight + this.top;
735
640
  },
736
641
  'handle-render': function () {
737
- if (this.updateCache && !this.cache.empty) {
642
+ if (this.updateCache) {
738
643
  this.updateCache = false;
739
- this.populateTiles(this.cache, null);
740
- this.renderCacheClear(this.cache, this.cacheTexture, this.mapContainer, this.mapContainerWrapper);
741
- this.tilesSpriteCache.texture = this.cacheTexture;
742
- this.cachePixels.setAll(
743
- (this.cache.x + 0.5) * this.tileWidth + this.left,
744
- (this.cache.y + 0.5) * this.tileHeight + this.top,
745
- (this.cache.width + 1) * this.tileWidth,
746
- (this.cache.height + 1) * this.tileHeight
747
- );
644
+ this.refreshAllCaches();
748
645
  }
749
646
  }
750
647
  });
@@ -782,24 +679,7 @@ export default createComponentClass(/** @lends platypus.components.RenderTiles.p
782
679
  this.updateBufferRegion(laxCam);
783
680
  }
784
681
 
785
- const
786
- ctw = this.cacheTilesWidth - 1,
787
- cth = this.cacheTilesHeight - 1,
788
- ctw2 = ctw / 2,
789
- cth2 = cth / 2;
790
-
791
- //only attempt to draw children that are relevant
792
- tempC.setAll(Math.round((laxCam.x - this.left) / this.tileWidth - ctw2) + ctw2, Math.round((laxCam.y - this.top) / this.tileHeight - cth2) + cth2, ctw, cth);
793
- if (tempC.left < 0) {
794
- tempC.moveX(tempC.halfWidth);
795
- } else if (tempC.right > this.tilesWidth - 1) {
796
- tempC.moveX(this.tilesWidth - 1 - tempC.halfWidth);
797
- }
798
- if (tempC.top < 0) {
799
- tempC.moveY(tempC.halfHeight);
800
- } else if (tempC.bottom > this.tilesHeight - 1) {
801
- tempC.moveY(this.tilesHeight - 1 - tempC.halfHeight);
802
- }
682
+ this.alignCacheToCamera(tempC, vp);
803
683
 
804
684
  this.populateTiles(tempC, null);
805
685
  cache.set(tempC);
@@ -1079,7 +959,151 @@ export default createComponentClass(/** @lends platypus.components.RenderTiles.p
1079
959
  this.mapContainer.mask = new Graphics().rect(0, 0, this.cacheClipWidth, this.cacheClipHeight).fill(0x000000);
1080
960
  },
1081
961
 
962
+ /**
963
+ * Computes tile bounds for the panning cache centered on the current camera.
964
+ *
965
+ * @param {platypus.AABB} bounds - Receives the computed tile bounds.
966
+ * @param {platypus.AABB} viewport - The camera viewport.
967
+ */
968
+ alignCacheToCamera: function (bounds, viewport) {
969
+ const
970
+ ctw = this.cacheTilesWidth - 1,
971
+ cth = this.cacheTilesHeight - 1,
972
+ ctw2 = ctw / 2,
973
+ cth2 = cth / 2;
974
+
975
+ this.convertCamera(viewport);
976
+
977
+ bounds.setAll(
978
+ Math.round((this.laxCam.x - this.left) / this.tileWidth - ctw2) + ctw2,
979
+ Math.round((this.laxCam.y - this.top) / this.tileHeight - cth2) + cth2,
980
+ ctw, cth
981
+ );
982
+
983
+ if (bounds.left < 0) {
984
+ bounds.moveX(bounds.halfWidth);
985
+ } else if (bounds.right > this.tilesWidth - 1) {
986
+ bounds.moveX(this.tilesWidth - 1 - bounds.halfWidth);
987
+ }
988
+
989
+ if (bounds.top < 0) {
990
+ bounds.moveY(bounds.halfHeight);
991
+ } else if (bounds.bottom > this.tilesHeight - 1) {
992
+ bounds.moveY(this.tilesHeight - 1 - bounds.halfHeight);
993
+ }
994
+ },
995
+
996
+ /**
997
+ * Re-renders every texture in a multi-texture full-map cache grid.
998
+ */
999
+ refreshAllCacheGrid: function () {
1000
+ const
1001
+ {cache, cacheGrid, cacheTilesHeight, cacheTilesWidth, tilesHeight, tilesWidth} = this,
1002
+ tsh = tilesHeight - 1,
1003
+ tsw = tilesWidth - 1;
1004
+
1005
+ for (let x = 0; x < cacheGrid.length; x++) {
1006
+ for (let y = 0; y < cacheGrid[x].length; y++) {
1007
+ cache.setBounds(
1008
+ x * cacheTilesWidth,
1009
+ y * cacheTilesHeight,
1010
+ Math.min((x + 1) * cacheTilesWidth, tsw),
1011
+ Math.min((y + 1) * cacheTilesHeight, tsh)
1012
+ );
1013
+
1014
+ this.populate(cache, null);
1015
+ this.renderCacheWithExtrusion(cache, cacheGrid[x][y].texture, this.mapContainer, this.mapContainerWrapper);
1016
+ }
1017
+ }
1018
+ },
1019
+
1020
+ /**
1021
+ * Re-renders the current panning-cache window in place.
1022
+ * Uses the alternate render texture while drawing so we never render
1023
+ * into the texture that is currently displayed on screen.
1024
+ *
1025
+ * @param {platypus.AABB} [viewport] - Optional camera viewport.
1026
+ */
1027
+ refreshPanningCache: function (viewport) {
1028
+ const
1029
+ {cache, cachePixels, laxCam, left, top, tilesSprite} = this,
1030
+ vp = viewport ?? this.owner.parent.worldCamera?.viewport;
1031
+
1032
+ if (!vp) {
1033
+ return;
1034
+ }
1035
+
1036
+ if (cache.empty) {
1037
+ this.alignCacheToCamera(cache, vp);
1038
+ }
1039
+
1040
+ if (cache.empty) {
1041
+ return;
1042
+ }
1043
+
1044
+ this.detachCacheCopy();
1045
+
1046
+ this.tilesSpriteCache.texture = this.cacheTexture;
1047
+ this.cacheTexture = this.cacheTexture.alternate;
1048
+ tilesSprite.texture = this.cacheTexture;
1049
+
1050
+ this.populate(cache, null);
1051
+ this.renderCacheClear(cache, this.cacheTexture, this.mapContainer, this.mapContainerWrapper);
1052
+
1053
+ this.tilesSpriteCache.texture = this.cacheTexture;
1054
+ cachePixels.setAll(
1055
+ (cache.x + 0.5) * this.tileWidth + left,
1056
+ (cache.y + 0.5) * this.tileHeight + top,
1057
+ (cache.width + 1) * this.tileWidth,
1058
+ (cache.height + 1) * this.tileHeight
1059
+ );
1060
+
1061
+ this.convertCamera(vp);
1062
+ tilesSprite.x = vp.left - laxCam.left + cache.left * this.tileWidth + left;
1063
+ tilesSprite.y = vp.top - laxCam.top + cache.top * this.tileHeight + top;
1064
+ },
1065
+
1066
+ detachCacheCopy: function () {
1067
+ const
1068
+ {mapContainer, tilesSpriteCache} = this;
1069
+
1070
+ if (tilesSpriteCache?.parent === mapContainer) {
1071
+ mapContainer.removeChild(tilesSpriteCache);
1072
+ }
1073
+ },
1074
+
1075
+ /**
1076
+ * Re-renders all cached tile textures for the active caching strategy.
1077
+ *
1078
+ * @param {platypus.AABB} [viewport] - Optional camera viewport for panning caches.
1079
+ */
1080
+ refreshAllCaches: function (viewport) {
1081
+ if (!this.tileCache) {
1082
+ this.cachePixels.empty = true;
1083
+ return;
1084
+ }
1085
+
1086
+ if (this.cacheGrid) {
1087
+ this.refreshAllCacheGrid();
1088
+ return;
1089
+ }
1090
+
1091
+ if (this.cacheAll && this.cacheTexture) {
1092
+ this.populate(this.cache, null);
1093
+ this.render(this.cache, this.cacheTexture, this.mapContainer, this.mapContainerWrapper);
1094
+ return;
1095
+ }
1096
+
1097
+ if (this.cacheTexture) {
1098
+ this.refreshPanningCache(viewport);
1099
+ }
1100
+ },
1101
+
1082
1102
  populateTiles: function (bounds, oldBounds) {
1103
+ // When oldBounds is provided, only tiles outside the previous window are
1104
+ // drawn. That is correct for camera scrolling (reuse pixels from the old
1105
+ // cache texture) but must not be used for in-place tile edits — pass null
1106
+ // to redraw the entire bounds.
1083
1107
  const
1084
1108
  tiles = arrayCache.setUp();
1085
1109
 
@@ -1194,6 +1218,11 @@ export default createComponentClass(/** @lends platypus.components.RenderTiles.p
1194
1218
  container: wrapper,
1195
1219
  target: renderTexture
1196
1220
  });
1221
+
1222
+ if (oldCache?.parent === src) {
1223
+ src.removeChild(oldCache);
1224
+ }
1225
+
1197
1226
  renderTexture.requiresUpdate = true;
1198
1227
  },
1199
1228
 
@@ -1201,6 +1230,8 @@ export default createComponentClass(/** @lends platypus.components.RenderTiles.p
1201
1230
  const
1202
1231
  {renderer} = this;
1203
1232
 
1233
+ this.detachCacheCopy();
1234
+
1204
1235
  src.x = -bounds.left * this.tileWidth;
1205
1236
  src.y = -bounds.top * this.tileHeight;
1206
1237
  renderer.render({
@@ -1,5 +1,6 @@
1
1
  /* global atob, platypus */
2
2
  import {arrayCache, greenSlice, union} from '../utils/array.js';
3
+ import {cloneDefinition} from '../utils/clone.js';
3
4
  import AABB from '../AABB.js';
4
5
  import Data from '../Data.js';
5
6
  import DataMap from '../DataMap.js';
@@ -336,6 +337,7 @@ const
336
337
  },
337
338
  checkLevel = function (levelOrLevelId, ss) {
338
339
  const
340
+ cacheAssetsOnLevel = typeof levelOrLevelId === 'string',
339
341
  addObjectGroupAssets = (assets, objectGroup, tilesets) => {
340
342
  const objects = objectGroup.objects;
341
343
 
@@ -356,7 +358,7 @@ const
356
358
  level.tilesets = importTilesetData(level.tilesets);
357
359
  }
358
360
 
359
- if (level.assets) { // Property added by a previous parse (so that this algorithm isn't run on the same level multiple times)
361
+ if (cacheAssetsOnLevel && level.assets) { // Property added by a previous parse (so that this algorithm isn't run on the same level multiple times)
360
362
  union(assets, level.assets);
361
363
  } else if (level.layers) {
362
364
  for (let i = 0; i < level.layers.length; i++) {
@@ -405,7 +407,7 @@ const
405
407
  tile = tiles[l];
406
408
 
407
409
  if (((tile.id + firstgid) === id) && tile.objectgroup) {
408
- addObjectGroupAssets(assets, tile.objectgroup);
410
+ addObjectGroupAssets(assets, tile.objectgroup, level.tilesets);
409
411
  }
410
412
  }
411
413
  }
@@ -452,7 +454,9 @@ const
452
454
  union(assets, tilesets);
453
455
  arrayCache.recycle(tilesets);
454
456
  }
455
- level.assets = greenSlice(assets); // Save for later in case this level is checked again.
457
+ if (cacheAssetsOnLevel) {
458
+ level.assets = greenSlice(assets); // Save for later in case this level is checked again.
459
+ }
456
460
  }
457
461
  }
458
462
 
@@ -746,7 +750,7 @@ export default createComponentClass(/** @lends platypus.components.TiledLoader.p
746
750
  } = layerProperties ? checkParallax(layer) : layer,
747
751
  mapOffsetX = offsetX + layerOffsetX,
748
752
  mapOffsetY = offsetY + layerOffsetY,
749
- layerDefinition = JSON.parse(JSON.stringify(platypus.game.settings.entities[entityKind] ?? standardEntityLayers[entityKind])), //TODO: a bit of a hack to copy an object instead of overwrite values
753
+ layerDefinition = cloneDefinition(platypus.game.settings.entities[entityKind] ?? standardEntityLayers[entityKind]),
750
754
  layerDefinitionProperties = layerDefinition.properties = layerDefinition.properties ?? {},
751
755
  {components} = layerDefinition,
752
756
  importAnimation = {},