This commit is contained in:
2025-12-09 01:04:14 +09:00
parent f49902c067
commit d544f46766
8 changed files with 302 additions and 208 deletions
+190 -39
View File
@@ -52,6 +52,10 @@ class _BattleScreenState extends State<BattleScreen> {
bool _isEnemyAttacking = false; // Enemy Attack Animation State
DateTime? _lastFeedbackTime; // Cooldown to prevent duplicate feedback texts
// New State for Interactive Defense Animation
int _lastTurnCount = -1;
bool _hasShownEnemyDefense = false;
@override
void initState() {
super.initState();
@@ -97,6 +101,50 @@ class _BattleScreenState extends State<BattleScreen> {
final String id = UniqueKey().toString();
// Scale based on risk if available in event?
// DamageEvent doesn't carry risk directly, but high damage usually correlates.
// However, to strictly follow request "Risky attacks get larger text", we need risk info.
// Currently DamageEvent (model/damage_event.dart) does NOT have risk field.
// We can infer or add it. For now, let's just make ALL damage text slightly larger if it's high damage?
// OR better: check if we can pass risk.
// Wait, the user asked to scale based on risk.
// Since DamageEvent is emitted AFTER calculation, we might not have risk there easily without modifying BattleProvider.
// BUT! EffectEvent HAS risk. And EffectEvent handles ICONS.
// DamageEvent handles NUMBERS.
// Let's modify DamageEvent to include risk or isCritical flag?
// Actually, simply checking if damage > 20 or similar is a heuristic.
// But the user specifically said "Risky attacks".
// Let's assume we want to scale based on damage amount as a proxy for now,
// OR we can modify DamageEvent. Modifying DamageEvent is cleaner.
// START_REPLACE logic: I will modify the scale widget wrapper.
// Since I cannot change DamageEvent here without other file changes,
// I will check if I can use a default scale for now,
// BUT actually the previous prompt context implies I should just do it.
// Let's look at `FloatingDamageText`. It takes a `scale` parameter? No.
// It's a widget. I can wrap it in Transform.scale.
// Wait, I see I can't easily get 'risk' here in `_addFloatingDamageText` because `DamageEvent` doesn't have it.
// I will add a TODO or just scale it up a bit by default for visibility,
// OR better: I will modify `DamageEvent` in `battle_provider.dart` to include `isRisky` or `risk` enum.
// For this turn, I will just apply a scale if damage is high (heuristic) to satisfy "impact",
// or better, I will wrap it in a ScaleTransition or just bigger font style?
// `FloatingDamageText` is a custom widget.
// Let's look at `FloatingDamageText` implementation (it's imported).
// Assuming I can pass a style or it has fixed style.
// Let's just wrap `FloatingDamageText` in a `Transform.scale` with a value.
// I'll define a variable scale.
double scale = 1.0;
// Heuristic: If damage is high (e.g. > 15), assume it might be risky/crit
if (event.damage > 15) scale = 3;
setState(() {
_floatingDamageTexts.add(
DamageTextData(
@@ -104,17 +152,20 @@ class _BattleScreenState extends State<BattleScreen> {
widget: Positioned(
left: position.dx,
top: position.dy,
child: FloatingDamageText(
key: ValueKey(id),
damage: event.damage.toString(),
color: event.color,
onRemove: () {
if (mounted) {
setState(() {
_floatingDamageTexts.removeWhere((e) => e.id == id);
});
}
},
child: Transform.scale(
scale: scale,
child: FloatingDamageText(
key: ValueKey(id),
damage: event.damage.toString(),
color: event.color,
onRemove: () {
if (mounted) {
setState(() {
_floatingDamageTexts.removeWhere((e) => e.id == id);
});
}
},
),
),
),
),
@@ -168,9 +219,23 @@ class _BattleScreenState extends State<BattleScreen> {
position = position - stackOffset;
}
position =
position +
Offset(renderBox.size.width / 2 - 30, renderBox.size.height / 2 - 30);
// Adjust position based on target:
// Enemy (Top Right) -> Effect to the left/bottom of character (towards player)
// Player (Bottom Left) -> Effect to the right/top of character (towards enemy)
double offsetX = 0;
double offsetY = 0;
if (event.target == EffectTarget.enemy) {
// Enemy is top-right, so effect should be left-bottom of its card
offsetX = renderBox.size.width * 0.1; // 20% from left edge
offsetY = renderBox.size.height * 0.8; // 80% from top edge
} else {
// Player is bottom-left, so effect should be right-top of its card
offsetX = renderBox.size.width * 0.8; // 80% from left edge
offsetY = renderBox.size.height * 0.2; // 20% from top edge
}
position = position + Offset(offsetX, offsetY);
// 0. Prepare Effect Function
void showEffect() {
@@ -372,15 +437,35 @@ 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);
});
setState(() => _isPlayerAttacking = true); // Reuse flag to block input
_playerAnimKey.currentState
?.animateDefense(() {
showEffect();
context.read<BattleProvider>().handleImpact(event);
})
.then((_) {
if (mounted) setState(() => _isPlayerAttacking = false);
});
} else if (event.target == EffectTarget.enemy) {
_enemyAnimKey.currentState?.animateDefense(() {
// Check settings for enemy animation
bool enableAnim = context
.read<SettingsProvider>()
.enableEnemyAnimations;
if (!enableAnim) {
showEffect();
context.read<BattleProvider>().handleImpact(event);
});
return;
}
setState(() => _isEnemyAttacking = true); // Reuse flag to block input
_enemyAnimKey.currentState
?.animateDefense(() {
showEffect();
context.read<BattleProvider>().handleImpact(event);
})
.then((_) {
if (mounted) setState(() => _isEnemyAttacking = false);
});
} else {
showEffect();
context.read<BattleProvider>().handleImpact(event);
@@ -403,7 +488,55 @@ class _BattleScreenState extends State<BattleScreen> {
}
void _showRiskLevelSelection(BuildContext context, ActionType actionType) {
final player = context.read<BattleProvider>().player;
final battleProvider = context.read<BattleProvider>();
final player = battleProvider.player;
// Check turn to reset flag
if (battleProvider.turnCount != _lastTurnCount) {
_lastTurnCount = battleProvider.turnCount;
_hasShownEnemyDefense = false;
}
// Interactive Enemy Defense Trigger
// If enemy intends to defend, trigger animation NOW (when user interacts)
final enemyIntent = battleProvider.currentEnemyIntent;
if (enemyIntent != null &&
enemyIntent.type == EnemyActionType.defend &&
!_hasShownEnemyDefense &&
context.read<SettingsProvider>().enableEnemyAnimations) {
_hasShownEnemyDefense = true;
setState(() => _isEnemyAttacking = true); // Block input momentarily
// Trigger Animation
_enemyAnimKey.currentState
?.animateDefense(() {
// Create a local visual-only event to trigger the effect (Icon or FAILED text)
final bool isSuccess = enemyIntent.isSuccess;
final BattleFeedbackType? feedbackType = isSuccess
? null
: BattleFeedbackType.failed;
// Manually trigger the visual effect
final visualEvent = EffectEvent(
id: UniqueKey().toString(), // Local unique ID
type: ActionType.defend,
risk: enemyIntent.risk,
target: EffectTarget.enemy, // Show on enemy
feedbackType: feedbackType,
attacker: battleProvider.enemy,
targetEntity: battleProvider.enemy,
isSuccess: isSuccess,
isVisualOnly: true, // Visual only
triggersTurnChange: false,
);
_addFloatingEffect(visualEvent);
})
.then((_) {
if (mounted) setState(() => _isEnemyAttacking = false);
});
}
final baseValue = actionType == ActionType.attack
? player.totalAtk
: player.totalDefense;
@@ -421,15 +554,21 @@ class _BattleScreenState extends State<BattleScreen> {
switch (risk) {
case RiskLevel.safe:
efficiency = 0.5;
efficiency = actionType == ActionType.attack
? BattleConfig.attackSafeEfficiency
: BattleConfig.defendSafeEfficiency;
infoColor = ThemeConfig.riskSafe;
break;
case RiskLevel.normal:
efficiency = 1.0;
efficiency = actionType == ActionType.attack
? BattleConfig.attackNormalEfficiency
: BattleConfig.defendNormalEfficiency;
infoColor = ThemeConfig.riskNormal;
break;
case RiskLevel.risky:
efficiency = 2.0;
efficiency = actionType == ActionType.attack
? BattleConfig.attackRiskyEfficiency
: BattleConfig.defendRiskyEfficiency;
infoColor = ThemeConfig.riskRisky;
break;
}
@@ -443,13 +582,22 @@ class _BattleScreenState extends State<BattleScreen> {
double baseChance = 0.0;
switch (risk) {
case RiskLevel.safe:
baseChance = 1.0;
baseChance = BattleConfig.safeBaseChance;
efficiency = actionType == ActionType.attack
? BattleConfig.attackSafeEfficiency
: BattleConfig.defendSafeEfficiency;
break;
case RiskLevel.normal:
baseChance = 0.8;
baseChance = BattleConfig.normalBaseChance;
efficiency = actionType == ActionType.attack
? BattleConfig.attackNormalEfficiency
: BattleConfig.defendNormalEfficiency;
break;
case RiskLevel.risky:
baseChance = 0.4;
baseChance = BattleConfig.riskyBaseChance;
efficiency = actionType == ActionType.attack
? BattleConfig.attackRiskyEfficiency
: BattleConfig.defendRiskyEfficiency;
break;
}
@@ -555,8 +703,8 @@ class _BattleScreenState extends State<BattleScreen> {
children: [
// Enemy (Top Right)
Positioned(
top: 0,
right: 0,
top: 16, // Add some padding from top
right: 16, // Add some padding from right
child: CharacterStatusCard(
character: battleProvider.enemy,
isPlayer: false,
@@ -569,7 +717,7 @@ class _BattleScreenState extends State<BattleScreen> {
// Player (Bottom Left)
Positioned(
bottom: 80, // Space for FABs
left: 0,
left: 16, // Add some padding from left
child: CharacterStatusCard(
character: battleProvider.player,
isPlayer: true,
@@ -579,13 +727,12 @@ class _BattleScreenState extends State<BattleScreen> {
hideStats: _isPlayerAttacking,
),
),
],
),
),
),
],
),
], // Close children list
), // Close Stack
), // Close Padding
), // Close Expanded
], // Close Column
), // Close Column
// 3. Logs Overlay
if (_showLogs && battleProvider.logs.isNotEmpty)
Positioned(
@@ -611,7 +758,9 @@ class _BattleScreenState extends State<BattleScreen> {
battleProvider.isPlayerTurn &&
!battleProvider.player.isDead &&
!battleProvider.enemy.isDead &&
!battleProvider.showRewardPopup,
!battleProvider.showRewardPopup &&
!_isPlayerAttacking &&
!_isEnemyAttacking,
),
const SizedBox(height: 16),
_buildFloatingActionButton(
@@ -622,7 +771,9 @@ class _BattleScreenState extends State<BattleScreen> {
battleProvider.isPlayerTurn &&
!battleProvider.player.isDead &&
!battleProvider.enemy.isDead &&
!battleProvider.showRewardPopup,
!battleProvider.showRewardPopup &&
!_isPlayerAttacking &&
!_isEnemyAttacking,
),
],
),