import 'dart:async'; // StreamController 사용을 위해 import import 'dart:math'; import 'package:flutter/material.dart'; import '../game/models.dart'; import '../game/data.dart'; import '../utils.dart'; import '../game/enums.dart'; import '../game/save_manager.dart'; import '../game/config.dart'; import 'shop_provider.dart'; import '../game/logic.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; final Random _random; // Injected Random instance BattleProvider({required this.shopProvider, Random? random}) : _random = random ?? Random() { // 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']); // [Fix] Update player image path from latest data (in case of legacy save data) // This ensures that even if the save file has an old path, the UI uses the correct asset. if (player.name == "Warrior") { final template = PlayerTable.get("warrior"); if (template != null && template.image != null) { player.image = template.image; } } _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; // 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 // Add Potions for Testing (Requested by User) var healPotion = ItemTable.get('potion_heal_small'); var armorPotion = ItemTable.get('potion_armor_small'); var strPotion = ItemTable.get('potion_strength_small'); if (healPotion != null) player.addToInventory(healPotion.createItem()); if (armorPotion != null) player.addToInventory(armorPotion.createItem()); if (strPotion != null) player.addToInventory(strPotion.createItem()); _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); // Reset Player Armor at start of new stage player.armor = 0; 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(); } 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. Ensure Pre-emptive Enemy Defense is applied (if not already via animation) applyPendingEnemyDefense(); // 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(); // Allow player to choose another action 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( actionType: type, // Pass player's action type risk: risk, luck: player.totalLuck, baseValue: baseValue, random: _random, // Pass injected random ); if (result.success) { if (type == ActionType.attack) { // 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(), // 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( 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}!"); // 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(); }, ); } /// 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 void _startPlayerTurn() { // Player Turn Start Logic // Armor decay (Player) if (player.armor > 0) { player.armor = (player.armor * GameConfig.armorDecayRate).toInt(); _addLog("Player's armor decayed to ${player.armor}."); } if (player.isDead) { _onDefeat(); 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. if (currentEnemyIntent != null) { // Intent generated, waiting for player interaction or action to apply. } isPlayerTurn = true; turnCount++; notifyListeners(); } void _addLog(String log) { _logManager.addLog(log); notifyListeners(); } /// Check Status Effects at Start of Turn 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; } // --- 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..."); // Armor decay (Enemy) 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 (Pre-applied at start of Player Turn) // Just show a log or maintain stance visual _addLog("${enemy.name} maintains defensive stance."); // IMPORTANT: We still need to end the turn sequence properly. // Since no animation is needed (or a very short one), we can just delay slightly. int tid = _turnTransactionId; Future.delayed(const Duration(milliseconds: 500), () { if (tid != _turnTransactionId) return; _endEnemyTurn(); }); return; } 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(), // Use injected random type: ActionType.attack, risk: intent.risk, target: EffectTarget.player, feedbackType: null, attacker: enemy, targetEntity: player, damageValue: finalDamage, isSuccess: true, ); _effectEventController.sink.add(event); // UI monitors event -> animates -> calls handleImpact -> _endEnemyTurn return; } 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; } } } 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; _endEnemyTurn(); }); } } // Phase 2: End Enemy Turn & Generate Next Intent void _endEnemyTurn() { if (player.isDead) return; // Game Over check // Update enemy status at the end of their turn enemy.updateStatusEffects(); // Generate NEXT intent _generateEnemyIntent(); _processMiddleTurn(); } // Phase 3: Middle Turn (Apply Defense Effects) Future _processMiddleTurn() async { // Phase 3 Middle Turn - Reduced functionality as Defense is now Phase 1. int tid = _turnTransactionId; // await Future.delayed(const Duration(milliseconds: 200)); // Removed for faster turn transition if (tid != _turnTransactionId) return; _startPlayerTurn(); } 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(); } } /// Use a consumable item during battle (Free Action) void useConsumable(Item item) { if (item.slot != EquipmentSlot.consumable) { _addLog("Cannot use ${item.name}!"); return; } // 1. Apply Immediate Effects bool effectApplied = false; // Heal if (item.hpBonus > 0) { int currentHp = player.hp; player.heal(item.hpBonus); int healedAmount = player.hp - currentHp; if (healedAmount > 0) { _addLog("Used ${item.name}. Recovered $healedAmount HP."); effectApplied = true; } else { _addLog("Used ${item.name}. HP is already full."); // Still consume? Yes, usually potions are lost even if full HP if used. // But maybe valid to just say "Recovered 0 HP". effectApplied = true; } } // Armor if (item.armorBonus > 0) { player.armor += item.armorBonus; _addLog("Used ${item.name}. Gained ${item.armorBonus} Armor."); effectApplied = true; } // 2. Apply Status Effects (Buffs) if (item.effects.isNotEmpty) { for (var effect in item.effects) { player.addStatusEffect( StatusEffect( type: effect.type, duration: effect.duration, value: effect.value, ), ); // Log handled? Character.addStatusEffect might need logging or we log here. // Let's add specific logs for known buffs if (effect.type == StatusEffectType.attackUp) { _addLog( "Used ${item.name}. Attack Up for ${effect.duration} turn(s)!", ); } else { _addLog("Used ${item.name}. Applied ${effect.type.name}!"); } } effectApplied = true; } if (effectApplied) { player.inventory.remove(item); notifyListeners(); } } /// Proceed to next stage from non-battle stages (Shop, Rest) void proceedToNextStage() { stage++; _prepareNextStage(); } @visibleForTesting void generateEnemyIntent() { _generateEnemyIntent(); } void _generateEnemyIntent() { if (enemy.isDead) { currentEnemyIntent = null; return; } // Use the injected _random field // final random = Random(); // Removed // Decide Action Type // Check constraints bool canDefend = enemy.baseDefense > 0 && !enemy.hasStatus(StatusEffectType.defenseForbidden); bool canAttack = true; // Attack is always possible, but strength is affected by status. 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)]; CombatResult result; if (isAttack) { result = CombatCalculator.calculateActionOutcome( actionType: ActionType.attack, risk: risk, luck: enemy.totalLuck, baseValue: enemy.totalAtk, ); currentEnemyIntent = EnemyIntent( type: EnemyActionType.attack, value: result.value, // Damage value from CombatCalculator risk: risk, description: "${result.value} (${risk.name})", isSuccess: result.success, finalValue: result.value, ); } else { result = CombatCalculator.calculateActionOutcome( actionType: ActionType.defend, risk: risk, luck: enemy.totalLuck, baseValue: enemy.totalDefense, ); currentEnemyIntent = EnemyIntent( type: EnemyActionType.defend, value: result.value, // Armor value from CombatCalculator risk: risk, description: "${result.value} (${risk.name})", isSuccess: result.success, finalValue: result.value, ); } notifyListeners(); } /// Ensure the enemy's pending defense is applied. /// Called manually by UI during animation, or auto-called by playerAction as fallback. void applyPendingEnemyDefense() { if (currentEnemyIntent != null && currentEnemyIntent!.type == EnemyActionType.defend && !currentEnemyIntent!.isApplied) { final intent = currentEnemyIntent!; if (intent.isSuccess) { enemy.armor += intent.finalValue; _addLog( "${enemy.name} assumes a defensive stance (+${intent.finalValue} Armor).", ); } else { _addLog("${enemy.name} tried to defend but failed."); } intent.isApplied = true; notifyListeners(); } } /// Applies the effects of the enemy's intent (specifically Defense) /// This should be called just before the Player's turn starts. void _applyEnemyIntentEffects() { // No pre-emptive effects in Standard Turn-Based model. // Logic cleared. } // New public method to be called by UI at impact moment void handleImpact(EffectEvent event) { if (event.isVisualOnly) { // Logic Skipped. Just log if needed, but usually logging is done at event creation. // We do NOT process damage or armor here. notifyListeners(); return; } if ((event.isSuccess == false || event.feedbackType != null) && event.type != ActionType.defend) { // 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: Phase 1 relies on manual timer. Phase 3 relies on _processMiddleTurn sequence. _endEnemyTurn(); } 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, damageToHp); // 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!) { // 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(); } /// Tries to applyStatus effects from attacker's equipment to the target. void _tryApplyStatusEffects(Character attacker, Character target, int damageToHp) { List effectsToApply = CombatCalculator.getAppliedEffects( attacker, random: _random, // Pass injected random ); for (var effect in effectsToApply) { // Logic: Bleed requires HP damage (penetrating armor) if (effect.type == StatusEffectType.bleed && damageToHp <= 0) { continue; } target.addStatusEffect(effect); _addLog("Applied ${effect.type.name} to ${target.name}!"); } } }