Files
HEROLL/HEROLL_RULES_v0.2.8/01-关卡与棋盘/2026-08-31_逆向-HEROLL关卡楼层分布与刷新规则-report.md
T

253 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# HEROLL 2.0.1 关卡楼层分布与刷新规则逆向报告
> 仅限本机内部玩法验证。本文描述 HEROLL 2.0.1 的逆向结果,不授予原作代码、配置或美术资源的发行权。
## 1. 结论摘要
此前“关卡楼层分布和刷新规则”的任务没有形成独立交付物;本报告补齐该任务,并覆盖旧 GDD 中“每层固定注入 2 个有益地块”等未经闭环的描述。
### 1.1 两套关卡终局规则
普通主线共有 52 个启用关卡,分成两类:
| 规则 | 关卡数 | 配置值 | 实际含义 |
|---|---:|---:|---|
| `Default` | 33 | 10 / 13 / 15 | 固定楼层数;与 `board.csv` 对应关卡的行数完全一致 |
| `DiceGauge` | 19 | 50 | 每次合格的完整落地结算向实时 Boss Gauge 增加 50;不是“50 层”也不是直接阈值 |
`Default` 关卡的配置分布是:2 个 10 层关、3 个 13 层关、28 个 15 层关。所有 33 个关卡都满足:
```text
bossSpawnRuleValue == board.csv 中该 Normal Stage 的楼层行数
loop 索引范围 == 0 .. bossSpawnRuleValue - 1
```
因此第 1 关的权威规则是:`stage=0`、`Default=10`、`loop=0..9`,完成第 10 层后进入 `boss_0`。`board.csv` 的每一行是对应楼层的敌方数值模板,不是地块布局表。
### 1.2 棋盘不是逐层重建,而是持续变异
玩家通过起点时进入下一层。换层状态机执行“增加新的有益地块和敌人”,而不是重新生成整张棋盘。官方商店页也明确描述为:每次经过起点,新的敌人与事件会被加入不断变化的棋盘。
核心调用顺序为:
```text
经过起点 / 完成一圈
→ AddNextLoopTiles(loopIndex)
→ AddRandomGoodTiles(loopIndex + 2)
→ 等待表现完成
→ AddRandomEnemyTile(enemyTilesPerLoop, false)
→ 等待新增敌人及地块表现完成
```
注意:`loopIndex + 2` 是“有益地块元数据抽取/安装尝试次数”,不是无条件保证最终新增同样数量的可见地块。候选位置不足、地块保护规则或安装失败均可能使最终可见数量较少。
新局初始化是独立路径:
```text
SpawnInitalTiles
→ 执行初始地块生成(内部调用传入常量 3,但被调用方法的参数名未恢复)
InitNewGame
→ SpawnInitalTiles
→ SpawnAllCornerTiles
→ AddRandomGoodTiles(8)
```
`enemyTilesPerLoop` 是 `BoardController` 的序列化字段;本次 APK 表和已导出的场景资料没有恢复它的原始序列化数值。初始状态机中的常量 3 也不能在缺少方法参数名时直接等同为“3 个敌人”。因此可以准确实现调用结构,但不能把 3 或其他猜测值写成原作每层敌人数常量。
### 1.3 玩家有益地块不是固定事件池
`AddRandomGoodTiles(N)` 从玩家当前可用/装备的 `OutGameTile` 元数据牌组取 N 次,而不是直接从 `newtile.csv` 的全部 `GoodEvent` 等权抽取。
牌组规则已经从 `GetNextMetadata` 闭合:
1. 缓存 `_shuffledDeck` 与 `_currentDeckIndex`。
2. 牌组为空或索引到尾时,以当前玩家可用的局外地块列表重建并洗牌,索引归零。
3. 每次返回当前索引的一个元数据,然后索引加一。
4. 因而同一轮牌组内不重复;请求次数超过牌组大小时会重新洗牌,跨轮可能再次抽到同类地块。
5. 抽到的 `OutGameTile.refNewTileId` 再映射到 `NewTile`,播放 `PlayGoodFX` 并调用 `AddTileActEvent` 安装。
新账号有 8 个 `defaultEquip=True` 的稀有地块,刚好对应初始化的 8 次抽取:铁匠铺、恢复、传奇装备、英雄体、宝箱、抓娃娃、泡泡龙和 Spito。完整表见 `tables/default-good-tile-deck.csv`。
这意味着第 1 层初始化时,在牌组和位置均有效的前提下,默认 8 张会各抽到一次。后续层的抽取次数随传入的零基 `loopIndex` 增长。若 UI 楼层 `F` 以 1 开始,常规换层调用对应 `loopIndex=F-1`,则进入该层时的尝试数为 `F+1`;第 1 层仍走独立的固定 8 次初始化路径。
### 1.4 自然生成地块的资格过滤
`newtile.csv` 的关键字段含义由表头与运行时消费共同确认:
| 字段 | 规则 |
|---|---|
| `isEnabled` | 是否允许进入自然生成候选 |
| `minStage` | 最低主线 Stage |
| `minLoop` | 最低零基 Loop(楼层门槛) |
| `ETileTypeV2` | Empty / NormalEnemy / GoodEvent / BadEvent / BossAttack / Corner 等分类 |
| `maxCornerPerGame` | 单局同类角落地块上限;`-1` 表示该字段不设限 |
Stage 0 的自然候选在 `loop=0` 已包含 4 类普通敌人、6 类 GoodEvent、3 类 BadEvent、5 类 Corner、祝福、空地等;`loop=3` 解锁黑市、传送、彩票、21 点、Power Star、Dice Machine、泡泡龙,`loop=5` 解锁 Dice Catapult。`GoodEventGoldCarriage` 与 `GoodEventSpito` 还要求 `minStage=1`,因此不应在第 1 关自然生成。
这里必须区分两条入口:
- 自然生成:受 `newtile.isEnabled/minStage/minLoop` 过滤。
- 玩家有益地块注入:以当前 `OutGameTile` 牌组及 `refNewTileId` 为源,再由安装位置保护规则决定能否落盘。
完整资格表见 `tables/enabled-tile-eligibility.csv`。
### 1.5 商店刷新
起点是换层触发点。公开玩法资料明确记录“商店每 3 层在起点出现”,与原作实机和既有 GDD 的第 3、6、9 层观察一致;原生 `BoardController` 同时存在 `shopPerLoop` 字段和换层状态机中的商店分支。
玩法验证实现应采用:
```text
displayFloor % 3 == 0 → 起点显示商店
```
商店不属于每层随机地块抽取;它依附起点,并由层间隔控制。商店内部商品刷新、费用和购买规则属于单独的商店逆向报告。
## 2. Boss 压力刷新
### 2.1 `Default` 关卡
`Default` 不使用实时压力 Gauge。以 `stageinfo.bossSpawnRuleValue` 作为总楼层数,读取 `board.csv` 的 `loop=0..N-1` 数值;最后一层完成后进入关卡 Boss。第 1 关为 10 层后 `boss_0`。
存在 `midBossSpawnValues` 时,`Default` 关卡用楼层进度注册中 Boss。配置值 7 与楼层中点一致,但原生比较中的零/一基显示转换没有恢复出托管变量名;因此只能把“7”作为原始门槛配置,验收时必须以原作录像校准 UI 层号和触发帧,不能直接断言是内部 `loop=7` 或用户可见“第 7 层”。
### 2.2 `DiceGauge` 关卡
每次合格的落地结算执行:
```ts
runtimeBossGauge = clamp(
runtimeBossGauge + stageInfo.bossSpawnRuleValue,
0,
100,
);
```
当前 19 个 `DiceGauge` 关卡的增量都为 50,所以正常路径两次完整掷骰/落地结算蓄满 100。`MoveTile` 随后依次检查 `TrySpawnBossTilesByGauge` 与 `TryRegisterMidBossByGaugeAsync`。
结合原生数据流和项目中已验证的原作实机观察,完整周期为:
```text
Gauge 0
→ 第 1 次完整落地:50
→ 若配置 midBossSpawnValues=20,中 Boss Gauge 注册路径已具备数值条件(强推断)
→ 第 2 次完整落地:100
→ 进入 Boss 压力倒计时
→ 后续 4 次玩家掷骰:4 → 3 → 2 → 1 → 0
→ 随机转换 4 个互不重复、非祝福地块为 Boss 压力地块
→ 实时 Gauge 清零,进入下一周期
```
`BoardController.BossDiceCounter` 是倒计时显示字段,不是实时 Gauge。`AddBossDiceNumber(v)` 只做 `counter += v`,`HandleBossDiceRolled(v)` 只做 `counter -= v`;两者不得与 `runtimeBossGauge` 合并成同一个变量。`midBossSpawnValues=20` 被 `TryRegisterMidBossByGaugeAsync` 消费的总体语义可信,但被混淆的共享谓词没有恢复成可读比较式,所以精确的 `>=20` 运算符仍标为强推断。
原生证据能确认:Gauge 加 50、上下限 0/100、移动后进入 Boss 生成链、保存被替换前地块、生成后同一 Gauge 字段清零。4 次倒计时、4 个唯一非祝福地块来自原作运行时观察。以下边界仍未从静态代码恢复成可靠托管语义:
- 合格候选不足 4 个时的降级顺序;
- 除祝福外是否还存在模式专属保护地块;
- 被 Boss 压力覆盖的地块在攻击结束后是原样恢复还是按事件结果恢复。
玩法验证版应把这些边界做成可配置策略并记录 seed/候选列表;在获得对应录像前不得声称是原作精确规则。
## 3. 第 1 关开发用规则
| 项目 | 原作规则 | 来源/置信度 |
|---|---|---|
| Stage | `0`,Grass,Normal | 源码确认 / 高 |
| 楼层 | 10 层,内部 `loop=0..9` | `stageinfo` 与 `board` 完全闭合 / 高 |
| 最终 Boss | `boss_0`,`skill_0`,攻击冷却 7 | 源码确认 / 高 |
| 敌人数值 | 严格逐行读取 `stage0-loop-enemy-stats.csv` | 源码确认 / 高 |
| 初始角落 | `SpawnAllCornerTiles` | 原生调用确认 / 高 |
| 初始有益地块 | `AddRandomGoodTiles(8)` | 原生常量确认 / 高 |
| 默认有益牌组 | 8 个默认装备的 OutGameTile | 配置确认 / 高 |
| 换层有益注入 | `AddRandomGoodTiles(loopIndex+2)` | 原生算术确认 / 高 |
| 换层敌人注入 | `AddRandomEnemyTile(enemyTilesPerLoop,false)` | 原生调用确认;数量值未恢复 / 中高 |
| 商店 | 起点每 3 个显示楼层出现 | 实机/社区交叉验证 + 原生字段 / 中高 |
| Boss 压力 Gauge | 第 1 关不启用 | `Default` 配置 / 高 |
| 通关 | 完成第 10 层后进入 `boss_0` | 配置与楼层表闭合 / 高 |
第 1 关 MVP 不应继续使用以下旧假设:
- 每层固定新增 2 个有益地块;
- 六类事件每局强制各出现一次;
- 全部 GoodEvent 从固定池等权抽取;
- 第 1 关使用两掷蓄压、四掷 Boss 压力循环。
## 4. 实现伪代码
```ts
function enterNewRun(stage: StageInfo, playerDeck: OutGameTileMeta[]) {
loopIndex = 0;
spawnInitialTiles(); // 内部常量 3 的参数语义仍需动态闭环
spawnAllCornerTiles();
addRandomGoodTiles(8, playerDeck);
}
async function passStartTile() {
loopIndex += 1;
if (isDefaultStage && loopIndex >= stage.bossSpawnRuleValue) {
enterFinalBoss(stage.bossID);
return;
}
await addRandomGoodTiles(loopIndex + 2, currentPlayerGoodTileDeck);
await addRandomEnemyTiles(enemyTilesPerLoop, false);
const displayFloor = loopIndex + 1;
if (displayFloor % shopPerLoop === 0) {
showShopOnStartTile();
}
}
function drawGoodTileMeta(): OutGameTileMeta | null {
if (eligiblePlayerDeck.length === 0) return null;
if (!shuffledDeck || currentDeckIndex >= shuffledDeck.length) {
shuffledDeck = shuffle(copy(eligiblePlayerDeck));
currentDeckIndex = 0;
}
return shuffledDeck[currentDeckIndex++];
}
function onQualifiedTileSettlement() {
if (stage.bossSpawnRule !== 'DiceGauge') return;
runtimeBossGauge = clamp(
runtimeBossGauge + stage.bossSpawnRuleValue,
0,
100,
);
tryRegisterMidBossByGauge();
trySpawnBossTilesByGauge();
}
```
## 5. 证据链
| Evidence | Finding | Design/Path |
|---|---|---|
| E-STAGE-CONFIG-001 | 两类 Boss 规则;Default 值与楼层行数完全一致;地块门槛和默认牌组 | F-STAGE-001, F-TILE-001 |
| E-STAGE-NATIVE-001 | 初始化 8 次、换层 `loop+2`、敌人字段注入、洗牌无放回、Gauge 加法/清零 | F-REFRESH-001, F-GAUGE-001 |
| E-STAGE-RUNTIME-001 | DiceGauge 的 2 次蓄压、4 次倒计时、4 个非祝福覆盖 | F-GAUGE-002 |
| E-STAGE-WEB-001 | 经过起点换层、每 3 层商店、每圈新增敌人与事件 | F-REFRESH-002 |
本地证据为主,互联网资料仅用于交叉验证玩家可观察语义;其中 Steam 页面属于同系列后续公开描述,不能替代 2.0.1 本地证据。外部参考:[Heroll Wiki Tiles](https://heroll.fandom.com/wiki/Tiles)、[Steam《HEROLL: IF》页面](https://store.steampowered.com/app/4838670/)。
## 6. 配套表格
- `tables/stage-boss-rules.csv`:52 个普通主线关卡的 Boss / 中 Boss 配置。
- `tables/stage0-loop-enemy-stats.csv`:第 1 关 10 层敌方数值。
- `tables/enabled-tile-eligibility.csv`:启用地块的 Stage / Loop 门槛与角落上限。
- `tables/default-good-tile-deck.csv`:新账号 8 个默认有益地块元数据。
## 7. 验收用例
1. 对全部 33 个 `Default` 关卡校验 `boardRows == bossSpawnRuleValue`。
2. 第 1 关只读取 `loop=0..9`;第 10 层后进入 `boss_0`,不得启用 DiceGauge。
3. 新局固定调用 8 次有益地块抽取;在默认 8 张牌组下,首轮抽取不重复。
4. 换层调用次数必须是 `loopIndex+2`,并记录“请求数 / 成功安装数 / 失败原因”。
5. 同一洗牌轮不重复;耗尽后重洗,固定 seed 下结果可复现。
6. `minStage/minLoop/isEnabled/maxCornerPerGame` 的自然生成过滤逐项测试。
7. DiceGauge 关卡两次合格落地达到 100,之后四次掷骰倒计时,生成 4 个唯一非祝福候选并清零。
8. Gauge 与 BossDiceCounter 使用两个字段,存档恢复时不得相互覆盖。
9. 商店仅依附起点,并按显示楼层 3 的倍数触发。
10. `enemyTilesPerLoop` 在恢复原作序列化值前标为外部配置,不得硬编码猜测值并标注“源码确认”。