import { useState, useRef, useCallback, useEffect, Fragment } from 'react'
import './App.css'
import maleBody from './assets/body/malebody.png'
import pistolShotMp3 from './assets/pistolshot.mp3'

// ─────────────────────────────────────────────────────────────────────────────
// WORLD DATA
// ─────────────────────────────────────────────────────────────────────────────

const locations = [
  { id: 'A', name: 'Alpha',   description: 'A quiet village at the edge of the forest.', x: 200, y: 300 },
  { id: 'B', name: 'Beta',    description: 'A bustling trade port by the sea.',           x: 600, y: 300 },
  { id: 'C', name: 'Gamma',   description: 'A hidden monastery in the mountains.',        x: 400, y: 130 },
  { id: 'D', name: 'Delta',   description: 'An ancient ruined city swallowed by desert.', x: 100, y: 130 },
  { id: 'E', name: 'Epsilon', description: 'A floating island held aloft by magic.',      x: 700, y: 130 },
]

const connections = [
  { from: 'A', to: 'B' }, { from: 'A', to: 'C' }, { from: 'A', to: 'D' },
  { from: 'B', to: 'C' }, { from: 'B', to: 'E' },
  { from: 'C', to: 'D' }, { from: 'C', to: 'E' },
]

const characters = [
  { id: 'player', name: 'John',            location: 'A', size: 1 },
  { id: '1',      name: 'Mara',           location: 'A', money: 120, inventory: ['pistol_ammo'], size: 1 },
  { id: '2',      name: 'Rook',           location: 'A', money: 0,   inventory: ['pistol_ammo'], size: 1 },
  { id: '3',      name: 'Vex',            location: 'B', money: 500, inventory: ['pistol_ammo'], size: 1 },
  { id: 'd1',     name: 'Dust Gutter',    location: 'D', money: 15,  inventory: ['pistol_ammo'], size: 1 },
  { id: 'd2',     name: 'Bone Collector', location: 'D', money: 30,  inventory: ['pistol_ammo'], size: 1 },
  { id: 'd3',     name: 'Sand Viper',     location: 'D', money: 8,   inventory: ['pistol_ammo'], size: 1 },
  { id: 'd4',     name: 'Rust-Eye',       location: 'D', money: 45,  inventory: ['pistol_ammo'], size: 1 },
  { id: 'd5',      name: 'Cinder',         location: 'D', money: 22,  inventory: ['pistol_ammo'], size: 1 },
]

const weapons = [
  { id: 'fists',       name: 'Fists',       damage: 35,  condition: 100, maxCondition: 100, passives: ['Brawler'],          weight: 0, speed: 14, skills: [{ name: 'Jab',          mult: 1.0, damageType: 'blunt', range: 12 }, { name: 'Hook',         mult: 1.3, damageType: 'blunt', range: 12 }, { name: 'Kick',     mult: 1.5, damageType: 'blunt', range: 14 }, { name: 'Shove', mult: 0, damageType: 'shove', range: 10 }] },
  { id: 'pistol',      name: 'Pistol',      damage: 70,  condition: 100, maxCondition: 100, passives: ['Ranged'],          weight: 3, speed: 12, skills: [{ name: 'Shoot',        mult: 1.0, damageType: 'pierce', range: 200 }, { name: 'Load', mult: 0, damageType: 'reload', range: 0 }, { name: 'Unload', mult: 0, damageType: 'unload', range: 0 }] },
  { id: 'rusty_sword', name: 'Rusty Sword', damage: 60,  condition: 65,  maxCondition: 100, passives: ['Bleed'],            weight: 4, speed: 10, skills: [{ name: 'Slash',         mult: 1.0, damageType: 'slash', range: 15 }, { name: 'Stab',         mult: 1.2, damageType: 'pierce', range: 18 }] },
  { id: 'steel_sword', name: 'Steel Sword', damage: 85,  condition: 100, maxCondition: 100, passives: ['Bleed', 'Pierce'],  weight: 5, speed: 9,  skills: [{ name: 'Slash',         mult: 1.0, damageType: 'slash', range: 15 }, { name: 'Stab',         mult: 1.2, damageType: 'pierce', range: 18 }, { name: 'Overhead', mult: 1.4, damageType: 'slash', range: 14 }] },
  { id: 'iron_shield', name: 'Iron Shield', damage: 40,  condition: 100, maxCondition: 100, passives: ['Block', 'Sturdy'],  weight: 6, speed: 7,  skills: [{ name: 'Bash',          mult: 1.0, damageType: 'blunt', range: 12 }, { name: 'Shield Slam',  mult: 1.3, damageType: 'blunt', range: 13 }] },
  { id: 'dagger',      name: 'Dagger',      damage: 50,  condition: 100, maxCondition: 100, passives: ['Quick', 'Bleed'],   weight: 2, speed: 13, skills: [{ name: 'Slash',         mult: 1.0, damageType: 'slash', range: 12 }, { name: 'Stab',         mult: 1.3, damageType: 'pierce', range: 15 }] },
  { id: 'warhammer',   name: 'Warhammer',   damage: 100, condition: 100, maxCondition: 100, passives: ['Crush', 'Stagger'], weight: 8, speed: 6,  skills: [{ name: 'Pound',         mult: 1.0, damageType: 'blunt', range: 14 }, { name: 'Slam',         mult: 1.4, damageType: 'blunt', range: 13 }] },
]

const items = [
  { id: 'pistol_ammo',   name: 'Pistol Ammo',   description: 'Standard pistol ammunition.',                                   weight: 0.5 },
  { id: 'bouncing_ammo', name: 'Bouncing Ammo', description: 'Experimental ammo that bounces off walls.',                      weight: 0.5 },
]

const itemMap = {}
items.forEach(i => { itemMap[i.id] = i })

const weaponMap = {}
weapons.forEach(w => { weaponMap[w.id] = w })
// Virtual weapon entries for bare arms
const ARM_WEAPON = { damage: 35, condition: 100, maxCondition: 100, passives: ['Brawler'], weight: 0, speed: 14, skills: [{ name: 'Jab', mult: 1.0, damageType: 'blunt', range: 12 }, { name: 'Hook', mult: 1.3, damageType: 'blunt', range: 12 }, { name: 'Kick', mult: 1.5, damageType: 'blunt', range: 14 }, { name: 'Shove', mult: 0, damageType: 'shove', range: 10 }] }
weaponMap['left_arm']  = { id: 'left_arm',  name: 'Left Arm',  ...ARM_WEAPON }
weaponMap['right_arm'] = { id: 'right_arm', name: 'Right Arm', ...ARM_WEAPON }

// ─────────────────────────────────────────────────────────────────────────────
// CONSTANTS
// ─────────────────────────────────────────────────────────────────────────────

const MAX_BODY   = { head: 100, torso: 100, leftArm: 100, rightArm: 100, leftLeg: 100, rightLeg: 100 }
const MAX_HP     = Object.values(MAX_BODY).reduce((a, b) => a + b, 0)
const MAX_WEIGHT = 50

function generateLocationBlocks(locationId) {
  let seed = 0
  for (let i = 0; i < locationId.length; i++) {
    seed = ((seed << 5) - seed) + locationId.charCodeAt(i)
    seed = seed & seed
  }
  let s = seed
  function rng() { s = (s * 1664525 + 1013904223) & 0xFFFFFFFF; return (s >>> 0) / 0xFFFFFFFF }
  const blocks = []
  const num = 3 + Math.floor(rng() * 3)
  for (let i = 0; i < num; i++) {
    const w = 15 + Math.floor(rng() * 25); const h = 15 + Math.floor(rng() * 35)
    const x = 5 + Math.floor(rng() * (150 - w - 5)); const y = 5 + Math.floor(rng() * (150 - h - 5))
    blocks.push({ x, y, w, h })
  }
  return blocks
}

const ITEM_VALUES = {
  pistol_ammo: 5, bouncing_ammo: 8, steel_sword: 80, iron_shield: 50, dagger: 30,
  rusty_sword: 40, pistol: 100, warhammer: 90,
}
function getItemValue(id) {
  if (ITEM_VALUES[id] !== undefined) return ITEM_VALUES[id]
  if (weaponMap[id]) return weaponMap[id].damage
  return 1
}

const INJURY_DECAY = { cut: 0.3, laceration: 0.6, stab: 0.8 }
const TURN_SPEED = 6
const PISTOL_MAG = 2

const bodyPartLabels = {
  head: 'Head', torso: 'Torso',
  leftArm: 'Left Arm', rightArm: 'Right Arm',
  leftLeg: 'Left Leg', rightLeg: 'Right Leg',
}

// ─────────────────────────────────────────────────────────────────────────────
// PURE HELPERS  (no React state)
// ─────────────────────────────────────────────────────────────────────────────

function rectCollides(px, py, block) {
  return px >= block.x && px <= block.x + block.w && py >= block.y && py <= block.y + block.h
}

function partColor(hp, max) {
  const r = hp / max
  if (r > 0.6) return '#22c55e'
  if (r > 0.3) return '#eab308'
  return '#ef4444'
}

function freshBody()    { return { head: 100, torso: 100, leftArm: 100, rightArm: 100, leftLeg: 100, rightLeg: 100 } }
function freshInjuries(){ return { head: [], torso: [], leftArm: [], rightArm: [], leftLeg: [], rightLeg: [] } }
function totalHp(body)  { return Object.values(body).reduce((a, b) => a + b, 0) }
function isDead(integrity) { return !integrity || integrity.head <= 0 || integrity.torso <= 0 }

function createCharacter(data) {
  const weaponId = data.weapon ?? 'fists'
  const wpn = weaponMap[weaponId]
  const integrity = data.integrity ?? freshBody()
  const injuries = data.injuries ?? freshInjuries()
  return {
    id: data.id ?? 'unknown',
    name: data.name ?? 'Unknown',
    age: data.age ?? Math.floor(Math.random() * 30) + 18,
    location: data.location ?? 'Unknown',
    money: data.money ?? 0,
    inventory: [...(data.inventory ?? [])],
    equipped: data.equipped ? { left: data.equipped.left, right: data.equipped.right } : { left: 'fists', right: 'fists' },
    weaponConditions: data.weaponConditions ? { ...data.weaponConditions } : {},
    integrity: { ...integrity },
    injuries: Object.keys(injuries).reduce((acc, k) => { acc[k] = [...injuries[k]]; return acc }, {}),
    size: data.size ?? 1,
    fillColor: data.fillColor ?? `hsl(${Math.random() * 360}, 70%, 55%)`,
    weapon: weaponId,
    pos: data.pos ? { x: data.pos.x, y: data.pos.y } : undefined,
    speed: (data.speed ?? wpn?.speed ?? 10) / 2,
    maxSpeed: data.maxSpeed ?? 2,
    attack: data.attack ?? wpn?.damage ?? 30,
    weaponMagazines: data.weaponMagazines ? { ...data.weaponMagazines } : {},
    itemCounts: data.itemCounts ? { ...data.itemCounts } : {},
  }
}

function randomPart() {
  const parts = ['head', 'torso', 'leftArm', 'rightArm', 'leftLeg', 'rightLeg']
  return parts[Math.floor(Math.random() * parts.length)]
}

function injuryType(damageType) {
  if (damageType === 'blunt')  return 'laceration'
  if (damageType === 'pierce') return 'stab'
  return 'cut'
}

function applyInjury(injuries, part, type, severity) {
  return { ...injuries, [part]: [...(injuries[part] ?? []), { type, severity }] }
}

function calcIntegrity(integrity, injuries, deltaTime = 1) {
  let drain = 0
  for (const part of Object.keys(injuries)) {
    for (const inj of injuries[part]) drain += (INJURY_DECAY[inj.type] ?? 0.3) * inj.severity
  }
  const out = {}
  for (const part of Object.keys(integrity)) out[part] = Math.max(0, integrity[part] - drain * deltaTime)
  return out
}

// ─────────────────────────────────────────────────────────────────────────────
// MODULAR COMBAT ENGINE
// These functions are used by BOTH player and enemy logic.
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Move an actor toward a target, respecting the given blocks.
 * Returns the new {x, y} position.
 * @param {{x:number,y:number}} from
 * @param {{x:number,y:number}} to
 * @param {number} speed   – units per step
 * @returns {{x:number,y:number}}
 */
function computeMove(from, to, speed, blocks) {
  const dx = to.x - from.x
  const dy = to.y - from.y
  const dist = Math.sqrt(dx * dx + dy * dy)
  if (dist <= speed) return { x: to.x, y: to.y }

  const step = 0.5
  const totalSteps = Math.ceil(speed / step)
  let cx = from.x, cy = from.y; const blk = blocks ?? []
  for (let i = 0; i < totalSteps; i++) {
    const remaining = speed - i * step
    const thisStep = Math.min(step, remaining)
    const nx = cx + (dx / dist) * thisStep
    const ny = cy + (dy / dist) * thisStep
    if (blk.some(b => rectCollides(nx, ny, b))) break
    cx = nx; cy = ny
  }
  return { x: cx, y: cy }
}

/**
 * Compute a shove effect, returning the pushed position.
 */
function computeShove(attackerPos, targetPos, blocks) {
  const pushDist = 30 + Math.floor(Math.random() * 20)
  const dx = targetPos.x - attackerPos.x
  const dy = targetPos.y - attackerPos.y
  const dist = Math.sqrt(dx * dx + dy * dy) || 1
  const nx = dx / dist, ny = dy / dist
  let ex = targetPos.x, ey = targetPos.y; const blk = blocks ?? []
  const steps = Math.ceil(pushDist / 3)
  for (let i = 1; i <= steps; i++) {
    const sx = targetPos.x + nx * (pushDist * i / steps)
    const sy = targetPos.y + ny * (pushDist * i / steps)
    if (blk.some(b => rectCollides(sx, sy, b))) break
    ex = sx; ey = sy
  }
  const actualPush = Math.sqrt((ex - targetPos.x) ** 2 + (ey - targetPos.y) ** 2)
  const clamped = { x: Math.max(5, Math.min(145, ex)), y: Math.max(5, Math.min(145, ey)) }
  return { pos: clamped, actualPush }
}

/**
 * Resolve damage for a single hit.
 * Works for any attacker — pass in weapon, skill, condition, and an optional bothHandsMult.
 * Returns { dmg, severity, injType, injLabel }
 */
function computeHit({ weaponId, skill, weaponConditions, bothMult = 1.0 }) {
  if (skill.damageType === 'shove') return { dmg: 0, severity: 0, injType: 'shove', injLabel: 'shove' }
  const base = weaponMap[weaponId]
  if (!base) return { dmg: 1, severity: 1, injType: 'cut', injLabel: 'cut' }

  const cond = weaponConditions?.[weaponId] ?? base.maxCondition
  const conditionPenalty = cond < 30 ? 0.5 : cond < 60 ? 0.25 : 0
  const adjusted = Math.round(base.damage * skill.mult * bothMult * (1 - conditionPenalty))
  const variance = Math.floor(Math.random() * (Math.max(2, adjusted / 2) + 1)) - Math.floor(Math.max(2, adjusted / 4))
  const dmg = Math.max(1, adjusted + variance)
  const severity = Math.round(dmg / 5) + 1
  const injType = injuryType(skill.damageType)
  const injLabel = injType === 'laceration' ? 'laceration' : injType === 'stab' ? 'stab wound' : 'cut'
  return { dmg, severity, injType, injLabel }
}

/**
 * Get range for a weapon + skill combo.
 */
function getSkillRange(weaponId, skillName) {
  if (weaponId === 'left_arm' || weaponId === 'right_arm') return 12
  const base = weaponMap[weaponId]
  if (!base) return 15
  const skill = (base.skills ?? []).find(s => s.name === skillName)
  return skill?.range ?? 15
}

/**
 * Check whether an attacker at `attackerPos` can reach target at `targetPos`
 * with the given weapon + skill.
 */
function canReach(attackerPos, targetPos, weaponId, skillName) {
  const dx = attackerPos.x - targetPos.x
  const dy = attackerPos.y - targetPos.y
  return Math.sqrt(dx * dx + dy * dy) <= getSkillRange(weaponId, skillName)
}

/**
 * Build a player attack action for the action queue.
 * Returns an action object that actionHandlers.attack can process.
 */
function buildPlayerAttack({ weaponId, skillName, targetPart, enemyIndex, equippedWeapons, weaponConditions, bodyParts, bodyInjuries, combat, actor = 'player', charId = 'player' }) {
  const base = weaponMap[weaponId]
  const isArm = weaponId === 'left_arm' || weaponId === 'right_arm'
  const skill = isArm
    ? { name: 'Punch', mult: 0.3, damageType: 'blunt', range: 12 }
    : (base?.skills ?? []).find(s => s.name === skillName) ?? { name: 'Attack', mult: 1.0, damageType: 'blunt' }

  if (skill.damageType === 'shove') {
    return { type: 'shove_pending', actor, weaponId, skillName, enemyIndex, charId }
  }

  const bothMult = (!isArm && equippedWeapons.left === weaponId && equippedWeapons.right === weaponId) ? 1.5 : 1.0
  const { dmg, severity, injType, injLabel } = computeHit({ weaponId, skill, weaponConditions, bothMult })
  const enemyId = combat.enemyIds[enemyIndex]
  const enemy = enemyId ? combat.characters[enemyId] : null

  return {
    type: 'attack',
    actor,
    charId,
    enemyCharId: enemyId,
    weaponId, skillName, targetPart, enemyIndex,
    dmg, severity, injType, injLabel,
    attackerParts:   { ...bodyParts },
    attackerInjuries: { ...bodyInjuries },
    targetParts:     enemy ? { ...enemy.integrity } : {},
    targetInjuries:  enemy ? { ...enemy.injuries } : {},
  }
}

/**
 * Run enemy AI. Returns an array of raw action objects.
 * Uses the same computeMove / computeHit as the player.
 */
function runEnemyAI(combat, playerParts, playerInjuries, deadThisTick) {
  const actions = []
  const partyIds = combat.partyIds ?? [combat.playerEntityId].filter(Boolean)
  const aliveParty = partyIds.filter(pid => {
    const ch = combat.characters[pid]
    return ch && !isDead(ch.integrity) && !(deadThisTick?.has(pid))
  })
  if (aliveParty.length === 0) return actions

  for (const cid of Object.keys(combat.characters)) {
    if (partyIds.includes(cid) || (combat.npcIds ?? []).includes(cid)) continue
    const e = combat.characters[cid]
    if (isDead(e.integrity)) continue

    // Find nearest alive party member for this enemy
    let targetId = null, targetPos = null, minDist = Infinity
    for (const pid of aliveParty) {
      const pch = combat.characters[pid]
      const d = Math.sqrt((pch.pos.x - e.pos.x) ** 2 + (pch.pos.y - e.pos.y) ** 2)
      if (d < minDist) { minDist = d; targetId = pid; targetPos = pch.pos }
    }
    if (!targetId) continue
    const targetChar = combat.characters[targetId]

    // ── in range → attack
      const weaponId = e.weapon ?? 'fists'
      const base = weaponMap[weaponId]
      const skills = base?.skills ?? [{ name: 'Attack', mult: 1.0, damageType: 'blunt', range: 15 }]
      const maxRange = Math.max(...skills.map(s => s.range ?? 15))
      if (minDist > maxRange) {
        // out of range → move toward target
        const speed = e.speed > 12 ? 10 : 5
        const newPos = computeMove(e.pos, targetPos, speed, combat.blocks)
        if (newPos.x !== e.pos.x || newPos.y !== e.pos.y) {
          actions.push({ type: 'move', charId: cid, fromX: e.pos.x, fromY: e.pos.y, toX: newPos.x, toY: newPos.y, speed })
        }
        continue
      }
      const skill = skills[Math.floor(Math.random() * skills.length)]
      const { dmg, severity, injType, injLabel } = computeHit({ weaponId, skill, weaponConditions: {} })
      const targetPart = randomPart()
      actions.push({
        type: 'attack',
        actor: 'enemy',
        charId: cid,
        targetCharId: targetId,
        targetPart,
        weaponId,
        skillName: skill.name,
        dmg, severity, injType, injLabel,
        attackerParts:   { ...e.integrity },
        attackerInjuries: { ...e.injuries },
        targetParts:     { ...targetChar.integrity },
        targetInjuries:  { ...targetChar.injuries },
      })
  }

  return actions
}

// ─────────────────────────────────────────────────────────────────────────────
// BODY IMAGE COMPONENT
// ─────────────────────────────────────────────────────────────────────────────

const bodyOverlays = [
  { part: 'head',     left: '22%', top: '1%',  width: '56%', height: '20%' },
  { part: 'torso',    left: '18%', top: '22%', width: '64%', height: '35%' },
  { part: 'leftArm',  left: '2%',  top: '22%', width: '20%', height: '34%' },
  { part: 'rightArm', left: '78%', top: '22%', width: '20%', height: '34%' },
  { part: 'leftLeg',  left: '20%', top: '58%', width: '30%', height: '36%' },
  { part: 'rightLeg', left: '50%', top: '58%', width: '30%', height: '36%' },
]

