littlejsengine 1.22.0 → 1.23.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/README.md +1 -1
- package/dist/littlejs.d.ts +633 -94
- package/dist/littlejs.esm.js +2203 -222
- package/dist/littlejs.esm.min.js +3 -3
- package/dist/littlejs.js +2190 -222
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +716 -76
- package/package.json +1 -1
- 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/render3d.js +15 -7
- package/plugins/render3dEditor.js +1021 -111
- package/plugins/render3dExtras.js +82 -21
- package/plugins/render3dLevel.js +300 -6
- package/src/engine.js +1 -1
- package/src/engineAudio.js +10 -2
- package/src/engineBuild.mjs +1 -0
- package/src/engineDraw.js +15 -3
- package/src/engineEditor.js +465 -36
- package/src/engineExport.js +5 -0
- package/src/engineInput.js +48 -6
- package/src/engineRelease.js +13 -2
- package/src/engineSettings.js +14 -0
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
|
|
185
|
+
* @param {string} text
|
|
186
|
+
* @param {string} fileName
|
|
187
|
+
* @return {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
|
|
|
@@ -438,7 +742,14 @@ async function editorSave(record, pickAgain=false)
|
|
|
438
742
|
if (!record) return;
|
|
439
743
|
editorStrokeEnd();
|
|
440
744
|
const text = editorMapJSON(record); // the map as it is now, later edits wait for a later save
|
|
441
|
-
|
|
745
|
+
// its place in line is taken now, and the game's hook is asked when its turn comes, so saves are written in
|
|
746
|
+
// the order they were asked for however long a hook takes
|
|
747
|
+
const saved = (async ()=>
|
|
748
|
+
{
|
|
749
|
+
await record.saving;
|
|
750
|
+
if (await editorCall('onSave', text, record.fileName) === true) return 'kept'; // the game kept it itself
|
|
751
|
+
return editorSaveText(record, text, pickAgain);
|
|
752
|
+
})();
|
|
442
753
|
record.saving = saved.catch(()=> {});
|
|
443
754
|
return saved;
|
|
444
755
|
}
|
|
@@ -562,7 +873,7 @@ function editorPaintData(record, data, objects, width=record.map.width, height=r
|
|
|
562
873
|
editorStrokeEnd();
|
|
563
874
|
if (width !== record.map.width || height !== record.map.height)
|
|
564
875
|
{
|
|
565
|
-
if (!
|
|
876
|
+
if (!editorHas('onRestart')) return;
|
|
566
877
|
const before = editorMapSnapshot(record);
|
|
567
878
|
editorSetMapSnapshot(record, {width, height, layers: data, objects});
|
|
568
879
|
editorMapChanged(record, before);
|
|
@@ -607,7 +918,7 @@ function editorPendingFits(record, saved)
|
|
|
607
918
|
{
|
|
608
919
|
const map = record.map, data = editorTileLayerData(map.layers), groups = editorObjectGroups(map.layers).length;
|
|
609
920
|
const width = saved.width ?? map.width, height = saved.height ?? map.height;
|
|
610
|
-
return (width === map.width && height === map.height ||
|
|
921
|
+
return (width === map.width && height === map.height || editorHas('onRestart')) &&
|
|
611
922
|
saved.layers?.length === data.length && saved.layers.every((layer)=> layer.length === width * height) &&
|
|
612
923
|
(saved.objects ?? []).every((objects, i)=> i < groups || !objects.length);
|
|
613
924
|
}
|
|
@@ -711,7 +1022,7 @@ function editorMapChanged(record, before)
|
|
|
711
1022
|
function editorResize(record, width, height)
|
|
712
1023
|
{
|
|
713
1024
|
const map = record?.map;
|
|
714
|
-
if (!map || record.synthetic || record.pending || !
|
|
1025
|
+
if (!map || record.synthetic || record.pending || !editorHas('onRestart')) return false;
|
|
715
1026
|
if (width === map.width && height === map.height) return false;
|
|
716
1027
|
editorStrokeEnd();
|
|
717
1028
|
const before = editorMapSnapshot(record);
|
|
@@ -765,7 +1076,7 @@ function editorSetCell(layer, pos, gid)
|
|
|
765
1076
|
}
|
|
766
1077
|
const t = editorGidToTile(gid);
|
|
767
1078
|
live.setData(pos, t ? new TileLayerData(t.tile, t.direction, t.mirror, color) : new TileLayerData, true);
|
|
768
|
-
if (
|
|
1079
|
+
if (editorHas('onTile'))
|
|
769
1080
|
levelEditor.onTile(live, pos.copy(), t?.tile);
|
|
770
1081
|
else if (live instanceof TileCollisionLayer && live.isSolid)
|
|
771
1082
|
live.setCollisionData(pos, gid ? 1 : 0); // as tileLayersLoad gives it
|
|
@@ -834,7 +1145,7 @@ function editorChanged(stroke)
|
|
|
834
1145
|
editorRedraw(stroke);
|
|
835
1146
|
for (const record of new Set(stroke.map(editorEntryRecord)))
|
|
836
1147
|
editorAutosave(record);
|
|
837
|
-
stroke.some((entry)=> entry.resize) && levelEditor.onRestart
|
|
1148
|
+
stroke.some((entry)=> entry.resize) && editorHas('onRestart') && levelEditor.onRestart();
|
|
838
1149
|
}
|
|
839
1150
|
|
|
840
1151
|
// the tile layers a stroke touched draw again whole
|
|
@@ -1027,6 +1338,7 @@ let editorObjectDrag, editorObjectBox;
|
|
|
1027
1338
|
function editorSelectLayer(layer)
|
|
1028
1339
|
{
|
|
1029
1340
|
editorStrokeEnd();
|
|
1341
|
+
levelEditor.tool = undefined; // a tool of the game's own is put down
|
|
1030
1342
|
editorLastCell = editorObjectDrag = editorRightPress = editorObjectBox = undefined; // presses stay on their layer
|
|
1031
1343
|
editorSelectionDrag = undefined; // a move so far is kept, the stroke ended
|
|
1032
1344
|
editorObjectSelection.clear();
|
|
@@ -1654,6 +1966,7 @@ const editorHelpLines =
|
|
|
1654
1966
|
function editorHint()
|
|
1655
1967
|
{
|
|
1656
1968
|
if (keyIsDown('Space')) return 'Drag to pan';
|
|
1969
|
+
if (editorGameTool()) return editorGameTool().hint || levelEditor.tool;
|
|
1657
1970
|
if (editorObjectLayer)
|
|
1658
1971
|
return editorObjectSelection.size ? 'Selection: drag moves · Delete removes · Ctrl+C copy · Ctrl+X cut · click to clear' :
|
|
1659
1972
|
'Left place / select · drag moves · Right pick / drag select · Delete removes';
|
|
@@ -1722,6 +2035,8 @@ function editorPanelInit()
|
|
|
1722
2035
|
button(undo, 'Undo', ()=> editorUndo(), 'Ctrl+Z');
|
|
1723
2036
|
button(undo, 'Redo', ()=> editorUndo(true), 'Ctrl+Y');
|
|
1724
2037
|
button(undo, 'Keys', ()=> editorHelp = !editorHelp, 'Every control, ?');
|
|
2038
|
+
// the game's own tools, buttons and controls, near the top where the tools are
|
|
2039
|
+
const game = editorElement('div', editorPanel);
|
|
1725
2040
|
|
|
1726
2041
|
// a file that changed under its autosave
|
|
1727
2042
|
const pending = editorElement('div', editorPanel, 'padding:4px;margin:4px 0;background:#630;border-radius:3px');
|
|
@@ -1808,9 +2123,10 @@ function editorPanelInit()
|
|
|
1808
2123
|
const help = editorElement('div', editorPanel, 'color:#aaa;margin-top:4px;border-top:1px solid #444;padding-top:4px');
|
|
1809
2124
|
for (const line of editorHelpLines)
|
|
1810
2125
|
editorElement('div', help, 'margin:2px 0', line);
|
|
2126
|
+
const gameHelp = editorElement('div', help);
|
|
1811
2127
|
button(help, 'Close', ()=> editorHelp = false, '?');
|
|
1812
2128
|
|
|
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};
|
|
2129
|
+
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
2130
|
}
|
|
1815
2131
|
|
|
1816
2132
|
// the palette's cell size in pixels and how many to a row
|
|
@@ -1969,15 +2285,15 @@ function editorPanelUpdate()
|
|
|
1969
2285
|
p.hint.textContent = editorHint();
|
|
1970
2286
|
p.help.style.display = editorHelp ? '' : 'none';
|
|
1971
2287
|
p.storage.style.display = editorSaveFailed ? '' : 'none';
|
|
1972
|
-
p.restart.style.display =
|
|
2288
|
+
p.restart.style.display = editorHas('onRestart') ? '' : 'none';
|
|
1973
2289
|
p.advancedToggle.textContent = editorAdvanced ? 'Advanced ▾' : 'Advanced ▸';
|
|
1974
2290
|
p.advanced.style.display = editorAdvanced ? '' : 'none';
|
|
1975
|
-
p.playFromLabel.style.display =
|
|
2291
|
+
p.playFromLabel.style.display = editorHas('onPlayFrom') ? 'flex' : 'none';
|
|
1976
2292
|
p.playFrom.checked = editorPlayFromMouse;
|
|
1977
2293
|
|
|
1978
2294
|
// the level's size, shown until a field is being typed in; without the Restart hook it can not change
|
|
1979
2295
|
const record = editorRecord(), map = record?.map;
|
|
1980
|
-
const canResize =
|
|
2296
|
+
const canResize = editorHas('onRestart') && !record?.synthetic && !record?.pending;
|
|
1981
2297
|
p.sizeRow.style.display = map ? 'flex' : 'none';
|
|
1982
2298
|
if (map && !p.sizeRow.contains(document.activeElement))
|
|
1983
2299
|
{
|
|
@@ -2047,6 +2363,7 @@ function editorIsTextField(target)
|
|
|
2047
2363
|
function editorOnKeyDown(e)
|
|
2048
2364
|
{
|
|
2049
2365
|
if (!editorIsOpen || e.repeat || e.altKey || editorIsTextField(e.target)) return;
|
|
2366
|
+
if (editorToolHeld) return; // the keys wait for a tool of the game's own to let go
|
|
2050
2367
|
if (editorSelectionDrag && !(debugKey && e.code === debugKey))
|
|
2051
2368
|
{
|
|
2052
2369
|
// 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 +2371,12 @@ function editorOnKeyDown(e)
|
|
|
2054
2371
|
editorStrokeEnd();
|
|
2055
2372
|
editorSelectionDrag = undefined;
|
|
2056
2373
|
}
|
|
2057
|
-
let key = e.key.length === 1 ? e.key.toLowerCase() : e.key;
|
|
2058
|
-
if (key.length === 1 && !/[a-z?]/.test(key))
|
|
2374
|
+
let key = e.key === ' ' ? 'Space' : e.key.length === 1 ? e.key.toLowerCase() : e.key;
|
|
2375
|
+
if (key.length === 1 && !/[a-z?]/.test(key) && !levelEditor.keys[key])
|
|
2059
2376
|
key = e.code?.match(/^Key([A-Z])$/)?.[1].toLowerCase() ?? key;
|
|
2060
|
-
|
|
2377
|
+
// a key of the game's own first, it takes the place of one of the editor's
|
|
2378
|
+
const ctrl = e.ctrlKey || e.metaKey, own = levelEditor.keys[(ctrl ? 'ctrl+' : '') + key];
|
|
2379
|
+
const action = own ? (shift)=> own.action.call(levelEditor, shift) : (ctrl ? editorCtrlKeys : editorKeys)[key];
|
|
2061
2380
|
if (action && action(e.shiftKey) !== false)
|
|
2062
2381
|
e.preventDefault();
|
|
2063
2382
|
}
|
|
@@ -2251,6 +2570,15 @@ function editorUpdate()
|
|
|
2251
2570
|
return;
|
|
2252
2571
|
}
|
|
2253
2572
|
|
|
2573
|
+
// Escape while a tool of the game's own is held takes its press back, and stays in the editor
|
|
2574
|
+
if (editorToolHeld && debugKey && keyWasPressed(debugKey))
|
|
2575
|
+
{
|
|
2576
|
+
inputClearKey(debugKey);
|
|
2577
|
+
editorStrokeCancel();
|
|
2578
|
+
editorToolHeld = false;
|
|
2579
|
+
return;
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2254
2582
|
// in an editing session Escape, the debug key, switches between playing and editing; taken, so the debug
|
|
2255
2583
|
// overlay waits till the session ends
|
|
2256
2584
|
if (editorSession && debugKey && keyWasPressed(debugKey))
|
|
@@ -2281,7 +2609,7 @@ function editorUpdate()
|
|
|
2281
2609
|
// keeps them while it is open
|
|
2282
2610
|
for (let i = 1; !debugOverlay && i <= 9; ++i)
|
|
2283
2611
|
{
|
|
2284
|
-
if (!keyWasPressed('Digit' + i)) continue;
|
|
2612
|
+
if (!keyWasPressed('Digit' + i) || levelEditor.keys[i]) continue; // a digit of the game's own is its
|
|
2285
2613
|
inputClearKey('Digit' + i);
|
|
2286
2614
|
const layer = editorLayers()[i - 1];
|
|
2287
2615
|
layer && layer !== (editorObjectLayer ?? editorLayer) && editorSelectLayer(layer); // a drag does not carry across
|
|
@@ -2294,6 +2622,8 @@ function editorUpdate()
|
|
|
2294
2622
|
editorApplyCamera();
|
|
2295
2623
|
|
|
2296
2624
|
editorMouseOnPanel || (editorMouseScreen = mousePosScreen.copy());
|
|
2625
|
+
editorCall('onUpdate');
|
|
2626
|
+
if (editorToolUpdate(space)) return; // a tool of the game's own has the mouse
|
|
2297
2627
|
if (editorObjectLayer)
|
|
2298
2628
|
return editorUpdateObjects(space);
|
|
2299
2629
|
const layer = editorLayer?.live.destroyed ? undefined : editorLayer, mouse = screenToWorld(editorMouseScreen);
|
|
@@ -2363,10 +2693,87 @@ function editorUpdate()
|
|
|
2363
2693
|
}
|
|
2364
2694
|
}
|
|
2365
2695
|
|
|
2696
|
+
// 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
|
|
2697
|
+
// picking a layer, a tile or an object type puts the game's tool down
|
|
2698
|
+
let editorToolHeld = false, editorToolName, editorToolPicked = [];
|
|
2699
|
+
|
|
2700
|
+
// what a tool's callbacks are given: where the mouse is in the level, the cell under it on the selected tile
|
|
2701
|
+
// layer, and the keys held
|
|
2702
|
+
function editorToolAt()
|
|
2703
|
+
{
|
|
2704
|
+
const layer = editorObjectLayer || editorLayer?.live.destroyed ? undefined : editorLayer;
|
|
2705
|
+
const pos = screenToWorld(editorMouseScreen);
|
|
2706
|
+
return {pos, cell: layer && !editorMouseOnPanel ? editorCellAt(layer.live, pos) || undefined : undefined,
|
|
2707
|
+
ray: undefined, shift: keyIsDown('ShiftLeft') || keyIsDown('ShiftRight'),
|
|
2708
|
+
ctrl: keyIsDown('ControlLeft') || keyIsDown('ControlRight')};
|
|
2709
|
+
}
|
|
2710
|
+
|
|
2711
|
+
// the game's tool, when one is on: the left button is its, a press, a drag and a release one undo, the right
|
|
2712
|
+
// button takes a held press back; true when it has the mouse
|
|
2713
|
+
function editorToolUpdate(space)
|
|
2714
|
+
{
|
|
2715
|
+
const picked = [editorLayer, editorObjectLayer, editorBrush, editorObjectBrush];
|
|
2716
|
+
if (levelEditor.tool !== editorToolName)
|
|
2717
|
+
editorToolName = levelEditor.tool, editorToolPicked = picked; // turned on, or off, by its key or button
|
|
2718
|
+
else if (picked.some((value, i)=> value !== editorToolPicked[i]))
|
|
2719
|
+
levelEditor.tool = editorToolName = undefined; // something of the editor's was picked
|
|
2720
|
+
const tool = editorGameTool();
|
|
2721
|
+
if (!tool)
|
|
2722
|
+
{
|
|
2723
|
+
editorToolHeld = false;
|
|
2724
|
+
return false;
|
|
2725
|
+
}
|
|
2726
|
+
const at = editorToolAt();
|
|
2727
|
+
editorHover = at.cell;
|
|
2728
|
+
editorRightPress = undefined; // a right press from before the tool was on is not the editor's to finish
|
|
2729
|
+
if (editorToolHeld && mouseWasPressed(2))
|
|
2730
|
+
{
|
|
2731
|
+
inputClearKey(2);
|
|
2732
|
+
editorStrokeCancel();
|
|
2733
|
+
editorToolHeld = false;
|
|
2734
|
+
}
|
|
2735
|
+
else if (mouseWasPressed(0) && !space && !editorMouseOnPanel)
|
|
2736
|
+
editorToolHeld = tool.onPress?.(at) !== false;
|
|
2737
|
+
else if (editorToolHeld && mouseIsDown(0))
|
|
2738
|
+
tool.onDrag?.(at);
|
|
2739
|
+
if (editorToolHeld && !mouseIsDown(0))
|
|
2740
|
+
{
|
|
2741
|
+
tool.onRelease?.(at);
|
|
2742
|
+
editorStrokeEnd();
|
|
2743
|
+
editorToolHeld = false;
|
|
2744
|
+
}
|
|
2745
|
+
return true;
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2748
|
+
// the 2D editor's edit functions, what levelEditor.edit2D gives a game's own keys, buttons and tools
|
|
2749
|
+
const editorEdit2D =
|
|
2750
|
+
{
|
|
2751
|
+
get map() { return editorRecord()?.map; },
|
|
2752
|
+
get layer() { return editorObjectLayer ? undefined : editorLayer?.live; },
|
|
2753
|
+
get objects() { return editorObjectLayer ? editorObjectsCopy(editorObjectList(editorObjectLayer)) : []; },
|
|
2754
|
+
get hover() { return editorHover && !editorObjectLayer ? editorHover.copy() : undefined; },
|
|
2755
|
+
get selection() { return editorObjectSelection; },
|
|
2756
|
+
paint(cell, tile, direction=0, mirror=false)
|
|
2757
|
+
{
|
|
2758
|
+
const layer = editorObjectLayer ? undefined : editorLayer, size = layer?.live.size;
|
|
2759
|
+
if (!layer || layer.live.destroyed || layer.record.pending || !isVector2(cell)) return false;
|
|
2760
|
+
const x = floor(cell.x), y = floor(cell.y);
|
|
2761
|
+
if (x < 0 || y < 0 || x >= size.x || y >= size.y) return false;
|
|
2762
|
+
editorPaint(layer, vec2(x, y), tile < 0 ? 0 : editorTileToGid(tile, direction & 3, mirror));
|
|
2763
|
+
return true;
|
|
2764
|
+
},
|
|
2765
|
+
changeObjects(change) { return editorChangeObjects(editorObjectLayer, change); },
|
|
2766
|
+
strokeEnd() { editorStrokeEnd(); },
|
|
2767
|
+
strokeCancel() { editorStrokeCancel(); },
|
|
2768
|
+
bulk(edit) { editorBulkEdit(edit); },
|
|
2769
|
+
undo(redo=false) { return editorUndo(redo); },
|
|
2770
|
+
toJSON() { const record = editorRecord(); return record ? editorMapJSON(record) : ''; },
|
|
2771
|
+
};
|
|
2772
|
+
|
|
2366
2773
|
// switch to play and have the game rebuild its level from the changed map, when it has a hook for that
|
|
2367
2774
|
function editorRestart()
|
|
2368
2775
|
{
|
|
2369
|
-
if (!
|
|
2776
|
+
if (!editorHas('onRestart')) return;
|
|
2370
2777
|
editorSetOpen(false); // ends a held stroke, and hands back the game's pause and camera, Escape comes back
|
|
2371
2778
|
levelEditor.onRestart();
|
|
2372
2779
|
}
|
|
@@ -2380,6 +2787,11 @@ function editorPreRender() { editorIsOpen && editorApplyCamera(); }
|
|
|
2380
2787
|
function editorRender()
|
|
2381
2788
|
{
|
|
2382
2789
|
headlessMode || editorPanelUpdate();
|
|
2790
|
+
if (!headlessMode && editorIsOpen && editorPanelParts)
|
|
2791
|
+
{
|
|
2792
|
+
editorGamePanel(editorPanelParts.game);
|
|
2793
|
+
editorGameHelp(editorPanelParts.gameHelp);
|
|
2794
|
+
}
|
|
2383
2795
|
if (!editorIsOpen || headlessMode) return;
|
|
2384
2796
|
const layer = editorLayer?.live.destroyed ? undefined : editorLayer;
|
|
2385
2797
|
if (!layer && !editorObjectLayer) return;
|
|
@@ -2428,12 +2840,12 @@ function editorRender()
|
|
|
2428
2840
|
if (editorSelection && !editorObjectLayer)
|
|
2429
2841
|
outline(editorSelection.min, editorSelection.max.add(vec2(1)), hsl(.15, 1, .6), thin * 3);
|
|
2430
2842
|
const last = editorLastPlaced, shift = keyIsDown('ShiftLeft') || keyIsDown('ShiftRight');
|
|
2431
|
-
if (shift && editorHover && last?.layer === layer && !mouseIsDown(0))
|
|
2843
|
+
if (shift && editorHover && last?.layer === layer && !mouseIsDown(0) && !editorGameTool())
|
|
2432
2844
|
drawLine(live.pos.add(last.pos).add(vec2(.5)), live.pos.add(editorHover).add(vec2(.5)), thin * 2,
|
|
2433
2845
|
hsl(.55, 1, .6, .6), undefined, 0, glEnable, false);
|
|
2434
2846
|
|
|
2435
|
-
// the brush where it would paint, the Erase brush's cells in red
|
|
2436
|
-
if (editorHover)
|
|
2847
|
+
// the brush where it would paint, the Erase brush's cells in red; a tool of the game's own draws its own
|
|
2848
|
+
if (editorHover && !editorGameTool())
|
|
2437
2849
|
{
|
|
2438
2850
|
const grid = editorStampGrid(layer, editorBrush), {width: w, height: h} = editorBrush;
|
|
2439
2851
|
for (let y = h; y--;)
|
|
@@ -2451,7 +2863,24 @@ function editorRender()
|
|
|
2451
2863
|
editorObjectLayer && editorRenderObjects(thin);
|
|
2452
2864
|
}
|
|
2453
2865
|
|
|
2454
|
-
|
|
2866
|
+
// the game's own drawing in the level, over the editor's: the editor's onDraw, and its tool's while one is on
|
|
2867
|
+
function editorRenderGame()
|
|
2868
|
+
{
|
|
2869
|
+
if (!editorIsOpen) return;
|
|
2870
|
+
editorCall('onDraw');
|
|
2871
|
+
editorGameTool()?.onDraw?.(editorToolAt());
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2874
|
+
// the lines of the game's own keys in a panel's help, made again when they change
|
|
2875
|
+
function editorGameHelp(element)
|
|
2876
|
+
{
|
|
2877
|
+
const lines = editorGameHelpLines(), key = lines.join('\n');
|
|
2878
|
+
if (element.dataset.key === key) return;
|
|
2879
|
+
element.dataset.key = key;
|
|
2880
|
+
element.replaceChildren(...lines.map((line)=> editorElement('div', undefined, 'margin:2px 0', line)));
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
debug && engineAddPlugin(editorUpdate, ()=> { editorRender(); editorRenderGame(); });
|
|
2455
2884
|
|
|
2456
2885
|
// the editor's own listeners for letter shortcuts and the wheel, the engine's input reads keys by position
|
|
2457
2886
|
if (debug && globalThis.document?.addEventListener)
|