Skip to content

feat: recover beta work and add Modern Ops FPS/TPS slice - #119

Merged
BrandDead merged 11 commits into
main-tL2525from
feat/modern-gameplay-completion
Aug 29, 2026
Merged

feat: recover beta work and add Modern Ops FPS/TPS slice#119
BrandDead merged 11 commits into
main-tL2525from
feat/modern-gameplay-completion

Conversation

@BrandDead

@BrandDead BrandDead commented Aug 26, 2026

Copy link
Copy Markdown
Owner

Summary

This branch recovers the completed but unmerged copilot/dev-oplan work, adds a repository-specific completion roadmap and reusable skill, and applies that workflow to a playable Babylon.js Modern Ops vertical slice. The existing strategy layer can now launch one deterministic encounter in tactical, first-person, or third-person presentation while preserving the same combat session and applying one atomic result back to the block economy, heat, morale, crew injuries, and hospital/bail flow.

Included work

  • Recovers manifest-driven runtime assets, Block DNA expansion, Ghost Crew rival behavior, beta-gate smoke coverage, release checklist, and associated fixes from the divergent branch.
  • Adds docs/MODERN_GAMEPLAY_COMPLETION_PLAN.md and skills/dealt-slide-game-completion/.
  • Adds CombatSessionController as the presentation-neutral action adapter over the existing deterministic combat model.
  • Adds lazy-loaded Babylon.js Modern Ops with tactical, FPS, and TPS cameras; semantic input; movement; fire/reload; cover; extraction; pause/retreat; tracers/impacts; and lifecycle-safe disposal.
  • Integrates OPS 3D into the claimed-block Strip view and returns results through the existing applyEncounterResult boundary.
  • Adds generated Las Olas storefront runtime art and records prompts/provenance in ASSETS.md.
  • Separates extraction from crew spawn, tunes opening opposition pacing for a usable action window, and adds deterministic regression coverage.
  • Removes circular application-level Vite chunks, isolates Babylon as a lazy vendor chunk, and switches production minification to memory-bounded esbuild.

Verification

Gate Result
TypeScript Pass
Frontend tests 45 files, 693 tests passed
Backend tests 42 passed
Runtime asset audit 110 assets, 5.81 MB / 20 MB, 0 errors, 0 warnings
ESLint 0 errors; 196 existing warnings
Production build Pass; 2,875 modules; 21.15 s
Browser/WebGL WebGL2; one canvas; tactical/FPS/TPS switch; live fire updates ammo/status; 0 captured runtime exceptions
Strategy result handoff Secured result changed pending income $840→$982, heat 1→2, morale 85%→89%, and opened the existing injury/hospital flow

Scope and issue relationship

This PR advances #45, #77, #78, #79, #80, and #81. It does not auto-close them because the repository still needs full asset replacement, a larger 30–40 Block DNA catalog, broader Ghost Crew balancing/persistence hardening, production character/vehicle art, mobile/touch action controls, performance budgets on real devices, and final beta/release evidence.

Review order

  1. docs/MODERN_GAMEPLAY_COMPLETION_PLAN.md
  2. skills/dealt-slide-game-completion/SKILL.md
  3. frontend/src/game/combat/CombatSessionController.ts
  4. frontend/src/game/ops/
  5. frontend/src/components/ops/
  6. frontend/src/components/map/BlockModeView.tsx
  7. frontend/vite.config.ts

Notes

The project’s default branch is main-tL2525. The older copilot/dev-oplan branch is now superseded by this branch because its six unique commits were recovered and reconciled here.


Note

Medium Risk
Touches the encounter result boundary, economy/heat application, and adds a large lazy 3D stack, but keeps authoritative combat in the existing session with new tests and CI asset gates.

Overview
Adds a lazy-loaded Babylon.js “OPS 3D” encounter from block view: tactical, first-person, and third-person cameras share one CombatSessionController over the existing deterministic CombatSession, with HUD, input, procedural grid-to-world scene, and the same applyEncounterResult handoff as the Phaser encounter.

