764 lines
28 KiB
Dart
764 lines
28 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import 'package:provider/provider.dart';
|
|
import '../providers/battle_provider.dart';
|
|
|
|
import '../game/enums.dart';
|
|
import '../game/model/item.dart';
|
|
import '../game/model/damage_event.dart';
|
|
import '../game/model/effect_event.dart';
|
|
import 'dart:async';
|
|
import '../widgets/responsive_container.dart';
|
|
import '../utils/item_utils.dart';
|
|
import '../widgets/battle/character_status_card.dart';
|
|
import '../widgets/battle/battle_log_overlay.dart';
|
|
import '../widgets/battle/floating_battle_texts.dart';
|
|
import '../widgets/stage/shop_ui.dart';
|
|
import '../widgets/stage/rest_ui.dart';
|
|
import '../widgets/battle/shake_widget.dart';
|
|
import '../widgets/battle/battle_animation_widget.dart';
|
|
import '../widgets/battle/explosion_widget.dart';
|
|
import 'main_menu_screen.dart';
|
|
import '../game/config/battle_config.dart';
|
|
import '../game/config/theme_config.dart';
|
|
|
|
class BattleScreen extends StatefulWidget {
|
|
const BattleScreen({super.key});
|
|
|
|
@override
|
|
State<BattleScreen> createState() => _BattleScreenState();
|
|
}
|
|
|
|
class _BattleScreenState extends State<BattleScreen> {
|
|
final List<DamageTextData> _floatingDamageTexts = [];
|
|
final List<FloatingEffectData> _floatingEffects = [];
|
|
final List<FeedbackTextData> _floatingFeedbackTexts = [];
|
|
StreamSubscription<DamageEvent>? _damageSubscription;
|
|
StreamSubscription<EffectEvent>? _effectSubscription;
|
|
final GlobalKey _playerKey = GlobalKey();
|
|
final GlobalKey _enemyKey = GlobalKey();
|
|
final GlobalKey _stackKey = GlobalKey();
|
|
final GlobalKey<ShakeWidgetState> _shakeKey = GlobalKey<ShakeWidgetState>();
|
|
final GlobalKey<BattleAnimationWidgetState> _playerAnimKey =
|
|
GlobalKey<BattleAnimationWidgetState>();
|
|
final GlobalKey<ExplosionWidgetState> _explosionKey =
|
|
GlobalKey<ExplosionWidgetState>();
|
|
bool _showLogs = true;
|
|
bool _isPlayerAttacking = false; // Player Attack Animation State
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
final battleProvider = context.read<BattleProvider>();
|
|
_damageSubscription = battleProvider.damageStream.listen(
|
|
_addFloatingDamageText,
|
|
);
|
|
_effectSubscription = battleProvider.effectStream.listen(
|
|
_addFloatingEffect,
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_damageSubscription?.cancel();
|
|
_effectSubscription?.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
void _addFloatingDamageText(DamageEvent event) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (!mounted) return;
|
|
|
|
GlobalKey targetKey = event.target == DamageTarget.player
|
|
? _playerKey
|
|
: _enemyKey;
|
|
|
|
if (targetKey.currentContext == null) return;
|
|
RenderBox? renderBox =
|
|
targetKey.currentContext!.findRenderObject() as RenderBox?;
|
|
if (renderBox == null) return;
|
|
|
|
Offset position = renderBox.localToGlobal(Offset.zero);
|
|
|
|
RenderBox? stackRenderBox =
|
|
_stackKey.currentContext?.findRenderObject() as RenderBox?;
|
|
if (stackRenderBox != null) {
|
|
Offset stackOffset = stackRenderBox.localToGlobal(Offset.zero);
|
|
position = position - stackOffset;
|
|
}
|
|
|
|
position = position + Offset(renderBox.size.width / 2 - 20, -20);
|
|
|
|
final String id = UniqueKey().toString();
|
|
|
|
setState(() {
|
|
_floatingDamageTexts.add(
|
|
DamageTextData(
|
|
id: id,
|
|
widget: Positioned(
|
|
left: position.dx,
|
|
top: position.dy,
|
|
child: FloatingDamageText(
|
|
key: ValueKey(id),
|
|
damage: event.damage.toString(),
|
|
color: event.color,
|
|
onRemove: () {
|
|
if (mounted) {
|
|
setState(() {
|
|
_floatingDamageTexts.removeWhere((e) => e.id == id);
|
|
});
|
|
}
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
final Set<String> _processedEffectIds = {};
|
|
|
|
void _addFloatingEffect(EffectEvent event) {
|
|
if (_processedEffectIds.contains(event.id)) {
|
|
return;
|
|
}
|
|
_processedEffectIds.add(event.id);
|
|
if (_processedEffectIds.length > 20) {
|
|
_processedEffectIds.remove(_processedEffectIds.first);
|
|
}
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (!mounted) return;
|
|
|
|
GlobalKey targetKey = event.target == EffectTarget.player
|
|
? _playerKey
|
|
: _enemyKey;
|
|
if (targetKey.currentContext == null) return;
|
|
|
|
RenderBox? renderBox =
|
|
targetKey.currentContext!.findRenderObject() as RenderBox?;
|
|
if (renderBox == null) return;
|
|
|
|
Offset position = renderBox.localToGlobal(Offset.zero);
|
|
|
|
RenderBox? stackRenderBox =
|
|
_stackKey.currentContext?.findRenderObject() as RenderBox?;
|
|
if (stackRenderBox != null) {
|
|
Offset stackOffset = stackRenderBox.localToGlobal(Offset.zero);
|
|
position = position - stackOffset;
|
|
}
|
|
|
|
position =
|
|
position +
|
|
Offset(renderBox.size.width / 2 - 30, renderBox.size.height / 2 - 30);
|
|
|
|
// 0. Prepare Effect Function
|
|
void showEffect() {
|
|
if (!mounted) return;
|
|
|
|
// feedbackType이 존재하면 해당 텍스트를 표시하고 기존 이펙트 아이콘은 건너뜜
|
|
if (event.feedbackType != null) {
|
|
String feedbackText;
|
|
Color feedbackColor;
|
|
switch (event.feedbackType) {
|
|
case BattleFeedbackType.miss:
|
|
feedbackText = "MISS";
|
|
feedbackColor = ThemeConfig.missText;
|
|
break;
|
|
case BattleFeedbackType.failed:
|
|
feedbackText = "FAILED";
|
|
feedbackColor = ThemeConfig.failedText;
|
|
break;
|
|
default:
|
|
feedbackText = ""; // Should not happen with current enums
|
|
feedbackColor = ThemeConfig.textColorWhite;
|
|
}
|
|
|
|
final String id = UniqueKey().toString();
|
|
setState(() {
|
|
_floatingFeedbackTexts.add(
|
|
FeedbackTextData(
|
|
id: id,
|
|
widget: Positioned(
|
|
left: position.dx,
|
|
top: position.dy,
|
|
child: FloatingFeedbackText(
|
|
key: ValueKey(id),
|
|
feedback: feedbackText,
|
|
color: feedbackColor,
|
|
onRemove: () {
|
|
if (mounted) {
|
|
setState(() {
|
|
_floatingFeedbackTexts.removeWhere((e) => e.id == id);
|
|
});
|
|
}
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
return; // feedbackType이 있으면 아이콘 이펙트는 표시하지 않음
|
|
}
|
|
|
|
// Use BattleConfig for Icon, Color, and Size
|
|
IconData icon = BattleConfig.getIcon(event.type);
|
|
Color color = BattleConfig.getColor(event.type, event.risk);
|
|
double size = BattleConfig.getSize(event.risk);
|
|
|
|
final String id = UniqueKey().toString();
|
|
|
|
setState(() {
|
|
_floatingEffects.add(
|
|
FloatingEffectData(
|
|
id: id,
|
|
widget: Positioned(
|
|
left: position.dx,
|
|
top: position.dy,
|
|
child: FloatingEffect(
|
|
key: ValueKey(id),
|
|
icon: icon,
|
|
color: color,
|
|
size: size,
|
|
onRemove: () {
|
|
if (mounted) {
|
|
setState(() {
|
|
_floatingEffects.removeWhere((e) => e.id == id);
|
|
});
|
|
}
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
}
|
|
|
|
// 1. Attack Animation Trigger (All Risk Levels)
|
|
if (event.type == ActionType.attack &&
|
|
event.target == EffectTarget.enemy &&
|
|
event.feedbackType == null) {
|
|
// Calculate target position (Enemy) relative to Player
|
|
final RenderBox? playerBox =
|
|
_playerKey.currentContext?.findRenderObject() as RenderBox?;
|
|
final RenderBox? enemyBox =
|
|
_enemyKey.currentContext?.findRenderObject() as RenderBox?;
|
|
|
|
if (playerBox != null && enemyBox != null) {
|
|
final playerPos = playerBox.localToGlobal(Offset.zero);
|
|
final enemyPos = enemyBox.localToGlobal(Offset.zero);
|
|
|
|
final offset = enemyPos - playerPos;
|
|
|
|
// Start Animation: Hide Stats
|
|
setState(() {
|
|
_isPlayerAttacking = true;
|
|
});
|
|
|
|
_playerAnimKey.currentState
|
|
?.animateAttack(offset, () {
|
|
showEffect(); // Show Effect at Impact!
|
|
// Shake and Explosion ONLY for Risky
|
|
if (event.risk == RiskLevel.risky) {
|
|
_shakeKey.currentState?.shake();
|
|
|
|
RenderBox? stackBox =
|
|
_stackKey.currentContext?.findRenderObject()
|
|
as RenderBox?;
|
|
if (stackBox != null) {
|
|
Offset localEnemyPos = stackBox.globalToLocal(enemyPos);
|
|
// Center of the enemy card roughly
|
|
localEnemyPos += Offset(
|
|
enemyBox.size.width / 2,
|
|
enemyBox.size.height / 2,
|
|
);
|
|
_explosionKey.currentState?.explode(localEnemyPos);
|
|
}
|
|
}
|
|
}, event.risk)
|
|
.then((_) {
|
|
// End Animation: Show Stats
|
|
if (mounted) {
|
|
setState(() {
|
|
_isPlayerAttacking = false;
|
|
});
|
|
}
|
|
});
|
|
}
|
|
} else {
|
|
// Not a player attack, show immediately
|
|
showEffect();
|
|
}
|
|
});
|
|
}
|
|
|
|
void _showRiskLevelSelection(BuildContext context, ActionType actionType) {
|
|
final player = context.read<BattleProvider>().player;
|
|
final baseValue = actionType == ActionType.attack
|
|
? player.totalAtk
|
|
: player.totalDefense;
|
|
|
|
showDialog(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return SimpleDialog(
|
|
title: Text("Select Risk Level for ${actionType.name}"),
|
|
children: RiskLevel.values.map((risk) {
|
|
String infoText = "";
|
|
Color infoColor = Colors.black;
|
|
double efficiency = 0.0;
|
|
int expectedValue = 0;
|
|
|
|
switch (risk) {
|
|
case RiskLevel.safe:
|
|
efficiency = 0.5;
|
|
infoColor = ThemeConfig.riskSafe;
|
|
break;
|
|
case RiskLevel.normal:
|
|
efficiency = 1.0;
|
|
infoColor = ThemeConfig.riskNormal;
|
|
break;
|
|
case RiskLevel.risky:
|
|
efficiency = 2.0;
|
|
infoColor = ThemeConfig.riskRisky;
|
|
break;
|
|
}
|
|
|
|
expectedValue = (baseValue * efficiency).toInt();
|
|
String valueUnit = actionType == ActionType.attack
|
|
? "Dmg"
|
|
: "Armor";
|
|
String successRate = "";
|
|
|
|
double baseChance = 0.0;
|
|
switch (risk) {
|
|
case RiskLevel.safe:
|
|
baseChance = 1.0;
|
|
break;
|
|
case RiskLevel.normal:
|
|
baseChance = 0.8;
|
|
break;
|
|
case RiskLevel.risky:
|
|
baseChance = 0.4;
|
|
break;
|
|
}
|
|
|
|
double finalChance = baseChance + (player.totalLuck / 100.0);
|
|
if (finalChance > 1.0) finalChance = 1.0;
|
|
successRate = "${(finalChance * 100).toInt()}%";
|
|
|
|
infoText =
|
|
"Success: $successRate, Eff: ${(efficiency * 100).toInt()}% ($expectedValue $valueUnit)";
|
|
|
|
return SimpleDialogOption(
|
|
onPressed: () {
|
|
context.read<BattleProvider>().playerAction(actionType, risk);
|
|
Navigator.pop(context);
|
|
},
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
risk.name,
|
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
Text(
|
|
infoText,
|
|
style: TextStyle(fontSize: 12, color: infoColor),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}).toList(),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ResponsiveContainer(
|
|
child: 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 ShakeWidget(
|
|
key: _shakeKey,
|
|
child: Stack(
|
|
key: _stackKey,
|
|
children: [
|
|
// 1. Background (Black)
|
|
Container(color: ThemeConfig.battleBg),
|
|
|
|
// 2. Battle Content (Top Bar + Characters)
|
|
Column(
|
|
children: [
|
|
// Top Bar
|
|
Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Flexible(
|
|
child: FittedBox(
|
|
fit: BoxFit.scaleDown,
|
|
child: Text(
|
|
"Stage ${battleProvider.stage}",
|
|
style: const TextStyle(
|
|
color: ThemeConfig.textColorWhite,
|
|
fontSize: ThemeConfig.fontSizeHeader,
|
|
fontWeight: ThemeConfig.fontWeightBold,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Flexible(
|
|
child: FittedBox(
|
|
fit: BoxFit.scaleDown,
|
|
child: Text(
|
|
"Turn ${battleProvider.turnCount}",
|
|
style: const TextStyle(
|
|
color: ThemeConfig.textColorWhite,
|
|
fontSize: ThemeConfig.fontSizeHeader,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Battle Area (Characters) - Expanded to fill available space
|
|
Expanded(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Stack(
|
|
children: [
|
|
// Enemy (Top Right)
|
|
Positioned(
|
|
top: 0,
|
|
right: 0,
|
|
child: CharacterStatusCard(
|
|
character: battleProvider.enemy,
|
|
isPlayer: false,
|
|
isTurn: !battleProvider.isPlayerTurn,
|
|
key: _enemyKey,
|
|
),
|
|
),
|
|
// Player (Bottom Left)
|
|
Positioned(
|
|
bottom: 80, // Space for FABs
|
|
left: 0,
|
|
child: CharacterStatusCard(
|
|
character: battleProvider.player,
|
|
isPlayer: true,
|
|
isTurn: battleProvider.isPlayerTurn,
|
|
key: _playerKey,
|
|
animationKey: _playerAnimKey,
|
|
hideStats: _isPlayerAttacking,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
// 3. Logs Overlay
|
|
if (_showLogs && battleProvider.logs.isNotEmpty)
|
|
Positioned(
|
|
top: 60,
|
|
left: 16,
|
|
right: 16,
|
|
height: 150,
|
|
child: BattleLogOverlay(logs: battleProvider.logs),
|
|
),
|
|
|
|
// 4. Floating Action Buttons (Bottom Right)
|
|
Positioned(
|
|
bottom: 20,
|
|
right: 20,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
_buildFloatingActionButton(
|
|
context,
|
|
"ATK",
|
|
ThemeConfig.btnActionActive,
|
|
ActionType.attack,
|
|
battleProvider.isPlayerTurn &&
|
|
!battleProvider.player.isDead &&
|
|
!battleProvider.enemy.isDead &&
|
|
!battleProvider.showRewardPopup,
|
|
),
|
|
const SizedBox(height: 16),
|
|
_buildFloatingActionButton(
|
|
context,
|
|
"DEF",
|
|
ThemeConfig.btnDefendActive,
|
|
ActionType.defend,
|
|
battleProvider.isPlayerTurn &&
|
|
!battleProvider.player.isDead &&
|
|
!battleProvider.enemy.isDead &&
|
|
!battleProvider.showRewardPopup,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// 5. Log Toggle Button (Bottom Left)
|
|
Positioned(
|
|
bottom: 20,
|
|
left: 20,
|
|
child: FloatingActionButton(
|
|
heroTag: "logToggle",
|
|
mini: true,
|
|
backgroundColor: Colors.grey[800],
|
|
onPressed: () {
|
|
setState(() {
|
|
_showLogs = !_showLogs;
|
|
});
|
|
},
|
|
child: Icon(
|
|
_showLogs ? Icons.visibility_off : Icons.visibility,
|
|
color: ThemeConfig.textColorWhite,
|
|
),
|
|
),
|
|
),
|
|
|
|
// Reward Popup
|
|
if (battleProvider.showRewardPopup)
|
|
Container(
|
|
color: ThemeConfig.cardBgColor,
|
|
child: Center(
|
|
child: SimpleDialog(
|
|
title: Row(
|
|
children: [
|
|
const Text("Victory! Choose a Reward"),
|
|
const Spacer(),
|
|
Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(Icons.monetization_on, color: ThemeConfig.statGoldColor, size: 18),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
"${battleProvider.lastGoldReward} G",
|
|
style: TextStyle(
|
|
color: ThemeConfig.statGoldColor,
|
|
fontSize: ThemeConfig.fontSizeBody,
|
|
fontWeight: ThemeConfig.fontWeightBold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
children: battleProvider.rewardOptions.map((item) {
|
|
bool isSkip = item.id == "reward_skip";
|
|
return SimpleDialogOption(
|
|
onPressed: () {
|
|
bool success = battleProvider.selectReward(item);
|
|
if (!success) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text(
|
|
"Inventory is full! Cannot take item.",
|
|
),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
},
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
if (!isSkip)
|
|
Container(
|
|
padding: const EdgeInsets.all(4),
|
|
decoration: BoxDecoration(
|
|
color: Colors.blueGrey[700],
|
|
borderRadius: BorderRadius.circular(
|
|
4),
|
|
border: Border.all(
|
|
color: item.rarity !=
|
|
ItemRarity.magic
|
|
? ItemUtils.getRarityColor(
|
|
item.rarity,
|
|
)
|
|
: ThemeConfig.rarityCommon,
|
|
),
|
|
),
|
|
child: Image.asset(
|
|
ItemUtils.getIconPath(item.slot),
|
|
width: 24,
|
|
height: 24,
|
|
fit: BoxFit.contain,
|
|
filterQuality: FilterQuality.high,
|
|
),
|
|
),
|
|
if (!isSkip) const SizedBox(width: 12),
|
|
Text(
|
|
item.name,
|
|
style: TextStyle(
|
|
fontWeight: ThemeConfig.fontWeightBold,
|
|
fontSize: ThemeConfig.fontSizeLarge,
|
|
color: isSkip
|
|
? ThemeConfig.textColorGrey
|
|
: ItemUtils.getRarityColor(
|
|
item.rarity),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
if (!isSkip) _buildItemStatText(item),
|
|
Text(
|
|
item.description,
|
|
style: const TextStyle(
|
|
fontSize: ThemeConfig.fontSizeMedium,
|
|
color: ThemeConfig.textColorGrey,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
),
|
|
),
|
|
|
|
// 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)
|
|
Container(
|
|
color: ThemeConfig.battleBg,
|
|
child: Center(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Text(
|
|
"DEFEAT",
|
|
style: TextStyle(
|
|
color: ThemeConfig.statHpColor,
|
|
fontSize: ThemeConfig.fontSizeHuge,
|
|
fontWeight: ThemeConfig.fontWeightBold,
|
|
letterSpacing: 4.0,
|
|
),
|
|
),
|
|
const SizedBox(height: 32),
|
|
ElevatedButton(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.grey[800],
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 32,
|
|
vertical: 16,
|
|
),
|
|
),
|
|
onPressed: () {
|
|
Navigator.of(context).pushAndRemoveUntil(
|
|
MaterialPageRoute(
|
|
builder: (context) => const MainMenuScreen(),
|
|
),
|
|
(route) => false,
|
|
);
|
|
},
|
|
child: const Text(
|
|
"Return to Main Menu",
|
|
style: TextStyle(
|
|
color: ThemeConfig.textColorWhite,
|
|
fontSize: ThemeConfig.fontSizeHeader,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildItemStatText(Item item) {
|
|
List<String> stats = [];
|
|
if (item.atkBonus > 0) stats.add("+${item.atkBonus} ATK");
|
|
if (item.hpBonus > 0) stats.add("+${item.hpBonus} HP");
|
|
if (item.armorBonus > 0) stats.add("+${item.armorBonus} DEF");
|
|
if (item.luck > 0) stats.add("+${item.luck} Luck");
|
|
|
|
List<String> effectTexts = item.effects.map((e) => e.description).toList();
|
|
|
|
if (stats.isEmpty && effectTexts.isEmpty) return const SizedBox.shrink();
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
if (stats.isNotEmpty)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 4.0, bottom: 4.0),
|
|
child: Text(
|
|
stats.join(", "),
|
|
style: const TextStyle(fontSize: ThemeConfig.fontSizeMedium, color: ThemeConfig.statAtkColor),
|
|
),
|
|
),
|
|
if (effectTexts.isNotEmpty)
|
|
Padding(
|
|
padding: const EdgeInsets.only(bottom: 4.0),
|
|
child: Text(
|
|
effectTexts.join(", "),
|
|
style: const TextStyle(fontSize: 11, color: ThemeConfig.rarityLegendary), // 11 is custom, keep or change? Let's use Small
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildFloatingActionButton(
|
|
BuildContext context,
|
|
String label,
|
|
Color color,
|
|
ActionType actionType,
|
|
bool isEnabled,
|
|
) {
|
|
String iconPath;
|
|
if (actionType == ActionType.attack) {
|
|
iconPath = 'assets/data/icon/icon_weapon.png';
|
|
} else {
|
|
iconPath = 'assets/data/icon/icon_shield.png';
|
|
}
|
|
|
|
return FloatingActionButton(
|
|
heroTag: label,
|
|
onPressed: isEnabled
|
|
? () => _showRiskLevelSelection(context, actionType)
|
|
: null,
|
|
backgroundColor: isEnabled ? color : ThemeConfig.btnDisabled,
|
|
child: Image.asset(
|
|
iconPath,
|
|
width: 32,
|
|
height: 32,
|
|
color: ThemeConfig.textColorWhite, // Tint icon white
|
|
fit: BoxFit.contain,
|
|
filterQuality: FilterQuality.high,
|
|
),
|
|
);
|
|
}
|
|
}
|