Files
kidgo/app/lib/src/game/sprites.dart
T

449 lines
17 KiB
Dart

// 程序化像素绘制(无外部图片资源)。对应 web 版 sprites.js。
// 全部使用 Flutter dart:ui 的 Canvas / Paint / Path,逐函数镜像。
import 'dart:ui' show Canvas, Paint, Path, Rect, Color, Offset, PaintingStyle, StrokeCap, TextAlign;
import 'dart:math' show sin, cos, pi, max, min, sqrt, atan2;
import 'package:flutter/widgets.dart';
import 'package:iwbtc/src/core/util.dart';
import 'package:iwbtc/src/game/views.dart';
Color rgba(int r, int g, int b, double a) =>
Color.fromARGB((a * 255).round().clamp(0, 255), r, g, b);
class C {
static const Color stone = Color(0xff6b6b7e);
static const Color stoneDark = Color(0xff3a3a48);
static const Color stoneLite = Color(0xff8f8fa6);
static const Color grass = Color(0xff4f9e46);
static const Color grassDark = Color(0xff2c5c2b);
static const Color brick = Color(0xffa8543c);
static const Color brickDark = Color(0xff632a1d);
static const Color metal = Color(0xff5a6478);
static const Color metalDark = Color(0xff333a4a);
static const Color ice = Color(0xff8fd8f0);
static const Color iceDark = Color(0xff4a90b8);
static const Color water = Color(0xff2a6fd8);
static const Color spike = Color(0xffcfdde9);
static const Color spikeDark = Color(0xffa8bccf);
static const Color kid = Color(0xffd94a55);
static const Color kidDark = Color(0xff8e2530);
static const Color skin = Color(0xfff5cfa8);
static const Color hair = Color(0xff5a3a22);
static const Color pant = Color(0xff2c3a7a);
static const Color save = Color(0xffb06bff);
static const Color goal = Color(0xffffc63a);
static const Color gun = Color(0xffb8b8c8);
static const Color apple = Color(0xffe03050);
}
void px(Canvas c, double x, double y, double w, double h, Color col) {
final p = Paint()..color = col;
c.drawRect(Rect.fromLTWH(x, y, w, h), p);
}
// ---------------- 地形 ----------------
void drawBlock(Canvas c, double x, double y, int variant, [bool lit = false]) {
const s = TILE;
Color base = C.stone, dark = C.stoneDark, lite = C.stoneLite;
if (variant == 1) {
base = C.grass; dark = C.grassDark; lite = const Color(0xff7fd06a);
} else if (variant == 2) {
base = C.brick; dark = C.brickDark; lite = const Color(0xffd1785c);
} else if (variant == 3) {
base = C.metal; dark = C.metalDark; lite = const Color(0xff8a94ad);
}
px(c, x, y, s, s, dark);
px(c, x + 1, y + 1, s - 2, s - 2, base);
if (variant == 2) {
px(c, x + 1, y + 8, s - 2, 1, dark);
px(c, x + 1, y + 17, s - 2, 1, dark);
px(c, x + 10, y + 1, 1, 7, dark);
px(c, x + 20, y + 9, 1, 8, dark);
px(c, x + 6, y + 18, 1, 7, dark);
} else if (variant == 1) {
px(c, x + 1, y + 1, s - 2, 5, lite);
for (int i = 0; i < 4; i++) px(c, x + 3 + i * 7, y + 2, 1, 4, dark);
} else {
final n = hashNoise(x.round(), y.round());
px(c, x + 2, y + 2, 6, 2, lite);
if (n > 0.5) px(c, x + 18, y + 12, 8, 2, dark);
if (n < 0.35) px(c, x + 6, y + 22, 7, 2, dark);
}
px(c, x, y, s, 1, lite);
px(c, x, y, 1, s, lite);
px(c, x, y + s - 1, s, 1, dark);
px(c, x + s - 1, y, 1, s, dark);
if (lit) px(c, x, y, s, s, rgba(255, 255, 255, 0.09));
}
void drawIce(Canvas c, double x, double y) {
const s = TILE;
px(c, x, y, s, s, C.iceDark);
px(c, x + 1, y + 1, s - 2, s - 2, C.ice);
px(c, x + 2, y + 3, 10, 3, const Color(0xffe6fbff));
px(c, x + 18, y + 16, 8, 2, rgba(255, 255, 255, 0.7));
px(c, x, y, s, 1, const Color(0xffe6fbff));
px(c, x, y + s - 1, s, 1, C.iceDark);
}
void drawWater(Canvas c, double x, double y, int t) {
const s = TILE;
px(c, x, y, s, s, rgba(40, 110, 215, 0.55));
final off = sin(t / 260 + x * 0.09) * 3;
px(c, x, y + 4 + off, s, 3, rgba(120, 200, 255, 0.35));
px(c, x, y + 16 - off, s, 2, rgba(120, 200, 255, 0.35));
}
void drawOneWay(Canvas c, double x, double y) {
const s = TILE;
px(c, x, y, s, 6, C.metalDark);
px(c, x, y, s, 3, const Color(0xff9aa6bd));
for (int i = 0; i < 8; i++) px(c, x + i * 4, y + 6, 2, 3, C.metalDark);
}
void drawSpike(Canvas c, double x, double y, int dir, int t) {
const s = TILE;
late double ax, ay, bx, by, cx2, cy2;
if (dir == 0) {
ax = x + s / 2; ay = y; bx = x; by = y + s; cx2 = x + s; cy2 = y + s;
} else if (dir == 1) {
ax = x + s / 2; ay = y + s; bx = x; by = y; cx2 = x + s; cy2 = y;
} else if (dir == 2) {
ax = x; ay = y + s / 2; bx = x + s; by = y; cx2 = x + s; cy2 = y + s;
} else {
ax = x + s; ay = y + s / 2; bx = x; by = y; cx2 = x; cy2 = y + s;
}
late Offset from, to;
if (dir == 0) { from = Offset(x, y); to = Offset(x, y + s); }
else if (dir == 1) { from = Offset(x, y + s); to = Offset(x, y); }
else if (dir == 2) { from = Offset(x, y); to = Offset(x + s, y); }
else { from = Offset(x + s, y); to = Offset(x, y); }
final rect = Rect.fromLTWH(x, y, s, s);
final g = LinearGradient(
begin: _gradAlign(from, rect), end: _gradAlign(to, rect),
colors: const [Color(0xfff4f9fd), Color(0xffcfdde9), Color(0xffa8bccf)],
);
final p = Path();
p.moveTo(ax, ay); p.lineTo(bx, by); p.lineTo(cx2, cy2); p.close();
final paint = Paint()..shader = g.createShader(rect);
c.drawPath(p, paint);
final stroke = Paint()
..color = rgba(150, 175, 200, 0.9)
..style = PaintingStyle.stroke
..strokeWidth = 1;
c.drawPath(p, stroke);
}
void drawKiller(Canvas c, double x, double y, int t) {
const s = TILE;
final bob = sin(t / 300 + x) * 1.5;
c.save();
c.translate(x + s / 2, y + s / 2 + bob);
final p = Paint()..color = C.apple;
c.drawCircle(Offset(0, 2), 11, p);
final p2 = Paint()..color = const Color(0xffff7a90);
c.drawCircle(Offset(-4, -2), 4, p2);
px(c, -1, -13, 2, 6, const Color(0xff6b4423));
final leaf = Paint()..color = const Color(0xff3ddc84);
c.drawOval(Rect.fromLTWH(1, -13, 10, 5.2), leaf);
px(c, -5, 0, 2, 2, const Color(0xff000000));
px(c, 3, 0, 2, 2, const Color(0xff000000));
c.restore();
}
void drawVanish(Canvas c, double x, double y, int state, int t) {
const s = TILE;
final alpha = state == 0 ? 1.0 : state == 1 ? (sin(t / 60) > 0 ? 0.55 : 0.9) : 0.14;
drawBlock(c, x, y, 3);
if (alpha < 1) {
// 模拟 globalAlpha:用半透明黑覆盖来压暗
px(c, x, y, s, s, Color.fromARGB(((1 - alpha) * 255).round().clamp(0, 255), 0, 0, 0));
}
if (state != 2) {
final sp = Paint()
..color = rgba(255, 200, 60, 0.55)
..style = PaintingStyle.stroke
..strokeWidth = 1;
c.drawRect(Rect.fromLTWH(x + 2.5, y + 2.5, s - 5, s - 5), sp);
}
}
void drawJumpRef(Canvas c, double x, double y, int t) {
const s = TILE;
final b = sin(t / 200 + x) * 2;
final p = Path();
p.moveTo(x + s / 2, y + 8 + b);
p.lineTo(x + s - 6, y + s - 10 + b);
p.lineTo(x + 6, y + s - 10 + b);
p.close();
c.drawPath(p, Paint()..color = const Color(0xff3ddc84));
final sp = Paint()
..color = const Color(0xff0d5c30)
..style = PaintingStyle.stroke
..strokeWidth = 1;
c.drawPath(p, sp);
}
// ---------------- 玩家 ----------------
void drawKid(Canvas c, PlayerView p, int t) {
final x = p.x.roundToDouble(), y = p.y.roundToDouble();
final w = p.w, h = p.h;
final air = !p.grounded;
c.save();
c.translate(x, y);
// 身体(红衣)
px(c, 2, 7, w - 4, 7, C.kid);
px(c, 2, 7, w - 4, 1, shade(C.kid, 40));
if (air) {
px(c, p.facing > 0 ? w - 3 : 0, 5, 3, 5, C.skin);
} else {
px(c, p.facing > 0 ? w - 3 : 0, 9, 3, 5, C.skin);
}
if (air) {
px(c, 3, 14, 4, 4, C.pant);
px(c, w - 7, 14, 4, 3, C.pant);
} else {
px(c, 2, 14, 4, 4, C.pant);
px(c, w - 6, 14, 4, 4, C.pant);
}
px(c, 1, 0, w - 2, 8, C.skin);
px(c, 1, 0, w - 2, 3, C.hair);
px(c, p.facing > 0 ? w - 5 : 3, 1, 3, 2, C.hair);
px(c, p.facing > 0 ? w - 6 : 3, 4, 2, 2, const Color(0xff101018));
px(c, p.facing > 0 ? w - 3 : 0, 4, 1, 2, const Color(0xff101018));
c.restore();
if (p.hasGun && !p.dead) {
final gx = p.facing > 0 ? x + w - 1 : x - 7;
final gy = y + 9;
px(c, gx, gy, 8, 3, C.gun);
px(c, p.facing > 0 ? gx + 6 : gx, gy + 1, 3, 2, const Color(0xff7a7a8c));
px(c, gx + 1, gy + 3, 3, 3, const Color(0xff6a6a7c));
}
}
// ---------------- 实体 ----------------
void drawSave(Canvas c, EntityView e, int t) {
final cx = e.x + e.w / 2, cy = e.y + e.h / 2;
final on = e.active;
final pulse = 1 + sin(t / 180) * 0.09;
if (on) {
final p = Paint()..color = rgba(176, 107, 255, 0.28);
c.drawCircle(Offset(cx, cy), 15 * pulse, p);
} else {
final r = 13 + sin(t / 26) * 2.2;
final sp = Paint()
..color = rgba(138, 106, 184, 0.30 + sin(t / 26) * 0.12)
..style = PaintingStyle.stroke
..strokeWidth = 1.5;
c.drawCircle(Offset(cx, cy), r, sp);
}
final col = on ? C.save : const Color(0xff8f74b5);
final p = Paint()..color = col;
c.drawCircle(Offset(cx - 3, cy - 1), 4.2, p);
c.drawCircle(Offset(cx + 4, cy + 1), 4.2, p);
c.drawCircle(Offset(cx, cy + 5), 4.2, p);
c.drawCircle(Offset(cx + 1, cy - 5), 4.2, p);
if (on) {
c.drawCircle(Offset(cx - 1, cy - 2), 1.6, Paint()..color = const Color(0xffe5d0ff));
}
px(c, cx + 1, cy - 9, 2, 4, const Color(0xff4a8a3a));
}
void drawGoal(Canvas c, EntityView e, int t) {
final x = e.x, y = e.y, w = e.w, h = e.h;
px(c, x + 3, y + 2, w - 6, h - 2, const Color(0xff2a1e3a));
px(c, x + 5, y + 4, w - 10, h - 6, const Color(0xff140d20));
final gr = Rect.fromLTWH(x + 5, y + 4, w - 10, h - 6);
final g = LinearGradient(
begin: _gradAlign(Offset(x, y + h), gr), end: _gradAlign(Offset(x, y), gr),
colors: [rgba(255, 200, 60, 0.55), rgba(255, 200, 60, 0)],
);
c.drawRect(gr, Paint()..shader = g.createShader(gr));
for (int i = 0; i < 3; i++) {
final pp = (((t / 700) + i / 3) % 1);
px(c, x + 7 + ((i * 9) % (w - 12)), y + h - 4 - pp * (h - 8), 2, 2, C.goal);
}
px(c, x + 3, y + 2, w - 6, 2, C.goal);
px(c, x + 3, y + 2, 2, h - 2, const Color(0xffc99a1e));
px(c, x + w - 5, y + 2, 2, h - 2, const Color(0xffc99a1e));
}
void drawGun(Canvas c, EntityView e, int t) {
final x = e.x, y = e.y + sin(t / 250) * 2;
px(c, x + 2, y + 6, 20, 7, const Color(0xff4a4a5c));
px(c, x + 2, y + 6, 20, 2, const Color(0xff8a8aa0));
px(c, x + 18, y + 8, 9, 3, C.gun);
px(c, x + 6, y + 13, 5, 8, const Color(0xff3a3a4c));
px(c, x + 8, y + 19, 3, 3, const Color(0xff2a2a3a));
px(c, x + 24, y + 7, 2, 5, const Color(0xffd0d0e0));
}
void drawApple(Canvas c, EntityView e, int t) {
final cx = e.x + e.w / 2, cy = e.y + e.h / 2 + sin(t / 320 + e.x) * 1.5;
c.drawCircle(Offset(cx, cy), 8, Paint()..color = C.apple);
c.drawCircle(Offset(cx - 3, cy - 3), 3, Paint()..color = const Color(0xffff8095));
px(c, cx - 1, cy - 11, 2, 4, const Color(0xff6b4423));
c.drawOval(Rect.fromLTWH(cx + 4 - 4, cy - 9 - 2, 8, 4), Paint()..color = const Color(0xff3ddc84));
}
void drawWarp(Canvas c, EntityView e, int t) {
final cx = e.x + e.w / 2, cy = e.y + e.h / 2;
for (int i = 0; i < 3; i++) {
final r = 14 - i * 4;
final a = t / (300 + i * 120);
final sp = Paint()
..color = Color.fromARGB(((0.85 - i * 0.2) * 255).round().clamp(0, 255), (170 - i * 30).clamp(0, 255), (90 + i * 40).clamp(0, 255), 255)
..style = PaintingStyle.stroke
..strokeWidth = 2;
c.drawArc(Rect.fromLTWH(cx - r, cy - r, r * 2, r * 2), a, pi * 1.4, false, sp);
}
c.drawCircle(Offset(cx, cy), 5, Paint()..color = rgba(120, 60, 200, 0.35));
}
void drawCamera(Canvas c, EntityView e, int t) {
final x = e.x, y = e.y, w = e.w, h = e.h;
_dashedRect(c, Rect.fromLTWH(x + 1, y + 1, w - 2, h - 2),
rgba(80, 220, 255, 0.35 + sin(t / 300) * 0.15), 1, 4, 4);
final ix = x + w / 2 - 9, iy = y + 6;
px(c, ix, iy + 3, 18, 11, const Color(0xff1e1e2a));
px(c, ix + 2, iy + 5, 14, 7, const Color(0xff3a3a4c));
c.drawCircle(Offset(ix + 9, iy + 8.5), 3.5, Paint()..color = const Color(0xff7ae0ff));
px(c, ix + 12, iy, 5, 3, const Color(0xff1e1e2a));
px(c, ix + 13, iy - 1, 3, 2, const Color(0xffff4060));
}
void drawSign(Canvas c, EntityView e, int t) {
final x = e.x, y = e.y, w = e.w, h = e.h;
px(c, x + w / 2 - 1, y + h - 8, 3, 8, const Color(0xff6b4423));
px(c, x, y + 2, w, h - 10, const Color(0xffc9a86b));
px(c, x + 2, y + 4, w - 4, h - 14, const Color(0xffe8d5a8));
px(c, x + 4, y + 8, w - 8, 2, const Color(0xff8a7550));
px(c, x + 4, y + 13, w - 12, 2, const Color(0xff8a7550));
}
void drawTurret(Canvas c, EntityView e, int t) {
final x = e.x, y = e.y;
const s = TILE;
px(c, x + 2, y + 6, s - 4, s - 6, const Color(0xff4a4a5e));
px(c, x + 4, y + 8, s - 8, s - 12, const Color(0xff6a6a80));
final dirx = e.dir == 'left' ? -1 : e.dir == 'right' ? 1 : 0;
final diry = e.dir == 'up' ? -1 : e.dir == 'down' ? 1 : 0;
final cx = x + s / 2, cy = y + s / 2;
px(c, cx + dirx * 8 - 3, cy + diry * 8 - 3, 7, 7, const Color(0xff2a2a38));
final blink = (t ~/ 300) % 3 == 0;
px(c, cx + dirx * 12 - 2, cy + diry * 12 - 2, 4, 4, blink ? const Color(0xffff4060) : const Color(0xff8a2030));
}
void drawMovingPlatform(Canvas c, MoverView m) {
final x = m.x, y = m.y, w = m.w, h = m.h;
px(c, x, y, w, h, const Color(0xff3a3a4c));
px(c, x + 1, y + 1, w - 2, h - 2, const Color(0xff6a6a84));
px(c, x + 1, y + 1, w - 2, 2, const Color(0xff9a9ab4));
for (double i = 0; i < w; i += 8) px(c, x + i + 2, y + h - 4, 4, 2, const Color(0xff3a3a4c));
_dashedLine(c, Offset(m.ax + m.aw / 2, m.ay + m.ah / 2), Offset(m.bx + m.aw / 2, m.by + m.ah / 2),
rgba(255, 255, 255, 0.10), 1, 3, 5);
}
void drawDropSpike(Canvas c, EntityView e, int t) {
const s = TILE;
if (!e.falling) {
px(c, e.x + 4, e.y + 4, s - 8, s - 8, rgba(74, 74, 92, 0.85));
px(c, e.x + 6, e.y + 6, s - 12, s - 12, rgba(106, 106, 128, 0.85));
} else {
drawSpike(c, e.x, e.y, 2, t);
}
}
void drawBullet(Canvas c, BulletView b) {
px(c, b.x - 1, b.y - 1, b.w + 2, b.h + 2, rgba(255, 240, 120, 0.35));
px(c, b.x, b.y, b.w, b.h, const Color(0xfffff6a0));
px(c, b.x, b.y + b.h - 2, b.w, 2, const Color(0xffffb040));
}
void drawEBullet(Canvas c, BulletView b, int t) {
final cx = b.x + b.w / 2, cy = b.y + b.h / 2;
c.drawCircle(Offset(cx, cy), b.w / 2 + 1, Paint()..color = hex(b.col));
c.drawCircle(Offset(cx - 1.5, cy - 1.5), b.w / 4, Paint()..color = const Color(0xffffffff));
if (b.big) {
final sp = Paint()
..color = rgba(255, 255, 255, 0.4)
..style = PaintingStyle.stroke;
c.drawCircle(Offset(cx, cy), b.w / 2 + 4 + sin(t / 90) * 2, sp);
}
}
// ---------------- 死亡特效 ----------------
void drawDeathRing(Canvas c, DeathFX fx) {
final t = fx.t;
if (t > 14) return;
final r = 4 + t * 1.8;
final a = max(0.0, 1 - t / 14);
final sp = Paint()
..color = rgba(255, 143, 163, a * 0.85)
..style = PaintingStyle.stroke
..strokeWidth = 2;
c.drawCircle(Offset(fx.x, fx.y), r, sp);
c.drawCircle(Offset(fx.x, fx.y), r * 0.55, Paint()..color = rgba(255, 92, 122, a * 0.3));
}
void drawDeathMark(Canvas c, DeathMark dm) {
const s = 4.0;
final sp = Paint()
..color = rgba(0, 0, 0, 0.55)
..style = PaintingStyle.stroke
..strokeWidth = 3
..strokeCap = StrokeCap.round;
_line(c, sp, dm.x - s, dm.y - s, dm.x + s, dm.y + s);
_line(c, sp, dm.x + s, dm.y - s, dm.x - s, dm.y + s);
final sp2 = Paint()
..color = const Color(0xffff2535)
..style = PaintingStyle.stroke
..strokeWidth = 2
..strokeCap = StrokeCap.round;
_line(c, sp2, dm.x - s, dm.y - s, dm.x + s, dm.y + s);
_line(c, sp2, dm.x + s, dm.y - s, dm.x - s, dm.y + s);
}
// ---------------- 文本 ----------------
void drawText(Canvas c, String text, double x, double y,
{Color color = const Color(0xffffffff), double size = 11, TextAlign align = TextAlign.center}) {
final tp = TextPainter(
text: TextSpan(text: text, style: TextStyle(color: color, fontSize: size, fontFamily: 'Courier')),
textAlign: align,
textDirection: TextDirection.ltr,
);
tp.layout();
tp.paint(c, Offset(x - (align == TextAlign.center ? tp.width / 2 : 0), y));
}
// ---------------- 虚线辅助 ----------------
void _dashedLine(Canvas c, Offset a, Offset b, Color col, double width, double dash, double gap) {
final p = Paint()..color = col..strokeWidth = width;
final dx = b.dx - a.dx, dy = b.dy - a.dy;
final dist = sqrt(dx * dx + dy * dy);
if (dist == 0) return;
final ux = dx / dist, uy = dy / dist;
double d = 0;
while (d < dist) {
final d2 = min(d + dash, dist);
c.drawLine(Offset(a.dx + ux * d, a.dy + uy * d), Offset(a.dx + ux * d2, a.dy + uy * d2), p);
d += dash + gap;
}
}
void _dashedRect(Canvas c, Rect r, Color col, double width, double dash, double gap) {
_dashedLine(c, r.topLeft, r.topRight, col, width, dash, gap);
_dashedLine(c, r.topRight, r.bottomRight, col, width, dash, gap);
_dashedLine(c, r.bottomRight, r.bottomLeft, col, width, dash, gap);
_dashedLine(c, r.bottomLeft, r.topLeft, col, width, dash, gap);
}
void _line(Canvas c, Paint p, double x1, double y1, double x2, double y2) {
c.drawLine(Offset(x1, y1), Offset(x2, y2), p);
}
// 把像素空间的绝对 Offset 换算成 Flutter LinearGradient 所需的 Alignment。
Alignment _gradAlign(Offset abs, Rect r) =>
Alignment(((abs.dx - r.left) - r.width / 2) / (r.width / 2),
((abs.dy - r.top) - r.height / 2) / (r.height / 2));