暗黑破坏神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 负责真正的数据规则。这样的模块,不只是"能用",也具备继续扩展和长期维护的开发价值。

相关推荐
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
G果2 年前
matlab 小数取余 rem 和 mod有 bug
matlab·debug·rem·mod·取余
windyjl2 年前
UE5 Mod Support 思路——纯蓝图
ue5·虚幻引擎·unreal engine·mod·模组