750 lines
20 KiB
Dart
750 lines
20 KiB
Dart
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<Item> rewardOptions = [];
|
|
bool showRewardPopup = false;
|
|
int _lastGoldReward = 0;
|
|
|
|
final ShopProvider shopProvider;
|
|
final Random _random;
|
|
|
|
BattleProvider({required this.shopProvider, Random? random})
|
|
: _random = random ?? Random();
|
|
|
|
List<String> 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<DamageEvent>.broadcast();
|
|
Stream<DamageEvent> get damageStream => _damageEventController.stream;
|
|
|
|
final _effectEventController = StreamController<EffectEvent>.broadcast();
|
|
Stream<EffectEvent> get effectStream => _effectEventController.stream;
|
|
|
|
final _healEventController = StreamController<HealEvent>.broadcast();
|
|
Stream<HealEvent> 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<String, dynamic> 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<Item> 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<void> 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<void> _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<String, dynamic> 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<void> _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<StatusEffect> 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();
|
|
}
|
|
}
|