fix: improve fuzzy handling
This commit is contained in:
@@ -1,253 +1,254 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
|
<head>
|
||||||
<head>
|
<meta charset="utf-8" />
|
||||||
<meta charset="utf-8" />
|
<style>
|
||||||
<style>
|
body {
|
||||||
body {
|
margin: 0;
|
||||||
margin: 0;
|
background: #111;
|
||||||
background: #111;
|
color: white;
|
||||||
color: white;
|
font-family: sans-serif;
|
||||||
font-family: sans-serif;
|
overflow: auto;
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 10;
|
|
||||||
background: #000c;
|
|
||||||
padding: 8px;
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
button,
|
|
||||||
select,
|
|
||||||
input {
|
|
||||||
font: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas {
|
|
||||||
display: block;
|
|
||||||
margin: 12px auto;
|
|
||||||
background: #000;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div class="bar">
|
|
||||||
<button id="refresh">refresh</button>
|
|
||||||
<label>
|
|
||||||
mode
|
|
||||||
<select id="mode">
|
|
||||||
<option value="edge">edge diff</option>
|
|
||||||
<option value="diff">raw diff</option>
|
|
||||||
<option value="overlay">red/green overlay</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
threshold
|
|
||||||
<input id="threshold" type="range" min="0" max="120" value="85">
|
|
||||||
<span id="thresholdText">85</span>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
time: <span id="timingText">Loading...</span><span style="color: gray;">(image load complete in browser)</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<canvas id="canvas" width="1870" height="778"></canvas>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
const type = urlParams.get('type');
|
|
||||||
|
|
||||||
let MINE, ORIG, W, H;
|
|
||||||
if (type === 'list') {
|
|
||||||
MINE = "/v1/card/genshin/775956514";
|
|
||||||
ORIG = "/v1/card/genshin/diff/listoriginal.webp";
|
|
||||||
W = 1200;
|
|
||||||
H = 732;
|
|
||||||
} else {
|
|
||||||
MINE = "/v1/card/genshin/775956514/ororon";
|
|
||||||
ORIG = "/v1/card/genshin/diff/ororo.png";
|
|
||||||
W = 1870;
|
|
||||||
H = 778;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {HTMLCanvasElement}
|
|
||||||
*/
|
|
||||||
const canvas = document.getElementById("canvas");
|
|
||||||
canvas.width = W;
|
|
||||||
canvas.height = H;
|
|
||||||
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
||||||
|
|
||||||
const mode = document.getElementById("mode");
|
|
||||||
const threshold = document.getElementById("threshold");
|
|
||||||
const thresholdText = document.getElementById("thresholdText");
|
|
||||||
const timingText = document.getElementById("timingText");
|
|
||||||
|
|
||||||
let mineData, origData, refreshing = false;
|
|
||||||
|
|
||||||
function loadImage(src) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const img = new Image();
|
|
||||||
img.crossOrigin = "anonymous";
|
|
||||||
img.onload = () => resolve(img);
|
|
||||||
img.onerror = reject;
|
|
||||||
img.src = src + (src.includes("?") ? "&" : "?") + "cacheBust=" + Date.now();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawToData(img) {
|
|
||||||
const c = document.createElement("canvas");
|
|
||||||
c.width = W;
|
|
||||||
c.height = H;
|
|
||||||
const x = c.getContext("2d", { willReadFrequently: true });
|
|
||||||
x.drawImage(img, 0, 0, W, H);
|
|
||||||
return x.getImageData(0, 0, W, H);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refresh() {
|
|
||||||
if (refreshing) return;
|
|
||||||
refreshing = true;
|
|
||||||
ctx.fillStyle = "#00000088";
|
|
||||||
ctx.fillRect(0, 0, W, H);
|
|
||||||
ctx.fillStyle = "white";
|
|
||||||
let line = 30;
|
|
||||||
function log(text) {
|
|
||||||
ctx.fillText(text, 20, line);
|
|
||||||
line += 16;
|
|
||||||
}
|
}
|
||||||
log("fetching...");
|
|
||||||
|
|
||||||
const start = Date.now();
|
.bar {
|
||||||
const [mineImg, origImg] = await Promise.all([
|
position: sticky;
|
||||||
loadImage(MINE).finally(() => timingText.innerText = `${Date.now() - start}ms`),
|
top: 0;
|
||||||
loadImage(ORIG),
|
z-index: 10;
|
||||||
]);
|
background: #000c;
|
||||||
|
padding: 8px;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
log("loading...");
|
button,
|
||||||
mineData = drawToData(mineImg);
|
select,
|
||||||
if (!origData) origData = drawToData(origImg);
|
input {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
log("rendering...");
|
canvas {
|
||||||
render();
|
display: block;
|
||||||
refreshing = false;
|
margin: 12px auto;
|
||||||
}
|
background: #000;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
function lum(d, i) {
|
<body>
|
||||||
return d[i] * 0.299 + d[i + 1] * 0.587 + d[i + 2] * 0.114;
|
<div class="bar">
|
||||||
}
|
<button id="refresh">refresh</button>
|
||||||
|
<label>
|
||||||
|
mode
|
||||||
|
<select id="mode">
|
||||||
|
<option value="edge">edge diff</option>
|
||||||
|
<option value="diff">raw diff</option>
|
||||||
|
<option value="overlay">red/green overlay</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
threshold
|
||||||
|
<input id="threshold" type="range" min="0" max="120" value="85" />
|
||||||
|
<span id="thresholdText">85</span>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
time: <span id="timingText">Loading...</span
|
||||||
|
><span style="color: gray">(image load complete in browser)</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
function edgeAt(data, x, y) {
|
<canvas id="canvas" width="1870" height="778"></canvas>
|
||||||
const d = data.data;
|
|
||||||
const i = (y * W + x) * 4;
|
|
||||||
|
|
||||||
const l = lum(d, i);
|
<script>
|
||||||
const lx = x > 0 ? lum(d, i - 4) : l;
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const rx = x < W - 1 ? lum(d, i + 4) : l;
|
const type = urlParams.get("type");
|
||||||
const uy = y > 0 ? lum(d, i - W * 4) : l;
|
|
||||||
const dy = y < H - 1 ? lum(d, i + W * 4) : l;
|
|
||||||
|
|
||||||
return Math.abs(rx - lx) + Math.abs(dy - uy);
|
let MINE, ORIG, W, H;
|
||||||
}
|
if (type === "list") {
|
||||||
|
MINE = "/v1/card/genshin/775956514";
|
||||||
|
ORIG = "/v1/card/genshin/diff/listoriginal.webp";
|
||||||
|
W = 1200;
|
||||||
|
H = 732;
|
||||||
|
} else {
|
||||||
|
MINE = "/v1/card/genshin/775956514/ororon";
|
||||||
|
ORIG = "/v1/card/genshin/diff/ororo.png";
|
||||||
|
W = 1870;
|
||||||
|
H = 778;
|
||||||
|
}
|
||||||
|
|
||||||
function render() {
|
/**
|
||||||
if (!mineData || !origData) return;
|
* @type {HTMLCanvasElement}
|
||||||
|
*/
|
||||||
|
const canvas = document.getElementById("canvas");
|
||||||
|
canvas.width = W;
|
||||||
|
canvas.height = H;
|
||||||
|
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
||||||
|
|
||||||
const out = ctx.createImageData(W, H);
|
const mode = document.getElementById("mode");
|
||||||
const o = out.data;
|
const threshold = document.getElementById("threshold");
|
||||||
const m = mineData.data;
|
const thresholdText = document.getElementById("thresholdText");
|
||||||
const r = origData.data;
|
const timingText = document.getElementById("timingText");
|
||||||
const t = Number(threshold.value);
|
|
||||||
|
|
||||||
thresholdText.textContent = t;
|
let mineData,
|
||||||
|
origData,
|
||||||
|
refreshing = false;
|
||||||
|
|
||||||
for (let y = 0; y < H; y++) {
|
function loadImage(src) {
|
||||||
for (let x = 0; x < W; x++) {
|
return new Promise((resolve, reject) => {
|
||||||
const i = (y * W + x) * 4;
|
const img = new Image();
|
||||||
|
img.crossOrigin = "anonymous";
|
||||||
|
img.onload = () => resolve(img);
|
||||||
|
img.onerror = reject;
|
||||||
|
img.src =
|
||||||
|
src + (src.includes("?") ? "&" : "?") + "cacheBust=" + Date.now();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (mode.value === "edge") {
|
function drawToData(img) {
|
||||||
const me = edgeAt(mineData, x, y);
|
const c = document.createElement("canvas");
|
||||||
const oe = edgeAt(origData, x, y);
|
c.width = W;
|
||||||
|
c.height = H;
|
||||||
|
const x = c.getContext("2d", { willReadFrequently: true });
|
||||||
|
x.drawImage(img, 0, 0, W, H);
|
||||||
|
return x.getImageData(0, 0, W, H);
|
||||||
|
}
|
||||||
|
|
||||||
const mineEdge = me > t;
|
async function refresh() {
|
||||||
const origEdge = oe > t;
|
if (refreshing) return;
|
||||||
|
refreshing = true;
|
||||||
|
ctx.fillStyle = "#00000088";
|
||||||
|
ctx.fillRect(0, 0, W, H);
|
||||||
|
ctx.fillStyle = "white";
|
||||||
|
let line = 30;
|
||||||
|
function log(text) {
|
||||||
|
ctx.fillText(text, 20, line);
|
||||||
|
line += 16;
|
||||||
|
}
|
||||||
|
log("fetching...");
|
||||||
|
|
||||||
if (mineEdge && origEdge) {
|
const start = Date.now();
|
||||||
// matching = green
|
const [mineImg, origImg] = await Promise.all([
|
||||||
o[i] = 0;
|
loadImage(MINE).finally(
|
||||||
o[i + 1] = 255;
|
() => (timingText.innerText = `${Date.now() - start}ms`),
|
||||||
o[i + 2] = 0;
|
),
|
||||||
o[i + 3] = 255;
|
loadImage(ORIG),
|
||||||
} else if (origEdge) {
|
]);
|
||||||
// original missing from mine = red
|
|
||||||
o[i] = 255;
|
log("loading...");
|
||||||
o[i + 1] = 0;
|
mineData = drawToData(mineImg);
|
||||||
o[i + 2] = 0;
|
if (!origData) origData = drawToData(origImg);
|
||||||
o[i + 3] = 255;
|
|
||||||
} else if (mineEdge) {
|
log("rendering...");
|
||||||
// extra in mine = yellow
|
render();
|
||||||
o[i] = 255;
|
refreshing = false;
|
||||||
o[i + 1] = 255;
|
}
|
||||||
o[i + 2] = 0;
|
|
||||||
o[i + 3] = 255;
|
function lum(d, i) {
|
||||||
|
return d[i] * 0.299 + d[i + 1] * 0.587 + d[i + 2] * 0.114;
|
||||||
|
}
|
||||||
|
|
||||||
|
function edgeAt(data, x, y) {
|
||||||
|
const d = data.data;
|
||||||
|
const i = (y * W + x) * 4;
|
||||||
|
|
||||||
|
const l = lum(d, i);
|
||||||
|
const lx = x > 0 ? lum(d, i - 4) : l;
|
||||||
|
const rx = x < W - 1 ? lum(d, i + 4) : l;
|
||||||
|
const uy = y > 0 ? lum(d, i - W * 4) : l;
|
||||||
|
const dy = y < H - 1 ? lum(d, i + W * 4) : l;
|
||||||
|
|
||||||
|
return Math.abs(rx - lx) + Math.abs(dy - uy);
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
if (!mineData || !origData) return;
|
||||||
|
|
||||||
|
const out = ctx.createImageData(W, H);
|
||||||
|
const o = out.data;
|
||||||
|
const m = mineData.data;
|
||||||
|
const r = origData.data;
|
||||||
|
const t = Number(threshold.value);
|
||||||
|
|
||||||
|
thresholdText.textContent = t;
|
||||||
|
|
||||||
|
for (let y = 0; y < H; y++) {
|
||||||
|
for (let x = 0; x < W; x++) {
|
||||||
|
const i = (y * W + x) * 4;
|
||||||
|
|
||||||
|
if (mode.value === "edge") {
|
||||||
|
const me = edgeAt(mineData, x, y);
|
||||||
|
const oe = edgeAt(origData, x, y);
|
||||||
|
|
||||||
|
const mineEdge = me > t;
|
||||||
|
const origEdge = oe > t;
|
||||||
|
|
||||||
|
if (mineEdge && origEdge) {
|
||||||
|
// matching = green
|
||||||
|
o[i] = 0;
|
||||||
|
o[i + 1] = 255;
|
||||||
|
o[i + 2] = 0;
|
||||||
|
o[i + 3] = 255;
|
||||||
|
} else if (origEdge) {
|
||||||
|
// original missing from mine = red
|
||||||
|
o[i] = 255;
|
||||||
|
o[i + 1] = 0;
|
||||||
|
o[i + 2] = 0;
|
||||||
|
o[i + 3] = 255;
|
||||||
|
} else if (mineEdge) {
|
||||||
|
// extra in mine = yellow
|
||||||
|
o[i] = 255;
|
||||||
|
o[i + 1] = 255;
|
||||||
|
o[i + 2] = 0;
|
||||||
|
o[i + 3] = 255;
|
||||||
|
} else {
|
||||||
|
o[i + 3] = 0;
|
||||||
|
}
|
||||||
|
} else if (mode.value === "diff") {
|
||||||
|
const dr = Math.abs(m[i] - r[i]);
|
||||||
|
const dg = Math.abs(m[i + 1] - r[i + 1]);
|
||||||
|
const db = Math.abs(m[i + 2] - r[i + 2]);
|
||||||
|
const diff = Math.max(dr, dg, db);
|
||||||
|
|
||||||
|
o[i] = diff;
|
||||||
|
o[i + 1] = diff;
|
||||||
|
o[i + 2] = diff;
|
||||||
|
o[i + 3] = diff > t ? 255 : 0;
|
||||||
} else {
|
} else {
|
||||||
o[i + 3] = 0;
|
o[i] = m[i]; // mine red-ish
|
||||||
|
o[i + 1] = r[i + 1]; // original green-ish
|
||||||
|
o[i + 2] = 0;
|
||||||
|
o[i + 3] = 220;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (mode.value === "diff") {
|
|
||||||
const dr = Math.abs(m[i] - r[i]);
|
|
||||||
const dg = Math.abs(m[i + 1] - r[i + 1]);
|
|
||||||
const db = Math.abs(m[i + 2] - r[i + 2]);
|
|
||||||
const diff = Math.max(dr, dg, db);
|
|
||||||
|
|
||||||
o[i] = diff;
|
|
||||||
o[i + 1] = diff;
|
|
||||||
o[i + 2] = diff;
|
|
||||||
o[i + 3] = diff > t ? 255 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
o[i] = m[i]; // mine red-ish
|
|
||||||
o[i + 1] = r[i + 1]; // original green-ish
|
|
||||||
o[i + 2] = 0;
|
|
||||||
o[i + 3] = 220;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx.putImageData(out, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.putImageData(out, 0, 0);
|
document.getElementById("refresh").onclick = refresh;
|
||||||
}
|
mode.oninput = render;
|
||||||
|
threshold.oninput = render;
|
||||||
|
|
||||||
document.getElementById("refresh").onclick = refresh;
|
refresh().catch((err) => {
|
||||||
mode.oninput = render;
|
console.error(err);
|
||||||
threshold.oninput = render;
|
ctx.fillStyle = "red";
|
||||||
|
ctx.fillText(String(err), 20, 30);
|
||||||
|
});
|
||||||
|
|
||||||
refresh().catch(err => {
|
window.addEventListener("keydown", (e) => {
|
||||||
console.error(err);
|
if (
|
||||||
ctx.fillStyle = "red";
|
e.key === "Enter" &&
|
||||||
ctx.fillText(String(err), 20, 30);
|
!["INPUT", "SELECT"].includes(e.target.tagName)
|
||||||
});
|
) {
|
||||||
|
e.preventDefault();
|
||||||
window.addEventListener("keydown", (e) => {
|
refresh();
|
||||||
if (e.key === "Enter" && !["INPUT", "SELECT"].includes(e.target.tagName)) {
|
}
|
||||||
e.preventDefault();
|
});
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// window.addEventListener("focus", () => {
|
|
||||||
// refresh();
|
|
||||||
// });
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
|
|
||||||
|
// window.addEventListener("focus", () => {
|
||||||
|
// refresh();
|
||||||
|
// });
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -55,7 +55,8 @@ export function endfieldCard(app: App) {
|
|||||||
params: t.Object({ uid: t.String() }),
|
params: t.Object({ uid: t.String() }),
|
||||||
detail: {
|
detail: {
|
||||||
tags: ["Endfield"],
|
tags: ["Endfield"],
|
||||||
description: "Preload data required to render an Endfield template card.",
|
description:
|
||||||
|
"Preload data required to render an Endfield template card.",
|
||||||
},
|
},
|
||||||
afterResponse: ({ log }) => log`Data preload complete`,
|
afterResponse: ({ log }) => log`Data preload complete`,
|
||||||
},
|
},
|
||||||
@@ -121,7 +122,8 @@ export function endfieldCard(app: App) {
|
|||||||
{ http: "http", card: "card" },
|
{ http: "http", card: "card" },
|
||||||
{
|
{
|
||||||
default: "http",
|
default: "http",
|
||||||
description: "Type of error to return when card fails to render.",
|
description:
|
||||||
|
"Type of error to return when card fails to render.",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
debug: t.Boolean({
|
debug: t.Boolean({
|
||||||
@@ -134,7 +136,8 @@ export function endfieldCard(app: App) {
|
|||||||
),
|
),
|
||||||
detail: {
|
detail: {
|
||||||
tags: ["Endfield"],
|
tags: ["Endfield"],
|
||||||
description: "Generate a placeholder Endfield profile card image from a UID.",
|
description:
|
||||||
|
"Generate a placeholder Endfield profile card image from a UID.",
|
||||||
},
|
},
|
||||||
afterResponse: ({ log }) => log`Rendering completed`,
|
afterResponse: ({ log }) => log`Rendering completed`,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -61,7 +61,9 @@ export function EndfieldTemplate({
|
|||||||
<div tw="text-[1.55em] leading-[1.15] font-semibold">
|
<div tw="text-[1.55em] leading-[1.15] font-semibold">
|
||||||
Route + Card Shell
|
Route + Card Shell
|
||||||
</div>
|
</div>
|
||||||
<div tw="text-white/60 mt-[.35em]">Ready for game-specific layout work</div>
|
<div tw="text-white/60 mt-[.35em]">
|
||||||
|
Ready for game-specific layout work
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div tw="rounded-[1em] border border-white/10 bg-black/20 p-[1.2em]">
|
<div tw="rounded-[1em] border border-white/10 bg-black/20 p-[1.2em]">
|
||||||
@@ -71,7 +73,9 @@ export function EndfieldTemplate({
|
|||||||
<div tw="text-[1.55em] leading-[1.15] font-semibold">
|
<div tw="text-[1.55em] leading-[1.15] font-semibold">
|
||||||
Character Grid
|
Character Grid
|
||||||
</div>
|
</div>
|
||||||
<div tw="text-white/60 mt-[.35em]">Replace placeholder sections with Endfield data</div>
|
<div tw="text-white/60 mt-[.35em]">
|
||||||
|
Replace placeholder sections with Endfield data
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -80,7 +84,8 @@ export function EndfieldTemplate({
|
|||||||
Signature
|
Signature
|
||||||
</div>
|
</div>
|
||||||
<div tw="text-[1.05em] leading-[1.45] text-white/82 whitespace-pre-wrap">
|
<div tw="text-[1.05em] leading-[1.45] text-white/82 whitespace-pre-wrap">
|
||||||
{signature?.trim() || "No signature available in the fetched payload yet."}
|
{signature?.trim() ||
|
||||||
|
"No signature available in the fetched payload yet."}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { mkdir, readdir, rm } from "node:fs/promises";
|
|||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
|
||||||
import { file, hash, write } from "bun";
|
import { file, hash, write } from "bun";
|
||||||
import type { ResolvedPlayerInfo} from "enka.ts";
|
import type { ResolvedPlayerInfo } from "enka.ts";
|
||||||
import { TTLCache } from "enka.ts";
|
import { TTLCache } from "enka.ts";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@@ -13,11 +13,9 @@ import type {
|
|||||||
EnkaNamecards,
|
EnkaNamecards,
|
||||||
EnkaPfps,
|
EnkaPfps,
|
||||||
RelicStore,
|
RelicStore,
|
||||||
WeaponStore} from "@/games/genshin/types/enka";
|
WeaponStore,
|
||||||
import {
|
|
||||||
ElementList
|
|
||||||
} from "@/games/genshin/types/enka";
|
} from "@/games/genshin/types/enka";
|
||||||
|
import { ElementList } from "@/games/genshin/types/enka";
|
||||||
import { Deduplicator } from "@/lib/dedupe";
|
import { Deduplicator } from "@/lib/dedupe";
|
||||||
import { createLogger } from "@/lib/logger";
|
import { createLogger } from "@/lib/logger";
|
||||||
|
|
||||||
@@ -210,10 +208,11 @@ export async function getEnkaStore<K extends keyof StoreTypeMap>(
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DEPRECATED: No longer needed
|
||||||
export function getAvatar(
|
export function getAvatar(
|
||||||
store: AvatarStore,
|
store: AvatarStore,
|
||||||
playerInfo: ResolvedPlayerInfo,
|
playerInfo: ResolvedPlayerInfo,
|
||||||
id: number,
|
id: string | number,
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
Object.entries(store).find(
|
Object.entries(store).find(
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { BaseAvatar } from "enka.ts";
|
import type { BaseAvatar } from "enka.ts";
|
||||||
import { distance } from "fastest-levenshtein";
|
import { distance } from "fastest-levenshtein";
|
||||||
|
|
||||||
function shortAvatarId(id: number) {
|
function shortAvatarId(id: string | number) {
|
||||||
// 10000120 -> 120
|
// 10000120 -> 120
|
||||||
return String(id).replace(/^10*/, "");
|
return String(id).replace(/^10*/, "").replace(/^-\d+/, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
function norm(s: string) {
|
function norm(s: string) {
|
||||||
@@ -60,5 +60,8 @@ function fuzzyNameMatch<T extends { avatar: BaseAvatar }>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function avatarAliases(name: string) {
|
function avatarAliases(name: string) {
|
||||||
return [name, ...name.split(" ")];
|
const aliases: Record<string, string[]> = {
|
||||||
|
Traveler: ["Aether", "Lumine"],
|
||||||
|
};
|
||||||
|
return [name, ...name.split(" "), ...(aliases[name] ?? [])];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import type { ResolvedAvatarInfo } from "enka.ts";
|
|||||||
import { AvatarElement } from "enka.ts";
|
import { AvatarElement } from "enka.ts";
|
||||||
|
|
||||||
import type { EnkaLocsLanguage } from "@/games/genshin/types/enka";
|
import type { EnkaLocsLanguage } from "@/games/genshin/types/enka";
|
||||||
import { FightPropMapEnum, PercentageFightProp } from "@/games/genshin/types/fightPropMap";
|
import {
|
||||||
|
FightPropMapEnum,
|
||||||
|
PercentageFightProp,
|
||||||
|
} from "@/games/genshin/types/fightPropMap";
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
interface DisplayRow {
|
interface DisplayRow {
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import type { Affix } from "@/games/genshin/types/enka";
|
import type { Affix } from "@/games/genshin/types/enka";
|
||||||
import { FightPropMapEnum, PercentageFightProp } from "@/games/genshin/types/fightPropMap";
|
import {
|
||||||
|
FightPropMapEnum,
|
||||||
|
PercentageFightProp,
|
||||||
|
} from "@/games/genshin/types/fightPropMap";
|
||||||
|
|
||||||
export type RollSummary = {
|
export type RollSummary = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import {
|
|||||||
} from "@/games/genshin/types/enka";
|
} from "@/games/genshin/types/enka";
|
||||||
import type { Logger } from "@/lib/logger";
|
import type { Logger } from "@/lib/logger";
|
||||||
import { arrRandom } from "@/lib/util";
|
import { arrRandom } from "@/lib/util";
|
||||||
|
|
||||||
import { CardResponse } from "@/v1/response";
|
import { CardResponse } from "@/v1/response";
|
||||||
|
|
||||||
export async function ListPage(
|
export async function ListPage(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { t } from "elysia";
|
import { t } from "elysia";
|
||||||
|
|
||||||
export type AvatarStore = Record<number, Avatar>;
|
export type AvatarStore = Record<string | number, Avatar>;
|
||||||
|
|
||||||
export interface Avatar {
|
export interface Avatar {
|
||||||
Element: ElementType;
|
Element: ElementType;
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,4 @@
|
|||||||
|
import { cors } from "@elysia/cors";
|
||||||
import openapi from "@elysiajs/openapi";
|
import openapi from "@elysiajs/openapi";
|
||||||
import { Elysia, t } from "elysia";
|
import { Elysia, t } from "elysia";
|
||||||
|
|
||||||
@@ -5,7 +6,6 @@ import { version } from "../package.json";
|
|||||||
import { pm } from "./lib/auth";
|
import { pm } from "./lib/auth";
|
||||||
import { createLogger } from "./lib/logger";
|
import { createLogger } from "./lib/logger";
|
||||||
import { v1routes } from "./v1";
|
import { v1routes } from "./v1";
|
||||||
import { cors } from '@elysia/cors'
|
|
||||||
|
|
||||||
const log = createLogger("APP");
|
const log = createLogger("APP");
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import { feature } from "bun:bundle";
|
import { feature } from "bun:bundle";
|
||||||
import type { InspectColor} from "node:util";
|
import type { InspectColor } from "node:util";
|
||||||
import { styleText } from "node:util";
|
import { styleText } from "node:util";
|
||||||
|
|
||||||
import { inspect } from "bun";
|
import { inspect } from "bun";
|
||||||
|
|||||||
+14
-14
@@ -10,19 +10,19 @@ export function v1routes(app: App) {
|
|||||||
.use(card)
|
.use(card)
|
||||||
// .use(endfieldCard)
|
// .use(endfieldCard)
|
||||||
.get(
|
.get(
|
||||||
"/status/:status",
|
"/status/:status",
|
||||||
async ({ params: { status }, set }) => {
|
async ({ params: { status }, set }) => {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`http://mts.dgnr.us:3999/__error/${status}/testerror.astrxl.dev`,
|
`http://mts.dgnr.us:3999/__error/${status}/testerror.astrxl.dev`,
|
||||||
);
|
);
|
||||||
set.headers["content-type"] = "text/html";
|
set.headers["content-type"] = "text/html";
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
params: t.Object({
|
params: t.Object({
|
||||||
status: t.RegExp(/^[1-5][0-9][0-9]$/),
|
status: t.RegExp(/^[1-5][0-9][0-9]$/),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
|
||||||
import type { MaybePromise} from "bun";
|
import type { MaybePromise } from "bun";
|
||||||
import { $, write } from "bun";
|
import { $, write } from "bun";
|
||||||
|
|
||||||
import { createLogger } from "@/lib/logger";
|
import { createLogger } from "@/lib/logger";
|
||||||
|
|||||||
Reference in New Issue
Block a user