update
This commit is contained in:
@@ -31,6 +31,7 @@ class EnemyIntent {
|
||||
final String description;
|
||||
final bool isSuccess;
|
||||
final int finalValue;
|
||||
bool isApplied; // Mutable flag to prevent double execution
|
||||
|
||||
EnemyIntent({
|
||||
required this.type,
|
||||
@@ -39,6 +40,7 @@ class EnemyIntent {
|
||||
required this.description,
|
||||
required this.isSuccess,
|
||||
required this.finalValue,
|
||||
this.isApplied = false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -51,6 +53,8 @@ class BattleProvider with ChangeNotifier {
|
||||
|
||||
final BattleLogManager _logManager = BattleLogManager();
|
||||
bool isPlayerTurn = true;
|
||||
int _turnTransactionId = 0; // To prevent async race conditions
|
||||
bool skipAnimations = false; // Sync with SettingsProvider
|
||||
|
||||
int stage = 1;
|
||||
int turnCount = 1;
|
||||
@@ -88,6 +92,7 @@ class BattleProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
void loadFromSave(Map<String, dynamic> data) {
|
||||
_turnTransactionId++; // Invalidate previous timers
|
||||
stage = data['stage'];
|
||||
turnCount = data['turnCount'];
|
||||
player = Character.fromJson(data['player']);
|
||||
@@ -100,6 +105,7 @@ class BattleProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
void initializeBattle() {
|
||||
_turnTransactionId++; // Invalidate previous timers
|
||||
stage = 1;
|
||||
turnCount = 1;
|
||||
// Load player from PlayerTable
|
||||
@@ -180,6 +186,7 @@ class BattleProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
void _prepareNextStage() {
|
||||
_turnTransactionId++; // Invalidate previous timers
|
||||
// Save Game at the start of each stage
|
||||
SaveManager.saveGame(this);
|
||||
|
||||
@@ -216,6 +223,7 @@ class BattleProvider with ChangeNotifier {
|
||||
showRewardPopup = false;
|
||||
|
||||
_generateEnemyIntent(); // Generate first intent
|
||||
_applyEnemyIntentEffects(); // Apply effects if it's a pre-emptive action (Defense)
|
||||
|
||||
_addLog("Stage $stage ($type) started! A wild ${enemy.name} appeared.");
|
||||
} else if (type == StageType.shop) {
|
||||
@@ -258,10 +266,69 @@ class BattleProvider with ChangeNotifier {
|
||||
if (!isPlayerTurn || player.isDead || enemy.isDead || showRewardPopup)
|
||||
return;
|
||||
|
||||
// Update Enemy Status Effects at the start of Player's turn (user request)
|
||||
enemy.updateStatusEffects();
|
||||
// 0. Apply Enemy Pre-emptive Defense (Just-in-Time)
|
||||
if (currentEnemyIntent?.type == EnemyActionType.defend &&
|
||||
!currentEnemyIntent!.isApplied) {
|
||||
final intent = currentEnemyIntent!;
|
||||
|
||||
// 1. Check for Defense Forbidden status
|
||||
if (intent.isSuccess) {
|
||||
enemy.armor += intent.finalValue;
|
||||
_addLog(
|
||||
"Enemy raises shield just in time! (+${intent.finalValue} Armor)",
|
||||
);
|
||||
|
||||
// Visual Effect for Enemy Defense Success
|
||||
final event = EffectEvent(
|
||||
id:
|
||||
DateTime.now().millisecondsSinceEpoch.toString() +
|
||||
Random().nextInt(1000).toString(),
|
||||
type: ActionType.defend,
|
||||
risk: intent.risk,
|
||||
target: EffectTarget.enemy,
|
||||
feedbackType: null,
|
||||
attacker: enemy,
|
||||
targetEntity: enemy,
|
||||
armorGained: intent.finalValue,
|
||||
isSuccess: true,
|
||||
);
|
||||
_effectEventController.sink.add(event);
|
||||
}
|
||||
if (!intent.isSuccess) {
|
||||
_addLog("Enemy tried to raise shield but fumbled!");
|
||||
print("[Logic Debug] Enemy Defense Fumbled Event"); // Debug Log
|
||||
|
||||
// Visual Effect for Enemy Defense Failure
|
||||
final event = EffectEvent(
|
||||
id:
|
||||
DateTime.now().millisecondsSinceEpoch.toString() +
|
||||
Random().nextInt(1000).toString(),
|
||||
type: ActionType.defend,
|
||||
risk: intent.risk,
|
||||
target: EffectTarget.enemy,
|
||||
feedbackType: BattleFeedbackType.failed,
|
||||
attacker: enemy,
|
||||
targetEntity: enemy,
|
||||
isSuccess: false,
|
||||
);
|
||||
_effectEventController.sink.add(event);
|
||||
}
|
||||
|
||||
intent.isApplied = true;
|
||||
|
||||
notifyListeners();
|
||||
|
||||
// Re-add delay to show the defense before player attack lands
|
||||
|
||||
int tid = _turnTransactionId;
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
if (tid != _turnTransactionId) return;
|
||||
}
|
||||
|
||||
// Update Enemy Status Effects at the start of Player's turn (user request)
|
||||
|
||||
enemy.updateStatusEffects(); // 1. Check for Defense Forbidden status
|
||||
if (type == ActionType.defend &&
|
||||
player.hasStatus(StatusEffectType.defenseForbidden)) {
|
||||
_addLog("Cannot defend! You are under Defense Forbidden status.");
|
||||
@@ -335,7 +402,7 @@ class BattleProvider with ChangeNotifier {
|
||||
isSuccess: true,
|
||||
);
|
||||
_effectEventController.sink.add(event);
|
||||
handleImpact(event); // Process impact via handleImpact for safety
|
||||
// handleImpact(event); // REMOVED: Driven by UI
|
||||
}
|
||||
} else {
|
||||
// Failure
|
||||
@@ -366,16 +433,19 @@ class BattleProvider with ChangeNotifier {
|
||||
event,
|
||||
); // Send event for miss/fail feedback
|
||||
_addLog("${player.name}'s ${type.name} ${feedbackType.name}!");
|
||||
handleImpact(event); // Process impact via handleImpact for safety
|
||||
print("[Logic Debug] Player Action Failed Event"); // Debug Log
|
||||
// handleImpact(event); // REMOVED: Driven by UI
|
||||
}
|
||||
|
||||
// Now check for enemy death (if applicable from bleed, or previous impacts)
|
||||
// Now check for enemy death (if applicable from bleed, or previous impacts)
|
||||
if (enemy.isDead) {
|
||||
// Check enemy death after player's action
|
||||
_onVictory();
|
||||
return;
|
||||
}
|
||||
|
||||
// Removed redundant `if (enemy.isDead)` check as it's handled in `_processAttackImpact`
|
||||
|
||||
_endPlayerTurn();
|
||||
|
||||
}
|
||||
// _endPlayerTurn(); // REMOVED: Driven by UI via handleImpact
|
||||
}
|
||||
|
||||
void _endPlayerTurn() {
|
||||
// Update durations at end of turn
|
||||
@@ -387,49 +457,76 @@ class BattleProvider with ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
|
||||
int tid = _turnTransactionId;
|
||||
Future.delayed(
|
||||
const Duration(milliseconds: GameConfig.animDelayEnemyTurn),
|
||||
() => _enemyTurn(),
|
||||
() {
|
||||
if (tid != _turnTransactionId) return;
|
||||
_startEnemyTurn();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _enemyTurn() async {
|
||||
// --- Turn Management Phases ---
|
||||
|
||||
// Phase 1: Enemy Action Phase
|
||||
Future<void> _startEnemyTurn() async {
|
||||
_turnTransactionId++; // Start of Enemy Turn Phase
|
||||
if (!isPlayerTurn && (player.isDead || enemy.isDead)) return;
|
||||
|
||||
_addLog("Enemy's turn...");
|
||||
await Future.delayed(
|
||||
const Duration(milliseconds: GameConfig.animDelayEnemyTurn),
|
||||
);
|
||||
// REMOVED: Initial delay for faster pacing
|
||||
// await Future.delayed(
|
||||
// const Duration(milliseconds: GameConfig.animDelayEnemyTurn),
|
||||
// );
|
||||
|
||||
// Enemy Turn Start Logic
|
||||
// Armor decay
|
||||
if (enemy.armor > 0) {
|
||||
enemy.armor = (enemy.armor * GameConfig.armorDecayRate).toInt();
|
||||
_addLog("Enemy's armor decayed to ${enemy.armor}.");
|
||||
}
|
||||
|
||||
// 1. Process Start-of-Turn Effects for Enemy
|
||||
// Process Start-of-Turn Effects
|
||||
bool canAct = _processStartTurnEffects(enemy);
|
||||
|
||||
// Check death from bleed before acting
|
||||
if (enemy.isDead) {
|
||||
_onVictory();
|
||||
return;
|
||||
}
|
||||
|
||||
if (canAct && currentEnemyIntent != null) {
|
||||
if (canAct && currentEnemyIntent != null) {
|
||||
final intent = currentEnemyIntent!;
|
||||
|
||||
final intent = currentEnemyIntent!;
|
||||
if (intent.type == EnemyActionType.defend) {
|
||||
// Defensive Action (Non-animating)
|
||||
|
||||
|
||||
// Check if already applied in Phase 3 of previous turn
|
||||
if (intent.isApplied) {
|
||||
_addLog("Enemy maintains defensive stance.");
|
||||
// Proceed manually
|
||||
int tid = _turnTransactionId;
|
||||
int delay = skipAnimations ? 500 : 1000; // Faster if animations off
|
||||
Future.delayed(Duration(milliseconds: delay), () {
|
||||
if (tid != _turnTransactionId) return;
|
||||
_endEnemyTurn();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (intent.type == EnemyActionType.defend) {
|
||||
|
||||
// Already handled in _generateEnemyIntent
|
||||
|
||||
_addLog("Enemy maintains defensive stance.");
|
||||
|
||||
} else { // Attack Logic
|
||||
if (intent.isSuccess) {
|
||||
// ... (success logic) ...
|
||||
} else {
|
||||
// ... (failure logic) ...
|
||||
}
|
||||
// For defense (if not applied), we proceed manually
|
||||
int tid = _turnTransactionId;
|
||||
int delay = skipAnimations ? 500 : 1500; // Faster if animations off
|
||||
Future.delayed(Duration(milliseconds: delay), () {
|
||||
if (tid != _turnTransactionId) return;
|
||||
_endEnemyTurn();
|
||||
});
|
||||
} else {
|
||||
// Attack Action (Animating)
|
||||
if (intent.isSuccess) {
|
||||
final event = EffectEvent(
|
||||
id:
|
||||
@@ -445,8 +542,12 @@ class BattleProvider with ChangeNotifier {
|
||||
isSuccess: true,
|
||||
);
|
||||
_effectEventController.sink.add(event);
|
||||
// No Future.delayed here, BattleScreen will trigger impact
|
||||
// CRITICAL: We DO NOT call _endEnemyTurn here.
|
||||
// The UI will play the animation, then call handleImpact.
|
||||
// handleImpact will trigger _endEnemyTurn.
|
||||
return; // Exit _startEnemyTurn after emitting event for UI to handle
|
||||
} else {
|
||||
// Missed Attack
|
||||
_addLog("Enemy's ${intent.risk.name} attack missed!");
|
||||
final event = EffectEvent(
|
||||
id:
|
||||
@@ -460,43 +561,53 @@ class BattleProvider with ChangeNotifier {
|
||||
targetEntity: player,
|
||||
isSuccess: false,
|
||||
);
|
||||
_effectEventController.sink.add(
|
||||
event,
|
||||
); // Send event for miss feedback
|
||||
handleImpact(event); // Process impact via handleImpact for safety
|
||||
_effectEventController.sink.add(event);
|
||||
return; // Exit _startEnemyTurn after emitting event for UI to handle
|
||||
}
|
||||
}
|
||||
} 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.");
|
||||
int tid = _turnTransactionId;
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
if (tid != _turnTransactionId) return;
|
||||
_endEnemyTurn();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for potential animations to finish before generating next intent
|
||||
// If attacking, we need to wait for the attack animation + return
|
||||
if (currentEnemyIntent?.type == EnemyActionType.attack &&
|
||||
currentEnemyIntent?.isSuccess == true) {
|
||||
int animDelay = GameConfig.animDelayNormal;
|
||||
if (currentEnemyIntent!.risk == RiskLevel.safe)
|
||||
animDelay = GameConfig.animDelaySafe;
|
||||
if (currentEnemyIntent!.risk == RiskLevel.risky)
|
||||
animDelay = GameConfig.animDelayRisky;
|
||||
// Phase 2: End Enemy Turn & Generate Next Intent
|
||||
void _endEnemyTurn() {
|
||||
if (player.isDead) return; // Game Over check
|
||||
|
||||
// Wait for impact (handled by UI) + Return time + small buffer
|
||||
// Since we removed the pre-impact delay, the UI animation starts immediately.
|
||||
// We want to generate intent AFTER the full animation cycle.
|
||||
// Full cycle ~= 2 * animDelay (Forward + Reverse)
|
||||
await Future.delayed(Duration(milliseconds: animDelay));
|
||||
} else {
|
||||
// For non-animating actions, a small pause is nice for pacing
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
}
|
||||
// Generate NEXT intent
|
||||
_generateEnemyIntent();
|
||||
|
||||
// Generate next intent
|
||||
if (!player.isDead) {
|
||||
_generateEnemyIntent();
|
||||
}
|
||||
_processMiddleTurn();
|
||||
}
|
||||
|
||||
// Phase 3: Middle Turn (Apply Defense Effects)
|
||||
Future<void> _processMiddleTurn() async {
|
||||
// Apply Intent Effects (Pre-emptive Defense)
|
||||
int tid = _turnTransactionId;
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
if (tid != _turnTransactionId) return;
|
||||
_applyEnemyIntentEffects();
|
||||
|
||||
// REMOVED: Delay for faster pacing
|
||||
// Small pause to let the player see the enemy's new stance
|
||||
|
||||
_startPlayerTurn();
|
||||
}
|
||||
|
||||
// Phase 4: Start Player Turn
|
||||
void _startPlayerTurn() {
|
||||
// Player Turn Start Logic
|
||||
// Armor decay
|
||||
if (player.armor > 0) {
|
||||
@@ -505,7 +616,7 @@ class BattleProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
if (player.isDead) {
|
||||
await _onDefeat();
|
||||
_onDefeat();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -628,7 +739,6 @@ class BattleProvider with ChangeNotifier {
|
||||
|
||||
stage++;
|
||||
showRewardPopup = false;
|
||||
rewardOptions.clear(); // Clear options to prevent flash on next victory
|
||||
|
||||
_prepareNextStage();
|
||||
|
||||
@@ -753,7 +863,7 @@ class BattleProvider with ChangeNotifier {
|
||||
// Defend Intent
|
||||
int baseDef = enemy.totalDefense;
|
||||
// Variance removed
|
||||
int armor = (baseDef * 2 * efficiency).toInt();
|
||||
int armor = (baseDef * efficiency).toInt();
|
||||
|
||||
// Calculate success immediately
|
||||
bool success = false;
|
||||
@@ -778,39 +888,104 @@ class BattleProvider with ChangeNotifier {
|
||||
finalValue: armor,
|
||||
);
|
||||
|
||||
// Apply defense immediately if successful
|
||||
if (success) {
|
||||
enemy.armor += armor;
|
||||
_addLog("Enemy prepares defense! (+$armor Armor)");
|
||||
_effectEventController.sink.add(
|
||||
EffectEvent(
|
||||
id:
|
||||
DateTime.now().millisecondsSinceEpoch.toString() +
|
||||
Random().nextInt(1000).toString(),
|
||||
type: ActionType.defend,
|
||||
risk: risk,
|
||||
target: EffectTarget.enemy,
|
||||
feedbackType: null, // 방어 성공이므로 feedbackType 없음
|
||||
),
|
||||
);
|
||||
} else {
|
||||
_addLog("Enemy tried to defend but fumbled!");
|
||||
}
|
||||
// Note: Armor is NO LONGER applied here instantly.
|
||||
// It is applied in _applyEnemyIntentEffects() which is called before Player turn.
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Applies the effects of the enemy's intent (specifically Defense)
|
||||
/// This should be called just before the Player's turn starts.
|
||||
void _applyEnemyIntentEffects() {
|
||||
if (currentEnemyIntent == null || enemy.isDead) return;
|
||||
|
||||
// Prevent duplicate application
|
||||
if (currentEnemyIntent!.isApplied) return;
|
||||
|
||||
if (currentEnemyIntent!.type == EnemyActionType.defend) {
|
||||
// Logic moved to playerAction for "Just-in-Time" defense.
|
||||
// Nothing to do here except maybe log intent (optional, but Intent UI covers it).
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// New public method to be called by UI at impact moment
|
||||
void handleImpact(EffectEvent event) {
|
||||
if (event.isSuccess == false || event.feedbackType != null) {
|
||||
// If it's a miss/fail/feedback, just log and return
|
||||
// Logging and feedback text should already be handled when event created
|
||||
notifyListeners(); // Ensure UI updates for log
|
||||
|
||||
// Even on failure, proceed to end turn logic
|
||||
if (event.attacker == player) {
|
||||
_endPlayerTurn();
|
||||
} else if (event.attacker == enemy) {
|
||||
// Special Case: Do NOT call _endEnemyTurn for Enemy Defense (Phase 1 & 3).
|
||||
// Phase 1 relies on manual timer. Phase 3 relies on _processMiddleTurn sequence.
|
||||
if (event.type != ActionType.defend) {
|
||||
_endEnemyTurn();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Special Case: Enemy Defense (Phase 3 & Phase 1)
|
||||
// - Phase 3 Defense: Logic applied in _applyEnemyIntentEffects. Event is Visual Only.
|
||||
// - Phase 1 Defense: Logic applied in _startEnemyTurn (if we add it there) or here?
|
||||
// Wait, Phase 1 Defense is distinct.
|
||||
// However, currently Phase 1 Defense also uses _effectEventController.sink.add(event).
|
||||
// BUT Phase 1 Defense Logic is NOT applied in _startEnemyTurn yet (it just emits event).
|
||||
// So Phase 1 Defense SHOULD go through _processAttackImpact?
|
||||
// NO, because Phase 1 Defense uses the same ActionType.defend.
|
||||
|
||||
// Let's look at _startEnemyTurn for Phase 1 Defense:
|
||||
// It emits event with armorGained. It does NOT increase armor directly.
|
||||
// So for Phase 1, we NEED handleImpact -> _processAttackImpact.
|
||||
|
||||
// Let's look at _applyEnemyIntentEffects for Phase 3 Defense:
|
||||
// It increases armor DIRECTLY: "enemy.armor += intent.finalValue;"
|
||||
// AND it emits event.
|
||||
|
||||
// This discrepancy is the root cause.
|
||||
// We should standardize.
|
||||
|
||||
// DECISION: Phase 3 Defense event should be flagged or handled as visual-only.
|
||||
// Since we can't easily add flags to EffectEvent without changing other files,
|
||||
// let's rely on the context.
|
||||
|
||||
// Actually, simply removing the direct armor application in _applyEnemyIntentEffects
|
||||
// and letting handleImpact do it is cleaner?
|
||||
// NO, because Phase 3 needs armor applied BEFORE Player Turn starts, independent of UI speed.
|
||||
// And _processMiddleTurn relies on the logic sequence.
|
||||
|
||||
// So, we MUST block handleImpact for Phase 3 Defense.
|
||||
// Phase 1 Defense (Rare, usually Attack) needs to work too.
|
||||
|
||||
// BUT wait, _startEnemyTurn (Phase 1) code:
|
||||
// if (intent.type == EnemyActionType.defend) { ... sink.add(event); ... }
|
||||
// It does NOT apply armor. So Phase 1 relies on handleImpact.
|
||||
|
||||
// PROBLEM: handleImpact cannot distinguish Phase 1 vs Phase 3 event easily.
|
||||
|
||||
// FIX: Update _startEnemyTurn (Phase 1) to ALSO apply armor directly and make the event visual-only.
|
||||
// Then we can globally block Enemy Defend in handleImpact.
|
||||
|
||||
// Step 1: Modifying handleImpact to block ALL Enemy Defend logic.
|
||||
if (event.attacker == enemy && event.type == ActionType.defend) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only process actual attack or defend impacts here
|
||||
_processAttackImpact(event);
|
||||
|
||||
// After processing impact, proceed to end turn logic
|
||||
if (event.triggersTurnChange) {
|
||||
if (event.attacker == player) {
|
||||
_endPlayerTurn();
|
||||
} else if (event.attacker == enemy) {
|
||||
_endEnemyTurn();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Refactored common attack impact logic
|
||||
|
||||
Reference in New Issue
Block a user