import 'dart:async'; // StreamController 사용을 위해 import import 'dart:math'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; // For context.read in _prepareNextStage import '../game/model/entity.dart'; import '../game/model/item.dart'; import '../game/model/status_effect.dart'; import '../game/model/stage.dart'; import '../game/data/item_table.dart'; import '../game/data/enemy_table.dart'; import '../game/data/player_table.dart'; import '../utils/game_math.dart'; import '../game/enums.dart'; import '../game/model/damage_event.dart'; // DamageEvent import import '../game/model/effect_event.dart'; // EffectEvent import import '../game/save_manager.dart'; import '../game/config/game_config.dart'; import '../game/config/battle_config.dart'; // Import BattleConfig 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; final RiskLevel risk; final String description; final bool isSuccess; final int finalValue; bool isApplied; // Mutable flag to prevent double execution EnemyIntent({ required this.type, required this.value, required this.risk, required this.description, required this.isSuccess, required this.finalValue, this.isApplied = false, }); } class BattleProvider with ChangeNotifier { late Character player; late Character enemy; // Kept for compatibility, active during Battle/Elite late StageModel currentStage; // The current stage object EnemyIntent? currentEnemyIntent; 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; List rewardOptions = []; bool showRewardPopup = false; int _lastGoldReward = 0; // New: Stores gold gained from last victory List get logs => _logManager.logs; int get lastGoldReward => _lastGoldReward; void refreshUI() { notifyListeners(); } // Damage Event Stream final _damageEventController = StreamController.broadcast(); Stream get damageStream => _damageEventController.stream; // Effect Event Stream final _effectEventController = StreamController.broadcast(); Stream get effectStream => _effectEventController.stream; // Dependency injection final ShopProvider shopProvider; BattleProvider({required this.shopProvider}) { // initializeBattle(); // Do not auto-start logic } @override void dispose() { _damageEventController.close(); // StreamController 닫기 _effectEventController.close(); super.dispose(); } void loadFromSave(Map data) { _turnTransactionId++; // Invalidate previous timers stage = data['stage']; turnCount = data['turnCount']; player = Character.fromJson(data['player']); _logManager.clear(); _addLog("Game Loaded! Resuming Stage $stage"); _prepareNextStage(); notifyListeners(); } void initializeBattle() { _turnTransactionId++; // Invalidate previous timers stage = 1; turnCount = 1; // Load player from PlayerTable final playerTemplate = PlayerTable.get("warrior"); if (playerTemplate != null) { player = playerTemplate.createCharacter(); } else { // Fallback if data is missing player = Character( name: "Player", maxHp: 50, armor: 0, atk: 5, baseDefense: 5, ); } // Give test gold player.gold = GameConfig.startingGold; // Provide starter equipment // final starterSword = Item( // id: "starter_sword", // name: "Wooden Sword", // description: "A basic sword", // atkBonus: 5, // hpBonus: 0, // slot: EquipmentSlot.weapon, // ); // final starterArmor = Item( // id: "starter_armor", // name: "Leather Armor", // description: "Basic protection", // atkBonus: 0, // hpBonus: 20, // slot: EquipmentSlot.armor, // ); // final starterShield = Item( // id: "starter_shield", // name: "Wooden Shield", // description: "A small shield", // atkBonus: 0, // hpBonus: 0, // armorBonus: 3, // slot: EquipmentSlot.shield, // ); // final starterRing = Item( // id: "starter_ring", // name: "Copper Ring", // description: "A simple ring", // atkBonus: 1, // hpBonus: 5, // slot: EquipmentSlot.accessory, // ); // player.addToInventory(starterSword); // player.equip(starterSword); // player.addToInventory(starterArmor); // player.equip(starterArmor); // player.addToInventory(starterShield); // player.equip(starterShield); // player.addToInventory(starterRing); // player.equip(starterRing); // Add new status effect items for testing player.addToInventory(ItemTable.weapons[3].createItem()); // Stunning Hammer player.addToInventory(ItemTable.weapons[4].createItem()); // Jagged Dagger player.addToInventory(ItemTable.weapons[5].createItem()); // Sunderer Axe player.addToInventory(ItemTable.shields[3].createItem()); // Cursed Shield _prepareNextStage(); _logManager.clear(); _addLog("Game Started! Stage 1"); notifyListeners(); } void _prepareNextStage() { _turnTransactionId++; // Invalidate previous timers // Save Game at the start of each stage SaveManager.saveGame(this); StageType type; // Stage Type Logic if (stage % GameConfig.eliteStageInterval == 0) { type = StageType.elite; // Every 10th stage is a Boss/Elite } else if (stage % GameConfig.shopStageInterval == 0) { type = StageType.shop; // Every 5th stage is a Shop (except 10, 20...) } else if (stage % GameConfig.restStageInterval == 0) { type = StageType.rest; // Every 8th stage is a Rest } else { type = StageType.battle; } // Prepare Data based on Type Character? newEnemy; List shopItems = []; if (type == StageType.battle || type == StageType.elite) { bool isElite = type == StageType.elite; EnemyTemplate template = EnemyTable.getRandomEnemy( stage: stage, isElite: isElite, ); newEnemy = template.createCharacter(stage: stage); // Assign to the main 'enemy' field for UI compatibility enemy = newEnemy; isPlayerTurn = true; 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) { // Generate random items for shop using ShopProvider shopProvider.generateShopItems(stage); shopItems = shopProvider.availableItems; // Dummy enemy to prevent null errors in existing UI (until UI is fully updated) enemy = Character(name: "Merchant", maxHp: 9999, armor: 0, atk: 0); _addLog("Stage $stage: Entered a Shop."); } else if (type == StageType.rest) { // Dummy enemy enemy = Character(name: "Campfire", maxHp: 9999, armor: 0, atk: 0); _addLog("Stage $stage: Found a safe resting spot."); } currentStage = StageModel( type: type, enemy: newEnemy, shopItems: shopItems, // Pass items from ShopProvider ); turnCount = 1; notifyListeners(); } // Shop-related methods are now handled by ShopProvider // Replaces _spawnEnemy // void _spawnEnemy() { ... } - Removed Future _onDefeat() async { _addLog("Player defeated! Enemy wins!"); await SaveManager.clearSaveData(); notifyListeners(); } /// Handle player's action choice Future playerAction(ActionType type, RiskLevel risk) async { if (!isPlayerTurn || player.isDead || enemy.isDead || showRewardPopup) return; // 0. Apply Enemy Pre-emptive Defense (Just-in-Time) if (currentEnemyIntent?.type == EnemyActionType.defend && !currentEnemyIntent!.isApplied) { final intent = currentEnemyIntent!; 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."); notifyListeners(); // 상태 변경을 알림 _endPlayerTurn(); return; } isPlayerTurn = false; notifyListeners(); // 2. Process Start-of-Turn Effects (Stun, Bleed) bool canAct = _processStartTurnEffects(player); if (player.isDead) { await _onDefeat(); return; } if (!canAct) { _endPlayerTurn(); // Skip turn if stunned return; } _addLog("Player chose to ${type.name} with ${risk.name} risk."); // Calculate Outcome using CombatCalculator int baseValue = (type == ActionType.attack) ? player.totalAtk : player.totalDefense; final result = CombatCalculator.calculateActionOutcome( risk: risk, luck: player.totalLuck, baseValue: baseValue, ); if (result.success) { if (type == ActionType.attack) { 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 } else { // Defense Success - Impact is immediate, so process it directly final event = EffectEvent( id: DateTime.now().millisecondsSinceEpoch.toString() + Random().nextInt(1000).toString(), type: ActionType.defend, risk: risk, target: EffectTarget.player, feedbackType: null, targetEntity: player, // player is target for defense armorGained: result.value, attacker: player, // player is attacker in this context isSuccess: true, ); _effectEventController.sink.add(event); // handleImpact(event); // REMOVED: Driven by UI } } else { // Failure final eventId = DateTime.now().millisecondsSinceEpoch.toString() + Random().nextInt(1000).toString(); BattleFeedbackType feedbackType = (type == ActionType.attack) ? BattleFeedbackType.miss : BattleFeedbackType.failed; EffectTarget eventTarget = (type == ActionType.attack) ? EffectTarget.enemy : EffectTarget.player; Character eventTargetEntity = (type == ActionType.attack) ? enemy : player; final event = EffectEvent( id: eventId, type: type, risk: risk, target: eventTarget, feedbackType: feedbackType, attacker: player, targetEntity: eventTargetEntity, isSuccess: false, ); _effectEventController.sink.add( event, ); // Send event for miss/fail feedback _addLog("${player.name}'s ${type.name} ${feedbackType.name}!"); 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) if (enemy.isDead) { // Check enemy death after player's action _onVictory(); return; } // _endPlayerTurn(); // REMOVED: Driven by UI via handleImpact } void _endPlayerTurn() { // Update durations at end of turn player.updateStatusEffects(); // Check if enemy is dead from bleed if (enemy.isDead) { _onVictory(); return; } int tid = _turnTransactionId; Future.delayed( const Duration(milliseconds: GameConfig.animDelayEnemyTurn), () { if (tid != _turnTransactionId) return; _startEnemyTurn(); }, ); } // --- Turn Management Phases --- // Phase 1: Enemy Action Phase Future _startEnemyTurn() async { _turnTransactionId++; // Start of Enemy Turn Phase if (!isPlayerTurn && (player.isDead || enemy.isDead)) return; _addLog("Enemy's turn..."); // REMOVED: Initial delay for faster pacing // await Future.delayed( // const Duration(milliseconds: GameConfig.animDelayEnemyTurn), // ); // Armor decay if (enemy.armor > 0) { enemy.armor = (enemy.armor * GameConfig.armorDecayRate).toInt(); _addLog("Enemy's armor decayed to ${enemy.armor}."); } // Process Start-of-Turn Effects bool canAct = _processStartTurnEffects(enemy); if (enemy.isDead) { _onVictory(); return; } if (canAct && currentEnemyIntent != null) { 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.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: DateTime.now().millisecondsSinceEpoch.toString() + Random().nextInt(1000).toString(), type: ActionType.attack, risk: intent.risk, target: EffectTarget.player, feedbackType: null, attacker: enemy, targetEntity: player, damageValue: intent.finalValue, isSuccess: true, ); _effectEventController.sink.add(event); // 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: DateTime.now().millisecondsSinceEpoch.toString() + Random().nextInt(1000).toString(), type: ActionType.attack, risk: intent.risk, target: EffectTarget.player, feedbackType: BattleFeedbackType.miss, attacker: enemy, targetEntity: player, isSuccess: false, ); _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(); }); } } // Phase 2: End Enemy Turn & Generate Next Intent void _endEnemyTurn() { if (player.isDead) return; // Game Over check // Generate NEXT intent _generateEnemyIntent(); _processMiddleTurn(); } // Phase 3: Middle Turn (Apply Defense Effects) Future _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) { player.armor = (player.armor * GameConfig.armorDecayRate).toInt(); _addLog("Player's armor decayed to ${player.armor}."); } if (player.isDead) { _onDefeat(); return; } isPlayerTurn = true; turnCount++; notifyListeners(); } void _addLog(String message) { _logManager.addLog(message); notifyListeners(); } void _onVictory() { // Calculate Gold Reward // Base 10 + (Stage * 5) + Random variance final random = Random(); int goldReward = GameConfig.baseGoldReward + (stage * GameConfig.goldRewardPerStage) + random.nextInt(GameConfig.goldRewardVariance); player.gold += goldReward; _lastGoldReward = goldReward; // Store for UI display _addLog("Enemy defeated! Gained $goldReward Gold."); _addLog("Choose a reward."); ItemTier currentTier = ItemTier.tier1; if (stage > GameConfig.tier2StageMax) currentTier = ItemTier.tier3; else if (stage > GameConfig.tier1StageMax) currentTier = ItemTier.tier2; rewardOptions = []; bool isElite = currentStage.type == StageType.elite; bool isTier1 = currentTier == ItemTier.tier1; // Get 3 distinct items for (int i = 0; i < 3; i++) { ItemRarity? minRarity; ItemRarity? maxRarity; // 1. Elite Reward Logic (First Item only) if (isElite && i == 0) { if (isTier1) { // Tier 1 Elite: Guaranteed Rare minRarity = ItemRarity.rare; maxRarity = ItemRarity .rare; // Or allow higher? Request said "Guaranteed Rare 1 drop". Let's fix to Rare. } else { // Tier 2/3 Elite: Guaranteed Legendary minRarity = ItemRarity.legendary; // maxRarity = ItemRarity.legendary; // Optional, but let's allow Unique too if weights permit, or fix to Legendary. Request said "Guaranteed Legendary". } } // 2. Standard Reward Logic (Others) else { if (isTier1) { // Tier 1 Normal/Other Rewards: Max Magic (No Rare+) maxRarity = ItemRarity.magic; } // Tier 2/3 Normal: No extra restrictions } ItemTemplate? item = ItemTable.getRandomItem( tier: currentTier, minRarity: minRarity, maxRarity: maxRarity, ); if (item != null) { rewardOptions.add(item.createItem(stage: stage)); } } // Add "None" (Skip) Option // We can represent "None" as a null or a special Item. // Using a special Item with ID "reward_skip" is safer for List. rewardOptions.add( Item( id: "reward_skip", name: "Skip Reward", description: "Take nothing and move on.", atkBonus: 0, hpBonus: 0, slot: EquipmentSlot.accessory, ), ); showRewardPopup = true; notifyListeners(); } bool selectReward(Item item) { if (item.id == "reward_skip") { _addLog("Skipped reward."); _completeStage(); return true; } else { bool added = player.addToInventory(item); if (added) { _addLog("Added ${item.name} to inventory."); _completeStage(); return true; } else { _addLog("Inventory is full! Could not take ${item.name}."); return false; } } } void _completeStage() { // Heal player after selecting reward int healAmount = GameMath.floor( player.totalMaxHp * GameConfig.stageHealRatio, ); player.heal(healAmount); _addLog("Stage Cleared! Recovered $healAmount HP."); stage++; showRewardPopup = false; _prepareNextStage(); // Log moved to _prepareNextStage // isPlayerTurn = true; // Handled in _prepareNextStage for battles notifyListeners(); } void equipItem(Item item) { if (player.equip(item)) { _addLog("Equipped ${item.name}."); } else { _addLog( "Failed to equip ${item.name}.", ); // Should not happen if logic is correct } notifyListeners(); } void unequipItem(Item item) { if (player.unequip(item)) { _addLog("Unequipped ${item.name}."); } else { _addLog("Failed to unequip ${item.name} (Inventory might be full)."); } notifyListeners(); } void discardItem(Item item) { if (player.inventory.remove(item)) { _addLog("Discarded ${item.name}."); notifyListeners(); } } void sellItem(Item item) { if (player.inventory.remove(item)) { int sellPrice = GameMath.floor( item.price * GameConfig.sellPriceMultiplier, ); player.gold += sellPrice; _addLog("Sold ${item.name} for $sellPrice G."); notifyListeners(); } } /// Proceed to next stage from non-battle stages (Shop, Rest) void proceedToNextStage() { stage++; _prepareNextStage(); } void _generateEnemyIntent() { if (enemy.isDead) { currentEnemyIntent = null; return; } final random = Random(); // Decide Action Type // If baseDefense is 0, CANNOT defend. bool canDefend = enemy.baseDefense > 0; // Check for DefenseForbidden status if (enemy.hasStatus(StatusEffectType.defenseForbidden)) { canDefend = false; } bool isAttack = true; if (canDefend) { // 70% Attack, 30% Defend isAttack = random.nextDouble() < BattleConfig.enemyAttackChance; } else { isAttack = true; } // Decide Risk Level RiskLevel risk = RiskLevel.values[random.nextInt(RiskLevel.values.length)]; double efficiency = 1.0; switch (risk) { case RiskLevel.safe: efficiency = BattleConfig.safeEfficiency; break; case RiskLevel.normal: efficiency = BattleConfig.normalEfficiency; break; case RiskLevel.risky: efficiency = BattleConfig.riskyEfficiency; break; } if (isAttack) { // Attack Intent // Variance removed as per request int damage = (enemy.totalAtk * efficiency).toInt(); if (damage < 1) damage = 1; // Calculate success immediately bool success = false; switch (risk) { case RiskLevel.safe: success = random.nextDouble() < BattleConfig.safeBaseChance; break; case RiskLevel.normal: success = random.nextDouble() < BattleConfig.normalBaseChance; break; case RiskLevel.risky: success = random.nextDouble() < BattleConfig.riskyBaseChance; break; } currentEnemyIntent = EnemyIntent( type: EnemyActionType.attack, value: damage, risk: risk, description: "Attacks for $damage (${risk.name})", isSuccess: success, finalValue: damage, ); } else { // Defend Intent int baseDef = enemy.totalDefense; // Variance removed int armor = (baseDef * efficiency).toInt(); // Calculate success immediately bool success = false; switch (risk) { case RiskLevel.safe: success = random.nextDouble() < BattleConfig.safeBaseChance; break; case RiskLevel.normal: success = random.nextDouble() < BattleConfig.normalBaseChance; break; case RiskLevel.risky: success = random.nextDouble() < BattleConfig.riskyBaseChance; break; } currentEnemyIntent = EnemyIntent( type: EnemyActionType.defend, value: armor, risk: risk, description: "Defends for $armor (${risk.name})", isSuccess: success, finalValue: armor, ); // 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 void _processAttackImpact(EffectEvent event) { final attacker = event.attacker!; final target = event.targetEntity!; // Attack type needs detailed damage calculation if (event.type == ActionType.attack) { int incomingDamage = event.damageValue!; // Calculate Damage to HP using CombatCalculator int damageToHp = CombatCalculator.calculateDamageToHp( incomingDamage: incomingDamage, currentArmor: target.armor, isVulnerable: target.hasStatus(StatusEffectType.vulnerable), ); // Calculate Remaining Armor int remainingArmor = CombatCalculator.calculateRemainingArmor( incomingDamage: incomingDamage, currentArmor: target.armor, isVulnerable: target.hasStatus(StatusEffectType.vulnerable), ); // Log details if (target.armor > 0) { int absorbed = target.armor - remainingArmor; if (damageToHp == 0) { _addLog("${target.name}'s armor absorbed all damage."); } else { _addLog("${target.name}'s armor absorbed $absorbed damage."); } } target.armor = remainingArmor; if (damageToHp > 0) { target.hp -= damageToHp; if (target.hp < 0) target.hp = 0; _damageEventController.sink.add( DamageEvent( damage: damageToHp, target: (target == player) ? DamageTarget.player : DamageTarget.enemy, type: target.hasStatus(StatusEffectType.vulnerable) ? DamageType.vulnerable : DamageType.normal, ), ); _addLog("${attacker.name} dealt $damageToHp damage to ${target.name}."); } else { _addLog("${attacker.name}'s attack was fully blocked by armor."); } // Try applying status effects _tryApplyStatusEffects(attacker, target); } else if (event.type == ActionType.defend) { // Defense Impact is immediate (no anim delay from UI) if (event.isSuccess!) { // Check success again for clarity int armorGained = event.armorGained!; target.armor += armorGained; _addLog("${target.name} gained $armorGained armor."); } else { // Failed Defense _addLog("${target.name}'s defense failed!"); } } // Check for death after impact if (target.isDead) { if (target == player) { _onDefeat(); } else { _onVictory(); } } notifyListeners(); } /// 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) { final result = CombatCalculator.processStartTurnEffects(character); int totalBleed = result['bleedDamage']; bool isStunned = result['isStunned']; // 1. Bleed 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 _damageEventController.sink.add( DamageEvent( damage: totalBleed, target: (character == player) ? DamageTarget.player : DamageTarget.enemy, type: DamageType.bleed, ), ); } // 2. Stun Check if (isStunned) { _addLog("${character.name} is stunned!"); } return !isStunned; } /// Tries to apply status effects from attacker's equipment to the target. void _tryApplyStatusEffects(Character attacker, Character target) { List effectsToApply = CombatCalculator.getAppliedEffects( attacker, ); for (var effect in effectsToApply) { target.addStatusEffect(effect); _addLog("Applied ${effect.type.name} to ${target.name}!"); } } /// Process effects that happen at the start of the turn (Bleed, Stun). }