LastStand v0.7.1 纯修复版:开发闭环复盘
入档:2026-07-29 项目:LastStand · 死守(Godot 4 波次生存 FPS) 项目目录(库外裸路径):
E:\last-stand发布地址:GitHub Release v0.7.1 itch.io:https://mr-salticidae.itch.io/last-stand 验证状态:⚠️ 全部 headless 自动化校验;没有真机试玩,手感变化未经验证;后期敌人分布调整(第 5 条)作者尚未打到第 25 波之后验收
事实记录(不可修改区)
- 起点:作者经医生建议停药精神科药物已一年半、状态稳定,经两周前复诊确认可控;四个多月前接触 vibe coding 后启动 LastStand 开发,状态出现持续性变化(睡眠需求减少、精力充沛)。本次 v0.7.1 是这一周期下的产出。
- 版本性质:纯修复版,没有新内容。从 v0.7.0(2026-06 发布,经济改造 + 步态动效 + 实测批修)升级而来。
- 五个修复全部来自代码审读:没有一条是玩家报的。作者逐文件读代码时发现。
- 修复 ① 波次剩余数显示错误:每波开局 HUD 先显示「剩 0 只」,再花约 0.8 秒从 0 数到满。根因:
_alive_enemies.size()在生成阶段取的是「已落地数」,而敌人要等各自的spawn_effect_duration(0.8 秒,boss/elite 1.28 秒)生成烟播完才 append。改法:新增_wave_total与_wave_killed,剩余数 = 总数 - 已击杀。对照实验坐实:改前 9 个事件(0→1→2→…→7),改后单个remaining=7。 - 修复 ② 提前清波(竞态):
_spawn_at是协程但不被 await,生成循环只等spawn_stagger(0.1 秒/只)就置COMBAT。此时最后几只还没落地。这段窗口(最长 0.8 秒)内清光已落地的敌人会提前通关、弹升级面板,剩余敌人生成到下一波。改法:新增_spawns_pending计数,清波判定前检查是否还有未落地的敌人。 - 修复 ③ 弹药节省在霰弹枪上多倍生效:传说卡,卡面「命中时 25% 概率不消耗弹药」。原回补写在
_fire_pellet内,该函数按弹丸调用;霰弹枪pellet_count = 8,一枪扣 1 发却 roll 8 次,8 颗全中时期望回补 2.0 发——弹匣越打越满,直接抵消 v0.7.0 刚做的货币稀缺化。改法:移到try_shoot整枪只 roll 一次。 - 修复 ④ 切枪打断换弹后的计时器串扰:
on_unequipped清is_reloading,但已排下的create_timer仍会到点。切回来重新换弹时,旧回调会把新的那次提前结算。改法:加_reload_token代次校验,回调只认自己那一代。 - 修复 ⑤ 后期 grunt 绝迹:敌种抽取用一次
randf()分区间,brute与runner概率各自随波次增长、上限之和(1.0 + 0.55 = 1.55)超过 1.0,第 21–23 波起 grunt 拿不到任何区间,同时 runner 被静默截断到 40%。改法:brute_chance_max从 1.0 收到 0.30,后期稳定为 brute 30% / runner 55% / grunt 15%。附带影响:grunt 专属的包抄 AI(极限档限定)不再从最后十波消失。 - 内部改动:敌人互推 / 告警 / 同步攻击三处原本每帧各自调
get_nodes_in_group("enemy"),改为按物理帧号缓存一次、全场共用;补齐互推里缺失的is Enemy类型守卫;README 引擎版本从 4.6 更正到 4.7(项目实际已是 4.7);新增tools/validate_scripts.gd脚本编译校验工具。 - 发布流程:GitHub Release 自动构建 Windows 包 → 上传 Release 附件;B站动态经 Ultracode 工作流(3 角度起草 × 3 视角评审 → 合成定稿)产出;itch.io devlog 与 GitHub Release 文案另存。
- 未验证:没有真机试玩;第 5 条改了后期敌人分布,作者还没打过第 25 波之后;没有玩家反馈回收。
以上先按 复盘事实先行原则 冻结。下文所有「有效」只指本次修复逻辑成立,不外推成「游戏平衡已调好」或「手感已改善」。
一句话结论
这一版的核心教训是:没有测试套件的项目,修复的置信度靠工具补;五个 bug 全藏在「两个看起来一样的数其实不是一回事」里——剩余数 vs 已落地数、整枪 vs 单弹丸、当前换弹 vs 被取消的那次换弹。
一、实际走通的链路
- 先建安全网再动手:项目零测试,先写
tools/validate_scripts.gd全量编译校验(57 个脚本),作为改动前后的回归基线。没有这一步就不该改核心战斗文件。 - 从最容易藏 bug 的文件下手:
enemy.gd1479 行、wave_manager.gd366 行是状态机最复杂的地方;v0.7.0 刚改过的经济系统(weapon.gd)是回归风险最高的地方。这三处果然全中。 - 每个 fix 必须有独立证据:① 用运行时探针 + 改前/改后对照(9 个事件 → 1 个事件);③ 用常量核算(
pellet_count=8×ammo_save_chance=0.25= 期望回补 2.0);⑤ 用参数求和(1.0 + 0.55 > 1.0)。 - 诚实边界前置:B站动态里明确写了「只跑了 headless 校验,没真机试玩」「Windows 包还没传」。这些不是在谦虚,是在避免读者基于错误前提做决策(比如下载不存在的新版本)。
- 构建产物与发布文案分开存档:三渠道(GitHub / itch / B站)文案各存一份在
docs/release_notes/v0.7.1.md,B站那条又经 Ultracode 工作流单独打磨。
二、工具链真正帮到的环节
1. 脚本编译校验器
--editor --quit 只解析被场景引用到的脚本,孤立文件的语法错误抓不到;--check-only 阶段 autoload 尚未注册,57 个文件里 21 个误报 Identifier not found。最终方案是在完整启动的引擎里逐个 load() + reload()。
踩到的坑:编译失败的脚本 ResourceLoader.load 仍返回非 null 对象,只判空会把所有文件都报成健康,必须看 reload() 的返回码。
2. Ultracode 工作流写 B站动态
3 个角度(沿用旧格式 / 「剩 0 只」开场钩子 / 开发者复盘)并行起草,每稿由 3 个独立视角(B站观众 / 事实核查 / 作者语气)评分。结果:story 与 devlog 并列 7.7 分,convention 7.0 分。以 story 为骨架合成,事实核查视角砍掉 9 处失真表述(「上线这么久」→ 项目才两三个月;「清光场上敌人」→ 逻辑自毁;「把上一版经济改造抵消了」→ 只在抽到卡且用霰弹枪时成立)。
这延伸了 子代理并行批量生成与校验组装_v1 的核心:并行单元应互斥,最终组装与验收必须集中。
3. 导出模板的安装
Godot 4.7.1 只装了 Web 模板(上次为 Toy 平台下的),Windows 模板要从 GitHub Release 下载。.tpz 本质是 zip 但扩展名不同,Expand-Archive 不认;后台下载进程锁住文件时,FileShare.ReadWrite 也绕不过。最终先让下载跑完(约 1.2GB),再用 .NET ZipFile::ExtractToDirectory 解压,解压后注意模板在 templates/ 子目录里,要移到 4.7.1.stable/ 根下 Godot 才认。
三、阻塞与恢复
| 阻塞 | 事实原因 | 恢复方式 | 沉淀 |
|---|---|---|---|
| 项目零测试,改核心战斗逻辑无安全网 | 没有人手写的单元/集成测试 | 先写 tools/validate_scripts.gd 全量编译校验,57 个脚本作为回归基线 | 无测试的项目,工具补置信度;校验器本身要故意插一个语法错误验证它真的会报 |
--check-only 报 21 个假阳性 | 该阶段 autoload 未注册 | 改为在完整引擎内 load() + reload() | 两种 CLI 校验方式都有盲区,不能单独依赖 |
ResourceLoader.load 对坏脚本返回非 null | Godot 设计:编译失败仍返回 GDScript 对象 | 用 reload() 返回码判断,不用 null 检查 | 校验脚本编译状态必须看 reload() 返回码 |
| Windows 导出模板没装全 | 4.7.1 只装了 Web 模板 | 从 GitHub Release 下载 1.2GB tpz,解压后移到正确目录 | 发布前检查模板完整性,别等 Export 失败了才装 |
| B站动态初稿失真 | 多个说法超出事实边界 | Ultracode 工作流 + 独立事实核查视角砍掉 9 处 | 对外发布内容必须过独立事实核查,作者自审会漏 |
| 后期敌人分布改了但没验收 | 作者还没打过第 25 波之后 | B站动态先标「未验证」,等朋友试玩反馈 | 平衡调整必须真机验收,不能只靠逻辑推导 |
四、可复用方法
方法 1:剩余数 ≠ 已落地数
凡是「生成需要时间」的波次/队列系统,剩余数必须由「计划总数 - 已消耗」推导,不能直接取「当前已存在实例数」。 Godot 里 spawn + 协程 await 是典型场景:循环不等协程结束就推进状态,导致「系统认为的剩余」和「玩家看到的剩余」不一样。
方法 2:按弹丸结算 vs 按枪结算
多张「单次开火多个子单元」的设计(散弹、多弹道、连射),所有「概率触发」效果必须在整枪层级 roll 一次。如果在子单元层级 roll,期望值 = 子单元数 × 概率,平衡直接崩。这次 ammo_save_chance 在霰弹枪上就是 8 倍生效。
方法 3:异步操作的代次校验
凡是「可取消的延迟操作」(换弹、施法、定时技能),取消时要同时作废已排下的回调。 Godot 里 create_timer 不会自动取消,必须手写 token / 代次校验。模式:每次启动自增 token,回调只认自己那一代。
方法 4:概率上限之和必须 < 1.0
用一次 randf() 分区间抽类型时,所有类型的上限之和必须严格小于 1.0。否则最「晚」的类型会彻底拿不出区间,同时倒数第二类会被静默截断。这次 brute 上限 1.0 + runner 上限 0.55 = 1.55,grunt 直接消失。
方法 5:诚实边界三问
发布前自问三个问题,答不清楚就不写:
- 这件事我真的实测过吗?(没实测 → 不写「手感好了」)
- 这件事所有人都能接触到吗?(构建包没传 → 不写「快去下载」)
- 这件事的前提条件是什么?(只在极限档成立 → 不能写成全局结论)
五、上帝视角:这一轮的整体图景
v0.7.1 是一个特殊周期下的产出:作者经医生同意停药、状态稳定,经复诊确认可控,在 vibe coding 启动独立游戏后进入一个持续性高精力周期。这一版没有加任何新东西,但五个 bug 全藏得深——不是因为代码写得差,而是因为状态机的边界语义容易被误解(剩余 vs 落地、整枪 vs 弹丸、当前换弹 vs 被取消的换弹)。
它同时是一次完整的发布链路演练:代码审读 → 修复 → headless 校验 → 构建 → GitHub Release → B站动态 → 知识库复盘。这条链路从 5 月项目启动以来第一次完整跑通,下一版(v0.7.2,计划:玩家反馈驱动的经济/难度调优 + 武器 2D 持枪视图 + HUD 重绘)会在同一套安全网下迭代。
关联文档
- 顶层方法论:复盘事实先行原则 · 交付前实测证伪律_v1 · 子代理并行批量生成与校验组装_v1
- 工具链经验:Claude_Code_Worktree隔离的协作陷阱 · Claude完成报告核查心法
- Godot 行为档案:导出运行时无系统字体回退律_CJK豆腐块_v1(Godot 导出系列)
- 项目资产:
E:\last-stand(库外裸路径) - 发布产出:GitHub Release v0.7.1 · B站动态文案