暗黑破坏神2 MOD修改工具游戏设置怪物等级

游戏设置-怪物等级 对应的是 monlvl.txt 这张全局怪物等级曲线表。它和 Levels.txt 里按地图配置怪物等级的思路不同,也和 monstats.txt 里逐个怪物定义生命、抗性、伤害的思路不同。这个模块处理的是"等级区间对应的整套成长倍率",也就是怪物在不同等级下的防御、命中、生命、伤害、经验等整列数据如何延展。对于暗黑破坏神2 MOD 工具来说,这类数据一旦改动,影响范围往往不是单个地图或单个怪物,而是整条怪物强度曲线。

也正因为影响面很大,这个功能值得被拆成独立编辑页。当前源码没有把 monlvl.txt 的三十多个列字段直接铺成大表单,而是把界面压缩成"当前等级显示 + 目标等级输入 + 刷新/应用"这一类工具型交互。页面层负责表达修改意图,真正的行扩展、行裁剪、编码读取和文件回写交给 Tauri 命令与 Rust 处理层完成。这种拆法很适合全局型规则表:一方面能减少误操作,另一方面也能把复杂算法从前端表单中剥离出来。

文章目录

文件说明

从模块组织方式来看,游戏设置-怪物等级 不是一个孤立的 Vue 页面,而是由页面壳层、设置页签路由、工具页组件、状态控制器、通用读取缓存、字段元数据、分组配置、命令桥接和 Rust 写回算法共同组成。和 整体难度佣兵雇佣 这类整表编辑页相比,怪物等级模块更像一个"参数化生成器":界面只输入目标最大等级,后端负责把 monlvl.txt 扩展到目标上限或裁剪掉超出的等级行。

模块文件职责表如下。表中既列出运行时会直接参与当前功能的文件,也保留与该模块规范相关的 grouping、layout、columns 等配置文件。需要注意的是,当前怪物等级页并没有直接渲染完整字段网格,因此部分配置文件处于"已定义、当前页面未直接消费"的状态,这一点在源码中可以明确区分出来。

文件名 文件类型 模块职责 与界面或数据处理的关系 备注
src/pages/HomePage.vue Vue 主页面壳层 侧栏切到"游戏设置"时挂载 SettingsTab,顶部显示 settingsLoadedMeta 当前模块的上层容器
src/modules/settings/SettingsTab.vue Vue 游戏设置总入口 子页签切到 monLevel 时渲染 SettingsMonLevelTab,并同步 levelToolTab='mon' 模块路由层
src/modules/settings/monLevel/SettingsMonLevelTab.vue Vue 怪物等级工具页 展示当前等级、目标等级输入框、刷新按钮、应用按钮 当前模块的直接界面
src/modules/commonControls/NumberStepper.vue Vue 通用数字步进组件 为目标等级输入提供 +/-、回车提交和数值输入事件 公共组件复用
src/pages/home/useHomePageController.js JS 首页控制器与状态装配 定义 levelTools.moncurrentLevelToolsettingsLoadedMeta、页签切换后的自动刷新逻辑 状态层核心
src/pages/home/settingsHelpers.js JS 设置模块通用 helper 读取 monlvl.txt 当前最大等级,调用 py_apply_level_tools 修改文件 读写调用层
src/pages/home/tableDataLoader.js JS 表数据读取缓存 py_load_table 做缓存、去重和失效控制 读取性能层
src/assets/resources/index.json JSON 字段元数据索引 MonLvl.txt.columns.json 纳入统一列描述系统 元数据入口
src/assets/resources/files/MonLvl.txt.columns.json JSON monlvl.txt 字段说明 定义 LevelACHPXP 等列的中文含义和描述 数据说明层
src/modules/settings/monLevel/monLevel.grouping.json JSON 分组配置 定义字段默认归入"基础信息"与"其他" 当前页未直接渲染整表
src/modules/settings/monLevel/monLevel.layout.json JSON 布局顺序配置 定义分组顺序和回退组 当前页未直接渲染整表
src/modules/settings/monLevel/monLevelGrouping.js JS 分组解析封装 通过 groupingFactory 生成字段分组解析器 当前页未直接调用,但属于模块规范
src/modules/commonControls/groupingFactory.js JS 通用分组工厂 把 grouping/layout 配置转换成 resolveInfoByField 等方法 配置解析层
src-tauri/src/main.rs Rust Tauri 命令注册 暴露 py_load_tablepy_apply_level_tools 给前端 invoke 前后端桥接层
src-tauri/src/commands/files.rs Rust 文件读取命令 读取 TXT、保留编码、解析为 header + rows + row_count 返回前端 数据解析层
src-tauri/src/commands/level_tools.rs Rust 等级工具算法 apply_max_mon_level 负责按目标等级扩行、裁行并写回 monlvl.txt 当前模块最核心的写入层

