Show skill target modifiers in mission roller
This commit is contained in:
@@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user