refactoring : barrel pattern

This commit is contained in:
Horoli 2025-12-09 14:40:25 +09:00
parent ac4df02654
commit bb3b34c9b1
36 changed files with 708 additions and 497 deletions

6
lib/game/config.dart Normal file
View File

@ -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';

View File

@ -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%

View File

@ -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;

5
lib/game/data.dart Normal file
View File

@ -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';

View File

@ -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<ItemRarity, int> activeWeights = Map.from(weights ?? ItemConfig.defaultRarityWeights);
Map<ItemRarity, int> 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)];

3
lib/game/logic.dart Normal file
View File

@ -0,0 +1,3 @@
export 'logic/battle_log_manager.dart';
export 'logic/combat_calculator.dart';
export 'logic/loot_generator.dart';

8
lib/game/models.dart Normal file
View File

@ -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';

View File

@ -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<void> saveGame(BattleProvider provider) async {
final prefs = await SharedPreferences.getInstance();
final saveData = {
'stage': provider.stage,
'turnCount': provider.turnCount,

View File

@ -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<ShopProvider, BattleProvider>(
create: (context) => BattleProvider(

3
lib/providers.dart Normal file
View File

@ -0,0 +1,3 @@
export 'providers/battle_provider.dart';
export 'providers/settings_provider.dart';
export 'providers/shop_provider.dart';

View File

@ -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;

View File

@ -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<Item> 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;

6
lib/screens.dart Normal file
View File

@ -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';

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,
),
);
}
}

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(),

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});

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(),
),
);
},

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',
),
],
),
),
),
),
),
);
},
);
}
}

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});

2
lib/utils.dart Normal file
View File

@ -0,0 +1,2 @@
export 'utils/game_math.dart';
export 'utils/item_utils.dart';

View File

@ -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) {

5
lib/widgets.dart Normal file
View File

@ -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';

9
lib/widgets/battle.dart Normal file
View File

@ -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';

View File

@ -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,
),
],
);
}
}

View File

@ -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<BattleProvider>();
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,
),
),
),
),
],
),
);
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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(),
);
}
}

1
lib/widgets/common.dart Normal file
View File

@ -0,0 +1 @@
export 'common/item_card_widget.dart';

View File

@ -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<String> 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<String> 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,
),
),
),
],
);
}
}

View File

@ -0,0 +1,2 @@
export 'inventory/character_stats_widget.dart';
export 'inventory/inventory_grid_widget.dart';

View File

@ -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});

View File

@ -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<String> 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<String> 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,
),
),
),
],
);
}
}

2
lib/widgets/stage.dart Normal file
View File

@ -0,0 +1,2 @@
export 'stage/rest_ui.dart';
export 'stage/shop_ui.dart';

View File

@ -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,
),
);
},

View File

@ -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)