v0.1.1: 养成闭环 + 管理自主 + 存档安全
- 指婚/续弦:成员详情可主动找人结亲(血亲自动排除、鳏寡可再醮) - 装备 UI:法宝从府库装备/替换,战力实时反馈 - 御敌点将:劫掠事件可自选迎战阵容(默认 Top4 可改) - 宗祠祭祖:每两年一次,灵石150 → 声望+6/全族修为小升 - 回档时间轴:每季快照列表,一键回卷(回卷前自动保当前档) - 战报匣子:史书页全文战报留存可翻阅 - 年度族簿纸笺:每年初弹收支/人丁/战力简报 - 媒人提示条 + 寻衅一年冷却 + 出生率与劫掠频率微调 - 单测 19→29(storage mock roundtrip/回档/指婚血亲/祭祖/寻衅/装备/账本)
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { SaveSlot, SaveDbDriver } from '../src/renderer/game/storage/slots'
|
||||
import { GameState } from '../src/renderer/game/types/domain'
|
||||
import { World } from '../src/renderer/game/engine/world'
|
||||
|
||||
interface Row {
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
class MemoryDriver implements SaveDbDriver {
|
||||
tables = new Map<string, Map<string, Row>>()
|
||||
|
||||
async open(_name: string): Promise<void> {}
|
||||
|
||||
async close(): Promise<void> {}
|
||||
|
||||
async run(sql: string, params: unknown[] = []): Promise<unknown> {
|
||||
const m = /insert into (\w+)\s*\(([^)]+)\)\s*values\s*\(([^)]+)\)/i.exec(sql)
|
||||
if (m) {
|
||||
const table = m[1]
|
||||
const cols = m[2].split(',').map((c) => c.trim())
|
||||
const row: Row = {}
|
||||
cols.forEach((c, i) => {
|
||||
row[c] = params[i]
|
||||
})
|
||||
const t = this.tables.get(table) ?? new Map<string, Row>()
|
||||
t.set(String(row['id'] ?? table + t.size), row)
|
||||
this.tables.set(table, t)
|
||||
}
|
||||
if (/update\s+(\w+)\s+set/i.test(sql)) {
|
||||
const table = /update\s+(\w+)/i.exec(sql)![1]
|
||||
const setCol = /set\s+(\w+)\s*=\s*\?/i.exec(sql)![1]
|
||||
const setVal = params[0]
|
||||
const whereLiteral = /\bwhere\s+(\w+)\s*=\s*'([^']+)'/i.exec(sql)
|
||||
const wherePlaceholder = /where\s+(\w+)\s*=\s*\?/i.exec(sql)
|
||||
const whereCol = (whereLiteral?.[1] ?? wherePlaceholder?.[1]) as string | undefined
|
||||
const whereVal =
|
||||
(whereLiteral?.[2] as string | undefined) ?? (wherePlaceholder ? String(params[1]) : undefined)
|
||||
if (whereCol && whereVal !== undefined) {
|
||||
const t = this.tables.get(table)
|
||||
if (t) {
|
||||
for (const [k, row] of t.entries()) {
|
||||
if (String(row[whereCol]) === whereVal) {
|
||||
t.set(k, { ...row, [setCol]: setVal })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const del = /delete from (\w+)\s+where\s+(\w+)\s*=\s*\?/i.exec(sql)
|
||||
if (del) {
|
||||
const table = del[1]
|
||||
const col = del[2]
|
||||
const val = String(params[0])
|
||||
const t = this.tables.get(table)
|
||||
if (t) {
|
||||
for (const [k, row] of t.entries()) {
|
||||
if (String(row[col]) === val) t.delete(k)
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async all<T>(sql: string, params: unknown[] = []): Promise<T[]> {
|
||||
const lower = sql.toLowerCase()
|
||||
const isSnapshot = /from snapshot/.test(lower)
|
||||
const isMeta = /from meta/.test(lower)
|
||||
const isChronicle = /from chronicle/.test(lower)
|
||||
|
||||
const table = isSnapshot ? 'snapshot' : isMeta ? 'meta' : isChronicle ? 'chronicle' : null
|
||||
if (!table) return []
|
||||
let rows = [...(this.tables.get(table)?.values() ?? [])].map((r) => ({ ...r }))
|
||||
|
||||
if (/\bwhere\b/.test(lower)) {
|
||||
const m = /where\s+(\w+)\s*=\s*\?/i.exec(lower)
|
||||
if (m) {
|
||||
const col = m[1]
|
||||
const val = String(params[0])
|
||||
rows = rows.filter((r) => String(r[col]) === val)
|
||||
}
|
||||
}
|
||||
|
||||
if (isSnapshot && /\border by/i.test(lower)) {
|
||||
rows = rows.sort((a, b) => {
|
||||
const y = Number(b['year']) - Number(a['year'])
|
||||
if (y !== 0) return y
|
||||
return Number(b['month']) - Number(a['month'])
|
||||
})
|
||||
}
|
||||
|
||||
if (isSnapshot) {
|
||||
const wantData = /select data/.test(lower)
|
||||
rows = rows.map((r) => (wantData ? { data: String(r['data']) } : r))
|
||||
} else if (isMeta) {
|
||||
const wantValue = /select value/.test(lower)
|
||||
rows = rows.map((r) => (wantValue ? { value: String(r['value']) } : r))
|
||||
}
|
||||
return rows as T[]
|
||||
}
|
||||
|
||||
async exec(sql: string): Promise<unknown> {
|
||||
const create = /create table (?:if not exists )?(\w+)/i.exec(sql)
|
||||
if (create && !this.tables.has(create[1])) this.tables.set(create[1], new Map())
|
||||
const del = /delete from (\w+)/i.exec(sql)
|
||||
if (del) this.tables.set(del[1], new Map())
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
describe('SaveSlot storage roundtrip', () => {
|
||||
it('saves & loads latest state faithfully', async () => {
|
||||
const slot = new SaveSlot(1, new MemoryDriver())
|
||||
const w = World.create({ seed: 's1', surname: '陈', familyName: '陈家', motto: 'm', difficulty: 'normal' })
|
||||
const id = await slot.saveState(w.state, '自动')
|
||||
const loaded = await slot.loadState()
|
||||
expect(loaded).not.toBeNull()
|
||||
expect(loaded!.family.surname).toBe('陈')
|
||||
expect(loaded!.members['x1'].name).toBe(w.state.members['x1'].name)
|
||||
expect(id).toBeTruthy()
|
||||
})
|
||||
|
||||
it('lists snapshots newest first', async () => {
|
||||
const slot = new SaveSlot(2, new MemoryDriver())
|
||||
const w = World.create({ seed: 's2', surname: '周', familyName: '周家', motto: 'm', difficulty: 'normal' })
|
||||
w.state.year = 1
|
||||
w.state.month = 1
|
||||
await slot.saveState(w.state, 'a')
|
||||
const s2 = JSON.parse(JSON.stringify(w.state)) as GameState
|
||||
s2.year = 2
|
||||
s2.month = 3
|
||||
await slot.saveState(s2, 'b')
|
||||
const snaps = await slot.listSnapshots()
|
||||
expect(snaps.length).toBe(2)
|
||||
expect(snaps[0].year).toBe(2)
|
||||
expect(snaps[1].year).toBe(1)
|
||||
})
|
||||
|
||||
it('loads by id and rounds meta', async () => {
|
||||
const slot = new SaveSlot(3, new MemoryDriver())
|
||||
const w = World.create({ seed: 's3', surname: '郑', familyName: '郑家', motto: 'm', difficulty: 'normal' })
|
||||
const id = await slot.saveState(w.state, 'm')
|
||||
const byId = await slot.loadState(id)
|
||||
expect(byId?.family.name).toBe('郑家')
|
||||
await slot.setMeta({ slot: 3, surname: '郑', name: '郑家', estate: w.state.family.estate, year: 1, month: 1, generation: 1, members: 5, reputation: 5, updatedAt: 'now', version: 1 })
|
||||
const meta = await slot.getMeta()
|
||||
expect(meta?.name).toBe('郑家')
|
||||
expect(meta?.members).toBe(5)
|
||||
})
|
||||
|
||||
it('keeps snapshot tail trimmed', async () => {
|
||||
const slot = new SaveSlot(5, new MemoryDriver())
|
||||
const w = World.create({ seed: 's5', surname: '钱', familyName: '钱家', motto: 'm', difficulty: 'normal' })
|
||||
for (let i = 0; i < 15; i++) {
|
||||
w.state.year = i + 1
|
||||
await slot.saveState(w.state, 's' + i)
|
||||
}
|
||||
const snaps = await slot.listSnapshots()
|
||||
expect(snaps.length).toBe(12)
|
||||
expect(snaps[0].year).toBe(15)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user