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 */}
-
+
- {/* ── 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 (<>
-
-
-
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) }}>
+
)}