feat: unify territory encounter vertical slice - #115
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_391c58a4-ac26-4b28-85fe-c9df95679924) |
723d02d to
be7c8e1
Compare
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_1512c612-ea56-431f-bd5b-72d8be147968) |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 723d02dd82
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (!selectedBlockId) return; | ||
| const activeBlock = blocks[selectedBlockId]; | ||
| applyEncounterResult(selectedBlockId, result); | ||
| updatePlayer({ heat: Math.max(0, Math.min(5, (player.heat ?? 0) + result.heatDelta)) }); |
There was a problem hiding this comment.
Keep global player heat on its 0–100 scale
When an encounter resolves while the player's global heat exceeds 5, this clamps that global value to 5 even though usePlayerStore.updateHeat and the HUD define it on a 0–100 scale. For example, a player at 60 heat who secures the encounter is reset to 5, effectively clearing raid pressure; translate the encounter's block-scale delta before applying it to global heat rather than applying the block's 0–5 bound.
Useful? React with 👍 / 👎.
| name: 'Scout', | ||
| team: 'crew', | ||
| role: 'recruit', | ||
| position: findSafePoint(terrain), |
There was a problem hiding this comment.
Spawn the fallback crew away from the objective
For a block with no healthy placements, the fallback scout is positioned at findSafePoint(terrain), and line 135 independently chooses the extraction with that same deterministic helper. The scout therefore starts directly on the exit, so the initial “Secure exit” action immediately produces a secured result and income without movement or combat.
Useful? React with 👍 / 👎.
|
|
||
| function runOppositionTurn(session: CombatSession): CombatSession { | ||
| let next = session; | ||
| const crew = next.combatants.filter((actor) => actor.team === 'crew' && !actor.isDown); |
There was a problem hiding this comment.
Refresh crew state between opposition attacks
When multiple opposition actors attack the same crew member in one turn, this array retains the pre-turn combatant objects. Each later applyDamage therefore subtracts from the original health and overwrites the damage written by earlier attackers; several successful shots can leave the target with only one shot's damage. Resolve each target from the current next session before applying an attack.
Useful? React with 👍 / 👎.
|
|
||
| export function prepareEncounter(block: BlockData): EncounterPreparation { | ||
| const resolved = resolveBlockDNA(block.lat, block.lng, block.address); | ||
| const seed = hashString(`${block.id}:${resolved.seed}:${block.heat}:${block.morale}`); |
There was a problem hiding this comment.
Give separate encounters distinct persistence identities
The encounter seed—and consequently sessionId and the result idempotency key—depends only on block ID, DNA seed, heat, and morale. Once consequences are clamped, such as a secured encounter at heat 5 and morale 100, every subsequent legitimate encounter has the same key, so applyEncounterResult treats future victories as replays and silently drops their income and other consequences.
Useful? React with 👍 / 👎.
| const terrain = toTerrain(block, resolved.dna.globalCoverBonus, resolved.zoneLayout); | ||
| const crew = toCrew(block.placements); | ||
| const fallbackCrew: Combatant[] = crew.length > 0 ? crew : [{ | ||
| id: 'crew-scout', |
There was a problem hiding this comment.
Exclude the synthetic scout from persistent casualty handling
If the no-placement fallback scout is downed, its synthetic ID crew-scout is included in crewDown; BlockModeView then creates a hospital incident for that nonexistent roster member. The player can be charged $2,200 for “Crew member” even though updateMember cannot heal anyone, or incur a morale penalty for abandoning an actor that was never part of the gang.
Useful? React with 👍 / 👎.
| {showEncounter ? ( | ||
| <UnifiedEncounter | ||
| block={block} | ||
| onResolved={handleEncounterResolved} | ||
| onClose={() => setShowEncounter(false)} | ||
| /> |
There was a problem hiding this comment.
Let view tabs actually close the encounter
Once showEncounter is true, the content branch always renders UnifiedEncounter, but the Top-Down, Street, and Drugs tab handlers do not clear that flag. Clicking those tabs changes the underlying viewMode and active-tab styling while leaving the encounter on screen, so the user cannot navigate to those views through their corresponding controls until they separately close the encounter.
Useful? React with 👍 / 👎.
| const target = this.session.combatants | ||
| .filter((candidate) => candidate.team === 'opposition' && !candidate.isDown) | ||
| .sort((left, right) => this.manhattan(actor.position, left.position) - this.manhattan(actor.position, right.position))[0]; | ||
| if (target) this.dispatch({ type: 'aim-fire', actorId: actor.id, targetId: target.id }); |
There was a problem hiding this comment.
Choose the nearest valid target for keyboard firing
The keyboard, gamepad, and React “Fire nearest” controls select solely by Manhattan distance. If the closest opponent is outside range or behind an impassable line-of-sight cell while a slightly farther opponent is shootable, the command is blocked and these input modes cannot fire at the valid target; filter candidates by the same range and line-of-sight rules before selecting the nearest one.
Useful? React with 👍 / 👎.
| const targetCell = cellAt(session, target.position); | ||
| const rangePenalty = Math.max(0, distance(source.position, target.position) - 1) * 0.045; | ||
| const hitChance = Math.max(0.18, Math.min(0.9, 0.76 + source.level * 0.025 - (targetCell?.cover ?? 0) * 0.35 - rangePenalty)); |
There was a problem hiding this comment.
Apply exposure to the advertised combat risk
Although encounter preparation derives an exposure value for every terrain cell and presents exposure percentages as tactical modifiers, hit resolution only reads the target cell's cover. Two positions with equal cover but radically different exposure therefore have identical combat risk, so placement exposure and DNA exposure changes do not deliver the tactical effect shown to the player.
Useful? React with 👍 / 👎.
| name: index === 0 ? 'Lookout' : `Rival ${index + 1}`, | ||
| team: 'opposition' as const, | ||
| role: 'opposition' as const, | ||
| position: findOppositionStart(terrain, seed, index), |
There was a problem hiding this comment.
Reserve crew cells before spawning opposition
Opposition starts are selected without excluding the positions already occupied by prepared crew. Whenever a deployed member occupies one of the seeded cells in rows 0–3, both teams begin on the same tile, their sprites and health bars overlap, and normal movement cannot enter or disentangle that occupied tile even though both actors can shoot each other at distance zero.
Useful? React with 👍 / 👎.
| if (!resolved || loaded.has(resolved.url)) continue; | ||
| const key = this.textureKey(actor); | ||
| this.load.image(key, resolved.url); | ||
| loaded.add(resolved.url); |
There was a problem hiding this comment.
Load a texture key for every resolved crew role
The preload loop deduplicates by resolved URL but rendering looks textures up by role-based keys. Roles such as chemist, runner, and boss deliberately resolve to another role's asset; if that source role was loaded first, the fallback URL is skipped and no texture is registered under the later role's key, causing that crew member to render as a colored circle despite a valid resolved asset.
Useful? React with 👍 / 👎.
# Conflicts: # frontend/src/components/map/BlockModeView.tsx
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_c2c73a11-58f1-4a85-b4f0-62a9319b9d82) |
Player impact
Adds a privacy-safe territory-to-encounter vertical slice: a location-inspired tactical briefing, a deterministic Phaser top-down encounter, accessible controls, readable shooter feedback, and persistent crew/block consequences.
Implementation
CombatSessionwith seeded commands, cover, line of sight, reload, objective, retreat, and result contracts.Evidence
npm run validate: 617 passing tests; typecheck and asset audit pass.Follow-up
Resolve the bootstrap/preview issue and run the production build in a higher-memory CI runner before merging.
Note
Medium Risk
New combat simulation now drives durable block heat, morale, income, and crew health, so bugs can persist bad territory outcomes. Combat remains local/offline and legacy drive-by/raid paths stay as rollback.
Overview
Players can now launch a unified top-down encounter from a claimed block instead of the primary Drive-By/Slide action. The scene is prepared from block DNA, placements, cover, heat, and morale, then resolved into persistent crew and territory consequences.
A pure TypeScript
CombatSessionowns movement, LOS/cover, fire/reload, extraction, retreat, and seeded RNG. Phaser only renders and captures input; React owns briefing, HUD, reduced-motion, and the result dialog.blockStore.applyEncounterResultapplies each outcome once (crew downed, heat/morale/income clamps). Nearby curated DNA now wins over generic address keywords so authored local character carries into terrain.Legacy drive-by and raid screens remain in the tree as rollback. Visual acceptance is still pending because preview bootstrap stayed blank locally.
Reviewed by Cursor Bugbot for commit e3a2c83. Bugbot is set up for automated code reviews on this repo. Configure here.