Split roller into Skill and Combat tabs
Extract the skill-check UI/logic into a standalone SkillRoller component and move shared dice helpers (d100, degrees, uid) into rollHelpers.js. DeathwatchRoller now hosts the shared roll history + player sheet and a Combat/Skill tab toggle; the skill-only characteristic/training helpers moved into SkillRoller. Behaviour unchanged, including first-skill preselect (now done inside SkillRoller) and the manual target override. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { MISSION_ROLL_CONTEXT_KEY } from '../utils/diceRoller';
|
||||
import SkillRoller from './SkillRoller';
|
||||
import { d100, degrees, uid } from './rollHelpers';
|
||||
|
||||
// Tooltip component for abbreviations
|
||||
function Tooltip({ children, text }) {
|
||||
@@ -77,14 +79,6 @@ function rollDice(terms, opts) {
|
||||
return { rolls: out, total: out.reduce((a,b)=>a+b,0) }
|
||||
}
|
||||
|
||||
function d100() { return Math.floor(Math.random()*100)+1 }
|
||||
|
||||
function degrees(target, roll) {
|
||||
const success = roll <= target
|
||||
if (success) { const diff = target - roll; const dos = 1 + Math.floor(diff/10); return { success, dos, dof: 0 } }
|
||||
const diff = roll - target; const dof = 1 + Math.floor(diff/10); return { success, dos: 0, dof }
|
||||
}
|
||||
|
||||
function hitsFromDoS(mode, dos, rof) {
|
||||
const r = rof && rof>0 ? rof : 1
|
||||
if (mode === 'single') return Math.min(1, r)
|
||||
@@ -371,58 +365,6 @@ function normalizeWeapon(w) {
|
||||
|
||||
function computeWeaponDefaults(w) { return { damage: w.damage, tearing: !!w.tearing, proven: w.proven || 0, pen: w.pen || 0, reliable: !!w.reliable, rof: w.rof || 1, mode: (w.modes && w.modes[0]) || 'single' } }
|
||||
|
||||
function uid() { return Math.random().toString(36).slice(2) + Date.now().toString(36) }
|
||||
|
||||
const CHARACTERISTIC_ALIASES = {
|
||||
WS: ['WS', 'Ws', 'ws'],
|
||||
BS: ['BS', 'Bs', 'bs'],
|
||||
S: ['S', 's'],
|
||||
T: ['T', 't'],
|
||||
Ag: ['Ag', 'AG', 'ag'],
|
||||
Int: ['Int', 'INT', 'int'],
|
||||
Per: ['Per', 'PER', 'per'],
|
||||
WP: ['WP', 'Wp', 'wp'],
|
||||
Wp: ['WP', 'Wp', 'wp'],
|
||||
Fel: ['Fel', 'FEL', 'fel'],
|
||||
}
|
||||
|
||||
function characteristicFromSkillName(name) {
|
||||
const match = String(name || '').match(/\(([^)]+)\)/)
|
||||
const raw = match ? match[1].trim() : 'Int'
|
||||
if (/^wp$/i.test(raw)) return 'WP'
|
||||
if (/^ag$/i.test(raw)) return 'Ag'
|
||||
if (/^int$/i.test(raw)) return 'Int'
|
||||
if (/^per$/i.test(raw)) return 'Per'
|
||||
if (/^fel$/i.test(raw)) return 'Fel'
|
||||
if (/^bs$/i.test(raw)) return 'BS'
|
||||
if (/^ws$/i.test(raw)) return 'WS'
|
||||
if (/^s$/i.test(raw)) return 'S'
|
||||
if (/^t$/i.test(raw)) return 'T'
|
||||
return raw
|
||||
}
|
||||
|
||||
function characteristicValue(chars, key, fallback = 45) {
|
||||
const aliases = CHARACTERISTIC_ALIASES[key] || [key]
|
||||
for (const alias of aliases) {
|
||||
if (typeof chars?.[alias] === 'number') return chars[alias]
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
function skillTrainingBonus(skill) {
|
||||
if (skill?.plus20) return 20
|
||||
if (skill?.plus10) return 10
|
||||
if (skill?.trained) return 0
|
||||
return -20
|
||||
}
|
||||
|
||||
function skillTrainingLabel(skill) {
|
||||
if (skill?.plus20) return 'Trained +20'
|
||||
if (skill?.plus10) return 'Trained +10'
|
||||
if (skill?.trained) return 'Trained'
|
||||
return 'Untrained -20'
|
||||
}
|
||||
|
||||
function DeathwatchRoller({ authedPlayer }) {
|
||||
const [bs,setBS] = useState(45)
|
||||
const [ws,setWS] = useState(45)
|
||||
@@ -463,10 +405,7 @@ function DeathwatchRoller({ authedPlayer }) {
|
||||
const [attackerName,setAttackerName] = useState('Custom/None')
|
||||
const [playerSkills, setPlayerSkills] = useState({})
|
||||
const [playerCharacteristics, setPlayerCharacteristics] = useState({})
|
||||
const [skillRollName, setSkillRollName] = useState('')
|
||||
const [skillRollModifier, setSkillRollModifier] = useState(0)
|
||||
const [manualSkillRoll, setManualSkillRoll] = useState('')
|
||||
const [skillTargetOverride, setSkillTargetOverride] = useState('')
|
||||
const [rollerTab, setRollerTab] = useState('combat')
|
||||
|
||||
// Load enemies from database API
|
||||
async function loadEnemiesFromAPI() {
|
||||
@@ -666,12 +605,6 @@ function DeathwatchRoller({ authedPlayer }) {
|
||||
const sheetSkills = tab.skills || {}
|
||||
setPlayerCharacteristics(chars)
|
||||
setPlayerSkills(sheetSkills)
|
||||
const skillNames = Object.keys(sheetSkills).sort((a, b) => {
|
||||
const at = sheetSkills[a]?.trained ? 0 : 1
|
||||
const bt = sheetSkills[b]?.trained ? 0 : 1
|
||||
return at - bt || a.localeCompare(b)
|
||||
})
|
||||
if (skillNames.length > 0) setSkillRollName(prev => prev || skillNames[0])
|
||||
|
||||
if (typeof chars.BS === 'number') setBS(chars.BS)
|
||||
if (typeof chars.WS === 'number') setWS(chars.WS)
|
||||
@@ -1188,25 +1121,6 @@ function DeathwatchRoller({ authedPlayer }) {
|
||||
|
||||
const allowedModes = useMemo(()=>{ const w = weapons.find(x=>x.name===weaponName); return w && Array.isArray(w.modes) && w.modes.length ? w.modes : ['single','semi','full'] },[weapons, weaponName])
|
||||
|
||||
const skillOptions = useMemo(() => {
|
||||
return Object.keys(playerSkills || {}).sort((a, b) => {
|
||||
const at = playerSkills[a]?.trained ? 0 : 1
|
||||
const bt = playerSkills[b]?.trained ? 0 : 1
|
||||
return at - bt || a.localeCompare(b)
|
||||
})
|
||||
}, [playerSkills])
|
||||
|
||||
const selectedSkill = playerSkills?.[skillRollName] || null
|
||||
const selectedSkillCharacteristic = characteristicFromSkillName(skillRollName)
|
||||
const selectedSkillBase = characteristicValue(playerCharacteristics, selectedSkillCharacteristic, 45)
|
||||
const selectedSkillTrainingBonus = skillTrainingBonus(selectedSkill)
|
||||
const selectedSkillTarget = Math.max(1, Math.min(100, selectedSkillBase + selectedSkillTrainingBonus + (Number(skillRollModifier) || 0)))
|
||||
const skillTargetOverrideNum = parseInt(String(skillTargetOverride).trim(), 10)
|
||||
const hasSkillTargetOverride = String(skillTargetOverride).trim() !== '' && Number.isFinite(skillTargetOverrideNum)
|
||||
const effectiveSkillTarget = hasSkillTargetOverride
|
||||
? Math.max(1, Math.min(100, skillTargetOverrideNum))
|
||||
: selectedSkillTarget
|
||||
|
||||
const woundPct = maxWounds>0 ? Math.max(0, Math.min(100, (curWounds/maxWounds)*100)) : 0
|
||||
const status = curWounds<=0 ? 'Dead' : (curWounds<maxWounds ? 'Wounded' : 'Alive')
|
||||
const statusColor = status==='Dead' ? 'bg-rose-600' : status==='Wounded' ? 'bg-amber-500' : 'bg-emerald-600'
|
||||
@@ -1229,47 +1143,6 @@ function DeathwatchRoller({ authedPlayer }) {
|
||||
}
|
||||
}
|
||||
|
||||
function rollSkillCheck() {
|
||||
setError('')
|
||||
setInfo('')
|
||||
if (!skillRollName && !hasSkillTargetOverride) {
|
||||
setError('Select a skill or enter a target number')
|
||||
return
|
||||
}
|
||||
let roll
|
||||
if (manualSkillRoll.trim()) {
|
||||
const parsed = parseInt(manualSkillRoll.trim(), 10)
|
||||
if (!Number.isFinite(parsed) || parsed < 1 || parsed > 100) {
|
||||
setError('Manual skill roll must be a number between 1-100')
|
||||
return
|
||||
}
|
||||
roll = parsed
|
||||
} else {
|
||||
roll = d100()
|
||||
}
|
||||
const label = skillRollName || `Target ${effectiveSkillTarget}`
|
||||
const dg = degrees(effectiveSkillTarget, roll)
|
||||
const res = {
|
||||
id: uid(),
|
||||
ts: Date.now(),
|
||||
rollKind: 'skill',
|
||||
skillName: label,
|
||||
characteristic: skillRollName ? selectedSkillCharacteristic : null,
|
||||
base: skillRollName ? selectedSkillBase : null,
|
||||
training: skillRollName ? skillTrainingLabel(selectedSkill) : null,
|
||||
modifier: Number(skillRollModifier) || 0,
|
||||
target: effectiveSkillTarget,
|
||||
targetOverride: hasSkillTargetOverride,
|
||||
attackRoll: roll,
|
||||
success: dg.success,
|
||||
dos: dg.dos,
|
||||
dof: dg.dof,
|
||||
using: skillRollName ? selectedSkillCharacteristic : null,
|
||||
}
|
||||
addRollResult(res)
|
||||
setInfo(`${label}: ${dg.success ? `Success (${dg.dos} DoS)` : `Fail (${dg.dof} DoF)`}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-slate-100 p-6 md:p-10">
|
||||
<div className="mx-auto max-w-6xl space-y-6">
|
||||
@@ -1335,7 +1208,20 @@ function DeathwatchRoller({ authedPlayer }) {
|
||||
</div>
|
||||
<div className="grid md:grid-cols-3 gap-6">
|
||||
<div className="md:col-span-2 rounded-2xl bg-slate-800 border border-slate-700 p-5 space-y-4 shadow-xl">
|
||||
{awaitingDefense && pendingHits && (
|
||||
<div className="flex gap-2">
|
||||
<button onClick={()=>setRollerTab('combat')} className={`rounded-xl px-4 py-2 ${rollerTab==='combat' ? 'bg-sky-700' : 'bg-slate-700 hover:bg-slate-600'}`}>Combat Roller</button>
|
||||
<button onClick={()=>setRollerTab('skill')} className={`rounded-xl px-4 py-2 ${rollerTab==='skill' ? 'bg-sky-700' : 'bg-slate-700 hover:bg-slate-600'}`}>Skill Roller</button>
|
||||
</div>
|
||||
{rollerTab === 'skill' && (
|
||||
<SkillRoller
|
||||
playerSkills={playerSkills}
|
||||
playerCharacteristics={playerCharacteristics}
|
||||
onResult={addRollResult}
|
||||
setError={setError}
|
||||
setInfo={setInfo}
|
||||
/>
|
||||
)}
|
||||
{rollerTab === 'combat' && awaitingDefense && pendingHits && (
|
||||
<div className="mb-4 rounded-xl bg-amber-800 border border-amber-600 p-4">
|
||||
<div className="text-lg font-bold mb-2">Incoming Attack!</div>
|
||||
<div className="text-sm mb-4">
|
||||
@@ -1472,47 +1358,7 @@ function DeathwatchRoller({ authedPlayer }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="rounded-xl bg-slate-900 border border-slate-600 p-4 space-y-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<div className="font-semibold">Skill Check</div>
|
||||
<div className="text-xs opacity-70">Uses this player sheet's skills and characteristics.</div>
|
||||
</div>
|
||||
<button onClick={rollSkillCheck} disabled={skillOptions.length === 0 && !hasSkillTargetOverride} className="rounded-xl px-4 py-2 bg-emerald-700 hover:bg-emerald-600 disabled:opacity-40">Roll Skill</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-5 gap-3 items-end">
|
||||
<div className="md:col-span-2">
|
||||
<label className="text-xs uppercase opacity-70">Skill</label>
|
||||
<select className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" value={skillRollName} onChange={e=>setSkillRollName(e.target.value)} disabled={skillOptions.length === 0}>
|
||||
{skillOptions.length === 0 && <option value="">No sheet skills loaded</option>}
|
||||
{skillOptions.map(skill => (
|
||||
<option key={skill} value={skill}>{skill} • {skillTrainingLabel(playerSkills[skill])}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">Modifier</label>
|
||||
<input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" value={skillRollModifier} onChange={e=>setSkillRollModifier(parseInt(e.target.value||'0', 10))} disabled={hasSkillTargetOverride} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">Target</label>
|
||||
<input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" min="1" max="100" placeholder="auto" value={skillTargetOverride} onChange={e=>setSkillTargetOverride(e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">Manual d100</label>
|
||||
<input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" min="1" max="100" placeholder="optional" value={manualSkillRoll} onChange={e=>setManualSkillRoll(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm opacity-85">
|
||||
Target <span className="font-semibold">{effectiveSkillTarget}</span>
|
||||
{hasSkillTargetOverride ? (
|
||||
<span> = manual target</span>
|
||||
) : skillRollName && (
|
||||
<span> = {selectedSkillCharacteristic} {selectedSkillBase} {selectedSkillTrainingBonus >= 0 ? '+' : ''}{selectedSkillTrainingBonus} training {Number(skillRollModifier) ? `${Number(skillRollModifier) > 0 ? '+' : ''}${Number(skillRollModifier)} mod` : '+0 mod'}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{rollerTab === 'combat' && (<>
|
||||
<div className="grid grid-cols-2 lg:grid-cols-6 gap-4 items-end">
|
||||
<div className="lg:col-span-3">
|
||||
<label className="text-xs uppercase opacity-70">Weapon</label>
|
||||
@@ -1817,6 +1663,7 @@ function DeathwatchRoller({ authedPlayer }) {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>)}
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
<div className="rounded-2xl bg-slate-800 backdrop-blur border border-slate-600 p-5 shadow-xl">
|
||||
|
||||
167
src/components/SkillRoller.jsx
Normal file
167
src/components/SkillRoller.jsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { d100, degrees, uid } from './rollHelpers'
|
||||
|
||||
const CHARACTERISTIC_ALIASES = {
|
||||
WS: ['WS', 'Ws', 'ws'],
|
||||
BS: ['BS', 'Bs', 'bs'],
|
||||
S: ['S', 's'],
|
||||
T: ['T', 't'],
|
||||
Ag: ['Ag', 'AG', 'ag'],
|
||||
Int: ['Int', 'INT', 'int'],
|
||||
Per: ['Per', 'PER', 'per'],
|
||||
WP: ['WP', 'Wp', 'wp'],
|
||||
Wp: ['WP', 'Wp', 'wp'],
|
||||
Fel: ['Fel', 'FEL', 'fel'],
|
||||
}
|
||||
|
||||
function characteristicFromSkillName(name) {
|
||||
const match = String(name || '').match(/\(([^)]+)\)/)
|
||||
const raw = match ? match[1].trim() : 'Int'
|
||||
if (/^wp$/i.test(raw)) return 'WP'
|
||||
if (/^ag$/i.test(raw)) return 'Ag'
|
||||
if (/^int$/i.test(raw)) return 'Int'
|
||||
if (/^per$/i.test(raw)) return 'Per'
|
||||
if (/^fel$/i.test(raw)) return 'Fel'
|
||||
if (/^bs$/i.test(raw)) return 'BS'
|
||||
if (/^ws$/i.test(raw)) return 'WS'
|
||||
if (/^s$/i.test(raw)) return 'S'
|
||||
if (/^t$/i.test(raw)) return 'T'
|
||||
return raw
|
||||
}
|
||||
|
||||
function characteristicValue(chars, key, fallback = 45) {
|
||||
const aliases = CHARACTERISTIC_ALIASES[key] || [key]
|
||||
for (const alias of aliases) {
|
||||
if (typeof chars?.[alias] === 'number') return chars[alias]
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
function skillTrainingBonus(skill) {
|
||||
if (skill?.plus20) return 20
|
||||
if (skill?.plus10) return 10
|
||||
if (skill?.trained) return 0
|
||||
return -20
|
||||
}
|
||||
|
||||
function skillTrainingLabel(skill) {
|
||||
if (skill?.plus20) return 'Trained +20'
|
||||
if (skill?.plus10) return 'Trained +10'
|
||||
if (skill?.trained) return 'Trained'
|
||||
return 'Untrained -20'
|
||||
}
|
||||
|
||||
export default function SkillRoller({ playerSkills, playerCharacteristics, onResult, setError, setInfo }) {
|
||||
const [skillRollName, setSkillRollName] = useState('')
|
||||
const [skillRollModifier, setSkillRollModifier] = useState(0)
|
||||
const [manualSkillRoll, setManualSkillRoll] = useState('')
|
||||
const [skillTargetOverride, setSkillTargetOverride] = useState('')
|
||||
|
||||
const skillOptions = useMemo(() => {
|
||||
return Object.keys(playerSkills || {}).sort((a, b) => {
|
||||
const at = playerSkills[a]?.trained ? 0 : 1
|
||||
const bt = playerSkills[b]?.trained ? 0 : 1
|
||||
return at - bt || a.localeCompare(b)
|
||||
})
|
||||
}, [playerSkills])
|
||||
|
||||
// Preselect the first (best-trained) skill once the sheet loads.
|
||||
useEffect(() => {
|
||||
if (!skillRollName && skillOptions.length > 0) setSkillRollName(skillOptions[0])
|
||||
}, [skillOptions, skillRollName])
|
||||
|
||||
const selectedSkill = playerSkills?.[skillRollName] || null
|
||||
const selectedSkillCharacteristic = characteristicFromSkillName(skillRollName)
|
||||
const selectedSkillBase = characteristicValue(playerCharacteristics, selectedSkillCharacteristic, 45)
|
||||
const selectedSkillTrainingBonus = skillTrainingBonus(selectedSkill)
|
||||
const selectedSkillTarget = Math.max(1, Math.min(100, selectedSkillBase + selectedSkillTrainingBonus + (Number(skillRollModifier) || 0)))
|
||||
const skillTargetOverrideNum = parseInt(String(skillTargetOverride).trim(), 10)
|
||||
const hasSkillTargetOverride = String(skillTargetOverride).trim() !== '' && Number.isFinite(skillTargetOverrideNum)
|
||||
const effectiveSkillTarget = hasSkillTargetOverride
|
||||
? Math.max(1, Math.min(100, skillTargetOverrideNum))
|
||||
: selectedSkillTarget
|
||||
|
||||
function rollSkillCheck() {
|
||||
setError('')
|
||||
setInfo('')
|
||||
if (!skillRollName && !hasSkillTargetOverride) {
|
||||
setError('Select a skill or enter a target number')
|
||||
return
|
||||
}
|
||||
let roll
|
||||
if (manualSkillRoll.trim()) {
|
||||
const parsed = parseInt(manualSkillRoll.trim(), 10)
|
||||
if (!Number.isFinite(parsed) || parsed < 1 || parsed > 100) {
|
||||
setError('Manual skill roll must be a number between 1-100')
|
||||
return
|
||||
}
|
||||
roll = parsed
|
||||
} else {
|
||||
roll = d100()
|
||||
}
|
||||
const label = skillRollName || `Target ${effectiveSkillTarget}`
|
||||
const dg = degrees(effectiveSkillTarget, roll)
|
||||
const res = {
|
||||
id: uid(),
|
||||
ts: Date.now(),
|
||||
rollKind: 'skill',
|
||||
skillName: label,
|
||||
characteristic: skillRollName ? selectedSkillCharacteristic : null,
|
||||
base: skillRollName ? selectedSkillBase : null,
|
||||
training: skillRollName ? skillTrainingLabel(selectedSkill) : null,
|
||||
modifier: Number(skillRollModifier) || 0,
|
||||
target: effectiveSkillTarget,
|
||||
targetOverride: hasSkillTargetOverride,
|
||||
attackRoll: roll,
|
||||
success: dg.success,
|
||||
dos: dg.dos,
|
||||
dof: dg.dof,
|
||||
using: skillRollName ? selectedSkillCharacteristic : null,
|
||||
}
|
||||
onResult(res)
|
||||
setInfo(`${label}: ${dg.success ? `Success (${dg.dos} DoS)` : `Fail (${dg.dof} DoF)`}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-xl bg-slate-900 border border-slate-600 p-4 space-y-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<div className="font-semibold">Skill Check</div>
|
||||
<div className="text-xs opacity-70">Uses this player sheet's skills and characteristics.</div>
|
||||
</div>
|
||||
<button onClick={rollSkillCheck} disabled={skillOptions.length === 0 && !hasSkillTargetOverride} className="rounded-xl px-4 py-2 bg-emerald-700 hover:bg-emerald-600 disabled:opacity-40">Roll Skill</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-5 gap-3 items-end">
|
||||
<div className="md:col-span-2">
|
||||
<label className="text-xs uppercase opacity-70">Skill</label>
|
||||
<select className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" value={skillRollName} onChange={e=>setSkillRollName(e.target.value)} disabled={skillOptions.length === 0}>
|
||||
{skillOptions.length === 0 && <option value="">No sheet skills loaded</option>}
|
||||
{skillOptions.map(skill => (
|
||||
<option key={skill} value={skill}>{skill} • {skillTrainingLabel(playerSkills[skill])}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">Modifier</label>
|
||||
<input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" value={skillRollModifier} onChange={e=>setSkillRollModifier(parseInt(e.target.value||'0', 10))} disabled={hasSkillTargetOverride} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">Target</label>
|
||||
<input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" min="1" max="100" placeholder="auto" value={skillTargetOverride} onChange={e=>setSkillTargetOverride(e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">Manual d100</label>
|
||||
<input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" min="1" max="100" placeholder="optional" value={manualSkillRoll} onChange={e=>setManualSkillRoll(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm opacity-85">
|
||||
Target <span className="font-semibold">{effectiveSkillTarget}</span>
|
||||
{hasSkillTargetOverride ? (
|
||||
<span> = manual target</span>
|
||||
) : skillRollName && (
|
||||
<span> = {selectedSkillCharacteristic} {selectedSkillBase} {selectedSkillTrainingBonus >= 0 ? '+' : ''}{selectedSkillTrainingBonus} training {Number(skillRollModifier) ? `${Number(skillRollModifier) > 0 ? '+' : ''}${Number(skillRollModifier)} mod` : '+0 mod'}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
11
src/components/rollHelpers.js
Normal file
11
src/components/rollHelpers.js
Normal file
@@ -0,0 +1,11 @@
|
||||
// Shared dice/degree helpers used by the combat and skill rollers.
|
||||
|
||||
export function d100() { return Math.floor(Math.random() * 100) + 1 }
|
||||
|
||||
export function degrees(target, roll) {
|
||||
const success = roll <= target
|
||||
if (success) { const diff = target - roll; const dos = 1 + Math.floor(diff / 10); return { success, dos, dof: 0 } }
|
||||
const diff = roll - target; const dof = 1 + Math.floor(diff / 10); return { success, dos: 0, dof }
|
||||
}
|
||||
|
||||
export function uid() { return Math.random().toString(36).slice(2) + Date.now().toString(36) }
|
||||
Reference in New Issue
Block a user