ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

装备洗练计算器:云存档与自定义模拟实战

装备洗练计算器:云存档与自定义模拟实战 在《胜利女神NIKKE》这类养成游戏中装备洗练是提升战力的核心环节之一但洗练结果完全随机手动计算期望、记录配置、对比方案非常痛苦。尤其是当你需要在不同设备之间迁移存档或者想针对自己的词条池做个性化模拟时普通计算器根本不够用。本文会围绕“装备洗练计算器”的完整实现重点讲解云存档和自定义模拟这两个进阶能力并给出可复制的前端代码。无论你是游戏工具爱好者还是想练手前端项目都能从这套方案里找到可以落地的思路。1. 背景与核心概念1.1 装备洗练计算器是什么装备洗练是养成游戏中常见的随机词条玩法。玩家消耗洗练材料系统从词条池中随机生成若干条属性可能是攻击、防御、暴击率等不同词条也可能出现更高稀有度的特殊词条。由于词条组合千变万化手动计算“我要洗多少次才能出理想属性”几乎不可能于是就有了装备洗练计算器。它本质上是一个概率计算与模拟工具根据词条池、词条权重、洗练次数计算目标词条组合出现的理论概率。通过大量模拟给出接近真实情况的统计结果比如“模拟一万次成功次数约为 48 次”。帮助玩家决定优先洗哪个部位、准备多少材料避免盲目消耗。1.2 为什么需要云存档和自定义模拟普通计算器通常有几个痛点参数写死不能适配不同玩家的角色等级、词条池、目标词条。配置结果只存在本地换设备或清理浏览器后就丢失。无法同时记录多个方案的模拟结果不方便对比。云存档解决的是“数据可迁移、可恢复”的问题。把用户的参数、历史模拟记录、目标配置保存到 JSON 文件或云端数据库换设备时导入即可不用重新设置。自定义模拟解决的是“个性化计算”的问题。玩家可以自己配置词条池、目标属性、材料成本、洗练次数上限让计算结果真正符合自己的账号现状而不是一个通用公式。2. 方案设计与技术选型2.1 功能拆解先明确一个最小可用的装备洗练计算器需要哪些功能词条池管理维护词条名称、稀有度、权重。目标词条配置让用户选择期望出现的词条组合。洗练模拟根据配置随机生成洗练结果统计成功率。自定义模拟参数模拟次数、保底规则、是否允许重复词条等。本地存档用户配置和模拟记录自动保存到浏览器 localStorage。云存档通过导出 JSON 文件实现跨设备迁移或接入后端接口实现真正云同步。这套设计既照顾了纯前端工具的轻量性又保留了后续扩展后端的能力。2.2 技术选型本文示例采用纯前端技术栈零依赖、可直接运行语言HTML CSS JavaScript存储localStorage本地 JSON 文件导入导出云存档的轻量替代运行环境现代浏览器Chrome、Edge、Firefox构建工具不需要单 HTML 文件即可如果你计划接入真正的云端同步可以在本文基础上增加后端 API 或云数据库如 Firebase、Supabase但核心模拟逻辑不需要改变。2.3 项目结构为了便于理解我们先设计文件结构。虽然有多个文件但最终可以合并成一个 HTML 发布。equip-simulator/ ├── index.html // 页面结构 ├── style.css // 样式 ├── app.js // 核心逻辑 └── README.md // 说明文档如果只是为了快速体验可以直接把 CSS 和 JS 写到 index.html 里。本文为了阅读清晰按单文件示例讲解所有核心代码都放在一个 HTML 中。3. 核心模块实现3.1 洗练概率模型洗练的本质是一次带权重的随机抽取。假设某件装备的词条池如下词条名权重攻击力 3%30防御力 3%20暴击率 1%10暴击伤害 5%5生命值 2%15效果命中 2%10每次洗练会从词条池中随机抽取一个词条抽取概率由权重占比决定。如果允许重复词条那么每次抽取都是独立事件如果不允许重复则需要移除已抽到的词条。计算目标词条出现概率的公式是目标词条概率 目标词条权重之和 / 词条池权重之和多次模拟则直接按概率随机生成结果统计目标组合出现的次数。3.2 自定义模拟模块自定义模拟需要暴露以下参数模拟次数词条池用户可增删改目标词条组合是否允许重复词条每次洗练消耗的材料数模拟流程如下根据用户配置构建词条池数组。循环指定次数每次随机抽取词条。判断抽取结果是否满足目标组合。统计成功次数和成功率。输出模拟结果并计算平均消耗。这里要注意模拟次数越大结果越接近理论概率但浏览器计算量也会增大。建议默认模拟 1000 次用户可自行调整。3.3 本地存档与云存档方案本地存档非常适合保存工具类配置。使用 localStorage 的代码很简单localStorage.setItem(equipSimConfig, JSON.stringify(config)); const saved JSON.parse(localStorage.getItem(equipSimConfig));但 localStorage 有容量限制通常 5MB 左右且只保存在当前浏览器。解决跨设备问题最简单的方式是提供“导出存档”和“导入存档”功能。导出存档的核心是生成一个 JSON 文件然后触发浏览器下载const blob new Blob([JSON.stringify(config, null, 2)], { type: application/json }); const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download equip-sim-config.json; a.click(); URL.revokeObjectURL(url);导入存档则通过 file input 读取用户上传的 JSON 文件const reader new FileReader(); reader.onload function(e) { const config JSON.parse(e.target.result); applyConfig(config); localStorage.setItem(equipSimConfig, JSON.stringify(config)); }; reader.readAsText(file);这种“导出/导入”方式没有真正上传到服务器但已经足够让用户把配置存到网盘、邮箱或云盘实现跨设备迁移。如果后续要接入真正的云同步只需要将导出接口替换为 POST 请求将导入接口替换为 GET 请求即可。4. 完整实战从零实现一个装备洗练计算器接下来我们实现一个可运行的单文件版本。为了方便讲解我将代码分成三个部分HTML 结构、CSS 样式、JavaScript 逻辑。你可以直接复制到本地 index.html 中运行。4.1 HTML 页面结构首先构建页面布局。页面分为三个区域参数配置区词条池配置区结果展示区!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title装备洗练计算器/title style /* 样式写在后面 */ /style /head body div classcontainer h1装备洗练计算器/h1 p classsubtitle支持自定义模拟、云存档导入导出/p div classcard h2模拟参数/h2 div classform-group label forsimCount模拟次数/label input typenumber idsimCount value1000 min100 max50000 /div div classform-group label forallowRepeat允许重复词条/label input typecheckbox idallowRepeat checked /div button idsimulateBtn开始模拟/button /div div classcard h2词条池配置/h2 p每行一个词条格式词条名|权重/p textarea idpoolInput rows6 攻击力3%|30 防御力3%|20 暴击率1%|10 暴击伤害5%|5 生命值2%|15 效果命中2%|10 /textarea /div div classcard h2目标词条组合/h2 p每行一个词条需要模拟结果同时包含这些词条/p textarea idtargetInput rows3 攻击力3% 暴击率1% /textarea /div div classcard h2结果/h2 div idresult p尚未运行模拟/p /div /div div classcard h2存档管理/h2 button idsaveBtn保存到本地/button button idexportBtn导出存档/button button idimportBtn导入存档/button input typefile idimportFile accept.json styledisplay:none /div /div script // JS 逻辑写在后面 /script /body /html4.2 CSS 样式为了让页面看起来更清爽加上简单的卡片式布局。这里只保留核心样式你可以按需美化。* { box-sizing: border-box; } body { font-family: Microsoft YaHei, PingFang SC, sans-serif; background: #f5f7fa; margin: 0; padding: 20px; color: #333; } .container { max-width: 720px; margin: 0 auto; } .card { background: #fff; border-radius: 10px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); padding: 20px; margin-bottom: 16px; } .card h2 { margin-top: 0; font-size: 20px; border-left: 4px solid #4a90d9; padding-left: 10px; } .form-group { margin-bottom: 12px; } label { display: block; margin-bottom: 4px; font-weight: 600; } input[typenumber] { width: 120px; padding: 8px; border: 1px solid #ccc; border-radius: 6px; } input[typecheckbox] { width: 18px; height: 18px; margin-top: 8px; } textarea { width: 100%; border: 1px solid #ccc; border-radius: 6px; padding: 10px; font-family: monospace; font-size: 14px; } button { background: #4a90d9; color: #fff; border: none; border-radius: 6px; padding: 10px 16px; font-size: 14px; cursor: pointer; margin-right: 8px; margin-top: 8px; } button:hover { background: #357abd; } #result { margin-top: 10px; font-size: 14px; line-height: 1.8; }4.3 JavaScript 核心逻辑这是整个计算器最重要的一部分。我会按模块拆解代码然后说明运行逻辑。// 状态管理 let config { simCount: 1000, allowRepeat: true, pool: [], target: [] }; // 基础函数 function parsePool(text) { return text.split(\n) .map(line line.trim()) .filter(line line.length 0) .map(line { const [name, weightStr] line.split(|); return { name: name.trim(), weight: parseFloat(weightStr) || 0 }; }) .filter(item item.name.length 0 item.weight 0); } function parseTarget(text) { return text.split(\n) .map(line line.trim()) .filter(line line.length 0); } // 洗练模拟 function runSimulation() { const pool config.pool; const target config.target; const simCount config.simCount; const allowRepeat config.allowRepeat; if (pool.length 0) { alert(词条池不能为空); return null; } if (target.some(t !pool.some(p p.name t))) { alert(目标词条不在词条池中请检查配置); return null; } const cdf buildCDF(pool); let successCount 0; const MAX_LOOP 50000; for (let i 0; i Math.min(simCount, MAX_LOOP); i) { const result drawOnce(cdf, pool, allowRepeat); const isSuccess target.every(t result.includes(t)); if (isSuccess) { successCount; } } const actualCount Math.min(simCount, MAX_LOOP); const successRate (successCount / actualCount * 100).toFixed(2); return { successCount: successCount, actualCount: actualCount, successRate: successRate }; } function buildCDF(pool) { const totalWeight pool.reduce((sum, item) sum item.weight, 0); let cum 0; return pool.map(item { cum item.weight / totalWeight; return cum; }); } function drawOnce(cdf, pool, allowRepeat) { if (allowRepeat) { const r Math.random(); let idx 0; for (let i 0; i cdf.length; i) { if (r cdf[i]) { idx i; break; } } return [pool[idx].name]; } else { // 不重复抽取每次从剩余词条中重新计算概率 const remainingPool [...pool]; const result []; while (remainingPool.length 0) { const totalWeight remainingPool.reduce((sum, item) sum item.weight, 0); const r Math.random(); let cum 0; let chosenIndex 0; for (let i 0; i remainingPool.length; i) { cum remainingPool[i].weight / totalWeight; if (r cum) { chosenIndex i; break; } } result.push(remainingPool[chosenIndex].name); remainingPool.splice(chosenIndex, 1); } return result; } }这段代码中buildCDF构建了一个累积分布函数数组用来按权重随机抽取词条。drawOnce负责执行一次完整洗练如果允许重复则每次从完整词条池中抽取一个词条。如果不允许重复则会从剩余词条池中按权重抽取直到所有词条被抽完。注意在不允许重复的情况下每次抽取后都要重新计算权重占比否则概率会不准确。接下来是界面交互逻辑// 界面绑定 const simCountInput document.getElementById(simCount); const allowRepeatInput document.getElementById(allowRepeat); const poolInput document.getElementById(poolInput); const targetInput document.getElementById(targetInput); const simulateBtn document.getElementById(simulateBtn); const resultDiv document.getElementById(result); const saveBtn document.getElementById(saveBtn); const exportBtn document.getElementById(exportBtn); const importBtn document.getElementById(importBtn); const importFile document.getElementById(importFile); function readForm() { config.simCount parseInt(simCountInput.value, 10) || 1000; config.allowRepeat allowRepeatInput.checked; config.pool parsePool(poolInput.value); config.target parseTarget(targetInput.value); } function writeForm() { simCountInput.value config.simCount; allowRepeatInput.checked config.allowRepeat; poolInput.value config.pool.map(p ${p.name}|${p.weight}).join(\n); targetInput.value config.target.join(\n); } simulateBtn.addEventListener(click, function() { readForm(); const result runSimulation(); if (result) { resultDiv.innerHTML p模拟次数${result.actualCount}/p p成功次数${result.successCount}/p p成功率${result.successRate}%/p p平均消耗${config.simCount} 次洗练需要 ${result.actualCount * 1} 个材料按每次消耗 1 个材料估算/p ; // 将结果保存到 localStorage config.lastResult result; localStorage.setItem(equipSimConfig, JSON.stringify(config)); } }); saveBtn.addEventListener(click, function() { readForm(); localStorage.setItem(equipSimConfig, JSON.stringify(config)); alert(已保存到本地浏览器); }); exportBtn.addEventListener(click, function() { readForm(); const blob new Blob([JSON.stringify(config, null, 2)], { type: application/json }); const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download equip-sim-config.json; a.click(); URL.revokeObjectURL(url); }); importBtn.addEventListener(click, function() { importFile.click(); }); importFile.addEventListener(change, function(e) { const file e.target.files[0]; if (!file) return; const reader new FileReader(); reader.onload function(event) { try { const parsed JSON.parse(event.target.result); if (parsed.pool parsed.target) { config parsed; writeForm(); localStorage.setItem(equipSimConfig, JSON.stringify(config)); alert(存档导入成功); } else { alert(存档格式不正确); } } catch (err) { alert(解析存档失败 err.message); } }; reader.readAsText(file); e.target.value ; }); // 初始化 function init() { const saved localStorage.getItem(equipSimConfig); if (saved) { try { config JSON.parse(saved); if (!config.pool || !config.target) { config { simCount: 1000, allowRepeat: true, pool: [], target: [] }; } } catch (e) { console.warn(本地存档解析失败使用默认配置); } } writeForm(); } init();4.4 运行与验证把以上代码复制到一个.html文件中用浏览器打开你会看到如下界面默认词条池包含 6 个词条。默认目标组合是“攻击力3%”和“暴击率1%”。点击“开始模拟”结果区域会显示模拟次数、成功次数、成功率。如果你点击“导出存档”浏览器会下载一个 JSON 文件。把这个文件上传到网盘或发送到另一台设备再打开计算器点击“导入存档”选择该文件配置就会恢复。这就是云存档的轻量实现。4.5 结果说明需要注意模拟结果本身存在随机误差。当模拟次数为 1000 次时成功率可能在理论概率附近波动但不会完全一致。这是正常现象。一般来说模拟次数越多结果越稳定。如果模拟次数太少统计结果可能偏离真实概率。在“不允许重复词条”模式下同一个目标词条只会出现一次会影响后续概率计算因此成功率会比“允许重复”模式低。5. 常见问题与排查思路5.1 模拟成功率总是 0%可能原因目标词条组合里有词条不在词条池中。词条池权重全为 0导致无法抽取。模拟次数太少目标词条出现的概率极低1000 次内没抽中。排查方法检查目标词条是否严格匹配词条池中的名称包括空格、符号。检查词条池每行是否都包含|分隔符。尝试增加模拟次数到 50000看成功率是否大于 0。5.2 导出的存档导入后配置丢失常见原因导入时没有等待文件读取完成就关闭页面。JSON 文件被篡改字段缺失。浏览器限制 localStorage 写入。解决方案在导入回调中增加错误提示避免静默失败。导出前用JSON.stringify(config, null, 2)生成可读格式方便检查。如果 localStorage 不可用可以增加 try/catch并提示用户使用导出功能。5.3 模拟次数过多导致页面卡顿当我们设置 50000 次模拟时浏览器主线程会持续运行可能卡住页面。优化方向用setTimeout或requestAnimationFrame把计算拆成多个帧执行。使用 Web Worker 进行后台计算避免阻塞 UI。限制最大模拟次数比如 50000 次超出时提示用户。代码中已经用Math.min(simCount, MAX_LOOP)做了最大限制但更好的做法是使用 Web Worker。下面是一个简化的 worker 使用思路const worker new Worker(sim-worker.js); worker.onmessage function(e) { resultDiv.innerHTML 成功率${e.data.successRate}%; }; worker.postMessage({ pool, target, simCount, allowRepeat });Web Worker 内部可以放置与runSimulation相同的逻辑这样不会阻塞页面。5.4 对存档安全有疑虑由于纯前端导出 JSON存档内容没有加密任何人都可以查看。如果后续要接入真正的云同步建议对存档进行加密传输至少使用 HTTPS并在服务端对用户身份进行鉴权。6. 最佳实践与工程建议6.1 数据模型设计要考虑扩展性对于游戏工具类项目数据结构不要写死。建议使用以下模型{ version: 1, simCount: 1000, allowRepeat: true, pool: [ { name: 攻击力3%, weight: 30 } ], target: [攻击力3%, 暴击率1%], lastResult: { successCount: 12, actualCount: 1000, successRate: 1.20 } }保留version字段后续升级数据结构时可以写迁移逻辑兼容旧存档。6.2 云存档方案的分层设计如果你不想停留在“导出/导入”阶段可以设计下面三层第一层本地 localStorage保证用户离线可用。第二层导出 / 导入 JSON让用户可以手动备份。第三层远程 API实现登录后自动云同步。远程 API 的核心接口很简单POST /api/save # 保存存档 GET /api/load # 加载存档请求时携带用户 token服务端将 JSON 存档存储到数据库。前端只需要在模拟成功后调用POST /api/save在页面加载时调用GET /api/load即可。6.3 模拟准确性验证在编写概率模拟代码时建议先建立一个“理论概率”对照表。例如词条池总共 6 个词条目标词条组合为 2 个词条且允许重复。单次洗练抽中第一个目标词条的概率 30 / 100 30%。抽中第二个目标词条的概率 10 / 100 10%。同时抽中这两个词条单次判定为同时包含的理论概率 30% × 10% 3%。用这个理论值去对比模拟结果如果差距在 1% 以内说明模拟逻辑基本可靠。6.4 安全性注意事项如果接入远程同步不要在前端写明文数据库密码。用户存档可能包含敏感信息导出时建议增加密码保护。涉及用户上传 JSON 时一定要做字段校验防止 XSS 攻击不要直接用innerHTML渲染用户输入。当前示例代码中用了innerHTML来展示结果但由于结果是从代码内部生成风险可控。如果你要把用户输入词条名渲染到页面请使用textContent或转义 HTML。7. 总结与后续优化方向本文从实际痛点出发完成了一个装备洗练计算器的核心设计重点实现了自定义模拟和轻量级云存档导入/导出 JSON功能。通过这套代码你可以快速搭建一个单文件工具也可以继续扩展成完整的全栈应用。接下来可以做的事情增加更丰富的词条池编辑功能比如按装备部位区分词条池。增加保底机制模拟让计算公式更贴近真实游戏。接入远程同步 API解决真正的多设备自动同步问题。把计算器打包成 PWA支持手机端离线使用。无论你是游戏爱好者还是前端开发者这个项目都很适合用来练习事件处理、状态管理、文件读写和数组算法。希望这篇文章能给你带来实用的参考。如果你在本地方案基础上接入真实云存档也欢迎继续分享你的实现经验。
返回列表