暗黑破坏神2 MOD修改工具装备编辑暗金物品

装备编辑-暗金物品 对应的是 UniqueItems.txt 这张暗金物品属性表。它和 weapons.txtarmor.txtmisc.txt 的定位不同:基础装备表定义底材,UniqueItems.txt 定义基于底材生成的唯一品质物品。当前模块会影响暗金物品是否允许随机掉落、掉落等级、需求等级、稀有度权重、价格修正、背包图标覆盖、基础物品代码绑定,以及最多 12 组 prop/par/min/max 词条属性。

在暗黑破坏神2 MOD 制作中,暗金物品往往是装备系统平衡的重点。一个暗金条目既要绑定某个底材 code,又要配置固定词条,还要处理名称、图标、掉落门槛和携带限制。与普通底材不同,暗金物品不是单纯改一组基础攻防数值,而是在基础物品之上叠加一组唯一化规则。因此,将 UniqueItems.txt 放进装备编辑页并单独做成"暗金物品"入口,有助于把底材编辑和唯一属性编辑区分开来。

文章目录

文件说明

装备编辑-暗金物品 模块由页面入口、通用装备编辑器、字段元数据、公共分组配置、复制联动逻辑和 Tauri 本地写回链路共同组成。页面包装文件很薄,核心逻辑集中在 ItemEquipEditor.vueuseHomePageController.jsitemEquipHelpers.js 中。字段说明主要来自 UniqueItems.txt.columns.json,它定义了暗金条目的基础字段、掉落字段、经济字段、显示覆盖字段和 12 组词条字段。

模块文件职责表如下。

文件名 文件类型 模块职责 与界面或数据处理的关系 备注
src/modules/itemEdit/ItemEditTab.vue Vue 装备编辑总入口 顶部页签中包含 UniqueItems.txt,标签为"暗金物品" 页面入口层
src/modules/itemEdit/unique/ItemEquipUniqueTab.vue Vue 暗金物品包装页 file-name="UniqueItems.txt" 传给通用编辑器 当前模块最薄的一层
src/modules/itemEdit/ItemEquipEditor.vue Vue 通用装备编辑器 提供检索、条目切换、分组编辑、复制、描述编辑和保存 当前模块主界面
src/pages/home/useHomePageController.js JS 装备编辑状态装配 生成字段对象、分组列表、当前分组和描述分组 状态与解析层
src/pages/home/itemEquipHelpers.js JS 装备数据读取、保存和复制联动 负责 loadItemEquipFilesaveItemEquipFilecloneBaseItemRowByCodecloneUniqueOrSetItemRowByCode 数据交互核心
src/pages/home/tableDataLoader.js JS 通用 TXT 读取缓存 py_load_table 读取结果做缓存、去重和更新 读取性能层
src/modules/itemEdit/common/itemEquip.grouping.json JSON 通用装备字段分组规则 按列名、范围组、说明关键词把字段归入业务组 当前暗金页实际分组主配置
src/modules/itemEdit/common/itemEquipGrouping.js JS 通用分组解析器 根据 range_group、列名和说明文本解析字段分组 当前暗金页实际解析层
src/modules/itemEdit/unique/unique.grouping.json JSON 暗金模块标准化分组配置 定义"基础信息/其他"两组包装配置 当前源码未看到主链路直接引用
src/modules/itemEdit/unique/unique.layout.json JSON 暗金模块布局顺序 定义暗金包装配置的组顺序 当前源码未看到主链路直接引用
src/modules/itemEdit/unique/uniqueGrouping.js JS 暗金模块分组解析封装 通过 groupingFactory 输出暗金分组解析接口 当前源码未看到主链路直接引用
src/assets/resources/index.json JSON 列元数据索引 UniqueItems.txt.columns.json 纳入统一资源索引 元数据入口
src/assets/resources/files/UniqueItems.txt.columns.json JSON 暗金字段中文与说明 定义暗金条目、掉落、等级、价格、显示覆盖和词条属性等字段 字段说明层
src-tauri/src/main.rs Rust Tauri 命令注册 暴露 py_load_tablepy_save_table_json 给前端调用 命令桥接层
src-tauri/src/commands/files.rs Rust 本地 TXT 读写命令 负责 UniqueItems.txt 的编码读取、表格解析、写回和备份 后端数据层

从文件职责可以看出,暗金模块的页面壳层非常轻,真正的功能分布在通用装备编辑体系里。ItemEquipUniqueTab.vue 只负责传入文件名;ItemEquipEditor.vue 负责界面交互;useHomePageController.js 负责把表格行变成字段对象和分组;itemEquipHelpers.js 负责读取、保存、复制底材和复制暗金/套装条目;Rust 侧负责最终本地落盘。

