# 《磁铁别乱吸》技术方案、埋点与验收计划

> 目标：把“概念可玩”与“可以上线”分开验收。`demo.html` 证明核心交互；真实跨设备存档、榜单校验、广告回调与合规能力必须在平台环境另行联调。

---

## 1. 交付边界

| 层 | 本次交付 | 不应误报为已完成 |
|---|---|---|
| 核心玩法 | Canvas 磁力、碰撞、永久结团、危险连带、至少两类胜利谓词 | 完整商用物理引擎、84 关量产 |
| 持久化 | 统一 SDK 的 H5 “云/本地”双写模拟，刷新可恢复 | 真实微信/抖音跨设备云存档 |
| 社交 | 榜单/求助 UI 与模拟数据 | 真实防作弊榜、好友回流计数 |
| 广告 | 激励视频倒计时模拟、前 90 秒闸门与免费出口 | 真实广告 SDK、真实收益或 eCPM |
| 支付 | 不实现 | 去插屏/外观直售验单、退款与持权 |

只有本表右列全部补齐并通过平台/双设备实测，才可把 F1/A9/F6 从“方案通过”改成“运行态通过”。

---

## 2. 客户端架构

```text
输入层（Pointer Events / 键鼠兜底）
        ↓
边轨约束 + 极性状态 + 广告/暂停闸门
        ↓
固定步长磁场物理（60 Hz）
        ↓
碰撞解算 → 永久结团 → 场景谓词
        ↓                    ↓
表现层（Canvas/WebAudio）    事件/确定性回放
        ↓                    ↓
结算/Meta UI ← GameSDK 适配层 → L1/L2 API（正式版）
```

技术栈：原生 HTML/CSS/JavaScript、Canvas 2D、Pointer Events、WebAudio；零构建、无 CDN、无第三方运行时依赖，外部只引用仓库共享的 `../sdk/game-sdk.js`。

### 2.1 帧循环

- 渲染用 `requestAnimationFrame`，逻辑用累加器固定 `1/60s` 步长。
- 单帧最多补 5 个逻辑步，防后台恢复时“物件穿墙”；超额时间丢弃并埋点 `physics_frame_drop`。
- 物理侧只读取不可变的 `level.seed`，同一输入不因尝试次数变化；奖励侧另用 `rewardSeed + runAttempt`，与物理解算完全分流。
- 物理状态使用整数或统一精度量化后写回回放，避免设备浮点差异扩大。

### 2.2 磁力与可读性

简化磁力不追求科学仿真，追求稳定、可解释：

```text
distanceNorm = clamp(distance / fieldRadius, 0, 1)
force = polarity * power * (1 - distanceNorm)^2 / max(mass, 0.5)
velocity = clamp((velocity + direction * force * dt) * damping, maxSpeed)
```

- 磁铁只在场外边轨移动，防止“贴着目标硬拖”的万能解。
- 手指按下先进入 200ms 预览宽限，预测线立即出现但磁力尚不施加；宽限结束后才连续通电。倒计时全程继续，松手不返还时间，结算完成前锁输入，防止反复短按套利。
- 场强圈、受力箭头、预计终点虚影同时显示；危险对象用图形符号、抖动和连带线三重提示，不以颜色作唯一语义。
- 吸引/排斥使用相反箭头、磁铁朝向与文字标签三重区分。

### 2.3 碰撞与永久结团

- 动态物件首版统一圆形碰撞体；视觉可为非圆，但命中轮廓必须可见。
- 非穿透修正后计算法向相对速度；低于 `clusterRelativeSpeedMax` 且双方可磁化时结团。
- 团块保存成员 ID、总质量、包围半径、危险标签并集、目标标签并集；本局永久不可拆。
- 排斥只把团块当一个整体推动。关卡若需要拆团，属于新引擎，不能悄悄加进当前规则。
- 团块进入出口时按成员逐项验收；只要含 `battery`，危险出口立即失败。

### 2.4 易碎物与推物

