import { useState, useRef, useEffect, useCallback } from "react"; import { motion, AnimatePresence } from "framer-motion"; import "./index.css"; const API = ""; /* ═══════════════════════════════════════════════════════ SUBCOMPONENTS ═══════════════════════════════════════════════════════ */ /* ── HP Bar ── */ function HpBar({ current, max, color, label }) { const pct = Math.max(0, (current / max) * 100); const grad = color === "red" ? "linear-gradient(90deg,#991b1b,#dc2626,#f87171)" : "linear-gradient(90deg,#065f46,#059669,#34d399)"; const glow = color === "red" ? "rgba(248,113,113,0.35)" : "rgba(52,211,153,0.35)"; return (
{label} {current} /{max} {pct.toFixed(0)}%
); } /* ── Resistance Row ── */ function ResBar({ label, icon, value, flashing }) { const pct = (value / 80) * 100; let tag, tc; if (value >= 80) { tag = "IMMUNE"; tc = "text-cyan"; } else if (value >= 60) { tag = "HARD"; tc = "text-cyan"; } else if (value > 0) { tag = `${value}`; tc = "text-amber"; } else { tag = "—"; tc = "text-muted/40"; } return ( {icon} {label}
{tag}
); } /* ── Action Button ── */ function Btn({ label, onClick, variant = "default", disabled }) { const styles = { default: "border-outline-variant/40 text-muted hover:text-text hover:border-cyan/30 hover:bg-cyan-dim", danger: "border-red/30 text-red hover:bg-red-dim", primary: "border-cyan/30 text-cyan hover:bg-cyan-dim", reset: "border-outline/30 text-muted/60 hover:text-muted", }; return ( {label} ); } /* ── Judgment Overlay ── */ function JudgmentOverlay({ show }) { return ( {show && (
JUDGMENT STRIKE
— stack consumed —
)}
); } /* ── Stat Chip (small inline metric) ── */ function StatChip({ label, value, color = "text-text" }) { return (
{label}
{value}
); } /* ═══════════════════════════════════════════════════════ MAIN APP ═══════════════════════════════════════════════════════ */ /* ── Attack category colors ── */ const CAT_COLORS = { PHYSICAL: { text: "text-orange-400", bg: "bg-orange-500/15", border: "border-orange-500/30", hex: "#f97316" }, CE: { text: "text-purple-400", bg: "bg-purple-500/15", border: "border-purple-500/30", hex: "#a855f7" }, TECHNIQUE: { text: "text-teal-400", bg: "bg-teal-500/15", border: "border-teal-500/30", hex: "#06b6d4" }, }; const catColor = (type) => CAT_COLORS[type] || CAT_COLORS.PHYSICAL; export default function App() { const [state, setState] = useState(null); const [logs, setLogs] = useState([]); const [loading, setLoading] = useState(false); const [shakeClass, setShakeClass] = useState(""); const [flashRes, setFlashRes] = useState(null); const [showJudgment, setShowJudgment] = useState(false); const [wheelRot, setWheelRot] = useState(0); const [adaptFlash, setAdaptFlash] = useState(false); const [lastLog, setLastLog] = useState(null); const [difficulty, setDifficulty] = useState("hard"); const [autoPlay, setAutoPlay] = useState(false); const [modelStatus, setModelStatus] = useState(null); const logRef = useRef(null); const prevRes = useRef({ Physical: 0, CE: 0, Technique: 0 }); const autoRef = useRef(null); useEffect(() => { if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight; }, [logs]); const triggerShake = useCallback((heavy) => { setShakeClass(heavy ? "shake-heavy" : "shake-sm"); setTimeout(() => setShakeClass(""), heavy ? 500 : 350); }, []); const MOCK_STATE = { enemy_hp: 856, enemy_hp_max: 2000, mahoraga_hp: 1400, mahoraga_hp_max: 1500, resistances: { Physical: 40, CE: 80, Technique: 15 }, adaptation_stack: 3, heal_cooldown: 0, turn_number: 7, max_turns: 30, done: false, done_reason: null, turn_log: null, difficulty: "hard", }; async function doReset(diff, clearLogs = true) { const d2use = diff || difficulty; setLoading(true); setAutoPlay(false); try { const r = await fetch(`${API}/api/reset`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ difficulty: d2use }), }); const d = await r.json(); setState(d); } catch { setState({ ...MOCK_STATE, difficulty: d2use }); } if (clearLogs) { setLogs([]); setLastLog(null); } setWheelRot(0); prevRes.current = { Physical: 0, CE: 0, Technique: 0 }; setLoading(false); } function processStepResult(d) { const log = d.turn_log; if (log) { setLastLog(log); if (log.damage_taken > 150) triggerShake(true); else if (log.damage_taken > 80) triggerShake(false); if (log.correct_adaptation) { setAdaptFlash(true); setTimeout(() => setAdaptFlash(false), 1200); } if (log.correct_adaptation) setWheelRot((p) => p + 45); else if (log.mahoraga_action === "Judgment Strike" && log.damage_dealt > 0) setWheelRot((p) => p + 180); else setWheelRot((p) => p + 10); if (log.mahoraga_action === "Judgment Strike" && log.damage_dealt > 200) { setShowJudgment(true); triggerShake(true); setTimeout(() => setShowJudgment(false), 2000); } setLogs((prev) => [...prev, log]); } if (d.resistances) { const p = prevRes.current; for (const k of ["Physical", "CE", "Technique"]) { if (d.resistances[k] > p[k]) { setFlashRes(k); setTimeout(() => setFlashRes(null), 500); break; } } prevRes.current = { ...d.resistances }; } setState(d); setLoading(false); } async function doStep(action) { if (!state || state.done || loading) return; setLoading(true); try { const r = await fetch(`${API}/api/step`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ player_action: action }), }); if (!r.ok) { setLoading(false); return; } const d = await r.json(); processStepResult(d); } catch { setLoading(false); } } /* ── Auto-play timer ── */ useEffect(() => { if (autoPlay && state && !state.done && !loading) { autoRef.current = setTimeout(async () => { try { const r = await fetch(`${API}/api/step`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ player_action: null }) }); if (!r.ok) { setAutoPlay(false); return; } const d = await r.json(); processStepResult(d); } catch { setAutoPlay(false); } }, 1200); } return () => clearTimeout(autoRef.current); }, [autoPlay, state, loading]); /* ── Stop auto-play when game ends ── */ useEffect(() => { if (state?.done) { setAutoPlay(false); // Auto-reset stats after 3 seconds, but keep combat logs const t = setTimeout(() => doReset(null, false), 3000); return () => clearTimeout(t); } }, [state?.done]); /* ── Check model status on mount ── */ useEffect(() => { doReset(); fetch(`${API}/api/model-status`).then(r => r.json()).then(setModelStatus).catch(() => {}); }, []); /* ── Loading state ── */ if (!state) return (
); const done = state.done; return ( <>
{/* ═══════ HEADER ═══════ */}
MAHORAGA
Adaptive Combat AI • RL + LLM Engine
{autoPlay && ( ● AUTO-PLAY )} {difficulty} T{state.turn_number}/{state.max_turns} {done ? state.done_reason || "ENDED" : "LIVE"}
{/* ═══════ MAIN BENTO GRID ═══════ */}
{/* ── COL 1-5: Left Column (Boss + Player stacked) ── */}
{/* Boss: Mahoraga (LLM-powered adaptive enemy) */}
smart_toy MAHORAGA LLM BOSS
{state.llm_raw ? 'AI THINKING...' : 'AWAITING'}
{state.adaptation_stack} } color="text-cyan" /> = 3 ? "MAX" : state.adaptation_stack >= 2 ? "HIGH" : "LOW"} color={state.adaptation_stack >= 3 ? "text-red" : state.adaptation_stack >= 2 ? "text-amber" : "text-green"} />
{/* Player Status (user-controlled, compact) */}
person CHALLENGER YOU
{difficulty.toUpperCase()} MODE
{/* Resistances */}
security MAHORAGA RESISTANCES
{/* ── COL 6-8: Center Column (Wheel + Phase + Tactics) ── */}
{/* Boss AI Phase */}
BOSS AI INTELLIGENCE PHASE
{[ { n: "I", label: "TUTORIAL", desc: "Always Physical", active: state.turn_number <= 5 }, { n: "II", label: "PATTERN", desc: "Cycling + 15% RNG", active: state.turn_number > 5 && state.turn_number <= 15 }, { n: "III", label: "ADAPTIVE", desc: "Targets weakness", active: state.turn_number > 15 }, ].map((ph) => (
{ph.n}
{ph.label}
))}
{difficulty === "easy" && (
LOCKED TO PHASE I
)} {difficulty === "medium" && state.turn_number > 15 && (
PHASE III DISABLED
)}
{/* Mahoraga Wheel Visualization */}
{adaptFlash && (
)}
{/* Compact wheel stats */}
Stack {state.adaptation_stack}
Rot {(wheelRot / 45).toFixed(0)}
{/* Tactical Summary */}
YOUR LAST ATTACK
{lastLog ? (
{lastLog.enemy_attack_type} {lastLog.enemy_subtype} -{lastLog.damage_taken}
) : (
NO DATA
)}
{/* Adaptation Banner */} {lastLog && lastLog.correct_adaptation ? (
published_with_changes MAHORAGA ADAPTED
YOUR {lastLog.enemy_attack_type} WAS COUNTERED
Boss adapted to your attack type.
) : lastLog ? (
sync_problem MAHORAGA: {lastLog.mahoraga_action}
You dealt: {lastLog.damage_taken} · Boss dealt: {lastLog.damage_dealt}
) : (
Awaiting engagement...
)}
{/* ── COL 9-12: Right Column (Combat Log) ── */}
list_alt COMBAT LOG
{logs.length} events
{logs.length === 0 ? (
{">"} AWAITING COMBAT DATA...
) : ( logs.map((l, i) => (
T{l.turn}
{l.correct_adaptation && ( published_with_changes )} 0 ? "text-green" : "text-red/60"}`}> {l.reward > 0 ? "+" : ""}{l.reward}
{/* Player action line */}
YOU {l.enemy_attack_type} {l.enemy_subtype} -{l.damage_taken} to boss
{/* Mahoraga response line */}
BOSS {l.mahoraga_action} {l.correct_adaptation && ADAPTED!} {l.damage_dealt > 0 && -{l.damage_dealt} to you}
)) )}
{/* ═══════ BOTTOM ACTION BAR ═══════ */}
{/* Difficulty selector */} {["easy", "medium", "hard"].map((d) => ( { setDifficulty(d); doReset(d); }} className={`px-2 py-1 rounded-md text-[8px] font-bold tracking-wider uppercase border cursor-pointer transition-all ${ difficulty === d ? d === "easy" ? "bg-green/15 text-green border-green/40" : d === "medium" ? "bg-amber/15 text-amber border-amber/40" : "bg-red/15 text-red border-red/40" : "bg-surface/40 text-muted/50 border-outline-variant/20 hover:text-muted" }`} > {d === "medium" ? "MED" : d} ))}
{/* Manual player attacks */} doStep("PHYSICAL")} disabled={done || autoPlay} /> doStep("CE")} disabled={done || autoPlay} /> doStep("TECHNIQUE")} disabled={done || autoPlay} />
{/* Auto-play + Reset */} setAutoPlay(!autoPlay)} disabled={done} className={`px-3 py-1.5 rounded-md text-[9px] font-bold tracking-wider uppercase border cursor-pointer transition-all disabled:opacity-25 disabled:cursor-not-allowed ${ autoPlay ? "bg-amber/20 text-amber border-amber/40 animate-pulse" : "bg-surface/60 text-muted border-outline-variant/30 hover:text-cyan hover:border-cyan/30" }`} > {autoPlay ? "⏸ STOP AUTO" : "▶ AUTO-PLAY"} doReset()} variant="reset" /> {/* Reward indicator */} {lastLog && ( 0 ? "text-green" : "text-red"}`} > {lastLog.reward > 0 ? "+" : ""} {lastLog.reward} )}
{/* ═══════ DONE OVERLAY ═══════ */} {done && (
ENGAGEMENT OVER
{state.done_reason}
You: {state.enemy_hp} HP | Mahoraga (Boss): {state.mahoraga_hp} HP | T{state.turn_number}
)}
); }