ui: real SKILL TREE tab (research) + SHIP SYSTEMS buttons gated by researched techs

This commit is contained in:
2026-09-29 00:35:39 +00:00
parent 7b0227cde9
commit 94e9fc728c
3 changed files with 152 additions and 52 deletions
+20 -8
View File
@@ -4,21 +4,24 @@ import { useState } from 'react'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
import { BUILD_PART_COST, MAX_CAPACITY, MAX_LEVEL, expandCost, totalQty, upgradeCost } from '@/lib/game/ship-costs'
import { BUILD_PART_COST, expandCost, totalQty, upgradeCost } from '@/lib/game/ship-costs'
import type { ShipState, ShipSystemDto } from '@/lib/game/ship'
import { buildGate, effectiveCapacityCap, effectiveLevelCap } from '@/lib/game/tech'
// SHIP SYSTEMS tab — live data. The parent (MissionControl) owns the ship state so the left
// column mirrors actions immediately; this panel applies each action's response directly via
// onChanged (no polling). Buttons are disabled when unaffordable or capped, using the same
// cost constants the API enforces (lib/game/ship-costs.ts).
// onChanged (no polling). Buttons are disabled when unaffordable, capped or gated by an
// unresearched tech, using the same pure helpers the API enforces (lib/game/ship-costs.ts +
// lib/game/tech.ts).
interface ShipSystemsPanelProps {
ship: ShipState | null
narrow?: boolean
onChanged: (next: ShipState) => void
researchedSlugs: Set<string> // slugs of researched techs — raises caps, lifts build gates
}
export function ShipSystemsPanel({ ship, narrow, onChanged }: ShipSystemsPanelProps) {
export function ShipSystemsPanel({ ship, narrow, onChanged, researchedSlugs }: ShipSystemsPanelProps) {
const [error, setError] = useState<string | null>(null)
const [busy, setBusy] = useState(false) // one action at a time — turn-based game
@@ -57,7 +60,7 @@ export function ShipSystemsPanel({ ship, narrow, onChanged }: ShipSystemsPanelPr
)}
<div style={{ display: 'grid', gridTemplateColumns: narrow ? '1fr' : 'repeat(2, 1fr)', gap: '0.75rem' }}>
{ship.systems.map((system) => (
<SystemCard key={system.id} system={system} resources={resources} parts={parts} busy={busy} onAct={act} />
<SystemCard key={system.id} system={system} resources={resources} parts={parts} busy={busy} researchedSlugs={researchedSlugs} onAct={act} />
))}
</div>
</div>
@@ -69,17 +72,23 @@ function SystemCard({
resources,
parts,
busy,
researchedSlugs,
onAct,
}: {
system: ShipSystemDto
resources: number
parts: number
busy: boolean
researchedSlugs: Set<string>
onAct: (url: string, body: unknown) => void
}) {
const upgradeDisabled = busy || !system.built || system.level >= MAX_LEVEL || resources < upgradeCost(system.level)
const expandDisabled = busy || !system.built || system.capacity >= MAX_CAPACITY || resources < expandCost(system.capacity)
const buildDisabled = busy || system.built || parts < BUILD_PART_COST
const maxLevel = effectiveLevelCap(system.type, researchedSlugs)
const maxCapacity = effectiveCapacityCap(system.type, researchedSlugs)
const gate = buildGate(system.type, researchedSlugs) // name of the required tech, or null when ungated/researched
const upgradeDisabled = busy || !system.built || system.level >= maxLevel || resources < upgradeCost(system.level)
const expandDisabled = busy || !system.built || system.capacity >= maxCapacity || resources < expandCost(system.capacity)
const buildDisabled = busy || system.built || gate !== null || parts < BUILD_PART_COST
return (
<Card>
@@ -100,6 +109,9 @@ function SystemCard({
? `UPGRADE ${upgradeCost(system.level)}× RES · EXPAND ${expandCost(system.capacity)}× RES`
: `BUILD ${BUILD_PART_COST}× PARTS`}
</p>
{!system.built && gate !== null && (
<p style={{ fontSize: '0.62rem', color: 'var(--color-red)', letterSpacing: '0.08em' }}>REQUIRES // {gate}</p>
)}
</CardContent>
<CardFooter className="gap-2">
{system.built ? (