Add HQ entry from city view

Co-authored-by: Luke The Dev <iamlukethedev@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-04-05 17:43:42 +00:00
co-authored by Luke The Dev
parent e7766a7242
commit cb9dd9739b
2 changed files with 82 additions and 6 deletions
+23 -2
View File
@@ -2704,12 +2704,13 @@ export function RetroOffice3D({
target: [number, number, number];
zoom?: number;
} | null>(null);
const [sceneMode, setSceneMode] = useState<"city" | "office">("city");
const cityScapeMode = sceneMode === "city";
const LOCAL_CAMERA_TARGET = useMemo(
() =>
toWorld(LOCAL_OFFICE_CANVAS_WIDTH / 2, LOCAL_OFFICE_CANVAS_HEIGHT / 2),
[],
);
const cityScapeMode = true;
const cityCameraTarget = LOCAL_CAMERA_TARGET;
const CAM_POS = useMemo<[number, number, number]>(() => {
const activeTarget = cityScapeMode ? cityCameraTarget : LOCAL_CAMERA_TARGET;
@@ -2728,6 +2729,12 @@ export function RetroOffice3D({
() => ({ pos: CAM_POS, target: cameraTarget, zoom: cameraZoom }),
[CAM_POS, cameraTarget, cameraZoom]
);
const handleEnterOfficeFromCity = useCallback(() => {
setSceneMode("office");
}, []);
const handleReturnToCity = useCallback(() => {
setSceneMode("city");
}, []);
const canvasResetKey = useMemo(
() =>
[
@@ -5460,7 +5467,10 @@ export function RetroOffice3D({
/>
{/* Floor + walls — always visible, no async loading. */}
<SceneFloorAndWalls showRemoteOffice={cityScapeMode} />
<SceneFloorAndWalls
showRemoteOffice={cityScapeMode}
onEnterHeadquarters={handleEnterOfficeFromCity}
/>
{/* Wall pictures — procedural, no async loading. */}
<SceneWallPictures showRemoteOffice={cityScapeMode} />
@@ -7373,6 +7383,17 @@ export function RetroOffice3D({
)}
</div>
) : null}
{!immersiveOverlayActive && !cityScapeMode ? (
<div className="absolute top-3 left-3 z-20">
<button
type="button"
onClick={handleReturnToCity}
className="rounded-md border border-cyan-500/35 bg-[#071018]/92 px-3 py-1.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-cyan-100 transition-colors backdrop-blur-sm hover:border-cyan-300/55 hover:text-white"
>
Back to City
</button>
</div>
) : null}
{!readOnly && settingsModalOpen ? (
<div className="absolute inset-0 z-30 flex items-start justify-end overflow-y-auto bg-black/35 p-4 backdrop-blur-[1px]">
<div className="flex max-h-[calc(100vh-2rem)] w-full max-w-sm flex-col overflow-hidden rounded-xl border border-cyan-500/20 bg-[#05090d]/95 shadow-2xl">
@@ -1,6 +1,6 @@
"use client";
import { memo, useMemo, useRef, type ReactNode } from "react";
import { memo, useMemo, useRef, useState, type ReactNode } from "react";
import { useFrame } from "@react-three/fiber";
import type * as THREE from "three";
import {
@@ -653,8 +653,17 @@ function RoadTile({
);
}
function FullCityScene({ centerX, centerZ }: { centerX: number; centerZ: number }) {
function FullCityScene({
centerX,
centerZ,
onEnterHeadquarters,
}: {
centerX: number;
centerZ: number;
onEnterHeadquarters?: () => void;
}) {
const cityGrid = useMemo(() => buildCityGrid(), []);
const [hqHovered, setHqHovered] = useState(false);
const baseOffsetX = centerX - ((CITY_GRID_COLUMNS - 1) * CITY_GRID_CELL_SIZE) / 2;
const baseOffsetZ = centerZ - ((CITY_GRID_ROWS - 1) * CITY_GRID_CELL_SIZE) / 2;
const cityWidth = CITY_GRID_COLUMNS * CITY_GRID_CELL_SIZE;
@@ -753,7 +762,29 @@ function FullCityScene({ centerX, centerZ }: { centerX: number; centerZ: number
/>
);
})}
<group position={hqPosition}>
<group
position={hqPosition}
onPointerOver={(event) => {
event.stopPropagation();
setHqHovered(true);
}}
onPointerOut={(event) => {
event.stopPropagation();
setHqHovered(false);
}}
onClick={(event) => {
event.stopPropagation();
onEnterHeadquarters?.();
}}
>
<mesh position={[0, 0.01, 0.18]} rotation={[-Math.PI / 2, 0, 0]}>
<planeGeometry args={[3.2, 2.8]} />
<meshBasicMaterial
color={hqHovered ? "#fde68a" : "#bbf7d0"}
transparent
opacity={hqHovered ? 0.24 : 0.14}
/>
</mesh>
<TowerBlock
position={[-0.52, 0, -0.26]}
width={0.9}
@@ -782,6 +813,22 @@ function FullCityScene({ centerX, centerZ }: { centerX: number; centerZ: number
trimColor="#dcfce7"
windowColor="#bfdbfe"
/>
<mesh position={[0, 0.02, -1.02]} rotation={[-Math.PI / 2, 0, 0]}>
<planeGeometry args={[1.85, 0.34]} />
<meshBasicMaterial
color={hqHovered ? "#fef08a" : "#e0f2fe"}
transparent
opacity={0.92}
/>
</mesh>
<mesh position={[0, 0.03, -1.02]}>
<planeGeometry args={[1.7, 0.18]} />
<meshBasicMaterial color="#0f172a" transparent opacity={0.98} />
</mesh>
<mesh position={[0, 0.04, -1.02]}>
<planeGeometry args={[1.58, 0.12]} />
<meshBasicMaterial color={hqHovered ? "#fef08a" : "#f8fafc"} transparent opacity={1} />
</mesh>
</group>
<CityTrafficLayer
cityRoadSpanX={CITY_GRID_CELL_SIZE * CITY_GRID_COLUMNS * 0.82}
@@ -861,8 +908,10 @@ const buildCityGrid = (): CityGridCell[] => {
export const FloorAndWalls = memo(function FloorAndWalls({
showRemoteOffice = true,
onEnterHeadquarters,
}: {
showRemoteOffice?: boolean;
onEnterHeadquarters?: () => void;
}) {
const districtWidth = CANVAS_W * SCALE;
const districtHeight = CANVAS_H * SCALE;
@@ -919,7 +968,13 @@ export const FloorAndWalls = memo(function FloorAndWalls({
const groundHeight = showRemoteOffice ? districtHeight : localOfficeHeight;
if (showRemoteOffice) {
return <FullCityScene centerX={localOfficeCenterX} centerZ={localOfficeCenterZ} />;
return (
<FullCityScene
centerX={localOfficeCenterX}
centerZ={localOfficeCenterZ}
onEnterHeadquarters={onEnterHeadquarters}
/>
);
}
return (