combat ui
This commit is contained in:
+116
-53
@@ -74,6 +74,8 @@ items.forEach(i => { itemMap[i.id] = i })
|
||||
|
||||
const weaponMap = {}
|
||||
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_HP = Object.values(MAX_BODY).reduce((a, b) => a + b, 0)
|
||||
@@ -245,6 +247,8 @@ function App() {
|
||||
const [limbSelect, setLimbSelect] = useState(null)
|
||||
const [selectedTarget, setSelectedTarget] = useState(null)
|
||||
const [selectedSkill, setSelectedSkill] = useState(null)
|
||||
const [hoveredSkill, setHoveredSkill] = useState(null)
|
||||
const [hoveredEnemy, setHoveredEnemy] = useState(null)
|
||||
const [selectedEnemy, setSelectedEnemy] = useState(0)
|
||||
const [targetEnemy, setTargetEnemy] = useState(null)
|
||||
const [attackEnemy, setAttackEnemy] = useState(null)
|
||||
@@ -1017,57 +1021,52 @@ function App() {
|
||||
</div>
|
||||
)}
|
||||
{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()}>
|
||||
<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 weapon = weaponMap[weapId]
|
||||
const weaponName = weapon?.name ?? weapId
|
||||
const skills = weapon?.skills ?? []
|
||||
if (isArm && !selectedSkill) selectSkill('Punch')
|
||||
return (
|
||||
<>
|
||||
<div style={{ position: 'relative', minHeight: 520 }}>
|
||||
<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 style={{ display: 'flex', gap: 16, alignItems: 'flex-start', paddingRight: 250 }}>
|
||||
{skills.length > 0 && (
|
||||
<div>
|
||||
<div className="inv-parts-title" style={{ marginBottom: 8 }}>Select Skill</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
{skills.map((skill, i) => (
|
||||
<button key={i} className={'limb-target-btn' + (selectedSkill === skill.name ? ' limb-target-selected' : '')} onClick={() => { setSelectedSkill(skill.name); setAttackEnemy(null) }}
|
||||
onMouseEnter={() => setHoveredSkill(skill.name)}
|
||||
onMouseLeave={() => setHoveredSkill(null)}>
|
||||
<div style={{ fontWeight: 'bold' }}>{skill.name}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</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>
|
||||
)}
|
||||
)}
|
||||
{selectedSkill && (
|
||||
<div>
|
||||
<div className="inv-parts-title" style={{ marginBottom: 8 }}>Select Enemy</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
{(combat?.enemies || []).map((enemy, i) => {
|
||||
const inRange = canAttack(limbSelect, selectedSkill, i)
|
||||
return (
|
||||
<button key={i} className={'limb-target-btn' + (attackEnemy === i ? ' limb-target-selected' : '')} onClick={() => setAttackEnemy(i)}
|
||||
style={{ opacity: inRange ? 1 : 0.4, textAlign: 'center' }}
|
||||
disabled={!inRange}
|
||||
onMouseEnter={() => setHoveredEnemy(i)}
|
||||
onMouseLeave={() => setHoveredEnemy(null)}>
|
||||
<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>
|
||||
)}
|
||||
{attackEnemy !== null && (
|
||||
<div>
|
||||
<div className="inv-parts-title" style={{ marginBottom: 8 }}>Limb</div>
|
||||
<div className="limb-target-grid" style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
@@ -1092,18 +1091,82 @@ function App() {
|
||||
})}
|
||||
</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 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>
|
||||
{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>
|
||||
|
||||
Reference in New Issue
Block a user