combat ui

This commit is contained in:
2026-06-01 03:10:14 -04:00
parent d93c7c87da
commit 23577dbfd7
+116 -53
View File
@@ -74,6 +74,8 @@ items.forEach(i => { itemMap[i.id] = i })
const weaponMap = {} const weaponMap = {}
weapons.forEach(w => { weaponMap[w.id] = w }) weapons.forEach(w => { weaponMap[w.id] = w })
weaponMap['left_arm'] = { id: 'left_arm', name: 'Left Arm', damage: 35, condition: 100, maxCondition: 100, passives: ['Brawler'], weight: 0, speed: 14, skills: [{ name: 'Jab', mult: 1.0, damageType: 'blunt', range: 12 }, { name: 'Hook', mult: 1.3, damageType: 'blunt', range: 12 }, { name: 'Kick', mult: 1.5, damageType: 'blunt', range: 14 }, { name: 'Shove', mult: 0, damageType: 'shove', range: 10 }] }
weaponMap['right_arm'] = { id: 'right_arm', name: 'Right Arm', damage: 35, condition: 100, maxCondition: 100, passives: ['Brawler'], weight: 0, speed: 14, skills: [{ name: 'Jab', mult: 1.0, damageType: 'blunt', range: 12 }, { name: 'Hook', mult: 1.3, damageType: 'blunt', range: 12 }, { name: 'Kick', mult: 1.5, damageType: 'blunt', range: 14 }, { name: 'Shove', mult: 0, damageType: 'shove', range: 10 }] }
const MAX_BODY = { head: 100, torso: 100, leftArm: 100, rightArm: 100, leftLeg: 100, rightLeg: 100 } const MAX_BODY = { head: 100, torso: 100, leftArm: 100, rightArm: 100, leftLeg: 100, rightLeg: 100 }
const MAX_HP = Object.values(MAX_BODY).reduce((a, b) => a + b, 0) const MAX_HP = Object.values(MAX_BODY).reduce((a, b) => a + b, 0)
@@ -245,6 +247,8 @@ function App() {
const [limbSelect, setLimbSelect] = useState(null) const [limbSelect, setLimbSelect] = useState(null)
const [selectedTarget, setSelectedTarget] = useState(null) const [selectedTarget, setSelectedTarget] = useState(null)
const [selectedSkill, setSelectedSkill] = useState(null) const [selectedSkill, setSelectedSkill] = useState(null)
const [hoveredSkill, setHoveredSkill] = useState(null)
const [hoveredEnemy, setHoveredEnemy] = useState(null)
const [selectedEnemy, setSelectedEnemy] = useState(0) const [selectedEnemy, setSelectedEnemy] = useState(0)
const [targetEnemy, setTargetEnemy] = useState(null) const [targetEnemy, setTargetEnemy] = useState(null)
const [attackEnemy, setAttackEnemy] = useState(null) const [attackEnemy, setAttackEnemy] = useState(null)
@@ -1017,57 +1021,52 @@ function App() {
</div> </div>
)} )}
{limbSelect && ( {limbSelect && (
<div className="inventory-overlay" onClick={() => cancelAttack()} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 0 }}> <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()}> <div className="inventory-panel limb-select-panel" onClick={e => e.stopPropagation()}>
<button className="inv-close" onClick={() => cancelAttack()}>X</button>
{(() => { {(() => {
const weapId = limbSelect const weapId = limbSelect
const isArm = weapId === 'left_arm' || weapId === 'right_arm' const weapon = weaponMap[weapId]
const weapon = !isArm ? weaponMap[weapId] : null const weaponName = weapon?.name ?? weapId
const weaponName = isArm ? (weapId === 'left_arm' ? 'Left Arm' : 'Right Arm') : (weapon?.name ?? weapId)
const skills = weapon?.skills ?? [] const skills = weapon?.skills ?? []
if (isArm && !selectedSkill) selectSkill('Punch')
return ( return (
<> <div style={{ position: 'relative', minHeight: 520 }}>
<div className="inv-parts-title limb-select-title">{weaponName}</div> <div className="inv-parts-title limb-select-title">{weaponName}</div>
{!selectedSkill && skills.length > 0 ? ( <div style={{ display: 'flex', gap: 16, alignItems: 'flex-start', paddingRight: 250 }}>
<div className="limb-select-skills"> {skills.length > 0 && (
<div className="inv-parts-title" style={{ marginBottom: 8 }}>Select Skill</div> <div>
<div className="limb-select-grid"> <div className="inv-parts-title" style={{ marginBottom: 8 }}>Select Skill</div>
{skills.map((skill, i) => ( <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
<button key={i} className="limb-target-btn" onClick={() => { setSelectedSkill(skill.name); setAttackEnemy(null) }}> {skills.map((skill, i) => (
<div style={{ fontWeight: 'bold' }}>{skill.name}</div> <button key={i} className={'limb-target-btn' + (selectedSkill === skill.name ? ' limb-target-selected' : '')} onClick={() => { setSelectedSkill(skill.name); setAttackEnemy(null) }}
<div style={{ fontSize: 11, color: '#6b7280' }}>{skill.damageType} x{skill.mult} (rng {skill.range})</div> onMouseEnter={() => setHoveredSkill(skill.name)}
</button> onMouseLeave={() => setHoveredSkill(null)}>
))} <div style={{ fontWeight: 'bold' }}>{skill.name}</div>
</button>
))}
</div>
</div> </div>
</div> )}
) : selectedSkill && attackEnemy === null ? ( {selectedSkill && (
<div className="limb-select-enemies"> <div>
<div className="inv-parts-title" style={{ marginBottom: 8 }}>Select Enemy</div> <div className="inv-parts-title" style={{ marginBottom: 8 }}>Select Enemy</div>
<div className="limb-select-grid"> <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
{(combat?.enemies || []).map((enemy, i) => ( {(combat?.enemies || []).map((enemy, i) => {
<button key={i} className="limb-target-btn" onClick={() => setAttackEnemy(i)} const inRange = canAttack(limbSelect, selectedSkill, i)
style={{ textAlign: 'center' }}> return (
<div style={{ fontWeight: 'bold' }}>{enemy.name}</div> <button key={i} className={'limb-target-btn' + (attackEnemy === i ? ' limb-target-selected' : '')} onClick={() => setAttackEnemy(i)}
<div style={{ fontSize: 11, color: '#6b7280' }}>HP: {Object.values(enemy.integrity).reduce((a, b) => a + b, 0)}</div> style={{ opacity: inRange ? 1 : 0.4, textAlign: 'center' }}
</button> disabled={!inRange}
))} onMouseEnter={() => setHoveredEnemy(i)}
</div> onMouseLeave={() => setHoveredEnemy(null)}>
</div> <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>
<div className="limb-select-body" style={{ display: 'flex', gap: 16 }}> </button>
<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> </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>
)}
{attackEnemy !== null && (
<div> <div>
<div className="inv-parts-title" style={{ marginBottom: 8 }}>Limb</div> <div className="inv-parts-title" style={{ marginBottom: 8 }}>Limb</div>
<div className="limb-target-grid" style={{ display: 'flex', flexDirection: 'column', gap: 4 }}> <div className="limb-target-grid" style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
@@ -1092,18 +1091,82 @@ function App() {
})} })}
</div> </div>
</div> </div>
)}
</div>
{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 && skills.length > 0) {
const sName = hoveredSkill
const s = skills.find(x => x.name === sName)
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} attack</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 (selectedSkill && attackEnemy === null) {
const ei = hoveredEnemy
const e = ei !== null && combat?.enemies[ei]
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>
<div><span style={{ color: '#6b7280' }}>Head: </span><span>{e.integrity.head}</span></div>
<div><span style={{ color: '#6b7280' }}>Torso: </span><span>{e.integrity.torso}</span></div>
<div><span style={{ color: '#6b7280' }}>L.Arm: </span><span>{e.integrity.larm}</span></div>
<div><span style={{ color: '#6b7280' }}>R.Arm: </span><span>{e.integrity.rarm}</span></div>
<div><span style={{ color: '#6b7280' }}>L.Leg: </span><span>{e.integrity.lleg}</span></div>
<div><span style={{ color: '#6b7280' }}>R.Leg: </span><span>{e.integrity.rleg}</span></div>
</div>
</>
)
}
if (selectedSkill && attackEnemy !== null) {
const e = combat?.enemies[attackEnemy]
const limbKeyMap = { torso: 'torso', head: 'head', leftArm: 'larm', rightArm: 'rarm', leftLeg: 'lleg', rightLeg: 'rleg' }
const limbKey = selectedTarget ? (limbKeyMap[selectedTarget] || selectedTarget) : null
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 limb: {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>
</>
)
}
return null
})()}
</div>
)}
</div> </div>
)} )})()}
</> </div>
) <div style={{ display: 'flex', gap: 8 }}>
})()} <button className="limb-confirm-btn" onClick={() => executeAttack()}
disabled={!selectedSkill || attackEnemy === null || !selectedTarget || !!moveAnim || !!attackAnim || playerHp <= 0}
style={{ opacity: !selectedSkill || attackEnemy === null || !selectedTarget || !!moveAnim || !!attackAnim || playerHp <= 0 ? 0.4 : 1 }}>Confirm</button>
<button className="limb-cancel-btn" onClick={() => cancelAttack()} style={{ width: 'auto', padding: '10px 24px' }}>Cancel</button>
</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>
)} )}
</div> </div>