把这些文件按层次拆开后,结构会更清楚。HomePage.vueSettingsTab.vue 属于页面壳层,负责把"游戏设置"和"怪物等级"放到正确的位置。SettingsMonLevelTab.vueNumberStepper.vue 属于界面层,负责表达当前值、输入值与操作按钮。useHomePageController.jssettingsHelpers.jstableDataLoader.js 属于配置与状态层,负责上下文注入、缓存、刷新和命令调用。index.jsonMonLvl.txt.columns.jsonmonLevel.grouping.jsonmonLevel.layout.jsonmonLevelGrouping.js 属于配置层和解析层。main.rsfiles.rslevel_tools.rs 则是数据层和命令层,承担真正的读取、解析、回写、备份工作。

接下来再看真正参与 UI 构建、字段展示、数据读取与保存链路的方法和状态。这个模块不是整表表单页,因此不需要把所有列变量机械罗列出来,重点应该落在"界面看见了什么"和"点击之后会发生什么"。

字段/配置项/方法 所属文件 作用 在界面中的体现 修改后的影响
settingsTab src/modules/settings/SettingsTab.vue 标识当前游戏设置子页签 点击"怪物等级"按钮后切换到当前模块 决定渲染哪个设置页组件
levelToolTab src/modules/settings/SettingsTab.vue / useHomePageController.js 标识当前等级工具类型 切到 monLevel 时会被同步成 mon 决定刷新和应用时操作哪张文件
levelTools.mon.file src/pages/home/useHomePageController.js 定义怪物等级工具对应文件 页面不直接显示字段名,但所有读取和写回都指向 monlvl.txt 直接决定数据源
levelTools.mon.loadedRows src/pages/home/useHomePageController.js 记录当前读取到的行数 顶部 settingsLoadedMeta 会显示"已加载 monlvl.txt,共 X 行" 用于加载状态展示
currentLevelTool.currentText src/pages/home/useHomePageController.js 输出当前最大等级文本 "当前等级"区域直接显示 用于确认当前曲线长度
currentLevelTool.input src/pages/home/useHomePageController.js 保存待应用的目标等级 NumberStepper 输入框显示和回填该值 决定应用时的目标上限
updateLevelInputByKey src/pages/home/useHomePageController.js 处理输入框实时改值 手动输入整数时触发 改变待提交目标
stepLevelInputByKey src/pages/home/useHomePageController.js 处理步进器加减 点击 +/- 时触发 快速调整目标等级
refreshCurrentLevel src/pages/home/settingsHelpers.js 重新读取当前最大等级 点击"刷新"按钮触发 重新加载 monlvl.txt 并更新状态
applyCurrentLevelTool src/pages/home/settingsHelpers.js 应用当前目标等级 点击"应用"按钮或回车触发 调用后端改写 monlvl.txt
settingsLoadedMeta src/pages/home/useHomePageController.js 生成顶部状态文案 页头显示当前模块已加载文件和行数 反馈读取结果
loadTableData src/pages/home/tableDataLoader.js 带缓存读取 TXT 表 页面切换与刷新时不会无意义重复读取 提升读取性能并减少并发请求
invoke('py_load_table') src/pages/home/tableDataLoader.js 间接调用 读取表格数据 界面无感,结果体现在当前等级和加载状态 提供读取链路
invoke('py_apply_level_tools') src/pages/home/settingsHelpers.js 调用 Rust 等级工具 界面无感,结果体现在应用成功与失败提示 提供写回链路
apply_max_mon_level src-tauri/src/commands/level_tools.rs monlvl.txt 执行扩行或裁剪 不直接出现在界面 改变曲线长度和新增行数值

从界面设计角度看,这些状态和方法之所以会出现在当前模块里,是因为页面目标并不是"编辑某一行某一列",而是"把一条曲线整体拉长或缩短"。也就是说,界面需要展示的是当前曲线长度和目标曲线长度,而不是 AC(H)L-XP(N) 之类的逐列细节。细节列仍然存在于 monlvl.txt 内部,只是被收敛到了算法层处理。

由于这个模块更适合从数据字段角度理解,再补一张字段说明表会更直观。这里的字段并非来自想象,而是可以同时从 MonLvl.txt.columns.json 和仓库中的 src-tauri/bak/monlvl.txt_* 备份样本中确认到。元数据文件用合并描述解释了字段含义,备份文件则展示了真实表头是如何展开成多列的。

