npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@overworld-engine/devtools

v1.5.0

Published

Dev-time content validation (dialogues/quests) and event-bus logging for Overworld games

Downloads

769

Readme

@overworld-engine/devtools

开发期工具:内容静态校验(对话 / 任务 / 物品 / 成就)、事件总线日志事件总线剖析。 所有校验器都是纯函数,只返回问题列表、从不抛出;assertValidContent 供 开发启动脚本与测试在存在 error 时快速失败。

零交叉依赖:结构化输入类型

devtools 校验的是 dialogue / quest / inventory / achievements 包的内容数据, 但不 import 这些包——唯一的运行时依赖是 @overworld-engine/core。输入类型 (DialogueTreeLikeQuestLikeItemLikeAchievementLike)是在本包内 定义的结构化子集(duck typing):TypeScript 是结构化类型系统,真实的 DialogueTree / QuestDefinition / ItemDefinition / AchievementDefinition 可以直接原样传入,无需转换。

校验 API

import { assertValidContent, formatReport, validateContent } from '@overworld-engine/devtools'

const report = validateContent(
  { dialogues, quests, items, achievements },        // 每个分区都可选
  {
    effectTypes: effects.types(),                     // 注册表已知类型,提供才检查
    conditionTypes: conditions.types(),
    knownEvents: ['player:moved', 'item:added'],      // 提供才检查 trigger.event
    questStartEffectType: 'quest.start',              // 跨分区检查用,默认 'quest.start'
  }
)
console.log(formatReport(report))                     // 人类可读的多行摘要
if (import.meta.env.DEV) assertValidContent({ dialogues, quests }) // 有 error 即抛出

ValidationReport = { issues, errors, warnings, ok },ok 表示没有 error (warning 不影响)。每条 ValidationIssueseveritysource (如 dialogue:guide-intro)、path(如 nodes.hello.responses.ask.next)与 message

分区校验器也可单独使用:validateDialogues / validateQuests / validateItems / validateAchievements

规则一览

| 分区 | error | warning | | --- | --- | --- | | 对话 | 树/节点 id 重复;startNodeId 缺失;next / response.next 指向不存在的节点 | 从 startNodeId 不可达的节点;空 responses: [];endsDialogue 节点上永不生效的 next;未注册的 effect/condition 类型 | | 任务 | 任务/目标 id 重复;零目标;target < 1;前置/chainNext 指向未知任务;前置循环(A 依赖 B 依赖 A) | 未注册的奖励/前置条件类型;trigger.event 不在已知事件表;autoStart 同时又是别人的 chainNext(双重启动);从未被内容启动的任务 | | 物品 | id 重复 | 未注册的 useEffects 类型;maxStack < 1 | | 成就 | id 重复;trigger.count < 1 | 缺失 trigger 字段;未注册的奖励类型;trigger.event 不在已知事件表 |

跨分区(validateContent 独有):对话中 quest.start 类效果的 params.questId 若缺失或不是已知任务 id → error;同时,被这类效果启动的 任务不再报"从未被启动"的 warning。

终止节点语义(与对话引擎一致)

已对照 @overworld-engine/dialogue 引擎源码确认:没有 responses 也没有 next 的节点就是合法的终止节点——advance() 会在此结束对话并计为完成,与 endsDialogue: true 等价,因此校验器不会对它报任何问题。相反, endsDialogue 节点上的 next 永远不会被走到(advance() 先判 endsDialogue),会得到 warning,且不算作可达性的边。

JSON Schema(外部内容文件)

面向把内容写成 .json 文件的游戏,devtools 导出一组手写的 JSON Schema(draft 2020-12),描述真实的内容类型:DialogueTree / QuestDefinition / ItemDefinition / AchievementDefinitionEffectRef / ConditionRef

  • 单对象:dialogueTreeSchemaquestDefinitionSchemaitemDefinitionSchemaachievementDefinitionSchemaeffectRefSchemaconditionRefSchema
  • 数组(整份内容文件的形状,$id-list 后缀):dialogueTreesSchemaquestDefinitionsSchemaitemDefinitionsSchemaachievementDefinitionsSchema
  • contentBundleSchema:{ dialogues?, quests?, items?, achievements? } 整包(与 validateContent 的输入同形)
  • allContentSchemas:按名字迭代所有 schema;schemaFor('quests') 等 直接取某一分区的数组 schema