- 非磁性可推物受团块碰撞冲量；位移和完整度是独立状态。
- `impactSpeed > fragileBreakSpeed` 时破裂，先定格再结算失败原因。
- 轻推关使用“位移 ≥X 且完整度 >0”谓词，不以清屏或分数替代。

---

## 3. 关卡数据与离线校验

示例见 [`levels.json`](levels.json)。正式 schema：

| 字段 | 必填 | 说明 |
|---|---|---|
| `id/chapter/name/scene` | 是 | 唯一 ID 与一句具象剧本 |
| `seed` | 是 | 确定性种子；发布后不可静默更改榜单种子 |
| `timerSeconds` | 是 | 威胁时长；L1 可设宽松，不用 0 表示隐藏规则 |
| `magnet.rail/polarities/start` | 是 | 可移动边轨、开放极性、起点 |
| `goals[]` | 是 | `extract / forbidExtract / push / bridge / protectInZone` 等谓词 |
| `goals[].unlocksObjectId` / `objects[].lockedByGoal` | 按依赖 | 前置谓词完成后才解除目标锁定；锁定物可见但不受力、不可入槽，例：先移开玻璃再提取照片 |
| `ports[]` | 按目标 | 救援口、保管区、危废区 |
| `objects[]` | 是 | 归一化坐标、质量、半径、标签 |
| `terrain[]` | 否 | 墙、窄缝、桥基、可推遮挡、移动威胁 |
| `tutorial[]` | 否 | 首次出现的新规则，不生成遮屏教程页 |

目标依赖必须由数据驱动：`lockedByGoal: "push:glass"` 指向同关唯一谓词；谓词未完成时目标仍参与可见性与危险预览，但磁力解算和入槽判定跳过它。完成后在同一固定逻辑步解除锁定并写入回放事件，不能只靠 UI 遮罩伪装。

### 3.1 发布前自动检查

每个关卡必须通过：

1. ID、对象 ID、port/zone 引用唯一且存在；
2. `unlocksObjectId / lockedByGoal` 双向引用一致、依赖图无环，所有锁定目标最终可由可解谓词释放；
3. 至少一个具象胜利谓词，禁止 `score >= N`、`clearAll` 作为唯一胜利；
4. 除显式标记 `pureSuccessTutorial` 的生产 L01（`levels.json` 样例为 EX01）外，至少一个“不可乱吸”的对象、危险后果或空间目标；纯成功豁免只允许一关且不得进入榜单；
5. 目标对象初始不在出口，危险对象初始不触发失败；无显式 `contains/cover` 关系的碰撞体初始不得重叠；
6. 固定种子求解器或人工回放至少有一个解；
7. 标准工具等级可解，Lv0 也可免费过；
8. 最优回放不是“把磁铁沿边扫一圈持续按住”；
9. 同一输入回放 20 次结果哈希一致；
10. 低端设备模拟不出现穿墙或 NaN；
11. 关卡首发回放、schema 版本与校验哈希一同归档。

---

## 4. 持久化数据模型

### 4.1 L1 玩家存档

```json
{
  "schemaVersion": 1,
  "revision": 42,
  "gameId": "g24_citie",
  "updatedAt": 1783651200000,
  "progress": {
    "highestLevel": 12,
    "starsByLevel": {"L01": 3},
    "tutorialFlags": ["drag", "danger-link"],
    "freeTrialUsedByLevel": ["L01", "L02"],
    "firstNearMissReplayUsed": true
  },
  "economy": {
    "coin": 360,
    "energy": 4,
    "energyAt": 1783651200000
  },
  "meta": {
    "workshop": {"lens": 2, "stabilizer": 1, "arm": 1},
    "yardNode": 4,
    "repairJob": {"node": 5, "completeAt": 1783652100000},
    "relicIds": ["key_01"],
    "relicPity": 3,
    "relicBox": {"readyAt": 1783654800000, "freeClaimDate": "2026-07-10", "adOpensToday": 1},
    "skins": ["magnet_default"],
    "displayCommissionIds": ["2026-W28-photo-01"],
    "commissionCycle": "2026-W28",
    "streak": {"count": 3, "lastDate": "2026-07-10", "repairWeek": "2026-W28"}
  },
  "daily": {
    "date": "2026-07-10",
    "tasks": {},
    "rewardedClears": 6,
    "adCaps": {},
    "shareCaps": {}
  },
  "entitlements": {"removeInterstitial": false, "cosmetics": []}
}
```

