Files
pixelheros/docs/superpowers/specs/2026-08-21-hero-equip-design.md
pan e537170812 docs: add hero personal equip system design
Spec for per-hero equipment with random affixes: equip bases,
affix pools, quality by affix count, box/gacha acquisition,
out-of-run persistence, and in-run snapshot into HeroAttrsComp.
2026-08-21 10:43:49 +08:00

12 KiB
Raw Blame History

英雄个人装备系统设计

  • 日期2026-08-21
  • 状态已评审brainstorming 阶段用户逐节确认)
  • 涉及文件:HeroEquipSet.ts(新增)、heroSet.tsHeroAttrs.tsHeroAttrsComp.tsSingletonModuleComp.tsGameEvent.tsEquipSet.ts(改名 TalentSet

1. 背景与目标

现有"装备"EquipSet.ts8701~8820 段)是全体驻场光环卡:购买后全队加属性,经 EquipBoxComp → FieldSkillHelper 全局聚合。

本设计新增英雄个人装备:每个英雄可装备一件装备,词条只作用于该英雄本身;与现有光环卡并存,光环卡在表述上改名为战队天赋

核心特性:

  • 装备在玩家获得时(开箱子 / 抽卡)随机生成词条并固定(暗黑式:从词条池随机抽 N 条、数值在区间内随机)。
  • 词条数量与数值高低决定装备品质
  • 装备是局外资产,随云存档持久化;局内只有战斗,不产生、不修改装备,开局快照生效。

2. 数据结构HeroEquipSet.ts新增

位置:assets/script/game/common/config/HeroEquipSet.ts,与 EquipSet.ts(将改名 TalentSet.ts)平级。

import { Attrs } from "./HeroAttrs";

/** 装备品质由词条数推导1=普通 2=稀有 3=史诗 4=传说) */
export enum EquipQuality {
    Normal = 1,
    Rare = 2,
    Epic = 3,
    Legend = 4,
}

/** 词条池条目attr 指向 Attrs 枚举roll 时 value 在 [min, max] 内随机 */
export interface AffixPoolEntry {
    attr: Attrs;
    min: number;        // 数值型=整数下限;概率型=百分点下限
    max: number;
    weight: number;     // 抽取权重
}

/** 装备底座:只定义随机规则,不含固定词条(段位 9001-9099 */
export interface HeroEquipBase {
    base_id: number;
    name: string;
    icon: string;
    affix_count: [number, number];  // 词条数区间,如 [1,2]
    affix_pool: AffixPoolEntry[];
}

/** 装备实例roll 生成后固定;不冗余存 name/icon查询走底座表 */
export interface HeroEquipInstance {
    inst_id: number;        // 实例唯一 idequip_next_id 自增)
    base_id: number;
    affixes: { attr: Attrs; value: number }[];  // quality = affixes.length不单独存储
}

/** 装备箱子配置(段位 8901-89992 档) */
export interface EquipBoxConfig {
    uuid: number;
    name: string;
    icon: string;
    pool: { base_id: number; weight: number }[];  // 底座加权池
}

词条口径(与现有百分比口径一致)

  • 数值型(仅 ap / hp_max / defenseFlat 数值roll 取整数
  • 概率型其余全部百分点roll 取 1 位小数
  • 词条池仅允许出现第 4.4 节白名单中的 attr配置时校验。

品质规则

  • quality = 词条数14 对应普通传说),初版从简;数值高低不参与品质,后续可扩展档位评分。

3. Roll 生成与获取途径

3.1 核心纯函数HeroEquipSet.ts

/**
 * 获得装备时调用:按底座规则 roll 出一件词条固定的装备实例
 * @param base_id 装备底座 id
 * @param instId 实例 id由 SingletonModuleComp 分配)
 */
export function rollHeroEquip(base_id: number, instId: number): HeroEquipInstance | null

Roll 流程:

  1. affix_count: [min, max] 内均匀随机词条数 N。
  2. affix_pool 按 weight 加权不放回抽 N 条不同 attr同装备不重复属性
  3. 每条词条在 [min, max] 内随机(数值型取整,概率型 1 位小数)。
  4. 品质 = N。
/**
 * 按底座池加权抽取并 roll 出 count 件装备(箱子/抽卡统一入口)
 * @param pool 底座加权池
 * @param count 抽取件数
 * @param allocInstId 实例 id 分配器(每次调用返回新 id
 * @param guaranteeMinQuality N 连保底品质0=不保底)
 */
