diff --git a/lib/game/config.dart b/lib/game/config.dart new file mode 100644 index 0000000..bd56165 --- /dev/null +++ b/lib/game/config.dart @@ -0,0 +1,6 @@ +export 'config/animation_config.dart'; +export 'config/app_strings.dart'; +export 'config/battle_config.dart'; +export 'config/game_config.dart'; +export 'config/item_config.dart'; +export 'config/theme_config.dart'; diff --git a/lib/game/config/battle_config.dart b/lib/game/config/battle_config.dart index dd3f38d..3b049b3 100644 --- a/lib/game/config/battle_config.dart +++ b/lib/game/config/battle_config.dart @@ -11,15 +11,34 @@ class BattleConfig { static const Color normalColor = Colors.white; static const Color safeColor = Colors.grey; + // Layout & Animation + static const int feedbackCooldownMs = 300; + static const double damageTextOffsetY = -20.0; + static const double damageTextOffsetX = -20.0; + + // Effect Offsets (Relative to Card Size) + static const double effectEnemyOffsetX = 0.1; // 10% + static const double effectEnemyOffsetY = 0.8; // 80% + static const double effectPlayerOffsetX = 0.8; // 80% + static const double effectPlayerOffsetY = 0.2; // 20% + + // Logs + static const double logsOverlayHeight = 150.0; + static const Color defendRiskyColor = Colors.deepPurpleAccent; static const Color defendNormalColor = Colors.blueAccent; static const Color defendSafeColor = Colors.greenAccent; // Sizes - static const double sizeRisky = 80.0; // User increased this in previous edit + static const double sizeRisky = 80.0; static const double sizeNormal = 60.0; static const double sizeSafe = 40.0; + // Damage Text Scale + static const double damageScaleNormal = 1.0; + static const double damageScaleHigh = 3.0; + static const int highDamageThreshold = 15; + // Logic Constants // Safe static const double safeBaseChance = 1.0; // 100% diff --git a/lib/game/config/theme_config.dart b/lib/game/config/theme_config.dart index 1b62192..3ad0e78 100644 --- a/lib/game/config/theme_config.dart +++ b/lib/game/config/theme_config.dart @@ -66,6 +66,16 @@ class ThemeConfig { static const Color enemyIntentBorder = Colors.redAccent; static final Color? selectionCardBg = Colors.blueGrey[800]; static const Color selectionIconColor = Colors.blue; + static final Color toggleBtnBg = Colors.grey[800]!; + static final Color rewardItemBg = Colors.blueGrey[700]!; + static const Color snackBarErrorBg = Colors.red; + static const Color iconColorWhite = Colors.white; + static const Color menuButtonBg = Color(0xFF424242); // Grey 800 + static const double itemIconSizeSmall = 18.0; + static const double itemIconSizeMedium = 24.0; + static const double letterSpacingHeader = 4.0; + static const double paddingBtnHorizontal = 32.0; + static const double paddingBtnVertical = 16.0; // Feedback Colors static const Color damageTextDefault = Colors.red; diff --git a/lib/game/data.dart b/lib/game/data.dart new file mode 100644 index 0000000..d8798e9 --- /dev/null +++ b/lib/game/data.dart @@ -0,0 +1,5 @@ +export 'data/enemy_table.dart'; +export 'data/item_table.dart'; +export 'data/item_prefix_table.dart'; +export 'data/name_generator.dart'; +export 'data/player_table.dart'; diff --git a/lib/game/data/item_table.dart b/lib/game/data/item_table.dart index 7469aa8..e000afb 100644 --- a/lib/game/data/item_table.dart +++ b/lib/game/data/item_table.dart @@ -7,7 +7,6 @@ import '../config/item_config.dart'; // import 'item_prefix_table.dart'; // Logic moved to LootGenerator // import 'name_generator.dart'; // Logic moved to LootGenerator import '../logic/loot_generator.dart'; // Import LootGenerator -import '../../utils/game_math.dart'; class ItemTemplate { final String id; @@ -70,7 +69,7 @@ class ItemTemplate { } Item createItem({int stage = 1}) { - // Stage parameter kept for interface compatibility but unused here, + // Stage parameter kept for interface compatibility but unused here, // as scaling is now handled via Tier/Rarity in LootGenerator/Table logic. return LootGenerator.generate(this); } @@ -125,7 +124,7 @@ class ItemTable { } /// Returns a random item based on Tier and Rarity weights. - /// + /// /// [tier]: The tier of items to select from. /// [slot]: Optional. If provided, only items of this slot are considered. /// [weights]: Optional map of rarity weights. Key: Rarity, Value: Weight. @@ -143,11 +142,13 @@ class ItemTable { if (slot != null) { candidates = candidates.where((item) => item.slot == slot); } - + if (candidates.isEmpty) return null; // 2. Prepare Rarity Weights (Filtered by min/max) - Map activeWeights = Map.from(weights ?? ItemConfig.defaultRarityWeights); + Map activeWeights = Map.from( + weights ?? ItemConfig.defaultRarityWeights, + ); if (minRarity != null) { activeWeights.removeWhere((r, w) => r.index < minRarity.index); @@ -157,13 +158,17 @@ class ItemTable { } if (activeWeights.isEmpty) { - // Fallback: If weights eliminated all options (e.g. misconfiguration), + // Fallback: If weights eliminated all options (e.g. misconfiguration), // try to find ANY item within rarity range from candidates. if (minRarity != null) { - candidates = candidates.where((item) => item.rarity.index >= minRarity.index); + candidates = candidates.where( + (item) => item.rarity.index >= minRarity.index, + ); } if (maxRarity != null) { - candidates = candidates.where((item) => item.rarity.index <= maxRarity.index); + candidates = candidates.where( + (item) => item.rarity.index <= maxRarity.index, + ); } if (candidates.isEmpty) return null; return candidates.toList()[_random.nextInt(candidates.length)]; @@ -172,10 +177,10 @@ class ItemTable { // 3. Determine Target Rarity based on filtered weights int totalWeight = activeWeights.values.fold(0, (sum, w) => sum + w); int roll = _random.nextInt(totalWeight); - + ItemRarity? selectedRarity; int currentSum = 0; - + for (var entry in activeWeights.entries) { currentSum += entry.value; if (roll < currentSum) { @@ -185,15 +190,21 @@ class ItemTable { } // 4. Filter candidates by Selected Rarity - var rarityCandidates = candidates.where((item) => item.rarity == selectedRarity).toList(); + var rarityCandidates = candidates + .where((item) => item.rarity == selectedRarity) + .toList(); // 5. Fallback: If no items of selected rarity, use any item from the filtered candidates (respecting min/max) if (rarityCandidates.isEmpty) { - if (minRarity != null) { - candidates = candidates.where((item) => item.rarity.index >= minRarity.index); + if (minRarity != null) { + candidates = candidates.where( + (item) => item.rarity.index >= minRarity.index, + ); } if (maxRarity != null) { - candidates = candidates.where((item) => item.rarity.index <= maxRarity.index); + candidates = candidates.where( + (item) => item.rarity.index <= maxRarity.index, + ); } if (candidates.isEmpty) return null; return candidates.toList()[_random.nextInt(candidates.length)]; diff --git a/lib/game/logic.dart b/lib/game/logic.dart new file mode 100644 index 0000000..b7edfdf --- /dev/null +++ b/lib/game/logic.dart @@ -0,0 +1,3 @@ +export 'logic/battle_log_manager.dart'; +export 'logic/combat_calculator.dart'; +export 'logic/loot_generator.dart'; diff --git a/lib/game/models.dart b/lib/game/models.dart new file mode 100644 index 0000000..2a57607 --- /dev/null +++ b/lib/game/models.dart @@ -0,0 +1,8 @@ +export 'model/damage_event.dart'; +export 'model/effect_event.dart'; +export 'model/entity.dart'; +export 'model/item.dart'; +export 'model/stage.dart'; +export 'model/stat.dart'; +export 'model/stat_modifier.dart'; +export 'model/status_effect.dart'; diff --git a/lib/game/save_manager.dart b/lib/game/save_manager.dart index 655d2c2..2b368ae 100644 --- a/lib/game/save_manager.dart +++ b/lib/game/save_manager.dart @@ -1,6 +1,6 @@ import 'dart:convert'; import 'package:shared_preferences/shared_preferences.dart'; -import '../providers/battle_provider.dart'; +import '../providers.dart'; import 'model/entity.dart'; import 'config/game_config.dart'; @@ -9,7 +9,7 @@ class SaveManager { static Future saveGame(BattleProvider provider) async { final prefs = await SharedPreferences.getInstance(); - + final saveData = { 'stage': provider.stage, 'turnCount': provider.turnCount, diff --git a/lib/main.dart b/lib/main.dart index eb33c50..cc30bba 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,10 +3,8 @@ import 'package:provider/provider.dart'; import 'game/data/item_table.dart'; import 'game/data/enemy_table.dart'; import 'game/data/player_table.dart'; -import 'providers/battle_provider.dart'; -import 'providers/shop_provider.dart'; // Import ShopProvider -import 'providers/settings_provider.dart'; // Import SettingsProvider -import 'screens/main_menu_screen.dart'; +import 'providers.dart'; +import 'screens.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -23,7 +21,9 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MultiProvider( providers: [ - ChangeNotifierProvider(create: (_) => SettingsProvider()), // Register SettingsProvider + ChangeNotifierProvider( + create: (_) => SettingsProvider(), + ), // Register SettingsProvider ChangeNotifierProvider(create: (_) => ShopProvider()), ChangeNotifierProxyProvider( create: (context) => BattleProvider( diff --git a/lib/providers.dart b/lib/providers.dart new file mode 100644 index 0000000..67eec2d --- /dev/null +++ b/lib/providers.dart @@ -0,0 +1,3 @@ +export 'providers/battle_provider.dart'; +export 'providers/settings_provider.dart'; +export 'providers/shop_provider.dart'; diff --git a/lib/providers/battle_provider.dart b/lib/providers/battle_provider.dart index 4e63c7e..c76710b 100644 --- a/lib/providers/battle_provider.dart +++ b/lib/providers/battle_provider.dart @@ -3,26 +3,17 @@ import 'dart:math'; import 'package:flutter/material.dart'; -import '../game/model/entity.dart'; -import '../game/model/item.dart'; -import '../game/model/status_effect.dart'; -import '../game/model/stage.dart'; -import '../game/data/item_table.dart'; +import '../game/models.dart'; +import '../game/data.dart'; -import '../game/data/enemy_table.dart'; -import '../game/data/player_table.dart'; -import '../utils/game_math.dart'; +import '../utils.dart'; import '../game/enums.dart'; -import '../game/model/damage_event.dart'; // DamageEvent import -import '../game/model/effect_event.dart'; // EffectEvent import import '../game/save_manager.dart'; -import '../game/config/game_config.dart'; -import '../game/config/battle_config.dart'; // Import BattleConfig -import 'shop_provider.dart'; // Import ShopProvider +import '../game/config.dart'; +import 'shop_provider.dart'; -import '../game/logic/battle_log_manager.dart'; -import '../game/logic/combat_calculator.dart'; +import '../game/logic.dart'; class EnemyIntent { final EnemyActionType type; diff --git a/lib/providers/shop_provider.dart b/lib/providers/shop_provider.dart index 638806f..68e7ad5 100644 --- a/lib/providers/shop_provider.dart +++ b/lib/providers/shop_provider.dart @@ -1,10 +1,8 @@ import 'package:flutter/material.dart'; -import '../game/model/item.dart'; -import '../game/model/entity.dart'; -import '../game/data/item_table.dart'; +import '../game/models.dart'; +import '../game/data.dart'; import '../game/enums.dart'; -import '../game/config/game_config.dart'; -import '../utils/game_math.dart'; +import '../game/config.dart'; class ShopProvider with ChangeNotifier { List availableItems = []; @@ -25,7 +23,8 @@ class ShopProvider with ChangeNotifier { currentTier = ItemTier.tier2; availableItems = []; - for (int i = 0; i < 4; i++) { // Generate 4 items + for (int i = 0; i < 4; i++) { + // Generate 4 items ItemTemplate? template = ItemTable.getRandomItem(tier: currentTier); if (template != null) { availableItems.add(template.createItem(stage: stage)); @@ -38,7 +37,9 @@ class ShopProvider with ChangeNotifier { const int rerollCost = GameConfig.shopRerollCost; if (player.gold >= rerollCost) { player.gold -= rerollCost; - generateShopItems(currentStageNumber); // Regenerate based on current stage + generateShopItems( + currentStageNumber, + ); // Regenerate based on current stage _lastShopMessage = "Shop items rerolled for $rerollCost G."; notifyListeners(); return true; diff --git a/lib/screens.dart b/lib/screens.dart new file mode 100644 index 0000000..1e9f8e0 --- /dev/null +++ b/lib/screens.dart @@ -0,0 +1,6 @@ +export 'screens/battle_screen.dart'; +export 'screens/character_selection_screen.dart'; +export 'screens/inventory_screen.dart'; +export 'screens/main_menu_screen.dart'; +export 'screens/main_wrapper.dart'; +export 'screens/settings_screen.dart'; diff --git a/lib/screens/battle_screen.dart b/lib/screens/battle_screen.dart index 87ad26d..7b252e7 100644 --- a/lib/screens/battle_screen.dart +++ b/lib/screens/battle_screen.dart @@ -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 { 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 { // 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 { // "[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 { 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 { } 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(); 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; } - // 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 { .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().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 { 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 { 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 { 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 { 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 { content: Text( "${AppStrings.inventoryFull} Cannot take item.", ), - backgroundColor: Colors.red, + backgroundColor: + ThemeConfig.snackBarErrorBg, ), ); } @@ -860,7 +743,7 @@ class _BattleScreenState extends State { 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 { ), 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 { 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 { ], ); } - - 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, - ), - ); - } } diff --git a/lib/screens/character_selection_screen.dart b/lib/screens/character_selection_screen.dart index 6bec011..dd3238b 100644 --- a/lib/screens/character_selection_screen.dart +++ b/lib/screens/character_selection_screen.dart @@ -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(), diff --git a/lib/screens/inventory_screen.dart b/lib/screens/inventory_screen.dart index b05d455..3105ccf 100644 --- a/lib/screens/inventory_screen.dart +++ b/lib/screens/inventory_screen.dart @@ -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}); diff --git a/lib/screens/main_menu_screen.dart b/lib/screens/main_menu_screen.dart index 7e844fa..cfc7b7a 100644 --- a/lib/screens/main_menu_screen.dart +++ b/lib/screens/main_menu_screen.dart @@ -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 { 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 { 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(), ), ); }, diff --git a/lib/screens/main_wrapper.dart b/lib/screens/main_wrapper.dart index 50df1b8..1a0b45c 100644 --- a/lib/screens/main_wrapper.dart +++ b/lib/screens/main_wrapper.dart @@ -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 { @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( + 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', - ), - ], + ), ), ), - ), - ), + ); + }, ); } } diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 04837cb..3a97e7b 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -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}); diff --git a/lib/utils.dart b/lib/utils.dart new file mode 100644 index 0000000..b622b1a --- /dev/null +++ b/lib/utils.dart @@ -0,0 +1,2 @@ +export 'utils/game_math.dart'; +export 'utils/item_utils.dart'; diff --git a/lib/utils/item_utils.dart b/lib/utils/item_utils.dart index 19212be..a8c806c 100644 --- a/lib/utils/item_utils.dart +++ b/lib/utils/item_utils.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import '../game/enums.dart'; -import '../game/config/theme_config.dart'; +import '../game/config.dart'; class ItemUtils { static Color getRarityColor(ItemRarity rarity) { diff --git a/lib/widgets.dart b/lib/widgets.dart new file mode 100644 index 0000000..fbafce0 --- /dev/null +++ b/lib/widgets.dart @@ -0,0 +1,5 @@ +export 'widgets/responsive_container.dart'; +export 'widgets/battle.dart'; +export 'widgets/common.dart'; +export 'widgets/inventory.dart'; +export 'widgets/stage.dart'; diff --git a/lib/widgets/battle.dart b/lib/widgets/battle.dart new file mode 100644 index 0000000..38e027c --- /dev/null +++ b/lib/widgets/battle.dart @@ -0,0 +1,9 @@ +export 'battle/battle_animation_widget.dart'; +export 'battle/battle_controls.dart'; +export 'battle/battle_header.dart'; +export 'battle/battle_log_overlay.dart'; +export 'battle/character_status_card.dart'; +export 'battle/explosion_widget.dart'; +export 'battle/floating_battle_texts.dart'; +export 'battle/risk_selection_dialog.dart'; +export 'battle/shake_widget.dart'; diff --git a/lib/widgets/battle/battle_controls.dart b/lib/widgets/battle/battle_controls.dart new file mode 100644 index 0000000..0e37f08 --- /dev/null +++ b/lib/widgets/battle/battle_controls.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import '../../game/enums.dart'; +import '../../game/config.dart'; + +class BattleControls extends StatelessWidget { + final bool isAttackEnabled; + final bool isDefendEnabled; + final VoidCallback onAttackPressed; + final VoidCallback onDefendPressed; + + const BattleControls({ + super.key, + required this.isAttackEnabled, + required this.isDefendEnabled, + required this.onAttackPressed, + required this.onDefendPressed, + }); + + Widget _buildFloatingActionButton({ + required String label, + required Color color, + required ActionType actionType, + required bool isEnabled, + required VoidCallback onPressed, + }) { + 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 ? onPressed : 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, + ), + ); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildFloatingActionButton( + label: "ATK", + color: ThemeConfig.btnActionActive, + actionType: ActionType.attack, + isEnabled: isAttackEnabled, + onPressed: onAttackPressed, + ), + const SizedBox(height: 16), + _buildFloatingActionButton( + label: "DEF", + color: ThemeConfig.btnDefendActive, + actionType: ActionType.defend, + isEnabled: isDefendEnabled, + onPressed: onDefendPressed, + ), + ], + ); + } +} diff --git a/lib/widgets/battle/battle_header.dart b/lib/widgets/battle/battle_header.dart new file mode 100644 index 0000000..9bd39bd --- /dev/null +++ b/lib/widgets/battle/battle_header.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import '../../providers.dart'; +import '../../game/config.dart'; +import 'package:provider/provider.dart'; + +class BattleHeader extends StatelessWidget { + const BattleHeader({super.key}); + + String _getStageDisplayString(int stage) { + // 3 Stages per Tier logic + // Tier 1: 1, 2, 3 -> Underground Illegal Arena + // Tier 2: 4, 5, 6 -> Colosseum + // Tier 3: 7, 8, 9 -> King's Arena + + final int tier = (stage - 1) ~/ 3 + 1; + final int round = (stage - 1) % 3 + 1; + String tierName = ""; + + switch (tier) { + case 1: + tierName = "지하 불법 투기장"; + break; + case 2: + tierName = "콜로세움"; + break; + case 3: + default: + tierName = "왕의 투기장"; + break; + } + + return "Tier $tier $tierName - $round"; + } + + @override + Widget build(BuildContext context) { + final battleProvider = context.watch(); + + return Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + _getStageDisplayString(battleProvider.stage), + style: const TextStyle( + color: ThemeConfig.textColorWhite, + fontSize: ThemeConfig.fontSizeLarge, + fontWeight: ThemeConfig.fontWeightBold, + ), + ), + ), + ), + Flexible( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + "${AppStrings.turn} ${battleProvider.turnCount}", + style: const TextStyle( + color: ThemeConfig.textColorWhite, + fontSize: ThemeConfig.fontSizeHeader, + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/battle/character_status_card.dart b/lib/widgets/battle/character_status_card.dart index f46c0d5..7349b20 100644 --- a/lib/widgets/battle/character_status_card.dart +++ b/lib/widgets/battle/character_status_card.dart @@ -1,11 +1,10 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import '../../game/model/entity.dart'; +import '../../game/models.dart'; import '../../game/enums.dart'; -import '../../providers/battle_provider.dart'; +import '../../providers.dart'; import 'battle_animation_widget.dart'; -import '../../game/config/theme_config.dart'; -import '../../game/config/animation_config.dart'; +import '../../game/config.dart'; class CharacterStatusCard extends StatelessWidget { final Character character; diff --git a/lib/widgets/battle/floating_battle_texts.dart b/lib/widgets/battle/floating_battle_texts.dart index 574ef03..afd9062 100644 --- a/lib/widgets/battle/floating_battle_texts.dart +++ b/lib/widgets/battle/floating_battle_texts.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; -import '../../game/config/theme_config.dart'; -import '../../game/config/animation_config.dart'; +import '../../game/config.dart'; class FloatingDamageText extends StatefulWidget { final String damage; diff --git a/lib/widgets/battle/risk_selection_dialog.dart b/lib/widgets/battle/risk_selection_dialog.dart new file mode 100644 index 0000000..c4dd609 --- /dev/null +++ b/lib/widgets/battle/risk_selection_dialog.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; +import '../../game/enums.dart'; +import '../../game/models.dart'; +import '../../game/config.dart'; + +class RiskSelectionDialog extends StatelessWidget { + final ActionType actionType; + final Character player; + final Function(RiskLevel) onSelected; + + const RiskSelectionDialog({ + super.key, + required this.actionType, + required this.player, + required this.onSelected, + }); + + @override + Widget build(BuildContext context) { + final baseValue = actionType == ActionType.attack + ? player.totalAtk + : player.totalDefense; + + 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"; + + double baseChance = 0.0; + switch (risk) { + case RiskLevel.safe: + baseChance = BattleConfig.safeBaseChance; + break; + case RiskLevel.normal: + baseChance = BattleConfig.normalBaseChance; + break; + case RiskLevel.risky: + baseChance = BattleConfig.riskyBaseChance; + break; + } + + double finalChance = baseChance + (player.totalLuck / 100.0); + if (finalChance > 1.0) finalChance = 1.0; + String successRate = "${(finalChance * 100).toInt()}%"; + + infoText = + "Success: $successRate, Eff: ${(efficiency * 100).toInt()}% ($expectedValue $valueUnit)"; + + return SimpleDialogOption( + onPressed: () => onSelected(risk), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + risk.name, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + Text(infoText, style: TextStyle(fontSize: 12, color: infoColor)), + ], + ), + ); + }).toList(), + ); + } +} diff --git a/lib/widgets/common.dart b/lib/widgets/common.dart new file mode 100644 index 0000000..f20a5b9 --- /dev/null +++ b/lib/widgets/common.dart @@ -0,0 +1 @@ +export 'common/item_card_widget.dart'; diff --git a/lib/widgets/common/item_card_widget.dart b/lib/widgets/common/item_card_widget.dart new file mode 100644 index 0000000..33eee6e --- /dev/null +++ b/lib/widgets/common/item_card_widget.dart @@ -0,0 +1,133 @@ +import 'package:flutter/material.dart'; +import '../../game/models.dart'; +import '../../game/enums.dart'; +import '../../utils.dart'; +import '../../game/config.dart'; + +class ItemCardWidget extends StatelessWidget { + final Item item; + final VoidCallback? onTap; + final bool showPrice; + final bool canBuy; + + const ItemCardWidget({ + super.key, + required this.item, + this.onTap, + this.showPrice = false, + this.canBuy = true, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Card( + color: ThemeConfig.shopItemCardBg, // Configurable if needed + shape: item.rarity != ItemRarity.magic + ? RoundedRectangleBorder( + side: BorderSide( + color: ItemUtils.getRarityColor(item.rarity), + width: 2.0, + ), + borderRadius: BorderRadius.circular(8.0), + ) + : null, + child: Padding( + padding: const EdgeInsets.all(4.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Image.asset( + ItemUtils.getIconPath(item.slot), + fit: BoxFit.contain, + ), + ), + Text( + item.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.bold, + color: ItemUtils.getRarityColor(item.rarity), + fontSize: 12, + ), + ), + // Show Item Stats (Fixed to show negative values) + FittedBox(fit: BoxFit.scaleDown, child: _buildItemStatText(item)), + if (showPrice) ...[ + const Spacer(), + Text( + "${item.price} G", + style: TextStyle( + color: canBuy + ? ThemeConfig.statGoldColor + : ThemeConfig.textColorGrey, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ], + ], + ), + ), + ), + ); + } + + Widget _buildItemStatText(Item item) { + List stats = []; + + // Helper to format stat string + String formatStat(int value, String label) { + String sign = value > 0 ? "+" : ""; // Negative values already have '-' + return "$sign$value $label"; + } + + if (item.atkBonus != 0) { + stats.add(formatStat(item.atkBonus, AppStrings.atk)); + } + if (item.hpBonus != 0) { + stats.add(formatStat(item.hpBonus, AppStrings.hp)); + } + if (item.armorBonus != 0) { + stats.add(formatStat(item.armorBonus, AppStrings.def)); + } + if (item.luck != 0) { + stats.add(formatStat(item.luck, AppStrings.luck)); + } + + List effectTexts = item.effects.map((e) => e.description).toList(); + + if (stats.isEmpty && effectTexts.isEmpty) return const SizedBox.shrink(); + + return Column( + children: [ + if (stats.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 2.0, bottom: 2.0), + child: Text( + stats.join(", "), + style: const TextStyle( + fontSize: ThemeConfig.fontSizeSmall, + color: ThemeConfig.statAtkColor, + ), + textAlign: TextAlign.center, + ), + ), + if (effectTexts.isNotEmpty) + Padding( + padding: const EdgeInsets.only(bottom: 2.0), + child: Text( + effectTexts.join("\n"), + style: const TextStyle( + fontSize: ThemeConfig.fontSizeTiny, + color: ThemeConfig.rarityLegendary, + ), + ), + ), + ], + ); + } +} diff --git a/lib/widgets/inventory.dart b/lib/widgets/inventory.dart new file mode 100644 index 0000000..6561688 --- /dev/null +++ b/lib/widgets/inventory.dart @@ -0,0 +1,2 @@ +export 'inventory/character_stats_widget.dart'; +export 'inventory/inventory_grid_widget.dart'; diff --git a/lib/widgets/inventory/character_stats_widget.dart b/lib/widgets/inventory/character_stats_widget.dart index acca58d..67fa5ca 100644 --- a/lib/widgets/inventory/character_stats_widget.dart +++ b/lib/widgets/inventory/character_stats_widget.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.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 CharacterStatsWidget extends StatelessWidget { const CharacterStatsWidget({super.key}); diff --git a/lib/widgets/inventory/inventory_grid_widget.dart b/lib/widgets/inventory/inventory_grid_widget.dart index a2deb3b..fc8b238 100644 --- a/lib/widgets/inventory/inventory_grid_widget.dart +++ b/lib/widgets/inventory/inventory_grid_widget.dart @@ -1,11 +1,10 @@ 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 '../../game/config.dart'; +import '../common/item_card_widget.dart'; class InventoryGridWidget extends StatelessWidget { const InventoryGridWidget({super.key}); @@ -49,66 +48,13 @@ class InventoryGridWidget extends StatelessWidget { onTap: () { _showItemActionDialog(context, battleProvider, item); }, - child: Card( - color: ThemeConfig.inventoryCardBg, - shape: item.rarity != ItemRarity.magic - ? RoundedRectangleBorder( - side: BorderSide( - color: ItemUtils.getRarityColor(item.rarity), - width: 2.0, - ), - borderRadius: BorderRadius.circular(4.0), - ) - : null, - child: Stack( - children: [ - Positioned( - left: 4, - top: 4, - child: Opacity( - opacity: 0.5, - child: Image.asset( - ItemUtils.getIconPath(item.slot), - width: 40, - height: 40, - fit: BoxFit.contain, - filterQuality: FilterQuality.high, - ), - ), - ), - Center( - child: Padding( - padding: const EdgeInsets.all(4.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - FittedBox( - fit: BoxFit.scaleDown, - child: Text( - item.name, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: ThemeConfig.fontSizeSmall, - fontWeight: - ThemeConfig.fontWeightBold, - color: ItemUtils.getRarityColor( - item.rarity, - ), - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - FittedBox( - fit: BoxFit.scaleDown, - child: _buildItemStatText(item), - ), - ], - ), - ), - ), - ], - ), + child: ItemCardWidget( + item: item, + // Inventory items usually don't show price unless in sell mode, + // but logic here implies standard view. + // If needed, we can toggle showPrice based on context. + showPrice: false, + canBuy: false, ), ); } else { @@ -381,44 +327,4 @@ class InventoryGridWidget extends StatelessWidget { ), ); } - - Widget _buildItemStatText(Item item) { - List stats = []; - if (item.atkBonus > 0) stats.add("+${item.atkBonus} ${AppStrings.atk}"); - if (item.hpBonus > 0) stats.add("+${item.hpBonus} ${AppStrings.hp}"); - if (item.armorBonus > 0) stats.add("+${item.armorBonus} ${AppStrings.def}"); - if (item.luck > 0) stats.add("+${item.luck} ${AppStrings.luck}"); - - List effectTexts = item.effects.map((e) => e.description).toList(); - - if (stats.isEmpty && effectTexts.isEmpty) return const SizedBox.shrink(); - - return Column( - children: [ - if (stats.isNotEmpty) - Padding( - padding: const EdgeInsets.only(top: 2.0, bottom: 2.0), - child: Text( - stats.join(", "), - style: const TextStyle( - fontSize: ThemeConfig.fontSizeSmall, - color: ThemeConfig.statAtkColor, - ), - textAlign: TextAlign.center, - ), - ), - if (effectTexts.isNotEmpty) - Padding( - padding: const EdgeInsets.only(bottom: 2.0), - child: Text( - effectTexts.join("\n"), - style: const TextStyle( - fontSize: ThemeConfig.fontSizeTiny, - color: ThemeConfig.rarityLegendary, - ), - ), - ), - ], - ); - } } diff --git a/lib/widgets/stage.dart b/lib/widgets/stage.dart new file mode 100644 index 0000000..7e9880e --- /dev/null +++ b/lib/widgets/stage.dart @@ -0,0 +1,2 @@ +export 'stage/rest_ui.dart'; +export 'stage/shop_ui.dart'; diff --git a/lib/widgets/stage/shop_ui.dart b/lib/widgets/stage/shop_ui.dart index 5618ede..ed4e8e9 100644 --- a/lib/widgets/stage/shop_ui.dart +++ b/lib/widgets/stage/shop_ui.dart @@ -3,12 +3,11 @@ import 'package:provider/provider.dart'; import '../../../providers/battle_provider.dart'; import '../../../providers/shop_provider.dart'; import '../../../game/model/item.dart'; -import '../../../utils/item_utils.dart'; -import '../../../game/enums.dart'; import '../../../game/config/theme_config.dart'; import '../../../game/config/game_config.dart'; import '../../../game/model/entity.dart'; import '../inventory/inventory_grid_widget.dart'; +import '../common/item_card_widget.dart'; class ShopUI extends StatelessWidget { final BattleProvider battleProvider; @@ -117,58 +116,10 @@ class ShopUI extends StatelessWidget { shopProvider, player, ), - child: Card( - color: ThemeConfig.shopItemCardBg, - shape: item.rarity != ItemRarity.magic - ? RoundedRectangleBorder( - side: BorderSide( - color: ItemUtils.getRarityColor( - item.rarity, - ), - width: 2.0, - ), - borderRadius: BorderRadius.circular( - 8.0, - ), - ) - : null, - child: Padding( - padding: const EdgeInsets.all(4.0), - child: Column( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Expanded( - child: Image.asset( - ItemUtils.getIconPath(item.slot), - fit: BoxFit.contain, - ), - ), - Text( - item.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontWeight: FontWeight.bold, - color: ItemUtils.getRarityColor( - item.rarity, - ), - fontSize: 12, - ), - ), - Text( - "${item.price} G", - style: TextStyle( - color: canBuy - ? ThemeConfig.statGoldColor - : ThemeConfig.textColorGrey, - fontWeight: FontWeight.bold, - fontSize: 12, - ), - ), - ], - ), - ), + child: ItemCardWidget( + item: item, + showPrice: true, + canBuy: canBuy, ), ); }, diff --git a/prompt/00_project_context_restore.md b/prompt/00_project_context_restore.md index 6d3fe2b..7048256 100644 --- a/prompt/00_project_context_restore.md +++ b/prompt/00_project_context_restore.md @@ -68,6 +68,13 @@ - **자동 저장:** 스테이지 클리어 시 `SaveManager`를 통해 자동 저장. - **Permadeath:** 패배 시 저장 데이터 삭제 (로그라이크 요소). +### F. 코드 구조 (Code Structure - Barrel Pattern) + +- **Barrel File Pattern:** `lib/` 내의 모든 주요 디렉토리는 해당 폴더의 파일들을 묶어주는 단일 진입점 파일(`.dart`)을 가집니다. + - `lib/game/models.dart`, `lib/game/config.dart`, `lib/game/data.dart`, `lib/game/logic.dart` + - `lib/providers.dart`, `lib/utils.dart`, `lib/screens.dart`, `lib/widgets.dart` +- **Imports:** 개별 파일 import 대신 위 Barrel File을 사용하여 가독성과 유지보수성을 높였습니다. + ## 3. 작업 컨벤션 (Working Conventions) - **Prompt Driven Development:** `prompt/XX_description.md` 유지. (유사 작업 통합 및 인덱스 정리 권장) @@ -75,6 +82,7 @@ - **Config Management:** 하드코딩되는 값들은 `config` 폴더 내 파일들(`lib/game/config/` 등)에서 통합 관리할 수 있도록 작성해야 합니다. - **State Management:** `Provider` (UI 상태) + `Stream` (이벤트성 데이터). - **Data:** JSON 기반 + `Table` 클래스로 로드. +- **Barrel File Pattern (Strict):** `lib/` 하위의 모든 주요 디렉토리는 Barrel File을 유지해야 하며, 외부에서 참조 시 **반드시** 이 Barrel File을 import 해야 합니다. 개별 파일에 대한 직접 import는 허용되지 않습니다. ## 4. 최근 주요 변경 사항 (Change Log) @@ -87,6 +95,7 @@ - **[Improvement] Visual Impact:** Risky 공격 및 높은 데미지 시 Floating Damage Text의 크기 확대. Floating Effect/Feedback Text의 위치 조정. - **[Refactor] Balancing System:** `BattleConfig`에서 공격/방어 효율 상수를 분리하고 `CombatCalculator` 및 관련 로직에 적용하여 밸런싱의 유연성 확보. - **[Fix] UI Stability:** `CharacterStatusCard`의 Intent UI 텍스트 길이에 따른 레이아웃 흔들림 방지 (`FittedBox`). `BattleScreen` 내 `Stack` 위젯 구성 문법 오류 수정. +- **[Refactor] Barrel Pattern Adoption:** 프로젝트 전체(`lib/` 하위)에 Barrel File 패턴을 적용하여 Import 구문을 통합하고 디렉토리 의존성을 명확하게 정리. ## 5. 다음 단계 (Next Steps)