mod: 优化跳跃手感,去iwanna元素
This commit is contained in:
1 parent
6c07133a9d
commit
156887e4c3
5 files changed
+71
-25
No files matched your search
@@ -84,3 +84,12 @@
|
||||
2. 顺手加固同行的 null 兜底:`(data['doors'] as List?)?.cast<Map>()` 在 `doors` 为 null 时会因 `.firstWhere` 调在 null 上抛 `NoSuchMethodError`,改为 `?? const <Map>[]`,消除同类崩溃。
|
||||
- **回归测试**:新增 `含 warp 但无匹配门的关卡可正常构建(doorId 类型回归)`——构造含 `W` 且 `doors:[]` 的关卡,`Level(data)` 不再抛 TypeError,且 warp 实体的 `doorId` 为 int(回退序号)。
|
||||
- 验证:`flutter test` 22/22、`flutter analyze` 0 error。
|
||||
|
||||
## 跳跃手感「稍微飘一点」,但高度与二段高度不变(PHY 微调)
|
||||
用户:"跳跃手感再稍微飘一点,但是跳跃高度和其他都不能变"。
|
||||
- **原理**:飘 = 更低重力换更长滞空;但顶点高度由 v²/(2g) 决定,同比例降重力 + 提初速可精确保持高度。物理常量在 `app/lib/src/game/player.dart` 的 `PHY`(web 的 `js/player.js`/`tools/lib-reach.mjs` 在当前仓库已不存在,移植版是唯一定义处,无需再同步求解器)。
|
||||
- **改后值**:GRAV 0.25→0.23、GRAV_FALL 0.32→0.30、GRAV_DJ 0.35→0.322、JUMP 6.6→6.326、DJUMP 6.7→6.420;HSPD/MAXFALL/DJ_INERTIA/WATER_*/ICE_*/SLIDE_* 全未动。
|
||||
- **验证(Python 复刻引擎积分 `vy=min(vy+g,MAXFALL);y+=vy`)**:单跳顶点 83.850px、二段自身 60.800px、总高仍 4.5 格 —— 三者 Δ 均 ≈0;滞空 49→51 帧(+2,≈4%,即"稍微飘")。
|
||||
- **代价(须告知用户)**:滞空变长使单次跳跃的水平飞行距离增加约 HSPD×2 ≈ 5px,关卡跳跃可达性会略变"更宽松",这是"飘"的固有副作用;用户明确要求不重调地图,故未动关卡。
|
||||
- **回归测试**:`test/gameplay_test.dart` 新增 `满力单跳顶点高度保持 ≈83.85px(飘但不改高度)`;并把旧的 `JUMP 6.6 / GRAV 0.25` 断言同步改为新值(松键截止量硬编码 -3 未变,只把 0.25→0.23)。
|
||||
- 验证:`flutter test` 23/23、`flutter analyze` 0 error。
|
||||
@@ -29,6 +29,7 @@ import 'package:iwbtc/src/ui/editor_panel.dart'
|
||||
|
||||
class AppRoot extends StatefulWidget {
|
||||
const AppRoot({super.key});
|
||||
|
||||
@override
|
||||
State<AppRoot> createState() => _AppRootState();
|
||||
}
|
||||
|
||||
@@ -16,12 +16,18 @@ class SavePoint {
|
||||
}
|
||||
|
||||
class PHY {
|
||||
static const double GRAV = 0.25;
|
||||
static const double GRAV_FALL = 0.32;
|
||||
static const double GRAV_DJ = 0.35;
|
||||
// 2026-10-25:手感「稍微飘一点」。飘 = 更低重力换更长滞空,但高度由
|
||||
// v²/(2g) 决定——同比例降重力并提初速即可让顶点高度零变化:
|
||||
// GRAV 0.25→0.23、GRAV_FALL 0.32→0.30、GRAV_DJ 0.35→0.322
|
||||
// JUMP 6.6→6.326、DJUMP 6.7→6.420(按引擎积分顺序反解,单跳顶点仍
|
||||
// 83.85px、二段自身仍 60.80px,总高仍 4.5 格;滞空 49→51 帧 ≈ +4%)。
|
||||
// HSPD / MAXFALL / DJ_INERTIA / WATER_* / ICE_* / SLIDE_* 未动。
|
||||
static const double GRAV = 0.23;
|
||||
static const double GRAV_FALL = 0.30;
|
||||
static const double GRAV_DJ = 0.322;
|
||||
static const double MAXFALL = 9;
|
||||
static const double JUMP = 6.6;
|
||||
static const double DJUMP = 6.7;
|
||||
static const double JUMP = 6.326;
|
||||
static const double DJUMP = 6.420;
|
||||
static const double DJ_INERTIA = 0;
|
||||
static const double HSPD = 2.6;
|
||||
static const double WATER_GRAV = 0.16;
|
||||
|
||||
+27
-16
@@ -28,6 +28,7 @@ class TitleScreen extends StatelessWidget {
|
||||
final World world;
|
||||
final VoidCallback onSettings;
|
||||
final VoidCallback onEditor;
|
||||
|
||||
const TitleScreen({
|
||||
super.key,
|
||||
required this.world,
|
||||
@@ -46,20 +47,12 @@ class TitleScreen extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
const Text(
|
||||
'FAN-MADE TRIBUTE PROJECT',
|
||||
style: TextStyle(
|
||||
color: Color(0xff8a8aa8),
|
||||
fontSize: 12,
|
||||
letterSpacing: 2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
RichText(
|
||||
text: const TextSpan(
|
||||
children: <InlineSpan>[
|
||||
TextSpan(
|
||||
text: 'I WANNA BE\nTHE ',
|
||||
text: 'Kid ',
|
||||
style: TextStyle(
|
||||
color: Color(0xffffffff),
|
||||
fontSize: 44,
|
||||
@@ -68,7 +61,7 @@ class TitleScreen extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: 'CREATOR',
|
||||
text: 'Go',
|
||||
style: TextStyle(
|
||||
color: Color(0xffff5c7a),
|
||||
fontSize: 44,
|
||||
@@ -82,7 +75,7 @@ class TitleScreen extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'跳刺 · 耐久 · BOSS · 坑向 — 23 个内置关卡',
|
||||
'跳刺 · 耐久 · BOSS · 坑向',
|
||||
style: TextStyle(color: Color(0xffc8c8d8), fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
@@ -96,7 +89,7 @@ class TitleScreen extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
const Text(
|
||||
'非商业同人作品 · 致敬 I Wanna Be The Guy / I Wanna Be The Creator',
|
||||
'开源地址:https://gitee.com/cancerai/kidgo',
|
||||
style: TextStyle(color: Color(0xff55556a), fontSize: 11),
|
||||
),
|
||||
],
|
||||
@@ -111,6 +104,7 @@ class LevelSelectScreen extends StatelessWidget {
|
||||
final World world;
|
||||
final ValueListenable<int> reload;
|
||||
final VoidCallback onChanged;
|
||||
|
||||
const LevelSelectScreen({
|
||||
super.key,
|
||||
required this.world,
|
||||
@@ -139,6 +133,7 @@ class _LevelBody extends StatefulWidget {
|
||||
final World world;
|
||||
final void Function(Map<String, dynamic>) onPlay;
|
||||
final VoidCallback onChanged;
|
||||
|
||||
const _LevelBody({
|
||||
required this.world,
|
||||
required this.onPlay,
|
||||
@@ -240,6 +235,7 @@ class _LevelCard extends StatelessWidget {
|
||||
final bool manage;
|
||||
final VoidCallback onPlay;
|
||||
final VoidCallback onDelete;
|
||||
|
||||
const _LevelCard({
|
||||
required this.lv,
|
||||
required this.manage,
|
||||
@@ -342,10 +338,7 @@ class _LevelCard extends StatelessWidget {
|
||||
const Positioned(
|
||||
top: 6,
|
||||
right: 6,
|
||||
child: Text(
|
||||
'✔',
|
||||
style: TextStyle(color: Color(0xffb06bff)),
|
||||
),
|
||||
child: Text('✔', style: TextStyle(color: Color(0xffb06bff))),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -363,6 +356,7 @@ String _fmt(int ms) {
|
||||
class PauseScreen extends StatelessWidget {
|
||||
final World world;
|
||||
final String quitLabel;
|
||||
|
||||
const PauseScreen({super.key, required this.world, required this.quitLabel});
|
||||
|
||||
@override
|
||||
@@ -391,6 +385,7 @@ class PauseScreen extends StatelessWidget {
|
||||
class ResultScreen extends StatelessWidget {
|
||||
final World world;
|
||||
final Map<String, dynamic> result;
|
||||
|
||||
const ResultScreen({super.key, required this.world, required this.result});
|
||||
|
||||
@override
|
||||
@@ -434,6 +429,7 @@ class SettingsScreen extends StatefulWidget {
|
||||
final VoidCallback onBack;
|
||||
final VoidCallback onTouchEditor; // 进入独立全屏的「自定义按键」页
|
||||
final bool isTouch;
|
||||
|
||||
const SettingsScreen({
|
||||
super.key,
|
||||
required this.world,
|
||||
@@ -684,6 +680,7 @@ class _KeyRow extends StatelessWidget {
|
||||
final void Function(int) onChip;
|
||||
final void Function(int) onRemove;
|
||||
final VoidCallback onAdd;
|
||||
|
||||
const _KeyRow({
|
||||
required this.act,
|
||||
required this.label,
|
||||
@@ -779,7 +776,9 @@ class _KeyRow extends StatelessWidget {
|
||||
/// 注意:内部不能放 Spacer/Expanded(需要有限高度),列表类屏幕请用 ListView 自滚。
|
||||
class _FitScroll extends StatelessWidget {
|
||||
final Widget child;
|
||||
|
||||
const _FitScroll({required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
@@ -798,7 +797,9 @@ class _FitScroll extends StatelessWidget {
|
||||
|
||||
class _MenuBg extends StatelessWidget {
|
||||
final Widget child;
|
||||
|
||||
const _MenuBg({required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
@@ -819,7 +820,9 @@ class _MenuBg extends StatelessWidget {
|
||||
class _MenuButton extends StatelessWidget {
|
||||
final String text;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
const _MenuButton(this.text, {required this.onPressed});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
@@ -844,7 +847,9 @@ class _SmallButton extends StatelessWidget {
|
||||
final String text;
|
||||
final VoidCallback onPressed;
|
||||
final bool on;
|
||||
|
||||
const _SmallButton(this.text, {required this.onPressed, this.on = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ElevatedButton(
|
||||
@@ -866,7 +871,9 @@ class _Tab extends StatelessWidget {
|
||||
final String text;
|
||||
final bool on;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
const _Tab(this.text, this.on, {required this.onPressed});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ElevatedButton(
|
||||
@@ -884,7 +891,9 @@ class _Tab extends StatelessWidget {
|
||||
class _Modal extends StatelessWidget {
|
||||
final String title;
|
||||
final List<Widget> children;
|
||||
|
||||
const _Modal({required this.title, required this.children});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
@@ -924,7 +933,9 @@ class _Modal extends StatelessWidget {
|
||||
class _ResultLine extends StatelessWidget {
|
||||
final String k;
|
||||
final String v;
|
||||
|
||||
const _ResultLine(this.k, this.v);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
|
||||
@@ -69,7 +69,7 @@ void main() {
|
||||
expect(world.player!.vx, closeTo(2.6, 0.001));
|
||||
});
|
||||
|
||||
test('跳跃初速与重力与 web 版一致(JUMP 6.6 / GRAV 0.25)', () async {
|
||||
test('跳跃初速与重力(当前调校:JUMP 6.326 / GRAV 0.23)', () async {
|
||||
final world = await _start();
|
||||
// 先落地,避免出生即空中导致二段跳语义不同
|
||||
_step(world, 30);
|
||||
@@ -77,8 +77,8 @@ void main() {
|
||||
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: '上升重力不符');
|
||||
// 按住不放时持续受 GRAV = 0.23:-6.326 + 0.23 × 2
|
||||
expect(P.vy, closeTo(-6.326 + 0.46, 0.001), reason: '上升重力/初速不符');
|
||||
});
|
||||
|
||||
test('松键即停:可变跳跃高度(与 web 版一致)', () async {
|
||||
@@ -90,7 +90,26 @@ void main() {
|
||||
// 中途松开: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 收紧');
|
||||
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 {
|
||||
|
||||
Reference in new issue
Block a user