diff --git a/src/App.css b/src/App.css index 04c9483..293cea4 100644 --- a/src/App.css +++ b/src/App.css @@ -2478,6 +2478,28 @@ body { min-width: 20px; 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 { margin-top: 12px; padding: 10px 12px; @@ -2504,3 +2526,41 @@ body { .trade-propose-btn:hover { 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); } +} diff --git a/src/App.jsx b/src/App.jsx index 329ea28..5f30a72 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -32,11 +32,12 @@ const characters = [ { 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: '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 = [ { 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: '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 }] }, @@ -45,8 +46,8 @@ const weapons = [ ] const items = [ - { id: 'pistol_ammo', name: 'Pistol Ammo', description: 'Standard pistol ammunition.', weight: 0.5 }, - { id: 'bouncing_ammo', name: 'Bouncing Ammo', description: 'Experimental ammo that bounces off walls.', 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, bulletSize: '9mm' }, ] const itemMap = {} @@ -97,6 +98,7 @@ function getItemValue(id) { const INJURY_DECAY = { cut: 0.3, laceration: 0.6, stab: 0.8 } const TURN_SPEED = 6 +const CURRENCY = 'ahn' const PISTOL_MAG = 7 const bodyPartLabels = { @@ -593,6 +595,22 @@ export default function App() { const [fightRecord, setFightRecord] = useState(null) 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 const [showMenu, setShowMenu] = useState(true) const [showCharCreate, setShowCharCreate] = useState(false) @@ -733,6 +751,7 @@ export default function App() { const subSteps = Math.max(1, Math.ceil(moveDist)) let hit = false + let hitCx = null, hitCy = null for (let s = 1; s <= subSteps; s++) { const t = s / subSteps @@ -759,7 +778,7 @@ export default function App() { break } applyProjectileHit(p, null) - hit = true; break + hit = true; hitCx = cx; hitCy = cy; break } for (const cid of Object.keys(cc.characters)) { @@ -770,12 +789,16 @@ export default function App() { const r = cfg.hitRadius + 4 * (e.size ?? 1) if (Math.sqrt((cx - ePos.x) ** 2 + (cy - ePos.y) ** 2) < r) { applyProjectileHit(p, cid) - hit = true; break + hit = true; hitCx = cx; hitCy = cy; 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 @@ -1764,7 +1787,7 @@ class MoveAction { equipped: { left: 'fists', right: 'fists' }, weaponConditions: {}, weaponMagazines: {}, - itemCounts: {}, + itemCounts: { ...(lc.itemCounts ?? {}) }, inventory: [...(lc.inventory ?? [])], fillColor: lc.fillColor, }) @@ -1971,7 +1994,7 @@ class MoveAction { } const itemCounts = actorChar?.itemCounts ?? (actorId === 'player' ? playerRef.current.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) { setCombat(c => ({ ...c, log: [...c.log, 'No ammo in inventory!'] })) return @@ -2198,9 +2221,9 @@ class MoveAction { if (!combat || charId === 'player') return true const src = combat.characters?.player const tgt = combat.characters?.[charId] - if (!src || !tgt) return true - const dx = src.x - tgt.x - const dy = src.y - tgt.y + if (!src?.pos || !tgt?.pos) return true + const dx = src.pos.x - tgt.pos.x + const dy = src.pos.y - tgt.pos.y return Math.sqrt(dx * dx + dy * dy) <= 35 } @@ -2811,11 +2834,11 @@ class MoveAction {