game/lib/providers/battle_provider.dart

1042 lines
32 KiB
Dart

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<Item> rewardOptions = [];
bool showRewardPopup = false;
int _lastGoldReward = 0; // New: Stores gold gained from last victory
List<String> get logs => _logManager.logs;
int get lastGoldReward => _lastGoldReward;
void refreshUI() {
notifyListeners();
}
// Damage Event Stream
final _damageEventController = StreamController<DamageEvent>.broadcast();
Stream<DamageEvent> get damageStream => _damageEventController.stream;
// Effect Event Stream
final _effectEventController = StreamController<EffectEvent>.broadcast();
Stream<EffectEvent> 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<String, dynamic> 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<Item> 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<void> _onDefeat() async {
_addLog("Player defeated! Enemy wins!");
await SaveManager.clearSaveData();
notifyListeners();
}
/// Handle player's action choice
Future<void> playerAction(ActionType type, RiskLevel risk) async {
if (!isPlayerTurn || player.isDead || enemy.isDead || showRewardPopup)
return;
// 0. Apply Enemy Pre-emptive Defense - REMOVED (Standard Turn-Based Logic)
// Defense now happens on Enemy's Turn.
// 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}!");
// 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 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;
}
// [New] Apply Pre-emptive Enemy Intent (Defense/Buffs)
if (currentEnemyIntent != null) {
final intent = currentEnemyIntent!;
if (intent.type == EnemyActionType.defend) {
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; // Mark as applied so we don't do it again
}
// Add other pre-emptive intent types here if needed (e.g., Buffs)
}
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<void> _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) {
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);
// 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) {
_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<void> _processMiddleTurn() async {
// Phase 3 Middle Turn - Reduced functionality as Defense is now Phase 1.
int tid = _turnTransactionId;
await Future.delayed(const Duration(milliseconds: 200)); // Short pause
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<Item>.
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() {
// 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.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;
}
// 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.
// REMOVED: Blocking Enemy Defend. Now we want to process it.
// 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();
}
/// Tries to apply status effects from attacker's equipment to the target.
void _tryApplyStatusEffects(Character attacker, Character target) {
List<StatusEffect> effectsToApply = CombatCalculator.getAppliedEffects(
attacker,
);
for (var effect in effectsToApply) {
target.addStatusEffect(effect);
_addLog("Applied ${effect.type.name} to ${target.name}!");
}
}
}