Files
ChronicleOfTheImmortalClan/src/renderer/ui/panels/ExpeditionPanel.tsx
T
thzxx 809707408b 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 基线固化(修为耗草/炼丹/灾害序列变更受控)
2026-08-23 14:13:13 +08:00

162 lines
6.7 KiB
TypeScript

import { useGameStore } from '../store'
import { MISSIONS, missionById } from '../../game/data/secrets'
import { sendMission, recallAll } from '../../game/engine/runtime/Systems/missions'
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)
const bump = useGameStore((s) => s.bump)
const selectedMissionDef = useGameStore((s) => s.selectedMissionDef)
const setMissionDef = useGameStore((s) => s.setMissionDef)
const revision = useGameStore((s) => s.revision)
void revision
const [squad, setSquad] = useState<string[]>([])
const [formation, setFormation] = useState<FormationId>('vanguard')
if (!world) return null
const w = world
const s = w.state
const active = s.missions.filter((m) => !m.done)
const candidates = Object.values(s.members).filter(
(c) => c.alive && w.ageOf(c) >= 16 && c.state !== 'expedition'
)
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]))
}
return (
<div>
<div className="help-text">
探秘即遣队伍远行:越远越险,收获越丰。途中遇袭会结算战报——带足好手再上路。
(同期至多三支队伍在外)
</div>
<div className="mission-card">
<div className="card-title" style={{ fontSize: '1rem' }}>择一行路</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill,minmax(180px,1fr))', gap: 8 }}>
{MISSIONS.map((m) => {
const sel = selectedMissionDef === m.id
return (
<div
key={m.id}
style={{
border: sel ? '1px solid var(--gold)' : '1px solid var(--line)',
borderRadius: 4,
padding: '8px 10px',
cursor: 'pointer',
background: 'var(--panel)'
}}
onClick={() => {
setMissionDef(sel ? undefined : m.id)
setSquad([])
}}
>
<div><span className="s-icon">{m.icon}</span> {m.name}</div>
<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>
)
})}
</div>
{def && (
<div style={{ marginTop: 12, borderTop: '1px solid var(--line)', paddingTop: 10 }}>
<div className="dim" style={{ marginBottom: 6 }}>
队伍人选({squad.length}/{def.maxMembers} 人,至少{def.minMembers}人)
</div>
<div className="squad-picker">
{candidates.map((c) => (
<div
key={c.id}
className={`squadian ${squad.includes(c.id) ? 'sel' : ''}`}
onClick={() => toggle(c.id)}
>
{c.name}·{describeRealm(c.realm)}·战力{Math.round(combatPowerOf(w, c))}
</div>
))}
{candidates.length === 0 && <span className="dim2">无可派遣之人</span>}
</div>
<div className="formation-pick" style={{ marginBottom: 8 }}>
{(Object.keys(FORMATIONS) as FormationId[]).map((fid) => (
<div
key={fid}
className={`tag ${formation === fid ? 'gold-t' : ''}`}
style={{ cursor: 'pointer', padding: '3px 10px' }}
onClick={() => setFormation(fid)}
title={FORMATIONS[fid].desc}
>
{FORMATIONS[fid].name}
</div>
))}
</div>
<button
className="btn btn-primary"
disabled={squad.length < def.minMembers || squad.length > def.maxMembers || active.length >= 3}
onClick={() => {
sendMission(w, def.id, squad, formation)
setSquad([])
bump()
}}
>
遣队发兵
</button>
</div>
)}
</div>
{active.length > 0 && (
<div>
<div className="card-title" style={{ fontSize: '1rem', marginTop: 8 }}>在外队伍</div>
{active.map((m) => {
const def2 = missionById(m.defId)
const stage = def2.stages[m.stage]
return (
<div key={m.id} className="mission-card">
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<b><span className="s-icon">{def2.icon}</span> {def2.name}</b>
<span className="dim2" style={{ fontSize: '0.85rem' }}>
{m.memberIds.map((id) => s.members[id]?.name ?? '?').join('、')}
</span>
<span className="dim" style={{ marginLeft: 'auto', fontSize: '0.85rem' }}>
{m.stage + 1}/{def2.stages.length}
{stage ? ` · ${stage.title}` : ''}
</span>
</div>
<div className="dim2" style={{ fontSize: '0.8rem', margin: '6px 0' }}>
启程 {m.startYear}{m.startMonth} · 途中札记:
</div>
<div className="dim" style={{ fontSize: '0.85rem', lineHeight: 1.6 }}>
{m.log.slice(-3).map((l, i) => (
<div key={i}>· {l}</div>
))}
</div>
<button className="btn btn-sm" style={{ marginTop: 6 }} onClick={() => { recallAll(w, m.id); bump() }}>
召回
</button>
</div>
)
})}
</div>
)}
</div>
)
}