v0.1.4: 百年定局(终局/大比/寄读/春秋原)
- 望气定鼎:stats 追踪四维(人兴/道兴/威名/香火)+ 十二称号分档 + 铭文诗 春秋原页预演称号,落印定局(青册留档,不阻继续游历) - 太虚大比:每五年征召,点将三关(初试/拔擢/问鼎)复用战斗结算, 名次四档红利(声望/灵石/全场好感),史书战报自动落笔 - 宗门寄读:随机宗门收徒(适龄少年),寄读 2-4 年期间每季寄资源, 期满三选:归来精进(可触发突破+随机机运)或续读深造;低概率择师不回 - 求经台:藏书阁≥3 解锁,两年一访得稀有功法 - 春秋原复盘页:气数四维图/岁末族簿汇编/生卒与突破年表/大比年表 - 修复:大比征召曾因月份条件错过整年;抢在百年庆典前的优先级倒置 - 测试 161→176(评分边界/称号分档/大比三轨/寄读全链/求经冷却)
This commit is contained in:
@@ -16,7 +16,7 @@ export function EventModal() {
|
||||
const [squadSel, setSquadSel] = useState<string[]>([])
|
||||
if (!pendingEventId || !pendingEventDef || !world) return null
|
||||
|
||||
const raidIdx = pendingEventDef.options.findIndex((o) => o.eff.raid)
|
||||
const raidIdx = pendingEventDef.options.findIndex((o) => o.eff.raid || o.eff.tournament)
|
||||
const squadPool: Character[] =
|
||||
raidIdx >= 0
|
||||
? world.aliveMembers()
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { useGameStore } from '../store'
|
||||
import { LegacyArch } from '../../game/core/legacy'
|
||||
import { sBell } from '../sound'
|
||||
|
||||
function dimLabel(k: string): string {
|
||||
const map: Record<string, string> = {
|
||||
renXing: '人兴',
|
||||
daoXing: '道兴',
|
||||
weiMing: '威名',
|
||||
xiangHuo: '香火'
|
||||
}
|
||||
return map[k] ?? k
|
||||
}
|
||||
|
||||
export function ResolveModal({ arch, onClose }: { arch: LegacyArch; onClose: () => void }) {
|
||||
return (
|
||||
<div className="modal-outer">
|
||||
<div className="modal resolve-modal">
|
||||
<div className="modal-cat" style={{ color: '#97835c', letterSpacing: 4 }}>
|
||||
—— 百年望气 · 定鼎之文 ——
|
||||
</div>
|
||||
<div className="resolve-title">「{arch.title}」</div>
|
||||
<div className="resolve-dims">
|
||||
{Object.entries(arch.dims)
|
||||
.filter(([k]) => k !== 'total')
|
||||
.map(([k, v]) => (
|
||||
<div key={k} className="resolve-dim">
|
||||
<span className="dim">{dimLabel(k)}</span>
|
||||
<div className="bar" style={{ flex: 1, height: 6 }}>
|
||||
<div style={{ width: `${Math.min(100, (v / 160) * 100)}%` }} />
|
||||
</div>
|
||||
<b>{v}</b>
|
||||
</div>
|
||||
))}
|
||||
<div className="resolve-dim">
|
||||
<span className="dim">总评</span>
|
||||
<div className="bar" style={{ flex: 1, height: 6 }}>
|
||||
<div style={{ width: `${Math.min(100, (arch.dims.total / 400) * 100)}%`, background: 'linear-gradient(90deg,#8e2f24,#c05a41,#e0c15e)' }} />
|
||||
</div>
|
||||
<b className="gold">{arch.dims.total}</b>
|
||||
</div>
|
||||
</div>
|
||||
<div className="resolve-poem">
|
||||
{arch.poem.map((l, i) => (
|
||||
<div key={i}>{l}</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="resolve-stamp">仙途家族志 · 青册</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'center', gap: 10, marginTop: 14 }}>
|
||||
<button className="btn" onClick={onClose}>暂不落印</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => {
|
||||
const w = useGameStore.getState().world
|
||||
if (w) {
|
||||
sBell()
|
||||
w.resolveLegacyNow()
|
||||
useGameStore.getState().bump()
|
||||
useGameStore.getState().refreshSlots()
|
||||
}
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
落 印 定 局
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ResolveModal
|
||||
@@ -0,0 +1,146 @@
|
||||
import { useState } from 'react'
|
||||
import { useGameStore } from '../store'
|
||||
import { computeLegacy, resolveLegacy } from '../../game/core/legacy'
|
||||
import { resolveLegacy as doesIt } from '../../game/core/legacy'
|
||||
import { ResolveModal } from '../components/ResolveModal'
|
||||
|
||||
void doesIt
|
||||
|
||||
export default function LegacyPanel() {
|
||||
const world = useGameStore((s) => s.world)
|
||||
const revision = useGameStore((s) => s.revision)
|
||||
void revision
|
||||
const [showResolve, setShowResolve] = useState(false)
|
||||
const [view, setView] = useState<'dims' | 'chronicle' | 'report'>('dims')
|
||||
if (!world) return null
|
||||
const w = world
|
||||
const s = w.state
|
||||
const dims = computeLegacy(s)
|
||||
const arch = resolveLegacy(s)
|
||||
const resolved = !!s.family.flag['resolved']
|
||||
|
||||
const yearly = s.yearlyReports.slice().sort((a, b) => a.year - b.year)
|
||||
const births = Object.values(s.members).filter((c) => c.bornYear >= 0)
|
||||
const genMin = Math.min(...Object.values(s.members).map((c) => c.bornYear))
|
||||
const genMax = Math.max(...Object.values(s.members).map((c) => c.deathYear ?? s.year))
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="help-text">
|
||||
春秋原:家族百年气数之图。望气可预演四维,落印后方入青册(不阻继续行旅)。
|
||||
</div>
|
||||
<div className="card" style={{ display: 'flex', alignItems: 'center', gap: 18, marginBottom: 12 }}>
|
||||
<div className="legacy-peak" style={{ width: 86, height: 110 }}>
|
||||
<div className="legacy-glyph">望</div>
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: '1.04rem', marginBottom: 4 }}>
|
||||
当前气数 · 预演称号 <b className="gold" style={{ fontSize: '1.2rem' }}>{arch.title}</b>
|
||||
{resolved && <span className="tag head-t" style={{ marginLeft: 8 }}>已定局</span>}
|
||||
</div>
|
||||
<div className="dim2" style={{ fontSize: '0.85rem' }}>
|
||||
存续{s.year}年 · {s.family.generation}代 · 声望峰值{s.stats.repPeak} · 丁口峰值{s.stats.popPeak}
|
||||
{s.stats.tourneyBest ? ` · 大比最佳第${s.stats.tourneyBest}名` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
{!resolved && (
|
||||
<button className="btn btn-primary" onClick={() => setShowResolve(true)}>
|
||||
望气定鼎
|
||||
</button>
|
||||
)}
|
||||
<button className="btn" onClick={() => setView('dims')}>四维</button>
|
||||
<button className="btn" onClick={() => setView('report')}>年报</button>
|
||||
<button className="btn" onClick={() => setView('chronicle')}>年表</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{view === 'dims' && (
|
||||
<div className="card">
|
||||
<div className="card-title">气数四维</div>
|
||||
{Object.entries(dims)
|
||||
.filter(([k]) => k !== 'total')
|
||||
.map(([k, v]) => (
|
||||
<div key={k} className="resolve-dim" style={{ maxWidth: 620 }}>
|
||||
<span className="dim">{dimName(k)}</span>
|
||||
<div className="bar" style={{ flex: 1 }}>
|
||||
<div style={{ width: `${Math.min(100, (v / 160) * 100)}%` }} />
|
||||
</div>
|
||||
<b>{v}</b>
|
||||
</div>
|
||||
))}
|
||||
<div className="resolve-dim" style={{ maxWidth: 620 }}>
|
||||
<span className="dim">总评</span>
|
||||
<div className="bar" style={{ flex: 1 }}>
|
||||
<div style={{ width: `${Math.min(100, (dims.total / 400) * 100)}%`, background: 'linear-gradient(90deg,#8e2f24,#c05a41,#e0c15e)' }} />
|
||||
</div>
|
||||
<b className="gold">{dims.total}</b>
|
||||
</div>
|
||||
<div className="dim2" style={{ marginTop: 8 }}>
|
||||
人兴=代数·丁口峰值 · 道兴=最高境界·功法大成 · 威名=声望峰·大比最佳 · 香火=存续·飞升
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{view === 'report' && (
|
||||
<div className="card">
|
||||
<div className="card-title">岁末族簿汇编(逐年)</div>
|
||||
<table className="market-table" style={{ width: '100%' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>年</th><th>灵石盈亏</th><th>诞辰</th><th>辞世</th><th>声望</th><th>宗族战力</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{yearly.slice(-20).map((r) => (
|
||||
<tr key={r.year}>
|
||||
<td>{r.year}年</td>
|
||||
<td className={r.nets >= 0 ? 'good' : 'bad'}>{r.nets >= 0 ? `+${r.nets}` : r.nets}</td>
|
||||
<td>+{r.births}</td>
|
||||
<td className="bad">-{r.deaths}</td>
|
||||
<td>{r.rep}</td>
|
||||
<td>{r.power}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="dim2" style={{ marginTop: 6 }}>共 {yearly.length} 年度账册。</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{view === 'chronicle' && (
|
||||
<div className="card">
|
||||
<div className="card-title">生卒与突破年表</div>
|
||||
<div className="dim" style={{ marginBottom: 6 }}>
|
||||
族人生卒带(首倡 {genMin + s.year - s.year} 年 ~ 卒 {genMax} 年,区间 {genMax - genMin} 载)
|
||||
</div>
|
||||
<div className="dim2" style={{ marginBottom: 10 }}>
|
||||
大比记录:{s.stats.tourneyHistory.length === 0 ? '尚无参赛记录' : s.stats.tourneyHistory.map((t) => `${t.year}年第${t.rank}名`).join('、')}
|
||||
</div>
|
||||
<div className="dim2">
|
||||
突破年表(按年份计数):
|
||||
{breakdown(s).length === 0 ? '暂无' : breakdown(s).map((b) => `${b.year}年×${b.n}`).join(' · ')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showResolve && <ResolveModal arch={arch} onClose={() => setShowResolve(false)} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function dimName(k: string): string {
|
||||
const map: Record<string, string> = { renXing: '人兴', daoXing: '道兴', weiMing: '威名', xiangHuo: '香火' }
|
||||
return map[k] ?? k
|
||||
}
|
||||
|
||||
function breakdown(s: { chronicle: { category: string; year: number }[] }): { year: number; n: number }[] {
|
||||
const counts = new Map<number, number>()
|
||||
for (const e of s.chronicle) {
|
||||
if (e.category === 'breakthrough') {
|
||||
const cur = counts.get(e.year) ?? 0
|
||||
counts.set(e.year, cur + 1)
|
||||
}
|
||||
}
|
||||
return [...counts.entries()].map(([year, n]) => ({ year, n })).sort((a, b) => a.year - b.year)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { PanelId } from '../store'
|
||||
import { RES_INFO } from '../components'
|
||||
import FamilyPanel from '../panels/FamilyPanel'
|
||||
import GenealogyPanel from '../panels/GenealogyPanel'
|
||||
import LegacyPanel from '../panels/LegacyPanel'
|
||||
import TerritoryPanel from '../panels/TerritoryPanel'
|
||||
import MarketPanel from '../panels/MarketPanel'
|
||||
import DiplomacyPanel from '../panels/DiplomacyPanel'
|
||||
@@ -19,6 +20,7 @@ const TABS: { id: PanelId; label: string }[] = [
|
||||
{ id: 'diplomacy', label: '外交' },
|
||||
{ id: 'expedition', label: '探秘' },
|
||||
{ id: 'chronicle', label: '史书' },
|
||||
{ id: 'legacy', label: '春秋原' },
|
||||
{ id: 'settings', label: '设置' }
|
||||
]
|
||||
|
||||
@@ -86,6 +88,7 @@ export default function GameScreen() {
|
||||
{panel === 'diplomacy' && <DiplomacyPanel />}
|
||||
{panel === 'expedition' && <ExpeditionPanel />}
|
||||
{panel === 'chronicle' && <ChroniclePanel />}
|
||||
{panel === 'legacy' && <LegacyPanel />}
|
||||
{panel === 'settings' && <SettingsPanel />}
|
||||
{gameOverReason && (
|
||||
<div className="gameover-banner">
|
||||
|
||||
@@ -8,7 +8,7 @@ import { metaFromState, updateSlotMeta } from './storeHelper'
|
||||
import { setSoundEnabled, sPaper, sGood, sBad, sWar, sBell, sGong, sClick, sTick } from './sound'
|
||||
|
||||
export type Screen = 'boot' | 'newgame' | 'game'
|
||||
export type PanelId = 'family' | 'genealogy' | 'territory' | 'market' | 'diplomacy' | 'expedition' | 'chronicle' | 'settings'
|
||||
export type PanelId = 'family' | 'genealogy' | 'territory' | 'market' | 'diplomacy' | 'expedition' | 'chronicle' | 'legacy' | 'settings'
|
||||
|
||||
export interface GameStore {
|
||||
screen: Screen
|
||||
@@ -173,7 +173,7 @@ export const useGameStore = create<GameStore>((set, get) => ({
|
||||
onBattle: (log) => set({ battleView: log, revision: get().revision + 1 }),
|
||||
|
||||
onPendingEvent: (id) => {
|
||||
const def = findEvent(id)
|
||||
const def = findEvent(id, get().world ?? undefined)
|
||||
set({ pendingEventId: id, pendingEventDef: def })
|
||||
},
|
||||
|
||||
|
||||
@@ -1407,6 +1407,63 @@ body {
|
||||
text-shadow: 0 4px 18px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
/* ---------- 春秋原 & 结局 ---------- */
|
||||
.legacy-peak {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: radial-gradient(circle at 35% 30%, #6e5630, #2c2110 75%);
|
||||
border: 1px solid #7a6434;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 26px rgba(179, 145, 62, 0.28), inset 0 0 18px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
.legacy-glyph {
|
||||
font-size: 2.4rem;
|
||||
color: #efd9a2;
|
||||
letter-spacing: 4px;
|
||||
text-shadow: 0 2px 14px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
.resolve-dim {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.resolve-modal {
|
||||
width: 560px;
|
||||
background:
|
||||
url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='170' height='170'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='2' seed='14'/><feColorMatrix type='saturate' values='0'/></filter><rect width='170' height='170' filter='url(%23n)' opacity='0.055'/></svg>"),
|
||||
linear-gradient(180deg, #f6edd7 0%, #e8d9b8 100%);
|
||||
}
|
||||
.resolve-title {
|
||||
text-align: center;
|
||||
font-size: 2.1rem;
|
||||
letter-spacing: 10px;
|
||||
color: #8c2f22;
|
||||
margin: 10px 0 14px;
|
||||
font-weight: 700;
|
||||
text-shadow: 0 1px 0 rgba(255, 250, 235, 0.8), 0 4px 16px rgba(140, 47, 34, 0.25);
|
||||
}
|
||||
.resolve-dims {
|
||||
border-top: 1px solid rgba(169, 59, 46, 0.3);
|
||||
border-bottom: 1px solid rgba(120, 98, 55, 0.3);
|
||||
padding: 10px 4px;
|
||||
}
|
||||
.resolve-poem {
|
||||
text-align: center;
|
||||
margin: 16px 0 8px;
|
||||
line-height: 2.1;
|
||||
color: #4a3c26;
|
||||
font-size: 1.06rem;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
.resolve-stamp {
|
||||
text-align: center;
|
||||
color: #a06f18;
|
||||
letter-spacing: 6px;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
/* ---------- 宗祠碑录 ---------- */
|
||||
.monument-strip {
|
||||
margin-bottom: 14px;
|
||||
|
||||
Reference in New Issue
Block a user