import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { MISSION_ROLL_CONTEXT_KEY } from '../utils/diceRoller';
import SkillRoller from './SkillRoller';
import FateControls from './FateControls';
import { useFate } from './useFate';
import { applyFateToTest, d100, degrees, uid } from './rollHelpers';

// Tooltip component for abbreviations
function Tooltip({ children, text }) {
  return (
    <span className="group relative inline-block cursor-help border-b border-dotted border-slate-500 hover:border-white">
      {children}
      <span className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 text-xs bg-black text-white rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50">
        {text}
      </span>
    </span>
  )
}

function simpleCSVParse(txt) {
  const lines = String(txt ?? '').split(/\r?\n|\r/).filter(l=>l.trim().length>0)
  if (lines.length===0) return []
  const headers = lines[0].split(',').map(h=>h.trim())
  const rows = []
  for (let i=1;i<lines.length;i++) {
    const cells = lines[i].split(',')
    const obj = {}
    for (let j=0;j<headers.length;j++) obj[headers[j]] = (cells[j]||'').trim()
    rows.push(obj)
  }
  return rows
}

function parseDice(spec) {
  const s = String(spec || '').replace(/\s+/g, '').toLowerCase()
  const parts = s.split('+')
  let flat = 0
  let terms = []
  for (const p of parts) {
    if (!p) continue
    const m = p.match(/^(\d+)d(\d+)$/)
    if (m) {
      const c = parseInt(m[1], 10)
      const f = parseInt(m[2], 10)
      if (!Number.isFinite(c) || !Number.isFinite(f) || c < 1 || f < 2) throw new Error('Invalid dice bounds')
      terms.push({ count: c, faces: f })
    } else {
      const n = Number(p)
      if (!Number.isNaN(n)) flat += n
      else throw new Error(`Invalid dice term: ${p}`)
    }
  }
  if (terms.length === 0) terms = [{ count: 1, faces: 10 }]
  return { terms, flat }
}

function weaponDamageScore(weapon) {
  const text = String(weapon?.damage || '').replace(/\s+/g, '')
  const match = text.match(/(\d+)d(\d+)(?:([+-])(\d+))?/i)
  if (!match) return -1
  const count = Math.max(1, Number(match[1]) || 1)
  const faces = Math.max(2, Number(match[2]) || 10)
  const sign = match[3] === '-' ? -1 : 1
  const flat = match[4] ? sign * (Number(match[4]) || 0) : 0
  return count * ((faces + 1) / 2) + flat
}

function weaponIsGrenade(weapon) {
  return /grenade/i.test(String(weapon?.name || '')) || String(weapon?.class || '').toLowerCase() === 'grenade'
}

function pickMainWeapon(weapons) {
  const list = Array.isArray(weapons) ? weapons.filter(w => w && w.name) : []
  const nonGrenades = list.filter(w => !weaponIsGrenade(w))
  const pool = nonGrenades.length ? nonGrenades : list
  return pool.slice().sort((a, b) => weaponDamageScore(b) - weaponDamageScore(a))[0] || null
}

function hasMeleeWeapon(weapons) {
  return Array.isArray(weapons) && weapons.some(w => String(w?.class || '').toLowerCase() === 'melee')
}

function extractDiceDamage(value) {
  const match = String(value || '').match(/\d+d\d+(?:\s*[+-]\s*\d+)?/i)
  return match ? match[0].replace(/\s+/g, '') : ''
}

function normalizeDamageExpression(value, fallback = '1d10') {
  if (typeof value === 'string') {
    const cleaned = extractDiceDamage(value) || value.trim()
    return cleaned || fallback
  }
  if (value && typeof value === 'object') {
    const nested = extractDiceDamage(value.damage ?? value.value ?? value.text ?? value.stats?.damage)
    return nested || fallback
  }
  return fallback
}

function extractPen(value) {
  const match = String(value || '').match(/\bPen\s*(\d+)/i)
  return match ? Number(match[1]) : undefined
}

function rollDie(faces) { return Math.floor(Math.random()*faces)+1 }

function rollDice(terms, opts) {
  const tearing = !!(opts && opts.tearing)
  const proven = Math.max(0, opts && typeof opts.proven==='number' ? opts.proven : 0)
  const out = []
  for (const t of terms) {
    for (let i=0;i<t.count;i++) {
      let r = rollDie(t.faces)
      if (tearing) { const alt = rollDie(t.faces); r = Math.max(r, alt) }
      if (proven>0 && t.faces===10) r = Math.max(r, proven)
      out.push(r)
    }
  }
  return { rolls: out, total: out.reduce((a,b)=>a+b,0) }
}

function hitsFromDoS(mode, dos, rof) {
  const r = rof && rof>0 ? rof : 1
  if (mode === 'single') return Math.min(1, r)
  if (mode === 'semi') return Math.max(1, Math.min(1 + Math.floor(dos/2), r))
  return Math.max(1, Math.min(1 + dos, r))
}

function hitLocationFromRoll(roll) {
  const rev = Number(String(roll).padStart(2,'0').split('').reverse().join(''))
  if (rev >= 1 && rev <= 10) return 'Head'
  if (rev <= 20) return 'Right Arm'
  if (rev <= 30) return 'Left Arm'
  if (rev <= 70) return 'Body'
  if (rev <= 85) return 'Right Leg'
  return 'Left Leg'
}

function mitigateDamage(dmg, tb, armour) { return Math.max(0, dmg - tb - armour) }

const STORAGE_PRESETS = 'dw:presets:v3'
const STORAGE_HISTORY = 'dw:history:v2'
const STORAGE_WEAPONS = 'dw:weapons:v3'
const STORAGE_TRACKER = 'dw:tracker:v1'
const STORAGE_PLAYER_TRACKER = 'dw:player-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)' },
}

const PHASE_TIMING = {
  attack: 700,
  defense: 900,
  damage: 900,
  wounds: 1400,
}

