无名杀 lib.skill 技能格式全解析:从标准包源码到自定义技能编写指南

原创2026-10-03 10:02:311,971 阅读
文章标签:游戏开发前端

无名杀 lib.skill 技能格式全解析:从标准包源码到自定义技能编写指南

本文以 docs/lib-skill-format.md 为骨架,结合无名杀(noname)开源仓库中 apps/core/character/standard/skill.js 的标准包技能代码与 apps/core/noname 下技能引擎的源码实现,系统讲解 lib.skill 技能对象的全部字段、分类写法与运行机制。读完本文,你将掌握触发技、主动技、视为技、mod 规则修改、group/subSkill 组合技能等全部格式的完整写法,并能在阅读或编写任意角色包技能时快速定位引擎层的真实处理逻辑。

lib.skill:一张技能表

无名杀中,lib.skill 本质是一张技能表,键是技能名,值是该技能的配置对象:

lib.skill = {
  技能名: 技能配置对象,
};

角色包里的 skill.js 通常导出一个 skills 对象,加载后会被合并进 lib.skill。以仓库中标准包技能文件 apps/core/character/standard/skill.js 为例,其开头的 stdqingjiao(标准版清剿)就是最简洁的触发技形态:

const skills = {
  stdqingjiao: {
    audio: 2,
    trigger: { player: "phaseJieshuBegin" },
    forced: true,
    async content(event, trigger, player) {
      await player.draw();
    },
  },
};

export default skills;

在源码中可以看到,stdqingjiao 实际还带有 zhuSkill: true(主公技)与 filter 条件(自己本回合造成过对其他群势力角色的伤害),并且以 export default skills 导出,这正是“角色包 skill.js 导出 skills 对象、加载后合并入 lib.skill”的落地写法。

技能初始化:finishSkill

技能对象在加载时并不是原样入库,而是要经过引擎的初始化处理。核心入口是 apps/core/noname/game/index.js 中的 game.finishSkill(skillId, sub)(约第 8677 行起),它会做以下几件关键事:

  • 继承:若存在 info.inherit,会把被继承技能对象中缺失的字段(含 audio、对象型字段的深拷贝)复制过来,并回填 lib.translate 翻译。
  • viewAs 规范化:info.viewAs 是字符串时会被规范成 { name: 字符串 } 对象(第 8721-8723 行),所以文档与模板里 viewAs: "sha" 与 viewAs: { name: "sha" } 是等效的。
  • limited 补默认:limited: true 的技能会自动补齐 mark、intro、skillAnimation、init 等限定技默认字段(第 8754-8760 行)。
  • subSkill 展开:把子技能展开到顶层 lib.skill(详见下文 group 与 subSkill 小节)。
  • round 子技能生成:round 字段存在时,会自动追加一个 技能名_roundcount 的子技能用于按轮次计数(第 8783 行起)。
  • 模式可用性:按当前模式筛选翻译,必要时用 deleteSkill 将技能替换为“此模式下不可用”的占位对象。

因此在阅读技能代码时,看到“明明没写 mark 却有标记”的限定技,正是 finishSkill 自动补全的结果。

核心分类:一个技能对象的字段分组

一个技能对象通常由以下几组字段组成,本文后续各小节依次展开:

  • 触发技:trigger、filter、check、cost、content
  • 主动技:enable、filterCard、selectCard、filterTarget、selectTarget、content
  • 视为技:enable、filterCard、viewAs、viewAsFilter、position、prompt
  • 规则修改:mod
  • 组合技能:group、subSkill、sourceSkill
  • 展示和状态:audio、intro、mark、marktext、onremove、init
  • AI:ai

这套分组对应着引擎中技能的四类用途:等事件来了自动响应(触发技)、玩家在合适时机主动发动(主动技)、把手牌当作另一张牌用(视为技)、持续改变规则(mod)。

触发技:监听事件时机

触发技是数量最多的一类,典型格式:

skillName: {
  audio: 2,
  trigger: { player: "damageEnd" },
  filter(event, player) {
    return event.num > 0;
  },
  check(event, player) {
    return true;
  },
  async content(event, trigger, player) {
    await player.draw();
  },
}

trigger:事件时机与 role

