This commit is contained in:
2026-06-18 03:43:43 -04:00
parent 819306aba3
commit 4270cdca75
2 changed files with 153 additions and 30 deletions
+60
View File
@@ -2478,6 +2478,28 @@ body {
min-width: 20px; min-width: 20px;
text-align: center; text-align: center;
} }
.trade-qty-input {
width: 40px;
height: 22px;
border: 1px solid #4a4a6a;
border-radius: 3px;
background: #16162a;
color: #e0e0e0;
font-size: 12px;
text-align: center;
outline: none;
}
.trade-qty-input:focus {
border-color: #6366f1;
}
.trade-qty-input::-webkit-inner-spin-button,
.trade-qty-input::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
.trade-qty-input[type='number'] {
-moz-appearance: textfield;
}
.trade-bar { .trade-bar {
margin-top: 12px; margin-top: 12px;
padding: 10px 12px; padding: 10px 12px;
@@ -2504,3 +2526,41 @@ body {
.trade-propose-btn:hover { .trade-propose-btn:hover {
background: #4b5563; background: #4b5563;
} }
.notifications-container {
position: fixed;
bottom: 12px;
right: 12px;
display: flex;
flex-direction: column;
gap: 6px;
z-index: 9999;
pointer-events: none;
}
.notification {
width: 220px;
min-height: 56px;
background: #1e1e2e;
border: 1px solid #4a4a6a;
border-radius: 4px;
padding: 8px 14px;
font-size: 13px;
color: #e0e0e0;
animation: notif-in 0.2s ease-out;
display: flex;
flex-direction: column;
gap: 4px;
}
.notification-subject {
font-weight: bold;
font-size: 13px;
color: #fbbf24;
}
.notification-desc {
font-size: 12px;
color: #9ca3af;
}
@keyframes notif-in {
from { opacity: 0; transform: translateX(20px); }
to { opacity: 1; transform: translateX(0); }
}
+93 -30
View File
@@ -32,11 +32,12 @@ const characters = [
{ id: 'd3', name: 'Sand Viper', location: 'D', money: 8, inventory: ['pistol_ammo'], size: 1 }, { id: 'd3', name: 'Sand Viper', location: 'D', money: 8, inventory: ['pistol_ammo'], size: 1 },
{ id: 'd4', name: 'Rust-Eye', location: 'D', money: 45, inventory: ['pistol_ammo'], size: 1 }, { id: 'd4', name: 'Rust-Eye', location: 'D', money: 45, inventory: ['pistol_ammo'], size: 1 },
{ id: 'd5', name: 'Cinder', location: 'D', money: 22, inventory: ['pistol_ammo'], size: 1 }, { id: 'd5', name: 'Cinder', location: 'D', money: 22, inventory: ['pistol_ammo'], size: 1 },
{ id: 'e1', name: 'Zephyr', location: 'E', money: 9999, inventory: ['pistol', 'pistol', 'steel_sword', 'iron_shield', 'dagger', 'warhammer', 'rusty_sword', 'pistol_ammo', 'bouncing_ammo'], itemCounts: { pistol_ammo: 20, bouncing_ammo: 5 }, size: 1 },
] ]
const weapons = [ const weapons = [
{ id: 'fists', name: 'Fists', damage: 35, condition: 100, maxCondition: 100, passives: ['Brawler'], weight: 0, speed: 14, skills: [{ name: 'Jab', mult: 1.0, damageType: 'blunt', range: 12 }, { name: 'Hook', mult: 1.3, damageType: 'blunt', range: 12 }, { name: 'Kick', mult: 1.5, damageType: 'blunt', range: 14 }, { name: 'Shove', mult: 0, damageType: 'shove', range: 10 }] }, { id: 'fists', name: 'Fists', damage: 35, condition: 100, maxCondition: 100, passives: ['Brawler'], weight: 0, speed: 14, skills: [{ name: 'Jab', mult: 1.0, damageType: 'blunt', range: 12 }, { name: 'Hook', mult: 1.3, damageType: 'blunt', range: 12 }, { name: 'Kick', mult: 1.5, damageType: 'blunt', range: 14 }, { name: 'Shove', mult: 0, damageType: 'shove', range: 10 }] },
{ id: 'pistol', name: 'Pistol', damage: 70, condition: 100, maxCondition: 100, passives: ['Ranged'], weight: 3, speed: 12, skills: [{ name: 'Shoot', mult: 1.0, damageType: 'pierce', range: 200 }, { name: 'Load', mult: 0, damageType: 'reload', range: 0 }, { name: 'Unload', mult: 0, damageType: 'unload', range: 0 }] }, { id: 'pistol', name: 'Pistol', damage: 70, condition: 100, maxCondition: 100, passives: ['Ranged'], weight: 3, speed: 12, compatibleBulletSizes: ['9mm'], skills: [{ name: 'Shoot', mult: 1.0, damageType: 'pierce', range: 200 }, { name: 'Load', mult: 0, damageType: 'reload', range: 0 }, { name: 'Unload', mult: 0, damageType: 'unload', range: 0 }] },
{ id: 'rusty_sword', name: 'Rusty Sword', damage: 60, condition: 65, maxCondition: 100, passives: ['Bleed'], weight: 4, speed: 10, skills: [{ name: 'Slash', mult: 1.0, damageType: 'slash', range: 15 }, { name: 'Stab', mult: 1.2, damageType: 'pierce', range: 18 }] }, { id: 'rusty_sword', name: 'Rusty Sword', damage: 60, condition: 65, maxCondition: 100, passives: ['Bleed'], weight: 4, speed: 10, skills: [{ name: 'Slash', mult: 1.0, damageType: 'slash', range: 15 }, { name: 'Stab', mult: 1.2, damageType: 'pierce', range: 18 }] },
{ id: 'steel_sword', name: 'Steel Sword', damage: 85, condition: 100, maxCondition: 100, passives: ['Bleed', 'Pierce'], weight: 5, speed: 9, skills: [{ name: 'Slash', mult: 1.0, damageType: 'slash', range: 15 }, { name: 'Stab', mult: 1.2, damageType: 'pierce', range: 18 }, { name: 'Overhead', mult: 1.4, damageType: 'slash', range: 14 }] }, { id: 'steel_sword', name: 'Steel Sword', damage: 85, condition: 100, maxCondition: 100, passives: ['Bleed', 'Pierce'], weight: 5, speed: 9, skills: [{ name: 'Slash', mult: 1.0, damageType: 'slash', range: 15 }, { name: 'Stab', mult: 1.2, damageType: 'pierce', range: 18 }, { name: 'Overhead', mult: 1.4, damageType: 'slash', range: 14 }] },
{ id: 'iron_shield', name: 'Iron Shield', damage: 40, condition: 100, maxCondition: 100, passives: ['Block', 'Sturdy'], weight: 6, speed: 7, skills: [{ name: 'Bash', mult: 1.0, damageType: 'blunt', range: 12 }, { name: 'Shield Slam', mult: 1.3, damageType: 'blunt', range: 13 }] }, { id: 'iron_shield', name: 'Iron Shield', damage: 40, condition: 100, maxCondition: 100, passives: ['Block', 'Sturdy'], weight: 6, speed: 7, skills: [{ name: 'Bash', mult: 1.0, damageType: 'blunt', range: 12 }, { name: 'Shield Slam', mult: 1.3, damageType: 'blunt', range: 13 }] },
@@ -45,8 +46,8 @@ const weapons = [
] ]
const items = [ const items = [
{ id: 'pistol_ammo', name: 'Pistol Ammo', description: 'Standard pistol ammunition.', weight: 0.5 }, { id: 'pistol_ammo', name: 'Pistol Ammo', description: 'Standard pistol ammunition.', weight: 0.5, bulletSize: '9mm' },
{ id: 'bouncing_ammo', name: 'Bouncing Ammo', description: 'Experimental ammo that bounces off walls.', weight: 0.5 }, { id: 'bouncing_ammo', name: 'Bouncing Ammo', description: 'Experimental ammo that bounces off walls.', weight: 0.5, bulletSize: '9mm' },
] ]
const itemMap = {} const itemMap = {}
@@ -97,6 +98,7 @@ function getItemValue(id) {
const INJURY_DECAY = { cut: 0.3, laceration: 0.6, stab: 0.8 } const INJURY_DECAY = { cut: 0.3, laceration: 0.6, stab: 0.8 }
const TURN_SPEED = 6 const TURN_SPEED = 6
const CURRENCY = 'ahn'
const PISTOL_MAG = 7 const PISTOL_MAG = 7
const bodyPartLabels = { const bodyPartLabels = {
@@ -593,6 +595,22 @@ export default function App() {
const [fightRecord, setFightRecord] = useState(null) const [fightRecord, setFightRecord] = useState(null)
const [replayActive, setReplayActive] = useState(false) const [replayActive, setReplayActive] = useState(false)
// ── notifications
const [notifications, setNotifications] = useState([])
useEffect(() => {
const id = setInterval(() => {
setNotifications(prev => [...prev, { id: Date.now(), subject: 'Test', description: 'This is a test notification.' }])
}, 5000)
return () => clearInterval(id)
}, [])
useEffect(() => {
if (notifications.length === 0) return
const id = setTimeout(() => setNotifications(prev => prev.slice(1)), 4000)
return () => clearTimeout(id)
}, [notifications])
// ── menu // ── menu
const [showMenu, setShowMenu] = useState(true) const [showMenu, setShowMenu] = useState(true)
const [showCharCreate, setShowCharCreate] = useState(false) const [showCharCreate, setShowCharCreate] = useState(false)
@@ -733,6 +751,7 @@ export default function App() {
const subSteps = Math.max(1, Math.ceil(moveDist)) const subSteps = Math.max(1, Math.ceil(moveDist))
let hit = false let hit = false
let hitCx = null, hitCy = null
for (let s = 1; s <= subSteps; s++) { for (let s = 1; s <= subSteps; s++) {
const t = s / subSteps const t = s / subSteps
@@ -759,7 +778,7 @@ export default function App() {
break break
} }
applyProjectileHit(p, null) applyProjectileHit(p, null)
hit = true; break hit = true; hitCx = cx; hitCy = cy; break
} }
for (const cid of Object.keys(cc.characters)) { for (const cid of Object.keys(cc.characters)) {
@@ -770,12 +789,16 @@ export default function App() {
const r = cfg.hitRadius + 4 * (e.size ?? 1) const r = cfg.hitRadius + 4 * (e.size ?? 1)
if (Math.sqrt((cx - ePos.x) ** 2 + (cy - ePos.y) ** 2) < r) { if (Math.sqrt((cx - ePos.x) ** 2 + (cy - ePos.y) ** 2) < r) {
applyProjectileHit(p, cid) applyProjectileHit(p, cid)
hit = true; break hit = true; hitCx = cx; hitCy = cy; break
} }
} }
if (hit) break if (hit) break
} }
if (hit) continue if (hit) {
if (hitCx != null) { p.pos.x = hitCx; p.pos.y = hitCy }
out.push(p)
continue
}
if (moveDist < stepDist) continue if (moveDist < stepDist) continue
@@ -1764,7 +1787,7 @@ class MoveAction {
equipped: { left: 'fists', right: 'fists' }, equipped: { left: 'fists', right: 'fists' },
weaponConditions: {}, weaponConditions: {},
weaponMagazines: {}, weaponMagazines: {},
itemCounts: {}, itemCounts: { ...(lc.itemCounts ?? {}) },
inventory: [...(lc.inventory ?? [])], inventory: [...(lc.inventory ?? [])],
fillColor: lc.fillColor, fillColor: lc.fillColor,
}) })
@@ -1971,7 +1994,7 @@ class MoveAction {
} }
const itemCounts = actorChar?.itemCounts ?? (actorId === 'player' ? playerRef.current.itemCounts : {}) const itemCounts = actorChar?.itemCounts ?? (actorId === 'player' ? playerRef.current.itemCounts : {})
const available = Object.entries(itemCounts) const available = Object.entries(itemCounts)
.filter(([id, qty]) => qty > 0 && (id === 'pistol_ammo' || id === 'bouncing_ammo')) .filter(([id, qty]) => qty > 0 && (weaponMap.pistol?.compatibleBulletSizes ?? []).includes(itemMap[id]?.bulletSize))
if (available.length === 0) { if (available.length === 0) {
setCombat(c => ({ ...c, log: [...c.log, 'No ammo in inventory!'] })) setCombat(c => ({ ...c, log: [...c.log, 'No ammo in inventory!'] }))
return return
@@ -2198,9 +2221,9 @@ class MoveAction {
if (!combat || charId === 'player') return true if (!combat || charId === 'player') return true
const src = combat.characters?.player const src = combat.characters?.player
const tgt = combat.characters?.[charId] const tgt = combat.characters?.[charId]
if (!src || !tgt) return true if (!src?.pos || !tgt?.pos) return true
const dx = src.x - tgt.x const dx = src.pos.x - tgt.pos.x
const dy = src.y - tgt.y const dy = src.pos.y - tgt.pos.y
return Math.sqrt(dx * dx + dy * dy) <= 35 return Math.sqrt(dx * dx + dy * dy) <= 35
} }
@@ -2811,11 +2834,11 @@ class MoveAction {
<div className="inv-parts-title" style={{ marginBottom: 8 }}>Select Ammo Type</div> <div className="inv-parts-title" style={{ marginBottom: 8 }}>Select Ammo Type</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
{Object.entries(actorChar?.itemCounts ?? {}) {Object.entries(actorChar?.itemCounts ?? {})
.filter(([id, qty]) => qty > 0 && (id === 'pistol_ammo' || id === 'bouncing_ammo')) .filter(([id, qty]) => qty > 0 && (weaponMap.pistol?.compatibleBulletSizes ?? []).includes(itemMap[id]?.bulletSize))
.length === 0 .length === 0
? <div style={{ color: '#6b7280', padding: 12 }}>No ammo available</div> ? <div style={{ color: '#6b7280', padding: 12 }}>No ammo available</div>
: Object.entries(actorChar?.itemCounts ?? {}) : Object.entries(actorChar?.itemCounts ?? {})
.filter(([id, qty]) => qty > 0 && (id === 'pistol_ammo' || id === 'bouncing_ammo')) .filter(([id, qty]) => qty > 0 && (weaponMap.pistol?.compatibleBulletSizes ?? []).includes(itemMap[id]?.bulletSize))
.map(([id, qty]) => ( .map(([id, qty]) => (
<div key={id} className="tree-node leaf" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '10px 12px', cursor: 'pointer', borderRadius: 3, marginBottom: 4 }} <div key={id} className="tree-node leaf" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '10px 12px', cursor: 'pointer', borderRadius: 3, marginBottom: 4 }}
onMouseEnter={e => e.currentTarget.style.background = '#1e1e30'} onMouseEnter={e => e.currentTarget.style.background = '#1e1e30'}
@@ -3016,7 +3039,7 @@ class MoveAction {
<div key={c.id} className="character-entry" onContextMenu={e => { e.preventDefault(); e.stopPropagation(); setHoverInfo(null); setHoveredChar(null); setContextMenu({ x: e.clientX, y: e.clientY, charId: c.id }) }}> <div key={c.id} className="character-entry" onContextMenu={e => { e.preventDefault(); e.stopPropagation(); setHoverInfo(null); setHoveredChar(null); setContextMenu({ x: e.clientX, y: e.clientY, charId: c.id }) }}>
<div className="character-name" style={{ cursor: 'pointer' }} onClick={() => { setShowInventory(true); setPanelTarget(c.id) }}>{c.name}</div> <div className="character-name" style={{ cursor: 'pointer' }} onClick={() => { setShowInventory(true); setPanelTarget(c.id) }}>{c.name}</div>
<div className="character-details"> <div className="character-details">
{c.money !== undefined && <span className="char-money">{c.money}g</span>} {c.money !== undefined && <span className="char-money">{c.money} {CURRENCY}</span>}
{c.inventory?.length > 0 && <span className="char-inv">{c.inventory.map(id => itemMap[id]?.name ?? id).join(', ')}</span>} {c.inventory?.length > 0 && <span className="char-inv">{c.inventory.map(id => itemMap[id]?.name ?? id).join(', ')}</span>}
</div> </div>
</div> </div>
@@ -3145,7 +3168,7 @@ onContextMenu={e => { e.preventDefault(); setHoverInfo(null); setHoveredChar(nul
}) })
} }
{isPlayer && <div className="inv-weight">Weight: {charWeight}/{MAX_WEIGHT}</div>} {isPlayer && <div className="inv-weight">Weight: {charWeight}/{MAX_WEIGHT}</div>}
<div className="inv-money">{character.money || 0}g</div> <div className="inv-money">{character.money || 0} {CURRENCY}</div>
</div> </div>
)} )}
{character && invTab === 'stats' && ( {character && invTab === 'stats' && (
@@ -3160,7 +3183,7 @@ onContextMenu={e => { e.preventDefault(); setHoverInfo(null); setHoveredChar(nul
<div className="stat-row"><span className="stat-label">Integrity</span><span className="stat-value">{charHp}/{MAX_HP}</span></div> <div className="stat-row"><span className="stat-label">Integrity</span><span className="stat-value">{charHp}/{MAX_HP}</span></div>
<div className="stat-row"><span className="stat-label">Items</span><span className="stat-value">{character.inventory.length}</span></div> <div className="stat-row"><span className="stat-label">Items</span><span className="stat-value">{character.inventory.length}</span></div>
{isPlayer && <div className="stat-row"><span className="stat-label">Weight</span><span className="stat-value">{charWeight}/{MAX_WEIGHT}</span></div>} {isPlayer && <div className="stat-row"><span className="stat-label">Weight</span><span className="stat-value">{charWeight}/{MAX_WEIGHT}</span></div>}
<div className="stat-row"><span className="stat-label">Money</span><span className="stat-value">{character.money || 0}g</span></div> <div className="stat-row"><span className="stat-label">Money</span><span className="stat-value">{character.money || 0} {CURRENCY}</span></div>
</div> </div>
)} )}
</div> </div>
@@ -3187,7 +3210,7 @@ onContextMenu={e => { e.preventDefault(); setHoverInfo(null); setHoveredChar(nul
<div className="settings-section"> <div className="settings-section">
<div className="settings-section-title">Player</div> <div className="settings-section-title">Player</div>
<div className="settings-row"><span className="settings-label">Location</span><span className="settings-value">{locMap[player.location]?.name ?? player.location}</span></div> <div className="settings-row"><span className="settings-label">Location</span><span className="settings-value">{locMap[player.location]?.name ?? player.location}</span></div>
<div className="settings-row"><span className="settings-label">Money</span><span className="settings-value">{player.money}g</span></div> <div className="settings-row"><span className="settings-label">Money</span><span className="settings-value">{player.money} {CURRENCY}</span></div>
</div> </div>
</div> </div>
</div> </div>
@@ -3249,7 +3272,12 @@ onContextMenu={e => { e.preventDefault(); setHoverInfo(null); setHoveredChar(nul
const items = [] const items = []
for (const id of (ch.inventory ?? [])) { for (const id of (ch.inventory ?? [])) {
if (seen.has(id)) continue; seen.add(id) if (seen.has(id)) continue; seen.add(id)
items.push({ id, name: weaponMap[id]?.name ?? itemMap[id]?.name ?? id, qty: 1, value: getItemValue(id), cond: ch.weaponConditions?.[id] ?? weaponMap[id]?.maxCondition ?? null, maxCond: weaponMap[id]?.maxCondition ?? null }) const stacked = ch.itemCounts?.[id]
if (stacked !== undefined) {
items.push({ id, name: itemMap[id]?.name ?? id, qty: stacked, value: getItemValue(id) })
} else {
items.push({ id, name: weaponMap[id]?.name ?? itemMap[id]?.name ?? id, qty: 1, value: getItemValue(id), cond: ch.weaponConditions?.[id] ?? weaponMap[id]?.maxCondition ?? null, maxCond: weaponMap[id]?.maxCondition ?? null })
}
} }
for (const [id, qty] of Object.entries(ch.itemCounts ?? {})) { for (const [id, qty] of Object.entries(ch.itemCounts ?? {})) {
if (qty > 0) { if (qty > 0) {
@@ -3276,7 +3304,6 @@ onContextMenu={e => { e.preventDefault(); setHoverInfo(null); setHoveredChar(nul
} }
for (const [id, qty] of giveEntries) { for (const [id, qty] of giveEntries) {
if (weaponMap[id]) { if (weaponMap[id]) {
// weapon: remove from player inventory, give to target
newPlayer.inventory = newPlayer.inventory.filter(i => i !== id) newPlayer.inventory = newPlayer.inventory.filter(i => i !== id)
if (tgtWorld) { if (tgtWorld) {
tgtWorld.inventory = tgtWorld.inventory ?? [] tgtWorld.inventory = tgtWorld.inventory ?? []
@@ -3286,14 +3313,13 @@ onContextMenu={e => { e.preventDefault(); setHoverInfo(null); setHoveredChar(nul
delete newPlayer.weaponConditions[id] delete newPlayer.weaponConditions[id]
} }
} }
} else if (itemMap[id]) { } else {
// ammo/item: reduce player count, increase target count
newPlayer.itemCounts = { ...(newPlayer.itemCounts ?? {}) } newPlayer.itemCounts = { ...(newPlayer.itemCounts ?? {}) }
newPlayer.itemCounts[id] = (newPlayer.itemCounts[id] ?? 0) - qty newPlayer.itemCounts[id] = (newPlayer.itemCounts[id] ?? 0) - qty
if (newPlayer.itemCounts[id] <= 0) delete newPlayer.itemCounts[id] if (newPlayer.itemCounts[id] <= 0) delete newPlayer.itemCounts[id]
if (tgtWorld) { if (tgtWorld) {
const tgt = tgtWorld const tgt = tgtWorld
tgt.inventory = [...(tgt.inventory ?? []), id] if (!tgt.inventory.includes(id)) tgt.inventory = [...(tgt.inventory ?? []), id]
tgt.itemCounts = { ...(tgt.itemCounts ?? {}) } tgt.itemCounts = { ...(tgt.itemCounts ?? {}) }
tgt.itemCounts[id] = (tgt.itemCounts[id] ?? 0) + qty tgt.itemCounts[id] = (tgt.itemCounts[id] ?? 0) + qty
} }
@@ -3301,7 +3327,6 @@ onContextMenu={e => { e.preventDefault(); setHoverInfo(null); setHoveredChar(nul
} }
for (const [id, qty] of takeEntries) { for (const [id, qty] of takeEntries) {
if (weaponMap[id]) { if (weaponMap[id]) {
// weapon: remove from target inventory, give to player
if (tgtWorld) { if (tgtWorld) {
tgtWorld.inventory = tgtWorld.inventory.filter(i => i !== id) tgtWorld.inventory = tgtWorld.inventory.filter(i => i !== id)
if (tgtWorld.weaponConditions?.[id] != null) { if (tgtWorld.weaponConditions?.[id] != null) {
@@ -3311,20 +3336,32 @@ onContextMenu={e => { e.preventDefault(); setHoverInfo(null); setHoveredChar(nul
} }
} }
newPlayer.inventory = [...(newPlayer.inventory ?? []), id] newPlayer.inventory = [...(newPlayer.inventory ?? []), id]
} else if (itemMap[id]) { } else {
// ammo/item: reduce target count (from itemCounts and/or inventory)
if (tgtWorld) { if (tgtWorld) {
const tgt = tgtWorld const tgt = tgtWorld
tgt.inventory = (tgt.inventory ?? []).filter(i => i !== id)
tgt.itemCounts = { ...(tgt.itemCounts ?? {}) } tgt.itemCounts = { ...(tgt.itemCounts ?? {}) }
tgt.itemCounts[id] = (tgt.itemCounts[id] ?? 0) - qty tgt.itemCounts[id] = (tgt.itemCounts[id] ?? 0) - qty
if (tgt.itemCounts[id] <= 0) delete tgt.itemCounts[id] if (tgt.itemCounts[id] <= 0) {
delete tgt.itemCounts[id]
tgt.inventory = (tgt.inventory ?? []).filter(i => i !== id)
}
} }
if (!newPlayer.inventory.includes(id)) newPlayer.inventory = [...newPlayer.inventory, id]
newPlayer.itemCounts = { ...(newPlayer.itemCounts ?? {}) } newPlayer.itemCounts = { ...(newPlayer.itemCounts ?? {}) }
newPlayer.itemCounts[id] = (newPlayer.itemCounts[id] ?? 0) + qty newPlayer.itemCounts[id] = (newPlayer.itemCounts[id] ?? 0) + qty
} }
} }
setPlayer(newPlayer) setPlayer(newPlayer)
if (combat) {
setCombat(prev => {
const chars = { ...prev.characters }
chars.player = { ...chars.player, inventory: [...newPlayer.inventory], itemCounts: { ...newPlayer.itemCounts }, weaponConditions: { ...newPlayer.weaponConditions } }
if (tradeTarget && chars[tradeTarget] && tgtWorld) {
chars[tradeTarget] = { ...chars[tradeTarget], inventory: [...tgtWorld.inventory], itemCounts: { ...tgtWorld.itemCounts }, weaponConditions: { ...tgtWorld.weaponConditions } }
}
return { ...prev, log: [...prev.log, 'Trade completed.'], characters: chars }
})
}
setShowTrade(false) setShowTrade(false)
setTradeTarget(null) setTradeTarget(null)
setTradeGive({}) setTradeGive({})
@@ -3344,7 +3381,15 @@ onContextMenu={e => { e.preventDefault(); setHoverInfo(null); setHoveredChar(nul
<span className="trade-item-name">{item.name}{item.qty > 1 ? ` (${item.qty})` : ''}{item.cond != null ? ` [${item.cond}/${item.maxCond}]` : ''}</span> <span className="trade-item-name">{item.name}{item.qty > 1 ? ` (${item.qty})` : ''}{item.cond != null ? ` [${item.cond}/${item.maxCond}]` : ''}</span>
<div className="trade-qty-controls"> <div className="trade-qty-controls">
<button className="trade-qty-btn" onClick={() => setTradeGive(prev => ({ ...prev, [item.id]: Math.max(0, (prev[item.id] ?? 0) - 1) }))}>-</button> <button className="trade-qty-btn" onClick={() => setTradeGive(prev => ({ ...prev, [item.id]: Math.max(0, (prev[item.id] ?? 0) - 1) }))}>-</button>
<span className="trade-qty">{tradeGive[item.id] ?? 0}</span> {item.cond != null ? (
<span className="trade-qty">{tradeGive[item.id] ?? 0}</span>
) : (
<input className="trade-qty-input" type="number" min="0" max={item.qty} value={tradeGive[item.id] ?? 0}
onChange={e => {
const v = parseInt(e.target.value, 10)
setTradeGive(prev => ({ ...prev, [item.id]: isNaN(v) ? 0 : Math.max(0, Math.min(item.qty, v)) }))
}} />
)}
<button className="trade-qty-btn" onClick={() => setTradeGive(prev => ({ ...prev, [item.id]: Math.min(item.qty, (prev[item.id] ?? 0) + 1) }))}>+</button> <button className="trade-qty-btn" onClick={() => setTradeGive(prev => ({ ...prev, [item.id]: Math.min(item.qty, (prev[item.id] ?? 0) + 1) }))}>+</button>
</div> </div>
</div> </div>
@@ -3359,7 +3404,15 @@ onContextMenu={e => { e.preventDefault(); setHoverInfo(null); setHoveredChar(nul
<span className="trade-item-name">{item.name}{item.qty > 1 ? ` (${item.qty})` : ''}{item.cond != null ? ` [${item.cond}/${item.maxCond}]` : ''}</span> <span className="trade-item-name">{item.name}{item.qty > 1 ? ` (${item.qty})` : ''}{item.cond != null ? ` [${item.cond}/${item.maxCond}]` : ''}</span>
<div className="trade-qty-controls"> <div className="trade-qty-controls">
<button className="trade-qty-btn" onClick={() => setTradeTake(prev => ({ ...prev, [item.id]: Math.max(0, (prev[item.id] ?? 0) - 1) }))}>-</button> <button className="trade-qty-btn" onClick={() => setTradeTake(prev => ({ ...prev, [item.id]: Math.max(0, (prev[item.id] ?? 0) - 1) }))}>-</button>
<span className="trade-qty">{tradeTake[item.id] ?? 0}</span> {item.cond != null ? (
<span className="trade-qty">{tradeTake[item.id] ?? 0}</span>
) : (
<input className="trade-qty-input" type="number" min="0" max={item.qty} value={tradeTake[item.id] ?? 0}
onChange={e => {
const v = parseInt(e.target.value, 10)
setTradeTake(prev => ({ ...prev, [item.id]: isNaN(v) ? 0 : Math.max(0, Math.min(item.qty, v)) }))
}} />
)}
<button className="trade-qty-btn" onClick={() => setTradeTake(prev => ({ ...prev, [item.id]: Math.min(item.qty, (prev[item.id] ?? 0) + 1) }))}>+</button> <button className="trade-qty-btn" onClick={() => setTradeTake(prev => ({ ...prev, [item.id]: Math.min(item.qty, (prev[item.id] ?? 0) + 1) }))}>+</button>
</div> </div>
</div> </div>
@@ -3368,7 +3421,7 @@ onContextMenu={e => { e.preventDefault(); setHoverInfo(null); setHoveredChar(nul
</div> </div>
</div> </div>
<div className="trade-bar"> <div className="trade-bar">
<span className="trade-bar-label">Give: <span className="trade-give-val">{giveVal}g</span> &nbsp; Take: <span className="trade-take-val">{takeVal}g</span></span> <span className="trade-bar-label">Give: <span className="trade-give-val">{giveVal} {CURRENCY}</span> &nbsp; Take: <span className="trade-take-val">{takeVal} {CURRENCY}</span></span>
</div> </div>
<button className="trade-propose-btn" onClick={proposeTrade}>Propose Trade</button> <button className="trade-propose-btn" onClick={proposeTrade}>Propose Trade</button>
</div> </div>
@@ -3409,6 +3462,16 @@ onContextMenu={e => { e.preventDefault(); setHoverInfo(null); setHoveredChar(nul
<button className={`context-menu-btn${canTrade(contextMenu.charId) ? '' : ' disabled'}`} disabled={!canTrade(contextMenu.charId)} onClick={e => { e.stopPropagation(); if (canTrade(contextMenu.charId)) startTrade(contextMenu.charId) }}>Trade</button> <button className={`context-menu-btn${canTrade(contextMenu.charId) ? '' : ' disabled'}`} disabled={!canTrade(contextMenu.charId)} onClick={e => { e.stopPropagation(); if (canTrade(contextMenu.charId)) startTrade(contextMenu.charId) }}>Trade</button>
</div> </div>
)} )}
{/* ═══════════════════════════════════════════════════════ NOTIFICATIONS */}
<div className="notifications-container">
{notifications.map(n => (
<div key={n.id} className="notification">
<div className="notification-subject">{n.subject}</div>
<div className="notification-desc">{n.description}</div>
</div>
))}
</div>
</div> </div>
) )
} }