Combat roller: single enemy field; heavy weapons full-auto from RoF data
- Remove the redundant 'Attacker (from enemies)' dropdown and handler; the single Enemy selector remains as the target/defender profile. - Parse the explicit single/semi/full RoF triplet (e.g. '-/-/6') from armoury stats so weapon modes/RoF come from data: the Heavy Bolter is now full-auto only at RoF 6. Bare numeric triplets defer to the class heuristic to avoid range-string collisions. Weapon selection already auto-fills damage/pen/rof/ mode/tearing via computeWeaponDefaults. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -173,13 +173,30 @@ function mapBuildEntryToWeapon(entry) {
|
||||
else if (clsText.includes('basic')) cls = 'basic'
|
||||
else if (clsText.includes('heavy')) cls = 'heavy'
|
||||
|
||||
// Derive modes from class heuristics
|
||||
// Prefer the explicit single/semi/full RoF triplet (e.g. "-/-/6" = full-auto
|
||||
// only at RoF 6, "S/3/-" = single + semi). A "-" means that mode is unavailable.
|
||||
let modes = []
|
||||
if (cls === 'melee') modes = ['single']
|
||||
else if (cls === 'pistol') modes = ['single','semi']
|
||||
else if (cls === 'basic') modes = ['single','semi','full']
|
||||
else if (cls === 'heavy') modes = ['full']
|
||||
else modes = ['single']
|
||||
const triplet = dmgText.match(/(?:^|[\s;(])([0-9S-]+)\s*\/\s*([0-9-]+)\s*\/\s*([0-9-]+)(?!m)/i)
|
||||
// Only trust it as RoF notation if it carries an "S" or "-" marker; a bare
|
||||
// numeric triplet is more likely a range (e.g. "30/60/90m"), so defer to class.
|
||||
if (triplet && /[S-]/i.test(triplet[0])) {
|
||||
const [single, semi, full] = [triplet[1], triplet[2], triplet[3]]
|
||||
if (single !== '-') modes.push('single')
|
||||
if (semi !== '-') modes.push('semi')
|
||||
if (full !== '-') modes.push('full')
|
||||
const fullRof = parseInt(full, 10)
|
||||
const semiRof = parseInt(semi, 10)
|
||||
if (Number.isFinite(fullRof)) rof = Math.max(rof, fullRof)
|
||||
else if (Number.isFinite(semiRof)) rof = Math.max(rof, semiRof)
|
||||
}
|
||||
// Fall back to class heuristics when no triplet is present.
|
||||
if (modes.length === 0) {
|
||||
if (cls === 'melee') modes = ['single']
|
||||
else if (cls === 'pistol') modes = ['single','semi']
|
||||
else if (cls === 'basic') modes = ['single','semi','full']
|
||||
else if (cls === 'heavy') modes = ['full']
|
||||
else modes = ['single']
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
@@ -402,7 +419,6 @@ function DeathwatchRoller({ authedPlayer }) {
|
||||
const [enemiesLoading,setEnemiesLoading] = useState(false)
|
||||
const [enemiesError,setEnemiesError] = useState(null)
|
||||
const [enemyName,setEnemyName] = useState('Custom/None')
|
||||
const [attackerName,setAttackerName] = useState('Custom/None')
|
||||
const [playerSkills, setPlayerSkills] = useState({})
|
||||
const [playerCharacteristics, setPlayerCharacteristics] = useState({})
|
||||
const [rollerTab, setRollerTab] = useState('combat')
|
||||
@@ -737,37 +753,6 @@ function DeathwatchRoller({ authedPlayer }) {
|
||||
else setDefenderModifier(0)
|
||||
}
|
||||
|
||||
function onSelectAttacker(name) {
|
||||
setAttackerName(name)
|
||||
const e = enemies.find(x=>x.name===name)
|
||||
if (!e || name === "Custom/None") return
|
||||
// Extract WS/BS from enemy
|
||||
function findNumeric(o, keys) {
|
||||
if (!o || typeof o !== "object") return undefined
|
||||
for (const k of keys) {
|
||||
if (typeof o[k] === "number") return o[k]
|
||||
const lower = k.toLowerCase()
|
||||
for (const ok of Object.keys(o)) {
|
||||
if (ok.toLowerCase() === lower && typeof o[ok] === "number") return o[ok]
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
const char = e.characteristics || (e.tabInfo && e.tabInfo.characteristics) || {}
|
||||
const wsVal = findNumeric(e, ["ws", "Ws", "WS"]) ?? findNumeric(char, ["ws"])
|
||||
const bsVal = findNumeric(e, ["bs", "Bs", "BS"]) ?? findNumeric(char, ["bs"])
|
||||
if (typeof wsVal === "number") setWS(wsVal)
|
||||
if (typeof bsVal === "number") setBS(bsVal)
|
||||
const enemyDamage = extractDiceDamage(e.damage)
|
||||
if (enemyDamage) setDamage(enemyDamage)
|
||||
if (typeof e.pen === "number") setPen(e.pen)
|
||||
else {
|
||||
const enemyPen = extractPen(e.damage)
|
||||
if (typeof enemyPen === "number") setPen(enemyPen)
|
||||
}
|
||||
if (e.attack) setWeaponName(e.attack)
|
||||
}
|
||||
|
||||
function importFromJSONText(txt) {
|
||||
try {
|
||||
const arr = JSON.parse(txt)
|
||||
@@ -1396,12 +1381,6 @@ function DeathwatchRoller({ authedPlayer }) {
|
||||
<Tooltip text="Toughness Bonus">TB</Tooltip> / <Tooltip text="Armour Rating">AR</Tooltip> / <Tooltip text="Wounds">W</Tooltip> • {enemies.length} enemies loaded
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">Attacker (from enemies)</label>
|
||||
<select className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" value={attackerName} onChange={e=>onSelectAttacker(e.target.value)}>
|
||||
{enemies.map(en => (<option key={"atk-"+en.name} value={en.name}>{en.name}{en.name==="Custom/None" ? "" : ` (WS ${en.ws??"-"} / BS ${en.bs??"-"})`}</option>))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs uppercase opacity-70">
|
||||
<Tooltip text="Ballistic Skill - Used for ranged attacks">Attacker's BS</Tooltip>
|
||||
|
||||
Reference in New Issue
Block a user