// 无头玩法测试:验证「输入 → World → 物理」链路真的在动(手感保真的核心)。 // 不依赖 widget 树,直接用固定步长推进,与游戏内 world.tick 完全一致。 import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:iwbtc/src/audio/sfx.dart' show Sfx; import 'package:iwbtc/src/core/input.dart' show Input; import 'package:iwbtc/src/core/save.dart' show Save; import 'package:iwbtc/src/game/levels.dart' show loadLevels, LEVELS; import 'package:iwbtc/src/game/world.dart' show GameHost, World; class _Host implements GameHost { String screen = ''; String toast = ''; bool touchShown = false; @override void onBossBar(dynamic boss) {} @override void onHud(Map hud) {} @override void onPauseMenu(String label) {} @override void onResult(Map result) {} @override void onShowScreen(String s) => screen = s; @override void onToast(String text) => toast = text; @override void editorShow() {} @override void touchHide() => touchShown = false; @override void touchSetGun(bool has) {} @override void touchShow() => touchShown = true; } Future _start() async { SharedPreferences.setMockInitialValues({}); await Save.load(); await loadLevels(); Input.clear(); Sfx.enabled = false; // 测试环境没有音频插件,保持静音(Sfx 也不会因此报错) final world = World(_Host()); world.startLevel(LEVELS.first, 0); return world; } /// 推进 n 个固定 60Hz 逻辑帧。 void _step(World w, int n) { for (int i = 0; i < n; i++) { w.tick(17); } } void main() { TestWidgetsFlutterBinding.ensureInitialized(); test('进入关卡后按方向键玩家会移动', () async { final world = await _start(); expect(world.state, 'playing'); expect(world.player, isNotNull); final double x0 = world.player!.x; Input.onKey('ArrowRight', true); _step(world, 15); Input.onKey('ArrowRight', false); expect(world.player!.x, greaterThan(x0), reason: '按住右键后玩家没有向右移动'); // 水平速度与 web 版一致(HSPD = 2.6) expect(world.player!.vx, closeTo(2.6, 0.001)); }); test('跳跃初速与重力与 web 版一致(JUMP 6.6 / GRAV 0.25)', () async { final world = await _start(); // 先落地,避免出生即空中导致二段跳语义不同 _step(world, 30); final P = world.player!; Input.onKey('ShiftLeft', true); world.tick(17); world.tick(17); // 按住不放时持续受 GRAV = 0.25:-6.6 + 0.25 × 2 expect(P.vy, closeTo(-6.6 + 0.5, 0.001), reason: '上升重力不符'); }); test('松键即停:可变跳跃高度(与 web 版一致)', () async { final world = await _start(); _step(world, 30); final P = world.player!; Input.onKey('ShiftLeft', true); world.tick(17); // 中途松开:web 版 player.js 会把 vy 收紧到 -3,再叠加一帧重力 → -2.75 Input.onKey('ShiftLeft', false); world.tick(17); expect(P.vy, closeTo(-3 + 0.25, 0.001), reason: '松键后的上升速度未按 -3 收紧'); }); test('死亡后不自动重生,按 R 才回存档点', () async { final world = await _start(); final P = world.player!; P.kill(); expect(P.dead, isTrue); final double deadX = P.x; Input.onKey('KeyR', false); _step(world, 30); expect(P.dead, isTrue, reason: '死亡后不应自动重生'); expect(P.x, deadX, reason: '死亡后玩家应定格在死亡点'); Input.onKey('KeyR', true); world.tick(17); Input.onKey('KeyR', false); world.tick(17); expect(P.dead, isFalse, reason: '按 R 后应重回存档点'); expect(world.deaths, 1); }); }