From 2ff7932048591ed038eb739c0cad490ad0bbd8c7 Mon Sep 17 00:00:00 2001 From: johnruina Date: Fri, 19 Jun 2026 01:12:35 -0400 Subject: [PATCH] broken target select --- src/App.css | 18 +++ src/App.jsx | 405 ++++++++++++++++++++++++---------------------------- 2 files changed, 203 insertions(+), 220 deletions(-) diff --git a/src/App.css b/src/App.css index 293cea4..494a732 100644 --- a/src/App.css +++ b/src/App.css @@ -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; diff --git a/src/App.jsx b/src/App.jsx index 5f30a72..1e96fd1 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -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 ( +
+ {skills.map((skill, si) => { + const active = selectedSkill === skill.name + const isSpecial = skill.name === 'Load' || skill.name === 'Unload' + return ( + + {active && isSpecial ? ( +
{ + if (skill.name === 'Load') { startReload(); return } + if (skill.name === 'Unload') { unloadPistol(); return } + }} + onMouseEnter={() => setHoveredSkill(skill.name)} + onMouseLeave={() => setHoveredSkill(null)}> + + {skill.name} +
+ ) : active ? ( +
{ + setAttackEnemy(null); setSelectedTarget(null) + setAimMode(true) + }} + onMouseEnter={() => setHoveredSkill(skill.name)} + onMouseLeave={() => setHoveredSkill(null)}> + + {skill.name} +
+ ) : ( +
{ + 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)}> + + {skill.name} +
+ )} + {/* Targeting / limb selection when this skill is active */} + {active && !isSpecial && ( +
+ {attackEnemy === null ? ( +
+ Click the minimap to target +
+ ) : ( + <> + {[{ 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 }) => ( +
selectLimbTarget(key)}> + + {label} +
+ ))} +
+ + +
+ + )} +
+ )} +
+ ) + })} + {/* Ammo selection when reloading */} + {selectedSkill === 'Load' && ( +
+ {(() => { + const ammoItems = Object.entries(ac?.itemCounts ?? {}) + .filter(([id, qty]) => qty > 0 && (weaponMap.pistol?.compatibleBulletSizes ?? []).includes(itemMap[id]?.bulletSize)) + return ammoItems.length === 0 + ?
No ammo available
+ : ammoItems.map(([id, qty]) => ( +
loadAmmoType(id)}> + + {itemMap[id]?.name ?? id} + ×{qty} +
+ )) + })()} +
+ )} +
+ ) + } + 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) && (
{(() => { - const entries = [] + const makeWeap = (weaponId, label) => { + const dropKey = `${cid}-${weaponId}` + const isOpen = skillDropdownOpen.includes(dropKey) + return ( + +
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) + }}> + {isOpen ? '▼' : '▶'} + {label} +
+ {isOpen && renderSkillDropdown(weaponId, cid)} +
+ ) + } + const pieces = [] if (lw === 'fists') { - entries.push(
setHoverInfo({ type: 'weapon', id: 'left_arm' })} onMouseLeave={() => setHoverInfo(null)} - onClick={() => { setCurrentActor(cid); setLimbSelect('left_arm'); setSelectedSkill(null); setSelectedTarget(null); setMoveMode(null); setAimMode(false) }}> - Left Arm
) + pieces.push(makeWeap('left_arm', 'Left Arm')) } else if (lw !== rw) { - entries.push(
setHoverInfo({ type: 'weapon', id: lw })} onMouseLeave={() => setHoverInfo(null)} - onClick={() => { setCurrentActor(cid); setLimbSelect(lw); setSelectedSkill(null); setSelectedTarget(null); setMoveMode(null); setAimMode(false) }}> - Left: {weaponMap[lw]?.name ?? lw}
) + pieces.push(makeWeap(lw, `Left: ${weaponMap[lw]?.name ?? lw}`)) } if (rw === 'fists') { - entries.push(
setHoverInfo({ type: 'weapon', id: 'right_arm' })} onMouseLeave={() => setHoverInfo(null)} - onClick={() => { setCurrentActor(cid); setLimbSelect('right_arm'); setSelectedSkill(null); setSelectedTarget(null); setMoveMode(null); setAimMode(false) }}> - Right Arm
) + pieces.push(makeWeap('right_arm', 'Right Arm')) } else if (lw !== rw) { - entries.push(
setHoverInfo({ type: 'weapon', id: rw })} onMouseLeave={() => setHoverInfo(null)} - onClick={() => { setCurrentActor(cid); setLimbSelect(rw); setSelectedSkill(null); setSelectedTarget(null); setMoveMode(null); setAimMode(false) }}> - Right: {weaponMap[rw]?.name ?? rw}
) + pieces.push(makeWeap(rw, `Right: ${weaponMap[rw]?.name ?? rw}`)) } if (lw === rw && lw !== 'fists' && lw !== 'left_arm' && lw !== 'right_arm') { - entries.push(
setHoverInfo({ type: 'weapon', id: lw })} onMouseLeave={() => setHoverInfo(null)} - onClick={() => { setCurrentActor(cid); setLimbSelect(lw); setSelectedSkill(null); setSelectedTarget(null); setMoveMode(null); setAimMode(false) }}> - Both: {weaponMap[lw]?.name ?? lw}
) + pieces.push(makeWeap(lw, `Both: ${weaponMap[lw]?.name ?? lw}`)) } if (lw === 'fists' && rw === 'fists') { - entries.push(
setHoverInfo({ type: 'weapon', id: 'fists' })} onMouseLeave={() => setHoverInfo(null)} - onClick={() => { setCurrentActor(cid); setLimbSelect('fists'); setSelectedSkill(null); setSelectedTarget(null); setMoveMode(null); setAimMode(false) }}> - Fists
) + pieces.push(makeWeap('fists', 'Fists')) } - return entries + return pieces })()}
)} @@ -2464,17 +2568,19 @@ class MoveAction { {/* ── center: minimap + log */}
-
+
{ + 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) { - queueShot(tx, ty) + 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') { @@ -2555,10 +2678,18 @@ class MoveAction { { 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 }) }} /> + 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) } + } + }} /> ) })} @@ -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 {
- {/* ── 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 ( -
-
e.stopPropagation()}> - {(() => { - const weapon = weaponMap[limbSelect] - const weaponName = weapon?.name ?? limbSelect - const skills = weapon?.skills ?? [] - return ( -
-
- {weaponName} — {actorChar?.name ?? actorId} - {limbSelect === 'pistol' && Ammo: {actorChar?.weaponMagazines?.pistol?.length ?? 0}/{PISTOL_MAG} ({itemMap[actorChar?.weaponMagazines?.pistol?.[0]?.ammoType]?.name ?? 'none'})} -
-
- {/* skill list */} - {skills.length > 0 && ( -
-
Select Skill
-
- {skills.map((skill, i) => ( - - ))} -
-
- )} - {/* reload: ammo selection */} - {selectedSkill === 'Load' && ( -
-
Select Ammo Type
-
- {Object.entries(actorChar?.itemCounts ?? {}) - .filter(([id, qty]) => qty > 0 && (weaponMap.pistol?.compatibleBulletSizes ?? []).includes(itemMap[id]?.bulletSize)) - .length === 0 - ?
No ammo available
- : Object.entries(actorChar?.itemCounts ?? {}) - .filter(([id, qty]) => qty > 0 && (weaponMap.pistol?.compatibleBulletSizes ?? []).includes(itemMap[id]?.bulletSize)) - .map(([id, qty]) => ( -
e.currentTarget.style.background = '#1e1e30'} - onMouseLeave={e => e.currentTarget.style.background = 'none'} - onClick={() => loadAmmoType(id)}> - {itemMap[id]?.name ?? id} - ×{qty} -
- )) - } -
-
- )} - {/* enemy list (hidden during reload) */} - {selectedSkill && selectedSkill !== 'Load' && ( -
-
Select Enemy
-
- {(combat?.enemyIds ?? []).map((eid, i) => { - const enemy = combat.characters[eid] - const inRange = canReach(actorPos, enemy.pos, limbSelect, selectedSkill) - return ( - - ) - })} -
-
- )} - {/* limb selector (hidden during reload) */} - {selectedSkill !== 'Load' && attackEnemy !== null && ( -
-
Limb
-
- {[{ 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}) => ( - - ))} -
-
- )} -
- {/* side info panel */} - {skills.length > 0 && ( -
- {(() => { - 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 (<> -
-
{s.name}
-
{descs[s.damageType] || s.damageType}
-
-
-
Damage: {Math.round((weapon?.damage ?? 0) * s.mult)}
-
Multiplier: {s.mult}x
-
Type: {s.damageType}
-
Range: {s.range}
-
- ) - } - if (attackEnemy === null) { - const e = hoveredEnemy !== null && combat ? combat.characters[combat.enemyIds[hoveredEnemy]] : null - if (!e) return null - return (<> -
-
{e.name}
-
-
-
HP: {Object.values(e.integrity).reduce((a,b)=>a+b,0)}
- {Object.entries(e.integrity).map(([p,v]) =>
{bodyPartLabels[p]}: {v}
)} -
- ) - } - const e = combat ? combat.characters[combat.enemyIds[attackEnemy]] : null - const limbKey = selectedTarget - const hp = limbKey && e ? e.integrity[limbKey] : null - return (<> -
-
{e?.name || ''}
-
Target: {selectedTarget ? bodyPartLabels[selectedTarget] : 'None'}
-
-
-
Limb HP: {hp !== null ? hp : '-'}
-
- ) - })()} -
- )} -
- ) - })()} -
-
- {limbSelect !== 'pistol' && } - -
-
- ) - })()} )} @@ -3459,6 +3423,7 @@ onContextMenu={e => { e.preventDefault(); setHoverInfo(null); setHoveredChar(nul {/* ═══════════════════════════════════════════════════════ CONTEXT MENU */} {contextMenu && (
{ setHoverInfo(null); setHoveredChar(null); setContextMenu(null) }}> +
)}