function mapPlayerWeapon(w) {
  const type = String(w.type || w.class || '').toLowerCase()
  const name = String(w.name || '').toLowerCase()
  const hint = `${type} ${name}`
  const clsMap = { heavy: 'heavy', pistol: 'pistol', basic: 'basic', melee: 'melee', bolt: 'basic', bolter: 'basic', thrown: 'grenade', grenade: 'grenade' }
  const cls = clsMap[type] || (
    hint.includes('grenade') || hint.includes('thrown') ? 'grenade'
      : hint.includes('pistol') ? 'pistol'
      : hint.includes('melee') || hint.includes('sword') || hint.includes('axe') || hint.includes('fist') || hint.includes('hammer') || hint.includes('knife') || hint.includes('crozius') || hint.includes('maul') || hint.includes('staff') || hint.includes('claw') || hint.includes('chain')
        ? 'melee'
        : hint.includes('heavy')
          ? 'heavy'
          : 'basic'
  )
  const modesDefault = cls === 'melee' ? ['single'] : cls === 'pistol' ? ['single'] : cls === 'heavy' ? ['single'] : ['single', 'semi']
  return normalizeWeapon({
    name: w.name,
    class: cls,
    damage: normalizeDamageExpression(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).
// We'll attempt to fetch any packaged JSON at runtime from known public paths.
let _builtWeapons = null

function mapBuildEntryToWeapon(entry) {
  const name = String(entry.name || '').trim()
  const stats = entry.stats || {}
  const dmgText = String(stats.damage || '')
  const rawDamage = dmgText
  // Extract first dice expression like 2d10+5
  const diceMatch = dmgText.match(/(\d+d\d+(?:\+\d+)?)/i)
  const damage = diceMatch ? diceMatch[1] : '1d10'
  const tearing = /tearing/i.test(dmgText) || /tearing/i.test(stats.source || '')
  const reliable = /reliable/i.test(dmgText) || /reliable/i.test(stats.source || '')
  const penMatch = dmgText.match(/Pen\s*([0-9]+)/i)
  const pen = penMatch ? Math.max(0, parseInt(penMatch[1], 10)) : 0
  // Try to extract RoF from explicit token, otherwise approximate from Clip
  let rof = 1
  const rofMatch = dmgText.match(/RoF\s*[:\s]*([0-9]+)/i) || dmgText.match(/rof\s*[:\s]*([0-9]+)/i)
  if (rofMatch) rof = Math.max(1, parseInt(rofMatch[1], 10))
  else {
    const clipMatch = dmgText.match(/Clip\s*([0-9]+)/i)
    if (clipMatch) {
      const clip = Math.max(0, parseInt(clipMatch[1], 10))
      if (clip >= 100) rof = 10
      else if (clip >= 30) rof = 4
      else if (clip >= 14) rof = 3
      else if (clip >= 6) rof = 2
      else rof = 1
    }
  }

  // Map class string into roller classes
  const clsText = String(stats.class || entry.category || '').toLowerCase()
  let cls = 'other'
  if (clsText.includes('melee')) cls = 'melee'
  else if (clsText.includes('pistol')) cls = 'pistol'
  else if (clsText.includes('basic')) cls = 'basic'
  else if (clsText.includes('heavy')) cls = 'heavy'

  // 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 = []
  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,
    class: cls,
    damage,
  rawDamage,
    pen,
    rof,
    modes,
    tearing,
    reliable
  }
}

// Normalize bestiary entry shapes (same logic as BestiaryTab) so different consumers get a consistent shape
// Removed normalizeEntry function - now using database API transformation

function buildWeaponsList() {
  if (!_builtWeapons) return []
  const out = []
  if (Array.isArray(_builtWeapons.rangedWeapons)) out.push(..._builtWeapons.rangedWeapons.map(mapBuildEntryToWeapon))
  if (Array.isArray(_builtWeapons.meleeWeapons)) out.push(..._builtWeapons.meleeWeapons.map(mapBuildEntryToWeapon))
  if (Array.isArray(_builtWeapons.grenades)) out.push(..._builtWeapons.grenades.map(mapBuildEntryToWeapon))
  if (Array.isArray(_builtWeapons.other)) out.push(..._builtWeapons.other.map(mapBuildEntryToWeapon))
  // Preserve additional metadata (req, renown, source) onto mapped entries when available
  for (const cat of ['rangedWeapons','meleeWeapons','grenades','other']) {
    if (!Array.isArray(_builtWeapons[cat])) continue
    for (const e of _builtWeapons[cat]) {
      const key = String(e.name || '').trim().toLowerCase()
      const mapped = out.find(o => o.name.toLowerCase() === key)
      if (!mapped) continue
      if (typeof e.req !== 'undefined') mapped.req = e.req
      if (typeof e.renown !== 'undefined') mapped.renown = e.renown
      if (e.stats && e.stats.source) mapped.source = e.stats.source
      else if (e.source) mapped.source = e.source
    }
  }
  // Deduplicate by name (case-insensitive)
  const byName = new Map()
  for (const w of out) {
    if (!w.name) continue
    byName.set(w.name.toLowerCase(), w)
  }
  return Array.from(byName.values()).sort((a,b)=>a.name.localeCompare(b.name))
}

// Merge helper moved to module scope so top-level import helpers can use it
function mergeWeapons(existing, incoming) {
  const byName = new Map((Array.isArray(existing) ? existing : []).map(w=>[w.name.toLowerCase(), w]))
  for (const w of (Array.isArray(incoming) ? incoming : [])) {
    if (!w || !w.name) continue
    byName.set(w.name.toLowerCase(), w)
  }
  return Array.from(byName.values()).sort((a,b)=>a.name.localeCompare(b.name))
}

// Import built DB into weapons (merge with existing)
function importBuildWeapons(existing) {
  const built = buildWeaponsList()
  if (!built || built.length===0) return existing
  // Attach metadata for display; merge preserving existing items
  const merged = mergeWeapons(existing, built)
  return merged
}

function safeGet(key) {
  try {
    if (typeof window === 'undefined' || !('localStorage' in window)) return null
    const raw = window.localStorage.getItem(key)
    return raw ? JSON.parse(raw) : null
  } catch { return null }
}
function safeSet(key, val) { try { if (typeof window !== 'undefined' && 'localStorage' in window) window.localStorage.setItem(key, JSON.stringify(val)) } catch {} }

const defaultWeapons = [
  { name:'Astartes Bolt Pistol', class:'pistol', damage:'2d10+5', pen:5, rof:3, modes:['single','semi'], tearing:true },
  { name:'Astartes Bolter (Godwyn)', class:'basic', damage:'2d10+5', pen:5, rof:4, modes:['single','semi','full'], tearing:true },
  { name:'Astartes Boltgun (Stalker)', class:'basic', damage:'2d10+5', pen:5, rof:1, modes:['single'], tearing:true },
  { name:'Astartes Combi-Weapon', class:'basic', damage:'2d10+5', pen:5, rof:4, modes:['single','semi','full'], tearing:true },
  { name:'Astartes Heavy Bolter', class:'heavy', damage:'2d10+10', pen:6, rof:10, modes:['full'], tearing:true },
  { name:'Astartes Storm Bolter', class:'basic', damage:'2d10+5', pen:5, rof:4, modes:['single','semi','full'], tearing:true },
  { name:'Astartes Plasma Cannon', class:'heavy', damage:'2d10+11', pen:10, rof:1, modes:['single'] },
  { name:'Astartes Plasma Gun (Ragefire)', class:'basic', damage:'1d10+9', pen:8, rof:2, modes:['single','semi'] },
  { name:'Astartes Plasma Pistol', class:'pistol', damage:'1d10+8', pen:8, rof:2, modes:['single','semi'] },
  { name:'Astartes Infernus Pistol', class:'pistol', damage:'2d10+8', pen:13, rof:1, modes:['single'] },
  { name:'Astartes Meltagun (Vulkan)', class:'basic', damage:'2d10+8', pen:13, rof:1, modes:['single'] },
  { name:'Astartes Multi-melta (Maxima)', class:'heavy', damage:'4d10+6', pen:13, rof:1, modes:['single'] },
  { name:'Astartes Flamer', class:'basic', damage:'2d10+2', pen:3, rof:1, modes:['single'] },
  { name:'Astartes Hand Flamer', class:'pistol', damage:'2d10+2', pen:3, rof:1, modes:['single'] },
  { name:'Astartes Heavy Flamer', class:'heavy', damage:'2d10+6', pen:6, rof:1, modes:['single'] },
  { name:'Astartes Assault Cannon', class:'heavy', damage:'3d10+6', pen:6, rof:10, modes:['full'], tearing:true },
  { name:'Astartes Shotgun', class:'basic', damage:'2d10+3', pen:0, rof:2, modes:['single','semi'], reliable:true },
  { name:'Autogun', class:'basic', damage:'1d10+3', pen:0, rof:10, modes:['single','semi','full'] },
  { name:'Autopistol', class:'pistol', damage:'1d10+2', pen:0, rof:6, modes:['single','full'] },
  { name:'Astartes Lascannon', class:'heavy', damage:'6d10+10', pen:10, rof:1, modes:['single'] },
  { name:'Lasgun', class:'basic', damage:'1d10+3', pen:0, rof:3, modes:['single','semi'], reliable:true },
  { name:'Laspistol', class:'pistol', damage:'1d10+2', pen:0, rof:1, modes:['single'], reliable:true },
  { name:'Astartes Digital Flamer', class:'pistol', damage:'2d10+2', pen:3, rof:1, modes:['single'] },
  { name:'Astartes Digital Laser', class:'pistol', damage:'1d10+4', pen:7, rof:1, modes:['single'], reliable:true },
  { name:'Astartes Digital Melta', class:'pistol', damage:'2d10+5', pen:12, rof:1, modes:['single'] },
  { name:'Astartes Sniper Rifle', class:'basic', damage:'1d10', pen:0, rof:1, modes:['single'] },
  { name:'Astartes Chainsword', class:'melee', damage:'1d10+3', pen:4, rof:1, modes:['single'], tearing:true },
  { name:'Astartes Power Axe', class:'melee', damage:'1d10+8', pen:6, rof:1, modes:['single'] },
  { name:'Astartes Power Sword', class:'melee', damage:'1d10+6', pen:6, rof:1, modes:['single'] },
  { name:'Astartes Power Fist', class:'melee', damage:'2d10', pen:9, rof:1, modes:['single'] },
  { name:'Astartes Thunder Hammer', class:'melee', damage:'2d10+5', pen:8, rof:1, modes:['single'] },
  { name:'Astartes Lightning Claw', class:'melee', damage:'1d10+6', pen:8, rof:1, modes:['single'], tearing:true },
  { name:'Astartes Chainfist', class:'melee', damage:'2d10', pen:10, rof:1, modes:['single'], tearing:true },
  { name:'Astartes Combat Knife', class:'melee', damage:'1d10+2', pen:2, rof:1, modes:['single'] },
  { name:'Ceremonial Sword', class:'melee', damage:'1d10+3', pen:2, rof:1, modes:['single'] },
  { name:'Sacris Claymore', class:'melee', damage:'2d10+2', pen:2, rof:1, modes:['single'] },
  { name:'Astartes Force Staff', class:'melee', damage:'1d10+1', pen:0, rof:1, modes:['single'] },
  { name:'Astartes Force Sword', class:'melee', damage:'1d10+2', pen:2, rof:1, modes:['single'] }
]

const defaultEnemies = [
  { name:'Custom/None', tb:4, armour:5, wounds:20 },
  { name:'Imperial Guardsman', tb:3, armourByLoc:{'Head':4,'Body':4,'Left Arm':4,'Right Arm':4,'Left Leg':4,'Right Leg':4}, wounds:10 },
  { name:'Chaos Space Marine', tb:8, armourByLoc:{'Head':8,'Body':10,'Left Arm':8,'Right Arm':8,'Left Leg':8,'Right Leg':8}, wounds:29 },
  { name:'Tyranid Warrior', tb:10, armourByLoc:{'Head':8,'Body':8,'Left Arm':8,'Right Arm':8,'Left Leg':8,'Right Leg':8}, wounds:48 },
  { name:'Hormagaunt', tb:3, armourByLoc:{'Head':3,'Body':3,'Left Arm':3,'Right Arm':3,'Left Leg':3,'Right Leg':3}, wounds:9 },
  { name:'Termagant', tb:3, armourByLoc:{'Head':3,'Body':3,'Left Arm':3,'Right Arm':3,'Left Leg':3,'Right Leg':3}, wounds:9 },
  { name:'Hive Tyrant', tb:15, armourByLoc:{'Head':10,'Body':10,'Left Arm':10,'Right Arm':10,'Left Leg':10,'Right Leg':10}, wounds:120 },
  { name:'Tau Commander (Crisis Suit)', tb:10, armourByLoc:{'Head':9,'Body':9,'Left Arm':9,'Right Arm':9,'Left Leg':9,'Right Leg':9}, wounds:90 },
  { name:'Industrial Servitor', tb:5, armourByLoc:{'Head':7,'Body':7,'Left Arm':7,'Right Arm':7,'Left Leg':7,'Right Leg':7}, wounds:20 },
  { name:'Ork Boy', tb:5, armour:3, wounds:13 },
  { name:'Ork Nob', tb:6, armour:4, wounds:22 },
  { name:'Genestealer', tb:6, armour:6, wounds:22 }
]

function mergeEnemies(existing, incoming) {
  const byName = new Map()
  for (const enemy of [...(existing || []), ...(incoming || [])]) {
    if (!enemy?.name) continue
    byName.set(String(enemy.name).toLowerCase(), enemy)
  }
  return Array.from(byName.values()).sort((a, b) => {
    if (a.name === 'Custom/None') return -1
    if (b.name === 'Custom/None') return 1
    return a.name.localeCompare(b.name)
  })
}

function missionEnemyToRoller(enemy, sceneCombat) {
  if (!enemy) return null
  const name = typeof enemy === 'string' ? enemy : enemy.name || enemy.type
  if (!name) return null
  const source = typeof enemy === 'object' ? enemy : {}
  const isHorde = !!(source.horde || /horde/i.test(String(name)))
  const ag = typeof source.ag === 'number'
    ? source.ag
    : (typeof source.agBonus === 'number' ? source.agBonus * 10 : undefined)
  return {
    name,
    horde: isHorde,
    magnitude: isHorde ? (source.magnitude ?? source.wounds ?? 20) : undefined,
    attack: source.attack || source.weapon || sceneCombat?.weapon,
    tb: source.tb ?? sceneCombat?.targetTB ?? 4,
    armour: source.armour ?? source.ap ?? sceneCombat?.targetArmour ?? 0,
    wounds: isHorde ? (source.magnitude ?? source.wounds ?? 20) : (source.wounds ?? 20),
    ws: source.ws ?? sceneCombat?.ws,
    bs: source.bs ?? sceneCombat?.bs,
    ag,
    damage: extractDiceDamage(source.damage ?? sceneCombat?.damage) || (source.damage ?? sceneCombat?.damage),
    pen: source.pen ?? extractPen(source.damage ?? sceneCombat?.damage),
    special: source.special,
    source: 'active mission'
  }
}

function normalizeWeapon(w) {
  const name = String(w.name||'').trim()
  const clsRaw = String(w.class||'other').toLowerCase()
  const cls = ['melee','pistol','basic','heavy','other'].includes(clsRaw) ? clsRaw : 'other'
  const damage = String(w.damage||'1d10').trim()
  let modes = Array.isArray(w.modes) ? w.modes.map(m=>String(m).toLowerCase()) : []
  modes = modes.filter(m=>['single','semi','full'].includes(m))
  if (modes.length===0) modes = ['single']
  const rof = Math.max(1, Number.isFinite(+w.rof) ? +w.rof : 1)
  const pen = Math.max(0, Number.isFinite(+w.pen) ? +w.pen : 0)
  const tearing = !!w.tearing
  const reliable = !!w.reliable
  const proven = Math.max(0, Number.isFinite(+w.proven) ? +w.proven : 0)
  return { name, class: cls, damage, modes, rof, pen, tearing, reliable, proven }
}

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' } }

// Read a characteristic across casings — sheets store lowercase (ws/bs/ag/t);
// some enemy sources use upper-case. Accepts numeric or numeric-string values.
function readChar(chars, key) {
  if (!chars || typeof chars !== 'object') return undefined
  for (const k of [key, key.toLowerCase(), key.toUpperCase()]) {
    const v = chars[k]
    if (typeof v === 'number' && Number.isFinite(v)) return v
    if (typeof v === 'string' && v.trim() !== '' && Number.isFinite(+v)) return +v
  }
  return undefined
}

// Player sheet armour is keyed head/body/ra/la/rl/ll; map to roller locations.
const PLAYER_ARMOUR_MAP = { head: 'Head', body: 'Body', ra: 'Right Arm', la: 'Left Arm', rl: 'Right Leg', ll: 'Left Leg' }

function playerArmourToMap(armour) {
  const out = { 'Head': 0, 'Body': 0, 'Left Arm': 0, 'Right Arm': 0, 'Left Leg': 0, 'Right Leg': 0 }
  if (armour && typeof armour === 'object') {
    for (const [k, part] of Object.entries(PLAYER_ARMOUR_MAP)) {
      const v = armour[k]
      if (v != null && String(v).trim() !== '' && Number.isFinite(+v)) out[part] = Math.max(0, +v)
    }
  }
  return out
}

// Player wounds may be a number or an object { total, current }.
function playerWoundsTotals(w) {
  if (typeof w === 'number') return { max: Math.max(0, w), cur: Math.max(0, w) }
  if (w && typeof w === 'object') {
    const max = Number.isFinite(+w.total) ? Math.max(0, +w.total) : 0
    const cur = Number.isFinite(+w.current) ? Math.max(0, +w.current) : max
    return { max, cur: cur > 0 ? cur : max }
  }
  return { max: 0, cur: 0 }
}

function DeathwatchRoller({ authedPlayer }) {
  const [bs,setBS] = useState(45)
  const [ws,setWS] = useState(45)
  const [defenderBS, setDefenderBS] = useState(45)
  const [defenderWS, setDefenderWS] = useState(45)
  const [defenderAg, setDefenderAg] = useState(45)
  const [modifier,setModifier] = useState(0)
  const [defenderModifier,setDefenderModifier] = useState(0)
  const [defenseType, setDefenseType] = useState('dodge')
  const [coverBonus, setCoverBonus] = useState(0)
  const [reactionUsed, setReactionUsed] = useState(false)
  const [aim,setAim] = useState(0)
  const [difficulty,setDifficulty] = useState('normal')
  const [awaitingDefense, setAwaitingDefense] = useState(false)
  const [pendingHits, setPendingHits] = useState(null)
  const [mode,setMode] = useState('single')
  const [rof,setRof] = useState(1)
  const [damage,setDamage] = useState('1d10+5')
  const [tearing,setTearing] = useState(false)
  const [proven,setProven] = useState(0)
  const [pen,setPen] = useState(0)
  const [reliable,setReliable] = useState(false)
  const [rfConfirm,setRfConfirm] = useState(true)
  const [targetTB,setTargetTB] = useState(4)
  const [targetArmour,setTargetArmour] = useState(5)
  const [weapons,setWeapons] = useState(()=> {
    const stored = safeGet(STORAGE_WEAPONS)
    if (stored && Array.isArray(stored)) return stored
    const built = buildWeaponsList()
    if (built && built.length) return mergeWeapons(defaultWeapons, built)
    return defaultWeapons
  })
  const [weaponName,setWeaponName] = useState('')
  const [enemies,setEnemies] = useState(defaultEnemies)
  const [enemiesLoading,setEnemiesLoading] = useState(false)
  const [enemiesError,setEnemiesError] = useState(null)
  const [enemyName,setEnemyName] = useState('Custom/None')
  const [playerSkills, setPlayerSkills] = useState({})
  const [playerCharacteristics, setPlayerCharacteristics] = useState({})
  const [players, setPlayers] = useState([]) // GM roster for "roll as" actor selection
  const [actingAs, setActingAs] = useState('') // "player:Name" | "enemy:Name" | '' (self/manual)
  const [rollerTab, setRollerTab] = useState('combat')
  const [lastCombatResult, setLastCombatResult] = useState(null)

  // ── Horde combat state ─────────────────────────────────────────────────────
  const [hordeCombat, setHordeCombat] = useState(null) // { enemy, players: [{name, ag, bs, ws, wounds, maxWounds, initiative, alive}], round, turnIndex, log }
  const [selectedHordePlayers, setSelectedHordePlayers] = useState([]) // names of selected players
  const isGM = authedPlayer === 'gm'
  // Fate belongs to whichever player is acting: the logged-in player normally, or
  // the player a GM is "rolling as" (GM/enemy actors have no fate pool).
  const fateSubject = isGM
    ? (actingAs.startsWith('player:') ? actingAs.slice('player:'.length) : null)
    : authedPlayer
  const { fate, spendFate } = useFate(fateSubject)

  // Load the player roster (GM only) so the GM can roll as any player. Full
  // tabInfo is only returned when authenticated, so send the GM/session headers.
  useEffect(() => {
    ;(async () => {
      try {
        const headers = {}
        if (isGM) {
          headers['x-gm-secret'] = process.env.REACT_APP_GM_SECRET || 'defaultsecret'
          try {
            const sid = window.localStorage.getItem('dw:shop:sessionId')
            if (sid) headers['x-session-id'] = JSON.parse(sid)
          } catch { /* ignore */ }
        }
        const res = await fetch('/api/players', { cache: 'no-store', headers })
        if (!res.ok) return
        const data = await res.json()
        const list = Array.isArray(data) ? data : (data.players || [])
        const names = list.filter(p => p && p.name).map(p => p.name)
        if (isGM && list.some(p => p.tabInfo)) {
          setPlayers(list.filter(p => p && p.name))
          return
        }
        const hydrated = await Promise.all(names.map(async name => {
          try {
            const playerRes = await fetch(`/api/players/${encodeURIComponent(name)}`, { cache: 'no-store' })
            if (!playerRes.ok) return { name }
            const player = await playerRes.json()
            return player && player.name ? player : { name }
          } catch {
            return { name }
          }
        }))
        setPlayers(hydrated.filter(p => p && p.name))
      } catch (e) {
        console.info('[DW] roster load failed', e && e.message)
      }
    })()
  }, [isGM])

  // Load enemies from database API
  async function loadEnemiesFromAPI() {
    setEnemiesLoading(true)
    setEnemiesError(null)
    try {
      const response = await fetch('/api/bestiary/enemies', { cache: 'no-store' })
      let apiEnemies = []
      if (response.ok) {
        apiEnemies = await response.json()
        console.log(`Loaded ${apiEnemies.length} enemies from database`)
      } else {
        throw new Error(`HTTP ${response.status}`)
      }

      let missionEnemies = []
      try {
        const missionResponse = await fetch('/api/missions/active/player', { cache: 'no-store' })
        if (missionResponse.ok) {
          const mission = await missionResponse.json()
          const scene = Array.isArray(mission?.scenes) ? mission.scenes[0] : null
          const sceneEnemies = Array.isArray(scene?.enemies) ? scene.enemies : []
          missionEnemies = sceneEnemies
            .map(enemy => missionEnemyToRoller(enemy, scene?.combat))
            .filter(Boolean)
        }
      } catch (missionError) {
        console.info('No active mission enemies loaded:', missionError?.message)
      }

      const allEnemies = mergeEnemies(defaultEnemies, [...apiEnemies, ...missionEnemies])
      setEnemies(allEnemies)
    } catch (error) {
      console.error('Failed to load enemies from API:', error)
      setEnemiesError(error.message)
      // Keep default enemies on error
      setEnemies(defaultEnemies)
    } finally {
      setEnemiesLoading(false)
    }
  }

  // Load enemies on component mount
  useEffect(() => {
    loadEnemiesFromAPI()
  }, [])

  // Populate module-scoped built weapons from the server-side DB API so
  // buildWeaponsList() can use database-sourced weapon entries instead of
  // falling back to packaged JSON files at runtime.
  useEffect(() => {
    (async () => {
        try {
          const res = await fetch('/api/weapons', { cache: 'no-store' })
          if (!res.ok) return
          const json = await res.json()
          // Some consumers expect the original build DB shape with keys like
          // rangedWeapons / meleeWeapons / grenades / other. If the API returns
          // a flat array, group by category to mimic that shape.
          let built = json
          if (Array.isArray(json)) {
            const grouped = { rangedWeapons: [], meleeWeapons: [], grenades: [], other: [] }
            for (const it of json) {
              const cat = String(it.category || '').toLowerCase()
              if (cat.includes('ranged') || cat.includes('ranged weapon') || cat.includes('ranged')) grouped.rangedWeapons.push(it)
              else if (cat.includes('melee')) grouped.meleeWeapons.push(it)
              else if (cat.includes('grenade')) grouped.grenades.push(it)
              else grouped.other.push(it)
            }
            built = grouped
          }
          // assign to module-scoped variable used by buildWeaponsList()
          _builtWeapons = built
          console.info('[DW] Loaded built weapons from /api/weapons', Array.isArray(json) ? json.length : Object.keys(json || {}).length)
      } catch (err) {
        console.info('[DW] Failed to load built weapons from API:', err && err.message)
      }
    })()
  }, [])

  const trackerInit = safeGet(STORAGE_TRACKER)
  const playerTrackerInit = safeGet(STORAGE_PLAYER_TRACKER)
  const [isHordeMode, setIsHordeMode] = useState(false)
  const [maxWounds,setMaxWounds] = useState(() => Math.max(0, trackerInit?.maxWounds ?? 20))
  const [curWounds,setCurWounds] = useState(() => Math.max(0, Math.min(trackerInit?.curWounds ?? 20, trackerInit?.maxWounds ?? 20)))
  const [playerMaxWounds,setPlayerMaxWounds] = useState(() => Math.max(0, playerTrackerInit?.maxWounds ?? 20))
  const [playerCurWounds,setPlayerCurWounds] = useState(() => Math.max(0, Math.min(playerTrackerInit?.curWounds ?? 20, playerTrackerInit?.maxWounds ?? 20)))
  const [partDamage,setPartDamage] = useState(() => {
    const zero = { 'Head':0, 'Body':0, 'Left Arm':0, 'Right Arm':0, 'Left Leg':0, 'Right Leg':0 }
    if (!trackerInit?.partDamage) return zero
    return { ...zero, ...trackerInit.partDamage }
  })

  const [armourMap,setArmourMap] = useState({ 'Head':5, 'Body':5, 'Left Arm':5, 'Right Arm':5, 'Left Leg':5, 'Right Leg':5 })

  const [manageWeapons,setManageWeapons] = useState(false)
  const [manageTab,setManageTab] = useState('json')
  const [appendMode,setAppendMode] = useState(true)
  const [weaponsJSON,setWeaponsJSON] = useState(JSON.stringify(weapons,null,2))
  const [weaponsCSV,setWeaponsCSV] = useState('name,class,damage,pen,rof,modes,tearing,proven,reliable')
  const [weaponFilter,setWeaponFilter] = useState('')
  const [presets,setPresets] = useState(()=> safeGet(STORAGE_PRESETS) ?? [])
  const [presetName,setPresetName] = useState('')
  const [history,setHistory] = useState(()=> safeGet(STORAGE_HISTORY) ?? [])
  const [error,setError] = useState('')
  const [info,setInfo] = useState('')
  const [combatRound, setCombatRound] = useState(() => {
    try { const s = JSON.parse(localStorage.getItem('dw:combat-state') || 'null'); return s || null; } catch { return null; }
  })
  const [normalCombat, setNormalCombat] = useState(null)
  const [progressStep, setProgressStep] = useState('idle') // idle | attack | defense | damage | wounds
  const [lastAppliedDamage, setLastAppliedDamage] = useState(0)
  const [lastRollState, setLastRollState] = useState({ label: 'Ready', tone: 'slate' })
  
  // Manual mode state variables
  const [manualMode, setManualMode] = useState(false)
  const [manualAttackRoll, setManualAttackRoll] = useState('')
  const [manualDefenseRoll, setManualDefenseRoll] = useState('')
  const [manualDamageRolls, setManualDamageRolls] = useState('')

  const applyMissionContext = useCallback((ctx) => {
    if (!ctx || typeof ctx !== 'object') return
    if (typeof ctx.bs === 'number') setBS(ctx.bs)
    if (typeof ctx.ws === 'number') setWS(ctx.ws)
    if (typeof ctx.modifier === 'number') setModifier(ctx.modifier)
    if (typeof ctx.aim === 'number') setAim(ctx.aim)
    if (ctx.mode) setMode(ctx.mode)
    if (typeof ctx.rof === 'number') setRof(ctx.rof)
    if (ctx.damage) setDamage(ctx.damage)
    if (typeof ctx.tearing === 'boolean') setTearing(ctx.tearing)
    if (typeof ctx.proven === 'number') setProven(ctx.proven)
    if (typeof ctx.pen === 'number') setPen(ctx.pen)
    if (typeof ctx.reliable === 'boolean') setReliable(ctx.reliable)
    if (typeof ctx.targetTB === 'number') setTargetTB(ctx.targetTB)
    if (typeof ctx.targetArmour === 'number') {
      setTargetArmour(ctx.targetArmour)
      setArmourMap(uniformArmourMap(ctx.targetArmour))
    }
    if (ctx.weapon) setWeaponName(ctx.weapon)
    if (ctx.enemy) setEnemyName(ctx.enemy)
  }, [])

  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])

  // Fire-mode to-hit modifier (ranged only, Deathwatch core): Semi-Auto Burst
  // grants +10 BS and Full-Auto Burst grants +20 BS. Melee attacks are unaffected.
  const modeMod = useMemo(() => {
    if (usingSkill !== 'BS') return 0
    if (mode === 'semi') return 10
    if (mode === 'full') return 20
    return 0
  }, [usingSkill, mode])

  // 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 + modeMod)), [baseSkill, modifier, aim, diffMod, conditionMod, modeMod])

  useEffect(()=>{ safeSet(STORAGE_HISTORY, history) },[history])
  useEffect(()=>{ safeSet(STORAGE_PRESETS, presets) },[presets])
  useEffect(()=>{ safeSet(STORAGE_WEAPONS, weapons) },[weapons])
  // Removed localStorage persistence - using database API instead
  useEffect(()=>{ safeSet(STORAGE_TRACKER, { maxWounds, curWounds, partDamage }) },[maxWounds, curWounds, partDamage])
  useEffect(()=>{ safeSet(STORAGE_PLAYER_TRACKER, { maxWounds: playerMaxWounds, curWounds: playerCurWounds }) },[playerMaxWounds, playerCurWounds])

  useEffect(() => {
    function readContext() {
      const ctx = safeGet(MISSION_ROLL_CONTEXT_KEY)
      if (ctx) applyMissionContext(ctx)
    }
    readContext()
    window.addEventListener('dw:mission-roll-context', readContext)
    return () => window.removeEventListener('dw:mission-roll-context', readContext)
  }, [applyMissionContext])

  useEffect(() => {
    function onCombatState(e) {
      setCombatRound(e.detail || null)
    }
    window.addEventListener('dw:combat-state', onCombatState)
    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 || {}
	        const sheetSkills = tab.skills || {}
	        setPlayerCharacteristics(chars)
	        setPlayerSkills(sheetSkills)

	        // Sheets store characteristics lower-case (bs/ws/ag); read robustly.
	        const bsv = readChar(chars, 'bs'); if (bsv != null) setBS(bsv)
        const wsv = readChar(chars, 'ws'); if (wsv != null) setWS(wsv)
        const agv = readChar(chars, 'ag'); if (agv != null) setDefenderAg(agv)

        // Wounds may be a number or { total, current }.
        const { max: sheetMaxWounds, cur: sheetCurWounds } = playerWoundsTotals(tab.wounds)
        if (sheetMaxWounds > 0) {
          setPlayerMaxWounds(sheetMaxWounds)
          setPlayerCurWounds(sheetCurWounds)
        }

        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 () => {
      const stored = safeGet(STORAGE_WEAPONS)
      if ((!stored || !Array.isArray(stored) || stored.length===0)) {
        // Prefer server API for weapons; if unavailable, keep defaultWeapons
        try {
          const res = await fetch('/api/weapons', { cache: 'no-store' })
          if (res.ok) {
            const json = await res.json()
            // Attempt mapping via existing helpers
            let list = []
            if (Array.isArray(json)) list = json.map(item => item.stats ? mapBuildEntryToWeapon(item) : normalizeWeapon(item))
            else if (json && (json.rangedWeapons || json.meleeWeapons)) {
              if (Array.isArray(json.rangedWeapons)) list.push(...json.rangedWeapons.map(mapBuildEntryToWeapon))
              if (Array.isArray(json.meleeWeapons)) list.push(...json.meleeWeapons.map(mapBuildEntryToWeapon))
              if (Array.isArray(json.grenades)) list.push(...json.grenades.map(mapBuildEntryToWeapon))
              if (Array.isArray(json.other)) list.push(...json.other.map(mapBuildEntryToWeapon))
            }
            const merged = mergeWeapons(defaultWeapons, list && list.length ? list : [])
            setWeapons(merged)
            safeSet(STORAGE_WEAPONS, merged)
            if (list && list.length) setInfo('Imported weapons from database')
          } else {
            const merged = mergeWeapons(defaultWeapons, [])
            setWeapons(merged)
            safeSet(STORAGE_WEAPONS, merged)
          }
        } catch (e) {
          console.info('[DW] first-run weapons fetch failed', e && e.message)
          const merged = mergeWeapons(defaultWeapons, [])
          setWeapons(merged)
          safeSet(STORAGE_WEAPONS, merged)
        }
      }
    })()
  }, [])

  useEffect(()=>{ if (curWounds>maxWounds) setCurWounds(maxWounds) },[maxWounds, curWounds])
  useEffect(()=>{ if (playerCurWounds>playerMaxWounds) setPlayerCurWounds(playerMaxWounds) },[playerMaxWounds, playerCurWounds])

  const filteredWeapons = useMemo(()=>{
    const q = weaponFilter.trim().toLowerCase()
    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)
    const w = weapons.find(x=>x.name===name)
    if (!w) return
    const d = computeWeaponDefaults(w)
    setDamage(d.damage); setTearing(d.tearing); setProven(d.proven); setPen(d.pen); setReliable(d.reliable); setRof(d.rof); setMode(d.mode)
  }

  function uniformArmourMap(val){
    const v = Math.max(0, Number.isFinite(+val)? +val : 0)
    return { 'Head':v, 'Body':v, 'Left Arm':v, 'Right Arm':v, 'Left Leg':v, 'Right Leg':v }
  }

  function onSelectEnemy(name) {
    setEnemyName(name)
    const e = enemies.find(x=>x.name===name)
    if (!e) return
  const isHorde = !!e.horde
  setIsHordeMode(isHorde)
  setTargetTB(e.tb); setTargetArmour(e.armour ?? 0)
  if (isHorde) {
    // Horde: no body parts, just magnitude pool
    setArmourMap(uniformArmourMap(e.armour ?? 0))
    const mag = Number(e.magnitude ?? e.wounds ?? 20)
    const magVal = Number.isFinite(mag) && mag > 0 ? Math.round(mag) : 20
    setMaxWounds(magVal); setCurWounds(magVal); setPartDamage({})
  } else {
    if (e.armourByLoc) setArmourMap({ ...uniformArmourMap(0), ...e.armourByLoc })
    else setArmourMap(uniformArmourMap(e.armour ?? 0))
    // Apply wounds — coerce robustly (bestiary wounds can arrive as strings). Fall
    // back to a usable default so a missing/zero value doesn't leave a dead tracker.
    const woundsNum = Number(e.wounds)
    const woundsVal = Number.isFinite(woundsNum) && woundsNum > 0 ? Math.round(woundsNum) : 15
    setMaxWounds(woundsVal); setCurWounds(woundsVal); setPartDamage({ 'Head':0, 'Body':0, 'Left Arm':0, 'Right Arm':0, 'Left Leg':0, 'Right Leg':0 })
  }
    // Robustly extract AG/WS/BS from common enemy shapes: top-level, .characteristics, .tabInfo.characteristics
    function findNumeric(o, keys) {
      if (!o || typeof o !== 'object') return undefined
      for (const k of keys) {
        if (typeof o[k] === 'number') return o[k]
        // case-insensitive variants
        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 agVal = findNumeric(e, ['ag', 'Ag', 'AG']) ?? findNumeric(char, ['ag'])
    const wsVal = findNumeric(e, ['ws', 'Ws', 'WS']) ?? findNumeric(char, ['ws'])
    const bsVal = findNumeric(e, ['bs', 'Bs', 'BS']) ?? findNumeric(char, ['bs'])
    if (typeof agVal === 'number') setDefenderAg(agVal)
    if (typeof wsVal === 'number') setDefenderWS(wsVal)
    if (typeof bsVal === 'number') setDefenderBS(bsVal)

    // Apply any stored/default defender modifier (try multiple common keys)
    const modVal = findNumeric(e, ['defenderModifier','defenderMod','defMod','defenceModifier','defenceMod']) ?? findNumeric(e, ['modifier','mod'])
    if (typeof modVal === 'number') setDefenderModifier(modVal)
    else setDefenderModifier(0)
  }

  // ── GM "roll as" (attacker) ────────────────────────────────────────────────
  function applyPlayerAttacker(player) {
    const tab = player.tabInfo || {}
    const chars = tab.characteristics || {}
    setPlayerCharacteristics(chars)
    setPlayerSkills(tab.skills || {})
    const bsv = readChar(chars, 'bs'); if (bsv != null) setBS(bsv)
    const wsv = readChar(chars, 'ws'); if (wsv != null) setWS(wsv)
    const pw = Array.isArray(tab.weapons) ? tab.weapons.map(mapPlayerWeapon).filter(w => w.name) : []
    if (pw.length) {
      setWeapons(pw)
      const first = pw[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)
    }
  }

  function applyEnemyAttacker(enemy) {
    const chars = enemy.characteristics || (enemy.tabInfo && enemy.tabInfo.characteristics) || {}
    const bsv = (typeof enemy.bs === 'number' ? enemy.bs : readChar(chars, 'bs'))
    const wsv = (typeof enemy.ws === 'number' ? enemy.ws : readChar(chars, 'ws'))
    // A single attack path uses BS unless a melee weapon is picked; seed both so
    // whichever the GM fires with has a sensible number.
    if (bsv != null || wsv != null) { setBS(bsv ?? wsv); setWS(wsv ?? bsv) }
    const dmg = extractDiceDamage(enemy.damage) || (typeof enemy.damage === 'string' ? enemy.damage : '')
    if (dmg) setDamage(dmg)
    if (enemy.pen != null && Number.isFinite(+enemy.pen)) setPen(+enemy.pen)
    setWeaponName('')      // enemy uses its statline attack, not a weapon-list entry
    setPlayerSkills({})    // no skill sheet → Skill Roller falls back to manual Target
    setPlayerCharacteristics(chars)
  }

  function onSelectActor(value) {
    setActingAs(value)
    if (!value) { setInfo('Rolling as yourself (manual stats)'); return }
    const idx = value.indexOf(':')
    const kind = value.slice(0, idx)
    const name = value.slice(idx + 1)
    if (kind === 'player') { const p = players.find(pl => pl.name === name); if (p) applyPlayerAttacker(p) }
    else if (kind === 'enemy') { const e = enemies.find(en => en.name === name); if (e) applyEnemyAttacker(e) }
    setInfo(`Rolling as ${name}`)
  }

  // ── Target = player (defender) ─────────────────────────────────────────────
  function applyPlayerTarget(player) {
    const tab = player.tabInfo || {}
    const chars = tab.characteristics || {}
    const ag = readChar(chars, 'ag'); if (ag != null) setDefenderAg(ag)
    const ws = readChar(chars, 'ws'); if (ws != null) setDefenderWS(ws)
    const bs = readChar(chars, 'bs'); if (bs != null) setDefenderBS(bs)
    const t = readChar(chars, 't'); if (t != null) setTargetTB(Math.floor(t / 10))
    const amap = playerArmourToMap(tab.armour)
    setArmourMap(amap)
    setTargetArmour(amap['Body'] || 0)
    const { max, cur } = playerWoundsTotals(tab.wounds)
    if (max > 0) {
      setPlayerMaxWounds(max)
      setPlayerCurWounds(cur)
    }
    setDefenderModifier(0)
  }

  function onSelectTarget(value) {
    const p = players.find(pl => pl.name === value)
    if (p) { setEnemyName(value); applyPlayerTarget(p); return }
    onSelectEnemy(value)
  }

  function getAuthedPlayerRecord() {
    if (!authedPlayer || authedPlayer === 'gm') return null
    return players.find(p => String(p.name).toLowerCase() === String(authedPlayer).toLowerCase()) || null
  }

  function buildPlayerCombatant(player, fallbackName) {
    const tab = player?.tabInfo || {}
    const chars = tab.characteristics || {}
    const { max, cur } = playerWoundsTotals(tab.wounds)
    const weaponsList = Array.isArray(tab.weapons) ? tab.weapons.map(mapPlayerWeapon).filter(w => w.name) : []
    const selected = weaponsList.find(w => w.name === weaponName) || pickMainWeapon(weaponsList) || weaponsList[0] || null
    const armour = playerArmourToMap(tab.armour)
    return {
      name: player?.name || fallbackName || 'Player',
      side: 'player',
      kind: 'player',
      bs: readChar(chars, 'bs') ?? bs,
      ws: readChar(chars, 'ws') ?? ws,
      ag: readChar(chars, 'ag') ?? defenderAg,
      tb: Math.floor((readChar(chars, 't') ?? 40) / 10),
      armourMap: armour,
      armourBody: armour.Body || 0,
      wounds: max > 0 ? cur : playerCurWounds,
      maxWounds: max > 0 ? max : playerMaxWounds,
      weapons: weaponsList,
      weaponName: selected?.name || weaponName || 'Weapon',
      weaponClass: selected?.class || (usingSkill === 'WS' ? 'melee' : 'basic'),
      damage: normalizeDamageExpression(selected?.damage || damage, damage || '1d10+5'),
      tearing: !!(selected?.tearing ?? tearing),
      proven: selected?.proven || proven || 0,
      pen: selected?.pen ?? pen ?? 0,
      rof: selected?.rof || rof || 1,
      mode: (selected?.modes && selected.modes.includes(mode)) ? mode : ((selected?.modes && selected.modes[0]) || mode || 'single'),
      reactionRound: 0,
      alive: (max > 0 ? cur : playerCurWounds) > 0
    }
  }

  function buildEnemyCombatant(enemy) {
    const chars = enemy?.characteristics || (enemy?.tabInfo && enemy.tabInfo.characteristics) || {}
    const damageExpr = normalizeDamageExpression(enemy?.damage, damage || '1d10+3')
    const rawEnemyWounds = Number(enemy?.wounds)
    const enemyMaxWounds = Number.isFinite(rawEnemyWounds) && rawEnemyWounds > 0 ? rawEnemyWounds : maxWounds
    const enemyCurrentWounds = enemy?.name === enemyName ? Math.min(curWounds, enemyMaxWounds) : enemyMaxWounds
    return {
      name: enemy?.name || 'Enemy',
      side: 'enemy',
      kind: 'enemy',
      bs: (typeof enemy?.bs === 'number' ? enemy.bs : (readChar(chars, 'bs') ?? defenderBS)),
      ws: (typeof enemy?.ws === 'number' ? enemy.ws : (readChar(chars, 'ws') ?? defenderWS)),
      ag: (typeof enemy?.ag === 'number' ? enemy.ag : (readChar(chars, 'ag') ?? 30)),
      tb: Number(enemy?.tb ?? targetTB ?? 4),
      armourMap: enemy?.armourByLoc || uniformArmourMap(enemy?.armour ?? targetArmour ?? 0),
      armourBody: Number(enemy?.armour ?? targetArmour ?? 0),
      wounds: enemyCurrentWounds,
      maxWounds: enemyMaxWounds,
      weapons: [],
      weaponName: enemy?.name || 'Attack',
      weaponClass: ((typeof enemy?.ws === 'number' ? enemy.ws : 0) > (typeof enemy?.bs === 'number' ? enemy.bs : 0)) ? 'melee' : 'basic',
      damage: damageExpr,
      tearing: /tearing/i.test(String(enemy?.special || enemy?.damage || '')),
      proven: 0,
      pen: Number(enemy?.pen ?? 0),
      rof: 1,
      mode: 'single',
      reactionRound: 0,
      alive: curWounds > 0
    }
  }

  function buildManualCombatant() {
    return {
      name: authedPlayer && authedPlayer !== 'gm' ? authedPlayer : 'Attacker',
      side: 'player',
      kind: 'manual',
      bs,
      ws,
      ag: defenderAg || 35,
      tb: Math.max(0, Math.floor((playerMaxWounds || 20) / 10)),
      armourMap: uniformArmourMap(0),
      armourBody: 0,
      wounds: playerCurWounds,
      maxWounds: playerMaxWounds,
      weapons: [],
      weaponName: weaponName || 'Weapon',
      weaponClass: usingSkill === 'WS' ? 'melee' : 'basic',
      damage: normalizeDamageExpression(damage, '1d10+5'),
      tearing,
      proven,
      pen,
      rof,
      mode,
      reactionRound: 0,
      alive: playerCurWounds > 0
    }
  }

  function syncNormalCombatTrackers(combat) {
    if (!combat) return
    const playerActor = combat.order.find(actor => actor.side === 'player') || null
    const enemyActor = combat.order.find(actor => actor.side === 'enemy') || null
    if (playerActor) {
      setPlayerMaxWounds(playerActor.maxWounds)
      setPlayerCurWounds(Math.max(0, playerActor.wounds))
    }
    if (enemyActor) {
      setMaxWounds(enemyActor.maxWounds)
      setCurWounds(Math.max(0, enemyActor.wounds))
    }
  }

  function startNormalCombat() {
    const actorPlayer = actingAs.startsWith('player:') ? players.find(p => p.name === actingAs.slice('player:'.length)) : null
    const actorEnemy = actingAs.startsWith('enemy:') ? enemies.find(en => en.name === actingAs.slice('enemy:'.length)) : null
    const attacker = actorEnemy
      ? buildEnemyCombatant(actorEnemy)
      : actorPlayer
        ? buildPlayerCombatant(actorPlayer, actorPlayer.name)
        : (getAuthedPlayerRecord() ? buildPlayerCombatant(getAuthedPlayerRecord(), authedPlayer) : buildManualCombatant())

    const targetPlayer = players.find(p => p.name === enemyName) || null
    const targetEnemy = enemies.find(en => en.name === enemyName) || null
    const defender = targetPlayer
      ? buildPlayerCombatant(targetPlayer, targetPlayer.name)
      : targetEnemy
        ? buildEnemyCombatant(targetEnemy)
        : null

    if (!defender) {
      setInfo('Select a valid target to start combat.')
      return
    }
    if (attacker.name === defender.name && attacker.side === defender.side) {
      setInfo('Attacker and defender cannot be the same combatant.')
      return
    }

    const baseOrder = [attacker, defender].map(actor => {
      const initiativeRoll = Math.floor(Math.random() * 10) + 1
      const initiative = initiativeRoll + Math.floor((actor.ag || 30) / 10)
      return { ...actor, initiativeRoll, initiative }
    }).sort((a, b) => b.initiative - a.initiative)

    const combat = {
      active: true,
      round: 1,
      turnIndex: 0,
      order: baseOrder,
      currentTurn: { name: baseOrder[0].name, initiative: baseOrder[0].initiative },
      nextTurn: { name: baseOrder[1].name, initiative: baseOrder[1].initiative }
    }
    syncNormalCombatTrackers(combat)
    setNormalCombat(combat)
    setInfo(`Combat started. ${baseOrder[0].name} acts first.`)
  }

  function endNormalCombat() {
    setNormalCombat(null)
    setLastRollState({ label: 'Ready', tone: 'slate' })
    setInfo('Normal combat ended.')
  }

  function advanceNormalCombat(combat) {
    if (!combat?.active) return
    const living = combat.order.filter(actor => actor.alive)
    if (living.length <= 1) {
      const survivor = living[0]
      setNormalCombat({ ...combat, active: false })
      setInfo(survivor ? `${survivor.name} wins the combat.` : 'Combat ended.')
      return
    }
    const nextIndex = (combat.turnIndex + 1) % combat.order.length
    const nextRound = nextIndex <= combat.turnIndex ? combat.round + 1 : combat.round
    const nextOrder = nextIndex <= combat.turnIndex
      ? combat.order.map(actor => ({ ...actor, reactionRound: 0 }))
      : combat.order
    const nextCombat = {
      ...combat,
      order: nextOrder,
      round: nextRound,
      turnIndex: nextIndex,
      currentTurn: { name: nextOrder[nextIndex].name, initiative: nextOrder[nextIndex].initiative },
      nextTurn: { name: nextOrder[(nextIndex + 1) % nextOrder.length].name, initiative: nextOrder[(nextIndex + 1) % nextOrder.length].initiative }
    }
    setNormalCombat(nextCombat)
    setInfo(`Round ${nextCombat.round} - ${nextCombat.currentTurn.name}'s turn.`)
  }

  function importFromJSONText(txt) {
    try {
      const arr = JSON.parse(txt)
      if (!Array.isArray(arr)) throw new Error('JSON must be an array')
      const next = arr.map(normalizeWeapon).filter(w=>w.name)
      const merged = appendMode ? mergeWeapons(weapons, next) : next
      setWeapons(merged); setWeaponsJSON(JSON.stringify(merged,null,2)); setInfo(`Loaded ${next.length} weapons`); console.info('[DW] Imported JSON weapons:', next.length)
    } catch (e) { setError('Invalid weapons JSON'); console.error('[DW] Import JSON error:', e) }
  }

  async function fetchWeaponsFromServer() {
    setError(''); setInfo('')
  const endpoints = ['/api/weapons']
    let lastErr = null
    for (const ep of endpoints) {
      try {
        const res = await fetch(ep, { cache: 'no-store' })
        if (!res.ok) throw new Error(`HTTP ${res.status}`)
        const json = await res.json()
        let list = []
        // If the server returned the same shaped build DB (with rangedWeapons/meleeWeapons), map entries
        if (json && (json.rangedWeapons || json.meleeWeapons || json.grenades || json.other)) {
          const items = []
          if (Array.isArray(json.rangedWeapons)) items.push(...json.rangedWeapons.map(mapBuildEntryToWeapon))
          if (Array.isArray(json.meleeWeapons)) items.push(...json.meleeWeapons.map(mapBuildEntryToWeapon))
          if (Array.isArray(json.grenades)) items.push(...json.grenades.map(mapBuildEntryToWeapon))
          if (Array.isArray(json.other)) items.push(...json.other.map(mapBuildEntryToWeapon))
          list = items
        } else if (Array.isArray(json)) {
          // Server might return an already-normalized array
          list = json.map(item => {
            // If item looks like a build entry, map it, otherwise normalize directly
            if (item.stats || item.damage && item.damage.includes('d')) return normalizeWeapon(mapBuildEntryToWeapon(item))
            return normalizeWeapon(item)
          })
        } else {
          throw new Error('Unexpected payload')
        }

        const merged = appendMode ? mergeWeapons(weapons, list) : list
        setWeapons(merged)
        setWeaponsJSON(JSON.stringify(merged,null,2))
        setInfo(`Fetched ${list.length} weapons from ${ep}`)
        console.info('[DW] Fetched weapons from', ep, list.length)
        return
      } catch (e) {
        lastErr = e
        console.info('[DW] fetchWeaponsFromServer failed for', ep, e && e.message)
      }
    }
    setError('Failed to fetch weapons from server')
    console.error('[DW] fetchWeaponsFromServer last error:', lastErr)
  }

  function importFromCSVText(txt) {
    try {
      const rows = simpleCSVParse(txt)
      const mapped = rows.map(r => normalizeWeapon({
        name: r.name,
        class: r.class,
        damage: r.damage,
        pen: Number(r.pen || 0),
        rof: Number(r.rof || 1),
        modes: String(r.modes || 'single').split('|').map(s => s.trim().toLowerCase()),
        tearing: ['1', 'true', 'yes', 'y'].includes(String(r.tearing || '').toLowerCase()),
        proven: Number(r.proven || 0),
        reliable: ['1', 'true', 'yes', 'y'].includes(String(r.reliable || '').toLowerCase()),
      }))
      const merged = appendMode ? mergeWeapons(weapons, mapped) : mapped
      setWeapons(merged); setWeaponsJSON(JSON.stringify(merged,null,2)); setInfo(`Loaded ${mapped.length} weapons from CSV`); console.info('[DW] Imported CSV weapons:', mapped.length)
    } catch (e) { setError('Invalid CSV'); console.error('[DW] Import CSV error:', e) }
  }

  // ...existing code uses module-scope mergeWeapons helper

  function exportWeapons() {
    try {
      const blob = new Blob([JSON.stringify(weapons,null,2)], { type: 'application/json' })
      const url = URL.createObjectURL(blob)
      const a = document.createElement('a')
      a.href = url
      a.download = 'deathwatch-weapons.json'
      document.body.appendChild(a)
      a.click()
      a.remove()
      setInfo('Exported current weapons to JSON')
      console.info('[DW] Exported weapons:', weapons.length)
      setTimeout(()=> URL.revokeObjectURL(url), 2500)
    } catch (e) { setError('Export failed'); console.error('[DW] Export error:', e) }
  }

  function armourFor(where){ return (armourMap[where] ?? targetArmour) }

  function runNormalCombatAttack() {
    if (!normalCombat?.active) return
    setError('')
    setInfo('')
    setProgressStep('attack')
    const combat = {
      ...normalCombat,
      order: normalCombat.order.map(actor => ({ ...actor, armourMap: { ...(actor.armourMap || {}) } }))
    }
    const attacker = combat.order[combat.turnIndex]
    const targetIndex = combat.order.findIndex((actor, idx) => idx !== combat.turnIndex && actor.alive)
    const defender = targetIndex >= 0 ? combat.order[targetIndex] : null
    if (!attacker || !defender || !attacker.alive || !defender.alive) {
      setInfo('No valid combatants remain.')
      setNormalCombat({ ...combat, active: false })
      return
    }

    let attackRoll
    if (manualMode && manualAttackRoll.trim()) {
      const manualRoll = parseInt(manualAttackRoll.trim(), 10)
      if (isNaN(manualRoll) || manualRoll < 1 || manualRoll > 100) {
        setError('Manual attack roll must be a number between 1-100')
        return
      }
      attackRoll = manualRoll
    } else {
      attackRoll = d100()
    }

    const isRanged = String(attacker.weaponClass || '').toLowerCase() !== 'melee'
    const attackTarget = Math.max(0, Math.min(100, isRanged ? attacker.bs : attacker.ws))
    const dg = degrees(attackTarget, attackRoll)
    const where = hitLocationFromRoll(attackRoll)

    if (!dg.success) {
      addRollResult({ id: uid(), ts: Date.now(), attackRoll, target: attackTarget, success: false, dos: 0, dof: dg.dof, mode: attacker.mode, rof: attacker.rof, hits: 0, where, jammed: false, perHit: [], weapon: attacker.weaponName, actor: attacker.name, enemy: defender.name, using: isRanged ? 'BS' : 'WS' })
      setLastRollState({ label: 'Fail', tone: 'rose' })
      setInfo(`${attacker.name} misses ${defender.name} (DoF ${dg.dof}).`)
      setTimeout(() => {
        setProgressStep('idle')
        advanceNormalCombat(combat)
      }, PHASE_TIMING.attack)
      return
    }

    const hits = hitsFromDoS(attacker.mode, dg.dos, attacker.rof)
    let negatedHits = 0
    let defenseRoll = null
    let defenseTarget = null
    let defenseTypeName = null

    if (defender.side === 'player' && defender.alive) {
      const canParry = !isRanged && hasMeleeWeapon(defender.weapons)
      defenseTypeName = canParry ? 'Parry' : 'Dodge'
      defenseTarget = Math.max(0, Math.min(100, canParry ? defender.ws : defender.ag))
      const reactionAvailable = (defender.reactionRound || 0) !== combat.round
      if (reactionAvailable && defenseTarget > 0) {
        setProgressStep('defense')
        defenseRoll = (manualMode && manualDefenseRoll.trim())
          ? parseInt(manualDefenseRoll.trim(), 10)
          : d100()
        const defenseResult = degrees(defenseTarget, defenseRoll)
        defender.reactionRound = combat.round
        negatedHits = defenseResult.success ? Math.min(hits, defenseResult.dos) : 0
        if (defenseResult.success) setLastRollState({ label: 'Defended', tone: 'amber' })
        else setLastRollState({ label: 'Defense Failed', tone: 'rose' })
      } else {
        defenseTypeName = 'No Reaction'
      }
    }

    const hitsToApply = Math.max(0, hits - negatedHits)
    const perHit = []
    let totalApplied = 0
    let nextParts = { ...partDamage }
    let dmgSpec
    try {
      dmgSpec = parseDice(normalizeDamageExpression(attacker.damage, '1d10+5'))
    } catch {
      dmgSpec = parseDice('1d10+5')
    }

    for (let i = 0; i < hitsToApply; i += 1) {
      const rolled = (manualMode && manualDamageRolls.trim())
        ? (() => {
            const manualRolls = manualDamageRolls.trim().split(/[,\s]+/).map(s => parseInt(s.trim(), 10)).filter(n => !isNaN(n) && n >= 1)
            return { rolls: manualRolls, total: manualRolls.reduce((a, b) => a + b, 0) }
          })()
        : rollDice(dmgSpec.terms, { tearing: !!attacker.tearing, proven: attacker.proven || 0 })
      const raw = rolled.total + dmgSpec.flat
      const arForLoc = Math.max(0, ((defender.armourMap && defender.armourMap[where]) ?? defender.armourBody ?? 0) - (attacker.pen || 0))
      const mitigated = Math.max(0, raw - defender.tb - arForLoc)
      perHit.push({ where, raw, mitigated, detail: `${rolled.rolls.join(', ')}${dmgSpec.flat ? ` + ${dmgSpec.flat}` : ''} - TB ${defender.tb} - Armour(${where}) ${arForLoc} = ${mitigated}` })
      totalApplied += mitigated
      if (defender.side === 'enemy' && BODY_PARTS.includes(where)) nextParts[where] = (nextParts[where] || 0) + mitigated
    }

    defender.wounds = Math.max(0, defender.wounds - totalApplied)
    defender.alive = defender.wounds > 0

    addRollResult({
      id: uid(),
      ts: Date.now(),
      attackRoll,
      target: attackTarget,
      success: true,
      dos: dg.dos,
      dof: 0,
      mode: attacker.mode,
      rof: attacker.rof,
      hits,
      hitsNegated: negatedHits,
      hitsApplied: hitsToApply,
      where,
      jammed: false,
      perHit,
      weapon: attacker.weaponName,
      actor: attacker.name,
      enemy: defender.name,
      using: isRanged ? 'BS' : 'WS',
      defenseRoll,
      defenseTarget,
      defenseType: defenseTypeName,
      defenseFailed: defenseRoll != null ? negatedHits === 0 : false
    })

    if (defender.side === 'enemy') {
      setPartDamage(nextParts)
      setCurWounds(defender.wounds)
    } else {
      setPlayerCurWounds(defender.wounds)
    }
    syncNormalCombatTrackers(combat)
    setLastAppliedDamage(totalApplied)
    setLastRollState({ label: totalApplied > 0 ? 'Hit' : 'No Damage', tone: totalApplied > 0 ? 'emerald' : 'amber' })
    const defenseText = defenseRoll != null
      ? ` ${defender.name} ${defenseTypeName?.toLowerCase() || 'defends'}${negatedHits > 0 ? ` and negates ${negatedHits}` : ' fails to negate the hit'}.`
      : ''
    setInfo(`${attacker.name} hits ${defender.name}.${defenseText} ${totalApplied > 0 ? `${totalApplied} damage applied.` : 'No damage applied.'}`)
    setNormalCombat(combat)
    const afterAttackDelay = defenseRoll != null ? PHASE_TIMING.defense : PHASE_TIMING.attack
    setTimeout(() => {
      setProgressStep('damage')
      setTimeout(() => {
        setProgressStep('wounds')
        setTimeout(() => {
          setProgressStep('idle')
          advanceNormalCombat(combat)
        }, PHASE_TIMING.wounds)
      }, PHASE_TIMING.damage)
    }, afterAttackDelay)
  }

  function runAttack() {
    if (normalCombat?.active) {
      runNormalCombatAttack()
      return
    }
    setError(''); setInfo('')
  setProgressStep('attack')
    setLastRollState({ label: 'Rolling', tone: 'sky' })
    setReactionUsed(false) // Reset reaction for new attack
    let dmgSpec
    try { dmgSpec = parseDice(damage) } catch (e) { setError('Invalid damage expression'); return }
    
    // Use manual roll if manual mode is enabled
    let attackRoll
    if (manualMode && manualAttackRoll.trim()) {
      const manualRoll = parseInt(manualAttackRoll.trim())
      if (isNaN(manualRoll) || manualRoll < 1 || manualRoll > 100) {
        setError('Manual attack roll must be a number between 1-100')
        return
      }
      attackRoll = manualRoll
    } else {
      attackRoll = d100()
    }
    
    const isRanged = usingSkill==='BS'
    // Ranged weapons jam on high rolls: single shots on 96–00, Semi-/Full-Auto
    // fire on 94–00. Reliable weapons only jam on an unmodified 00 (100).
    let jamThreshold = 101 // melee never jams
    if (isRanged) {
      if (reliable) jamThreshold = 100
      else if (mode === 'semi' || mode === 'full') jamThreshold = 94
      else jamThreshold = 96
    }
    const jammed = attackRoll >= jamThreshold
    const dg = degrees(target, attackRoll)
    const where = hitLocationFromRoll(attackRoll)

    if (dg.success) {
      const hits = hitsFromDoS(mode, dg.dos, rof)
      // Store attack info and prompt for defense roll
      setPendingHits({
        hits,
        attackRoll,
        where,
        isRanged,
        dmgSpec,
        jammed,
        mode,
        rof
      })
      setAwaitingDefense(true)
      setProgressStep('defense')
      setLastRollState({ label: 'Hit', tone: 'emerald' })
      setInfo(`Attack hit! Roll defense to determine if damage is applied.`)
      return
    }

    // Attack failed - no damage
    const res = { id: uid(), ts: Date.now(), attackRoll, target, success: dg.success, dos: dg.dos, dof: dg.dof, mode, rof, hits: 0, where, jammed, perHit: [], weapon: weaponName || undefined, actor: actingAs ? actingAs.split(':').slice(1).join(':') : (authedPlayer || 'Attacker'), enemy: enemyName || undefined, using: usingSkill }
    addRollResult(res)
    setLastRollState({ label: 'Fail', tone: 'rose' })
    setInfo(`Attack missed with DoF ${dg.dof}`)
    // Clear progress after a short delay
    setTimeout(()=> setProgressStep('idle'), 1200)
  }

  function resetTracker() {
    setCurWounds(maxWounds)
    setPartDamage({ 'Head':0, 'Body':0, 'Left Arm':0, 'Right Arm':0, 'Left Leg':0, 'Right Leg':0 })
    setAwaitingDefense(false)
    setPendingHits(null)
    setReactionUsed(false)
    setProgressStep('idle')
    setLastAppliedDamage(0)
    setLastRollState({ label: 'Ready', tone: 'slate' })
    setError('')
    setInfo('')
    setManualAttackRoll('')
    setManualDefenseRoll('')
    setManualDamageRolls('')
  }

  function resetPlayerTracker() {
    setPlayerCurWounds(playerMaxWounds)
  }

  function saveCurrentAsPreset() { if (!presetName.trim()) return; const p = { name: presetName.trim(), bs, ws, modifier, aim, mode, rof, damage, tearing, proven, pen, reliable, rfConfirm, targetTB, targetArmour, weapon: weaponName || undefined, enemy: enemyName || undefined }; setPresets(prev => { const others = prev.filter(x => x.name !== p.name); return [...others, p].sort((a,b)=>a.name.localeCompare(b.name)) }); setPresetName('') }
  function applyPreset(p) { setBS(p.bs ?? bs); setWS(p.ws ?? ws); setModifier(p.modifier); setAim(p.aim); setMode(p.mode); setRof(p.rof); setDamage(p.damage); setTearing(p.tearing); setProven(p.proven); setPen(p.pen); setReliable(p.reliable); setRfConfirm(p.rfConfirm); setTargetTB(p.targetTB); setTargetArmour(p.targetArmour); if (p.weapon) { onSelectWeapon(p.weapon) } else { setWeaponName('') } if (p.enemy) { onSelectEnemy(p.enemy) } else { setEnemyName('Custom/None') } }
  function deletePreset(name) { setPresets(prev => prev.filter(x => x.name !== name)) }
  function clearHistory() { setHistory([]) }

  function runDefense() {
    setError(''); setInfo('')
  setProgressStep('defense')
    if (!pendingHits) return
    
    // Check if reaction is already used
    if (reactionUsed) {
      setError('Reaction already used this round')
      return
    }

    // Determine defense type and target
    let defenseTarget, defenseSkill, defenseTypeName
    const isRanged = pendingHits.isRanged
    
    switch (defenseType) {
      case 'dodge':
        defenseSkill = defenderAg
        defenseTypeName = 'Dodge'
        break
      case 'parry':
        if (!isRanged) {
          defenseSkill = defenderWS
          defenseTypeName = 'Parry'
        } else {
          setError('Cannot parry ranged attacks')
          return
        }
        break
      case 'block':
        defenseSkill = defenderWS
        defenseTypeName = 'Block'
        break
      case 'cover':
        defenseSkill = defenderAg
        defenseTypeName = 'Cover'
        break
      default:
        defenseSkill = isRanged ? defenderBS : defenderWS
        defenseTypeName = isRanged ? 'BS' : 'WS'
    }

    // Calculate defense target with modifiers
    let totalModifier = defenderModifier
    if (defenseType === 'cover') totalModifier += 20 + coverBonus // Cover: +20 plus any input cover bonus
    if (defenseType === 'block') totalModifier += 10 // Block provides +10 bonus
    
    defenseTarget = Math.max(0, Math.min(100, defenseSkill + totalModifier))
    
    // Use manual roll if manual mode is enabled
    let defenseRoll
    if (manualMode && manualDefenseRoll.trim()) {
      const manualRoll = parseInt(manualDefenseRoll.trim())
      if (isNaN(manualRoll) || manualRoll < 1 || manualRoll > 100) {
        setError('Manual defense roll must be a number between 1-100')
        return
      }
      defenseRoll = manualRoll
    } else {
      defenseRoll = d100()
    }
    const dg = degrees(defenseTarget, defenseRoll)

    // Deathwatch: a successful reaction negates one hit per Degree of Success.
    // A single Dodge/Parry does NOT wipe out an entire Semi-/Full-Auto burst —
    // only DoS hits are evaded and any remaining hits still strike.
    const negatedHits = dg.success ? Math.min(pendingHits.hits, dg.dos) : 0
    const hitsToApply = pendingHits.hits - negatedHits

    if (hitsToApply <= 0) {
      setInfo(`${defenseTypeName} successful! ${negatedHits} hit${negatedHits === 1 ? '' : 's'} evaded (DoS ${dg.dos}).`)
      setLastRollState({ label: 'Defended', tone: 'amber' })
      const res = {
        id: uid(),
        ts: Date.now(),
        attackRoll: pendingHits.attackRoll,
        target,
        success: true,
        hits: pendingHits.hits,
        hitsNegated: negatedHits,
        hitsApplied: 0,
        where: pendingHits.where,
        jammed: pendingHits.jammed,
        perHit: [],
        weapon: weaponName || undefined,
        actor: actingAs ? actingAs.split(':').slice(1).join(':') : (authedPlayer || 'Attacker'),
        enemy: enemyName || undefined,
        using: pendingHits.isRanged ? 'BS' : 'WS',
        defenseRoll,
        defenseTarget,
        defenseType: defenseTypeName,
        defenseFailed: false,
        mode: pendingHits.mode,
        rof: pendingHits.rof
      }
      addRollResult(res)
      setAwaitingDefense(false)
      setPendingHits(null)
      setReactionUsed(true)
  // Defense successful - no damage applied; show wounds briefly then idle
  setProgressStep('wounds')
  setTimeout(()=> setProgressStep('idle'), 1200)
      return
    }

    // Defense failed, or only part of a burst was evaded — apply the hits that got through.
    const where = pendingHits.where
    const perHit = []
    let totalApplied = 0
    let nextParts = { ...partDamage }

    for (let i=0; i<hitsToApply; i++) {
      let r
      if (manualMode && manualDamageRolls.trim()) {
        // Parse manual damage rolls
        const manualRolls = manualDamageRolls.trim().split(/[,\s]+/).map(s => parseInt(s.trim())).filter(n => !isNaN(n) && n >= 1)
        if (manualRolls.length === 0) {
          setError('Manual damage rolls must be comma-separated numbers (e.g., "7, 9")')
          return
        }
        r = { rolls: manualRolls, total: manualRolls.reduce((a,b) => a+b, 0) }
      } else {
        r = rollDice(pendingHits.dmgSpec.terms, { tearing, proven })
      }
      
      const tenCount = r.rolls.filter(x=>x===10).length
      let rfExtras = []
      if (tenCount>0) {
        if (rfConfirm) {
          let confirm
          if (manualMode && manualAttackRoll.trim()) {
            // Use a second manual roll for RF confirmation if available
            const confirmRoll = parseInt(manualAttackRoll.trim())
            if (!isNaN(confirmRoll) && confirmRoll >= 1 && confirmRoll <= 100) {
              confirm = confirmRoll
            } else {
              confirm = d100()
            }
          } else {
            confirm = d100()
          }
          if (confirm <= target) rfExtras = Array.from({length: tenCount}, ()=> Math.floor(Math.random()*10)+1)
        } else {
          rfExtras = Array.from({length: tenCount}, ()=> Math.floor(Math.random()*10)+1)
        }
      }
      const raw = r.total + rfExtras.reduce((a,b)=>a+b,0) + pendingHits.dmgSpec.flat
      const flatText = pendingHits.dmgSpec.flat ? (' + ' + pendingHits.dmgSpec.flat) : ''
      const rollsText = r.rolls.join(', ') + (rfExtras.length ? (' | RF: ' + rfExtras.join(', ')) : '')

      if (isHordeMode) {
        // Horde mode: damage vs TB only (no armour, no body parts)
        // Each point of damage above TB = 1 creature killed from the horde
        const magnitudeLoss = Math.max(0, raw - targetTB)
        const damageText = magnitudeLoss > 0 ? `${magnitudeLoss} creatures` : `0 (Negated)`
        perHit.push({ where: 'Horde', raw, mitigated: magnitudeLoss, detail: `${rollsText}${flatText} - TB ${targetTB} = ${damageText}` })
        totalApplied += magnitudeLoss
      } else {
        const arForLoc = Math.max(0, armourFor(where) - pen)
        const mitigated = mitigateDamage(raw, targetTB, arForLoc)
        const damageText = mitigated > 0 ? `${mitigated}` : `${mitigated} (Negated)`
        perHit.push({ where, raw, mitigated, detail: `${rollsText}${flatText} - TB (Toughness Bonus) ${targetTB} - Armour(${where}) ${arForLoc} = ${damageText}` })
        if (BODY_PARTS.includes(where)) nextParts[where] = (nextParts[where]||0) + mitigated
        totalApplied += Math.max(0, mitigated|0)
      }
    }
    
    const res = {
      id: uid(),
      ts: Date.now(),
      attackRoll: pendingHits.attackRoll,
      target,
      success: true,
      hits: pendingHits.hits,
      hitsNegated: negatedHits,
      hitsApplied: hitsToApply,
      where,
      jammed: pendingHits.jammed,
      perHit,
      weapon: weaponName || undefined,
      actor: actingAs ? actingAs.split(':').slice(1).join(':') : (authedPlayer || 'Attacker'),
      enemy: enemyName || undefined,
      using: pendingHits.isRanged ? 'BS' : 'WS',
      defenseRoll,
      defenseTarget,
      defenseType: defenseTypeName,
      defenseFailed: !dg.success,
      mode: pendingHits.mode,
      rof: pendingHits.rof
    }
    // A partial evasion (burst reaction) uses the reaction but still lets some hits through.
    const defenseSummary = negatedHits > 0
      ? `${defenseTypeName} evaded ${negatedHits} of ${pendingHits.hits} hit${pendingHits.hits === 1 ? '' : 's'} (DoS ${dg.dos}); ${hitsToApply} got through`
      : `${defenseTypeName} failed with DoF (Degrees of Failure) ${dg.dof}`

    addRollResult(res)
      if (totalApplied>0) {
      const applied = Math.max(0, Math.floor(totalApplied))
      if (isHordeMode) {
        setCurWounds(prev => {
          const next = Math.max(0, Number(prev) - applied)
          console.info('[DW] Horde: Applied', applied, '-> magnitude', prev, '=>', next)
          return next
        })
        setLastAppliedDamage(applied)
        setLastRollState({ label: applied > 0 ? 'Hit' : 'No Damage', tone: applied > 0 ? 'emerald' : 'amber' })
        setInfo(`${defenseSummary}. ${applied} creatures killed from horde.`)
      } else {
        setPartDamage(nextParts)
        setCurWounds(prev => {
          const next = Math.max(0, Number(prev) - applied)
          console.info('[DW] Applied damage', applied, '-> wounds', prev, '=>', next)
          return next
        })
        setLastAppliedDamage(applied)
        setLastRollState({ label: applied > 0 ? 'Hit' : 'No Damage', tone: applied > 0 ? 'emerald' : 'amber' })
        setInfo(`${defenseSummary}. Applied ${applied} damage to ${where}`)
      }
      // Show damage then wounds
      setProgressStep('damage')
      setTimeout(()=> setProgressStep('wounds'), 250)
      setTimeout(()=> setProgressStep('idle'), 2200)
    } else {
      if (isHordeMode) {
        setInfo(`${defenseSummary}. No creatures killed - damage did not exceed TB.`)
      } else {
        setInfo(`${defenseSummary}. No damage applied - attack completely negated by armour and toughness.`)
      }
      setLastAppliedDamage(0)
      setLastRollState({ label: 'No Damage', tone: 'amber' })
      setProgressStep('wounds')
      setTimeout(()=> setProgressStep('idle'), 1200)
    }
    
    setAwaitingDefense(false)
    setPendingHits(null)
    setReactionUsed(true)
  }

  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 woundPct = maxWounds>0 ? Math.max(0, Math.min(100, (curWounds/maxWounds)*100)) : 0
  const playerWoundPct = playerMaxWounds>0 ? Math.max(0, Math.min(100, (playerCurWounds/playerMaxWounds)*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'
  const rollStateColor = lastRollState.tone === 'rose'
    ? 'bg-rose-700 text-rose-100'
    : lastRollState.tone === 'emerald'
      ? 'bg-emerald-700 text-emerald-100'
      : lastRollState.tone === 'amber'
        ? 'bg-amber-500 text-black'
        : lastRollState.tone === 'sky'
          ? 'bg-sky-700 text-sky-50'
          : 'bg-slate-700 text-slate-100'
  const activeCombatRound = normalCombat?.active ? normalCombat : combatRound

  function recordResult(result) {
    setHistory(prev => [result, ...prev].slice(0, 50))
    try {
      fetch('/api/missions/rolls', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          playerName: authedPlayer || 'unknown',
          rollType: result.rollKind || 'combat',
          label: result.skillName || result.weapon || result.enemy || 'Dice Roller',
          payload: result,
        }),
      }).catch(() => {});
    } catch (e) {
      // Local roll history still works if the shared mission feed is unavailable.
    }
  }

  // Combat results also become the "last roll" that fate points can modify.
  function addRollResult(result) {
    setLastCombatResult(result)
    recordResult(result)
  }

  // Replace the most recent history entry in place (used by fate re-rolls).
  function replaceLastResult(result) {
    setHistory(prev => prev.length ? [result, ...prev.slice(1)] : [result])
  }

  async function applyCombatFate(action) {
    if (!lastCombatResult) return
    const ok = await spendFate()
    if (!ok) { setError('No fate points remaining'); return }
    const next = applyFateToTest(lastCombatResult, action)
    if (typeof next.mode === 'string') next.hits = hitsFromDoS(next.mode, next.dos, next.rof)
    setLastCombatResult(next)
    replaceLastResult(next)
    setInfo(`Attack ${next.success ? `hit (${next.dos} DoS, ${next.hits} hit${next.hits === 1 ? '' : 's'})` : `missed (${next.dof} DoF)`} ✧ ${next.fate.join(', ')} — re-resolve damage if needed`)
  }

  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">
        {(error||info) && (
          <div className={`rounded-xl px-4 py-3 ${error? 'bg-rose-800 border border-rose-600 text-rose-200':'bg-emerald-800 border border-emerald-600 text-emerald-200'}`}>
            {error ? error : <div dangerouslySetInnerHTML={{ __html: info }} />}
          </div>
        )}
        {myConditions.length > 0 && (
          <div className="rounded-xl border border-rose-700/50 bg-rose-950/30 px-4 py-2 flex flex-wrap items-center gap-3">
            <span className="text-xs font-semibold uppercase tracking-wide text-rose-300">Conditions active</span>
            {myConditions.map(cond => (
              <span key={cond} className="rounded bg-rose-800/60 px-2 py-0.5 text-xs text-rose-100">
                {CONDITION_MODS[cond]?.label || cond}
              </span>
            ))}
            {conditionMod !== 0 && (
              <span className="ml-auto text-xs text-rose-200 font-semibold">
                {conditionMod > 0 ? '+' : ''}{conditionMod} to attack target
              </span>
            )}
          </div>
        )}
        <div className="flex items-center justify-between">
          <h1 className="text-3xl md:text-4xl font-extrabold tracking-tight">Deathwatch Roller Pro</h1>
          <div className="flex items-center gap-3">
            {activeCombatRound?.active && (
              <span className="text-xs px-2 py-1 rounded bg-red-700 text-red-100 font-bold">Round {activeCombatRound.round}</span>
            )}
            {activeCombatRound?.active && activeCombatRound?.currentTurn?.name && (
              <span className="text-xs px-2 py-1 rounded bg-cyan-700 text-cyan-50 font-bold">
                Now {activeCombatRound.currentTurn.name}
              </span>
            )}
            {activeCombatRound?.active && activeCombatRound?.nextTurn?.name && (
              <span className="text-xs px-2 py-1 rounded bg-slate-700 text-slate-100 font-bold">
                Next {activeCombatRound.nextTurn.name}
              </span>
            )}
            <div className="text-xs uppercase tracking-wide opacity-70">
              Target {target} • {usingSkill}
              {modeMod !== 0 && <span className="text-sky-300 ml-1">({modeMod > 0 ? '+' : ''}{modeMod} {mode})</span>}
              {conditionMod !== 0 && <span className="text-rose-300 ml-1">({conditionMod > 0 ? '+' : ''}{conditionMod} cond)</span>}
            </div>
            <span className={`text-xs px-2 py-1 rounded ${statusColor}`}>{status}</span>
            <button 
              onClick={() => setInfo(`
                <strong>Abbreviations Guide:</strong><br/>
                • <strong>BS</strong> - Ballistic Skill (ranged attacks)<br/>
                • <strong>WS</strong> - Weapon Skill (melee attacks)<br/>
                • <strong>Ag</strong> - Agility (dodge/cover defense)<br/>
                • <strong>TB</strong> - Toughness Bonus (reduces damage)<br/>
                • <strong>AR</strong> - Armour Rating (reduces damage)<br/>
                • <strong>Pen</strong> - Penetration (reduces armour)<br/>
                • <strong>RoF</strong> - Rate of Fire (max shots)<br/>
                • <strong>DoS</strong> - Degrees of Success<br/>
                • <strong>DoF</strong> - Degrees of Failure<br/>
                • <strong>RF</strong> - Righteous Fury (extra damage on 10s)<br/>
                • <strong>W</strong> - Wounds (health points)<br/><br/>
                <strong>Damage Calculation:</strong><br/>
                Final Damage = Raw Damage - TB - (Armour - Pen)<br/>
                If result ≤ 0, damage is completely negated<br/><br/>
                <strong>Manual Mode:</strong><br/>
                • Enable to input dice rolls manually<br/>
                • Attack/Defense: Enter 1-100 values<br/>
                • Damage: Enter comma-separated numbers (e.g., "7, 9")<br/>
                • Useful for physical dice or GM-controlled rolls
              `)} 
              className="text-xs px-2 py-1 rounded bg-slate-700 hover:bg-slate-600"
            >
              ?
            </button>
          </div>
        </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">
            {isGM && (
              <div className="rounded-xl border border-indigo-700/50 bg-indigo-950/30 px-3 py-2">
                <label className="text-xs uppercase opacity-70 block mb-1">Roll as (GM)</label>
                <select className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" value={actingAs} onChange={e=>onSelectActor(e.target.value)}>
                  <option value="">Yourself / manual stats</option>
                  {players.length > 0 && (
                    <optgroup label="Players">
                      {players.map(p => (<option key={`player:${p.name}`} value={`player:${p.name}`}>{p.name}</option>))}
                    </optgroup>
                  )}
                  <optgroup label="Enemies">
                    {enemies.filter(en => en.name !== 'Custom/None').map(en => (<option key={`enemy:${en.name}`} value={`enemy:${en.name}`}>{en.name}</option>))}
                  </optgroup>
                </select>
                <div className="text-xs opacity-60 mt-1">Loads the chosen actor's BS/WS, skills, and weapon. Pick the defender in the Target list below.</div>
              </div>
            )}
            <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>
              <button onClick={()=>setRollerTab('horde')} className={`rounded-xl px-4 py-2 ${rollerTab==='horde' ? 'bg-amber-700' : 'bg-slate-700 hover:bg-slate-600'}`}>Horde Combat</button>
            </div>
            {rollerTab === 'skill' && (
              <SkillRoller
                playerSkills={playerSkills}
                playerCharacteristics={playerCharacteristics}
                onResult={recordResult}
                onReplaceResult={replaceLastResult}
                fate={fate}
                spendFate={spendFate}
                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">
                  Attack hit with {pendingHits.hits} potential hit{pendingHits.hits !== 1 ? 's' : ''}.
                  Roll defense to determine if damage is applied.
                </div>
                <div className="grid grid-cols-2 gap-4 mb-4">
                  <div>
                    <label className="text-xs uppercase opacity-70">Defense Type</label>
                    <select className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" value={defenseType} onChange={e=>setDefenseType(e.target.value)}>
                      <option value="dodge">Dodge (Agility)</option>
                      <option value="parry" disabled={pendingHits?.isRanged}>Parry (WS - Melee Only)</option>
                      <option value="block">Block (WS +10)</option>
                      <option value="cover">Cover (Agility +20)</option>
                    </select>
                    {pendingHits && (
                      <div className="text-xs opacity-60 mt-1">
                        {pendingHits.isRanged ? 'Ranged attack - Parry disabled' : 'Melee attack - Parry available'}
                      </div>
                    )}
                  </div>
                  <div>
                    <label className="text-xs uppercase opacity-70">Cover Bonus</label>
                    <input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" value={coverBonus} onChange={e=>setCoverBonus(parseInt(e.target.value||'0'))} />
                  </div>
                  <div>
                    <label className="text-xs uppercase opacity-70">
                      <Tooltip text="Agility - Used for dodge and cover defenses">Defender's Ag</Tooltip>
                    </label>
                    <input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" value={defenderAg} onChange={e=>setDefenderAg(parseInt(e.target.value||'0'))} />
                  </div>
                  <div>
                    <label className="text-xs uppercase opacity-70">
                      <Tooltip text="Weapon Skill - Used for parry and block defenses">Defender's WS</Tooltip>
                    </label>
                    <input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" value={defenderWS} onChange={e=>setDefenderWS(parseInt(e.target.value||'0'))} />
                  </div>
                  <div>
                    <label className="text-xs uppercase opacity-70">Defense Modifier</label>
                    <input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" value={defenderModifier} onChange={e=>setDefenderModifier(parseInt(e.target.value||'0'))} />
                  </div>
                  <div className="flex items-center">
                    <label className="text-xs uppercase opacity-70">Reaction Used</label>
                    <input type="checkbox" className="ml-2 align-middle" checked={reactionUsed} onChange={e=>setReactionUsed(e.target.checked)} />
                  </div>
                </div>
                <div className="flex gap-2">
                  <button onClick={runDefense} className="rounded-xl px-4 py-2 bg-amber-600 hover:bg-amber-500">Roll Defense</button>
                  <button onClick={()=>{ 
                    // Apply damage without defense
                    const where = pendingHits.where
                    const perHit = []
                    let totalApplied = 0
                    let nextParts = { ...partDamage }
                    
                    for (let i=0; i<pendingHits.hits; i++) {
                      let r
                      if (manualMode && manualDamageRolls.trim()) {
                        // Parse manual damage rolls
                        const manualRolls = manualDamageRolls.trim().split(/[,\s]+/).map(s => parseInt(s.trim())).filter(n => !isNaN(n) && n >= 1)
                        if (manualRolls.length === 0) {
                          setError('Manual damage rolls must be comma-separated numbers (e.g., "7, 9")')
                          return
                        }
                        r = { rolls: manualRolls, total: manualRolls.reduce((a,b) => a+b, 0) }
                      } else {
                        r = rollDice(pendingHits.dmgSpec.terms, { tearing, proven })
                      }
                      
                      const tenCount = r.rolls.filter(x=>x===10).length
                      let rfExtras = []
                      if (tenCount>0) {
                        if (rfConfirm) {
                          let confirm
                          if (manualMode && manualAttackRoll.trim()) {
                            // Use a second manual roll for RF confirmation if available
                            const confirmRoll = parseInt(manualAttackRoll.trim())
                            if (!isNaN(confirmRoll) && confirmRoll >= 1 && confirmRoll <= 100) {
                              confirm = confirmRoll
                            } else {
                              confirm = d100()
                            }
                          } else {
                            confirm = d100()
                          }
                          if (confirm <= target) rfExtras = Array.from({length: tenCount}, ()=> Math.floor(Math.random()*10)+1)
                        } else {
                          rfExtras = Array.from({length: tenCount}, ()=> Math.floor(Math.random()*10)+1)
                        }
                      }
                      const raw = r.total + rfExtras.reduce((a,b)=>a+b,0) + pendingHits.dmgSpec.flat
                      const arForLoc = Math.max(0, armourFor(where) - pen)
                      const mitigated = mitigateDamage(raw, targetTB, arForLoc)
                      const flatText = pendingHits.dmgSpec.flat ? (' + ' + pendingHits.dmgSpec.flat) : ''
                      const rollsText = r.rolls.join(', ') + (rfExtras.length ? (' | RF: ' + rfExtras.join(', ')) : '')
                      const damageText = mitigated > 0 ? `${mitigated}` : `${mitigated} (Negated)`
                      perHit.push({ where, raw, mitigated, detail: `${rollsText}${flatText} - TB (Toughness Bonus) ${targetTB} - Armour(${where}) ${arForLoc} = ${damageText}` })
                      if (BODY_PARTS.includes(where)) nextParts[where] = (nextParts[where]||0) + mitigated
                      totalApplied += Math.max(0, mitigated|0)
                    }
                    
                    const res = { 
                      id: uid(), 
                      ts: Date.now(), 
                      attackRoll: pendingHits.attackRoll,
                      target, 
                      success: true, 
                      hits: pendingHits.hits, 
                      where, 
                      jammed: pendingHits.jammed, 
                      perHit,
                      weapon: weaponName || undefined,
                      actor: actingAs ? actingAs.split(':').slice(1).join(':') : (authedPlayer || 'Attacker'),
                      enemy: enemyName || undefined,
                      using: pendingHits.isRanged ? 'BS' : 'WS',
                      defenseType: 'No Defense',
                      defenseFailed: true,
                      mode: pendingHits.mode,
                      rof: pendingHits.rof
                    }
                    
                    addRollResult(res)
                    if (totalApplied>0) {
                      setPartDamage(nextParts)
                      setCurWounds(w => Math.max(0, w - totalApplied))
                      setInfo(`No defense used. Applied ${totalApplied} damage to ${where}`)
                    } else {
                      setInfo(`No defense used. No damage applied - attack completely negated by armour and toughness.`)
                    }
                    
                    setAwaitingDefense(false)
                    setPendingHits(null)
                  }} className="rounded-xl px-4 py-2 bg-slate-700 hover:bg-slate-600">No Defense</button>
                </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>
                <div className="flex gap-2">
                  <div className="flex-1">
                    <input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2 mb-2" placeholder="Search weapons..." value={weaponFilter} onChange={e=>setWeaponFilter(e.target.value)} />
                    <select className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" value={weaponName} onChange={e=>onSelectWeapon(e.target.value)}>
                      <option value="">Custom</option>
                      {filteredWeapons.map(w => (<option key={w.name} value={w.name}>{w.name}</option>))}
                    </select>
                  </div>
                  <button onClick={()=>{ setManageWeapons(v=>!v); setManageTab('json'); setWeaponsJSON(JSON.stringify(weapons,null,2)); setWeaponsCSV('name,class,damage,pen,rof,modes,tearing,proven,reliable') }} className="rounded-xl px-3 py-2 bg-slate-700 hover:bg-slate-600">Manage</button>
                </div>
              </div>
              <div className="lg:col-span-3">
                <div className="flex items-center justify-between mb-1">
                  <label className="text-xs uppercase opacity-70">{isGM ? 'Target (defender)' : 'Enemy'}</label>
                  <div className="flex items-center gap-2">
                    {enemiesLoading && <span className="text-xs text-blue-400">Loading...</span>}
                    {enemiesError && <span className="text-xs text-red-400">Error: {enemiesError}</span>}
                    <button
                      onClick={loadEnemiesFromAPI}
                      disabled={enemiesLoading}
                      className="text-xs px-2 py-1 rounded bg-slate-700 hover:bg-slate-600 disabled:opacity-50"
                      title="Refresh enemies from database"
                    >
                      ↻
                    </button>
                  </div>
                </div>
                <select className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" value={enemyName} onChange={e=>onSelectTarget(e.target.value)}>
                  {isGM && players.length > 0 && (
                    <optgroup label="Players (defender)">
                      {players.map(p => (<option key={`t-player:${p.name}`} value={p.name}>{p.name}</option>))}
                    </optgroup>
                  )}
                  <optgroup label="Enemies">
                    {enemies.map(en => {
                      const isHorde = !!(en.horde || /horde/i.test(String(en.name || '')))
                      const prefix = isHorde ? `Horde TB ${en.tb ?? '-'}` : `TB ${en.tb ?? '-'}`
                      const bodyLabel = isHorde
                        ? `Mag ${en.magnitude ?? en.wounds ?? '-'}`
                        : `W ${en.wounds ?? '-'}`
                      return (
                        <option key={en.name} value={en.name}>
                          {en.name}{en.name==='Custom/None' ? '' : ` (${prefix} / AR ${en.armour ?? 'Var'} / ${bodyLabel})`}
                        </option>
                      )
                    })}
                  </optgroup>
                </select>
                <div className="text-xs opacity-60 mt-1">
                  <Tooltip text="Toughness Bonus">TB</Tooltip> / <Tooltip text="Armour Rating">AR</Tooltip> / <Tooltip text="Wounds or Magnitude">W/Mag</Tooltip> • {enemies.length} enemies loaded
                  {isGM && players.length > 0 && ` • ${players.length} players`}
                </div>
              </div>
              <div>
                <label className="text-xs uppercase opacity-70">
                  <Tooltip text="Ballistic Skill - Used for ranged attacks">Attacker's BS</Tooltip>
                </label>
                <input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" value={bs} onChange={e=>setBS(parseInt(e.target.value||'0'))} />
              </div>
              <div>
                <label className="text-xs uppercase opacity-70">
                  <Tooltip text="Weapon Skill - Used for melee attacks">Attacker's WS</Tooltip>
                </label>
                <input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" value={ws} onChange={e=>setWS(parseInt(e.target.value||'0'))} />
              </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={modifier} onChange={e=>setModifier(parseInt(e.target.value||'0'))} />
              </div>
              <div>
                <label className="text-xs uppercase opacity-70">Aim</label>
                <select className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" value={aim} onChange={e=>setAim(parseInt(e.target.value))}>
                  <option value={0}>None</option>
                  <option value={10}>Half (+10)</option>
                  <option value={20}>Full (+20)</option>
                </select>
              </div>
              <div>
                <label className="text-xs uppercase opacity-70">Difficulty</label>
                <select className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" value={difficulty} onChange={e=>setDifficulty(e.target.value)}>
                  <option value="easy">Easy (+20)</option>
                  <option value="normal">Normal (+0)</option>
                  <option value="hard">Hard (-20)</option>
                  <option value="deadly">Deadly (-30)</option>
                </select>
              </div>
              <div>
                <label className="text-xs uppercase opacity-70">Mode</label>
                <select className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" value={mode} onChange={e=>setMode(e.target.value)}>
                  {(['single','semi','full']).map(m => (
                    <option key={m} value={m} disabled={!allowedModes.includes(m)}>{m[0].toUpperCase()+m.slice(1)}</option>
                  ))}
                </select>
              </div>
              <div>
                <label className="text-xs uppercase opacity-70">
                  <Tooltip text="Rate of Fire - Maximum shots per attack">RoF</Tooltip>
                </label>
                <input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" min={1} value={rof} onChange={e=>setRof(parseInt(e.target.value||'1'))} />
              </div>
              <div className="lg:col-span-3">
                <label className="text-xs uppercase opacity-70">Damage</label>
                <input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" value={damage} onChange={e=>{ setDamage(e.target.value); if (error) setError('') }} />
              </div>
              <div>
                <label className="text-xs uppercase opacity-70">
                  <Tooltip text="Tearing - Roll damage dice twice, take highest">Tearing</Tooltip>
                </label>
                <input type="checkbox" className="ml-2 align-middle" checked={tearing} onChange={e=>setTearing(e.target.checked)} />
              </div>
              <div>
                <label className="text-xs uppercase opacity-70">
                  <Tooltip text="Proven - Minimum value for d10 damage dice">Proven</Tooltip>
                </label>
                <input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" min={0} max={10} value={proven} onChange={e=>setProven(parseInt(e.target.value||'0'))} />
              </div>
              <div>
                <label className="text-xs uppercase opacity-70">
                  <Tooltip text="Penetration - Reduces target's armour value">Pen</Tooltip>
                </label>
                <input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" min={0} value={pen} onChange={e=>setPen(parseInt(e.target.value||'0'))} />
              </div>
              <div>
                <label className="text-xs uppercase opacity-70">
                  <Tooltip text="Reliable - Weapon cannot jam on natural 96-00">Reliable</Tooltip>
                </label>
                <input type="checkbox" className="ml-2 align-middle" checked={reliable} onChange={e=>setReliable(e.target.checked)} />
              </div>
              <div>
                <label className="text-xs uppercase opacity-70">
                  <Tooltip text="Righteous Fury Confirm - Roll to confirm extra damage on 10s">RF confirm</Tooltip>
                </label>
                <input type="checkbox" className="ml-2 align-middle" checked={rfConfirm} onChange={e=>setRfConfirm(e.target.checked)} />
              </div>
              <div>
                <label className="text-xs uppercase opacity-70">
                  <Tooltip text="Toughness Bonus - Reduces incoming damage">Target TB</Tooltip>
                </label>
                <input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" min={0} value={targetTB} onChange={e=>setTargetTB(parseInt(e.target.value||'0'))} />
              </div>
              <div>
                <label className="text-xs uppercase opacity-70">
                  <Tooltip text="Armour - Reduces incoming damage (Body location)">Target Armour (Body)</Tooltip>
                </label>
                <input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" min={0} value={targetArmour} onChange={e=>{ const v = parseInt(e.target.value||'0'); setTargetArmour(v); setArmourMap(uniformArmourMap(v)) }} />
              </div>
            </div>
            
            {/* Manual Mode Controls */}
            <div className="rounded-xl bg-amber-800 border border-amber-700 p-4 space-y-3">
              <div className="flex items-center justify-between">
                <div className="text-sm font-semibold text-amber-200">Manual Dice Mode</div>
                <div className="flex items-center gap-2">
                  <label className="flex items-center gap-2 text-xs">
                    <input 
                      type="checkbox" 
                      checked={manualMode} 
                      onChange={e=>{
                        setManualMode(e.target.checked)
                        if (!e.target.checked) {
                          // Clear manual rolls when disabling manual mode
                          setManualAttackRoll('')
                          setManualDefenseRoll('')
                          setManualDamageRolls('')
                        }
                      }} 
                      className="rounded"
                    />
                    Enable Manual Rolls
                  </label>
                </div>
              </div>
              
              {manualMode && (
                <div className="grid grid-cols-1 md:grid-cols-3 gap-3 text-sm">
                  <div>
                    <label className="text-xs uppercase opacity-70">Manual Attack Roll (1-100)</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="e.g., 45"
                      value={manualAttackRoll} 
                      onChange={e=>setManualAttackRoll(e.target.value)} 
                    />
                  </div>
                  <div>
                    <label className="text-xs uppercase opacity-70">Manual Defense Roll (1-100)</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="e.g., 67"
                      value={manualDefenseRoll} 
                      onChange={e=>setManualDefenseRoll(e.target.value)} 
                    />
                  </div>
                  <div>
                    <label className="text-xs uppercase opacity-70">Manual Damage Rolls</label>
                    <input 
                      className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" 
                      type="text"
                      placeholder="e.g., 7, 9"
                      value={manualDamageRolls} 
                      onChange={e=>setManualDamageRolls(e.target.value)} 
                    />
                    <div className="text-xs opacity-60 mt-1">Comma-separated numbers</div>
                  </div>
                </div>
              )}
            </div>
            
            <div className="flex flex-wrap items-center gap-3">
              <button onClick={normalCombat?.active ? endNormalCombat : startNormalCombat} className={`rounded-xl px-4 py-2.5 ${normalCombat?.active ? 'bg-rose-700 hover:bg-rose-600' : 'bg-emerald-700 hover:bg-emerald-600'} active:scale-[.99] transition`}>
                {normalCombat?.active ? 'End Combat' : 'Start Combat'}
              </button>
              <button onClick={runAttack} className="rounded-xl px-5 py-2.5 bg-blue-600 hover:bg-blue-500 active:scale-[.99] transition">Roll</button>
              <div className="text-sm opacity-80">Target <span className="font-semibold">{target}</span></div>
              <FateControls
                fate={fate}
                disabled={!lastCombatResult}
                onReroll={()=>applyCombatFate('reroll')}
                onPlus10={()=>applyCombatFate('plus10')}
                onAddDos={()=>applyCombatFate('dos')}
              />
            </div>
            {/* Progress tracker */}
            <div className="mt-3">
              <div className="flex items-center gap-2 text-sm">
                <div className={`px-3 py-1 rounded font-semibold ${progressStep==='attack' ? 'bg-blue-600 text-white animate-pulse' : 'bg-slate-800 text-white'}`}>Attack</div>
                <div className={`px-3 py-1 rounded font-semibold ${progressStep==='defense' ? 'bg-amber-500 text-black animate-pulse' : 'bg-slate-800 text-white'}`}>Defense</div>
                <div className={`px-3 py-1 rounded font-semibold ${progressStep==='damage' ? 'bg-rose-500 text-black animate-pulse' : 'bg-slate-800 text-white'}`}>Damage</div>
                <div className={`px-3 py-1 rounded font-semibold ${progressStep==='wounds' ? 'bg-emerald-500 text-black animate-pulse' : 'bg-slate-800 text-white'}`}>Wounds</div>
                <div className="ml-3 text-xs opacity-70">Phase: <span className="font-semibold">{progressStep}</span></div>
                <div className="ml-4 text-xs opacity-80">Last Applied: <span className="font-semibold">{lastAppliedDamage}</span></div>
                <div className={`ml-4 text-xs px-2 py-1 rounded font-semibold ${rollStateColor}`}>{lastRollState.label}</div>
              </div>
            </div>
            {manageWeapons && (
              <>
              <div className="rounded-xl bg-slate-800 border border-slate-600 p-4 space-y-3">
                <div className="flex items-center justify-between">
                  <div className="text-sm font-semibold">Manage Weapons</div>
                  <div className="flex items-center gap-3 text-xs">
                    <label className="flex items-center gap-1"><input type="checkbox" checked={appendMode} onChange={e=>setAppendMode(e.target.checked)} />Append</label>
                    <button onClick={()=>setManageWeapons(false)} className="rounded-xl px-3 py-1.5 bg-slate-700">Close</button>
                  </div>
                </div>
                <div className="flex gap-2 text-xs">
                  <button onClick={()=>setManageTab('json')} className={`px-2 py-1 rounded ${manageTab==='json'?'bg-slate-600':'bg-slate-800'}`}>JSON</button>
                  <button onClick={()=>setManageTab('csv')} className={`px-2 py-1 rounded ${manageTab==='csv'?'bg-slate-600':'bg-slate-800'}`}>CSV</button>
                  <button onClick={exportWeapons} className="px-2 py-1 rounded bg-emerald-700">Export JSON</button>
                </div>
                {manageTab==='json' ? (
                  <>
                    <textarea className="w-full h-48 rounded-xl border border-slate-600 bg-slate-900 px-3 py-2 font-mono text-xs" value={weaponsJSON} onChange={e=>setWeaponsJSON(e.target.value)} />
                    <div className="flex gap-2">
                      <button onClick={()=>importFromJSONText(weaponsJSON)} className="rounded-xl px-3 py-2 bg-blue-600">Import JSON</button>
                      <button onClick={()=>{ const merged = importBuildWeapons(weapons); setWeapons(merged); setWeaponsJSON(JSON.stringify(merged,null,2)); setInfo('Imported built DB weapons') }} className="rounded-xl px-3 py-2 bg-emerald-700">Import Built DB</button>
                      <button onClick={()=>{ fetchWeaponsFromServer() }} className="rounded-xl px-3 py-2 bg-indigo-700">Fetch from Server</button>
                      <button onClick={()=>{ setWeapons(defaultWeapons); setWeaponsJSON(JSON.stringify(defaultWeapons,null,2)); setInfo('Reset to defaults') }} className="rounded-xl px-3 py-2 bg-slate-700">Reset Defaults</button>
                    </div>
                  </>
                ) : (
                  <>
                    <textarea className="w-full h-48 rounded-xl border border-slate-600 bg-slate-900 px-3 py-2 font-mono text-xs" value={weaponsCSV} onChange={e=>setWeaponsCSV(e.target.value)} />
                    <div className="flex gap-2">
                      <button onClick={()=>importFromCSVText(weaponsCSV)} className="rounded-xl px-3 py-2 bg-blue-600">Import CSV</button>
                    </div>
                  </>
                )}
              </div>
              {weapons.length>0 && (
                <div className="mt-3 text-xs">
                  <div className="font-semibold mb-1">Weapons Loaded ({weapons.length})</div>
                  <div className="max-h-40 overflow-auto">
                    {weapons.slice(0,200).map(w => (
                      <div key={w.name} className="text-xs opacity-80 mb-1">{w.name} {w.req ? `• Req ${w.req}` : ''} {w.renown ? `• ${w.renown}` : ''} {w.source ? `• ${w.source}` : ''}</div>
                    ))}
                  </div>
                </div>
              )}
              </>
            )}
	            {history[0] && history[0].rollKind === 'skill' && (
	              <div className="rounded-xl bg-slate-800 border border-slate-600 p-4 space-y-2">
	                <div className="text-sm">
	                  {history[0].skillName} • {history[0].characteristic} • Roll <span className="font-semibold">{history[0].attackRoll}</span> vs {history[0].target} → {history[0].success ? <span className="text-emerald-400">Success ({history[0].dos} DoS)</span> : <span className="text-rose-400">Fail ({history[0].dof} DoF)</span>}
	                </div>
	                <div className="text-xs opacity-70">{history[0].training} • base {history[0].base} • modifier {history[0].modifier >= 0 ? '+' : ''}{history[0].modifier}</div>
	              </div>
	            )}
	            {history[0] && history[0].rollKind !== 'skill' && (
	              <div className="rounded-xl bg-slate-800 border border-slate-600 p-4 space-y-2">
                {(history[0].actor || history[0].enemy) && (
                  <div className="flex flex-wrap items-center gap-2 text-xs">
                    {history[0].actor && <span className="rounded-lg bg-sky-900/70 border border-sky-700 px-2 py-1 font-semibold text-sky-100">{history[0].actor}</span>}
                    <span className="text-slate-400 font-semibold">vs</span>
                    {history[0].enemy && <span className="rounded-lg bg-rose-900/70 border border-rose-700 px-2 py-1 font-semibold text-rose-100">{history[0].enemy}</span>}
                  </div>
                )}
                <div className="text-sm">{history[0].weapon ? `${history[0].weapon} • ` : ''}{history[0].using} • Attack <span className="font-semibold">{history[0].attackRoll}</span> → <span className={history[0].success ? "text-emerald-400" : "text-rose-400"}>{history[0].success ? 'Hit!' : 'Miss!'}</span></div>
                {history[0].defenseRoll && (
                  <div className="text-sm">{history[0].defenseType || 'Defense'} <span className="font-semibold">{history[0].defenseRoll}</span> vs {history[0].defenseTarget} → {history[0].defenseFailed ? <span className="text-rose-400">Failed!</span> : <span className="text-emerald-400">Defended!</span>}</div>
                )}
                <div className="text-sm">{history[0].mode?.toUpperCase() || 'SINGLE'} • RoF <span className="font-semibold">{history[0].rof || 1}</span> • Hits <span className="font-semibold">{history[0].hits}</span> • {history[0].where}{history[0].jammed && <span className="ml-2 text-rose-400">JAM</span>}</div>
                <div className="space-y-1">
                  <div className="text-sm font-medium">
                    Damage applied: <span className="font-semibold">{(history[0].perHit || []).reduce((sum, h) => sum + (h.mitigated || 0), 0)}</span>
                    {(!history[0].perHit || history[0].perHit.length === 0) && history[0].hits > 0 && (
                      <span className="text-amber-400"> — negated{history[0].defenseType ? ` by ${history[0].defenseType}` : ''}</span>
                    )}
                  </div>
                  {history[0].perHit && history[0].perHit.length > 0 && (
                    <ul className="text-sm list-disc ml-6">
                      {history[0].perHit.map((h, i)=>(<li key={i}><span className="font-semibold">{h.mitigated}</span> ({h.detail})</li>))}
                    </ul>
                  )}
                </div>
              </div>
            )}
            <div className="pt-2 border-t border-slate-600">
              <div className="flex gap-2">
                <input className="flex-1 rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" placeholder="Preset name" value={presetName} onChange={e=>setPresetName(e.target.value)} />
                <button onClick={saveCurrentAsPreset} className="rounded-xl px-3 py-2 bg-slate-700 hover:bg-slate-600">Save</button>
              </div>
              <div className="flex flex-wrap gap-2 mt-2">
                {presets.map(p => (
                  <div key={p.name} className="rounded-xl border border-slate-600 bg-slate-800 px-3 py-2 flex items-center gap-2">
                    <button className="underline" onClick={()=>applyPreset(p)}>{p.name}</button>
                    <button onClick={()=>deletePreset(p.name)} className="text-xs px-2 py-1 rounded bg-rose-600">x</button>
                  </div>
                ))}
              </div>
            </div>
            </>)}
            {rollerTab === 'horde' && (
              <HordeCombatPanel
                players={players}
                enemies={enemies}
                hordeCombat={hordeCombat}
                setHordeCombat={setHordeCombat}
                authedPlayer={authedPlayer}
                selectedHordePlayers={selectedHordePlayers}
                setSelectedHordePlayers={setSelectedHordePlayers}
                d100={d100}
                degrees={degrees}
                parseDice={parseDice}
                rollDice={rollDice}
                setInfo={setInfo}
                setError={setError}
                lastRollState={lastRollState}
                rollStateColor={rollStateColor}
                setLastRollState={setLastRollState}
                addRollResult={addRollResult}
              />
            )}
          </div>
          <div className="space-y-6">
            <div className="rounded-2xl bg-slate-800 backdrop-blur border border-slate-600 p-5 shadow-xl">
              <div className="flex items-center justify-between mb-3">
                <div className="font-semibold">{((rollerTab === 'horde' || isHordeMode) && hordeCombat) ? 'Player Wound Tracker' : 'Enemy Wounds Tracker'}</div>
                <button onClick={resetTracker} className="text-xs rounded px-2 py-1 bg-slate-700 hover:bg-slate-600">Reset</button>
              </div>
              {((rollerTab === 'horde' || isHordeMode) && hordeCombat) ? (
                (() => {
                  const playerRows = hordeCombat.players
                    .map(p => ({ ...p, kind: 'player' }))
                    .sort((a, b) => b.initiative - a.initiative);
                  const combinedOrder = [
                    ...hordeCombat.players.map(p => ({ ...p, kind: 'player' })),
                    ...(hordeCombat.enemy ? [{ ...hordeCombat.enemy, kind: 'enemy' }] : [])
                  ].sort((a, b) => b.initiative - a.initiative);
                  const currentActor = hordeCombat.turnIndex >= 0 ? combinedOrder[hordeCombat.turnIndex] || null : null;
                  return (
                    <div className="space-y-2">
                      {playerRows.map((actor, idx) => {
                        const isCurrent = currentActor?.kind === 'player' && actor.name === currentActor.name;
                        const woundPct = actor.maxWounds > 0 ? Math.max(0, (actor.wounds / actor.maxWounds) * 100) : 0;
                        return (
                          <div key={`${actor.kind}-${actor.name}`} className={`rounded-xl border px-3 py-2 ${isCurrent ? 'border-cyan-500 bg-cyan-950/40' : 'border-slate-700 bg-slate-900'} ${!actor.alive ? 'opacity-55' : ''}`}>
                            <div className="flex items-center justify-between gap-2">
                              <div className="flex items-center gap-2 min-w-0">
                                <span className="text-xs opacity-50">#{idx + 1}</span>
                                {isCurrent && <span className="rounded bg-cyan-700 px-1.5 py-0.5 text-[11px] font-semibold text-white">Now</span>}
                                <span className="font-semibold truncate">{actor.name}</span>
                              </div>
                              <span className={`text-xs px-1.5 py-0.5 rounded ${actor.alive ? 'bg-emerald-800 text-emerald-300' : 'bg-red-800 text-red-300'}`}>
                                {actor.alive ? 'Up' : 'Down'}
                              </span>
                            </div>
                            <div className="mt-2 h-2 w-full bg-slate-700 rounded overflow-hidden">
                              <div className={`h-2 transition-all ${actor.alive ? 'bg-emerald-500' : 'bg-red-600'}`} style={{width: `${woundPct}%`}}></div>
                            </div>
                            <div className="mt-1 flex items-center justify-between text-xs opacity-70">
                              <span>{actor.wounds}/{actor.maxWounds} wounds</span>
                              <span>Init {actor.initiative}</span>
                            </div>
                          </div>
                        );
                      })}
                    </div>
                  );
                })()
              ) : (
                // NORMAL MODE: wounds + body parts
                <div className="space-y-4">
                  <div className="grid grid-cols-2 gap-3">
                    <div>
                      <label className="text-xs uppercase opacity-70">Max Wounds</label>
                      <input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" min={0} value={maxWounds} onChange={e=>setMaxWounds(Math.max(0, parseInt(e.target.value||'0')))} />
                    </div>
                    <div>
                      <label className="text-xs uppercase opacity-70">Current Wounds</label>
                      <input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" min={0} value={curWounds} onChange={e=>setCurWounds(Math.max(0, parseInt(e.target.value||'0')))} />
                    </div>
                    <div className="col-span-2">
                      <div className="h-2 w-full bg-slate-800 rounded overflow-hidden">
                        <div className="h-2 bg-emerald-500" style={{width: `${woundPct}%`}}></div>
                      </div>
                      <div className="text-xs opacity-70 mt-1">{curWounds}/{maxWounds} remaining</div>
                    </div>
                  </div>
                  <div className="mt-3 grid grid-cols-2 sm:grid-cols-3 gap-2">
                    {BODY_PARTS.map(p => (
                      <div key={p} className="rounded-xl border border-slate-600 bg-slate-900 p-3">
                        <div className="text-xs opacity-70">{p}</div>
                        <div className="text-lg font-semibold">{partDamage[p]||0}</div>
                        <div className="text-xs opacity-60 mt-1">
                          <Tooltip text="Armour Rating">AR</Tooltip> {armourMap[p]??targetArmour}
                        </div>
                      </div>
                    ))}
                  </div>
                  <div className="border-t border-slate-700 pt-4">
                    <div className="flex items-center justify-between mb-3">
                      <div className="font-semibold">Player Wound Tracker</div>
                      <button onClick={resetPlayerTracker} className="text-xs rounded px-2 py-1 bg-slate-700 hover:bg-slate-600">Reset</button>
                    </div>
                    <div className="grid grid-cols-2 gap-3">
                      <div>
                        <label className="text-xs uppercase opacity-70">Max Wounds</label>
                        <input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" min={0} value={playerMaxWounds} onChange={e=>setPlayerMaxWounds(Math.max(0, parseInt(e.target.value||'0')))} />
                      </div>
                      <div>
                        <label className="text-xs uppercase opacity-70">Current Wounds</label>
                        <input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" min={0} value={playerCurWounds} onChange={e=>setPlayerCurWounds(Math.max(0, parseInt(e.target.value||'0')))} />
                      </div>
                      <div className="col-span-2">
                        <div className="h-2 w-full bg-slate-800 rounded overflow-hidden">
                          <div className="h-2 bg-cyan-500" style={{width: `${playerWoundPct}%`}}></div>
                        </div>
                        <div className="text-xs opacity-70 mt-1">{playerCurWounds}/{playerMaxWounds} remaining</div>
                      </div>
                    </div>
                  </div>
                </div>
              )}
            </div>
            <div className="rounded-2xl bg-slate-800 backdrop-blur border border-slate-600 p-5 shadow-xl flex flex-col">
              <div className="flex items-center justify-between mb-2">
                <div className="font-semibold">Roll History</div>
                <button onClick={clearHistory} className="text-xs rounded px-2 py-1 rounded-xl bg-slate-700 hover:bg-slate-600">Clear</button>
              </div>
              <div className="overflow-y-auto space-y-2 pr-2" style={{maxHeight:'520px'}}>
                {history.length===0 && <div className="text-sm opacity-70">No rolls yet</div>}
	                {history.map(r => {
	                  const perHitList = Array.isArray(r.perHit) ? r.perHit : [];
	                  const totalDamage = perHitList.reduce((sum, h) => sum + (h.mitigated || 0), 0);
	                  const wasNegated = r.rollKind !== 'skill' && totalDamage === 0 && (r.hitsNegated > 0 || (r.success && r.hits > 0 && perHitList.length === 0));
	                  return (
	                  <div key={r.id} className="rounded-xl border border-slate-600 bg-slate-900 p-3">
	                    <div className="flex items-center justify-between text-xs opacity-80">
	                      <div>{new Date(r.ts).toLocaleTimeString()}</div>
	                      <div>{r.rollKind === 'skill' ? `${r.skillName} • ${r.using}` : `${r.weapon ? `${r.weapon} • ` : ''}${r.using || 'Unknown'} • ${r.mode ? r.mode.toUpperCase() : 'SINGLE'} • RoF ${r.rof || 1}`}</div>
	                    </div>
                      {r.rollKind !== 'skill' && (r.actor || r.enemy) && (
                        <div className="flex flex-wrap items-center gap-2 mt-2 text-xs">
                          {r.actor && <span className="rounded-lg bg-sky-900/70 border border-sky-700 px-2 py-1 font-semibold text-sky-100">{r.actor}</span>}
                          <span className="text-slate-500 font-semibold">vs</span>
                          {r.enemy && <span className="rounded-lg bg-rose-900/70 border border-rose-700 px-2 py-1 font-semibold text-rose-100">{r.enemy}</span>}
                        </div>
                      )}
	                    <div className="text-sm mt-1">
	                      {r.success ? <span className="text-emerald-400">Success</span> : <span className="text-rose-400">Fail</span>} • Roll {r.attackRoll || '?'} • Target {r.target || '?'}
	                      {r.rollKind === 'skill' ? ` • ${r.success ? `${r.dos || 0} DoS` : `${r.dof || 0} DoF`} • ${r.training || ''}` : ` • Hits ${r.hits || 0} • ${r.where || 'Unknown'}${r.jammed ? ' • JAM' : ''}`}
	                    </div>
                    {r.rollKind !== 'skill' && (
                      <div className="text-xs opacity-80 mt-1">
                        Damage applied: <span className="font-semibold text-slate-100">{totalDamage}</span>
                        {perHitList.length > 1 && (
                          <span> ({perHitList.map((h, i) => `${i + 1}:${h.mitigated || 0}`).join('  ')})</span>
                        )}
                        {wasNegated && <span className="text-amber-400"> — negated{r.defenseType ? ` by ${r.defenseType}` : ''}</span>}
                      </div>
                    )}
                  </div>
                  );
                })}
              </div>
            </div>
            </div>
        </div>
      </div>
    </section>
  )
}

