Skip to content

feat: unify territory encounter vertical slice - #115

Merged
BrandDead merged 2 commits into
main-tL2525from
feat/unified-territory-encounter
Aug 21, 2026
Merged

feat: unify territory encounter vertical slice#115
BrandDead merged 2 commits into
main-tL2525from
feat/unified-territory-encounter

Conversation

@BrandDead

@BrandDead BrandDead commented Aug 20, 2026

Copy link
Copy Markdown
Owner

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

  • Adds one pure CombatSession with seeded commands, cover, line of sight, reload, objective, retreat, and result contracts.
  • Routes the existing Block screen’s primary Encounter action into the unified Phaser scene while leaving legacy modes available as rollback paths.
  • Makes nearby curated location DNA take precedence over generic address keywords so authored local character carries into encounter terrain.
  • Applies results idempotently to block heat, morale, income, and recovery follow-up.

Evidence

  • npm run validate: 617 passing tests; typecheck and asset audit pass.
  • Focused deterministic/session/store tests pass.
  • Production build is blocked in this 3.8 GiB sandbox by an OOM termination during Vite chunk rendering, after 2,416 modules transform.
  • Browser preview reached the shell but rendered a pre-existing blank root, so manual visual acceptance remains pending.

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 CombatSession owns 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.applyEncounterResult applies 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.

@vercel

vercel Bot commented Aug 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
slide Ready Ready Preview Aug 21, 2026 2:20pm

@cursor

cursor Bot commented Aug 20, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot 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)

@BrandDead
BrandDead force-pushed the feat/unified-territory-encounter branch from 723d02d to be7c8e1 Compare August 20, 2026 16:17
@cursor

cursor Bot commented Aug 20, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot 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)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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)) });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +339 to +344
{showEncounter ? (
<UnifiedEncounter
block={block}
onResolved={handleEncounterResolved}
onClose={() => setShowEncounter(false)}
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +243 to +246
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 });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +95 to +97
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));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +72 to +75
if (!resolved || loaded.has(resolved.url)) continue;
const key = this.textureKey(actor);
this.load.image(key, resolved.url);
loaded.add(resolved.url);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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
@cursor

cursor Bot commented Aug 21, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot 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)

@BrandDead
BrandDead merged commit a2edb70 into main-tL2525 Aug 21, 2026
6 checks passed
@BrandDead
BrandDead deleted the feat/unified-territory-encounter branch August 21, 2026 14:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant