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 normalColor = Colors.white;
static const Color safeColor = Colors.grey; 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 defendRiskyColor = Colors.deepPurpleAccent;
static const Color defendNormalColor = Colors.blueAccent; static const Color defendNormalColor = Colors.blueAccent;
static const Color defendSafeColor = Colors.greenAccent; static const Color defendSafeColor = Colors.greenAccent;
// Sizes // 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 sizeNormal = 60.0;
static const double sizeSafe = 40.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 // Logic Constants
// Safe // Safe
static const double safeBaseChance = 1.0; // 100% static const double safeBaseChance = 1.0; // 100%

View File

@ -66,6 +66,16 @@ class ThemeConfig {
static const Color enemyIntentBorder = Colors.redAccent; static const Color enemyIntentBorder = Colors.redAccent;
static final Color? selectionCardBg = Colors.blueGrey[800]; static final Color? selectionCardBg = Colors.blueGrey[800];
static const Color selectionIconColor = Colors.blue; 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 // Feedback Colors
static const Color damageTextDefault = Colors.red; 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 'item_prefix_table.dart'; // Logic moved to LootGenerator
// import 'name_generator.dart'; // Logic moved to LootGenerator // import 'name_generator.dart'; // Logic moved to LootGenerator
import '../logic/loot_generator.dart'; // Import LootGenerator import '../logic/loot_generator.dart'; // Import LootGenerator
import '../../utils/game_math.dart';
class ItemTemplate { class ItemTemplate {
final String id; final String id;
@ -70,7 +69,7 @@ class ItemTemplate {
} }
Item createItem({int stage = 1}) { 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. // as scaling is now handled via Tier/Rarity in LootGenerator/Table logic.
return LootGenerator.generate(this); return LootGenerator.generate(this);
} }
@ -125,7 +124,7 @@ class ItemTable {
} }
/// Returns a random item based on Tier and Rarity weights. /// Returns a random item based on Tier and Rarity weights.
/// ///
/// [tier]: The tier of items to select from. /// [tier]: The tier of items to select from.
/// [slot]: Optional. If provided, only items of this slot are considered. /// [slot]: Optional. If provided, only items of this slot are considered.
/// [weights]: Optional map of rarity weights. Key: Rarity, Value: Weight. /// [weights]: Optional map of rarity weights. Key: Rarity, Value: Weight.
@ -143,11 +142,13 @@ class ItemTable {
if (slot != null) { if (slot != null) {
candidates = candidates.where((item) => item.slot == slot); candidates = candidates.where((item) => item.slot == slot);
} }
if (candidates.isEmpty) return null; if (candidates.isEmpty) return null;
// 2. Prepare Rarity Weights (Filtered by min/max) // 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) { if (minRarity != null) {
activeWeights.removeWhere((r, w) => r.index < minRarity.index); activeWeights.removeWhere((r, w) => r.index < minRarity.index);
@ -157,13 +158,17 @@ class ItemTable {
} }
if (activeWeights.isEmpty) { 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. // try to find ANY item within rarity range from candidates.
if (minRarity != null) { if (minRarity != null) {
candidates = candidates.where((item) => item.rarity.index >= minRarity.index); candidates = candidates.where(
(item) => item.rarity.index >= minRarity.index,
);
} }
if (maxRarity != null) { 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; if (candidates.isEmpty) return null;
return candidates.toList()[_random.nextInt(candidates.length)]; return candidates.toList()[_random.nextInt(candidates.length)];
@ -172,10 +177,10 @@ class ItemTable {
// 3. Determine Target Rarity based on filtered weights // 3. Determine Target Rarity based on filtered weights
int totalWeight = activeWeights.values.fold(0, (sum, w) => sum + w); int totalWeight = activeWeights.values.fold(0, (sum, w) => sum + w);
int roll = _random.nextInt(totalWeight); int roll = _random.nextInt(totalWeight);
ItemRarity? selectedRarity; ItemRarity? selectedRarity;
int currentSum = 0; int currentSum = 0;
for (var entry in activeWeights.entries) { for (var entry in activeWeights.entries) {
currentSum += entry.value; currentSum += entry.value;
if (roll < currentSum) { if (roll < currentSum) {
@ -185,15 +190,21 @@ class ItemTable {
} }
// 4. Filter candidates by Selected Rarity // 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) // 5. Fallback: If no items of selected rarity, use any item from the filtered candidates (respecting min/max)
if (rarityCandidates.isEmpty) { if (rarityCandidates.isEmpty) {
if (minRarity != null) { if (minRarity != null) {
candidates = candidates.where((item) => item.rarity.index >= minRarity.index); candidates = candidates.where(
(item) => item.rarity.index >= minRarity.index,
);
} }
if (maxRarity != null) { 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; if (candidates.isEmpty) return null;
return candidates.toList()[_random.nextInt(candidates.length)]; 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 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '../providers/battle_provider.dart'; import '../providers.dart';
import 'model/entity.dart'; import 'model/entity.dart';
import 'config/game_config.dart'; import 'config/game_config.dart';
@ -9,7 +9,7 @@ class SaveManager {
static Future<void> saveGame(BattleProvider provider) async { static Future<void> saveGame(BattleProvider provider) async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
final saveData = { final saveData = {
'stage': provider.stage, 'stage': provider.stage,
'turnCount': provider.turnCount, 'turnCount': provider.turnCount,

View File

@ -3,10 +3,8 @@ import 'package:provider/provider.dart';
import 'game/data/item_table.dart'; import 'game/data/item_table.dart';
import 'game/data/enemy_table.dart'; import 'game/data/enemy_table.dart';
import 'game/data/player_table.dart'; import 'game/data/player_table.dart';
import 'providers/battle_provider.dart'; import 'providers.dart';
import 'providers/shop_provider.dart'; // Import ShopProvider import 'screens.dart';
import 'providers/settings_provider.dart'; // Import SettingsProvider
import 'screens/main_menu_screen.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@ -23,7 +21,9 @@ class MyApp extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MultiProvider( return MultiProvider(
providers: [ providers: [
ChangeNotifierProvider(create: (_) => SettingsProvider()), // Register SettingsProvider ChangeNotifierProvider(
create: (_) => SettingsProvider(),
), // Register SettingsProvider
ChangeNotifierProvider(create: (_) => ShopProvider()), ChangeNotifierProvider(create: (_) => ShopProvider()),
ChangeNotifierProxyProvider<ShopProvider, BattleProvider>( ChangeNotifierProxyProvider<ShopProvider, BattleProvider>(
create: (context) => 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 'package:flutter/material.dart';
import '../game/model/entity.dart'; import '../game/models.dart';
import '../game/model/item.dart'; import '../game/data.dart';
import '../game/model/status_effect.dart';
import '../game/model/stage.dart';
import '../game/data/item_table.dart';
import '../game/data/enemy_table.dart'; import '../utils.dart';
import '../game/data/player_table.dart';
import '../utils/game_math.dart';
import '../game/enums.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/save_manager.dart';
import '../game/config/game_config.dart'; import '../game/config.dart';
import '../game/config/battle_config.dart'; // Import BattleConfig import 'shop_provider.dart';
import 'shop_provider.dart'; // Import ShopProvider
import '../game/logic/battle_log_manager.dart'; import '../game/logic.dart';
import '../game/logic/combat_calculator.dart';
class EnemyIntent { class EnemyIntent {
final EnemyActionType type; final EnemyActionType type;

View File

@ -1,10 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../game/model/item.dart'; import '../game/models.dart';
import '../game/model/entity.dart'; import '../game/data.dart';
import '../game/data/item_table.dart';
import '../game/enums.dart'; import '../game/enums.dart';
import '../game/config/game_config.dart'; import '../game/config.dart';
import '../utils/game_math.dart';
class ShopProvider with ChangeNotifier { class ShopProvider with ChangeNotifier {
List<Item> availableItems = []; List<Item> availableItems = [];
@ -25,7 +23,8 @@ class ShopProvider with ChangeNotifier {
currentTier = ItemTier.tier2; currentTier = ItemTier.tier2;
availableItems = []; 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); ItemTemplate? template = ItemTable.getRandomItem(tier: currentTier);
if (template != null) { if (template != null) {
availableItems.add(template.createItem(stage: stage)); availableItems.add(template.createItem(stage: stage));
@ -38,7 +37,9 @@ class ShopProvider with ChangeNotifier {
const int rerollCost = GameConfig.shopRerollCost; const int rerollCost = GameConfig.shopRerollCost;
if (player.gold >= rerollCost) { if (player.gold >= rerollCost) {
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."; _lastShopMessage = "Shop items rerolled for $rerollCost G.";
notifyListeners(); notifyListeners();
return true; 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:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../providers/battle_provider.dart'; import '../providers.dart';
import '../game/enums.dart'; import '../game/enums.dart';
import '../game/model/item.dart'; import '../game/models.dart';
import '../game/model/damage_event.dart';
import '../game/model/effect_event.dart';
import 'dart:async'; import 'dart:async';
import '../widgets/responsive_container.dart'; import '../widgets.dart';
import '../utils/item_utils.dart'; import '../utils.dart';
import '../widgets/battle/character_status_card.dart';
import '../widgets/battle/battle_log_overlay.dart';
import '../widgets/battle/floating_battle_texts.dart';
import '../widgets/stage/shop_ui.dart';
import '../widgets/stage/rest_ui.dart';
import '../widgets/battle/shake_widget.dart';
import '../widgets/battle/battle_animation_widget.dart';
import '../widgets/battle/explosion_widget.dart';
import 'main_menu_screen.dart'; import 'main_menu_screen.dart';
import '../game/config/battle_config.dart'; import '../game/config.dart';
import '../game/config/theme_config.dart';
import '../game/config/app_strings.dart';
import '../providers/settings_provider.dart'; // Import SettingsProvider
class BattleScreen extends StatefulWidget { class BattleScreen extends StatefulWidget {
const BattleScreen({super.key}); const BattleScreen({super.key});
@ -97,7 +84,12 @@ class _BattleScreenState extends State<BattleScreen> {
position = position - stackOffset; 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(); 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. // Let's just wrap `FloatingDamageText` in a `Transform.scale` with a value.
// I'll define a variable scale. // I'll define a variable scale.
double scale = 1.0; double scale = BattleConfig.damageScaleNormal;
// Heuristic: If damage is high (e.g. > 15), assume it might be risky/crit if (event.damage > BattleConfig.highDamageThreshold) {
if (event.damage > 15) scale = 3; scale = BattleConfig.damageScaleHigh;
}
setState(() { setState(() {
_floatingDamageTexts.add( _floatingDamageTexts.add(
@ -195,7 +188,8 @@ class _BattleScreenState extends State<BattleScreen> {
// "[UI Debug] Feedback Event: ${event.id}, Type: ${event.feedbackType}", // "[UI Debug] Feedback Event: ${event.id}, Type: ${event.feedbackType}",
// ); // );
if (_lastFeedbackTime != null && if (_lastFeedbackTime != null &&
DateTime.now().difference(_lastFeedbackTime!).inMilliseconds < 300) { DateTime.now().difference(_lastFeedbackTime!).inMilliseconds <
BattleConfig.feedbackCooldownMs) {
return; // Skip if too soon return; // Skip if too soon
} }
_lastFeedbackTime = DateTime.now(); _lastFeedbackTime = DateTime.now();
@ -227,12 +221,12 @@ class _BattleScreenState extends State<BattleScreen> {
if (event.target == EffectTarget.enemy) { if (event.target == EffectTarget.enemy) {
// Enemy is top-right, so effect should be left-bottom of its card // Enemy is top-right, so effect should be left-bottom of its card
offsetX = renderBox.size.width * 0.1; // 20% from left edge offsetX = renderBox.size.width * BattleConfig.effectEnemyOffsetX;
offsetY = renderBox.size.height * 0.8; // 80% from top edge offsetY = renderBox.size.height * BattleConfig.effectEnemyOffsetY;
} else { } else {
// Player is bottom-left, so effect should be right-top of its card // Player is bottom-left, so effect should be right-top of its card
offsetX = renderBox.size.width * 0.8; // 80% from left edge offsetX = renderBox.size.width * BattleConfig.effectPlayerOffsetX;
offsetY = renderBox.size.height * 0.2; // 20% from top edge offsetY = renderBox.size.height * BattleConfig.effectPlayerOffsetY;
} }
position = position + Offset(offsetX, offsetY); position = position + Offset(offsetX, offsetY);
@ -488,17 +482,39 @@ class _BattleScreenState extends State<BattleScreen> {
} }
void _showRiskLevelSelection(BuildContext context, ActionType actionType) { 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 battleProvider = context.read<BattleProvider>();
final player = battleProvider.player; 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 // Check turn to reset flag
if (battleProvider.turnCount != _lastTurnCount) { if (battleProvider.turnCount != _lastTurnCount) {
_lastTurnCount = battleProvider.turnCount; _lastTurnCount = battleProvider.turnCount;
_hasShownEnemyDefense = false; _hasShownEnemyDefense = false;
} }
// Interactive Enemy Defense Trigger
// If enemy intends to defend, trigger animation NOW (when user interacts)
final enemyIntent = battleProvider.currentEnemyIntent; final enemyIntent = battleProvider.currentEnemyIntent;
if (enemyIntent != null && if (enemyIntent != null &&
enemyIntent.type == EnemyActionType.defend && enemyIntent.type == EnemyActionType.defend &&
@ -538,102 +554,10 @@ class _BattleScreenState extends State<BattleScreen> {
.then((_) { .then((_) {
if (mounted) setState(() => _isEnemyAttacking = false); if (mounted) setState(() => _isEnemyAttacking = false);
}); });
return true;
} }
return false;
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(),
);
},
);
} }
@override @override
@ -664,39 +588,7 @@ class _BattleScreenState extends State<BattleScreen> {
Column( Column(
children: [ children: [
// Top Bar // Top Bar
Padding( const BattleHeader(),
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,
),
),
),
),
],
),
),
// Battle Area (Characters) - Expanded to fill available space // Battle Area (Characters) - Expanded to fill available space
Expanded( Expanded(
@ -742,43 +634,33 @@ class _BattleScreenState extends State<BattleScreen> {
top: 60, top: 60,
left: 16, left: 16,
right: 16, right: 16,
height: 150, height: BattleConfig.logsOverlayHeight,
child: BattleLogOverlay(logs: battleProvider.logs), child: BattleLogOverlay(logs: battleProvider.logs),
), ),
// 4. Floating Action Buttons (Bottom Right) // 4. Battle Controls (Bottom Right)
Positioned( Positioned(
bottom: 20, bottom: 20,
right: 20, right: 20,
child: Column( child: BattleControls(
mainAxisSize: MainAxisSize.min, isAttackEnabled:
children: [
_buildFloatingActionButton(
context,
"ATK",
ThemeConfig.btnActionActive,
ActionType.attack,
battleProvider.isPlayerTurn && battleProvider.isPlayerTurn &&
!battleProvider.player.isDead && !battleProvider.player.isDead &&
!battleProvider.enemy.isDead && !battleProvider.enemy.isDead &&
!battleProvider.showRewardPopup && !battleProvider.showRewardPopup &&
!_isPlayerAttacking && !_isPlayerAttacking &&
!_isEnemyAttacking, !_isEnemyAttacking,
), isDefendEnabled:
const SizedBox(height: 16),
_buildFloatingActionButton(
context,
"DEF",
ThemeConfig.btnDefendActive,
ActionType.defend,
battleProvider.isPlayerTurn && battleProvider.isPlayerTurn &&
!battleProvider.player.isDead && !battleProvider.player.isDead &&
!battleProvider.enemy.isDead && !battleProvider.enemy.isDead &&
!battleProvider.showRewardPopup && !battleProvider.showRewardPopup &&
!_isPlayerAttacking && !_isPlayerAttacking &&
!_isEnemyAttacking, !_isEnemyAttacking,
), onAttackPressed: () =>
], _showRiskLevelSelection(context, ActionType.attack),
onDefendPressed: () =>
_showRiskLevelSelection(context, ActionType.defend),
), ),
), ),
@ -789,7 +671,7 @@ class _BattleScreenState extends State<BattleScreen> {
child: FloatingActionButton( child: FloatingActionButton(
heroTag: "logToggle", heroTag: "logToggle",
mini: true, mini: true,
backgroundColor: Colors.grey[800], backgroundColor: ThemeConfig.toggleBtnBg,
onPressed: () { onPressed: () {
setState(() { setState(() {
_showLogs = !_showLogs; _showLogs = !_showLogs;
@ -820,7 +702,7 @@ class _BattleScreenState extends State<BattleScreen> {
Icon( Icon(
Icons.monetization_on, Icons.monetization_on,
color: ThemeConfig.statGoldColor, color: ThemeConfig.statGoldColor,
size: 18, size: ThemeConfig.itemIconSizeSmall,
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
@ -846,7 +728,8 @@ class _BattleScreenState extends State<BattleScreen> {
content: Text( content: Text(
"${AppStrings.inventoryFull} Cannot take item.", "${AppStrings.inventoryFull} Cannot take item.",
), ),
backgroundColor: Colors.red, backgroundColor:
ThemeConfig.snackBarErrorBg,
), ),
); );
} }
@ -860,7 +743,7 @@ class _BattleScreenState extends State<BattleScreen> {
Container( Container(
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.blueGrey[700], color: ThemeConfig.rewardItemBg,
borderRadius: BorderRadius.circular( borderRadius: BorderRadius.circular(
4, 4,
), ),
@ -875,8 +758,9 @@ class _BattleScreenState extends State<BattleScreen> {
), ),
child: Image.asset( child: Image.asset(
ItemUtils.getIconPath(item.slot), ItemUtils.getIconPath(item.slot),
width: 24, width: ThemeConfig.itemIconSizeMedium,
height: 24, height:
ThemeConfig.itemIconSizeMedium,
fit: BoxFit.contain, fit: BoxFit.contain,
filterQuality: FilterQuality.high, filterQuality: FilterQuality.high,
), ),
@ -934,16 +818,16 @@ class _BattleScreenState extends State<BattleScreen> {
color: ThemeConfig.statHpColor, color: ThemeConfig.statHpColor,
fontSize: ThemeConfig.fontSizeHuge, fontSize: ThemeConfig.fontSizeHuge,
fontWeight: ThemeConfig.fontWeightBold, fontWeight: ThemeConfig.fontWeightBold,
letterSpacing: 4.0, letterSpacing: ThemeConfig.letterSpacingHeader,
), ),
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey[800], backgroundColor: ThemeConfig.menuButtonBg,
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 32, horizontal: ThemeConfig.paddingBtnHorizontal,
vertical: 16, vertical: ThemeConfig.paddingBtnVertical,
), ),
), ),
onPressed: () { 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:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../providers/battle_provider.dart'; import '../providers.dart';
import '../game/data/player_table.dart'; import '../game/data.dart';
import 'main_wrapper.dart'; import 'main_wrapper.dart';
import '../widgets/responsive_container.dart'; import '../widgets.dart';
import '../game/config/theme_config.dart'; import '../game/config.dart';
import '../game/config/app_strings.dart';
class CharacterSelectionScreen extends StatelessWidget { class CharacterSelectionScreen extends StatelessWidget {
const CharacterSelectionScreen({super.key}); const CharacterSelectionScreen({super.key});
@ -75,7 +74,9 @@ class CharacterSelectionScreen extends StatelessWidget {
Text( Text(
warrior.description, warrior.description,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle(color: ThemeConfig.textColorGrey), style: const TextStyle(
color: ThemeConfig.textColorGrey,
),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
const Divider(), const Divider(),

View File

@ -1,13 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../providers/battle_provider.dart'; import '../providers.dart';
import '../game/model/item.dart'; import '../game/models.dart';
import '../game/enums.dart'; import '../game/enums.dart';
import '../utils/item_utils.dart'; import '../utils.dart';
import '../game/config/theme_config.dart'; import '../game/config.dart';
import '../game/config/app_strings.dart'; import '../widgets.dart';
import '../widgets/inventory/character_stats_widget.dart';
import '../widgets/inventory/inventory_grid_widget.dart';
class InventoryScreen extends StatelessWidget { class InventoryScreen extends StatelessWidget {
const InventoryScreen({super.key}); const InventoryScreen({super.key});

View File

@ -2,11 +2,10 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'character_selection_screen.dart'; import 'character_selection_screen.dart';
import 'main_wrapper.dart'; import 'main_wrapper.dart';
import '../widgets/responsive_container.dart'; import '../widgets.dart';
import '../game/save_manager.dart'; import '../game/save_manager.dart';
import '../providers/battle_provider.dart'; import '../providers.dart';
import '../game/config/theme_config.dart'; import '../game/config.dart';
import '../game/config/app_strings.dart';
class MainMenuScreen extends StatefulWidget { class MainMenuScreen extends StatefulWidget {
const MainMenuScreen({super.key}); const MainMenuScreen({super.key});
@ -61,10 +60,7 @@ class _MainMenuScreenState extends State<MainMenuScreen> {
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topCenter, begin: Alignment.topCenter,
end: Alignment.bottomCenter, end: Alignment.bottomCenter,
colors: [ colors: [ThemeConfig.mainMenuBgTop, ThemeConfig.mainMenuBgBottom],
ThemeConfig.mainMenuBgTop,
ThemeConfig.mainMenuBgBottom,
],
), ),
), ),
child: ResponsiveContainer( child: ResponsiveContainer(
@ -121,10 +117,11 @@ class _MainMenuScreenState extends State<MainMenuScreen> {
onPressed: () { onPressed: () {
// Warn if save exists? Or just overwrite on save. // Warn if save exists? Or just overwrite on save.
// For now, simpler flow. // For now, simpler flow.
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const CharacterSelectionScreen(), builder: (context) =>
const CharacterSelectionScreen(),
), ),
); );
}, },

View File

@ -1,9 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers.dart';
import '../game/enums.dart';
import 'battle_screen.dart'; import 'battle_screen.dart';
import 'inventory_screen.dart'; import 'inventory_screen.dart';
import 'settings_screen.dart'; import 'settings_screen.dart';
import '../widgets/responsive_container.dart'; import '../widgets.dart';
import '../game/config/theme_config.dart'; import '../game/config.dart';
class MainWrapper extends StatefulWidget { class MainWrapper extends StatefulWidget {
const MainWrapper({super.key}); const MainWrapper({super.key});
@ -23,37 +26,82 @@ class _MainWrapperState extends State<MainWrapper> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Consumer<BattleProvider>(
backgroundColor: ThemeConfig.mainMenuBgTop, // Outer background for web builder: (context, battleProvider, child) {
body: Center( // Determine the first tab's icon and label based on StageType
child: ResponsiveContainer( String stageLabel = "Battle";
child: Scaffold( IconData stageIcon = Icons.flash_on;
body: IndexedStack(index: _currentIndex, children: _screens),
bottomNavigationBar: BottomNavigationBar( // Ensure we check null safety if currentStage isn't ready (though it should be)
currentIndex: _currentIndex, // battleProvider.currentStage might be accessed safely if standardized
onTap: (index) { // Assuming battleProvider.currentStage is accessible or we check stage type logic
setState(() { try {
_currentIndex = index; final stageType = battleProvider.currentStage.type;
}); switch (stageType) {
}, case StageType.battle:
items: const [ case StageType.elite:
BottomNavigationBarItem( stageLabel = "Battle";
icon: Icon(Icons.flash_on), stageIcon = Icons.flash_on;
label: 'Battle', 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:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../providers/battle_provider.dart'; import '../providers.dart';
import '../providers/settings_provider.dart'; // Import SettingsProvider
import 'main_menu_screen.dart'; import 'main_menu_screen.dart';
import '../game/config/theme_config.dart'; import '../game/config.dart';
import '../game/config/app_strings.dart';
class SettingsScreen extends StatelessWidget { class SettingsScreen extends StatelessWidget {
const SettingsScreen({super.key}); 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 'package:flutter/material.dart';
import '../game/enums.dart'; import '../game/enums.dart';
import '../game/config/theme_config.dart'; import '../game/config.dart';
class ItemUtils { class ItemUtils {
static Color getRarityColor(ItemRarity rarity) { 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:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../game/model/entity.dart'; import '../../game/models.dart';
import '../../game/enums.dart'; import '../../game/enums.dart';
import '../../providers/battle_provider.dart'; import '../../providers.dart';
import 'battle_animation_widget.dart'; import 'battle_animation_widget.dart';
import '../../game/config/theme_config.dart'; import '../../game/config.dart';
import '../../game/config/animation_config.dart';
class CharacterStatusCard extends StatelessWidget { class CharacterStatusCard extends StatelessWidget {
final Character character; final Character character;

View File

@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../game/config/theme_config.dart'; import '../../game/config.dart';
import '../../game/config/animation_config.dart';
class FloatingDamageText extends StatefulWidget { class FloatingDamageText extends StatefulWidget {
final String damage; 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:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../providers/battle_provider.dart'; import '../../providers.dart';
import '../../game/config/theme_config.dart'; import '../../game/config.dart';
import '../../game/config/app_strings.dart';
class CharacterStatsWidget extends StatelessWidget { class CharacterStatsWidget extends StatelessWidget {
const CharacterStatsWidget({super.key}); const CharacterStatsWidget({super.key});

View File

@ -1,11 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../providers/battle_provider.dart'; import '../../providers.dart';
import '../../game/model/item.dart'; import '../../game/models.dart';
import '../../game/enums.dart'; import '../../game/enums.dart';
import '../../utils/item_utils.dart'; import '../../game/config.dart';
import '../../game/config/theme_config.dart'; import '../common/item_card_widget.dart';
import '../../game/config/app_strings.dart';
class InventoryGridWidget extends StatelessWidget { class InventoryGridWidget extends StatelessWidget {
const InventoryGridWidget({super.key}); const InventoryGridWidget({super.key});
@ -49,66 +48,13 @@ class InventoryGridWidget extends StatelessWidget {
onTap: () { onTap: () {
_showItemActionDialog(context, battleProvider, item); _showItemActionDialog(context, battleProvider, item);
}, },
child: Card( child: ItemCardWidget(
color: ThemeConfig.inventoryCardBg, item: item,
shape: item.rarity != ItemRarity.magic // Inventory items usually don't show price unless in sell mode,
? RoundedRectangleBorder( // but logic here implies standard view.
side: BorderSide( // If needed, we can toggle showPrice based on context.
color: ItemUtils.getRarityColor(item.rarity), showPrice: false,
width: 2.0, canBuy: false,
),
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),
),
],
),
),
),
],
),
), ),
); );
} else { } 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/battle_provider.dart';
import '../../../providers/shop_provider.dart'; import '../../../providers/shop_provider.dart';
import '../../../game/model/item.dart'; import '../../../game/model/item.dart';
import '../../../utils/item_utils.dart';
import '../../../game/enums.dart';
import '../../../game/config/theme_config.dart'; import '../../../game/config/theme_config.dart';
import '../../../game/config/game_config.dart'; import '../../../game/config/game_config.dart';
import '../../../game/model/entity.dart'; import '../../../game/model/entity.dart';
import '../inventory/inventory_grid_widget.dart'; import '../inventory/inventory_grid_widget.dart';
import '../common/item_card_widget.dart';
class ShopUI extends StatelessWidget { class ShopUI extends StatelessWidget {
final BattleProvider battleProvider; final BattleProvider battleProvider;
@ -117,58 +116,10 @@ class ShopUI extends StatelessWidget {
shopProvider, shopProvider,
player, player,
), ),
child: Card( child: ItemCardWidget(
color: ThemeConfig.shopItemCardBg, item: item,
shape: item.rarity != ItemRarity.magic showPrice: true,
? RoundedRectangleBorder( canBuy: canBuy,
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,
),
),
],
),
),
), ),
); );
}, },

View File

@ -68,6 +68,13 @@
- **자동 저장:** 스테이지 클리어 시 `SaveManager`를 통해 자동 저장. - **자동 저장:** 스테이지 클리어 시 `SaveManager`를 통해 자동 저장.
- **Permadeath:** 패배 시 저장 데이터 삭제 (로그라이크 요소). - **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) ## 3. 작업 컨벤션 (Working Conventions)
- **Prompt Driven Development:** `prompt/XX_description.md` 유지. (유사 작업 통합 및 인덱스 정리 권장) - **Prompt Driven Development:** `prompt/XX_description.md` 유지. (유사 작업 통합 및 인덱스 정리 권장)
@ -75,6 +82,7 @@
- **Config Management:** 하드코딩되는 값들은 `config` 폴더 내 파일들(`lib/game/config/` 등)에서 통합 관리할 수 있도록 작성해야 합니다. - **Config Management:** 하드코딩되는 값들은 `config` 폴더 내 파일들(`lib/game/config/` 등)에서 통합 관리할 수 있도록 작성해야 합니다.
- **State Management:** `Provider` (UI 상태) + `Stream` (이벤트성 데이터). - **State Management:** `Provider` (UI 상태) + `Stream` (이벤트성 데이터).
- **Data:** JSON 기반 + `Table` 클래스로 로드. - **Data:** JSON 기반 + `Table` 클래스로 로드.
- **Barrel File Pattern (Strict):** `lib/` 하위의 모든 주요 디렉토리는 Barrel File을 유지해야 하며, 외부에서 참조 시 **반드시** 이 Barrel File을 import 해야 합니다. 개별 파일에 대한 직접 import는 허용되지 않습니다.
## 4. 최근 주요 변경 사항 (Change Log) ## 4. 최근 주요 변경 사항 (Change Log)
@ -87,6 +95,7 @@
- **[Improvement] Visual Impact:** Risky 공격 및 높은 데미지 시 Floating Damage Text의 크기 확대. Floating Effect/Feedback Text의 위치 조정. - **[Improvement] Visual Impact:** Risky 공격 및 높은 데미지 시 Floating Damage Text의 크기 확대. Floating Effect/Feedback Text의 위치 조정.
- **[Refactor] Balancing System:** `BattleConfig`에서 공격/방어 효율 상수를 분리하고 `CombatCalculator` 및 관련 로직에 적용하여 밸런싱의 유연성 확보. - **[Refactor] Balancing System:** `BattleConfig`에서 공격/방어 효율 상수를 분리하고 `CombatCalculator` 및 관련 로직에 적용하여 밸런싱의 유연성 확보.
- **[Fix] UI Stability:** `CharacterStatusCard`의 Intent UI 텍스트 길이에 따른 레이아웃 흔들림 방지 (`FittedBox`). `BattleScreen``Stack` 위젯 구성 문법 오류 수정. - **[Fix] UI Stability:** `CharacterStatusCard`의 Intent UI 텍스트 길이에 따른 레이아웃 흔들림 방지 (`FittedBox`). `BattleScreen``Stack` 위젯 구성 문법 오류 수정.
- **[Refactor] Barrel Pattern Adoption:** 프로젝트 전체(`lib/` 하위)에 Barrel File 패턴을 적용하여 Import 구문을 통합하고 디렉토리 의존성을 명확하게 정리.
## 5. 다음 단계 (Next Steps) ## 5. 다음 단계 (Next Steps)