littlejsengine 1.22.0 → 1.23.1
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/COPYRIGHT.txt +65 -38
- package/EDITOR.md +359 -0
- package/README.md +12 -7
- package/REFERENCE.md +2314 -0
- package/dist/littlejs.d.ts +640 -100
- package/dist/littlejs.esm.js +3596 -1444
- package/dist/littlejs.esm.min.js +3 -3
- package/dist/littlejs.js +3583 -1444
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +2106 -1355
- package/package.json +4 -3
- package/plugins/gltf.js +29 -13
- package/plugins/lightSystem.js +8 -6
- package/plugins/parallax.js +174 -0
- package/plugins/particleEffects.js +37 -22
- package/plugins/pluginExport.js +8 -0
- package/plugins/postProcess.js +5 -2
- package/plugins/render3d.js +59 -21
- package/plugins/render3dEditor.js +1058 -118
- package/plugins/render3dExtras.js +82 -21
- package/plugins/render3dLevel.js +328 -8
- package/plugins/textureSheet.js +1 -0
- package/plugins/tweenSystem.js +39 -34
- package/plugins/uiSystem.js +7 -3
- package/plugins/zzfxm.js +161 -159
- package/src/engine.js +1 -1
- package/src/engineAudio.js +17 -3
- package/src/engineBuild.mjs +18 -2
- package/src/engineDraw.js +26 -10
- package/src/engineEditor.js +515 -52
- package/src/engineExport.js +5 -0
- package/src/engineInput.js +48 -6
- package/src/engineObject.js +675 -675
- package/src/engineRelease.js +17 -2
- package/src/engineSettings.js +14 -0
- package/src/engineTileLayer.js +2 -1
- package/src/engineUtilities.js +291 -291
- package/src/engineWebGL.js +12 -3
package/src/engineEditor.js
CHANGED
|
@@ -31,32 +31,101 @@
|
|
|
31
31
|
* @memberof Editor
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* @typedef {Object} EditorToolAt - What the level editor gives the callbacks of a tool of the game's own
|
|
36
|
+
* @property {any} pos - Where the mouse is in the level: a Vector2 in the 2D editor; in the 3D one a Vector3 on
|
|
37
|
+
* the level or the ground, undefined when the mouse is over the panel
|
|
38
|
+
* @property {Vector2|undefined} cell - 2D: the tile cell under the mouse on the selected tile layer
|
|
39
|
+
* @property {any} ray - 3D: the mouse's Ray3D
|
|
40
|
+
* @property {boolean} shift - Is Shift held
|
|
41
|
+
* @property {boolean} ctrl - Is Ctrl held
|
|
42
|
+
* @memberof Editor
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @typedef {Object} EditorTool - A tool of the game's own for the level editor, see LevelEditor.addTool
|
|
47
|
+
* @property {string} [key] - The key that picks it, as addKey spells one
|
|
48
|
+
* @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
|
|
50
|
+
* 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
|
|
54
|
+
* @memberof Editor
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @typedef {Object} EditorEdit2D - The 2D level editor's edit functions, levelEditor.edit2D
|
|
59
|
+
* @property {Object|undefined} map - The Tiled map of the selected layer
|
|
60
|
+
* @property {TileLayer|undefined} layer - The selected tile layer, undefined on an object layer
|
|
61
|
+
* @property {Array<Object>} objects - A copy of the selected object layer's objects, as Tiled has them
|
|
62
|
+
* @property {Vector2|undefined} hover - The cell under the mouse on the selected tile layer
|
|
63
|
+
* @property {Set<number>} selection - The selected objects' ids
|
|
64
|
+
* @property {function(Vector2, number, ...any): boolean} paint - Set a cell of the selected tile layer
|
|
65
|
+
* to a tile index, -1 erases, with a direction 0 to 3 and a mirror; false off the layer or with none
|
|
66
|
+
* @property {function(function(Array<Object>): void): boolean} changeObjects - Edit a copy of the selected object
|
|
67
|
+
* layer's objects; false when nothing changed
|
|
68
|
+
* @property {function(): void} strokeEnd - End the edit: one undo step, and the autosave
|
|
69
|
+
* @property {function(): void} strokeCancel - Take the edit being made back
|
|
70
|
+
* @property {function(function(): void): void} bulk - Many paints with one redraw a layer
|
|
71
|
+
* @property {function(...any): any} undo - Undo, or redo with true
|
|
72
|
+
* @property {function(): string} toJSON - The map as the text Save writes
|
|
73
|
+
* @memberof Editor
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @typedef {Object} EditorEdit3D - The 3D level editor's edit functions, levelEditor.edit3D; a position, a
|
|
78
|
+
* rotation and a scale are Vector3, the rotation in degrees
|
|
79
|
+
* @property {Object|undefined} level - The level object being edited
|
|
80
|
+
* @property {Array<Object>} objects - Its objects, the list in the level: read it, edit through change
|
|
81
|
+
* @property {Set<number>} selection - The selected objects' ids
|
|
82
|
+
* @property {function(): Array<Object>} selected - The selected objects
|
|
83
|
+
* @property {function(number): any} made - What the game made for an object's id
|
|
84
|
+
* @property {function(function(Array<Object>): void): boolean} change - Edit a copy of the object list; false
|
|
85
|
+
* when nothing changed
|
|
86
|
+
* @property {function(string, function(any): any): boolean} changePart - Edit the level's scene, voxels, terrain
|
|
87
|
+
* or prefabs block
|
|
88
|
+
* @property {function(): void} strokeEnd - End the edit: one undo step, and the autosave
|
|
89
|
+
* @property {function(): void} strokeCancel - Take the edit being made back
|
|
90
|
+
* @property {function(string, any): number|undefined} place - Add an object of a type at a position, selected;
|
|
91
|
+
* its id, undefined when there is no such type or the level can not be edited
|
|
92
|
+
* @property {function(Object, ...any): void} setTransform - Write a place, a rotation and a scale on an
|
|
93
|
+
* object of the list given to change; one left out is kept
|
|
94
|
+
* @property {function(Object, string, any): void} setProperty - Write a property on an object of the list given
|
|
95
|
+
* to change, left out of the file when it is the type's default
|
|
96
|
+
* @property {function(Object): any} pos - An object's position
|
|
97
|
+
* @property {function(Object): any} rotation - Its rotation, in degrees
|
|
98
|
+
* @property {function(Object): any} scale - Its scale
|
|
99
|
+
* @property {function(): any} mousePoint - Where the mouse is on the level or the ground, undefined over the panel
|
|
100
|
+
* @property {function(...any): boolean} undo - Undo, or redo with true
|
|
101
|
+
* @property {function(): string} toJSON - The level as the text Save writes
|
|
102
|
+
* @memberof Editor
|
|
103
|
+
*/
|
|
104
|
+
|
|
34
105
|
/**
|
|
35
106
|
* The level editor, open it to pause the game and edit its level, close it to play on with the changes
|
|
36
107
|
* - One of it, levelEditor, 0 on the debug overlay opens and closes it too
|
|
37
|
-
* -
|
|
108
|
+
* - A game makes it its own: it sets the hooks, adds keys, panel buttons and tools, or extends this class and
|
|
109
|
+
* gives its own with setLevelEditor; the same calls work in the 2D editor and the 3D one, see EDITOR.md
|
|
110
|
+
* - In release builds LevelEditor is a stub that never opens: what a game adds is taken and never called
|
|
38
111
|
* @memberof Editor
|
|
39
112
|
* @example
|
|
40
113
|
* levelEditor.onRestart = ()=> loadLevel(); // adds a Restart button that rebuilds the level
|
|
41
114
|
* levelEditor.onTile = (layer, pos, tile)=> layer.setCollisionData(pos, tile === ladderTile ? -1 : tile ? 1 : 0);
|
|
115
|
+
* levelEditor.addKey('k', ()=> clearEnemies(), 'K: clear the enemies');
|
|
116
|
+
*
|
|
117
|
+
* // or as a class of the game's own
|
|
118
|
+
* class MyEditor extends LevelEditor
|
|
119
|
+
* {
|
|
120
|
+
* onRestart() { loadLevel(); }
|
|
121
|
+
* onDraw() { drawSpawnZones(); }
|
|
122
|
+
* }
|
|
123
|
+
* setLevelEditor(new MyEditor);
|
|
42
124
|
*/
|
|
43
125
|
class LevelEditor
|
|
44
126
|
{
|
|
45
127
|
constructor()
|
|
46
128
|
{
|
|
47
|
-
/** @property {EditorTileCallback|undefined} - What the game does when the editor paints a tile, like
|
|
48
|
-
* setting its collision or its look the way the game does when it loads the level; without one, the
|
|
49
|
-
* collision layer gets collision 1 where there is a tile
|
|
50
|
-
* @type {EditorTileCallback|undefined} */
|
|
51
|
-
this.onTile = undefined;
|
|
52
|
-
/** @property {Function|undefined} - Rebuild the level from the map the editor changed, a Restart button
|
|
53
|
-
* calls it after switching to play; without one there is no Restart button
|
|
54
|
-
* @type {EditorRestartCallback|undefined} */
|
|
55
|
-
this.onRestart = undefined;
|
|
56
|
-
/** @property {Function|undefined} - Put the player at a world position; with it, the editor's Advanced
|
|
57
|
-
* section has Play from mouse, which starts play there, Escape at the mouse and Play at the view center
|
|
58
|
-
* @type {EditorPlayFromCallback|undefined} */
|
|
59
|
-
this.onPlayFrom = undefined;
|
|
60
129
|
/** @property {Array<number>|undefined} - The tiles the palette shows, in its order, for a sheet that also holds
|
|
61
130
|
* sprites and art that are not level tiles; undefined shows every tile of the sheet
|
|
62
131
|
* @type {Array<number>|undefined} */
|
|
@@ -65,15 +134,133 @@ class LevelEditor
|
|
|
65
134
|
* 3D one when a level was loaded with level3DLoad and there is a Render3DPlugin
|
|
66
135
|
* @type {boolean|undefined} */
|
|
67
136
|
this.use3D = undefined;
|
|
137
|
+
/** @property {string|undefined} - The tool of the game's own that is on, one added with addTool, undefined
|
|
138
|
+
* while a tool of the editor's is
|
|
139
|
+
* @type {string|undefined} */
|
|
140
|
+
this.tool = undefined;
|
|
141
|
+
// what the game added: its keys by how they are spelled, its panel buttons and its tools by name
|
|
142
|
+
/** @type {Object<string, {action: function(boolean): any, help: string|undefined, warned: boolean}>} */
|
|
143
|
+
this.keys = {};
|
|
144
|
+
/** @type {Array<{label: string, onClick: Function, title: string}>} */
|
|
145
|
+
this.buttons = [];
|
|
146
|
+
/** @type {Object<string, EditorTool>} */
|
|
147
|
+
this.tools = {};
|
|
68
148
|
}
|
|
69
149
|
|
|
150
|
+
/** What the game does when the 2D editor paints a tile, like setting its collision or its look the way the
|
|
151
|
+
* game does when it loads the level; set it or override it, without one the collision layer gets collision 1
|
|
152
|
+
* where there is a tile
|
|
153
|
+
* @param {TileLayer} layer - The layer painted
|
|
154
|
+
* @param {Vector2} pos - The cell's position in the layer
|
|
155
|
+
* @param {number|undefined} tile - The tile painted, undefined when erased */
|
|
156
|
+
onTile(layer, pos, tile) {}
|
|
157
|
+
|
|
158
|
+
/** Rebuild the level from what the editor changed; set it or override it and the editor has a Restart button,
|
|
159
|
+
* which calls it after switching to play */
|
|
160
|
+
onRestart() {}
|
|
161
|
+
|
|
162
|
+
/** 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
|
+
* and the editor has Play from mouse, which starts play there
|
|
164
|
+
* @param {any} pos */
|
|
165
|
+
onPlayFrom(pos) {}
|
|
166
|
+
|
|
167
|
+
/** Called when the editor opens, to set or override */
|
|
168
|
+
onOpen() {}
|
|
169
|
+
|
|
170
|
+
/** Called when the editor closes and the game plays on, to set or override */
|
|
171
|
+
onClose() {}
|
|
172
|
+
|
|
173
|
+
/** Called each frame while the editor is open, to set or override */
|
|
174
|
+
onUpdate() {}
|
|
175
|
+
|
|
176
|
+
/** Called while the editor draws the level, to draw overlays of the game's own in it, to set or override */
|
|
177
|
+
onDraw() {}
|
|
178
|
+
|
|
179
|
+
/** Called once when the editor's panel is made, with a box in it for the game's own controls, to set or override
|
|
180
|
+
* @param {HTMLElement} element */
|
|
181
|
+
onPanel(element) {}
|
|
182
|
+
|
|
183
|
+
/** Called by Save with the text of the file and its name; return true when the game kept it itself, and the
|
|
184
|
+
* editor writes no file, to set or override; it may be async
|
|
185
|
+
* @param {string} text
|
|
186
|
+
* @param {string} fileName
|
|
187
|
+
* @return {boolean|void|Promise<boolean|void>} */
|
|
188
|
+
onSave(text, fileName) { return false; }
|
|
189
|
+
|
|
70
190
|
/** True while the editor is open, the game is paused under it
|
|
71
191
|
* @return {boolean} */
|
|
72
|
-
get isOpen() { return editorIsOpen; }
|
|
192
|
+
get isOpen() { return editorIsOpen || !!editorOther?.isOpen(); }
|
|
193
|
+
|
|
194
|
+
/** True when the editor in use is the 3D one, as use3D says or since a 3D level was loaded
|
|
195
|
+
* @return {boolean} */
|
|
196
|
+
get is3D() { return !!editorOther?.wanted(); }
|
|
197
|
+
|
|
198
|
+
/** The 2D editor's edit functions, for a key, a button or a tool of the game's own: map, the Tiled map; layer,
|
|
199
|
+
* the selected TileLayer; objects, a copy of the selected object layer's; hover, the cell under the mouse;
|
|
200
|
+
* selection, the selected objects' ids; paint(cell, tile, direction, mirror), a tile index, -1 erases;
|
|
201
|
+
* changeObjects((list)=> ...); strokeEnd() and strokeCancel(); bulk(()=> ...); undo(redo); toJSON()
|
|
202
|
+
* @return {EditorEdit2D} */
|
|
203
|
+
get edit2D() { return editorEdit2D; }
|
|
204
|
+
|
|
205
|
+
/** The 3D editor's edit functions, undefined without the 3D plugins: level; objects; selection, a Set of ids;
|
|
206
|
+
* selected(); made(id); change((list)=> ...); changePart(name, (part)=> ...); strokeEnd() and strokeCancel();
|
|
207
|
+
* place(type, pos3D); setTransform(object, pos3D, rotationDegrees, scale3D); setProperty(object, name, value);
|
|
208
|
+
* pos(object), rotation(object) and scale(object); mousePoint(); undo(redo); toJSON()
|
|
209
|
+
* @return {EditorEdit3D|undefined} */
|
|
210
|
+
get edit3D() { return editorOther?.edit; }
|
|
211
|
+
|
|
212
|
+
/** Add a key of the game's own to the editor, the 2D and the 3D one
|
|
213
|
+
* - Escape and 0 are the editor's own, to play and to exit, and Ctrl is the one modifier: Shift is given to
|
|
214
|
+
* the action
|
|
215
|
+
* @param {string} key - A letter or digit, 'k', or a key's name in any case, 'Delete', 'F2', 'Enter', 'Space';
|
|
216
|
+
* 'ctrl+k' with Ctrl or Cmd
|
|
217
|
+
* @param {function(boolean): any} action - Called with whether Shift is held; returning false says it did
|
|
218
|
+
* nothing, and the key is left to the browser
|
|
219
|
+
* @param {string} [helpLine] - A line for the editor's help */
|
|
220
|
+
addKey(key, action, helpLine)
|
|
221
|
+
{
|
|
222
|
+
ASSERT(typeof action == 'function', 'a key needs an action');
|
|
223
|
+
const spelled = editorKeySpelling(key), name = spelled.replace('ctrl+', '');
|
|
224
|
+
if (name === 'Escape' || name === '0' || name.length > 1 && name.includes('+'))
|
|
225
|
+
{
|
|
226
|
+
console.warn(`levelEditor.addKey: ${key} is not taken, ` + (name.includes('+') ?
|
|
227
|
+
'Ctrl is the one modifier, Shift is given to the action' : 'Escape and 0 are the editor\'s own'));
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
this.keys[spelled] = {action, help: helpLine, warned: false};
|
|
231
|
+
this === levelEditor && editorKeyClashes();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Add a button of the game's own to the editor's panel
|
|
235
|
+
* @param {string} label
|
|
236
|
+
* @param {Function} onClick
|
|
237
|
+
* @param {string} [title] - What it says when the mouse is over it */
|
|
238
|
+
addButton(label, onClick, title='') { this.buttons.push({label, onClick, title}); }
|
|
239
|
+
|
|
240
|
+
/** Add a tool of the game's own: a button in the panel by its name, and while it is on the left button in the
|
|
241
|
+
* level is its, not the editor's own tools'
|
|
242
|
+
* - Each callback is given {pos, cell, ray, shift, ctrl}: pos is where the mouse is in the level, a Vector2 in
|
|
243
|
+
* 2D and a Vector3 on the level or the ground in 3D; cell is the tile under it in 2D; ray is the mouse's
|
|
244
|
+
* in 3D
|
|
245
|
+
* - What the callbacks change through edit2D or edit3D is one undo: the editor ends the stroke at the release,
|
|
246
|
+
* and takes it back when the right button or Escape ends the press
|
|
247
|
+
* @param {string} name
|
|
248
|
+
* @param {EditorTool} tool - {key, hint, onPress, onDrag, onRelease, onDraw}, each optional; onPress returning
|
|
249
|
+
* false says the press was not the tool's */
|
|
250
|
+
addTool(name, tool)
|
|
251
|
+
{
|
|
252
|
+
ASSERT(!!tool && typeof tool == 'object', 'a tool is an object of callbacks');
|
|
253
|
+
const before = this.tools[name]; // added again, it takes the place of the first, its key too
|
|
254
|
+
before?.key && delete this.keys[editorKeySpelling(before.key)];
|
|
255
|
+
this.tools[name] = tool;
|
|
256
|
+
tool.key && this.addKey(tool.key, ()=> { this.tool = this.tool === name ? undefined : name; });
|
|
257
|
+
}
|
|
73
258
|
|
|
74
259
|
/** Open the editor, pausing the game; until close(), Escape (the debug key) switches between playing and editing */
|
|
75
260
|
open()
|
|
76
261
|
{
|
|
262
|
+
if (editorOther?.wanted())
|
|
263
|
+
return editorOther.open();
|
|
77
264
|
if (!editorSession)
|
|
78
265
|
editorCameraScale = cameraScale; // a new session starts at the game's zoom, a return keeps the editor's
|
|
79
266
|
editorSession = true;
|
|
@@ -84,6 +271,8 @@ class LevelEditor
|
|
|
84
271
|
* overlay again */
|
|
85
272
|
close()
|
|
86
273
|
{
|
|
274
|
+
if (editorOther?.active())
|
|
275
|
+
return editorOther.close();
|
|
87
276
|
editorSession = false;
|
|
88
277
|
editorSetOpen(false);
|
|
89
278
|
}
|
|
@@ -92,7 +281,118 @@ class LevelEditor
|
|
|
92
281
|
/** The level editor, levelEditor.open() to edit the level, levelEditor.close() to play on with the changes
|
|
93
282
|
* @type {LevelEditor}
|
|
94
283
|
* @memberof Editor */
|
|
95
|
-
|
|
284
|
+
let levelEditor = new LevelEditor;
|
|
285
|
+
|
|
286
|
+
/** Make a level editor of the game's own the one in use: an instance of a class that extends LevelEditor, with
|
|
287
|
+
* the game's hooks as its methods; set it before the editor opens
|
|
288
|
+
* @param {LevelEditor} editor
|
|
289
|
+
* @memberof Editor */
|
|
290
|
+
function setLevelEditor(editor)
|
|
291
|
+
{
|
|
292
|
+
ASSERT(editor instanceof LevelEditor, 'a level editor extends LevelEditor');
|
|
293
|
+
levelEditor.isOpen && console.warn('setLevelEditor: the editor is open, close it first');
|
|
294
|
+
levelEditor = editor;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// the other editor, the 3D one, when its plugin is there: it says if it is the one wanted and open, opens and
|
|
298
|
+
// closes, and has its edit functions; the 2D editor is this file
|
|
299
|
+
/** @type {{wanted: function(): boolean, isOpen: function(): boolean, active: function(): boolean, open: Function,
|
|
300
|
+
* close: Function, hasKey: function(string, boolean): boolean, edit: EditorEdit3D}|undefined} */
|
|
301
|
+
let editorOther;
|
|
302
|
+
|
|
303
|
+
// if the game gave a hook: set on the editor, or a method of its own class, not the empty one of LevelEditor
|
|
304
|
+
function editorHas(name)
|
|
305
|
+
{
|
|
306
|
+
const hook = levelEditor[name];
|
|
307
|
+
return typeof hook == 'function' && hook !== LevelEditor.prototype[name];
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// a hook or a moment of the game's, called when it is one: a game may set one to undefined to take it away
|
|
311
|
+
function editorCall(name, ...args)
|
|
312
|
+
{
|
|
313
|
+
const hook = levelEditor[name];
|
|
314
|
+
return typeof hook == 'function' ? hook.apply(levelEditor, args) : undefined;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// the names keys have, by their lower case, so a game may spell one in any case
|
|
318
|
+
const editorKeyNames = {};
|
|
319
|
+
for (const name of ['Delete', 'Backspace', 'Enter', 'Tab', 'Escape', 'Space', 'Home', 'End', 'PageUp', 'PageDown',
|
|
320
|
+
'Insert', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ...[...Array(12)].map((v, i)=> 'F' + (i + 1))])
|
|
321
|
+
editorKeyNames[name.toLowerCase()] = name;
|
|
322
|
+
|
|
323
|
+
// a key as addKey keeps it: a letter in lower case, a name as keys are named, 'ctrl+' in front with Ctrl
|
|
324
|
+
function editorKeySpelling(key)
|
|
325
|
+
{
|
|
326
|
+
const text = String(key), ctrl = text.trim().toLowerCase().startsWith('ctrl+');
|
|
327
|
+
let name = ctrl ? text.trim().slice(5) : text;
|
|
328
|
+
name = name === ' ' ? 'Space' : name.trim();
|
|
329
|
+
name = name.length === 1 ? name.toLowerCase() : editorKeyNames[name.toLowerCase()] ?? name;
|
|
330
|
+
return (ctrl ? 'ctrl+' : '') + name;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// say once, for each key of the game's own that is one of the open editor's too, that the game's takes its place:
|
|
334
|
+
// when the editor opens, or when the key is added while it is open, since the 2D and 3D editors' keys differ
|
|
335
|
+
function editorKeyClashes()
|
|
336
|
+
{
|
|
337
|
+
if (!levelEditor.isOpen) return;
|
|
338
|
+
for (const [spelled, key] of Object.entries(levelEditor.keys))
|
|
339
|
+
{
|
|
340
|
+
const ctrl = spelled.startsWith('ctrl+'), name = spelled.slice(ctrl ? 5 : 0);
|
|
341
|
+
// in 2D the digits 1 to 9 pick a layer
|
|
342
|
+
const clash = levelEditor.is3D ? editorOther.hasKey(name, ctrl) :
|
|
343
|
+
!!(ctrl ? editorCtrlKeys : editorKeys)[name] || !ctrl && name.length === 1 && name >= '1' && name <= '9';
|
|
344
|
+
if (key.warned || !clash) continue;
|
|
345
|
+
key.warned = true;
|
|
346
|
+
console.warn(`levelEditor.addKey: ${spelled} is a key of the editor's, the game's takes its place`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// the help lines of the game's own keys
|
|
351
|
+
function editorGameHelpLines()
|
|
352
|
+
{ return Object.values(levelEditor.keys).map((key)=> key.help).filter((line)=> !!line); }
|
|
353
|
+
|
|
354
|
+
// the tool of the game's own that is on, undefined when there is none
|
|
355
|
+
function editorGameTool() { return levelEditor.tool !== undefined ? levelEditor.tools[levelEditor.tool] : undefined; }
|
|
356
|
+
|
|
357
|
+
// the game's box of an editor's panel: a row of its tools and its buttons, made again when one was added, the
|
|
358
|
+
// tool that is on lit; and, the first time for a level editor, onPanel is given the box for controls of its own
|
|
359
|
+
const editorGamePanels = new WeakMap;
|
|
360
|
+
function editorGamePanel(box)
|
|
361
|
+
{
|
|
362
|
+
let state = editorGamePanels.get(box);
|
|
363
|
+
if (state?.owner !== levelEditor)
|
|
364
|
+
{
|
|
365
|
+
box.replaceChildren();
|
|
366
|
+
state = {owner: levelEditor, row: editorElement('div', box, 'display:flex;gap:4px;margin:4px 0;flex-wrap:wrap'),
|
|
367
|
+
key: '', buttons: {}};
|
|
368
|
+
editorGamePanels.set(box, state);
|
|
369
|
+
editorCall('onPanel', box);
|
|
370
|
+
}
|
|
371
|
+
const tools = Object.keys(levelEditor.tools);
|
|
372
|
+
const key = tools.map((name)=> name + '\t' + (levelEditor.tools[name].hint || '')).join('\n') + '\n\n' +
|
|
373
|
+
levelEditor.buttons.length;
|
|
374
|
+
if (state.key !== key)
|
|
375
|
+
{
|
|
376
|
+
state.key = key;
|
|
377
|
+
state.buttons = {};
|
|
378
|
+
const press = 'padding:3px 6px;cursor:pointer';
|
|
379
|
+
state.row.replaceChildren(...tools.map((name)=>
|
|
380
|
+
{
|
|
381
|
+
const b = state.buttons[name] = editorElement('button', undefined, press, name);
|
|
382
|
+
b.title = levelEditor.tools[name].hint || '';
|
|
383
|
+
b.onclick = ()=> { levelEditor.tool = levelEditor.tool === name ? undefined : name; b.blur?.(); };
|
|
384
|
+
return b;
|
|
385
|
+
}), ...levelEditor.buttons.map((button)=>
|
|
386
|
+
{
|
|
387
|
+
const b = editorElement('button', undefined, press, button.label);
|
|
388
|
+
b.title = button.title;
|
|
389
|
+
b.onclick = ()=> { button.onClick.call(levelEditor); b.blur?.(); };
|
|
390
|
+
return b;
|
|
391
|
+
}));
|
|
392
|
+
}
|
|
393
|
+
for (const name of tools)
|
|
394
|
+
state.buttons[name].style.outline = name === levelEditor.tool ? '2px solid #4af' : '';
|
|
395
|
+
}
|
|
96
396
|
|
|
97
397
|
// if the editor is open, and if it was opened and not exited, while Escape switches between playing and editing
|
|
98
398
|
let editorIsOpen = false, editorSession = false;
|
|
@@ -104,7 +404,7 @@ let editorPlayFromMouse = false, editorAdvanced = false;
|
|
|
104
404
|
function editorPlay(pos)
|
|
105
405
|
{
|
|
106
406
|
editorSetOpen(false);
|
|
107
|
-
editorPlayFromMouse && pos && levelEditor.onPlayFrom
|
|
407
|
+
editorPlayFromMouse && pos && editorHas('onPlayFrom') && levelEditor.onPlayFrom(pos.copy());
|
|
108
408
|
}
|
|
109
409
|
|
|
110
410
|
// the game's pause and camera from before the editor opened, handed back when it closes
|
|
@@ -156,10 +456,13 @@ function editorSetOpen(open)
|
|
|
156
456
|
layers.filter((layer)=> !layer.isObjects).at(-1));
|
|
157
457
|
layers.includes(editorObjectLayer) || (editorObjectLayer = editorLayer ? undefined :
|
|
158
458
|
layers.find((layer)=> layer.isObjects)); // a level of objects alone
|
|
459
|
+
editorCall('onOpen');
|
|
460
|
+
editorKeyClashes();
|
|
159
461
|
}
|
|
160
462
|
else
|
|
161
463
|
{
|
|
162
464
|
editorStrokeEnd();
|
|
465
|
+
editorToolHeld = false;
|
|
163
466
|
editorSelection = editorSelectionDrag = editorRightPress = editorObjectDrag = editorObjectBox = undefined;
|
|
164
467
|
editorObjectSelection.clear();
|
|
165
468
|
editorMouseOnPanel = false; // the panel hides, with no mouseleave
|
|
@@ -168,6 +471,7 @@ function editorSetOpen(open)
|
|
|
168
471
|
setCameraPos(state.cameraPos);
|
|
169
472
|
setCameraScale(state.cameraScale);
|
|
170
473
|
setCameraAngle(state.cameraAngle);
|
|
474
|
+
editorCall('onClose');
|
|
171
475
|
}
|
|
172
476
|
}
|
|
173
477
|
|
|
@@ -247,7 +551,7 @@ function editorMapRestore(map)
|
|
|
247
551
|
if (!map.layers || data.some((layer)=> !Array.isArray(layer))) return map;
|
|
248
552
|
const objects = editorObjectGroups(map.layers).map((group)=> group.objects ?? []);
|
|
249
553
|
const url = editorFetchedURLs.get(map), hash = editorMapHash(data, objects, editorMapLayout(map));
|
|
250
|
-
const fileName = url
|
|
554
|
+
const fileName = editorFileName(url, 'level.json');
|
|
251
555
|
const record = {map, url, fileName, key: editorMapKey(map, url, hash), hash,
|
|
252
556
|
original: data.map((layer)=> [...layer]), originalObjects: editorObjectsCopy(objects),
|
|
253
557
|
originalSize: {width: map.width, height: map.height}, layers: []};
|
|
@@ -278,6 +582,8 @@ function editorMapRestore(map)
|
|
|
278
582
|
editorRestoreObjects(map, saved);
|
|
279
583
|
console.warn(`LittleJS editor: brought back unsaved edits to ${record.fileName}, ` +
|
|
280
584
|
'Save in the editor (Esc then 0) writes them to the file');
|
|
585
|
+
saved.stale && console.warn(`LittleJS editor: they are older than the last edits to ${record.fileName}, ` +
|
|
586
|
+
'which storage had no room for');
|
|
281
587
|
}
|
|
282
588
|
return map;
|
|
283
589
|
}
|
|
@@ -438,7 +744,14 @@ async function editorSave(record, pickAgain=false)
|
|
|
438
744
|
if (!record) return;
|
|
439
745
|
editorStrokeEnd();
|
|
440
746
|
const text = editorMapJSON(record); // the map as it is now, later edits wait for a later save
|
|
441
|
-
|
|
747
|
+
// its place in line is taken now, and the game's hook is asked when its turn comes, so saves are written in
|
|
748
|
+
// the order they were asked for however long a hook takes
|
|
749
|
+
const saved = (async ()=>
|
|
750
|
+
{
|
|
751
|
+
await record.saving;
|
|
752
|
+
if (await editorCall('onSave', text, record.fileName) === true) return 'kept'; // the game kept it itself
|
|
753
|
+
return editorSaveText(record, text, pickAgain);
|
|
754
|
+
})();
|
|
442
755
|
record.saving = saved.catch(()=> {});
|
|
443
756
|
return saved;
|
|
444
757
|
}
|
|
@@ -472,6 +785,7 @@ async function editorSaveText(record, text, pickAgain)
|
|
|
472
785
|
if (error?.name === 'AbortError') return; // the picker was closed, nothing saved
|
|
473
786
|
record.fileHandle = undefined; // a file it could not write, a download instead, and not kept
|
|
474
787
|
editorFileStore.set(editorFileKey(record), undefined);
|
|
788
|
+
console.warn(`LittleJS editor: ${record.fileName} could not be written, downloaded instead`, error);
|
|
475
789
|
}
|
|
476
790
|
}
|
|
477
791
|
saveText(text, record.fileName, 'application/json');
|
|
@@ -488,15 +802,35 @@ const editorSaves = ()=> readSaveData(editorSaveName(), {});
|
|
|
488
802
|
// if the last autosave did not fit in storage, the panel says so
|
|
489
803
|
let editorSaveFailed = false;
|
|
490
804
|
|
|
491
|
-
// write the autosaves, noting whether storage took them
|
|
492
|
-
|
|
805
|
+
// write the autosaves, noting whether storage took them; when it did not, the autosave it still has of this map
|
|
806
|
+
// is an older one, and is marked so, for the reload that brings it back to say so
|
|
807
|
+
function editorWriteSaves(saves, key, name=editorSaveName())
|
|
493
808
|
{
|
|
494
809
|
try
|
|
495
810
|
{
|
|
496
|
-
localStorage.setItem(
|
|
497
|
-
editorSaveFailed = false;
|
|
811
|
+
localStorage.setItem(name, JSON.stringify(saves));
|
|
812
|
+
return editorSaveFailed = false;
|
|
498
813
|
}
|
|
499
814
|
catch { editorSaveFailed = true; }
|
|
815
|
+
try
|
|
816
|
+
{
|
|
817
|
+
const kept = readSaveData(name, {});
|
|
818
|
+
if (!kept[key] || kept[key].stale) return true;
|
|
819
|
+
kept[key].stale = true;
|
|
820
|
+
localStorage.setItem(name, JSON.stringify(kept));
|
|
821
|
+
}
|
|
822
|
+
catch {} // no room for the mark either
|
|
823
|
+
return true;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// the name of the file a level was fetched from, as it is on disk: without its folder and query, and with what
|
|
827
|
+
// the url spells with a percent as it is written, so it is the name a picked file has
|
|
828
|
+
function editorFileName(url, fallback)
|
|
829
|
+
{
|
|
830
|
+
const name = url?.split(/[?#]/)[0].split('/').pop();
|
|
831
|
+
if (!name) return fallback;
|
|
832
|
+
try { return decodeURIComponent(name); }
|
|
833
|
+
catch { return name; }
|
|
500
834
|
}
|
|
501
835
|
|
|
502
836
|
// a map's name for its autosave, the file it was fetched from without a query, or its size, layer names and data as loaded; a map with
|
|
@@ -552,17 +886,17 @@ function editorAutosave(record)
|
|
|
552
886
|
else
|
|
553
887
|
saves[record.key] = {hash: record.hash, savedHash: record.savedHash, width: map.width, height: map.height,
|
|
554
888
|
layers: data, objects, nextobjectid: map.nextobjectid};
|
|
555
|
-
editorWriteSaves(saves);
|
|
889
|
+
editorWriteSaves(saves, record.key);
|
|
556
890
|
}
|
|
557
891
|
|
|
558
892
|
// 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
|
|
559
893
|
// object layers' objects from a list of them; data of another size resizes the map, which needs the Restart hook
|
|
560
|
-
function editorPaintData(record, data, objects, width=record.map.width, height=record.map.height)
|
|
894
|
+
function editorPaintData(record, data, objects, width=record.map.width, height=record.map.height, keepUnknown=false)
|
|
561
895
|
{
|
|
562
896
|
editorStrokeEnd();
|
|
563
897
|
if (width !== record.map.width || height !== record.map.height)
|
|
564
898
|
{
|
|
565
|
-
if (!
|
|
899
|
+
if (!editorHas('onRestart')) return;
|
|
566
900
|
const before = editorMapSnapshot(record);
|
|
567
901
|
editorSetMapSnapshot(record, {width, height, layers: data, objects});
|
|
568
902
|
editorMapChanged(record, before);
|
|
@@ -577,7 +911,9 @@ function editorPaintData(record, data, objects, width=record.map.width, height=r
|
|
|
577
911
|
if (gids?.length === width * height)
|
|
578
912
|
gids.forEach((gid, i)=> editorPaint(layer, vec2(i % width, height - 1 - (i / width | 0)), gid));
|
|
579
913
|
}
|
|
580
|
-
|
|
914
|
+
// an autosave from before the file had an object layer leaves that layer as the file has it; Reset to file
|
|
915
|
+
// empties a layer the file did not have
|
|
916
|
+
editorObjectLayers(record).forEach((layer, i)=> objects && !(keepUnknown && i >= objects.length) &&
|
|
581
917
|
editorChangeObjects(layer, (list)=> list.splice(0, list.length, ...editorObjectsCopy(objects[i] ?? []))));
|
|
582
918
|
});
|
|
583
919
|
editorStroke ? editorStrokeEnd() : editorAutosave(record);
|
|
@@ -597,7 +933,7 @@ function editorApplyPending(record)
|
|
|
597
933
|
return false;
|
|
598
934
|
}
|
|
599
935
|
record.pending = record.pendingUnfit = undefined;
|
|
600
|
-
editorPaintData(record, saved.layers, saved.objects, saved.width, saved.height);
|
|
936
|
+
editorPaintData(record, saved.layers, saved.objects, saved.width, saved.height, true);
|
|
601
937
|
return true;
|
|
602
938
|
}
|
|
603
939
|
|
|
@@ -607,7 +943,7 @@ function editorPendingFits(record, saved)
|
|
|
607
943
|
{
|
|
608
944
|
const map = record.map, data = editorTileLayerData(map.layers), groups = editorObjectGroups(map.layers).length;
|
|
609
945
|
const width = saved.width ?? map.width, height = saved.height ?? map.height;
|
|
610
|
-
return (width === map.width && height === map.height ||
|
|
946
|
+
return (width === map.width && height === map.height || editorHas('onRestart')) &&
|
|
611
947
|
saved.layers?.length === data.length && saved.layers.every((layer)=> layer.length === width * height) &&
|
|
612
948
|
(saved.objects ?? []).every((objects, i)=> i < groups || !objects.length);
|
|
613
949
|
}
|
|
@@ -702,8 +1038,7 @@ function editorClearSelections()
|
|
|
702
1038
|
function editorMapChanged(record, before)
|
|
703
1039
|
{
|
|
704
1040
|
const stroke = [{resize: record, before, after: editorMapSnapshot(record)}];
|
|
705
|
-
|
|
706
|
-
editorRedoList.length = 0;
|
|
1041
|
+
editorUndoPush(stroke);
|
|
707
1042
|
editorChanged(stroke);
|
|
708
1043
|
}
|
|
709
1044
|
|
|
@@ -711,7 +1046,7 @@ function editorMapChanged(record, before)
|
|
|
711
1046
|
function editorResize(record, width, height)
|
|
712
1047
|
{
|
|
713
1048
|
const map = record?.map;
|
|
714
|
-
if (!map || record.synthetic || record.pending || !
|
|
1049
|
+
if (!map || record.synthetic || record.pending || !editorHas('onRestart')) return false;
|
|
715
1050
|
if (width === map.width && height === map.height) return false;
|
|
716
1051
|
editorStrokeEnd();
|
|
717
1052
|
const before = editorMapSnapshot(record);
|
|
@@ -765,7 +1100,7 @@ function editorSetCell(layer, pos, gid)
|
|
|
765
1100
|
}
|
|
766
1101
|
const t = editorGidToTile(gid);
|
|
767
1102
|
live.setData(pos, t ? new TileLayerData(t.tile, t.direction, t.mirror, color) : new TileLayerData, true);
|
|
768
|
-
if (
|
|
1103
|
+
if (editorHas('onTile'))
|
|
769
1104
|
levelEditor.onTile(live, pos.copy(), t?.tile);
|
|
770
1105
|
else if (live instanceof TileCollisionLayer && live.isSolid)
|
|
771
1106
|
live.setCollisionData(pos, gid ? 1 : 0); // as tileLayersLoad gives it
|
|
@@ -780,12 +1115,20 @@ function editorPaint(layer, pos, gid)
|
|
|
780
1115
|
(editorStroke ||= []).push({layer, pos: pos.copy(), before, after: gid});
|
|
781
1116
|
}
|
|
782
1117
|
|
|
1118
|
+
// an edit goes on the undo list, which keeps the last 100, as the 3D editor's does: a fill of a large layer is
|
|
1119
|
+
// an entry for every cell, and a session of them would hold millions
|
|
1120
|
+
function editorUndoPush(stroke)
|
|
1121
|
+
{
|
|
1122
|
+
editorUndoList.push(stroke);
|
|
1123
|
+
editorUndoList.length > 100 && editorUndoList.shift();
|
|
1124
|
+
editorRedoList.length = 0;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
783
1127
|
// the stroke is done, it can be undone as one
|
|
784
1128
|
function editorStrokeEnd()
|
|
785
1129
|
{
|
|
786
1130
|
if (!editorStroke) return;
|
|
787
|
-
|
|
788
|
-
editorRedoList.length = 0;
|
|
1131
|
+
editorUndoPush(editorStroke);
|
|
789
1132
|
editorChanged(editorStroke);
|
|
790
1133
|
editorStroke = undefined;
|
|
791
1134
|
}
|
|
@@ -834,7 +1177,7 @@ function editorChanged(stroke)
|
|
|
834
1177
|
editorRedraw(stroke);
|
|
835
1178
|
for (const record of new Set(stroke.map(editorEntryRecord)))
|
|
836
1179
|
editorAutosave(record);
|
|
837
|
-
stroke.some((entry)=> entry.resize) && levelEditor.onRestart
|
|
1180
|
+
stroke.some((entry)=> entry.resize) && editorHas('onRestart') && levelEditor.onRestart();
|
|
838
1181
|
}
|
|
839
1182
|
|
|
840
1183
|
// the tile layers a stroke touched draw again whole
|
|
@@ -1027,6 +1370,7 @@ let editorObjectDrag, editorObjectBox;
|
|
|
1027
1370
|
function editorSelectLayer(layer)
|
|
1028
1371
|
{
|
|
1029
1372
|
editorStrokeEnd();
|
|
1373
|
+
levelEditor.tool = undefined; // a tool of the game's own is put down
|
|
1030
1374
|
editorLastCell = editorObjectDrag = editorRightPress = editorObjectBox = undefined; // presses stay on their layer
|
|
1031
1375
|
editorSelectionDrag = undefined; // a move so far is kept, the stroke ended
|
|
1032
1376
|
editorObjectSelection.clear();
|
|
@@ -1194,7 +1538,7 @@ function editorUpdateObjects(space)
|
|
|
1194
1538
|
if (!editorObjectSelection.has(hit.id))
|
|
1195
1539
|
editorObjectSelection = new Set([hit.id]);
|
|
1196
1540
|
editorObjectDrag = {start: snap(mouse), from: new Map(editorSelectedObjects().map((object)=>
|
|
1197
|
-
[object.id,
|
|
1541
|
+
[object.id, {x: object.x, y: object.y}]))};
|
|
1198
1542
|
}
|
|
1199
1543
|
else if (editorObjectSelection.size)
|
|
1200
1544
|
editorObjectSelection.clear();
|
|
@@ -1209,8 +1553,10 @@ function editorUpdateObjects(space)
|
|
|
1209
1553
|
{
|
|
1210
1554
|
for (const object of list)
|
|
1211
1555
|
{
|
|
1212
|
-
|
|
1213
|
-
from
|
|
1556
|
+
// from the numbers it had, in the map's own units: through cells and back they pick up a hair
|
|
1557
|
+
const from = drag.from.get(object.id), {tilewidth=1, tileheight=1} = record.map;
|
|
1558
|
+
if (from)
|
|
1559
|
+
object.x = from.x + delta.x * tilewidth, object.y = from.y - delta.y * tileheight;
|
|
1214
1560
|
}
|
|
1215
1561
|
});
|
|
1216
1562
|
}
|
|
@@ -1654,6 +2000,7 @@ const editorHelpLines =
|
|
|
1654
2000
|
function editorHint()
|
|
1655
2001
|
{
|
|
1656
2002
|
if (keyIsDown('Space')) return 'Drag to pan';
|
|
2003
|
+
if (editorGameTool()) return editorGameTool().hint || levelEditor.tool;
|
|
1657
2004
|
if (editorObjectLayer)
|
|
1658
2005
|
return editorObjectSelection.size ? 'Selection: drag moves · Delete removes · Ctrl+C copy · Ctrl+X cut · click to clear' :
|
|
1659
2006
|
'Left place / select · drag moves · Right pick / drag select · Delete removes';
|
|
@@ -1722,6 +2069,8 @@ function editorPanelInit()
|
|
|
1722
2069
|
button(undo, 'Undo', ()=> editorUndo(), 'Ctrl+Z');
|
|
1723
2070
|
button(undo, 'Redo', ()=> editorUndo(true), 'Ctrl+Y');
|
|
1724
2071
|
button(undo, 'Keys', ()=> editorHelp = !editorHelp, 'Every control, ?');
|
|
2072
|
+
// the game's own tools, buttons and controls, near the top where the tools are
|
|
2073
|
+
const game = editorElement('div', editorPanel);
|
|
1725
2074
|
|
|
1726
2075
|
// a file that changed under its autosave
|
|
1727
2076
|
const pending = editorElement('div', editorPanel, 'padding:4px;margin:4px 0;background:#630;border-radius:3px');
|
|
@@ -1808,9 +2157,10 @@ function editorPanelInit()
|
|
|
1808
2157
|
const help = editorElement('div', editorPanel, 'color:#aaa;margin-top:4px;border-top:1px solid #444;padding-top:4px');
|
|
1809
2158
|
for (const line of editorHelpLines)
|
|
1810
2159
|
editorElement('div', help, 'margin:2px 0', line);
|
|
2160
|
+
const gameHelp = editorElement('div', help);
|
|
1811
2161
|
button(help, 'Close', ()=> editorHelp = false, '?');
|
|
1812
2162
|
|
|
1813
|
-
editorPanelParts = {pendingUnfit, reset, advancedToggle, advanced, playFromLabel, playFrom, sizeRow, sizeX, sizeY, resize, restart, pending, layerRow, allLayers, turns, palette, brush, properties, status, storage, hint, help, layers: undefined};
|
|
2163
|
+
editorPanelParts = {game, gameHelp, pendingUnfit, reset, advancedToggle, advanced, playFromLabel, playFrom, sizeRow, sizeX, sizeY, resize, restart, pending, layerRow, allLayers, turns, palette, brush, properties, status, storage, hint, help, layers: undefined};
|
|
1814
2164
|
}
|
|
1815
2165
|
|
|
1816
2166
|
// the palette's cell size in pixels and how many to a row
|
|
@@ -1969,15 +2319,15 @@ function editorPanelUpdate()
|
|
|
1969
2319
|
p.hint.textContent = editorHint();
|
|
1970
2320
|
p.help.style.display = editorHelp ? '' : 'none';
|
|
1971
2321
|
p.storage.style.display = editorSaveFailed ? '' : 'none';
|
|
1972
|
-
p.restart.style.display =
|
|
2322
|
+
p.restart.style.display = editorHas('onRestart') ? '' : 'none';
|
|
1973
2323
|
p.advancedToggle.textContent = editorAdvanced ? 'Advanced ▾' : 'Advanced ▸';
|
|
1974
2324
|
p.advanced.style.display = editorAdvanced ? '' : 'none';
|
|
1975
|
-
p.playFromLabel.style.display =
|
|
2325
|
+
p.playFromLabel.style.display = editorHas('onPlayFrom') ? 'flex' : 'none';
|
|
1976
2326
|
p.playFrom.checked = editorPlayFromMouse;
|
|
1977
2327
|
|
|
1978
2328
|
// the level's size, shown until a field is being typed in; without the Restart hook it can not change
|
|
1979
2329
|
const record = editorRecord(), map = record?.map;
|
|
1980
|
-
const canResize =
|
|
2330
|
+
const canResize = editorHas('onRestart') && !record?.synthetic && !record?.pending;
|
|
1981
2331
|
p.sizeRow.style.display = map ? 'flex' : 'none';
|
|
1982
2332
|
if (map && !p.sizeRow.contains(document.activeElement))
|
|
1983
2333
|
{
|
|
@@ -2047,6 +2397,7 @@ function editorIsTextField(target)
|
|
|
2047
2397
|
function editorOnKeyDown(e)
|
|
2048
2398
|
{
|
|
2049
2399
|
if (!editorIsOpen || e.repeat || e.altKey || editorIsTextField(e.target)) return;
|
|
2400
|
+
if (editorToolHeld) return; // the keys wait for a tool of the game's own to let go
|
|
2050
2401
|
if (editorSelectionDrag && !(debugKey && e.code === debugKey))
|
|
2051
2402
|
{
|
|
2052
2403
|
// a key ends a selection drag where it is, an undo of its own, so Delete or undo acts on what is there;
|
|
@@ -2054,10 +2405,12 @@ function editorOnKeyDown(e)
|
|
|
2054
2405
|
editorStrokeEnd();
|
|
2055
2406
|
editorSelectionDrag = undefined;
|
|
2056
2407
|
}
|
|
2057
|
-
let key = e.key.length === 1 ? e.key.toLowerCase() : e.key;
|
|
2058
|
-
if (key.length === 1 && !/[a-z?]/.test(key))
|
|
2408
|
+
let key = e.key === ' ' ? 'Space' : e.key.length === 1 ? e.key.toLowerCase() : e.key;
|
|
2409
|
+
if (key.length === 1 && !/[a-z?]/.test(key) && !levelEditor.keys[key])
|
|
2059
2410
|
key = e.code?.match(/^Key([A-Z])$/)?.[1].toLowerCase() ?? key;
|
|
2060
|
-
|
|
2411
|
+
// a key of the game's own first, it takes the place of one of the editor's
|
|
2412
|
+
const ctrl = e.ctrlKey || e.metaKey, own = levelEditor.keys[(ctrl ? 'ctrl+' : '') + key];
|
|
2413
|
+
const action = own ? (shift)=> own.action.call(levelEditor, shift) : (ctrl ? editorCtrlKeys : editorKeys)[key];
|
|
2061
2414
|
if (action && action(e.shiftKey) !== false)
|
|
2062
2415
|
e.preventDefault();
|
|
2063
2416
|
}
|
|
@@ -2251,6 +2604,15 @@ function editorUpdate()
|
|
|
2251
2604
|
return;
|
|
2252
2605
|
}
|
|
2253
2606
|
|
|
2607
|
+
// Escape while a tool of the game's own is held takes its press back, and stays in the editor
|
|
2608
|
+
if (editorToolHeld && debugKey && keyWasPressed(debugKey))
|
|
2609
|
+
{
|
|
2610
|
+
inputClearKey(debugKey);
|
|
2611
|
+
editorStrokeCancel();
|
|
2612
|
+
editorToolHeld = false;
|
|
2613
|
+
return;
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2254
2616
|
// in an editing session Escape, the debug key, switches between playing and editing; taken, so the debug
|
|
2255
2617
|
// overlay waits till the session ends
|
|
2256
2618
|
if (editorSession && debugKey && keyWasPressed(debugKey))
|
|
@@ -2281,7 +2643,7 @@ function editorUpdate()
|
|
|
2281
2643
|
// keeps them while it is open
|
|
2282
2644
|
for (let i = 1; !debugOverlay && i <= 9; ++i)
|
|
2283
2645
|
{
|
|
2284
|
-
if (!keyWasPressed('Digit' + i)) continue;
|
|
2646
|
+
if (!keyWasPressed('Digit' + i) || levelEditor.keys[i]) continue; // a digit of the game's own is its
|
|
2285
2647
|
inputClearKey('Digit' + i);
|
|
2286
2648
|
const layer = editorLayers()[i - 1];
|
|
2287
2649
|
layer && layer !== (editorObjectLayer ?? editorLayer) && editorSelectLayer(layer); // a drag does not carry across
|
|
@@ -2294,6 +2656,8 @@ function editorUpdate()
|
|
|
2294
2656
|
editorApplyCamera();
|
|
2295
2657
|
|
|
2296
2658
|
editorMouseOnPanel || (editorMouseScreen = mousePosScreen.copy());
|
|
2659
|
+
editorCall('onUpdate');
|
|
2660
|
+
if (editorToolUpdate(space)) return; // a tool of the game's own has the mouse
|
|
2297
2661
|
if (editorObjectLayer)
|
|
2298
2662
|
return editorUpdateObjects(space);
|
|
2299
2663
|
const layer = editorLayer?.live.destroyed ? undefined : editorLayer, mouse = screenToWorld(editorMouseScreen);
|
|
@@ -2363,10 +2727,87 @@ function editorUpdate()
|
|
|
2363
2727
|
}
|
|
2364
2728
|
}
|
|
2365
2729
|
|
|
2730
|
+
// a tool of the game's own: if its press is held, and what was picked in the editor when it was turned on, since
|
|
2731
|
+
// picking a layer, a tile or an object type puts the game's tool down
|
|
2732
|
+
let editorToolHeld = false, editorToolName, editorToolPicked = [];
|
|
2733
|
+
|
|
2734
|
+
// what a tool's callbacks are given: where the mouse is in the level, the cell under it on the selected tile
|
|
2735
|
+
// layer, and the keys held
|
|
2736
|
+
function editorToolAt()
|
|
2737
|
+
{
|
|
2738
|
+
const layer = editorObjectLayer || editorLayer?.live.destroyed ? undefined : editorLayer;
|
|
2739
|
+
const pos = screenToWorld(editorMouseScreen);
|
|
2740
|
+
return {pos, cell: layer && !editorMouseOnPanel ? editorCellAt(layer.live, pos) || undefined : undefined,
|
|
2741
|
+
ray: undefined, shift: keyIsDown('ShiftLeft') || keyIsDown('ShiftRight'),
|
|
2742
|
+
ctrl: keyIsDown('ControlLeft') || keyIsDown('ControlRight')};
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
// the game's tool, when one is on: the left button is its, a press, a drag and a release one undo, the right
|
|
2746
|
+
// button takes a held press back; true when it has the mouse
|
|
2747
|
+
function editorToolUpdate(space)
|
|
2748
|
+
{
|
|
2749
|
+
const picked = [editorLayer, editorObjectLayer, editorBrush, editorObjectBrush];
|
|
2750
|
+
if (levelEditor.tool !== editorToolName)
|
|
2751
|
+
editorToolName = levelEditor.tool, editorToolPicked = picked; // turned on, or off, by its key or button
|
|
2752
|
+
else if (picked.some((value, i)=> value !== editorToolPicked[i]))
|
|
2753
|
+
levelEditor.tool = editorToolName = undefined; // something of the editor's was picked
|
|
2754
|
+
const tool = editorGameTool();
|
|
2755
|
+
if (!tool)
|
|
2756
|
+
{
|
|
2757
|
+
editorToolHeld = false;
|
|
2758
|
+
return false;
|
|
2759
|
+
}
|
|
2760
|
+
const at = editorToolAt();
|
|
2761
|
+
editorHover = at.cell;
|
|
2762
|
+
editorRightPress = undefined; // a right press from before the tool was on is not the editor's to finish
|
|
2763
|
+
if (editorToolHeld && mouseWasPressed(2))
|
|
2764
|
+
{
|
|
2765
|
+
inputClearKey(2);
|
|
2766
|
+
editorStrokeCancel();
|
|
2767
|
+
editorToolHeld = false;
|
|
2768
|
+
}
|
|
2769
|
+
else if (mouseWasPressed(0) && !space && !editorMouseOnPanel)
|
|
2770
|
+
editorToolHeld = tool.onPress?.(at) !== false;
|
|
2771
|
+
else if (editorToolHeld && mouseIsDown(0))
|
|
2772
|
+
tool.onDrag?.(at);
|
|
2773
|
+
if (editorToolHeld && !mouseIsDown(0))
|
|
2774
|
+
{
|
|
2775
|
+
tool.onRelease?.(at);
|
|
2776
|
+
editorStrokeEnd();
|
|
2777
|
+
editorToolHeld = false;
|
|
2778
|
+
}
|
|
2779
|
+
return true;
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
// the 2D editor's edit functions, what levelEditor.edit2D gives a game's own keys, buttons and tools
|
|
2783
|
+
const editorEdit2D =
|
|
2784
|
+
{
|
|
2785
|
+
get map() { return editorRecord()?.map; },
|
|
2786
|
+
get layer() { return editorObjectLayer ? undefined : editorLayer?.live; },
|
|
2787
|
+
get objects() { return editorObjectLayer ? editorObjectsCopy(editorObjectList(editorObjectLayer)) : []; },
|
|
2788
|
+
get hover() { return editorHover && !editorObjectLayer ? editorHover.copy() : undefined; },
|
|
2789
|
+
get selection() { return editorObjectSelection; },
|
|
2790
|
+
paint(cell, tile, direction=0, mirror=false)
|
|
2791
|
+
{
|
|
2792
|
+
const layer = editorObjectLayer ? undefined : editorLayer, size = layer?.live.size;
|
|
2793
|
+
if (!layer || layer.live.destroyed || layer.record.pending || !isVector2(cell)) return false;
|
|
2794
|
+
const x = floor(cell.x), y = floor(cell.y);
|
|
2795
|
+
if (x < 0 || y < 0 || x >= size.x || y >= size.y) return false;
|
|
2796
|
+
editorPaint(layer, vec2(x, y), tile < 0 ? 0 : editorTileToGid(tile, direction & 3, mirror));
|
|
2797
|
+
return true;
|
|
2798
|
+
},
|
|
2799
|
+
changeObjects(change) { return editorChangeObjects(editorObjectLayer, change); },
|
|
2800
|
+
strokeEnd() { editorStrokeEnd(); },
|
|
2801
|
+
strokeCancel() { editorStrokeCancel(); },
|
|
2802
|
+
bulk(edit) { editorBulkEdit(edit); },
|
|
2803
|
+
undo(redo=false) { return editorUndo(redo); },
|
|
2804
|
+
toJSON() { const record = editorRecord(); return record ? editorMapJSON(record) : ''; },
|
|
2805
|
+
};
|
|
2806
|
+
|
|
2366
2807
|
// switch to play and have the game rebuild its level from the changed map, when it has a hook for that
|
|
2367
2808
|
function editorRestart()
|
|
2368
2809
|
{
|
|
2369
|
-
if (!
|
|
2810
|
+
if (!editorHas('onRestart')) return;
|
|
2370
2811
|
editorSetOpen(false); // ends a held stroke, and hands back the game's pause and camera, Escape comes back
|
|
2371
2812
|
levelEditor.onRestart();
|
|
2372
2813
|
}
|
|
@@ -2380,6 +2821,11 @@ function editorPreRender() { editorIsOpen && editorApplyCamera(); }
|
|
|
2380
2821
|
function editorRender()
|
|
2381
2822
|
{
|
|
2382
2823
|
headlessMode || editorPanelUpdate();
|
|
2824
|
+
if (!headlessMode && editorIsOpen && editorPanelParts)
|
|
2825
|
+
{
|
|
2826
|
+
editorGamePanel(editorPanelParts.game);
|
|
2827
|
+
editorGameHelp(editorPanelParts.gameHelp);
|
|
2828
|
+
}
|
|
2383
2829
|
if (!editorIsOpen || headlessMode) return;
|
|
2384
2830
|
const layer = editorLayer?.live.destroyed ? undefined : editorLayer;
|
|
2385
2831
|
if (!layer && !editorObjectLayer) return;
|
|
@@ -2428,12 +2874,12 @@ function editorRender()
|
|
|
2428
2874
|
if (editorSelection && !editorObjectLayer)
|
|
2429
2875
|
outline(editorSelection.min, editorSelection.max.add(vec2(1)), hsl(.15, 1, .6), thin * 3);
|
|
2430
2876
|
const last = editorLastPlaced, shift = keyIsDown('ShiftLeft') || keyIsDown('ShiftRight');
|
|
2431
|
-
if (shift && editorHover && last?.layer === layer && !mouseIsDown(0))
|
|
2877
|
+
if (shift && editorHover && last?.layer === layer && !mouseIsDown(0) && !editorGameTool())
|
|
2432
2878
|
drawLine(live.pos.add(last.pos).add(vec2(.5)), live.pos.add(editorHover).add(vec2(.5)), thin * 2,
|
|
2433
2879
|
hsl(.55, 1, .6, .6), undefined, 0, glEnable, false);
|
|
2434
2880
|
|
|
2435
|
-
// the brush where it would paint, the Erase brush's cells in red
|
|
2436
|
-
if (editorHover)
|
|
2881
|
+
// the brush where it would paint, the Erase brush's cells in red; a tool of the game's own draws its own
|
|
2882
|
+
if (editorHover && !editorGameTool())
|
|
2437
2883
|
{
|
|
2438
2884
|
const grid = editorStampGrid(layer, editorBrush), {width: w, height: h} = editorBrush;
|
|
2439
2885
|
for (let y = h; y--;)
|
|
@@ -2451,7 +2897,24 @@ function editorRender()
|
|
|
2451
2897
|
editorObjectLayer && editorRenderObjects(thin);
|
|
2452
2898
|
}
|
|
2453
2899
|
|
|
2454
|
-
|
|
2900
|
+
// the game's own drawing in the level, over the editor's: the editor's onDraw, and its tool's while one is on
|
|
2901
|
+
function editorRenderGame()
|
|
2902
|
+
{
|
|
2903
|
+
if (!editorIsOpen) return;
|
|
2904
|
+
editorCall('onDraw');
|
|
2905
|
+
editorGameTool()?.onDraw?.(editorToolAt());
|
|
2906
|
+
}
|
|
2907
|
+
|
|
2908
|
+
// the lines of the game's own keys in a panel's help, made again when they change
|
|
2909
|
+
function editorGameHelp(element)
|
|
2910
|
+
{
|
|
2911
|
+
const lines = editorGameHelpLines(), key = lines.join('\n');
|
|
2912
|
+
if (element.dataset.key === key) return;
|
|
2913
|
+
element.dataset.key = key;
|
|
2914
|
+
element.replaceChildren(...lines.map((line)=> editorElement('div', undefined, 'margin:2px 0', line)));
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
debug && engineAddPlugin(editorUpdate, ()=> { editorRender(); editorRenderGame(); });
|
|
2455
2918
|
|
|
2456
2919
|
// the editor's own listeners for letter shortcuts and the wheel, the engine's input reads keys by position
|
|
2457
2920
|
if (debug && globalThis.document?.addEventListener)
|