update
This commit is contained in:
parent
d5609aff0f
commit
dcfb8ab9de
|
|
@ -6,7 +6,8 @@
|
||||||
"baseAtk": 5,
|
"baseAtk": 5,
|
||||||
"baseDefense": 5,
|
"baseDefense": 5,
|
||||||
"image": "assets/images/enemies/goblin.png",
|
"image": "assets/images/enemies/goblin.png",
|
||||||
"equipment": ["rusty_dagger"]
|
"equipment": ["rusty_dagger"],
|
||||||
|
"tier": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Slime",
|
"name": "Slime",
|
||||||
|
|
@ -14,7 +15,8 @@
|
||||||
"baseAtk": 3,
|
"baseAtk": 3,
|
||||||
"baseDefense": 5,
|
"baseDefense": 5,
|
||||||
"image": "assets/images/enemies/slime.png",
|
"image": "assets/images/enemies/slime.png",
|
||||||
"equipment": ["rusty_dagger"]
|
"equipment": ["rusty_dagger"],
|
||||||
|
"tier": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Wolf",
|
"name": "Wolf",
|
||||||
|
|
@ -22,7 +24,8 @@
|
||||||
"baseAtk": 7,
|
"baseAtk": 7,
|
||||||
"baseDefense": 5,
|
"baseDefense": 5,
|
||||||
"image": "assets/images/enemies/wolf.png",
|
"image": "assets/images/enemies/wolf.png",
|
||||||
"equipment": ["rusty_dagger"]
|
"equipment": ["rusty_dagger"],
|
||||||
|
"tier": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Bandit",
|
"name": "Bandit",
|
||||||
|
|
@ -30,7 +33,8 @@
|
||||||
"baseAtk": 6,
|
"baseAtk": 6,
|
||||||
"baseDefense": 5,
|
"baseDefense": 5,
|
||||||
"image": "assets/images/enemies/bandit.png",
|
"image": "assets/images/enemies/bandit.png",
|
||||||
"equipment": ["rusty_dagger"]
|
"equipment": ["rusty_dagger"],
|
||||||
|
"tier": 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Skeleton",
|
"name": "Skeleton",
|
||||||
|
|
@ -38,7 +42,26 @@
|
||||||
"baseAtk": 8,
|
"baseAtk": 8,
|
||||||
"baseDefense": 5,
|
"baseDefense": 5,
|
||||||
"image": "assets/images/enemies/skeleton.png",
|
"image": "assets/images/enemies/skeleton.png",
|
||||||
"equipment": ["rusty_dagger"]
|
"equipment": ["rusty_dagger"],
|
||||||
|
"tier": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Shadow Assassin",
|
||||||
|
"baseHp": 40,
|
||||||
|
"baseAtk": 10,
|
||||||
|
"baseDefense": 2,
|
||||||
|
"image": "assets/images/enemies/shadow_assassin.png",
|
||||||
|
"equipment": ["jagged_dagger"],
|
||||||
|
"tier": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Armored Bear",
|
||||||
|
"baseHp": 60,
|
||||||
|
"baseAtk": 8,
|
||||||
|
"baseDefense": 8,
|
||||||
|
"image": "assets/images/enemies/armored_bear.png",
|
||||||
|
"equipment": ["iron_sword"],
|
||||||
|
"tier": 3
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"elite": [
|
"elite": [
|
||||||
|
|
@ -48,7 +71,8 @@
|
||||||
"baseAtk": 12,
|
"baseAtk": 12,
|
||||||
"baseDefense": 3,
|
"baseDefense": 3,
|
||||||
"image": "assets/images/enemies/orc_warrior.png",
|
"image": "assets/images/enemies/orc_warrior.png",
|
||||||
"equipment": ["battle_axe", "leather_vest"]
|
"equipment": ["battle_axe", "leather_vest"],
|
||||||
|
"tier": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Giant Spider",
|
"name": "Giant Spider",
|
||||||
|
|
@ -56,7 +80,8 @@
|
||||||
"baseAtk": 15,
|
"baseAtk": 15,
|
||||||
"baseDefense": 2,
|
"baseDefense": 2,
|
||||||
"image": "assets/images/enemies/giant_spider.png",
|
"image": "assets/images/enemies/giant_spider.png",
|
||||||
"equipment": ["jagged_dagger"]
|
"equipment": ["jagged_dagger"],
|
||||||
|
"tier": 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Dark Knight",
|
"name": "Dark Knight",
|
||||||
|
|
@ -64,7 +89,8 @@
|
||||||
"baseAtk": 10,
|
"baseAtk": 10,
|
||||||
"baseDefense": 5,
|
"baseDefense": 5,
|
||||||
"image": "assets/images/enemies/dark_knight.png",
|
"image": "assets/images/enemies/dark_knight.png",
|
||||||
"equipment": ["stunning_hammer", "kite_shield"]
|
"equipment": ["stunning_hammer", "kite_shield"],
|
||||||
|
"tier": 3
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,27 @@
|
||||||
{
|
{
|
||||||
"weapons": [
|
"weapons": [
|
||||||
|
{
|
||||||
|
"id": "short_bow",
|
||||||
|
"name": "Short Bow",
|
||||||
|
"description": "A basic bow for beginners.",
|
||||||
|
"baseAtk": 2,
|
||||||
|
"slot": "weapon",
|
||||||
|
"price": 15,
|
||||||
|
"image": "assets/images/items/short_bow.png",
|
||||||
|
"rarity": "normal",
|
||||||
|
"tier": "tier1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "long_sword",
|
||||||
|
"name": "Long Sword",
|
||||||
|
"description": "A versatile blade.",
|
||||||
|
"baseAtk": 6,
|
||||||
|
"slot": "weapon",
|
||||||
|
"price": 50,
|
||||||
|
"image": "assets/images/items/long_sword.png",
|
||||||
|
"rarity": "normal",
|
||||||
|
"tier": "tier2"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "rusty_dagger",
|
"id": "rusty_dagger",
|
||||||
"name": "Rusty Dagger",
|
"name": "Rusty Dagger",
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,10 @@ class ItemConfig {
|
||||||
/// Used when selecting random items in Shop or Rewards.
|
/// Used when selecting random items in Shop or Rewards.
|
||||||
/// Higher weight = Higher chance.
|
/// Higher weight = Higher chance.
|
||||||
static const Map<ItemRarity, int> defaultRarityWeights = {
|
static const Map<ItemRarity, int> defaultRarityWeights = {
|
||||||
ItemRarity.magic: 60,
|
ItemRarity.normal: 50,
|
||||||
ItemRarity.rare: 30,
|
ItemRarity.magic: 30,
|
||||||
ItemRarity.legendary: 9,
|
ItemRarity.rare: 15,
|
||||||
|
ItemRarity.legendary: 4,
|
||||||
ItemRarity.unique: 1,
|
ItemRarity.unique: 1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ class ThemeConfig {
|
||||||
static const Color mainTitleColor = Colors.white;
|
static const Color mainTitleColor = Colors.white;
|
||||||
static const Color subTitleColor = Colors.grey;
|
static const Color subTitleColor = Colors.grey;
|
||||||
static const Color mainIconColor = Colors.amber;
|
static const Color mainIconColor = Colors.amber;
|
||||||
|
|
||||||
// Button Colors
|
// Button Colors
|
||||||
static const Color btnNewGameBg = Color(0xFFFFA000); // Colors.amber[700]
|
static const Color btnNewGameBg = Color(0xFFFFA000); // Colors.amber[700]
|
||||||
static const Color btnNewGameText = Colors.black;
|
static const Color btnNewGameText = Colors.black;
|
||||||
|
|
@ -37,14 +37,14 @@ class ThemeConfig {
|
||||||
static const Color btnDisabled = Colors.grey;
|
static const Color btnDisabled = Colors.grey;
|
||||||
static const Color btnRestartBg = Colors.orange;
|
static const Color btnRestartBg = Colors.orange;
|
||||||
static const Color btnReturnMenuBg = Colors.red;
|
static const Color btnReturnMenuBg = Colors.red;
|
||||||
|
|
||||||
// Stat Colors
|
// Stat Colors
|
||||||
static const Color statHpColor = Colors.red;
|
static const Color statHpColor = Colors.red;
|
||||||
static const Color statHpPlayerColor = Colors.green;
|
static const Color statHpPlayerColor = Colors.green;
|
||||||
static const Color statHpEnemyColor = Colors.red;
|
static const Color statHpEnemyColor = Colors.red;
|
||||||
static const Color statAtkColor = Colors.blueAccent;
|
static const Color statAtkColor = Colors.blueAccent;
|
||||||
static const Color statDefColor =
|
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 statLuckColor = Colors.green;
|
||||||
static const Color statGoldColor = Colors.amber;
|
static const Color statGoldColor = Colors.amber;
|
||||||
|
|
||||||
|
|
@ -82,6 +82,7 @@ class ThemeConfig {
|
||||||
static const Color effectText = Colors.white;
|
static const Color effectText = Colors.white;
|
||||||
|
|
||||||
// Rarity Colors
|
// Rarity Colors
|
||||||
|
static const Color rarityNormal = Colors.white;
|
||||||
static const Color rarityMagic = Colors.blueAccent;
|
static const Color rarityMagic = Colors.blueAccent;
|
||||||
static const Color rarityRare = Colors.yellow;
|
static const Color rarityRare = Colors.yellow;
|
||||||
static const Color rarityLegendary = Colors.orange;
|
static const Color rarityLegendary = Colors.orange;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:math';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import '../model/entity.dart';
|
import '../model/entity.dart';
|
||||||
|
import '../config/game_config.dart';
|
||||||
|
|
||||||
import 'item_table.dart';
|
import 'item_table.dart';
|
||||||
|
|
||||||
|
|
@ -11,6 +13,7 @@ class EnemyTemplate {
|
||||||
final int baseDefense;
|
final int baseDefense;
|
||||||
final String? image;
|
final String? image;
|
||||||
final List<String> equipmentIds;
|
final List<String> equipmentIds;
|
||||||
|
final int tier;
|
||||||
|
|
||||||
const EnemyTemplate({
|
const EnemyTemplate({
|
||||||
required this.name,
|
required this.name,
|
||||||
|
|
@ -19,6 +22,7 @@ class EnemyTemplate {
|
||||||
required this.baseDefense,
|
required this.baseDefense,
|
||||||
this.image,
|
this.image,
|
||||||
this.equipmentIds = const [],
|
this.equipmentIds = const [],
|
||||||
|
this.tier = 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory EnemyTemplate.fromJson(Map<String, dynamic> json) {
|
factory EnemyTemplate.fromJson(Map<String, dynamic> json) {
|
||||||
|
|
@ -29,6 +33,7 @@ class EnemyTemplate {
|
||||||
baseDefense: json['baseDefense'] ?? 0,
|
baseDefense: json['baseDefense'] ?? 0,
|
||||||
image: json['image'],
|
image: json['image'],
|
||||||
equipmentIds: (json['equipment'] as List<dynamic>?)?.cast<String>() ?? [],
|
equipmentIds: (json['equipment'] as List<dynamic>?)?.cast<String>() ?? [],
|
||||||
|
tier: json['tier'] ?? 1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,6 +68,7 @@ class EnemyTemplate {
|
||||||
class EnemyTable {
|
class EnemyTable {
|
||||||
static List<EnemyTemplate> normalEnemies = [];
|
static List<EnemyTemplate> normalEnemies = [];
|
||||||
static List<EnemyTemplate> eliteEnemies = [];
|
static List<EnemyTemplate> eliteEnemies = [];
|
||||||
|
static final Random _random = Random();
|
||||||
|
|
||||||
static Future<void> load() async {
|
static Future<void> load() async {
|
||||||
final String jsonString = await rootBundle.loadString(
|
final String jsonString = await rootBundle.loadString(
|
||||||
|
|
@ -77,4 +83,34 @@ class EnemyTable {
|
||||||
.map((e) => EnemyTemplate.fromJson(e))
|
.map((e) => EnemyTemplate.fromJson(e))
|
||||||
.toList();
|
.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 String prefix;
|
||||||
final Map<StatType, int> statChanges;
|
final Map<StatType, int> statChanges;
|
||||||
final List<EquipmentSlot>? allowedSlots; // Null means allowed for all slots
|
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({
|
const ItemModifier({
|
||||||
required this.prefix,
|
required this.prefix,
|
||||||
required this.statChanges,
|
this.statChanges = const {},
|
||||||
this.allowedSlots,
|
this.allowedSlots,
|
||||||
|
this.multiplier = 1.0,
|
||||||
|
this.weight = 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class ItemPrefixTable {
|
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 = [
|
static const List<ItemModifier> magicPrefixes = [
|
||||||
// Weapons
|
// Weapons
|
||||||
ItemModifier(
|
ItemModifier(
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import '../enums.dart';
|
||||||
import '../config/item_config.dart';
|
import '../config/item_config.dart';
|
||||||
import 'item_prefix_table.dart'; // Import prefix table
|
import 'item_prefix_table.dart'; // Import prefix table
|
||||||
import 'name_generator.dart'; // Import name generator
|
import 'name_generator.dart'; // Import name generator
|
||||||
|
import '../../utils/game_math.dart';
|
||||||
|
|
||||||
class ItemTemplate {
|
class ItemTemplate {
|
||||||
final String id;
|
final String id;
|
||||||
|
|
@ -79,8 +80,40 @@ class ItemTemplate {
|
||||||
|
|
||||||
final random = Random();
|
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)
|
// 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
|
if (random.nextBool()) { // 50% chance
|
||||||
// Filter valid prefixes for this slot
|
// Filter valid prefixes for this slot
|
||||||
final validPrefixes = ItemPrefixTable.magicPrefixes.where((p) {
|
final validPrefixes = ItemPrefixTable.magicPrefixes.where((p) {
|
||||||
|
|
@ -208,11 +241,14 @@ class ItemTable {
|
||||||
/// [tier]: The tier of items to select from.
|
/// [tier]: The tier of items to select from.
|
||||||
/// [slot]: Optional. If provided, only items of this slot are considered.
|
/// [slot]: Optional. If provided, only items of this slot are considered.
|
||||||
/// [weights]: Optional map of rarity weights. Key: Rarity, Value: Weight.
|
/// [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({
|
static ItemTemplate? getRandomItem({
|
||||||
required ItemTier tier,
|
required ItemTier tier,
|
||||||
EquipmentSlot? slot,
|
EquipmentSlot? slot,
|
||||||
Map<ItemRarity, int>? weights,
|
Map<ItemRarity, int>? weights,
|
||||||
|
ItemRarity? minRarity,
|
||||||
|
ItemRarity? maxRarity,
|
||||||
}) {
|
}) {
|
||||||
// 1. Filter by Tier and Slot (if provided)
|
// 1. Filter by Tier and Slot (if provided)
|
||||||
var candidates = allItems.where((item) => item.tier == tier);
|
var candidates = allItems.where((item) => item.tier == tier);
|
||||||
|
|
@ -222,16 +258,37 @@ class ItemTable {
|
||||||
|
|
||||||
if (candidates.isEmpty) return null;
|
if (candidates.isEmpty) return null;
|
||||||
|
|
||||||
// 2. Determine Target Rarity based on weights
|
// 2. Prepare Rarity Weights (Filtered by min/max)
|
||||||
final rarityWeights = weights ?? ItemConfig.defaultRarityWeights;
|
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);
|
int roll = _random.nextInt(totalWeight);
|
||||||
|
|
||||||
ItemRarity? selectedRarity;
|
ItemRarity? selectedRarity;
|
||||||
int currentSum = 0;
|
int currentSum = 0;
|
||||||
|
|
||||||
for (var entry in rarityWeights.entries) {
|
for (var entry in activeWeights.entries) {
|
||||||
currentSum += entry.value;
|
currentSum += entry.value;
|
||||||
if (roll < currentSum) {
|
if (roll < currentSum) {
|
||||||
selectedRarity = entry.key;
|
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();
|
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 (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)];
|
return candidates.toList()[_random.nextInt(candidates.length)];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Pick random item
|
// 6. Pick random item
|
||||||
return rarityCandidates[_random.nextInt(rarityCandidates.length)];
|
return rarityCandidates[_random.nextInt(rarityCandidates.length)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,6 @@ enum DamageType { normal, bleed, vulnerable }
|
||||||
|
|
||||||
enum StatType { maxHp, atk, defense, luck }
|
enum StatType { maxHp, atk, defense, luck }
|
||||||
|
|
||||||
enum ItemRarity { magic, rare, legendary, unique }
|
enum ItemRarity { normal, magic, rare, legendary, unique }
|
||||||
|
|
||||||
enum ItemTier { tier1, tier2, tier3 }
|
enum ItemTier { tier1, tier2, tier3 }
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,10 @@ class BattleProvider with ChangeNotifier {
|
||||||
List<String> get logs => battleLogs;
|
List<String> get logs => battleLogs;
|
||||||
int get lastGoldReward => _lastGoldReward;
|
int get lastGoldReward => _lastGoldReward;
|
||||||
|
|
||||||
|
void refreshUI() {
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
// Damage Event Stream
|
// Damage Event Stream
|
||||||
final _damageEventController = StreamController<DamageEvent>.broadcast();
|
final _damageEventController = StreamController<DamageEvent>.broadcast();
|
||||||
Stream<DamageEvent> get damageStream => _damageEventController.stream;
|
Stream<DamageEvent> get damageStream => _damageEventController.stream;
|
||||||
|
|
@ -83,10 +87,10 @@ class BattleProvider with ChangeNotifier {
|
||||||
stage = data['stage'];
|
stage = data['stage'];
|
||||||
turnCount = data['turnCount'];
|
turnCount = data['turnCount'];
|
||||||
player = Character.fromJson(data['player']);
|
player = Character.fromJson(data['player']);
|
||||||
|
|
||||||
battleLogs.clear();
|
battleLogs.clear();
|
||||||
_addLog("Game Loaded! Resuming Stage $stage");
|
_addLog("Game Loaded! Resuming Stage $stage");
|
||||||
|
|
||||||
_prepareNextStage();
|
_prepareNextStage();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
@ -113,51 +117,51 @@ class BattleProvider with ChangeNotifier {
|
||||||
player.gold = GameConfig.startingGold;
|
player.gold = GameConfig.startingGold;
|
||||||
|
|
||||||
// Provide starter equipment
|
// Provide starter equipment
|
||||||
final starterSword = Item(
|
// final starterSword = Item(
|
||||||
id: "starter_sword",
|
// id: "starter_sword",
|
||||||
name: "Wooden Sword",
|
// name: "Wooden Sword",
|
||||||
description: "A basic sword",
|
// description: "A basic sword",
|
||||||
atkBonus: 5,
|
// atkBonus: 5,
|
||||||
hpBonus: 0,
|
// hpBonus: 0,
|
||||||
slot: EquipmentSlot.weapon,
|
// slot: EquipmentSlot.weapon,
|
||||||
);
|
// );
|
||||||
final starterArmor = Item(
|
// final starterArmor = Item(
|
||||||
id: "starter_armor",
|
// id: "starter_armor",
|
||||||
name: "Leather Armor",
|
// name: "Leather Armor",
|
||||||
description: "Basic protection",
|
// description: "Basic protection",
|
||||||
atkBonus: 0,
|
// atkBonus: 0,
|
||||||
hpBonus: 20,
|
// hpBonus: 20,
|
||||||
slot: EquipmentSlot.armor,
|
// slot: EquipmentSlot.armor,
|
||||||
);
|
// );
|
||||||
final starterShield = Item(
|
// final starterShield = Item(
|
||||||
id: "starter_shield",
|
// id: "starter_shield",
|
||||||
name: "Wooden Shield",
|
// name: "Wooden Shield",
|
||||||
description: "A small shield",
|
// description: "A small shield",
|
||||||
atkBonus: 0,
|
// atkBonus: 0,
|
||||||
hpBonus: 0,
|
// hpBonus: 0,
|
||||||
armorBonus: 3,
|
// armorBonus: 3,
|
||||||
slot: EquipmentSlot.shield,
|
// slot: EquipmentSlot.shield,
|
||||||
);
|
// );
|
||||||
final starterRing = Item(
|
// final starterRing = Item(
|
||||||
id: "starter_ring",
|
// id: "starter_ring",
|
||||||
name: "Copper Ring",
|
// name: "Copper Ring",
|
||||||
description: "A simple ring",
|
// description: "A simple ring",
|
||||||
atkBonus: 1,
|
// atkBonus: 1,
|
||||||
hpBonus: 5,
|
// hpBonus: 5,
|
||||||
slot: EquipmentSlot.accessory,
|
// slot: EquipmentSlot.accessory,
|
||||||
);
|
// );
|
||||||
|
|
||||||
player.addToInventory(starterSword);
|
// player.addToInventory(starterSword);
|
||||||
player.equip(starterSword);
|
// player.equip(starterSword);
|
||||||
|
|
||||||
player.addToInventory(starterArmor);
|
// player.addToInventory(starterArmor);
|
||||||
player.equip(starterArmor);
|
// player.equip(starterArmor);
|
||||||
|
|
||||||
player.addToInventory(starterShield);
|
// player.addToInventory(starterShield);
|
||||||
player.equip(starterShield);
|
// player.equip(starterShield);
|
||||||
|
|
||||||
player.addToInventory(starterRing);
|
// player.addToInventory(starterRing);
|
||||||
player.equip(starterRing);
|
// player.equip(starterRing);
|
||||||
|
|
||||||
// Add new status effect items for testing
|
// Add new status effect items for testing
|
||||||
player.addToInventory(ItemTable.weapons[3].createItem()); // Stunning Hammer
|
player.addToInventory(ItemTable.weapons[3].createItem()); // Stunning Hammer
|
||||||
|
|
@ -194,36 +198,8 @@ class BattleProvider with ChangeNotifier {
|
||||||
|
|
||||||
if (type == StageType.battle || type == StageType.elite) {
|
if (type == StageType.battle || type == StageType.elite) {
|
||||||
bool isElite = type == StageType.elite;
|
bool isElite = type == StageType.elite;
|
||||||
// Select random enemy template
|
|
||||||
final random = Random();
|
EnemyTemplate template = EnemyTable.getRandomEnemy(stage: stage, isElite: isElite);
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
newEnemy = template.createCharacter(stage: stage);
|
newEnemy = template.createCharacter(stage: stage);
|
||||||
|
|
||||||
|
|
@ -264,6 +240,12 @@ class BattleProvider with ChangeNotifier {
|
||||||
// Replaces _spawnEnemy
|
// Replaces _spawnEnemy
|
||||||
// void _spawnEnemy() { ... } - Removed
|
// void _spawnEnemy() { ... } - Removed
|
||||||
|
|
||||||
|
Future<void> _onDefeat() async {
|
||||||
|
_addLog("Player defeated! Enemy wins!");
|
||||||
|
await SaveManager.clearSaveData();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
/// Handle player's action choice
|
/// Handle player's action choice
|
||||||
|
|
||||||
Future<void> playerAction(ActionType type, RiskLevel risk) async {
|
Future<void> playerAction(ActionType type, RiskLevel risk) async {
|
||||||
|
|
@ -287,6 +269,12 @@ class BattleProvider with ChangeNotifier {
|
||||||
|
|
||||||
// 2. Process Start-of-Turn Effects (Stun, Bleed)
|
// 2. Process Start-of-Turn Effects (Stun, Bleed)
|
||||||
bool canAct = _processStartTurnEffects(player);
|
bool canAct = _processStartTurnEffects(player);
|
||||||
|
|
||||||
|
if (player.isDead) {
|
||||||
|
await _onDefeat();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!canAct) {
|
if (!canAct) {
|
||||||
_endPlayerTurn(); // Skip turn if stunned
|
_endPlayerTurn(); // Skip turn if stunned
|
||||||
return;
|
return;
|
||||||
|
|
@ -341,11 +329,17 @@ class BattleProvider with ChangeNotifier {
|
||||||
|
|
||||||
// Animation Delays to sync with Impact
|
// Animation Delays to sync with Impact
|
||||||
if (risk == RiskLevel.safe) {
|
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) {
|
} 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) {
|
} else if (risk == RiskLevel.risky) {
|
||||||
await Future.delayed(const Duration(milliseconds: GameConfig.animDelayRisky));
|
await Future.delayed(
|
||||||
|
const Duration(milliseconds: GameConfig.animDelayRisky),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
int damageToHp = 0;
|
int damageToHp = 0;
|
||||||
|
|
@ -437,14 +431,19 @@ class BattleProvider with ChangeNotifier {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future.delayed(const Duration(milliseconds: GameConfig.animDelayEnemyTurn), () => _enemyTurn());
|
Future.delayed(
|
||||||
|
const Duration(milliseconds: GameConfig.animDelayEnemyTurn),
|
||||||
|
() => _enemyTurn(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _enemyTurn() async {
|
Future<void> _enemyTurn() async {
|
||||||
if (!isPlayerTurn && (player.isDead || enemy.isDead)) return;
|
if (!isPlayerTurn && (player.isDead || enemy.isDead)) return;
|
||||||
|
|
||||||
_addLog("Enemy's turn...");
|
_addLog("Enemy's turn...");
|
||||||
await Future.delayed(const Duration(milliseconds: GameConfig.animDelayEnemyTurn));
|
await Future.delayed(
|
||||||
|
const Duration(milliseconds: GameConfig.animDelayEnemyTurn),
|
||||||
|
);
|
||||||
|
|
||||||
// Enemy Turn Start Logic
|
// Enemy Turn Start Logic
|
||||||
// Armor decay
|
// Armor decay
|
||||||
|
|
@ -543,7 +542,8 @@ class BattleProvider with ChangeNotifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (player.isDead) {
|
if (player.isDead) {
|
||||||
_addLog("Player defeated! Enemy wins!");
|
await _onDefeat();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
isPlayerTurn = true;
|
isPlayerTurn = true;
|
||||||
|
|
@ -654,15 +654,6 @@ class BattleProvider with ChangeNotifier {
|
||||||
_addLog("Enemy defeated! Gained $goldReward Gold.");
|
_addLog("Enemy defeated! Gained $goldReward Gold.");
|
||||||
_addLog("Choose a reward.");
|
_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;
|
ItemTier currentTier = ItemTier.tier1;
|
||||||
if (stage > GameConfig.tier2StageMax)
|
if (stage > GameConfig.tier2StageMax)
|
||||||
currentTier = ItemTier.tier3;
|
currentTier = ItemTier.tier3;
|
||||||
|
|
@ -670,9 +661,42 @@ class BattleProvider with ChangeNotifier {
|
||||||
currentTier = ItemTier.tier2;
|
currentTier = ItemTier.tier2;
|
||||||
|
|
||||||
rewardOptions = [];
|
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++) {
|
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) {
|
if (item != null) {
|
||||||
rewardOptions.add(item.createItem(stage: stage));
|
rewardOptions.add(item.createItem(stage: stage));
|
||||||
}
|
}
|
||||||
|
|
@ -716,7 +740,9 @@ class BattleProvider with ChangeNotifier {
|
||||||
|
|
||||||
void _completeStage() {
|
void _completeStage() {
|
||||||
// Heal player after selecting reward
|
// Heal player after selecting reward
|
||||||
int healAmount = GameMath.floor(player.totalMaxHp * GameConfig.stageHealRatio);
|
int healAmount = GameMath.floor(
|
||||||
|
player.totalMaxHp * GameConfig.stageHealRatio,
|
||||||
|
);
|
||||||
player.heal(healAmount);
|
player.heal(healAmount);
|
||||||
_addLog("Stage Cleared! Recovered $healAmount HP.");
|
_addLog("Stage Cleared! Recovered $healAmount HP.");
|
||||||
|
|
||||||
|
|
@ -760,7 +786,9 @@ class BattleProvider with ChangeNotifier {
|
||||||
|
|
||||||
void sellItem(Item item) {
|
void sellItem(Item item) {
|
||||||
if (player.inventory.remove(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;
|
player.gold += sellPrice;
|
||||||
_addLog("Sold ${item.name} for $sellPrice G.");
|
_addLog("Sold ${item.name} for $sellPrice G.");
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
|
||||||
|
|
@ -601,6 +601,7 @@ class _BattleScreenState extends State<BattleScreen> {
|
||||||
width: 24,
|
width: 24,
|
||||||
height: 24,
|
height: 24,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
|
filterQuality: FilterQuality.high,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (!isSkip) const SizedBox(width: 12),
|
if (!isSkip) const SizedBox(width: 12),
|
||||||
|
|
@ -755,6 +756,7 @@ class _BattleScreenState extends State<BattleScreen> {
|
||||||
height: 32,
|
height: 32,
|
||||||
color: ThemeConfig.textColorWhite, // Tint icon white
|
color: ThemeConfig.textColorWhite, // Tint icon white
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
|
filterQuality: FilterQuality.high,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,20 +39,29 @@ class InventoryScreen extends StatelessWidget {
|
||||||
_buildStatItem(
|
_buildStatItem(
|
||||||
"HP",
|
"HP",
|
||||||
"${player.hp}/${player.totalMaxHp}",
|
"${player.hp}/${player.totalMaxHp}",
|
||||||
|
color: ThemeConfig.statHpColor,
|
||||||
),
|
),
|
||||||
_buildStatItem("ATK", "${player.totalAtk}"),
|
|
||||||
_buildStatItem("DEF", "${player.totalDefense}"),
|
|
||||||
_buildStatItem("Shield", "${player.armor}"),
|
|
||||||
_buildStatItem(
|
_buildStatItem(
|
||||||
"Gold",
|
"ATK",
|
||||||
"${player.gold} G",
|
"${player.totalAtk}",
|
||||||
color: ThemeConfig.statGoldColor,
|
color: ThemeConfig.statAtkColor,
|
||||||
),
|
),
|
||||||
|
_buildStatItem(
|
||||||
|
"DEF",
|
||||||
|
"${player.totalDefense}",
|
||||||
|
color: ThemeConfig.statDefColor,
|
||||||
|
),
|
||||||
|
_buildStatItem("Shield", "${player.armor}"),
|
||||||
_buildStatItem(
|
_buildStatItem(
|
||||||
"Luck",
|
"Luck",
|
||||||
"${player.totalLuck}",
|
"${player.totalLuck}",
|
||||||
color: ThemeConfig.statLuckColor,
|
color: ThemeConfig.statLuckColor,
|
||||||
),
|
),
|
||||||
|
_buildStatItem(
|
||||||
|
"Gold",
|
||||||
|
"${player.gold} G",
|
||||||
|
color: ThemeConfig.statGoldColor,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -94,12 +103,14 @@ class InventoryScreen extends StatelessWidget {
|
||||||
color: item != null
|
color: item != null
|
||||||
? ThemeConfig.equipmentCardBg
|
? ThemeConfig.equipmentCardBg
|
||||||
: ThemeConfig.emptySlotBg,
|
: ThemeConfig.emptySlotBg,
|
||||||
shape: item != null &&
|
shape:
|
||||||
|
item != null &&
|
||||||
item.rarity != ItemRarity.magic
|
item.rarity != ItemRarity.magic
|
||||||
? RoundedRectangleBorder(
|
? RoundedRectangleBorder(
|
||||||
side: BorderSide(
|
side: BorderSide(
|
||||||
color:
|
color: ItemUtils.getRarityColor(
|
||||||
ItemUtils.getRarityColor(item.rarity),
|
item.rarity,
|
||||||
|
),
|
||||||
width: 2.0,
|
width: 2.0,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(4.0),
|
borderRadius: BorderRadius.circular(4.0),
|
||||||
|
|
@ -125,12 +136,15 @@ class InventoryScreen extends StatelessWidget {
|
||||||
left: 4,
|
left: 4,
|
||||||
top: 4,
|
top: 4,
|
||||||
child: Opacity(
|
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(
|
child: Image.asset(
|
||||||
ItemUtils.getIconPath(slot),
|
ItemUtils.getIconPath(slot),
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
|
filterQuality: FilterQuality.high,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -151,8 +165,10 @@ class InventoryScreen extends StatelessWidget {
|
||||||
item?.name ?? "Empty",
|
item?.name ?? "Empty",
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: ThemeConfig.fontSizeSmall,
|
fontSize:
|
||||||
fontWeight: ThemeConfig.fontWeightBold,
|
ThemeConfig.fontSizeSmall,
|
||||||
|
fontWeight:
|
||||||
|
ThemeConfig.fontWeightBold,
|
||||||
color: item != null
|
color: item != null
|
||||||
? ItemUtils.getRarityColor(
|
? ItemUtils.getRarityColor(
|
||||||
item.rarity,
|
item.rarity,
|
||||||
|
|
@ -237,12 +253,14 @@ class InventoryScreen extends StatelessWidget {
|
||||||
left: 4,
|
left: 4,
|
||||||
top: 4,
|
top: 4,
|
||||||
child: Opacity(
|
child: Opacity(
|
||||||
opacity: 0.5, // Adjusted opacity for image visibility
|
opacity:
|
||||||
|
0.5, // Adjusted opacity for image visibility
|
||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
ItemUtils.getIconPath(item.slot),
|
ItemUtils.getIconPath(item.slot),
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
|
filterQuality: FilterQuality.high,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -260,7 +278,8 @@ class InventoryScreen extends StatelessWidget {
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: ThemeConfig.fontSizeSmall,
|
fontSize: ThemeConfig.fontSizeSmall,
|
||||||
fontWeight: ThemeConfig.fontWeightBold,
|
fontWeight:
|
||||||
|
ThemeConfig.fontWeightBold,
|
||||||
color: ItemUtils.getRarityColor(
|
color: ItemUtils.getRarityColor(
|
||||||
item.rarity,
|
item.rarity,
|
||||||
),
|
),
|
||||||
|
|
@ -289,7 +308,10 @@ class InventoryScreen extends StatelessWidget {
|
||||||
color: ThemeConfig.emptySlotBg,
|
color: ThemeConfig.emptySlotBg,
|
||||||
),
|
),
|
||||||
child: const Center(
|
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}) {
|
Widget _buildStatItem(String label, String value, {Color? color}) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Text(label, style: const TextStyle(color: ThemeConfig.textColorGrey, fontSize: 12)),
|
Text(
|
||||||
|
label,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: ThemeConfig.textColorGrey,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
Text(
|
Text(
|
||||||
value,
|
value,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
|
@ -358,7 +386,10 @@ class InventoryScreen extends StatelessWidget {
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.attach_money, color: ThemeConfig.statGoldColor),
|
const Icon(
|
||||||
|
Icons.attach_money,
|
||||||
|
color: ThemeConfig.statGoldColor,
|
||||||
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Text("Sell (${item.price} G)"),
|
Text("Sell (${item.price} G)"),
|
||||||
],
|
],
|
||||||
|
|
@ -402,7 +433,9 @@ class InventoryScreen extends StatelessWidget {
|
||||||
child: const Text("Cancel"),
|
child: const Text("Cancel"),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(backgroundColor: ThemeConfig.statGoldColor),
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: ThemeConfig.statGoldColor,
|
||||||
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
provider.sellItem(item);
|
provider.sellItem(item);
|
||||||
Navigator.pop(ctx);
|
Navigator.pop(ctx);
|
||||||
|
|
@ -430,7 +463,9 @@ class InventoryScreen extends StatelessWidget {
|
||||||
child: const Text("Cancel"),
|
child: const Text("Cancel"),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(backgroundColor: ThemeConfig.btnActionActive),
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: ThemeConfig.btnActionActive,
|
||||||
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
provider.discardItem(item);
|
provider.discardItem(item);
|
||||||
Navigator.pop(ctx);
|
Navigator.pop(ctx);
|
||||||
|
|
@ -481,7 +516,10 @@ class InventoryScreen extends StatelessWidget {
|
||||||
if (oldItem != null)
|
if (oldItem != null)
|
||||||
Text(
|
Text(
|
||||||
"Replaces ${oldItem.name}",
|
"Replaces ${oldItem.name}",
|
||||||
style: const TextStyle(fontSize: 12, color: ThemeConfig.textColorGrey),
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: ThemeConfig.textColorGrey,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_buildStatChangeRow("Max HP", currentMaxHp, newMaxHp),
|
_buildStatChangeRow("Max HP", currentMaxHp, newMaxHp),
|
||||||
|
|
@ -575,7 +613,9 @@ class InventoryScreen extends StatelessWidget {
|
||||||
int diff = newVal - oldVal;
|
int diff = newVal - oldVal;
|
||||||
Color color = diff > 0
|
Color color = diff > 0
|
||||||
? ThemeConfig.statDiffPositive
|
? ThemeConfig.statDiffPositive
|
||||||
: (diff < 0 ? ThemeConfig.statDiffNegative : ThemeConfig.statDiffNeutral);
|
: (diff < 0
|
||||||
|
? ThemeConfig.statDiffNegative
|
||||||
|
: ThemeConfig.statDiffNeutral);
|
||||||
String diffText = diff > 0 ? "(+$diff)" : (diff < 0 ? "($diff)" : "");
|
String diffText = diff > 0 ? "(+$diff)" : (diff < 0 ? "($diff)" : "");
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
|
|
@ -586,8 +626,15 @@ class InventoryScreen extends StatelessWidget {
|
||||||
Text(label),
|
Text(label),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text("$oldVal", style: const TextStyle(color: ThemeConfig.textColorGrey)),
|
Text(
|
||||||
const Icon(Icons.arrow_right, size: 16, color: ThemeConfig.textColorGrey),
|
"$oldVal",
|
||||||
|
style: const TextStyle(color: ThemeConfig.textColorGrey),
|
||||||
|
),
|
||||||
|
const Icon(
|
||||||
|
Icons.arrow_right,
|
||||||
|
size: 16,
|
||||||
|
color: ThemeConfig.textColorGrey,
|
||||||
|
),
|
||||||
Text(
|
Text(
|
||||||
"$newVal",
|
"$newVal",
|
||||||
style: const TextStyle(fontWeight: ThemeConfig.fontWeightBold),
|
style: const TextStyle(fontWeight: ThemeConfig.fontWeightBold),
|
||||||
|
|
@ -627,7 +674,10 @@ class InventoryScreen extends StatelessWidget {
|
||||||
padding: const EdgeInsets.only(top: 2.0, bottom: 2.0),
|
padding: const EdgeInsets.only(top: 2.0, bottom: 2.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
stats.join(", "),
|
stats.join(", "),
|
||||||
style: const TextStyle(fontSize: ThemeConfig.fontSizeSmall, color: ThemeConfig.statAtkColor),
|
style: const TextStyle(
|
||||||
|
fontSize: ThemeConfig.fontSizeSmall,
|
||||||
|
color: ThemeConfig.statAtkColor,
|
||||||
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -636,7 +686,10 @@ class InventoryScreen extends StatelessWidget {
|
||||||
padding: const EdgeInsets.only(bottom: 2.0),
|
padding: const EdgeInsets.only(bottom: 2.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
effectTexts.join("\n"),
|
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 {
|
class ItemUtils {
|
||||||
static Color getRarityColor(ItemRarity rarity) {
|
static Color getRarityColor(ItemRarity rarity) {
|
||||||
switch (rarity) {
|
switch (rarity) {
|
||||||
|
case ItemRarity.normal:
|
||||||
|
return ThemeConfig.rarityNormal;
|
||||||
case ItemRarity.magic:
|
case ItemRarity.magic:
|
||||||
return ThemeConfig.rarityMagic;
|
return ThemeConfig.rarityMagic;
|
||||||
case ItemRarity.rare:
|
case ItemRarity.rare:
|
||||||
|
|
|
||||||
|
|
@ -21,18 +21,6 @@ class ShopUI extends StatelessWidget {
|
||||||
final player = battleProvider.player;
|
final player = battleProvider.player;
|
||||||
final shopItems = shopProvider.availableItems;
|
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(
|
return Container(
|
||||||
color: ThemeConfig.shopBg,
|
color: ThemeConfig.shopBg,
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
|
@ -139,6 +127,7 @@ class ShopUI extends StatelessWidget {
|
||||||
width: 48,
|
width: 48,
|
||||||
height: 48,
|
height: 48,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
|
filterQuality: FilterQuality.high,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -154,7 +143,8 @@ class ShopUI extends StatelessWidget {
|
||||||
color: ItemUtils.getRarityColor(
|
color: ItemUtils.getRarityColor(
|
||||||
item.rarity,
|
item.rarity,
|
||||||
),
|
),
|
||||||
fontSize: ThemeConfig.fontSizeMedium,
|
fontSize:
|
||||||
|
ThemeConfig.fontSizeMedium,
|
||||||
),
|
),
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
|
|
@ -215,10 +205,20 @@ class ShopUI extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: player.gold >= GameConfig.shopRerollCost
|
onPressed: player.gold >= GameConfig.shopRerollCost
|
||||||
? () => shopProvider.rerollShopItems(
|
? () {
|
||||||
player,
|
bool success = shopProvider.rerollShopItems(
|
||||||
battleProvider.stage,
|
player,
|
||||||
)
|
battleProvider.stage,
|
||||||
|
);
|
||||||
|
if (!success) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text("Not enough gold to reroll!"),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
: null,
|
: null,
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.refresh,
|
Icons.refresh,
|
||||||
|
|
@ -287,8 +287,33 @@ class ShopUI extends StatelessWidget {
|
||||||
backgroundColor: ThemeConfig.statGoldColor,
|
backgroundColor: ThemeConfig.statGoldColor,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
shopProvider.buyItem(item, player);
|
bool success = shopProvider.buyItem(item, player);
|
||||||
Navigator.pop(ctx);
|
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)),
|
child: const Text("Buy", style: TextStyle(color: Colors.black)),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -73,8 +73,7 @@
|
||||||
### E. 스테이지 시스템 (`StageModel`)
|
### E. 스테이지 시스템 (`StageModel`)
|
||||||
|
|
||||||
- **타입:** Battle, Shop, Rest, Elite.
|
- **타입:** Battle, Shop, Rest, Elite.
|
||||||
- **적 생성:** 스테이지 레벨에 따른 스탯 스케일링 적용.
|
- **적 등장 테이블 (Enemy Pool):** 적 조우 시, 현재 스테이지(라운드)에 따라 등장 가능한 적 목록(`enemy_table_pool`)을 설정하여 해당 범위 내에서 적을 랜덤 생성해야 함.
|
||||||
- **적 등장 테이블 (Enemy Pull):** 적 조우 시, 현재 스테이지(라운드)에 따라 등장 가능한 적 목록(`enemy_table_pull`)을 설정하여 해당 범위 내에서 적을 랜덤 생성해야 함.
|
|
||||||
- **게임 구조 (Game Structure):**
|
- **게임 구조 (Game Structure):**
|
||||||
- **총 3라운드 (3 Rounds):** 각 라운드는 12개의 스테이지로 구성 (12/12/12).
|
- **총 3라운드 (3 Rounds):** 각 라운드는 12개의 스테이지로 구성 (12/12/12).
|
||||||
- **라운드 구성:**
|
- **라운드 구성:**
|
||||||
|
|
@ -115,7 +114,7 @@
|
||||||
|
|
||||||
- **Prompt Driven Development:** `prompt/XX_description.md` 유지.
|
- **Prompt Driven Development:** `prompt/XX_description.md` 유지.
|
||||||
- **유사 작업 통합:** 작업 내용이 이전 프롬프트와 유사한 경우 새로운 프롬프트를 생성하지 않고 기존 프롬프트에 내용을 추가합니다.
|
- **유사 작업 통합:** 작업 내용이 이전 프롬프트와 유사한 경우 새로운 프롬프트를 생성하지 않고 기존 프롬프트에 내용을 추가합니다.
|
||||||
- **Language:** **모든 프롬프트 파일(prompt/XX_...)은 반드시 한국어(Korean)로 작성해야 합니다.**
|
- **Language:** **모든 프롬프트 파일(prompt/XX\_...)은 반드시 한국어(Korean)로 작성해야 합니다.**
|
||||||
- **Config Management:** 하드코딩되는 값들은 `config` 폴더 내 파일들(`lib/game/config/` 등)에서 통합 관리할 수 있도록 작성해야 합니다.
|
- **Config Management:** 하드코딩되는 값들은 `config` 폴더 내 파일들(`lib/game/config/` 등)에서 통합 관리할 수 있도록 작성해야 합니다.
|
||||||
- **State Management:** `Provider` + `Stream` (이벤트성 데이터).
|
- **State Management:** `Provider` + `Stream` (이벤트성 데이터).
|
||||||
- **Data:** JSON 기반.
|
- **Data:** JSON 기반.
|
||||||
|
|
@ -159,4 +158,10 @@
|
||||||
- [x] 47_inventory_full_handling.md
|
- [x] 47_inventory_full_handling.md
|
||||||
- [x] 48_refactor_stage_ui.md
|
- [x] 48_refactor_stage_ui.md
|
||||||
- [x] 49_implement_item_icons.md
|
- [x] 49_implement_item_icons.md
|
||||||
|
- [x] 50_expand_item_pool.md
|
||||||
|
- [x] 51_refactor_prefix_table.md
|
||||||
|
- [x] 52_round_based_enemy_pool.md
|
||||||
|
- [x] 53_refine_stage_rewards.md
|
||||||
|
- [x] 54_fix_shop_logic.md
|
||||||
|
- [x] 55_fix_shop_ui_sync.md
|
||||||
|
- [x] 56_permadeath_implementation.md
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@
|
||||||
- **`BattleScreen` (`lib/screens/battle_screen.dart`):**
|
- **`BattleScreen` (`lib/screens/battle_screen.dart`):**
|
||||||
- 스테이지 클리어 보상 팝업의 아이템 아이콘 교체.
|
- 스테이지 클리어 보상 팝업의 아이템 아이콘 교체.
|
||||||
- **플로팅 액션 버튼(FAB):** ATK/DEF 버튼의 아이콘을 `icon_weapon.png`와 `icon_shield.png`로 교체하고 흰색 틴트 적용.
|
- **플로팅 액션 버튼(FAB):** ATK/DEF 버튼의 아이콘을 `icon_weapon.png`와 `icon_shield.png`로 교체하고 흰색 틴트 적용.
|
||||||
|
- **고해상도 이미지 처리:** 모든 `Image.asset` 위젯에 `filterQuality: FilterQuality.high`를 적용하여 이미지 축소 시 발생하는 앨리어싱(깨짐) 현상 완화.
|
||||||
|
|
||||||
## 3. 결과 (Result)
|
## 3. 결과 (Result)
|
||||||
- 게임 내 아이템 표현이 단순 기호에서 구체적인 이미지로 변경되어 시각적 몰입도가 향상되었습니다.
|
- 게임 내 아이템 표현이 단순 기호에서 구체적인 이미지로 변경되어 시각적 몰입도가 향상되었습니다.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
# 53. 아이템 풀 확장 및 접두사 시스템 (Item Pool Expansion & Prefix System)
|
||||||
|
|
||||||
|
## 1. 목표 (Goal)
|
||||||
|
- 아이템 풀을 다양화하기 위해 `ItemRarity.normal`(일반 등급)을 추가합니다.
|
||||||
|
- 일반 등급 아이템의 드랍 확률을 가장 높게 설정하고, 새로운 무기 데이터(Short Bow, Long Sword)를 추가합니다.
|
||||||
|
- 일반 등급 아이템 생성 시 확률적으로 접두사(Crude, Old, High-quality)를 부여하여 스탯이 변동되는 시스템을 구현합니다.
|
||||||
|
|
||||||
|
## 2. 구현 상세 (Implementation Details)
|
||||||
|
|
||||||
|
### Enum 및 설정 업데이트
|
||||||
|
- **`ItemRarity`:** `normal` 등급 추가 (가장 낮은 등급).
|
||||||
|
- **`ThemeConfig` & `ItemUtils`:** `normal` 등급의 색상(흰색) 매핑 추가.
|
||||||
|
- **`ItemConfig`:** `defaultRarityWeights`를 수정하여 Normal 등급이 가장 높은 확률(50%)을 가지도록 조정.
|
||||||
|
|
||||||
|
### 데이터 추가 (`items.json`)
|
||||||
|
- **신규 무기:**
|
||||||
|
- `short_bow` (Tier 1, Normal)
|
||||||
|
- `long_sword` (Tier 2, Normal)
|
||||||
|
|
||||||
|
### 로직 구현 (`ItemTemplate`)
|
||||||
|
- **`createItem` 메서드 수정:**
|
||||||
|
- **Normal 등급 로직:** 0~100 주사위를 굴려 접두사 부여.
|
||||||
|
- **0-25 (Crude/조잡한):** 이름에 "Crude " 추가, 모든 스탯 -10%.
|
||||||
|
- **26-50 (Old/낡은):** 이름에 "Old " 추가, 모든 스탯 -5%.
|
||||||
|
- **51-75 (Base):** 변동 없음.
|
||||||
|
- **76-100 (High-quality/상급):** 이름에 "High-quality " 추가, 모든 스탯 +10%.
|
||||||
|
- 스탯 계산 시 `GameMath` 또는 `floor`를 사용하여 정수형 유지.
|
||||||
|
|
||||||
|
## 3. 결과 (Result)
|
||||||
|
- 초반부 아이템 획득의 다양성이 증가하고, 같은 아이템이라도 접두사에 따라 성능 차이가 발생하여 파밍의 재미가 추가되었습니다.
|
||||||
|
- `normal` 등급 아이템이 자주 등장하여 기본적인 장비 수급이 원활해졌습니다.
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
# 54. 접두사 테이블 리팩토링 (Prefix Table Refactoring)
|
||||||
|
|
||||||
|
## 1. 목표 (Goal)
|
||||||
|
- `ItemTable`에 하드코딩되어 있던 Normal 등급 접두사 로직(이름, 배율, 확률)을 `ItemPrefixTable`로 이동하여 데이터 기반으로 관리합니다.
|
||||||
|
- `ItemModifier` 클래스를 확장하여 스탯 배율(`multiplier`)과 가중치(`weight`)를 지원하도록 개선합니다.
|
||||||
|
|
||||||
|
## 2. 구현 상세 (Implementation Details)
|
||||||
|
|
||||||
|
### `ItemPrefixTable` 개선
|
||||||
|
- **`ItemModifier` 구조 변경:**
|
||||||
|
- `multiplier`: 퍼센트 기반 스탯 변경을 위한 필드 추가 (기본값 1.0).
|
||||||
|
- `weight`: 랜덤 선택 가중치를 위한 필드 추가 (기본값 1).
|
||||||
|
- **`normalPrefixes` 데이터 추가:**
|
||||||
|
- Crude (0.9, weight 25)
|
||||||
|
- Old (0.95, weight 25)
|
||||||
|
- Standard (1.0, weight 25, empty prefix)
|
||||||
|
- High-quality (1.1, weight 25)
|
||||||
|
|
||||||
|
### `ItemTable` 로직 수정
|
||||||
|
- **`createItem` 메서드:**
|
||||||
|
- 하드코딩된 `if-else` 확률 로직을 제거.
|
||||||
|
- `ItemPrefixTable.normalPrefixes`를 사용하여 가중치 기반 랜덤 선택(Weighted Random Selection) 알고리즘 구현.
|
||||||
|
- 선택된 Modifier의 `multiplier`를 적용하여 스탯 계산.
|
||||||
|
|
||||||
|
## 3. 결과 (Result)
|
||||||
|
- 접두사 데이터 추가 및 밸런스 조정이 `ItemPrefixTable` 수정만으로 가능해졌습니다.
|
||||||
|
- 코드 중복이 줄어들고 확장성이 향상되었습니다.
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
# 52. 라운드별 적 등장 시스템 (Round-based Enemy Pool)
|
||||||
|
|
||||||
|
## 1. 목표 (Goal)
|
||||||
|
- 게임 진행도(라운드)에 따라 등장하는 적들을 다르게 설정하여 난이도 곡선을 구현합니다.
|
||||||
|
- `enemies.json` 데이터에 `tier` 필드를 추가하고, 스테이지에 맞는 적을 소환하는 로직을 `EnemyTable`에 구현합니다.
|
||||||
|
|
||||||
|
## 2. 구현 상세 (Implementation Details)
|
||||||
|
|
||||||
|
### 데이터 구조 변경 (`enemies.json` & `EnemyTemplate`)
|
||||||
|
- **JSON:** `tier` 필드 추가 (1, 2, 3).
|
||||||
|
- **Tier 1:** Goblin, Slime, Wolf, Orc Warrior(Elite).
|
||||||
|
- **Tier 2:** Bandit, Skeleton, Giant Spider(Elite).
|
||||||
|
- **Tier 3:** Shadow Assassin, Armored Bear, Dark Knight(Elite).
|
||||||
|
- **Template:** `tier` 필드를 파싱하고 저장하도록 클래스 업데이트.
|
||||||
|
|
||||||
|
### 로직 구현 (`EnemyTable`)
|
||||||
|
- **`getRandomEnemy(int stage)` 메서드 추가:**
|
||||||
|
- 현재 스테이지에 따라 목표 Tier를 결정 (1: 1~12, 2: 13~24, 3: 25+).
|
||||||
|
- 해당 Tier의 적 목록에서 랜덤 선택.
|
||||||
|
- 해당 Tier의 적이 없을 경우 하위 Tier 또는 전체 풀에서 선택하는 폴백 로직 포함.
|
||||||
|
|
||||||
|
### 전투 연동 (`BattleProvider`)
|
||||||
|
- `_prepareNextStage`에서 적 생성 시 `EnemyTable.getRandomEnemy`를 호출하여 스테이지에 적합한 적이 등장하도록 변경.
|
||||||
|
|
||||||
|
## 3. 결과 (Result)
|
||||||
|
- 게임 초반에는 약한 적, 후반에는 강한 적이 등장하여 단계적인 난이도 상승을 경험할 수 있습니다.
|
||||||
|
- 데이터 주도적으로 적의 등장 시기를 제어할 수 있게 되었습니다.
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
# 55. 스테이지 보상 로직 개선 (Refine Stage Reward Logic)
|
||||||
|
|
||||||
|
## 1. 목표 (Goal)
|
||||||
|
- 스테이지 티어(1~3) 및 타입(일반/엘리트)에 따라 아이템 보상의 희귀도(`Rarity`)를 조정하여 밸런스를 맞춥니다.
|
||||||
|
- **Tier 1:** 일반 전투에서는 Rare 등급 이상 등장 불가 (최대 Magic). 엘리트 전투 승리 시 첫 번째 보상으로 Rare 등급 확정.
|
||||||
|
- **Tier 2/3:** 엘리트 전투 승리 시 첫 번째 보상으로 Legendary 등급 확정.
|
||||||
|
|
||||||
|
## 2. 구현 상세 (Implementation Details)
|
||||||
|
|
||||||
|
### `ItemTable` 개선
|
||||||
|
- **`getRandomItem` 메서드 확장:**
|
||||||
|
- `minRarity`, `maxRarity` 선택적 매개변수 추가.
|
||||||
|
- 가중치 랜덤 선택 전에 희귀도 범위를 기반으로 후보군 및 가중치를 필터링하는 로직 구현.
|
||||||
|
|
||||||
|
### `BattleProvider` 수정
|
||||||
|
- **`_onVictory` 메서드 로직 변경:**
|
||||||
|
- 현재 스테이지의 `Tier`와 `isElite` 여부를 확인.
|
||||||
|
- **Tier 1 Normal:** `maxRarity`를 `Magic`으로 제한.
|
||||||
|
- **Tier 1 Elite:** 첫 번째 보상 생성 시 `minRarity`와 `maxRarity`를 `Rare`로 고정 (확정 Rare).
|
||||||
|
- **Tier 2/3 Elite:** 첫 번째 보상 생성 시 `minRarity`를 `Legendary`로 설정 (확정 Legendary).
|
||||||
|
- 나머지 보상 슬롯(2, 3번)은 해당 티어의 일반적인 규칙(Tier 1은 Magic 제한)을 따름.
|
||||||
|
|
||||||
|
## 3. 결과 (Result)
|
||||||
|
- 초반(Tier 1)에 지나치게 강력한 아이템이 나오는 것을 방지하고, 엘리트 몬스터 처치에 대한 확실한 보상(확정 Rare/Legendary)을 제공하여 도전 욕구를 고취시켰습니다.
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
# 56. 상점 UI 및 로직 디버깅 (Shop UI & Logic Debugging)
|
||||||
|
|
||||||
|
## 1. 목표 (Goal)
|
||||||
|
- 상점 구매 기능이 정상적으로 작동하지 않는 문제(성공 시에도 빨간색 에러 메시지 표시 등)를 해결합니다.
|
||||||
|
- `ShopUI`에서 `ShopProvider`와의 연동 로직을 개선하여 사용자 피드백(SnackBar)을 명확하게 만듭니다.
|
||||||
|
|
||||||
|
## 2. 구현 상세 (Implementation Details)
|
||||||
|
|
||||||
|
### `ShopUI` 수정
|
||||||
|
- **구매 확인 다이얼로그 (`_showBuyConfirmation`):**
|
||||||
|
- 기존: `buyItem` 호출 후 결과 확인 없이 다이얼로그 닫음 + `ShopProvider`의 메시지 상태에 의존하여 `build` 메서드에서 스낵바 출력 (타이밍 이슈 및 색상 고정 문제 발생).
|
||||||
|
- **변경:** `shopProvider.buyItem(item, player)`의 `bool` 반환값을 직접 확인.
|
||||||
|
- **성공 (`true`):** 다이얼로그 닫고 **초록색** "Bought [Item Name]" 스낵바 출력.
|
||||||
|
- **실패 (`false`):** 다이얼로그 닫고 **빨간색** 에러 메시지(Provider의 `lastShopMessage`) 스낵바 출력.
|
||||||
|
- **불필요한 코드 제거:** `build` 메서드 내의 `WidgetsBinding.instance.addPostFrameCallback` 블록 삭제.
|
||||||
|
|
||||||
|
### `ShopProvider` 확인
|
||||||
|
- `buyItem` 메서드는 이미 성공/실패 여부를 `bool`로 반환하고, 실패 시 `_lastShopMessage`를 설정하도록 잘 구현되어 있음. (수정 불필요)
|
||||||
|
|
||||||
|
## 3. 결과 (Result)
|
||||||
|
- 상점 아이템 구매 성공 시 정상적으로 초록색 메시지가 뜨고, 골드 부족이나 인벤토리 가득 참 등의 실패 시에는 빨간색 에러 메시지가 뜹니다.
|
||||||
|
- 구매 로직과 UI 피드백이 동기화되어 사용자 혼란을 방지했습니다.
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
# 57. 상점 구매 UI 동기화 버그 수정 (Fix Shop Purchase UI Sync Bug)
|
||||||
|
|
||||||
|
## 1. 목표 (Goal)
|
||||||
|
- 상점에서 아이템 구매 성공 시, 인벤토리 및 골드 상태 변화가 UI에 즉시 반영되지 않는 문제를 해결합니다.
|
||||||
|
- `ShopProvider`가 `BattleProvider` 소유의 `player` 객체를 수정했을 때, `BattleProvider`를 구독하는 위젯들이 갱신되도록 강제합니다.
|
||||||
|
|
||||||
|
## 2. 구현 상세 (Implementation Details)
|
||||||
|
|
||||||
|
### `BattleProvider`
|
||||||
|
- `refreshUI()` 메서드 추가: 단순히 `notifyListeners()`를 호출하여 `BattleProvider`의 상태 변경을 알리는 public 메서드.
|
||||||
|
|
||||||
|
### `ShopUI`
|
||||||
|
- **`_showBuyConfirmation` 수정:**
|
||||||
|
- `shopProvider.buyItem` 호출 후 성공(`true`) 시, `battleProvider.refreshUI()`를 호출.
|
||||||
|
- 이를 통해 `InventoryScreen`이나 상단 바의 골드 표시 등 `BattleProvider`를 구독하는 모든 UI가 재빌드되어, 변경된 인벤토리와 골드 상태를 즉시 반영.
|
||||||
|
|
||||||
|
## 3. 결과 (Result)
|
||||||
|
- 상점 구매 직후 인벤토리에 아이템이 정상적으로 표시되고, 소모된 골드가 UI에 즉시 업데이트됩니다.
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
# 58. 패배 시 저장 데이터 삭제 (Permadeath Implementation)
|
||||||
|
|
||||||
|
## 1. 목표 (Goal)
|
||||||
|
- 로그라이크 장르 특성에 맞춰 플레이어 패배(사망) 시 저장된 진행 데이터를 즉시 삭제하여 영구적인 죽음(Permadeath)을 구현합니다.
|
||||||
|
|
||||||
|
## 2. 구현 상세 (Implementation Details)
|
||||||
|
|
||||||
|
### `BattleProvider`
|
||||||
|
- **`_onDefeat` 메서드 추가:**
|
||||||
|
- 비동기(`async`) 메서드.
|
||||||
|
- "Player defeated! Enemy wins!" 로그 추가.
|
||||||
|
- `SaveManager.clearSaveData()` 호출하여 저장 파일 삭제.
|
||||||
|
- `notifyListeners()` 호출하여 UI 갱신.
|
||||||
|
- **패배 조건 체크 추가:**
|
||||||
|
- `playerAction`: 턴 시작 시 상태이상(출혈 등)으로 인한 사망 체크.
|
||||||
|
- `_enemyTurn`: 적 공격 후 및 턴 종료 시 사망 체크.
|
||||||
|
- 사망 확인 시 `_onDefeat` 호출.
|
||||||
|
|
||||||
|
## 3. 결과 (Result)
|
||||||
|
- 플레이어가 게임에서 패배하면 메인 메뉴로 돌아가더라도 '이어하기' 버튼이 활성화되지 않습니다(저장 데이터 삭제됨).
|
||||||
|
- 긴장감 있는 게임 플레이 환경이 조성되었습니다.
|
||||||
Loading…
Reference in New Issue