diff --git a/src/components/DeathwatchRoller.jsx b/src/components/DeathwatchRoller.jsx index 479b72f..69b6d47 100755 --- a/src/components/DeathwatchRoller.jsx +++ b/src/components/DeathwatchRoller.jsx @@ -99,6 +99,34 @@ const STORAGE_HISTORY = 'dw:history:v2' const STORAGE_WEAPONS = 'dw:weapons:v3' const STORAGE_TRACKER = 'dw:tracker:v1' +// Condition → attack modifier (Deathwatch core rules) +const CONDITION_MODS = { + Pinned: { bs: -20, ws: 0, label: 'Pinned (–20 BS)' }, + Stunned: { bs: -20, ws: -20, label: 'Stunned (–20)' }, + 'Knocked Down': { bs: -20, ws: -20, label: 'Knocked Down (–20)' }, + Fatigued: { bs: -10, ws: -10, label: 'Fatigued (–10)' }, + Grappled: { bs: 0, ws: -20, label: 'Grappled (–20 WS, ranged blocked)' }, + Bleeding: { bs: 0, ws: 0, label: 'Bleeding (1 wound/turn)' }, +} + +function mapPlayerWeapon(w) { + const type = String(w.type || w.class || '').toLowerCase() + const clsMap = { heavy: 'heavy', pistol: 'pistol', basic: 'basic', melee: 'melee', bolt: 'basic', bolter: 'basic' } + const cls = clsMap[type] || (type.includes('pistol') ? 'pistol' : type.includes('melee') || type.includes('sword') || type.includes('axe') || type.includes('fist') || type.includes('hammer') ? 'melee' : 'basic') + const modesDefault = cls === 'melee' ? ['single'] : cls === 'pistol' ? ['single'] : cls === 'heavy' ? ['single'] : ['single', 'semi'] + return normalizeWeapon({ + name: w.name, + class: cls, + damage: w.damage || '1d10', + pen: w.pen != null ? +w.pen : (w.ap != null ? Math.abs(+w.ap) : 0), + rof: w.rof || 1, + modes: Array.isArray(w.modes) && w.modes.length ? w.modes : modesDefault, + tearing: !!w.tearing, + reliable: !!w.reliable, + proven: w.proven || 0, + }) +} + const BODY_PARTS = ['Head','Body','Left Arm','Right Arm','Left Leg','Right Leg'] // Note: don't require files outside `src/` (CRA will fail the production build). @@ -460,7 +488,27 @@ function DeathwatchRoller({ authedPlayer }) { const usingSkill = useMemo(()=>{ const w = weapons.find(x=>x.name===weaponName); if (!w) return 'BS'; return w.class==='melee' ? 'WS' : 'BS' },[weaponName, weapons]) const diffMod = useMemo(()=>{ if (difficulty==='easy') return 20; if (difficulty==='hard') return -20; if (difficulty==='deadly') return -30; return 0 },[difficulty]) const baseSkill = useMemo(()=> usingSkill === 'BS' ? bs : ws, [usingSkill, bs, ws]) - const target = useMemo(()=> Math.max(0, Math.min(100, baseSkill + modifier + aim + diffMod)), [baseSkill, modifier, aim, diffMod]) + + // Active conditions for this player, read from combat-state localStorage/event + const myConditions = useMemo(() => { + if (!authedPlayer || authedPlayer === 'gm') return [] + return (combatRound?.conditions || {})[authedPlayer] || [] + }, [combatRound, authedPlayer]) + + const conditionMod = useMemo(() => { + let mod = 0 + for (const cond of myConditions) { + const entry = CONDITION_MODS[cond] + if (!entry) continue + mod += usingSkill === 'WS' ? entry.ws : entry.bs + } + return mod + }, [myConditions, usingSkill]) + + // Grappled blocks ranged weapons + const isGrappled = myConditions.includes('Grappled') + + const target = useMemo(()=> Math.max(0, Math.min(100, baseSkill + modifier + aim + diffMod + conditionMod)), [baseSkill, modifier, aim, diffMod, conditionMod]) useEffect(()=>{ safeSet(STORAGE_HISTORY, history) },[history]) useEffect(()=>{ safeSet(STORAGE_PRESETS, presets) },[presets]) @@ -486,6 +534,49 @@ function DeathwatchRoller({ authedPlayer }) { return () => window.removeEventListener('dw:combat-state', onCombatState) }, []) + // Load player character sheet on mount — sets BS/WS/wounds and replaces weapon list + useEffect(() => { + if (!authedPlayer || authedPlayer === 'gm') return + ;(async () => { + try { + const res = await fetch(`/api/players/${authedPlayer}`, { cache: 'no-store' }) + if (!res.ok) return + const player = await res.json() + const tab = player.tabInfo || {} + const chars = tab.characteristics || {} + + if (typeof chars.BS === 'number') setBS(chars.BS) + if (typeof chars.WS === 'number') setWS(chars.WS) + if (typeof chars.Ag === 'number') { + // Set defender agility as own Ag for dodge calculations + setDefenderAg(chars.Ag) + } + + const sheetWounds = tab.wounds + if (typeof sheetWounds === 'number' && sheetWounds > 0) { + setMaxWounds(sheetWounds) + // Only reset curWounds if tracker wasn't already customised this session + const stored = safeGet(STORAGE_TRACKER) + if (!stored || stored.maxWounds !== sheetWounds) setCurWounds(sheetWounds) + } + + const playerWeapons = Array.isArray(tab.weapons) ? tab.weapons.map(mapPlayerWeapon).filter(w => w.name) : [] + if (playerWeapons.length > 0) { + setWeapons(playerWeapons) + // Pre-select first weapon and apply its defaults + const first = playerWeapons[0] + setWeaponName(first.name) + const d = computeWeaponDefaults(first) + setDamage(d.damage); setTearing(d.tearing); setProven(d.proven) + setPen(d.pen); setReliable(d.reliable); setRof(d.rof); setMode(d.mode) + } + } catch (e) { + console.info('[DW] Player sheet load failed', e && e.message) + } + })() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [authedPlayer]) + // On first run, if there are no stored weapons try to fetch the packaged DB from public useEffect(()=>{ (async () => { @@ -528,9 +619,12 @@ function DeathwatchRoller({ authedPlayer }) { const filteredWeapons = useMemo(()=>{ const q = weaponFilter.trim().toLowerCase() - if (!q) return weapons - return weapons.filter(w => w.name.toLowerCase().includes(q)) - },[weapons, weaponFilter]) + let list = weapons + // Grappled: can only use melee/pistol while in close combat + if (isGrappled) list = list.filter(w => w.class === 'melee' || w.class === 'pistol') + if (!q) return list + return list.filter(w => w.name.toLowerCase().includes(q)) + },[weapons, weaponFilter, isGrappled]) function onSelectWeapon(name) { setWeaponName(name) @@ -988,13 +1082,31 @@ function DeathwatchRoller({ authedPlayer }) { {error ? error :
}
)} + {myConditions.length > 0 && ( +
+ Conditions active + {myConditions.map(cond => ( + + {CONDITION_MODS[cond]?.label || cond} + + ))} + {conditionMod !== 0 && ( + + {conditionMod > 0 ? '+' : ''}{conditionMod} to attack target + + )} +
+ )}

Deathwatch Roller Pro

{combatRound?.active && ( Round {combatRound.round} )} -
Target {target} • {usingSkill}
+
+ Target {target} • {usingSkill} + {conditionMod !== 0 && ({conditionMod > 0 ? '+' : ''}{conditionMod} cond)} +
{status}