原则：关卡进度、币、工坊、图鉴、保底、streak 与持权全部云端；本地只是离线缓存。服务端保存修订号，客户端提交 `baseRevision`，冲突按字段类型合并：

- 单调进度取较高值；
- 集合取并集；
- 钱包只重放服务端流水，不做 `max(local, cloud)`；
- 保底取已确认广告/宝箱事件重算，不接受客户端直接写计数；
- `freeTrialUsedByLevel`、`relicIds`、`skins`、`displayCommissionIds` 取集合并集，避免重复免费资格或丢永久陈列；
- `firstNearMissReplayUsed` 按布尔 OR 合并，任一设备已领取就不能再次白给；
- `relicBox.readyAt / freeClaimDate / adOpensToday`、修复等待与 streak 均按服务端日期、时区和幂等事件重算，客户端时钟不参与发奖。

### 4.2 L2 轻服务端端点

| 方法/端点 | 用途 | 关键校验 |
|---|---|---|
| `POST /session/login` | 平台 code 换 uid/session | 平台签名、实名/防沉迷状态 |
| `GET/PUT /save/g24_citie` | 拉取/提交存档 | revision、schema、字段白名单 |
| `POST /economy/claim` | 结算/任务/广告奖励 | nonce 幂等、服务端奖励表；`rewardedClears` 每自然日硬帽 8，超帽不发币/奇物 |
| `POST /economy/spend` | 工坊、修复、外观、同图重置、双周陈列委托 | `txnId` 幂等、配置价、资产前置、revision；原子扣币 + 发资产，客户端不能直写余额 |
| `POST /relic-box/open` | 免费箱、等待完成或 `rv_relic` 开箱 | 服务端时间、placement 回调、每日帽、pity 原子更新 |
| `POST /replay/submit` | 同图榜成绩 | 关卡哈希、输入流、工具标准化、`assistFlags`、不可回滚 `totalPoweredMs`、时长合理性 |
| `GET /leaderboard/daily` | 好友/城市/全服分榜 | 隐私授权、分组与分页 |
| `POST /share/create` | 创建求助卡 | 每日上限、场景白名单 |
| `POST /share/visit` | 好友回流发双方奖励 | shareId、不同 uid、一次性幂等 |
| `POST /ad/verify` | 平台广告完成回调 | placement、nonce、回调签名 |
| `POST /payment/verify` | 仅二期去插屏/外观 | 服务端验单、退款、持权；冷启动不开 |

---

## 5. 安全与反作弊

- 排行榜只接收输入事件流，不接收客户端自报最终分；服务端用同版本固定步长重放。
- 每日同图使用标准化工具等级；Meta 不进入榜单物理参数。提示、撤销或 5 秒回放一旦使用就写入不可回滚的 `assistFlags`，该局只进练习榜；主榜只收 `assistFlags=0` 的首个有效成绩。
- `totalPoweredMs` 从首次按下起单调累计，回放 / 撤销不回滚；服务端按输入流重算，避免广告擦掉错误通电时间。
- 钱包采用不可变流水：`txnId, uid, source, amount, configVersion, createdAt`。
- 所有消费走服务端原子事务：验证配置价与前置 → 写负向钱包流水 → 发工坊 / 修复 / 外观 / 陈列资产；任一步失败整笔回滚。
- 广告、分享、任务、支付均用唯一 nonce；重复回调返回原结果。
- 客户端时间只用于表现，体力、签到、活动、频控以服务器时间为准。
- 远程配置签名并有回滚版本；不得热更成强制广告、暗改概率或加入违规付费。
- 收集最少个人信息；日志中 uid 脱敏，不记录实名原文或聊天内容。

