gun stuff combat stuff

This commit is contained in:
2026-06-17 03:26:18 -04:00
parent a066f15279
commit 819306aba3
2 changed files with 208 additions and 90 deletions
+141 -23
View File
@@ -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,12 +1984,13 @@ 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
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
@@ -1986,7 +2036,7 @@ class MoveAction {
}
})
}
setLimbSelect(null); setSelectedSkill(null); setSelectedTarget(null); setAttackEnemy(null)
}, 500)
}
const unloadPistol = () => {
@@ -2003,6 +2053,9 @@ class MoveAction {
const top = mag.pistol[cur - 1]
const ammoId = top.ammoType
const ammoName = itemMap[ammoId]?.name ?? ammoId
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
@@ -2011,7 +2064,18 @@ class MoveAction {
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.`] }))
setCombat(prev => ({
...prev,
characters: {
...prev.characters,
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, `Unloaded 1 round of ${ammoName} from the pistol.`],
}))
} else {
setCombat(prev => {
const ch = prev.characters[actorId]
@@ -2032,7 +2096,7 @@ class MoveAction {
}
})
}
setLimbSelect(null); setSelectedSkill(null); setSelectedTarget(null); setAttackEnemy(null)
}, 250)
}
// ─────────────────────────────────────────────────────────────────────────
@@ -2163,7 +2227,7 @@ class MoveAction {
if (showCharCreate) {
return <CharacterCreator onConfirm={(opts) => {
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 <circle cx={rp.x} cy={rp.y} r={getSkillRange(limbSelect, selectedSkill)} fill="none" stroke="rgba(52,152,219,0.5)" strokeWidth="1" strokeDasharray="4 4" />
})()}
{moveMode && moveMode !== 'turn' && minimapHover && combat && (() => {
const actorId = currentActor ?? 'player'
const ch = combat.characters[actorId]
if (!ch?.pos) return null
return (
<line x1={ch.pos.x} y1={ch.pos.y} x2={minimapHover.x} y2={minimapHover.y}
stroke="#22c55e" strokeWidth="1.5" strokeDasharray="3 3" opacity="0.7" />
)
})()}
{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 (
<>
<path d={pathD} stroke="#ef4444" strokeWidth="1" strokeDasharray="3 3" fill="none" />
<line x1={ch.pos.x} y1={ch.pos.y} x2={last1.x} y2={last1.y}
stroke="#ef4444" strokeWidth="0.5" strokeDasharray="2 4" opacity="0.5" />
<line x1={ch.pos.x} y1={ch.pos.y} x2={last2.x} y2={last2.y}
stroke="#ef4444" strokeWidth="0.5" strokeDasharray="2 4" opacity="0.5" />
</>
)
})()}
{/* Party member circles */}
{combat && (() => {
const partyCircles = combat.partyIds ?? [combat.playerEntityId].filter(Boolean)
Binary file not shown.