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

在暗黑破坏神2 MOD 制作中,暗金物品往往是装备系统平衡的重点。一个暗金条目既要绑定某个底材 code,又要配置固定词条,还要处理名称、图标、掉落门槛和携带限制。与普通底材不同,暗金物品不是单纯改一组基础攻防数值,而是在基础物品之上叠加一组唯一化规则。因此,将 UniqueItems.txt 放进装备编辑页并单独做成"暗金物品"入口,有助于把底材编辑和唯一属性编辑区分开来。
文章目录
文件说明
装备编辑-暗金物品 模块由页面入口、通用装备编辑器、字段元数据、公共分组配置、复制联动逻辑和 Tauri 本地写回链路共同组成。页面包装文件很薄,核心逻辑集中在 ItemEquipEditor.vue、useHomePageController.js 和 itemEquipHelpers.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 | 装备数据读取、保存和复制联动 | 负责 loadItemEquipFile、saveItemEquipFile、cloneBaseItemRowByCode、cloneUniqueOrSetItemRowByCode |
数据交互核心 |
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_table、py_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~12、par1~12、min1~12、max1~12 会聚合到词条属性 |
提升词条字段可读性 |
isCopyModeFile |
ItemEquipEditor.vue |
判断复制模式文件 | UniqueItems.txt 按钮显示"复制" |
决定复制当前条目而不是新增空白行 |
applyUniqueDefaultLevels |
ItemEquipEditor.vue |
暗金复制时设置默认等级 | 复制暗金条目时隐式执行 | 将 lvl 设为 80、lvl req 设为 65 |
cloneBaseItemRowByCode |
itemEquipHelpers.js |
按 code 联动复制基础底材 | 暗金复制链路中可能触发 | 在 weapons/armor/misc 中追加对应底材 |
cloneUniqueOrSetItemRowByCode |
itemEquipHelpers.js |
按 code 联动复制暗金/套装条目 | 基础物品复制时可反向联动 | 复制 UniqueItems.txt 或 SetItems.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.txt、armor.txt 或 misc.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 |
复制暗金时重置 lvl 与 lvl 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 中的 zh、desc_zh、desc_en、range_group 进入公共分组解析。prop1~12、par1~12、min1~12、max1~12 依靠 range_group 聚合到"词条属性",rarity 和掉落相关说明会进入掉落类分组,cost mult、cost add 等字段会被归入价格相关区域或 fallback 分组。这样处理之后,暗金表虽然列多,但页面仍然保持统一的分组编辑体验。
从当前代码能够确认,unique 目录下存在 uniqueGrouping.js、unique.grouping.json 和 unique.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 会被设置为 80,lvl 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.txt、armor.txt、misc.txt 中查找源 code。如果找到对应底材,就复制该底材行并把 code 替换为目标 code;如果存在 namestr,还会让 namestr 与目标 code 对齐;如果 normcode、ubercode、ultracode 自引用源 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.txt 和 SetItems.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、列元数据、技能选项和掉落类名。保存时先恢复隐藏列,再把 header 和 rows 序列化交给 py_save_table_json。Rust 侧负责 JSON 反序列化、按原编码写回 TXT,并返回备份信息。当前代码能够确认这是本地 Tauri 文件写回链路,不是远程 HTTP 接口。
操作演示
从使用路径看,暗金物品页适合处理"基于底材的唯一装备扩展"。进入装备编辑后切到"暗金物品",页面会加载 UniqueItems.txt,顶部用于搜索条目,左侧用于切换分组,右侧用于编辑当前暗金的掉落、等级、基础 code、显示覆盖和词条属性。复制暗金时,工具会尝试处理名称、code、底材和默认等级,明显比普通新增空白行更贴合暗金制作流程。
操作演示表如下。
| 操作阶段 | 界面表现 | 操作动作 | 可确认结果 |
|---|---|---|---|
| 模块进入 | 装备编辑顶部显示多个文件页签 | 点击"暗金物品" | itemEquipState.fileName 切到 UniqueItems.txt,挂载暗金包装页 |
| 初次读取 | 页面开始加载当前文件 | 自动读取或点击刷新 | loadItemEquipFile 读取 UniqueItems.txt,生成表头、行数据、列元数据和搜索缓存 |
| 条目检索 | 顶部显示数据检索框 | 输入名称、code 或指定字段关键词 | 根据当前搜索规则筛选暗金条目 |
| 条目切换 | 条目选择器列出暗金记录 | 点击目标记录 | activeRowIndex 切到目标行,右侧字段同步刷新 |
| 编辑基础标识 | 基础标识组显示 index、code、version、carry1 等字段 |
修改键值、基础 code 或携带限制 | 当前暗金定义行被更新 |
| 编辑掉落与等级 | 掉落和需求相关分组显示 enabled、rarity、lvl、lvl req |
修改数值或开关 | 暗金随机掉落、稀有度和使用门槛被更新 |
| 编辑显示覆盖 | 显示相关分组显示 invtransform、invfile、flippyfile、dropsound 等字段 |
修改资源或颜色字段 | 暗金显示覆盖参数被更新 |
| 编辑词条属性 | 词条属性组显示 prop/par/min/max 系列字段 |
设置属性 code、参数和数值范围 | 暗金固定词条被更新 |
| 复制暗金条目 | 左侧按钮显示"复制" | 点击复制 | 当前暗金行被复制,新 code 与新 key 会被尝试生成 |
| 复制默认等级 | 复制暗金时触发专用逻辑 | 无需额外操作 | 新行 lvl 默认设为 80,lvl 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 写回。这样的结构既复用了通用编辑能力,又针对暗金物品的底材依赖关系做了专门处理。