feat: add subweapon support, weapon type logic, and compact item card UI options
This commit is contained in:
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../providers/settings_provider.dart';
|
||||
import '../../game/enums.dart';
|
||||
import '../../game/config.dart';
|
||||
|
||||
class BattleAnimationWidget extends StatefulWidget {
|
||||
final Widget child;
|
||||
@@ -24,11 +25,11 @@ class BattleAnimationWidgetState extends State<BattleAnimationWidget>
|
||||
super.initState();
|
||||
_scaleController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 800),
|
||||
duration: AnimationConfig.attackRiskyScale,
|
||||
);
|
||||
_translateController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 1000),
|
||||
duration: AnimationConfig.attackRiskyDash,
|
||||
);
|
||||
|
||||
_scaleAnimation = Tween<double>(
|
||||
@@ -58,80 +59,69 @@ class BattleAnimationWidgetState extends State<BattleAnimationWidget>
|
||||
VoidCallback? onAnimationMiddle,
|
||||
VoidCallback? onAnimationEnd,
|
||||
}) async {
|
||||
// onAnimationStart?.call(); // Start Phase
|
||||
_resetControllers();
|
||||
onAnimationStart?.call();
|
||||
|
||||
if (risk == RiskLevel.safe || risk == RiskLevel.normal) {
|
||||
// Safe & Normal: Dash/Wobble without scale
|
||||
final isSafe = risk == RiskLevel.safe;
|
||||
final duration = isSafe ? 500 : 400;
|
||||
final duration = AnimationConfig.getAttackDuration(risk);
|
||||
final offsetFactor = isSafe ? 0.2 : 0.5;
|
||||
final curve = isSafe
|
||||
? AnimationConfig.attackSafeCurve
|
||||
: AnimationConfig.attackNormalCurve;
|
||||
|
||||
_translateController.duration = Duration(milliseconds: duration);
|
||||
_translateAnimation =
|
||||
Tween<Offset>(
|
||||
begin: Offset.zero,
|
||||
end: targetOffset * offsetFactor,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _translateController,
|
||||
curve: Curves.easeOutQuad,
|
||||
),
|
||||
);
|
||||
_translateController.duration = duration;
|
||||
_translateAnimation = Tween<Offset>(
|
||||
begin: Offset.zero,
|
||||
end: targetOffset * offsetFactor,
|
||||
).animate(CurvedAnimation(parent: _translateController, curve: curve));
|
||||
|
||||
await _translateController.forward();
|
||||
if (!mounted) return;
|
||||
|
||||
// onAnimationMiddle?.call(); // Middle Phase
|
||||
onAnimationEnd?.call();
|
||||
onImpact();
|
||||
|
||||
await _translateController.reverse();
|
||||
} else {
|
||||
onAnimationStart?.call(); // Start Phase
|
||||
// Risky: Scale + Heavy Dash
|
||||
final attackScale = context.read<SettingsProvider>().attackAnimScale;
|
||||
_scaleAnimation = Tween<double>(begin: 1.0, end: attackScale).animate(
|
||||
CurvedAnimation(parent: _scaleController, curve: Curves.easeOut),
|
||||
);
|
||||
|
||||
_scaleController.duration = const Duration(milliseconds: 600);
|
||||
_translateController.duration = const Duration(milliseconds: 500);
|
||||
_scaleController.duration = AnimationConfig.attackRiskyScale;
|
||||
_translateController.duration = AnimationConfig.attackRiskyDash;
|
||||
|
||||
// 1. Scale Up (Preparation)
|
||||
await _scaleController.forward();
|
||||
if (!mounted) return;
|
||||
|
||||
onAnimationMiddle?.call(); // Middle Phase
|
||||
onAnimationMiddle?.call();
|
||||
|
||||
// 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
|
||||
curve: AnimationConfig.attackRiskyDashCurve,
|
||||
),
|
||||
);
|
||||
|
||||
await _translateController.forward();
|
||||
if (!mounted) return;
|
||||
|
||||
// onAnimationEnd?.call(); // End Phase (Moved before Impact)
|
||||
|
||||
// 3. Impact Callback (Shake)
|
||||
onAnimationEnd?.call();
|
||||
onImpact();
|
||||
|
||||
// 4. Return (Reset)
|
||||
_scaleController.reverse();
|
||||
await _translateController.reverse();
|
||||
await Future.wait([
|
||||
_scaleController.reverse(),
|
||||
_translateController.reverse(),
|
||||
]);
|
||||
}
|
||||
|
||||
// onAnimationEnd removed from here
|
||||
}
|
||||
|
||||
Future<void> animateDefense(VoidCallback onImpact) async {
|
||||
// Defense: Wobble/Shake horizontally
|
||||
_resetControllers();
|
||||
_translateController.duration = const Duration(milliseconds: 800);
|
||||
|
||||
// Sequence: Left -> Right -> Center
|
||||
@@ -165,6 +155,17 @@ class BattleAnimationWidgetState extends State<BattleAnimationWidget>
|
||||
_translateController.reset();
|
||||
}
|
||||
|
||||
void _resetControllers() {
|
||||
if (_scaleController.isAnimating) {
|
||||
_scaleController.stop();
|
||||
}
|
||||
if (_translateController.isAnimating) {
|
||||
_translateController.stop();
|
||||
}
|
||||
_scaleController.reset();
|
||||
_translateController.reset();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
|
||||
@@ -84,7 +84,9 @@ class CharacterStatusCard extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
"${effect.type.name.toUpperCase()} (${effect.duration})",
|
||||
effect.stacks > 1
|
||||
? "${effect.type.name.toUpperCase()} x${effect.stacks} (${effect.duration})"
|
||||
: "${effect.type.name.toUpperCase()} (${effect.duration})",
|
||||
style: const TextStyle(
|
||||
color: ThemeConfig.effectText,
|
||||
fontSize: ThemeConfig.statusEffectFontSize,
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui' as ui;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class SpriteEffect {
|
||||
final Offset position;
|
||||
final String assetPath;
|
||||
final int frameCount;
|
||||
final double tileWidth;
|
||||
final double tileHeight;
|
||||
final double scale;
|
||||
|
||||
ui.Image? image;
|
||||
int currentFrame = 0;
|
||||
bool isFinished = false;
|
||||
|
||||
SpriteEffect({
|
||||
required this.position,
|
||||
required this.assetPath,
|
||||
required this.frameCount,
|
||||
this.tileWidth = 100.0,
|
||||
this.tileHeight = 100.0,
|
||||
this.scale = 2.0,
|
||||
});
|
||||
}
|
||||
|
||||
class EffectSpriteWidget extends StatefulWidget {
|
||||
const EffectSpriteWidget({super.key});
|
||||
|
||||
@override
|
||||
EffectSpriteWidgetState createState() => EffectSpriteWidgetState();
|
||||
}
|
||||
|
||||
class EffectSpriteWidgetState extends State<EffectSpriteWidget>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
final List<SpriteEffect> _effects = [];
|
||||
final Map<String, ui.Image> _imageCache = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Approximately 10 FPS (100ms per frame)
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 100),
|
||||
);
|
||||
_controller.addStatusListener((status) {
|
||||
if (status == AnimationStatus.completed) {
|
||||
_updateFrames();
|
||||
if (_effects.isNotEmpty) {
|
||||
_controller.forward(from: 0);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> playEffect({
|
||||
required Offset position,
|
||||
required String assetPath,
|
||||
required int frameCount,
|
||||
double tileWidth = 100.0,
|
||||
double tileHeight = 100.0,
|
||||
double scale = 2.0,
|
||||
}) async {
|
||||
final effect = SpriteEffect(
|
||||
position: position,
|
||||
assetPath: assetPath,
|
||||
frameCount: frameCount,
|
||||
tileWidth: tileWidth,
|
||||
tileHeight: tileHeight,
|
||||
scale: scale,
|
||||
);
|
||||
|
||||
// Preload image if not cached
|
||||
if (!_imageCache.containsKey(assetPath)) {
|
||||
try {
|
||||
final ByteData data = await rootBundle.load(assetPath);
|
||||
final List<int> bytes = data.buffer.asUint8List();
|
||||
final Completer<ui.Image> completer = Completer();
|
||||
ui.decodeImageFromList(Uint8List.fromList(bytes), (ui.Image img) {
|
||||
completer.complete(img);
|
||||
});
|
||||
_imageCache[assetPath] = await completer.future;
|
||||
} catch (e) {
|
||||
debugPrint('Failed to load effect image $assetPath: $e');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
effect.image = _imageCache[assetPath];
|
||||
|
||||
setState(() {
|
||||
_effects.add(effect);
|
||||
if (!_controller.isAnimating) {
|
||||
_controller.forward(from: 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _updateFrames() {
|
||||
if (_effects.isEmpty) return;
|
||||
|
||||
setState(() {
|
||||
for (var i = _effects.length - 1; i >= 0; i--) {
|
||||
final effect = _effects[i];
|
||||
effect.currentFrame++;
|
||||
if (effect.currentFrame >= effect.frameCount) {
|
||||
effect.isFinished = true;
|
||||
_effects.removeAt(i);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_effects.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
return IgnorePointer(
|
||||
child: CustomPaint(
|
||||
size: Size.infinite,
|
||||
painter: MultiSpriteEffectPainter(effects: _effects),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MultiSpriteEffectPainter extends CustomPainter {
|
||||
final List<SpriteEffect> effects;
|
||||
|
||||
MultiSpriteEffectPainter({required this.effects});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
for (final effect in effects) {
|
||||
if (effect.image == null) continue;
|
||||
|
||||
final double srcX = effect.currentFrame * effect.tileWidth;
|
||||
final double srcY = 0.0;
|
||||
final Rect src = Rect.fromLTWH(srcX, srcY, effect.tileWidth, effect.tileHeight);
|
||||
|
||||
final double drawWidth = effect.tileWidth * effect.scale;
|
||||
final double drawHeight = effect.tileHeight * effect.scale;
|
||||
// Center the effect on the position
|
||||
final Rect dst = Rect.fromLTWH(
|
||||
effect.position.dx - drawWidth / 2,
|
||||
effect.position.dy - drawHeight / 2,
|
||||
drawWidth,
|
||||
drawHeight,
|
||||
);
|
||||
|
||||
canvas.drawImageRect(
|
||||
effect.image!,
|
||||
src,
|
||||
dst,
|
||||
Paint()..filterQuality = FilterQuality.none,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant MultiSpriteEffectPainter oldDelegate) {
|
||||
return true; // Repaint constantly while animating
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,8 @@ class ExplosionWidgetState extends State<ExplosionWidget>
|
||||
final List<Particle> _particles = [];
|
||||
final Random _random = Random();
|
||||
|
||||
bool get isAnimating => _controller.isAnimating || _particles.isNotEmpty;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -127,7 +129,7 @@ class ExplosionPainter extends CustomPainter {
|
||||
void paint(Canvas canvas, Size size) {
|
||||
for (final p in particles) {
|
||||
final paint = Paint()
|
||||
..color = p.color.withOpacity(p.life.clamp(0.0, 1.0))
|
||||
..color = p.color.withValues(alpha: p.life.clamp(0.0, 1.0))
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
canvas.drawCircle(p.position, p.size, paint);
|
||||
|
||||
@@ -8,11 +8,11 @@ class FloatingDamageText extends StatefulWidget {
|
||||
final VoidCallback onRemove;
|
||||
|
||||
const FloatingDamageText({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.damage,
|
||||
required this.color,
|
||||
required this.onRemove,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
FloatingDamageTextState createState() => FloatingDamageTextState();
|
||||
@@ -111,12 +111,12 @@ class FloatingEffect extends StatefulWidget {
|
||||
final VoidCallback onRemove;
|
||||
|
||||
const FloatingEffect({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
required this.size,
|
||||
required this.onRemove,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
FloatingEffectState createState() => FloatingEffectState();
|
||||
@@ -193,11 +193,11 @@ class FloatingFeedbackText extends StatefulWidget {
|
||||
final VoidCallback onRemove;
|
||||
|
||||
const FloatingFeedbackText({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.feedback,
|
||||
required this.color,
|
||||
required this.onRemove,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
FloatingFeedbackTextState createState() => FloatingFeedbackTextState();
|
||||
|
||||
Reference in New Issue
Block a user