diff --git a/src/components/MissionTab.jsx b/src/components/MissionTab.jsx index 34edb70..9e3f401 100644 --- a/src/components/MissionTab.jsx +++ b/src/components/MissionTab.jsx @@ -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}` : ''} + {skillBreakdown && ( +
+
+ Target Number + {skillBreakdown.target} +
+
+
Base {skillBreakdown.characteristic}: {skillBreakdown.base}
+
Training: {skillBreakdown.trainingLabel} ({skillBreakdown.training >= 0 ? '+' : ''}{skillBreakdown.training})
+
Scene modifier: {skillBreakdown.checkModifier >= 0 ? '+' : ''}{skillBreakdown.checkModifier}
+
Conditions: {skillBreakdown.conditionTotal >= 0 ? '+' : ''}{skillBreakdown.conditionTotal}
+
+ {skillBreakdown.conditionMods.length > 0 ? ( +
+ {skillBreakdown.conditionMods.map(mod => ( + + {mod.label} {mod.value ? `${mod.value > 0 ? '+' : ''}${mod.value}` : '+0'} + + ))} +
+ ) : ( +
No active condition modifiers for this skill check.
+ )} +
+ )} {selectedCheck.rulesNote && (
{selectedCheck.rulesNote}
)} @@ -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.'} )} -
- {selectedPlayer || 'Player'} wounds: {selectedPlayerWounds} -
{latestSkillRoll ? (
{rollSummary(latestSkillRoll)}