- consumable items
This commit is contained in:
2025-12-10 16:05:42 +09:00
parent 8f72b9a812
commit 09d8fdcfe9
24 changed files with 859 additions and 369 deletions
+121 -27
View File
@@ -73,7 +73,7 @@ class BattleProvider with ChangeNotifier {
final Random _random; // Injected Random instance
BattleProvider({required this.shopProvider, Random? random})
: _random = random ?? Random() {
: _random = random ?? Random() {
// initializeBattle(); // Do not auto-start logic
}
@@ -125,6 +125,15 @@ class BattleProvider with ChangeNotifier {
player.addToInventory(ItemTable.weapons[5].createItem()); // Sunderer Axe
player.addToInventory(ItemTable.shields[3].createItem()); // Cursed Shield
// Add Potions for Testing (Requested by User)
var healPotion = ItemTable.get('potion_heal_small');
var armorPotion = ItemTable.get('potion_armor_small');
var strPotion = ItemTable.get('potion_strength_small');
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");
@@ -222,7 +231,7 @@ class BattleProvider with ChangeNotifier {
// _endPlayerTurn(); // Allow player to choose another action
return;
}
isPlayerTurn = false;
notifyListeners();
@@ -257,7 +266,11 @@ class BattleProvider with ChangeNotifier {
if (result.success) {
if (type == ActionType.attack) {
// 1. Check for Dodge (Moved from _processAttackImpact)
if (CombatCalculator.calculateDodge(enemy.totalDodge, random: _random)) { // Pass injected random
if (CombatCalculator.calculateDodge(
enemy.totalDodge,
random: _random,
)) {
// Pass injected random
_addLog("${enemy.name} dodged the attack!");
final event = EffectEvent(
id:
@@ -383,14 +396,22 @@ class BattleProvider with ChangeNotifier {
// Recalculate value based on current stats
if (intent.type == EnemyActionType.attack) {
newValue = (enemy.totalAtk *
CombatCalculator.getEfficiency(ActionType.attack, intent.risk))
.toInt();
newValue =
(enemy.totalAtk *
CombatCalculator.getEfficiency(
ActionType.attack,
intent.risk,
))
.toInt();
if (newValue < 1 && enemy.totalAtk > 0) newValue = 1;
} else {
newValue = (enemy.totalDefense *
CombatCalculator.getEfficiency(ActionType.defend, intent.risk))
.toInt();
newValue =
(enemy.totalDefense *
CombatCalculator.getEfficiency(
ActionType.defend,
intent.risk,
))
.toInt();
if (newValue < 1 && enemy.totalDefense > 0) newValue = 1;
}
@@ -520,7 +541,11 @@ class BattleProvider with ChangeNotifier {
// Attack Action (Animating)
if (intent.isSuccess) {
// 1. Check for Dodge
if (CombatCalculator.calculateDodge(player.totalDodge, random: _random)) { // Pass injected random
if (CombatCalculator.calculateDodge(
player.totalDodge,
random: _random,
)) {
// Pass injected random
_addLog("${player.name} dodged the attack!");
final event = EffectEvent(
id:
@@ -540,9 +565,13 @@ class BattleProvider with ChangeNotifier {
}
// Recalculate damage to account for status changes (like Disarmed)
int finalDamage = (enemy.totalAtk *
CombatCalculator.getEfficiency(ActionType.attack, intent.risk))
.toInt();
int finalDamage =
(enemy.totalAtk *
CombatCalculator.getEfficiency(
ActionType.attack,
intent.risk,
))
.toInt();
if (finalDamage < 1 && enemy.totalAtk > 0) finalDamage = 1;
final event = EffectEvent(
@@ -579,17 +608,18 @@ class BattleProvider with ChangeNotifier {
_effectEventController.sink.add(event);
return;
}
}
} else if (!canAct) { // If cannot act (stunned)
_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.");
}
} else if (!canAct) {
// If cannot act (stunned)
_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;
@@ -777,6 +807,68 @@ class BattleProvider with ChangeNotifier {
}
}
/// Use a consumable item during battle (Free Action)
void useConsumable(Item item) {
if (item.slot != EquipmentSlot.consumable) {
_addLog("Cannot use ${item.name}!");
return;
}
// 1. Apply Immediate Effects
bool effectApplied = false;
// Heal
if (item.hpBonus > 0) {
int currentHp = player.hp;
player.heal(item.hpBonus);
int healedAmount = player.hp - currentHp;
if (healedAmount > 0) {
_addLog("Used ${item.name}. Recovered $healedAmount HP.");
effectApplied = true;
} else {
_addLog("Used ${item.name}. HP is already full.");
// Still consume? Yes, usually potions are lost even if full HP if used.
// But maybe valid to just say "Recovered 0 HP".
effectApplied = true;
}
}
// Armor
if (item.armorBonus > 0) {
player.armor += item.armorBonus;
_addLog("Used ${item.name}. Gained ${item.armorBonus} Armor.");
effectApplied = true;
}
// 2. Apply Status Effects (Buffs)
if (item.effects.isNotEmpty) {
for (var effect in item.effects) {
player.addStatusEffect(
StatusEffect(
type: effect.type,
duration: effect.duration,
value: effect.value,
),
);
// Log handled? Character.addStatusEffect might need logging or we log here.
// Let's add specific logs for known buffs
if (effect.type == StatusEffectType.attackUp) {
_addLog(
"Used ${item.name}. Attack Up for ${effect.duration} turn(s)!",
);
} else {
_addLog("Used ${item.name}. Applied ${effect.type.name}!");
}
}
effectApplied = true;
}
if (effectApplied) {
player.inventory.remove(item);
notifyListeners();
}
}
/// Proceed to next stage from non-battle stages (Shop, Rest)
void proceedToNextStage() {
stage++;
@@ -799,9 +891,11 @@ class BattleProvider with ChangeNotifier {
// Decide Action Type
// Check constraints
bool canDefend = enemy.baseDefense > 0 &&
bool canDefend =
enemy.baseDefense > 0 &&
!enemy.hasStatus(StatusEffectType.defenseForbidden);
bool canAttack = true; // Attack is always possible, but strength is affected by status.
bool canAttack =
true; // Attack is always possible, but strength is affected by status.
bool isAttack = true; // Default to attack
@@ -981,7 +1075,7 @@ class BattleProvider with ChangeNotifier {
// Try applying status effects
_tryApplyStatusEffects(attacker, target);
// If target is enemy, update intent to reflect potential status changes (e.g. Disarmed)
if (target == enemy) {
updateEnemyIntent();
+11
View File
@@ -3,10 +3,13 @@ import 'package:shared_preferences/shared_preferences.dart';
class SettingsProvider with ChangeNotifier {
static const String _keyEnemyAnim = 'settings_enemy_anim';
static const String _keyAttackAnimScale = 'settings_attack_anim_scale';
bool _enableEnemyAnimations = true; // Default: Enabled
double _attackAnimScale = 5.0; // Default: 5.0
bool get enableEnemyAnimations => _enableEnemyAnimations;
double get attackAnimScale => _attackAnimScale;
SettingsProvider() {
_loadSettings();
@@ -15,6 +18,7 @@ class SettingsProvider with ChangeNotifier {
Future<void> _loadSettings() async {
final prefs = await SharedPreferences.getInstance();
_enableEnemyAnimations = prefs.getBool(_keyEnemyAnim) ?? true;
_attackAnimScale = prefs.getDouble(_keyAttackAnimScale) ?? 5.0;
notifyListeners();
}
@@ -24,4 +28,11 @@ class SettingsProvider with ChangeNotifier {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_keyEnemyAnim, value);
}
Future<void> setAttackAnimScale(double value) async {
_attackAnimScale = value;
notifyListeners();
final prefs = await SharedPreferences.getInstance();
await prefs.setDouble(_keyAttackAnimScale, value);
}
}
+43 -4
View File
@@ -23,11 +23,50 @@ class ShopProvider with ChangeNotifier {
currentTier = ItemTier.tier2;
availableItems = [];
availableItems = [];
// 1. Generate 4 Random Equipment Items
for (int i = 0; i < 4; i++) {
// Generate 4 items
ItemTemplate? template = ItemTable.getRandomItem(tier: currentTier);
if (template != null) {
availableItems.add(template.createItem(stage: stage));
// Exclude consumables from this pool if getRandomItem includes them by default (it does if we don't filter)
// We need to implement slot exclusion or explicit slot inclusion in getRandomItem?
// Or simply cycle slots?
// ItemTable.getRandomItem picks from allItems which now includes consumables.
// We should add filtering to getRandomItem logic OR filter here.
// Let's filter here by retrying or explicitly asking for non-consumables.
// Actually, ItemTable.getRandomItem accepts 'slot'. But we want ANY equipment.
// Let's rely on type checking or add 'excludeSlot' to getRandomItem (too much change).
// Simpler: Just pick random, if consumable, reroll? Or better:
// Let's update getRandomItem to support multiple allowed slots? No.
// Let's just pick strictly by slot rotation or random filtering.
// Let's try simple filtering loop.
while (true) {
ItemTemplate? template = ItemTable.getRandomItem(tier: currentTier);
if (template != null && template.slot != EquipmentSlot.consumable) {
availableItems.add(template.createItem(stage: stage));
break;
}
}
}
// 2. Generate 2 Random Consumables
// Consumables might always be Tier 1 for now, or match current tier?
// Let's match current tier (though we only defined Tier 1 potions).
// If no potions at current tier, fallback to Tier 1?
// ItemTable.consumables currently only has items.
// Let's just pick from ItemTable.consumables directly for simplicity and safety.
if (ItemTable.consumables.isNotEmpty) {
for (int i = 0; i < 2; i++) {
ItemTemplate? consTemplate = ItemTable.getRandomItem(
tier: ItemTier.tier1, // Potions are Tier 1 for now
slot: EquipmentSlot.consumable,
);
if (consTemplate != null) {
availableItems.add(consTemplate.createItem(stage: stage));
}
}
}
notifyListeners();