update
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
import '../enums.dart';
|
||||
|
||||
class ItemConfig {
|
||||
/// Default weights for item rarity generation.
|
||||
/// 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.unique: 1,
|
||||
};
|
||||
}
|
||||
@@ -33,4 +33,10 @@ class ThemeConfig {
|
||||
// Status Effect Colors
|
||||
static const Color effectBg = Colors.deepOrange;
|
||||
static const Color effectText = Colors.white;
|
||||
|
||||
// Rarity Colors
|
||||
static const Color rarityMagic = Colors.blueAccent;
|
||||
static const Color rarityRare = Colors.yellow;
|
||||
static const Color rarityLegendary = Colors.orange;
|
||||
static const Color rarityUnique = Colors.purple;
|
||||
}
|
||||
|
||||
@@ -33,16 +33,14 @@ class EnemyTemplate {
|
||||
}
|
||||
|
||||
Character createCharacter({int stage = 1}) {
|
||||
// Simple additive scaling
|
||||
int scaledHp = baseHp + (stage - 1) * 5;
|
||||
int scaledAtk = baseAtk + (stage - 1);
|
||||
int scaledDefense = baseDefense + (stage ~/ 5); // +1 defense every 5 stages
|
||||
// Stage-based scaling for enemy stats is removed to simplify balancing.
|
||||
// Enemy stats are now fixed as defined in the EnemyTemplate.
|
||||
|
||||
final character = Character(
|
||||
name: name,
|
||||
maxHp: scaledHp,
|
||||
atk: scaledAtk,
|
||||
baseDefense: scaledDefense,
|
||||
maxHp: baseHp,
|
||||
atk: baseAtk,
|
||||
baseDefense: baseDefense,
|
||||
armor: 0,
|
||||
image: image,
|
||||
);
|
||||
@@ -51,9 +49,8 @@ class EnemyTemplate {
|
||||
for (final itemId in equipmentIds) {
|
||||
final itemTemplate = ItemTable.get(itemId);
|
||||
if (itemTemplate != null) {
|
||||
// Create item scaled to stage (optional, currently stage 1)
|
||||
// Enemies might get stronger items at higher stages
|
||||
final item = itemTemplate.createItem(stage: stage);
|
||||
// Items no longer scale by stage, pass no stage parameter
|
||||
final item = itemTemplate.createItem();
|
||||
character.addToInventory(item);
|
||||
character.equip(item);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import '../enums.dart';
|
||||
|
||||
class ItemModifier {
|
||||
final String prefix;
|
||||
final Map<StatType, int> statChanges;
|
||||
final List<EquipmentSlot>? allowedSlots; // Null means allowed for all slots
|
||||
|
||||
const ItemModifier({
|
||||
required this.prefix,
|
||||
required this.statChanges,
|
||||
this.allowedSlots,
|
||||
});
|
||||
}
|
||||
|
||||
class ItemPrefixTable {
|
||||
static const List<ItemModifier> magicPrefixes = [
|
||||
// Weapons
|
||||
ItemModifier(
|
||||
prefix: "Sharp",
|
||||
statChanges: {StatType.atk: 2},
|
||||
allowedSlots: [EquipmentSlot.weapon, EquipmentSlot.accessory]
|
||||
),
|
||||
ItemModifier(
|
||||
prefix: "Heavy",
|
||||
statChanges: {StatType.atk: 3},
|
||||
allowedSlots: [EquipmentSlot.weapon]
|
||||
),
|
||||
ItemModifier(
|
||||
prefix: "Swift",
|
||||
statChanges: {StatType.atk: 1},
|
||||
allowedSlots: [EquipmentSlot.weapon, EquipmentSlot.accessory]
|
||||
),
|
||||
|
||||
// Armor / Shield
|
||||
ItemModifier(
|
||||
prefix: "Sturdy",
|
||||
statChanges: {StatType.maxHp: 10},
|
||||
allowedSlots: [EquipmentSlot.armor, EquipmentSlot.shield, EquipmentSlot.accessory]
|
||||
),
|
||||
ItemModifier(
|
||||
prefix: "Hard",
|
||||
statChanges: {StatType.defense: 1},
|
||||
allowedSlots: [EquipmentSlot.armor, EquipmentSlot.shield]
|
||||
),
|
||||
|
||||
// General / Accessory
|
||||
ItemModifier(
|
||||
prefix: "Lucky",
|
||||
statChanges: {StatType.luck: 5},
|
||||
allowedSlots: null // All slots (e.g. Lucky Sword is fine)
|
||||
),
|
||||
];
|
||||
|
||||
static const List<ItemModifier> rarePrefixes = [
|
||||
// Offensive (Weapons/Accessories)
|
||||
ItemModifier(
|
||||
prefix: "Deadly",
|
||||
statChanges: {StatType.atk: 5, StatType.luck: 5},
|
||||
allowedSlots: [EquipmentSlot.weapon, EquipmentSlot.accessory]
|
||||
),
|
||||
ItemModifier(
|
||||
prefix: "Vicious",
|
||||
statChanges: {StatType.atk: 6, StatType.maxHp: -5},
|
||||
allowedSlots: [EquipmentSlot.weapon]
|
||||
),
|
||||
ItemModifier(
|
||||
prefix: "Brutal",
|
||||
statChanges: {StatType.atk: 8},
|
||||
allowedSlots: [EquipmentSlot.weapon]
|
||||
),
|
||||
|
||||
// Defensive (Armor/Shields/Accessories)
|
||||
ItemModifier(
|
||||
prefix: "Guardian's",
|
||||
statChanges: {StatType.defense: 3, StatType.maxHp: 20},
|
||||
allowedSlots: [EquipmentSlot.armor, EquipmentSlot.shield, EquipmentSlot.accessory]
|
||||
),
|
||||
ItemModifier(
|
||||
prefix: "Ancient",
|
||||
statChanges: {StatType.defense: 2, StatType.maxHp: 15},
|
||||
allowedSlots: [EquipmentSlot.armor, EquipmentSlot.shield]
|
||||
),
|
||||
ItemModifier(
|
||||
prefix: "Divine",
|
||||
statChanges: {StatType.defense: 2, StatType.maxHp: 30, StatType.luck: 5},
|
||||
allowedSlots: [EquipmentSlot.armor, EquipmentSlot.shield, EquipmentSlot.accessory]
|
||||
),
|
||||
|
||||
// Versatile
|
||||
ItemModifier(
|
||||
prefix: "Heroic",
|
||||
statChanges: {StatType.atk: 2, StatType.defense: 2, StatType.maxHp: 10},
|
||||
allowedSlots: null
|
||||
),
|
||||
];
|
||||
|
||||
// Special names for Rare/Unique replacements (Optional usage)
|
||||
static const List<String> rareNames = [
|
||||
"Earthshatter", "Soulrender", "Widowmaker", "Lightbringer"
|
||||
];
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../model/item.dart';
|
||||
import '../enums.dart';
|
||||
import '../config/item_config.dart';
|
||||
import 'item_prefix_table.dart'; // Import prefix table
|
||||
import 'name_generator.dart'; // Import name generator
|
||||
|
||||
class ItemTemplate {
|
||||
final String id;
|
||||
@@ -15,6 +19,8 @@ class ItemTemplate {
|
||||
final int price;
|
||||
final String? image;
|
||||
final int luck;
|
||||
final ItemRarity rarity;
|
||||
final ItemTier tier;
|
||||
|
||||
const ItemTemplate({
|
||||
required this.id,
|
||||
@@ -28,6 +34,8 @@ class ItemTemplate {
|
||||
required this.price,
|
||||
this.image,
|
||||
this.luck = 0,
|
||||
this.rarity = ItemRarity.magic,
|
||||
this.tier = ItemTier.tier1,
|
||||
});
|
||||
|
||||
factory ItemTemplate.fromJson(Map<String, dynamic> json) {
|
||||
@@ -50,27 +58,99 @@ class ItemTemplate {
|
||||
price: json['price'] ?? 10,
|
||||
image: json['image'],
|
||||
luck: json['luck'] ?? 0,
|
||||
rarity: json['rarity'] != null
|
||||
? ItemRarity.values.firstWhere((e) => e.name == json['rarity'])
|
||||
: ItemRarity.magic,
|
||||
tier: json['tier'] != null
|
||||
? ItemTier.values.firstWhere((e) => e.name == json['tier'])
|
||||
: ItemTier.tier1,
|
||||
);
|
||||
}
|
||||
|
||||
Item createItem({int stage = 1}) {
|
||||
// Scale stats based on stage
|
||||
int scaledAtk = (atkBonus * (1 + (stage - 1) * 0.1)).toInt();
|
||||
int scaledHp = (hpBonus * (1 + (stage - 1) * 0.1)).toInt();
|
||||
int scaledArmor = (armorBonus * (1 + (stage - 1) * 0.1)).toInt();
|
||||
// Stage-based scaling is removed.
|
||||
// Apply Prefix Logic based on Rarity.
|
||||
|
||||
String finalName = name;
|
||||
int finalAtk = atkBonus;
|
||||
int finalHp = hpBonus;
|
||||
int finalArmor = armorBonus;
|
||||
int finalLuck = luck;
|
||||
|
||||
final random = Random();
|
||||
|
||||
// 1. Magic Rarity: 50% chance to get a Magic Prefix (1 stat change)
|
||||
if (rarity == ItemRarity.magic) {
|
||||
if (random.nextBool()) { // 50% chance
|
||||
// Filter valid prefixes for this slot
|
||||
final validPrefixes = ItemPrefixTable.magicPrefixes.where((p) {
|
||||
return p.allowedSlots == null || p.allowedSlots!.contains(slot);
|
||||
}).toList();
|
||||
|
||||
if (validPrefixes.isNotEmpty) {
|
||||
final modifier = validPrefixes[random.nextInt(validPrefixes.length)];
|
||||
finalName = "${modifier.prefix} $name";
|
||||
|
||||
modifier.statChanges.forEach((stat, value) {
|
||||
switch(stat) {
|
||||
case StatType.atk: finalAtk += value; break;
|
||||
case StatType.maxHp: finalHp += value; break;
|
||||
case StatType.defense: finalArmor += value; break;
|
||||
case StatType.luck: finalLuck += value; break;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// 2. Rare Rarity: Always get a Rare Prefix (2 stat changes or stronger)
|
||||
else if (rarity == ItemRarity.rare) {
|
||||
bool nameChanged = false;
|
||||
|
||||
// Always generate a completely new cool name for Rare items
|
||||
finalName = NameGenerator.generateName(slot);
|
||||
nameChanged = true;
|
||||
|
||||
// Filter valid prefixes for this slot
|
||||
final validPrefixes = ItemPrefixTable.rarePrefixes.where((p) {
|
||||
return p.allowedSlots == null || p.allowedSlots!.contains(slot);
|
||||
}).toList();
|
||||
|
||||
if (validPrefixes.isNotEmpty) {
|
||||
final modifier = validPrefixes[random.nextInt(validPrefixes.length)];
|
||||
|
||||
// If name wasn't already changed by NameGenerator, apply prefix to name
|
||||
if (!nameChanged) {
|
||||
finalName = "${modifier.prefix} $name";
|
||||
}
|
||||
// Even if name changed, we STILL apply the stats from the prefix modifier!
|
||||
// Because NameGenerator is just visual flavor, stats come from the modifier.
|
||||
|
||||
modifier.statChanges.forEach((stat, value) {
|
||||
switch(stat) {
|
||||
case StatType.atk: finalAtk += value; break;
|
||||
case StatType.maxHp: finalHp += value; break;
|
||||
case StatType.defense: finalArmor += value; break;
|
||||
case StatType.luck: finalLuck += value; break;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// Legendary/Unique items usually keep their original names/stats as they are special.
|
||||
|
||||
return Item(
|
||||
id: id,
|
||||
name: "$name${stage > 1 ? ' +${stage - 1}' : ''}",
|
||||
name: finalName,
|
||||
description: description,
|
||||
atkBonus: scaledAtk,
|
||||
hpBonus: scaledHp,
|
||||
armorBonus: scaledArmor,
|
||||
atkBonus: finalAtk,
|
||||
hpBonus: finalHp,
|
||||
armorBonus: finalArmor,
|
||||
slot: slot,
|
||||
effects: effects,
|
||||
price: price,
|
||||
image: image,
|
||||
luck: luck,
|
||||
luck: finalLuck,
|
||||
rarity: rarity,
|
||||
tier: tier,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -115,4 +195,59 @@ class ItemTable {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static final Random _random = Random();
|
||||
|
||||
/// Returns all items matching the given tier.
|
||||
static List<ItemTemplate> getItemsByTier(ItemTier tier) {
|
||||
return allItems.where((item) => item.tier == tier).toList();
|
||||
}
|
||||
|
||||
/// Returns a random item based on Tier and Rarity weights.
|
||||
///
|
||||
/// [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.
|
||||
static ItemTemplate? getRandomItem({
|
||||
required ItemTier tier,
|
||||
EquipmentSlot? slot,
|
||||
Map<ItemRarity, int>? weights,
|
||||
}) {
|
||||
// 1. Filter by Tier and Slot (if provided)
|
||||
var candidates = allItems.where((item) => item.tier == tier);
|
||||
if (slot != null) {
|
||||
candidates = candidates.where((item) => item.slot == slot);
|
||||
}
|
||||
|
||||
if (candidates.isEmpty) return null;
|
||||
|
||||
// 2. Determine Target Rarity based on weights
|
||||
final rarityWeights = weights ?? ItemConfig.defaultRarityWeights;
|
||||
|
||||
int totalWeight = rarityWeights.values.fold(0, (sum, w) => sum + w);
|
||||
int roll = _random.nextInt(totalWeight);
|
||||
|
||||
ItemRarity? selectedRarity;
|
||||
int currentSum = 0;
|
||||
|
||||
for (var entry in rarityWeights.entries) {
|
||||
currentSum += entry.value;
|
||||
if (roll < currentSum) {
|
||||
selectedRarity = entry.key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 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
|
||||
if (rarityCandidates.isEmpty) {
|
||||
return candidates.toList()[_random.nextInt(candidates.length)];
|
||||
}
|
||||
|
||||
// 5. Pick random item
|
||||
return rarityCandidates[_random.nextInt(rarityCandidates.length)];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import 'dart:math';
|
||||
import '../enums.dart';
|
||||
|
||||
class NameGenerator {
|
||||
static final Random _random = Random();
|
||||
|
||||
// Adjectives suitable for powerful items
|
||||
static const List<String> _adjectives = [
|
||||
"Crimson", "Shadow", "Azure", "Burning", "Frozen", "Ancient",
|
||||
"Cursed", "Blessed", "Savage", "Eternal", "Dark", "Holy",
|
||||
"Storm", "Void", "Crystal", "Iron", "Blood", "Night"
|
||||
];
|
||||
|
||||
// Nouns specifically for Weapons
|
||||
static const List<String> _weaponNouns = [
|
||||
"Fang", "Claw", "Reaper", "Breaker", "Slayer", "Edge",
|
||||
"Blade", "Spike", "Crusher", "Whisper", "Howl", "Strike",
|
||||
"Bane", "Fury", "Vengeance", "Thorn"
|
||||
];
|
||||
|
||||
// Nouns specifically for Armor
|
||||
static const List<String> _armorNouns = [
|
||||
"Guard", "Wall", "Shelter", "Skin", "Scale", "Plate",
|
||||
"Bulwark", "Veil", "Shroud", "Ward", "Barrier", "Bastion",
|
||||
"Mantle", "Aegis", "Carapace"
|
||||
];
|
||||
|
||||
// Nouns specifically for Shields
|
||||
static const List<String> _shieldNouns = [
|
||||
"Wall", "Barrier", "Aegis", "Defender", "Blockade",
|
||||
"Resolve", "Sanctuary", "Buckler", "Tower", "Gate"
|
||||
];
|
||||
|
||||
// Nouns specifically for Accessories
|
||||
static const List<String> _accessoryNouns = [
|
||||
"Heart", "Soul", "Eye", "Tear", "Spark", "Ember",
|
||||
"Drop", "Mark", "Sign", "Omen", "Wish", "Star"
|
||||
];
|
||||
|
||||
static String generateName(EquipmentSlot slot) {
|
||||
String adjective = _adjectives[_random.nextInt(_adjectives.length)];
|
||||
String noun = "";
|
||||
|
||||
switch (slot) {
|
||||
case EquipmentSlot.weapon:
|
||||
noun = _weaponNouns[_random.nextInt(_weaponNouns.length)];
|
||||
break;
|
||||
case EquipmentSlot.armor:
|
||||
noun = _armorNouns[_random.nextInt(_armorNouns.length)];
|
||||
break;
|
||||
case EquipmentSlot.shield:
|
||||
noun = _shieldNouns[_random.nextInt(_shieldNouns.length)];
|
||||
break;
|
||||
case EquipmentSlot.accessory:
|
||||
noun = _accessoryNouns[_random.nextInt(_accessoryNouns.length)];
|
||||
break;
|
||||
}
|
||||
|
||||
// 20% Chance for "Noun of Noun" format (e.g. "Fang of Shadow")
|
||||
if (_random.nextDouble() < 0.2) {
|
||||
String suffixNoun = _adjectives[_random.nextInt(_adjectives.length)]; // Reuse adjectives as nouns sometimes works (e.g. "of Blood")
|
||||
// Better: use a subset of adjectives that work as nouns or generic nouns
|
||||
return "$noun of $suffixNoun";
|
||||
}
|
||||
|
||||
return "$adjective $noun";
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -33,4 +33,8 @@ enum EquipmentSlot { weapon, armor, shield, accessory }
|
||||
|
||||
enum DamageType { normal, bleed, vulnerable }
|
||||
|
||||
enum StatType { maxHp, atk, defense }
|
||||
enum StatType { maxHp, atk, defense, luck }
|
||||
|
||||
enum ItemRarity { magic, rare, legendary, unique }
|
||||
|
||||
enum ItemTier { tier1, tier2, tier3 }
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'item.dart';
|
||||
import 'status_effect.dart';
|
||||
import 'stat_modifier.dart';
|
||||
import '../enums.dart';
|
||||
import '../data/item_table.dart';
|
||||
|
||||
class Character {
|
||||
String name;
|
||||
@@ -36,6 +37,77 @@ class Character {
|
||||
baseAtk = atk,
|
||||
hp = hp ?? maxHp;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'name': name,
|
||||
'hp': hp,
|
||||
'baseMaxHp': baseMaxHp,
|
||||
'armor': armor,
|
||||
'baseAtk': baseAtk,
|
||||
'baseDefense': baseDefense,
|
||||
'gold': gold,
|
||||
'image': image,
|
||||
'equipment': equipment.map(
|
||||
(key, value) => MapEntry(key.name, value.id),
|
||||
),
|
||||
'inventory': inventory.map((e) => e.id).toList(),
|
||||
'statusEffects': statusEffects.map((e) => e.toJson()).toList(),
|
||||
'permanentModifiers': permanentModifiers.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
factory Character.fromJson(Map<String, dynamic> json) {
|
||||
final char = Character(
|
||||
name: json['name'],
|
||||
hp: json['hp'],
|
||||
maxHp: json['baseMaxHp'],
|
||||
armor: json['armor'],
|
||||
atk: json['baseAtk'],
|
||||
baseDefense: json['baseDefense'],
|
||||
gold: json['gold'],
|
||||
image: json['image'],
|
||||
);
|
||||
|
||||
// Restore Equipment
|
||||
final equipMap = json['equipment'] as Map<String, dynamic>;
|
||||
equipMap.forEach((slotName, itemId) {
|
||||
final template = ItemTable.get(itemId);
|
||||
if (template != null) {
|
||||
// Find slot enum
|
||||
final slot = EquipmentSlot.values.firstWhere(
|
||||
(e) => e.name == slotName,
|
||||
orElse: () => EquipmentSlot.weapon, // Fallback
|
||||
);
|
||||
char.equipment[slot] = template.createItem();
|
||||
}
|
||||
});
|
||||
|
||||
// Restore Inventory
|
||||
final invList = json['inventory'] as List<dynamic>;
|
||||
for (var itemId in invList) {
|
||||
final template = ItemTable.get(itemId);
|
||||
if (template != null) {
|
||||
char.inventory.add(template.createItem());
|
||||
}
|
||||
}
|
||||
|
||||
// Restore Status Effects
|
||||
if (json['statusEffects'] != null) {
|
||||
char.statusEffects = (json['statusEffects'] as List)
|
||||
.map((e) => StatusEffect.fromJson(e))
|
||||
.toList();
|
||||
}
|
||||
|
||||
// Restore Permanent Modifiers
|
||||
if (json['permanentModifiers'] != null) {
|
||||
char.permanentModifiers = (json['permanentModifiers'] as List)
|
||||
.map((e) => PermanentStatModifier.fromJson(e))
|
||||
.toList();
|
||||
}
|
||||
|
||||
return char;
|
||||
}
|
||||
|
||||
/// Adds a status effect. If it already exists, it refreshes duration or stacks based on logic.
|
||||
/// For now, we'll implement a simple refresh/overwrite logic.
|
||||
void addStatusEffect(StatusEffect newEffect) {
|
||||
|
||||
@@ -47,6 +47,8 @@ class Item {
|
||||
final int price; // New: Sell/Buy value
|
||||
final String? image; // New: Image path
|
||||
final int luck; // Success rate bonus (e.g. 5 = 5%)
|
||||
final ItemRarity rarity;
|
||||
final ItemTier tier;
|
||||
|
||||
const Item({
|
||||
required this.id,
|
||||
@@ -60,6 +62,8 @@ class Item {
|
||||
this.price = 0,
|
||||
this.image,
|
||||
this.luck = 0,
|
||||
this.rarity = ItemRarity.magic,
|
||||
this.tier = ItemTier.tier1,
|
||||
});
|
||||
|
||||
String get typeName {
|
||||
|
||||
@@ -6,4 +6,20 @@ class StatusEffect {
|
||||
final int value; // Intensity (e.g., bleed damage amount)
|
||||
|
||||
StatusEffect({required this.type, required this.duration, this.value = 0});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'type': type.name,
|
||||
'duration': duration,
|
||||
'value': value,
|
||||
};
|
||||
}
|
||||
|
||||
factory StatusEffect.fromJson(Map<String, dynamic> json) {
|
||||
return StatusEffect(
|
||||
type: StatusEffectType.values.firstWhere((e) => e.name == json['type']),
|
||||
duration: json['duration'],
|
||||
value: json['value'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import 'dart:convert';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../providers/battle_provider.dart';
|
||||
import 'model/entity.dart';
|
||||
|
||||
class SaveManager {
|
||||
static const String _saveKey = 'game_save_data';
|
||||
|
||||
static Future<void> saveGame(BattleProvider provider) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
final saveData = {
|
||||
'stage': provider.stage,
|
||||
'turnCount': provider.turnCount,
|
||||
'player': provider.player.toJson(),
|
||||
'timestamp': DateTime.now().toIso8601String(),
|
||||
};
|
||||
|
||||
await prefs.setString(_saveKey, jsonEncode(saveData));
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>?> loadGame() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (!prefs.containsKey(_saveKey)) return null;
|
||||
|
||||
final jsonStr = prefs.getString(_saveKey);
|
||||
if (jsonStr == null) return null;
|
||||
|
||||
try {
|
||||
return jsonDecode(jsonStr);
|
||||
} catch (e) {
|
||||
print("Error loading save data: $e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<bool> hasSaveData() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.containsKey(_saveKey);
|
||||
}
|
||||
|
||||
static Future<void> clearSaveData() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_saveKey);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,8 @@ 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';
|
||||
|
||||
class EnemyIntent {
|
||||
final EnemyActionType type;
|
||||
final int value;
|
||||
@@ -47,8 +49,10 @@ class BattleProvider with ChangeNotifier {
|
||||
int turnCount = 1;
|
||||
List<Item> rewardOptions = [];
|
||||
bool showRewardPopup = false;
|
||||
int _lastGoldReward = 0; // New: Stores gold gained from last victory
|
||||
|
||||
List<String> get logs => battleLogs;
|
||||
int get lastGoldReward => _lastGoldReward;
|
||||
|
||||
// Damage Event Stream
|
||||
final _damageEventController = StreamController<DamageEvent>.broadcast();
|
||||
@@ -69,6 +73,18 @@ class BattleProvider with ChangeNotifier {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void loadFromSave(Map<String, dynamic> data) {
|
||||
stage = data['stage'];
|
||||
turnCount = data['turnCount'];
|
||||
player = Character.fromJson(data['player']);
|
||||
|
||||
battleLogs.clear();
|
||||
_addLog("Game Loaded! Resuming Stage $stage");
|
||||
|
||||
_prepareNextStage();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void initializeBattle() {
|
||||
stage = 1;
|
||||
turnCount = 1;
|
||||
@@ -87,6 +103,9 @@ class BattleProvider with ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
// Give test gold
|
||||
player.gold = 50;
|
||||
|
||||
// Provide starter equipment
|
||||
final starterSword = Item(
|
||||
id: "starter_sword",
|
||||
@@ -147,6 +166,9 @@ class BattleProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
void _prepareNextStage() {
|
||||
// Save Game at the start of each stage
|
||||
SaveManager.saveGame(this);
|
||||
|
||||
StageType type;
|
||||
|
||||
// Stage Type Logic
|
||||
@@ -209,15 +231,7 @@ class BattleProvider with ChangeNotifier {
|
||||
_addLog("Stage $stage ($type) started! A wild ${enemy.name} appeared.");
|
||||
} else if (type == StageType.shop) {
|
||||
// Generate random items for shop
|
||||
final random = Random();
|
||||
List<ItemTemplate> allTemplates = List.from(ItemTable.allItems);
|
||||
allTemplates.shuffle(random);
|
||||
|
||||
int count = min(4, allTemplates.length);
|
||||
shopItems = allTemplates
|
||||
.sublist(0, count)
|
||||
.map((t) => t.createItem(stage: stage))
|
||||
.toList();
|
||||
shopItems = _generateShopItems();
|
||||
|
||||
// Dummy enemy to prevent null errors in existing UI (until UI is fully updated)
|
||||
enemy = Character(name: "Merchant", maxHp: 9999, armor: 0, atk: 0);
|
||||
@@ -238,6 +252,55 @@ class BattleProvider with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Generate 4 random items for the shop based on current stage tier
|
||||
List<Item> _generateShopItems() {
|
||||
ItemTier currentTier = ItemTier.tier1;
|
||||
if (stage > 24)
|
||||
currentTier = ItemTier.tier3;
|
||||
else if (stage > 12)
|
||||
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 = 50;
|
||||
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!");
|
||||
}
|
||||
}
|
||||
|
||||
// Replaces _spawnEnemy
|
||||
// void _spawnEnemy() { ... } - Removed
|
||||
|
||||
@@ -621,28 +684,68 @@ class BattleProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
void _onVictory() {
|
||||
_addLog("Enemy defeated! Choose a reward.");
|
||||
|
||||
// Calculate Gold Reward
|
||||
// Base 10 + (Stage * 5) + Random variance
|
||||
final random = Random();
|
||||
int goldReward = 10 + (stage * 5) + random.nextInt(10);
|
||||
|
||||
player.gold += goldReward;
|
||||
_lastGoldReward = goldReward; // Store for UI display
|
||||
_addLog("Enemy defeated! Gained $goldReward Gold.");
|
||||
_addLog("Choose a reward.");
|
||||
|
||||
List<ItemTemplate> allTemplates = List.from(ItemTable.allItems);
|
||||
allTemplates.shuffle(random); // Shuffle to randomize selection
|
||||
|
||||
// Take first 3 items (ensure distinct templates if possible, though list is small now)
|
||||
int count = min(3, allTemplates.length);
|
||||
rewardOptions = allTemplates.sublist(0, count).map((template) {
|
||||
return template.createItem(stage: stage);
|
||||
}).toList();
|
||||
// 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 > 24)
|
||||
currentTier = ItemTier.tier3;
|
||||
else if (stage > 12)
|
||||
currentTier = ItemTier.tier2;
|
||||
|
||||
rewardOptions = [];
|
||||
// Get 3 distinct items if possible
|
||||
for (int i = 0; i < 3; i++) {
|
||||
ItemTemplate? item = ItemTable.getRandomItem(tier: currentTier);
|
||||
if (item != null) {
|
||||
rewardOptions.add(item.createItem(stage: stage));
|
||||
}
|
||||
}
|
||||
|
||||
// Add "None" (Skip) Option
|
||||
// We can represent "None" as a null or a special Item.
|
||||
// Using a special Item with ID "reward_skip" is safer for List<Item>.
|
||||
rewardOptions.add(
|
||||
Item(
|
||||
id: "reward_skip",
|
||||
name: "Skip Reward",
|
||||
description: "Take nothing and move on.",
|
||||
atkBonus: 0,
|
||||
hpBonus: 0,
|
||||
slot: EquipmentSlot.accessory,
|
||||
),
|
||||
);
|
||||
|
||||
showRewardPopup = true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void selectReward(Item item) {
|
||||
bool added = player.addToInventory(item);
|
||||
if (added) {
|
||||
_addLog("Added ${item.name} to inventory.");
|
||||
if (item.id == "reward_skip") {
|
||||
_addLog("Skipped reward.");
|
||||
} else {
|
||||
_addLog("Inventory is full! ${item.name} discarded.");
|
||||
bool added = player.addToInventory(item);
|
||||
if (added) {
|
||||
_addLog("Added ${item.name} to inventory.");
|
||||
} else {
|
||||
_addLog("Inventory is full! ${item.name} discarded.");
|
||||
}
|
||||
}
|
||||
|
||||
// Heal player after selecting reward
|
||||
|
||||
@@ -538,8 +538,29 @@ class _BattleScreenState extends State<BattleScreen> {
|
||||
color: Colors.black54,
|
||||
child: Center(
|
||||
child: SimpleDialog(
|
||||
title: const Text("Victory! Choose a Reward"),
|
||||
title: Row(
|
||||
children: [
|
||||
const Text("Victory! Choose a Reward"),
|
||||
const Spacer(),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.monetization_on, color: Colors.amber, size: 18),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
"${battleProvider.lastGoldReward} G",
|
||||
style: TextStyle(
|
||||
color: Colors.amber,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
children: battleProvider.rewardOptions.map((item) {
|
||||
bool isSkip = item.id == "reward_skip";
|
||||
return SimpleDialogOption(
|
||||
onPressed: () {
|
||||
battleProvider.selectReward(item);
|
||||
@@ -549,30 +570,43 @@ class _BattleScreenState extends State<BattleScreen> {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blueGrey[700],
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: Colors.grey),
|
||||
if (!isSkip)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blueGrey[700],
|
||||
borderRadius: BorderRadius.circular(
|
||||
4),
|
||||
border: Border.all(
|
||||
color: item.rarity !=
|
||||
ItemRarity.magic
|
||||
? ItemUtils.getRarityColor(
|
||||
item.rarity,
|
||||
)
|
||||
: Colors.grey,
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
ItemUtils.getIcon(item.slot),
|
||||
color: ItemUtils.getColor(item.slot),
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
ItemUtils.getIcon(item.slot),
|
||||
color: ItemUtils.getColor(item.slot),
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
if (!isSkip) const SizedBox(width: 12),
|
||||
Text(
|
||||
item.name,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: isSkip
|
||||
? Colors.grey
|
||||
: ItemUtils.getRarityColor(
|
||||
item.rarity),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
_buildItemStatText(item),
|
||||
if (!isSkip) _buildItemStatText(item),
|
||||
Text(
|
||||
item.description,
|
||||
style: const TextStyle(
|
||||
|
||||
@@ -93,6 +93,17 @@ class InventoryScreen extends StatelessWidget {
|
||||
color: item != null
|
||||
? Colors.blueGrey[600]
|
||||
: Colors.grey[800],
|
||||
shape: item != null &&
|
||||
item.rarity != ItemRarity.magic
|
||||
? RoundedRectangleBorder(
|
||||
side: BorderSide(
|
||||
color:
|
||||
ItemUtils.getRarityColor(item.rarity),
|
||||
width: 2.0,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
)
|
||||
: null,
|
||||
child: Stack(
|
||||
children: [
|
||||
// Slot Name (Top Right)
|
||||
@@ -143,7 +154,9 @@ class InventoryScreen extends StatelessWidget {
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: item != null
|
||||
? Colors.white
|
||||
? ItemUtils.getRarityColor(
|
||||
item.rarity,
|
||||
)
|
||||
: Colors.grey,
|
||||
),
|
||||
maxLines: 2,
|
||||
@@ -206,6 +219,17 @@ class InventoryScreen extends StatelessWidget {
|
||||
},
|
||||
child: Card(
|
||||
color: Colors.blueGrey[700],
|
||||
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: [
|
||||
// Faded Icon in Top-Left
|
||||
@@ -233,9 +257,12 @@ class InventoryScreen extends StatelessWidget {
|
||||
child: Text(
|
||||
item.name,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: ItemUtils.getRarityColor(
|
||||
item.rarity,
|
||||
),
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
|
||||
@@ -1,10 +1,54 @@
|
||||
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 '../game/save_manager.dart';
|
||||
import '../providers/battle_provider.dart';
|
||||
|
||||
class MainMenuScreen extends StatelessWidget {
|
||||
class MainMenuScreen extends StatefulWidget {
|
||||
const MainMenuScreen({super.key});
|
||||
|
||||
@override
|
||||
State<MainMenuScreen> createState() => _MainMenuScreenState();
|
||||
}
|
||||
|
||||
class _MainMenuScreenState extends State<MainMenuScreen> {
|
||||
bool _hasSave = false;
|
||||
bool _isLoading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_checkSaveData();
|
||||
}
|
||||
|
||||
Future<void> _checkSaveData() async {
|
||||
final hasSave = await SaveManager.hasSaveData();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_hasSave = hasSave;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _continueGame() async {
|
||||
final data = await SaveManager.loadGame();
|
||||
if (data != null && mounted) {
|
||||
context.read<BattleProvider>().loadFromSave(data);
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const MainWrapper()),
|
||||
);
|
||||
} else {
|
||||
// Handle load error
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Failed to load save data.')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -18,55 +62,78 @@ class MainMenuScreen extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
child: ResponsiveContainer(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.gavel, size: 100, color: Colors.amber),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
"COLOSSEUM'S CHOICE",
|
||||
style: TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 2.0,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Text(
|
||||
"Rise as a Legend",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 60),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const CharacterSelectionScreen(),
|
||||
child: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.gavel, size: 100, color: Colors.amber),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
"COLOSSEUM'S CHOICE",
|
||||
style: TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 2.0,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 50,
|
||||
vertical: 15,
|
||||
),
|
||||
backgroundColor: Colors.amber[700],
|
||||
foregroundColor: Colors.black,
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Text(
|
||||
"Rise as a Legend",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 60),
|
||||
if (_hasSave) ...[
|
||||
ElevatedButton(
|
||||
onPressed: _continueGame,
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 50,
|
||||
vertical: 15,
|
||||
),
|
||||
backgroundColor: Colors.blueAccent,
|
||||
foregroundColor: Colors.white,
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
child: const Text("CONTINUE"),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
// Warn if save exists? Or just overwrite on save.
|
||||
// For now, simpler flow.
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const CharacterSelectionScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 50,
|
||||
vertical: 15,
|
||||
),
|
||||
backgroundColor: Colors.amber[700],
|
||||
foregroundColor: Colors.black,
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
child: const Text("NEW GAME"),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Text("START GAME"),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'battle_screen.dart';
|
||||
import 'inventory_screen.dart';
|
||||
import 'settings_screen.dart';
|
||||
import '../widgets/responsive_container.dart';
|
||||
|
||||
class MainWrapper extends StatefulWidget {
|
||||
@@ -13,7 +14,11 @@ class MainWrapper extends StatefulWidget {
|
||||
class _MainWrapperState extends State<MainWrapper> {
|
||||
int _currentIndex = 0;
|
||||
|
||||
final List<Widget> _screens = [const BattleScreen(), const InventoryScreen()];
|
||||
final List<Widget> _screens = [
|
||||
const BattleScreen(),
|
||||
const InventoryScreen(),
|
||||
const SettingsScreen(),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -39,6 +44,10 @@ class _MainWrapperState extends State<MainWrapper> {
|
||||
icon: Icon(Icons.backpack),
|
||||
label: 'Inventory',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.settings),
|
||||
label: 'Settings',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/battle_provider.dart';
|
||||
import 'main_menu_screen.dart';
|
||||
|
||||
class SettingsScreen extends StatelessWidget {
|
||||
const SettingsScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text(
|
||||
'Settings',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
// Placeholder for future settings
|
||||
const Text(
|
||||
'Effect Intensity: Normal',
|
||||
style: TextStyle(color: Colors.white70),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
'Volume: 100%',
|
||||
style: TextStyle(color: Colors.white70),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
|
||||
// Restart Button
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.orange,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12),
|
||||
),
|
||||
onPressed: () {
|
||||
_showConfirmationDialog(
|
||||
context,
|
||||
title: 'Restart Game?',
|
||||
content: 'All progress will be lost. Are you sure?',
|
||||
onConfirm: () {
|
||||
context.read<BattleProvider>().initializeBattle();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Game Restarted!')),
|
||||
);
|
||||
// Optionally switch tab back to Battle (index 0)
|
||||
// But MainWrapper controls the index.
|
||||
// We can't easily switch tab from here without a callback or Provider.
|
||||
// For now, just restart logic is enough.
|
||||
},
|
||||
);
|
||||
},
|
||||
child: const Text('Restart Game'),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Return to Main Menu Button
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12),
|
||||
),
|
||||
onPressed: () {
|
||||
_showConfirmationDialog(
|
||||
context,
|
||||
title: 'Return to Main Menu?',
|
||||
content: 'Unsaved progress may be lost. (Progress is saved automatically after each stage)',
|
||||
onConfirm: () {
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(builder: (context) => const MainMenuScreen()),
|
||||
(route) => false,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: const Text('Return to Main Menu'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showConfirmationDialog(BuildContext context, {required String title, required String content, required VoidCallback onConfirm}) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(title),
|
||||
content: Text(content),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
onConfirm();
|
||||
},
|
||||
child: const Text('Confirm', style: TextStyle(color: Colors.red)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,21 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../game/enums.dart';
|
||||
import '../game/config/theme_config.dart';
|
||||
|
||||
class ItemUtils {
|
||||
static Color getRarityColor(ItemRarity rarity) {
|
||||
switch (rarity) {
|
||||
case ItemRarity.magic:
|
||||
return ThemeConfig.rarityMagic;
|
||||
case ItemRarity.rare:
|
||||
return ThemeConfig.rarityRare;
|
||||
case ItemRarity.legendary:
|
||||
return ThemeConfig.rarityLegendary;
|
||||
case ItemRarity.unique:
|
||||
return ThemeConfig.rarityUnique;
|
||||
}
|
||||
}
|
||||
|
||||
static IconData getIcon(EquipmentSlot slot) {
|
||||
switch (slot) {
|
||||
case EquipmentSlot.weapon:
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../providers/battle_provider.dart';
|
||||
import '../../game/model/item.dart';
|
||||
import '../../utils/item_utils.dart';
|
||||
import '../../game/enums.dart';
|
||||
|
||||
class ShopUI extends StatelessWidget {
|
||||
final BattleProvider battleProvider;
|
||||
@@ -8,24 +11,238 @@ class ShopUI extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
final player = battleProvider.player;
|
||||
final shopItems = battleProvider.currentStage.shopItems;
|
||||
|
||||
return Container(
|
||||
color: Colors.black87,
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.store, size: 64, color: Colors.amber),
|
||||
// Header: Merchant Icon & Player Gold
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Row(
|
||||
children: [
|
||||
Icon(Icons.store, size: 32, color: Colors.amber),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
"Merchant",
|
||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.monetization_on, color: Colors.amber),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
"${player.gold} G",
|
||||
style: const TextStyle(
|
||||
color: Colors.amber,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(color: Colors.grey),
|
||||
const SizedBox(height: 16),
|
||||
const Text("Merchant Shop", style: TextStyle(fontSize: 24)),
|
||||
const SizedBox(height: 8),
|
||||
const Text("Buying/Selling feature coming soon!"),
|
||||
const SizedBox(height: 32),
|
||||
ElevatedButton(
|
||||
onPressed: () => battleProvider.proceedToNextStage(),
|
||||
child: const Text("Leave Shop"),
|
||||
|
||||
// Shop Items Grid
|
||||
Expanded(
|
||||
child: shopItems.isEmpty
|
||||
? const Center(
|
||||
child: Text(
|
||||
"Sold Out",
|
||||
style: TextStyle(color: Colors.grey, 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: Colors.blueGrey[800],
|
||||
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 ? Colors.amber : Colors.grey,
|
||||
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: Colors.blueGrey,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
),
|
||||
onPressed: player.gold >= 50
|
||||
? () => battleProvider.rerollShopItems()
|
||||
: null,
|
||||
icon: const Icon(Icons.refresh, color: Colors.white),
|
||||
label: const Text(
|
||||
"Reroll (50 G)",
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.redAccent,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
),
|
||||
onPressed: () => battleProvider.proceedToNextStage(),
|
||||
icon: const Icon(Icons.exit_to_app, color: Colors.white),
|
||||
label: const Text(
|
||||
"Leave Shop",
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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: Colors.amber),
|
||||
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: Colors.orangeAccent),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RestUI extends StatelessWidget {
|
||||
|
||||
Reference in New Issue
Block a user