Show skill target modifiers in mission roller

This commit is contained in:
2026-06-30 08:18:09 +02:00
parent b6cff53b8e
commit a08afbaf0f

View File

@@ -161,6 +161,13 @@ function skillTrainingBonus(skill) {
return -20;
}
function skillTrainingLabel(skill) {
if (skill?.plus20) return '+20 trained';
if (skill?.plus10) return '+10 trained';
if (skill?.trained) return '+0 trained';
return '-20 untrained';
}
function matchingSheetSkill(skills, check) {
if (!skills || !check) return null;
const wanted = String(check.skill || check.name || '').toLowerCase();
@@ -179,6 +186,32 @@ function woundsLabel(wounds) {
return `${current}/${total} W${fatigue}`;
}
function conditionSkillModifiers(conditions = [], check = {}) {
const mods = [];
const skill = String(check.skill || check.name || '').toLowerCase();
if (conditions.includes('Fatigued')) {
mods.push({ label: 'Fatigued', value: -10, note: '-10 to tests' });
}
if (conditions.includes('Knocked Down') && skill.includes('dodge')) {
mods.push({ label: 'Knocked Down', value: -20, note: '-20 to Dodge while prone/knocked down' });
}
if (conditions.includes('Pinned')) {
mods.push({ label: 'Pinned', value: 0, note: 'No general skill modifier; limits aggressive action and affects shooting' });
}
if (conditions.includes('Grappled')) {
mods.push({ label: 'Grappled', value: 0, note: 'Only relevant grapple/escape actions are normally available' });
}
if (conditions.includes('Bleeding')) {
mods.push({ label: 'Bleeding', value: 0, note: 'No direct skill modifier; ongoing danger remains' });
}
if (conditions.includes('Stunned')) {
mods.push({ label: 'Stunned', value: 0, note: 'Normally cannot take regular actions until recovered' });
}
return mods;
}
function rollSummary(roll) {
if (!roll) return '';
const payload = roll.payload || {};
@@ -869,16 +902,31 @@ export default function MissionTab({ authedPlayer }) {
return playerSheets[name]?.tabInfo || {};
}
function targetForPlayerCheck(playerName, check) {
function targetBreakdownForPlayerCheck(playerName, check, scene) {
const tab = playerSheet(playerName);
const chars = tab.characteristics || {};
const skill = matchingSheetSkill(tab.skills || {}, check);
const characteristic = check.characteristic || 'Int';
if (!Object.keys(chars).length) {
return clampTarget((check.target || 45) + (check.modifier || 0));
}
const base = characteristicValue(chars, characteristic, check.target || 45);
return clampTarget(base + skillTrainingBonus(skill) + (check.modifier || 0));
const conditionNames = (scene?.combatState?.conditions || {})[playerName] || [];
const conditionMods = conditionSkillModifiers(conditionNames, check);
const conditionTotal = conditionMods.reduce((sum, item) => sum + Number(item.value || 0), 0);
const checkModifier = Number(check.modifier || 0);
const training = Object.keys(chars).length ? skillTrainingBonus(skill) : 0;
const base = Object.keys(chars).length
? characteristicValue(chars, characteristic, check.target || 45)
: (check.target || 45);
const target = clampTarget(base + training + checkModifier + conditionTotal);
return {
target,
base,
characteristic,
training,
trainingLabel: Object.keys(chars).length ? skillTrainingLabel(skill) : 'sheet missing',
checkModifier,
conditionMods,
conditionTotal,
conditions: conditionNames,
};
}
function hasMiniSkillRolled(sceneIndex, scene, playerName, check) {
@@ -902,7 +950,8 @@ export default function MissionTab({ authedPlayer }) {
if (!check || !state.player) return;
if (!scene?.rollsUnlocked && hasMiniSkillRolled(sceneIndex, scene, state.player, check)) return;
const feedSceneIndex = isGM ? sceneIndex : activeSceneIndex;
const target = targetForPlayerCheck(state.player, check);
const targetBreakdown = targetBreakdownForPlayerCheck(state.player, check, scene);
const target = targetBreakdown.target;
const roll = d100();
const result = degrees(target, roll);
const rollLocks = {
@@ -920,6 +969,7 @@ export default function MissionTab({ authedPlayer }) {
sceneTitle: titleForScene(scene, sceneIndex),
mini: true,
check,
targetBreakdown,
target,
roll,
...result,
@@ -1079,7 +1129,8 @@ export default function MissionTab({ authedPlayer }) {
const initiatives = Array.isArray(combatStateForScene.initiatives) ? combatStateForScene.initiatives : [];
const selectedCheck = checks.find(check => check.name === state.checkName) || checks[0];
const selectedPlayer = state.player || (isGM ? playerList[0] : authedPlayer);
const skillTarget = selectedCheck && selectedPlayer ? targetForPlayerCheck(selectedPlayer, selectedCheck) : null;
const skillBreakdown = selectedCheck && selectedPlayer ? targetBreakdownForPlayerCheck(selectedPlayer, selectedCheck, scene) : null;
const skillTarget = skillBreakdown?.target ?? null;
const skillAlreadyRolled = Boolean(selectedCheck && selectedPlayer && hasMiniSkillRolled(sceneIndex, scene, selectedPlayer, selectedCheck));
const skillLocked = skillAlreadyRolled && !scene?.rollsUnlocked;
const feedSceneIndex = isGM ? sceneIndex : activeSceneIndex;
@@ -1149,6 +1200,31 @@ export default function MissionTab({ authedPlayer }) {
{' '}({selectedCheck.characteristic || 'Characteristic'})
{selectedCheck.modifier ? ` · ${selectedCheck.modifier > 0 ? '+' : ''}${selectedCheck.modifier}` : ''}
</div>
{skillBreakdown && (
<div className="mt-2 rounded border border-cyan-900/50 bg-cyan-950/20 p-2">
<div className="flex items-center justify-between gap-2">
<span className="font-semibold uppercase tracking-wide text-cyan-200">Target Number</span>
<span className="text-lg font-bold tabular-nums text-cyan-100">{skillBreakdown.target}</span>
</div>
<div className="mt-1 grid gap-1 text-slate-400">
<div>Base {skillBreakdown.characteristic}: <span className="text-slate-200">{skillBreakdown.base}</span></div>
<div>Training: <span className="text-slate-200">{skillBreakdown.trainingLabel} ({skillBreakdown.training >= 0 ? '+' : ''}{skillBreakdown.training})</span></div>
<div>Scene modifier: <span className="text-slate-200">{skillBreakdown.checkModifier >= 0 ? '+' : ''}{skillBreakdown.checkModifier}</span></div>
<div>Conditions: <span className="text-slate-200">{skillBreakdown.conditionTotal >= 0 ? '+' : ''}{skillBreakdown.conditionTotal}</span></div>
</div>
{skillBreakdown.conditionMods.length > 0 ? (
<div className="mt-2 flex flex-wrap gap-1">
{skillBreakdown.conditionMods.map(mod => (
<span key={`${mod.label}-${mod.value}-${mod.note}`} className="rounded bg-slate-800 px-1.5 py-0.5 text-[11px] text-slate-300" title={mod.note}>
{mod.label} {mod.value ? `${mod.value > 0 ? '+' : ''}${mod.value}` : '+0'}
</span>
))}
</div>
) : (
<div className="mt-2 text-[11px] text-slate-500">No active condition modifiers for this skill check.</div>
)}
</div>
)}
{selectedCheck.rulesNote && (
<div className="mt-1 text-slate-400">{selectedCheck.rulesNote}</div>
)}
@@ -1170,9 +1246,6 @@ export default function MissionTab({ authedPlayer }) {
{scene?.rollsUnlocked ? 'GM unlocked extra rolls for this scene.' : 'Locked: 1 skill roll per player for this check.'}
</div>
)}
<div className="rounded border border-slate-800 bg-slate-900/60 px-2 py-1 text-xs text-slate-400">
{selectedPlayer || 'Player'} wounds: <span className="text-slate-200">{selectedPlayerWounds}</span>
</div>
{latestSkillRoll ? (
<div className={`rounded px-2 py-1 text-xs ${latestSkillRoll.payload?.success ? 'bg-emerald-950/40 text-emerald-200' : 'bg-rose-950/40 text-rose-200'}`}>
{rollSummary(latestSkillRoll)}