littlejsengine 1.24.2 → 1.24.3

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/dist/littlejs.js CHANGED
@@ -35,7 +35,7 @@ const engineName = 'LittleJS';
35
35
  * @type {string}
36
36
  * @default
37
37
  * @memberof Engine */
38
- const engineVersion = '1.24.2';
38
+ const engineVersion = '1.24.3';
39
39
 
40
40
  /** Frames per second to update
41
41
  * @type {number}
@@ -168,7 +168,7 @@ let timeFixedStart = 0, frameFixedStart = 0;
168
168
  let windowWidthLast = 0, windowHeightLast = 0, windowPixelRatioLast = 0;
169
169
  let engineUpdateInternal; // assigned by engineInit so engineStep can drive it
170
170
  let engineFrameScheduled = false; // a frame of the loop is asked for and has not run yet
171
- let engineFrameErrors = 0; // errors a release build's loop went on past
171
+ let engineFrameErrorLast; // the last error a release build's loop went on past, as text
172
172
 
173
173
  // the pairs of objects asked about a collision this update, so the other's own physics does not ask again: each
174
174
  // asker's others, with true for a pair both said to resolve, and false for one left overlapping, ignored or only
@@ -422,20 +422,30 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
422
422
  frameTimeBufferMS = 0;
423
423
  }
424
424
 
425
- // update multiple frames if necessary in case of slow framerate
426
- for (; frameTimeBufferMS >= 0; frameTimeBufferMS -= 1e3 / frameRate)
425
+ // update multiple frames if necessary in case of slow framerate; a tick that throws still spends its time
426
+ // and the smoothing still comes back, so a release build going on past an error each tick keeps its rate
427
+ try
427
428
  {
428
- // read again each tick, so a pause set by the game stops the rest of this frame's catch-up ticks
429
- const frozenTick = paused || !(timeScale * debugScale);
429
+ while (frameTimeBufferMS >= 0)
430
+ {
431
+ try
432
+ {
433
+ // read again each tick, so a pause set by the game stops the rest of this frame's catch-up ticks
434
+ const frozenTick = paused || !(timeScale * debugScale);
430
435
 
431
- // increment frame and update time, frozen does not advance time
432
- if (!frozenTick)
433
- time = timeFixedStart + (frame++ - frameFixedStart) / frameRate;
434
- engineTick(frozenTick);
436
+ // increment frame and update time, frozen does not advance time
437
+ if (!frozenTick)
438
+ time = timeFixedStart + (frame++ - frameFixedStart) / frameRate;
439
+ engineTick(frozenTick);
440
+ }
441
+ finally { frameTimeBufferMS -= 1e3 / frameRate; }
442
+ }
443
+ }
444
+ finally
445
+ {
446
+ // add the time smoothing back in
447
+ frameTimeBufferMS += deltaSmooth;
435
448
  }
436
-
437
- // add the time smoothing back in
438
- frameTimeBufferMS += deltaSmooth;
439
449
  }
440
450
 
441
451
  // one tick of the loop: update game and objects, when frozen update everything except them
@@ -732,9 +742,13 @@ function engineScheduleFrame()
732
742
  try { engineUpdateInternal(frameTimeMS); }
733
743
  catch (error)
734
744
  {
735
- // a release build goes on past an error in a frame, a frozen game is the worst a player can get; only the
736
- // first is logged, one every frame would flood the console
737
- engineFrameErrors++ || console.error(error);
745
+ // a release build goes on past an error in a frame, a frozen game is the worst a player can get; an error
746
+ // is logged when it is not the last one again, one every frame would flood the console
747
+ const text = String(error);
748
+ text === engineFrameErrorLast || console.error(error);
749
+ engineFrameErrorLast = text;
750
+ // the frame's input is cleared as its tick would have, or a key press that threw would be pressed again
751
+ inputUpdatePost();
738
752
  engineScheduleFrame();
739
753
  }
740
754
  };
@@ -5187,10 +5201,13 @@ function tile(index=0, size=tileDefaultSize, texture=0, padding=tileDefaultPaddi
5187
5201
  // create tile info object
5188
5202
  const textureInfo = typeof texture === 'number' ?
5189
5203
  textureInfos[texture] : texture;
5190
- if (headlessMode && !textureInfo?.size.x)
5191
- return new TileInfo(new Vector2, size.copy(), textureInfo, padding, bleed); // no image loaded, no place in it
5192
- ASSERT(textureInfo instanceof TextureInfo, 'tile texture is not loaded');
5193
- ASSERT(textureInfo.size.x > 0, 'tile texture is not loaded');
5204
+ if (!textureInfo?.size.x)
5205
+ {
5206
+ // no image: headless loads none and keeps the size; an image that failed to load, whose warning named it, gives
5207
+ // a tile of no size, which draws nothing; a slot never given an image is a mistake a debug build points out
5208
+ ASSERT(headlessMode || textureInfo instanceof TextureInfo, 'tile texture is not loaded', texture);
5209
+ return new TileInfo(new Vector2, headlessMode ? size.copy() : new Vector2, textureInfo, padding, bleed);
5210
+ }
5194
5211
 
5195
5212
  // get the position of the tile
5196
5213
  const sizePaddedX = size.x + padding*2;
@@ -5658,8 +5675,8 @@ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
5658
5675
  if (headlessMode && !context) return; // headless has no canvas, only a context passed in is drawn to
5659
5676
 
5660
5677
  const textureInfo = tileInfo?.textureInfo;
5661
- if (textureInfo && !(tileInfo.size.x && tileInfo.size.y))
5662
- return; // a tile with no area draws nothing, like a sprite still loading
5678
+ if (textureInfo && !(tileInfo.size.x && tileInfo.size.y && textureInfo.size.x))
5679
+ return; // a tile with no area draws nothing, like a sprite still loading, and nor does an image that failed
5663
5680
  const bleed = tileInfo?.bleed ?? 0;
5664
5681
  if (useWebGL && glEnable)
5665
5682
  {
@@ -5753,7 +5770,7 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
5753
5770
  * @param {boolean} [screenSpace=drawScreenSpace]
5754
5771
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
5755
5772
  * @memberof Draw */