字段名 中文含义 所属分组 前端展示方式 实际用途说明
Level 等级行标识 基础标识 当前页不逐列展示 后端以首列最大数值判断当前等级上限
AC / AC(N) / AC(H) 普通、噩梦、地狱防御倍率 怪物成长 当前页不逐列展示 扩等级时按末两行差值持续推导
TH / TH(N) / TH(H) 普通、噩梦、地狱命中倍率 怪物成长 当前页不逐列展示 决定更高等级怪物命中成长
HP / HP(N) / HP(H) 普通、噩梦、地狱生命倍率 怪物成长 当前页不逐列展示 决定更高等级怪物生命成长
DM / DM(N) / DM(H) 普通、噩梦、地狱伤害倍率 怪物成长 当前页不逐列展示 决定更高等级怪物伤害成长
XP / XP(N) / XP(H) 普通、噩梦、地狱经验倍率 奖励成长 当前页不逐列展示 决定击杀怪物经验收益
L-AC / L-AC(N) / L-AC(H) 天梯防御倍率 天梯成长 当前页不逐列展示 天梯模式下的防御成长曲线
L-TH / L-TH(N) / L-TH(H) 天梯命中倍率 天梯成长 当前页不逐列展示 天梯模式下的命中成长曲线
L-HP / L-HP(N) / L-HP(H) 天梯生命倍率 天梯成长 当前页不逐列展示 天梯模式下的生命成长曲线
L-DM / L-DM(N) / L-DM(H) 天梯伤害倍率 天梯成长 当前页不逐列展示 天梯模式下的伤害成长曲线
L-XP / L-XP(N) / L-XP(H) 天梯经验倍率 天梯成长 当前页不逐列展示 天梯模式下的经验收益曲线

从当前代码能够确认的内容是,怪物等级页虽然只暴露了一个"新的最大等级"输入框,但背后对应的是一整张多列曲线表。这正是当前模块存在价值的来源:工具使用者不需要手工维护三十多列等级行,工具作者也不需要把整个 monlvl.txt 硬铺到页面上,只要保证"目标上限 -> 曲线扩展/裁剪"的规则稳定即可。

软件开发

从开发视角看,怪物等级模块非常有代表性。它没有走传统后台管理那种"大表单 + 保存整行"的路线,而是把前端压缩成一个极简工具,把算法沉到 Rust 层。这个选择并不是为了省页面代码,而是因为 monlvl.txt 的修改本质不是普通字段编辑,而是"根据目标最大等级重新整理整张曲线表"。一旦把这类逻辑写死在页面里,界面状态、校验、备份、编码写回、缓存失效都会迅速膨胀。

这类设计也能看出整个工具的工程取向。页面壳层负责挂载模块,状态层负责输入和结果,配置层负责统一元数据与分组能力,命令层负责文件桥接,算法层负责业务改写。和整表模块共用的不是表单内容,而是 settingsCtxloadTableDataNumberStepper、Tauri invoke、TXT 解析函数这套基础设施。也就是说,模块级差异体现在写入策略,而不是体现在基础框架上。

核心实现结构表如下。

实现层 核心文件/代码 作用说明 设计意义
页面壳层 HomePage.vue / SettingsTab.vue 让"游戏设置 -> 怪物等级"成为明确可切换的独立页签 把导航和具体功能解耦,降低设置页膨胀风险
交互层 SettingsMonLevelTab.vue 只显示当前值、目标值和刷新/应用按钮 把复杂表格操作收束为单一工具操作
公共组件层 NumberStepper.vue 提供统一的数值输入、步进和回车提交事件 保持输入交互一致,避免每个模块重复造轮子
状态层 useHomePageController.js 管理 levelTools.moncurrentLevelTool、顶部加载提示和页签切换联动 把界面状态从组件模板中抽离
读取层 tableDataLoader.js / files.rs 读取 monlvl.txt,保留编码并解析为 header + rows 统一 TXT 读取方式,兼顾性能和编码安全
命令桥接层 main.rs 注册 py_load_tablepy_apply_level_tools 让前端只关心调用意图,不关心本地文件细节
应用逻辑层 settingsHelpers.js 根据当前工具类型决定读取、校验、调用哪个后端命令 共用一套 helper 覆盖人物、怪物、技能等级工具
后端算法层 level_tools.rs 按目标等级扩展或裁剪 monlvl.txt 并写回 把曲线推导和落盘细节集中到 Rust 侧

页面入口代码

vue 复制代码
const settingsTabComponents = {
  charLevel: SettingsCharLevelTab,
  charStats: SettingsCharStatsTab,
  skillLevel: SettingsSkillLevelTab,
  monLevel: SettingsMonLevelTab,
  level: SettingsMonLevelTab,
  difficultyLevel: SettingsDifficultyLevelTab,
  hireling: SettingsHirelingTab,
}

