【引擎审计修复】 - P0:大比 once(原整年刷 12 场、奖励通胀);flag 年份键 3 年剪枝(长线膨胀) - P1:飞升离世累加 feishengCount(飞升之资称号可达);圣者护族接消费(原死代码); 大比/传薪挂能力卡门控;pending 抢渡劫时不再重设 - P2:version 硬编码统一 0.1.11;yearaxis 飞升归类;MonumentList 挂载 【UI 审计修复】 - P0:已故成员卡显示「已殁」(原秀活动 status);空队迎战禁用+提示(原静默失效) - P1:指婚候选去截断(滚动列表);终局出口(回卷/主菜单按钮);saveNow 反馈; 事件来临写入 feed(k-event 使用);遣队「最强 N 人/清空」一键;寻衅确认+联姻原因提示; 未建建筑视觉 uc 类 - P2:speed 死代码;Modal 分层 z-index;FamilyPanel 在世/先人分隔 【UX 三件套】 - 警讯徽章(瓶颈/重伤/在外/媒缘/孤嗣之忧)顶栏一屏秒答「我该干嘛」 - 首启引导任务条(四步目标 + 完成推进,可跳不强制) - 顶栏丹药瘦身(4 资源);数字千/万化;瓶颈卡红框高亮 【测试】905→923(urgency/guide/format 三模块 + 审计修复回归 6 项) 金钟罩三档重固化(0.1.11),typecheck/build 通过
323 lines
14 KiB
TypeScript
323 lines
14 KiB
TypeScript
import { useGameStore } from '../store'
|
||
import { Character, GameState } from '../../game/types/domain'
|
||
import { describeRealm, nextRealm } from '../../game/data/realms'
|
||
import { describeRoots, ROOT_GRADE_NAMES } from '../../game/data/elements'
|
||
import { TRAITS } from '../../game/data/traits'
|
||
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 { POSTS, POST_ORDER } from '../../game/data/posts'
|
||
import { buildBiography } from '../../game/core/biography'
|
||
|
||
function aspName(c: Character): string {
|
||
const map: Record<string, string> = {
|
||
dao: '志向·求道', zhen: '志向·镇族', cheng: '志向·承宗', shang: '志向·商略', bing: '志向·兵略', geng: '志向·耕读', yun: '志向·云游'
|
||
}
|
||
return map[c.aspiration ?? ''] ?? '志向·未定'
|
||
}
|
||
|
||
function fitTag(c: Character): string | null {
|
||
const t = TECHNIQUES.find((x) => x.id === c.techniqueId)
|
||
if (!t) return null
|
||
return t.element === c.roots.primary ? `本命·${t.element}契合` : null
|
||
}
|
||
|
||
function deadish(c: Character): boolean {
|
||
return !c.alive
|
||
}
|
||
|
||
function gradeShort(g: number): string {
|
||
const map: Record<number, string> = { 0: '凡', 1: '黄', 2: '玄', 3: '地', 4: '天', 5: '仙' }
|
||
return map[g] ?? '?'
|
||
}
|
||
|
||
function pillCount(s: GameState, id: string): number {
|
||
return s.family.inventory[id] ?? 0
|
||
}
|
||
|
||
function equipableItems(member: Character, s: GameState): string[] {
|
||
const inv = Object.keys(ARTIFACT_POWER).filter((ai) => (s.family.inventory[ai] ?? 0) > 0)
|
||
if (member.equipment && !inv.includes(member.equipment)) inv.push(member.equipment)
|
||
return inv
|
||
}
|
||
|
||
export function MemberModal({ member }: { member: Character }) {
|
||
const world = useGameStore((s) => s.world)
|
||
const setSelected = useGameStore((s) => s.setSelected)
|
||
const bump = useGameStore((s) => s.bump)
|
||
if (!world) return null
|
||
const w = world
|
||
const s = w.state
|
||
const next = nextRealm(member.realm)
|
||
const nextName = next ? describeRealm(next) : '已至巅峰'
|
||
const tech = TECHNIQUES.find((t) => t.id === member.techniqueId)
|
||
const power = member.alive ? combatPowerOf(w, member) : 0
|
||
const lifespan = lifespanOf(w, member)
|
||
|
||
return (
|
||
<div className="modal-outer" onClick={() => setSelected(undefined)}>
|
||
<div className="modal member-modal" onClick={(e) => e.stopPropagation()}>
|
||
<div className="modal-title" style={{ fontSize: '1.4rem' }}>
|
||
{member.name}
|
||
{!member.alive && <span className="bad" style={{ fontSize: '1rem' }}> · 殁</span>}
|
||
</div>
|
||
<div className="modal-cat">
|
||
第{member.generation}代 · {member.gender === 'male' ? '男' : '女'} · {w.ageOf(member)}岁 · 寿数约{lifespan} · {describeRoots(member.roots)}
|
||
</div>
|
||
|
||
<div className="mm-grid">
|
||
<div className="mm-line"><span>境界进度</span><b>{describeRealm(member.realm)} {Math.floor(member.realmProgress)}%</b></div>
|
||
<div className="mm-line"><span>下一关</span><b className={member.realmProgress >= 100 ? 'gold' : ''}>{nextName}</b></div>
|
||
<div className="mm-line"><span>战力</span><b>{Math.round(power)}</b></div>
|
||
<div className="mm-line"><span>状态</span><b>
|
||
{member.state === 'meditation'
|
||
? '闭关'
|
||
: member.state === 'expedition'
|
||
? '外出'
|
||
: member.state === 'wounded'
|
||
? '养伤'
|
||
: member.state === 'apprentice'
|
||
? '寄读'
|
||
: '无事'}
|
||
</b></div>
|
||
<div className="mm-line"><span>气血</span><b>{Math.round(member.health)}</b></div>
|
||
<div className="mm-line"><span>功法</span><b>{tech ? `${tech.name}(${gradeShort(tech.grade)}阶)` : member.realm.major === 'mortal' ? '—' : '未习功法'}</b></div>
|
||
<div className="mm-line"><span>法宝</span><b>{member.equipment ? `${ITEMS[member.equipment].name}(+${Math.round((ARTIFACT_POWER[member.equipment] ?? 0) * 100)}%)` : '—'}</b></div>
|
||
<div className="mm-line"><span>姻亲</span><b>{member.spouseId ? s.members[member.spouseId]?.name ?? '' : member.spouseHouse ?? '未婚'}</b></div>
|
||
</div>
|
||
|
||
<div className="bar bar-red" style={{ marginTop: 10 }} title="气血">
|
||
<div style={{ width: `${member.health}%` }} />
|
||
</div>
|
||
|
||
<div className="mm-sec">
|
||
<div className="mm-grid">
|
||
<div className="mm-line"><span>悟性</span><b>{member.perception}/10</b></div>
|
||
<div className="mm-line"><span>根骨</span><b>{member.physique}/10</b></div>
|
||
<div className="mm-line"><span>心性</span><b>{member.mind}/10</b></div>
|
||
<div className="mm-line"><span>魅力</span><b>{member.charm}/10</b></div>
|
||
<div className="mm-line"><span>气运</span><b>{member.fortune}/12</b></div>
|
||
<div className="mm-line"><span>品性</span><b>{member.fortune ? (member.fortune > 7 ? '俊逸' : '端正') : '—'}</b></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mm-sec">
|
||
<div className="dim" style={{ marginBottom: 4 }}>秉性</div>
|
||
{member.traits.map((t) => (
|
||
<span key={t} className="tag" style={{ marginRight: 4 }}>
|
||
{TRAITS[t]?.name ?? t}
|
||
</span>
|
||
))}
|
||
{member.traits.length === 0 && <span className="dim2">平淡无奇</span>}
|
||
</div>
|
||
|
||
<div className="mm-sec">
|
||
<div className="dim" style={{ marginBottom: 6 }}>起居</div>
|
||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||
{member.alive && member.state !== 'expedition' && (
|
||
<button
|
||
className="btn btn-sm"
|
||
onClick={() => {
|
||
w.setMeditation(member.id, member.state !== 'meditation')
|
||
bump()
|
||
}}
|
||
>
|
||
{member.state === 'meditation' ? '出关' : '闭关'}
|
||
</button>
|
||
)}
|
||
{member.alive && member.realmProgress >= 100 && member.realm.major !== 'spirit' && (
|
||
<button
|
||
className="btn btn-sm btn-primary"
|
||
onClick={() => {
|
||
w.assistedBreakthrough(member.id)
|
||
bump()
|
||
}}
|
||
>
|
||
冲击{nextName}
|
||
</button>
|
||
)}
|
||
{member.alive && (
|
||
<>
|
||
<button
|
||
className="btn btn-sm"
|
||
disabled={pillCount(s, 'pill-qiyuan') <= 0}
|
||
title={`聚气丹×${pillCount(s, 'pill-qiyuan')}`}
|
||
onClick={() => {
|
||
w.takePill(member.id, 'pill-qiyuan')
|
||
bump()
|
||
}}
|
||
>
|
||
服聚气丹({pillCount(s, 'pill-qiyuan')})
|
||
</button>
|
||
{member.realm.major !== 'mortal' && (
|
||
<button
|
||
className="btn btn-sm"
|
||
disabled={pillCount(s, 'pill-ningyuan') <= 0}
|
||
title={`凝元丹×${pillCount(s, 'pill-ningyuan')}`}
|
||
onClick={() => {
|
||
w.takePill(member.id, 'pill-ningyuan')
|
||
bump()
|
||
}}
|
||
>
|
||
服凝元丹({pillCount(s, 'pill-ningyuan')})
|
||
</button>
|
||
)}
|
||
{member.realmProgress >= 70 && member.realm.major !== 'mortal' && (
|
||
<button
|
||
className="btn btn-sm"
|
||
disabled={pillCount(s, 'pill-pojing') <= 0}
|
||
title={`破境丹×${pillCount(s, 'pill-pojing')}`}
|
||
onClick={() => {
|
||
w.takePill(member.id, 'pill-pojing')
|
||
bump()
|
||
}}
|
||
>
|
||
服破境丹({pillCount(s, 'pill-pojing')})
|
||
</button>
|
||
)}
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{member.alive && (
|
||
<div className="mm-sec">
|
||
<div className="dim" style={{ marginBottom: 6 }}>传功(藏书阁所录)</div>
|
||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
||
{s.family.techniques.map((tid) => (
|
||
<button
|
||
key={tid}
|
||
className="btn btn-sm"
|
||
style={{ borderColor: member.techniqueId === tid ? 'var(--gold)' : 'var(--line)' }}
|
||
onClick={() => {
|
||
w.giveTechnique(member.id, tid)
|
||
bump()
|
||
}}
|
||
>
|
||
{TECHNIQUES.find((t) => t.id === tid)?.name ?? tid}
|
||
</button>
|
||
))}
|
||
{s.family.techniques.length === 0 && <span className="dim2">无典籍存库</span>}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{member.alive && (
|
||
<div className="mm-sec">
|
||
<div className="dim" style={{ marginBottom: 6 }}>法宝装备(府库所列)</div>
|
||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
||
{equipableItems(member, s).length === 0 && <span className="dim2">府库暂无存货(可往坊市购得或于秘境寻获)</span>}
|
||
{equipableItems(member, s).map((ai) => (
|
||
<button
|
||
key={ai}
|
||
className="btn btn-sm"
|
||
style={{
|
||
borderColor: member.equipment === ai ? 'var(--vermilion)' : undefined,
|
||
color: member.equipment === ai ? 'var(--vermilion-bright)' : undefined
|
||
}}
|
||
onClick={() => {
|
||
w.equip(member.id, ai)
|
||
bump()
|
||
}}
|
||
>
|
||
<span className="s-icon" style={{ marginRight: 4 }}>{ITEMS[ai].icon}</span>
|
||
{ITEMS[ai].name}(+{Math.round((ARTIFACT_POWER[ai] ?? 0) * 100)}%){member.equipment === ai ? ' · 已佩' : ''}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{member.alive && (
|
||
<div className="mm-sec">
|
||
<div className="dim" style={{ marginBottom: 6 }}>职事(族中分工)</div>
|
||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
||
<button
|
||
className={`btn btn-sm ${!member.post ? 'btn-primary' : ''}`}
|
||
onClick={() => {
|
||
w.assignPost(member.id, undefined)
|
||
bump()
|
||
}}
|
||
>
|
||
闲居
|
||
</button>
|
||
{POST_ORDER.filter((pid) => pid !== 'head').map((pid) => {
|
||
const def = POSTS[pid]
|
||
const maxed = w.postCount(pid) >= def.max && member.post !== pid
|
||
return (
|
||
<button
|
||
key={pid}
|
||
className={`btn btn-sm ${member.post === pid ? 'btn-primary' : ''}`}
|
||
disabled={maxed && !!member.post}
|
||
style={maxed && !member.post ? { opacity: 0.45 } : undefined}
|
||
title={`${def.desc}${w.postCount(pid)}/${def.max} 在任`}
|
||
onClick={() => {
|
||
w.assignPost(member.id, pid)
|
||
bump()
|
||
}}
|
||
>
|
||
{def.name}({w.postCount(pid)}/{def.max})
|
||
</button>
|
||
)
|
||
})}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{member.alive && (
|
||
<div className="mm-sec">
|
||
<div className="dim" style={{ marginBottom: 4 }}>志向与契合</div>
|
||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
||
<span className="tag">{aspName(member)}</span>
|
||
{fitTag(member) && <span className="tag gold-t">{fitTag(member)}</span>}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="mm-sec">
|
||
<div className="dim" style={{ marginBottom: 6 }}>小传(春秋原记录)</div>
|
||
<div className="bio-box">
|
||
{buildBiography(w.state, member.id).map((l, i) => (
|
||
<div key={i} className={deadish(member) && l.label === '墓志' ? 'gold' : ''}>
|
||
{l.label && <b className="bio-label">{l.label} · </b>}
|
||
{l.text}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{member.alive && w.canMarry(member.id) && (
|
||
<div className="mm-sec">
|
||
<div className="dim" style={{ marginBottom: 6 }}>指婚(寻配族内共居者,远亲无碍)</div>
|
||
<div className="marriage-list">
|
||
{w.marriageCandidatesOf(member.id).map((cand) => (
|
||
<button
|
||
key={cand.id}
|
||
className="btn btn-sm"
|
||
onClick={() => {
|
||
w.marryTo(member.id, cand.id)
|
||
bump()
|
||
}}
|
||
>
|
||
{cand.name} · {describeRealm(cand.realm)} · {w.ageOf(cand)}岁
|
||
</button>
|
||
))}
|
||
{w.marriageCandidatesOf(member.id).length === 0 && <span className="dim2">暂无适配人选</span>}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 16 }}>
|
||
{member.children.length > 0 && (
|
||
<span className="dim2">
|
||
子女:{member.children.map((cid) => s.members[cid]?.name ?? '?').join('、')}
|
||
</span>
|
||
)}
|
||
<button className="btn" onClick={() => setSelected(undefined)}>阖上</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|