trigger 表示监听的事件时机,其键是 role,值是要监听的事件名。四种常见 role 的含义:

trigger: { player: "damageEnd" }        // 当前事件的 event.player 是自己
trigger: { source: "damageSource" }     // 当前事件的 event.source 是自己
trigger: { target: "useCardToTarget" }  // 当前事件的 event.target 是自己
trigger: { global: "phaseJieshuBegin" } // 全局时机

时机既可以是字符串,也可以是数组(表示监听多个事件名):

trigger: {
  player: ["chooseToRespondBefore", "chooseToUseBefore"],
}

触发技的引擎执行流程

触发技大致执行流程:

事件触发
  -> 按 trigger 匹配技能
  -> filter 判断是否能触发
  -> forced/direct/frequent/cost/check 决定是否发动
  -> 创建技能事件
  -> 执行 content

从源码看,这一流程由事件系统驱动:apps/core/noname/library/element/gameEvent.ts(约第 637 行)在时机到达时创建 arrangeTrigger 事件,其核心执行体在 apps/core/noname/library/element/content.ts 的 async arrangeTrigger(event, trigger, player)(第 4017 行起):

  1. 先通过 lib.filter.filterTrigger(trigger, info.player, event.triggername, info.skill, info.indexedData)(定义于 apps/core/noname/library/index.js 第 11043 行起)筛出所有 filter 通过的可触发技能;
  2. 按 priority 排序,同一优先级的技能若语义不同会让玩家选择先触发哪一个;
  3. 若其中存在 silent(静默)或无翻译描述的技能,会优先执行,避免频繁弹窗;
  4. 对每个技能调用 game.createTrigger(...) 进入发动判定(info.forced 直接发动,否则走 frequent/direct/cost/check 的确认流程)。

filterTrigger 的实现里先解析 info.trigger,把 player/source/target/global 等 role 映射到当前事件对象上,再调用技能自身的 filter(event, player) 判断是否满足触发条件——这就是 filter 与 trigger 在引擎中的调用关系。

cost 与 content:代价与效果本体

cost 用于支付代价或选择目标,通常把结果写到 event.result:

async cost(event, trigger, player) {
  event.result = await player
    .chooseTarget(get.prompt2(event.skill), lib.filter.notMe)
    .set("ai", target => get.attitude(player, target))
    .forResult();
}

如果需要把额外数据传给 content,可写入 cost_data:

event.result = {
  bool: true,
  cost_data: {
    cards: result.cards,
  },
};

content 是技能效果本体,推荐使用异步写法,三个参数的含义:

async content(event, trigger, player) {
  // event: 当前技能事件
  // trigger: 被触发的原始事件
  // player: 技能拥有者
}

content 中常用数据:

  • event.cards:cost 或主动技选择的牌
  • event.targets:cost 或主动技选择的目标数组
  • event.target:主动技单目标
  • event.cost_data:cost 传入的额外数据
  • trigger.player、trigger.source、trigger.card 等原始事件信息

标准包中的 stdxiaoguo(标准版骁果)是“cost + content”分工的完整范例(见 apps/core/character/standard/skill.js 第 21-90 行):cost 里用 chooseToDiscard 让玩家弃一张基本牌(event.result 携带 cards),content 里先按 event.cards 弃置该牌,再让目标“弃一张装备牌或受到 1 点伤害”,通过 chooseToDiscard 的返回 bool 决定是否执行 target.damage()。注意 content 中取目标用的是 trigger.player(即 global: "phaseJieshuBegin" 时机的回合玩家),这展示了“cost 选择 + content 使用 trigger 原始信息”的常见组合。

主动技:enable 声明可用场景

主动技主要通过 enable 声明可用场景:

skillName: {
  audio: 2,
  enable: "phaseUse",
  usable: 1,
  filter(event, player) {
    return player.countCards("h") > 0;
  },
  filterCard: true,
  selectCard: 1,
  filterTarget(card, player, target) {
    return player != target;
  },
  async content(event, trigger, player) {
    await event.target.damage();
  },
}

常见 enable 取值:

enable: "phaseUse"                         // 出牌阶段
enable: "chooseToUse"                      // 响应使用牌
enable: "chooseToRespond"                  // 响应打出牌
enable: ["chooseToUse", "chooseToRespond"] // 多场景