5756
- function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=CLEAR_WHITE, angle=0, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
5773
+ function drawRectGradient(pos, size=vec2(1), colorTop=WHITE, colorBottom=CLEAR_WHITE, angle=0, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
5757
5774
  {
5758
5775
  ASSERT(isVector2(pos), 'pos must be a vec2');
5759
5776
  ASSERT(isVector2(size), 'size must be a vec2');
@@ -6748,8 +6765,7 @@ function bakeTintedImage(image, color, additiveColor)
6748
6765
  * @param {number} dHeight
6749
6766
  * @param {Color} color
6750
6767
  * @param {Color} [additiveColor]
6751
- * @param {number} [bleed] - How many pixels to shrink the source, used to fix bleeding
6752
- * @memberof Draw */
6768
+ * @param {number} [bleed] - How many pixels to shrink the source, used to fix bleeding */
6753
6769
  function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight, color, additiveColor, bleed=0)
6754
6770
  {
6755
6771
  const sx2 = bleed;
@@ -7575,8 +7591,9 @@ function inputInit()
7575
7591
  if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
7576
7592
  audioContext.resume();
7577
7593
 
7578
- // a press in the bars around a letterboxed canvas is not one on its edge; a drag out of it still moves
7579
- if (!inCanvas(e.x, e.y))
7594
+ // a press in the bars around a letterboxed canvas is not one on its edge; a drag out of it still moves; under
7595
+ // pointer lock the mouse stays where the lock began, which may be in a bar, and every click is the game's
7596
+ if (!pointerLockIsActive() && !inCanvas(e.x, e.y))
7580
7597
  return;
7581
7598
  inputData[0][e.button] = 3;
7582
7599
 
@@ -7993,9 +8010,11 @@ function inputUpdate()
7993
8010
  (gamepadIsDown(12,i)&&1) - (gamepadIsDown(13,i)&&1));
7994
8011
  }
7995
8012
 
7996
- // copy dpad to left analog stick when pressed
8013
+ // copy dpad to left analog stick when pressed; a gamepad with no sticks has that one, at rest when not
7997
8014
  if (gamepadDirectionEmulateStick && (dpad.x || dpad.y))