function BodyImage({ body }) {
  return (
    <div className="body-image-wrap">
      <img src={maleBody} alt="" className="male-body" />
      {bodyOverlays.map(o => (
        <div key={o.part} className="body-part-overlay"
          style={{ left: o.left, top: o.top, width: o.width, height: o.height, background: partColor(body[o.part], MAX_BODY[o.part]) }} />
      ))}
    </div>
  )
}

// ─────────────────────────────────────────────────────────────────────────────
// CHARACTER CREATOR
// ─────────────────────────────────────────────────────────────────────────────

function CharacterCreator({ onConfirm }) {
  const [name, setName] = useState('John')
  const [weaponId, setWeaponId] = useState('rusty_sword')
  const [fillColor, setFillColor] = useState('#fbbf24')
  const [locationId, setLocationId] = useState('A')

  const startWeapons = [
    { id: 'rusty_sword', name: 'Rusty Sword' },
    { id: 'pistol', name: 'Pistol' },
    { id: 'steel_sword', name: 'Steel Sword' },
    { id: 'iron_shield', name: 'Iron Shield' },
    { id: 'dagger', name: 'Dagger' },
    { id: 'fists', name: 'Fists only' },
  ]

  return (
    <div className="menu-screen">
      <div className="char-create-panel">
        <h1 className="char-create-title">CREATE CHARACTER</h1>

        <div className="char-create-field">
          <label>Name</label>
          <input type="text" value={name} onChange={e => setName(e.target.value)} className="char-create-input" maxLength={20} />
        </div>

        <div className="char-create-field">
          <label>Starting Weapon</label>
          <div className="char-create-options">
            {startWeapons.map(w => (
              <button key={w.id} className={'char-create-option' + (weaponId === w.id ? ' selected' : '')}
                onClick={() => setWeaponId(w.id)}>{w.name}</button>
            ))}
          </div>
        </div>

        <div className="char-create-field">
          <label>Fill Color</label>
          <input type="color" value={fillColor} onChange={e => setFillColor(e.target.value)} className="char-create-color" />
        </div>

        <div className="char-create-field">
          <label>Starting Location</label>
          <div className="char-create-options">
            {locations.map(l => (
              <button key={l.id} className={'char-create-option' + (locationId === l.id ? ' selected' : '')}
                onClick={() => setLocationId(l.id)}>{l.id} — {l.name}</button>
            ))}
          </div>
        </div>

        <button className="menu-btn" onClick={() => onConfirm({ name, weaponId, fillColor, locationId })}>Start Game</button>
      </div>
    </div>
  )
}

// ─────────────────────────────────────────────────────────────────────────────
// APP
// ─────────────────────────────────────────────────────────────────────────────

