littlejsengine 1.21.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 +1258 -141
- package/dist/littlejs.esm.js +5023 -429
- package/dist/littlejs.esm.min.js +22 -9
- package/dist/littlejs.js +4977 -430
- package/dist/littlejs.min.js +21 -8
- package/dist/littlejs.release.js +2759 -313
- package/package.json +3 -2
- package/plugins/gltf.js +7 -1
- package/plugins/lightSystem.js +50 -5
- package/plugins/math3d.js +42 -20
- package/plugins/parallax.js +174 -0
- package/plugins/particleEffects.js +760 -0
- package/plugins/pluginExport.js +40 -0
- package/plugins/postProcess.js +206 -28
- package/plugins/render3d.js +495 -188
- package/plugins/render3dEditor.js +1754 -80
- package/plugins/render3dExtras.js +294 -21
- package/plugins/render3dLevel.js +463 -6
- package/plugins/sceneSystem.js +3 -2
- package/src/engine.js +1 -1
- package/src/engineAudio.js +25 -8
- package/src/engineBuild.mjs +2 -0
- package/src/engineDraw.js +59 -4
- package/src/engineEditor.js +475 -37
- package/src/engineExport.js +7 -0
- package/src/engineInput.js +48 -6
- package/src/engineParticles.js +80 -11
- package/src/engineRelease.js +13 -2
- package/src/engineSettings.js +14 -0
- package/src/engineWebGL.js +27 -10
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
|
|
|
@@ -432,12 +736,28 @@ function editorSetBaseline(record, written)
|
|
|
432
736
|
// save a map as Tiled JSON: where the browser lets a page write files, Chrome and Edge, to a file picked once and
|
|
433
737
|
// written again on each Save after, even after a reload once the browser gives permission, or picked again with
|
|
434
738
|
// Save As; elsewhere as a download under the name of the file it came from; resolves to how it saved, undefined
|
|
435
|
-
// when the picker was closed
|
|
739
|
+
// when the picker was closed; saves of a map run one at a time in the order asked, so the file ends with the last
|
|
436
740
|
async function editorSave(record, pickAgain=false)
|
|
437
741
|
{
|
|
438
742
|
if (!record) return;
|
|
439
743
|
editorStrokeEnd();
|
|
440
|
-
const text = editorMapJSON(record),
|
|
744
|
+
const text = editorMapJSON(record); // the map as it is now, later edits wait for a later save
|
|
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
|
+
})();
|
|
753
|
+
record.saving = saved.catch(()=> {});
|
|
754
|
+
return saved;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// write one save of a map, the one before it done
|
|
758
|
+
async function editorSaveText(record, text, pickAgain)
|
|
759
|
+
{
|
|
760
|
+
const picker = /** @type {any} */ (globalThis).showSaveFilePicker;
|
|
441
761
|
if (picker)
|
|
442
762
|
{
|
|
443
763
|
try
|
|
@@ -553,7 +873,7 @@ function editorPaintData(record, data, objects, width=record.map.width, height=r
|
|
|
553
873
|
editorStrokeEnd();
|
|
554
874
|
if (width !== record.map.width || height !== record.map.height)
|
|
555
875
|
{
|
|
556
|
-
if (!
|
|
876
|
+
if (!editorHas('onRestart')) return;
|
|
557
877
|
const before = editorMapSnapshot(record);
|
|
558
878
|
editorSetMapSnapshot(record, {width, height, layers: data, objects});
|
|
559
879
|
editorMapChanged(record, before);
|
|
@@ -598,7 +918,7 @@ function editorPendingFits(record, saved)
|
|
|
598
918
|
{
|
|
599
919
|
const map = record.map, data = editorTileLayerData(map.layers), groups = editorObjectGroups(map.layers).length;
|
|
600
920
|
const width = saved.width ?? map.width, height = saved.height ?? map.height;
|
|
601
|
-
return (width === map.width && height === map.height ||
|
|
921
|
+
return (width === map.width && height === map.height || editorHas('onRestart')) &&
|
|
602
922
|
saved.layers?.length === data.length && saved.layers.every((layer)=> layer.length === width * height) &&
|
|
603
923
|
(saved.objects ?? []).every((objects, i)=> i < groups || !objects.length);
|
|
604
924
|
}
|
|
@@ -702,7 +1022,7 @@ function editorMapChanged(record, before)
|
|
|
702
1022
|
function editorResize(record, width, height)
|
|
703
1023
|
{
|
|
704
1024
|
const map = record?.map;
|
|
705
|
-
if (!map || record.synthetic || record.pending || !
|
|
1025
|
+
if (!map || record.synthetic || record.pending || !editorHas('onRestart')) return false;
|
|
706
1026
|
if (width === map.width && height === map.height) return false;
|
|
707
1027
|
editorStrokeEnd();
|
|
708
1028
|
const before = editorMapSnapshot(record);
|
|
@@ -756,7 +1076,7 @@ function editorSetCell(layer, pos, gid)
|
|
|
756
1076
|
}
|
|
757
1077
|
const t = editorGidToTile(gid);
|
|
758
1078
|
live.setData(pos, t ? new TileLayerData(t.tile, t.direction, t.mirror, color) : new TileLayerData, true);
|
|
759
|
-
if (
|
|
1079
|
+
if (editorHas('onTile'))
|
|
760
1080
|
levelEditor.onTile(live, pos.copy(), t?.tile);
|
|
761
1081
|
else if (live instanceof TileCollisionLayer && live.isSolid)
|
|
762
1082
|
live.setCollisionData(pos, gid ? 1 : 0); // as tileLayersLoad gives it
|
|
@@ -825,7 +1145,7 @@ function editorChanged(stroke)
|
|
|
825
1145
|
editorRedraw(stroke);
|
|
826
1146
|
for (const record of new Set(stroke.map(editorEntryRecord)))
|
|
827
1147
|
editorAutosave(record);
|
|
828
|
-
stroke.some((entry)=> entry.resize) && levelEditor.onRestart
|
|
1148
|
+
stroke.some((entry)=> entry.resize) && editorHas('onRestart') && levelEditor.onRestart();
|
|
829
1149
|
}
|
|
830
1150
|
|
|
831
1151
|
// the tile layers a stroke touched draw again whole
|
|
@@ -1018,6 +1338,7 @@ let editorObjectDrag, editorObjectBox;
|
|
|
1018
1338
|
function editorSelectLayer(layer)
|
|
1019
1339
|
{
|
|
1020
1340
|
editorStrokeEnd();
|
|
1341
|
+
levelEditor.tool = undefined; // a tool of the game's own is put down
|
|
1021
1342
|
editorLastCell = editorObjectDrag = editorRightPress = editorObjectBox = undefined; // presses stay on their layer
|
|
1022
1343
|
editorSelectionDrag = undefined; // a move so far is kept, the stroke ended
|
|
1023
1344
|
editorObjectSelection.clear();
|
|
@@ -1645,6 +1966,7 @@ const editorHelpLines =
|
|
|
1645
1966
|
function editorHint()
|
|
1646
1967
|
{
|
|
1647
1968
|
if (keyIsDown('Space')) return 'Drag to pan';
|
|
1969
|
+
if (editorGameTool()) return editorGameTool().hint || levelEditor.tool;
|
|
1648
1970
|
if (editorObjectLayer)
|
|
1649
1971
|
return editorObjectSelection.size ? 'Selection: drag moves · Delete removes · Ctrl+C copy · Ctrl+X cut · click to clear' :
|
|
1650
1972
|
'Left place / select · drag moves · Right pick / drag select · Delete removes';
|
|
@@ -1713,6 +2035,8 @@ function editorPanelInit()
|
|
|
1713
2035
|
button(undo, 'Undo', ()=> editorUndo(), 'Ctrl+Z');
|
|
1714
2036
|
button(undo, 'Redo', ()=> editorUndo(true), 'Ctrl+Y');
|
|
1715
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);
|
|
1716
2040
|
|
|
1717
2041
|
// a file that changed under its autosave
|
|
1718
2042
|
const pending = editorElement('div', editorPanel, 'padding:4px;margin:4px 0;background:#630;border-radius:3px');
|
|
@@ -1799,9 +2123,10 @@ function editorPanelInit()
|
|
|
1799
2123
|
const help = editorElement('div', editorPanel, 'color:#aaa;margin-top:4px;border-top:1px solid #444;padding-top:4px');
|
|
1800
2124
|
for (const line of editorHelpLines)
|
|
1801
2125
|
editorElement('div', help, 'margin:2px 0', line);
|
|
2126
|
+
const gameHelp = editorElement('div', help);
|
|
1802
2127
|
button(help, 'Close', ()=> editorHelp = false, '?');
|
|
1803
2128
|
|
|
1804
|
-
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};
|
|
1805
2130
|
}
|
|
1806
2131
|
|
|
1807
2132
|
// the palette's cell size in pixels and how many to a row
|
|
@@ -1960,15 +2285,15 @@ function editorPanelUpdate()
|
|
|
1960
2285
|
p.hint.textContent = editorHint();
|
|
1961
2286
|
p.help.style.display = editorHelp ? '' : 'none';
|
|
1962
2287
|
p.storage.style.display = editorSaveFailed ? '' : 'none';
|
|
1963
|
-
p.restart.style.display =
|
|
2288
|
+
p.restart.style.display = editorHas('onRestart') ? '' : 'none';
|
|
1964
2289
|
p.advancedToggle.textContent = editorAdvanced ? 'Advanced ▾' : 'Advanced ▸';
|
|
1965
2290
|
p.advanced.style.display = editorAdvanced ? '' : 'none';
|
|
1966
|
-
p.playFromLabel.style.display =
|
|
2291
|
+
p.playFromLabel.style.display = editorHas('onPlayFrom') ? 'flex' : 'none';
|
|
1967
2292
|
p.playFrom.checked = editorPlayFromMouse;
|
|
1968
2293
|
|
|
1969
2294
|
// the level's size, shown until a field is being typed in; without the Restart hook it can not change
|
|
1970
2295
|
const record = editorRecord(), map = record?.map;
|
|
1971
|
-
const canResize =
|
|
2296
|
+
const canResize = editorHas('onRestart') && !record?.synthetic && !record?.pending;
|
|
1972
2297
|
p.sizeRow.style.display = map ? 'flex' : 'none';
|
|
1973
2298
|
if (map && !p.sizeRow.contains(document.activeElement))
|
|
1974
2299
|
{
|
|
@@ -2038,6 +2363,7 @@ function editorIsTextField(target)
|
|
|
2038
2363
|
function editorOnKeyDown(e)
|
|
2039
2364
|
{
|
|
2040
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
|
|
2041
2367
|
if (editorSelectionDrag && !(debugKey && e.code === debugKey))
|
|
2042
2368
|
{
|
|
2043
2369
|
// a key ends a selection drag where it is, an undo of its own, so Delete or undo acts on what is there;
|
|
@@ -2045,10 +2371,12 @@ function editorOnKeyDown(e)
|
|
|
2045
2371
|
editorStrokeEnd();
|
|
2046
2372
|
editorSelectionDrag = undefined;
|
|
2047
2373
|
}
|
|
2048
|
-
let key = e.key.length === 1 ? e.key.toLowerCase() : e.key;
|
|
2049
|
-
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])
|
|
2050
2376
|
key = e.code?.match(/^Key([A-Z])$/)?.[1].toLowerCase() ?? key;
|
|
2051
|
-
|
|
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];
|
|
2052
2380
|
if (action && action(e.shiftKey) !== false)
|
|
2053
2381
|
e.preventDefault();
|
|
2054
2382
|
}
|
|
@@ -2242,6 +2570,15 @@ function editorUpdate()
|
|
|
2242
2570
|
return;
|
|
2243
2571
|
}
|
|
2244
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
|
+
|
|
2245
2582
|
// in an editing session Escape, the debug key, switches between playing and editing; taken, so the debug
|
|
2246
2583
|
// overlay waits till the session ends
|
|
2247
2584
|
if (editorSession && debugKey && keyWasPressed(debugKey))
|
|
@@ -2272,7 +2609,7 @@ function editorUpdate()
|
|
|
2272
2609
|
// keeps them while it is open
|
|
2273
2610
|
for (let i = 1; !debugOverlay && i <= 9; ++i)
|
|
2274
2611
|
{
|
|
2275
|
-
if (!keyWasPressed('Digit' + i)) continue;
|
|
2612
|
+
if (!keyWasPressed('Digit' + i) || levelEditor.keys[i]) continue; // a digit of the game's own is its
|
|
2276
2613
|
inputClearKey('Digit' + i);
|
|
2277
2614
|
const layer = editorLayers()[i - 1];
|
|
2278
2615
|
layer && layer !== (editorObjectLayer ?? editorLayer) && editorSelectLayer(layer); // a drag does not carry across
|
|
@@ -2285,6 +2622,8 @@ function editorUpdate()
|
|
|
2285
2622
|
editorApplyCamera();
|
|
2286
2623
|
|
|
2287
2624
|
editorMouseOnPanel || (editorMouseScreen = mousePosScreen.copy());
|
|
2625
|
+
editorCall('onUpdate');
|
|
2626
|
+
if (editorToolUpdate(space)) return; // a tool of the game's own has the mouse
|
|
2288
2627
|
if (editorObjectLayer)
|
|
2289
2628
|
return editorUpdateObjects(space);
|
|
2290
2629
|
const layer = editorLayer?.live.destroyed ? undefined : editorLayer, mouse = screenToWorld(editorMouseScreen);
|
|
@@ -2354,10 +2693,87 @@ function editorUpdate()
|
|
|
2354
2693
|
}
|
|
2355
2694
|
}
|
|
2356
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
|
+
|
|
2357
2773
|
// switch to play and have the game rebuild its level from the changed map, when it has a hook for that
|
|
2358
2774
|
function editorRestart()
|
|
2359
2775
|
{
|
|
2360
|
-
if (!
|
|
2776
|
+
if (!editorHas('onRestart')) return;
|
|
2361
2777
|
editorSetOpen(false); // ends a held stroke, and hands back the game's pause and camera, Escape comes back
|
|
2362
2778
|
levelEditor.onRestart();
|
|
2363
2779
|
}
|
|
@@ -2371,6 +2787,11 @@ function editorPreRender() { editorIsOpen && editorApplyCamera(); }
|
|
|
2371
2787
|
function editorRender()
|
|
2372
2788
|
{
|
|
2373
2789
|
headlessMode || editorPanelUpdate();
|
|
2790
|
+
if (!headlessMode && editorIsOpen && editorPanelParts)
|
|
2791
|
+
{
|
|
2792
|
+
editorGamePanel(editorPanelParts.game);
|
|
2793
|
+
editorGameHelp(editorPanelParts.gameHelp);
|
|
2794
|
+
}
|
|
2374
2795
|
if (!editorIsOpen || headlessMode) return;
|
|
2375
2796
|
const layer = editorLayer?.live.destroyed ? undefined : editorLayer;
|
|
2376
2797
|
if (!layer && !editorObjectLayer) return;
|
|
@@ -2419,12 +2840,12 @@ function editorRender()
|
|
|
2419
2840
|
if (editorSelection && !editorObjectLayer)
|
|
2420
2841
|
outline(editorSelection.min, editorSelection.max.add(vec2(1)), hsl(.15, 1, .6), thin * 3);
|
|
2421
2842
|
const last = editorLastPlaced, shift = keyIsDown('ShiftLeft') || keyIsDown('ShiftRight');
|
|
2422
|
-
if (shift && editorHover && last?.layer === layer && !mouseIsDown(0))
|
|
2843
|
+
if (shift && editorHover && last?.layer === layer && !mouseIsDown(0) && !editorGameTool())
|
|
2423
2844
|
drawLine(live.pos.add(last.pos).add(vec2(.5)), live.pos.add(editorHover).add(vec2(.5)), thin * 2,
|
|
2424
2845
|
hsl(.55, 1, .6, .6), undefined, 0, glEnable, false);
|
|
2425
2846
|
|
|
2426
|
-
// the brush where it would paint, the Erase brush's cells in red
|
|
2427
|
-
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())
|
|
2428
2849
|
{
|
|
2429
2850
|
const grid = editorStampGrid(layer, editorBrush), {width: w, height: h} = editorBrush;
|
|
2430
2851
|
for (let y = h; y--;)
|
|
@@ -2442,7 +2863,24 @@ function editorRender()
|
|
|
2442
2863
|
editorObjectLayer && editorRenderObjects(thin);
|
|
2443
2864
|
}
|
|
2444
2865
|
|
|
2445
|
-
|
|
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(); });
|
|
2446
2884
|
|
|
2447
2885
|
// the editor's own listeners for letter shortcuts and the wheel, the engine's input reads keys by position
|
|
2448
2886
|
if (debug && globalThis.document?.addEventListener)
|