模块

ArtsData:修订间差异

来自蒙纳多研究本部

头号云​(留言 | 贡献)
创建页面,内容为“--[[ Module:Arts —— 战技页面动态化模块 用法: {{#invoke:Arts|main|shulk}} 按角色渲染全部战技 {{#invoke:Arts|main|12|13|14}} 按战技 ID 渲染 数据来源: * 数值(倍率/填充/命中/效果时间/效果数值)—— XBDB 实时拉取 bdat_common.pc_arts + 公式计算 * AP 消耗 / 中文名 / 官方描述 —— Module:Arts/static(由 XC1 DE 解包数据生成) * 类型 / 习得条件 —— pc_arts…”
 
头号云​(留言 | 贡献)
无编辑摘要
 
(未显示同一用户的13个中间版本)
第1行: 第1行:
--[[
-- Module:ArtsData —— 战技模块 v2(主模块,XBDB 调用全部收敛于 source)
Module:Arts —— 战技页面动态化模块
 
用法:
  {{#invoke:Arts|main|shulk}}        按角色渲染全部战技
  {{#invoke:Arts|main|12|13|14}}    按战技 ID 渲染
数据来源:
  * 数值(倍率/填充/命中/效果时间/效果数值)—— XBDB 实时拉取 bdat_common.pc_arts + 公式计算
  * AP 消耗 / 中文名 / 官方描述 —— Module:Arts/static(由 XC1 DE 解包数据生成)
  * 类型 / 习得条件 —— pc_arts 的 atk_type / get_type / get_lv 字段
公式(已用逐级数据标定,并对 wiki 原页面逐格校验 100%):
  倍率    = (rate1 + (rank-1)*grow_powl) / 100
  倍率上限 = (rate2 + (rank-1)*grow_powh) / 100    仅当 rate2 ~= rate1
  填充时间 = recast * (1 - (rank-1)*glow_recast/100) 秒
  命中修正 = dex + 5*(rank-1) %
  效果时间 = (st_time + (rank-1)*grow_st_time) / 10 秒
  效果数值 = st_val + (rank-1)*grow_st_val
]]
local p = {}
local p = {}
local post = require("Module:RequestHelper").post
 
local source = require("Module:ArtsData/source")
local lv1 = require("Module:ArtsData/lv1")
local descs = require("Module:ArtsData/desc")
local getArgs = require("Module:Arguments").getArgs
local getArgs = require("Module:Arguments").getArgs
local html = require("Module:Html")
local html = require("Module:Html")
local compress = html.compress
local compress = html.compress
-- 子模块路径自适应:无论本模块部署成什么名字,都能找到自己的子页
 
-- (解决了"模块:Arts 已被占用、需改名"的部署问题)
local SELF = (...):match("^(.+)/[^/]+$") or (...)
local static = require(SELF .. "/static")
local acquire = require(SELF .. "/acquire")
local ROMAN = {"I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII"}
local ROMAN = {"I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII"}
local MAX_RANK = 12
local MAX_RANK = 12
-- ---------- 工具 ----------
 
local function num(v)
local function num(v)
   if v == nil then return 0 end
   if v == nil then return 0 end
   return tonumber(v) or 0
   return tonumber(v) or 0
end
end
--[[
 
数值格式化(四舍五入 + 剥离小数尾零)
注意:绝不能对整数部分做尾零剥离("50" 不能变成 "5")
]]
local function fmt(v, digits)
local function fmt(v, digits)
   digits = digits or 2
   digits = digits or 2
   local factor = 10 ^ digits
   local factor = 10 ^ digits
  -- 用 floor(x+0.5) 实现 ROUND_HALF_UP;先加一个小 epsilon 抵消浮点表示误差
   local scaled = v * factor
   local scaled = v * factor
   local rounded
   local rounded
第48行: 第28行:
     rounded = -math.floor(-scaled + 0.5 + 1e-9)
     rounded = -math.floor(-scaled + 0.5 + 1e-9)
   end
   end
   if digits == 0 then
   if digits == 0 then
     return tostring(rounded)
     return tostring(rounded)
   end
   end
   local intPart = math.floor(math.abs(rounded) / factor)
   local intPart = math.floor(math.abs(rounded) / factor)
   local fracPart = math.abs(rounded) % factor
   local fracPart = math.abs(rounded) % factor
   local fracStr = string.format("%0" .. digits .. "d", fracPart)
   local fracStr = string.format("%0" .. digits .. "d", fracPart)
  -- 仅剥离小数尾零
   fracStr = fracStr:gsub("0+$", "")
   fracStr = fracStr:gsub("0+$", "")
   local sign = rounded < 0 and "-" or ""
   local sign = rounded < 0 and "-" or ""
第62行: 第43行:
   return sign .. tostring(intPart) .. "." .. fracStr
   return sign .. tostring(intPart) .. "." .. fracStr
end
end
-- atk_type: 1=物理 2=以太 3=状态
 
local ATK_TYPE = {[1] = "物理", [2] = "以太", [3] = "状态"}
-- atk_type=4/6 为梅莉亚元素系(召唤/释放),攻击本质是以太伤害,标"以太"
-- 习得条件:get_type 1=初始/等级, 2=剧情习得
-- (旧扩展无此映射故落"未知";sim 数据验证:4=元素发射+召X术 7 个,6=X元素发射 6 个)
local ATK_TYPE = {[1] = "物理", [2] = "以太", [3] = "状态", [4] = "以太", [6] = "以太"}
 
-- 灵气判定(方案 A,数据驱动):atk_type=3 且 st_type >= 206 → 自身系状态(灵气/增益)
-- 低段 st_type(<206)是对敌减益(破防 18、睡眠 9 等)
local function isAura(data)
  return num(data.atk_type) == 3 and num(data.st_type) >= 206
end
 
-- 状态类颜色:灵气=橙、状态(对敌)=紫(MediaWiki 标准色板)
local AURA_COLOR = "#e69138"
local DEBUFF_COLOR = "#674ea7"
 
-- 类型着色徽章:物理=红、以太=青、灵气=橙、状态=紫
local TYPE_COLOR = {
  ["物理"] = "#cc0000",
  ["以太"] = "#3d85c6",
}
local function typeBadge(atype, aura)
  local color = aura and AURA_COLOR or DEBUFF_COLOR
  if atype == "物理" then color = TYPE_COLOR["物理"] end
  if atype == "以太" then color = TYPE_COLOR["以太"] end
  return '<span style="color:' .. color .. ';font-weight:bold;">' .. atype .. "</span>"
end
 
-- 效果摘要:去标签、截断(概览表用)
-- 注意:截断必须用 mw.ustring(按码点),string.sub/# 按字节会把汉字切成
-- 非法 UTF-8,导致下游 Html compress 的 gsub 报 "string is not UTF-8"
local function descSummary(artId, limit)
  local d = descs[artId] or ""
  d = d:gsub("%[[^%]]*%]", "")      -- 去掉 [ML:icon ...] 类标记
  d = d:gsub("%s+", " ")
  if mw.ustring.len(d) > limit then
    d = mw.ustring.sub(d, 1, limit) .. "…"
  end
  return d
end
 
local function learnText(data)
local function learnText(data)
   if num(data.get_type) == 2 then
   if num(data.get_type) == 2 then
第75行: 第93行:
   return "初始拥有"
   return "初始拥有"
end
end
-- ---------- 等级序列展开 ----------
 
-- 通用等差序列:(rank1 + (rank-1)*grow) / scale
local function series(rank1, grow, digits, scale)
local function series(rank1, grow, digits, scale)
   scale = scale or 1
   scale = scale or 1
第85行: 第102行:
   return out
   return out
end
end
-- 填充时间:按基础冷却的百分比递减;recast=0 返回 nil(页面显示 —)
 
local function recastSeries(data)
local function recastSeries(data)
   local recast = num(data.recast)
   local recast = num(data.recast)
第96行: 第113行:
   return out
   return out
end
end
-- 命中修正:dex + 5*(rank-1)
 
local function dexSeries(data)
local function dexSeries(data)
   local dex = num(data.dex)
   local dex = num(data.dex)
第105行: 第122行:
   return out
   return out
end
end
-- ---------- 表格行 ----------
 
local function apSeries(data)
  local pc = num(data.pc)
  local idx = num(data.idx) + 1
  local rateRow = lv1.grow_arts_rate[pc]
  if not rateRow or not rateRow[idx] then
    return nil
  end
  local rate = rateRow[idx]
  local out = {}
  for r = 1, MAX_RANK do
    out[r] = tostring(math.floor(lv1.grow_arts_base[r] * rate / 100 + 0.5))
  end
  return out
end
 
local function row12(label, cells)
local function row12(label, cells)
   local t = {"<tr><th>" .. label .. "</th>"}
   local t = {"<tr><th>" .. label .. "</th>"}
第115行: 第147行:
   return table.concat(t)
   return table.concat(t)
end
end
local function rowSpan(label, text)
local function rowSpan(label, text)
   if not text or text == "" then return "" end
   if not text or text == "" then return "" end
第120行: 第153行:
     '" style="text-align:left;">' .. text .. "</td></tr>"
     '" style="text-align:left;">' .. text .. "</td></tr>"
end
end
-- 安全查询:XBDB 查询未注册表会抛 PHP 异常,必须包裹
 
local function fetchArt(artId)
function p.renderArt(artId)
  local ok, data = pcall(post, "bdat_common.pc_arts", artId)
   if not source.isValidArtId(artId) then
   if not ok then
     return ""
    mw.log("Arts: 查询失败 id=" .. tostring(artId) .. " err=" .. tostring(data))
     return nil
  end
  if type(data) ~= "table" or not data.row_id then
    return nil
   end
   end
  return data
   local data = source.fetchArt(artId)
end
-- ---------- 单战技渲染 ----------
function p.renderArt(artId)
   local data = fetchArt(artId)
   if not data then
   if not data then
     return ""
     return ""
   end
   end
  local s = static.arts[artId] or {}
 
   local name = (data.name and data.name ~= "") and data.name or s.name or ("战技 " .. artId)
   local name = (data.name and data.name ~= "") and data.name
    or source.fetchDesc(artId) or ("战技 " .. artId)
   local atype = ATK_TYPE[num(data.atk_type)] or "未知"
   local atype = ATK_TYPE[num(data.atk_type)] or "未知"
   local learn = learnText(data)
   local learn = learnText(data)
  local isTalent = num(data.arts_type) == 1
   local out = {}
   local out = {}
   out[#out + 1] = html.h3(name)
  local aura = isAura(data)
   out[#out + 1] = "'''类型''':" .. atype .. "  '''习得''':" .. learn
  -- 方向 1 改版:每技一个可折叠块(MediaWiki 原生 mw-collapsible,零 JS)
   -- 表头
  -- 默认全展开(2026-10-12):站点折叠 JS 与锚点联动不可靠(Foreground 皮肤),
  -- 总览跳转后直接可见内容;保留 mw-collapsible 类,读者仍可手动折叠
  -- 战技名不染色:颜色信息只由类型徽章传达
   out[#out + 1] = '<table class="wikitable mw-collapsible" id="art-' ..
    artId .. '" style="width:100%;">'
  -- 折叠块头部:名 + 类型(元素小字缀)/习得/目标范围
  -- 属性段已删(2026-10-12),元素仅攻击技且非"无"/"物理"时以小字缀在类型后
  local elemText = source.elemName(data)
  local tgt = source.tgtName(data)
  local rng = source.rangeName(data)
  local tgtTxt = ""
  if tgt then
    tgtTxt = " · " .. tgt
    if rng then tgtTxt = tgtTxt .. rng end
  end
  local typeHtml = typeBadge(atype, aura)
  if elemText and elemText ~= "无" and elemText ~= "物理" then
    typeHtml = typeHtml .. '<small style="color:#777;">' .. elemText .. "</small>"
  end
   out[#out + 1] = '<tr><th style="text-align:left;">' .. name ..
    '<small> (' .. typeHtml ..
    " · " .. learn .. tgtTxt .. ")</small></th></tr>"
   out[#out + 1] = '<tr><td style="text-align:left;">'
 
   local head = {"<tr><th>等级</th>"}
   local head = {"<tr><th>等级</th>"}
   for r = 1, MAX_RANK do
   for r = 1, MAX_RANK do
第151行: 第201行:
   end
   end
   head[#head + 1] = "</tr>"
   head[#head + 1] = "</tr>"
   local rows = {}
   local rows = {}
  -- 效果说明(官方描述)
 
   rows[#rows + 1] = rowSpan("效果", s.desc or "")
   rows[#rows + 1] = rowSpan("效果", descs[artId] or "")
  -- AP 消耗(天赋战技无 AP 数据时输出全 0,与原页面保持一致)
 
   do
   if not isTalent then
    local cells = {}
     rows[#rows + 1] = row12("AP 消耗", apSeries(data))
    for r = 1, MAX_RANK do
      cells[r] = s.ap and tostring(s.ap[r] or 0) or "0"
    end
     rows[#rows + 1] = row12("AP 消耗", cells)
   end
   end
  -- 倍率
 
   local r1, r2 = num(data.rate1), num(data.rate2)
   local r1, r2 = num(data.rate1), num(data.rate2)
   if r2 ~= r1 then
   if r2 ~= r1 then
第170行: 第217行:
     rows[#rows + 1] = row12("倍率", series(r1, data.grow_powl, 2, 100))
     rows[#rows + 1] = row12("倍率", series(r1, data.grow_powl, 2, 100))
   end
   end
  -- 填充时间
 
   local rs = recastSeries(data)
   local rs = recastSeries(data)
   if rs then
   if rs then
第177行: 第224行:
     rows[#rows + 1] = row12("填充时间(秒)", nil)
     rows[#rows + 1] = row12("填充时间(秒)", nil)
   end
   end
  -- 效果时间
 
   if num(data.st_time) ~= 0 or num(data.grow_st_time) ~= 0 then
   if num(data.st_time) ~= 0 or num(data.grow_st_time) ~= 0 then
     rows[#rows + 1] = row12("效果时间(秒)", series(data.st_time, data.grow_st_time, 1, 10))
     rows[#rows + 1] = row12("效果时间(秒)", series(data.st_time, data.grow_st_time, 1, 10))
   end
   end
  -- 命中修正
 
   rows[#rows + 1] = row12("命中修正(决定版)", dexSeries(data))
   rows[#rows + 1] = row12("命中修正(决定版)", dexSeries(data))
  -- 效果数值
 
   if num(data.st_val) ~= 0 or num(data.grow_st_val) ~= 0 then
   if num(data.st_val) ~= 0 or num(data.grow_st_val) ~= 0 then
     rows[#rows + 1] = row12("效果数值", series(data.st_val, data.grow_st_val, 0, 1))
     rows[#rows + 1] = row12("效果数值", series(data.st_val, data.grow_st_val, 0, 1))
   end
   end
   out[#out + 1] = '<table class="wikitable" style="text-align:center;">' ..
   out[#out + 1] = '<table class="wikitable" style="text-align:center;">' ..
     table.concat(head) .. table.concat(rows) .. "</table>"
     table.concat(head) .. table.concat(rows) .. "</table>"
   -- SMW 关键字段
 
   out[#out + 1] = "</td></tr></table>"
 
   local maxRate
   local maxRate
   if r2 ~= r1 then
   if r2 ~= r1 then
第196行: 第246行:
     maxRate = (r1 + 11 * num(data.grow_powl)) / 100
     maxRate = (r1 + 11 * num(data.grow_powl)) / 100
   end
   end
   mw.smw.set({
   mw.smw.subobject({
     artId = artId,
     artId = artId,
     artName = name,
     artName = name,
第203行: 第253行:
     artRate = maxRate,
     artRate = maxRate,
     artRecast = num(data.recast),
     artRecast = num(data.recast),
    artDesc = descs[artId] or "",
    artIsTalent = isTalent and "1" or "0",
    artIsAura = aura and "1" or "0",
    artElem = source.elemName(data) or "",
    artTarget = source.tgtName(data) or "",
    artRange = source.rangeName(data) or "",
    artHate = num(data.hate),
    artChain = source.chainName(data),
   })
   })
   return table.concat(out, "\n")
   return table.concat(out, "\n")
end
end
-- ---------- 获取表渲染 ----------
 
local ACQ_HEADER = {"战技名", "等级", "地点", "掉落", "商店", "出售", "备注"}
-- 总览表:一行一技,链接到下方折叠块的锚点(id = art-<id>)
local function renderAcquire(roleKey)
-- 列:战技名(着色+锚点)/ 类型(元素小字缀)/ 习得 / 目标 / 连锁 / 仇恨 / 效果摘要
  local rows = acquire[roleKey]
-- 属性列已删(2026-10-12):带攻击元素的技基本只有梅莉亚/力奇,单独一列性价比低,
  if not rows or #rows == 0 then
-- 元素信息压缩进类型列小字(如 以太⁠雷),状态技 elem 非攻击属性不显示
    return ""
local function renderOverview(ids)
  end
  local rows = {}
  local out = {html.h2("获取")}
  for _, id in ipairs(ids) do
  out[#out + 1] = '<table class="wikitable sortable">'
    local data = source.fetchArt(id)
  out[#out + 1] = "<tr>"
    if data then
  for _, h in ipairs(ACQ_HEADER) do
      local name = (data.name and data.name ~= "") and data.name
    out[#out + 1] = "<th>" .. h .. "</th>"
        or source.fetchDesc(id) or ("战技 " .. id)
  end
      local atype = ATK_TYPE[num(data.atk_type)] or "未知"
  out[#out + 1] = "</tr>"
      local aura = isAura(data)
  for _, r in ipairs(rows) do
      -- 类型列:徽章 + 元素小字缀(仅攻击技且元素非"无"时)
    out[#out + 1] = "<tr>"
      local typeHtml = typeBadge(atype, aura)
    out[#out + 1] = "<td>" .. (r.art or "") .. "</td>"
      local elem = source.elemName(data)
    out[#out + 1] = "<td>" .. (r.level or "") .. "</td>"
      if elem and elem ~= "无" and elem ~= "物理" then
    out[#out + 1] = "<td>" .. (r.place or "") .. "</td>"
        typeHtml = typeHtml .. '<small style="color:#777;">' .. elem .. "</small>"
    out[#out + 1] = "<td>" .. (r.drop or "") .. "</td>"
      end
    out[#out + 1] = "<td>" .. (r.shop or "") .. "</td>"
      -- 目标 / 范围 / 连锁 / 仇恨
    out[#out + 1] = "<td>" .. (r.sell or "") .. "</td>"
      local tgt = source.tgtName(data) or "—"
    out[#out + 1] = "<td>" .. (r.note or "") .. "</td>"
      local rng = source.rangeName(data)
    out[#out + 1] = "</tr>"
      if rng then tgt = tgt .. "·" .. rng end
      local chain = source.chainName(data)
      local hate = source.hateText(data) or "—"
      rows[#rows + 1] = "<tr>"
        .. '<td style="text-align:left;">[[' .. "#art-" .. id .. "|" .. name .. "]]</td>"
        .. "<td>" .. typeHtml .. "</td>"
        .. "<td>" .. learnText(data) .. "</td>"
        .. "<td>" .. tgt .. "</td>"
        .. "<td>" .. chain .. "</td>"
        .. "<td>" .. hate .. "</td>"
        .. '<td style="text-align:left;">' .. descSummary(id, 40) .. "</td>"
        .. "</tr>"
    end
   end
   end
   out[#out + 1] = "</table>"
   if #rows == 0 then return "" end
  return table.concat(out)
  return '<table class="wikitable" style="text-align:center;">'
    .. "<tr><th>战技名</th><th>类型</th><th>习得</th><th>目标</th><th>连锁</th><th>仇恨</th><th>效果摘要</th></tr>"
    .. table.concat(rows)
    .. "</table>"
end
end
-- ---------- 主入口 ----------
 
function p.main(frame)
function p.main(frame)
   local args = getArgs(frame)
   local args = getArgs(frame)
   local out = {}
   local out = {}
   local first = args[1]
   local first = args[1]
   local role = first and static.roles[first] or nil
   local role = first and lv1.roles[first] or nil
 
   if role then
   if role then
     out[#out + 1] = "== 战技 =="
     out[#out + 1] = html.h2("总览")
    out[#out + 1] = renderOverview(role.ids)
    local talent, normal = {}, {}
     for _, id in ipairs(role.ids) do
     for _, id in ipairs(role.ids) do
       out[#out + 1] = p.renderArt(id)
       local data = source.fetchArt(id)
      local isTalent = data and num(data.arts_type) == 1
      table.insert(isTalent and talent or normal, id)
    end
    if #talent > 0 then
      out[#out + 1] = html.h2("天赋战技")
      for _, id in ipairs(talent) do
        out[#out + 1] = p.renderArt(id)
      end
     end
     end
     -- 获取表(可通过 noacquire=1 关闭)
     if #normal > 0 then
    if args.noacquire ~= "1" then
      out[#out + 1] = html.h2("战技")
       out[#out + 1] = renderAcquire(first)
       for _, id in ipairs(normal) do
        out[#out + 1] = p.renderArt(id)
      end
     end
     end
   else
   else
第257行: 第345行:
     end
     end
   end
   end
   if #out == 0 then
   if #out == 0 then
     return ""
     return ""
   end
   end
   return compress(table.concat(out, "\n"))
   return compress(table.concat(out, "\n"))
end
end
return p
return p

2026年10月13日 (二) 16:30的最新版本

此模块的文档可以在模块:ArtsData/doc创建

-- Module:ArtsData —— 战技模块 v2(主模块,XBDB 调用全部收敛于 source)

local p = {}

local source = require("Module:ArtsData/source")
local lv1 = require("Module:ArtsData/lv1")
local descs = require("Module:ArtsData/desc")
local getArgs = require("Module:Arguments").getArgs
local html = require("Module:Html")
local compress = html.compress

local ROMAN = {"I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII"}
local MAX_RANK = 12

local function num(v)
  if v == nil then return 0 end
  return tonumber(v) or 0
end

local function fmt(v, digits)
  digits = digits or 2
  local factor = 10 ^ digits
  local scaled = v * factor
  local rounded
  if scaled >= 0 then
    rounded = math.floor(scaled + 0.5 + 1e-9)
  else
    rounded = -math.floor(-scaled + 0.5 + 1e-9)
  end

  if digits == 0 then
    return tostring(rounded)
  end

  local intPart = math.floor(math.abs(rounded) / factor)
  local fracPart = math.abs(rounded) % factor
  local fracStr = string.format("%0" .. digits .. "d", fracPart)
  fracStr = fracStr:gsub("0+$", "")
  local sign = rounded < 0 and "-" or ""
  if fracStr == "" then
    return sign .. tostring(intPart)
  end
  return sign .. tostring(intPart) .. "." .. fracStr
end

-- atk_type=4/6 为梅莉亚元素系(召唤/释放),攻击本质是以太伤害,标"以太"
-- (旧扩展无此映射故落"未知";sim 数据验证:4=元素发射+召X术 7 个,6=X元素发射 6 个)
local ATK_TYPE = {[1] = "物理", [2] = "以太", [3] = "状态", [4] = "以太", [6] = "以太"}

-- 灵气判定(方案 A,数据驱动):atk_type=3 且 st_type >= 206 → 自身系状态(灵气/增益)
-- 低段 st_type(<206)是对敌减益(破防 18、睡眠 9 等)
local function isAura(data)
  return num(data.atk_type) == 3 and num(data.st_type) >= 206
end

-- 状态类颜色:灵气=橙、状态(对敌)=紫(MediaWiki 标准色板)
local AURA_COLOR = "#e69138"
local DEBUFF_COLOR = "#674ea7"

-- 类型着色徽章:物理=红、以太=青、灵气=橙、状态=紫
local TYPE_COLOR = {
  ["物理"] = "#cc0000",
  ["以太"] = "#3d85c6",
}
local function typeBadge(atype, aura)
  local color = aura and AURA_COLOR or DEBUFF_COLOR
  if atype == "物理" then color = TYPE_COLOR["物理"] end
  if atype == "以太" then color = TYPE_COLOR["以太"] end
  return '<span style="color:' .. color .. ';font-weight:bold;">' .. atype .. "</span>"
end

-- 效果摘要:去标签、截断(概览表用)
-- 注意:截断必须用 mw.ustring(按码点),string.sub/# 按字节会把汉字切成
-- 非法 UTF-8,导致下游 Html compress 的 gsub 报 "string is not UTF-8"
local function descSummary(artId, limit)
  local d = descs[artId] or ""
  d = d:gsub("%[[^%]]*%]", "")      -- 去掉 [ML:icon ...] 类标记
  d = d:gsub("%s+", " ")
  if mw.ustring.len(d) > limit then
    d = mw.ustring.sub(d, 1, limit) .. "…"
  end
  return d
end

local function learnText(data)
  if num(data.get_type) == 2 then
    return "剧情习得"
  end
  local lv = num(data.get_lv)
  if lv > 0 then
    return "等级 " .. lv
  end
  return "初始拥有"
end

local function series(rank1, grow, digits, scale)
  scale = scale or 1
  local out = {}
  for r = 1, MAX_RANK do
    out[r] = fmt((num(rank1) + (r - 1) * num(grow)) / scale, digits)
  end
  return out
end

local function recastSeries(data)
  local recast = num(data.recast)
  if recast == 0 then return nil end
  local glow = num(data.glow_recast)
  local out = {}
  for r = 1, MAX_RANK do
    out[r] = fmt(recast * (1 - (r - 1) * glow / 100), 1)
  end
  return out
end

local function dexSeries(data)
  local dex = num(data.dex)
  local out = {}
  for r = 1, MAX_RANK do
    out[r] = "+" .. tostring(dex + 5 * (r - 1)) .. "%"
  end
  return out
end

local function apSeries(data)
  local pc = num(data.pc)
  local idx = num(data.idx) + 1
  local rateRow = lv1.grow_arts_rate[pc]
  if not rateRow or not rateRow[idx] then
    return nil
  end
  local rate = rateRow[idx]
  local out = {}
  for r = 1, MAX_RANK do
    out[r] = tostring(math.floor(lv1.grow_arts_base[r] * rate / 100 + 0.5))
  end
  return out
end

local function row12(label, cells)
  local t = {"<tr><th>" .. label .. "</th>"}
  for r = 1, MAX_RANK do
    local v = cells and cells[r] or nil
    t[#t + 1] = "<td>" .. (v or "—") .. "</td>"
  end
  t[#t + 1] = "</tr>"
  return table.concat(t)
end

local function rowSpan(label, text)
  if not text or text == "" then return "" end
  return '<tr><th>' .. label .. '</th><td colspan="' .. MAX_RANK ..
    '" style="text-align:left;">' .. text .. "</td></tr>"
end

function p.renderArt(artId)
  if not source.isValidArtId(artId) then
    return ""
  end
  local data = source.fetchArt(artId)
  if not data then
    return ""
  end

  local name = (data.name and data.name ~= "") and data.name
    or source.fetchDesc(artId) or ("战技 " .. artId)
  local atype = ATK_TYPE[num(data.atk_type)] or "未知"
  local learn = learnText(data)
  local isTalent = num(data.arts_type) == 1

  local out = {}
  local aura = isAura(data)
  -- 方向 1 改版:每技一个可折叠块(MediaWiki 原生 mw-collapsible,零 JS)
  -- 默认全展开(2026-10-12):站点折叠 JS 与锚点联动不可靠(Foreground 皮肤),
  -- 总览跳转后直接可见内容;保留 mw-collapsible 类,读者仍可手动折叠
  -- 战技名不染色:颜色信息只由类型徽章传达
  out[#out + 1] = '<table class="wikitable mw-collapsible" id="art-' ..
    artId .. '" style="width:100%;">'
  -- 折叠块头部:名 + 类型(元素小字缀)/习得/目标范围
  -- 属性段已删(2026-10-12),元素仅攻击技且非"无"/"物理"时以小字缀在类型后
  local elemText = source.elemName(data)
  local tgt = source.tgtName(data)
  local rng = source.rangeName(data)
  local tgtTxt = ""
  if tgt then
    tgtTxt = " · " .. tgt
    if rng then tgtTxt = tgtTxt .. rng end
  end
  local typeHtml = typeBadge(atype, aura)
  if elemText and elemText ~= "无" and elemText ~= "物理" then
    typeHtml = typeHtml .. '<small style="color:#777;">' .. elemText .. "</small>"
  end
  out[#out + 1] = '<tr><th style="text-align:left;">' .. name ..
    '<small> (' .. typeHtml ..
    " · " .. learn .. tgtTxt .. ")</small></th></tr>"
  out[#out + 1] = '<tr><td style="text-align:left;">'

  local head = {"<tr><th>等级</th>"}
  for r = 1, MAX_RANK do
    head[#head + 1] = "<th>" .. ROMAN[r] .. "</th>"
  end
  head[#head + 1] = "</tr>"

  local rows = {}

  rows[#rows + 1] = rowSpan("效果", descs[artId] or "")

  if not isTalent then
    rows[#rows + 1] = row12("AP 消耗", apSeries(data))
  end

  local r1, r2 = num(data.rate1), num(data.rate2)
  if r2 ~= r1 then
    rows[#rows + 1] = row12("倍率(下限)", series(r1, data.grow_powl, 2, 100))
    rows[#rows + 1] = row12("倍率(上限)", series(r2, data.grow_powh, 2, 100))
  else
    rows[#rows + 1] = row12("倍率", series(r1, data.grow_powl, 2, 100))
  end

  local rs = recastSeries(data)
  if rs then
    rows[#rows + 1] = row12("填充时间(秒)", rs)
  else
    rows[#rows + 1] = row12("填充时间(秒)", nil)
  end

  if num(data.st_time) ~= 0 or num(data.grow_st_time) ~= 0 then
    rows[#rows + 1] = row12("效果时间(秒)", series(data.st_time, data.grow_st_time, 1, 10))
  end

  rows[#rows + 1] = row12("命中修正(决定版)", dexSeries(data))

  if num(data.st_val) ~= 0 or num(data.grow_st_val) ~= 0 then
    rows[#rows + 1] = row12("效果数值", series(data.st_val, data.grow_st_val, 0, 1))
  end

  out[#out + 1] = '<table class="wikitable" style="text-align:center;">' ..
    table.concat(head) .. table.concat(rows) .. "</table>"

  out[#out + 1] = "</td></tr></table>"

  local maxRate
  if r2 ~= r1 then
    maxRate = (r2 + 11 * num(data.grow_powh)) / 100
  else
    maxRate = (r1 + 11 * num(data.grow_powl)) / 100
  end
  mw.smw.subobject({
    artId = artId,
    artName = name,
    artType = atype,
    artLearn = learn,
    artRate = maxRate,
    artRecast = num(data.recast),
    artDesc = descs[artId] or "",
    artIsTalent = isTalent and "1" or "0",
    artIsAura = aura and "1" or "0",
    artElem = source.elemName(data) or "",
    artTarget = source.tgtName(data) or "",
    artRange = source.rangeName(data) or "",
    artHate = num(data.hate),
    artChain = source.chainName(data),
  })

  return table.concat(out, "\n")
end

-- 总览表:一行一技,链接到下方折叠块的锚点(id = art-<id>)
-- 列:战技名(着色+锚点)/ 类型(元素小字缀)/ 习得 / 目标 / 连锁 / 仇恨 / 效果摘要
-- 属性列已删(2026-10-12):带攻击元素的技基本只有梅莉亚/力奇,单独一列性价比低,
-- 元素信息压缩进类型列小字(如 以太⁠雷),状态技 elem 非攻击属性不显示
local function renderOverview(ids)
  local rows = {}
  for _, id in ipairs(ids) do
    local data = source.fetchArt(id)
    if data then
      local name = (data.name and data.name ~= "") and data.name
        or source.fetchDesc(id) or ("战技 " .. id)
      local atype = ATK_TYPE[num(data.atk_type)] or "未知"
      local aura = isAura(data)
      -- 类型列:徽章 + 元素小字缀(仅攻击技且元素非"无"时)
      local typeHtml = typeBadge(atype, aura)
      local elem = source.elemName(data)
      if elem and elem ~= "无" and elem ~= "物理" then
        typeHtml = typeHtml .. '<small style="color:#777;">' .. elem .. "</small>"
      end
      -- 目标 / 范围 / 连锁 / 仇恨
      local tgt = source.tgtName(data) or "—"
      local rng = source.rangeName(data)
      if rng then tgt = tgt .. "·" .. rng end
      local chain = source.chainName(data)
      local hate = source.hateText(data) or "—"
      rows[#rows + 1] = "<tr>"
        .. '<td style="text-align:left;">[[' .. "#art-" .. id .. "|" .. name .. "]]</td>"
        .. "<td>" .. typeHtml .. "</td>"
        .. "<td>" .. learnText(data) .. "</td>"
        .. "<td>" .. tgt .. "</td>"
        .. "<td>" .. chain .. "</td>"
        .. "<td>" .. hate .. "</td>"
        .. '<td style="text-align:left;">' .. descSummary(id, 40) .. "</td>"
        .. "</tr>"
    end
  end
  if #rows == 0 then return "" end
  return '<table class="wikitable" style="text-align:center;">'
    .. "<tr><th>战技名</th><th>类型</th><th>习得</th><th>目标</th><th>连锁</th><th>仇恨</th><th>效果摘要</th></tr>"
    .. table.concat(rows)
    .. "</table>"
end

function p.main(frame)
  local args = getArgs(frame)
  local out = {}

  local first = args[1]
  local role = first and lv1.roles[first] or nil

  if role then
    out[#out + 1] = html.h2("总览")
    out[#out + 1] = renderOverview(role.ids)
    local talent, normal = {}, {}
    for _, id in ipairs(role.ids) do
      local data = source.fetchArt(id)
      local isTalent = data and num(data.arts_type) == 1
      table.insert(isTalent and talent or normal, id)
    end
    if #talent > 0 then
      out[#out + 1] = html.h2("天赋战技")
      for _, id in ipairs(talent) do
        out[#out + 1] = p.renderArt(id)
      end
    end
    if #normal > 0 then
      out[#out + 1] = html.h2("战技")
      for _, id in ipairs(normal) do
        out[#out + 1] = p.renderArt(id)
      end
    end
  else
    for _, v in ipairs(args) do
      local id = tonumber(mw.text.trim(v))
      if id then
        out[#out + 1] = p.renderArt(id)
      end
    end
  end

  if #out == 0 then
    return ""
  end

  return compress(table.concat(out, "\n"))
end

return p