奇 迹 0.97 · 经 典 还 原 · 刷 怪 爆 装
| 移动 | WASD 或 鼠标左键点地面(自动寻路) |
| 触屏操作 | 左下摇杆移动 · 单指左右滑环绕视角(360° 无限制)· 上下拖动调俯仰 · 双指捏合缩放 · 点技能图标施法 · 点怪锁定 |
| 横屏 / 全屏 | 手机横过来会自动切横屏布局;点右侧 ⛶ 横屏 可全屏并锁定横屏。微信内浏览器锁竖屏时,该按钮会把画面整体旋转 90°(横持手机即可),这是微信里唯一可行方式 |
| 视角模式(两个选项) | 自由环绕(★ v24r167 起为默认):水平 360° 无限制环绕(右键拖动 / QE / 触屏单指左右滑)、俯仰上下拖(9°~73°)、缩放 滚轮 / 双指捏合(10~42 米)。45° 固定(设置 → 画面 → 视角模式 里一键切换 · 渲染通道最省):方位角 / 俯角锁定、视距双指可调 18~40m(拖动、QE、手柄右摇杆一律不动轴,只随角色平移)—— 渲染通道按几何收紧(视锥余量砍半 / 收池半径收紧 / 阴影覆盖收到 ±40m 跟随玩家),帧率更稳、更省电。网址参数 ?cam45=0 强制自由、?cam45=1 强制固定(显式指定优先于存档);切换即时生效并记入存档。 |
| 勇者大陆 | 勇者大陆已换装成暴风城要塞(白石城墙 + 深蓝尖顶 + 金狮旗,低多边形建模)。安全区不刷怪,但技能栏照常显示可用。想回旧版小镇加 ?oldtown=1 |
| 性能层 · 屏外收池(v24r20) | 转身/跑动时,屏幕外和走远了的建筑立刻停止渲染(含人物背后的楼),回头看再恢复。不会重新编译材质,也不会为了收池而重画阴影。?perf0=1 可一键关掉、和 r15 完全一样;?cullout=90 收得更远、?cullin=48 更早收、?fovm=0.6 视锥放宽少收;开 ?stats=1 面板多一行「收池 / 在册」,底部还有一行「相机 0.62 · 盒 215」= 当前镜头收缩比与参与碰撞的盒子数 |
| 草也一起流送(v24r20) | 地表草簇由一整块 4200 棵改成一格一格的草块(24m 一块,几何与材质全共享),于是跑近一段距离才长出这一撮草、转身不看就收掉,跟树一个逻辑。城堡不这么干(见下条,地标按视锥收会闪)。看草块数:?stats=1 |
| 城堡不再闪 / 不再整块消失(v24r20) | ① 要塞(主堡 / 双塔 / 城墙 / 护城河 / 前庭)不参与视锥剔除 —— 只按「跑出 130 米」收、走回 90 米内才恢复(中间 40m 滞回带,怎么走都不闪)。② 修掉一个会让整段永久消失的坑:着色器预热收尾时会把「刚被性能层复活的构件」又按下去藏起来,从此性能层以为它可见、实际永不出现 —— 现在预热只熄灭自己点亮过的、且性能层没要求显示的那些 |
| 相机防穿模 · 弹簧式(v24r20) | 相机有物理碰撞:从角色身后打射线,撞到楼 / 塔 / 城墙就自动让路。碰撞盒是逐块精确的(要塞几何按材质合并成 6~8 个 mesh,拿「整段包围盒」当碰撞体会在空院子、空广场上误挡相机 —— 那正是「一碰到建筑就猛地拉近」的元凶)。撞上不再瞬移:相机是挂在角色背后的一根弹簧,余量带(0.45~2.4m,随命中距离缩放)就是缓冲行程,被墙「顶」着连续平移、墙走开再慢慢退回原来的视距(v24r87:收 / 放拆成两个刚度 —— 拉近 13 让得快,放远 2.5 慢慢来,19m 臂长上 5.7m 回原长:峰值 17.6 → 7.7 m/s、95% 1.32s → 3.02s);再加侧须预判(相机有体积,靠墙转视角时提前收,而不是撞上那一下)。玩家在城内时城墙还当「容器」,只挡侧面不挡顶面(俯视照样能飞过城墙看全景)。关:?camcol0=1;软硬 ?camcolkin= / ?camcolkout=;离身最近 ?camcolmind=;预判条数 ?camcolwhisk=0 |
| 镜头拉高也不卡(v24r22 起分两档) | 镜头拉高俯视看全景时,屏幕里会塞进整片城 + 外圈草树,渲染范围暴涨 —— 边走边看远处就掉帧。现在按「视野里有没有城堡」分两档,镜头高度 ≤ 9m 时两档都不生效(和 r20 一模一样): · 城堡档(视野里有要塞 / 城门 / 雕像)→ 视距完全不缩,视锥余量还反向放宽 30%(「视锥如果在城堡,要扩大」)—— 城堡永远完整; · 野外档(看远处、视野里没有地标)→ 镜头越高缩得越狠,到 32m 高空时视距只剩一半(×0.5),渲染量直接砍半(「摄像机比建筑高一点就够」)。 地标本身不参与缩距 —— 修掉 r21 的 bug:那版把要塞 130m 的收池半径一起乘了缩放、被砍到 84.5m,于是镜头一拉高、人往前走两步整座城堡就没了。 关: ?hz0=1(全关)/ ?wild0=1(永不缩距)/ ?land0=1(不做城堡放宽);调幅度 ?h0=9&h1=32&hshrink=0.5&hfov=0.45&hgrow=0.3;?stats=1 面板「收池」行末尾是 · 高0.00 · 城堡档/野外档 · 视距1.00 |
| 角色实体碰撞 · 扫掠滑动(v24r23 重写) | 角色不再穿墙:撞到楼 / 塔 / 城墙 / 树会被拦下,贴着墙自然滑过去。算法换成 Quake / 魔兽世界客户端那一套 扫掠 + 滑动(sweep & slide):先算「这一步最早会撞到哪」,只走到接触点,再把「撞进去」的那点位移去掉(切线方向保留)。性质:永不穿透(压根没走进实体里面)、永不弹飞(位移只会被缩短)、永不卡死(最坏是原地不动,不会像旧版那样把人按回上一帧位置 = 完全动不了)。草簇天生没有碰撞体 → 照常踩过去,正是「除了草」。矮台基 / 台阶按能迈上去的高度(1.4m)放行,改成直接踩上去(角色与镜头一起抬高,不会半身陷进台子里)。关:?playercol0=1;调 ?playercolr=(半径)/ ?playercolstep=(迈步高度)/ ?playercolit=(滑动迭代轮数) |
| 整座城堡一起收放(v24r22) | 要塞以前是按 7 个分片分别收的:各片中心到玩家的距离不同,会被分批收掉 ⇒ 画面里城堡缺一角(「整座城堡没有分块,不在视锥范围立马不见了」)。现在改成整座一个整体:收池半径 OUT+r ≈ 227m,而地图最远对角只有 110m ⇒ 在任何位置都不会被收掉、绝不会缺一块;分片仍然保留逐块碰撞盒(221 块)给相机和角色用。要塞是按材质合并的(个位数 draw call),常驻代价很小 |
| 撞到建筑升高(v24r22 已关闭) | 曾经做过「相机一撞到建筑就立即升到它顶上」,实测不行(视角在贴地跟拍和屋顶俯视之间来回跳,很晕)→ v24r23 起默认关闭,回到沿水平方向让路。想再试:?camrise1=1 |
| 出生不卡住 · 散件巨盒 / 多余 NPC(v24r23) | 修掉两个真凶:① 挂在全城各处的旗帜段,它的整段包围盒竟是整座城堡那么大的实心立方体(60×42×60)——被当成碰撞体之后,角色一进游戏就永远判定「在实体内部」,会被弹飞或卡死。现在这类的散件段一律不参与碰撞,另外加了一道巨盒防线(高 > 12m 且水平两轴都 > 25m 的包围盒一律不当碰撞体);② 暴风城里不该有居民 NPC(那套 NPC 是给旧都铎小镇摆的:铁匠铺门口 / 市集 / 城门正中……会出现一大群和玩家一个模样的人站在大道上)→ 现在只在 ?oldtown=1 旧镇里出现。出生 / 复活 / 换图后还会做一次落点安全化:万一起点落在实体里,以它为中心螺旋外扩找一块站得住的空地 |
| 有怪也顺 · 冰风谷(v24r24) | 「进冰风谷有怪就卡」的根子在怪物身上背的开销,三条一起砍:① 普通怪不再自带点光源——原来每个暗黑骑士身上挂一盏灯,同时在场 20 只 = 20 盏动态点光;在 three 的前向渲染下,光源数量一变(刷怪 / 死怪)就要把所有受光材质的 shader 重编译一遍(几百毫秒的硬卡),而且每像素每帧还要循环 20 次。现在普通怪改为自发光胸核、0 盏,只有冰霜魔王留 1 盏;掉落物的点光也一起下线(打怪掉一地金币 = 现场动态增删十几盏灯,捡拾时一直在重编译)。② 怪物模型改成「模板缓存 + clone」:原来每只怪都重新建十几个 Mesh 并把整套材质克隆一份(20 只怪 ≈ 500 个材质,而且每帧都要遍历它们做受击染色);现在同种怪只建一次模板,之后 clone 出去的实例共享几何与材质(20 只怪只用 25 个材质),刷怪时不再掉帧。受击反馈换成「模型弹一下 + 命中爆点 + 伤害飘字」。③ 普通怪不再投影,阴影图只由玩家和 Boss 的移动触发重画(旧版「任何一只怪一走动,就每 2 帧把整张 4096² 阴影重渲一遍」,在冰风谷等于永远全速重渲)。④ 刷怪密度按画质档收敛(低配 6 个点 × 2 只,常规 8 × 3;旧版 10 × 4 ≈ 21 只常驻)。开关:?moblight1=1(恢复怪物点光)/ ?itemlight1=1(恢复掉落物点光)/ ?mobshadow1=1(普通怪恢复投影)/ ?moblight0=1(连 Boss 那盏也关) |
| 地面不打架 · 城墙不藏旗(v24r25) | 三处「贴面打架」一次清干净:① 前庭地砖抬到草地起伏之上——主世界草地的顶点带 ±0.15m 的随风起伏(windGround amp=0.15),而地砖原来只抬 0.05(要塞整体 ×0.8 后世界才 0.04m),草皮会时隐时现地戳出地砖,这就是「一下子地砖一下子草皮」;现在地砖在世界 0.24m、中央纹章拼花 0.33m,各留 0.09m 以上余量,彻底分开。② 城门上的城旗脱出墙体——南门楼那面垂旗原来挂在局部 z=30.2,正好嵌在「吊闸木门」与「门洞上方墙体」内部(整面旗埋在墙里);现在移到门楼外立面之外(双塔外沿再往外 0.7m),布幔按飘动峰值 ±0.44m 校核过,怎么晃都不碰,高度也压到门楣墙下沿与徽记盘之间。③ 看不见的地砖不渲染——地砖从「一整片合并成一个 mesh」改成按 16m 切成 14 格各自收池,视锥外的那几格直接不画;玩家周围 24m 内永不收(脚下与眼前的地面绝不会突然消失),转身回头看时立刻补回来。开关:?court0=1(关掉分块收池,回到整片常驻) |
| 模型资产 · 建筑也能换模型(v24r27 / 示例挪到城门外 r28 / 材质可换贴图·可改色 r29 / OBJ 读 .mtl r30 / 狐狸转身放大 r32 / AI 建模进游戏 r33) | 要塞的每一段建筑都能换成美工做的 .glb,带骨骼动画的会自己播。按 N 打开「模型资产」面板: ① 可整段替换的 7 个段落:主堡 · 双塔 · 城墙(含城门)· 护城河/桥 · 前庭广场 · 中央喷泉 · 旗帜; ② 可叠加摆件的 7 个装饰位:主堡顶 · 左塔顶 · 右塔顶 · 前庭左侧 · 前庭右侧 · 城门外左侧 · 城门外右侧(在不改原几何的前提下加雕像/钟楼/风车)。 用法:把 .glb 丢进 assets/models/ext/ → 跑 python tools/model-scan.py(或面板点【扫描】)→ 给槽位【选文件】或直接把模型拖到卡片上,场景里立刻生效(不用刷新、不用重建世界);面板里还能切动画(模型自带的 clip 会列出来)、调缩放/旋转/上下左右偏移、【恢复内置】。目录里已经放了 4 个官方示例模型可以直接试: Fox(3 段跑跳动画)· RobotExpressive(11 段)· LittlestTokyo(整个小场景带机械联动)· BoxTextured(UV 贴图对照),授权见 assets/models/ext/README.md。两条重要说明:① 段落被换掉时原轮廓会变成隐形碰撞壳(角色照样沿着原来的墙走,不会穿过去),想连壳一起去掉加 ?extcol0=1;② 模型请按世界米建模(面板会显示每个槽位的参考尺寸与锚点坐标)。开关: ?modelui=1 进游戏开面板 / ?ext=building:keep=我的主堡.glb 网址直接试装(可多组)。★ 现在默认就装好了两个示例模型,就在城门外:沿中央大道往城门走到底、穿过吊闸出城,左手边是一只跑动的狐狸(Fox,3 段动画;r32 起背靠城墙站着、放大到 4 倍,约 1.5×4.7×9.3 米)、右手边是一个会做动作的机器人(RobotExpressive,14 段动画)—— 这就是「外部模型接口」的活体样例。不想要就在面板里点【恢复内置】。 Draco 压缩的 .glb 也能直接装(体积能小一个数量级,解码器已内置、按需下载,首次用约 250KB)。 ★ v24r29 · 模型的贴图与颜色也能改:展开任一外部模型的【详情】,最下面多了一栏材质 —— 告诉你这个模型有几个材质、各是「有贴图」还是「纯色」、有没有 UV;每个材质配一个颜色选择器(拖着实时变)和金属度/粗糙度输入框,「全部材质 →」可以整只模型一起换色,【撤销改色】一次回到模型自带的颜色。 换贴图:模型里每个材质都自动进了「贴图资产」面板 —— 按 U → 分组选「外部模型贴图」→ 找到 模型.glb · 材质名,把 png 拖上去即可,点【恢复】就是回到模型自带的那张贴图。两个容易踩的点:① 模型没展 UV 时贴图贴不上去(整块面只会采样到图案的一个像素,看起来还是纯色)—— 面板会把这种槽位标成「无UV」并警告,此时只能改颜色;② glb 里存的底色是线性空间的值,游戏里显示成 sRGB 后数字会不一样(是同一个颜色),别以为读错了。 ★ v24r30 · OBJ 也能带材质了(以前是死文件):引擎现在会自己读 OBJ 旁边那份 .mtl —— Kd 当底色(美术改 mtl 就改色,不用碰代码)、d 当透明度、map_Kd 当漫反射贴图。**同名材质共用一份**(改 Steel = 所有钢件一起变)。没找到 .mtl 时回落到内置色板,不会变成一片白。⚠ 但 OBJ 想真的贴上图,导出的文件必须展过 UV:文件里要有 vt、面写成 f v/vt/vn(3ds Max 导出时勾「纹理坐标」,Blender 里先 U 展 UV)。本目录里的 hero_max.obj / knight_max.obj 就一个 vt 都没有、mtl 里也没有 map_Kd → 它们**只能改颜色,贴不上图**;面板会直接把这种槽位标成「无UV」并告诉你原因。另外 OBJ 的贴图永远是外部图片文件(不像 glb 能内嵌),要和 obj/mtl 放同一个目录。★ v24r31 · 模型的贴图也能用「丢文件」方式换:模型槽位的文件名把 : 写成 @(Windows 文件名不允许冒号)—— 例如狐狸是 ext@prop@gate_left@fox_material.png/jpg,丢进 assets/textures/ 后点面板【扫描目录】或跑 python tools/tex-scan.py 即生效;【恢复】仍是回到模型自带的那张图。城门外的狐狸已经换了张「霜白狐」新皮肤当样例(原版橙狐随时可恢复)。★ v24r32 · 狐狸转身面朝城外 + 放大 4 倍:出城左手那只狐狸现在是背靠城墙、面朝远方的姿态( rotY 3.1416 → 0),体型也放大到 4 倍(scale 0.015 → 0.06,世界里约 1.5 × 4.7 × 9.3 米)—— 走过吊闸就能看到一只大狐狸立在墙根。⚠ 放大后必须挪位置:狐狸体长 9.3 米,而门外锚点离墙沿只有 3.4 米,直接放大会把尾巴插进城墙 1.9 米。所以给它加了 oz: 2.3(置入偏移,米)整体往城外推,尾巴离墙面留 0.4 米。想自己调大小:按 N → 狐狸那张卡 → 改【缩放】,同时把【前后偏移 oz】跟着改(换算:oz ≈ 88.1 × scale − 3.0 —— 88.1 是 Fox.glb 在 -z 方向的原生伸出量、3.0 是「锚点到墙沿 3.4 米 − 留 0.4 米余量」;懒算就直接跑 node tools/r32-foxplace-probe.cjs 看真几何)。★ v24r33 · AI 直接生成模型(天生带 UV + 贴图):用 AI(混元 3D)一句话 / 一张图就能生成 .glb,自带展好的 UV 和烘焙的 PBR 贴图,丢进 assets/models/ext/ 按流程装载即可 —— 前庭左侧那尊守卫雕像就是 AI 生成的(80028 三角,scale 2.6 ≈ 世界 2.8 米高)。装好后按 U → 「外部模型贴图」里拖一张 png 就能给它换肤,【恢复】回到 AI 原图。两个注意点:① AI 生成的是静态雕塑(没有骨骼动画),适合建筑段 / 装饰位,替不了会动的角色;② 生成的贴图是未压缩 PNG(容易几十 MB),发布前跑 python tools/r33-shrink-glb.py 模型.glb 压成 2K JPG(守卫雕像 37.5MB → 3.5MB,几何一字节不动)。★ v24r34 · 静态雕像也能有动作了(程序化绑定):AI 模型没有骨骼、整身还是连成一体的网格 —— 引擎现在会在装载时按空间权重把「剑 + 双手」自动绑到一根程序化骨骼上,再播手写的关键帧动画。前庭左侧的守卫雕像默认就在循环演 「LichPoint 举剑指前」:0.6 秒猛抡 102° 把竖拄的剑抡到水平指向前方、定住 1.9 秒、再缓缓收回(4.4 秒无缝循环),基座和身体纹丝不动。按 N → 雕像卡片里能看到 LichPoint 这个 clip,可随时切换或停播。想给别的静态模型配动作:在 model-map.json 里加 "rig": "swordpoint"(目前内置的是按守卫雕像几何定制的剑指动画,换模型要重定区域参数)。★ v24r35 · 有动作的雕像单独挪到一边,放大 3 倍:这尊会演「举剑指前」的守卫雕像从前庭左侧挪到了前庭右侧(进城沿中央大道往南走,右手边),并且 scale 2.6 → 7.8(正好 3 倍,世界里高 8.35 米、宽 3.0 米、深 3.5 米),rotY −90° 面朝大道;左侧留空。用 489 块碰撞盒 + 256 条球面射线实测:不穿城墙 / 塔 / 喷泉 / 地砖,举剑时剑尖伸到离中轴 7.96 米处仍不挡通道(中轴半宽 6 米)。左侧那个位置现在是空的 —— 按N 面板随时可以放别的模型。 ★ v24r36 · 剑指动画修正:手会跟着剑一起抬起来了;剑抡起时不再把基座顶面的石头拉出斜面、也不再顶凹肚子 —— 「手 + 剑」现在作为整体从身体上干净分离(分离缝就像把剑从石头里拔出来)。剑轴中心按网格实测修正了 3.5cm 的偏差。 ★ v24r37 · 手向前伸,剑整根指向前方:旋转轴心从「手心」上移到肩部(绕手心转手当然不动)—— 现在是握剑的手带着整条剑一起绕肩抡到水平:手明显向前伸出、柄头抬到肩高、剑身水平直指前方;剑尖插在石座里的下段留在原位(像「石中剑」的剑桩),剑从此不会刮到基座的石头。同时根治了上一版「剑抡起时把石头拉出一条斜面」的真凶:雕像网格是连成一体的,剑转身时会把石头表面拉出半米长的蒙皮膜 —— 现在引擎会在剪口处把混合面整面剪开归边(拓扑剪开),两侧各自完整变形,分离缝干净利落。左右肩甲也不会再被手带得晃动。 ★ v24r53 · 整根剑完全离开石头 + 巫妖王举剑:上一版插在石座里的「剑桩」就是截图里没离开的刀尖 —— 现在整根剑连尖端一起拔出(剑身改用盒形权重区域,正好罩住菱形剑截面而不带起石槽壁,基座石头实测零位移)。动作对齐巫妖王视频:静止拄剑 → 整根剑从石座大幅抡起举过头顶 → 收势水平直指前方 → 保持 → 收回,5.2 秒无缝循环。 |
| 带骨骼的模型(FBX / GLB 自带骨骼 + 蒙皮 + 动画)· v24r53 | 混元 3D 生成的模型是静态雕塑(0 骨骼、0 蒙皮),动作全靠引擎程序化造骨(procrig);而从 Mixamo / 3ds Max / Blender 导出的 .fbx 自带真骨骼 + 蒙皮 + 动画 —— 引擎会直接播它自带的 clip(procrig 自动不插手,不会重复造骨)。⚠ 两个必须做的预处理:① 先转成 .glb(fbx 体积大、解析慢);② 减面 —— 本次样例:20MB FBX / 50 万面 → 1.25MB / 4 万面,28 根骨骼与 2.1 秒动画一条不丢。 装载后按 N 打开面板:可以切动画,并用新增的【动作速度】( animRate,就是 mixer 的 timeScale)程序改动作快慢:1 原速 / 0.5 慢放一倍 / 2 双倍速。★ 现在前庭最外侧( prop:court_right3)站的就是这样一个带骨武者:28 根人形骨骼(Hips / Spine / Head…)、1 个 SkinnedMesh、自带 2.117 秒原地空翻 / 翻滚动作,厘米单位 → scale 0.01 = 世界 1.73 米,rotY −90°、与左侧剑修对称(x ±18.9 米)。⚠ 这个模型没有 UV(纯色材质),所以别给它配流体 coat(贴不了水流)。 |
| 黑骑士流体装备层真生效 · v24r53 | 修一个让「流体光泽」完全没出现的真 bug:VFX.fluidCoat 原来会跳过带 noOutline 标记的网格,而模型装载器给每一个外部模型都打了这个标记(外部模型不参与描边)→ 所有网格被跳过 → 一件水壳都没生成。现在外壳分两层:水纹流动壳(贴着铠甲往下淌的程序化水流贴图,UV 每帧滚动)+外发光晕壳(BackSide 纯色叠加,只在轮廓边缘透出光 —— MU「+9 以上装备」那种泛光)。 参数在 model-map.json 的 coat 字段:grow 外壳厚度 / opacity 浓度 / speed 水流速度 / drip 水珠频率 / ripple 脚下水波间隔 / dir 方向(up 从脚底往头顶爬)。黑骑士当前值:厚度 1.05、浓度 0.85、流速 0.5、流光 0.9。 |
| ★ 流体水珠/涟漪真正落在雕像身上 · v24r59 | 修「雕像装备没有流体效果」的第二个真 bug(水壳本身正常,但一滴水珠都看不到):水珠/涟漪粒子走 VFX.burst/mist/frostWave,收的是世界坐标;而流体层是在模型装载器里建壳的 —— 那时雕像还没被摆进世界(整体还在原点),于是按当时包围盒算出的中心点全在原点附近 → 所有水珠/涟漪都跑到城堡中心(多半埋在地上或喷泉里),雕像身上一颗水珠都没有,看着就是「没有流体效果」。现在每帧把模型局部中心用 localToWorld 换算成真实世界坐标再发射,无论雕像被摆到哪、之后重摆多少次,水珠与脚下涟漪都精确落在雕像本体上(验收 tools/r56-coat-worldspace-test.cjs:雕像搬到 (40,20) 后 19/19 粒子全部命中,落在原点的 0 个)。 |
| 外星人:科目三 → 武侠动作 · 程序生成的动作 · v24r58/r59/r78/r79 | 丢进来的 .fbx 骨架蒙皮很干净(28 根标准 Mixamo 人形骨 + 1 个 SkinnedMesh),但自带动画是 0 段 —— 所以动作全部是用代码按它真实骨架程序注入的。★★ v24r79(用户「跳科目三的模型,去掉动作,加入新的武侠动作数据」):科目三作废 —— ① dropAnims:["KechuSan"] 把那段舞从动画列表里剔除(面板里也不再出现);② 新增 js/wuxia.js 注入三套武侠动作:抱拳起势(起手抱拳 → 开势推掌 → 收势,6.0s)/云手太极(双手身前划圆、腰为轴、虚实转换,5.6s)/弓步冲拳(左腿上前成弓步、腰带右拳冲出,4.4s),默认播抱拳起势,按 N 面板可切换。★ 为什么这套要用「世界轴增量」写关键帧:alien 的静止姿态是 T-pose(双臂水平伸向 ±x),而且每根骨的局部静止四元数是歪的(父链 Armature 带旋转)—— 直接写局部欧拉必然姿势错乱。wuxia.js 一律按 qL = qPw0⁻¹·qR·qPw0·qL_rest 换算,于是「绕世界 X 转 30°」在每根骨上含义一致:rx=向后摆、ry=转身、rz=左(+x)侧抬起,与骨架朝向无关。手位全部由 tools/wuxia-pose-lab.cjs 实测校准(抱拳时双手间距 0.23m、掌高 2.82m、身前 0.84m,均是世界坐标)。⚠ 下沉要靠「先屈膝再压髋」:马步/弓步的蹲是「先屈膝把脚抬高,再用 root 位置轨道把脚压回地面」,只沉髋不弯腿会让脚穿进地里(实测差 4~6 单位)。 (历史:科目三的编舞(4 小节 @108bpm = 8.89 秒无缝循环):压重心+甩肩+手臂画圈+头点动+膝盖随节拍微屈弹动,r58~r77 期间在播,现已退役、代码保留在 js/wuxia.js 之外的历史里。)位置(v24r79):按用户口径「放在狐狸旁边」—— 从城内前庭前排搬到城门外左手边、狐狸( prop:gate_left)旁边,两者留 0.86m 间隙不重叠(外星人宽 3.95m / 狐狸 1.51m,并排看着像一对)。锚点照旧从门洞几何推出(x = -(门洞半宽 3.6 + 2.4 + 3.6) = -9.6m、z 与狐狸同排、y = 0.2 压在草地起伏之上),scale 0.022(源文件是厘米单位、身高 190 → 世界 4.19 米,与 4.74m 的狐狸同高量级)。⚠ 槽位 key 仍叫 prop:court_front(r59 起的历史名,位置一律以 stormwind.js 的锚点表为准)。历史:r58 曾放左塔顶(34m 高看不清)→ r59~r78 在城内前庭前排显眼位 → r79 挪到城外狐狸旁。面板能切动作、改【动作速度】(animRate,调 0.5 就是慢动作打拳)。⚠ 贴图在转换时摘掉了(node 里解不了 FBX 内嵌贴图,空白贴图还会让 GLB 导出失败)—— 用游戏内 U「贴图资产」面板拖 png 即可补上。 |
| ★ 3D LUT 精确调色(可切换 3 套风格)· v24r81 | 参考项目 2026-10-06-08-18-04/messenger-copy(SvelteKit + Three r184 逆向商业站的学习工程)里最有价值的一件:原站后处理用的是 3D LUT 四面体插值(apply3DLUTTetrahedral + sampler3D tLUT + lut.ktx2),而 mu3d 只有「三段式」近似调色(阴影/中间调/高光各乘一色 + 色温 + S 曲线)—— 近似调色做不了跨色相的精确映射(比如「把土黄统一压成青灰,同时保住朱红的饱和」,乘法近似会连带把朱红也推走)。① 新模块 js/lut.js:16³ = 4096 个采样点,三套预设 —— mu 青灰冷调(暗部蓝 0.109 vs 红 0.075、高光红 1.00 vs 蓝 0.865,贴近 0.97 原版的阴郁魔幻)/ dusk 黄昏暖金 / neo 中性轻度(中灰几乎不动,风险最低),另有 off 一键回到原三段式。② 四面体插值:按 f.r / f.g / f.b 的大小关系选 6 个四面体之一,权重取 1-fx, fx-fy, fy-fz, fz —— 比三线性插值在立方体体对角线附近没有可见接缝(三线性把立方体当 8 顶点加权,对角线处梯度不连续)。③ 双通路降级:WebGL2 走原生 sampler3D;WebGL1 / 老内核自动退到 2D 条带(16 张 16×16 切片拼 256×16),两条路径共用同一份烘焙数据 ⇒ 逐点像素级一致(回归测试用 8 个探针 × 3 通道验证过)。④ 可切换/可调:游戏内按 L 键循环(选择记 localStorage,下次进来沿用);也可用网址参数 ?lut=mu|dusk|neo|off、?lutstr=0.55 调强度。⚠ 顺序有讲究:LUT 放在 ACES + 三段式 + 饱和之后、暗角/颗粒之前 —— 它是「最终色彩映射」,应作用在已压到显示空间、基础色已定型的画面上;放暗角之后会把渐晕的暗部一起映射,四角出现色偏。 验收 tools/r81-lut-test.cjs 42/0。 |
| ★ 屏幕空间描边(省掉翻倍三角形)· v24r81 | mu3d 原有的 VFX.outline() 是几何壳描边 —— 复制一份网格、放大 1+thickness、BackSide 渲染。代价是三角形数直接翻倍,所以 game.js 的注释一直写着「描边=复制网格翻倍三角形,只给 Boss 保留;普通怪省掉」。① 改成读深度图算边缘( fx.js 新增 OUTLINE 着色器):取中心与上下左右 4 邻居的线性深度,用「最大邻居差」而非单像素差(单像素差对细边会漏检,邻域取 max 能抓住 2px 内的突变)—— 不增加任何三角形,于是能给所有怪物上描边。② 三个必须的处理:相对归一化(差值除以中心深度 —— 远处地面深度梯度天然大,不归一化会把远景整片描成黑边);天空排除(深度≈1 不描,否则地平线画一条线);距离淡出(太远的边不描,与参考项目 uOutlineFade 同思路)。阈值用 smoothstep 做柔和过渡,不是硬边。③ 双路径共存、自动让位:屏幕空间可用时 VFX.outline() 跳过几何壳(不再翻倍三角形);不可用时(软渲染 / 无深度纹理 / 关 AO)自动退回几何壳 —— 老行为一字不变。?geomoutline=1 可强制几何壳,?nooutline 全关,?outline=1 在手机上手动开(手机默认关 AO ⇒ 没深度图,这是有意的性能取舍:全屏深度 pass 在手机 GPU 上不便宜)。④ 深度纹理改按需建:原来只在 AO 时建,现在 needDepthTex = useAO || useOutline;同时 useMSAA 的判据跟着改成 !needDepthTex(建了深度纹理就不能开 MSAA —— MSAA 的 depth 无法 resolve 到 depthTexture)。⚠ 两次 TDZ 陷阱: useOutline 必须在 __POSTQ 赋值前声明、needDepthTex 必须在 useMSAA 前声明 —— 先用后声明抛的 ReferenceError 会被 try/catch 静默吞掉,表现为「整条后处理莫名降级成直通」(MEMORY 里 RT_TYPE 踩过同一个坑)。回归测试专门守这两条。验收 tools/r81s-outline-test.cjs 40/0;端到端 ?forcegpu=1 实机校验:LUT 走 3D 纹理路径(mode:"3d")、四档切换 uniforms 实况正确、零 shader 编译错误。 |
| ★ 模型解码:模板缓存 + 让帧拆分 · v24r81 | 原立项是「把模型解码搬进 Web Worker」,实测推翻了假设(见 tools/r81w-decode-bench.cjs):① Draco 本来就在 worker 里—— modelloader.js 里早有 d.setWorkerLimit(2),且 35MB / 12 个 GLB 里只有 LittlestTokyo.glb 一个用 Draco,搬它没有意义。② GLTF 解析不适合搬 worker—— GLTFLoader.parse 的产物是 three 对象图(Scene/Mesh/BufferGeometry/材质/SkinnedMesh/AnimationClip),跨 worker 必须 transfer typed array 再在主线程重建一遍,省下的只有「读 JSON chunk + 拼对象树」;而参考项目(messenger-copy)用 worker 是因为它做的是过程化地形(纯数据 → instance matrix),不是资产管线,不可类比。加上微信/QQ X5 旧 WebView 的 Worker 支持不齐 —— 收益/风险比不成立。③ 真正值得省的是「重复解码」 + 「连续阻塞」:实测单模型 parse 5~36ms,场景加载总量 ≈182ms(node)/ 浏览器 ×1.75 ≈ 0.32s 主线程连续卡顿。 改造:a. 解析模板缓存 state.parseCache —— 同一 (文件, rig/wuxia/dropAnims) 只真解析一次;命中时走 cloneParsed()(clone(true) 深拷节点树,但 geometry / material / texture 是共享引用 ⇒ 解码只发生一次,代价只有节点拷贝)。缓存键刻意不含 scale/rot/y 等纯摆位字段(摆位是 clone 后各自套的,换个位置不该触发重解码)。b. 骨架 rebind —— clone 出来的 SkinnedMesh 骨架仍指向原 bone,两条蒙皮会互相拉扯,所以按同名骨骼重建 Skeleton 并 bind()(有 SkeletonUtils 优先用官方实现,失败兜底不炸)。c. 让帧拆分 yieldFrame() —— 每「真解析」一次就 await 一个宏任务,总耗时不变但最长连续阻塞从 182ms 降到单个最大 36ms;用 MessageChannel 而非 setTimeout(0)(后者在后台标签页被钳到 1s+,会把加载拖死),不可用时降级微任务。d. 自检入口 ML.decodeStats() 暴露 hits/misses/workerOK(真机可查缓存是否生效)。⚠ COPIES 语义保留: monster:5 那种「预解析多份给对象池」是另一件事,现在只把「多份」从「多解析」改成「多克隆」(实测 COPIES 涉及的槽位当前 file 全为空,属死配置,但语义不能混)。验收 tools/r81w-decode-test.cjs 43/0;缓存前后实测(tools/r81w-cache-bench.cjs):同一批模型 cold 474.9ms → warm 31.7ms(16 hits / 12 misses,省 93%)。 |
| ★ 音乐卡点舞:狐狸旁再加一只外星人 · 去掉白亮流光壳 · v24r80 | 按用户口径「《Whiplash》音乐同步卡点跳舞 …… 复制外星人模型导入动作,去掉白亮的纹理,用原纹理就可以,放在狐狸旁边」做了三件事: ① 编舞数据(20 拍 @126BPM 4/4,9.52 秒)—— 新增 WhiplashDance:1-4 拍右脚踩点 / 左脚交叉小碎步 / 右前爪扣颈侧 / 猛甩头到左耳贴肩 / 双爪快抬 2 次;5-8 拍屈膝下蹲 / 胯部左→后→右三点绕环 / 双爪 W 型上抬 3 次 / 猫头左右小甩;9-12 拍跳成丁字步 / 右爪举左爪叉腰 / 顶胯 + 快速抖肩;13-16 拍踩步重心前移 / 摸头顶肩后拉 / 身体反 wave / 前爪前戳顶胯;17-20 拍左脚后撤坐胯定格 / 抬头眼神冷感 / 收尾干净利落。② 新写法「拍 + 姿态序列」—— 卡点舞用「拍」写比用秒写直观:clip 里写 unit:"beat" / bpm:126 / duration:20,关键帧写成 seq:[[拍号, 姿态名],…] + poses 姿态库(还支持 ext 继承片段、root / rootAdd 世界增量位移),由 js/wuxia.js 的 expand() 转置成逐骨轨道 —— 写的时候像动画师打点,运行时还是 three 要的逐骨 QuaternionKeyframeTrack。③ 手位靠反解、不靠试凑—— tools/wuxia-pose-lab.cjs --ik R:-11,150,2 在真骨架上网格搜索上臂/前臂的 (ry,rz),误差 <0.2 单位(身高 150)。⚠ 两条实测教训:骨骼旋转不会移动它自己的关节原点(Hips / Shoulder 的世界位置整段几乎不变,「用髋位置量顶胯」会得到 0.000);头的「面部朝向」是 X 轴不是 Z 轴(BASE 时 Y=头顶朝上、Z=模型右侧、X=+z 正前;Head:[+30,0,0] 是低头,所以 rx<0 才是抬头;Head:[0,0,+30] 是头向右倒)。④ 第二只外星人:新增装饰位 prop:gate_left2(同一个 alien.glb 的第二份实例,不复制二进制;每个槽位各自 parse 一份、互不干扰),就摆在狐狸(prop:gate_left)左手边再往外一个身位 —— 实测三者并排不重叠:舞者 x[-16.48..-12.52] / 外星人 [-11.58..-7.62] / 狐狸 [-6.76..-5.24],间隙 0.95m 与 0.87m。⑤ 去掉「白亮的纹理」:那层亮不是贴图( alien.glb 里 images: [] / textures: 0,贴图在 FBX→GLB 时被摘掉了),而是 v24r69 给全站统一加的 金属流光外壳(coat.kind=metal,纯白 bandColor + 菲涅尔边缘光)—— 所以修法是在 model-map 里删掉 coat 字段(不写 = 不加外壳),本槽位与 prop:court_front 一起摘掉,露出模型自带材质;其余 9 个槽位的金属流光一个没动(反证:MU 剑士那份仍在)。⚠ 踩点/坐胯的「沉」必须真的弯膝:只压 root 会把脚压进地面(实测第一版坐胯下沉 0.000m —— 因为只把左脚后撤、膝没弯,髋一点都不会降)。现在全段 1/60s 密采样脚陷地最多 3.7cm(草皮本身起伏 ±0.15m)。验收 tools/r80-whiplash-test.cjs 79/0。 |
| ★ 画面侧倾(camera banking / up 自转)· v24r82 | 参考项目 2026-10-06-08-18-04/messenger-copy 的 src/lib/messenger/camera.ts 里有一个 mu3d 从来没有的能力:up 向量参与旋转 —— 也就是「画面侧倾」。它为此绕开内置 camera.lookAt(),自己造矩阵:lookMatrix.lookAt(pos, declaredTarget, declaredUp) + quaternion.setFromRotationMatrix()。而 mu3d 的 up 恒为 (0,1,0),画面永远是水平的。① 等价但更省的做法:不重造矩阵,直接 camera.lookAt(...) 之后 camera.rotateZ(bank) —— 绕相机本地 Z 轴(= 视线轴)歪头。零分配、不改动 camera.up(真 THREE 实测确认)⇒ 不污染任何读 up 的代码,也不留跨帧状态(lookAt 每帧都 setFromRotationMatrix,rotateZ 不累积 —— 测试逐位验过)。② 方向契约(用真 THREE 量出来的):相机在 (0,0,10) 看原点时,rotateZ(+0.2) 让世界 +X 的点 NDC.y 由 0 → -0.1721(下沉)、-X 的点 → +0.1721(上升) ⇒ bank > 0 = 「画面右侧下沉」= 相机歪头向右(地平线左高右低)。换成正交相机后屏幕位移反算的倾角与输入角精确相等(1e-9)。③ 两条正交来源:lateral(沿镜头右向的移动速度 m/s)+ azVel(镜头水平转向率 d(cam.az)/dt),都做「向同侧倾」=压弯:右移 / 右转 → 画面右沉。⚠ 注意 cam.az 减小才是「视线右转」(fwd=(-sin az, -cos az) 顺时针),所以转向增益取负。④ 两条通道、两套解析解:持续项走临界阻尼弹簧解析解(与 js/camsmooth.js 同源:帧率无关、无条件稳定、无过冲);受击走一条独立的欠阻尼弹簧解析解(ζ=0.5)—— 给一个角速度冲击,画面「歪出去 → 回弹一次 → 归零」,3 秒后精确归零(不是 1e-9)。实测峰值命中闭式 t*=atan(ωd/λ)/ωd、回弹比命中 e^(-λπ/ωd)。⑤ 受击侧倾: damagePlayer() 增加可选的推力方向参数(三处调用点分别传「弹体飞行方向 / 怪→玩家 / Boss→玩家」),方向取推力在镜头右向的分量符号 —— 被打的一侧下沉。缺参数时按 +1 兜底,老调用点不受影响。⑥ 防晕与边界:舒适档( COMFORT 默认开)整体减半 —— 侧倾是旋转运动,最容易诱发不适;固定机位(?cam45=1)不侧倾;换图 / 复活时 reset() 并复位 camAzPrev(否则 cam.az 被重置那一下会算出巨大的 azVel);azVel 另限 ±6 rad/s。非 running(开场环绕)不给输入,免得还没进游戏画面就是歪的。⑦ 旋钮:v24r85 起默认关( ?bank1=1 才开)、?bank0=1 强制关;?bankflip=1 翻转手感方向;?banklat= / ?bankazv= / ?bankmax=(默认 0.20 rad ≈ 11.5°)/ ?bankw= / ?bankimpw= / ?bankcomp=。验收 tools/r82-bank-test.cjs 83/0(含真 THREE 的方向契约与角度精确性、帧率无关、解析峰值/回弹比、受击叠加、开关与降级)。 |
| ★ 健康自愈改口径:慢启动豁免 · v24r83 | 原来的健康检查是「15 秒还没就绪就注销 SW + 清空缓存 + location.replace('?fresh=1&v=…')」。它的初衷是治「旧 SW 拿旧 js 喂页面」—— 但那类失败属于「快速失败」(脚本瞬间返回却报错),根本不需要等 15 秒;而真正的慢启动会被这一条当场误杀。① 实测证据:软件渲染(SwiftShader —— 无独显的云电脑 / 虚拟机会走这条路)从零启动到 __MU_BOOT 可用要 38.32 秒,而 CDP 抓包显示页面在 39.12 秒被重载(Page.frameNavigated 捕到 2 次)—— 那个已经即将启动成功的会话被自愈干掉了(__MU_BOOT 于 38.32s 出现,自愈的 Promise 链 0.8s 后把它 replace 掉)。② 新判据(三条,按优先级): window.__BOOTING === true ⇒ 引擎正在按部就班地加载(boot.js 的 inject() 自带 15s×3 次超时兜底,真卡住它会自己把 __BOOTING 置 false)⇒ 只是慢,不打断;__BOOTING === false ⇒ 收工了却没交出 __MUAPI = 真失败 ⇒ 立刻自救;兜底超时默认 15s(老行为),软渲染自动放宽到 90s,?slowboot=<ms> 可手动覆盖。③ 自救「每会话最多一次」( sessionStorage 记 mu_selfheal_<pathname>):原来用 window.__muSelfHealed,它是随文档重置的 —— 重载后又拿到一次自救机会 ⇒ 慢机器上会「超时 → 清缓存 → 重载 → 再超时」无限循环;现在再失败就 fatal() 报可读原因,不再循环。启动成功会解除自救锁(下次真失败仍可自救)。④ 30 秒那条也加守卫:还在加载就不弹「启动超时」,否则软渲染 40s 的正常启动会被误报。 验收 tools/r83-slowboot-test.cjs(把 index.html 里这段脚本原样提取真跑,用假时钟模拟:慢启动不误杀 / 明确失败立刻救 / 每会话仅一次 / slowboot 与软渲染预算 / 成功后解锁)。 |
| ★ 相机不再左右摆 + 双指缩放稳定化 · v24r85 | 用户真机反馈两条:「摄像机走动时候左右摆动,这个绝对不可以」「用两指拉大和缩小视距,就要固定,不要一下子拉进一下子拉远」。 ① 走动左右摆 = v24r82 的相机侧倾(见下一条 r82 条目):它的两条持续项在走路时同时非零 —— 相机自动咬合( cam.az += CAM_SNAP·dt,把镜头追到角色朝向背后)让 cam.az 一直微调 ⇒ azVel 持续非零;横移又让 lateral 反复变号 ⇒ 画面被按出「走一下歪一下」。现在 cambank.js 的 ON 默认改成 false(要开必须显式 ?bank1=1;?bank0=1 仍可强制关,旧链接兼容)⇒ 不传参数一次都不歪(连受击脉冲也不给)。② 双指缩放「一下拉到底」的真凶是 Math.max(20, …) 那个兜底:旧代码只有一行 cam.dist = dist0 * d0 / d,而 d0 = Math.max(20, 两指距离) —— 两指并拢落地(真机极常见:同一只手两指一起按、或第二指刚触屏坐标还没稳)就把基准钉死在 20px,随后手指自然分开到 300px ⇒ 15 倍比例 ⇒ 视距一步顶到 42 或 10 米。另有两处火上浇油:基准取的是 cam.dist(目标臂长)而不是 camCtl.dist(实际臂长),弹簧(ω=18)没走完就按下第二指 ⇒ 按下瞬间跳一下、弹簧再追一下;手指抖动还 1:1 进目标值(无死区、无滤波)。现在改成「等分开 → 锁基准 → 低通 → 死区 → 限速」五步:两指间距低通后必须 ≥ PINCH_ARM(默认 96px)才建立基准(在那之前任何移动都不缩放);基准取实际臂长 ⇒ 锁定瞬间画面纹丝不动;距离低通 ?pinchlp=(0.45);比例偏离 1 不足 2% 时完全不动(放着不会自己漂);目标臂长每帧最多变 ?pinchvmax=(默认 14 m/s,全行程 10~42m 约 2.3s)⇒ 手势再猛也不会一帧到位。之后仍走原有的临界阻尼弹簧缓入缓出。刻意不加惯性 —— 要的是「松手就固定住」,不是甩一下再滑一段。?pinch0=1 直接禁用双指缩放。③ 顺手修掉一个假开关:r84 面板的「侧倾:开/关」改的是 MUCamBank.cfg(模块级 URL 快照),而 push() / kick() 判断的是 create() 内部那份实例 cfg —— 两个不同对象,点按钮根本改不动运行时(界面文字变了、画面不变)。现在面板读 camBank.cfg(实例),按钮才真正生效。验收 tools/r85-pinch-bank-test.cjs;r82 侧倾测试的默认值断言同步改成「默认关 / ?bank1=1 才开」。 |
| ★ 新增「✨ 新功能」面板(把 r81~r83 的能力列举出来 + 现场演示)· v24r84 | 用户口径:「把新加的功能、相机功能,举个例子在里面列举出来」。于是做了一个游戏内面板:把近三轮的新增能力逐条列出,每条都配一段「举例」,并且每项都配了能直接点的按钮 —— 不用翻文档就能看到效果。 入口:顶部 ✨ 新功能 按钮 · 键盘 V · 开始界面也写了提示。 ① 相机侧倾:【侧倾:开/关】直接改 MUCamBank.cfg.ON(push() 与 kick() 都是按帧读它,所以运行时切换立即生效);【演示 · 向右压弯】/【向左压弯】调 camBank.kick(3.5, ±1) 现场注入受击脉冲 —— 就是被打中那一下的「歪出去 → 回弹一次 → 3 秒精确归零」。② 3D LUT 调色:走既有的 cycleLUT()(与 L 键同一条路径),按钮文案实时显示当前档位(读 LUT.resolve(),中文名用既有的 LUT_LABEL)。③ 屏幕空间描边:调 __POST.fx.outline(0.85 / 0) 实时开关;可用性判据读 window.__POSTQ.outline(管线自检探针),不可用时按钮直接说明「本机走几何壳兜底」,不会假装开成功。④ 模型解码缓存:读 ML.decodeStats() 打印 misses / hits / 复用率 / 缓存条目 / 槽位数 / Worker 可用性 —— 把 r81 的「474.9ms → 31.7ms、省 93%」摆在同一屏对照。⑤ 运行环境与慢启动豁免:读 window.__POSTQ 与 window.__BOOTING,报渲染路径(软/硬)、画质档、后处理是否降级、描边与 AO 状态、以及引擎是否仍在加载。⚠ 实现取舍:面板不去读 window.__POST.uniforms「猜」状态(描边强度并不在对外 uniforms 里),而是走对外 API + 自检探针两条稳定入口;按钮文案统一由 syncAll() 刷新,键盘 V 打开时也调一次(导出为 window.__MU_SYNC_NEW)。⚠ 顺手把 tools/r83-slowboot-test.cjs 的版本戳断言改成不写死版本号:原来写的是 vSw === 'v24r83',每轮升版必然假红(与 r70 那条「CHANGED ≤8 条」是同一类脆弱写法);现在改成「所有戳互相一致,且都等于 sw.js 的 VER」—— 升版天然通过,漏改照样抓得住。 |
| ★ 相机撞到物体拉进后,慢慢恢复原来的视距 · v24r87 | 用户口径:「相机撞到物体拉进后,慢慢恢复原来的视距」。 ① 病根:r73 为了治「一下子拉近一下子拉远」,把摇臂的「收」与「放」做成同刚(都 13 = 对称)—— 这在「收」这一侧是对的(撞过来必须让得快,才有安全余量),但在「放」这一侧就成了「障碍一走开、画面唰一下弹回去」:19m 臂长上从 5.7m 弹回原长,峰值速度 17.64 m/s(≈63 km/h)、95% 只要 1.32s、26% 只要 0.28s。 ② 改法:两个方向拆成两个刚度 —— KIN 仍 13(ω≈3.61 / τ≈0.28s,收侧一行未动,序列与 r73 逐位相同),KOUT 13 → 2.5(ω≈1.58 / τ≈0.63s)。这是真实摇臂的脾气:收臂要躲墙,急;放臂只是回位,不急。③ 实测对照( tools/r87-cam-release-test.cjs 把临界阻尼闭式解逐条跑出来,19m 臂长 0.30 → 1.00):峰值速度 17.64 → 7.74 m/s(砍 56%);到位曲线 0.63s 26% / 1.27s 59% / 1.90s 80% / 95% 3.02s(旧 1.32s)/ 98% 3.70s(旧 1.63s)—— 放远时间 ×2.29;前段慢慢起头、尾巴拖得长,且 40s 内精确收敛回 1.000000000。两个方向都仍是临界阻尼解析解(帧率无关、单调、不过冲、无反向),所以慢下来不会带回 r71 那种「一快一慢的节奏差」。④ 附赠的稳定性:沿墙走过 0.4s 的门洞缺口时,旧版会先放出 42.3%(4.6m)又被墙顶回去 —— 这就是「一起一落」的来源;新版只放 13.3%(抖动幅度 ×0.31),走完缺口几乎看不出相机动过。 ⑤ 为什么用「降刚度」而不是「限速」:解析解对任何 dt 都精确、连续、不过冲;限速会制造「匀速直线 + 折角」,那正是 r71 被否掉的味道(r73 也正是因此把 REL 归零)。?camhold / ?camrel 两个旋钮仍留着,默认都为 0。⑥ 现场可 A/B:游戏内按 V → 【演示 · 撞墙拉进 → 慢慢恢复】把「背后有墙」假装 1.2s 再放开,走的是真实通路( camDemoPull() 只覆盖射线给出的 want,迟滞死区与弹簧原样生效);【查看当前参数】直接打印当前 KIN/KOUT/ω/τ、峰值速度与到位时间。?camcolkout=1 更慢(95% 要 4.75s)、?camcolkout=13 回到旧版对称手感。验收 tools/r87-cam-release-test.cjs 63/0(默认值 / 新旧对照 / 收侧逐位未变 / 沿墙缺口 / 曲线性质 / 旋钮与接线 六组);r73 那两条「KIN === KOUT」的断言按新口径翻正(换成「收 13 / 放 2.5 + 放远比拉近软 + 时间常数 ×2.3」,并保留「r73 的对称仍可显式开回」),68/0 —— 不是删断言。 |
★ 冰系技能调节器 icestorm.html · v24r86 | 用户口径:「有冰风暴技能调节器吗」—— 之前只有龙卷风有调节器(tornado.html),冰系三个技能(冰风谷专属)全都没有。本轮补上。① 一个页面管三个技能:页签切「极光风暴(冰风暴)」( VFX.crystalStorm 持续型,绕体公转+自转+浮动+概率拖尾)、「冰霜新星」(3 波 frostWave + frostDecal 霜环 + 9 根 iceSpike + burst)、「绝对零度」(霜环 + 光柱 + 4 波冰波 + 30 根沿螺旋延迟升起的冰刺 + shards/mist)。② 与游戏同口径:三个 cast*() 是照抄 game.js 技能表分支写的,默认值一个没改(极光风暴 10 枚 / 0x7dffd4 / 半径 5.6 / 10 秒;冰霜新星 半径 12 / 3 波 × 110ms / 波强 1.3 / 刺高 1.8~4.0;绝对零度 半径 24 / 刺高 2.6~7.0 / 铺开 0.55s / 光柱 22m / 螺旋 3.1 圈)⇒ 页面上一个数不动时放出来的就是游戏里的原样。滑块走的是 DEFS 声明表(key/名/min/max/step/小数位),加一个旋钮只要加一行。③ 顺带给 VFX.crystalStorm 开了旋钮:签名从 (count, colorHex) 扩到 (count, colorHex, opt) —— r/rJit/h/hJit/size/orbit/bob/spin/emissive/opacity/light/lightDist/trail/trailSize/trailLife 共 15 个;一个字段都不给 = 与 r86 之前逐字节同行为(游戏里就是不给),并且点光高度从写死的 1.6 改成 h+0.8(缺省 h=0.8 ⇒ 还是 1.6)。④ 顺手修了一个真漏: crystalStorm().dispose() 原来只把 group 从场景摘掉,OctahedronGeometry 与材质从不解 ⇒ 冰风谷每放一次「极光风暴」就漏 1 份几何 + 1 个材质。现在连几何带材质一起还回去,并加 dead 守卫防重复释放(与 tornado / iceberg 的 userData.onVfxDone 同一口径)。⑤ 零外部资源:只引 three.iife.js + vfx.js(与游戏共用同一份特效代码);天空/冰原/法师/尺度参照全是 canvas 与基础几何现搓;地面画了 5.6 / 12 / 24 三道「作用半径」圈(就是三个技能的 radius)+ 一圈冰柱,一眼能看出 blast 到哪;另有实时地环跟着作用半径缩放。验收 tools/r86-icestorm-test.cjs 56/0(默认口径逐项核对 / 旋钮透传 / 公转·浮动·自转实测 / dispose 真收 1+1 且重复调被守卫挡住 / 页面零外部资源与 API 存在性 / inline 脚本可编译 / 离线清单收录)。 |
| ★ 法师「程序龙卷风」技能(纯程序化:零贴图 / 零模型)· v24r86 | 用户口径:「给法师做个程序龙卷风技能,做好放效果出来展示下」。法师(前庭右侧槽位 prop:court_right2)原本打的是 r77 的黑龙波(几道半透明黑圈),现在换成漏斗状气旋。① 零外部资源 = 「程序」的全部含义:整条龙卷风只用 LatheGeometry(旋转体漏斗)+ TorusGeometry(环带,**默认关**,见 ⑧)+ CylinderGeometry(扬尘裙)+ RingGeometry(贴地尘环);气流纹路是 js/vfx.js 里现搓的一张 256×256 canvas 贴图(44 条竖向亮条 + 越往上越密的横向细纹,RepeatWrapping,全局单例只生成一次),不读任何 png / glb。② 剖面用幂函数而不是圆锥: r(u) = R·(0.2 + (top−0.2)·u1.9)(u=0 底 / 1 顶)—— 指数 1.9 让底部收成细颈、往上迅速张开,才是漏斗(真龙卷风也是「上宽下窄 + 底部细颈」);实测顶部半径是底部的 13.5 倍。③ 三层壳差速自转 + uv 螺旋偏移:同一份几何跑三遍(外壳 1.16× 近不透明 NormalBlending / 中壳 1.0× additive / 内芯 0.62× additive 转速最快),把 uv 的 x 加上 0.55·uv.y ⇒ 静态就是缠绕状;三层转速不同才有体积感,否则只是「一张贴图在转」。④ 向心螺线粒子:从既有粒子池发射,径向速度取负(向心)、切向为正(随气旋转)、并叠一个沿高度的上升分量 —— 看上去就是「尘被卷进去再甩上来」;发射密度按 Math.pow(rand,1.4) 偏置 ⇒ 底部比顶部密。⑤ 收场干净:底部扬尘裙 + 2 个贴地尘环 + 低空碎屑(带 floor 弹跳);全程自收 4 份几何 + 6 个材质(环带关掉后的实数;开着是 9 + 7),且 Group 没有 geometry,所以按项目既有约定在 VFX.add 的 ttl 回调里自己 cleanup()(否则每放一次就漏一份)。⑥ 配置即旋钮、改一行就能回退: model-map.json 的 prop:court_right2 写 fx:"tornado",配套 fxTall / fxR / fxTop / fxSpin / fxSpeed / fxBands / fxAhead / fxColor / fxColor2 / fxAlpha / fxDensity / fxDebris / fxTtl;出手点用 root.userData.modelRoot 的世界四元数算朝向(外壳 Group 的四元数恒为单位,直接用会朝错),位置沿朝向前推 fxAhead。把 fx 改回 "dragonWave" 就退回黑龙波 —— dragonWave 本体与它那三个旋钮(fxLen/fxRings/fxRingN)一行未删,回归测试里现场用 ML.config() 切回去验过。⑦ 展示页 tornado.html(同样零外部资源,只引 three.iife.js + vfx.js,与游戏共用同一份特效代码):程序化天空/草地/石柱与一个程序化法师,面板可拖动调高度 / 底部半径 / 顶部倍数 / 自转 / 前进 / 透明度 / 尘量,另有「自动循环 / 原地卷 / 碎屑」开关与 4 个预设机位;手机可直接打开(局域网地址 http://<本机IP>:8188/tornado.html)。⑧ 环带默认关(同日追加口径「龙卷风不要四个圈圈」):原先默认 5 道 TorusGeometry 圆环横套在漏斗上(越往上转得越快),远看像给龙卷风套了几个呼啦圈,把气流本身的螺旋感压住了 ⇒ fxBands:0,现在一道都不画。VFX.tornado 里改成 bandN>0 才建环带材质与网格(0 时连材质都不建,不空转)——代码一行未删,想再要那几道圈只把 fxBands 改成 >0,验收 G 组专门用 bands:3 守住这条能力(默认关 ≠ 删能力)。⚠ 贴地那两个 RingGeometry 尘环不是「圈圈」,是把它按在地上的接地阴影,保留。验收 tools/r86-tornado-test.cjs 77/0(造型 / 运动 / 粒子 / 收场 / 接线 / 配置与回退 / 能力保留 七组,跑在真 three 沙箱里:三层差速、uv 螺旋、粒子向心速度符号、scene.children 只剩 4 个池 + 1 个天气点云、几何与材质计数、旋钮逐个透传);r76 / r77 两套黑龙波测试口径同步迁移,42/0 与 39/0。 |
★ 冰风谷战士技能调节台 warrior.html · v24r88 | 本行只做索引(v24r88 这一轮是同工作区多路并行改的,这条由另一路产出,细节以页面本身与验收为准):冰风谷战士的近战技能调参台,页内直连 VFX.slashArc / burst / frostWave / iceSpike / iceberg / mist(旋风斩 / 剑气斩 / 冰霜新星 / 冰山 等),零外部资源、与游戏共用同一份 js/vfx.js;验收 tools/r88-warrior-test.cjs 59/0,已列入 sw.js 的 CHANGED 与回归清单 outputs/run_mu3d_regression.sh。⚠ 与上一行的 icestorm.html 只有「冰霜新星」一项重叠(本页覆盖战士近战系 + 冰山;「极光风暴 / 绝对零度」只在 icestorm.html 里有)—— 要不要合并由用户定。⚠ 本条是 r30 「版本备忘录必须记最新一轮」那条断言逼出来的(升三处戳时忘了补行);若另一路补了自己的详细行,把本索引行删掉即可。 |
| ★ 武者「程序激光」技能(1 秒向前发射 · 全程序化)· v24r89 | 用户口径:「做个激光一样的技能 1秒向前发射」,随后给了一张奇迹MU 的截图:「像这样子的激光束」。① 技能本体 = VFX.laser()(js/vfx.js):三层同心圆柱 —— 白热内芯(0.16m)+ 主色外晕(0.62m,贴现搓的流动亮丝贴图 beamTex)+ 深蓝外层(1.30m),全部 AdditiveBlending(亮色能量体就该加法混合;黑体才切 Normal,dragonWave 的教训反过来用);枪口/命中各一颗亮球。② 「1 秒」就写在缺省里:opt.ttl 默认 1.0s;节奏 = 前 10% 从枪口「刺」到全长(ease-out 2.4,快但可见推进 =「发射」的读法)→ 中段 68% 满长保持 + 每帧 ±15% 高频闪烁(激光的电流感)→ 最后 22% 快速熄灭。③ 参考图那三件「像不像」的关键(v24r89 第二版按用户截图改口径):枪口爆闪(两枚 billboard 光团:白热核心 + 主色外晕,尺寸跟着半径走 ⇒ 那团「从枪口炸开的光」)、体积光团(opt.vol 12 枚光团绕束轴螺旋、沿束往末端流动 ⇒ 不是一根硬管子,是「里面有东西在跑」的等离子柱)、斜向下砸地 + 贴地光斑(opt.down ⇒ 俯角 asin(down/len),束端正好砸在地面并铺一枚世界水平的光斑 + 向上迸的溅射)。⚠ 两个坑:俯角必须用 'YXZ' 欧拉序(先俯后偏航,默认 XYZ 会让俯角绕世界 X 轴、方向直接歪掉);光斑要用 RX(−π/2 − tilt) 抵消整组俯角(否则会跟着束一起斜插进地里)。④ 命中读法:束第一次到全长时在末端打一次爆点,之后每帧从末端往枪口方向回溅火花(速度取负 =「打在东西上溅回来」);沿束另有电离微粒。⑤ 挂在前庭右侧的带骨骼武者(prop:court_right3,原本无技能):skill+autoCast 进场自动放,发射方向 = 模型世界朝向(rotY −90° ⇒ 朝中轴大道 −X),发射口 = 脚底前移 fxAhead 0.7m 再抬到 fxH 1.2m 胸口;束长 fxLen 12m、冰蓝白 fxColor 0x8fd8ff / 外层 fxHalo 0x2f6fd0,fxDown:true = 落到脚下地面(下沉量自动取发射口高度);想换人/换色/换时长改 model-map 一行即可。⑥ 展示页 laser.html(与游戏共用同一份 vfx.js,零外部资源):束长/外晕半径/体积光团/枪口爆闪/末端下沉/持续/透明度/火花 八个滑条 + 冰蓝白·赤红·翠绿·紫电四色 + 一键「参考图」预设。⑦ 收场自收 4 份几何 + 6 个材质(开贴地光斑时 5 + 7)(userData.onVfxDone 口径,与 tornado 同一套 —— VFX.update 只 dispose mesh.geometry,Group 没有 ⇒ 必须自己收;⚠ Sprite 用的是 three 内部共享几何,绝不能 dispose)。验收 tools/r89-laser-test.cjs 73/0。⑧ 同日第三版又按三句口径收了一次:「我只要中间最亮那条 / 边缘加点泛光 / 尽头的圆球不要 / 激光不要抖动太大」⇒ (a) 末端那颗白亮球删了(它原先会把视觉焦点钉在落点,中间的「最亮那条」反而没了主体感;落地那一摊仍由贴地光斑 + 回溅火花读出来 —— 现在整束只剩枪口那一颗球);(b) fxVol 由 12 改 0(12 枚螺旋体积光团会把束读成一根花管子;旋钮没删,给个数就回来);(c) 三层亮度重配比 —— 白热芯 .42→.62、外晕 .40→.34、最外层 .30→.18 ⇒ 中间那条最亮、边上只剩一层淡泛光;(d) 抖动/闪烁从 ±15% 压到 ±6%(再抖就不是激光是电焊)。(造型 / 1 秒节奏 / 命中回溅 / 参考图口径 / 收场 / 真赛场接线 / 配置回退与版本戳 七组)。⚠ 视觉验收不能靠截图对比:柱高是 Math.random(),跨会话比对无效 ⇒ 必须同会话 A/B(先轮询 VFX.meshes.length===0 拍「无束」参考帧,再点施法拍「有束」帧,再差值)。本机实测:远端机位 青蓝 61% / 白芯 15% / 蓝 18%(早前把内芯调太亮会整根过曝成白胶囊:白 34% ⇒ 已把内芯 .68→.42、外晕 .52→.40、爆闪 .70→.55)。 |
| ★ 仓库系统(储物箱)· v24r89 | 用户口径:「弄个仓库系统,可以存东西」。 ① 病根:mu3d 原先只有「背包 20 格」,且是硬上限 —— autoEquip() 里满了就当场丢弃(「背包已满,××已丢弃」)。而刷怪爆装正是这游戏的核心循环,20 格根本装不下「好东西留着、垃圾卖掉」这个动作,玩家只能被迫卖掉本来想留的装备。② 口径(照 MU 原版脾气):容量 60 格 = 10 列 × 6 行(与背包同列数,视觉上就是「背包的楼上」,不引入第二套网格坐标);仓库是冷存储 —— 一键最强 / 自动装备都只看背包(免得没穿的东西偷偷生效);账号级共享(不分职业,换角色还能看见自己存的东西 —— 这正是 MU 的体验,小号给大号存装备),物品自带 cls 字段所以不会混淆;持久化到 localStorage(key mu_stash)—— 这一条是仓库的立身之本,不能存的仓库等于没仓库。③ 架构:新模块 js/stash.js 不认识 DOM、也不认识玩家对象:它只知道自己是一个「有容量上限的物品数组」,所有搬运都是纯数组操作 + 结构化返回值(store/fetch/storeAll/fetchAll → {ok|moved, reason}),toast / 重渲染 / 音效一律留给 game.js。于是这些边界能在 node 的 vm 沙箱里真跑(满仓、空仓、「全部取回」被背包 20 格截断),不用碰浏览器 —— 与 camcol / camsmooth / cambank 同一套工程习惯。④ 物品清洗 cleanItem(防炸):仓库要写进 localStorage,而 localStorage 里的东西不可信(旧档、手改档、同名 key 残渣)。读档与存入一律过清洗:字段类型收敛、slot 不在白名单直接丢、rarity 夹到 0~3、itemLv ≥1、颜色只认 #RRGGBB;缺 power 时按 config.js 的同一公式补算(少了它排序会把好东西当 0 丢到最后,靠它挑装备的玩家就被骗了);存量超过容量时超出部分丢弃。⑤ 整理两种排法: 整理(按类型) = 槽位分组 → 品质↓ → 战力↓ → 等级↓ → 名称;按战力排 = 不分类型,纯战力降序。返回新数组不改原数组,并带「原始序号」兜底保证同键稳定(老引擎的 sort 不保证稳定)。⑥ 面板(右侧 🏦 仓库 按钮 / 快捷键 K):上半是仓库网格、下半是背包网格(同屏对照,才能双向点);点背包格 = 存入、点仓库格 = 取回;四个批量按钮(全部存入 / 全部取回 / 整理 / 按战力排)+ 实时容量计数(满仓变红);格子右下角标等级,鼠标悬停给完整名称。竖屏与横屏各自走 max-height + overflow,绝不撑破屏幕。⑦ 两个旋钮: ?stash=80 调容量(8~120,面板格数跟着变)· ?stashauto=1 让「背包满时自动存进仓库」(默认关 —— 关着=沿用旧的丢弃行为,不偷偷改变既有手感)· ?stash0=1 整体关掉。验收 tools/r89-stash-test.cjs:容量边界 / 搬运语义(含背包 20 上限截断)/ 排序稳定性与分组 / 序列化往返与坏档清洗 / URL 容量 / 接线(boot.js 注入顺序、面板节点、K 键、sw.js CHANGED、三处版本戳一致)。 |
| ★ 蓝黑龙波(MU 经典龙波)· v24r90 | 本行只做索引(v24r90 / v24r91 这两轮同样是同工作区多路并行产出,细节以页面本身与验收为准):龙系技能添了新形态 —— ① v24r90 蓝黑龙波:js/vfx.js 新增 VFX.dragonSurge(),一条侧视的节段长龙从施法者身上向前冲出去(头 + 锥吻 + 下颚 + 双角 + 两根长龙须 + 背鳍 + 双翼 + 四爪;龙身 15 节共用一份几何、按 sin(t·ω − u·k) 相位差扭着走 ⇒ 头先动、尾跟上,不是整条刚性平移),蓝黑配色、只有眼睛 / 翼膜 / 须尖走加法混合(龙体必须普通混合,否则蓝黑 = 加 0 = 看不见)。展示台 dragonwave.html(与游戏共用同一份 vfx.js,零外部资源;可调体长 / 体粗 / 节数 5~30 / 离地高 / 速度 / 波动 / 摆幅 / 配色)。验收 tools/r90-dragonwave-test.cjs 53/0。② v24r91 水墨群龙:给 dragonSurge 加参数(墨色浓淡不匀 + 墨晕壳 / 正弦蛇形航线会拐弯 / dragonSurgeGroup 五条按横向·高度·朝向·相位四维散布 / 三频叠加的若隐若现),不给参数就是 r90 逐字节原样;展示台 dragonink.html,验收 tools/r91-dragonink-test.cjs 64/0。⚠ 本索引行是按 r30「版本备忘录必须记最新一轮」那条断言补的(升三处戳时不能只改戳);若产出这两轮的会话补了自己的详细行,把本行删掉即可。 |
| ★ 激光束「芯增大 3 倍 + 去掉边缘透明部分」· v24r92 | 用户口径:「激光束技能,中间亮的那条芯增大3倍,不要边缘透明部分」。① 芯 ×3:VFX.laser 新增 opt.coreK(默认 3)⇒ 内芯半径 0.16 → 0.48,给 coreK:1 就回到 v24r89 的老粗细;② 不要边缘:外晕(0.62)与最外层(1.30)那两层半透明的边默认不建(连材质都不建,不空转)⇒ 束就是一根干净的白热芯,edge: true 才把三层壳叫回来(能力没删,不是删代码);③ 芯亮度 0.62 → 0.95(原来靠外面两层加法堆出「过曝」,现在就剩它自己,不提亮会发灰);④ opt.glow 保留为尺度基准 —— 枪口球 / 爆闪 / 贴地光斑都按它缩放,不能跟着外层一起归零;⑤ 槽位旋钮 fxCoreK / fxEdge 由 modelloader 透传(不给 = 走缺省 ⇒ 改 model-map 一行即可)。⑥ 收场口径随层数变:默认 2 几何 + 4 材质、砸地配置 3 + 5(edge:true 才回到 4 + 6 / 5 + 7)。验收 tools/r89-laser-test.cjs 80/0 —— 新增「edge:true + coreK:1 ⇒ v24r89 老造型 0.16 / 0.62 / 1.30 逐字节复现」这条回退断言。⚠ 只动了 js/vfx.js + js/modelloader.js,武者的「1 秒向前发射 / 命中回溅 / 砸地光斑 / 枪口爆闪」一项没碰。 |
| ★ NPC 的 AI 语音(靠近自动招呼 + Enter 打字追问)· v24r93 | 用户口径:「mu3d NPC 接 AI 语音,靠近自动说话,可以打字追问」。小镇里那套居民 NPC(js/lorencia.js 的 buildNPCs(),只在 ?oldtown=1 出现)原来只是站着不动的模型 + 头顶名字牌,一句声都没有 —— 这一轮给它们接上真正的嘴。① 谁在说: js/npcvoice.js(新模块)—— 走进 4.2 米 最近的 NPC 主动说一句;走远到 7 米之外才「重新武装」(否则站在旁边会一直重说);同一个 NPC 两次开口至少隔 30 秒;同一时刻只允许一个 NPC 说话(否则几个 NPC 一起响成一锅粥);每个 NPC 各记一份对话,离开 90 秒后忘掉(下次是「又见面了」)。按 Enter 打开输入框打字追问,Esc 关掉;打字时游戏按键被屏蔽(否则输入 f 会顺手捡装备)。② 说什么:豆包大模型(火山方舟)—— system prompt 由服务端按 NPC 身份拼:铁匠汉斯粗声粗气、商人帕特爱推销、守卫公事公办、村长稳重指路、祭司温柔祈福、吟游诗人押韵讲传闻,并把玩家的等级 / 生命 / 金币 / 当前地图一并喂进去,所以招呼是「看着你」说的。招呼限 40 字、追问限 60 字,服务端还会把引号、换行、markdown 削掉。 ③ 怎么出声:豆包语音合成 2.0( /api/v3/tts/unidirectional,新版控制台 X-Api-Key + X-Api-Resource-Id: seed-tts-2.0)—— 返回是 NDJSON,逐行 {"code":0,"data":"<base64 音频分片>"},code:20000000 收尾,分片解码拼起来就是完整 mp3,浏览器直接播。④ 关键取舍:密钥绝不进浏览器。台词与音频都走本地服务 serve.py 新增的代理端点(POST /api/npc/say / GET /api/npc/status)—— 一来前端 js 里带 key 等于送给任何人,二来浏览器直连火山域名会被 CORS 拦,同源转发两个问题一起解决。密钥只写在 voice-config.json(服务端读,改完不用重启),两个上游分别鉴权(对话 Bearer、发声 X-Api-Key),别混。⑤ 不破坏便携离线:没配密钥 / 断网 / 服务没起 —— 一律退回内置台词库(每个身份三句)+ 字幕,并用浏览器自带朗读兜底,字幕框还会变色以示「这是兜底台词」; --no-voice 可整个关掉。这样云电脑不带网络时,「走近有人说话」这件事也不会消失。⚠ 顺带: lorencia.js 的 npc() 现在会挂 userData.npcKind/npcName(以前身份只活在工厂函数的局部变量里,外部拿不到)。验收 tools/r93-npcvoice-test.cjs。 |
| ★ 修复:小镇居民全都定格不动(城门守卫「卡住了」)· v24r94 | 用户口径:「城门守卫卡住了」。查下来不是语音模块的问题 —— 是 js/lorencia.js 的 buildNPCs() 里一句提前 return g; 把整段待机动画的登记跳过了:if (list) { for (...) put(...); return g; } —— 数据驱动分支直接返回,后面的 g.userData.animates = animates; 从来没执行到。而 game.js 是逐帧遍历 worldGroup.userData.animates 来推进的,收不到任何一项 ⇒ 场上 10 个居民全部定格:不呼吸、不转头、铁匠不抡锤、城门守卫杵着不动。为什么一直没被发现:这条 bug 只走实机路径( data/town.json 有 npcs,WorldData 就绪);硬编码兜底那条分支反倒一切正常 —— 所以验收必须两条路径都跑,只测一条必然漏(见 tools/r94-npc-anim-test.cjs)。修法:把 if (list) { … return g; } 改成 if (list) { … } else { … },让 animates / animate 的登记落在两条分支之外,必然执行。顺带排除的另一种可能:守卫被嵌进城门模型里 —— models.js 的 gate() 只有 1.7m 厚、站在 z=38,守卫在 z=34,不重叠,不是这个原因。⚠ r93 挂的 userData.npcKind / npcName 身份标签没被碰坏(回归里专门盯了城门守卫)。 |
| ★ 手机能开对话了 + 风暴城的「模型」变成 NPC · v24r95 | 用户口径:「手机端可以语音打字」「回到原来风暴城,里面的模型改成 npc」。 ① 手机原来根本开不了对话。 openAsk() 只由 Enter 触发 —— 手机没有 Enter 键,提示条写着「按 Enter 说话」却无处可按,移动端等于打不开。现在提示条本身就是按钮(#npcPrompt.in 恢复 pointer-events:auto,触屏文案改成「💬 点这里 与「××」说话」);对话框补了 ✕ 关闭按钮(手机也没有 Esc 键,旧版只能靠「发送空内容」退出);输入框加 enterkeyhint="send"(软键盘右下角变「发送」)。② 软键盘会盖住输入框。输入框是 position:fixed 贴**视口**底部的,键盘一弹「视口底部」就是键盘下沿 —— 输入框整个被盖住,玩家看到的画面是「点了没反应」。现在用 visualViewport 算出键盘高度把它顶到键盘之上(resize 与 scroll 都要听:iOS 上键盘弹出只触发 scroll)。触屏下输入框字号提到 16px —— 小于 16px 时 iOS 聚焦会自动放大整页。③ 中文输入法会抢回车。选字时按回车是「确认候选词」不是「发送」,不挡的话「这城里有」没选完就被发出去 ⇒ 加了 isComposing / keyCode 229 守卫。④ 风暴城里的不是 NPC,是装饰道具。要塞前庭那些人物( prop:court_* / gate_*)是 model-map.json 挂上去的 .glb 外部模型:剑士 mu_knight、剑姬 swordmaid、守卫石像 guard_statue、法师 mage、仙灵 fairy/fairy_real、武者 rigged_fighter、异客 alien —— 对渲染管线它们只是道具,没身份、没名字牌,语音模块认不出来。新增 js/npcroles.js(「模型文件 → 这个角色是谁」表),stormwind.js 挂完模型顺手写 userData.npcKind/npcName/npcSlot;✕ 狐狸 Fox 与机器人 RobotExpressive 在显式排除名单里(不让宠物开口说人话)。 |
| ★ 修复:外部模型「偶发加载不出来」(卡在让帧上,永不落地)· v24r96 | 这一轮是给「风暴城模型 → NPC」做真机验收时查出来的真 bug,正对上用户以前反复报的「模型/流体神秘不见了,刷新一下又好了」。 ① 症状:真进游戏跑一遍,14 个外部槽位稳定停在 11 个再也不动,而 ML.status().failed 是空的 —— 既不报错、也不出现,模型就「消失」了。② 根因: js/modelloader.js 的 yieldFrame()(v24r81 引入,用来「每解析完一个模型就让一帧、别把主线程连成一长条同步阻塞」)。旧实现是每次调用都重写回调:_yieldChan.port1.onmessage = function(){ res(); } —— 这个赋值会把上一次调用留下的 res 顶掉。而 ML.preload() 是一次性把 14 个槽位的 ensure() 并发抛出去的,任何一个 parse 未命中都会走到这里,于是很容易出现「两个 yieldFrame 落在同一批微任务里」:A:onmessage=resA; post(msgA) → B:onmessage=resB; post(msgB) ⇒ 派发 msgA 时执行的是 resB,而派发 msgB 时 resB 已经完了 ⇒ A 的 Promise 永远不 settle,A 所在的 ensure() 就永远停在 await yieldFrame():那个槽位永远不进 state.cache。③ 最小复现(照抄旧实现,5 行):5 个并发 yieldFrame() → 只有最后 1 个settle,前 4 个挂死。软件渲染(无独显云电脑 / 虚拟机)parse 慢、撞车概率高,14 个槽位稳定卡 3~5 个;真机偶发(多个 parse 在同一批微任务里完成就撞上)—— 这就是「刷新一下又好了」的全貌。④ 修法:改成一个常驻 handler + 先入先出队列( _yieldQ),谁调谁拿自己的 resolve,与 postMessage 顺序一一对应。⑤ 顺带查清的一件事(不是产品 bug):自动化测试用全新浏览器 profile 时, index.html 末尾的 controllerchange → location.reload()(SW 首次接手)会正好落在 ML.preload() 跑到一半的位置,把整个预载结果连文档一起冲掉,看起来就像「外部模型压根挂不上」。真机上 SW 早已装好、controller 非空,永远不会再触发。已把「等稳定文档(等 SW 接管 + 观察文档标记)」写进 tools/r95-live-smoke.cjs,并加了「25s 预载预算超时后剩下的槽位还会继续落地」的轮询。验收: tools/r81w-decode-test.cjs 新增「C2. 让帧层并发安全」6 条(49/49);tools/r95-live-smoke.cjs 真进游戏(真 WebGL + 点职业卡 + 等 14 个模型落地 + 9 个 NPC)。 |
| ★ 输入面板压成「九宫格」并躲到右边角落 · v24r102 | 用户口径:「键盘挡住输入窗口了,把键盘压缩成相当于竖屏9宫格放右边角落」。澄清后:要压缩的是这条 NPC 输入面板本身(不是系统输入法、不是 HUD 技能键)。 ① 挡住有两层原因。一是面板原本是 min(rot-h − 24px, 620px) 的长板(手机上等于横贯整屏);二是更要命的——微信 X5 / 部分安卓 WebView 压根不上报 visualViewport(键盘引起的视口变化它不报)⇒ 键盘高算出 0 ⇒ 面板按「键盘没弹」摆放,停在屏幕底部,被输入法整块盖住。后一条自测测不出来,是玩家在手机上撞出来的。② 压缩 + 挪窝。长度砍到 min(rot-h × 0.58, 300px)(视觉体量 ≈ 竖屏九宫格键盘那一小坨),内部改成两行——上行 输入框+✕,下行 🎤+发送——不再靠拉长来塞按钮;位置从「左右居中」挪到贴物理右边缘(top: var(--ask-corner)),落在右上角那块地方,键盘再高也够不着。纵向仍跟键盘上沿走(--ask-inset)。③ 兜底(本轮正主)。只要输入框处于编辑态而键盘高仍测不到,就按 innerH × 0.45 估一个(宁高勿低:估高只是往上抬一点,估低就被压住),并在 focus/blur 后补打 120/400/800ms 三次重算(键盘有弹出动画,事件常晚一步)。验收: tools/r102-ask-compact-live.cjs 16/16(真页面 + 安卓 UA + 412×915 竖屏 + 触摸;量到长度 239、厚度 102(两行生效)、rect.right=400(贴右 12px);★ 无头里天然就是「visualViewport 不报键盘」⇒ 正好复现微信那个场景,量到 --ask-inset=422px、面板底边 493,整块抬到预估键盘之上)。 |
| ★ 视频接近「自愈起播」+ 装备流光扫光 · v24r183 | 用户口径两条:「里面的视频流 靠近不播放」「叫你把刀赋予一层闪闪发光的效果,没叫你搞几个萤火虫在那飞」。 ① 靠近不播放的病根:r182 只在「由远变近」的那一帧调一次 play() —— 那一拍如果被自动播放策略/媒体时序拒掉(reject 被静默吞掉),之后就永远哑火,真机表现正是走近了也不放。现在 videoProx 改成自愈重试:人还在屏前(≤16m)而视频停着 → 每 0.5s 幂等补一脚(已播就跳过,零成本);play() 被拒不再无声吞掉,留 console.warn(真机远程排查有据可查)。走远(>18m)暂停回零、下次接近从头看 —— 口径不变。② 装备流光重做:r182 的萤火虫小点整组撤掉(.ff/.ffedge/四边轨道/ffblink 全删)。 .eqfx 现在是一道斜向高光带周期性扫过整格图标(eqshine,115° 白金渐变带 2.8s 一扫)+ 金色辉光呼吸(eqglow,与扫光同周期齐峰)—— 就是商业游戏装备那种「闪闪发光」的流光质感;mix-blend-mode:screen 只提亮不遮物,纯 CSS 零 JS 成本。验收:r183 套件(扫光 CSS / eqFx 无 ff / videoProx 自愈与阈值)+ r169/r180 回归 + 真浏览器传送探针(远→近自动起播路径)。 |
| ★ 视频幕墙「接近才播」+ 装备萤火虫特效 · v24r182 | 用户口径两条:「进去视频流都播放完了,要角色接近才播放」「把背包那把刀周边的效果改成:下载资源进度条边缘一闪一闪的、刀轮廓边缘像萤火虫的效果」。 ① 视频接近才播(js/stormwind.js):原版进游戏就起播,等走到南墙前 15 秒的舞早放完了。现在 VSCREEN 记住屏面世界坐标(x=10, z=28.6),game.js 主循环每帧把玩家坐标喂给 STORMWIND.videoProx()(内部 0.5s 节流):距屏 ≤16m 起播、>18m 暂停并回零(4m 滞后带防边界来回抖动;回零保证下次接近从头看);canplay / 10s 保底定时器的 kick 也只在接近时才真播 —— 人没到,屏就静静停在首帧海报上。② 装备萤火虫特效(index.html CSS + game.js eqFx):替换 r181 的 GIF 扫光 —— .eqfx 现在是格子轮廓一圈呼吸微光(ffedge 动画 = 进度条边缘的闪烁质感)+ 8 颗小光点沿四边爬行、随机明灭(.ff,四条边各自的关键帧轨道 + 负延迟错峰),像萤火虫绕着刀轮廓飞。纯 CSS 动画零 JS 每帧成本,仍 mix-blend-mode:screen 只提亮不遮物;只挂装备栏已穿戴格,背包/仓库不挂。验收:r182 套件(版本五连 / videoProx 阈值与节流 / kick 门控 / 萤火虫 CSS)+ r169/r180 回归。 |
| ★ 装备挂 GIF 流光特效 + 修复城门旁视频幕墙播放不了 · v24r181 | 用户口径两条:「显示一个 gif 动画效果挂在装备上」「纠正城门左边的视频流播放不了」。 ① 装备流光:新特效图 assets/icons/eq_glow.gif(透明底 GIF 动画,金色扫光沿对角线滑过 + 边角呼吸,6 帧循环)—— 装备栏已穿戴的格子自动盖上这层动画(.eqfx,mix-blend-mode:screen 金光只提亮不遮物;背包/仓库不挂);404 自拆零风险,进 sw.js ASSETS+CHANGED。② 视频幕墙三层修复(城内南墙内皮 x=10,门洞东侧屏 —— 玩家视角的「城门左边」):服务器层: tools/nocache_server.py 新增 Range/206 支持(此前只有整包 200 —— iOS/Safari 媒体栈对带 Range 的请求硬性要求 206+Content-Range,回整包就起不来,这是「播放不了」的服务器侧根因);SW 层:assets/video/ 请求绕过 SW 直连网络(此前走缓存优先 respondWith,SW 回整包 200 同样卡死 iOS;视频 3.6MB 在线播放即可);页面层(stormwind.js):video 元素真挂进 DOM(0 尺寸隐藏 —— 不挂 DOM 的媒体元素部分内核拒绝起播)、muted 属性双设、显式 load()、stalled 重载兜底、10s 起播保底定时器;VSCREEN.ver 升 v24r181 绕旧缓存。验收:curl Range 实测 206 + Content-Range;r169 视频幕墙套件回归;r180 图标套件扩展守卫。 |
| ★ 装备/道具 2D 图标系统(PNG 静态 + 透明 GIF 动画) · v24r180 | 用户口径:「商业游戏里面很多装备道具图标都是 2D png 图片或者 gif 透明黑背景动画图片,要实现这个功能」。 ① 新模块 js/itemicon.js:物品对象 → 图标 URL 的解析器。优先级:物品自带 img 字段 → 商店道具按 id(hp/mp/stone/safe/box)→ 武器按职业(weapon_bk/sm/elf)→ 其余装备按槽位(armor/helm/ring/wing)。索引来自 assets/icons/manifest.json(首次用到 fetch 一次,不让前端瞎猜扩展名白发 404)。② 渲染策略:emoji 全保留、图片叠上面 —— 6 个渲染点(装备栏 / 背包 / 仓库 / 商店列表 / 强化面板×2)在原 emoji 后面追加 <img class="iticon">(绝对定位盖住);图 404 时 onerror 自拆,emoji 自然露出 —— 图挂了永远不会出现破图图标。GIF 的透明动画由浏览器 <img> 原生驱动,零 JS 成本。③ 图标库 assets/icons/(tools/r180-gen-icons.py 生成,64×64 透明底、4 倍超采样):9 张静态 PNG(剑/杖/弓/甲/盔/红药/蓝药/强化石/保护符)+ 3 张透明 GIF 动画(戒指高光旋转 / 翅膀扇动 / 宝箱光晕呼吸)。换皮 = 直接覆盖同名文件,不用改代码;?icon0=1 一键回落纯 emoji。④ 离线:15 个新文件全部进 sw.js 的 ASSETS(首次安装预缓存)与 CHANGED(老玩家增量更新)—— 不列的话离线玩家会全部回落 emoji。验收 tools/r180-icon-test.cjs:manifest 全键命中 / pick 优先级 / gif 是真动画(多帧)/ html 自拆属性 / 六渲染点接线。 |
| ★★★ 「进度条永远 0/0」的真病根:SW 问询应答没挂 waitUntil(回包必丢) · v24r179 | ① 症状:页面每 3s 连问一次(qpost 一直涨到 27+、ctl=true),__MU_PRE 却永远 idle 0/0,进度条纹丝不动 —— 也就永远等不到 r178 要求的「真下完」,进不了选角。② 病根: message 处理器的**异步部分没挂 e.waitUntil()**。Chrome 只保证「处理函数同步段」期间 SW 活着,之后的 caches.open + 160 次 c.match 全是异步,worker 一被判空闲就直接回收:同步那帧发出去了,真答案(缺几项 / done)永远发不出来;下次唤醒 PRE 已归零。以前偶发能成,是因为同时有 install / 补下在给 SW 续命,纯复访就只剩空转。③ 修法:整条核对包进 const job = (async () => {...})() 并 e.waitUntil(job);rep() 改**双路投递**(e.source + clients.matchAll 广播,因为 e.source 指向已过期 client 时浏览器不抛错但不投递);核对前先同步回一帧 probe:true 快照(页面只当心跳、不覆盖进度,避免复访从 90% 闪回 0%);caches.open 失败不再回 done(r178 之后 done 就等于开闸放人),如实回 run + err。④ 验收: tools/r176-repair-test.cjs 静态 28/0(新增 4 条 r179 守卫,含 waitUntil 那条)+ 真浏览器 —— 首轮封锁期如实 159/160 fail=1 → 闸不开·选角卡隐藏,解锁刷新后 phase=done 160/160 fail=0、账目 160==160、__MU_READY 真开、点角色进游戏。 |
| ★★★ 资源没下完就不许进选角:删掉「可进入」兜底 · v24r178 | 用户口径:「要等资源下载完,才显示角色选择,把可进入去掉,逻辑改下」。 ① 原来会「提前放人」: assetsDone() 有六条开闸路 —— 真下完、20s 问询超时、30s 进度停摆、180s 硬上限、12s 注册不上、压根没 SW。后几条都会把 __MU_ASSETS_OK 置 true 并显示「可进入 · 资源后台下载中 x%」,玩家就能在资源没齐时看见并点选职业卡 —— 正是历次「进去看不到所有资源」的来源。② 现在只认一条: assetsDone(why) 只放行 sw-full(SW 真下完且 done>=total)与 no-sw(这台机器压根没有 SW —— 不存在「下完」判据,硬等就是永久白屏);问询超时 / 停摆 / 硬上限 / 注册失败一律 return,只在控制台留一条「资源还没下完(xxx)→ 继续等,不开闸」,注册失败按「无 SW」处理。③ 文案:「可进入 · 资源后台下载中 x%」整条下线,改成「资源下载中 x%(n/m)· 下完才能选角」,有失败项时带「N 项失败 · 正在重试」。 ④ 为什么「等」不会白等:靠 v24r177 的闭环 —— 页面每 3s 问一次、SW 发现缺项就地补下(30s 节流),慢网多等一会儿必然收敛;选角卡仍由 .gate-off 藏着,下完那一刻才揭幕(v24r173 的揭幕机制一行未动)。验收: tools/r176-repair-test.cjs 静态段 +4 条(兜底开不了闸 / 诊断留痕 / 「可进入」下线 / 卡片仍靠 gate-off 隐藏)+ 真浏览器新增「资源没下完 ⇒ ok=false、ready=false、卡隐藏」与「补下完成后 READY 真开、点角色进游戏」。 |
| ★★★ 真机「0/159 · 159 项失败 / 没有进度条」根治:isChanged 隐形炸弹 + 问询不再死等 load + 刷新即补下 · v24r177 | 用户截图:资源闸永远停在「0/159 · 159 项失败」,点角色进不去;口径「要下载所有资源到手机,点角色要立即进游戏」。 ① 真因一: isChanged() 从来没定义过(r165 起就在 grab() 里被调用)——直连 Service Worker 线程取证实测 ReferenceError: isChanged is not defined,于是 install 的 160 项逐项全部抛错(fail=160/160),缓存永远是空的;在线时文件走网络直取,游戏照样能跑,所以这个 bug 被 stalled 兜底掩盖了十几个版本。现在补回该助手(剥 ?v= 与前导 ./、/ 后与 CHANGED 比对)。② 真因二:问询整块挂在 load 上——弱网首访 load 可能几十秒不来(headless 实测 76s 仍 readyState=interactive),tryQuery 一次都没发出去,页面一条 muPre 都收不到(arm=1 / qn=0);手动补发一次立刻收到 done 160/160。改为 DOMContentLoaded 即起+每 3s 重问一次,且判据是「资源真下完」而不是「闸放行了」——兜底放行只决定玩家能不能先进去,后台该补的还得补。③ 刷新即补下:install 每个版本只跑一次,首轮失败后刷新只会「报状态」不会「再下载」。新增 kickRepair()(PRE.repairing+installing 防重入、30s 节流),两条残缺路(同进程 finished && fail>0、跨进程缓存审计缺项)都就地起一轮 runCachePass();用户每刷新一次就自动重试一轮。④ 顺带修掉「假完成」:补下会把 fail 清零、done 从 0 重算,而 PRE.finished 还是上一轮的 true ⇒ 问询会回「done 但只有 13/160」,页面据此提前开闸、进度条一闪就没了。现在应答优先级改为「下载中一律回 run」,页面侧 done 也必须对数才放行。⑤ 诊断口: __MU_ARM(问询闸已布防)/ __MU_QN(首次问询次数)/ __MU_TICK(3s 重问循环心跳)/ SW 侧 PRE.q(收到几次问询)。验收: tools/r176-repair-test.cjs 24/0(静态 18 条含「未定义助手」通用守卫 + 真浏览器全链路:首轮 159/160 fail=1 → 解锁 → 刷新 → 12→160 → done 160/160 → __MU_READY → 点角色进游戏 38s);回归 r170 23/0、r171 13/0、r172 16/0、r173 45/0。 |
| ★★★ 开局下载闸:资源进度条 → 下完才能进 → 点角色立即进 → 进去全资源 · v24r170 | 口径:「打开网址,显示要下载资源进度,下载完才能进游戏,点角色要立马进到游戏,进去可以看到所有资源」。 ① 找到「进去看不到所有资源」的根因:世界里实际摆放的 11 个 GLB 模型(32.7MB)压根不在 Service Worker 预缓存清单 —— 模型解析器原走首屏后空闲加载、模型本体走进游戏后 ML.preload 25s 竞速,弱网下就是「模型晚到/神秘消失」(r130 只修了自愈,下载这一步省不掉)。现在 11 个模型全部进 ASSETS 预缓存(首包含量 17.9→50.6MB);N 键模型库 ext/ 其余 143MB 是面板按需加载的,故意不进闸(全塞首包=190MB,手机吃不消)。 ② 两段进度合一条进度条(职业卡上方):引擎段 —— boot.js 的 __BOOTPROG 逐脚本计数(内核+模块+地图数据+模型解析器);资源段 —— sw.js install 逐项缓存时向页面广播 muPre(done/total,每 5 项一报),页面显示「资源下载中 x%(n/154)」。③ 闸门: window.__MU_READY = 引擎就绪 && 资源就绪。就绪前点职业卡只记住选择(「已选 X · 资源就绪后自动进入」),就绪瞬间自动进入;就绪后点卡立即开局 —— 模型全在本地缓存,ML.preload 秒回,进去就是全的。解析器(GLTF/OBJ/FBX)从「首屏后空闲再拉」并进主链:引擎就绪 = 解析器就绪,点卡那一刻不会因为解析器没到而模型回落内置体。④ 复访不重下:SW 增量缓存本来就是「缓存里已有就跳过」;页面还能主动 muPreQuery 问状态 —— 同版本复访秒回 done(纯本地 IO),进度条一闪即过。⑤ 兜底四样(闸是体验优化,绝不锁死玩家):无 SW 2.5s 放行 / 问询 8s 不回放行(旧版 SW 没应答器)/ 进度停摆 30s 放行 / 硬上限 180s 放行 —— 资源后台继续补。 验收 tools/r170-preload-test.cjs:静态 23 条(含「model-map 引用的模型必须全在 ASSETS」的动态守卫)+ 真浏览器首访全链路。 |
| ★★★ 城内墙底视频幕墙 —— 手机视频贴上城墙 · v24r169 | 口径:「弄这个视频到风暴城里面的墙底上」。手机拍的舞蹈视频(540×960 竖屏 · 24fps · 15.1 秒 · 3.6MB)整段贴到城内南墙内侧的墙基上(门洞东侧 x=10,屏面 3.04×5.4m,9:16 零拉伸;屏底压在勒脚上沿,不挡门洞、不插进墙里)。 ① 播放三件套:muted + playsinline + loop —— 手机浏览器自动播放的硬性要求;加载前先垫视频首帧海报(26KB),视频 canplay 后换上 THREE.VideoTexture(照抄 crackTex 两条铁律:占位与真内容同尺寸 + 接管换全新 Texture)。自动播放被系统策略拦时,下一次点屏会自动补播(手势兜底)。② 屏幕质感:屏面用 MeshBasicMaterial 自发光 —— 不吃场景光照,夜里也亮,像真的挂了块屏幕;石框嵌墙外挑 0.18m,与墙面错面不闪块。③ 性能无伤:视频解码走浏览器硬解,不占 WebGL 预算;散件段 noCol 不生成碰撞盒;3.6MB 已 faststart (moov 前置),进游戏边下边播,已列入 SW 预缓存(二次进入秒出)。 验收 tools/r169-video-wall-test.cjs:静态 23 条 + 真浏览器挂载/尺寸/朝向/材质隔离断言(headless 无 H.264 解码器时自动降级验海报垫底路径)。 |
| ★★★ 触屏默认降载:渲染倍率 0.8 + 开局即省档 —— 「卡了」的正面回应 · v24r168 | 口径:「卡了」。r167 把视角退回自由后真机仍卡,说明瓶颈不在视角档位 —— 手机端最大的一笔是填充率(整屏像素数 × 每像素的着色开销)与阴影 pass,本版把这两笔直接打进默认值: ① 触屏默认渲染倍率 0.8( js/settings.js DEFAULTS.pixel + js/game.js PR_USER 初值同步)—— 1080p 屏每帧少画 36% 像素,肉眼几乎无感,帧时间立减;桌面不受影响(仍 1.0)。老存档里压着出厂默认 pixel=1.0 的玩家做一次性迁移到 0.8(闸 mu.mig168 只跑一次;玩家真动过滑块就尊重他的选择)。② 触屏开局即 tier1( js/game.js 开局降载判据从 QUALITY==='low' 放宽到 QUALITY==='low' || COARSE)—— 所有手机/平板进游戏第一帧就是省档:关阴影(省掉一整遍阴影几何 pass,手机端最大单项)+ 粒子预算 ×0.7 + 收池 -15%。此前 r162 只给 low 配判定的机器开局降,4~8 核的中端机要等帧率守门员跑完 3 秒窗口才动手 —— 头 10 秒「新手第一次走动」正好最卡,玩家已经判定「这游戏卡」了。嫌没影子可在设置里开回(守门员回升档时恢复的是面板上的原值)。③ 关于「卡」的三笔账(写给玩家):视角档位 ≈10%(169 vs 188 draw calls)→ 已退;填充率 ≈36%(渲染倍率 1.0→0.8)→ 本版默认砍掉;阴影 pass ≈一整遍几何 → 触屏开局即关。三笔都落进默认值后,若真机仍不流畅,剩下的就是机型本身到顶了 —— 那时再谈「删减场景装饰/再压分辨率」的取舍。 验收: tools/r162-lowend-test.cjs 静态段补三条断言(触屏 pixel 默认 0.8 / 开局降载判据含 COARSE / pixel 迁移闸);r160/r163/r166 等静态回归复跑全绿。 |
| ★★★ 默认视角改回「自由环绕(360°)」—— 45° 固定档退为可选 · v24r167 | 口径:「还是卡,返回360°自由吧」。玩家实测 45° 固定档在真机上还是卡 —— 收紧剔除省下的那点 draw call(实测同屏 169 vs 188,约 10%)抵不过玩家对画面的直观不满,所以默认视角退回自由环绕,45° 固定档保留但退为可选。 ① 默认档翻转( js/game.js):CAM_MODE_PARAM 无参数时返回 'free'(原 'fixed45');js/settings.js 的 DEFAULTS.view 同步改 'free',面板两颗按钮顺序调成自由在前(左边=默认),操作说明与设置说明文案一起改口径。② 旧存档一次性迁移:老玩家存档里压着 view:'fixed45',直接改默认值会被存档覆盖回 45°(等于没改)。所以 applySave 里加迁移:存档里的 fixed45 迁成 free,并用 localStorage['mu.mig167'] 做只跑一次的闸 —— 迁移之后玩家在面板里主动选回 45° 会被尊重并照常落盘(否则就是「选回 45° 不生效」的新 bug)。?cam45=1 显式指定依旧压过存档。③ 45° 固定档一行没删:r160 的几何收紧通道(视锥余量 0.16 / 收池 52m / 遮挡 32 列 / 阴影 ±40m 跟随)与 r166 的 18~40m 视距推拉全部保留,设置面板或 ?cam45=1 随时能进 —— 低端机想省电仍然有这条路。④ 验收翻正: tools/r160-viewmode-test.cjs 场景顺序对调并补两条 —— 默认档验 free(360° 可转、通道不收紧),45° 档搬到切档后验收紧全套,面板验「默认高亮自由 + 点 45° 真能切」,?cam45=1 验 URL 压过默认,新增 [G] 存档迁移(真走 applySave:旧 fixed45 → free、迁移闸只跑一次、迁移后选回 45° 生效)。47/0 全绿。连带翻正 tools/camera-free-test.cjs 四条过期断言(默认档 / 面板默认值 / 滚轮与双指不再被 !CAM45 拦住),33/0。⑤ 关于「卡」:视角档位只影响 10% 上下的开销,真正的大头是渲染倍率与阴影 —— 手机上还卡就进 设置 → 画面把「渲染倍率」调到 0.6、阴影关掉(或直接点低画质预设);帧率守门员(r162)会自动降采样与结构降档,无需手动干预。 |
| ★★★ 45° 固定档放开视距:双指可推拉 18~40m,方位与俯角仍锁死 · v24r166 | 口径:「视距改成可以双指放大缩短,收缩延长」。这是 r163「视距和轴都固定」的定向松绑 —— 只放开距离一个自由度,轴与俯角一寸不动。 ① 闸门改口径( js/game.js updateCamera)—— r163 的每帧硬闸里原本有 cam.dist 拉回 32、camCtl.dist 强设、camCtl.dv = 0 三行。前两行直接撤掉,第三行是对弹簧速度的「每帧清零」—— 它才是双指推不动的真凶:目标值改了,弹簧速度每帧被清,臂长只能一帧挪一点点,手感像在拉橡皮筋。现在闸内只做两件事:俯角恒 CAM45_PITCH、视距钳在 CAM45_DIST_MIN~MAX;清惯性角速度那条保留(orbitVel.az 在下面是无条件参与的,不清就能把轴拧走)。② 两只手同一入口: pinchZoom 去掉 || CAM45 早退、if (!CAM45) 包住的滚轮也拆掉 —— 手机双指、PC/触控板滚轮都走同一条「等比缩放 + 死区 + 限速」的老路,只是范围按档位取(45° 用 18~40,自由环绕仍是 10~42)。方向沿用 r105 口径:张开 = 拉远、捏合 = 拉近。③ 上限 40 不是随手定的:45° 通道的收池半径 CH_FIX.OUT = 52(r160 按 32m 臂长算的几何),相机在玩家身后 dist×cos45°;40m 时覆盖到玩家前方 20m 需要 28.3+20 = 48.3 < 52 还剩余量,再远(46m 就要 52.5)远景物件会在画面边缘 pop-in。要更远须先放宽剔除半径,那是另一件事。④ 切档仍回标准构图:进 45° 档 cam.dist 回 32m 基准,并把 camCtl.dist 一起对齐、速度清零 —— 这是 r163 那个「切回 45° 要 4 秒才走到 32m」的根治点(视距可变之后不能再靠每帧强设来对齐了,只能在对齐的这个一次性时刻做)。⑤ 诊断口: camInfo() 增 distMin / distMax / distMovable;验收看的是 renderDist(真进矩阵的臂长),不是 cam.dist 目标值。验收 tools/r166-camdist-test.cjs:静态 23 条 + 真浏览器 57 条 —— 双指张开视距变大、捏合变小、上下限 18/40 夹住、滚轮同效,而同一串手势里 renderAz 与 renderPitch 全程零变化;拖动 / QE 在 45° 下依旧什么都不做;切自由环绕后范围回到 10~42 且照常能转能缩。r163 套件同步翻正(原来「双指/滚轮不许动视距」四条断言作废,改为「必须能动、但轴不许动」)。 |
| ★★★ 画面观感商业化 · 第一波:10 个大槽位换 CC0 实拍贴图 —— 草地/城墙/铺地脱胎换骨 · v24r165 | 口径:「没一点商业游戏的感觉」→ 用户选定主攻画面观感、可花小钱。取证(headless 多机位截图)定位三大观感杀手里先打最大的:程序噪点贴图(草地荧光绿大色块、砖墙模糊重复)。 做法:零代码,全走项目现成的 TEXREG 贴图资产管线(v24r26 为美工准备的「放图即换」通道):素材选 ambientCG(协议 CC0,可商用免署名),11 个 1K 原包逐张目检选型(Bricks083/084 正是暴风城味奶油石砌块、RoofingTiles002 是鱼鳞瓦), tools/prep-cc0-textures.py 统一调色适配画风(草地压饱和 ×0.82 去荧光感、石地压暗、屋瓦留白给材质冷色乘法)后落盘 30 张 webp ≈ 6.1MB(法线/粗糙度 ≤512px 控体积),tex-scan.py 生成 manifest 自动加载。槽位覆盖:草地/石地/冰原三大地表 + 要塞花岗岩墙/深色墙基/地砖/蓝瓦/木门 + 旧镇砌石/鹅卵石 —— 每槽三件套(漫反射 sRGB + 法线线性 + 粗糙度灰度),平铺参数按铁律继承,近看不糊、俯视无摩尔纹。来源与处理记录全部落在 assets/textures/README.md 第六节。⚠ 模型级短板(素体 NPC/方块怪/纸片树)不在本轮范围 —— 那是 Synty 级资产替换的活,另开一轮。 | 口径:「把这张图片贴在城门上的旗」。手机拍的照片整张贴上南门楼那面垂旗(原「蓝底金狮」位),照片 3:4 构图不拉伸不裁切(旗面 3.0×6.0 → 3.6×4.8,旗顶 9.9m 与原旗同高:门洞上方墙体下沿之下、徽记盘之下、门楼外沿之外 0.7m —— v24r25 的三道安全余量一条没动)。 ★ 材质隔离是本版的关键设计: sw_banner 是全城 287 面垂旗共用的一张 canvas 图,直接换它 = 整座城的旗都变成照片 —— 所以照片旗走独立材质 + 独立贴图通道(photoBannerMat / photoFlagTex,照抄 vfx.js crackTex 的两条铁律:① 占位图与真图尺寸一致(512×683,WebGL2 首传锁死尺寸);② 换图换全新 Texture 对象),其余 286 面金狮旗一个字节不动。验收 tools/r164-photo-flag-test.cjs 23/0:材质与同组金狮旗完全隔离(siblings=0)、真照片异步接管成功、node 沙箱(r25 用 vm 真建要塞)降级不崩。飘动动画沿用同一个顶点级波场 —— 照片贴在布面上跟着一起飘。想换照片:替换 assets/textures/photo-banner.jpg(保持 512×683)并把 stormwind.js 里 PHOTO_FLAG.ver 升一位(绕缓存)。 |
| ★★★ 45° 固定机位「轴 + 视距」双锁 —— 真正的固定视角 · v24r163 | 口径:「视距和轴都固定」。 ① 轴:默认锁死( js/game.js)—— 原来 CAM45_YAW_LOCK = FLAG('camlock'),也就是默认不锁、要锁得自己在网址后面加 ?camlock。于是「45° 固定」名不副实:鼠标拖 / 单指拖空地 / QE / 手柄右摇杆都能把轴拧走,松手就停在玩家拧到的那个角度。现在改成 !FLAG('camlock0') —— 默认锁死,?camlock0=1 留作对照调试口。四类输入的早退守卫(orbitBy / orbitStick / QE / 自动咬合)全部沿用,一处未改,只是它们守的那个布尔从「默认 false」变成「默认 true」。② 视距 + 俯角:每帧硬闸 —— 输入层都守住了,但偏差还藏在联动层: pitchEff 的「缩放-俯仰联动」按 (dist − 19) × 0.006 算,而 45° 的臂长是 32m,于是标称 45°、实渲染 49.5°(cam.pitch 诊断口仍报漂亮的 45.0°,所以这偏差一直没被发现)。联动的自变量是视距,视距一锁,前提就没了 —— 现在 45° 下 pitchEff 直接取 CAM45_PITCH,并在 updateCamera 开头加闸:cam.dist / cam.pitch 每帧拉回 CAM45_DIST(32m) / CAM45_PITCH(45°),顺带清掉拖拽惯性残角速度。值相等时一次浮点比较就跳过,热路径无感。③ 撤掉 r162 的双指转轴手势 —— 上一版把「轴不能旋转」误读成「轴要能转」,给 45° 加了双指绕轴旋转;本版按真实口径整段撤除:45° 下双指什么都不做(既不转轴也不缩放),自由环绕下仍是原来的双指缩放。 ④ 验收要能看见「实渲染」: camInfo() 新增 lock 与 renderAz / renderPitch / renderDist(由 camera.position − camTarget 反算,是真正进矩阵的合量,含 pitchEff / 弹簧臂长 / 碰撞挪位);只看 cam.pitch 永远 45.0°,看不到 49.5° 这类偏差。验收 tools/r163-camlock-test.cjs:静态 13 条 + 真浏览器 12 条 —— 鼠标拖 220px / 双指拖 180px / 滚轮 / QE 四路输入下 renderAz / renderPitch / renderDist 全部零变化,切到自由环绕后同一套输入照样能转能缩(证明不是把相机弄死了)。 |
| ★★ 帧率守门员两级降档 —— 低配手机也能玩 · v24r162 | 口径:「45°轴不能旋转,优化下渲染算法,低配置手机也能玩」。 ① 【已在 v24r163 撤销】本版曾把 45° 下的双指改成「绕轴旋转」(理由是把「轴不能旋转」误读成「轴要能转」)—— 用户真实口径是轴与视距都要固定,故 r163 已把这条手势整段撤掉,45° 下双指什么都不做(轴/俯角/视距三样全锁,见 r163 条目)。以下 ②③ 仍有效: ② 帧率守门员两级降档阶梯( js/game.js DYN + js/perf.js setLowTier)—— 原来只有「降渲染倍率」一条路,降到头还卡就没救了。现在 DYN.level(0..4,只降采样,肉眼几乎无损)降到最低还掉帧才动 DYN.tier 结构性开销:tier1 = 关阴影(省一整遍几何 pass)+ 粒子预算 ×0.7 + 收池 ×0.85;tier2 = 粒子 ×0.45 + 收池 ×0.65 + FOV 余量 +0.04 + 遮挡扫描列 ×0.625。降得快(fps<34 逐窗降)、升得慢(fps>52 连续 6 窗 ≈18s 才升一级),回升时阴影还原的是玩家自己的选择(shadowWas 冻结),升档不抢面板。★ 真机适配:玩家不必做任何事 —— 掉帧自动降、流畅自动回;__MUAPI.autoQuality()/forceTier(t) 为诊断口。③ 低配判据补内存: navigator.deviceMemory ≤ 2GB 直接判 low(2/3GB 机器常虚报 4/8 核,只按核数会把它们错判成中端);判成 low 开局即 tier1(省掉开局最卡的前几秒),peBase 只在 PE 就绪且未折算时捕获一次(防「越降越低」)。 |
| ★★★ 无尽试炼「进得去、听得见、看得见」—— 转场黑幕 / 试炼音效 / 深渊回廊 / 落地光门 · v24r161 | 口径:「按照设计案分开 6 个员工分 6 个任务开发,加快进度,争取公测上线」。六个工种按文件零重叠并行落地(设计案 docs/r156-trial-mapbook-design.md 的 §10 终稿):① 转场层(新文件 js/transition.js,window.MUTransition)—— 修的是「hubpanel.close() 是 display:none 硬切、进试炼那一帧世界重建的 pop 完全裸露」。f0→f2(0.067s)压黑到 0.95(两帧内必须盖实)、f2→f9 竖直裂隙撕开到屏宽(内缘紫 #6a3fb0→橙 #ff8c5a)、f9 = 亮度峰值 = 落地光门重音、f9→f33 淡出(1.10s 拆幕)、2% 残影贴到 f42。z-index 按 kind 分档:trial=90(压过手册 84)、return=70(必须低于结算面板 72,否则阵亡回城时成绩单被黑幕盖死)。层挂 document.documentElement —— 与 r155 #hubMask 同源的 rot 陷阱。begin 幂等、cancel() 同帧摘层、isActive() 拆幕即 false。② 试炼音效( js/sfx.js + js/trial.js)—— 修两个真 bug:sfx.js TABLE 里压根没有 wave 键(play() 静默 stats.denied++)⇒ 所有非首领波入场全程无声;进试炼借的是 quest_done(交任务三音上行,听感错位)。补 trial_portal/trial_wave/trial_elite/trial_bosshorn 四键(全部程序化合成、零音频文件,每键内建 0.30s pre-roll,重音正好落在落怪帧 fW),并按波数加层(>10 低音、>20 金属泛音)—— 「越走越深」听得见。波次预警:tick 的 prep/gap 分支跨过 0.30s 时用 R.warned 一次性 flag 发一声,删掉原 244 行避免双响。③ 深渊回廊环境( js/models.js + js/game.js)—— 补 SKY_PRESET.arena / ENV_PRESET.arena(此前缺条目回退成勇者大陆的碧蓝天穹);新增地面符文双环(外 r=13m / 内 r=6.5m,自发光、additive) + 石柱环列 8 根(半数断柱、InstancedMesh 合批)。仅 arena 生效,其余四图零改动;新增 draw call ≤4。诊断口 __MU_ARENA_ENV。④ 落地光门与裂隙( js/vfx.js + 新贴图 assets/textures/crack.png)—— VFX.gateLanding(x,z):双层反向自转光环(外橙内紫)+ 橙光柱 + 冰波 + ≤200 粒子,1.20s;VFX.ashAbyss(on) 深渊灰烬变体;VFX.crackGlow(on) 地面裂隙 8~14 道、4~6s 呼吸。软渲染/低画质/reduce-motion 三档降级齐全。⑤ 手册入场动画( index.html + js/hubpanel.js)—— 补上一直没实挂的 .hub-anim 门控类(写了不生效的缺口):muHubFadeIn .45s / muCardIn .5s 错峰 0-.16s(只动 transform 不动 opacity,动画卡住也不会永久隐形点不动)/ muPanelRotIn ≤.28s(守住回归脚本 sleep(400) 采样点)/ muBadgeBreath 1.6s 呼吸;退出维持硬切(设计案已裁决退场动画是死代码)。⑥ 验收:新增 r161-transition-test / r161-audio-test / r161-arena-env-test 三条套件,静态段 67/0 全绿。⑦ 制作人合并接线(六工种接口各自交付后,三处「没人负责调」的缝由主程补上):① switchMap 加 arena 守卫 —— 深渊回廊的落地演出归转场层独占,不再叠一套换图光柱;② VFX.ashAbyss 必须在 buildWorld 之前设置(天气粒子建世界时按当时参数一次性配好,建完再改只影响下一张图,实测 n 一直是 400);③ MUSFX.TABLE 开只读诊断口;④ 联调取证 tools/r161-integration-probe.cjs:22/0 全绿(黑幕→光门→余烬→裂隙→逆幕全链路,事件序列 begin:trial → gate、回城 begin:return)。 |
| ★★★ 双视角可选(45° 固定 / 自由环绕)+ 45° 固定渲染通道优化 · v24r160 | 口径:「市场的游戏都是 45° 固定视角,可以选择全视角和 45 度固定视角两个选项,更新优化固定 45 度视角渲染通道,让游戏不卡」。 ① 视角模式开关:设置 → 画面 → 「视角模式」两颗按钮 —— 45° 固定(默认,同类 ARPG 标准机位:俯角/距离锁定)与 自由环绕(俯仰 9°~73°、360° 环绕、缩放 10~42)。切换即时生效、写进存档; ?cam45=0 / ?freecam=1 URL 习惯保留且优先于存档。切换走同一条 setCamMode(面板 / 验收口 __MUAPI.camMode() 同一路径),拖动惯性清零、45° 下不做画面侧倾(地面倾斜很晕)。② 45° 固定渲染通道( js/perf.js setChannel('fixed45'))—— 固定俯角 ⇒ 视锥在地面上的覆盖是固定梯形,三条判定可以放开:视锥余量 0.30→0.16 rad(自由视角的 17° 余量是给「拖俯仰防抖」留的,固定俯角用不了这么多);收池半径 72→52m(45°+臂长 32+FOV 34° 时地面可见最远约 20m,塔/树冠等顶高>8m 的物体豁免保留原口径);遮挡扫描列数 64→32(45° 时 tan=1 恒定,精度不降 CPU 减半)。四条保守闸(地标/guard/大件/同箱)一个不动。③ 阴影通道:45° 视野只到玩家前方约 20m,继续用 144m 阴影覆盖 = 90% 纹素花在画面外 —— 覆盖收到 ±40m 且跟随玩家(按光空间纹素整数倍对齐平移,sun 与 target 同量平移方向不变,静止零漂移、2cm 微动不重画);手机 mapSize 1024→512、桌面 high 4096→2048,阴影 pass 填充量 1/4,纹素密度不劣于原来。自由视角仍走老的「固定 ±72m 绝不跟随」,一行未改。 验收 r160 39/0(默认档/切换/参数逐项/设置面板真点/跟随要跟-静止要稳-微动不抖/?cam45=0);r82 断言同步(camBank 总是创建、运行时判 CAM45;CHANGED/ASSETS 守卫补齐)。新增 tools/r160-viewmode-test.cjs。 |
| ★ 45° 高度阴影遮挡剔除 —— 永远看不到的地方不渲染 · v24r159 | 口径:「固定 45 度角,视距不固定,永远看不到的地方不用渲染,节省电量」。 原理:俯角固定时「谁挡住谁」与视距无关 —— 定义截距 = 物体顶高 + tan(俯角)×沿视线水平距离,近处截距更高者挡住后面同视线的物体(45° 时即「影长 = 物高」)。按 64 列 × 2m 深度箱做前缀最大值扫描( js/perf.js occSweep),每帧几微秒、零分配,比每项算 acos 还便宜。视距不固定天然支持:公式里没有相机距离这一项,zoom 只改「能看多远」,不改「谁挡住谁」。保守化四闸(误收 = 画面缺块,绝不允许):① 被挡物按全半径张角取各列最小 M —— 有一列没挡死就不收;② 遮挡物只按 0.6 半径投影;③ 同深度箱互不遮挡;④ 地标 / guard 圈内 / 半径>10m 大件一律不参与。 要塞墙入列:整组要塞是地标(永不收池),但借 camcol 同款 colBoxes(世界坐标 AABB,211 个)注册成「只挡人、不被挡」的静态遮挡盒(addWalls/clearWalls)—— 城里最主要的遮挡来源就是它。节奏:收要慢(连续 8 次判定才收)、放要快(一不挡立刻复活 —— 晚一帧就是画面里多个洞)。俯角闸门:tan 值在 0.25~3 之外(近水平 / 近正俯视)整条不判,自动退回纯距离/视锥。验收 r159 29/0(合成场景精确几何断言 17 条 + 真机不变量 + 关掉遮挡 draw call 回弹实测);实测当前世界收益约 2~4 draw call(树/地砖稀疏、大件受保护),机制就位、世界越密收益越大; ?occ0=1 一键退回 r158。新增 tools/r159-occ-test.cjs。 |
| ★★★ 修复:手册面板在手机上「打开是竖屏的」—— 面板跟随游戏旋转 + 玩家视角滚动兜底 · v24r158 | 用户口径:「地图册打开是竖屏的,没有切换成横屏」。根因:手机(微信锁竖屏)一直处于 html.rot(body{transform:rotate(90deg)},玩家横握手机看),而 r155 为修滚动把手册面板挪到未旋转的 <html> 下 —— 面板不跟 body 转,玩家眼里是侧躺 90° 的一页竖版;且玩家自然的竖划手势(= 物理横向)对不上滚动轴。r156/r157 的「强制横屏」对触屏是空操作(触屏本就默认 rot),所以连着三轮「没横屏」。修法(js/hubpanel.js):① rot 下给 #hubMask 施加与 body 相同的变换 + 逻辑横屏尺寸(--rot-w/--rot-h 由 applyOrientation 写入)—— 面板与游戏同朝向,玩家看到正立、宽扁的横屏面板(逻辑宽 ≈ 843px,四栏签/卡片横排);仓库/商店等面板本就在旋转的 body 里、从没被说歪,手册现在与它们一致。② 玩家视角滚动兜底 —— 旋转 90° 的滚动容器浏览器原生触摸手势不跟(物理横划 scrollTop 纹丝不动,CDP 实测;这才是 r152~r155 一路「拉不动」的本质),rot 下面板用自己的触摸驱动接管:物理横向位移 → scrollTop(scrollTop = 起点 + dx,方向按旋转矩阵推导),主轴判别(|dx|≥|dy|)+ 8px 位移门槛(轻点/按钮点击不受影响),只在 html.rot 时接管,竖屏/真横屏仍走原生惯性滚动。窗口/画布宽高照旧不动(纯 CSS 旋转)。⚠ 改动仅 js/hubpanel.js + 版本四连;新增 tools/r158-hub-rot-test.cjs(rot 面板几何 + 玩家视角滑动实测)。 |
| ★★★ 试炼入口收敛:顶部按钮/地图册卡撤场 → 手册「成就」后独立栏 + 全面板横屏 · v24r157 | 用户口径:「无尽试炼已经放进地图册了,窗口顶上那个不要,把功能也放进,地图册 成就后面,再把窗口顶那个去掉,把整个地图册面板支持横着看,窗口宽高不要动」。 ① 顶部 ⚔试炼 按钮撤场 —— js/game.js 的 #mapBar 不再拼 MUTrial.btnHTML(),窗口顶上只剩「✨ 新功能」;syncBtn() 对 #btnTrial 缺席本就空转,trial.js 零改动兼容;快捷键 Y 保留。② 「无尽试炼」独立成栏(成就后面) —— js/hubpanel.js 的 TABS 按约定追加到末尾:{ id:'trial', ic:'🌀', mod:'MUTrial' };js/trial.js 按手册契约新增 panelHTML()(状态横幅 + 开始/回到/退出按钮 + 四格统计 + 完整记录榜)与 handleClick()(data-trx = enter/back/leave,进场成功收手册直接看战场)。③ 地图册栏顶部那张试炼卡撤场 —— js/mapbook.js 删 trialCardHTML/enterTrial/试炼卡 CSS,地图册回归纯地图卡,不再与试炼栏重复摆入口。④ 整个手册面板横屏 —— hubpanel.open() 对任意栏(地图册/任务/成就/试炼)强制横屏、close() 统一还原,setTab() 不再切栏还原(手册开着就是横屏);横屏仍是 html.rot CSS 旋转,不碰窗口/画布宽高。非触屏桌面与 ?rot=0 两道闸原样保留。⚠ 改动文件: js/game.js / js/hubpanel.js / js/trial.js / js/mapbook.js + 版本戳四连;r142/r143/r156 三条套件断言同步。 |
| ★★★ 无尽试炼进地图册 + 地图册默认横屏 · v24r156 | 用户口径:「无尽试炼 放进地图册,把地图册改成默认横屏」。 ① 无尽试炼入口卡进地图册 —— js/mapbook.js 新增 trialCardHTML(),作为地图册首张卡(排在地图卡之前):紫色 trial 卡(.card.trial:紫边 #6a4a8a / 紫底 rgba(40,28,60,.32) / 标题 #e7c8ff / 紫渐变 tag),与金系地图卡「形状+配色」双区分 —— 试炼是「模式」不是「地点」,不该混进「点一张卡片前往该地」的列表。点卡走 MUTrial.enter()(不切图):进行中显示「进行中 · 第 N 波 —— 点此回到试炼」+ tag「第 N 波」;?trial0=1(试炼系统关)显示灰显「未启用」卡;进入成功(或已在试炼中)才收手册,失败则面板留在屏上并 toast 原因(不再「点了没反应」)。② 地图册默认横屏 —— js/hubpanel.js 在 open() / setTab() / close() 三处按「当前是不是地图册栏」调 window.__MU_FORCE_LAND(on)(js/game.js 新增):进地图册 → 强制横屏;切到任务/成就栏、关面板 → 还原进入前的方向(单槽记忆 _landPrev,幂等、可重入)。两道边界闸(静态条件,on/off 对称 ⇒ 不会「进了横屏出不来」):① 非触屏桌面不强制旋转(窄长桌面窗口被转 90° 纯属负担,除非显式 ?rot=1);② 用户显式 ?rot=0(r150 的「退出横屏」开关)一律尊重。触屏设备本就默认横屏(ROT_WANT 对触屏默认 true)⇒ 对绝大多数手机是零视觉变化;真正生效的是「触屏 + 用户手动切过竖屏」这条路径。⚠ 改动文件: js/mapbook.js(试炼卡 + handleClick 分流)+ js/hubpanel.js(forceLandscape 三处接线)+ js/game.js(__MU_FORCE_LAND + 两道闸)+ 版本戳四连;tools/r142-mapbook-test.cjs / tools/r143-hub-test.cjs 三条卡数断言同步(试炼卡复用 .card 让计数 4→5 ⇒ 断言改查 .card[data-mid],另加试炼卡存在断言)。 |
| ★★★ 动画加长 + 冒险手册面板 rot 模式真机可上下拉 · v24r155 | ① 动画痕迹太短 → 加长 + 加大幅度。用户口径「动画痕迹太短」。菜单上下弹跳 muMenuIn .3s→.5s、muMenuOut .2s→.45s,起步位移/缩放手势也加大(菜单从 +26px/.5 提到 +40px/.4、过冲 1.14→1.2×;扇形 muFanIn .36s→.65s、muFanOut .28s→.55s,进场 rotate(−42°/.5)→(−62°/.38)、过冲 1.1→1.12×)。回归脚本 [3c] 的预算与收尾采样 sleep 同步放宽(菜单 .55s / 扇形 .9s 预算、sleep 400→700)。② 冒险手册面板在 rot 模式(手机默认强制横屏)真机能上下拉滑动了。r152 只把面板从「被甩出屏外」救回屏内,但没救回真机手势 —— 根因:手机默认进 html.rot(body{transform:rotate(90deg)}),面板是 body 后代、position:fixed 跟着被转,触摸坐标轴错位:屏幕竖向滑动映射到面板内部横向,而面板只 overflow-y 可滚 ⇒ 手指上下滑 box 的 scrollTop 纹丝不动(CDP 实测 rot 模式 touch 滑 box delta=0,竖屏却正常)。修法:把 #hubMask 从 document.body 挪到 document.documentElement(未被旋转的 <html>),彻底脱离旋转坐标系 —— 面板正立、竖向手势直接对应 overflow-y 滚动;非 rot 模式 <html> 也不旋转,挂在哪都一样、零副作用。CDP 复测:rot 模式 touch 滑 box 的 scrollTop 从 0 涨到 285,与竖屏一致。⚠ 改动文件: js/hubpanel.js(挂载点改 documentElement)+ index.html(动画 CSS)+ 三处版本戳一致(index/html·boot.js·voiceclips.js·sw.js)。 |
| ★★★ 菜单上下弹跳 + 扇形旋转进出的动画过渡 · v24r154 | 用户口径:「扇形隐藏或者菜单隐藏,都要有个动画过度。菜单是上下不断踹出现的动画,扇形是转动不断出现的动画」。 ① 菜单:上下弹跳(不是平移) —— 收起态从「向右平移 76px 缩小」改成 @keyframes muMenuIn:从下方 +26px、缩到 .5 起步 → 冲过头 −9px/1.14× → 回落 +5px → 微沉 −2px → 落位,是「弹一下再弹回来」的过冲手感(单条 transition 画不出多段过冲,这是换 animation 的唯一理由)。六颗按钮 0→.08s 错峰弹入(延迟表抽成 --mu-d 自定义属性,供 html.rot 横向变体复用)。收起走 muMenuOut(+22px 缩着退场,统一 delay 0 快速回位)。html.rot 另配 muMenuInX/OutX:rot 下 body rotate(90deg),局部 +X 才映射到屏幕向下,直接用 translateY 会在真机上变成左右弹。② 扇形:旋转着出现 / 转着退场 —— 进场 rotate(−42deg) scale(.5) → +9deg 过冲 → −4deg → 0,退场反向转出并缩到 .5;逐颗 animation-delay 递增(0→.16s),观感像「一圈扫出去」。关键坑:visibility 必须等动画放完才切(transition:visibility 0s linear .28s),否则「切 hidden」在动画第一帧就生效 ⇒ 整段旋转根本看不见 —— 这正是 r149 硬切的老毛病。③ 动画门控 body.menu-anim(最容易漏的一处) —— 动画全挂在 body.menu-anim… 下,这个 class 由 game.js 在首次点击菜单珠时才加。不门控有两个真 bug:① 加载时若 localStorage 记着「收起」,收起动画会从可见凭空闪一下再消失;② 记着「开」则白弹一次,且动画尾巴会伸到回归脚本 sleep(400) 采样点之后,把按钮矩形量歪。挂上之后就一直留着,之后每次切换都有动画。④ 一处刻意「不做」 —— 早前试过在动画播放期间给扇形挂 .animating + pointer-events:none 压掉点按,靠 animationend 摘除。实测无头/后台标签页里 animationend 不按预期触发 ⇒ .animating 挂住不摘 ⇒ 技能钮永久点不动(回归脚本拖动段直接崩)。教训:用「事件摘除 class」做临时禁用是危险兜底 —— 事件不触发就等于永久禁用。宁可让用户在淡入的前几十毫秒点空(没施法),也不要冒功能失灵的风险。现在只留 #skillFan.dragging .fskill { animation:none !important }(拖动时关动画,无状态可摘,天然安全)。⑤ 三个真 bug(都是回归脚本实测逮到的,不是推演) —— 本轮最有价值的产出: ·容器级动画污染整圈扇面几何:最初把 rotate/scale 加在 #skillFan 上,而容器 transform 会改掉所有技能钮的 getBoundingClientRect(),槽位角/拖动触点/相交断言全按这些矩形算。实测动画时间线并非永远推进(无头与后台标签页里 playState='running' 但 currentTime 恒为 0),此时整圈扇面停在 rotate(-42deg) scale(.5):容器 470→332px、β 从 12° 偏到 54°,且不自愈(要切页面才恢复)。⇒ 改成旋转动画挂每颗 .fskill、容器绝不碰 transform;整块淡入淡出另用容器的 muFanFadeIn/Out(opacity 是非布局属性、不改 rect ⇒ 几何零污染)。·关键帧里的 opacity 盖掉槽位淡出: fanLayout 给每颗钮内联写 style.opacity 实现「转出扇面(β>fHi)就淡出隐藏」,而 CSS animation 优先级高于内联样式 —— 关键帧一旦带 opacity,槽位淡出整个失效,实测7 颗技能全部常亮、「转出扇面的技能再也藏不住」。·同上问题在菜单上更致命: opacity 是「看不看得见/点不点得到」的功能态,动画时钟一冻结,按钮就永久停在 0% 帧 ⇒ 展开时 opacity 0「菜单点了没反应」、收起时 opacity 1「收不起来」。⇒ opacity 全部交回 class 基态,动画只碰 transform;填充模式也一并从 both 换成 forwards(both 的 backwards-fill 会把 0% 帧当「动画开始前的样式」冻在元素上)。以上三条已写成防回归断言(旋转关键帧不含 opacity / 菜单关键帧不含 opacity / 容器 rect 宽恒等于 CSS 宽)。 ⑥ 尊重系统设置: @media (prefers-reduced-motion: reduce) 下所有动画压到 .01s,等效直接切换但状态正确(⚠ 无头 Chrome 默认就是这个偏好,验收脚本必须用 Emulation.setEmulatedMedia 关掉,否则量到的全是降级后的 .01s)。验收: tools/r147-fan-test.cjs 新增 [3c] 段(关键帧确实含 rotate/translateY 与过冲、旋转挂在钮上而容器 transform:none、门控下双向动画名正确、各段时长都在预算内收尾、落位后停在收起基态)。 |
| ★★★ 菜单搬到顶部 · 扇轮图标增大两倍 · 菜单↔扇轮互让 · v24r149 | 用户口径:「菜单放在最上端,地图的下面,不要遮住地图;点菜单显示所有按钮,底部没地方放的,弄两列放,要从底部溢出;同时扇形技能隐藏,再点菜单扇形技能显示;菜单只露出部分按钮;扇形技能图标增大两倍,间隔拉大点」。六件事一次做完: ① 菜单珠搬到最上端 —— #btnMenu 从右下角(bottom:168)移到地图条正下方(竖屏 top:100 / 横屏 top:40,都贴地图条下沿且与居中地图条无交叠,实测竖屏 mapBar 44~98 / 珠 100~158、横屏 mapBar 6~28 / 珠 40~84)。右列 7 颗功能钮不再写死在 CSS,改由 layoutMenu()(game.js)统一布局:从珠下 170(横屏 96)起、58px 步进往下排,竖⇄横/旋转切换与 resize 时自动重排。② 底部放不下 → 折两列、从底部溢出 —— maxN = floor((屏高 − 列顶 − 12) / 58):竖屏 915 高排得下 7 颗(单列 170~576),横屏 412 高只容 5 颗 → 铁匠/设置折到第二列(right 70,在第一列左侧),列本身继续往底部方向延伸。实测横屏展开:左列 Help/Music/Orient/Bright/Stash(t 96~328)、第二列 Shop/Set(t 96/154、right 70)。③ 菜单 ↔ 扇轮互让 —— body.menu-open #skillFan{visibility:hidden}:点开菜单 → 扇形技能整块让位(功能钮要占右缘两列);再点菜单 → 扇轮立刻回来。④ 收起态只露部分按钮 —— 原「7 颗全藏」改为只藏 亮度/仓库/铁匠/设置 4 颗,说明/音乐/横屏 3 颗常亮(含横屏同步,收起态横屏也只露这 3 颗 + 珠 + 跳)。 ⑤ 扇轮图标增大两倍 + 间隔拉大 —— 竖屏 58→116px(终极技 64→128)、横屏 40→80px(终极技 88);槽距 16.5°→23°(横屏 17°→18.5°),半径 132→310(横屏 104→280)。槽位角必须同时满足钮不伸出右缘(β≥12°)与钮不进左下血蓝球带(β≤58°,球心距须 >84px)→ 竖屏扇面收为 3 槽(β=12/35/58,相邻圆心距 123.6px > 116px 直径不重叠,淡出边界 fHi=72 把落到球上的第 4 槽自动隐藏),横屏锚在右缘中段仍可开满 5 槽(β=6~80,圆心距 90px)。冰风谷 7 技竖屏初始露 3 藏 4,滑一下把尾端冰技转出来。 验收 tools/r147-fan-test.cjs 50/0(新增:珠在最上端且不遮地图条 / 收起露 3 藏 4 / 展开 7 颗 + 扇轮让位 + 关掉恢复 / 横屏折第二列 right=70 / 钮 116·80px / 竖屏露3藏4 / 23° 槽距吸附);回归 r143 68/0 · r142 29/0 · r141 22/0 · r139 16/0 · r128 123/0。 |
| ★★★ 扇轮横屏也开 + r147「手机看不到」的两个真凶 · v24r148 | 用户口径:「没看见扇形技能栏」。两个独立根因,一次修掉: ① 真凶一(送达):v24r147 漏改 sw.js 的 CHANGED 清单 —— 我只升了 const VER,没把本轮改过的 index.html / js/game.js 写进 CHANGED。而 sw 对 js/HTML 走缓存优先且缓存键剥掉 ?v=(见 sw.js fetch 里的注释)⇒ 没列 = 手机继续吃 r146 的旧副本,那版压根没有 #skillFan,所以「改完等于没改」。本轮把两个文件正式列进 CHANGED(装过 r146 的手机这次才被删缓存重下)。发版铁律补一条:改完代码先列 CHANGED,再升版本号。② 真凶二(设计):扇轮原来只开在竖屏 —— r147 我按「横屏右缘已被 r145 功能图标列占满」把横屏一刀切回底部技能栏;但用户发的参考图本身就是横屏截图(1080×470),他就在横屏玩 ⇒ 看到的是旧技能栏而不是扇轮。本轮横屏也开扇轮,锚点从「屏幕右下角」改为右缘中段(OX=70 让开 40px 图标列 right 6~46,OY=92 让开底部血蓝球;R=104、槽距 17°、40px 小圆 / 终极技 44px)—— 正是参考图里那圈技能的位置。③ 强制旋转也覆盖( html.rot,微信锁竖屏时页面自己转 90°):画布是逻辑坐标系,扇轮的画布尺寸取 innerHeight×innerWidth、指针坐标按 lx=sy / ly=viewportW−sx 逆映射(与 index.html 里 html.rot body rotate(90deg) 的口径一致),滑动方向在旋转模式下依然跟手。④ 底部技能栏彻底退场(扇轮一统天下,键盘 1/2/3 照旧),竖 ⇄ 横切换时几何基准变了 → 强制重建扇面并复位偏移。验收 tools/r147-fan-test.cjs 36/0(新增:横屏扇轮 7 颗、底部技能栏全程 display:none、横屏按钮不与 r145 图标列/血蓝球重叠、强制旋转模式 ?rot=1 下扇轮几何与滑动跟手;竖屏原 26 条全保留)。回归 r143 68/0 · r142 29/0 · r141 22/0 · r139 16/0 · r128 123/0。 |
| ★★★ 技能扇形轮盘:右下角 90° 扇弧排列,顺/逆时针滑动换技能 · v24r147 | 用户口径:又发一张奇迹MU截图 ——「可以露出几个菜单列表,到达右下角的隐藏,把技能排列 90° 扇形排列,可以顺时针 逆时针滑动,展示技能出来」。参考图右下角:技能圆钮沿 90° 扇弧围着角落排开,多余的技能「绕到角后」藏起来,滑动扇面旋转换技能。 ① 技能搬进扇形轮盘(竖屏/桌面) —— 新增 #skillFan:技能圆钮(58px 暗金底金边圆,冰系蓝边、终极技紫色大圆 64px,与 r145 圆图标同一家族)沿右下角 90° 扇弧排列(扇弧半径 132px,相邻技能夹角 16.5°,可见 5 槽);转出扇面(绕过右缘/底缘)的技能自动淡出并不可点 —— 正是「到达右下角的隐藏」。原底部技能栏在竖屏退场(血蓝球在窄屏改靠左排让位),横屏(html.land)自动切回底部技能栏(右缘已被 r145 功能图标列占满,扇轮没有落脚点)——⚠ v24r148 起改为横屏也开扇轮(锚点移到右缘中段),见上一条。② 顺/逆时针滑动 —— 按住弧面拖动 = 整圈旋转:指尖极角(相对右下角)实时驱动每颗技能的槽位角,手指走到哪技能跟到哪;松手吸附到最近一格(0.5 格以内归位);带 ±0.6 格橡皮筋回弹。技能多于 5 个(冰风谷 6~7 技)时滑动才换页,5 个以内整圈锁死。 ③ 施法路全保留 —— 点按技能 = 原地无转动 → tryCast(idx)(拖过就算旋转、不施法);键盘 1/2/3 照旧;冷却黑幕/倒计时/蓝量不足置灰/.ready 金边在圆钮上原样工作;竖/横屏切换时两套容器强制重建(共享 sig 复位)。回归 r143 68/0 · r142 29/0 · r141 22/0 · r139 16/0 · r128 123/0。 |
| ★★ 菜单珠:右列功能图标默认收起,一键全弹出 · v24r146 | 用户口径:又发一张奇迹MU截图「点击菜单,全部都出来」—— 参考图右列平时只露一颗红色菜单宝珠,点它整列图标弹出。 ① 菜单珠 —— 新增 #btnMenu 红色宝珠按钮(58px 圆,radial 红底 + 金边,☰ + 「菜单」),排在右列最底(手机/横屏贴右下角,拇指最容易够到,与参考图同位)。② 默认收起 + 一键全出 —— 右列 7 颗功能钮(说明/音乐/横屏/亮度/仓库/铁匠/设置)默认 opacity:0 + 向右滑出 + 不可点;点菜单珠 → body.menu-open,整列逐颗错峰滑入(stagger 0~0.24s);再点收回(标签变「收起」、珠子亮金边)。状态存 localStorage(mu_menu),下次进来沿用。跳是游戏动作键,与菜单珠一样常驻不收。回归 r141 22/0(新增:菜单珠存在且初始收起 / 点击后 body.menu-open / 展开态 7 钮 elementFromPoint 全命中自己)· 其余套件随发版复跑。 |
| ★★ 右列改奇迹MU手游同款圆形金边图标 · v24r145 | 用户口径:发来一张奇迹MU手游截图「参考这个面板」—— 右侧一列圆形金边图标(图标+内部小字标签)。 ① 圆图标化 —— 右列 8 颗按钮(说明/音乐/横屏/亮度/仓库/铁匠/设置/跳)全部改为 .g 图标字形 + .t 中文短标签 的 54px 圆形按钮(横屏 42px):radial 暗金底 + 2px 金边 + 内阴影,悬停金光;结构照参考图。② 状态表达改版 —— 原来靠按钮文字变化表达状态(♪ Ice Tarus:开/关、☀ 亮度:柔和/标准/高亮、⛶ 横屏/退出横屏),圆里塞不下长文字:音乐=「开」冰蓝描边(沿用旧口径)、亮度/横屏/仓库=金边高亮,完整状态进 title(长按/悬停可见),亮度档位另有 toast 反馈。横屏时标签变「退出」。③ 排版节奏 —— 桌面列步进 42→58px(圆 54px 不重叠),手机竖屏 56→58px,横屏步进 46px;顺手修掉桌面端「跳」按钮与「横屏」按钮同 top:300 完全重叠的老问题(跳=游戏动作键,单独排到列尾)。 ⚠ 覆盖坑 —— index.html 后段「金属拉丝」共享块( #mapBar button, #btnHelp…)在同文件更靠后、同优先级,会把圆图标的 radial 底/金边打回方形金属条:已把右列按钮从该块与对应 hover 块中摘除。回归 r141 20/0(音乐断言同步为 title+.on 口径)· 其余套件随发版复跑。 |
| ★★★ 右列/顶部入口精简 + 手册贴顶 + 上下拉真机修复 · v24r144 | 用户口径:「去掉右边的 音效 任务 成就;窗口顶上的 勇者大陆 冰风谷 3个地图,冒险手册里都有了,不要摆出来;冒险手册窗口没置顶,而且上下发动不了」。 ① 入口精简(三个按钮退场) —— 右列 📜任务 / 🏅成就 / 🔊音效 三按钮删除:任务/成就 = 冒险手册的栏签(点右上角小地图进手册切栏;键盘 J/U 仍在),音效 = 设置面板里的静音开关(同一状态同一条路,功能没丢)。右列只留 铁匠铺/设置(位置紧凑上移补齐,桌面/竖屏/横屏三套布局同步)。② 顶部地图按钮退场 —— #mapBar 不再生成「勇者大陆/失落地牢/冰风谷」三个地图按钮(改图走 MAPS 数组处),切图入口收敛为:冒险手册的地图卡(同一道 MULevel.isUnlocked 闸)/ M 键循环 / 任务「前往」引路自动切图;自动战斗/试炼/✨新功能 照旧留在 mapBar。refreshMapBar / updateSpawnUI / MUI18n 对 button[data-map] 的查询落空即空转,无需改。③ 手册「置顶」 —— r143 的面板是「padding-top:56px + 居中」,顶部空出一大块透出暗化游戏画面(用户看到的「没置顶」正是这块)。改 align-items:flex-start + padding:10px 8px 12px:面板贴着屏幕顶部(top=10px),撑满时 bottom=100dvh−12,标题永远一眼可见(dvh 随真机地址栏实时收紧)。④「上下发动不了」的真根因与修法 —— r143 自创的「外框钉住 + 内层滚」是三层嵌套滚动容器(#hubMask hidden > .box hidden > #hubBody 滚),无头里 63 项全过、真机内核就是滚不动。本轮放弃自创结构,改仓库 #stashMask 同款(用户确认过仓库在真机上「上下拉面板不会动」): .box 自己 overflow-y:auto(唯一滚动容器)+ touch-action:pan-y(声明在 .box 与遮罩上,防 body 的 none 被老内核一路取交集掐死)+ overscroll-behavior:contain;表头(标题/三栏签/子栏签/✕)position:sticky;top:0 钉在滚动视口顶、不透明背景 —— 内容从它下面滚过,面板框/表头/标题/✕ 照样纹丝不动,还少一层嵌套。⑤ 顺手的真修复 —— 手册的 keydown 原来挂 document,而 quest/meta 挂 window:从 window 派发的按键事件到不了 document(事件不下传)⇒ 统一改挂 window(两条派发路都通)。验收 tools/r143-hub-test.cjs 68/0(★ 新增退场断言:btnQuest/btnMeta/btnSfx 不存在 + mapBar 无 data-map 按钮 + 铁匠铺/设置/自动战斗/新功能保留;手册贴顶三断言;真触摸上拖下拖:内容滚 0→208→0、面板框/表头/标题逐像素不动;Shift+M/J/U/Esc/点遮罩/✕ 开关路全通;锁定卡不切图+解锁真切图;前往引路;滚到底 ✕ 仍可点;零页面错误)。回归 r142 29/0(手册贴顶口径 + 铁匠铺保持 r142 下移口径)· r141 20/0(按钮命中列表收成 7 个)· r139 15/0(右列断言同步)· r128 123/0。⚠ 测试侧教训:r144 起 #hubBody 高度=内容全高,拖动起手点必须取在 .box 的视口矩形内(拿 hubBody 算会算到视口外、触摸点被丢弃)。 |
| ★★★ 地图册/任务/成就 三栏合一「冒险手册」+ 上下拉面板不会动 · v24r143 | 用户口径:「仓库的面板正常,上下拉面板不会动;把地图册 任务 成就 放到一个面板里,里面横着分栏显示 地图册 任务 成就,都要像仓库的面板」。 ① 三栏合一(新模块 js/hubpanel.js「冒险手册」) —— 一块面板、顶部一排横向栏签(🗺 地图册 | 📜 任务 | 🏅 成就),点栏签即切;成就栏里保留 4 个子栏签(成就/图鉴/统计/事件日志)。入口全数保留且落到对应栏:点右上角小地图 / Shift+M → 地图册,📜 任务按钮 / J → 任务,🏅 成就按钮 / U → 成就。三个模块(quest/meta/mapbook)不再自建遮罩,改为吐 panelHTML()+接 handleClick(),旧遮罩(#qMask/#metaMask/#mbMask)彻底退场;任务追踪条 #qTrack 照旧在 HUD 上。②「上下拉面板不会动」的真根因与修法 —— 仓库面板稳,是因为它的遮罩不可滚;而 r142 给任务/成就/铁匠/地图册的遮罩加了 overflow-y:auto(当时想给真机 vh 膨胀兜底),遮罩自己成了滚动容器,内容区滚到边界后手势会滚动链接力给遮罩 ⇒ 「往下拉内容、整块面板跟着跑」。修法三件套:遮罩 overflow:hidden(永远不是滚动容器,滚动链到此为止)+ 内容区 overscroll-behavior:contain(滚到边界就地吃掉手势,双保险)+ 内容区 touch-action:pan-y(body 全局 none 会掐手势,必须显式声明)。比仓库更进一步:表头钉在框里 —— 标题/三栏签/✕ 是面板框的第一个 flex 子项、内容区独占剩下的高度,滚动的只有内容 ⇒ 上下拉时面板框、标题、栏签、✕ 全部逐像素不动(r142 的 sticky ✕ 只保住了 ✕,标题照样被顶走)。几何与 r142 逐像素一致:撑满时 top=7dvh+56、bottom=93dvh(底部不动)。③ 同族顺手修 —— 铁匠铺(shop.js)遮罩同步改不可滚(与手册同一口径);修掉全局 .sub{display:flex}(index.html 给小地图副行用的)把手册说明文字拆成三列的冲突(#hubMask .sub 显式 display:block);sw.js 离线清单补上 r142 漏列的 js/mapbook.js(首次离线安装会「点小地图没反应」)。⚠ 无头验收的坑: Input.synthesizeScrollGesture 在无头 Chrome 的 touch 源下不产生滚动(mouse 源正常)—— 用它会把「机制没坏」误判成「滚不动」;改用手写 touchStart→touchMove×N→touchEnd 序列才测得出真滚动(诊断留痕 tools/_r143-gesture-probe.cjs)。验收 tools/r143-hub-test.cjs 63/0(单面板清场 / 三栏横排不重叠 / 任务 19 行 / 成就 4 子栏互切 / 遮罩不可滚 / 表头标题栏签逐像素不动 / 真触摸上拖下拖:内容滚、面板纹丝不动 / 仓库对照基线 / 撑满几何三断言 / 锁定卡不切图+解锁真切图 / 前往引路 / 六条开关路 / 滚到底 ✕ 仍可点 / 零页面错误)。回归 r142 31/0(选择器已同步到手册)· r139 17/0 · r141 19/0 · r128 123/0。 |
| ★★★ 地图册 + 弹窗标题被切真根因 + 角色面板能滑了 · v24r142 | 用户口径:「把勇者大陆 冰风谷 单独放进地图册;点击角色支持滑动下拉,像任务/成就/铁匠一样可以上下拉;任务/成就/铁匠点开窗口上面标题看不见,放下来一点,底部不用改动」。 ① 地图册(新模块 js/mapbook.js) —— 每张地图一张大卡(勇者大陆/失落地牢/冰风谷),写着这是什么地方、现在能不能去;点卡片即切图,走顶部地图栏同一道解锁闸(锁住的卡显示「🔒 需先通关××」并 toast 说明,绝不绕过)。入口 = 点右上角小地图(手游惯例)或 Shift+M(M 键保留旧的「循环切图」语义)。面板同任务/成就/铁匠规格:内容 overflow-y:auto 可上下滑、✕ 钉顶、点遮罩/Esc 关。② 弹窗标题被切的真根因(三个点名面板同修) —— 真机浏览器的 100vh 比「实际可视高度」大(地址栏/工具栏),老写法 max-height:86vh + flex 居中会在内容撑满时把面板顶部顶出不可滚动的溢出区 —— 标题就是这么被切没的(无头环境 100vh=视口复现不了,靠几何推演定位)。修法三件套:max-height 改 calc(86dvh − 56px)(dvh 随地址栏实时收缩,真机准确)+ box 改 margin:auto 安全居中(溢出时贴顶可达,不再有不可达区)+ mask 加 padding-top:56px(面板整体放下来一点)。底部逐像素不变:内容撑满时 top = 7dvh+56、bottom = 93dvh —— 与旧版完全一致(验收实测 top=120.0 / bottom=850.9,公式分毫不差);内容不满时整体只下移居中半量 28px、且永不超过旧底部。③ 角色面板「支持滑动下拉」的真根因 —— 不是没有 overflow(手机竖屏早就有 max-height:62vh;overflow-y:auto),而是 body { touch-action:none }(防游戏页面橡皮筋)会沿祖先链取交集掐掉一切触摸滚动手势 —— 面板上的手势被全局禁了。修:charPanel 显式 touch-action:pan-y(js/modelui.js、js/texui.js 同款先例);顺手补桌面端 max-height(以前桌面无上限,属性/装备一多直接溢出屏幕底部)+ 真机 dvh 兜上限(min(62vh, calc(100dvh − 200px)))。验收 tools/r142-mapbook-test.cjs 36/0(点小地图开册/3 卡与当前图高亮/锁定卡不切图+toast/解锁后真切图/✕与遮罩关闭/四面板几何三断言/撑满场景逐像素/charPanel 可滚/touch-action 断言/零页面错误)。回归 r128-flow-test 123/0 · r139-equip-live 17/0 · r141 19/0。 |
| ★★★ 序章目标条能关了 + 音乐关闭「失效」真根因 · v24r141 | 用户口径:「勇者大陆 序章 要有关闭功能,音乐关闭功能失效」。CDP 真机诊断(elementFromPoint 逐按钮命中 + hook AudioContext 读真实 gain): ① 音乐按钮不是逻辑坏,是根本点不到 —— 「♪ Ice Tarus」按钮在 #hud(z:30) 的 stacking context 里,z-index 再大也压不过外层:新手引导卡 #ftCard(z:36, body 顶层) 展开时「跳过 ✕」正好落在按钮上、目标条(z:34)压住 横屏/仓库/帮助 —— 实测 hit=.sk/.nm/.ob 全部 self=false。直接调 btnMusic.click() 逻辑层一切正常(on 翻转、master 0.8→0 真静音)⇒ 纯 UI 遮挡。修法两刀:目标条从右侧挪顶部居中(右侧按钮列整列解放);引导卡 z 36→28(压到 #hud 之下 —— 卡片区没有 HUD 实心元素,视觉不变、按钮恢复可点)。另给功能按钮统一 z-index:38。② 序章目标条加 ✕ 关闭 —— 事件委托绑在常驻 box 上(refreshHud 每次重写 innerHTML,绑子元素会被冲掉);点 ✕ 隐藏 + localStorage mulv_hud=0;同图读档不重弹,换图自动重新展示(新关卡的新目标有必要让玩家看到,看完再关)。③ 音乐选择持久化(第二个真 bug) —— startGame 原来无条件 MUSIC.setOn(true),且按钮 toggle 不写设置 ⇒ 玩家关掉后重开角色/读档又强制响起来;右上按钮与设置面板「背景音乐」两个入口也互不同步。修:按钮点击写回 MUSettings.set('music',now) 持久化;startGame 改为尊重设置值;设置面板改音乐时反向同步 HUD 按钮文字/状态。验收 tools/r141-music-lvbox-test.cjs 19/0(10 按钮命中全过 / 目标条顶部居中带 ✕ / 点✕隐藏+标记 / 同图重进不弹 / 换图重弹 / 点关后 MUSIC ctx master 0.8→0 真实静音 / 重进保持关闭 / 设置面板同步)。回归 r128-flow-test 123/0 · r139-equip-live 17/0。 |
| ★★★ 「进游戏看不到流光」两个根因全修 + 白天可见度 · v24r140 | 用户实机口径:「这个效果不错 但我进去没看到」。追到两条独立根因: ① 雕像流光风格从来没换过 —— model-map 里 9 个装饰位槽位的 coat 只写了 kind:'flow' 漏写 style,走的一直是 r128 的 surface 老风格,r134~r138 五轮调好的白色 45° 斜波只在 flow-lab 里有。已全部补 style:'darkwave'。② 更狠:换图后流光壳被永久拆掉 —— 启动期 9 槽 armorFlow 全部成功(启动记录器实测 ok:true×9),但 buildWorld 再次执行时 ML.detachAll() 把 coat dispose 并置 null,c.single 却留在缓存里 ⇒ 新世界 place 时 get() 见 single 在就直接复用旧实例,coat 永远不会重建 —— 表现正是「flow-lab 里效果好好的,进游戏雕像上干干净净」。修法:detachAll 补 dropSingle(c)(mixer/动作引擎一并摘干净),下次 get() 走完整 makeInstance 重建(GLB 在 roots[] 里,重建只是 normalize+绑动画,开销可忽略)。③ 白天可见度 —— 0.16 的 waveGain 是 flow-lab 暗场景口径,日光地图的亮金盔甲上几乎隐形(A/B 实测 0.16/0.35/0.55/0.80 四档同机位对比),9 槽 + 玩家装备流光 + NPC 统一抬 waveGain:0.5(波带扫过时白光明显、不炸版)。新增验收 tools/r140-statue-flow.cjs 14/0(9 槽 flow 壳挂载 / 场景 27 壳全 uStyle=2 无 surface 残留 / 帧差证明波在游戏里真的在动 / 无页面错误);增益 A/B tools/r140-gain-test.cjs;特写 tools/r140-statue-shot.cjs。回归 r128-flow-test 123/0。 |
| ★★ 穿上的装备上模型了 + 左上角那排按钮挪进右列 · v24r139 | 用户实机口径:「去掉左上角 音乐关闭 设置 这些,右边有了;实现穿个装备上去」。两件事: ① 左上角按钮排根因与迁移 —— 「📜任务 / 🔨铁匠铺 / 🏅成就 / 🔊音效 / ⚙设置」五个入口(v24r118 加的)CSS 里一直没有 position,掉进 #hud(fixed inset:0)的普通流,全部堆在页面左上角一排(真机实测 rect (0,2)~(262,2))。修法 = 补上 absolute 定位并入右侧按钮列(桌面 top 426~594 / 手机竖屏 bottom 510~678 / 横屏 bottom 246~382,都与既有列同节奏)—— 左上角清空、手机上没有键盘所以入口一个不能删。② 穿装备可视化 —— 此前装备是纯数值(equip 槽位只加属性),模型永远是职业本色,调了五轮的装备流光在玩家身上根本看不到。新增 applyEquipVisual(p):HERO3D 程序化角色按槽位把部件染成装备品质色(weapon→剑刃/杖珠/弓臂、armor→全套甲片、helm→盔件/发箍、wing→翼膜金翎、ring→胸宝石);材质铁律 = buildHero 的材质是 mat() 共享缓存(同 matKey+accent 共用一份),必须 clone 后改色、原材质记 userData.__origMat,脱下时 dispose 克隆还原(可反复穿脱)。稀有度≥2(稀有/传说)→ VFX.armorFlow 全身流光壳(style darkwave = r137/r138 调好的白色斜波上行单向循环;tier 按稀有度 epic 紫 / legendary 金;换装 dispose 旧壳重建,壳挂在骨骼下跟随动画)。接线四路:equipBroadcast 末尾(拾取自动穿/背包点击/一键最强)+ 装备格点击脱下 + __MUAPI.equipTest + 读档恢复;开局走 rollItem 送一把 lv1 武器(与掉落同一条路),一进游戏就看得见。验收 tools/r139-equip-live.cjs 17/0(真页面真进游戏:五按钮 absolute 右列且左上角无残留 / 开局短剑剑刃染 d8d8d8 / equipTest 传说剑→剑刃 c07bff 自发光 + 68 片流光壳挂骨骼下 / 脱下恢复出厂 e2eaf4 + 壳摘除 / 无页面错误);特写 tools/r139-equip-shot.cjs(传说剑+传说龙甲上身,盔甲染紫+流光);回归 r128-flow-test 123/0。 |
| ★★★ 地狱炼狱地图 + 冒险手册栏签横滑 + 新功能追加到末尾约定 · v24r153 | 用户口径:「把地狱炼狱加入冒险图册里地图面板,新功能到冒险图册里后面,标题栏可以左右滑动」。 ① 第四张地图「地狱炼狱」 —— config.MAPS 新增 hell(排在冰风谷之后、试炼场之前,需先通关冰风谷才解锁),复用 props:'stone'(石地+火盆+飘火星,不另开一套建模/贴图);新增三怪 hellhound / lavagolem / flamelord(复用 wolf/golem/lord 建模只换暗红配色与数值,数值按 level.js 目标手感反推)。地狱气质由四条通道叠加:地图 ground/fog/sky 暗红板 + game.js LIGHT_PRESET.hell(全场最暗、光源从下往上打红光)+ models.js SKY_PRESET.hell / ENV_PRESET.hell 暗红天穹与反射 + GRADE_PRESET.hell 暖暗调色;level.js 新增 hell 关卡(目标:击败炼狱魔君),地图册卡片 🔥 图标、锁定文案「需先通关冰风谷」。② 手册标题栏可左右滑动 —— #hubTabs 改 overflow-x:auto + flex-wrap:nowrap、按钮 flex:0 0 auto 按内容自然宽度排(不再均分)、隐藏滚动条;父级 touch-action 放开 pan-x(遮罩/盒子一并改 pan-x pan-y,否则真机横向手势被掐死拖不动);切栏时把当前栏签滚入视野。③ 新功能追加到末尾约定 —— TABS 数组注释写明「以后新增栏签一律追加到末尾」,栏数多了自动横向溢出滑动;本轮不折叠既有面板(仓库/铁匠/设置等),保持任务聚焦。验收脚本 tools/r143-hub-test.cjs 已扩展 [11] 地狱炼狱卡 / 锁定解锁 / 栏签横滑断言。 |
| ★★★ 菜单搬到顶部 · 扇轮图标增大两倍 · 菜单↔扇轮互让 · v24r149 | 用户口径:「菜单放在最上端,地图的下面,不要遮住地图;点菜单显示所有按钮,底部没地方放的,弄两列放,要从底部溢出;同时扇形技能隐藏,再点菜单扇形技能显示;菜单只露出部分按钮;扇形技能图标增大两倍,间隔拉大点」。六件事一次做完: ① 菜单珠搬到最上端 —— #btnMenu 从右下角(bottom:168)移到地图条正下方(竖屏 top:100 / 横屏 top:40,都贴地图条下沿且与居中地图条无交叠,实测竖屏 mapBar 44~98 / 珠 100~158、横屏 mapBar 6~28 / 珠 40~84)。右列 7 颗功能钮不再写死在 CSS,改由 layoutMenu()(game.js)统一布局:从珠下 170(横屏 96)起、58px 步进往下排,竖⇄横/旋转切换与 resize 时自动重排。② 底部放不下 → 折两列、从底部溢出 —— maxN = floor((屏高 − 列顶 − 12) / 58):竖屏 915 高排得下 7 颗(单列 170~576),横屏 412 高只容 5 颗 → 铁匠/设置折到第二列(right 70,在第一列左侧),列本身继续往底部方向延伸。实测横屏展开:左列 Help/Music/Orient/Bright/Stash(t 96~328)、第二列 Shop/Set(t 96/154、right 70)。③ 菜单 ↔ 扇轮互让 —— body.menu-open #skillFan{visibility:hidden}:点开菜单 → 扇形技能整块让位(功能钮要占右缘两列);再点菜单 → 扇轮立刻回来。④ 收起态只露部分按钮 —— 原「7 颗全藏」改为只藏 亮度/仓库/铁匠/设置 4 颗,说明/音乐/横屏 3 颗常亮(含横屏同步,收起态横屏也只露这 3 颗 + 珠 + 跳)。 ⑤ 扇轮图标增大两倍 + 间隔拉大 —— 竖屏 58→116px(终极技 64→128)、横屏 40→80px(终极技 88);槽距 16.5°→23°(横屏 17°→18.5°),半径 132→310(横屏 104→280)。槽位角必须同时满足钮不伸出右缘(β≥12°)与钮不进左下血蓝球带(β≤58°,球心距须 >84px)→ 竖屏扇面收为 3 槽(β=12/35/58,相邻圆心距 123.6px > 116px 直径不重叠,淡出边界 fHi=72 把落到球上的第 4 槽自动隐藏),横屏锚在右缘中段仍可开满 5 槽(β=6~80,圆心距 90px)。冰风谷 7 技竖屏初始露 3 藏 4,滑一下把尾端冰技转出来。 验收 tools/r147-fan-test.cjs 50/0(新增:珠在最上端且不遮地图条 / 收起露 3 藏 4 / 展开 7 颗 + 扇轮让位 + 关掉恢复 / 横屏折第二列 right=70 / 钮 116·80px / 竖屏露3藏4 / 23° 槽距吸附);回归 r143 68/0 · r142 29/0 · r141 22/0 · r139 16/0 · r128 123/0。 |
| ★★★ 扇轮横屏也开 + r147「手机看不到」的两个真凶 · v24r148 | 用户口径:「没看见扇形技能栏」。两个独立根因,一次修掉: ① 真凶一(送达):v24r147 漏改 sw.js 的 CHANGED 清单 —— 我只升了 const VER,没把本轮改过的 index.html / js/game.js 写进 CHANGED。而 sw 对 js/HTML 走缓存优先且缓存键剥掉 ?v=(见 sw.js fetch 里的注释)⇒ 没列 = 手机继续吃 r146 的旧副本,那版压根没有 #skillFan,所以「改完等于没改」。本轮把两个文件正式列进 CHANGED(装过 r146 的手机这次才被删缓存重下)。发版铁律补一条:改完代码先列 CHANGED,再升版本号。② 真凶二(设计):扇轮原来只开在竖屏 —— r147 我按「横屏右缘已被 r145 功能图标列占满」把横屏一刀切回底部技能栏;但用户发的参考图本身就是横屏截图(1080×470),他就在横屏玩 ⇒ 看到的是旧技能栏而不是扇轮。本轮横屏也开扇轮,锚点从「屏幕右下角」改为右缘中段(OX=70 让开 40px 图标列 right 6~46,OY=92 让开底部血蓝球;R=104、槽距 17°、40px 小圆 / 终极技 44px)—— 正是参考图里那圈技能的位置。③ 强制旋转也覆盖( html.rot,微信锁竖屏时页面自己转 90°):画布是逻辑坐标系,扇轮的画布尺寸取 innerHeight×innerWidth、指针坐标按 lx=sy / ly=viewportW−sx 逆映射(与 index.html 里 html.rot body rotate(90deg) 的口径一致),滑动方向在旋转模式下依然跟手。④ 底部技能栏彻底退场(扇轮一统天下,键盘 1/2/3 照旧),竖 ⇄ 横切换时几何基准变了 → 强制重建扇面并复位偏移。验收 tools/r147-fan-test.cjs 36/0(新增:横屏扇轮 7 颗、底部技能栏全程 display:none、横屏按钮不与 r145 图标列/血蓝球重叠、强制旋转模式 ?rot=1 下扇轮几何与滑动跟手;竖屏原 26 条全保留)。回归 r143 68/0 · r142 29/0 · r141 22/0 · r139 16/0 · r128 123/0。 |
| ★★★ 技能扇形轮盘:右下角 90° 扇弧排列,顺/逆时针滑动换技能 · v24r147 | 用户口径:又发一张奇迹MU截图 ——「可以露出几个菜单列表,到达右下角的隐藏,把技能排列 90° 扇形排列,可以顺时针 逆时针滑动,展示技能出来」。参考图右下角:技能圆钮沿 90° 扇弧围着角落排开,多余的技能「绕到角后」藏起来,滑动扇面旋转换技能。 ① 技能搬进扇形轮盘(竖屏/桌面) —— 新增 #skillFan:技能圆钮(58px 暗金底金边圆,冰系蓝边、终极技紫色大圆 64px,与 r145 圆图标同一家族)沿右下角 90° 扇弧排列(扇弧半径 132px,相邻技能夹角 16.5°,可见 5 槽);转出扇面(绕过右缘/底缘)的技能自动淡出并不可点 —— 正是「到达右下角的隐藏」。原底部技能栏在竖屏退场(血蓝球在窄屏改靠左排让位),横屏(html.land)自动切回底部技能栏(右缘已被 r145 功能图标列占满,扇轮没有落脚点)——⚠ v24r148 起改为横屏也开扇轮(锚点移到右缘中段),见上一条。② 顺/逆时针滑动 —— 按住弧面拖动 = 整圈旋转:指尖极角(相对右下角)实时驱动每颗技能的槽位角,手指走到哪技能跟到哪;松手吸附到最近一格(0.5 格以内归位);带 ±0.6 格橡皮筋回弹。技能多于 5 个(冰风谷 6~7 技)时滑动才换页,5 个以内整圈锁死。 ③ 施法路全保留 —— 点按技能 = 原地无转动 → tryCast(idx)(拖过就算旋转、不施法);键盘 1/2/3 照旧;冷却黑幕/倒计时/蓝量不足置灰/.ready 金边在圆钮上原样工作;竖/横屏切换时两套容器强制重建(共享 sig 复位)。回归 r143 68/0 · r142 29/0 · r141 22/0 · r139 16/0 · r128 123/0。 |
| ★★ 菜单珠:右列功能图标默认收起,一键全弹出 · v24r146 | 用户口径:又发一张奇迹MU截图「点击菜单,全部都出来」—— 参考图右列平时只露一颗红色菜单宝珠,点它整列图标弹出。 ① 菜单珠 —— 新增 #btnMenu 红色宝珠按钮(58px 圆,radial 红底 + 金边,☰ + 「菜单」),排在右列最底(手机/横屏贴右下角,拇指最容易够到,与参考图同位)。② 默认收起 + 一键全出 —— 右列 7 颗功能钮(说明/音乐/横屏/亮度/仓库/铁匠/设置)默认 opacity:0 + 向右滑出 + 不可点;点菜单珠 → body.menu-open,整列逐颗错峰滑入(stagger 0~0.24s);再点收回(标签变「收起」、珠子亮金边)。状态存 localStorage(mu_menu),下次进来沿用。跳是游戏动作键,与菜单珠一样常驻不收。回归 r141 22/0(新增:菜单珠存在且初始收起 / 点击后 body.menu-open / 展开态 7 钮 elementFromPoint 全命中自己)· 其余套件随发版复跑。 |
| ★★ 右列改奇迹MU手游同款圆形金边图标 · v24r145 | 用户口径:发来一张奇迹MU手游截图「参考这个面板」—— 右侧一列圆形金边图标(图标+内部小字标签)。 ① 圆图标化 —— 右列 8 颗按钮(说明/音乐/横屏/亮度/仓库/铁匠/设置/跳)全部改为 .g 图标字形 + .t 中文短标签 的 54px 圆形按钮(横屏 42px):radial 暗金底 + 2px 金边 + 内阴影,悬停金光;结构照参考图。② 状态表达改版 —— 原来靠按钮文字变化表达状态(♪ Ice Tarus:开/关、☀ 亮度:柔和/标准/高亮、⛶ 横屏/退出横屏),圆里塞不下长文字:音乐=「开」冰蓝描边(沿用旧口径)、亮度/横屏/仓库=金边高亮,完整状态进 title(长按/悬停可见),亮度档位另有 toast 反馈。横屏时标签变「退出」。③ 排版节奏 —— 桌面列步进 42→58px(圆 54px 不重叠),手机竖屏 56→58px,横屏步进 46px;顺手修掉桌面端「跳」按钮与「横屏」按钮同 top:300 完全重叠的老问题(跳=游戏动作键,单独排到列尾)。 ⚠ 覆盖坑 —— index.html 后段「金属拉丝」共享块( #mapBar button, #btnHelp…)在同文件更靠后、同优先级,会把圆图标的 radial 底/金边打回方形金属条:已把右列按钮从该块与对应 hover 块中摘除。回归 r141 20/0(音乐断言同步为 title+.on 口径)· 其余套件随发版复跑。 |
| ★★★ 右列/顶部入口精简 + 手册贴顶 + 上下拉真机修复 · v24r144 | 用户口径:「去掉右边的 音效 任务 成就;窗口顶上的 勇者大陆 冰风谷 3个地图,冒险手册里都有了,不要摆出来;冒险手册窗口没置顶,而且上下发动不了」。 ① 入口精简(三个按钮退场) —— 右列 📜任务 / 🏅成就 / 🔊音效 三按钮删除:任务/成就 = 冒险手册的栏签(点右上角小地图进手册切栏;键盘 J/U 仍在),音效 = 设置面板里的静音开关(同一状态同一条路,功能没丢)。右列只留 铁匠铺/设置(位置紧凑上移补齐,桌面/竖屏/横屏三套布局同步)。② 顶部地图按钮退场 —— #mapBar 不再生成「勇者大陆/失落地牢/冰风谷」三个地图按钮(改图走 MAPS 数组处),切图入口收敛为:冒险手册的地图卡(同一道 MULevel.isUnlocked 闸)/ M 键循环 / 任务「前往」引路自动切图;自动战斗/试炼/✨新功能 照旧留在 mapBar。refreshMapBar / updateSpawnUI / MUI18n 对 button[data-map] 的查询落空即空转,无需改。③ 手册「置顶」 —— r143 的面板是「padding-top:56px + 居中」,顶部空出一大块透出暗化游戏画面(用户看到的「没置顶」正是这块)。改 align-items:flex-start + padding:10px 8px 12px:面板贴着屏幕顶部(top=10px),撑满时 bottom=100dvh−12,标题永远一眼可见(dvh 随真机地址栏实时收紧)。④「上下发动不了」的真根因与修法 —— r143 自创的「外框钉住 + 内层滚」是三层嵌套滚动容器(#hubMask hidden > .box hidden > #hubBody 滚),无头里 63 项全过、真机内核就是滚不动。本轮放弃自创结构,改仓库 #stashMask 同款(用户确认过仓库在真机上「上下拉面板不会动」): .box 自己 overflow-y:auto(唯一滚动容器)+ touch-action:pan-y(声明在 .box 与遮罩上,防 body 的 none 被老内核一路取交集掐死)+ overscroll-behavior:contain;表头(标题/三栏签/子栏签/✕)position:sticky;top:0 钉在滚动视口顶、不透明背景 —— 内容从它下面滚过,面板框/表头/标题/✕ 照样纹丝不动,还少一层嵌套。⑤ 顺手的真修复 —— 手册的 keydown 原来挂 document,而 quest/meta 挂 window:从 window 派发的按键事件到不了 document(事件不下传)⇒ 统一改挂 window(两条派发路都通)。验收 tools/r143-hub-test.cjs 68/0(★ 新增退场断言:btnQuest/btnMeta/btnSfx 不存在 + mapBar 无 data-map 按钮 + 铁匠铺/设置/自动战斗/新功能保留;手册贴顶三断言;真触摸上拖下拖:内容滚 0→208→0、面板框/表头/标题逐像素不动;Shift+M/J/U/Esc/点遮罩/✕ 开关路全通;锁定卡不切图+解锁真切图;前往引路;滚到底 ✕ 仍可点;零页面错误)。回归 r142 29/0(手册贴顶口径 + 铁匠铺保持 r142 下移口径)· r141 20/0(按钮命中列表收成 7 个)· r139 15/0(右列断言同步)· r128 123/0。⚠ 测试侧教训:r144 起 #hubBody 高度=内容全高,拖动起手点必须取在 .box 的视口矩形内(拿 hubBody 算会算到视口外、触摸点被丢弃)。 |
| ★★★ 地图册/任务/成就 三栏合一「冒险手册」+ 上下拉面板不会动 · v24r143 | 用户口径:「仓库的面板正常,上下拉面板不会动;把地图册 任务 成就 放到一个面板里,里面横着分栏显示 地图册 任务 成就,都要像仓库的面板」。 ① 三栏合一(新模块 js/hubpanel.js「冒险手册」) —— 一块面板、顶部一排横向栏签(🗺 地图册 | 📜 任务 | 🏅 成就),点栏签即切;成就栏里保留 4 个子栏签(成就/图鉴/统计/事件日志)。入口全数保留且落到对应栏:点右上角小地图 / Shift+M → 地图册,📜 任务按钮 / J → 任务,🏅 成就按钮 / U → 成就。三个模块(quest/meta/mapbook)不再自建遮罩,改为吐 panelHTML()+接 handleClick(),旧遮罩(#qMask/#metaMask/#mbMask)彻底退场;任务追踪条 #qTrack 照旧在 HUD 上。②「上下拉面板不会动」的真根因与修法 —— 仓库面板稳,是因为它的遮罩不可滚;而 r142 给任务/成就/铁匠/地图册的遮罩加了 overflow-y:auto(当时想给真机 vh 膨胀兜底),遮罩自己成了滚动容器,内容区滚到边界后手势会滚动链接力给遮罩 ⇒ 「往下拉内容、整块面板跟着跑」。修法三件套:遮罩 overflow:hidden(永远不是滚动容器,滚动链到此为止)+ 内容区 overscroll-behavior:contain(滚到边界就地吃掉手势,双保险)+ 内容区 touch-action:pan-y(body 全局 none 会掐手势,必须显式声明)。比仓库更进一步:表头钉在框里 —— 标题/三栏签/✕ 是面板框的第一个 flex 子项、内容区独占剩下的高度,滚动的只有内容 ⇒ 上下拉时面板框、标题、栏签、✕ 全部逐像素不动(r142 的 sticky ✕ 只保住了 ✕,标题照样被顶走)。几何与 r142 逐像素一致:撑满时 top=7dvh+56、bottom=93dvh(底部不动)。③ 同族顺手修 —— 铁匠铺(shop.js)遮罩同步改不可滚(与手册同一口径);修掉全局 .sub{display:flex}(index.html 给小地图副行用的)把手册说明文字拆成三列的冲突(#hubMask .sub 显式 display:block);sw.js 离线清单补上 r142 漏列的 js/mapbook.js(首次离线安装会「点小地图没反应」)。⚠ 无头验收的坑: Input.synthesizeScrollGesture 在无头 Chrome 的 touch 源下不产生滚动(mouse 源正常)—— 用它会把「机制没坏」误判成「滚不动」;改用手写 touchStart→touchMove×N→touchEnd 序列才测得出真滚动(诊断留痕 tools/_r143-gesture-probe.cjs)。验收 tools/r143-hub-test.cjs 63/0(单面板清场 / 三栏横排不重叠 / 任务 19 行 / 成就 4 子栏互切 / 遮罩不可滚 / 表头标题栏签逐像素不动 / 真触摸上拖下拖:内容滚、面板纹丝不动 / 仓库对照基线 / 撑满几何三断言 / 锁定卡不切图+解锁真切图 / 前往引路 / 六条开关路 / 滚到底 ✕ 仍可点 / 零页面错误)。回归 r142 31/0(选择器已同步到手册)· r139 17/0 · r141 19/0 · r128 123/0。 |
| ★★★ 地图册 + 弹窗标题被切真根因 + 角色面板能滑了 · v24r142 | 用户口径:「把勇者大陆 冰风谷 单独放进地图册;点击角色支持滑动下拉,像任务/成就/铁匠一样可以上下拉;任务/成就/铁匠点开窗口上面标题看不见,放下来一点,底部不用改动」。 ① 地图册(新模块 js/mapbook.js) —— 每张地图一张大卡(勇者大陆/失落地牢/冰风谷),写着这是什么地方、现在能不能去;点卡片即切图,走顶部地图栏同一道解锁闸(锁住的卡显示「🔒 需先通关××」并 toast 说明,绝不绕过)。入口 = 点右上角小地图(手游惯例)或 Shift+M(M 键保留旧的「循环切图」语义)。面板同任务/成就/铁匠规格:内容 overflow-y:auto 可上下滑、✕ 钉顶、点遮罩/Esc 关。② 弹窗标题被切的真根因(三个点名面板同修) —— 真机浏览器的 100vh 比「实际可视高度」大(地址栏/工具栏),老写法 max-height:86vh + flex 居中会在内容撑满时把面板顶部顶出不可滚动的溢出区 —— 标题就是这么被切没的(无头环境 100vh=视口复现不了,靠几何推演定位)。修法三件套:max-height 改 calc(86dvh − 56px)(dvh 随地址栏实时收缩,真机准确)+ box 改 margin:auto 安全居中(溢出时贴顶可达,不再有不可达区)+ mask 加 padding-top:56px(面板整体放下来一点)。底部逐像素不变:内容撑满时 top = 7dvh+56、bottom = 93dvh —— 与旧版完全一致(验收实测 top=120.0 / bottom=850.9,公式分毫不差);内容不满时整体只下移居中半量 28px、且永不超过旧底部。③ 角色面板「支持滑动下拉」的真根因 —— 不是没有 overflow(手机竖屏早就有 max-height:62vh;overflow-y:auto),而是 body { touch-action:none }(防游戏页面橡皮筋)会沿祖先链取交集掐掉一切触摸滚动手势 —— 面板上的手势被全局禁了。修:charPanel 显式 touch-action:pan-y(js/modelui.js、js/texui.js 同款先例);顺手补桌面端 max-height(以前桌面无上限,属性/装备一多直接溢出屏幕底部)+ 真机 dvh 兜上限(min(62vh, calc(100dvh − 200px)))。验收 tools/r142-mapbook-test.cjs 36/0(点小地图开册/3 卡与当前图高亮/锁定卡不切图+toast/解锁后真切图/✕与遮罩关闭/四面板几何三断言/撑满场景逐像素/charPanel 可滚/touch-action 断言/零页面错误)。回归 r128-flow-test 123/0 · r139-equip-live 17/0 · r141 19/0。 |
| ★★★ 序章目标条能关了 + 音乐关闭「失效」真根因 · v24r141 | 用户口径:「勇者大陆 序章 要有关闭功能,音乐关闭功能失效」。CDP 真机诊断(elementFromPoint 逐按钮命中 + hook AudioContext 读真实 gain): ① 音乐按钮不是逻辑坏,是根本点不到 —— 「♪ Ice Tarus」按钮在 #hud(z:30) 的 stacking context 里,z-index 再大也压不过外层:新手引导卡 #ftCard(z:36, body 顶层) 展开时「跳过 ✕」正好落在按钮上、目标条(z:34)压住 横屏/仓库/帮助 —— 实测 hit=.sk/.nm/.ob 全部 self=false。直接调 btnMusic.click() 逻辑层一切正常(on 翻转、master 0.8→0 真静音)⇒ 纯 UI 遮挡。修法两刀:目标条从右侧挪顶部居中(右侧按钮列整列解放);引导卡 z 36→28(压到 #hud 之下 —— 卡片区没有 HUD 实心元素,视觉不变、按钮恢复可点)。另给功能按钮统一 z-index:38。② 序章目标条加 ✕ 关闭 —— 事件委托绑在常驻 box 上(refreshHud 每次重写 innerHTML,绑子元素会被冲掉);点 ✕ 隐藏 + localStorage mulv_hud=0;同图读档不重弹,换图自动重新展示(新关卡的新目标有必要让玩家看到,看完再关)。③ 音乐选择持久化(第二个真 bug) —— startGame 原来无条件 MUSIC.setOn(true),且按钮 toggle 不写设置 ⇒ 玩家关掉后重开角色/读档又强制响起来;右上按钮与设置面板「背景音乐」两个入口也互不同步。修:按钮点击写回 MUSettings.set('music',now) 持久化;startGame 改为尊重设置值;设置面板改音乐时反向同步 HUD 按钮文字/状态。验收 tools/r141-music-lvbox-test.cjs 19/0(10 按钮命中全过 / 目标条顶部居中带 ✕ / 点✕隐藏+标记 / 同图重进不弹 / 换图重弹 / 点关后 MUSIC ctx master 0.8→0 真实静音 / 重进保持关闭 / 设置面板同步)。回归 r128-flow-test 123/0 · r139-equip-live 17/0。 |
| ★★★ 「进游戏看不到流光」两个根因全修 + 白天可见度 · v24r140 | 用户实机口径:「这个效果不错 但我进去没看到」。追到两条独立根因: ① 雕像流光风格从来没换过 —— model-map 里 9 个装饰位槽位的 coat 只写了 kind:'flow' 漏写 style,走的一直是 r128 的 surface 老风格,r134~r138 五轮调好的白色 45° 斜波只在 flow-lab 里有。已全部补 style:'darkwave'。② 更狠:换图后流光壳被永久拆掉 —— 启动期 9 槽 armorFlow 全部成功(启动记录器实测 ok:true×9),但 buildWorld 再次执行时 ML.detachAll() 把 coat dispose 并置 null,c.single 却留在缓存里 ⇒ 新世界 place 时 get() 见 single 在就直接复用旧实例,coat 永远不会重建 —— 表现正是「flow-lab 里效果好好的,进游戏雕像上干干净净」。修法:detachAll 补 dropSingle(c)(mixer/动作引擎一并摘干净),下次 get() 走完整 makeInstance 重建(GLB 在 roots[] 里,重建只是 normalize+绑动画,开销可忽略)。③ 白天可见度 —— 0.16 的 waveGain 是 flow-lab 暗场景口径,日光地图的亮金盔甲上几乎隐形(A/B 实测 0.16/0.35/0.55/0.80 四档同机位对比),9 槽 + 玩家装备流光 + NPC 统一抬 waveGain:0.5(波带扫过时白光明显、不炸版)。新增验收 tools/r140-statue-flow.cjs 14/0(9 槽 flow 壳挂载 / 场景 27 壳全 uStyle=2 无 surface 残留 / 帧差证明波在游戏里真的在动 / 无页面错误);增益 A/B tools/r140-gain-test.cjs;特写 tools/r140-statue-shot.cjs。回归 r128-flow-test 123/0。 |
| ★★ 穿上的装备上模型了 + 左上角那排按钮挪进右列 · v24r139 | 用户实机口径:「去掉左上角 音乐关闭 设置 这些,右边有了;实现穿个装备上去」。两件事: ① 左上角按钮排根因与迁移 —— 「📜任务 / 🔨铁匠铺 / 🏅成就 / 🔊音效 / ⚙设置」五个入口(v24r118 加的)CSS 里一直没有 position,掉进 #hud(fixed inset:0)的普通流,全部堆在页面左上角一排(真机实测 rect (0,2)~(262,2))。修法 = 补上 absolute 定位并入右侧按钮列(桌面 top 426~594 / 手机竖屏 bottom 510~678 / 横屏 bottom 246~382,都与既有列同节奏)—— 左上角清空、手机上没有键盘所以入口一个不能删。② 穿装备可视化 —— 此前装备是纯数值(equip 槽位只加属性),模型永远是职业本色,调了五轮的装备流光在玩家身上根本看不到。新增 applyEquipVisual(p):HERO3D 程序化角色按槽位把部件染成装备品质色(weapon→剑刃/杖珠/弓臂、armor→全套甲片、helm→盔件/发箍、wing→翼膜金翎、ring→胸宝石);材质铁律 = buildHero 的材质是 mat() 共享缓存(同 matKey+accent 共用一份),必须 clone 后改色、原材质记 userData.__origMat,脱下时 dispose 克隆还原(可反复穿脱)。稀有度≥2(稀有/传说)→ VFX.armorFlow 全身流光壳(style darkwave = r137/r138 调好的白色斜波上行单向循环;tier 按稀有度 epic 紫 / legendary 金;换装 dispose 旧壳重建,壳挂在骨骼下跟随动画)。接线四路:equipBroadcast 末尾(拾取自动穿/背包点击/一键最强)+ 装备格点击脱下 + __MUAPI.equipTest + 读档恢复;开局走 rollItem 送一把 lv1 武器(与掉落同一条路),一进游戏就看得见。验收 tools/r139-equip-live.cjs 17/0(真页面真进游戏:五按钮 absolute 右列且左上角无残留 / 开局短剑剑刃染 d8d8d8 / equipTest 传说剑→剑刃 c07bff 自发光 + 68 片流光壳挂骨骼下 / 脱下恢复出厂 e2eaf4 + 壳摘除 / 无页面错误);特写 tools/r139-equip-shot.cjs(传说剑+传说龙甲上身,盔甲染紫+流光);回归 r128-flow-test 123/0。 |
| ★★★ 冒险手册「转屏后拉不动」真根因修复 · v24r152 | 用户口径:「修复冒险图册里的每个面板,支持上下滑动,看内容,参考仓库这个模块是正常的,可以上下滑动」。 ① 真根因(只在 rot 旋转 90° 模式,默认横屏走的正是它)。#hubMask 旧写法是默认 row 方向 flex + 盒子 max-height:calc(100dvh − 22px)。rot 下 100dvh 解析成物理屏高 915,但旋转后逻辑容器只有 412 高 ⇒ 盒子被撑到 891 高、超出 412 的逻辑空间,整块被甩到屏幕外(CDP 实测 rect l=−491、中心 x=−44),手指根本碰不到 —— 这就是「面板上下拉不动、看不到内容」的真凶。仓库 #stashMask 能活,是因为它 flex-direction:column 把盒子纵轴压成主轴、纵向被压缩到 410。② 修法(与仓库完全同构)。#hubMask 改 flex-direction:column;盒子 max-height:100%(对遮罩内容盒取百分比,三种画面口径一次性都对:竖屏 893 / 真横屏 390 / rot 390;rot 下 100dvh 会解析成 915 是错的,所以用百分比绕开)+ min-height:0(column flex 里让盒子真能压到内容高以下的开关)。③ touch-action 不用动。rot 下已有专用规则 html.rot #hubMask .box{touch-action:pan-x pan-y}(rot 里物理 x = 逻辑 y,必须同时放开 x 手势才能滚),仓库在 rot 下实测也靠这条活。验收:rot 模式 CDP 实测手册在屏内( l12 t264 w390 h387 全落在 412 逻辑视口内)且可滚(横拖 scrollTop→205,内容 sh2287 > ch388 溢出可滚);仓库对照同样可滚。回归 r143 68/0 · r142 29/0 · r141 22/0 · r139 16/0(累计 135 项 0 失败)。 |
| ★★★ 扇形回右下角 · 技能钮改小 · 小地图改横扁 · v24r151 | 用户口径:「菜单收起的时候,扇形技能栏在右下角位置,扇形改成显示出 4 个技能,每个技能按钮改小一点,同时把地图高改小点、长加大点,使扇形技能在右下角不跟菜单有冲突,计算好坐标」。 ① 扇形搬回真正的右下角 —— 横屏锚点从「右缘中段 ox70/oy92、R280」改回屏幕右下角 (0,0)、R=150。旧配置实测(915×412)扇面顺着右缘一路爬到屏幕顶部(idx0 中心 y=44),既不在右下角又贴着菜单列。 ② 技能钮 80→48px(终极技 88→52;图标字 30→19、技能名 15→9、倒计时 24→15),仍是 4 槽。 ③ 四槽坐标按实测矩形反推(不是推演,是 CDP 量出来反解):不探出右缘需 R·sinβ ≥ HALF ⇒ β ≥ 9.2°(取 b0=14,留 4.8°);不探出底缘需 R·cosβ ≥ HALF ⇒ β ≤ 80.8°(第 4 槽取 74,留 6.8°);相邻弦长 2·150·sin10° = 52.1px > 48px 直径不重叠。④ 顺手修掉「扇面像多露了一个」 —— r150 的 fHi=84 让第 5 颗停在 opacity .50(半透明残影肉眼可见),改 fHi=82 使其归零;验收补了「第 5 颗起必须完全透明」的断言。⑤ 小地图改横扁 168² → 240×120(用户口径「高改小点、长加大点」)—— 这块的高度就是右侧功能列的起点:实测 #mmWrap 底 106→82,菜单珠 top 110→90、整列上移 20px(列顶 164→144),扇形最上面那颗顶边 y=242 与列底 228 之间才留出余量。「不与菜单冲突」就是这么算出来的,不是目测。比例尺 1.354px/m 不变 ⇒ 横向视野 ±89m / 纵向 ±44m,看得更宽。验收 tools/r147-fan-test.cjs 61/0:新增「扇整团离右/离底 ≤180 且四颗全在右下象限」、「扇形四颗与菜单列矩形零相交」(不再用中心近似距离,直接判矩形)、「第 5 颗起 opacity 全 0」、「画布 240×120」。回归 r143 68/0 · r142 29/0 · r141 22/0 · r139 16/0(累计 196 项 0 失败)。⚠ 扇形与列之间只剩约 14px 余量:地图尺寸或功能列有任何改动,必须回 CDP 重新量相交关系,别照抄参数。 |
| ★★★ 默认横屏 · 右列只留 菜单/背包/手自 · 扇轮 4 槽 · v24r150 | 参考图 caidan3 一轮收敛: ① 进游戏默认横屏 —— 触屏设备 ROT_WANT 默认开(微信锁竖屏走 html.rot 画面自转,真横屏走 html.land),?rot=0 可退出;桌面不受影响。② 跳按钮退场 —— #btnJump 删除(空格 / 点屏幕仍可跳)。 ③ 右列只留 菜单珠 + 背包 + 手动/自动 —— 新增 #btnAuto 圆钮(与键盘 T 同走 toggleAuto,状态 .on 金边 + 文字 自动/手动);地图条里的「⚔ 自动战斗」长条退场;说明/音乐/横屏/亮度/铁匠/设置 6 颗收进菜单珠、点开才滑出。 ④ 地图变小 + 菜单贴地图下面 —— 小地图竖屏 .78→.7、横屏 .62→.5 缩放;菜单珠贴小地图正下方(竖屏 top 164 / 横屏 top 110)。 ⑤ 扇形露出 4 个技能框 —— 横屏 FAN_D 改 β=10/30/50/70(step 18.5°→20°,弦长 97px>80 直径不重叠;b0 10 保证不探出顶缘),竖屏维持 3 槽(血蓝球碰撞约束)。 验收 tools/r147-fan-test.cjs 58 项全绿 —— 新增 [0]「默认横屏」专项(rot+land / 逻辑画布 915×412 / 珠贴地图条底边 / 跳退场 / #btnAuto 上岗 / 4 槽)+ 原竖·横·旋转三套口径(竖屏段落统一走 ?rot=0 退出自动横屏后才量,否则量到的是 rotate 之后的物理坐标)。⑥ 顺手修掉一个真 bug:转屏( updateSkillUI 的方向切换分支)漏了把 fanOff 归零 —— 竖屏翻到第 3 页后转横屏,旧偏移被带进来且 fanMaxOff 只有 3,横屏扇面永远停在尾端、再滑也顶在天花板;现已随重建一并回到第一页。 |
| ★★★ 流光改「一波一波单向循环」 · v24r138 | 用户实机口径:「纹理要一波一波循环,你那明显是来回流,或者你有什么好算法」。「来回流」两个来源:① 演示 GIF 是正放+倒放的 ping-pong(观感直接来回)—— 改单向循环:把光带周期定成 8.000s(速度 0.125/s),拍 16×0.5s = 恰好一个完整周期,首尾天然衔接(帧差 6.0 与中间帧 4.8~5.4 同量级),GIF 正序循环永不倒放;② 实机里主带 0.13/s、副带 0.105/s 不同速 ⇒ 两带相对运动读成摆动 —— 改同速 0.125/s(相位 fTr2 = 2·fSlope − t·0.250,折算同速)。另加不对称高斯:行进方向前缘 k=7(陡)、后缘 k=3.6(缓),上沿锐利下沿渐散,像波浪一波波往前推(对称钟形没有方向感)。主带密度 1.0:一条爬顶的同时下一条从底部冒出 ⇒ 永续单向循环。shot 脚本支持 --frames=16。回归 r128-flow-test 123/0。 |
| ★★★ 渐近流光改「白色 45° 斜纹上行」 · v24r137 | 用户实机口径:「改为加渐近白色纹理,45 度在装备表面向上移动的效果」。两处改:① 颜色改纯白 fWaveC(不再跟品阶色 uFlwFlow 走,黑龙波那版读成紫光);② 行进坐标从纯高度改斜向投影 fSlope = dot(世界坐标−中心, uWaveDir)×uWaveInvSpan——uWaveDir 默认 (0.707,1,0.707),等相位条带与水平面成 45°、随时间斜着向上爬。⚠ 迭代教训:首版用 fAz·tilt 螺旋,披风是平面片、方位角在其上几乎不变化,斜纹在披风上读不出来;改固定方向平面波后整件模型(含披风)统一斜向,附带好处是无 atan 接缝、斜度可连续调。二版把带密度收敛(主带 1.25+副带 2.05 ⇒ 全身 3 条斜纹读成花纹 → 主带 1.0+副带降权 0.22 = 一条主光带+浓淡)。flow-lab 加「斜度」滑条(0=水平环,1≈45°)。回归 r128-flow-test 123/0(uniform 47:uWaveTilt 换 uWaveDir+uWaveInvSpan)。 |
| ★★★ 黑龙波改「渐近流光」+ 修复 sw.js · v24r136 | 用户口径:「不管披风,效果优先,渐近的流光效果,模型装备是一个整体没分开护甲/盔甲所以导致这样」。darkwave 整个换算法:主体不再是贴图花纹(整体一件的模型上按柱面 UV 平铺图案必然读成「换了贴图」),改为自下而上缓缓爬升的宽软光带——周期高斯钟形(⚠ pow(0.5+0.5·sin,n) 这种写法 arcsine 分布两端聚集,指数再大均值也压不下去,首版全身糊成冰雕就是它;exp(-d²·k) 才真「窄而软」),主带 k=5 + 副带 k=7 更窄更快 ⇒ 光连续往上淌、渐亮渐暗无硬边。贴图降级为亮度起伏(uWaveCut 改义「纹理参与度」)。峰值 ≈0.36、均增 ≈0.04(定量探针校验),底+光刚好压泛光阈走过 ⇒ 过顶起晕、离开晕退。⚠ 重大修复:r134 发版脚本的正则把 sw.js 的 const ASSETS 数组整个吞了(CHANGED 结尾是 ] 不是 ];,indexOf('];') 越界匹配到 ASSETS 尾部)—— sw.js 从 r134 起就在 install 时 ReferenceError,新 SW 装不上、手机一直吃旧缓存;本轮从 out/_sw_current.js(v24r128 备份)回植 87 项并补 flow-lab.html + darkwave.png,逐项验证文件存在。回归 r128-flow-test 123/0。 |
| ★★★ 黑龙波流光返工:暗底剔除 + 细亮线 · v24r135 | 用户实机反馈:「明显把原来的装备纹理淹没了,要在原装备纹理表面流动一层光」。三处根治:① 暗底剔除(uWaveCut=0.62)—— 贴图「暗紫底」亮度也有 0.2~0.4,直接 additive 等于整件装备罩一层紫光;现在取 fWL=max(r,g,b),低于阈值的底乘 0(原纹理全程可见),只有纹芯按 smoothstep 淡入。② 行进光窗 —— 只让一段图案在亮、窗口自下而上走(1.5 个波次扫完全身),光是在「路过」表面而不是印在装备上。③ 细亮线化 —— 波纹密度 2→4.2、剔除过渡收窄到 0.10、副层权重降 0.45:披风不再整片斑马纹,只余几条细光线上游。⚠⚠ 最大教训:中途一条 sed 弄丢 vec3 fWave = …; 的分号 ⇒ fragment 整段编译失败、效果静默消失(node --check 查不出 GLSL 错,之后两轮「调参」全在调没生效的着色器)—— 靠 CDP console 抓 THREE.WebGLProgram: Shader Error 才定位;r128-flow-test 已加「GLSL 分号守卫」断言防复发。flow-lab 加「暗底剔除」滑条 + window.S/statues 调试钩子。回归 r128-flow-test 123/0(uniform 45 个;darkwave 参数改判「存在+合理域」守意图不守数值)。 |
| ★★★ 装备流光加「黑龙波流动」 · v24r134 | 用户口径:「有没有流动的效果,弄黑龙波纹理上去行吗」—— r133 的 surface 是纯呼吸(图案不动,只有明暗),本轮补上「流动」。新增第三种风格 style:'darkwave'(uStyle=2,uniform 分支,program 仍只编译一次;surface/comet 一行未删):黑龙波能量贴图在装备表面流淌(暗紫→洋红→青白核,MU 黑暗骑士本色)。贴图 tools/r134-bake-darkwave.py → assets/textures/darkwave.png(512² 双向无缝,r115 周期噪声基建;带宽 cos(2π·5·Xu) 整数条 + 两层 fbm 域扭曲扭成龙形波纹 + ridged 撕边 + sstep 亮芯;无缝落差 0.02/255)。加载走 darkWaveTex()「占位 512² + 异步接管」—— 两条铁律照抄 dustTex(占位与真图**同尺寸**,WebGL2 texStorage2D 首传锁死;换图必须**换全新 Texture 对象**),差别是贴图不挂 material.map 而走 mat.__flowUniforms.uWaveTex(sampler uniform 每帧重绑,改值即生效)。采样:柱面世界 UV(方位角×2 + 高度,不依赖模型自带 uv —— 外壳是共享原几何有些槽没有 uv);两层同向视差上升(主 0.050/s、副 1.31× 密度 0.036/s)—— 首版副层反向快滚,合成图案「沸腾」不走「流淌」,已改同向。亮度读法沿用 r133 表面口径(fFace 正对最亮 + fGrad + 轻呼吸 0.62~1.0),芯尖 ×uHdr 过泛光阈。flow-lab.html 加「黑龙波流动」按钮 + 波纹尺度/流动速度/波纹亮度/呼吸频率四滑条(呼吸频率调 0 可看纯流动)。⚠ 工程点:node 沙箱无 Image ⇒ darkWaveTex() 加 typeof Image === 'undefined' 跳过异步接管(否则 r128-flow-test 那类 vm 沙箱崩在 armorFlow)。验收:真渲染 8 帧(t 间隔 0.5s)+ 纯流动 4 帧,相邻帧差分 12~18(图案持续变化);同 t 两帧 diff=0.000(渲染完全确定,流动来自采样滚动非噪声)。回归 r128-flow-test 更新到 121/0(新增 darkwave uStyle=2 / 参数 / 柱面 UV 采样 / surface-comet 仍零贴图 等断言)。 |
| ★★★ 装备流光改「表面呼吸发光」 · v24r133 | 用户实机口径:「市场游戏的流光特效不是这样的,装备表面发光,渐近发光」—— r128 那版是「外壳+彗尾扫光带+碎钻」,光在装备外面飘;市面 MU 类手游是装备表面自己发光、整片渐亮渐暗。VFX.armorFlow 加 style 开关(uniform 分支,program 仍只编译一次):缺省 'surface' = 表面发光(亮度按 N·V 加权——正对镜头的面最亮,这是"表面发光"而非"轮廓描边"的关键;自下而上渐变;pow(sin,1.6) 呼吸 0.38Hz≈2.6s)+ 保留 style:'comet' 一行回退旧彗尾。⚠ 数值教训:加色光层峰值冲过 1.0 会直接削顶成白雕像(第一版 0.82×1.8=1.3,实测白片、呼吸比被压到 1.35);泛光阈值 0.7 是对合成后画面算的 ⇒ 光层峰值 0.20×1.8≈0.27(底色 0.5+0.27 在亮相位越阈起泛光、暗相位退回阈下)。真渲染逐帧验收 tools/r133-surface-shot.cjs(flow-lab 定格 8 帧)+ tools/r133-luma.py:躯干亮度 0.28↔0.64 平滑呼吸(比 2.33)。flow-lab.html 加 surface/comet 风格切换按钮 + __setStyle 钩子。r128-flow-test 更新到 118 例(旧铠甲色不变)。 |
| ★★★ 龙卷风贴图去「流光」:哑光泥沙重烘 · v24r132 | 用户实机口径:「流光特效 原来都不是这样子」—— r115/r116 那张烘图实机上读成湿亮的釉面(色带顶 0.455 的「亮沙」段 + spark 打亮 0.33 + 平滑团絮 ⇒ 高光一坨坨像金属反光)。重烘 tools/r131-bake-tornado.py:顶段压到 0.26、spark 砍到 0.09、新增两层磨砂调制(高频 96² ±17% + 中频 32² ±7%)把所有平滑梯度打成砂面。体检:高亮像素 0.00%、可见区均色 0.09、无缝落差 0.01/255。真页面截图 out/r115-tw-mid-zoom.png:黑龙卷风 + 干尘颗粒,釉面消失。贴图走缓存优先 ⇒ 必须进 CHANGED(本轮已列)。验收 tools/r115-tornado-shot.cjs(dust swapped/matsOk 8/8)。 |
| ★★★ 晨间实测三连修:引导关得掉 · 带我去真走 · 打怪镜头不怼怪 · v24r131 | 用户实测口径:「新手引导窗口关不掉。点带我去,没按路线走,打怪时,不要摄像机面对怪物」(另有任务奖励领取、帮助/仓库弹窗关闭、NPC 流光,一并记档)。三条逐个追到源码根因: ① 引导窗口关不掉:纯逻辑 bug —— ftue.js 的 skip() 只设了 st.skipped=true,但 refresh() 的隐藏条件根本不看这个标志,卡片照常 display:block;而主循环 tick 在 skipped 时提前 return,连「后面有代码会把它藏起来」的机会都没有 ⇒ 点「跳过 ✕」弹个 toast、卡片永远留在屏幕上(PC 上同样复现,只是没人在 PC 上点过)。修:refresh() 隐藏条件补上 st.skipped;顺带把「跳过 ✕」从 11px 暗灰小字换成 13px + 11×13px 触点(手机手指好按)。② 带我去没按路线走:引导第 1 步是「走路」, ftueGuide('move') 原来只弹一句文字、一个字节的移动都不设 —— 「带我去」点了原地不动。修:move 步骤真的设 p.moveTarget(朝面朝方向走 9 米),FTUE 的 move 计数吃的是实际位移,走够自动翻页;loot 步骤本来就是真走的,保持。③ 打怪时镜头怼着怪:镜头咬合( updateCamera 的 CAM_SYNC,「相机跑到角色背后、人机合一」)的触发条件是「正在移动」—— 而自动战斗追怪也是在移动,且角色面朝怪 ⇒ 镜头跟着转到正面、怼着怪看。修:追怪位移处打标 S.autoMoveT,咬合条件加一道闸「最近 0.8s 内的移动不是自动战斗在走」—— 玩家自己操纵的移动照旧咬合,追怪全程镜头保持玩家上次的视角,追怪停止 0.8s 后恢复。同轮捎带的(上一轮反馈,代码已落):任务追踪条上「已完成未领取」的委托/每日,点一下直接领取(手机没有 J 键,旧文案「点 J 领取奖励」、点下去却走引路,奖励永远领不到);帮助 / 仓库弹窗同款四件套(sticky ✕ + 点遮罩 + Esc,帮助表格一滚 ✕ 就被带走是「关不掉」的另一个源);旧镇 NPC(铁匠/商人/守卫等 10 人)上 armorFlow 钢阶流光(只裹袍身+金属帽,?oldtown=1 专属)。 验收: tools/r131-live-check.cjs(真页面安卓档 + 真触控事件)· 回归 r129b-fixes-test 67/0 · r129c-quest-chain-test 42/0。 |
| ★★★ 通宵实测五连修:勇者大陆有怪 · 弹窗关得掉 · 新手自动引路 · 晚到模型自愈 · 任务步骤零静默 · v24r130 | 用户口径:「今晚加班,把UI界面任务逻辑,测试通畅,弹窗关不掉,勇者大陆没怪,点新手,没自动引路,模型有些没加载进去」。四条 bug 逐条追到源码根因(不猜),外加盘点出的两处同族缺陷: ① 勇者大陆没怪:根因是 config.js 写着 spawn:[] 而 game.js 的刷怪闸是 !map.safe && map.spawn.length —— 但 ftue 第 2 步要求「先干掉 1 只」、关卡目标「完成新手引导」 ⇒ 引导与关卡双双死锁,且 level.js 的提示还写着「这里不会刷怪」。修:把 spider / wolf 填回勇者大陆(全仓查证:这两种怪原本没有任何地图引用,显然就是留给它的)、spawnR:[46,74](城堡最大半径≈34、出生点在南城门洞 (0,32),环最近 46m ⇒ 镇中心天然安全)、safe 收窄成「镇中心安全区」语义(只剩入场提示在用)、刷怪闸改只看 spawn.length(试炼场 spawn:[] 依旧不刷,绕开 pool[i%0] 的 TypeError);level.js 提示改成「怪物在城门外的野地里」。② 弹窗关不掉:任务 / 商店 / 角色三个面板的 ✕ 是 position:absolute;right:12px 挂在 700/720/740px 定宽盒子上 —— 手机竖屏(375~412 CSS px)下 ✕ 落在屏幕外,遮罩却照收事件 ⇒ 真关不掉;✕ 还在 overflow-y:auto 滚动体里,一滚就被带走;且三条关闭路一条都没有(settings 两样都有,这三个没有)。修(三面板同款四件套):宽度 min(94vw,Npx) + ✕ 改 position:sticky 钉死在滚动体上方 + 点遮罩空白关闭(判据严格 === mask,点内容不误关)+ Esc。③ 点新手没自动引路:全项目原本没有一个字节的导航能力(追踪条 pointer-events:none、任务行只有 领取/已完成/未解锁 三种按钮)。新增 navigateToQuest(game.js):步骤写 map → 切图;写怪 id → 从各地图 spawn 表反查住哪张图;落到本图后走向最近的同类活怪(p.target,自动战斗无缝接管),怪没刷出来就走向最近刷怪点等(moveTarget + 压住自动战斗)。切图与地图栏走同一道闸 MULevel.isUnlocked,锁住就把 lockReason 原样说出来;试炼场一律拒(「要由试炼系统带进去」)。移动只复用「点地移动」那两条原语,不开第二套移动逻辑。入口接了三处:任务日志的「前 往」按钮、追踪条条目(容器仍 pointer-events:none ⇒ 空白处照样穿透到画布)、新手卡新增绿色「✦ 带 我 去」按钮(ftueGuide 把引导七步逐一映射成动作)。④ 模型有些没加载进去: ML.preload 是 25s 竞速,弱网 / 隧道下超时先到 ⇒ 世界带着「已就绪」建起来,晚到的模型落进 cache,但 ensure() 从不 notify() ⇒ ML.onChange 不触发 ⇒ 摆件永远不上场景,且无报错、无 failed、连失败 toast 都没有,只有刷新能救。修:ensure() 在 return 前 notify(key)(晚到自愈);超时改非静默(state.timeoutHit + warn,ML.status() 可读 timeoutHit/slots)。⑤ 任务步骤零静默(盘点出来的同族缺陷): gold/item 两类步骤(s2 商人的账本 / d3 每日进账 / s4 长老的好奇)原来点「前 往」屏幕零反应(函数返回空串,调用方 if(msg) 直接吞掉);boss:true 但没写 id 的每日(d2 击败 1 只首领)原来按「谁最近」挑目标,会把人带到小怪身上打死也不涨进度。修:补 gold/item 分支、兜底从空串改成必有话说(以后新增 kind 忘补分支也只是提示「按任务描述手动完成」)、首领判据抽成 match() 与刷怪点同源(def.boss)。验收: tools/r129b-fixes-test.cjs 67/0(静态)· tools/r129b-live-check.cjs 21/0(真页面安卓档:场上 4 只怪 / 三条关闭路 / 点追踪条真的拿到 target / ext_* 12 节点 / 零报错)· tools/r129c-quest-chain-test.cjs 42/0(把 game.js 的 navigateToQuest 原文切片进沙箱跑:19 条步骤全走查零静默、跨图/解锁闸/试炼场、首领挑首领、解锁链无死锁的图论检查、引导七步全覆盖)。 |
| ★★★ 渲染流水线体检:把「不该渲染的」从每帧里拿掉 · v24r129 | 用户口径:「优化下,画面渲染流水线,不求东西多,只求满足大多手机流畅玩得起,该渲染的不该渲染的管理好」。 先量再改:新写 tools/r129-pipeline-probe.cjs —— 真页面 + 安卓 UA + 412×915@2 + 触屏模拟,在页面内包 renderer.info.reset 划帧、包 render.render 按「趟」归因 calls/三角面/CPU-ms、再给每个可绘制对象挂 onBeforeRender 拿到逐物体三角面归属(这才逮到真凶)。同口径实测四件事:① 软粒子的「深度预渲染」= 把整个可见场景又画一遍(两趟三角面几乎逐字节相同:86.4 万 vs 86.4 万),而且它把加法混合的发光外壳当成不透明几何写进深度图(three 的 allowOverride 默认 true)—— 既踩脏深度图(粒子飘到光效后面会被误判成贴着表面而淡掉)又白付几何。修法:新增 LAYER_NO_PRE 免渲染层(MUSoft.noPre/sweepNoPre,modelloader.coatStack 统一登记所有外壳)。用层不用 visible:perf.js 的对象池正拿 visible 收池,抢它会复活已收构件。② 单个摆件吃掉三分之一: prop:court_center 的 fairy_real.glb 是 286,040 面(同类模型 5~8 万),每帧 57.2 万面。新增「超重型单体摆件」判据(?softhipoly= 默认 20 万面):⚠ 只看单个 geometry 且排除 InstancedMesh(乘实例数的话铺地草块个个过线,一踢就把地面踢出预渲染、方向全反);⚠ 不能排除 SkinnedMesh —— 这个头号目标因 rig:"fairy" 被装骨后本身就是 SkinnedMesh。③ 预渲染原本是常驻开销:旧判据「粒子数 ≥ 32」被开局常驻的 346 颗环境粒子恒真顶住。改成 need 举手 ∨ 技能 Mesh ∨ 粒子数 ≥ WORTH(600)。举手判据是「成批」还是「逐帧滴」(burst/shards/mist + 天气举手;天空火龙每帧撒 1 颗余烬这种常驻装饰不举)—— 一开始把举手放进了 Pool.emit/update,那条龙就把闸门永久顶死(诊断显示 need 一直满格、a1 池常驻 15 颗),是探针的「谁在举手」诊断逮出来的。④ 点光源按档定量( PLIGHT:low 0 / 手机 2 / 桌面 7)—— NUM_POINT_LIGHTS 是编译期常量、灯循环按全屏像素付,运行时改 visible 会让全材质重编译,所以只能在创建时定。另修:手机 q=high 原本在 MSAA×2 之上又叠了一趟 FXAA;泛光模糊写死 3 轮 → BLOOM_PASS(手机 2 轮)。结果(同口径 412×915@2 · SwiftShader):趟数 7→6、draw call 304.6→156.4(−49%)、三角面/帧 172.9 万→86.6 万(−50%)、帧间隔 715.9ms→275.8ms(−61%)、点光源 6→3。 ⚠ 想对比排除前后画面有没有差别: ?preold=1 恢复旧口径(预渲染画全部可见物体),真机刷新一次即可 A/B。关:?softfx0=1 / ?softhipoly=0 / ?lights=N / ?bloompass=N。验收 tools/r129-perf-test.cjs 85/0(把真实表达式抠出来求值 + 遍历 model-map 里 12 个真 GLB 逐个数面校验阈值;另配 tools/r129-tunnel-qr.cjs 出隧道二维码页,tools/tunnel-check.cjs 走公网链路真机口径复核)。 |
| ★★★ 法师龙卷风改成「泥沙尘贴图」的黑龙卷风 · v24r115 | 用户口径:「把法师的龙卷风,变成贴图的,看起来有黑龙卷风模样。有泥沙尘混合体的效果」。 ① 贴图换成离线烘的「泥沙尘混合体」。旧版(r86)是 canvas 现搓的 swirlTex:44 根白条 + 若干横向细纹,配色淡紫 —— 没有噪声算子就搓不出湍流(团絮 / 撕边 / 沙粒),静态读起来就是「条纹柱」。新增 tools/r115-bake-tornado.py 用 numpy 烘一张 512²、u/v 双向无缝的沙尘图(assets/textures/tornado-dust.png):①拉长的气流条纹(ridged 各向异性 4:1)②团絮尘块(fbm)③沙粒闪光(超高频 + 独立 res,这是「沙」区别于「烟」的关键)—— 密度场过阈值化撕出透明缝隙,色带从近黑(0.035)→黑灰→暗泥灰→泥褐→土→沙→亮沙,低密度偏灰(尘)/中段偏褐(泥)/高密偏黄(沙)=「混合体」。② 混合模式 additive → Normal(三层全改)。加色混合里没有黑色(黑=加 0=彻底隐形,就是黑龙波当年那条教训)⇒ 要走「黑」只能 Normal;贴图自带 alpha(17% 全透)所以不会糊成实心板。 ③ 配色按泥沙分层:外层亮沙尘幕 / 中层土黄 / 内芯近黑(黑柱核心);粒子、扬尘裙、贴地尘环同步换色; model-map.json 法师槽位 fxColor 0xBFB4D4→0x9D8A68、fxColor2 0xC08CFF→0x6A5A42。★ 踩到并修掉两个坑(都写进代码注释了):(a) 贴图加载必须照抄火那套「占位 + 异步换图」 —— 占位与真图尺寸必须一致(WebGL2 的 texStorage2D 会把 1×1 占位锁死,之后传 512 报 Offset overflows texture dimensions),且换图必须换全新 Texture 对象(Texture.image setter 不动 source.version,只写 image+needsUpdate 一个像素都不传);顺手加了 VFX.dust.info() 自检(材质有没有全指到新纹理)。(b) 无缝的两个隐藏约束:域扭曲不能用含 shear 的坐标采样(v 边界会错开 ⇒ 一道横缝,实测 34.9/255),且 shear 必须是整数( shear·nu 要对每一项都是 res 的倍数,含 nu=8 那几项)—— 修完 0.02/255。滚动 offset 由 tornado 每帧从单例现取(闭包持有旧对象会在换图后「定格」)。验收: tornado.html(演示页,配色同步改泥沙)、tools/r115-bake-tornado.py(烘图 + 无缝/alpha/暖度体检)、tools/_dust-try.py(参数 A/B 对比)。 |
| ★★★ 「火在实机里看不见」的根因修掉了 + 火放大到看得见 · v24r114 | 接 v24r113(火刚改成材质贴图烘的序列帧)—— 图集离线量下来明明是对的(flame 块暖度 +0.85、占据格高 99%),可实机火盆上空空如也,而且一个错都不报。 ① 根因在 three 的两道门,不在贴图。原来靠 tex.image = img; tex.needsUpdate = true; 把占位图换成真图,这条路一个像素都传不上去:(a) 版本门 —— WebGLTextures.uploadTexture() 的判据是 source.version !== sourceProperties.__version,而 Texture.image 的 setter 只做 this.source.data = value、不动 source.version,needsUpdate = true 加的是 texture.version,白设;(b) 尺寸门 —— WebGL2 下 three 首次上传走 immutable texStorage2D,1×1 占位图把显存纹理锁死成 1×1,之后再传 1024×1024 只能走 texSubImage2D ⇒ GL_INVALID_VALUE: Offset overflows texture dimensions。修法:换一张全新的 Texture 对象(新对象在 properties 里没有记录 ⇒ initTexture 里 forceUpload = true ⇒ 全量重分配 + 全量上传),并同步所有已注入的粒子池材质。只有「先建粒子池、后异步换图」这条实机路径会中招,所以 r113-flip-min.html 加了 ?async=1 确定性复现它。② 机制通了,但火仍然「小到看不见」。粒子池的 size( 0.34×PSIZE=0.255)是火/冰/极光/火花共用的,动它等于把冰霜法力一起放大;而 flame 块只被 fire 用 ⇒ 在顶点着色器里按 aFlip.y 只给火单独放大 gl_PointSize(FLAME_POINT_GAIN=2.5)。贴图侧同时把火苗在格子里烘得更实(实心面积 11.6% → 18.1%,原来上部全是 alpha<0.1 的丝,「有形」但不成像)。③ 教训:改 shader 注入必须同时换 cacheKey 版本号( muFlip1→muFlip2)—— three 用 cacheKey 命中已编译 program,只改 onBeforeCompile 而不改 key 会直接复用旧 program、注入静默失效(本项目为此白跑过一整轮实验)。实机复验:图集 {ready:true, 1024×1024, blocks:4}、自检 checkUpload → ok:true(三个池的 map 全部指到新纹理)、火盆旁 40+ 颗火焰粒子、控制台不再有 GL_INVALID_VALUE;同机位 1x / 0.4x 对照图里,0.4x 只有几个零星橙点、1x 是成片橙黄火焰。验收: tools/r113-firepit-hero.cjs(火盆特写:藏 UI + size 对照 + 自动裁放)、r113-flip-min.cjs(十几秒验证图集/注入/取帧)。 |
| ★★★ 火改成「材质贴图」烘的序列帧 · v24r113 | 用户口径:「不怎么像火,用材质贴图的特效放上去看看」。 ① 为什么上一版不像火(找的是具体原因,不是感觉)。v24r111 的序列帧是在 Canvas 2D 里用 createRadialGradient 画一圈软圆点拼出来的 —— 堆再多层都还是「边缘平滑的棉花糖」。真火有三个硬特征:(a) 黑体温度渐变(芯白→黄→橙→暗红)、(b) 边缘被湍流撕成丝而不是光滑圆、(c) 整体随生命向上翻卷。而 Canvas 2D 没有噪声算子,这三条一条都做不出来。② 换材质贴图。形态挪到离屏烘焙: tools/r113-bake-firetex.py 用 fBm 值噪声 + 域扭曲 + 阈值化燃烧场 + 黑体色带 算出 16 帧,烘成 assets/textures/flip-atlas-r113.png(1024×1024,8 列 16 帧 × 4 块),flipbook.js 只负责加载 + 注入 shader。噪声粒子图必须有阈值化(smoothstep(阈值, 阈值+宽, 噪声))才出丝 —— 第一版把噪声乘在椭圆遮罩上,16 帧全是一模一样的蛋,就是漏了这一步。③ 火单独占一块( flame),因为 burst 是全服共用的。vfx.js 按尺寸分档选块(<0.22 dot / <1.05 burst / 其余 puff)⇒ 法力、冰霜、治疗、圣光全在套 burst。给 burst 烘上火色,法力(顶点色冷蓝)会被乘成暗棕。所以新增第 4 块 flame 只给 fire 用,burst 保持中性白(换成新湍流形态但不带固有色)。④ 辉光层 + 边缘窗(两个都是被叠加逼出来的)。只有火舌时 additive 叠 30 个 = 30 片硬边贴纸拼贴(一眼假);真火每颗粒子是「一大团柔光 + 中间一颗锐利火核」,于是把两层烘进同一张图(按 alpha 加权平均 rgb、alpha 相加)。更隐蔽的是:高斯辉光在格子四角有 ~0.17 残值,单个 sprite 看不出来,叠 30 个就浮出方块轮廓(整团变马赛克)⇒ 辉光乘四边 smoothstep 边缘窗归零。 ⑤ 缓存与兜底。 assets/textures/ 在 SW 里是网络优先,图集另补进 ASSETS(离线首次安装也要有);加载失败自动回落 Canvas 烘焙(否则贴图停在 1×1 透明 = 所有粒子永久不可见,且是静默的)。?flipframes= 仍可强制走旧路调参。验收: tools/r113-firetex-check.cjs。 |
| ★ 暴风城南门放一盆「真烧起来」的火 · v24r112 | 用户口径:「放在风暴城就得」。出生点斜前方(2.2, 0, 28.4)固定摆一盆石火盆。① 占位火苗关掉:models.js 里 brazier 的火苗是个整球(IcosahedronGeometry(0.5)),看着就是「挂在盆上的发光球」;② 改由粒子真烧:fire 挂两条发射器(PE.update 里有单帧 10 颗上限,一条不够密)+ ember 一条 + smoke 一条;③ offset 的方向:它是相对火苗物体的局部偏移,火苗锚点在 y=1.25、盆沿约 0.9,所以要往下压 0.42 才贴着盆沿烧(再低会被封口的盆体挡住);④ 火光忽明忽暗:一盏 PointLight,用两个不同频率的正弦叠一点随机推 intensity(纯 Math.random() 每帧全跳,看着像坏掉的日光灯)。?nofire=1 关掉。 |
| ★★★ 序列帧(flipbook):粒子不再是「一堆圆点」· v24r111 | 接着「技能跟商业级别差太远」这条往下补的第三件事(前两件:软粒子 v24r109、顿帧 v24r110)。 ① 病根:形状不演化。原来每个粒子从头到尾都是同一个圆( circleTex),
只是大小/亮度在变。而商业特效里的烟、爆燃、涡流全是烘焙序列帧 ——
形态本身在演化:烟从紧实的核向外翻卷、边缘长出絮状;爆燃先是一个白热亮点、再炸开成舌状。
形状不演化,粒子永远是「一堆圆点」,这是最一眼看出「网页 demo」的地方。② 程序化烘焙,不是找美术要图。 vfx.js / particles.js 一直在用 Canvas 2D
现场画贴图(circleTex / waterTex / swirlTex …),
零外部资产、离线自洽。这里沿用同一条路:把一个「形态函数」在 8 个时间点上各画一遍、拼成图集。③ 一张图集切成三「块」,不是一刀切。一个 Pool 只有一张贴图,可一池里混着 性格完全不同的粒子 —— 冰晶/火花/血滴/水滴要的是锐利的小亮点, 烟/尘/雾要的是蓬松的团,火/圣光要的是炸开的舌状。 给一池所有粒子套同一套序列帧,必然有一类被毁掉(火花变成一团糊)。 ⇒ 图集切成三块,每颗粒子自带 block 下标: dot(8 帧完全相同 = 老观感,不想改的预设零回归)/ puff(烟尘,外沿实测 0.145→0.466,约 3.2 倍膨胀)/ burst(爆燃,0.122→0.467,约 3.8 倍,比烟更急)。 于是「要不要序列帧」变成每个预设一个字段: smoke/dust/frost/mist/aurora→puff,fire/holy/mana→burst,ice/spark/blood/water→dot。 ④ 四个坑(都在 js/flipbook.js 注释里)。
(a) 图集必须关 mipmap —— mipmap 是把整张图集跨格子降采样的,
开着采样某格会混进隔壁格的内容(末帧膨胀到格边时最明显,会看到「半个别的帧」);
(b) uv 的 v 轴是反的 —— canvas 第 0 行在顶部、纹理 v=0 在底部,
多一块还多一层行号换算((块数−1−block)×每块行数 + (每块行数−1) − 帧行)。
忘了翻帧序会上下颠倒,而首末帧形态接近 ⇒ 肉眼几乎看不出来,只会觉得「动画怪怪的」;
(c) 进度必须走 per-particle 属性 —— 用 uniform 推全局时间会让同一池所有粒子
整齐划一地「一起呼吸」;
(d) customProgramCacheKey 必须串联不能覆盖 —— 软粒子(v24r109)
也在改同一批材质,谁后注入谁覆盖 ⇒ 两个 shader 结构不同的材质拿到同一个 program cache key
⇒ 复用错 program,症状是「软粒子时不时失效 / 序列帧时不时不动」,随机且极难复现。⑤ 怎么在没有美术、没有肉眼的前提下验「会不会被裁出直边」。 模块自带 FLIP.audit(),逐块逐帧量最外沿(单位=格),
离线单测直接断言 ≤ 0.49 —— 不靠看截图。
行号换算那条也是这么验的:让假 2D context 把「每帧画进了哪个格子」记下来当基准,
再按 shader 公式算一遍采样格子,两者对不上就是行号写反;
还加了一条反证(故意去掉翻转,测试必须能抓到),确认这条断言不是空跑。⑥ 旋钮与降级。 ?flip0=1 整体关掉(退回老圆点,行为与 v24r110 完全一致)·
?flipframes= 帧数 · ?flipsize= 格子像素。
模块没加载时两个粒子池原样不动,继续用 circleTex。验收: tools/r111-flipbook-test.cjs 100/0(形态函数 / audit / 关 mipmap /
shader 注入 / 行号换算 vs 烘焙布局 / cacheKey 串联 / 接线源码约束 / 版本戳);
tools/r111-flipbook-live.cjs 真页面(GL 无报错 = 注入后的 shader 真的编译链接通过了、
三个池与 VFX 四档池都接上 aFlip、smoke→puff / ice→dot / fire→burst 落到运行时、
推帧后进度在涨、同一池各颗进度互不相同)。 |
| ★★★ 打击顿帧(hitstop):每一刀都「打在肉上」· v24r110 | 接着上一轮的软粒子往下补「打击感」。查看下来:项目里**已经有**两套慢动作
(S.slowUntil 1.1 秒 0.28 倍、slash.js 的 slowmo 0.3 倍),
但没有顿帧 —— 而这两件事的语义完全不同:慢动作是拉长时间、强调「这一刻很重要」(大招、绝杀),持续 0.3~1.1 秒,缓入缓出; 顿帧是几乎不动、强调「这一下打中了」(每次命中),持续 0.03~0.14 秒,硬切硬回。 拿慢动作代替顿帧,只会得到一个软绵绵的「卡了一下」。 ① 分级按「节奏」而不是按伤害数值。伤害可以几万,但手感上它就是「一下」—— 按数值缩放会让小怪也卡半秒。真正该拉长的是重拍: 普通 30ms < 打 Boss 45ms < 挨打 50ms < 暴击 60ms < 击杀 90ms < 击杀 Boss 140ms < 阵亡 130ms。 冻结时的时间缩放取 0.04 而不是 0(60ms 里世界只走 2.4ms = 肉眼完全静止, 但 x *= dt 的累积量不会彻底停住,避免除零那类只在真机上偶发的问题)。② 三个坑(都在 js/hitstop.js 注释里)。
(a) 时长必须用真实时间算 —— loop 里 S.time += raw(真实秒)
而 dt = raw * timeScale,所以 S.time 本身就是真实时间;
一旦换成任何被缩放过的时钟,冻结期间它几乎不涨 ⇒ 永不到期 = 游戏卡死;
(b) 恢复必须硬切 —— 原来那套 lerp(timeScale, 1, 0.12) 从 0.04 拉回 1
要四十多帧(≈0.9 秒),那样就不是「顿一下」而是「慢慢加速」,观感全错;
(c) 连续命中要封顶 —— 否则砍进怪堆每帧都命中、每次都「从现在起再冻 60ms」,
时间线被越推越远,整个游戏黏住不动。现在一轮冻结总长有上限(0.2 秒),
连击可以延长(有「连打」的爽感)但绝不越过它。③ 与慢动作的优先级。 HITSTOP.step() 返回的是「接管 / 不接管」
而不是一个数字 —— 冻结期间接管(0.04),解冻那一帧硬切回「当时的真实速度」
(有慢动作就回 0.28、没有就回 1),其余帧不接管、原样走旧的缓动。
直接返回 1 会把慢动作覆盖掉,返回 0.28 又会在没慢动作时凭空变慢。④ 验收时又逮到两个真 bug(都是「看着像好的、其实是坏的」那种)。 (a) 两个时钟不同源。起初 fire() 收调用方传进来的 S.time,
而 step()/stats() 不传时退回 performance.now() —— 两者
不同源(一个从游戏启动算、一个从页面加载算,实测差几百秒)。
后果极隐蔽:fire 明明成功、left 也算对了,但 stats().frozen 永远是 false,
因为 until 和「现在」根本不在同一条时间轴上。
⇒ 改成模块自持唯一秒表,调用方**不必也无需**传时间(也就没有传错的可能)。(b) 一轮上限没跟着时长系数走。 ?hitstopmul= 调大时,小档变长了、
大档却被硬编码的 0.2 秒上限掐回来 —— 「击杀 Boss」和普通命中一样长,那一档等于白设。
⇒ 生效上限 = 基准 × 系数。真页面上就是这么发现的:setMul(20) 请求 2.8 秒,
被钳回 0.2 秒,逐帧轨迹里一帧都采不到。⑤ 验收证据(真页面逐帧采 S.timeScale)。
1.000 → 1.000 → 0.040 → 0.040 → 1.000 → 1.000 ——
冻结两帧后一帧之内跳回原速,中间没有任何 0.3/0.5/0.7 的缓动中间值。
这一条同时证明了「真的接管了渲染循环的 dt」和「恢复是硬的」。⑥ 低帧率下的验收取法。软渲染的 headless 实测只有 ~1fps, 而默认那一档只有 0.03~0.14 秒 ⇒ 跨不满一帧、整帧被跳过、一点都采不到。 所以:默认值用「同一次 JS 调用内读快照」验(不依赖帧率), 逐帧形状用 setMul(20) 放大到秒级再验。旋钮: ?hitstop0=1 关掉 · ?hitstopmul= 时长总系数(默认 1,真机调手感)·
?hitstopscale= 冻结时缩放(默认 0.04)。自检:HITSTOP.stats();
运行时也可 HITSTOP.setMul(2) 现调手感。
验收:tools/r110-hitstop-test.cjs 44/0(整个模块在 node 里真跑,注入假时钟)
+ tools/r110-hitstop-live.cjs 22/0(真页面 · 真进游戏 · 逐帧采 timeScale)。 |
| ★★★ 软粒子(Soft Particle):粒子贴地/贴墙不再被硬切一刀 · v24r109 | 起因:技能特效「跟商业级差太远」。查下来根因不是缺编辑器 —— skills.js 自述
「全部程序化几何 + 加法混合,零贴图依赖」,实测 flipbook 序列帧 0 命中、软粒子没有、顿帧没有。
软粒子是其中性价比最高的一项:改一处 shader、全体粒子受益。① 软粒子是什么。圆形粒子插进地面时,一半在地上一半在地下 —— 那条直挺挺的交界硬边 就是「这是贴片」的头号破绽。软粒子按「粒子离身后表面还有多远」把 alpha 淡掉 ⇒ 交界消失。 ② 为什么必须单独准备深度(本轮唯一的真难点)。主渲染那趟 sceneRT 的
depthTexture 正挂着当 depth attachment,粒子此刻去采样它 = feedback loop
(同一张纹理既写又读,WebGL 未定义行为)。⇒ 新增 js/softfx.js:
先用独立 RT 渲一趟「不含粒子的深度」(MUSoft.pre()),粒子再读那一张。
成本全在这一趟,省法是:深度图半分辨率、只写深度不写色、关阴影更新、
不值得跑时整趟跳过(下面 ⑤ 的两道闸门)。③ 三个坑(都在代码注释里记着)。 (a) 注入与设层必须成对 —— 只注入材质不挪到软粒子层,对象会被算进自己的深度图 ⇒ 自己跟自己比 ⇒ 差值 0 ⇒ 整个淡成透明直接消失; (b) 淡出方向是 sceneZ − fragZ(粒子在表面前面,后面早被 depthTest 挡了),
写成反的会把悬空的正常粒子也糊掉 —— 这种 bug 一眼看不出,只会觉得「整体变虚」;
(c) 锚点必须是 premultiplied_alpha_fragment —— three r185 的
points_frag 没有 dithering_fragment(basic_frag 才有),
拿它当锚点会在粒子上静默失效(不报错、功能没了、看不出来),已用 ShaderLib 实测确认。④ 防重复注入用 WeakSet 而不是 userData 标记。three 的 Material.copy()
对 userData 是 JSON 深拷贝、但不复制 onBeforeCompile ⇒ 特效每次 cast
clone 出来的材质既丢了注入、又带着标记 ⇒ 用标记会永久漏掉。WeakSet 认实例,新实例会被补注入。⑤ 成本闸门(跑回归时才发现的)。软粒子上线后 r108 回归挂了一条:
同样时间窗内采样帧数从 120 掉到 116。查下来是判据太松,两道闸门依次补上:(a) 「有没有粒子」要看 drawRange 而不是 visible ——
粒子池是预分配的(一次性开 1500 个顶点,靠 setDrawRange(0,n) 控制画几个),
空闲时 visible 仍然是 true。只看 visible 的话,开局什么技能都没放,
软粒子层里就有 7 个常驻空池被当成「有粒子在场」⇒ 每帧白跑一趟全场景深度渲染。(b) 「值不值得」看粒子总数(默认 < 32 颗整趟跳过) —— 补完 (a) 还是不够: 一进游戏,场景里本来就有十几颗环境火星在飘,为它们付一整趟场景渲染同样不划算, 而零星几颗淡不淡肉眼根本不分 ⇒ 判据从「有没有」改成「值不值得」。 非粒子的技能特效(Mesh)不受阈值限制 —— 它们数量少但个体大,有一个在场就照跑。 修完实测:常驻环境粒子 12 颗 < 阈值 ⇒ pre = 0(零常驻开销);
撒一批技能粒子(172 颗)⇒ pre 立刻开始跑;r108 走动采样从 116 帧回到 224 帧。旋钮: ?softfx0=1 关掉 · ?softfxscale= 软过渡距离(米,默认 0.6)·
?softfxres= 深度图分辨率倍率(默认 0.5)· ?softfxmin= 触发预渲染的
最少粒子数(默认 32)。自检:window.__SOFTQ / MUSoft.stats()。 |
| ★★★ 碰到东西不再抖:改用「遮挡物淡出」(业界标准解法)· v24r108 | 用户口径:「碰到东西不要抖,像地震一样,头晕,不然就穿墙吧,或者你有什么好算法吗」。 ① 先量清楚抖在哪(这一步不能省)。新写了 tools/r107-shake-diag.cjs 逐帧采相机世界坐标,结果出乎意料:视距是锁死的(22.000~22.000),抖的不是距离,是 camSlideAzW(挪位目标)——154 帧里 19 次单帧跳变,单次最大跳 0.96 rad = 55°。一帧之内镜头方位跳 55°,这就是「地震」。而且它是 r107 自己引入的:上一轮把挪位幅度从 66° 放大到 85°,候选更极端,一选中就跳得更狠。② 换个方向:不要动相机。r105(绕开)/ r106(拉近)/ r107(绕开不收镜)三版全在动相机,而只要动相机就没有不抖的解 —— 障碍在时间上是时有时无的,任何「绕」的决策都会反复改主意。 ③ 业界标准解法:遮挡物淡出。相机位置 / 角度 / 视距全部锁死、一动不动,改成把挡住视线的那个物体变半透明(保留轮廓,看得见角色)。《原神》《塞尔达》《魔兽世界》都是这么干的。相机不动 ⇒ 没有任何运动 ⇒ 零抖动。 ④ 可行性先验证再动手。实测老都铎小镇的可碰撞物是 113 个独立 Mesh、每个 1 个碰撞盒,粒度刚好 —— 只淡那一面墙 / 那根柱子,不会整片消失。 ⑤ 两个实现坑(都踩了)。(a) 材质共享:269 个材质里 106 个被多个物体共用,直接改 opacity 会连坐(淡一面墙 = 半个镇子一起透明)⇒ 淡出时给这个 mesh 克隆一份专用材质。(b) shader 重编译:transparent 由 false 改 true 必须 needsUpdate,而那会触发 shader 重编译(手机几百 ms 硬卡)⇒ 只在首次设一次,之后永远保留克隆材质(不还原、不 dispose),只改 opacity。⑥ 挪位(r103 那套让路)默认关闭 —— 它就是 55° 跳变的来源;淡出已经解决了遮挡,不再需要它。想对比: ?camslide1=1。开关: ?camfade0=1 关掉淡出(退回纯穿墙)· ?camfadeop= 淡到多少(默认 0.18)· ?camfadetau= 淡入淡出时间常数(默认 0.10 秒)· ?camslide1=1 开回旧挪位。验收( tools/r108-fade-live.cjs)22/22:不转镜头段 168 帧 —— 相机位移 max 0.220 m、二阶差分 max 0.151(r107 同口径 7.513,↓98%)、抖动比 0.198;挪位跨度恒 0.000 rad;被实心挡的 89 帧全部在淡(100% 命中);视距恒 = 用户设定值;静止 0 抖动。回归全绿:camcol-test 48/0 · r106-wall-test 19/19 · r103-cam-slide-test 22/22 · r103-cam-slide-live 16/16 · r105-pinch-dist-live 20/20(判据去掉了「挪位必须介入」那一条 —— 本版故意不动;挪位相关能力改由单测守)。 |
| ★★ 相机改成「只让路、绝不收镜」(防穿墙不再靠拉近)· v24r107 | 用户口径:「不行,摄像头一撞到障碍物就拉近,然后恢复视距,这样老拉来来去,看的都头晕,要不撞到东西不拉摄像机,要不恢复上一个版本穿墙算了」。 ① 病根在 v24r106 的机制本身。那一版的防穿墙是把相机收回来实现的(压低弹簧目标 + 位置硬钳制)。静态几何上它是对的(测试里穿墙真的 0 帧),但真机上障碍时有时无(走过一根柱子、擦过一道墙角)⇒ 镜头就收 → 障碍消失 → 放 → 又撞上 → 再收,一路拉扯 ⇒ 晕。晕比穿模更难受,所以这一版把优先级调头。 ② 硬闸默认关闭。 CAM_WALL 改成默认 false(?camwall=1 可开回旧行为)⇒ 撞到东西一寸都不往回收,视距永远是你双指调出来的那个值。③ 连「位置硬钳制」也要一起关。只关目标不够 —— 摇臂的 arm({ hard: true }) 是在位置上把相机拽回命中面,等于换了个地方收镜。现在它与 CAM_WALL 绑死,默认关。④ 防穿墙改由「挪位让路」独立承担(只改角度、不改视距,所以不会晕),同时把挪位幅度放大:横向 66°→85°、抬高 24°→30°,让「绕得开」的覆盖面更大。 ⑤ 代价讲清楚:绕不开的窄巷 / 死角仍会穿过去(相当于是 r105 的行为)—— 这是「不拉近」与「不穿墙」二选一时,用户明确选择的取舍。 开关: ?camwall=1 开回旧硬闸 · ?camslidemax= / ?camslidelift= 调挪位幅度。验收: tools/r107-nosqueeze-live.cjs 19/19 —— 240 帧里撞到实心体 31 帧、camCol 全程恒 = 1.000(一寸未收)、实际臂长/用户视距恒 = 1.000、静止 115 帧波动 0.000;同一脚本还带对照实验:?camwall=1 同样撞墙时 camCol 最低 0.631 ⇒ 证明默认版的「不收镜」是开关关出来的、不是没撞到。回归全绿:r105-pinch-dist-live 20/20(判据已从 r106 的放宽口径恢复成全帧硬口径:撞到东西也一寸不让)· r103-cam-slide-live 16/16 · camcol-test 48/0 · r106-wall-test 19/19 · r103-cam-slide-test 22/22。 |
| ★★ 防穿墙:相机绝不再戳进墙里(最后一道硬闸)· v24r106 | 用户口径:「相机在这个基础上,加防碰撞,不能穿墙过去」。 先说病因。v24r105 为了守住「撞到东西不能拉近」,把碰撞给出的安全臂长一律抬回 1(弹簧目标永远是满视距),靠挪位绕开障碍;同时 hard(命中面绝对上限)的钳制默认是关的。于是:挪位绕得开 ⇒ 没问题;绕不开(窄巷 / 贴着城墙走 / 被包住)⇒ 相机就直挺挺戳进墙里了 —— 这就是你看到的「穿墙过去」。① 补最后一道闸。新增 MUCamCol.wall():无论弹簧目标多少、挪位绕到哪儿,相机中心永远不许越过实体表面(再留 0.25m 贴面余量,免得正好贴面看到墙内壁)。② 关键是判据要分成两条(这一步不能省)。 hit() 带侧须(把相机当有体积的球)⇒ 管「挑站位 / 该不该让路」,是手感;wall() 只打中心射线 ⇒ 管「绝不穿模」,是安全。共用一个判据就会立刻掉回 r104 那个坑 —— 为了不擦到 6 米外的墙角,被拉近到贴脸。③ 挪位评分改成「不穿墙优先」。以前只比开阔度 r,会出现「r 到 1 了但其实正戳在墙里」的误判,于是明明在穿模却判定「绕开了」、全力伸出去。现在:中心通 ⇒ 加 2 分,比「少穿几厘米」优先。 ④ 穿墙时突破「站着不动」的锁定。否则:人站着不动 → 锁站位 → 但那个站位正在墙里 → 永远不修正,画面一直卡在墙内部。 优先级:防穿墙 > 不拉近。绕得开时它根本不触发(完整保留 r105「视距纹丝不动」的手感);只有真要穿墙才拽住 —— 宁可被拉回一点,也绝不穿墙。 旋钮: ?camwallpad= 贴面余量(米,默认 0.25)· ?camwall0=1 关掉(仅供对照)。调试: S.camWall(1 = 一路通,<1 = 再伸就穿模了)· S.camIn(1 = 相机正在墙里,正常玩恒 0)。⑤ 首轮上真机又挖出 4 个真 bug(都只在「真撞到墙」时才暴露 —— 前 6 次跑一路开阔,等于没测到)。 (a) 下限反超命中面。 Math.max(0.05, 命中面-余量) 在玩家紧贴墙(命中面 < 0.05,22m 臂长上不到 1.1m)时,0.05 反而越过了进入面,把相机推进墙里;camsmooth.js 里「先夹命中面、后夹绝对下限 0.05」的顺序同理。⇒ 下限一律改成 min 收:宁可靠得更近,绝不越面。(b) 落位复查自相矛盾。相机已戳进盒子时, ray() 会因为「射线起点在盒内」走容器分支把它跳过 ⇒ 返回 1 ⇒ 复查判定「畅通」、不修正。⇒ 复查改成用 inside()(点包含)二分求根。(c) 挪位触发条件恒真。 hit() 的返回值自带余量(pad ≥ 0.45m),「完全通畅」时也只有 ≈0.98,永远小于阈值 0.995 ⇒ 搜索每 0.16 秒无条件跑一次。r105 那套旧评分搜不出「更好的位置」所以没暴露;r106 换成「不穿墙优先」后真能搜到 ⇒ 人站着不动镜头也自己扫。⇒ 改成「真被挡(<0.9)才搜」。(d) 停滞闸逻辑反转。 !(hasSpot && idle) 里的 hasSpot 是「当前偏移非零」推断的 ⇒ 偏移归零那一帧 hasSpot 变 false,又重新允许搜索 ⇒ 搜到新目标 ⇒ 归零 ⇒ 再搜……自激闭环(实测静止时挪位目标在 1.12 rad 上来回跳、14 秒收不住,画面就是「镜头自己左右扫」)。⇒ 直接判「人没在动就不搜」,与偏移量无关。验收: tools/r106-wall-test.cjs(真实几何)19/19,含「绕角色一圈 72 个方位 · 18 个被实体挡 · 钳制后 0 个仍在墙内」;tools/r106-cam-wall-live.cjs(真页面 · 安卓 UA · 412×915 · 真触摸)16/16 —— 边走边转镜头主动扫墙,223 帧里 70 帧被实心挡住、camWall 最低 0.000,穿墙 0 帧,静止 126 帧挪位波动 0.000 rad / 臂长波动 0.000 m;回归 camcol-test 48/0 · r103-cam-slide-test 22/22 · r103-cam-slide-live 16/16 · r105-pinch-dist-live 20/20(该文件判据按「防穿墙 > 不拉近」更新:只在「通畅并站稳」后要求回到满视距,贴墙时允许被压 —— 但必须能自己长回来)。 |
| ★ 手机双指调视距:张开=拉远、松手即锁,撞到东西绝不再拉近 · v24r105 | 用户口径:「双指扩大就是迅速增加视距,缩小就是减少视距;一旦调好视距不能随便变化;相机碰到障碍物不能拉近」。 ① 方向反过来。张开 = 视距变大(看得更远),捏合 = 变小。旧方向(张开 = 拉近,3D 游戏惯例)留 ?pinchin=1。② 迅速。三处一起放:低通 0.45→0.78、目标限速 14→55 m/s、手势期间臂长弹簧 ω 18→45 ⇒ 手指分到哪儿画面几乎同步到哪儿;松手后回到 ω=18,最后那点误差自己收干净。 ③ ★ 真凶不是碰撞,是「侧须」。相机那两条体积预判须是固定 20° 张角,侧向偏移 = 距离×sin20° ⇒ 视距 19m 时等于给相机套了个 6.2 米宽的虚拟体积:相机明明站在空地上,6 米外有个墙角也算「撞上了」⇒ 一路被压到 camCol≈0.26(19m→4.7m),走动时更狠(0.089 = 1.7m,等于贴脸)。真页面诊断( MUCamCol.diag())证明:中心射线是通的,全是侧须在挡。现在改成按相机真实半径(默认 0.8m)换算张角 —— 19m 时 2.4°、4m 时 11°:远了不虚胖,近了仍保留「提前预判」。调半径 ?camcolrcam=,回旧行为 ?camcolfixspread0=1。④ 视距锁。双指松手即上锁;碰撞给出的安全臂长一律抬回 1( ?camsq=0.35 才允许压到 35%)⇒ 挡住了只靠挪位绕:左右最多 66°(原 49°)、绕不开再抬高最多 24°(原 11°)、重搜 0.30s→0.16s;搜索改两级(粗找方位 0.16 rad 一档 + 附近细调 0.05 rad × 三档抬高),抬高档改成绝对值不再累加(否则被挡久了相机一路爬上天)。⑤ 挪位的「站住」三道闸(第一版教训)。只做 ①②③④ 时真页面反而更糟:站着不动,挪位目标 2 秒内波动 1.66 rad(镜头自己在左右扫)。根因是反馈环 —— 相机一挪,性能层的收放判定就跟着变,同一角度的开阔度 r 于是跟着变,人不动也在抖。三闸:(a) 站得住就不动(开阔度 ≥ 0.75 压根不搜索, ?camslideok=);(b) 换完站位的 0.8s 冷却内不再改主意,除非被挡死(?camslidecool=);(c) 人站着没动就不再重新挑站位(?camslideidle=)。效果:静止时挪位角波动 0.0000 rad。验收: tools/r105-pinch-dist-live.cjs 19/19(真页面 · 安卓 UA · 412×915 竖屏 · 真触摸事件:张开=拉远、手势一停就到位、松手 0.4s 不滑行、锁上后走 6 秒视距纹丝不动、撞到东西(24/46 帧确实被挡)实际臂长仍 = 视距);tools/r103-cam-slide-live.cjs 16/16(静止 2 秒波动 0.0000 rad、行走臂长恒 1.000);几何层 tools/camcol-test.cjs 48/0 与 tools/r103-cam-slide-test.cjs 22/22 已按新口径迁移(新增「24m 处侧向 3m 的墙不算撞上」这条反证)。 |
| ★ 撞到东西不再自动拉近 —— 视距只归两指管,靠挪位绕开障碍 · v24r103 | 用户口径:「两指放大缩小决定视距,视距决定了不能随便拉近拉远,撞到东西就在那不动,通过挪动避开障碍物」。 旧行为(r20~r102):相机撞到实体就缩短臂长让路 —— 那本身就是一次自动拉近,正是「随便拉近拉远」的来源。 新行为:视距( cam.dist)只由两指/滚轮决定,碰撞一律不改它;挡住了就在球面上挪动相机(左右绕,绕不开再抬高 camslidelift),挪到最开阔的位姿站住。三道防抖(缺一个都会抖,真页面实测:抖起来 19m 臂长上就是 4~8m 的来回推拉):① 每 0.3s 才重搜一次;② 搜索基准用用户视距而不是被压缩后的实际臂长(否则压缩与挪位互相追着跑);③ 够用就不动 —— 当前位姿已经够开阔就维持原样,不再为「再开阔一点点」微调。 实在绕不开(窄巷、四面被围)才回退到压缩兜底。 手感旋钮: ?camslidemax= 最多挪多少(默认 0.85 rad)· ?camslidek= 跟随快慢 · ?camslidetrig= 挡到多少才值得挪 · ?camslidehyst= 迟滞 · ?camslideback= 归位快慢 · ?camslide0=1 关掉回到旧的「拉近」。 |
| ★ 输入框自己也转 90°:文字方向和手机自带键盘一致 · v24r101 | 用户口径:「把输入对话框(手机自带键盘……的)把它顺时针旋转 90 度」。 ① 为什么必须转:安卓输入法是「系统画的」。它按物理屏横排(字朝右、从下往上读),不受网页 transform 影响;而输入条原来的长边跑在「逻辑 x」上,偏偏逻辑 x = 物理 y( html.rot body 的 rotate(90deg) 把两个轴换了)⇒ 它在物理屏上是竖的、字朝下。于是同一块屏幕上,键盘的字和输入框的字差 90°。玩家说「手机自带键盘是竖着的」正是这个视角:把手机转过来看游戏时,系统键盘看着是竖的,输入框的字却是正的 —— 两边对不上。② 解法:在 body 的 rotate(90deg) 之下,再给输入条自己 rotate(-90deg)。两下相消 ⇒ 输入条的局部 +x 落到物理 +x(左→右)、字形朝上,与系统键盘完全同向。真浏览器量到:输入条累计旋转 0°、系统键盘 0°、body +90°;两边各放一对探针量「文字方向」,点积 = +1(同向)。反过来( +90deg)点积 = −1 —— 在物理屏上是倒的,所以默认值定 --ask-rot: -90deg(留成变量,但默认那个才对)。③ 转过之后几何要重算(长边换轴了)。长边从「逻辑 x」搬到「逻辑 y」⇒ 长度不能再拿 --rot-w 算,改由 --rot-h(= 物理宽)封顶:min(calc(var(--rot-h) - 24px), 620px),物理屏上两端各留 12px。transform-origin 放右下角,再配 translate(…, -100%)(−100% = 自身高)把长边拉回 top 起点,top: calc(50% - 长/2) 让长边沿逻辑 y 居中。④ 真浏览器量出来的一个 bug(已修)。原点一开始放的是右上角:方向那半边没问题,但厚度是朝键盘那一侧长的 —— 量到条子占物理 y 518→569,而键盘上沿从 340 起 ⇒ 盖住 41px,键盘一弹就压掉半条。改右下角后是 279→330、键盘上沿 340 ⇒ gap = 10px,整条稳稳留在键盘外侧。 ⑤ 顺带清掉一条旧账。上一轮为「别被切掉」打的宽度补丁( --rot-w − --ask-inset − 8px)现在不需要了:长边不再吃键盘高度(键盘那条轴是逻辑 x,长度跑在逻辑 y 上)⇒ 宽度与键盘高度解耦。验收: tools/r101-ask-rotate-test.cjs 45/45(CSS 契约 / 方向数学 / 几何不变量,含「右上角原点会把条子塞进键盘底下」的回归锁 / 版本戳与缓存清单);tools/r101-ask-rotate-live.cjs 26/26(真无头 Edge:把模拟键盘挂在 <html> 下复现「系统画的键盘」、两对探针量文字方向点积 = +1、累计矩阵 = 0°、gapToKb = 10px,并拍 mu3d-r101-ask-rot-OK.png / mu3d-r101-ask-rot-CW-wrong.png 两张对比图)。 |
| ★ NPC 真的开口出声了:离线语音包(149 条预生成 mp3)· v24r100 | 用户口径:「要NPC 说出声音」。 ① 为什么之前没声(这次挖到底了)。发声链路本来是「服务器 TTS(火山引擎,要 voice-config.json 里的 key)→ 系统 speechSynthesis」。而那两把 key 默认就是空的 ⇒ 每次说话第一步就抛 not_configured ⇒ 全部落到系统 TTS;偏偏安卓国内 ROM 基本没有中文语音引擎(微信 X5 内核更彻底,连 API 都不给)⇒ 现场就是「字幕有、一整个没声」。这条路再修代码也救不回来 —— 得有既不依赖系统 TTS、也不依赖网络的音源。② 方案:把内置台词全部预生成成 mp3,随包发。台词全集很小( LINES 13 个角色 × 3 + REPLY 13 × 2 + INTENT 7 组 × 3),去重后 149 条、共 ≈3.7MB。生成器 mu3d/tools/make-voice-clips.py 直接从 npcvoice.js 里解析台词(两边永不失同步),用 edge-tts(微软 Edge 朗读那条免费接口,无需 key;只在生成时联网,播放端完全离线)合成到 mu3d/voice/*.mp3。4 种音色按角色分:云希(铁匠 / 商人 / 武者 / 异客)、云健(守卫 / 村长 / 剑士 / 吟游诗人)、晓晓(祭司 / 剑姬 / 法师)、晓伊(仙灵 / 默认居民)。同一句台词由不同角色说出来 = 两个文件,声线不会串。③ 播放顺序:本地语音包 → 系统语音 → 如实报错。新的唯一出口 npcSpeak():先查清单里有没有这句的 mp3(有就播,完全不惊动系统 TTS);没收录(云端现编的台词)才交给 speechSynthesis;两条都不行就由 voiceHint() 把原因写到屏幕上,不静默。清单做成 js(js/voiceclips.js)而不是 json:SW 那套「缓存优先 + 版本戳精准失效」直接复用,不必为一个新目录再造缓存规则。④ 手机上还差的那一环:自动播放许可。安卓 Chrome 对「没有任何手势就播放」会直接拒绝 —— 而 NPC 打招呼恰恰是自动触发的。两处修:(a) 整页共用同一个 <audio> 元素(不是每条 new Audio())—— 许可绑在元素上,解锁过的那一个才留得住;顺带天然串行,不会两句叠着念。(b) 第一次 pointerdown 时拿它放一段 0.03 秒的内联静音 wav(8kHz / 8bit,380 字节 base64,不额外发请求),把这把「钥匙」拿到手。放不出来的极端情况(文件坏了 / 仍被拦)会回落系统语音,不是就此哑掉。⑤ 缓存与发版。 js/voiceclips.js 进 boot.js 的 CORE(排在 npcvoice.js 之前)与 sw.js 的 CHANGED / ASSETS;149 个 mp3 故意不进 ASSETS(否则首次安装变成 4MB 大下载)—— 它们走 SW 默认的「资源:缓存优先」分支,第一次播到哪句就缓存哪句,之后离线可播。文件名含台词原文的 sha1,改台词或换音色文件名就变,旧文件自然作废,不需要额外的失效机制。⚠ 这套东西的意义:空 key 的默认形态、断网的云电脑、手机走 http 局域网 —— 现在都能听到 NPC 说话。想换成云端音色,仍是在 voice-config.json 填豆包密钥(有服务端音频时优先播它,服务器那条路排在最前)。验收: tools/r100-voice-clips-test.cjs(清单结构 / 清单 ↔ 磁盘一一对应 / 内置台词 100% 覆盖 / 查找与回落 / 共享 audio 与手势解锁 / 接线与版本戳)。 |
| ★ 手机端语音 + 把输入条贴到键盘那一边 · v24r99 | 用户口径:「手机测试 没语音」「把键盘横着放在右边」「加语音输入功能」—— 澄清后是:字幕出来了、一声音都没有;「键盘」指手机自带的输入法键盘;机型 = 安卓(Chrome / 微信)。 ① 「字幕有、没声」的四条真因(都在代码里)。(a) cancel 和 speak 挤在同一个 tick —— 安卓 Chrome 会把新的一句吞掉(旧版每次说话都先 cancel 再立刻 speak)⇒ 现在没在说就不 cancel,真在说才 cancel 且 speak 延后 80ms(唯一例外:还没预热时必须在手势的同步路径里发),并用 gen 代次防「旧的一句把新的一句顶掉」。(b) 挑到了联网声 —— 安卓上「Google 普通话」是 localService=false 的联网声,国内连不上就静默无声 ⇒ scanVoices() 改成本机声优先并按序分槽(f1→m1→f2),保证 pick 拿到的就是离线声。(c) 只认 voiceURI —— 部分安卓只认它、不认传进去的 voice 对象 ⇒ 两个都写。(d) paused 卡死 —— 安卓上 pause() 被实现成 cancel ⇒ speak 前若 paused 改成 resume()。另加出声验证:speak() 不抛异常也可能什么都没发生 ⇒ 900ms 后查一次(speaking/pending),没起声就清掉 voice 重试一次再复验。更关键的是让失败看得见:旧版只在控制台报,玩家只觉得「坏了」⇒ 字幕条多一行 .npcSubHint(暖色)写清「为什么没声 + 两条可照做的路」(手机设置里装中文 TTS 语音包 / voice-config.json 填豆包密钥),且一个会话只提示一次(不刷屏)。② 「把键盘横着放在右边」= 输入条贴到物理下边缘。先算清哪条 CSS 边 = 哪条物理边( html.rot body 上是 rotate(90deg) translateY(-100%) + transform-origin:0 0,真浏览器实测矩阵 matrix(0,1,-1,0,480,0) ⇒ 物理 x = rot-h − 逻辑 y、物理 y = 逻辑 x)⇒ CSS bottom 落在物理左边缘,而键盘那条边(物理下边缘)是 CSS right。旧版只会写 bottom ⇒ 输入条被推到键盘的对面,够不着也看不见。fitAsk() 现在按 html.rot 切到 right 锚定 + 垂直居中,键盘弹起时挂 .kb(顺手把「对「X」说:」收掉省宽度)。③ 宽度必须让开 right 占掉的那一段(真浏览器里量出来的真 bug)。旧公式 min(0.92*rot-w, 620px) 没考虑 right:短屏手机上 rot-w=640 ⇒ 宽 588.8,而右侧只剩 640−122=518 ⇒ 溢出 70.8px,在旋转视图里就是「输入条左端被切掉」。现在宽度 = min(var(--rot-w) − var(--ask-inset) − 8px, 620px),--ask-inset 由 fitAsk() 按键盘高度写到根元素上(键盘弹起时宽度自己缩)。④ 语音输入:能用就用,不能用就把路指出来。网页拿麦克风( SpeechRecognition)只在安全上下文可用:https / localhost 行,http://192.168.x.x 不行(安卓 Chrome 直接 SecurityError 或压根没有构造函数,微信 X5 内核连 API 都不给)⇒ 能用就现场把说的话转成文字填进输入框(not-allowed / 没听清 / 启动被拒分别说清);不能用就说清原因 + 顺手把输入框聚焦(把输入法叫出来) —— 输入法键盘上那个 🎤 是手机上唯一稳定的语音输入。输入框右侧新增 🎤 按钮(正在听时变红点并呼吸)。验收: tools/r99-mobile-voice-test.cjs 85 / 85(vm:旋转布局贴键盘那一边 / 安卓没声的四条真因 / 语音输入三种结局 / 版本戳与缓存清单);tools/r99-mobile-voice-live.cjs 23 / 23(真无头 Edge:🎤 真建进 DOM 且点了必有反应、焦点真落到输入框、旋转布局的真计算样式与几何 —— rectBottom = 518 = rot-w−122、rectTop = 8 ≥ 0(没被切)、「居中」那侧会是 575,区分度 57px)。⚠ 两条踩过的坑记这儿免得下次再踩:(1) 对定位元素,Chrome 的 getComputedStyle().left 返回的是 used value(像素),left:auto 会算成一个具体数字 ⇒「left 是不是 auto」这种断言永远测不出东西,要改成量 right / 量几何;(2) #npcAsk 的 transform 有 .18s 过渡,加完 class 立刻量拿到的是动画中间值(这里曾量出 matrix(1,0,0,1,-156.305,-27.675))⇒ 量几何前先 *{transition:none}。 |
| ★ 手机端测试二维码 + 局域网双栈(IPv4 打不开就走 IPv6)· v24r98 | 用户口径:「要手机端的测试二维码」。 ① 手机为什么连不上(上一轮已定位到内核层)。Windows 防火墙是关的,真正在丢包的是 360 网络防护 —— 它的过滤器挂在 WFP 的 FWPM_LAYER_INBOUND_TRANSPORT_V4(只拦 IPv4 入站),BOOTTIME + PERSISTENT,且和 Windows 防火墙两套独立。关键点:它只挂 IPv4 那一层 ⇒ IPv6 那条路没人管,而手机和电脑在同一个家用路由器下时,手机往往也拿到了同一个 /64 的全局 IPv6(2409 开头,电信/移动的家宽默认都给)。② 所以 serve.py 现在双栈。--lan 原本只绑 0.0.0.0,现在同一个端口再加一个 [::] 的 IPv6 监听(新增 Server6,显式 IPV6_V6ONLY=1,不跟 IPv4 抢端口;绑不上不致命,退回纯 IPv4 继续跑)。启动横幅会同时印出 http://192.168.1.x:8156/ 和 http://[2409:...]:8156/ 两组地址。不想要 IPv6 就加 --no-lan6。③ 地址怎么挑。新增 lan_ips6():只吐全局地址,::1 / fe80::(带 %zone,手机用不了)/ fc00::/7(ULA)一律踢掉;Windows 那张网卡同时挂的「临时 IPv6 地址」(RFC4941,会变)排到最后当备胎,稳定的那个排第一给二维码。④ 二维码怎么来的。纯 Python 生成( segno,离线、无外网依赖),四种组合各一张:主页 / 旧都铎小镇(?oldtown=1,NPC 语音只能在这儿测)× IPv4 / IPv6;另附一个自包含 HTML 测试页(SVG 内联,扫不出来时页面上直接抄地址)。验收: tools/r98-lan6-test.py 31 / 31 —— 真绑真连:0.0.0.0 与 [::] 同端口共存、127.0.0.1 与 ::1 都能拿到 200 且字节数一致、--no-lan6 时 ::1 确实连不上、横幅真的印出了方括号地址。 |
| ★ 本地语音:不填密钥也有声,而且每个角色是不同嗓子 · v24r97 | 用户口径:「先用本地的吧」—— 不接云端密钥,用机器自带的语音引擎说话。 ① 原来的本地兜底几乎是哑的。 speakFallback() 只做了 u.lang = 'zh-CN',没有挑中文声 —— 浏览器完全可以拿系统里的英文声去念中文(Windows 上就是 Microsoft Zira 用英语腔念汉字,听着像乱码),甚至干脆不响。而且 speechSynthesis.getVoices() 是异步的:首次调用常常返回空表,不等 voiceschanged 就选声,只能选到 null。② 现在是一台真正的本地引擎( js/npcvoice.js 内 LTS):按名字片段挑中文声(kangkang/康康、huihui/慧慧、yaoyao/瑶瑶…,不写全名 —— 语音名随系统语言和版本变),分 m1 / f1 / f2 三档声音槽;12 个角色各配一套语速与音调(长老 0.82 慢而苍老、仙灵 1.18 快而高、守卫 0.95/0.86 低沉)—— 一镇子人不再共用一个嗓子。真浏览器实测:系统给 26 个声、其中 7 个中文,槽位落到 m1=Microsoft Kangkang / f1=Microsoft Huihui / f2=Microsoft Yaoyao。③ 手势预热。iOS 上第一条 speak() 必须从用户手势里发出去,否则之后所有台词都被静音(现象就是「NPC 不说话」)。现在首次 pointerdown 时(与解 AudioContext 同一处)发一条 volume=0 的空 utterance 占位,只发一次。④ speak 前先 cancel。旧版不说就往上堆,上一条没念完新的一条只能排队,越拖越远。 ⑤ 两种没声音的环境都不炸。系统里一个中文声都没有(部分安卓 Chrome)→ localVoice().why = 'no-zh',退到表里仅有的那个声继续出声(比完全静默强);浏览器根本没有 speechSynthesis → why = 'no-api',优雅返回 false,游戏照跑。⑥ 追问不再「问东答西」。旧版追问走的是和主动招呼同一个 pickLine() —— 你问「城门怎么走」,铁匠回你一句「要打把好刀?」。现在追问走 pickReply():7 条意图(问候 / 告别 / 道谢 / 问路 / 打怪练级 / 武器装备 / 买卖价钱)先匹配,没命中再退到 12 个角色各自的应答库 —— 守卫会跟你讲城门规矩,仙灵会说自己只管花和前庭。⚠ 说明:本地模式没有大模型(本机也没装 Ollama 之类的本地模型),应答是预置台词 + 关键词意图,不是真语义理解;想要真正的自由对话+豆包音色,仍要在 voice-config.json 里填那两把密钥。验收: tools/r97-local-tts-test.cjs 63 / 0(打桩:首扫为空、无中文声、无 API、分角色、cancel、预热、意图应答、端到端出声);tools/r97-local-tts-live.cjs真无头 Edge(真 getVoices() + 真挑声 + 真交给引擎)。 |
| 黑骑士披上「喷泉里的水」· v24r53 | 之前那层流体是 vfx 自己画的程序化亮条贴图(additive,扁平、不吃光)。现在直接用前庭大喷泉那份真水体材质(stormwind 的 waterMat):水纹漫反射 + Sobel 法线贴图 + 低粗糙度(0.05)+ 金属度(0.34)+ 自发光 —— 与喷泉的水帘/水面是同一张贴图,会吃场景光照,有高光与波光,就是「喷泉里的流体」裹在铠甲上。UV 流动与喷泉主水柱同向( dir:"up" = 从脚底往头顶爬,像喷泉从下往上喷;改成 "down" 就是溢流水帘那种往下淌),并且漫反射与法线同步滚动(只滚颜色不滚法线 = 水纹明暗和高光错层,看着像贴图在滑、水没动)。外壳参数在 model-map.json 的 coat:加了 pool 字段(fountain = 喷泉真水 / proc = 退回程序化水纹)。喷泉材质不在位时自动降级,不会像 r51 那样静默失效。 |
| 流体光泽加厚:水 + 流光 + 光晕三层壳 · v24r53 | 上一版只有「水」,看着太淡。查下来有两个原因:① 水纹密度写死 repeat(2.2, 4.0),套在 8.35 米高的雕像上 = 一个水纹周期被拉到 2 米,糊成一层淡雾;② 半透明的水只给「质感」,没有「光泽」。现在:水纹密度按模型尺寸自适应(≈每 1.1 米一个循环,雕像实测 repeat 2.74×7.59),金属度 0.52 / 自发光 1.15 让水光挂得住;再叠一层流光壳(additive 亮条,滚得比水快 1.9 倍)—— 一道道光泽顺着铠甲往下淌,最外层仍是 BackSide 光晕(只描轮廓边光)。 model-map.json 的 coat 新增 glow(流光强度,0 = 只要水)。雕像当前:厚度 1.04、浓度 0.66、流速 0.42、流光 0.6。 |
| 贴图资产 · 美工接口(v24r26) | 所有贴图都能换成美工画的图,不用改一行代码。把图片按文件名丢进 assets/textures/:<槽位key>.png(漫反射,必须)· _n.png(法线,可选)· _r.png(粗糙度,可选),例:sw_granite.png 换要塞白石的墙、mu_ground_grass.png 换整片草地。生效方式三选一:① 按 U 打开「贴图资产」面板 → 点【扫描目录】(最省事);② 跑 python tools/tex-scan.py 生成清单,之后刷新页面自动加载;③ 网址加 ?tex=sw_granite=我的图.png 临时试单张。面板里还能直接把 png 拖到槽位上,立刻看到效果,不用先落地文件;也能【导出待办】拿一张清单给美工。总共 23 个场景槽位(暴风城 6 · 天空与地面 6 · 旧都铎小镇 11),另外还有一组自动生成的「外部模型贴图」—— 每个装进游戏的外部模型,它的每个材质都会在这里出现一个槽位(v24r29),可以直接给模型换贴图,点【恢复】回到模型自带的那张图,每个槽位都标注了这张贴图覆盖多少世界米与建议分辨率 —— 照这个密度画,近看就不会糊。两条硬约束:① 地面/广场这种大面积铺地图别画高频细节(45° 俯视必摩尔纹);② 天空是 2:1 的等距圆柱投影,整幅贴一次、不要平铺。换图是秒生效的 —— 同类型贴图互换只更新 uniform,不会重编译 shader(不会卡);法线/粗糙度没交就会继续用程序化那版,只交漫反射也能用。开关: ?texext0=1 完全关掉外部贴图 / ?texui=1 进游戏就打开面板 / ?texdir=目录/ 换图目录 |
| 地板波纹 / 城门那块(v24r20) | 查出来是两个真原因:① 地板贴图图案频率太高 —— 512 贴图里 4×4 块石头、每 2m 就重复一次 ⇒ 石缝只有 50cm,还叠了一层逐像素噪声,45° 俯视 + 相机移动必然摩尔纹。现在换成大地砖(2×2 块、一张贴图覆盖 4m ⇒ 石缝稀 4 倍)+ 低频颗粒 + 法线强度降到 0.22,广场和桥面(城门那块)一起换。② 广场地砖 / 桥面自己也在投影,斜射阳光下会「自阴影」出一层随视距流动的细纹(阴影粉刺)—— 现在这类平铺件全部不投影,阴影图的法线偏移也从 5cm 提到 16cm |
| 跳跃(v24r15) | 点一下屏幕 / 空格 / 右侧 ⤒ 跳 按钮 / 摇杆双击 都能跳,跑动中也一样能跳(腾空 0.74 秒、最高 1.37 米,空中不能二段跳,落地自动弹)。站着调镜头人不会跟着转(朝向锁死),只有跑动时镜头才咬到背后。想恢复「点屏幕=点地走过去」加 ?tapmove=1;关跳 ?jump0=1;调手感 ?jumpv=8.5 跳更高 / ?jumpg=26 更利落 |
| 角色模型 | 主角是多网格骨骼角色(67 个独立部件 + 27 根骨骼),带待机 / 走 / 跑 / 劈砍 / 施法 / 受击 / 倒地全套动作。想试 3ds Max 导出的 OBJ 自动装配版加 ?heromodel=obj;想回旧模型加 ?oldhero=1;粒子粗细可用 ?psize=0.5~1.5 调节 |
| 广场喷泉 / 流水 | 广场西侧的分层大喷泉:顶部主水柱 + 4 只狮头斜向外抛的水弧 + 溢流水帘 + 水面涟漪扩散(水的运动是真·抛物线粒子,受重力下落)。想关掉流体对照性能:?fluid=0;连火焰粒子一起关:?nopart。看粒子数:?stats=1 |
| 亮度 / 画质 | 右侧 ☀ 亮度 按钮或 B 键三档循环(柔和 100% / 标准 125% / 高亮 155%,会记住)。网址参数:?bright=1.4 更亮 · ?ssaa=1 关超采样省性能 · ?freecam=1 解锁自由运镜 |
| 普通攻击 | 自动战斗开启时自动攻击最近敌人(锁定:左键点怪) |
| 技能 | 1 2 3 职业技(剑士 3 = 天地十字剑:起跳横斩 + 落地竖劈)· 冰风谷额外解锁 4 冰霜新星 / 5 极光风暴 / 6 绝对零度。技能栏在所有地图(含勇者大陆安全区)都常驻显示 |
| 自动战斗 | T 键 或 顶部按钮(挂机刷怪) |
| 拾取 | 走近自动拾取,F 吸取周边掉落 |
| 切换地图 | M 键 或 顶部地图按钮 |
| 新功能演示 | 按 V 键(或点顶部「✨ 新功能」)打开 —— 把本次新增的仓库储物箱(v24r89)、相机侧倾(v24r85 起默认关)、相机避障「慢慢恢复」(v24r87,带现场演示)、3D LUT 调色、屏幕空间描边、模型解码缓存、慢启动豁免、法师程序龙卷风(v24r86,附展示页)逐条列出来,每条都配一段「举例」说明,并且能直接点按钮现场看效果 |
| 属性加点 | 升级获得 5 点,点属性后面的 + |
| 仓库 · 储物箱(v24r89) | 背包只有 20 格、满了就当场丢弃;现在多了一个 60 格仓库:点右侧 🏦 仓库 或按 K 打开 —— 点背包里的东西 = 存进仓库,点仓库里的 = 取回背包,另有【全部存入 / 全部取回 / 整理(按类型)/ 按战力排】。仓库与背包同为 10 列,看着就是「背包的楼上」。刷新、关浏览器、换角色都不会丢(存 localStorage)。仓库里的装备是冷存储 —— 不参与自动装备、也不参与「一键最强」,免得没穿的东西偷偷生效。 容量: ?stash=80(8~120,默认 60)· 背包满时自动入库:?stashauto=1(默认关,关着=沿用旧的「丢弃」行为)· 整体关掉:?stash0=1 |
| 和 NPC 说话(v24r93) | 走到小镇居民身边(4 米内)他会自己开口打招呼 —— 台词由 AI 按身份和你的 等级 / 血量 / 所在地图现生成,声音是豆包语音合成。屏幕下方会出现 「按 Enter 与「铁匠 汉斯」说话」→ 按 Enter 打字追问,Enter 发送、Esc 关闭。⚠ 居民 NPC 只在旧都铎小镇(网址加 ?oldtown=1)出现;没配密钥或断网时会用内置台词兜底(字幕框变冷色),照样有人说话。密钥填在包目录的 voice-config.json。 |
| 不填密钥也有声(本地语音 · v24r97) | voice-config.json 里两把密钥都空着也照样能听见 NPC 说话 —— 用你电脑 / 手机上系统自带的语音引擎念(Windows 上就是慧慧 / 康康 / 瑶瑶),不联网、不要密钥、断网也不影响。而且每个角色的嗓子不一样:守卫 / 铁匠 / 剑士用康康(男声、低沉),村长最慢最苍老,仙灵瑶瑶又快又高,商人和吟游诗人用慧慧 —— 走近不同的人,听到的不是同一个复读机。 想用嘴问:按 Enter(手机点提示条)打字追问,也能听到回话;问「怎么走」、「多少钱」、「哪里打怪」他会照着答。 ⚠ 本地模式是预置台词 + 关键词应答,不是真 AI 对话;要自由对话+豆包音色,还是得填 voice-config.json 里的两把密钥。 |
| 手机上怎么连(二维码 / IPv6 · v24r98) | 电脑上双击 run-lan.cmd(或 python serve.py --lan),窗口里会印出手机访问地址;想扫码就打开包目录下的 qr-phone.html,四种组合都印好了(主页 / 旧都铎小镇 × IPv4 / IPv6)。⚠ IPv4 打不开就先扫 IPv6 那张:360 之类的「网络防护」只拦 IPv4 入站(Windows 内核 WFP 的 FWPM_LAYER_INBOUND_TRANSPORT_V4),IPv6 那条路它不管 —— 所以 serve.py --lan 现在是双栈(0.0.0.0 + [::] 同端口)。不想要 IPv6:--no-lan6。手机第一次进游戏先点一下屏幕(解音频锁 + 给系统语音做手势预热),否则 NPC 不发声。 想测 NPC 语音:网址后面加 ?oldtown=1 进旧都铎小镇,居民 NPC 只在那儿。 |
| 手机上和 NPC 说话(v24r95) | 手机上走近居民,屏幕下方会出现「💬 点这里 与「××」说话」—— 点它就打开对话(不用找 Enter 键,手机本来也没有)。输入框右侧的 ✕ 是关闭;发送按软键盘右下角的发送键,或点「发送」按钮。 想用嘴说:点输入框调出键盘,再点键盘上自带的 🎤(讯飞 / 搜狗 / Gboard 都有)—— 这是手机上唯一可靠的「语音打字」,不需要任何额外设置。 ⚠ 风暴城要塞里的剑士 / 剑姬 / 法师 / 守卫石像 / 仙灵 / 武者 / 异客 现在也是 NPC,走近同样会开口(它们原来是装饰道具)。 |
| 手机上「没声音」怎么办 + 输入条会贴到键盘那边(v24r99) | ① 手机上没有声音(字幕有、喇叭不响):不是你操作错 —— 常见就三种:没点过屏幕(第一次进游戏先点一下,浏览器才允许出声)、系统里没有中文语音包、或者挑到的是「Google 普通话」那种要联网的嗓子(国内连不上就哑了)。现在这几种情况屏幕上会直接写出来(字幕条下面那行暖色小字),照它说的做即可:手机「设置 → 辅助功能 / 无障碍 → 文字转语音(TTS)」装一个中文语音包(优先选「本地 / 离线」的)。 ② 输入框会贴到键盘那一边:手机竖着拿、网页被强制转 90° 时,系统输入法是贴在屏幕下边的 —— 在转过 90° 的画面里那正好是右边。所以打字追问的输入条现在会贴在键盘上方(不再被键盘盖住,也不会跑到对面去)。 ③ 想用嘴说:输入框右侧有 🎤 按钮。它能用就直接把你说的话变成文字填进输入框(只有 https / localhost 能用,例如在电脑上);手机走局域网 http://192.168.x.x 时浏览器不给网页麦克风 —— 这时点 🎤 会直接告诉你原因,并顺手把输入法键盘叫出来,点键盘上自带的 🎤 一样能语音打字,说完点「发送」。 |
| NPC 不出声怎么查(v24r100 起:内置台词一律有声) | NPC 说的每一句内置台词现在都是随包的 mp3(mu3d/voice/,149 条)—— 不联网、不用装中文语音包、安卓手机也能出声。所以「没声音」基本只剩下面三种:① 没点过屏幕:浏览器要求页面先有一次点击,才允许它自动出声(这一下同时在给音频解锁)。手机上先点一下,再走近 NPC。 ② 看字幕条下面那行暖色小字:它会直接写清是哪一环没通(点一下屏幕 / 装中文语音包 / 填密钥),照它说的做即可。 ③ 走错地图了:网址后面加 ?oldtown=1 进旧都铎小镇 —— 那里的居民 NPC 最密集;风暴城要塞里的剑士 / 剑姬 / 法师 / 仙灵 / 武者 / 异客也都会说话,但要真的走近。想换音色或改台词:改完 js/npcvoice.js 里的台词,重跑一次 python mu3d/tools/make-voice-clips.py(需 pip install edge-tts,只在生成时联网)—— 它会自动只补缺失的那几条并重建清单,已有的 mp3 直接复用。 |
| 输入框的字和手机键盘同一个方向(v24r101) | 手机竖屏被网页强制转 90° 时,输入框自己也转了 90°,和手机自带键盘的字同一个方向 —— 抬手看键盘、低头看输入框,两边不用再换着脖子看。 位置仍贴在键盘那一侧:长边顺着屏幕铺开、两端各留 12px;键盘一弹起就挪到键盘上沿外侧 10px,整条都在键盘外面(不会被压住半截)。 方向是实测定的:反方向(+90°)在同一块屏上是倒的(字上下颠倒,见变更日志里那两张对比图),所以默认用它。真要翻过来,说一声即可 —— 不只是改角度,锚边与原点也要跟着换。 |
| 装备 | 拾取时自动穿上更强的;点背包或装备栏可手动调整。装不下的先存仓库(见上一条) |
| 相机侧倾v24r82 · v24r85 起默认关 |
画面会像「歪头」一样侧倾 —— 这是 mu3d 以前完全没有的能力(旧版相机 up 恒为 (0,1,0),做不出侧倾)。 ⚠ 真机反馈「走动时画面左右摆动,绝对不可以」→ v24r85 已默认关闭:不传参数就永远水平,走路不会再摆。想看效果点下面按钮。 举例:向右横移时画面向右压弯,满速约 11.5°;被怪打中的瞬间甩到 12.8° 再回弹,3 秒后精确归零。 来源 = 横向移动速度 + 镜头转向率,两路叠加走临界阻尼(帧率无关、不过冲);受击另走欠阻尼通道。 网址参数 ?bank1=1 显式开启 · ?bank0=1 强制关 · ?bankflip=1 换方向 · ?bankmax= 调上限 |
| 3D LUT 调色v24r81 |
真正的 16³ 立体查找表(4096 个采样点)+ 四面体插值,能跨色相做精确映射。 举例:把土黄统一压成青灰、同时保住朱红的饱和 —— 旧版「阴影/中间调/高光各乘一个颜色」的乘法近似会把朱红一起推走。 比三线性好在立方体体对角线附近没有可见接缝;WebGL2 走原生 3D 纹理,老内核(微信/QQ)自动退到 256×16 条带,两路共用同一份烘焙数据。 键盘 L 同效 · 网址参数 ?lut=mu|dusk|neo|off · ?lutstr=0.55 调强度 |
| 屏幕空间描边v24r81 |
描边改为读深度图算边缘(中心 + 4 邻居取最大深度差),不增加任何三角形。 举例:旧版是「复制网格放大 + BackSide」的几何壳,三角形数直接翻倍,所以注释里一直写着「只给 Boss 保留、普通怪省掉」;现在所有怪都能有描边。 三处必须处理:相对归一化(差值÷中心深度,否则远景整片描黑)、天空排除(否则地平线画一条线)、距离淡出 + smoothstep 柔和阈值。 不可用时自动退回几何壳描边,老行为一字不变 · ?nooutline=1 关闭 · ?geomoutline=1 强制几何壳 |
| 模型解码缓存v24r81 |
同一「文件 + 配置」只真解析一次,其余走 clone 深拷节点树(几何/材质/贴图共享引用,解码只发生一次)。 举例:实测 cold 474.9ms → warm 31.7ms,16 hits / 12 misses,省 93%。 骨架必须按同名骨骼重建 Skeleton 再 bind(否则两条蒙皮互相拉扯);每解析一次让一帧(MessageChannel),最长连续阻塞从 182ms 降到单个最大 36ms。 顺带修两个真 bug:OBJ 的 obj 字段漏进缓存(材质面板会整块消失);缓存命中绕过了解析器可用性检查(assertLoader)—— 这条最值钱。 |
| 慢启动豁免v24r83 |
引擎还在加载时,健康自愈不再打断它;只有「明确失败」才自救,且每会话最多一次。 举例:无头软渲染实测 __MU_BOOT 要到 38.32s 才可用,而旧版 15s 就清缓存重载 —— 等于把已经启动成功的会话干掉,还可能无限重载。现在软渲染预算自动放宽到 90s。 可调 ?slowboot=<毫秒> |
| 相机避障「慢慢恢复」v24r87 |
相机撞到墙 / 城堡被压近之后,障碍一走开是慢慢地退回原来的视距,不再「唰」一下弹回去。 举例:19 米的摇臂被压到 5.7 米,放开后 0.63 秒才回 26%、1.27 秒回 59%、3 秒走完 95%,之后还有一条长长的缓尾。旧版(收放同刚)这一段 1.31 秒就走完 95%、峰值速度 17.7 m/s —— 相当于一列高铁的回弹。 做法:把「收」与「放」拆成两个刚度 —— 拉近保持 13(撞过来必须让得快,才有安全余量),放远降到 2.5(ω 3.61 → 1.58,峰值速度砍 56%)。两个方向都仍是临界阻尼解析解,所以慢下来也不会带回 r71 那种「一快一慢的节奏差」。 网址参数 ?camcolkout=1 更慢(全段约 4.7 秒)· ?camcolkout=13 回到旧版对称手感 · ?camcolkin= 调收臂速度 |
| 法师 · 程序龙卷风v24r86 |
法师的技能换成漏斗状气旋,全部零贴图、零模型 —— 几何现搓(旋转体漏斗 + 扬尘裙 + 贴地尘环;环带默认关 —— 用户「不要四个圈圈」),气流纹路是 256×256 canvas 现画的。 举例:同一份几何跑三层、转速不同(1.16× / 1.0× / 0.62×)+ uv 螺旋偏移 ⇒ 才有体积感;粒子径向速度取负(向心)= 尘被卷进去再甩上来,底部比顶部密。 打开独立展示页 tornado.html 可拖动调高度 / 底半径 / 自转 / 透明度 / 尘量;把 model-map.json 的 fx 改回 "dragonWave" 就退回黑龙波 |
| 战士 · 技能调节台v24r88 |
战士三个技能做了一台独立可调台(warrior.html):地裂斩冰山 / 旋风斩 / 天地十字剑 三个页签各带自己的参数。地裂斩可调:高度 / 底半径 / 复制座数(1~8) / 间距 / 左右错开 / 伴生小冰山(0~5,默认 0 = 只要一座) / 存续秒数 —— 冰身色与冰心辉光两套色板成对给(6 组配色,首组就是冰风谷)。 场景是冰风谷的冷调:冰面地面 + 远处冰柱做尺度参照 + 程序化剑士扛巨剑交代朝向;阴影真的开着,能看出冰山落在地上的样子。 打开战士技能调节台 warrior.html 拖动 = 环绕 · 滚轮/双指 = 缩放 · ?auto=1 直接进自动循环 · 「恢复默认」一键还原 |
| 程序黑龙 · 天上飞v24r89 |
一条真的龙在天上飞:头(锥吻 + 下颚 + 双角 + 发光双眼)、颈、身、四爪、双翼、尾,全套。 为什么程序化:项目里没有龙的模型( assets/models/ext 只有 fox / fairy / alien / knight / mage…),所以整条龙几何现搓 —— 跟法师那个龙卷风同一路子,零模型零贴图。
龙身 = 沿曲线扫出的管:曲线是解析蛇形,管半径沿程收细(尾 0.35 → 中 1.0 → 头 0.55)⇒ 头粗尾细,不是等粗的管子;飞行动作是沿程相位差,头部先动尾部跟上,像真的在游,不是整条刚性平移。
⚠ 黑龙最大的坑:龙体必须用普通混合—— 加法混合下黑色 = 加 0 = 彻底看不见(dragonWave 的 v24r76 单色模式踩过同一个坑,那次把blend 也切了)。所以只有眼睛和翼膜走加法混合,那才是黑龙最好认的地方。
打开程序黑龙 dragon.html
可调:飞行高度 / 速度 / 左右绕行 / 蛇形幅度 / 翼扇动 / 侧倾转弯 / 体长 / 大小 / 存续;6 组配色(龙体一律深色,眼光翼膜走亮色);有「跟随龙头」开关和四档预设机位
|
| 蓝黑龙波 · MU 经典龙波v24r90 |
按参考图做的另一种形态:一条侧视的长龙从角色身上向前冲出去 —— 和上面那条天上飞的立体龙是两回事。 龙身是一节一节的(参考图的身体就是节段,像龙脊/蜈蚣)—— 这是它和「能量波」的区别。全龙共用一份八面体几何,每节只改 scale,15 节也只占1 份 GPU 缓冲。体型头粗尾细(profile 沿身体递变)。 部位:头(锥吻 + 张开的下颚 + 双角 + 两根长龙须 + 发光双眼)+ 背鳍 + 展开双翼 + 四爪(前 2 后 2)。龙须是「龙不是蛇」的关键。 运动:节段按 sin(t·ω − u·k) 摆动,u 沿身体递增 ⇒头部先动、尾部滞后,龙身在空间里扭着走,不是整条刚性平移;翼缓扇、须向后飘、角随动微颤。 ⚠ 配色坑(同前两条):龙体必须普通混合 —— 加法混合下蓝黑 = 加 0 = 彻底看不见。只有眼睛 / 翼膜 / 须尖走加法,那才是黑龙波最好认的地方。 打开蓝黑龙波 dragonwave.html 可调:体长 / 体粗 / 节数(5~30) / 离地高 / 推进速度 / 波动频率 / 摆幅 / 翼扇动 / 存续;6 组配色(首组即蓝黑龙波);「跟随龙头」开关 |
| 水墨群龙 · 五龙戏墨v24r91 |
按用户口径把黑龙改成国画水墨 + 会拐弯 + 5 条一起飞 + 若隐若现,四项都是给 dragonSurge加参数(不给就是原来的蓝黑龙波,逐字节不变)。① 水墨:墨色浓淡不匀(每节一份材质,深浅由 sin 分布)+ 每节套一层放大的「墨晕」壳。水墨的浓淡不靠颜色,靠墨在纸上洇开的边界;而且墨晕必须用普通混合(加法会让浓处发亮,那是发光体不是墨),节的 emissiveIntensity 压到 0.05。
② 拐弯:改走正弦蛇形航线 —— 切向与前进方向成 sin(k·s),位置用解析积分。所以是一直拐(第一版用固定贝塞尔弧,t→1 之后龙就直着走出画面,拐弯只发生一次),而且与帧率无关。
③ 五龙:dragonSurgeGroup 一次放 N 条,横向 / 高度 / 朝向 / 相位四个维度散布 —— 只散位置的话五条会叠成一条粗的。
④ 若隐若现:三个不同频率的正弦叠加(单频一眼看穿规律),五条各带相位偏移(否则像整体闪灯);墨晕壳同步呼吸。
打开水墨群龙 dragonink.html
16 个滑块:条数 / 横向散布 / 高度散布 / 朝向散布 / 浓淡起伏 / 起伏快慢 / 墨色浓度 / 体长 / 体粗 / 节数 / 飞行高度 / 推进速度 / 拐弯强度 / 蛇形摆幅 / 波动频率 / 存续;6 组水墨配色(墨+纸成对);宣纸底色 + 皴笔地面 + 三层远山,不开阴影(墨没有投影)
|
| 仓库 · 储物箱v24r89 · 全新 |
背包只有 20 格、满了当场丢弃 —— 现在多了一个 60 格仓库(10 列 × 6 行,与背包同列数,看着就是「背包的楼上」)。刷到的好东西先存着,刷新 / 关浏览器 / 换角色都不会丢。 举例:刷一小时爆了 30 件装备,以前只能一边刷一边卖(想留的那件往往已经被卖了);现在面板里点一下「全部存入 →」一键搬进仓库,回城再慢慢挑。 口径:仓库里的装备是冷存储 —— 不参与自动装备、也不参与「一键最强」,免得没穿的东西在偷偷生效;存进去时统一过一遍字段清洗(旧档 / 改坏的存档读进来不会把界面搞崩)。 快捷键 K · 容量 ?stash=80 · 背包满时自动入库 ?stashauto=1 · 整体关掉 ?stash0=1
|