game/lib/screens/battle_screen.dart

406 lines
13 KiB
Dart

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<BattleScreen> createState() => _BattleScreenState();
}
class _BattleScreenState extends State<BattleScreen> 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<BattleProvider>());
}
@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<BattleProvider>();
final player = battleProvider.player;
showDialog(
context: context,
builder: (BuildContext context) {
return RiskSelectionDialog(
actionType: actionType,
player: player,
onSelected: (risk) {
context.read<BattleProvider>().playerAction(actionType, risk);
Navigator.pop(context);
},
);
},
);
}
/// Triggers enemy defense animation if applicable. Returns true if triggered.
bool _triggerEnemyDefenseIfNeeded(BuildContext context) {
final battleProvider = context.read<BattleProvider>();
// 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<SettingsProvider>().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<BattleProvider>();
final List<Item> 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<BattleProvider>(
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(),
],
);
}
}