littlejsengine 1.22.0 → 1.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/COPYRIGHT.txt +65 -38
- package/EDITOR.md +359 -0
- package/README.md +12 -7
- package/REFERENCE.md +2314 -0
- package/dist/littlejs.d.ts +640 -100
- package/dist/littlejs.esm.js +3596 -1444
- package/dist/littlejs.esm.min.js +3 -3
- package/dist/littlejs.js +3583 -1444
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +2106 -1355
- package/package.json +4 -3
- package/plugins/gltf.js +29 -13
- package/plugins/lightSystem.js +8 -6
- package/plugins/parallax.js +174 -0
- package/plugins/particleEffects.js +37 -22
- package/plugins/pluginExport.js +8 -0
- package/plugins/postProcess.js +5 -2
- package/plugins/render3d.js +59 -21
- package/plugins/render3dEditor.js +1058 -118
- package/plugins/render3dExtras.js +82 -21
- package/plugins/render3dLevel.js +328 -8
- package/plugins/textureSheet.js +1 -0
- package/plugins/tweenSystem.js +39 -34
- package/plugins/uiSystem.js +7 -3
- package/plugins/zzfxm.js +161 -159
- package/src/engine.js +1 -1
- package/src/engineAudio.js +17 -3
- package/src/engineBuild.mjs +18 -2
- package/src/engineDraw.js +26 -10
- package/src/engineEditor.js +515 -52
- package/src/engineExport.js +5 -0
- package/src/engineInput.js +48 -6
- package/src/engineObject.js +675 -675
- package/src/engineRelease.js +17 -2
- package/src/engineSettings.js +14 -0
- package/src/engineTileLayer.js +2 -1
- package/src/engineUtilities.js +291 -291
- package/src/engineWebGL.js +12 -3
package/dist/littlejs.release.js
CHANGED
|
@@ -35,7 +35,7 @@ const engineName = 'LittleJS';
|
|
|
35
35
|
* @type {string}
|
|
36
36
|
* @default
|
|
37
37
|
* @memberof Engine */
|
|
38
|
-
const engineVersion = '1.
|
|
38
|
+
const engineVersion = '1.23.1';
|
|
39
39
|
|
|
40
40
|
/** Frames per second to update
|
|
41
41
|
* @type {number}
|
|
@@ -982,8 +982,23 @@ function tweak(){}
|
|
|
982
982
|
function tweakButton(){}
|
|
983
983
|
function tweakDivider(){}
|
|
984
984
|
function tweakEngineDefaults(){}
|
|
985
|
-
|
|
986
|
-
|
|
985
|
+
class LevelEditor
|
|
986
|
+
{
|
|
987
|
+
constructor()
|
|
988
|
+
{
|
|
989
|
+
this.paletteTiles = this.use3D = this.tool = undefined;
|
|
990
|
+
this.keys = {}, this.buttons = [], this.tools = {};
|
|
991
|
+
}
|
|
992
|
+
get isOpen() { return false; }
|
|
993
|
+
get is3D() { return false; }
|
|
994
|
+
get edit2D() { return undefined; }
|
|
995
|
+
get edit3D() { return undefined; }
|
|
996
|
+
open(){} close(){} addKey(){} addButton(){} addTool(){}
|
|
997
|
+
onTile(){} onRestart(){} onPlayFrom(){} onOpen(){} onClose(){} onUpdate(){} onDraw(){} onPanel(){}
|
|
998
|
+
onSave(){ return false; }
|
|
999
|
+
}
|
|
1000
|
+
let levelEditor = new LevelEditor;
|
|
1001
|
+
function setLevelEditor(editor){ levelEditor = editor; }
|
|
987
1002
|
function editorMapRestore(map){ return map; }
|
|
988
1003
|
function editorMapLoaded(){}
|
|
989
1004
|
function editorJSONFetched(){}
|
|
@@ -2281,295 +2296,295 @@ const PURPLE = debugProtectConstant(rgb(.5,0,1));
|
|
|
2281
2296
|
* @type {Color}
|
|
2282
2297
|
* @memberof Math */
|
|
2283
2298
|
const MAGENTA = debugProtectConstant(rgb(1,0,1));
|
|
2284
|
-
/**
|
|
2285
|
-
* LittleJS Utility Classes and Functions
|
|
2286
|
-
* - Timer - tracks time automatically with support for pause and real-time modes
|
|
2287
|
-
* - Time formatting helper
|
|
2288
|
-
* - JSON file fetching
|
|
2289
|
-
* - File saving (text, canvas, data URLs)
|
|
2290
|
-
* - Native share dialog support
|
|
2291
|
-
* - Local storage save data management
|
|
2292
|
-
* - Value noise (1D and 2D)
|
|
2293
|
-
* @namespace Utilities
|
|
2294
|
-
*/
|
|
2295
|
-
|
|
2296
|
-
/**
|
|
2297
|
-
* Timer object tracks how long has passed since it was set
|
|
2298
|
-
* @memberof Engine
|
|
2299
|
-
* @example
|
|
2300
|
-
* let a = new Timer; // creates a timer that is not set
|
|
2301
|
-
* a.set(3); // sets the timer to 3 seconds
|
|
2302
|
-
*
|
|
2303
|
-
* let b = new Timer(1); // creates a timer with 1 second left
|
|
2304
|
-
* b.unset(); // unset the timer
|
|
2305
|
-
*/
|
|
2306
|
-
class Timer
|
|
2307
|
-
{
|
|
2308
|
-
/** Create a timer object set time passed in
|
|
2309
|
-
* @param {number} [timeLeft] - How much time left before the timer is elapsed in seconds (undefined = unset)
|
|
2310
|
-
* @param {boolean} [useRealTime] - Should the timer keep running even when the game is paused? (useful for UI) */
|
|
2311
|
-
constructor(timeLeft, useRealTime=false)
|
|
2312
|
-
{
|
|
2313
|
-
false&&ASSERT(timeLeft === undefined || isNumber(timeLeft), 'Constructed Timer is invalid.', timeLeft);
|
|
2314
|
-
this.useRealTime = useRealTime;
|
|
2315
|
-
const globalTime = this.getGlobalTime();
|
|
2316
|
-
/** @type {number|undefined} */
|
|
2317
|
-
this.time = timeLeft === undefined ? undefined : globalTime + timeLeft;
|
|
2318
|
-
/** @type {number|undefined} */
|
|
2319
|
-
this.setTime = timeLeft;
|
|
2320
|
-
}
|
|
2321
|
-
|
|
2322
|
-
/** Set the timer with seconds passed in
|
|
2323
|
-
* @param {number} [timeLeft] - How much time left before the timer is elapsed in seconds */
|
|
2324
|
-
set(timeLeft=0)
|
|
2325
|
-
{
|
|
2326
|
-
false&&ASSERT(isNumber(timeLeft), 'Timer is invalid.', timeLeft);
|
|
2327
|
-
const globalTime = this.getGlobalTime();
|
|
2328
|
-
this.time = globalTime + timeLeft;
|
|
2329
|
-
this.setTime = timeLeft;
|
|
2330
|
-
}
|
|
2331
|
-
|
|
2332
|
-
/** Set if the timer should keep running even when the game is paused
|
|
2333
|
-
* @param {boolean} [useRealTime] */
|
|
2334
|
-
setUseRealTime(useRealTime=true)
|
|
2335
|
-
{
|
|
2336
|
-
false&&ASSERT(!this.isSet(), 'Cannot change global time setting while timer is set.');
|
|
2337
|
-
this.useRealTime = useRealTime;
|
|
2338
|
-
}
|
|
2339
|
-
|
|
2340
|
-
/** Unset the timer */
|
|
2341
|
-
unset() { this.time = undefined; }
|
|
2342
|
-
|
|
2343
|
-
/** Returns true if set
|
|
2344
|
-
* @return {boolean} */
|
|
2345
|
-
isSet() { return this.time !== undefined; }
|
|
2346
|
-
|
|
2347
|
-
/** Returns true if set and has not elapsed
|
|
2348
|
-
* @return {boolean} */
|
|
2349
|
-
active() { return this.getGlobalTime() < this.time; }
|
|
2350
|
-
|
|
2351
|
-
/** Returns true if set and elapsed
|
|
2352
|
-
* @return {boolean} */
|
|
2353
|
-
elapsed() { return this.getGlobalTime() >= this.time; }
|
|
2354
|
-
|
|
2355
|
-
/** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
|
|
2356
|
-
* @return {number} */
|
|
2357
|
-
get() { return this.isSet()? this.getGlobalTime() - this.time : 0; }
|
|
2358
|
-
|
|
2359
|
-
/** Get percentage elapsed based on time it was set to, returns 0 if not set.
|
|
2360
|
-
* Zero-duration timers report 1 (already elapsed).
|
|
2361
|
-
* @return {number} */
|
|
2362
|
-
getPercent()
|
|
2363
|
-
{
|
|
2364
|
-
if (!this.isSet()) return 0;
|
|
2365
|
-
if (!this.setTime) return 1;
|
|
2366
|
-
return 1 - percent(this.time - this.getGlobalTime(), 0, this.setTime);
|
|
2367
|
-
}
|
|
2368
|
-
|
|
2369
|
-
/** Get the time this timer was set to, returns 0 if not set
|
|
2370
|
-
* @return {number} */
|
|
2371
|
-
getSetTime() { return this.isSet() ? this.setTime : 0; }
|
|
2372
|
-
|
|
2373
|
-
/** Get the current global time this timer is based on
|
|
2374
|
-
* @return {number} */
|
|
2375
|
-
getGlobalTime() { return this.useRealTime ? timeReal : time; }
|
|
2376
|
-
|
|
2377
|
-
/** Returns this timer expressed as a string
|
|
2378
|
-
* @return {string} */
|
|
2379
|
-
toString() { return this.isSet() ? abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }
|
|
2380
|
-
|
|
2381
|
-
/** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
|
|
2382
|
-
* @return {number} */
|
|
2383
|
-
valueOf() { return this.get(); }
|
|
2384
|
-
}
|
|
2385
|
-
|
|
2386
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
2387
|
-
|
|
2388
|
-
/** Formats seconds to mm:ss style for display purposes
|
|
2389
|
-
* @param {number} t - time in seconds
|
|
2390
|
-
* @return {string}
|
|
2391
|
-
* @memberof Utilities */
|
|
2392
|
-
function formatTime(t)
|
|
2393
|
-
{
|
|
2394
|
-
const signStr = t <= -1 ? '-' : ''; // no sign when it shows 0:00
|
|
2395
|
-
t = abs(t)|0;
|
|
2396
|
-
return signStr + (t/60|0) + ':' + (t%60<10?'0':'') + t%60;
|
|
2397
|
-
}
|
|
2398
|
-
|
|
2399
|
-
/** Fetches a JSON file from a URL and returns the parsed JSON object. Must be used with await!
|
|
2400
|
-
* @param {string} url - URL of JSON file
|
|
2401
|
-
* @return {Promise<object>}
|
|
2402
|
-
* @memberof Utilities */
|
|
2403
|
-
async function fetchJSON(url)
|
|
2404
|
-
{
|
|
2405
|
-
const response = await fetch(url);
|
|
2406
|
-
if (!response.ok)
|
|
2407
|
-
throw new Error(`Failed to fetch JSON from ${url}: ${response.status} ${response.statusText}`);
|
|
2408
|
-
const json = await response.json();
|
|
2409
|
-
editorJSONFetched(url, json); // debug builds remember the file a level came from, to save it by that name
|
|
2410
|
-
return json;
|
|
2411
|
-
}
|
|
2412
|
-
|
|
2413
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
2414
|
-
|
|
2415
|
-
/** Save a text file to disk
|
|
2416
|
-
* @param {string} text
|
|
2417
|
-
* @param {string} [filename]
|
|
2418
|
-
* @param {string} [type]
|
|
2419
|
-
* @memberof Utilities */
|
|
2420
|
-
function saveText(text, filename='text', type='text/plain')
|
|
2421
|
-
{ saveDataURL(URL.createObjectURL(new Blob([text], {'type':type})), filename, 1e3); } // freed once the download has it
|
|
2422
|
-
|
|
2423
|
-
/** Create an offscreen canvas to draw into, and return its 2D context
|
|
2424
|
-
* - The canvas is context.canvas, which is what TextureInfo and the like take
|
|
2425
|
-
* @param {number} width - In pixels
|
|
2426
|
-
* @param {number} [height] - In pixels, defaults to the width for a square
|
|
2427
|
-
* @param {boolean} [willReadFrequently] - Keep it in software, faster when getImageData is called on it often
|
|
2428
|
-
* @return {OffscreenCanvasRenderingContext2D}
|
|
2429
|
-
* @memberof Utilities */
|
|
2430
|
-
function createCanvasContext(width, height=width, willReadFrequently=false)
|
|
2431
|
-
{
|
|
2432
|
-
false&&ASSERT(isNumber(width) && isNumber(height), 'canvas width and height must be numbers', width, height);
|
|
2433
|
-
return new OffscreenCanvas(width, height).getContext('2d', {willReadFrequently});
|
|
2434
|
-
}
|
|
2435
|
-
|
|
2436
|
-
/** Save a canvas to disk
|
|
2437
|
-
* @param {HTMLCanvasElement|OffscreenCanvas} canvas
|
|
2438
|
-
* @param {string} [filename]
|
|
2439
|
-
* @param {string} [type]
|
|
2440
|
-
* @memberof Utilities */
|
|
2441
|
-
function saveCanvas(canvas, filename='screenshot', type='image/png')
|
|
2442
|
-
{
|
|
2443
|
-
if (canvas instanceof OffscreenCanvas)
|
|
2444
|
-
{
|
|
2445
|
-
// copy to temporary canvas and save
|
|
2446
|
-
const saveCanvas = document.createElement('canvas');
|
|
2447
|
-
saveCanvas.width = canvas.width;
|
|
2448
|
-
saveCanvas.height = canvas.height;
|
|
2449
|
-
saveCanvas.getContext('2d').drawImage(canvas, 0, 0);
|
|
2450
|
-
saveDataURL(saveCanvas.toDataURL(type), filename);
|
|
2451
|
-
}
|
|
2452
|
-
else
|
|
2453
|
-
saveDataURL(canvas.toDataURL(type), filename);
|
|
2454
|
-
}
|
|
2455
|
-
|
|
2456
|
-
/** Save a data url to disk
|
|
2457
|
-
* @param {string} url
|
|
2458
|
-
* @param {string} [filename]
|
|
2459
|
-
* @param {number} [revokeTime] - how long before revoking the url
|
|
2460
|
-
* @memberof Utilities */
|
|
2461
|
-
function saveDataURL(url, filename='download', revokeTime)
|
|
2462
|
-
{
|
|
2463
|
-
false&&ASSERT(isStringLike(url), 'saveDataURL requires url string');
|
|
2464
|
-
false&&ASSERT(isStringLike(filename), 'saveDataURL requires filename string');
|
|
2465
|
-
|
|
2466
|
-
// create link for saving screenshots
|
|
2467
|
-
const link = document.createElement('a');
|
|
2468
|
-
link.download = filename;
|
|
2469
|
-
link.href = url;
|
|
2470
|
-
link.click();
|
|
2471
|
-
if (revokeTime !== undefined)
|
|
2472
|
-
setTimeout(()=> URL.revokeObjectURL(url), revokeTime);
|
|
2473
|
-
}
|
|
2474
|
-
|
|
2475
|
-
/** Share content using the native share dialog if available
|
|
2476
|
-
* @param {string} title - title of the share
|
|
2477
|
-
* @param {string} url - url to share
|
|
2478
|
-
* @param {Function} [callback] - Called when share is complete
|
|
2479
|
-
* @memberof Utilities */
|
|
2480
|
-
function shareURL(title, url, callback)
|
|
2481
|
-
{
|
|
2482
|
-
false&&ASSERT(isStringLike(title), 'shareURL requires title string');
|
|
2483
|
-
false&&ASSERT(isStringLike(url), 'shareURL requires url string');
|
|
2484
|
-
navigator.share?.({title, url}).then(()=>callback?.(), ()=>{}); // a player who cancels is not an error
|
|
2485
|
-
}
|
|
2486
|
-
|
|
2487
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
2488
|
-
|
|
2489
|
-
/** Read save data from local storage
|
|
2490
|
-
* - The result has the type of defaultSaveData, or any when there is none
|
|
2491
|
-
* @template {Object<string, any>} [T=any]
|
|
2492
|
-
* @param {string} saveName - unique name for the game/save
|
|
2493
|
-
* @param {T} [defaultSaveData] - default values, result is {...default, ...loaded} so this must be an object
|
|
2494
|
-
* @return {T}
|
|
2495
|
-
* @memberof Utilities */
|
|
2496
|
-
function readSaveData(saveName, defaultSaveData)
|
|
2497
|
-
{
|
|
2498
|
-
false&&ASSERT(isStringLike(saveName), 'readSaveData requires saveName string');
|
|
2499
|
-
false&&ASSERT(defaultSaveData === undefined ||
|
|
2500
|
-
(typeof defaultSaveData === 'object' && defaultSaveData !== null),
|
|
2501
|
-
'readSaveData: default must be an object - the result is ' +
|
|
2502
|
-
'{...default, ...loaded}, so a scalar default yields {}. ' +
|
|
2503
|
-
'Use readSaveData(key, {best:0}).best');
|
|
2504
|
-
|
|
2505
|
-
// tolerate localStorage being unavailable (iOS private mode, sandboxed
|
|
2506
|
-
// iframes) and corrupt JSON in stored data
|
|
2507
|
-
let loadedData = {};
|
|
2508
|
-
try
|
|
2509
|
-
{
|
|
2510
|
-
const data = localStorage.getItem(saveName); // not the brackets, a name like 'key' or 'length' is a Storage member
|
|
2511
|
-
if (data)
|
|
2512
|
-
{
|
|
2513
|
-
try { loadedData = JSON.parse(data); }
|
|
2514
|
-
catch { false&&LOG('readSaveData: corrupt JSON for', saveName, '— using defaults'); }
|
|
2515
|
-
}
|
|
2516
|
-
}
|
|
2517
|
-
catch { false&&LOG('readSaveData: localStorage unavailable — using defaults'); }
|
|
2518
|
-
return { .../** @type {object} */ (defaultSaveData), ...loadedData };
|
|
2519
|
-
}
|
|
2520
|
-
|
|
2521
|
-
/** Write save data to local storage
|
|
2522
|
-
* @param {string} saveName - unique name for the game/save
|
|
2523
|
-
* @param {object} saveData - object containing data to be saved
|
|
2524
|
-
* @memberof Utilities */
|
|
2525
|
-
function writeSaveData(saveName, saveData)
|
|
2526
|
-
{
|
|
2527
|
-
false&&ASSERT(isStringLike(saveName), 'writeSaveData requires saveName string');
|
|
2528
|
-
// tolerate localStorage being unavailable or quota exceeded
|
|
2529
|
-
try { localStorage.setItem(saveName, JSON.stringify(saveData)); }
|
|
2530
|
-
catch { false&&LOG('writeSaveData: failed to write', saveName); }
|
|
2531
|
-
}
|
|
2532
|
-
|
|
2533
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
2534
|
-
|
|
2535
|
-
// Deterministic well-distributed hash of an integer lattice index to [0, 1).
|
|
2536
|
-
// Murmur3 finalizer — adjacent integers produce uncorrelated outputs.
|
|
2537
|
-
function noiseHash(i)
|
|
2538
|
-
{
|
|
2539
|
-
let h = (i | 0) ^ 0x9e3779b9;
|
|
2540
|
-
h = Math.imul(h ^ (h >>> 16), 0x85ebca6b);
|
|
2541
|
-
h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35);
|
|
2542
|
-
h ^= h >>> 16;
|
|
2543
|
-
return (h >>> 0) / 2**32;
|
|
2544
|
-
}
|
|
2545
|
-
|
|
2546
|
-
/** 1D value noise — returns a smooth value in [0, 1] for any real x.
|
|
2547
|
-
* Integer inputs land on deterministic lattice values; non-integer inputs
|
|
2548
|
-
* are interpolated with smoothStep for C1 continuity.
|
|
2549
|
-
* @param {number} x
|
|
2550
|
-
* @return {number}
|
|
2551
|
-
* @memberof Utilities */
|
|
2552
|
-
function noise1D(x)
|
|
2553
|
-
{
|
|
2554
|
-
const i = floor(x);
|
|
2555
|
-
return lerp(noiseHash(i), noiseHash(i + 1), smoothStep(x - i));
|
|
2556
|
-
}
|
|
2557
|
-
|
|
2558
|
-
/** 2D value noise — returns a smooth value in [0, 1] for any real (x, y).
|
|
2559
|
-
* @param {number} x
|
|
2560
|
-
* @param {number} y
|
|
2561
|
-
* @return {number}
|
|
2562
|
-
* @memberof Utilities */
|
|
2563
|
-
function noise2D(x, y)
|
|
2564
|
-
{
|
|
2565
|
-
const ix = floor(x), iy = floor(y);
|
|
2566
|
-
const fx = smoothStep(x - ix), fy = smoothStep(y - iy);
|
|
2567
|
-
// large prime decorrelates neighboring rows
|
|
2568
|
-
const h = (a, b) => noiseHash(a + b * 374761393);
|
|
2569
|
-
return lerp(
|
|
2570
|
-
lerp(h(ix, iy ), h(ix + 1, iy ), fx),
|
|
2571
|
-
lerp(h(ix, iy + 1), h(ix + 1, iy + 1), fx),
|
|
2572
|
-
fy);
|
|
2299
|
+
/**
|
|
2300
|
+
* LittleJS Utility Classes and Functions
|
|
2301
|
+
* - Timer - tracks time automatically with support for pause and real-time modes
|
|
2302
|
+
* - Time formatting helper
|
|
2303
|
+
* - JSON file fetching
|
|
2304
|
+
* - File saving (text, canvas, data URLs)
|
|
2305
|
+
* - Native share dialog support
|
|
2306
|
+
* - Local storage save data management
|
|
2307
|
+
* - Value noise (1D and 2D)
|
|
2308
|
+
* @namespace Utilities
|
|
2309
|
+
*/
|
|
2310
|
+
|
|
2311
|
+
/**
|
|
2312
|
+
* Timer object tracks how long has passed since it was set
|
|
2313
|
+
* @memberof Engine
|
|
2314
|
+
* @example
|
|
2315
|
+
* let a = new Timer; // creates a timer that is not set
|
|
2316
|
+
* a.set(3); // sets the timer to 3 seconds
|
|
2317
|
+
*
|
|
2318
|
+
* let b = new Timer(1); // creates a timer with 1 second left
|
|
2319
|
+
* b.unset(); // unset the timer
|
|
2320
|
+
*/
|
|
2321
|
+
class Timer
|
|
2322
|
+
{
|
|
2323
|
+
/** Create a timer object set time passed in
|
|
2324
|
+
* @param {number} [timeLeft] - How much time left before the timer is elapsed in seconds (undefined = unset)
|
|
2325
|
+
* @param {boolean} [useRealTime] - Should the timer keep running even when the game is paused? (useful for UI) */
|
|
2326
|
+
constructor(timeLeft, useRealTime=false)
|
|
2327
|
+
{
|
|
2328
|
+
false&&ASSERT(timeLeft === undefined || isNumber(timeLeft), 'Constructed Timer is invalid.', timeLeft);
|
|
2329
|
+
this.useRealTime = useRealTime;
|
|
2330
|
+
const globalTime = this.getGlobalTime();
|
|
2331
|
+
/** @type {number|undefined} */
|
|
2332
|
+
this.time = timeLeft === undefined ? undefined : globalTime + timeLeft;
|
|
2333
|
+
/** @type {number|undefined} */
|
|
2334
|
+
this.setTime = timeLeft;
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
/** Set the timer with seconds passed in
|
|
2338
|
+
* @param {number} [timeLeft] - How much time left before the timer is elapsed in seconds */
|
|
2339
|
+
set(timeLeft=0)
|
|
2340
|
+
{
|
|
2341
|
+
false&&ASSERT(isNumber(timeLeft), 'Timer is invalid.', timeLeft);
|
|
2342
|
+
const globalTime = this.getGlobalTime();
|
|
2343
|
+
this.time = globalTime + timeLeft;
|
|
2344
|
+
this.setTime = timeLeft;
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
/** Set if the timer should keep running even when the game is paused
|
|
2348
|
+
* @param {boolean} [useRealTime] */
|
|
2349
|
+
setUseRealTime(useRealTime=true)
|
|
2350
|
+
{
|
|
2351
|
+
false&&ASSERT(!this.isSet(), 'Cannot change global time setting while timer is set.');
|
|
2352
|
+
this.useRealTime = useRealTime;
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
/** Unset the timer */
|
|
2356
|
+
unset() { this.time = undefined; }
|
|
2357
|
+
|
|
2358
|
+
/** Returns true if set
|
|
2359
|
+
* @return {boolean} */
|
|
2360
|
+
isSet() { return this.time !== undefined; }
|
|
2361
|
+
|
|
2362
|
+
/** Returns true if set and has not elapsed
|
|
2363
|
+
* @return {boolean} */
|
|
2364
|
+
active() { return this.getGlobalTime() < this.time; }
|
|
2365
|
+
|
|
2366
|
+
/** Returns true if set and elapsed
|
|
2367
|
+
* @return {boolean} */
|
|
2368
|
+
elapsed() { return this.getGlobalTime() >= this.time; }
|
|
2369
|
+
|
|
2370
|
+
/** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
|
|
2371
|
+
* @return {number} */
|
|
2372
|
+
get() { return this.isSet()? this.getGlobalTime() - this.time : 0; }
|
|
2373
|
+
|
|
2374
|
+
/** Get percentage elapsed based on time it was set to, returns 0 if not set.
|
|
2375
|
+
* Zero-duration timers report 1 (already elapsed).
|
|
2376
|
+
* @return {number} */
|
|
2377
|
+
getPercent()
|
|
2378
|
+
{
|
|
2379
|
+
if (!this.isSet()) return 0;
|
|
2380
|
+
if (!(this.setTime > 0)) return 1;
|
|
2381
|
+
return 1 - percent(this.time - this.getGlobalTime(), 0, this.setTime);
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
/** Get the time this timer was set to, returns 0 if not set
|
|
2385
|
+
* @return {number} */
|
|
2386
|
+
getSetTime() { return this.isSet() ? this.setTime : 0; }
|
|
2387
|
+
|
|
2388
|
+
/** Get the current global time this timer is based on
|
|
2389
|
+
* @return {number} */
|
|
2390
|
+
getGlobalTime() { return this.useRealTime ? timeReal : time; }
|
|
2391
|
+
|
|
2392
|
+
/** Returns this timer expressed as a string
|
|
2393
|
+
* @return {string} */
|
|
2394
|
+
toString() { return this.isSet() ? abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }
|
|
2395
|
+
|
|
2396
|
+
/** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
|
|
2397
|
+
* @return {number} */
|
|
2398
|
+
valueOf() { return this.get(); }
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2402
|
+
|
|
2403
|
+
/** Formats seconds to mm:ss style for display purposes
|
|
2404
|
+
* @param {number} t - time in seconds
|
|
2405
|
+
* @return {string}
|
|
2406
|
+
* @memberof Utilities */
|
|
2407
|
+
function formatTime(t)
|
|
2408
|
+
{
|
|
2409
|
+
const signStr = t <= -1 ? '-' : ''; // no sign when it shows 0:00
|
|
2410
|
+
t = abs(t)|0;
|
|
2411
|
+
return signStr + (t/60|0) + ':' + (t%60<10?'0':'') + t%60;
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
/** Fetches a JSON file from a URL and returns the parsed JSON object. Must be used with await!
|
|
2415
|
+
* @param {string} url - URL of JSON file
|
|
2416
|
+
* @return {Promise<object>}
|
|
2417
|
+
* @memberof Utilities */
|
|
2418
|
+
async function fetchJSON(url)
|
|
2419
|
+
{
|
|
2420
|
+
const response = await fetch(url);
|
|
2421
|
+
if (!response.ok)
|
|
2422
|
+
throw new Error(`Failed to fetch JSON from ${url}: ${response.status} ${response.statusText}`);
|
|
2423
|
+
const json = await response.json();
|
|
2424
|
+
editorJSONFetched(url, json); // debug builds remember the file a level came from, to save it by that name
|
|
2425
|
+
return json;
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2429
|
+
|
|
2430
|
+
/** Save a text file to disk
|
|
2431
|
+
* @param {string} text
|
|
2432
|
+
* @param {string} [filename]
|
|
2433
|
+
* @param {string} [type]
|
|
2434
|
+
* @memberof Utilities */
|
|
2435
|
+
function saveText(text, filename='text', type='text/plain')
|
|
2436
|
+
{ saveDataURL(URL.createObjectURL(new Blob([text], {'type':type})), filename, 1e3); } // freed once the download has it
|
|
2437
|
+
|
|
2438
|
+
/** Create an offscreen canvas to draw into, and return its 2D context
|
|
2439
|
+
* - The canvas is context.canvas, which is what TextureInfo and the like take
|
|
2440
|
+
* @param {number} width - In pixels
|
|
2441
|
+
* @param {number} [height] - In pixels, defaults to the width for a square
|
|
2442
|
+
* @param {boolean} [willReadFrequently] - Keep it in software, faster when getImageData is called on it often
|
|
2443
|
+
* @return {OffscreenCanvasRenderingContext2D}
|
|
2444
|
+
* @memberof Utilities */
|
|
2445
|
+
function createCanvasContext(width, height=width, willReadFrequently=false)
|
|
2446
|
+
{
|
|
2447
|
+
false&&ASSERT(isNumber(width) && isNumber(height), 'canvas width and height must be numbers', width, height);
|
|
2448
|
+
return new OffscreenCanvas(width, height).getContext('2d', {willReadFrequently});
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
/** Save a canvas to disk
|
|
2452
|
+
* @param {HTMLCanvasElement|OffscreenCanvas} canvas
|
|
2453
|
+
* @param {string} [filename]
|
|
2454
|
+
* @param {string} [type]
|
|
2455
|
+
* @memberof Utilities */
|
|
2456
|
+
function saveCanvas(canvas, filename='screenshot', type='image/png')
|
|
2457
|
+
{
|
|
2458
|
+
if (canvas instanceof OffscreenCanvas)
|
|
2459
|
+
{
|
|
2460
|
+
// copy to temporary canvas and save
|
|
2461
|
+
const saveCanvas = document.createElement('canvas');
|
|
2462
|
+
saveCanvas.width = canvas.width;
|
|
2463
|
+
saveCanvas.height = canvas.height;
|
|
2464
|
+
saveCanvas.getContext('2d').drawImage(canvas, 0, 0);
|
|
2465
|
+
saveDataURL(saveCanvas.toDataURL(type), filename);
|
|
2466
|
+
}
|
|
2467
|
+
else
|
|
2468
|
+
saveDataURL(canvas.toDataURL(type), filename);
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
/** Save a data url to disk
|
|
2472
|
+
* @param {string} url
|
|
2473
|
+
* @param {string} [filename]
|
|
2474
|
+
* @param {number} [revokeTime] - how long before revoking the url
|
|
2475
|
+
* @memberof Utilities */
|
|
2476
|
+
function saveDataURL(url, filename='download', revokeTime)
|
|
2477
|
+
{
|
|
2478
|
+
false&&ASSERT(isStringLike(url), 'saveDataURL requires url string');
|
|
2479
|
+
false&&ASSERT(isStringLike(filename), 'saveDataURL requires filename string');
|
|
2480
|
+
|
|
2481
|
+
// create link for saving screenshots
|
|
2482
|
+
const link = document.createElement('a');
|
|
2483
|
+
link.download = filename;
|
|
2484
|
+
link.href = url;
|
|
2485
|
+
link.click();
|
|
2486
|
+
if (revokeTime !== undefined)
|
|
2487
|
+
setTimeout(()=> URL.revokeObjectURL(url), revokeTime);
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
/** Share content using the native share dialog if available
|
|
2491
|
+
* @param {string} title - title of the share
|
|
2492
|
+
* @param {string} url - url to share
|
|
2493
|
+
* @param {Function} [callback] - Called when share is complete
|
|
2494
|
+
* @memberof Utilities */
|
|
2495
|
+
function shareURL(title, url, callback)
|
|
2496
|
+
{
|
|
2497
|
+
false&&ASSERT(isStringLike(title), 'shareURL requires title string');
|
|
2498
|
+
false&&ASSERT(isStringLike(url), 'shareURL requires url string');
|
|
2499
|
+
navigator.share?.({title, url}).then(()=>callback?.(), ()=>{}); // a player who cancels is not an error
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2503
|
+
|
|
2504
|
+
/** Read save data from local storage
|
|
2505
|
+
* - The result has the type of defaultSaveData, or any when there is none
|
|
2506
|
+
* @template {Object<string, any>} [T=any]
|
|
2507
|
+
* @param {string} saveName - unique name for the game/save
|
|
2508
|
+
* @param {T} [defaultSaveData] - default values, result is {...default, ...loaded} so this must be an object
|
|
2509
|
+
* @return {T}
|
|
2510
|
+
* @memberof Utilities */
|
|
2511
|
+
function readSaveData(saveName, defaultSaveData)
|
|
2512
|
+
{
|
|
2513
|
+
false&&ASSERT(isStringLike(saveName), 'readSaveData requires saveName string');
|
|
2514
|
+
false&&ASSERT(defaultSaveData === undefined ||
|
|
2515
|
+
(typeof defaultSaveData === 'object' && defaultSaveData !== null),
|
|
2516
|
+
'readSaveData: default must be an object - the result is ' +
|
|
2517
|
+
'{...default, ...loaded}, so a scalar default yields {}. ' +
|
|
2518
|
+
'Use readSaveData(key, {best:0}).best');
|
|
2519
|
+
|
|
2520
|
+
// tolerate localStorage being unavailable (iOS private mode, sandboxed
|
|
2521
|
+
// iframes) and corrupt JSON in stored data
|
|
2522
|
+
let loadedData = {};
|
|
2523
|
+
try
|
|
2524
|
+
{
|
|
2525
|
+
const data = localStorage.getItem(saveName); // not the brackets, a name like 'key' or 'length' is a Storage member
|
|
2526
|
+
if (data)
|
|
2527
|
+
{
|
|
2528
|
+
try { loadedData = JSON.parse(data); }
|
|
2529
|
+
catch { false&&LOG('readSaveData: corrupt JSON for', saveName, '— using defaults'); }
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
catch { false&&LOG('readSaveData: localStorage unavailable — using defaults'); }
|
|
2533
|
+
return { .../** @type {object} */ (defaultSaveData), ...loadedData };
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
/** Write save data to local storage
|
|
2537
|
+
* @param {string} saveName - unique name for the game/save
|
|
2538
|
+
* @param {object} saveData - object containing data to be saved
|
|
2539
|
+
* @memberof Utilities */
|
|
2540
|
+
function writeSaveData(saveName, saveData)
|
|
2541
|
+
{
|
|
2542
|
+
false&&ASSERT(isStringLike(saveName), 'writeSaveData requires saveName string');
|
|
2543
|
+
// tolerate localStorage being unavailable or quota exceeded
|
|
2544
|
+
try { localStorage.setItem(saveName, JSON.stringify(saveData)); }
|
|
2545
|
+
catch { false&&LOG('writeSaveData: failed to write', saveName); }
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2549
|
+
|
|
2550
|
+
// Deterministic well-distributed hash of an integer lattice index to [0, 1).
|
|
2551
|
+
// Murmur3 finalizer — adjacent integers produce uncorrelated outputs.
|
|
2552
|
+
function noiseHash(i)
|
|
2553
|
+
{
|
|
2554
|
+
let h = (i | 0) ^ 0x9e3779b9;
|
|
2555
|
+
h = Math.imul(h ^ (h >>> 16), 0x85ebca6b);
|
|
2556
|
+
h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35);
|
|
2557
|
+
h ^= h >>> 16;
|
|
2558
|
+
return (h >>> 0) / 2**32;
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
/** 1D value noise — returns a smooth value in [0, 1] for any real x.
|
|
2562
|
+
* Integer inputs land on deterministic lattice values; non-integer inputs
|
|
2563
|
+
* are interpolated with smoothStep for C1 continuity.
|
|
2564
|
+
* @param {number} x
|
|
2565
|
+
* @return {number}
|
|
2566
|
+
* @memberof Utilities */
|
|
2567
|
+
function noise1D(x)
|
|
2568
|
+
{
|
|
2569
|
+
const i = floor(x);
|
|
2570
|
+
return lerp(noiseHash(i), noiseHash(i + 1), smoothStep(x - i));
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
/** 2D value noise — returns a smooth value in [0, 1] for any real (x, y).
|
|
2574
|
+
* @param {number} x
|
|
2575
|
+
* @param {number} y
|
|
2576
|
+
* @return {number}
|
|
2577
|
+
* @memberof Utilities */
|
|
2578
|
+
function noise2D(x, y)
|
|
2579
|
+
{
|
|
2580
|
+
const ix = floor(x), iy = floor(y);
|
|
2581
|
+
const fx = smoothStep(x - ix), fy = smoothStep(y - iy);
|
|
2582
|
+
// large prime decorrelates neighboring rows
|
|
2583
|
+
const h = (a, b) => noiseHash(a + b * 374761393);
|
|
2584
|
+
return lerp(
|
|
2585
|
+
lerp(h(ix, iy ), h(ix + 1, iy ), fx),
|
|
2586
|
+
lerp(h(ix, iy + 1), h(ix + 1, iy + 1), fx),
|
|
2587
|
+
fy);
|
|
2573
2588
|
}
|
|
2574
2589
|
/**
|
|
2575
2590
|
* LittleJS Engine Settings
|
|
@@ -2884,6 +2899,15 @@ let touchGamepadEnable = false;
|
|
|
2884
2899
|
* @memberof Settings */
|
|
2885
2900
|
let touchGamepadPassthrough = false;
|
|
2886
2901
|
|
|
2902
|
+
/** True if a pinch of two fingers turns the mouse wheel: touchPinch is added to mouseWheel, so what zooms with
|
|
2903
|
+
* the wheel zooms with a pinch, the mouse is between the two fingers, and a second finger lets go of the button
|
|
2904
|
+
* the first pressed, since a pinch is not a drag; turn it off for a game with its own use for two fingers, the
|
|
2905
|
+
* first finger stays the mouse and touchPinch can still be read
|
|
2906
|
+
* @type {boolean}
|
|
2907
|
+
* @default
|
|
2908
|
+
* @memberof Settings */
|
|
2909
|
+
let touchPinchWheel = true;
|
|
2910
|
+
|
|
2887
2911
|
/** Size of center button if touch gamepad should have start button in the center
|
|
2888
2912
|
* - Prevents activating when pressed near virtual stick or face buttons
|
|
2889
2913
|
* - When the game is paused, any touch will press the button
|
|
@@ -3279,6 +3303,11 @@ function setTouchInputEnable(enable) { touchInputEnable = enable; }
|
|
|
3279
3303
|
* @memberof Settings */
|
|
3280
3304
|
function setTouchGamepadEnable(enable) { touchGamepadEnable = enable; }
|
|
3281
3305
|
|
|
3306
|
+
/** Set if a pinch of two fingers turns the mouse wheel
|
|
3307
|
+
* @param {boolean} enable
|
|
3308
|
+
* @memberof Settings */
|
|
3309
|
+
function setTouchPinchWheel(enable) { touchPinchWheel = enable; }
|
|
3310
|
+
|
|
3282
3311
|
/** Set if touches outside the gamepad controls should still drive mouse/touch input
|
|
3283
3312
|
* @param {boolean} passthrough
|
|
3284
3313
|
* @memberof Settings */
|
|
@@ -3318,776 +3347,776 @@ function setTouchGamepadLeftStick(enable)
|
|
|
3318
3347
|
function setTouchGamepadLeftButtonCount(count)
|
|
3319
3348
|
{
|
|
3320
3349
|
touchGamepadLeftButtonCount = count;
|
|
3321
|
-
if (count > 0)
|
|
3322
|
-
touchGamepadLeftStick = false;
|
|
3323
|
-
}
|
|
3324
|
-
|
|
3325
|
-
/** Set if the touch gamepad right side is an analog stick (or dpad) instead of face buttons
|
|
3326
|
-
* @param {boolean} rightStick
|
|
3327
|
-
* @memberof Settings */
|
|
3328
|
-
function setTouchGamepadRightStick(rightStick)
|
|
3329
|
-
{
|
|
3330
|
-
touchGamepadRightStick = rightStick;
|
|
3331
|
-
if (rightStick)
|
|
3332
|
-
touchGamepadButtonCount = 0;
|
|
3333
|
-
}
|
|
3334
|
-
|
|
3335
|
-
/** Set if touch gamepad should be analog stick or 8 way dpad
|
|
3336
|
-
* @param {boolean} analog
|
|
3337
|
-
* @memberof Settings */
|
|
3338
|
-
function setTouchGamepadAnalog(analog) { touchGamepadAnalog = analog; }
|
|
3339
|
-
|
|
3340
|
-
/** Set if touch gamepad directional controls should float to where you press
|
|
3341
|
-
* @param {boolean} floating
|
|
3342
|
-
* @memberof Settings */
|
|
3343
|
-
function setTouchGamepadFloating(floating) { touchGamepadFloating = floating; }
|
|
3344
|
-
|
|
3345
|
-
/** Set size of virtual gamepad for touch devices in viewport CSS pixels
|
|
3346
|
-
* @param {number} size
|
|
3347
|
-
* @memberof Settings */
|
|
3348
|
-
function setTouchGamepadSize(size) { touchGamepadSize = size; }
|
|
3349
|
-
|
|
3350
|
-
/** Set transparency of touch gamepad overlay
|
|
3351
|
-
* @param {number} alpha
|
|
3352
|
-
* @memberof Settings */
|
|
3353
|
-
function setTouchGamepadAlpha(alpha) { touchGamepadAlpha = alpha; }
|
|
3354
|
-
|
|
3355
|
-
/** Set how long to display the touch gamepad on screen in seconds, set to 0 to always display
|
|
3356
|
-
* @param {number} time
|
|
3357
|
-
* @memberof Settings */
|
|
3358
|
-
function setTouchGamepadDisplayTime(time) { touchGamepadDisplayTime = time; }
|
|
3359
|
-
|
|
3360
|
-
/** Set duration in ms to vibrate when a touch gamepad face or start button is pressed (0 disables)
|
|
3361
|
-
* @param {number} ms
|
|
3362
|
-
* @memberof Settings */
|
|
3363
|
-
function setTouchGamepadVibration(ms) { touchGamepadVibration = ms; }
|
|
3364
|
-
|
|
3365
|
-
/** Set to allow vibration hardware if it exists
|
|
3366
|
-
* @param {boolean} enable
|
|
3367
|
-
* @memberof Settings */
|
|
3368
|
-
function setVibrateEnable(enable) { vibrateEnable = enable; }
|
|
3369
|
-
|
|
3370
|
-
/** Set if audio is enabled, false turns all sound off
|
|
3371
|
-
* @param {boolean} enable
|
|
3372
|
-
* @memberof Settings */
|
|
3373
|
-
function setSoundEnable(enable) { soundEnable = enable; }
|
|
3374
|
-
|
|
3375
|
-
/** Set volume scale to apply to all sound, music and speech
|
|
3376
|
-
* @param {number} volume
|
|
3377
|
-
* @memberof Settings */
|
|
3378
|
-
function setSoundVolume(volume)
|
|
3379
|
-
{
|
|
3380
|
-
soundVolume = volume;
|
|
3381
|
-
audioUpdateVolume(); // update gain immediately, sound off or not
|
|
3382
|
-
}
|
|
3383
|
-
|
|
3384
|
-
/** Set default range where sound no longer plays
|
|
3385
|
-
* @param {number} range
|
|
3386
|
-
* @memberof Settings */
|
|
3387
|
-
function setSoundDefaultRange(range) { soundDefaultRange = range; }
|
|
3388
|
-
|
|
3389
|
-
/** Set default range percent to start tapering off sound
|
|
3390
|
-
* @param {number} taper
|
|
3391
|
-
* @memberof Settings */
|
|
3392
|
-
function setSoundDefaultTaper(taper) { soundDefaultTaper = taper; }
|
|
3393
|
-
|
|
3394
|
-
/** Set if all sound pauses while the page is hidden
|
|
3395
|
-
* @param {boolean} pause
|
|
3396
|
-
* @memberof Settings */
|
|
3397
|
-
function setSoundPauseWhenHidden(pause) { soundPauseWhenHidden = pause; }
|
|
3398
|
-
|
|
3399
|
-
/** Set if watermark with FPS should be shown
|
|
3400
|
-
* @param {boolean} show
|
|
3401
|
-
* @memberof Debug */
|
|
3402
|
-
function setDebugWatermark(show) { debugWatermark = show; }
|
|
3403
|
-
|
|
3404
|
-
/** Set key code used to toggle debug mode, Esc by default
|
|
3405
|
-
* @param {string} key
|
|
3406
|
-
* @memberof Debug */
|
|
3407
|
-
function setDebugKey(key) { debugKey = key; }
|
|
3408
|
-
|
|
3409
|
-
/** Set if the debug keys work while the overlay is closed, the number keys and the +/- time keys
|
|
3410
|
-
* @param {boolean} [enable]
|
|
3411
|
-
* @memberof Debug */
|
|
3412
|
-
function setDebugKeysAlways(enable=true) { debugKeysAlways = enable; }
|
|
3413
|
-
|
|
3414
|
-
/** Set if the tweakables panel is shown, as 9 does while the debug overlay is open
|
|
3415
|
-
* @param {boolean} [show]
|
|
3416
|
-
* @memberof Debug */
|
|
3417
|
-
function setDebugTweakables(show=true) { debugTweakables = show; }
|
|
3418
|
-
/**
|
|
3419
|
-
* LittleJS Object System
|
|
3420
|
-
* - EngineObject is the base class for all game objects
|
|
3421
|
-
* - Handles automatic updating, rendering, physics, and collision
|
|
3422
|
-
* - Supports parent-child hierarchies with transform inheritance
|
|
3423
|
-
* - 2D physics with velocity, acceleration, damping, and gravity
|
|
3424
|
-
* - Collision system with tiles and other objects
|
|
3425
|
-
* - Renders sprites from tile sheets with color and rotation
|
|
3426
|
-
* - Objects sorted by renderOrder for layered rendering
|
|
3427
|
-
*/
|
|
3428
|
-
|
|
3429
|
-
/**
|
|
3430
|
-
* LittleJS Object Base Object Class
|
|
3431
|
-
* - Top level object class used by the engine
|
|
3432
|
-
* - Automatically adds self to object list
|
|
3433
|
-
* - Will be updated and rendered each frame
|
|
3434
|
-
* - Renders as a sprite from a tilesheet by default
|
|
3435
|
-
* - Can have color and additive color applied
|
|
3436
|
-
* - 2D Physics and collision system
|
|
3437
|
-
* - Sorted by renderOrder
|
|
3438
|
-
* - Objects can have children attached
|
|
3439
|
-
* - Parents are updated before children, and set child transform
|
|
3440
|
-
* - Call destroy() to get rid of objects
|
|
3441
|
-
*
|
|
3442
|
-
* The physics system used by objects is simple and fast with some caveats...
|
|
3443
|
-
* - Collision uses the axis aligned size, the object's rotation angle is only for rendering
|
|
3444
|
-
* - Objects are guaranteed to not intersect tile collision from physics
|
|
3445
|
-
* - If an object starts or is moved inside tile collision, it will not collide with that tile
|
|
3446
|
-
* - Collision for objects can be set to be solid to block other objects
|
|
3447
|
-
* - Objects may get pushed into overlapping other solid objects, if so they will push away
|
|
3448
|
-
* - A static solid (mass 0) moved by its velocity, like a door or an elevator, pushes objects out of its way
|
|
3449
|
-
* - Solid objects are more performance intensive and should be used sparingly
|
|
3450
|
-
* @memberof Engine
|
|
3451
|
-
* @example
|
|
3452
|
-
* // create an engine object, normally you would first extend the class with your own
|
|
3453
|
-
* const pos = vec2(2,3);
|
|
3454
|
-
* const object = new EngineObject(pos);
|
|
3455
|
-
*/
|
|
3456
|
-
class EngineObject
|
|
3457
|
-
{
|
|
3458
|
-
/** Create an engine object and adds it to the list of objects
|
|
3459
|
-
* @param {Vector2} [pos=vec2()] - World space position of the object
|
|
3460
|
-
* @param {Vector2} [size=vec2(1)] - World space size of the object
|
|
3461
|
-
* @param {TileInfo} [tileInfo] - Tile info to render object (undefined is untextured)
|
|
3462
|
-
* @param {number} [angle] - Angle the object is rotated by
|
|
3463
|
-
* @param {Color} [color=WHITE] - Color to apply to tile when rendered
|
|
3464
|
-
* @param {number} [renderOrder] - Objects sorted by renderOrder before being rendered
|
|
3465
|
-
*/
|
|
3466
|
-
constructor(pos=vec2(), size=vec2(1), tileInfo, angle=0, color=WHITE, renderOrder=0)
|
|
3467
|
-
{
|
|
3468
|
-
// check passed in params
|
|
3469
|
-
false&&ASSERT(isVector2(pos), 'object pos must be a vec2');
|
|
3470
|
-
false&&ASSERT(isVector2(size), 'object size must be a vec2');
|
|
3471
|
-
false&&ASSERT(!tileInfo || tileInfo instanceof TileInfo, 'object tileInfo should be a TileInfo or undefined');
|
|
3472
|
-
false&&ASSERT(typeof angle === 'number' && isFinite(angle), 'object angle should be a number');
|
|
3473
|
-
false&&ASSERT(isColor(color), 'object color should be a valid rgba color');
|
|
3474
|
-
false&&ASSERT(typeof renderOrder === 'number', 'object renderOrder should be a number');
|
|
3475
|
-
|
|
3476
|
-
/** @property {Vector2} - World space position of the object */
|
|
3477
|
-
this.pos = pos.copy();
|
|
3478
|
-
/** @property {Vector2} - World space width and height of the object */
|
|
3479
|
-
this.size = size.copy();
|
|
3480
|
-
/** @property {Vector2|undefined} - Size of object used for drawing, uses size if not set
|
|
3481
|
-
* @type {Vector2|undefined} */
|
|
3482
|
-
this.drawSize = undefined;
|
|
3483
|
-
/** @property {TileInfo|undefined} - Tile info to render object (undefined is untextured)
|
|
3484
|
-
* @type {TileInfo|undefined} */
|
|
3485
|
-
this.tileInfo = tileInfo;
|
|
3486
|
-
/** @property {number} - Angle to rotate the object */
|
|
3487
|
-
this.angle = angle;
|
|
3488
|
-
/** @property {Color} - Color to apply when rendered */
|
|
3489
|
-
this.color = color.copy();
|
|
3490
|
-
/** @property {Color|undefined} - Additive color to apply when rendered
|
|
3491
|
-
* @type {Color|undefined} */
|
|
3492
|
-
this.additiveColor = undefined;
|
|
3493
|
-
/** @property {Shader|undefined} - Custom shader to render with, undefined for the engine's own
|
|
3494
|
-
* @type {Shader|undefined} */
|
|
3495
|
-
this.shader = undefined;
|
|
3496
|
-
/** @property {boolean} - Does this object draw into the light system's shadow map; false for a floor layer, a background, a pickup */
|
|
3497
|
-
this.castShadow = true;
|
|
3498
|
-
/** @property {number} - With the light system, how much it lights itself: 0 lit only by the lights, 1 full
|
|
3499
|
-
* brightness in its own colors whatever the lights do, between partly; drawn into the lightmap through
|
|
3500
|
-
* renderEmissive, as 3D's emissive. Exact for solid pixels; a partly transparent one is self lit by its alpha
|
|
3501
|
-
* too, so a half alpha pixel shows at a quarter and a fading emissive sprite fades a little faster */
|
|
3502
|
-
this.emissive = 0;
|
|
3503
|
-
/** @property {boolean} - Should the rendered tile flip along the y axis. Affects rendering and the local→world transform of attached children (a mirrored parent flips its children's localPos.x and localAngle). Does not affect this object's own physics, collision, or localToWorld/worldToLocal. */
|
|
3504
|
-
this.mirror = false;
|
|
3505
|
-
/** @property {boolean} - Has object been destroyed? */
|
|
3506
|
-
this.destroyed = false;
|
|
3507
|
-
this.updatePass = 0; // the engine update pass it was last updated in, so nothing updates twice in one
|
|
3508
|
-
|
|
3509
|
-
// physical properties
|
|
3510
|
-
/** @property {number} - How heavy the object is, static if 0: a static object moves by its velocity but does not
|
|
3511
|
-
* collide on its own, the moving ones collide with it */
|
|
3512
|
-
this.mass = objectDefaultMass;
|
|
3513
|
-
/** @property {number} - Fraction of velocity kept each frame, 1 keeps all of it, 0 stops at once */
|
|
3514
|
-
this.damping = objectDefaultDamping;
|
|
3515
|
-
/** @property {number} - Fraction of angular velocity kept each frame, 1 keeps all of it, 0 stops at once */
|
|
3516
|
-
this.angleDamping = objectDefaultAngleDamping;
|
|
3517
|
-
/** @property {number} - How bouncy the object is when colliding (0-1) */
|
|
3518
|
-
this.restitution = objectDefaultRestitution;
|
|
3519
|
-
/** @property {number} - Fraction of sliding speed kept each frame on the ground, 1 is no friction, 0 stops at
|
|
3520
|
-
* once, the more slippery of the object and its ground is used */
|
|
3521
|
-
this.friction = objectDefaultFriction;
|
|
3522
|
-
/** @property {number} - How much to scale gravity by for this object */
|
|
3523
|
-
this.gravityScale = 1;
|
|
3524
|
-
/** @property {number} - Objects are sorted by render order */
|
|
3525
|
-
this.renderOrder = renderOrder;
|
|
3526
|
-
/** @property {Vector2} - Velocity of the object, in world units per frame */
|
|
3527
|
-
this.velocity = vec2();
|
|
3528
|
-
/** @property {number} - Angular velocity of the object, in radians per frame */
|
|
3529
|
-
this.angleVelocity = 0;
|
|
3530
|
-
/** @property {number} - Track when object was created */
|
|
3531
|
-
this.spawnTime = time;
|
|
3532
|
-
/** @property {Array<EngineObject>} - List of children of this object
|
|
3533
|
-
* @type {Array<EngineObject>} */
|
|
3534
|
-
this.children = [];
|
|
3535
|
-
/** @property {boolean} - Limit object speed along x and y axis */
|
|
3536
|
-
this.clampSpeed = true;
|
|
3537
|
-
/** @property {EngineObject|undefined} - Object we are standing on, if any
|
|
3538
|
-
* @type {EngineObject|undefined} */
|
|
3539
|
-
this.groundObject = undefined;
|
|
3540
|
-
|
|
3541
|
-
// parent child system
|
|
3542
|
-
/** @property {EngineObject|undefined} - Parent of object if in local space
|
|
3543
|
-
* @type {EngineObject|undefined} */
|
|
3544
|
-
this.parent = undefined;
|
|
3545
|
-
/** @property {Vector2|undefined} - Position relative to the parent, only while attached to one
|
|
3546
|
-
* @type {Vector2|undefined} */
|
|
3547
|
-
this.localPos = undefined;
|
|
3548
|
-
/** @property {number} - Local angle if child */
|
|
3549
|
-
this.localAngle = 0;
|
|
3550
|
-
|
|
3551
|
-
// collision flags
|
|
3552
|
-
/** @property {boolean} - Object collides with the level, its tile collision layers
|
|
3553
|
-
* @type {boolean} */
|
|
3554
|
-
this.collideLevel = false;
|
|
3555
|
-
/** @property {boolean} - Object collides with solid objects */
|
|
3556
|
-
this.collideSolidObjects = false;
|
|
3557
|
-
/** @property {boolean} - Object collides with and blocks other objects */
|
|
3558
|
-
this.isSolid = false;
|
|
3559
|
-
/** @property {boolean} - Object collides with raycasts */
|
|
3560
|
-
this.collideRaycast = false;
|
|
3561
|
-
|
|
3562
|
-
/** @property {boolean} - Object is skipped by engineObjectsDestroy, for things that outlive a level like a camera
|
|
3563
|
-
* - Calling destroy on it still destroys it, and its children go with it either way */
|
|
3564
|
-
this.persistent = false;
|
|
3565
|
-
|
|
3566
|
-
// add to list of objects
|
|
3567
|
-
engineObjects.push(this);
|
|
3568
|
-
}
|
|
3569
|
-
|
|
3570
|
-
/** Update the object transform, called automatically by engine even when paused
|
|
3571
|
-
* @param {boolean} [updateChildren] - Also update the children's transforms */
|
|
3572
|
-
updateTransforms(updateChildren=true)
|
|
3573
|
-
{
|
|
3574
|
-
const parent = this.parent;
|
|
3575
|
-
if (parent)
|
|
3576
|
-
{
|
|
3577
|
-
// compose with parent transform inline to avoid intermediate vector allocs
|
|
3578
|
-
const mirror = parent.getMirrorSign();
|
|
3579
|
-
const lp = this.localPos, pp = parent.pos;
|
|
3580
|
-
const lx = lp.x*mirror, ly = lp.y, pa = parent.angle;
|
|
3581
|
-
if (pa)
|
|
3582
|
-
{
|
|
3583
|
-
const c = cos(-pa), s = sin(-pa);
|
|
3584
|
-
this.pos.set(lx*c - ly*s + pp.x, lx*s + ly*c + pp.y);
|
|
3585
|
-
}
|
|
3586
|
-
else
|
|
3587
|
-
this.pos.set(lx + pp.x, ly + pp.y);
|
|
3588
|
-
this.angle = mirror*this.localAngle + pa;
|
|
3589
|
-
}
|
|
3590
|
-
|
|
3591
|
-
// update children
|
|
3592
|
-
if (updateChildren)
|
|
3593
|
-
for (const child of this.children)
|
|
3594
|
-
child.updateTransforms();
|
|
3595
|
-
}
|
|
3596
|
-
|
|
3597
|
-
/** Update the object physics, called automatically by engine once each frame. Can be overridden to stop or change how physics works for an object. */
|
|
3598
|
-
updatePhysics()
|
|
3599
|
-
{
|
|
3600
|
-
// child objects do not have physics
|
|
3601
|
-
false&&ASSERT(!this.parent);
|
|
3602
|
-
|
|
3603
|
-
// bail if a collision callback destroyed us mid-frame
|
|
3604
|
-
if (this.destroyed) return;
|
|
3605
|
-
|
|
3606
|
-
// limit max speed to prevent missing collisions, only for what collides: with solids, or with tiles while it
|
|
3607
|
-
// has a mass, which tile collision needs; anything else moves as fast as it is told
|
|
3608
|
-
if (this.clampSpeed && enablePhysicsSolver && (this.collideSolidObjects || this.collideLevel && this.mass))
|
|
3609
|
-
{
|
|
3610
|
-
this.velocity.x = clamp(this.velocity.x, -objectMaxSpeed, objectMaxSpeed);
|
|
3611
|
-
this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
|
|
3612
|
-
}
|
|
3613
|
-
|
|
3614
|
-
// physics sanity checks
|
|
3615
|
-
false&&ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1, 'angleDamping must be 0 to 1, the fraction kept each frame');
|
|
3616
|
-
false&&ASSERT(this.damping >= 0 && this.damping <= 1, 'damping must be 0 to 1, the fraction of velocity kept each frame');
|
|
3617
|
-
|
|
3618
|
-
// apply physics; only the solver needs where the object was, so only then is it copied
|
|
3619
|
-
const solve = enablePhysicsSolver && this.mass;
|
|
3620
|
-
const oldPos = solve ? this.pos.copy() : undefined;
|
|
3621
|
-
this.velocity.x *= this.damping;
|
|
3622
|
-
this.velocity.y *= this.damping;
|
|
3623
|
-
if (this.mass)
|
|
3624
|
-
{
|
|
3625
|
-
// apply gravity only if it has mass
|
|
3626
|
-
this.velocity.x += gravity.x * this.gravityScale;
|
|
3627
|
-
this.velocity.y += gravity.y * this.gravityScale;
|
|
3628
|
-
}
|
|
3629
|
-
this.pos.x += this.velocity.x;
|
|
3630
|
-
this.pos.y += this.velocity.y;
|
|
3631
|
-
this.angle += this.angleVelocity *= this.angleDamping;
|
|
3632
|
-
|
|
3633
|
-
// don't do collision for static objects or if solver disabled
|
|
3634
|
-
if (!solve) return;
|
|
3635
|
-
|
|
3636
|
-
// which way is down for this object, a negative gravityScale falls up and lands on ceilings
|
|
3637
|
-
const gravityY = this.gravityScale < 0 ? -gravity.y : gravity.y;
|
|
3638
|
-
const wasFalling = this.velocity.y < 0 && gravityY < 0 || this.velocity.y > 0 && gravityY > 0;
|
|
3639
|
-
if (this.groundObject)
|
|
3640
|
-
{
|
|
3641
|
-
// apply friction in local space of ground object
|
|
3642
|
-
const friction = max(this.friction, this.groundObject.friction);
|
|
3643
|
-
const groundSpeed = this.groundObject.velocity.x;
|
|
3644
|
-
this.velocity.x = groundSpeed + (this.velocity.x - groundSpeed) * friction;
|
|
3645
|
-
this.groundObject = undefined;
|
|
3646
|
-
}
|
|
3647
|
-
|
|
3648
|
-
// an object with no width or height has no box to push out of, or to be pushed out of
|
|
3649
|
-
if (this.collideSolidObjects && this.size.x && this.size.y)
|
|
3650
|
-
{
|
|
3651
|
-
// check collisions against solid objects
|
|
3652
|
-
const epsilon = .001; // necessary to push slightly outside of the collision
|
|
3653
|
-
for (const o of engineObjectsCollideStaticLast)
|
|
3654
|
-
{
|
|
3655
|
-
// skip destroyed, child objects, self collision, or objects with no box
|
|
3656
|
-
if (o.destroyed || o.parent || o === this || !o.size.x || !o.size.y) continue;
|
|
3657
|
-
|
|
3658
|
-
// non solid objects don't collide with each other
|
|
3659
|
-
if (!this.isSolid && !o.isSolid) continue;
|
|
3660
|
-
|
|
3661
|
-
// check collision
|
|
3662
|
-
if (!this.isOverlappingObject(o)) continue;
|
|
3663
|
-
|
|
3664
|
-
// each moving object checks its own contacts, so a pair the other one already asked about this frame
|
|
3665
|
-
// is not asked twice: left overlapping, ignored or only nudged apart it is skipped, and one both said
|
|
3666
|
-
// to resolve, pushed back together since, is resolved again
|
|
3667
|
-
const answer = engineObjectsCollidePairAnswer(o, this);
|
|
3668
|
-
if (answer === false) continue;
|
|
3669
|
-
if (!answer)
|
|
3670
|
-
{
|
|
3671
|
-
// notify objects of collision and check if should be resolved
|
|
3672
|
-
const collide1 = this.collideWithObject(o);
|
|
3673
|
-
const collide2 = o.collideWithObject(this);
|
|
3674
|
-
if (!collide1 || !collide2)
|
|
3675
|
-
{
|
|
3676
|
-
engineObjectsCollidePairAdd(this, o);
|
|
3677
|
-
continue;
|
|
3678
|
-
}
|
|
3679
|
-
}
|
|
3680
|
-
|
|
3681
|
-
const wasOverlapping = isOverlapping(oldPos, this.size, o.pos, o.size);
|
|
3682
|
-
if (wasOverlapping && (!o.mass || o.groundObject))
|
|
3683
|
-
{
|
|
3684
|
-
// a static solid that moved into it, like a door or an elevator, pushes it out the shortest way
|
|
3685
|
-
// at once and carries it along, it would only drift out slowly and the solid would pass through;
|
|
3686
|
-
// an object standing on something counts as fixed too, so a stack on an elevator rides together;
|
|
3687
|
-
// it bounces off relative to the mover, a paddle moved by setting pos bounces a ball as a wall does
|
|
3688
|
-
const push = collideBoxBox(this.pos, this.size, o.pos, o.size);
|
|
3689
|
-
if (push)
|
|
3690
|
-
{
|
|
3691
|
-
this.pos.x += push.x + sign(push.x) * epsilon;
|
|
3692
|
-
this.pos.y += push.y + sign(push.y) * epsilon;
|
|
3693
|
-
const restitution = max(this.restitution, o.restitution);
|
|
3694
|
-
if (push.x)
|
|
3695
|
-
{
|
|
3696
|
-
const v = this.velocity.x - o.velocity.x;
|
|
3697
|
-
if (v * push.x < 0) // moving into it
|
|
3698
|
-
this.velocity.x = o.velocity.x - v * restitution;
|
|
3699
|
-
}
|
|
3700
|
-
else
|
|
3701
|
-
{
|
|
3702
|
-
const v = this.velocity.y - o.velocity.y;
|
|
3703
|
-
if (v * push.y < 0) // moving into it
|
|
3704
|
-
this.velocity.y = o.velocity.y - v * restitution;
|
|
3705
|
-
if (push.y * gravityY < 0) // pushed up against gravity, it stands on it
|
|
3706
|
-
this.groundObject = o;
|
|
3707
|
-
}
|
|
3708
|
-
}
|
|
3709
|
-
engineObjectsCollidePairAdd(this, o);
|
|
3710
|
-
|
|
3711
|
-
debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f00', undefined, false);
|
|
3712
|
-
continue;
|
|
3713
|
-
}
|
|
3714
|
-
if (wasOverlapping)
|
|
3715
|
-
{
|
|
3716
|
-
// if already was touching, try to push away
|
|
3717
|
-
const deltaPos = oldPos.subtract(o.pos);
|
|
3718
|
-
const length = deltaPos.length();
|
|
3719
|
-
const pushAwayAccel = .001;
|
|
3720
|
-
const velocity = length < .001 ? vec2(0, pushAwayAccel) : deltaPos.scale(pushAwayAccel/length);
|
|
3721
|
-
this.velocity = this.velocity.add(velocity);
|
|
3722
|
-
if (o.mass) // push away other object if not fixed
|
|
3723
|
-
o.velocity = o.velocity.subtract(velocity);
|
|
3724
|
-
engineObjectsCollidePairAdd(this, o);
|
|
3725
|
-
|
|
3726
|
-
debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f00', undefined, false);
|
|
3727
|
-
continue;
|
|
3728
|
-
}
|
|
3729
|
-
|
|
3730
|
-
// check for collision
|
|
3731
|
-
const sizeBoth = this.size.add(o.size);
|
|
3732
|
-
// prefer to push up if small delta, up away from this object's own gravity
|
|
3733
|
-
const smallStepUp = (gravityY > 0 ? o.pos.y - oldPos.y : oldPos.y - o.pos.y)*2 > sizeBoth.y - abs(gravityY);
|
|
3734
|
-
const isBlockedX = abs(oldPos.y - o.pos.y)*2 < sizeBoth.y;
|
|
3735
|
-
const isBlockedY = abs(oldPos.x - o.pos.x)*2 < sizeBoth.x;
|
|
3736
|
-
const restitution = max(this.restitution, o.restitution);
|
|
3737
|
-
|
|
3738
|
-
if (smallStepUp || isBlockedY || !isBlockedX) // resolve y collision
|
|
3739
|
-
{
|
|
3740
|
-
// push outside object collision
|
|
3741
|
-
this.pos.y = o.pos.y + (sizeBoth.y/2 + epsilon) * sign(oldPos.y - o.pos.y);
|
|
3742
|
-
if ((o.groundObject && wasFalling) || !o.mass)
|
|
3743
|
-
{
|
|
3744
|
-
// set ground object if landed on something
|
|
3745
|
-
if (wasFalling)
|
|
3746
|
-
this.groundObject = o;
|
|
3747
|
-
|
|
3748
|
-
// bounce if other object is fixed or grounded, relative to it so a rider keeps up with a
|
|
3749
|
-
// platform moving down instead of landing on it again every few frames
|
|
3750
|
-
this.velocity.y = o.velocity.y - (this.velocity.y - o.velocity.y) * restitution;
|
|
3751
|
-
}
|
|
3752
|
-
else // o has mass here, the other branch already handled the massless case
|
|
3753
|
-
{
|
|
3754
|
-
// inelastic collision
|
|
3755
|
-
const inelastic = (this.mass * this.velocity.y + o.mass * o.velocity.y) / (this.mass + o.mass);
|
|
3756
|
-
|
|
3757
|
-
// elastic collision
|
|
3758
|
-
const elastic0 = this.velocity.y * (this.mass - o.mass) / (this.mass + o.mass)
|
|
3759
|
-
+ o.velocity.y * 2 * o.mass / (this.mass + o.mass);
|
|
3760
|
-
const elastic1 = o.velocity.y * (o.mass - this.mass) / (this.mass + o.mass)
|
|
3761
|
-
+ this.velocity.y * 2 * this.mass / (this.mass + o.mass);
|
|
3762
|
-
|
|
3763
|
-
// lerp between elastic or inelastic based on restitution
|
|
3764
|
-
this.velocity.y = lerp(inelastic, elastic0, restitution);
|
|
3765
|
-
o.velocity.y = lerp(inelastic, elastic1, restitution);
|
|
3766
|
-
}
|
|
3767
|
-
}
|
|
3768
|
-
if (!smallStepUp && isBlockedX) // resolve x collision
|
|
3769
|
-
{
|
|
3770
|
-
// push outside collision
|
|
3771
|
-
this.pos.x = o.pos.x + (sizeBoth.x/2 + epsilon) * sign(oldPos.x - o.pos.x);
|
|
3772
|
-
if (o.mass)
|
|
3773
|
-
{
|
|
3774
|
-
// inelastic collision
|
|
3775
|
-
const inelastic = (this.mass * this.velocity.x + o.mass * o.velocity.x) / (this.mass + o.mass);
|
|
3776
|
-
|
|
3777
|
-
// elastic collision
|
|
3778
|
-
const elastic0 = this.velocity.x * (this.mass - o.mass) / (this.mass + o.mass)
|
|
3779
|
-
+ o.velocity.x * 2 * o.mass / (this.mass + o.mass);
|
|
3780
|
-
const elastic1 = o.velocity.x * (o.mass - this.mass) / (this.mass + o.mass)
|
|
3781
|
-
+ this.velocity.x * 2 * this.mass / (this.mass + o.mass);
|
|
3782
|
-
|
|
3783
|
-
// lerp between elastic or inelastic based on restitution
|
|
3784
|
-
this.velocity.x = lerp(inelastic, elastic0, restitution);
|
|
3785
|
-
o.velocity.x = lerp(inelastic, elastic1, restitution);
|
|
3786
|
-
}
|
|
3787
|
-
else // bounce if other object is fixed, relative to it as a landing is
|
|
3788
|
-
this.velocity.x = o.velocity.x - (this.velocity.x - o.velocity.x) * restitution;
|
|
3789
|
-
}
|
|
3790
|
-
engineObjectsCollidePairAdd(this, o, true);
|
|
3791
|
-
debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f0f', undefined, false);
|
|
3792
|
-
}
|
|
3793
|
-
}
|
|
3794
|
-
if (this.collideLevel)
|
|
3795
|
-
{
|
|
3796
|
-
// check collision against tiles
|
|
3797
|
-
const hitLayer = tileCollisionTest(this.pos, this.size, this);
|
|
3798
|
-
if (hitLayer)
|
|
3799
|
-
{
|
|
3800
|
-
// if already was stuck in collision, don't do anything
|
|
3801
|
-
// this should not happen unless something starts in collision
|
|
3802
|
-
if (!tileCollisionTest(oldPos, this.size, this))
|
|
3803
|
-
{
|
|
3804
|
-
// test which side we bounced off (or both if a corner)
|
|
3805
|
-
const isBlockedX = tileCollisionTest(vec2(this.pos.x, oldPos.y), this.size, this);
|
|
3806
|
-
const isBlockedY = tileCollisionTest(vec2(oldPos.x, this.pos.y), this.size, this);
|
|
3807
|
-
const restitution = max(this.restitution, hitLayer.restitution);
|
|
3808
|
-
if (isBlockedX)
|
|
3809
|
-
{
|
|
3810
|
-
// a ledge caught less than maxMove below its top lifts the object onto it instead of stopping it,
|
|
3811
|
-
// down off a ceiling ledge when gravity points up; zero gravity counts as down
|
|
3812
|
-
const epsilon = 1e-3;
|
|
3813
|
-
const maxMove = .1;
|
|
3814
|
-
const y = gravityY > 0 ?
|
|
3815
|
-
ceil( oldPos.y+this.size.y/2-1) - this.size.y/2 - epsilon :
|
|
3816
|
-
floor(oldPos.y-this.size.y/2+1) + this.size.y/2 + epsilon;
|
|
3817
|
-
if (abs(y - this.pos.y) < maxMove && !tileCollisionTest(vec2(this.pos.x, y), this.size, this))
|
|
3818
|
-
{
|
|
3819
|
-
this.pos.y = y;
|
|
3820
|
-
debugPhysics && debugRect(this.pos, this.size, '#ff0', 0, 0, false, false);
|
|
3821
|
-
return;
|
|
3822
|
-
}
|
|
3823
|
-
|
|
3824
|
-
// move to previous X position and bounce
|
|
3825
|
-
this.pos.x = oldPos.x;
|
|
3826
|
-
this.velocity.x *= -restitution;
|
|
3827
|
-
}
|
|
3828
|
-
if (isBlockedY || !isBlockedX)
|
|
3829
|
-
{
|
|
3830
|
-
if (wasFalling)
|
|
3831
|
-
{
|
|
3832
|
-
// adjust position to slightly away from nearest tile
|
|
3833
|
-
// this prevents gap between object and ground
|
|
3834
|
-
const epsilon = .0001;
|
|
3835
|
-
const offset = this.size.y/2 + epsilon;
|
|
3836
|
-
// rounded in the layer's space as its collision test is, or a bottom a hair below a
|
|
3837
|
-
// grid line would round to the row under it, inside the floor, and fall through
|
|
3838
|
-
const layerY = hitLayer.pos.y;
|
|
3839
|
-
this.pos.y = layerY + (gravityY < 0 ?
|
|
3840
|
-
floor(oldPos.y - layerY - this.size.y/2) + offset :
|
|
3841
|
-
ceil( oldPos.y - layerY + this.size.y/2) - offset);
|
|
3842
|
-
|
|
3843
|
-
// set ground object for tile collision
|
|
3844
|
-
this.groundObject = hitLayer;
|
|
3845
|
-
}
|
|
3846
|
-
else
|
|
3847
|
-
{
|
|
3848
|
-
// move to previous Y position
|
|
3849
|
-
this.pos.y = oldPos.y;
|
|
3850
|
-
this.groundObject = undefined;
|
|
3851
|
-
}
|
|
3852
|
-
// bounce velocity
|
|
3853
|
-
this.velocity.y *= -restitution;
|
|
3854
|
-
}
|
|
3855
|
-
debugPhysics && debugRect(this.pos, this.size, '#f00', 0, 0, false, false);
|
|
3856
|
-
}
|
|
3857
|
-
}
|
|
3858
|
-
}
|
|
3859
|
-
}
|
|
3860
|
-
|
|
3861
|
-
/** Update the object, called automatically by engine once each frame. Does nothing by default. */
|
|
3862
|
-
update() {}
|
|
3863
|
-
|
|
3864
|
-
/** Render the object, draws a tile by default, automatically called each frame, sorted by renderOrder */
|
|
3865
|
-
render()
|
|
3866
|
-
{
|
|
3867
|
-
// default object render
|
|
3868
|
-
drawTile(this.pos, this.drawSize || this.size, this.tileInfo, this.color, this.angle, this.mirror, this.additiveColor, glEnable, false);
|
|
3869
|
-
}
|
|
3870
|
-
|
|
3871
|
-
/** Optional hook called during the light system plugin's lightmap pass to draw this object's lightmap contribution. Does nothing by default. */
|
|
3872
|
-
renderLight() {}
|
|
3873
|
-
|
|
3874
|
-
/** Draw this object into the light system's shadow map, called during its shadow pass when castShadow is set.
|
|
3875
|
-
* Calls render() by default so the object casts its own shape; override to cast a different one, like a blob at a character's feet so its body stays lit;
|
|
3876
|
-
* screen space WebGL draws in render() are skipped during the pass */
|
|
3877
|
-
renderShadow() { this.render(); }
|
|
3878
|
-
|
|
3879
|
-
/** Draw this object's shape into the light system's lightmap, called during its light pass when emissive is above
|
|
3880
|
-
* 0; what it draws shows in its own colors that much brighter. Calls render() by default so the whole object
|
|
3881
|
-
* glows; override to glow a part, like a robot's eyes */
|
|
3882
|
-
renderEmissive() { this.render(); }
|
|
3883
|
-
|
|
3884
|
-
/** Destroy this object, destroy its children, detach its parent, and mark it for removal
|
|
3885
|
-
* @param {boolean} [immediate] - true removes attached effects like particle emitters at once, false lets them finish first */
|
|
3886
|
-
destroy(immediate=false)
|
|
3887
|
-
{
|
|
3888
|
-
if (this.destroyed) return;
|
|
3889
|
-
|
|
3890
|
-
// disconnect from parent and destroy children
|
|
3891
|
-
this.destroyed = true;
|
|
3892
|
-
this.parent?.removeChild(this);
|
|
3893
|
-
for (const child of this.children)
|
|
3894
|
-
{
|
|
3895
|
-
child.parent = undefined;
|
|
3896
|
-
child.destroy(immediate);
|
|
3897
|
-
}
|
|
3898
|
-
}
|
|
3899
|
-
|
|
3900
|
-
/** Convert from local space to world space
|
|
3901
|
-
* @param {Vector2} pos - local space point */
|
|
3902
|
-
localToWorld(pos) { return this.pos.add(pos.rotate(this.angle)); }
|
|
3903
|
-
|
|
3904
|
-
/** Convert from world space to local space
|
|
3905
|
-
* @param {Vector2} pos - world space point */
|
|
3906
|
-
worldToLocal(pos) { return pos.subtract(this.pos).rotate(-this.angle); }
|
|
3907
|
-
|
|
3908
|
-
/** Convert from local space to world space for a vector (rotation only)
|
|
3909
|
-
* @param {Vector2} vec - local space vector */
|
|
3910
|
-
localToWorldVector(vec) { return vec.rotate(this.angle); }
|
|
3911
|
-
|
|
3912
|
-
/** Convert from world space to local space for a vector (rotation only)
|
|
3913
|
-
* @param {Vector2} vec - world space vector */
|
|
3914
|
-
worldToLocalVector(vec) { return vec.rotate(-this.angle); }
|
|
3915
|
-
|
|
3916
|
-
/** Called to check if a tile collision should be resolved. Return true for physics to resolve the collision or false to ignore and resolve it manually.
|
|
3917
|
-
* - Called for each solid tile the physics tests, which can be several times a frame for the same tile, and for
|
|
3918
|
-
* positions it only tries, so keep it free of side effects or guard them to once a frame
|
|
3919
|
-
* - this.pos has already moved, so a check on where it came from, like a one way platform, needs the position
|
|
3920
|
-
* saved in update, as the platformer example does
|
|
3921
|
-
* @param {number} tileData - the value of the tile at the position
|
|
3922
|
-
* @param {Vector2} pos - the tile's bottom left corner in world space
|
|
3923
|
-
* @return {boolean} - true if the collision should be resolved by modifying it's position and velocity */
|
|
3924
|
-
collideWithTile(tileData, pos) { return tileData > 0; }
|
|
3925
|
-
|
|
3926
|
-
/** Called by the engine to check if an object collision should be resolved. Return true for physics to resolve the collision or false to ignore and resolve it manually.
|
|
3927
|
-
* - Both objects of a touching pair are asked once a frame, whichever order they update in; an object that
|
|
3928
|
-
* destroys itself here is gone at the end of the frame and is still asked about the pairs left this frame, so a
|
|
3929
|
-
* bullet that should hit one thing checks its own destroyed flag first
|
|
3930
|
-
* @param {EngineObject} object - the object to test against
|
|
3931
|
-
* @param {Vector3} [push] - what it would take to move this object clear, a Vector3 from the 3D plugin, undefined in 2D
|
|
3932
|
-
* @return {boolean} - true if the collision should be resolved by modifying it's position and velocity
|
|
3933
|
-
*/
|
|
3934
|
-
collideWithObject(object, push) { return true; }
|
|
3935
|
-
|
|
3936
|
-
/** Get this object's up vector
|
|
3937
|
-
* @param {number} [scale] - length of the vector
|
|
3938
|
-
* @return {Vector2} */
|
|
3939
|
-
getUp(scale=1) { return vec2().setAngle(this.angle, scale); }
|
|
3940
|
-
|
|
3941
|
-
/** Get this object's right vector
|
|
3942
|
-
* @param {number} [scale] - length of the vector
|
|
3943
|
-
* @return {Vector2} */
|
|
3944
|
-
getRight(scale=1) { return vec2().setAngle(this.angle+PI/2, scale); }
|
|
3945
|
-
|
|
3946
|
-
/** How long since the object was created
|
|
3947
|
-
* @return {number} */
|
|
3948
|
-
getAliveTime() { return time - this.spawnTime; }
|
|
3350
|
+
if (count > 0)
|
|
3351
|
+
touchGamepadLeftStick = false;
|
|
3352
|
+
}
|
|
3949
3353
|
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3354
|
+
/** Set if the touch gamepad right side is an analog stick (or dpad) instead of face buttons
|
|
3355
|
+
* @param {boolean} rightStick
|
|
3356
|
+
* @memberof Settings */
|
|
3357
|
+
function setTouchGamepadRightStick(rightStick)
|
|
3358
|
+
{
|
|
3359
|
+
touchGamepadRightStick = rightStick;
|
|
3360
|
+
if (rightStick)
|
|
3361
|
+
touchGamepadButtonCount = 0;
|
|
3362
|
+
}
|
|
3953
3363
|
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
{ if (this.mass) this.velocity = this.velocity.add(acceleration); }
|
|
3364
|
+
/** Set if touch gamepad should be analog stick or 8 way dpad
|
|
3365
|
+
* @param {boolean} analog
|
|
3366
|
+
* @memberof Settings */
|
|
3367
|
+
function setTouchGamepadAnalog(analog) { touchGamepadAnalog = analog; }
|
|
3959
3368
|
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
{ if (this.mass) this.angleVelocity += acceleration; }
|
|
3369
|
+
/** Set if touch gamepad directional controls should float to where you press
|
|
3370
|
+
* @param {boolean} floating
|
|
3371
|
+
* @memberof Settings */
|
|
3372
|
+
function setTouchGamepadFloating(floating) { touchGamepadFloating = floating; }
|
|
3965
3373
|
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
{ if (this.mass) this.applyAcceleration(force.scale(1/this.mass)); }
|
|
3374
|
+
/** Set size of virtual gamepad for touch devices in viewport CSS pixels
|
|
3375
|
+
* @param {number} size
|
|
3376
|
+
* @memberof Settings */
|
|
3377
|
+
function setTouchGamepadSize(size) { touchGamepadSize = size; }
|
|
3971
3378
|
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3379
|
+
/** Set transparency of touch gamepad overlay
|
|
3380
|
+
* @param {number} alpha
|
|
3381
|
+
* @memberof Settings */
|
|
3382
|
+
function setTouchGamepadAlpha(alpha) { touchGamepadAlpha = alpha; }
|
|
3975
3383
|
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
* @return {EngineObject} The child object added */
|
|
3981
|
-
addChild(child, localPos=vec2(), localAngle=0)
|
|
3982
|
-
{
|
|
3983
|
-
false&&ASSERT(!this.destroyed, 'cannot add child to destroyed object');
|
|
3984
|
-
if (this.destroyed) return child;
|
|
3985
|
-
false&&ASSERT(!child.parent && !this.children.includes(child), 'child already has a parent, removeChild it first or use attach');
|
|
3986
|
-
false&&ASSERT(child instanceof EngineObject, 'child must be an EngineObject');
|
|
3987
|
-
false&&ASSERT(!child.destroyed, 'cannot add a destroyed child');
|
|
3988
|
-
for (let p = /** @type {EngineObject} */ (this); p; p = p.parent)
|
|
3989
|
-
false&&ASSERT(p !== child, 'cannot add an object as a child of itself or of its own child');
|
|
3990
|
-
this.children.push(child);
|
|
3991
|
-
child.parent = this;
|
|
3992
|
-
child.localPos = localPos.copy();
|
|
3993
|
-
child.localAngle = localAngle;
|
|
3994
|
-
child.updateTransforms();
|
|
3995
|
-
return child;
|
|
3996
|
-
}
|
|
3384
|
+
/** Set how long to display the touch gamepad on screen in seconds, set to 0 to always display
|
|
3385
|
+
* @param {number} time
|
|
3386
|
+
* @memberof Settings */
|
|
3387
|
+
function setTouchGamepadDisplayTime(time) { touchGamepadDisplayTime = time; }
|
|
3997
3388
|
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
attach(child)
|
|
4003
|
-
{
|
|
4004
|
-
false&&ASSERT(child instanceof EngineObject, 'child must be an EngineObject');
|
|
4005
|
-
false&&ASSERT(child !== this, 'cannot attach self');
|
|
4006
|
-
child.parent?.removeChild(child);
|
|
4007
|
-
// the local values that updateTransforms turns back into the child's current pos and angle
|
|
4008
|
-
const mirror = this.getMirrorSign(), local = this.worldToLocal(child.pos);
|
|
4009
|
-
local.x *= mirror;
|
|
4010
|
-
return this.addChild(child, local, mirror * (child.angle - this.angle));
|
|
4011
|
-
}
|
|
3389
|
+
/** Set duration in ms to vibrate when a touch gamepad face or start button is pressed (0 disables)
|
|
3390
|
+
* @param {number} ms
|
|
3391
|
+
* @memberof Settings */
|
|
3392
|
+
function setTouchGamepadVibration(ms) { touchGamepadVibration = ms; }
|
|
4012
3393
|
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
false&&ASSERT(child.parent === this && this.children.includes(child));
|
|
4018
|
-
const i = this.children.indexOf(child);
|
|
4019
|
-
if (i < 0) return; // not a child of this one, release has no assert
|
|
4020
|
-
this.children.splice(i, 1);
|
|
4021
|
-
child.parent = child.localPos = undefined;
|
|
4022
|
-
}
|
|
3394
|
+
/** Set to allow vibration hardware if it exists
|
|
3395
|
+
* @param {boolean} enable
|
|
3396
|
+
* @memberof Settings */
|
|
3397
|
+
function setVibrateEnable(enable) { vibrateEnable = enable; }
|
|
4023
3398
|
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
{ return this.isOverlapping(object.pos, object.size); }
|
|
3399
|
+
/** Set if audio is enabled, false turns all sound off
|
|
3400
|
+
* @param {boolean} enable
|
|
3401
|
+
* @memberof Settings */
|
|
3402
|
+
function setSoundEnable(enable) { soundEnable = enable; }
|
|
4029
3403
|
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
3404
|
+
/** Set volume scale to apply to all sound, music and speech
|
|
3405
|
+
* @param {number} volume
|
|
3406
|
+
* @memberof Settings */
|
|
3407
|
+
function setSoundVolume(volume)
|
|
3408
|
+
{
|
|
3409
|
+
soundVolume = volume;
|
|
3410
|
+
audioUpdateVolume(); // update gain immediately, sound off or not
|
|
3411
|
+
}
|
|
4036
3412
|
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
* @param {boolean} [collideRaycast] - Does it collide with raycasts? */
|
|
4042
|
-
setCollision(collideSolidObjects=true, isSolid=true, collideLevel=true, collideRaycast=true)
|
|
4043
|
-
{
|
|
4044
|
-
false&&ASSERT(collideSolidObjects || !isSolid, 'solid objects must be set to collide');
|
|
3413
|
+
/** Set default range where sound no longer plays
|
|
3414
|
+
* @param {number} range
|
|
3415
|
+
* @memberof Settings */
|
|
3416
|
+
function setSoundDefaultRange(range) { soundDefaultRange = range; }
|
|
4045
3417
|
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
}
|
|
3418
|
+
/** Set default range percent to start tapering off sound
|
|
3419
|
+
* @param {number} taper
|
|
3420
|
+
* @memberof Settings */
|
|
3421
|
+
function setSoundDefaultTaper(taper) { soundDefaultTaper = taper; }
|
|
4051
3422
|
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
3423
|
+
/** Set if all sound pauses while the page is hidden
|
|
3424
|
+
* @param {boolean} pause
|
|
3425
|
+
* @memberof Settings */
|
|
3426
|
+
function setSoundPauseWhenHidden(pause) { soundPauseWhenHidden = pause; }
|
|
4056
3427
|
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
let text = 'type = ' + this.constructor.name;
|
|
4062
|
-
if (this.pos.x || this.pos.y)
|
|
4063
|
-
text += '\npos = ' + this.pos;
|
|
4064
|
-
if (this.velocity.x || this.velocity.y)
|
|
4065
|
-
text += '\nvelocity = ' + this.velocity;
|
|
4066
|
-
if (this.size.x || this.size.y)
|
|
4067
|
-
text += '\nsize = ' + this.size;
|
|
4068
|
-
if (this.angle)
|
|
4069
|
-
text += '\nangle = ' + this.angle.toFixed(3);
|
|
4070
|
-
text += '\ncolor = ' + this.color;
|
|
4071
|
-
return text;
|
|
4072
|
-
}
|
|
3428
|
+
/** Set if watermark with FPS should be shown
|
|
3429
|
+
* @param {boolean} show
|
|
3430
|
+
* @memberof Debug */
|
|
3431
|
+
function setDebugWatermark(show) { debugWatermark = show; }
|
|
4073
3432
|
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
3433
|
+
/** Set key code used to toggle debug mode, Esc by default
|
|
3434
|
+
* @param {string} key
|
|
3435
|
+
* @memberof Debug */
|
|
3436
|
+
function setDebugKey(key) { debugKey = key; }
|
|
4078
3437
|
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
3438
|
+
/** Set if the debug keys work while the overlay is closed, the number keys and the +/- time keys
|
|
3439
|
+
* @param {boolean} [enable]
|
|
3440
|
+
* @memberof Debug */
|
|
3441
|
+
function setDebugKeysAlways(enable=true) { debugKeysAlways = enable; }
|
|
4082
3442
|
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
3443
|
+
/** Set if the tweakables panel is shown, as 9 does while the debug overlay is open
|
|
3444
|
+
* @param {boolean} [show]
|
|
3445
|
+
* @memberof Debug */
|
|
3446
|
+
function setDebugTweakables(show=true) { debugTweakables = show; }
|
|
3447
|
+
/**
|
|
3448
|
+
* LittleJS Object System
|
|
3449
|
+
* - EngineObject is the base class for all game objects
|
|
3450
|
+
* - Handles automatic updating, rendering, physics, and collision
|
|
3451
|
+
* - Supports parent-child hierarchies with transform inheritance
|
|
3452
|
+
* - 2D physics with velocity, acceleration, damping, and gravity
|
|
3453
|
+
* - Collision system with tiles and other objects
|
|
3454
|
+
* - Renders sprites from tile sheets with color and rotation
|
|
3455
|
+
* - Objects sorted by renderOrder for layered rendering
|
|
3456
|
+
*/
|
|
3457
|
+
|
|
3458
|
+
/**
|
|
3459
|
+
* LittleJS Object Base Object Class
|
|
3460
|
+
* - Top level object class used by the engine
|
|
3461
|
+
* - Automatically adds self to object list
|
|
3462
|
+
* - Will be updated and rendered each frame
|
|
3463
|
+
* - Renders as a sprite from a tilesheet by default
|
|
3464
|
+
* - Can have color and additive color applied
|
|
3465
|
+
* - 2D Physics and collision system
|
|
3466
|
+
* - Sorted by renderOrder
|
|
3467
|
+
* - Objects can have children attached
|
|
3468
|
+
* - Parents are updated before children, and set child transform
|
|
3469
|
+
* - Call destroy() to get rid of objects
|
|
3470
|
+
*
|
|
3471
|
+
* The physics system used by objects is simple and fast with some caveats...
|
|
3472
|
+
* - Collision uses the axis aligned size, the object's rotation angle is only for rendering
|
|
3473
|
+
* - Objects are guaranteed to not intersect tile collision from physics
|
|
3474
|
+
* - If an object starts or is moved inside tile collision, it will not collide with that tile
|
|
3475
|
+
* - Collision for objects can be set to be solid to block other objects
|
|
3476
|
+
* - Objects may get pushed into overlapping other solid objects, if so they will push away
|
|
3477
|
+
* - A static solid (mass 0) moved by its velocity, like a door or an elevator, pushes objects out of its way
|
|
3478
|
+
* - Solid objects are more performance intensive and should be used sparingly
|
|
3479
|
+
* @memberof Engine
|
|
3480
|
+
* @example
|
|
3481
|
+
* // create an engine object, normally you would first extend the class with your own
|
|
3482
|
+
* const pos = vec2(2,3);
|
|
3483
|
+
* const object = new EngineObject(pos);
|
|
3484
|
+
*/
|
|
3485
|
+
class EngineObject
|
|
3486
|
+
{
|
|
3487
|
+
/** Create an engine object and adds it to the list of objects
|
|
3488
|
+
* @param {Vector2} [pos=vec2()] - World space position of the object
|
|
3489
|
+
* @param {Vector2} [size=vec2(1)] - World space size of the object
|
|
3490
|
+
* @param {TileInfo} [tileInfo] - Tile info to render object (undefined is untextured)
|
|
3491
|
+
* @param {number} [angle] - Angle the object is rotated by
|
|
3492
|
+
* @param {Color} [color=WHITE] - Color to apply to tile when rendered
|
|
3493
|
+
* @param {number} [renderOrder] - Objects sorted by renderOrder before being rendered
|
|
3494
|
+
*/
|
|
3495
|
+
constructor(pos=vec2(), size=vec2(1), tileInfo, angle=0, color=WHITE, renderOrder=0)
|
|
3496
|
+
{
|
|
3497
|
+
// check passed in params
|
|
3498
|
+
false&&ASSERT(isVector2(pos), 'object pos must be a vec2');
|
|
3499
|
+
false&&ASSERT(isVector2(size), 'object size must be a vec2');
|
|
3500
|
+
false&&ASSERT(!tileInfo || tileInfo instanceof TileInfo, 'object tileInfo should be a TileInfo or undefined');
|
|
3501
|
+
false&&ASSERT(typeof angle === 'number' && isFinite(angle), 'object angle should be a number');
|
|
3502
|
+
false&&ASSERT(isColor(color), 'object color should be a valid rgba color');
|
|
3503
|
+
false&&ASSERT(typeof renderOrder === 'number', 'object renderOrder should be a number');
|
|
3504
|
+
|
|
3505
|
+
/** @property {Vector2} - World space position of the object */
|
|
3506
|
+
this.pos = pos.copy();
|
|
3507
|
+
/** @property {Vector2} - World space width and height of the object */
|
|
3508
|
+
this.size = size.copy();
|
|
3509
|
+
/** @property {Vector2|undefined} - Size of object used for drawing, uses size if not set
|
|
3510
|
+
* @type {Vector2|undefined} */
|
|
3511
|
+
this.drawSize = undefined;
|
|
3512
|
+
/** @property {TileInfo|undefined} - Tile info to render object (undefined is untextured)
|
|
3513
|
+
* @type {TileInfo|undefined} */
|
|
3514
|
+
this.tileInfo = tileInfo;
|
|
3515
|
+
/** @property {number} - Angle to rotate the object */
|
|
3516
|
+
this.angle = angle;
|
|
3517
|
+
/** @property {Color} - Color to apply when rendered */
|
|
3518
|
+
this.color = color.copy();
|
|
3519
|
+
/** @property {Color|undefined} - Additive color to apply when rendered
|
|
3520
|
+
* @type {Color|undefined} */
|
|
3521
|
+
this.additiveColor = undefined;
|
|
3522
|
+
/** @property {Shader|undefined} - Custom shader to render with, undefined for the engine's own
|
|
3523
|
+
* @type {Shader|undefined} */
|
|
3524
|
+
this.shader = undefined;
|
|
3525
|
+
/** @property {boolean} - Does this object draw into the light system's shadow map; false for a floor layer, a background, a pickup */
|
|
3526
|
+
this.castShadow = true;
|
|
3527
|
+
/** @property {number} - With the light system, how much it lights itself: 0 lit only by the lights, 1 full
|
|
3528
|
+
* brightness in its own colors whatever the lights do, between partly; drawn into the lightmap through
|
|
3529
|
+
* renderEmissive, as 3D's emissive. Exact for solid pixels; a partly transparent one is self lit by its alpha
|
|
3530
|
+
* too, so a half alpha pixel shows at a quarter and a fading emissive sprite fades a little faster */
|
|
3531
|
+
this.emissive = 0;
|
|
3532
|
+
/** @property {boolean} - Should the rendered tile flip along the y axis. Affects rendering and the local→world transform of attached children (a mirrored parent flips its children's localPos.x and localAngle). Does not affect this object's own physics, collision, or localToWorld/worldToLocal. */
|
|
3533
|
+
this.mirror = false;
|
|
3534
|
+
/** @property {boolean} - Has object been destroyed? */
|
|
3535
|
+
this.destroyed = false;
|
|
3536
|
+
this.updatePass = 0; // the engine update pass it was last updated in, so nothing updates twice in one
|
|
3537
|
+
|
|
3538
|
+
// physical properties
|
|
3539
|
+
/** @property {number} - How heavy the object is, static if 0: a static object moves by its velocity but does not
|
|
3540
|
+
* collide on its own, the moving ones collide with it */
|
|
3541
|
+
this.mass = objectDefaultMass;
|
|
3542
|
+
/** @property {number} - Fraction of velocity kept each frame, 1 keeps all of it, 0 stops at once */
|
|
3543
|
+
this.damping = objectDefaultDamping;
|
|
3544
|
+
/** @property {number} - Fraction of angular velocity kept each frame, 1 keeps all of it, 0 stops at once */
|
|
3545
|
+
this.angleDamping = objectDefaultAngleDamping;
|
|
3546
|
+
/** @property {number} - How bouncy the object is when colliding (0-1) */
|
|
3547
|
+
this.restitution = objectDefaultRestitution;
|
|
3548
|
+
/** @property {number} - Fraction of sliding speed kept each frame on the ground, 1 is no friction, 0 stops at
|
|
3549
|
+
* once, the more slippery of the object and its ground is used */
|
|
3550
|
+
this.friction = objectDefaultFriction;
|
|
3551
|
+
/** @property {number} - How much to scale gravity by for this object */
|
|
3552
|
+
this.gravityScale = 1;
|
|
3553
|
+
/** @property {number} - Objects are sorted by render order */
|
|
3554
|
+
this.renderOrder = renderOrder;
|
|
3555
|
+
/** @property {Vector2} - Velocity of the object, in world units per frame */
|
|
3556
|
+
this.velocity = vec2();
|
|
3557
|
+
/** @property {number} - Angular velocity of the object, in radians per frame */
|
|
3558
|
+
this.angleVelocity = 0;
|
|
3559
|
+
/** @property {number} - Track when object was created */
|
|
3560
|
+
this.spawnTime = time;
|
|
3561
|
+
/** @property {Array<EngineObject>} - List of children of this object
|
|
3562
|
+
* @type {Array<EngineObject>} */
|
|
3563
|
+
this.children = [];
|
|
3564
|
+
/** @property {boolean} - Limit object speed along x and y axis */
|
|
3565
|
+
this.clampSpeed = true;
|
|
3566
|
+
/** @property {EngineObject|undefined} - Object we are standing on, if any
|
|
3567
|
+
* @type {EngineObject|undefined} */
|
|
3568
|
+
this.groundObject = undefined;
|
|
3569
|
+
|
|
3570
|
+
// parent child system
|
|
3571
|
+
/** @property {EngineObject|undefined} - Parent of object if in local space
|
|
3572
|
+
* @type {EngineObject|undefined} */
|
|
3573
|
+
this.parent = undefined;
|
|
3574
|
+
/** @property {Vector2|undefined} - Position relative to the parent, only while attached to one
|
|
3575
|
+
* @type {Vector2|undefined} */
|
|
3576
|
+
this.localPos = undefined;
|
|
3577
|
+
/** @property {number} - Local angle if child */
|
|
3578
|
+
this.localAngle = 0;
|
|
3579
|
+
|
|
3580
|
+
// collision flags
|
|
3581
|
+
/** @property {boolean} - Object collides with the level, its tile collision layers
|
|
3582
|
+
* @type {boolean} */
|
|
3583
|
+
this.collideLevel = false;
|
|
3584
|
+
/** @property {boolean} - Object collides with solid objects */
|
|
3585
|
+
this.collideSolidObjects = false;
|
|
3586
|
+
/** @property {boolean} - Object collides with and blocks other objects */
|
|
3587
|
+
this.isSolid = false;
|
|
3588
|
+
/** @property {boolean} - Object collides with raycasts */
|
|
3589
|
+
this.collideRaycast = false;
|
|
3590
|
+
|
|
3591
|
+
/** @property {boolean} - Object is skipped by engineObjectsDestroy, for things that outlive a level like a camera
|
|
3592
|
+
* - Calling destroy on it still destroys it, and its children go with it either way */
|
|
3593
|
+
this.persistent = false;
|
|
3594
|
+
|
|
3595
|
+
// add to list of objects
|
|
3596
|
+
engineObjects.push(this);
|
|
3597
|
+
}
|
|
3598
|
+
|
|
3599
|
+
/** Update the object transform, called automatically by engine even when paused
|
|
3600
|
+
* @param {boolean} [updateChildren] - Also update the children's transforms */
|
|
3601
|
+
updateTransforms(updateChildren=true)
|
|
3602
|
+
{
|
|
3603
|
+
const parent = this.parent;
|
|
3604
|
+
if (parent)
|
|
3605
|
+
{
|
|
3606
|
+
// compose with parent transform inline to avoid intermediate vector allocs
|
|
3607
|
+
const mirror = parent.getMirrorSign();
|
|
3608
|
+
const lp = this.localPos, pp = parent.pos;
|
|
3609
|
+
const lx = lp.x*mirror, ly = lp.y, pa = parent.angle;
|
|
3610
|
+
if (pa)
|
|
3611
|
+
{
|
|
3612
|
+
const c = cos(-pa), s = sin(-pa);
|
|
3613
|
+
this.pos.set(lx*c - ly*s + pp.x, lx*s + ly*c + pp.y);
|
|
3614
|
+
}
|
|
3615
|
+
else
|
|
3616
|
+
this.pos.set(lx + pp.x, ly + pp.y);
|
|
3617
|
+
this.angle = mirror*this.localAngle + pa;
|
|
3618
|
+
}
|
|
3619
|
+
|
|
3620
|
+
// update children
|
|
3621
|
+
if (updateChildren)
|
|
3622
|
+
for (const child of this.children)
|
|
3623
|
+
child.updateTransforms();
|
|
3624
|
+
}
|
|
3625
|
+
|
|
3626
|
+
/** Update the object physics, called automatically by engine once each frame. Can be overridden to stop or change how physics works for an object. */
|
|
3627
|
+
updatePhysics()
|
|
3628
|
+
{
|
|
3629
|
+
// child objects do not have physics
|
|
3630
|
+
false&&ASSERT(!this.parent);
|
|
3631
|
+
|
|
3632
|
+
// bail if a collision callback destroyed us mid-frame
|
|
3633
|
+
if (this.destroyed) return;
|
|
3634
|
+
|
|
3635
|
+
// limit max speed to prevent missing collisions, only for what collides: with solids, or with tiles while it
|
|
3636
|
+
// has a mass, which tile collision needs; anything else moves as fast as it is told
|
|
3637
|
+
if (this.clampSpeed && enablePhysicsSolver && (this.collideSolidObjects || this.collideLevel && this.mass))
|
|
3638
|
+
{
|
|
3639
|
+
this.velocity.x = clamp(this.velocity.x, -objectMaxSpeed, objectMaxSpeed);
|
|
3640
|
+
this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
|
|
3641
|
+
}
|
|
3642
|
+
|
|
3643
|
+
// physics sanity checks
|
|
3644
|
+
false&&ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1, 'angleDamping must be 0 to 1, the fraction kept each frame');
|
|
3645
|
+
false&&ASSERT(this.damping >= 0 && this.damping <= 1, 'damping must be 0 to 1, the fraction of velocity kept each frame');
|
|
3646
|
+
|
|
3647
|
+
// apply physics; only the solver needs where the object was, so only then is it copied
|
|
3648
|
+
const solve = enablePhysicsSolver && this.mass;
|
|
3649
|
+
const oldPos = solve ? this.pos.copy() : undefined;
|
|
3650
|
+
this.velocity.x *= this.damping;
|
|
3651
|
+
this.velocity.y *= this.damping;
|
|
3652
|
+
if (this.mass)
|
|
3653
|
+
{
|
|
3654
|
+
// apply gravity only if it has mass
|
|
3655
|
+
this.velocity.x += gravity.x * this.gravityScale;
|
|
3656
|
+
this.velocity.y += gravity.y * this.gravityScale;
|
|
3657
|
+
}
|
|
3658
|
+
this.pos.x += this.velocity.x;
|
|
3659
|
+
this.pos.y += this.velocity.y;
|
|
3660
|
+
this.angle += this.angleVelocity *= this.angleDamping;
|
|
3661
|
+
|
|
3662
|
+
// don't do collision for static objects or if solver disabled
|
|
3663
|
+
if (!solve) return;
|
|
3664
|
+
|
|
3665
|
+
// which way is down for this object, a negative gravityScale falls up and lands on ceilings
|
|
3666
|
+
const gravityY = this.gravityScale < 0 ? -gravity.y : gravity.y;
|
|
3667
|
+
const wasFalling = this.velocity.y < 0 && gravityY < 0 || this.velocity.y > 0 && gravityY > 0;
|
|
3668
|
+
if (this.groundObject)
|
|
3669
|
+
{
|
|
3670
|
+
// apply friction in local space of ground object
|
|
3671
|
+
const friction = max(this.friction, this.groundObject.friction);
|
|
3672
|
+
const groundSpeed = this.groundObject.velocity.x;
|
|
3673
|
+
this.velocity.x = groundSpeed + (this.velocity.x - groundSpeed) * friction;
|
|
3674
|
+
this.groundObject = undefined;
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3677
|
+
// an object with no width or height has no box to push out of, or to be pushed out of
|
|
3678
|
+
if (this.collideSolidObjects && this.size.x && this.size.y)
|
|
3679
|
+
{
|
|
3680
|
+
// check collisions against solid objects
|
|
3681
|
+
const epsilon = .001; // necessary to push slightly outside of the collision
|
|
3682
|
+
for (const o of engineObjectsCollideStaticLast)
|
|
3683
|
+
{
|
|
3684
|
+
// skip destroyed, child objects, self collision, or objects with no box
|
|
3685
|
+
if (o.destroyed || o.parent || o === this || !o.size.x || !o.size.y) continue;
|
|
3686
|
+
|
|
3687
|
+
// non solid objects don't collide with each other
|
|
3688
|
+
if (!this.isSolid && !o.isSolid) continue;
|
|
3689
|
+
|
|
3690
|
+
// check collision
|
|
3691
|
+
if (!this.isOverlappingObject(o)) continue;
|
|
3692
|
+
|
|
3693
|
+
// each moving object checks its own contacts, so a pair the other one already asked about this frame
|
|
3694
|
+
// is not asked twice: left overlapping, ignored or only nudged apart it is skipped, and one both said
|
|
3695
|
+
// to resolve, pushed back together since, is resolved again
|
|
3696
|
+
const answer = engineObjectsCollidePairAnswer(o, this);
|
|
3697
|
+
if (answer === false) continue;
|
|
3698
|
+
if (!answer)
|
|
3699
|
+
{
|
|
3700
|
+
// notify objects of collision and check if should be resolved
|
|
3701
|
+
const collide1 = this.collideWithObject(o);
|
|
3702
|
+
const collide2 = o.collideWithObject(this);
|
|
3703
|
+
if (!collide1 || !collide2)
|
|
3704
|
+
{
|
|
3705
|
+
engineObjectsCollidePairAdd(this, o);
|
|
3706
|
+
continue;
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
|
|
3710
|
+
const wasOverlapping = isOverlapping(oldPos, this.size, o.pos, o.size);
|
|
3711
|
+
if (wasOverlapping && (!o.mass || o.groundObject))
|
|
3712
|
+
{
|
|
3713
|
+
// a static solid that moved into it, like a door or an elevator, pushes it out the shortest way
|
|
3714
|
+
// at once and carries it along, it would only drift out slowly and the solid would pass through;
|
|
3715
|
+
// an object standing on something counts as fixed too, so a stack on an elevator rides together;
|
|
3716
|
+
// it bounces off relative to the mover, a paddle moved by setting pos bounces a ball as a wall does
|
|
3717
|
+
const push = collideBoxBox(this.pos, this.size, o.pos, o.size);
|
|
3718
|
+
if (push)
|
|
3719
|
+
{
|
|
3720
|
+
this.pos.x += push.x + sign(push.x) * epsilon;
|
|
3721
|
+
this.pos.y += push.y + sign(push.y) * epsilon;
|
|
3722
|
+
const restitution = max(this.restitution, o.restitution);
|
|
3723
|
+
if (push.x)
|
|
3724
|
+
{
|
|
3725
|
+
const v = this.velocity.x - o.velocity.x;
|
|
3726
|
+
if (v * push.x < 0) // moving into it
|
|
3727
|
+
this.velocity.x = o.velocity.x - v * restitution;
|
|
3728
|
+
}
|
|
3729
|
+
else
|
|
3730
|
+
{
|
|
3731
|
+
const v = this.velocity.y - o.velocity.y;
|
|
3732
|
+
if (v * push.y < 0) // moving into it
|
|
3733
|
+
this.velocity.y = o.velocity.y - v * restitution;
|
|
3734
|
+
if (push.y * gravityY < 0) // pushed up against gravity, it stands on it
|
|
3735
|
+
this.groundObject = o;
|
|
3736
|
+
}
|
|
3737
|
+
}
|
|
3738
|
+
engineObjectsCollidePairAdd(this, o);
|
|
3739
|
+
|
|
3740
|
+
debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f00', undefined, false);
|
|
3741
|
+
continue;
|
|
3742
|
+
}
|
|
3743
|
+
if (wasOverlapping)
|
|
3744
|
+
{
|
|
3745
|
+
// if already was touching, try to push away
|
|
3746
|
+
const deltaPos = oldPos.subtract(o.pos);
|
|
3747
|
+
const length = deltaPos.length();
|
|
3748
|
+
const pushAwayAccel = .001;
|
|
3749
|
+
const velocity = length < .001 ? vec2(0, pushAwayAccel) : deltaPos.scale(pushAwayAccel/length);
|
|
3750
|
+
this.velocity = this.velocity.add(velocity);
|
|
3751
|
+
if (o.mass) // push away other object if not fixed
|
|
3752
|
+
o.velocity = o.velocity.subtract(velocity);
|
|
3753
|
+
engineObjectsCollidePairAdd(this, o);
|
|
3754
|
+
|
|
3755
|
+
debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f00', undefined, false);
|
|
3756
|
+
continue;
|
|
3757
|
+
}
|
|
3758
|
+
|
|
3759
|
+
// check for collision
|
|
3760
|
+
const sizeBoth = this.size.add(o.size);
|
|
3761
|
+
// prefer to push up if small delta, up away from this object's own gravity
|
|
3762
|
+
const smallStepUp = (gravityY > 0 ? o.pos.y - oldPos.y : oldPos.y - o.pos.y)*2 > sizeBoth.y - abs(gravityY);
|
|
3763
|
+
const isBlockedX = abs(oldPos.y - o.pos.y)*2 < sizeBoth.y;
|
|
3764
|
+
const isBlockedY = abs(oldPos.x - o.pos.x)*2 < sizeBoth.x;
|
|
3765
|
+
const restitution = max(this.restitution, o.restitution);
|
|
3766
|
+
|
|
3767
|
+
if (smallStepUp || isBlockedY || !isBlockedX) // resolve y collision
|
|
3768
|
+
{
|
|
3769
|
+
// push outside object collision
|
|
3770
|
+
this.pos.y = o.pos.y + (sizeBoth.y/2 + epsilon) * sign(oldPos.y - o.pos.y);
|
|
3771
|
+
if ((o.groundObject && wasFalling) || !o.mass)
|
|
3772
|
+
{
|
|
3773
|
+
// set ground object if landed on something
|
|
3774
|
+
if (wasFalling)
|
|
3775
|
+
this.groundObject = o;
|
|
3776
|
+
|
|
3777
|
+
// bounce if other object is fixed or grounded, relative to it so a rider keeps up with a
|
|
3778
|
+
// platform moving down instead of landing on it again every few frames
|
|
3779
|
+
this.velocity.y = o.velocity.y - (this.velocity.y - o.velocity.y) * restitution;
|
|
3780
|
+
}
|
|
3781
|
+
else // o has mass here, the other branch already handled the massless case
|
|
3782
|
+
{
|
|
3783
|
+
// inelastic collision
|
|
3784
|
+
const inelastic = (this.mass * this.velocity.y + o.mass * o.velocity.y) / (this.mass + o.mass);
|
|
3785
|
+
|
|
3786
|
+
// elastic collision
|
|
3787
|
+
const elastic0 = this.velocity.y * (this.mass - o.mass) / (this.mass + o.mass)
|
|
3788
|
+
+ o.velocity.y * 2 * o.mass / (this.mass + o.mass);
|
|
3789
|
+
const elastic1 = o.velocity.y * (o.mass - this.mass) / (this.mass + o.mass)
|
|
3790
|
+
+ this.velocity.y * 2 * this.mass / (this.mass + o.mass);
|
|
3791
|
+
|
|
3792
|
+
// lerp between elastic or inelastic based on restitution
|
|
3793
|
+
this.velocity.y = lerp(inelastic, elastic0, restitution);
|
|
3794
|
+
o.velocity.y = lerp(inelastic, elastic1, restitution);
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3797
|
+
if (!smallStepUp && isBlockedX) // resolve x collision
|
|
3798
|
+
{
|
|
3799
|
+
// push outside collision
|
|
3800
|
+
this.pos.x = o.pos.x + (sizeBoth.x/2 + epsilon) * sign(oldPos.x - o.pos.x);
|
|
3801
|
+
if (o.mass)
|
|
3802
|
+
{
|
|
3803
|
+
// inelastic collision
|
|
3804
|
+
const inelastic = (this.mass * this.velocity.x + o.mass * o.velocity.x) / (this.mass + o.mass);
|
|
3805
|
+
|
|
3806
|
+
// elastic collision
|
|
3807
|
+
const elastic0 = this.velocity.x * (this.mass - o.mass) / (this.mass + o.mass)
|
|
3808
|
+
+ o.velocity.x * 2 * o.mass / (this.mass + o.mass);
|
|
3809
|
+
const elastic1 = o.velocity.x * (o.mass - this.mass) / (this.mass + o.mass)
|
|
3810
|
+
+ this.velocity.x * 2 * this.mass / (this.mass + o.mass);
|
|
3811
|
+
|
|
3812
|
+
// lerp between elastic or inelastic based on restitution
|
|
3813
|
+
this.velocity.x = lerp(inelastic, elastic0, restitution);
|
|
3814
|
+
o.velocity.x = lerp(inelastic, elastic1, restitution);
|
|
3815
|
+
}
|
|
3816
|
+
else // bounce if other object is fixed, relative to it as a landing is
|
|
3817
|
+
this.velocity.x = o.velocity.x - (this.velocity.x - o.velocity.x) * restitution;
|
|
3818
|
+
}
|
|
3819
|
+
engineObjectsCollidePairAdd(this, o, true);
|
|
3820
|
+
debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f0f', undefined, false);
|
|
3821
|
+
}
|
|
3822
|
+
}
|
|
3823
|
+
if (this.collideLevel)
|
|
3824
|
+
{
|
|
3825
|
+
// check collision against tiles
|
|
3826
|
+
const hitLayer = tileCollisionTest(this.pos, this.size, this);
|
|
3827
|
+
if (hitLayer)
|
|
3828
|
+
{
|
|
3829
|
+
// if already was stuck in collision, don't do anything
|
|
3830
|
+
// this should not happen unless something starts in collision
|
|
3831
|
+
if (!tileCollisionTest(oldPos, this.size, this))
|
|
3832
|
+
{
|
|
3833
|
+
// test which side we bounced off (or both if a corner)
|
|
3834
|
+
const isBlockedX = tileCollisionTest(vec2(this.pos.x, oldPos.y), this.size, this);
|
|
3835
|
+
const isBlockedY = tileCollisionTest(vec2(oldPos.x, this.pos.y), this.size, this);
|
|
3836
|
+
const restitution = max(this.restitution, hitLayer.restitution);
|
|
3837
|
+
if (isBlockedX)
|
|
3838
|
+
{
|
|
3839
|
+
// a ledge caught less than maxMove below its top lifts the object onto it instead of stopping it,
|
|
3840
|
+
// down off a ceiling ledge when gravity points up; zero gravity counts as down
|
|
3841
|
+
const epsilon = 1e-3;
|
|
3842
|
+
const maxMove = .1;
|
|
3843
|
+
const y = gravityY > 0 ?
|
|
3844
|
+
ceil( oldPos.y+this.size.y/2-1) - this.size.y/2 - epsilon :
|
|
3845
|
+
floor(oldPos.y-this.size.y/2+1) + this.size.y/2 + epsilon;
|
|
3846
|
+
if (abs(y - this.pos.y) < maxMove && !tileCollisionTest(vec2(this.pos.x, y), this.size, this))
|
|
3847
|
+
{
|
|
3848
|
+
this.pos.y = y;
|
|
3849
|
+
debugPhysics && debugRect(this.pos, this.size, '#ff0', 0, 0, false, false);
|
|
3850
|
+
return;
|
|
3851
|
+
}
|
|
3852
|
+
|
|
3853
|
+
// move to previous X position and bounce
|
|
3854
|
+
this.pos.x = oldPos.x;
|
|
3855
|
+
this.velocity.x *= -restitution;
|
|
3856
|
+
}
|
|
3857
|
+
if (isBlockedY || !isBlockedX)
|
|
3858
|
+
{
|
|
3859
|
+
if (wasFalling)
|
|
3860
|
+
{
|
|
3861
|
+
// adjust position to slightly away from nearest tile
|
|
3862
|
+
// this prevents gap between object and ground
|
|
3863
|
+
const epsilon = .0001;
|
|
3864
|
+
const offset = this.size.y/2 + epsilon;
|
|
3865
|
+
// rounded in the layer's space as its collision test is, or a bottom a hair below a
|
|
3866
|
+
// grid line would round to the row under it, inside the floor, and fall through
|
|
3867
|
+
const layerY = hitLayer.pos.y;
|
|
3868
|
+
this.pos.y = layerY + (gravityY < 0 ?
|
|
3869
|
+
floor(oldPos.y - layerY - this.size.y/2) + offset :
|
|
3870
|
+
ceil( oldPos.y - layerY + this.size.y/2) - offset);
|
|
3871
|
+
|
|
3872
|
+
// set ground object for tile collision
|
|
3873
|
+
this.groundObject = hitLayer;
|
|
3874
|
+
}
|
|
3875
|
+
else
|
|
3876
|
+
{
|
|
3877
|
+
// move to previous Y position
|
|
3878
|
+
this.pos.y = oldPos.y;
|
|
3879
|
+
this.groundObject = undefined;
|
|
3880
|
+
}
|
|
3881
|
+
// bounce velocity
|
|
3882
|
+
this.velocity.y *= -restitution;
|
|
3883
|
+
}
|
|
3884
|
+
debugPhysics && debugRect(this.pos, this.size, '#f00', 0, 0, false, false);
|
|
3885
|
+
}
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3888
|
+
}
|
|
3889
|
+
|
|
3890
|
+
/** Update the object, called automatically by engine once each frame. Does nothing by default. */
|
|
3891
|
+
update() {}
|
|
3892
|
+
|
|
3893
|
+
/** Render the object, draws a tile by default, automatically called each frame, sorted by renderOrder */
|
|
3894
|
+
render()
|
|
3895
|
+
{
|
|
3896
|
+
// default object render
|
|
3897
|
+
drawTile(this.pos, this.drawSize || this.size, this.tileInfo, this.color, this.angle, this.mirror, this.additiveColor, glEnable, false);
|
|
3898
|
+
}
|
|
3899
|
+
|
|
3900
|
+
/** Optional hook called during the light system plugin's lightmap pass to draw this object's lightmap contribution. Does nothing by default. */
|
|
3901
|
+
renderLight() {}
|
|
3902
|
+
|
|
3903
|
+
/** Draw this object into the light system's shadow map, called during its shadow pass when castShadow is set.
|
|
3904
|
+
* Calls render() by default so the object casts its own shape; override to cast a different one, like a blob at a character's feet so its body stays lit;
|
|
3905
|
+
* screen space WebGL draws in render() are skipped during the pass */
|
|
3906
|
+
renderShadow() { this.render(); }
|
|
3907
|
+
|
|
3908
|
+
/** Draw this object's shape into the light system's lightmap, called during its light pass when emissive is above
|
|
3909
|
+
* 0; what it draws shows in its own colors that much brighter. Calls render() by default so the whole object
|
|
3910
|
+
* glows; override to glow a part, like a robot's eyes */
|
|
3911
|
+
renderEmissive() { this.render(); }
|
|
3912
|
+
|
|
3913
|
+
/** Destroy this object, destroy its children, detach its parent, and mark it for removal
|
|
3914
|
+
* @param {boolean} [immediate] - true removes attached effects like particle emitters at once, false lets them finish first */
|
|
3915
|
+
destroy(immediate=false)
|
|
3916
|
+
{
|
|
3917
|
+
if (this.destroyed) return;
|
|
3918
|
+
|
|
3919
|
+
// disconnect from parent and destroy children
|
|
3920
|
+
this.destroyed = true;
|
|
3921
|
+
this.parent?.removeChild(this);
|
|
3922
|
+
for (const child of this.children)
|
|
3923
|
+
{
|
|
3924
|
+
child.parent = undefined;
|
|
3925
|
+
child.destroy(immediate);
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
3928
|
+
|
|
3929
|
+
/** Convert from local space to world space
|
|
3930
|
+
* @param {Vector2} pos - local space point */
|
|
3931
|
+
localToWorld(pos) { return this.pos.add(pos.rotate(this.angle)); }
|
|
3932
|
+
|
|
3933
|
+
/** Convert from world space to local space
|
|
3934
|
+
* @param {Vector2} pos - world space point */
|
|
3935
|
+
worldToLocal(pos) { return pos.subtract(this.pos).rotate(-this.angle); }
|
|
3936
|
+
|
|
3937
|
+
/** Convert from local space to world space for a vector (rotation only)
|
|
3938
|
+
* @param {Vector2} vec - local space vector */
|
|
3939
|
+
localToWorldVector(vec) { return vec.rotate(this.angle); }
|
|
3940
|
+
|
|
3941
|
+
/** Convert from world space to local space for a vector (rotation only)
|
|
3942
|
+
* @param {Vector2} vec - world space vector */
|
|
3943
|
+
worldToLocalVector(vec) { return vec.rotate(-this.angle); }
|
|
3944
|
+
|
|
3945
|
+
/** Called to check if a tile collision should be resolved. Return true for physics to resolve the collision or false to ignore and resolve it manually.
|
|
3946
|
+
* - Called for each solid tile the physics tests, which can be several times a frame for the same tile, and for
|
|
3947
|
+
* positions it only tries, so keep it free of side effects or guard them to once a frame
|
|
3948
|
+
* - this.pos has already moved, so a check on where it came from, like a one way platform, needs the position
|
|
3949
|
+
* saved in update, as the platformer example does
|
|
3950
|
+
* @param {number} tileData - the value of the tile at the position
|
|
3951
|
+
* @param {Vector2} pos - the tile's bottom left corner in world space
|
|
3952
|
+
* @return {boolean} - true if the collision should be resolved by modifying it's position and velocity */
|
|
3953
|
+
collideWithTile(tileData, pos) { return tileData > 0; }
|
|
3954
|
+
|
|
3955
|
+
/** Called by the engine to check if an object collision should be resolved. Return true for physics to resolve the collision or false to ignore and resolve it manually.
|
|
3956
|
+
* - Both objects of a touching pair are asked once a frame, whichever order they update in; an object that
|
|
3957
|
+
* destroys itself here is gone at the end of the frame and is still asked about the pairs left this frame, so a
|
|
3958
|
+
* bullet that should hit one thing checks its own destroyed flag first
|
|
3959
|
+
* @param {EngineObject} object - the object to test against
|
|
3960
|
+
* @param {Vector3} [push] - what it would take to move this object clear, a Vector3 from the 3D plugin, undefined in 2D
|
|
3961
|
+
* @return {boolean} - true if the collision should be resolved by modifying it's position and velocity
|
|
3962
|
+
*/
|
|
3963
|
+
collideWithObject(object, push) { return true; }
|
|
3964
|
+
|
|
3965
|
+
/** Get this object's up vector
|
|
3966
|
+
* @param {number} [scale] - length of the vector
|
|
3967
|
+
* @return {Vector2} */
|
|
3968
|
+
getUp(scale=1) { return vec2().setAngle(this.angle, scale); }
|
|
3969
|
+
|
|
3970
|
+
/** Get this object's right vector
|
|
3971
|
+
* @param {number} [scale] - length of the vector
|
|
3972
|
+
* @return {Vector2} */
|
|
3973
|
+
getRight(scale=1) { return vec2().setAngle(this.angle+PI/2, scale); }
|
|
3974
|
+
|
|
3975
|
+
/** How long since the object was created
|
|
3976
|
+
* @return {number} */
|
|
3977
|
+
getAliveTime() { return time - this.spawnTime; }
|
|
3978
|
+
|
|
3979
|
+
/** Get the speed of this object
|
|
3980
|
+
* @return {number} */
|
|
3981
|
+
getSpeed() { return this.velocity.length(); }
|
|
3982
|
+
|
|
3983
|
+
/** Apply acceleration to this object (adjust velocity, not affected by mass)
|
|
3984
|
+
* - Does nothing on a static object (mass 0), set its velocity instead
|
|
3985
|
+
* @param {Vector2} acceleration */
|
|
3986
|
+
applyAcceleration(acceleration)
|
|
3987
|
+
{ if (this.mass) this.velocity = this.velocity.add(acceleration); }
|
|
3988
|
+
|
|
3989
|
+
/** Apply angular acceleration to this object
|
|
3990
|
+
* - Does nothing on a static object (mass 0), set its angleVelocity instead
|
|
3991
|
+
* @param {number} acceleration */
|
|
3992
|
+
applyAngularAcceleration(acceleration)
|
|
3993
|
+
{ if (this.mass) this.angleVelocity += acceleration; }
|
|
3994
|
+
|
|
3995
|
+
/** Apply force to this object (adjust velocity, affected by mass)
|
|
3996
|
+
* - Does nothing on a static object (mass 0), set its velocity instead
|
|
3997
|
+
* @param {Vector2} force */
|
|
3998
|
+
applyForce(force)
|
|
3999
|
+
{ if (this.mass) this.applyAcceleration(force.scale(1/this.mass)); }
|
|
4000
|
+
|
|
4001
|
+
/** Get the direction of the mirror
|
|
4002
|
+
* @return {number} -1 if this.mirror is true, or 1 if not mirrored */
|
|
4003
|
+
getMirrorSign() { return this.mirror ? -1 : 1; }
|
|
4004
|
+
|
|
4005
|
+
/** Attaches a child to this with a local transform, returns child for chaining
|
|
4006
|
+
* @param {EngineObject} child
|
|
4007
|
+
* @param {Vector2} [localPos=vec2()]
|
|
4008
|
+
* @param {number} [localAngle]
|
|
4009
|
+
* @return {EngineObject} The child object added */
|
|
4010
|
+
addChild(child, localPos=vec2(), localAngle=0)
|
|
4011
|
+
{
|
|
4012
|
+
false&&ASSERT(!this.destroyed, 'cannot add child to destroyed object');
|
|
4013
|
+
if (this.destroyed) return child;
|
|
4014
|
+
false&&ASSERT(!child.parent && !this.children.includes(child), 'child already has a parent, removeChild it first or use attach');
|
|
4015
|
+
false&&ASSERT(child instanceof EngineObject, 'child must be an EngineObject');
|
|
4016
|
+
false&&ASSERT(!child.destroyed, 'cannot add a destroyed child');
|
|
4017
|
+
for (let p = /** @type {EngineObject} */ (this); p; p = p.parent)
|
|
4018
|
+
false&&ASSERT(p !== child, 'cannot add an object as a child of itself or of its own child');
|
|
4019
|
+
this.children.push(child);
|
|
4020
|
+
child.parent = this;
|
|
4021
|
+
child.localPos = localPos.copy();
|
|
4022
|
+
child.localAngle = localAngle;
|
|
4023
|
+
child.updateTransforms();
|
|
4024
|
+
return child;
|
|
4025
|
+
}
|
|
4026
|
+
|
|
4027
|
+
/** Attaches a child to this without moving it: the local transform is worked out from where the child is now,
|
|
4028
|
+
* where addChild takes one; a child of something else is moved over, returns child for chaining
|
|
4029
|
+
* @param {EngineObject} child
|
|
4030
|
+
* @return {EngineObject} The child object attached */
|
|
4031
|
+
attach(child)
|
|
4032
|
+
{
|
|
4033
|
+
false&&ASSERT(child instanceof EngineObject, 'child must be an EngineObject');
|
|
4034
|
+
false&&ASSERT(child !== this, 'cannot attach self');
|
|
4035
|
+
child.parent?.removeChild(child);
|
|
4036
|
+
// the local values that updateTransforms turns back into the child's current pos and angle
|
|
4037
|
+
const mirror = this.getMirrorSign(), local = this.worldToLocal(child.pos);
|
|
4038
|
+
local.x *= mirror;
|
|
4039
|
+
return this.addChild(child, local, mirror * (child.angle - this.angle));
|
|
4040
|
+
}
|
|
4041
|
+
|
|
4042
|
+
/** Removes a child from this one, it stays where it is in the world
|
|
4043
|
+
* @param {EngineObject} child */
|
|
4044
|
+
removeChild(child)
|
|
4045
|
+
{
|
|
4046
|
+
false&&ASSERT(child.parent === this && this.children.includes(child));
|
|
4047
|
+
const i = this.children.indexOf(child);
|
|
4048
|
+
if (i < 0) return; // not a child of this one, release has no assert
|
|
4049
|
+
this.children.splice(i, 1);
|
|
4050
|
+
child.parent = child.localPos = undefined;
|
|
4051
|
+
}
|
|
4052
|
+
|
|
4053
|
+
/** Check if this object's box overlaps another object's box
|
|
4054
|
+
* @param {EngineObject} object
|
|
4055
|
+
* @return {boolean} */
|
|
4056
|
+
isOverlappingObject(object)
|
|
4057
|
+
{ return this.isOverlapping(object.pos, object.size); }
|
|
4058
|
+
|
|
4059
|
+
/** Check if overlapping a point or aligned bounding box
|
|
4060
|
+
* @param {Vector2} pos - Center of box
|
|
4061
|
+
* @param {Vector2} [size=vec2()] - Size of box, uses a point if undefined
|
|
4062
|
+
* @return {boolean} */
|
|
4063
|
+
isOverlapping(pos, size=vec2())
|
|
4064
|
+
{ return isOverlapping(this.pos, this.size, pos, size); }
|
|
4065
|
+
|
|
4066
|
+
/** Set how this object collides
|
|
4067
|
+
* @param {boolean} [collideSolidObjects] - Does it collide with solid objects?
|
|
4068
|
+
* @param {boolean} [isSolid] - Does it collide with and block other objects? (expensive in large numbers)
|
|
4069
|
+
* @param {boolean} [collideLevel] - Does it collide with the level, its tile collision layers?
|
|
4070
|
+
* @param {boolean} [collideRaycast] - Does it collide with raycasts? */
|
|
4071
|
+
setCollision(collideSolidObjects=true, isSolid=true, collideLevel=true, collideRaycast=true)
|
|
4072
|
+
{
|
|
4073
|
+
false&&ASSERT(collideSolidObjects || !isSolid, 'solid objects must be set to collide');
|
|
4074
|
+
|
|
4075
|
+
this.collideSolidObjects = collideSolidObjects;
|
|
4076
|
+
this.isSolid = isSolid;
|
|
4077
|
+
this.collideLevel = collideLevel;
|
|
4078
|
+
this.collideRaycast = collideRaycast;
|
|
4079
|
+
}
|
|
4080
|
+
|
|
4081
|
+
/** @deprecated since 1.20, use collideLevel
|
|
4082
|
+
* @type {boolean} */
|
|
4083
|
+
get collideTiles() { return this.collideLevel; }
|
|
4084
|
+
set collideTiles(collide) { this.collideLevel = collide; }
|
|
4085
|
+
|
|
4086
|
+
/** Returns string containing info about this object for debugging
|
|
4087
|
+
* @return {string} */
|
|
4088
|
+
toString()
|
|
4089
|
+
{
|
|
4090
|
+
let text = 'type = ' + this.constructor.name;
|
|
4091
|
+
if (this.pos.x || this.pos.y)
|
|
4092
|
+
text += '\npos = ' + this.pos;
|
|
4093
|
+
if (this.velocity.x || this.velocity.y)
|
|
4094
|
+
text += '\nvelocity = ' + this.velocity;
|
|
4095
|
+
if (this.size.x || this.size.y)
|
|
4096
|
+
text += '\nsize = ' + this.size;
|
|
4097
|
+
if (this.angle)
|
|
4098
|
+
text += '\nangle = ' + this.angle.toFixed(3);
|
|
4099
|
+
text += '\ncolor = ' + this.color;
|
|
4100
|
+
return text;
|
|
4101
|
+
}
|
|
4102
|
+
|
|
4103
|
+
/** Render debug info for this object */
|
|
4104
|
+
renderDebugInfo()
|
|
4105
|
+
{
|
|
4106
|
+
if (!debug) return;
|
|
4107
|
+
|
|
4108
|
+
// check if there is anything to show
|
|
4109
|
+
const hasPhysics = this.collideLevel || this.collideSolidObjects || this.isSolid;
|
|
4110
|
+
if (!hasPhysics && !this.parent) return;
|
|
4111
|
+
|
|
4112
|
+
// show object info for debugging
|
|
4113
|
+
const size = vec2(max(this.size.x, .2), max(this.size.y, .2));
|
|
4114
|
+
const color = rgb(this.collideLevel?1:0, this.collideSolidObjects?1:0, this.isSolid?1:0, .5);
|
|
4115
|
+
debugRect(this.pos, size, color, 0, hasPhysics ? 0 : this.angle, hasPhysics, false); // collision ignores the angle
|
|
4116
|
+
if (this.parent)
|
|
4117
|
+
debugRect(this.pos, size.scale(.8), rgb(1,1,1,.5), 0, this.angle, false, false);
|
|
4118
|
+
this.parent && debugLine(this.pos, this.parent.pos, rgb(1,1,1,.5), .5, 0, false);
|
|
4119
|
+
}
|
|
4091
4120
|
}
|
|
4092
4121
|
/**
|
|
4093
4122
|
* LittleJS Drawing System
|
|
@@ -4585,17 +4614,29 @@ class SpriteAnimator
|
|
|
4585
4614
|
* @return {SpriteAnimation} */
|
|
4586
4615
|
get clip() { return this.clips[this.name]; }
|
|
4587
4616
|
|
|
4617
|
+
/** The clip showing now, once a clip that has ended has called its onEnd: the callback may switch clips, and a
|
|
4618
|
+
* read gives the clip it switched to, not the one that ended
|
|
4619
|
+
* @return {SpriteAnimation}
|
|
4620
|
+
* @ignore */
|
|
4621
|
+
get settledClip()
|
|
4622
|
+
{
|
|
4623
|
+
// each clip's own isDone calls its onEnd once; a few switches in a row at most
|
|
4624
|
+
for (let i = 4, clip; i-- && clip !== this.clip;)
|
|
4625
|
+
(clip = this.clip).isDone;
|
|
4626
|
+
return this.clip;
|
|
4627
|
+
}
|
|
4628
|
+
|
|
4588
4629
|
/** The tile of the frame showing now
|
|
4589
4630
|
* @return {TileInfo} */
|
|
4590
|
-
get tileInfo() { return this.
|
|
4631
|
+
get tileInfo() { return this.settledClip.tileInfo; }
|
|
4591
4632
|
|
|
4592
4633
|
/** The frame of the clip showing now
|
|
4593
4634
|
* @return {number} */
|
|
4594
|
-
get frame() { return this.
|
|
4635
|
+
get frame() { return this.settledClip.frame; }
|
|
4595
4636
|
|
|
4596
4637
|
/** True once the clip showing now is a play that has ended
|
|
4597
4638
|
* @return {boolean} */
|
|
4598
|
-
get isDone() { return this.
|
|
4639
|
+
get isDone() { return this.settledClip.isDone; }
|
|
4599
4640
|
}
|
|
4600
4641
|
|
|
4601
4642
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -5020,7 +5061,7 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
|
|
|
5020
5061
|
// build regular polygon points
|
|
5021
5062
|
const points = [];
|
|
5022
5063
|
const sizeX = size.x/2, sizeY = size.y/2;
|
|
5023
|
-
for (let i=sides; i
|
|
5064
|
+
for (let i=sides; i-- > 0;) // a count that is not whole, or below zero, still ends
|
|
5024
5065
|
{
|
|
5025
5066
|
const a = (i/sides)*PI*2;
|
|
5026
5067
|
points.push(vec2(sin(a)*sizeX, cos(a)*sizeY));
|
|
@@ -5131,7 +5172,7 @@ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineC
|
|
|
5131
5172
|
if (!ring)
|
|
5132
5173
|
{
|
|
5133
5174
|
const points = [];
|
|
5134
|
-
for (let i=sides; i
|
|
5175
|
+
for (let i=sides; i-- > 0;)
|
|
5135
5176
|
{
|
|
5136
5177
|
const a = (i/sides)*PI*2;
|
|
5137
5178
|
points.push(vec2(sin(a), cos(a)));
|
|
@@ -5231,7 +5272,7 @@ function drawEllipseGradient(pos, size=vec2(1), colorInner=WHITE, colorOuter=CLE
|
|
|
5231
5272
|
const startA = (offset%sides)/sides*PI*2;
|
|
5232
5273
|
const points = [rim(startA)];
|
|
5233
5274
|
const colors = [outerInt];
|
|
5234
|
-
for (let i=sides; i
|
|
5275
|
+
for (let i=sides; i-- > 0;)
|
|
5235
5276
|
{
|
|
5236
5277
|
const a = ((i+offset)%sides)/sides*PI*2;
|
|
5237
5278
|
points.push(pos);
|
|
@@ -5654,7 +5695,8 @@ function isOnScreen(pos, size=0)
|
|
|
5654
5695
|
function setAdditiveBlendMode(additive=true)
|
|
5655
5696
|
{
|
|
5656
5697
|
glAdditive = additive;
|
|
5657
|
-
drawContext
|
|
5698
|
+
if (drawContext) // none headless
|
|
5699
|
+
drawContext.globalCompositeOperation = additive ? 'lighter' : 'source-over';
|
|
5658
5700
|
}
|
|
5659
5701
|
|
|
5660
5702
|
/** Set the Shader that 2D draws use from now on, none for the engine's own
|
|
@@ -5777,9 +5819,12 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
|
|
|
5777
5819
|
}
|
|
5778
5820
|
else
|
|
5779
5821
|
{
|
|
5780
|
-
// copy to offscreen canvas
|
|
5781
|
-
|
|
5782
|
-
workReadCanvas.height
|
|
5822
|
+
// copy to offscreen canvas, sized again only when the size changes: setting a size remakes the canvas even
|
|
5823
|
+
// to the same one, and a game's tinted tiles are mostly of one size
|
|
5824
|
+
if (workReadCanvas.width !== sWidth || workReadCanvas.height !== sHeight)
|
|
5825
|
+
workReadCanvas.width = sWidth, workReadCanvas.height = sHeight;
|
|
5826
|
+
else
|
|
5827
|
+
workReadContext.clearRect(0, 0, sWidth, sHeight);
|
|
5783
5828
|
workReadContext.drawImage(image, sx|0, sy|0, sWidth, sHeight, 0, 0, sWidth, sHeight);
|
|
5784
5829
|
|
|
5785
5830
|
// tint image using offscreen work context
|
|
@@ -6034,6 +6079,13 @@ let mouseDeltaScreen = vec2();
|
|
|
6034
6079
|
* @memberof Input */
|
|
6035
6080
|
let mouseWheel = 0;
|
|
6036
6081
|
|
|
6082
|
+
/** Touch pinch delta this frame, in the mouse wheel's units: two fingers moving apart are negative, as the wheel
|
|
6083
|
+
* turned up is, and moving together positive, 1 for every 50 pixels; added to mouseWheel while touchPinchWheel
|
|
6084
|
+
* is on, so a game that zooms with the wheel zooms with a pinch
|
|
6085
|
+
* @type {number}
|
|
6086
|
+
* @memberof Input */
|
|
6087
|
+
let touchPinch = 0;
|
|
6088
|
+
|
|
6037
6089
|
/** True if mouse was inside the document window, set to false when mouse leaves
|
|
6038
6090
|
* @type {boolean}
|
|
6039
6091
|
* @memberof Input */
|
|
@@ -6122,6 +6174,7 @@ function inputClearKey(key, device=0, clearDown=true, clearPressed=true, clearRe
|
|
|
6122
6174
|
const inputWASDToArrow = {KeyW:'ArrowUp', KeyS:'ArrowDown', KeyA:'ArrowLeft', KeyD:'ArrowRight'};
|
|
6123
6175
|
const inputArrowToWASD = {ArrowUp:'KeyW', ArrowDown:'KeyS', ArrowLeft:'KeyA', ArrowRight:'KeyD'};
|
|
6124
6176
|
const inputKeysHeld = new Set; // the keys physically down, since an arrow's slot is shared with its alias
|
|
6177
|
+
let inputPinch; // the two fingers pinching and how far apart they were, undefined with no pinch
|
|
6125
6178
|
let inputWasTouching = 0, inputTouchIdentifier; // the touch driving the mouse, cleared with the input so only a new touch presses
|
|
6126
6179
|
let inputLastTouchTime = -1e9; // when a touch event last came, the mouse events a browser makes from a tap follow it
|
|
6127
6180
|
|
|
@@ -6164,6 +6217,7 @@ function inputClear()
|
|
|
6164
6217
|
if (!i || inputData[i]) inputData[i] = [];
|
|
6165
6218
|
inputKeysHeld.clear();
|
|
6166
6219
|
inputWasTouching = 0;
|
|
6220
|
+
inputPinch = undefined;
|
|
6167
6221
|
// release the touch gamepad, a finger still on it has to lift before it can take a control again
|
|
6168
6222
|
touchGamepadPointerRole.clear();
|
|
6169
6223
|
touchGamepadButtons.length = 0;
|
|
@@ -6691,19 +6745,52 @@ function inputInit()
|
|
|
6691
6745
|
const pressTouch = !inputWasTouching && e.type == 'touchstart' &&
|
|
6692
6746
|
[...e.changedTouches].find(t=> !isGamepadTouch(t));
|
|
6693
6747
|
const touch = pressTouch || gameTouches[0];
|
|
6748
|
+
let pos = vec2(touch.clientX, touch.clientY);
|
|
6749
|
+
|
|
6750
|
+
// two fingers pinch: how far they moved together or apart is the touch screen's wheel, a
|
|
6751
|
+
// click for every 50 pixels, and the mouse is between them, the place to zoom about; with
|
|
6752
|
+
// touchPinchWheel off the pinch is only measured, and the first finger is the mouse as ever
|
|
6753
|
+
const pinching = touching > 1 && touchPinchWheel;
|
|
6754
|
+
if (touching > 1)
|
|
6755
|
+
{
|
|
6756
|
+
const [a, b] = gameTouches, key = a.identifier + ' ' + b.identifier;
|
|
6757
|
+
const distance = hypot(a.clientX - b.clientX, a.clientY - b.clientY);
|
|
6758
|
+
if (inputPinch && inputPinch.key === key)
|
|
6759
|
+
{
|
|
6760
|
+
const delta = (inputPinch.distance - distance) / 50;
|
|
6761
|
+
touchPinch += delta;
|
|
6762
|
+
if (touchPinchWheel)
|
|
6763
|
+
mouseWheel += delta;
|
|
6764
|
+
}
|
|
6765
|
+
inputPinch = {key, distance}; // other fingers start it over, with no jump
|
|
6766
|
+
if (pinching)
|
|
6767
|
+
pos = vec2((a.clientX + b.clientX) / 2, (a.clientY + b.clientY) / 2);
|
|
6768
|
+
}
|
|
6769
|
+
else
|
|
6770
|
+
inputPinch = undefined;
|
|
6694
6771
|
|
|
6695
6772
|
// set event pos and pass it along
|
|
6696
|
-
const pos = vec2(touch.clientX, touch.clientY);
|
|
6697
6773
|
const mousePosScreenLast = mousePosScreen;
|
|
6698
6774
|
mousePosScreen = mouseEventToScreen(pos);
|
|
6699
|
-
if (
|
|
6775
|
+
if (pinching)
|
|
6776
|
+
{
|
|
6777
|
+
// a pinch is not a press or a drag, the finger that pressed lets go
|
|
6778
|
+
if (inputData[0][button] & 1)
|
|
6779
|
+
inputData[0][button] = inputData[0][button] & 2 | 4;
|
|
6780
|
+
}
|
|
6781
|
+
else if (inputWasTouching && touch.identifier === inputTouchIdentifier)
|
|
6700
6782
|
mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
|
|
6701
6783
|
else if (pressTouch)
|
|
6702
6784
|
inputData[0][button] = 3;
|
|
6703
|
-
|
|
6785
|
+
// the finger left after a pinch moves the mouse from where it is, with no jump and no press
|
|
6786
|
+
inputTouchIdentifier = pinching ? undefined : touch.identifier;
|
|
6787
|
+
}
|
|
6788
|
+
else
|
|
6789
|
+
{
|
|
6790
|
+
inputPinch = undefined;
|
|
6791
|
+
if (inputWasTouching && (inputData[0][button] & 1))
|
|
6792
|
+
inputData[0][button] = inputData[0][button] & 2 | 4; // released only if it was pressed
|
|
6704
6793
|
}
|
|
6705
|
-
else if (inputWasTouching && (inputData[0][button] & 1))
|
|
6706
|
-
inputData[0][button] = inputData[0][button] & 2 | 4; // released only if it was pressed
|
|
6707
6794
|
|
|
6708
6795
|
// set was touching
|
|
6709
6796
|
inputWasTouching = touching;
|
|
@@ -6962,7 +7049,7 @@ function inputUpdatePost()
|
|
|
6962
7049
|
for (const deviceInputData of inputData)
|
|
6963
7050
|
for (const i in deviceInputData)
|
|
6964
7051
|
deviceInputData[i] &= 1;
|
|
6965
|
-
mouseWheel = 0;
|
|
7052
|
+
mouseWheel = touchPinch = 0;
|
|
6966
7053
|
mouseDelta = vec2();
|
|
6967
7054
|
mouseDeltaScreen = vec2();
|
|
6968
7055
|
}
|
|
@@ -7683,7 +7770,13 @@ class Sound
|
|
|
7683
7770
|
*/
|
|
7684
7771
|
constructor(asset, randomness, range, taper=soundDefaultTaper, onloadCallback)
|
|
7685
7772
|
{
|
|
7686
|
-
if (!soundEnable || headlessMode)
|
|
7773
|
+
if (!soundEnable || headlessMode)
|
|
7774
|
+
{
|
|
7775
|
+
// no sound is made: it counts as loaded, so a game that waits for its sounds goes on
|
|
7776
|
+
this.loadedPercent = 1;
|
|
7777
|
+
onloadCallback?.(this);
|
|
7778
|
+
return;
|
|
7779
|
+
}
|
|
7687
7780
|
const rangeIsDefault = range === undefined;
|
|
7688
7781
|
if (rangeIsDefault)
|
|
7689
7782
|
range = soundDefaultRange;
|
|
@@ -7694,10 +7787,9 @@ class Sound
|
|
|
7694
7787
|
false&&ASSERT(isNumber(range), 'range must be a number');
|
|
7695
7788
|
false&&ASSERT(isNumber(taper), 'taper must be a number');
|
|
7696
7789
|
|
|
7697
|
-
/** @property {number} - World space max range of sound */
|
|
7698
7790
|
this.range = range;
|
|
7699
7791
|
/** @property {boolean} - No range was given, so it got soundDefaultRange; played in 3D such a sound is heard
|
|
7700
|
-
* to render3D.soundDefaultRange instead, which is further */
|
|
7792
|
+
* to render3D.soundDefaultRange instead, which is further; setting range makes it false */
|
|
7701
7793
|
this.rangeIsDefault = rangeIsDefault;
|
|
7702
7794
|
/** @property {number} - At what percentage of range should it start tapering */
|
|
7703
7795
|
this.taper = taper;
|
|
@@ -7753,6 +7845,15 @@ class Sound
|
|
|
7753
7845
|
}
|
|
7754
7846
|
}
|
|
7755
7847
|
|
|
7848
|
+
/** World space max range of sound, 0 for no limit; a range that is set is the sound's own, in 3D too
|
|
7849
|
+
* @type {number} */
|
|
7850
|
+
get range() { return this.rangeValue; }
|
|
7851
|
+
set range(range)
|
|
7852
|
+
{
|
|
7853
|
+
this.rangeValue = range;
|
|
7854
|
+
this.rangeIsDefault = false;
|
|
7855
|
+
}
|
|
7856
|
+
|
|
7756
7857
|
/** Sample data for each channel
|
|
7757
7858
|
* Sounds keep their samples in an audio buffer, so reading this rebuilds
|
|
7758
7859
|
* the arrays from it and caches them. The copies are safe to hold onto,
|
|
@@ -8822,7 +8923,8 @@ function objectLayersMake(tileMapData, object)
|
|
|
8822
8923
|
const name = object.type || object.class, type = objectLayersTypes.get(name);
|
|
8823
8924
|
if (!type)
|
|
8824
8925
|
{
|
|
8825
|
-
|
|
8926
|
+
// an object with no type, a shape or a text someone drew in Tiled, is not one a game makes
|
|
8927
|
+
debug && name && console.warn(`objectLayersLoad: no type added for ${name}, skipped`);
|
|
8826
8928
|
return;
|
|
8827
8929
|
}
|
|
8828
8930
|
const {height=0, tilewidth=1, tileheight=1} = tileMapData;
|
|
@@ -10906,6 +11008,9 @@ function glDrawUntextured(x, y, sizeX, sizeY, angle, rgba)
|
|
|
10906
11008
|
glDraw(x, y, sizeX, sizeY, angle, 0, 0, 0, 0, 0, rgba);
|
|
10907
11009
|
}
|
|
10908
11010
|
|
|
11011
|
+
// the list and the vectors glDrawPointsTransform writes its points into
|
|
11012
|
+
const glPointsOut = [], glPointsPool = [];
|
|
11013
|
+
|
|
10909
11014
|
/** Transform and add a polygon to the gl draw list
|
|
10910
11015
|
* @param {Array<Vector2>} points - Array of Vector2 points
|
|
10911
11016
|
* @param {number} rgba - Color of the polygon as a 32-bit integer
|
|
@@ -10918,15 +11023,21 @@ function glDrawUntextured(x, y, sizeX, sizeY, angle, rgba)
|
|
|
10918
11023
|
* @memberof WebGL */
|
|
10919
11024
|
function glDrawPointsTransform(points, rgba, x, y, sx, sy, angle, tristrip=true)
|
|
10920
11025
|
{
|
|
10921
|
-
|
|
11026
|
+
// written into vectors kept for this, a circle would make one for each of its sides at every draw; they are
|
|
11027
|
+
// read into the batch before this returns
|
|
11028
|
+
const pointsOut = glPointsOut, pool = glPointsPool, count = points.length;
|
|
11029
|
+
pointsOut.length = count;
|
|
10922
11030
|
const sa = sin(-angle);
|
|
10923
11031
|
const ca = cos(-angle);
|
|
10924
|
-
for (
|
|
11032
|
+
for (let i = 0; i < count; ++i)
|
|
10925
11033
|
{
|
|
10926
11034
|
// transform the point
|
|
11035
|
+
const p = points[i], out = pool[i] ||= vec2();
|
|
10927
11036
|
const px = p.x*sx;
|
|
10928
11037
|
const py = p.y*sy;
|
|
10929
|
-
|
|
11038
|
+
out.x = x + ca*px - sa*py;
|
|
11039
|
+
out.y = y + sa*px + ca*py;
|
|
11040
|
+
pointsOut[i] = out;
|
|
10930
11041
|
}
|
|
10931
11042
|
const drawPoints = tristrip ? glPolyStrip(pointsOut) : pointsOut;
|
|
10932
11043
|
glDrawPoints(drawPoints, rgba);
|
|
@@ -12361,8 +12472,11 @@ class PostProcessPlugin
|
|
|
12361
12472
|
{
|
|
12362
12473
|
// copy main canvas to work canvas at the backing store size,
|
|
12363
12474
|
// mainCanvasSize is css pixels so it would lose resolution
|
|
12364
|
-
|
|
12365
|
-
workCanvas.height
|
|
12475
|
+
// sized again only when the canvas changed, setting a size remakes the canvas even to the same one
|
|
12476
|
+
if (workCanvas.width !== mainCanvas.width || workCanvas.height !== mainCanvas.height)
|
|
12477
|
+
workCanvas.width = mainCanvas.width, workCanvas.height = mainCanvas.height;
|
|
12478
|
+
else
|
|
12479
|
+
workContext.clearRect(0, 0, workCanvas.width, workCanvas.height);
|
|
12366
12480
|
glCopyToContext(workContext);
|
|
12367
12481
|
workContext.drawImage(mainCanvas, 0, 0);
|
|
12368
12482
|
// clear the main canvas with clearRect, resizing it would also
|
|
@@ -13118,13 +13232,13 @@ class LightSystemPlugin
|
|
|
13118
13232
|
// an automatic size follows the canvas, so reallocate the lightmap when
|
|
13119
13233
|
// the canvas changed size, after the flush so the batch keeps its texture
|
|
13120
13234
|
const size = lightSystem.textureSize;
|
|
13121
|
-
|
|
13122
|
-
|
|
13235
|
+
const wantX = glClampTextureSize(mainCanvasSize.x), wantY = glClampTextureSize(mainCanvasSize.y);
|
|
13236
|
+
if (lightSystem.textureSizeAuto && (size.x !== wantX || size.y !== wantY))
|
|
13123
13237
|
{
|
|
13124
|
-
lightSystem.textureSize =
|
|
13238
|
+
lightSystem.textureSize = vec2(wantX, wantY);
|
|
13125
13239
|
glContext.bindTexture(glContext.TEXTURE_2D, lightSystem.texture);
|
|
13126
13240
|
glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA,
|
|
13127
|
-
|
|
13241
|
+
wantX, wantY, 0,
|
|
13128
13242
|
glContext.RGBA, glContext.UNSIGNED_BYTE, null);
|
|
13129
13243
|
// put back the texture the engine tracks, a draw in renderLight must not sample the lightmap
|
|
13130
13244
|
if (glActiveTexture)
|
|
@@ -13264,8 +13378,10 @@ class LightSystemPlugin
|
|
|
13264
13378
|
this.shadowMapSize = glClampTextureSize(this.shadowMapSize);
|
|
13265
13379
|
this.shadowTextureSize = glClampTextureSize(this.shadowTextureSize);
|
|
13266
13380
|
const size = this.textureSize;
|
|
13267
|
-
if (size)
|
|
13268
|
-
|
|
13381
|
+
if (!size) return;
|
|
13382
|
+
const x = glClampTextureSize(size.x), y = glClampTextureSize(size.y);
|
|
13383
|
+
if (x !== size.x || y !== size.y)
|
|
13384
|
+
this.textureSize = vec2(x, y); // only when it is too large, this runs every frame
|
|
13269
13385
|
}
|
|
13270
13386
|
|
|
13271
13387
|
/** Draw a single Light's falloff blob into the currently bound lightmap.
|
|
@@ -13454,163 +13570,165 @@ class Light extends EngineObject
|
|
|
13454
13570
|
}
|
|
13455
13571
|
}
|
|
13456
13572
|
|
|
13457
|
-
/**
|
|
13458
|
-
* LittleJS ZzFXM Plugin
|
|
13459
|
-
*
|
|
13460
|
-
|
|
13461
|
-
|
|
13462
|
-
|
|
13463
|
-
|
|
13464
|
-
|
|
13465
|
-
*
|
|
13466
|
-
*
|
|
13467
|
-
*
|
|
13468
|
-
* @
|
|
13469
|
-
*
|
|
13470
|
-
*
|
|
13471
|
-
*
|
|
13472
|
-
*
|
|
13473
|
-
*
|
|
13474
|
-
*
|
|
13475
|
-
*
|
|
13476
|
-
*
|
|
13477
|
-
*
|
|
13478
|
-
*
|
|
13479
|
-
*
|
|
13480
|
-
*
|
|
13481
|
-
*
|
|
13482
|
-
*
|
|
13483
|
-
*
|
|
13484
|
-
*
|
|
13485
|
-
*
|
|
13486
|
-
*
|
|
13487
|
-
*
|
|
13488
|
-
*
|
|
13489
|
-
* ]
|
|
13490
|
-
*
|
|
13491
|
-
*
|
|
13492
|
-
*
|
|
13493
|
-
|
|
13494
|
-
|
|
13495
|
-
|
|
13496
|
-
|
|
13497
|
-
|
|
13498
|
-
|
|
13499
|
-
|
|
13500
|
-
|
|
13501
|
-
|
|
13502
|
-
|
|
13503
|
-
|
|
13504
|
-
|
|
13505
|
-
|
|
13506
|
-
|
|
13507
|
-
|
|
13508
|
-
|
|
13509
|
-
|
|
13510
|
-
|
|
13511
|
-
|
|
13512
|
-
|
|
13513
|
-
|
|
13514
|
-
|
|
13515
|
-
|
|
13516
|
-
|
|
13517
|
-
* @param {Array}
|
|
13518
|
-
* @param {
|
|
13519
|
-
* @
|
|
13520
|
-
* @
|
|
13521
|
-
|
|
13522
|
-
|
|
13523
|
-
|
|
13524
|
-
|
|
13525
|
-
let
|
|
13526
|
-
let
|
|
13527
|
-
let
|
|
13528
|
-
let
|
|
13529
|
-
let
|
|
13530
|
-
let
|
|
13531
|
-
let
|
|
13532
|
-
let
|
|
13533
|
-
let
|
|
13534
|
-
let
|
|
13535
|
-
let
|
|
13536
|
-
let
|
|
13537
|
-
let
|
|
13538
|
-
let
|
|
13539
|
-
let
|
|
13540
|
-
let
|
|
13541
|
-
let
|
|
13542
|
-
let
|
|
13543
|
-
let
|
|
13544
|
-
|
|
13545
|
-
|
|
13546
|
-
|
|
13547
|
-
|
|
13548
|
-
|
|
13549
|
-
|
|
13550
|
-
|
|
13551
|
-
|
|
13552
|
-
|
|
13553
|
-
|
|
13554
|
-
|
|
13555
|
-
|
|
13556
|
-
|
|
13557
|
-
|
|
13558
|
-
|
|
13559
|
-
|
|
13560
|
-
|
|
13561
|
-
//
|
|
13562
|
-
|
|
13563
|
-
for
|
|
13564
|
-
|
|
13565
|
-
|
|
13566
|
-
|
|
13567
|
-
|
|
13568
|
-
|
|
13569
|
-
|
|
13570
|
-
|
|
13571
|
-
|
|
13572
|
-
|
|
13573
|
-
|
|
13574
|
-
|
|
13575
|
-
|
|
13576
|
-
|
|
13577
|
-
|
|
13578
|
-
|
|
13579
|
-
|
|
13580
|
-
|
|
13581
|
-
|
|
13582
|
-
|
|
13583
|
-
|
|
13584
|
-
|
|
13585
|
-
|
|
13586
|
-
|
|
13587
|
-
|
|
13588
|
-
|
|
13589
|
-
|
|
13590
|
-
|
|
13591
|
-
|
|
13592
|
-
|
|
13593
|
-
|
|
13594
|
-
|
|
13595
|
-
|
|
13596
|
-
|
|
13597
|
-
|
|
13598
|
-
|
|
13599
|
-
|
|
13600
|
-
|
|
13601
|
-
|
|
13602
|
-
|
|
13603
|
-
|
|
13604
|
-
|
|
13605
|
-
|
|
13606
|
-
|
|
13607
|
-
|
|
13608
|
-
|
|
13609
|
-
|
|
13610
|
-
|
|
13611
|
-
|
|
13612
|
-
|
|
13613
|
-
|
|
13573
|
+
/**
|
|
13574
|
+
* LittleJS ZzFXM Plugin
|
|
13575
|
+
* - A port of ZzFXM, the Zuper Zmall Zound Zynth music player, by Keith Clark and Frank Force, MIT licensed,
|
|
13576
|
+
* https://github.com/keithclark/ZzFXM; its notice is in COPYRIGHT.txt
|
|
13577
|
+
* @namespace ZzFXM
|
|
13578
|
+
*/
|
|
13579
|
+
|
|
13580
|
+
/**
|
|
13581
|
+
* Music Object - Stores a zzfx music track for later use
|
|
13582
|
+
*
|
|
13583
|
+
* <a href=https://keithclark.github.io/ZzFXM/>Create music with the ZzFXM tracker.</a>
|
|
13584
|
+
* @extends Sound
|
|
13585
|
+
* @memberof ZzFXM
|
|
13586
|
+
* @example
|
|
13587
|
+
* // create some music
|
|
13588
|
+
* const music_example = new ZzFXMusic(
|
|
13589
|
+
* [
|
|
13590
|
+
* [ // instruments
|
|
13591
|
+
* [,0,400] // simple note
|
|
13592
|
+
* ],
|
|
13593
|
+
* [ // patterns
|
|
13594
|
+
* [ // pattern 1
|
|
13595
|
+
* [ // channel 0
|
|
13596
|
+
* 0, -1, // instrument 0, left speaker
|
|
13597
|
+
* 1, 0, 9, 1 // channel notes
|
|
13598
|
+
* ],
|
|
13599
|
+
* [ // channel 1
|
|
13600
|
+
* 0, 1, // instrument 0, right speaker
|
|
13601
|
+
* 0, 12, 17, -1 // channel notes
|
|
13602
|
+
* ]
|
|
13603
|
+
* ],
|
|
13604
|
+
* ],
|
|
13605
|
+
* [0, 0, 0, 0], // sequence, play pattern 0 four times
|
|
13606
|
+
* 90 // BPM
|
|
13607
|
+
* ]);
|
|
13608
|
+
*
|
|
13609
|
+
* // play the music on a loop
|
|
13610
|
+
* music_example.playMusic();
|
|
13611
|
+
*/
|
|
13612
|
+
class ZzFXMusic extends Sound
|
|
13613
|
+
{
|
|
13614
|
+
/** Create a music object and cache the zzfx music samples for later use
|
|
13615
|
+
* @param {[Array, Array, Array, number?, ...any[]]} zzfxMusic - Array of zzfx music parameters: instruments, patterns, sequence, and an optional BPM; anything after it, like the tracker's metadata, is ignored
|
|
13616
|
+
*/
|
|
13617
|
+
constructor(zzfxMusic)
|
|
13618
|
+
{
|
|
13619
|
+
super(undefined);
|
|
13620
|
+
|
|
13621
|
+
if (!soundEnable || headlessMode) return;
|
|
13622
|
+
const [instruments, patterns, sequence, BPM] = zzfxMusic;
|
|
13623
|
+
super.sampleChannels = zzfxM(instruments, patterns, sequence, BPM); // the setter, without declaring a field that hides it in the typings
|
|
13624
|
+
this.buildSampleBuffer(); // hand the samples to an audio buffer now, like a zzfx sound, so the arrays are released
|
|
13625
|
+
this.loadedPercent = 1; // generated in place, so it is loaded like a zzfx sound
|
|
13626
|
+
}
|
|
13627
|
+
}
|
|
13628
|
+
|
|
13629
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
13630
|
+
// ZzFX Music Renderer v2.0.3 by Keith Clark and Frank Force
|
|
13631
|
+
|
|
13632
|
+
/** Generate samples for a ZzFM song with given parameters
|
|
13633
|
+
* @param {Array} instruments - Array of ZzFX sound parameters
|
|
13634
|
+
* @param {Array} patterns - Array of pattern data
|
|
13635
|
+
* @param {Array} sequence - Array of pattern indexes
|
|
13636
|
+
* @param {number} [BPM] - Playback speed of the song in BPM
|
|
13637
|
+
* @return {Array} - Left and right channel sample data
|
|
13638
|
+
* @memberof ZzFXM */
|
|
13639
|
+
function zzfxM(instruments, patterns, sequence, BPM = 125)
|
|
13640
|
+
{
|
|
13641
|
+
let i, j, k;
|
|
13642
|
+
let instrumentParameters;
|
|
13643
|
+
let note;
|
|
13644
|
+
let sample;
|
|
13645
|
+
let patternChannel;
|
|
13646
|
+
let notFirstBeat;
|
|
13647
|
+
let stop;
|
|
13648
|
+
let instrument;
|
|
13649
|
+
let attenuation;
|
|
13650
|
+
let outSampleOffset;
|
|
13651
|
+
let isSequenceEnd;
|
|
13652
|
+
let sampleOffset = 0;
|
|
13653
|
+
let nextSampleOffset;
|
|
13654
|
+
let sampleBuffer = [];
|
|
13655
|
+
let leftChannelBuffer = [];
|
|
13656
|
+
let rightChannelBuffer = [];
|
|
13657
|
+
let channelIndex = 0;
|
|
13658
|
+
let panning = 0;
|
|
13659
|
+
let hasMore = 1;
|
|
13660
|
+
let sampleCache = {};
|
|
13661
|
+
let beatLength = audioDefaultSampleRate / BPM * 60 >> 2;
|
|
13662
|
+
|
|
13663
|
+
// for each channel in order until there are no more
|
|
13664
|
+
for (; hasMore; channelIndex++) {
|
|
13665
|
+
|
|
13666
|
+
// reset current values
|
|
13667
|
+
sampleBuffer = [hasMore = notFirstBeat = outSampleOffset = 0];
|
|
13668
|
+
|
|
13669
|
+
// for each pattern in sequence
|
|
13670
|
+
sequence.forEach((patternIndex, sequenceIndex)=> {
|
|
13671
|
+
// get pattern for current channel, use empty 1 note pattern if none found
|
|
13672
|
+
patternChannel = patterns[patternIndex][channelIndex] || [0, 0, 0];
|
|
13673
|
+
|
|
13674
|
+
// check if there are more channels
|
|
13675
|
+
hasMore |= patterns[patternIndex][channelIndex]&&1;
|
|
13676
|
+
|
|
13677
|
+
// get next offset, use the length of first channel
|
|
13678
|
+
nextSampleOffset = outSampleOffset + (patterns[patternIndex][0].length - 2 - (notFirstBeat?0:1)) * beatLength;
|
|
13679
|
+
// for each beat in pattern, plus one extra if end of sequence
|
|
13680
|
+
isSequenceEnd = sequenceIndex === sequence.length - 1;
|
|
13681
|
+
for (i = 2, k = outSampleOffset; i < patternChannel.length + isSequenceEnd; notFirstBeat = ++i) {
|
|
13682
|
+
|
|
13683
|
+
// <channel-note>
|
|
13684
|
+
note = patternChannel[i];
|
|
13685
|
+
|
|
13686
|
+
// stop if end, different instrument or new note
|
|
13687
|
+
stop = i === patternChannel.length + isSequenceEnd - 1 && isSequenceEnd ||
|
|
13688
|
+
instrument !== (patternChannel[0] || 0) || note | 0;
|
|
13689
|
+
|
|
13690
|
+
// fill buffer with samples for previous beat, most cpu intensive part
|
|
13691
|
+
for (j = 0; j < beatLength && notFirstBeat;
|
|
13692
|
+
|
|
13693
|
+
// fade off attenuation at end of beat if stopping note, prevents clicking
|
|
13694
|
+
j++ > beatLength - 99 && stop && attenuation < 1? attenuation += 1 / 99 : 0
|
|
13695
|
+
) {
|
|
13696
|
+
// copy sample to stereo buffers with panning
|
|
13697
|
+
sample = (1 - attenuation) * sampleBuffer[sampleOffset++] / 2 || 0;
|
|
13698
|
+
leftChannelBuffer[k] = (leftChannelBuffer[k] || 0) - sample * panning + sample;
|
|
13699
|
+
rightChannelBuffer[k] = (rightChannelBuffer[k++] || 0) + sample * panning + sample;
|
|
13700
|
+
}
|
|
13701
|
+
|
|
13702
|
+
// set up for next note
|
|
13703
|
+
if (note) {
|
|
13704
|
+
// set attenuation
|
|
13705
|
+
attenuation = note % 1;
|
|
13706
|
+
panning = patternChannel[1] || 0;
|
|
13707
|
+
if (note |= 0) {
|
|
13708
|
+
// get cached sample
|
|
13709
|
+
sampleBuffer = sampleCache[
|
|
13710
|
+
[
|
|
13711
|
+
instrument = patternChannel[sampleOffset = 0] || 0,
|
|
13712
|
+
note
|
|
13713
|
+
]
|
|
13714
|
+
] = sampleCache[[instrument, note]] || (
|
|
13715
|
+
// add sample to cache
|
|
13716
|
+
instrumentParameters = [...instruments[instrument]],
|
|
13717
|
+
instrumentParameters[2] = (instrumentParameters[2] || 220) * 2**(note / 12 - 1),
|
|
13718
|
+
|
|
13719
|
+
// allow negative values to stop notes
|
|
13720
|
+
note > 0 ? zzfxG(...instrumentParameters) : []
|
|
13721
|
+
);
|
|
13722
|
+
}
|
|
13723
|
+
}
|
|
13724
|
+
}
|
|
13725
|
+
|
|
13726
|
+
// update the sample offset
|
|
13727
|
+
outSampleOffset = nextSampleOffset;
|
|
13728
|
+
});
|
|
13729
|
+
}
|
|
13730
|
+
|
|
13731
|
+
return [leftChannelBuffer, rightChannelBuffer];
|
|
13614
13732
|
}
|
|
13615
13733
|
/**
|
|
13616
13734
|
* LittleJS Audio Effects Plugin
|
|
@@ -14601,8 +14719,7 @@ class UISystemPlugin
|
|
|
14601
14719
|
}
|
|
14602
14720
|
}
|
|
14603
14721
|
|
|
14604
|
-
/** Destroy and remove all objects
|
|
14605
|
-
* @memberof UISystem */
|
|
14722
|
+
/** Destroy and remove all objects */
|
|
14606
14723
|
destroyObjects()
|
|
14607
14724
|
{
|
|
14608
14725
|
for (const o of this.uiObjects)
|
|
@@ -15123,8 +15240,10 @@ class UIObject
|
|
|
15123
15240
|
return void inputClearKey(0, 0, false, true, false);
|
|
15124
15241
|
}
|
|
15125
15242
|
}
|
|
15243
|
+
// the object that was the active one going into this update: one pressed and let go inside a
|
|
15244
|
+
// frame is clicked on the next, with its release, not on both
|
|
15126
15245
|
if (!uiSystem.activateOnPress)
|
|
15127
|
-
if (!mouseDown && this.isActiveObject() && this.interactive)
|
|
15246
|
+
if (!mouseDown && isActive && this.isActiveObject() && this.interactive)
|
|
15128
15247
|
this.click();
|
|
15129
15248
|
if (this.destroyed) return;
|
|
15130
15249
|
}
|
|
@@ -15867,7 +15986,10 @@ class UIVideo extends UIObject
|
|
|
15867
15986
|
if (this.destroyed)
|
|
15868
15987
|
return;
|
|
15869
15988
|
|
|
15989
|
+
// let go of the media too, a paused video keeps what it has loaded
|
|
15870
15990
|
this.video.pause();
|
|
15991
|
+
this.video.removeAttribute?.('src');
|
|
15992
|
+
this.video.load?.();
|
|
15871
15993
|
this.video.remove();
|
|
15872
15994
|
super.destroy();
|
|
15873
15995
|
}
|
|
@@ -19206,6 +19328,7 @@ function loadAtlas(imageSrc, jsonSrc, padding=textureSheetPadding)
|
|
|
19206
19328
|
for (const group of parseAtlas(data))
|
|
19207
19329
|
{
|
|
19208
19330
|
// reserve a block of full size cells, one per frame
|
|
19331
|
+
if (!group.frames.length) continue; // a tag with no frames, the groups after it still load
|
|
19209
19332
|
const sourceSize = group.frames[0].sourceSize;
|
|
19210
19333
|
const blockSize = vec2(sourceSize.x*group.frames.length, sourceSize.y);
|
|
19211
19334
|
const added = textureSheetAdd(blockSize, sourceSize, padding);
|
|
@@ -19991,45 +20114,50 @@ function tweenUpdate(gameDelta, realDelta)
|
|
|
19991
20114
|
const list = tweenUpdateList.length ? [] : tweenUpdateList, pass = ++tweenUpdatePass;
|
|
19992
20115
|
for (const t of tweenActive)
|
|
19993
20116
|
list.push(t);
|
|
19994
|
-
|
|
20117
|
+
// the list is let go however the walk ends: a callback that throws must not leave it held, or every
|
|
20118
|
+
// update after would take it for an update still going and make a list of its own
|
|
20119
|
+
try
|
|
19995
20120
|
{
|
|
19996
|
-
|
|
19997
|
-
// stopped, or started again by a callback this update, or during an update a callback ran inside it, which
|
|
19998
|
-
// counts on from this one
|
|
19999
|
-
if (!t.active || t.activePass >= pass) continue;
|
|
20000
|
-
let dt;
|
|
20001
|
-
if (enginePath)
|
|
20121
|
+
for (let i = list.length; i--;)
|
|
20002
20122
|
{
|
|
20003
|
-
|
|
20004
|
-
|
|
20005
|
-
|
|
20006
|
-
t.
|
|
20007
|
-
|
|
20008
|
-
|
|
20009
|
-
|
|
20010
|
-
|
|
20011
|
-
|
|
20123
|
+
const t = list[i];
|
|
20124
|
+
// stopped, or started again by a callback this update, or during an update a callback ran inside it, which
|
|
20125
|
+
// counts on from this one
|
|
20126
|
+
if (!t.active || t.activePass >= pass) continue;
|
|
20127
|
+
let dt;
|
|
20128
|
+
if (enginePath)
|
|
20129
|
+
{
|
|
20130
|
+
// a paused tween keeps count too, so it does not jump when resumed
|
|
20131
|
+
dt = t.useRealTime ? timeReal - t.lastTimeReal : time - t.lastTime;
|
|
20132
|
+
t.lastTime = time;
|
|
20133
|
+
t.lastTimeReal = timeReal;
|
|
20134
|
+
}
|
|
20135
|
+
else
|
|
20136
|
+
dt = t.useRealTime ? realDelta : gameDelta;
|
|
20137
|
+
if (t.target?.destroyed) { t.stop(); continue; } // its object is gone, paused or not
|
|
20138
|
+
if (t.paused || dt <= 0) continue;
|
|
20012
20139
|
|
|
20013
|
-
|
|
20014
|
-
|
|
20015
|
-
|
|
20016
|
-
|
|
20017
|
-
|
|
20018
|
-
|
|
20019
|
-
|
|
20020
|
-
|
|
20021
|
-
|
|
20022
|
-
|
|
20023
|
-
|
|
20024
|
-
|
|
20025
|
-
|
|
20026
|
-
|
|
20027
|
-
|
|
20028
|
-
|
|
20029
|
-
|
|
20140
|
+
t.life -= dt;
|
|
20141
|
+
if (t.life > 1e-9) // the engine's deltas add up a rounding error short of the duration
|
|
20142
|
+
{
|
|
20143
|
+
t.callback(t.interp(t.life));
|
|
20144
|
+
}
|
|
20145
|
+
else
|
|
20146
|
+
{
|
|
20147
|
+
// Completion: fire end value, remove from active, start the next iteration
|
|
20148
|
+
// of a loop or pingPong, or when there is none it has completed, fire onComplete
|
|
20149
|
+
t.callback(t.interp(0));
|
|
20150
|
+
if (!t.active || t.activePass >= pass)
|
|
20151
|
+
continue; // stopped or restarted by its own callback, the run it was on ends without completing
|
|
20152
|
+
tweenDeactivate(t);
|
|
20153
|
+
const next = t.thenCallback;
|
|
20154
|
+
t.thenCallback = undefined;
|
|
20155
|
+
if (!(next && next()) && t.onComplete)
|
|
20156
|
+
t.onComplete();
|
|
20157
|
+
}
|
|
20030
20158
|
}
|
|
20031
20159
|
}
|
|
20032
|
-
list.length = 0;
|
|
20160
|
+
finally { list.length = 0; }
|
|
20033
20161
|
}
|
|
20034
20162
|
|
|
20035
20163
|
/** Stop every active tween, ending loops too, without calling their then-callbacks.
|
|
@@ -20103,6 +20231,179 @@ function sceneUpdate() { paused || !timeScale || sceneCurrent?.update?.(); }
|
|
|
20103
20231
|
function sceneRender() { sceneCurrent?.render?.(); }
|
|
20104
20232
|
function sceneRenderPost() { sceneCurrent?.renderPost?.(); }
|
|
20105
20233
|
|
|
20234
|
+
/**
|
|
20235
|
+
* LittleJS Parallax Plugin
|
|
20236
|
+
* - A background layer that follows the camera by a part of its movement, so it looks far away
|
|
20237
|
+
* - Repeats across the view, so a level of any width has a background
|
|
20238
|
+
* - Draws mountains by default, or anything a game draws into it
|
|
20239
|
+
* @namespace Parallax
|
|
20240
|
+
*/
|
|
20241
|
+
|
|
20242
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
20243
|
+
|
|
20244
|
+
/**
|
|
20245
|
+
* A background layer with parallax: an image that follows the camera by a part of its movement and repeats across
|
|
20246
|
+
* the view, drawn once into a canvas by a function, mountains when none is given
|
|
20247
|
+
* - A far layer has a parallax near 1 and a low renderOrder, a near one a smaller parallax and a higher renderOrder
|
|
20248
|
+
* - The image should meet itself at its left and right edges to repeat without a seam, as the mountains do
|
|
20249
|
+
* @memberof Parallax
|
|
20250
|
+
* @extends CanvasLayer
|
|
20251
|
+
* @example
|
|
20252
|
+
* // three ranges of mountains, the far ones lighter and slower
|
|
20253
|
+
* for (let i = 3; i--;)
|
|
20254
|
+
* new ParallaxLayer(vec2(0, i*2), vec2(40, 20), .9 - i*.2, -1e3 + i,
|
|
20255
|
+
* parallaxMountains(hsl(.6, .3, .7 - i*.2), hsl(.6, .5, .2), i));
|
|
20256
|
+
*
|
|
20257
|
+
* // or draw your own image into the layer
|
|
20258
|
+
* new ParallaxLayer(vec2(), vec2(40, 20), .5, -1e3, (context, size)=>
|
|
20259
|
+
* {
|
|
20260
|
+
* context.fillStyle = '#fff';
|
|
20261
|
+
* context.fillRect(size.x/2 - 20, size.y/2 - 20, 40, 40);
|
|
20262
|
+
* });
|
|
20263
|
+
*/
|
|
20264
|
+
class ParallaxLayer extends CanvasLayer
|
|
20265
|
+
{
|
|
20266
|
+
/** Create a parallax layer and draw its image
|
|
20267
|
+
* @param {Vector2} [pos] - Where the middle of the image is in the world when the camera is there too
|
|
20268
|
+
* @param {Vector2} [size] - World size of one copy of the image
|
|
20269
|
+
* @param {number|Vector2} [parallax] - How much of the camera's movement it follows, 0 stays with the world
|
|
20270
|
+
* and 1 with the screen, a Vector2 to follow x and y by different amounts
|
|
20271
|
+
* @param {number} [renderOrder] - Low to draw behind the game, far layers lowest
|
|
20272
|
+
* @param {function(OffscreenCanvasRenderingContext2D, Vector2, ParallaxLayer): void} [drawFunction] - Draws
|
|
20273
|
+
* the image, given the canvas context, its size in pixels and the layer; mountains when not given
|
|
20274
|
+
* @param {Vector2} [canvasSize] - Size of the image in pixels */
|
|
20275
|
+
constructor(pos=vec2(), size=vec2(32, 16), parallax=.5, renderOrder=-1e3, drawFunction=parallaxMountains(),
|
|
20276
|
+
canvasSize=vec2(512, 256))
|
|
20277
|
+
{
|
|
20278
|
+
false&&ASSERT(isNumber(parallax) || isVector2(parallax), 'parallax must be a number or a Vector2');
|
|
20279
|
+
false&&ASSERT(typeof drawFunction == 'function', 'drawFunction must be a function');
|
|
20280
|
+
super(pos, size, 0, renderOrder, canvasSize);
|
|
20281
|
+
|
|
20282
|
+
/** @property {Vector2} - How much of the camera's movement it follows on each axis, 0 stays with the world
|
|
20283
|
+
* and 1 with the screen */
|
|
20284
|
+
this.parallax = isVector2(parallax) ? /** @type {Vector2} */ (parallax).copy() :
|
|
20285
|
+
vec2(/** @type {number} */ (parallax));
|
|
20286
|
+
/** @property {boolean} - Repeat the image across the view, left and right */
|
|
20287
|
+
this.wrapX = true;
|
|
20288
|
+
/** @property {boolean} - Repeat the image up and down the view */
|
|
20289
|
+
this.wrapY = false;
|
|
20290
|
+
/** @property {number} - How much the camera's zoom changes its size on the screen: 1 like the world, 0 not
|
|
20291
|
+
* at all, the same on the screen at any zoom as it is at zoomScale */
|
|
20292
|
+
this.zoomFollow = 1;
|
|
20293
|
+
/** @property {number} - The camera scale its size is given for, used when zoomFollow is under 1 */
|
|
20294
|
+
this.zoomScale = cameraScale;
|
|
20295
|
+
/** @property {function(OffscreenCanvasRenderingContext2D, Vector2, ParallaxLayer): void} - Draws the
|
|
20296
|
+
* image, redraw() after changing it */
|
|
20297
|
+
this.drawFunction = drawFunction;
|
|
20298
|
+
this.redraw();
|
|
20299
|
+
}
|
|
20300
|
+
|
|
20301
|
+
/** Draw the image again with drawFunction, after changing it or what it draws from */
|
|
20302
|
+
redraw()
|
|
20303
|
+
{
|
|
20304
|
+
const context = this.context;
|
|
20305
|
+
if (!context) return; // headless
|
|
20306
|
+
const size = vec2(this.canvas.width, this.canvas.height);
|
|
20307
|
+
context.clearRect(0, 0, size.x, size.y);
|
|
20308
|
+
this.drawFunction(context, size, this);
|
|
20309
|
+
this.updateWebGL();
|
|
20310
|
+
}
|
|
20311
|
+
|
|
20312
|
+
// how much smaller it is drawn than its size, from the camera's zoom and how much of it the layer follows
|
|
20313
|
+
zoomFactor()
|
|
20314
|
+
{ return this.zoomFollow < 1 && cameraScale > 0 ? (this.zoomScale / cameraScale) ** (1 - this.zoomFollow) : 1; }
|
|
20315
|
+
|
|
20316
|
+
/** World size one copy of the image is drawn at now, its size unless zoomFollow is under 1
|
|
20317
|
+
* @return {Vector2} */
|
|
20318
|
+
getDrawSize() { return this.size.scale(this.zoomFactor()); }
|
|
20319
|
+
|
|
20320
|
+
/** Where each copy of the image is drawn now, in the world: one place, or with wrapping every place that
|
|
20321
|
+
* shows in the view
|
|
20322
|
+
* @return {Array<Vector2>} */
|
|
20323
|
+
getDrawPositions()
|
|
20324
|
+
{
|
|
20325
|
+
// its place seen from the camera is cut down by the parallax, and by the zoom it does not follow
|
|
20326
|
+
const zoom = this.zoomFactor(), size = this.size.scale(zoom), p = this.parallax;
|
|
20327
|
+
const offset = this.pos.subtract(cameraPos);
|
|
20328
|
+
const center = cameraPos.add(vec2(offset.x * (1 - p.x) * zoom, offset.y * (1 - p.y) * zoom));
|
|
20329
|
+
|
|
20330
|
+
// the copies that reach into the view; a turned camera sees as far as its corner each way
|
|
20331
|
+
const view = mainCanvasSize.scale(.5 / (cameraScale || 1));
|
|
20332
|
+
const reach = cameraAngle ? vec2(view.length()) : view;
|
|
20333
|
+
const range = (wrap, axis)=>
|
|
20334
|
+
{
|
|
20335
|
+
if (!wrap || !(size[axis] > 0)) return [0, 0];
|
|
20336
|
+
const from = cameraPos[axis] - reach[axis] - size[axis] / 2 - center[axis];
|
|
20337
|
+
const to = cameraPos[axis] + reach[axis] + size[axis] / 2 - center[axis];
|
|
20338
|
+
const first = ceil(from / size[axis]);
|
|
20339
|
+
return [first, min(floor(to / size[axis]), first + 99)]; // a tiny image does not draw without end
|
|
20340
|
+
};
|
|
20341
|
+
const [x0, x1] = range(this.wrapX, 'x'), [y0, y1] = range(this.wrapY, 'y'), positions = [];
|
|
20342
|
+
for (let y = y0; y <= y1; ++y)
|
|
20343
|
+
for (let x = x0; x <= x1; ++x)
|
|
20344
|
+
positions.push(vec2(center.x + x * size.x, center.y + y * size.y));
|
|
20345
|
+
return positions;
|
|
20346
|
+
}
|
|
20347
|
+
|
|
20348
|
+
// Render the layer, called automatically by the engine
|
|
20349
|
+
render()
|
|
20350
|
+
{
|
|
20351
|
+
const size = this.getDrawSize();
|
|
20352
|
+
for (const pos of this.getDrawPositions())
|
|
20353
|
+
this.draw(pos, size, this.color, 0, this.mirror, this.additiveColor, false);
|
|
20354
|
+
}
|
|
20355
|
+
}
|
|
20356
|
+
|
|
20357
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
20358
|
+
|
|
20359
|
+
// the ridge of a range of mountains: a height for each column of an image and one more, the last the same as the
|
|
20360
|
+
// first so the image repeats with no step; it wanders about the middle, turning now and then
|
|
20361
|
+
function parallaxRidge(width, height, random)
|
|
20362
|
+
{
|
|
20363
|
+
const pointiness = .2, levelness = .005, slopeRange = height / 256; // how often it turns, how hard it is
|
|
20364
|
+
const ridge = new Float32Array(width + 1), middle = height / 2; // pulled level, how steep it gets
|
|
20365
|
+
let y = middle, slope = random.float(-slopeRange, slopeRange);
|
|
20366
|
+
for (let x = 0; x <= width; ++x)
|
|
20367
|
+
{
|
|
20368
|
+
ridge[x] = y;
|
|
20369
|
+
y += slope -= (y - middle) * levelness;
|
|
20370
|
+
if (random.float() < pointiness)
|
|
20371
|
+
slope = random.float(-slopeRange, slopeRange);
|
|
20372
|
+
}
|
|
20373
|
+
// take out the difference between its ends, a little from each column, and keep it inside the image
|
|
20374
|
+
const drift = ridge[width] - ridge[0];
|
|
20375
|
+
for (let x = 0; x <= width; ++x)
|
|
20376
|
+
ridge[x] = clamp(ridge[x] - drift * x / width, 0, height);
|
|
20377
|
+
return ridge;
|
|
20378
|
+
}
|
|
20379
|
+
|
|
20380
|
+
/** A draw function for a ParallaxLayer: a range of mountains across the image, shaded from the color of their
|
|
20381
|
+
* peaks down to the color of their feet, that repeats with no seam
|
|
20382
|
+
* @param {Color} [topColor] - Color at the peaks
|
|
20383
|
+
* @param {Color} [bottomColor] - Color at the bottom of the image
|
|
20384
|
+
* @param {number} [seed] - The same seed gives the same mountains, random when not given
|
|
20385
|
+
* @return {function(OffscreenCanvasRenderingContext2D, Vector2): void}
|
|
20386
|
+
* @memberof Parallax */
|
|
20387
|
+
function parallaxMountains(topColor=hsl(.6, .2, .6), bottomColor=hsl(.6, .3, .25), seed)
|
|
20388
|
+
{
|
|
20389
|
+
return (context, size)=>
|
|
20390
|
+
{
|
|
20391
|
+
const w = size.x, h = size.y;
|
|
20392
|
+
const random = new RandomGenerator(seed === undefined ? randInt(1e9) + 1 : seed * 7919 + 1);
|
|
20393
|
+
const ridge = parallaxRidge(w, h, random);
|
|
20394
|
+
|
|
20395
|
+
// a strip one pixel wide with the shading, stretched down each column from its peak
|
|
20396
|
+
const strip = createCanvasContext(1, h);
|
|
20397
|
+
for (let i = h; i--;)
|
|
20398
|
+
{
|
|
20399
|
+
strip.fillStyle = topColor.lerp(bottomColor, i / h).toString();
|
|
20400
|
+
strip.fillRect(0, i, 1, 1);
|
|
20401
|
+
}
|
|
20402
|
+
for (let x = w; x--;)
|
|
20403
|
+
ridge[x] < h && context.drawImage(strip.canvas, 0, 0, 1, h, x, ridge[x], 1, h - ridge[x]);
|
|
20404
|
+
};
|
|
20405
|
+
}
|
|
20406
|
+
|
|
20106
20407
|
/**
|
|
20107
20408
|
* LittleJS PathFinder Plugin
|
|
20108
20409
|
* - Grid-based A* pathfinder with two-pass smoothing for natural-looking paths
|
|
@@ -22238,17 +22539,19 @@ function render3DInstance(mesh, matrix, tileInfo, color)
|
|
|
22238
22539
|
// make room for one more instance of a mesh under a texture and the current draw state, flushing a batch that
|
|
22239
22540
|
// differs first, and return where its 24 floats go in mesh.instanceData: the matrix, the tint and the uv rect
|
|
22240
22541
|
// drawMesh passes whether its matrix mirrors, and its batch then culls by the mesh and winds by that at the flush,
|
|
22241
|
-
// so it never sets the draw state for them, which would move the state's version at every draw
|
|
22242
|
-
|
|
22542
|
+
// so it never sets the draw state for them, which would move the state's version at every draw; drawBillboard
|
|
22543
|
+
// passes that its batch is unlit, for the same reason
|
|
22544
|
+
function render3DInstanceSlot(mesh, textureInfo, mirrored, unlit=false)
|
|
22243
22545
|
{
|
|
22244
22546
|
const r = render3D;
|
|
22245
22547
|
if (mesh.instanceCount && (mesh.instanceTextureInfo !== textureInfo || mesh.instanceMirrored !== mirrored
|
|
22246
|
-
|| render3DStateChanged(mesh.instanceState)))
|
|
22548
|
+
|| mesh.instanceUnlit !== unlit || render3DStateChanged(mesh.instanceState)))
|
|
22247
22549
|
render3DFlushInstances(mesh);
|
|
22248
22550
|
if (!mesh.instanceCount)
|
|
22249
22551
|
{
|
|
22250
22552
|
mesh.instanceTextureInfo = textureInfo;
|
|
22251
22553
|
mesh.instanceMirrored = mirrored;
|
|
22554
|
+
mesh.instanceUnlit = unlit;
|
|
22252
22555
|
mesh.instanceState = render3DCaptureBatchState();
|
|
22253
22556
|
r.instanceMeshes.push(mesh);
|
|
22254
22557
|
}
|
|
@@ -22282,6 +22585,8 @@ function render3DFlushInstances(only)
|
|
|
22282
22585
|
const state = mesh.instanceState;
|
|
22283
22586
|
if (mesh.instanceMirrored !== undefined) // a drawMesh batch: culled by its mesh, wound by its matrices
|
|
22284
22587
|
state.cullBackFaces = !mesh.doubleSided, state.mirrored = mesh.instanceMirrored;
|
|
22588
|
+
if (mesh.instanceUnlit) // a batch of sprites
|
|
22589
|
+
state.lighting = false;
|
|
22285
22590
|
render3DDrawInstanced(mesh, buffer, count, mesh.instanceTextureInfo, state);
|
|
22286
22591
|
}
|
|
22287
22592
|
if (!only)
|
|
@@ -22369,12 +22674,17 @@ function render3DShadowCaster()
|
|
|
22369
22674
|
light.intensity > 0 && light.color.a > 0 ? light : undefined;
|
|
22370
22675
|
}
|
|
22371
22676
|
|
|
22677
|
+
// the widest cone a shadow map can look down, to each side; the light that casts the shadows lights no wider, or
|
|
22678
|
+
// what is past the map's edge would be lit with nothing to shade it
|
|
22679
|
+
const RENDER3D_SHADOW_CONE_MAX = 1.35;
|
|
22680
|
+
|
|
22372
22681
|
// a light's cone as the four numbers the shader gets: the way it shines, scaled so that its dot with the way to
|
|
22373
22682
|
// a point, less the fourth number, is 0 at the edge of the cone and 1 where its fade starts; a light with no cone
|
|
22374
22683
|
// gets numbers that make it 1 every way
|
|
22375
22684
|
function render3DLightCone(light)
|
|
22376
22685
|
{
|
|
22377
|
-
const
|
|
22686
|
+
const widest = light === render3DShadowCaster() ? RENDER3D_SHADOW_CONE_MAX : PI;
|
|
22687
|
+
const angle = light.directional ? 0 : min(light.coneAngle, widest);
|
|
22378
22688
|
if (!(angle > 0)) return [0, 0, 0, -1];
|
|
22379
22689
|
const outer = cos(angle), inner = cos(angle * (1 - clamp(light.coneSoftness)));
|
|
22380
22690
|
const k = 1 / max(inner - outer, 1e-4), forward = light.getForward3D();
|
|
@@ -22633,6 +22943,7 @@ class Render3DPlugin
|
|
|
22633
22943
|
/** @type {TextureInfo|undefined} */
|
|
22634
22944
|
this.streamTileInfo = undefined;
|
|
22635
22945
|
this.streamState = undefined; // captured state the pending batch was drawn under
|
|
22946
|
+
this.streamUnlit = false; // the pending batch is drawn unlit whatever that state says, billboards are
|
|
22636
22947
|
/** @type {Mesh|undefined} */
|
|
22637
22948
|
this.capture = undefined; // the mesh a bake is filling
|
|
22638
22949
|
/** @type {Array<{distance: number, state: Object, draw: function(): void}>|undefined} */
|
|
@@ -23023,6 +23334,8 @@ class Render3DPlugin
|
|
|
23023
23334
|
{
|
|
23024
23335
|
if (!this.streamCount || !render3DCanDraw()) return;
|
|
23025
23336
|
const gl = glContext;
|
|
23337
|
+
if (this.streamUnlit)
|
|
23338
|
+
this.streamState.lighting = false; // its own copy of the state
|
|
23026
23339
|
render3DSetDrawUniforms(RENDER3D_IDENTITY, this.streamTileInfo, WHITE, RENDER3D_FULL_UV_RECT, this.streamState);
|
|
23027
23340
|
render3DBindVertexBuffer(this.streamBuffer);
|
|
23028
23341
|
gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.streamFloats, 0, this.streamCount * RENDER3D_VERTEX_FLOATS);
|
|
@@ -23161,8 +23474,9 @@ class Render3DPlugin
|
|
|
23161
23474
|
// a spotlight's map looks down its cone with perspective, from just in front of it out to its radius
|
|
23162
23475
|
const pos = caster.getWorldPos3D(), forward = caster.getForward3D();
|
|
23163
23476
|
const up = abs(forward.y) > .99 ? vec3(0, 0, 1) : vec3(0, 1, 0);
|
|
23164
|
-
|
|
23165
|
-
const
|
|
23477
|
+
// the near plane stays in front of the far one however small the light
|
|
23478
|
+
const far = caster.radius, near = min(max(far / 500, .02), far / 2);
|
|
23479
|
+
const fov = min(caster.coneAngle, RENDER3D_SHADOW_CONE_MAX) * 2 + .1;
|
|
23166
23480
|
this.shadowMatrix = Matrix4.perspective(fov, 1, near, far).multiply(
|
|
23167
23481
|
Matrix4.lookAt(pos, pos.add(forward), up).invert());
|
|
23168
23482
|
this.shadowPlanes = render3DFrustumPlanes(this.shadowMatrix);
|
|
@@ -23256,13 +23570,34 @@ class Render3DPlugin
|
|
|
23256
23570
|
return this.queueTransparent(p, ()=> this.drawBillboard(p, s, tileInfo, c, angle, upright));
|
|
23257
23571
|
}
|
|
23258
23572
|
|
|
23259
|
-
//
|
|
23260
|
-
//
|
|
23261
|
-
const
|
|
23262
|
-
this.
|
|
23263
|
-
|
|
23264
|
-
|
|
23265
|
-
|
|
23573
|
+
// unlit on screen, in the shadow map the object's flag decides; the batch is told, the draw state is not
|
|
23574
|
+
// set, which would move its version at every sprite
|
|
23575
|
+
const unlit = !this.shadowPass;
|
|
23576
|
+
if (!this.blend && this.depthTest && this.instancing)
|
|
23577
|
+
{
|
|
23578
|
+
// an opaque sprite is one more instance of the shared quad, as a mesh's uses are: every sprite of a
|
|
23579
|
+
// sheet is one instanced draw at the end of the stage, with nothing written per corner; the shader
|
|
23580
|
+
// drops its see through texels, so it needs no sorting
|
|
23581
|
+
if (!render3DCanDraw() || this.shadowPass && !this.lighting) return; // unlit things cast no shadow
|
|
23582
|
+
if (this.frustumCulling && !render3DSphereVisible(pos.x, pos.y, pos.z, hypot(size.x, size.y) / 2))
|
|
23583
|
+
return;
|
|
23584
|
+
const quad = this.billboardMesh, cb = this.cameraBack, uv = render3DGetTileUVs(tileInfo);
|
|
23585
|
+
render3DMeshUpload(quad);
|
|
23586
|
+
// the quad's half axes, doubled for its unit square
|
|
23587
|
+
const a = render3DBillboardAxes(size, angle, upright);
|
|
23588
|
+
const k = render3DInstanceSlot(quad, render3DTextureOf(tileInfo), false, unlit), data = quad.instanceData;
|
|
23589
|
+
data[k] = a[0] * 2; data[k+1] = a[1] * 2; data[k+2] = a[2] * 2; data[k+3] = 0;
|
|
23590
|
+
data[k+4] = a[3] * 2; data[k+5] = a[4] * 2; data[k+6] = a[5] * 2; data[k+7] = 0;
|
|
23591
|
+
data[k+8] = cb.x; data[k+9] = cb.y; data[k+10] = cb.z; data[k+11] = 0;
|
|
23592
|
+
data[k+12] = pos.x; data[k+13] = pos.y; data[k+14] = pos.z; data[k+15] = 1;
|
|
23593
|
+
data[k+16] = color.r; data[k+17] = color.g; data[k+18] = color.b; data[k+19] = color.a;
|
|
23594
|
+
data[k+20] = uv.x; data[k+21] = uv.y; data[k+22] = uv.w; data[k+23] = uv.h;
|
|
23595
|
+
return;
|
|
23596
|
+
}
|
|
23597
|
+
|
|
23598
|
+
// one that blends is drawn in its sorted place: the quad's six stream vertices written straight in with no
|
|
23599
|
+
// vectors made, for sprites that blend, and for particles when instancing is off
|
|
23600
|
+
const uvRect = render3DBeginStrip(6, tileInfo, unlit);
|
|
23266
23601
|
if (!uvRect) return;
|
|
23267
23602
|
const a = render3DBillboardAxes(size, angle, upright);
|
|
23268
23603
|
const rx = a[0], ry = a[1], rz = a[2], ux = a[3], uy = a[4], uz = a[5];
|
|
@@ -24374,8 +24709,9 @@ function render3DRenderDepth()
|
|
|
24374
24709
|
// which way they face, so keeping the count even keeps every strip facing out.
|
|
24375
24710
|
|
|
24376
24711
|
// make room in the stream for a strip of count vertices under the current state and texture, flushing a batch that
|
|
24377
|
-
// differs first; returns the uv rect to map the vertices with, or undefined when nothing can be drawn
|
|
24378
|
-
|
|
24712
|
+
// differs first; returns the uv rect to map the vertices with, or undefined when nothing can be drawn; unlit draws
|
|
24713
|
+
// the batch unlit whatever the state says, so a billboard does not set the state and set it back at every draw
|
|
24714
|
+
function render3DBeginStrip(count, tileInfo, unlit=false)
|
|
24379
24715
|
{
|
|
24380
24716
|
const r = render3D;
|
|
24381
24717
|
if (!render3DCanDraw()) return;
|
|
@@ -24383,12 +24719,13 @@ function render3DBeginStrip(count, tileInfo)
|
|
|
24383
24719
|
false&&ASSERT(count <= RENDER3D_MAX_STREAM_VERTS, 'strip is too large for the stream, bake it into a mesh');
|
|
24384
24720
|
if (count > RENDER3D_MAX_STREAM_VERTS) return;
|
|
24385
24721
|
const textureInfo = render3DTextureOf(tileInfo);
|
|
24386
|
-
if (r.streamCount && (textureInfo !== r.streamTileInfo ||
|
|
24387
|
-
|| r.streamCount + count > RENDER3D_MAX_STREAM_VERTS))
|
|
24722
|
+
if (r.streamCount && (textureInfo !== r.streamTileInfo || r.streamUnlit !== unlit
|
|
24723
|
+
|| render3DStateChanged(r.streamState) || r.streamCount + count > RENDER3D_MAX_STREAM_VERTS))
|
|
24388
24724
|
r.flush();
|
|
24389
24725
|
render3DFlushBeforeOverlay();
|
|
24390
24726
|
if (!r.streamCount)
|
|
24391
24727
|
r.streamState = render3DCaptureBatchState();
|
|
24728
|
+
r.streamUnlit = unlit;
|
|
24392
24729
|
r.streamTileInfo = textureInfo;
|
|
24393
24730
|
return render3DGetTileUVs(tileInfo);
|
|
24394
24731
|
}
|
|
@@ -26596,7 +26933,7 @@ class InstancedMesh3D extends EngineObject3D
|
|
|
26596
26933
|
* - intensity multiplies the color, above 1 for a light brighter than white
|
|
26597
26934
|
* - radius is a world distance, so scale3D does not change it
|
|
26598
26935
|
* - An alpha, an intensity or a radius of 0 switches it off, and a light that is off takes none of those slots
|
|
26599
|
-
* - Draws nothing
|
|
26936
|
+
* - Draws nothing but its glow, when it has one; add a small emissive mesh if the lamp itself should be seen
|
|
26600
26937
|
* @extends EngineObject3D
|
|
26601
26938
|
* @memberof Render3D
|
|
26602
26939
|
* @example
|
|
@@ -26645,8 +26982,10 @@ class Light3D extends EngineObject3D
|
|
|
26645
26982
|
const r = render3D, c = this.color, pos = render3DObjectMatrix(this).getTranslation();
|
|
26646
26983
|
const toCamera = r.camera.pos.subtract(pos), distance = toCamera.length();
|
|
26647
26984
|
const at = distance ? pos.add(toCamera.scale(min(this.glow, distance) / 2 / distance)) : pos;
|
|
26648
|
-
const color = rgb(c.r, c.g, c.b, c.a * min(this.intensity, 1));
|
|
26649
|
-
|
|
26985
|
+
const color = rgb(c.r, c.g, c.b, c.a * min(this.intensity, 1)), texture = engineGlowTexture(this.glowFalloff);
|
|
26986
|
+
// a browser that can not make the glow's texture draws no glow, not a plain square
|
|
26987
|
+
if (texture || !glContext)
|
|
26988
|
+
r.drawBillboard(at, vec2(this.glow), texture, color);
|
|
26650
26989
|
}
|
|
26651
26990
|
}
|
|
26652
26991
|
|
|
@@ -26656,7 +26995,7 @@ class Light3D extends EngineObject3D
|
|
|
26656
26995
|
* - It shines from its position toward the origin, like a three.js DirectionalLight: only the direction to it
|
|
26657
26996
|
* counts, so moving it or its parent swings the light around; parent it to a sun in the sky and it follows
|
|
26658
26997
|
* - It cannot sit on the origin, since that leaves no direction
|
|
26659
|
-
* -
|
|
26998
|
+
* - It casts no shadow: the sun, render3D.sunDirection, does, or a spotlight set as render3D.shadowLight
|
|
26660
26999
|
* @extends Light3D
|
|
26661
27000
|
* @memberof Render3D
|
|
26662
27001
|
* @example
|
|
@@ -27733,6 +28072,9 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
27733
28072
|
/** @property {boolean} - Particles hit the level, the height maps and voxel maps, bouncing by restitution and
|
|
27734
28073
|
* sliding along by friction; off by default, it tests each particle's move against the level every frame */
|
|
27735
28074
|
this.collideLevel = false;
|
|
28075
|
+
/** @property {number} - How much a particle grips where it lands, 0 to 1: its speed along the surface is
|
|
28076
|
+
* cut by this much on each hit, on top of the friction, 1 stops it there */
|
|
28077
|
+
this.stick = 0;
|
|
27736
28078
|
/** @property {Particle3DCallback|undefined} - Called with each particle as it is made
|
|
27737
28079
|
* @type {Particle3DCallback|undefined} */
|
|
27738
28080
|
this.particleCreateCallback = undefined;
|
|
@@ -27752,7 +28094,7 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
27752
28094
|
/** @property {Vector3|undefined} - Where the emitter was at its last update, for when its parent is destroyed
|
|
27753
28095
|
* @type {Vector3|undefined} */
|
|
27754
28096
|
this.worldPos3D = undefined;
|
|
27755
|
-
this.emitTimeBuffer =
|
|
28097
|
+
this.emitTimeBuffer = 1; // the first particle comes at once, as the 2D emitter's does
|
|
27756
28098
|
}
|
|
27757
28099
|
|
|
27758
28100
|
/** Spawn new particles, move the live ones, and go away when done */
|
|
@@ -27769,9 +28111,10 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
27769
28111
|
{
|
|
27770
28112
|
// a rate of zero is an emitter fed by hand, and the global scale only quiets it,
|
|
27771
28113
|
// neither is a reason to stop counting down the emit time
|
|
27772
|
-
|
|
28114
|
+
const rate = this.emitRate * particleEmitRateScale;
|
|
28115
|
+
if (rate > 0 && rate < Infinity)
|
|
27773
28116
|
{
|
|
27774
|
-
this.emitTimeBuffer +=
|
|
28117
|
+
this.emitTimeBuffer += rate * timeDelta;
|
|
27775
28118
|
for (; this.emitTimeBuffer >= 1; --this.emitTimeBuffer)
|
|
27776
28119
|
this.emitParticle();
|
|
27777
28120
|
}
|
|
@@ -27811,7 +28154,9 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
27811
28154
|
const vz = data[k+5] = data[k+5] * damping + gravityZ;
|
|
27812
28155
|
data[k] += vx, data[k+1] += vy, data[k+2] += vz;
|
|
27813
28156
|
data[k+18] += data[k+19] *= angleDamping;
|
|
27814
|
-
|
|
28157
|
+
// true when its collide callback destroyed it, it has had its last callback but the destroy one
|
|
28158
|
+
const gone = collideLevel && this.particleCollide(k, data[k] - vx, data[k+1] - vy, data[k+2] - vz);
|
|
28159
|
+
collideLevel && reread();
|
|
27815
28160
|
const t = i * trailMax * 3;
|
|
27816
28161
|
if (trailMax)
|
|
27817
28162
|
{
|
|
@@ -27823,7 +28168,7 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
27823
28168
|
trail[j] = data[k], trail[j+1] = data[k+1], trail[j+2] = data[k+2];
|
|
27824
28169
|
data[k+20] = n + 1;
|
|
27825
28170
|
}
|
|
27826
|
-
updateCallback && (this.particleCall(updateCallback, k), reread());
|
|
28171
|
+
updateCallback && !gone && (this.particleCall(updateCallback, k), reread());
|
|
27827
28172
|
if ((data[k+17] += timeDelta) >= data[k+16])
|
|
27828
28173
|
{
|
|
27829
28174
|
// dead: the last particle takes its slot, trail and all, order does not matter
|
|
@@ -27925,13 +28270,14 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
27925
28270
|
out[k] = view.pos.x, out[k+1] = view.pos.y, out[k+2] = view.pos.z;
|
|
27926
28271
|
out[k+3] = view.velocity.x, out[k+4] = view.velocity.y, out[k+5] = view.velocity.z;
|
|
27927
28272
|
if (view.destroyed)
|
|
27928
|
-
out[k+17] = max(out[k+17], out[k+16]
|
|
28273
|
+
out[k+17] = max(out[k+17], out[k+16]); // its life lived: a step short of it can round to just under
|
|
27929
28274
|
return result;
|
|
27930
28275
|
}
|
|
27931
28276
|
|
|
27932
28277
|
// stop the particle at k where its last move went into the level, from where it was: back at the surface, a hair
|
|
27933
28278
|
// off it, its speed into it turned around by restitution and its speed along it kept by friction, unless the
|
|
27934
|
-
// collide callback lets it through; one that starts inside is let go, as a 2D one is
|
|
28279
|
+
// collide callback lets it through; one that starts inside is let go, as a 2D one is; true when the callback
|
|
28280
|
+
// destroyed it
|
|
27935
28281
|
particleCollide(k, x, y, z)
|
|
27936
28282
|
{
|
|
27937
28283
|
let data = this.particleData;
|
|
@@ -27944,18 +28290,21 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
27944
28290
|
if (h && (!hit || h.distance < hit.distance))
|
|
27945
28291
|
hit = h, level = l;
|
|
27946
28292
|
}
|
|
27947
|
-
if (!hit) return;
|
|
28293
|
+
if (!hit) return false;
|
|
27948
28294
|
const point = from.lerp(to, hit.distance);
|
|
27949
28295
|
const callback = this.particleCollideCallback;
|
|
27950
28296
|
if (callback && (!this.particleCall(callback, k, level, point.copy()) || this.particleView.destroyed))
|
|
27951
|
-
return; // let through, or destroyed by the callback
|
|
28297
|
+
return this.particleView.destroyed; // let through, or destroyed by the callback
|
|
27952
28298
|
data = this.particleData; // as the callback left it, an emit may have grown it
|
|
27953
28299
|
|
|
27954
28300
|
const n = hit.normal, v = vec3(data[k+3], data[k+4], data[k+5]), into = n.scale(v.dot(n));
|
|
27955
|
-
|
|
28301
|
+
// the larger friction of the two, as objects take it, then the emitter's own grip, which a level can not undo
|
|
28302
|
+
const restitution = max(this.restitution, level.restitution);
|
|
28303
|
+
const friction = max(this.friction, level.friction) * (1 - clamp(this.stick));
|
|
27956
28304
|
const out = v.subtract(into).scale(friction).subtract(into.scale(restitution)), p = point.add(n.scale(1e-3));
|
|
27957
28305
|
data[k] = p.x, data[k+1] = p.y, data[k+2] = p.z;
|
|
27958
28306
|
data[k+3] = out.x, data[k+4] = out.y, data[k+5] = out.z;
|
|
28307
|
+
return false;
|
|
27959
28308
|
}
|
|
27960
28309
|
|
|
27961
28310
|
/** Draw the particles, as flat squares or as streaks when trailTime is set
|
|
@@ -28219,6 +28568,11 @@ class LensFlare3D extends EngineObject3D
|
|
|
28219
28568
|
* height; shape is glow, disc or ring
|
|
28220
28569
|
* @type {Array<{at: number, size: number, color: Color, shape: string}>|undefined} */
|
|
28221
28570
|
this.elements = undefined;
|
|
28571
|
+
/** @property {Light3D|undefined} - A light the flare is of in place of the sun, a lamp or a spotlight: the
|
|
28572
|
+
* flare is at the light and in its color, smaller from farther than the light reaches, hidden by what is
|
|
28573
|
+
* in front of the light, and a spotlight's shows from inside its beam only
|
|
28574
|
+
* @type {Light3D|undefined} */
|
|
28575
|
+
this.light = undefined;
|
|
28222
28576
|
/** @property {boolean} - Fade out when something is between the camera and the sun */
|
|
28223
28577
|
this.occlusion = true;
|
|
28224
28578
|
/** @property {number} - Seconds the flare takes to fade out or in when the sun is hidden or shows again */
|
|
@@ -28257,13 +28611,30 @@ class LensFlare3D extends EngineObject3D
|
|
|
28257
28611
|
return this.made;
|
|
28258
28612
|
}
|
|
28259
28613
|
|
|
28260
|
-
|
|
28614
|
+
// what the flare is of, seen from the camera: the way to it, how far it is, Infinity for the sun, and a point
|
|
28615
|
+
// to find it on the screen by; undefined with no sun direction, or a light that is gone or at the camera
|
|
28616
|
+
flareSource()
|
|
28617
|
+
{
|
|
28618
|
+
const camera = render3D.camera.pos, light = this.light;
|
|
28619
|
+
if (!light)
|
|
28620
|
+
{
|
|
28621
|
+
const sun = render3D.sunDirection;
|
|
28622
|
+
if (!sun.lengthSquared()) return;
|
|
28623
|
+
const direction = sun.normalize();
|
|
28624
|
+
return {direction, distance: Infinity, pos: camera.add(direction.scale(100))};
|
|
28625
|
+
}
|
|
28626
|
+
if (light.destroyed) return;
|
|
28627
|
+
const pos = light.getWorldPos3D(), offset = pos.subtract(camera), distance = offset.length();
|
|
28628
|
+
return distance ? {direction: offset.scale(1 / distance), distance, pos} : undefined;
|
|
28629
|
+
}
|
|
28630
|
+
|
|
28631
|
+
/** Where the sun, or the flare's light, is on the screen, in pixels like mousePosScreen, undefined when it is
|
|
28632
|
+
* behind the camera
|
|
28261
28633
|
* @return {Vector2|undefined} */
|
|
28262
28634
|
getSunScreenPos()
|
|
28263
28635
|
{
|
|
28264
|
-
const
|
|
28265
|
-
|
|
28266
|
-
return render3D.worldToScreen(render3D.camera.pos.add(direction.normalize(100)));
|
|
28636
|
+
const source = this.flareSource();
|
|
28637
|
+
return source && render3D.worldToScreen(source.pos);
|
|
28267
28638
|
}
|
|
28268
28639
|
|
|
28269
28640
|
/** The parts of the flare as they are drawn now: each one's place on the screen, its size in pixels and its
|
|
@@ -28277,7 +28648,19 @@ class LensFlare3D extends EngineObject3D
|
|
|
28277
28648
|
const off = max(abs(sun.x - center.x) / center.x, abs(sun.y - center.y) / center.y);
|
|
28278
28649
|
const strength = this.visible * clamp((1.3 - off) / .5) * this.intensity;
|
|
28279
28650
|
if (!(strength > 0)) return [];
|
|
28280
|
-
|
|
28651
|
+
let tint = this.color.multiply(render3D.sunColor), height = mainCanvasSize.y * this.flareSize;
|
|
28652
|
+
const light = this.light, source = light && this.flareSource();
|
|
28653
|
+
if (light)
|
|
28654
|
+
{
|
|
28655
|
+
if (!source) return [];
|
|
28656
|
+
// a light's flare is the light's color, as bright as the light up to 1, and only from inside its cone
|
|
28657
|
+
const cone = render3DLightCone(light), d = source.direction;
|
|
28658
|
+
const inCone = clamp(-(cone[0] * d.x + cone[1] * d.y + cone[2] * d.z) - cone[3]);
|
|
28659
|
+
const c = light.color, bright = c.a * clamp(light.intensity) * inCone;
|
|
28660
|
+
if (!(bright > 0)) return [];
|
|
28661
|
+
tint = this.color.multiply(rgb(c.r, c.g, c.b, bright));
|
|
28662
|
+
height *= min(1, light.radius / source.distance); // smaller from farther than it reaches
|
|
28663
|
+
}
|
|
28281
28664
|
return this.getElements().map((e)=>
|
|
28282
28665
|
{
|
|
28283
28666
|
const c = e.color.multiply(tint);
|
|
@@ -28287,15 +28670,32 @@ class LensFlare3D extends EngineObject3D
|
|
|
28287
28670
|
});
|
|
28288
28671
|
}
|
|
28289
28672
|
|
|
28290
|
-
/** Is something between the camera and the sun: the level, or an object that is not see
|
|
28673
|
+
/** Is something between the camera and the sun, or the flare's light: the level, or an object that is not see
|
|
28674
|
+
* through
|
|
28291
28675
|
* @return {boolean} */
|
|
28292
28676
|
isSunHidden()
|
|
28293
28677
|
{
|
|
28294
|
-
const
|
|
28295
|
-
if (!
|
|
28296
|
-
const
|
|
28297
|
-
|
|
28298
|
-
|
|
28678
|
+
const source = this.flareSource();
|
|
28679
|
+
if (!source) return true;
|
|
28680
|
+
const ray = new Ray3D(render3D.camera.pos, source.direction), reach = source.distance;
|
|
28681
|
+
const blockers = engineObjects.filter((o)=> o !== this && o instanceof EngineObject3D && !o.destroyed &&
|
|
28682
|
+
!o.transparent && !o.additive);
|
|
28683
|
+
// a voxel map says block by block what is see through: glass, water and leaves let the sun by, and the
|
|
28684
|
+
// ray goes on to the blocks behind them
|
|
28685
|
+
const maps = /** @type {Array<VoxelMap>} */ (typeof VoxelMap == 'undefined' ? [] :
|
|
28686
|
+
blockers.filter((o)=> o instanceof VoxelMap));
|
|
28687
|
+
for (const map of maps)
|
|
28688
|
+
if (map.raycast(ray, reach, (type)=> !map.blockType(type).seeThrough))
|
|
28689
|
+
return true;
|
|
28690
|
+
// an object is hit by its box, and a box the camera is inside, a room, a wide floor or the player's own
|
|
28691
|
+
// body, says nothing of what is in the way: the ray the other way hits it too, and it is left out; so is
|
|
28692
|
+
// one the light is inside, its lamp
|
|
28693
|
+
const backward = ray.direction.scale(-1), back = new Ray3D(ray.origin, backward);
|
|
28694
|
+
const lamp = this.light && [new Ray3D(source.pos, ray.direction), new Ray3D(source.pos, backward)];
|
|
28695
|
+
const hits = (r, o)=> render3DRaycastObject(r, o) !== undefined;
|
|
28696
|
+
const around = (o)=> !(o instanceof HeightMap) && (hits(back, o) || lamp && hits(lamp[0], o) && hits(lamp[1], o));
|
|
28697
|
+
const hit = render3D.pick(ray, blockers.filter((o)=> !maps.some((map)=> map === o) && !around(o)));
|
|
28698
|
+
return !!hit && hit.distance < reach;
|
|
28299
28699
|
}
|
|
28300
28700
|
|
|
28301
28701
|
/** Ease visible toward whether the sun shows, called automatically each frame */
|
|
@@ -29425,7 +29825,10 @@ function level3DAddMesh(name, mesh, tileInfo, color=WHITE)
|
|
|
29425
29825
|
* returned, see level3DVoxelSetup for its sheet
|
|
29426
29826
|
* - A level may hold a terrain, in a terrain block: pos, its center, size, its size in the world along x and z,
|
|
29427
29827
|
* height, how tall a full height is, color, and heights, rows of 0 to 1 from -z to +z, each from -x to +x; it
|
|
29428
|
-
* is made a HeightMap, returned with what else was made
|
|
29828
|
+
* is made a HeightMap, returned with what else was made; paint, when it has it, colors its samples: colors, a
|
|
29829
|
+
* list, and cells, runs of a count and a color along the rows, 0 for the terrain's own and 1 the list's first
|
|
29830
|
+
* - A level may hold prefabs of its own, in a prefabs block, each by its name as level3DAddPrefab takes it; they
|
|
29831
|
+
* are added before its objects are made, one the game added itself keeps its place
|
|
29429
29832
|
* - A level may set the scene too, in a scene block beside its objects: sky, three colors for straight up, the
|
|
29430
29833
|
* horizon and straight down, ambient, how much of them lights the scene, .5 when not given, sunDirection and
|
|
29431
29834
|
* sunColor, fog, its start and end, fogColor, the horizon color when not given, and shadows; what the block
|
|
@@ -29438,6 +29841,7 @@ function level3DLoad(level)
|
|
|
29438
29841
|
false&&ASSERT(!!level && typeof level === 'object', 'a level is an object, {} for a new one');
|
|
29439
29842
|
false&&ASSERT(!(level.littlejs3D > LEVEL3D_VERSION), 'the level was made by a newer LittleJS');
|
|
29440
29843
|
editor3DLevelLoaded(level); // debug builds: the 3D editor takes the level, its autosaved edits go in first
|
|
29844
|
+
level3DPrefabsAdd(level.prefabs);
|
|
29441
29845
|
level3DSceneApply(level.scene);
|
|
29442
29846
|
const made = [], map = level3DVoxelMap = level3DVoxelsMake(level.voxels);
|
|
29443
29847
|
map && made.push(map);
|
|
@@ -29459,7 +29863,8 @@ let level3DVoxelTiles, level3DVoxelSetupMap, level3DVoxelMap;
|
|
|
29459
29863
|
|
|
29460
29864
|
/** How a level's block map is made: the sheet its blocks show tiles of, and a function to set it up
|
|
29461
29865
|
* - A level's voxels block makes a VoxelMap when the level loads, with texture 0 and the default tile size unless
|
|
29462
|
-
* a sheet is given here; a block's type shows that tile of the sheet on every face
|
|
29866
|
+
* a sheet is given here; a block's type shows that tile of the sheet on every face; a game that has loaded no
|
|
29867
|
+
* image gets plain blocks, a color for each type
|
|
29463
29868
|
* - setup is called with each map a level makes, to give block types their own faces or make them see-through
|
|
29464
29869
|
* - Call it before level3DLoad; with no arguments the defaults are back
|
|
29465
29870
|
* @param {TileInfo} [tileInfo] - The sheet's first tile, as for a VoxelMap
|
|
@@ -29521,7 +29926,7 @@ function level3DVoxelsMake(voxels)
|
|
|
29521
29926
|
{
|
|
29522
29927
|
const shape = level3DVoxelsShape(voxels);
|
|
29523
29928
|
if (!shape || typeof VoxelMap == 'undefined') return;
|
|
29524
|
-
const map = new VoxelMap(shape.pos, shape.size, level3DVoxelTiles ||
|
|
29929
|
+
const map = new VoxelMap(shape.pos, shape.size, level3DVoxelTiles || level3DVoxelPlainTiles());
|
|
29525
29930
|
level3DVoxelsDecode(voxels.blocks, map.data);
|
|
29526
29931
|
level3DVoxelSetupMap?.(map);
|
|
29527
29932
|
map.rebuild();
|
|
@@ -29548,14 +29953,74 @@ function level3DTerrainShape(terrain)
|
|
|
29548
29953
|
height: isNumber(terrain.height) && terrain.height > 0 ? terrain.height : 1, heights};
|
|
29549
29954
|
}
|
|
29550
29955
|
|
|
29956
|
+
// a hex color of a level as a Color, undefined when it is not one
|
|
29957
|
+
function level3DHexColor(hex)
|
|
29958
|
+
{ return /^#([0-9a-f]{6}|[0-9a-f]{8})$/i.test(hex) ? new Color().setHex(hex) : undefined; }
|
|
29959
|
+
|
|
29960
|
+
// the paint of a level's terrain block, for a terrain of so many samples: its colors as they are written and a
|
|
29961
|
+
// color for each sample, row after row, 0 for none and 1 the first of the colors; undefined when it has none or
|
|
29962
|
+
// it does not fit the terrain
|
|
29963
|
+
function level3DTerrainPaint(terrain, samples)
|
|
29964
|
+
{
|
|
29965
|
+
const paint = terrain?.paint, colors = paint?.colors, runs = paint?.cells;
|
|
29966
|
+
if (!isArray(colors) || !isArray(runs) || colors.length > 255 || !colors.every(level3DHexColor)) return;
|
|
29967
|
+
const cells = new Uint8Array(samples);
|
|
29968
|
+
let at = 0;
|
|
29969
|
+
for (let i = 0; i < runs.length; i += 2)
|
|
29970
|
+
{
|
|
29971
|
+
const count = runs[i], color = runs[i + 1];
|
|
29972
|
+
if (!(count > 0 && count % 1 === 0 && color >= 0 && color % 1 === 0) || color > colors.length ||
|
|
29973
|
+
at + count > samples) return;
|
|
29974
|
+
cells.fill(color, at, at += count);
|
|
29975
|
+
}
|
|
29976
|
+
return at === samples ? {colors: colors.slice(), cells} : undefined;
|
|
29977
|
+
}
|
|
29978
|
+
|
|
29979
|
+
// give a terrain's map the colors of its block: its own color over the whole of it, or with paint a color for
|
|
29980
|
+
// each sample, the terrain's own where it is not painted; the caller builds the map again
|
|
29981
|
+
function level3DTerrainSetColors(map, terrain, paint=level3DTerrainPaint(terrain, map.rows * map.columns))
|
|
29982
|
+
{
|
|
29983
|
+
const base = level3DHexColor(terrain.color) || WHITE;
|
|
29984
|
+
map.color = base, map.colors = undefined;
|
|
29985
|
+
if (!paint || !paint.cells.some((v)=> v)) return;
|
|
29986
|
+
const palette = [base, ...paint.colors.map(level3DHexColor)];
|
|
29987
|
+
map.color = WHITE;
|
|
29988
|
+
map.colors = map.heights.map((row, r)=> row.map((v, c)=> palette[paint.cells[r * map.columns + c]] || base));
|
|
29989
|
+
}
|
|
29990
|
+
|
|
29991
|
+
// the sheet a level's blocks show when the game gave none with level3DVoxelSetup: the game's first image, as a
|
|
29992
|
+
// VoxelMap takes it, or, for a game that has loaded no image, plain tiles made here, a color for each type, so a
|
|
29993
|
+
// block map in a game with no art can still be seen and painted
|
|
29994
|
+
let level3DVoxelPlain;
|
|
29995
|
+
function level3DVoxelPlainTiles()
|
|
29996
|
+
{
|
|
29997
|
+
if (textureInfos[0]?.size.x || headlessMode || typeof OffscreenCanvas == 'undefined')
|
|
29998
|
+
return tile();
|
|
29999
|
+
if (!level3DVoxelPlain)
|
|
30000
|
+
{
|
|
30001
|
+
// 16 by 16 tiles of 16 pixels, each a color of its own with a darker edge, so the blocks read as blocks
|
|
30002
|
+
const context = createCanvasContext(256);
|
|
30003
|
+
for (let i = 0; i < 256; ++i)
|
|
30004
|
+
{
|
|
30005
|
+
const x = i % 16 * 16, y = (i / 16 | 0) * 16, hue = i * .618 % 1;
|
|
30006
|
+
context.fillStyle = hsl(hue, .45, .4).toString();
|
|
30007
|
+
context.fillRect(x, y, 16, 16);
|
|
30008
|
+
context.fillStyle = hsl(hue, .5, .55).toString();
|
|
30009
|
+
context.fillRect(x + 1, y + 1, 14, 14);
|
|
30010
|
+
}
|
|
30011
|
+
level3DVoxelPlain = new TextureInfo(context.canvas);
|
|
30012
|
+
}
|
|
30013
|
+
return tile(0, 16, level3DVoxelPlain, 0);
|
|
30014
|
+
}
|
|
30015
|
+
|
|
29551
30016
|
// the HeightMap of a level's terrain block, undefined when it has none or it is wrong
|
|
29552
30017
|
function level3DTerrainMake(terrain)
|
|
29553
30018
|
{
|
|
29554
30019
|
const shape = level3DTerrainShape(terrain);
|
|
29555
30020
|
if (!shape || typeof HeightMap == 'undefined') return;
|
|
29556
30021
|
const map = new HeightMap(shape.heights, shape.size, shape.height, undefined, shape.pos);
|
|
29557
|
-
|
|
29558
|
-
|
|
30022
|
+
level3DTerrainSetColors(map, terrain);
|
|
30023
|
+
map.colors && map.rebuild();
|
|
29559
30024
|
return map;
|
|
29560
30025
|
}
|
|
29561
30026
|
|
|
@@ -29628,6 +30093,14 @@ function level3DProperties(type, object)
|
|
|
29628
30093
|
// make one object of a level from the type added for its name, undefined when there is no such type or it made
|
|
29629
30094
|
// nothing
|
|
29630
30095
|
function level3DMake(object)
|
|
30096
|
+
{
|
|
30097
|
+
return level3DMakeAt(object, level3DVector(object.pos, vec3()),
|
|
30098
|
+
level3DVector(object.rotation, vec3()).scale(PI / 180), level3DVector(object.scale, vec3(1)));
|
|
30099
|
+
}
|
|
30100
|
+
|
|
30101
|
+
// make a level object's game object at a place, a rotation in radians and a scale, its own as level3DMake gives
|
|
30102
|
+
// them or the ones a prefab's instance puts it at
|
|
30103
|
+
function level3DMakeAt(object, pos, rotation, scale)
|
|
29631
30104
|
{
|
|
29632
30105
|
const type = level3DTypes.get(object.type);
|
|
29633
30106
|
if (!type)
|
|
@@ -29635,7 +30108,7 @@ function level3DMake(object)
|
|
|
29635
30108
|
debug && console.warn(`level3DLoad: no type added for ${object.type}, skipped`);
|
|
29636
30109
|
return;
|
|
29637
30110
|
}
|
|
29638
|
-
const
|
|
30111
|
+
const properties = level3DProperties(type, object);
|
|
29639
30112
|
const {make} = type, count = engineObjects.length;
|
|
29640
30113
|
let result;
|
|
29641
30114
|
try { result = make.prototype ? new make(pos, properties) : make(pos, properties); }
|
|
@@ -29653,16 +30126,263 @@ function level3DMake(object)
|
|
|
29653
30126
|
if (result instanceof EngineObject3D)
|
|
29654
30127
|
{
|
|
29655
30128
|
level3DBaseScale.set(result, result.scale3D.copy());
|
|
29656
|
-
result.rotation3D =
|
|
29657
|
-
result.scale3D = result.scale3D.multiply(
|
|
30129
|
+
result.rotation3D = rotation.copy();
|
|
30130
|
+
result.scale3D = result.scale3D.multiply(scale);
|
|
29658
30131
|
}
|
|
29659
30132
|
|
|
29660
30133
|
// the type's own properties are set on it, one the type has no default for could be a field of the engine's
|
|
29661
30134
|
for (const key in type.defaults)
|
|
29662
30135
|
result[key] = properties[key];
|
|
30136
|
+
// a prefab's instance makes its parts now that it is where it goes
|
|
30137
|
+
result instanceof Prefab3D && result.placeAt(pos);
|
|
29663
30138
|
return result;
|
|
29664
30139
|
}
|
|
29665
30140
|
|
|
30141
|
+
/** Make one object of a type added with level3DAddType, level3DAddMesh or level3DAddPrefab, from code, with no
|
|
30142
|
+
* level: a prefab's instance, or a plain type
|
|
30143
|
+
* @param {string} type - The type's name
|
|
30144
|
+
* @param {Vector3} [pos3D]
|
|
30145
|
+
* @param {Vector3} [rotation3D] - In radians, as an object has it
|
|
30146
|
+
* @param {Vector3} [scale3D] - Times the scale the type makes it with
|
|
30147
|
+
* @param {Object} [properties] - Over the type's defaults
|
|
30148
|
+
* @return {any} - What the type made, a Prefab3D for a prefab, undefined when there is no such type
|
|
30149
|
+
* @memberof Level3D
|
|
30150
|
+
* @example
|
|
30151
|
+
* level3DAddPrefab('House', await fetchJSON('house.json'));
|
|
30152
|
+
* level3DSpawn('House', vec3(10, 0, 0), vec3(0, PI/2, 0)); */
|
|
30153
|
+
function level3DSpawn(type, pos3D=vec3(), rotation3D=vec3(), scale3D=vec3(1), properties)
|
|
30154
|
+
{
|
|
30155
|
+
false&&ASSERT(isVector3(pos3D) && isVector3(rotation3D) && isVector3(scale3D), 'pos3D, rotation3D and scale3D are vec3');
|
|
30156
|
+
return level3DMakeAt(properties ? {type, properties} : {type}, pos3D.copy(), rotation3D, scale3D);
|
|
30157
|
+
}
|
|
30158
|
+
|
|
30159
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
30160
|
+
// prefabs: a prefab is a small level, a list of objects about its own origin, placed many times
|
|
30161
|
+
|
|
30162
|
+
// the prefabs by name: how an instance is built, its objects, fromLevel, true for one a level's own prefabs
|
|
30163
|
+
// block added, which one the game adds takes the place of, and source, the object it was added from, which the
|
|
30164
|
+
// level editor knows a level that is a prefab by
|
|
30165
|
+
/** @type {Map<string, {attached: boolean, objects: Array<Object>, fromLevel: boolean, source: Object}>} */
|
|
30166
|
+
const level3DPrefabs = new Map;
|
|
30167
|
+
|
|
30168
|
+
// how many prefabs deep an instance being made is, a prefab that holds itself is stopped; and how many of those
|
|
30169
|
+
// are attached: a prefab inside an attached one is attached too, or its parts would be left behind in the world
|
|
30170
|
+
let level3DPrefabDepth = 0, level3DPrefabAttached = 0;
|
|
30171
|
+
|
|
30172
|
+
/** Add a prefab: a small level, its objects placed about its own origin, to place many times under one name
|
|
30173
|
+
* - It is a type from then on: a level's object of that type, the level editor's Place list and level3DSpawn make
|
|
30174
|
+
* an instance, a Prefab3D; an instance is made of the prefab as it is then, so the level editor, where a prefab
|
|
30175
|
+
* changes, makes its instances again
|
|
30176
|
+
* - The prefab is a level as the level editor saves it, {objects: [...]}, so the editor is the prefab editor too;
|
|
30177
|
+
* only its objects are used, and they may be of other prefabs
|
|
30178
|
+
* - With attached true in it the parts are children of the instance and move with it as one body, without
|
|
30179
|
+
* collision of their own, a prefab inside it too; its handle is at the middle of the box around them and its
|
|
30180
|
+
* size3D is that box, so setCollision makes the body solid where it is seen; otherwise each part is an object of its own in the world and collides as one placed
|
|
30181
|
+
* by hand does
|
|
30182
|
+
* - Adding a name again replaces it
|
|
30183
|
+
* @param {string} name - The type its instances have in a level
|
|
30184
|
+
* @param {Object} prefab - {objects, attached}
|
|
30185
|
+
* @memberof Level3D
|
|
30186
|
+
* @example
|
|
30187
|
+
* level3DAddPrefab('Tower', {objects: [{type: 'Box', pos: [0, 1, 0], scale: [2, 2, 2]},
|
|
30188
|
+
* {type: 'Cylinder', pos: [0, 3, 0]}]});
|
|
30189
|
+
* level3DLoad({objects: [{type: 'Tower', pos: [5, 0, 5]}, {type: 'Tower', pos: [-5, 0, 5], rotation: [0, 45, 0]}]}); */
|
|
30190
|
+
function level3DAddPrefab(name, prefab) { level3DPrefabSet(name, prefab, false); }
|
|
30191
|
+
|
|
30192
|
+
// add a prefab, the game's or a level's own
|
|
30193
|
+
function level3DPrefabSet(name, prefab, fromLevel)
|
|
30194
|
+
{
|
|
30195
|
+
false&&ASSERT(!!prefab && typeof prefab === 'object', 'a prefab is an object, {objects: [...]}');
|
|
30196
|
+
name = String(name);
|
|
30197
|
+
const objects = isArray(prefab?.objects) ? prefab.objects.filter((o)=> o && typeof o === 'object') : [];
|
|
30198
|
+
// a copy, what the game or the editor does to its own afterwards does not change the instances made later
|
|
30199
|
+
level3DPrefabs.set(name, {attached: !!prefab?.attached, objects: JSON.parse(JSON.stringify(objects)), fromLevel,
|
|
30200
|
+
source: prefab});
|
|
30201
|
+
// a class of its own, so the level editor makes its instances again as it does a class's
|
|
30202
|
+
level3DTypes.set(name, {make: class extends Prefab3D { constructor(pos) { super(pos, name); } }, defaults: {},
|
|
30203
|
+
tileInfo: undefined});
|
|
30204
|
+
}
|
|
30205
|
+
|
|
30206
|
+
// add a level's own prefabs, each unless the game added one of that name or it is a plain type's name
|
|
30207
|
+
function level3DPrefabsAdd(prefabs)
|
|
30208
|
+
{
|
|
30209
|
+
if (!prefabs || typeof prefabs !== 'object' || isArray(prefabs)) return;
|
|
30210
|
+
for (const name in prefabs)
|
|
30211
|
+
{
|
|
30212
|
+
const known = level3DPrefabs.get(name);
|
|
30213
|
+
if (known ? !known.fromLevel : level3DTypes.has(name)) continue;
|
|
30214
|
+
prefabs[name] && typeof prefabs[name] === 'object' && level3DPrefabSet(name, prefabs[name], true);
|
|
30215
|
+
}
|
|
30216
|
+
}
|
|
30217
|
+
|
|
30218
|
+
/** Load a prefab from a file the level editor saved and add it
|
|
30219
|
+
* @param {string} name - The type its instances have in a level
|
|
30220
|
+
* @param {string} url
|
|
30221
|
+
* @return {Promise<void>}
|
|
30222
|
+
* @memberof Level3D */
|
|
30223
|
+
async function level3DLoadPrefab(name, url) { level3DAddPrefab(name, await fetchJSON(url)); }
|
|
30224
|
+
|
|
30225
|
+
// where an instance puts one of its prefab's objects: the object's own place, turn and size inside the instance's
|
|
30226
|
+
function level3DPrefabPartTransform(instance, object)
|
|
30227
|
+
{
|
|
30228
|
+
const pos = level3DVector(object.pos, vec3()), scale = level3DVector(object.scale, vec3(1));
|
|
30229
|
+
const rotation = level3DVector(object.rotation, vec3()).scale(PI / 180);
|
|
30230
|
+
// the two turns one after the other; the sizes multiply along each axis, which is exact for an instance sized
|
|
30231
|
+
// evenly or a part that is not turned, and as near as a box can be otherwise
|
|
30232
|
+
const turn = buildMatrix(vec3(), instance.rotation3D).multiply(buildMatrix(vec3(), rotation));
|
|
30233
|
+
return {pos: buildMatrix(instance.pos3D, instance.rotation3D, instance.scale3D).transformPoint(pos),
|
|
30234
|
+
rotation: turn.getRotation(), scale: scale.multiply(instance.scale3D)};
|
|
30235
|
+
}
|
|
30236
|
+
|
|
30237
|
+
/**
|
|
30238
|
+
* An instance of a prefab, what a prefab's type makes: a handle with no shape of its own, and its parts, what the
|
|
30239
|
+
* prefab's objects made
|
|
30240
|
+
* - Parts of a prefab that is not attached are objects of their own in the world: moving, turning or sizing the
|
|
30241
|
+
* handle puts them where it now says with its next update, or call placeParts; destroying it destroys them
|
|
30242
|
+
* - Parts of an attached prefab are its children and move with it as one body
|
|
30243
|
+
* @extends EngineObject3D
|
|
30244
|
+
* @memberof Level3D
|
|
30245
|
+
*/
|
|
30246
|
+
class Prefab3D extends EngineObject3D
|
|
30247
|
+
{
|
|
30248
|
+
/** Create an instance of a prefab, made by its type: place one with a level or level3DSpawn
|
|
30249
|
+
* @param {Vector3} [pos3D]
|
|
30250
|
+
* @param {string} [prefabName] - A prefab added with level3DAddPrefab */
|
|
30251
|
+
constructor(pos3D, prefabName='')
|
|
30252
|
+
{
|
|
30253
|
+
super(pos3D);
|
|
30254
|
+
/** @property {string} - The prefab it is an instance of */
|
|
30255
|
+
this.prefabName = prefabName;
|
|
30256
|
+
/** @property {boolean} - Are its parts its children, moving with it as one body */
|
|
30257
|
+
this.attached = !!level3DPrefabs.get(prefabName)?.attached || level3DPrefabAttached > 0;
|
|
30258
|
+
/** @property {Array<any>} - What the prefab's objects made, in the prefab's order */
|
|
30259
|
+
this.parts = [];
|
|
30260
|
+
/** @property {Vector3} - From the prefab's origin to the handle, in the prefab's own space: nothing for
|
|
30261
|
+
* separate parts, and for an attached prefab the middle of the box around its parts, where its handle
|
|
30262
|
+
* is, as an object's place is the middle of its body */
|
|
30263
|
+
this.originOffset = vec3();
|
|
30264
|
+
this.partObjects = []; // the prefab's object of each part
|
|
30265
|
+
this.partsPlaced = ''; // where the handle was when its parts were last placed, undefined parts not made
|
|
30266
|
+
this.partsMade = false;
|
|
30267
|
+
}
|
|
30268
|
+
|
|
30269
|
+
/** Put the instance with its prefab's origin at a place, turned and sized as the handle is: where a level's
|
|
30270
|
+
* object or level3DSpawn says it goes; the handle of an attached prefab is then at the middle of its body
|
|
30271
|
+
* @param {Vector3} pos3D */
|
|
30272
|
+
placeAt(pos3D)
|
|
30273
|
+
{
|
|
30274
|
+
this.pos3D = pos3D.copy();
|
|
30275
|
+
this.placeParts();
|
|
30276
|
+
const o = this.originOffset;
|
|
30277
|
+
if (o.x || o.y || o.z)
|
|
30278
|
+
this.pos3D = buildMatrix(pos3D, this.rotation3D, this.scale3D).transformPoint(o);
|
|
30279
|
+
}
|
|
30280
|
+
|
|
30281
|
+
/** Put the parts where the handle is now, making them the first time; called by the handle's update when it
|
|
30282
|
+
* has moved, turned or changed size */
|
|
30283
|
+
placeParts()
|
|
30284
|
+
{
|
|
30285
|
+
if (this.destroyed) return;
|
|
30286
|
+
this.partsMade ||= (this.makeParts(), true);
|
|
30287
|
+
this.partsPlaced = this.placeKey();
|
|
30288
|
+
if (this.attached) return; // children follow by themselves
|
|
30289
|
+
this.parts.forEach((part, i)=>
|
|
30290
|
+
{
|
|
30291
|
+
if (!(part instanceof EngineObject3D)) return; // what a function made is where it was made
|
|
30292
|
+
const to = level3DPrefabPartTransform(this, this.partObjects[i]);
|
|
30293
|
+
part.rotation3D = to.rotation;
|
|
30294
|
+
part.scale3D = (level3DBaseScale.get(part) ?? vec3(1)).multiply(to.scale);
|
|
30295
|
+
// a prefab inside this one goes by its origin, which is not where an attached one's handle is
|
|
30296
|
+
part instanceof Prefab3D ? part.placeAt(to.pos) : part.pos3D = to.pos;
|
|
30297
|
+
});
|
|
30298
|
+
}
|
|
30299
|
+
|
|
30300
|
+
// the handle's place, turn and size as one text, to tell when it has changed
|
|
30301
|
+
placeKey()
|
|
30302
|
+
{
|
|
30303
|
+
const p = this.pos3D, r = this.rotation3D, s = this.scale3D;
|
|
30304
|
+
return [p.x, p.y, p.z, r.x, r.y, r.z, s.x, s.y, s.z].join();
|
|
30305
|
+
}
|
|
30306
|
+
|
|
30307
|
+
// make the parts, each where the instance puts it, or attached at its own place inside the instance
|
|
30308
|
+
makeParts()
|
|
30309
|
+
{
|
|
30310
|
+
const prefab = level3DPrefabs.get(this.prefabName);
|
|
30311
|
+
if (!prefab) return;
|
|
30312
|
+
if (level3DPrefabDepth >= 8)
|
|
30313
|
+
{
|
|
30314
|
+
debug && console.error(`level3DLoad: the prefab ${this.prefabName} holds itself, left out there`);
|
|
30315
|
+
return;
|
|
30316
|
+
}
|
|
30317
|
+
++level3DPrefabDepth;
|
|
30318
|
+
this.attached && ++level3DPrefabAttached;
|
|
30319
|
+
try
|
|
30320
|
+
{
|
|
30321
|
+
const low = vec3(Infinity), high = vec3(-Infinity), children = [];
|
|
30322
|
+
for (const object of prefab.objects)
|
|
30323
|
+
{
|
|
30324
|
+
const local = {pos: level3DVector(object.pos, vec3()), scale: level3DVector(object.scale, vec3(1)),
|
|
30325
|
+
rotation: level3DVector(object.rotation, vec3()).scale(PI / 180)};
|
|
30326
|
+
const at = this.attached ? local : level3DPrefabPartTransform(this, object);
|
|
30327
|
+
const part = level3DMakeAt(object, at.pos, at.rotation, at.scale);
|
|
30328
|
+
if (!part) continue; // level3DMakeAt gives an object or nothing
|
|
30329
|
+
this.parts.push(part);
|
|
30330
|
+
this.partObjects.push(object);
|
|
30331
|
+
if (!this.attached || !(part instanceof EngineObject3D)) continue;
|
|
30332
|
+
// a child rides with its parent and has no collision of its own
|
|
30333
|
+
part.setCollision(false, false, false);
|
|
30334
|
+
children.push(part);
|
|
30335
|
+
// the corners of its box as it is turned; its place is its middle, a prefab inside this one too
|
|
30336
|
+
const box = buildMatrix(part.pos3D, part.rotation3D, part.size3D.multiply(part.scale3D));
|
|
30337
|
+
for (let i = 8; i--;)
|
|
30338
|
+
{
|
|
30339
|
+
const p = box.transformPoint(vec3(i & 1 ? .5 : -.5, i & 2 ? .5 : -.5, i & 4 ? .5 : -.5));
|
|
30340
|
+
for (const k of ['x', 'y', 'z'])
|
|
30341
|
+
low[k] = min(low[k], p[k]), high[k] = max(high[k], p[k]);
|
|
30342
|
+
}
|
|
30343
|
+
}
|
|
30344
|
+
// an attached instance is the box around its parts, its handle at the middle of it as an object's
|
|
30345
|
+
// place is the middle of its body, so a game that makes it solid has it solid where it is seen
|
|
30346
|
+
if (children.length)
|
|
30347
|
+
{
|
|
30348
|
+
this.size3D = high.subtract(low);
|
|
30349
|
+
this.originOffset = low.add(high).scale(.5);
|
|
30350
|
+
}
|
|
30351
|
+
for (const part of children)
|
|
30352
|
+
{
|
|
30353
|
+
part.pos3D = part.pos3D.subtract(this.originOffset);
|
|
30354
|
+
this.addChild(part);
|
|
30355
|
+
}
|
|
30356
|
+
}
|
|
30357
|
+
finally
|
|
30358
|
+
{
|
|
30359
|
+
--level3DPrefabDepth;
|
|
30360
|
+
this.attached && --level3DPrefabAttached;
|
|
30361
|
+
}
|
|
30362
|
+
}
|
|
30363
|
+
|
|
30364
|
+
/** Keep the parts with the handle, called automatically each frame */
|
|
30365
|
+
update()
|
|
30366
|
+
{
|
|
30367
|
+
super.update();
|
|
30368
|
+
if (!this.attached && this.partsPlaced !== this.placeKey())
|
|
30369
|
+
this.placeParts();
|
|
30370
|
+
}
|
|
30371
|
+
|
|
30372
|
+
/** Destroy the instance and its parts
|
|
30373
|
+
* @param {boolean} [immediate] */
|
|
30374
|
+
destroy(immediate)
|
|
30375
|
+
{
|
|
30376
|
+
if (this.destroyed) return;
|
|
30377
|
+
for (const part of this.parts)
|
|
30378
|
+
part.parent === this || part.destroy?.(immediate); // children go with their parent
|
|
30379
|
+
super.destroy(immediate);
|
|
30380
|
+
}
|
|
30381
|
+
|
|
30382
|
+
/** A prefab's instance has nothing of its own to draw */
|
|
30383
|
+
render3D() {}
|
|
30384
|
+
}
|
|
30385
|
+
|
|
29666
30386
|
///////////////////////////////////////////////////////////////////////////////
|
|
29667
30387
|
// the built-in types
|
|
29668
30388
|
|
|
@@ -29876,7 +30596,9 @@ function particleEffectSanitize(raw)
|
|
|
29876
30596
|
settings[setting.name] = value === '' || particleEffectShapes.includes(value) ? value : fallback;
|
|
29877
30597
|
else if (isNumber(value))
|
|
29878
30598
|
{
|
|
29879
|
-
|
|
30599
|
+
// an angle past half a turn comes around, a clamp would point it the other way
|
|
30600
|
+
const wraps = setting.name === 'angle' && isFinite(value) && abs(value) > PI;
|
|
30601
|
+
const clamped = clamp(wraps ? mod(value + PI, 2*PI) - PI : value, setting.hardMin, setting.hardMax);
|
|
29880
30602
|
settings[setting.name] = setting.step >= 1 ? round(clamped) : clamped;
|
|
29881
30603
|
}
|
|
29882
30604
|
else
|
|
@@ -29955,9 +30677,11 @@ function particleEffectsGet(name) { return particleEffectsByName.get(String(name
|
|
|
29955
30677
|
* @param {string} text
|
|
29956
30678
|
* @return {Array<Object>}
|
|
29957
30679
|
* @memberof ParticleEffects */
|
|
29958
|
-
function particleEffectsParse(text)
|
|
30680
|
+
function particleEffectsParse(text) { return particleEffectsFromData(JSON.parse(text)); }
|
|
30681
|
+
|
|
30682
|
+
// a library file's data, or one effect's, into sanitized effects
|
|
30683
|
+
function particleEffectsFromData(data)
|
|
29959
30684
|
{
|
|
29960
|
-
const data = JSON.parse(text);
|
|
29961
30685
|
const list = isArray(data?.effects) ? data.effects : data?.settings ? [data] : undefined;
|
|
29962
30686
|
if (!list || !list.length)
|
|
29963
30687
|
throw new Error('no effects in this file');
|
|
@@ -29976,7 +30700,7 @@ function particleEffectsText(effects) { return JSON.stringify({version:1, effect
|
|
|
29976
30700
|
* @memberof ParticleEffects */
|
|
29977
30701
|
async function particleEffectsLoad(url)
|
|
29978
30702
|
{
|
|
29979
|
-
const effects =
|
|
30703
|
+
const effects = particleEffectsFromData(await fetchJSON(url));
|
|
29980
30704
|
particleEffectsAdd(effects);
|
|
29981
30705
|
return effects;
|
|
29982
30706
|
}
|
|
@@ -30154,7 +30878,13 @@ function particleEffectTileInfo(s)
|
|
|
30154
30878
|
* @memberof ParticleEffects */
|
|
30155
30879
|
function particleEffectApply(emitter, effect)
|
|
30156
30880
|
{
|
|
30157
|
-
|
|
30881
|
+
// an effect written by hand or changed since it was added
|
|
30882
|
+
particleEffectSet(emitter, particleEffectSanitize(effect));
|
|
30883
|
+
}
|
|
30884
|
+
|
|
30885
|
+
// set a 2D emitter to an effect that is sanitized already, as a play's is
|
|
30886
|
+
function particleEffectSet(emitter, effect)
|
|
30887
|
+
{
|
|
30158
30888
|
const s = effect.settings, ended = emitter.emitTime < 0; // destroyed, its last particles going
|
|
30159
30889
|
for (const setting of particleEffectSettings)
|
|
30160
30890
|
if (!particleEffectIndirect.includes(setting.name))
|
|
@@ -30190,15 +30920,14 @@ function particleEffectResolve(nameOrEffect, options)
|
|
|
30190
30920
|
* - Attach it to an object with addChild to follow it
|
|
30191
30921
|
* @param {string|Object} nameOrEffect - A built-in or added effect's name, or an effect
|
|
30192
30922
|
* @param {Vector2} [pos]
|
|
30193
|
-
* @param {Object} [options]
|
|
30194
|
-
*
|
|
30195
|
-
*
|
|
30196
|
-
*
|
|
30197
|
-
*
|
|
30198
|
-
*
|
|
30199
|
-
* tinted by its colors
|
|
30200
|
-
*
|
|
30201
|
-
* the effect's own for this play
|
|
30923
|
+
* @param {Object} [options] - What to change for this play, each left out when not wanted:
|
|
30924
|
+
* scale grows the whole effect, the built-ins fit a one unit object at 1;
|
|
30925
|
+
* hue turns its colors around the color wheel, 1 is all the way;
|
|
30926
|
+
* saturation multiplies its saturation, 0 is grey;
|
|
30927
|
+
* angle is its direction, 0 is up, the effect's own angle when not given;
|
|
30928
|
+
* tileInfo, a TileInfo or a TextureInfo, is the game's own art to draw with in place of the effect's shape,
|
|
30929
|
+
* tinted by its colors, a whole texture drawn as one tile;
|
|
30930
|
+
* and any effect setting by its name, emitTime, emitRate, speed and the rest, replaces the effect's own
|
|
30202
30931
|
* @return {ParticleEmitter|undefined} - undefined when there is no such effect
|
|
30203
30932
|
* @memberof ParticleEffects */
|
|
30204
30933
|
function particleEffect(nameOrEffect, pos=vec2(), options={})
|
|
@@ -30206,7 +30935,7 @@ function particleEffect(nameOrEffect, pos=vec2(), options={})
|
|
|
30206
30935
|
const effect = particleEffectResolve(nameOrEffect, options);
|
|
30207
30936
|
if (!effect) return;
|
|
30208
30937
|
const emitter = new ParticleEmitter(pos.copy());
|
|
30209
|
-
|
|
30938
|
+
particleEffectSet(emitter, effect);
|
|
30210
30939
|
emitter.scale = options.scale ?? 1;
|
|
30211
30940
|
if (options.angle !== undefined)
|
|
30212
30941
|
emitter.angle = options.angle;
|
|
@@ -30270,7 +30999,7 @@ function particleEffectFromEmitter(emitter, name='Effect')
|
|
|
30270
30999
|
p.velocity.y += (p.pos.x - c.x) * s * .002;
|
|
30271
31000
|
};
|
|
30272
31001
|
b('wind').update3D = (p, s)=> p.velocity.x += s * .004 * p.scale * min(p.age / p.lifeTime, 1);
|
|
30273
|
-
// stick has no 3D push:
|
|
31002
|
+
// stick has no 3D push: particleEffectApply3D makes it the emitter's stick, its grip on landing
|
|
30274
31003
|
}
|
|
30275
31004
|
|
|
30276
31005
|
/** Set a 3D emitter to an effect, live, so a running one keeps its particles; its place, scale and flatten stay
|
|
@@ -30279,7 +31008,13 @@ function particleEffectFromEmitter(emitter, name='Effect')
|
|
|
30279
31008
|
* @memberof ParticleEffects */
|
|
30280
31009
|
function particleEffectApply3D(emitter, effect)
|
|
30281
31010
|
{
|
|
30282
|
-
|
|
31011
|
+
// an effect written by hand or changed since it was added
|
|
31012
|
+
particleEffectSet3D(emitter, particleEffectSanitize(effect));
|
|
31013
|
+
}
|
|
31014
|
+
|
|
31015
|
+
// set a 3D emitter to an effect that is sanitized already, as a play's is
|
|
31016
|
+
function particleEffectSet3D(emitter, effect)
|
|
31017
|
+
{
|
|
30283
31018
|
const s = effect.settings, e = emitter;
|
|
30284
31019
|
// a circle is a sphere and a rectangle a box as deep as it is wide
|
|
30285
31020
|
e.emitSize = s.emitRect ? vec3(s.emitSize, s.emitHeight, s.emitSize) : s.emitSize;
|
|
@@ -30292,8 +31027,8 @@ function particleEffectApply3D(emitter, effect)
|
|
|
30292
31027
|
'randomness', 'additive', 'gravityScale', 'angleSpeed', 'angleDamping', 'collideLevel', 'restitution'])
|
|
30293
31028
|
e[name] = s[name];
|
|
30294
31029
|
e.trailTime = s.trailScale / 60; // a stretch of speed times trailScale is a streak of that many frames
|
|
30295
|
-
|
|
30296
|
-
e.
|
|
31030
|
+
e.friction = s.friction;
|
|
31031
|
+
e.stick = effect.behaviors.find(b=> b.name == 'stick')?.strength ?? 0; // in 3D the emitter grips on landing
|
|
30297
31032
|
// a 2D emitter at angle a shoots along (sin a, cos a), a z turn r takes up to (-sin r, cos r), so r is -a
|
|
30298
31033
|
e.rotation3D = vec3(0, 0, -s.angle);
|
|
30299
31034
|
e.particleUpdateCallback = particleEffectUpdateCallback(effect.behaviors, true);
|
|
@@ -30303,8 +31038,8 @@ function particleEffectApply3D(emitter, effect)
|
|
|
30303
31038
|
* - The same effect data as particleEffect, so the look carries across: a circle spawn area becomes a sphere and a
|
|
30304
31039
|
* rectangle a box as deep as it is wide, both flat across the way it emits with options.flatten, a disc or a
|
|
30305
31040
|
* sheet; a trail becomes a streak of the same length, and the settings the 3D emitter lacks (particleConeAngle,
|
|
30306
|
-
* randomColorLinear, velocityInheritance, localSpace) are left out; the stick behavior becomes the
|
|
30307
|
-
* particle lands
|
|
31041
|
+
* randomColorLinear, velocityInheritance, localSpace) are left out; the stick behavior becomes the emitter's
|
|
31042
|
+
* stick, its grip where a particle lands
|
|
30308
31043
|
* @param {string|Object} nameOrEffect - A built-in or added effect's name, or an effect
|
|
30309
31044
|
* @param {Vector3} [pos3D]
|
|
30310
31045
|
* @param {Object} [options] - scale, hue, saturation, angle, tileInfo and any setting as particleEffect; angle turns it
|
|
@@ -30316,7 +31051,7 @@ function particleEffect3D(nameOrEffect, pos3D=vec3(), options={})
|
|
|
30316
31051
|
const effect = particleEffectResolve(nameOrEffect, options);
|
|
30317
31052
|
if (!effect) return;
|
|
30318
31053
|
const e = new ParticleEmitter3D(pos3D.copy());
|
|
30319
|
-
|
|
31054
|
+
particleEffectSet3D(e, effect);
|
|
30320
31055
|
if (options.angle !== undefined)
|
|
30321
31056
|
e.rotation3D = vec3(0, 0, -options.angle);
|
|
30322
31057
|
e.scale3D = vec3(options.scale ?? 1);
|
|
@@ -30847,6 +31582,11 @@ async function parseGLTF(data, baseUrl='')
|
|
|
30847
31582
|
for (const name of json.extensionsRequired || [])
|
|
30848
31583
|
if (name === 'KHR_draco_mesh_compression' || name === 'EXT_meshopt_compression')
|
|
30849
31584
|
throw new Error(`glTF with ${name} is not read, export it uncompressed`);
|
|
31585
|
+
else if (name === 'KHR_texture_basisu')
|
|
31586
|
+
throw new Error('glTF with KHR_texture_basisu textures is not read, export them as png or jpeg');
|
|
31587
|
+
// one that only may use them has other images to fall back on, or none: said, since it loads with them left out
|
|
31588
|
+
if (json.extensionsUsed?.includes('KHR_texture_basisu') && !json.extensionsRequired?.includes('KHR_texture_basisu'))
|
|
31589
|
+
console.warn('glTF: KHR_texture_basisu textures are not read and are left out, export them as png or jpeg');
|
|
30850
31590
|
|
|
30851
31591
|
// the buffers: the GLB's own, a data uri, or a file beside the model
|
|
30852
31592
|
const buffers = await Promise.all((json.buffers || []).map((buffer, i)=>
|
|
@@ -30889,7 +31629,8 @@ async function parseGLTF(data, baseUrl='')
|
|
|
30889
31629
|
const bitmap = normalTextures.has(index) ?
|
|
30890
31630
|
await createImageBitmap(blob, {colorSpaceConversion: 'none', premultiplyAlpha: 'none'}) :
|
|
30891
31631
|
opaque ? await createImageBitmap(blob, {premultiplyAlpha: 'none'}).then(gltfOpaqueImage) : await createImageBitmap(blob);
|
|
30892
|
-
|
|
31632
|
+
// REPEAT by default, and hard edged only when its sampler says NEAREST, not as the game's tiles are
|
|
31633
|
+
return new TextureInfo(bitmap, true, [sampler.wrapS ?? 10497, sampler.wrapT ?? 10497], sampler.magFilter === 9728);
|
|
30893
31634
|
}
|
|
30894
31635
|
catch (e) { false&&LOG('glTF image not loaded', e); }
|
|
30895
31636
|
}));
|
|
@@ -30999,17 +31740,25 @@ function gltfOpaqueImage(image)
|
|
|
30999
31740
|
{
|
|
31000
31741
|
const gl = glContext, {width, height} = image;
|
|
31001
31742
|
if (gl.isContextLost()) return image; // nothing to read back through, it keeps its alpha
|
|
31002
|
-
|
|
31003
|
-
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
31004
|
-
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
|
|
31005
|
-
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
|
|
31006
|
-
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
|
|
31743
|
+
// the room to read into first: for a huge image it can fail, and must not with the framebuffer bound
|
|
31007
31744
|
const data = new Uint8ClampedArray(width * height * 4);
|
|
31008
|
-
gl.
|
|
31009
|
-
|
|
31010
|
-
|
|
31011
|
-
|
|
31012
|
-
|
|
31745
|
+
const texture = gl.createTexture(), framebuffer = gl.createFramebuffer(), bound = gl.getParameter(gl.FRAMEBUFFER_BINDING);
|
|
31746
|
+
try
|
|
31747
|
+
{
|
|
31748
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
31749
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
|
|
31750
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
|
|
31751
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
|
|
31752
|
+
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, data); // row 0 is the image's first row, as uploaded
|
|
31753
|
+
}
|
|
31754
|
+
finally
|
|
31755
|
+
{
|
|
31756
|
+
// the 2D renderer's own bindings back, whatever happened
|
|
31757
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, bound);
|
|
31758
|
+
gl.bindTexture(gl.TEXTURE_2D, glActiveTexture);
|
|
31759
|
+
gl.deleteFramebuffer(framebuffer);
|
|
31760
|
+
gl.deleteTexture(texture);
|
|
31761
|
+
}
|
|
31013
31762
|
for (let i = 3; i < data.length; i += 4)
|
|
31014
31763
|
data[i] = 255;
|
|
31015
31764
|
image.close();
|
|
@@ -31146,7 +31895,7 @@ function gltfAccessor(json, buffers, index)
|
|
|
31146
31895
|
}
|
|
31147
31896
|
if (scale !== 1)
|
|
31148
31897
|
for (let i = 0; i < out.length; ++i)
|
|
31149
|
-
out[i]
|
|
31898
|
+
out[i] = max(out[i] / scale, -1); // a signed type's lowest value is -1 too, as the format says
|
|
31150
31899
|
return {data: out, components, count: a.count};
|
|
31151
31900
|
}
|
|
31152
31901
|
|
|
@@ -31210,8 +31959,10 @@ function gltfPart(json, buffers, textures, primitive, matrix, name)
|
|
|
31210
31959
|
const [er, eg, eb] = material.emissiveFactor || [0, 0, 0];
|
|
31211
31960
|
if (er || eg || eb)
|
|
31212
31961
|
{
|
|
31962
|
+
// KHR_materials_emissive_strength makes it that many times brighter, emission above 1 in Blender
|
|
31963
|
+
const strength = material.extensions?.KHR_materials_emissive_strength?.emissiveStrength ?? 1;
|
|
31213
31964
|
part.emissiveMap = emissiveRef ? textures[emissiveRef.index] : gltfWhiteTexture();
|
|
31214
|
-
part.emissiveMapColor = rgb(gltfSRGB(er), gltfSRGB(eg), gltfSRGB(eb));
|
|
31965
|
+
part.emissiveMapColor = rgb(gltfSRGB(er) * strength, gltfSRGB(eg) * strength, gltfSRGB(eb) * strength);
|
|
31215
31966
|
}
|
|
31216
31967
|
return part;
|
|
31217
31968
|
}
|