game/lib/widgets/battle/effect_sprite_widget.dart

174 lines
4.3 KiB
Dart

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
}
}