v0.1.1: 养成闭环 + 管理自主 + 存档安全
- 指婚/续弦:成员详情可主动找人结亲(血亲自动排除、鳏寡可再醮) - 装备 UI:法宝从府库装备/替换,战力实时反馈 - 御敌点将:劫掠事件可自选迎战阵容(默认 Top4 可改) - 宗祠祭祖:每两年一次,灵石150 → 声望+6/全族修为小升 - 回档时间轴:每季快照列表,一键回卷(回卷前自动保当前档) - 战报匣子:史书页全文战报留存可翻阅 - 年度族簿纸笺:每年初弹收支/人丁/战力简报 - 媒人提示条 + 寻衅一年冷却 + 出生率与劫掠频率微调 - 单测 19→29(storage mock roundtrip/回档/指婚血亲/祭祖/寻衅/装备/账本)
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
import { useState } from 'react'
|
||||
import { useGameStore } from '../store'
|
||||
import { applyEventChoice } from '../../game/engine/systems/events'
|
||||
import { eventCategoryName, EventDef } from '../../game/data/events'
|
||||
import { applyEventChoice, rankPower } from '../../game/engine/systems/events'
|
||||
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'
|
||||
|
||||
export function EventModal() {
|
||||
const pendingEventId = useGameStore((s) => s.pendingEventId)
|
||||
@@ -8,17 +12,44 @@ export function EventModal() {
|
||||
const world = useGameStore((s) => s.world)
|
||||
const bump = useGameStore((s) => s.bump)
|
||||
const setSpeed = useGameStore((s) => s.setSpeed)
|
||||
const [squadPicking, setSquadPicking] = useState(false)
|
||||
const [squadSel, setSquadSel] = useState<string[]>([])
|
||||
if (!pendingEventId || !pendingEventDef || !world) return null
|
||||
|
||||
const choose = (idx: number) => {
|
||||
applyEventChoice(world, pendingEventId, idx)
|
||||
const raidIdx = pendingEventDef.options.findIndex((o) => o.eff.raid)
|
||||
const squadPool: Character[] =
|
||||
raidIdx >= 0
|
||||
? world.aliveMembers()
|
||||
.filter((c) => world.ageOf(c) >= 16 && c.state !== 'expedition')
|
||||
.sort((a, b) => rankPower(world, b) - rankPower(world, a))
|
||||
: []
|
||||
|
||||
const closeModal = () => {
|
||||
useGameStore.setState({ pendingEventId: undefined, pendingEventDef: undefined })
|
||||
setSquadPicking(false)
|
||||
setSquadSel([])
|
||||
bump()
|
||||
// 重要事务后暂停推算,给玩家喘息
|
||||
}
|
||||
|
||||
const choose = (idx: number) => {
|
||||
const isRaid = raidIdx >= 0 && raidIdx === idx
|
||||
if (isRaid && !squadPicking) {
|
||||
setSquadPicking(true)
|
||||
setSquadSel(squadPool.slice(0, 4).map((c) => c.id))
|
||||
return
|
||||
}
|
||||
applyEventChoice(world, pendingEventId, idx, isRaid ? squadSel : undefined)
|
||||
closeModal()
|
||||
const sp = useGameStore.getState().speed
|
||||
if (sp > 1) setSpeed(1)
|
||||
}
|
||||
|
||||
const toggleSquad = (id: string) => {
|
||||
setSquadSel((old) =>
|
||||
old.includes(id) ? old.filter((x) => x !== id) : old.length >= 4 ? [...old.slice(1), id] : [...old, id]
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="modal-outer">
|
||||
<div className="modal">
|
||||
@@ -28,13 +59,36 @@ export function EventModal() {
|
||||
{pendingEventDef.once && ' · 仅此一次'}
|
||||
</div>
|
||||
<div className="modal-text">{pendingEventDef.text}</div>
|
||||
|
||||
{squadPicking && (
|
||||
<div className="modal-squad" style={{ marginBottom: 14 }}>
|
||||
<div className="dim" style={{ marginBottom: 6 }}>迎战点将(默认四人,可改动)</div>
|
||||
<div className="squad-picker">
|
||||
{squadPool.map((c) => (
|
||||
<div
|
||||
key={c.id}
|
||||
className={`squadian ${squadSel.includes(c.id) ? 'sel' : ''}`}
|
||||
onClick={() => toggleSquad(c.id)}
|
||||
>
|
||||
{c.name}·{describeRealm(c.realm)}·战力{Math.round(combatPowerOf(world, c))}
|
||||
</div>
|
||||
))}
|
||||
{squadPool.length === 0 && <span className="dim2">无可出战之人</span>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="modal-opts">
|
||||
{pendingEventDef.options.map((o, i) => (
|
||||
<button key={i} className="opt-btn" onClick={() => choose(i)}>
|
||||
{o.label}
|
||||
{o.hint && <span className="hint">{o.hint}</span>}
|
||||
</button>
|
||||
))}
|
||||
{pendingEventDef.options.map((o, i) => {
|
||||
const isRaid = raidIdx === i
|
||||
return (
|
||||
<button key={i} className="opt-btn" onClick={() => choose(i)}>
|
||||
{isRaid && squadPicking ? '出战!(点将已毕)' : o.label}
|
||||
{isRaid && !squadPicking && <span className="hint">点将迎战 · 可自选阵容</span>}
|
||||
{!isRaid && o.hint && <span className="hint">{o.hint}</span>}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useGameStore } from '../store'
|
||||
import { Character } from '../../game/types/domain'
|
||||
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'
|
||||
@@ -8,6 +8,12 @@ import { ITEMS, ARTIFACT_POWER } from '../../game/data/items'
|
||||
import { combatPowerOf } from '../../game/engine/systems/combat'
|
||||
import { lifespanOf } from '../../game/engine/systems/lifecycle'
|
||||
|
||||
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)
|
||||
@@ -153,6 +159,53 @@ export function MemberModal({ member }: { member: Character }) {
|
||||
</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 && w.canMarry(member.id) && (
|
||||
<div className="mm-sec">
|
||||
<div className="dim" style={{ marginBottom: 6 }}>指婚(寻配族内共居者,远亲无碍)</div>
|
||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
||||
{w.marriageCandidatesOf(member.id).slice(0, 6).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">
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useGameStore } from '../store'
|
||||
import { YearlyReport } from '../../game/types/domain'
|
||||
|
||||
export function PaperModal() {
|
||||
const report = useGameStore((s) => s.paperReport)
|
||||
const closePaper = useGameStore((s) => s.closePaper)
|
||||
if (!report) return null
|
||||
|
||||
const nets = report.nets
|
||||
const y = report.year
|
||||
|
||||
return (
|
||||
<div className="modal-outer" onClick={closePaper}>
|
||||
<div className="modal yearpaper-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="modal-title" style={{ fontSize: '1.3rem' }}>岁 末 族 簿</div>
|
||||
<div className="modal-cat">{y}年 全 族 收 支 与 人 丁 情 形</div>
|
||||
<div className="modal-text" style={{ textAlign: 'center', fontSize: '0.98rem', lineHeight: 2.1 }}>
|
||||
<div>灵石盈亏 <b className={nets >= 0 ? 'good' : 'bad'}>{nets >= 0 ? `+${nets}` : nets}</b> 枚</div>
|
||||
<div>人丁 <b className="good">新增 {report.births}</b> · <b className="bad">辞世 {report.deaths}</b></div>
|
||||
<div>家族声望 <b>{report.rep}</b> · 宗族战力 <b>{report.power}</b></div>
|
||||
{report.deaths > report.births && <div className="bad">丁口在减 —— 传宗接代是头等大事</div>}
|
||||
{report.births > report.deaths && <div className="good">丁口渐旺,香火有望。</div>}
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<button className="btn btn-primary" onClick={closePaper}>
|
||||
翻 过 此 页
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -21,6 +21,7 @@ export default function ChroniclePanel() {
|
||||
const revision = useGameStore((s) => s.revision)
|
||||
void revision
|
||||
const [filter, setFilter] = useState<string>('all')
|
||||
const [battleId, setBattleId] = useState<string | null>(null)
|
||||
const entries = useMemo(() => {
|
||||
if (!world) return []
|
||||
const all = [...world.state.chronicle]
|
||||
@@ -28,6 +29,8 @@ export default function ChroniclePanel() {
|
||||
return filtered.slice().sort((a, b) => (b.year - a.year) || (b.month - a.month))
|
||||
}, [world, filter])
|
||||
if (!world) return null
|
||||
const battles = world.state.battles.slice().reverse()
|
||||
const battle = battles.find((b) => b.id === battleId)
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -54,6 +57,49 @@ export default function ChroniclePanel() {
|
||||
))}
|
||||
{entries.length === 0 && <div className="dim2">暂无记载。</div>}
|
||||
</div>
|
||||
|
||||
<div className="card-title" style={{ fontSize: '1.02rem', marginTop: 22 }}>战报匣(全文留存)</div>
|
||||
{battle ? (
|
||||
<div className="mission-card" style={{ maxWidth: 720 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<b>{battle.title}</b>
|
||||
<span className="dim2" style={{ fontSize: '0.85rem' }}>
|
||||
{battle.year}年{battle.month}月 ·{' '}
|
||||
<span className={battle.winner === 'player' ? 'good' : battle.winner === 'enemy' ? 'bad' : 'warn'}>
|
||||
{battle.winner === 'player' ? '我方得胜' : battle.winner === 'enemy' ? '我方败北' : '平分秋色'}
|
||||
</span>
|
||||
</span>
|
||||
<button className="btn btn-sm" style={{ marginLeft: 'auto' }} onClick={() => setBattleId(null)}>
|
||||
收起
|
||||
</button>
|
||||
</div>
|
||||
<div className="dim" style={{ fontSize: '0.92rem', lineHeight: 1.8, marginTop: 8 }}>
|
||||
{battle.lines.slice(1).map((l, i) => (
|
||||
<div key={i}>· {l}</div>
|
||||
))}
|
||||
{battle.losses.length > 0 && (
|
||||
<div className="bad">我方折损:{battle.losses.join('、')}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : battles.length === 0 ? (
|
||||
<div className="dim2" style={{ marginBottom: 6 }}>尚无战事可录。</div>
|
||||
) : (
|
||||
<div style={{ maxWidth: 720 }}>
|
||||
{battles.map((b) => (
|
||||
<div key={b.id} className="set-row" style={{ maxWidth: 720 }}>
|
||||
<span>
|
||||
<span className="ch-year" style={{ marginRight: 10 }}>{b.year}年{b.month}月</span>
|
||||
{b.title}
|
||||
<span className={`tag ${b.winner === 'player' ? 'good' : b.winner === 'enemy' ? 'bad' : 'warn'}`} style={{ marginLeft: 8 }}>
|
||||
{b.winner === 'player' ? '胜' : b.winner === 'enemy' ? '负' : '平'}
|
||||
</span>
|
||||
</span>
|
||||
<button className="btn btn-sm" onClick={() => setBattleId(b.id)}>阅</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -60,7 +60,13 @@ export default function DiplomacyPanel() {
|
||||
>
|
||||
{married ? '已联姻' : '联姻'}
|
||||
</button>
|
||||
<button className="btn btn-sm" onClick={() => { (npc.relation = Math.max(-100, npc.relation - 20)); bump() }}>
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
onClick={() => {
|
||||
w.tauntNpc(npc.id)
|
||||
bump()
|
||||
}}
|
||||
>
|
||||
寻衅
|
||||
</button>
|
||||
{npc.relation < -30 && (
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Character } from '../../game/types/domain'
|
||||
export default function FamilyPanel() {
|
||||
const world = useGameStore((s) => s.world)
|
||||
const selectedMemberId = useGameStore((s) => s.selectedMemberId)
|
||||
const setSelected = useGameStore((s) => s.setSelected)
|
||||
const revision = useGameStore((s) => s.revision)
|
||||
void revision
|
||||
if (!world) return null
|
||||
@@ -19,6 +20,17 @@ export default function FamilyPanel() {
|
||||
})
|
||||
const selected = selectedMemberId ? s.members[selectedMemberId] as Character | undefined : undefined
|
||||
|
||||
const marriageReady = (() => {
|
||||
if (!world) return []
|
||||
return alive.filter(
|
||||
(c) =>
|
||||
world.ageOf(c) >= 16 &&
|
||||
world.ageOf(c) <= 46 &&
|
||||
c.state !== 'expedition' &&
|
||||
(!c.spouseId || world.isWidowed(c))
|
||||
)
|
||||
})()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="card" style={{ marginBottom: 12, display: 'flex', gap: 20, alignItems: 'center' }}>
|
||||
@@ -32,6 +44,15 @@ export default function FamilyPanel() {
|
||||
<span className="dim">当前代数 <b className="gold">{s.family.generation}</b></span>
|
||||
</div>
|
||||
</div>
|
||||
{marriageReady.length >= 2 && (
|
||||
<div
|
||||
className="matchmaker-strip"
|
||||
onClick={() => setSelected(marriageReady[0].id)}
|
||||
title="族中有适婚未配者,点开详情可指婚"
|
||||
>
|
||||
媒上花开:族中尚有 <b>{marriageReady.length}</b> 位适婚未配,<span className="underline">可置办婚事</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="member-grid">
|
||||
{sorted.map((c) => (
|
||||
<MemberCard key={c.id} c={c} />
|
||||
|
||||
@@ -8,6 +8,9 @@ export default function SettingsPanel() {
|
||||
const exportSave = useGameStore((s) => s.exportSave)
|
||||
const importSave = useGameStore((s) => s.importSave)
|
||||
const refreshSlots = useGameStore((s) => s.refreshSlots)
|
||||
const refreshSnapshots = useGameStore((s) => s.refreshSnapshots)
|
||||
const restoreSnapshot = useGameStore((s) => s.restoreSnapshot)
|
||||
const snapshots = useGameStore((s) => s.snapshots)
|
||||
const go = useGameStore((s) => s.go)
|
||||
const setSpeed = useGameStore((s) => s.setSpeed)
|
||||
if (!world) return null
|
||||
@@ -19,6 +22,35 @@ export default function SettingsPanel() {
|
||||
<span>手动存点(当前第 {slot} 档)</span>
|
||||
<button className="btn btn-sm" onClick={() => void saveNow()}>立即保存</button>
|
||||
</div>
|
||||
<div className="set-row">
|
||||
<span>回档时间轴(每季自动快照,最近12份)</span>
|
||||
<button className="btn btn-sm" onClick={() => void refreshSnapshots()}>刷新列表</button>
|
||||
</div>
|
||||
{snapshots.length > 0 ? (
|
||||
<div style={{ maxWidth: 660, margin: '6px 0 10px' }}>
|
||||
{snapshots.slice(0, 12).map((sn, i) => (
|
||||
<div key={sn.id} className="set-row" style={{ padding: '6px 4px' }}>
|
||||
<span className={sn.year === world.state.year ? 'gold' : ''}>
|
||||
{sn.year}年{sn.month}月{sn.label !== '自动' ? ` · ${sn.label}` : ''}
|
||||
{sn.year === world.state.year && sn.month === world.state.month ? '(当前)' : ''}
|
||||
</span>
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
disabled={sn.year === world.state.year && sn.month === world.state.month}
|
||||
onClick={() => {
|
||||
if (window.confirm(`回卷到 ${sn.year}年${sn.month}月?当前进度会先保存为回档前夕。此操作不可撤销。`)) {
|
||||
void restoreSnapshot(sn.id).then(() => bump())
|
||||
}
|
||||
}}
|
||||
>
|
||||
回卷
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="dim2" style={{ marginBottom: 8 }}>尚未生成快照(推进时间或保存后自动出现)</div>
|
||||
)}
|
||||
<div className="set-row">
|
||||
<span>导出存档(JSON 文件)</span>
|
||||
<button className="btn btn-sm" onClick={() => void exportSave()}>导出</button>
|
||||
|
||||
@@ -68,6 +68,19 @@ export default function TerritoryPanel() {
|
||||
) : (
|
||||
<span className="gold">已臻极致</span>
|
||||
)}
|
||||
{id === 'zongci' && (
|
||||
<button
|
||||
className="btn btn-sm btn-primary"
|
||||
disabled={world.state.year - ((fam.flag['lastRiteYear'] as number) ?? -9) < 2 || fam.stones < 150}
|
||||
title="耗灵石150 · 每两年一回 · 声望+6 · 全族修为气血微升"
|
||||
onClick={() => {
|
||||
world.ancestralRite()
|
||||
bump()
|
||||
}}
|
||||
>
|
||||
祭祖{world.state.year - ((fam.flag['lastRiteYear'] as number) ?? -9) < 2 ? '(冷却)' : '150灵石'}
|
||||
</button>
|
||||
)}
|
||||
<span className="dim2" style={{ marginLeft: 'auto' }}>{levelLabel(level)}</span>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -2,7 +2,7 @@ import { create } from 'zustand'
|
||||
import { World, WorldEventBus } from '../game/engine/world'
|
||||
import { EventDef } from '../game/data/events'
|
||||
import { findEvent, applyEventChoice } from '../game/engine/systems/events'
|
||||
import { BattleLog, ChronicleEntry, GameState, LogItem, SaveMeta } from '../game/types/domain'
|
||||
import { BattleLog, ChronicleEntry, GameState, LogItem, SaveMeta, YearlyReport, SnapshotMeta } from '../game/types/domain'
|
||||
import { getSlotManager, getSaveSlot } from '../game/storage/db'
|
||||
import { metaFromState, updateSlotMeta } from './storeHelper'
|
||||
|
||||
@@ -26,6 +26,8 @@ export interface GameStore {
|
||||
revision: number
|
||||
toast?: string
|
||||
gameOverReason?: string
|
||||
paperReport?: YearlyReport
|
||||
snapshots: SnapshotMeta[]
|
||||
|
||||
init: () => Promise<void>
|
||||
go: (screen: Screen) => void
|
||||
@@ -48,6 +50,10 @@ export interface GameStore {
|
||||
importSave: (slot: number) => Promise<void>
|
||||
deleteSave: (slot: number) => Promise<void>
|
||||
refreshSlots: () => Promise<void>
|
||||
restoreSnapshot: (snapshotId: string) => Promise<void>
|
||||
refreshSnapshots: () => Promise<void>
|
||||
onYearPaper: (report: YearlyReport) => void
|
||||
closePaper: () => void
|
||||
}
|
||||
|
||||
const LOG_CAP = 260
|
||||
@@ -70,6 +76,8 @@ export const useGameStore = create<GameStore>((set, get) => ({
|
||||
revision: 0,
|
||||
toast: undefined,
|
||||
gameOverReason: undefined,
|
||||
paperReport: undefined,
|
||||
snapshots: [],
|
||||
|
||||
init: async () => {
|
||||
const manager = getSlotManager()
|
||||
@@ -166,6 +174,32 @@ export const useGameStore = create<GameStore>((set, get) => ({
|
||||
|
||||
onGameOver: (reason) => set({ gameOverReason: reason, speed: 0, revision: get().revision + 1 }),
|
||||
|
||||
onYearPaper: (report) => {
|
||||
if (report.year >= 2) {
|
||||
set({ paperReport: report, speed: 0 })
|
||||
}
|
||||
},
|
||||
|
||||
closePaper: () => set({ paperReport: undefined }),
|
||||
|
||||
restoreSnapshot: async (snapshotId: string) => {
|
||||
const st = get()
|
||||
const file = await getSaveSlot(st.slot)
|
||||
await file.open()
|
||||
await st.saveNow('回档前')
|
||||
const state = await file.loadState(snapshotId)
|
||||
if (!state) throw new Error('找不到快照')
|
||||
openState(state, st.slot)
|
||||
},
|
||||
|
||||
refreshSnapshots: async () => {
|
||||
const st = get()
|
||||
const file = await getSaveSlot(st.slot)
|
||||
await file.open()
|
||||
const snapshots = await file.listSnapshots()
|
||||
set({ snapshots })
|
||||
},
|
||||
|
||||
saveNow: async (label = '手动') => {
|
||||
await Stash.save(get(), label)
|
||||
},
|
||||
@@ -239,6 +273,7 @@ export const useGameStore = create<GameStore>((set, get) => ({
|
||||
const manager = getSlotManager()
|
||||
const slots = await manager.listSlotMetas()
|
||||
set({ slots })
|
||||
void get().refreshSnapshots()
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -273,7 +308,8 @@ function makeBus(st: GameStore): WorldEventBus {
|
||||
},
|
||||
onBattle: (log) => st.onBattle(log),
|
||||
onPendingEvent: (id) => st.onPendingEvent(id),
|
||||
onGameOver: (reason) => st.onGameOver(reason)
|
||||
onGameOver: (reason) => st.onGameOver(reason),
|
||||
onYearPaper: (entry) => st.onYearPaper(entry)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1229,6 +1229,42 @@ body {
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
/* ---------- 媒人提示 / 年度族簿 ---------- */
|
||||
.matchmaker-strip {
|
||||
background: linear-gradient(180deg, rgba(240, 214, 160, 0.12), rgba(240, 214, 160, 0.05));
|
||||
border: 1px solid rgba(216, 178, 116, 0.35);
|
||||
border-left: 3px solid var(--gold);
|
||||
color: #d8c08a;
|
||||
padding: 9px 14px;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
font-size: 0.92rem;
|
||||
letter-spacing: 1px;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
.matchmaker-strip:hover {
|
||||
border-color: var(--gold-soft);
|
||||
border-left-color: var(--gold);
|
||||
}
|
||||
.underline {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
color: var(--gold-soft);
|
||||
}
|
||||
.yearpaper-modal {
|
||||
width: 520px;
|
||||
}
|
||||
.yearpaper-modal .modal-text {
|
||||
border-top: 1px dashed rgba(120, 98, 55, 0.4);
|
||||
border-bottom: 1px dashed rgba(120, 98, 55, 0.4);
|
||||
}
|
||||
.modal-squad .squad-picker {
|
||||
background: rgba(180, 150, 90, 0.08);
|
||||
border-radius: 3px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
设置 & 杂项
|
||||
============================================================ */
|
||||
|
||||
Reference in New Issue
Block a user