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
+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) {