diff --git a/Dockerfile b/Dockerfile index b045c8d..af88dc7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,10 +2,6 @@ FROM node:20-bookworm-slim AS builder WORKDIR /app -RUN apt-get update \ - && apt-get install -y --no-install-recommends python3 make g++ \ - && rm -rf /var/lib/apt/lists/* - RUN corepack enable COPY . . diff --git a/apps/api/src/features/game/game.schemas.ts b/apps/api/src/features/game/game.schemas.ts index a22dd40..56f37b4 100644 --- a/apps/api/src/features/game/game.schemas.ts +++ b/apps/api/src/features/game/game.schemas.ts @@ -16,10 +16,19 @@ export const gameActionSchema = z.discriminatedUnion('type', [ z.object({ type: z.literal('use-item'), itemId: z.string().min(1) }), z.object({ type: z.literal('equip-item'), itemId: z.string().min(1) }), z.object({ type: z.literal('unequip-item'), slot: z.enum(['weapon', 'body', 'tool']) }), + z.object({ + type: z.literal('stash-item'), + itemId: z.string().min(1), + count: z.number().int().min(1).max(99), + }), + z.object({ + type: z.literal('retrieve-item'), + itemId: z.string().min(1), + count: z.number().int().min(1).max(99), + }), z.object({ type: z.literal('trade'), offerId: z.string().min(1) }), z.object({ type: z.literal('rest'), minutes: z.number().int().min(10).max(240) }), ]); export type CreateGameInput = z.infer; export type GameActionInput = z.infer; - diff --git a/apps/web/public/generated/ui/status-atlas.png b/apps/web/public/generated/ui/status-atlas.png new file mode 100644 index 0000000..95e6dc8 Binary files /dev/null and b/apps/web/public/generated/ui/status-atlas.png differ diff --git a/apps/web/public/generated/ui/status/energy.png b/apps/web/public/generated/ui/status/energy.png new file mode 100644 index 0000000..4ee1691 Binary files /dev/null and b/apps/web/public/generated/ui/status/energy.png differ diff --git a/apps/web/public/generated/ui/status/hunger.png b/apps/web/public/generated/ui/status/hunger.png new file mode 100644 index 0000000..2fbef12 Binary files /dev/null and b/apps/web/public/generated/ui/status/hunger.png differ diff --git a/apps/web/public/generated/ui/status/life.png b/apps/web/public/generated/ui/status/life.png new file mode 100644 index 0000000..578fa67 Binary files /dev/null and b/apps/web/public/generated/ui/status/life.png differ diff --git a/apps/web/public/generated/ui/status/radiation.png b/apps/web/public/generated/ui/status/radiation.png new file mode 100644 index 0000000..0256a4a Binary files /dev/null and b/apps/web/public/generated/ui/status/radiation.png differ diff --git a/apps/web/public/generated/ui/status/sanity.png b/apps/web/public/generated/ui/status/sanity.png new file mode 100644 index 0000000..683ad56 Binary files /dev/null and b/apps/web/public/generated/ui/status/sanity.png differ diff --git a/apps/web/public/generated/ui/status/thirst.png b/apps/web/public/generated/ui/status/thirst.png new file mode 100644 index 0000000..8716048 Binary files /dev/null and b/apps/web/public/generated/ui/status/thirst.png differ diff --git a/apps/web/src/App.css b/apps/web/src/App.css index 4cd6c6b..38575fc 100644 --- a/apps/web/src/App.css +++ b/apps/web/src/App.css @@ -267,6 +267,18 @@ input, background: rgba(215, 138, 51, 0.12); } +.hud-chip.warn { + color: #f1c27b; + border-color: rgba(224, 180, 102, 0.34); + background: rgba(224, 180, 102, 0.12); +} + +.hud-chip.danger { + color: #f0a08d; + border-color: rgba(225, 109, 76, 0.34); + background: rgba(225, 109, 76, 0.12); +} + .hud-chip.muted { color: var(--muted); } @@ -1072,11 +1084,33 @@ input, padding: 0.75rem; } +.effect-card-head { + display: flex; + align-items: center; + gap: 0.65rem; +} + +.status-effect-icon { + width: 42px; + height: 42px; + object-fit: cover; + border-radius: 12px; + border: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(0, 0, 0, 0.36); +} + .effect-card p { margin: 0.35rem 0 0; font-size: 0.78rem; } +.effect-card small { + display: block; + margin-top: 0.45rem; + color: var(--muted); + line-height: 1.45; +} + .effect-card.tone-good { border-color: rgba(141, 194, 106, 0.24); } @@ -1538,7 +1572,7 @@ input, .overlay-summary-grid { display: grid; - grid-template-columns: repeat(5, minmax(0, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(132px, 1fr)); gap: 0.55rem; } @@ -1577,6 +1611,13 @@ input, grid-template-columns: minmax(0, 1.4fr) 320px; } +.overlay-main-stack { + min-height: 0; + display: grid; + gap: 0.75rem; + grid-template-rows: minmax(0, 1fr) minmax(0, 1fr); +} + .mission-overlay-grid { grid-template-columns: minmax(0, 1.2fr) 340px; } @@ -1659,6 +1700,471 @@ input, background: rgba(215, 138, 51, 0.26); } +.game-grid { + grid-template-columns: 272px minmax(0, 1fr) 352px; +} + +.panel-route.expedition-panel, +.panel-command.theater-panel, +.panel-loadout.survivor-panel { + padding: 0.85rem; +} + +.panel-route.expedition-panel { + grid-template-rows: auto auto minmax(0, 1fr) minmax(0, 0.78fr); +} + +.expedition-head, +.survivor-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 0.75rem; +} + +.expedition-head h2, +.survivor-head strong { + margin: 0.3rem 0 0; + font-size: 1.28rem; + line-height: 1; + letter-spacing: -0.03em; +} + +.expedition-core { + display: grid; + gap: 0.8rem; + padding: 0.95rem; + border-radius: 18px; + border: 1px solid rgba(215, 138, 51, 0.2); + background: + linear-gradient(180deg, rgba(215, 138, 51, 0.08), rgba(255, 255, 255, 0)), + rgba(10, 8, 7, 0.68); +} + +.expedition-core p { + margin: 0; +} + +.expedition-status-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 0.5rem; +} + +.expedition-status-card { + padding: 0.7rem; + border-radius: 14px; + border: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(255, 255, 255, 0.03); +} + +.expedition-status-card small { + display: block; + margin-bottom: 0.25rem; + color: var(--muted); + font-size: 0.66rem; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.route-stack, +.expedition-map-panel, +.survivor-supply-panel, +.survivor-actions-panel { + min-height: 0; + display: grid; + grid-template-rows: auto minmax(0, 1fr); +} + +.expedition-route-list, +.expedition-map-grid { + align-content: start; +} + +.expedition-route-card { + grid-template-columns: 76px minmax(0, 1fr) 56px; + padding: 0.55rem; +} + +.expedition-route-card .route-card-thumb { + min-height: 96px; +} + +.expedition-route-card .route-card-copy strong { + font-size: 1rem; +} + +.expedition-route-card .route-card-state { + min-width: 0; + align-items: flex-start; + gap: 0.45rem; +} + +.expedition-map-grid { + grid-template-columns: 1fr; +} + +.theater-panel { + grid-template-rows: 294px auto auto; +} + +.theater-scene { + position: relative; + overflow: hidden; + border-radius: 22px; + border: 1px solid rgba(215, 138, 51, 0.2); +} + +.theater-scene::after { + content: ''; + position: absolute; + inset: 0; + background: + linear-gradient(90deg, rgba(6, 5, 4, 0.1), rgba(6, 5, 4, 0.72) 74%), + linear-gradient(180deg, rgba(6, 5, 4, 0.06), rgba(6, 5, 4, 0.78)); +} + +.theater-scene-copy, +.theater-scene-side { + position: absolute; + z-index: 1; +} + +.theater-scene-copy { + left: 1rem; + right: 21rem; + bottom: 1rem; + display: grid; + gap: 0.45rem; +} + +.theater-scene-copy h2 { + margin: 0; + font-size: clamp(2.6rem, 4vw, 4.3rem); + line-height: 0.9; + letter-spacing: -0.06em; +} + +.theater-scene-copy p:last-child { + margin: 0; + max-width: 35rem; + color: rgba(239, 226, 206, 0.8); +} + +.theater-scene-side { + top: 1rem; + right: 1rem; + width: 300px; + display: grid; + gap: 0.55rem; +} + +.scene-focus-card { + padding: 0.85rem; + border-radius: 16px; + border: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(7, 6, 5, 0.58); + backdrop-filter: blur(14px); +} + +.scene-focus-card.error { + border-color: rgba(225, 109, 76, 0.28); +} + +.scene-focus-card span, +.primary-operation-copy span, +.intel-focus-card span, +.survivor-metric-card span { + display: block; + margin-bottom: 0.28rem; + color: var(--muted); + font-size: 0.68rem; + letter-spacing: 0.14em; + text-transform: uppercase; +} + +.scene-focus-card strong, +.intel-focus-card strong { + display: block; + font-size: 0.98rem; +} + +.scene-focus-card p, +.intel-focus-card p { + margin: 0.3rem 0 0; + color: rgba(239, 226, 206, 0.8); + line-height: 1.45; +} + +.theater-deck { + display: grid; + grid-template-columns: minmax(0, 1.15fr) 316px; + gap: 0.75rem; +} + +.primary-operation-card { + display: grid; + gap: 0.8rem; + padding: 1rem; + border-radius: 20px; + border: 1px solid rgba(215, 138, 51, 0.26); + background: + radial-gradient(circle at top right, rgba(215, 138, 51, 0.14), transparent 28%), + linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0)), + rgba(11, 9, 8, 0.86); +} + +.primary-operation-copy h3 { + margin: 0; + font-size: clamp(1.7rem, 2.8vw, 2.5rem); + line-height: 0.94; + letter-spacing: -0.04em; +} + +.primary-operation-copy p, +.secondary-operation-card p { + margin: 0.4rem 0 0; + color: rgba(239, 226, 206, 0.78); + line-height: 1.45; +} + +.operation-chip-row { + display: flex; + flex-wrap: wrap; + gap: 0.45rem; +} + +.operation-chip-row span { + display: inline-flex; + align-items: center; + min-height: 1.9rem; + padding: 0 0.7rem; + border-radius: 999px; + border: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(255, 255, 255, 0.03); + font-size: 0.72rem; + color: rgba(239, 226, 206, 0.82); +} + +.primary-operation-card small { + color: #f0c27a; + font-size: 0.78rem; +} + +.operation-commit-button { + width: min(100%, 220px); +} + +.secondary-operation-stack { + display: grid; + gap: 0.55rem; +} + +.secondary-operation-card { + min-height: 0; + display: grid; + gap: 0.3rem; + text-align: left; + padding: 0.85rem; + border-radius: 16px; + border: 1px solid rgba(255, 255, 255, 0.08); + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0)), + rgba(10, 8, 7, 0.6); +} + +.secondary-operation-card strong { + font-size: 0.98rem; +} + +.secondary-operation-card small { + color: var(--muted); + font-size: 0.72rem; +} + +.theater-intel-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 0.75rem; +} + +.intel-focus-card { + min-height: 142px; + padding: 0.9rem; + border-radius: 18px; + border: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(10, 8, 7, 0.52); +} + +.intel-focus-card.system { + display: grid; + grid-template-rows: auto auto 1fr; +} + +.intel-system-links { + display: flex; + flex-wrap: wrap; + gap: 0.45rem; + align-content: end; + margin-top: 0.7rem; +} + +.survivor-panel { + grid-template-rows: auto auto auto auto auto; +} + +.survivor-head-chips { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 0.45rem; +} + +.survivor-rig { + display: grid; + grid-template-columns: 110px minmax(0, 1fr) 110px; + gap: 0.65rem; + min-height: 328px; +} + +.rig-column { + display: grid; + gap: 0.55rem; +} + +.rig-slot { + padding: 0.65rem; +} + +.rig-figure { + min-height: 328px; +} + +.rig-figure-overlay { + right: 0.85rem; + left: 0.85rem; + bottom: 4.85rem; + text-align: left; +} + +.rig-figure-overlay strong { + display: block; + margin-top: 0.25rem; + font-size: 0.92rem; +} + +.survivor-metrics-strip { + position: absolute; + left: 0.85rem; + right: 0.85rem; + bottom: 0.85rem; + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 0.45rem; +} + +.survivor-metric-card { + padding: 0.6rem; + border-radius: 14px; + border: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(7, 6, 5, 0.58); +} + +.survivor-metric-card strong { + font-size: 1.1rem; +} + +.survivor-warning-strip { + display: grid; + gap: 0.5rem; +} + +.survivor-warning-card { + display: grid; + grid-template-columns: 42px minmax(0, 1fr); + gap: 0.65rem; + align-items: start; + padding: 0.75rem; + border-radius: 16px; + border: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(10, 8, 7, 0.48); +} + +.survivor-warning-card strong { + display: block; + font-size: 0.94rem; +} + +.survivor-warning-card p { + margin: 0.26rem 0 0; + color: rgba(239, 226, 206, 0.78); + font-size: 0.78rem; + line-height: 1.42; +} + +.survivor-supply-grid { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 0.5rem; +} + +.survivor-supply-card { + min-height: 120px; + display: grid; + gap: 0.45rem; + align-content: start; + text-align: left; + padding: 0.6rem; + border-radius: 16px; + border: 1px solid rgba(255, 255, 255, 0.08); + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0)), + rgba(10, 8, 7, 0.54); +} + +.survivor-supply-card .asset-thumb { + width: 100%; + min-width: 0; +} + +.survivor-supply-copy span { + display: block; + margin-top: 0.22rem; + color: #f1c27b; +} + +.survivor-supply-copy small { + display: block; + margin-top: 0.22rem; + color: var(--muted); + font-size: 0.68rem; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.survivor-supply-card.locked { + place-items: center; + border-style: dashed; +} + +.tactical-dock { + grid-template-columns: 208px minmax(0, 1fr) 196px; +} + +.tactical-operator { + min-width: 0; +} + +.dock-commit { + display: flex; + justify-content: flex-end; +} + +.dock-commit-button { + width: 100%; + min-height: 48px; +} + @media (max-width: 1580px) { .top-hud { grid-template-columns: 250px 104px minmax(0, 1fr); @@ -1670,12 +2176,28 @@ input, } .game-grid { - grid-template-columns: 286px minmax(0, 1fr) 372px; + grid-template-columns: 248px minmax(0, 1fr) 330px; } .loadout-matrix { grid-template-columns: 112px minmax(0, 1fr) 112px; } + + .theater-deck { + grid-template-columns: minmax(0, 1fr) 280px; + } + + .theater-scene-copy { + right: 18rem; + } + + .theater-scene-side { + width: 260px; + } + + .survivor-rig { + grid-template-columns: 96px minmax(0, 1fr) 96px; + } } @media (max-width: 1340px) { @@ -1713,6 +2235,45 @@ input, .command-dock { align-items: start; } + + .theater-panel, + .panel-route.expedition-panel, + .survivor-panel { + grid-template-rows: auto; + } + + .theater-scene { + min-height: 320px; + } + + .theater-scene-copy, + .theater-scene-side { + position: absolute; + } + + .theater-scene-copy { + right: 1rem; + } + + .theater-scene-side { + width: 260px; + } + + .theater-deck, + .theater-intel-grid, + .survivor-rig, + .tactical-dock { + grid-template-columns: 1fr; + } + + .rig-column { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .survivor-head, + .expedition-head { + align-items: flex-start; + } } @media (max-width: 860px) { @@ -1733,6 +2294,35 @@ input, grid-template-columns: 1fr; } + .theater-scene { + min-height: 360px; + } + + .theater-scene-copy, + .theater-scene-side { + position: static; + width: auto; + } + + .theater-scene { + display: grid; + align-content: end; + gap: 0.7rem; + padding: 1rem; + } + + .theater-scene::after { + inset: 0; + } + + .expedition-status-grid, + .theater-intel-grid, + .survivor-metrics-strip, + .rig-column, + .survivor-supply-grid { + grid-template-columns: 1fr; + } + .quick-slot-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } @@ -1760,4 +2350,8 @@ input, .new-game-card { min-height: 420px; } + + .expedition-route-card { + grid-template-columns: 1fr; + } } diff --git a/apps/web/src/features/game/components/BottomDock.tsx b/apps/web/src/features/game/components/BottomDock.tsx index 10c9766..04f0181 100644 --- a/apps/web/src/features/game/components/BottomDock.tsx +++ b/apps/web/src/features/game/components/BottomDock.tsx @@ -1,22 +1,27 @@ import type { OverlayPanel } from '../types'; -import type { GameView } from '@tinywaste/game-core'; -import { getActiveQuestCount, getSupplyCounts } from '../hudModel'; +import type { GameAction, GameView } from '@tinywaste/game-core'; +import { getActiveQuestCount, getCurrentPlace, getDominantAlert, getSupplyCounts } from '../hudModel'; export function BottomDock({ activePanel, onOpenPanel, + onSendAction, view, }: { activePanel: OverlayPanel | null; onOpenPanel: (panel: OverlayPanel) => void; + onSendAction: (action: GameAction) => void; view: GameView; }) { const supplyCounts = getSupplyCounts(view); const activeQuestCount = getActiveQuestCount(view); + const currentPlace = getCurrentPlace(view); + const homeRoute = view.map.edges.find((edge) => edge.to === 'home'); + const dominantAlert = getDominantAlert(view); return ( -