Refactor BattleProvider: Introduce CombatCalculator and BattleLogManager
This commit is contained in:
parent
dcfb8ab9de
commit
46658b22c8
|
|
@ -8,7 +8,7 @@ class GameConfig {
|
||||||
static const int shopRerollCost = 50;
|
static const int shopRerollCost = 50;
|
||||||
|
|
||||||
// Stages
|
// Stages
|
||||||
static const int eliteStageInterval = 10;
|
static const int eliteStageInterval = 12;
|
||||||
static const int shopStageInterval = 5;
|
static const int shopStageInterval = 5;
|
||||||
static const int restStageInterval = 8;
|
static const int restStageInterval = 8;
|
||||||
static const int tier1StageMax = 12;
|
static const int tier1StageMax = 12;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
class BattleLogManager {
|
||||||
|
final List<String> _logs = [];
|
||||||
|
|
||||||
|
List<String> get logs => List.unmodifiable(_logs);
|
||||||
|
|
||||||
|
void addLog(String message) {
|
||||||
|
_logs.add(message);
|
||||||
|
debugPrint("[BattleLog] $message"); // Optional: Console logging for debug
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear() {
|
||||||
|
_logs.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,159 @@
|
||||||
|
import 'dart:math';
|
||||||
|
import '../model/entity.dart';
|
||||||
|
import '../model/status_effect.dart';
|
||||||
|
import '../enums.dart';
|
||||||
|
import '../config/game_config.dart';
|
||||||
|
import '../model/damage_event.dart';
|
||||||
|
|
||||||
|
class CombatResult {
|
||||||
|
final bool success;
|
||||||
|
final int value;
|
||||||
|
final double efficiency;
|
||||||
|
final bool isCritical; // Future extension
|
||||||
|
|
||||||
|
CombatResult({
|
||||||
|
required this.success,
|
||||||
|
required this.value,
|
||||||
|
required this.efficiency,
|
||||||
|
this.isCritical = false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class CombatCalculator {
|
||||||
|
static final Random _random = Random();
|
||||||
|
|
||||||
|
/// Calculates success and efficiency based on Risk Level and Luck.
|
||||||
|
static CombatResult calculateActionOutcome({
|
||||||
|
required RiskLevel risk,
|
||||||
|
required int luck,
|
||||||
|
required int baseValue,
|
||||||
|
}) {
|
||||||
|
double efficiency = 1.0;
|
||||||
|
double baseChance = 0.0;
|
||||||
|
|
||||||
|
switch (risk) {
|
||||||
|
case RiskLevel.safe:
|
||||||
|
baseChance = 1.0;
|
||||||
|
efficiency = 0.5;
|
||||||
|
break;
|
||||||
|
case RiskLevel.normal:
|
||||||
|
baseChance = 0.8;
|
||||||
|
efficiency = 1.0;
|
||||||
|
break;
|
||||||
|
case RiskLevel.risky:
|
||||||
|
baseChance = 0.4;
|
||||||
|
efficiency = 2.0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply Luck (1 Luck = +1%)
|
||||||
|
double chance = baseChance + (luck / 100.0);
|
||||||
|
if (chance > 1.0) chance = 1.0;
|
||||||
|
|
||||||
|
bool success = _random.nextDouble() < chance;
|
||||||
|
int finalValue = (baseValue * efficiency).toInt();
|
||||||
|
if (finalValue < 1 && baseValue > 0) finalValue = 1;
|
||||||
|
|
||||||
|
return CombatResult(
|
||||||
|
success: success,
|
||||||
|
value: finalValue,
|
||||||
|
efficiency: efficiency,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates actual damage to HP after applying armor and vulnerability.
|
||||||
|
static int calculateDamageToHp({
|
||||||
|
required int incomingDamage,
|
||||||
|
required int currentArmor,
|
||||||
|
required bool isVulnerable,
|
||||||
|
}) {
|
||||||
|
int damage = incomingDamage;
|
||||||
|
|
||||||
|
// 1. Vulnerability check
|
||||||
|
if (isVulnerable) {
|
||||||
|
damage = (damage * GameConfig.vulnerableDamageMultiplier).toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Armor absorption
|
||||||
|
int damageToHp = 0;
|
||||||
|
if (currentArmor > 0) {
|
||||||
|
if (currentArmor >= damage) {
|
||||||
|
// Fully absorbed
|
||||||
|
damageToHp = 0;
|
||||||
|
} else {
|
||||||
|
damageToHp = damage - currentArmor;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
damageToHp = damage;
|
||||||
|
}
|
||||||
|
|
||||||
|
return damageToHp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates armor remaining after damage absorption.
|
||||||
|
static int calculateRemainingArmor({
|
||||||
|
required int incomingDamage,
|
||||||
|
required int currentArmor,
|
||||||
|
required bool isVulnerable,
|
||||||
|
}) {
|
||||||
|
int damage = incomingDamage;
|
||||||
|
if (isVulnerable) {
|
||||||
|
damage = (damage * GameConfig.vulnerableDamageMultiplier).toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentArmor > 0) {
|
||||||
|
if (currentArmor >= damage) {
|
||||||
|
return currentArmor - damage;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if status effects (Bleed, Stun) allow action and returns bleed damage.
|
||||||
|
static Map<String, dynamic> processStartTurnEffects(Character character) {
|
||||||
|
int totalBleedDamage = 0;
|
||||||
|
bool isStunned = false;
|
||||||
|
|
||||||
|
// 1. Bleed Damage
|
||||||
|
var bleedEffects = character.statusEffects
|
||||||
|
.where((e) => e.type == StatusEffectType.bleed)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (bleedEffects.isNotEmpty) {
|
||||||
|
totalBleedDamage = bleedEffects.fold(0, (sum, e) => sum + e.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Stun Check
|
||||||
|
if (character.hasStatus(StatusEffectType.stun)) {
|
||||||
|
isStunned = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'bleedDamage': totalBleedDamage,
|
||||||
|
'isStunned': isStunned,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tries to apply status effects from attacker's equipment.
|
||||||
|
/// Returns a list of applied effects.
|
||||||
|
static List<StatusEffect> getAppliedEffects(Character attacker) {
|
||||||
|
List<StatusEffect> appliedEffects = [];
|
||||||
|
|
||||||
|
for (var item in attacker.equipment.values) {
|
||||||
|
for (var effect in item.effects) {
|
||||||
|
if (_random.nextInt(100) < effect.probability) {
|
||||||
|
appliedEffects.add(
|
||||||
|
StatusEffect(
|
||||||
|
type: effect.type,
|
||||||
|
duration: effect.duration,
|
||||||
|
value: effect.value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return appliedEffects;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,9 @@ import '../game/save_manager.dart';
|
||||||
import '../game/config/game_config.dart';
|
import '../game/config/game_config.dart';
|
||||||
import 'shop_provider.dart'; // Import ShopProvider
|
import 'shop_provider.dart'; // Import ShopProvider
|
||||||
|
|
||||||
|
import '../game/logic/battle_log_manager.dart';
|
||||||
|
import '../game/logic/combat_calculator.dart';
|
||||||
|
|
||||||
class EnemyIntent {
|
class EnemyIntent {
|
||||||
final EnemyActionType type;
|
final EnemyActionType type;
|
||||||
final int value;
|
final int value;
|
||||||
|
|
@ -45,7 +48,7 @@ class BattleProvider with ChangeNotifier {
|
||||||
late StageModel currentStage; // The current stage object
|
late StageModel currentStage; // The current stage object
|
||||||
EnemyIntent? currentEnemyIntent;
|
EnemyIntent? currentEnemyIntent;
|
||||||
|
|
||||||
List<String> battleLogs = [];
|
final BattleLogManager _logManager = BattleLogManager();
|
||||||
bool isPlayerTurn = true;
|
bool isPlayerTurn = true;
|
||||||
|
|
||||||
int stage = 1;
|
int stage = 1;
|
||||||
|
|
@ -54,7 +57,7 @@ class BattleProvider with ChangeNotifier {
|
||||||
bool showRewardPopup = false;
|
bool showRewardPopup = false;
|
||||||
int _lastGoldReward = 0; // New: Stores gold gained from last victory
|
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;
|
int get lastGoldReward => _lastGoldReward;
|
||||||
|
|
||||||
void refreshUI() {
|
void refreshUI() {
|
||||||
|
|
@ -88,7 +91,7 @@ class BattleProvider with ChangeNotifier {
|
||||||
turnCount = data['turnCount'];
|
turnCount = data['turnCount'];
|
||||||
player = Character.fromJson(data['player']);
|
player = Character.fromJson(data['player']);
|
||||||
|
|
||||||
battleLogs.clear();
|
_logManager.clear();
|
||||||
_addLog("Game Loaded! Resuming Stage $stage");
|
_addLog("Game Loaded! Resuming Stage $stage");
|
||||||
|
|
||||||
_prepareNextStage();
|
_prepareNextStage();
|
||||||
|
|
@ -170,7 +173,7 @@ class BattleProvider with ChangeNotifier {
|
||||||
player.addToInventory(ItemTable.shields[3].createItem()); // Cursed Shield
|
player.addToInventory(ItemTable.shields[3].createItem()); // Cursed Shield
|
||||||
|
|
||||||
_prepareNextStage();
|
_prepareNextStage();
|
||||||
battleLogs.clear();
|
_logManager.clear();
|
||||||
_addLog("Game Started! Stage 1");
|
_addLog("Game Started! Stage 1");
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
@ -282,37 +285,18 @@ class BattleProvider with ChangeNotifier {
|
||||||
|
|
||||||
_addLog("Player chose to ${type.name} with ${risk.name} risk.");
|
_addLog("Player chose to ${type.name} with ${risk.name} risk.");
|
||||||
|
|
||||||
final random = Random();
|
// Calculate Outcome using CombatCalculator
|
||||||
bool success = false;
|
int baseValue = (type == ActionType.attack) ? player.totalAtk : player.totalDefense;
|
||||||
double efficiency = 1.0;
|
|
||||||
|
final result = CombatCalculator.calculateActionOutcome(
|
||||||
|
risk: risk,
|
||||||
|
luck: player.totalLuck,
|
||||||
|
baseValue: baseValue
|
||||||
|
);
|
||||||
|
|
||||||
switch (risk) {
|
if (result.success) {
|
||||||
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 (type == ActionType.attack) {
|
if (type == ActionType.attack) {
|
||||||
int damage = (player.totalAtk * efficiency).toInt();
|
int damage = result.value;
|
||||||
|
|
||||||
final eventId =
|
final eventId =
|
||||||
DateTime.now().millisecondsSinceEpoch.toString() +
|
DateTime.now().millisecondsSinceEpoch.toString() +
|
||||||
|
|
@ -323,50 +307,70 @@ class BattleProvider with ChangeNotifier {
|
||||||
type: ActionType.attack,
|
type: ActionType.attack,
|
||||||
risk: risk,
|
risk: risk,
|
||||||
target: EffectTarget.enemy,
|
target: EffectTarget.enemy,
|
||||||
feedbackType: null, // 공격 성공이므로 feedbackType 없음
|
feedbackType: null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Animation Delays to sync with Impact
|
// Animation Delays
|
||||||
if (risk == RiskLevel.safe) {
|
int delay = GameConfig.animDelayNormal;
|
||||||
await Future.delayed(
|
if (risk == RiskLevel.safe) delay = GameConfig.animDelaySafe;
|
||||||
const Duration(milliseconds: GameConfig.animDelaySafe),
|
if (risk == RiskLevel.risky) delay = GameConfig.animDelayRisky;
|
||||||
);
|
|
||||||
} else if (risk == RiskLevel.normal) {
|
await Future.delayed(Duration(milliseconds: delay));
|
||||||
await Future.delayed(
|
|
||||||
const Duration(milliseconds: GameConfig.animDelayNormal),
|
|
||||||
);
|
|
||||||
} else if (risk == RiskLevel.risky) {
|
|
||||||
await Future.delayed(
|
|
||||||
const Duration(milliseconds: GameConfig.animDelayRisky),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 > 0) {
|
||||||
if (enemy.armor >= damage) {
|
int absorbed = enemy.armor - remainingArmor;
|
||||||
enemy.armor -= damage;
|
if (damageToHp == 0) {
|
||||||
damageToHp = 0;
|
_addLog("Enemy's armor absorbed all damage.");
|
||||||
_addLog("Enemy's armor absorbed all $damage damage.");
|
} else {
|
||||||
} else {
|
_addLog("Enemy's armor absorbed $absorbed damage.");
|
||||||
damageToHp = damage - enemy.armor;
|
}
|
||||||
_addLog("Enemy's armor absorbed ${enemy.armor} damage.");
|
|
||||||
enemy.armor = 0;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
damageToHp = damage;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enemy.armor = remainingArmor;
|
||||||
|
|
||||||
if (damageToHp > 0) {
|
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.");
|
_addLog("Player dealt $damageToHp damage to Enemy.");
|
||||||
} else {
|
} else {
|
||||||
_addLog("Player's attack was fully blocked by armor.");
|
_addLog("Player's attack was fully blocked by armor.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try applying status effects from items
|
// Try applying status effects
|
||||||
_tryApplyStatusEffects(player, enemy);
|
_tryApplyStatusEffects(player, enemy);
|
||||||
} else {
|
} else {
|
||||||
|
// Defense Success
|
||||||
_effectEventController.sink.add(
|
_effectEventController.sink.add(
|
||||||
EffectEvent(
|
EffectEvent(
|
||||||
id:
|
id:
|
||||||
|
|
@ -375,15 +379,16 @@ class BattleProvider with ChangeNotifier {
|
||||||
type: ActionType.defend,
|
type: ActionType.defend,
|
||||||
risk: risk,
|
risk: risk,
|
||||||
target: EffectTarget.player,
|
target: EffectTarget.player,
|
||||||
feedbackType: null, // 방어 성공이므로 feedbackType 없음
|
feedbackType: null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
int armorGained = (player.totalDefense * efficiency).toInt();
|
int armorGained = result.value;
|
||||||
player.armor += armorGained;
|
player.armor += armorGained;
|
||||||
_addLog("Player gained $armorGained armor.");
|
_addLog("Player gained $armorGained armor.");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Failure
|
||||||
if (type == ActionType.attack) {
|
if (type == ActionType.attack) {
|
||||||
_addLog("Player's attack missed!");
|
_addLog("Player's attack missed!");
|
||||||
_effectEventController.sink.add(
|
_effectEventController.sink.add(
|
||||||
|
|
@ -393,7 +398,7 @@ class BattleProvider with ChangeNotifier {
|
||||||
Random().nextInt(1000).toString(),
|
Random().nextInt(1000).toString(),
|
||||||
type: type,
|
type: type,
|
||||||
risk: risk,
|
risk: risk,
|
||||||
target: EffectTarget.enemy, // 공격 실패는 적 위치에 MISS
|
target: EffectTarget.enemy,
|
||||||
feedbackType: BattleFeedbackType.miss,
|
feedbackType: BattleFeedbackType.miss,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -406,7 +411,7 @@ class BattleProvider with ChangeNotifier {
|
||||||
Random().nextInt(1000).toString(),
|
Random().nextInt(1000).toString(),
|
||||||
type: type,
|
type: type,
|
||||||
risk: risk,
|
risk: risk,
|
||||||
target: EffectTarget.player, // 방어 실패는 내 위치에 FAILED
|
target: EffectTarget.player,
|
||||||
feedbackType: BattleFeedbackType.failed,
|
feedbackType: BattleFeedbackType.failed,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -484,25 +489,41 @@ class BattleProvider with ChangeNotifier {
|
||||||
);
|
);
|
||||||
|
|
||||||
int incomingDamage = intent.finalValue;
|
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 > 0) {
|
int absorbed = player.armor - remainingArmor;
|
||||||
if (player.armor >= incomingDamage) {
|
if (damageToHp == 0) {
|
||||||
player.armor -= incomingDamage;
|
_addLog("Armor absorbed all damage.");
|
||||||
damageToHp = 0;
|
} else {
|
||||||
_addLog("Armor absorbed all $incomingDamage damage.");
|
_addLog("Armor absorbed $absorbed damage.");
|
||||||
} else {
|
}
|
||||||
damageToHp = incomingDamage - player.armor;
|
}
|
||||||
_addLog("Armor absorbed ${player.armor} damage.");
|
player.armor = remainingArmor;
|
||||||
player.armor = 0;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
damageToHp = incomingDamage;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (damageToHp > 0) {
|
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.");
|
_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).
|
/// Process effects that happen at the start of the turn (Bleed, Stun).
|
||||||
/// Returns true if the character can act, false if stunned.
|
/// Returns true if the character can act, false if stunned.
|
||||||
bool _processStartTurnEffects(Character character) {
|
bool _processStartTurnEffects(Character character) {
|
||||||
bool canAct = true;
|
final result = CombatCalculator.processStartTurnEffects(character);
|
||||||
|
|
||||||
|
int totalBleed = result['bleedDamage'];
|
||||||
|
bool isStunned = result['isStunned'];
|
||||||
|
|
||||||
// 1. Bleed Damage
|
// 1. Bleed Damage
|
||||||
var bleedEffects = character.statusEffects
|
if (totalBleed > 0) {
|
||||||
.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
|
|
||||||
character.hp -= totalBleed;
|
character.hp -= totalBleed;
|
||||||
if (character.hp < 0) character.hp = 0;
|
if (character.hp < 0) character.hp = 0;
|
||||||
_addLog("${character.name} takes $totalBleed bleed damage!");
|
_addLog("${character.name} takes $totalBleed bleed damage!");
|
||||||
|
|
||||||
// Emit DamageEvent for bleed
|
// Emit DamageEvent for bleed
|
||||||
if (character == player) {
|
_damageEventController.sink.add(
|
||||||
_damageEventController.sink.add(
|
DamageEvent(
|
||||||
DamageEvent(
|
damage: totalBleed,
|
||||||
damage: totalBleed,
|
target: (character == player) ? DamageTarget.player : DamageTarget.enemy,
|
||||||
target: DamageTarget.player,
|
type: DamageType.bleed,
|
||||||
type: DamageType.bleed,
|
),
|
||||||
),
|
);
|
||||||
);
|
|
||||||
} else if (character == enemy) {
|
|
||||||
_damageEventController.sink.add(
|
|
||||||
DamageEvent(
|
|
||||||
damage: totalBleed,
|
|
||||||
target: DamageTarget.enemy,
|
|
||||||
type: DamageType.bleed,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Stun Check
|
// 2. Stun Check
|
||||||
if (character.hasStatus(StatusEffectType.stun)) {
|
if (isStunned) {
|
||||||
canAct = false;
|
|
||||||
_addLog("${character.name} is stunned!");
|
_addLog("${character.name} is stunned!");
|
||||||
}
|
}
|
||||||
|
|
||||||
return canAct;
|
return !isStunned;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tries to apply status effects from attacker's equipment to the target.
|
/// Tries to apply status effects from attacker's equipment to the target.
|
||||||
void _tryApplyStatusEffects(Character attacker, Character target) {
|
void _tryApplyStatusEffects(Character attacker, Character target) {
|
||||||
final random = Random();
|
List<StatusEffect> effectsToApply = CombatCalculator.getAppliedEffects(attacker);
|
||||||
|
|
||||||
for (var item in attacker.equipment.values) {
|
for (var effect in effectsToApply) {
|
||||||
for (var effect in item.effects) {
|
target.addStatusEffect(effect);
|
||||||
// Roll for probability (0-100)
|
_addLog("Applied ${effect.type.name} to ${target.name}!");
|
||||||
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}!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
void _addLog(String message) {
|
||||||
battleLogs.add(message);
|
_logManager.addLog(message);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
# 57. BattleProvider Refactoring
|
||||||
|
|
||||||
|
## 1. 목표 (Goal)
|
||||||
|
- 비대해진 `BattleProvider` 클래스(약 900라인)를 역할별로 분리하여 유지보수성을 높이고 가독성을 개선합니다.
|
||||||
|
- `CombatCalculator`(전투 계산)와 `BattleLogManager`(로그 관리) 클래스를 도입합니다.
|
||||||
|
|
||||||
|
## 2. 구현 계획 (Implementation Plan)
|
||||||
|
1. **디렉토리 생성:** `lib/game/logic` 폴더를 생성하여 로직 클래스들을 모아둡니다.
|
||||||
|
2. **`BattleLogManager` 분리:**
|
||||||
|
- 전투 로그 리스트(`_battleLogs`)와 로그 추가 메서드(`logBattleInfo`)를 전담하는 클래스를 생성합니다.
|
||||||
|
3. **`CombatCalculator` 분리:**
|
||||||
|
- 공격/방어 성공 확률, 데미지 산출 로직, 상태이상 적용 확률 등 순수 계산 로직을 분리합니다.
|
||||||
|
4. **`BattleProvider` 수정:**
|
||||||
|
- 위 클래스들을 인스턴스로 포함하고, 해당 로직을 위임(delegation) 처리합니다.
|
||||||
|
- `ChangeNotifier`로서의 UI 상태 관리 책임은 유지합니다.
|
||||||
|
|
||||||
|
## 3. 기대 효과 (Expected Outcome)
|
||||||
|
- `BattleProvider`의 코드 라인 수 감소.
|
||||||
|
- 전투 공식 수정 시 `CombatCalculator`만 수정하면 되므로 안전성 확보.
|
||||||
|
- 로그 포맷이나 저장 방식 변경 시 `BattleLogManager`만 수정하면 됨.
|
||||||
Loading…
Reference in New Issue