Online co-op browser remake of Arknights' Stronghold Protocol: Alliance (client-simulated combat, official waves/bonds/items/strategies, 3D board, facing wheel, operator loadout, official emotes, deployment scripts).
94 lines
4.2 KiB
JavaScript
94 lines
4.2 KiB
JavaScript
// Small polyfills for the older browsers the client supports (Safari / iOS 15.0–15.3, Firefox ESR, older Chromium on
|
||
// Android tablets). Imported first by main.js (and the dev harnesses), so every later module — including the ones
|
||
// loaded on demand (render engine, battle sim) — sees them. Each one is installed only when the browser lacks it
|
||
// (feature detection, never UA sniffing) and is a faithful subset of the standard behaviour:
|
||
// Object.hasOwn Safari < 15.4, Firefox < 92
|
||
// Array/String/TypedArray .at() Safari < 15.4, Firefox < 90
|
||
// Array .findLast / .findLastIndex Safari < 15.4, Firefox < 104
|
||
// structuredClone Safari < 15.4, Firefox < 94 (JSON-safe values + Map/Set/Date; enough for
|
||
// the plain state objects the client clones)
|
||
// globalThis.queueMicrotask very old WebViews
|
||
// `installCompat(target)` is exported for tests (it patches the given global object instead).
|
||
|
||
/**
|
||
* Install the polyfills on `g` (default globalThis). Returns the names that were missing and got installed.
|
||
* @param {any} [g]
|
||
* @returns {string[]}
|
||
*/
|
||
export function installCompat(g = globalThis) {
|
||
const added = [];
|
||
const def = (obj, name, fn) => {
|
||
if (!obj || typeof obj[name] === 'function') return;
|
||
try {
|
||
Object.defineProperty(obj, name, { value: fn, writable: true, configurable: true, enumerable: false });
|
||
added.push(name);
|
||
} catch { /* frozen: leave it */ }
|
||
};
|
||
const O = g.Object || Object;
|
||
def(O, 'hasOwn', function hasOwn(obj, key) {
|
||
if (obj == null) throw new TypeError('Cannot convert undefined or null to object');
|
||
return Object.prototype.hasOwnProperty.call(Object(obj), key);
|
||
});
|
||
function at(i) {
|
||
const len = this.length >>> 0;
|
||
let k = Math.trunc(Number(i)) || 0;
|
||
if (k < 0) k += len;
|
||
if (k < 0 || k >= len) return undefined;
|
||
return typeof this === 'string' || this instanceof String ? String(this).charAt(k) : this[k];
|
||
}
|
||
def((g.Array || Array).prototype, 'at', at);
|
||
def((g.String || String).prototype, 'at', at);
|
||
const TA = g.Int8Array ? Object.getPrototypeOf(g.Int8Array.prototype) : null;
|
||
if (TA) def(TA, 'at', at);
|
||
def((g.Array || Array).prototype, 'findLast', function findLast(fn, thisArg) {
|
||
for (let i = (this.length >>> 0) - 1; i >= 0; i--) if (fn.call(thisArg, this[i], i, this)) return this[i];
|
||
return undefined;
|
||
});
|
||
def((g.Array || Array).prototype, 'findLastIndex', function findLastIndex(fn, thisArg) {
|
||
for (let i = (this.length >>> 0) - 1; i >= 0; i--) if (fn.call(thisArg, this[i], i, this)) return i;
|
||
return -1;
|
||
});
|
||
def(g, 'structuredClone', function structuredClone(value) { return cloneValue(value, new Map()); });
|
||
def(g, 'queueMicrotask', function queueMicrotask(fn) { Promise.resolve().then(fn).catch((err) => setTimeout(() => { throw err; })); });
|
||
return added;
|
||
}
|
||
|
||
/** Deep clone of plain data (objects, arrays, Map, Set, Date, RegExp, typed arrays), cycles preserved. */
|
||
function cloneValue(v, seen) {
|
||
if (v === null || typeof v !== 'object') {
|
||
if (typeof v === 'function' || typeof v === 'symbol') throw new TypeError('structuredClone: value could not be cloned');
|
||
return v;
|
||
}
|
||
if (seen.has(v)) return seen.get(v);
|
||
let out;
|
||
if (v instanceof Date) out = new Date(v.getTime());
|
||
else if (v instanceof RegExp) out = new RegExp(v.source, v.flags);
|
||
else if (ArrayBuffer.isView(v)) out = new v.constructor(v);
|
||
else if (v instanceof ArrayBuffer) out = v.slice(0);
|
||
else if (v instanceof Map) {
|
||
out = new Map();
|
||
seen.set(v, out);
|
||
for (const [k, x] of v) out.set(cloneValue(k, seen), cloneValue(x, seen));
|
||
return out;
|
||
} else if (v instanceof Set) {
|
||
out = new Set();
|
||
seen.set(v, out);
|
||
for (const x of v) out.add(cloneValue(x, seen));
|
||
return out;
|
||
} else if (Array.isArray(v)) {
|
||
out = new Array(v.length);
|
||
seen.set(v, out);
|
||
for (let i = 0; i < v.length; i++) out[i] = cloneValue(v[i], seen);
|
||
return out;
|
||
} else {
|
||
out = {};
|
||
seen.set(v, out);
|
||
for (const k of Object.keys(v)) out[k] = cloneValue(v[k], seen);
|
||
return out;
|
||
}
|
||
seen.set(v, out);
|
||
return out;
|
||
}
|
||
|
||
export const installedPolyfills = installCompat();
|