平台通知

全屏浏览 VibeHub

全屏浏览,获得更沉浸的体验。你可以随时通过菜单退出全屏。

返回《dsv4.1f 鹈鹕自行车赛》

《dsv4.1f 鹈鹕自行车赛》创意工坊

选择 Mod,下次启动自动加载

收藏用于稍后查找;“开启”决定下次启动时加载哪些 Mod。所有已开启 Mod 会一起进入游戏。

启动游戏

推荐方式 · 不需要懂编程

复制任务给 Agent,让它发布、更新或删除

Prompt 只注入作品、能力、依赖和你的 Mod ID,并要求 Agent 实时读取唯一官方 Skill;固定命令与规则不再复制到任务文本中。

作者允许的 Mod 能力

发布 Mod 时必须声明使用哪些能力。以下内容是作者约定,平台不会扫描或限制 Mod 代码;只安装你信任的内容。

联机策略:游戏进入联机前应禁用全部 Mod

顶部提示条hud.toast

可以在屏幕上方的胶囊提示条里显示自定义文本(最长 80 字,可设置显示时长)。只能显示文字,不能插入 HTML、图片或按钮。

作者声明:只影响本地表现

HUD 徽章hud.badge

可以在左侧 HUD 增加一块文字徽章(自定义颜色与文本)。同一 id 重复调用是更新同一块徽章而不是新增,不能移动或覆盖游戏自带的计时、速度、耐久等面板。

作者声明:只影响本地表现

比赛流程事件event.lifecycle

可以监听游戏启动就绪、每场开跑、每完成一圈、自己完赛、返回主菜单这些流程事件,拿到圈数、用时、是否联机等数据。只能读取,不能修改比赛流程或结果。

作者声明:只影响本地表现

道具拾取事件event.pickup

可以在自己吃到道具时收到通知,拿到道具类型与拾取时的耐久、氮气数值。不能替玩家拾取道具,也不能修改道具布局或效果。

作者声明:只影响本地表现

每帧回调event.frame

可以注册每帧执行的回调并拿到帧间隔时间,用于自绘界面或统计。回调必须足够轻,不能阻塞主循环,也不允许自行开启额外的动画循环或定时器风暴。

作者声明:只影响本地表现

读取游戏状态state.read

可以只读地拿到当前场景、圈数、速度、耐久、氮气、房间号、玩家列表,以及本机玩家和对手的位置对象。只能读取快照,不能写入或伪造位置、圈数与完赛时间。

作者声明:只影响本地表现

读取赛道与 Three.jsscene.read

可以拿到赛道曲线、赛道长度、路面半宽、总圈数,以及游戏使用的 Three.js 命名空间,用于自绘轨迹或可视化。不能修改赛道几何、围栏或拾取物布局。

作者声明:只影响本地表现

单机速度倍率tuning.speed

可以在单机模式下调整最高速与加速度倍率(0.25~4 倍),用于做轻松或硬核难度。联机对局中该调用会被直接拒绝并返回失败,避免不同玩家物理参数不一致。

作者声明:联机时不应使用

本地存档storage.local

可以在按 Mod 命名空间隔离的浏览器 localStorage 里读写自己的数据,用于纪录、成就和设置。数据只存在本机浏览器,不能用来存储或传递账号、Token 等凭据。

作者声明:只影响本地表现

调试日志debug.log

可以向控制台输出带 [mod] 前缀的调试日志,方便开发排查。仅用于调试,不应输出玩家隐私数据。

作者声明:只影响本地表现

作者开发规范

Agent 会同时收到下面这份本游戏专属规范。

《鹈鹕自行车赛 · low-poly》创意工坊开发规范

项目 slug:low-poly  基础 Mod API 版本:1.0.0  文档版本:1.0.0

本文档是作者(游戏)与 Mod 创作者之间的开发约定,不是平台扫描规则。平台不会扫描或限制 Mod 代码, 所以请按本文档的边界来写:超出约定的用法不会被拦截,但会在后续版本里直接失效。


0. 游戏架构(先读这段)

  • 浏览器 WebGL 低多边形单车竞速:index.html(界面/样式)+ game.js(ES Module,全部游戏逻辑)+ Three.js 0.150.1。
  • 两种模式:单机(3 台 AI 对手)与联机(1 房最多 4 人,HTTP 轮询同步位置)。
  • 联机是实时位置同步:每位玩家上报自己的位置,服务端做道具拾取仲裁,客户端只做远端插值。
  • 启动顺序(index.html 里固定):加载官方 Loader → await import("./game.js")(此时 Mod API 已建好并挂到 window.MyGame)→ await beforeStart(注入启动前 Mod)→ start()(建场景、开主循环)→ markGameReady() → await afterStart(注入启动后 Mod)。
  • 因此:Mod API 在游戏主循环跑起来之前就可用,注册类调用请在 before-start 阶段完成。

