import 'dart:async'; import 'dart:math'; import 'package:flutter/material.dart'; import '../game/models.dart'; import '../game/data.dart'; import '../game/enums.dart'; import '../game/save_manager.dart'; import '../game/config.dart'; import '../game/logic.dart'; import 'shop_provider.dart'; class BattleProvider with ChangeNotifier { static final Duration _turnEffectVisualDelay = AnimationConfig.floatingTextDuration + const Duration(milliseconds: 100); late Character player; late Character enemy; late StageModel currentStage; EnemyIntent? currentEnemyIntent; final BattleLogManager _logManager = BattleLogManager(); bool isPlayerTurn = true; int _turnTransactionId = 0; int stage = 1; int turnCount = 1; List rewardOptions = []; bool showRewardPopup = false; int _lastGoldReward = 0; final ShopProvider shopProvider; final Random _random; BattleProvider({required this.shopProvider, Random? random}) : _random = random ?? Random(); List get logs => _logManager.logs; int get lastGoldReward => _lastGoldReward; StageType get nextStageType => StageManager.getStageTypeFor(stage + 1); StageType getStageTypeFor(int stageNumber) { return StageManager.getStageTypeFor(stageNumber); } void refreshUI() { notifyListeners(); } // Streams for UI Feedback final _damageEventController = StreamController.broadcast(); Stream get damageStream => _damageEventController.stream; final _effectEventController = StreamController.broadcast(); Stream get effectStream => _effectEventController.stream; final _healEventController = StreamController.broadcast(); Stream get healStream => _healEventController.stream; @override void dispose() { _damageEventController.close(); _effectEventController.close(); _healEventController.close(); super.dispose(); } // --- Initialization & Flow --- void initializeBattle() { player = PlayerTable.get("warrior")?.createCharacter() ?? Character(name: "Player", maxHp: 50, armor: 0, atk: 5, baseDefense: 5); player.gold = GameConfig.startingGold; stage = 1; // Starting items var healPotion = ItemTable.get('potion_heal_small'); var armorPotion = ItemTable.get('potion_armor_small'); var strPotion = ItemTable.get('potion_strength_small'); _addStartingItem('war_hammer'); // Stun test weapon _addStartingItem('barbed_net'); // Bleed test weapon _addStartingItem('hooked_spear'); // Disarm test weapon _addStartingItem('cursed_shield'); // Defense-forbidden test shield 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 _addStartingItem(String itemId) { final item = ItemTable.get(itemId); if (item != null) { player.addToInventory(item.createItem()); } } void loadFromSave(Map data) { player = Character.fromJson(data['player']); stage = data['stage'] ?? 1; _prepareNextStage(); _logManager.clear(); _addLog("Game Loaded! Stage $stage"); notifyListeners(); } void _prepareNextStage() { _turnTransactionId++; SaveManager.saveGame(this); player.armor = 0; StageType type = StageManager.getStageTypeFor(stage); enemy = StageManager.generateEnemy(stage, type); List shopItems = []; if (type == StageType.battle || type == StageType.elite) { isPlayerTurn = true; showRewardPopup = false; _generateEnemyIntent(); } else if (type == StageType.shop) { shopProvider.generateShopItems(stage); shopItems = shopProvider.availableItems; _addLog("Stage $stage: Entered a Shop."); } else if (type == StageType.rest) { _addLog("Stage $stage: Found a safe resting spot."); } currentStage = StageModel(type: type, enemy: enemy, shopItems: shopItems); turnCount = 1; notifyListeners(); } // --- Combat Logic --- Future playerAction(ActionType type, RiskLevel risk) async { if (!isPlayerTurn || player.isDead || enemy.isDead || showRewardPopup) { return; } applyPendingEnemyDefense(); if (type == ActionType.defend && player.hasStatus(StatusEffectType.defenseForbidden)) { _addLog("Cannot defend! You are under Defense Forbidden status."); notifyListeners(); return; } isPlayerTurn = false; notifyListeners(); TurnEffectResult turnEffect = _processStartTurnEffects(player); if (player.isDead) { await _onDefeat(); return; } if (turnEffect.effectTriggered) { await Future.delayed(_turnEffectVisualDelay); } if (!turnEffect.canAct) { _endPlayerTurn(); return; } _addLog("Player chose to ${type.name} with ${risk.name} risk."); int baseValue = (type == ActionType.attack) ? player.totalAtk : player.totalDefense; final result = CombatCalculator.calculateActionOutcome( actionType: type, risk: risk, luck: player.totalLuck, baseValue: baseValue, random: _random, ); if (result.success) { if (type == ActionType.attack) { if (CombatCalculator.calculateDodge( enemy.totalDodge, random: _random, )) { _addLog("${enemy.name} dodged the attack!"); _effectEventController.sink.add( EffectEventFactory.createDodgeEvent( attacker: player, target: enemy, effectTarget: EffectTarget.enemy, risk: risk, random: _random, ), ); } else { _effectEventController.sink.add( EffectEventFactory.createAttackEvent( attacker: player, target: enemy, effectTarget: EffectTarget.enemy, risk: risk, damage: result.value, random: _random, ), ); } } else { _effectEventController.sink.add( EffectEventFactory.createDefenseEvent( attacker: player, target: player, effectTarget: EffectTarget.player, risk: risk, armorGained: result.value, random: _random, ), ); } } else { final event = EffectEventFactory.createFailureEvent( attacker: player, target: (type == ActionType.attack) ? enemy : player, effectTarget: (type == ActionType.attack) ? EffectTarget.enemy : EffectTarget.player, type: type, risk: risk, random: _random, ); _effectEventController.sink.add(event); _addLog("${player.name}'s ${type.name} ${event.feedbackType!.name}!"); } if (enemy.isDead) { _onVictory(); } } void _endPlayerTurn() { if (enemy.isDead) return; isPlayerTurn = false; player.updateEndOfTurnStatusEffects(); final tid = _turnTransactionId; Future.delayed( const Duration(milliseconds: GameConfig.animDelayEnemyTurn), () { if (tid != _turnTransactionId) return; _processEnemyTurn(); }, ); } Future _processEnemyTurn() async { if (enemy.isDead || player.isDead) return; turnCount++; TurnEffectResult turnEffect = _processStartTurnEffects(enemy); if (enemy.isDead) { _onVictory(); return; } if (turnEffect.effectTriggered) { await Future.delayed(_turnEffectVisualDelay); } if (currentEnemyIntent != null && turnEffect.canAct) { final intent = currentEnemyIntent!; if (intent.type == EnemyActionType.defend) { _addLog("${enemy.name} maintains defensive stance."); _effectEventController.sink.add( EffectEventFactory.createDefenseEvent( attacker: enemy, target: enemy, effectTarget: EffectTarget.enemy, risk: intent.risk, armorGained: intent.finalValue, random: _random, ), ); } else { if (intent.isSuccess) { if (CombatCalculator.calculateDodge( player.totalDodge, random: _random, )) { _addLog("${player.name} dodged the attack!"); _effectEventController.sink.add( EffectEventFactory.createDodgeEvent( attacker: enemy, target: player, effectTarget: EffectTarget.player, risk: intent.risk, random: _random, ), ); } else { int finalDamage = (enemy.totalAtk * CombatCalculator.getEfficiency( ActionType.attack, intent.risk, )) .toInt(); if (finalDamage < 1 && enemy.totalAtk > 0) finalDamage = 1; _effectEventController.sink.add( EffectEventFactory.createAttackEvent( attacker: enemy, target: player, effectTarget: EffectTarget.player, risk: intent.risk, damage: finalDamage, random: _random, ), ); } } else { _addLog("Enemy's ${intent.risk.name} attack missed!"); _effectEventController.sink.add( EffectEventFactory.createFailureEvent( attacker: enemy, target: player, effectTarget: EffectTarget.player, type: ActionType.attack, risk: intent.risk, random: _random, ), ); } } } else if (!turnEffect.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(); }); } } void _endEnemyTurn() { if (player.isDead) return; enemy.updateEndOfTurnStatusEffects(); _generateEnemyIntent(); _startPlayerTurn(); } void _startPlayerTurn() { isPlayerTurn = true; notifyListeners(); } TurnEffectResult _processStartTurnEffects(Character character) { Map result = CombatCalculator.processStartTurnEffects( character, ); int bleedDamage = result['bleedDamage']; bool isStunned = result['isStunned']; if (bleedDamage > 0) { character.hp -= bleedDamage; if (character.hp < 0) character.hp = 0; _addLog("${character.name} took $bleedDamage bleed damage."); _damageEventController.sink.add( DamageEvent( damage: bleedDamage, armorDamage: 0, target: (character == player) ? DamageTarget.player : DamageTarget.enemy, type: DamageType.bleed, ), ); } character.updateStartOfTurnStatusEffects(); return TurnEffectResult( canAct: !isStunned, effectTriggered: bleedDamage > 0 || isStunned, ); } // --- Post-Animation Impacts --- void handleImpact(EffectEvent event) { if (event.isVisualOnly) { notifyListeners(); return; } if ((event.isSuccess == false || event.feedbackType != null) && event.type != ActionType.defend) { notifyListeners(); if (event.attacker == player) { _endPlayerTurn(); } else if (event.attacker == enemy) { _endEnemyTurn(); } return; } _processAttackImpact(event); if (event.triggersTurnChange) { if (event.attacker == player) { _endPlayerTurn(); } else if (event.attacker == enemy) { _endEnemyTurn(); } } } void _processAttackImpact(EffectEvent event) { final attacker = event.attacker!; final target = event.targetEntity!; if (event.type == ActionType.attack) { int incomingDamage = event.damageValue!; int damageToHp = CombatCalculator.calculateDamageToHp( incomingDamage: incomingDamage, currentArmor: target.armor, isVulnerable: target.hasStatus(StatusEffectType.vulnerable), ); int remainingArmor = CombatCalculator.calculateRemainingArmor( incomingDamage: incomingDamage, currentArmor: target.armor, isVulnerable: target.hasStatus(StatusEffectType.vulnerable), ); int armorDamage = target.armor - remainingArmor; target.armor = remainingArmor; if (damageToHp > 0) { target.hp -= damageToHp; if (target.hp < 0) target.hp = 0; } if (damageToHp > 0 || armorDamage > 0) { _damageEventController.sink.add( DamageEvent( damage: damageToHp, armorDamage: armorDamage, target: (target == player) ? DamageTarget.player : DamageTarget.enemy, type: target.hasStatus(StatusEffectType.vulnerable) ? DamageType.vulnerable : DamageType.normal, risk: event.risk, ), ); } if (damageToHp > 0) { _addLog("${attacker.name} dealt $damageToHp damage to ${target.name}."); } else if (armorDamage > 0) { _addLog("${attacker.name}'s attack was fully blocked by armor."); } _tryApplyStatusEffects(attacker, target, damageToHp); if (target == enemy) { updateEnemyIntent(); } } else if (event.type == ActionType.defend) { if (event.isSuccess!) { int armorGained = event.armorGained!; target.armor += armorGained; _addLog("${target.name} gained $armorGained armor."); } else { _addLog("${target.name}'s defense failed!"); } } if (target.isDead) { if (target == player) { _onDefeat(); } else { _onVictory(); } } notifyListeners(); } // --- Rewards & Victory --- void _onVictory() { if (stage >= GameConfig.maxStage) { _onFinalVictory(); return; } int goldReward = StageManager.calculateGoldReward(stage, _random); player.gold += goldReward; _lastGoldReward = goldReward; _addLog("Enemy defeated! Gained $goldReward Gold."); _addLog("Choose a reward."); rewardOptions = StageManager.generateRewardOptions( stage, currentStage.type, ); showRewardPopup = true; notifyListeners(); } void _onFinalVictory() { _addLog("CONGRATULATIONS! You have conquered the dungeon!"); // You could set a flag like isGameCompleted = true here notifyListeners(); } Future _onDefeat() async { _addLog("Player defeated! Enemy wins!"); await SaveManager.clearSaveData(); notifyListeners(); } bool selectReward(Item item, {bool completeStage = true}) { if (item.id == "reward_skip") { _addLog("Skipped reward."); } else { if (player.addToInventory(item)) { _addLog("Gained ${item.name}."); } else { _addLog("Inventory full! Could not take ${item.name}."); return false; } } if (completeStage) { showRewardPopup = false; stage++; _prepareNextStage(); } notifyListeners(); return true; } // --- Helper Methods --- void _addLog(String message) { _logManager.addLog(message); notifyListeners(); } void _generateEnemyIntent() { currentEnemyIntent = EnemyAIService.generateIntent(enemy, _random); notifyListeners(); } void generateEnemyIntent() { _generateEnemyIntent(); } /// Recalculates the currently telegraphed enemy intent without changing /// its action type, risk, or success roll. void updateEnemyIntent() { if (currentEnemyIntent == null || enemy.isDead) return; final intent = currentEnemyIntent!; final actionType = intent.type == EnemyActionType.attack ? ActionType.attack : ActionType.defend; final baseValue = intent.type == EnemyActionType.attack ? enemy.totalAtk : enemy.totalDefense; var newValue = (baseValue * CombatCalculator.getEfficiency(actionType, intent.risk)) .toInt(); if (newValue < 1 && baseValue > 0) { newValue = 1; } currentEnemyIntent = EnemyIntent( type: intent.type, value: newValue, risk: intent.risk, description: "$newValue (${intent.risk.name})", isSuccess: intent.isSuccess, finalValue: newValue, isApplied: intent.isApplied, ); notifyListeners(); } 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(); } } void _tryApplyStatusEffects( Character attacker, Character target, int damageToHp, ) { List effectsToApply = CombatCalculator.getAppliedEffects( attacker, random: _random, ); for (var effect in effectsToApply) { if (effect.type == StatusEffectType.bleed && damageToHp <= 0) continue; target.addStatusEffect(effect); _addLog("Applied ${effect.type.name} to ${target.name}!"); } } // --- Equipment Management --- void equipItem(Item item, {EquipmentSlot? targetSlot}) { bool success = false; if (targetSlot != null) { success = player.equipToSlot(item, targetSlot); } else { success = player.equip(item); } if (success) { _addLog("Equipped ${item.name}."); notifyListeners(); } } void unequipItem(Item item) { if (player.unequip(item)) { _addLog("Unequipped ${item.name}."); notifyListeners(); } } void sellItem(Item item) { int price = (item.price * GameConfig.sellPriceMultiplier).floor(); player.gold += price; player.inventory.remove(item); _addLog("Sold ${item.name} for $price G."); notifyListeners(); } void discardItem(Item item) { player.inventory.remove(item); _addLog("Discarded ${item.name}."); notifyListeners(); } void useConsumable(Item item) { if (item.slot != EquipmentSlot.consumable) { _addLog("Cannot use ${item.name}."); return; } var effectApplied = false; if (item.hpBonus > 0) { final hpBefore = player.hp; player.heal(item.hpBonus); final healedAmount = player.hp - hpBefore; if (healedAmount > 0) { _addLog("Used ${item.name}: Healed $healedAmount HP."); _healEventController.sink.add( HealEvent(amount: healedAmount, target: HealTarget.player), ); } else { _addLog("Used ${item.name}: HP is already full."); } effectApplied = true; } if (item.armorBonus > 0) { player.armor += item.armorBonus; _addLog("Used ${item.name}: Gained ${item.armorBonus} Armor."); effectApplied = true; } for (var effect in item.effects) { if (effect.type == StatusEffectType.heal) { final hpBefore = player.hp; player.heal(effect.value); final healedAmount = player.hp - hpBefore; if (healedAmount > 0) { _addLog("Used ${item.name}: Healed $healedAmount HP."); _healEventController.sink.add( HealEvent(amount: healedAmount, target: HealTarget.player), ); } else { _addLog("Used ${item.name}: HP is already full."); } } else { player.addStatusEffect( StatusEffect( type: effect.type, duration: effect.duration, value: effect.value, ), ); _addLog("Used ${item.name}: Applied ${effect.type.name}."); } effectApplied = true; } if (effectApplied) { player.inventory.remove(item); notifyListeners(); } } void proceedToNextStage() { stage++; _prepareNextStage(); } }