diff --git a/Dockerfile.client b/Dockerfile.client index b25b581..ef9b1f8 100644 --- a/Dockerfile.client +++ b/Dockerfile.client @@ -1,4 +1,9 @@ +# ============================================================================ +# Multi-Stage Build for React 18 + Vite Client & Nginx Proxy (:3000) +# ============================================================================ + FROM node:22-alpine as builder + WORKDIR /app COPY client/package*.json ./ @@ -7,25 +12,47 @@ RUN npm install COPY client ./ RUN npm run build +# Nginx Production Serving Layer FROM nginx:alpine + COPY --from=builder /app/dist /usr/share/nginx/html + +RUN rm -f /etc/nginx/conf.d/default.conf + COPY <<-'EOF_NGINX' /etc/nginx/conf.d/default.conf server { listen 3000; + server_name localhost; + location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } + + # Authoritative Rust Engine Proxy location /api/ { - proxy_pass http://vtt-engine:8080/api/; + proxy_pass http://vtt-engine:8088/api/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; } + + # Python Multi-Agent Orchestrator Proxy + location /api/v1/ { + proxy_pass http://vtt-orchestrator:8000/api/v1/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + } + + # Rust WebSocket CRDT Relay location /ws/ { - proxy_pass http://vtt-engine:8080/ws/; + proxy_pass http://vtt-engine:8088/ws/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; @@ -35,4 +62,7 @@ server { EOF_NGINX EXPOSE 3000 + +HEALTHCHECK --interval=5s --timeout=3s --retries=5 CMD wget -q --spider http://localhost:3000/ || exit 1 + CMD ["nginx", "-g", "daemon off;"] diff --git a/Dockerfile.python b/Dockerfile.python index c9a51db..14241f4 100644 --- a/Dockerfile.python +++ b/Dockerfile.python @@ -1,12 +1,22 @@ -FROM python:3.11-slim -WORKDIR /app +# ============================================================================ +# Python Multi-Agent Orchestrator & Invariant Auditor (:8000) +# ============================================================================ -RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/* +FROM python:3.12-slim + +WORKDIR /app +RUN apt-get update && apt-get install -y --no-install-recommends curl gcc python3-dev && rm -rf /var/lib/apt/lists/* COPY python/pyproject.toml ./ +COPY compendium ./compendium COPY python/vtt_orchestrator ./vtt_orchestrator RUN pip install --no-cache-dir . EXPOSE 8000 +ENV PYTHONPATH=/app +ENV PYTHONUNBUFFERED=1 + +HEALTHCHECK --interval=5s --timeout=3s --retries=5 CMD curl -f http://localhost:8000/api/v1/dynasty/factions || exit 1 + CMD ["uvicorn", "vtt_orchestrator.server:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/Dockerfile.rust b/Dockerfile.rust index 097ba1c..70cc081 100644 --- a/Dockerfile.rust +++ b/Dockerfile.rust @@ -1,17 +1,29 @@ -# Multi-stage Rust build for vtt-server +# ============================================================================ +# Multi-Stage Rust Build for Authoritative VTT Engine (:8088) +# ============================================================================ + FROM rust:1.85-slim-bookworm as builder WORKDIR /usr/src/vtt +RUN apt-get update && apt-get install -y --no-install-recommends pkg-config libssl-dev && rm -rf /var/lib/apt/lists/* + COPY Cargo.toml Cargo.lock ./ COPY crates ./crates RUN cargo build --release -p vtt-server +# Minimal Runtime Container FROM debian:bookworm-slim + WORKDIR /app -RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl && rm -rf /var/lib/apt/lists/* COPY --from=builder /usr/src/vtt/target/release/vtt-server /usr/local/bin/vtt-server -EXPOSE 8080 + +EXPOSE 8088 ENV RUST_LOG=info +ENV PORT=8088 + +HEALTHCHECK --interval=5s --timeout=3s --retries=5 CMD curl -f http://localhost:8088/health || exit 1 + CMD ["vtt-server"] diff --git a/client/src/App.tsx b/client/src/App.tsx index f34a102..e5ecf0a 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -11,6 +11,7 @@ import { CharacterBuilderView } from './components/CharacterBuilderView'; import { LobbyView } from './components/LobbyView'; import { DynastyView } from './components/DynastyView'; import { BundleManagerView } from './components/BundleManagerView'; +import { QuestDialogueView } from './components/QuestDialogueView'; import { WfcStudioView } from './components/WfcStudioView'; import { AnalyticsView } from './components/AnalyticsView'; import { ParticleFXManager } from './render/particle_effects'; @@ -20,6 +21,7 @@ import { globalWebRTCMesh } from './render/webrtc_mesh'; export function App() { const [currentView, setCurrentView] = useState('tabletop'); + const [campaignTitle, setCampaignTitle] = useState('The Fall of Baron Vane'); const [isSafetyOpen, setIsSafetyOpen] = useState(false); const [isAudioMixerOpen, setIsAudioMixerOpen] = useState(false); const [latencyMs, setLatencyMs] = useState(8); @@ -56,6 +58,7 @@ export function App() { color: '#8b5cf6', isPlayer: true, avatarIconType: 'caster', + elevationFeet: 15, }, { id: 'orc_warlord_1', @@ -457,6 +460,23 @@ export function App() { ]); }; + const handleInjectQuest = (questTitle: string, initialObjective: string) => { + setCampaignTitle(questTitle); + addSystemMessage(`QUEST ACTIVATED: ${questTitle}`); + setMessages((prev) => [ + ...prev, + { + id: `quest_${Date.now()}`, + sender: 'Campaign Director (AI)', + role: 'system', + content: `⚔️ Quest Activated: "${questTitle}"\n\n${initialObjective}`, + timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), + }, + ]); + globalSpatialAudio.playSpatialCreatureRoar(4, 4); + setCurrentView('tabletop'); + }; + const handleApplyWfcMap = (matrix: number[][], width: number, height: number) => { const newWalls: { x: number; y: number }[] = []; for (let y = 0; y < height; y++) { @@ -480,7 +500,7 @@ export function App() { onOpenSafety={() => setIsSafetyOpen(true)} onOpenAudioMixer={() => setIsAudioMixerOpen(true)} latencyMs={latencyMs} - campaignName="The Fall of Baron Vane" + campaignName={campaignTitle} /> {/* View Content */} @@ -559,6 +579,10 @@ export function App() { /> )} + {currentView === 'quests' && ( + + )} + {currentView === 'wfc' && ( )} diff --git a/client/src/components/Navbar.tsx b/client/src/components/Navbar.tsx index 916e01d..4721f42 100644 --- a/client/src/components/Navbar.tsx +++ b/client/src/components/Navbar.tsx @@ -16,11 +16,12 @@ import { Crown, Package, Radio, - Sliders + Sliders, + Scroll } from 'lucide-react'; import { globalAudio } from '../render/audio_manager'; -export type SaaSView = 'tabletop' | 'compendium' | 'builder' | 'lobby' | 'dynasty' | 'bundles' | 'wfc' | 'analytics'; +export type SaaSView = 'tabletop' | 'compendium' | 'builder' | 'lobby' | 'dynasty' | 'bundles' | 'quests' | 'wfc' | 'analytics'; interface NavbarProps { currentView: SaaSView; @@ -54,6 +55,7 @@ export const Navbar: React.FC = ({ { id: 'lobby', label: 'Campaign Lobby', icon: }, { id: 'dynasty', label: 'Dynasty & Factions', icon: }, { id: 'bundles', label: 'Campaign Bundles', icon: }, + { id: 'quests', label: 'Quest & Dialogue', icon: }, { id: 'wfc', label: 'WFC Dungeon Studio', icon: }, { id: 'analytics', label: 'SLA Telemetry', icon: }, ]; diff --git a/client/src/components/QuestDialogueView.tsx b/client/src/components/QuestDialogueView.tsx new file mode 100644 index 0000000..5980dbc --- /dev/null +++ b/client/src/components/QuestDialogueView.tsx @@ -0,0 +1,352 @@ +import React, { useState, useEffect } from 'react'; +import { + Compass, + GitBranch, + Shield, + Award, + Coins, + Sparkles, + CheckCircle, + AlertTriangle, + Send, + RefreshCw, + Users, + Scroll, + ArrowRight, + Flame, + Check, +} from 'lucide-react'; + +interface QuestChoice { + choice_id: string; + target_node_id: string; + prompt_text: string; + skill_check_required?: [string, number]; + faction_reputation_deltas: Record; + rewards_gold: number; + rewards_xp: number; +} + +interface QuestNode { + node_id: string; + node_type: string; + title: string; + narrative_prompt: string; + associated_faction_id?: string; + choices: QuestChoice[]; +} + +interface QuestGraph { + quest_id: string; + title: string; + summary: string; + initial_node_id: string; + nodes: Record; +} + +interface ConcordiaPactResult { + pact_agreed: boolean; + final_terms: string; + house_a_approval: number; + house_b_approval: number; + reputation_deltas: Record; + consequence_narrative: string; +} + +interface QuestDialogueViewProps { + onInjectQuest?: (questTitle: string, initialObjective: string) => void; +} + +export const QuestDialogueView: React.FC = ({ onInjectQuest }) => { + const [quest, setQuest] = useState(null); + const [currentNodeId, setCurrentNodeId] = useState('node_hook'); + const [history, setHistory] = useState(['node_hook']); + const [diplomacyRoll, setDiplomacyRoll] = useState(16); + const [concessions, setConcessions] = useState('Equal trade tariff exemptions and shared mining rights'); + const [pactResult, setPactResult] = useState(null); + const [isNegotiating, setIsNegotiating] = useState(false); + const [injectedSuccess, setInjectedSuccess] = useState(false); + + useEffect(() => { + fetchQuest(); + }, []); + + const fetchQuest = async () => { + try { + const res = await fetch('/api/v1/quest/active'); + if (res.ok) { + const data = await res.json(); + setQuest(data); + setCurrentNodeId(data.initial_node_id); + setHistory([data.initial_node_id]); + } + } catch (err) { + console.error('Error fetching quest:', err); + } + }; + + const handleSelectChoice = (targetNodeId: string) => { + setCurrentNodeId(targetNodeId); + setHistory((prev) => [...prev, targetNodeId]); + }; + + const handleNegotiatePact = async () => { + setIsNegotiating(true); + try { + const res = await fetch('/api/v1/quest/concordia-negotiate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + house_a: 'House Vane', + house_b: 'House Silverpeak', + diplomacy_roll: diplomacyRoll, + concessions_offered: concessions, + }), + }); + if (res.ok) { + const data = await res.json(); + setPactResult(data); + } + } catch (err) { + console.error('Error negotiating treaty:', err); + } finally { + setIsNegotiating(false); + } + }; + + const handleDeployToTabletop = () => { + if (quest && onInjectQuest) { + const currentNode = quest.nodes[currentNodeId] || quest.nodes[quest.initial_node_id]; + onInjectQuest(quest.title, currentNode.narrative_prompt); + setInjectedSuccess(true); + setTimeout(() => setInjectedSuccess(false), 3500); + } + }; + + if (!quest) { + return ( +
+ Loading Quest & Concordia Engine... +
+ ); + } + + const currentNode = quest.nodes[currentNodeId] || quest.nodes[quest.initial_node_id]; + + return ( +
+ {/* Header Banner */} +
+
+
+
+
+ +
+
+
+

{quest.title}

+ + DAG Quest Tree + +
+

{quest.summary}

+
+
+ + +
+
+ + {/* Main Grid: Interactive Dialogue Tree & Concordia Console */} +
+ {/* Left Column: Active Quest State & Decision Branches (7 cols) */} +
+
+
+
+ +

Active Quest Stage

+
+ + {currentNode.node_type} + +
+ + {/* Narrative Box */} +
+ "{currentNode.narrative_prompt}" +
+ + {/* Choices */} +
+

+ + Branching Choices & Decisions +

+ + {currentNode.choices.length === 0 ? ( +
+ + Questline Stage Complete! Outcome resolved. +
+ ) : ( +
+ {currentNode.choices.map((choice) => ( +
handleSelectChoice(choice.target_node_id)} + className="p-4 bg-slate-800/60 hover:bg-slate-800/90 border border-slate-700/60 hover:border-amber-500/50 rounded-lg cursor-pointer transition-all group shadow-sm" + > +
+

+ {choice.prompt_text} +

+ +
+ +
+ {choice.skill_check_required && ( + + DC {choice.skill_check_required[1]} {choice.skill_check_required[0]} + + )} + {choice.rewards_gold > 0 && ( + + + +{choice.rewards_gold} gp + + )} + {choice.rewards_xp > 0 && ( + + + +{choice.rewards_xp} XP + + )} +
+
+ ))} +
+ )} +
+ + {/* History Breadcrumbs */} + {history.length > 1 && ( +
+ Timeline: +
+ {history.map((hid, idx) => ( + setCurrentNodeId(hid)} + className="cursor-pointer hover:text-amber-300 hover:underline transition-colors" + > + {quest.nodes[hid]?.title || hid} {idx < history.length - 1 && '→'} + + ))} +
+
+ )} +
+
+ + {/* Right Column: Concordia Multi-NPC Negotiation Console (5 cols) */} +
+
+
+ +

Concordia Treaty Negotiation

+
+ +

+ Simulate autonomous multi-party negotiations between Noble House leaders. Offer concessions and roll diplomacy checks. +

+ +
+
+ +
+ setDiplomacyRoll(parseInt(e.target.value))} + className="w-full accent-amber-500 h-1.5 bg-slate-700 rounded-lg" + /> + + {diplomacyRoll} + +
+
+ +
+ +