fix: 修复迁移后的基础UI显示问题
This commit is contained in:
1 parent
18017f819a
commit
a93e06f60c
68 files changed
+2293
-8404
No files matched your search
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user