littlejsengine 1.24.3 → 1.26.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.
- package/CHANGELOG.md +123 -0
- package/EDITOR.md +2 -2
- package/FAQ.md +55 -11
- package/README.md +6 -2
- package/REFERENCE.md +203 -57
- package/dist/littlejs.d.ts +933 -313
- package/dist/littlejs.esm.js +5143 -2419
- package/dist/littlejs.esm.min.js +38 -24
- package/dist/littlejs.js +5132 -2416
- package/dist/littlejs.min.js +37 -23
- package/dist/littlejs.release.js +4905 -2232
- package/package.json +4 -4
- package/plugins/box2d.js +141 -56
- package/plugins/drawUtilities.js +374 -380
- package/plugins/gltf.js +383 -60
- package/plugins/lightSystem.js +412 -30
- package/plugins/math3d.js +1015 -1014
- package/plugins/medalSystem.js +19 -7
- package/plugins/newgrounds.js +5 -6
- package/plugins/parallax.js +1 -0
- package/plugins/particleEffects.js +47 -14
- package/plugins/pathFinder.js +60 -38
- package/plugins/pluginExport.js +11 -0
- package/plugins/postProcess.js +200 -67
- package/plugins/render3d.js +493 -2391
- package/plugins/render3dDebug.js +5 -8
- package/plugins/render3dEditor.js +84 -9
- package/plugins/render3dExtras.js +122 -33
- package/plugins/render3dLevel.js +67 -8
- package/plugins/render3dMesh.js +1158 -0
- package/plugins/render3dObject.js +1451 -0
- package/plugins/render3dVoxels.js +15 -3
- package/plugins/textureSheet.js +9 -8
- package/plugins/threejs.js +1 -1
- package/plugins/tweenSystem.js +117 -87
- package/plugins/uiSystem.js +62 -28
- package/plugins/wavedash.js +229 -0
- package/src/engine.js +192 -40
- package/src/engineAudio.js +47 -27
- package/src/engineBuild.mjs +6 -1
- package/src/engineDebug.js +23 -162
- package/src/engineDraw.js +148 -43
- package/src/engineEditor.js +153 -40
- package/src/engineExport.js +0 -3
- package/src/engineInput.js +31 -25
- package/src/engineMath.js +38 -20
- package/src/engineObject.js +68 -50
- package/src/engineParticles.js +66 -18
- package/src/engineRelease.js +18 -15
- package/src/engineSettings.js +48 -9
- package/src/engineTileLayer.js +44 -18
- package/src/engineUtilities.js +56 -11
- package/src/engineWebGL.js +68 -22
package/src/engineEditor.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* @callback EditorPlayFromCallback - Puts the player at a world position, for the level editor's Play from mouse
|
|
17
|
-
* @param {
|
|
17
|
+
* @param {any} pos - Where to start playing, a Vector2 from the 2D editor and a Vector3 from the 3D one
|
|
18
18
|
* @memberof Editor
|
|
19
19
|
*/
|
|
20
20
|
|
|
@@ -32,25 +32,29 @@
|
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
* @
|
|
36
|
-
* @
|
|
35
|
+
* @template [P=Vector2]
|
|
36
|
+
* @typedef {Object} EditorToolAt - What the level editor gives the callbacks of a tool of the game's own; P is
|
|
37
|
+
* the type of pos, a Vector2 for a 2D tool, the default, and Vector3|undefined for a 3D one
|
|
38
|
+
* @property {P} pos - Where the mouse is in the level: a Vector2 in the 2D editor; in the 3D one a Vector3 on
|
|
37
39
|
* the level or the ground, undefined when the mouse is over the panel
|
|
38
40
|
* @property {Vector2|undefined} cell - 2D: the tile cell under the mouse on the selected tile layer
|
|
39
|
-
* @property {
|
|
41
|
+
* @property {Ray3D|undefined} ray - 3D: the mouse's Ray3D
|
|
40
42
|
* @property {boolean} shift - Is Shift held
|
|
41
43
|
* @property {boolean} ctrl - Is Ctrl held
|
|
42
44
|
* @memberof Editor
|
|
43
45
|
*/
|
|
44
46
|
|
|
45
47
|
/**
|
|
46
|
-
* @
|
|
48
|
+
* @template [P=Vector2]
|
|
49
|
+
* @typedef {Object} EditorTool - A tool of the game's own for the level editor, see LevelEditor.addTool; P is the
|
|
50
|
+
* type of its pos, as EditorToolAt has it
|
|
47
51
|
* @property {string} [key] - The key that picks it, as addKey spells one
|
|
48
52
|
* @property {string} [hint] - The hint line while it is on
|
|
49
|
-
* @property {function(EditorToolAt): any} [onPress] - The left button went down in the level; returning false
|
|
53
|
+
* @property {function(EditorToolAt<P>): any} [onPress] - The left button went down in the level; returning false
|
|
50
54
|
* says the press was not the tool's
|
|
51
|
-
* @property {function(EditorToolAt): any} [onDrag] - Each frame it is held
|
|
52
|
-
* @property {function(EditorToolAt): any} [onRelease] - It was let go
|
|
53
|
-
* @property {function(EditorToolAt): any} [onDraw] - Each frame the tool is on, to draw its cursor or preview
|
|
55
|
+
* @property {function(EditorToolAt<P>): any} [onDrag] - Each frame it is held
|
|
56
|
+
* @property {function(EditorToolAt<P>): any} [onRelease] - It was let go
|
|
57
|
+
* @property {function(EditorToolAt<P>): any} [onDraw] - Each frame the tool is on, to draw its cursor or preview
|
|
54
58
|
* @memberof Editor
|
|
55
59
|
*/
|
|
56
60
|
|
|
@@ -143,7 +147,7 @@ class LevelEditor
|
|
|
143
147
|
this.keys = {};
|
|
144
148
|
/** @type {Array<{label: string, onClick: Function, title: string}>} */
|
|
145
149
|
this.buttons = [];
|
|
146
|
-
/** @type {Object<string, EditorTool
|
|
150
|
+
/** @type {Object<string, EditorTool<any>>} */
|
|
147
151
|
this.tools = {};
|
|
148
152
|
}
|
|
149
153
|
|
|
@@ -161,7 +165,7 @@ class LevelEditor
|
|
|
161
165
|
|
|
162
166
|
/** Put the player at a position, a Vector2 in the 2D editor and a Vector3 in the 3D one; set it or override it
|
|
163
167
|
* and the editor has Play from mouse, which starts play there
|
|
164
|
-
* @param {any} pos */
|
|
168
|
+
* @param {any} pos - A Vector2 from the 2D editor, a Vector3 from the 3D one */
|
|
165
169
|
onPlayFrom(pos) {}
|
|
166
170
|
|
|
167
171
|
/** Called when the editor opens, to set or override */
|
|
@@ -244,8 +248,9 @@ class LevelEditor
|
|
|
244
248
|
* in 3D
|
|
245
249
|
* - What the callbacks change through edit2D or edit3D is one undo: the editor ends the stroke at the release,
|
|
246
250
|
* and takes it back when the right button or Escape ends the press
|
|
251
|
+
* @template [P=Vector2]
|
|
247
252
|
* @param {string} name
|
|
248
|
-
* @param {EditorTool} tool - {key, hint, onPress, onDrag, onRelease, onDraw}, each optional; onPress returning
|
|
253
|
+
* @param {EditorTool<P>} tool - {key, hint, onPress, onDrag, onRelease, onDraw}, each optional; onPress returning
|
|
249
254
|
* false says the press was not the tool's */
|
|
250
255
|
addTool(name, tool)
|
|
251
256
|
{
|
|
@@ -260,7 +265,10 @@ class LevelEditor
|
|
|
260
265
|
open()
|
|
261
266
|
{
|
|
262
267
|
if (editorOther?.wanted())
|
|
263
|
-
|
|
268
|
+
{
|
|
269
|
+
editorOther.open();
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
264
272
|
if (!editorSession)
|
|
265
273
|
editorCameraScale = cameraScale; // a new session starts at the game's zoom, a return keeps the editor's
|
|
266
274
|
editorSession = true;
|
|
@@ -272,7 +280,10 @@ class LevelEditor
|
|
|
272
280
|
close()
|
|
273
281
|
{
|
|
274
282
|
if (editorOther?.active())
|
|
275
|
-
|
|
283
|
+
{
|
|
284
|
+
editorOther.close();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
276
287
|
editorSession = false;
|
|
277
288
|
editorSetOpen(false);
|
|
278
289
|
}
|
|
@@ -462,6 +473,7 @@ function editorSetOpen(open)
|
|
|
462
473
|
else
|
|
463
474
|
{
|
|
464
475
|
editorStrokeEnd();
|
|
476
|
+
editorAutosaveFlush(); // the game plays on with what the editor has kept, the stroke just ended too
|
|
465
477
|
editorToolHeld = false;
|
|
466
478
|
editorSelection = editorSelectionDrag = editorRightPress = editorObjectDrag = editorObjectBox = undefined;
|
|
467
479
|
editorObjectSelection.clear();
|
|
@@ -533,6 +545,7 @@ const editorTileLayerData = (layers)=> editorTileLayers(layers).map((layer)=> la
|
|
|
533
545
|
// panel to apply or drop, and one that already has the autosaved data, saved from the editor, drops it
|
|
534
546
|
function editorMapRestore(map)
|
|
535
547
|
{
|
|
548
|
+
editorAutosaveFlush(); // what an earlier map has waiting is written before this one reads the autosaves
|
|
536
549
|
if (editorMapList.some((record)=> record.map === map)) return map; // loaded again, it has the changes
|
|
537
550
|
|
|
538
551
|
// a map object seen before, its record retired when its layers went, gets it back as it was, its data has the
|
|
@@ -568,7 +581,8 @@ function editorMapRestore(map)
|
|
|
568
581
|
const sameFile = saved.hash === record.hash || saved.savedHash === record.hash || !saved.width &&
|
|
569
582
|
(saved.hash === editorMapHash(data, objects) || !saved.objects && saved.hash === editorMapHash(data));
|
|
570
583
|
if ((saved.width ?? map.width) === map.width && (saved.height ?? map.height) === map.height &&
|
|
571
|
-
editorSameData(saved.layers, data) && editorSameData(saved.objects ?? [], objects))
|
|
584
|
+
editorSameData(saved.layers, data) && editorSameData(editorObjectsKept(saved.objects ?? [], objects.length), objects)
|
|
585
|
+
&& !(saved.nextobjectid > (map.nextobjectid ?? 1)))
|
|
572
586
|
editorDiscardPending(record);
|
|
573
587
|
else if (!sameFile || !editorSavedFits(saved, data.length, saved.width ?? map.width, saved.height ?? map.height) ||
|
|
574
588
|
!editorResizeMap(map, saved.width ?? map.width, saved.height ?? map.height) || !editorCopyData(data, saved.layers))
|
|
@@ -579,7 +593,7 @@ function editorMapRestore(map)
|
|
|
579
593
|
}
|
|
580
594
|
else
|
|
581
595
|
{
|
|
582
|
-
editorRestoreObjects(map, saved);
|
|
596
|
+
editorRestoreObjects(map, saved, true);
|
|
583
597
|
console.warn(`LittleJS editor: brought back unsaved edits to ${record.fileName}, ` +
|
|
584
598
|
'Save in the editor (Esc then 0) writes them to the file');
|
|
585
599
|
saved.stale && console.warn(`LittleJS editor: they are older than the last edits to ${record.fileName}, ` +
|
|
@@ -646,6 +660,8 @@ function editorLayerRecord(live)
|
|
|
646
660
|
const record = {map, synthetic: true, fileName: 'level.json', layers: []};
|
|
647
661
|
const layer = {record, source, live, color: WHITE};
|
|
648
662
|
record.layers.push(layer);
|
|
663
|
+
// the records of layers made in code that are gone, as a game that makes them again on a restart leaves them
|
|
664
|
+
editorRetire((other)=> other.synthetic && other.layers.every((l)=> l.live.destroyed));
|
|
649
665
|
editorMapList.push(record);
|
|
650
666
|
return layer;
|
|
651
667
|
}
|
|
@@ -742,6 +758,10 @@ function editorSetBaseline(record, written)
|
|
|
742
758
|
const objects = editorObjectGroups(written.layers).map((group)=> group.objects ?? []);
|
|
743
759
|
Object.assign(record, {original: data, originalObjects: objects, savedHash: editorMapHash(data, objects,
|
|
744
760
|
editorMapLayout(written)), originalSize: {width: written.width, height: written.height}});
|
|
761
|
+
// an Objects layer the editor made is the file's own once a Save wrote it, so it stays when emptied after
|
|
762
|
+
const writtenIds = new Set(editorObjectGroups(written.layers).map((group)=> group.id));
|
|
763
|
+
for (const group of editorObjectGroups(record.map.layers))
|
|
764
|
+
writtenIds.has(group.id) && editorMadeGroups.delete(group);
|
|
745
765
|
record.pending || editorAutosave(record); // edits waiting to be applied keep their autosave
|
|
746
766
|
}
|
|
747
767
|
|
|
@@ -753,6 +773,7 @@ async function editorSave(record, pickAgain=false)
|
|
|
753
773
|
{
|
|
754
774
|
if (!record) return;
|
|
755
775
|
editorStrokeEnd();
|
|
776
|
+
editorAutosaveFlush();
|
|
756
777
|
const text = editorMapJSON(record); // the map as it is now, later edits wait for a later save
|
|
757
778
|
// its place in line is taken now, and the game's hook is asked when its turn comes, so saves are written in
|
|
758
779
|
// the order they were asked for however long a hook takes
|
|
@@ -770,6 +791,7 @@ async function editorSave(record, pickAgain=false)
|
|
|
770
791
|
async function editorSaveText(record, text, pickAgain)
|
|
771
792
|
{
|
|
772
793
|
const picker = /** @type {any} */ (globalThis).showSaveFilePicker;
|
|
794
|
+
let written = false;
|
|
773
795
|
if (picker)
|
|
774
796
|
{
|
|
775
797
|
try
|
|
@@ -785,10 +807,7 @@ async function editorSaveText(record, text, pickAgain)
|
|
|
785
807
|
const writable = await record.fileHandle.createWritable();
|
|
786
808
|
await writable.write(text);
|
|
787
809
|
await writable.close();
|
|
788
|
-
|
|
789
|
-
if (record.fileHandle.name === record.fileName)
|
|
790
|
-
editorSetBaseline(record, JSON.parse(text));
|
|
791
|
-
return 'written';
|
|
810
|
+
written = true;
|
|
792
811
|
}
|
|
793
812
|
catch (error)
|
|
794
813
|
{
|
|
@@ -798,6 +817,14 @@ async function editorSaveText(record, text, pickAgain)
|
|
|
798
817
|
console.warn(`LittleJS editor: ${record.fileName} could not be written, downloaded instead`, error);
|
|
799
818
|
}
|
|
800
819
|
}
|
|
820
|
+
if (written)
|
|
821
|
+
{
|
|
822
|
+
// outside the write's catch, so an error here is its own and not a failed write that downloads a copy;
|
|
823
|
+
// the browser gives the picked file's name but not its folder, so the name is what says it is the map's
|
|
824
|
+
if (record.fileHandle.name === record.fileName)
|
|
825
|
+
editorSetBaseline(record, JSON.parse(text));
|
|
826
|
+
return 'written';
|
|
827
|
+
}
|
|
801
828
|
saveText(text, record.fileName, 'application/json');
|
|
802
829
|
return 'downloaded';
|
|
803
830
|
}
|
|
@@ -818,7 +845,9 @@ function editorWriteSaves(saves, key, name=editorSaveName())
|
|
|
818
845
|
{
|
|
819
846
|
try
|
|
820
847
|
{
|
|
821
|
-
|
|
848
|
+
const text = JSON.stringify(saves);
|
|
849
|
+
editorAutosaveSize = text.length; // how much an autosave writes, which says whether the next one waits
|
|
850
|
+
localStorage.setItem(name, text);
|
|
822
851
|
return editorSaveFailed = false;
|
|
823
852
|
}
|
|
824
853
|
catch { editorSaveFailed = true; }
|
|
@@ -888,22 +917,54 @@ const editorSavedFits = (saved, count, width, height)=> isArray(saved.layers) &&
|
|
|
888
917
|
// remember a map's tile data and objects, or forget them when they are back to the file
|
|
889
918
|
function editorAutosave(record)
|
|
890
919
|
{
|
|
920
|
+
editorAutosaveWaiting.delete(record); // written now
|
|
891
921
|
if (record.synthetic) return; // a layer made in code has no load to bring it back in, save it to a file
|
|
892
922
|
const saves = editorSaves(), map = record.map, data = editorTileLayerData(map.layers);
|
|
893
923
|
const objects = editorObjectGroups(map.layers).map((group)=> group.objects ?? []);
|
|
894
|
-
const original = record.originalObjects ?? [], kept = objects.
|
|
895
|
-
while (kept.length > original.length && !kept[kept.length - 1].length)
|
|
896
|
-
kept.pop(); // an Objects layer the editor made, empty again, is not an edit
|
|
924
|
+
const original = record.originalObjects ?? [], kept = editorObjectsKept(objects, original.length);
|
|
897
925
|
const size = record.originalSize, sameSize = map.width === size.width && map.height === size.height;
|
|
898
926
|
// the map as it was loaded has nothing to keep; one a Save wrote is kept until a reload shows the file has it
|
|
899
927
|
if (sameSize && editorSameData(data, record.original) && editorSameData(kept, original) && !record.savedHash)
|
|
900
928
|
delete saves[record.key];
|
|
901
929
|
else
|
|
902
930
|
saves[record.key] = {hash: record.hash, savedHash: record.savedHash, width: map.width, height: map.height,
|
|
903
|
-
layers: data, objects, nextobjectid: map.nextobjectid};
|
|
931
|
+
layers: data, objects: kept, nextobjectid: map.nextobjectid};
|
|
904
932
|
editorWriteSaves(saves, record.key);
|
|
905
933
|
}
|
|
906
934
|
|
|
935
|
+
// a map's lists of objects with the empty ones past count left off: an Objects layer the editor made, empty again,
|
|
936
|
+
// is not an edit, and a Save leaves it out of the file too
|
|
937
|
+
function editorObjectsKept(objects, count)
|
|
938
|
+
{
|
|
939
|
+
const kept = objects.slice();
|
|
940
|
+
while (kept.length > count && !kept[kept.length - 1]?.length)
|
|
941
|
+
kept.pop();
|
|
942
|
+
return kept;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// an autosave writing more than this many characters, a big map's, waits until the edits stop for a moment, since
|
|
946
|
+
// writing it after every stroke would cost more than the stroke; a small map's is written at once, as always
|
|
947
|
+
const EDITOR_AUTOSAVE_WAIT_SIZE = 1e5;
|
|
948
|
+
let editorAutosaveSize = 0, editorAutosaveTimer;
|
|
949
|
+
const editorAutosaveWaiting = new Set;
|
|
950
|
+
|
|
951
|
+
// autosave a map after an edit: at once when its autosave is small, or a second after the last of a run of edits
|
|
952
|
+
function editorAutosaveSoon(record)
|
|
953
|
+
{
|
|
954
|
+
if (editorAutosaveSize < EDITOR_AUTOSAVE_WAIT_SIZE)
|
|
955
|
+
return editorAutosave(record);
|
|
956
|
+
editorAutosaveWaiting.add(record);
|
|
957
|
+
clearTimeout(editorAutosaveTimer);
|
|
958
|
+
editorAutosaveTimer = setTimeout(editorAutosaveFlush, 1e3);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// write every autosave that is waiting, as the editor closes, the page hides, a map is saved or another loads
|
|
962
|
+
function editorAutosaveFlush()
|
|
963
|
+
{
|
|
964
|
+
clearTimeout(editorAutosaveTimer);
|
|
965
|
+
[...editorAutosaveWaiting].forEach(editorAutosave);
|
|
966
|
+
}
|
|
967
|
+
|
|
907
968
|
// paint every cell of a map's layers from a list of tile data, the tile layers of the map in order, and set its
|
|
908
969
|
// object layers' objects from a list of them; data of another size resizes the map, which needs the Restart hook
|
|
909
970
|
function editorPaintData(record, data, objects, width=record.map.width, height=record.map.height, keepUnknown=false)
|
|
@@ -1008,7 +1069,9 @@ function editorResizeMap(map, width, height)
|
|
|
1008
1069
|
}
|
|
1009
1070
|
// an object on the map before and off it after is dropped, one that was already off it stays
|
|
1010
1071
|
const {tilewidth=1, tileheight=1} = map, oldHeight = map.height;
|
|
1011
|
-
|
|
1072
|
+
// a tile object's y is its bottom, a point's or a rectangle's its top
|
|
1073
|
+
const inside = (object, w, h)=> object.x >= 0 && object.x < w * tilewidth && (object.gid === undefined ?
|
|
1074
|
+
object.y >= 0 && object.y < h * tileheight : object.y > 0 && object.y <= h * tileheight);
|
|
1012
1075
|
for (const group of editorObjectGroups(map.layers))
|
|
1013
1076
|
{
|
|
1014
1077
|
group.objects &&= group.objects.filter((object)=>
|
|
@@ -1084,6 +1147,8 @@ let editorStroke;
|
|
|
1084
1147
|
|
|
1085
1148
|
// during a big edit, the layers whose cell by cell redraws are held off, with the redraw each had of its own
|
|
1086
1149
|
let editorHeldRedraws;
|
|
1150
|
+
// the live layers a change can only show by drawing them again whole, drawn when it is done
|
|
1151
|
+
const editorRedrawWhole = new Set;
|
|
1087
1152
|
|
|
1088
1153
|
// make a big edit, a fill, clear, undo, apply or revert, with each layer's cell by cell redraws held off, a game's
|
|
1089
1154
|
// tile callback's included; the stroke's end draws each layer it touched again whole
|
|
@@ -1110,6 +1175,11 @@ function editorSetCell(layer, pos, gid)
|
|
|
1110
1175
|
source.data[index] = gid;
|
|
1111
1176
|
if (live.destroyed) return before; // a layer the game let go of, the map still has the change
|
|
1112
1177
|
|
|
1178
|
+
// a plain cell draws itself here; held draws, a game's onTile, which may change the cells around, and its own
|
|
1179
|
+
// drawing over the layer show only when the whole layer draws again, once the change is done
|
|
1180
|
+
if (editorHeldRedraws || editorHas('onTile') || live.onRedraw !== TileLayer.prototype.onRedraw)
|
|
1181
|
+
editorRedrawWhole.add(live);
|
|
1182
|
+
|
|
1113
1183
|
if (editorHeldRedraws && !editorHeldRedraws.has(live))
|
|
1114
1184
|
{
|
|
1115
1185
|
editorHeldRedraws.set(live, live.hasOwnProperty('redrawTileData') ? live.redrawTileData : undefined);
|
|
@@ -1187,21 +1257,21 @@ function editorUndo(redo=false)
|
|
|
1187
1257
|
editorChanged(stroke);
|
|
1188
1258
|
}
|
|
1189
1259
|
|
|
1190
|
-
// after a change the tile layers it touched draw again whole,
|
|
1191
|
-
//
|
|
1260
|
+
// after a change the tile layers it touched that need it draw again whole, and the maps it touched are autosaved;
|
|
1261
|
+
// a resized map is made again by the game, with the editor staying open
|
|
1192
1262
|
function editorChanged(stroke)
|
|
1193
1263
|
{
|
|
1194
1264
|
editorRedraw(stroke);
|
|
1195
1265
|
for (const record of new Set(stroke.map(editorEntryRecord)))
|
|
1196
|
-
|
|
1266
|
+
editorAutosaveSoon(record);
|
|
1197
1267
|
stroke.some((entry)=> entry.resize) && editorHas('onRestart') && levelEditor.onRestart();
|
|
1198
1268
|
}
|
|
1199
1269
|
|
|
1200
|
-
// the tile layers a stroke touched draw again whole
|
|
1270
|
+
// the tile layers a stroke touched that need it draw again whole
|
|
1201
1271
|
function editorRedraw(stroke)
|
|
1202
1272
|
{
|
|
1203
1273
|
for (const layer of new Set(stroke.filter((entry)=> entry.layer).map((entry)=> entry.layer)))
|
|
1204
|
-
layer.live.destroyed
|
|
1274
|
+
editorRedrawWhole.delete(layer.live) && !layer.live.destroyed && layer.live.redraw();
|
|
1205
1275
|
}
|
|
1206
1276
|
|
|
1207
1277
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -1370,13 +1440,17 @@ function editorObjectSetProperty(object, name, value, defaultValue)
|
|
|
1370
1440
|
delete object.properties;
|
|
1371
1441
|
}
|
|
1372
1442
|
|
|
1373
|
-
// copy saved objects into a map's object layers, making an Objects layer for any it saved that the map lacks
|
|
1374
|
-
|
|
1443
|
+
// copy saved objects into a map's object layers, making an Objects layer for any it saved that the map lacks; from
|
|
1444
|
+
// an autosave, which keeps no empty list of a layer the editor made and no Save wrote, an empty one is of a layer a
|
|
1445
|
+
// Save wrote, the file's own, which stays when empty; an undo's snapshot keeps every list, so it does not say that
|
|
1446
|
+
function editorRestoreObjects(map, saved, fromAutosave=false)
|
|
1375
1447
|
{
|
|
1376
1448
|
const groups = editorObjectGroups(map.layers);
|
|
1377
1449
|
(saved.objects ?? []).forEach((objects, i)=>
|
|
1378
1450
|
{
|
|
1379
|
-
|
|
1451
|
+
const group = groups[i] ?? editorNewObjectGroup(map);
|
|
1452
|
+
group.objects = editorObjectsCopy(objects);
|
|
1453
|
+
fromAutosave && !objects.length && editorMadeGroups.delete(group);
|
|
1380
1454
|
});
|
|
1381
1455
|
if (saved.nextobjectid > (map.nextobjectid ?? 1))
|
|
1382
1456
|
map.nextobjectid = saved.nextobjectid;
|
|
@@ -1708,6 +1782,40 @@ function editorGidMirror(gid)
|
|
|
1708
1782
|
return t ? editorTileToGid(t.tile, (4 - t.direction) % 4, !t.mirror) : gid;
|
|
1709
1783
|
}
|
|
1710
1784
|
|
|
1785
|
+
// a stamp's object turned a quarter clockwise with it: its place by the stamp, its body by its own rotation, which
|
|
1786
|
+
// Tiled turns clockwise about the object's place, so the place needs no other move
|
|
1787
|
+
function editorStampObjectTurn(entry, width)
|
|
1788
|
+
{
|
|
1789
|
+
const offset = vec2(entry.offset.y, width - entry.offset.x), object = entry.object;
|
|
1790
|
+
return {...entry, offset, ...(object && {object: {...object, rotation: ((object.rotation || 0) + 90) % 360}})};
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
// a stamp's object mirrored left to right with it: its rotation the other way, a polygon's points mirrored about its
|
|
1794
|
+
// place, a tile object's image flipped, and a box, ellipse, text or tile, placed by a corner, placed by the corner
|
|
1795
|
+
// that becomes it, its width along its turn from the mirrored place; mirrored twice it is as it was
|
|
1796
|
+
function editorStampObjectMirror(entry, width)
|
|
1797
|
+
{
|
|
1798
|
+
const offset = vec2(width - entry.offset.x, entry.offset.y), object = entry.object;
|
|
1799
|
+
if (!object) return {...entry, offset};
|
|
1800
|
+
const mirrored = {...object, rotation: object.rotation ? -object.rotation : 0};
|
|
1801
|
+
const points = (list)=> list.map((p)=> ({...p, x: -p.x}));
|
|
1802
|
+
if (object.polygon || object.polyline)
|
|
1803
|
+
{
|
|
1804
|
+
object.polygon && (mirrored.polygon = points(object.polygon));
|
|
1805
|
+
object.polyline && (mirrored.polyline = points(object.polyline));
|
|
1806
|
+
}
|
|
1807
|
+
else
|
|
1808
|
+
{
|
|
1809
|
+
// Tiled's pixels run down, the stamp's cells up
|
|
1810
|
+
const angle = (object.rotation || 0) * PI / 180, w = object.width || 0, {x: tw, y: th} = entry.tileSize ?? vec2(1);
|
|
1811
|
+
offset.x -= w * cos(angle) / tw;
|
|
1812
|
+
offset.y -= w * sin(angle) / th;
|
|
1813
|
+
}
|
|
1814
|
+
if (object.gid)
|
|
1815
|
+
mirrored.gid = (object.gid ^ 0x80000000) >>> 0; // Tiled's flip across
|
|
1816
|
+
return {...entry, offset, object: mirrored};
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1711
1819
|
// a stamp turned a quarter turn clockwise on screen, or back, its cells, their tiles and its objects together; cell
|
|
1712
1820
|
// (x, y) goes to (y, width - 1 - x), so the bottom row becomes the left column, its left end at the top
|
|
1713
1821
|
function editorStampTurn(stamp, back=false)
|
|
@@ -1715,7 +1823,7 @@ function editorStampTurn(stamp, back=false)
|
|
|
1715
1823
|
for (let turns = back ? 3 : 1; turns--;)
|
|
1716
1824
|
{
|
|
1717
1825
|
const {width, height} = stamp;
|
|
1718
|
-
const objects = stamp.objects?.map((
|
|
1826
|
+
const objects = stamp.objects?.map((entry)=> editorStampObjectTurn(entry, width));
|
|
1719
1827
|
stamp = {width: height, height: width, grids: stamp.grids.map((grid)=>
|
|
1720
1828
|
{
|
|
1721
1829
|
const turned = [];
|
|
@@ -1732,7 +1840,7 @@ function editorStampTurn(stamp, back=false)
|
|
|
1732
1840
|
function editorStampMirror(stamp)
|
|
1733
1841
|
{
|
|
1734
1842
|
const {width, height} = stamp;
|
|
1735
|
-
const objects = stamp.objects?.map((
|
|
1843
|
+
const objects = stamp.objects?.map((entry)=> editorStampObjectMirror(entry, width));
|
|
1736
1844
|
return {width, height, grids: stamp.grids.map((grid)=>
|
|
1737
1845
|
{
|
|
1738
1846
|
const mirrored = [];
|
|
@@ -1864,8 +1972,9 @@ function editorClear()
|
|
|
1864
1972
|
editorStrokeEnd();
|
|
1865
1973
|
}
|
|
1866
1974
|
|
|
1867
|
-
// the objects of a map's object layers inside a tile area, each {group, type, offset, object}: the index of
|
|
1868
|
-
// object layer, its offset from the area's bottom left corner,
|
|
1975
|
+
// the objects of a map's object layers inside a tile area, each {group, type, offset, object, tileSize}: the index of
|
|
1976
|
+
// its object layer, its offset from the area's bottom left corner, a copy of it, and the map's tile size in pixels,
|
|
1977
|
+
// which a mirror reads its width in cells by, as a stamp keeps them
|
|
1869
1978
|
function editorAreaObjects(layer, area)
|
|
1870
1979
|
{
|
|
1871
1980
|
const corner = layer.live.pos.add(area.min), far = layer.live.pos.add(area.max).add(vec2(1));
|
|
@@ -1874,7 +1983,7 @@ function editorAreaObjects(layer, area)
|
|
|
1874
1983
|
const ids = new Set(editorObjectsIn(objectLayer, corner, far));
|
|
1875
1984
|
return (objectLayer.group?.objects ?? []).filter((object)=> ids.has(object.id)).map((object)=>
|
|
1876
1985
|
({group, type: object.type || object.class, offset: editorObjectPos(layer.record, object).subtract(corner),
|
|
1877
|
-
object: editorObjectsCopy(object)}));
|
|
1986
|
+
object: editorObjectsCopy(object), tileSize: vec2(layer.record.map.tilewidth || 1, layer.record.map.tileheight || 1)}));
|
|
1878
1987
|
});
|
|
1879
1988
|
}
|
|
1880
1989
|
|
|
@@ -2266,7 +2375,8 @@ function editorPaletteDrawObjects(canvas)
|
|
|
2266
2375
|
else
|
|
2267
2376
|
{
|
|
2268
2377
|
context.fillStyle = '#ccc';
|
|
2269
|
-
|
|
2378
|
+
const label = textGraphemes(name).slice(0, 4).join(''); // whole characters, never half an emoji
|
|
2379
|
+
context.fillText(label, x + cell / 2, y + cell / 2);
|
|
2270
2380
|
}
|
|
2271
2381
|
if (editorObjectBrush?.length !== 1 || editorObjectBrush[0].type !== name) return;
|
|
2272
2382
|
context.strokeStyle = '#4af';
|
|
@@ -2963,5 +3073,8 @@ debug && engineAddPlugin(editorUpdate, ()=> { editorRender(); editorRenderGame()
|
|
|
2963
3073
|
if (debug && globalThis.document?.addEventListener)
|
|
2964
3074
|
{
|
|
2965
3075
|
document.addEventListener('keydown', editorOnKeyDown);
|
|
3076
|
+
// a page going away writes what is waiting, the last event it is sure to get
|
|
3077
|
+
document.addEventListener('visibilitychange', ()=> document.hidden && editorAutosaveFlush());
|
|
3078
|
+
globalThis.addEventListener?.('pagehide', editorAutosaveFlush);
|
|
2966
3079
|
document.addEventListener('wheel', editorOnWheel, {passive: true});
|
|
2967
3080
|
}
|
package/src/engineExport.js
CHANGED
package/src/engineInput.js
CHANGED
|
@@ -198,9 +198,11 @@ function inputClear()
|
|
|
198
198
|
* @memberof Input */
|
|
199
199
|
function keyIsDown(key, device=0)
|
|
200
200
|
{
|
|
201
|
-
ASSERT(isStringLike(key), 'key must be a
|
|
202
|
-
ASSERT(typeof key !== 'string' || key.length > 1,
|
|
203
|
-
|
|
201
|
+
ASSERT(isStringLike(key), 'keyIsDown: key must be a code like \'KeyA\' or a number', key);
|
|
202
|
+
ASSERT(typeof key !== 'string' || key.length > 1,
|
|
203
|
+
"keyIsDown: keys are codes like 'KeyW' or 'Space', not characters", key);
|
|
204
|
+
ASSERT(device > 0 || typeof key !== 'number' || key < 5,
|
|
205
|
+
"keyIsDown: keyboard keys are codes like 'KeyA' or 'Space', the numbers 0 to 4 are mouse buttons", key);
|
|
204
206
|
return !!(inputData[device]?.[key] & 1) && !inputCaptureHides(device);
|
|
205
207
|
}
|
|
206
208
|
|
|
@@ -211,9 +213,11 @@ function keyIsDown(key, device=0)
|
|
|
211
213
|
* @memberof Input */
|
|
212
214
|
function keyWasPressed(key, device=0)
|
|
213
215
|
{
|
|
214
|
-
ASSERT(isStringLike(key), 'key must be a
|
|
215
|
-
ASSERT(typeof key !== 'string' || key.length > 1,
|
|
216
|
-
|
|
216
|
+
ASSERT(isStringLike(key), 'keyWasPressed: key must be a code like \'KeyA\' or a number', key);
|
|
217
|
+
ASSERT(typeof key !== 'string' || key.length > 1,
|
|
218
|
+
"keyWasPressed: keys are codes like 'KeyW' or 'Space', not characters", key);
|
|
219
|
+
ASSERT(device > 0 || typeof key !== 'number' || key < 5,
|
|
220
|
+
"keyWasPressed: keyboard keys are codes like 'KeyA' or 'Space', the numbers 0 to 4 are mouse buttons", key);
|
|
217
221
|
return !!(inputData[device]?.[key] & 2) && !inputCaptureHides(device);
|
|
218
222
|
}
|
|
219
223
|
|
|
@@ -224,9 +228,11 @@ function keyWasPressed(key, device=0)
|
|
|
224
228
|
* @memberof Input */
|
|
225
229
|
function keyWasReleased(key, device=0)
|
|
226
230
|
{
|
|
227
|
-
ASSERT(isStringLike(key), 'key must be a
|
|
228
|
-
ASSERT(typeof key !== 'string' || key.length > 1,
|
|
229
|
-
|
|
231
|
+
ASSERT(isStringLike(key), 'keyWasReleased: key must be a code like \'KeyA\' or a number', key);
|
|
232
|
+
ASSERT(typeof key !== 'string' || key.length > 1,
|
|
233
|
+
"keyWasReleased: keys are codes like 'KeyW' or 'Space', not characters", key);
|
|
234
|
+
ASSERT(device > 0 || typeof key !== 'number' || key < 5,
|
|
235
|
+
"keyWasReleased: keyboard keys are codes like 'KeyA' or 'Space', the numbers 0 to 4 are mouse buttons", key);
|
|
230
236
|
return !!(inputData[device]?.[key] & 4) && !inputCaptureHides(device);
|
|
231
237
|
}
|
|
232
238
|
|
|
@@ -248,35 +254,32 @@ function keyDirection(up='ArrowUp', down='ArrowDown', left='ArrowLeft', right='A
|
|
|
248
254
|
}
|
|
249
255
|
|
|
250
256
|
/** Returns true if mouse button is down
|
|
251
|
-
* @function
|
|
252
257
|
* @param {number} button
|
|
253
258
|
* @return {boolean}
|
|
254
259
|
* @memberof Input */
|
|
255
260
|
function mouseIsDown(button)
|
|
256
261
|
{
|
|
257
|
-
ASSERT(isNumber(button), '
|
|
262
|
+
ASSERT(isNumber(button), 'mouseIsDown: button is 0 (left), 1 (middle) or 2 (right)', button);
|
|
258
263
|
return keyIsDown(button);
|
|
259
264
|
}
|
|
260
265
|
|
|
261
266
|
/** Returns true if mouse button was pressed
|
|
262
|
-
* @function
|
|
263
267
|
* @param {number} button
|
|
264
268
|
* @return {boolean}
|
|
265
269
|
* @memberof Input */
|
|
266
270
|
function mouseWasPressed(button)
|
|
267
271
|
{
|
|
268
|
-
ASSERT(isNumber(button), '
|
|
272
|
+
ASSERT(isNumber(button), 'mouseWasPressed: button is 0 (left), 1 (middle) or 2 (right)', button);
|
|
269
273
|
return keyWasPressed(button);
|
|
270
274
|
}
|
|
271
275
|
|
|
272
276
|
/** Returns true if mouse button was released
|
|
273
|
-
* @function
|
|
274
277
|
* @param {number} button
|
|
275
278
|
* @return {boolean}
|
|
276
279
|
* @memberof Input */
|
|
277
280
|
function mouseWasReleased(button)
|
|
278
281
|
{
|
|
279
|
-
ASSERT(isNumber(button), '
|
|
282
|
+
ASSERT(isNumber(button), 'mouseWasReleased: button is 0 (left), 1 (middle) or 2 (right)', button);
|
|
280
283
|
return keyWasReleased(button);
|
|
281
284
|
}
|
|
282
285
|
|
|
@@ -498,21 +501,24 @@ function inputInit()
|
|
|
498
501
|
{
|
|
499
502
|
// fix stalled audio requiring user interaction, a keyboard only game has no other gesture
|
|
500
503
|
if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
|
|
501
|
-
|
|
504
|
+
audioResume();
|
|
502
505
|
|
|
503
506
|
// keys typed into an html text field are the player's typing, not game input;
|
|
504
507
|
// a key already down still releases on keyup, which only lets go of keys that are down
|
|
505
508
|
const typing = isTextInput(e.target) || isTextInput(document.activeElement);
|
|
506
|
-
|
|
509
|
+
// a key already held, a keydown the browser did not mark as a repeat, is not pressed again, but with Cmd down
|
|
510
|
+
// it is: a Mac sends no keyup for a key let go while Cmd is held, so each tap of Cmd+Z comes as a keydown alone
|
|
511
|
+
if (!e.repeat && !typing && (!inputKeysHeld.has(e.code) || e.metaKey))
|
|
507
512
|
{
|
|
508
513
|
inputKeysHeld.add(e.code);
|
|
509
|
-
// an arrow its alias already holds down is not pressed again, like its release waits for both
|
|
514
|
+
// an arrow its alias already holds down is not pressed again, like its release waits for both; a release
|
|
515
|
+
// earlier this frame is kept, so a quick release and press reads as both
|
|
510
516
|
if (!(inputWASDEmulateDirection && inputKeysHeld.has(inputArrowToWASD[e.code]) && inputData[0][e.code] & 1))
|
|
511
|
-
inputData[0][e.code] = 3;
|
|
517
|
+
inputData[0][e.code] = 3 | inputData[0][e.code] & 4;
|
|
512
518
|
// an alias presses its arrow's slot too, unless the arrow itself already holds it down
|
|
513
519
|
const remap = remapKey(e.code);
|
|
514
520
|
if (remap !== e.code && !(inputData[0][remap] & 1))
|
|
515
|
-
inputData[0][remap] = 3;
|
|
521
|
+
inputData[0][remap] = 3 | inputData[0][remap] & 4;
|
|
516
522
|
}
|
|
517
523
|
|
|
518
524
|
// try to prevent default browser handling of input
|
|
@@ -584,13 +590,13 @@ function inputInit()
|
|
|
584
590
|
|
|
585
591
|
// fix stalled audio requiring user interaction
|
|
586
592
|
if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
|
|
587
|
-
|
|
593
|
+
audioResume();
|
|
588
594
|
|
|
589
595
|
// a press in the bars around a letterboxed canvas is not one on its edge; a drag out of it still moves; under
|
|
590
596
|
// pointer lock the mouse stays where the lock began, which may be in a bar, and every click is the game's
|
|
591
597
|
if (!pointerLockIsActive() && !inCanvas(e.x, e.y))
|
|
592
598
|
return;
|
|
593
|
-
inputData[0][e.button] = 3;
|
|
599
|
+
inputData[0][e.button] = 3 | inputData[0][e.button] & 4; // a release earlier this frame is kept
|
|
594
600
|
|
|
595
601
|
const mousePosScreenLast = mousePosScreen;
|
|
596
602
|
mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
|
|
@@ -684,7 +690,7 @@ function inputInit()
|
|
|
684
690
|
|
|
685
691
|
// fix stalled audio requiring user interaction
|
|
686
692
|
if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
|
|
687
|
-
|
|
693
|
+
audioResume();
|
|
688
694
|
|
|
689
695
|
// when the touch gamepad is enabled it owns touch input: suppress the
|
|
690
696
|
// touch->mouse passthrough entirely unless touchGamepadPassthrough is set
|
|
@@ -744,7 +750,7 @@ function inputInit()
|
|
|
744
750
|
else if (inputWasTouching && touch.identifier === inputTouchIdentifier)
|
|
745
751
|
mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
|
|
746
752
|
else if (pressTouch && inCanvas(pressTouch.clientX, pressTouch.clientY))
|
|
747
|
-
inputData[0][button] = 3; // a tap in the bars
|
|
753
|
+
inputData[0][button] = 3 | inputData[0][button] & 4; // a tap in the bars is not a press
|
|
748
754
|
// the finger left after a pinch moves the mouse from where it is, with no jump and no press
|
|
749
755
|
inputTouchIdentifier = pinching ? undefined : touch.identifier;
|
|
750
756
|
}
|
|
@@ -1449,7 +1455,7 @@ function touchGamepadPointerDown(e, zone)
|
|
|
1449
1455
|
|
|
1450
1456
|
// resume audio on first interaction
|
|
1451
1457
|
if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
|
|
1452
|
-
|
|
1458
|
+
audioResume();
|
|
1453
1459
|
|
|
1454
1460
|
// while paused, any touch is the start button; a control belongs to the first finger on it until that
|
|
1455
1461
|
// finger lifts, so a second finger landing on the same one neither takes it over nor lets it go
|