game/lib/widgets/battle/battle_animation_widget.dart

162 lines
4.6 KiB
Dart

import 'package:flutter/material.dart';
import '../../game/enums.dart';
class BattleAnimationWidget extends StatefulWidget {
final Widget child;
const BattleAnimationWidget({super.key, required this.child});
@override
BattleAnimationWidgetState createState() => BattleAnimationWidgetState();
}
class BattleAnimationWidgetState extends State<BattleAnimationWidget>
with TickerProviderStateMixin {
late AnimationController _scaleController;
late AnimationController _translateController;
late Animation<double> _scaleAnimation;
late Animation<Offset> _translateAnimation;
@override
void initState() {
super.initState();
_scaleController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 800),
);
_translateController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1000),
);
_scaleAnimation = Tween<double>(
begin: 1.0,
end: 2.0,
).animate(CurvedAnimation(parent: _scaleController, curve: Curves.easeOut));
// Default translation, will be updated on animateAttack
_translateAnimation = Tween<Offset>(
begin: Offset.zero,
end: Offset.zero,
).animate(_translateController);
}
@override
void dispose() {
_scaleController.dispose();
_translateController.dispose();
super.dispose();
}
Future<void> animateAttack(
Offset targetOffset,
VoidCallback onImpact,
RiskLevel risk,
) async {
if (risk == RiskLevel.safe || risk == RiskLevel.normal) {
// Safe & Normal: Dash/Wobble without scale
final isSafe = risk == RiskLevel.safe;
final duration = isSafe ? 500 : 400;
final offsetFactor = isSafe ? 0.2 : 0.5;
_translateController.duration = Duration(milliseconds: duration);
_translateAnimation =
Tween<Offset>(
begin: Offset.zero,
end: targetOffset * offsetFactor,
).animate(
CurvedAnimation(
parent: _translateController,
curve: Curves.easeOutQuad,
),
);
await _translateController.forward();
if (!mounted) return;
onImpact();
await _translateController.reverse();
} else {
// Risky: Scale + Heavy Dash
_scaleController.duration = const Duration(milliseconds: 600);
_translateController.duration = const Duration(milliseconds: 500);
// 1. Scale Up (Preparation)
await _scaleController.forward();
if (!mounted) return;
// 2. Dash to Target (Impact)
// Adjust offset to prevent complete overlap (stop slightly short) since both share the same layer stack
final adjustedOffset = targetOffset * 0.5;
_translateAnimation =
Tween<Offset>(begin: Offset.zero, end: adjustedOffset).animate(
CurvedAnimation(
parent: _translateController,
curve: Curves.easeInExpo, // Heavy impact curve
),
);
await _translateController.forward();
if (!mounted) return;
// 3. Impact Callback (Shake)
onImpact();
// 4. Return (Reset)
_scaleController.reverse();
_translateController.reverse();
}
}
Future<void> animateDefense(VoidCallback onImpact) async {
// Defense: Wobble/Shake horizontally
_translateController.duration = const Duration(milliseconds: 800);
// Sequence: Left -> Right -> Center
_translateAnimation =
TweenSequence<Offset>([
TweenSequenceItem(
tween: Tween<Offset>(begin: Offset.zero, end: const Offset(-10, 0)),
weight: 25,
),
TweenSequenceItem(
tween: Tween<Offset>(
begin: const Offset(-10, 0),
end: const Offset(10, 0),
),
weight: 50,
),
TweenSequenceItem(
tween: Tween<Offset>(begin: const Offset(10, 0), end: Offset.zero),
weight: 25,
),
]).animate(
CurvedAnimation(
parent: _translateController,
curve: Curves.easeInOut,
),
);
await _translateController.forward();
if (!mounted) return;
onImpact();
_translateController.reset();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: Listenable.merge([_scaleController, _translateController]),
builder: (context, child) {
return Transform.translate(
offset: _translateAnimation.value,
child: Transform.scale(
scale: _scaleAnimation.value,
child: widget.child,
),
);
},
);
}
}