Also lands recovered beta work: Block DNA library 8 → 17 with claim-time stamping and DNA-scaled income; persistent Ghost Crew world ticks, map NPC turf, grudge on drive-by/attack, and GhostThreatBanner; runtime assets pointed at WebP + runtimeManifest, new Las Olas / Modern Ops art entries, npm run assets:audit in CI, beta smoke tests, and release checklist docs.

Combat pacing changes: extraction away from crew spawn, slower/weaker opposition turns, plus controller and coordinate tests. Build: @babylonjs/core as a dedicated lazy vendor chunk (per existing Vite manual-chunk strategy).

Reviewed by Cursor Bugbot for commit e458cba. Bugbot is set up for automated code reviews on this repo. Configure here.

BrandDead and others added 10 commits August 26, 2026 03:32
…dit as CI gate, fix stale .png refs

Co-authored-by: BrandDead <174973198+BrandDead@users.noreply.github.com>
…ssetManifest paths to on-disk webp

Co-authored-by: BrandDead <174973198+BrandDead@users.noreply.github.com>
…claim, scale income by DNA multiplier

Co-authored-by: BrandDead <174973198+BrandDead@users.noreply.github.com>
…grudge memory (#81)

Co-authored-by: BrandDead <174973198+BrandDead@users.noreply.github.com>
…dation

Co-authored-by: BrandDead <174973198+BrandDead@users.noreply.github.com>
…ater pure, fix test loop condition

Co-authored-by: BrandDead <174973198+BrandDead@users.noreply.github.com>
@vercel

vercel Bot commented Aug 26, 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 26, 2026 4:21am

Request Review

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Ghost grudge cleared before complete
    • CarCrewSelector now uses consumePendingTarget() instead of reset() to preserve targetCrewId until after DriveByGame records the grudge attack.
  • ✅ Fixed: Combat FX replay after log trim
    • OpsWorld.handleEvents now detects when latestEventId has been trimmed from the event log and updates the tracking ID without replaying events.

Create PR

Or push these changes by commenting:

@cursor push d2e1d4a913
Preview (d2e1d4a913)
diff --git a/frontend/src/components/driveby/CarCrewSelector.tsx b/frontend/src/components/driveby/CarCrewSelector.tsx
--- a/frontend/src/components/driveby/CarCrewSelector.tsx
+++ b/frontend/src/components/driveby/CarCrewSelector.tsx
@@ -258,7 +258,7 @@
                 ? { address: normalizeAddressText(typed), seedMode: 'text-seed' }
                 : null);
             onConfirm({ seats, targetBlock: finalTarget });
-            useCombatIntentStore.getState().reset();
+            useCombatIntentStore.getState().consumePendingTarget();
           }}
           disabled={!canLaunch}
           whileTap={canLaunch ? { scale: 0.95 } : {}}

diff --git a/frontend/src/components/driveby/DriveByGame.tsx b/frontend/src/components/driveby/DriveByGame.tsx
--- a/frontend/src/components/driveby/DriveByGame.tsx
+++ b/frontend/src/components/driveby/DriveByGame.tsx
@@ -7,11 +7,11 @@
 
 import React, { useState, useCallback } from 'react';
 import { useNavigationStore, usePlayerStore, useGangStore } from '../../stores/gameStore';
+import { useCombatIntentStore } from '../../stores/combatIntentStore';
+import { useGhostStore } from '../../stores/ghostCrewStore';
 import CarCrewSelector, { type CarCrew } from './CarCrewSelector';
 import DriveByEngine from './DriveByEngine';
 import { vaultDeposit } from '../../utils/moneyRouter';
