From 02608ee44e072a677e74f6253c2db25c13b79ac6 Mon Sep 17 00:00:00 2001 From: johnruina Date: Sun, 28 Jun 2026 21:21:24 -0400 Subject: [PATCH] i dunno --- splat.png | Bin 0 -> 37907 bytes src/App.jsx | 1801 +++++++++++++++++++++--------------------- src/assets/splat.png | Bin 0 -> 37907 bytes 3 files changed, 880 insertions(+), 921 deletions(-) create mode 100644 splat.png create mode 100644 src/assets/splat.png diff --git a/splat.png b/splat.png new file mode 100644 index 0000000000000000000000000000000000000000..39cb7aa26aabfd965517ce8c4d340c3ef56f6b6a GIT binary patch literal 37907 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 literal 0 HcmV?d00001 diff --git a/src/App.jsx b/src/App.jsx index b7453bf..044a7fa 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,6 +1,7 @@ import { useState, useRef, useCallback, useEffect, useLayoutEffect, Fragment } from 'react' import './App.css' import maleBody from './assets/body/malebody.png' +import splatImg from './assets/splat.png' import pistolShotMp3 from './assets/pistolshot.mp3' import chamberBulletMp3 from './assets/chamberbullet.mp3' import names from './names.json' @@ -79,10 +80,30 @@ function generateWorld() { relationships: { b1: 'enemy' }, size: 1, }) + // Arena NPCs for Place (loc6) — 10 left vs 10 right, all enemies cross-team + const placeLeftIds = Array.from({ length: 10 }, (_, i) => `place_l${i}`) + const placeRightIds = Array.from({ length: 10 }, (_, i) => `place_r${i}`) + const placeWeapons = ['rusty_sword', 'dagger', 'steel_sword', 'warhammer', 'iron_shield'] + for (let i = 0; i < 10; i++) { + const wpn = placeWeapons[i % placeWeapons.length] + const relsL = {}; for (const rid of placeRightIds) relsL[rid] = 'enemy' + characters.push({ + id: `place_l${i}`, name: `Attacker ${i + 1}`, location: 'loc6', + inventory: [wpn], equipped: { left: wpn, right: 'fists' }, + relationships: relsL, size: 1, fillColor: `hsl(220, 70%, 50%)`, + }) + const relsR = {}; for (const lid of placeLeftIds) relsR[lid] = 'enemy' + characters.push({ + id: `place_r${i}`, name: `Defender ${i + 1}`, location: 'loc6', + inventory: [wpn], equipped: { left: wpn, right: 'fists' }, + relationships: relsR, size: 1, fillColor: `hsl(0, 70%, 50%)`, + }) + } + // Generic NPCs for the remaining locations let npcId = 0 for (let i = 0; i < 7; i++) { - if (i === zephyrIdx || i === fightIdx) continue + if (i === zephyrIdx || i === fightIdx || i === 6) continue characters.push({ id: `npc${npcId}`, location: `loc${i}`, money: 50 + Math.floor(Math.random() * 200), @@ -717,9 +738,9 @@ export default function App() { const [weather, setWeather] = useState('clear') const [defeatedNpcs, setDefeatedNpcs] = useState([]) const [characterOverrides, setCharacterOverrides] = useState({}) + const characterOverridesRef = useRef({}); characterOverridesRef.current = JSON.parse(JSON.stringify(characterOverrides)) const worldItemsRef = useRef({}) const groundItemsRef = useRef([]) - const savedMapsRef = useRef({}) const liveCharacters = characters.map(c => ({ ...c, ...characterOverrides[c.id] })).filter(c => !defeatedNpcs.includes(c.id)) // ── ui state @@ -745,6 +766,7 @@ export default function App() { const [passTimeHours, setPassTimeHours] = useState(1) const [skipAnim, setSkipAnim] = useState(null) const [zoomAnim, setZoomAnim] = useState(null) + const [minimapZoomAnim, setMinimapZoomAnim] = useState(null) // ── combat state @@ -777,6 +799,11 @@ export default function App() { const [hitShake, setHitShake] = useState(null) const [minimapView, setMinimapView] = useState({ x: 0, y: 0, scale: 1 }) + // ── camera target: which active location to render in the minimap + // null = follow current (player) location, otherwise a background locId + const [viewingLoc, setViewingLoc] = useState(null) + const [hoveredLocName, setHoveredLocName] = useState(null) + // ── ui busy (mirrors ref-based action state for re-renders) const [busy, setBusy] = useState(false) @@ -789,6 +816,9 @@ export default function App() { // ── projectiles (modular projectile framework) const [projectiles, setProjectiles] = useState([]) + // ── blood splats + const [bloodSplats, setBloodSplats] = useState([]) + // ── ground items const [groundItems, setGroundItems] = useState([]) @@ -826,17 +856,19 @@ export default function App() { const [speedMult, setSpeedMult] = useState(1) const playerRef = useRef(null); playerRef.current = player const magazineRef = useRef({ pistol: [{ ammoType: 'pistol_ammo' }, { ammoType: 'pistol_ammo' }] }); magazineRef.current = player.weaponMagazines - const deadThisTickRef = useRef(new Set()) const debugAbilitiesRef = useRef([]) const pocketReturnRef = useRef(null) - const pocketNPCsRef = useRef({}) // { npcId: originalLocation } - const pocketNPCDataRef = useRef({}) // { npcId: snapshotCharacterData } - const pocketDimIdRef = useRef(null) // stable pocket dimension ID + + // ── Multi-location system ── + // Track all known locations and which are actively processing each frame. + // The pocket dimension is a regular entry in the active locations system. + const activeLocationsRef = useRef([]) // [locId, ...] actively processed each frame + const backgroundDataRef = useRef({}) // { [locId]: { blocks, worldSize, groundItems, projectiles } } + const backgroundRefsRef = useRef({}) // { [locId]: { actions: {}, dirs: {}, positions: {}, velocities: {}, deadThisTick: Set } } // ── Universal character action/direction/position refs ── - // Every character (player, enemies) uses the same ref structure. - // The player entity ID is combat.playerEntityId. - // Character IDs: 'player', 'enemy_0', 'enemy_1', ... + // These hold the CURRENT/VISIBLE location's data (player's location). + // Background active locations use backgroundRefsRef instead. const characterActionsRef = useRef({}) // { [charId]: action } const characterDirsRef = useRef({}) // { [charId]: {dx,dy} } const characterPositionsRef = useRef({}) // { [charId]: {x,y} } @@ -861,6 +893,432 @@ export default function App() { const pannedKeysRef = useRef(new Set()) const panRafRef = useRef(null) + // ── Pre-create pocket dimension as a non-active background location ── + const POCKET_DIM_ID = 'pocket_dim' + backgroundDataRef.current[POCKET_DIM_ID] = { + blocks: [{ x: 130, y: 130, w: 60, h: 60 }], + worldSize: 320, + groundItems: [], + projectiles: [], + splats: [], + } + const pocketDimBgRefs = ensureBgRefs(POCKET_DIM_ID) + + // ── Background location helpers ── + // Ensure a background location has its ref namespace initialised. + function ensureBgRefs(locId) { + if (!backgroundRefsRef.current[locId]) backgroundRefsRef.current[locId] = { actions: {}, dirs: {}, positions: {}, velocities: {}, deadThisTick: new Set() } + return backgroundRefsRef.current[locId] + } + + // ── Background hit/damage helpers (mutate backgroundData directly) ── + function applyBackgroundHit(anim, chars, data, refs) { + const attackerId = anim.charId + const targetId = anim.targetCharId + if (!targetId) return + const attackerPos = refs.positions[attackerId] ?? chars[attackerId]?.pos + const targetPos = refs.positions[targetId] ?? chars[targetId]?.pos + if (!attackerPos || !targetPos) return + const inRange = canReach(attackerPos, targetPos, anim.weaponId, anim.skillName) + if (!inRange) return + const target = chars[targetId] + if (!target) return + const decayedTarget = calcIntegrity(target.integrity, target.injuries, ANIM_CFG[anim.type]?.hitAt ?? 0.5) + const prevHp = decayedTarget[anim.targetPart] + const hitTarget = propagateDestruction({ ...decayedTarget, [anim.targetPart]: Math.max(0, prevHp - (anim.dmg ?? 0)) }) + const newInjuries = prevHp > 0 && (anim.severity ?? 0) > 0 + ? applyInjury(target.injuries, anim.targetPart, anim.injType ?? 'blunt', anim.severity ?? 0) + : target.injuries + chars[targetId] = { ...target, integrity: hitTarget, injuries: newInjuries } + if (isDead(hitTarget)) { + refs.deadThisTick.add(targetId) + } + const dx = targetPos.x - attackerPos.x, dy = targetPos.y - attackerPos.y + const d = Math.sqrt(dx * dx + dy * dy) || 1 + refs.velocities[targetId] = { x: (dx / d) * 80, y: (dy / d) * 80 } + if (!data.splats) data.splats = [] + data.splats.push({ x: targetPos.x, y: targetPos.y, rot: Math.random() * 360 }) + } + + function applyBackgroundProjectileHit(proj, charId, chars, data, refs) { + proj.hitFired = true + if (!charId) return + const target = chars[charId] + if (!target || isDead(target.integrity)) return + const skill = weaponMap.pistol?.skills?.[0] + if (!skill) return + const { dmg, severity, injType } = computeHit({ weaponId: 'pistol', skill, weaponConditions: {}, bothMult: 1.0 }) + const part = ALL_PARTS[Math.floor(Math.random() * ALL_PARTS.length)] + const integrity = { ...target.integrity } + const injuries = { ...(target.injuries ?? freshInjuries()) } + integrity[part] = Math.max(0, (integrity[part] ?? 100) - dmg) + Object.assign(integrity, propagateDestruction(integrity)) + if (integrity[part] > 0 && severity > 0) { + injuries[part] = [...(injuries[part] ?? []), { type: injType, severity }] + } + chars[charId] = { ...target, integrity, injuries } + if (isDead(integrity)) { + refs.deadThisTick.add(charId) + } + refs.velocities[charId] = { x: proj.dir.x * 80, y: proj.dir.y * 80 } + if (!data.splats) data.splats = [] + data.splats.push({ x: proj.pos.x, y: proj.pos.y, rot: Math.random() * 360 }) + } + + // Process one frame of ALL game logic for a background (non-visible) location. + // Mirrors the visible location tick but uses backgroundDataRef + backgroundRefsRef. + function tickBackgroundLocation(locId, dt) { + const data = backgroundDataRef.current[locId] + if (!data) return + const refs = ensureBgRefs(locId) + const positions = refs.positions + const velocities = refs.velocities + const dirs = refs.dirs + const actions = refs.actions + const deadThisTick = refs.deadThisTick; deadThisTick.clear() + + // Build character map from global state (characters store their location) + const chars = {} + const isPlayerLoc = locId === playerRef.current?.location + for (const c of characters) { + const override = characterOverridesRef.current[c.id] + const merged = override ? { ...c, ...override } : c + if (merged.location !== locId) continue + const integrity = merged.integrity ?? freshBody() + const pos = positions[c.id] ?? merged.pos ?? { x: 20, y: 75 } + const inj = merged.injuries ? JSON.parse(JSON.stringify(merged.injuries)) : freshInjuries() + chars[c.id] = { ...merged, integrity: { ...integrity }, injuries: inj, pos: { ...pos } } + } + if (isPlayerLoc) { + if (!positions['player']) { + const visPos = characterPositionsRef.current['player'] + if (visPos) positions['player'] = { ...visPos } + else positions['player'] = { x: 160, y: 160 } + } + const pd = playerRef.current + chars['player'] = { + id: 'player', name: pd.name, + pos: { ...positions['player'] }, + integrity: { ...pd.integrity }, + injuries: JSON.parse(JSON.stringify(pd.injuries ?? freshInjuries())), + equipped: { ...pd.equipped }, + weaponConditions: { ...pd.weaponConditions }, + weaponMagazines: { ...pd.weaponMagazines }, + itemCounts: { ...pd.itemCounts }, + relationships: { ...(pd.relationships ?? {}) }, + owner: 'player', fillColor: pd.fillColor, + } + } + + const blk = data.blocks ?? [] + const wSize = data.worldSize ?? 160 + const wMax = wSize - 15 + + // Build fresh snapshot with live positions + const fresh = (() => { + const fc = {} + for (const [cid, c] of Object.entries(chars)) { + const pos = positions[cid] ?? c.pos + fc[cid] = { ...c, pos: { ...pos } } + } + return { blocks: blk, worldSize: wSize, characters: fc } + })() + + // Tick all character actions + for (const cid of Object.keys(chars)) { + if (isDead(chars[cid]?.integrity) || isUnconscious(chars[cid]?.integrity) || deadThisTick.has(cid)) { + if (actions[cid]) actions[cid] = null + continue + } + const rawAnim = actions[cid] + if (!rawAnim) continue + + const done = rawAnim.step(dt) + if (ANIM_CFG[rawAnim.type] && rawAnim.elapsed >= ANIM_CFG[rawAnim.type].hitAt && !rawAnim.hitFired) { + rawAnim.hitFired = true + if (rawAnim.type === 'shoot') { + const pp = positions[cid] ?? chars[cid]?.pos + const dir = dirs[cid] ?? { dx: 0, dy: 1 } + const offset = 5 + const spawnPos = { x: pp.x + dir.dx * offset, y: pp.y + dir.dy * offset } + const actor = cid === 'player' ? 'player' : 'party' + const charMag = chars[cid]?.weaponMagazines ?? {} + if (charMag.pistol?.length > 0) { + const bullet = charMag.pistol[0] + const newMag = { pistol: charMag.pistol.slice(1) } + chars[cid] = { ...chars[cid], weaponMagazines: { ...(chars[cid]?.weaponMagazines ?? {}), ...newMag } } + const ddx = rawAnim.endPos.x - spawnPos.x + const ddy = rawAnim.endPos.y - spawnPos.y + const d = Math.sqrt(ddx * ddx + ddy * ddy) || 1 + const dir2 = { x: ddx / d, y: ddy / d } + const facing = dirs[cid] ?? { dx: 0, dy: 1 } + const dot = Math.max(-1, Math.min(1, facing.dx * dir2.x + facing.dy * dir2.y)) + const maxDeviation = (1 - dot) / 2 * (Math.PI / 4) + const deviation = (Math.random() * 2 - 1) * maxDeviation + const cos = Math.cos(deviation); const sin = Math.sin(deviation) + const spreadDir = { x: dir2.x * cos - dir2.y * sin, y: dir2.x * sin + dir2.y * cos } + const finalDir = { x: isNaN(spreadDir.x) ? dir2.x : spreadDir.x, y: isNaN(spreadDir.y) ? dir2.y : spreadDir.y } + const ammoType = bullet.ammoType + if (!refs.projectiles) refs.projectiles = [] + refs.projectiles.push(createProjectile(spawnPos, { x: spawnPos.x + finalDir.x * d, y: spawnPos.y + finalDir.y * d }, actor, 'pistol', 'Shoot', ammoType, cid)) + } + } else { + applyBackgroundHit(rawAnim, chars, data, refs) + } + } + if (done) { + if (rawAnim.type === 'move' && cid !== 'player') { + const char = chars[cid] + if (char && char.owner !== 'player') { + const e = fresh.characters[cid] ?? char + const charPos = positions[cid] ?? e?.pos + if (e && !isDead(e.integrity) && !isUnconscious(e.integrity)) { + let targetId = null, minDist = Infinity, targetPos = null + if (e.relationships) { + for (const [pid, rel] of Object.entries(e.relationships)) { + if (rel !== 'enemy') continue + const pch = fresh.characters[pid] + if (!pch || isDead(pch.integrity) || deadThisTick.has(pid)) continue + const d = Math.sqrt((pch.pos.x - charPos.x) ** 2 + (pch.pos.y - charPos.y) ** 2) + if (d < minDist) { minDist = d; targetId = pid; targetPos = pch.pos } + } + } + if (targetId) { + const targetChar = fresh.characters[targetId] + const wpn = e.weapon ?? 'fists' + const base = weaponMap[wpn] + const skills = base?.skills ?? [{ name: 'Attack', mult: 1.0, damageType: 'blunt', range: 15 }] + const maxRange = Math.max(...skills.map(s => s.range ?? 15)) + if (minDist <= maxRange) { + const skill = skills[Math.floor(Math.random() * skills.length)] + const { dmg, severity, injType, injLabel } = computeHit({ weaponId: wpn, skill, weaponConditions: {} }) + const targetPart = randomPart() + const action = { + type: 'attack', actor: 'enemy', charId: cid, + targetCharId: targetId, targetPart, + weaponId: wpn, skillName: skill.name, + dmg, severity, injType, injLabel, + attackerParts: { ...e.integrity }, + attackerInjuries: { ...e.injuries }, + targetParts: { ...targetChar.integrity }, + targetInjuries: { ...targetChar.injuries }, + } + actions[cid] = createWeaponStrikeAnim(action, fresh, velocities) + continue + } + if (targetPos) { + const dx = targetPos.x - charPos.x; const dy = targetPos.y - charPos.y + const d = Math.sqrt(dx * dx + dy * dy) + const dir = d > 0 ? { dx: dx / d, dy: dy / d } : { dx: 0, dy: 1 } + dirs[cid] = dir + if (positions[cid]) { + velocities[cid] = { + x: (dx / d) * (chars[cid]?.maxSpeed ?? 1) * 60 * (getMovement(chars[cid]?.integrity) / 100 || 1), + y: (dy / d) * (chars[cid]?.maxSpeed ?? 1) * 60 * (getMovement(chars[cid]?.integrity) / 100 || 1), + } + } + actions[cid] = null + continue + } + } + } + } + } + actions[cid] = null + } + } + + // Sync velocities written by MoveAction.step (visible refs) → bg refs for physics step + // Only for characters whose action was a MoveAction (player-directed movement) + for (const [cid, action] of Object.entries(actions)) { + if (action instanceof MoveAction) { + const visVel = characterVelocitiesRef.current[cid] + if (visVel) velocities[cid] = { ...visVel } + } + } + + // Physics step (using background refs) + const charIds = Object.keys(positions) + for (let iter = 0; iter < 2; iter++) { + for (const cid of charIds) { + const pos = positions[cid] + if (!pos) continue + const vel = velocities[cid] + if (!vel) continue + const ch = chars[cid] + if (ch && isUnconscious(ch.integrity)) { + if (vel.x !== 0 || vel.y !== 0) { vel.x = 0; vel.y = 0 } + continue + } + let nx = pos.x, ny = pos.y + if (vel.x !== 0 || vel.y !== 0) { + const travel = Math.max(Math.abs(vel.x * dt), Math.abs(vel.y * dt)) + const subSteps = Math.max(1, Math.ceil(travel / 2)) + const subDt = dt / subSteps + for (let i = 0; i < subSteps; i++) { + let sx = nx + vel.x * subDt; let sy = ny + vel.y * subDt + const hitBlock = blk.find(b => rectCollides(sx, sy, b)) + if (hitBlock) { + const leftPen = sx - hitBlock.x; const rightPen = (hitBlock.x + hitBlock.w) - sx + const topPen = sy - hitBlock.y; const bottomPen = (hitBlock.y + hitBlock.h) - sy + if (Math.min(leftPen, rightPen) < Math.min(topPen, bottomPen)) { vel.x = 0; sx = leftPen < rightPen ? hitBlock.x : hitBlock.x + hitBlock.w } + else { vel.y = 0; sy = topPen < bottomPen ? hitBlock.y : hitBlock.y + hitBlock.h } + } + nx = Math.max(5, Math.min(wMax, sx)); ny = Math.max(5, Math.min(wMax, sy)) + } + vel.x /= (1 + FRICTION * dt); vel.y /= (1 + FRICTION * dt) + if (Math.abs(vel.x) < 0.01) vel.x = 0 + if (Math.abs(vel.y) < 0.01) vel.y = 0 + } + for (const b of blk) { + if (!rectCollides(nx, ny, b)) continue + const leftPen = nx - b.x; const rightPen = (b.x + b.w) - nx + const topPen = ny - b.y; const bottomPen = (b.y + b.h) - ny + if (Math.min(leftPen, rightPen) < Math.min(topPen, bottomPen)) { nx = leftPen < rightPen ? b.x : b.x + b.w } + else { ny = topPen < bottomPen ? b.y : b.y + b.h } + } + for (const oid of charIds) { + if (oid === cid) continue + const op = positions[oid]; if (!op) continue + const dx = nx - op.x; const dy = ny - op.y; const dist = Math.sqrt(dx * dx + dy * dy) + if (dist < 10 && dist > 0.001) { + const push = (10 - dist) * 0.5; const px2 = (dx / dist) * push; const py2 = (dy / dist) * push + nx += px2; ny += py2; op.x -= px2; op.y -= py2 + } + } + nx = Math.max(5, Math.min(wMax, nx)); ny = Math.max(5, Math.min(wMax, ny)) + positions[cid] = { x: nx, y: ny } + } + } + + // Run enemy AI on background characters + if (timerActiveRef.current) { + const aiActions = runEnemyAI(fresh, {}, {}, deadThisTick) + for (const act of aiActions) { + const actor = fresh.characters[act.charId] + if (!actor || isDead(actor.integrity) || isUnconscious(actor.integrity)) continue + if (act.type === 'attack') { + const target = fresh.characters[act.targetCharId] + if (!target || isDead(target.integrity)) continue + } + const existing = actions[act.charId] + const isMove = existing?.type === 'move' + if (act.type === 'attack' && (!existing || isMove)) { + actions[act.charId] = createWeaponStrikeAnim(act, fresh, velocities) + } else if (act.type === 'move') { + const dx = act.toX - act.fromX; const dy = act.toY - act.fromY + const d = Math.sqrt(dx * dx + dy * dy) + if (d > 0) { + dirs[act.charId] = { dx: dx / d, dy: dy / d } + if (positions[act.charId]) { + velocities[act.charId] = { + x: (dx / d) * (chars[act.charId]?.maxSpeed ?? 1) * 60 * (getMovement(chars[act.charId]?.integrity) / 100 || 1), + y: (dy / d) * (chars[act.charId]?.maxSpeed ?? 1) * 60 * (getMovement(chars[act.charId]?.integrity) / 100 || 1), + } + } + } + actions[act.charId] = null + } + } + } + + // Step projectiles for background location + if (refs.projectiles && refs.projectiles.length > 0) { + const remaining = [] + for (const p of refs.projectiles) { + if (p.hitFired) continue + const cfg = PROJ_CFG[p.type] + const stepDist = cfg.speed * dt + const remainingDist = p.dist - p.distTraveled + const moveDist = Math.min(stepDist, remainingDist) + 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 + const cx = p.pos.x + p.dir.x * moveDist * t + const cy = p.pos.y + p.dir.y * moveDist * t + const curBlocks = blk + const hitBlock = curBlocks.find(b => rectCollides(cx, cy, b)) + if (hitBlock) { + if (p.ammoType === 'bouncing_ammo') { + const dl = cx - hitBlock.x; const dr = (hitBlock.x + hitBlock.w) - cx + const dt2 = cy - hitBlock.y; const db = (hitBlock.y + hitBlock.h) - cy + if (Math.min(dl, dr) < Math.min(dt2, db)) { 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 + } + applyBackgroundProjectileHit(p, null, chars, data, refs) + hit = true; hitCx = cx; hitCy = cy; break + } + for (const eid of Object.keys(chars)) { + if (eid === p.charId) continue + const e = chars[eid] + if (!e || isDead(e.integrity)) continue + const ePos = positions[eid] ?? e.pos + const r = cfg.hitRadius + 4 * (e.size ?? 1) + if (Math.sqrt((cx - ePos.x) ** 2 + (cy - ePos.y) ** 2) < r) { + applyBackgroundProjectileHit(p, eid, chars, data, refs) + hit = true; hitCx = cx; hitCy = cy; break + } + } + if (hit) break + } + if (hit) { + if (hitCx != null) { p.pos.x = hitCx; p.pos.y = hitCy } + remaining.push(p) + continue + } + if (moveDist < stepDist) continue + p.distTraveled += moveDist + p.pos.x += p.dir.x * moveDist + p.pos.y += p.dir.y * moveDist + remaining.push(p) + } + refs.projectiles = remaining + } + + // Sync local character mutations back to the ref (persistence across renders) + for (const [cid, ch] of Object.entries(chars)) { + if (cid === 'player') continue + const refEntry = characterOverridesRef.current[cid] ?? {} + if (ch.integrity) refEntry.integrity = { ...ch.integrity } + if (ch.injuries) refEntry.injuries = JSON.parse(JSON.stringify(ch.injuries)) + if (ch.weaponMagazines) refEntry.weaponMagazines = { ...ch.weaponMagazines } + if (ch.itemCounts) refEntry.itemCounts = { ...ch.itemCounts } + characterOverridesRef.current[cid] = refEntry + } + } + + // Helper: activate a background location (add to active set, store its data). + function activateBgLocation(locId, data, charIds) { + if (!locId) return + if (!activeLocationsRef.current.includes(locId)) { + activeLocationsRef.current = [...activeLocationsRef.current, locId] + } + backgroundDataRef.current[locId] = { blocks: data.blocks, worldSize: data.worldSize, groundItems: data.groundItems ?? [], projectiles: data.projectiles ?? [], splats: data.splats ?? [] } + const bg = ensureBgRefs(locId) + for (const cid of charIds) { + if (cid === 'player') continue + const ch = data.characters[cid] + if (ch?.pos) bg.positions[cid] = { ...ch.pos } + bg.velocities[cid] = { x: 0, y: 0 } + bg.dirs[cid] = { dx: 0, dy: 1 } + bg.actions[cid] = null + } + } + + // Helper: deactivate a background location + function deactivateBgLocation(locId) { + if (!locId) return + activeLocationsRef.current = activeLocationsRef.current.filter(id => id !== locId) + delete backgroundDataRef.current[locId] + delete backgroundRefsRef.current[locId] + } + // ───────────────────────────────────────────────────────────────────────── // DERIVED // ───────────────────────────────────────────────────────────────────────── @@ -951,91 +1409,12 @@ export default function App() { } } - /** - * Step all active projectiles. Returns the filtered array of still-active projectiles. - * Applies hit damage / logging as side effects. - */ - function stepProjectiles(projs, dt, cc) { - if (!cc) return [] - const out = [] - for (const p of projs) { - if (p.hitFired) continue - - const cfg = PROJ_CFG[p.type] - const stepDist = cfg.speed * dt - const remaining = p.dist - p.distTraveled - const moveDist = Math.min(stepDist, remaining) - - 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 - const cx = p.pos.x + p.dir.x * moveDist * t - const cy = p.pos.y + p.dir.y * moveDist * t - - const curBlocks = cc.blocks ?? [] - const hitBlock = curBlocks.find(b => rectCollides(cx, cy, b)) - if (hitBlock) { - if (p.ammoType === 'bouncing_ammo') { - const dl = cx - hitBlock.x - const dr = (hitBlock.x + hitBlock.w) - cx - const dt = cy - hitBlock.y - const db = (hitBlock.y + hitBlock.h) - cy - if (Math.min(dl, dr) < Math.min(dt, db)) { - 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) - hit = true; hitCx = cx; hitCy = cy; break - } - - for (const cid of Object.keys(cc.characters)) { - if (cid === p.charId) continue - const e = cc.characters[cid] - if (!e || isDead(e.integrity)) continue - const ePos = characterPositionsRef.current[cid] ?? e.pos - 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; hitCx = cx; hitCy = cy; break - } - } - if (hit) break - } - if (hit) { - if (hitCx != null) { p.pos.x = hitCx; p.pos.y = hitCy } - out.push(p) - continue - } - - if (moveDist < stepDist) continue - - p.distTraveled += moveDist - p.pos.x += p.dir.x * moveDist - p.pos.y += p.dir.y * moveDist - out.push(p) - } - return out - } - - /** - * Factory: create a weapon animation based on the skill's damage type. - * slash → arcing motion, pierce → straight thrust, blunt → straight strike. - */ - function createWeaponStrikeAnim(action, cc) { + function createWeaponStrikeAnim(action, cc, velRef) { if (!cc) return null const attackerId = action.charId - if (characterVelocitiesRef.current[attackerId]) { - characterVelocitiesRef.current[attackerId] = { x: 0, y: 0 } + const targetVel = velRef ?? characterVelocitiesRef.current + if (targetVel[attackerId]) { + targetVel[attackerId] = { x: 0, y: 0 } } const isPlayerSide = action.actor === 'player' || action.actor === 'party' const targetId = action.targetCharId ?? (isPlayerSide ? action.enemyCharId : ( @@ -1201,64 +1580,6 @@ export default function App() { } } - /** - * Apply damage / log when a projectile hits something. - */ - function applyProjectileHit(proj, charId) { - proj.hitFired = true - if (!charId) { - setCombat(c => ({ ...c, log: [...c.log, 'The bullet hits a wall!'] })) - return - } - - const cc = combatRef.current - const target = cc?.characters[charId] - if (!target || isDead(target.integrity)) return - - const skill = weaponMap.pistol?.skills?.[0] - if (!skill) return - - const shooterChar = cc?.characters[proj.charId] - const shooterConditions = proj.charId === 'player' ? playerRef.current.weaponConditions : (shooterChar?.weaponConditions ?? {}) - const shooterName = proj.charId === 'player' ? 'You' : (shooterChar?.name ?? 'Someone') - - const { dmg, severity, injType, injLabel } = computeHit({ - weaponId: 'pistol', - skill, - weaponConditions: shooterConditions, - bothMult: 1.0, - }) - - const part = ALL_PARTS[Math.floor(Math.random() * ALL_PARTS.length)] - - setCombat(c => { - const characters = { ...c.characters } - const e = { ...characters[charId] } - const integrity = { ...e.integrity } - const injuries = { ...(e.injuries ?? freshInjuries()) } - - integrity[part] = Math.max(0, (integrity[part] ?? 100) - dmg) - Object.assign(integrity, propagateDestruction(integrity)) - if (integrity[part] > 0 && severity > 0) { - injuries[part] = [...(injuries[part] ?? []), { type: injType, severity }] - } - e.integrity = integrity - e.injuries = injuries - characters[charId] = e - - const log = dmg > 0 - ? [`${shooterName} shoots ${e.name} in the ${bodyPartLabels[part] ?? part} for ${dmg} damage! ${injLabel} (severity ${severity}).`] - : [`${shooterName}'s shot glances off harmlessly.`] - if (isDead(integrity)) { - if (part === 'head') log.push(`${shooterName} blows ${e.name}'s head off!`) - log.push(`${e.name} is defeated!`) - deadThisTickRef.current.add(charId) - } - return { ...c, characters, log: [...c.log, ...log] } - }) - setHitShake({ actor: 'enemy', charId, start: performance.now() }) - } - /** Queue a shot toward a map coordinate — queues a shoot action. */ const queueShot = (tx, ty) => { if (!combat) return @@ -1419,263 +1740,6 @@ class MoveAction { } } - function applyAnimHit(anim) { - const liveCc = combatRef.current - const attackerId = anim.charId - const isPlayerSide = anim.actor === 'player' || anim.actor === 'party' - const targetId = anim.targetCharId ?? (isPlayerSide ? anim.enemyCharId : ( - liveCc ? Object.keys(liveCc.characters).find(cid => { - return liveCc.characters[cid]?.owner === 'player' - }) ?? liveCc.playerEntityId : liveCc?.playerEntityId - )) - - let inRange = false - if (liveCc) { - // Primary: live positions from characterPositionsRef (updated by move actions) - const attackerPos = characterPositionsRef.current[attackerId] ?? liveCc.characters[attackerId]?.pos - const targetPos = characterPositionsRef.current[targetId] ?? liveCc.characters[targetId]?.pos - if (attackerPos && targetPos) { - inRange = canReach(attackerPos, targetPos, anim.weaponId, anim.skillName) - } - // Fallback: rendered combatRef positions - if (!inRange) { - 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) { - 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 attackerName = liveCc?.characters[anim.charId]?.name ?? (anim.actor === 'player' ? 'You' : 'Enemy') - const msg = isPlayerSide - ? `Your attack misses! (distance combatRef=${dist.toFixed(1)} refs=${dist2.toFixed(1)} range=${range})` - : `${attackerName}'s attack misses!` - setCombat(prev => ({ ...prev, log: [...prev.log, msg] })) - return - } - - setHitShake({ actor: anim.actor, charId: anim.charId, start: performance.now() }) - - if (isPlayerSide) { - const wid = anim.weaponId - if (wid !== 'fists' && wid !== 'left_arm' && wid !== 'right_arm') { - const base = weaponMap[wid] - if (anim.actor === 'player') { - setPlayer(p => ({ ...p, weaponConditions: { ...p.weaponConditions, [wid]: Math.max(0, (p.weaponConditions[wid] ?? base.maxCondition) - (1 + Math.floor(Math.random() * 2))) } })) - } else { - setCombat(prev => { - const ch = prev.characters[anim.charId] - if (!ch) return prev - return { ...prev, characters: { ...prev.characters, [anim.charId]: { ...ch, weaponConditions: { ...ch.weaponConditions, [wid]: Math.max(0, (ch.weaponConditions[wid] ?? base.maxCondition) - (1 + Math.floor(Math.random() * 2))) } } } } - }) - } - } - - if (anim.actor === 'player') { - const decayedPlayer = calcIntegrity(playerRef.current.integrity, playerRef.current.injuries, ANIM_CFG[anim.type].hitAt) - setPlayer(p => ({ ...p, integrity: decayedPlayer })) - setPlayer(p => ({ ...p, injuries: playerRef.current.injuries })) - } else { - setCombat(prev => { - const ch = prev.characters[anim.charId] - if (!ch) return prev - const decayed = calcIntegrity(ch.integrity, ch.injuries, ANIM_CFG[anim.type].hitAt) - return { ...prev, characters: { ...prev.characters, [anim.charId]: { ...ch, integrity: decayed } } } - }) - } - - if (liveCc && anim.enemyCharId) { - const enemy = liveCc.characters[anim.enemyCharId] - if (enemy) { - const decayedEnemy = calcIntegrity(enemy.integrity, enemy.injuries, ANIM_CFG[anim.type].hitAt) - const prevHp = decayedEnemy[anim.targetPart] - const hitEnemy = propagateDestruction({ ...decayedEnemy, [anim.targetPart]: Math.max(0, prevHp - anim.dmg) }) - const newInjuries = prevHp > 0 && anim.severity > 0 - ? applyInjury(enemy.injuries, anim.targetPart, anim.injType, anim.severity) - : enemy.injuries - const attackerName = liveCc.characters[anim.charId]?.name ?? 'You' - - const log = anim.dmg > 0 - ? [`${attackerName} ${anim.skillName?.toLowerCase() ?? 'attack'}s ${enemy.name}'s ${bodyPartLabels[anim.targetPart]} — ${anim.injLabel} (severity ${anim.severity}, -${anim.dmg} integrity).`] - : [] - if (isDead(hitEnemy)) { - if (anim.targetPart === 'head') log.push(`${attackerName} crushes ${enemy.name}'s head!`) - log.push(`${enemy.name} is defeated!`) - deadThisTickRef.current.add(anim.enemyCharId) - } - - setCombat(prev => ({ - ...prev, - characters: { ...prev.characters, [anim.enemyCharId]: { ...enemy, integrity: hitEnemy, injuries: newInjuries } }, - log: [...prev.log, ...log], - })) - } - } - } else { - const currentEnemy = liveCc?.characters[anim.charId] - const decayedEnemy = currentEnemy - ? calcIntegrity(currentEnemy.integrity, currentEnemy.injuries, ANIM_CFG[anim.type].hitAt) - : calcIntegrity(anim.attackerParts, anim.attackerInjuries, ANIM_CFG[anim.type].hitAt) - - // Find target party member - const targetCharId = anim.targetCharId ?? ( - combat ? Object.keys(combat.characters).find(cid => { - return combat.characters[cid]?.owner === 'player' - }) ?? 'player' : 'player' - ) - const targetChar = liveCc?.characters[targetCharId] - const targetIntegrity = targetChar?.integrity ?? playerRef.current.integrity - const targetInjuries = targetChar?.injuries ?? playerRef.current.injuries - - const decayedTarget = calcIntegrity(targetIntegrity, targetInjuries, ANIM_CFG[anim.type].hitAt) - const prevHp = decayedTarget[anim.targetPart] - const hitTarget = propagateDestruction({ ...decayedTarget, [anim.targetPart]: Math.max(0, prevHp - anim.dmg) }) - const newInjuries = prevHp > 0 && anim.severity > 0 - ? applyInjury(targetInjuries, anim.targetPart, anim.injType, anim.severity) - : targetInjuries - - if (liveCc) { - const enemy = liveCc.characters[anim.charId] - const enemyName = enemy?.name ?? 'Enemy' - - setCombat(prev => ({ - ...prev, - characters: { - ...prev.characters, - [anim.charId]: { ...enemy, integrity: decayedEnemy }, - [targetCharId]: { ...prev.characters[targetCharId], integrity: hitTarget, injuries: newInjuries }, - }, - log: anim.dmg > 0 - ? [...prev.log, `${enemyName} lands a ${anim.injLabel} on ${targetChar?.name ?? 'your'} ${bodyPartLabels[anim.targetPart]} (severity ${anim.severity}, integrity -${anim.dmg}).`] - : prev.log, - })) - } - - if (isDead(hitTarget)) { - deadThisTickRef.current.add(targetCharId) - } - if (targetCharId === 'player') { - setPlayer(p => ({ ...p, integrity: hitTarget, injuries: newInjuries })) - } - } - } - - // ───────────────────────────────────────────────────────────────────────── - // PHYSICS — integrates velocities, resolves collisions, applies friction - // ───────────────────────────────────────────────────────────────────────── - - function stepPhysics(dt, blocks) { - const blk = blocks ?? [] - const charIds = Object.keys(characterPositionsRef.current) - const updates = {} - - // Multiple iterations for stable constraint resolution - for (let iter = 0; iter < 2; iter++) { - for (const cid of charIds) { - const pos = characterPositionsRef.current[cid] - if (!pos) continue - const vel = characterVelocitiesRef.current[cid] - if (!vel) continue - - // Unconscious characters cannot move - const cc = combatRef.current - const ch = cc?.characters[cid] - if (ch && isUnconscious(ch.integrity)) { - if (vel.x !== 0 || vel.y !== 0) { vel.x = 0; vel.y = 0 } - continue - } - - let nx = pos.x, ny = pos.y - - // 1. Velocity integration (sub-stepped for collision safety) - if (vel.x !== 0 || vel.y !== 0) { - const travel = Math.max(Math.abs(vel.x * dt), Math.abs(vel.y * dt)) - const subSteps = Math.max(1, Math.ceil(travel / 2)) - const subDt = dt / subSteps - for (let i = 0; i < subSteps; i++) { - let sx = nx + vel.x * subDt - let sy = ny + vel.y * subDt - const hitBlock = blk.find(b => rectCollides(sx, sy, b)) - if (hitBlock) { - const leftPen = sx - hitBlock.x - const rightPen = (hitBlock.x + hitBlock.w) - sx - const topPen = sy - hitBlock.y - const bottomPen= (hitBlock.y + hitBlock.h) - sy - const minPenX = Math.min(leftPen, rightPen) - const minPenY = Math.min(topPen, bottomPen) - if (minPenX < minPenY) { vel.x = 0; sx = leftPen < rightPen ? hitBlock.x : hitBlock.x + hitBlock.w } - else { vel.y = 0; sy = topPen < bottomPen ? hitBlock.y : hitBlock.y + hitBlock.h } - } - nx = Math.max(5, Math.min(145, sx)) - ny = Math.max(5, Math.min(145, sy)) - } - vel.x /= (1 + FRICTION * dt) - vel.y /= (1 + FRICTION * dt) - if (Math.abs(vel.x) < 0.01) vel.x = 0 - if (Math.abs(vel.y) < 0.01) vel.y = 0 - } - - // 2. Block containment — push toward nearest edge if inside a block - for (const b of blk) { - if (!rectCollides(nx, ny, b)) continue - const leftPen = nx - b.x - const rightPen = (b.x + b.w) - nx - const topPen = ny - b.y - const bottomPen= (b.y + b.h) - ny - const minPenX = Math.min(leftPen, rightPen) - const minPenY = Math.min(topPen, bottomPen) - if (minPenX < minPenY) { nx = leftPen < rightPen ? b.x : b.x + b.w } - else { ny = topPen < bottomPen ? b.y : b.y + b.h } - } - - // 3. Character-character proximity — push apart when too close - for (const otherCid of charIds) { - if (otherCid === cid) continue - const otherPos = characterPositionsRef.current[otherCid] - if (!otherPos) continue - const dx = nx - otherPos.x - const dy = ny - otherPos.y - const dist = Math.sqrt(dx * dx + dy * dy) - if (dist < 10 && dist > 0.001) { - const push = (10 - dist) * 0.5 - const pushX = (dx / dist) * push - const pushY = (dy / dist) * push - nx += pushX; ny += pushY - otherPos.x -= pushX; otherPos.y -= pushY - updates[otherCid] = { x: otherPos.x, y: otherPos.y } - } - } - - // 4. Clamp to world bounds - nx = Math.max(5, Math.min(145, nx)) - ny = Math.max(5, Math.min(145, ny)) - pos.x = nx; pos.y = ny - updates[cid] = { x: nx, y: ny } - } - } - - const keys = Object.keys(updates) - if (keys.length > 0) { - setCombat(c => { - if (!c) return c - const chars = { ...c.characters } - for (const cid of keys) { - if (chars[cid]) chars[cid] = { ...chars[cid], pos: { ...updates[cid] } } - } - return { ...c, characters: chars } - }) - } - } - // ───────────────────────────────────────────────────────────────────────── // TIMER — ticks while timerActive is true, updates all character actions // ───────────────────────────────────────────────────────────────────────── @@ -1734,21 +1798,19 @@ class MoveAction { const action = characterActionsRef.current[cid] return action != null && !action.cancellable }) - const anyQueued = partyCids.length > 0 && partyCids.every(cid => { - const ch = cc.characters[cid] - if (isDead(ch?.integrity)) return true - return characterActionsRef.current[cid] != null - }) const wasBusy = prevBusyRef.current prevBusyRef.current = allBusy setBusy(allBusy) - if (anyQueued && !timerActiveRef.current && partyCids.some(cid => !isDead(cc.characters[cid]?.integrity))) { + // Auto-start timer when player has a queued action; auto-stop when idle + const playerCid = cc?.playerEntityId ?? 'player' + const playerHasAction = characterActionsRef.current[playerCid] != null + if (playerHasAction && !timerActiveRef.current) { timerActiveRef.current = true manualTimerRef.current = false setTimerRunning(true) } - if (!anyQueued && timerActiveRef.current && !manualTimerRef.current) { + if (!playerHasAction && timerActiveRef.current && !manualTimerRef.current) { timerActiveRef.current = false setTimerRunning(false) } @@ -1787,8 +1849,6 @@ class MoveAction { const dt = rawDt * speedRef.current lastTime = now - deadThisTickRef.current = new Set() - setCombatTime(t => t + dt) setDisplayTime(t => t + dt) setGameTime(gt => { @@ -1801,200 +1861,102 @@ class MoveAction { return { day: d, hour: h, minute } }) - // ── Build fresh combat snapshot with live positions ── + // Bridge player-queued actions and velocities into background refs before the unified tick + const curLoc = playerRef.current.location + const playerBgRefs = backgroundRefsRef.current[curLoc] + if (playerBgRefs) { + for (const [cid, action] of Object.entries(characterActionsRef.current)) { + if (action) playerBgRefs.actions[cid] = action + } + for (const [cid, vel] of Object.entries(characterVelocitiesRef.current)) { + if (vel) playerBgRefs.velocities[cid] = { ...vel } + } + } + + // ── Tick ALL active locations uniformly (player's location included) ── + for (const locId of activeLocationsRef.current) { + tickBackgroundLocation(locId, dt) + } + + // ── Sync visible combat state from player's background location ── + if (playerBgRefs) { + // Build character map from global state (includes dead chars for rendering) + const bgChars = {} + for (const c of characters) { + const override = characterOverridesRef.current[c.id] + const merged = override ? { ...c, ...override } : c + if (merged.location === curLoc) { + const integrity = merged.integrity ?? freshBody() + const pos = playerBgRefs.positions[c.id] ?? merged.pos ?? { x: 20, y: 75 } + const inj = merged.injuries ? JSON.parse(JSON.stringify(merged.injuries)) : freshInjuries() + bgChars[c.id] = { ...merged, integrity: { ...integrity }, injuries: inj, pos: { ...pos } } + } + } + const allCids = new Set([ + ...Object.keys(bgChars), + ...Object.keys(playerBgRefs.positions), + ]) + // Copy bgRefs back into visible refs so the rest of the UI works + for (const cid of allCids) { + characterPositionsRef.current[cid] = playerBgRefs.positions[cid] + ? { ...playerBgRefs.positions[cid] } + : { ...bgChars[cid]?.pos } + characterVelocitiesRef.current[cid] = playerBgRefs.velocities[cid] + ? { ...playerBgRefs.velocities[cid] } + : { x: 0, y: 0 } + characterDirsRef.current[cid] = playerBgRefs.dirs[cid] + ? { ...playerBgRefs.dirs[cid] } + : { dx: 0, dy: 1 } + characterActionsRef.current[cid] = playerBgRefs.actions[cid] ?? null + } + // Sync combat state from global character state + background refs + setCombat(c => { + if (!c) return c + const chars = {} + for (const [cid, ch] of Object.entries(bgChars)) { + chars[cid] = { + ...ch, + pos: playerBgRefs.positions[cid] + ? { ...playerBgRefs.positions[cid] } + : { ...ch.pos }, + } + } + // Preserve player character (not stored in bgChars) + const playerEntry = c.characters['player'] + if (playerEntry) { + chars['player'] = { + ...playerEntry, + pos: characterPositionsRef.current['player'] + ? { ...characterPositionsRef.current['player'] } + : { ...playerEntry.pos }, + } + } + return { ...c, characters: chars } + }) + // Sync projectiles from background + setProjectiles([...(playerBgRefs.projectiles ?? [])]) + // Sync blood splats from background + setBloodSplats([...(backgroundDataRef.current[curLoc]?.splats ?? [])]) + // Persist background tick mutations back to React character overrides + setCharacterOverrides(prev => { + const next = { ...prev } + for (const [cid, ch] of Object.entries(bgChars)) { + if (cid === 'player') continue + next[cid] = { ...(next[cid] ?? {}), integrity: { ...ch.integrity }, injuries: ch.injuries ? JSON.parse(JSON.stringify(ch.injuries)) : undefined } + } + return next + }) + } + + // ── Collect active animations for rendering ── const activeAnims = [] - const cc = combatRef.current - const playerCharId = cc?.playerEntityId - const fresh = cc ? (() => { - const chars = {} - for (const cid of Object.keys(cc.characters)) { - const c = cc.characters[cid] - const pos = characterPositionsRef.current[cid] ?? c.pos - chars[cid] = { ...c, pos: { ...pos } } - } - return { ...cc, characters: chars } - })() : null - - // ── Tick ALL character actions uniformly ── - if (cc) { - for (const cid of Object.keys(cc.characters)) { - // Skip dead or unconscious characters — clear their action and move on - if (isDead(cc.characters[cid]?.integrity) || isUnconscious(cc.characters[cid]?.integrity) || deadThisTickRef.current.has(cid)) { - if (characterActionsRef.current[cid]) { - characterActionsRef.current[cid] = null - } - continue - } - const rawAnim = characterActionsRef.current[cid] - if (!rawAnim) continue - - const done = rawAnim.step(dt) - if (ANIM_CFG[rawAnim.type] && rawAnim.elapsed >= ANIM_CFG[rawAnim.type].hitAt && !rawAnim.hitFired) { - rawAnim.hitFired = true - if (rawAnim.type === 'shoot') { - const pp = characterPositionsRef.current[cid] ?? cc.characters[cid]?.pos - const dir = characterDirsRef.current[cid] ?? { dx: 0, dy: 1 } - const offset = 5 - const spawnPos = { x: pp.x + dir.dx * offset, y: pp.y + dir.dy * offset } - const actor = cid === 'player' ? 'player' : 'party' - const charMag = cid === 'player' ? magazineRef.current : (cc.characters[cid]?.weaponMagazines ?? {}) - if (charMag.pistol?.length > 0) { - const bullet = charMag.pistol[0] - const newMag = { pistol: charMag.pistol.slice(1) } - if (cid === 'player') { - magazineRef.current = newMag - setPlayer(p => ({ ...p, weaponMagazines: { ...p.weaponMagazines, ...newMag } })) - } else { - setCombat(prev => ({ - ...prev, - characters: { - ...prev.characters, - [cid]: { ...prev.characters[cid], weaponMagazines: { ...prev.characters[cid].weaponMagazines, ...newMag } }, - }, - })) - } - const ddx = rawAnim.endPos.x - spawnPos.x - const ddy = rawAnim.endPos.y - spawnPos.y - const d = Math.sqrt(ddx * ddx + ddy * ddy) || 1 - const dir = { x: ddx / d, y: ddy / d } - const facing = characterDirsRef.current[cid] ?? { dx: 0, dy: 1 } - const dot = Math.max(-1, Math.min(1, facing.dx * dir.x + facing.dy * dir.y)) - const maxDeviation = (1 - dot) / 2 * (Math.PI / 4) - const deviation = (Math.random() * 2 - 1) * maxDeviation - 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 finalDir = { x: isNaN(spreadDir.x) ? dir.x : spreadDir.x, y: isNaN(spreadDir.y) ? dir.y : spreadDir.y } - const ammoType = bullet.ammoType - setProjectiles(prev => [...prev, createProjectile(spawnPos, { x: spawnPos.x + finalDir.x * d, y: spawnPos.y + finalDir.y * d }, actor, 'pistol', 'Shoot', ammoType, cid)]) - new Audio(pistolShotMp3).play().catch(() => {}) - } else if (cid === 'player') { - setCombat(c => ({ ...c, log: [...c.log, 'Click! The pistol is empty.'] })) - } else { - setCombat(c => ({ ...c, log: [...c.log, `${cc.characters[cid]?.name ?? 'Party member'} clicks — the pistol is empty.`] })) - } - } else { - applyAnimHit(rawAnim) - } - } - if (done) { - // ── Enemy move → attack transition ── - if (rawAnim.type === 'move' && cid !== playerCharId) { - const char = cc.characters[cid] - if (!char || char.owner === 'player') continue - const e = fresh?.characters[cid] ?? char - const charPos = characterPositionsRef.current[cid] ?? e?.pos - if (e && !isDead(e.integrity) && !isUnconscious(e.integrity)) { - let targetId = null, minDist = Infinity, targetPos = null - if (e.relationships) { - for (const [pid, rel] of Object.entries(e.relationships)) { - if (rel !== 'enemy') continue - const pch = (fresh ?? cc).characters[pid] - if (!pch || isDead(pch.integrity) || deadThisTickRef.current.has(pid)) continue - const d = Math.sqrt((pch.pos.x - charPos.x) ** 2 + (pch.pos.y - charPos.y) ** 2) - if (d < minDist) { minDist = d; targetId = pid; targetPos = pch.pos } - } - } - if (targetId) { - const targetChar = (fresh ?? cc).characters[targetId] - const wpn = e.weapon ?? 'fists' - const base = weaponMap[wpn] - const skills = base?.skills ?? [{ name: 'Attack', mult: 1.0, damageType: 'blunt', range: 15 }] - const maxRange = Math.max(...skills.map(s => s.range ?? 15)) - if (minDist <= maxRange) { - const skill = skills[Math.floor(Math.random() * skills.length)] - const { dmg, severity, injType, injLabel } = computeHit({ weaponId: wpn, skill, weaponConditions: {} }) - const targetPart = randomPart() - const action = { - type: 'attack', actor: 'enemy', charId: cid, - targetCharId: targetId, - targetPart, - weaponId: wpn, skillName: skill.name, - dmg, severity, injType, injLabel, - attackerParts: { ...e.integrity }, - attackerInjuries: { ...e.injuries }, - targetParts: { ...targetChar.integrity }, - targetInjuries: { ...targetChar.injuries }, - } - characterActionsRef.current[cid] = createWeaponStrikeAnim(action, fresh ?? cc) - continue - } - // Still out of range after move → keep chasing with fresh target - if (targetPos) { - const dx = targetPos.x - charPos.x, dy = targetPos.y - charPos.y - const d = Math.sqrt(dx * dx + dy * dy) - const dir = d > 0 ? { dx: dx / d, dy: dy / d } : { dx: 0, dy: 1 } - characterActionsRef.current[cid] = new ParallelAction({ - type: 'move', - actions: [ - new TurnAction({ - targetDir: dir, - getDir: () => characterDirsRef.current[cid] ?? { dx: 0, dy: 1 }, - setDir: d2 => { characterDirsRef.current[cid] = d2 } - }), - new MoveAction({ - charId: cid, fromX: charPos.x, fromY: charPos.y, - toX: targetPos.x, toY: targetPos.y, - blocks: cc.blocks, - }) - ] - }) - continue - } - } - } - } - characterActionsRef.current[cid] = null - } else { - if (ANIM_CFG[rawAnim.type]) { - activeAnims.push(rawAnim) - } + if (playerBgRefs) { + for (const rawAnim of Object.values(playerBgRefs.actions)) { + if (rawAnim && ANIM_CFG[rawAnim.type] && !rawAnim.done) { + activeAnims.push(rawAnim) } } } - - // ── Physics step (integrate velocities, resolve collisions) ── - if (cc) { stepPhysics(dt, cc.blocks) } - - // ── Run enemy AI every frame for idle enemies ── - if (fresh && timerActiveRef.current) { - const actions = runEnemyAI(fresh, playerRef.current.integrity, playerRef.current.injuries, deadThisTickRef.current) - for (const act of actions) { - const actor = fresh.characters[act.charId] - if (!actor || isDead(actor.integrity) || isUnconscious(actor.integrity)) continue - if (act.type === 'attack') { - const target = fresh.characters[act.targetCharId] - if (!target || isDead(target.integrity)) continue - } - const existing = characterActionsRef.current[act.charId] - const isMove = existing?.type === 'move' - if (act.type === 'attack' && (!existing || isMove)) { - characterActionsRef.current[act.charId] = createWeaponStrikeAnim(act, fresh) - } else if (act.type === 'move') { - 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 ParallelAction({ - 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, - blocks: fresh.blocks, - }) - ] - }) - } - } - } - - // ── Step projectiles ── - setProjectiles(prev => stepProjectiles(prev, dt, combatRef.current)) - - // ── Update rendering state for active animations ── setAnimations(activeAnims) } @@ -2128,23 +2090,46 @@ class MoveAction { return () => cancelAnimationFrame(frame) }, [zoomAnim]) - // ── minimap wheel zoom (non-passive to allow preventDefault) + // ── minimap zoom animation (smooth scroll-wheel zoom) useEffect(() => { + if (!minimapZoomAnim) return + const frame = requestAnimationFrame(function tick() { + const t = Math.min((performance.now() - minimapZoomAnim.start) / 150, 1) + const ease = 1 - Math.pow(1 - t, 3) + setMinimapView({ + x: minimapZoomAnim.from.x + (minimapZoomAnim.to.x - minimapZoomAnim.from.x) * ease, + y: minimapZoomAnim.from.y + (minimapZoomAnim.to.y - minimapZoomAnim.from.y) * ease, + scale: minimapZoomAnim.from.scale + (minimapZoomAnim.to.scale - minimapZoomAnim.from.scale) * ease, + }) + if (t >= 1) setMinimapZoomAnim(null) + else requestAnimationFrame(tick) + }) + return () => cancelAnimationFrame(frame) + }, [minimapZoomAnim]) + + // ── minimap wheel zoom (layout effect — always runs so handler is never missed) + useLayoutEffect(() => { const el = minimapSvgRef.current if (!el) return const handler = e => { - if (!moveModeRef.current && !aimModeRef.current) return e.preventDefault() - const d = e.deltaY > 0 ? 0.9 : 1.1 + const ws = viewingLoc + ? (backgroundDataRef.current[viewingLoc]?.worldSize ?? (viewingLoc?.startsWith?.('pocket_') ? 320 : 160)) + : (combatRef.current?.worldSize ?? 160) + const d = e.deltaY > 0 ? 0.8 : 1.25 const r = el.getBoundingClientRect() - setMinimapView(v => { - const ns = Math.max(0.5, Math.min(3, v.scale * d)) - return { x: v.x + (e.clientX - r.left) / r.width * (160/v.scale - 160/ns), y: v.y + (e.clientY - r.top) / r.height * (160/v.scale - 160/ns), scale: ns } - }) + const v = minimapViewRef.current + const S = v.scale + const nx = (e.clientX - r.left) / r.width + const ny = (e.clientY - r.top) / r.height + const ns = Math.max(0.25, Math.min(5, S * d)) + const toX = v.x * ns / S + nx * ws * (ns / S - 1) + const toY = v.y * ns / S + ny * ws * (ns / S - 1) + setMinimapZoomAnim({ from: { x: v.x, y: v.y, scale: S }, to: { x: toX, y: toY, scale: ns }, start: performance.now() }) } el.addEventListener('wheel', handler, { passive: false }) return () => el.removeEventListener('wheel', handler) - }, [setMinimapView]) + }) // NOTE: the previous native `click` listener on the minimap SVG (registered // via addEventListener with an empty dependency array) was removed here. @@ -2184,11 +2169,8 @@ const makeEnemy = (npc, index) => { const startFight = (npcList) => { const list = npcList ?? [] if (list.length === 0) return - // Save current map state before fight - if (combat) { - savedMapsRef.current[player.location] = saveCurrentMapState() - worldItemsRef.current[player.location] = groundItemsRef.current - } + // Sync current character state to global before fight + syncCombatToGlobal(player.location) const enemies = list.map((npc, i) => makeEnemy(npc, i)) const characters = {} @@ -2236,7 +2218,6 @@ const makeEnemy = (npc, index) => { playerEntityId: 'player', characters, log: [`You engage ${enemies.map(e => e.name).join(' and ')} in combat!`], - blocks: generateLocationBlocks(), } setCombat(initCombat) const locItems = worldItemsRef.current[player.location] ?? [] @@ -2269,27 +2250,17 @@ const makeEnemy = (npc, index) => { /** Enter a location — creates a combat view with all NPCs present as non-hostile */ const enterLocation = (locationId, playerData) => { const pd = playerData ?? player - - // Restore saved map if one exists for this location - const saved = savedMapsRef.current[locationId] - if (saved) { - restoreMapState(locationId, saved, pd) - const locItems = worldItemsRef.current[locationId] ?? saved.groundItems - setGroundItems(locItems); groundItemsRef.current = locItems - setCombatTime(0); setDisplayTime(0) - recordingRef.current = null; setFightRecord(null); setProjectiles([]); setHitShake(null) - speedRef.current = 1; setSpeedMult(1) - setExpandedWeapon(Object.keys(characters).filter(cid => { - const ch = characters[cid]; return ch && (ch.owner === 'player' || ch.relationships?.player === 'ally') - })); setAimMode(false); setLimbSelect(null); setSelectedSkill(null); setSelectedTarget(null); setAttackEnemy(null); setCurrentActor(null); setSkillDropdownOpen([]) - return - } + const isPlace = locationId === 'loc6' + const isPocket = locationId === POCKET_DIM_ID + const pocketDef = isPocket ? backgroundDataRef.current[POCKET_DIM_ID] : null + const ws = isPocket ? (pocketDef?.worldSize ?? 320) : isPlace ? 400 : 160 + const savedPositions = backgroundRefsRef.current[locationId]?.positions ?? {} const characters = {} characters['player'] = createCharacter({ id: 'player', name: pd.name, - pos: { x: 20, y: 75 }, + pos: savedPositions['player'] ?? (isPocket ? { x: 160, y: 160 } : isPlace ? { x: 200, y: 370 } : { x: 20, y: 75 }), integrity: { ...pd.integrity }, injuries: JSON.parse(JSON.stringify(pd.injuries)), equipped: { ...pd.equipped }, @@ -2303,49 +2274,52 @@ const makeEnemy = (npc, index) => { const liveChars = liveCharacters.filter(c => c.id !== 'player' && c.location === locationId && (!c.integrity || !isDead(c.integrity))) let xOff = 50 + let placeLIdx = 0, placeRIdx = 0 for (const lc of liveChars) { - if (lc.equipped) { - characters[lc.id] = createCharacter({ - id: lc.id, name: lc.name, - pos: { x: xOff, y: 130 }, - integrity: lc.integrity ? { ...lc.integrity } : undefined, - injuries: JSON.parse(JSON.stringify(lc.injuries ?? pd.injuries)), - equipped: { ...lc.equipped }, - weaponConditions: { ...lc.weaponConditions }, - weaponMagazines: { ...lc.weaponMagazines }, - itemCounts: { ...lc.itemCounts }, - inventory: [...(lc.inventory ?? [])], - fillColor: lc.fillColor, - relationships: lc.relationships ? { ...lc.relationships } : undefined, - owner: null, - }) - } else { - characters[lc.id] = createCharacter({ - id: lc.id, name: lc.name, - pos: { x: xOff, y: 130 }, - integrity: freshBody(), - injuries: freshInjuries(), - equipped: { left: 'fists', right: 'fists' }, - weaponConditions: {}, - weaponMagazines: {}, - itemCounts: { ...(lc.itemCounts ?? {}) }, - inventory: [...(lc.inventory ?? [])], - fillColor: lc.fillColor, - relationships: lc.relationships ? { ...lc.relationships } : undefined, - owner: null, - }) + const savedPos = savedPositions[lc.id] + let pos = savedPos ? { x: savedPos.x, y: savedPos.y } : { x: xOff, y: 130 } + if (isPlace && lc.id.startsWith('place_l')) { + pos = { x: 30, y: 30 + placeLIdx * (340 / 9) } + placeLIdx++ + } else if (isPlace && lc.id.startsWith('place_r')) { + pos = { x: ws - 30, y: 30 + placeRIdx * (340 / 9) } + placeRIdx++ + } else if (isPocket) { + pos = { x: 80 + Math.random() * 160, y: 80 + Math.random() * 160 } + } else if (!savedPos) { + xOff += 25 } - xOff += 25 + characters[lc.id] = createCharacter({ + id: lc.id, name: lc.name, + pos, + integrity: lc.integrity ? { ...lc.integrity } : freshBody(), + injuries: lc.injuries ? JSON.parse(JSON.stringify(lc.injuries)) : freshInjuries(), + equipped: lc.equipped ? { left: lc.equipped.left, right: lc.equipped.right } : { left: 'fists', right: 'fists' }, + weaponConditions: lc.weaponConditions ? { ...lc.weaponConditions } : {}, + weaponMagazines: lc.weaponMagazines ? { ...lc.weaponMagazines } : {}, + itemCounts: lc.itemCounts ? { ...lc.itemCounts } : {}, + inventory: [...(lc.inventory ?? [])], + fillColor: lc.fillColor, + relationships: lc.relationships ? { ...lc.relationships } : undefined, + owner: null, + }) } const locName = locMap[locationId]?.name ?? locationId const initCombat = { playerEntityId: 'player', characters, - log: [`You arrive at ${locName}.`], - blocks: generateLocationBlocks(), + log: [isPocket ? `DEBUG: Entered pocket dimension.` : `You arrive at ${locName}.`], } setCombat(initCombat) + if (!isPocket) { + for (const id of [...activeLocationsRef.current]) { + if (id !== locationId && id !== POCKET_DIM_ID) deactivateBgLocation(id) + } + } + const locBlocks = isPocket ? (pocketDef?.blocks ?? [{ x: 130, y: 130, w: 60, h: 60 }]) : isPlace ? [] : generateLocationBlocks() + const bgData = { blocks: locBlocks, worldSize: ws, characters, groundItems: [], projectiles: [], splats: [] } + activateBgLocation(locationId, bgData, Object.keys(characters)) const locItems = worldItemsRef.current[locationId] ?? [] setGroundItems(locItems); groundItemsRef.current = locItems setCombatTime(0); setDisplayTime(0) @@ -2379,89 +2353,47 @@ const makeEnemy = (npc, index) => { }) if (deadIds.length > 0) setDefeatedNpcs(prev => [...new Set([...prev, ...deadIds])]) - // Sync party member combat state back to world - const cc = combatRef.current - if (cc) { - const partyCids = Object.keys(cc.characters).filter(cid => { - const ch = cc.characters[cid] - return ch && (ch.owner === 'player' || ch.relationships?.player === 'ally') - }) - setCharacterOverrides(prev => { - const next = { ...prev } - for (const pid of partyCids) { - if (pid === 'player') continue - const combatChar = cc.characters[pid] - if (combatChar) { - next[pid] = { - weaponConditions: { ...combatChar.weaponConditions }, - itemCounts: { ...combatChar.itemCounts }, - weaponMagazines: { ...combatChar.weaponMagazines }, - inventory: [...(combatChar.inventory ?? [])], - integrity: { ...combatChar.integrity }, - injuries: JSON.parse(JSON.stringify(combatChar.injuries)), - } - } - } - return next - }) - for (const pid of partyCids) { - if (pid === 'player') continue - if (isDead(cc.characters[pid]?.integrity)) { - setDefeatedNpcs(prev => [...new Set([...prev, pid])]) - } + // Sync ALL combat character state back to world + syncCombatToGlobal(player.location) + for (const [cid, ch] of Object.entries(combat.characters)) { + if (cid === 'player') continue + if (isDead(ch.integrity)) { + setDefeatedNpcs(prev => [...new Set([...prev, cid])]) } } } } - /** Save the current map (combat) state for a location */ - const saveCurrentMapState = () => { - if (!combat) return null + /** Sync all combat characters at a location back to global character state */ + const syncCombatToGlobal = (locationId) => { + if (!combat) return const cc = combatRef.current ?? combat - return { - playerEntityId: cc.playerEntityId, - characters: JSON.parse(JSON.stringify(cc.characters)), - log: [...cc.log], - blocks: cc.blocks, - groundItems: [...(groundItemsRef.current ?? [])], + worldItemsRef.current[locationId] = groundItemsRef.current + const updates = {} + for (const [cid, ch] of Object.entries(cc.characters)) { + if (cid === 'player') continue + updates[cid] = { + pos: ch.pos ? { x: ch.pos.x, y: ch.pos.y } : undefined, + integrity: ch.integrity ? { ...ch.integrity } : undefined, + injuries: ch.injuries ? JSON.parse(JSON.stringify(ch.injuries)) : undefined, + equipped: ch.equipped ? { left: ch.equipped.left, right: ch.equipped.right } : undefined, + weaponConditions: ch.weaponConditions ? { ...ch.weaponConditions } : undefined, + weaponMagazines: ch.weaponMagazines ? { ...ch.weaponMagazines } : undefined, + itemCounts: ch.itemCounts ? { ...ch.itemCounts } : undefined, + inventory: ch.inventory ? [...ch.inventory] : undefined, + relationships: ch.relationships ? { ...ch.relationships } : undefined, + money: ch.money ?? undefined, + } } - } - - /** Restore a saved map state, updating player with current data */ - const restoreMapState = (locationId, saved, playerData) => { - const pd = playerData ?? player - const savedPlayer = saved.characters['player'] - saved.characters['player'] = createCharacter({ - id: 'player', name: pd.name, - pos: savedPlayer?.pos ?? { x: 20, y: 75 }, - integrity: { ...pd.integrity }, - injuries: JSON.parse(JSON.stringify(pd.injuries)), - equipped: { ...pd.equipped }, - weaponConditions: { ...pd.weaponConditions }, - weaponMagazines: { ...pd.weaponMagazines }, - itemCounts: { ...pd.itemCounts }, - relationships: { ...(pd.relationships ?? {}) }, - owner: 'player', - fillColor: pd.fillColor, + setCharacterOverrides(prev => { + const next = { ...prev } + for (const [cid, data] of Object.entries(updates)) { + next[cid] = { ...(prev[cid] ?? {}), ...data } + } + return next }) - setCombat({ - playerEntityId: saved.playerEntityId, - characters: saved.characters, - log: [`You arrive at ${locMap[locationId]?.name ?? locationId}.`], - blocks: saved.blocks, - }) - setGroundItems(saved.groundItems) - groundItemsRef.current = saved.groundItems - setCombatTime(0); setDisplayTime(0) - characterPositionsRef.current = {} - characterVelocitiesRef.current = {} - characterDirsRef.current = {} - characterActionsRef.current = {} - for (const cid of Object.keys(saved.characters)) { - characterPositionsRef.current[cid] = { ...saved.characters[cid].pos } - characterVelocitiesRef.current[cid] = { x: 0, y: 0 } - characterDirsRef.current[cid] = { dx: 0, dy: 1 } - characterActionsRef.current[cid] = null + for (const [cid, data] of Object.entries(updates)) { + characterOverridesRef.current[cid] = { ...(characterOverridesRef.current[cid] ?? {}), ...data } } } @@ -2560,131 +2492,60 @@ const makeEnemy = (npc, index) => { } const debugPocketDimension = (targetId) => { - const cc = combatRef.current - if (!cc) return const locId = player.location - if (locId?.startsWith?.('pocket_')) { + const cc = combatRef.current + // ── EXIT pocket dimension ── + if (locId === POCKET_DIM_ID) { const returnLoc = pocketReturnRef.current - if (returnLoc) { - const restored = [] - for (const npcId of Object.keys(pocketNPCsRef.current)) { - const origLoc = pocketNPCsRef.current[npcId] - setCharacterOverrides(prev => { - const next = { ...prev }; const existing = next[npcId] - if (existing) next[npcId] = { ...existing, location: origLoc } - else next[npcId] = { location: origLoc } - return next - }) - restored.push(npcId) - } - pocketNPCsRef.current = {} - pocketNPCDataRef.current = {} - pocketDimIdRef.current = null - enterLocation(returnLoc) - setMinimapView({ x: 0, y: 0, scale: 1 }) - const extra = restored.length ? ` (restored ${restored.map(id => cc.characters[id]?.name ?? id).join(', ')})` : '' - setCombat(c => c ? { ...c, log: [...c.log, `DEBUG: Returned from pocket dimension.${extra}`] } : c) - } + if (!returnLoc) return + pocketReturnRef.current = null + syncCombatToGlobal(locId) + setPlayer(p => ({ ...p, location: returnLoc })) + setMinimapView({ x: 0, y: 0, scale: 1 }); setViewingLoc(null) + enterLocation(returnLoc) return } - if (targetId && targetId !== 'player') { - const npcChar = cc.characters[targetId] - if (npcChar && !isDead(npcChar.integrity)) { - if (!pocketDimIdRef.current) pocketDimIdRef.current = `pocket_${Date.now()}` - const pocketId = pocketDimIdRef.current - pocketNPCDataRef.current[targetId] = { - id: targetId, name: npcChar.name, - integrity: { ...npcChar.integrity }, - injuries: JSON.parse(JSON.stringify(npcChar.injuries ?? playerRef.current.injuries)), - equipped: { ...(npcChar.equipped ?? { left: 'fists', right: 'fists' }) }, - weaponConditions: { ...(npcChar.weaponConditions ?? {}) }, - weaponMagazines: { ...(npcChar.weaponMagazines ?? {}) }, - itemCounts: { ...(npcChar.itemCounts ?? {}) }, - inventory: [...(npcChar.inventory ?? [])], - relationships: { ...(npcChar.relationships ?? {}) }, - size: npcChar.size, - owner: null, - fillColor: npcChar.fillColor, - } - pocketNPCsRef.current[targetId] = locId - setCharacterOverrides(prev => ({ - ...prev, [targetId]: { ...prev[targetId], location: pocketId } - })) - setCombat(c => { - if (!c) return c - const chars = { ...c.characters } - delete chars[targetId] - return { ...c, characters: chars, log: [...c.log, `DEBUG: ${npcChar.name ?? targetId} sent to pocket dimension.`] } - }) - } + // ── MOVE character to pocket dimension ── + const charId = targetId || 'player' + const isPlayerChar = charId === 'player' + if (isPlayerChar) { + if (!cc) return + pocketReturnRef.current = locId + syncCombatToGlobal(locId) + setPlayer(p => ({ ...p, location: POCKET_DIM_ID })) + enterLocation(POCKET_DIM_ID) return } - pocketReturnRef.current = locId - savedMapsRef.current[locId] = saveCurrentMapState() - worldItemsRef.current[locId] = groundItemsRef.current - if (!pocketDimIdRef.current) pocketDimIdRef.current = `pocket_${Date.now()}` - const pocketId = pocketDimIdRef.current - const pos = { x: 80, y: 80 } - const pd = playerRef.current - const pocketChars = {} - pocketChars['player'] = createCharacter({ - id: 'player', name: pd.name, - pos, - integrity: { ...pd.integrity }, - injuries: JSON.parse(JSON.stringify(pd.injuries)), - equipped: { ...pd.equipped }, - weaponConditions: { ...pd.weaponConditions }, - weaponMagazines: { ...pd.weaponMagazines }, - itemCounts: { ...pd.itemCounts }, - relationships: { ...(pd.relationships ?? {}) }, - owner: 'player', - fillColor: pd.fillColor, + // Send NPC to pocket dimension: just change their location + const npcChar = cc?.characters[targetId] + if (!npcChar || isDead(npcChar.integrity)) return + pocketDimBgRefs.positions[targetId] = npcChar.pos ? { ...npcChar.pos } : { x: 160, y: 160 } + pocketDimBgRefs.velocities[targetId] = { x: 0, y: 0 } + pocketDimBgRefs.dirs[targetId] = { dx: 0, dy: 1 } + pocketDimBgRefs.actions[targetId] = null + setCharacterOverrides(prev => ({ ...prev, [targetId]: { ...prev[targetId], location: POCKET_DIM_ID } })) + characterOverridesRef.current[targetId] = { ...(characterOverridesRef.current[targetId] ?? {}), location: POCKET_DIM_ID } + if (!activeLocationsRef.current.includes(POCKET_DIM_ID)) { + activeLocationsRef.current = [...activeLocationsRef.current, POCKET_DIM_ID] + } + // Remove NPC from current location's background refs + const playerBgRefsLoc = backgroundRefsRef.current[locId] + if (playerBgRefsLoc) { + delete playerBgRefsLoc.positions[targetId] + delete playerBgRefsLoc.velocities[targetId] + delete playerBgRefsLoc.dirs[targetId] + delete playerBgRefsLoc.actions[targetId] + } + delete characterPositionsRef.current[targetId] + delete characterVelocitiesRef.current[targetId] + delete characterDirsRef.current[targetId] + delete characterActionsRef.current[targetId] + setCombat(c => { + if (!c) return c + const chars = { ...c.characters } + delete chars[targetId] + return { ...c, characters: chars, log: [...c.log, `DEBUG: ${npcChar.name ?? targetId} sent to pocket dimension.`] } }) - let xOff = 105 - for (const npcId of Object.keys(pocketNPCDataRef.current)) { - const data = pocketNPCDataRef.current[npcId] - pocketChars[npcId] = createCharacter({ - id: data.id, name: data.name, - pos: { x: xOff, y: 80 }, - integrity: data.integrity, - injuries: data.injuries, - equipped: data.equipped, - weaponConditions: data.weaponConditions, - weaponMagazines: data.weaponMagazines, - itemCounts: data.itemCounts, - inventory: data.inventory, - relationships: data.relationships, - size: data.size, - owner: data.owner, - fillColor: data.fillColor, - }) - xOff += 25 - } - const initCombat = { - playerEntityId: 'player', - characters: pocketChars, - log: [`DEBUG: Entered pocket dimension.${Object.keys(pocketNPCDataRef.current).length ? ` (${Object.keys(pocketNPCDataRef.current).length} NPCs present)` : ''}`], - blocks: [], - } - setCombat(initCombat) - setMinimapView({ x: 0, y: 0, scale: 0.6 }) - setPlayer(p => ({ ...p, location: pocketId })) - setGroundItems([]); groundItemsRef.current = [] - setCombatTime(0); setDisplayTime(0) - characterActionsRef.current = {} - characterPositionsRef.current = {} - characterVelocitiesRef.current = {} - characterDirsRef.current = {} - for (const cid of Object.keys(pocketChars)) { - characterActionsRef.current[cid] = null - characterPositionsRef.current[cid] = pocketChars[cid].pos - characterVelocitiesRef.current[cid] = { x: 0, y: 0 } - characterDirsRef.current[cid] = { dx: 0, dy: 1 } - } - recordingRef.current = null; setFightRecord(null) - setProjectiles([]); setHitShake(null) - speedRef.current = 1; setSpeedMult(1) - setAimMode(false); setLimbSelect(null); setSelectedSkill(null); setSelectedTarget(null); setAttackEnemy(null); setCurrentActor(null); setSkillDropdownOpen([]) } const startTimer = () => { @@ -2748,7 +2609,7 @@ const makeEnemy = (npc, index) => { const dist = Math.sqrt((targetX - curPos.x) ** 2 + (targetY - curPos.y) ** 2) if (dist < 1) return const pct = moveMode === 'run' ? 1.0 : 0.5 - const moveAction = new MoveAction({ type: 'move', charId: actorId, fromX: curPos.x, fromY: curPos.y, toX: targetX, toY: targetY, speedMult: pct, blocks: combat.blocks }) + const moveAction = new MoveAction({ type: 'move', charId: actorId, fromX: curPos.x, fromY: curPos.y, toX: targetX, toY: targetY, speedMult: pct, blocks: backgroundDataRef.current[player.location]?.blocks ?? [] }) const dx = targetX - curPos.x, dy = targetY - curPos.y const d = Math.sqrt(dx * dx + dy * dy) const dir = d > 0 ? { dx: dx / d, dy: dy / d } : { dx: 0, dy: 1 } @@ -2950,11 +2811,8 @@ const makeEnemy = (npc, index) => { const travel = (targetId) => { if (travelAnim) return - // Save current map state before leaving - if (combat) { - savedMapsRef.current[player.location] = saveCurrentMapState() - worldItemsRef.current[player.location] = groundItemsRef.current - } + // Sync all character state to global before leaving + syncCombatToGlobal(player.location) const from = locations.find(l => l.id === player.location) const to = locations.find(l => l.id === targetId) if (!from || !to) return @@ -3520,7 +3378,54 @@ const makeEnemy = (npc, index) => {
- { + const orderedIds = activeLocationsRef.current.includes(player.location) + ? activeLocationsRef.current + : [player.location, ...activeLocationsRef.current] + return ( +
+ {orderedIds.map(id => { + const isCurrent = id === player.location + const loc = locations.find(l => l.id === id) + const name = loc ? loc.name : (id?.startsWith?.('pocket_') ? 'Pocket' : id) + const isActive = isCurrent ? !viewingLoc : viewingLoc === id + const color = isCurrent ? '#22c55e' : '#f59e0b' + return ( + + ) + })} + {hoveredLocName && ( +
{hoveredLocName}
+ )} +
+ ) + })()} + + { if (e.button !== 0) return @@ -3534,7 +3439,8 @@ const makeEnemy = (npc, index) => { const dx = ev.clientX - minimapPanStart.current.x, dy = ev.clientY - minimapPanStart.current.y if (Math.abs(dx) > 2 || Math.abs(dy) > 2) minimapMovedRef.current = true const r = svg.getBoundingClientRect() - setMinimapView(v => ({ ...v, x: minimapPanStart.current.vx - (dx / r.width) * 160 / v.scale, y: minimapPanStart.current.vy - (dy / r.height) * 160 / v.scale })) + const ws = viewingLoc ? (backgroundDataRef.current[viewingLoc]?.worldSize ?? (viewingLoc?.startsWith?.('pocket_') ? 320 : 160)) : (combatRef.current?.worldSize ?? 160) + setMinimapView(v => ({ ...v, x: minimapPanStart.current.vx - (dx / r.width) * ws, y: minimapPanStart.current.vy - (dy / r.height) * ws })) } const onUp = () => { minimapPanRef.current = false; window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp) } window.addEventListener('mousemove', onMove); window.addEventListener('mouseup', onUp) @@ -3543,8 +3449,9 @@ const makeEnemy = (npc, index) => { if (minimapPanRef.current) return const r = e.currentTarget.getBoundingClientRect() const mv = minimapViewRef.current - const mx = ((e.clientX - r.left) / r.width) * 160 / mv.scale + mv.x - const my = ((e.clientY - r.top) / r.height) * 160 / mv.scale + mv.y + const ws = viewingLoc ? (backgroundDataRef.current[viewingLoc]?.worldSize ?? (viewingLoc?.startsWith?.('pocket_') ? 320 : 160)) : (combatRef.current?.worldSize ?? 160) + const mx = ((e.clientX - r.left) / r.width) * ws / mv.scale + mv.x + const my = ((e.clientY - r.top) / r.height) * ws / mv.scale + mv.y setMinimapHover({ x: mx, y: my }) }} onPointerLeave={() => setMinimapHover(null)} @@ -3553,8 +3460,9 @@ const makeEnemy = (npc, index) => { if (!moveModeRef.current && !aimModeRef.current) return const r = e.currentTarget.getBoundingClientRect() const mv = minimapViewRef.current - const tx = ((e.clientX - r.left) / r.width) * 160 / mv.scale + mv.x - const ty = ((e.clientY - r.top) / r.height) * 160 / mv.scale + mv.y + const ws = viewingLoc ? (backgroundDataRef.current[viewingLoc]?.worldSize ?? (viewingLoc?.startsWith?.('pocket_') ? 320 : 160)) : (combatRef.current?.worldSize ?? 160) + const tx = ((e.clientX - r.left) / r.width) * ws / mv.scale + mv.x + const ty = ((e.clientY - r.top) / r.height) * ws / mv.scale + mv.y if (aimModeRef.current) { if (selectedSkillRef.current !== 'Shoot' || limbSelectRef.current !== 'pistol') { const cc = combatRef.current @@ -3601,19 +3509,72 @@ const makeEnemy = (npc, index) => { transformOrigin: '0 0', transition: hitShake ? 'none' : undefined, }}> - - {(combat?.blocks ?? []).map((block, i) => )} + {(() => { + const isViewingBg = !!viewingLoc + const renderData = isViewingBg + ? (() => { + const bd = backgroundDataRef.current[viewingLoc] + const br = backgroundRefsRef.current[viewingLoc] + if (!bd || !br) return null + const chars = {} + for (const c of characters) { + const override = characterOverridesRef.current[c.id] + const merged = override ? { ...c, ...override } : c + if (merged.location !== viewingLoc) continue + const integrity = merged.integrity ?? freshBody() + const pos = br.positions[c.id] ?? merged.pos ?? { x: 20, y: 75 } + const inj = merged.injuries ? JSON.parse(JSON.stringify(merged.injuries)) : freshInjuries() + chars[c.id] = { ...merged, integrity: { ...integrity }, injuries: inj, pos: { ...pos } } + } + return { + worldSize: bd.worldSize, + blocks: bd.blocks, + characters: chars, + dirs: br.dirs, + projectiles: bd.projectiles ?? [], + groundItems: bd.groundItems ?? [], + splats: bd.splats ?? [], + } + })() + : (() => { + const locData = backgroundDataRef.current[player.location] + return combat + ? { + worldSize: locData?.worldSize ?? 160, + blocks: locData?.blocks ?? [], + characters: combat.characters, + dirs: characterDirsRef.current, + projectiles: projectiles, + groundItems: groundItems, + splats: bloodSplats, + } + : null + })() + if (!renderData) return null + const ws = renderData.worldSize ?? 160 + const chars = renderData.characters ?? {} + const dirs = renderData.dirs ?? {} + return ( + <> + {(renderData.blocks ?? []).map((block, i) => )} + {/* Blood splats */} + {(renderData.splats ?? []).map((s, i) => ( + + ))} {/* All character circles */} - {combat && Object.keys(combat.characters).map(cid => { - const ch = combat.characters[cid] + {(() => { if (combat && player.location === 'pocket_dim') { const charIds = Object.keys(chars); const npcsAtPocket = characters.filter(c => characterOverridesRef.current[c.id]?.location === POCKET_DIM_ID || c.location === POCKET_DIM_ID).map(c => c.id); if (charIds.length <= 1) console.log('[POCKET RENDER] chars:', charIds, 'combat has player?', !!combat.characters['player'], 'npcs at pocket:', npcsAtPocket) } })()} + {Object.keys(chars).map(cid => { + const ch = chars[cid] if (!ch) return null const dead = isDead(ch.integrity) - const isShaking = hitShake?.charId === cid + const isShaking = !isViewingBg && hitShake?.charId === cid const cx = isShaking ? ch.pos.x + (Math.random() - 0.5) * 1.5 : ch.pos.x const cy = isShaking ? ch.pos.y + (Math.random() - 0.5) * 1.5 : ch.pos.y const chSize = ch.size ?? 1 const radius = ch.owner === 'player' ? 4 * chSize : 5 * chSize - const isTargetable = aimMode && !dead && (selectedSkill !== 'Shoot' || limbSelect !== 'pistol') + const isTargetable = !isViewingBg && aimMode && !dead && (selectedSkill !== 'Shoot' || limbSelect !== 'pistol') const hostileToPlayer = ch.relationships?.player === 'enemy' const ownerStroke = ch.owner === 'player' ? '#3498db' : hostileToPlayer ? '#e74c3c' : '#22c55e' return ( @@ -3634,12 +3595,12 @@ const makeEnemy = (npc, index) => { onContextMenu={e => { e.preventDefault(); e.stopPropagation(); setHoverInfo(null); setHoveredChar(null); setContextMenu({ x: e.clientX, y: e.clientY, charId: cid }) }} /> ) })} - {combat && selectedSkill && limbSelect && (() => { + {!isViewingBg && combat && selectedSkill && limbSelect && (() => { const rangeActor = currentActor ?? 'player' const rp = combat.characters[rangeActor]?.pos ?? combat.characters['player'].pos return })()} - {moveMode && moveMode !== 'turn' && minimapHover && combat && (() => { + {!isViewingBg && moveMode && moveMode !== 'turn' && minimapHover && combat && (() => { const actorId = currentActor ?? 'player' const ch = combat.characters[actorId] if (!ch?.pos) return null @@ -3648,7 +3609,7 @@ const makeEnemy = (npc, index) => { stroke="#22c55e" strokeWidth="1.5" strokeDasharray="3 3" opacity="0.7" /> ) })()} - {aimMode && minimapHover && combat && limbSelect === 'pistol' && (() => { + {!isViewingBg && aimMode && minimapHover && combat && limbSelect === 'pistol' && (() => { const actorId = currentActor ?? 'player' const ch = combat.characters[actorId] if (!ch?.pos) return null @@ -3668,7 +3629,7 @@ const makeEnemy = (npc, index) => { 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 blocks = backgroundDataRef.current[player.location]?.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) @@ -3683,8 +3644,34 @@ const makeEnemy = (npc, index) => { ) })()} - {/* Weapon circles for all characters */} - {combat && (() => { + {(renderData.projectiles ?? []).map(p => ( + + ))} + {(renderData.groundItems ?? []).map(gi => ( + { e.preventDefault(); e.stopPropagation(); setContextMenu({ x: e.clientX, y: e.clientY, groundId: gi.id, itemId: gi.itemId }) }} /> + ))} + {Object.keys(chars).map(cid => { + const ch = chars[cid] + if (!ch || isDead(ch.integrity)) return null + const d = dirs[cid] ?? { dx: 0, dy: 1 } + const isShaking = !isViewingBg && hitShake?.charId === cid + const cx = isShaking ? ch.pos.x + (Math.random() - 0.5) * 1.5 : ch.pos.x + const cy = isShaking ? ch.pos.y + (Math.random() - 0.5) * 1.5 : ch.pos.y + const isHovered = hoveredChar === cid + const px = -d.dy, py = d.dx + const arrowColor = ch.owner === 'player' ? '#3498db' : ch.relationships?.player === 'enemy' ? '#e74c3c' : '#22c55e' + return ( + + ) + })} + {(() => { const wpnOff = (dir, side, off) => { if (side === 'front') return { x: dir.dx * off, y: dir.dy * off } if (side === 'left') return { x: dir.dy * off, y: -dir.dx * off } @@ -3693,12 +3680,12 @@ const makeEnemy = (npc, index) => { const isFist = w => w === 'fists' || w === 'left_arm' || w === 'right_arm' const isAtk = t => [ANIM_TYPES.SLASH, ANIM_TYPES.PIERCE_STRIKE, ANIM_TYPES.FIST_STRIKE, ANIM_TYPES.FIST_HOOK, ANIM_TYPES.OVERHEAD].includes(t) const allSlots = [] - Object.keys(combat.characters).forEach(cid => { - const ch = combat.characters[cid] + Object.keys(chars).forEach(cid => { + const ch = chars[cid] if (!ch || isDead(ch.integrity)) return - const dir = characterDirsRef.current[cid] ?? { dx: 0, dy: 1 } + const dir = dirs[cid] ?? { dx: 0, dy: 1 } const off = 7 - const shake = hitShake?.charId === cid + const shake = !isViewingBg && hitShake?.charId === cid const bx = shake ? ch.pos.x + (Math.random() - 0.5) * 1.5 : ch.pos.x const by = shake ? ch.pos.y + (Math.random() - 0.5) * 1.5 : ch.pos.y const chPos = ch.pos @@ -3735,37 +3722,9 @@ const makeEnemy = (npc, index) => { }) const flat = allSlots.map(s => ) return flat + })()} + ) })()} - {/* Projectile dots */} - {projectiles.map(p => ( - - ))} - {/* Ground items */} - {groundItems.map(gi => ( - { e.preventDefault(); e.stopPropagation(); setContextMenu({ x: e.clientX, y: e.clientY, groundId: gi.id, itemId: gi.itemId }) }} /> - ))} - {/* Direction arrows for all characters */} - {combat && Object.keys(combat.characters).map(cid => { - const ch = combat.characters[cid] - if (!ch || isDead(ch.integrity)) return null - const d = characterDirsRef.current[cid] ?? { dx: 0, dy: 1 } - const isShaking = hitShake?.charId === cid - const cx = isShaking ? ch.pos.x + (Math.random() - 0.5) * 1.5 : ch.pos.x - const cy = isShaking ? ch.pos.y + (Math.random() - 0.5) * 1.5 : ch.pos.y - const isHovered = hoveredChar === cid - const px = -d.dy, py = d.dx - const arrowColor = ch.owner === 'player' ? '#3498db' : ch.relationships?.player === 'enemy' ? '#e74c3c' : '#22c55e' - return ( - - ) - })}
@@ -3805,7 +3764,7 @@ const makeEnemy = (npc, index) => { onMouseEnter={() => setHovered(l.id)} onMouseLeave={() => setHovered(null)} /> {l.name} - {!travelAnim && player.location === l.id && ( + {!travelAnim && player.location === l.id && !player.location?.startsWith?.('pocket_') && ( <> P )} diff --git a/src/assets/splat.png b/src/assets/splat.png new file mode 100644 index 0000000000000000000000000000000000000000..39cb7aa26aabfd965517ce8c4d340c3ef56f6b6a GIT binary patch literal 37907 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 literal 0 HcmV?d00001