fix: 修复迁移后的基础UI显示问题

This commit is contained in:
cancerai committed 2026-10-25 16:03:05 +08:00
1 parent 18017f819a
commit a93e06f60c
68 files changed
+2293 -8404

No files matched your search

+8 -2
View File
@@ -59,7 +59,12 @@ class Entity implements EntityView, CamZoneLike {
double speed = 2.4;
int timer = 0;
String kind = 'ball';
String? doorId;
// 与 web 原版一致:doorId 既可来自 data.doors[].id(常为 String),
// 也可在无匹配门时回退到实体序号 idx(int)。JS 弱类型下二者都能用 === 比较;
// 移植到 Dart 后若声明为 String?,回退分支把 int 赋给 String? 会抛
// "type 'int' is not a subtype of type 'String?'"。故用 Object? 容纳两种类型,
// == 比较(world.dart)对 String/String、int/int 走值相等,语义与 web 一致。
Object? doorId;
int pair = 0;
dynamic to;
int cool = 0;
@@ -252,7 +257,8 @@ class Level implements LevelLike {
}
if (type == 'warp') {
final cfg = (idx < warpSrc.length ? warpSrc[idx] : {}) as Map? ?? {};
final door = (data['doors'] as List?)?.cast<Map>()
final doors = (data['doors'] as List?)?.cast<Map>() ?? const <Map>[];
final door = doors
.firstWhere((d) => d['tx'] == tx && d['ty'] == ty, orElse: () => <String, dynamic>{});
e.doorId = (door != null && door.isNotEmpty) ? door['id'] : idx;
e.pair = cfg['pair'] ?? (idx % 2 == 0 ? idx + 1 : idx - 1);
+82 -25
View File
@@ -1,10 +1,18 @@
// 游戏循环 / 状态机(对应 web 版 main.js)。
// 用固定 60Hz 时间步长驱动已移植的纯 Canvas 游戏逻辑,保证手感 1:1。
// 渲染在 800×608 虚拟分辨率下完成,由 GameCanvas 做信箱(letterbox)缩放。
import 'dart:ui' show Canvas, Color, Paint, Path, Rect, Offset, TextAlign, TextDirection;
// 渲染在虚拟视口下完成(≥800×608、随屏幕比例加宽),由 GameCanvas 铺满全屏。
import 'dart:ui'
show Canvas, Color, Paint, Path, Rect, Offset, TextAlign, TextDirection;
import 'dart:math' show sin;
import 'package:flutter/widgets.dart'
show TextPainter, TextSpan, TextStyle, LinearGradient, Alignment, FontWeight, ValueNotifier;
show
TextPainter,
TextSpan,
TextStyle,
LinearGradient,
Alignment,
FontWeight,
ValueNotifier;
import 'package:iwbtc/src/core/input.dart';
import 'package:iwbtc/src/core/save.dart';
import 'package:iwbtc/src/core/util.dart';
@@ -32,8 +40,10 @@ abstract class GameHost {
void onBossBar(dynamic boss);
void onToast(String text);
void onResult(Map<String, dynamic> r);
/// 暂停菜单「退出」按钮文案随来源变化。
void onPauseMenu(String label);
/// 进入编辑器界面。
void editorShow();
void touchShow();
@@ -75,10 +85,29 @@ class World implements LevelTestRunner {
double _acc = 0;
String _lastToast = '';
/// 当前虚拟视口(GameCanvas 每帧 paint 前同步)。
/// 保证 ≥ 800×608;屏幕更宽/更高时另一维加宽,画面铺满全屏无黑边。
double viewW = W;
double viewH = H;
World(this.host);
/// 由 GameCanvas 在 paint 前调用:视口随屏幕比例变化。
/// 只改纯数值字段,不触发任何通知(paint 阶段禁止 notifier)。
void setViewport(double vw, double vh) {
if (vw == viewW && vh == viewH) return;
viewW = vw;
viewH = vh;
cam?.w = vw;
cam?.h = vh;
}
// ---------------- 关卡流程 ----------------
Map<String, dynamic> faceData(Map<String, dynamic> map, Map<String, dynamic> f, int i) {
Map<String, dynamic> faceData(
Map<String, dynamic> map,
Map<String, dynamic> f,
int i,
) {
final grid = (f['grid'] as List?)?.cast<String>() ?? <String>[];
return <String, dynamic>{
'id': (map['id'] ?? 'map').toString() + '-' + i.toString(),
@@ -115,7 +144,7 @@ class World implements LevelTestRunner {
level = map![face];
player = Player(level!);
cam = Camera(W, H);
cam = Camera(viewW, viewH);
cam!.reset(level!, player!);
player!.onDeath = () {
@@ -194,8 +223,14 @@ class World implements LevelTestRunner {
}
cam!.reset(level!, player!);
Sfx.instance.warp();
level!.particles.spawn(player!.cx, player!.cy, 12,
colors: const ['#a86bff', '#fff'], speed: 2.5, life: 24);
level!.particles.spawn(
player!.cx,
player!.cy,
12,
colors: const ['#a86bff', '#fff'],
speed: 2.5,
life: 24,
);
}
void doRespawnFace() {
@@ -229,7 +264,12 @@ class World implements LevelTestRunner {
void shoot() {
final P = player;
if (state != 'playing' || P == null || P.dead || !P.hasGun || P.shootCool != 0) return;
if (state != 'playing' ||
P == null ||
P.dead ||
!P.hasGun ||
P.shootCool != 0)
return;
P.shoot();
}
@@ -334,7 +374,8 @@ class World implements LevelTestRunner {
if (++hudTick % 5 == 0) {
final L = level!;
host.onHud(<String, dynamic>{
'name': (data!['name'] ?? 'STAGE') +
'name':
(data!['name'] ?? 'STAGE') +
(map!.length > 1 ? ' · 面${face + 1}/${map!.length}' : ''),
'deaths': deaths,
'ms': ms,
@@ -345,7 +386,8 @@ class World implements LevelTestRunner {
'totalSaves': L.totalSaves,
});
host.touchSetGun(P.hasGun);
final boss = L.bosses.where((b) => !b.dead).firstOrNull ?? L.bosses.firstOrNull;
final boss =
L.bosses.where((b) => !b.dead).firstOrNull ?? L.bosses.firstOrNull;
host.onBossBar(L.bosses.isNotEmpty ? boss : null);
_pushToast(L.toast ?? '');
}
@@ -371,8 +413,11 @@ class World implements LevelTestRunner {
} else if (state == 'paused' && Input.hit('pause')) {
resume();
}
// 每个固定步长都刷新输入边沿(prev=now)。否则某一帧跑 0 次更新、
// 下一帧跑 2 次时,同一次按键会被当两帧的 hit —— 表现为误触二段跳;
// 合帧亦不应丢失按键:now 在两次更新间保持为 true,下一子步才清 prev。
Input.endFrame();
}
Input.endFrame();
// toast 推送必须在 Ticker 回调阶段(transientCallbacks)完成,
// 不能放进 render() —— paint 阶段改被 ValueListenableBuilder 监听的
// notifier 会触发 "Build scheduled during frame"(晚一帧且刷警告)。
@@ -380,14 +425,16 @@ class World implements LevelTestRunner {
if (state != 'playing') time++;
}
// ---------------- 渲染(800×608 虚拟空间) ----------------
// ---------------- 渲染(虚拟视口空间,≥800×608 随屏幕比例加宽) ----------------
void render(Canvas c) {
final t = time;
final vw = viewW;
final vh = viewH;
if (state == 'playing' || state == 'paused' || state == 'result') {
final L = level!;
final P = player!;
final C = cam!;
c.drawRect(Rect.fromLTWH(0, 0, W, H), Paint()..color = hex('#0a0a14'));
c.drawRect(Rect.fromLTWH(0, 0, vw, vh), Paint()..color = hex('#0a0a14'));
L.drawBg(c, C, t);
c.save();
C.apply(c);
@@ -404,7 +451,9 @@ class World implements LevelTestRunner {
text: TextSpan(
text: useTouch ? '按右下角重生键' : '按 R 重生',
style: TextStyle(
color: const Color(0xffff8fa3).withValues(alpha: (blink ? 1.0 : 0.3) * 255),
color: const Color(
0xffff8fa3,
).withValues(alpha: (blink ? 1.0 : 0.3) * 255),
fontSize: 20,
fontWeight: FontWeight.bold,
fontFamily: 'monospace',
@@ -414,7 +463,7 @@ class World implements LevelTestRunner {
textDirection: TextDirection.ltr,
);
tp.layout();
tp.paint(c, Offset(W / 2 - tp.width / 2, H / 2 - 36 - tp.height / 2));
tp.paint(c, Offset(vw / 2 - tp.width / 2, vh / 2 - 36 - tp.height / 2));
final sub = TextPainter(
text: TextSpan(
text: 'Backspace 整关重来',
@@ -428,7 +477,10 @@ class World implements LevelTestRunner {
textDirection: TextDirection.ltr,
);
sub.layout();
sub.paint(c, Offset(W / 2 - sub.width / 2, H / 2 - 12 - sub.height / 2));
sub.paint(
c,
Offset(vw / 2 - sub.width / 2, vh / 2 - 12 - sub.height / 2),
);
}
} else {
drawTitleBg(c);
@@ -436,16 +488,18 @@ class World implements LevelTestRunner {
}
void drawTitleBg(Canvas c) {
final vw = viewW;
final vh = viewH;
final g = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: <Color>[hex('#181828'), hex('#08080f')],
).createShader(Rect.fromLTWH(0, 0, W, H));
c.drawRect(Rect.fromLTWH(0, 0, W, H), Paint()..shader = g);
).createShader(Rect.fromLTWH(0, 0, vw, vh));
c.drawRect(Rect.fromLTWH(0, 0, vw, vh), Paint()..shader = g);
for (int i = 0; i < 26; i++) {
final double x = ((i * 137 + time * 0.15) % (W + 60)) - 30;
final double y = 120 + ((i * 71) % (H - 160)).toDouble();
final double x = ((i * 137 + time * 0.15) % (vw + 60)) - 30;
final double y = 120 + ((i * 71) % (vh - 160)).toDouble();
final col = i % 3 == 0 ? hex('#3a3a4c') : hex('#2a2a38');
final p = Path()
..moveTo(x, y + 18)
@@ -455,13 +509,16 @@ class World implements LevelTestRunner {
c.drawPath(p, Paint()..color = col.withValues(alpha: 0.5 * 255));
}
c.drawRect(Rect.fromLTWH(0, H - 30, W, 30), Paint()..color = hex('#1a1a26'));
c.drawRect(
Rect.fromLTWH(0, vh - 30, vw, 30),
Paint()..color = hex('#1a1a26'),
);
final spikePaint = Paint()..color = hex('#c8c8d8');
for (double x = 0; x < W; x += 22) {
for (double x = 0; x < vw; x += 22) {
final p = Path()
..moveTo(x, H - 4)
..lineTo(x + 11, H - 26)
..lineTo(x + 22, H - 4)
..moveTo(x, vh - 4)
..lineTo(x + 11, vh - 26)
..lineTo(x + 22, vh - 4)
..close();
c.drawPath(p, spikePaint);
}