下面这张表整理当前模块真正参与界面构建、字段分组、复制行为和保存逻辑的字段、配置项与方法。

字段/配置项/方法 所属文件 作用 在界面中的体现 修改后的影响
itemEquipFileTabs useHomePageController.js 定义装备编辑页签 顶部显示"暗金物品" 决定进入 UniqueItems.txt 编辑
itemEquipState.fileName useHomePageController.js 当前编辑文件名 选中暗金物品后值为 UniqueItems.txt 决定读取、分组、保存目标
ItemEquipUniqueTab.vue src/modules/itemEdit/unique 暗金页包装组件 挂载 ItemEquipEditor 让暗金页复用通用编辑器
currentItemEquipFields useHomePageController.js 将当前行转成字段对象 字段标签、tooltip、当前值都来自这里 决定右侧表单内容
resolveItemEquipGroupByField itemEquipGrouping.js 字段分组解析 生成"基础标识、需求与价格、掉落与生成、词条属性"等分组 决定字段显示位置
range_group UniqueItems.txt.columns.json 标识字段范围组 prop1~12par1~12min1~12max1~12 会聚合到词条属性 提升词条字段可读性
isCopyModeFile ItemEquipEditor.vue 判断复制模式文件 UniqueItems.txt 按钮显示"复制" 决定复制当前条目而不是新增空白行
applyUniqueDefaultLevels ItemEquipEditor.vue 暗金复制时设置默认等级 复制暗金条目时隐式执行 lvl 设为 80lvl req 设为 65
cloneBaseItemRowByCode itemEquipHelpers.js 按 code 联动复制基础底材 暗金复制链路中可能触发 weapons/armor/misc 中追加对应底材
cloneUniqueOrSetItemRowByCode itemEquipHelpers.js 按 code 联动复制暗金/套装条目 基础物品复制时可反向联动 复制 UniqueItems.txtSetItems.txt 命中条目
descEnabledFiles useHomePageController.js 决定哪些文件追加"装备描述" UniqueItems.txt 会出现描述分组 支持暗金名称/描述编辑入口
saveItemEquipAndDescription ItemEquipEditor.vue 联合保存装备表与描述 点击保存按钮触发 保存暗金表,处于描述分组时继续保存描述
saveItemEquipFile itemEquipHelpers.js 保存当前装备文件 保存按钮底层实现 走 Tauri 本地文件写回
py_save_table_json main.rs / files.rs 本地保存命令 前端无感,保存时调用 写回 TXT 并生成备份

暗金物品字段较多,适合从数据字段角度再做一次归纳。下表只选取当前 UniqueItems.txt.columns.json 中能够确认的代表字段。

字段名 中文含义 所属分组 前端展示方式 实际用途说明
index 装备自定义名称 基础标识 文本输入 暗金条目的名称 key 或引用 key
version 适用版本 基础标识 文本或版本输入 区分经典模式与资料片模式
enabled 允许随机掉落 掉落与生成 布尔输入 控制是否可作为随机暗金掉落候选
rarity 稀有度权重 掉落与生成 数字输入 调整该暗金相对其他暗金的生成概率
lvl 掉落等级 需求与价格 数字输入 控制掉落该暗金所需的物品等级
lvl req 需求等级 需求与价格 数字输入 控制角色使用该暗金所需等级
code 基础物品代码 基础标识 文本输入 匹配 weapons.txtarmor.txtmisc.txt 中的底材 code
carry1 仅可携带1件 基础标识 / 其他 布尔输入 控制是否最多携带一件
cost mult 价格倍率 需求与价格 数字输入 修改购买、出售、修理费用倍率
cost add 价格加成 需求与价格 数字输入 在倍率之后追加固定价格修正
invtransform 背包UI染色 显示与背包 文本输入 控制背包图标颜色变化
invfile 背包图标覆盖 显示与背包 文本输入 覆盖基础物品图标
flippyfile 地面显示覆盖 显示与背包 文本输入 覆盖基础物品地面显示资源
dropsound 掉落音效覆盖 显示与背包 文本输入 覆盖基础物品掉落音效
usesound 使用音效覆盖 显示与背包 文本输入 覆盖基础物品使用音效
prop1 ~ prop12 词条属性 词条属性 属性代码输入 引用 Properties.txt 的 code 字段
par1 ~ par12 词条参数 词条属性 文本或数字输入 配合 prop# 提供参数
min1 ~ min12 词条最小值 词条属性 数字输入 配合 prop# 提供最小值
max1 ~ max12 词条最大值 词条属性 数字输入 配合 prop# 提供最大值
firstLadderSeason 首个天梯赛季 其他 数字输入 控制暗金赛季可用范围
lastLadderSeason 末个天梯赛季 其他 数字输入 配合首个赛季字段限制天梯范围

