v0.1.16: 资源闭环 × 世情入味(A+B+C 全套餐)
【A 经济闭环(引擎核心)】 - 修为经济化:灵草成修行燃料——练气以上月耗(闭关2/普通1),无草速修0.6~1.0衰减+缺草每年一告 - 炼丹等级化:bonusOf 白名单表达式求值器(extra 单源);craftChance 转正(L5=100%),丹方 PILL_RECIPES(破境丹需丹房3级),失败折半退料 - 炼器筑宝:FORGE_RECIPES(法器/灵器/法宝)灵矿兽核入炉 - 秘灵灵脉:secretQiOf→rollWarbooty 收益门槛(0.5~1.5 折算),掉落按秘境 techGrades 过滤(暗墨林不再出仙典) - NPC 实力参战:raid 强度/风险随 npc.power 浮动(0.5~1.8)——外交成长期博弈 - 灾年落效:CALAMITY_FAMILY 月度减产乘子 + 疫病/兽潮一次性体感 【B 世界活在 UI】 - 天下面板:快讯时间线/行情/灾年/群雄谱(宗主详情+换代数)+ 结盟(setAlliance,关联岁差不降+年利) - 结盟引擎 truly 落地:allied 字段第一次被写值 【C 写史与治理】 - 手写史书:ChroniclePanel 输入条(misc 入史)+ MemberModal 册立家主 + 年报要闻/辞世讣告 + EventModal 收益预览 - C13 破境丹渡劫修正:需渡劫大境界时药力→tribBoost(+12%)入渡劫事件,不再跳三选 - C14 存储防线:loadState 过 migrate;MigrationError(TOO_NEW/未知版本) 醒目提示 - C15 registry 补全(aspirations/formations/season)+ startNewGame facade 复用 bug 【测试】986 全绿(37 套件,新增经济闭环 7 例);金钟罩 0.1.16 基线固化(修为耗草/炼丹/灾害序列变更受控)
This commit is contained in:
@@ -5,6 +5,7 @@ import { combatPowerOf } from '../../game/engine/runtime/Systems/combat'
|
||||
import { useState } from 'react'
|
||||
import { describeRealm, MAJOR_ORDER } from '../../game/data/realms'
|
||||
import { FORMATIONS, FormationId } from '../../game/data/formations'
|
||||
import { WorldSim } from '../../game/engine/sim/WorldSim'
|
||||
|
||||
export default function ExpeditionPanel() {
|
||||
const world = useGameStore((s) => s.world)
|
||||
@@ -24,6 +25,14 @@ export default function ExpeditionPanel() {
|
||||
)
|
||||
const def = selectedMissionDef ? missionById(selectedMissionDef) : null
|
||||
|
||||
const qiOf = (id: string): number | null => {
|
||||
try {
|
||||
return Math.round(new WorldSim(w).secretQiOf(id))
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const toggle = (id: string) => {
|
||||
setSquad((old) => (old.includes(id) ? old.filter((x) => x !== id) : [...old, id]))
|
||||
}
|
||||
@@ -58,6 +67,11 @@ export default function ExpeditionPanel() {
|
||||
<div className="dim2" style={{ fontSize: '0.78rem' }}>{m.region} · 适合{describeRealm({ major: m.realmHint, minor: 0 })}</div>
|
||||
<div className="dim2" style={{ fontSize: '0.78rem' }}>{m.minMembers}-{m.maxMembers}人 · {m.stages.length}段</div>
|
||||
<div className="dim" style={{ fontSize: '0.8rem', marginTop: 3 }}>{m.desc}</div>
|
||||
{qiOf(m.id) !== null && (
|
||||
<div className="dim2" style={{ fontSize: '0.76rem', marginTop: 3 }}>
|
||||
灵脉 {qiOf(m.id)}% (探索后-6,回复2/月;灵气越足所获越丰)
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -210,7 +210,7 @@ export default function SettingsPanel() {
|
||||
· 「外交」与四邻结好联姻;仇雠之族隔岁来犯,打得赢名望大涨,打不赢蚀钱伤丁。<br />
|
||||
· 「史书」自动记述繁华与凋零——百年之后,后人翻开这一卷家族志,见代代薪火、历历雪泥。
|
||||
</div>
|
||||
<div className="dim2" style={{ marginTop: 8 }}>版本 0.1.15 · Chronicle of the Immortal Clan</div>
|
||||
<div className="dim2" style={{ marginTop: 8 }}>版本 0.1.16 · Chronicle of the Immortal Clan</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import { useGameStore } from '../store'
|
||||
import { npcById } from '../../game/data/npcs'
|
||||
import { MAJOR_NAMES } from '../../game/data/realms'
|
||||
import { POOL_BASE } from '../../game/engine/sim/worldsim-data'
|
||||
import { useState } from 'react'
|
||||
|
||||
const RES_LABEL: Record<string, string> = { lingcao: '灵草', lingkuang: '灵矿', beastcore: '兽核' }
|
||||
const REALM_IDX = ['mortal', 'qi', 'foundation', 'core', 'nascent', 'spirit'] as const
|
||||
|
||||
export default function WorldPanel() {
|
||||
const world = useGameStore((s) => s.world)
|
||||
const bump = useGameStore((s) => s.bump)
|
||||
const revision = useGameStore((s) => s.revision)
|
||||
void revision
|
||||
const [view, setView] = useState<string | null>(null)
|
||||
if (!world) return null
|
||||
const w = world
|
||||
const ws = w.state.worldSim
|
||||
if (!ws) return <div className="help-text">天机未显……(世界演化尚未展开)</div>
|
||||
|
||||
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 (
|
||||
<div>
|
||||
<div className="help-text">
|
||||
天下时局尽览:行市起落、宗主更替、灾年吉凶——皆随世界自演化而来。
|
||||
结盟后岁差不降,每年另有盟利奉上(需关系≥40)。
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', marginBottom: 10 }}>
|
||||
<span className="tag gold-t">灵潮 {tideLabel}({Math.round(tide * 100)}%)</span>
|
||||
{calamity
|
||||
? <span className="tag" style={{ borderColor: '#a33', color: '#e8a08a' }}>灾年·{calamity}(灵植减产,市价将行)</span>
|
||||
: <span className="tag">风调雨顺</span>}
|
||||
{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 (
|
||||
<span key={k} className="tag"><span className={cls}>{RES_LABEL[k] ?? k}{dir}{Math.round(r)}%</span></span>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
|
||||
<div>
|
||||
<h4 className="dim" style={{ margin: '4px 0 6px' }}>天下快讯</h4>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, maxHeight: 380, overflowY: 'auto' }}>
|
||||
{news.length === 0 && <div className="dim2">风平浪静,尚无消息。</div>}
|
||||
{news.map((row, i) => (
|
||||
<div key={i} className="ch-item" style={{ padding: '5px 8px' }}>
|
||||
<span className="ch-month">{row.year}年{row.month}月</span>
|
||||
<span>{row.text}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="dim" style={{ margin: '4px 0 6px' }}>群雄谱(点开宗主详情)</h4>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
{Object.values(w.state.npcFamilies).map((npc) => {
|
||||
const def = npcById(npc.id)
|
||||
const dyn = (ws.npcDyn ?? {})[npc.id]
|
||||
const open = view === npc.id
|
||||
return (
|
||||
<div key={npc.id} style={{ border: '1px solid rgba(140,100,50,0.3)', borderRadius: 6, padding: '7px 10px', background: 'rgba(18,14,9,0.4)' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }} onClick={() => setView(open ? null : npc.id)}>
|
||||
<span>
|
||||
<b>{npc.name}</b> <span className="dim">{def.style}</span>
|
||||
{npc.allied && <span className="tag gold-t" style={{ marginLeft: 6 }}>盟</span>}
|
||||
</span>
|
||||
<span className="dim">势力{npc.power} · 关系{npc.relation}</span>
|
||||
</div>
|
||||
{open && (
|
||||
<div style={{ marginTop: 6, fontSize: '0.86rem', color: '#c8b285', borderTop: '1px solid rgba(140,100,50,0.25)', paddingTop: 6 }}>
|
||||
<div className="dim">宗主:{dyn?.leaderName ?? '未知'}({dyn ? MAJOR_NAMES[REALM_IDX[dyn.leaderRealmIdx] ?? 'qi'] : ''} · {dyn?.leaderAge ?? '?'}岁)</div>
|
||||
<div className="dim2" style={{ margin: '2px 0 6px' }}>{def.desc}</div>
|
||||
<div className="dim">新近之事:{dyn?.lastEvent ? `${dyn.lastEvent}(${dyn.lastEventYear ?? '?'}年)` : '暂无'}</div>
|
||||
{npc.alliedSinceYear && <div className="dim">同盟自 {npc.alliedSinceYear} 年</div>}
|
||||
<div style={{ marginTop: 6, display: 'flex', gap: 6 }}>
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
disabled={npc.allied || npc.relation < 40}
|
||||
title={npc.relation < 40 ? '关系不足(需40)' : ''}
|
||||
onClick={() => { w.setAlliance(npc.id, true); bump() }}
|
||||
>缔盟</button>
|
||||
{npc.allied && (
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
onClick={() => { w.setAlliance(npc.id, false); bump() }}
|
||||
>解盟(关系-20)</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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' && <ExpeditionPanel />}
|
||||
{panel === 'chronicle' && <ChroniclePanel />}
|
||||
{panel === 'legacy' && <LegacyPanel />}
|
||||
{panel === 'world' && <WorldPanel />}
|
||||
{panel === 'settings' && <SettingsPanel />}
|
||||
{gameOverReason && (
|
||||
<div className="gameover-banner">
|
||||
|
||||
@@ -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<GameStore>((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('开局')
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user