broken target select
This commit is contained in:
+18
@@ -479,6 +479,24 @@ body {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.tree-leaf-selected {
|
||||
background: #2d2d44;
|
||||
color: #fcd34d;
|
||||
}
|
||||
|
||||
.tree-leaf-selected:hover {
|
||||
background: #3d3d54;
|
||||
}
|
||||
|
||||
.tree-folder-selected {
|
||||
background: #2d2d44;
|
||||
color: #fcd34d !important;
|
||||
}
|
||||
|
||||
.tree-folder-selected:hover {
|
||||
background: #3d3d54;
|
||||
}
|
||||
|
||||
.combat-back {
|
||||
background: none;
|
||||
border: none;
|
||||
|
||||
+182
-217
@@ -561,6 +561,7 @@ export default function App() {
|
||||
const [weaponsOpen, setWeaponsOpen] = useState([])
|
||||
const [itemsOpen, setItemsOpen] = useState([])
|
||||
const [movementOpen, setMovementOpen] = useState([])
|
||||
const [skillDropdownOpen,setSkillDropdownOpen]= useState([])
|
||||
const [limbSelect, setLimbSelect] = useState(null)
|
||||
const [selectedTarget, setSelectedTarget] = useState(null)
|
||||
const [selectedSkill, setSelectedSkill] = useState(null)
|
||||
@@ -598,13 +599,7 @@ export default function App() {
|
||||
// ── notifications
|
||||
const [notifications, setNotifications] = useState([])
|
||||
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => {
|
||||
setNotifications(prev => [...prev, { id: Date.now(), subject: 'Test', description: 'This is a test notification.' }])
|
||||
}, 5000)
|
||||
return () => clearInterval(id)
|
||||
}, [])
|
||||
|
||||
// auto-dismiss a notification every 4s
|
||||
useEffect(() => {
|
||||
if (notifications.length === 0) return
|
||||
const id = setTimeout(() => setNotifications(prev => prev.slice(1)), 4000)
|
||||
@@ -1013,7 +1008,7 @@ export default function App() {
|
||||
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)
|
||||
setLimbSelect(null); setMoveMode(null); setAimMode(false); setSelectedSkill(null); setCurrentActor(null); setSkillDropdownOpen([])
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1817,7 +1812,7 @@ class MoveAction {
|
||||
}
|
||||
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)
|
||||
setAimMode(false); setLimbSelect(null); setSelectedSkill(null); setSelectedTarget(null); setAttackEnemy(null); setCurrentActor(null); setSkillDropdownOpen([])
|
||||
}
|
||||
|
||||
const endFight = () => {
|
||||
@@ -1911,17 +1906,17 @@ class MoveAction {
|
||||
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)
|
||||
setLimbSelect(null); setSelectedSkill(null); setSelectedTarget(null); setAttackEnemy(null); setCurrentActor(null); setSkillDropdownOpen([])
|
||||
return
|
||||
}
|
||||
|
||||
const anim = createWeaponStrikeAnim(action, combatRef.current)
|
||||
characterActionsRef.current[actorId] = anim
|
||||
setLimbSelect(null); setSelectedSkill(null); setSelectedTarget(null); setAttackEnemy(null); setCurrentActor(null)
|
||||
setLimbSelect(null); setSelectedSkill(null); setSelectedTarget(null); setAttackEnemy(null); setCurrentActor(null); setSkillDropdownOpen([])
|
||||
}
|
||||
|
||||
const cancelAttack = () => {
|
||||
setLimbSelect(null); setSelectedSkill(null); setSelectedTarget(null); setAttackEnemy(null); setCurrentActor(null)
|
||||
setLimbSelect(null); setSelectedSkill(null); setSelectedTarget(null); setAttackEnemy(null); setCurrentActor(null); setSkillDropdownOpen([])
|
||||
setAimMode(false)
|
||||
}
|
||||
|
||||
@@ -2012,7 +2007,7 @@ class MoveAction {
|
||||
if (reserve <= 0) return
|
||||
const ammoName = itemMap[ammoId]?.name ?? ammoId
|
||||
new Audio(chamberBulletMp3).play().catch(() => {})
|
||||
setLimbSelect(null); setSelectedSkill(null); setSelectedTarget(null); setAttackEnemy(null)
|
||||
setLimbSelect(null); setSelectedSkill(null); setSelectedTarget(null); setAttackEnemy(null); setSkillDropdownOpen([])
|
||||
setTimeout(() => {
|
||||
if (actorId === 'player') {
|
||||
const newMag = { pistol: [...magazineRef.current.pistol, { ammoType: ammoId }] }
|
||||
@@ -2077,7 +2072,7 @@ class MoveAction {
|
||||
const ammoId = top.ammoType
|
||||
const ammoName = itemMap[ammoId]?.name ?? ammoId
|
||||
new Audio(chamberBulletMp3).play().catch(() => {})
|
||||
setLimbSelect(null); setSelectedSkill(null); setSelectedTarget(null); setAttackEnemy(null)
|
||||
setLimbSelect(null); setSelectedSkill(null); setSelectedTarget(null); setAttackEnemy(null); setSkillDropdownOpen([])
|
||||
setTimeout(() => {
|
||||
if (actorId === 'player') {
|
||||
const newMag = { pistol: mag.pistol.slice(0, -1) }
|
||||
@@ -2203,6 +2198,111 @@ class MoveAction {
|
||||
const partMap = { larm: 'leftArm', rarm: 'rightArm', lleg: 'leftLeg', rleg: 'rightLeg' }
|
||||
const selectLimbTarget = (part) => setSelectedTarget(partMap[part] || part)
|
||||
|
||||
/** Render inline skill → enemy → limb → confirm dropdown inside the action tree */
|
||||
const renderSkillDropdown = (weaponId, actorId) => {
|
||||
const weapon = weaponMap[weaponId]
|
||||
if (!weapon) return null
|
||||
const skills = weapon.skills ?? []
|
||||
const isPistol = weaponId === 'pistol'
|
||||
const ac = combat?.characters[actorId]
|
||||
const actorPos = ac ? (characterPositionsRef.current[actorId] ?? ac.pos) : null
|
||||
return (
|
||||
<div className="tree-children">
|
||||
{skills.map((skill, si) => {
|
||||
const active = selectedSkill === skill.name
|
||||
const isSpecial = skill.name === 'Load' || skill.name === 'Unload'
|
||||
return (
|
||||
<Fragment key={si}>
|
||||
{active && isSpecial ? (
|
||||
<div className="tree-node leaf tree-leaf-selected"
|
||||
onClick={() => {
|
||||
if (skill.name === 'Load') { startReload(); return }
|
||||
if (skill.name === 'Unload') { unloadPistol(); return }
|
||||
}}
|
||||
onMouseEnter={() => setHoveredSkill(skill.name)}
|
||||
onMouseLeave={() => setHoveredSkill(null)}>
|
||||
<span className="tree-bullet">○</span>
|
||||
<span className="tree-label">{skill.name}</span>
|
||||
</div>
|
||||
) : active ? (
|
||||
<div className="tree-node folder tree-folder-selected"
|
||||
onClick={() => {
|
||||
setAttackEnemy(null); setSelectedTarget(null)
|
||||
setAimMode(true)
|
||||
}}
|
||||
onMouseEnter={() => setHoveredSkill(skill.name)}
|
||||
onMouseLeave={() => setHoveredSkill(null)}>
|
||||
<span className="tree-toggle">▼</span>
|
||||
<span className="tree-label">{skill.name}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="tree-node leaf"
|
||||
onClick={() => {
|
||||
if (isSpecial) { if (skill.name === 'Load') { startReload(); return }; if (skill.name === 'Unload') { unloadPistol(); return } }
|
||||
setSelectedSkill(skill.name); setLimbSelect(weaponId); setCurrentActor(actorId)
|
||||
setAttackEnemy(null); setSelectedTarget(null)
|
||||
setAimMode(true)
|
||||
}}
|
||||
onMouseEnter={() => setHoveredSkill(skill.name)}
|
||||
onMouseLeave={() => setHoveredSkill(null)}>
|
||||
<span className="tree-bullet">○</span>
|
||||
<span className="tree-label">{skill.name}</span>
|
||||
</div>
|
||||
)}
|
||||
{/* Targeting / limb selection when this skill is active */}
|
||||
{active && !isSpecial && (
|
||||
<div className="tree-children">
|
||||
{attackEnemy === null ? (
|
||||
<div className="tree-node leaf" style={{ cursor: 'default', color: '#9ca3af', fontSize: 12, fontStyle: 'italic' }}>
|
||||
Click the minimap to target
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{[{ 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 }) => (
|
||||
<div key={key}
|
||||
className={'tree-node leaf' + (selectedTarget === (partMap[key] || key) ? ' tree-leaf-selected' : '')}
|
||||
onClick={() => selectLimbTarget(key)}>
|
||||
<span className="tree-bullet">○</span>
|
||||
<span className="tree-label">{label}</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="tree-node leaf" style={{ cursor: 'default', padding: '6px 8px', gap: 8 }}>
|
||||
<button className="limb-confirm-btn" onClick={executeAttack}
|
||||
disabled={!selectedSkill || attackEnemy === null || !selectedTarget || busy || (ac ? isDead(ac.integrity) : false)}
|
||||
style={{ padding: '4px 14px', fontSize: 12, opacity: (!selectedSkill || attackEnemy === null || !selectedTarget || busy || (ac ? isDead(ac.integrity) : false)) ? 0.4 : 1 }}>
|
||||
Confirm
|
||||
</button>
|
||||
<button className="limb-cancel-btn" onClick={cancelAttack} style={{ padding: '4px 14px', fontSize: 12 }}>Cancel</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
{/* Ammo selection when reloading */}
|
||||
{selectedSkill === 'Load' && (
|
||||
<div className="tree-children">
|
||||
{(() => {
|
||||
const ammoItems = Object.entries(ac?.itemCounts ?? {})
|
||||
.filter(([id, qty]) => qty > 0 && (weaponMap.pistol?.compatibleBulletSizes ?? []).includes(itemMap[id]?.bulletSize))
|
||||
return ammoItems.length === 0
|
||||
? <div className="tree-node leaf" style={{ cursor: 'default', color: '#6b7280', fontSize: 12 }}>No ammo available</div>
|
||||
: ammoItems.map(([id, qty]) => (
|
||||
<div key={id} className="tree-node leaf" onClick={() => loadAmmoType(id)}>
|
||||
<span className="tree-bullet">○</span>
|
||||
<span className="tree-label">{itemMap[id]?.name ?? id}</span>
|
||||
<span style={{ marginLeft: 6, color: '#9ca3af', fontSize: 12 }}>×{qty}</span>
|
||||
</div>
|
||||
))
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
@@ -2369,42 +2469,46 @@ class MoveAction {
|
||||
{weaponsOpen.includes(cid) && (
|
||||
<div className="tree-children">
|
||||
{(() => {
|
||||
const entries = []
|
||||
const makeWeap = (weaponId, label) => {
|
||||
const dropKey = `${cid}-${weaponId}`
|
||||
const isOpen = skillDropdownOpen.includes(dropKey)
|
||||
return (
|
||||
<Fragment key={weaponId}>
|
||||
<div className="tree-node folder"
|
||||
onMouseEnter={() => setHoverInfo({ type: 'weapon', id: weaponId })}
|
||||
onMouseLeave={() => setHoverInfo(null)}
|
||||
onClick={() => {
|
||||
const collapse = skillDropdownOpen.includes(dropKey)
|
||||
setSkillDropdownOpen(prev => collapse ? prev.filter(x => x !== dropKey) : [...prev, dropKey])
|
||||
setCurrentActor(cid); setLimbSelect(collapse ? null : weaponId)
|
||||
setSelectedSkill(null); setSelectedTarget(null); setAttackEnemy(null)
|
||||
setMoveMode(null); setAimMode(false)
|
||||
}}>
|
||||
<span className="tree-toggle">{isOpen ? '▼' : '▶'}</span>
|
||||
<span className="tree-label">{label}</span>
|
||||
</div>
|
||||
{isOpen && renderSkillDropdown(weaponId, cid)}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
const pieces = []
|
||||
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>)
|
||||
pieces.push(makeWeap('left_arm', 'Left Arm'))
|
||||
} 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>)
|
||||
pieces.push(makeWeap(lw, `Left: ${weaponMap[lw]?.name ?? lw}`))
|
||||
}
|
||||
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>)
|
||||
pieces.push(makeWeap('right_arm', 'Right Arm'))
|
||||
} 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>)
|
||||
pieces.push(makeWeap(rw, `Right: ${weaponMap[rw]?.name ?? rw}`))
|
||||
}
|
||||
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>)
|
||||
pieces.push(makeWeap(lw, `Both: ${weaponMap[lw]?.name ?? lw}`))
|
||||
}
|
||||
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>)
|
||||
pieces.push(makeWeap('fists', 'Fists'))
|
||||
}
|
||||
return entries
|
||||
return pieces
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
@@ -2464,17 +2568,19 @@ class MoveAction {
|
||||
|
||||
{/* ── 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-container" style={{ position: 'relative', borderColor: hitShake ? '#ef4444' : undefined, cursor: aimMode && limbSelect === 'pistol' ? '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') }}
|
||||
style={{ cursor: aimMode && limbSelect === 'pistol' ? 'crosshair' : (minimapPanRef.current ? 'grabbing' : 'grab') }}
|
||||
onMouseDown={e => {
|
||||
if (e.button !== 0) return
|
||||
minimapMovedRef.current = false
|
||||
if (limbSelect) return
|
||||
minimapPanRef.current = true; minimapMovedRef.current = false
|
||||
minimapPanRef.current = true
|
||||
minimapPanStart.current = { x: e.clientX, y: e.clientY, vx: minimapView.x, vy: minimapView.y }
|
||||
const svg = e.currentTarget
|
||||
const onMove = ev => {
|
||||
@@ -2496,7 +2602,7 @@ class MoveAction {
|
||||
}}
|
||||
onPointerLeave={() => setMinimapHover(null)}
|
||||
onWheel={e => {
|
||||
if (limbSelect) return; e.preventDefault()
|
||||
if (!moveMode && !aimMode) return; e.preventDefault()
|
||||
const d = e.deltaY > 0 ? 0.9 : 1.1
|
||||
const r = e.currentTarget.getBoundingClientRect()
|
||||
setMinimapView(v => {
|
||||
@@ -2506,12 +2612,29 @@ class MoveAction {
|
||||
}}
|
||||
onClick={e => {
|
||||
if (minimapMovedRef.current) { minimapMovedRef.current = false; return }
|
||||
if (limbSelect || (!moveMode && !aimMode)) return
|
||||
if (!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) {
|
||||
if (selectedSkill !== 'Shoot' || limbSelect !== 'pistol') {
|
||||
const cc = combatRef.current
|
||||
const enemies = cc?.enemyIds ?? []
|
||||
let closest = -1, closestDist = 30
|
||||
enemies.forEach((eid, i) => {
|
||||
const e = cc.characters[eid]
|
||||
if (!e?.pos) return
|
||||
const dx = tx - e.pos.x, dy = ty - e.pos.y
|
||||
const dist = Math.sqrt(dx*dx + dy*dy)
|
||||
if (dist < closestDist) { closestDist = dist; closest = i }
|
||||
})
|
||||
if (closest >= 0) {
|
||||
setAttackEnemy(closest); setSelectedTarget(null)
|
||||
setAimMode(false)
|
||||
}
|
||||
} else {
|
||||
queueShot(tx, ty)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (moveMode === 'turn') {
|
||||
@@ -2557,8 +2680,16 @@ class MoveAction {
|
||||
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 }) }} />
|
||||
onContextMenu={e => { e.preventDefault(); e.stopPropagation(); setHoverInfo(null); setHoveredChar(null); setContextMenu({ x: e.clientX, y: e.clientY, charId: eid }) }}
|
||||
onMouseDown={e => {
|
||||
e.stopPropagation()
|
||||
if (e.button !== 0) return
|
||||
if (aimMode && selectedSkill && limbSelect && limbSelect !== 'pistol') {
|
||||
const cc = combatRef.current
|
||||
const ei = cc?.enemyIds?.indexOf(eid) ?? -1
|
||||
if (ei >= 0) { setAttackEnemy(ei); setSelectedTarget(null); setAimMode(false) }
|
||||
}
|
||||
}} />
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
@@ -2576,7 +2707,7 @@ class MoveAction {
|
||||
stroke="#22c55e" strokeWidth="1.5" strokeDasharray="3 3" opacity="0.7" />
|
||||
)
|
||||
})()}
|
||||
{aimMode && minimapHover && combat && (() => {
|
||||
{aimMode && minimapHover && combat && limbSelect === 'pistol' && (() => {
|
||||
const actorId = currentActor ?? 'player'
|
||||
const ch = combat.characters[actorId]
|
||||
if (!ch?.pos) return null
|
||||
@@ -2625,7 +2756,7 @@ class MoveAction {
|
||||
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) }}
|
||||
onClick={e => { e.stopPropagation() }}
|
||||
onContextMenu={e => { e.preventDefault(); e.stopPropagation(); setHoverInfo(null); setHoveredChar(null); setContextMenu({ x: e.clientX, y: e.clientY, charId: cid }) }} />
|
||||
)
|
||||
})
|
||||
@@ -2639,7 +2770,7 @@ class MoveAction {
|
||||
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) }}
|
||||
onClick={e => { e.stopPropagation() }}
|
||||
onContextMenu={e => { e.preventDefault(); e.stopPropagation(); setHoverInfo(null); setHoveredChar(null); setContextMenu({ x: e.clientX, y: e.clientY, charId: nid }) }} />
|
||||
)
|
||||
})}
|
||||
@@ -2782,174 +2913,7 @@ class MoveAction {
|
||||
</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 && (weaponMap.pistol?.compatibleBulletSizes ?? []).includes(itemMap[id]?.bulletSize))
|
||||
.length === 0
|
||||
? <div style={{ color: '#6b7280', padding: 12 }}>No ammo available</div>
|
||||
: Object.entries(actorChar?.itemCounts ?? {})
|
||||
.filter(([id, qty]) => qty > 0 && (weaponMap.pistol?.compatibleBulletSizes ?? []).includes(itemMap[id]?.bulletSize))
|
||||
.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>
|
||||
)
|
||||
})()}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -3459,6 +3423,7 @@ onContextMenu={e => { e.preventDefault(); setHoverInfo(null); setHoveredChar(nul
|
||||
{/* ═══════════════════════════════════════════════════════ 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" onClick={e => { e.stopPropagation(); setShowInventory(true); setPanelTarget(contextMenu.charId); setContextMenu(null) }}>Inspect</button>
|
||||
<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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user