从这些字段可以看出,暗金物品模块是"底材引用 + 唯一属性 + 掉落控制 + 显示覆盖"的综合编辑页。它不直接定义基础武器或防具的伤害防御,而是通过 code 绑定底材,再用 prop/par/min/max 定义暗金专属属性。

软件开发

从开发实现来看,装备编辑-暗金物品 是共享装备编辑器体系里的复制联动型模块。它的入口和基础字段渲染都与其它装备文件一致,但复制逻辑比普通基础物品更复杂。复制暗金时,代码会生成随机新 code,复制名称/描述键,必要时联动复制基础底材,并对暗金等级字段做默认修正。这个实现说明项目作者并不是把 UniqueItems.txt 当成普通表格处理,而是针对暗金物品的底材依赖关系做了增强。

核心实现结构表如下。

实现层 核心文件/代码 作用说明 设计意义
页面入口层 ItemEditTab.vue / ItemEquipUniqueTab.vue 把"暗金物品"挂入装备编辑页,并传入 UniqueItems.txt 保持模块入口极简
字段解析层 currentItemEquipFields header + row + colMeta 转成字段对象 页面不直接处理原始二维数组
分组配置层 itemEquip.grouping.json / itemEquipGrouping.js 按列名、范围组和说明文本归类字段 将词条、掉落、价格、显示字段分组
复制模式层 isCopyModeFile / handleAddOrCopyItemEquipRow 暗金页走复制模式 适合基于已有暗金快速扩展新条目
暗金默认值层 applyUniqueDefaultLevels 复制暗金时重置 lvllvl req 避免新暗金继承不合适等级
底材联动层 cloneBaseItemRowByCode 复制暗金时联动复制底材 保持暗金 code 与基础装备 code 可匹配
描述联动层 saveItemEquipAndDescription 保存暗金表并按条件保存描述 把数据和文本入口放在同一操作中
保存链路层 saveItemEquipFile / py_save_table_json / save_table_json 整表序列化、本地落盘、备份生成 保证保存完整性和可回滚性

页面入口代码

vue 复制代码
<template>
  <section class="item-edit-section">
    <div class="settings-tabs">
      <button
        v-for="file in itemEquipFileTabs"
        :key="`equip-file-${file.file}`"
        class="settings-tab"
        :class="{ active: itemEquipState.fileName === file.file }"
        @click="switchItemEquipFile(file.file)"
      >
        {{ file.label }}
      </button>
    </div>
    <section class="item-edit-card">
      <ItemEquipWeapons v-if="itemEquipState.fileName === 'weapons.txt'" />
      <ItemEquipArmor v-else-if="itemEquipState.fileName === 'armor.txt'" />
      <ItemEquipBelts v-else-if="itemEquipState.fileName === 'belts.txt'" />
      <ItemEquipMisc v-else-if="itemEquipState.fileName === 'misc.txt'" />
      <ItemEquipSets v-else-if="itemEquipState.fileName === 'Sets.txt'" />
      <ItemEquipSetItems v-else-if="itemEquipState.fileName === 'SetItems.txt'" />
      <ItemEquipUnique v-else />
    </section>
  </section>
</template>
vue 复制代码
<template>
  <ItemEquipEditor file-name="UniqueItems.txt" />
</template>

<script setup>
import ItemEquipEditor from '../ItemEquipEditor.vue'
</script>

暗金页的包装方式非常轻量。装备编辑页签中存在 UniqueItems.txt,标签为"暗金物品";进入该页后,最终渲染的是 ItemEquipUniqueTab.vue,它只负责把 UniqueItems.txt 交给通用编辑器。当前模块的差异不在页面入口层,而在字段元数据、复制逻辑和底材联动层。

字段解析与分组代码

