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 (
);
}
/* ═══════════════════════════════════════════════════════
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 ═══════ */}
{/* ═══════ 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) => (
))}
{difficulty === "easy" && (
LOCKED TO PHASE I
)}
{difficulty === "medium" && state.turn_number > 15 && (
PHASE III DISABLED
)}
{/* Mahoraga Wheel Visualization */}
{/* 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}
)}
>
);
}