1. 玩家可以制作哪些 Mod

只开放与上面这套架构真实匹配的能力:

方向可以做什么典型玩法
界面扩展顶部胶囊提示、HUD 徽章、圈速/排名信息面板圈速计时器、连击提示、完赛播报
观赛与记录监听开赛/每圈/完赛/拾取,读取只读状态本地战绩统计、路线复盘点
单机数值调整单机最高速与加速度倍率(0.25×~4×)轻松模式、硬核模式
本地存档按 Mod 命名空间隔离的 localStorage个人纪录、成就、设置

不开放:赛道几何与拾取物布局、车辆模型与贴图、联机协议与远端玩家状态、服务端代码、账号数据。


2. 加载阶段:什么时候该写什么

Mod 清单里的 loadPhase 只有两个值:

  • before-start(默认,推荐):在游戏 start() 之前注入。适合所有注册类调用: on()/once()、addHudBadge()、storage()、setSpeedMultiplier()、读取 api.version 做兼容判断。 此时游戏对象已存在,但主循环还没跑,不要读位置/速度,也不要依赖别的 Mod 已经注册。
  • after-start:在 start() 且 markGameReady() 之后注入。适合需要"游戏已经在跑"的 Mod, 例如依赖其它 Mod 已经注册好的徽章位、或需要立即读一帧状态做初始化。

禁止的跨阶段依赖:before-start 的 Mod 不得依赖任何 after-start 的 Mod(后者注入更晚, 那时前者已经执行完)。反过来可以。


3. 公共对象与 Mod API

游戏暴露唯一入口:

window.MyGame   // = modApi

modApi 是唯一公共对象。它不是安全沙箱,但没有它就无法与游戏交互。

3.1 元信息

字段类型说明
versionstringMod API 版本,如 "1.0.0"
slugstring作品 slug,恒为 "low-poly"
gamestring游戏名 "鹈鹕自行车赛"
multiplayerboolean本作自带联机,恒为 true

3.2 事件

方法参数返回
on(event, fn)事件名、回调退订函数 off()
once(event, fn)同上,只触发一次退订函数
off(event, fn)事件名、原回调—

事件表:

事件名触发时机回调参数
game:beforeStartstart() 被调用{}
game:ready主循环已启动{}
race:start每场开跑(单机/联机都有){ online:boolean, laps:number, room:string|null }
lap自己完成一圈{ lap:number, total:number, time:number, online:boolean }(time 是从起跑累计的总用时,不是本圈分段;要分段自己跟上一圈相减)
race:end自己完赛{ time:number, lap:number, online:boolean, hp:number, pickups:number }
pickup自己吃到道具{ type:"fix"|"nitro", x, y, z, lap, hp, nitro }
ui:menu从大厅/赛道返回主菜单{}
tick每帧dt:number(秒)
mod:changed数值被改动{ speedMul:number }

回调抛出的异常会被捕获并 console.warn,不会中断游戏或其它 Mod。

3.3 只读状态

方法返回
getState(){ scene, online, raceT, lap, totalLaps, frac, speed, hp, nitro, finished, room, players }
getPlayer()本机玩家对象(位置 x/z/y、speed、hp、nitro、lap、frac …)
getRivals()单机 AI 数组(联机时为空数组)
getPeers()联机远端玩家数组(单机时为空数组)
findPeer(id)按玩家号查远端玩家,找不到返回 null
getTrack(){ curve, length, halfWidth, laps }(curve 是 Three.js 曲线,可用于取点)
THREEThree.js 命名空间(0.150.1)

3.4 数值调节

方法参数说明
setSpeedMultiplier(v)0.25 ~ 4乘 MAXV / NITRO_MAX / ACCEL。联机时返回 false 且不生效
getSpeedMultiplier()—当前倍率

3.5 界面

方法参数说明
toast(text, ms?)文本、毫秒(默认 2200)顶部胶囊提示,最长 80 字
addHudBadge({id, text, color?})id 必填且唯一左侧 HUD 徽章;同一 id 重复调用是更新而非叠加

3.6 存储

const st = api.storage('my-mod');   // 命名空间隔离
st.get(key, default)   // 读
st.set(key, value)     // 写
st.all()               // 取该命名空间全部

底层 localStorage,实际键名 pelican-mod:<命名空间>。写失败(隐私模式/配额)会静默忽略。

3.7 调试

api.log(...) → 带 [mod] 前缀的 console.log。