const activeSettingsTabComponent = computed(() => settingsTabComponents[settingsTab.value] || SettingsCharLevelTab)

function switchSettingsTab(nextTab) {
  settingsTab.value = nextTab
  if (nextTab === 'skillLevel') {
    levelToolTab.value = 'skill'
    return
  }
  if (nextTab === 'monLevel' || nextTab === 'level') {
    levelToolTab.value = 'mon'
  }
}

这段代码说明当前项目并没有把所有设置页堆在一个大组件里,而是通过 settingsTabComponents 做子模块映射。怪物等级页对应 monLevel,同时兼容旧键值 level。切换到该页签时,levelToolTab 会同步成 mon,后续 refreshCurrentLevelapplyCurrentLevelTool 都会以"怪物等级工具"身份运行。这个设计很关键,因为人物等级、怪物等级、技能等级三套工具虽然共享 helper,但目标文件和应用策略并不相同。

模块交互代码

vue 复制代码
<template>
  <section class="level-card">
    <div class="settings-split-layout">
      <aside class="settings-split-side charstats-side">
        <div class="charstats-side-list"></div>
        <div class="settings-split-side-actions charstats-side-actions">
          <button class="warning" @click="refreshCurrentLevel">刷新</button>
          <button class="success" @click="applyCurrentLevelTool">应用</button>
        </div>
      </aside>

      <section class="settings-split-main charstats-main char-exp-main-panel">
        <div class="row level-char-row">
          <label>当前等级</label>
          <div class="current-level-text">{{ currentLevelTool.currentText || '未知' }}</div>

          <label>新的最大等级</label>
          <NumberStepper
            wrapper-class="level-max-step"
            :model-value="currentLevelTool.input"
            type="number"
            min="1"
            step="1"
            placeholder="请输入大于 0 的整数"
            @update:model-value="updateLevelInputByKey(levelToolTab, $event)"
            @step="stepLevelInputByKey(levelToolTab, $event)"
            @enter="applyCurrentLevelTool"
          />
        </div>

        <div v-if="currentLevelTool.statusError" class="level-status error">
          {{ currentLevelTool.statusText || ' ' }}
        </div>
      </section>
    </div>
  </section>
</template>

<script>
import { inject } from 'vue'
import NumberStepper from '../../commonControls/NumberStepper.vue'

export default {
  components: { NumberStepper },
  setup() {
    return inject('settingsTabCtx') || {}
  },
}
</script>

这段模板把模块定位表达得很清楚。页面没有逐列渲染 monlvl.txt,而是只暴露当前等级和目标等级输入。NumberStepper 作为公共组件被直接复用,输入、步进、回车三类事件都接回 settingsTabCtx。界面层的职责只剩下表达当前状态和收集目标值,不承担任何曲线推导逻辑。这种做法让模块非常适合做"全局规则型操作页"。

状态模型与页签联动代码

js 复制代码
const levelTools = reactive({
  char: {
    title: '人物最大等级(experience.txt)',
    file: 'experience.txt',
    current: '',
    input: '',
    statusText: '',
    statusOk: false,
    statusError: false,
    loadedRows: 0,
  },
  mon: {
    title: '怪物等级上限(monlvl.txt)',
    file: 'monlvl.txt',
    current: '',
    input: '',
    statusText: '',
    statusOk: false,
    statusError: false,
    loadedRows: 0,
  },
  skill: {
    title: '技能等级上限(skills.txt)',
    file: 'skills.txt',
    current: '',
    input: '',
    statusText: '',
    statusOk: false,
    statusError: false,
    loadedRows: 0,
  },
})

const currentLevelTool = computed(() => {
  const tool = levelTools[levelToolTab.value] || levelTools.mon
  return {
    ...tool,
    currentText: tool.current === '' ? '未检测' : String(tool.current),
  }
})

watch(settingsTab, async (value) => {
  if (activeTab.value !== 'settings') return

  if (value === 'monLevel') {
    const changed = levelToolTab.value !== 'mon'
    levelToolTab.value = 'mon'
    if (!changed) {
      await refreshCurrentLevel(false)
    }
  }
})

这段代码把三个等级工具收进同一个 levelTools 状态对象里,怪物等级只是其中的 mon 分支。好处在于上下文结构统一,坏处则被控制在很小范围内,因为真正的差异化逻辑放在 settingsHelpers.js。当设置页签切到 monLevel 时,控制器并不会临时创建一套新状态,而是把 levelToolTab 切换到 mon,让后续所有读取和应用逻辑自然落到 monlvl.txt 上。这比为每个等级工具维护独立页面状态更稳定,也更便于扩展。