主动技各选择字段的职责:

  • filter(event, player):技能整体是否可用
  • filterCard(card, player):哪些牌可被选择
  • selectCard:选择牌数,如 1、[1, Infinity]、-1(-1 表示不选实体牌)
  • position:可选牌区域,如 "h"、"he"、"hs"、"hes"
  • filterTarget(card, player, target):哪些角色可被选择
  • selectTarget:选择目标数
  • check(card):AI 选牌评估
  • discard: false:不走默认弃牌
  • lose: false:不走默认失去牌
  • delay:是否/如何延迟展示

标准包中的 stdshushen(标准版淑慎)展示了主动技目标选择的另一种形态:cost 里用 chooseTarget 选择目标并写入 event.targets,content 里通过 event.targets[0] 取目标(见 apps/core/character/standard/skill.js 第 92-113 行)。可见主动技的目标既可以用 filterTarget/selectTarget 走引擎默认选择流程,也可以在 cost 里手动 chooseTarget,两条路径最终都以 event.targets / event.target 形式把目标交给 content。

视为技:把某些牌当作另一张牌

视为技把某些牌当作另一张牌使用或打出:

skillName: {
  enable: ["chooseToUse", "chooseToRespond"],
  filterCard(card) {
    return get.color(card) == "red";
  },
  viewAs: { name: "sha" },
  viewAsFilter(player) {
    return player.countCards("hs", { color: "red" }) > 0;
  },
  position: "hs",
  prompt: "将一张红色牌当杀使用或打出",
  check(card) {
    return 5 - get.value(card);
  },
}

关键字段:

  • viewAs: { name: "sha" }:视为的牌(也支持字符串写法,初始化时会被 finishSkill 规范为对象)
  • filterCard:原始牌选择条件
  • viewAsFilter:技能按钮是否可出现
  • position:从哪些区域选原始牌
  • prompt:技能提示文本

若不需要选择实体牌(如纯声明式视为),可以使用:

filterCard() {
  return false;
},
selectCard: -1,
viewAs: { name: "sha" },

从引擎看,viewAsFilter 是视为技是否出现在 UI 按钮区的总开关:apps/core/noname/library/element/player.js(约第 3096 行)在收集可选视为技时判断 !info.viewAsFilter || info.viewAsFilter(player) !== false;apps/core/noname/library/index.js(约第 11187 行)也以 viewAsFilter(player) === false 作为屏蔽条件。而 viewAs 若是字符串,finishSkill 会将其规范为 { name: 字符串 }(apps/core/noname/game/index.js 第 8721-8723 行)。

mod:持续性规则修改

mod 是持续性规则修改,可单独存在,不一定需要 trigger/content:

skillName: {
  mod: {
    maxHandcard(player, num) {
      return num + 1;
    },
    cardUsable(card, player, num) {
      if (card.name == "sha") return Infinity;
    },
    targetEnabled(card, player, target) {
      if (target.countCards("h") == 0 && card.name == "sha") return false;
    },
  },
}

常见 mod 项:

  • maxHandcard(player, num):手牌上限
  • cardUsable(card, player, num):牌使用次数
  • targetEnabled(card, player, target):是否能成为目标
  • targetInRange(card, player, target):是否无视距离
  • aiValue(player, card, num):AI 牌值
  • aiUseful(player, card, num):AI 可用价值

在源码中,这些 mod 项并非散落在各处被硬编码调用,而是统一注册进 lib.skill 全局对象的 mod 表中:apps/core/noname/library/skill.js(第 491、548 行)会做类似 lib.skill[技能名].mod.maxHandcard = mod、lib.skill[技能名].mod.cardUsable = mod 的挂载,游戏在结算手牌上限、使用次数、目标合法性等环节时按角色技能逐一合并这些 mod 函数求值。cardUsable 返回 Infinity 即代表“杀的使用次数不限”,返回 num + 1 则是在原基础上加一次。

group 与 subSkill:组合技能

group 表示拥有主技能时,同时拥有其他技能逻辑:

skillName: {
  group: ["skillName_part1", "skillName_part2"],
}

subSkill 适合把一个技能拆成多个子技能:

