refactoring : barrel pattern
This commit is contained in:
parent
ac4df02654
commit
bb3b34c9b1
|
|
@ -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';
|
||||
|
|
@ -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%
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -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;
|
||||
|
|
@ -147,7 +146,9 @@ class ItemTable {
|
|||
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);
|
||||
|
|
@ -160,10 +161,14 @@ class ItemTable {
|
|||
// 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)];
|
||||
|
|
@ -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)];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
export 'logic/battle_log_manager.dart';
|
||||
export 'logic/combat_calculator.dart';
|
||||
export 'logic/loot_generator.dart';
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
export 'model/damage_event.dart';
|
||||
export 'model/effect_event.dart';
|
||||
export 'model/entity.dart';
|
||||
export 'model/item.dart';
|
||||
export 'model/stage.dart';
|
||||
export 'model/stat.dart';
|
||||
export 'model/stat_modifier.dart';
|
||||
export 'model/status_effect.dart';
|
||||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
export 'providers/battle_provider.dart';
|
||||
export 'providers/settings_provider.dart';
|
||||
export 'providers/shop_provider.dart';
|
||||
|
|
@ -3,26 +3,17 @@ import 'dart:math';
|
|||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../game/model/entity.dart';
|
||||
import '../game/model/item.dart';
|
||||
import '../game/model/status_effect.dart';
|
||||
import '../game/model/stage.dart';
|
||||
import '../game/data/item_table.dart';
|
||||
import '../game/models.dart';
|
||||
import '../game/data.dart';
|
||||
|
||||
import '../game/data/enemy_table.dart';
|
||||
import '../game/data/player_table.dart';
|
||||
import '../utils/game_math.dart';
|
||||
import '../utils.dart';
|
||||
import '../game/enums.dart';
|
||||
import '../game/model/damage_event.dart'; // DamageEvent import
|
||||
import '../game/model/effect_event.dart'; // EffectEvent import
|
||||
|
||||
import '../game/save_manager.dart';
|
||||
import '../game/config/game_config.dart';
|
||||
import '../game/config/battle_config.dart'; // Import BattleConfig
|
||||
import 'shop_provider.dart'; // Import ShopProvider
|
||||
import '../game/config.dart';
|
||||
import 'shop_provider.dart';
|
||||
|
||||
import '../game/logic/battle_log_manager.dart';
|
||||
import '../game/logic/combat_calculator.dart';
|
||||
import '../game/logic.dart';
|
||||
|
||||
class EnemyIntent {
|
||||
final EnemyActionType type;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
export 'utils/game_math.dart';
|
||||
export 'utils/item_utils.dart';
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import '../game/enums.dart';
|
||||
import '../game/config/theme_config.dart';
|
||||
import '../game/config.dart';
|
||||
|
||||
class ItemUtils {
|
||||
static Color getRarityColor(ItemRarity rarity) {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -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';
|
||||
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export 'common/item_card_widget.dart';
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import '../../game/models.dart';
|
||||
import '../../game/enums.dart';
|
||||
import '../../utils.dart';
|
||||
import '../../game/config.dart';
|
||||
|
||||
class ItemCardWidget extends StatelessWidget {
|
||||
final Item item;
|
||||
final VoidCallback? onTap;
|
||||
final bool showPrice;
|
||||
final bool canBuy;
|
||||
|
||||
const ItemCardWidget({
|
||||
super.key,
|
||||
required this.item,
|
||||
this.onTap,
|
||||
this.showPrice = false,
|
||||
this.canBuy = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Card(
|
||||
color: ThemeConfig.shopItemCardBg, // Configurable if needed
|
||||
shape: item.rarity != ItemRarity.magic
|
||||
? RoundedRectangleBorder(
|
||||
side: BorderSide(
|
||||
color: ItemUtils.getRarityColor(item.rarity),
|
||||
width: 2.0,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
)
|
||||
: null,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Image.asset(
|
||||
ItemUtils.getIconPath(item.slot),
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
item.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: ItemUtils.getRarityColor(item.rarity),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
// Show Item Stats (Fixed to show negative values)
|
||||
FittedBox(fit: BoxFit.scaleDown, child: _buildItemStatText(item)),
|
||||
if (showPrice) ...[
|
||||
const Spacer(),
|
||||
Text(
|
||||
"${item.price} G",
|
||||
style: TextStyle(
|
||||
color: canBuy
|
||||
? ThemeConfig.statGoldColor
|
||||
: ThemeConfig.textColorGrey,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildItemStatText(Item item) {
|
||||
List<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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export 'inventory/character_stats_widget.dart';
|
||||
export 'inventory/inventory_grid_widget.dart';
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../providers/battle_provider.dart';
|
||||
import '../../game/config/theme_config.dart';
|
||||
import '../../game/config/app_strings.dart';
|
||||
import '../../providers.dart';
|
||||
import '../../game/config.dart';
|
||||
|
||||
class CharacterStatsWidget extends StatelessWidget {
|
||||
const CharacterStatsWidget({super.key});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
export 'stage/rest_ui.dart';
|
||||
export 'stage/shop_ui.dart';
|
||||
|
|
@ -3,12 +3,11 @@ import 'package:provider/provider.dart';
|
|||
import '../../../providers/battle_provider.dart';
|
||||
import '../../../providers/shop_provider.dart';
|
||||
import '../../../game/model/item.dart';
|
||||
import '../../../utils/item_utils.dart';
|
||||
import '../../../game/enums.dart';
|
||||
import '../../../game/config/theme_config.dart';
|
||||
import '../../../game/config/game_config.dart';
|
||||
import '../../../game/model/entity.dart';
|
||||
import '../inventory/inventory_grid_widget.dart';
|
||||
import '../common/item_card_widget.dart';
|
||||
|
||||
class ShopUI extends StatelessWidget {
|
||||
final BattleProvider battleProvider;
|
||||
|
|
@ -117,58 +116,10 @@ class ShopUI extends StatelessWidget {
|
|||
shopProvider,
|
||||
player,
|
||||
),
|
||||
child: Card(
|
||||
color: ThemeConfig.shopItemCardBg,
|
||||
shape: item.rarity != ItemRarity.magic
|
||||
? RoundedRectangleBorder(
|
||||
side: BorderSide(
|
||||
color: ItemUtils.getRarityColor(
|
||||
item.rarity,
|
||||
),
|
||||
width: 2.0,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(
|
||||
8.0,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: Column(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Image.asset(
|
||||
ItemUtils.getIconPath(item.slot),
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
item.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: ItemUtils.getRarityColor(
|
||||
item.rarity,
|
||||
),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"${item.price} G",
|
||||
style: TextStyle(
|
||||
color: canBuy
|
||||
? ThemeConfig.statGoldColor
|
||||
: ThemeConfig.textColorGrey,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: ItemCardWidget(
|
||||
item: item,
|
||||
showPrice: true,
|
||||
canBuy: canBuy,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue