装备系统 · 实现规格(契约文件)
2026-10-07 定稿。这份文件是并行开发的唯一契约来源 —— 前端/后端各自按这里写, 不要自行改字段名,需要改先改这份文件。 老大拍板的决策: 1. 装备穿给门人(不是固定主角) 2. 战斗过程在前端跑,掉落的计算在后端 3. 静态表全部放后端,前端启动拉一次 4. 金币上顶栏,纸/玄晶/灵玉放仓库材料页 5. 招募的门人都能参战,一次最多上阵 5 人,当前只有一个小队 6. 仓库是按钮打开的浮窗(不依赖建筑摆在场上) 7. 掉落技能书,交给某人学习后获得新技能
0. 项目铁律(违反必返工)
- 后端权威,前端只读。 所有状态存
server/worlds/<userId>/,前端刷新不丢。 - 除
GET /api/health、GET /api/config、POST /api/auth/login|register外,所有接口都要 token。
前端不用手写 Authorization —— src/api.js 已包装 fetch 自动注入,不要新增 NO_TOKEN_PATHS。
- 新增"按账号存的数据"必须往
server/world.mjs的getWorld()里加 Store,
不要在 index.mjs 里 new 全局单例。
- 🔴 绝不能把装备加成写回门人的本体属性
attrs。
本体属性(hp/mp/agi/str/con/int)是招募时按资质 roll 的、永不改; 装备加成只作用于「战斗属性 Stat」。三层分离见 §2。
- 掉落的 roll 必须在后端。 前端只上报"在场景 N 击杀了 Boss"这个事实。
- 时间相关结算一律挂
clock.gameSeconds(),不用真实时间。 - 注释写中文,风格沿用现有文件(顶部一段说明"为什么这么做",正文短注释)。
- 存档写入用
tmp + rename原子写(照抄server/heroes.mjs的_save())。 - ⚠️ Python 字符串/JS 模板里写中文提示词时,内层不要用 ASCII 双引号。
1. 技能表
1.1 数据结构(server/skills.mjs)
{
id: 'jianqi',
name: '剑气纵横',
school: 'jian', // jian 剑 dao 刀 gun 棍 quan 拳掌 anqi 暗器 qinggong 轻功 neigong 内功 waigong 外功
quality: 'common', // common 普通 / rare 珍稀 / epic 绝世 —— 决定技能书档次
icon: '⚔',
type: 'attack', // attack 攻击 / control 控制 / buff 增益 / heal 恢复
targets: { mode: 'multi', n: 3 }, // ★ 目标模式,见下表
hits: 1, // 段数(>1 为多段攻击)
mult: 1.6, // 伤害倍率(相对攻击力)
cost: 16, // 内力消耗
cd: 6.0, // 冷却秒
effects: [], // 附加效果,见 1.3
desc: '剑光化作三道,斩向身前之敌。'
}
1.2 目标模式(targets.mode)
| mode | 含义 | 会被传奇威能改吗 |
|---|---|---|
single | 最靠近的一只 | ✅ 可改成 multi / all |
multi | 最靠前的 n 只(n 由 targets.n 定) | ✅ 可改 n 或改 all |
random | 场上随机 n 只 | ✅ |
all | 场上所有活怪 | — |
self | 自己(增益/回复类) | ❌ |
team | 我方全体(编队 5 人) | ❌ |
运行时解析函数(放 src/idle/idleWorld.js,战斗中调用):
/** 把技能的目标模式解析成实际的敌人数组 */
export function resolveTargets(skill, mobs, heroSlotX) {
const alive = mobs.filter((m) => m.alive).sort((a, b) => a.x - b.x); // 靠近我方的在前
if (!alive.length) return [];
const t = skill.targets;
switch (t.mode) {
case 'single': return alive.slice(0, 1);
case 'multi': return alive.slice(0, t.n || 2);
case 'random': return [...alive].sort(() => Math.random() - 0.5).slice(0, t.n || 3);
case 'all': return alive;
default: return [];
}
}
1.3 附加效果(effects[])
| type | 字段 | 作用 |
|---|---|---|
armorPen | v(0–1), dur | 降低目标防御 v,持续 dur 秒 |
stun | dur | 定身/眩晕,目标停止出手 |
slow | v, dur | 目标出手间隔 ×(1+v),v 取正数表示变慢 |
dot | v(每跳伤害比例), dur | 持续伤害 |
lifesteal | v(0–1) | 伤害的 v 转为自身回血 |
reflect | v | 持续时间内反弹伤害 |
dodgeUp | v, dur | 自身闪避 +v |
atkSpeedUp | v, dur | 自身出手间隔 ×(1−v) |
damageReduce | v, dur | 自身受到伤害 ×(1−v) |
regenHp | v(每秒最大生命比例), dur | 持续回血 |
regenMp | v(一次性比例) | 立即回内力 |
heal | v(最大生命比例) | 治疗(对 self/team) |
战斗是挂机自动的,效果只做数值层,不需要额外动作素材;渲染层用已有的受击闪红/伤害数字表达即可。
1.4 入门技能(12 个 · 招募时随机 1–5 个)
| id | 名称 | 门派 | 类型 | 目标 | 倍率 | 段数 | 耗蓝 | CD | 附加 |
|---|---|---|---|---|---|---|---|---|---|
jichujian | 基础剑法 | 剑 | 攻击 | 单体 | 1.35 | 1 | 6 | 3.5 | — |
daofa | 刀法入门 | 刀 | 攻击 | 单体 | 1.15 | 2 | 8 | 4.5 | — |
gunfa | 棍法 | 棍 | 攻击 | 前排 2 | 1.05 | 1 | 12 | 6.0 | — |
changquan | 长拳 | 拳掌 | 攻击 | 单体 | 1.50 | 1 | 8 | 4.0 | 破防 15% / 4s |
zhangfa | 掌法 | 拳掌 | 攻击 | 单体 | 1.60 | 1 | 14 | 6.5 | 15% 击晕 1s |
dianxue | 点穴术 | 拳掌 | 控制 | 单体 | 0.70 | 1 | 10 | 8.0 | 定身 1.5s |
anqi | 暗器手法 | 暗器 | 攻击 | 随机 3 | 0.85 | 1 | 16 | 7.0 | — |
qingshen | 轻身术 | 轻功 | 增益 | 自身 | — | — | 12 | 14 | 闪避 +25% / 8s |
yufeng | 御风步 | 轻功 | 增益 | 自身 | — | — | 14 | 15 | 攻速 +30% / 6s |
neigong | 内功心法 | 内功 | 恢复 | 自身 | — | — | 0 | 20 | 回内力 35% |
guixi | 龟息功 | 内功 | 恢复 | 自身 | — | — | 18 | 22 | 每秒回血 1.5% / 8s |
tiebushan | 铁布衫 | 外功 | 增益 | 自身 | — | — | 16 | 20 | 减伤 40% / 8s |
1.5 高阶技能(技能书掉落,学到才拥有)
| id | 名称 | 门派 | 类型 | 目标 | 倍率 | 耗蓝 | CD | 附加 | 品质 |
|---|---|---|---|---|---|---|---|---|---|
jianqi | 剑气纵横 | 剑 | 攻击 | 前排 3 | 1.60 | 16 | 6.0 | — | 珍稀 |
liuyun | 流云剑法 | 剑 | 攻击 | 单体 | 3.10 | 30 | 9.0 | — | 珍稀 |
wanjian | 万剑归宗 | 剑 | 攻击 | 全体 | 1.40 | 42 | 13.0 | — | 绝世 |
xuezhan | 血战八荒 | 刀 | 攻击 | 全体 | 1.20 | 38 | 12.0 | 吸血 20% | 绝世 |
luohan | 罗汉伏魔 | 棍 | 攻击 | 全体 | 1.10 | 36 | 11.0 | 击晕 30% / 1.2s | 绝世 |
kanglong | 亢龙有悔 | 掌 | 攻击 | 单体 | 4.20 | 40 | 12.0 | 破防 30% / 6s | 绝世 |
baoyu | 暴雨梨花 | 暗器 | 攻击 | 随机 5 | 0.90 | 28 | 8.5 | — | 珍稀 |
jinzhong | 金钟罩 | 外功 | 增益 | 全队 | — | 30 | 22 | 全队减伤 30% / 10s | 珍稀 |
huichun | 回春术 | 内功 | 恢复 | 全队 | — | 34 | 18 | 全队回血 25% | 珍稀 |
1.6 技能书
// 技能书是"物品",落在仓库的「技能书」分页
{
id: 'bk_xxxx',
skillId: 'jianqi',
quality: 'rare', // 与技能 quality 一致
count: 1,
ts: 1791300000000
}
- 掉落:2026-10-07 起已停发(
books恒为空数组,字段保留兼容老存档;掉落见 §5.5)。 - 学习接口:
POST /api/skills/learn { bookId, heroId } - 门人技能位满(5 个)→ 返回
409,错误信息里带上"要先遗忘一个",前端弹替换选择。 - 学习成功 → 消耗技能书、门人
skills里加入该技能 id。 - 遗忘接口:
POST /api/skills/forget { heroId, skillId }(不返还技能书,防刷)。
⚠️ 门人skills字段从"中文名字符串数组"改为技能 id 数组(如['jichujian','yufeng'])。 老的存档里是中文名,HeroStore._load()里做一次兼容映射(中文名 → id,映射表放 skills.mjs)。
2. 属性三层(🔴 最容易埋雷的地方)
1 本体属性 Attr → 2 战斗属性 Stat → 3 装备/套装/威能加成
招募时 roll 公式换算 叠加在 Stat 上
永不改变 (含等级成长)
2.1 换算公式
// 门人等级成长(新增 level/exp)
export function heroGrow(level) {
const l = Math.max(1, level);
return {
hp: 1 + 0.12 * (l - 1),
mp: 1 + 0.10 * (l - 1),
atk: 1 + 0.06 * (l - 1),
def: 1 + 0.05 * (l - 1),
};
}
// 本体属性 + 装备 + 功法 → 战斗属性(真源:server/heroes.mjs)
// items 只吃已穿戴(wornBy != null)的装备;arts 是门人身上的功法(server/arts.mjs)
export function statsFor(hero, items = [], arts = []) {
const a = hero.attrs;
const g = heroGrow(hero.level || 1);
const base = {
maxHp: (90 + a.con * 6 + a.str * 2) * g.hp,
maxMp: (40 + a.int * 4 + a.con * 1) * g.mp,
atk: (8 + a.str * 1.8 + a.agi * 0.6) * g.atk,
def: (2 + a.con * 0.7) * g.def,
crit: 0.05 + a.agi * 0.004,
critDmg: 1.5 + a.int * 0.012,
atkInterval: 1.15 / (1 + a.agi * 0.012),
hpRegen: 2 + a.con * 0.12,
mpRegen: 1.5 + a.int * 0.10,
dodge: 0.02 + a.agi * 0.002,
armorPen: 0,
};
// 3 叠加装备(sumEquipStats)与功法(sumArtStats):固定值先加、百分比后乘
let flat = {}, pct = {};
for (const it of items) { /* 汇总 it.base × refineMul(refine) 与已鉴定词条 */ }
const out = {};
for (const k of Object.keys(base)) {
out[k] = (base[k] + (flat[k] || 0)) * (1 + (pct[k] || 0));
}
return out; // 取整在函数内完成(血/蓝/攻/防整数,比例项 4 位小数)
}
顺序铁律:(base + Σ固定值) × (1 + Σ百分比) × 威能系数。固定值先加、百分比后乘。
输出除上表外还有四项(真源 server/heroes.mjs):nativeBonus(是否穿着本命兵器 —— 命中时武器 base ×1.25,jobs.mjs 的 WEAPON_BONUS)、elemDmg / elemRes(功法带来的属性伤害 / 抗性,elemRes 封顶 0.75)、artAttrs(功法主属性合计,无功法为 null)。
3. 装备数据
3.1 装备实例(server/worlds/<uid>/equips.json)
{
id: 'it_k3f9a2',
defId: 'sword_duanyun',
slot: 'weapon', // weapon helmet armor pants boots ring1 ring2 necklace
quality: 'legend', // white green blue gold legend
ilvl: 42,
reqLv: 19, // 门人等级不够穿不上 = clamp(round((ilvl−4)×0.5),1,30)
refine: 5, // 加工 +0 ~ +10
star: 2, // ★ 合成星级 0~5(缺省/老存档没有这个字段 ⇒ 按 0 读)
base: { atk: 74 }, // ★ 落库定型,不每次重算
affixes: [ // 鉴定前 value 对前端遮蔽(接口里置 null)
{ id: 'aff_str', value: 15, tier: 4 }
],
identified: false,
soulId: 'soul_liuyun_multi', // 传奇威能,非传奇为 null
soulImprinted: false,
setId: 'set_liuyun', // 可空
locked: false, // ★ 锁定后禁止销毁/分解/萃取
wornBy: null, // 门人 id;在仓库则为 null
from: { stage: 42, mob: 'bandit' },
ts: 1791300000000,
}
3.2 品质 → 词条数与强化上限
| quality | 中文 | 词条数 | 强化上限 | 备注 |
|---|---|---|---|---|
white | 白 | 0 | +5 | — |
green | 绿 | 1 | +6 | — |
blue | 蓝 | 2 | +8 | 可属套装 |
gold | 金 | 3 | +10 | 可铭刻威能 |
legend | 传奇 | 3 | +10 | 额外 1 条传奇威能 |
3.3 静态表(server/equipDefs.mjs 统一导出)
导出四张表 + 两个函数:
export const EQUIP_DEFS = { /* defId → 模板 */ };
export const AFFIXES = { /* affId → 词条定义 */ };
export const SETS = { /* setId → 套装定义 */ };
export const SOULS = { /* soulId → 传奇威能定义 */ };
export const DROP_TABLE = { /* 掉落规则 */ };
/** 造一件随机装备(掉落的唯一入口,**只能后端调用**) */
export function rollItem({ ilvl, quality, slot, defId });
/** 按品质与 ilvl 汇总战斗属性加成 */
export function sumEquipStats(items);
模板示例:
'sword_duanyun': {
defId: 'sword_duanyun', name: '断云剑', slot: 'weapon', kind: 'sword',
icon: 'weapon_sword', tier: 3,
// base = baseAttrs × growth^(ilvl-1);growth 按 tier 取自 TIER_GROWTH
baseAttrs: { atk: 27 }, growth: 1.045,
affixPool: ['atk', 'str', 'crit', 'spd', 'armorPen'],
allowedQuality: ['white', 'green', 'blue', 'gold'],
setId: null, soulId: null,
}
- 每个槽位准备 3–5 个模板(白/绿/蓝/金共用模板,靠 quality 决定词条数)。
- 套装模板单独一套(
setId有值)。 - 传奇模板固定名字与固定威能。
growth由档位决定(equipDefs.mjs的TIER_GROWTH):t1 1.028 / t2 1.036 / t3 1.045 / t4 1.052 / t5 1.060。kind是形制大类(武器 = 十八般兵器之一,防具 = light/medium/heavy),职业本命武器判定与图标基名都靠它。
3.4 词条
四类共约 30 条,字段:{ id, name, category, stat, tier, min, max, slots[], weight }
value = 基准 × tier倍率 × (1 + ilvl × 0.035) × rand(0.85, 1.15)
tier 倍率: T1 ×1.00 / T2 ×1.35 / T3 ×1.80 / T4 ×2.40 / T5 ×3.20
品质决定 tier 上限: 绿 ≤T2 / 蓝 ≤T3 / 金 ≤T4 / 传奇 ≤T5
3.5 套装(6 套,见Design Docs §6)
{ setId, name, theme, slots[], bonus: { 2:{...}, 3:{...}, 4:{...} } } 只统计已穿戴件数(wornBy != null 且 setId 相同)。
3.6 传奇威能(6 类)
{ soulId, name, type, slot, desc, effect, tier } type ∈ skillMod | skillProc | skillBoost | passive | mechanic | conversion
★ 至少要有一件实现 skillMod 且把技能从单体/前排改成全体的样板:
'soul_liuyun_multi': {
soulId: 'soul_liuyun_multi', name: '流云化雨', type: 'skillMod', slot: 'weapon',
desc: '「流云剑法」改为攻击全体敌人,单次伤害降为 65%',
effect: { skillId: 'liuyun', patch: { 'targets.mode': 'all', multMul: 0.65 } },
tier: 'legend',
}
3.7 加工程度与消耗
| refine | 铁 | 石 | 金币 | 成功率 | 失败后果 |
|---|---|---|---|---|---|
| +1/+2/+3 | 3/5/8 | 2/3/5 | 60/110/180 | 100% | — |
| +4/+5/+6 | 12/18/26 | 8/12/18 | 280/420/620 | 75/65/55% | 只掉材料,不掉级 |
| +7/+8/+9 | 36/50/68 | 26/36/48 | 900/1300/1800 | 45/38/30% | 25% 概率掉 1 级 |
| +10 | 95 | 70 | 2600 | 25% | 35% 概率掉 1 级 |
1 附加材料(REFINE_MATS) —— 到这一档才解锁的新材料:
| refine | 附加材料 |
|---|---|
| +1~+3 | — |
| +4 | 纸 ×1 |
| +5 | 纸 ×2 |
| +6 | 纸 ×3 + 玄铁 ×1 |
| +7 | 纸 ×4 + 玄铁 ×2 |
| +8 | 纸 ×6 + 玄铁 ×3 + 玄晶碎片 ×1 |
| +9 | 纸 ×9 + 玄铁 ×5 + 玄晶碎片 ×2 |
| +10 | 纸 ×14 + 玄铁 ×8 + 玄晶碎片 ×4 + 灵玉 ×1 |
2 品质系数(QUALITY_COST_MUL) —— 铁 / 石 / 金币与附加材料都乘它:白 1.0 / 绿 1.25 / 蓝 1.6 / 金 2.1 / 传奇 2.8。
实际消耗 =(基础曲线 + 该档附加材料)× 品质系数(金币十位取整、材料至少 1,scaleCost);武器铺等级的折扣再乘一层(§5.3 的costMul)。
属性收益:+1~+3 每级 +8%、+4~+6 每级 +11%、+7~+9 每级 +14%、+10 一次性 +20%(累积;refineMul 同时用于 baseEff 与穿戴后的战斗属性)。
3.8 鉴定消耗
🔴 2026-10-14 起只有传奇需要鉴定(老大:「什么都要鉴定,好麻烦 —— 设置橙装以上才鉴定」)。 判据收在 equipDefs.IDENTIFY_QUALITIES = ['legend'] / needsIdentify(q), rollItem 里 identified = !needsIdentify(q) —— 凡品/良品/精品/宝器 掉落即已鉴定。
鉴定费 = 金币基准(IDENTIFY_BASE)× (1 + ilvl×0.02) + 品质档固定材料(IDENTIFY_MATS):
| quality | 消耗 |
|---|---|
| white / green / blue / gold | 无需鉴定(掉落即 identified: true;IDENTIFY_BASE 里那几条已作废,留表只为"五档列全") |
| legend | 金币 2000 × (1+ilvl×0.02) + 纸 ×16 + 玄铁 ×8 + 玄晶碎片 ×4 + 灵玉 ×1 |
低四档出生就是已鉴定(rollItem),前端不该给「鉴定」入口(按钮 disabled + 文案"这类装备不需要鉴定"); 后端identify()对它们直接 409「…装不需要鉴定,直接就能用(只有传奇要鉴定)」,并把老存档里标着未鉴定的低档装备顺手补正。 ⚠️ 那句 409 的文案不能写成"已经鉴定过了"—— 会让玩家以为"我是不是点晚了"(2026-10-08 定的口径)。
3.9 销毁产出
| quality | 铁 | 石 | 玄晶碎片 | 灵玉 |
|---|---|---|---|---|
| white | 1–2 | — | — | — |
| green | 2–4 | 1–2 | — | — |
| blue | 4–7 | 3–5 | 30% ×1 | — |
| gold | 8–14 | 6–10 | 60% ×2 | 25% ×1 |
| legend | 不可直接销毁 → 走萃取,产出「传承之魂」 |
加工过的按 50% × refine 返还铁石。
3.10 装备合成(升星,2026-10-11)
老大原话:「装备也能合成,3 个相同装备可以合一个新的,也是升星级。属性增加也是 1.5 倍。 合成后的随机属性是随机生成的。合成要消耗一定的资源。」+ 追问定下的三条口径。
规则
| 项 | 口径 |
|---|---|
| 「相同」 | 同 defId + 同 quality(装等不参与 —— 装备是按模板掉的实例、装等带抖动,要求三件装等也一样就是凑不齐) |
| 数量 | 3 件 ★n → 1 件 ★(n+1),递推到 EQUIP_MAX_STAR = 5 |
| 星级倍率 | 每星 ×1.5(equipStarMul):0★×1.00 / 1★×1.50 / 2★×2.25 / 3★×3.375 / 4★×5.0625 / 5★×7.5938 |
| 作用层 | 只放大落库定型的 base(与加工同一层:refineMul × 专精 × equipStarMul),不动词条 |
| 产出 | 新的一件:词条重新 roll(不继承原料)、品质与模板不变、ilvl = 三件原料的最高值、refine = 0、出生即 identified: true、from = { mob: 'fuse' }(出处显示「合成所得」) |
| 原料池 | 仓库里 wornBy == null && !locked 的同款(穿在身上 / 锁定的不参与) |
| 选取 | 同组取最老的 3 件(ts 升序,同 ts 用 id 打平);传奇也能合(合成不是销毁,与萃取不冲突) |
消耗表(EQUIP_FUSE_COST,键 = 产出星级) —— 铁 + 石 + 金币,再乘品质系数 (EQUIP_FUSE_QUALITY_MUL = 加工用的 QUALITY_COST_MUL:白 1.0 / 绿 1.25 / 蓝 1.6 / 金 2.1 / 传奇 2.8):
| 产出星级 | 铁 | 石 | 金币 |
|---|---|---|---|
| ★1 | 8 | 6 | 600 |
| ★2 | 20 | 15 | 3,000 |
| ★3 | 50 | 38 | 14,000 |
| ★4 | 120 | 90 | 55,000 |
| ★5 | 280 | 210 | 220,000 |
实际消耗走equipFuseCost(quality, star)→scaleCost(金币十位取整、材料至少 1)。 合到 ★5 共需 3⁵ = 243 件同款原料;白装全程约 200 万金币量级。
接口:POST /api/equips/fuse({ defId, quality, star },star = 原料星级 0..4)、 POST /api/equips/fuseAll({ defId?, quality? },不给 = 全部)。失败码: EQUIP_FUSE_NOT_ENOUGH(不够 3 件)、EQUIP_FUSE_MATERIAL(材料不够)—— 两者都不动账。
🔴 三条不能松的约束
- 模板必须允许这一档品质(
def.allowedQuality.includes(q)):不查的话rollItem
会静默降档,三件金装能合出一件蓝装;
- 造产物在扣料之前:
rollItem是纯内存操作,失败零损失;反过来写就是"料没了、装备也没了"; - 星级要进
baseEff(maskItem)与sumEquipStats两处:只改一处就会出现
"打怪变强、卡片数字纹丝不动"(加工那次的坑)。
3.11 穿戴等级需求(reqLv,2026-10-14 下调)
reqLv = clamp(round((ilvl − 4) × 0.5), 1, 30) —— 整体减半 + 30 级封顶。
| ilvl | 旧 max(1, ilvl−4) | 新 | ilvl | 旧 | 新 |
|---|---|---|---|---|---|
| 5 | 1 | 1 | 40 | 36 | 18 |
| 10 | 6 | 3 | 50 | 46 | 23 |
| 20 | 16 | 8 | 60 | 56 | 28 |
| 30 | 26 | 13 | 63+ | 59+ | 30(封顶) |
- 唯一入口
equipDefs.reqLvOf(ilvl)(REQ_LV_RATIO = 0.5/REQ_LV_CAP = 30)。
rollItem 出生写库;equips._load() 对老存档按同一公式重算(不是"取小")。
- 🔴 不倒挂(老大追加:「穿戴等级需求和装备属性、品质不要倒挂」)——
reqLv 是 ilvl 的纯函数:1 对 ilvl 单调不减;2 品质不参与(同装等五档门槛相同, 不会"传奇比白装还好穿");3 星级 / 加工不参与(只涨强度不动门槛, 不会"我加工完反而穿不上");4 迁移必须重算,否则外部改过的"高装等低门槛"会断掉单调性。
- 为什么下调:旧口径下第 71 关(ilvl 73)要 69 级才穿得上,而
heroes.expToNext = 52 × 1.23^(L−1) 让 30 级往后基本到不了 ⇒ 后期掉落全是"看得见摸不着"。
- 验收:
tools/verify_mats.mjs⑧ 节(单调 / 封顶 / 按比例下调 / 品质无关 / 存档一致 / 真门还在)。
4. 编队
// server/worlds/<uid>/squads.json
{
squads: [
{ id: 'sq_default', name: '第一小队', members: ['h_xxx', 'h_yyy'], maxMembers: 5 }
],
activeSquadId: 'sq_default',
}
- 当前只有一个小队(
sq_default),maxMembers = 5。 - 成员必须是已招募的门人;同一人不能进两个队(当前只有一队,天然满足)。
- 上阵顺序 =
members数组顺序 → 决定战斗里的站位(第一个在最前)。
接口:
GET /api/squad→ 返回小队 + 所有已招募门人的战力摘要(方便前端选择)POST /api/squad/set { members: [heroId, ...] }→ 覆盖式设置(≤5 人,校验都是已招募的)
5. 后端接口契约
全部需要 token。响应统一{ ok, ... };出错返回{ error, status }。
5.1 静态表
| Method | 路径 | 返回 |
|---|---|---|
| GET | /api/game/meta | { skills, equipDefs, affixes, sets, souls, attrDefs, equipSlots }(一次拉全,前端缓存) |
5.2 技能
| Method | 路径 | 请求体 | 说明 |
|---|---|---|---|
| POST | /api/skills/learn | { bookId, heroId } | 技能位满返回 409 |
| POST | /api/skills/forget | { heroId, skillId } | 不返还技能书 |
技能书列表跟仓库一起返回(GET /api/warehouse 的 books 字段)。
5.3 仓库 / 装备
| Method | 路径 | 请求体 | 返回要点 |
|---|---|---|---|
| GET | /api/warehouse | — | { items, books, souls, mats, capacity, used, maxStar, starMul, fuseCost, fuseQualityMul } ← 后四项是合成曲线,由后端下发(见 §3.10) |
| GET | /api/warehouse/hero/:heroId | — | { hero, items, stats, sets, souls, powerScore } ← 穿上后的战斗属性 |
| POST | /api/equips/equip | { itemId, heroId, slot? } | slot 可选:戒指互通时指定落 ring1 / ring2(不传落实例自带 slot);原装备自动回仓库 |
| POST | /api/equips/unequip | { heroId, slot } | — |
| POST | /api/equips/lock | { id, locked } | — |
| POST | /api/equips/identify | { id } | 扣金币/材料 → 揭晓 |
| POST | /api/equips/refine | { id } | 扣铁石金币 + 附加材料 → roll 成功率(成本受武器铺折扣,见下) |
| POST | /api/equips/destroy | { ids: [] } | 返回 { destroyed, gained, skipped }(锁定的进 skipped) |
| POST | /api/equips/extract | { id } | 传奇 → 传承之魂 |
| POST | /api/equips/imprint | { soulId, itemId } | 铭刻威能 |
| POST | /api/equips/fuse | { defId, quality, star } | 合成一件(3 件 ★star → 1 件 ★star+1);见 §3.10 |
| POST | /api/equips/fuseAll | { defId?, quality? } | 一键合成:从低星往高星链式推,材料不够就停 |
⚠️ 未鉴定装备在响应里必须把 affixes[].value 置为 null(前端渲染成 ???), 否则改前端就能提前看到词条。
出库的装备实例(maskItem,仓库列表与门人视图同一份):带 name、kind、star(归一 0..5,缺省 0)、baseEff(= base × refineMul(refine) × equipStarMul(star),加工与星级后的实际基础属性 —— 详情卡显示它,base 是落库原值不要覆盖)、wornSlot(这件装备当前穿在哪一格,没穿为 null;戒指按它渲染,不按实例自己的 slot)。两处 souls 形状不同:仓库 = 威能册 [{ soulId, name, count, ts }],门人视图 = 身上已生效的威能定义数组。
equip 的 409:1 越权槽位(slot 传了这件装备装不进的格子,如给武器传 ring1);2 未鉴定不能穿;3 等级不足;4 武器 kind 必须等于职业的 weaponKind(本命兵器不符)。slot 是未知槽位名 → 400。
refine 的 costMul:由武器铺等级折算 —— 路由层调 refineDiscount(ctx.buildings.levelOf('wuqipu'))(buildingLevel.mjs:每级 −5%,1 级 = 无折扣),equips.mjs 再夹到 [0.5, 1]。只影响成本数字,不影响成功率。
5.4 挂机战果上报(★ 掉落在这里算)
POST /api/idle/report
{
stageIdx: 42,
waveSeq: 108, // ★ 必填:本波序号(缺了/非法直接 400)—— 后端按 (stageIdx, waveSeq) 幂等去重
waveKills: 3, // 本次上报的这一波击杀数(后端夹到 ≤ waveKillCap = 15)
isBoss: true, // 这一波是不是 Boss
bossCleared: true, // Boss 是否被击败
}
→ {
ok: true,
duplicate: false, // 重复上报 → true,各产出字段全空
stageIdx: 42,
ilvl: 43, // 本波掉落用的物品等级 = 场景等级 ± 2
drops: [ /* 完整装备实例(已入库、已遮蔽) */ ],
books: [], // 恒为空数组(技能书 2026-10-07 停发,字段保留兼容老存档)
mats: [ { id, qty, name, img, icon } ],
gold: 320,
exp: 900, // 实际发出的经验总额(人均 × 人数)
expTargets: [ { heroId, exp, level, restExp } ], // 每人本次到账 / 结算后等级 / 升级后的剩余经验
leveled: [ /* 本次真的升过级的门人:{ id, name, level, exp, levels } */ ],
}
- 后端按
stageIdx取 ilvl 与掉落概率(§5.5),roll 出结果并写进 equips.json,返回给前端播动画。 - 同一个
(stageIdx, waveSeq)重复上报要幂等(带waveSeq递增序号,后端记已处理的序号,重复时返回duplicate: true+ 空结果)。 - ⚠️ 单次上报的掉落数量硬上限(
DROP_TABLE.caps):≤4 件装备 / ≤2 本书 / ≤4 种材料,防止伪造。 expTargets里的restExp是升级结算之后账本里的剩余经验,前端直接对齐 —— 不要拿exp自己再结算一遍(会双重升级)。
5.5 掉落表
真源:server/equipDefs.mjs 的 DROP_TABLE。
| 来源 | 装备 | 判定 |
|---|---|---|
| 普通怪(野猪) | 20% 掉 1 件 | common.equipChance = 0.20 |
| Boss(山贼) | 100% 掉,至多 3 件 | boss.equipChance = 1.0 / maxEquip = 3,只在 bossCleared 时结算 |
| 关底头目(山贼头目) | 必掉 4 件,必出金以上 | lord.minEquip = maxEquip = 4 / minQuality = 'gold';是不是关底由后端 isEndOfRegion(stageIdx) 判 |
| 游历节点首通 | 固定 1 件(必出蓝以上) | journey.equip = 1 / minQuality = 'blue';每节点只给一次(cleared 集合),不占关底配额(关底波时额外给) |
技能书:2026-10-07 起停发 —— books 恒为空数组,字段保留兼容老存档。
品质概率按场景等级分档(qualityBands):
| 场景等级 | 白 | 绿 | 蓝 | 金 | 传奇 |
|---|---|---|---|---|---|
| ≤15 | 42% | 36% | 19% | 3% | — |
| ≤35 | 22% | 34% | 30% | 12% | 2% |
| ≤55 | 8% | 26% | 34% | 24% | 8% |
| >55 | 2% | 14% | 32% | 36% | 16% |
ilvl = 场景等级 ± 2(ilvlJitter = 2,下限 1;「场景等级」= stageIdx + 1)。首通件必出蓝以上。
材料掉落(DROP_TABLE.mats —— 纸 / 玄铁 / 玄晶碎片 / 灵玉的产出):
| 来源 | 掉不掉 / roll 几次 | 数量倍率 |
|---|---|---|
| 普通怪 | 55% 掉、roll 1 次 | ×1 |
| Boss(山贼) | 必 roll 2 次 | ×2 |
| 关底头目 | 必 roll 3 次 | ×3 |
| 游历首通 | 保底 roll 1 次(必给) | ×1 |
掉哪几种按场景等级加权:
| 场景等级 | 纸 | 玄铁 | 玄晶碎片 | 灵玉 |
|---|---|---|---|---|
| ≤15 | 86% | 14% | — | — |
| ≤35 | 55% | 32% | 13% | — |
| ≤55 | 38% | 34% | 22% | 6% |
| >55 | 28% | 34% | 27% | 11% |
单次数量区间(mats.qty):纸 1–3 / 玄铁 1–2 / 玄晶碎片 1 / 灵玉 1,再乘数量倍率。⚠️「roll 几次」不是「几种」—— 同一材料被重复抽中时数量累加,低档位(只有纸 / 玄铁)实际最多 2 种;单次最多 4 种(caps.mat)。
6. 资源扩展
在 server/resources.mjs 的 inventory 里新增 5 项(key 与代码严格一致)。纸 / 玄铁 / 玄晶碎片 / 灵玉没有产出建筑(building: null),只从战斗掉落来(玄晶 / 灵玉另有分解装备的小量产出,见 §3.9):
| key | 名称 | 上顶栏? | 来源 |
|---|---|---|---|
gold | 金币 | ✅ 上 | 战斗掉落、战斗结算 |
paper | 纸 | ❌ 仓库材料页 | 战斗掉落(无产出建筑) |
xuantie | 玄铁 | ❌ 仓库材料页 | 战斗掉落(无产出建筑) |
crystal | 玄晶碎片 | ❌ 仓库材料页 | 战斗掉落 + 分解蓝/金装(§3.9) |
jade | 灵玉 | ❌ 仓库材料页 | 战斗掉落 + 分解金装(§3.9) |
⚠️ 顶栏现在 4 项(铁/石/木/粮),加上金币共 5 项。不要再往顶栏加。
7. 建筑:仓库
// src/buildings.js 的 BUILDINGS + server/buildings.mjs 的 BUILDING_DEFS(两处同步)
{ id: 'cangku', name: '仓库', icon: '🏦', file: 'cangku.glb',
cells: 'quad', target: 4.6, limit: 1, desc: '占 4 格' }
- 模型:从
d:/3d-game/game/assets/models/bld_cangku_01.glb
复制到 public/assets/buildings/cangku.glb。
- 仓库界面由 HUD 上的「仓库」按钮打开(不依赖建筑是否已建)。
同时保留点击 3D 仓库模型也能打开(照 src/main.js 里点大殿开招募浮窗的写法加一条)。
8. 文件职责划分(并行开发用,避免互相改同一个文件)
| 文件 | 负责 |
|---|---|
server/skills.mjs server/equipDefs.mjs server/equips.mjs server/squads.mjs server/heroes.mjs server/resources.mjs server/buildings.mjs server/world.mjs server/index.mjs | 后端 |
src/warehouse.js(新)· index.html · src/main.js · src/api.js | 前端 UI |
src/hall.js(装配/编队/技能书) | 前端 UI |
src/idle/idleWorld.js src/idle/idleBattle.js src/idle/idleBand.js · src/buildings.js | 前端战斗 |
共享文件的接口先约定死:
// main.js 里这样装配(前端 UI 负责写,前端战斗只需按签名实现)
const battle = new IdleBattle({ squad: [], report: postIdleReport });
// battle.setSquad(heroesWithEquips) // 客户端拿到小队数据后灌进去
最近更新
- 2026-10-11:装备合成(升星)上线 —— 新增 §3.10(规则 / 消耗表 / 三条硬约束)、
装备实例加 star 字段、baseEff 纳入星级倍率、GET /api/warehouse 下发 maxStar / starMul / fuseCost / fuseQualityMul、新增 POST /api/equips/fuse 与 /api/equips/fuseAll。 验收:node tools/verify_equip_fuse.mjs(75 条)+ node tools/verify_equip_fuse_ui.mjs(38 条)。
- 2026-10-10:按实现核对修正 —— 鉴定材料表、加工附加材料与品质系数、武器铺 costMul 折扣、
视图新增字段(souls/baseEff/wornSlot/name/kind)、equip 的 slot 参数与两条 409、 防伪上限(4/2/4)、上报 waveSeq 与返回字段、掉落表与品质权重四档、材料掉落节、 TIER_GROWTH 示例、statsFor 签名、材料来源口径。