4. 一个最小可运行 Mod

vibehub.mod.json:

{
  "title": "圈速提示",
  "summary": "每完成一圈在顶部提示用时",
  "description": "监听 lap 事件,把当前圈数与本圈用时显示在顶部。",
  "version": "1.0.0",
  "releaseNotes": "首个版本",
  "entry": "mod.js",
  "entryType": "classic",
  "styles": [],
  "loadPhase": "before-start",
  "dependencyIds": [],
  "capabilities": ["event.lifecycle", "hud.toast"]
}

mod.js:

(function () {
  const api = window.MyGame;
  if (!api) throw new Error('需要《鹈鹕自行车赛》Mod API');

  if (api.version.split('.')[0] !== '1') {
    api.log('Mod API 主版本不是 1,可能不兼容');
  }

  api.on('lap', (e) => {
    api.toast(`第 ${e.lap}/${e.total} 圈 · ${e.time.toFixed(1)}s`);
  });

  api.on('race:end', (e) => {
    api.toast(`完赛 ${e.time.toFixed(2)}s · 拾取 ${e.pickups} 次`, 4000);
  });
})();

5. 更多完整 Mod 案例

每个例子都对应真实可用的 API,capabilities 直接抄进清单即可。

例 2:单机轻松档(数值调节 + 徽章)

vibehub.mod.json:

{
  "title": "节奏档位",
  "summary": "在单机模式下切换轻松/普通/硬核,每次回主菜单换下一档",
  "description": "调整最高速与加速度倍率,并在 HUD 显示当前档位。联机时自动失效并提示。",
  "version": "1.0.0",
  "releaseNotes": "首个版本",
  "entry": "mod.js",
  "entryType": "classic",
  "styles": [],
  "loadPhase": "before-start",
  "dependencyIds": [],
  "capabilities": ["tuning.speed", "hud.badge", "hud.toast", "event.lifecycle", "storage.local"]
}

mod.js:

(function () {
  var api = window.MyGame;
  var MUL = { 轻松: 0.7, 普通: 1, 硬核: 1.6 };
  var ORDER = ['轻松', '普通', '硬核'];
  var st = api.storage('pace-mode');
  var mode = st.get('mode', '轻松');
  if (ORDER.indexOf(mode) < 0) mode = '轻松';

  function apply(m) {
    mode = m;
    var ok = api.setSpeedMultiplier(MUL[m]);      // 联机时返回 false 且不生效
    api.addHudBadge({ id: 'pace-mode', text: '档位 ' + m, color: ok ? '#7dffa8' : '#ffb35c' });
    if (!ok) api.toast('节奏档位只在单机生效,联机中已自动关闭', 3200);
    st.set('mode', m);
  }
  apply(mode);

  api.on('ui:menu', function () {                 // 每次回主菜单换下一档
    apply(ORDER[(ORDER.indexOf(mode) + 1) % ORDER.length]);
  });
})();

例 3:完赛战报(启动后加载 + 本地纪录)

loadPhase 用 after-start,因为它要靠游戏已经跑起来才能拿到完整的一局数据。

{
  "title": "完赛战报",
  "summary": "每局结束汇总用时、拾取与耐久,并记录最快单圈",
  "description": "监听 race:end,把本局结果播报在顶部,同时把最快成绩存在本地。",
  "version": "1.0.0",
  "releaseNotes": "首个版本",
  "entry": "mod.js",
  "entryType": "classic",
  "styles": [],
  "loadPhase": "after-start",
  "dependencyIds": [],
  "capabilities": ["event.lifecycle", "hud.toast", "storage.local", "hud.badge"]
}
(function () {
  var api = window.MyGame;
  var st = api.storage('recap');

  api.on('race:end', function (e) {
    var best = st.get('best', 0);
    var isPB = !best || e.time < best;
    if (isPB) st.set('best', e.time);

    var line = (e.online ? '联机' : '单机') + ' 完赛 ' +
      e.time.toFixed(2) + 's · 拾取 ' + e.pickups + ' 次 · 剩余耐久 ' + Math.round(e.hp);
    api.toast(isPB ? '★ 新纪录!' + line : line, 4200);
  });

  var runs = (st.get('runs', 0) || 0) + 1;
  st.set('runs', runs);
  api.addHudBadge({ id: 'recap', text: '第 ' + runs + ' 局' });
})();

例 4:连击计数器(拾取事件 + 每帧回调)

