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) => ( + + ))} + {' '} - + {' '} + )} diff --git a/src/renderer/ui/panels/TerritoryPanel.tsx b/src/renderer/ui/panels/TerritoryPanel.tsx index 83e0e95..9d5c6d4 100644 --- a/src/renderer/ui/panels/TerritoryPanel.tsx +++ b/src/renderer/ui/panels/TerritoryPanel.tsx @@ -69,6 +69,19 @@ export default function TerritoryPanel() { ) : ( 已臻极致 )} + {id === 'cangshu' && ( + + )} {id === 'zongci' && (