From 5fef79705b785f62c2b0574840a3d30c204cf90f Mon Sep 17 00:00:00 2001
From: thzxx <1440196015@qq.com>
Date: Sun, 23 Aug 2026 12:03:59 +0800
Subject: [PATCH] =?UTF-8?q?v0.1.12:=20=E5=BC=B9=E6=A1=86=E6=9C=89=E9=81=93?=
=?UTF-8?q?=EF=BC=88ModalShell=20=E7=BB=9F=E4=B8=80=E5=A3=B3=20+=20?=
=?UTF-8?q?=E5=BA=95=E9=83=A8=E7=8B=AC=E7=AB=8B=E4=B8=80=E6=8E=92=20+=20?=
=?UTF-8?q?=E5=85=A8=E6=8C=89=E9=92=AE=E6=A3=80=E4=BF=AE=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
【弹框体系】
- 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 通过
---
package.json | 2 +-
src/renderer/game/engine/systems/diplomacy.ts | 8 +-
src/renderer/game/engine/world.ts | 6 ++
src/renderer/ui/components/BattleModal.tsx | 21 ++--
src/renderer/ui/components/EventModal.tsx | 63 ++++++-----
src/renderer/ui/components/MemberModal.tsx | 44 +++++---
src/renderer/ui/components/ModalShell.tsx | 61 +++++++++++
src/renderer/ui/components/PaperModal.tsx | 20 ++--
src/renderer/ui/components/ResolveModal.tsx | 54 +++++-----
src/renderer/ui/panels/DiplomacyPanel.tsx | 17 ++-
src/renderer/ui/panels/MarketPanel.tsx | 15 ++-
src/renderer/ui/panels/TerritoryPanel.tsx | 13 +++
tests/modal-0.1.12.test.ts | 101 ++++++++++++++++++
13 files changed, 329 insertions(+), 96 deletions(-)
create mode 100644 src/renderer/ui/components/ModalShell.tsx
create mode 100644 tests/modal-0.1.12.test.ts
diff --git a/package.json b/package.json
index 966c5bb..9a4f29e 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "chronicle-of-the-immortal-clan",
"productName": "仙途家族志",
- "version": "0.1.11",
+ "version": "0.1.12",
"description": "修仙 · 家族 · 经营 · 战斗 模拟器",
"main": "./out/main/index.js",
"author": "MetonaTeam",
diff --git a/src/renderer/game/engine/systems/diplomacy.ts b/src/renderer/game/engine/systems/diplomacy.ts
index ca16fe6..7cf4eeb 100644
--- a/src/renderer/game/engine/systems/diplomacy.ts
+++ b/src/renderer/game/engine/systems/diplomacy.ts
@@ -32,12 +32,18 @@ export function npcRelation(w: World, npcId: string): number {
return w.state.npcFamilies[npcId]?.relation ?? 0
}
+export function calcGiftGain(stones: number): number {
+ return Math.max(1, Math.round(stones / 12))
+}
+
+export const GIFT_TIERS = [40, 120, 300] as const
+
export function giftNpc(w: World, npcId: string, stones: number): boolean {
const fam = w.state.family
if (stones <= 0 || fam.stones < stones) return false
fam.stones -= stones
const npc = w.state.npcFamilies[npcId]
- const gain = Math.max(1, Math.round(stones / 12))
+ const gain = calcGiftGain(stones)
npc.relation = Math.min(100, npc.relation + gain)
w.log('info', `厚礼送往${npc.name},两家关系 +${gain}。`)
return true
diff --git a/src/renderer/game/engine/world.ts b/src/renderer/game/engine/world.ts
index e60eaf3..0f20d46 100644
--- a/src/renderer/game/engine/world.ts
+++ b/src/renderer/game/engine/world.ts
@@ -298,6 +298,12 @@ export class World {
this.pruneYearFlags()
}
+ requeueEvent(eventId: string): void {
+ const s = this.state
+ if (!s.eventQueue.includes(eventId)) s.eventQueue.push(eventId)
+ s.pendingEvent = undefined
+ }
+
/** 年度清理:剔除 3 年以前的年份前缀 flag 键(防长线膨胀) */
private pruneYearFlags(): void {
const fam = this.state.family
diff --git a/src/renderer/ui/components/BattleModal.tsx b/src/renderer/ui/components/BattleModal.tsx
index 23b9be7..72b3cab 100644
--- a/src/renderer/ui/components/BattleModal.tsx
+++ b/src/renderer/ui/components/BattleModal.tsx
@@ -1,4 +1,5 @@
import { useGameStore } from '../store'
+import { ModalShell } from './ModalShell'
export function BattleModal() {
const battleView = useGameStore((s) => s.battleView)
@@ -7,13 +8,15 @@ export function BattleModal() {
const winnerLabel =
battleView.winner === 'player' ? '✦ 我方得胜 ✦' : battleView.winner === 'enemy' ? '✕ 我方败北 ✕' : '◌ 平分秋色 ◌'
+ const close = () => useGameStore.setState({ battleView: undefined })
return (
-
-
-
{battleView.title}
-
- {battleView.year}年{battleView.month}月 · {winnerLabel}
-
+
合上战报}
+ >
{battleView.lines.map((l, i) => (
{l}
@@ -24,11 +27,7 @@ export function BattleModal() {
)}
-
-
-
-
-
+
)
}
diff --git a/src/renderer/ui/components/EventModal.tsx b/src/renderer/ui/components/EventModal.tsx
index 9cf2d2f..0715fa1 100644
--- a/src/renderer/ui/components/EventModal.tsx
+++ b/src/renderer/ui/components/EventModal.tsx
@@ -6,6 +6,7 @@ import { describeRealm } from '../../game/data/realms'
import { Character } from '../../game/types/domain'
import { combatPowerOf } from '../../game/engine/systems/combat'
import { FORMATIONS, FormationId } from '../../game/data/formations'
+import { ModalShell } from './ModalShell'
export function EventModal() {
const pendingEventId = useGameStore((s) => s.pendingEventId)
@@ -52,15 +53,42 @@ export function EventModal() {
)
}
+ const dorm = () => {
+ // 「稍后再议」:把事件按回队列,不消灭、不误弃
+ world.requeueEvent(pendingEventId)
+ closeModal()
+ }
+
return (
-
-
-
{pendingEventDef.name}
-
- {eventCategoryName(pendingEventDef.category)}
- {pendingEventDef.once && ' · 仅此一次'}
-
-
{pendingEventDef.text}
+
+
+
+ >
+ ) : (
+
+ )
+ }
+ >
+ {pendingEventDef.text}
{squadPicking && (
@@ -95,21 +123,7 @@ export function EventModal() {
{squadPicking ? (
- <>
-
-
- >
+
依选迎战(若现选不满员将由家族精锐补齐)
) : (
pendingEventDef.options.map((o, i) => {
const isRaid = raidIdx === i
@@ -123,7 +137,6 @@ export function EventModal() {
})
)}
-
-
+
)
}
diff --git a/src/renderer/ui/components/MemberModal.tsx b/src/renderer/ui/components/MemberModal.tsx
index f56f6d5..32cc90e 100644
--- a/src/renderer/ui/components/MemberModal.tsx
+++ b/src/renderer/ui/components/MemberModal.tsx
@@ -7,7 +7,9 @@ import { TECHNIQUES } from '../../game/data/techniques'
import { ITEMS, ARTIFACT_POWER } from '../../game/data/items'
import { combatPowerOf } from '../../game/engine/systems/combat'
import { lifespanOf } from '../../game/engine/systems/lifecycle'
+import { perAttemptChance } from '../../game/engine/systems/cultivation'
import { POSTS, POST_ORDER } from '../../game/data/posts'
+import { ModalShell } from './ModalShell'
import { buildBiography } from '../../game/core/biography'
function aspName(c: Character): string {
@@ -55,9 +57,31 @@ export function MemberModal({ member }: { member: Character }) {
const power = member.alive ? combatPowerOf(w, member) : 0
const lifespan = lifespanOf(w, member)
+ const close = () => setSelected(undefined)
+ const confirmBreak = () => {
+ const chance = Math.round(perAttemptChance(w, member) * 100)
+ if (window.confirm(`${member.name} 冲击【${nextName}】,成功率约 ${chance}%。或以族运为注?`)) {
+ w.assistedBreakthrough(member.id)
+ bump()
+ }
+ }
return (
-
setSelected(undefined)}>
-
e.stopPropagation()}>
+
+ {member.children.length > 0 && (
+
+ 子女:{member.children.map((cid) => s.members[cid]?.name ?? '?').join('、')}
+
+ )}
+
+ >
+ }
+ >
{member.name}
{!member.alive && · 殁}
@@ -129,10 +153,7 @@ export function MemberModal({ member }: { member: Character }) {
{member.alive && member.realmProgress >= 100 && member.realm.major !== 'spirit' && (
@@ -308,15 +329,6 @@ export function MemberModal({ member }: { member: Character }) {
)}
-
- {member.children.length > 0 && (
-
- 子女:{member.children.map((cid) => s.members[cid]?.name ?? '?').join('、')}
-
- )}
-
-
-
-
+
)
}
diff --git a/src/renderer/ui/components/ModalShell.tsx b/src/renderer/ui/components/ModalShell.tsx
new file mode 100644
index 0000000..f80350f
--- /dev/null
+++ b/src/renderer/ui/components/ModalShell.tsx
@@ -0,0 +1,61 @@
+import { useEffect, type ReactNode } from 'react'
+
+export interface ModalShellProps {
+ title: string
+ category?: string
+ width?: number
+ allowOuter?: boolean
+ onClose: () => void
+ footer?: ReactNode
+ footerAlign?: 'right' | 'center' | 'space'
+ children: ReactNode
+ closeLabel?: string
+}
+
+/**
+ * 统一弹框壳(0.1.12):
+ * - 右上独立关闭钮(与内容完全分离)
+ * - ESC 键关闭
+ * - 底部独立一排(footer)与内容区明显区分(分隔线 + 底色差异 + 粘性)
+ * - 宽度 min(92vw, Npx),零横向滚动条
+ */
+export function ModalShell({
+ title,
+ category,
+ width = 680,
+ allowOuter = true,
+ onClose,
+ footer,
+ footerAlign = 'right',
+ children,
+ closeLabel = '关闭'
+}: ModalShellProps) {
+ useEffect(() => {
+ const onKey = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') {
+ e.stopPropagation()
+ onClose()
+ }
+ }
+ window.addEventListener('keydown', onKey)
+ return () => window.removeEventListener('keydown', onKey)
+ }, [onClose])
+
+ return (
+
+
e.stopPropagation()} data-modal-kind="shell">
+
+
{title}
+ {category &&
{category}
}
+
{children}
+ {footer && (
+
+ {footer}
+
+ )}
+
+
+ )
+}
diff --git a/src/renderer/ui/components/PaperModal.tsx b/src/renderer/ui/components/PaperModal.tsx
index aead9d3..78f1402 100644
--- a/src/renderer/ui/components/PaperModal.tsx
+++ b/src/renderer/ui/components/PaperModal.tsx
@@ -1,5 +1,6 @@
import { useGameStore } from '../store'
import { YearlyReport } from '../../game/types/domain'
+import { ModalShell } from './ModalShell'
export function PaperModal() {
const report = useGameStore((s) => s.paperReport)
@@ -10,10 +11,13 @@ export function PaperModal() {
const y = report.year
return (
-
-
e.stopPropagation()}>
-
岁 末 族 簿
-
{y}年 全 族 收 支 与 人 丁 情 形
+
翻 过 此 页}
+ >
灵石盈亏 = 0 ? 'good' : 'bad'}>{nets >= 0 ? `+${nets}` : nets} 枚
人丁 新增 {report.births} · 辞世 {report.deaths}
@@ -21,12 +25,6 @@ export function PaperModal() {
{report.deaths > report.births &&
丁口在减 —— 传宗接代是头等大事
}
{report.births > report.deaths &&
丁口渐旺,香火有望。
}
-
-
-
-
-
+
)
}
diff --git a/src/renderer/ui/components/ResolveModal.tsx b/src/renderer/ui/components/ResolveModal.tsx
index eaa204b..81129d8 100644
--- a/src/renderer/ui/components/ResolveModal.tsx
+++ b/src/renderer/ui/components/ResolveModal.tsx
@@ -1,6 +1,7 @@
import { useGameStore } from '../store'
import { LegacyArch } from '../../game/core/legacy'
import { sBell } from '../sound'
+import { ModalShell } from './ModalShell'
function dimLabel(k: string): string {
const map: Record
= {
@@ -13,12 +14,34 @@ function dimLabel(k: string): string {
}
export function ResolveModal({ arch, onClose }: { arch: LegacyArch; onClose: () => void }) {
+ const close = onClose
return (
-
-
-
- —— 百年望气 · 定鼎之文 ——
-
+
+
+
+ >
+ }
+ >
「{arch.title}」
{Object.entries(arch.dims)
@@ -46,26 +69,7 @@ export function ResolveModal({ arch, onClose }: { arch: LegacyArch; onClose: ()
))}
仙途家族志 · 青册
-
-
-
-
-
-
+
)
}
diff --git a/src/renderer/ui/panels/DiplomacyPanel.tsx b/src/renderer/ui/panels/DiplomacyPanel.tsx
index 949feb9..f942fe2 100644
--- a/src/renderer/ui/panels/DiplomacyPanel.tsx
+++ b/src/renderer/ui/panels/DiplomacyPanel.tsx
@@ -1,6 +1,6 @@
import { useGameStore } from '../store'
import { npcById } from '../../game/data/npcs'
-import { giftNpc, makePeace, marryNpcFamily } from '../../game/engine/systems/diplomacy'
+import { giftNpc, makePeace, marryNpcFamily, calcGiftGain, GIFT_TIERS } from '../../game/engine/systems/diplomacy'
import { useMemo } from 'react'
import { MAJOR_NAMES } from '../../game/data/realms'
@@ -50,9 +50,18 @@ export default function DiplomacyPanel() {
{MAJOR_NAMES[def.leaderRealm]}宗主
-
+ {GIFT_TIERS.map((t) => (
+
+ ))}
+