i don even know anymore
This commit is contained in:
+126
-75
@@ -119,6 +119,7 @@ function partColor(hp, max) {
|
||||
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 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() {
|
||||
const parts = ['head', 'torso', 'leftArm', 'rightArm', 'leftLeg', 'rightLeg']
|
||||
@@ -204,6 +205,7 @@ function computeShove(attackerPos, targetPos) {
|
||||
* Returns { dmg, severity, injType, injLabel }
|
||||
*/
|
||||
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]
|
||||
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++) {
|
||||
const e = combat.enemies[i]
|
||||
if (e.integrity.head <= 0) continue
|
||||
if (isDead(e.integrity)) continue
|
||||
|
||||
const dist = Math.sqrt(
|
||||
(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 [gameTime, setGameTime] = useState({ day: 1, hour: 8 })
|
||||
const [weather, setWeather] = useState('clear')
|
||||
const [defeatedNpcs, setDefeatedNpcs] = useState([])
|
||||
|
||||
// ── ui state
|
||||
const [hovered, setHovered] = useState(null)
|
||||
@@ -407,7 +410,7 @@ export default function App() {
|
||||
const [moveMode, setMoveMode] = useState(null)
|
||||
|
||||
// ── animation state
|
||||
const [attackLerp, setAttackLerp] = useState(null)
|
||||
const [attackLerp, setAttackLerp] = useState({ player: null, enemies: {} })
|
||||
const [hitShake, setHitShake] = useState(null)
|
||||
const [minimapView, setMinimapView] = useState({ x: 0, y: 0, scale: 1 })
|
||||
|
||||
@@ -454,7 +457,7 @@ export default function App() {
|
||||
const playerHp = totalHp(bodyParts)
|
||||
const currentWeight = playerInventory.reduce((sum, id) => sum + (itemMap[id]?.weight ?? 0), 0)
|
||||
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 result = []
|
||||
@@ -469,7 +472,7 @@ export default function App() {
|
||||
// TIMER — only ticks while timerActive is true
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const LUNGE_DIST = 15
|
||||
const LUNGE_DIST = 18
|
||||
|
||||
function makeAttackAnim(action, cc) {
|
||||
const isPlayer = action.actor === 'player'
|
||||
@@ -512,20 +515,29 @@ export default function App() {
|
||||
* Advance an attack animation by dt seconds. Returns true when done.
|
||||
*/
|
||||
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
|
||||
const hitAt = 0.12
|
||||
const hitAt = 0.15
|
||||
const returnAt = 0.30
|
||||
|
||||
const t = anim.elapsed < hitAt
|
||||
? anim.elapsed / hitAt
|
||||
: anim.elapsed < 0.25
|
||||
? 1 - (anim.elapsed - hitAt) / (0.25 - hitAt)
|
||||
: anim.elapsed < returnAt
|
||||
? 1 - (anim.elapsed - hitAt) / (returnAt - hitAt)
|
||||
: 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) {
|
||||
anim.hitFired = true
|
||||
|
||||
const liveCc = combatRef.current
|
||||
let inRange = false
|
||||
if (liveCc) {
|
||||
if (anim.actor === 'player') {
|
||||
@@ -541,35 +553,38 @@ export default function App() {
|
||||
}
|
||||
}
|
||||
|
||||
setHitShake({ enemyIndex: anim.enemyIndex, start: performance.now() })
|
||||
|
||||
if (!inRange) {
|
||||
const msg = anim.actor === 'player'
|
||||
? 'Your attack misses!'
|
||||
: `${liveCc?.enemies[anim.enemyIndex]?.name ?? 'Enemy'}'s attack misses!`
|
||||
setCombat(prev => ({ ...prev, log: [...prev.log, msg] }))
|
||||
} else if (anim.actor === 'player') {
|
||||
// ── Player attacking enemy ──
|
||||
} else {
|
||||
setHitShake({ actor: anim.actor, enemyIndex: anim.enemyIndex, start: performance.now() })
|
||||
if (anim.actor === 'player') {
|
||||
// ── Player attacking enemy ──
|
||||
const wid = anim.weaponId
|
||||
if (wid !== 'fists' && wid !== 'left_arm' && wid !== 'right_arm') {
|
||||
const base = weaponMap[wid]
|
||||
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) {
|
||||
const enemy = liveCc.enemies[anim.enemyIndex]
|
||||
if (enemy) {
|
||||
const decayedEnemy = calcIntegrity(anim.targetParts, anim.targetInjuries, hitAt)
|
||||
const hitEnemy = { ...decayedEnemy, [anim.targetPart]: Math.max(0, decayedEnemy[anim.targetPart] - anim.dmg) }
|
||||
const newInjuries = applyInjury(anim.targetInjuries, anim.targetPart, anim.injType, anim.severity)
|
||||
const decayedEnemy = calcIntegrity(enemy.integrity, enemy.injuries, hitAt)
|
||||
const prevHp = decayedEnemy[anim.targetPart]
|
||||
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 = [
|
||||
`You ${anim.skillName?.toLowerCase() ?? 'attack'} ${enemy.name}'s ${bodyPartLabels[anim.targetPart]} — ${anim.injLabel} (severity ${anim.severity}, -${anim.dmg} integrity).`,
|
||||
]
|
||||
if (hitEnemy.head <= 0) {
|
||||
log.push(`You crush ${enemy.name}'s head!`)
|
||||
const log = anim.dmg > 0
|
||||
? [`You ${anim.skillName?.toLowerCase() ?? 'attack'} ${enemy.name}'s ${bodyPartLabels[anim.targetPart]} — ${anim.injLabel} (severity ${anim.severity}, -${anim.dmg} integrity).`]
|
||||
: []
|
||||
if (isDead(hitEnemy)) {
|
||||
if (anim.targetPart === 'head') log.push(`You crush ${enemy.name}'s head!`)
|
||||
log.push(`${enemy.name} is defeated!`)
|
||||
}
|
||||
|
||||
@@ -582,13 +597,19 @@ export default function App() {
|
||||
}
|
||||
|
||||
setBodyParts(decayedPlayer)
|
||||
setBodyInjuries(anim.attackerInjuries)
|
||||
setBodyInjuries(bodyInjuriesRef.current)
|
||||
} else {
|
||||
// ── Enemy attacking player ──
|
||||
const decayedEnemy = calcIntegrity(anim.attackerParts, anim.attackerInjuries, hitAt)
|
||||
|
||||
const hitPlayer = { ...anim.targetParts, [anim.targetPart]: Math.max(0, (anim.targetParts[anim.targetPart] ?? 100) - anim.dmg) }
|
||||
const newInjuries = applyInjury(anim.targetInjuries, anim.targetPart, anim.injType, anim.severity)
|
||||
const currentEnemy = liveCc?.enemies[anim.enemyIndex]
|
||||
const decayedEnemy = currentEnemy
|
||||
? calcIntegrity(currentEnemy.integrity, currentEnemy.injuries, hitAt)
|
||||
: 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) {
|
||||
const enemy = liveCc.enemies[anim.enemyIndex]
|
||||
@@ -597,17 +618,22 @@ export default function App() {
|
||||
setCombat(prev => ({
|
||||
...prev,
|
||||
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)
|
||||
setBodyInjuries(newInjuries)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (anim.elapsed >= 0.35) {
|
||||
setAttackLerp(null)
|
||||
if (anim.elapsed >= 0.45) {
|
||||
setAttackLerp(prev => anim.actor === 'player'
|
||||
? { ...prev, player: null }
|
||||
: { ...prev, enemies: { ...prev.enemies, [anim.enemyIndex]: undefined } })
|
||||
setHitShake(null)
|
||||
return true
|
||||
}
|
||||
@@ -715,7 +741,7 @@ export default function App() {
|
||||
if (anim.type === 'enemyMove') {
|
||||
const e = fresh?.enemies[i] ?? cc.enemies[i]
|
||||
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(
|
||||
((fresh ?? cc).playerPos.x - finalPos.x) ** 2 + ((fresh ?? cc).playerPos.y - finalPos.y) ** 2
|
||||
)
|
||||
@@ -800,7 +826,7 @@ export default function App() {
|
||||
|
||||
useEffect(() => {
|
||||
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])
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
@@ -814,7 +840,7 @@ export default function App() {
|
||||
combat: JSON.parse(JSON.stringify(combat)),
|
||||
bodyParts: JSON.parse(JSON.stringify(bodyParts)),
|
||||
bodyInjuries: JSON.parse(JSON.stringify(bodyInjuries)),
|
||||
attackLerp: attackLerp ? { ...attackLerp } : null,
|
||||
attackLerp: JSON.parse(JSON.stringify(attackLerp)),
|
||||
hitShake: hitShake ? { ...hitShake } : null,
|
||||
})
|
||||
}
|
||||
@@ -894,7 +920,7 @@ export default function App() {
|
||||
const rec = fightRecord
|
||||
const f0 = rec.frames[0]
|
||||
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)
|
||||
replayIdxRef.current = 0
|
||||
|
||||
@@ -906,7 +932,7 @@ export default function App() {
|
||||
replayIdxRef.current = idx
|
||||
const f = rec.frames[idx]
|
||||
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)
|
||||
}
|
||||
if (idx >= rec.frames.length - 1) return
|
||||
@@ -950,7 +976,7 @@ export default function App() {
|
||||
enemyActionsRef.current = initCombat.enemies.map(() => null)
|
||||
positionsRef.current = {}
|
||||
setSelectedEnemy(0); setReplayActive(false)
|
||||
setExpandedWeapon('ROOT')
|
||||
setExpandedWeapon('ROOT'); setAttackLerp({ player: null, enemies: {} }); setHitShake(null); setMoveMode(null)
|
||||
recordingRef.current = {
|
||||
initialCombat: JSON.parse(JSON.stringify(initCombat)),
|
||||
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 }
|
||||
timerActiveRef.current = false; setTimerRunning(false)
|
||||
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. */
|
||||
const executeAttack = () => {
|
||||
if (!limbSelect || !selectedTarget) return
|
||||
if (!combat) return
|
||||
const cc = combatRef.current
|
||||
if (!cc) return
|
||||
if (playerActionRef.current) return
|
||||
if (isDead(bodyPartsRef.current)) return
|
||||
const ei = attackEnemy ?? selectedEnemy
|
||||
const enemy = combat.enemies[ei]
|
||||
if (!enemy || enemy.integrity.head <= 0) return
|
||||
const enemy = cc.enemies[ei]
|
||||
if (!enemy || isDead(enemy.integrity)) return
|
||||
|
||||
const weaponId = limbSelect
|
||||
if (!canReach(combat.playerPos, enemy.pos, weaponId, selectedSkill)) return
|
||||
if (!canReach(cc.playerPos, enemy.pos, weaponId, selectedSkill)) return
|
||||
|
||||
const action = buildPlayerAttack({
|
||||
weaponId, skillName: selectedSkill, targetPart: selectedTarget,
|
||||
enemyIndex: ei, equippedWeapons, weaponConditions,
|
||||
bodyParts, bodyInjuries, combat,
|
||||
bodyParts: bodyPartsRef.current, bodyInjuries: bodyInjuriesRef.current, combat: cc,
|
||||
})
|
||||
|
||||
if (action.type === 'shove_pending') {
|
||||
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)
|
||||
setCombat(c => ({
|
||||
...c,
|
||||
@@ -1119,7 +1153,7 @@ export default function App() {
|
||||
<div className="combat-layout">
|
||||
{/* ── left: action tree */}
|
||||
<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="tree-root">
|
||||
<div className="tree-node folder" onClick={() => setExpandedWeapon(expandedWeapon === 'ROOT' ? null : 'ROOT')}>
|
||||
@@ -1215,7 +1249,7 @@ export default function App() {
|
||||
|
||||
{/* ── center: minimap + log */}
|
||||
<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">
|
||||
<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>
|
||||
@@ -1254,17 +1288,23 @@ export default function App() {
|
||||
startMove(((e.clientX - r.left) / r.width) * 160 / minimapView.scale + minimapView.x,
|
||||
((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" />
|
||||
{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) => {
|
||||
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 (
|
||||
<circle key={i}
|
||||
cx={eLerp ? attackLerp.from.x + (attackLerp.to.x - attackLerp.from.x) * attackLerp.t : enemy.pos.x}
|
||||
cy={eLerp ? attackLerp.from.y + (attackLerp.to.y - attackLerp.from.y) * attackLerp.t : 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 }) }}
|
||||
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 ? 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={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, defeated: isDead(enemy.integrity) }) }}
|
||||
onMouseMove={e => setMousePos({ x: e.clientX, y: e.clientY })}
|
||||
onMouseLeave={() => setHoverInfo(null)}
|
||||
onClick={e => { e.stopPropagation(); setShowInventory(true); setPanelTarget(i) }} />
|
||||
@@ -1275,9 +1315,9 @@ export default function App() {
|
||||
)}
|
||||
{combat && (
|
||||
<circle
|
||||
cx={attackLerp && attackLerp.actor === 'player' ? attackLerp.from.x + (attackLerp.to.x - attackLerp.from.x) * attackLerp.t : combat.playerPos.x}
|
||||
cy={attackLerp && attackLerp.actor === 'player' ? attackLerp.from.y + (attackLerp.to.y - attackLerp.from.y) * attackLerp.t : combat.playerPos.y}
|
||||
r="4" fill="#3498db" stroke="#000" strokeWidth="0.5" />
|
||||
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.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={isDead(bodyParts) ? '#6b7280' : '#3498db'} stroke="#000" strokeWidth="0.5" opacity={isDead(bodyParts) ? 0.5 : 1} />
|
||||
)}
|
||||
</g>
|
||||
</svg>
|
||||
@@ -1420,15 +1460,15 @@ export default function App() {
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<button className="limb-confirm-btn" onClick={executeAttack}
|
||||
disabled={!selectedSkill || attackEnemy === null || !selectedTarget || busy || playerHp <= 0}
|
||||
style={{ opacity: (!selectedSkill || attackEnemy === null || !selectedTarget || busy || playerHp <= 0) ? 0.4 : 1 }}>Confirm</button>
|
||||
disabled={!selectedSkill || attackEnemy === null || !selectedTarget || busy || isDead(bodyParts)}
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── 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-title">Combat Over</div>
|
||||
<div className="combat-over-btns">
|
||||
@@ -1538,7 +1578,7 @@ export default function App() {
|
||||
</div>
|
||||
|
||||
{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-desc">{locMap[hovered].description}</div>
|
||||
</div>
|
||||
@@ -1603,25 +1643,36 @@ export default function App() {
|
||||
<div className="location-log-content">
|
||||
{locationLog.map((line, i) => <div key={i} className="location-log-line">{line}</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 className="location-main-col">
|
||||
<div className="location-section">
|
||||
<div className="location-section-header">Surroundings</div>
|
||||
<div className="location-section-body">
|
||||
<div className="location-surrounding-desc">{locMap[showLocationModal]?.description}</div>
|
||||
<div className="location-surrounding-connections">
|
||||
<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>
|
||||
})}
|
||||
</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>
|
||||
)}
|
||||
{(() => {
|
||||
const corpsesHere = defeatedNpcs
|
||||
.map(id => characters.find(c => c.id === id))
|
||||
.filter(c => c && c.location === showLocationModal)
|
||||
return corpsesHere.length === 0
|
||||
? <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>
|
||||
</div>
|
||||
<div className="location-section">
|
||||
@@ -1874,19 +1925,19 @@ export default function App() {
|
||||
|
||||
{/* ═══════════════════════════════════════════════════════ TOOLTIPS */}
|
||||
{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-desc">{locMap[hovered].description}</div>
|
||||
</div>
|
||||
)}
|
||||
{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-desc">{itemMap[hoverInfo.id].description}</div>
|
||||
</div>
|
||||
)}
|
||||
{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'
|
||||
? <><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>
|
||||
@@ -1897,7 +1948,7 @@ export default function App() {
|
||||
</div>
|
||||
)}
|
||||
{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-desc">{hoverInfo.defeated ? 'Defeated' : 'Hostile'}</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user