---

## 6. 埋点字典

所有事件公共字段：`uid_hash, session_id, game_version, config_version, level_id, level_seed, platform, device_tier, ts_server`。

### 6.1 核心漏斗

| 事件 | 关键参数 | 回答的问题 |
|---|---|---|
| `session_start` | source, is_new, streak | 新/回流从哪来 |
| `home_play_tap` | elapsed_ms | 是否 2 秒开玩 |
| `level_start` | energy_before, tool_standardized | 进入率/体力阻塞 |
| `magnet_press` | edge, polarity, affected_ids | 首动作时间、受力对象数 |
| `magnet_move_sample` | x, y, polarity, active_ms | 路径与万能扫边检测；10 Hz 采样 |
| `object_cluster` | member_ids, total_mass, has_hazard | 结团是否改变局面 |
| `danger_link_seen` | hazard_id, target_id | 危险预览是否真正出现 |
| `polarity_toggle` | from, to, elapsed_ms | 极性理解 |
| `goal_progress` | predicate, current, target | near-miss 精确差距 |
| `level_finish` | result, reason, seconds, presses, clusters, replay_hash, is_near_miss, total_powered_ms, assist_flags | 完成率、真实近失、确定性与榜单资格 |
| `retry_tap` | delay_ms, channel | One More Try |
| `level_exit` | reason, progress | 卡死/困惑点 |

### 6.2 留存与 Meta

| 事件 | 参数 |
|---|---|
| `reward_reveal` | coin, rarity, pity_before, pity_after |
| `coin_txn` | delta, source_or_sink, balance_after |
| `workshop_upgrade` | line, from_lv, to_lv, cost |
| `yard_repair` | node, cost, wait_seconds |
| `relic_unlock` | relic_id, source, pity_used |
| `daily_task_claim` | task_id, progress |
| `streak_update` | before, after, repaired |
| `leaderboard_submit` | board, result, total_powered_ms, assist_flags, reject_reason |

### 6.3 广告/分享

| 事件 | 参数 | 纪律 |
|---|---|---|
| `ad_offer_view` | placement, elapsed_session, cap_left | 前 90 秒应为 0 |
| `ad_click` | placement | 必须来自明确按钮 |
| `ad_result` | placement, complete, reward_txn | 一次完播一次奖励 |
| `free_exit_choose` | friction, channel | 监控广告是否成为事实唯一出口 |
| `share_create` | scene, share_id | 不等于发奖 |
| `share_visit` | share_id, visitor_new | 回流后才发奖 |
| `interstitial_show` | previous_break, since_last_ad | 自然断点与间隔审计 |

---

## 7. 实验设计

### 7.1 原型测试顺序

1. 关闭 Meta、广告、体力，只测 6 关灰盒。
2. 测 10 秒理解、物理解释率、首次结团后的路线变化、首败重开。
3. 核心过线后接 L1、工坊、修复和奇物；再测 D1/D3。
4. 无广告版本过 G6 后逐个开放主动激励位。
5. 插屏最后测试，任何留存损伤优先回滚。

### 7.2 A/B 原则

- 用户级稳定分桶，最短覆盖完整留存窗口；同一轮只改一个变量。
- 首要护栏：D1、D7、崩溃率、关卡退出率、广告取消率、投诉率。
- 不能因短期 ARPDAU 上升而接受显著留存下降。
- 未成年人、低样本地区和版本迁移用户不进入商业化实验。

