diff --git a/docs/project-structure.txt b/docs/project-structure.txt index 71a62a7..9f198b8 100644 --- a/docs/project-structure.txt +++ b/docs/project-structure.txt @@ -336,6 +336,7 @@ Mana-Loop/ │ │ │ │ │ └── pact-ritual.ts │ │ │ │ ├── attunementStore.ts │ │ │ │ ├── combat-actions.ts +│ │ │ │ ├── combat-descent-actions.ts │ │ │ │ ├── combat-state.types.ts │ │ │ │ ├── combatStore.ts │ │ │ │ ├── crafting-equipment-tick.ts diff --git a/src/components/game/tabs/SpireCombatPage/RoomDisplay.tsx b/src/components/game/tabs/SpireCombatPage/RoomDisplay.tsx index 8d348db..a9e55f9 100644 --- a/src/components/game/tabs/SpireCombatPage/RoomDisplay.tsx +++ b/src/components/game/tabs/SpireCombatPage/RoomDisplay.tsx @@ -12,6 +12,13 @@ import { DebugName } from '@/components/game/debug/debug-context'; interface RoomDisplayProps { floorState: FloorState; floor: number; + /** 0-indexed current room (spec §3) */ + roomIndex?: number; + /** Total rooms on this floor (spec §3) */ + totalRooms?: number; + /** Current in-game time for display (combat spec §10) */ + day?: number; + hour?: number; } function EnemyRow({ enemy }: { enemy: EnemyState }) { @@ -72,7 +79,7 @@ function EnemyRow({ enemy }: { enemy: EnemyState }) { ); } -export function RoomDisplay({ floorState }: RoomDisplayProps) { +export function RoomDisplay({ floorState, floor, roomIndex, totalRooms, day, hour }: RoomDisplayProps) { // Guard against null/undefined/stale floorState if (!floorState || !floorState.roomType) { return ( @@ -86,6 +93,17 @@ export function RoomDisplay({ floorState }: RoomDisplayProps) { const roomDisplay = getSpireRoomTypeDisplay(floorState.roomType as RoomType); + // ─── Spec §3: Show "Room X / Y" ───────────────────────────────────────── + const showRoomIndex = roomIndex !== undefined && totalRooms !== undefined; + const roomLabel = showRoomIndex + ? `Room ${roomIndex + 1} / ${totalRooms}` + : null; + + // ─── Combat spec §10: In-game time display ─────────────────────────────── + const timeLabel = day !== undefined && hour !== undefined + ? `D${day} H${Math.floor(hour)}` + : null; + // Handle special room types (cast to string for extended types) const rt = floorState.roomType as string; @@ -97,6 +115,8 @@ export function RoomDisplay({ floorState }: RoomDisplayProps) { 💚 Recovery Room + {roomLabel && {roomLabel}} + {timeLabel && {timeLabel}} @@ -119,6 +139,8 @@ export function RoomDisplay({ floorState }: RoomDisplayProps) { 📚 Ancient Library + {roomLabel && {roomLabel}} + {timeLabel && {timeLabel}} @@ -136,6 +158,8 @@ export function RoomDisplay({ floorState }: RoomDisplayProps) { 💎 Treasure Room + {roomLabel && {roomLabel}} + {timeLabel && {timeLabel}} @@ -156,6 +180,8 @@ export function RoomDisplay({ floorState }: RoomDisplayProps) { 🧩 Puzzle Room — {puzzleId.replace(/_/g, ' ')} + {roomLabel && {roomLabel}} + {timeLabel && {timeLabel}} @@ -186,6 +212,16 @@ export function RoomDisplay({ floorState }: RoomDisplayProps) { {roomDisplay.icon} {roomDisplay.label} {isGuardian && BOSS} + {/* ── Spec §3: Room type badge + room index ── */} + {roomLabel && ( + + {roomLabel} + + )} + {/* ── Combat spec §10: In-game time ── */} + {timeLabel && ( + {timeLabel} + )} diff --git a/src/components/game/tabs/SpireCombatPage/SpireCombatPage.tsx b/src/components/game/tabs/SpireCombatPage/SpireCombatPage.tsx index 9834da4..870b83d 100644 --- a/src/components/game/tabs/SpireCombatPage/SpireCombatPage.tsx +++ b/src/components/game/tabs/SpireCombatPage/SpireCombatPage.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useMemo, useRef } from 'react'; import { useShallow } from 'zustand/react/shallow'; -import { useCombatStore, useManaStore, usePrestigeStore, fmt, computeMaxMana, computeRegen } from '@/lib/game/stores'; +import { useCombatStore, useManaStore, usePrestigeStore, useGameStore, fmt, computeMaxMana, computeRegen } from '@/lib/game/stores'; import { computeDisciplineEffects } from '@/lib/game/effects/discipline-effects'; import { getUnifiedEffects } from '@/lib/game/effects'; import { useCraftingStore } from '@/lib/game/stores/craftingStore'; @@ -56,6 +56,7 @@ function useSpireStats(prestigeUpgrades: Record, equippedInstanc export function SpireCombatPage() { const [roomsCleared, setRoomsCleared] = useState(0); + // ─── Spec: read room-aware state from combat store ─────────────────────── const { currentFloor, floorHP, @@ -64,6 +65,11 @@ export function SpireCombatPage() { isDescending, currentRoom, activityLog, + climbDirection, + currentRoomIndex, + roomsPerFloor, + isDescentComplete, + startFloor, setCurrentRoom, setFloorHP, setClearedFloor, @@ -81,6 +87,11 @@ export function SpireCombatPage() { isDescending: s.isDescending, currentRoom: s.currentRoom, activityLog: s.activityLog, + climbDirection: s.climbDirection, + currentRoomIndex: s.currentRoomIndex, + roomsPerFloor: s.roomsPerFloor, + isDescentComplete: s.isDescentComplete, + startFloor: s.startFloor, setCurrentRoom: s.setCurrentRoom, setFloorHP: s.setFloorHP, setClearedFloor: s.setClearedFloor, @@ -107,6 +118,10 @@ export function SpireCombatPage() { equipmentInstances: s.equipmentInstances, }))); + // ─── Combat spec §10: read current in-game time ────────────────────────── + const day = useGameStore((s) => s.day); + const hour = useGameStore((s) => s.hour); + const { maxMana, baseRegen } = useSpireStats(prestigeUpgrades, equippedInstances, equipmentInstances); // Use a deterministic seed based on floor to avoid Math.random() causing @@ -129,8 +144,6 @@ export function SpireCombatPage() { // Generate initial room when floor or room count changes. // Uses a ref guard to prevent infinite re-render loops. - // Generate room on mount and when floor changes. - // Uses a ref guard to prevent infinite re-render loops. const lastGeneratedRef = useRef(null); useEffect(() => { const key = `${currentFloor}:${totalRooms}`; @@ -236,7 +249,14 @@ export function SpireCombatPage() {
- +
diff --git a/src/components/game/tabs/SpireCombatPage/SpireHeader.tsx b/src/components/game/tabs/SpireCombatPage/SpireHeader.tsx index a1cc533..7c2d194 100644 --- a/src/components/game/tabs/SpireCombatPage/SpireHeader.tsx +++ b/src/components/game/tabs/SpireCombatPage/SpireHeader.tsx @@ -5,7 +5,7 @@ import { useShallow } from 'zustand/react/shallow'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; import { Progress } from '@/components/ui/progress'; -import { Mountain, ArrowUp, ArrowDown, LogOut } from 'lucide-react'; +import { Mountain, ArrowUp, ArrowDown, LogOut, ChevronDown } from 'lucide-react'; import { getGuardianForFloor, isGuardianFloor } from '@/lib/game/data/guardian-encounters'; import { DebugName } from '@/components/game/debug/debug-context'; @@ -34,11 +34,16 @@ export function SpireHeader({ }: SpireHeaderProps) { const maxFloorReached = useCombatStore((s) => s.maxFloorReached); const insight = usePrestigeStore((s) => s.insight); + // ─── Spec: read climbDirection and isDescentComplete ───────────────────── + const climbDirection = useCombatStore((s) => s.climbDirection); + const isDescentComplete = useCombatStore((s) => s.isDescentComplete); + const enterDescentMode = useCombatStore((s) => s.enterDescentMode); const guardian = getGuardianForFloor(currentFloor); const isGuardian = isGuardianFloor(currentFloor); const hpPercent = floorMaxHP > 0 ? (floorHP / floorMaxHP) * 100 : 100; const roomProgress = totalRooms > 0 ? ((roomsCleared) / totalRooms) * 100 : 0; + const isAscending = climbDirection === 'up'; return ( @@ -59,27 +64,47 @@ export function SpireHeader({
- - - {currentFloor === 1 && ( + {/* ── Spec §4.5: "Descend" button available at any point during ascent ── */} + {isAscending && ( + + )} + + {/* Legacy climb buttons (kept for non-spire-mode compatibility) */} + {!climbDirection && ( + <> + + + + )} + + {/* ── Spec §4.9: "Exit Spire" only visible when isDescentComplete ── */} + {isDescentComplete && (