分组配置与布局代码

js 复制代码
import groupingConfig from './monLevel.grouping.json'
import layoutConfig from './monLevel.layout.json'
import { createGroupingResolver } from '../../commonControls/groupingFactory'

const resolver = createGroupingResolver(groupingConfig, layoutConfig)

export const MONLEVEL_GROUP_ORDER = resolver.GROUP_ORDER
export const MONLEVEL_GROUP_FALLBACK = resolver.GROUP_FALLBACK

export function resolveMonLevelGroupingInfoByField(field) {
  return resolver.resolveInfoByField(field)
}

export function resolveMonLevelGroupByField(field) {
  return resolver.resolveGroupByField(field)
}

export function resolveMonLevelControlByField(field) {
  return resolver.resolveControlByField(field)
}
json 复制代码
{
  "groups": {
    "基础信息": {
      "patterns": [
        ".*"
      ],
      "subgroup_order": [],
      "subgroups": {}
    },
    "其他": {}
  },
  "field_controls": [
    {}
  ]
}
json 复制代码
{
  "order": [
    "基础信息",
    "其他"
  ],
  "fallback": "其他"
}

从当前代码能够确认,这个模块已经具备完整的 grouping/layout 规范文件和解析封装,但当前 SettingsMonLevelTab.vue 并没有直接消费这些分组结果。也就是说,模块规范层已经就位,运行时页面仍然采用工具型极简界面。monLevel.grouping.json 的配置也很克制,几乎等于"所有字段默认归入基础信息"。这恰好反映了模块定位:当前页不是完整表格编辑器,分组配置更像是为后续统一规范预留的接口,而不是当前交互主轴。

读取当前值代码

js 复制代码
async function readLevelCurrent(key, options = {}) {
  const force = options.force !== false

  if (!state.excelPath) {
    const message = '请先在文件配置定位 TXT 目录'
    setLevelToolStatus(key, message, 'error')
    levelToolByKey(key).loadedRows = 0
    appendLog(`等级读取失败: ${message}`)
    return
  }

  const tool = levelToolByKey(key)

  try {
    const result = await loadTableData(state.excelPath, tool.file, { force })
    const rows = result.rows || []
    tool.loadedRows = rows.length

    let current = ''

    if (key === 'mon') {
      const levels = rows
        .map((row) => Number(row[0]))
        .filter((num) => Number.isFinite(num))
      current = levels.length ? String(Math.max(...levels)) : ''
    }

    tool.current = current
    if (!tool.input) {
      tool.input = current || ''
    }

    setLevelToolStatus(key, `当前: ${current || '未知'}`, current ? 'ok' : 'info')
    appendLog(`等级已加载: ${tool.file}`)
    appendLog(`已读取 ${tool.file} 当前等级: ${current || '未知'}`)
  } catch (error) {
    tool.current = ''
    tool.loadedRows = 0
    const message = `读取失败: ${String(error)}`
    setLevelToolStatus(key, message, 'error')
    appendLog(`等级读取失败(${tool.file}): ${String(error)}`)
  }
}

怪物等级的"当前值"读取逻辑非常直接。当前模块并不依赖 MonLvl.txt.columns.json 去算当前上限,而是把 monlvl.txt 读成二维数组后,直接扫描首列 Level 的数值,取最大值作为当前等级上限。这个策略很符合业务本质:对于怪物等级工具来说,界面真正关心的不是某个倍率列的值,而是这张曲线表已经延伸到了哪一级。loadedRows 也在这里被同步更新,页面顶部状态就能显示"已加载 monlvl.txt,共 X 行"。

应用命令调用代码

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

  const tool = levelToolByKey(key)
  const target = Number((tool.input || '').trim())

  if (!Number.isInteger(target) || target <= 0) {
    setLevelToolStatus(key, '请输入大于 0 的整数', 'error')
    return
  }

  const charMax = key === 'char' ? String(target) : '0'
  const monMax = key === 'mon' ? String(target) : '0'
  const skillMax = key === 'skill' ? String(target) : '0'

  try {
    const result = await invoke('py_apply_level_tools', {
      folder: state.excelPath,
      charMax,
      monMax,
      skillMax,
    })

    const changes = Array.isArray(result?.changes) ? result.changes : []
    const changedFiles = changes.map((item) => item?.file).filter(Boolean).join(', ')
    for (const item of changes) {
      if (item?.file) invalidateTableCache(item.file)
    }

    setLevelToolStatus(key, `已更新到 ${target}${changedFiles ? `(${changedFiles})` : ''}`, 'ok')
    appendLog(`等级修改成功: ${tool.file} -> ${target}`)

    await readLevelCurrent(key)

    if (editorState.fileName && normKey(editorState.fileName) === normKey(tool.file)) {
      await openEditorFile()
    }
  } catch (error) {
    setLevelToolStatus(key, `应用失败: ${String(error)}`, 'error')
    appendLog(`等级修改失败: ${String(error)}`)
  }
}

