This commit is contained in:
2025-12-08 18:57:17 +09:00
parent eab99eee6f
commit f49902c067
6 changed files with 223 additions and 254 deletions
+101 -178
View File
@@ -266,65 +266,8 @@ class BattleProvider with ChangeNotifier {
if (!isPlayerTurn || player.isDead || enemy.isDead || showRewardPopup)
return;
// 0. Apply Enemy Pre-emptive Defense (Just-in-Time)
if (currentEnemyIntent?.type == EnemyActionType.defend &&
!currentEnemyIntent!.isApplied) {
final intent = currentEnemyIntent!;
if (intent.isSuccess) {
enemy.armor += intent.finalValue;
_addLog(
"Enemy raises shield just in time! (+${intent.finalValue} Armor)",
);
// Visual Effect for Enemy Defense Success
final event = EffectEvent(
id:
DateTime.now().millisecondsSinceEpoch.toString() +
Random().nextInt(1000).toString(),
type: ActionType.defend,
risk: intent.risk,
target: EffectTarget.enemy,
feedbackType: null,
attacker: enemy,
targetEntity: enemy,
armorGained: intent.finalValue,
isSuccess: true,
);
_effectEventController.sink.add(event);
}
if (!intent.isSuccess) {
_addLog("Enemy tried to raise shield but fumbled!");
print("[Logic Debug] Enemy Defense Fumbled Event"); // Debug Log
// Visual Effect for Enemy Defense Failure
final event = EffectEvent(
id:
DateTime.now().millisecondsSinceEpoch.toString() +
Random().nextInt(1000).toString(),
type: ActionType.defend,
risk: intent.risk,
target: EffectTarget.enemy,
feedbackType: BattleFeedbackType.failed,
attacker: enemy,
targetEntity: enemy,
isSuccess: false,
);
_effectEventController.sink.add(event);
}
intent.isApplied = true;
notifyListeners();
// Re-add delay to show the defense before player attack lands
int tid = _turnTransactionId;
await Future.delayed(const Duration(milliseconds: 500));
if (tid != _turnTransactionId) return;
}
// 0. Apply Enemy Pre-emptive Defense - REMOVED (Standard Turn-Based Logic)
// Defense now happens on Enemy's Turn.
// Update Enemy Status Effects at the start of Player's turn (user request)
@@ -433,7 +376,6 @@ class BattleProvider with ChangeNotifier {
event,
); // Send event for miss/fail feedback
_addLog("${player.name}'s ${type.name} ${feedbackType.name}!");
print("[Logic Debug] Player Action Failed Event"); // Debug Log
// handleImpact(event); // REMOVED: Driven by UI
}
@@ -469,18 +411,90 @@ class BattleProvider with ChangeNotifier {
// --- Turn Management Phases ---
// Phase 4: Start Player Turn
void _startPlayerTurn() {
// Player Turn Start Logic
// Armor decay (Player)
if (player.armor > 0) {
player.armor = (player.armor * GameConfig.armorDecayRate).toInt();
_addLog("Player's armor decayed to ${player.armor}.");
}
if (player.isDead) {
_onDefeat();
return;
}
// [New] Apply Pre-emptive Enemy Intent (Defense/Buffs)
if (currentEnemyIntent != null) {
final intent = currentEnemyIntent!;
if (intent.type == EnemyActionType.defend) {
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; // Mark as applied so we don't do it again
}
// Add other pre-emptive intent types here if needed (e.g., Buffs)
}
isPlayerTurn = true;
turnCount++;
notifyListeners();
}
void _addLog(String log) {
_logManager.addLog(log);
notifyListeners();
}
/// Check Status Effects at Start of Turn
bool _processStartTurnEffects(Character character) {
final result = CombatCalculator.processStartTurnEffects(character);
int totalBleed = result['bleedDamage'];
bool isStunned = result['isStunned'];
// 1. Bleed Damage
if (totalBleed > 0) {
character.hp -= totalBleed;
if (character.hp < 0) character.hp = 0;
_addLog("${character.name} takes $totalBleed bleed damage!");
// Emit DamageEvent for bleed
_damageEventController.sink.add(
DamageEvent(
damage: totalBleed,
target: (character == player)
? DamageTarget.player
: DamageTarget.enemy,
type: DamageType.bleed,
),
);
}
// 2. Stun Check
if (isStunned) {
_addLog("${character.name} is stunned!");
}
return !isStunned;
}
// --- Turn Management Phases ---
// Phase 1: Enemy Action Phase
Future<void> _startEnemyTurn() async {
_turnTransactionId++; // Start of Enemy Turn Phase
if (!isPlayerTurn && (player.isDead || enemy.isDead)) return;
_addLog("Enemy's turn...");
// REMOVED: Initial delay for faster pacing
// await Future.delayed(
// const Duration(milliseconds: GameConfig.animDelayEnemyTurn),
// );
// Armor decay
// Armor decay (Enemy)
if (enemy.armor > 0) {
enemy.armor = (enemy.armor * GameConfig.armorDecayRate).toInt();
_addLog("Enemy's armor decayed to ${enemy.armor}.");
@@ -498,33 +512,18 @@ class BattleProvider with ChangeNotifier {
final intent = currentEnemyIntent!;
if (intent.type == EnemyActionType.defend) {
// Defensive Action (Non-animating)
// Defensive Action (Pre-applied at start of Player Turn)
// Just show a log or maintain stance visual
_addLog("${enemy.name} maintains defensive stance.");
// Check if already applied in Phase 3 of previous turn
if (intent.isApplied) {
_addLog("Enemy maintains defensive stance.");
// Proceed manually
int tid = _turnTransactionId;
int delay = skipAnimations ? 500 : 1000; // Faster if animations off
Future.delayed(Duration(milliseconds: delay), () {
if (tid != _turnTransactionId) return;
_endEnemyTurn();
});
return;
}
if (intent.isSuccess) {
// ... (success logic) ...
} else {
// ... (failure logic) ...
}
// For defense (if not applied), we proceed manually
// IMPORTANT: We still need to end the turn sequence properly.
// Since no animation is needed (or a very short one), we can just delay slightly.
int tid = _turnTransactionId;
int delay = skipAnimations ? 500 : 1500; // Faster if animations off
Future.delayed(Duration(milliseconds: delay), () {
Future.delayed(const Duration(milliseconds: 500), () {
if (tid != _turnTransactionId) return;
_endEnemyTurn();
});
return;
} else {
// Attack Action (Animating)
if (intent.isSuccess) {
@@ -542,10 +541,8 @@ class BattleProvider with ChangeNotifier {
isSuccess: true,
);
_effectEventController.sink.add(event);
// CRITICAL: We DO NOT call _endEnemyTurn here.
// The UI will play the animation, then call handleImpact.
// handleImpact will trigger _endEnemyTurn.
return; // Exit _startEnemyTurn after emitting event for UI to handle
// UI monitors event -> animates -> calls handleImpact -> _endEnemyTurn
return;
} else {
// Missed Attack
_addLog("Enemy's ${intent.risk.name} attack missed!");
@@ -562,7 +559,7 @@ class BattleProvider with ChangeNotifier {
isSuccess: false,
);
_effectEventController.sink.add(event);
return; // Exit _startEnemyTurn after emitting event for UI to handle
return;
}
}
} else if (!canAct) {
@@ -594,42 +591,14 @@ class BattleProvider with ChangeNotifier {
// Phase 3: Middle Turn (Apply Defense Effects)
Future<void> _processMiddleTurn() async {
// Apply Intent Effects (Pre-emptive Defense)
// Phase 3 Middle Turn - Reduced functionality as Defense is now Phase 1.
int tid = _turnTransactionId;
await Future.delayed(const Duration(milliseconds: 500));
await Future.delayed(const Duration(milliseconds: 200)); // Short pause
if (tid != _turnTransactionId) return;
_applyEnemyIntentEffects();
// REMOVED: Delay for faster pacing
// Small pause to let the player see the enemy's new stance
_startPlayerTurn();
}
// Phase 4: Start Player Turn
void _startPlayerTurn() {
// Player Turn Start Logic
// Armor decay
if (player.armor > 0) {
player.armor = (player.armor * GameConfig.armorDecayRate).toInt();
_addLog("Player's armor decayed to ${player.armor}.");
}
if (player.isDead) {
_onDefeat();
return;
}
isPlayerTurn = true;
turnCount++;
notifyListeners();
}
void _addLog(String message) {
_logManager.addLog(message);
notifyListeners();
}
void _onVictory() {
// Calculate Gold Reward
// Base 10 + (Stage * 5) + Random variance
@@ -897,21 +866,14 @@ class BattleProvider with ChangeNotifier {
/// Applies the effects of the enemy's intent (specifically Defense)
/// This should be called just before the Player's turn starts.
void _applyEnemyIntentEffects() {
if (currentEnemyIntent == null || enemy.isDead) return;
// Prevent duplicate application
if (currentEnemyIntent!.isApplied) return;
if (currentEnemyIntent!.type == EnemyActionType.defend) {
// Logic moved to playerAction for "Just-in-Time" defense.
// Nothing to do here except maybe log intent (optional, but Intent UI covers it).
return;
}
// No pre-emptive effects in Standard Turn-Based model.
// Logic cleared.
}
// New public method to be called by UI at impact moment
void handleImpact(EffectEvent event) {
if (event.isSuccess == false || event.feedbackType != null) {
if ((event.isSuccess == false || event.feedbackType != null) &&
event.type != ActionType.defend) {
// If it's a miss/fail/feedback, just log and return
// Logging and feedback text should already be handled when event created
notifyListeners(); // Ensure UI updates for log
@@ -920,11 +882,8 @@ class BattleProvider with ChangeNotifier {
if (event.attacker == player) {
_endPlayerTurn();
} else if (event.attacker == enemy) {
// Special Case: Do NOT call _endEnemyTurn for Enemy Defense (Phase 1 & 3).
// Phase 1 relies on manual timer. Phase 3 relies on _processMiddleTurn sequence.
if (event.type != ActionType.defend) {
_endEnemyTurn();
}
// Special Case: Phase 1 relies on manual timer. Phase 3 relies on _processMiddleTurn sequence.
_endEnemyTurn();
}
return;
}
@@ -970,10 +929,10 @@ class BattleProvider with ChangeNotifier {
// FIX: Update _startEnemyTurn (Phase 1) to ALSO apply armor directly and make the event visual-only.
// Then we can globally block Enemy Defend in handleImpact.
// Step 1: Modifying handleImpact to block ALL Enemy Defend logic.
if (event.attacker == enemy && event.type == ActionType.defend) {
return;
}
// REMOVED: Blocking Enemy Defend. Now we want to process it.
// if (event.attacker == enemy && event.type == ActionType.defend) {
// return;
// }
// Only process actual attack or defend impacts here
_processAttackImpact(event);
@@ -1068,40 +1027,6 @@ class BattleProvider with ChangeNotifier {
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) {
final result = CombatCalculator.processStartTurnEffects(character);
int totalBleed = result['bleedDamage'];
bool isStunned = result['isStunned'];
// 1. Bleed Damage
if (totalBleed > 0) {
character.hp -= totalBleed;
if (character.hp < 0) character.hp = 0;
_addLog("${character.name} takes $totalBleed bleed damage!");
// Emit DamageEvent for bleed
_damageEventController.sink.add(
DamageEvent(
damage: totalBleed,
target: (character == player)
? DamageTarget.player
: DamageTarget.enemy,
type: DamageType.bleed,
),
);
}
// 2. Stun Check
if (isStunned) {
_addLog("${character.name} is stunned!");
}
return !isStunned;
}
/// Tries to apply status effects from attacker's equipment to the target.
void _tryApplyStatusEffects(Character attacker, Character target) {
List<StatusEffect> effectsToApply = CombatCalculator.getAppliedEffects(
@@ -1113,6 +1038,4 @@ class BattleProvider with ChangeNotifier {
_addLog("Applied ${effect.type.name} to ${target.name}!");
}
}
/// Process effects that happen at the start of the turn (Bleed, Stun).
}
+2 -2
View File
@@ -4,7 +4,7 @@ import 'package:shared_preferences/shared_preferences.dart';
class SettingsProvider with ChangeNotifier {
static const String _keyEnemyAnim = 'settings_enemy_anim';
bool _enableEnemyAnimations = false; // Default: Disabled
bool _enableEnemyAnimations = true; // Default: Enabled
bool get enableEnemyAnimations => _enableEnemyAnimations;
@@ -14,7 +14,7 @@ class SettingsProvider with ChangeNotifier {
Future<void> _loadSettings() async {
final prefs = await SharedPreferences.getInstance();
_enableEnemyAnimations = prefs.getBool(_keyEnemyAnim) ?? false;
_enableEnemyAnimations = prefs.getBool(_keyEnemyAnim) ?? true;
notifyListeners();
}
+37 -32
View File
@@ -126,11 +126,13 @@ class _BattleScreenState extends State<BattleScreen> {
void _addFloatingEffect(EffectEvent event) {
if (_processedEffectIds.contains(event.id)) {
// print("[UI Debug] Duplicate Event Ignored: ${event.id}");
return;
}
_processedEffectIds.add(event.id);
if (_processedEffectIds.length > 20) {
// Keep the set size manageable
if (_processedEffectIds.length > 50) {
_processedEffectIds.remove(_processedEffectIds.first);
}
@@ -138,7 +140,9 @@ class _BattleScreenState extends State<BattleScreen> {
// Feedback Text Cooldown
if (event.feedbackType != null) {
print("[UI Debug] Feedback Event: ${event.id}, Type: ${event.feedbackType}");
// print(
// "[UI Debug] Feedback Event: ${event.id}, Type: ${event.feedbackType}",
// );
if (_lastFeedbackTime != null &&
DateTime.now().difference(_lastFeedbackTime!).inMilliseconds < 300) {
return; // Skip if too soon
@@ -194,7 +198,7 @@ class _BattleScreenState extends State<BattleScreen> {
// Prevent duplicate feedback texts for the same event ID (UI Level)
if (_floatingFeedbackTexts.any((e) => e.eventId == event.id)) {
return;
return;
}
setState(() {
@@ -208,7 +212,7 @@ class _BattleScreenState extends State<BattleScreen> {
top: position.dy,
child: FloatingFeedbackText(
key: ValueKey(id),
feedback: "$feedbackText (${event.id.substring(0, 4)})",
feedback: feedbackText,
color: feedbackColor,
onRemove: () {
if (mounted) {
@@ -259,9 +263,7 @@ class _BattleScreenState extends State<BattleScreen> {
}
// 1. Player Attack Animation Trigger (Success or Miss)
if (event.type == ActionType.attack &&
event.target == EffectTarget.enemy) {
if (event.type == ActionType.attack && event.target == EffectTarget.enemy) {
final RenderBox? playerBox =
_playerKey.currentContext?.findRenderObject() as RenderBox?;
final RenderBox? enemyBox =
@@ -277,11 +279,13 @@ class _BattleScreenState extends State<BattleScreen> {
});
// Force SAFE animation for MISS, otherwise use event risk
final RiskLevel animRisk = event.feedbackType != null ? RiskLevel.safe : event.risk;
final RiskLevel animRisk = event.feedbackType != null
? RiskLevel.safe
: event.risk;
_playerAnimKey.currentState
?.animateAttack(offset, () {
showEffect();
showEffect();
context.read<BattleProvider>().handleImpact(event);
if (event.risk == RiskLevel.risky && event.feedbackType == null) {
@@ -306,11 +310,10 @@ class _BattleScreenState extends State<BattleScreen> {
}
});
}
}
}
// 2. Enemy Attack Animation Trigger (Success or Miss)
else if (event.type == ActionType.attack &&
event.target == EffectTarget.player) {
bool enableAnim = context.read<SettingsProvider>().enableEnemyAnimations;
if (!enableAnim) {
@@ -334,11 +337,13 @@ class _BattleScreenState extends State<BattleScreen> {
});
// Force SAFE animation for MISS
final RiskLevel animRisk = event.feedbackType != null ? RiskLevel.safe : event.risk;
final RiskLevel animRisk = event.feedbackType != null
? RiskLevel.safe
: event.risk;
_enemyAnimKey.currentState
?.animateAttack(offset, () {
showEffect();
showEffect();
context.read<BattleProvider>().handleImpact(event);
if (event.risk == RiskLevel.risky && event.feedbackType == null) {
@@ -363,36 +368,36 @@ class _BattleScreenState extends State<BattleScreen> {
}
});
}
}
}
// 3. Defend Animation Trigger (Success OR Failure)
else if (event.type == ActionType.defend) {
if (event.target == EffectTarget.player) {
_playerAnimKey.currentState?.animateDefense(() {
showEffect();
context.read<BattleProvider>().handleImpact(event);
});
} else if (event.target == EffectTarget.enemy) {
_enemyAnimKey.currentState?.animateDefense(() {
showEffect();
context.read<BattleProvider>().handleImpact(event);
});
} else {
if (event.target == EffectTarget.player) {
_playerAnimKey.currentState?.animateDefense(() {
showEffect();
context.read<BattleProvider>().handleImpact(event);
}
});
} else if (event.target == EffectTarget.enemy) {
_enemyAnimKey.currentState?.animateDefense(() {
showEffect();
context.read<BattleProvider>().handleImpact(event);
});
} else {
showEffect();
context.read<BattleProvider>().handleImpact(event);
}
}
// 4. Others (Feedback for attacks, Buffs, etc.)
else {
showEffect();
// If it's a feedback event (MISS/FAILED for attacks), wait 500ms.
if (event.feedbackType != null) {
Future.delayed(const Duration(milliseconds: 500), () {
if (mounted) context.read<BattleProvider>().handleImpact(event);
});
Future.delayed(const Duration(milliseconds: 500), () {
if (mounted) context.read<BattleProvider>().handleImpact(event);
});
} else {
// Success events (Icon)
context.read<BattleProvider>().handleImpact(event);
// Success events (Icon)
context.read<BattleProvider>().handleImpact(event);
}
}
}
+12 -4
View File
@@ -90,9 +90,18 @@ class CharacterStatusCard extends StatelessWidget {
}).toList(),
),
),
Text("ATK: ${character.totalAtk}", style: const TextStyle(color: ThemeConfig.textColorWhite)),
Text("DEF: ${character.totalDefense}", style: const TextStyle(color: ThemeConfig.textColorWhite)),
Text("LUCK: ${character.totalLuck}", style: const TextStyle(color: ThemeConfig.textColorWhite)),
Text(
"ATK: ${character.totalAtk}",
style: const TextStyle(color: ThemeConfig.textColorWhite),
),
Text(
"DEF: ${character.totalDefense}",
style: const TextStyle(color: ThemeConfig.textColorWhite),
),
Text(
"LUCK: ${character.totalLuck}",
style: const TextStyle(color: ThemeConfig.textColorWhite),
),
],
),
),
@@ -124,7 +133,6 @@ class CharacterStatusCard extends StatelessWidget {
),
),
const SizedBox(height: 8), // 아이콘과 정보 사이 간격
if (!isPlayer && !hideStats)
Consumer<BattleProvider>(
builder: (context, provider, child) {
@@ -268,9 +268,10 @@ class FloatingFeedbackTextState extends State<FloatingFeedbackText>
fontWeight: FontWeight.bold,
shadows: const [
Shadow(
blurRadius: 2.0,
blurRadius:
0.0, // Sharper shadow to avoid blurry/double look
color: ThemeConfig.feedbackShadow,
offset: Offset(1.0, 1.0),
offset: Offset(1.5, 1.5),
),
],
),