game/lib/providers/battle_provider.dart

456 lines
12 KiB
Dart

import 'dart:math';
import 'package:flutter/foundation.dart';
import '../game/model/entity.dart';
import '../game/model/item.dart';
import '../game/model/status_effect.dart';
import '../game/model/stage.dart'; // Import StageModel
import '../game/data/item_table.dart';
import '../utils/game_math.dart';
enum ActionType { attack, defend }
enum RiskLevel { safe, normal, risky }
class BattleProvider with ChangeNotifier {
late Character player;
late Character enemy; // Kept for compatibility, active during Battle/Elite
late StageModel currentStage; // The current stage object
List<String> battleLogs = [];
bool isPlayerTurn = true;
int stage = 1;
List<Item> rewardOptions = [];
bool showRewardPopup = false;
BattleProvider() {
// initializeBattle(); // Do not auto-start logic
}
void initializeBattle() {
stage = 1;
player = Character(
name: "Player",
maxHp: 100,
armor: 0,
atk: 10,
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;
int hpMultiplier = isElite ? 1 : 1;
int atkMultiplier = isElite ? 4 : 2;
int enemyHp = 1 + (stage - 1) * hpMultiplier;
int enemyAtk = 8 + (stage - 1) * atkMultiplier;
String name = isElite ? "Elite Guardian" : "Enemy";
newEnemy = Character(name: name, maxHp: enemyHp, armor: 0, atk: enemyAtk);
// Assign to the main 'enemy' field for UI compatibility
enemy = newEnemy;
isPlayerTurn = true;
showRewardPopup = false;
_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,
);
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();
_applyDamage(enemy, damage);
_addLog("Player dealt $damage damage to Enemy.");
// Try applying status effects from items
_tryApplyStatusEffects(player, enemy);
} else {
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));
// 1. Process Start-of-Turn Effects for Enemy
bool canAct = _processStartTurnEffects(enemy);
// Check death from bleed before acting
if (enemy.isDead) {
_onVictory();
return;
}
if (canAct) {
int incomingDamage = enemy.totalAtk;
int damageToHp = 0;
// Enemy attack logic
// (Simple logic: Enemy always attacks for now)
// Note: Enemy doesn't have equipment yet, so no effects applied by enemy.
// 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);
_addLog("Enemy dealt $damageToHp damage to Player HP.");
}
} else {
_addLog("Enemy is stunned and cannot act!");
}
// 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;
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);
character.hp -= totalBleed;
if (character.hp < 0) character.hp = 0;
_addLog("${character.name} takes $totalBleed bleed damage!");
}
// 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) {
// Check Vulnerable
if (target.hasStatus(StatusEffectType.vulnerable)) {
damage = (damage * 1.5).toInt();
_addLog("Vulnerable! Damage increased to $damage.");
}
target.hp -= damage;
if (target.hp < 0) target.hp = 0;
}
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.5);
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)) {
player.gold += item.price;
_addLog("Sold ${item.name} for ${item.price} G.");
notifyListeners();
}
}
/// Proceed to next stage from non-battle stages (Shop, Rest)
void proceedToNextStage() {
stage++;
_prepareNextStage();
}
}