这段逻辑明确区分了三个等级工具的共同入口和不同目标。当前模块场景下,只有 monMax 会被赋值成目标等级,charMaxskillMax 都保持为 0。这意味着前端不会直接改写 monlvl.txt,只会把"目标等级是多少"这个意图交给后端。命令成功后,前端会失效对应文件缓存,再次读取最新值,并在必要时刷新文件编辑器。这种结构比直接在前端拼装整张新表稳健得多。

命令桥接与文件解析代码

rust 复制代码
#[tauri::command]
fn py_load_table(folder: String, file: String) -> Result<Value, String> {
    commands::files::load_table(folder, file)
}

#[tauri::command]
fn py_apply_level_tools(
    folder: String,
    char_max: String,
    mon_max: String,
    skill_max: String,
) -> Result<Value, String> {
    commands::level_tools::apply_level_tools(folder, char_max, mon_max, skill_max)
}
rust 复制代码
pub fn load_table(folder: String, file: String) -> Result<Value, String> {
    let path = table_path(&folder, &file);
    let (text, encoding) = read_text_with_encoding(&path)?;
    let (header, rows) = parse_tab_text(&text);
    if header.is_empty() {
        return Err(format!("header empty: {}", file.trim()));
    }
    let backup_dir = backup_dir()?;
    Ok(json!({
        "folder": PathBuf::from(folder.trim()).to_string_lossy(),
        "file": file.trim(),
        "path": path.to_string_lossy(),
        "encoding": encoding,
        "backup_dir": backup_dir.to_string_lossy(),
        "header": header,
        "rows": rows,
        "row_count": rows.len()
    }))
}

这段代码把前后端的边界画得很清晰。前端只能看到 py_load_tablepy_apply_level_tools 这两个命令名,具体读取和写回细节在 Rust 层完成。load_table 先保留原文件编码,再通过 parse_tab_text 把 TAB 分隔文本拆成 header + rows 的结构返回前端。也就是说,当前模块的"解析器"并不是单独的一段表单转换代码,而是 read_text_with_encoding + parse_tab_text + JSON 返回结构 这条通用链路。怪物等级页是在这条链路的结果之上,再额外计算出"当前最大等级"。

后端等级扩展代码

rust 复制代码
fn apply_max_mon_level(folder: &Path, new_max: i64) -> Result<Value, String> {
    let file = "monlvl.txt";
    let path = folder.join(file);
    let (text, encoding) = read_text_with_encoding(&path)?;
    let (header, mut data) = parse_tab_text(&text);
    let level_idx = 0usize;
    let mut numeric_rows: Vec<(usize, i64)> = data
        .iter()
        .enumerate()
        .filter_map(|(idx, row)| {
            row.get(level_idx)?
                .trim()
                .parse::<i64>()
                .ok()
                .map(|v| (idx, v))
        })
        .collect();
    if numeric_rows.is_empty() {
        return Err("monlvl.txt 閹靛彞绗夐崚鎵搼缁狙嗩攽".to_string());
    }
    numeric_rows.sort_by_key(|(_, level)| *level);
    let max_existing = numeric_rows.last().map(|(_, lvl)| *lvl).unwrap_or(0);
    if new_max < max_existing {
        data = data
            .into_iter()
            .filter(|row| {
                let v = row.get(level_idx).map(|v| v.trim()).unwrap_or("");
                if let Ok(level) = v.parse::<i64>() {
                    level <= new_max
                } else {
                    true
                }
            })
            .collect();
    } else if new_max > max_existing {
        let (last_idx, _) = *numeric_rows
            .last()
            .ok_or_else(|| "monlvl.txt missing level rows".to_string())?;
        let prev_idx = if numeric_rows.len() >= 2 {
            numeric_rows[numeric_rows.len() - 2].0
        } else {
            last_idx
        };
        let last = data[last_idx].clone();
        let prev = data[prev_idx].clone();
        let mut deltas: HashMap<usize, i64> = HashMap::new();
        let mut last_vals: HashMap<usize, i64> = HashMap::new();
        for idx in 1..header.len() {
            deltas.insert(idx, to_i64(&last[idx]) - to_i64(&prev[idx]));
            last_vals.insert(idx, to_i64(&last[idx]));
        }
        for level in (max_existing + 1)..=new_max {
            let mut row = vec![String::new(); header.len()];
            row[level_idx] = level.to_string();
            for idx in 1..header.len() {
                let next = last_vals.get(&idx).copied().unwrap_or(0)
                    + deltas.get(&idx).copied().unwrap_or(0);
                last_vals.insert(idx, next);
                row[idx] = next.to_string();
            }
            data.push(row);
        }
    }
    let backup = write_table(&path, &header, &data, &encoding, None)?;
    Ok(json!({
        "file": file,
        "backup_path": backup
    }))
}

