(null)
+ if (!world) return null
+ const w = world
+ const ws = w.state.worldSim
+ if (!ws) return 天机未显……(世界演化尚未展开)
+
+ const tide = ws.tide ?? 0.5
+ const tideLabel = tide > 0.9 ? '灵涨' : tide < 0.7 ? '灵衰' : '汐平'
+ const pool = ws.marketPool ?? {}
+ const news = [...(ws.newsFeed ?? [])].slice().reverse()
+ const calamity = ws.calamity
+
+ return (
+
+
+ 天下时局尽览:行市起落、宗主更替、灾年吉凶——皆随世界自演化而来。
+ 结盟后岁差不降,每年另有盟利奉上(需关系≥40)。
+
+
+ 灵潮 {tideLabel}({Math.round(tide * 100)}%)
+ {calamity
+ ? 灾年·{calamity}(灵植减产,市价将行)
+ : 风调雨顺}
+ {Object.entries(pool).map(([k, v]) => {
+ const base = POOL_BASE[k as keyof typeof POOL_BASE] ?? 100
+ const r = ((v as number) / base) * 100
+ const cls = r > 115 ? 'bad' : r < 85 ? 'good' : 'dim'
+ const dir = r > 105 ? '↑' : r < 95 ? '↓' : '→'
+ return (
+ {RES_LABEL[k] ?? k}{dir}{Math.round(r)}%
+ )
+ })}
+
+
+
+
天下快讯
+
+ {news.length === 0 &&
风平浪静,尚无消息。
}
+ {news.map((row, i) => (
+
+ {row.year}年{row.month}月
+ {row.text}
+
+ ))}
+
+
+
+
群雄谱(点开宗主详情)
+
+ {Object.values(w.state.npcFamilies).map((npc) => {
+ const def = npcById(npc.id)
+ const dyn = (ws.npcDyn ?? {})[npc.id]
+ const open = view === npc.id
+ return (
+
+
setView(open ? null : npc.id)}>
+
+ {npc.name} {def.style}
+ {npc.allied && 盟}
+
+ 势力{npc.power} · 关系{npc.relation}
+
+ {open && (
+
+
宗主:{dyn?.leaderName ?? '未知'}({dyn ? MAJOR_NAMES[REALM_IDX[dyn.leaderRealmIdx] ?? 'qi'] : ''} · {dyn?.leaderAge ?? '?'}岁)
+
{def.desc}
+
新近之事:{dyn?.lastEvent ? `${dyn.lastEvent}(${dyn.lastEventYear ?? '?'}年)` : '暂无'}
+ {npc.alliedSinceYear &&
同盟自 {npc.alliedSinceYear} 年
}
+
+
+ {npc.allied && (
+
+ )}
+
+
+ )}
+
+ )
+ })}
+
+
+
+
+ )
+}
diff --git a/src/renderer/ui/screens/GameScreen.tsx b/src/renderer/ui/screens/GameScreen.tsx
index c2e2e38..8b51f68 100644
--- a/src/renderer/ui/screens/GameScreen.tsx
+++ b/src/renderer/ui/screens/GameScreen.tsx
@@ -9,6 +9,7 @@ import MarketPanel from '../panels/MarketPanel'
import DiplomacyPanel from '../panels/DiplomacyPanel'
import ExpeditionPanel from '../panels/ExpeditionPanel'
import ChroniclePanel from '../panels/ChroniclePanel'
+import WorldPanel from '../panels/WorldPanel'
import SettingsPanel from '../panels/SettingsPanel'
import { LogFeed } from '../components/LogFeed'
import { UrgentBadges } from '../components/UrgentBadges'
@@ -28,6 +29,7 @@ const TABS: { id: PanelId; label: string }[] = [
{ id: 'expedition', label: '探秘' },
{ id: 'chronicle', label: '史书' },
{ id: 'legacy', label: '春秋原' },
+ { id: 'world', label: '天下' },
{ id: 'settings', label: '设置' }
]
@@ -113,6 +115,7 @@ export default function GameScreen() {
{panel === 'expedition' && }
{panel === 'chronicle' && }
{panel === 'legacy' && }
+ {panel === 'world' && }
{panel === 'settings' && }
{gameOverReason && (
diff --git a/src/renderer/ui/store.ts b/src/renderer/ui/store.ts
index 0c19eea..c3fd650 100644
--- a/src/renderer/ui/store.ts
+++ b/src/renderer/ui/store.ts
@@ -11,7 +11,7 @@ import { setSoundEnabled, sPaper, sGood, sBad, sWar, sBell, sTick } from './soun
import { seasonOf } from '../game/data/season'
export type Screen = 'boot' | 'newgame' | 'game'
-export type PanelId = 'family' | 'genealogy' | 'territory' | 'market' | 'diplomacy' | 'expedition' | 'chronicle' | 'legacy' | 'settings'
+export type PanelId = 'family' | 'genealogy' | 'territory' | 'market' | 'diplomacy' | 'expedition' | 'chronicle' | 'legacy' | 'world' | 'settings'
export interface GameStore {
screen: Screen
@@ -150,7 +150,8 @@ export const useGameStore = create((set, get) => ({
set({ world, engine, slot, screen: 'game', panel: 'family', logFeed: [], battleView: undefined, pendingEventId: undefined, pendingEventDef: undefined, revision: 1, speed: 0, gameOverReason: undefined, paperReport: undefined, toast: undefined, selectedMemberId: undefined, selectedMissionDef: undefined })
const st = get()
st.world?.out.push(makeBus(st))
- const facade = st.facade ?? new GameFacade(world, slot)
+ // 新档永远建新门面(旧 facade 绑定旧 world,复用会回放错误目标)
+ const facade = new GameFacade(world, slot)
set({ facade })
await st.saveNow('开局')
},
diff --git a/src/renderer/ui/styles.css b/src/renderer/ui/styles.css
index d2a700c..3de41f6 100644
--- a/src/renderer/ui/styles.css
+++ b/src/renderer/ui/styles.css
@@ -1355,6 +1355,29 @@ html[data-blade] .battle-lines {
.modal-title {
padding-right: 44px;
}
+.opt-eff {
+ display: block;
+ font-size: 0.74rem;
+ color: rgba(150, 132, 96, 0.9);
+ margin-top: 3px;
+ letter-spacing: 0.02em;
+}
+.ch-draft {
+ flex: 1;
+ min-width: 200px;
+ background: rgba(18, 14, 9, 0.75);
+ border: 1px solid rgba(140, 100, 50, 0.5);
+ border-radius: 4px;
+ color: #e8d5a4;
+ padding: 7px 10px;
+ font-family: inherit;
+ font-size: 0.9rem;
+}
+.ch-draft:focus {
+ outline: none;
+ border-color: var(--vermilion);
+ box-shadow: 0 0 0 2px rgba(200, 60, 30, 0.18);
+}
.modal-opts {
display: flex;
flex-direction: column;
diff --git a/tests/audit-regression.test.ts b/tests/audit-regression.test.ts
index b493950..483c01e 100644
--- a/tests/audit-regression.test.ts
+++ b/tests/audit-regression.test.ts
@@ -82,7 +82,7 @@ describe('审计回归:P0 修复固化', () => {
})
it('防御性修补后金钟罩不变(行为等价确认)', () => {
- expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('a977654a')
- expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('ee501983')
+ expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('9531969e')
+ expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('aeefac96')
})
})
diff --git a/tests/clock.test.ts b/tests/clock.test.ts
index d741544..ace8263 100644
--- a/tests/clock.test.ts
+++ b/tests/clock.test.ts
@@ -7,11 +7,11 @@ import { World } from '../src/renderer/game/engine/runtime/World'
* 任何改动(重构日程/调平衡/加系统)若改变了确定性序列或结果,此测试立刻报红。
* 更新规则:仅当**有意**变更序列逻辑时,三枚 seed 指纹同版更新并注明原因。
*/
-// 0.1.15 深度审计最终基线:worldsim 修复(换代年首化/power年度/relation去漂/快讯节流/灵脉初始化)后固化。
+// 0.1.16 资源闭环基线:修为耗草/炼丹概率化/铸器/灵脉折算/NPC战力/灾变落效/渡劫药力/结盟后固化。
const GOLDEN: Record> = {
- 'bell-seed-1': { 560: 'a977654a', 1200: 'faefa00a', 2160: '5b7b375b' },
- 'bell-seed-2': { 560: '1c41881c', 1200: 'a06af06c', 2160: '142d04c3' },
- 'bell-seed-3': { 560: 'ee501983', 1200: '2a899a90', 2160: '0e490e5c' }
+ 'bell-seed-1': { 560: '9531969e', 1200: 'cdd70056', 2160: '7e33a89d' },
+ 'bell-seed-2': { 560: 'f2ecc3e1', 1200: 'dab62832', 2160: '03c02233' },
+ 'bell-seed-3': { 560: 'aeefac96', 1200: '9eda1d0f', 2160: 'b3bcf822' }
}
const TIERS = [
diff --git a/tests/economy-loop-0.1.16.test.ts b/tests/economy-loop-0.1.16.test.ts
new file mode 100644
index 0000000..9f978bf
--- /dev/null
+++ b/tests/economy-loop-0.1.16.test.ts
@@ -0,0 +1,91 @@
+import { describe, expect, it } from 'vitest'
+import { World } from '../src/renderer/game/engine/runtime/World'
+import { PILL_RECIPES, FORGE_RECIPES, pillRecipeByOutput, forgeRecipeByOutput } from '../src/renderer/game/data/items'
+import { bonusOf } from '../src/renderer/game/data/buildings'
+import { MAJORS } from '../src/renderer/game/data/realms'
+import { monthlyRate } from '../src/renderer/game/engine/runtime/Systems/cultivation'
+
+describe('0.1.16 资源闭环', () => {
+ it('修为耗草:有草速修,无草缓修(0.6底)', () => {
+ const w = World.create({ seed: 'l1', surname: '叶', familyName: '叶家', motto: 'm', difficulty: 'normal' })
+ const c = Object.values(w.state.members)[0]!
+ c.realm = { major: 'qi', minor: 2 }
+ createWorldWithHerbs(w, 10)
+ const fast = monthlyRate(w, c, 1)
+ const slow = monthlyRate(w, c, 0.6)
+ expect(fast).toBeGreaterThan(slow * 1.5)
+ })
+
+ it('丹方单源:craftPill 按配方消耗且门槛生效', () => {
+ const w = World.create({ seed: 'l2', surname: '李', familyName: '李家', motto: 'm', difficulty: 'normal' })
+ w.state.family.buildings = { danfang: 1 }
+ const fam = w.state.family
+ fam.inventory['lingcao'] = 100
+ fam.inventory['beastcore'] = 10
+ fam.stones = 1000
+ const r = pillRecipeByOutput('pill-ningyuan')!
+ expect(r.danfangLevel).toBe(2)
+ expect(w.craftPill('ningyuan')).toBe(false) // 丹房1级不足
+ expect(fam.inventory['lingcao']).toBe(100)
+ })
+
+ it('铸器配方:材料足够即成,入库', () => {
+ const w = World.create({ seed: 'l3', surname: '韩', familyName: '韩家', motto: 'm', difficulty: 'normal' })
+ const fam = w.state.family
+ fam.inventory['lingkuang'] = 100
+ fam.inventory['beastcore'] = 50
+ fam.stones = 5000
+ const r = forgeRecipeByOutput('weapon-ling')!
+ expect(w.forgeArtifact('weapon-ling')).toBe(true)
+ expect(fam.inventory['weapon-ling']).toBe(1)
+ expect(fam.inventory['lingkuang']).toBe(100 - r.lingkuang)
+ })
+
+ it('bonusOf 表达式安全:非法符号返回0', () => {
+ expect(bonusOf('danfang', 'craftChance', 2)).toBeCloseTo(0.7)
+ expect(bonusOf('lingtian', 'nonexist', 1)).toBe(0)
+ expect(bonusOf('danfang', 'craftChance', 1)).toBeCloseTo(0.6)
+ })
+
+ it('tribBoost:破境丹大境界走渡劫而非直破', () => {
+ const w = World.create({ seed: 'l4', surname: '齐', familyName: '齐家', motto: 'm', difficulty: 'normal' })
+ const c = Object.values(w.state.members)[1]!
+ c.realm = { major: 'foundation', minor: (MAJORS.foundation.minorLayers - 1) }
+ c.realmProgress = 100
+ w.state.family.inventory['pill-pojing'] = 1
+ w.state.family.buildings = { danfang: 1 }
+ w.takePill(c.id, 'pill-pojing')
+ expect(c.tribBoost).toBeGreaterThan(0)
+ expect(w.state.pendingEvent).toBeTruthy() // 渡劫事件挂起
+ })
+
+ it('setAlliance:关系不足拒绝,足够成盟', () => {
+ const w = World.create({ seed: 'l5', surname: '荀', familyName: '荀家', motto: 'm', difficulty: 'normal' })
+ const npcId = Object.keys(w.state.npcFamilies)[0]!
+ w.state.npcFamilies[npcId].relation = 10
+ expect(w.setAlliance(npcId, true)).toBe(false)
+ w.state.npcFamilies[npcId].relation = 60
+ expect(w.setAlliance(npcId, true)).toBe(true)
+ expect(w.state.npcFamilies[npcId].allied).toBe(true)
+ })
+
+ it('劫掠强度随NPC实力浮动(早期远弱于后期)', () => {
+ const w = World.create({ seed: 'l6', surname: '苗', familyName: '苗家', motto: 'm', difficulty: 'normal' })
+ const npcId = Object.keys(w.state.npcFamilies)[0]!
+ const w1 = World.create({ seed: 'l6', surname: '苗', familyName: '苗家', motto: 'm', difficulty: 'normal' })
+ w.state.npcFamilies[npcId].power = 60
+ w1.state.npcFamilies[npcId].power = 240
+ const team = [w.state.members['x1']!]
+ expect(combatStrengthOf(w, npcId)).toBeLessThan(combatStrengthOf(w1, npcId))
+ void team
+ })
+})
+
+function createWorldWithHerbs(w: World, n: number): void {
+ w.state.family.inventory['lingcao'] = n
+}
+
+function combatStrengthOf(w: World, npcId: string): number {
+ const npc = w.state.npcFamilies[npcId]
+ return Math.min(1.8, Math.max(0.5, 0.5 + ((npc.power ?? 60) / 120) * 0.5))
+}
diff --git a/tests/facade-registry.test.ts b/tests/facade-registry.test.ts
index 09f7bbc..4743c6b 100644
--- a/tests/facade-registry.test.ts
+++ b/tests/facade-registry.test.ts
@@ -162,7 +162,7 @@ describe('GameFacade 门面', () => {
const f = new GameFacade(w, 1)
const info = f.about()
expect(info.title).toBe('仙途家族志')
- expect(info.version).toContain('0.1.15')
+ expect(info.version).toContain('0.1.16')
expect(info.modules).toBeGreaterThanOrEqual(11)
expect(info.systems).toBeGreaterThan(0)
expect(info.plugins).toBeGreaterThanOrEqual(3)
@@ -170,7 +170,7 @@ describe('GameFacade 门面', () => {
it('默认配置金钟罩不受门面化影响', () => {
PACK.reset()
- expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('a977654a')
- expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('ee501983')
+ expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('9531969e')
+ expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('aeefac96')
})
})
diff --git a/tests/plugin.test.ts b/tests/plugin.test.ts
index e469042..e8abc9e 100644
--- a/tests/plugin.test.ts
+++ b/tests/plugin.test.ts
@@ -80,9 +80,9 @@ describe('PluginCore 插件协议', () => {
})
it('默认管线金钟罩不受插件层影响', () => {
- expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('a977654a')
- expect(stateFingerprint(longRun('bell-seed-2').state)).toBe('1c41881c')
- expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('ee501983')
+ expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('9531969e')
+ expect(stateFingerprint(longRun('bell-seed-2').state)).toBe('f2ecc3e1')
+ expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('aeefac96')
})
it('facade 插件查询与 about.plugins', () => {