每个 schema 都自包含(共享形状内嵌在各自的 $defs 里)、带唯一 $id(如 https://overworld.dev/schemas/dialogue-tree.json),可以单独交给 任何校验器或编辑器。

与 ajv 一起使用

devtools 不依赖 ajv——schema 只是普通数据。在你自己的构建/CI 里装 ajv(draft 2020-12 要从 ajv/dist/2020 导入):

import Ajv from 'ajv/dist/2020'
import { schemaFor } from '@overworld-engine/devtools'
import { readFileSync } from 'node:fs'

const ajv = new Ajv({ allErrors: true })
const validate = ajv.compile(schemaFor('quests'))
const quests = JSON.parse(readFileSync('content/quests.json', 'utf8'))
if (!validate(quests)) console.error(validate.errors)

编辑器提示($schema)

把 schema 写到磁盘后,在内容 JSON 顶部加 $schema 即可获得编辑器内的 自动补全与即时校验(VS Code 原生支持;additionalProperties: true,多出的 $schema 字段不会校验失败):

// content/quests.json 若是单个对象;数组文件请在 VS Code 的
// json.schemas 设置里把文件模式映射到对应的 *-list schema
{ "$schema": "./schemas/quest-definition.json", "id": "welcome", ... }

保真度说明

  • additionalProperties: true:TypeScript 接口是结构化开放的,内容类型 本来就允许游戏扩展额外字段(如 ItemDefinition.metadata),schema 不会 拒绝它们。
  • number 而非 integer:target / count / maxStack 在 TS 里是 number,校验器也只检查 >= 1,schema 与之一致。
  • minimum: 1 只加在校验器判为 error 的地方:任务目标 target 与成就 trigger.count(maxStack < 1 只是 warning,schema 不硬性限制)。
  • 成就的 trigger 为必填,取值 AchievementTrigger | null (oneOf: [trigger, null]),与真实 AchievementDefinition 一致—— null 显式表示仅手动解锁。
  • 跨对象规则(id 唯一、next 悬空、前置循环……)超出 JSON Schema 能力, 解析后请继续用 validateContent 检查。

事件日志

import { bindEventLogger, createEventRecorder } from '@overworld-engine/devtools'
import { gameEvents } from '@overworld-engine/core'

// 开发期把总线上的所有事件打到控制台(基于 bus.onAny)
const unbind = bindEventLogger(gameEvents, {
  filter: (event) => event.startsWith('quest:'),  // 可选
  includePayload: true,                            // 默认 true
  log: (line, payload) => console.debug(line, payload), // 默认 console.debug,前缀 [overworld]
})
unbind()

// 测试断言:录制事件序列
const recorder = createEventRecorder(bus)
bus.emit('quest:started', { questId: 'welcome' })
recorder.events // [{ event: 'quest:started', payload: {...}, at: 0 }]
recorder.stop()

注意:RecordedEvent.at单调递增的序号(0、1、2…),不是 Date.now() 时间戳——这样在假时钟、同毫秒多次 emit 或任何运行环境下, 顺序断言都是确定的。

事件总线剖析(profileBus)

profileBus(bus, options?) 通过包裹 bus.emit(monkey-patch,stop() 时还原) 统计每个事件的发射次数与同步分发耗时,每事件累计 { count, totalMs, maxMs, lastMs }(EventStats):

import { profileBus } from '@overworld-engine/devtools'
import { gameEvents } from '@overworld-engine/core'

const profiler = profileBus(gameEvents)
// ... 跑一段游戏 ...
console.log(profiler.report())   // 按 totalMs 倒序的对齐文本表格
profiler.top(3)                  // 最贵的 3 个事件(默认按 totalMs,可选 'count')
profiler.reset()                 // 清空统计,继续剖析
profiler.stop()                  // 还原原始 emit(幂等)
  • options.now 可注入时钟(默认 performance.now),测试里注入假时钟即可 得到确定性的耗时断言;监听器里发起的异步工作不计入耗时。
  • 重复剖析同一总线会链式包裹(打 warning,两者都正常统计);此时 stop() 必须按 LIFO 顺序调用(后启动的先 stop),乱序 stop 会把过期的 emit 还原回去,叠在上面的 profiler 会悄悄失效。
  • bindEventLogger / createEventRecorder 互不干扰:它们走 bus.onAny, 剖析包裹的是 emit 本身。