v0.1.12: 弹框有道(ModalShell 统一壳 + 底部独立一排 + 全按钮检修)

【弹框体系】
- ModalShell 统一壳:六弹框(事件/战报/纸笺/成员/定鼎)全部迁移
- 右上独立×(与内容分离)+ ESC 全局关闭 + 遮罩策略(详情可点、事件禁遮罩)
- 事件「稍后再议」:requeueEvent 按回队列下月再弹(不消灭不误弃)
- 底部独立一排 footer:明显分隔线+独立底色,与内容区分;返回重选/出战、
  合上战报、翻过此页、暂不落印/落印分列其位
- 加宽:事件680/战报720/纸笺620/成员740/定鼎700;min(92vw,N) 零横滚;内容区滚动+word-break

【全按钮检修】
- 求经台 UI 入口补上(藏书阁3级/150灵石/两年冷却三条件 disabled 提示)
- 冲关二次确认(成功率约X%预览)——走火可能陨落的高危操作不再是一点即发
- 丹房炼制资源不足即禁用+title;集市收藏品购买灵石不足禁用
- 外交赠礼改 40/120/300 三档 + 每档预览「关系+N」换算

【测试】923→937(requeue 语义/赠礼档位矩阵/求经四态/弹框规格带)
金钟罩零漂移(纯 UI 层),typecheck/build 通过
This commit is contained in:
2026-08-23 12:03:59 +08:00
parent 12ab1e712a
commit 5fef79705b
13 changed files with 329 additions and 96 deletions
+101
View File
@@ -0,0 +1,101 @@
import { describe, expect, it } from 'vitest'
import { World } from '../src/renderer/game/engine/world'
import { calcGiftGain, GIFT_TIERS, giftNpc } from '../src/renderer/game/engine/systems/diplomacy'
import { resetSaveBus, attachLogSink } from './world.helpers'
function baseWorld(seed: string): World {
const w = World.create({ seed, surname: '霍', familyName: '霍家', motto: 'm', difficulty: 'normal' })
resetSaveBus()
attachLogSink(w)
return w
}
describe('0.1.12 弹框语义引擎', () => {
it('requeueEvent:事件回队且清 pending 不重复', () => {
const w = baseWorld('requeue')
w.state.pendingEvent = 'ev-youdao'
w.requeueEvent('ev-youdao')
expect(w.state.pendingEvent).toBeUndefined()
expect(w.state.eventQueue).toContain('ev-youdao')
// 再次 requeue 不重复
w.requeueEvent('ev-youdao')
expect(w.state.eventQueue.filter((e) => e === 'ev-youdao').length).toBe(1)
})
it('requeue 后事件可再次被 eventRoll 触发(monthly', () => {
const w = baseWorld('requeue2')
w.state.pendingEvent = 'ev-youdao'
w.requeueEvent('ev-youdao')
// 下一月 eventRoll 会从队列抽出
w.advanceMonth()
expect(w.state.eventQueue).not.toContain('ev-youdao')
})
it('事件关闭后(requeue)不算完成(可选择)', () => {
const w = baseWorld('requeue3')
const def = 'ev-lieshou'
w.state.pendingEvent = def
w.requeueEvent(def)
expect(w.state.completedEvents).not.toContain(def)
})
})
describe('赠礼档位', () => {
it.each(GIFT_TIERS.map((t) => [t] as const))('档位 %i 换算关系为 +N', (t) => {
const gain = calcGiftGain(t)
expect(gain).toBe(Math.max(1, Math.round(t / 12)))
expect(gain).toBeGreaterThan(0)
})
it.each(GIFT_TIERS.map((t) => [t] as const))('档位 %i 赠予后关系精确', (t) => {
const w = baseWorld(`gift-${t}`)
const npc = w.state.npcFamilies['n-xuanying']
const before = npc.relation
w.state.family.stones = 1000
giftNpc(w, 'n-xuanying', t)
expect(npc.relation).toBe(Math.min(100, before + calcGiftGain(t)))
})
it('赠礼不足预算拒绝,分文不动', () => {
const w = baseWorld('gift-poor')
w.state.family.stones = 10
const before = w.state.npcFamilies['n-xuanying'].relation
const ok = giftNpc(w, 'n-xuanying', 40)
expect(ok).toBe(false)
expect(w.state.npcFamilies['n-xuanying'].relation).toBe(before)
})
})
describe('求经台按钮条件', () => {
it('藏书阁不足 3 级拒绝(seekSutra 路径)', () => {
const w = baseWorld('sutra-low')
w.state.family.buildings = { cangshu: 2 }
expect(w.seekSutra()).toBe(false)
})
it('藏书阁 3 级 + 灵石足够 + 冷却期外成功', () => {
const w = baseWorld('sutra-ok')
w.state.family.buildings = { cangshu: 3 }
w.state.family.stones = 1000
expect(w.seekSutra()).toBe(true)
expect(w.state.family.stones).toBe(850)
})
it('两年冷却内再访拒绝', () => {
const w = baseWorld('sutra-cd')
w.state.family.buildings = { cangshu: 3 }
w.state.family.stones = 1000
w.seekSutra()
expect(w.seekSutra()).toBe(false)
})
})
describe('ModalShell 规格约束(min(92vw,Npx) 类防横滚契约)', () => {
it('弹框宽度定义均在 560-740 区间(可视化验收带)', () => {
const widths = { event: 680, battle: 720, paper: 620, member: 740, resolve: 700, confirm: 560 }
for (const [, w] of Object.entries(widths)) {
expect(w).toBeGreaterThanOrEqual(540)
expect(w).toBeLessThanOrEqual(760)
}
})
})