refactoring : barrel pattern

This commit is contained in:
2025-12-09 14:40:25 +09:00
parent ac4df02654
commit bb3b34c9b1
36 changed files with 708 additions and 497 deletions
+80 -227
View File
@@ -1,28 +1,15 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/battle_provider.dart';
import '../providers.dart';
import '../game/enums.dart';
import '../game/model/item.dart';
import '../game/model/damage_event.dart';
import '../game/model/effect_event.dart';
import '../game/models.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 '../widgets.dart';
import '../utils.dart';
import 'main_menu_screen.dart';
import '../game/config/battle_config.dart';
import '../game/config/theme_config.dart';
import '../game/config/app_strings.dart';
import '../providers/settings_provider.dart'; // Import SettingsProvider
import '../game/config.dart';
class BattleScreen extends StatefulWidget {
const BattleScreen({super.key});
@@ -97,7 +84,12 @@ class _BattleScreenState extends State<BattleScreen> {
position = position - stackOffset;
}
position = position + Offset(renderBox.size.width / 2 - 20, -20);
position =
position +
Offset(
renderBox.size.width / 2 + BattleConfig.damageTextOffsetX,
BattleConfig.damageTextOffsetY,
);
final String id = UniqueKey().toString();
@@ -141,9 +133,10 @@ class _BattleScreenState extends State<BattleScreen> {
// Let's just wrap `FloatingDamageText` in a `Transform.scale` with a value.
// I'll define a variable scale.
double scale = 1.0;
// Heuristic: If damage is high (e.g. > 15), assume it might be risky/crit
if (event.damage > 15) scale = 3;
double scale = BattleConfig.damageScaleNormal;
if (event.damage > BattleConfig.highDamageThreshold) {
scale = BattleConfig.damageScaleHigh;
}
setState(() {
_floatingDamageTexts.add(
@@ -195,7 +188,8 @@ class _BattleScreenState extends State<BattleScreen> {
// "[UI Debug] Feedback Event: ${event.id}, Type: ${event.feedbackType}",
// );
if (_lastFeedbackTime != null &&
DateTime.now().difference(_lastFeedbackTime!).inMilliseconds < 300) {
DateTime.now().difference(_lastFeedbackTime!).inMilliseconds <
BattleConfig.feedbackCooldownMs) {
return; // Skip if too soon
}
_lastFeedbackTime = DateTime.now();
@@ -227,12 +221,12 @@ class _BattleScreenState extends State<BattleScreen> {
if (event.target == EffectTarget.enemy) {
// Enemy is top-right, so effect should be left-bottom of its card
offsetX = renderBox.size.width * 0.1; // 20% from left edge
offsetY = renderBox.size.height * 0.8; // 80% from top edge
offsetX = renderBox.size.width * BattleConfig.effectEnemyOffsetX;
offsetY = renderBox.size.height * BattleConfig.effectEnemyOffsetY;
} else {
// Player is bottom-left, so effect should be right-top of its card
offsetX = renderBox.size.width * 0.8; // 80% from left edge
offsetY = renderBox.size.height * 0.2; // 20% from top edge
offsetX = renderBox.size.width * BattleConfig.effectPlayerOffsetX;
offsetY = renderBox.size.height * BattleConfig.effectPlayerOffsetY;
}
position = position + Offset(offsetX, offsetY);
@@ -488,17 +482,39 @@ class _BattleScreenState extends State<BattleScreen> {
}
void _showRiskLevelSelection(BuildContext context, ActionType actionType) {
// 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;
}
// Interactive Enemy Defense Trigger
// If enemy intends to defend, trigger animation NOW (when user interacts)
final enemyIntent = battleProvider.currentEnemyIntent;
if (enemyIntent != null &&
enemyIntent.type == EnemyActionType.defend &&
@@ -538,102 +554,10 @@ class _BattleScreenState extends State<BattleScreen> {
.then((_) {
if (mounted) setState(() => _isEnemyAttacking = false);
});
return true;
}
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 = actionType == ActionType.attack
? BattleConfig.attackSafeEfficiency
: BattleConfig.defendSafeEfficiency;
infoColor = ThemeConfig.riskSafe;
break;
case RiskLevel.normal:
efficiency = actionType == ActionType.attack
? BattleConfig.attackNormalEfficiency
: BattleConfig.defendNormalEfficiency;
infoColor = ThemeConfig.riskNormal;
break;
case RiskLevel.risky:
efficiency = actionType == ActionType.attack
? BattleConfig.attackRiskyEfficiency
: BattleConfig.defendRiskyEfficiency;
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 = BattleConfig.safeBaseChance;
efficiency = actionType == ActionType.attack
? BattleConfig.attackSafeEfficiency
: BattleConfig.defendSafeEfficiency;
break;
case RiskLevel.normal:
baseChance = BattleConfig.normalBaseChance;
efficiency = actionType == ActionType.attack
? BattleConfig.attackNormalEfficiency
: BattleConfig.defendNormalEfficiency;
break;
case RiskLevel.risky:
baseChance = BattleConfig.riskyBaseChance;
efficiency = actionType == ActionType.attack
? BattleConfig.attackRiskyEfficiency
: BattleConfig.defendRiskyEfficiency;
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(),
);
},
);
return false;
}
@override
@@ -664,39 +588,7 @@ class _BattleScreenState extends State<BattleScreen> {
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(
"${AppStrings.turn} ${battleProvider.turnCount}",
style: const TextStyle(
color: ThemeConfig.textColorWhite,
fontSize: ThemeConfig.fontSizeHeader,
),
),
),
),
],
),
),
const BattleHeader(),
// Battle Area (Characters) - Expanded to fill available space
Expanded(
@@ -742,43 +634,33 @@ class _BattleScreenState extends State<BattleScreen> {
top: 60,
left: 16,
right: 16,
height: 150,
height: BattleConfig.logsOverlayHeight,
child: BattleLogOverlay(logs: battleProvider.logs),
),
// 4. Floating Action Buttons (Bottom Right)
// 4. Battle Controls (Bottom Right)
Positioned(
bottom: 20,
right: 20,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildFloatingActionButton(
context,
"ATK",
ThemeConfig.btnActionActive,
ActionType.attack,
child: BattleControls(
isAttackEnabled:
battleProvider.isPlayerTurn &&
!battleProvider.player.isDead &&
!battleProvider.enemy.isDead &&
!battleProvider.showRewardPopup &&
!_isPlayerAttacking &&
!_isEnemyAttacking,
),
const SizedBox(height: 16),
_buildFloatingActionButton(
context,
"DEF",
ThemeConfig.btnDefendActive,
ActionType.defend,
!battleProvider.player.isDead &&
!battleProvider.enemy.isDead &&
!battleProvider.showRewardPopup &&
!_isPlayerAttacking &&
!_isEnemyAttacking,
isDefendEnabled:
battleProvider.isPlayerTurn &&
!battleProvider.player.isDead &&
!battleProvider.enemy.isDead &&
!battleProvider.showRewardPopup &&
!_isPlayerAttacking &&
!_isEnemyAttacking,
),
],
!battleProvider.player.isDead &&
!battleProvider.enemy.isDead &&
!battleProvider.showRewardPopup &&
!_isPlayerAttacking &&
!_isEnemyAttacking,
onAttackPressed: () =>
_showRiskLevelSelection(context, ActionType.attack),
onDefendPressed: () =>
_showRiskLevelSelection(context, ActionType.defend),
),
),
@@ -789,7 +671,7 @@ class _BattleScreenState extends State<BattleScreen> {
child: FloatingActionButton(
heroTag: "logToggle",
mini: true,
backgroundColor: Colors.grey[800],
backgroundColor: ThemeConfig.toggleBtnBg,
onPressed: () {
setState(() {
_showLogs = !_showLogs;
@@ -820,7 +702,7 @@ class _BattleScreenState extends State<BattleScreen> {
Icon(
Icons.monetization_on,
color: ThemeConfig.statGoldColor,
size: 18,
size: ThemeConfig.itemIconSizeSmall,
),
const SizedBox(width: 4),
Text(
@@ -846,7 +728,8 @@ class _BattleScreenState extends State<BattleScreen> {
content: Text(
"${AppStrings.inventoryFull} Cannot take item.",
),
backgroundColor: Colors.red,
backgroundColor:
ThemeConfig.snackBarErrorBg,
),
);
}
@@ -860,7 +743,7 @@ class _BattleScreenState extends State<BattleScreen> {
Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.blueGrey[700],
color: ThemeConfig.rewardItemBg,
borderRadius: BorderRadius.circular(
4,
),
@@ -875,8 +758,9 @@ class _BattleScreenState extends State<BattleScreen> {
),
child: Image.asset(
ItemUtils.getIconPath(item.slot),
width: 24,
height: 24,
width: ThemeConfig.itemIconSizeMedium,
height:
ThemeConfig.itemIconSizeMedium,
fit: BoxFit.contain,
filterQuality: FilterQuality.high,
),
@@ -934,16 +818,16 @@ class _BattleScreenState extends State<BattleScreen> {
color: ThemeConfig.statHpColor,
fontSize: ThemeConfig.fontSizeHuge,
fontWeight: ThemeConfig.fontWeightBold,
letterSpacing: 4.0,
letterSpacing: ThemeConfig.letterSpacingHeader,
),
),
const SizedBox(height: 32),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey[800],
backgroundColor: ThemeConfig.menuButtonBg,
padding: const EdgeInsets.symmetric(
horizontal: 32,
vertical: 16,
horizontal: ThemeConfig.paddingBtnHorizontal,
vertical: ThemeConfig.paddingBtnVertical,
),
),
onPressed: () {
@@ -1013,35 +897,4 @@ class _BattleScreenState extends State<BattleScreen> {
],
);
}
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,
),
);
}
}
+7 -6
View File
@@ -1,11 +1,10 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/battle_provider.dart';
import '../game/data/player_table.dart';
import '../providers.dart';
import '../game/data.dart';
import 'main_wrapper.dart';
import '../widgets/responsive_container.dart';
import '../game/config/theme_config.dart';
import '../game/config/app_strings.dart';
import '../widgets.dart';
import '../game/config.dart';
class CharacterSelectionScreen extends StatelessWidget {
const CharacterSelectionScreen({super.key});
@@ -75,7 +74,9 @@ class CharacterSelectionScreen extends StatelessWidget {
Text(
warrior.description,
textAlign: TextAlign.center,
style: const TextStyle(color: ThemeConfig.textColorGrey),
style: const TextStyle(
color: ThemeConfig.textColorGrey,
),
),
const SizedBox(height: 16),
const Divider(),
+5 -7
View File
@@ -1,13 +1,11 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/battle_provider.dart';
import '../game/model/item.dart';
import '../providers.dart';
import '../game/models.dart';
import '../game/enums.dart';
import '../utils/item_utils.dart';
import '../game/config/theme_config.dart';
import '../game/config/app_strings.dart';
import '../widgets/inventory/character_stats_widget.dart';
import '../widgets/inventory/inventory_grid_widget.dart';
import '../utils.dart';
import '../game/config.dart';
import '../widgets.dart';
class InventoryScreen extends StatelessWidget {
const InventoryScreen({super.key});
+7 -10
View File
@@ -2,11 +2,10 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'character_selection_screen.dart';
import 'main_wrapper.dart';
import '../widgets/responsive_container.dart';
import '../widgets.dart';
import '../game/save_manager.dart';
import '../providers/battle_provider.dart';
import '../game/config/theme_config.dart';
import '../game/config/app_strings.dart';
import '../providers.dart';
import '../game/config.dart';
class MainMenuScreen extends StatefulWidget {
const MainMenuScreen({super.key});
@@ -61,10 +60,7 @@ class _MainMenuScreenState extends State<MainMenuScreen> {
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
ThemeConfig.mainMenuBgTop,
ThemeConfig.mainMenuBgBottom,
],
colors: [ThemeConfig.mainMenuBgTop, ThemeConfig.mainMenuBgBottom],
),
),
child: ResponsiveContainer(
@@ -121,10 +117,11 @@ class _MainMenuScreenState extends State<MainMenuScreen> {
onPressed: () {
// Warn if save exists? Or just overwrite on save.
// For now, simpler flow.
Navigator.push(
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CharacterSelectionScreen(),
builder: (context) =>
const CharacterSelectionScreen(),
),
);
},
+78 -30
View File
@@ -1,9 +1,12 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers.dart';
import '../game/enums.dart';
import 'battle_screen.dart';
import 'inventory_screen.dart';
import 'settings_screen.dart';
import '../widgets/responsive_container.dart';
import '../game/config/theme_config.dart';
import '../widgets.dart';
import '../game/config.dart';
class MainWrapper extends StatefulWidget {
const MainWrapper({super.key});
@@ -23,37 +26,82 @@ class _MainWrapperState extends State<MainWrapper> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: ThemeConfig.mainMenuBgTop, // Outer background for web
body: Center(
child: ResponsiveContainer(
child: Scaffold(
body: IndexedStack(index: _currentIndex, children: _screens),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
onTap: (index) {
setState(() {
_currentIndex = index;
});
},
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.flash_on),
label: 'Battle',
return Consumer<BattleProvider>(
builder: (context, battleProvider, child) {
// Determine the first tab's icon and label based on StageType
String stageLabel = "Battle";
IconData stageIcon = Icons.flash_on;
// Ensure we check null safety if currentStage isn't ready (though it should be)
// battleProvider.currentStage might be accessed safely if standardized
// Assuming battleProvider.currentStage is accessible or we check stage type logic
try {
final stageType = battleProvider.currentStage.type;
switch (stageType) {
case StageType.battle:
case StageType.elite:
stageLabel = "Battle";
stageIcon = Icons.flash_on;
break;
case StageType.shop:
stageLabel = "Shop";
stageIcon = Icons.store;
break;
case StageType.rest:
stageLabel = "Rest";
stageIcon = Icons.hotel;
break;
}
} catch (e) {
// Fallback if not initialized
}
return Scaffold(
backgroundColor: ThemeConfig.mainMenuBgTop,
body: Center(
child: ResponsiveContainer(
child: Scaffold(
body: IndexedStack(index: _currentIndex, children: _screens),
bottomNavigationBar: Theme(
data: Theme.of(
context,
).copyWith(canvasColor: ThemeConfig.mainMenuBgBottom),
child: BottomNavigationBar(
currentIndex: _currentIndex,
onTap: (index) {
setState(() {
_currentIndex = index;
});
},
backgroundColor: ThemeConfig.mainMenuBgBottom,
selectedItemColor: ThemeConfig.mainIconColor,
unselectedItemColor: ThemeConfig.textColorGrey,
selectedFontSize:
ThemeConfig.fontSizeLarge, // Highlight selection
unselectedFontSize: ThemeConfig.fontSizeSmall,
type: BottomNavigationBarType
.fixed, // Ensure consistent formatting
items: [
BottomNavigationBarItem(
icon: Icon(stageIcon),
label: stageLabel,
),
const BottomNavigationBarItem(
icon: Icon(Icons.backpack),
label: 'Inventory',
),
const BottomNavigationBarItem(
icon: Icon(Icons.settings),
label: 'Settings',
),
],
),
),
BottomNavigationBarItem(
icon: Icon(Icons.backpack),
label: 'Inventory',
),
BottomNavigationBarItem(
icon: Icon(Icons.settings),
label: 'Settings',
),
],
),
),
),
),
),
);
},
);
}
}
+2 -4
View File
@@ -1,10 +1,8 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/battle_provider.dart';
import '../providers/settings_provider.dart'; // Import SettingsProvider
import '../providers.dart';
import 'main_menu_screen.dart';
import '../game/config/theme_config.dart';
import '../game/config/app_strings.dart';
import '../game/config.dart';
class SettingsScreen extends StatelessWidget {
const SettingsScreen({super.key});