This commit is contained in:
2026-06-01 02:06:34 -04:00
parent 351e6c3a98
commit d93c7c87da
4 changed files with 488 additions and 363 deletions
Binary file not shown.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

+112 -7
View File
@@ -947,15 +947,86 @@ body {
cursor: not-allowed;
}
.limb-cancel-btn {
background: #1e1e30;
color: #6b7280;
border: 1px solid #2e303a;
border-radius: 3px;
padding: 14px 40px;
cursor: pointer;
.limb-select-panel {
width: 600px;
min-height: 0;
}
.limb-select-title {
margin-bottom: 16px;
font-size: 16px;
font-weight: bold;
color: #e0e0e0;
}
.limb-select-skills {
margin-bottom: 8px;
}
.limb-select-grid {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.limb-target-btn {
text-align: center;
cursor: pointer;
border: 1px solid #5F71C5;
border-radius: 1px;
padding: 8px 14px;
font-weight: bold;
font-size: 13px;
background: none;
color: #5F71C5;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.limb-target-btn:hover {
background: #1e2642;
color: #5F71C5;
}
.limb-target-selected {
background: #1e2642;
border-color: #fbbf24;
color: #fbbf24;
}
.limb-target-selected:hover {
background: #1e2642;
border-color: #fcd34d;
color: #fcd34d;
}
.limb-confirm-btn {
border: 1px solid #22c55e;
border-radius: 1px;
padding: 10px 24px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
background: #22c55e;
color: #000;
transition: background 0.15s;
white-space: nowrap;
}
.limb-confirm-btn:hover {
background: #16a34a;
}
.limb-cancel-btn {
text-align: center;
border: 1px solid #2e303a;
border-radius: 1px;
padding: 10px 0;
cursor: pointer;
font-size: 14px;
font-weight: bold;
background: #1e1e30;
color: #6b7280;
transition: color 0.15s, border-color 0.15s;
}
.limb-cancel-btn:hover {
@@ -1318,6 +1389,40 @@ body {
justify-content: center;
}
.world-info-panel {
background: rgba(15, 15, 26, 0.75);
border: 2px solid #5F71C5;
border-radius: 3px;
padding: 24px 32px 32px;
min-width: 280px;
position: relative;
}
.world-info-grid {
display: flex;
flex-direction: column;
gap: 8px;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 0;
border-bottom: 1px solid #2e303a;
}
.info-label {
color: #6b7280;
font-size: 14px;
}
.info-value {
color: #e0e0e0;
font-size: 14px;
font-weight: bold;
}
.inventory-panel {
background: rgba(15, 15, 26, 0.75);
border: 2px solid #5F71C5;
+296 -276
View File
@@ -196,6 +196,7 @@ function App() {
const [weaponConditions, setWeaponConditions] = useState({ rusty_sword: 65 })
const [buyArmChoice, setBuyArmChoice] = useState(null)
const [showInventory, setShowInventory] = useState(false)
const [showWorldInfo, setShowWorldInfo] = useState(false)
const [invTab, setInvTab] = useState('health')
const [gameTime, setGameTime] = useState({ day: 1, hour: 8 })
const [contextMenu, setContextMenu] = useState(null)
@@ -246,9 +247,12 @@ function App() {
const [selectedSkill, setSelectedSkill] = useState(null)
const [selectedEnemy, setSelectedEnemy] = useState(0)
const [targetEnemy, setTargetEnemy] = useState(null)
const [showEnemyInv, setShowEnemyInv] = useState(false)
const [enemyInvTab, setEnemyInvTab] = useState('health')
const [charView, setCharView] = useState(null)
const [attackEnemy, setAttackEnemy] = useState(null)
const [panelTarget, setPanelTarget] = useState('player')
const [minimapView, setMinimapView] = useState({ x: 0, y: 0, scale: 1 })
const minimapPanRef = useRef(false)
const minimapPanStart = useRef({ x: 0, y: 0 })
const minimapMovedRef = useRef(false)
const liveCharacters = characters
@@ -364,6 +368,8 @@ function App() {
return {
id: npc.id,
name: npc.name,
age: Math.floor(Math.random() * 30) + 18,
location: npc.location ?? 'Unknown',
pos: { x: 120 + index * spread, y: 60 + index * spread },
integrity: freshBody(),
injuries: freshInjuries(),
@@ -708,15 +714,30 @@ function App() {
}, [combatTime, combat, bodyInjuries])
const handleItemClick = (itemId, i) => {
setCharView('player')
setShowInventory(true); setPanelTarget('player')
}
const confirmLimbAttack = (part) => {
if (!limbSelect) return
const partMap = { larm: 'leftArm', rarm: 'rightArm', lleg: 'leftLeg', rleg: 'rightLeg' }
const selectLimbTarget = (part) => {
const fullPart = partMap[part] || part
playerAttack(fullPart, limbSelect, null, selectedEnemy)
setSelectedTarget(fullPart)
}
const executeAttack = () => {
if (!limbSelect || !selectedTarget) return
playerAttack(selectedTarget, limbSelect, selectedSkill, attackEnemy)
setLimbSelect(null)
setSelectedSkill(null)
setSelectedTarget(null)
setAttackEnemy(null)
}
const cancelAttack = () => {
setLimbSelect(null)
setSelectedSkill(null)
setSelectedTarget(null)
setAttackEnemy(null)
}
return (
@@ -737,7 +758,7 @@ function App() {
<div className="tree-root">
<div className="tree-node folder" onClick={() => setExpandedWeapon(expandedWeapon === 'ROOT' ? null : 'ROOT')}>
<span className="tree-toggle">{expandedWeapon === 'ROOT' ? '▼' : '▶'}</span>
<span className="tree-label" style={{ color: '#22c55e', cursor: 'pointer' }} onClick={e => { e.stopPropagation(); setCharView('player') }}>Player</span>
<span className="tree-label" style={{ color: '#22c55e', cursor: 'pointer' }} onClick={e => { e.stopPropagation(); setShowInventory(true); setPanelTarget('player') }}>Player</span>
</div>
{expandedWeapon === 'ROOT' && (
<div className="tree-children">
@@ -856,26 +877,76 @@ function App() {
)}
</div>
<div className="combat-center">
<div className="minimap-container">
<svg viewBox="0 0 160 160" className="minimap" onClick={(e) => {
<div className="minimap-container" style={{ position: 'relative' }}>
<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
const dy = ev.clientY - minimapPanStart.current.y
if (Math.abs(dx) > 2 || Math.abs(dy) > 2) minimapMovedRef.current = true
const r = svg.getBoundingClientRect()
const worldDx = (dx / r.width) * 160 / minimapView.scale
const worldDy = (dy / r.height) * 160 / minimapView.scale
setMinimapView(v => {
return { ...v, x: minimapPanStart.current.vx - worldDx, y: minimapPanStart.current.vy - worldDy }
})
}
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 delta = e.deltaY > 0 ? 0.9 : 1.1
const rect = e.currentTarget.getBoundingClientRect()
const cx = e.clientX, cy = e.clientY
setMinimapView(v => {
const newScale = Math.max(0.5, Math.min(3, v.scale * delta))
const mx = (cx - rect.left) / rect.width
const my = (cy - rect.top) / rect.height
const newW = 160 / newScale
const newX = v.x + mx * (160 / v.scale - 160 / newScale)
const newY = v.y + my * (160 / v.scale - 160 / newScale)
return { x: newX, y: newY, scale: newScale }
})
}}
onClick={(e) => {
if (minimapMovedRef.current) { minimapMovedRef.current = false; return }
if (limbSelect) return
if (!moveMode) return
const rect = e.currentTarget.getBoundingClientRect()
const x = ((e.clientX - rect.left) / rect.width) * 160
const y = ((e.clientY - rect.top) / rect.height) * 160
const x = ((e.clientX - rect.left) / rect.width) * 160 / minimapView.scale + minimapView.x
const y = ((e.clientY - rect.top) / rect.height) * 160 / minimapView.scale + minimapView.y
startMove(x, y)
}}>
<g style={{ transform: `translate(${-minimapView.x * minimapView.scale}px, ${-minimapView.y * minimapView.scale}px) scale(${minimapView.scale})`, transformOrigin: '0 0' }}>
<rect x="0" y="0" width="160" height="160" fill="#171721" />
{MAP_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?.enemies || []).map((enemy, i) => (
<circle key={i} cx={enemy.pos.x} cy={enemy.pos.y} r="5" fill="#e74c3c" stroke="#000" strokeWidth="1.5"
<circle key={i} cx={enemy.pos.x} cy={enemy.pos.y} r="5" fill="#e74c3c" stroke="#000" strokeWidth="0.5"
onMouseEnter={(e) => { e.stopPropagation(); setHoverInfo({ type: 'enemy', name: enemy.name, attack: enemy.attack, speed: enemy.speed }) }}
onMouseMove={(e) => setMousePos({ x: e.clientX, y: e.clientY })}
onMouseLeave={() => setHoverInfo(null)}
onClick={() => { setTargetEnemy(i); setCharView(i) }} />
onClick={(e) => { e.stopPropagation(); setTargetEnemy(i); setShowInventory(true); setPanelTarget(i) }} />
))}
{combat && <circle cx={combat.playerPos.x} cy={combat.playerPos.y} r="4" fill="#3498db" stroke="#000" strokeWidth="1.5" />}
{combat && selectedSkill && limbSelect && <circle cx={combat.playerPos.x} cy={combat.playerPos.y} r={getSkillRange(limbSelect, selectedSkill)} fill="none" stroke="rgba(52,152,219,0.5)" strokeWidth="1" strokeDasharray="4 4" />}
{combat && <circle cx={combat.playerPos.x} cy={combat.playerPos.y} r="4" fill="#3498db" stroke="#000" strokeWidth="0.5" />}
</g>
</svg>
</div>
<div className="combat-log">
@@ -890,254 +961,14 @@ function App() {
<div className="combat-right">
<div className="enemy-list">
{(combat?.enemies || []).map((enemy, i) => (
<div key={i} className="enemy-entry" onClick={() => { setSelectedEnemy(i); setTargetEnemy(i); setCharView(i) }}>
<div key={i} className="enemy-entry" onClick={() => { setSelectedEnemy(i); setTargetEnemy(i); setShowInventory(true); setPanelTarget(i) }}>
<span className="enemy-name">{enemy.name}</span>
</div>
))}
</div>
</div>
</div>
{charView !== null && (
<div className="inventory-overlay" onClick={() => { setCharView(null); setHoverInfo(null); setContextMenu(null) }}
onContextMenu={e => { e.preventDefault(); setContextMenu(null) }}
onMouseMove={(e) => setMousePos({ x: e.clientX, y: e.clientY })}>
<div className="inventory-panel" onClick={e => e.stopPropagation()}>
<button className="inv-close" onClick={() => { setCharView(null); setHoverInfo(null) }}>X</button>
<div className="inv-layout">
<div className="inv-body-col">
{charView === 'player' ? (
<BodyImage body={bodyParts} />
) : combat?.enemies[charView] ? (
<BodyImage body={combat.enemies[charView].integrity} />
) : null}
</div>
<div className="inv-right-col">
<div className="inv-tabs">
{charView === 'player' ? (
<>
<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>
</>
) : (
<>
<button className={`inv-tab ${enemyInvTab === 'health' ? 'active' : ''}`}
onClick={() => setEnemyInvTab('health')}>Health</button>
<button className={`inv-tab ${enemyInvTab === 'inventory' ? 'active' : ''}`}
onClick={() => setEnemyInvTab('inventory')}>Inventory</button>
<button className={`inv-tab ${enemyInvTab === 'stats' ? 'active' : ''}`}
onClick={() => setEnemyInvTab('stats')}>Statistics</button>
</>
)}
</div>
{charView === 'player' ? (
<>
{invTab === 'health' && (
<div className="inv-parts-col">
<div className="inv-parts-title">Body Parts</div>
{Object.keys(MAX_BODY).map(part => {
const hp = bodyParts[part]
const max = MAX_BODY[part]
const injuries = bodyInjuries[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>{playerHp}/{MAX_HP}</span>
</div>
</div>
)}
{invTab === 'inventory' && (
<div className="inv-inv-content">
<div className="inv-equip-section">
<div className="inv-parts-title">Weapons</div>
{(() => {
const lw = equippedWeapons.left, rw = equippedWeapons.right
if (lw === rw) {
return (
<div className="equip-row"
onMouseEnter={() => setHoverInfo({ type: 'weapon', id: lw })}
onMouseLeave={() => setHoverInfo(null)}
onContextMenu={e => { e.preventDefault(); setContextMenu({ x: e.clientX, y: e.clientY, weaponId: lw, side: 'both' }) }}>
<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: {weaponConditions[lw] ?? weaponMap[lw].maxCondition}/{weaponMap[lw].maxCondition}</span>
{weaponMap[lw].passives.length > 0 && (
<span className="equip-passives">{weaponMap[lw].passives.join(', ')}</span>
)}</>
)}
</div>
)
}
return ['left', 'right'].map(side => {
const wid = equippedWeapons[side]
if (wid === 'fists') return null
return (
<div key={side} className="equip-row"
onMouseEnter={() => setHoverInfo({ type: 'weapon', id: wid })}
onMouseLeave={() => setHoverInfo(null)}
onContextMenu={e => { e.preventDefault(); setContextMenu({ x: e.clientX, y: e.clientY, weaponId: wid, side }) }}>
<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: {weaponConditions[wid] ?? weaponMap[wid].maxCondition}/{weaponMap[wid].maxCondition}</span>
{weaponMap[wid].passives.length > 0 && (
<span className="equip-passives">{weaponMap[wid].passives.join(', ')}</span>
)}
</div>
)
})
})()}
</div>
<div className="inv-divider" />
<div className="inv-parts-title">Inventory ({playerInventory.length})</div>
{playerInventory.length === 0 ? (
<div className="inv-empty">Empty</div>
) : (
<>
{playerInventory.map((itemId, i) => (
<div key={i} className="inv-item"
onMouseEnter={() => setHoverInfo({ type: 'item', id: itemId })}
onMouseLeave={() => setHoverInfo(null)}>
{itemMap[itemId]?.name ?? itemId}
</div>
))}
</>
)}
<div className="inv-weight">Weight: {currentWeight}/{MAX_WEIGHT}</div>
<div className="inv-money">{playerMoney}g</div>
</div>
)}
{invTab === 'stats' && (
<div className="inv-stats-content">
<div className="inv-parts-title">Character</div>
<div className="stat-row"><span className="stat-label">Name</span><span className="stat-value">You</span></div>
<div className="stat-row"><span className="stat-label">Age</span><span className="stat-value">24</span></div>
<div className="stat-row"><span className="stat-label">Location</span><span className="stat-value">{locMap[playerLoc]?.name ?? playerLoc}</span></div>
<div className="stat-row"><span className="stat-label">Day</span><span className="stat-value">{gameTime.day}</span></div>
<div className="stat-row"><span className="stat-label">Time</span><span className="stat-value">{String(gameTime.hour).padStart(2, '0')}:00</span></div>
<div className="stat-row"><span className="stat-label">Weather</span><span className="stat-value">{weatherLabels[weather]}</span></div>
<div className="inv-divider" />
<div className="stat-row"><span className="stat-label">Equipped</span><span className="stat-value">{equippedWeapons.left === equippedWeapons.right ? (equippedWeapons.left === 'fists' ? 'Fists' : `Both: ${weaponMap[equippedWeapons.left].name}`) : [equippedWeapons.left !== 'fists' ? `L:${weaponMap[equippedWeapons.left].name}` : '', equippedWeapons.right !== 'fists' ? `R:${weaponMap[equippedWeapons.right].name}` : ''].filter(Boolean).join(' + ')}</span></div>
{(() => {
const shown = new Set()
const rows = []
const addWeapon = (wid) => {
if (shown.has(wid) || wid === 'fists') return
shown.add(wid)
rows.push(
<Fragment key={wid}>
<div className="stat-row"><span className="stat-label">Skills</span><span className="stat-value">{weaponMap[wid].skills.map(s => s.name).join(', ')}</span></div>
<div className="stat-row"><span className="stat-label">Weapon SPD</span><span className="stat-value">{weaponMap[wid].speed}</span></div>
</Fragment>
)
}
addWeapon(equippedWeapons.left)
addWeapon(equippedWeapons.right)
return rows
})()}
<div className="inv-divider" />
<div className="stat-row"><span className="stat-label">Integrity</span><span className="stat-value">{playerHp}/{MAX_HP}</span></div>
<div className="stat-row"><span className="stat-label">Items</span><span className="stat-value">{playerInventory.length}</span></div>
<div className="stat-row"><span className="stat-label">Weight</span><span className="stat-value">{currentWeight}/{MAX_WEIGHT}</span></div>
<div className="stat-row"><span className="stat-label">Money</span><span className="stat-value">{playerMoney}g</span></div>
</div>
)}
</>
) : combat?.enemies[charView] ? (
<>
{enemyInvTab === 'health' && (
<div className="inv-parts-col">
<div className="inv-parts-title">Body Parts</div>
{Object.keys(MAX_BODY).map(part => {
const hp = combat.enemies[charView].integrity[part]
const max = MAX_BODY[part]
const injuries = combat.enemies[charView].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>{Object.values(combat.enemies[charView].integrity).reduce((a, b) => a + b, 0)}/{MAX_HP}</span>
</div>
</div>
)}
{enemyInvTab === 'inventory' && (
<div className="inv-inv-content">
<div className="inv-parts-title">Inventory ({(combat.enemies[charView].inventory || []).length})</div>
{(combat.enemies[charView].inventory || []).length === 0 ? (
<div className="inv-empty">Empty</div>
) : (
<>
{combat.enemies[charView].inventory.map((itemId, i) => (
<div key={i} className="inv-item"
onMouseEnter={() => setHoverInfo({ type: 'item', id: itemId })}
onMouseLeave={() => setHoverInfo(null)}>
{itemMap[itemId]?.name ?? itemId}
</div>
))}
</>
)}
<div className="inv-money">{combat.enemies[charView].money || 0}g</div>
</div>
)}
{enemyInvTab === 'stats' && (
<div className="inv-stats-content">
<div className="inv-parts-title">{combat.enemies[charView].name}</div>
<div className="stat-row"><span className="stat-label">Attack</span><span className="stat-value">{combat.enemies[charView].attack || 0}</span></div>
<div className="stat-row"><span className="stat-label">Speed</span><span className="stat-value">{combat.enemies[charView].speed || 0}</span></div>
</div>
)}
</>
) : null}
</div>
</div>
</div>
</div>
)}
{showSettings && (
<div className="settings-sidebar">
<div className="settings-header">
@@ -1186,21 +1017,93 @@ function App() {
</div>
)}
{limbSelect && (
<div className="limb-select-overlay" onClick={() => setLimbSelect(null)}>
<div className="limb-select-panel" onClick={e => e.stopPropagation()}>
<div className="limb-select-title">Select body part to attack</div>
<div className="limb-select-actions">
<button className="limb-action-btn" onClick={() => confirmLimbAttack("torso")}>Torso</button>
<button className="limb-action-btn" onClick={() => confirmLimbAttack("head")}>Head</button>
<button className="limb-action-btn" onClick={() => confirmLimbAttack("larm")}>Left Arm</button>
<button className="limb-action-btn" onClick={() => confirmLimbAttack("rarm")}>Right Arm</button>
<button className="limb-action-btn" onClick={() => confirmLimbAttack("lleg")}>Left Leg</button>
<button className="limb-action-btn" onClick={() => confirmLimbAttack("rleg")}>Right Leg</button>
</div>
<div className="limb-cancel-row">
<button className="limb-cancel-btn" onClick={() => setLimbSelect(null)}>Cancel</button>
<div className="inventory-overlay" onClick={() => cancelAttack()} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 0 }}>
<div className="inventory-panel limb-select-panel" onClick={e => e.stopPropagation()}>
<button className="inv-close" onClick={() => cancelAttack()}>X</button>
{(() => {
const weapId = limbSelect
const isArm = weapId === 'left_arm' || weapId === 'right_arm'
const weapon = !isArm ? weaponMap[weapId] : null
const weaponName = isArm ? (weapId === 'left_arm' ? 'Left Arm' : 'Right Arm') : (weapon?.name ?? weapId)
const skills = weapon?.skills ?? []
if (isArm && !selectedSkill) selectSkill('Punch')
return (
<>
<div className="inv-parts-title limb-select-title">{weaponName}</div>
{!selectedSkill && skills.length > 0 ? (
<div className="limb-select-skills">
<div className="inv-parts-title" style={{ marginBottom: 8 }}>Select Skill</div>
<div className="limb-select-grid">
{skills.map((skill, i) => (
<button key={i} className="limb-target-btn" onClick={() => { setSelectedSkill(skill.name); setAttackEnemy(null) }}>
<div style={{ fontWeight: 'bold' }}>{skill.name}</div>
<div style={{ fontSize: 11, color: '#6b7280' }}>{skill.damageType} x{skill.mult} (rng {skill.range})</div>
</button>
))}
</div>
</div>
) : selectedSkill && attackEnemy === null ? (
<div className="limb-select-enemies">
<div className="inv-parts-title" style={{ marginBottom: 8 }}>Select Enemy</div>
<div className="limb-select-grid">
{(combat?.enemies || []).map((enemy, i) => (
<button key={i} className="limb-target-btn" onClick={() => setAttackEnemy(i)}
style={{ textAlign: 'center' }}>
<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>
) : (
<div className="limb-select-body" style={{ display: 'flex', gap: 16 }}>
<div className="limb-select-info" style={{ flex: 1 }}>
{skills.length > 0 && selectedSkill && (
<div className="inv-parts-title" style={{ marginBottom: 8, color: '#fbbf24' }}>Skill: {selectedSkill}</div>
)}
{attackEnemy !== null && combat?.enemies[attackEnemy] && (
<div className="inv-parts-title" style={{ marginBottom: 8, color: '#22c55e' }}>Target: {combat.enemies[attackEnemy].name}</div>
)}
{isArm && (
<div className="inv-parts-title" style={{ marginBottom: 8, color: '#6b7280', fontSize: 12 }}>A weak unarmed punch (DMG: 10)</div>
)}
</div>
<div>
<div className="inv-parts-title" style={{ marginBottom: 8 }}>Limb</div>
<div className="limb-target-grid" 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 }) => {
const fullPart = partMap[key] || key
const isSelected = selectedTarget === fullPart
return (
<button key={key}
className={'limb-target-btn' + (isSelected ? ' limb-target-selected' : '')}
onClick={() => selectLimbTarget(key)}
style={{ whiteSpace: 'nowrap', padding: '6px 16px' }}>
{label}
</button>
)
})}
</div>
</div>
</div>
)}
</>
)
})()}
</div>
{selectedTarget && (
<div className="attack-action-bar" style={{ display: 'flex', flexDirection: 'column', gap: 8, marginLeft: 12 }}>
<button className="limb-confirm-btn" onClick={() => executeAttack()}>Confirm</button>
<button className="limb-cancel-btn" onClick={() => cancelAttack()} style={{ width: 'auto', padding: '10px 24px' }}>Cancel</button>
</div>
)}
</div>
)}
</div>
@@ -1268,7 +1171,14 @@ function App() {
<div className="top-bar">
<div className="top-bar-left">
<button className="icon-btn" onClick={() => setShowInventory(true)} title="Character">
<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={() => { 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" />
@@ -1483,16 +1393,22 @@ function App() {
</div>
</div>
)}
</>
)}
{showInventory && (
<div className="inventory-overlay" onClick={() => { setShowInventory(false); setHoverInfo(null); setContextMenu(null) }}
<div className="inventory-overlay" onClick={() => { setShowInventory(false); setPanelTarget('player'); setHoverInfo(null); setContextMenu(null) }}
onContextMenu={e => { e.preventDefault(); setContextMenu(null) }}
onMouseMove={(e) => setMousePos({ x: e.clientX, y: e.clientY })}>
<div className="inventory-panel" onClick={e => e.stopPropagation()}>
<button className="inv-close" onClick={() => { setShowInventory(false); setHoverInfo(null) }}>X</button>
<button className="inv-close" onClick={() => { setShowInventory(false); setPanelTarget('player'); setHoverInfo(null) }}>X</button>
<div className="inv-layout">
<div className="inv-body-col">
{panelTarget === 'player' ? (
<BodyImage body={bodyParts} />
) : combat?.enemies[panelTarget] ? (
<BodyImage body={combat.enemies[panelTarget].integrity} />
) : null}
</div>
<div className="inv-right-col">
<div className="inv-tabs">
@@ -1504,6 +1420,8 @@ function App() {
onClick={() => setInvTab('stats')}>Statistics</button>
</div>
{panelTarget === 'player' ? (
<>
{invTab === 'health' && (
<div className="inv-parts-col">
<div className="inv-parts-title">Body Parts</div>
@@ -1606,9 +1524,6 @@ function App() {
<div className="stat-row"><span className="stat-label">Name</span><span className="stat-value">You</span></div>
<div className="stat-row"><span className="stat-label">Age</span><span className="stat-value">24</span></div>
<div className="stat-row"><span className="stat-label">Location</span><span className="stat-value">{locMap[playerLoc]?.name ?? playerLoc}</span></div>
<div className="stat-row"><span className="stat-label">Day</span><span className="stat-value">{gameTime.day}</span></div>
<div className="stat-row"><span className="stat-label">Time</span><span className="stat-value">{String(gameTime.hour).padStart(2, '0')}:00</span></div>
<div className="stat-row"><span className="stat-label">Weather</span><span className="stat-value">{weatherLabels[weather]}</span></div>
<div className="inv-divider" />
<div className="stat-row"><span className="stat-label">Equipped</span><span className="stat-value">{equippedWeapons.left === equippedWeapons.right ? (equippedWeapons.left === 'fists' ? 'Fists' : `Both: ${weaponMap[equippedWeapons.left].name}`) : [equippedWeapons.left !== 'fists' ? `L:${weaponMap[equippedWeapons.left].name}` : '', equippedWeapons.right !== 'fists' ? `R:${weaponMap[equippedWeapons.right].name}` : ''].filter(Boolean).join(' + ')}</span></div>
{(() => {
@@ -1635,6 +1550,98 @@ function App() {
<div className="stat-row"><span className="stat-label">Money</span><span className="stat-value">{playerMoney}g</span></div>
</div>
)}
</>
) : combat?.enemies[panelTarget] ? (
<>
{invTab === 'health' && (
<div className="inv-parts-col">
<div className="inv-parts-title">Body Parts</div>
{Object.keys(MAX_BODY).map(part => {
const hp = combat.enemies[panelTarget].integrity[part]
const max = MAX_BODY[part]
const injuries = combat.enemies[panelTarget].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>{Object.values(combat.enemies[panelTarget].integrity).reduce((a, b) => a + b, 0)}/{MAX_HP}</span>
</div>
</div>
)}
{invTab === 'inventory' && (
<div className="inv-inv-content">
<div className="inv-parts-title">Inventory ({(combat.enemies[panelTarget].inventory || []).length})</div>
{(combat.enemies[panelTarget].inventory || []).length === 0 ? (
<div className="inv-empty">Empty</div>
) : (
<>
{combat.enemies[panelTarget].inventory.map((itemId, i) => (
<div key={i} className="inv-item"
onMouseEnter={() => setHoverInfo({ type: 'item', id: itemId })}
onMouseLeave={() => setHoverInfo(null)}>
{itemMap[itemId]?.name ?? itemId}
</div>
))}
</>
)}
<div className="inv-money">{combat.enemies[panelTarget].money || 0}g</div>
</div>
)}
{invTab === 'stats' && (
<div className="inv-stats-content">
{(() => {
const e = combat.enemies[panelTarget]
const wid = e.weapon ?? 'fists'
const wep = weaponMap[wid]
return (
<>
<div className="inv-parts-title">{e.name}</div>
<div className="stat-row"><span className="stat-label">Name</span><span className="stat-value">{e.name}</span></div>
<div className="stat-row"><span className="stat-label">Age</span><span className="stat-value">{e.age ?? '??'}</span></div>
<div className="stat-row"><span className="stat-label">Location</span><span className="stat-value">{e.location ?? 'Unknown'}</span></div>
<div className="inv-divider" />
<div className="stat-row"><span className="stat-label">Weapon</span><span className="stat-value">{wep?.name ?? wid}</span></div>
{wep && wid !== 'fists' && (
<><div className="stat-row"><span className="stat-label">DMG</span><span className="stat-value">{e.attack}</span></div>
<div className="stat-row"><span className="stat-label">SPD</span><span className="stat-value">{e.speed}</span></div></>
)}
{wep && wep.skills.length > 0 && (
<div className="stat-row"><span className="stat-label">Skills</span><span className="stat-value">{wep.skills.map(s => s.name).join(', ')}</span></div>
)}
{wid === 'fists' && (
<div className="stat-row"><span className="stat-label">Attack</span><span className="stat-value">{e.attack}</span></div>
)}
<div className="inv-divider" />
<div className="stat-row"><span className="stat-label">Integrity</span><span className="stat-value">{Object.values(e.integrity).reduce((a, b) => a + b, 0)}/{MAX_HP}</span></div>
<div className="stat-row"><span className="stat-label">Items</span><span className="stat-value">{(e.inventory || []).length}</span></div>
<div className="stat-row"><span className="stat-label">Money</span><span className="stat-value">{e.money || 0}g</span></div>
</>
)
})()}
</div>
)}
</>
) : null}
</div>
</div>
</div>
@@ -1688,7 +1695,20 @@ function App() {
</div>
</div>
)}
</>
{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">Day</span><span className="info-value">{gameTime.day}</span></div>
<div className="info-row"><span className="info-label">Time</span><span className="info-value">{String(gameTime.hour).padStart(2, '0')}:00</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[playerLoc]?.name ?? playerLoc}</span></div>
</div>
</div>
</div>
)}
{hoverInfo?.type === 'enemy' && (