直面两个核心病灶:①剧情错触发(白展堂未招募就开擂台赛), ②主界面信息爆炸(11 个 Tab 一字排开,新手一脸懵)。 本方案从规则层(数据 + 选择器)、结构层(Tab 解锁节奏)、表达层(引导 CoachMark)三层联动, 让玩家在 12 个月里,沿着"认识客栈 → 招贤纳士 → 触发主线 → 解锁支线"这条主线,一路有方向、有惊喜地玩下去。
权重函数 selectWeightedGameEvent 的判定维度只有:时间窗、时段、营业阶段、年份阶段、羁绊阶段、擂台赛阶段。完全没读 person.unlocked。
// 现在 selection.ts 的判定维度(节选)
isDayWindowAllowed(event, state.day) // 天数窗口
event.timeAffinity.includes(state.timeOfDay) // 白天/黄昏/夜晚
getBusinessPhaseWeight(...) // 开业/守业...
getYearPhaseWeight(...) // 开元年/次年
getBondSeriesWeight(...) // 羁绊阶段
getTournamentSeriesWeight(...) // 擂台赛阶段
// ❌ 没有 getStaffUnlockedWeight(...)
triggerConditions 字段形同虚设所有事件都填了 triggerConditions: ['reputation >= 60', 'cleanliness >= 50'] 这种字符串,但 selection.ts 从来不解析它——它只是给「事件目录」面板当说明文案展示的。
// randomEvents.ts 看着很像那么回事 triggerConditions: ['water < 20', 'cleanliness < 85'], triggerConditions: ['safety < 80', 'guo_lu_romance not settled'], // ↑↑↑ 这些全是字符串,selection.ts 根本不读 // 只在 EventTableDirectoryPanel.tsx L244 显示在目录里
这 4 个事件都写死了具体角色,但事件对象里没有 requiredStaff 字段。郭芙蓉/李大嘴/白展堂/吕秀才没招募也会被选中。
martial_match_guo_guard → 郭芙蓉守擂 martial_match_dazui_red_silk → 李大嘴误挂红绸 martial_match_bai_old_debt → 白展堂旧账擂台 martial_match_xiucai_words → 吕秀才文斗招亲 // ↑ 都没声明 requiredStaff: ['guo'] / ['li'] / ['bai'] / ['lu'] // ↑ 玩家没招募也照常触发,文案里"郭芙蓉把抹布一啪"直接穿帮
玩家开服第一天,界面已经平铺了 账本/门客/菜谱/后院/后厨/闯荡/行囊/设施/竞拍/预警/联机共 11 个 Tab。新手一脸懵:「我现在该点哪个?」
const TAB_ITEMS = [
{ tab: 'ledger', label: '账本' },
{ tab: 'staff', label: '门客' },
{ tab: 'recipes', label: '菜谱' },
{ tab: 'backyard', label: '后院' },
{ tab: 'kitchen', label: '后厨' },
{ tab: 'adventure', label: '闯荡' },
{ tab: 'baggage', label: '行囊' },
{ tab: 'facilities',label: '设施' },
{ tab: 'auction', label: '竞拍' },
{ tab: 'warning', label: '预警' },
{ tab: 'networking',label: '联机' },
];
这不是 bug,是机制缺失。需要在 GameEvent 类型里加 requiredStaff 字段,在 selectWeightedGameEvent 里新增 getStaffUnlockedWeight,再给 triggerConditions 字符串写个轻量 DSL 解析器(或者把常用条件结构化成字段)。
给 GameEvent 加 requiredStaff?: StaffId[],给 WeightedGameEventState 加 unlockedStaff: Set<StaffId>。
// types / RandomEventPopup.tsx
interface GameEvent {
...
requiredStaff?: StaffId[]; // 需要这些人都招募
requiredStaffMode?: 'all' | 'any'; // 默认 'all'
requiredDayMin?: number; // 替代散乱的 dayWindow
requiredPhase?: BusinessPhase;
requiredYearPhase?: StoryYearPhase;
...
}
在 selectWeightedGameEvent 增加 3 个判定函数,硬性约束 → 软性加权。
// selection.ts isStaffRequirementSatisfied(event, state) → 任何一个 requiredStaff 没招募 → 权重 0 getStaffBonusWeight(event, state) → 核心角色已招募 → ×1.3 (剧情更对味) getPhaseBonusWeight(event, state) → 命中 requiredYearPhase → ×1.5
当事件被锁住时:
🔒 主线 Tab 上挂"🔒"角标 💡 点击锁住的 Tab → 弹出解锁条件 📜 事件目录里: requiredStaff 文案化显示 🎉 解锁瞬间: 朱砂红"已解锁"小弹窗 + 礼花
核心逻辑约 +80 行(selection.ts 新增 3 个判定函数 + 1 个硬性短路);数据迁移约 30+ 个事件 补 requiredStaff(脚本生成 1 小时可完成);UI 改动 ConsoleTabsBar / 新增 ProgressOnboardingOverlay 两个组件。
以下是从 mainline-tournament.ts、role-*.ts、side-*.ts 里盘点出的核心事件。每个事件都标上 必填前置(硬约束)和 选填加权重(软加权)。
| 事件 ID | 名称 | 系列 | 必填前置(required) | 软加权(bonus) |
|---|---|---|---|---|
martial_match_guo_guard |
郭芙蓉临时守擂 | 擂台赛 | requiredStaff: ['guo'] | guo_lu_romance 阶段 friction +30% |
martial_match_dazui_red_silk |
李大嘴误挂红绸 | 擂台赛 | requiredStaff: ['li'] | 夜市客流高峰 +20% |
martial_match_bai_old_debt |
白展堂旧账擂台 | 擂台赛 | requiredStaff: ['bai'] | 治安 < 60 +30% |
martial_match_xiucai_words |
吕秀才文斗招亲 | 擂台赛 | requiredStaff: ['lu'] | 声望 ≥ 50 +25% |
day_guo_front_door |
郭芙蓉正门风波 | 角色线 A | requiredStaff: ['guo'] day ≥ 18 |
羁绊 seed → 2.4 / friction → 3.4 |
bai_secret_old_friend |
白展堂旧友重逢 | 角色线 B | requiredStaff: ['bai'] day ≥ 20 |
羁绊 friction +30% |
qian_refuge |
钱掌柜托付 | 角色线 E | requiredStaff: ['lu'] | 羁绊 seed +20% |
side-official |
官差税务/查案 | 支线 A | day ≥ 60 · 声望 ≥ 30 | 治安 ≤ 50 +20% |
side-night-risk |
夜间风险 | 支线 B | requiredStaff: ['bai'] (白展堂在场才镇得住) | 夜间时间窗 +50% |
side-old-ledger |
旧账风波 | 支线 D | day ≥ 240 | 官府在场 +20% |
用一个小脚本 tools/scripts/annotate-required-staff.cjs,扫所有 .ts 事件表,匹配标题/描述里的角色名("郭芙蓉"/"白展堂"/"李大嘴"/"吕秀才"/"佟湘玉"),自动写 requiredStaff,没匹配到的打 TODO。这样 1 小时能跑完 30+ 个事件。
把 11 个 Tab 拆成 「核心 4 个 + 收纳 7 个」。核心 4 个跟着玩家进度逐步解锁,其他 7 个收纳在 「更多 ▾」 抽屉里 + 进度条提示「X 天后开放」。
| Tab | 解锁时机 | 视觉处理 |
|---|---|---|
| 📒 账本 | DAY 1(默认) | 深朱砂 · 高亮 |
| 👩🍳 后厨 | DAY 1(默认,李大嘴已招募) | 深朱砂 · 高亮 |
| 👥 门客 | DAY 1(默认) | 深朱砂 · 高亮 |
| 📦 行囊 | DAY 4(首次掉落) | DAY 4 后朱砂高亮 |
| Tab | 解锁时机 | 锁时文案 |
|---|---|---|
| 🥘 菜谱 | 累计研发 2 道菜 | 「完成 2 道菜研发后开放」 |
| 🏚️ 后院 | DAY 35 招募吕秀才后 | 「李大嘴可劈柴 / 等他入伙」 |
| 🔨 设施 | DAY 70 招募郭芙蓉后 | 「扩建需郭芙蓉」 |
| 🎒 行囊 | DAY 4 首次掉落 | 「首次掉落开启」 |
| ⚔️ 闯荡 | DAY 140 招募白展堂后 | 「江湖路险,等白少侠」 |
| 💰 竞拍 | DAY 270 官府竞拍开局 | 「衙门大印盖下后开放」 |
| 🤝 联机 | DAY 210 开放盟友系统 | 「客栈名号传开后可联机」 |
预警不是「解锁就消失」,而是常驻红点。当玩家有任何未处理的失败预警,预警 Tab 在所有阶段都显示 🔴。但点进去的子页面是分阶段开放的:月 1-3 只显示「食安 + 卫生」一类;月 4+ 才有「治安 + 夜巡」;月 6+ 才有「江湖风险 + 擂台赛」。
大功能/角色/赛季首次上线,触发全屏华丽大卡(沿用「官府竞标」拍板的那套)。
不打断节奏的轻量引导,只在首次进入某 Tab 时显示一次。
锁住的 Tab 上挂"距解锁还有 X 天 / X 个条件",让玩家有期待感。
给 GameEvent 加 requiredStaff 字段;给 4 个擂台赛事件 + 角色线 + 关键支线补字段;selectWeightedGameEvent 加 isStaffRequirementSatisfied 硬性短路。
ConsoleTabsBar 拆核心 4 + 收纳 7;锁住 Tab 视觉降噪 + hover 提示;新增 tabUnlockConfig.ts 集中配置解锁条件。
封装 CinematicUnveilModal,支持「招募 / 功能 / 赛季」三类揭幕;卷轴展开 + 印章 + 礼花 + 角色台词;触发节点:角色首次招募 / Tab 解锁 / 大事件开场。
OnboardingCoach.tsx 4 步引导(首登 / 招募首人 / 解锁首 Tab / 触发首剧情);跳过即存档标记;月进度条在主界面顶部 ribbon 下方常驻。
P0 + P1 一次性做完(约 1.5 天工作量),立刻解决"擂台赛穿帮"和"主界面信息爆炸"两个最痛的问题。
P2 + P3 作为新功能上线时的"首次彩蛋",可以跟着剧情任务逐个加,不用一次全做完。