skillName: {
  group: ["skillName_sha", "skillName_shan"],
  subSkill: {
    sha: {
      enable: ["chooseToUse", "chooseToRespond"],
      viewAs: { name: "sha" },
    },
    shan: {
      enable: ["chooseToUse", "chooseToRespond"],
      viewAs: { name: "shan" },
    },
  },
}

加载时会展开为顶层技能表项:

lib.skill.skillName_sha
lib.skill.skillName_shan

子技能通常会自动带上:

sub: true
sourceSkill: "skillName"

subSkill 的展开由 finishSkill 实现(apps/core/noname/game/index.js 第 8761-8782 行):遍历 info.subSkill 的每个键 value,生成 技能名_value 的顶层键,自动标记 sub = true,未显式指定 sourceSkill 时补上主技能名,并顺带把子技能的 name/description/marktext 回填进 lib.translate(分别对应 技能名_value、技能名_value_info、技能名_value_bg)。这意味着子技能可以被 group 引用、被其他技能通过 sourceSkill 溯源、也可以单独成为 get.info() 的对象。

标记、状态与移除

常用字段:

skillName: {
  mark: true,
  marktext: "义",
  intro: {
    content: "expansion",
    markcount: "expansion",
  },
  init(player, skill) {
    player.storage[skill] = 0;
  },
  onremove(player, skill) {
    delete player.storage[skill];
  },
}

常见含义:

  • mark:是否显示技能标记
  • marktext:标记文字
  • intro:标记说明
  • intro.markcount:标记计数方式(如 "expansion" 表示按某个计数扩展显示)
  • init:获得技能时初始化(常用于 player.storage[skill] 计数清零)
  • onremove:移除技能时清理
  • onremove: true:简单清理该技能 storage

limited: true 会自动补齐 init(player.storage[skill] = false),配合 mark/intro/skillAnimation 形成限定技的“已发动”标记展示闭环。

控制字段

高频控制字段一览:

  • forced: true:强制发动,不询问(如不额外设置 locked 则默认 locked 为 true)
  • frequent: true:频繁发动,可自动发动
  • direct: true:直接发动,减少确认流程
  • locked: true:锁定技
  • zhuSkill: true:主公技
  • limited: true:限定技
  • skillAnimation: true:发动动画
  • animationColor: "orange":动画颜色
  • preHidden: true:国战等模式下可预亮
  • silent: true:静默触发
  • popup: false:不显示技能 popup
  • firstDo: true:同一时机中优先处理
  • lastDo: true:同一时机中延后处理
  • sourceSkill: "xxx":归属技能
  • charlotte: true:状态技/特殊隐藏/不常规移除类标记
  • usable: 1:每阶段或每时机可用次数限制
  • round: 1:按轮次限制

这些字段在引擎中都有明确的消费点:forced 在 createTrigger 中直接 result = { bool: true } 跳过询问(apps/core/noname/library/element/content.ts 第 4125 行);frequent 支持布尔、函数与 "check" 三种形态,其中 frequent: "check" 会复用 check 函数的返回值作为自动发动判定(同文件第 4128-4142 行);direct 则自动交换 AI 操控权并直接发动。round: 1 会触发 finishSkill 自动生成 技能名_roundcount 子技能用于轮次计数。

AI 字段

ai 不直接改变规则,主要影响电脑决策、响应判断和界面评估:

ai: {
  order: 3,
  threaten: 0.8,
  expose: 0.1,
  respondSha: true,
  respondShan: true,
  result: {
    player(player) {
      return 1;
    },
    target(player, target) {
      return get.attitude(player, target);
    },
  },
  effect: {
    target(card, player, target, current) {
      if (get.tag(card, "damage")) return [1, 0.5];
    },
  },
  skillTagFilter(player, tag, arg) {
    return true;
  },
  tag: {
    rejudge: 1,
  },
}

常见字段:

  • order:主动使用优先级
  • result.player:对自己的收益
  • result.target:对目标的收益
  • effect.target:被某牌影响时的修正
  • respondSha / respondShan:可响应杀/闪
  • skillTagFilter:技能标签是否有效
  • threaten:威胁度
  • expose:身份暴露倾向
  • tag:能力标签,如 rejudge