export default function App() {
  // ── world state
  const [player, setPlayer] = useState(createCharacter({
    id: 'player', name: 'John', age: 24, location: 'A', money: 50,
    inventory: ['pistol_ammo', 'bouncing_ammo'],
    equipped: { left: 'pistol', right: 'rusty_sword' },
    weaponConditions: { rusty_sword: 65, pistol: 100 },
    size: 1.3,
    weaponMagazines: { pistol: [{ ammoType: 'pistol_ammo' }, { ammoType: 'pistol_ammo' }] },
    itemCounts: { pistol_ammo: 20, bouncing_ammo: 10 },
  }))
  const [gameTime,        setGameTime]        = useState({ day: 1, hour: 8, minute: 0 })
  const [weather,         setWeather]         = useState('clear')
  const [defeatedNpcs,    setDefeatedNpcs]    = useState([])
  const [characterOverrides, setCharacterOverrides] = useState({})
  const liveCharacters = characters.map(c => ({ ...c, ...characterOverrides[c.id] })).filter(c => !defeatedNpcs.includes(c.id))

  // ── ui state
  const [hovered,          setHovered]          = useState(null)
  const [selected,         setSelected]         = useState(null)
  const [mousePos,         setMousePos]         = useState({ x: 0, y: 0 })
  const [view,             setView]             = useState({ x: 0, y: 0, scale: 1 })
  const [travelAnim,       setTravelAnim]       = useState(null)
  const [hoverInfo,        setHoverInfo]        = useState(null)
  const [showInventory,    setShowInventory]    = useState(false)
  const [showWorldInfo,   setShowWorldInfo]   = useState(false)
  const [showWorldMap,    setShowWorldMap]    = useState(false)
  const [showSettings,     setShowSettings]     = useState(false)
  const [invTab,           setInvTab]           = useState('health')
  const [panelTarget,      setPanelTarget]      = useState('player')
  const [contextMenu,      setContextMenu]      = useState(null)
  const [equipArmChoice,   setEquipArmChoice]   = useState(null)
  const [showTrade,        setShowTrade]        = useState(null)
  const [tradeTarget,      setTradeTarget]      = useState(null)
  const [tradeGive,        setTradeGive]        = useState({})
  const [tradeTake,        setTradeTake]        = useState({})
  const [passTimeOpen,     setPassTimeOpen]     = useState(false)
  const [passTimeHours,    setPassTimeHours]    = useState(1)
  const [skipAnim,         setSkipAnim]         = useState(null)
  const [zoomAnim,         setZoomAnim]         = useState(null)


  // ── combat state
  const [combat,           setCombat]           = useState(null)
  const [combatTime,       setCombatTime]       = useState(0)
  const [displayTime,      setDisplayTime]      = useState(0)
  // timerActive: drives the timer — kept as a ref only (no re-render needed)
  // so the rAF loop sees it synchronously on the very next frame.

  // ── combat ui
  const [expandedWeapon,   setExpandedWeapon]   = useState([])
  const [weaponsOpen,      setWeaponsOpen]      = useState([])
  const [itemsOpen,        setItemsOpen]        = useState([])
  const [movementOpen,     setMovementOpen]     = useState([])
  const [limbSelect,       setLimbSelect]       = useState(null)
  const [selectedTarget,   setSelectedTarget]   = useState(null)
  const [selectedSkill,    setSelectedSkill]    = useState(null)
  const [hoveredSkill,     setHoveredSkill]     = useState(null)
  const [hoveredEnemy,     setHoveredEnemy]     = useState(null)
  const [selectedEnemy,    setSelectedEnemy]    = useState(0)
  const [attackEnemy,      setAttackEnemy]      = useState(null)
  const [moveMode,         setMoveMode]         = useState(null)
  const [currentActor,     setCurrentActor]     = useState(null)

  // ── animation state
  const [animations,       setAnimations]       = useState([])
  const [hitShake,         setHitShake]         = useState(null)
  const [minimapView,      setMinimapView]      = useState({ x: 0, y: 0, scale: 1 })

  // ── ui busy (mirrors ref-based action state for re-renders)
  const [busy,             setBusy]             = useState(false)

  // ── aiming mode (pistol skill selected, waiting for minimap click)
  const [aimMode,          setAimMode]          = useState(false)

  // ── reload ammo picker removed; uses selectedSkill === 'Load' inline

  // ── projectiles (modular projectile framework)
  const [projectiles, setProjectiles] = useState([])

  // ── hovered character for arrow glow
  const [hoveredChar, setHoveredChar] = useState(null)

  // ── replay
  const [fightRecord,      setFightRecord]      = useState(null)
  const [replayActive,     setReplayActive]     = useState(false)

  // ── menu
  const [showMenu, setShowMenu] = useState(true)
  const [showCharCreate, setShowCharCreate] = useState(false)

  // ── refs (live values for rAF callbacks)
  const combatRef          = useRef(null);          combatRef.current          = combat
  const combatTimeRef      = useRef(0);             combatTimeRef.current      = combatTime
  const displayTimeRef     = useRef(0);             displayTimeRef.current     = displayTime
  const timerActiveRef     = useRef(false)
  const [timerRunning,     setTimerRunning]     = useState(false)
  const prevBusyRef        = useRef(false)
  const speedRef           = useRef(1)
  const [speedMult,        setSpeedMult]        = useState(1)
  const playerRef           = useRef(null);          playerRef.current          = player
  const magazineRef        = useRef({ pistol: [{ ammoType: 'pistol_ammo' }, { ammoType: 'pistol_ammo' }] }); magazineRef.current        = player.weaponMagazines
  const deadThisTickRef    = useRef(new Set())

  // ── Universal character action/direction/position refs ──
  // Every character (player, enemies) uses the same ref structure.
  // The player entity ID is combat.playerEntityId.
  // Character IDs: 'player', 'enemy_0', 'enemy_1', ...
  const characterActionsRef = useRef({})    // { [charId]: action }
  const characterDirsRef    = useRef({})    // { [charId]: {dx,dy} }
  const characterPositionsRef = useRef({})  // { [charId]: {x,y} }

  const recordingRef       = useRef(null)
  const viewRef            = useRef(null);          viewRef.current            = view
  const svgRef             = useRef(null)
  const isPanning          = useRef(false)
  const panStart           = useRef({ x: 0, y: 0 })
  const minimapPanRef      = useRef(false)
  const minimapPanStart    = useRef({ x: 0, y: 0 })
  const minimapMovedRef    = useRef(false)
  const replayElapsedRef   = useRef(0)

  // ─────────────────────────────────────────────────────────────────────────
  // DERIVED
  // ─────────────────────────────────────────────────────────────────────────

  const locMap        = {}; locations.forEach(l => { locMap[l.id] = l })
  const weatherLabels = { clear: 'Clear', cloudy: 'Cloudy', rain: 'Rain', storm: 'Storm' }
  const getDateString = (day) => {
    const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']
    const DAYS   = [31,28,31,30,31,30,31,31,30,31,30,31]
    let d = day, m = 5 // June
    while (d > DAYS[m]) { d -= DAYS[m]; m++; if (m > 11) m = 0 }
    return `${MONTHS[m]} ${d}`
  }
  const playerHp      = totalHp(player.integrity)
  // liveCharacters defined above with characterOverrides

  const connectedTo = (locId) => {
    const result = []
    for (const c of connections) {
      if (c.from === locId) result.push(c.to)
      if (c.to === locId)   result.push(c.from)
    }
    return result
  }

  // ─────────────────────────────────────────────────────────────────────────
  // ANIMATION SYSTEM — modular & extensible
  // ─────────────────────────────────────────────────────────────────────────
  // To add a new animation type:
  //   1. Add an entry to ANIM_TYPES
  //   2. Add a config to ANIM_CFG   { duration, hitAt }
  //   3. Add a factory function that creates the animation object
  //   4. Add a case to getAnimPos() for rendering
  //   5. Call the factory when creating the animation
  // ─────────────────────────────────────────────────────────────────────────

  const ANIM_TYPES = {
    SLASH: 'slash',
    PIERCE_STRIKE: 'pierce_strike',
    FIST_STRIKE: 'fist_strike',
    FIST_HOOK: 'fist_hook',
    SHOOT: 'shoot',
  }

  const ANIM_CFG = {
    [ANIM_TYPES.SLASH]: { duration: 0.3, hitAt: 0.15 },
    [ANIM_TYPES.PIERCE_STRIKE]: { duration: 0.3, hitAt: 0.15 },
    [ANIM_TYPES.FIST_STRIKE]: { duration: 0.25, hitAt: 0.1 },
    [ANIM_TYPES.FIST_HOOK]: { duration: 0.3, hitAt: 0.15 },
    [ANIM_TYPES.SHOOT]: { duration: 0.5, hitAt: 0.5 },
  }

  // ─────────────────────────────────────────────────────────────────────────
  // MODULAR PROJECTILES FRAMEWORK
  // Types, config, factory, and step function.
  // ─────────────────────────────────────────────────────────────────────────

  const PROJ_TYPES = {
    BULLET: 'bullet',
  }

  const PROJ_CFG = {
    [PROJ_TYPES.BULLET]: { speed: 800, hitRadius: 10 },
  }

  function createProjectile(startPos, endPos, actor, weaponId, skillName, ammoType, charId) {
    const dx = endPos.x - startPos.x
    const dy = endPos.y - startPos.y
    const clickDist = Math.sqrt(dx * dx + dy * dy) || 1
    const range = weaponMap[weaponId]?.skills?.find(s => s.name === skillName)?.range ?? 60
    return {
      id: `proj-${Date.now()}`,
      type: PROJ_TYPES.BULLET,
      actor,
      charId: charId ?? 'player',
      startPos: { ...startPos },
      endPos: { ...endPos },
      pos: { ...startPos },
      distTraveled: 0,
      dist: Math.max(clickDist, range),
      dir: { x: dx / clickDist, y: dy / clickDist },
      weaponId,
      skillName,
      ammoType,
      hitFired: false,
    }
  }

  /**
   * Step all active projectiles. Returns the filtered array of still-active projectiles.
   * Applies hit damage / logging as side effects.
   */
  function stepProjectiles(projs, dt, cc) {
    if (!cc) return []
    const out = []
    for (const p of projs) {
      if (p.hitFired) continue

      const cfg = PROJ_CFG[p.type]
      const stepDist = cfg.speed * dt
      const remaining = p.dist - p.distTraveled
      const moveDist = Math.min(stepDist, remaining)

      const subSteps = Math.max(1, Math.ceil(moveDist))
      let hit = false

      for (let s = 1; s <= subSteps; s++) {
        const t = s / subSteps
        const cx = p.pos.x + p.dir.x * moveDist * t
        const cy = p.pos.y + p.dir.y * moveDist * t

        const curBlocks = cc.blocks ?? []
        const hitBlock = curBlocks.find(b => rectCollides(cx, cy, b))
        if (hitBlock) {
          if (p.ammoType === 'bouncing_ammo') {
            const dl = cx - hitBlock.x
            const dr = (hitBlock.x + hitBlock.w) - cx
            const dt = cy - hitBlock.y
            const db = (hitBlock.y + hitBlock.h) - cy
            if (Math.min(dl, dr) < Math.min(dt, db)) {
              p.dir.x = -p.dir.x
            } else {
              p.dir.y = -p.dir.y
            }
            p.pos.x = cx - p.dir.x * moveDist * (1 - t)
            p.pos.y = cy - p.dir.y * moveDist * (1 - t)
            p.distTraveled = 0
            p.dist -= moveDist * t
            break
          }
          applyProjectileHit(p, null)
          hit = true; break
        }

        for (const cid of Object.keys(cc.characters)) {
          if (cid === p.charId) continue
          const e = cc.characters[cid]
          if (!e || isDead(e.integrity)) continue
          const ePos = characterPositionsRef.current[cid] ?? e.pos
          const r = cfg.hitRadius + 4 * (e.size ?? 1)
          if (Math.sqrt((cx - ePos.x) ** 2 + (cy - ePos.y) ** 2) < r) {
            applyProjectileHit(p, cid)
            hit = true; break
          }
        }
        if (hit) break
      }
      if (hit) continue

      if (moveDist < stepDist) continue

      p.distTraveled += moveDist
      p.pos.x += p.dir.x * moveDist
      p.pos.y += p.dir.y * moveDist
      out.push(p)
    }
    return out
  }

  /**
   * Factory: create a weapon animation based on the skill's damage type.
   * slash → arcing motion, pierce → straight thrust, blunt → straight strike.
   */
  function createWeaponStrikeAnim(action, cc) {
    if (!cc) return null
    const isPlayerSide = action.actor === 'player' || action.actor === 'party'
    const attackerId = action.charId
    const targetId = action.targetCharId ?? (isPlayerSide ? action.enemyCharId : (cc.partyIds?.[0] ?? cc.playerEntityId))
    const attacker = cc.characters[attackerId]
    const target = cc.characters[targetId]
    const startPos = attacker ? { ...attacker.pos } : { x: 20, y: 75 }
    const endPos = target ? { ...target.pos } : { x: 20, y: 75 }

    const weapon = weaponMap[action.weaponId]
    const skill = weapon?.skills?.find(s => s.name === action.skillName)
    const damageType = skill?.damageType ?? 'blunt'
    const isFist = action.weaponId === 'fists' || action.weaponId === 'left_arm' || action.weaponId === 'right_arm'
    const type = isFist
      ? (action.weaponId === 'right_arm' ? ANIM_TYPES.FIST_HOOK : ANIM_TYPES.FIST_STRIKE)
      : damageType === 'slash' ? ANIM_TYPES.SLASH
      : ANIM_TYPES.PIERCE_STRIKE

    return new AnimAction({
      id: `${action.actor}-${targetId}-${Date.now()}`,
      type,
      actor: action.actor,
      charId: action.charId,
      startPos,
      endPos,
      elapsed: 0,
      hitFired: false,
      targetPart: action.targetPart,
      weaponId: action.weaponId,
      skillName: action.skillName,
      dmg: action.dmg,
      severity: action.severity,
      injType: action.injType,
      injLabel: action.injLabel,
      attackerParts: action.attackerParts,
      attackerInjuries: action.attackerInjuries,
      targetParts: action.targetParts,
      targetInjuries: action.targetInjuries,
      enemyCharId: action.enemyCharId,
      targetCharId: action.targetCharId,
    })
  }

  /**
   * Slash: arc toward the target (perpendicular offset).
   */
  function getSlashPos(anim) {
    const { hitAt, duration } = ANIM_CFG[ANIM_TYPES.SLASH]
    const t = anim.elapsed < hitAt
      ? anim.elapsed / hitAt
      : anim.elapsed < duration
        ? 1 - (anim.elapsed - hitAt) / (duration - hitAt)
        : 0
    const dx = anim.endPos.x - anim.startPos.x
    const dy = anim.endPos.y - anim.startPos.y
    const dist = Math.sqrt(dx * dx + dy * dy) || 1
    const nx = -dy / dist
    const ny = dx / dist
    const arcHeight = Math.sin(t * Math.PI) * 8
    return {
      x: anim.startPos.x + dx * t + nx * arcHeight,
      y: anim.startPos.y + dy * t + ny * arcHeight,
    }
  }

  /**
   * Pierce / blunt: straight thrust toward the target.
   */
  function getPierceStrikePos(anim) {
    const { hitAt, duration } = ANIM_CFG[ANIM_TYPES.PIERCE_STRIKE]
    const t = anim.elapsed < hitAt
      ? anim.elapsed / hitAt
      : anim.elapsed < duration
        ? 1 - (anim.elapsed - hitAt) / (duration - hitAt)
        : 0
    return {
      x: anim.startPos.x + (anim.endPos.x - anim.startPos.x) * t,
      y: anim.startPos.y + (anim.endPos.y - anim.startPos.y) * t,
    }
  }

  /**
   * Fist jab: wind back slightly, snap forward to target, return.
   */
  function getFistStrikePos(anim) {
    const { hitAt, duration } = ANIM_CFG[ANIM_TYPES.FIST_STRIKE]
    const t = Math.min(anim.elapsed / duration, 1)

    let progress
    if (anim.elapsed < hitAt) {
      const p = anim.elapsed / hitAt
      progress = -0.15 + 1.15 * p
    } else {
      const p = (anim.elapsed - hitAt) / (duration - hitAt)
      progress = 1 - p * p
    }

    const dx = anim.endPos.x - anim.startPos.x
    const dy = anim.endPos.y - anim.startPos.y

    return {
      x: anim.startPos.x + dx * progress,
      y: anim.startPos.y + dy * progress,
    }
  }

  /**
   * Fist hook: wide curved arc — sweeps out to the side then into the target.
   */
  function getFistHookPos(anim) {
    const { hitAt, duration } = ANIM_CFG[ANIM_TYPES.FIST_HOOK]
    const t = anim.elapsed < hitAt
      ? anim.elapsed / hitAt
      : anim.elapsed < duration
        ? 1 - (anim.elapsed - hitAt) / (duration - hitAt)
        : 0

    const mid = {
      x: (anim.startPos.x + anim.endPos.x) / 2,
      y: (anim.startPos.y + anim.endPos.y) / 2,
    }
    const dx = anim.endPos.x - anim.startPos.x
    const dy = anim.endPos.y - anim.startPos.y
    const dist = Math.sqrt(dx * dx + dy * dy) || 1
    const nx = -dy / dist
    const ny = dx / dist
    const arcWidth = Math.sin(t * Math.PI) * 12

    return {
      x: anim.startPos.x + dx * t + nx * arcWidth,
      y: anim.startPos.y + dy * t + ny * arcWidth,
    }
  }

  /**
   * Apply damage / log when a projectile hits something.
   */
  function applyProjectileHit(proj, charId) {
    proj.hitFired = true
    if (!charId) {
      setCombat(c => ({ ...c, log: [...c.log, 'The bullet hits a wall!'] }))
      return
    }

    const cc = combatRef.current
    const target = cc?.characters[charId]
    if (!target || isDead(target.integrity)) return

    const skill = weaponMap.pistol?.skills?.[0]
    if (!skill) return

    const shooterChar = cc?.characters[proj.charId]
    const shooterConditions = proj.charId === 'player' ? playerRef.current.weaponConditions : (shooterChar?.weaponConditions ?? {})
    const shooterName = proj.charId === 'player' ? 'You' : (shooterChar?.name ?? 'Someone')

    const { dmg, severity, injType, injLabel } = computeHit({
      weaponId: 'pistol',
      skill,
      weaponConditions: shooterConditions,
      bothMult: 1.0,
    })

    const parts = ['head', 'torso', 'leftArm', 'rightArm', 'leftLeg', 'rightLeg']
    const part = parts[Math.floor(Math.random() * parts.length)]

    setCombat(c => {
      const characters = { ...c.characters }
      const e = { ...characters[charId] }
      const integrity = { ...e.integrity }
      const injuries = { ...(e.injuries ?? { head: [], torso: [], leftArm: [], rightArm: [], leftLeg: [], rightLeg: [] }) }

      integrity[part] = Math.max(0, (integrity[part] ?? 100) - dmg)
      if (integrity[part] > 0 && severity > 0) {
        injuries[part] = [...(injuries[part] ?? []), { type: injType, severity }]
      }
      e.integrity = integrity
      e.injuries = injuries
      characters[charId] = e

      const log = dmg > 0
        ? [`${shooterName} shoots ${e.name} in the ${bodyPartLabels[part] ?? part} for ${dmg} damage! ${injLabel} (severity ${severity}).`]
        : [`${shooterName}'s shot glances off harmlessly.`]
      if (isDead(integrity)) {
        if (part === 'head') log.push(`${shooterName} blows ${e.name}'s head off!`)
        log.push(`${e.name} is defeated!`)
        deadThisTickRef.current.add(charId)
      }
      return { ...c, characters, log: [...c.log, ...log] }
    })
    setHitShake({ actor: 'enemy', charId, start: performance.now() })
  }

  /** Queue a shot toward a map coordinate — queues a shoot action. */
  const queueShot = (tx, ty) => {
    if (!combat) return
    const actorId = currentActor ?? 'player'
    const startPos = characterPositionsRef.current[actorId] ?? combat.characters[actorId]?.pos ?? combat.characters[combat.playerEntityId].pos
    characterActionsRef.current[actorId] = new AnimAction({ type: 'shoot', startPos: { x: startPos.x, y: startPos.y }, endPos: { x: tx, y: ty }, elapsed: 0 })
    setLimbSelect(null); setMoveMode(null); setAimMode(false); setSelectedSkill(null); setCurrentActor(null)
  }

  /**
   * Dispatch: get the current position of any animation type.
   * Returns { x, y } or null if the type is unknown.
   */
  function getAnimPos(anim) {
    switch (anim?.type) {
      case ANIM_TYPES.SLASH: return getSlashPos(anim)
      case ANIM_TYPES.PIERCE_STRIKE: return getPierceStrikePos(anim)
      case ANIM_TYPES.FIST_STRIKE: return getFistStrikePos(anim)
      case ANIM_TYPES.FIST_HOOK: return getFistHookPos(anim)
      default: return null
    }
  }

class AnimAction {
  constructor(data) { Object.assign(this, data); this.elapsed = this.elapsed ?? 0; this.hitFired = this.hitFired ?? false }
  step(dt) {
    const cfg = ANIM_CFG[this.type]
    if (!cfg) return true
    this.elapsed += dt
    return this.elapsed >= cfg.duration
  }
}

class TurnAction {
  constructor(data) { Object.assign(this, data) }
  step(dt) {
    const dir = this.getDir()
    const tgt = this.targetDir
    if (dir.dx === tgt.dx && dir.dy === tgt.dy) return true
    const curAngle = Math.atan2(dir.dy, dir.dx)
    const tgtAngle = Math.atan2(tgt.dy, tgt.dx)
    let delta = tgtAngle - curAngle
    while (delta > Math.PI) delta -= 2 * Math.PI
    while (delta < -Math.PI) delta += 2 * Math.PI
    const maxStep = TURN_SPEED * dt
    if (Math.abs(delta) <= maxStep) {
      this.setDir({ dx: tgt.dx, dy: tgt.dy })
      return true
    }
    const newAngle = curAngle + Math.sign(delta) * maxStep
    this.setDir({ dx: Math.cos(newAngle), dy: Math.sin(newAngle) })
    return false
  }
}

class SeqAction {
  constructor(data) { Object.assign(this, data); this.index = 0 }
  step(dt) {
    while (this.index < this.actions.length) {
      const done = this.actions[this.index].step(dt)
      if (!done) return false
      this.index++
    }
    return true
  }
}

class ParallelAction {
  constructor(data) { Object.assign(this, data) }
  step(dt) {
    let moveDone = false
    for (const a of this.actions) {
      if (a.step(dt) && a instanceof MoveAction) moveDone = true
    }
    return moveDone
  }
}

class MoveAction {
  constructor(data) { Object.assign(this, data); this.cx = this.fromX; this.cy = this.fromY }
  step(dt) {
    const steps = Math.max(1, Math.round(dt * 60)); const blk = this.blocks ?? []
    for (let i = 0; i < steps; i++) {
      const next = computeMove({ x: this.cx, y: this.cy }, { x: this.toX, y: this.toY }, this.speed * dt * 60 / steps, blk)
      if (next.x === this.cx && next.y === this.cy) return true
      this.cx = next.x; this.cy = next.y
      this.setPos(next)
      if (next.x === this.toX && next.y === this.toY) return true
    }
    return false
  }
}

  function applyAnimHit(anim) {
    const liveCc = combatRef.current
    const attackerId = anim.charId
    const isPlayerSide = anim.actor === 'player' || anim.actor === 'party'
    const targetId = anim.targetCharId ?? (isPlayerSide ? anim.enemyCharId : (liveCc?.partyIds?.[0] ?? liveCc?.playerEntityId))

    let inRange = false
    if (liveCc) {
      // Primary: live positions from characterPositionsRef (updated by move actions)
      const attackerPos = characterPositionsRef.current[attackerId] ?? liveCc.characters[attackerId]?.pos
      const targetPos = characterPositionsRef.current[targetId] ?? liveCc.characters[targetId]?.pos
      if (attackerPos && targetPos) {
        inRange = canReach(attackerPos, targetPos, anim.weaponId, anim.skillName)
      }
      // Fallback: rendered combatRef positions
      if (!inRange) {
        const a2 = liveCc.characters[attackerId]?.pos
        const t2 = liveCc.characters[targetId]?.pos
        if (a2 && t2) {
          inRange = canReach(a2, t2, anim.weaponId, anim.skillName)
        }
      }
    }

    if (!inRange) {
      const aPos = liveCc?.characters[attackerId]?.pos
      const tPos = liveCc?.characters[targetId]?.pos
      const aPos2 = characterPositionsRef.current[attackerId]
      const tPos2 = characterPositionsRef.current[targetId]
      const dist = aPos && tPos ? Math.sqrt((aPos.x - tPos.x)**2 + (aPos.y - tPos.y)**2) : -1
      const dist2 = aPos2 && tPos2 ? Math.sqrt((aPos2.x - tPos2.x)**2 + (aPos2.y - tPos2.y)**2) : -1
      const range = getSkillRange(anim.weaponId, anim.skillName)
      const attackerName = liveCc?.characters[anim.charId]?.name ?? (anim.actor === 'player' ? 'You' : 'Enemy')
      const msg = isPlayerSide
        ? `Your attack misses! (distance combatRef=${dist.toFixed(1)} refs=${dist2.toFixed(1)} range=${range})`
        : `${attackerName}'s attack misses!`
      setCombat(prev => ({ ...prev, log: [...prev.log, msg] }))
      return
    }

    setHitShake({ actor: anim.actor, charId: anim.charId, start: performance.now() })

    if (isPlayerSide) {
      const wid = anim.weaponId
      if (wid !== 'fists' && wid !== 'left_arm' && wid !== 'right_arm') {
        const base = weaponMap[wid]
        if (anim.actor === 'player') {
          setPlayer(p => ({ ...p, weaponConditions: { ...p.weaponConditions, [wid]: Math.max(0, (p.weaponConditions[wid] ?? base.maxCondition) - (1 + Math.floor(Math.random() * 2))) } }))
        } else {
          setCombat(prev => {
            const ch = prev.characters[anim.charId]
            if (!ch) return prev
            return { ...prev, characters: { ...prev.characters, [anim.charId]: { ...ch, weaponConditions: { ...ch.weaponConditions, [wid]: Math.max(0, (ch.weaponConditions[wid] ?? base.maxCondition) - (1 + Math.floor(Math.random() * 2))) } } } }
          })
        }
      }

      if (anim.actor === 'player') {
        const decayedPlayer = calcIntegrity(playerRef.current.integrity, playerRef.current.injuries, ANIM_CFG[anim.type].hitAt)
        setPlayer(p => ({ ...p, integrity: decayedPlayer }))
        setPlayer(p => ({ ...p, injuries: playerRef.current.injuries }))
      } else {
        setCombat(prev => {
          const ch = prev.characters[anim.charId]
          if (!ch) return prev
          const decayed = calcIntegrity(ch.integrity, ch.injuries, ANIM_CFG[anim.type].hitAt)
          return { ...prev, characters: { ...prev.characters, [anim.charId]: { ...ch, integrity: decayed } } }
        })
      }

      if (liveCc && anim.enemyCharId) {
        const enemy = liveCc.characters[anim.enemyCharId]
        if (enemy) {
          const decayedEnemy  = calcIntegrity(enemy.integrity, enemy.injuries, ANIM_CFG[anim.type].hitAt)
          const prevHp        = decayedEnemy[anim.targetPart]
          const hitEnemy      = { ...decayedEnemy, [anim.targetPart]: Math.max(0, prevHp - anim.dmg) }
          const newInjuries   = prevHp > 0 && anim.severity > 0
            ? applyInjury(enemy.injuries, anim.targetPart, anim.injType, anim.severity)
            : enemy.injuries
          const attackerName = liveCc.characters[anim.charId]?.name ?? 'You'

          const log = anim.dmg > 0
            ? [`${attackerName} ${anim.skillName?.toLowerCase() ?? 'attack'}s ${enemy.name}'s ${bodyPartLabels[anim.targetPart]} — ${anim.injLabel} (severity ${anim.severity}, -${anim.dmg} integrity).`]
            : []
          if (isDead(hitEnemy)) {
            if (anim.targetPart === 'head') log.push(`${attackerName} crushes ${enemy.name}'s head!`)
            log.push(`${enemy.name} is defeated!`)
            deadThisTickRef.current.add(anim.enemyCharId)
          }

          setCombat(prev => ({
            ...prev,
            characters: { ...prev.characters, [anim.enemyCharId]: { ...enemy, integrity: hitEnemy, injuries: newInjuries } },
            log: [...prev.log, ...log],
          }))
        }
      }
    } else {
      const currentEnemy  = liveCc?.characters[anim.charId]
      const decayedEnemy  = currentEnemy
        ? calcIntegrity(currentEnemy.integrity, currentEnemy.injuries, ANIM_CFG[anim.type].hitAt)
        : calcIntegrity(anim.attackerParts, anim.attackerInjuries, ANIM_CFG[anim.type].hitAt)

      // Find target party member
      const targetCharId = anim.targetCharId ?? (combat?.partyIds?.[0] ?? 'player')
      const targetChar = liveCc?.characters[targetCharId]
      const targetIntegrity = targetChar?.integrity ?? playerRef.current.integrity
      const targetInjuries = targetChar?.injuries ?? playerRef.current.injuries

      const decayedTarget = calcIntegrity(targetIntegrity, targetInjuries, ANIM_CFG[anim.type].hitAt)
      const prevHp        = decayedTarget[anim.targetPart]
      const hitTarget     = { ...decayedTarget, [anim.targetPart]: Math.max(0, prevHp - anim.dmg) }
      const newInjuries   = prevHp > 0 && anim.severity > 0
        ? applyInjury(targetInjuries, anim.targetPart, anim.injType, anim.severity)
        : targetInjuries

      if (liveCc) {
        const enemy = liveCc.characters[anim.charId]
        const enemyName = enemy?.name ?? 'Enemy'

        setCombat(prev => ({
          ...prev,
          characters: {
            ...prev.characters,
            [anim.charId]: { ...enemy, integrity: decayedEnemy },
            [targetCharId]: { ...prev.characters[targetCharId], integrity: hitTarget, injuries: newInjuries },
          },
          log: anim.dmg > 0
            ? [...prev.log, `${enemyName} lands a ${anim.injLabel} on ${targetChar?.name ?? 'your'} ${bodyPartLabels[anim.targetPart]} (severity ${anim.severity}, integrity -${anim.dmg}).`]
            : prev.log,
        }))
      }

      if (isDead(hitTarget)) {
        deadThisTickRef.current.add(targetCharId)
      }
      if (targetCharId === 'player') {
        setPlayer(p => ({ ...p, integrity: hitTarget, injuries: newInjuries }))
      }
    }
  }

  // ─────────────────────────────────────────────────────────────────────────
  // TIMER — ticks while timerActive is true, updates all character actions
  // ─────────────────────────────────────────────────────────────────────────

  useEffect(() => {
    if (!combat) { setDisplayTime(0); return }
    let lastTime = performance.now()
    let rafId

    const tick = () => {
      rafId = requestAnimationFrame(tick)
      const now = performance.now()

      // ── Replay mode ──
      if (replayActive) {
        const rawDt = (now - lastTime) / 1000
        const dt    = rawDt * speedRef.current
        lastTime    = now
        const rec = fightRecord
        if (rec) {
          replayElapsedRef.current += dt
          const lastFrame = rec.frames[rec.frames.length - 1]
          const idx = rec.frames.findLastIndex(f => f.time <= replayElapsedRef.current)
          if (idx >= 0) {
            const f = rec.frames[idx]
            setCombat(f.combat)
            setPlayer(p => ({ ...p, integrity: f.playerIntegrity, injuries: f.playerInjuries, equipped: f.playerEquipped ?? p.equipped, weaponConditions: f.playerWeaponConditions ?? p.weaponConditions }))
            if (f.playerDir) characterDirsRef.current['player'] = f.playerDir
            setAnimations(Array.isArray(f.animations) ? f.animations : [])
            setProjectiles(Array.isArray(f.projectiles) ? f.projectiles : [])
            setHitShake(f.hitShake ?? null)
          }
          setCombatTime(replayElapsedRef.current)
          setDisplayTime(replayElapsedRef.current)
          if (replayElapsedRef.current >= lastFrame.time) setReplayActive(false)
        }
        return
      }

      // ── Effects (run every frame, independent of timer) ──
      setHitShake(prev => {
        if (!prev) return prev
        return (performance.now() - prev.start) / 1000 >= 0.3 ? null : prev
      })

      // ── Auto timer on busy state transitions; manual toggle via spacebar ──
      {
        const cc = combatRef.current
        const partyIds = cc?.partyIds ?? [cc?.playerEntityId].filter(Boolean)
        const allBusy = partyIds.length > 0 && partyIds.every(cid => {
          const ch = cc.characters[cid]
          return isDead(ch?.integrity) || characterActionsRef.current[cid] != null
        })
        const wasBusy = prevBusyRef.current
        prevBusyRef.current = allBusy
        setBusy(allBusy)

        if (allBusy && !wasBusy && !timerActiveRef.current && partyIds.some(cid => !isDead(cc.characters[cid]?.integrity))) {
          timerActiveRef.current = true
          setTimerRunning(true)
        }
        if (!allBusy && wasBusy && timerActiveRef.current) {
          timerActiveRef.current = false
          setTimerRunning(false)
        }
      }

      // ── Stop timer if all party members die ──
      {
        const cc = combatRef.current
        if (cc && timerActiveRef.current) {
          const partyIds = cc.partyIds ?? [cc.playerEntityId].filter(Boolean)
          if (partyIds.every(cid => isDead(cc.characters[cid]?.integrity))) {
            stopTimer()
          }
        }
      }

      // dt is zero when the timer is off — animations don't play
      if (!timerActiveRef.current) {
        lastTime = now
        setAnimations([])
        return
      }

      const rawDt = (now - lastTime) / 1000
      const dt    = rawDt * speedRef.current
      lastTime    = now

      deadThisTickRef.current = new Set()

      setCombatTime(t => t + dt)
      setDisplayTime(t => t + dt)
      setGameTime(gt => {
        let minute = (gt.minute ?? 0) + dt / 60
        let extraH = Math.floor(minute / 60)
        minute = minute % 60
        let h = gt.hour + extraH
        let d = gt.day
        if (h >= 24) { d += Math.floor(h / 24); h = h % 24 }
        return { day: d, hour: h, minute }
      })

      // ── Build fresh combat snapshot with live positions ──
      const activeAnims = []
      const cc = combatRef.current
      const playerCharId = cc?.playerEntityId
      const fresh = cc ? (() => {
        const chars = {}
        for (const cid of Object.keys(cc.characters)) {
          const c = cc.characters[cid]
          const pos = characterPositionsRef.current[cid] ?? c.pos
          chars[cid] = { ...c, pos: { ...pos } }
        }
        return { ...cc, characters: chars }
      })() : null

      // ── Tick ALL character actions uniformly ──
      if (cc) {
        for (const cid of Object.keys(cc.characters)) {
          // Skip dead characters — clear their action and move on
          if (isDead(cc.characters[cid]?.integrity) || deadThisTickRef.current.has(cid)) {
            if (characterActionsRef.current[cid]) {
              characterActionsRef.current[cid] = null
            }
            continue
          }
          const rawAnim = characterActionsRef.current[cid]
          if (!rawAnim) continue

          // Sync move action start position from live ref
          if (rawAnim.type === 'move') {
            const cp = characterPositionsRef.current[cid]
            if (cp) { rawAnim.cx = cp.x; rawAnim.cy = cp.y }
          }

          const done = rawAnim.step(dt)
          if (ANIM_CFG[rawAnim.type] && rawAnim.elapsed >= ANIM_CFG[rawAnim.type].hitAt && !rawAnim.hitFired) {
            rawAnim.hitFired = true
              if (rawAnim.type === 'shoot') {
                const pp = characterPositionsRef.current[cid] ?? cc.characters[cid]?.pos
                const dir = characterDirsRef.current[cid] ?? { dx: 0, dy: 1 }
                const offset = 5
                const spawnPos = { x: pp.x + dir.dx * offset, y: pp.y + dir.dy * offset }
                const actor = cid === 'player' ? 'player' : 'party'
                const charMag = cid === 'player' ? magazineRef.current : (cc.characters[cid]?.weaponMagazines ?? {})
                if (charMag.pistol?.length > 0) {
                  const bullet = charMag.pistol[0]
                  const newMag = { pistol: charMag.pistol.slice(1) }
                  if (cid === 'player') {
                    magazineRef.current = newMag
                    setPlayer(p => ({ ...p, weaponMagazines: { ...p.weaponMagazines, ...newMag } }))
                  } else {
                    setCombat(prev => ({
                      ...prev,
                      characters: {
                        ...prev.characters,
                        [cid]: { ...prev.characters[cid], weaponMagazines: { ...prev.characters[cid].weaponMagazines, ...newMag } },
                      },
                    }))
                  }
                  const ddx = rawAnim.endPos.x - spawnPos.x
                  const ddy = rawAnim.endPos.y - spawnPos.y
                  const d = Math.sqrt(ddx * ddx + ddy * ddy) || 1
                  const dir = { x: ddx / d, y: ddy / d }
                  const facing = characterDirsRef.current[cid] ?? { dx: 0, dy: 1 }
                  const dot = Math.max(-1, Math.min(1, facing.dx * dir.x + facing.dy * dir.y))
                  const maxDeviation = (1 - dot) / 2 * (Math.PI / 4)
                  const deviation = (Math.random() * 2 - 1) * maxDeviation
                  const cos = Math.cos(deviation), sin = Math.sin(deviation)
                  const spreadDir = { x: dir.x * cos - dir.y * sin, y: dir.x * sin + dir.y * cos }
                  const finalDir = { x: isNaN(spreadDir.x) ? dir.x : spreadDir.x, y: isNaN(spreadDir.y) ? dir.y : spreadDir.y }
                  const ammoType = bullet.ammoType
                  setProjectiles(prev => [...prev, createProjectile(spawnPos, { x: spawnPos.x + finalDir.x * d, y: spawnPos.y + finalDir.y * d }, actor, 'pistol', 'Shoot', ammoType, cid)])
                  new Audio(pistolShotMp3).play().catch(() => {})
                } else if (cid === 'player') {
                  setCombat(c => ({ ...c, log: [...c.log, 'Click! The pistol is empty.'] }))
                } else {
                  setCombat(c => ({ ...c, log: [...c.log, `${cc.characters[cid]?.name ?? 'Party member'} clicks — the pistol is empty.`] }))
                }
            } else {
              applyAnimHit(rawAnim)
            }
          }
          if (done) {
            // ── Enemy move → attack transition ──
            // Enemy move → attack transition (skip party members)
              if (rawAnim.type === 'move' && cid !== playerCharId && !(cc.partyIds ?? []).includes(cid)) {
                const e = fresh?.characters[cid] ?? cc.characters[cid]
                const moveSub = rawAnim.actions?.find(a => a instanceof MoveAction)
                const finalPos = moveSub ? { x: moveSub.toX, y: moveSub.toY } : { x: rawAnim.toX, y: rawAnim.toY }
                if (e && !isDead(e.integrity)) {
                  const partyIds = cc.partyIds ?? [cc.playerEntityId].filter(Boolean)
                  let targetId = null, minDist = Infinity
                  for (const pid of partyIds) {
                    const pch = (fresh ?? cc).characters[pid]
                    if (!pch || isDead(pch.integrity) || deadThisTickRef.current.has(pid)) continue
                    const d = Math.sqrt((pch.pos.x - finalPos.x) ** 2 + (pch.pos.y - finalPos.y) ** 2)
                    if (d < minDist) { minDist = d; targetId = pid }
                  }
                if (targetId) {
                  const targetChar = (fresh ?? cc).characters[targetId]
                  const wpn = e.weapon ?? 'fists'
                  const base = weaponMap[wpn]
                  const skills = base?.skills ?? [{ name: 'Attack', mult: 1.0, damageType: 'blunt', range: 15 }]
                  const maxRange = Math.max(...skills.map(s => s.range ?? 15))
                  if (minDist <= maxRange) {
                    const skill = skills[Math.floor(Math.random() * skills.length)]
                    const { dmg, severity, injType, injLabel } = computeHit({ weaponId: wpn, skill, weaponConditions: {} })
                    const targetPart = randomPart()
                    const action = {
                      type: 'attack', actor: 'enemy', charId: cid,
                      targetCharId: targetId,
                      targetPart,
                      weaponId: wpn, skillName: skill.name,
                      dmg, severity, injType, injLabel,
                      attackerParts: { ...e.integrity },
                      attackerInjuries: { ...e.injuries },
                      targetParts: { ...targetChar.integrity },
                      targetInjuries: { ...targetChar.injuries },
                    }
                    characterActionsRef.current[cid] = createWeaponStrikeAnim(action, fresh ?? cc)
                    continue
                  }
                }
              }
            }
            characterActionsRef.current[cid] = null
          } else {
            if (ANIM_CFG[rawAnim.type]) {
              activeAnims.push(rawAnim)
            }
          }
        }
      }

      // ── Run enemy AI every frame for idle enemies ──
      if (fresh) {
        const actions = runEnemyAI(fresh, playerRef.current.integrity, playerRef.current.injuries, deadThisTickRef.current)
        for (const act of actions) {
          if (act.type === 'attack' && !characterActionsRef.current[act.charId]) {
            characterActionsRef.current[act.charId] = createWeaponStrikeAnim(act, fresh)
          } else if (act.type === 'move' && !characterActionsRef.current[act.charId]) {
            const dx = act.toX - act.fromX, dy = act.toY - act.fromY
            const d = Math.sqrt(dx * dx + dy * dy)
            const dir = d > 0 ? { dx: dx / d, dy: dy / d } : { dx: 0, dy: 1 }
            characterActionsRef.current[act.charId] = new ParallelAction({
              type: 'move',
              actions: [
                new TurnAction({
                  targetDir: dir,
                  getDir: () => characterDirsRef.current[act.charId] ?? { dx: 0, dy: 1 },
                  setDir: d2 => { characterDirsRef.current[act.charId] = d2 }
                }),
                new MoveAction({
                  ...act,
                  blocks: fresh.blocks,
                  setPos: p => {
                    setCombat(c => ({
                      ...c,
                      characters: { ...c.characters, [act.charId]: { ...c.characters[act.charId], pos: { ...p } } }
                    }))
                    characterPositionsRef.current[act.charId] = p
                  }
                })
              ]
            })
          }
        }
      }

      // ── Step projectiles ──
      setProjectiles(prev => stepProjectiles(prev, dt, combatRef.current))

      // ── Update rendering state for active animations ──
      setAnimations(activeAnims)

    }

    rafId = requestAnimationFrame(tick)
    return () => cancelAnimationFrame(rafId)
  }, [combat, replayActive])

  // ─────────────────────────────────────────────────────────────────────────
  // TRAVEL ANIMATION (world map)
  // ─────────────────────────────────────────────────────────────────────────

  useEffect(() => {
    if (!travelAnim) return
    if (travelAnim.startTs === null) { setTravelAnim(a => ({ ...a, startTs: performance.now() })); return }
    const frame = requestAnimationFrame(() => {
      const progress = Math.min(1, (performance.now() - travelAnim.startTs) / 1000)
      setTravelAnim(a => ({ ...a, progress }))
      if (progress >= 1) { setPlayer(p => ({ ...p, location: travelAnim.toId })); setTravelAnim(null); enterLocation(travelAnim.toId) }
    })
    return () => cancelAnimationFrame(frame)
  }, [travelAnim])

  // ─────────────────────────────────────────────────────────────────────────
  // COMBAT OVER
  // ─────────────────────────────────────────────────────────────────────────

  useEffect(() => {
    if (!combat || replayActive) return
    const enemyIds = combat.enemyIds ?? []
    if (enemyIds.length === 0) return
    const partyIds = combat.partyIds ?? [combat.playerEntityId].filter(Boolean)
    const allPartyDead = partyIds.every(cid => isDead(combat.characters[cid]?.integrity))
    const allEnemiesDead = !enemyIds.some(eid => !isDead(combat.characters[eid].integrity))
    if (allPartyDead || allEnemiesDead) endFight()
  }, [combat, player.integrity, playerHp])

  // ─────────────────────────────────────────────────────────────────────────
  // KEYBINDS
  // ─────────────────────────────────────────────────────────────────────────

  // ─────────────────────────────────────────────────────────────────────────
  // SNAPSHOT (recording)
  // ─────────────────────────────────────────────────────────────────────────

  const pushSnapshot = () => {
    if (!recordingRef.current || !combat) return
    recordingRef.current.frames.push({
      time: combatTime,
      combat: JSON.parse(JSON.stringify(combat)),
      playerIntegrity: JSON.parse(JSON.stringify(player.integrity)),
      playerInjuries: JSON.parse(JSON.stringify(player.injuries)),
      playerEquipped: { ...player.equipped },
      playerWeaponConditions: { ...player.weaponConditions },
      playerDir: { ...(characterDirsRef.current['player'] ?? { dx: 0, dy: 1 }) },
      animations: JSON.parse(JSON.stringify(animations)),
      projectiles: JSON.parse(JSON.stringify(projectiles)),
      hitShake:   hitShake   ? { ...hitShake }   : null,
    })
  }

  useEffect(() => {
    if (!recordingRef.current || !combat || replayActive || !timerActiveRef.current) return
    const prev = recordingRef.current.frames[recordingRef.current.frames.length - 1]
    if (JSON.stringify(prev.combat) !== JSON.stringify(combat) || JSON.stringify(prev.playerIntegrity) !== JSON.stringify(player.integrity)) {
      pushSnapshot()
    }
  }, [combat, player.integrity, player.injuries])

  // ─────────────────────────────────────────────────────────────────────────
  // WORLD CLOCK (outside combat)
  // ─────────────────────────────────────────────────────────────────────────

  useEffect(() => {
    if (combat) return
    const id = setInterval(() => setGameTime(gt => { let h = gt.hour + 1, d = gt.day; if (h >= 24) { h = 0; d++ } return { day: d, hour: h, minute: 0 } }), 5000)
    return () => clearInterval(id)
  }, [combat])

  useEffect(() => {
    if (combat) return
    const id = setInterval(() => {
      const types = ['clear', 'clear', 'cloudy', 'cloudy', 'rain', 'storm']
      setWeather(types[Math.floor(Math.random() * types.length)])
    }, 30000)
    return () => clearInterval(id)
  }, [combat])

  // ─────────────────────────────────────────────────────────────────────────
  // SKIP TIME ANIMATION
  // ─────────────────────────────────────────────────────────────────────────

  useEffect(() => {
    if (!skipAnim) return
    const { current: start, target, start: startTime } = skipAnim
    const duration = (target - start) * 2000
    const frame = requestAnimationFrame(function tick() {
      const elapsed = performance.now() - startTime
      const t = Math.min(elapsed / duration, 1)
      setSkipAnim(s => ({ ...s, current: start + (target - start) * t }))
      if (t >= 1) setSkipAnim(null)
      else requestAnimationFrame(tick)
    })
    return () => cancelAnimationFrame(frame)
  }, [skipAnim])

  // ─────────────────────────────────────────────────────────────────────────
  // ZOOM ANIMATION
  // ─────────────────────────────────────────────────────────────────────────

  useEffect(() => {
    if (!zoomAnim) return
    const frame = requestAnimationFrame(function tick() {
      const t    = Math.min((performance.now() - zoomAnim.start) / 300, 1)
      const ease = 1 - Math.pow(1 - t, 3)
      setView({
        x:     zoomAnim.from.x     + (zoomAnim.to.x     - zoomAnim.from.x)     * ease,
        y:     zoomAnim.from.y     + (zoomAnim.to.y     - zoomAnim.from.y)     * ease,
        scale: zoomAnim.from.scale + (zoomAnim.to.scale - zoomAnim.from.scale) * ease,
      })
      if (t >= 1) setZoomAnim(null)
      else requestAnimationFrame(tick)
    })
    return () => cancelAnimationFrame(frame)
  }, [zoomAnim])

  // ─────────────────────────────────────────────────────────────────────────
  // REPLAY
  // ─────────────────────────────────────────────────────────────────────────

  const startReplay = () => {
    setReplayActive(true)
    const rec = fightRecord
    const f0  = rec.frames[0]
    setCombat(f0.combat); setPlayer(p => ({ ...p, integrity: f0.playerIntegrity, injuries: f0.playerInjuries, equipped: f0.playerEquipped ?? p.equipped, weaponConditions: f0.playerWeaponConditions ?? p.weaponConditions }))
    characterDirsRef.current['player'] = f0.playerDir ?? { dx: 0, dy: 1 }
    setAnimations(Array.isArray(f0.animations) ? f0.animations : []); setProjectiles(Array.isArray(f0.projectiles) ? f0.projectiles : []); setHitShake(f0.hitShake ?? null)
    setCombatTime(0); setDisplayTime(0); characterActionsRef.current = {}
  }

  // ─────────────────────────────────────────────────────────────────────────
  // COMBAT ACTIONS
  // ─────────────────────────────────────────────────────────────────────────

  const makeEnemy = (npc, index) => {
    return createCharacter({
      ...npc,
      id: npc.id ?? `enemy_${index}`,
      pos: { x: 120 + index * 20, y: 60 + index * 20 },
    })
  }

  const startFight = (npcList) => {
    const list = npcList ?? []
    if (list.length === 0) return
    const enemies = list.map((npc, i) => makeEnemy(npc, i))
    const characters = {}

    // Find party members (player + allies at same location)
    const partyCharIds = ['player']
    const livePartyChars = liveCharacters.filter(c => c.id !== 'player' && c.location === player.location && !isDead(c.integrity) && c.equipped)
    for (const pc of livePartyChars) {
      partyCharIds.push(pc.id)
    }

    characters['player'] = createCharacter({
      id: 'player', name: player.name,
      pos: { x: 20, y: 75 },
      integrity: { ...player.integrity },
      injuries: JSON.parse(JSON.stringify(player.injuries)),
      equipped: { ...player.equipped },
      weaponConditions: { ...player.weaponConditions },
      weaponMagazines: { ...player.weaponMagazines },
      itemCounts: { ...player.itemCounts },
    })
    for (const pc of livePartyChars) {
      characters[pc.id] = createCharacter({
        id: pc.id, name: pc.name,
        pos: { x: 30 + partyCharIds.indexOf(pc.id) * 20, y: 80 },
        integrity: freshBody(),
        injuries: freshInjuries(),
        equipped: { ...(pc.equipped ?? { left: 'fists', right: 'fists' }) },
        weaponConditions: { ...(pc.weaponConditions ?? {}) },
        weaponMagazines: { ...(pc.weaponMagazines ?? {}) },
        itemCounts: { ...(pc.itemCounts ?? {}) },
        inventory: [...(pc.inventory ?? [])],
        size: pc.size ?? 1,
          fillColor: pc.fillColor,
      })
    }
    for (const e of enemies) {
      const cid = e.id
      characters[cid] = { ...e, pos: { ...e.pos } }
    }
    const enemyIds = enemies.map(e => e.id)
    const initCombat = {
      playerEntityId: 'player',
      partyIds: partyCharIds,
      characters,
      enemyIds,
      log: [`You engage ${enemies.map(e => e.name).join(' and ')} in combat!`],
      blocks: generateLocationBlocks(player.location),
    }
    setCombat(initCombat)
    stopTimer(); setBusy(false); prevBusyRef.current = false
    setCombatTime(0); setDisplayTime(0)
    characterActionsRef.current = {}
    characterPositionsRef.current = {}
    characterDirsRef.current = {}
    for (const cid of Object.keys(characters)) {
      characterActionsRef.current[cid] = null
      characterPositionsRef.current[cid] = { ...characters[cid].pos }
      characterDirsRef.current[cid] = { dx: 0, dy: 1 }
    }
    setSelectedEnemy(0); setReplayActive(false); setCurrentActor(null)
    setExpandedWeapon(['player']); setAnimations([]); setHitShake(null); setMoveMode(null)
    recordingRef.current = {
      initialCombat: JSON.parse(JSON.stringify(initCombat)),
      frames: [{ time: 0, combat: JSON.parse(JSON.stringify(initCombat)),
        playerIntegrity: freshBody(), playerInjuries: freshInjuries(),
        playerEquipped: player.equipped, playerWeaponConditions: player.weaponConditions,
        playerDir: { dx: 0, dy: 1 }, animations: [], projectiles: [], hitShake: null }],
    }
  }

  /** Enter a location — creates a combat view with all NPCs present as non-hostile */
  const enterLocation = (locationId) => {
    const characters = {}

    characters['player'] = createCharacter({
      id: 'player', name: player.name,
      pos: { x: 20, y: 75 },
      integrity: { ...player.integrity },
      injuries: JSON.parse(JSON.stringify(player.injuries)),
      equipped: { ...player.equipped },
      weaponConditions: { ...player.weaponConditions },
      weaponMagazines: { ...player.weaponMagazines },
      itemCounts: { ...player.itemCounts },
    })

    const partyCharIds = ['player']
    const npcIds = []
    const liveChars = liveCharacters.filter(c => c.id !== 'player' && c.location === locationId && (!c.integrity || !isDead(c.integrity)))
    let xOff = 50
    for (const lc of liveChars) {
      if (lc.equipped) {
        partyCharIds.push(lc.id)
        characters[lc.id] = createCharacter({
          id: lc.id, name: lc.name,
          pos: { x: xOff, y: 130 },
          integrity: { ...lc.integrity },
          injuries: JSON.parse(JSON.stringify(lc.injuries ?? player.injuries)),
          equipped: { ...lc.equipped },
          weaponConditions: { ...lc.weaponConditions },
          weaponMagazines: { ...lc.weaponMagazines },
          itemCounts: { ...lc.itemCounts },
          inventory: [...(lc.inventory ?? [])],
          fillColor: lc.fillColor,
        })
      } else {
        npcIds.push(lc.id)
        characters[lc.id] = createCharacter({
          id: lc.id, name: lc.name,
          pos: { x: xOff, y: 130 },
          integrity: freshBody(),
          injuries: freshInjuries(),
          equipped: { left: 'fists', right: 'fists' },
          weaponConditions: {},
          weaponMagazines: {},
          itemCounts: {},
          inventory: [...(lc.inventory ?? [])],
          fillColor: lc.fillColor,
        })
      }
      xOff += 25
    }

    const locName = locMap[locationId]?.name ?? locationId
    const initCombat = {
      playerEntityId: 'player',
      partyIds: partyCharIds,
      npcIds,
      characters,
      enemyIds: [],
      log: [`You arrive at ${locName}.`],
      blocks: generateLocationBlocks(locationId),
    }
    setCombat(initCombat)
    setCombatTime(0); setDisplayTime(0)
    characterActionsRef.current = {}
    characterPositionsRef.current = {}
    for (const cid of Object.keys(characters)) {
      characterPositionsRef.current[cid] = { ...characters[cid].pos }
    }
    recordingRef.current = null; setFightRecord(null); setProjectiles([]); setHitShake(null)
    speedRef.current = 1; setSpeedMult(1)
    setAimMode(false); setLimbSelect(null); setSelectedSkill(null); setSelectedTarget(null); setAttackEnemy(null); setCurrentActor(null)
  }

  const endFight = () => {
    if (recordingRef.current) { pushSnapshot(); setFightRecord(recordingRef.current); recordingRef.current = null }
    stopTimer()
    setCombatTime(0); setDisplayTime(0)
    characterActionsRef.current = {}
    setProjectiles([])
    if (combat) {
      const deadIds = combat.enemyIds.map(id => combat.characters[id]).filter(e => isDead(e.integrity)).map(e => e.id)
      if (deadIds.length > 0) setDefeatedNpcs(prev => [...new Set([...prev, ...deadIds])])

      // Sync party member combat state back to world
      const cc = combatRef.current
      if (cc) {
        setCharacterOverrides(prev => {
          const next = { ...prev }
          for (const pid of cc.partyIds ?? []) {
            if (pid === 'player') continue
            const combatChar = cc.characters[pid]
            if (combatChar) {
              next[pid] = {
                weaponConditions: { ...combatChar.weaponConditions },
                itemCounts: { ...combatChar.itemCounts },
                weaponMagazines: { ...combatChar.weaponMagazines },
                inventory: [...(combatChar.inventory ?? [])],
                integrity: { ...combatChar.integrity },
                injuries: JSON.parse(JSON.stringify(combatChar.injuries)),
              }
            }
          }
          return next
        })
        for (const pid of cc.partyIds ?? []) {
          if (pid === 'player') continue
          if (isDead(cc.characters[pid]?.integrity)) {
            setDefeatedNpcs(prev => [...new Set([...prev, pid])])
          }
        }
      }
    }
  }

  /** Execute a player attack; queue it for execution. */
  const executeAttack = () => {
    if (!limbSelect || !selectedTarget) {
      setCombat(c => ({ ...c, log: [...c.log, `DBG: early return limbSelect=${limbSelect} target=${selectedTarget}`] }))
      return
    }
    const actorId = currentActor ?? 'player'
    const cc = combatRef.current
    if (!cc) { setCombat(c => ({ ...c, log: [...c.log, 'DBG: no combat'] })); return }
    const actorChar = cc.characters[actorId]
    if (!actorChar || isDead(actorChar.integrity)) { setCombat(c => ({ ...c, log: [...c.log, 'DBG: actor dead'] })); return }
    const ei     = attackEnemy ?? selectedEnemy
    const enemyId = cc.enemyIds[ei]
    const enemy  = cc.characters[enemyId]
    if (!enemy || isDead(enemy.integrity)) {
      setCombat(c => ({ ...c, log: [...c.log, `DBG: bad enemy ei=${ei} id=${enemyId} enemy=${!!enemy} dead=${enemy ? isDead(enemy.integrity) : '?'}`] }))
      return
    }

    const weaponId = limbSelect
    const actorPos = characterPositionsRef.current[actorId] ?? actorChar.pos
    if (!canReach(actorPos, enemy.pos, weaponId, selectedSkill)) {
      const d = Math.sqrt(
        (actorPos.x - enemy.pos.x)**2 + (actorPos.y - enemy.pos.y)**2
      )
      setCombat(c => ({ ...c, log: [...c.log, `Too far (${d.toFixed(0)} > ${getSkillRange(weaponId, selectedSkill)})`] }))
      return
    }

    const action = buildPlayerAttack({
      weaponId, skillName: selectedSkill, targetPart: selectedTarget,
      enemyIndex: ei,
      equippedWeapons: actorChar.equipped ?? { left: 'fists', right: 'fists' },
      weaponConditions: actorChar.weaponConditions ?? {},
      bodyParts: actorChar.integrity,
      bodyInjuries: actorChar.injuries,
      combat: cc,
      actor: actorId === 'player' ? 'player' : 'party',
      charId: actorId,
    })

    if (action.type === 'shove_pending') {
      const enemy2 = cc.characters[action.enemyCharId]
      if (!enemy2 || isDead(enemy2.integrity)) return
      const { pos, actualPush } = computeShove(actorPos, enemy2.pos, fresh.blocks)
      setCombat(c => ({
        ...c,
        characters: { ...c.characters, [action.enemyCharId]: { ...enemy2, pos } },
        log: [...c.log, `${actorChar.name} shoves ${enemy2.name} back ${Math.round(actualPush)} units!`],
      }))
      setLimbSelect(null); setSelectedSkill(null); setSelectedTarget(null); setAttackEnemy(null); setCurrentActor(null)
      return
    }

    const anim = createWeaponStrikeAnim(action, combatRef.current)
    characterActionsRef.current[actorId] = anim
    setLimbSelect(null); setSelectedSkill(null); setSelectedTarget(null); setAttackEnemy(null); setCurrentActor(null)
  }

  const cancelAttack = () => {
    setLimbSelect(null); setSelectedSkill(null); setSelectedTarget(null); setAttackEnemy(null); setCurrentActor(null)
    setAimMode(false)
  }

  const startTimer = () => {
    if (!timerActiveRef.current) {
      timerActiveRef.current = true
      setTimerRunning(true)
    }
  }
  const stopTimer = () => {
    timerActiveRef.current = false
    setTimerRunning(false)
  }
  const toggleTimer = () => {
    if (timerActiveRef.current) { stopTimer() } else { startTimer() }
  }

  // Spacebar toggles timer, Escape cancels aim mode
  useEffect(() => {
    const onKey = (e) => {
      if (e.code === 'Space' && combat) { e.preventDefault(); toggleTimer() }
      if (e.key === 'Escape' && aimMode) { setAimMode(false); setSelectedSkill(null) }
    }
    window.addEventListener('keydown', onKey)
    return () => window.removeEventListener('keydown', onKey)
  }, [aimMode, combat])

  /** Execute a player move; queue it for execution. */
  const startMove = (targetX, targetY) => {
    if (!combat) return
    const actorId = currentActor ?? 'player'
    const ch = combat.characters[actorId]
    if (!ch) return
    const curPos = characterPositionsRef.current[actorId] ?? ch.pos
    const dist  = Math.sqrt((targetX - curPos.x) ** 2 + (targetY - curPos.y) ** 2)
    if (dist < 1) return
    const maxSpd = ch.maxSpeed ?? 10
    const pct = moveMode === 'run' ? 1.0 : 0.5
    const speed = maxSpd * pct
    const moveAction = new MoveAction({ type: 'move', fromX: curPos.x, fromY: curPos.y, toX: targetX, toY: targetY, speed, blocks: combat.blocks,
      setPos: (p, cid) => {
        const id = cid || actorId
        setCombat(c => ({ ...c, characters: { ...c.characters, [id]: { ...c.characters[id], pos: { ...p } } } }))
        characterPositionsRef.current[id] = p
      }
    })
    const dx = targetX - curPos.x, dy = targetY - curPos.y
    const d = Math.sqrt(dx * dx + dy * dy)
    const dir = d > 0 ? { dx: dx / d, dy: dy / d } : { dx: 0, dy: 1 }
    characterActionsRef.current[actorId] = new ParallelAction({
      actions: [
        new TurnAction({ targetDir: dir, getDir: () => characterDirsRef.current[actorId] ?? { dx: 0, dy: 1 }, setDir: d => { characterDirsRef.current[actorId] = d } }),
        moveAction
      ]
    })
    setMoveMode(null); setCurrentActor(null)
  }

  /** Show ammo selection UI based on what's in inventory */
  const startReload = () => {
    if (!combat) return
    const actorId = currentActor ?? 'player'
    const cc = combatRef.current
    const actorChar = cc?.characters[actorId]
    const mag = actorId === 'player' ? magazineRef.current : (actorChar?.weaponMagazines ?? {})
    const cur = mag.pistol?.length ?? 0
    if (cur >= PISTOL_MAG) {
      setCombat(c => ({ ...c, log: [...c.log, 'The pistol is already fully loaded.'] }))
      return
    }
    const itemCounts = actorChar?.itemCounts ?? (actorId === 'player' ? playerRef.current.itemCounts : {})
    const available = Object.entries(itemCounts)
      .filter(([id, qty]) => qty > 0 && (id === 'pistol_ammo' || id === 'bouncing_ammo'))
    if (available.length === 0) {
      setCombat(c => ({ ...c, log: [...c.log, 'No ammo in inventory!'] }))
      return
    }
    setSelectedSkill('Load'); setAttackEnemy(null); setSelectedTarget(null)
  }

  /** Load one round of the given ammo type into the weapon */
  const loadAmmoType = (ammoId) => {
    const actorId = currentActor ?? 'player'
    const cc = combatRef.current
    const actorChar = cc?.characters[actorId]
    const mag = actorId === 'player' ? magazineRef.current : (actorChar?.weaponMagazines ?? { pistol: [] })
    const itemCounts = actorChar?.itemCounts ?? (actorId === 'player' ? playerRef.current.itemCounts : {})
    const cur = mag.pistol?.length ?? 0
    const reserve = itemCounts[ammoId] ?? 0
    if (reserve <= 0) return
    const ammoName = itemMap[ammoId]?.name ?? ammoId
    if (actorId === 'player') {
      const newMag = { pistol: [...magazineRef.current.pistol, { ammoType: ammoId }] }
      magazineRef.current = newMag
      setPlayer(p => {
        const newCount = (p.itemCounts[ammoId] ?? 0) - 1
        const counts = { ...p.itemCounts, [ammoId]: newCount }
        if (newCount <= 0) delete counts[ammoId]
        return {
          ...p,
          weaponMagazines: { ...p.weaponMagazines, ...newMag },
          itemCounts: counts,
        }
      })
      setCombat(prev => {
        const ch = prev.characters['player']
        const newCount = (ch?.itemCounts?.[ammoId] ?? 0) - 1
        const counts = { ...(ch?.itemCounts ?? {}), [ammoId]: newCount }
        if (newCount <= 0) delete counts[ammoId]
        return {
          ...prev,
          characters: {
            ...prev.characters,
            player: { ...ch, itemCounts: counts },
          },
          log: [...prev.log, `Loaded 1 round of ${ammoName} into the pistol.`],
        }
      })
    } else {
      setCombat(prev => {
        const ch = prev.characters[actorId]
        if (!ch) return prev
        const chMag = ch.weaponMagazines ?? { pistol: [] }
        const newMag = { pistol: [...(chMag.pistol ?? []), { ammoType: ammoId }] }
        const newCounts = { ...ch.itemCounts, [ammoId]: (ch.itemCounts?.[ammoId] ?? 0) - 1 }
        if (newCounts[ammoId] <= 0) delete newCounts[ammoId]
        return {
          ...prev,
          characters: {
            ...prev.characters,
            [actorId]: { ...ch, weaponMagazines: { ...ch.weaponMagazines, ...newMag }, itemCounts: newCounts },
          },
          log: [...prev.log, `${ch.name ?? 'Party member'} loaded 1 round of ${ammoName} into the pistol.`],
        }
      })
    }
    setLimbSelect(null); setSelectedSkill(null); setSelectedTarget(null); setAttackEnemy(null)
  }

  const unloadPistol = () => {
    if (!combat) return
    const actorId = currentActor ?? 'player'
    const cc = combatRef.current
    const actorChar = cc?.characters[actorId]
    const mag = actorId === 'player' ? magazineRef.current : (actorChar?.weaponMagazines ?? { pistol: [] })
    const cur = mag.pistol?.length ?? 0
    if (cur === 0) {
      setCombat(c => ({ ...c, log: [...c.log, 'The pistol is already empty.'] }))
      return
    }
    const top = mag.pistol[cur - 1]
    const ammoId = top.ammoType
    const ammoName = itemMap[ammoId]?.name ?? ammoId
    if (actorId === 'player') {
      const newMag = { pistol: mag.pistol.slice(0, -1) }
      magazineRef.current = newMag
      setPlayer(p => ({
        ...p,
        weaponMagazines: { ...p.weaponMagazines, ...newMag },
        itemCounts: { ...p.itemCounts, [ammoId]: (p.itemCounts[ammoId] ?? 0) + 1 },
      }))
      setCombat(c => ({ ...c, log: [...c.log, `Unloaded 1 round of ${ammoName} from the pistol.`] }))
    } else {
      setCombat(prev => {
        const ch = prev.characters[actorId]
        if (!ch) return prev
        const chMag = ch.weaponMagazines ?? { pistol: [] }
        const newMag = { pistol: (chMag.pistol ?? []).slice(0, -1) }
        return {
          ...prev,
          characters: {
            ...prev.characters,
            [actorId]: {
              ...ch,
              weaponMagazines: { ...ch.weaponMagazines, ...newMag },
              itemCounts: { ...ch.itemCounts, [ammoId]: (ch.itemCounts?.[ammoId] ?? 0) + 1 },
            },
          },
          log: [...prev.log, `${ch.name ?? 'Party member'} unloaded 1 round of ${ammoName} from the pistol.`],
        }
      })
    }
    setLimbSelect(null); setSelectedSkill(null); setSelectedTarget(null); setAttackEnemy(null)
  }

  // ─────────────────────────────────────────────────────────────────────────
  // MAP / ZOOM HELPERS
  // ─────────────────────────────────────────────────────────────────────────

  const zoomTo = (factor, mx, my) => {
    const v = viewRef.current
    const rect = svgRef.current.getBoundingClientRect()
    const s = Math.min(rect.width / 800, rect.height / 600)
    const ox = (rect.width - 800 * s) / 2, oy = (rect.height - 600 * s) / 2
    const vbX = (mx - ox) / s, vbY = (my - oy) / s
    const ns = Math.max(0.3, Math.min(3, v.scale * factor))
    const f  = ns / v.scale
    setZoomAnim({ from: { x: v.x, y: v.y, scale: v.scale }, to: { x: vbX * (1 - f) + v.x * f, y: vbY * (1 - f) + v.y * f, scale: ns }, start: performance.now() })
  }

  const zoomIn  = () => { const r = svgRef.current.getBoundingClientRect(); zoomTo(1.3,   r.width / 2, r.height / 2) }
  const zoomOut = () => { const r = svgRef.current.getBoundingClientRect(); zoomTo(1/1.3, r.width / 2, r.height / 2) }

  const handleWheel = (e) => { e.preventDefault(); const r = svgRef.current.getBoundingClientRect(); zoomTo(e.deltaY > 0 ? 1/1.3 : 1.3, e.clientX - r.left, e.clientY - r.top) }

  const handleBgPointerDown = (e) => {
    if (e.button !== 0) return
    const v = viewRef.current, r = svgRef.current.getBoundingClientRect()
    const s = Math.min(r.width / 800, r.height / 600)
    const ox = (r.width - 800*s)/2, oy = (r.height - 600*s)/2
    isPanning.current = true
    panStart.current  = { x: e.clientX - ox, y: e.clientY - oy, vx: v.x, vy: v.y, s }
    e.currentTarget.setPointerCapture(e.pointerId)
  }
  const handleBgPointerMove = (e) => {
    if (!isPanning.current) return
    const v = viewRef.current, r = svgRef.current.getBoundingClientRect()
    const s = Math.min(r.width / 800, r.height / 600)
    const ox = (r.width - 800*s)/2, oy = (r.height - 600*s)/2
    const dx = (e.clientX - ox - panStart.current.x) / panStart.current.s / v.scale
    const dy = (e.clientY - oy - panStart.current.y) / panStart.current.s / v.scale
    setView({ ...v, x: panStart.current.vx + dx, y: panStart.current.vy + dy })
  }
  const handleBgPointerUp = () => { isPanning.current = false }

  // ─────────────────────────────────────────────────────────────────────────
  // SHOP
  // ─────────────────────────────────────────────────────────────────────────

  const travel = (targetId) => {
    if (travelAnim) return
    const from = locations.find(l => l.id === player.location)
    const to   = locations.find(l => l.id === targetId)
    if (!from || !to) return
    setTravelAnim({ from, to, fromId: player.location, toId: targetId, startTs: null, progress: 0 })
  }

  const unequipWeapon = (arm) => {
    const wid = player.equipped[arm]
    if (wid === 'fists' || wid === 'left_arm' || wid === 'right_arm') return
    setPlayer(p => ({ ...p, inventory: [...p.inventory, wid], equipped: { ...p.equipped, [arm]: 'fists' } }))
  }

  const unequipBoth = () => {
    const wid = player.equipped.left
    if (wid === 'fists' || wid === 'left_arm' || wid === 'right_arm') return
    setPlayer(p => ({ ...p, inventory: [...p.inventory, wid], equipped: { left: 'fists', right: 'fists' } }))
  }

  const equipFromInventory = (weaponId, arm) => {
    setPlayer(p => ({
      ...p,
      inventory: p.inventory.filter(id => id !== weaponId),
      equipped: arm === 'both' ? { left: weaponId, right: weaponId } : { ...p.equipped, [arm]: weaponId },
      weaponConditions: p.weaponConditions[weaponId] !== undefined ? p.weaponConditions : { ...p.weaponConditions, [weaponId]: weaponMap[weaponId].maxCondition },
    }))
    setEquipArmChoice(null)
  }

  // ─────────────────────────────────────────────────────────────────────────
  // DISPLAY HELPERS
  // ─────────────────────────────────────────────────────────────────────────

  const partMap      = { larm: 'leftArm', rarm: 'rightArm', lleg: 'leftLeg', rleg: 'rightLeg' }
  const selectLimbTarget = (part) => setSelectedTarget(partMap[part] || part)

  const skipTotalHours = skipAnim !== null ? skipAnim.current : null
  const displayDay     = skipTotalHours !== null ? Math.floor(skipTotalHours / 24) : gameTime.day
  const displayHour    = skipTotalHours !== null ? Math.floor(skipTotalHours % 24) : gameTime.hour
  const displayMinute  = skipTotalHours !== null ? 0 : (gameTime.minute ?? 0)
  const dateString     = getDateString(displayDay)

  // busy is React state, updated in the timer loop to reflect refs

  // ─────────────────────────────────────────────────────────────────────────
  // RENDER
  // ─────────────────────────────────────────────────────────────────────────

  const handleNewGame = () => { setShowMenu(false); setShowCharCreate(true) }

  const canTrade = (charId) => {
    if (!combat || charId === 'player') return true
    const src = combat.characters?.player
    const tgt = combat.characters?.[charId]
    if (!src || !tgt) return true
    const dx = src.x - tgt.x
    const dy = src.y - tgt.y
    return Math.sqrt(dx * dx + dy * dy) <= 35
  }

  const startTrade = (targetId) => {
    setHoverInfo(null); setHoveredChar(null); setContextMenu(null)
    setShowTrade(true)
    setTradeTarget(targetId)
    setTradeGive({})
    setTradeTake({})
  }

  if (showMenu) {
    return (
      <div className="menu-screen">
        <div className="menu-content">
          <h1 className="menu-title">CITYGAMIN</h1>
          <p className="menu-subtitle">A game of survival and conquest</p>
          <button className="menu-btn" onClick={handleNewGame}>New Game</button>
        </div>
      </div>
    )
  }

  if (showCharCreate) {
    return <CharacterCreator onConfirm={(opts) => {
      const { name, weaponId, fillColor, locationId } = opts
      setPlayer(createCharacter({
        id: 'player', name, age: 24, location: locationId, money: 50,
        inventory: weaponId === 'pistol' ? ['pistol_ammo', 'bouncing_ammo'] : [],
        equipped: { left: weaponId === 'pistol' ? 'pistol' : 'fists', right: weaponId === 'pistol' ? 'fists' : weaponId },
        weaponConditions: weaponId !== 'fists' ? { [weaponId]: weaponMap[weaponId]?.maxCondition ?? 100 } : {},
        size: 1.3,
        weaponMagazines: weaponId === 'pistol' ? { pistol: [{ ammoType: 'pistol_ammo' }, { ammoType: 'pistol_ammo' }] } : {},
        itemCounts: weaponId === 'pistol' ? { pistol_ammo: 20, bouncing_ammo: 10 } : {},
        fillColor,
      }))
      setShowCharCreate(false)
      setTimeout(() => enterLocation(locationId), 50)
    }} />
  }

  return (
    <div className="game-map" onMouseMove={e => setMousePos({ x: e.clientX, y: e.clientY })} onContextMenu={e => { e.preventDefault(); setContextMenu(null) }}>

      <div className="top-bar">
        <div className="top-bar-left">
          {combat && (
            <button className="icon-btn" onClick={() => setShowWorldMap(s => !s)} title={showWorldMap ? 'Combat View' : 'World Map'}>
              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="7" height="7" /><rect x="14" y="3" width="7" height="7" /><rect x="3" y="14" width="7" height="7" /><rect x="14" y="14" width="7" height="7" /></svg>
            </button>
          )}
          <button className="icon-btn" onClick={() => { setShowInventory(true); setPanelTarget('player') }} title="Character">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="8" r="4" /><path d="M4 22a8 8 0 0 1 16 0" /></svg>
          </button>
          <button className="icon-btn" onClick={() => setShowWorldInfo(s => !s)} title="World">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10" /><ellipse cx="12" cy="12" rx="4" ry="10" /><path d="M2 12h20" /></svg>
          </button>
          <button className="icon-btn" onClick={() => { setPassTimeHours(1); setPassTimeOpen(s => !s) }} title="Pass Time">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10" /><polyline points="12 6 12 12 16 14" /></svg>
          </button>
          <button className="icon-btn" onClick={() => setShowSettings(s => !s)} title="Settings">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" /></svg>
          </button>
        </div>
        <div className="top-bar-right">
          {(!combat || showWorldMap) && (
            <div className="game-time-display">
              {(() => {
                const d = displayDay; const h = displayHour; const min = displayMinute
                const m = Math.floor(min); const s = Math.floor((min % 1) * 60)
                return `${getDateString(d)} ${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`
              })()}
            </div>
          )}
          {combat && !showWorldMap && (
            <div className="combat-timer">
              <button className="timer-toggle" onClick={toggleTimer} title={timerRunning ? 'Pause' : 'Play'} style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#9ca3af', padding: 0, display: 'flex', alignItems: 'center' }}>
                {timerRunning ? (
                  <svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor">
                    <rect x="3" y="2" width="4" height="12" rx="1" /><rect x="9" y="2" width="4" height="12" rx="1" />
                  </svg>
                ) : (
                  <svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor">
                    <polygon points="3,2 14,8 3,14" />
                  </svg>
                )}
              </button>
              {(() => {
                const min = gameTime.minute ?? 0
                const h = gameTime.hour
                const m = Math.floor(min)
                const s = Math.floor((min % 1) * 60)
                const cs = Math.floor((((min % 1) * 60) % 1) * 100)
                return `${getDateString(gameTime.day)} ${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}.${String(cs).padStart(2,'0')}`
              })()}
              <div style={{ display: 'flex', gap: 4, marginLeft: 12 }}>
                {[1, 0.5, 0.25, 0.1].map(s => (
                  <button key={s} onClick={() => { speedRef.current = s; setSpeedMult(s) }}
                    style={{ background: speedMult === s ? '#2d2d44' : 'none', border: '1px solid #2d2d44', color: '#9ca3af', cursor: 'pointer', padding: '2px 6px', borderRadius: 3, fontSize: 11, fontWeight: speedMult === s ? 'bold' : 'normal' }}>
                    {s}x
                  </button>
                ))}
              </div>
            </div>
          )}
        </div>
      </div>

      {/* ═══════════════════════════════════════════════════════ COMBAT VIEW */}
      {combat && !showWorldMap && (
        <>
        <div className="combat-layout">
            {/* ── left: action tree (per party member) */}
            <div className="combat-left">
              {combat && (() => {
                const partyIds = combat.partyIds ?? [combat.playerEntityId].filter(Boolean)
                const anyActionPossible = partyIds.some(cid => !isDead(combat.characters[cid]?.integrity))
                return anyActionPossible && (
                  <div className={`combat-actions${busy ? ' combat-busy' : ''}${aimMode ? ' combat-aiming' : ''}`} style={aimMode ? { pointerEvents: 'none', opacity: 0.5 } : undefined}>
                    {partyIds.map(cid => {
                      const ch = combat.characters[cid]
                      if (!ch) return null
                      const isDeadChar = isDead(ch.integrity)
                      const charEquipped = ch.equipped ?? { left: 'fists', right: 'fists' }
                      const lw = charEquipped.left, rw = charEquipped.right
                      const charName = ch.name ?? cid
                      return (
                        <div key={cid} className="tree-root" style={{ opacity: isDeadChar ? 0.4 : 1, pointerEvents: isDeadChar ? 'none' : undefined }}>
                          <div className="tree-node folder" onClick={() => setExpandedWeapon(prev => prev.includes(cid) ? prev.filter(x => x !== cid) : [...prev, cid])}>
                            <span className="tree-toggle">{expandedWeapon.includes(cid) ? '▼' : '▶'}</span>
                            <span className="tree-label" style={{ color: '#22c55e', cursor: 'pointer' }}
                              onClick={e => { e.stopPropagation(); setShowInventory(true); setPanelTarget(cid) }}>{charName}{isDeadChar ? ' (Dead)' : ''}</span>
                          </div>
                          {expandedWeapon.includes(cid) && (
                            <div className="tree-children">
                              {/* Weapons */}
                              <div className="tree-node folder" onClick={() => setWeaponsOpen(prev => prev.includes(cid) ? prev.filter(x => x !== cid) : [...prev, cid])}>
                                <span className="tree-toggle">{weaponsOpen.includes(cid) ? '▼' : '▶'}</span>
                                <span className="tree-label">Weapons</span>
                              </div>
                              {weaponsOpen.includes(cid) && (
                                <div className="tree-children">
                                  {(() => {
                                    const entries = []
                                    if (lw === 'fists') {
                                      entries.push(<div key="left-arm" className="tree-node leaf"
                                        onMouseEnter={() => setHoverInfo({ type: 'weapon', id: 'left_arm' })} onMouseLeave={() => setHoverInfo(null)}
                                        onClick={() => { setCurrentActor(cid); setLimbSelect('left_arm'); setSelectedSkill(null); setSelectedTarget(null); setMoveMode(null); setAimMode(false) }}>
                                        <span className="tree-bullet">○</span><span className="tree-label">Left Arm</span></div>)
                                    } else if (lw !== rw) {
                                      entries.push(<div key="left-weap" className="tree-node leaf"
                                        onMouseEnter={() => setHoverInfo({ type: 'weapon', id: lw })} onMouseLeave={() => setHoverInfo(null)}
                                        onClick={() => { setCurrentActor(cid); setLimbSelect(lw); setSelectedSkill(null); setSelectedTarget(null); setMoveMode(null); setAimMode(false) }}>
                                        <span className="tree-bullet">○</span><span className="tree-label">Left: {weaponMap[lw]?.name ?? lw}</span></div>)
                                    }
                                    if (rw === 'fists') {
                                      entries.push(<div key="right-arm" className="tree-node leaf"
                                        onMouseEnter={() => setHoverInfo({ type: 'weapon', id: 'right_arm' })} onMouseLeave={() => setHoverInfo(null)}
                                        onClick={() => { setCurrentActor(cid); setLimbSelect('right_arm'); setSelectedSkill(null); setSelectedTarget(null); setMoveMode(null); setAimMode(false) }}>
                                        <span className="tree-bullet">○</span><span className="tree-label">Right Arm</span></div>)
                                    } else if (lw !== rw) {
                                      entries.push(<div key="right-weap" className="tree-node leaf"
                                        onMouseEnter={() => setHoverInfo({ type: 'weapon', id: rw })} onMouseLeave={() => setHoverInfo(null)}
                                        onClick={() => { setCurrentActor(cid); setLimbSelect(rw); setSelectedSkill(null); setSelectedTarget(null); setMoveMode(null); setAimMode(false) }}>
                                        <span className="tree-bullet">○</span><span className="tree-label">Right: {weaponMap[rw]?.name ?? rw}</span></div>)
                                    }
                                    if (lw === rw && lw !== 'fists' && lw !== 'left_arm' && lw !== 'right_arm') {
                                      entries.push(<div key="both-weap" className="tree-node leaf"
                                        onMouseEnter={() => setHoverInfo({ type: 'weapon', id: lw })} onMouseLeave={() => setHoverInfo(null)}
                                        onClick={() => { setCurrentActor(cid); setLimbSelect(lw); setSelectedSkill(null); setSelectedTarget(null); setMoveMode(null); setAimMode(false) }}>
                                        <span className="tree-bullet">○</span><span className="tree-label">Both: {weaponMap[lw]?.name ?? lw}</span></div>)
                                    }
                                    if (lw === 'fists' && rw === 'fists') {
                                      entries.push(<div key="fists" className="tree-node leaf"
                                        onMouseEnter={() => setHoverInfo({ type: 'weapon', id: 'fists' })} onMouseLeave={() => setHoverInfo(null)}
                                        onClick={() => { setCurrentActor(cid); setLimbSelect('fists'); setSelectedSkill(null); setSelectedTarget(null); setMoveMode(null); setAimMode(false) }}>
                                        <span className="tree-bullet">○</span><span className="tree-label">Fists</span></div>)
                                    }
                                    return entries
                                  })()}
                                </div>
                              )}
                              {/* Items */}
                              <div className="tree-node folder" onClick={() => setItemsOpen(prev => prev.includes(cid) ? prev.filter(x => x !== cid) : [...prev, cid])}>
                                <span className="tree-toggle">{itemsOpen.includes(cid) ? '▼' : '▶'}</span>
                                <span className="tree-label">Items</span>
                              </div>
                              {itemsOpen.includes(cid) && (
                                <div className="tree-children">
                                  {(ch.inventory ?? []).length === 0
                                    ? <div className="move-hint">No items</div>
                                    : ch.inventory.map((itemId, i) => {
                                      const qty = ch.itemCounts?.[itemId]
                                      if (qty !== undefined && qty <= 0) return null
                                      return (
                                        <div key={i} className="tree-node leaf" onClick={() => { setShowInventory(true); setPanelTarget(cid) }}>
                                          <span className="tree-bullet">○</span>
                                          <span className="tree-label">{itemMap[itemId]?.name ?? itemId}</span>
                                          {qty !== undefined && <span style={{ marginLeft: 6, color: '#9ca3af', fontSize: 12 }}>×{qty}</span>}
                                        </div>
                                      )
                                    })
                                  }
                                </div>
                              )}

                              {/* Movement */}
                              <div className="tree-node folder" onClick={() => setMovementOpen(prev => prev.includes(cid) ? prev.filter(x => x !== cid) : [...prev, cid])}>
                                <span className="tree-toggle">{movementOpen.includes(cid) ? '▼' : '▶'}</span>
                                <span className="tree-label">Movement</span>
                              </div>
                              {movementOpen.includes(cid) && (
                                <div className="tree-children">
                                  <div className="tree-node leaf" onClick={() => { if (!busy) { setCurrentActor(cid); setMoveMode(m => m === 'walk' ? null : 'walk'); setAimMode(false) } }}>
                                    <span className="tree-bullet">○</span><span className="tree-label">Walk</span>
                                  </div>
                                  <div className="tree-node leaf" onClick={() => { if (!busy) { setCurrentActor(cid); setMoveMode(m => m === 'run' ? null : 'run'); setAimMode(false) } }}>
                                    <span className="tree-bullet">○</span><span className="tree-label">Run</span>
                                  </div>
                                  <div className="tree-node leaf" onClick={() => { if (!busy) { setCurrentActor(cid); setMoveMode(m => m === 'turn' ? null : 'turn'); setAimMode(false) } }}>
                                    <span className="tree-bullet">○</span><span className="tree-label">Turn</span>
                                  </div>
                                  {moveMode && moveMode !== 'turn' && <div className="tree-children" style={{ paddingLeft: 0 }}><div className="move-hint">Click on the mini-map to move</div></div>}
                                  {moveMode === 'turn' && <div className="tree-children" style={{ paddingLeft: 0 }}><div className="move-hint">Click on the mini-map to face a direction</div></div>}
                                </div>
                              )}
                            </div>
                          )}
                        </div>
                      )
                    })}
                  </div>
                )
              })()}
            </div>

            {/* ── center: minimap + log */}
            <div className="combat-center">
              <div className="minimap-container" style={{ position: 'relative', borderColor: hitShake ? '#ef4444' : undefined, cursor: aimMode ? 'crosshair' : undefined }}>
                <div className="minimap-toolbar">
                  <button className="zoom-btn" onClick={() => setMinimapView(v => { const ns = Math.min(3, v.scale*1.5); return { x: v.x + 0.5*(160/v.scale - 160/ns), y: v.y + 0.5*(160/v.scale - 160/ns), scale: ns } })}>+</button>
                  <button className="zoom-btn" onClick={() => setMinimapView(v => { const ns = Math.max(0.5, v.scale/1.5); return { x: v.x + 0.5*(160/v.scale - 160/ns), y: v.y + 0.5*(160/v.scale - 160/ns), scale: ns } })}>−</button>
                  <button className="zoom-btn" onClick={() => setMinimapView({ x: 0, y: 0, scale: 1 })}>⟲</button>
                </div>
                <svg viewBox="0 0 160 160" className="minimap"
                  style={{ cursor: limbSelect ? 'default' : (minimapPanRef.current ? 'grabbing' : 'grab') }}
                  onMouseDown={e => {
                    if (limbSelect) return
                    minimapPanRef.current = true; minimapMovedRef.current = false
                    minimapPanStart.current = { x: e.clientX, y: e.clientY, vx: minimapView.x, vy: minimapView.y }
                    const svg = e.currentTarget
                    const onMove = ev => {
                      if (!minimapPanRef.current) return
                      const dx = ev.clientX - minimapPanStart.current.x, dy = ev.clientY - minimapPanStart.current.y
                      if (Math.abs(dx) > 2 || Math.abs(dy) > 2) minimapMovedRef.current = true
                      const r = svg.getBoundingClientRect()
                      setMinimapView(v => ({ ...v, x: minimapPanStart.current.vx - (dx / r.width) * 160 / v.scale, y: minimapPanStart.current.vy - (dy / r.height) * 160 / v.scale }))
                    }
                    const onUp = () => { minimapPanRef.current = false; window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp) }
                    window.addEventListener('mousemove', onMove); window.addEventListener('mouseup', onUp)
                  }}
                  onWheel={e => {
                    if (limbSelect) return; e.preventDefault()
                    const d = e.deltaY > 0 ? 0.9 : 1.1
                    const r = e.currentTarget.getBoundingClientRect()
                    setMinimapView(v => {
                      const ns = Math.max(0.5, Math.min(3, v.scale * d))
                      return { x: v.x + (e.clientX - r.left) / r.width * (160/v.scale - 160/ns), y: v.y + (e.clientY - r.top) / r.height * (160/v.scale - 160/ns), scale: ns }
                    })
                  }}
                  onClick={e => {
                    if (minimapMovedRef.current) { minimapMovedRef.current = false; return }
                    if (limbSelect || (!moveMode && !aimMode)) return
                    const r = e.currentTarget.getBoundingClientRect()
                    const tx = ((e.clientX - r.left) / r.width) * 160 / minimapView.scale + minimapView.x
                    const ty = ((e.clientY - r.top)  / r.height) * 160 / minimapView.scale + minimapView.y
                    if (aimMode) {
                      queueShot(tx, ty)
                      return
                    }
                        if (moveMode === 'turn') {
                          const turnActor = currentActor ?? 'player'
                          const pp = combat.characters[turnActor]?.pos ?? combat.characters['player'].pos
                          const dx = tx - pp.x
                          const dy = ty - pp.y
                          const d = Math.sqrt(dx * dx + dy * dy)
                          if (d > 0) {
                            characterActionsRef.current[turnActor] = new TurnAction({ targetDir: { dx: dx / d, dy: dy / d }, getDir: () => characterDirsRef.current[turnActor] ?? { dx: 0, dy: 1 }, setDir: d2 => { characterDirsRef.current[turnActor] = d2 } })
                            setMoveMode(null); setCurrentActor(null)
                          }
                          return
                        }
                    startMove(tx, ty)
                  }}>
                  <defs>
                    <filter id="arrowGlow">
                      <feGaussianBlur stdDeviation="1.5" result="blur" />
                      <feMerge>
                        <feMergeNode in="blur" />
                        <feMergeNode in="SourceGraphic" />
                      </feMerge>
                    </filter>
                  </defs>
                  <g style={{
                    transform: `translate(${-minimapView.x * minimapView.scale + (hitShake ? (Math.random() - 0.5) * 2 : 0)}px, ${-minimapView.y * minimapView.scale + (hitShake ? (Math.random() - 0.5) * 2 : 0)}px) scale(${minimapView.scale})`,
                    transformOrigin: '0 0',
                    transition: hitShake ? 'none' : undefined,
                  }}>
                    <rect x="0" y="0" width="160" height="160" fill="#171721" />
                    {(combat?.blocks ?? []).map((block, i) => <rect key={i} x={block.x} y={block.y} width={block.w} height={block.h} fill="#2e303a" stroke="#1f2028" strokeWidth="1" />)}
                    {(combat?.enemyIds ?? []).map(eid => {
                      const enemy = combat.characters[eid]
                      const dead = isDead(enemy.integrity)
                      const isShaking = hitShake && hitShake.charId === eid && hitShake.actor === 'enemy'
                      const eSize = enemy.size ?? 1
                      const ex = isShaking ? enemy.pos.x + (Math.random() - 0.5) * 1.5 : enemy.pos.x
                      const ey = isShaking ? enemy.pos.y + (Math.random() - 0.5) * 1.5 : enemy.pos.y
                      return (
                        <Fragment key={eid}>
                          <circle cx={ex} cy={ey} r={5 * eSize}
                            fill={dead ? '#6b7280' : enemy.fillColor} stroke={dead ? '#6b7280' : '#e74c3c'} strokeWidth="1" opacity={dead ? 0.5 : 1}
                            onMouseEnter={e => { e.stopPropagation(); setHoverInfo({ type: 'enemy', name: enemy.name, attack: enemy.attack, speed: enemy.speed, defeated: isDead(enemy.integrity) }); setHoveredChar(`enemy-${eid}`) }}
                             onMouseLeave={() => { setHoverInfo(null); setHoveredChar(null) }}
                             onClick={e => { e.stopPropagation(); setShowInventory(true); setPanelTarget(eid) }}
                             onContextMenu={e => { e.preventDefault(); e.stopPropagation(); setHoverInfo(null); setHoveredChar(null); setContextMenu({ x: e.clientX, y: e.clientY, charId: eid }) }} />
                        </Fragment>
                      )
                    })}
                    {combat && selectedSkill && limbSelect && (() => {
                      const rangeActor = currentActor ?? 'player'
                      const rp = combat.characters[rangeActor]?.pos ?? combat.characters['player'].pos
                      return <circle cx={rp.x} cy={rp.y} r={getSkillRange(limbSelect, selectedSkill)} fill="none" stroke="rgba(52,152,219,0.5)" strokeWidth="1" strokeDasharray="4 4" />
                    })()}
                    {/* Party member circles */}
                    {combat && (() => {
                      const partyCircles = combat.partyIds ?? [combat.playerEntityId].filter(Boolean)
                      return partyCircles.map(cid => {
                        const ch = combat.characters[cid]
                        if (!ch) return null
                        const chDead = isDead(ch.integrity)
                        const color = cid === 'player' ? player.fillColor : ch.fillColor
                        return (
                          <circle key={`party-${cid}`}
                            cx={hitShake && hitShake.charId === cid ? ch.pos.x + (Math.random() - 0.5) * 1.5 : ch.pos.x}
                            cy={hitShake && hitShake.charId === cid ? ch.pos.y + (Math.random() - 0.5) * 1.5 : ch.pos.y}
                            r={4 * (ch.size ?? 1)} fill={chDead ? '#6b7280' : color} stroke={chDead ? '#6b7280' : '#3498db'} strokeWidth="1" opacity={chDead ? 0.5 : 1}
                            onMouseEnter={() => setHoveredChar(cid)} onMouseLeave={() => setHoveredChar(null)}
                            onClick={e => { e.stopPropagation(); setShowInventory(true); setPanelTarget(cid) }}
                            onContextMenu={e => { e.preventDefault(); e.stopPropagation(); setHoverInfo(null); setHoveredChar(null); setContextMenu({ x: e.clientX, y: e.clientY, charId: cid }) }} />
                        )
                      })
                    })()}
                    {/* Non-hostile NPC circles */}
                    {combat?.npcIds?.map(nid => {
                      const ch = combat.characters[nid]
                      if (!ch) return null
                      return (
                        <circle key={`npc-${nid}`}
                          cx={ch.pos.x} cy={ch.pos.y}
                          r={4} fill={ch.fillColor} stroke="#22c55e" strokeWidth="1" opacity={0.7}
                          onMouseEnter={() => setHoveredChar(nid)} onMouseLeave={() => setHoveredChar(null)}
                          onClick={e => { e.stopPropagation(); setShowInventory(true); setPanelTarget(nid) }}
                          onContextMenu={e => { e.preventDefault(); e.stopPropagation(); setHoverInfo(null); setHoveredChar(null); setContextMenu({ x: e.clientX, y: e.clientY, charId: nid }) }} />
                      )
                    })}
                    {/* Weapon circles for party members */}
                    {combat && (() => {
                      const wpnOff = (dir, side, off) => {
                        if (side === 'front') return { x: dir.dx * off, y: dir.dy * off }
                        if (side === 'left') return { x: dir.dy * off, y: -dir.dx * off }
                        return { x: -dir.dy * off, y: dir.dx * off }
                      }
                      const wpnDots = (slots, bx, by) => slots.map(s =>
                        <circle key={s.key} cx={bx + s.ox} cy={by + s.oy} r="3" fill={s.fill} stroke="#000" strokeWidth="0.5" />
                      )
                      const isFist = w => w === 'fists' || w === 'left_arm' || w === 'right_arm'
                      const isAtk = t => [ANIM_TYPES.SLASH, ANIM_TYPES.PIERCE_STRIKE, ANIM_TYPES.FIST_STRIKE, ANIM_TYPES.FIST_HOOK].includes(t)
                      const allSlots = []
                      const partyIds = combat.partyIds ?? [combat.playerEntityId].filter(Boolean)
                      partyIds.forEach(cid => {
                        const ch = combat.characters[cid]
                        if (!ch || isDead(ch.integrity)) return
                        const equipped = ch.equipped ?? { left: 'fists', right: 'fists' }
                        const lw = equipped.left, rw = equipped.right
                        const dir = characterDirsRef.current[cid] ?? { dx: 0, dy: 1 }
                        const off = 7
                        const shake = hitShake?.charId === cid
                        const bx = shake ? ch.pos.x + (Math.random() - 0.5) * 1.5 : ch.pos.x
                        const by = shake ? ch.pos.y + (Math.random() - 0.5) * 1.5 : ch.pos.y
                        const bothWpn = lw === rw && !isFist(lw)
                        const chPos = ch.pos
                        if (bothWpn) {
                          const anim = animations.find(a => a.charId === cid && isAtk(a.type) && a.weaponId === lw)
                          const p = anim ? { x: getAnimPos(anim).x - chPos.x, y: getAnimPos(anim).y - chPos.y } : wpnOff(dir, 'front', off)
                          allSlots.push({ key: `pw-${cid}-both`, ox: p.x, oy: p.y, fill: '#fbbf24', bx, by })
                        } else {
                          const lAnim = animations.find(a => a.charId === cid && isAtk(a.type) && (a.weaponId === lw || a.weaponId === 'left_arm'))
                          const rAnim = animations.find(a => a.charId === cid && isAtk(a.type) && (a.weaponId === rw || a.weaponId === 'right_arm'))
                          const bothAnim = animations.find(a => a.charId === cid && isAtk(a.type) && a.weaponId === 'fists')
                          if (!isFist(lw) || lAnim || bothAnim) {
                            const ap = (isFist(lw) && bothAnim) ? bothAnim : lAnim
                            const p = ap ? { x: getAnimPos(ap).x - chPos.x, y: getAnimPos(ap).y - chPos.y } : wpnOff(dir, 'left', off)
                            allSlots.push({ key: `pw-${cid}-left`, ox: p.x, oy: p.y, fill: isFist(lw) ? ch.fillColor : '#fbbf24', bx, by })
                          }
                          if (!isFist(rw) || rAnim || bothAnim) {
                            const ap = (isFist(rw) && bothAnim) ? bothAnim : rAnim
                            const p = ap ? { x: getAnimPos(ap).x - chPos.x, y: getAnimPos(ap).y - chPos.y } : wpnOff(dir, 'right', off)
                            allSlots.push({ key: `pw-${cid}-right`, ox: p.x, oy: p.y, fill: isFist(rw) ? ch.fillColor : '#fbbf24', bx, by })
                          }
                        }
                      })
                      const flat = allSlots.map(s => <circle key={s.key} cx={s.bx + s.ox} cy={s.by + s.oy} r="3" fill={s.fill} stroke="#000" strokeWidth="0.5" />)
                      return flat
                    })()}
                    {combat && combat.enemyIds.map(eid => {
                      const enemy = combat.characters[eid]
                      const dead = isDead(enemy.integrity)
                      if (dead) return null
                      const wpn = enemy.weapon
                      if (!wpn) return null
                      const dir = characterDirsRef.current[eid] ?? { dx: 0, dy: 1 }
                      const shake = hitShake && hitShake.charId === eid && hitShake.actor === 'enemy'
                      const bx = shake ? enemy.pos.x + (Math.random() - 0.5) * 1.5 : enemy.pos.x
                      const by = shake ? enemy.pos.y + (Math.random() - 0.5) * 1.5 : enemy.pos.y
                      const isFist = wpn === 'fists' || wpn === 'left_arm' || wpn === 'right_arm'
                      const enemyAnim = animations.find(a => a.charId === eid && (a.type === ANIM_TYPES.SLASH || a.type === ANIM_TYPES.PIERCE_STRIKE || a.type === ANIM_TYPES.FIST_STRIKE || a.type === ANIM_TYPES.FIST_HOOK))
                      if (isFist && !enemyAnim) return null
                      const p = enemyAnim ? { x: getAnimPos(enemyAnim).x - enemy.pos.x, y: getAnimPos(enemyAnim).y - enemy.pos.y } : { x: -dir.dy * 7, y: dir.dx * 7 }
                      return <circle key={`ew-${eid}`} cx={bx + p.x} cy={by + p.y} r="3" fill={isFist ? enemy.fillColor : '#f97316'} stroke="#000" strokeWidth="0.5" />
                    })}
                    {/* Projectile dots */}
                    {projectiles.map(p => (
                      <circle key={p.id} cx={p.pos.x} cy={p.pos.y} r="1.5" fill="#fbbf24" stroke="#ff6b35" strokeWidth="1" />
                    ))}
                    {/* Direction arrows (on top, in front of character) */}
                    {combat && (() => {
                      const partyIds = combat.partyIds ?? [combat.playerEntityId].filter(Boolean)
                      const anyAlive = partyIds.some(cid => !isDead(combat.characters[cid]?.integrity))
                      if (!anyAlive) return null
                      return partyIds.map(cid => {
                        const ch = combat.characters[cid]
                        if (!ch || isDead(ch.integrity)) return null
                        const d = characterDirsRef.current[cid] ?? { dx: 0, dy: 1 }
                        const isShaking = hitShake?.charId === cid
                        const cx = isShaking ? ch.pos.x + (Math.random() - 0.5) * 1.5 : ch.pos.x
                        const cy = isShaking ? ch.pos.y + (Math.random() - 0.5) * 1.5 : ch.pos.y
                        const isHovered = hoveredChar === cid
                        const px = -d.dy, py = d.dx
                        return (
                          <polygon key={`arrow-${cid}`} points={[
                            `${cx + d.dx * 10},${cy + d.dy * 10}`,
                            `${cx + d.dx * 7 + px * 2},${cy + d.dy * 7 + py * 2}`,
                            `${cx + d.dx * 7 - px * 2},${cy + d.dy * 7 - py * 2}`,
                          ].join(' ')}
                            fill={isHovered ? '#fff' : '#3498db'} opacity="0.8" filter={isHovered ? 'url(#arrowGlow)' : undefined} />
                        )
                      })
                    })()}
                    {combat && combat.enemyIds.map(eid => {
                      const enemy = combat.characters[eid]
                      const dead = isDead(enemy.integrity)
                      if (dead) return null
                      const isShaking = hitShake && hitShake.charId === eid && hitShake.actor === 'enemy'
                      const ex = isShaking ? enemy.pos.x + (Math.random() - 0.5) * 1.5 : enemy.pos.x
                      const ey = isShaking ? enemy.pos.y + (Math.random() - 0.5) * 1.5 : enemy.pos.y
                      const d = characterDirsRef.current[eid] ?? { dx: 0, dy: 1 }
                      const isHovered = hoveredChar === `enemy-${eid}`
                      const px = -d.dy, py = d.dx
                      return (
                        <polygon key={`arrow-${eid}`} points={[
                          `${ex + d.dx * 10},${ey + d.dy * 10}`,
                          `${ex + d.dx * 7 + px * 2},${ey + d.dy * 7 + py * 2}`,
                          `${ex + d.dx * 7 - px * 2},${ey + d.dy * 7 - py * 2}`,
                        ].join(' ')}
                          fill={isHovered ? '#fff' : '#e74c3c'} opacity="0.8" filter={isHovered ? 'url(#arrowGlow)' : undefined} />
                      )
                    })}
                  </g>
                </svg>
              </div>
              <div className="combat-log">
                <div className="combat-log-title">Combat Log</div>
                <div className="combat-log-entries">
                  {(combat?.log || []).map((entry, i) => <div key={i} className="combat-log-entry">{entry}</div>)}
                </div>
              </div>
            </div>

            {/* ── right: enemy list */}
            <div className="combat-right">
              <div className="enemy-list">
                {(combat?.enemyIds ?? []).map(eid => {
                  const enemy = combat.characters[eid]
                  const idx = combat.enemyIds.indexOf(eid)
                  return (
                  <div key={eid} className="enemy-entry" onClick={() => { setSelectedEnemy(idx); setShowInventory(true); setPanelTarget(eid) }} onContextMenu={e => { e.preventDefault(); e.stopPropagation(); setContextMenu({ x: e.clientX, y: e.clientY, charId: eid }) }}>
                    <span className="enemy-name">{enemy.name}</span>
                  </div>
                  )
                })}
              </div>
            </div>
          </div>

          {/* ── attack modal */}
          {limbSelect && (() => {
            const actorId = currentActor ?? 'player'
            const actorChar = combat?.characters[actorId]
            // If the current actor is dead, find the next alive party member
            const liveActorId = (actorChar && isDead(actorChar.integrity))
              ? (combat.partyIds ?? []).find(pid => !isDead(combat.characters[pid]?.integrity)) ?? 'player'
              : actorId
            const resolvedActorChar = combat?.characters[liveActorId]
            const actorPos = resolvedActorChar ? (characterPositionsRef.current[liveActorId] ?? resolvedActorChar.pos) : { x: 20, y: 75 }
            const actorEquipped = resolvedActorChar?.equipped ?? { left: 'fists', right: 'fists' }
            return (
            <div className="inventory-overlay" onClick={cancelAttack}
              style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 8 }}>
              <div className="inventory-panel limb-select-panel" onClick={e => e.stopPropagation()}>
                {(() => {
                  const weapon     = weaponMap[limbSelect]
                  const weaponName = weapon?.name ?? limbSelect
                  const skills     = weapon?.skills ?? []
                  return (
                    <div style={{ position: 'relative', minHeight: 520 }}>
                      <div className="inv-parts-title limb-select-title" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
                        <span>{weaponName} — {actorChar?.name ?? actorId}</span>
                        {limbSelect === 'pistol' && <span style={{ fontSize: 13, color: '#9ca3af', fontWeight: 'normal' }}>Ammo: {actorChar?.weaponMagazines?.pistol?.length ?? 0}/{PISTOL_MAG} <span style={{ color: '#fbbf24' }}>({itemMap[actorChar?.weaponMagazines?.pistol?.[0]?.ammoType]?.name ?? 'none'})</span></span>}
                      </div>
                      <div style={{ display: 'flex', gap: 16, alignItems: 'flex-start', paddingRight: 250 }}>
                        {/* skill list */}
                        {skills.length > 0 && (
                          <div>
                            <div className="inv-parts-title" style={{ marginBottom: 8 }}>Select Skill</div>
                            <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
                              {skills.map((skill, i) => (
                                  <button key={i} className={'limb-target-btn' + (selectedSkill === skill.name ? ' limb-target-selected' : '')}
                                    onClick={() => {
                                      if (skill.name === 'Load') { startReload(); return }
                                      if (skill.name === 'Unload') { unloadPistol(); return }
                                      setSelectedSkill(skill.name); setAttackEnemy(null)
                                      if (limbSelect === 'pistol') { setAimMode(true); setLimbSelect(null) }
                                    }}
                                  onMouseEnter={() => setHoveredSkill(skill.name)} onMouseLeave={() => setHoveredSkill(null)}>
                                  <div style={{ fontWeight: 'bold' }}>{skill.name}</div>
                                </button>
                              ))}
                            </div>
                          </div>
                        )}
                        {/* reload: ammo selection */}
                        {selectedSkill === 'Load' && (
                          <div>
                            <div className="inv-parts-title" style={{ marginBottom: 8 }}>Select Ammo Type</div>
                            <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
                              {Object.entries(actorChar?.itemCounts ?? {})
                                .filter(([id, qty]) => qty > 0 && (id === 'pistol_ammo' || id === 'bouncing_ammo'))
                                .length === 0
                                ? <div style={{ color: '#6b7280', padding: 12 }}>No ammo available</div>
                                : Object.entries(actorChar?.itemCounts ?? {})
                                  .filter(([id, qty]) => qty > 0 && (id === 'pistol_ammo' || id === 'bouncing_ammo'))
                                  .map(([id, qty]) => (
                                    <div key={id} className="tree-node leaf" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '10px 12px', cursor: 'pointer', borderRadius: 3, marginBottom: 4 }}
                                      onMouseEnter={e => e.currentTarget.style.background = '#1e1e30'}
                                      onMouseLeave={e => e.currentTarget.style.background = 'none'}
                                      onClick={() => loadAmmoType(id)}>
                                      <span style={{ fontWeight: 'bold' }}>{itemMap[id]?.name ?? id}</span>
                                      <span style={{ color: '#9ca3af' }}>×{qty}</span>
                                    </div>
                                  ))
                              }
                            </div>
                          </div>
                        )}
                        {/* enemy list (hidden during reload) */}
                        {selectedSkill && selectedSkill !== 'Load' && (
                          <div>
                            <div className="inv-parts-title" style={{ marginBottom: 8 }}>Select Enemy</div>
                            <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
                              {(combat?.enemyIds ?? []).map((eid, i) => {
                                const enemy = combat.characters[eid]
                                const inRange = canReach(actorPos, enemy.pos, limbSelect, selectedSkill)
                                return (
                                  <button key={eid} className={'limb-target-btn' + (attackEnemy === i ? ' limb-target-selected' : '')}
                                    onClick={() => setAttackEnemy(i)} style={{ opacity: inRange ? 1 : 0.4 }} disabled={!inRange}
                                    onMouseEnter={() => setHoveredEnemy(i)} onMouseLeave={() => setHoveredEnemy(null)}>
                                    <div style={{ fontWeight: 'bold' }}>{enemy.name}</div>
                                    <div style={{ fontSize: 11, color: '#6b7280' }}>HP: {Object.values(enemy.integrity).reduce((a,b)=>a+b,0)}</div>
                                  </button>
                                )
                              })}
                            </div>
                          </div>
                        )}
                        {/* limb selector (hidden during reload) */}
                        {selectedSkill !== 'Load' && attackEnemy !== null && (
                          <div>
                            <div className="inv-parts-title" style={{ marginBottom: 8 }}>Limb</div>
                            <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
                              {[{ key:'torso',label:'Torso'},{ key:'head',label:'Head'},{ key:'larm',label:'Left Arm'},{ key:'rarm',label:'Right Arm'},{ key:'lleg',label:'Left Leg'},{ key:'rleg',label:'Right Leg'}].map(({key,label}) => (
                                <button key={key} className={'limb-target-btn' + (selectedTarget === (partMap[key]||key) ? ' limb-target-selected' : '')}
                                  onClick={() => selectLimbTarget(key)} style={{ whiteSpace: 'nowrap', padding: '6px 16px' }}>
                                  {label}
                                </button>
                              ))}
                            </div>
                          </div>
                        )}
                      </div>

                      {/* side info panel */}
                      {skills.length > 0 && (
                        <div style={{ position: 'absolute', right: 0, top: 0, bottom: 0, width: 220, background: '#1a1a2e', border: '1px solid #2d2d44', borderRadius: 6, padding: 14 }}>
                          {(() => {
                            if (!selectedSkill) {
                              const s = skills.find(x => x.name === hoveredSkill)
                              if (!s) return null
                              const descs = { blunt: 'Blunt force trauma', slash: 'Slashing cut', pierce: 'Piercing strike', shove: 'Knocks the target back' }
                              return (<>
                                <div style={{ background: '#16162a', border: '1px solid #2d2d44', borderRadius: 4, padding: '8px 12px', marginBottom: 12 }}>
                                  <div style={{ color: '#fbbf24', fontWeight: 'bold', fontSize: 14 }}>{s.name}</div>
                                  <div style={{ color: '#9ca3af', fontSize: 12, marginTop: 4 }}>{descs[s.damageType] || s.damageType}</div>
                                </div>
                                <div style={{ fontSize: 13, color: '#d1d5db', lineHeight: 2 }}>
                                  <div><span style={{ color: '#6b7280' }}>Damage: </span><span style={{ color: '#ef4444' }}>{Math.round((weapon?.damage ?? 0) * s.mult)}</span></div>
                                  <div><span style={{ color: '#6b7280' }}>Multiplier: </span><span>{s.mult}x</span></div>
                                  <div><span style={{ color: '#6b7280' }}>Type: </span><span style={{ color: '#a78bfa' }}>{s.damageType}</span></div>
                                  <div><span style={{ color: '#6b7280' }}>Range: </span><span>{s.range}</span></div>
                                </div>
                              </>)
                            }
                            if (attackEnemy === null) {
                              const e = hoveredEnemy !== null && combat ? combat.characters[combat.enemyIds[hoveredEnemy]] : null
                              if (!e) return null
                              return (<>
                                <div style={{ background: '#16162a', border: '1px solid #2d2d44', borderRadius: 4, padding: '8px 12px', marginBottom: 12 }}>
                                  <div style={{ color: '#22c55e', fontWeight: 'bold', fontSize: 14 }}>{e.name}</div>
                                </div>
                                <div style={{ fontSize: 13, color: '#d1d5db', lineHeight: 2 }}>
                                  <div><span style={{ color: '#6b7280' }}>HP: </span><span style={{ color: '#ef4444' }}>{Object.values(e.integrity).reduce((a,b)=>a+b,0)}</span></div>
                                  {Object.entries(e.integrity).map(([p,v]) => <div key={p}><span style={{ color: '#6b7280' }}>{bodyPartLabels[p]}: </span><span>{v}</span></div>)}
                                </div>
                              </>)
                            }
                            const e = combat ? combat.characters[combat.enemyIds[attackEnemy]] : null
                            const limbKey = selectedTarget
                            const hp = limbKey && e ? e.integrity[limbKey] : null
                            return (<>
                              <div style={{ background: '#16162a', border: '1px solid #2d2d44', borderRadius: 4, padding: '8px 12px', marginBottom: 12 }}>
                                <div style={{ color: '#fbbf24', fontWeight: 'bold', fontSize: 14 }}>{e?.name || ''}</div>
                                <div style={{ color: '#9ca3af', fontSize: 12, marginTop: 4 }}>Target: {selectedTarget ? bodyPartLabels[selectedTarget] : 'None'}</div>
                              </div>
                              <div style={{ fontSize: 13, color: '#d1d5db', lineHeight: 2 }}>
                                <div><span style={{ color: '#6b7280' }}>Limb HP: </span><span style={{ color: '#ef4444' }}>{hp !== null ? hp : '-'}</span></div>
                              </div>
                            </>)
                          })()}
                        </div>
                      )}
                    </div>
                  )
                })()}
              </div>
              <div style={{ display: 'flex', gap: 8 }}>
                {limbSelect !== 'pistol' && <button className="limb-confirm-btn" onClick={executeAttack}
                  disabled={!selectedSkill || attackEnemy === null || !selectedTarget || busy || (resolvedActorChar ? isDead(resolvedActorChar.integrity) : false)}
                  style={{ opacity: (!selectedSkill || attackEnemy === null || !selectedTarget || busy || (resolvedActorChar ? isDead(resolvedActorChar.integrity) : false)) ? 0.4 : 1 }}>Confirm</button>}
                <button className="limb-cancel-btn" onClick={cancelAttack} style={{ width: 'auto', padding: '10px 24px' }}>Cancel</button>
              </div>
            </div>
            )
          })()}
        </>
      )}

      {/* ═══════════════════════════════════════════════════════ WORLD MAP */}
      {(!combat || showWorldMap) && (
        <>
          <svg ref={svgRef} viewBox="0 0 800 600" className="map-svg"
            onWheel={handleWheel}
            >
            <rect width="800" height="600" fill="transparent"
              onPointerDown={handleBgPointerDown}
              onPointerMove={handleBgPointerMove}
              onPointerUp={handleBgPointerUp}
              style={{ cursor: isPanning.current ? 'grabbing' : 'grab' }} />
            <g style={{ transform: `translate(${view.x}px, ${view.y}px) scale(${view.scale})`, transformOrigin: '0 0', willChange: 'transform' }}>
              {connections.map((c, i) => {
                const from = locMap[c.from], to = locMap[c.to]
                return <line key={i} x1={from.x} y1={from.y} x2={to.x} y2={to.y} stroke={selected ? '#6b7280' : '#5F71C5'} strokeWidth={selected ? 2 : 3} />
              })}
              {locations.map(l => (
                <g key={l.id}>
                  <circle cx={l.x} cy={l.y} r={20}
                    fill={selected === l.id ? '#1e2642' : '#1a1a2e'}
                    stroke="#5F71C5" strokeWidth={selected === l.id ? 4 : 3}
                    style={{ cursor: 'pointer', transition: 'all 0.2s' }}
                    onClick={() => setSelected(selected === l.id ? null : l.id)}
                    onMouseEnter={() => setHovered(l.id)}
                    onMouseLeave={() => setHovered(null)} />
                  <text x={l.x} y={l.y + 6} textAnchor="middle" fill="#5F71C5" fontSize={16} fontWeight="bold" pointerEvents="none">{l.id}</text>
                  {!travelAnim && player.location === l.id && (
                    <><circle cx={l.x} cy={l.y - 28} r={6} fill="#22c55e" stroke="#22c55e" strokeWidth={2} />
                    <text x={l.x} y={l.y - 25} textAnchor="middle" fill="#22c55e" fontSize={9} fontWeight="bold" pointerEvents="none">P</text></>
                  )}
                </g>
              ))}
              {travelAnim && (
                <circle cx={travelAnim.from.x + (travelAnim.to.x - travelAnim.from.x) * travelAnim.progress}
                  cy={travelAnim.from.y + (travelAnim.to.y - travelAnim.from.y) * travelAnim.progress - 28}
                  r={6} fill="#22c55e" stroke="#22c55e" strokeWidth={2} />
              )}
            </g>
          </svg>

          {passTimeOpen && (
            <div className="pass-time-panel">
              <div className="pass-time-header">Pass Time</div>
              <div className="pass-time-slider-row">
                <input type="range" min="1" max="24" value={passTimeHours} onChange={e => setPassTimeHours(Number(e.target.value))} className="pass-time-slider" />
                <span className="pass-time-label">{passTimeHours}h</span>
              </div>
              <div className="pass-time-btns">
                <button className="pass-time-confirm" disabled={skipAnim !== null} onClick={() => {
                  const totalH = gameTime.day * 24 + gameTime.hour
                  const target = totalH + passTimeHours
                  setGameTime(gt => { let h = gt.hour + passTimeHours, d = gt.day; if (h >= 24) { d += Math.floor(h/24); h = h%24 } return { day: d, hour: h, minute: 0 } })
                  setSkipAnim({ current: totalH, target, start: performance.now() })
                  setPassTimeOpen(false)
                }}>Skip</button>
                <button className="pass-time-cancel" onClick={() => setPassTimeOpen(false)}>Cancel</button>
              </div>
            </div>
          )}

          <div className="bottom-left-btns">
            <button className="fight-rand-btn" onClick={() => {
              const shuffled = [...liveCharacters.filter(c => c.id !== 'player')].sort(() => Math.random() - 0.5)
              const picked = shuffled.slice(0, 2).map((c, i) => i === 0 ? { ...c, weapon: 'rusty_sword' } : c)
              setSelected(null); startFight(picked)
            }}>Fight 2 random enemies</button>
          </div>

          {selected && (
            <div className="info-panel">
              <div className="info-name">{locMap[selected].name}</div>
              <div className="info-desc">{locMap[selected].description}</div>
              {player.location !== selected && connectedTo(player.location).includes(selected) && (
                <div className="travel-section">
                  <div className="travel-label">Travel here</div>
                  <button className="travel-btn go-btn" onClick={() => travel(selected)}>Go to {locMap[selected].name}</button>
                </div>
              )}
              <div className="characters-section">
                <div className="characters-label">Characters here</div>
                {liveCharacters.filter(c => c.location === selected).length === 0
                  ? <div className="characters-empty">None</div>
                  : liveCharacters.filter(c => c.location === selected).map(c => (
                    <div key={c.id} className="character-entry" onContextMenu={e => { e.preventDefault(); e.stopPropagation(); setHoverInfo(null); setHoveredChar(null); setContextMenu({ x: e.clientX, y: e.clientY, charId: c.id }) }}>
                      <div className="character-name" style={{ cursor: 'pointer' }} onClick={() => { setShowInventory(true); setPanelTarget(c.id) }}>{c.name}</div>
                      <div className="character-details">
                        {c.money !== undefined && <span className="char-money">{c.money}g</span>}
                        {c.inventory?.length > 0 && <span className="char-inv">{c.inventory.map(id => itemMap[id]?.name ?? id).join(', ')}</span>}
                      </div>
                    </div>
                  ))
                }
              </div>
              <button className="close-btn" onClick={() => setSelected(null)}>Close</button>
            </div>
          )}


        </>
      )}

      {/* ═══════════════════════════════════════════════════════ INVENTORY PANEL */}
      {showInventory && (() => {
        const character = panelTarget === 'player'
          ? player
          : combat && combat.characters[panelTarget]
            ? combat.characters[panelTarget]
            : (() => { const npc = characters.find(c => c.id === panelTarget); return npc ? createCharacter(npc) : null })()
        const isPlayer = character?.id === 'player'
        const charWeight = character?.inventory?.reduce((sum, id) => sum + (itemMap[id]?.weight ?? 0) * ((character.itemCounts?.[id] ?? 0) || 1), 0) ?? 0
        const charHp = character ? totalHp(character.integrity) : 0
        return (
        <div className="inventory-overlay"
          onClick={() => { setShowInventory(false); setPanelTarget('player'); setHoverInfo(null); setContextMenu(null) }}
onContextMenu={e => { e.preventDefault(); setHoverInfo(null); setHoveredChar(null); setContextMenu(null) }}
          >
          <div className="inventory-panel" onClick={e => e.stopPropagation()}>
            <button className="inv-close" onClick={() => { setShowInventory(false); setPanelTarget('player'); setHoverInfo(null) }}>X</button>
            <div className="inv-layout">
              <div className="inv-body-col">
                {character && <BodyImage body={character.integrity} />}
              </div>
              <div className="inv-right-col">
                <div className="inv-tabs">
                  <button className={`inv-tab ${invTab === 'health'    ? 'active' : ''}`} onClick={() => setInvTab('health')}>Health</button>
                  <button className={`inv-tab ${invTab === 'inventory' ? 'active' : ''}`} onClick={() => setInvTab('inventory')}>Inventory</button>
                  <button className={`inv-tab ${invTab === 'stats'     ? 'active' : ''}`} onClick={() => setInvTab('stats')}>Statistics</button>
                </div>

                {character && invTab === 'health' && (
                  <div className="inv-parts-col">
                    <div className="inv-parts-title">Body Parts</div>
                    {Object.keys(MAX_BODY).map(part => {
                      const hp = character.integrity[part], max = MAX_BODY[part]
                      const injuries = character.injuries[part] ?? []
                      return (
                        <div key={part}>
                          <div className="part-row">
                            <div className="part-color" style={{ background: partColor(hp, max) }} />
                            <div className="part-label">{bodyPartLabels[part]}</div>
                            <div className="part-bar-track"><div className="part-bar-fill" style={{ width: `${(hp/max)*100}%`, background: partColor(hp, max) }} /></div>
                            <div className="part-hp">{hp}/{max}</div>
                          </div>
                          {injuries.length > 0 && <div className="part-injuries">{injuries.map((inj,i) => <span key={i} className={`part-injury part-injury--${inj.type}`}>{inj.type} ({inj.severity})</span>)}</div>}
                        </div>
                      )
                    })}
                    <div className="inv-total-row"><span>Integrity</span><span>{charHp}/{MAX_HP}</span></div>
                  </div>
                )}
                {character && invTab === 'inventory' && (
                  <div className="inv-inv-content">
                    {isPlayer && (
                      <>
                        <div className="inv-equip-section">
                          <div className="inv-parts-title">Weapons</div>
                          {(() => {
                            const lw = character.equipped.left, rw = character.equipped.right
                            if (lw === rw) return (
                              <div className="equip-row" onMouseEnter={() => setHoverInfo({ type:'weapon', id:lw })} onMouseLeave={() => setHoverInfo(null)}>
                                <span className="equip-name">{lw === 'fists' ? 'Fists' : `Both: ${weaponMap[lw].name}`}</span>
                                {lw !== 'fists' && <><span className="equip-stat">DMG: {weaponMap[lw].damage}</span><span className="equip-stat">COND: {character.weaponConditions[lw] ?? weaponMap[lw].maxCondition}/{weaponMap[lw].maxCondition}</span>{weaponMap[lw].passives.length > 0 && <span className="equip-passives">{weaponMap[lw].passives.join(', ')}</span>}<button className="equip-unequip-btn" onClick={unequipBoth}>Unequip</button></>}
                              </div>
                            )
                            return ['left','right'].map(side => {
                              const wid = character.equipped[side]
                              if (wid === 'fists') return null
                              return (
                                <div key={side} className="equip-row" onMouseEnter={() => setHoverInfo({ type:'weapon', id:wid })} onMouseLeave={() => setHoverInfo(null)}>
                                  <span className="equip-name">{side === 'left' ? 'Left' : 'Right'}: {weaponMap[wid].name}</span>
                                  <span className="equip-stat">DMG: {weaponMap[wid].damage}</span>
                                  <span className="equip-stat">COND: {character.weaponConditions[wid] ?? weaponMap[wid].maxCondition}/{weaponMap[wid].maxCondition}</span>
                                  {weaponMap[wid].passives.length > 0 && <span className="equip-passives">{weaponMap[wid].passives.join(', ')}</span>}
                                  <button className="equip-unequip-btn" onClick={() => unequipWeapon(side)}>Unequip</button>
                                </div>
                              )
                            })
                          })()}
                        </div>
                        <div className="inv-divider" />
                      </>
                    )}
                    {!isPlayer && character.weapon && character.weapon !== 'fists' && (
                      <div className="inv-equip-section">
                        <div className="inv-parts-title">Equipped Weapon</div>
                        {(() => {
                          const wep = weaponMap[character.weapon]
                          return (
                            <div className="equip-row" onMouseEnter={() => setHoverInfo({ type:'weapon', id:character.weapon })} onMouseLeave={() => setHoverInfo(null)}>
                              <span className="equip-name">{wep?.name ?? character.weapon}</span>
                              {wep && <><span className="equip-stat">DMG: {character.attack}</span><span className="equip-stat">SPD: {character.speed}</span>{wep.passives?.length > 0 && <span className="equip-passives">{wep.passives.join(', ')}</span>}</>}
                            </div>
                          )
                        })()}
                      </div>
                    )}
                    {!isPlayer && character.weapon && character.weapon !== 'fists' && <div className="inv-divider" />}
                    <div className="inv-parts-title">Inventory ({character.inventory.length})</div>
                    {character.inventory.length === 0
                      ? <div className="inv-empty">Empty</div>
                      : character.inventory.map((itemId, i) => {
                        const qty = character.itemCounts?.[itemId]
                        if (qty !== undefined && qty <= 0) return null
                        return (
                          <div key={i} className="inv-item" onMouseEnter={() => setHoverInfo({ type:'item', id:itemId })} onMouseLeave={() => setHoverInfo(null)}>
                            {itemMap[itemId]?.name ?? itemId}
                            {qty !== undefined
                              ? <span style={{ marginLeft: 6, color: '#9ca3af' }}>×{qty}</span>
                              : itemMap[itemId] && <span style={{ marginLeft: 6, color: '#9ca3af' }}>×1</span>}
                            {isPlayer && weaponMap[itemId] && <button className="equip-btn" onClick={() => setEquipArmChoice(itemId)}>Equip</button>}
                          </div>
                        )
                      })
                    }
                    {isPlayer && <div className="inv-weight">Weight: {charWeight}/{MAX_WEIGHT}</div>}
                    <div className="inv-money">{character.money || 0}g</div>
                  </div>
                )}
                {character && invTab === 'stats' && (
                  <div className="inv-stats-content">
                    <div className="inv-parts-title">{isPlayer ? 'Character' : character.name}</div>
                    <div className="stat-row"><span className="stat-label">Name</span><span className="stat-value">{isPlayer ? 'You' : character.name}</span></div>
                    <div className="stat-row"><span className="stat-label">Age</span><span className="stat-value">{character.age ?? '??'}</span></div>
                    {isPlayer && <div className="stat-row"><span className="stat-label">Size</span><span className="stat-value">{character.size ?? 1}x</span></div>}
                    <div className="stat-row"><span className="stat-label">Location</span><span className="stat-value">{locMap[character.location]?.name ?? character.location}</span></div>
                    <div className="inv-divider" />
                    <div className="inv-divider" />
                    <div className="stat-row"><span className="stat-label">Integrity</span><span className="stat-value">{charHp}/{MAX_HP}</span></div>
                    <div className="stat-row"><span className="stat-label">Items</span><span className="stat-value">{character.inventory.length}</span></div>
                    {isPlayer && <div className="stat-row"><span className="stat-label">Weight</span><span className="stat-value">{charWeight}/{MAX_WEIGHT}</span></div>}
                    <div className="stat-row"><span className="stat-label">Money</span><span className="stat-value">{character.money || 0}g</span></div>
                  </div>
                )}
              </div>
            </div>
          </div>
        </div>
      )})()}

      {/* ═══════════════════════════════════════════════════════ SETTINGS */}
      {showSettings && (
        <div className="settings-sidebar">
          <div className="settings-header">Settings<button className="settings-close" onClick={() => setShowSettings(false)}>X</button></div>
          <div className="settings-body">
            <div className="settings-section">
              <div className="settings-section-title">Display</div>
              <div className="settings-row"><span className="settings-label">Zoom</span><div className="settings-zoom-group"><button className="settings-zoom-btn" onClick={zoomOut}>−</button><span className="settings-zoom-val">{Math.round(view.scale*100)}%</span><button className="settings-zoom-btn" onClick={zoomIn}>+</button></div></div>
            </div>
            <div className="settings-section">
              <div className="settings-section-title">Game</div>
              <div className="settings-row"><span className="settings-label">Date</span><span className="settings-value">{getDateString(gameTime.day)}</span></div>
              <div className="settings-row"><span className="settings-label">Time</span><span className="settings-value">{String(gameTime.hour).padStart(2,'0')}:{String(Math.floor(gameTime.minute ?? 0)).padStart(2,'0')}</span></div>
              <div className="settings-row"><span className="settings-label">Weather</span><span className="settings-value">{weatherLabels[weather]}</span></div>
            </div>
            <div className="settings-section">
              <div className="settings-section-title">Player</div>
              <div className="settings-row"><span className="settings-label">Location</span><span className="settings-value">{locMap[player.location]?.name ?? player.location}</span></div>
              <div className="settings-row"><span className="settings-label">Money</span><span className="settings-value">{player.money}g</span></div>
            </div>
          </div>
        </div>
      )}

      {/* ═══════════════════════════════════════════════════════ WORLD INFO */}
      {showWorldInfo && (
        <div className="inventory-overlay" onClick={() => setShowWorldInfo(false)}>
          <div className="world-info-panel" onClick={e => e.stopPropagation()}>
            <button className="inv-close" onClick={() => setShowWorldInfo(false)}>X</button>
            <div className="inv-parts-title" style={{ marginBottom: 16 }}>World</div>
            <div className="world-info-grid">
              <div className="info-row"><span className="info-label">Date</span><span className="info-value">{getDateString(gameTime.day)}</span></div>
              <div className="info-row"><span className="info-label">Time</span><span className="info-value">{String(gameTime.hour).padStart(2,'0')}:{String(Math.floor(gameTime.minute ?? 0)).padStart(2,'0')}</span></div>
              <div className="info-row"><span className="info-label">Weather</span><span className="info-value">{weatherLabels[weather]}</span></div>
              <div className="info-row"><span className="info-label">Location</span><span className="info-value">{locMap[player.location]?.name ?? player.location}</span></div>
            </div>
          </div>
        </div>
      )}

      {/* ═══════════════════════════════════════════════════════ TOOLTIPS */}
      {hovered && (
        <div className="tooltip" style={{ left: mousePos.x + 16, top: mousePos.y }}>
          <div className="tooltip-name">{locMap[hovered].name}</div>
          <div className="tooltip-desc">{locMap[hovered].description}</div>
        </div>
      )}
      {hoverInfo?.type === 'item' && itemMap[hoverInfo.id] && (
        <div className="tooltip" style={{ left: mousePos.x + 16, top: mousePos.y }}>
          <div className="tooltip-name">{itemMap[hoverInfo.id].name}</div>
          <div className="tooltip-desc">{itemMap[hoverInfo.id].description}</div>
        </div>
      )}
      {hoverInfo?.type === 'weapon' && weaponMap[hoverInfo.id] && (
        <div className="tooltip" style={{ left: mousePos.x + 16, top: mousePos.y }}>
          {hoverInfo.id === 'left_arm' || hoverInfo.id === 'right_arm'
            ? <><div className="tooltip-name">{weaponMap[hoverInfo.id].name}</div><div className="tooltip-desc">Unarmed strike (DMG: 10)</div></>
            : <><div className="tooltip-name">{weaponMap[hoverInfo.id].name}</div><div className="tooltip-desc">DMG: {weaponMap[hoverInfo.id].damage} SPD: {weaponMap[hoverInfo.id].speed}</div>
               <div className="tooltip-desc">Condition: {player.weaponConditions[hoverInfo.id] ?? weaponMap[hoverInfo.id].maxCondition}/{weaponMap[hoverInfo.id].maxCondition}</div>
               <div className="tooltip-desc">Skills: {weaponMap[hoverInfo.id].skills.map(s=>`${s.name} (${s.damageType})`).join(', ')}</div>
               {weaponMap[hoverInfo.id].passives.length > 0 && <div className="tooltip-desc">Passives: {weaponMap[hoverInfo.id].passives.join(', ')}</div>}</>
          }
        </div>
      )}

      {/* ═══════════════════════════════════════════════════════ TRADE WINDOW */}
      {showTrade && (() => {
        const srcChar = player
        const tgtChar = (() => {
          if (tradeTarget === 'player') return player
          if (combat && combat.characters[tradeTarget]) return combat.characters[tradeTarget]
          const npc = characters.find(c => c.id === tradeTarget)
          return npc ? createCharacter(npc) : null
        })()
        if (!srcChar || !tgtChar) return null
        function getCharItems(ch) {
          const seen = new Set()
          const items = []
          for (const id of (ch.inventory ?? [])) {
            if (seen.has(id)) continue; seen.add(id)
            items.push({ id, name: weaponMap[id]?.name ?? itemMap[id]?.name ?? id, qty: 1, value: getItemValue(id), cond: ch.weaponConditions?.[id] ?? weaponMap[id]?.maxCondition ?? null, maxCond: weaponMap[id]?.maxCondition ?? null })
          }
          for (const [id, qty] of Object.entries(ch.itemCounts ?? {})) {
            if (qty > 0) {
              if (seen.has(id)) continue; seen.add(id)
              items.push({ id, name: itemMap[id]?.name ?? id, qty, value: getItemValue(id) })
            }
          }
          return items
        }
        const srcItems = getCharItems(srcChar)
        const tgtItems = getCharItems(tgtChar)
        const giveVal = Object.entries(tradeGive).reduce((s, [id, q]) => s + getItemValue(id) * q, 0)
        const takeVal = Object.entries(tradeTake).reduce((s, [id, q]) => s + getItemValue(id) * q, 0)
        const proposeTrade = () => {
          const giveEntries = Object.entries(tradeGive).filter(([id, q]) => q > 0)
          const takeEntries = Object.entries(tradeTake).filter(([id, q]) => q > 0)
          if (giveEntries.length === 0 && takeEntries.length === 0) return
          const newPlayer = { ...player }
          const tgtWorld = characters.find(c => c.id === tradeTarget)
          if (tgtWorld) {
            tgtWorld.inventory = [...(tgtWorld.inventory ?? [])]
            tgtWorld.itemCounts = { ...(tgtWorld.itemCounts ?? {}) }
            tgtWorld.weaponConditions = { ...(tgtWorld.weaponConditions ?? {}) }
          }
          for (const [id, qty] of giveEntries) {
            if (weaponMap[id]) {
              // weapon: remove from player inventory, give to target
              newPlayer.inventory = newPlayer.inventory.filter(i => i !== id)
              if (tgtWorld) {
                tgtWorld.inventory = tgtWorld.inventory ?? []
                tgtWorld.inventory.push(id)
                if (newPlayer.weaponConditions?.[id] != null) {
                  tgtWorld.weaponConditions[id] = newPlayer.weaponConditions[id]
                  delete newPlayer.weaponConditions[id]
                }
              }
            } else if (itemMap[id]) {
              // ammo/item: reduce player count, increase target count
              newPlayer.itemCounts = { ...(newPlayer.itemCounts ?? {}) }
              newPlayer.itemCounts[id] = (newPlayer.itemCounts[id] ?? 0) - qty
              if (newPlayer.itemCounts[id] <= 0) delete newPlayer.itemCounts[id]
              if (tgtWorld) {
                const tgt = tgtWorld
                tgt.inventory = [...(tgt.inventory ?? []), id]
                tgt.itemCounts = { ...(tgt.itemCounts ?? {}) }
                tgt.itemCounts[id] = (tgt.itemCounts[id] ?? 0) + qty
              }
            }
          }
          for (const [id, qty] of takeEntries) {
            if (weaponMap[id]) {
              // weapon: remove from target inventory, give to player
              if (tgtWorld) {
                tgtWorld.inventory = tgtWorld.inventory.filter(i => i !== id)
                if (tgtWorld.weaponConditions?.[id] != null) {
                  newPlayer.weaponConditions = { ...(newPlayer.weaponConditions ?? {}) }
                  newPlayer.weaponConditions[id] = tgtWorld.weaponConditions[id]
                  delete tgtWorld.weaponConditions[id]
                }
              }
              newPlayer.inventory = [...(newPlayer.inventory ?? []), id]
            } else if (itemMap[id]) {
              // ammo/item: reduce target count (from itemCounts and/or inventory)
              if (tgtWorld) {
                const tgt = tgtWorld
                tgt.inventory = (tgt.inventory ?? []).filter(i => i !== id)
                tgt.itemCounts = { ...(tgt.itemCounts ?? {}) }
                tgt.itemCounts[id] = (tgt.itemCounts[id] ?? 0) - qty
                if (tgt.itemCounts[id] <= 0) delete tgt.itemCounts[id]
              }
              newPlayer.itemCounts = { ...(newPlayer.itemCounts ?? {}) }
              newPlayer.itemCounts[id] = (newPlayer.itemCounts[id] ?? 0) + qty
            }
          }
          setPlayer(newPlayer)
          setShowTrade(false)
          setTradeTarget(null)
          setTradeGive({})
          setTradeTake({})
        }
        return (
        <div className="trade-overlay" onClick={() => { setShowTrade(false); setTradeTarget(null) }}>
          <div className="trade-panel" onClick={e => e.stopPropagation()}>
            <button className="trade-close" onClick={() => { setShowTrade(false); setTradeTarget(null) }}>X</button>
            <div className="trade-title">Trade with {tgtChar.name ?? tradeTarget}</div>
            <div className="trade-layout">
              <div className="trade-col trade-col-left">
                <div className="trade-col-header">{srcChar.name} Offers</div>
                <div className="trade-items">
                  {srcItems.map(item => (
                    <div key={item.id} className="trade-item">
                      <span className="trade-item-name">{item.name}{item.qty > 1 ? ` (${item.qty})` : ''}{item.cond != null ? ` [${item.cond}/${item.maxCond}]` : ''}</span>
                      <div className="trade-qty-controls">
                        <button className="trade-qty-btn" onClick={() => setTradeGive(prev => ({ ...prev, [item.id]: Math.max(0, (prev[item.id] ?? 0) - 1) }))}>-</button>
                        <span className="trade-qty">{tradeGive[item.id] ?? 0}</span>
                        <button className="trade-qty-btn" onClick={() => setTradeGive(prev => ({ ...prev, [item.id]: Math.min(item.qty, (prev[item.id] ?? 0) + 1) }))}>+</button>
                      </div>
                    </div>
                  ))}
                </div>
              </div>
              <div className="trade-col trade-col-right">
                <div className="trade-col-header">{tgtChar.name} Offers</div>
                <div className="trade-items">
                  {tgtItems.map(item => (
                    <div key={item.id} className="trade-item">
                      <span className="trade-item-name">{item.name}{item.qty > 1 ? ` (${item.qty})` : ''}{item.cond != null ? ` [${item.cond}/${item.maxCond}]` : ''}</span>
                      <div className="trade-qty-controls">
                        <button className="trade-qty-btn" onClick={() => setTradeTake(prev => ({ ...prev, [item.id]: Math.max(0, (prev[item.id] ?? 0) - 1) }))}>-</button>
                        <span className="trade-qty">{tradeTake[item.id] ?? 0}</span>
                        <button className="trade-qty-btn" onClick={() => setTradeTake(prev => ({ ...prev, [item.id]: Math.min(item.qty, (prev[item.id] ?? 0) + 1) }))}>+</button>
                      </div>
                    </div>
                  ))}
                </div>
              </div>
            </div>
            <div className="trade-bar">
              <span className="trade-bar-label">Give: <span className="trade-give-val">{giveVal}g</span> &nbsp; Take: <span className="trade-take-val">{takeVal}g</span></span>
            </div>
            <button className="trade-propose-btn" onClick={proposeTrade}>Propose Trade</button>
          </div>
        </div>
        )
      })()}

      {hoverInfo?.type === 'enemy' && (
        <div className="tooltip" style={{ left: mousePos.x + 16, top: mousePos.y }}>
          <div className="tooltip-name">{hoverInfo.name}</div>
          <div className="tooltip-desc">{hoverInfo.defeated ? 'Defeated' : 'Hostile'}</div>
        </div>
      )}
      {hoveredChar && !hoveredChar.startsWith('enemy-') && combat?.characters[hoveredChar] && (
        <div className="tooltip" style={{ left: mousePos.x + 16, top: mousePos.y }}>
          <div className="tooltip-name">{combat.characters[hoveredChar].name ?? hoveredChar}</div>
        </div>
      )}

      {/* ═══════════════════════════════════════════════════════ EQUIP ARM CHOICE */}
      {equipArmChoice && (
        <div className="arm-choice-overlay" onClick={() => setEquipArmChoice(null)}>
          <div className="arm-choice-panel" onClick={e => e.stopPropagation()}>
            <div className="arm-choice-title">Equip {weaponMap[equipArmChoice].name}</div>
            <div className="arm-choice-btns">
              <button className="arm-choice-btn" onClick={() => equipFromInventory(equipArmChoice, 'left')}>Left Arm</button>
              <button className="arm-choice-btn" onClick={() => equipFromInventory(equipArmChoice, 'right')}>Right Arm</button>
              <button className="arm-choice-btn" onClick={() => equipFromInventory(equipArmChoice, 'both')}>Both Arms</button>
            </div>
            <button className="arm-choice-cancel" onClick={() => setEquipArmChoice(null)}>Cancel</button>
          </div>
        </div>
      )}

      {/* ═══════════════════════════════════════════════════════ CONTEXT MENU */}
      {contextMenu && (
        <div className="context-menu" style={{ left: contextMenu.x, top: contextMenu.y }} onClick={() => { setHoverInfo(null); setHoveredChar(null); setContextMenu(null) }}>
          <button className={`context-menu-btn${canTrade(contextMenu.charId) ? '' : ' disabled'}`} disabled={!canTrade(contextMenu.charId)} onClick={e => { e.stopPropagation(); if (canTrade(contextMenu.charId)) startTrade(contextMenu.charId) }}>Trade</button>
        </div>
      )}
    </div>
  )
}