js 复制代码
const currentItemEquipFields = computed(() => {
  const rowIndex = currentItemEquipRowIndex.value
  if (rowIndex < 0) return []

  const row = itemEquipState.rows[rowIndex] || []
  const hasMap = Object.keys(itemEquipState.colMeta || {}).length > 0

  return itemEquipState.header
    .map((col, index) => {
      const meta = itemEquipColMeta(col)
      const zh = (meta?.zh || '').trim()

      return {
        index,
        colName: col,
        en: col,
        zh,
        meta,
        tooltip: itemEquipColTooltip(col),
        value: row[index] || '',
      }
    })
    .filter((field) => {
      if (!hasMap) return true
      return field.zh.length > 0
    })
})
js 复制代码
export function resolveItemEquipGroupByField(field) {
  const colName = String(field?.colName || '').trim()
  const meta = field?.meta || null
  const rangeGroup = normKey(meta?.range_group || '')
  const key = normKey(colName)
  const desc = normKey(`${meta?.desc_zh || ''} ${meta?.desc_en || ''} ${meta?.zh || ''}`)

  return resolveGroupByRule(key, rangeGroup, desc) || ITEM_EQUIP_GROUP_FALLBACK
}
json 复制代码
{
  "order": [
    "基础标识",
    "需求与价格",
    "攻防属性",
    "掉落与生成",
    "显示与背包",
    "镶嵌与孔",
    "技能与词缀",
    "词条属性",
    "其他"
  ],
  "fallback": "其他",
  "rules": [
    {
      "group": "词条属性",
      "range_groups": [
        "prop1 (to prop12)",
        "par1 (to par12)",
        "min1 (to min12)",
        "max1 (to max12)"
      ]
    },
    {
      "group": "基础标识",
      "columns": ["name", "index", "code", "namestr", "version", "*id", "id", "carry1"]
    },
    {
      "group": "需求与价格",
      "includes": ["req", "level", "cost"],
      "desc_keywords": ["需求", "价格"]
    },
    {
      "group": "掉落与生成",
      "includes": ["spawn", "rarity", "prob", "drop"],
      "desc_keywords": ["掉落", "生成"]
    }
  ]
}

当前暗金页没有把 UniqueItems.txt 的每一列写死在模板中。控制器会把当前行转成统一字段对象,字段对象再结合 UniqueItems.txt.columns.json 中的 zhdesc_zhdesc_enrange_group 进入公共分组解析。prop1~12par1~12min1~12max1~12 依靠 range_group 聚合到"词条属性",rarity 和掉落相关说明会进入掉落类分组,cost multcost add 等字段会被归入价格相关区域或 fallback 分组。这样处理之后,暗金表虽然列多,但页面仍然保持统一的分组编辑体验。

从当前代码能够确认,unique 目录下存在 uniqueGrouping.jsunique.grouping.jsonunique.layout.json,但主编辑链路没有直接引用它们。当前运行时主要依赖公共 itemEquipGrouping

复制与暗金默认等级代码

js 复制代码
const isCopyModeFile = computed(() => {
  const file = normKey(itemEquipState.fileName)
  return file === 'uniqueitems.txt' || file === 'setitems.txt' || file === 'sets.txt'
})

const addOrCopyActionLabel = computed(() =>
  (isCopyModeFile.value || isBaseItemFile.value) ? '复制' : '新增',
)

function applyUniqueDefaultLevels(header, row) {
  const findCol = (names) => {
    for (const name of names) {
      const idx = header.findIndex((h) => normKey(h) === normKey(name))
      if (idx >= 0) return idx
    }
    return -1
  }
  const lvlIndex = findCol(['lvl'])
  if (lvlIndex >= 0) row[lvlIndex] = '80'
  const lvlReqIndex = findCol(['lvl req', 'lvlreq'])
  if (lvlReqIndex >= 0) row[lvlReqIndex] = '65'
}
js 复制代码
async function handleAddOrCopyItemEquipRow() {
  if (isCopyModeFile.value || isBaseItemFile.value) {
    const rows = Array.isArray(itemEquipState.rows) ? itemEquipState.rows : []
    const header = Array.isArray(itemEquipState.header) ? itemEquipState.header : []
    if (!rows.length || !header.length) {
      addItemEquipRow()
      return
    }

    const current = getActiveRowIndex()
    const sourceIndex = current >= 0 && current < rows.length ? current : 0
    const sourceRow = Array.isArray(rows[sourceIndex]) ? rows[sourceIndex] : []
    const clone = [...sourceRow]
    while (clone.length < header.length) clone.push('')

    const file = normKey(itemEquipState.fileName)
    const shouldCloneBaseMaterial = file === 'uniqueitems.txt' || file === 'setitems.txt'
    const keyColIndex = resolveCopyNameKeyColIndex(header)
    const codeColIndex = resolveCopyCodeColIndex(header)
    const sourceKey = keyColIndex >= 0 ? String(sourceRow?.[keyColIndex] || '').trim() : ''
    const targetKey = keyColIndex >= 0 ? buildCloneNameKey(sourceKey, rows, keyColIndex) : ''
    const sourceCode = codeColIndex >= 0 ? String(sourceRow?.[codeColIndex] || '').trim() : ''

    let targetCode = ''
    if (codeColIndex >= 0) {
      if (shouldCloneBaseMaterial) {
        targetCode = await buildRandomCloneCode(sourceCode)
      } else {
        targetCode = buildCloneCode(sourceCode, rows, codeColIndex)
      }
    }

    if (keyColIndex >= 0 && targetKey) clone[keyColIndex] = targetKey
    if (codeColIndex >= 0 && targetCode) clone[codeColIndex] = targetCode

    const idColIndex = resolveIdColIndex(header)
    if ((file === 'uniqueitems.txt' || file === 'setitems.txt') && idColIndex >= 0) {
      clone[idColIndex] = buildNextNumericId(rows, idColIndex)
    }
    if (file === 'uniqueitems.txt') {
      applyUniqueDefaultLevels(header, clone)
    }

    const insertAt = rows.length
    itemEquipState.rows.push(clone)
    itemEquipState.sourceRows.push([...clone])
    itemEquipState.activeRowIndex = insertAt
  }
}