{
  "title": "连击计数器",
  "summary": "连续吃到道具就累加连击,4 秒没吃到清零",
  "description": "统计连续拾取,在 HUD 上显示连击数,5 连以上变金色。",
  "version": "1.0.0",
  "releaseNotes": "首个版本",
  "entry": "mod.js",
  "entryType": "classic",
  "styles": [],
  "loadPhase": "before-start",
  "dependencyIds": [],
  "capabilities": ["event.pickup", "event.frame", "hud.badge"]
}
(function () {
  var api = window.MyGame;
  var combo = 0, timer = 0, shown = -1;

  api.on('pickup', function () { combo++; timer = 4; });
  api.addHudBadge({ id: 'combo', text: '连击 0' });

  api.on('tick', function (dt) {
    if (timer > 0) { timer -= dt; if (timer <= 0) combo = 0; }
    if (combo === shown) return;                  // 只在数值变化时写 DOM
    shown = combo;
    api.addHudBadge({ id: 'combo', text: '连击 ' + combo,
      color: combo >= 5 ? '#ffe14d' : '#7dffa8' });
  });
})();

例 5:圈速分段(自己算分段)

lap 事件给的是累计总用时,所以要自己跟上一圈相减才是本圈成绩。

(function () {
  var api = window.MyGame;
  var prev = 0, splits = [];

  api.on('race:start', function () { prev = 0; splits = []; });
  api.on('lap', function (e) {
    var split = e.time - prev;
    prev = e.time;
    splits.push(split);
    api.toast('第 ' + e.lap + '/' + e.total + ' 圈 · ' + split.toFixed(2) + 's');
  });
  api.on('race:end', function () {
    if (splits.length) api.toast('最快一圈 ' + Math.min.apply(null, splits).toFixed(2) + 's', 4000);
  });
})();

6. 多 Mod 同时启用:命名、去重与冲突

  • HUD 徽章:addHudBadge 的 id 全局唯一。两个 Mod 用同一个 id 会互相覆盖(后注册的生效)。 请用 你的modid.功能 形式,例如 speedrun.split。
  • 存储:api.storage(ns) 的命名空间请用你的 Mod ID,不同 Mod 之间不会串数据。
  • 事件回调:同一事件上多个 Mod 的回调按注册顺序依次执行;一个 Mod 抛错不影响其它 Mod。 不要试图"取消"别人的回调(off 只退订你自己拿到的那个函数)。
  • 不要覆盖 window.MyGame、不要改写 game.js 内部变量、不要给共享对象挂同名属性。
  • 如果两个 Mod 都想改速度倍率:setSpeedMultiplier 是覆盖式的,最后调用的生效。 需要组合请自己先读 getSpeedMultiplier() 再写回,并在 releaseNotes 里说明。
  • 不要自行给 window 挂全局变量;必须暴露时用 window.__你的modid__ 这类唯一名。

7. 存档、联机、加载顺序与版本兼容

  • 存档:游戏本身没有云端存档;Mod 数据存在浏览器 localStorage,换设备/清缓存即丢失。 不要在里面存账号、Token 或任何凭据。
  • 联机策略:mods-disabled。本作联机是实时位置同步对战,任何 Mod 都可能造成不同步。 因此游戏在进入联机大厅前会拒绝所有已启用的 Mod,并提示玩家先去关闭; 同时 setSpeedMultiplier 在联机中直接返回 false。 想让 Mod 与联机共存请等作者切换到 author-managed 策略。
  • 加载顺序:before-start → start() → markGameReady() → after-start。见第 2 节。
  • 版本兼容:api.version 是语义化版本。主版本号变化代表破坏性变更;Mod 应在初始化时检查 主版本并在不匹配时降级或提示,而不是直接抛错让游戏白屏。

8. 明确禁止使用的能力

  • 读取、写入或转发任何账号凭据、Token、Cookie、管理接口或他人数据。
  • 直接操作私有 DOM(HUD 以外的内部元素)、覆盖 game.js 内部函数或变量、改写 window.MyGame。
  • 修改赛道几何、拾取物布局、其它玩家的车辆状态或比赛结果(联机以服务端为权威)。
  • 注入联机协议消息、伪造自己的位置或完赛时间。
  • 发起与游戏无关的网络请求、加载远程脚本、把 Mod 资源做成远程外链 (图片/音频/字体/WASM/Worker 必须放在 Mod 包内)。
  • 阻塞主循环:不要在 tick 回调里做重活;不要自己起 requestAnimationFrame 或定时器风暴。
  • 多线程/长时间占用:after-start Worker 请用 VibeHubWorkshop.createWorker(modId, path) 创建单文件 Worker。
手动发布 Mod高级方式:手动填写资料并上传 ZIP 或入口脚本

发布你的 Mod

登录后可以上传完整 Mod 包或单脚本。

登录