Files
kidgo/app/test/gameplay_test.dart

135 lines
4.6 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 无头玩法测试:验证「输入 → 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);
});
}