update : icon image

This commit is contained in:
2025-12-07 16:48:03 +09:00
parent 8771f2c1af
commit d5609aff0f
21 changed files with 684 additions and 394 deletions
+11 -1
View File
@@ -4,6 +4,7 @@ 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 'screens/main_menu_screen.dart';
void main() async {
@@ -20,7 +21,16 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [ChangeNotifierProvider(create: (_) => BattleProvider())],
providers: [
ChangeNotifierProvider(create: (_) => ShopProvider()),
ChangeNotifierProxyProvider<ShopProvider, BattleProvider>(
create: (context) => BattleProvider(
shopProvider: Provider.of<ShopProvider>(context, listen: false),
),
update: (context, shopProvider, battleProvider) =>
battleProvider ?? BattleProvider(shopProvider: shopProvider),
),
],
child: MaterialApp(
title: "Colosseum's Choice",
theme: ThemeData.dark(),
+20 -54
View File
@@ -2,6 +2,7 @@ import 'dart:async'; // StreamController 사용을 위해 import
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; // For context.read in _prepareNextStage
import '../game/model/entity.dart';
import '../game/model/item.dart';
import '../game/model/status_effect.dart';
@@ -17,6 +18,7 @@ import '../game/model/effect_event.dart'; // EffectEvent import
import '../game/save_manager.dart';
import '../game/config/game_config.dart';
import 'shop_provider.dart'; // Import ShopProvider
class EnemyIntent {
final EnemyActionType type;
@@ -63,7 +65,10 @@ class BattleProvider with ChangeNotifier {
final _effectEventController = StreamController<EffectEvent>.broadcast();
Stream<EffectEvent> get effectStream => _effectEventController.stream;
BattleProvider() {
// Dependency injection
final ShopProvider shopProvider;
BattleProvider({required this.shopProvider}) {
// initializeBattle(); // Do not auto-start logic
}
@@ -231,8 +236,9 @@ class BattleProvider with ChangeNotifier {
_addLog("Stage $stage ($type) started! A wild ${enemy.name} appeared.");
} else if (type == StageType.shop) {
// Generate random items for shop
shopItems = _generateShopItems();
// Generate random items for shop using ShopProvider
shopProvider.generateShopItems(stage);
shopItems = shopProvider.availableItems;
// Dummy enemy to prevent null errors in existing UI (until UI is fully updated)
enemy = Character(name: "Merchant", maxHp: 9999, armor: 0, atk: 0);
@@ -247,60 +253,13 @@ class BattleProvider with ChangeNotifier {
currentStage = StageModel(
type: type,
enemy: newEnemy,
shopItems: shopItems,
shopItems: shopItems, // Pass items from ShopProvider
);
turnCount = 1;
notifyListeners();
}
/// Generate 4 random items for the shop based on current stage tier
List<Item> _generateShopItems() {
ItemTier currentTier = ItemTier.tier1;
if (stage > GameConfig.tier2StageMax)
currentTier = ItemTier.tier3;
else if (stage > GameConfig.tier1StageMax)
currentTier = ItemTier.tier2;
List<Item> items = [];
for (int i = 0; i < 4; i++) {
ItemTemplate? template = ItemTable.getRandomItem(tier: currentTier);
if (template != null) {
items.add(template.createItem(stage: stage));
}
}
return items;
}
void rerollShopItems() {
const int rerollCost = GameConfig.shopRerollCost;
if (player.gold >= rerollCost) {
player.gold -= rerollCost;
// Modify the existing list because shopItems is final
currentStage.shopItems.clear();
currentStage.shopItems.addAll(_generateShopItems());
_addLog("Shop items rerolled for $rerollCost G.");
notifyListeners();
} else {
_addLog("Not enough gold to reroll!");
}
}
void buyItem(Item item) {
if (player.gold >= item.price) {
bool added = player.addToInventory(item);
if (added) {
player.gold -= item.price;
currentStage.shopItems.remove(item); // Remove from shop
_addLog("Bought ${item.name} for ${item.price} G.");
} else {
_addLog("Inventory is full!");
}
notifyListeners();
} else {
_addLog("Not enough gold!");
}
}
// Shop-related methods are now handled by ShopProvider
// Replaces _spawnEnemy
// void _spawnEnemy() { ... } - Removed
@@ -737,18 +696,25 @@ class BattleProvider with ChangeNotifier {
notifyListeners();
}
void selectReward(Item item) {
bool selectReward(Item item) {
if (item.id == "reward_skip") {
_addLog("Skipped reward.");
_completeStage();
return true;
} else {
bool added = player.addToInventory(item);
if (added) {
_addLog("Added ${item.name} to inventory.");
_completeStage();
return true;
} else {
_addLog("Inventory is full! ${item.name} discarded.");
_addLog("Inventory is full! Could not take ${item.name}.");
return false;
}
}
}
void _completeStage() {
// Heal player after selecting reward
int healAmount = GameMath.floor(player.totalMaxHp * GameConfig.stageHealRatio);
player.heal(healAmount);
+72
View File
@@ -0,0 +1,72 @@
import 'package:flutter/material.dart';
import '../game/model/item.dart';
import '../game/model/entity.dart';
import '../game/data/item_table.dart';
import '../game/enums.dart';
import '../game/config/game_config.dart';
import '../utils/game_math.dart';
class ShopProvider with ChangeNotifier {
List<Item> availableItems = [];
String _lastShopMessage = '';
String get lastShopMessage => _lastShopMessage;
void clearMessage() {
_lastShopMessage = '';
notifyListeners();
}
void generateShopItems(int stage) {
ItemTier currentTier = ItemTier.tier1;
if (stage > GameConfig.tier2StageMax)
currentTier = ItemTier.tier3;
else if (stage > GameConfig.tier1StageMax)
currentTier = ItemTier.tier2;
availableItems = [];
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));
}
}
notifyListeners();
}
bool rerollShopItems(Character player, int currentStageNumber) {
const int rerollCost = GameConfig.shopRerollCost;
if (player.gold >= rerollCost) {
player.gold -= rerollCost;
generateShopItems(currentStageNumber); // Regenerate based on current stage
_lastShopMessage = "Shop items rerolled for $rerollCost G.";
notifyListeners();
return true;
} else {
_lastShopMessage = "Not enough gold to reroll!";
notifyListeners();
return false;
}
}
bool buyItem(Item item, Character player) {
if (player.gold >= item.price) {
if (player.inventory.length < player.maxInventorySize) {
player.gold -= item.price;
player.addToInventory(item);
availableItems.remove(item); // Remove from shop
_lastShopMessage = "Bought ${item.name} for ${item.price} G.";
notifyListeners();
return true;
} else {
_lastShopMessage = "Inventory is full! Cannot buy ${item.name}.";
notifyListeners();
return false;
}
} else {
_lastShopMessage = "Not enough gold!";
notifyListeners();
return false;
}
}
}
+32 -10
View File
@@ -13,7 +13,8 @@ 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/battle/stage_ui.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';
@@ -489,7 +490,6 @@ class _BattleScreenState extends State<BattleScreen> {
_buildFloatingActionButton(
context,
"ATK",
Icons.whatshot,
ThemeConfig.btnActionActive,
ActionType.attack,
battleProvider.isPlayerTurn &&
@@ -501,7 +501,6 @@ class _BattleScreenState extends State<BattleScreen> {
_buildFloatingActionButton(
context,
"DEF",
Icons.shield,
ThemeConfig.btnDefendActive,
ActionType.defend,
battleProvider.isPlayerTurn &&
@@ -564,7 +563,17 @@ class _BattleScreenState extends State<BattleScreen> {
bool isSkip = item.id == "reward_skip";
return SimpleDialogOption(
onPressed: () {
battleProvider.selectReward(item);
bool success = battleProvider.selectReward(item);
if (!success) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
"Inventory is full! Cannot take item.",
),
backgroundColor: Colors.red,
),
);
}
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -587,10 +596,11 @@ class _BattleScreenState extends State<BattleScreen> {
: ThemeConfig.rarityCommon,
),
),
child: Icon(
ItemUtils.getIcon(item.slot),
color: ItemUtils.getColor(item.slot),
size: 24,
child: Image.asset(
ItemUtils.getIconPath(item.slot),
width: 24,
height: 24,
fit: BoxFit.contain,
),
),
if (!isSkip) const SizedBox(width: 12),
@@ -722,18 +732,30 @@ class _BattleScreenState extends State<BattleScreen> {
Widget _buildFloatingActionButton(
BuildContext context,
String label,
IconData icon,
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: Icon(icon),
child: Image.asset(
iconPath,
width: 32,
height: 32,
color: ThemeConfig.textColorWhite, // Tint icon white
fit: BoxFit.contain,
),
);
}
}
+12 -12
View File
@@ -125,13 +125,12 @@ class InventoryScreen extends StatelessWidget {
left: 4,
top: 4,
child: Opacity(
opacity: item != null ? 0.2 : 0.1,
child: Icon(
ItemUtils.getIcon(slot),
size: 40,
color: item != null
? ItemUtils.getColor(slot)
: ThemeConfig.textColorGrey,
opacity: item != null ? 0.5 : 0.2, // Increase opacity slightly for images
child: Image.asset(
ItemUtils.getIconPath(slot),
width: 40,
height: 40,
fit: BoxFit.contain,
),
),
),
@@ -238,11 +237,12 @@ class InventoryScreen extends StatelessWidget {
left: 4,
top: 4,
child: Opacity(
opacity: 0.2,
child: Icon(
ItemUtils.getIcon(item.slot),
size: 40,
color: ItemUtils.getColor(item.slot),
opacity: 0.5, // Adjusted opacity for image visibility
child: Image.asset(
ItemUtils.getIconPath(item.slot),
width: 40,
height: 40,
fit: BoxFit.contain,
),
),
),
+1
View File
@@ -37,6 +37,7 @@ class _MainMenuScreenState extends State<MainMenuScreen> {
Future<void> _continueGame() async {
final data = await SaveManager.loadGame();
if (data != null && mounted) {
// BattleProvider is already provided with ShopProvider via ProxyProvider in main.dart
context.read<BattleProvider>().loadFromSave(data);
Navigator.pushReplacement(
context,
+5 -18
View File
@@ -16,29 +16,16 @@ class ItemUtils {
}
}
static IconData getIcon(EquipmentSlot slot) {
static String getIconPath(EquipmentSlot slot) {
switch (slot) {
case EquipmentSlot.weapon:
return Icons.change_history; // Triangle
return 'assets/data/icon/icon_weapon.png';
case EquipmentSlot.shield:
return Icons.shield;
return 'assets/data/icon/icon_shield.png';
case EquipmentSlot.armor:
return Icons.checkroom;
return 'assets/data/icon/icon_armor.png';
case EquipmentSlot.accessory:
return Icons.diamond;
}
}
static Color getColor(EquipmentSlot slot) {
switch (slot) {
case EquipmentSlot.weapon:
return Colors.red;
case EquipmentSlot.shield:
return Colors.blue;
case EquipmentSlot.armor:
return Colors.blue;
case EquipmentSlot.accessory:
return Colors.orange;
return 'assets/data/icon/icon_accessory.png';
}
}
}
-277
View File
@@ -1,277 +0,0 @@
import 'package:flutter/material.dart';
import '../../providers/battle_provider.dart';
import '../../game/model/item.dart';
import '../../utils/item_utils.dart';
import '../../game/enums.dart';
import '../../game/config/theme_config.dart';
class ShopUI extends StatelessWidget {
final BattleProvider battleProvider;
const ShopUI({super.key, required this.battleProvider});
@override
Widget build(BuildContext context) {
final player = battleProvider.player;
final shopItems = battleProvider.currentStage.shopItems;
return Container(
color: ThemeConfig.shopBg,
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
// Header: Merchant Icon & Player Gold
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Row(
children: [
Icon(Icons.store, size: 32, color: ThemeConfig.mainIconColor),
SizedBox(width: 8),
Text(
"Merchant",
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: ThemeConfig.textColorWhite),
),
],
),
Row(
children: [
const Icon(Icons.monetization_on, color: ThemeConfig.statGoldColor),
const SizedBox(width: 4),
Text(
"${player.gold} G",
style: const TextStyle(
color: ThemeConfig.statGoldColor,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
],
),
],
),
const Divider(color: ThemeConfig.textColorGrey),
const SizedBox(height: 16),
// Shop Items Grid
Expanded(
child: shopItems.isEmpty
? const Center(
child: Text(
"Sold Out",
style: TextStyle(color: ThemeConfig.textColorGrey, fontSize: 24),
),
)
: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, // 2 columns
crossAxisSpacing: 16.0,
mainAxisSpacing: 16.0,
childAspectRatio: 0.8, // Taller cards
),
itemCount: shopItems.length,
itemBuilder: (context, index) {
final item = shopItems[index];
final canBuy = player.gold >= item.price;
return InkWell(
onTap: () => _showBuyConfirmation(context, item),
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(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Icon
Expanded(
flex: 2,
child: Center(
child: Icon(
ItemUtils.getIcon(item.slot),
size: 48,
color: ItemUtils.getColor(item.slot),
),
),
),
// Name
Expanded(
flex: 1,
child: Center(
child: Text(
item.name,
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
color: ItemUtils.getRarityColor(item.rarity),
fontSize: 12,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
),
// Stats
Expanded(
flex: 1,
child: _buildItemStatText(item),
),
// Price Button
SizedBox(
height: 32,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: canBuy ? ThemeConfig.statGoldColor : ThemeConfig.btnDisabled,
foregroundColor: Colors.black,
padding: EdgeInsets.zero,
),
onPressed: canBuy
? () => _showBuyConfirmation(context, item)
: null,
child: Text(
"${item.price} G",
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
),
],
),
),
),
);
},
),
),
const SizedBox(height: 16),
// Footer Buttons (Reroll & Leave)
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: ThemeConfig.btnRerollBg,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
onPressed: player.gold >= 50
? () => battleProvider.rerollShopItems()
: null,
icon: const Icon(Icons.refresh, color: ThemeConfig.textColorWhite),
label: const Text(
"Reroll (50 G)",
style: TextStyle(color: ThemeConfig.textColorWhite),
),
),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: ThemeConfig.btnLeaveBg,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
onPressed: () => battleProvider.proceedToNextStage(),
icon: const Icon(Icons.exit_to_app, color: ThemeConfig.textColorWhite),
label: const Text(
"Leave Shop",
style: TextStyle(color: ThemeConfig.textColorWhite),
),
),
],
),
],
),
);
}
void _showBuyConfirmation(BuildContext context, Item item) {
if (battleProvider.player.gold < item.price) return;
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text("Buy Item"),
content: Text("Buy ${item.name} for ${item.price} G?"),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text("Cancel"),
),
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: ThemeConfig.statGoldColor),
onPressed: () {
battleProvider.buyItem(item);
Navigator.pop(ctx);
},
child: const Text("Buy", style: TextStyle(color: Colors.black)),
),
],
),
);
}
Widget _buildItemStatText(Item item) {
List<String> stats = [];
if (item.atkBonus > 0) stats.add("ATK +${item.atkBonus}");
if (item.hpBonus > 0) stats.add("HP +${item.hpBonus}");
if (item.armorBonus > 0) stats.add("DEF +${item.armorBonus}");
if (item.luck > 0) stats.add("LUCK +${item.luck}");
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (stats.isNotEmpty)
Text(
stats.join(", "),
style: const TextStyle(fontSize: 10, color: Colors.white70),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (item.effects.isNotEmpty)
Text(
item.effects.first.type.name.toUpperCase(),
style: const TextStyle(fontSize: 9, color: ThemeConfig.rarityLegendary),
textAlign: TextAlign.center,
),
],
);
}
}
class RestUI extends StatelessWidget {
final BattleProvider battleProvider;
const RestUI({super.key, required this.battleProvider});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.local_hotel, size: 64, color: ThemeConfig.btnRestBg),
const SizedBox(height: 16),
const Text("Rest Area", style: TextStyle(fontSize: 24, color: ThemeConfig.textColorWhite)),
const SizedBox(height: 8),
const Text("Take a breath and heal.", style: TextStyle(color: ThemeConfig.textColorWhite)),
const SizedBox(height: 32),
ElevatedButton(
onPressed: () {
battleProvider.player.heal(20);
battleProvider.proceedToNextStage();
},
child: const Text("Rest & Leave (+20 HP)"),
),
],
),
);
}
}
+37
View File
@@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import '../../../providers/battle_provider.dart';
import '../../../game/config/theme_config.dart';
class RestUI extends StatelessWidget {
final BattleProvider battleProvider;
const RestUI({super.key, required this.battleProvider});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.local_hotel, size: 64, color: ThemeConfig.btnRestBg),
const SizedBox(height: 16),
const Text("Rest Area", style: TextStyle(fontSize: 24, color: ThemeConfig.textColorWhite)),
const SizedBox(height: 8),
const Text("Take a breath and heal.", style: TextStyle(color: ThemeConfig.textColorWhite)),
const SizedBox(height: 32),
ElevatedButton(
onPressed: () {
// Use GameConfig for heal amount if possible, or keep hardcoded for now?
// Let's use GameConfig.stageHealRatio * 2 or fixed 20?
// Previous logic was hardcoded 20. Let's keep it simple for now or use a better logic.
// "Rest & Leave (+20 HP)" -> Hardcoded in text too.
battleProvider.player.heal(20);
battleProvider.proceedToNextStage();
},
child: const Text("Rest & Leave (+20 HP)"),
),
],
),
);
}
}
+333
View File
@@ -0,0 +1,333 @@
import 'package:flutter/material.dart';
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';
class ShopUI extends StatelessWidget {
final BattleProvider battleProvider;
const ShopUI({super.key, required this.battleProvider});
@override
Widget build(BuildContext context) {
return Consumer<ShopProvider>(
builder: (context, shopProvider, child) {
final player = battleProvider.player;
final shopItems = shopProvider.availableItems;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (shopProvider.lastShopMessage.isNotEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(shopProvider.lastShopMessage),
backgroundColor: Colors.red,
),
);
shopProvider.clearMessage();
}
});
return Container(
color: ThemeConfig.shopBg,
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Row(
children: [
Icon(
Icons.store,
size: 32,
color: ThemeConfig.mainIconColor,
),
SizedBox(width: 8),
Text(
"Merchant",
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: ThemeConfig.textColorWhite,
),
),
],
),
Row(
children: [
const Icon(
Icons.monetization_on,
color: ThemeConfig.statGoldColor,
),
const SizedBox(width: 4),
Text(
"${player.gold} G",
style: const TextStyle(
color: ThemeConfig.statGoldColor,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
],
),
],
),
const Divider(color: ThemeConfig.textColorGrey),
const SizedBox(height: 16),
Expanded(
child: shopItems.isEmpty
? const Center(
child: Text(
"Sold Out",
style: TextStyle(
color: ThemeConfig.textColorGrey,
fontSize: 24,
),
),
)
: GridView.builder(
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 16.0,
mainAxisSpacing: 16.0,
childAspectRatio: 0.8,
),
itemCount: shopItems.length,
itemBuilder: (context, index) {
final item = shopItems[index];
final canBuy = player.gold >= item.price;
return InkWell(
onTap: () => _showBuyConfirmation(
context,
item,
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(8.0),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.stretch,
children: [
Expanded(
flex: 2,
child: Center(
child: Image.asset(
ItemUtils.getIconPath(item.slot),
width: 48,
height: 48,
fit: BoxFit.contain,
),
),
),
Expanded(
flex: 1,
child: Center(
child: Text(
item.name,
textAlign: TextAlign.center,
style: TextStyle(
fontWeight:
ThemeConfig.fontWeightBold,
color: ItemUtils.getRarityColor(
item.rarity,
),
fontSize: ThemeConfig.fontSizeMedium,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
),
Expanded(
flex: 1,
child: _buildItemStatText(item),
),
SizedBox(
height: 32,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: canBuy
? ThemeConfig.statGoldColor
: ThemeConfig.btnDisabled,
foregroundColor: Colors.black,
padding: EdgeInsets.zero,
),
onPressed: canBuy
? () => _showBuyConfirmation(
context,
item,
shopProvider,
player,
)
: null,
child: Text(
"${item.price} G",
style: const TextStyle(
fontWeight:
ThemeConfig.fontWeightBold,
),
),
),
),
],
),
),
),
);
},
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: ThemeConfig.btnRerollBg,
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
),
onPressed: player.gold >= GameConfig.shopRerollCost
? () => shopProvider.rerollShopItems(
player,
battleProvider.stage,
)
: null,
icon: const Icon(
Icons.refresh,
color: ThemeConfig.textColorWhite,
),
label: Text(
"Reroll (${GameConfig.shopRerollCost} G)",
style: const TextStyle(color: ThemeConfig.textColorWhite),
),
),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: ThemeConfig.btnLeaveBg,
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
),
onPressed: () => battleProvider.proceedToNextStage(),
icon: const Icon(
Icons.exit_to_app,
color: ThemeConfig.textColorWhite,
),
label: const Text(
"Leave Shop",
style: TextStyle(color: ThemeConfig.textColorWhite),
),
),
],
),
],
),
);
},
);
}
void _showBuyConfirmation(
BuildContext context,
Item item,
ShopProvider shopProvider,
Character player,
) {
if (player.gold < item.price) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Not enough gold!"),
backgroundColor: Colors.red,
),
);
return;
}
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text("Buy Item"),
content: Text("Buy ${item.name} for ${item.price} G?"),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text("Cancel"),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: ThemeConfig.statGoldColor,
),
onPressed: () {
shopProvider.buyItem(item, player);
Navigator.pop(ctx);
},
child: const Text("Buy", style: TextStyle(color: Colors.black)),
),
],
),
);
}
Widget _buildItemStatText(Item item) {
List<String> stats = [];
if (item.atkBonus > 0) stats.add("ATK +${item.atkBonus}");
if (item.hpBonus > 0) stats.add("HP +${item.hpBonus}");
if (item.armorBonus > 0) stats.add("DEF +${item.armorBonus}");
if (item.luck > 0) stats.add("LUCK +${item.luck}");
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (stats.isNotEmpty)
Text(
stats.join(", "),
style: const TextStyle(
fontSize: ThemeConfig.fontSizeSmall,
color: Colors.white70,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (item.effects.isNotEmpty)
Text(
item.effects.first.type.name.toUpperCase(),
style: const TextStyle(
fontSize: ThemeConfig.fontSizeTiny,
color: ThemeConfig.rarityLegendary,
),
textAlign: TextAlign.center,
),
],
);
}
}