7998
8015
  sticks[0] = dpad.clampLength();
8016
+ else if (gamepadDirectionEmulateStick && !sticks.length)
8017
+ sticks[0] = vec2();
7999
8018
  }
8000
8019
 
8001
8020
  // disable touch gamepad if using real gamepad
@@ -8742,7 +8761,7 @@ class Sound
8742
8761
  */
8743
8762
  constructor(asset, randomness, range, taper=soundDefaultTaper, onloadCallback)
8744
8763
  {
8745
- if (!soundEnable || headlessMode)
8764
+ if (!soundEnable || headlessMode || !audioContext) // a browser with no audio makes none either
8746
8765
  {
8747
8766
  // no sound is made: it counts as loaded, so a game that waits for its sounds goes on
8748
8767
  this.loadedPercent = 1;
@@ -8812,8 +8831,8 @@ class Sound
8812
8831
  // load the audio file, a URL object as bundlers give works like its string;
8813
8832
  // report failures rather than leaving an unhandled rejection, the sound just stays unloaded and silent
8814
8833
  const filename = asset + '';
8815
- engineAddLoad(this.loadSound(filename).catch(e=>
8816
- LOG('Sound load failed for', filename, '-', e.message))); // startup waits for it
8834
+ engineAddLoad(this.loadSound(filename).catch(e=> // startup waits for it, and a release build says why too
8835
+ console.warn('Sound load failed for', filename, '-', e?.message ?? e)));
8817
8836
  }
8818
8837
  }
8819
8838
 
@@ -11862,7 +11881,7 @@ function glSetTextureWrap(texture, wrap=true)
11862
11881
  /** Compile WebGL shader of the given type, will throw errors if in debug mode
11863
11882
  * @param {string} source
11864
11883
  * @param {number} type
11865
- * @return {WebGLShader}
11884
+ * @return {WebGLShader|undefined} - undefined with no WebGL
11866
11885
  * @memberof WebGL */
11867
11886
  function glCompileShader(source, type)
