山门厢房系统 · 设计与落地稿 v1 IDLE WUXIA · SYSTEM DESIGN
山门厢房系统
厢房建造 · 招募名额 · 三人阵型 · 开号创建主角
一次把「住哪儿 → 能招几个 → 谁能上阵 → 从谁开始」这条链打通。 厢房是山门的人口容量建筑:先盖房,才招得到人。
版本 v1 状态 待评审 可建格 89 厢房上限 3 → 15 上阵 5 → 3
🔴 全文状态:机制已改制(2026-10-10)—— 本文的「建造 / 数量上限」口径整体失效
老大 2026-10-10 拍板:「把原来的数量限制改成建筑的等级限制」 + 「默认每个建筑一开始就已经建造好,并且已经是 1 级了」。据此:
- 建造关闭:
POST /api/buildings返回 400(BUILD_DISABLED),
厢房开局就有 1 间,玩家不再"盖房"、也不再"拆房"。
- 名额改由等级给:门人名额 = 1(开局那间)+ 厢房等级加成(每级 +5,1 → 5 级)
→ 1 级 1 人、5 级 21 人。(步长 2026-10-11 由每级 +1 改为每级 +5; 招募台一次刷 5 个候选,步长与"一排 5 张卡"同频。) 旧的「声望档位 → 能建 3~15 间」已被 prestige.LEVEL_TIERS(5 档 → 等级上限)取代。
- 本文 §1.2 建造规则、§1.3 上限曲线、§1.4 造价、§2.1 名额算法、§2.3 双保险
均按旧机制撰写,保留作历史记录;现行口径见 建筑与等级。
- 仍有效的部分:§2.2 谁不占名额(主角不占)、§3 三人阵型与仇恨权重、
§4 开号创建主角 —— 与建筑机制无关。
需求四件事,一条链
1 厢房:设定 → 建模 → 建造
先出设定图定样式,再用 Blender 建低模。占 1 格,山上任一可建格都能盖。初始上限 3 间,随地图进度解锁更多。
2 厢房 → 招募名额
1 间 = 1 个门人名额。没盖厢房就招不到人。初始主角不占名额。
3 上阵 5 人 → 3 人
分为前排 / 中间 / 后排,三格自由拖放,主角也占一格。前排更易被攻击、后排更安全。
4 重新开号 → 创建主角
强制引导:不建主角进不了作品。自定义名字 + 职业 + 性别,性别仅外观。
已确认的四条口径
| 决策点 | 口径 | 影响面 |
|---|---|---|
| 厢房 ↔ 人数 | 1 间 = 1 人,初始可招 3 人 | heroes.recruit |
| 扩展节奏 | 每完成 1 个区域 +1 间 → 上限 3…15 | ranks.regionsDone |
| 主角与阵位 | 3 格完全自由,主角也占一格 | squads / hall |
| 排位差异 | 带数值:前排更易被攻击 | idleBattle._pickUnit |
01厢房:山门的人口容量
1.1 定位 —— 它不产资源,它"住人"
现有建筑分两类:功能建筑(大殿 = 招募台 / 仓库 = 储物 / 武器铺)与 产出建筑(铁矿 / 采石场 / 伐木场 / 农田,每 300 作品秒产一次料,见 server/resources.mjs 的 PRODUCE_SEC)。
厢房是第三类:容量建筑。它不进 resources.mjs 的产出表( buildingToRes 认不出它就跳过),唯一的产出物是 "多一个门人名额"。
一句话契约
可招募门人上限 = 已建造的厢房数,主角(isStarter)不占名额。
1.2 建造规则 —— 一格一间,四校验照旧
厢房走已有的建筑投放链路(src/buildings.js → POST /api/buildings → BuildingStore.add()), 不新开系统。落位四校验原样继承:
- 格子存在 —— 必须在山的轮廓内
- 同一高度 —— 四格不同台阶会让建筑悬空/穿模(厢房占 1 格,天然满足)
- 无占用 —— 后端
occupied()集合直接卡住,防两个标签页同时建
- 非峰顶 ——
NO_BUILD_LEVEL = MAX_STEP_N - 1,最高一级台阶留作景观
大殿 · hex9(占 9 格)
武器铺 · quad(占 4 格)
厢房 · single(占 1 格)
← 正六边形一格
山上可建格 · 实测 89 格
4×4 平台 32 格 / 6×6 平台 36 格 / 单格平台 5 格 / 山顶平台 16 格
全通时的格位账(上限 15 间)
■ 厢房 15 ■ 大殿 9 ■ 武器铺 4 + 仓库 4 ■ 资源建筑 4 → 共 40 格 / 89 格,余量 55%
图 1 · 三种占格的对比,以及 15 间厢房在全通时的格位占用核算(89 格余量充足)
⚠️ 本节的「上限口径」已被声望系统取代(2026-10-08)
厢房上限不再等于 3 + 已完成区域数,改为由声望档位给出 (server/prestige.mjs 的 TIERS 表,仍是 3 → 15 间)。 区域数只通过"通关给历练声望"这一条间接影响上限。
现行口径见 声望系统设计.html。本节保留作历史记录 —— 注意它的曲线与现行档位并不重合(例:全通时旧口径 15 间,而只带主角的新口径是 12 间, 剩下 3 间要靠门派声望补)。
下文凡出现「每完成 1 个区域 +1 间」「3 + 区域数」之处,均请按上述新口径理解。
1.3 上限曲线 —— 每完成 1 个区域 +1 间
与资质阶梯同一把尺子:都读 server/ranks.mjs 的 regionsDone(stageIdx)(12 个区域、索引 = 已完成区域数)。 不新造进度口径 —— 两个系统读同一个函数,才不会出现"资质涨了但厢房没涨"。
15 9 3 已完成区域数(0 → 12) 厢房上限
开局 3 间
全通 15 间
0 6 12
图 2 · 厢房上限曲线:上限 = 3 + regionsDone(stageIdx),线性、无断点
| 已完成区域 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 厢房上限 / 门人名额 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
| 名册总人数(含主角) | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
| 当前关卡区间 | 1-6 | 7-12 | 13-18 | 19-24 | 25-30 | 31-36 | 37-42 | 43-48 | 49-54 | 55-60 | 61-66 | 67-70 | 全通 |
⚠️ 上限只放开"能盖几间",不等于"自动盖好"
进度到了只是把 limit 抬高,玩家仍要自己去山上把厢房盖下来。 这样"扩厢房"才有动作感;自动补房会把建造系统架空。
1.4 厢房造价 —— 建议沿用"免资源"
现有 7 种建筑全都不消耗资源(BUILDING_DEFS 没有 cost 字段, POST /api/buildings 只做占格校验)。给厢房单独加造价会破坏一致性, 而且"先盖 3 间房"是开局的第一个动作,收费会卡住新手。
结论:v1 不收费。若后续要给建造加经济,应该一次性给所有建筑统一加 (在 BUILDING_DEFS 加 cost 字段 + 路由层扣资源), 而不是只卡厢房。
02厢房 → 招募名额
2.1 名额怎么算
已招募门人数(不含主角) < 厢房数 才允许招募。
等价写法:canRecruit = nonStarterCount < roomCount
| 场景 | 期望行为 | 校验层 |
|---|---|---|
| 0 间厢房 + 点招募 | 拒绝(409),提示「先去山上盖一间厢房」 | 后端权威 + 前端禁用 |
| 3 间厢房 + 已招 3 人 + 点招募 | 拒绝(409),提示「厢房已满(3/3)」 | 后端权威 + 前端禁用 |
| 主角 + 3 人 + 3 间厢房 | 放行 —— 主角不占名额 | isStarter 豁免 |
| 遣散 1 名门人 | 名额立刻释放,可再招 1 人 | dismiss() |
| 拆掉一间厢房(有门人住) | 拒绝 —— 提示「有门人居住,先遣散再拆」 | 后端权威 |
| 进度涨了但没盖房 | 名额不变(上限 ≠ 实际间数) | 读实际建造数 |
🔴 名额的分子是"实际建造数",不是"上限"
这是最容易写错的一处:limit 是能盖几间, count 是已经盖了几间。名额必须读后者 —— 否则玩家一次厢房都没盖,就能凭空招满 15 人。
⚠️ 2026-10-10 口径变更:建造关闭后厢房恒为"开局那 1 间", 名额 = 1 + 厢房等级加成(每级 +5,1 → 5 级)(步长 2026-10-11 由 +1 改为 +5)。 上面这条"不读上限"的精神不变,只是加数从"间数"换成了"等级"。
2.2 角色标记:谁不占名额
server/heroes.mjs 里已有 isStarter 标记 (ensureStarter() 写入)。名额统计只认这一条:
// heroCount = 需要厢房的人数(主角不算) const heroCount = heroes.filter(h => !h.isStarter).length; const canRecruit = heroCount < roomCount;
老存档(已有主角但没这个字段)走一次性迁移:把最早入伙的那位 (recruitedAt 最小)标记为 isStarter。 迁移幂等,写在 heroes._load() 里。
2.3 双保险:前端禁用 + 后端拒绝
沿用项目既有铁律 —— 前端禁用按钮只是体验,权威必须在后端。 前端从 /api/hall 的 economy 里读 heroCap / heroCount / roomCount,名额满时按钮置灰 + 说明原因; 后端在 POST /api/hall/recruit 里复核,超编返回 409。
03阵型 5 → 3:前排 / 中间 / 后排
3.1 三格语义
三格沿用现有的固定站位模型(members 数组允许 null 表示空格)。坐标方向约定不变:
- 界面:最左 = 后排,最右 = 前排(
hall.js的gridOf())
- 契约:
members[0]= 最前排,下标越大越靠后
- 战斗:
unitSlotX(i) = HERO_X_RATIO - UNIT_GAP × i,i=0 站最右(最接近怪)
我方纵深 迎敌方向 ▶
后排 slot 2 · 最安全 权重 1
中间 slot 1 权重 2
前排 slot 0 · 最危险 权重 3
👹 加权选目标
满员 3 人时的受击概率分布
前排 50.0% 中间 33.3% 后排 16.7%
图 3 · 三格阵型与仇恨权重:前排 3 / 中间 2 / 后排 1,满员时受击概率 50 : 33 : 17
3.2 仇恨权重 —— 把"永远打最前排"改成加权随机
现状 idleBattle._pickUnit() 是确定性的:取 x 最大的那位, 也就是永远只打最前排。3 人阵下这会让前排必死、后排完全不挨打, 排位就失去了"风险分配"的意义。
改法
按格位给权重 前排 3 / 中间 2 / 后排 1,在存活的我方单位里加权随机。
| 场上存活情况 | 格位权重 | 受击概率 |
|---|---|---|
| 满员 3 人 | 3 : 2 : 1 | 50.0% / 33.3% / 16.7% |
| 2 人(前排 + 中间) | 3 : 2 | 60.0% / 40.0% |
| 2 人(前排 + 后排,中间空) | 3 : 1 | 75.0% / 25.0% |
| 1 人(只放后排) | 1 | 100% |
| 前排阵亡,剩中 + 后 | 2 : 1 | 66.7% / 33.3% |
权重只作用在目标选择上,不额外改承伤倍率 —— "前排更易被打"本身就自然产生了"前排掉血更快"的结果。 单一规则、可解释、可验证(verify_battle_front.mjs 统计 1000 次分布)。
⚠️ 连带的必改项:编队进战斗时不能把空位压紧
现在 src/main.js 的 refreshSquad() 会 members.filter(Boolean),而 idleBattle._buildSquad() 按压紧后的下标算 slot 和 x。后果:
- 玩家把某人拖到后排、把前排留空 → 战斗里他却被顶到前排挨打
- 排位权重全按错位算,"后排更安全"这条规则被静默吞掉修法:注入战斗时保留原始格位。每个 unit 带
slot(0/1/2),
_buildSquad 用 u.slot 而非数组下标来定 x 与排位。 界面上的"站哪儿"与战斗里的"站哪儿"必须是同一个数。
3.3 老存档迁移
已上阵 5 人的存档:SquadStore._load() 已有 .slice(0, MAX_MEMBERS),5 → 3 自动生效。 保留语义为"留前排 3 位"(members[0..2],因为 members[0] 是最前排)—— 被截掉的两人回到名册待命,不丢人、不丢装备。
04开号创建主角
4.1 强制引导流程
1
注册 / 重新开号
Email + Password注册,或点「重新开始」清档
2
查状态
/api/hall 返回 needStarter:true
3
创建主角
填名字 · 选职业(18 选 1)· 选性别
4
发装备
本命白武器 + 本门派起手功法(后端自动)
5
进作品
主角默认上阵前排;未创建则被浮层阻断
🔴 阻断是"进不了作品",不是"弹个提示"
选择强制口径:创建界面的浮层不可关闭(无 ×、点遮罩不关、Esc 无效)。 后端也要能拦住 —— 除 /api/health、/api/config、 鉴权接口与创建主角接口本身外的业务接口,在无主角时返回 409。 否则前端绕过一次,新号就能在"没有主角"的状态下跑挂机。
4.2 三个输入项
| 输入 | 规则 | 校验 |
|---|---|---|
| 名字 | 2–8 个字符,允许中英文数字,自动 trim;不允许与其他门人重名 | 后端 400 + 前端即时提示 |
| 职业 | 18 职业全开放,按 6 类定位分组展示(近战力 / 近战敏 / 坦克 / 控制 / 远程 / 辅助) 每项显示:职业名 · 兵器 · 定位 · 一句描述 | 必须命中 jobs.mjs 的 jobId,否则 400 |
| 性别 | m / f,仅影响立绘与头像,不进属性、不进战斗 | 取值白名单 |
创建后由现有链路自动补齐(复用 world.mjs 的 grantStarterWeapon() / arts.grantStarterArt()): 本命白武器(按职业的 weaponKind)+ 本门派起手功法 + 默认上阵前排。
4.3 老账号怎么办
- 已有主角的老账号:
needStarter = false,不弹创建界面,原主角(无名 / 剑侠 / 辛 / 男)原样保留。
starterGranted落盘标记语义不变 —— 造过一次就不再造,玩家把主角遣散了也不会又冒出来。
- 老账号若想换主角:走「重新开始」(
POST /api/world/reset),清档后重新创建。
05数据契约与改动清单
5.1 涉及的文件
| 文件 | 改动 | 性质 |
|---|---|---|
| server/buildings.mjs | 加 xuanfang 定义(cells: single);add() 支持动态 limit(由路由层算好传入);拆除时校验"有门人居住则拒绝" | 改 |
| server/heroes.mjs | 加名额校验(recruit(id, { cap }));isStarter 老存档迁移;新增按 名字/职业/性别 创建主角的入口 | 改 |
| server/squads.mjs | MAX_MEMBERS 5 → 3 | 改 |
| server/index.mjs | 新增 POST /api/hero/starter;POST /api/hall/recruit 与 POST /api/buildings 接上厢房数/名额;/api/hall 的 economy 补 heroCap / heroCount / roomCount / needStarter | 改 |
| src/buildings.js | BUILDINGS 加厢房(icon / limit 动态 / desc) | 改 |
| src/hall.js | 上阵区 5 → 3 格 + 前/中/后标签;招募台显示名额;新增创建主角界面 | 改 |
| src/main.js | refreshSquad() 保留原始格位(不再 filter 掉空位);接入创建主角引导阻断 | 改 |
| src/idle/idleBattle.js | MAX_SQUAD 5 → 3;_buildSquad 用 u.slot;_pickUnit 改加权随机 | 改 |
| public/assets/buildings/xuanfang.glb | Blender 新建模型 | 新 |
| docs/厢房设定图/ | Seedream 生成的设定图(正 / 侧 / 三视角) | 新 |
5.2 存档格式
buildings.json 不变
{ "buildings": [ { "defId": "xuanfang", "q": 3, "r": -2, "ts": 1759... } ] } 厢房复用同一张表,不新增文件。
heroes.json 补字段
{ "starterGranted": true, "heroes": [ { "id": "h_1", "name": "无名", "job": "swordsman", "gender": "m", "isStarter": true } ] } isStarter 已有,只补迁移。
⚠️ 加后端接口的两个必改点(项目既有铁律)
world.mjs的getWorld()里 new —— 不要全局单例,否则所有账号共享数据
index.mjs的makeCtx()是显式列举字段的,跨 Store 协作(读world.buildings算名额)必须在路由层做
06落地顺序与验收判据
6.1 实施顺序(低耦合优先)
| 序 | 步骤 | 为什么排这个位置 |
|---|---|---|
| 1 | 厢房设定图(Seedream)→ Blender 建模 → glb 入库 | 素材链路最长,先启动不阻塞代码 |
| 2 | 后端:厢房建筑 + 名额校验 + 3 人阵 + 创建主角接口 | 契约先固化,前端照着做 |
| 3 | 前端:建筑面板 / 大殿名额 / 上阵 3 格 / 创建主角引导 | 依赖 2 的接口形状 |
| 4 | 战斗:slot 保留 + 仇恨权重 | 与前端同改 main.js,放在同一批做避免冲突 |
| 5 | 验收脚本 + 全链路回归 | 收尾 |
6.2 验收判据(5 条脚本)
| 脚本 | 关键断言 |
|---|---|
| verify_xuanfang.mjs | 上限曲线 3→15 随进度;超建 409;同一格重复建 409;1 格 1 间;拆有人住的厢房 409 |
| verify_recruit_cap.mjs | 0 间时招募 409;满额 409;主角不占名额(主角 + 3 人 + 3 间放行);遣散后释放名额 |
| verify_squad3.mjs | 3 格;members[0] = 最前排;空位保留(拖到第 3 格不停在第 1 格);5 人老存档截断成 3 |
| verify_starter.mjs | 名字 1 字 / 9 字 400;非法职业 400;性别白名单;重复创建 409;无主角时业务接口 409 |
| verify_battle_front.mjs | 1000 次受击分布 ≈ 50 / 33 / 17(容差 ±5%);只放后排时 100% 打他 |
UI 侧两条额外校验(项目惯例,缺一不可)
- 视觉/结构渲染验证 —— 无头截图自查,不看图不算完成(
channel: 'msedge',新 context 要先注册/登录)
- 内容 vs 大纲一致性比对 —— 上阵区文案、名额提示、创建主角界面与实际接口字段逐条对上
⚠️ 并发开发注意
- 本目录同时有多个开发会话:
public/assets/buildings/里已有未被BUILDINGS引用的yaoyuan.glb/yishiting.glb(另一条线),不要动
- 跑测试前先
curl http://127.0.0.1:8000/api/health;验收脚本默认目标必须是正式常驻服务(8000),别写死临时端口
- 改动
src/hall.js的验收脚本别写死单个 CSS 类名,选择器多认几个
山门厢房系统 · 设计与落地稿 v1 | 2026-10-07 | 口径来源:老大四项需求 + 四轮确认(1 间 = 1 人 / 每区域 +1 间 / 3 格自由拖放 / 带仇恨权重 / 强制创建主角 / 性别仅外观)
相关文档:docs/大殿设计.html(建筑方法论)、docs/equip-impl-spec.md(编队契约 §4)、 docs/门人职业与形象设计.html(18 职业)、docs/功法系统设计.html(起手功法)