278 lines
9.7 KiB
JavaScript
278 lines
9.7 KiB
JavaScript
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,
|
|
};
|
|
}
|