Spaces:
Sleeping
Sleeping
Commit ·
dbcb71c
1
Parent(s): 4b89f2f
fix: swap combat roles - Mahoraga as AUTO boss, player as user-controlled, clear combat log labels
Browse files- api.py +15 -26
- env/mahoraga_env.py +22 -8
- frontend/src/App.jsx +94 -87
api.py
CHANGED
|
@@ -220,7 +220,7 @@ class CombatState(BaseModel):
|
|
| 220 |
|
| 221 |
|
| 222 |
class StepRequest(BaseModel):
|
| 223 |
-
|
| 224 |
|
| 225 |
|
| 226 |
class ResetRequest(BaseModel):
|
|
@@ -278,14 +278,23 @@ def reset(req: ResetRequest = ResetRequest()):
|
|
| 278 |
)
|
| 279 |
|
| 280 |
|
| 281 |
-
def _do_step(
|
| 282 |
-
"""Execute one turn of combat
|
| 283 |
global env
|
| 284 |
if env is None:
|
| 285 |
env = MahoragaEnv(difficulty=current_difficulty)
|
| 286 |
env.reset()
|
| 287 |
|
| 288 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
action_name = ACTION_NAMES.get(env.last_action, "Unknown")
|
| 290 |
|
| 291 |
turn_log = TurnLog(
|
|
@@ -324,28 +333,8 @@ def _do_step(action, llm_raw=None):
|
|
| 324 |
|
| 325 |
@app.post("/api/step", response_model=CombatState)
|
| 326 |
def step(req: StepRequest):
|
| 327 |
-
"""Execute one
|
| 328 |
-
return _do_step(req.
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
@app.post("/api/auto-step", response_model=CombatState)
|
| 332 |
-
def auto_step():
|
| 333 |
-
"""Execute one turn using the trained LLM to choose the action."""
|
| 334 |
-
global env
|
| 335 |
-
if env is None:
|
| 336 |
-
env = MahoragaEnv(difficulty=current_difficulty)
|
| 337 |
-
env.reset()
|
| 338 |
-
|
| 339 |
-
# Load model on first call
|
| 340 |
-
if not llm_loaded and not load_llm():
|
| 341 |
-
# Fallback to smart rule-based agent
|
| 342 |
-
action = _smart_agent_action()
|
| 343 |
-
return _do_step(action, llm_raw="[FALLBACK] rule-based")
|
| 344 |
-
|
| 345 |
-
# Get LLM's state observation
|
| 346 |
-
state_dict = env._get_state()
|
| 347 |
-
action, raw_output = llm_choose_action(state_dict)
|
| 348 |
-
return _do_step(action, llm_raw=raw_output)
|
| 349 |
|
| 350 |
|
| 351 |
@app.get("/api/model-status")
|
|
|
|
| 220 |
|
| 221 |
|
| 222 |
class StepRequest(BaseModel):
|
| 223 |
+
player_action: Optional[str] = None # None means auto (based on difficulty)
|
| 224 |
|
| 225 |
|
| 226 |
class ResetRequest(BaseModel):
|
|
|
|
| 278 |
)
|
| 279 |
|
| 280 |
|
| 281 |
+
def _do_step(player_action=None):
|
| 282 |
+
"""Execute one turn of combat. Mahoraga uses LLM to pick action, player uses player_action."""
|
| 283 |
global env
|
| 284 |
if env is None:
|
| 285 |
env = MahoragaEnv(difficulty=current_difficulty)
|
| 286 |
env.reset()
|
| 287 |
|
| 288 |
+
# Load model on first call
|
| 289 |
+
if not llm_loaded and not load_llm():
|
| 290 |
+
# Fallback to smart rule-based agent
|
| 291 |
+
mahoraga_action = _smart_agent_action()
|
| 292 |
+
llm_raw = "[FALLBACK] rule-based"
|
| 293 |
+
else:
|
| 294 |
+
state_dict = env._get_state()
|
| 295 |
+
mahoraga_action, llm_raw = llm_choose_action(state_dict)
|
| 296 |
+
|
| 297 |
+
state, reward, done, info = env.step(mahoraga_action, enemy_category_override=player_action)
|
| 298 |
action_name = ACTION_NAMES.get(env.last_action, "Unknown")
|
| 299 |
|
| 300 |
turn_log = TurnLog(
|
|
|
|
| 333 |
|
| 334 |
@app.post("/api/step", response_model=CombatState)
|
| 335 |
def step(req: StepRequest):
|
| 336 |
+
"""Execute one turn of combat."""
|
| 337 |
+
return _do_step(req.player_action)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 338 |
|
| 339 |
|
| 340 |
@app.get("/api/model-status")
|
env/mahoraga_env.py
CHANGED
|
@@ -52,7 +52,7 @@ class MahoragaEnv:
|
|
| 52 |
attack_history=list(self.attack_history)
|
| 53 |
)
|
| 54 |
|
| 55 |
-
def step(self, action):
|
| 56 |
validate_action(action)
|
| 57 |
self.turn_number += 1
|
| 58 |
|
|
@@ -67,13 +67,27 @@ class MahoragaEnv:
|
|
| 67 |
action = None # Nullify action — agent wastes turn
|
| 68 |
|
| 69 |
# 1. Enemy attacks first
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
enemy_damage = compute_enemy_damage(category, self.resistances, ignore_armor=ignore_armor)
|
| 79 |
self.agent_hp = max(0, self.agent_hp - enemy_damage)
|
|
|
|
| 52 |
attack_history=list(self.attack_history)
|
| 53 |
)
|
| 54 |
|
| 55 |
+
def step(self, action, enemy_category_override=None):
|
| 56 |
validate_action(action)
|
| 57 |
self.turn_number += 1
|
| 58 |
|
|
|
|
| 67 |
action = None # Nullify action — agent wastes turn
|
| 68 |
|
| 69 |
# 1. Enemy attacks first
|
| 70 |
+
if enemy_category_override:
|
| 71 |
+
import random
|
| 72 |
+
from utils.constants import SUBTYPES, BASE_DAMAGE
|
| 73 |
+
category = enemy_category_override.upper()
|
| 74 |
+
subtype = random.choice(SUBTYPES[category])
|
| 75 |
+
ignore_armor = (subtype == "PIERCE")
|
| 76 |
+
attack = {
|
| 77 |
+
"category": category,
|
| 78 |
+
"subtype": subtype,
|
| 79 |
+
"damage": BASE_DAMAGE[category],
|
| 80 |
+
"ignore_armor": ignore_armor
|
| 81 |
+
}
|
| 82 |
+
self.enemy.turn += 1 # Advance internal turn
|
| 83 |
+
else:
|
| 84 |
+
attack = self.enemy.get_attack(
|
| 85 |
+
turn_number=self.turn_number,
|
| 86 |
+
resistances=self.resistances
|
| 87 |
+
)
|
| 88 |
+
category = attack["category"]
|
| 89 |
+
subtype = attack["subtype"]
|
| 90 |
+
ignore_armor = attack["ignore_armor"]
|
| 91 |
|
| 92 |
enemy_damage = compute_enemy_damage(category, self.resistances, ignore_armor=ignore_armor)
|
| 93 |
self.agent_hp = max(0, self.agent_hp - enemy_damage)
|
frontend/src/App.jsx
CHANGED
|
@@ -193,7 +193,7 @@ export default function App() {
|
|
| 193 |
difficulty: "hard",
|
| 194 |
};
|
| 195 |
|
| 196 |
-
async function doReset(diff) {
|
| 197 |
const d2use = diff || difficulty;
|
| 198 |
setLoading(true);
|
| 199 |
setAutoPlay(false);
|
|
@@ -208,7 +208,7 @@ export default function App() {
|
|
| 208 |
} catch {
|
| 209 |
setState({ ...MOCK_STATE, difficulty: d2use });
|
| 210 |
}
|
| 211 |
-
setLogs([]); setLastLog(null);
|
| 212 |
setWheelRot(0); prevRes.current = { Physical: 0, CE: 0, Technique: 0 };
|
| 213 |
setLoading(false);
|
| 214 |
}
|
|
@@ -256,8 +256,9 @@ export default function App() {
|
|
| 256 |
const r = await fetch(`${API}/api/step`, {
|
| 257 |
method: "POST",
|
| 258 |
headers: { "Content-Type": "application/json" },
|
| 259 |
-
body: JSON.stringify({ action }),
|
| 260 |
});
|
|
|
|
| 261 |
const d = await r.json();
|
| 262 |
processStepResult(d);
|
| 263 |
} catch {
|
|
@@ -270,7 +271,12 @@ export default function App() {
|
|
| 270 |
if (autoPlay && state && !state.done && !loading) {
|
| 271 |
autoRef.current = setTimeout(async () => {
|
| 272 |
try {
|
| 273 |
-
const r = await fetch(`${API}/api/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
const d = await r.json();
|
| 275 |
processStepResult(d);
|
| 276 |
} catch { setAutoPlay(false); }
|
|
@@ -281,7 +287,12 @@ export default function App() {
|
|
| 281 |
|
| 282 |
/* ── Stop auto-play when game ends ── */
|
| 283 |
useEffect(() => {
|
| 284 |
-
if (state?.done)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 285 |
}, [state?.done]);
|
| 286 |
|
| 287 |
/* ── Check model status on mount ── */
|
|
@@ -323,7 +334,7 @@ export default function App() {
|
|
| 323 |
<div className="flex items-center gap-2">
|
| 324 |
{autoPlay && (
|
| 325 |
<span className="text-[8px] font-bold tracking-widest uppercase text-amber animate-pulse">
|
| 326 |
-
●
|
| 327 |
</span>
|
| 328 |
)}
|
| 329 |
<span className={`text-[8px] font-bold tracking-wider uppercase px-1.5 py-0.5 rounded ${
|
|
@@ -351,62 +362,62 @@ export default function App() {
|
|
| 351 |
{/* ═══════ MAIN BENTO GRID ═══════ */}
|
| 352 |
<div className="flex-1 grid grid-cols-12 gap-2 px-2 py-1.5 min-h-0 overflow-hidden">
|
| 353 |
|
| 354 |
-
{/* ── COL 1-5: Left Column (
|
| 355 |
<div className="col-span-5 flex flex-col gap-2 min-h-0">
|
| 356 |
|
| 357 |
-
{/*
|
| 358 |
<div className="glass-panel p-3 shrink-0">
|
| 359 |
<div className="flex items-center justify-between mb-2">
|
| 360 |
<div className="flex items-center gap-2">
|
| 361 |
-
<span className="material-symbols-outlined text-outline text-base">
|
| 362 |
<span className="text-[10px] font-bold tracking-[0.12em] uppercase text-red/70">
|
| 363 |
-
|
| 364 |
</span>
|
| 365 |
</div>
|
| 366 |
-
<span className="font-mono text-[9px] text-muted">ID: E-9942</span>
|
| 367 |
</div>
|
| 368 |
-
<HpBar current={state.
|
| 369 |
<div className="flex gap-1.5 mt-2">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 370 |
<StatChip
|
| 371 |
-
label="
|
| 372 |
-
value={state.
|
| 373 |
-
color={state.
|
| 374 |
-
/>
|
| 375 |
-
<StatChip
|
| 376 |
-
label="Phase"
|
| 377 |
-
value={state.turn_number <= 5 ? "I" : state.turn_number <= 15 ? "II" : "III"}
|
| 378 |
-
/>
|
| 379 |
-
<StatChip
|
| 380 |
-
label="Distance"
|
| 381 |
-
value="14.2m"
|
| 382 |
-
color="text-muted"
|
| 383 |
/>
|
|
|
|
| 384 |
</div>
|
| 385 |
</div>
|
| 386 |
|
| 387 |
-
{/*
|
| 388 |
<div className="glass-panel p-3 shrink-0">
|
| 389 |
<div className="flex items-center justify-between mb-2">
|
| 390 |
<div className="flex items-center gap-2">
|
| 391 |
-
<span className="material-symbols-outlined text-outline text-base">
|
| 392 |
<span className="text-[10px] font-bold tracking-[0.12em] uppercase text-green/70">
|
| 393 |
-
|
| 394 |
</span>
|
| 395 |
</div>
|
|
|
|
| 396 |
</div>
|
| 397 |
-
<HpBar current={state.
|
| 398 |
<div className="flex gap-1.5 mt-2">
|
| 399 |
-
<StatChip label="Stack" value={
|
| 400 |
-
<motion.span key={state.adaptation_stack} initial={{ scale: 1.5 }} animate={{ scale: 1 }}>
|
| 401 |
-
{state.adaptation_stack}
|
| 402 |
-
</motion.span>
|
| 403 |
-
} color="text-cyan" />
|
| 404 |
<StatChip
|
| 405 |
-
label="
|
| 406 |
-
value={state.
|
| 407 |
-
color={state.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 408 |
/>
|
| 409 |
-
<StatChip label="Adapt Rate" value="+2.4%/s" color="text-cyan" />
|
| 410 |
</div>
|
| 411 |
</div>
|
| 412 |
|
|
@@ -419,9 +430,9 @@ export default function App() {
|
|
| 419 |
</span>
|
| 420 |
</div>
|
| 421 |
<div className="space-y-1.5">
|
| 422 |
-
<ResBar label="Physical" icon="fitness_center" value={state.resistances.Physical} flashing={flashRes === "Physical"} />
|
| 423 |
-
<ResBar label="CE" icon="bolt" value={state.resistances.CE} flashing={flashRes === "CE"} />
|
| 424 |
-
<ResBar label="Technique" icon="precision_manufacturing" value={state.resistances.Technique} flashing={flashRes === "Technique"} />
|
| 425 |
</div>
|
| 426 |
</div>
|
| 427 |
</div>
|
|
@@ -429,10 +440,10 @@ export default function App() {
|
|
| 429 |
{/* ── COL 6-8: Center Column (Wheel + Phase + Tactics) ── */}
|
| 430 |
<div className="col-span-3 flex flex-col gap-2 min-h-0">
|
| 431 |
|
| 432 |
-
{/*
|
| 433 |
<div className="glass-panel p-3 shrink-0">
|
| 434 |
<div className="text-[8px] font-bold tracking-[0.2em] uppercase text-muted/50 mb-1.5">
|
| 435 |
-
|
| 436 |
</div>
|
| 437 |
<div className="flex gap-1">
|
| 438 |
{[
|
|
@@ -502,7 +513,7 @@ export default function App() {
|
|
| 502 |
|
| 503 |
{/* Tactical Summary */}
|
| 504 |
<div className="glass-panel p-2.5 shrink-0">
|
| 505 |
-
<div className="text-[7px] font-bold tracking-[0.15em] uppercase text-muted/40 mb-1">LAST
|
| 506 |
{lastLog ? (
|
| 507 |
<div className="flex items-center gap-2">
|
| 508 |
<span className={`text-[9px] font-bold px-1.5 py-0.5 rounded ${catColor(lastLog.enemy_attack_type).bg} ${catColor(lastLog.enemy_attack_type).text} border ${catColor(lastLog.enemy_attack_type).border}`}>
|
|
@@ -528,13 +539,13 @@ export default function App() {
|
|
| 528 |
>
|
| 529 |
<div className="flex items-center gap-2">
|
| 530 |
<span className="material-symbols-outlined text-cyan text-sm">published_with_changes</span>
|
| 531 |
-
<span className="text-[9px] font-bold tracking-[0.1em] uppercase text-cyan">ADAPTED</span>
|
| 532 |
</div>
|
| 533 |
<div className="text-xs font-black uppercase text-text mt-0.5">
|
| 534 |
-
{lastLog.enemy_attack_type} COUNTERED
|
| 535 |
</div>
|
| 536 |
<div className="text-[9px] text-muted mt-0.5">
|
| 537 |
-
|
| 538 |
</div>
|
| 539 |
</motion.div>
|
| 540 |
) : lastLog ? (
|
|
@@ -548,12 +559,12 @@ export default function App() {
|
|
| 548 |
<div className="flex items-center gap-2">
|
| 549 |
<span className="material-symbols-outlined text-muted text-sm">sync_problem</span>
|
| 550 |
<span className="text-[9px] font-bold tracking-[0.1em] uppercase text-muted">
|
| 551 |
-
{lastLog.mahoraga_action}
|
| 552 |
</span>
|
| 553 |
</div>
|
| 554 |
<div className="text-[10px] text-muted/60 mt-0.5 font-mono">
|
| 555 |
-
|
| 556 |
-
· <span className="text-
|
| 557 |
</div>
|
| 558 |
</motion.div>
|
| 559 |
) : (
|
|
@@ -596,37 +607,36 @@ export default function App() {
|
|
| 596 |
key={i}
|
| 597 |
initial={{ opacity: 0, x: -8 }}
|
| 598 |
animate={{ opacity: 1, x: 0 }}
|
| 599 |
-
className="
|
| 600 |
>
|
| 601 |
-
<
|
| 602 |
-
T{l.turn}
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
<
|
| 608 |
-
{l.
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
<
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
</
|
|
|
|
|
|
|
| 626 |
</div>
|
| 627 |
-
<span className={`font-mono text-[9px] font-bold shrink-0 ${l.reward > 0 ? "text-green" : "text-red/60"}`}>
|
| 628 |
-
{l.reward > 0 ? "+" : ""}{l.reward}
|
| 629 |
-
</span>
|
| 630 |
</motion.div>
|
| 631 |
))
|
| 632 |
)}
|
|
@@ -657,13 +667,10 @@ export default function App() {
|
|
| 657 |
|
| 658 |
<div className="w-px h-5 bg-outline-variant/20 mx-0.5" />
|
| 659 |
|
| 660 |
-
{/* Manual
|
| 661 |
-
<Btn label="
|
| 662 |
-
<Btn label="
|
| 663 |
-
<Btn label="
|
| 664 |
-
<div className="w-px h-5 bg-outline-variant/20 mx-0.5" />
|
| 665 |
-
<Btn label="Judgment Strike" onClick={() => doStep(3)} variant="danger" disabled={done || autoPlay} />
|
| 666 |
-
<Btn label="Regeneration" onClick={() => doStep(4)} variant="primary" disabled={done || autoPlay} />
|
| 667 |
<div className="w-px h-5 bg-outline-variant/20 mx-0.5" />
|
| 668 |
|
| 669 |
{/* Auto-play + Reset */}
|
|
@@ -677,7 +684,7 @@ export default function App() {
|
|
| 677 |
: "bg-surface/60 text-muted border-outline-variant/30 hover:text-cyan hover:border-cyan/30"
|
| 678 |
}`}
|
| 679 |
>
|
| 680 |
-
{autoPlay ? "⏸ STOP" : "▶
|
| 681 |
</motion.button>
|
| 682 |
<Btn label="Reset" onClick={() => doReset()} variant="reset" />
|
| 683 |
|
|
@@ -714,7 +721,7 @@ export default function App() {
|
|
| 714 |
</div>
|
| 715 |
<div className="text-sm text-muted mb-1">{state.done_reason}</div>
|
| 716 |
<div className="font-mono text-[10px] text-muted mb-5">
|
| 717 |
-
|
| 718 |
</div>
|
| 719 |
<Btn label="Deploy Again" onClick={doReset} variant="primary" />
|
| 720 |
</motion.div>
|
|
|
|
| 193 |
difficulty: "hard",
|
| 194 |
};
|
| 195 |
|
| 196 |
+
async function doReset(diff, clearLogs = true) {
|
| 197 |
const d2use = diff || difficulty;
|
| 198 |
setLoading(true);
|
| 199 |
setAutoPlay(false);
|
|
|
|
| 208 |
} catch {
|
| 209 |
setState({ ...MOCK_STATE, difficulty: d2use });
|
| 210 |
}
|
| 211 |
+
if (clearLogs) { setLogs([]); setLastLog(null); }
|
| 212 |
setWheelRot(0); prevRes.current = { Physical: 0, CE: 0, Technique: 0 };
|
| 213 |
setLoading(false);
|
| 214 |
}
|
|
|
|
| 256 |
const r = await fetch(`${API}/api/step`, {
|
| 257 |
method: "POST",
|
| 258 |
headers: { "Content-Type": "application/json" },
|
| 259 |
+
body: JSON.stringify({ player_action: action }),
|
| 260 |
});
|
| 261 |
+
if (!r.ok) { setLoading(false); return; }
|
| 262 |
const d = await r.json();
|
| 263 |
processStepResult(d);
|
| 264 |
} catch {
|
|
|
|
| 271 |
if (autoPlay && state && !state.done && !loading) {
|
| 272 |
autoRef.current = setTimeout(async () => {
|
| 273 |
try {
|
| 274 |
+
const r = await fetch(`${API}/api/step`, {
|
| 275 |
+
method: "POST",
|
| 276 |
+
headers: { "Content-Type": "application/json" },
|
| 277 |
+
body: JSON.stringify({ player_action: null })
|
| 278 |
+
});
|
| 279 |
+
if (!r.ok) { setAutoPlay(false); return; }
|
| 280 |
const d = await r.json();
|
| 281 |
processStepResult(d);
|
| 282 |
} catch { setAutoPlay(false); }
|
|
|
|
| 287 |
|
| 288 |
/* ── Stop auto-play when game ends ── */
|
| 289 |
useEffect(() => {
|
| 290 |
+
if (state?.done) {
|
| 291 |
+
setAutoPlay(false);
|
| 292 |
+
// Auto-reset stats after 3 seconds, but keep combat logs
|
| 293 |
+
const t = setTimeout(() => doReset(null, false), 3000);
|
| 294 |
+
return () => clearTimeout(t);
|
| 295 |
+
}
|
| 296 |
}, [state?.done]);
|
| 297 |
|
| 298 |
/* ── Check model status on mount ── */
|
|
|
|
| 334 |
<div className="flex items-center gap-2">
|
| 335 |
{autoPlay && (
|
| 336 |
<span className="text-[8px] font-bold tracking-widest uppercase text-amber animate-pulse">
|
| 337 |
+
● AUTO-PLAY
|
| 338 |
</span>
|
| 339 |
)}
|
| 340 |
<span className={`text-[8px] font-bold tracking-wider uppercase px-1.5 py-0.5 rounded ${
|
|
|
|
| 362 |
{/* ═══════ MAIN BENTO GRID ═══════ */}
|
| 363 |
<div className="flex-1 grid grid-cols-12 gap-2 px-2 py-1.5 min-h-0 overflow-hidden">
|
| 364 |
|
| 365 |
+
{/* ── COL 1-5: Left Column (Boss + Player stacked) ── */}
|
| 366 |
<div className="col-span-5 flex flex-col gap-2 min-h-0">
|
| 367 |
|
| 368 |
+
{/* Boss: Mahoraga (LLM-controlled enemy) */}
|
| 369 |
<div className="glass-panel p-3 shrink-0">
|
| 370 |
<div className="flex items-center justify-between mb-2">
|
| 371 |
<div className="flex items-center gap-2">
|
| 372 |
+
<span className="material-symbols-outlined text-outline text-base">smart_toy</span>
|
| 373 |
<span className="text-[10px] font-bold tracking-[0.12em] uppercase text-red/70">
|
| 374 |
+
⚙ BOSS — MAHORAGA (AUTO)
|
| 375 |
</span>
|
| 376 |
</div>
|
|
|
|
| 377 |
</div>
|
| 378 |
+
<HpBar current={state.mahoraga_hp} max={state.mahoraga_hp_max} color="red" label="Boss Integrity" />
|
| 379 |
<div className="flex gap-1.5 mt-2">
|
| 380 |
+
<StatChip label="Stack" value={
|
| 381 |
+
<motion.span key={state.adaptation_stack} initial={{ scale: 1.5 }} animate={{ scale: 1 }}>
|
| 382 |
+
{state.adaptation_stack}
|
| 383 |
+
</motion.span>
|
| 384 |
+
} color="text-cyan" />
|
| 385 |
<StatChip
|
| 386 |
+
label="Heal CD"
|
| 387 |
+
value={state.heal_cooldown === 0 ? "RDY" : state.heal_cooldown}
|
| 388 |
+
color={state.heal_cooldown === 0 ? "text-green" : "text-red"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 389 |
/>
|
| 390 |
+
<StatChip label="Adapt Rate" value="+2.4%/s" color="text-cyan" />
|
| 391 |
</div>
|
| 392 |
</div>
|
| 393 |
|
| 394 |
+
{/* Player Status (user-controlled) */}
|
| 395 |
<div className="glass-panel p-3 shrink-0">
|
| 396 |
<div className="flex items-center justify-between mb-2">
|
| 397 |
<div className="flex items-center gap-2">
|
| 398 |
+
<span className="material-symbols-outlined text-outline text-base">person</span>
|
| 399 |
<span className="text-[10px] font-bold tracking-[0.12em] uppercase text-green/70">
|
| 400 |
+
⚔ YOU — PLAYER
|
| 401 |
</span>
|
| 402 |
</div>
|
| 403 |
+
<span className="font-mono text-[9px] text-muted">MANUAL CONTROL</span>
|
| 404 |
</div>
|
| 405 |
+
<HpBar current={state.enemy_hp} max={state.enemy_hp_max} color="green" label="Your Integrity" />
|
| 406 |
<div className="flex gap-1.5 mt-2">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 407 |
<StatChip
|
| 408 |
+
label="Status"
|
| 409 |
+
value={state.enemy_hp < 400 ? "CRIT" : state.enemy_hp < 700 ? "WARN" : "OK"}
|
| 410 |
+
color={state.enemy_hp < 400 ? "text-red" : state.enemy_hp < 700 ? "text-amber" : "text-green"}
|
| 411 |
+
/>
|
| 412 |
+
<StatChip
|
| 413 |
+
label="Phase"
|
| 414 |
+
value={state.turn_number <= 5 ? "I" : state.turn_number <= 15 ? "II" : "III"}
|
| 415 |
+
/>
|
| 416 |
+
<StatChip
|
| 417 |
+
label="Difficulty"
|
| 418 |
+
value={difficulty.toUpperCase()}
|
| 419 |
+
color={difficulty === "easy" ? "text-green" : difficulty === "medium" ? "text-amber" : "text-red"}
|
| 420 |
/>
|
|
|
|
| 421 |
</div>
|
| 422 |
</div>
|
| 423 |
|
|
|
|
| 430 |
</span>
|
| 431 |
</div>
|
| 432 |
<div className="space-y-1.5">
|
| 433 |
+
<ResBar label="Physical" icon="fitness_center" value={state.resistances?.Physical ?? 0} flashing={flashRes === "Physical"} />
|
| 434 |
+
<ResBar label="CE" icon="bolt" value={state.resistances?.CE ?? 0} flashing={flashRes === "CE"} />
|
| 435 |
+
<ResBar label="Technique" icon="precision_manufacturing" value={state.resistances?.Technique ?? 0} flashing={flashRes === "Technique"} />
|
| 436 |
</div>
|
| 437 |
</div>
|
| 438 |
</div>
|
|
|
|
| 440 |
{/* ── COL 6-8: Center Column (Wheel + Phase + Tactics) ── */}
|
| 441 |
<div className="col-span-3 flex flex-col gap-2 min-h-0">
|
| 442 |
|
| 443 |
+
{/* Mahoraga Adaptation Phase */}
|
| 444 |
<div className="glass-panel p-3 shrink-0">
|
| 445 |
<div className="text-[8px] font-bold tracking-[0.2em] uppercase text-muted/50 mb-1.5">
|
| 446 |
+
MAHORAGA PHASE
|
| 447 |
</div>
|
| 448 |
<div className="flex gap-1">
|
| 449 |
{[
|
|
|
|
| 513 |
|
| 514 |
{/* Tactical Summary */}
|
| 515 |
<div className="glass-panel p-2.5 shrink-0">
|
| 516 |
+
<div className="text-[7px] font-bold tracking-[0.15em] uppercase text-muted/40 mb-1">YOUR LAST ATTACK</div>
|
| 517 |
{lastLog ? (
|
| 518 |
<div className="flex items-center gap-2">
|
| 519 |
<span className={`text-[9px] font-bold px-1.5 py-0.5 rounded ${catColor(lastLog.enemy_attack_type).bg} ${catColor(lastLog.enemy_attack_type).text} border ${catColor(lastLog.enemy_attack_type).border}`}>
|
|
|
|
| 539 |
>
|
| 540 |
<div className="flex items-center gap-2">
|
| 541 |
<span className="material-symbols-outlined text-cyan text-sm">published_with_changes</span>
|
| 542 |
+
<span className="text-[9px] font-bold tracking-[0.1em] uppercase text-cyan">MAHORAGA ADAPTED</span>
|
| 543 |
</div>
|
| 544 |
<div className="text-xs font-black uppercase text-text mt-0.5">
|
| 545 |
+
YOUR {lastLog.enemy_attack_type} WAS COUNTERED
|
| 546 |
</div>
|
| 547 |
<div className="text-[9px] text-muted mt-0.5">
|
| 548 |
+
Boss adapted to your attack type.
|
| 549 |
</div>
|
| 550 |
</motion.div>
|
| 551 |
) : lastLog ? (
|
|
|
|
| 559 |
<div className="flex items-center gap-2">
|
| 560 |
<span className="material-symbols-outlined text-muted text-sm">sync_problem</span>
|
| 561 |
<span className="text-[9px] font-bold tracking-[0.1em] uppercase text-muted">
|
| 562 |
+
MAHORAGA: {lastLog.mahoraga_action}
|
| 563 |
</span>
|
| 564 |
</div>
|
| 565 |
<div className="text-[10px] text-muted/60 mt-0.5 font-mono">
|
| 566 |
+
You dealt: <span className="text-green">{lastLog.damage_taken}</span>
|
| 567 |
+
· Boss dealt: <span className="text-red">{lastLog.damage_dealt}</span>
|
| 568 |
</div>
|
| 569 |
</motion.div>
|
| 570 |
) : (
|
|
|
|
| 607 |
key={i}
|
| 608 |
initial={{ opacity: 0, x: -8 }}
|
| 609 |
animate={{ opacity: 1, x: 0 }}
|
| 610 |
+
className="py-1.5 border-b border-outline-variant/15 last:border-0"
|
| 611 |
>
|
| 612 |
+
<div className="flex items-center gap-1.5 mb-1">
|
| 613 |
+
<span className="font-mono text-[9px] text-outline-variant shrink-0 w-6">T{l.turn}</span>
|
| 614 |
+
<div className="shrink-0" style={{ width: 4, height: 4, borderRadius: "50%", backgroundColor: catColor(l.enemy_attack_type).hex }} />
|
| 615 |
+
{l.correct_adaptation && (
|
| 616 |
+
<span className="material-symbols-outlined text-cyan text-[11px]">published_with_changes</span>
|
| 617 |
+
)}
|
| 618 |
+
<span className={`font-mono text-[9px] font-bold shrink-0 ml-auto ${l.reward > 0 ? "text-green" : "text-red/60"}`}>
|
| 619 |
+
{l.reward > 0 ? "+" : ""}{l.reward}
|
| 620 |
+
</span>
|
| 621 |
+
</div>
|
| 622 |
+
{/* Player action line */}
|
| 623 |
+
<div className="flex items-center gap-1.5 ml-7 mb-0.5">
|
| 624 |
+
<span className="text-[8px] font-bold tracking-wider uppercase text-green/80 w-8">YOU</span>
|
| 625 |
+
<span className="text-[9px] text-muted/40">→</span>
|
| 626 |
+
<span className={`text-[9px] font-bold px-1 py-0.5 rounded ${catColor(l.enemy_attack_type).bg} ${catColor(l.enemy_attack_type).text} border ${catColor(l.enemy_attack_type).border}`}>
|
| 627 |
+
{l.enemy_attack_type}
|
| 628 |
+
</span>
|
| 629 |
+
<span className="font-mono text-[8px] text-muted/50">{l.enemy_subtype}</span>
|
| 630 |
+
<span className="font-mono text-[9px] text-green ml-auto">-{l.damage_taken} to boss</span>
|
| 631 |
+
</div>
|
| 632 |
+
{/* Mahoraga response line */}
|
| 633 |
+
<div className="flex items-center gap-1.5 ml-7">
|
| 634 |
+
<span className="text-[8px] font-bold tracking-wider uppercase text-red/80 w-8">BOSS</span>
|
| 635 |
+
<span className="text-[9px] text-muted/40">→</span>
|
| 636 |
+
<span className="text-[9px] font-bold text-text/80">{l.mahoraga_action}</span>
|
| 637 |
+
{l.correct_adaptation && <span className="text-[8px] text-cyan font-bold">ADAPTED!</span>}
|
| 638 |
+
{l.damage_dealt > 0 && <span className="font-mono text-[9px] text-red ml-auto">-{l.damage_dealt} to you</span>}
|
| 639 |
</div>
|
|
|
|
|
|
|
|
|
|
| 640 |
</motion.div>
|
| 641 |
))
|
| 642 |
)}
|
|
|
|
| 667 |
|
| 668 |
<div className="w-px h-5 bg-outline-variant/20 mx-0.5" />
|
| 669 |
|
| 670 |
+
{/* Manual player attacks */}
|
| 671 |
+
<Btn label="⚔ Physical" onClick={() => doStep("PHYSICAL")} disabled={done || autoPlay} />
|
| 672 |
+
<Btn label="⚔ CE" onClick={() => doStep("CE")} disabled={done || autoPlay} />
|
| 673 |
+
<Btn label="⚔ Technique" onClick={() => doStep("TECHNIQUE")} disabled={done || autoPlay} />
|
|
|
|
|
|
|
|
|
|
| 674 |
<div className="w-px h-5 bg-outline-variant/20 mx-0.5" />
|
| 675 |
|
| 676 |
{/* Auto-play + Reset */}
|
|
|
|
| 684 |
: "bg-surface/60 text-muted border-outline-variant/30 hover:text-cyan hover:border-cyan/30"
|
| 685 |
}`}
|
| 686 |
>
|
| 687 |
+
{autoPlay ? "⏸ STOP AUTO" : "▶ AUTO-PLAY"}
|
| 688 |
</motion.button>
|
| 689 |
<Btn label="Reset" onClick={() => doReset()} variant="reset" />
|
| 690 |
|
|
|
|
| 721 |
</div>
|
| 722 |
<div className="text-sm text-muted mb-1">{state.done_reason}</div>
|
| 723 |
<div className="font-mono text-[10px] text-muted mb-5">
|
| 724 |
+
You: {state.enemy_hp} HP | Mahoraga (Boss): {state.mahoraga_hp} HP | T{state.turn_number}
|
| 725 |
</div>
|
| 726 |
<Btn label="Deploy Again" onClick={doReset} variant="primary" />
|
| 727 |
</motion.div>
|