i don even know anymore
This commit is contained in:
+23
-23
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
+118
-67
@@ -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,14 +553,14 @@ 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 {
|
||||||
|
setHitShake({ actor: anim.actor, enemyIndex: anim.enemyIndex, start: performance.now() })
|
||||||
|
if (anim.actor === 'player') {
|
||||||
// ── Player attacking enemy ──
|
// ── 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') {
|
||||||
@@ -556,20 +568,23 @@ export default function App() {
|
|||||||
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,7 +618,9 @@ 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,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -605,9 +628,12 @@ export default function App() {
|
|||||||
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,27 +1643,38 @@ 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>
|
|
||||||
<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">Description</div>
|
||||||
<div className="location-section-body">
|
<div className="location-section-body">
|
||||||
<div className="location-surrounding-desc">{locMap[showLocationModal]?.description}</div>
|
<div className="location-surrounding-desc">{locMap[showLocationModal]?.description}</div>
|
||||||
<div className="location-surrounding-connections">
|
|
||||||
<div className="location-surrounding-subtitle">Connections</div>
|
<div className="location-surrounding-subtitle">Connections</div>
|
||||||
{connections.filter(c => c.from === showLocationModal || c.to === showLocationModal).map(c => {
|
{connections.filter(c => c.from === showLocationModal || c.to === showLocationModal).map(c => {
|
||||||
const id = c.from === showLocationModal ? c.to : c.from
|
const id = c.from === showLocationModal ? c.to : c.from
|
||||||
return <div key={id} className="location-surrounding-connection">{locMap[id]?.name}</div>
|
return <div key={id} className="location-surrounding-connection">{locMap[id]?.name}</div>
|
||||||
})}
|
})}
|
||||||
</div>
|
|
||||||
{shops.filter(s => s.locationId === showLocationModal).length > 0 && (
|
{shops.filter(s => s.locationId === showLocationModal).length > 0 && (
|
||||||
<div className="location-surrounding-shops">
|
<>
|
||||||
<div className="location-surrounding-subtitle">Shops</div>
|
<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>)}
|
{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>
|
||||||
|
<div className="location-main-col">
|
||||||
|
<div className="location-section">
|
||||||
|
<div className="location-section-header">Surroundings</div>
|
||||||
|
<div className="location-section-body">
|
||||||
|
{(() => {
|
||||||
|
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">
|
<div className="location-section">
|
||||||
<div className="location-section-header">Characters</div>
|
<div className="location-section-header">Characters</div>
|
||||||
<div className="location-section-body">
|
<div className="location-section-body">
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user