littlejsengine 1.24.0 → 1.24.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +86 -1
- package/EDITOR.md +2 -0
- package/FAQ.md +13 -2
- package/README.md +2 -2
- package/REFERENCE.md +27 -10
- package/dist/littlejs.d.ts +104 -29
- package/dist/littlejs.esm.js +2499 -1979
- package/dist/littlejs.esm.min.js +3 -3
- package/dist/littlejs.js +2496 -1979
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +2438 -1983
- package/package.json +1 -1
- package/plugins/box2d.js +1 -1
- package/plugins/gltf.js +62 -9
- package/plugins/lightSystem.js +1 -1
- package/plugins/math3d.js +1014 -1013
- package/plugins/medalSystem.js +7 -11
- package/plugins/particleEffects.js +2 -2
- package/plugins/pathFinder.js +2 -2
- package/plugins/pluginExport.js +1 -0
- package/plugins/render3d.js +134 -57
- package/plugins/render3dEditor.js +28 -12
- package/plugins/render3dExtras.js +32 -17
- package/plugins/render3dLevel.js +21 -12
- package/plugins/render3dVoxels.js +15 -8
- package/plugins/textureSheet.js +97 -3
- package/plugins/tweakables.js +1 -1
- package/plugins/tweenSystem.js +2 -2
- package/plugins/uiSystem.js +6 -2
- package/src/engine.js +34 -11
- package/src/engineAudio.js +13 -3
- package/src/engineDebug.js +4 -1
- package/src/engineDraw.js +29 -16
- package/src/engineEditor.js +99 -56
- package/src/engineExport.js +2 -0
- package/src/engineInput.js +17 -3
- package/src/engineMath.js +6 -2
- package/src/engineObject.js +675 -675
- package/src/engineParticles.js +1 -1
- package/src/engineSettings.js +21 -1
- package/src/engineTileLayer.js +142 -43
- package/src/engineUtilities.js +20 -8
- package/src/engineWebGL.js +12 -8
package/src/engineDraw.js
CHANGED
|
@@ -182,6 +182,10 @@ class TileInfo
|
|
|
182
182
|
this.padding = padding;
|
|
183
183
|
/** @property {TextureInfo} - The texture info for this tile */
|
|
184
184
|
this.textureInfo = textureInfo;
|
|
185
|
+
/** @property {Array<TileInfo>|undefined} - A tile set's tiles, each wherever it was packed, as loadTiles makes
|
|
186
|
+
* them: a tile layer given this tile info draws its tile n from tiles[n] and not from a grid on one sheet
|
|
187
|
+
* @type {Array<TileInfo>|undefined} */
|
|
188
|
+
this.tiles = undefined;
|
|
185
189
|
/** @property {number} - Shrinks tile by this many pixels to prevent neighbors bleeding */
|
|
186
190
|
this.bleed = bleed;
|
|
187
191
|
/** @property {number} - How many frames per row for frame(), 0 to keep frames on a single row */
|
|
@@ -868,13 +872,15 @@ function drawLineList(points, width=.1, color=WHITE, wrap=false, pos=vec2(), ang
|
|
|
868
872
|
if (useWebGL && glEnable)
|
|
869
873
|
{
|
|
870
874
|
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
871
|
-
let
|
|
875
|
+
let sx = 1, sy = 1;
|
|
872
876
|
if (screenSpace)
|
|
873
877
|
{
|
|
874
878
|
if (glSkipScreenSpace) return;
|
|
875
|
-
|
|
879
|
+
let size;
|
|
880
|
+
[pos, size, angle] = screenToWorldTransform(pos, vec2(1), angle);
|
|
881
|
+
sx = size.x, sy = size.y;
|
|
876
882
|
}
|
|
877
|
-
glDrawOutlineTransform(points, color.rgbaInt(), width, pos.x, pos.y,
|
|
883
|
+
glDrawOutlineTransform(points, color.rgbaInt(), width, pos.x, pos.y, sx, sy, angle, wrap);
|
|
878
884
|
}
|
|
879
885
|
else
|
|
880
886
|
{
|
|
@@ -940,17 +946,22 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
|
|
|
940
946
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
941
947
|
ASSERT(isNumber(sides), 'sides must be a number');
|
|
942
948
|
|
|
943
|
-
// build regular polygon points
|
|
944
|
-
const points =
|
|
949
|
+
// build regular polygon points, into vectors kept for the next, as drawPoly is done with them when it returns
|
|
950
|
+
const points = drawRegularPolyPoints, pool = drawRegularPolyPool;
|
|
951
|
+
points.length = 0;
|
|
945
952
|
const sizeX = size.x/2, sizeY = size.y/2;
|
|
946
953
|
for (let i=sides; i-- > 0;) // a count that is not whole, or below zero, still ends
|
|
947
954
|
{
|
|
948
|
-
const a = (i/sides)*PI*2;
|
|
949
|
-
|
|
955
|
+
const a = (i/sides)*PI*2, point = pool[points.length] ||= vec2();
|
|
956
|
+
point.x = sin(a)*sizeX, point.y = cos(a)*sizeY;
|
|
957
|
+
points.push(point);
|
|
950
958
|
}
|
|
951
959
|
drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebGL, screenSpace, context);
|
|
952
960
|
}
|
|
953
961
|
|
|
962
|
+
// the points drawRegularPoly draws and the vectors it writes them into
|
|
963
|
+
const drawRegularPolyPoints = [], drawRegularPolyPool = [];
|
|
964
|
+
|
|
954
965
|
/** Draw colored polygon using passed in points
|
|
955
966
|
* - WebGL fills a polygon whose edges do not cross, concave or not; a self crossing one, like a star through its
|
|
956
967
|
* outer points, fills wrong there, so draw it as its simple outline or in parts
|
|
@@ -978,15 +989,17 @@ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(),
|
|
|
978
989
|
if (useWebGL && glEnable)
|
|
979
990
|
{
|
|
980
991
|
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
981
|
-
let
|
|
992
|
+
let sx = 1, sy = 1;
|
|
982
993
|
if (screenSpace)
|
|
983
994
|
{
|
|
984
995
|
if (glSkipScreenSpace) return;
|
|
985
|
-
|
|
996
|
+
let size;
|
|
997
|
+
[pos, size, angle] = screenToWorldTransform(pos, vec2(1), angle);
|
|
998
|
+
sx = size.x, sy = size.y;
|
|
986
999
|
}
|
|
987
|
-
glDrawPointsTransform(points, color.rgbaInt(), pos.x, pos.y,
|
|
1000
|
+
glDrawPointsTransform(points, color.rgbaInt(), pos.x, pos.y, sx, sy, angle);
|
|
988
1001
|
if (lineWidth > 0)
|
|
989
|
-
glDrawOutlineTransform(points, lineColor.rgbaInt(), lineWidth, pos.x, pos.y,
|
|
1002
|
+
glDrawOutlineTransform(points, lineColor.rgbaInt(), lineWidth, pos.x, pos.y, sx, sy, angle);
|
|
990
1003
|
}
|
|
991
1004
|
else
|
|
992
1005
|
{
|
|
@@ -1631,7 +1644,7 @@ function tintImageData(data, color, additiveColor)
|
|
|
1631
1644
|
if (additiveColor && !isBlack(additiveColor))
|
|
1632
1645
|
{
|
|
1633
1646
|
// multiply + additive (slower), color.a is baked into the alpha channel here
|
|
1634
|
-
const colorMultiply = [color.r, color.g, color.b, color.a];
|
|
1647
|
+
const colorMultiply = [clamp(color.r), clamp(color.g), clamp(color.b), clamp(color.a)];
|
|
1635
1648
|
const colorAdd = [additiveColor.r * 255, additiveColor.g * 255,
|
|
1636
1649
|
additiveColor.b * 255, additiveColor.a * 255];
|
|
1637
1650
|
for (let i = 0; i < data.length; ++i)
|
|
@@ -1642,9 +1655,9 @@ function tintImageData(data, color, additiveColor)
|
|
|
1642
1655
|
// RGB only, faster — alpha left intact for the caller
|
|
1643
1656
|
for (let i = 0; i < data.length; i+=4)
|
|
1644
1657
|
{
|
|
1645
|
-
data[i ] *= color.r;
|
|
1646
|
-
data[i+1] *= color.g;
|
|
1647
|
-
data[i+2] *= color.b;
|
|
1658
|
+
data[i ] *= clamp(color.r);
|
|
1659
|
+
data[i+1] *= clamp(color.g);
|
|
1660
|
+
data[i+2] *= clamp(color.b);
|
|
1648
1661
|
}
|
|
1649
1662
|
return false;
|
|
1650
1663
|
}
|
|
@@ -1896,7 +1909,7 @@ function engineGlowTexture(falloff=1)
|
|
|
1896
1909
|
ASSERT(isNumber(falloff) && falloff > 0, 'glowFalloff must be a number above 0');
|
|
1897
1910
|
const key = max(round(falloff * 10), 1) / 10;
|
|
1898
1911
|
let texture = engineGlowTextures.get(key);
|
|
1899
|
-
if (texture || !glContext ||
|
|
1912
|
+
if (texture || !glContext || !canvasAvailable()) return texture;
|
|
1900
1913
|
const size = 64, context = createCanvasContext(size), steps = 16;
|
|
1901
1914
|
const gradient = context.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2);
|
|
1902
1915
|
for (let i = 0; i <= steps; ++i)
|
package/src/engineEditor.js
CHANGED
|
@@ -453,7 +453,7 @@ function editorSetOpen(open)
|
|
|
453
453
|
// the layer edited last, when it is still there, or the collision layer
|
|
454
454
|
const layers = editorLayers();
|
|
455
455
|
layers.includes(editorLayer) || (editorLayer = layers.find((layer)=> layer.live?.isSolid) ??
|
|
456
|
-
layers.filter((layer)=> !layer.isObjects).
|
|
456
|
+
layers.filter((layer)=> !layer.isObjects).pop());
|
|
457
457
|
layers.includes(editorObjectLayer) || (editorObjectLayer = editorLayer ? undefined :
|
|
458
458
|
layers.find((layer)=> layer.isObjects)); // a level of objects alone
|
|
459
459
|
editorCall('onOpen');
|
|
@@ -570,8 +570,8 @@ function editorMapRestore(map)
|
|
|
570
570
|
if ((saved.width ?? map.width) === map.width && (saved.height ?? map.height) === map.height &&
|
|
571
571
|
editorSameData(saved.layers, data) && editorSameData(saved.objects ?? [], objects))
|
|
572
572
|
editorDiscardPending(record);
|
|
573
|
-
else if (!sameFile || !
|
|
574
|
-
!editorCopyData(data, saved.layers))
|
|
573
|
+
else if (!sameFile || !editorSavedFits(saved, data.length, saved.width ?? map.width, saved.height ?? map.height) ||
|
|
574
|
+
!editorResizeMap(map, saved.width ?? map.width, saved.height ?? map.height) || !editorCopyData(data, saved.layers))
|
|
575
575
|
{
|
|
576
576
|
record.pending = saved;
|
|
577
577
|
console.warn(`LittleJS editor: ${record.fileName} changed since its autosaved edits, ` +
|
|
@@ -654,7 +654,7 @@ function editorLayerRecord(live)
|
|
|
654
654
|
function editorTilesets(live)
|
|
655
655
|
{
|
|
656
656
|
const tileInfo = live.tileInfo, image = tileInfo?.textureInfo?.image;
|
|
657
|
-
if (!image) return [];
|
|
657
|
+
if (!image || tileInfo.tiles) return []; // a tile set is packed from many images, none of them a Tiled tileset
|
|
658
658
|
const {x: tilewidth, y: tileheight} = tileInfo.size, padding = tileInfo.padding;
|
|
659
659
|
const columns = tileInfo.columns || floor(image.width / (tilewidth + padding*2));
|
|
660
660
|
const rows = floor(image.height / (tileheight + padding*2));
|
|
@@ -670,7 +670,17 @@ function editorJSONFetched(url, json)
|
|
|
670
670
|
{ json && typeof json === 'object' && editorFetchedURLs.set(json, String(url)); }
|
|
671
671
|
|
|
672
672
|
// a map as Tiled JSON, everything it was loaded with kept, the tile data as the editor left it
|
|
673
|
-
function editorMapJSON(record)
|
|
673
|
+
function editorMapJSON(record)
|
|
674
|
+
{
|
|
675
|
+
// an object layer the editor made for a first object, every object of which was taken back, is left out
|
|
676
|
+
const map = record.map, made = map.layers.find((layer)=> editorMadeGroups.has(layer) && !layer.objects.length);
|
|
677
|
+
if (!made) return JSON.stringify(map);
|
|
678
|
+
return JSON.stringify({...map, layers: map.layers.filter((layer)=> layer !== made),
|
|
679
|
+
...(map.nextlayerid === made.id + 1 && {nextlayerid: made.id})});
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// the object layers the editor made in a map, for a map that had none
|
|
683
|
+
const editorMadeGroups = new WeakSet;
|
|
674
684
|
|
|
675
685
|
// the files picked to save maps to, kept across reloads in IndexedDB by page and map, which is where a browser
|
|
676
686
|
// lets a page keep them; each resolves to undefined when the store can not be used
|
|
@@ -870,6 +880,11 @@ function editorCopyData(data, saved)
|
|
|
870
880
|
return true;
|
|
871
881
|
}
|
|
872
882
|
|
|
883
|
+
// are an autosave's tiles a layer for each of the map's, each of its own size, so the map is resized only for tiles
|
|
884
|
+
// that fit it
|
|
885
|
+
const editorSavedFits = (saved, count, width, height)=> isArray(saved.layers) && saved.layers.length === count &&
|
|
886
|
+
saved.layers.every((layer)=> isArray(layer) && layer.length === width * height);
|
|
887
|
+
|
|
873
888
|
// remember a map's tile data and objects, or forget them when they are back to the file
|
|
874
889
|
function editorAutosave(record)
|
|
875
890
|
{
|
|
@@ -877,7 +892,7 @@ function editorAutosave(record)
|
|
|
877
892
|
const saves = editorSaves(), map = record.map, data = editorTileLayerData(map.layers);
|
|
878
893
|
const objects = editorObjectGroups(map.layers).map((group)=> group.objects ?? []);
|
|
879
894
|
const original = record.originalObjects ?? [], kept = objects.slice();
|
|
880
|
-
while (kept.length > original.length && !kept.
|
|
895
|
+
while (kept.length > original.length && !kept[kept.length - 1].length)
|
|
881
896
|
kept.pop(); // an Objects layer the editor made, empty again, is not an edit
|
|
882
897
|
const size = record.originalSize, sameSize = map.width === size.width && map.height === size.height;
|
|
883
898
|
// the map as it was loaded has nothing to keep; one a Save wrote is kept until a reload shows the file has it
|
|
@@ -898,7 +913,7 @@ function editorPaintData(record, data, objects, width=record.map.width, height=r
|
|
|
898
913
|
{
|
|
899
914
|
if (!editorHas('onRestart')) return;
|
|
900
915
|
const before = editorMapSnapshot(record);
|
|
901
|
-
editorSetMapSnapshot(record, {width, height, layers: data, objects});
|
|
916
|
+
editorSetMapSnapshot(record, {width, height, layers: data, objects}, keepUnknown);
|
|
902
917
|
editorMapChanged(record, before);
|
|
903
918
|
return;
|
|
904
919
|
}
|
|
@@ -1015,13 +1030,15 @@ function editorMapSnapshot(record)
|
|
|
1015
1030
|
return editorObjectsCopy({width: map.width, height: map.height, layers: editorTileLayerData(map.layers), objects});
|
|
1016
1031
|
}
|
|
1017
1032
|
|
|
1018
|
-
// give a map a size, tiles and objects, the game's Restart hook makes its layers again
|
|
1019
|
-
|
|
1033
|
+
// give a map a size, tiles and objects, the game's Restart hook makes its layers again; keepUnknown leaves an object
|
|
1034
|
+
// layer past the snapshot's as it is, as applying an autosave from before the file had it does
|
|
1035
|
+
function editorSetMapSnapshot(record, snapshot, keepUnknown=false)
|
|
1020
1036
|
{
|
|
1021
1037
|
const map = record.map;
|
|
1022
1038
|
editorResizeMap(map, snapshot.width, snapshot.height);
|
|
1023
1039
|
editorCopyData(editorTileLayerData(map.layers), snapshot.layers);
|
|
1024
1040
|
editorRestoreObjects(map, snapshot);
|
|
1041
|
+
if (!keepUnknown)
|
|
1025
1042
|
for (const group of editorObjectGroups(map.layers).slice(snapshot.objects.length))
|
|
1026
1043
|
group.objects = []; // an Objects layer the editor made since
|
|
1027
1044
|
editorClearSelections();
|
|
@@ -1216,13 +1233,15 @@ function editorObjectLayers(record)
|
|
|
1216
1233
|
return record.objectLayers;
|
|
1217
1234
|
}
|
|
1218
1235
|
|
|
1219
|
-
// a new Tiled object layer named Objects, on top of a map
|
|
1236
|
+
// a new Tiled object layer named Objects, on top of a map, with an id past every layer's, in groups too
|
|
1220
1237
|
function editorNewObjectGroup(map)
|
|
1221
1238
|
{
|
|
1222
|
-
const
|
|
1239
|
+
const ids = (layers)=> (layers || []).flatMap((layer)=> [(layer.id | 0) + 1, ...ids(layer.layers)]);
|
|
1240
|
+
const id = max(map.nextlayerid ?? 1, ...ids(map.layers)), group = {draworder: 'topdown', id, name: 'Objects',
|
|
1223
1241
|
objects: [], opacity: 1, type: 'objectgroup', visible: true, x: 0, y: 0};
|
|
1224
1242
|
map.nextlayerid = id + 1;
|
|
1225
1243
|
map.layers.push(group);
|
|
1244
|
+
editorMadeGroups.add(group);
|
|
1226
1245
|
return group;
|
|
1227
1246
|
}
|
|
1228
1247
|
|
|
@@ -1246,12 +1265,13 @@ function editorObjectPos(record, object)
|
|
|
1246
1265
|
return vec2(object.x / tilewidth, height - object.y / tileheight);
|
|
1247
1266
|
}
|
|
1248
1267
|
|
|
1249
|
-
// set an object's position in the map from a world position
|
|
1268
|
+
// set an object's position in the map from a world position, to a millionth of a pixel, so a place turned into
|
|
1269
|
+
// cells and back is written as it was and not a hair off
|
|
1250
1270
|
function editorObjectSetPos(record, object, pos)
|
|
1251
1271
|
{
|
|
1252
1272
|
const {height=0, tilewidth=1, tileheight=1} = record.map;
|
|
1253
|
-
object.x = pos.x * tilewidth;
|
|
1254
|
-
object.y = (height - pos.y) * tileheight;
|
|
1273
|
+
object.x = round(pos.x * tilewidth * 1e6) / 1e6;
|
|
1274
|
+
object.y = round((height - pos.y) * tileheight * 1e6) / 1e6;
|
|
1255
1275
|
}
|
|
1256
1276
|
|
|
1257
1277
|
// called by objectLayersLoad for each object it read, the editor keeps what the game made by the object's id
|
|
@@ -1299,8 +1319,8 @@ function editorSetObjects(layer, list)
|
|
|
1299
1319
|
if ('pos' in made && (old.x !== object.x || old.y !== object.y))
|
|
1300
1320
|
made.pos = editorObjectPos(record, object);
|
|
1301
1321
|
const was = objectLayersProperties(type, old), now = objectLayersProperties(type, object);
|
|
1302
|
-
for (const name in now)
|
|
1303
|
-
editorSameData(was[name], now[name]) || (made[name] = now[name]);
|
|
1322
|
+
for (const name in now) // a method's name is left out, as objectLayersMake leaves it
|
|
1323
|
+
editorSameData(was[name], now[name]) || typeof made[name] === 'function' || (made[name] = now[name]);
|
|
1304
1324
|
}
|
|
1305
1325
|
}
|
|
1306
1326
|
}
|
|
@@ -1314,7 +1334,7 @@ function editorChangeObjects(layer, change)
|
|
|
1314
1334
|
change(after);
|
|
1315
1335
|
if (editorSameData(before, after)) return false;
|
|
1316
1336
|
editorSetObjects(layer, after);
|
|
1317
|
-
const last = editorStroke?.
|
|
1337
|
+
const last = editorStroke?.[editorStroke.length - 1];
|
|
1318
1338
|
if (last?.objectLayer === layer)
|
|
1319
1339
|
last.after = after;
|
|
1320
1340
|
else
|
|
@@ -1328,15 +1348,21 @@ function editorObjectSetProperty(object, name, value, defaultValue)
|
|
|
1328
1348
|
{
|
|
1329
1349
|
if (Number.isInteger(defaultValue) && isNumber(value))
|
|
1330
1350
|
value = round(value); // an integer stays one, as Tiled keeps an int
|
|
1331
|
-
const
|
|
1351
|
+
const list = object.properties ?? [], at = list.findIndex((property)=> property.name === name), old = list[at];
|
|
1352
|
+
const properties = list.filter((property)=> property.name !== name);
|
|
1332
1353
|
const text = (v)=> isColor(v) ? v.toString() : isVector2(v) ? v.x + ',' + v.y : JSON.stringify(v);
|
|
1333
1354
|
if (text(value) !== text(defaultValue))
|
|
1334
1355
|
{
|
|
1335
1356
|
const hex = isColor(value) && value.toString();
|
|
1336
|
-
|
|
1357
|
+
const property = hex ? {name, type: 'color', value: '#' + hex.slice(7, 9) + hex.slice(1, 7)} :
|
|
1337
1358
|
isVector2(value) ? {name, type: 'string', value: text(value)} :
|
|
1338
1359
|
{name, type: typeof value === 'boolean' ? 'bool' : typeof value === 'string' ? 'string' :
|
|
1339
|
-
Number.isInteger(defaultValue) ? 'int' : 'float', value}
|
|
1360
|
+
Number.isInteger(defaultValue) ? 'int' : 'float', value};
|
|
1361
|
+
|
|
1362
|
+
// one the map had keeps its place and its kind where the value fits it, a file stays a file, an object an object
|
|
1363
|
+
if (old?.type === 'file' && property.type === 'string' || old?.type === 'object' && property.type === 'int')
|
|
1364
|
+
property.type = old.type;
|
|
1365
|
+
at < 0 ? properties.push(property) : properties.splice(at, 0, property);
|
|
1340
1366
|
}
|
|
1341
1367
|
if (properties.length)
|
|
1342
1368
|
object.properties = properties;
|
|
@@ -1422,14 +1448,15 @@ function editorObjectsIn(layer, a, b)
|
|
|
1422
1448
|
function editorPickObject(object)
|
|
1423
1449
|
{ editorObjectBrush = [{type: object.type || object.class, properties: editorObjectsCopy(object.properties ?? []), offset: vec2()}]; }
|
|
1424
1450
|
|
|
1425
|
-
// a new Tiled
|
|
1426
|
-
|
|
1451
|
+
// a new Tiled object for a brush's object, at a world position, with the map's next id: a copy of the object it was
|
|
1452
|
+
// copied from, its name, size, turn and the rest kept, or a point object of the brush's type and properties
|
|
1453
|
+
function editorNewObject(record, {type, properties, object}, pos)
|
|
1427
1454
|
{
|
|
1428
|
-
const
|
|
1429
|
-
width: 0, height: 0, x: 0, y: 0};
|
|
1430
|
-
|
|
1431
|
-
editorObjectSetPos(record,
|
|
1432
|
-
return
|
|
1455
|
+
const made = object ? editorObjectsCopy(object) : {id: 0, name: '', type, point: true, rotation: 0, visible: true,
|
|
1456
|
+
width: 0, height: 0, x: 0, y: 0, ...(properties.length && {properties: editorObjectsCopy(properties)})};
|
|
1457
|
+
made.id = editorNextObjectId(record.map);
|
|
1458
|
+
editorObjectSetPos(record, made, pos);
|
|
1459
|
+
return made;
|
|
1433
1460
|
}
|
|
1434
1461
|
|
|
1435
1462
|
// place an object brush's objects at a position plus each one's offset, as one undo
|
|
@@ -1460,8 +1487,8 @@ function editorDeleteObjects()
|
|
|
1460
1487
|
return true;
|
|
1461
1488
|
}
|
|
1462
1489
|
|
|
1463
|
-
// the selected objects into the brush,
|
|
1464
|
-
// cleared so the next click places them
|
|
1490
|
+
// the selected objects into the brush, each a copy with its offset from the lowest corner of their positions, and
|
|
1491
|
+
// the selection cleared so the next click places them
|
|
1465
1492
|
function editorCopyObjects()
|
|
1466
1493
|
{
|
|
1467
1494
|
const objects = editorSelectedObjects();
|
|
@@ -1469,7 +1496,7 @@ function editorCopyObjects()
|
|
|
1469
1496
|
const positions = objects.map((object)=> editorObjectPos(editorObjectLayer.record, object));
|
|
1470
1497
|
const low = vec2(min(...positions.map((p)=> p.x)), min(...positions.map((p)=> p.y)));
|
|
1471
1498
|
editorObjectBrush = editorObjectClipboard = objects.map((object, i)=> ({type: object.type || object.class,
|
|
1472
|
-
|
|
1499
|
+
offset: positions[i].subtract(low), object: editorObjectsCopy(object)}));
|
|
1473
1500
|
editorObjectSelection.clear();
|
|
1474
1501
|
return true;
|
|
1475
1502
|
}
|
|
@@ -1605,12 +1632,8 @@ function editorCellClamped(live, worldPos)
|
|
|
1605
1632
|
function editorArea(a, b)
|
|
1606
1633
|
{ return {min: vec2(min(a.x, b.x), min(a.y, b.y)), max: vec2(max(a.x, b.x), max(a.y, b.y))}; }
|
|
1607
1634
|
|
|
1608
|
-
// the tile info a layer draws a tile with, as TileLayer.drawTileData picks it
|
|
1609
|
-
function editorTileInfo(live, tile)
|
|
1610
|
-
{
|
|
1611
|
-
const t = live.tileInfo;
|
|
1612
|
-
return t && (t.columns ? t.frame(tile) : t.index(tile));
|
|
1613
|
-
}
|
|
1635
|
+
// the tile info a layer draws a tile with, as TileLayer.drawTileData picks it, undefined past the end of its sheet
|
|
1636
|
+
function editorTileInfo(live, tile) { return tileLayerTileInfo(live.tileInfo, tile); }
|
|
1614
1637
|
|
|
1615
1638
|
///////////////////////////////////////////////////////////////////////////////
|
|
1616
1639
|
// stamps
|
|
@@ -1841,8 +1864,8 @@ function editorClear()
|
|
|
1841
1864
|
editorStrokeEnd();
|
|
1842
1865
|
}
|
|
1843
1866
|
|
|
1844
|
-
// the objects of a map's object layers inside a tile area, each {group, type,
|
|
1845
|
-
// object layer,
|
|
1867
|
+
// the objects of a map's object layers inside a tile area, each {group, type, offset, object}: the index of its
|
|
1868
|
+
// object layer, its offset from the area's bottom left corner, and a copy of it, as a stamp keeps them
|
|
1846
1869
|
function editorAreaObjects(layer, area)
|
|
1847
1870
|
{
|
|
1848
1871
|
const corner = layer.live.pos.add(area.min), far = layer.live.pos.add(area.max).add(vec2(1));
|
|
@@ -1850,8 +1873,8 @@ function editorAreaObjects(layer, area)
|
|
|
1850
1873
|
{
|
|
1851
1874
|
const ids = new Set(editorObjectsIn(objectLayer, corner, far));
|
|
1852
1875
|
return (objectLayer.group?.objects ?? []).filter((object)=> ids.has(object.id)).map((object)=>
|
|
1853
|
-
({group, type: object.type || object.class,
|
|
1854
|
-
|
|
1876
|
+
({group, type: object.type || object.class, offset: editorObjectPos(layer.record, object).subtract(corner),
|
|
1877
|
+
object: editorObjectsCopy(object)}));
|
|
1855
1878
|
});
|
|
1856
1879
|
}
|
|
1857
1880
|
|
|
@@ -1939,12 +1962,13 @@ function editorSelectionDragTo(cell)
|
|
|
1939
1962
|
from.arrayCheck(target.live.size) && editorPaint(target, from, 0);
|
|
1940
1963
|
}
|
|
1941
1964
|
editorPaintStamp(layer, editorSelection.min, stamp);
|
|
1965
|
+
// whole cells added to the places as written, as an object drag does, so they come back to the digit
|
|
1966
|
+
const {tilewidth=1, tileheight=1} = layer.record.map;
|
|
1942
1967
|
for (const {objectLayer, ids} of drag.objects)
|
|
1943
1968
|
editorChangeObjects(objectLayer, (list)=>
|
|
1944
1969
|
{
|
|
1945
1970
|
for (const object of list)
|
|
1946
|
-
ids.has(object.id) &&
|
|
1947
|
-
editorObjectPos(layer.record, object).add(delta));
|
|
1971
|
+
ids.has(object.id) && (object.x += delta.x * tilewidth, object.y -= delta.y * tileheight);
|
|
1948
1972
|
});
|
|
1949
1973
|
});
|
|
1950
1974
|
editorStroke && editorRedraw(editorStroke);
|
|
@@ -2174,16 +2198,30 @@ function editorPaletteTiles(layer)
|
|
|
2174
2198
|
const live = layer?.live, image = live?.tileInfo?.textureInfo?.image, list = levelEditor.paletteTiles;
|
|
2175
2199
|
if (list && live)
|
|
2176
2200
|
{
|
|
2177
|
-
if (layer.palette?.list !== list)
|
|
2178
|
-
layer.palette = {list,
|
|
2201
|
+
if (layer.palette?.list !== list || layer.palette.count !== live.tileInfo?.tiles?.length)
|
|
2202
|
+
layer.palette = {list, count: live.tileInfo?.tiles?.length, // a tile set fills in as it loads
|
|
2203
|
+
tiles: list.map((tile)=> ({tile, tileInfo: editorTileInfo(live, tile)}))
|
|
2204
|
+
.filter((t)=> t.tileInfo || !live.tileInfo)}; // a tile set, or a sheet, has only so many
|
|
2205
|
+
return layer.palette.tiles;
|
|
2206
|
+
}
|
|
2207
|
+
// a tile set lists its tiles, each a tile the palette offers
|
|
2208
|
+
const set = live?.tileInfo?.tiles;
|
|
2209
|
+
if (set)
|
|
2210
|
+
{
|
|
2211
|
+
if (layer.palette?.set !== set || layer.palette.tiles.length !== set.length)
|
|
2212
|
+
layer.palette = {set, tiles: set.map((tileInfo, tile)=> ({tile, tileInfo}))};
|
|
2179
2213
|
return layer.palette.tiles;
|
|
2180
2214
|
}
|
|
2181
2215
|
if (!image) return [];
|
|
2182
2216
|
if (layer.palette?.image === image) return layer.palette.tiles;
|
|
2183
2217
|
|
|
2184
|
-
|
|
2218
|
+
// a sheet read by its columns, a tileset with a margin or a spacing, is read with frame(), which asserts the
|
|
2219
|
+
// frame is inside the image, so a row past the image's end is found before its frame is made
|
|
2220
|
+
const tiles = [], base = live.tileInfo, columns = base.columns;
|
|
2185
2221
|
for (let i = 0; i < 4096; ++i)
|
|
2186
2222
|
{
|
|
2223
|
+
if (columns && base.pos.y + (i / columns | 0) * (base.size.y + base.padding*2) + base.size.y > image.height)
|
|
2224
|
+
break;
|
|
2187
2225
|
const t = editorTileInfo(live, i);
|
|
2188
2226
|
if (t.pos.x + t.size.x > image.width || t.pos.y + t.size.y > image.height) break;
|
|
2189
2227
|
tiles.push({tile: i, tileInfo: t});
|
|
@@ -2199,7 +2237,7 @@ function editorPaletteTiles(layer)
|
|
|
2199
2237
|
size.y).data.buffer);
|
|
2200
2238
|
return pixels.every((p)=> p === pixels[0]);
|
|
2201
2239
|
};
|
|
2202
|
-
while (tiles.length > 1 && blank(tiles.
|
|
2240
|
+
while (tiles.length > 1 && blank(tiles[tiles.length - 1]))
|
|
2203
2241
|
tiles.pop();
|
|
2204
2242
|
}
|
|
2205
2243
|
catch {}
|
|
@@ -2241,7 +2279,7 @@ function editorPaletteDrawObjects(canvas)
|
|
|
2241
2279
|
function editorPaletteDraw(canvas, layer)
|
|
2242
2280
|
{
|
|
2243
2281
|
const tiles = editorPaletteTiles(layer), cell = editorPaletteCell, columns = editorPaletteColumns;
|
|
2244
|
-
const
|
|
2282
|
+
const slots = tiles.length + 1;
|
|
2245
2283
|
canvas.style.display = tiles.length ? '' : 'none';
|
|
2246
2284
|
canvas.width = columns * cell;
|
|
2247
2285
|
canvas.height = ceil(slots / columns) * cell;
|
|
@@ -2256,10 +2294,13 @@ function editorPaletteDraw(canvas, layer)
|
|
|
2256
2294
|
context.moveTo(8, 8), context.lineTo(cell - 8, cell - 8);
|
|
2257
2295
|
context.moveTo(cell - 8, 8), context.lineTo(8, cell - 8);
|
|
2258
2296
|
context.stroke();
|
|
2259
|
-
tiles.forEach(({tileInfo
|
|
2297
|
+
tiles.forEach(({tileInfo}, i)=>
|
|
2260
2298
|
{
|
|
2261
|
-
|
|
2262
|
-
|
|
2299
|
+
if (!tileInfo) return; // headless, a layer with no sheet
|
|
2300
|
+
const {pos, size, textureInfo} = tileInfo;
|
|
2301
|
+
// each from its own sheet, a tile set's may be on several
|
|
2302
|
+
const slot = i + 1, x = slot % columns * cell, y = (slot / columns | 0) * cell, image = textureInfo?.image;
|
|
2303
|
+
image && context.drawImage(image, pos.x, pos.y, size.x, size.y, x + 2, y + 2, cell - 4, cell - 4);
|
|
2263
2304
|
});
|
|
2264
2305
|
|
|
2265
2306
|
// the brush's slot outlined, when it is one tile the palette shows or Erase
|
|
@@ -2552,7 +2593,7 @@ function editorPropertiesUpdate(box)
|
|
|
2552
2593
|
input.type = 'number';
|
|
2553
2594
|
input.step = Number.isInteger(defaultValue) ? '1' : 'any';
|
|
2554
2595
|
input.value = String(value);
|
|
2555
|
-
input.onchange = ()=> { const v = parseFloat(input.value);
|
|
2596
|
+
input.onchange = ()=> { const v = parseFloat(input.value); isFinite(v) && set(v); }; // 1e999 is not one
|
|
2556
2597
|
}
|
|
2557
2598
|
else if (typeof defaultValue === 'string')
|
|
2558
2599
|
{
|
|
@@ -2574,7 +2615,7 @@ function editorPropertiesUpdate(box)
|
|
|
2574
2615
|
{
|
|
2575
2616
|
// both checked before a vector is made of them, an emptied or half typed field shows its value again
|
|
2576
2617
|
const px = parseFloat(input.value), py = parseFloat(y.value);
|
|
2577
|
-
if (
|
|
2618
|
+
if (isFinite(px) && isFinite(py))
|
|
2578
2619
|
set(vec2(px, py));
|
|
2579
2620
|
else
|
|
2580
2621
|
input.value = String(value.x), y.value = String(value.y);
|
|
@@ -2847,8 +2888,9 @@ function editorRender()
|
|
|
2847
2888
|
for (let y = y0; y < y1; ++y)
|
|
2848
2889
|
{
|
|
2849
2890
|
const t = editorGidToTile(source.data[x + (height - 1 - y) * width]);
|
|
2850
|
-
|
|
2851
|
-
|
|
2891
|
+
const tileInfo = t && editorTileInfo(live, t.tile);
|
|
2892
|
+
if (tileInfo && live.getData(vec2(x, y)).tile === undefined)
|
|
2893
|
+
drawTile(live.pos.add(vec2(x + .5, y + .5)), vec2(1), tileInfo, ghost,
|
|
2852
2894
|
t.direction * PI/2, t.mirror, undefined, glEnable, false);
|
|
2853
2895
|
}
|
|
2854
2896
|
|
|
@@ -2887,8 +2929,9 @@ function editorRender()
|
|
|
2887
2929
|
{
|
|
2888
2930
|
const gid = grid[x + y * w], t = editorGidToTile(gid);
|
|
2889
2931
|
const center = live.pos.add(editorHover).add(vec2(x + .5, y + .5));
|
|
2890
|
-
|
|
2891
|
-
|
|
2932
|
+
const tileInfo = t && editorTileInfo(live, t.tile);
|
|
2933
|
+
if (tileInfo)
|
|
2934
|
+
drawTile(center, vec2(1), tileInfo, hsl(0, 0, 1, .7), t.direction * PI/2, t.mirror, undefined, glEnable, false);
|
|
2892
2935
|
else if (gid === 0)
|
|
2893
2936
|
drawRect(center, vec2(1), hsl(0, 1, .5, .3), undefined, undefined, false);
|
|
2894
2937
|
}
|
package/src/engineExport.js
CHANGED
|
@@ -113,6 +113,7 @@ export
|
|
|
113
113
|
touchGamepadVibration,
|
|
114
114
|
vibrateEnable,
|
|
115
115
|
soundEnable,
|
|
116
|
+
soundIgnoreSilentSwitch,
|
|
116
117
|
soundVolume,
|
|
117
118
|
soundDefaultRange,
|
|
118
119
|
soundDefaultTaper,
|
|
@@ -175,6 +176,7 @@ export
|
|
|
175
176
|
setTouchGamepadVibration,
|
|
176
177
|
setVibrateEnable,
|
|
177
178
|
setSoundEnable,
|
|
179
|
+
setSoundIgnoreSilentSwitch,
|
|
178
180
|
setSoundVolume,
|
|
179
181
|
setSoundDefaultRange,
|
|
180
182
|
setSoundDefaultTaper,
|
package/src/engineInput.js
CHANGED
|
@@ -586,6 +586,9 @@ function inputInit()
|
|
|
586
586
|
if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
|
|
587
587
|
audioContext.resume();
|
|
588
588
|
|
|
589
|
+
// a press in the bars around a letterboxed canvas is not one on its edge; a drag out of it still moves
|
|
590
|
+
if (!inCanvas(e.x, e.y))
|
|
591
|
+
return;
|
|
589
592
|
inputData[0][e.button] = 3;
|
|
590
593
|
|
|
591
594
|
const mousePosScreenLast = mousePosScreen;
|
|
@@ -739,8 +742,8 @@ function inputInit()
|
|
|
739
742
|
}
|
|
740
743
|
else if (inputWasTouching && touch.identifier === inputTouchIdentifier)
|
|
741
744
|
mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
|
|
742
|
-
else if (pressTouch)
|
|
743
|
-
inputData[0][button] = 3;
|
|
745
|
+
else if (pressTouch && inCanvas(pressTouch.clientX, pressTouch.clientY))
|
|
746
|
+
inputData[0][button] = 3; // a tap in the bars around the canvas is not a press
|
|
744
747
|
// the finger left after a pinch moves the mouse from where it is, with no jump and no press
|
|
745
748
|
inputTouchIdentifier = pinching ? undefined : touch.identifier;
|
|
746
749
|
}
|
|
@@ -771,6 +774,13 @@ function inputInit()
|
|
|
771
774
|
|
|
772
775
|
}
|
|
773
776
|
|
|
777
|
+
// is a point of the window on the canvas
|
|
778
|
+
function inCanvas(x, y)
|
|
779
|
+
{
|
|
780
|
+
const rect = mainCanvas.getBoundingClientRect();
|
|
781
|
+
return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
|
|
782
|
+
}
|
|
783
|
+
|
|
774
784
|
// convert a mouse or touch event position to screen space
|
|
775
785
|
function mouseEventToScreen(mousePos)
|
|
776
786
|
{
|
|
@@ -941,7 +951,7 @@ function inputUpdate()
|
|
|
941
951
|
const centered = gamepadAxisCentered[i] ?? (gamepadAxisCentered[i] = []);
|
|
942
952
|
const readAxis = (j)=>
|
|
943
953
|
{
|
|
944
|
-
const v = gamepad.axes[j];
|
|
954
|
+
const v = isFinite(gamepad.axes[j]) ? gamepad.axes[j] : 0; // a broken axis reads as centered
|
|
945
955
|
if (isStandard && j < 4)
|
|
946
956
|
return v; // spec guarantees axes 0-3 are the two sticks
|
|
947
957
|
if (!gamepadAxisFilterEnable)
|
|
@@ -973,6 +983,10 @@ function inputUpdate()
|
|
|
973
983
|
if (button.pressed && (!button.value || button.value > .9))
|
|
974
984
|
hadInput = true;
|
|
975
985
|
}
|
|
986
|
+
|
|
987
|
+
// a different gamepad in the same slot with fewer buttons or sticks holds none of the last one's
|
|
988
|
+
data.length = buttonsLast.length = gamepad.buttons.length;
|
|
989
|
+
sticks.length = gamepad.axes.length >> 1;
|
|
976
990
|
|
|
977
991
|
// set new primary gamepad if current is not connected
|
|
978
992
|
if (hadInput)
|
package/src/engineMath.js
CHANGED
|
@@ -200,11 +200,15 @@ function smoothStep(percent)
|
|
|
200
200
|
* @memberof Math */
|
|
201
201
|
function isPowerOfTwo(value) { return value > 0 && value % 1 === 0 && 2**round(log2(value)) === value; } // any size, not only 32 bits
|
|
202
202
|
|
|
203
|
-
/** Returns the nearest power of two not less than the value
|
|
203
|
+
/** Returns the nearest whole power of two not less than the value, 1 for a value of 1 or less
|
|
204
204
|
* @param {number} value
|
|
205
205
|
* @return {number}
|
|
206
206
|
* @memberof Math */
|
|
207
|
-
function nearestPowerOfTwo(value)
|
|
207
|
+
function nearestPowerOfTwo(value)
|
|
208
|
+
{
|
|
209
|
+
const power = 2**ceil(log2(max(value, 1)));
|
|
210
|
+
return power < value ? power * 2 : power; // log2 of a value just above a power can round down to it
|
|
211
|
+
}
|
|
208
212
|
|
|
209
213
|
/** Returns true if two axis aligned bounding boxes are overlapping
|
|
210
214
|
* this can be used for simple collision detection between objects
|