diff --git a/js/quests/SideQuests.js b/js/quests/SideQuests.js index 1288a7a..c2bc658 100644 --- a/js/quests/SideQuests.js +++ b/js/quests/SideQuests.js @@ -1734,6 +1734,43 @@ export function parseSideQuestId(id) { return { systemId: m[1], worldKey: m[2].toLowerCase(), category: cat, slot: Math.floor(Number(m[4])) }; } +/** + * The COPY POOL a posting draws from (data/sidequests.json → descriptions). + * + * THREE-DEEP FALLBACK, fixed order (a board always reads something): + * 1. descriptions.faction.. — the FACTION'S OWN voice + * (apex / mindweave / zenith). Each of the nine pools is written in that + * faction's doctrine and names the rival it will actually fight through + * the {enemy} placeholder (the posting's seeded enemyFaction). + * 2. descriptions.faction. — the shared war-voice pool: the + * documented fallback for a faction with no pool of its own (a new + * power, or a hand-edited data file). + * 3. descriptions.neutral. — the neutral voice, last resort. + * + * A pool that stands but is not an array (a typo in the data) falls through + * to the next link rather than posting nothing. Pure in (context, category, + * faction): no RNG, so it never disturbs the posting's seeded draw. + * + * @param {object} ctx postingContext() shape ({ context: 'neutral'|'faction', … }) + * @param {string} cat the category id + * @param {string|null} [factionId] the board's faction id (a faction board) + * @returns {object[]} the variant pool ([] = no copy at all) + */ +function descriptionPool(ctx, cat, factionId = null) { + const fallbacks = []; + const own = String(factionId ?? '').trim(); + if (ctx?.context === 'faction' && own !== '') { + fallbacks.push(`sidequests.descriptions.faction.${own}.${cat}`); + } + fallbacks.push(`sidequests.descriptions.faction.${cat}`); + fallbacks.push(`sidequests.descriptions.neutral.${cat}`); + for (const path of fallbacks) { + const pool = config.get(path, null); + if (Array.isArray(pool)) return pool; + } + return []; +} + /** One posting from its pool variant + reward roll (null on a bad pool entry). */ function buildPosting(seed, rec, ctx, cat, slot, r, galaxy = null, usedDossier = null, world = 'p1') { const meta = sideQuestCategoryMeta(cat); @@ -1772,10 +1809,11 @@ function buildPosting(seed, rec, ctx, cat, slot, r, galaxy = null, usedDossier = if (!escortDestinationFor(seed, postingId, rec.id, galaxy)) return null; } - const poolPath = `sidequests.descriptions.${ctx.context}.${cat}`; - let pool = config.get(poolPath, null); - if (!Array.isArray(pool)) pool = config.get(`sidequests.descriptions.neutral.${cat}`, null); // the neutral voice as fallback - pool = Array.isArray(pool) ? pool : []; + // The copy pool: this faction's own pool when it has one (the three-deep + // fallback above), so an Apex board reads Apex and a Zenith board reads + // Zenith. The draw rides the posting's own seeded stream, exactly as the + // shared pool did — same seed, same slot, same variant index. + const pool = descriptionPool(ctx, cat, factionCtx ? rec.faction : null); let v = pool.length > 0 ? pool[r.int(0, pool.length - 1)] : null; // A WANTED LIST never posts the same leader twice on one board: the seeded // draw stands when it is fresh, otherwise the next entry forward (wrapping)