这就是当前模块的核心算法。目标等级小于当前上限时,后端会把 Level 大于目标值的所有行过滤掉;目标等级大于当前上限时,后端会取现有最后两行,按每一列的差值持续向上推导,直到补齐到目标等级。这里真正重要的设计点有两个。其一,新增行不是固定模板复制,而是"末两行差值递推",这能保证曲线延伸符合原有增长趋势。其二,写回时调用的是 write_table(..., None),从当前代码能够确认,这条专用链路并没有走 py_save_table_json 的自定义备份路径参数,而是使用默认备份目录策略。源码里个别错误字符串还存在编码异常,这也是当前实现中可以直接观察到的细节。

综合这几段代码就能理解,当前模块为什么没有把表单写死在页面里。因为真正复杂的不是输入框,而是"如何稳定地改写整张等级曲线表"。如果把所有逻辑都放在 SettingsMonLevelTab.vue,页面不仅要负责校验,还要负责行扩展、编码兼容、备份和缓存刷新,维护成本会非常高。当前实现把这些问题拆散到各自最合适的层级里,前端保持轻量,后端保持确定性,整体结构比直接硬写表单更适合 D2R 这类 TXT 驱动的工具。

从当前代码能够确认的内容主要集中在前端状态层、Tauri 命令桥接、TXT 解析链路和 Rust 文件改写层。monlvl.txt 的读取、扩展、裁剪、回写和备份路径返回都能在源码中定位到。游戏进程是否热重载该文件、改动是否在运行中的游戏里即时生效,这部分仓库中没有完整证据,因此不能当作源码已确认事实来写。

操作演示

就软件使用路径而言,当前模块的操作流程非常清晰。进入"游戏设置"后点击"怪物等级",页面会先围绕 levelToolTab='mon' 建立上下文。点击"刷新"时,前端读取 monlvl.txt 并计算首列最大等级。输入目标值并点击"应用"后,前端只把 monMax 传给后端命令,后端完成整张曲线表的调整。也就是说,界面上虽然只有一个数字输入,但背后改动的是整条等级曲线。

操作演示表如下。表中只写能够从当前代码确认的界面行为和数据变化,不补写仓库中不存在的提示框、接口返回页或游戏内即时表现。

演示环节 界面与动作 源码依据 可确认结果
模块进入 主侧栏点击"游戏设置",子页签点击"怪物等级" HomePage.vue 挂载 SettingsTabSettingsTab.vue 渲染 SettingsMonLevelTab 当前模块页面被激活
初始加载 页面切入后触发状态同步 settingsTab 切到 monLevellevelToolTab 会被设置为 mon 后续刷新和应用都指向 monlvl.txt
读取当前值 点击"刷新"按钮 refreshCurrentLevel -> readLevelCurrent('mon') 读取 monlvl.txt 首列最大值并显示在"当前等级"区域
查看加载状态 观察页头状态文案 settingsLoadedMeta 使用 loadedRows 生成文案 可以确认当前已加载文件名和行数
输入目标值 在 NumberStepper 中输入整数或点击 +/- updateLevelInputByKeystepLevelInputByKey 目标等级写入 currentLevelTool.input
应用更高等级 输入一个大于当前值的整数后点击"应用" applyLevelToolByKey 调用 py_apply_level_tools,Rust 进入扩行分支 monlvl.txt 会按末两行差值继续补齐到目标等级
应用更低等级 输入一个小于当前值的整数后点击"应用" apply_max_mon_level 进入裁剪分支 Level 超出目标值的行会被删除
刷新校验 应用完成后再次点击"刷新" 再次走 readLevelCurrent('mon') 界面上的"当前等级"会回到最新文件状态
编辑器联动 若文件编辑器正打开 monlvl.txt applyLevelToolByKey 内有 openEditorFile() 判断 文件编辑区会同步刷新当前文件内容
缓存失效 应用命令完成后 invalidateTableCache(item.file) 避免后续读取命中旧缓存

再从数据结果的角度看,这个模块并不是只写一个"最大等级数字"。仓库里的 src-tauri/bak/monlvl.txt_2026_02_16_01_45_54 备份样本可以确认真实表头是 Level、AC、AC(N)、AC(H)、L-AC...L-XP(H) 这一整套列,前几行内容也能看出它是一张按等级递增的成长曲线表。这意味着"应用"按钮的语义不是更新单一字段,而是对整张曲线表进行结构性改写。对于 MOD 工具来说,这种工具型封装的价值非常大,因为手工维护三十多列等级行既费时,也容易在高等级扩展场景下出错。