| 实验 | A | B | 主指标 | 护栏 |
|---|---|---|---|---|
| 受力预览 | 1.0 秒 | 1.5 秒 | 10 秒理解/解释率 | 关卡时长 |
| 结团阈值 | 基线 | +10% 容错 | 重开率/爽感 | 万能吸策略率 |
| 首广告位 | 只结算翻倍 | 结算 + 提示 | 激励渗透 | D1/D3 |
| 体力恢复 | 10 分钟 | 12 分钟 | 日会话数 | 次留/负反馈 |
| 插屏 | 无 | 3 局且 120 秒 | 7 日 ROI | D1/D7、退出率 |

---

## 8. QA 验收矩阵

### 8.1 核心玩法

- [ ] 首次进入 ≤2 秒可点击开玩；10 秒内无需文字页完成首个磁吸。
- [ ] 磁铁严格在边轨；手指移出后可恢复，不锁死输入。
- [ ] 吸引与排斥的方向、图标、文字和动画一致。
- [ ] 同一回放运行 20 次，胜负、团块成员与奖励前状态哈希一致。
- [ ] 低速碰撞结团，高速碰撞不结团；团块本关不可拆。
- [ ] 团块危险标签取成员并集，电池不能被外层普通废铁遮蔽。
- [ ] 至少通过提取、轻推、搭桥两类不同胜利谓词。
- [ ] 没有一关以纯分数、纯计时或清屏作为唯一胜利。
- [ ] 只有达到像素距离 / 目标进度阈值的失败才标 `is_near_miss` 和“差一点”；完全未操作或进度过低只显示真实当前进度。
- [ ] 同关第一次失败一键重开不追加扣磁能；资格用完后再次重开扣 1，UI 文案与钱包流水一致。
- [ ] 暂停、切后台、旋转屏幕后无时间跳变/物体穿墙。

### 8.2 经济/留存

- [ ] 新号前 10 分钟能完成 2–3 次可见升级。
- [ ] 金属币所有 source/sink 都有服务端 reason code。
- [ ] `rewardedClears` 每自然日最多 8；第 9 次胜利仍记练习成绩，但不发金属币、不滚奇物、不出现翻倍入口。
- [ ] 体力自然恢复不受改本地时间影响。
- [ ] 6 箱保底在刷新、换设备、离线冲突后不丢不重复。
- [ ] 奇物箱 `readyAt/freeClaimDate/adOpensToday` 使用服务端时间，重复免费领 / 广告回调不重复开箱。
- [ ] 双周陈列委托原子扣币并写入 `displayCommissionIds`；换设备与并发提交不丢、不重复扣。
- [ ] 工坊 Lv0 可过所有主线；Meta 不进入标准榜。
- [ ] 断签补签每自然周最多一次。
- [ ] 35 天至少一张内容/收集/赛季表可执行，不用“以后更新”代替。

### 8.3 广告/分享

- [ ] 新会话前 90 秒 `ad_offer_view = 0`，且结算 / 图鉴页面不出现带视频图标的广告 CTA；`?qa=1` 只用于测试包验收。
- [ ] 所有激励按钮有明确“看视频”字样，未点击不创建广告请求。
- [ ] 广告失败/无填充时给出免费结果或返回原流程，不吞奖励/体力。
- [ ] 结算基础奖励可直接领取；视频只追加一次等额奖励。
- [ ] 每个便利摩擦点至少有广告外的一条免费出口。
- [ ] 插屏只在自然断点，间隔满足远程配置与平台较严者。
- [ ] 去插屏持权启用后只去被动插屏，主动激励入口仍在。
- [ ] 分享取消不惩罚；只有好友真实回流才发双方奖励。

### 8.4 持久化/服务端

- [ ] 设备 A 升级/开图鉴后，设备 B 登录可见同一状态。
- [ ] 清缓存重登不丢关卡、币、工坊、保底、streak、持权。
- [ ] 两设备并发提交不会复制币或回退图鉴。
- [ ] 重复广告/分享/支付回调不重复发奖。
- [ ] 篡改客户端最终分无法上榜；合法输入流能服务端复现。
- [ ] 提示 / 撤销 / 回放后 `assistFlags` 不可回滚且只进练习榜；`totalPoweredMs` 经回放仍单调增加，主榜无法借广告擦时长。
- [ ] `/economy/spend` 重放同一 `txnId` 不重复扣币；篡改陈列 / 工坊价格或直接 PUT 余额均被拒绝。

