mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-27 21:58:08 -05:00
Scanner kill feed event
This commit is contained in:
@@ -22,6 +22,8 @@ import {
|
||||
LOCALIZED_WEAPON_NAMES,
|
||||
} from "../../app/features/scanner/core/detectors/death/localized-messages";
|
||||
import { ALL_WEAPON_ENTRIES } from "../../app/features/scanner/core/detectors/death/weapon-names";
|
||||
import { KILL_MESSAGE_TEMPLATES } from "../../app/features/scanner/core/detectors/kill/localized-messages";
|
||||
import { KILL_TEXT_HEIGHT } from "../../app/features/scanner/core/detectors/kill/rois";
|
||||
import type { AtlasMeta } from "../../app/features/scanner/core/glyphs";
|
||||
import {
|
||||
ALL_LOBBY_ENTRIES,
|
||||
@@ -456,3 +458,18 @@ await build("death-tag-name", 42, [
|
||||
{ family: "BlitzBold", pxs: [53, 54], chars: nameCharset() },
|
||||
{ family: "Rowdy", pxs: [52, 54], chars: nameCharset() },
|
||||
]);
|
||||
// kill feed: the "Splatted <name>!" rows bottom-center, BlitzMain with ~24px
|
||||
// caps (kill/rois.ts) — the scoreboard-names charset plus every language's row text
|
||||
const killFeedTexts = KILL_MESSAGE_TEMPLATES.flatMap((t) => [t.pre, t.post]);
|
||||
await build("kill-feed", KILL_TEXT_HEIGHT, [
|
||||
{
|
||||
family: "BlitzMain",
|
||||
pxs: [28, 29],
|
||||
chars: [
|
||||
...nameCharset(),
|
||||
...NAME_GREEK,
|
||||
...nameSymbols("BlitzMain"),
|
||||
...localizedChars(killFeedTexts, "BlitzMain"),
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -13,11 +13,14 @@
|
||||
* LayoutMsg/Mng_Result_00 replay-browser VICTORY / DEFEAT tags
|
||||
* LayoutMsg/VS_Beaten_00 (999) death-burst message; the weapon placeholder sits on
|
||||
* line 1 or 2 by language, so it becomes a per-language template
|
||||
* LayoutMsg/VS_BeatMessage_00 (000) kill-feed row ("Splatted <name>!"); the name placeholder
|
||||
* splits it into a per-language pre/post text pair
|
||||
* CommonMsg/Weapon/WeaponName_* weapon names, mapped to canonical entries via USen
|
||||
*
|
||||
* Usage: pnpm scanner:build-localized-entries [path-to-splat3]
|
||||
* Writes app/features/scanner/core/localized-entries.ts
|
||||
* and app/features/scanner/core/detectors/death/localized-messages.ts
|
||||
* and app/features/scanner/core/detectors/kill/localized-messages.ts
|
||||
*/
|
||||
import { readdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
@@ -38,6 +41,10 @@ const OUT_MESSAGES = new URL(
|
||||
"../../app/features/scanner/core/detectors/death/localized-messages.ts",
|
||||
import.meta.url,
|
||||
).pathname;
|
||||
const OUT_KILL_MESSAGES = new URL(
|
||||
"../../app/features/scanner/core/detectors/kill/localized-messages.ts",
|
||||
import.meta.url,
|
||||
).pathname;
|
||||
|
||||
const CANONICAL_LANG = "USen";
|
||||
const misc = gameMisc as Record<string, string>;
|
||||
@@ -270,6 +277,30 @@ for (const lang of languages) {
|
||||
});
|
||||
}
|
||||
|
||||
interface KillTemplate {
|
||||
langs: string[];
|
||||
pre: string;
|
||||
post: string;
|
||||
}
|
||||
|
||||
// the name placeholder is the first group; KRko carries a second (a particle
|
||||
// chosen by the name's final syllable) that clean() drops along with markup
|
||||
const killTemplates: KillTemplate[] = [];
|
||||
for (const lang of languages) {
|
||||
const raw = dumps.get(lang)!["LayoutMsg/VS_BeatMessage_00"]!["000"]!;
|
||||
const line = raw.replace(PLACEHOLDER, SENTINEL).replace(/\[[^\]]*\]/g, "");
|
||||
if (!line.includes(SENTINEL) || line.includes("\n"))
|
||||
throw new Error(
|
||||
`${lang}: kill message is not one line with a name: ${raw}`,
|
||||
);
|
||||
const [pre, post] = line
|
||||
.split(SENTINEL)
|
||||
.map((s) => s.replace(/[ \t]+/g, " ")) as [string, string];
|
||||
const existing = killTemplates.find((t) => t.pre === pre && t.post === post);
|
||||
if (existing) existing.langs.push(lang);
|
||||
else killTemplates.push({ langs: [lang], pre, post });
|
||||
}
|
||||
|
||||
const WEAPON_MSGS = [
|
||||
"CommonMsg/Weapon/WeaponName_Main",
|
||||
"CommonMsg/Weapon/WeaponName_Sub",
|
||||
@@ -410,11 +441,37 @@ export const LOCALIZED_WEAPON_NAMES: Readonly<
|
||||
`,
|
||||
);
|
||||
|
||||
writeFileSync(
|
||||
OUT_KILL_MESSAGES,
|
||||
`${banner(
|
||||
`Per-language kill-feed row templates: the "Splatted <name>!" row
|
||||
* wraps the splatted player's name in language-specific text on either side
|
||||
* (spaces kept as rendered; either side may be empty).`,
|
||||
)}
|
||||
|
||||
export interface KillMessageTemplate {
|
||||
/** languages sharing this exact template */
|
||||
langs: readonly string[];
|
||||
/** constant text before the name (may end with a space, or be empty) */
|
||||
pre: string;
|
||||
/** constant text after the name (may start with a space, or be empty) */
|
||||
post: string;
|
||||
}
|
||||
|
||||
export const KILL_MESSAGE_TEMPLATES: readonly KillMessageTemplate[] = ${JSON.stringify(
|
||||
killTemplates,
|
||||
null,
|
||||
2,
|
||||
)};
|
||||
`,
|
||||
);
|
||||
|
||||
console.info(
|
||||
`localized-entries: ${languages.length} languages, ` +
|
||||
`${languageEntries.reduce((n, l) => n + l.stages.length, 0)} stage strings`,
|
||||
);
|
||||
console.info(
|
||||
`localized-messages: ${templates.length} death templates, ` +
|
||||
`${Object.values(localizedWeaponNames).reduce((n, e) => n + e.length, 0)} localized weapon names`,
|
||||
`${Object.values(localizedWeaponNames).reduce((n, e) => n + e.length, 0)} localized weapon names, ` +
|
||||
`${killTemplates.length} kill templates`,
|
||||
);
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
*/
|
||||
import { loadOpenCV, type Mat } from "../../app/features/scanner/core/cv";
|
||||
import * as death from "../../app/features/scanner/core/detectors/death/rois";
|
||||
import * as kill from "../../app/features/scanner/core/detectors/kill/rois";
|
||||
import * as mapStart from "../../app/features/scanner/core/detectors/map-start/rois";
|
||||
import * as minimap from "../../app/features/scanner/core/detectors/minimap/rois";
|
||||
import { TIMER_DIGIT_ROI } from "../../app/features/scanner/core/detectors/objective/rois";
|
||||
import * as sb from "../../app/features/scanner/core/detectors/scoreboard/rois";
|
||||
import * as bl from "../../app/features/scanner/core/detectors/scoreboard-battle-log/rois";
|
||||
import * as replay from "../../app/features/scanner/core/detectors/scoreboard-battle-log-replay/rois";
|
||||
@@ -22,7 +24,7 @@ const [imagePath, outPath = "roi-overlay.png", detector = "scoreboard"] =
|
||||
process.argv.slice(2);
|
||||
if (!imagePath) {
|
||||
console.error(
|
||||
"usage: vite-node -c scripts/scanner/vite-node.config.ts scripts/scanner/overlay-rois.ts <image> [out.png] [scoreboard|scoreboard-battle-log-replay|scoreboard-battle-log|death|map-start|minimap]",
|
||||
"usage: vite-node -c scripts/scanner/vite-node.config.ts scripts/scanner/overlay-rois.ts <image> [out.png] [scoreboard|scoreboard-battle-log-replay|scoreboard-battle-log|death|kill|map-start|minimap]",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -105,6 +107,13 @@ if (detector === "scoreboard") {
|
||||
for (const roi of [...death.GATE_BURST_PROBES, ...death.GATE_PANEL_PROBES]) {
|
||||
rect(frame, roi, [255, 255, 0]);
|
||||
}
|
||||
} else if (detector === "kill") {
|
||||
for (let row = 0; row < kill.MAX_ROWS; row++) {
|
||||
rect(frame, kill.textRoi(row), [0, 255, 0]);
|
||||
rect(frame, kill.skullRoi(row), [0, 128, 255]);
|
||||
for (const roi of kill.darkProbes(row)) rect(frame, roi, [255, 255, 0]);
|
||||
}
|
||||
rect(frame, TIMER_DIGIT_ROI, [255, 0, 0]);
|
||||
} else if (detector === "map-start") {
|
||||
rect(frame, mapStart.MODE_LABEL_ROI, [0, 255, 0]);
|
||||
rect(frame, mapStart.MODE_BLOCK_ROI, [255, 0, 0]);
|
||||
|
||||
@@ -9,6 +9,10 @@ import {
|
||||
createDeathDetector,
|
||||
type DeathData,
|
||||
} from "../../app/features/scanner/core/detectors/death/index";
|
||||
import {
|
||||
createKillDetector,
|
||||
type KillData,
|
||||
} from "../../app/features/scanner/core/detectors/kill/index";
|
||||
import {
|
||||
createMapStartDetector,
|
||||
type MapStartData,
|
||||
@@ -523,3 +527,70 @@ for (const config of configs) {
|
||||
for (const m of misses) console.info(` ${m}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Kill fixtures: the feed's stacked rows (names, newest first) and the timer.
|
||||
{
|
||||
const detector = createKillDetector(resources);
|
||||
const fixtures = loadFixtures("kill");
|
||||
const tally = {
|
||||
gate: { ok: 0, total: 0 } as Tally,
|
||||
time: { ok: 0, total: 0 } as Tally,
|
||||
rows: { ok: 0, total: 0 } as Tally,
|
||||
names: { ok: 0, total: 0 } as Tally,
|
||||
};
|
||||
let charEdits = 0;
|
||||
let charTotal = 0;
|
||||
const misses: string[] = [];
|
||||
|
||||
for (const fixture of fixtures) {
|
||||
const { gate, events } = await runDetectorOnFixture<KillData>(
|
||||
detector,
|
||||
fixture,
|
||||
);
|
||||
const expectPositive = fixture.expected.event === "Kill";
|
||||
tally.gate.total++;
|
||||
if (gate.pass === expectPositive) tally.gate.ok++;
|
||||
if (!expectPositive || !events[0]) continue;
|
||||
const event = events[0];
|
||||
const expected = fixture.expected.data ?? {};
|
||||
if (expected.time !== undefined) {
|
||||
tally.time.total++;
|
||||
if (event.data.time === expected.time) tally.time.ok++;
|
||||
else
|
||||
misses.push(
|
||||
`${fixture.name}: time ${event.data.time} != ${expected.time}`,
|
||||
);
|
||||
}
|
||||
if (expected.names !== undefined) {
|
||||
tally.rows.total++;
|
||||
if (event.data.names.length === expected.names.length) tally.rows.ok++;
|
||||
else
|
||||
misses.push(
|
||||
`${fixture.name}: ${event.data.names.length} rows != ${expected.names.length}`,
|
||||
);
|
||||
for (const [row, want] of expected.names.entries()) {
|
||||
if (want === null) continue;
|
||||
tally.names.total++;
|
||||
const got = event.data.names[row] ?? "";
|
||||
const dist = editDistance(got, want);
|
||||
charEdits += dist;
|
||||
charTotal += want.length;
|
||||
if (dist === 0) tally.names.ok++;
|
||||
else misses.push(`${fixture.name}: row ${row} "${got}" != "${want}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.info(`\n=== kill (${fixtures.length} fixtures) ===`);
|
||||
console.info(`gate ${pct(tally.gate)}`);
|
||||
console.info(`time ${pct(tally.time)}`);
|
||||
console.info(`rows ${pct(tally.rows)}`);
|
||||
console.info(`names ${pct(tally.names)}`);
|
||||
console.info(
|
||||
`name CER ${charTotal ? ((100 * charEdits) / charTotal).toFixed(2) : "n/a"}% (${charEdits} edits / ${charTotal} chars)`,
|
||||
);
|
||||
if (misses.length > 0) {
|
||||
console.info("misses:");
|
||||
for (const m of misses) console.info(` ${m}`);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user