暗金页被 isCopyModeFile 识别为复制模式文件,因此按钮显示为"复制"。复制逻辑会克隆当前行、生成目标 key、生成目标 code,并在 UniqueItems.txt 场景下调用 applyUniqueDefaultLevels。从源码能够确认,复制暗金时 lvl 会被设置为 80lvl req 会被设置为 65。这不是所有装备文件共有的行为,而是暗金复制路径上的专门处理。

底材联动复制代码

js 复制代码
async function cloneBaseItemRowByCode(sourceCode, targetCode, options = {}) {
  const source = String(sourceCode || '').trim()
  const target = String(targetCode || '').trim()
  if (!source || !target || normKey(source) === normKey(target)) {
    return { ok: false, reason: 'source/target code 无效或相同' }
  }
  if (!state.excelPath) {
    return { ok: false, reason: 'TXT目录未配置' }
  }

  const files = ['weapons.txt', 'armor.txt', 'misc.txt']
  for (const file of files) {
    const result = await loadTableData(state.excelPath, file, { force: false })
    const header = Array.isArray(result?.header) ? result.header : []
    const rows = cloneRows(Array.isArray(result?.rows) ? result.rows : [])
    const codeIndex = header.findIndex((h) => normKey(h) === 'code')
    if (codeIndex < 0 || !rows.length) continue

    const sourceIndex = rows.findIndex((row) => normKey(row?.[codeIndex]) === normKey(source))
    if (sourceIndex < 0) continue

    const exists = rows.some((row) => normKey(row?.[codeIndex]) === normKey(target))
    if (exists) {
      return { ok: true, file, inserted: false, reason: `${target} 已存在` }
    }

    const sourceRow = rows[sourceIndex] || []
    const cloned = [...sourceRow]
    while (cloned.length < header.length) cloned.push('')
    cloned[codeIndex] = target

    const namestrIndex = header.findIndex((h) => normKey(h) === 'namestr')
    if (namestrIndex >= 0) {
      cloned[namestrIndex] = target
    }

    for (const colName of ['normcode', 'ubercode', 'ultracode']) {
      const idx = header.findIndex((h) => normKey(h) === colName)
      if (idx < 0) continue
      if (normKey(cloned[idx]) === normKey(source)) cloned[idx] = target
    }

    rows.push(cloned)

    await invoke('py_save_table_json', {
      folder: state.excelPath,
      file,
      encoding: String(result?.encoding || 'utf-8'),
      headerJson: JSON.stringify(header),
      rowsJson: JSON.stringify(rows),
      backupPath: backupPath.value || buildBackupPath(state.modPath),
    })

    setCachedTableResult(state.excelPath, file, {
      encoding: String(result?.encoding || 'utf-8'),
      header: [...header],
      rows: cloneRows(rows),
      row_count: rows.length,
      path: joinExcelFilePath(state.excelPath, file),
    })

    return { ok: true, file, inserted: true }
  }

  return { ok: false, reason: `未在 weapons/armor/misc 找到基础 code: ${source}` }
}

暗金物品的 code 字段必须匹配基础底材文件中的 code。当前复制逻辑会在暗金复制时触发 cloneBaseItemRowByCode,到 weapons.txtarmor.txtmisc.txt 中查找源 code。如果找到对应底材,就复制该底材行并把 code 替换为目标 code;如果存在 namestr,还会让 namestr 与目标 code 对齐;如果 normcodeubercodeultracode 自引用源 code,也会同步替换为目标 code。这个设计可以减少复制暗金后底材 code 不存在的问题。

暗金/套装联动复制代码

