feat: fetch roles at runtime and prepare solver

This commit is contained in:
2026-05-31 23:25:48 +02:00
parent 3968e775cc
commit 64b60b08a7
11 changed files with 151 additions and 30 deletions

2
.gitignore vendored
View File

@@ -1,2 +1,4 @@
node_modules/ node_modules/
dist/ dist/
src/generated/roles.ts
.env

View File

@@ -3,8 +3,8 @@
"version": "1.0.0", "version": "1.0.0",
"description": "Tweaks for WoV Assassins Convention", "description": "Tweaks for WoV Assassins Convention",
"scripts": { "scripts": {
"build": "esbuild src/content.ts src/popup.ts src/background.ts --bundle --outdir=dist --platform=browser --target=chrome120", "build": "node scripts/build.mjs",
"watch": "esbuild src/content.ts src/popup.ts src/background.ts --bundle --outdir=dist --platform=browser --target=chrome120 --watch", "watch": "node scripts/build.mjs --watch",
"dev": "pnpm watch" "dev": "pnpm watch"
}, },
"devDependencies": { "devDependencies": {

63
scripts/build.mjs Normal file
View File

@@ -0,0 +1,63 @@
import * as esbuild from "esbuild";
import { execSync } from "child_process";
import { writeFileSync, mkdirSync, readFileSync } from "fs";
// read .env file is present
try {
for (const line of readFileSync(".env", "utf8").split("\n")) {
const [key, ...rest] = line.split("=");
if (key && rest.length) {
process.env[key.trim()] = rest.join("=").trim();
}
}
} catch {}
const apiKey = process.env.WOV_API_KEY;
if (!process.env.WOV_API_KEY) {
throw new Error("WOV_API_KEY is not set");
}
let revision;
try { revision = execSync("git rev-parse HEAD").toString().trim(); }
catch { throw new Error("failed to get git revision"); }
// Fetch roles
console.log("fetching roles... ");
const res = await fetch("https://api.wolvesville.com/roles", { headers: { "Authorization": `Bot ${apiKey}` } });
const data = await res.json();
if (!res.ok) {
throw new Error(`roles API error ${res.status}: ${JSON.stringify(data)}`);
}
if (!Array.isArray(data.roles) || data.roles.length === 0) {
throw new Error(`unexpected roles response: ${JSON.stringify(data)}`);
}
if (data.roles.some(r => typeof r.name !== "string")) {
throw new Error("some roles are missing a name field");
}
const code = `// Auto generated by scripts/build.mjs\nexport const ROLES: string[] = ${JSON.stringify(data.roles.map(r => r.name), null, 4)};\n`;
mkdirSync("src/generated", { recursive: true });
writeFileSync("src/generated/roles.ts", code);
console.log("done.");
// Bundle
const options = {
entryPoints: ["src/content.ts", "src/popup.ts", "src/background.ts"],
bundle: true,
outdir: "dist",
platform: "browser",
target: "chrome120",
define: {
__GIT_REVISION__: JSON.stringify(revision),
__GITEA_API__: JSON.stringify("https://git.pihkaal.me/api/v1/repos/pihkaal/wov-assassins-convention-tweaks"),
},
};
if (process.argv.includes("--watch")) {
const ctx = await esbuild.context(options);
await ctx.watch();
console.log("watching...");
} else {
await esbuild.build(options);
}

View File

@@ -26,3 +26,24 @@ chrome.runtime.onMessage.addListener((message: SimulateClickMessage) => {
}); });
}); });
}); });
const checkForUpdate = async (): Promise<void> => {
try {
const res = await fetch(`${__GITEA_API__}/branches/main`);
const data = await res.json();
const latest: string = data.commit.id;
const needsUpdate = latest !== __GIT_REVISION__;
await chrome.storage.local.set({ needsUpdate });
if (needsUpdate) {
chrome.action.setBadgeText({ text: "!" });
chrome.action.setBadgeBackgroundColor({ color: "#ef4444" });
} else {
chrome.action.setBadgeText({ text: "" });
}
} catch {}
};
chrome.runtime.onStartup.addListener(checkForUpdate);
chrome.runtime.onInstalled.addListener(checkForUpdate);

View File

