v0.1.9: 百年报告 & 发布之窗(报告/迁移/阵型/冬祷/哨兵)
- 百年族史报告:buildReport 纯函数(人口/声望/战力三线、死因榜、突破密度、 大比战绩、代际长度带)+ 春秋原 SVG 折线视图 + 自动措辞结语 - 存档迁移管线 v2:migrate 链(v1→v2)+ 未来版本拒载(TOO_NEW)+ 导出信封 带 appId/schemaVersion + 导入全链路迁移校验;saveState 自动写 CURRENT_SCHEMA - 战斗阵型:锋阵(+10%攻)/雁阵(御5%+溃伤-15%)/蛇阵(溃伤-30%),三处出战接入 (遣队/大比/劫掠)点将面板下拉 + 战报首行标阵 - 冬至岁祷:每年十一月三选(观星+3声望/祈福全族修为/问卜吉凶) - 发布准备:scripts/verify-release.mjs 验收哨兵(npm run verify/verify:package) + README 全面重写(架构/守护/版本沿革);版本号三处归一 - 测试 861→881(报告四例/迁移五链/阵型六则含胜率统计/冬祷四则); 金钟罩三指纹零漂移;typecheck/build 通过
This commit is contained in:
@@ -5,6 +5,7 @@ import { eventCategoryName } from '../../game/data/events'
|
||||
import { describeRealm } from '../../game/data/realms'
|
||||
import { Character } from '../../game/types/domain'
|
||||
import { combatPowerOf } from '../../game/engine/systems/combat'
|
||||
import { FORMATIONS, FormationId } from '../../game/data/formations'
|
||||
|
||||
export function EventModal() {
|
||||
const pendingEventId = useGameStore((s) => s.pendingEventId)
|
||||
@@ -14,6 +15,7 @@ export function EventModal() {
|
||||
const setSpeed = useGameStore((s) => s.setSpeed)
|
||||
const [squadPicking, setSquadPicking] = useState(false)
|
||||
const [squadSel, setSquadSel] = useState<string[]>([])
|
||||
const [formation, setFormation] = useState<FormationId>('vanguard')
|
||||
if (!pendingEventId || !pendingEventDef || !world) return null
|
||||
|
||||
const raidIdx = pendingEventDef.options.findIndex((o) => o.eff.raid || o.eff.tournament)
|
||||
@@ -62,7 +64,20 @@ export function EventModal() {
|
||||
|
||||
{squadPicking && (
|
||||
<div className="modal-squad" style={{ marginBottom: 14 }}>
|
||||
<div className="dim" style={{ marginBottom: 6 }}>迎战点将(默认四人,可改动)</div>
|
||||
<div className="dim" style={{ marginBottom: 6 }}>迎战·列阵(默认四人,可改动)</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}·{FORMATIONS[fid].desc}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="squad-picker">
|
||||
{squadPool.map((c) => (
|
||||
<div
|
||||
|
||||
@@ -4,6 +4,7 @@ import { sendMission, recallAll } from '../../game/engine/systems/missions'
|
||||
import { combatPowerOf } from '../../game/engine/systems/combat'
|
||||
import { useState } from 'react'
|
||||
import { describeRealm, MAJOR_ORDER } from '../../game/data/realms'
|
||||
import { FORMATIONS, FormationId } from '../../game/data/formations'
|
||||
|
||||
export default function ExpeditionPanel() {
|
||||
const world = useGameStore((s) => s.world)
|
||||
@@ -13,6 +14,7 @@ export default function ExpeditionPanel() {
|
||||
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
|
||||
@@ -78,11 +80,24 @@ export default function ExpeditionPanel() {
|
||||
))}
|
||||
{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)
|
||||
sendMission(w, def.id, squad, formation)
|
||||
setSquad([])
|
||||
bump()
|
||||
}}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { resolveLegacy as doesIt } from '../../game/core/legacy'
|
||||
import { ResolveModal } from '../components/ResolveModal'
|
||||
import { buildBiography } from '../../game/core/biography'
|
||||
import { yearAxis, decadeLabel } from '../../game/core/yearaxis'
|
||||
import { buildReport, verdictLine } from '../../game/core/legacyreport'
|
||||
|
||||
void doesIt
|
||||
|
||||
@@ -13,7 +14,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' | 'scroll' | 'axis'>('dims')
|
||||
const [view, setView] = useState<'dims' | 'chronicle' | 'report' | 'scroll' | 'axis' | 'legacy'>('dims')
|
||||
if (!world) return null
|
||||
const w = world
|
||||
const s = w.state
|
||||
@@ -56,6 +57,7 @@ export default function LegacyPanel() {
|
||||
<button className="btn" onClick={() => setView('chronicle')}>年表</button>
|
||||
<button className="btn" onClick={() => setView('scroll')}>长卷</button>
|
||||
<button className="btn" onClick={() => setView('axis')}>年轴</button>
|
||||
<button className="btn" onClick={() => setView('legacy')}>百年报告</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -112,6 +114,8 @@ export default function LegacyPanel() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{view === 'legacy' && <LegacyReportView />}
|
||||
|
||||
{view === 'axis' && <AxisView />}
|
||||
|
||||
{view === 'scroll' && <ScrollScroll />}
|
||||
@@ -137,6 +141,48 @@ export default function LegacyPanel() {
|
||||
)
|
||||
}
|
||||
|
||||
function LegacyReportView() {
|
||||
const world = useGameStore((s) => s.world)
|
||||
if (!world) return null
|
||||
const r = buildReport(world.state)
|
||||
const span = r.years.length
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-title">百年族史报告</div>
|
||||
{span < 4 ? (
|
||||
<div className="dim">开卷尚浅,待岁月沉淀后再启此卷。</div>
|
||||
) : (
|
||||
<>
|
||||
<Sparklines r={r} />
|
||||
<div className="dim" style={{ marginTop: 10 }}>死因榜:{Object.entries(r.deathCauses).filter(([k]) => k !== '').sort((a, b) => b[1] - a[1]).slice(0, 4).map(([k, v]) => `${k}×${v}`).join(' · ') || '尚无记录'}</div>
|
||||
<div className="dim" style={{ marginTop: 6 }}>突破之劲:{r.breakthroughs.slice(-6).map((b) => `${b.year}年×${b.count}`).join(' · ')}</div>
|
||||
<div className="resolve-poem" style={{ marginTop: 14 }}>{verdictLine(r)}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Sparklines({ r }: { r: ReturnType<typeof buildReport> }) {
|
||||
const width = 640
|
||||
const height = 150
|
||||
const pts = (seq: number[], color: string) => {
|
||||
const max = Math.max(...seq, 1)
|
||||
const step = seq.length > 1 ? width / (seq.length - 1) : width
|
||||
return seq.map((v, i) => `${(i * step).toFixed(1)},${(height - 20 - (v / max) * (height - 40)).toFixed(1)}`).join(' ')
|
||||
}
|
||||
return (
|
||||
<svg width="100%" viewBox={`0 0 ${width} ${height}`} style={{ maxHeight: 160, background: 'rgba(20,15,9,0.5)', borderRadius: 4 }}>
|
||||
<polyline fill="none" stroke="#8fae6a" strokeWidth="2" points={pts(r.population, 'green')} />
|
||||
<polyline fill="none" stroke="#c9a227" strokeWidth="2" points={pts(r.reputation, 'gold')} />
|
||||
<polyline fill="none" stroke="#a9a9a9" strokeWidth="2" points={pts(r.power, 'gray')} />
|
||||
<text x="4" y="14" fill="#8fae6a" fontSize="10">人口</text>
|
||||
<text x="64" y="14" fill="#c9a227" fontSize="10">声望</text>
|
||||
<text x="124" y="14" fill="#aaa" fontSize="10">战力</text>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function AxisView() {
|
||||
const world = useGameStore((s) => s.world)
|
||||
if (!world) return null
|
||||
|
||||
@@ -1498,3 +1498,10 @@ body {
|
||||
.monument-strip .card-title {
|
||||
color: #cdaa63;
|
||||
}
|
||||
|
||||
/* ---------- 阵型点选 ---------- */
|
||||
.formation-pick {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user