js 复制代码
async function cloneUniqueOrSetItemRowByCode(sourceCode, targetCode, options = {}) {
  const source = String(sourceCode || '').trim()
  const target = String(targetCode || '').trim()
  if (!source || !target || normKey(source) === normKey(target)) {
    return { ok: false, reason: 'source/target code 无效或相同' }
  }
  if (!state.excelPath) {
    return { ok: false, reason: 'TXT目录未配置' }
  }

  const files = ['UniqueItems.txt', 'SetItems.txt']
  const summaries = []
  let clonedAny = false

  for (const file of files) {
    const result = await loadTableData(state.excelPath, file, { force: false })
    const header = Array.isArray(result?.header) ? result.header : []
    const rows = cloneRows(Array.isArray(result?.rows) ? result.rows : [])
    const codeIndex = header.findIndex((h) => normKey(h) === 'code')
    if (codeIndex < 0 || !rows.length) continue

    const sourceIndexes = rows
      .map((row, idx) => ({ idx, code: normKey(row?.[codeIndex]) }))
      .filter((item) => item.code === normKey(source))
      .map((item) => item.idx)

    if (!sourceIndexes.length) continue
    if (sourceIndexes.length > 1) {
      summaries.push(`${file}: 命中 ${sourceIndexes.length} 条,未自动克隆(避免批量误复制)`)
      continue
    }

    const exists = rows.some((row) => normKey(row?.[codeIndex]) === normKey(target))
    if (exists) {
      summaries.push(`${file}: ${target} 已存在`)
      continue
    }

    const sourceIndex = sourceIndexes[0]
    const sourceRow = rows[sourceIndex] || []
    const cloned = [...sourceRow]
    while (cloned.length < header.length) cloned.push('')
    cloned[codeIndex] = target

    const idColIndex = resolveIdColIndexInHeader(header)
    if (idColIndex >= 0) {
      cloned[idColIndex] = nextRowIdByColumn(rows, idColIndex)
    }
    if (normKey(file) === 'uniqueitems.txt') {
      applyUniqueCloneDefaultLevels(header, cloned)
    }

    rows.push(cloned)

    await invoke('py_save_table_json', {
      folder: state.excelPath,
      file,
      encoding: String(result?.encoding || 'utf-8'),
      headerJson: JSON.stringify(header),
      rowsJson: JSON.stringify(rows),
      backupPath: backupPath.value || buildBackupPath(state.modPath),
    })

    setCachedTableResult(state.excelPath, file, {
      encoding: String(result?.encoding || 'utf-8'),
      header: [...header],
      rows: cloneRows(rows),
      row_count: rows.length,
      path: joinExcelFilePath(state.excelPath, file),
    })

    clonedAny = true
    summaries.push(`${file}: 已克隆(${source} -> ${target},末尾追加)`)
  }

  if (!clonedAny) {
    return { ok: false, reason: summaries.join(';') || `未在 UniqueItems/SetItems 找到 code: ${source}` }
  }
  appendLog(`已联动复制暗金/套装条目: ${summaries.join(';')}`)
  return { ok: true, summary: summaries }
}

这段函数主要用于基础物品复制时反向联动暗金和套装件。虽然当前文章主题是暗金页,但它说明了整个装备编辑体系对 code 关系的重视。函数会在 UniqueItems.txtSetItems.txt 中寻找唯一命中的 source code,避免多条命中时批量误复制,并在复制 UniqueItems.txt 时应用暗金默认等级。当前暗金模块与底材、套装件之间并不是孤立关系,而是通过 code 建立了可追踪的复制链路。

读取与保存代码

