13 KiB
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 个关卡都满足:
bossSpawnRuleValue == board.csv 中该 Normal Stage 的楼层行数
loop 索引范围 == 0 .. bossSpawnRuleValue - 1
因此第 1 关的权威规则是:stage=0、Default=10、loop=0..9,完成第 10 层后进入 boss_0。board.csv 的每一行是对应楼层的敌方数值模板,不是地块布局表。
1.2 棋盘不是逐层重建,而是持续变异
玩家通过起点时进入下一层。换层状态机执行“增加新的有益地块和敌人”,而不是重新生成整张棋盘。官方商店页也明确描述为:每次经过起点,新的敌人与事件会被加入不断变化的棋盘。
核心调用顺序为:
经过起点 / 完成一圈
→ AddNextLoopTiles(loopIndex)
→ AddRandomGoodTiles(loopIndex + 2)
→ 等待表现完成
→ AddRandomEnemyTile(enemyTilesPerLoop, false)
→ 等待新增敌人及地块表现完成
注意:loopIndex + 2 是“有益地块元数据抽取/安装尝试次数”,不是无条件保证最终新增同样数量的可见地块。候选位置不足、地块保护规则或安装失败均可能使最终可见数量较少。
新局初始化是独立路径:
SpawnInitalTiles
→ 执行初始地块生成(内部调用传入常量 3,但被调用方法的参数名未恢复)
InitNewGame
→ SpawnInitalTiles
→ SpawnAllCornerTiles
→ AddRandomGoodTiles(8)
enemyTilesPerLoop 是 BoardController 的序列化字段;本次 APK 表和已导出的场景资料没有恢复它的原始序列化数值。初始状态机中的常量 3 也不能在缺少方法参数名时直接等同为“3 个敌人”。因此可以准确实现调用结构,但不能把 3 或其他猜测值写成原作每层敌人数常量。
1.3 玩家有益地块不是固定事件池
AddRandomGoodTiles(N) 从玩家当前可用/装备的 OutGameTile 元数据牌组取 N 次,而不是直接从 newtile.csv 的全部 GoodEvent 等权抽取。
牌组规则已经从 GetNextMetadata 闭合:
- 缓存
_shuffledDeck与_currentDeckIndex。 - 牌组为空或索引到尾时,以当前玩家可用的局外地块列表重建并洗牌,索引归零。
- 每次返回当前索引的一个元数据,然后索引加一。
- 因而同一轮牌组内不重复;请求次数超过牌组大小时会重新洗牌,跨轮可能再次抽到同类地块。
- 抽到的
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 字段和换层状态机中的商店分支。
玩法验证实现应采用:
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 关卡
每次合格的落地结算执行:
runtimeBossGauge = clamp(
runtimeBossGauge + stageInfo.bossSpawnRuleValue,
0,
100,
);
当前 19 个 DiceGauge 关卡的增量都为 50,所以正常路径两次完整掷骰/落地结算蓄满 100。MoveTile 随后依次检查 TrySpawnBossTilesByGauge 与 TryRegisterMidBossByGaugeAsync。
结合原生数据流和项目中已验证的原作实机观察,完整周期为:
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. 实现伪代码
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、Steam《HEROLL: IF》页面。
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. 验收用例
- 对全部 33 个
Default关卡校验boardRows == bossSpawnRuleValue。 - 第 1 关只读取
loop=0..9;第 10 层后进入boss_0,不得启用 DiceGauge。 - 新局固定调用 8 次有益地块抽取;在默认 8 张牌组下,首轮抽取不重复。
- 换层调用次数必须是
loopIndex+2,并记录“请求数 / 成功安装数 / 失败原因”。 - 同一洗牌轮不重复;耗尽后重洗,固定 seed 下结果可复现。
minStage/minLoop/isEnabled/maxCornerPerGame的自然生成过滤逐项测试。- DiceGauge 关卡两次合格落地达到 100,之后四次掷骰倒计时,生成 4 个唯一非祝福候选并清零。
- Gauge 与 BossDiceCounter 使用两个字段,存档恢复时不得相互覆盖。
- 商店仅依附起点,并按显示楼层 3 的倍数触发。
enemyTilesPerLoop在恢复原作序列化值前标为外部配置,不得硬编码猜测值并标注“源码确认”。