LineInkGuide/lib/game_components.dart

396 lines
11 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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