396 lines
11 KiB
Dart
396 lines
11 KiB
Dart
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<LineInkGuideGame> 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<LineInkGuideGame> 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<LineInkGuideGame> 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<Vector2> 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<Vector2> 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;
|
||
}
|