11868
11887
  {
@@ -11882,7 +11901,7 @@ function glCompileShader(source, type)
11882
11901
  /** Create WebGL program with given shaders
11883
11902
  * @param {string} vsSource
11884
11903
  * @param {string} fsSource
11885
- * @return {WebGLProgram}
11904
+ * @return {WebGLProgram|undefined} - undefined with no WebGL
11886
11905
  * @memberof WebGL */
11887
11906
  function glCreateProgram(vsSource, fsSource)
11888
11907
  {
@@ -11930,7 +11949,7 @@ function glShaderProgram(shader)
11930
11949
  * @param {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|ImageBitmap} [image]
11931
11950
  * @param {boolean|Array<number>} [wrap] - true for REPEAT, false for CLAMP_TO_EDGE, or the WebGL modes across and down
11932
11951
  * @param {boolean} [pixelated] - Hard edged or smooth, undefined follows tilesPixelated
11933
- * @return {WebGLTexture}
11952
+ * @return {WebGLTexture|undefined} - undefined with no WebGL
11934
11953
  * @memberof WebGL */
11935
11954
  function glCreateTexture(image, wrap=false, pixelated=tilesPixelated)
11936
11955
  {
@@ -17790,7 +17809,7 @@ class Box2dObject extends EngineObject
17790
17809
  * @param {number} angle */
17791
17810
  setTransform(pos, angle)
17792
17811
  {
17793
- this.pos = pos.copy();
17812
+ this.pos.set(pos.x, pos.y); // its own pos kept, as an EngineObject's is
17794
17813
  this.angle = angle;
17795
17814
  // box2d uses reverse angle
17796
17815
  const x = pos.x, y = pos.y;
@@ -17806,7 +17825,7 @@ class Box2dObject extends EngineObject
17806
17825
  * @param {Vector2} pos */
17807
17826
  setPosition(pos)
17808
17827
  {
17809
- this.pos = pos.copy();
17828
+ this.pos.set(pos.x, pos.y); // its own pos kept, as an EngineObject's is
17810
17829
  const x = pos.x, y = pos.y;
17811
17830
  box2dWhenUnlocked(()=>
17812
17831
  {
@@ -19307,7 +19326,8 @@ class Box2dPlugin
19307
19326
  return box2d.raycastAll(start, end, includeSensors)[0];
19308
19327
  }
19309
19328
 
19310
- /** box aabb cast and return all the objects
19329
+ /** Every object whose shapes' bounding boxes overlap a box, not the shapes themselves, so near the corner of a
19330
+ * turned box or a circle it finds one the box does not touch; use pointCast or a raycast for exact
19311
19331
  * @param {Vector2} pos
19312
19332
  * @param {Vector2} size
19313
19333
  * @param {boolean} [includeSensors] - Also find sensors, trigger zones are passed through by default
@@ -19337,7 +19357,8 @@ class Box2dPlugin
19337
19357
  return queryObjects;
19338
19358
  }
19339
19359
 
19340
- /** box aabb cast and return the first object
19360
+ /** The first object whose shapes' bounding boxes overlap a box, not the shapes themselves, so near the corner of
19361
+ * a turned box or a circle it finds one the box does not touch; use pointCast or a raycast for exact
19341
19362
  * @param {Vector2} pos
19342
19363
  * @param {Vector2} size
19343
19364
  * @param {boolean} [includeSensors] - Also find sensors, trigger zones are passed through by default
@@ -19597,8 +19618,10 @@ async function box2dInit()
19597
19618
  {
19598
19619
  if (o.body)
19599
19620
  {
19621
+ // moved in place, as the engine moves an EngineObject's pos, so what holds it follows the body;
19600
19622
  // box2d uses reverse angle
19601
- o.pos = box2d.vec2From(o.body.GetPosition());
19623
+ const p = o.body.GetPosition();
19624
+ o.pos.set(p.get_x(), p.get_y());
19602
19625
  o.angle = -o.body.GetAngle();
19603
19626
  }
19604
19627
  }
@@ -20094,6 +20117,8 @@ let textureSheetPendingCount = 0;
20094
20117
  const textureSheetLoaded = new Map;
20095
20118
  // a load's key: what it was given, a vector by its numbers
20096
20119
  const textureSheetLoadKey = (...parts)=> parts.map((p)=> isVector2(p) ? p.x + ',' + p.y : String(p)).join('|');
20120
+ // a load whose file failed is forgotten, so loading it again tries again, as when the server comes back
20121
+ const textureSheetLoadFailed = (key, loaded)=> textureSheetLoaded.get(key) === loaded && textureSheetLoaded.delete(key);
20097
20122
 
20098
20123
  /**
20099
20124
  * Texture Sheet - A texture that images are packed into as they load
@@ -20250,7 +20275,8 @@ class TextureSheet
20250
20275
  * - Pass frameSize for animations, then step through them with TileInfo.frame
20251
20276
  * - Grid images keep their layout and frames wrap down to the next row
20252
20277
  * - Pass sourcePadding if the source image has padding baked in around frames
20253
- * - The same image loaded again with the same settings gives back what the first load did, packed once
20278
+ * - The same image loaded again with the same settings gives back what the first load did, packed once, unless
20279
+ * that load failed
20254
20280
  * @param {string} src - Image source path
20255
20281
  * @param {Vector2|number} [frameSize] - Size of each animation frame in pixels, or the whole image less its
20256
20282
  * source padding if not passed
@@ -20305,7 +20331,7 @@ function loadSprite(src, frameSize, padding=textureSheetPadding, sourcePadding=0
20305
20331
  if (!added)
20306
20332
  {
20307
20333
  // leave the tile empty, no sheet can hold it
20308
- LOG('loadSprite image is too large to fit on a texture sheet:', src);
20334
+ console.warn('loadSprite image is too large to fit on a texture sheet:', src);
20309
20335
  return;
20310
20336
  }
20311
20337
  Object.assign(tileInfo, added.tile);
@@ -20314,7 +20340,8 @@ function loadSprite(src, frameSize, padding=textureSheetPadding, sourcePadding=0
20314
20340
  else
20315
20341
  {
20316
20342
  // leave the tile empty if the image failed to load
20317
- LOG('loadSprite failed to load image:', src);
20343
+ console.warn('loadSprite failed to load image:', src);
20344
+ textureSheetLoadFailed(key, tileInfo);
20318
20345
  }
20319
20346
  });
20320
20347
 
@@ -20332,7 +20359,8 @@ function loadSprite(src, frameSize, padding=textureSheetPadding, sourcePadding=0
20332
20359
  * is not read, cut it into its tiles first
20333
20360
  * - An image that fails to load, or that no sheet can hold, adds no tiles and says so in the console, so the tiles
20334
20361
  * of the images after it move up
20335
- * - The same images loaded again with the same settings give back what the first load did, packed once
20362
+ * - The same images loaded again with the same settings give back what the first load did, packed once, unless
20363
+ * an image of it failed
20336
20364
  * @param {Array<string>} sources - Image source paths
20337
20365
  * @param {Vector2|number} [tileSize] - Size of a tile in pixels
20338
20366
  * @param {number} [padding] - How many pixels padding around each tile on the sheet
@@ -20374,6 +20402,7 @@ function loadTiles(sources, tileSize=tileDefaultSize, padding=textureSheetPaddin
20374
20402
  // the whole tiles of it, an edge past the last one left out
20375
20403
  const columns = image.width / size.x | 0, rows = image.height / size.y | 0, count = columns * rows;
20376
20404
  const added = count && textureSheetAdd(vec2(columns * size.x, rows * size.y), size, padding, 0);
20405
+ image.width || textureSheetLoadFailed(key, set);
20377
20406
  if (!added)
20378
20407
  return console.warn('loadTiles: ' + src + (count ? ' does not fit on a texture sheet' :
20379
20408
  ' failed to load, or is smaller than a tile') + ', its tiles are left out');
@@ -20392,7 +20421,8 @@ function loadTiles(sources, tileSize=tileDefaultSize, padding=textureSheetPaddin
20392
20421
  * - Aseprite frame tags become animations, so do names like run_0, run_1
20393
20422
  * - Trimmed frames are restored to their full source size when packed
20394
20423
  * - Rotated frames are rotated back upright when packed
20395
- * - The same atlas loaded again by its paths, with the same padding, gives back what the first load did, packed once
20424
+ * - The same atlas loaded again by its paths, with the same padding, gives back what the first load did, packed
20425
+ * once, unless that load failed
20396
20426
  * @param {string} imageSrc - Atlas image path
20397
20427
  * @param {string|Object} jsonSrc - Atlas json path, or already parsed json data
20398
20428
  * @param {number} [padding] - How many pixels padding around each frame
@@ -20447,7 +20477,7 @@ function loadAtlas(imageSrc, jsonSrc, padding=textureSheetPadding)
20447
20477
  const added = textureSheetAdd(blockSize, sourceSize, padding);
20448
20478
  if (!added)
20449
20479
  {
20450
- LOG('loadAtlas frames are too large to fit on a texture sheet:', group.name);
20480
+ console.warn('loadAtlas frames are too large to fit on a texture sheet:', group.name);
20451
20481
  continue;
20452
20482
  }
20453
20483
  const {sheet, tile} = added;
@@ -20481,7 +20511,8 @@ function loadAtlas(imageSrc, jsonSrc, padding=textureSheetPadding)
20481
20511
  else
20482
20512
  {
20483
20513
  // leave the atlas empty if either file failed to load
20484
- LOG('loadAtlas failed to load:', imageSrc, jsonSrc);
20514
+ console.warn('loadAtlas failed to load:', imageSrc, jsonSrc);
20515
+ textureSheetLoadFailed(key, atlas);
20485
20516
  }
20486
20517
  });
20487
20518
 
@@ -28924,9 +28955,9 @@ class HeightMap extends EngineObject3D
28924
28955
  levelSegment3D(from, to)
28925
28956
  {
28926
28957
  const m = this.pos3D, size = this.mapSize;
28927
- if (abs(to.x - m.x) > size.x / 2 || abs(to.z - m.z) > size.y / 2) return; // off the map
28958
+ if (abs(to.x - m.x) > size.x / 2 || abs(to.z - m.z) > size.y / 2) return undefined; // off the map
28928
28959
  const a = from.y - this.getHeight(from.x, from.z), b = to.y - this.getHeight(to.x, to.z);
28929
- if (a < 0 || b >= 0) return; // above all the way, or under from the start
28960
+ if (a < 0 || b >= 0) return undefined; // above all the way, or under from the start
28930
28961
  const distance = a / (a - b);
28931
28962
  return {distance, normal: this.getNormal(from.x + (to.x - from.x) * distance, from.z + (to.z - from.z) * distance)};
28932
28963
  }
@@ -29964,9 +29995,11 @@ class LensFlare3D extends EngineObject3D
29964
29995
  return this.made;
29965
29996
  }
29966
29997
 
29967
- // what the flare is of, seen from the camera: the way to it, how far it is, Infinity for the sun and for a
29968
- // directional light, which shines from its place toward the origin, and a point to find it on the screen by;
29969
- // undefined with no direction, or a light that is gone or at the camera
29998
+ /** What the flare is of, seen from the camera: the way to it, how far it is, Infinity for the sun and for a
29999
+ * directional light, which shines from its place toward the origin, and a point to find it on the screen by;
30000
+ * undefined with no direction, or a light that is gone or at the camera
30001
+ * @return {{direction: Vector3, distance: number, pos: Vector3}|undefined}
30002
+ * @ignore */
29970
30003
  flareSource()
29971
30004
  {
29972
30005
  const camera = render3D.camera.pos, light = this.light;
@@ -29982,9 +30015,11 @@ class LensFlare3D extends EngineObject3D
29982
30015
  return distance ? {direction: offset.scale(1 / distance), distance, pos} : undefined;
29983
30016
  }
29984
30017
 
29985
- // how the flare would show with nothing in the way: where its source is on the screen, how strong it is there,
29986
- // fading as it leaves the screen, its tint and the height its sizes are parts of; undefined when it would not
29987
- // show at all, behind the camera, off the screen, or of a light that is off or seen from outside its cone
30018
+ /** How the flare would show with nothing in the way: where its source is on the screen, how strong it is there,
30019
+ * fading as it leaves the screen, its tint and the height its sizes are parts of; undefined when it would not
30020
+ * show at all, behind the camera, off the screen, or of a light that is off or seen from outside its cone
30021
+ * @return {{sun: Vector2, center: Vector2, strength: number, tint: Color, height: number}|undefined}
30022
+ * @ignore */
29988
30023
  flareLook()
29989
30024
  {
29990
30025
  const source = this.flareSource(), center = mainCanvasSize.scale(.5);
@@ -33285,11 +33320,16 @@ function gltfFetch(uri, baseUrl, files)
33285
33320
  const path = clean(baseUrl + name);
33286
33321
  if (path === undefined)
33287
33322
  return Promise.reject(new Error('glTF needs ' + name + ', which is above the files given'));
33288
- const lower = path.toLowerCase(), file = path.slice(path.lastIndexOf('/') + 1);
33323
+ const lower = path.toLowerCase(), file = lower.slice(lower.lastIndexOf('/') + 1);
33289
33324
  const byPath = new Map([...files].map(([key, value])=> [clean(key)?.toLowerCase(), value]));
33290
33325
  const found = files.get(path) ?? byPath.get(lower), keys = [...byPath.keys()];
33291
33326
  if (found)
33292
33327
  return Promise.resolve(new Response(found));
33328
+ // what is in a folder dropped, not the folder, leaves the folder's own name out of the paths: the longest key
33329
+ // the path ends with, a folder and more, as a name alone is only taken when one file has it
33330
+ const end = keys.filter((k)=> k?.includes('/') && lower.endsWith('/' + k)).sort((a, b)=> b.length - a.length)[0];
33331
+ if (end)
33332
+ return Promise.resolve(new Response(byPath.get(end)));
33293
33333
  const named = new Set(keys.filter((k)=> k?.slice(k.lastIndexOf('/') + 1) === file).map((k)=> byPath.get(k)));
33294
33334
  if (named.size === 1)
33295
33335
  return Promise.resolve(new Response([...named][0]));