update
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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 }
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user