js 复制代码
async function loadItemEquipFile(force = false) {
  if (!state.excelPath) {
    setItemEquipStatus('请先在文件配置定位 TXT 目录', 'error')
    return
  }

  try {
    itemEquipState.loading = true
    const file = itemEquipState.fileName || 'weapons.txt'
    const result = await loadTableData(state.excelPath, file, { force })

    itemEquipState.fileName = file
    itemEquipState.encoding = result.encoding || 'utf-8'
    itemEquipState.header = result.header || []
    itemEquipState.rows = (result.rows || []).map((row) => [...row])
    itemEquipState.sourceRows = cloneRows(result.rows || [])

    await loadItemEquipColumnMapping(file)
    await loadItemEquipSkillOptions()
    await loadTreasureClassNameOptions()
    rebuildItemEquipRowSearchCache()

    setItemEquipStatus(`已加载 ${file},共 ${itemEquipState.rows.length} 行`, 'ok')
    appendLog(`装备编辑已加载: ${file}`)
  } catch (error) {
    setItemEquipStatus(`加载失败: ${String(error)}`, 'error')
  } finally {
    itemEquipState.loading = false
  }
}
js 复制代码
async function saveItemEquipFile() {
  if (!state.excelPath || !itemEquipState.header.length) {
    setItemEquipStatus('请先加载道具文件', 'error')
    return false
  }

  try {
    itemEquipState.loading = true

    const hiddenColIndexes = itemEquipState.header
      .map((col, index) => ({ col, index }))
      .filter(({ col }) => {
        const meta = itemEquipColMeta(col)
        if (!meta) return false
        return String(meta?.zh || '').trim().length === 0
      })
      .map((item) => item.index)

    const sourceRows = cloneRows(itemEquipState.sourceRows)
    const rowsForSave = cloneRows(itemEquipState.rows).map((row, rowIndex) => {
      const source = sourceRows[rowIndex]
      if (!Array.isArray(source)) return row
      for (const colIndex of hiddenColIndexes) {
        row[colIndex] = String(source[colIndex] ?? '')
      }
      return row
    })

    const result = await invoke('py_save_table_json', {
      folder: state.excelPath,
      file: itemEquipState.fileName,
      encoding: itemEquipState.encoding,
      headerJson: JSON.stringify(itemEquipState.header),
      rowsJson: JSON.stringify(rowsForSave),
      backupPath: backupPath.value || buildBackupPath(state.modPath),
    })

    if (typeof syncSqliteTxtFile === 'function') {
      await syncSqliteTxtFile(itemEquipState.fileName, { logPrefix: '装备编辑保存后 SQLite' })
    }

    setItemEquipStatus(`保存成功: ${itemEquipState.fileName}`, 'ok')
    itemEquipState.rows = cloneRows(rowsForSave)
    itemEquipState.sourceRows = cloneRows(rowsForSave)
    setCachedTableResult(state.excelPath, itemEquipState.fileName, {
      encoding: itemEquipState.encoding,
      header: [...itemEquipState.header],
      rows: cloneRows(rowsForSave),
      row_count: itemEquipState.rows.length,
      path: joinExcelFilePath(state.excelPath, itemEquipState.fileName),
    })

    appendLog(`装备编辑保存成功: ${itemEquipState.fileName}`)
    return true
  } catch (error) {
    setItemEquipStatus(`保存失败: ${String(error)}`, 'error')
    return false
  } finally {
    itemEquipState.loading = false
  }
}
rust 复制代码
pub fn save_table_json(
    folder: String,
    file: String,
    encoding: String,
    header_json: String,
    rows_json: String,
    backup_path: Option<String>,
) -> Result<Value, String> {
    let path = table_path(&folder, &file);
    let header_values: Vec<Value> =
        serde_json::from_str(&header_json).map_err(|error| format!("invalid header: {error}"))?;
    let rows_values: Vec<Vec<Value>> =
        serde_json::from_str(&rows_json).map_err(|error| format!("invalid rows: {error}"))?;
    let header: Vec<String> = header_values
        .into_iter()
        .map(|v| v.to_string().trim_matches('\"').to_string())
        .collect();
    let rows: Vec<Vec<String>> = rows_values
        .into_iter()
        .map(|row| {
            row.into_iter()
                .map(|v| match v {
                    Value::String(s) => s,
                    other => other.to_string(),
                })
                .collect()
        })
        .collect();
    let backup_dir = normalize_backup_dir(backup_path.as_deref())?;
    let backup_path = write_table(&path, &header, &rows, &encoding, Some(&backup_dir))?;
    Ok(json!({
        "file": file.trim(),
        "path": path.to_string_lossy(),
        "encoding": encoding,
        "backup_path": backup_path,
        "backup_dir": backup_dir.to_string_lossy(),
        "row_count": rows.len()
    }))
}

暗金页的读取和保存仍然走通用链路。读取时加载 UniqueItems.txt、列元数据、技能选项和掉落类名。保存时先恢复隐藏列,再把 headerrows 序列化交给 py_save_table_json。Rust 侧负责 JSON 反序列化、按原编码写回 TXT,并返回备份信息。当前代码能够确认这是本地 Tauri 文件写回链路,不是远程 HTTP 接口。

操作演示

从使用路径看,暗金物品页适合处理"基于底材的唯一装备扩展"。进入装备编辑后切到"暗金物品",页面会加载 UniqueItems.txt,顶部用于搜索条目,左侧用于切换分组,右侧用于编辑当前暗金的掉落、等级、基础 code、显示覆盖和词条属性。复制暗金时,工具会尝试处理名称、code、底材和默认等级,明显比普通新增空白行更贴合暗金制作流程。

操作演示表如下。

