This commit is contained in:
2025-12-10 01:35:09 +09:00
parent 5b1ce14ed4
commit 8f72b9a812
20 changed files with 547 additions and 181 deletions
+2
View File
@@ -18,6 +18,8 @@ class GameConfig {
static const double stageHealRatio = 0.1;
static const double vulnerableDamageMultiplier = 1.5;
static const double armorDecayRate = 1.0;
static const double disarmedDamageMultiplier =
0.2; // New: Reduces ATK to 10% when disarmed
// Rewards
static const int baseGoldReward = 10;
+15 -3
View File
@@ -11,6 +11,7 @@ class EnemyTemplate {
final int baseHp;
final int baseAtk;
final int baseDefense;
final int baseDodge; // New: Base dodge chance
final String? image;
final List<String> equipmentIds;
final int tier;
@@ -20,6 +21,7 @@ class EnemyTemplate {
required this.baseHp,
required this.baseAtk,
required this.baseDefense,
this.baseDodge = 1, // Default value
this.image,
this.equipmentIds = const [],
this.tier = 1,
@@ -31,6 +33,7 @@ class EnemyTemplate {
baseHp: json['baseHp'] ?? 10,
baseAtk: json['baseAtk'] ?? 1,
baseDefense: json['baseDefense'] ?? 0,
baseDodge: json['baseDodge'] ?? 1, // Parse from JSON or default to 1
image: json['image'],
equipmentIds: (json['equipment'] as List<dynamic>?)?.cast<String>() ?? [],
tier: json['tier'] ?? 1,
@@ -46,6 +49,7 @@ class EnemyTemplate {
maxHp: baseHp,
atk: baseAtk,
baseDefense: baseDefense,
baseDodge: baseDodge, // Pass baseDodge to Character constructor
armor: 0,
image: image,
);
@@ -85,7 +89,10 @@ class EnemyTable {
}
/// Returns a random enemy suitable for the current stage.
static EnemyTemplate getRandomEnemy({required int stage, bool isElite = false}) {
static EnemyTemplate getRandomEnemy({
required int stage,
bool isElite = false,
}) {
int targetTier = 1;
if (stage > GameConfig.tier2StageMax) {
targetTier = 3;
@@ -94,7 +101,7 @@ class EnemyTable {
}
List<EnemyTemplate> pool = isElite ? eliteEnemies : normalEnemies;
// Filter by tier
var tierPool = pool.where((e) => e.tier == targetTier).toList();
@@ -108,7 +115,12 @@ class EnemyTable {
if (tierPool.isEmpty) {
// Should not happen if JSON is correct
return const EnemyTemplate(name: "Fallback Enemy", baseHp: 10, baseAtk: 1, baseDefense: 0);
return const EnemyTemplate(
name: "Fallback Enemy",
baseHp: 10,
baseAtk: 1,
baseDefense: 0,
);
}
return tierPool[_random.nextInt(tierPool.length)];
+3
View File
@@ -15,6 +15,7 @@ class ItemTemplate {
final int atkBonus;
final int hpBonus;
final int armorBonus;
final int dodge; // New
final EquipmentSlot slot;
final List<ItemEffect> effects;
final int price;
@@ -30,6 +31,7 @@ class ItemTemplate {
required this.atkBonus,
required this.hpBonus,
required this.armorBonus,
this.dodge = 0,
required this.slot,
required this.effects,
required this.price,
@@ -54,6 +56,7 @@ class ItemTemplate {
atkBonus: json['atkBonus'] ?? json['baseAtk'] ?? 0,
hpBonus: json['hpBonus'] ?? json['baseHp'] ?? 0,
armorBonus: json['armorBonus'] ?? json['baseArmor'] ?? 0,
dodge: json['dodge'] ?? 0,
slot: EquipmentSlot.values.firstWhere((e) => e.name == json['slot']),
effects: effectsList,
price: json['price'] ?? 10,
+4
View File
@@ -9,6 +9,7 @@ class PlayerTemplate {
final int baseHp;
final int baseAtk;
final int baseDefense;
final int baseDodge; // New field
final String? image;
const PlayerTemplate({
@@ -18,6 +19,7 @@ class PlayerTemplate {
required this.baseHp,
required this.baseAtk,
required this.baseDefense,
this.baseDodge = 1, // Default 1
this.image,
});
@@ -29,6 +31,7 @@ class PlayerTemplate {
baseHp: json['baseHp'],
baseAtk: json['baseAtk'],
baseDefense: json['baseDefense'],
baseDodge: json['baseDodge'] ?? 1, // Parse with default
image: json['image'],
);
}
@@ -39,6 +42,7 @@ class PlayerTemplate {
maxHp: baseHp,
atk: baseAtk,
baseDefense: baseDefense,
baseDodge: baseDodge, // Use template value
armor: 0,
);
}
+3 -1
View File
@@ -9,12 +9,14 @@ enum StatusEffectType {
vulnerable, // Takes 50% more damage
bleed, // Takes damage at start/end of turn
defenseForbidden, // Cannot use Defend action
disarmed, // Attack strength reduced (e.g., 10%)
}
/// 공격 실패 시 이펙트 피드백 타입 정의
enum BattleFeedbackType {
miss, // 공격이 빗나감
failed, // 방어 실패
dodge, // 회피 성공
}
/// 스탯에 적용될 수 있는 수정자(Modifier)의 타입 정의.
@@ -33,7 +35,7 @@ enum EquipmentSlot { weapon, armor, shield, accessory }
enum DamageType { normal, bleed, vulnerable }
enum StatType { maxHp, atk, defense, luck }
enum StatType { maxHp, atk, defense, luck, dodge }
enum ItemRarity { normal, magic, rare, legendary, unique }
+34 -14
View File
@@ -23,34 +23,45 @@ class CombatResult {
class CombatCalculator {
static final Random _random = Random();
/// Helper to get efficiency multiplier based on risk and action type.
static double getEfficiency(ActionType actionType, RiskLevel risk) {
switch (risk) {
case RiskLevel.safe:
return actionType == ActionType.attack
? BattleConfig.attackSafeEfficiency
: BattleConfig.defendSafeEfficiency;
case RiskLevel.normal:
return actionType == ActionType.attack
? BattleConfig.attackNormalEfficiency
: BattleConfig.defendNormalEfficiency;
case RiskLevel.risky:
return actionType == ActionType.attack
? BattleConfig.attackRiskyEfficiency
: BattleConfig.defendRiskyEfficiency;
}
}
/// Calculates success and efficiency based on Risk Level and Luck.
static CombatResult calculateActionOutcome({
required ActionType actionType, // New: Action type (attack or defend)
required RiskLevel risk,
required int luck,
required int baseValue,
Random? random, // Injectable Random
}) {
double efficiency = 1.0;
final effectiveRandom = random ?? _random;
double efficiency = getEfficiency(actionType, risk);
double baseChance = 0.0;
switch (risk) {
case RiskLevel.safe:
baseChance = BattleConfig.safeBaseChance;
efficiency = actionType == ActionType.attack
? BattleConfig.attackSafeEfficiency
: BattleConfig.defendSafeEfficiency;
break;
case RiskLevel.normal:
baseChance = BattleConfig.normalBaseChance;
efficiency = actionType == ActionType.attack
? BattleConfig.attackNormalEfficiency
: BattleConfig.defendNormalEfficiency;
break;
case RiskLevel.risky:
baseChance = BattleConfig.riskyBaseChance;
efficiency = actionType == ActionType.attack
? BattleConfig.attackRiskyEfficiency
: BattleConfig.defendRiskyEfficiency;
break;
}
@@ -58,7 +69,7 @@ class CombatCalculator {
double chance = baseChance + (luck / 100.0);
if (chance > 1.0) chance = 1.0;
bool success = _random.nextDouble() < chance;
bool success = effectiveRandom.nextDouble() < chance;
int finalValue = (baseValue * efficiency).toInt();
if (finalValue < 1 && baseValue > 0) finalValue = 1;
@@ -146,12 +157,13 @@ class CombatCalculator {
/// Tries to apply status effects from attacker's equipment.
/// Returns a list of applied effects.
static List<StatusEffect> getAppliedEffects(Character attacker) {
static List<StatusEffect> getAppliedEffects(Character attacker, {Random? random}) {
final effectiveRandom = random ?? _random;
List<StatusEffect> appliedEffects = [];
for (var item in attacker.equipment.values) {
for (var effect in item.effects) {
if (_random.nextInt(100) < effect.probability) {
if (effectiveRandom.nextInt(100) < effect.probability) {
appliedEffects.add(
StatusEffect(
type: effect.type,
@@ -164,4 +176,12 @@ class CombatCalculator {
}
return appliedEffects;
}
/// Calculates if a dodge occurs.
/// [targetDodge] is the total dodge chance percentage (e.g. 5 = 5%).
static bool calculateDodge(int targetDodge, {Random? random}) {
final effectiveRandom = random ?? _random;
if (targetDodge <= 0) return false;
return effectiveRandom.nextInt(100) < targetDodge;
}
}
+10
View File
@@ -16,6 +16,7 @@ class LootGenerator {
int finalHp = template.hpBonus;
int finalArmor = template.armorBonus;
int finalLuck = template.luck;
int finalDodge = template.dodge;
// 0. Normal Rarity: Prefix logic for base stat variations
if (template.rarity == ItemRarity.normal) {
@@ -44,6 +45,8 @@ class LootGenerator {
finalAtk = (finalAtk * mult).floor();
finalHp = (finalHp * mult).floor();
finalArmor = (finalArmor * mult).floor();
// Dodge typically stays integer, but if we want to scale it:
// finalDodge = (finalDodge * mult).floor();
}
}
}
@@ -75,6 +78,9 @@ class LootGenerator {
case StatType.luck:
finalLuck += value;
break;
case StatType.dodge: // Handle dodge
finalDodge += value;
break;
}
});
}
@@ -117,6 +123,9 @@ class LootGenerator {
case StatType.luck:
finalLuck += value;
break;
case StatType.dodge: // Handle dodge
finalDodge += value;
break;
}
});
}
@@ -130,6 +139,7 @@ class LootGenerator {
atkBonus: finalAtk,
hpBonus: finalHp,
armorBonus: finalArmor,
dodge: finalDodge, // Pass dodge
slot: template.slot,
effects: template.effects,
price: template.price,
+15 -1
View File
@@ -12,6 +12,7 @@ class Character {
int armor; // Current temporary shield/armor points in battle
int baseAtk;
int baseDefense; // Base defense stat
int baseDodge; // New: Base dodge chance (e.g. 1 = 1%)
int gold; // New: Currency
String? image; // New: Image path
@@ -32,6 +33,7 @@ class Character {
required this.armor,
required int atk,
this.baseDefense = 0,
this.baseDodge = 1,
this.gold = 0,
this.image,
}) : baseMaxHp = maxHp,
@@ -46,6 +48,7 @@ class Character {
'armor': armor,
'baseAtk': baseAtk,
'baseDefense': baseDefense,
'baseDodge': baseDodge,
'gold': gold,
'image': image,
'equipment': equipment.map((key, value) => MapEntry(key.name, value.id)),
@@ -63,6 +66,7 @@ class Character {
armor: json['armor'],
atk: json['baseAtk'],
baseDefense: json['baseDefense'],
baseDodge: json['baseDodge'] ?? 1,
gold: json['gold'],
image: json['image'],
);
@@ -161,7 +165,12 @@ class Character {
int get totalAtk {
int bonus = equipment.values.fold(0, (sum, item) => sum + item.atkBonus);
return baseAtk + bonus;
int finalAtk = baseAtk + bonus;
if (hasStatus(StatusEffectType.disarmed)) {
finalAtk = (finalAtk * GameConfig.disarmedDamageMultiplier).toInt();
}
return finalAtk;
}
int get totalDefense {
@@ -169,6 +178,11 @@ class Character {
return baseDefense + bonus;
}
int get totalDodge {
int bonus = equipment.values.fold(0, (sum, item) => sum + item.dodge);
return baseDodge + bonus;
}
int get totalLuck {
return equipment.values.fold(0, (sum, item) => sum + item.luck);
}
+3
View File
@@ -27,6 +27,7 @@ class ItemEffect {
String typeStr = type.name.toUpperCase();
// Customize names if needed
if (type == StatusEffectType.defenseForbidden) typeStr = "UNBLOCKABLE";
if (type == StatusEffectType.disarmed) typeStr = "DISARM";
String durationStr = "${duration}t";
String valStr = value > 0 ? " ($value dmg)" : "";
@@ -42,6 +43,7 @@ class Item {
final int atkBonus;
final int hpBonus;
final int armorBonus; // New stat for defense
final int dodge; // New: Dodge chance bonus
final EquipmentSlot slot;
final List<ItemEffect> effects; // Status effects this item can inflict
final int price; // New: Sell/Buy value
@@ -57,6 +59,7 @@ class Item {
required this.atkBonus,
required this.hpBonus,
this.armorBonus = 0, // Default to 0 for backward compatibility
this.dodge = 0, // Default to 0
required this.slot,
this.effects = const [], // Default to no effects
this.price = 0,
+158 -43
View File
@@ -70,8 +70,10 @@ class BattleProvider with ChangeNotifier {
// Dependency injection
final ShopProvider shopProvider;
final Random _random; // Injected Random instance
BattleProvider({required this.shopProvider}) {
BattleProvider({required this.shopProvider, Random? random})
: _random = random ?? Random() {
// initializeBattle(); // Do not auto-start logic
}
@@ -134,6 +136,9 @@ class BattleProvider with ChangeNotifier {
// Save Game at the start of each stage
SaveManager.saveGame(this);
// Reset Player Armor at start of new stage
player.armor = 0;
StageType type;
// Stage Type Logic
@@ -214,10 +219,10 @@ class BattleProvider with ChangeNotifier {
player.hasStatus(StatusEffectType.defenseForbidden)) {
_addLog("Cannot defend! You are under Defense Forbidden status.");
notifyListeners(); // 상태 변경을 알림
_endPlayerTurn();
// _endPlayerTurn(); // Allow player to choose another action
return;
}
isPlayerTurn = false;
notifyListeners();
@@ -246,28 +251,48 @@ class BattleProvider with ChangeNotifier {
risk: risk,
luck: player.totalLuck,
baseValue: baseValue,
random: _random, // Pass injected random
);
if (result.success) {
if (type == ActionType.attack) {
int damage = result.value;
// 1. Check for Dodge (Moved from _processAttackImpact)
if (CombatCalculator.calculateDodge(enemy.totalDodge, random: _random)) { // Pass injected random
_addLog("${enemy.name} dodged the attack!");
final event = EffectEvent(
id:
DateTime.now().millisecondsSinceEpoch.toString() +
_random.nextInt(1000).toString(), // Use injected random
type: ActionType.attack,
risk: risk,
target: EffectTarget.enemy,
feedbackType: BattleFeedbackType.dodge, // Dodge feedback
attacker: player,
targetEntity: enemy,
damageValue: 0,
isSuccess:
false, // Treated as fail for animation purposes (or custom)
);
_effectEventController.sink.add(event);
} else {
// 2. Hit Success
int damage = result.value;
final event = EffectEvent(
id:
DateTime.now().millisecondsSinceEpoch.toString() +
Random().nextInt(1000).toString(),
type: ActionType.attack,
risk: risk,
target: EffectTarget.enemy,
feedbackType: null,
attacker: player,
targetEntity: enemy,
damageValue: damage,
isSuccess: true,
);
_effectEventController.sink.add(
event,
); // No Future.delayed here, BattleScreen will trigger impact
final event = EffectEvent(
id:
DateTime.now().millisecondsSinceEpoch.toString() +
_random.nextInt(1000).toString(), // Use injected random
type: ActionType.attack,
risk: risk,
target: EffectTarget.enemy,
feedbackType: null,
attacker: player,
targetEntity: enemy,
damageValue: damage,
isSuccess: true,
);
_effectEventController.sink.add(event);
}
} else {
// Defense Success - Impact is immediate, so process it directly
final event = EffectEvent(
@@ -348,6 +373,40 @@ class BattleProvider with ChangeNotifier {
);
}
/// Recalculates the current enemy intent value based on current stats.
/// Used to update UI when enemy stats change (e.g. Disarmed applied).
void updateEnemyIntent() {
if (currentEnemyIntent == null || enemy.isDead) return;
final intent = currentEnemyIntent!;
int newValue = 0;
// Recalculate value based on current stats
if (intent.type == EnemyActionType.attack) {
newValue = (enemy.totalAtk *
CombatCalculator.getEfficiency(ActionType.attack, intent.risk))
.toInt();
if (newValue < 1 && enemy.totalAtk > 0) newValue = 1;
} else {
newValue = (enemy.totalDefense *
CombatCalculator.getEfficiency(ActionType.defend, intent.risk))
.toInt();
if (newValue < 1 && enemy.totalDefense > 0) newValue = 1;
}
// Replace intent with updated value, keeping other properties
currentEnemyIntent = EnemyIntent(
type: intent.type,
value: newValue,
risk: intent.risk,
description: "$newValue (${intent.risk.name})",
isSuccess: intent.isSuccess,
finalValue: newValue,
isApplied: intent.isApplied,
);
notifyListeners();
}
// --- Turn Management Phases ---
// Phase 4: Start Player Turn
@@ -364,6 +423,9 @@ class BattleProvider with ChangeNotifier {
return;
}
// Update Intent if stats changed (e.g. status effects expired)
updateEnemyIntent();
// [New] Apply Pre-emptive Enemy Intent (Defense/Buffs)
// MOVED: Logic moved to applyPendingEnemyDefense() to sync with animation.
// We just check intent existence here but do NOT apply effects yet.
@@ -430,7 +492,8 @@ class BattleProvider with ChangeNotifier {
}
// Process Start-of-Turn Effects
bool canAct = _processStartTurnEffects(enemy);
final result = CombatCalculator.processStartTurnEffects(enemy);
bool canAct = !result['isStunned'];
if (enemy.isDead) {
_onVictory();
@@ -456,17 +519,43 @@ class BattleProvider with ChangeNotifier {
} else {
// Attack Action (Animating)
if (intent.isSuccess) {
// 1. Check for Dodge
if (CombatCalculator.calculateDodge(player.totalDodge, random: _random)) { // Pass injected random
_addLog("${player.name} dodged the attack!");
final event = EffectEvent(
id:
DateTime.now().millisecondsSinceEpoch.toString() +
_random.nextInt(1000).toString(), // Use injected random
type: ActionType.attack,
risk: intent.risk,
target: EffectTarget.player,
feedbackType: BattleFeedbackType.dodge,
attacker: enemy,
targetEntity: player,
damageValue: 0,
isSuccess: false,
);
_effectEventController.sink.add(event);
return;
}
// Recalculate damage to account for status changes (like Disarmed)
int finalDamage = (enemy.totalAtk *
CombatCalculator.getEfficiency(ActionType.attack, intent.risk))
.toInt();
if (finalDamage < 1 && enemy.totalAtk > 0) finalDamage = 1;
final event = EffectEvent(
id:
DateTime.now().millisecondsSinceEpoch.toString() +
Random().nextInt(1000).toString(),
_random.nextInt(1000).toString(), // Use injected random
type: ActionType.attack,
risk: intent.risk,
target: EffectTarget.player,
feedbackType: null,
attacker: enemy,
targetEntity: player,
damageValue: intent.finalValue,
damageValue: finalDamage,
isSuccess: true,
);
_effectEventController.sink.add(event);
@@ -490,16 +579,17 @@ class BattleProvider with ChangeNotifier {
_effectEventController.sink.add(event);
return;
}
}
} else if (!canAct) {
_addLog("Enemy is stunned and cannot act!");
int tid = _turnTransactionId;
Future.delayed(const Duration(milliseconds: 500), () {
if (tid != _turnTransactionId) return;
_endEnemyTurn();
});
} else {
_addLog("Enemy did nothing.");
}
} else if (!canAct) { // If cannot act (stunned)
_addLog("Enemy is stunned and cannot act!");
int tid = _turnTransactionId;
Future.delayed(const Duration(milliseconds: 500), () {
if (tid != _turnTransactionId) return;
_endEnemyTurn();
});
} else {
_addLog("Enemy did nothing.");
int tid = _turnTransactionId;
Future.delayed(const Duration(milliseconds: 500), () {
if (tid != _turnTransactionId) return;
@@ -512,6 +602,9 @@ class BattleProvider with ChangeNotifier {
void _endEnemyTurn() {
if (player.isDead) return; // Game Over check
// Update enemy status at the end of their turn
enemy.updateStatusEffects();
// Generate NEXT intent
_generateEnemyIntent();
@@ -690,29 +783,45 @@ class BattleProvider with ChangeNotifier {
_prepareNextStage();
}
@visibleForTesting
void generateEnemyIntent() {
_generateEnemyIntent();
}
void _generateEnemyIntent() {
if (enemy.isDead) {
currentEnemyIntent = null;
return;
}
final random = Random();
// Use the injected _random field
// final random = Random(); // Removed
// Decide Action Type
bool canDefend = enemy.baseDefense > 0;
if (enemy.hasStatus(StatusEffectType.defenseForbidden)) {
canDefend = false;
}
bool isAttack = true;
// Check constraints
bool canDefend = enemy.baseDefense > 0 &&
!enemy.hasStatus(StatusEffectType.defenseForbidden);
bool canAttack = true; // Attack is always possible, but strength is affected by status.
if (canDefend) {
isAttack = random.nextDouble() < BattleConfig.enemyAttackChance;
} else {
bool isAttack = true; // Default to attack
if (canAttack && canDefend) {
// Both options available: Use configured probability
isAttack = _random.nextDouble() < BattleConfig.enemyAttackChance;
} else if (canAttack) {
// Must attack
isAttack = true;
} else if (canDefend) {
// Must defend
isAttack = false;
} else {
// Both forbidden (Rare case, effectively stunned but not via Stun status)
// Default to Defend as a fallback, outcomes will be handled by stats/luck
isAttack = false;
}
// Decide Risk Level
RiskLevel risk = RiskLevel.values[random.nextInt(RiskLevel.values.length)];
RiskLevel risk = RiskLevel.values[_random.nextInt(RiskLevel.values.length)];
CombatResult result;
if (isAttack) {
@@ -872,6 +981,11 @@ class BattleProvider with ChangeNotifier {
// Try applying status effects
_tryApplyStatusEffects(attacker, target);
// If target is enemy, update intent to reflect potential status changes (e.g. Disarmed)
if (target == enemy) {
updateEnemyIntent();
}
} else if (event.type == ActionType.defend) {
// Defense Impact is immediate (no anim delay from UI)
if (event.isSuccess!) {
@@ -900,6 +1014,7 @@ class BattleProvider with ChangeNotifier {
void _tryApplyStatusEffects(Character attacker, Character target) {
List<StatusEffect> effectsToApply = CombatCalculator.getAppliedEffects(
attacker,
random: _random, // Pass injected random
);
for (var effect in effectsToApply) {
+8 -2
View File
@@ -248,6 +248,10 @@ class _BattleScreenState extends State<BattleScreen> {
feedbackText = "FAILED";
feedbackColor = ThemeConfig.failedText;
break;
case BattleFeedbackType.dodge:
feedbackText = "DODGE";
feedbackColor = ThemeConfig.statLuckColor; // Use Luck color (Greenish)
break;
default:
feedbackText = "";
feedbackColor = ThemeConfig.textColorWhite;
@@ -649,14 +653,15 @@ class _BattleScreenState extends State<BattleScreen> {
!battleProvider.enemy.isDead &&
!battleProvider.showRewardPopup &&
!_isPlayerAttacking &&
!_isEnemyAttacking,
!_isEnemyAttacking, // Enabled even if disarmed (damage reduced)
isDefendEnabled:
battleProvider.isPlayerTurn &&
!battleProvider.player.isDead &&
!battleProvider.enemy.isDead &&
!battleProvider.showRewardPopup &&
!_isPlayerAttacking &&
!_isEnemyAttacking,
!_isEnemyAttacking &&
!battleProvider.player.hasStatus(StatusEffectType.defenseForbidden), // Disable if defense is forbidden
onAttackPressed: () =>
_showRiskLevelSelection(context, ActionType.attack),
onDefendPressed: () =>
@@ -859,6 +864,7 @@ class _BattleScreenState extends State<BattleScreen> {
if (item.hpBonus > 0) stats.add("+${item.hpBonus} ${AppStrings.hp}");
if (item.armorBonus > 0) stats.add("+${item.armorBonus} ${AppStrings.def}");
if (item.luck > 0) stats.add("+${item.luck} ${AppStrings.luck}");
if (item.dodge > 0) stats.add("+${item.dodge}% Dodge"); // Add Dodge
List<String> effectTexts = item.effects.map((e) => e.description).toList();
@@ -35,18 +35,23 @@ class CharacterStatsWidget extends StatelessWidget {
_buildStatItem(
AppStrings.atk,
"${player.totalAtk}",
color: ThemeConfig.statAtkColor,
// color: ThemeConfig.statAtkColor,
),
_buildStatItem(
AppStrings.def,
"${player.totalDefense}",
color: ThemeConfig.statDefColor,
// color: ThemeConfig.statDefColor,
),
_buildStatItem(AppStrings.armor, "${player.armor}"),
// _buildStatItem(AppStrings.armor, "${player.armor}"),
_buildStatItem(
AppStrings.luck,
"${player.totalLuck}",
color: ThemeConfig.statLuckColor,
// color: ThemeConfig.statLuckColor,
),
_buildStatItem(
"Dodge", // TODO: Add to AppStrings
"${player.totalDodge}%",
// color: ThemeConfig.statLuckColor,
),
_buildStatItem(
AppStrings.gold,
@@ -263,6 +263,11 @@ class InventoryGridWidget extends StatelessWidget {
player.totalLuck,
player.totalLuck - (oldItem?.luck ?? 0) + newItem.luck,
),
_buildStatChangeRow(
"Dodge",
player.totalDodge,
player.totalDodge - (oldItem?.dodge ?? 0) + newItem.dodge,
),
],
),
actions: [