### 8.5 兼容与性能

- [ ] 360×640、390×844、430×932 竖屏无关键按钮遮挡。
- [ ] iOS 安全区、Android 刘海/手势区均留边；系统字体放大 130% 仍可操作。
- [ ] 桌面鼠标、触屏 Pointer Events 均可玩；无 hover 才能完成的动作。
- [ ] 低端档 30 FPS 以上；逻辑固定步不因渲染降帧改变结果。
- [ ] 同屏 30 动态物件无持续长任务；内存稳定，无每局增长。
- [ ] 关闭声音、减少动态效果开关生效。

---

## 9. 合规上线闸门

- [ ] 实名与国家防沉迷系统联调；未成年人时段/时长由服务端执行。
- [ ] 登录/注册/下载位置有适龄提示、隐私政策与用户协议。
- [ ] 未成年人模式不开放随机箱，改为固定里程碑；不使用断签损失恐吓。
- [ ] 广告、插屏、分享逐项通过目标平台当期自审规范。
- [ ] 素材全部来自真实玩法录屏，目标/失败/奖励与线上一致。
- [ ] 若启用 IAP，仅去插屏与表达型直售；完成审批、服务端验单、退款与未成年人限制。
- [ ] 发行/平台/法务书面确认当期版号、备案、软著、隐私、广告与数据资质；不自行假设纯 IAA 豁免。
- [ ] 数据留存期限、删除/更正入口、未成年人个人信息监护人同意流程确认。

---

## 10. DEMO 本地验收命令

在 `free/20` 目录执行或由 CI 等价实现：

```powershell
# JSON 语法
Get-Content -Raw -Encoding UTF8 .\levels.json | ConvertFrom-Json | Out-Null

# 抽取 demo 内联脚本后用 Node 做语法检查（排除外部 SDK 标签）
# 实际验收脚本可使用仓库临时目录，检查后删除，不提交构建产物。

# 本地链接存在性
@('index.html','demo.html','UI设计需求.md','调研策划案.md','数值与运营表.md','技术与验收.md','levels.json') |
  ForEach-Object { if (-not (Test-Path $_)) { throw "missing: $_" } }

# gameId 唯一性复核（预期只命中 free/20 交付物）
rg -n "g24_citie" ..\..\
```

浏览器人工验收不可由 `node --check` 代替：至少完成 6 关、一次失败/重开、一次永久结团、一次吸斥切换、一次图鉴/工坊刷新恢复。正式清单共 7 个 placement；DEMO 在 `?qa=1` 下覆盖 `rv_replay / rv_energy / rv_double / rv_hint / rv_relic` 5 个并逐一验收免费出口，`rv_streak / rv_repair` 只在 UI 与正式接入方案中验收。

---

## 11. 发布判定

| 阶段 | 必须通过 | 未通过处理 |
|---|---|---|
| 灰盒 GO | 10 秒理解 ≥85%、物理解释率 ≥80%、L1 完成 ≥90%、首败重开 ≥35% | 重做核心可读性，不加 Meta |
| 留存 GO | 无广告版本中国 IAA 活跃次留 ≥40% | 调关卡/反馈；连续两轮 <35% 淘汰 |
| IAA GO | 主动激励 ≥4/DAU 且 D1/D7 无显著损伤 | 回滚最近开放的广告位 |
| 技术 GO | L1 双设备、L2 幂等/回放、防沉迷、崩溃/性能全过 | 不上线、不以 DEMO 替代 |
| 商业 GO | 7 日 ROI ≥1.0，投诉/误触/退款在阈值内 | 降密度或停止买量 |

最终原则：**可预测性优先于“热闹”，留存优先于广告密度，合规优先于上线日期。**
