i don even know anymore

This commit is contained in:
2026-06-03 00:10:21 -04:00
parent eba2031b99
commit 4eca9df6cc
2 changed files with 149 additions and 98 deletions
+23 -23
View File
@@ -2009,7 +2009,7 @@ body {
} }
.location-log-col { .location-log-col {
width: 200px; width: 280px;
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -2017,17 +2017,19 @@ body {
} }
.location-log-title { .location-log-title {
color: #6b7280; padding: 12px 16px;
font-size: 11px; font-size: 14px;
text-transform: uppercase; font-weight: bold;
letter-spacing: 1px; color: #e0e0e0;
padding: 14px 16px 8px; border-bottom: 1px solid #2e303a;
} }
.location-log-content { .location-log-content {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
padding: 0 16px 12px; padding: 0 16px 12px;
min-height: 0;
max-height: 130px;
} }
.location-log-line { .location-log-line {
@@ -2119,53 +2121,51 @@ body {
.location-surrounding-desc { .location-surrounding-desc {
color: #9ca3af; color: #9ca3af;
font-size: 13px; font-size: 13px;
padding: 12px 16px; padding: 14px 16px;
line-height: 1.5; line-height: 1.5;
border-bottom: 1px solid #1f2028; border-bottom: 1px solid #1f2028;
} }
.location-surrounding-connections {
padding: 8px 16px 12px;
border-bottom: 1px solid #1f2028;
}
.location-surrounding-shops {
padding: 8px 16px 12px;
}
.location-surrounding-subtitle { .location-surrounding-subtitle {
color: #6b7280; color: #6b7280;
font-size: 11px; font-size: 11px;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 1px; letter-spacing: 1px;
margin-bottom: 6px; padding: 14px 16px 6px;
margin-top: 4px;
} }
.location-surrounding-connection { .location-surrounding-connection {
color: #5F71C5; color: #5F71C5;
font-size: 13px; font-size: 13px;
padding: 3px 0; padding: 10px 16px;
border-bottom: 1px solid #1f2028;
} }
.location-surrounding-shop { .location-surrounding-shop {
color: #e0e0e0; color: #e0e0e0;
font-size: 13px; font-size: 13px;
padding: 3px 0; padding: 10px 16px;
border-bottom: 1px solid #1f2028;
}
.location-surrounding-corpse {
color: #6b7280;
font-size: 13px;
padding: 10px 16px;
border-bottom: 1px solid #1f2028;
} }
.location-npc-row { .location-npc-row {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
padding: 14px 16px; padding: 10px 16px;
border-bottom: 1px solid #1f2028; border-bottom: 1px solid #1f2028;
border-radius: 1px;
} }
.location-npc-name { .location-npc-name {
color: #e0e0e0; color: #e0e0e0;
font-size: 16px; font-size: 15px;
font-weight: bold; font-weight: bold;
flex: 1; flex: 1;
} }
+126 -75
View File
@@ -119,6 +119,7 @@ function partColor(hp, max) {
function freshBody() { return { head: 100, torso: 100, leftArm: 100, rightArm: 100, leftLeg: 100, rightLeg: 100 } } function freshBody() { return { head: 100, torso: 100, leftArm: 100, rightArm: 100, leftLeg: 100, rightLeg: 100 } }
function freshInjuries(){ return { head: [], torso: [], leftArm: [], rightArm: [], leftLeg: [], rightLeg: [] } } function freshInjuries(){ return { head: [], torso: [], leftArm: [], rightArm: [], leftLeg: [], rightLeg: [] } }
function totalHp(body) { return Object.values(body).reduce((a, b) => a + b, 0) } function totalHp(body) { return Object.values(body).reduce((a, b) => a + b, 0) }
function isDead(integrity) { return !integrity || integrity.head <= 0 || integrity.torso <= 0 }
function randomPart() { function randomPart() {
const parts = ['head', 'torso', 'leftArm', 'rightArm', 'leftLeg', 'rightLeg'] const parts = ['head', 'torso', 'leftArm', 'rightArm', 'leftLeg', 'rightLeg']
@@ -204,6 +205,7 @@ function computeShove(attackerPos, targetPos) {
* Returns { dmg, severity, injType, injLabel } * Returns { dmg, severity, injType, injLabel }
*/ */
function computeHit({ weaponId, skill, weaponConditions, bothMult = 1.0 }) { function computeHit({ weaponId, skill, weaponConditions, bothMult = 1.0 }) {
if (skill.damageType === 'shove') return { dmg: 0, severity: 0, injType: 'shove', injLabel: 'shove' }
const base = weaponMap[weaponId] const base = weaponMap[weaponId]
if (!base) return { dmg: 1, severity: 1, injType: 'cut', injLabel: 'cut' } if (!base) return { dmg: 1, severity: 1, injType: 'cut', injLabel: 'cut' }
@@ -280,7 +282,7 @@ function runEnemyAI(combat, playerParts, playerInjuries) {
for (let i = 0; i < combat.enemies.length; i++) { for (let i = 0; i < combat.enemies.length; i++) {
const e = combat.enemies[i] const e = combat.enemies[i]
if (e.integrity.head <= 0) continue if (isDead(e.integrity)) continue
const dist = Math.sqrt( const dist = Math.sqrt(
(combat.playerPos.x - e.pos.x) ** 2 + (combat.playerPos.y - e.pos.y) ** 2 (combat.playerPos.x - e.pos.x) ** 2 + (combat.playerPos.y - e.pos.y) ** 2
@@ -360,6 +362,7 @@ export default function App() {
const [weaponConditions,setWeaponConditions]= useState({ rusty_sword: 65 }) const [weaponConditions,setWeaponConditions]= useState({ rusty_sword: 65 })
const [gameTime, setGameTime] = useState({ day: 1, hour: 8 }) const [gameTime, setGameTime] = useState({ day: 1, hour: 8 })
const [weather, setWeather] = useState('clear') const [weather, setWeather] = useState('clear')
const [defeatedNpcs, setDefeatedNpcs] = useState([])
// ── ui state // ── ui state
const [hovered, setHovered] = useState(null) const [hovered, setHovered] = useState(null)
@@ -407,7 +410,7 @@ export default function App() {
const [moveMode, setMoveMode] = useState(null) const [moveMode, setMoveMode] = useState(null)
// ── animation state // ── animation state
const [attackLerp, setAttackLerp] = useState(null) const [attackLerp, setAttackLerp] = useState({ player: null, enemies: {} })
const [hitShake, setHitShake] = useState(null) const [hitShake, setHitShake] = useState(null)
const [minimapView, setMinimapView] = useState({ x: 0, y: 0, scale: 1 }) const [minimapView, setMinimapView] = useState({ x: 0, y: 0, scale: 1 })
@@ -454,7 +457,7 @@ export default function App() {
const playerHp = totalHp(bodyParts) const playerHp = totalHp(bodyParts)
const currentWeight = playerInventory.reduce((sum, id) => sum + (itemMap[id]?.weight ?? 0), 0) const currentWeight = playerInventory.reduce((sum, id) => sum + (itemMap[id]?.weight ?? 0), 0)
const shopAtLoc = shops.find(s => s.locationId === selected && s.locationId === playerLoc) const shopAtLoc = shops.find(s => s.locationId === selected && s.locationId === playerLoc)
const liveCharacters= characters const liveCharacters= characters.filter(c => !defeatedNpcs.includes(c.id))
const connectedTo = (locId) => { const connectedTo = (locId) => {
const result = [] const result = []
@@ -469,7 +472,7 @@ export default function App() {
// TIMER — only ticks while timerActive is true // TIMER — only ticks while timerActive is true
// ───────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────
const LUNGE_DIST = 15 const LUNGE_DIST = 18
function makeAttackAnim(action, cc) { function makeAttackAnim(action, cc) {
const isPlayer = action.actor === 'player' const isPlayer = action.actor === 'player'
@@ -512,20 +515,29 @@ export default function App() {
* Advance an attack animation by dt seconds. Returns true when done. * Advance an attack animation by dt seconds. Returns true when done.
*/ */
function tickAttack(anim, dt) { function tickAttack(anim, dt) {
const liveCc = combatRef.current
if (anim.actor === 'player' && isDead(bodyPartsRef.current)) return true
if (anim.actor === 'enemy') {
const e = liveCc?.enemies[anim.enemyIndex]
if (!e || isDead(e.integrity)) { enemyActionsRef.current[anim.enemyIndex] = null; return true }
}
anim.elapsed += dt anim.elapsed += dt
const hitAt = 0.12 const hitAt = 0.15
const returnAt = 0.30
const t = anim.elapsed < hitAt const t = anim.elapsed < hitAt
? anim.elapsed / hitAt ? anim.elapsed / hitAt
: anim.elapsed < 0.25 : anim.elapsed < returnAt
? 1 - (anim.elapsed - hitAt) / (0.25 - hitAt) ? 1 - (anim.elapsed - hitAt) / (returnAt - hitAt)
: 0 : 0
setAttackLerp({ actor: anim.actor, from: anim.startPos, to: anim.lungeTarget, t, enemyIndex: anim.enemyIndex }) setAttackLerp(prev => anim.actor === 'player'
? { ...prev, player: { from: anim.startPos, to: anim.lungeTarget, t } }
: { ...prev, enemies: { ...prev.enemies, [anim.enemyIndex]: { from: anim.startPos, to: anim.lungeTarget, t } } })
if (anim.elapsed >= hitAt && !anim.hitFired) { if (anim.elapsed >= hitAt && !anim.hitFired) {
anim.hitFired = true anim.hitFired = true
const liveCc = combatRef.current
let inRange = false let inRange = false
if (liveCc) { if (liveCc) {
if (anim.actor === 'player') { if (anim.actor === 'player') {
@@ -541,35 +553,38 @@ export default function App() {
} }
} }
setHitShake({ enemyIndex: anim.enemyIndex, start: performance.now() })
if (!inRange) { if (!inRange) {
const msg = anim.actor === 'player' const msg = anim.actor === 'player'
? 'Your attack misses!' ? 'Your attack misses!'
: `${liveCc?.enemies[anim.enemyIndex]?.name ?? 'Enemy'}'s attack misses!` : `${liveCc?.enemies[anim.enemyIndex]?.name ?? 'Enemy'}'s attack misses!`
setCombat(prev => ({ ...prev, log: [...prev.log, msg] })) setCombat(prev => ({ ...prev, log: [...prev.log, msg] }))
} else if (anim.actor === 'player') { } else {
// ── Player attacking enemy ── setHitShake({ actor: anim.actor, enemyIndex: anim.enemyIndex, start: performance.now() })
if (anim.actor === 'player') {
// ── Player attacking enemy ──
const wid = anim.weaponId const wid = anim.weaponId
if (wid !== 'fists' && wid !== 'left_arm' && wid !== 'right_arm') { if (wid !== 'fists' && wid !== 'left_arm' && wid !== 'right_arm') {
const base = weaponMap[wid] const base = weaponMap[wid]
setWeaponConditions(prev => ({ ...prev, [wid]: Math.max(0, (prev[wid] ?? base.maxCondition) - (1 + Math.floor(Math.random() * 2))) })) setWeaponConditions(prev => ({ ...prev, [wid]: Math.max(0, (prev[wid] ?? base.maxCondition) - (1 + Math.floor(Math.random() * 2))) }))
} }
const decayedPlayer = calcIntegrity(anim.attackerParts, anim.attackerInjuries, hitAt) const decayedPlayer = calcIntegrity(bodyPartsRef.current, bodyInjuriesRef.current, hitAt)
if (liveCc) { if (liveCc) {
const enemy = liveCc.enemies[anim.enemyIndex] const enemy = liveCc.enemies[anim.enemyIndex]
if (enemy) { if (enemy) {
const decayedEnemy = calcIntegrity(anim.targetParts, anim.targetInjuries, hitAt) const decayedEnemy = calcIntegrity(enemy.integrity, enemy.injuries, hitAt)
const hitEnemy = { ...decayedEnemy, [anim.targetPart]: Math.max(0, decayedEnemy[anim.targetPart] - anim.dmg) } const prevHp = decayedEnemy[anim.targetPart]
const newInjuries = applyInjury(anim.targetInjuries, anim.targetPart, anim.injType, anim.severity) const hitEnemy = { ...decayedEnemy, [anim.targetPart]: Math.max(0, prevHp - anim.dmg) }
const newInjuries = prevHp > 0 && anim.severity > 0
? applyInjury(enemy.injuries, anim.targetPart, anim.injType, anim.severity)
: enemy.injuries
const log = [ const log = anim.dmg > 0
`You ${anim.skillName?.toLowerCase() ?? 'attack'} ${enemy.name}'s ${bodyPartLabels[anim.targetPart]}${anim.injLabel} (severity ${anim.severity}, -${anim.dmg} integrity).`, ? [`You ${anim.skillName?.toLowerCase() ?? 'attack'} ${enemy.name}'s ${bodyPartLabels[anim.targetPart]}${anim.injLabel} (severity ${anim.severity}, -${anim.dmg} integrity).`]
] : []
if (hitEnemy.head <= 0) { if (isDead(hitEnemy)) {
log.push(`You crush ${enemy.name}'s head!`) if (anim.targetPart === 'head') log.push(`You crush ${enemy.name}'s head!`)
log.push(`${enemy.name} is defeated!`) log.push(`${enemy.name} is defeated!`)
} }
@@ -582,13 +597,19 @@ export default function App() {
} }
setBodyParts(decayedPlayer) setBodyParts(decayedPlayer)
setBodyInjuries(anim.attackerInjuries) setBodyInjuries(bodyInjuriesRef.current)
} else { } else {
// ── Enemy attacking player ── // ── Enemy attacking player ──
const decayedEnemy = calcIntegrity(anim.attackerParts, anim.attackerInjuries, hitAt) const currentEnemy = liveCc?.enemies[anim.enemyIndex]
const decayedEnemy = currentEnemy
const hitPlayer = { ...anim.targetParts, [anim.targetPart]: Math.max(0, (anim.targetParts[anim.targetPart] ?? 100) - anim.dmg) } ? calcIntegrity(currentEnemy.integrity, currentEnemy.injuries, hitAt)
const newInjuries = applyInjury(anim.targetInjuries, anim.targetPart, anim.injType, anim.severity) : calcIntegrity(anim.attackerParts, anim.attackerInjuries, hitAt)
const decayedPlayer = calcIntegrity(bodyPartsRef.current, bodyInjuriesRef.current, hitAt)
const prevHp = decayedPlayer[anim.targetPart]
const hitPlayer = { ...decayedPlayer, [anim.targetPart]: Math.max(0, prevHp - anim.dmg) }
const newInjuries = prevHp > 0 && anim.severity > 0
? applyInjury(bodyInjuriesRef.current, anim.targetPart, anim.injType, anim.severity)
: bodyInjuriesRef.current
if (liveCc) { if (liveCc) {
const enemy = liveCc.enemies[anim.enemyIndex] const enemy = liveCc.enemies[anim.enemyIndex]
@@ -597,17 +618,22 @@ export default function App() {
setCombat(prev => ({ setCombat(prev => ({
...prev, ...prev,
enemies: prev.enemies.map((e, i) => i === anim.enemyIndex ? { ...e, integrity: decayedEnemy } : e), enemies: prev.enemies.map((e, i) => i === anim.enemyIndex ? { ...e, integrity: decayedEnemy } : e),
log: [...prev.log, `${enemyName} lands a ${anim.injLabel} on your ${bodyPartLabels[anim.targetPart]} (severity ${anim.severity}, integrity -${anim.dmg}).`], log: anim.dmg > 0
? [...prev.log, `${enemyName} lands a ${anim.injLabel} on your ${bodyPartLabels[anim.targetPart]} (severity ${anim.severity}, integrity -${anim.dmg}).`]
: prev.log,
})) }))
} }
setBodyParts(hitPlayer) setBodyParts(hitPlayer)
setBodyInjuries(newInjuries) setBodyInjuries(newInjuries)
} }
}
} }
if (anim.elapsed >= 0.35) { if (anim.elapsed >= 0.45) {
setAttackLerp(null) setAttackLerp(prev => anim.actor === 'player'
? { ...prev, player: null }
: { ...prev, enemies: { ...prev.enemies, [anim.enemyIndex]: undefined } })
setHitShake(null) setHitShake(null)
return true return true
} }
@@ -715,7 +741,7 @@ export default function App() {
if (anim.type === 'enemyMove') { if (anim.type === 'enemyMove') {
const e = fresh?.enemies[i] ?? cc.enemies[i] const e = fresh?.enemies[i] ?? cc.enemies[i]
const finalPos = { x: anim.targets[i].toX, y: anim.targets[i].toY } const finalPos = { x: anim.targets[i].toX, y: anim.targets[i].toY }
if (e && e.integrity.head > 0) { if (e && !isDead(e.integrity)) {
const dist = Math.sqrt( const dist = Math.sqrt(
((fresh ?? cc).playerPos.x - finalPos.x) ** 2 + ((fresh ?? cc).playerPos.y - finalPos.y) ** 2 ((fresh ?? cc).playerPos.x - finalPos.x) ** 2 + ((fresh ?? cc).playerPos.y - finalPos.y) ** 2
) )
@@ -800,7 +826,7 @@ export default function App() {
useEffect(() => { useEffect(() => {
if (!combat || replayActive) return if (!combat || replayActive) return
if (playerHp <= 0 || !combat.enemies.some(e => e.integrity.head > 0)) endFight() if (isDead(bodyParts) || !combat.enemies.some(e => !isDead(e.integrity))) endFight()
}, [combat, bodyParts, playerHp]) }, [combat, bodyParts, playerHp])
// ───────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────
@@ -814,7 +840,7 @@ export default function App() {
combat: JSON.parse(JSON.stringify(combat)), combat: JSON.parse(JSON.stringify(combat)),
bodyParts: JSON.parse(JSON.stringify(bodyParts)), bodyParts: JSON.parse(JSON.stringify(bodyParts)),
bodyInjuries: JSON.parse(JSON.stringify(bodyInjuries)), bodyInjuries: JSON.parse(JSON.stringify(bodyInjuries)),
attackLerp: attackLerp ? { ...attackLerp } : null, attackLerp: JSON.parse(JSON.stringify(attackLerp)),
hitShake: hitShake ? { ...hitShake } : null, hitShake: hitShake ? { ...hitShake } : null,
}) })
} }
@@ -894,7 +920,7 @@ export default function App() {
const rec = fightRecord const rec = fightRecord
const f0 = rec.frames[0] const f0 = rec.frames[0]
setCombat(f0.combat); setBodyParts(f0.bodyParts); setBodyInjuries(f0.bodyInjuries) setCombat(f0.combat); setBodyParts(f0.bodyParts); setBodyInjuries(f0.bodyInjuries)
setAttackLerp(f0.attackLerp ?? null); setHitShake(f0.hitShake ?? null) setAttackLerp(f0.attackLerp && 'player' in f0.attackLerp ? f0.attackLerp : { player: null, enemies: {} }); setHitShake(f0.hitShake ?? null)
setCombatTime(0); setDisplayTime(0) setCombatTime(0); setDisplayTime(0)
replayIdxRef.current = 0 replayIdxRef.current = 0
@@ -906,7 +932,7 @@ export default function App() {
replayIdxRef.current = idx replayIdxRef.current = idx
const f = rec.frames[idx] const f = rec.frames[idx]
setCombat(f.combat); setBodyParts(f.bodyParts); setBodyInjuries(f.bodyInjuries) setCombat(f.combat); setBodyParts(f.bodyParts); setBodyInjuries(f.bodyInjuries)
setAttackLerp(f.attackLerp ?? null); setHitShake(f.hitShake ?? null) setAttackLerp(f.attackLerp && 'player' in f.attackLerp ? f.attackLerp : { player: null, enemies: {} }); setHitShake(f.hitShake ?? null)
setCombatTime(f.time) setCombatTime(f.time)
} }
if (idx >= rec.frames.length - 1) return if (idx >= rec.frames.length - 1) return
@@ -950,7 +976,7 @@ export default function App() {
enemyActionsRef.current = initCombat.enemies.map(() => null) enemyActionsRef.current = initCombat.enemies.map(() => null)
positionsRef.current = {} positionsRef.current = {}
setSelectedEnemy(0); setReplayActive(false) setSelectedEnemy(0); setReplayActive(false)
setExpandedWeapon('ROOT') setExpandedWeapon('ROOT'); setAttackLerp({ player: null, enemies: {} }); setHitShake(null); setMoveMode(null)
recordingRef.current = { recordingRef.current = {
initialCombat: JSON.parse(JSON.stringify(initCombat)), initialCombat: JSON.parse(JSON.stringify(initCombat)),
frames: [{ time: 0, combat: JSON.parse(JSON.stringify(initCombat)), bodyParts: freshBody(), bodyInjuries: freshInjuries(), attackLerp: null, hitShake: null }], frames: [{ time: 0, combat: JSON.parse(JSON.stringify(initCombat)), bodyParts: freshBody(), bodyInjuries: freshInjuries(), attackLerp: null, hitShake: null }],
@@ -961,29 +987,37 @@ export default function App() {
if (recordingRef.current) { pushSnapshot(); setFightRecord(recordingRef.current); recordingRef.current = null } if (recordingRef.current) { pushSnapshot(); setFightRecord(recordingRef.current); recordingRef.current = null }
timerActiveRef.current = false; setTimerRunning(false) timerActiveRef.current = false; setTimerRunning(false)
setCombatTime(0); setDisplayTime(0) setCombatTime(0); setDisplayTime(0)
playerActionRef.current = null
enemyActionsRef.current = []
if (combat) {
const deadIds = combat.enemies.filter(e => isDead(e.integrity)).map(e => e.id)
if (deadIds.length > 0) setDefeatedNpcs(prev => [...new Set([...prev, ...deadIds])])
}
} }
/** Execute a player attack; timer starts immediately. */ /** Execute a player attack; timer starts immediately. */
const executeAttack = () => { const executeAttack = () => {
if (!limbSelect || !selectedTarget) return if (!limbSelect || !selectedTarget) return
if (!combat) return const cc = combatRef.current
if (!cc) return
if (playerActionRef.current) return if (playerActionRef.current) return
if (isDead(bodyPartsRef.current)) return
const ei = attackEnemy ?? selectedEnemy const ei = attackEnemy ?? selectedEnemy
const enemy = combat.enemies[ei] const enemy = cc.enemies[ei]
if (!enemy || enemy.integrity.head <= 0) return if (!enemy || isDead(enemy.integrity)) return
const weaponId = limbSelect const weaponId = limbSelect
if (!canReach(combat.playerPos, enemy.pos, weaponId, selectedSkill)) return if (!canReach(cc.playerPos, enemy.pos, weaponId, selectedSkill)) return
const action = buildPlayerAttack({ const action = buildPlayerAttack({
weaponId, skillName: selectedSkill, targetPart: selectedTarget, weaponId, skillName: selectedSkill, targetPart: selectedTarget,
enemyIndex: ei, equippedWeapons, weaponConditions, enemyIndex: ei, equippedWeapons, weaponConditions,
bodyParts, bodyInjuries, combat, bodyParts: bodyPartsRef.current, bodyInjuries: bodyInjuriesRef.current, combat: cc,
}) })
if (action.type === 'shove_pending') { if (action.type === 'shove_pending') {
const enemy2 = combat.enemies[action.enemyIndex] const enemy2 = combat.enemies[action.enemyIndex]
if (!enemy2 || enemy2.integrity.head <= 0) return if (!enemy2 || isDead(enemy2.integrity)) return
const { pos, actualPush } = computeShove(combat.playerPos, enemy2.pos) const { pos, actualPush } = computeShove(combat.playerPos, enemy2.pos)
setCombat(c => ({ setCombat(c => ({
...c, ...c,
@@ -1119,7 +1153,7 @@ export default function App() {
<div className="combat-layout"> <div className="combat-layout">
{/* ── left: action tree */} {/* ── left: action tree */}
<div className="combat-left"> <div className="combat-left">
{playerHp > 0 && combat.enemies.some(e => e.integrity.head > 0) && ( {!isDead(bodyParts) && combat.enemies.some(e => !isDead(e.integrity)) && (
<div className={`combat-actions${busy ? ' combat-busy' : ''}`}> <div className={`combat-actions${busy ? ' combat-busy' : ''}`}>
<div className="tree-root"> <div className="tree-root">
<div className="tree-node folder" onClick={() => setExpandedWeapon(expandedWeapon === 'ROOT' ? null : 'ROOT')}> <div className="tree-node folder" onClick={() => setExpandedWeapon(expandedWeapon === 'ROOT' ? null : 'ROOT')}>
@@ -1215,7 +1249,7 @@ export default function App() {
{/* ── center: minimap + log */} {/* ── center: minimap + log */}
<div className="combat-center"> <div className="combat-center">
<div className="minimap-container" style={{ position: 'relative' }}> <div className="minimap-container" style={{ position: 'relative', borderColor: hitShake ? '#ef4444' : undefined }}>
<div className="minimap-toolbar"> <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.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(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>
@@ -1254,17 +1288,23 @@ export default function App() {
startMove(((e.clientX - r.left) / r.width) * 160 / minimapView.scale + minimapView.x, startMove(((e.clientX - r.left) / r.width) * 160 / minimapView.scale + minimapView.x,
((e.clientY - r.top) / r.height) * 160 / minimapView.scale + minimapView.y) ((e.clientY - r.top) / r.height) * 160 / minimapView.scale + minimapView.y)
}}> }}>
<g style={{ transform: `translate(${-minimapView.x * minimapView.scale}px, ${-minimapView.y * minimapView.scale}px) scale(${minimapView.scale})`, transformOrigin: '0 0' }}> <g style={{
transform: `translate(${-minimapView.x * minimapView.scale + (hitShake ? (Math.random() - 0.5) * 2 : 0)}px, ${-minimapView.y * minimapView.scale + (hitShake ? (Math.random() - 0.5) * 2 : 0)}px) scale(${minimapView.scale})`,
transformOrigin: '0 0',
transition: hitShake ? 'none' : undefined,
}}>
<rect x="0" y="0" width="160" height="160" fill="#171721" /> <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" />)} {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) => { {(combat?.enemies || []).map((enemy, i) => {
const eLerp = attackLerp && attackLerp.actor === 'enemy' && attackLerp.enemyIndex === i const dead = isDead(enemy.integrity)
const eLerp = attackLerp.enemies[i]
const isShaking = hitShake && hitShake.enemyIndex === i && hitShake.actor === 'enemy'
return ( return (
<circle key={i} <circle key={i}
cx={eLerp ? attackLerp.from.x + (attackLerp.to.x - attackLerp.from.x) * attackLerp.t : enemy.pos.x} cx={eLerp ? eLerp.from.x + (eLerp.to.x - eLerp.from.x) * eLerp.t : (isShaking ? enemy.pos.x + (Math.random() - 0.5) * 1.5 : enemy.pos.x)}
cy={eLerp ? attackLerp.from.y + (attackLerp.to.y - attackLerp.from.y) * attackLerp.t : enemy.pos.y} cy={eLerp ? eLerp.from.y + (eLerp.to.y - eLerp.from.y) * eLerp.t : (isShaking ? enemy.pos.y + (Math.random() - 0.5) * 1.5 : enemy.pos.y)}
r="5" fill="#e74c3c" stroke="#000" strokeWidth="0.5" r="5" fill={dead ? '#6b7280' : '#e74c3c'} stroke="#000" strokeWidth="0.5" opacity={dead ? 0.5 : 1}
onMouseEnter={e => { e.stopPropagation(); setHoverInfo({ type: 'enemy', name: enemy.name, attack: enemy.attack, speed: enemy.speed }) }} onMouseEnter={e => { e.stopPropagation(); setHoverInfo({ type: 'enemy', name: enemy.name, attack: enemy.attack, speed: enemy.speed, defeated: isDead(enemy.integrity) }) }}
onMouseMove={e => setMousePos({ x: e.clientX, y: e.clientY })} onMouseMove={e => setMousePos({ x: e.clientX, y: e.clientY })}
onMouseLeave={() => setHoverInfo(null)} onMouseLeave={() => setHoverInfo(null)}
onClick={e => { e.stopPropagation(); setShowInventory(true); setPanelTarget(i) }} /> onClick={e => { e.stopPropagation(); setShowInventory(true); setPanelTarget(i) }} />
@@ -1275,9 +1315,9 @@ export default function App() {
)} )}
{combat && ( {combat && (
<circle <circle
cx={attackLerp && attackLerp.actor === 'player' ? attackLerp.from.x + (attackLerp.to.x - attackLerp.from.x) * attackLerp.t : combat.playerPos.x} cx={attackLerp.player ? attackLerp.player.from.x + (attackLerp.player.to.x - attackLerp.player.from.x) * attackLerp.player.t : (hitShake && hitShake.actor === 'player' ? combat.playerPos.x + (Math.random() - 0.5) * 1.5 : combat.playerPos.x)}
cy={attackLerp && attackLerp.actor === 'player' ? attackLerp.from.y + (attackLerp.to.y - attackLerp.from.y) * attackLerp.t : combat.playerPos.y} cy={attackLerp.player ? attackLerp.player.from.y + (attackLerp.player.to.y - attackLerp.player.from.y) * attackLerp.player.t : (hitShake && hitShake.actor === 'player' ? combat.playerPos.y + (Math.random() - 0.5) * 1.5 : combat.playerPos.y)}
r="4" fill="#3498db" stroke="#000" strokeWidth="0.5" /> r="4" fill={isDead(bodyParts) ? '#6b7280' : '#3498db'} stroke="#000" strokeWidth="0.5" opacity={isDead(bodyParts) ? 0.5 : 1} />
)} )}
</g> </g>
</svg> </svg>
@@ -1420,15 +1460,15 @@ export default function App() {
</div> </div>
<div style={{ display: 'flex', gap: 8 }}> <div style={{ display: 'flex', gap: 8 }}>
<button className="limb-confirm-btn" onClick={executeAttack} <button className="limb-confirm-btn" onClick={executeAttack}
disabled={!selectedSkill || attackEnemy === null || !selectedTarget || busy || playerHp <= 0} disabled={!selectedSkill || attackEnemy === null || !selectedTarget || busy || isDead(bodyParts)}
style={{ opacity: (!selectedSkill || attackEnemy === null || !selectedTarget || busy || playerHp <= 0) ? 0.4 : 1 }}>Confirm</button> style={{ opacity: (!selectedSkill || attackEnemy === null || !selectedTarget || busy || isDead(bodyParts)) ? 0.4 : 1 }}>Confirm</button>
<button className="limb-cancel-btn" onClick={cancelAttack} style={{ width: 'auto', padding: '10px 24px' }}>Cancel</button> <button className="limb-cancel-btn" onClick={cancelAttack} style={{ width: 'auto', padding: '10px 24px' }}>Cancel</button>
</div> </div>
</div> </div>
)} )}
{/* ── combat over */} {/* ── combat over */}
{combat && (playerHp <= 0 || !combat.enemies.some(e => e.integrity.head > 0)) && ( {combat && (isDead(bodyParts) || !combat.enemies.some(e => !isDead(e.integrity))) && (
<div className="combat-over-panel"> <div className="combat-over-panel">
<div className="combat-over-title">Combat Over</div> <div className="combat-over-title">Combat Over</div>
<div className="combat-over-btns"> <div className="combat-over-btns">
@@ -1538,7 +1578,7 @@ export default function App() {
</div> </div>
{hovered && ( {hovered && (
<div className="tooltip" style={{ left: mousePos.x + 16, top: mousePos.y - 10 }}> <div className="tooltip" style={{ left: mousePos.x + 16, top: mousePos.y }}>
<div className="tooltip-name">{locMap[hovered].name}</div> <div className="tooltip-name">{locMap[hovered].name}</div>
<div className="tooltip-desc">{locMap[hovered].description}</div> <div className="tooltip-desc">{locMap[hovered].description}</div>
</div> </div>
@@ -1603,25 +1643,36 @@ export default function App() {
<div className="location-log-content"> <div className="location-log-content">
{locationLog.map((line, i) => <div key={i} className="location-log-line">{line}</div>)} {locationLog.map((line, i) => <div key={i} className="location-log-line">{line}</div>)}
</div> </div>
<div className="location-section">
<div className="location-section-header">Description</div>
<div className="location-section-body">
<div className="location-surrounding-desc">{locMap[showLocationModal]?.description}</div>
<div className="location-surrounding-subtitle">Connections</div>
{connections.filter(c => c.from === showLocationModal || c.to === showLocationModal).map(c => {
const id = c.from === showLocationModal ? c.to : c.from
return <div key={id} className="location-surrounding-connection">{locMap[id]?.name}</div>
})}
{shops.filter(s => s.locationId === showLocationModal).length > 0 && (
<>
<div className="location-surrounding-subtitle">Shops</div>
{shops.filter(s => s.locationId === showLocationModal).map(s => <div key={s.id} className="location-surrounding-shop">{s.name}</div>)}
</>
)}
</div>
</div>
</div> </div>
<div className="location-main-col"> <div className="location-main-col">
<div className="location-section"> <div className="location-section">
<div className="location-section-header">Surroundings</div> <div className="location-section-header">Surroundings</div>
<div className="location-section-body"> <div className="location-section-body">
<div className="location-surrounding-desc">{locMap[showLocationModal]?.description}</div> {(() => {
<div className="location-surrounding-connections"> const corpsesHere = defeatedNpcs
<div className="location-surrounding-subtitle">Connections</div> .map(id => characters.find(c => c.id === id))
{connections.filter(c => c.from === showLocationModal || c.to === showLocationModal).map(c => { .filter(c => c && c.location === showLocationModal)
const id = c.from === showLocationModal ? c.to : c.from return corpsesHere.length === 0
return <div key={id} className="location-surrounding-connection">{locMap[id]?.name}</div> ? <div className="location-empty">No corpses here.</div>
})} : corpsesHere.map(c => <div key={c.id} className="location-surrounding-corpse">{c.name}'s corpse</div>)
</div> })()}
{shops.filter(s => s.locationId === showLocationModal).length > 0 && (
<div className="location-surrounding-shops">
<div className="location-surrounding-subtitle">Shops</div>
{shops.filter(s => s.locationId === showLocationModal).map(s => <div key={s.id} className="location-surrounding-shop">{s.name}</div>)}
</div>
)}
</div> </div>
</div> </div>
<div className="location-section"> <div className="location-section">
@@ -1874,19 +1925,19 @@ export default function App() {
{/* ═══════════════════════════════════════════════════════ TOOLTIPS */} {/* ═══════════════════════════════════════════════════════ TOOLTIPS */}
{hovered && ( {hovered && (
<div className="tooltip" style={{ left: mousePos.x + 16, top: mousePos.y - 10 }}> <div className="tooltip" style={{ left: mousePos.x + 16, top: mousePos.y }}>
<div className="tooltip-name">{locMap[hovered].name}</div> <div className="tooltip-name">{locMap[hovered].name}</div>
<div className="tooltip-desc">{locMap[hovered].description}</div> <div className="tooltip-desc">{locMap[hovered].description}</div>
</div> </div>
)} )}
{hoverInfo?.type === 'item' && itemMap[hoverInfo.id] && ( {hoverInfo?.type === 'item' && itemMap[hoverInfo.id] && (
<div className="tooltip" style={{ left: mousePos.x + 16, top: mousePos.y - 10 }}> <div className="tooltip" style={{ left: mousePos.x + 16, top: mousePos.y }}>
<div className="tooltip-name">{itemMap[hoverInfo.id].name}</div> <div className="tooltip-name">{itemMap[hoverInfo.id].name}</div>
<div className="tooltip-desc">{itemMap[hoverInfo.id].description}</div> <div className="tooltip-desc">{itemMap[hoverInfo.id].description}</div>
</div> </div>
)} )}
{hoverInfo?.type === 'weapon' && weaponMap[hoverInfo.id] && ( {hoverInfo?.type === 'weapon' && weaponMap[hoverInfo.id] && (
<div className="tooltip" style={{ left: mousePos.x + 16, top: mousePos.y - 10 }}> <div className="tooltip" style={{ left: mousePos.x + 16, top: mousePos.y }}>
{hoverInfo.id === 'left_arm' || hoverInfo.id === 'right_arm' {hoverInfo.id === 'left_arm' || hoverInfo.id === 'right_arm'
? <><div className="tooltip-name">{weaponMap[hoverInfo.id].name}</div><div className="tooltip-desc">Unarmed strike (DMG: 10)</div></> ? <><div className="tooltip-name">{weaponMap[hoverInfo.id].name}</div><div className="tooltip-desc">Unarmed strike (DMG: 10)</div></>
: <><div className="tooltip-name">{weaponMap[hoverInfo.id].name}</div><div className="tooltip-desc">DMG: {weaponMap[hoverInfo.id].damage} SPD: {weaponMap[hoverInfo.id].speed}</div> : <><div className="tooltip-name">{weaponMap[hoverInfo.id].name}</div><div className="tooltip-desc">DMG: {weaponMap[hoverInfo.id].damage} SPD: {weaponMap[hoverInfo.id].speed}</div>
@@ -1897,7 +1948,7 @@ export default function App() {
</div> </div>
)} )}
{hoverInfo?.type === 'enemy' && ( {hoverInfo?.type === 'enemy' && (
<div className="tooltip" style={{ left: mousePos.x + 16, top: mousePos.y - 10 }}> <div className="tooltip" style={{ left: mousePos.x + 16, top: mousePos.y }}>
<div className="tooltip-name">{hoverInfo.name}</div> <div className="tooltip-name">{hoverInfo.name}</div>
<div className="tooltip-desc">{hoverInfo.defeated ? 'Defeated' : 'Hostile'}</div> <div className="tooltip-desc">{hoverInfo.defeated ? 'Defeated' : 'Hostile'}</div>
</div> </div>