标准包 stdshushen 末尾的 ai: { threaten: 0.8, expose: 0.1 } 就是一个最小 AI 配置(apps/core/character/standard/skill.js 第 113 行)。此外,主动技/视为技的 check(card) 函数也是 AI 体系的一部分——它给出“用这张牌发动技能”的估值,6 - get.value(card) 这类写法表示“价值越低越值得用来发动”。引擎还会在 finishSkill 时把 viewAs 对应牌(lib.card[info.viewAs.name].ai)的 AI 配置合并进技能对象(apps/core/noname/game/index.js 第 8735-8753 行),因此视为技常常无需重复编写完整的 ai 对象。

五种实战模板

以下模板直接取自原文档,均可复制到角色包 skill.js 中使用。

触发技模板

skillName: {
  audio: 2,
  trigger: { player: "damageEnd" },
  filter(event, player) {
    return event.num > 0;
  },
  check(event, player) {
    return true;
  },
  async content(event, trigger, player) {
    await player.draw();
  },
}

带代价触发技模板

skillName: {
  audio: 2,
  trigger: { global: "phaseJieshuBegin" },
  filter(event, player) {
    return event.player != player;
  },
  async cost(event, trigger, player) {
    event.result = await player
      .chooseToDiscard(get.prompt2(event.skill), "h")
      .set("ai", card => 6 - get.value(card))
      .forResult();
  },
  async content(event, trigger, player) {
    await trigger.player.damage();
  },
}

主动技模板

skillName: {
  audio: 2,
  enable: "phaseUse",
  usable: 1,
  filterCard: true,
  selectCard: 1,
  filterTarget(card, player, target) {
    return player != target;
  },
  check(card) {
    return 6 - get.value(card);
  },
  async content(event, trigger, player) {
    await event.target.damage();
  },
}

视为技模板

skillName: {
  audio: 2,
  enable: ["chooseToUse", "chooseToRespond"],
  filterCard(card) {
    return get.color(card) == "red";
  },
  viewAs: { name: "sha" },
  viewAsFilter(player) {
    return player.countCards("hs", { color: "red" }) > 0;
  },
  position: "hs",
  prompt: "将一张红色手牌当杀使用或打出",
  check(card) {
    return 5 - get.value(card);
  },
  ai: {
    respondSha: true,
  },
}

规则修改模板

skillName: {
  mod: {
    maxHandcard(player, num) {
      return num + player.countMark("skillName");
    },
    cardUsable(card, player, num) {
      if (card.name == "sha") return num + 1;
    },
  },
  intro: {
    content: "手牌上限+#",
  },
}

运行时注意点

编写与调试技能时需牢记以下几点,它们都能在源码中找到对应行为:

  • 技能名是 lib.skill 的键,也是默认翻译键,如 lib.translate.skillName 和 lib.translate.skillName_info;finishSkill 还会按模式(如身份、国战、单挑)优先选择带模式后缀的翻译文本。
  • trigger 技能由事件系统收集,创建 arrangeTrigger 事件统一排序执行(apps/core/noname/library/element/content.ts 第 4017 行起);同一时机多个技能按优先级排列,必要时让玩家选择触发顺序。
  • cost 不是必须字段;没有 cost 且不是 forced/direct 时,引擎通常会创建确认框(createTrigger 中 forced 直接发动、direct 快速发动,其余走确认流程)。
  • content 推荐使用 async content(event, trigger, player) 写法,以便 await 引擎提供的异步结算 API(如 player.draw()、target.damage())。
  • subSkill 会被展开到顶层 lib.skill,命名规则是 主技能名_子技能名,自动带 sub: true 与 sourceSkill。
  • viewAs 若是字符串,初始化时会被规范成 { name: 字符串 }。
  • limited: true 会自动补充部分限定技默认字段,如 mark、intro、skillAnimation、init。
  • ai 字段非常影响电脑行为;规则正确但 AI 缺失时,电脑可能不会合理发动——因此在发布角色包前,至少为每个主动技补上 check(card) 与必要的 ai.result。

深入阅读

若想进一步理解技能系统的完整运作,可以按以下路径继续阅读仓库源码:

结合本文与这些源码路径,你便可以从“照着模板写技能”进阶到“理解引擎每一步在做什么”,从而写出更稳健、AI 表现更合理的自定义技能。

登录后查看全文
noname