135 lines
4.6 KiB
Dart
135 lines
4.6 KiB
Dart
// 无头玩法测试:验证「输入 → 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<String, dynamic> hud) {}
|
||
@override
|
||
void onPauseMenu(String label) {}
|
||
@override
|
||
void onResult(Map<String, dynamic> 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<World> _start() async {
|
||
SharedPreferences.setMockInitialValues(<String, Object>{});
|
||
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('跳跃初速与重力(当前调校:JUMP 6.326 / GRAV 0.23)', () async {
|
||
final world = await _start();
|
||
// 先落地,避免出生即空中导致二段跳语义不同
|
||
_step(world, 30);
|
||
final P = world.player!;
|
||
Input.onKey('ShiftLeft', true);
|
||
world.tick(17);
|
||
world.tick(17);
|
||
// 按住不放时持续受 GRAV = 0.23:-6.326 + 0.23 × 2
|
||
expect(P.vy, closeTo(-6.326 + 0.46, 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.23, 0.001), reason: '松键后的上升速度未按 -3 收紧');
|
||
});
|
||
|
||
test('满力单跳顶点高度保持 ≈83.85px(飘但不改高度)', () async {
|
||
// 回归:改手感「更飘」必须保持顶点高度不变——否则天花板/平台间距会错位。
|
||
// 本改法是「同比例降重力 + 提初速」,顶点由 v²/(2g) 决定故应零变化。
|
||
final world = await _start();
|
||
_step(world, 30); // 落地稳定
|
||
final P = world.player!;
|
||
final double y0 = P.y;
|
||
Input.onKey('ShiftLeft', true); // 满力起跳(按住不放)
|
||
double minY = P.y;
|
||
for (int i = 0; i < 240; i++) {
|
||
world.tick(17);
|
||
if (P.y < minY) minY = P.y;
|
||
if (i > 5 && P.vy >= 0 && P.y >= y0) break; // 落回起跳高度
|
||
}
|
||
final double apex = y0 - minY;
|
||
expect(apex, closeTo(83.85, 0.5),
|
||
reason: '飘的改法必须保持单跳顶点高度 ≈83.85px(约 2.6 格)');
|
||
});
|
||
|
||
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);
|
||
});
|
||
}
|