Trigger Integrity And Onscreen Gate
Archived on 2026-08-20 · roadmap: game-web-phaser-template
Archived: 2026-08-20 · roadmap-id:
game-web-phaser-template
Why
一句话
断言的触发方式,使它对被验对象的损坏免疫。
真实经过
那一版游戏里旗子(终点)与刺一直往下掉、掉出画面。直接根因在游戏代码:
this.goal = this.physics.add.sprite(...)
this.goal.setImmovable(true) // ← Arcade 里 immovable 不关重力,只表示"不会被碰撞推动"要静止得 setAllowGravity(false) 或用 staticSprite;刺同理用了动态的 physics.add.group(),
而地面用的是 staticGroup()(正确)。全文件只有一处 staticGroup、零处 setAllowGravity。
但这一刀不修那个游戏。 要修的是下面这件事:
registerTrigger('level_advance', () => this.simulateReachGoal())
// simulateReachGoal() → this.player.setPosition(this.goal.x, this.goal.y)它把玩家传送到旗子的当前位置。旗子掉到哪,玩家就被传送到哪——overlap 照常触发、
分数照常变、score_feedback 照常绿。断言与被验对象共同移动,因此永远不会红。
🔴 这条规矩本来就有,它是靠人工 review 兜的,然后它失效了
templates/game-web-phaser/AGENTS.md:70 逐字写着:
The handler may only do what a real player's own actions could cause — spawn something in the world and let the existing overlap/collision logic react … This one line is enforced by human review, not by the type system; do not treat "the linter didn't complain" as permission.
模板参考实现做的是往玩家脚下生成一枚硬币(GameScene.ts:261 spawnCoinAtPlayer),让既有
overlap 逻辑自己反应。执行者写的是反过来的:把玩家搬到目标处。它没违反字面(没直接写
score),却破坏了意图。
所以本刀的形态不是"补一条规矩",是把一条已被实测证明会失效的散文纪律挪进代码层——
与 cogito-lib backlog 条目 13 同形态,也与该仓库既有的三处做对了的先例同形态
(scaffold-probe.ts / git-identity.ts / artifact-port.ts 都把判断从散文挪成了平台执行的探测)。
影响面不是这一版游戏
凡是用「registerTrigger + 把玩家传送到目标处」这种模式实现的断言,都有同样的免疫性——
它绕过了"玩家能不能真的走到那里"这个真问题。而模板文档鼓励的正是 registerTrigger 这套机制。
Non-goals
- ❌ 不修「用话造关」那一版游戏。存量生成项目不会自动拿到模板改动(发布链路是 changeset → Version Packages PR → npm → 下一次脚手架)。这一点如实记在这里,不假装它被修了。
- ❌ 不改断言模板集合(仍是上游 7 个),不新增
templateId。 - ❌ 不给 A/B 任何逃生阀(环境变量开关、
skip字段)。能被关掉的闸不是闸。 - ❌ 不动
press()/applyState()的既有契约,不新增任何 setter 形状的 harness 方法。