targeting fixed
This commit is contained in:
@@ -330,12 +330,15 @@ body {
|
||||
border: 2px solid #5F71C5;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.minimap-container svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.combat-log {
|
||||
|
||||
+175
-70
@@ -251,7 +251,7 @@ function computeMove(from, to, speed, blocks) {
|
||||
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
|
||||
cx = Math.max(5, Math.min(145, nx)); cy = Math.max(5, Math.min(145, ny))
|
||||
}
|
||||
return { x: cx, y: cy }
|
||||
}
|
||||
@@ -324,7 +324,7 @@ function canReach(attackerPos, targetPos, 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' }) {
|
||||
function buildPlayerAttack({ weaponId, skillName, targetPart, enemyIndex, equippedWeapons, weaponConditions, bodyParts, bodyInjuries, combat, actor = 'player', charId = 'player', targetCharId }) {
|
||||
const base = weaponMap[weaponId]
|
||||
const isArm = weaponId === 'left_arm' || weaponId === 'right_arm'
|
||||
const skill = isArm
|
||||
@@ -338,13 +338,14 @@ function buildPlayerAttack({ weaponId, skillName, targetPart, enemyIndex, equipp
|
||||
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
|
||||
const enemy = targetCharId ? combat.characters[targetCharId] : (enemyId ? combat.characters[enemyId] : null)
|
||||
|
||||
return {
|
||||
type: 'attack',
|
||||
actor,
|
||||
charId,
|
||||
enemyCharId: enemyId,
|
||||
enemyCharId: targetCharId || enemyId,
|
||||
targetCharId,
|
||||
weaponId, skillName, targetPart, enemyIndex,
|
||||
dmg, severity, injType, injLabel,
|
||||
attackerParts: { ...bodyParts },
|
||||
@@ -639,7 +640,16 @@ export default function App() {
|
||||
const minimapPanRef = useRef(false)
|
||||
const minimapPanStart = useRef({ x: 0, y: 0 })
|
||||
const minimapMovedRef = useRef(false)
|
||||
const minimapSvgRef = useRef(null)
|
||||
const moveModeRef = useRef(null); moveModeRef.current = moveMode
|
||||
const aimModeRef = useRef(false); aimModeRef.current = aimMode
|
||||
const selectedSkillRef = useRef(null); selectedSkillRef.current = selectedSkill
|
||||
const limbSelectRef = useRef(null); limbSelectRef.current = limbSelect
|
||||
const currentActorRef = useRef(null); currentActorRef.current = currentActor
|
||||
const minimapViewRef = useRef({ x: 0, y: 0, scale: 1 }); minimapViewRef.current = minimapView
|
||||
const replayElapsedRef = useRef(0)
|
||||
const pannedKeysRef = useRef(new Set())
|
||||
const panRafRef = useRef(null)
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// DERIVED
|
||||
@@ -1011,6 +1021,56 @@ export default function App() {
|
||||
setLimbSelect(null); setMoveMode(null); setAimMode(false); setSelectedSkill(null); setCurrentActor(null); setSkillDropdownOpen([])
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue a melee attack on a character ID (enemy, NPC, or party).
|
||||
*/
|
||||
const attackCharTarget = (charId) => {
|
||||
const cc = combatRef.current
|
||||
if (!cc) return
|
||||
const target = cc.characters[charId]
|
||||
if (!target || isDead(target.integrity)) return
|
||||
const actorId = currentActorRef.current ?? 'player'
|
||||
const actorChar = cc.characters[actorId]
|
||||
if (!actorChar || isDead(actorChar.integrity)) return
|
||||
const weaponId = limbSelectRef.current
|
||||
const skillName = selectedSkillRef.current
|
||||
if (!weaponId || !skillName) return
|
||||
const actorPos = characterPositionsRef.current[actorId] ?? actorChar.pos
|
||||
if (!canReach(actorPos, target.pos, weaponId, skillName)) {
|
||||
setCombat(c => ({ ...c, log: [...c.log, `Too far`] }))
|
||||
return
|
||||
}
|
||||
const targetPart = 'torso'
|
||||
const enemyIndex = (cc.enemyIds ?? []).indexOf(charId)
|
||||
const action = buildPlayerAttack({
|
||||
weaponId, skillName, targetPart,
|
||||
enemyIndex: enemyIndex >= 0 ? enemyIndex : 0,
|
||||
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,
|
||||
targetCharId: charId,
|
||||
})
|
||||
if (action.type === 'shove_pending') {
|
||||
const enemy2 = cc.characters[action.enemyCharId]
|
||||
if (!enemy2 || isDead(enemy2.integrity)) return
|
||||
const { pos, actualPush } = computeShove(actorPos, enemy2.pos, cc.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); setCurrentActor(null); setSkillDropdownOpen([]); setAimMode(false)
|
||||
return
|
||||
}
|
||||
const anim = createWeaponStrikeAnim(action, cc)
|
||||
characterActionsRef.current[actorId] = anim
|
||||
setLimbSelect(null); setSelectedSkill(null); setCurrentActor(null); setSkillDropdownOpen([]); setAimMode(false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch: get the current position of any animation type.
|
||||
* Returns { x, y } or null if the type is unknown.
|
||||
@@ -1638,6 +1698,33 @@ class MoveAction {
|
||||
return () => cancelAnimationFrame(frame)
|
||||
}, [zoomAnim])
|
||||
|
||||
// ── minimap wheel zoom (non-passive to allow preventDefault)
|
||||
useEffect(() => {
|
||||
const el = minimapSvgRef.current
|
||||
if (!el) return
|
||||
const handler = e => {
|
||||
if (!moveModeRef.current && !aimModeRef.current) return
|
||||
e.preventDefault()
|
||||
const d = e.deltaY > 0 ? 0.9 : 1.1
|
||||
const r = el.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 }
|
||||
})
|
||||
}
|
||||
el.addEventListener('wheel', handler, { passive: false })
|
||||
return () => el.removeEventListener('wheel', handler)
|
||||
}, [setMinimapView])
|
||||
|
||||
// NOTE: the previous native `click` listener on the minimap SVG (registered
|
||||
// via addEventListener with an empty dependency array) was removed here.
|
||||
// It duplicated the React `onClick` handler on the same <svg> element and
|
||||
// fired on the same click event, racing against it via refs that only
|
||||
// update on render (aimModeRef, selectedSkillRef, limbSelectRef, etc).
|
||||
// That race was the root cause of melee-target clicks on the minimap
|
||||
// sometimes doing nothing. The React onClick on the <svg> below — plus the
|
||||
// new direct onClick on each enemy <circle> — now fully own this behavior.
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// REPLAY
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
@@ -1934,15 +2021,40 @@ class MoveAction {
|
||||
if (timerActiveRef.current) { stopTimer() } else { startTimer() }
|
||||
}
|
||||
|
||||
// Spacebar toggles timer, Escape cancels aim mode
|
||||
// Spacebar toggles timer (if in combat), Escape cancels aim mode, WASD pans minimap
|
||||
useEffect(() => {
|
||||
const onKey = (e) => {
|
||||
if (e.code === 'Space' && combat) { e.preventDefault(); toggleTimer() }
|
||||
if (e.key === 'Escape' && aimMode) { setAimMode(false); setSelectedSkill(null) }
|
||||
let panRaf
|
||||
const tick = () => {
|
||||
panRaf = requestAnimationFrame(tick)
|
||||
const pk = pannedKeysRef.current
|
||||
if (pk.size === 0) return
|
||||
let panX = 0, panY = 0
|
||||
if (pk.has('a')) panX -= 1
|
||||
if (pk.has('d')) panX += 1
|
||||
if (pk.has('w')) panY -= 1
|
||||
if (pk.has('s')) panY += 1
|
||||
const len = Math.sqrt(panX * panX + panY * panY)
|
||||
if (len > 0) {
|
||||
setMinimapView(v => ({ ...v, x: v.x + (panX / len) * 100 * 0.016 / v.scale, y: v.y + (panY / len) * 100 * 0.016 / v.scale }))
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', onKey)
|
||||
return () => window.removeEventListener('keydown', onKey)
|
||||
}, [aimMode, combat])
|
||||
const onKeyDown = (e) => {
|
||||
if (e.code === 'Space' && combatRef.current) { e.preventDefault(); toggleTimer() }
|
||||
if (e.key === 'Escape' && aimModeRef.current) { setAimMode(false); setSelectedSkill(null) }
|
||||
const k = e.key.toLowerCase()
|
||||
if (!'wasd'.includes(k)) return
|
||||
e.preventDefault()
|
||||
pannedKeysRef.current.add(k)
|
||||
if (!panRaf) panRaf = requestAnimationFrame(tick)
|
||||
}
|
||||
const onKeyUp = (e) => {
|
||||
pannedKeysRef.current.delete(e.key.toLowerCase())
|
||||
if (pannedKeysRef.current.size === 0 && panRaf) { cancelAnimationFrame(panRaf); panRaf = null }
|
||||
}
|
||||
window.addEventListener('keydown', onKeyDown)
|
||||
window.addEventListener('keyup', onKeyUp)
|
||||
return () => { cancelAnimationFrame(panRaf); window.removeEventListener('keydown', onKeyDown); window.removeEventListener('keyup', onKeyUp) }
|
||||
}, [])
|
||||
|
||||
/** Execute a player move; queue it for execution. */
|
||||
const startMove = (targetX, targetY) => {
|
||||
@@ -2254,7 +2366,7 @@ class MoveAction {
|
||||
<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
|
||||
Click an enemy dot on the minimap to target
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
@@ -2569,12 +2681,8 @@ class MoveAction {
|
||||
{/* ── center: minimap + log */}
|
||||
<div className="combat-center">
|
||||
<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"
|
||||
|
||||
<svg ref={minimapSvgRef} viewBox="0 0 160 160" className="minimap"
|
||||
style={{ cursor: aimMode && limbSelect === 'pistol' ? 'crosshair' : (minimapPanRef.current ? 'grabbing' : 'grab') }}
|
||||
onMouseDown={e => {
|
||||
if (e.button !== 0) return
|
||||
@@ -2596,48 +2704,38 @@ class MoveAction {
|
||||
onPointerMove={e => {
|
||||
if (minimapPanRef.current) return
|
||||
const r = e.currentTarget.getBoundingClientRect()
|
||||
const mx = ((e.clientX - r.left) / r.width) * 160 / minimapView.scale + minimapView.x
|
||||
const my = ((e.clientY - r.top) / r.height) * 160 / minimapView.scale + minimapView.y
|
||||
const mv = minimapViewRef.current
|
||||
const mx = ((e.clientX - r.left) / r.width) * 160 / mv.scale + mv.x
|
||||
const my = ((e.clientY - r.top) / r.height) * 160 / mv.scale + mv.y
|
||||
setMinimapHover({ x: mx, y: my })
|
||||
}}
|
||||
onPointerLeave={() => setMinimapHover(null)}
|
||||
onWheel={e => {
|
||||
if (!moveMode && !aimMode) 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 (!moveMode && !aimMode) return
|
||||
if (!moveModeRef.current && !aimModeRef.current) 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 mv = minimapViewRef.current
|
||||
const tx = ((e.clientX - r.left) / r.width) * 160 / mv.scale + mv.x
|
||||
const ty = ((e.clientY - r.top) / r.height) * 160 / mv.scale + mv.y
|
||||
if (aimModeRef.current) {
|
||||
if (selectedSkillRef.current !== 'Shoot' || limbSelectRef.current !== '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 allCharIds = [...(cc?.enemyIds ?? []), ...(cc?.npcIds ?? []), ...(cc?.partyIds ?? [cc?.playerEntityId].filter(Boolean))]
|
||||
let closest = null, closestDist = 20
|
||||
allCharIds.forEach(cid => {
|
||||
const ch = cc.characters[cid]
|
||||
if (!ch?.pos) return
|
||||
const dx = tx - ch.pos.x, dy = ty - ch.pos.y
|
||||
const dist = Math.sqrt(dx*dx + dy*dy)
|
||||
if (dist < closestDist) { closestDist = dist; closest = i }
|
||||
if (dist < closestDist) { closestDist = dist; closest = cid }
|
||||
})
|
||||
if (closest >= 0) {
|
||||
setAttackEnemy(closest); setSelectedTarget(null)
|
||||
setAimMode(false)
|
||||
}
|
||||
if (closest) attackCharTarget(closest)
|
||||
} else {
|
||||
queueShot(tx, ty)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (moveMode === 'turn') {
|
||||
if (moveModeRef.current === 'turn') {
|
||||
const turnActor = currentActor ?? 'player'
|
||||
const pp = combat.characters[turnActor]?.pos ?? combat.characters['player'].pos
|
||||
const dx = tx - pp.x
|
||||
@@ -2661,42 +2759,47 @@ class MoveAction {
|
||||
</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})`,
|
||||
transform: `translate(${-minimapView.x + (hitShake ? (Math.random() - 0.5) * 2 : 0)}px, ${-minimapView.y + (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 => {
|
||||
{(combat?.enemyIds ?? []).map((eid, idx) => {
|
||||
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
|
||||
// Targeting in melee/ranged-aim mode: clicking the dot itself
|
||||
// is a hard, direct hit-test — independent of the SVG's own
|
||||
// click-coordinate nearest-enemy search above. This is what
|
||||
// makes "click the dot" reliably select the target.
|
||||
const isTargetable = aimMode && !dead && (selectedSkill !== 'Shoot' || limbSelect !== 'pistol')
|
||||
const isCurrentTarget = attackEnemy === idx
|
||||
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) }}
|
||||
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) }
|
||||
}
|
||||
}} />
|
||||
fill={dead ? '#6b7280' : enemy.fillColor}
|
||||
stroke={dead ? '#6b7280' : (hoveredChar === `enemy-${eid}` ? '#ffffff' : (isCurrentTarget ? '#fbbf24' : '#e74c3c'))}
|
||||
strokeWidth={dead ? 1 : (hoveredChar === `enemy-${eid}` || isCurrentTarget ? 1.25 : 1)}
|
||||
opacity={dead ? 0.5 : 1}
|
||||
style={{ cursor: isTargetable && limbSelect === 'pistol' ? 'crosshair' : isTargetable ? 'pointer' : undefined, pointerEvents: isTargetable ? 'auto' : 'none' }}
|
||||
onMouseEnter={() => setHoveredChar(`enemy-${eid}`)}
|
||||
onMouseLeave={() => setHoveredChar(null)}
|
||||
onClick={e => {
|
||||
if (!isTargetable) return
|
||||
e.stopPropagation()
|
||||
attackCharTarget(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" />
|
||||
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" pointerEvents="none" />
|
||||
})()}
|
||||
{moveMode && moveMode !== 'turn' && minimapHover && combat && (() => {
|
||||
const actorId = currentActor ?? 'player'
|
||||
@@ -2754,9 +2857,9 @@ class MoveAction {
|
||||
<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}
|
||||
r={4 * (ch.size ?? 1)} fill={chDead ? '#6b7280' : color} stroke={chDead ? '#6b7280' : (hoveredChar === cid ? '#ffffff' : '#3498db')} strokeWidth={hoveredChar === cid ? 1.25 : 1} opacity={chDead ? 0.5 : 1}
|
||||
onMouseEnter={() => setHoveredChar(cid)} onMouseLeave={() => setHoveredChar(null)}
|
||||
onClick={e => { e.stopPropagation() }}
|
||||
onClick={e => { e.stopPropagation(); if (aimMode && limbSelect !== 'pistol') attackCharTarget(cid) }}
|
||||
onContextMenu={e => { e.preventDefault(); e.stopPropagation(); setHoverInfo(null); setHoveredChar(null); setContextMenu({ x: e.clientX, y: e.clientY, charId: cid }) }} />
|
||||
)
|
||||
})
|
||||
@@ -2768,9 +2871,9 @@ class MoveAction {
|
||||
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}
|
||||
r={4} fill={ch.fillColor} stroke={hoveredChar === nid ? '#ffffff' : '#22c55e'} strokeWidth={hoveredChar === nid ? 1.25 : 1} opacity={hoveredChar === nid ? 1 : 0.7}
|
||||
onMouseEnter={() => setHoveredChar(nid)} onMouseLeave={() => setHoveredChar(null)}
|
||||
onClick={e => { e.stopPropagation() }}
|
||||
onClick={e => { e.stopPropagation(); if (aimMode && limbSelect !== 'pistol') attackCharTarget(nid) }}
|
||||
onContextMenu={e => { e.preventDefault(); e.stopPropagation(); setHoverInfo(null); setHoveredChar(null); setContextMenu({ x: e.clientX, y: e.clientY, charId: nid }) }} />
|
||||
)
|
||||
})}
|
||||
@@ -2904,7 +3007,7 @@ class MoveAction {
|
||||
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 }) }}>
|
||||
<div key={eid} className="enemy-entry" onClick={() => { setSelectedEnemy(idx); setAttackEnemy(idx); setSelectedTarget(null); 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>
|
||||
)
|
||||
@@ -3422,9 +3525,11 @@ 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 className="backdrop" style={{ position: 'fixed', inset: 0, zIndex: 299 }} onClick={() => setContextMenu(null)} onContextMenu={e => { e.preventDefault(); setContextMenu(null) }}>
|
||||
<div className="context-menu" style={{ left: contextMenu.x, top: contextMenu.y, position: 'absolute' }} onClick={e => { e.stopPropagation(); 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>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user