This commit is contained in:
2025-12-07 17:58:00 +09:00
parent d5609aff0f
commit dcfb8ab9de
22 changed files with 612 additions and 165 deletions
+4 -3
View File
@@ -5,9 +5,10 @@ class ItemConfig {
/// Used when selecting random items in Shop or Rewards.
/// Higher weight = Higher chance.
static const Map<ItemRarity, int> defaultRarityWeights = {
ItemRarity.magic: 60,
ItemRarity.rare: 30,
ItemRarity.legendary: 9,
ItemRarity.normal: 50,
ItemRarity.magic: 30,
ItemRarity.rare: 15,
ItemRarity.legendary: 4,
ItemRarity.unique: 1,
};
}
+4 -3
View File
@@ -23,7 +23,7 @@ class ThemeConfig {
static const Color mainTitleColor = Colors.white;
static const Color subTitleColor = Colors.grey;
static const Color mainIconColor = Colors.amber;
// Button Colors
static const Color btnNewGameBg = Color(0xFFFFA000); // Colors.amber[700]
static const Color btnNewGameText = Colors.black;
@@ -37,14 +37,14 @@ class ThemeConfig {
static const Color btnDisabled = Colors.grey;
static const Color btnRestartBg = Colors.orange;
static const Color btnReturnMenuBg = Colors.red;
// Stat Colors
static const Color statHpColor = Colors.red;
static const Color statHpPlayerColor = Colors.green;
static const Color statHpEnemyColor = Colors.red;
static const Color statAtkColor = Colors.blueAccent;
static const Color statDefColor =
Colors.green; // Or Blue depending on context
Colors.blueAccent; // Or Blue depending on context
static const Color statLuckColor = Colors.green;
static const Color statGoldColor = Colors.amber;
@@ -82,6 +82,7 @@ class ThemeConfig {
static const Color effectText = Colors.white;
// Rarity Colors
static const Color rarityNormal = Colors.white;
static const Color rarityMagic = Colors.blueAccent;
static const Color rarityRare = Colors.yellow;
static const Color rarityLegendary = Colors.orange;
+36
View File
@@ -1,6 +1,8 @@
import 'dart:convert';
import 'dart:math';
import 'package:flutter/services.dart';
import '../model/entity.dart';
import '../config/game_config.dart';
import 'item_table.dart';
@@ -11,6 +13,7 @@ class EnemyTemplate {
final int baseDefense;
final String? image;
final List<String> equipmentIds;
final int tier;
const EnemyTemplate({
required this.name,
@@ -19,6 +22,7 @@ class EnemyTemplate {
required this.baseDefense,
this.image,
this.equipmentIds = const [],
this.tier = 1,
});
factory EnemyTemplate.fromJson(Map<String, dynamic> json) {
@@ -29,6 +33,7 @@ class EnemyTemplate {
baseDefense: json['baseDefense'] ?? 0,
image: json['image'],
equipmentIds: (json['equipment'] as List<dynamic>?)?.cast<String>() ?? [],
tier: json['tier'] ?? 1,
);
}
@@ -63,6 +68,7 @@ class EnemyTemplate {
class EnemyTable {
static List<EnemyTemplate> normalEnemies = [];
static List<EnemyTemplate> eliteEnemies = [];
static final Random _random = Random();
static Future<void> load() async {
final String jsonString = await rootBundle.loadString(
@@ -77,4 +83,34 @@ class EnemyTable {
.map((e) => EnemyTemplate.fromJson(e))
.toList();
}
/// Returns a random enemy suitable for the current stage.
static EnemyTemplate getRandomEnemy({required int stage, bool isElite = false}) {
int targetTier = 1;
if (stage > GameConfig.tier2StageMax) {
targetTier = 3;
} else if (stage > GameConfig.tier1StageMax) {
targetTier = 2;
}
List<EnemyTemplate> pool = isElite ? eliteEnemies : normalEnemies;
// Filter by tier
var tierPool = pool.where((e) => e.tier == targetTier).toList();
// Fallback: If no enemies found for this tier, use lower tiers (or any)
if (tierPool.isEmpty) {
tierPool = pool.where((e) => e.tier <= targetTier).toList();
}
if (tierPool.isEmpty) {
tierPool = pool; // Absolute fallback
}
if (tierPool.isEmpty) {
// Should not happen if JSON is correct
return const EnemyTemplate(name: "Fallback Enemy", baseHp: 10, baseAtk: 1, baseDefense: 0);
}
return tierPool[_random.nextInt(tierPool.length)];
}
}
+12 -1
View File
@@ -4,15 +4,26 @@ class ItemModifier {
final String prefix;
final Map<StatType, int> statChanges;
final List<EquipmentSlot>? allowedSlots; // Null means allowed for all slots
final double multiplier; // For percent-based modifiers (Normal rarity)
final int weight; // Selection weight
const ItemModifier({
required this.prefix,
required this.statChanges,
this.statChanges = const {},
this.allowedSlots,
this.multiplier = 1.0,
this.weight = 1,
});
}
class ItemPrefixTable {
static const List<ItemModifier> normalPrefixes = [
ItemModifier(prefix: "Crude", multiplier: 0.9, weight: 25),
ItemModifier(prefix: "Old", multiplier: 0.95, weight: 25),
ItemModifier(prefix: "", multiplier: 1.0, weight: 25), // Standard
ItemModifier(prefix: "High-quality", multiplier: 1.1, weight: 25),
];
static const List<ItemModifier> magicPrefixes = [
// Weapons
ItemModifier(
+73 -9
View File
@@ -6,6 +6,7 @@ import '../enums.dart';
import '../config/item_config.dart';
import 'item_prefix_table.dart'; // Import prefix table
import 'name_generator.dart'; // Import name generator
import '../../utils/game_math.dart';
class ItemTemplate {
final String id;
@@ -79,8 +80,40 @@ class ItemTemplate {
final random = Random();
// 0. Normal Rarity: Prefix logic for base stat variations
if (rarity == ItemRarity.normal) {
// Weighted Random Selection
final prefixes = ItemPrefixTable.normalPrefixes;
int totalWeight = prefixes.fold(0, (sum, p) => sum + p.weight);
int roll = random.nextInt(totalWeight);
ItemModifier? selectedModifier;
int currentSum = 0;
for (var mod in prefixes) {
currentSum += mod.weight;
if (roll < currentSum) {
selectedModifier = mod;
break;
}
}
if (selectedModifier != null) {
if (selectedModifier.prefix.isNotEmpty) {
finalName = "${selectedModifier.prefix} $name";
}
double mult = selectedModifier.multiplier;
if (mult != 1.0) {
finalAtk = (finalAtk * mult).floor();
finalHp = (finalHp * mult).floor();
finalArmor = (finalArmor * mult).floor();
// Luck usually isn't scaled by small multipliers, but let's keep it consistent or skip.
// Skipping luck scaling for normal prefixes to avoid 0.
}
}
}
// 1. Magic Rarity: 50% chance to get a Magic Prefix (1 stat change)
if (rarity == ItemRarity.magic) {
else if (rarity == ItemRarity.magic) {
if (random.nextBool()) { // 50% chance
// Filter valid prefixes for this slot
final validPrefixes = ItemPrefixTable.magicPrefixes.where((p) {
@@ -208,11 +241,14 @@ class ItemTable {
/// [tier]: The tier of items to select from.
/// [slot]: Optional. If provided, only items of this slot are considered.
/// [weights]: Optional map of rarity weights. Key: Rarity, Value: Weight.
/// Default weights: Common: 60, Rare: 30, Epic: 9, Legendary: 1.
/// [minRarity]: Optional. Minimum rarity to consider (inclusive).
/// [maxRarity]: Optional. Maximum rarity to consider (inclusive).
static ItemTemplate? getRandomItem({
required ItemTier tier,
EquipmentSlot? slot,
Map<ItemRarity, int>? weights,
ItemRarity? minRarity,
ItemRarity? maxRarity,
}) {
// 1. Filter by Tier and Slot (if provided)
var candidates = allItems.where((item) => item.tier == tier);
@@ -222,16 +258,37 @@ class ItemTable {
if (candidates.isEmpty) return null;
// 2. Determine Target Rarity based on weights
final rarityWeights = weights ?? ItemConfig.defaultRarityWeights;
// 2. Prepare Rarity Weights (Filtered by min/max)
Map<ItemRarity, int> activeWeights = Map.from(weights ?? ItemConfig.defaultRarityWeights);
int totalWeight = rarityWeights.values.fold(0, (sum, w) => sum + w);
if (minRarity != null) {
activeWeights.removeWhere((r, w) => r.index < minRarity.index);
}
if (maxRarity != null) {
activeWeights.removeWhere((r, w) => r.index > maxRarity.index);
}
if (activeWeights.isEmpty) {
// 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);
}
if (maxRarity != null) {
candidates = candidates.where((item) => item.rarity.index <= maxRarity.index);
}
if (candidates.isEmpty) return null;
return candidates.toList()[_random.nextInt(candidates.length)];
}
// 3. Determine Target Rarity based on filtered weights
int totalWeight = activeWeights.values.fold(0, (sum, w) => sum + w);
int roll = _random.nextInt(totalWeight);
ItemRarity? selectedRarity;
int currentSum = 0;
for (var entry in rarityWeights.entries) {
for (var entry in activeWeights.entries) {
currentSum += entry.value;
if (roll < currentSum) {
selectedRarity = entry.key;
@@ -239,15 +296,22 @@ class ItemTable {
}
}
// 3. Filter candidates by Selected Rarity
// 4. Filter candidates by Selected Rarity
var rarityCandidates = candidates.where((item) => item.rarity == selectedRarity).toList();
// 4. Fallback: If no items of selected rarity, use any item from the filtered candidates
// 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 (maxRarity != null) {
candidates = candidates.where((item) => item.rarity.index <= maxRarity.index);
}
if (candidates.isEmpty) return null;
return candidates.toList()[_random.nextInt(candidates.length)];
}
// 5. Pick random item
// 6. Pick random item
return rarityCandidates[_random.nextInt(rarityCandidates.length)];
}
}
+1 -1
View File
@@ -35,6 +35,6 @@ enum DamageType { normal, bleed, vulnerable }
enum StatType { maxHp, atk, defense, luck }
enum ItemRarity { magic, rare, legendary, unique }
enum ItemRarity { normal, magic, rare, legendary, unique }
enum ItemTier { tier1, tier2, tier3 }
+120 -92
View File
@@ -57,6 +57,10 @@ class BattleProvider with ChangeNotifier {
List<String> get logs => battleLogs;
int get lastGoldReward => _lastGoldReward;
void refreshUI() {
notifyListeners();
}
// Damage Event Stream
final _damageEventController = StreamController<DamageEvent>.broadcast();
Stream<DamageEvent> get damageStream => _damageEventController.stream;
@@ -83,10 +87,10 @@ class BattleProvider with ChangeNotifier {
stage = data['stage'];
turnCount = data['turnCount'];
player = Character.fromJson(data['player']);
battleLogs.clear();
_addLog("Game Loaded! Resuming Stage $stage");
_prepareNextStage();
notifyListeners();
}
@@ -113,51 +117,51 @@ class BattleProvider with ChangeNotifier {
player.gold = GameConfig.startingGold;
// Provide starter equipment
final starterSword = Item(
id: "starter_sword",
name: "Wooden Sword",
description: "A basic sword",
atkBonus: 5,
hpBonus: 0,
slot: EquipmentSlot.weapon,
);
final starterArmor = Item(
id: "starter_armor",
name: "Leather Armor",
description: "Basic protection",
atkBonus: 0,
hpBonus: 20,
slot: EquipmentSlot.armor,
);
final starterShield = Item(
id: "starter_shield",
name: "Wooden Shield",
description: "A small shield",
atkBonus: 0,
hpBonus: 0,
armorBonus: 3,
slot: EquipmentSlot.shield,
);
final starterRing = Item(
id: "starter_ring",
name: "Copper Ring",
description: "A simple ring",
atkBonus: 1,
hpBonus: 5,
slot: EquipmentSlot.accessory,
);
// final starterSword = Item(
// id: "starter_sword",
// name: "Wooden Sword",
// description: "A basic sword",
// atkBonus: 5,
// hpBonus: 0,
// slot: EquipmentSlot.weapon,
// );
// final starterArmor = Item(
// id: "starter_armor",
// name: "Leather Armor",
// description: "Basic protection",
// atkBonus: 0,
// hpBonus: 20,
// slot: EquipmentSlot.armor,
// );
// final starterShield = Item(
// id: "starter_shield",
// name: "Wooden Shield",
// description: "A small shield",
// atkBonus: 0,
// hpBonus: 0,
// armorBonus: 3,
// slot: EquipmentSlot.shield,
// );
// final starterRing = Item(
// id: "starter_ring",
// name: "Copper Ring",
// description: "A simple ring",
// atkBonus: 1,
// hpBonus: 5,
// slot: EquipmentSlot.accessory,
// );
player.addToInventory(starterSword);
player.equip(starterSword);
// player.addToInventory(starterSword);
// player.equip(starterSword);
player.addToInventory(starterArmor);
player.equip(starterArmor);
// player.addToInventory(starterArmor);
// player.equip(starterArmor);
player.addToInventory(starterShield);
player.equip(starterShield);
// player.addToInventory(starterShield);
// player.equip(starterShield);
player.addToInventory(starterRing);
player.equip(starterRing);
// player.addToInventory(starterRing);
// player.equip(starterRing);
// Add new status effect items for testing
player.addToInventory(ItemTable.weapons[3].createItem()); // Stunning Hammer
@@ -194,36 +198,8 @@ class BattleProvider with ChangeNotifier {
if (type == StageType.battle || type == StageType.elite) {
bool isElite = type == StageType.elite;
// Select random enemy template
final random = Random();
EnemyTemplate template;
if (isElite) {
if (EnemyTable.eliteEnemies.isNotEmpty) {
template = EnemyTable
.eliteEnemies[random.nextInt(EnemyTable.eliteEnemies.length)];
} else {
// Fallback if no elite enemies loaded
template = const EnemyTemplate(
name: "Elite Guardian",
baseHp: 50,
baseAtk: 10,
baseDefense: 2,
);
}
} else {
if (EnemyTable.normalEnemies.isNotEmpty) {
template = EnemyTable
.normalEnemies[random.nextInt(EnemyTable.normalEnemies.length)];
} else {
// Fallback
template = const EnemyTemplate(
name: "Enemy",
baseHp: 20,
baseAtk: 5,
baseDefense: 0,
);
}
}
EnemyTemplate template = EnemyTable.getRandomEnemy(stage: stage, isElite: isElite);
newEnemy = template.createCharacter(stage: stage);
@@ -264,6 +240,12 @@ class BattleProvider with ChangeNotifier {
// Replaces _spawnEnemy
// void _spawnEnemy() { ... } - Removed
Future<void> _onDefeat() async {
_addLog("Player defeated! Enemy wins!");
await SaveManager.clearSaveData();
notifyListeners();
}
/// Handle player's action choice
Future<void> playerAction(ActionType type, RiskLevel risk) async {
@@ -287,6 +269,12 @@ class BattleProvider with ChangeNotifier {
// 2. Process Start-of-Turn Effects (Stun, Bleed)
bool canAct = _processStartTurnEffects(player);
if (player.isDead) {
await _onDefeat();
return;
}
if (!canAct) {
_endPlayerTurn(); // Skip turn if stunned
return;
@@ -341,11 +329,17 @@ class BattleProvider with ChangeNotifier {
// Animation Delays to sync with Impact
if (risk == RiskLevel.safe) {
await Future.delayed(const Duration(milliseconds: GameConfig.animDelaySafe));
await Future.delayed(
const Duration(milliseconds: GameConfig.animDelaySafe),
);
} else if (risk == RiskLevel.normal) {
await Future.delayed(const Duration(milliseconds: GameConfig.animDelayNormal));
await Future.delayed(
const Duration(milliseconds: GameConfig.animDelayNormal),
);
} else if (risk == RiskLevel.risky) {
await Future.delayed(const Duration(milliseconds: GameConfig.animDelayRisky));
await Future.delayed(
const Duration(milliseconds: GameConfig.animDelayRisky),
);
}
int damageToHp = 0;
@@ -437,14 +431,19 @@ class BattleProvider with ChangeNotifier {
return;
}
Future.delayed(const Duration(milliseconds: GameConfig.animDelayEnemyTurn), () => _enemyTurn());
Future.delayed(
const Duration(milliseconds: GameConfig.animDelayEnemyTurn),
() => _enemyTurn(),
);
}
Future<void> _enemyTurn() async {
if (!isPlayerTurn && (player.isDead || enemy.isDead)) return;
_addLog("Enemy's turn...");
await Future.delayed(const Duration(milliseconds: GameConfig.animDelayEnemyTurn));
await Future.delayed(
const Duration(milliseconds: GameConfig.animDelayEnemyTurn),
);
// Enemy Turn Start Logic
// Armor decay
@@ -543,7 +542,8 @@ class BattleProvider with ChangeNotifier {
}
if (player.isDead) {
_addLog("Player defeated! Enemy wins!");
await _onDefeat();
return;
}
isPlayerTurn = true;
@@ -654,15 +654,6 @@ class BattleProvider with ChangeNotifier {
_addLog("Enemy defeated! Gained $goldReward Gold.");
_addLog("Choose a reward.");
List<ItemTemplate> allTemplates = List.from(ItemTable.allItems);
allTemplates.shuffle(random); // Shuffle to randomize selection
// Item Rewards
// Logic: Get random items based on current round tier? For now just random.
// Ideally should use ItemTable.getRandomItem() with Tier logic.
// Let's use our new weighted random logic if available, or fallback to simple shuffle for now to keep it simple.
// Since we just refactored ItemTable, let's use getRandomItem!
ItemTier currentTier = ItemTier.tier1;
if (stage > GameConfig.tier2StageMax)
currentTier = ItemTier.tier3;
@@ -670,9 +661,42 @@ class BattleProvider with ChangeNotifier {
currentTier = ItemTier.tier2;
rewardOptions = [];
// Get 3 distinct items if possible
bool isElite = currentStage.type == StageType.elite;
bool isTier1 = currentTier == ItemTier.tier1;
// Get 3 distinct items
for (int i = 0; i < 3; i++) {
ItemTemplate? item = ItemTable.getRandomItem(tier: currentTier);
ItemRarity? minRarity;
ItemRarity? maxRarity;
// 1. Elite Reward Logic (First Item only)
if (isElite && i == 0) {
if (isTier1) {
// Tier 1 Elite: Guaranteed Rare
minRarity = ItemRarity.rare;
maxRarity = ItemRarity.rare; // Or allow higher? Request said "Guaranteed Rare 1 drop". Let's fix to Rare.
} else {
// Tier 2/3 Elite: Guaranteed Legendary
minRarity = ItemRarity.legendary;
// maxRarity = ItemRarity.legendary; // Optional, but let's allow Unique too if weights permit, or fix to Legendary. Request said "Guaranteed Legendary".
}
}
// 2. Standard Reward Logic (Others)
else {
if (isTier1) {
// Tier 1 Normal/Other Rewards: Max Magic (No Rare+)
maxRarity = ItemRarity.magic;
}
// Tier 2/3 Normal: No extra restrictions
}
ItemTemplate? item = ItemTable.getRandomItem(
tier: currentTier,
minRarity: minRarity,
maxRarity: maxRarity
);
if (item != null) {
rewardOptions.add(item.createItem(stage: stage));
}
@@ -716,7 +740,9 @@ class BattleProvider with ChangeNotifier {
void _completeStage() {
// Heal player after selecting reward
int healAmount = GameMath.floor(player.totalMaxHp * GameConfig.stageHealRatio);
int healAmount = GameMath.floor(
player.totalMaxHp * GameConfig.stageHealRatio,
);
player.heal(healAmount);
_addLog("Stage Cleared! Recovered $healAmount HP.");
@@ -760,7 +786,9 @@ class BattleProvider with ChangeNotifier {
void sellItem(Item item) {
if (player.inventory.remove(item)) {
int sellPrice = GameMath.floor(item.price * GameConfig.sellPriceMultiplier);
int sellPrice = GameMath.floor(
item.price * GameConfig.sellPriceMultiplier,
);
player.gold += sellPrice;
_addLog("Sold ${item.name} for $sellPrice G.");
notifyListeners();
+2
View File
@@ -601,6 +601,7 @@ class _BattleScreenState extends State<BattleScreen> {
width: 24,
height: 24,
fit: BoxFit.contain,
filterQuality: FilterQuality.high,
),
),
if (!isSkip) const SizedBox(width: 12),
@@ -755,6 +756,7 @@ class _BattleScreenState extends State<BattleScreen> {
height: 32,
color: ThemeConfig.textColorWhite, // Tint icon white
fit: BoxFit.contain,
filterQuality: FilterQuality.high,
),
);
}
+78 -25
View File
@@ -39,20 +39,29 @@ class InventoryScreen extends StatelessWidget {
_buildStatItem(
"HP",
"${player.hp}/${player.totalMaxHp}",
color: ThemeConfig.statHpColor,
),
_buildStatItem("ATK", "${player.totalAtk}"),
_buildStatItem("DEF", "${player.totalDefense}"),
_buildStatItem("Shield", "${player.armor}"),
_buildStatItem(
"Gold",
"${player.gold} G",
color: ThemeConfig.statGoldColor,
"ATK",
"${player.totalAtk}",
color: ThemeConfig.statAtkColor,
),
_buildStatItem(
"DEF",
"${player.totalDefense}",
color: ThemeConfig.statDefColor,
),
_buildStatItem("Shield", "${player.armor}"),
_buildStatItem(
"Luck",
"${player.totalLuck}",
color: ThemeConfig.statLuckColor,
),
_buildStatItem(
"Gold",
"${player.gold} G",
color: ThemeConfig.statGoldColor,
),
],
),
],
@@ -94,12 +103,14 @@ class InventoryScreen extends StatelessWidget {
color: item != null
? ThemeConfig.equipmentCardBg
: ThemeConfig.emptySlotBg,
shape: item != null &&
shape:
item != null &&
item.rarity != ItemRarity.magic
? RoundedRectangleBorder(
side: BorderSide(
color:
ItemUtils.getRarityColor(item.rarity),
color: ItemUtils.getRarityColor(
item.rarity,
),
width: 2.0,
),
borderRadius: BorderRadius.circular(4.0),
@@ -125,12 +136,15 @@ class InventoryScreen extends StatelessWidget {
left: 4,
top: 4,
child: Opacity(
opacity: item != null ? 0.5 : 0.2, // Increase opacity slightly for images
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,
filterQuality: FilterQuality.high,
),
),
),
@@ -151,8 +165,10 @@ class InventoryScreen extends StatelessWidget {
item?.name ?? "Empty",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: ThemeConfig.fontSizeSmall,
fontWeight: ThemeConfig.fontWeightBold,
fontSize:
ThemeConfig.fontSizeSmall,
fontWeight:
ThemeConfig.fontWeightBold,
color: item != null
? ItemUtils.getRarityColor(
item.rarity,
@@ -237,12 +253,14 @@ class InventoryScreen extends StatelessWidget {
left: 4,
top: 4,
child: Opacity(
opacity: 0.5, // Adjusted opacity for image visibility
opacity:
0.5, // Adjusted opacity for image visibility
child: Image.asset(
ItemUtils.getIconPath(item.slot),
width: 40,
height: 40,
fit: BoxFit.contain,
filterQuality: FilterQuality.high,
),
),
),
@@ -260,7 +278,8 @@ class InventoryScreen extends StatelessWidget {
textAlign: TextAlign.center,
style: TextStyle(
fontSize: ThemeConfig.fontSizeSmall,
fontWeight: ThemeConfig.fontWeightBold,
fontWeight:
ThemeConfig.fontWeightBold,
color: ItemUtils.getRarityColor(
item.rarity,
),
@@ -289,7 +308,10 @@ class InventoryScreen extends StatelessWidget {
color: ThemeConfig.emptySlotBg,
),
child: const Center(
child: Icon(Icons.add_box, color: ThemeConfig.textColorGrey),
child: Icon(
Icons.add_box,
color: ThemeConfig.textColorGrey,
),
),
);
}
@@ -306,7 +328,13 @@ class InventoryScreen extends StatelessWidget {
Widget _buildStatItem(String label, String value, {Color? color}) {
return Column(
children: [
Text(label, style: const TextStyle(color: ThemeConfig.textColorGrey, fontSize: 12)),
Text(
label,
style: const TextStyle(
color: ThemeConfig.textColorGrey,
fontSize: 12,
),
),
Text(
value,
style: TextStyle(
@@ -358,7 +386,10 @@ class InventoryScreen extends StatelessWidget {
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
const Icon(Icons.attach_money, color: ThemeConfig.statGoldColor),
const Icon(
Icons.attach_money,
color: ThemeConfig.statGoldColor,
),
const SizedBox(width: 10),
Text("Sell (${item.price} G)"),
],
@@ -402,7 +433,9 @@ class InventoryScreen extends StatelessWidget {
child: const Text("Cancel"),
),
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: ThemeConfig.statGoldColor),
style: ElevatedButton.styleFrom(
backgroundColor: ThemeConfig.statGoldColor,
),
onPressed: () {
provider.sellItem(item);
Navigator.pop(ctx);
@@ -430,7 +463,9 @@ class InventoryScreen extends StatelessWidget {
child: const Text("Cancel"),
),
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: ThemeConfig.btnActionActive),
style: ElevatedButton.styleFrom(
backgroundColor: ThemeConfig.btnActionActive,
),
onPressed: () {
provider.discardItem(item);
Navigator.pop(ctx);
@@ -481,7 +516,10 @@ class InventoryScreen extends StatelessWidget {
if (oldItem != null)
Text(
"Replaces ${oldItem.name}",
style: const TextStyle(fontSize: 12, color: ThemeConfig.textColorGrey),
style: const TextStyle(
fontSize: 12,
color: ThemeConfig.textColorGrey,
),
),
const SizedBox(height: 16),
_buildStatChangeRow("Max HP", currentMaxHp, newMaxHp),
@@ -575,7 +613,9 @@ class InventoryScreen extends StatelessWidget {
int diff = newVal - oldVal;
Color color = diff > 0
? ThemeConfig.statDiffPositive
: (diff < 0 ? ThemeConfig.statDiffNegative : ThemeConfig.statDiffNeutral);
: (diff < 0
? ThemeConfig.statDiffNegative
: ThemeConfig.statDiffNeutral);
String diffText = diff > 0 ? "(+$diff)" : (diff < 0 ? "($diff)" : "");
return Padding(
@@ -586,8 +626,15 @@ class InventoryScreen extends StatelessWidget {
Text(label),
Row(
children: [
Text("$oldVal", style: const TextStyle(color: ThemeConfig.textColorGrey)),
const Icon(Icons.arrow_right, size: 16, color: ThemeConfig.textColorGrey),
Text(
"$oldVal",
style: const TextStyle(color: ThemeConfig.textColorGrey),
),
const Icon(
Icons.arrow_right,
size: 16,
color: ThemeConfig.textColorGrey,
),
Text(
"$newVal",
style: const TextStyle(fontWeight: ThemeConfig.fontWeightBold),
@@ -627,7 +674,10 @@ class InventoryScreen extends StatelessWidget {
padding: const EdgeInsets.only(top: 2.0, bottom: 2.0),
child: Text(
stats.join(", "),
style: const TextStyle(fontSize: ThemeConfig.fontSizeSmall, color: ThemeConfig.statAtkColor),
style: const TextStyle(
fontSize: ThemeConfig.fontSizeSmall,
color: ThemeConfig.statAtkColor,
),
textAlign: TextAlign.center,
),
),
@@ -636,7 +686,10 @@ class InventoryScreen extends StatelessWidget {
padding: const EdgeInsets.only(bottom: 2.0),
child: Text(
effectTexts.join("\n"),
style: const TextStyle(fontSize: ThemeConfig.fontSizeTiny, color: ThemeConfig.rarityLegendary),
style: const TextStyle(
fontSize: ThemeConfig.fontSizeTiny,
color: ThemeConfig.rarityLegendary,
),
),
),
],
+2
View File
@@ -5,6 +5,8 @@ import '../game/config/theme_config.dart';
class ItemUtils {
static Color getRarityColor(ItemRarity rarity) {
switch (rarity) {
case ItemRarity.normal:
return ThemeConfig.rarityNormal;
case ItemRarity.magic:
return ThemeConfig.rarityMagic;
case ItemRarity.rare:
+44 -19
View File
@@ -21,18 +21,6 @@ class ShopUI extends StatelessWidget {
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),
@@ -139,6 +127,7 @@ class ShopUI extends StatelessWidget {
width: 48,
height: 48,
fit: BoxFit.contain,
filterQuality: FilterQuality.high,
),
),
),
@@ -154,7 +143,8 @@ class ShopUI extends StatelessWidget {
color: ItemUtils.getRarityColor(
item.rarity,
),
fontSize: ThemeConfig.fontSizeMedium,
fontSize:
ThemeConfig.fontSizeMedium,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
@@ -215,10 +205,20 @@ class ShopUI extends StatelessWidget {
),
),
onPressed: player.gold >= GameConfig.shopRerollCost
? () => shopProvider.rerollShopItems(
player,
battleProvider.stage,
)
? () {
bool success = shopProvider.rerollShopItems(
player,
battleProvider.stage,
);
if (!success) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Not enough gold to reroll!"),
backgroundColor: Colors.red,
),
);
}
}
: null,
icon: const Icon(
Icons.refresh,
@@ -287,8 +287,33 @@ class ShopUI extends StatelessWidget {
backgroundColor: ThemeConfig.statGoldColor,
),
onPressed: () {
shopProvider.buyItem(item, player);
Navigator.pop(ctx);
bool success = shopProvider.buyItem(item, player);
Navigator.pop(ctx); // Close dialog first
if (success) {
// Refresh BattleProvider to update UI (Gold, Inventory) since player object is owned by BattleProvider
// and ShopProvider modifies it directly without BattleProvider knowing.
// Ideally, ShopProvider should notify, but since we don't have a direct link back or a shared PlayerProvider,
// we trigger it from the UI.
// Alternatively, we could add refreshUI to BattleProvider.
// Assuming BattleProvider has refreshUI or we can just use notifyListeners if we had access, but we don't.
// Wait, we have battleProvider instance passed to ShopUI.
battleProvider.refreshUI();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Bought ${item.name}"),
backgroundColor: Colors.green,
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(shopProvider.lastShopMessage),
backgroundColor: Colors.red,
),
);
}
},
child: const Text("Buy", style: TextStyle(color: Colors.black)),
),