import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../providers.dart'; import '../game/enums.dart'; import '../game/models.dart'; import '../widgets.dart'; import '../utils.dart'; import '../game/config.dart'; import 'battle_visual_handler.dart'; class BattleScreen extends StatefulWidget { const BattleScreen({super.key}); @override State createState() => _BattleScreenState(); } class _BattleScreenState extends State with BattleVisualHandler { bool _showLogs = false; bool _showEquipmentSwapPanel = false; // New State for Interactive Defense Animation int _lastTurnCount = -1; bool _hasShownEnemyDefense = false; String? _getOverrideImage(bool isPlayer) { if (!isPlayer) { return null; // Enemy animation image logic can be added later } if (playerAnimPhase == AnimationPhase.block) { return "assets/images/character/Knight-Block.png"; } if (playerAnimPhase == AnimationPhase.hurt) { return "assets/images/character/Knight-Hurt.png"; } if (playerAnimPhase == AnimationPhase.start || playerAnimPhase == AnimationPhase.middle || playerAnimPhase == AnimationPhase.end) { if (!isAttackSuccess) return null; // Idle for failed attacks if (activeRiskLevel == RiskLevel.safe) { return "assets/images/character/Knight-Attack01.png"; } else if (activeRiskLevel == RiskLevel.normal) { return "assets/images/character/Knight-Attack02.png"; } else if (activeRiskLevel == RiskLevel.risky) { return "assets/images/character/Knight-Attack03.png"; } } return null; } @override void initState() { super.initState(); setupVisualListeners(context.read()); } @override void dispose() { disposeVisualListeners(); super.dispose(); } void _showRiskLevelSelection(BuildContext context, ActionType actionType) { if (_showEquipmentSwapPanel) { setState(() => _showEquipmentSwapPanel = false); } // 1. Check if we need to trigger enemy animation first bool triggered = _triggerEnemyDefenseIfNeeded(context); if (triggered) { return; // If triggered, we wait for animation (and input block) } final battleProvider = context.read(); final player = battleProvider.player; showDialog( context: context, builder: (BuildContext context) { return RiskSelectionDialog( actionType: actionType, player: player, onSelected: (risk) { context.read().playerAction(actionType, risk); Navigator.pop(context); }, ); }, ); } /// Triggers enemy defense animation if applicable. Returns true if triggered. bool _triggerEnemyDefenseIfNeeded(BuildContext context) { final battleProvider = context.read(); // Check turn to reset flag if (battleProvider.turnCount != _lastTurnCount) { _lastTurnCount = battleProvider.turnCount; _hasShownEnemyDefense = false; } final enemyIntent = battleProvider.currentEnemyIntent; if (enemyIntent != null && enemyIntent.type == EnemyActionType.defend && !_hasShownEnemyDefense && context.read().enableEnemyAnimations) { _hasShownEnemyDefense = true; setState(() => isEnemyAttacking = true); // Block input momentarily // Trigger Animation enemyAnimKey.currentState ?.animateDefense(() { // [New] Apply Logic Synced with Animation battleProvider.applyPendingEnemyDefense(); // Create a local visual-only event to trigger the effect (Icon or FAILED text) final bool isSuccess = enemyIntent.isSuccess; final BattleFeedbackType? feedbackType = isSuccess ? null : BattleFeedbackType.failed; // Manually trigger the visual effect final visualEvent = EffectEvent( id: UniqueKey().toString(), // Local unique ID type: ActionType.defend, risk: enemyIntent.risk, target: EffectTarget.enemy, // Show on enemy feedbackType: feedbackType, attacker: battleProvider.enemy, targetEntity: battleProvider.enemy, isSuccess: isSuccess, isVisualOnly: true, // Visual only triggersTurnChange: false, ); onEffectEvent(visualEvent); }) .then((_) { if (mounted) setState(() => isEnemyAttacking = false); }); return true; } return false; } void _showInventoryDialog(BuildContext context) { if (_showEquipmentSwapPanel) { setState(() => _showEquipmentSwapPanel = false); } final battleProvider = context.read(); final List consumables = battleProvider.player.inventory .where((item) => item.slot == EquipmentSlot.consumable) .toList(); if (consumables.isEmpty) { ToastUtils.showTopToast(context, "No consumable items!"); return; } showDialog( context: context, builder: (context) { return SimpleDialog( title: const Text("Use Item"), children: consumables.map((item) { return SimpleDialogOption( onPressed: () { battleProvider.useConsumable(item); Navigator.pop(context); }, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: ThemeConfig.rewardItemBg, borderRadius: BorderRadius.circular(4), border: Border.all( color: ItemUtils.getRarityColor(item.rarity), ), ), child: Image.asset( ItemUtils.getIconPath(item.slot), width: ThemeConfig.itemIconSizeMedium, height: ThemeConfig.itemIconSizeMedium, fit: BoxFit.contain, ), ), const SizedBox(width: 12), Text( item.name, style: TextStyle( fontWeight: ThemeConfig.fontWeightBold, fontSize: ThemeConfig.fontSizeLarge, color: ItemUtils.getRarityColor(item.rarity), ), ), ], ), ItemStatWidget(item: item), ], ), ); }).toList(), ); }, ); } void _toggleEquipmentSwapPanel() { setState(() { _showEquipmentSwapPanel = !_showEquipmentSwapPanel; }); } Widget _buildEquipmentSwapPanel() { return Material( color: Colors.transparent, child: Container( height: 236, decoration: BoxDecoration( color: ThemeConfig.battleBg.withValues(alpha: 0.92), border: Border.all(color: ThemeConfig.textColorGrey), borderRadius: BorderRadius.circular(8), boxShadow: const [ BoxShadow( color: Colors.black54, blurRadius: 10, offset: Offset(0, 4), ), ], ), child: Column( children: [ SizedBox( height: 40, child: Row( children: [ const SizedBox(width: 10), const Icon( Icons.swap_horiz, color: ThemeConfig.mainIconColor, size: 20, ), const SizedBox(width: 6), const Expanded( child: Text( "Equipment", maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: ThemeConfig.textColorWhite, fontSize: ThemeConfig.fontSizeBody, fontWeight: ThemeConfig.fontWeightBold, ), ), ), IconButton( visualDensity: VisualDensity.compact, onPressed: () { setState(() => _showEquipmentSwapPanel = false); }, icon: const Icon( Icons.close, color: ThemeConfig.textColorWhite, size: 18, ), ), ], ), ), const Divider(height: 1, color: ThemeConfig.textColorGrey), const Expanded( child: InventoryGridWidget( mode: InventoryGridMode.equipmentSwap, equipmentOnly: true, showHeader: false, gridPadding: EdgeInsets.all(8.0), childAspectRatio: 1.05, ), ), ], ), ), ); } bool _canUseEquipmentSwap(BattleProvider battleProvider) { return battleProvider.isPlayerTurn && !battleProvider.player.isDead && !battleProvider.enemy.isDead && !battleProvider.showRewardPopup && !isPlayerAttacking && !isEnemyAttacking; } Widget _buildEquipmentSwapButton(BattleProvider battleProvider) { final canUse = _canUseEquipmentSwap(battleProvider); return FloatingActionButton( heroTag: "equipmentSwap", mini: true, backgroundColor: canUse ? ThemeConfig.toggleBtnBg : ThemeConfig.btnDisabled, onPressed: canUse ? _toggleEquipmentSwapPanel : null, child: Icon( _showEquipmentSwapPanel && canUse ? Icons.close : Icons.swap_horiz, color: ThemeConfig.textColorWhite, ), ); } @override Widget build(BuildContext context) { return ResponsiveContainer( child: Stack( key: rootStackKey, children: [ Consumer( builder: (context, battleProvider, child) { if (battleProvider.currentStage.type == StageType.shop) { return ShopUI(battleProvider: battleProvider); } else if (battleProvider.currentStage.type == StageType.rest) { return RestUI(battleProvider: battleProvider); } return _buildBattleUI(battleProvider); }, ), EffectSpriteWidget(key: effectSpriteKey), ], ), ); } Widget _buildBattleUI(BattleProvider battleProvider) { return Stack( children: [ Column( children: [ const BattleHeader(), Expanded( child: BattleArena( battleProvider: battleProvider, playerKey: playerKey, enemyKey: enemyKey, playerAnimKey: playerAnimKey, enemyAnimKey: enemyAnimKey, shakeKey: shakeKey, stackKey: stackKey, isPlayerAttacking: isPlayerAttacking, isEnemyAttacking: isEnemyAttacking, playerOverrideImage: _getOverrideImage(true), ), ), ], ), BattleBottomSection( battleProvider: battleProvider, showLogs: _showLogs, isPlayerAttacking: isPlayerAttacking, isEnemyAttacking: isEnemyAttacking, onToggleLogs: () => setState(() => _showLogs = !_showLogs), onAttackPressed: () => _showRiskLevelSelection(context, ActionType.attack), onDefendPressed: () => _showRiskLevelSelection(context, ActionType.defend), onItemPressed: () => _showInventoryDialog(context), equipmentSwapButton: _buildEquipmentSwapButton(battleProvider), equipmentSwapPanel: _showEquipmentSwapPanel && _canUseEquipmentSwap(battleProvider) ? _buildEquipmentSwapPanel() : null, ), // Reward Popup if (battleProvider.showRewardPopup) BattleRewardOverlay(battleProvider: battleProvider), // Floating Effects ...floatingDamageTexts.map((e) => e.widget), ...floatingEffects.map((e) => e.widget), ...floatingFeedbackTexts.map((e) => e.widget), // Explosion Layer ExplosionWidget(key: explosionKey), // Game Over Overlay if (battleProvider.player.isDead) const BattleDefeatOverlay(), ], ); } }