export function rollEquipPool(
    pool: { base_id: number; weight: number }[],
    count: number,
    allocInstId: () => number,
    guaranteeMinQuality: EquipQuality = 0,
): HeroEquipInstance[]

保底逻辑:当 count >= GACHA_COUNT 且全部 < guaranteeMinQuality 时,最后一件重 roll 至 ≥ 保底品质。

3.2 可调参数GameSet.ts

GACHA_COUNT = 10,            // N 连连抽数(后期可调 20/30
GACHA_MAX_DRAW = 10,         // 单次最多一起抽的张数
GACHA_GUARANTEE_QUALITY = 2, // N 连保底品质(稀有)

3.3 双获取通道

途径 流程 产出
装备箱子2 档:普通箱/高级箱) 商店购买/奖励 → equip_boxes 库存 → 背包界面点击开箱 固定 1 件,走 rollEquipPool(box.pool, 1)
抽卡(消耗装备抽卡券) 抽卡界面选 1 抽 / N 连(默认 10≤ GACHA_MAX_DRAW→ 消耗 equip_tickets N 件,走 rollEquipPool(gachaPool, N, 保底)
  • 高级箱与普通箱的差异仅为 pool 中底座权重(高词条数底座权重更高)。
  • 单局结算奖励可发放:箱子(入 equip_boxes)和/或装备抽卡券(入 equip_tickets)。

4. 局内集成HeroAttrsComp

原则:局内快照,只读生效。 开战创建英雄实体时快照装备词条,局内不参与装备增删改。

4.1 快照字段

// HeroAttrsComp 新增
/** 装备词条加成缓存(局外穿戴装备在开局时快照;数值型=flat概率型=百分点) */
equip_mods: Partial<Record<Attrs, number>> = {};
  • 不存 HeroEquipInstance,只存"每条属性加了多少"的聚合结果(同 attr 多词条快照时已累加)。
  • reset() 中清空。
  • 局内 UI 若要显示装备名,从 hero_equips 反查,不影响战斗数据。

4.2 快照时机

英雄实体初始化(读 HeroInfo 填充属性的同一入口处)追加:

const instId = smc.data.hero_equips[hero_uuid];
const inst = instId ? smc.data.equips[instId] : null;
attrsComp.equip_mods = aggregateAffixes(inst);  // HeroEquipSet.ts 纯函数UI 属性预览可复用

4.3 结算接入(最小侵入,每个 getFinal 加一项)

数值型:装备 flat 先进基础值,再吃驻场光环百分比(乘算放大,装备更值钱):

public getFinalAp(): number {
    const equipFlat = this.equip_mods[Attrs.ap] ?? 0;
    const runtimeAp = this.getRuntimeAp(this.ap + equipFlat);
    const mods = this.aggregateTimedMods(Attrs.ap);
    return (runtimeAp + mods.flatSum) * (1 + mods.pctSum / 100);
}

顺序:装备 flat → 驻场光环 % → 计时 buff。同理 getFinalHpMaxgetFinalDefense

概率型:百分点直接累加(base + equip + timed

public getFinalCritical(): number {
    const base = this.getRuntimeCritical() + (this.equip_mods[Attrs.critical] ?? 0);
    const mods = this.aggregateTimedMods(Attrs.critical);
    return base + mods.flatSum + mods.pctSum;
}

4.4 接入清单(词条池 attr 白名单)

getFinal 方法 词条类型
getFinalAp / getFinalHpMax / getFinalDefense 数值型
getFinalCritical / getFinalCritDamage 概率型
getFinalFreezeChance / getFinalStunChance / getFinalParalyzeChance 概率型
getFinalPunctureChance / getFinalPunctureDmgBonus / getFinalWindFury 概率型
getFinalCriticalRes / getFinalFreezeRes / getFinalStunRes 概率型
getFinalKnockbackChance / getFinalKnockbackDistance / getFinalKnockbackRes 概率型
getFinalVulnerableRes / getFinalParalyzePower 概率型
getEffectiveSkillCdspeed 词条 → 攻速) 概率型

5. 局外数据持久化SingletonModuleComp.data

data: any = {
    // ...现有字段
    /** 玩家装备库key=inst_id局外资产随云存档持久化 */
    equips: {} as Record<number, HeroEquipInstance>,
    /** 实例 id 自增计数器(云端同步,防 id 冲突) */
    equip_next_id: 1,
    /** 英雄穿戴表key=英雄模板 uuidvalue=inst_id0=未穿戴) */
    hero_equips: {} as Record<number, number>,
    /** 装备箱子库存key=箱子 uuidvalue=数量 */
    equip_boxes: {} as Record<number, number>,
    /** 装备抽卡券数量 */
    equip_tickets: 0,
}

GameDate 接口同步补充上述可选字段。

数据量评估:每件装备实例 JSON 约 60~80 字节(只存 inst_id/base_id/affixes,不冗余存 name/icon500 件 ≈ 40KB远低于微信云存储 1MB 单记录上限。quality 不存储(= affixes.length)。

数据操作方法(封装在 SingletonModuleCompUI 不直改数据)

addEquipInstances(insts: HeroEquipInstance[]): void   // 入库 + markDataDirty + Equip_Update
wearEquip(heroUuid: number, instId: number): boolean   // 穿戴/替换,旧装自动回库
unwearEquip(heroUuid: number): void
consumeEquipBox(boxUuid: number): HeroEquipInstance | null  // 开箱扣库存→roll→入库→返回
drawEquips(count: number): HeroEquipInstance[] | null       // 抽卡扣券→roll(保底)→入库
addEquipTickets(n: number): void
addEquipBox(boxUuid: number, n: number): void

每个方法内部 gameDataSync.markDataDirty() + oops.message.dispatchEvent 对应事件。

新增事件GameEvent.ts

static Equip_Update = "Equip_Update";        // 装备库/穿戴表变更(背包/英雄界面刷新)
static EquipBox_Update = "EquipBox_Update";  // 箱子数量变更
static EquipTicket_Update = "EquipTicket_Update"; // 抽卡券数量变更

监听方必须在 onDestroy/onDisable 注销oops 规范)。

6. UI 与交互

6.1 英雄界面(唯一穿戴入口)

  • 英雄详情面板新增装备槽(属性区附近):空槽显示 +,已穿戴显示装备图标+品质边框。
  • 穿戴/换装:点击装备槽 → 弹出装备选择层,仅列未被任何英雄穿戴的装备(按品质排序)→ 点击即穿戴/替换(旧装回库)。
  • 卸下:已穿戴时选择层提供【卸下】按钮。
  • 已被其他英雄穿戴的装备在选择层中不显示

6.2 装备背包界面(资产总览 + 开箱)

  • 入口:主界面"装备"按钮(复用 tip_done.equip 引导标记)。
  • 布局:装备网格列表(按品质:传说→史诗→稀有→普通;已穿戴的标记穿戴英雄头像角标)+ 点击看词条详情;顶部箱子区(普通箱/高级箱图标+数量),点击开箱 → 展示战利品 → 入库。

6.3 抽卡界面

  • 入口:商店界面新增"装备抽卡"页签。
  • 【抽 1 次】= 1 张券;【抽 N 次】= N 张券N 显示当前 GACHA_COUNT);券不足按钮置灰。
  • 结果浮层:网格展示 N 件装备(图标+品质色),点击看词条;保底装备高亮"保底"标记。

