Fix: Delay Enemy Intent generation to allow attack animation to finish
This commit is contained in:
+309
-245
@@ -202,8 +202,11 @@ class BattleProvider with ChangeNotifier {
|
||||
|
||||
if (type == StageType.battle || type == StageType.elite) {
|
||||
bool isElite = type == StageType.elite;
|
||||
|
||||
EnemyTemplate template = EnemyTable.getRandomEnemy(stage: stage, isElite: isElite);
|
||||
|
||||
EnemyTemplate template = EnemyTable.getRandomEnemy(
|
||||
stage: stage,
|
||||
isElite: isElite,
|
||||
);
|
||||
|
||||
newEnemy = template.createCharacter(stage: stage);
|
||||
|
||||
@@ -251,7 +254,6 @@ class BattleProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
/// Handle player's action choice
|
||||
|
||||
Future<void> playerAction(ActionType type, RiskLevel risk) async {
|
||||
if (!isPlayerTurn || player.isDead || enemy.isDead || showRewardPopup)
|
||||
return;
|
||||
@@ -273,7 +275,7 @@ class BattleProvider with ChangeNotifier {
|
||||
|
||||
// 2. Process Start-of-Turn Effects (Stun, Bleed)
|
||||
bool canAct = _processStartTurnEffects(player);
|
||||
|
||||
|
||||
if (player.isDead) {
|
||||
await _onDefeat();
|
||||
return;
|
||||
@@ -287,139 +289,89 @@ class BattleProvider with ChangeNotifier {
|
||||
_addLog("Player chose to ${type.name} with ${risk.name} risk.");
|
||||
|
||||
// Calculate Outcome using CombatCalculator
|
||||
int baseValue = (type == ActionType.attack) ? player.totalAtk : player.totalDefense;
|
||||
|
||||
int baseValue = (type == ActionType.attack)
|
||||
? player.totalAtk
|
||||
: player.totalDefense;
|
||||
|
||||
final result = CombatCalculator.calculateActionOutcome(
|
||||
risk: risk,
|
||||
luck: player.totalLuck,
|
||||
baseValue: baseValue
|
||||
risk: risk,
|
||||
luck: player.totalLuck,
|
||||
baseValue: baseValue,
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
if (type == ActionType.attack) {
|
||||
int damage = result.value;
|
||||
|
||||
final eventId =
|
||||
DateTime.now().millisecondsSinceEpoch.toString() +
|
||||
Random().nextInt(1000).toString();
|
||||
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(
|
||||
EffectEvent(
|
||||
id: eventId,
|
||||
type: ActionType.attack,
|
||||
risk: risk,
|
||||
target: EffectTarget.enemy,
|
||||
feedbackType: null,
|
||||
),
|
||||
);
|
||||
|
||||
// Animation Delays
|
||||
int delay = GameConfig.animDelayNormal;
|
||||
if (risk == RiskLevel.safe) delay = GameConfig.animDelaySafe;
|
||||
if (risk == RiskLevel.risky) delay = GameConfig.animDelayRisky;
|
||||
|
||||
await Future.delayed(Duration(milliseconds: delay));
|
||||
|
||||
// Calculate Damage to HP using CombatCalculator
|
||||
int damageToHp = CombatCalculator.calculateDamageToHp(
|
||||
incomingDamage: damage,
|
||||
currentArmor: enemy.armor,
|
||||
isVulnerable: enemy.hasStatus(StatusEffectType.vulnerable)
|
||||
);
|
||||
|
||||
// Calculate Remaining Armor
|
||||
int remainingArmor = CombatCalculator.calculateRemainingArmor(
|
||||
incomingDamage: damage,
|
||||
currentArmor: enemy.armor,
|
||||
isVulnerable: enemy.hasStatus(StatusEffectType.vulnerable)
|
||||
);
|
||||
|
||||
// Log details
|
||||
if (enemy.armor > 0) {
|
||||
int absorbed = enemy.armor - remainingArmor;
|
||||
if (damageToHp == 0) {
|
||||
_addLog("Enemy's armor absorbed all damage.");
|
||||
} else {
|
||||
_addLog("Enemy's armor absorbed $absorbed damage.");
|
||||
}
|
||||
}
|
||||
|
||||
enemy.armor = remainingArmor;
|
||||
|
||||
if (damageToHp > 0) {
|
||||
// Note: _applyDamage internally handles Vulnerable multiplier again for the DamageEvent and logs.
|
||||
// To avoid double application, we should just pass the raw damage to _applyDamage
|
||||
// OR refactor _applyDamage.
|
||||
// Let's refactor _applyDamage to just apply the final value since we calculated it here.
|
||||
// actually _applyDamage handles the reduction of HP.
|
||||
// Let's call a simplified version or just do it here.
|
||||
|
||||
enemy.hp -= damageToHp;
|
||||
if (enemy.hp < 0) enemy.hp = 0;
|
||||
|
||||
_damageEventController.sink.add(
|
||||
DamageEvent(
|
||||
damage: damageToHp,
|
||||
target: DamageTarget.enemy,
|
||||
type: enemy.hasStatus(StatusEffectType.vulnerable) ? DamageType.vulnerable : DamageType.normal
|
||||
),
|
||||
);
|
||||
_addLog("Player dealt $damageToHp damage to Enemy.");
|
||||
} else {
|
||||
_addLog("Player's attack was fully blocked by armor.");
|
||||
}
|
||||
|
||||
// Try applying status effects
|
||||
_tryApplyStatusEffects(player, enemy);
|
||||
event,
|
||||
); // No Future.delayed here, BattleScreen will trigger impact
|
||||
} else {
|
||||
// Defense Success
|
||||
_effectEventController.sink.add(
|
||||
EffectEvent(
|
||||
id:
|
||||
DateTime.now().millisecondsSinceEpoch.toString() +
|
||||
Random().nextInt(1000).toString(),
|
||||
type: ActionType.defend,
|
||||
risk: risk,
|
||||
target: EffectTarget.player,
|
||||
feedbackType: null,
|
||||
),
|
||||
// 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,
|
||||
);
|
||||
|
||||
int armorGained = result.value;
|
||||
player.armor += armorGained;
|
||||
_addLog("Player gained $armorGained armor.");
|
||||
_effectEventController.sink.add(event);
|
||||
handleImpact(event); // Process impact via handleImpact for safety
|
||||
}
|
||||
} else {
|
||||
// Failure
|
||||
if (type == ActionType.attack) {
|
||||
_addLog("Player's attack missed!");
|
||||
_effectEventController.sink.add(
|
||||
EffectEvent(
|
||||
id:
|
||||
DateTime.now().millisecondsSinceEpoch.toString() +
|
||||
Random().nextInt(1000).toString(),
|
||||
type: type,
|
||||
risk: risk,
|
||||
target: EffectTarget.enemy,
|
||||
feedbackType: BattleFeedbackType.miss,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
_addLog("Player's defense failed!");
|
||||
_effectEventController.sink.add(
|
||||
EffectEvent(
|
||||
id:
|
||||
DateTime.now().millisecondsSinceEpoch.toString() +
|
||||
Random().nextInt(1000).toString(),
|
||||
type: type,
|
||||
risk: risk,
|
||||
target: EffectTarget.player,
|
||||
feedbackType: BattleFeedbackType.failed,
|
||||
),
|
||||
);
|
||||
}
|
||||
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); // Process impact via handleImpact for safety
|
||||
}
|
||||
|
||||
// Now check for enemy death (if applicable from bleed, or previous impacts)
|
||||
if (enemy.isDead) {
|
||||
// Check enemy death after player's action
|
||||
_onVictory();
|
||||
return;
|
||||
}
|
||||
@@ -465,92 +417,83 @@ class BattleProvider with ChangeNotifier {
|
||||
if (enemy.isDead) {
|
||||
_onVictory();
|
||||
return;
|
||||
// return; // Already handled by _processStartTurnEffects if damage applied
|
||||
}
|
||||
|
||||
if (canAct && currentEnemyIntent != null) {
|
||||
final intent = currentEnemyIntent!;
|
||||
|
||||
if (intent.type == EnemyActionType.defend) {
|
||||
// Already handled in _generateEnemyIntent
|
||||
_addLog("Enemy maintains defensive stance.");
|
||||
// Apply defense immediately if successful, then send event
|
||||
if (intent.isSuccess) {
|
||||
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);
|
||||
handleImpact(event); // Process impact via handleImpact for safety
|
||||
} else {
|
||||
_addLog("Enemy tried to defend but fumbled!");
|
||||
final event = EffectEvent(
|
||||
id:
|
||||
DateTime.now().millisecondsSinceEpoch.toString() +
|
||||
Random().nextInt(1000).toString(),
|
||||
type: ActionType.defend,
|
||||
risk: intent.risk,
|
||||
target: EffectTarget.enemy,
|
||||
feedbackType:
|
||||
BattleFeedbackType.failed, // Feedback type for failed defense
|
||||
attacker: enemy,
|
||||
targetEntity: enemy,
|
||||
isSuccess: false,
|
||||
);
|
||||
_effectEventController.sink.add(event);
|
||||
handleImpact(event); // Process impact via handleImpact for safety
|
||||
}
|
||||
} else {
|
||||
// Attack Logic
|
||||
if (intent.isSuccess) {
|
||||
_effectEventController.sink.add(
|
||||
EffectEvent(
|
||||
id:
|
||||
DateTime.now().millisecondsSinceEpoch.toString() +
|
||||
Random().nextInt(1000).toString(),
|
||||
type: ActionType.attack,
|
||||
risk: intent.risk,
|
||||
target: EffectTarget.player,
|
||||
feedbackType: null, // 공격 성공이므로 feedbackType 없음
|
||||
),
|
||||
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,
|
||||
);
|
||||
|
||||
// Fix: Wait for animation to play before applying damage
|
||||
// Determine delay based on risk level
|
||||
int delay = GameConfig.animDelayNormal;
|
||||
if (intent.risk == RiskLevel.safe) delay = GameConfig.animDelaySafe;
|
||||
if (intent.risk == RiskLevel.risky) delay = GameConfig.animDelayRisky;
|
||||
|
||||
await Future.delayed(Duration(milliseconds: delay));
|
||||
|
||||
int incomingDamage = intent.finalValue;
|
||||
|
||||
// Calculate Damage using Calculator
|
||||
int damageToHp = CombatCalculator.calculateDamageToHp(
|
||||
incomingDamage: incomingDamage,
|
||||
currentArmor: player.armor,
|
||||
isVulnerable: player.hasStatus(StatusEffectType.vulnerable)
|
||||
);
|
||||
|
||||
int remainingArmor = CombatCalculator.calculateRemainingArmor(
|
||||
incomingDamage: incomingDamage,
|
||||
currentArmor: player.armor,
|
||||
isVulnerable: player.hasStatus(StatusEffectType.vulnerable)
|
||||
);
|
||||
|
||||
if (player.armor > 0) {
|
||||
int absorbed = player.armor - remainingArmor;
|
||||
if (damageToHp == 0) {
|
||||
_addLog("Armor absorbed all damage.");
|
||||
} else {
|
||||
_addLog("Armor absorbed $absorbed damage.");
|
||||
}
|
||||
}
|
||||
player.armor = remainingArmor;
|
||||
|
||||
if (damageToHp > 0) {
|
||||
player.hp -= damageToHp;
|
||||
if (player.hp < 0) player.hp = 0;
|
||||
|
||||
_damageEventController.sink.add(
|
||||
DamageEvent(
|
||||
damage: damageToHp,
|
||||
target: DamageTarget.player,
|
||||
type: player.hasStatus(StatusEffectType.vulnerable) ? DamageType.vulnerable : DamageType.normal
|
||||
),
|
||||
);
|
||||
_addLog("Enemy dealt $damageToHp damage to Player HP.");
|
||||
}
|
||||
|
||||
// Try applying status effects from enemy equipment
|
||||
_tryApplyStatusEffects(enemy, player);
|
||||
_effectEventController.sink.add(event);
|
||||
// No Future.delayed here, BattleScreen will trigger impact
|
||||
} else {
|
||||
_addLog("Enemy's ${intent.risk.name} attack missed!");
|
||||
_effectEventController.sink.add(
|
||||
EffectEvent(
|
||||
id:
|
||||
DateTime.now().millisecondsSinceEpoch.toString() +
|
||||
Random().nextInt(1000).toString(),
|
||||
type: ActionType.attack, // 적의 공격이므로 ActionType.attack
|
||||
risk: intent.risk,
|
||||
target: EffectTarget.player, // 플레이어가 회피했으므로 플레이어 위치에 이펙트
|
||||
feedbackType: BattleFeedbackType.miss, // 변경: MISS 피드백
|
||||
),
|
||||
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,
|
||||
); // Send event for miss feedback
|
||||
handleImpact(event); // Process impact via handleImpact for safety
|
||||
}
|
||||
}
|
||||
} else if (!canAct) {
|
||||
@@ -559,6 +502,26 @@ class BattleProvider with ChangeNotifier {
|
||||
_addLog("Enemy did nothing.");
|
||||
}
|
||||
|
||||
// Wait for potential animations to finish before generating next intent
|
||||
// If attacking, we need to wait for the attack animation + return
|
||||
if (currentEnemyIntent?.type == EnemyActionType.attack &&
|
||||
currentEnemyIntent?.isSuccess == true) {
|
||||
int animDelay = GameConfig.animDelayNormal;
|
||||
if (currentEnemyIntent!.risk == RiskLevel.safe)
|
||||
animDelay = GameConfig.animDelaySafe;
|
||||
if (currentEnemyIntent!.risk == RiskLevel.risky)
|
||||
animDelay = GameConfig.animDelayRisky;
|
||||
|
||||
// Wait for impact (handled by UI) + Return time + small buffer
|
||||
// Since we removed the pre-impact delay, the UI animation starts immediately.
|
||||
// We want to generate intent AFTER the full animation cycle.
|
||||
// Full cycle ~= 2 * animDelay (Forward + Reverse)
|
||||
await Future.delayed(Duration(milliseconds: animDelay));
|
||||
} else {
|
||||
// For non-animating actions, a small pause is nice for pacing
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
}
|
||||
|
||||
// Generate next intent
|
||||
if (!player.isDead) {
|
||||
_generateEnemyIntent();
|
||||
@@ -581,50 +544,6 @@ class BattleProvider with ChangeNotifier {
|
||||
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<StatusEffect> effectsToApply = CombatCalculator.getAppliedEffects(attacker);
|
||||
|
||||
for (var effect in effectsToApply) {
|
||||
target.addStatusEffect(effect);
|
||||
_addLog("Applied ${effect.type.name} to ${target.name}!");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
void _addLog(String message) {
|
||||
_logManager.addLog(message);
|
||||
notifyListeners();
|
||||
@@ -634,7 +553,10 @@ class BattleProvider with ChangeNotifier {
|
||||
// Calculate Gold Reward
|
||||
// Base 10 + (Stage * 5) + Random variance
|
||||
final random = Random();
|
||||
int goldReward = GameConfig.baseGoldReward + (stage * GameConfig.goldRewardPerStage) + random.nextInt(GameConfig.goldRewardVariance);
|
||||
int goldReward =
|
||||
GameConfig.baseGoldReward +
|
||||
(stage * GameConfig.goldRewardPerStage) +
|
||||
random.nextInt(GameConfig.goldRewardVariance);
|
||||
|
||||
player.gold += goldReward;
|
||||
_lastGoldReward = goldReward; // Store for UI display
|
||||
@@ -648,7 +570,7 @@ class BattleProvider with ChangeNotifier {
|
||||
currentTier = ItemTier.tier2;
|
||||
|
||||
rewardOptions = [];
|
||||
|
||||
|
||||
bool isElite = currentStage.type == StageType.elite;
|
||||
bool isTier1 = currentTier == ItemTier.tier1;
|
||||
|
||||
@@ -662,13 +584,14 @@ class BattleProvider with ChangeNotifier {
|
||||
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.
|
||||
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) {
|
||||
@@ -679,11 +602,11 @@ class BattleProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
ItemTemplate? item = ItemTable.getRandomItem(
|
||||
tier: currentTier,
|
||||
tier: currentTier,
|
||||
minRarity: minRarity,
|
||||
maxRarity: maxRarity
|
||||
maxRarity: maxRarity,
|
||||
);
|
||||
|
||||
|
||||
if (item != null) {
|
||||
rewardOptions.add(item.createItem(stage: stage));
|
||||
}
|
||||
@@ -905,4 +828,145 @@ class BattleProvider with ChangeNotifier {
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 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
|
||||
return;
|
||||
}
|
||||
|
||||
// Only process actual attack or defend impacts here
|
||||
_processAttackImpact(event);
|
||||
}
|
||||
|
||||
// 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<StatusEffect> 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).
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user