diff --git a/ui/app/pages/Mission.tsx b/ui/app/pages/Mission.tsx index d12d3fa..1719273 100644 --- a/ui/app/pages/Mission.tsx +++ b/ui/app/pages/Mission.tsx @@ -1,44 +1,87 @@ import React, { useState, useEffect, useCallback, useMemo, useRef } from "react"; import { useNavigate, useParams } from "react-router-dom"; -import { Flex } from "@dynatrace/strato-components/layouts"; -import { Surface } from "@dynatrace/strato-components/layouts"; -import { - Heading, - Paragraph, - Strong, - Text, -} from "@dynatrace/strato-components/typography"; -import { Button } from "@dynatrace/strato-components/buttons"; -import { Chip } from "@dynatrace/strato-components-preview/content"; -import { - RadioGroup, - Radio, -} from "@dynatrace/strato-components-preview/forms"; -import { SuccessIcon } from "@dynatrace/strato-icons"; import { getMissionById } from "../data/missions"; import { MatrixBackground } from "../components/MatrixBackground"; - +const roomBg = "/ui/assets/room-bg.jpg"; type CheckpointStatus = "locked" | "active" | "completed"; const TIME_BONUS_PER_SECOND = 0.5; const HINT_PENALTY = 50; const WRONG_ANSWER_PENALTY = 100; +const CHOICE_KEYS = ["A", "B", "C", "D", "E", "F", "G", "H"]; + function formatTime(totalSeconds: number): string { const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`; } +/** Renders checkpoint instruction text, styling backtick-wrapped DQL as inline code */ +function renderInstruction(text: string): React.ReactNode { + const parts = text.split(/(`[^`]+`)/g); + return parts.map((part, i) => { + if (part.startsWith("`") && part.endsWith("`")) { + return ( + + {part.slice(1, -1)} + + ); + } + return {part}; + }); +} + +const DIFFICULTY_COLORS: Record = { + rookie: "rgba(29,187,126,0.7)", + operator: "rgba(255,191,0,0.7)", + elite: "rgba(231,76,60,0.7)", + legend: "rgba(231,76,60,0.7)", +}; + +const DIFFICULTY_BG: Record = { + rookie: "rgba(29,187,126,0.1)", + operator: "rgba(255,191,0,0.1)", + elite: "rgba(231,76,60,0.1)", + legend: "rgba(231,76,60,0.1)", +}; + +const scanLineStyle: React.CSSProperties = { + position: "absolute", + inset: 0, + background: + "repeating-linear-gradient(to bottom, transparent 0px, transparent 3px, rgba(0,0,0,0.05) 3px, rgba(0,0,0,0.05) 4px)", + pointerEvents: "none", + zIndex: 10, +}; + +const screenBase: React.CSSProperties = { + borderRadius: 4, + border: "1px solid rgba(255,255,255,0.07)", + overflow: "hidden", + position: "absolute", + background: "rgba(2,4,12,0.82)", + backdropFilter: "blur(2px)", + display: "flex", + flexDirection: "column", +}; + export const Mission = () => { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const mission = id ? getMissionById(id) : undefined; const [currentCheckpoint, setCurrentCheckpoint] = useState(0); - const [completedCheckpoints, setCompletedCheckpoints] = useState( - [] - ); + const [completedCheckpoints, setCompletedCheckpoints] = useState([]); const [timerSeconds, setTimerSeconds] = useState(null); const [isValidating, setIsValidating] = useState(false); const [selectedAnswer, setSelectedAnswer] = useState(""); @@ -50,6 +93,20 @@ export const Mission = () => { const [abandonConfirm, setAbandonConfirm] = useState(false); const hasTimedOutRef = useRef(false); + // Inject keyframes + useEffect(() => { + const styleId = "mission-war-room-keyframes"; + if (document.getElementById(styleId)) return; + const style = document.createElement("style"); + style.id = styleId; + style.textContent = `@keyframes missionLiveDot { 0%, 100% { opacity: 1; } 50% { opacity: 0.25; } }`; + document.head.appendChild(style); + return () => { + const el = document.getElementById(styleId); + if (el) el.remove(); + }; + }, []); + // Initialize timer when mission loads useEffect(() => { if (mission) { @@ -125,6 +182,7 @@ export const Mission = () => { const handleValidate = useCallback( (index: number) => { + console.log("handleValidate called, index:", index, "selectedAnswer:", selectedAnswer, "isValidating:", isValidating); if (isValidating || !mission) return; const checkpoint = mission.checkpoints[index]; @@ -202,313 +260,536 @@ export const Mission = () => { }); }, [mission, timerSeconds, baseScore, hintsUsed, navigate]); - // Mission not found + // ── Room shell (shared by all states) ── + const roomShell = (children: React.ReactNode) => ( +
+ {/* Room photo */} +
+ {/* UI layer */} +
+ {children} +
+
+ ); + + // ── Mission not found ── if (!mission) { - return ( - - - - Mission Not Found - - The requested mission does not exist or has been decommissioned. - - - - - + return roomShell( +
+
Mission Not Found
+
+ The requested mission does not exist or has been decommissioned. +
+ +
); } const displaySeconds = timerSeconds ?? mission.timerSeconds; const timerIsLow = displaySeconds < 60; - - return ( -
- - - {/* Left column — Briefing panel */} - - Mission - - - {/* Title + codename */} - - - {mission.codename} - - - {mission.title} - - {/* Role + difficulty badges */} - - {mission.role} - 0 : false; + const hintRevealed = checkpoint ? hintsRevealed.includes(checkpoint.id) : false; + + return roomShell( + <> + {/* ═══ LEFT screen — MatrixBackground + Timer + Abandon ═══ */} +
+
+ {/* Matrix canvas wrapper */} +
+ +
+ {/* Bottom dock: timer + abandon */} +
+
+ Time Remaining +
+
+ {formatTime(displaySeconds)} +
+
+ {!abandonConfirm ? ( + setAbandonConfirm(true)} + style={{ + fontSize: 11, + fontWeight: 600, + textTransform: "uppercase", + color: "rgba(220,80,80,0.9)", + cursor: "pointer", + letterSpacing: "0.08em", + border: "1px solid rgba(220,80,80,0.4)", + padding: "6px 12px", + borderRadius: 3, + background: "rgba(220,80,80,0.1)", + }} > - {mission.difficulty.toUpperCase()} - - - - {/* Briefing */} - {mission.briefing} - - {/* Timer */} - - - Time Remaining - + Abandon Mission + + ) : ( +
+ + This will end your mission. + +
+ navigate("/missions")} + style={{ + fontSize: 9, + textTransform: "uppercase", + color: "rgba(231,76,60,0.7)", + cursor: "pointer", + letterSpacing: "0.08em", + }} + > + Confirm + setAbandonConfirm(false)} style={{ - fontFamily: "monospace", - color: timerIsLow ? "var(--dt-colors-text-critical-default, #e74c3c)" : undefined, + fontSize: 9, + textTransform: "uppercase", + color: "rgba(150,170,200,0.4)", + cursor: "pointer", + letterSpacing: "0.08em", }} > - {formatTime(displaySeconds)} + Cancel - - - - - {/* Hint cost notice */} - - Intel requested: -{HINT_PENALTY} pts per hint used - - - - - - {/* Abandon Mission */} -
- {!abandonConfirm ? ( -
+
+ )} +
+
+
+ + {/* ═══ CENTER screen — Checkpoint ═══ */} +
+
+ {/* Top bar */} +
+ + {mission.title} — CP {currentCheckpoint + 1} of {mission.checkpoints.length} + +
+ + {formatTime(displaySeconds)} + +
+ + {/* Checkpoint body */} +
+ {/* Progress pips */} +
+ {mission.checkpoints.map((_, i) => { + const st = getCheckpointStatus(i); + let bg = "rgba(255,255,255,0.08)"; + if (st === "completed") bg = "#1dbb7e"; + if (st === "active") bg = "#1496ff"; + return ( +
+ ); + })} + - Abandon Mission - - ) : ( + Checkpoint {currentCheckpoint + 1} + +
+ + {cpStatus === "completed" || completedCheckpoints.includes(currentCheckpoint) ? ( + /* ── Completed state ── */
- This will end your mission. -
-
+ ) : checkpoint ? ( + <> + {/* Section label */} +
+ {checkpoint.title} +
+ + {/* Question text */} +
+ {renderInstruction(checkpoint.instruction)} +
+ + {/* Hint revealed */} + {hintRevealed && ( +
- Confirm - + {checkpoint.hint} +
+ )} + + {/* Multiple choice options */} + {isMultipleChoice && checkpoint.choices && ( +
+ {checkpoint.choices.map((option, i) => { + const isSelected = selectedAnswer === option; + return ( +
{ + console.log("choice clicked:", option); + setSelectedAnswer(option); + }} + style={{ + padding: "6px 10px", + borderRadius: 4, + border: isSelected + ? "1px solid rgba(20,150,255,0.45)" + : "1px solid rgba(255,255,255,0.07)", + fontSize: 11, + color: isSelected ? "#c8d4e8" : "rgba(210,225,240,0.85)", + background: isSelected + ? "rgba(20,150,255,0.1)" + : "rgba(255,255,255,0.025)", + cursor: "pointer", + display: "flex", + alignItems: "center", + gap: 8, + }} + > + + {CHOICE_KEYS[i] ?? String(i + 1)} + + {option} +
+ ); + })} +
+ )} + + {/* Answer error */} + {answerError && ( +
{answerError}
+ )} + + {/* Action row */} +
+ {/* Hint button */} +
+ {hintAvailable && !hintRevealed && ( + handleRequestHint(mission.checkpoints[currentCheckpoint].id)} + style={{ + fontSize: 11, + fontWeight: 600, + textTransform: "uppercase", + color: "rgba(255,255,255,0.8)", + cursor: "pointer", + letterSpacing: "0.08em", + border: "1px solid rgba(255,255,255,0.3)", + padding: "6px 12px", + borderRadius: 3, + background: "rgba(255,255,255,0.08)", + }} + > + ⊹ Request intel (−{HINT_PENALTY} pts) + + )} +
+ {/* Validate button */}
-
- )} + + ) : null}
- - - {/* Right column — Checkpoints */} - + + {/* ═══ RIGHT screen — Mission Briefing ═══ */} +
+
+
+ {/* Eyebrow */} +
+ Mission Briefing +
+ + {/* Mission title */} +
{mission.title}
+ + {/* Tags */} +
+ + {mission.role} + + + {mission.difficulty} + +
+ + {/* Divider */} +
+ + {/* Briefing text */} +
+ {mission.briefing} +
+
+
+ + {/* ── Bottom HUD strip ── */} +
- Checkpoints - - {mission.checkpoints.map((checkpoint, index) => { - const status = getCheckpointStatus(index); - const isMultipleChoice = checkpoint.type === "multiple-choice"; - const hintAvailable = checkpoint.hint.length > 0; - const hintRevealed = hintsRevealed.includes(checkpoint.id); - const isActive = status === "active"; - const isCompleted = status === "completed"; - const isLocked = status === "locked"; - - return ( - - - {/* Step number / status indicator */} - - {isCompleted ? ( - - ) : ( - - - {String(index + 1).padStart(2, "0")} - - - )} - - - {/* Content */} - - - {checkpoint.title} - {isCompleted && ( - - - Complete - - - - {checkpoint.points} pts - - - - )} - {isActive && ( - - Active - - )} - {isLocked && ( - Locked - )} - - - {!isLocked && ( - {checkpoint.instruction} - )} - - {/* Hint system */} - {isActive && hintAvailable && !hintRevealed && ( - - - - )} - - {hintRevealed && ( - - - - Intel - - {checkpoint.hint} - - Intel requested: -{HINT_PENALTY} pts - - - - )} - - {/* Multiple choice options */} - {isActive && isMultipleChoice && checkpoint.choices && ( - - setSelectedAnswer(value)} - > - {checkpoint.choices.map((option) => ( - - {option} - - ))} - - {answerError && ( - - {answerError} - - )} - - )} - - {/* Validate button */} - {isActive && ( - - - - )} - - - - ); - })} - - -
+ {/* Pulsing red dot */} +
+ Mission Control +
+ + {mission.codename} / {mission.title} + +
+ Operator Active +
+ Playground Env +
+ ); }; diff --git a/ui/assets/room-bg.jpg b/ui/assets/room-bg.jpg new file mode 100644 index 0000000..e69de29 diff --git a/ui/types.d.ts b/ui/types.d.ts index 2e9cb26..4dde23d 100644 --- a/ui/types.d.ts +++ b/ui/types.d.ts @@ -7,3 +7,8 @@ declare module "*.png" { const src: string; export default src; } + +declare module "*.jpg" { + const src: string; + export default src; +}