操作阶段 界面表现 操作动作 可确认结果
模块进入 装备编辑顶部显示多个文件页签 点击"暗金物品" itemEquipState.fileName 切到 UniqueItems.txt,挂载暗金包装页
初次读取 页面开始加载当前文件 自动读取或点击刷新 loadItemEquipFile 读取 UniqueItems.txt,生成表头、行数据、列元数据和搜索缓存
条目检索 顶部显示数据检索框 输入名称、code 或指定字段关键词 根据当前搜索规则筛选暗金条目
条目切换 条目选择器列出暗金记录 点击目标记录 activeRowIndex 切到目标行,右侧字段同步刷新
编辑基础标识 基础标识组显示 indexcodeversioncarry1 等字段 修改键值、基础 code 或携带限制 当前暗金定义行被更新
编辑掉落与等级 掉落和需求相关分组显示 enabledraritylvllvl req 修改数值或开关 暗金随机掉落、稀有度和使用门槛被更新
编辑显示覆盖 显示相关分组显示 invtransforminvfileflippyfiledropsound 等字段 修改资源或颜色字段 暗金显示覆盖参数被更新
编辑词条属性 词条属性组显示 prop/par/min/max 系列字段 设置属性 code、参数和数值范围 暗金固定词条被更新
复制暗金条目 左侧按钮显示"复制" 点击复制 当前暗金行被复制,新 code 与新 key 会被尝试生成
复制默认等级 复制暗金时触发专用逻辑 无需额外操作 新行 lvl 默认设为 80lvl req 默认设为 65
底材联动 暗金复制时根据 code 查找基础底材 复制过程中自动尝试 如命中 weapons/armor/misc,会联动追加目标底材行
描述编辑 分组中可出现"装备描述" 编辑名称或描述文本并保存 当前名称或描述键可通过联合保存入口写入
保存数据 点击保存按钮 保存当前修改 py_save_table_json 写回 UniqueItems.txt 并生成备份
缓存同步 保存后页面状态保持最新 再次读取或切换文件 前端缓存更新,避免旧数据回显
游戏内验证 返回游戏环境查看暗金掉落或属性表现 外部验证 从当前代码能够确认文件已写回,运行时生效时机仍依赖具体环境

这个操作流的价值在于把暗金编辑从"手动复制一行表格"提升到"带底材联动的结构化复制"。制作新暗金时,复制已有条目通常比新增空行更稳定,因为属性、显示、掉落和基础 code 的结构可以保留,再针对新目标逐步调整。

总结

装备编辑-暗金物品 是这套暗黑破坏神2 MOD 修改工具中非常核心的装备编辑模块。它以 UniqueItems.txt 为目标文件,处理暗金物品的掉落启用、稀有度、等级门槛、基础 code、显示覆盖、价格修正和最多 12 组固定词条。相比武器、防具这类底材表,它更强调"在底材之上定义唯一装备规则"。

从开发实现看,暗金模块仍然复用共享装备编辑器,但复制链路做了明显增强。UniqueItems.txt 被识别为复制模式文件,复制时会生成新 key 和 code,设置暗金默认等级,尝试联动复制基础底材,并复制相关名称/描述键。字段展示则依赖 UniqueItems.txt.columns.json 和公共 itemEquipGrouping,保存继续走 Tauri 本地 TXT 写回。这样的结构既复用了通用编辑能力,又针对暗金物品的底材依赖关系做了专门处理。

相关推荐
Mr数据杨4 小时前
暗黑破坏神2 MOD修改工具装备编辑武器物品
mod·游戏工具·暗黑破坏神2重置版
Mr数据杨4 小时前
暗黑破坏神2 MOD修改工具游戏设置怪物等级
mod·游戏工具·暗黑破坏神2重置版
cqbzcsq2 个月前
MC Forge 1.20.1 mod开发学习笔记(战利品、标签、配方)
java·笔记·学习·mod·mc
cqbzcsq2 个月前
MC Forge1.20.1 mod开发学习笔记(个人向)
笔记·学习·mod·mc·forge
中二病码农不会遇见C++学姐3 个月前
文明六mod制作
mod
金山毒霸电脑医生3 个月前
植物大战僵尸融合版下载与安装教程:PC/安卓/iOS 全面指南
游戏·ios·植物大战僵尸·植物大战僵尸融合版·游戏工具
njsgcs3 个月前
sifumod经验3 水管臂问题
mod
长城20247 个月前
求余运算和数学模运算的知识了解
mod·取余·求余·求模·数学模运算·mod运算
小草cys8 个月前
Minecraft 1.18.2 或更高版本(如1.19.2、1.20.1)选择模组mod加载器
mod·minecraft