7. 现有光环卡改名"战队天赋"

  • EquipSet.tsTalentSet.ts(文件改名);EquipPoolListTalentPoolListdrawEquipCardsdrawTalentCardsfindEquipByUuidfindTalentByUuid
  • UI 文案"装备"→"战队天赋"tip_done.equip 字段名不变(避免存档兼容问题),语义指向天赋面板。
  • 局内组件 EquipBoxComp / MissEquipComp / EquipListComp逻辑不变,仅引用路径与显示文案调整。

8. 错误处理与边界

  • rollHeroEquip 对未知 base_id 返回 nullmLogger.warnrollEquipPool 空池返回空数组。
  • 穿戴时 instId 不存在(云端数据异常)→ wearEquip 返回 false不崩溃。
  • 英雄被穿戴的装备在库中不存在 → 快照时按无装备处理(equip_mods = {})。
  • 词条池 attr 不在白名单 → 配置加载时 mLogger.warn 并跳过该词条。
  • 旧存档无 equips 等新字段 → Object.assign 合并后由访问方兜底初始化(参照 getHeroGrowth 的懒初始化模式)。

9. 不做的事YAGNI

  • 跨局账号级装备强化/分解/合成系统。
  • 装备部位(武器/盔甲等多槽位)——当前每英雄仅 1 槽。
  • 数值档位评分影响品质(初版品质=词条数)。
  • 局内掉落/换装。
  • 词条重 roll洗练