Refactor BattleProvider: Introduce CombatCalculator and BattleLogManager
This commit is contained in:
+123
-145
@@ -20,6 +20,9 @@ import '../game/save_manager.dart';
|
||||
import '../game/config/game_config.dart';
|
||||
import 'shop_provider.dart'; // Import ShopProvider
|
||||
|
||||
import '../game/logic/battle_log_manager.dart';
|
||||
import '../game/logic/combat_calculator.dart';
|
||||
|
||||
class EnemyIntent {
|
||||
final EnemyActionType type;
|
||||
final int value;
|
||||
@@ -45,7 +48,7 @@ class BattleProvider with ChangeNotifier {
|
||||
late StageModel currentStage; // The current stage object
|
||||
EnemyIntent? currentEnemyIntent;
|
||||
|
||||
List<String> battleLogs = [];
|
||||
final BattleLogManager _logManager = BattleLogManager();
|
||||
bool isPlayerTurn = true;
|
||||
|
||||
int stage = 1;
|
||||
@@ -54,7 +57,7 @@ class BattleProvider with ChangeNotifier {
|
||||
bool showRewardPopup = false;
|
||||
int _lastGoldReward = 0; // New: Stores gold gained from last victory
|
||||
|
||||
List<String> get logs => battleLogs;
|
||||
List<String> get logs => _logManager.logs;
|
||||
int get lastGoldReward => _lastGoldReward;
|
||||
|
||||
void refreshUI() {
|
||||
@@ -88,7 +91,7 @@ class BattleProvider with ChangeNotifier {
|
||||
turnCount = data['turnCount'];
|
||||
player = Character.fromJson(data['player']);
|
||||
|
||||
battleLogs.clear();
|
||||
_logManager.clear();
|
||||
_addLog("Game Loaded! Resuming Stage $stage");
|
||||
|
||||
_prepareNextStage();
|
||||
@@ -170,7 +173,7 @@ class BattleProvider with ChangeNotifier {
|
||||
player.addToInventory(ItemTable.shields[3].createItem()); // Cursed Shield
|
||||
|
||||
_prepareNextStage();
|
||||
battleLogs.clear();
|
||||
_logManager.clear();
|
||||
_addLog("Game Started! Stage 1");
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -282,37 +285,18 @@ class BattleProvider with ChangeNotifier {
|
||||
|
||||
_addLog("Player chose to ${type.name} with ${risk.name} risk.");
|
||||
|
||||
final random = Random();
|
||||
bool success = false;
|
||||
double efficiency = 1.0;
|
||||
// Calculate Outcome using CombatCalculator
|
||||
int baseValue = (type == ActionType.attack) ? player.totalAtk : player.totalDefense;
|
||||
|
||||
final result = CombatCalculator.calculateActionOutcome(
|
||||
risk: risk,
|
||||
luck: player.totalLuck,
|
||||
baseValue: baseValue
|
||||
);
|
||||
|
||||
switch (risk) {
|
||||
case RiskLevel.safe:
|
||||
// Safe: 100% base chance + luck
|
||||
double chance = 1.0 + (player.totalLuck / 100.0);
|
||||
if (chance > 1.0) chance = 1.0;
|
||||
success = random.nextDouble() < chance;
|
||||
efficiency = 0.5; // 50%
|
||||
break;
|
||||
case RiskLevel.normal:
|
||||
// Normal: 80% base chance + luck
|
||||
double chance = 0.8 + (player.totalLuck / 100.0);
|
||||
if (chance > 1.0) chance = 1.0;
|
||||
success = random.nextDouble() < chance;
|
||||
efficiency = 1.0; // 100%
|
||||
break;
|
||||
case RiskLevel.risky:
|
||||
// Risky: 40% base chance + luck
|
||||
double chance = 0.4 + (player.totalLuck / 100.0);
|
||||
if (chance > 1.0) chance = 1.0;
|
||||
success = random.nextDouble() < chance;
|
||||
efficiency = 2.0; // 200%
|
||||
break;
|
||||
}
|
||||
|
||||
if (success) {
|
||||
if (result.success) {
|
||||
if (type == ActionType.attack) {
|
||||
int damage = (player.totalAtk * efficiency).toInt();
|
||||
int damage = result.value;
|
||||
|
||||
final eventId =
|
||||
DateTime.now().millisecondsSinceEpoch.toString() +
|
||||
@@ -323,50 +307,70 @@ class BattleProvider with ChangeNotifier {
|
||||
type: ActionType.attack,
|
||||
risk: risk,
|
||||
target: EffectTarget.enemy,
|
||||
feedbackType: null, // 공격 성공이므로 feedbackType 없음
|
||||
feedbackType: null,
|
||||
),
|
||||
);
|
||||
|
||||
// Animation Delays to sync with Impact
|
||||
if (risk == RiskLevel.safe) {
|
||||
await Future.delayed(
|
||||
const Duration(milliseconds: GameConfig.animDelaySafe),
|
||||
);
|
||||
} else if (risk == RiskLevel.normal) {
|
||||
await Future.delayed(
|
||||
const Duration(milliseconds: GameConfig.animDelayNormal),
|
||||
);
|
||||
} else if (risk == RiskLevel.risky) {
|
||||
await Future.delayed(
|
||||
const Duration(milliseconds: GameConfig.animDelayRisky),
|
||||
);
|
||||
}
|
||||
// Animation Delays
|
||||
int delay = GameConfig.animDelayNormal;
|
||||
if (risk == RiskLevel.safe) delay = GameConfig.animDelaySafe;
|
||||
if (risk == RiskLevel.risky) delay = GameConfig.animDelayRisky;
|
||||
|
||||
await Future.delayed(Duration(milliseconds: delay));
|
||||
|
||||
int damageToHp = 0;
|
||||
// Calculate Damage to HP using CombatCalculator
|
||||
int damageToHp = CombatCalculator.calculateDamageToHp(
|
||||
incomingDamage: damage,
|
||||
currentArmor: enemy.armor,
|
||||
isVulnerable: enemy.hasStatus(StatusEffectType.vulnerable)
|
||||
);
|
||||
|
||||
// Calculate Remaining Armor
|
||||
int remainingArmor = CombatCalculator.calculateRemainingArmor(
|
||||
incomingDamage: damage,
|
||||
currentArmor: enemy.armor,
|
||||
isVulnerable: enemy.hasStatus(StatusEffectType.vulnerable)
|
||||
);
|
||||
|
||||
// Log details
|
||||
if (enemy.armor > 0) {
|
||||
if (enemy.armor >= damage) {
|
||||
enemy.armor -= damage;
|
||||
damageToHp = 0;
|
||||
_addLog("Enemy's armor absorbed all $damage damage.");
|
||||
} else {
|
||||
damageToHp = damage - enemy.armor;
|
||||
_addLog("Enemy's armor absorbed ${enemy.armor} damage.");
|
||||
enemy.armor = 0;
|
||||
}
|
||||
} else {
|
||||
damageToHp = damage;
|
||||
int absorbed = enemy.armor - remainingArmor;
|
||||
if (damageToHp == 0) {
|
||||
_addLog("Enemy's armor absorbed all damage.");
|
||||
} else {
|
||||
_addLog("Enemy's armor absorbed $absorbed damage.");
|
||||
}
|
||||
}
|
||||
|
||||
enemy.armor = remainingArmor;
|
||||
|
||||
if (damageToHp > 0) {
|
||||
_applyDamage(enemy, damageToHp, targetType: DamageTarget.enemy);
|
||||
// Note: _applyDamage internally handles Vulnerable multiplier again for the DamageEvent and logs.
|
||||
// To avoid double application, we should just pass the raw damage to _applyDamage
|
||||
// OR refactor _applyDamage.
|
||||
// Let's refactor _applyDamage to just apply the final value since we calculated it here.
|
||||
// actually _applyDamage handles the reduction of HP.
|
||||
// Let's call a simplified version or just do it here.
|
||||
|
||||
enemy.hp -= damageToHp;
|
||||
if (enemy.hp < 0) enemy.hp = 0;
|
||||
|
||||
_damageEventController.sink.add(
|
||||
DamageEvent(
|
||||
damage: damageToHp,
|
||||
target: DamageTarget.enemy,
|
||||
type: enemy.hasStatus(StatusEffectType.vulnerable) ? DamageType.vulnerable : DamageType.normal
|
||||
),
|
||||
);
|
||||
_addLog("Player dealt $damageToHp damage to Enemy.");
|
||||
} else {
|
||||
_addLog("Player's attack was fully blocked by armor.");
|
||||
}
|
||||
|
||||
// Try applying status effects from items
|
||||
// Try applying status effects
|
||||
_tryApplyStatusEffects(player, enemy);
|
||||
} else {
|
||||
// Defense Success
|
||||
_effectEventController.sink.add(
|
||||
EffectEvent(
|
||||
id:
|
||||
@@ -375,15 +379,16 @@ class BattleProvider with ChangeNotifier {
|
||||
type: ActionType.defend,
|
||||
risk: risk,
|
||||
target: EffectTarget.player,
|
||||
feedbackType: null, // 방어 성공이므로 feedbackType 없음
|
||||
feedbackType: null,
|
||||
),
|
||||
);
|
||||
|
||||
int armorGained = (player.totalDefense * efficiency).toInt();
|
||||
int armorGained = result.value;
|
||||
player.armor += armorGained;
|
||||
_addLog("Player gained $armorGained armor.");
|
||||
}
|
||||
} else {
|
||||
// Failure
|
||||
if (type == ActionType.attack) {
|
||||
_addLog("Player's attack missed!");
|
||||
_effectEventController.sink.add(
|
||||
@@ -393,7 +398,7 @@ class BattleProvider with ChangeNotifier {
|
||||
Random().nextInt(1000).toString(),
|
||||
type: type,
|
||||
risk: risk,
|
||||
target: EffectTarget.enemy, // 공격 실패는 적 위치에 MISS
|
||||
target: EffectTarget.enemy,
|
||||
feedbackType: BattleFeedbackType.miss,
|
||||
),
|
||||
);
|
||||
@@ -406,7 +411,7 @@ class BattleProvider with ChangeNotifier {
|
||||
Random().nextInt(1000).toString(),
|
||||
type: type,
|
||||
risk: risk,
|
||||
target: EffectTarget.player, // 방어 실패는 내 위치에 FAILED
|
||||
target: EffectTarget.player,
|
||||
feedbackType: BattleFeedbackType.failed,
|
||||
),
|
||||
);
|
||||
@@ -484,25 +489,41 @@ class BattleProvider with ChangeNotifier {
|
||||
);
|
||||
|
||||
int incomingDamage = intent.finalValue;
|
||||
int damageToHp = 0;
|
||||
|
||||
// Calculate Damage using Calculator
|
||||
int damageToHp = CombatCalculator.calculateDamageToHp(
|
||||
incomingDamage: incomingDamage,
|
||||
currentArmor: player.armor,
|
||||
isVulnerable: player.hasStatus(StatusEffectType.vulnerable)
|
||||
);
|
||||
|
||||
int remainingArmor = CombatCalculator.calculateRemainingArmor(
|
||||
incomingDamage: incomingDamage,
|
||||
currentArmor: player.armor,
|
||||
isVulnerable: player.hasStatus(StatusEffectType.vulnerable)
|
||||
);
|
||||
|
||||
// Handle Player Armor
|
||||
if (player.armor > 0) {
|
||||
if (player.armor >= incomingDamage) {
|
||||
player.armor -= incomingDamage;
|
||||
damageToHp = 0;
|
||||
_addLog("Armor absorbed all $incomingDamage damage.");
|
||||
} else {
|
||||
damageToHp = incomingDamage - player.armor;
|
||||
_addLog("Armor absorbed ${player.armor} damage.");
|
||||
player.armor = 0;
|
||||
}
|
||||
} else {
|
||||
damageToHp = incomingDamage;
|
||||
}
|
||||
if (player.armor > 0) {
|
||||
int absorbed = player.armor - remainingArmor;
|
||||
if (damageToHp == 0) {
|
||||
_addLog("Armor absorbed all damage.");
|
||||
} else {
|
||||
_addLog("Armor absorbed $absorbed damage.");
|
||||
}
|
||||
}
|
||||
player.armor = remainingArmor;
|
||||
|
||||
if (damageToHp > 0) {
|
||||
_applyDamage(player, damageToHp, targetType: DamageTarget.player);
|
||||
player.hp -= damageToHp;
|
||||
if (player.hp < 0) player.hp = 0;
|
||||
|
||||
_damageEventController.sink.add(
|
||||
DamageEvent(
|
||||
damage: damageToHp,
|
||||
target: DamageTarget.player,
|
||||
type: player.hasStatus(StatusEffectType.vulnerable) ? DamageType.vulnerable : DamageType.normal
|
||||
),
|
||||
);
|
||||
_addLog("Enemy dealt $damageToHp damage to Player HP.");
|
||||
}
|
||||
|
||||
@@ -554,92 +575,49 @@ class BattleProvider with ChangeNotifier {
|
||||
/// Process effects that happen at the start of the turn (Bleed, Stun).
|
||||
/// Returns true if the character can act, false if stunned.
|
||||
bool _processStartTurnEffects(Character character) {
|
||||
bool canAct = true;
|
||||
final result = CombatCalculator.processStartTurnEffects(character);
|
||||
|
||||
int totalBleed = result['bleedDamage'];
|
||||
bool isStunned = result['isStunned'];
|
||||
|
||||
// 1. Bleed Damage
|
||||
var bleedEffects = character.statusEffects
|
||||
.where((e) => e.type == StatusEffectType.bleed)
|
||||
.toList();
|
||||
if (bleedEffects.isNotEmpty) {
|
||||
int totalBleed = bleedEffects.fold(0, (sum, e) => sum + e.value);
|
||||
int previousHp = character.hp; // Record HP before damage
|
||||
if (totalBleed > 0) {
|
||||
character.hp -= totalBleed;
|
||||
if (character.hp < 0) character.hp = 0;
|
||||
_addLog("${character.name} takes $totalBleed bleed damage!");
|
||||
|
||||
// Emit DamageEvent for bleed
|
||||
if (character == player) {
|
||||
_damageEventController.sink.add(
|
||||
DamageEvent(
|
||||
damage: totalBleed,
|
||||
target: DamageTarget.player,
|
||||
type: DamageType.bleed,
|
||||
),
|
||||
);
|
||||
} else if (character == enemy) {
|
||||
_damageEventController.sink.add(
|
||||
DamageEvent(
|
||||
damage: totalBleed,
|
||||
target: DamageTarget.enemy,
|
||||
type: DamageType.bleed,
|
||||
),
|
||||
);
|
||||
}
|
||||
_damageEventController.sink.add(
|
||||
DamageEvent(
|
||||
damage: totalBleed,
|
||||
target: (character == player) ? DamageTarget.player : DamageTarget.enemy,
|
||||
type: DamageType.bleed,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Stun Check
|
||||
if (character.hasStatus(StatusEffectType.stun)) {
|
||||
canAct = false;
|
||||
if (isStunned) {
|
||||
_addLog("${character.name} is stunned!");
|
||||
}
|
||||
|
||||
return canAct;
|
||||
return !isStunned;
|
||||
}
|
||||
|
||||
/// Tries to apply status effects from attacker's equipment to the target.
|
||||
void _tryApplyStatusEffects(Character attacker, Character target) {
|
||||
final random = Random();
|
||||
|
||||
for (var item in attacker.equipment.values) {
|
||||
for (var effect in item.effects) {
|
||||
// Roll for probability (0-100)
|
||||
if (random.nextInt(100) < effect.probability) {
|
||||
// Apply effect
|
||||
final newStatus = StatusEffect(
|
||||
type: effect.type,
|
||||
duration: effect.duration,
|
||||
value: effect.value,
|
||||
);
|
||||
target.addStatusEffect(newStatus);
|
||||
_addLog("Applied ${effect.type.name} to ${target.name}!");
|
||||
}
|
||||
}
|
||||
List<StatusEffect> effectsToApply = CombatCalculator.getAppliedEffects(attacker);
|
||||
|
||||
for (var effect in effectsToApply) {
|
||||
target.addStatusEffect(effect);
|
||||
_addLog("Applied ${effect.type.name} to ${target.name}!");
|
||||
}
|
||||
}
|
||||
|
||||
void _applyDamage(
|
||||
Character target,
|
||||
int damage, {
|
||||
required DamageTarget targetType,
|
||||
DamageType type = DamageType.normal,
|
||||
}) {
|
||||
// Check Vulnerable
|
||||
if (target.hasStatus(StatusEffectType.vulnerable)) {
|
||||
damage = (damage * GameConfig.vulnerableDamageMultiplier).toInt();
|
||||
_addLog("Vulnerable! Damage increased to $damage.");
|
||||
type = DamageType.vulnerable;
|
||||
}
|
||||
|
||||
target.hp -= damage;
|
||||
if (target.hp < 0) target.hp = 0;
|
||||
|
||||
_damageEventController.sink.add(
|
||||
DamageEvent(damage: damage, target: targetType, type: type),
|
||||
);
|
||||
}
|
||||
|
||||
void _addLog(String message) {
|
||||
battleLogs.add(message);
|
||||
_logManager.addLog(message);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user