Files
mgt2-compendium-amiral-denisov/scripts/allyEnemyGenerator.js
T
uberwald a53c7ace53
Release Creation / build (release) Successful in 43s
Ready for release
2026-06-12 20:53:44 +02:00

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,
};
}