feat: rules midgame priority system, mission enhancements, and requisition shop improvements
- Add midgame priority ranking for rules (midgamePriority field) - Enhance rules search to support midgame filtering and sorting - Improve mission simulation and midgame tracking - Fix RequisitionShop player name handling - Add rules midplay simulation tests - Update MissionTab with midgame scene tracking - Add backend logging for rules updates
This commit is contained in:
@@ -12956,3 +12956,168 @@ Connected to MongoDB
|
||||
[2026-06-23T13:17:14.755Z] MariaDB: Error getting all weapons {"message":"Table 'deathwatch.weapons' doesn't exist","code":"ER_NO_SUCH_TABLE","errno":1146,"sql":"SELECT * FROM weapons ORDER BY id","sqlState":"42S02","sqlMessage":"Table 'deathwatch.weapons' doesn't exist"}
|
||||
[2026-06-23T13:17:14.768Z] MariaDB: Error getting all bestiary {"message":"Table 'deathwatch.bestiary' doesn't exist","code":"ER_NO_SUCH_TABLE","errno":1146,"sql":"SELECT * FROM bestiary ORDER BY id","sqlState":"42S02","sqlMessage":"Table 'deathwatch.bestiary' doesn't exist"}
|
||||
[2026-06-23T13:33:33.058Z] MariaDB: Tables created successfully
|
||||
[2026-06-23T21:19:35.818Z] SESSION: Session validation successful gm
|
||||
[2026-06-23T21:19:35.872Z] API: Fetch player gm success
|
||||
[2026-06-23T21:19:45.935Z] MariaDB: Updated mission progress 9
|
||||
[2026-06-23T21:19:52.803Z] MariaDB: Updated mission progress 9
|
||||
[2026-06-23T21:23:05.809Z] Session deleted: session_gm_1782248674070_qhcsas
|
||||
[2026-06-23T21:23:05.811Z] SESSION: Logout session_gm_1782248674070_qhcsas
|
||||
[2026-06-23T21:23:09.669Z] Session created: session_anders_1782249789618_j7t7ao
|
||||
[2026-06-23T21:23:09.671Z] API: Player login anders success
|
||||
[2026-06-23T21:23:09.694Z] API: Fetch player anders success
|
||||
[2026-06-23T21:23:09.699Z] SESSION: Session validation successful anders
|
||||
[2026-06-23T21:23:09.718Z] API: Fetch player anders success
|
||||
[2026-06-23T21:31:19.816Z] SESSION: Session validation successful anders
|
||||
[2026-06-23T21:31:19.842Z] API: Fetch player anders success
|
||||
[2026-06-23T21:42:44.161Z] SESSION: Session validation successful anders
|
||||
[2026-06-23T21:42:44.190Z] API: Fetch player anders success
|
||||
[2026-06-23T21:43:01.779Z] MariaDB: Updated mission progress 9
|
||||
[2026-06-23T21:43:02.800Z] MariaDB: Updated mission progress 9
|
||||
[2026-06-23T21:43:26.760Z] MariaDB: Updated mission progress 9
|
||||
[2026-06-23T21:43:28.139Z] MariaDB: Updated mission progress 9
|
||||
[2026-06-23T21:51:15.547Z] MariaDB: Error creating tables {"code":"ER_ACCESS_DENIED_ERROR","errno":1045,"sqlState":"28000","sqlMessage":"Access denied for user 'deathwatch'@'unifi' (using password: YES)"}
|
||||
[2026-06-23T21:51:24.282Z] API: Fetch all players (public) 5
|
||||
[2026-06-23T21:51:29.207Z] API: Fetch player anders success
|
||||
[2026-06-23T21:51:34.975Z] API: Fetch player anders success
|
||||
[2026-06-23T21:51:35.043Z] API: Fetch player christoffer success
|
||||
[2026-06-23T21:51:35.092Z] API: Fetch player claes success
|
||||
[2026-06-23T21:51:35.143Z] API: Fetch player phillip success
|
||||
[2026-06-23T22:13:55.577Z] API: Fetch all players (public) 5
|
||||
[2026-06-23T22:28:05.676Z] API: Fetch all players (public) 5
|
||||
[2026-06-23T22:28:05.682Z] SESSION: Session validation successful anders
|
||||
[2026-06-23T22:28:05.707Z] API: Fetch player anders success
|
||||
[2026-06-23T22:28:24.067Z] API: Fetch all players (public) 5
|
||||
[2026-06-23T22:28:24.073Z] SESSION: Session validation successful anders
|
||||
[2026-06-23T22:28:24.096Z] API: Fetch player anders success
|
||||
[2026-06-23T22:28:26.641Z] Session deleted: session_anders_1782249789618_j7t7ao
|
||||
[2026-06-23T22:28:26.643Z] SESSION: Logout session_anders_1782249789618_j7t7ao
|
||||
[2026-06-23T22:28:50.408Z] Session created: session_gm_1782253730353_zuylf6
|
||||
[2026-06-23T22:28:50.410Z] API: Player login gm success
|
||||
[2026-06-23T22:28:50.438Z] API: Fetch player gm success
|
||||
[2026-06-23T22:28:50.445Z] SESSION: Session validation successful gm
|
||||
[2026-06-23T22:28:50.472Z] API: Fetch player gm success
|
||||
[2026-06-23T22:29:21.031Z] MariaDB: Updated mission progress 9
|
||||
[2026-06-23T22:29:29.169Z] MariaDB: Error getting all bestiary {"message":"Table 'deathwatch.bestiary' doesn't exist","code":"ER_NO_SUCH_TABLE","errno":1146,"sql":"SELECT * FROM bestiary ORDER BY id","sqlState":"42S02","sqlMessage":"Table 'deathwatch.bestiary' doesn't exist"}
|
||||
[2026-06-23T22:29:29.171Z] MariaDB: Error getting all weapons {"message":"Table 'deathwatch.weapons' doesn't exist","code":"ER_NO_SUCH_TABLE","errno":1146,"sql":"SELECT * FROM weapons ORDER BY id","sqlState":"42S02","sqlMessage":"Table 'deathwatch.weapons' doesn't exist"}
|
||||
[2026-06-23T22:30:35.715Z] API: Fetch all players (public) 5
|
||||
[2026-06-23T22:30:40.608Z] API: Fetch player anders success
|
||||
[2026-06-23T22:30:48.559Z] API: Fetch all players (public) 5
|
||||
[2026-06-23T22:30:52.186Z] API: Fetch player anders success
|
||||
[2026-06-23T22:31:04.535Z] API: Fetch player anders success
|
||||
[2026-06-23T22:31:04.599Z] API: Fetch player christoffer success
|
||||
[2026-06-23T22:31:04.689Z] API: Fetch player claes success
|
||||
[2026-06-23T22:31:04.754Z] API: Fetch player phillip success
|
||||
[2026-06-23T22:37:40.170Z] MariaDB: Tables created successfully
|
||||
[2026-06-23T22:37:44.798Z] API: Fetch player anders success
|
||||
[2026-06-23T22:37:50.139Z] API: Fetch player christoffer success
|
||||
[2026-06-23T22:40:12.754Z] API: Fetch all players (public) 5
|
||||
[2026-06-23T22:40:17.142Z] API: Fetch player anders success
|
||||
[2026-06-23T22:42:05.342Z] API: Fetch player anders success
|
||||
[2026-06-23T22:42:05.417Z] API: Fetch player christoffer success
|
||||
[2026-06-23T22:42:05.507Z] API: Fetch player claes success
|
||||
[2026-06-23T22:42:05.587Z] API: Fetch player phillip success
|
||||
[2026-06-23T22:51:32.054Z] API: Fetch player anders success
|
||||
[2026-06-23T22:51:41.076Z] API: Fetch player anders success
|
||||
[2026-06-23T22:51:41.132Z] API: Fetch player christoffer success
|
||||
[2026-06-23T22:51:41.186Z] API: Fetch player claes success
|
||||
[2026-06-23T22:51:41.247Z] API: Fetch player phillip success
|
||||
[2026-06-24T08:13:37.788Z] MariaDB: Error creating tables {"code":"ER_ACCESS_DENIED_ERROR","errno":1045,"sqlState":"28000","sqlMessage":"Access denied for user 'deathwatch'@'unifi' (using password: YES)"}
|
||||
[2026-06-24T08:22:57.268Z] API: Fetch all players (public) 5
|
||||
[2026-06-24T08:23:16.201Z] MariaDB: Updated mission progress 9
|
||||
[2026-06-24T08:23:16.425Z] MariaDB: Updated mission progress 9
|
||||
[2026-06-24T08:25:06.391Z] MariaDB: Tables created successfully
|
||||
[2026-06-24T08:25:34.126Z] MariaDB: Tables created successfully
|
||||
[2026-06-24T08:26:39.167Z] MariaDB: Tables created successfully
|
||||
[2026-06-24T08:27:43.791Z] MariaDB: Tables created successfully
|
||||
[2026-06-24T08:30:55.243Z] MariaDB: Tables created successfully
|
||||
[2026-06-24T08:31:05.243Z] Simulation #1 completed: The Hunt for Fabius Bile — VICTORY
|
||||
[2026-06-24T08:31:12.631Z] Simulation #2 completed: The Hunt for Fabius Bile — VICTORY
|
||||
[2026-06-24T08:31:12.901Z] Simulation #3 completed: The Hunt for Fabius Bile — PARTIAL_FAILURE
|
||||
[2026-06-24T08:31:49.070Z] API: Fetch all players (public) 5
|
||||
[2026-06-24T08:31:49.074Z] SESSION: Session validation successful gm
|
||||
[2026-06-24T08:31:49.208Z] API: Fetch player gm success
|
||||
[2026-06-24T08:31:50.273Z] API: Fetch all players (public) 5
|
||||
[2026-06-24T08:32:40.599Z] API: Fetch all players (public) 5
|
||||
[2026-06-24T08:33:05.674Z] API: Fetch player anders success
|
||||
[2026-06-24T08:33:25.244Z] API: Fetch all players (public) 5
|
||||
[2026-06-24T08:33:27.960Z] MariaDB: Created player andreas
|
||||
[2026-06-24T08:33:27.961Z] API: Created player andreas success
|
||||
[2026-06-24T08:33:32.571Z] API: Fetch all players (public) 6
|
||||
[2026-06-24T08:33:56.731Z] API: Fetch player andreas success
|
||||
[2026-06-24T08:34:05.777Z] MariaDB: Updated player andreas
|
||||
[2026-06-24T08:34:05.779Z] API: Updated player andreas success
|
||||
[2026-06-24T08:34:12.700Z] API: Fetch player andreas success
|
||||
[2026-06-24T08:35:01.308Z] API: Fetch all players (public) 6
|
||||
[2026-06-24T08:35:10.183Z] Simulation #4 completed: The Hunt for Fabius Bile — VICTORY
|
||||
[2026-06-24T08:35:10.211Z] API: Fetch all players (public) 6
|
||||
[2026-06-24T08:35:21.222Z] Simulation #5 completed: The Hunt for Fabius Bile — VICTORY
|
||||
[2026-06-24T08:35:21.254Z] API: Fetch all players (public) 6
|
||||
[2026-06-24T08:38:11.460Z] MariaDB: Updated player andreas
|
||||
[2026-06-24T08:38:11.464Z] API: Updated player andreas success
|
||||
[2026-06-24T08:38:20.757Z] API: Fetch player andreas success
|
||||
[2026-06-24T08:47:21.904Z] MariaDB: Updated player anders
|
||||
[2026-06-24T08:47:21.910Z] API: Updated player anders success
|
||||
[2026-06-24T08:47:35.419Z] MariaDB: Updated player andreas
|
||||
[2026-06-24T08:47:35.421Z] API: Updated player andreas success
|
||||
[2026-06-24T08:47:53.448Z] MariaDB: Updated player christoffer
|
||||
[2026-06-24T08:47:53.450Z] API: Updated player christoffer success
|
||||
[2026-06-24T08:48:09.440Z] MariaDB: Updated player claes
|
||||
[2026-06-24T08:48:09.442Z] API: Updated player claes success
|
||||
[2026-06-24T08:48:29.596Z] MariaDB: Updated player phillip
|
||||
[2026-06-24T08:48:29.598Z] API: Updated player phillip success
|
||||
[2026-06-24T08:48:36.934Z] API: Fetch player anders success
|
||||
[2026-06-24T08:48:36.980Z] API: Fetch player andreas success
|
||||
[2026-06-24T08:48:37.026Z] API: Fetch player christoffer success
|
||||
[2026-06-24T08:48:37.083Z] API: Fetch player claes success
|
||||
[2026-06-24T08:48:37.144Z] API: Fetch player phillip success
|
||||
[2026-06-24T08:50:32.259Z] MariaDB: Updated player andreas
|
||||
[2026-06-24T08:50:32.263Z] API: Updated player andreas success
|
||||
[2026-06-24T08:50:41.668Z] API: Fetch player andreas success
|
||||
[2026-06-24T08:51:03.555Z] MariaDB: Updated player andreas
|
||||
[2026-06-24T08:51:03.556Z] API: Updated player andreas success
|
||||
[2026-06-24T08:51:09.123Z] API: Fetch player andreas success
|
||||
[2026-06-24T08:52:00.746Z] Simulation #6 completed: The Hunt for Fabius Bile — VICTORY
|
||||
[2026-06-24T08:52:10.863Z] Simulation #7 completed: The Hunt for Fabius Bile — VICTORY
|
||||
[2026-06-24T08:55:01.129Z] API: Fetch all players (public) 6
|
||||
[2026-06-24T08:55:01.154Z] SESSION: Session validation successful gm
|
||||
[2026-06-24T08:55:01.179Z] API: Fetch player gm success
|
||||
[2026-06-24T08:56:18.671Z] API: Fetch all players (public) 6
|
||||
[2026-06-24T08:56:22.973Z] Simulation #8 completed: The Hunt for Fabius Bile — VICTORY
|
||||
[2026-06-24T08:56:23.004Z] API: Fetch all players (public) 6
|
||||
[2026-06-24T08:56:45.435Z] Simulation #9 completed: The Hunt for Fabius Bile — VICTORY
|
||||
[2026-06-24T08:56:45.467Z] API: Fetch all players (public) 6
|
||||
[2026-06-24T08:56:52.641Z] Simulation #10 completed: The Hunt for Fabius Bile — VICTORY
|
||||
[2026-06-24T08:56:52.675Z] API: Fetch all players (public) 6
|
||||
[2026-06-24T09:00:12.185Z] API: Fetch all players (public) 6
|
||||
[2026-06-24T09:00:22.010Z] MariaDB: Updated mission progress 9
|
||||
[2026-06-24T09:02:33.620Z] MariaDB: Tables created successfully
|
||||
[2026-06-24T09:03:16.974Z] Simulation #11 completed: The Hunt for Fabius Bile — PARTIAL_FAILURE
|
||||
[2026-06-24T09:03:53.171Z] MariaDB: Tables created successfully
|
||||
[2026-06-24T09:03:59.717Z] Simulation #12 completed: The Hunt for Fabius Bile — VICTORY
|
||||
[2026-06-24T09:04:00.015Z] Simulation #13 completed: The Hunt for Fabius Bile — VICTORY
|
||||
[2026-06-24T09:04:00.514Z] Simulation #14 completed: The Hunt for Fabius Bile — PARTIAL_FAILURE
|
||||
[2026-06-24T09:04:00.826Z] Simulation #15 completed: The Hunt for Fabius Bile — PARTIAL_FAILURE
|
||||
[2026-06-24T09:04:01.082Z] Simulation #16 completed: The Hunt for Fabius Bile — PARTIAL_FAILURE
|
||||
[2026-06-24T09:04:33.097Z] API: Fetch all players (public) 6
|
||||
[2026-06-24T09:04:33.132Z] SESSION: Session validation successful gm
|
||||
[2026-06-24T09:04:33.159Z] API: Fetch player gm success
|
||||
[2026-06-24T09:04:34.550Z] API: Fetch all players (public) 6
|
||||
[2026-06-24T09:04:43.038Z] Simulation #17 completed: The Hunt for Fabius Bile — VICTORY
|
||||
[2026-06-24T09:04:43.072Z] API: Fetch all players (public) 6
|
||||
[2026-06-24T09:04:48.507Z] Simulation #18 completed: The Hunt for Fabius Bile — VICTORY
|
||||
[2026-06-24T09:04:48.538Z] API: Fetch all players (public) 6
|
||||
[2026-06-24T09:09:43.564Z] API: Fetch all players (public) 6
|
||||
[2026-06-24T09:09:51.120Z] SESSION: Session validation successful gm
|
||||
[2026-06-24T09:09:51.130Z] API: Fetch all players (public) 6
|
||||
[2026-06-24T09:09:51.144Z] API: Fetch player gm success
|
||||
[2026-06-24T09:09:51.795Z] API: Fetch all players (public) 6
|
||||
[2026-06-24T09:09:51.798Z] SESSION: Session validation successful gm
|
||||
[2026-06-24T09:09:51.821Z] API: Fetch player gm success
|
||||
[2026-06-24T09:09:51.976Z] API: Fetch all players (public) 6
|
||||
[2026-06-24T09:09:51.987Z] SESSION: Session validation successful gm
|
||||
[2026-06-24T09:09:52.010Z] API: Fetch player gm success
|
||||
[2026-06-24T09:16:50.410Z] API: Fetch all players (public) 6
|
||||
[2026-06-24T09:16:50.423Z] SESSION: Session validation successful gm
|
||||
[2026-06-24T09:16:50.443Z] API: Fetch player gm success
|
||||
[2026-06-24T09:17:13.311Z] MariaDB: Updated mission progress 9
|
||||
[2026-06-24T10:07:25.442Z] API: Fetch all players (public) 6
|
||||
|
||||
@@ -6,6 +6,19 @@ const router = express.Router();
|
||||
|
||||
console.log('Rules routes registered (MariaDB)');
|
||||
|
||||
const rulesJsonPath = path.join(__dirname, '..', 'rules', 'rules-database.json');
|
||||
|
||||
function parseMaybeJson(value, fallback = []) {
|
||||
if (Array.isArray(value)) return value;
|
||||
if (!value) return fallback;
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return Array.isArray(parsed) ? parsed : fallback;
|
||||
} catch (e) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
// Helpers to clean up OCR/extracted text
|
||||
function cleanText(s) {
|
||||
if (!s) return s;
|
||||
@@ -32,45 +45,75 @@ function cleanTitle(t) {
|
||||
return s;
|
||||
}
|
||||
|
||||
// Fetch all rules from MariaDB
|
||||
function normalizeRule(r) {
|
||||
const ruleId = r.rule_id || r.ruleId || r.id;
|
||||
return {
|
||||
id: ruleId || r.id,
|
||||
numericId: Number.isInteger(r.id) ? r.id : undefined,
|
||||
rule_id: ruleId,
|
||||
title: cleanTitle(r.title),
|
||||
summary: cleanText(r.summary || ''),
|
||||
content: cleanText(r.content),
|
||||
page: r.page,
|
||||
pageEnd: r.page_end || r.pageEnd || r.page,
|
||||
source: r.source,
|
||||
sourceAbbr: r.source_abbr || r.sourceAbbr,
|
||||
category: r.category,
|
||||
tags: parseMaybeJson(r.tags),
|
||||
aliases: parseMaybeJson(r.aliases),
|
||||
relatedRules: parseMaybeJson(r.related_rules || r.relatedRules),
|
||||
sourceMethod: r.source_method || r.sourceMethod || 'unknown',
|
||||
confidence: r.confidence == null ? null : Number(r.confidence),
|
||||
midgamePriority: r.midgame_priority || r.midgamePriority || 0
|
||||
};
|
||||
}
|
||||
|
||||
function getRulesFromJson() {
|
||||
try {
|
||||
if (!fs.existsSync(rulesJsonPath)) return [];
|
||||
const data = JSON.parse(fs.readFileSync(rulesJsonPath, 'utf8'));
|
||||
return (data.rules || []).map(normalizeRule);
|
||||
} catch (e) {
|
||||
console.error('Failed to read rules JSON fallback:', e);
|
||||
logToFile('Error reading rules JSON fallback:', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function mergeRules(dbRules, jsonRules) {
|
||||
const byId = new Map();
|
||||
for (const rule of dbRules || []) {
|
||||
byId.set(String(rule.rule_id || rule.id), rule);
|
||||
}
|
||||
for (const rule of jsonRules || []) {
|
||||
byId.set(String(rule.rule_id || rule.id), rule);
|
||||
}
|
||||
return Array.from(byId.values());
|
||||
}
|
||||
|
||||
// Fetch all rules from MariaDB and merge in the generated JSON midgame database.
|
||||
async function getAllRules() {
|
||||
try {
|
||||
const rows = await rulesHelpers.getAll();
|
||||
return rows.map(r => ({
|
||||
id: r.id,
|
||||
rule_id: r.rule_id,
|
||||
title: cleanTitle(r.title),
|
||||
content: cleanText(r.content),
|
||||
page: r.page,
|
||||
source: r.source,
|
||||
sourceAbbr: r.source_abbr,
|
||||
category: r.category
|
||||
}));
|
||||
return mergeRules(rows && rows.length ? rows.map(normalizeRule) : [], getRulesFromJson());
|
||||
} catch (e) {
|
||||
console.error('Failed to read rules from MariaDB:', e);
|
||||
logToFile('Error getting all rules:', e);
|
||||
return [];
|
||||
return getRulesFromJson();
|
||||
}
|
||||
}
|
||||
|
||||
async function getRuleById(ruleId) {
|
||||
try {
|
||||
const rows = await rulesHelpers.getAll();
|
||||
const row = rows.find(r => r.rule_id === ruleId || r.id === ruleId);
|
||||
const sourceRows = mergeRules(rows && rows.length ? rows.map(normalizeRule) : [], getRulesFromJson());
|
||||
const row = sourceRows.find(r => String(r.rule_id) === String(ruleId) || String(r.id) === String(ruleId) || String(r.numericId) === String(ruleId));
|
||||
if (!row) return null;
|
||||
return {
|
||||
id: row.rule_id || row.id,
|
||||
title: cleanTitle(row.title),
|
||||
content: cleanText(row.content),
|
||||
page: row.page,
|
||||
source: row.source,
|
||||
sourceAbbr: row.source_abbr,
|
||||
category: row.category
|
||||
};
|
||||
return row;
|
||||
} catch (e) {
|
||||
console.error('Failed to read rule by id:', e);
|
||||
logToFile('Error getting rule by id:', ruleId, e);
|
||||
return null;
|
||||
return getRulesFromJson().find(r => String(r.rule_id) === String(ruleId) || String(r.id) === String(ruleId)) || null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +122,9 @@ router.get('/categories', async (req, res) => {
|
||||
try {
|
||||
const rows = await getAllRules();
|
||||
const categories = [...new Set(rows.map(r => r.category).filter(Boolean))];
|
||||
const categoryList = categories.map(cat => ({ id: cat, name: cleanTitle(cat) }));
|
||||
const categoryList = categories
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map(cat => ({ id: cat, name: cleanTitle(cat.replace(/_/g, ' ')) }));
|
||||
res.json([{ id: 'all', name: 'All Rules' }, ...categoryList]);
|
||||
} catch (error) {
|
||||
console.error('Categories error:', error);
|
||||
@@ -90,41 +135,82 @@ router.get('/categories', async (req, res) => {
|
||||
// Search rules (MariaDB)
|
||||
router.get('/search', async (req, res) => {
|
||||
try {
|
||||
const { q: query, category, limit = 20 } = req.query;
|
||||
const { q: query, category, tag, source, midgame, limit = 20 } = req.query;
|
||||
if (!query || !query.trim()) return res.json([]);
|
||||
const limitInt = Math.max(1, parseInt(limit) || 20);
|
||||
|
||||
const allRules = await getAllRules();
|
||||
const term = query.toLowerCase();
|
||||
const term = query.trim().toLowerCase();
|
||||
const tokens = term.split(/\s+/).filter(Boolean);
|
||||
|
||||
const includesTerm = (value, t = term) => String(value || '').toLowerCase().includes(t);
|
||||
const arrayIncludes = (arr, t = term) => (arr || []).some(v => includesTerm(v, t));
|
||||
const tokenMatch = (rule) => {
|
||||
const haystacks = [
|
||||
rule.title,
|
||||
rule.summary,
|
||||
rule.content,
|
||||
rule.category,
|
||||
...(rule.tags || []),
|
||||
...(rule.aliases || [])
|
||||
].map(v => String(v || '').toLowerCase());
|
||||
return tokens.every(token => haystacks.some(v => v.includes(token)));
|
||||
};
|
||||
|
||||
const score = (rule, t) => {
|
||||
const title = (rule.title || '').toLowerCase();
|
||||
const summary = (rule.summary || '').toLowerCase();
|
||||
const content = (rule.content || '').toLowerCase();
|
||||
const tags = (rule.tags || []).map(v => String(v).toLowerCase());
|
||||
const aliases = (rule.aliases || []).map(v => String(v).toLowerCase());
|
||||
const exactTitle = title === t;
|
||||
const titleStarts = title.startsWith(t);
|
||||
const titleContains = title.includes(t);
|
||||
const exactAlias = aliases.includes(t);
|
||||
const aliasContains = aliases.some(a => a.includes(t));
|
||||
const tagContains = tags.some(a => a.includes(t));
|
||||
const summaryContains = summary.includes(t);
|
||||
const isCanonical = rule.source === 'Core Rulebook';
|
||||
const tokenHits = tokens.reduce((sum, token) => {
|
||||
return sum +
|
||||
(title.includes(token) ? 8 : 0) +
|
||||
(aliases.some(a => a.includes(token)) ? 6 : 0) +
|
||||
(tags.some(a => a.includes(token)) ? 4 : 0) +
|
||||
(summary.includes(token) ? 3 : 0) +
|
||||
(content.includes(token) ? 1 : 0);
|
||||
}, 0);
|
||||
// Higher = shown first
|
||||
return (exactTitle ? 100 : 0) +
|
||||
(exactAlias ? 90 : 0) +
|
||||
(titleStarts ? 50 : 0) +
|
||||
(titleContains ? 20 : 0) +
|
||||
(aliasContains ? 18 : 0) +
|
||||
(tagContains ? 15 : 0) +
|
||||
(summaryContains ? 8 : 0) +
|
||||
(isCanonical ? 10 : 0) +
|
||||
(Number(rule.midgamePriority) || 0) +
|
||||
tokenHits +
|
||||
(content.includes(t) ? 1 : 0);
|
||||
};
|
||||
|
||||
let filtered = allRules.filter(rule => {
|
||||
const titleMatch = rule.title && rule.title.toLowerCase().includes(term);
|
||||
const contentMatch = rule.content && rule.content.toLowerCase().includes(term);
|
||||
const titleMatch = includesTerm(rule.title);
|
||||
const summaryMatch = includesTerm(rule.summary);
|
||||
const contentMatch = includesTerm(rule.content);
|
||||
const tagMatch = arrayIncludes(rule.tags);
|
||||
const aliasMatch = arrayIncludes(rule.aliases);
|
||||
const categoryMatch = !category || category === 'all' || rule.category === category;
|
||||
return (titleMatch || contentMatch) && categoryMatch;
|
||||
const sourceMatch = !source || source === 'all' || rule.sourceAbbr === source || rule.source === source;
|
||||
const midgameMatch = !midgame || midgame === 'false' || Number(rule.midgamePriority || 0) >= 70;
|
||||
const explicitTagMatch = !tag || tag === 'all' || (rule.tags || []).some(t => String(t).toLowerCase() === String(tag).toLowerCase());
|
||||
return (titleMatch || summaryMatch || contentMatch || tagMatch || aliasMatch || tokenMatch(rule)) && categoryMatch && sourceMatch && midgameMatch && explicitTagMatch;
|
||||
});
|
||||
|
||||
// If category filtering yielded no results, try without category filter
|
||||
if (filtered.length === 0 && category && category !== 'all') {
|
||||
filtered = allRules.filter(rule => {
|
||||
const titleMatch = rule.title && rule.title.toLowerCase().includes(term);
|
||||
const contentMatch = rule.content && rule.content.toLowerCase().includes(term);
|
||||
return titleMatch || contentMatch;
|
||||
const sourceMatch = !source || source === 'all' || rule.sourceAbbr === source || rule.source === source;
|
||||
return sourceMatch && (includesTerm(rule.title) || includesTerm(rule.summary) || includesTerm(rule.content) || arrayIncludes(rule.tags) || arrayIncludes(rule.aliases) || tokenMatch(rule));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -157,7 +243,7 @@ router.get('/rule/:id', async (req, res) => {
|
||||
// Get random rules for discovery
|
||||
router.get('/random', async (req, res) => {
|
||||
try {
|
||||
const { count = 5, category } = req.query;
|
||||
const { count = 5, category, midgame } = req.query;
|
||||
const max = Math.max(1, parseInt(count) || 5);
|
||||
|
||||
const allRules = await getAllRules();
|
||||
@@ -166,6 +252,9 @@ router.get('/random', async (req, res) => {
|
||||
if (category && category !== 'all') {
|
||||
filtered = allRules.filter(rule => rule.category === category);
|
||||
}
|
||||
if (midgame && midgame !== 'false') {
|
||||
filtered = filtered.filter(rule => Number(rule.midgamePriority || 0) >= 70);
|
||||
}
|
||||
|
||||
// Shuffle and pick random rules
|
||||
const shuffled = filtered.sort(() => 0.5 - Math.random());
|
||||
@@ -186,6 +275,8 @@ router.get('/stats', async (req, res) => {
|
||||
|
||||
const categoryCount = {};
|
||||
const sourceCount = {};
|
||||
const methodCount = {};
|
||||
let midgameRules = 0;
|
||||
|
||||
allRules.forEach(rule => {
|
||||
if (rule.category) {
|
||||
@@ -194,12 +285,17 @@ router.get('/stats', async (req, res) => {
|
||||
if (rule.source) {
|
||||
sourceCount[rule.source] = (sourceCount[rule.source] || 0) + 1;
|
||||
}
|
||||
if (rule.sourceMethod) {
|
||||
methodCount[rule.sourceMethod] = (methodCount[rule.sourceMethod] || 0) + 1;
|
||||
}
|
||||
if (Number(rule.midgamePriority || 0) >= 70) midgameRules += 1;
|
||||
});
|
||||
|
||||
const categories = Object.entries(categoryCount).map(([category, c]) => ({ category, c }));
|
||||
const sources = Object.entries(sourceCount).map(([source, c]) => ({ source, c }));
|
||||
const sourceMethods = Object.entries(methodCount).map(([sourceMethod, c]) => ({ sourceMethod, c }));
|
||||
|
||||
res.json({ totalRules, categories, sources, searchTerms: 0 });
|
||||
res.json({ totalRules, midgameRules, categories, sources, sourceMethods, searchTerms: 0 });
|
||||
} catch (error) {
|
||||
console.error('Stats error:', error);
|
||||
res.status(500).json({ error: 'Failed to get stats' });
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user