import { NPC_RELATIONS } from './data/npcTables.js'; import { RELATION_FORMULAS, AFFINITY_INIMITY_MAP, POWER_INFLUENCE_MAP, AFFINITY_LABELS, INIMITY_LABELS, POWER_LABELS, INFLUENCE_LABELS, SPECIAL_CHARACTERISTICS_TABLE, } from './data/allyEnemyTables.js'; export function mapRollToValue(roll, mapping) { return mapping[roll] ?? 0; } export function getLabel(value, labels) { return labels.find(l => l.value === Math.abs(value)) ?? labels[0]; } export function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); } async function rollFormula(formula) { const roll = await new Roll(formula).evaluate(); return { formula, total: roll.total }; } function getD66Entry(entries, total) { return entries.find(e => e.d66 === total) ?? null; } async function rollD66(entries) { const tens = await rollFormula('1d6'); const ones = await rollFormula('1d6'); const total = (tens.total * 10) + ones.total; return { total, tens: tens.total, ones: ones.total, entry: getD66Entry(entries, total), }; } async function rollAffinityInimity(relationKey) { const formulas = RELATION_FORMULAS[relationKey]; let affinityRoll = null; let inimityRoll = null; if (formulas.affinity !== '0') { affinityRoll = await rollFormula(formulas.affinity); } if (formulas.inimity !== '0') { inimityRoll = await rollFormula(formulas.inimity); } return { affinityValue: affinityRoll ? mapRollToValue(affinityRoll.total, AFFINITY_INIMITY_MAP) : 0, inimityValue: inimityRoll ? mapRollToValue(inimityRoll.total, AFFINITY_INIMITY_MAP) : 0, affinityRoll, inimityRoll, formulas, }; } async function resolveSpecialCharacteristics(currentRelationKey, depth = 0) { if (depth > 5) return []; const d66Result = await rollD66(SPECIAL_CHARACTERISTICS_TABLE); if (!d66Result.entry) return []; const entry = d66Result.entry; const result = { d66: d66Result.total, text: entry.text, effects: entry.effects, appliedDeltas: { affinity: 0, inimity: 0, power: 0, influence: 0 }, rerollNote: null, swapNote: null, narrativeText: entry.effects.action === 'narrativeOnly' ? entry.text : null, newRelationKey: null, subCharacteristics: [], }; if (entry.effects.affinityMod) result.appliedDeltas.affinity = entry.effects.affinityMod; if (entry.effects.inimityMod) result.appliedDeltas.inimity = entry.effects.inimityMod; if (entry.effects.powerMod) result.appliedDeltas.power = entry.effects.powerMod; if (entry.effects.influenceMod) result.appliedDeltas.influence = entry.effects.influenceMod; if (entry.effects.action === 'extraRolls') { const count = entry.effects.actionValue || 1; for (let i = 0; i < count; i++) { const extra = await resolveSpecialCharacteristics(currentRelationKey, depth + 1); result.subCharacteristics.push(...extra); } } return result; } export async function generateAllyEnemy(relationKey = 'contact', options = {}) { const relation = NPC_RELATIONS[relationKey]; let currentRelationKey = relationKey; const initial = await rollAffinityInimity(relationKey); let affinityValue = initial.affinityValue; let inimityValue = initial.inimityValue; let affinityRoll = initial.affinityRoll; let inimityRoll = initial.inimityRoll; let currentFormulas = initial.formulas; const powerRoll = await rollFormula('2d6'); const influenceRoll = await rollFormula('2d6'); let powerValue = mapRollToValue(powerRoll.total, POWER_INFLUENCE_MAP); let influenceValue = mapRollToValue(influenceRoll.total, POWER_INFLUENCE_MAP); let specialRoll = null; let specialCharacteristics = []; if (options.includeSpecial !== false) { specialRoll = await rollFormula('2d6'); if (specialRoll.total >= 8) { let queue = await resolveSpecialCharacteristics(currentRelationKey); while (queue.length > 0) { const sc = queue.shift(); if (sc.effects.action === 'extraRolls') { queue.push(...sc.subCharacteristics); } if (sc.effects.action === 'moderateRelation') { if (currentRelationKey === 'enemy') { currentRelationKey = 'rival'; const rerolled = await rollAffinityInimity(currentRelationKey); affinityValue = rerolled.affinityValue; inimityValue = rerolled.inimityValue; affinityRoll = rerolled.affinityRoll; inimityRoll = rerolled.inimityRoll; currentFormulas = rerolled.formulas; sc.newRelationKey = currentRelationKey; } else if (currentRelationKey === 'ally') { currentRelationKey = 'contact'; const rerolled = await rollAffinityInimity(currentRelationKey); affinityValue = rerolled.affinityValue; inimityValue = rerolled.inimityValue; affinityRoll = rerolled.affinityRoll; inimityRoll = rerolled.inimityRoll; currentFormulas = rerolled.formulas; sc.newRelationKey = currentRelationKey; } } if (sc.effects.action === 'intensifyRelation') { if (currentRelationKey === 'rival') { currentRelationKey = 'enemy'; const rerolled = await rollAffinityInimity(currentRelationKey); affinityValue = rerolled.affinityValue; inimityValue = rerolled.inimityValue; affinityRoll = rerolled.affinityRoll; inimityRoll = rerolled.inimityRoll; currentFormulas = rerolled.formulas; sc.newRelationKey = currentRelationKey; } else if (currentRelationKey === 'contact') { currentRelationKey = 'ally'; const rerolled = await rollAffinityInimity(currentRelationKey); affinityValue = rerolled.affinityValue; inimityValue = rerolled.inimityValue; affinityRoll = rerolled.affinityRoll; inimityRoll = rerolled.inimityRoll; currentFormulas = rerolled.formulas; sc.newRelationKey = currentRelationKey; } } if (sc.effects.action === 'reRollAffinity') { const reroll = await rollFormula('2d6'); const rerolledValue = mapRollToValue(reroll.total, AFFINITY_INIMITY_MAP); if (rerolledValue > affinityValue) { sc.rerollNote = `Affinité relancée : ${reroll.total} → ${rerolledValue} (était ${affinityValue})`; sc.appliedDeltas.affinity = rerolledValue - affinityValue; } } if (sc.effects.action === 'reRollInimity') { const reroll = await rollFormula('2d6'); const rerolledValue = mapRollToValue(reroll.total, AFFINITY_INIMITY_MAP); if (rerolledValue > inimityValue) { sc.rerollNote = `Inimitié relancée : ${reroll.total} → ${rerolledValue} (était ${inimityValue})`; sc.appliedDeltas.inimity = rerolledValue - inimityValue; } } if (sc.effects.action === 'swapAffinityInimity') { sc.swapNote = 'Affinité et Inimitié échangées'; const tmpAff = affinityValue; const tmpInim = inimityValue; sc.appliedDeltas.affinity = tmpInim - affinityValue; sc.appliedDeltas.inimity = tmpAff - inimityValue; } if (sc.effects.action === 'setPowerToZero') { sc.appliedDeltas.power = -powerValue; } if (sc.effects.action === 'createEnemy') { sc.narrativeText = 'Un nouvel Ennemi commun au Voyageur et à cet individu est créé.'; } if (sc.effects.action === 'createContactOrRival') { const net = affinityValue - inimityValue; sc.narrativeText = net > 0 ? 'Un nouveau Contact est créé (Affinité supérieure à l\'Inimitié).' : 'Un nouveau Rival est créé (Inimitié supérieure à l\'Affinité).'; } let newAffinity = affinityValue + (sc.appliedDeltas.affinity || 0); let newInimity = inimityValue + (sc.appliedDeltas.inimity || 0); let newPower = powerValue + (sc.appliedDeltas.power || 0); let newInfluence = influenceValue + (sc.appliedDeltas.influence || 0); affinityValue = clamp(newAffinity, 0, 6); inimityValue = clamp(newInimity, 0, 6); powerValue = clamp(newPower, 0, 6); influenceValue = clamp(newInfluence, 0, 6); specialCharacteristics.push(sc); } } } const finalRelation = currentRelationKey !== relationKey ? NPC_RELATIONS[currentRelationKey] : relation; const netScore = affinityValue - inimityValue; return { success: true, type: 'ally-enemy', relation: { key: currentRelationKey, label: finalRelation.label, summary: finalRelation.summary }, originalRelationKey: relationKey, relationChanged: currentRelationKey !== relationKey, affinity: { formula: currentFormulas.affinity, roll: affinityRoll?.total ?? 0, value: affinityValue, label: getLabel(affinityValue, AFFINITY_LABELS).label, description: getLabel(affinityValue, AFFINITY_LABELS).description, }, inimity: { formula: currentFormulas.inimity, roll: inimityRoll?.total ?? 0, value: inimityValue, label: getLabel(inimityValue, INIMITY_LABELS).label, description: getLabel(inimityValue, INIMITY_LABELS).description, }, netScore, power: { value: powerValue, label: getLabel(powerValue, POWER_LABELS).label, description: getLabel(powerValue, POWER_LABELS).description, }, influence: { value: influenceValue, label: getLabel(influenceValue, INFLUENCE_LABELS).label, description: getLabel(influenceValue, INFLUENCE_LABELS).description, }, specialRoll: specialRoll ? { roll: specialRoll.total, triggered: specialRoll.total >= 8 } : null, specialCharacteristics, }; }