740 lines
20 KiB
Dart
740 lines
20 KiB
Dart
import 'dart:async'; // StreamController 사용을 위해 import
|
|
import 'dart:math';
|
|
|
|
import 'package:flutter/material.dart';
|
|
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 '../utils/game_math.dart';
|
|
import '../game/enums.dart';
|
|
import '../game/model/damage_event.dart'; // DamageEvent import
|
|
import '../game/model/effect_event.dart'; // EffectEvent import
|
|
|
|
class EnemyIntent {
|
|
final EnemyActionType type;
|
|
final int value;
|
|
final RiskLevel risk;
|
|
final String description;
|
|
final bool isSuccess;
|
|
final int finalValue;
|
|
|
|
EnemyIntent({
|
|
required this.type,
|
|
required this.value,
|
|
required this.risk,
|
|
required this.description,
|
|
required this.isSuccess,
|
|
required this.finalValue,
|
|
});
|
|
}
|
|
|
|
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;
|
|
|
|
List<String> battleLogs = [];
|
|
bool isPlayerTurn = true;
|
|
|
|
int stage = 1;
|
|
int turnCount = 1;
|
|
List<Item> rewardOptions = [];
|
|
bool showRewardPopup = false;
|
|
|
|
List<String> get logs => battleLogs;
|
|
|
|
// 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;
|
|
|
|
BattleProvider() {
|
|
// initializeBattle(); // Do not auto-start logic
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_damageEventController.close(); // StreamController 닫기
|
|
_effectEventController.close();
|
|
super.dispose();
|
|
}
|
|
|
|
void initializeBattle() {
|
|
stage = 1;
|
|
turnCount = 1;
|
|
player = Character(
|
|
name: "Player",
|
|
maxHp: 80,
|
|
armor: 0,
|
|
atk: 5,
|
|
baseDefense: 5,
|
|
);
|
|
|
|
// Provide starter equipment
|
|
final starterSword = Item(
|
|
name: "Wooden Sword",
|
|
description: "A basic sword",
|
|
atkBonus: 5,
|
|
hpBonus: 0,
|
|
slot: EquipmentSlot.weapon,
|
|
);
|
|
final starterArmor = Item(
|
|
name: "Leather Armor",
|
|
description: "Basic protection",
|
|
atkBonus: 0,
|
|
hpBonus: 20,
|
|
slot: EquipmentSlot.armor,
|
|
);
|
|
final starterShield = Item(
|
|
name: "Wooden Shield",
|
|
description: "A small shield",
|
|
atkBonus: 0,
|
|
hpBonus: 0,
|
|
armorBonus: 3,
|
|
slot: EquipmentSlot.shield,
|
|
);
|
|
final starterRing = Item(
|
|
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();
|
|
battleLogs.clear();
|
|
_addLog("Game Started! Stage 1");
|
|
notifyListeners();
|
|
}
|
|
|
|
void _prepareNextStage() {
|
|
StageType type;
|
|
|
|
// Stage Type Logic
|
|
if (stage % 10 == 0) {
|
|
type = StageType.elite; // Every 10th stage is a Boss/Elite
|
|
} else if (stage % 5 == 0) {
|
|
type = StageType.shop; // Every 5th stage is a Shop (except 10, 20...)
|
|
} else if (stage % 8 == 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;
|
|
// Select random enemy template
|
|
final random = Random();
|
|
EnemyTemplate template;
|
|
if (isElite) {
|
|
if (EnemyTable.eliteEnemies.isNotEmpty) {
|
|
template = EnemyTable
|
|
.eliteEnemies[random.nextInt(EnemyTable.eliteEnemies.length)];
|
|
} else {
|
|
// Fallback if no elite enemies loaded
|
|
template = const EnemyTemplate(
|
|
name: "Elite Guardian",
|
|
baseHp: 50,
|
|
baseAtk: 10,
|
|
baseDefense: 2,
|
|
);
|
|
}
|
|
} else {
|
|
if (EnemyTable.normalEnemies.isNotEmpty) {
|
|
template = EnemyTable
|
|
.normalEnemies[random.nextInt(EnemyTable.normalEnemies.length)];
|
|
} else {
|
|
// Fallback
|
|
template = const EnemyTemplate(
|
|
name: "Enemy",
|
|
baseHp: 20,
|
|
baseAtk: 5,
|
|
baseDefense: 0,
|
|
);
|
|
}
|
|
}
|
|
|
|
newEnemy = template.createCharacter(stage: stage);
|
|
|
|
// Assign to the main 'enemy' field for UI compatibility
|
|
enemy = newEnemy;
|
|
isPlayerTurn = true;
|
|
showRewardPopup = false;
|
|
|
|
_generateEnemyIntent(); // Generate first intent
|
|
|
|
_addLog("Stage $stage ($type) started! A wild ${enemy.name} appeared.");
|
|
} else if (type == StageType.shop) {
|
|
// Generate random items for shop
|
|
final random = Random();
|
|
List<ItemTemplate> allTemplates = List.from(ItemTable.allItems);
|
|
allTemplates.shuffle(random);
|
|
|
|
int count = min(4, allTemplates.length);
|
|
shopItems = allTemplates
|
|
.sublist(0, count)
|
|
.map((t) => t.createItem(stage: stage))
|
|
.toList();
|
|
|
|
// 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,
|
|
);
|
|
turnCount = 1;
|
|
notifyListeners();
|
|
}
|
|
|
|
// Replaces _spawnEnemy
|
|
// void _spawnEnemy() { ... } - Removed
|
|
|
|
/// Handle player's action choice
|
|
|
|
void playerAction(ActionType type, RiskLevel risk) {
|
|
if (!isPlayerTurn || player.isDead || enemy.isDead || showRewardPopup)
|
|
return;
|
|
|
|
// Update Enemy Status Effects at the start of Player's turn (user request)
|
|
|
|
enemy.updateStatusEffects();
|
|
|
|
// 1. Check for Defense Forbidden status
|
|
if (type == ActionType.defend &&
|
|
player.hasStatus(StatusEffectType.defenseForbidden)) {
|
|
_addLog("Cannot defend! You are under Defense Forbidden status.");
|
|
return;
|
|
}
|
|
|
|
isPlayerTurn = false;
|
|
notifyListeners();
|
|
|
|
// 2. Process Start-of-Turn Effects (Stun, Bleed)
|
|
bool canAct = _processStartTurnEffects(player);
|
|
if (!canAct) {
|
|
_endPlayerTurn(); // Skip turn if stunned
|
|
return;
|
|
}
|
|
|
|
_addLog("Player chose to ${type.name} with ${risk.name} risk.");
|
|
|
|
final random = Random();
|
|
bool success = false;
|
|
double efficiency = 1.0;
|
|
|
|
switch (risk) {
|
|
case RiskLevel.safe:
|
|
success = random.nextDouble() < 1.0; // 100%
|
|
efficiency = 0.5; // 50%
|
|
break;
|
|
case RiskLevel.normal:
|
|
success = random.nextDouble() < 0.8; // 80%
|
|
efficiency = 1.0; // 100%
|
|
break;
|
|
case RiskLevel.risky:
|
|
success = random.nextDouble() < 0.4; // 40%
|
|
efficiency = 2.0; // 200%
|
|
break;
|
|
}
|
|
|
|
if (success) {
|
|
if (type == ActionType.attack) {
|
|
int damage = (player.totalAtk * efficiency).toInt();
|
|
|
|
_effectEventController.sink.add(
|
|
EffectEvent(
|
|
type: ActionType.attack,
|
|
|
|
risk: risk,
|
|
|
|
target: EffectTarget.enemy,
|
|
),
|
|
);
|
|
|
|
int damageToHp = 0;
|
|
if (enemy.armor > 0) {
|
|
if (enemy.armor >= damage) {
|
|
enemy.armor -= damage;
|
|
damageToHp = 0;
|
|
_addLog("Enemy's armor absorbed all $damage damage.");
|
|
} else {
|
|
damageToHp = damage - enemy.armor;
|
|
_addLog("Enemy's armor absorbed ${enemy.armor} damage.");
|
|
enemy.armor = 0;
|
|
}
|
|
} else {
|
|
damageToHp = damage;
|
|
}
|
|
|
|
if (damageToHp > 0) {
|
|
_applyDamage(enemy, damageToHp, targetType: DamageTarget.enemy);
|
|
_addLog("Player dealt $damageToHp damage to Enemy.");
|
|
} else {
|
|
_addLog("Player's attack was fully blocked by armor.");
|
|
}
|
|
|
|
// Try applying status effects from items
|
|
|
|
_tryApplyStatusEffects(player, enemy);
|
|
} else {
|
|
_effectEventController.sink.add(
|
|
EffectEvent(
|
|
type: ActionType.defend,
|
|
|
|
risk: risk,
|
|
|
|
target: EffectTarget.player,
|
|
),
|
|
);
|
|
|
|
int armorGained = (player.totalDefense * efficiency).toInt();
|
|
|
|
player.armor += armorGained;
|
|
|
|
_addLog("Player gained $armorGained armor.");
|
|
}
|
|
} else {
|
|
_addLog("Player's action missed!");
|
|
}
|
|
|
|
if (enemy.isDead) {
|
|
_onVictory();
|
|
return;
|
|
}
|
|
|
|
_endPlayerTurn();
|
|
}
|
|
|
|
void _endPlayerTurn() {
|
|
// Update durations at end of turn
|
|
player.updateStatusEffects();
|
|
|
|
// Check if enemy is dead from bleed
|
|
if (enemy.isDead) {
|
|
_onVictory();
|
|
return;
|
|
}
|
|
|
|
Future.delayed(const Duration(seconds: 1), () => _enemyTurn());
|
|
}
|
|
|
|
Future<void> _enemyTurn() async {
|
|
if (!isPlayerTurn && (player.isDead || enemy.isDead)) return;
|
|
|
|
_addLog("Enemy's turn...");
|
|
await Future.delayed(const Duration(seconds: 1));
|
|
|
|
// Enemy Turn Start Logic
|
|
// Armor decay
|
|
if (enemy.armor > 0) {
|
|
enemy.armor = (enemy.armor * 0.5).toInt();
|
|
_addLog("Enemy's armor decayed to ${enemy.armor}.");
|
|
}
|
|
|
|
// 1. Process Start-of-Turn Effects for Enemy
|
|
bool canAct = _processStartTurnEffects(enemy);
|
|
|
|
// Check death from bleed before acting
|
|
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.");
|
|
} else {
|
|
// Attack Logic
|
|
if (intent.isSuccess) {
|
|
_effectEventController.sink.add(
|
|
EffectEvent(
|
|
type: ActionType.attack,
|
|
risk: intent.risk,
|
|
target: EffectTarget.player,
|
|
),
|
|
);
|
|
|
|
int incomingDamage = intent.finalValue;
|
|
int damageToHp = 0;
|
|
|
|
// Handle Player Armor
|
|
if (player.armor > 0) {
|
|
if (player.armor >= incomingDamage) {
|
|
player.armor -= incomingDamage;
|
|
damageToHp = 0;
|
|
_addLog("Armor absorbed all $incomingDamage damage.");
|
|
} else {
|
|
damageToHp = incomingDamage - player.armor;
|
|
_addLog("Armor absorbed ${player.armor} damage.");
|
|
player.armor = 0;
|
|
}
|
|
} else {
|
|
damageToHp = incomingDamage;
|
|
}
|
|
|
|
if (damageToHp > 0) {
|
|
_applyDamage(player, damageToHp, targetType: DamageTarget.player);
|
|
_addLog("Enemy dealt $damageToHp damage to Player HP.");
|
|
}
|
|
} else {
|
|
_addLog("Enemy's ${intent.risk.name} attack missed!");
|
|
}
|
|
}
|
|
} else if (!canAct) {
|
|
_addLog("Enemy is stunned and cannot act!");
|
|
} else {
|
|
_addLog("Enemy did nothing.");
|
|
}
|
|
|
|
// Generate next intent
|
|
if (!player.isDead) {
|
|
_generateEnemyIntent();
|
|
}
|
|
|
|
// Player Turn Start Logic
|
|
// Armor decay
|
|
if (player.armor > 0) {
|
|
player.armor = (player.armor * 0.5).toInt();
|
|
_addLog("Player's armor decayed to ${player.armor}.");
|
|
}
|
|
|
|
if (player.isDead) {
|
|
_addLog("Player defeated! Enemy wins!");
|
|
}
|
|
|
|
isPlayerTurn = true;
|
|
turnCount++;
|
|
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) {
|
|
bool canAct = true;
|
|
|
|
// 1. Bleed Damage
|
|
var bleedEffects = character.statusEffects
|
|
.where((e) => e.type == StatusEffectType.bleed)
|
|
.toList();
|
|
if (bleedEffects.isNotEmpty) {
|
|
int totalBleed = bleedEffects.fold(0, (sum, e) => sum + e.value);
|
|
int previousHp = character.hp; // Record HP before damage
|
|
character.hp -= totalBleed;
|
|
if (character.hp < 0) character.hp = 0;
|
|
_addLog("${character.name} takes $totalBleed bleed damage!");
|
|
|
|
// Emit DamageEvent for bleed
|
|
if (character == player) {
|
|
_damageEventController.sink.add(
|
|
DamageEvent(
|
|
damage: totalBleed,
|
|
target: DamageTarget.player,
|
|
type: DamageType.bleed,
|
|
),
|
|
);
|
|
} else if (character == enemy) {
|
|
_damageEventController.sink.add(
|
|
DamageEvent(
|
|
damage: totalBleed,
|
|
target: DamageTarget.enemy,
|
|
type: DamageType.bleed,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// 2. Stun Check
|
|
if (character.hasStatus(StatusEffectType.stun)) {
|
|
canAct = false;
|
|
_addLog("${character.name} is stunned!");
|
|
}
|
|
|
|
return canAct;
|
|
}
|
|
|
|
/// Tries to apply status effects from attacker's equipment to the target.
|
|
void _tryApplyStatusEffects(Character attacker, Character target) {
|
|
final random = Random();
|
|
|
|
for (var item in attacker.equipment.values) {
|
|
for (var effect in item.effects) {
|
|
// Roll for probability (0-100)
|
|
if (random.nextInt(100) < effect.probability) {
|
|
// Apply effect
|
|
final newStatus = StatusEffect(
|
|
type: effect.type,
|
|
duration: effect.duration,
|
|
value: effect.value,
|
|
);
|
|
target.addStatusEffect(newStatus);
|
|
_addLog("Applied ${effect.type.name} to ${target.name}!");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void _applyDamage(
|
|
Character target,
|
|
int damage, {
|
|
required DamageTarget targetType,
|
|
DamageType type = DamageType.normal,
|
|
}) {
|
|
// Check Vulnerable
|
|
if (target.hasStatus(StatusEffectType.vulnerable)) {
|
|
damage = (damage * 1.5).toInt();
|
|
_addLog("Vulnerable! Damage increased to $damage.");
|
|
type = DamageType.vulnerable;
|
|
}
|
|
|
|
target.hp -= damage;
|
|
if (target.hp < 0) target.hp = 0;
|
|
|
|
_damageEventController.sink.add(
|
|
DamageEvent(damage: damage, target: targetType, type: type),
|
|
);
|
|
}
|
|
|
|
void _addLog(String message) {
|
|
battleLogs.add(message);
|
|
notifyListeners();
|
|
}
|
|
|
|
void _onVictory() {
|
|
_addLog("Enemy defeated! Choose a reward.");
|
|
|
|
final random = Random();
|
|
List<ItemTemplate> allTemplates = List.from(ItemTable.allItems);
|
|
allTemplates.shuffle(random); // Shuffle to randomize selection
|
|
|
|
// Take first 3 items (ensure distinct templates if possible, though list is small now)
|
|
int count = min(3, allTemplates.length);
|
|
rewardOptions = allTemplates.sublist(0, count).map((template) {
|
|
return template.createItem(stage: stage);
|
|
}).toList();
|
|
|
|
showRewardPopup = true;
|
|
notifyListeners();
|
|
}
|
|
|
|
void selectReward(Item item) {
|
|
bool added = player.addToInventory(item);
|
|
if (added) {
|
|
_addLog("Added ${item.name} to inventory.");
|
|
} else {
|
|
_addLog("Inventory is full! ${item.name} discarded.");
|
|
}
|
|
|
|
// Heal player after selecting reward
|
|
int healAmount = GameMath.floor(player.totalMaxHp * 0.1);
|
|
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 * 0.6);
|
|
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;
|
|
bool isAttack = true;
|
|
|
|
if (canDefend) {
|
|
// 70% Attack, 30% Defend
|
|
isAttack = random.nextDouble() < 0.7;
|
|
} 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 = 0.5;
|
|
break;
|
|
case RiskLevel.normal:
|
|
efficiency = 1.0;
|
|
break;
|
|
case RiskLevel.risky:
|
|
efficiency = 2.0;
|
|
break;
|
|
}
|
|
|
|
if (isAttack) {
|
|
// Attack Intent
|
|
// Variance: +/- 20%
|
|
double variance = 0.8 + random.nextDouble() * 0.4;
|
|
int damage = (enemy.totalAtk * efficiency * variance).toInt();
|
|
if (damage < 1) damage = 1;
|
|
|
|
// Calculate success immediately
|
|
bool success = false;
|
|
switch (risk) {
|
|
case RiskLevel.safe:
|
|
success = random.nextDouble() < 1.0;
|
|
break;
|
|
case RiskLevel.normal:
|
|
success = random.nextDouble() < 0.8;
|
|
break;
|
|
case RiskLevel.risky:
|
|
success = random.nextDouble() < 0.4;
|
|
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
|
|
double variance = 0.8 + random.nextDouble() * 0.4;
|
|
int armor = (baseDef * 2 * efficiency * variance).toInt();
|
|
|
|
// Calculate success immediately
|
|
bool success = false;
|
|
switch (risk) {
|
|
case RiskLevel.safe:
|
|
success = random.nextDouble() < 1.0;
|
|
break;
|
|
case RiskLevel.normal:
|
|
success = random.nextDouble() < 0.8;
|
|
break;
|
|
case RiskLevel.risky:
|
|
success = random.nextDouble() < 0.4;
|
|
break;
|
|
}
|
|
|
|
currentEnemyIntent = EnemyIntent(
|
|
type: EnemyActionType.defend,
|
|
value: armor,
|
|
risk: risk,
|
|
description: "Defends for $armor (${risk.name})",
|
|
isSuccess: success,
|
|
finalValue: armor,
|
|
);
|
|
|
|
// Apply defense immediately if successful
|
|
if (success) {
|
|
enemy.armor += armor;
|
|
_addLog("Enemy prepares defense! (+$armor Armor)");
|
|
_effectEventController.sink.add(
|
|
EffectEvent(
|
|
type: ActionType.defend,
|
|
risk: risk,
|
|
target: EffectTarget.enemy,
|
|
),
|
|
);
|
|
} else {
|
|
_addLog("Enemy tried to defend but fumbled!");
|
|
}
|
|
}
|
|
notifyListeners();
|
|
}
|
|
}
|