i forgot
This commit is contained in:
+60
@@ -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); }
|
||||||
|
}
|
||||||
|
|||||||
+90
-27
@@ -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,8 +3272,13 @@ 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)
|
||||||
|
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 })
|
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) {
|
||||||
if (seen.has(id)) continue; seen.add(id)
|
if (seen.has(id)) continue; seen.add(id)
|
||||||
@@ -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>
|
||||||
|
{item.cond != null ? (
|
||||||
<span className="trade-qty">{tradeGive[item.id] ?? 0}</span>
|
<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>
|
||||||
|
{item.cond != null ? (
|
||||||
<span className="trade-qty">{tradeTake[item.id] ?? 0}</span>
|
<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> Take: <span className="trade-take-val">{takeVal}g</span></span>
|
<span className="trade-bar-label">Give: <span className="trade-give-val">{giveVal} {CURRENCY}</span> 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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user