gun
This commit is contained in:
+213
-101
@@ -21,13 +21,13 @@ const connections = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const characters = [
|
const characters = [
|
||||||
{ id: 'player', name: 'You', location: 'A', size: 1.3 },
|
{ id: 'player', name: 'You', location: 'A', size: 1 },
|
||||||
{ id: '1', name: 'Mara', location: 'A', money: 120, inventory: ['pistol_ammo'], size: 1 },
|
{ id: '1', name: 'Mara', location: 'A', money: 120, inventory: ['pistol_ammo'], size: 1 },
|
||||||
{ id: '2', name: 'Rook', location: 'A', money: 0, inventory: ['pistol_ammo'], size: 1.2 },
|
{ id: '2', name: 'Rook', location: 'A', money: 0, inventory: ['pistol_ammo'], size: 1 },
|
||||||
{ id: '3', name: 'Vex', location: 'B', money: 500, inventory: ['pistol_ammo'], size: 0.9 },
|
{ id: '3', name: 'Vex', location: 'B', money: 500, inventory: ['pistol_ammo'], size: 1 },
|
||||||
{ id: 'd1', name: 'Dust Gutter', location: 'D', money: 15, inventory: ['pistol_ammo'], size: 1 },
|
{ id: 'd1', name: 'Dust Gutter', location: 'D', money: 15, inventory: ['pistol_ammo'], size: 1 },
|
||||||
{ id: 'd2', name: 'Bone Collector', location: 'D', money: 30, inventory: ['pistol_ammo'], size: 1.1 },
|
{ id: 'd2', name: 'Bone Collector', location: 'D', money: 30, inventory: ['pistol_ammo'], size: 1 },
|
||||||
{ id: 'd3', name: 'Sand Viper', location: 'D', money: 8, inventory: ['pistol_ammo'], size: 0.8 },
|
{ 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 },
|
||||||
]
|
]
|
||||||
@@ -40,6 +40,7 @@ const shops = [
|
|||||||
{ name: 'Iron Shield', price: 50, weaponId: 'iron_shield' },
|
{ name: 'Iron Shield', price: 50, weaponId: 'iron_shield' },
|
||||||
{ name: 'Dagger', price: 30, weaponId: 'dagger' },
|
{ name: 'Dagger', price: 30, weaponId: 'dagger' },
|
||||||
{ name: 'Pistol Ammo', price: 5, itemId: 'pistol_ammo' },
|
{ name: 'Pistol Ammo', price: 5, itemId: 'pistol_ammo' },
|
||||||
|
{ name: 'Bouncing Ammo', price: 8, itemId: 'bouncing_ammo' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -56,6 +57,7 @@ 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 },
|
||||||
|
{ id: 'bouncing_ammo', name: 'Bouncing Ammo', description: 'Experimental ammo that bounces off walls.', weight: 0.5 },
|
||||||
]
|
]
|
||||||
|
|
||||||
const itemMap = {}
|
const itemMap = {}
|
||||||
@@ -132,7 +134,8 @@ function createCharacter(data) {
|
|||||||
fillColor: data.fillColor ?? `hsl(${Math.random() * 360}, 70%, 55%)`,
|
fillColor: data.fillColor ?? `hsl(${Math.random() * 360}, 70%, 55%)`,
|
||||||
weapon: weaponId,
|
weapon: weaponId,
|
||||||
pos: data.pos ? { x: data.pos.x, y: data.pos.y } : undefined,
|
pos: data.pos ? { x: data.pos.x, y: data.pos.y } : undefined,
|
||||||
speed: data.speed ?? wpn?.speed ?? 10,
|
speed: (data.speed ?? wpn?.speed ?? 10) / 2,
|
||||||
|
maxSpeed: data.maxSpeed ?? 2,
|
||||||
attack: data.attack ?? wpn?.damage ?? 30,
|
attack: data.attack ?? wpn?.damage ?? 30,
|
||||||
weaponMagazines: data.weaponMagazines ? { ...data.weaponMagazines } : {},
|
weaponMagazines: data.weaponMagazines ? { ...data.weaponMagazines } : {},
|
||||||
itemCounts: data.itemCounts ? { ...data.itemCounts } : {},
|
itemCounts: data.itemCounts ? { ...data.itemCounts } : {},
|
||||||
@@ -244,6 +247,7 @@ function computeHit({ weaponId, skill, weaponConditions, bothMult = 1.0 }) {
|
|||||||
* Get range for a weapon + skill combo.
|
* Get range for a weapon + skill combo.
|
||||||
*/
|
*/
|
||||||
function getSkillRange(weaponId, skillName) {
|
function getSkillRange(weaponId, skillName) {
|
||||||
|
if (weaponId === 'left_arm' || weaponId === 'right_arm') return 12
|
||||||
const base = weaponMap[weaponId]
|
const base = weaponMap[weaponId]
|
||||||
if (!base) return 15
|
if (!base) return 15
|
||||||
const skill = (base.skills ?? []).find(s => s.name === skillName)
|
const skill = (base.skills ?? []).find(s => s.name === skillName)
|
||||||
@@ -380,12 +384,12 @@ export default function App() {
|
|||||||
// ── world state
|
// ── world state
|
||||||
const [player, setPlayer] = useState(createCharacter({
|
const [player, setPlayer] = useState(createCharacter({
|
||||||
id: 'player', name: 'You', age: 24, location: 'A', money: 50,
|
id: 'player', name: 'You', age: 24, location: 'A', money: 50,
|
||||||
inventory: ['pistol_ammo'],
|
inventory: ['pistol_ammo', 'bouncing_ammo'],
|
||||||
equipped: { left: 'pistol', right: 'rusty_sword' },
|
equipped: { left: 'pistol', right: 'rusty_sword' },
|
||||||
weaponConditions: { rusty_sword: 65, pistol: 100 },
|
weaponConditions: { rusty_sword: 65, pistol: 100 },
|
||||||
size: 1.3,
|
size: 1.3,
|
||||||
weaponMagazines: { pistol: 2 },
|
weaponMagazines: { pistol: [{ ammoType: 'pistol_ammo' }, { ammoType: 'pistol_ammo' }] },
|
||||||
itemCounts: { pistol_ammo: 20 },
|
itemCounts: { pistol_ammo: 20, bouncing_ammo: 10 },
|
||||||
}))
|
}))
|
||||||
const [gameTime, setGameTime] = useState({ day: 1, hour: 8 })
|
const [gameTime, setGameTime] = useState({ day: 1, hour: 8 })
|
||||||
const [weather, setWeather] = useState('clear')
|
const [weather, setWeather] = useState('clear')
|
||||||
@@ -446,6 +450,8 @@ export default function App() {
|
|||||||
// ── aiming mode (pistol skill selected, waiting for minimap click)
|
// ── aiming mode (pistol skill selected, waiting for minimap click)
|
||||||
const [aimMode, setAimMode] = useState(false)
|
const [aimMode, setAimMode] = useState(false)
|
||||||
|
|
||||||
|
// ── reload ammo picker removed; uses selectedSkill === 'Reload' inline
|
||||||
|
|
||||||
// ── projectiles (modular projectile framework)
|
// ── projectiles (modular projectile framework)
|
||||||
const [projectiles, setProjectiles] = useState([])
|
const [projectiles, setProjectiles] = useState([])
|
||||||
|
|
||||||
@@ -462,10 +468,11 @@ export default function App() {
|
|||||||
const displayTimeRef = useRef(0); displayTimeRef.current = displayTime
|
const displayTimeRef = useRef(0); displayTimeRef.current = displayTime
|
||||||
const timerActiveRef = useRef(false)
|
const timerActiveRef = useRef(false)
|
||||||
const [timerRunning, setTimerRunning] = useState(false)
|
const [timerRunning, setTimerRunning] = useState(false)
|
||||||
|
const prevBusyRef = useRef(false)
|
||||||
const speedRef = useRef(1)
|
const speedRef = useRef(1)
|
||||||
const [speedMult, setSpeedMult] = useState(1)
|
const [speedMult, setSpeedMult] = useState(1)
|
||||||
const playerRef = useRef(null); playerRef.current = player
|
const playerRef = useRef(null); playerRef.current = player
|
||||||
const magazineRef = useRef({ pistol: 2 }); magazineRef.current = player.weaponMagazines
|
const magazineRef = useRef({ pistol: [{ ammoType: 'pistol_ammo' }, { ammoType: 'pistol_ammo' }] }); magazineRef.current = player.weaponMagazines
|
||||||
|
|
||||||
// ── Universal character action/direction/position refs ──
|
// ── Universal character action/direction/position refs ──
|
||||||
// Every character (player, enemies) uses the same ref structure.
|
// Every character (player, enemies) uses the same ref structure.
|
||||||
@@ -484,7 +491,6 @@ export default function App() {
|
|||||||
const minimapPanStart = useRef({ x: 0, y: 0 })
|
const minimapPanStart = useRef({ x: 0, y: 0 })
|
||||||
const minimapMovedRef = useRef(false)
|
const minimapMovedRef = useRef(false)
|
||||||
const replayElapsedRef = useRef(0)
|
const replayElapsedRef = useRef(0)
|
||||||
const wasBusyRef = useRef(false)
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
// DERIVED
|
// DERIVED
|
||||||
@@ -552,7 +558,7 @@ export default function App() {
|
|||||||
[PROJ_TYPES.BULLET]: { speed: 800, hitRadius: 10 },
|
[PROJ_TYPES.BULLET]: { speed: 800, hitRadius: 10 },
|
||||||
}
|
}
|
||||||
|
|
||||||
function createProjectile(startPos, endPos, actor, weaponId, skillName) {
|
function createProjectile(startPos, endPos, actor, weaponId, skillName, ammoType) {
|
||||||
const dx = endPos.x - startPos.x
|
const dx = endPos.x - startPos.x
|
||||||
const dy = endPos.y - startPos.y
|
const dy = endPos.y - startPos.y
|
||||||
const clickDist = Math.sqrt(dx * dx + dy * dy) || 1
|
const clickDist = Math.sqrt(dx * dx + dy * dy) || 1
|
||||||
@@ -569,6 +575,7 @@ export default function App() {
|
|||||||
dir: { x: dx / clickDist, y: dy / clickDist },
|
dir: { x: dx / clickDist, y: dy / clickDist },
|
||||||
weaponId,
|
weaponId,
|
||||||
skillName,
|
skillName,
|
||||||
|
ammoType,
|
||||||
hitFired: false,
|
hitFired: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -597,6 +604,18 @@ export default function App() {
|
|||||||
const cy = p.pos.y + p.dir.y * moveDist * t
|
const cy = p.pos.y + p.dir.y * moveDist * t
|
||||||
|
|
||||||
if (MAP_BLOCKS.some(b => rectCollides(cx, cy, b))) {
|
if (MAP_BLOCKS.some(b => rectCollides(cx, cy, b))) {
|
||||||
|
if (p.ammoType === 'bouncing_ammo') {
|
||||||
|
if (Math.abs(p.dir.x) > Math.abs(p.dir.y)) {
|
||||||
|
p.dir.x = -p.dir.x
|
||||||
|
} else {
|
||||||
|
p.dir.y = -p.dir.y
|
||||||
|
}
|
||||||
|
p.pos.x = cx - p.dir.x * moveDist * (1 - t)
|
||||||
|
p.pos.y = cy - p.dir.y * moveDist * (1 - t)
|
||||||
|
p.distTraveled = 0
|
||||||
|
p.dist -= moveDist * t
|
||||||
|
break
|
||||||
|
}
|
||||||
applyProjectileHit(p, null)
|
applyProjectileHit(p, null)
|
||||||
hit = true; break
|
hit = true; break
|
||||||
}
|
}
|
||||||
@@ -819,7 +838,6 @@ export default function App() {
|
|||||||
/** Queue a shot toward a map coordinate — queues a shoot action. */
|
/** Queue a shot toward a map coordinate — queues a shoot action. */
|
||||||
const queueShot = (tx, ty) => {
|
const queueShot = (tx, ty) => {
|
||||||
if (!combat) return
|
if (!combat) return
|
||||||
if (!timerActiveRef.current) { timerActiveRef.current = true; setTimerRunning(true) }
|
|
||||||
const startPos = characterPositionsRef.current[combat.playerEntityId] ?? combat.characters[combat.playerEntityId].pos
|
const startPos = characterPositionsRef.current[combat.playerEntityId] ?? combat.characters[combat.playerEntityId].pos
|
||||||
characterActionsRef.current[combat.playerEntityId] = new AnimAction({ type: 'shoot', startPos: { x: startPos.x, y: startPos.y }, endPos: { x: tx, y: ty }, elapsed: 0 })
|
characterActionsRef.current[combat.playerEntityId] = new AnimAction({ type: 'shoot', startPos: { x: startPos.x, y: startPos.y }, endPos: { x: tx, y: ty }, elapsed: 0 })
|
||||||
setLimbSelect(null); setMoveMode(null); setAimMode(false); setSelectedSkill(null)
|
setLimbSelect(null); setMoveMode(null); setAimMode(false); setSelectedSkill(null)
|
||||||
@@ -900,24 +918,37 @@ class MoveAction {
|
|||||||
|
|
||||||
function applyAnimHit(anim) {
|
function applyAnimHit(anim) {
|
||||||
const liveCc = combatRef.current
|
const liveCc = combatRef.current
|
||||||
|
const attackerId = anim.charId
|
||||||
|
const targetId = anim.actor === 'player' ? anim.enemyCharId : liveCc?.playerEntityId
|
||||||
|
|
||||||
let inRange = false
|
let inRange = false
|
||||||
if (liveCc) {
|
if (liveCc) {
|
||||||
const attackerId = anim.charId
|
// Primary: live positions from characterPositionsRef (updated by move actions)
|
||||||
const targetId = anim.actor === 'player' ? anim.enemyCharId : liveCc.playerEntityId
|
const attackerPos = characterPositionsRef.current[attackerId] ?? liveCc.characters[attackerId]?.pos
|
||||||
const attacker = liveCc.characters[attackerId]
|
const targetPos = characterPositionsRef.current[targetId] ?? liveCc.characters[targetId]?.pos
|
||||||
const target = liveCc.characters[targetId]
|
if (attackerPos && targetPos) {
|
||||||
if (attacker && target) {
|
inRange = canReach(attackerPos, targetPos, anim.weaponId, anim.skillName)
|
||||||
const range = anim.actor === 'player'
|
}
|
||||||
? getSkillRange(anim.weaponId, anim.skillName)
|
// Fallback: rendered combatRef positions
|
||||||
: 15
|
if (!inRange) {
|
||||||
inRange = canReach(attacker.pos, target.pos, anim.weaponId, anim.skillName)
|
const a2 = liveCc.characters[attackerId]?.pos
|
||||||
|
const t2 = liveCc.characters[targetId]?.pos
|
||||||
|
if (a2 && t2) {
|
||||||
|
inRange = canReach(a2, t2, anim.weaponId, anim.skillName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!inRange) {
|
if (!inRange) {
|
||||||
|
const aPos = liveCc?.characters[attackerId]?.pos
|
||||||
|
const tPos = liveCc?.characters[targetId]?.pos
|
||||||
|
const aPos2 = characterPositionsRef.current[attackerId]
|
||||||
|
const tPos2 = characterPositionsRef.current[targetId]
|
||||||
|
const dist = aPos && tPos ? Math.sqrt((aPos.x - tPos.x)**2 + (aPos.y - tPos.y)**2) : -1
|
||||||
|
const dist2 = aPos2 && tPos2 ? Math.sqrt((aPos2.x - tPos2.x)**2 + (aPos2.y - tPos2.y)**2) : -1
|
||||||
|
const range = getSkillRange(anim.weaponId, anim.skillName)
|
||||||
const msg = anim.actor === 'player'
|
const msg = anim.actor === 'player'
|
||||||
? 'Your attack misses!'
|
? `Your attack misses! (distance combatRef=${dist.toFixed(1)} refs=${dist2.toFixed(1)} range=${range})`
|
||||||
: `${liveCc?.characters[anim.charId]?.name ?? 'Enemy'}'s attack misses!`
|
: `${liveCc?.characters[anim.charId]?.name ?? 'Enemy'}'s attack misses!`
|
||||||
setCombat(prev => ({ ...prev, log: [...prev.log, msg] }))
|
setCombat(prev => ({ ...prev, log: [...prev.log, msg] }))
|
||||||
return
|
return
|
||||||
@@ -1003,12 +1034,12 @@ class MoveAction {
|
|||||||
const tick = () => {
|
const tick = () => {
|
||||||
rafId = requestAnimationFrame(tick)
|
rafId = requestAnimationFrame(tick)
|
||||||
const now = performance.now()
|
const now = performance.now()
|
||||||
const rawDt = (now - lastTime) / 1000
|
|
||||||
const dt = rawDt * speedRef.current
|
|
||||||
lastTime = now
|
|
||||||
|
|
||||||
// ── Replay mode ──
|
// ── Replay mode ──
|
||||||
if (replayActive) {
|
if (replayActive) {
|
||||||
|
const rawDt = (now - lastTime) / 1000
|
||||||
|
const dt = rawDt * speedRef.current
|
||||||
|
lastTime = now
|
||||||
const rec = fightRecord
|
const rec = fightRecord
|
||||||
if (rec) {
|
if (rec) {
|
||||||
replayElapsedRef.current += dt
|
replayElapsedRef.current += dt
|
||||||
@@ -1036,11 +1067,42 @@ class MoveAction {
|
|||||||
return (performance.now() - prev.start) / 1000 >= 0.3 ? null : prev
|
return (performance.now() - prev.start) / 1000 >= 0.3 ? null : prev
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ── Auto timer on busy state transitions; manual toggle via spacebar ──
|
||||||
|
{
|
||||||
|
const _pId = combatRef.current?.playerEntityId
|
||||||
|
const _busy = _pId ? characterActionsRef.current[_pId] !== null : false
|
||||||
|
const wasBusy = prevBusyRef.current
|
||||||
|
prevBusyRef.current = _busy
|
||||||
|
setBusy(_busy)
|
||||||
|
|
||||||
|
// Auto-start when player queues an action
|
||||||
|
if (_busy && !wasBusy && !timerActiveRef.current) {
|
||||||
|
timerActiveRef.current = true
|
||||||
|
setTimerRunning(true)
|
||||||
|
}
|
||||||
|
// Auto-stop when player's action completes
|
||||||
|
if (!_busy && wasBusy && timerActiveRef.current) {
|
||||||
|
timerActiveRef.current = false
|
||||||
|
setTimerRunning(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Stop timer if player dies ──
|
||||||
|
if (timerActiveRef.current && isDead(playerRef.current.integrity)) {
|
||||||
|
stopTimer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// dt is zero when the timer is off — animations don't play
|
||||||
if (!timerActiveRef.current) {
|
if (!timerActiveRef.current) {
|
||||||
|
lastTime = now
|
||||||
setAnimations([])
|
setAnimations([])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rawDt = (now - lastTime) / 1000
|
||||||
|
const dt = rawDt * speedRef.current
|
||||||
|
lastTime = now
|
||||||
|
|
||||||
setCombatTime(t => t + dt)
|
setCombatTime(t => t + dt)
|
||||||
setDisplayTime(t => t + dt)
|
setDisplayTime(t => t + dt)
|
||||||
|
|
||||||
@@ -1070,11 +1132,6 @@ class MoveAction {
|
|||||||
if (cp) { rawAnim.cx = cp.x; rawAnim.cy = cp.y }
|
if (cp) { rawAnim.cx = cp.x; rawAnim.cy = cp.y }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track elapsed for attack anims
|
|
||||||
if (ANIM_CFG[rawAnim.type]) {
|
|
||||||
if (rawAnim.elapsed !== undefined) rawAnim.elapsed += dt
|
|
||||||
}
|
|
||||||
|
|
||||||
const done = rawAnim.step(dt)
|
const done = rawAnim.step(dt)
|
||||||
if (ANIM_CFG[rawAnim.type] && rawAnim.elapsed >= ANIM_CFG[rawAnim.type].hitAt && !rawAnim.hitFired) {
|
if (ANIM_CFG[rawAnim.type] && rawAnim.elapsed >= ANIM_CFG[rawAnim.type].hitAt && !rawAnim.hitFired) {
|
||||||
rawAnim.hitFired = true
|
rawAnim.hitFired = true
|
||||||
@@ -1083,8 +1140,9 @@ class MoveAction {
|
|||||||
const dir = characterDirsRef.current[cid] ?? { dx: 0, dy: 1 }
|
const dir = characterDirsRef.current[cid] ?? { dx: 0, dy: 1 }
|
||||||
const offset = 5
|
const offset = 5
|
||||||
const spawnPos = { x: pp.x + dir.dx * offset, y: pp.y + dir.dy * offset }
|
const spawnPos = { x: pp.x + dir.dx * offset, y: pp.y + dir.dy * offset }
|
||||||
if (cid === 'player' && magazineRef.current.pistol > 0) {
|
if (cid === 'player' && magazineRef.current.pistol.length > 0) {
|
||||||
const newMag = { pistol: magazineRef.current.pistol - 1 }
|
const bullet = magazineRef.current.pistol[0]
|
||||||
|
const newMag = { pistol: magazineRef.current.pistol.slice(1) }
|
||||||
magazineRef.current = newMag
|
magazineRef.current = newMag
|
||||||
setPlayer(p => ({ ...p, weaponMagazines: { ...p.weaponMagazines, ...newMag } }))
|
setPlayer(p => ({ ...p, weaponMagazines: { ...p.weaponMagazines, ...newMag } }))
|
||||||
const ddx = rawAnim.endPos.x - spawnPos.x
|
const ddx = rawAnim.endPos.x - spawnPos.x
|
||||||
@@ -1098,7 +1156,8 @@ class MoveAction {
|
|||||||
const cos = Math.cos(deviation), sin = Math.sin(deviation)
|
const cos = Math.cos(deviation), sin = Math.sin(deviation)
|
||||||
const spreadDir = { x: dir.x * cos - dir.y * sin, y: dir.x * sin + dir.y * cos }
|
const spreadDir = { x: dir.x * cos - dir.y * sin, y: dir.x * sin + dir.y * cos }
|
||||||
const finalDir = { x: isNaN(spreadDir.x) ? dir.x : spreadDir.x, y: isNaN(spreadDir.y) ? dir.y : spreadDir.y }
|
const finalDir = { x: isNaN(spreadDir.x) ? dir.x : spreadDir.x, y: isNaN(spreadDir.y) ? dir.y : spreadDir.y }
|
||||||
setProjectiles(prev => [...prev, createProjectile(spawnPos, { x: spawnPos.x + finalDir.x * d, y: spawnPos.y + finalDir.y * d }, 'player', 'pistol', 'Shoot')])
|
const ammoType = bullet.ammoType
|
||||||
|
setProjectiles(prev => [...prev, createProjectile(spawnPos, { x: spawnPos.x + finalDir.x * d, y: spawnPos.y + finalDir.y * d }, 'player', 'pistol', 'Shoot', ammoType)])
|
||||||
} else if (cid === 'player') {
|
} else if (cid === 'player') {
|
||||||
setCombat(c => ({ ...c, log: [...c.log, 'Click! The pistol is empty.'] }))
|
setCombat(c => ({ ...c, log: [...c.log, 'Click! The pistol is empty.'] }))
|
||||||
}
|
}
|
||||||
@@ -1116,12 +1175,12 @@ class MoveAction {
|
|||||||
const dist = playerChar ? Math.sqrt(
|
const dist = playerChar ? Math.sqrt(
|
||||||
(playerChar.pos.x - finalPos.x) ** 2 + (playerChar.pos.y - finalPos.y) ** 2
|
(playerChar.pos.x - finalPos.x) ** 2 + (playerChar.pos.y - finalPos.y) ** 2
|
||||||
) : 999
|
) : 999
|
||||||
if (dist <= 15) {
|
|
||||||
const wpn = e.weapon ?? 'fists'
|
const wpn = e.weapon ?? 'fists'
|
||||||
const base = weaponMap[wpn]
|
const base = weaponMap[wpn]
|
||||||
const skill = (base?.skills ?? [{ name: 'Attack', mult: 1.0, damageType: 'blunt' }])[
|
const skill = (base?.skills ?? [{ name: 'Attack', mult: 1.0, damageType: 'blunt', range: 15 }])[
|
||||||
Math.floor(Math.random() * (base?.skills?.length ?? 1))
|
Math.floor(Math.random() * (base?.skills?.length ?? 1))
|
||||||
]
|
]
|
||||||
|
if (dist <= (skill.range ?? 15)) {
|
||||||
const { dmg, severity, injType, injLabel } = computeHit({ weaponId: wpn, skill, weaponConditions: {} })
|
const { dmg, severity, injType, injLabel } = computeHit({ weaponId: wpn, skill, weaponConditions: {} })
|
||||||
const targetPart = randomPart()
|
const targetPart = randomPart()
|
||||||
const action = {
|
const action = {
|
||||||
@@ -1154,7 +1213,18 @@ class MoveAction {
|
|||||||
if (act.type === 'attack' && !characterActionsRef.current[act.charId]) {
|
if (act.type === 'attack' && !characterActionsRef.current[act.charId]) {
|
||||||
characterActionsRef.current[act.charId] = createWeaponStrikeAnim(act, fresh)
|
characterActionsRef.current[act.charId] = createWeaponStrikeAnim(act, fresh)
|
||||||
} else if (act.type === 'move' && !characterActionsRef.current[act.charId]) {
|
} else if (act.type === 'move' && !characterActionsRef.current[act.charId]) {
|
||||||
characterActionsRef.current[act.charId] = new MoveAction({
|
const dx = act.toX - act.fromX, dy = act.toY - act.fromY
|
||||||
|
const d = Math.sqrt(dx * dx + dy * dy)
|
||||||
|
const dir = d > 0 ? { dx: dx / d, dy: dy / d } : { dx: 0, dy: 1 }
|
||||||
|
characterActionsRef.current[act.charId] = new SeqAction({
|
||||||
|
type: 'move',
|
||||||
|
actions: [
|
||||||
|
new TurnAction({
|
||||||
|
targetDir: dir,
|
||||||
|
getDir: () => characterDirsRef.current[act.charId] ?? { dx: 0, dy: 1 },
|
||||||
|
setDir: d2 => { characterDirsRef.current[act.charId] = d2 }
|
||||||
|
}),
|
||||||
|
new MoveAction({
|
||||||
...act,
|
...act,
|
||||||
setPos: p => {
|
setPos: p => {
|
||||||
setCombat(c => ({
|
setCombat(c => ({
|
||||||
@@ -1164,6 +1234,8 @@ class MoveAction {
|
|||||||
characterPositionsRef.current[act.charId] = p
|
characterPositionsRef.current[act.charId] = p
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
]
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1174,15 +1246,6 @@ class MoveAction {
|
|||||||
// ── Update rendering state for active animations ──
|
// ── Update rendering state for active animations ──
|
||||||
setAnimations(activeAnims)
|
setAnimations(activeAnims)
|
||||||
|
|
||||||
// ── Auto-stop timer when ALL actions complete ──
|
|
||||||
const anyBusy = Object.values(characterActionsRef.current).some(a => a !== null)
|
|
||||||
if (wasBusyRef.current && !anyBusy) {
|
|
||||||
timerActiveRef.current = false; setTimerRunning(false)
|
|
||||||
}
|
|
||||||
wasBusyRef.current = anyBusy
|
|
||||||
setBusy(anyBusy)
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rafId = requestAnimationFrame(tick)
|
rafId = requestAnimationFrame(tick)
|
||||||
@@ -1217,17 +1280,6 @@ class MoveAction {
|
|||||||
// KEYBINDS
|
// KEYBINDS
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleKeyDown = (e) => {
|
|
||||||
if (e.code === 'Space' && combat) {
|
|
||||||
e.preventDefault()
|
|
||||||
toggleTimer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window.addEventListener('keydown', handleKeyDown)
|
|
||||||
return () => window.removeEventListener('keydown', handleKeyDown)
|
|
||||||
}, [combat])
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
// SNAPSHOT (recording)
|
// SNAPSHOT (recording)
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
@@ -1369,7 +1421,7 @@ class MoveAction {
|
|||||||
log: [`You engage ${enemies.map(e => e.name).join(' and ')} in combat!`],
|
log: [`You engage ${enemies.map(e => e.name).join(' and ')} in combat!`],
|
||||||
}
|
}
|
||||||
setCombat(initCombat)
|
setCombat(initCombat)
|
||||||
timerActiveRef.current = false; setTimerRunning(false); setBusy(false)
|
stopTimer(); setBusy(false); prevBusyRef.current = false
|
||||||
setCombatTime(0); setDisplayTime(0)
|
setCombatTime(0); setDisplayTime(0)
|
||||||
characterActionsRef.current = {}
|
characterActionsRef.current = {}
|
||||||
characterPositionsRef.current = {}
|
characterPositionsRef.current = {}
|
||||||
@@ -1392,7 +1444,7 @@ class MoveAction {
|
|||||||
|
|
||||||
const endFight = () => {
|
const endFight = () => {
|
||||||
if (recordingRef.current) { pushSnapshot(); setFightRecord(recordingRef.current); recordingRef.current = null }
|
if (recordingRef.current) { pushSnapshot(); setFightRecord(recordingRef.current); recordingRef.current = null }
|
||||||
timerActiveRef.current = false; setTimerRunning(false)
|
stopTimer()
|
||||||
setCombatTime(0); setDisplayTime(0)
|
setCombatTime(0); setDisplayTime(0)
|
||||||
characterActionsRef.current = {}
|
characterActionsRef.current = {}
|
||||||
setProjectiles([])
|
setProjectiles([])
|
||||||
@@ -1404,17 +1456,29 @@ class MoveAction {
|
|||||||
|
|
||||||
/** Execute a player attack; queue it for execution. */
|
/** Execute a player attack; queue it for execution. */
|
||||||
const executeAttack = () => {
|
const executeAttack = () => {
|
||||||
if (!limbSelect || !selectedTarget) return
|
if (!limbSelect || !selectedTarget) {
|
||||||
|
setCombat(c => ({ ...c, log: [...c.log, `DBG: early return limbSelect=${limbSelect} target=${selectedTarget}`] }))
|
||||||
|
return
|
||||||
|
}
|
||||||
const cc = combatRef.current
|
const cc = combatRef.current
|
||||||
if (!cc) return
|
if (!cc) { setCombat(c => ({ ...c, log: [...c.log, 'DBG: no combat'] })); return }
|
||||||
if (isDead(playerRef.current.integrity)) return
|
if (isDead(playerRef.current.integrity)) { setCombat(c => ({ ...c, log: [...c.log, 'DBG: player dead'] })); return }
|
||||||
const ei = attackEnemy ?? selectedEnemy
|
const ei = attackEnemy ?? selectedEnemy
|
||||||
const enemyId = cc.enemyIds[ei]
|
const enemyId = cc.enemyIds[ei]
|
||||||
const enemy = cc.characters[enemyId]
|
const enemy = cc.characters[enemyId]
|
||||||
if (!enemy || isDead(enemy.integrity)) return
|
if (!enemy || isDead(enemy.integrity)) {
|
||||||
|
setCombat(c => ({ ...c, log: [...c.log, `DBG: bad enemy ei=${ei} id=${enemyId} enemy=${!!enemy} dead=${enemy ? isDead(enemy.integrity) : '?'}`] }))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const weaponId = limbSelect
|
const weaponId = limbSelect
|
||||||
if (!canReach(cc.characters['player'].pos, enemy.pos, weaponId, selectedSkill)) return
|
if (!canReach(cc.characters['player'].pos, enemy.pos, weaponId, selectedSkill)) {
|
||||||
|
const d = Math.sqrt(
|
||||||
|
(cc.characters['player'].pos.x - enemy.pos.x)**2 + (cc.characters['player'].pos.y - enemy.pos.y)**2
|
||||||
|
)
|
||||||
|
setCombat(c => ({ ...c, log: [...c.log, `Too far (${d.toFixed(0)} > ${getSkillRange(weaponId, selectedSkill)})`] }))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const action = buildPlayerAttack({
|
const action = buildPlayerAttack({
|
||||||
weaponId, skillName: selectedSkill, targetPart: selectedTarget,
|
weaponId, skillName: selectedSkill, targetPart: selectedTarget,
|
||||||
@@ -1426,7 +1490,6 @@ class MoveAction {
|
|||||||
const enemy2 = cc.characters[action.enemyCharId]
|
const enemy2 = cc.characters[action.enemyCharId]
|
||||||
if (!enemy2 || isDead(enemy2.integrity)) return
|
if (!enemy2 || isDead(enemy2.integrity)) return
|
||||||
const { pos, actualPush } = computeShove(cc.characters['player'].pos, enemy2.pos)
|
const { pos, actualPush } = computeShove(cc.characters['player'].pos, enemy2.pos)
|
||||||
if (!timerActiveRef.current) { timerActiveRef.current = true; setTimerRunning(true) }
|
|
||||||
setCombat(c => ({
|
setCombat(c => ({
|
||||||
...c,
|
...c,
|
||||||
characters: { ...c.characters, [action.enemyCharId]: { ...enemy2, pos } },
|
characters: { ...c.characters, [action.enemyCharId]: { ...enemy2, pos } },
|
||||||
@@ -1436,7 +1499,6 @@ class MoveAction {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!timerActiveRef.current) { timerActiveRef.current = true; setTimerRunning(true) }
|
|
||||||
const anim = createWeaponStrikeAnim(action, combatRef.current)
|
const anim = createWeaponStrikeAnim(action, combatRef.current)
|
||||||
characterActionsRef.current['player'] = anim
|
characterActionsRef.current['player'] = anim
|
||||||
setLimbSelect(null); setSelectedSkill(null); setSelectedTarget(null); setAttackEnemy(null)
|
setLimbSelect(null); setSelectedSkill(null); setSelectedTarget(null); setAttackEnemy(null)
|
||||||
@@ -1447,17 +1509,29 @@ class MoveAction {
|
|||||||
setAimMode(false)
|
setAimMode(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const startTimer = () => {
|
||||||
|
if (!timerActiveRef.current) {
|
||||||
|
timerActiveRef.current = true
|
||||||
|
setTimerRunning(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const stopTimer = () => {
|
||||||
|
timerActiveRef.current = false
|
||||||
|
setTimerRunning(false)
|
||||||
|
}
|
||||||
const toggleTimer = () => {
|
const toggleTimer = () => {
|
||||||
timerActiveRef.current = !timerActiveRef.current
|
if (timerActiveRef.current) { stopTimer() } else { startTimer() }
|
||||||
setTimerRunning(timerActiveRef.current)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Escape cancels aim mode
|
// Spacebar toggles timer, Escape cancels aim mode
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onKey = (e) => { if (e.key === 'Escape' && aimMode) { setAimMode(false); setSelectedSkill(null) } }
|
const onKey = (e) => {
|
||||||
|
if (e.code === 'Space' && combat) { e.preventDefault(); toggleTimer() }
|
||||||
|
if (e.key === 'Escape' && aimMode) { setAimMode(false); setSelectedSkill(null) }
|
||||||
|
}
|
||||||
window.addEventListener('keydown', onKey)
|
window.addEventListener('keydown', onKey)
|
||||||
return () => window.removeEventListener('keydown', onKey)
|
return () => window.removeEventListener('keydown', onKey)
|
||||||
}, [aimMode])
|
}, [aimMode, combat])
|
||||||
|
|
||||||
/** Execute a player move; queue it for execution. */
|
/** Execute a player move; queue it for execution. */
|
||||||
const startMove = (targetX, targetY) => {
|
const startMove = (targetX, targetY) => {
|
||||||
@@ -1465,16 +1539,15 @@ class MoveAction {
|
|||||||
const curPos = characterPositionsRef.current['player'] ?? combat.characters['player'].pos
|
const curPos = characterPositionsRef.current['player'] ?? combat.characters['player'].pos
|
||||||
const dist = Math.sqrt((targetX - curPos.x) ** 2 + (targetY - curPos.y) ** 2)
|
const dist = Math.sqrt((targetX - curPos.x) ** 2 + (targetY - curPos.y) ** 2)
|
||||||
if (dist < 1) return
|
if (dist < 1) return
|
||||||
const isRun = moveMode === 'run'
|
const maxSpd = playerRef.current.maxSpeed ?? 10
|
||||||
const speed = isRun ? 1.5 : 1
|
const pct = moveMode === 'run' ? 1.0 : 0.5
|
||||||
if (!timerActiveRef.current) { timerActiveRef.current = true; setTimerRunning(true) }
|
const speed = maxSpd * pct
|
||||||
const moveAction = new MoveAction({ type: 'move', fromX: curPos.x, fromY: curPos.y, toX: targetX, toY: targetY, speed,
|
const moveAction = new MoveAction({ type: 'move', fromX: curPos.x, fromY: curPos.y, toX: targetX, toY: targetY, speed,
|
||||||
setPos: p => {
|
setPos: p => {
|
||||||
setCombat(c => ({ ...c, characters: { ...c.characters, player: { ...c.characters.player, pos: { ...p } } } }))
|
setCombat(c => ({ ...c, characters: { ...c.characters, player: { ...c.characters.player, pos: { ...p } } } }))
|
||||||
characterPositionsRef.current['player'] = p
|
characterPositionsRef.current['player'] = p
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (isRun) {
|
|
||||||
const dx = targetX - curPos.x, dy = targetY - curPos.y
|
const dx = targetX - curPos.x, dy = targetY - curPos.y
|
||||||
const d = Math.sqrt(dx * dx + dy * dy)
|
const d = Math.sqrt(dx * dx + dy * dy)
|
||||||
const dir = d > 0 ? { dx: dx / d, dy: dy / d } : { dx: 0, dy: 1 }
|
const dir = d > 0 ? { dx: dx / d, dy: dy / d } : { dx: 0, dy: 1 }
|
||||||
@@ -1484,37 +1557,46 @@ class MoveAction {
|
|||||||
moveAction
|
moveAction
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
} else {
|
|
||||||
characterActionsRef.current['player'] = moveAction
|
|
||||||
}
|
|
||||||
setMoveMode(null)
|
setMoveMode(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const reloadPistol = () => {
|
/** Show ammo selection UI based on what's in inventory */
|
||||||
|
const startReload = () => {
|
||||||
if (!combat) return
|
if (!combat) return
|
||||||
const cur = magazineRef.current.pistol
|
const cur = magazineRef.current.pistol.length
|
||||||
if (cur >= PISTOL_MAG) {
|
if (cur >= PISTOL_MAG) {
|
||||||
setCombat(c => ({ ...c, log: [...c.log, 'The pistol is already fully loaded.'] }))
|
setCombat(c => ({ ...c, log: [...c.log, 'The pistol is already fully loaded.'] }))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const reserve = playerRef.current.itemCounts?.pistol_ammo ?? 0
|
const available = Object.entries(playerRef.current.itemCounts ?? {})
|
||||||
if (reserve <= 0) {
|
.filter(([id, qty]) => qty > 0 && (id === 'pistol_ammo' || id === 'bouncing_ammo'))
|
||||||
setCombat(c => ({ ...c, log: [...c.log, 'No pistol ammo left!'] }))
|
if (available.length === 0) {
|
||||||
|
setCombat(c => ({ ...c, log: [...c.log, 'No ammo in inventory!'] }))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const newMag = { pistol: cur + 1 }
|
setSelectedSkill('Reload'); setAttackEnemy(null); setSelectedTarget(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Load one round of the given ammo type into the weapon */
|
||||||
|
const loadAmmoType = (ammoId) => {
|
||||||
|
const cur = magazineRef.current.pistol.length
|
||||||
|
const reserve = playerRef.current.itemCounts?.[ammoId] ?? 0
|
||||||
|
if (reserve <= 0) return
|
||||||
|
const newMag = { pistol: [...magazineRef.current.pistol, { ammoType: ammoId }] }
|
||||||
magazineRef.current = newMag
|
magazineRef.current = newMag
|
||||||
|
const ammoName = itemMap[ammoId]?.name ?? ammoId
|
||||||
setPlayer(p => {
|
setPlayer(p => {
|
||||||
const newCount = p.itemCounts.pistol_ammo - 1
|
const newCount = (p.itemCounts[ammoId] ?? 0) - 1
|
||||||
const counts = { ...p.itemCounts, pistol_ammo: newCount }
|
const counts = { ...p.itemCounts, [ammoId]: newCount }
|
||||||
if (newCount <= 0) delete counts.pistol_ammo
|
if (newCount <= 0) delete counts[ammoId]
|
||||||
return {
|
return {
|
||||||
...p,
|
...p,
|
||||||
weaponMagazines: { ...p.weaponMagazines, ...newMag },
|
weaponMagazines: { ...p.weaponMagazines, ...newMag },
|
||||||
itemCounts: counts,
|
itemCounts: counts,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
setCombat(c => ({ ...c, log: [...c.log, 'Reloaded 1 round into the pistol.'] }))
|
setCombat(c => ({ ...c, log: [...c.log, `Loaded 1 round of ${ammoName} into the pistol.`] }))
|
||||||
|
setLimbSelect(null); setSelectedSkill(null); setSelectedTarget(null); setAttackEnemy(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
@@ -1572,12 +1654,13 @@ class MoveAction {
|
|||||||
const buyItem = (item) => {
|
const buyItem = (item) => {
|
||||||
if (player.money < item.price) return
|
if (player.money < item.price) return
|
||||||
if (item.weaponId) { setBuyArmChoice({ item }) }
|
if (item.weaponId) { setBuyArmChoice({ item }) }
|
||||||
else if (item.itemId === 'pistol_ammo') {
|
else if (item.itemId === 'pistol_ammo' || item.itemId === 'bouncing_ammo') {
|
||||||
|
const id = item.itemId
|
||||||
setPlayer(p => ({
|
setPlayer(p => ({
|
||||||
...p,
|
...p,
|
||||||
money: p.money - item.price,
|
money: p.money - item.price,
|
||||||
inventory: p.inventory.includes('pistol_ammo') ? p.inventory : [...p.inventory, 'pistol_ammo'],
|
inventory: p.inventory.includes(id) ? p.inventory : [...p.inventory, id],
|
||||||
itemCounts: { ...p.itemCounts, pistol_ammo: (p.itemCounts?.pistol_ammo ?? 0) + 1 },
|
itemCounts: { ...p.itemCounts, [id]: (p.itemCounts?.[id] ?? 0) + 1 },
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
else { setPlayer(p => ({ ...p, money: p.money - item.price, inventory: [...p.inventory, item.itemId] })) }
|
else { setPlayer(p => ({ ...p, money: p.money - item.price, inventory: [...p.inventory, item.itemId] })) }
|
||||||
@@ -1735,11 +1818,17 @@ class MoveAction {
|
|||||||
<div className="tree-children">
|
<div className="tree-children">
|
||||||
{player.inventory.length === 0
|
{player.inventory.length === 0
|
||||||
? <div className="move-hint">No items</div>
|
? <div className="move-hint">No items</div>
|
||||||
: player.inventory.map((itemId, i) => (
|
: player.inventory.map((itemId, i) => {
|
||||||
|
const qty = player.itemCounts?.[itemId]
|
||||||
|
if (qty !== undefined && qty <= 0) return null
|
||||||
|
return (
|
||||||
<div key={i} className="tree-node leaf" onClick={() => { setShowInventory(true); setPanelTarget('player') }}>
|
<div key={i} className="tree-node leaf" onClick={() => { setShowInventory(true); setPanelTarget('player') }}>
|
||||||
<span className="tree-bullet">○</span><span className="tree-label">{itemMap[itemId]?.name ?? itemId}</span>
|
<span className="tree-bullet">○</span>
|
||||||
|
<span className="tree-label">{itemMap[itemId]?.name ?? itemId}</span>
|
||||||
|
{qty !== undefined && <span style={{ marginLeft: 6, color: '#9ca3af', fontSize: 12 }}>×{qty}</span>}
|
||||||
</div>
|
</div>
|
||||||
))
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -1821,7 +1910,6 @@ class MoveAction {
|
|||||||
const dy = ty - pp.y
|
const dy = ty - pp.y
|
||||||
const d = Math.sqrt(dx * dx + dy * dy)
|
const d = Math.sqrt(dx * dx + dy * dy)
|
||||||
if (d > 0) {
|
if (d > 0) {
|
||||||
if (!timerActiveRef.current) { timerActiveRef.current = true; setTimerRunning(true) }
|
|
||||||
characterActionsRef.current['player'] = new TurnAction({ targetDir: { dx: dx / d, dy: dy / d }, getDir: () => characterDirsRef.current['player'] ?? { dx: 0, dy: 1 }, setDir: d => { characterDirsRef.current['player'] = d } })
|
characterActionsRef.current['player'] = new TurnAction({ targetDir: { dx: dx / d, dy: dy / d }, getDir: () => characterDirsRef.current['player'] ?? { dx: 0, dy: 1 }, setDir: d => { characterDirsRef.current['player'] = d } })
|
||||||
setMoveMode(null)
|
setMoveMode(null)
|
||||||
}
|
}
|
||||||
@@ -2014,7 +2102,7 @@ class MoveAction {
|
|||||||
<div style={{ position: 'relative', minHeight: 520 }}>
|
<div style={{ position: 'relative', minHeight: 520 }}>
|
||||||
<div className="inv-parts-title limb-select-title" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<div className="inv-parts-title limb-select-title" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<span>{weaponName}</span>
|
<span>{weaponName}</span>
|
||||||
{limbSelect === 'pistol' && <span style={{ fontSize: 13, color: '#9ca3af', fontWeight: 'normal' }}>Ammo: {player.weaponMagazines.pistol}/{PISTOL_MAG}</span>}
|
{limbSelect === 'pistol' && <span style={{ fontSize: 13, color: '#9ca3af', fontWeight: 'normal' }}>Ammo: {player.weaponMagazines.pistol?.length ?? 0}/{PISTOL_MAG} <span style={{ color: '#fbbf24' }}>({itemMap[player.weaponMagazines.pistol?.[0]?.ammoType]?.name ?? 'none'})</span></span>}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', gap: 16, alignItems: 'flex-start', paddingRight: 250 }}>
|
<div style={{ display: 'flex', gap: 16, alignItems: 'flex-start', paddingRight: 250 }}>
|
||||||
{/* skill list */}
|
{/* skill list */}
|
||||||
@@ -2025,7 +2113,7 @@ class MoveAction {
|
|||||||
{skills.map((skill, i) => (
|
{skills.map((skill, i) => (
|
||||||
<button key={i} className={'limb-target-btn' + (selectedSkill === skill.name ? ' limb-target-selected' : '')}
|
<button key={i} className={'limb-target-btn' + (selectedSkill === skill.name ? ' limb-target-selected' : '')}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (skill.name === 'Reload') { reloadPistol(); setLimbSelect(null); setSelectedSkill(null); return }
|
if (skill.name === 'Reload') { startReload(); return }
|
||||||
setSelectedSkill(skill.name); setAttackEnemy(null)
|
setSelectedSkill(skill.name); setAttackEnemy(null)
|
||||||
if (limbSelect === 'pistol') { setAimMode(true); setLimbSelect(null) }
|
if (limbSelect === 'pistol') { setAimMode(true); setLimbSelect(null) }
|
||||||
}}
|
}}
|
||||||
@@ -2036,8 +2124,32 @@ class MoveAction {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* enemy list */}
|
{/* reload: ammo selection */}
|
||||||
{selectedSkill && (
|
{selectedSkill === 'Reload' && (
|
||||||
|
<div>
|
||||||
|
<div className="inv-parts-title" style={{ marginBottom: 8 }}>Select Ammo Type</div>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||||
|
{Object.entries(playerRef.current.itemCounts ?? {})
|
||||||
|
.filter(([id, qty]) => qty > 0 && (id === 'pistol_ammo' || id === 'bouncing_ammo'))
|
||||||
|
.length === 0
|
||||||
|
? <div style={{ color: '#6b7280', padding: 12 }}>No ammo available</div>
|
||||||
|
: Object.entries(playerRef.current.itemCounts ?? {})
|
||||||
|
.filter(([id, qty]) => qty > 0 && (id === 'pistol_ammo' || id === 'bouncing_ammo'))
|
||||||
|
.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 }}
|
||||||
|
onMouseEnter={e => e.currentTarget.style.background = '#1e1e30'}
|
||||||
|
onMouseLeave={e => e.currentTarget.style.background = 'none'}
|
||||||
|
onClick={() => loadAmmoType(id)}>
|
||||||
|
<span style={{ fontWeight: 'bold' }}>{itemMap[id]?.name ?? id}</span>
|
||||||
|
<span style={{ color: '#9ca3af' }}>×{qty}</span>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* enemy list (hidden during reload) */}
|
||||||
|
{selectedSkill && selectedSkill !== 'Reload' && (
|
||||||
<div>
|
<div>
|
||||||
<div className="inv-parts-title" style={{ marginBottom: 8 }}>Select Enemy</div>
|
<div className="inv-parts-title" style={{ marginBottom: 8 }}>Select Enemy</div>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||||
@@ -2056,8 +2168,8 @@ class MoveAction {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* limb selector */}
|
{/* limb selector (hidden during reload) */}
|
||||||
{attackEnemy !== null && (
|
{selectedSkill !== 'Reload' && attackEnemy !== null && (
|
||||||
<div>
|
<div>
|
||||||
<div className="inv-parts-title" style={{ marginBottom: 8 }}>Limb</div>
|
<div className="inv-parts-title" style={{ marginBottom: 8 }}>Limb</div>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user