-import { useCombatIntentStore } from '../../stores/combatIntentStore';
-import { useGhostStore } from '../../stores/ghostCrewStore';
 
 interface GameStats {
   kills: number;
@@ -55,6 +55,7 @@
     if (targetCrewId && targetBlockId && stats.kills > 0) {
       useGhostStore.getState().recordPlayerAttack(targetCrewId, targetBlockId);
     }
+    useCombatIntentStore.getState().reset();
     
     // Update crew member XP/stats based on performance
     if (crew) {

diff --git a/frontend/src/game/ops/OpsWorld.ts b/frontend/src/game/ops/OpsWorld.ts
--- a/frontend/src/game/ops/OpsWorld.ts
+++ b/frontend/src/game/ops/OpsWorld.ts
@@ -350,9 +350,15 @@
   }
 
   private handleEvents(events: CombatEvent[]): void {
-    const startIndex = this.latestEventId
-      ? events.findIndex((event) => event.id === this.latestEventId) + 1
-      : 0;
+    if (events.length === 0) return;
+    const foundIndex = this.latestEventId
+      ? events.findIndex((event) => event.id === this.latestEventId)
+      : -1;
+    if (this.latestEventId && foundIndex === -1) {
+      this.latestEventId = events.at(-1)?.id ?? this.latestEventId;
+      return;
+    }
+    const startIndex = foundIndex + 1;
     const nextEvents = startIndex > 0 ? events.slice(startIndex) : events;
     nextEvents.forEach((event) => this.renderEvent(event));
     this.latestEventId = events.at(-1)?.id ?? this.latestEventId;

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit db0ab81. Configure here.

Comment thread frontend/src/components/driveby/DriveByGame.tsx
Comment thread frontend/src/game/ops/OpsWorld.ts

@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: db0ab81856

ℹ️ 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".

Comment on lines +53 to +56
const targetCrewId = useCombatIntentStore.getState().targetCrewId;
const targetBlockId = crew?.targetBlock?.placeId ?? null;
if (targetCrewId && targetBlockId && stats.kills > 0) {
useGhostStore.getState().recordPlayerAttack(targetCrewId, targetBlockId);

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 Preserve the ghost target through drive-by completion

For every Maps-originated drive-by, CarCrewSelector calls reset() immediately after onConfirm, and that reset clears targetCrewId. By the time handleGameEnd reads this field it is therefore always null, so successful kills on ghost turf never invoke recordPlayerAttack and the advertised grudge/retaliation path cannot run. Carry the crew ID into the launched CarCrew or defer clearing it until the result is processed.

Useful? React with 👍 / 👎.

Comment on lines +31 to +35
movement(): OpsMovementInput {
const forward = Number(this.keys.has('KeyW') || this.keys.has('ArrowUp'))
- Number(this.keys.has('KeyS') || this.keys.has('ArrowDown'));
const strafe = Number(this.keys.has('KeyD') || this.keys.has('ArrowRight'))
- Number(this.keys.has('KeyA') || this.keys.has('ArrowLeft'));

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 Add touch controls before exposing OPS on mobile

On touch-only iOS devices, OPS has no way to produce movement because movement() is driven exclusively by keyboard codes; reload, extract, and retreat are likewise keyboard-only, while the rendered mobile layout exposes only pause and camera buttons. Consequently a player can enter this full-screen encounter but cannot reach the exit or complete it normally without a hardware keyboard. Add touch movement/action controls or hide the mode on unsupported input devices.

AGENTS.md reference: AGENTS.md:L9-L9

Useful? React with 👍 / 👎.

Comment on lines +141 to +145
if (action.type === 'claim' && action.claimedDnaId) {
const dna = getDNAById(action.claimedDnaId);
if (dna) {
blockStore.upsertBlock(buildGhostBlock(updated, dna));
ctx.ghostOwnedBlockIds.add(`ghost-${dna.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 Apply ghost attacks to the targeted player block

Whenever decideGhostAction returns attack, this loop only updates the attacking crew and emits feed/notification entries; the sole blockStore mutation is guarded by action.type === 'claim'. The target remains player-owned with unchanged morale, placements, and income, so rival attacks can never damage or capture held turf despite announcing that a crew is moving on it. Resolve the attack against action.targetBlockId or create a pending encounter instead of treating it as notification-only.

Useful? React with 👍 / 👎.

Comment on lines +191 to +196
// Stamp the resolved Block DNA so the block keeps its archetype
// identity, income multiplier, and deployment cap after claim (#80).
dnaId: resolved.dna.id,
incomeMultiplier: resolved.incomeMultiplier,
heatDecayMultiplier: resolved.dna.heatDecayMultiplier,
maxMembers: resolved.maxMembers,

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 Enforce the stamped Block DNA deployment cap

Claimed blocks now store resolved.maxMembers as a deployment cap, but neither blockStore.placeMember nor either placement UI reads block.maxMembers; they continue accepting distinct members until the grid or global roster is exhausted. Thus a starter block capped at five can host six or more members, erasing the intended DNA balance difference. Reject new placements once the block reaches its cap while still allowing an already-deployed member to move.

Useful? React with 👍 / 👎.

Comment on lines +71 to +73
const latest = feed.find(
(e) => BANNER_ACTIONS.has(e.action) && e.id !== dismissedId,
);

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 Dismiss the banner instead of revealing older events

When the feed contains multiple claim or attack events—which a single multi-crew tick can produce—dismissing the current banner merely excludes that one ID, causing find() to immediately select and display the next older event. Users must click through every retained historical event, and old banners return after remount because the dismissal is local state. Track an acknowledged cutoff/set or hide the banner until a newer event arrives.

Useful? React with 👍 / 👎.

Comment on lines +193 to +196
dnaId: resolved.dna.id,
incomeMultiplier: resolved.incomeMultiplier,
heatDecayMultiplier: resolved.dna.heatDecayMultiplier,
maxMembers: resolved.maxMembers,

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 Persist the Block DNA fields with claimed blocks

In authenticated play, these DNA values survive only in the browser's local Zustand snapshot: the Supabase persistence metadata and Flask hydration mapper omit dnaId, incomeMultiplier, heatDecayMultiplier, and maxMembers. After clearing local storage or signing in on another device, the claimed block is hydrated with all four undefined, so subsequent placements fall back to a 1× income multiplier and the block loses its authored identity and modifiers. Persist these fields remotely or deterministically reconstruct them before inserting hydrated blocks.

Useful? React with 👍 / 👎.

Comment on lines +141 to +146
{result && (
<div className="ops-result" role="dialog" aria-modal="true" aria-label="Modern Ops result">
<span className={`ops-result__badge outcome-${result.outcome}`}>{result.outcome.toUpperCase()}</span>
<h3>{result.summary}</h3>
<p>{result.oppositionDown.length} opposition down · {result.crewDown.length} crew injured · heat {result.heatDelta >= 0 ? '+' : ''}{result.heatDelta}</p>
<button type="button" onClick={commitResult}>Apply result to the block</button>

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 Release pointer lock when the OPS result resolves

If the player has fired with the mouse in FPS/TPS mode, the canvas owns pointer lock when the result dialog appears. Pointer-lock mouse events remain directed to the canvas, but applying the result is available only through the dialog button and there is no keyboard equivalent, so the button cannot be clicked until the player independently knows to press Escape. Exit pointer lock when state.snapshot.phase becomes resolved, or provide a keyboard action for commitResult.

Useful? React with 👍 / 👎.

Comment on lines +45 to +47
if (cancelled) {
sceneHandle.dispose();
engine.dispose();

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 Ignore errors from cancelled OPS initialization

Because the app is wrapped in React.StrictMode, the first effect setup can be cleaned up while createModernOpsScene() is still awaiting readiness; that cleanup sets the captured engine to null. When the await resumes, the cancelled branch calls engine.dispose() unconditionally, throws, and the catch invokes onError, so the stale first initialization can place an error overlay over the healthy second StrictMode initialization. Keep a non-null local engine reference and suppress error reporting after cancellation.

Useful? React with 👍 / 👎.

@BrandDead
BrandDead merged commit 2a28c84 into main-tL2525 Aug 29, 2026
5 checks passed
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.

2 participants