// ── Horde Combat Panel ──────────────────────────────────────────────────────
function HordeCombatPanel({
  players, enemies, hordeCombat, setHordeCombat, authedPlayer,
  selectedHordePlayers, setSelectedHordePlayers,
  d100, degrees, parseDice, rollDice, setInfo, setError,
  lastRollState, rollStateColor, setLastRollState, addRollResult
}) {
  const hordeEnemies = useMemo(() => enemies.filter(e => e.horde), [enemies]);
  const orderedPlayers = useMemo(() => {
    return [...players]
      .filter(p => p && p.name && String(p.name).toLowerCase() !== 'gm')
      .sort((a, b) => a.name.localeCompare(b.name));
  }, [players]);
  const [enemyName, setEnemyName] = useState('');
  const [hordeMagnitude, setHordeMagnitude] = useState(0);
  const canEditMagnitude = authedPlayer === 'gm';

  useEffect(() => {
    const enemy = enemies.find(e => e.name === enemyName);
    const mag = Number(enemy?.magnitude ?? enemy?.wounds ?? 20);
    setHordeMagnitude(Number.isFinite(mag) && mag > 0 ? mag : 20);
  }, [enemyName, enemies]);

  useEffect(() => {
    if (!authedPlayer || authedPlayer === 'gm') return;
    if (selectedHordePlayers.length > 0) return;
    const match = orderedPlayers.find(p => String(p.name).toLowerCase() === String(authedPlayer).toLowerCase());
    const nextPlayer = orderedPlayers.find(p => !match || p.name !== match.name);
    const defaults = [];
    if (match) defaults.push(match.name);
    if (nextPlayer) defaults.push(nextPlayer.name);
    if (defaults.length > 0) setSelectedHordePlayers(defaults);
  }, [authedPlayer, orderedPlayers, selectedHordePlayers.length, setSelectedHordePlayers]);

  function selectPlayerCount(count) {
    const next = orderedPlayers.slice(0, Math.max(0, count)).map(p => p.name);
    setSelectedHordePlayers(next);
  }

  function startCombat() {
    const enemy = enemies.find(e => e.name === enemyName);
    if (!enemy) { setInfo('Select a horde enemy first'); return; }
    if (selectedHordePlayers.length === 0) { setInfo('Select at least one player'); return; }

    const playerList = selectedHordePlayers.map(name => {
      const p = players.find(pl => pl.name === name);
      if (!p) return null;
      const tab = p.tabInfo || {};
      const chars = tab.characteristics || {};
      const ag = (typeof chars.ag === 'number' ? chars.ag : 40);
      const bs = (typeof chars.bs === 'number' ? chars.bs : 40);
      const ws = (typeof chars.ws === 'number' ? chars.ws : 40);
      const t = (typeof chars.t === 'number' ? chars.t : 40);
      const currentWoundsRaw = typeof tab.wounds === 'object' && tab.wounds !== null
        ? Number(tab.wounds.current ?? tab.wounds.total ?? 12)
        : Number(tab.wounds || 12);
      const totalWoundsRaw = typeof tab.wounds === 'object' && tab.wounds !== null
        ? Number(tab.wounds.total ?? tab.wounds.current ?? 12)
        : Number(tab.wounds || 12);
      const maxWounds = Number.isFinite(totalWoundsRaw) && totalWoundsRaw > 0 ? totalWoundsRaw : 12;
      const currentWounds = Number.isFinite(currentWoundsRaw) && currentWoundsRaw >= 0 ? Math.min(currentWoundsRaw, maxWounds) : maxWounds;
      const weapons = Array.isArray(tab.weapons) ? tab.weapons.map(mapPlayerWeapon).filter(w => w.name) : [];
      const mainWeapon = pickMainWeapon(weapons);
      return {
        name,
        ag,
        bs,
        ws,
        tb: Math.max(0, Math.floor(t / 10)),
        wounds: currentWounds,
        maxWounds,
        weapons,
        mainWeaponName: mainWeapon?.name || '',
        mainWeaponDamage: normalizeDamageExpression(mainWeapon?.damage, ''),
        initiative: 0,
        alive: true,
        reactionRound: 0
      };
    }).filter(Boolean);

    if (playerList.length === 0) { setInfo('No valid players found'); return; }

    playerList.forEach(p => {
      const die = Math.floor(Math.random() * 10) + 1;
      const agBonus = Math.floor(p.ag / 10);
      p.initiative = die + agBonus;
      p.initiativeRoll = die;
    });
    playerList.sort((a, b) => b.initiative - a.initiative);

    const enemyMag = Number.isFinite(Number(hordeMagnitude)) && Number(hordeMagnitude) >= 0
      ? Number(hordeMagnitude)
      : Number(enemy.magnitude ?? enemy.wounds ?? 20);
    const enemyObj = {
      name: enemy.name, horde: true, magnitude: enemyMag, maxMagnitude: enemyMag,
      tb: enemy.tb || 4, damage: enemy.damage || '1d10+3',
      ws: enemy.ws || 30, bs: enemy.bs || 30, ag: enemy.ag || 30,
      initiativeRoll: Math.floor(Math.random() * 10) + 1,
      initiative: 0,
      alive: true
    };
    enemyObj.initiative = enemyObj.initiativeRoll + Math.floor((enemy.ag || 30) / 10);
    const initiativeOrder = [...playerList, enemyObj].sort((a, b) => b.initiative - a.initiative);
    const firstActorName = initiativeOrder[0]?.kind === 'enemy' ? enemyObj.name : initiativeOrder[0]?.name;

    setHordeCombat({
      enemy: enemyObj, players: playerList, round: 1, turnIndex: 0, phase: 'idle', lastApplied: 0,
      lastDetail: null,
      log: [
        `Combat started: ${playerList.map(p=>p.name).join(', ')} vs ${enemy.name} (Mag ${enemyMag}, TB ${enemyObj.tb})`,
        `Initiative rolls: ${initiativeOrder.map(entry => `${entry.name} rolled d10 ${entry.initiativeRoll ?? '?'} + ${Math.floor((entry.ag || 30) / 10)} = ${entry.initiative}`).join(' | ')}`,
        `Round 1 turn order begins with ${firstActorName || 'no-one'}.`
      ],
      playerDamage: '1d10+5'
    });
    setInfo('Horde combat started! Use Roll to advance the fight.');
  }

  function getHordeTurnOrder(combat) {
    if (!combat) return [];
    const playersOrder = combat.players.map(p => ({ ...p, kind: 'player' }));
    const enemyOrder = combat.enemy ? [{ ...combat.enemy, kind: 'enemy' }] : [];
    return [...playersOrder, ...enemyOrder].sort((a, b) => b.initiative - a.initiative);
  }

  function nextTurn(baseCombat = null) {
    const combat = baseCombat || hordeCombat;
    if (!combat) return;
    setError(''); setInfo('');
    let nc = { ...combat, players: combat.players.map(p => ({...p})), enemy: {...combat.enemy} };
    const turnOrder = getHordeTurnOrder(nc);
    let nextTurnIndex = nc.turnIndex;
    let nextActor = null;
    for (let i = 0; i < turnOrder.length; i += 1) {
      nextTurnIndex = (nextTurnIndex + 1) % turnOrder.length;
      nextActor = turnOrder[nextTurnIndex];
      let nextAlive = false;
      if (nextActor.kind === 'enemy') nextAlive = !!nc.enemy?.alive;
      else {
        for (const p of nc.players) {
          if (p.name === nextActor.name && p.alive) { nextAlive = true; break; }
        }
      }
      if (nextActor && nextAlive) break;
      nextActor = null;
    }
    if (!nextActor) {
      nc.log.push(`R${nc.round} no living players remain`);
      setHordeCombat(nc);
      setInfo('DEFEAT! All players down.');
      return;
    }
    if (nextTurnIndex <= nc.turnIndex) nc.round += 1;
    if (nextTurnIndex <= nc.turnIndex) {
      nc.players = nc.players.map(p => ({ ...p, reactionRound: 0 }));
    }
    nc.turnIndex = nextTurnIndex;
    nc.phase = 'idle';
    const currentLabel = nextActor.kind === 'enemy' ? nc.enemy.name : nextActor.name;
    nc.log.push(`R${nc.round} turn: ${currentLabel} (Init ${nextActor.initiative})`);
    setHordeCombat(nc);
    setInfo(`Round ${nc.round} - ${currentLabel}'s turn (Init ${nextActor.initiative}).`);
  }

  function playerAttack() {
    if (!hordeCombat) return;
    setError('');
    setLastRollState({ label: 'Rolling', tone: 'sky' });
    let nc = { ...hordeCombat, players: hordeCombat.players.map(p => ({...p})), enemy: {...hordeCombat.enemy} };
    const turnOrder = getHordeTurnOrder(nc);
    const actor = turnOrder[nc.turnIndex] || turnOrder.find(entry => entry.kind === 'enemy' ? nc.enemy?.alive : entry.alive) || null;
    if (!actor) { setInfo('No active horde turn'); return; }

    if (actor.kind === 'player') {
      const player = nc.players.find(p => p.name === actor.name) || null;
      const enemy = nc.enemy;
      if (!player || !player.alive) { setInfo('No active player turn'); return; }
      if (!enemy.alive) { setInfo('Horde already destroyed!'); return; }
      nc.phase = 'attack';
      nc.lastApplied = 0;

      const attackRoll = d100();
      const selectedWeapon = player.weapons?.find(w => w.name === player.mainWeaponName) || pickMainWeapon(player.weapons) || player.weapons?.[0] || null;
      const usesMelee = String(selectedWeapon?.class || '').toLowerCase() === 'melee';
      const attackTarget = usesMelee ? player.ws : player.bs;
      const dg = degrees(attackTarget, attackRoll);
      const damageExpr = normalizeDamageExpression(selectedWeapon?.damage || player.mainWeaponDamage || nc.playerDamage, '1d10+5');

      if (dg.success) {
        let dmgSpec;
        try {
          dmgSpec = parseDice(damageExpr);
        } catch {
          dmgSpec = parseDice('1d10+5');
        }
        const dmgResult = rollDice(dmgSpec.terms, { tearing: !!selectedWeapon?.tearing, proven: selectedWeapon?.proven || 0 });
        const rawDmg = dmgResult.total + dmgSpec.flat;
        nc.phase = 'damage';
        const magnitudeLoss = rawDmg > enemy.tb ? 1 : 0;
        enemy.magnitude -= magnitudeLoss;
        if (enemy.magnitude <= 0) { enemy.magnitude = 0; enemy.alive = false; }
        nc.lastApplied = magnitudeLoss;
        nc.lastDetail = {
          title: `${player.name} damage`,
          lines: [
            `${selectedWeapon ? selectedWeapon.name : 'Main weapon'} • ${damageExpr}`,
            `Attack ${attackRoll} vs ${attackTarget} ${usesMelee ? 'WS' : 'BS'} -> ${dg.dos} DoS`,
            `Damage rolls: ${dmgResult.rolls.join(', ')}${dmgSpec.flat ? ` + ${dmgSpec.flat}` : ''} = ${rawDmg}`,
            `TB ${enemy.tb} -> ${magnitudeLoss > 0 ? `${magnitudeLoss} magnitude lost` : '0 magnitude lost'}`
          ]
        };
        setLastRollState({ label: magnitudeLoss > 0 ? 'Hit' : 'No Damage', tone: magnitudeLoss > 0 ? 'emerald' : 'amber' });
        addRollResult({
          id: uid(),
          ts: Date.now(),
          attackRoll,
          target: attackTarget,
          success: true,
          dos: dg.dos,
          dof: 0,
          mode: 'HORDE',
          rof: 1,
          hits: 1,
          hitsNegated: 0,
          hitsApplied: magnitudeLoss > 0 ? 1 : 0,
          where: 'Horde',
          jammed: false,
          perHit: [{ where: 'Horde', raw: rawDmg, mitigated: magnitudeLoss, detail: `Rolls ${dmgResult.rolls.join(', ')}${dmgSpec.flat ? ` + ${dmgSpec.flat}` : ''} - TB ${enemy.tb} = ${magnitudeLoss}` }],
          weapon: selectedWeapon ? selectedWeapon.name : 'Main weapon',
          actor: player.name,
          enemy: enemy.name,
          using: usesMelee ? 'WS' : 'BS'
        });
        nc.log.push(`R${nc.round} ${player.name} attacks with ${selectedWeapon ? selectedWeapon.name : 'main weapon'}: ${attackRoll} vs ${attackTarget} ${usesMelee ? 'WS' : 'BS'} = ${dg.dos} DoS, dmg ${rawDmg} vs TB ${enemy.tb} = ${magnitudeLoss} magnitude${magnitudeLoss ? '' : ' (negated)'} (Mag ${enemy.magnitude}/${enemy.maxMagnitude})`);
        setInfo(`${player.name} hits! ${magnitudeLoss} creature(s) killed. ${enemy.alive ? `Mag ${enemy.magnitude}/${enemy.maxMagnitude}` : 'Horde destroyed!'}`);
      } else {
        nc.lastDetail = {
          title: `${player.name} attack`,
          lines: [
            `${selectedWeapon ? selectedWeapon.name : 'Main weapon'} • ${damageExpr}`,
            `Attack ${attackRoll} vs ${attackTarget} ${usesMelee ? 'WS' : 'BS'} -> MISS (DoF ${dg.dof})`
          ]
        };
        setLastRollState({ label: 'Fail', tone: 'rose' });
        addRollResult({
          id: uid(),
          ts: Date.now(),
          attackRoll,
          target: attackTarget,
          success: false,
          dos: 0,
          dof: dg.dof,
          mode: 'HORDE',
          rof: 1,
          hits: 0,
          where: 'Horde',
          jammed: false,
          perHit: [],
          weapon: selectedWeapon ? selectedWeapon.name : 'Main weapon',
          actor: player.name,
          enemy: enemy.name,
          using: usesMelee ? 'WS' : 'BS'
        });
        nc.log.push(`R${nc.round} ${player.name} attacks: ${attackRoll} vs ${attackTarget} ${usesMelee ? 'WS' : 'BS'} = MISS (DoF ${dg.dof})`);
        setInfo(`${player.name} misses (DoF ${dg.dof}).`);
      }

      setHordeCombat(nc);
      setTimeout(() => {
        setHordeCombat(prev => prev ? { ...prev, phase: 'wounds' } : prev);
        if (!enemy.alive) setInfo(`VICTORY! Survivors: ${nc.players.filter(p=>p.alive).map(p=>p.name).join(', ')}`);
        else if (nc.players.every(p => !p.alive)) setInfo('DEFEAT! All players down.');
        else {
          setTimeout(() => {
            setHordeCombat(prev => prev ? { ...prev, phase: 'idle' } : prev);
            nextTurn(nc);
          }, PHASE_TIMING.wounds);
        }
      }, PHASE_TIMING.damage);
      return;
    }

    const livingTargets = nc.players.filter(p => p.alive);
    const target = livingTargets.length
      ? livingTargets[Math.floor(Math.random() * livingTargets.length)]
      : null;
    if (!target) { setInfo('DEFEAT! All players down.'); return; }
    const enemyUsesMelee = Number(nc.enemy.ws || 0) > Number(nc.enemy.bs || 0);
    const canParry = enemyUsesMelee && hasMeleeWeapon(target.weapons);
    const reactionType = canParry ? 'Parry' : 'Dodge';
    const reactionStat = canParry ? Number(target.ws || 0) : Number(target.ag || 0);
    const reactionAvailable = (target.reactionRound || 0) !== nc.round;
    let reactionRoll = null;
    let reactionDos = 0;
    let reactionSucceeded = false;
    if (reactionAvailable && reactionStat > 0) {
      reactionRoll = d100();
      nc.phase = 'defense';
      const reactionResult = degrees(reactionStat, reactionRoll);
      reactionSucceeded = !!reactionResult.success;
      reactionDos = reactionSucceeded ? reactionResult.dos : 0;
      target.reactionRound = nc.round;
      nc.log.push(`  ${target.name} ${reactionType}: ${reactionRoll} vs ${reactionStat} = ${reactionSucceeded ? `SUCCESS (${reactionDos} DoS)` : `FAIL (DoF ${reactionResult.dof})`}`);
      if (reactionSucceeded) {
        nc.lastApplied = 0;
        nc.phase = 'defense';
        nc.lastDetail = {
          title: `${target.name} defense`,
          lines: [
            `${nc.enemy.name} targets ${target.name}`,
            `${reactionType} ${reactionRoll} vs ${reactionStat} -> SUCCESS (${reactionDos} DoS)`,
            `Attack negated before damage`
          ]
        };
        setLastRollState({ label: 'Defended', tone: 'amber' });
        addRollResult({
          id: uid(),
          ts: Date.now(),
          attackRoll: reactionRoll,
          target: reactionStat,
          success: true,
          dos: reactionDos,
          dof: 0,
          mode: 'HORDE',
          rof: 1,
          hits: 1,
          hitsNegated: 1,
          hitsApplied: 0,
          where: 'Self',
          jammed: false,
          perHit: [],
          weapon: nc.enemy.name,
          actor: nc.enemy.name,
          enemy: target.name,
          using: enemyUsesMelee ? 'WS' : 'BS',
          defenseType: reactionType,
          defenseRoll: reactionRoll,
          defenseTarget: reactionStat,
          defenseFailed: false
        });
        setInfo(`Round ${nc.round} - ${target.name} ${reactionType.toLowerCase()}s the horde attack.`);
        setHordeCombat(nc);
        setTimeout(() => {
          setHordeCombat(prev => prev ? { ...prev, phase: 'idle' } : prev);
          nextTurn(nc);
        }, PHASE_TIMING.defense);
        return;
      }
    } else {
      nc.log.push(`  ${target.name} has no reaction available this round`);
    }
    let eDmgSpec;
    try {
      eDmgSpec = parseDice(normalizeDamageExpression(nc.enemy.damage, '1d10+3'));
    } catch {
      eDmgSpec = parseDice('1d10+3');
    }
    const eDmgResult = rollDice(eDmgSpec.terms, { tearing: false, proven: 0 });
    const eRaw = eDmgResult.total + eDmgSpec.flat;
    const tb = Number.isFinite(target.tb) ? target.tb : Math.floor(target.maxWounds / 10);
    const dmgToPlayer = Math.max(0, eRaw - tb);
    target.wounds -= dmgToPlayer;
    if (target.wounds <= 0) { target.wounds = 0; target.alive = false; }
    nc.lastDetail = {
      title: `${nc.enemy.name} damage`,
      lines: [
        `${nc.enemy.name} targets ${target.name} randomly`,
        reactionRoll ? `${reactionType} ${reactionRoll} vs ${reactionStat} -> FAIL` : `${target.name} has no reaction available`,
        `Damage rolls: ${eDmgResult.rolls.join(', ')}${eDmgSpec.flat ? ` + ${eDmgSpec.flat}` : ''} = ${eRaw}`,
        `TB ${tb} -> ${dmgToPlayer} damage (${target.wounds}/${target.maxWounds} wounds left${target.alive ? '' : ', down'})`
      ]
    };
    setLastRollState({ label: dmgToPlayer > 0 ? 'Hit' : 'No Damage', tone: dmgToPlayer > 0 ? 'emerald' : 'amber' });
    addRollResult({
      id: uid(),
      ts: Date.now(),
      attackRoll: reactionRoll || eRaw,
      target: reactionRoll ? reactionStat : tb,
      success: dmgToPlayer > 0,
      dos: 0,
      dof: 0,
      mode: 'HORDE',
      rof: 1,
      hits: 1,
      hitsNegated: 0,
      hitsApplied: dmgToPlayer > 0 ? 1 : 0,
      where: 'Body',
      jammed: false,
      perHit: [{ where: 'Body', raw: eRaw, mitigated: dmgToPlayer, detail: `Rolls ${eDmgResult.rolls.join(', ')}${eDmgSpec.flat ? ` + ${eDmgSpec.flat}` : ''} - TB ${tb} = ${dmgToPlayer}` }],
      weapon: nc.enemy.name,
      actor: nc.enemy.name,
      enemy: target.name,
      using: enemyUsesMelee ? 'WS' : 'BS',
      defenseType: reactionRoll ? reactionType : 'No Reaction',
      defenseRoll: reactionRoll || undefined,
      defenseTarget: reactionRoll ? reactionStat : undefined,
      defenseFailed: !!reactionRoll
    });
    nc.log.push(`R${nc.round} turn: ${nc.enemy.name} (Init ${nc.enemy.initiative})`);
    nc.log.push(`  ${nc.enemy.name} targets ${target.name} randomly`);
    nc.log.push(`  ${nc.enemy.name} hits ${target.name}: dmg ${eRaw} vs TB ${tb} = ${dmgToPlayer} (${target.wounds}/${target.maxWounds}${target.alive ? '' : ' - DOWN!'})`);
    nc.lastApplied = dmgToPlayer;
    const reactionText = reactionRoll ? ` ${target.name} ${reactionType.toLowerCase()} roll ${reactionRoll} vs ${reactionStat} failed.` : ` ${target.name} had no reaction left.`;
    setInfo(`Round ${nc.round} - ${nc.enemy.name} hits ${target.name}.${reactionText}`);
    nc.phase = 'damage';
    setHordeCombat(nc);
    if (nc.players.every(p => !p.alive)) setInfo('DEFEAT! All players down.');
    else {
      setTimeout(() => {
        setHordeCombat(prev => prev ? { ...prev, phase: 'wounds' } : prev);
        setTimeout(() => {
          setHordeCombat(prev => prev ? { ...prev, phase: 'idle' } : prev);
          nextTurn(nc);
        }, PHASE_TIMING.wounds);
      }, PHASE_TIMING.damage);
    }
  }

  function skipTurn() {
    if (!hordeCombat) return;
    let nc = { ...hordeCombat, players: hordeCombat.players.map(p => ({...p})), enemy: {...hordeCombat.enemy} };
    const turnOrder = getHordeTurnOrder(nc);
    const actor = turnOrder[nc.turnIndex] || turnOrder.find(entry => entry.kind === 'enemy' ? nc.enemy?.alive : entry.alive) || null;
    const player = actor?.kind === 'player' ? nc.players.find(p => p.name === actor.name) || null : null;
    nc.log.push(`R${nc.round} ${player?.name || nc.enemy.name || '?'} skips`);
    nc.phase = 'wounds';
    setHordeCombat(nc);
    setTimeout(() => nextTurn(nc), 0);
  }

  function endCombat() {
    setHordeCombat(null); setSelectedHordePlayers([]); setEnemyName(''); setInfo('Combat ended.');
  }

  const sortedCombatants = useMemo(() => {
    return getHordeTurnOrder(hordeCombat);
  }, [hordeCombat]);

  const activeActor = hordeCombat?.turnIndex >= 0 ? sortedCombatants[hordeCombat.turnIndex] || null : null;
  const activePlayer = activeActor?.kind === 'player' ? hordeCombat.players.find(p => p.name === activeActor.name) || null : null;
  const selectedWeapon = useMemo(() => {
    if (!activePlayer) return null;
    const available = Array.isArray(activePlayer.weapons) ? activePlayer.weapons : [];
    return available.find(w => w.name === activePlayer.mainWeaponName) || pickMainWeapon(available) || available[0] || null;
  }, [activePlayer]);
  const attackDamageExpr = selectedWeapon?.damage || activePlayer?.mainWeaponDamage || hordeCombat?.playerDamage || '1d10+5';

  useEffect(() => {
    if (!hordeCombat) return;
    if (hordeCombat.turnIndex >= 0 && hordeCombat.turnIndex < sortedCombatants.length) return;
    const firstAliveIndex = sortedCombatants.findIndex(actor => actor.kind === 'enemy' ? hordeCombat.enemy?.alive : actor.alive);
    const normalizedIndex = firstAliveIndex >= 0 ? firstAliveIndex : 0;
    setHordeCombat(prev => prev ? { ...prev, turnIndex: normalizedIndex } : prev);
  }, [hordeCombat, setHordeCombat, sortedCombatants]);

  if (!hordeCombat) {
    return (
      <div className="rounded-lg bg-slate-900 border border-slate-700 p-5 shadow-xl">
        <div className="flex items-center justify-between gap-3 mb-4">
          <div>
            <div className="text-xs uppercase tracking-wide text-slate-400">Horde Combat</div>
            <div className="text-lg font-semibold text-white">Setup</div>
          </div>
          <div className="text-xs text-slate-400">{selectedHordePlayers.length} selected</div>
        </div>
        <div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
          <div className="rounded-lg border border-slate-700 bg-slate-950/40 p-4">
            <label className="text-xs uppercase tracking-wide text-slate-400">Horde Enemy</label>
            <select className="mt-1 w-full rounded border border-slate-600 bg-slate-900 px-3 py-2 text-sm text-slate-100"
              value={enemyName} onChange={e => setEnemyName(e.target.value)}>
              <option value="">Choose horde</option>
              {hordeEnemies.map(e => (
                <option key={e.name} value={e.name}>{e.name} (Mag {e.magnitude ?? e.wounds} / TB {e.tb})</option>
              ))}
            </select>
            <div className="mt-3 grid grid-cols-2 gap-2">
              <div>
                <label className="text-xs uppercase tracking-wide text-slate-400">Current Mag</label>
                <input
                  type="number"
                  min={0}
                  className="mt-1 w-full rounded border border-slate-600 bg-slate-900 px-3 py-2 text-sm text-slate-100"
                  value={hordeMagnitude}
                  onChange={e => setHordeMagnitude(Math.max(0, parseInt(e.target.value || '0')))}
                />
              </div>
              <div className="flex items-end">
                <button
                  type="button"
                  className="w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-xs text-slate-300 hover:bg-slate-800"
                  onClick={() => {
                    const enemy = enemies.find(en => en.name === enemyName)
                    const mag = Number(enemy?.magnitude ?? enemy?.wounds ?? 20)
                    setHordeMagnitude(Number.isFinite(mag) && mag > 0 ? mag : 20)
                  }}
                >
                  Reset
                </button>
              </div>
            </div>
            <div className="mt-3 flex flex-wrap gap-2">
              <button onClick={() => selectPlayerCount(0)} className="rounded border border-slate-700 bg-slate-900 px-3 py-1.5 text-xs text-slate-300 hover:bg-slate-800">None</button>
              {orderedPlayers.map((_, idx) => (
                <button
                  key={`count-${idx + 1}`}
                  onClick={() => selectPlayerCount(idx + 1)}
                  className="rounded border border-slate-700 bg-slate-900 px-3 py-1.5 text-xs text-slate-300 hover:bg-slate-800"
                >
                  {idx + 1}
                </button>
              ))}
            </div>
          </div>
          <div className="rounded-lg border border-slate-700 bg-slate-950/40 p-4">
            <label className="text-xs uppercase tracking-wide text-slate-400">Player Damage</label>
            <div className="mt-1 rounded border border-slate-600 bg-slate-900 px-3 py-2 text-sm text-slate-100">
              {attackDamageExpr}
            </div>
            <div className="mt-2 text-xs text-slate-500">
              {selectedWeapon ? `Main weapon: ${selectedWeapon.name}` : 'Used when a selected player has no weapon data.'}
            </div>
          </div>
        </div>
        <div className="mt-4 rounded-lg border border-slate-700 bg-slate-950/40 p-4">
          <div className="flex items-center justify-between gap-3">
            <label className="text-xs uppercase tracking-wide text-slate-400">Players</label>
            <div className="flex gap-2">
              <button onClick={() => setSelectedHordePlayers([])} className="rounded border border-slate-700 bg-slate-900 px-3 py-1.5 text-xs text-slate-300 hover:bg-slate-800">Clear</button>
              <button onClick={() => setSelectedHordePlayers(orderedPlayers.map(p => p.name))} className="rounded border border-slate-700 bg-slate-900 px-3 py-1.5 text-xs text-slate-300 hover:bg-slate-800">All</button>
            </div>
          </div>
          <div className="mt-3 grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3">
            {orderedPlayers.map(p => {
              const selected = selectedHordePlayers.includes(p.name);
              return (
                <button
                  key={p.name}
                  aria-pressed={selected}
                  onClick={() => {
                    if (selected) setSelectedHordePlayers(prev => prev.filter(n => n !== p.name));
                    else setSelectedHordePlayers(prev => [...prev, p.name]);
                  }}
                  className={`flex items-center justify-between rounded border px-3 py-2 text-left text-sm transition-colors ${selected ? 'border-cyan-500 bg-cyan-950/40 text-cyan-100' : 'border-slate-700 bg-slate-900 text-slate-200 hover:bg-slate-800'}`}
                >
                  <span className="font-medium">{p.name}</span>
                  <span className={`text-xs ${selected ? 'text-cyan-300' : 'text-slate-500'}`}>{selected ? 'Selected' : 'Add'}</span>
                </button>
              );
            })}
          </div>
          <div className="mt-2 text-xs text-slate-500">{selectedHordePlayers.length} player(s) selected</div>
        </div>
        <div className="mt-4 flex items-center justify-end gap-2">
          <button onClick={startCombat}
            className="rounded bg-emerald-700 px-4 py-2 text-sm font-semibold text-white hover:bg-emerald-600 disabled:opacity-50"
            disabled={!enemyName || selectedHordePlayers.length === 0}>
            Start Combat
          </button>
        </div>
      </div>
    );
  }

  const currentP = activePlayer || null;
  const enemy = hordeCombat.enemy;
  const magPct = enemy.maxMagnitude > 0 ? Math.max(0, (enemy.magnitude / enemy.maxMagnitude) * 100) : 0;
  const hordePhase = hordeCombat.phase || 'idle';

  return (
    <div className="rounded-2xl bg-slate-800 backdrop-blur border border-amber-600 p-5 shadow-xl space-y-4">
      <div className="flex items-center justify-between">
        <div className="font-semibold text-amber-300 text-lg">Horde Combat: {enemy.name}</div>
        <div className="flex gap-2">
          <span className="text-xs px-2 py-1 rounded bg-red-700 text-red-100 font-bold">Round {hordeCombat.round}</span>
          {activeActor && (
            <span className="text-xs px-2 py-1 rounded bg-cyan-700 text-cyan-50 font-bold">
              {activeActor.kind === 'enemy' ? `${enemy.name}'s turn` : `${currentP.name}'s turn`}
            </span>
          )}
          <button onClick={endCombat} className="text-xs px-2 py-1 rounded bg-slate-700 hover:bg-slate-600">End Combat</button>
        </div>
      </div>
      <div className="bg-slate-900 rounded-xl p-4 border border-slate-600">
        <div className="flex items-center justify-between mb-2">
          <span className="font-semibold">{enemy.name}</span>
          <span className={`text-sm font-bold ${enemy.alive ? 'text-amber-400' : 'text-slate-500'}`}>
            {enemy.alive ? `${enemy.magnitude}/${enemy.maxMagnitude} creatures` : 'DESTROYED'}
          </span>
        </div>
        <div className="h-3 w-full bg-slate-700 rounded overflow-hidden">
          <div className="h-3 bg-amber-500 transition-all" style={{width: `${magPct}%`}}></div>
        </div>
        <div className="mt-2 grid grid-cols-2 gap-2 text-xs">
          <div>
            <label className="text-slate-400 uppercase tracking-wide">Current Mag</label>
            {canEditMagnitude ? (
              <input
                type="number"
                min={0}
                className="mt-1 w-full rounded border border-slate-600 bg-slate-900 px-2 py-1 text-slate-100"
                value={enemy.magnitude}
                onChange={e => {
                  const nextMag = Math.max(0, parseInt(e.target.value || '0'))
                  setHordeCombat(prev => prev ? { ...prev, enemy: { ...prev.enemy, magnitude: nextMag, alive: nextMag > 0 } } : prev)
                }}
              />
            ) : (
              <div className="mt-1 rounded border border-slate-700 bg-slate-950/60 px-2 py-1 text-slate-200">{enemy.magnitude}</div>
            )}
          </div>
          <div>
            <label className="text-slate-400 uppercase tracking-wide">Max Mag</label>
            {canEditMagnitude ? (
              <input
                type="number"
                min={0}
                className="mt-1 w-full rounded border border-slate-600 bg-slate-900 px-2 py-1 text-slate-100"
                value={enemy.maxMagnitude}
                onChange={e => {
                  const nextMax = Math.max(0, parseInt(e.target.value || '0'))
                  setHordeCombat(prev => prev ? { ...prev, enemy: { ...prev.enemy, maxMagnitude: nextMax, magnitude: Math.min(prev.enemy.magnitude, nextMax), alive: Math.min(prev.enemy.magnitude, nextMax) > 0 } } : prev)
                }}
              />
            ) : (
              <div className="mt-1 rounded border border-slate-700 bg-slate-950/60 px-2 py-1 text-slate-200">{enemy.maxMagnitude}</div>
            )}
          </div>
        </div>
        <div className="text-xs opacity-60 mt-2">TB {enemy.tb} • Damage {enemy.damage}</div>
      </div>
      <div className="rounded-xl border border-slate-700 bg-slate-950/40 p-3">
        <div className="flex items-center gap-2 text-sm">
          <div className={`px-3 py-1 rounded font-semibold ${hordePhase==='attack' ? 'bg-blue-600 text-white animate-pulse' : 'bg-slate-800 text-white'}`}>Attack</div>
          <div className={`px-3 py-1 rounded font-semibold ${hordePhase==='defense' ? 'bg-amber-500 text-black animate-pulse' : 'bg-slate-800 text-white'}`}>Defense</div>
          <div className={`px-3 py-1 rounded font-semibold ${hordePhase==='damage' ? 'bg-rose-500 text-black animate-pulse' : 'bg-slate-800 text-white'}`}>Damage</div>
          <div className={`px-3 py-1 rounded font-semibold ${hordePhase==='wounds' ? 'bg-emerald-500 text-black animate-pulse' : 'bg-slate-800 text-white'}`}>Wounds</div>
          <div className="ml-3 text-xs opacity-70">Phase: <span className="font-semibold">{hordePhase}</span></div>
          <div className="ml-4 text-xs opacity-80">Last Applied: <span className="font-semibold">{hordeCombat.lastApplied || 0}</span></div>
          <div className={`ml-4 text-xs px-2 py-1 rounded font-semibold ${rollStateColor}`}>{lastRollState.label}</div>
        </div>
      </div>
      {hordeCombat.lastDetail && Array.isArray(hordeCombat.lastDetail.lines) && hordeCombat.lastDetail.lines.length > 0 && (
        <div className="bg-slate-900 rounded-xl p-4 border border-slate-600">
          <div className="text-xs uppercase opacity-70 mb-2">Damage Detail</div>
          <div className="text-sm font-semibold text-slate-100 mb-2">{hordeCombat.lastDetail.title}</div>
          <div className="space-y-1">
            {hordeCombat.lastDetail.lines.map((line, idx) => (
              <div key={`${hordeCombat.lastDetail.title}-${idx}`} className="rounded border border-slate-700 bg-slate-950/60 px-2 py-1 text-sm text-slate-200">
                {line}
              </div>
            ))}
          </div>
        </div>
      )}
      <div className="bg-slate-900 rounded-xl p-4 border border-slate-600">
          <div className="text-xs uppercase opacity-70 mb-2">Initiative Order</div>
        <div className="space-y-1">
          {sortedCombatants.map((actor, idx) => {
            const isCurrent = actor.name === activeActor?.name && actor.kind === activeActor?.kind;
            const isNext = idx === ((hordeCombat.turnIndex + 1) % sortedCombatants.length);
            return (
              <div key={`${actor.kind}-${actor.name}`} className={`flex items-center justify-between px-3 py-2 rounded ${isCurrent ? 'bg-cyan-900 border border-cyan-500' : 'bg-slate-800'} ${!actor.alive ? 'opacity-40' : ''}`}>
                <div className="flex items-center gap-2">
                  <span className="text-xs opacity-50">#{idx+1}</span>
                  {isCurrent && <span className="rounded bg-cyan-700 px-1.5 py-0.5 text-[11px] font-semibold text-white">Now</span>}
                  {!isCurrent && isNext && <span className="rounded bg-slate-700 px-1.5 py-0.5 text-[11px] font-semibold text-slate-200">Next</span>}
                  <span className="font-semibold">{actor.kind === 'enemy' ? `${actor.name}` : actor.name}</span>
                  <span className="text-xs opacity-60">Init {actor.initiative}</span>
                </div>
                <div className="flex items-center gap-2">
                  <div className="h-2 w-20 bg-slate-700 rounded overflow-hidden">
                    <div className={`h-2 ${actor.alive ? 'bg-emerald-500' : 'bg-red-600'}`} style={{width: `${(actor.wounds/actor.maxWounds)*100}%`}}></div>
                  </div>
                  <span className="text-xs">{actor.wounds}/{actor.maxWounds}</span>
                  <span className={`text-xs px-1 rounded ${actor.alive ? 'bg-emerald-800 text-emerald-300' : 'bg-red-800 text-red-300'}`}>
                    {actor.alive ? 'Alive' : 'Down'}
                  </span>
                </div>
              </div>
            );
          })}
        </div>
      </div>
      <div className="flex gap-2 flex-wrap items-end">
        <div>
          <label className="text-xs uppercase tracking-wide text-slate-400">Main Weapon</label>
          {currentP && Array.isArray(currentP.weapons) && currentP.weapons.length > 0 ? (
            <div className="mt-1 w-56 rounded border border-slate-600 bg-slate-900 px-3 py-2 text-sm text-slate-100">
              {selectedWeapon?.name || currentP.mainWeaponName || selectedWeapon?.name || 'Main weapon'}
            </div>
          ) : (
            <div className="text-xs opacity-60 mt-1">No sheet weapons found</div>
          )}
        </div>
        <div>
          <label className="text-xs uppercase tracking-wide text-slate-400">Damage</label>
          {selectedWeapon ? (
            <div className="mt-1 min-w-[9rem] rounded border border-slate-600 bg-slate-900 px-3 py-2 text-sm text-slate-100">
              {selectedWeapon.damage}
            </div>
          ) : (
            <div className="mt-1 min-w-[9rem] rounded border border-slate-600 bg-slate-900 px-3 py-2 text-sm text-slate-100">
              {hordeCombat.playerDamage || '1d10+5'}
            </div>
          )}
        </div>
        <button onClick={playerAttack} disabled={!hordeCombat.players.length || !enemy.alive} className="rounded-xl px-4 py-2 bg-amber-600 hover:bg-amber-500 font-bold disabled:opacity-50">Roll</button>
        <button onClick={skipTurn} disabled={!hordeCombat.players.length || !enemy.alive} className="rounded-xl px-4 py-2 bg-slate-700 hover:bg-slate-600 disabled:opacity-50">Skip Turn</button>
      </div>
      {!enemy.alive && <div className="text-center text-emerald-400 font-bold text-lg">VICTORY! Horde destroyed.</div>}
      {hordeCombat.players.every(p => !p.alive) && <div className="text-center text-red-400 font-bold text-lg">DEFEAT! All players down.</div>}
    </div>
  );
}

