MridulNegi2005 commited on
Commit
dbcb71c
·
1 Parent(s): 4b89f2f

fix: swap combat roles - Mahoraga as AUTO boss, player as user-controlled, clear combat log labels

Browse files
Files changed (3) hide show
  1. api.py +15 -26
  2. env/mahoraga_env.py +22 -8
  3. frontend/src/App.jsx +94 -87
api.py CHANGED
@@ -220,7 +220,7 @@ class CombatState(BaseModel):
220
 
221
 
222
  class StepRequest(BaseModel):
223
- action: int # 0-4
224
 
225
 
226
  class ResetRequest(BaseModel):
@@ -278,14 +278,23 @@ def reset(req: ResetRequest = ResetRequest()):
278
  )
279
 
280
 
281
- def _do_step(action, llm_raw=None):
282
- """Execute one turn of combat (shared by manual step and auto-step)."""
283
  global env
284
  if env is None:
285
  env = MahoragaEnv(difficulty=current_difficulty)
286
  env.reset()
287
 
288
- state, reward, done, info = env.step(action)
 
 
 
 
 
 
 
 
 
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 manual turn of combat."""
328
- return _do_step(req.action)
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
- attack = self.enemy.get_attack(
71
- turn_number=self.turn_number,
72
- resistances=self.resistances
73
- )
74
- category = attack["category"]
75
- subtype = attack["subtype"]
76
- ignore_armor = attack["ignore_armor"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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/auto-step`, { method: "POST" });
 
 
 
 
 
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) setAutoPlay(false);
 
 
 
 
 
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
- LLM AUTO
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 (Enemy + Mahoraga stacked) ── */}
355
  <div className="col-span-5 flex flex-col gap-2 min-h-0">
356
 
357
- {/* Target Status: Enemy */}
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">target</span>
362
  <span className="text-[10px] font-bold tracking-[0.12em] uppercase text-red/70">
363
- TARGET STATUS
364
  </span>
365
  </div>
366
- <span className="font-mono text-[9px] text-muted">ID: E-9942</span>
367
  </div>
368
- <HpBar current={state.enemy_hp} max={state.enemy_hp_max} color="red" label="Structural Integrity" />
369
  <div className="flex gap-1.5 mt-2">
 
 
 
 
 
370
  <StatChip
371
- label="Threat"
372
- value={state.enemy_hp < 400 ? "CRIT" : state.enemy_hp < 700 ? "HIGH" : "NOM"}
373
- color={state.enemy_hp < 400 ? "text-red" : state.enemy_hp < 700 ? "text-amber" : "text-cyan"}
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
- {/* Core: Mahoraga */}
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">memory</span>
392
  <span className="text-[10px] font-bold tracking-[0.12em] uppercase text-green/70">
393
- CORE: MAHORAGA
394
  </span>
395
  </div>
 
396
  </div>
397
- <HpBar current={state.mahoraga_hp} max={state.mahoraga_hp_max} color="green" label="System Integrity" />
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="Heal CD"
406
- value={state.heal_cooldown === 0 ? "RDY" : state.heal_cooldown}
407
- color={state.heal_cooldown === 0 ? "text-green" : "text-red"}
 
 
 
 
 
 
 
 
 
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
- {/* Enemy Phase Indicator */}
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
- ENEMY PHASE
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 INCOMING</div>
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
- Defensive parameters updated.
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
- DMG: <span className="text-red">{lastLog.damage_taken}</span> taken
556
- · <span className="text-green">{lastLog.damage_dealt}</span> dealt
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="flex gap-2 items-start py-1.5 border-b border-outline-variant/15 last:border-0"
600
  >
601
- <span className="font-mono text-[9px] text-outline-variant shrink-0 mt-0.5 w-6">
602
- T{l.turn}
603
- </span>
604
- {/* Category color dot */}
605
- <div className="shrink-0 mt-1.5" style={{ width: 4, height: 4, borderRadius: "50%", backgroundColor: catColor(l.enemy_attack_type).hex }} />
606
- <div className="flex-1 min-w-0">
607
- <div className="flex items-center gap-1">
608
- {l.correct_adaptation && (
609
- <span className="material-symbols-outlined text-cyan text-[11px]">
610
- published_with_changes
611
- </span>
612
- )}
613
- <span className={`text-[9px] font-bold tracking-wider uppercase ${l.correct_adaptation ? "text-cyan" : "text-muted/60"}`}>
614
- {l.correct_adaptation ? "ADAPTATION" : l.mahoraga_action}
615
- </span>
616
- </div>
617
- <div className="font-mono text-[9px] text-muted/70 mt-0.5">
618
- <span className={catColor(l.enemy_attack_type).text}>{l.enemy_subtype}</span>
619
- <span className="text-muted/30"> </span>
620
- <span className="text-text/80">{l.mahoraga_action}</span>
621
- <span className="text-muted/30"> | </span>
622
- <span className="text-amber">{l.damage_dealt}d</span>
623
- <span className="text-muted/30"> · </span>
624
- <span className="text-red/70">{l.damage_taken}t</span>
625
- </div>
 
 
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 actions */}
661
- <Btn label="Adapt Physical" onClick={() => doStep(0)} disabled={done || autoPlay} />
662
- <Btn label="Adapt CE" onClick={() => doStep(1)} disabled={done || autoPlay} />
663
- <Btn label="Adapt Technique" onClick={() => doStep(2)} disabled={done || autoPlay} />
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" : "▶ LLM AUTO"}
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
- Enemy: {state.enemy_hp} HP | Mahoraga: {state.mahoraga_hp} HP | T{state.turn_number}
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>