Explore(地图与探索)
Explore 提供数据驱动的地图与调查闭环。逻辑地图只引用稳定的表现 ID;背景、图标和点击区域由另一张表现表管理,资源包可以替换资源而不改变地图逻辑。
接口
契约版本为 explore@1。实现位于 src/core/explore/。
| 名称 | 类型 | policy | 用途 |
|---|---|---|---|
map:logic | 服务 | trusted | 打开、关闭地图,读取当前位置,触发热区与查询路径 |
explore:items | 服务 | trusted | 查询、授予、使用和重置本次会话的库存 |
explore:resolve-hotspot | 机制插槽 | trusted | 求值热区条件或改写动作 |
explore:resolve-trigger | 机制插槽 | trusted | 改写道具实际执行的动作 |
explore:on-hotspot-enter | 通知 | — | 实际执行热区动作后广播 |
explore:on-item-use | 通知 | — | 实际执行道具动作并完成扣减后广播 |
explore:pathfinder@1 | 实现位(single) | public | 提供寻路实现;默认 builtin,卸载回落 |
公共 registry 为 explore:maps、explore:presentations、explore:items。其中道具 registry 与库存服务同名,但只有一个服务入口:装载器仍按分层规则写入条目,调用方使用库存方法,不能绕过装载授权直接挂层。
逻辑地图与表现分离
explore:maps 的条目形状如下。presentation 是 explore:presentations 的稳定条目 ID,不能填写 VPath。
interface MapData {
start: string
nodes: Record<string, {
presentation: string
hotspots?: {
id: string
when?: Condition
action: ExploreAction
}[]
}>
}start 必须指向已有节点。同一节点的热区 ID 不重复,move 的目标必须存在。条件沿用剧本 IR 的 Condition,与剧情共用纯求值器,不通过插件之间的直接 import 共享实现。
explore:presentations 管理资源与几何:
interface PresentationData {
background?: string // 图片 VPath
icon?: string // 图片 VPath
hotspots?: Record<string, {
x: number
y: number
width: number
height: number
label?: IrText
}>
}每个表现条目至少声明背景、图标或热区。几何是逻辑画面的相对值:x、y 为左上点,width、height 必须为正数,整个区域不得越出 0–1 范围。地图节点打开或移动到时,必须有背景和本次逻辑热区所需的几何;缺失时明确报错,不换用其他资源,也不生成不可见按钮。
道具条目 { name, description?, icon?, consumable?, action? } 放进 explore:items。其中 icon 是表现条目的稳定 ID,界面再读取该条目的图片 VPath;只含图标的条目不需要地图背景。
热区动作与地图服务
四种动作共用一个联合类型:
type ExploreAction =
| { type: 'move'; node: string }
| { type: 'flags'; ops: FlagOp[] }
| { type: 'script'; chapter: string }
| { type: 'inspect'; text: IrText }map:logic 的同步接口如下:
| 方法 | 行为 |
|---|---|
open(map, at?) | 打开地图;at 缺省为 start。重复打开明确替换当前地图,不保存旧节点 |
close() | 关闭当前地图,不清除库存 |
snapshot() | 返回 { map, node } 的独立副本;没有地图时返回 undefined |
activate(hotspot) | 只接受当前节点声明的热区,经机制解析后执行;返回 { action, applied } |
path(to) | 返回含起终点的节点路径;当前节点为单元素,无路径为空数组 |
热区条件不满足时,applied 为 false,不执行动作也不广播。flags 逐项经 state:flags 提交并服从所有权;inspect 把文本交给地图界面;script 由界面经导航打开 core:story { chapter },逻辑服务不直接操作剧情舞台。
内置寻路按节点和热区的声明顺序,沿 move 拓扑做广度优先搜索,不使用随机数。插件可以登记新的寻路实现;按既有规则显式选择后生效,卸载所选实现会回落到 builtin。
会话内道具
explore:items 的库存接口如下:
list(): { item: string; count: number }[]
grant(item: string, count?: number): number
use(item: string): ExploreAction
reset(): voidgrant要求已存在的道具条目和正整数数量,返回授予后的总数。- 没有
action的道具产生说明性inspect;优先使用description,未提供时使用name。 use先经explore:resolve-trigger解析动作。无库存、机制异常、动作失败、条件拒绝或中止都不会扣库存。- 只有
consumable: true的道具在成功后扣一件,同一次调用只扣一次;其他道具不减少。 list返回独立数组;reset只清本次会话库存,不写存档。
两个机制都必须返回完整合法的动作。机制抛错发生在动作执行前,不改地图、库存或旗标;成功后的通知分别携带 { map, node, hotspot, action } 与 { item, remaining, action }。
地图界面与剧情入口
内置 core:explore 经 ui:screen@1 登记。独立 Stage 场景负责当前背景,纹理通过 Asset 取用;DOM 热区是带可读标签的真实按钮,支持焦点与键盘操作。道具图标也经 Asset 取得,并在撤下时释放显示地址。文本优先经现有国际化服务解析。
剧情界面压在地图之上时,地图只隐藏、不撤下,保留节点与库存;返回后可以继续操作。界面撤下会释放独立场景和资源,迟到的图标或文本结果不会写回旧界面。
剧本用以下指令进入地图:
{ "op": "explore:map", "args": { "map": "demo:map", "at": "entry" } }at 可省略。指令等待地图退栈,不写额外结果旗标;中止会取消等待、关闭地图并退回原剧情。普通退栈后,原剧本从下一条指令继续。
会话边界与验证
当前地图、节点和库存不自动写入 Save,也没有跨进程恢复协议。这个范围只说明本期没有地图快照格式,不表示玩家的剧情旗标会被永久丢弃,更不提前规定未来的恢复行为。若需要恢复地图与库存,须另登记版本化快照、缺失条目和缺失资源的处理规则。
测试文件:
test/unit/explore-data.test.ts:数据校验、稳定 ID 与默认路径顺序。test/contract/explore.test.ts:真实装载、地图与道具会话、条件、机制、通知、同名 registry 授权及卸载回落。test/contract/explore-command.test.ts:真实 VM 与导航的地图 → 剧情 → 地图 → 原剧本,以及中止清理。test/ui/explore-screen.test.ts:真实 DOM、键盘、资源纹理、库存显示、剧情往返与迟到回调清理。
当前范围不包括奖励、活动日程、地图存档、跨进程恢复或永久库存协议。完整回归 1228 项通过、80 项按环境跳过;Windows WebView2 与 MuMu Android 15/WebView 110 的共用契约各 432 项通过、2 项按条件跳过,Explore 的两份共用契约均通过。真实图片纹理由浏览器界面用例核验。
当前状态:已完成(本页所述地图、道具与会话内导航范围)。这不代表 Phase 3 整体、奖励或持久续玩已经完成。平台边界见当前状态与测试。