function __run_tests__() {
  const a = parseDice('1d10+5'); if (!(a.terms.length===1 && a.terms[0].count===1 && a.terms[0].faces===10 && a.flat===5)) throw new Error('parseDice a')
  const b = parseDice('2d10+3'); if (!(b.terms.length===1 && b.terms[0].count===2 && b.terms[0].faces===10 && b.flat===3)) throw new Error('parseDice b')
  const c1 = degrees(50,30); if (!(c1.success && c1.dos===3)) throw new Error('degrees success')
  const c2 = degrees(50,70); if (!(!c2.success && c2.dof===3)) throw new Error('degrees fail')
  if (hitsFromDoS('semi',3,3)!==2) throw new Error('hits semi')
  if (hitsFromDoS('full',3,3)!==3) throw new Error('hits full')
  if (hitsFromDoS('single',5,2)!==1) throw new Error('hits single')
  if (hitLocationFromRoll(1)!=='Head') throw new Error('hitloc head')
  if (hitLocationFromRoll(99)!=='Left Leg') throw new Error('hitloc leg')
  if (hitLocationFromRoll(15)!=='Body') throw new Error('hitloc body')
  if (mitigateDamage(15,4,5)!==6) throw new Error('mitigate a')
  if (mitigateDamage(7,4,5)!==0) throw new Error('mitigate b')
  const p = parseDice('1d10'); if (!(p.terms[0].count===1 && p.terms[0].faces===10 && p.flat===0)) throw new Error('parseDice c')
  if (hitsFromDoS('semi',1,3)!==1) throw new Error('hits semi edge')
  if (mitigateDamage(3,4,5)!==0) throw new Error('mitigate floor')
  try { parseDice('1d10+foo'); throw new Error('parseDice invalid should throw') } catch {}
  const z = (()=>{ try { parseDice('1d10+bar'); return 'ok' } catch { return 'err' } })(); if (z!=='err') throw new Error('invalid dice not handled')
  const eq = degrees(55,55); if (!(eq.success && eq.dos===1)) throw new Error('degrees equality')
  if (hitsFromDoS('semi',0,3)!==1) throw new Error('semi min hits')
  const sp = parseDice(' 2D10 + 5 '); if (!(sp.terms[0].count===2 && sp.terms[0].faces===10 && sp.flat===5)) throw new Error('parseDice spaces')
  const emp = parseDice(''); if (!(emp.terms[0].count===1 && emp.terms[0].faces===10 && emp.flat===0)) throw new Error('parseDice empty')
  try { parseDice('d10') } catch {}
  try { parseDice('0d10') } catch {}
  try { parseDice('1d0') } catch {}
  if (hitsFromDoS('single',3,0)!==1) throw new Error('single min hits')
  const csv1 = 'name,class,damage\nFoo,basic,1d10+5'; const r1 = simpleCSVParse(csv1); if (!(r1.length===1 && r1[0].name==='Foo')) throw new Error('csv lf')
  const csv2 = 'a,b\r\nc,d'; const r2 = simpleCSVParse(csv2); if (!(r2.length===1 && r2[0].a==='c' && r2[0].b==='d')) throw new Error('csv crlf')
}

// Run tests only in development
if (process.env.NODE_ENV === 'development') {
  let __DW_TESTS__RAN = false
  try {
    if (!__DW_TESTS__RAN) {
      __run_tests__()
      __DW_TESTS__RAN = true
    }
  } catch (e) {
    console.error('Test run failed:', e)
  }
}

export default DeathwatchRoller;

// Named export for reuse in other components
export { Tooltip };
