From 819306aba3cd9043dc241f68e90a43eb2d0e3027 Mon Sep 17 00:00:00 2001 From: johnruina Date: Wed, 17 Jun 2026 03:26:18 -0400 Subject: [PATCH] gun stuff combat stuff --- src/App.jsx | 298 ++++++++++++++++++++++++----------- src/assets/chamberbullet.mp3 | Bin 0 -> 14400 bytes 2 files changed, 208 insertions(+), 90 deletions(-) create mode 100644 src/assets/chamberbullet.mp3 diff --git a/src/App.jsx b/src/App.jsx index 2409394..329ea28 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -2,6 +2,7 @@ import { useState, useRef, useCallback, useEffect, Fragment } from 'react' import './App.css' import maleBody from './assets/body/malebody.png' import pistolShotMp3 from './assets/pistolshot.mp3' +import chamberBulletMp3 from './assets/chamberbullet.mp3' // ───────────────────────────────────────────────────────────────────────────── // WORLD DATA @@ -96,7 +97,7 @@ function getItemValue(id) { const INJURY_DECAY = { cut: 0.3, laceration: 0.6, stab: 0.8 } const TURN_SPEED = 6 -const PISTOL_MAG = 2 +const PISTOL_MAG = 7 const bodyPartLabels = { head: 'Head', torso: 'Torso', @@ -112,6 +113,49 @@ function rectCollides(px, py, block) { return px >= block.x && px <= block.x + block.w && py >= block.y && py <= block.y + block.h } +function simulateBulletTrajectory(startX, startY, endX, endY, blocks, maxBounces = 5, maxDist) { + const dx = endX - startX + const dy = endY - startY + const dirLen = Math.sqrt(dx * dx + dy * dy) || 1 + const dir = { x: dx / dirLen, y: dy / dirLen } + const points = [{ x: startX, y: startY }] + let px = startX, py = startY + let cx = dir.x, cy = dir.y + let remaining = maxDist ?? dirLen + let bounces = 0 + while (remaining > 0) { + const step = Math.min(2, remaining) + const nx = px + cx * step + const ny = py + cy * step + const hitBlock = blocks.find(b => rectCollides(nx, ny, b)) + if (hitBlock) { + points.push({ x: nx, y: ny }) + bounces++ + if (bounces > maxBounces) break + const dl = nx - hitBlock.x + const dr = (hitBlock.x + hitBlock.w) - nx + const dt = ny - hitBlock.y + const db = (hitBlock.y + hitBlock.h) - ny + if (Math.min(dl, dr) < Math.min(dt, db)) { + cx = -cx + } else { + cy = -cy + } + remaining -= step + px = nx + cx + py = ny + cy + continue + } + px = nx + py = ny + remaining -= step + } + if (points[points.length - 1].x !== px || points[points.length - 1].y !== py) { + points.push({ x: px, y: py }) + } + return points +} + function partColor(hp, max) { const r = hp / max if (r > 0.6) return '#22c55e' @@ -465,12 +509,12 @@ export default function App() { // ── world state const [player, setPlayer] = useState(createCharacter({ id: 'player', name: 'John', age: 24, location: 'A', money: 50, - inventory: ['pistol_ammo', 'bouncing_ammo'], - equipped: { left: 'pistol', right: 'rusty_sword' }, - weaponConditions: { rusty_sword: 65, pistol: 100 }, + inventory: [], + equipped: { left: 'fists', right: 'fists' }, + weaponConditions: {}, size: 1.3, - weaponMagazines: { pistol: [{ ammoType: 'pistol_ammo' }, { ammoType: 'pistol_ammo' }] }, - itemCounts: { pistol_ammo: 20, bouncing_ammo: 10 }, + weaponMagazines: {}, + itemCounts: {}, })) const [gameTime, setGameTime] = useState({ day: 1, hour: 8, minute: 0 }) const [weather, setWeather] = useState('clear') @@ -534,6 +578,7 @@ export default function App() { const [busy, setBusy] = useState(false) // ── aiming mode (pistol skill selected, waiting for minimap click) + const [minimapHover, setMinimapHover] = useState(null) const [aimMode, setAimMode] = useState(false) // ── reload ammo picker removed; uses selectedSkill === 'Load' inline @@ -1675,18 +1720,19 @@ class MoveAction { } /** Enter a location — creates a combat view with all NPCs present as non-hostile */ - const enterLocation = (locationId) => { + const enterLocation = (locationId, playerData) => { + const pd = playerData ?? player const characters = {} characters['player'] = createCharacter({ - id: 'player', name: player.name, + id: 'player', name: pd.name, pos: { x: 20, y: 75 }, - integrity: { ...player.integrity }, - injuries: JSON.parse(JSON.stringify(player.injuries)), - equipped: { ...player.equipped }, - weaponConditions: { ...player.weaponConditions }, - weaponMagazines: { ...player.weaponMagazines }, - itemCounts: { ...player.itemCounts }, + integrity: { ...pd.integrity }, + injuries: JSON.parse(JSON.stringify(pd.injuries)), + equipped: { ...pd.equipped }, + weaponConditions: { ...pd.weaponConditions }, + weaponMagazines: { ...pd.weaponMagazines }, + itemCounts: { ...pd.itemCounts }, }) const partyCharIds = ['player'] @@ -1700,7 +1746,7 @@ class MoveAction { id: lc.id, name: lc.name, pos: { x: xOff, y: 130 }, integrity: { ...lc.integrity }, - injuries: JSON.parse(JSON.stringify(lc.injuries ?? player.injuries)), + injuries: JSON.parse(JSON.stringify(lc.injuries ?? pd.injuries)), equipped: { ...lc.equipped }, weaponConditions: { ...lc.weaponConditions }, weaponMagazines: { ...lc.weaponMagazines }, @@ -1740,8 +1786,11 @@ class MoveAction { setCombatTime(0); setDisplayTime(0) characterActionsRef.current = {} characterPositionsRef.current = {} + characterDirsRef.current = {} for (const cid of Object.keys(characters)) { + characterActionsRef.current[cid] = null characterPositionsRef.current[cid] = { ...characters[cid].pos } + characterDirsRef.current[cid] = { dx: 0, dy: 1 } } recordingRef.current = null; setFightRecord(null); setProjectiles([]); setHitShake(null) speedRef.current = 1; setSpeedMult(1) @@ -1935,58 +1984,59 @@ class MoveAction { const actorId = currentActor ?? 'player' const cc = combatRef.current const actorChar = cc?.characters[actorId] - const mag = actorId === 'player' ? magazineRef.current : (actorChar?.weaponMagazines ?? { pistol: [] }) const itemCounts = actorChar?.itemCounts ?? (actorId === 'player' ? playerRef.current.itemCounts : {}) - const cur = mag.pistol?.length ?? 0 const reserve = itemCounts[ammoId] ?? 0 if (reserve <= 0) return const ammoName = itemMap[ammoId]?.name ?? ammoId - if (actorId === 'player') { - const newMag = { pistol: [...magazineRef.current.pistol, { ammoType: ammoId }] } - magazineRef.current = newMag - setPlayer(p => { - const newCount = (p.itemCounts[ammoId] ?? 0) - 1 - const counts = { ...p.itemCounts, [ammoId]: newCount } - if (newCount <= 0) delete counts[ammoId] - return { - ...p, - weaponMagazines: { ...p.weaponMagazines, ...newMag }, - itemCounts: counts, - } - }) - setCombat(prev => { - const ch = prev.characters['player'] - const newCount = (ch?.itemCounts?.[ammoId] ?? 0) - 1 - const counts = { ...(ch?.itemCounts ?? {}), [ammoId]: newCount } - if (newCount <= 0) delete counts[ammoId] - return { - ...prev, - characters: { - ...prev.characters, - player: { ...ch, itemCounts: counts }, - }, - log: [...prev.log, `Loaded 1 round of ${ammoName} into the pistol.`], - } - }) - } else { - setCombat(prev => { - const ch = prev.characters[actorId] - if (!ch) return prev - const chMag = ch.weaponMagazines ?? { pistol: [] } - const newMag = { pistol: [...(chMag.pistol ?? []), { ammoType: ammoId }] } - const newCounts = { ...ch.itemCounts, [ammoId]: (ch.itemCounts?.[ammoId] ?? 0) - 1 } - if (newCounts[ammoId] <= 0) delete newCounts[ammoId] - return { - ...prev, - characters: { - ...prev.characters, - [actorId]: { ...ch, weaponMagazines: { ...ch.weaponMagazines, ...newMag }, itemCounts: newCounts }, - }, - log: [...prev.log, `${ch.name ?? 'Party member'} loaded 1 round of ${ammoName} into the pistol.`], - } - }) - } + new Audio(chamberBulletMp3).play().catch(() => {}) setLimbSelect(null); setSelectedSkill(null); setSelectedTarget(null); setAttackEnemy(null) + setTimeout(() => { + if (actorId === 'player') { + const newMag = { pistol: [...magazineRef.current.pistol, { ammoType: ammoId }] } + magazineRef.current = newMag + setPlayer(p => { + const newCount = (p.itemCounts[ammoId] ?? 0) - 1 + const counts = { ...p.itemCounts, [ammoId]: newCount } + if (newCount <= 0) delete counts[ammoId] + return { + ...p, + weaponMagazines: { ...p.weaponMagazines, ...newMag }, + itemCounts: counts, + } + }) + setCombat(prev => { + const ch = prev.characters['player'] + const newCount = (ch?.itemCounts?.[ammoId] ?? 0) - 1 + const counts = { ...(ch?.itemCounts ?? {}), [ammoId]: newCount } + if (newCount <= 0) delete counts[ammoId] + return { + ...prev, + characters: { + ...prev.characters, + player: { ...ch, itemCounts: counts }, + }, + log: [...prev.log, `Loaded 1 round of ${ammoName} into the pistol.`], + } + }) + } else { + setCombat(prev => { + const ch = prev.characters[actorId] + if (!ch) return prev + const chMag = ch.weaponMagazines ?? { pistol: [] } + const newMag = { pistol: [...(chMag.pistol ?? []), { ammoType: ammoId }] } + const newCounts = { ...ch.itemCounts, [ammoId]: (ch.itemCounts?.[ammoId] ?? 0) - 1 } + if (newCounts[ammoId] <= 0) delete newCounts[ammoId] + return { + ...prev, + characters: { + ...prev.characters, + [actorId]: { ...ch, weaponMagazines: { ...ch.weaponMagazines, ...newMag }, itemCounts: newCounts }, + }, + log: [...prev.log, `${ch.name ?? 'Party member'} loaded 1 round of ${ammoName} into the pistol.`], + } + }) + } + }, 500) } const unloadPistol = () => { @@ -2003,36 +2053,50 @@ class MoveAction { const top = mag.pistol[cur - 1] const ammoId = top.ammoType const ammoName = itemMap[ammoId]?.name ?? ammoId - if (actorId === 'player') { - const newMag = { pistol: mag.pistol.slice(0, -1) } - magazineRef.current = newMag - setPlayer(p => ({ - ...p, - weaponMagazines: { ...p.weaponMagazines, ...newMag }, - itemCounts: { ...p.itemCounts, [ammoId]: (p.itemCounts[ammoId] ?? 0) + 1 }, - })) - setCombat(c => ({ ...c, log: [...c.log, `Unloaded 1 round of ${ammoName} from the pistol.`] })) - } else { - setCombat(prev => { - const ch = prev.characters[actorId] - if (!ch) return prev - const chMag = ch.weaponMagazines ?? { pistol: [] } - const newMag = { pistol: (chMag.pistol ?? []).slice(0, -1) } - return { + new Audio(chamberBulletMp3).play().catch(() => {}) + setLimbSelect(null); setSelectedSkill(null); setSelectedTarget(null); setAttackEnemy(null) + setTimeout(() => { + if (actorId === 'player') { + const newMag = { pistol: mag.pistol.slice(0, -1) } + magazineRef.current = newMag + setPlayer(p => ({ + ...p, + weaponMagazines: { ...p.weaponMagazines, ...newMag }, + itemCounts: { ...p.itemCounts, [ammoId]: (p.itemCounts[ammoId] ?? 0) + 1 }, + })) + setCombat(prev => ({ ...prev, characters: { ...prev.characters, - [actorId]: { - ...ch, - weaponMagazines: { ...ch.weaponMagazines, ...newMag }, - itemCounts: { ...ch.itemCounts, [ammoId]: (ch.itemCounts?.[ammoId] ?? 0) + 1 }, + player: { + ...prev.characters.player, + weaponMagazines: { ...prev.characters.player?.weaponMagazines, ...newMag }, + itemCounts: { ...(prev.characters.player?.itemCounts ?? {}), [ammoId]: ((prev.characters.player?.itemCounts?.[ammoId] ?? 0) + 1) }, }, }, - log: [...prev.log, `${ch.name ?? 'Party member'} unloaded 1 round of ${ammoName} from the pistol.`], - } - }) - } - setLimbSelect(null); setSelectedSkill(null); setSelectedTarget(null); setAttackEnemy(null) + log: [...prev.log, `Unloaded 1 round of ${ammoName} from the pistol.`], + })) + } else { + setCombat(prev => { + const ch = prev.characters[actorId] + if (!ch) return prev + const chMag = ch.weaponMagazines ?? { pistol: [] } + const newMag = { pistol: (chMag.pistol ?? []).slice(0, -1) } + return { + ...prev, + characters: { + ...prev.characters, + [actorId]: { + ...ch, + weaponMagazines: { ...ch.weaponMagazines, ...newMag }, + itemCounts: { ...ch.itemCounts, [ammoId]: (ch.itemCounts?.[ammoId] ?? 0) + 1 }, + }, + }, + log: [...prev.log, `${ch.name ?? 'Party member'} unloaded 1 round of ${ammoName} from the pistol.`], + } + }) + } + }, 250) } // ───────────────────────────────────────────────────────────────────────── @@ -2163,7 +2227,7 @@ class MoveAction { if (showCharCreate) { return { const { name, weaponId, fillColor, locationId } = opts - setPlayer(createCharacter({ + const newPlayer = createCharacter({ id: 'player', name, age: 24, location: locationId, money: 50, inventory: weaponId === 'pistol' ? ['pistol_ammo', 'bouncing_ammo'] : [], equipped: { left: weaponId === 'pistol' ? 'pistol' : 'fists', right: weaponId === 'pistol' ? 'fists' : weaponId }, @@ -2172,9 +2236,11 @@ class MoveAction { weaponMagazines: weaponId === 'pistol' ? { pistol: [{ ammoType: 'pistol_ammo' }, { ammoType: 'pistol_ammo' }] } : {}, itemCounts: weaponId === 'pistol' ? { pistol_ammo: 20, bouncing_ammo: 10 } : {}, fillColor, - })) + }) + setPlayer(newPlayer) + magazineRef.current = newPlayer.weaponMagazines setShowCharCreate(false) - setTimeout(() => enterLocation(locationId), 50) + setTimeout(() => enterLocation(locationId, newPlayer), 50) }} /> } @@ -2398,6 +2464,14 @@ class MoveAction { const onUp = () => { minimapPanRef.current = false; window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp) } window.addEventListener('mousemove', onMove); window.addEventListener('mouseup', onUp) }} + onPointerMove={e => { + if (minimapPanRef.current) return + const r = e.currentTarget.getBoundingClientRect() + const mx = ((e.clientX - r.left) / r.width) * 160 / minimapView.scale + minimapView.x + const my = ((e.clientY - r.top) / r.height) * 160 / minimapView.scale + minimapView.y + setMinimapHover({ x: mx, y: my }) + }} + onPointerLeave={() => setMinimapHover(null)} onWheel={e => { if (limbSelect) return; e.preventDefault() const d = e.deltaY > 0 ? 0.9 : 1.1 @@ -2470,6 +2544,50 @@ class MoveAction { const rp = combat.characters[rangeActor]?.pos ?? combat.characters['player'].pos return })()} + {moveMode && moveMode !== 'turn' && minimapHover && combat && (() => { + const actorId = currentActor ?? 'player' + const ch = combat.characters[actorId] + if (!ch?.pos) return null + return ( + + ) + })()} + {aimMode && minimapHover && combat && (() => { + const actorId = currentActor ?? 'player' + const ch = combat.characters[actorId] + if (!ch?.pos) return null + const charMag = actorId === 'player' ? magazineRef.current : (ch.weaponMagazines ?? {}) + const currentAmmo = charMag.pistol?.[0]?.ammoType + const dx = minimapHover.x - ch.pos.x + const dy = minimapHover.y - ch.pos.y + const cursorDist = Math.sqrt(dx * dx + dy * dy) || 1 + const range = weaponMap.pistol?.skills?.find(s => s.name === 'Shoot')?.range ?? 60 + const travelDist = Math.max(cursorDist, range) + const maxBounces = currentAmmo === 'bouncing_ammo' ? 5 : 0 + const facing = characterDirsRef.current[actorId] ?? { dx: 0, dy: 1 } + const dot = Math.max(-1, Math.min(1, facing.dx * (dx / cursorDist) + facing.dy * (dy / cursorDist))) + const maxDeviation = (1 - dot) / 2 * (Math.PI / 4) + const cosA = Math.cos(maxDeviation), sinA = Math.sin(maxDeviation) + const spreadX1 = (dx / cursorDist) * cosA - (dy / cursorDist) * sinA + const spreadY1 = (dx / cursorDist) * sinA + (dy / cursorDist) * cosA + const spreadX2 = (dx / cursorDist) * cosA + (dy / cursorDist) * sinA + const spreadY2 = -(dx / cursorDist) * sinA + (dy / cursorDist) * cosA + const blocks = combat.blocks ?? [] + const trajectoryPts = simulateBulletTrajectory(ch.pos.x, ch.pos.y, minimapHover.x, minimapHover.y, blocks, maxBounces, travelDist) + const last1 = simulateBulletTrajectory(ch.pos.x, ch.pos.y, ch.pos.x + spreadX1 * travelDist, ch.pos.y + spreadY1 * travelDist, blocks, maxBounces, travelDist).at(-1) + const last2 = simulateBulletTrajectory(ch.pos.x, ch.pos.y, ch.pos.x + spreadX2 * travelDist, ch.pos.y + spreadY2 * travelDist, blocks, maxBounces, travelDist).at(-1) + const pathD = trajectoryPts.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`).join(' ') + return ( + <> + + + + + ) + })()} {/* Party member circles */} {combat && (() => { const partyCircles = combat.partyIds ?? [combat.playerEntityId].filter(Boolean) diff --git a/src/assets/chamberbullet.mp3 b/src/assets/chamberbullet.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..d80c7df5d2f804a535ccf9d0754aff97b0d29cc8 GIT binary patch literal 14400 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