还需要补充一点保存行为边界。当前代码能够确认,怪物等级模块走的是专用命令 py_apply_level_tools,不是整表模块常见的 py_save_table_json。前者在 Rust 层直接调用 write_table 回写并返回备份路径,仓库中的 src-tauri/bak 目录也能看到现成的 monlvl.txt_* 备份文件样本。至于游戏是否在不重启的情况下立即读取了最新 monlvl.txt,当前仓库没有运行时监听或热更新证据,文章只能把结论限定在"文件已写回"和"界面可重新读取验证"这个范围内。

总结

游戏设置-怪物等级 是这个暗黑破坏神2 MOD 修改工具里很典型的一个全局规则型模块。它背后对应的是 monlvl.txt 整张怪物等级成长曲线表,但前端并没有把问题做成巨型表格,而是把操作收敛成"看当前值、设目标值、应用改写"这套工具流。这样的界面取舍并不保守,恰恰体现了工具作者对数据结构的理解:面对一张需要整表推导的曲线表,最合适的交互不是逐格编辑,而是把参数输入与算法写回分层处理。

从开发实现来看,这个模块也能代表整个项目的工程方式。页面壳层负责挂载,控制器负责状态,helper 负责读写调用,通用组件负责输入体验,JSON 配置负责模块规范,Tauri 命令负责桥接,Rust 负责最终算法和文件回写。这样组织带来的好处很直接,新增功能时不需要重新搭建整套基础设施,修改算法时也不必侵入 Vue 页面。对于长期维护 D2R TXT 工具的开发者来说,这种层次分工比把表单写死在页面里更稳,也更容易扩展。

为了避免结论超出材料边界,再用一张表收束本文能够确认和不能确认的部分。

边界类别 内容 依据
源码直接确认 页签挂载关系、当前等级读取方式、py_apply_level_tools 调用链、apply_max_mon_level 的扩行与裁剪逻辑 Vue、JS、Rust 源码可直接定位
源码直接确认 monlvl.txt 真实表头包含 LevelACHPDMXP 及其难度和天梯列 MonLvl.txt.columns.json 与仓库备份样本可相互印证
源码直接确认 应用命令会回写文件并生成备份 write_tablesrc-tauri/bak/monlvl.txt_* 样本可确认
从当前代码能够确认的内容 当前页面是工具型界面,不是整表字段编辑页;分组与布局配置已存在,但当前模板未直接消费 SettingsMonLevelTab.vuemonLevelGrouping.js、JSON 配置对照可确认
当前材料无法直接确认 游戏运行时是否即时热重载 monlvl.txt,是否存在独立后端服务做运行时同步 仓库内未看到对应监听、服务接口或热刷新实现

这也正是当前模块值得单独成文的原因。对于工具使用者来说,它降低了直接编辑曲线表的门槛。对于 MOD 制作者来说,它把全局怪物强度曲线的调整收敛成了可重复、可验证的操作。对于二次开发者来说,它展示了一个很完整的实现范式:Vue 负责表达意图,JS 负责状态和调用,Rust 负责真正的数据规则。这样的模块,不只是"能用",也具备继续扩展和长期维护的开发价值。

相关推荐
xianzi20208 天前
双手解放:左右键连点功能详解
功能详解·游戏工具·左右键连点
xianzi20208 天前
一键启动:F9/F10快捷键设计优势
用户体验·游戏工具·快捷键设计
sinat_333518878 天前
一目了然:悬浮窗功能介绍
用户体验·悬浮窗·游戏工具
Mr数据杨18 天前
暗黑破坏神2 MOD修改工具文件编辑
mod·游戏工具·暗黑破坏神2重置版
Mr数据杨18 天前
暗黑2重制 Mod开发工具汇总
mod·游戏工具·暗黑破坏神
Mr数据杨19 天前
暗黑破坏神2 MOD修改工具游戏设置整体难度
mod·游戏工具·暗黑破坏神2重置版
Mr数据杨19 天前
暗黑破坏神2 MOD修改工具装备编辑其他物品
mod·游戏工具·暗黑破坏神2重置版
Mr数据杨20 天前
暗黑破坏神2 MOD修改工具装备编辑暗金物品
mod·游戏工具·暗黑破坏神2重置版
Mr数据杨20 天前
暗黑破坏神2 MOD修改工具装备编辑武器物品
mod·游戏工具·暗黑破坏神2重置版