feat(0.1.16-P1): 资源闭环引擎(修为耗草/炼丹概率化/铸器/灵脉收益/NPC战力/灾变落效)+ C13C14
【A 经济闭环】 - 修为经济化:练气以上修行者月耗灵草(闭关2普通1),无草速修0.6~1.0衰减+缺草年告警 - 炼丹等级化:bonusOf 表达式引擎(白名单安全求值),craftChance 转正;丹方单源 PILL_RECIPES(聚气L1/凝元L2/破境L3)+ 失败折半退料 - 炼器筑宝:FORGE_RECIPES(法器/灵器/法宝,灵矿+兽核+灵石) - 秘境灵脉:secretQiOf→rollWarbooty 收益门槛(0.5~1.3折算);掉落按秘境 techGrades 过滤 - NPC 实力参战:raid 强度/风险随 npc.power 浮动(0.5~1.8) - 灾年落效:疫病/兽潮一次性袭击 + CALAMITY_FAMILY 月度减产乘子 【C 治理】 - C13 破境丹渡劫修正:大境界时药力→tribBoost 加成(+12%)入渡劫事件,不再跳过三选 - C14 存储防线:loadState 补 migrate;MigrationError(TOO_NEW/未知版本)提示升级 【UI 半批】 - 坊市新增 定制tab(丹方/铸器);ChroniclePanel 手写史书输入条;MemberModal 册立家主钮 - 年报摘要(要闻5条+辞世讣告);EventModal 选项收益预览 - 结盟引擎:setAlliance(relation≥40)/盟友岁差不降/年利15灵石
This commit is contained in:
@@ -8,6 +8,37 @@ import { combatPowerOf } from '../../game/engine/runtime/Systems/combat'
|
||||
import { FORMATIONS, FormationId } from '../../game/data/formations'
|
||||
import { ModalShell } from './ModalShell'
|
||||
|
||||
const RES_NAME: Record<string, string> = { lingcao: '灵草', lingkuang: '灵矿', beastcore: '兽核', stones: '灵石' }
|
||||
|
||||
function describeEff(eff: Record<string, never> | { [k: string]: unknown }): string {
|
||||
const parts: string[] = []
|
||||
const e = eff as { res?: Record<string, number>; rep?: number; relation?: Record<string, number>; addBuilding?: string; pillGain?: Record<string, number>; mission?: string; addTech?: string; raid?: { npcId: string }; trib?: { mode: string }; feisheng?: { stay: boolean }; techniqueChance?: number; artifactChance?: number; flag?: Record<string, unknown> }
|
||||
if (e.res) {
|
||||
for (const [k, v] of Object.entries(e.res)) {
|
||||
const name = RES_NAME[k] ?? k
|
||||
parts.push(v > 0 ? `${name}+${v}` : `${name}${v}`)
|
||||
}
|
||||
}
|
||||
if (e.rep) parts.push(`声望${e.rep > 0 ? '+' : ''}${e.rep}`)
|
||||
if (e.relation) {
|
||||
for (const v of Object.values(e.relation)) parts.push(v > 0 ? `交好+${v}` : `交恶${v}`)
|
||||
}
|
||||
if (e.addBuilding) parts.push('增筑' + e.addBuilding)
|
||||
if (e.pillGain) {
|
||||
for (const v of Object.values(e.pillGain)) parts.push(`得丹+${v}`)
|
||||
}
|
||||
if (e.addTech) parts.push('得功法')
|
||||
if (e.mission) parts.push('触发任务')
|
||||
if (e.techniqueChance) parts.push(`功法机率+${Math.round(e.techniqueChance * 100)}%`)
|
||||
if (e.artifactChance) parts.push(`宝器机率+${Math.round(e.artifactChance * 100)}%`)
|
||||
if (e.raid) parts.push('来敌犯境')
|
||||
if (e.trib) {
|
||||
parts.push(e.trib.mode === 'rash' ? '硬渡天劫' : e.trib.mode === 'guard' ? '护法渡劫' : '渡劫延一年')
|
||||
}
|
||||
if (e.feisheng) parts.push(e.feisheng.stay ? '留世不飞升' : '飞升离世')
|
||||
return parts.join(' · ')
|
||||
}
|
||||
|
||||
export function EventModal() {
|
||||
const pendingEventId = useGameStore((s) => s.pendingEventId)
|
||||
const pendingEventDef = useGameStore((s) => s.pendingEventDef)
|
||||
@@ -132,6 +163,7 @@ export function EventModal() {
|
||||
{o.label}
|
||||
{isRaid && <span className="hint">点将迎战 · 可自选阵容</span>}
|
||||
{!isRaid && o.hint && <span className="hint">{o.hint}</span>}
|
||||
<span className="opt-eff">{describeEff(o.eff as never)}</span>
|
||||
</button>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -252,6 +252,20 @@ export function MemberModal({ member }: { member: Character }) {
|
||||
|
||||
{member.alive && (
|
||||
<div className="mm-sec">
|
||||
<div className="dim" style={{ marginBottom: 6 }}>家主权柄</div>
|
||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 10 }}>
|
||||
<button
|
||||
className="btn btn-sm btn-primary"
|
||||
disabled={member.id === w.state.family.headId}
|
||||
title={member.id === w.state.family.headId ? '已在主位' : '册立为家主(名分即天命)'}
|
||||
onClick={() => {
|
||||
w.assignHead(member.id)
|
||||
bump()
|
||||
}}
|
||||
>
|
||||
册立家主
|
||||
</button>
|
||||
</div>
|
||||
<div className="dim" style={{ marginBottom: 6 }}>职事(族中分工)</div>
|
||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
||||
<button
|
||||
|
||||
@@ -4,11 +4,17 @@ import { ModalShell } from './ModalShell'
|
||||
|
||||
export function PaperModal() {
|
||||
const report = useGameStore((s) => s.paperReport)
|
||||
const world = useGameStore((s) => s.world)
|
||||
const closePaper = useGameStore((s) => s.closePaper)
|
||||
if (!report) return null
|
||||
|
||||
const nets = report.nets
|
||||
const y = report.year
|
||||
const yearEntries = (world?.state.chronicle ?? [])
|
||||
.filter((e) => e.year === y)
|
||||
.sort((a, b) => a.month - b.month)
|
||||
const majors = yearEntries.filter((e) => e.important).slice(0, 5)
|
||||
const obits = yearEntries.filter((e) => e.category === 'death').slice(0, 5)
|
||||
|
||||
return (
|
||||
<ModalShell
|
||||
@@ -25,6 +31,22 @@ export function PaperModal() {
|
||||
{report.deaths > report.births && <div className="bad">丁口在减 —— 传宗接代是头等大事</div>}
|
||||
{report.births > report.deaths && <div className="good">丁口渐旺,香火有望。</div>}
|
||||
</div>
|
||||
{majors.length > 0 && (
|
||||
<div style={{ marginTop: 12, borderTop: '1px solid rgba(140,100,50,0.35)', paddingTop: 10 }}>
|
||||
<div className="dim" style={{ marginBottom: 4 }}>家 族 要 闻</div>
|
||||
{majors.map((e) => (
|
||||
<div key={e.id} className="dim" style={{ fontSize: '0.9rem', lineHeight: 1.9 }}>· {e.month}月 {e.text}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{obits.length > 0 && (
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<div className="dim" style={{ marginBottom: 4 }}>此 年 辞 世 者</div>
|
||||
{obits.map((e) => (
|
||||
<div key={e.id} className="dim" style={{ fontSize: '0.9rem', lineHeight: 1.9 }}>· {e.month}月 {e.text}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</ModalShell>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ export default function ChroniclePanel() {
|
||||
void revision
|
||||
const [filter, setFilter] = useState<string>('all')
|
||||
const [battleId, setBattleId] = useState<string | null>(null)
|
||||
const [draft, setDraft] = useState('')
|
||||
const entries = useMemo(() => {
|
||||
if (!world) return []
|
||||
const all = [...world.state.chronicle]
|
||||
@@ -64,7 +65,30 @@ export default function ChroniclePanel() {
|
||||
return (
|
||||
<div>
|
||||
<div className="help-text">
|
||||
族史一卷:由系统自动记下的家族大事。倒序排列,可回看任意一年的兴衰细节。
|
||||
族史一卷:系统自动记下的家族大事,供族人题翰墨于旁。倒序排列,可回看任意一年的兴衰细节。
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, marginBottom: 10 }}>
|
||||
<input
|
||||
className="ch-draft"
|
||||
placeholder="题一笔族史……(如:某年宗祠大修,阖族祭祖)"
|
||||
value={draft}
|
||||
maxLength={80}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && draft.trim()) {
|
||||
world.chronicle('misc', draft.trim(), undefined, false)
|
||||
setDraft('')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
disabled={!draft.trim()}
|
||||
onClick={() => {
|
||||
world.chronicle('misc', draft.trim(), undefined, false)
|
||||
setDraft('')
|
||||
}}
|
||||
>落笔</button>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 10 }}>
|
||||
<button className={`btn btn-sm ${filter === 'all' ? 'btn-primary' : ''}`} onClick={() => setFilter('all')}>全部</button>
|
||||
|
||||
@@ -3,6 +3,8 @@ import { ITEMS } from '../../game/data/items'
|
||||
import { TECHNIQUES } from '../../game/data/techniques'
|
||||
import { techniqueGradeName } from '../../game/data/realms'
|
||||
import { marketPrice, buyItem, sellItem, buyTechnique } from '../../game/engine/sim/Market'
|
||||
import { PILL_RECIPES, FORGE_RECIPES } from '../../game/data/items'
|
||||
import { bonusOf } from '../../game/data/buildings'
|
||||
import { useMemo, useState } from 'react'
|
||||
|
||||
export default function MarketPanel() {
|
||||
@@ -10,7 +12,7 @@ export default function MarketPanel() {
|
||||
const bump = useGameStore((s) => s.bump)
|
||||
const revision = useGameStore((s) => s.revision)
|
||||
void revision
|
||||
const [tab, setTab] = useState<'goods' | 'tech'>('goods')
|
||||
const [tab, setTab] = useState<'goods' | 'tech' | 'craft'>('goods')
|
||||
if (!world) return null
|
||||
const w = world
|
||||
const fam = w.state.family
|
||||
@@ -28,6 +30,7 @@ export default function MarketPanel() {
|
||||
<div className="tabs" style={{ padding: 0, background: 'none', border: 'none', marginBottom: 10 }}>
|
||||
<div className={`tab ${tab === 'goods' ? 'sel' : ''}`} onClick={() => setTab('goods')}>货栈</div>
|
||||
<div className={`tab ${tab === 'tech' ? 'sel' : ''}`} onClick={() => setTab('tech')}>法帖</div>
|
||||
<div className={`tab ${tab === 'craft' ? 'sel' : ''}`} onClick={() => setTab('craft')}>定制</div>
|
||||
</div>
|
||||
{tab === 'goods' && (
|
||||
<>
|
||||
@@ -90,30 +93,7 @@ export default function MarketPanel() {
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
<tr>
|
||||
<td><b><span className="s-icon">炼</span> 丹房炼制</b></td>
|
||||
<td className="dim">灵草+辅矿 → 丹药(丹房等级越高成数越足)</td>
|
||||
<td className="dim">{fam.buildings['danfang'] ? `${fam.buildings['danfang']}级丹房` : '未建丹房'}</td>
|
||||
<td className="dim">灵草{inv['lingcao'] ?? 0}</td>
|
||||
<td>
|
||||
{fam.buildings['danfang'] && (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
disabled={(inv['lingcao'] ?? 0) < 15 || fam.stones < 20}
|
||||
title="需灵草15+灵石20"
|
||||
onClick={() => { w.craftPill('qiyuan'); bump() }}
|
||||
>炼气丹</button>{' '}
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
disabled={(inv['lingcao'] ?? 0) < 25 || (inv['beastcore'] ?? 0) < 4 || fam.stones < 60}
|
||||
title="需灵草25+兽核4+灵石60"
|
||||
onClick={() => { w.craftPill('ningyuan'); bump() }}
|
||||
>炼元丹</button>
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
@@ -173,6 +153,72 @@ export default function MarketPanel() {
|
||||
</table>
|
||||
</>
|
||||
)}
|
||||
{tab === 'craft' && (
|
||||
<>
|
||||
<div className="help-text">
|
||||
丹房依「灵草+兽核+灵石」配比丹药,成数随丹房等级;破境丹需丹房三级。炼器以灵矿兽核+灵石相合,无建筑门槛,只看家底。
|
||||
</div>
|
||||
<h4 className="dim" style={{ margin: '8px 0 4px' }}>丹房炼制(当前 {fam.buildings['danfang'] ?? 0} 级 · 成率 {Math.round(bonusOf('danfang', 'craftChance', fam.buildings['danfang'] ?? 0) * 100)}%)</h4>
|
||||
<table className="market-table">
|
||||
<thead>
|
||||
<tr><th>丹方</th><th>药性</th><th>配方</th><th>门槛</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{PILL_RECIPES.map((r) => {
|
||||
const lvl = fam.buildings['danfang'] ?? 0
|
||||
const enough = (inv['lingcao'] ?? 0) >= r.lingcao && (inv['beastcore'] ?? 0) >= r.beastcore && fam.stones >= r.stones
|
||||
return (
|
||||
<tr key={r.output}>
|
||||
<td><b><span className="s-icon">丹</span> {r.name}</b></td>
|
||||
<td className="dim">{r.desc}</td>
|
||||
<td className="dim">草{r.lingcao}·核{r.beastcore}·灵石{r.stones}</td>
|
||||
<td className="dim">{r.danfangLevel}级丹房</td>
|
||||
<td>
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
disabled={lvl < r.danfangLevel || !enough}
|
||||
title={lvl < r.danfangLevel ? `丹房需升到 ${r.danfangLevel} 级` : enough ? '炼制(失败折半退料)' : '材料不足'}
|
||||
onClick={() => { w.craftPill(r.output.replace('pill-', '') as 'qiyuan' | 'ningyuan' | 'pojing'); bump() }}
|
||||
>炼</button>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
<h4 className="dim" style={{ margin: '14px 0 4px' }}>铸器坊(灵矿+兽核+灵石 → 兵刃)</h4>
|
||||
<table className="market-table">
|
||||
<thead>
|
||||
<tr><th>兵刃</th><th>妙用</th><th>配方</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{FORGE_RECIPES.map((r) => {
|
||||
const enough = (inv['lingkuang'] ?? 0) >= r.lingkuang && (inv['beastcore'] ?? 0) >= r.beastcore && fam.stones >= r.stones
|
||||
return (
|
||||
<tr key={r.output}>
|
||||
<td><b><span className="s-icon">器</span> {r.name}</b></td>
|
||||
<td className="dim">{r.desc}</td>
|
||||
<td className="dim">矿{r.lingkuang}·核{r.beastcore}·灵石{r.stones}</td>
|
||||
<td>
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
disabled={!enough}
|
||||
title={enough ? '淬火铸成' : '材料不足'}
|
||||
onClick={() => { w.forgeArtifact(r.output as 'weapon-qi' | 'weapon-ling' | 'weapon-fa'); bump() }}
|
||||
>铸</button>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
{(fam.buildings['danfang'] ?? 0) > 0 && (
|
||||
<div className="dim2" style={{ marginTop: 8 }}>
|
||||
破境丹亦可等拍卖会落槌(需要时留神百年拍卖)。
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div className="dim2" style={{ marginTop: 10 }}>
|
||||
灵矿不足时可上坊市民间收购;妖丹于探秘与灵兽园中可得。
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user