v0.1.5: 人物志(志向/渡劫/列传/契合/回声)

- 志向系统:七向(求道/镇族/承宗/商略/兵略/耕读/云游)成年礼自动定志,
  量化加成入修炼/战力/坊市/灵田/添丁/声望,云游减修但际遇更频
- 大境界渡劫:炼气→筑基起,晋升改走劫难事件三选——硬渡/请护法(100灵石)/
  压制来年;护法失败或受牵连;渡劫成败与心性、气血挂勾,高阶有小陨落率
- 灵根本命契合:功法元素×主灵根,契合者修炼+6%/战力+4%,详情页「本命」徽章
- 人物列传:自动列传生成器(生平/修行脉络/历险/姻缘/子嗣/大比/墓志铭),
  成员详情小传分页 + 春秋原列传长卷
- 四邻回声:每两年一桩——友好赠礼/敌视暗桩/中立传闻三态动态事件
- 修复:回声事件 npcId 解析错误、事件优先级(惯例>传闻)
- 测试 176→197(志向加成/契合/渡劫三轨/压制通道/列传结构/回声三态)
This commit is contained in:
2026-08-23 09:08:16 +08:00
parent 98b60076f0
commit 106db18017
17 changed files with 791 additions and 7 deletions
@@ -8,6 +8,24 @@ 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 pillCount(s: GameState, id: string): number {
return s.family.inventory[id] ?? 0
@@ -232,6 +250,28 @@ export function MemberModal({ member }: { member: Character }) {
</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>
+27 -1
View File
@@ -3,6 +3,7 @@ import { useGameStore } from '../store'
import { computeLegacy, resolveLegacy } from '../../game/core/legacy'
import { resolveLegacy as doesIt } from '../../game/core/legacy'
import { ResolveModal } from '../components/ResolveModal'
import { buildBiography } from '../../game/core/biography'
void doesIt
@@ -11,7 +12,7 @@ export default function LegacyPanel() {
const revision = useGameStore((s) => s.revision)
void revision
const [showResolve, setShowResolve] = useState(false)
const [view, setView] = useState<'dims' | 'chronicle' | 'report'>('dims')
const [view, setView] = useState<'dims' | 'chronicle' | 'report' | 'scroll'>('dims')
if (!world) return null
const w = world
const s = w.state
@@ -52,6 +53,7 @@ export default function LegacyPanel() {
<button className="btn" onClick={() => setView('dims')}></button>
<button className="btn" onClick={() => setView('report')}></button>
<button className="btn" onClick={() => setView('chronicle')}></button>
<button className="btn" onClick={() => setView('scroll')}></button>
</div>
</div>
@@ -108,6 +110,8 @@ export default function LegacyPanel() {
</div>
)}
{view === 'scroll' && <ScrollScroll />}
{view === 'chronicle' && (
<div className="card">
<div className="card-title"></div>
@@ -129,6 +133,28 @@ export default function LegacyPanel() {
)
}
function ScrollScroll() {
const world = useGameStore((s) => s.world)
if (!world) return null
const mem = Object.values(world.state.members).sort((a, b) => b.generation - a.generation || b.bornYear - a.bornYear)
return (
<div className="card">
<div className="card-title"></div>
{mem.slice(0, 40).map((c) => {
const bio = buildBiography(world.state, c.id)
return (
<div key={c.id} className="bio-row">
<b>{c.name}</b>
<span className="dim" style={{ marginLeft: 8 }}>
{bio.map((l) => l.text).join(' ').slice(0, 90)}{bio.map((l) => l.text).join(' ').length > 90 ? '……' : ''}
</span>
</div>
)
})}
</div>
)
}
function dimName(k: string): string {
const map: Record<string, string> = { renXing: '人兴', daoXing: '道兴', weiMing: '威名', xiangHuo: '香火' }
return map[k] ?? k
+23
View File
@@ -1407,6 +1407,29 @@ body {
text-shadow: 0 4px 18px rgba(0, 0, 0, 0.8);
}
/* ---------- 列传 ---------- */
.bio-box {
max-height: 240px;
overflow-y: auto;
background: rgba(20, 15, 9, 0.5);
border: 1px solid rgba(120, 98, 55, 0.35);
border-radius: 3px;
padding: 8px 12px;
line-height: 1.8;
font-size: 0.9rem;
color: #e4d6b4;
}
.bio-label {
color: #cdaa63;
font-weight: 400;
}
.bio-row {
padding: 6px 0;
border-bottom: 1px dashed rgba(120, 98, 55, 0.3);
line-height: 1.7;
font-size: 0.9rem;
}
/* ---------- 春秋原 & 结局 ---------- */
.legacy-peak {
display: flex;