import 'dart:math' as math; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'app_theme.dart'; import 'levels.dart'; import 'managers.dart'; import 'game_engine.dart'; // --- Game Components --- class Ball extends BodyComponent with ContactCallbacks { final Vector2 start; Ball(this.start) : super(priority: 10); @override Body createBody() { final shape = CircleShape()..radius = 0.6; final body = world.createBody( BodyDef(type: BodyType.dynamic, position: start, bullet: true), )..createFixture( FixtureDef(shape, friction: 0.3, restitution: 0.4, density: 1.0), ); body.userData = this; // FIXED: Set userData on the body return body; } @override void render(Canvas canvas) { final skin = SkinManager.current; canvas.drawCircle(Offset.zero, 0.6, Paint()..color = skin.ballColor); canvas.drawCircle( const Offset(-0.15, -0.15), 0.15, Paint()..color = skin.eyeColor, ); } @override void update(double dt) { super.update(dt); // 如果小球掉出关卡范围,则通知游戏失败 final pos = body.position; if (game.simulating && (pos.y > 65 || pos.y < -5 || pos.x < -5 || pos.x > 25)) { game.onBallOutOfBounds(); } } } class Star extends BodyComponent with ContactCallbacks { final Vector2 pos; bool hit = false; Star(this.pos) : super(priority: 5); @override Body createBody() { final body = world.createBody(BodyDef(type: BodyType.static, position: pos)) ..createFixture(FixtureDef(CircleShape()..radius = 0.8, isSensor: true)); body.userData = this; // FIXED: Set userData on the body return body; } @override void beginContact(Object other, Contact contact) { if (other is Ball && !hit) { hit = true; game.collected++; game.starCountNotifier.value = game.collected; removeFromParent(); } } @override void render(Canvas canvas) { final paint = Paint()..color = AppTheme.accentYellow; final path = Path(); const int points = 5; const double innerRadius = 0.3; const double outerRadius = 0.7; const double step = math.pi / points; for (int i = 0; i < 2 * points; i++) { double radius = (i % 2 == 0) ? outerRadius : innerRadius; double angle = i * step - math.pi / 2; double x = math.cos(angle) * radius; double y = math.sin(angle) * radius; if (i == 0) path.moveTo(x, y); else path.lineTo(x, y); } path.close(); canvas.drawPath(path, paint); } } class Goal extends BodyComponent with ContactCallbacks { final Vector2 pos; Goal(this.pos) : super(priority: 4); @override Body createBody() { final body = world.createBody( BodyDef(type: BodyType.static, position: pos), ); // 创建物理碰撞体,让小球可以停在上面 body.createFixture( FixtureDef( PolygonShape()..setAsBoxXY(2.4, 0.6), friction: 0.6, restitution: 0.3, ), ); // 创建一个sensor用于检测小球接触(用于通关判断) body.createFixture( FixtureDef(PolygonShape()..setAsBoxXY(2.4, 0.6), isSensor: true), ); body.userData = this; // FIXED: Set userData on the body return body; } @override void beginContact(Object other, Contact contact) { if (other is! Ball) return; if (game.collected >= game.config.stars.length && game.winNotifier.value == null) { final r = game.inkNotifier.value / game.config.ink; final s = (r > 0.9) ? 5 : (r > 0.7) ? 4 : (r > 0.5) ? 3 : (r > 0.25) ? 2 : 1; final clearTime = game.currentRunDuration; final manager = LevelManager.instance; manager.saveProgress(game.config.id, s, levels.length); final unlocked = manager.unlockAchievementsForWin( levelId: game.config.id, stars: s, clearTime: clearTime, ); if (unlocked.isNotEmpty) { game.notifyAchievement(unlocked.first); } game.simulating = false; game.winNotifier.value = s; } } @override void render(Canvas canvas) { bool ready = game.starCountNotifier.value >= game.config.stars.length; // 绘制一个更美观的篮子样式 final baseColor = ready ? AppTheme.accentGreen : AppTheme.gridLine; final highlightColor = ready ? Color.lerp(AppTheme.accentGreen, Colors.white, 0.3)! : AppTheme.gridLine.withOpacity(0.5); // 篮子主体(带圆角的矩形) final basketRect = Rect.fromCenter( center: Offset.zero, width: 4.8, height: 1.2, ); final basketPaint = Paint() ..color = baseColor ..style = PaintingStyle.fill; canvas.drawRRect( RRect.fromRectAndRadius(basketRect, const Radius.circular(0.3)), basketPaint, ); // 篮子边框 final borderPaint = Paint() ..color = baseColor.withOpacity(0.8) ..style = PaintingStyle.stroke ..strokeWidth = 0.15; canvas.drawRRect( RRect.fromRectAndRadius(basketRect, const Radius.circular(0.3)), borderPaint, ); // 篮子内部线条(模拟篮子纹理) final innerPaint = Paint() ..color = highlightColor ..style = PaintingStyle.stroke ..strokeWidth = 0.08; canvas.drawLine( const Offset(-2.1, -0.3), const Offset(2.1, -0.3), innerPaint, ); canvas.drawLine(const Offset(-2.1, 0), const Offset(2.1, 0), innerPaint); canvas.drawLine( const Offset(-2.1, 0.3), const Offset(2.1, 0.3), innerPaint, ); // 如果未准备好,添加一个锁的图标 if (!ready) { final lockPaint = Paint() ..color = AppTheme.gridLine ..style = PaintingStyle.stroke ..strokeWidth = 0.12; // 简单的锁图标 canvas.drawRect( Rect.fromCenter(center: const Offset(0, 0.1), width: 0.4, height: 0.3), lockPaint, ); canvas.drawCircle(const Offset(0, -0.05), 0.15, lockPaint); } } } class UserLine extends BodyComponent { final List pts; UserLine(this.pts) : super(priority: 8); @override Body createBody() { final body = world.createBody(BodyDef(type: BodyType.static)); for (int i = 0; i < pts.length - 1; i++) { body.createFixture( FixtureDef(EdgeShape()..set(pts[i], pts[i + 1]), friction: 0.6), ); } return body; } @override void render(Canvas canvas) { final skin = SkinManager.current; final p = Paint() ..color = skin.inkColor ..strokeWidth = 0.25 ..strokeCap = StrokeCap.round ..style = PaintingStyle.stroke; final path = Path()..moveTo(pts.first.x, pts.first.y); for (var pt in pts) path.lineTo(pt.x, pt.y); canvas.drawPath(path, p); } } class Block extends BodyComponent { final BlockShape shape; Block(this.shape) : super(priority: 3); Block.fromRect(Rect r) : shape = BlockShape.fromRect(r), super(priority: 3); @override Body createBody() { final body = world.createBody( BodyDef( type: BodyType.static, position: shape.position, angle: shape.rotation, ), ); switch (shape.type) { case BlockShapeType.rectangle: final s = PolygonShape() ..setAsBox( shape.width / 2, shape.height / 2, Vector2.zero(), 0, // 旋转已经在 body 角度中处理 ); body.createFixture(FixtureDef(s, friction: 0.6, restitution: 0.3)); break; case BlockShapeType.triangle: // 创建三角形:顶点向上 final vertices = [ Vector2(0, -shape.height / 2), // 顶部 Vector2(-shape.width / 2, shape.height / 2), // 左下 Vector2(shape.width / 2, shape.height / 2), // 右下 ]; final s = PolygonShape()..set(vertices); body.createFixture(FixtureDef(s, friction: 0.6, restitution: 0.3)); break; case BlockShapeType.circle: if (shape.radius == null) break; final s = CircleShape()..radius = shape.radius!; body.createFixture(FixtureDef(s, friction: 0.6, restitution: 0.3)); break; } return body; } @override void render(Canvas canvas) { // BodyComponent 的 render 方法会自动将 canvas 变换到 body 的位置和角度 // 所以直接使用 Offset.zero 作为中心点即可 // 填充颜色 final fillPaint = Paint() ..color = AppTheme.obstacleColor ..style = PaintingStyle.fill; // 边框颜色(稍深一点) final borderPaint = Paint() ..color = AppTheme.obstacleColor.withOpacity(0.8) ..style = PaintingStyle.stroke ..strokeWidth = 0.1; switch (shape.type) { case BlockShapeType.rectangle: final rect = Rect.fromCenter( center: Offset.zero, width: shape.width, height: shape.height, ); canvas.drawRect(rect, fillPaint); canvas.drawRect(rect, borderPaint); break; case BlockShapeType.triangle: final path = Path() ..moveTo(0, -shape.height / 2) ..lineTo(-shape.width / 2, shape.height / 2) ..lineTo(shape.width / 2, shape.height / 2) ..close(); canvas.drawPath(path, fillPaint); canvas.drawPath(path, borderPaint); break; case BlockShapeType.circle: if (shape.radius == null) break; canvas.drawCircle(Offset.zero, shape.radius!, fillPaint); canvas.drawCircle(Offset.zero, shape.radius!, borderPaint); break; } } } class Boundary extends BodyComponent { final Vector2 a, b; Boundary(this.a, this.b); @override Body createBody() => world.createBody(BodyDef(type: BodyType.static)) ..createFixture(FixtureDef(EdgeShape()..set(a, b))); } class DrawingPreview extends Component { final List pts; DrawingPreview(this.pts); @override void render(Canvas canvas) { if (pts.length < 2) return; final skin = SkinManager.current; final p = Paint() ..color = skin.inkColor.withOpacity(0.3) ..strokeWidth = 0.2 ..style = PaintingStyle.stroke; final path = Path()..moveTo(pts.first.x, pts.first.y); for (var pt in pts) path.lineTo(pt.x, pt.y); canvas.drawPath(path, p); } } class BackgroundGrid extends Component { @override void render(Canvas canvas) { final gridPaint = Paint() ..color = AppTheme.gridLine ..strokeWidth = 0.05; for (double i = 0; i <= 20; i += 2) canvas.drawLine(Offset(i, 0), Offset(i, 60), gridPaint); for (double i = 0; i <= 60; i += 2) canvas.drawLine(Offset(0, i), Offset(20, i), gridPaint); } } class GridPainter extends CustomPainter { @override void paint(Canvas canvas, Size s) { final p = Paint() ..color = AppTheme.gridLine ..strokeWidth = 1; for (double i = 0; i < s.width; i += 32) canvas.drawLine(Offset(i, 0), Offset(i, s.height), p); for (double i = 0; i < s.height; i += 32) canvas.drawLine(Offset(0, i), Offset(s.width, i), p); } @override bool shouldRepaint(covariant CustomPainter old) => false; }