@@ -35,14 +35,14 @@ const onKeyDown = (event: KeyboardEvent): void => {
const init = async (): Promise<void> => { const init = async (): Promise<void> => {
const result = await chrome.storage.local.get(STORAGE_KEY); const result = await chrome.storage.local.get(STORAGE_KEY);
const state = result[STORAGE_KEY] as State | undefined; const state = result[STORAGE_KEY] as State | undefined;
enabled = state?.enabled ?? true; enabled = state?.keybinds ?? true;
document.addEventListener("keydown", onKeyDown, { capture: true }); document.addEventListener("keydown", onKeyDown, { capture: true });
chrome.storage.onChanged.addListener((changes) => { chrome.storage.onChanged.addListener((changes) => {
if (STORAGE_KEY in changes) { if (STORAGE_KEY in changes) {
const next = changes[STORAGE_KEY].newValue as State; const next = changes[STORAGE_KEY].newValue as State;
enabled = next.enabled; enabled = next.keybinds;
} }
}); });
}; };

2
src/env.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
declare const __GIT_REVISION__: string;
declare const __GITEA_API__: string;

0
src/generated/.gitkeep Normal file
View File

View File

@@ -16,17 +16,24 @@ h1 {
font-size: 0.95rem; font-size: 0.95rem;
font-weight: 700; font-weight: 700;
letter-spacing: 0.02em; letter-spacing: 0.02em;
margin-bottom: 6px;
}
p {
font-size: 0.75rem;
color: #94a3b8;
line-height: 1.4;
margin-bottom: 16px; margin-bottom: 16px;
} }
#toggle { #update-banner {
font-family: monospace;
font-size: 0.8rem;
font-weight: 700;
color: #fbbf24;
margin-bottom: 12px;
}
#tweaks {
display: flex;
flex-direction: column;
gap: 8px;
}
.row {
width: 100%; width: 100%;
padding: 9px; padding: 9px;
border: none; border: none;
@@ -36,14 +43,16 @@ p {
cursor: pointer; cursor: pointer;
letter-spacing: 0.05em; letter-spacing: 0.05em;
transition: background 0.15s; transition: background 0.15s;
font-family: monospace;
text-align: left;
} }
#toggle.on { .row.on {
background: #22c55e; background: #22c55e;
color: #fff; color: #fff;
} }
#toggle.off { .row.off {
background: #3f3f46; background: #3f3f46;
color: #94a3b8; color: #94a3b8;
} }

View File

@@ -2,12 +2,13 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>WoV Tweaks</title> <title>WAC</title>
<link rel="stylesheet" href="popup.css"> <link rel="stylesheet" href="popup.css">
</head> </head>
<body> <body>
<h1>Assassins Convention Tweaks</h1> <h1>Assassins Convention Tweaks</h1>
<button id="toggle"></button> <div id="update-banner" style="display:none">[!] Update available — reinstall the extension</div>
<div id="tweaks"></div>
<script src="../dist/popup.js"></script> <script src="../dist/popup.js"></script>
</body> </body>
</html> </html>

View File

@@ -1,34 +1,56 @@
import { State, STORAGE_KEY } from "./types"; import { State, STORAGE_KEY } from "./types";
const DEFAULT_STATE: State = { const defaultState: State = {
enabled: true, keybinds: true,
solver: true,
}; };
const getState = async (): Promise<State> => { const getState = async (): Promise<State> => {
const result = await chrome.storage.local.get(STORAGE_KEY); const result = await chrome.storage.local.get(STORAGE_KEY);
return (result[STORAGE_KEY] as State | undefined) ?? DEFAULT_STATE; return (result[STORAGE_KEY] as State | undefined) ?? defaultState;
}; };
const setState = async (state: State): Promise<void> => { const setState = async (state: State): Promise<void> => {
await chrome.storage.local.set({ [STORAGE_KEY]: state }); await chrome.storage.local.set({ [STORAGE_KEY]: state });
}; };
const render = (btn: HTMLButtonElement, enabled: boolean): void => { const createRow = (label: string, initial: boolean, onChange: (next: boolean) => void): HTMLButtonElement => {
btn.textContent = enabled ? "ON" : "OFF"; let on = initial;
btn.className = enabled ? "on" : "off"; const btn = document.createElement("button");
const render = () => {
btn.textContent = `[${on ? "ON" : "OFF"}] ${label}`;
btn.className = `row ${on ? "on" : "off"}`;
};
render();
btn.addEventListener("click", () => {
on = !on;
render();
onChange(on);
});
return btn;
}; };
const init = async (): Promise<void> => { const init = async (): Promise<void> => {
const btn = document.getElementById("toggle") as HTMLButtonElement; const container = document.getElementById("tweaks")!;
const state = await getState(); const state = await getState();
render(btn, state.enabled);
btn.addEventListener("click", async () => { const { needsUpdate } = await chrome.storage.local.get("needsUpdate");
if (needsUpdate) {
document.getElementById("update-banner")!.style.display = "block";
}
container.appendChild(createRow("Keybinds", state.keybinds, async (next) => {
const current = await getState(); const current = await getState();
const next = { enabled: !current.enabled }; await setState({ ...current, keybinds: next });
await setState(next); }));
render(btn, next.enabled);
}); container.appendChild(createRow("Solver", state.solver, async (next) => {
const current = await getState();
await setState({ ...current, solver: next });
}));
}; };
document.addEventListener("DOMContentLoaded", init); document.addEventListener("DOMContentLoaded", init);

View File

@@ -1,5 +1,6 @@
export type State = { export type State = {
enabled: boolean; keybinds: boolean;
solver: boolean;
}; };
export const STORAGE_KEY = "wov_assassins_convention_tweaks_state"; export const STORAGE_KEY = "wov_assassins_convention_tweaks_state";