update
This commit is contained in:
@@ -18,6 +18,8 @@ class GameConfig {
|
||||
static const double stageHealRatio = 0.1;
|
||||
static const double vulnerableDamageMultiplier = 1.5;
|
||||
static const double armorDecayRate = 1.0;
|
||||
static const double disarmedDamageMultiplier =
|
||||
0.2; // New: Reduces ATK to 10% when disarmed
|
||||
|
||||
// Rewards
|
||||
static const int baseGoldReward = 10;
|
||||
|
||||
@@ -11,6 +11,7 @@ class EnemyTemplate {
|
||||
final int baseHp;
|
||||
final int baseAtk;
|
||||
final int baseDefense;
|
||||
final int baseDodge; // New: Base dodge chance
|
||||
final String? image;
|
||||
final List<String> equipmentIds;
|
||||
final int tier;
|
||||
@@ -20,6 +21,7 @@ class EnemyTemplate {
|
||||
required this.baseHp,
|
||||
required this.baseAtk,
|
||||
required this.baseDefense,
|
||||
this.baseDodge = 1, // Default value
|
||||
this.image,
|
||||
this.equipmentIds = const [],
|
||||
this.tier = 1,
|
||||
@@ -31,6 +33,7 @@ class EnemyTemplate {
|
||||
baseHp: json['baseHp'] ?? 10,
|
||||
baseAtk: json['baseAtk'] ?? 1,
|
||||
baseDefense: json['baseDefense'] ?? 0,
|
||||
baseDodge: json['baseDodge'] ?? 1, // Parse from JSON or default to 1
|
||||
image: json['image'],
|
||||
equipmentIds: (json['equipment'] as List<dynamic>?)?.cast<String>() ?? [],
|
||||
tier: json['tier'] ?? 1,
|
||||
@@ -46,6 +49,7 @@ class EnemyTemplate {
|
||||
maxHp: baseHp,
|
||||
atk: baseAtk,
|
||||
baseDefense: baseDefense,
|
||||
baseDodge: baseDodge, // Pass baseDodge to Character constructor
|
||||
armor: 0,
|
||||
image: image,
|
||||
);
|
||||
@@ -85,7 +89,10 @@ class EnemyTable {
|
||||
}
|
||||
|
||||
/// Returns a random enemy suitable for the current stage.
|
||||
static EnemyTemplate getRandomEnemy({required int stage, bool isElite = false}) {
|
||||
static EnemyTemplate getRandomEnemy({
|
||||
required int stage,
|
||||
bool isElite = false,
|
||||
}) {
|
||||
int targetTier = 1;
|
||||
if (stage > GameConfig.tier2StageMax) {
|
||||
targetTier = 3;
|
||||
@@ -94,7 +101,7 @@ class EnemyTable {
|
||||
}
|
||||
|
||||
List<EnemyTemplate> pool = isElite ? eliteEnemies : normalEnemies;
|
||||
|
||||
|
||||
// Filter by tier
|
||||
var tierPool = pool.where((e) => e.tier == targetTier).toList();
|
||||
|
||||
@@ -108,7 +115,12 @@ class EnemyTable {
|
||||
|
||||
if (tierPool.isEmpty) {
|
||||
// Should not happen if JSON is correct
|
||||
return const EnemyTemplate(name: "Fallback Enemy", baseHp: 10, baseAtk: 1, baseDefense: 0);
|
||||
return const EnemyTemplate(
|
||||
name: "Fallback Enemy",
|
||||
baseHp: 10,
|
||||
baseAtk: 1,
|
||||
baseDefense: 0,
|
||||
);
|
||||
}
|
||||
|
||||
return tierPool[_random.nextInt(tierPool.length)];
|
||||
|
||||
@@ -15,6 +15,7 @@ class ItemTemplate {
|
||||
final int atkBonus;
|
||||
final int hpBonus;
|
||||
final int armorBonus;
|
||||
final int dodge; // New
|
||||
final EquipmentSlot slot;
|
||||
final List<ItemEffect> effects;
|
||||
final int price;
|
||||
@@ -30,6 +31,7 @@ class ItemTemplate {
|
||||
required this.atkBonus,
|
||||
required this.hpBonus,
|
||||
required this.armorBonus,
|
||||
this.dodge = 0,
|
||||
required this.slot,
|
||||
required this.effects,
|
||||
required this.price,
|
||||
@@ -54,6 +56,7 @@ class ItemTemplate {
|
||||
atkBonus: json['atkBonus'] ?? json['baseAtk'] ?? 0,
|
||||
hpBonus: json['hpBonus'] ?? json['baseHp'] ?? 0,
|
||||
armorBonus: json['armorBonus'] ?? json['baseArmor'] ?? 0,
|
||||
dodge: json['dodge'] ?? 0,
|
||||
slot: EquipmentSlot.values.firstWhere((e) => e.name == json['slot']),
|
||||
effects: effectsList,
|
||||
price: json['price'] ?? 10,
|
||||
|
||||
@@ -9,6 +9,7 @@ class PlayerTemplate {
|
||||
final int baseHp;
|
||||
final int baseAtk;
|
||||
final int baseDefense;
|
||||
final int baseDodge; // New field
|
||||
final String? image;
|
||||
|
||||
const PlayerTemplate({
|
||||
@@ -18,6 +19,7 @@ class PlayerTemplate {
|
||||
required this.baseHp,
|
||||
required this.baseAtk,
|
||||
required this.baseDefense,
|
||||
this.baseDodge = 1, // Default 1
|
||||
this.image,
|
||||
});
|
||||
|
||||
@@ -29,6 +31,7 @@ class PlayerTemplate {
|
||||
baseHp: json['baseHp'],
|
||||
baseAtk: json['baseAtk'],
|
||||
baseDefense: json['baseDefense'],
|
||||
baseDodge: json['baseDodge'] ?? 1, // Parse with default
|
||||
image: json['image'],
|
||||
);
|
||||
}
|
||||
@@ -39,6 +42,7 @@ class PlayerTemplate {
|
||||
maxHp: baseHp,
|
||||
atk: baseAtk,
|
||||
baseDefense: baseDefense,
|
||||
baseDodge: baseDodge, // Use template value
|
||||
armor: 0,
|
||||
);
|
||||
}
|
||||
|
||||
+3
-1
@@ -9,12 +9,14 @@ enum StatusEffectType {
|
||||
vulnerable, // Takes 50% more damage
|
||||
bleed, // Takes damage at start/end of turn
|
||||
defenseForbidden, // Cannot use Defend action
|
||||
disarmed, // Attack strength reduced (e.g., 10%)
|
||||
}
|
||||
|
||||
/// 공격 실패 시 이펙트 피드백 타입 정의
|
||||
enum BattleFeedbackType {
|
||||
miss, // 공격이 빗나감
|
||||
failed, // 방어 실패
|
||||
dodge, // 회피 성공
|
||||
}
|
||||
|
||||
/// 스탯에 적용될 수 있는 수정자(Modifier)의 타입 정의.
|
||||
@@ -33,7 +35,7 @@ enum EquipmentSlot { weapon, armor, shield, accessory }
|
||||
|
||||
enum DamageType { normal, bleed, vulnerable }
|
||||
|
||||
enum StatType { maxHp, atk, defense, luck }
|
||||
enum StatType { maxHp, atk, defense, luck, dodge }
|
||||
|
||||
enum ItemRarity { normal, magic, rare, legendary, unique }
|
||||
|
||||
|
||||
@@ -23,34 +23,45 @@ class CombatResult {
|
||||
class CombatCalculator {
|
||||
static final Random _random = Random();
|
||||
|
||||
/// Helper to get efficiency multiplier based on risk and action type.
|
||||
static double getEfficiency(ActionType actionType, RiskLevel risk) {
|
||||
switch (risk) {
|
||||
case RiskLevel.safe:
|
||||
return actionType == ActionType.attack
|
||||
? BattleConfig.attackSafeEfficiency
|
||||
: BattleConfig.defendSafeEfficiency;
|
||||
case RiskLevel.normal:
|
||||
return actionType == ActionType.attack
|
||||
? BattleConfig.attackNormalEfficiency
|
||||
: BattleConfig.defendNormalEfficiency;
|
||||
case RiskLevel.risky:
|
||||
return actionType == ActionType.attack
|
||||
? BattleConfig.attackRiskyEfficiency
|
||||
: BattleConfig.defendRiskyEfficiency;
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates success and efficiency based on Risk Level and Luck.
|
||||
static CombatResult calculateActionOutcome({
|
||||
required ActionType actionType, // New: Action type (attack or defend)
|
||||
required RiskLevel risk,
|
||||
required int luck,
|
||||
required int baseValue,
|
||||
Random? random, // Injectable Random
|
||||
}) {
|
||||
double efficiency = 1.0;
|
||||
final effectiveRandom = random ?? _random;
|
||||
double efficiency = getEfficiency(actionType, risk);
|
||||
|
||||
double baseChance = 0.0;
|
||||
|
||||
switch (risk) {
|
||||
case RiskLevel.safe:
|
||||
baseChance = BattleConfig.safeBaseChance;
|
||||
efficiency = actionType == ActionType.attack
|
||||
? BattleConfig.attackSafeEfficiency
|
||||
: BattleConfig.defendSafeEfficiency;
|
||||
break;
|
||||
case RiskLevel.normal:
|
||||
baseChance = BattleConfig.normalBaseChance;
|
||||
efficiency = actionType == ActionType.attack
|
||||
? BattleConfig.attackNormalEfficiency
|
||||
: BattleConfig.defendNormalEfficiency;
|
||||
break;
|
||||
case RiskLevel.risky:
|
||||
baseChance = BattleConfig.riskyBaseChance;
|
||||
efficiency = actionType == ActionType.attack
|
||||
? BattleConfig.attackRiskyEfficiency
|
||||
: BattleConfig.defendRiskyEfficiency;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -58,7 +69,7 @@ class CombatCalculator {
|
||||
double chance = baseChance + (luck / 100.0);
|
||||
if (chance > 1.0) chance = 1.0;
|
||||
|
||||
bool success = _random.nextDouble() < chance;
|
||||
bool success = effectiveRandom.nextDouble() < chance;
|
||||
int finalValue = (baseValue * efficiency).toInt();
|
||||
if (finalValue < 1 && baseValue > 0) finalValue = 1;
|
||||
|
||||
@@ -146,12 +157,13 @@ class CombatCalculator {
|
||||
|
||||
/// Tries to apply status effects from attacker's equipment.
|
||||
/// Returns a list of applied effects.
|
||||
static List<StatusEffect> getAppliedEffects(Character attacker) {
|
||||
static List<StatusEffect> getAppliedEffects(Character attacker, {Random? random}) {
|
||||
final effectiveRandom = random ?? _random;
|
||||
List<StatusEffect> appliedEffects = [];
|
||||
|
||||
for (var item in attacker.equipment.values) {
|
||||
for (var effect in item.effects) {
|
||||
if (_random.nextInt(100) < effect.probability) {
|
||||
if (effectiveRandom.nextInt(100) < effect.probability) {
|
||||
appliedEffects.add(
|
||||
StatusEffect(
|
||||
type: effect.type,
|
||||
@@ -164,4 +176,12 @@ class CombatCalculator {
|
||||
}
|
||||
return appliedEffects;
|
||||
}
|
||||
|
||||
/// Calculates if a dodge occurs.
|
||||
/// [targetDodge] is the total dodge chance percentage (e.g. 5 = 5%).
|
||||
static bool calculateDodge(int targetDodge, {Random? random}) {
|
||||
final effectiveRandom = random ?? _random;
|
||||
if (targetDodge <= 0) return false;
|
||||
return effectiveRandom.nextInt(100) < targetDodge;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ class LootGenerator {
|
||||
int finalHp = template.hpBonus;
|
||||
int finalArmor = template.armorBonus;
|
||||
int finalLuck = template.luck;
|
||||
int finalDodge = template.dodge;
|
||||
|
||||
// 0. Normal Rarity: Prefix logic for base stat variations
|
||||
if (template.rarity == ItemRarity.normal) {
|
||||
@@ -44,6 +45,8 @@ class LootGenerator {
|
||||
finalAtk = (finalAtk * mult).floor();
|
||||
finalHp = (finalHp * mult).floor();
|
||||
finalArmor = (finalArmor * mult).floor();
|
||||
// Dodge typically stays integer, but if we want to scale it:
|
||||
// finalDodge = (finalDodge * mult).floor();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,6 +78,9 @@ class LootGenerator {
|
||||
case StatType.luck:
|
||||
finalLuck += value;
|
||||
break;
|
||||
case StatType.dodge: // Handle dodge
|
||||
finalDodge += value;
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -117,6 +123,9 @@ class LootGenerator {
|
||||
case StatType.luck:
|
||||
finalLuck += value;
|
||||
break;
|
||||
case StatType.dodge: // Handle dodge
|
||||
finalDodge += value;
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -130,6 +139,7 @@ class LootGenerator {
|
||||
atkBonus: finalAtk,
|
||||
hpBonus: finalHp,
|
||||
armorBonus: finalArmor,
|
||||
dodge: finalDodge, // Pass dodge
|
||||
slot: template.slot,
|
||||
effects: template.effects,
|
||||
price: template.price,
|
||||
|
||||
@@ -12,6 +12,7 @@ class Character {
|
||||
int armor; // Current temporary shield/armor points in battle
|
||||
int baseAtk;
|
||||
int baseDefense; // Base defense stat
|
||||
int baseDodge; // New: Base dodge chance (e.g. 1 = 1%)
|
||||
|
||||
int gold; // New: Currency
|
||||
String? image; // New: Image path
|
||||
@@ -32,6 +33,7 @@ class Character {
|
||||
required this.armor,
|
||||
required int atk,
|
||||
this.baseDefense = 0,
|
||||
this.baseDodge = 1,
|
||||
this.gold = 0,
|
||||
this.image,
|
||||
}) : baseMaxHp = maxHp,
|
||||
@@ -46,6 +48,7 @@ class Character {
|
||||
'armor': armor,
|
||||
'baseAtk': baseAtk,
|
||||
'baseDefense': baseDefense,
|
||||
'baseDodge': baseDodge,
|
||||
'gold': gold,
|
||||
'image': image,
|
||||
'equipment': equipment.map((key, value) => MapEntry(key.name, value.id)),
|
||||
@@ -63,6 +66,7 @@ class Character {
|
||||
armor: json['armor'],
|
||||
atk: json['baseAtk'],
|
||||
baseDefense: json['baseDefense'],
|
||||
baseDodge: json['baseDodge'] ?? 1,
|
||||
gold: json['gold'],
|
||||
image: json['image'],
|
||||
);
|
||||
@@ -161,7 +165,12 @@ class Character {
|
||||
|
||||
int get totalAtk {
|
||||
int bonus = equipment.values.fold(0, (sum, item) => sum + item.atkBonus);
|
||||
return baseAtk + bonus;
|
||||
int finalAtk = baseAtk + bonus;
|
||||
|
||||
if (hasStatus(StatusEffectType.disarmed)) {
|
||||
finalAtk = (finalAtk * GameConfig.disarmedDamageMultiplier).toInt();
|
||||
}
|
||||
return finalAtk;
|
||||
}
|
||||
|
||||
int get totalDefense {
|
||||
@@ -169,6 +178,11 @@ class Character {
|
||||
return baseDefense + bonus;
|
||||
}
|
||||
|
||||
int get totalDodge {
|
||||
int bonus = equipment.values.fold(0, (sum, item) => sum + item.dodge);
|
||||
return baseDodge + bonus;
|
||||
}
|
||||
|
||||
int get totalLuck {
|
||||
return equipment.values.fold(0, (sum, item) => sum + item.luck);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ class ItemEffect {
|
||||
String typeStr = type.name.toUpperCase();
|
||||
// Customize names if needed
|
||||
if (type == StatusEffectType.defenseForbidden) typeStr = "UNBLOCKABLE";
|
||||
if (type == StatusEffectType.disarmed) typeStr = "DISARM";
|
||||
|
||||
String durationStr = "${duration}t";
|
||||
String valStr = value > 0 ? " ($value dmg)" : "";
|
||||
@@ -42,6 +43,7 @@ class Item {
|
||||
final int atkBonus;
|
||||
final int hpBonus;
|
||||
final int armorBonus; // New stat for defense
|
||||
final int dodge; // New: Dodge chance bonus
|
||||
final EquipmentSlot slot;
|
||||
final List<ItemEffect> effects; // Status effects this item can inflict
|
||||
final int price; // New: Sell/Buy value
|
||||
@@ -57,6 +59,7 @@ class Item {
|
||||
required this.atkBonus,
|
||||
required this.hpBonus,
|
||||
this.armorBonus = 0, // Default to 0 for backward compatibility
|
||||
this.dodge = 0, // Default to 0
|
||||
required this.slot,
|
||||
this.effects = const [], // Default to no effects
|
||||
this.price = 0,
|
||||
|
||||
@@ -70,8 +70,10 @@ class BattleProvider with ChangeNotifier {
|
||||
|
||||
// Dependency injection
|
||||
final ShopProvider shopProvider;
|
||||
final Random _random; // Injected Random instance
|
||||
|
||||
BattleProvider({required this.shopProvider}) {
|
||||
BattleProvider({required this.shopProvider, Random? random})
|
||||
: _random = random ?? Random() {
|
||||
// initializeBattle(); // Do not auto-start logic
|
||||
}
|
||||
|
||||
@@ -134,6 +136,9 @@ class BattleProvider with ChangeNotifier {
|
||||
// Save Game at the start of each stage
|
||||
SaveManager.saveGame(this);
|
||||
|
||||
// Reset Player Armor at start of new stage
|
||||
player.armor = 0;
|
||||
|
||||
StageType type;
|
||||
|
||||
// Stage Type Logic
|
||||
@@ -214,10 +219,10 @@ class BattleProvider with ChangeNotifier {
|
||||
player.hasStatus(StatusEffectType.defenseForbidden)) {
|
||||
_addLog("Cannot defend! You are under Defense Forbidden status.");
|
||||
notifyListeners(); // 상태 변경을 알림
|
||||
_endPlayerTurn();
|
||||
// _endPlayerTurn(); // Allow player to choose another action
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
isPlayerTurn = false;
|
||||
notifyListeners();
|
||||
|
||||
@@ -246,28 +251,48 @@ class BattleProvider with ChangeNotifier {
|
||||
risk: risk,
|
||||
luck: player.totalLuck,
|
||||
baseValue: baseValue,
|
||||
random: _random, // Pass injected random
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
if (type == ActionType.attack) {
|
||||
int damage = result.value;
|
||||
// 1. Check for Dodge (Moved from _processAttackImpact)
|
||||
if (CombatCalculator.calculateDodge(enemy.totalDodge, random: _random)) { // Pass injected random
|
||||
_addLog("${enemy.name} dodged the attack!");
|
||||
final event = EffectEvent(
|
||||
id:
|
||||
DateTime.now().millisecondsSinceEpoch.toString() +
|
||||
_random.nextInt(1000).toString(), // Use injected random
|
||||
type: ActionType.attack,
|
||||
risk: risk,
|
||||
target: EffectTarget.enemy,
|
||||
feedbackType: BattleFeedbackType.dodge, // Dodge feedback
|
||||
attacker: player,
|
||||
targetEntity: enemy,
|
||||
damageValue: 0,
|
||||
isSuccess:
|
||||
false, // Treated as fail for animation purposes (or custom)
|
||||
);
|
||||
_effectEventController.sink.add(event);
|
||||
} else {
|
||||
// 2. Hit Success
|
||||
int damage = result.value;
|
||||
|
||||
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(
|
||||
event,
|
||||
); // No Future.delayed here, BattleScreen will trigger impact
|
||||
final event = EffectEvent(
|
||||
id:
|
||||
DateTime.now().millisecondsSinceEpoch.toString() +
|
||||
_random.nextInt(1000).toString(), // Use injected random
|
||||
type: ActionType.attack,
|
||||
risk: risk,
|
||||
target: EffectTarget.enemy,
|
||||
feedbackType: null,
|
||||
attacker: player,
|
||||
targetEntity: enemy,
|
||||
damageValue: damage,
|
||||
isSuccess: true,
|
||||
);
|
||||
_effectEventController.sink.add(event);
|
||||
}
|
||||
} else {
|
||||
// Defense Success - Impact is immediate, so process it directly
|
||||
final event = EffectEvent(
|
||||
@@ -348,6 +373,40 @@ class BattleProvider with ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
/// Recalculates the current enemy intent value based on current stats.
|
||||
/// Used to update UI when enemy stats change (e.g. Disarmed applied).
|
||||
void updateEnemyIntent() {
|
||||
if (currentEnemyIntent == null || enemy.isDead) return;
|
||||
|
||||
final intent = currentEnemyIntent!;
|
||||
int newValue = 0;
|
||||
|
||||
// Recalculate value based on current stats
|
||||
if (intent.type == EnemyActionType.attack) {
|
||||
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();
|
||||
if (newValue < 1 && enemy.totalDefense > 0) newValue = 1;
|
||||
}
|
||||
|
||||
// Replace intent with updated value, keeping other properties
|
||||
currentEnemyIntent = EnemyIntent(
|
||||
type: intent.type,
|
||||
value: newValue,
|
||||
risk: intent.risk,
|
||||
description: "$newValue (${intent.risk.name})",
|
||||
isSuccess: intent.isSuccess,
|
||||
finalValue: newValue,
|
||||
isApplied: intent.isApplied,
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// --- Turn Management Phases ---
|
||||
|
||||
// Phase 4: Start Player Turn
|
||||
@@ -364,6 +423,9 @@ class BattleProvider with ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update Intent if stats changed (e.g. status effects expired)
|
||||
updateEnemyIntent();
|
||||
|
||||
// [New] Apply Pre-emptive Enemy Intent (Defense/Buffs)
|
||||
// MOVED: Logic moved to applyPendingEnemyDefense() to sync with animation.
|
||||
// We just check intent existence here but do NOT apply effects yet.
|
||||
@@ -430,7 +492,8 @@ class BattleProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
// Process Start-of-Turn Effects
|
||||
bool canAct = _processStartTurnEffects(enemy);
|
||||
final result = CombatCalculator.processStartTurnEffects(enemy);
|
||||
bool canAct = !result['isStunned'];
|
||||
|
||||
if (enemy.isDead) {
|
||||
_onVictory();
|
||||
@@ -456,17 +519,43 @@ class BattleProvider with ChangeNotifier {
|
||||
} else {
|
||||
// Attack Action (Animating)
|
||||
if (intent.isSuccess) {
|
||||
// 1. Check for Dodge
|
||||
if (CombatCalculator.calculateDodge(player.totalDodge, random: _random)) { // Pass injected random
|
||||
_addLog("${player.name} dodged the attack!");
|
||||
final event = EffectEvent(
|
||||
id:
|
||||
DateTime.now().millisecondsSinceEpoch.toString() +
|
||||
_random.nextInt(1000).toString(), // Use injected random
|
||||
type: ActionType.attack,
|
||||
risk: intent.risk,
|
||||
target: EffectTarget.player,
|
||||
feedbackType: BattleFeedbackType.dodge,
|
||||
attacker: enemy,
|
||||
targetEntity: player,
|
||||
damageValue: 0,
|
||||
isSuccess: false,
|
||||
);
|
||||
_effectEventController.sink.add(event);
|
||||
return;
|
||||
}
|
||||
|
||||
// Recalculate damage to account for status changes (like Disarmed)
|
||||
int finalDamage = (enemy.totalAtk *
|
||||
CombatCalculator.getEfficiency(ActionType.attack, intent.risk))
|
||||
.toInt();
|
||||
if (finalDamage < 1 && enemy.totalAtk > 0) finalDamage = 1;
|
||||
|
||||
final event = EffectEvent(
|
||||
id:
|
||||
DateTime.now().millisecondsSinceEpoch.toString() +
|
||||
Random().nextInt(1000).toString(),
|
||||
_random.nextInt(1000).toString(), // Use injected random
|
||||
type: ActionType.attack,
|
||||
risk: intent.risk,
|
||||
target: EffectTarget.player,
|
||||
feedbackType: null,
|
||||
attacker: enemy,
|
||||
targetEntity: player,
|
||||
damageValue: intent.finalValue,
|
||||
damageValue: finalDamage,
|
||||
isSuccess: true,
|
||||
);
|
||||
_effectEventController.sink.add(event);
|
||||
@@ -490,16 +579,17 @@ class BattleProvider with ChangeNotifier {
|
||||
_effectEventController.sink.add(event);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if (!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.");
|
||||
}
|
||||
} 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;
|
||||
@@ -512,6 +602,9 @@ class BattleProvider with ChangeNotifier {
|
||||
void _endEnemyTurn() {
|
||||
if (player.isDead) return; // Game Over check
|
||||
|
||||
// Update enemy status at the end of their turn
|
||||
enemy.updateStatusEffects();
|
||||
|
||||
// Generate NEXT intent
|
||||
_generateEnemyIntent();
|
||||
|
||||
@@ -690,29 +783,45 @@ class BattleProvider with ChangeNotifier {
|
||||
_prepareNextStage();
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
void generateEnemyIntent() {
|
||||
_generateEnemyIntent();
|
||||
}
|
||||
|
||||
void _generateEnemyIntent() {
|
||||
if (enemy.isDead) {
|
||||
currentEnemyIntent = null;
|
||||
return;
|
||||
}
|
||||
|
||||
final random = Random();
|
||||
// Use the injected _random field
|
||||
// final random = Random(); // Removed
|
||||
|
||||
// Decide Action Type
|
||||
bool canDefend = enemy.baseDefense > 0;
|
||||
if (enemy.hasStatus(StatusEffectType.defenseForbidden)) {
|
||||
canDefend = false;
|
||||
}
|
||||
bool isAttack = true;
|
||||
// Check constraints
|
||||
bool canDefend = enemy.baseDefense > 0 &&
|
||||
!enemy.hasStatus(StatusEffectType.defenseForbidden);
|
||||
bool canAttack = true; // Attack is always possible, but strength is affected by status.
|
||||
|
||||
if (canDefend) {
|
||||
isAttack = random.nextDouble() < BattleConfig.enemyAttackChance;
|
||||
} else {
|
||||
bool isAttack = true; // Default to attack
|
||||
|
||||
if (canAttack && canDefend) {
|
||||
// Both options available: Use configured probability
|
||||
isAttack = _random.nextDouble() < BattleConfig.enemyAttackChance;
|
||||
} else if (canAttack) {
|
||||
// Must attack
|
||||
isAttack = true;
|
||||
} else if (canDefend) {
|
||||
// Must defend
|
||||
isAttack = false;
|
||||
} else {
|
||||
// Both forbidden (Rare case, effectively stunned but not via Stun status)
|
||||
// Default to Defend as a fallback, outcomes will be handled by stats/luck
|
||||
isAttack = false;
|
||||
}
|
||||
|
||||
// Decide Risk Level
|
||||
RiskLevel risk = RiskLevel.values[random.nextInt(RiskLevel.values.length)];
|
||||
RiskLevel risk = RiskLevel.values[_random.nextInt(RiskLevel.values.length)];
|
||||
|
||||
CombatResult result;
|
||||
if (isAttack) {
|
||||
@@ -872,6 +981,11 @@ 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();
|
||||
}
|
||||
} else if (event.type == ActionType.defend) {
|
||||
// Defense Impact is immediate (no anim delay from UI)
|
||||
if (event.isSuccess!) {
|
||||
@@ -900,6 +1014,7 @@ class BattleProvider with ChangeNotifier {
|
||||
void _tryApplyStatusEffects(Character attacker, Character target) {
|
||||
List<StatusEffect> effectsToApply = CombatCalculator.getAppliedEffects(
|
||||
attacker,
|
||||
random: _random, // Pass injected random
|
||||
);
|
||||
|
||||
for (var effect in effectsToApply) {
|
||||
|
||||
@@ -248,6 +248,10 @@ class _BattleScreenState extends State<BattleScreen> {
|
||||
feedbackText = "FAILED";
|
||||
feedbackColor = ThemeConfig.failedText;
|
||||
break;
|
||||
case BattleFeedbackType.dodge:
|
||||
feedbackText = "DODGE";
|
||||
feedbackColor = ThemeConfig.statLuckColor; // Use Luck color (Greenish)
|
||||
break;
|
||||
default:
|
||||
feedbackText = "";
|
||||
feedbackColor = ThemeConfig.textColorWhite;
|
||||
@@ -649,14 +653,15 @@ class _BattleScreenState extends State<BattleScreen> {
|
||||
!battleProvider.enemy.isDead &&
|
||||
!battleProvider.showRewardPopup &&
|
||||
!_isPlayerAttacking &&
|
||||
!_isEnemyAttacking,
|
||||
!_isEnemyAttacking, // Enabled even if disarmed (damage reduced)
|
||||
isDefendEnabled:
|
||||
battleProvider.isPlayerTurn &&
|
||||
!battleProvider.player.isDead &&
|
||||
!battleProvider.enemy.isDead &&
|
||||
!battleProvider.showRewardPopup &&
|
||||
!_isPlayerAttacking &&
|
||||
!_isEnemyAttacking,
|
||||
!_isEnemyAttacking &&
|
||||
!battleProvider.player.hasStatus(StatusEffectType.defenseForbidden), // Disable if defense is forbidden
|
||||
onAttackPressed: () =>
|
||||
_showRiskLevelSelection(context, ActionType.attack),
|
||||
onDefendPressed: () =>
|
||||
@@ -859,6 +864,7 @@ class _BattleScreenState extends State<BattleScreen> {
|
||||
if (item.hpBonus > 0) stats.add("+${item.hpBonus} ${AppStrings.hp}");
|
||||
if (item.armorBonus > 0) stats.add("+${item.armorBonus} ${AppStrings.def}");
|
||||
if (item.luck > 0) stats.add("+${item.luck} ${AppStrings.luck}");
|
||||
if (item.dodge > 0) stats.add("+${item.dodge}% Dodge"); // Add Dodge
|
||||
|
||||
List<String> effectTexts = item.effects.map((e) => e.description).toList();
|
||||
|
||||
|
||||
@@ -35,18 +35,23 @@ class CharacterStatsWidget extends StatelessWidget {
|
||||
_buildStatItem(
|
||||
AppStrings.atk,
|
||||
"${player.totalAtk}",
|
||||
color: ThemeConfig.statAtkColor,
|
||||
// color: ThemeConfig.statAtkColor,
|
||||
),
|
||||
_buildStatItem(
|
||||
AppStrings.def,
|
||||
"${player.totalDefense}",
|
||||
color: ThemeConfig.statDefColor,
|
||||
// color: ThemeConfig.statDefColor,
|
||||
),
|
||||
_buildStatItem(AppStrings.armor, "${player.armor}"),
|
||||
// _buildStatItem(AppStrings.armor, "${player.armor}"),
|
||||
_buildStatItem(
|
||||
AppStrings.luck,
|
||||
"${player.totalLuck}",
|
||||
color: ThemeConfig.statLuckColor,
|
||||
// color: ThemeConfig.statLuckColor,
|
||||
),
|
||||
_buildStatItem(
|
||||
"Dodge", // TODO: Add to AppStrings
|
||||
"${player.totalDodge}%",
|
||||
// color: ThemeConfig.statLuckColor,
|
||||
),
|
||||
_buildStatItem(
|
||||
AppStrings.gold,
|
||||
|
||||
@@ -263,6 +263,11 @@ class InventoryGridWidget extends StatelessWidget {
|
||||
player.totalLuck,
|
||||
player.totalLuck - (oldItem?.luck ?? 0) + newItem.luck,
|
||||
),
|
||||
_buildStatChangeRow(
|
||||
"Dodge",
|
||||
player.totalDodge,
|
||||
player.totalDodge - (oldItem?.dodge ?? 0) + newItem.dodge,
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
|
||||
Reference in New Issue
Block a user