Overworld
包参考

@overworld-engine/quest

无头、事件驱动的任务状态机:目标进度由事件总线自动推进

无头(headless)、事件驱动的任务状态机。任务定义以数据注入,目标进度由事件总线 自动推进,奖励与前置条件通过 @overworld-engine/core 的效果/条件注册表解析 —— 引擎不 import 任何玩法系统。

定位

在没有解耦机制的实现里,任务系统往往直接 import 各个玩法 store,并为每种玩法 写死目标类型。本包用两个模式取代全部耦合:

  • 进度 = 事件订阅:目标声明 trigger: { event, filter?, amountFrom? }, 引擎只订阅活跃任务实际需要的事件;玩法系统只管 emit。
  • 奖励/前置 = 注册表引用:奖励写 { type: 'wallet.addGold', params: {...} }, 前置条件写 ConditionRef,由游戏在启动时注册对应处理器。

数据 Schema

interface QuestDefinition {
  id: string
  category?: string            // 自由分组标签
  title?: string               // 展示文本,可存 i18n key
  description?: string
  prerequisites?: {
    quests?: string[]          // 必须先完成的任务 id
    conditions?: ConditionRef[] // 经条件注册表求值(AND 语义)
  }
  objectives: ObjectiveDefinition[]
  rewards?: EffectRef[]        // 完成时经效果注册表执行
  autoStart?: boolean          // 注册/初始化时自动开始(前置通过才生效)
  chainNext?: string[]         // 完成后自动开始的后续任务
}

interface ObjectiveDefinition {
  id: string
  description?: string
  target: number               // 达到该进度即完成
  trigger?: {
    event: string              // 事件名(支持游戏扩展的事件表)
    filter?: Record<string, unknown> // payload 字段浅相等匹配,全部命中才计数
    amountFrom?: string        // 从 payload 取数值累加(如 'distance');省略则每次 +1
  }
  hidden?: boolean             // UI 提示:目标先隐藏
}

快速上手

import { createConditionRegistry, createEffectRegistry, gameEvents } from '@overworld-engine/core'
import { createQuestEngine } from '@overworld-engine/quest'

const conditions = createConditionRegistry<GameCtx>()
const effects = createEffectRegistry<GameCtx>()
conditions.register('minLevel', (params, ctx) => ctx.player.level >= (params.level as number))
effects.register('wallet.addGold', (params, ctx) => ctx.wallet.add(params.amount as number))

const quests = createQuestEngine({
  quests: [
    {
      id: 'walk-the-city',
      objectives: [
        { id: 'distance', target: 20, trigger: { event: 'player:moved', amountFrom: 'distance' } },
        { id: 'talk', target: 1, trigger: { event: 'dialogue:ended', filter: { npcId: 'guide' } } },
      ],
      rewards: [{ type: 'wallet.addGold', params: { amount: 100 } }],
      chainNext: ['visit-market'],
    },
  ],
  conditions,
  effects,
  context: () => gameContext,   // 传给条件/效果处理器,支持惰性求值
  events: gameEvents,           // 默认即全局总线,可换成自建 EventBus
  // persist: 省略/false=不持久化;true=默认配置;或 { name?, version?, storage? }
})

quests.startQuest('walk-the-city')
// 玩家移动系统只需:gameEvents.emit('player:moved', { position, distance })

API

成员说明
store底层 zustand vanilla store(StoreApi<QuestEngineState>),可直接 subscribe,React 里配合 useStore
getState()当前状态快照(等价于 store.getState())
registerQuests(...quests)运行期增量注册;autoStart 任务满足前置即自动开始
startQuest(id)校验前置(已完成集合 + 条件注册表),emit quest:started;未知 id 警告并返回 false
reportProgress(questId, objectiveId, amount?)手动推进(默认 +1),适合无事件可订阅的目标;非活跃任务被忽略,进度在 target 处截断
completeQuest(id)全部目标完成时自动触发;发奖励 → emit quest:completed → 自动开始满足前置的 chainNext
canStartQuest(id) / getAvailableQuests()前置判定 / 当前可开始的任务定义
isActive(id) / isCompleted(id)状态查询
resubscribe()重挂活跃任务的触发器订阅(常规场景自动处理)
dispose()解除全部总线订阅,停止自动推进(resubscribe() 可恢复)

状态字段(active 活跃任务(含每目标进度)、completed 已完成 id 列表、definitions) 通过 engine.getState() 快照读取,或订阅 engine.store。React 里用 zustand 的 useStore:

import { useStore } from 'zustand'

const active = useStore(quests.store, (s) => s.active)
const completed = useStore(quests.store, (s) => s.completed)

与事件总线的交互

  • 订阅:引擎按"活跃任务中未完成目标的 trigger.event 去重集合"动态挂载监听, 任务完成或目标达成后自动解除,不留悬挂监听。
  • 发布:quest:startedquest:objective-progress(含 current/target)、 quest:objective-completedquest:completed。HUD、音效、成就系统订阅即可, 无需引用本包。

持久化

显式开启(persist: true 或配置对象),经 persistOptions 写入 overworld:quest (可自定义 name/version/storage);省略或 false 则不持久化。 只持久化 active(含每目标进度)与 completed;任务定义属于内容,永不落盘。 重新水合后引擎会自动为恢复的活跃任务重挂触发器订阅(同步与异步 storage 均已处理)。

本页目录