From 156887e4c3f8dd4630c7839f97d795c43c85c10a Mon Sep 17 00:00:00 2001 From: CancerAi Date: Sun, 1 Nov 2026 01:50:53 +0800 Subject: [PATCH] =?UTF-8?q?mod:=20=E4=BC=98=E5=8C=96=E8=B7=B3=E8=B7=83?= =?UTF-8?q?=E6=89=8B=E6=84=9F=EF=BC=8C=E5=8E=BBiwanna=E5=85=83=E7=B4=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .workbuddy/memory/2026-10-25.md | 9 +++++++ app/lib/src/app_root.dart | 1 + app/lib/src/game/player.dart | 16 ++++++++---- app/lib/src/ui/screens.dart | 45 ++++++++++++++++++++------------- app/test/gameplay_test.dart | 27 +++++++++++++++++--- 5 files changed, 72 insertions(+), 26 deletions(-) diff --git a/.workbuddy/memory/2026-10-25.md b/.workbuddy/memory/2026-10-25.md index 3771871..1a3dd1b 100644 --- a/.workbuddy/memory/2026-10-25.md +++ b/.workbuddy/memory/2026-10-25.md @@ -84,3 +84,12 @@ 2. 顺手加固同行的 null 兜底:`(data['doors'] as List?)?.cast()` 在 `doors` 为 null 时会因 `.firstWhere` 调在 null 上抛 `NoSuchMethodError`,改为 `?? const []`,消除同类崩溃。 - **回归测试**:新增 `含 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。 diff --git a/app/lib/src/app_root.dart b/app/lib/src/app_root.dart index d1a3f3b..6f1c604 100644 --- a/app/lib/src/app_root.dart +++ b/app/lib/src/app_root.dart @@ -29,6 +29,7 @@ import 'package:iwbtc/src/ui/editor_panel.dart' class AppRoot extends StatefulWidget { const AppRoot({super.key}); + @override State createState() => _AppRootState(); } diff --git a/app/lib/src/game/player.dart b/app/lib/src/game/player.dart index 334d51c..6f4180c 100644 --- a/app/lib/src/game/player.dart +++ b/app/lib/src/game/player.dart @@ -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; diff --git a/app/lib/src/ui/screens.dart b/app/lib/src/ui/screens.dart index e93eccf..b0326ff 100644 --- a/app/lib/src/ui/screens.dart +++ b/app/lib/src/ui/screens.dart @@ -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: [ - const Text( - 'FAN-MADE TRIBUTE PROJECT', - style: TextStyle( - color: Color(0xff8a8aa8), - fontSize: 12, - letterSpacing: 2, - ), - ), const SizedBox(height: 8), RichText( text: const TextSpan( children: [ 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 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) 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 result; + const ResultScreen({super.key, required this.world, required this.result}); @override @@ -416,7 +411,7 @@ class ResultScreen extends StatelessWidget { style: TextStyle(color: Color(0xffffc63a), fontSize: 15), ), ), - const SizedBox(height: 8), + const SizedBox(height: 8), if (hasNext) _MenuButton('下一关', onPressed: () => world.nextLevel()), _MenuButton( '再挑战', @@ -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 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( diff --git a/app/test/gameplay_test.dart b/app/test/gameplay_test.dart index 0c78816..5056ffe 100644 --- a/app/test/gameplay_test.dart +++ b/app/test/gameplay_test.dart @@ -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 {