v0.1.28: 稳石——第三轮全量审计歼灭(2008 全绿 + 指纹补盲)
【P0 批(7 项,实证驱动)】
- P0-1 世界词库池无回滚(真实漂移实证)→ WorldGenPools 来源追踪 + removePool 精确回滚
+ MOD uninstall 自动摘除(池/灾因/配方/数值四类全局注册——P2-3 一并根治)
- P0-2 新贵 def 不落档(僵尸实证:power 恒 120 零演化)→ 生成即写 worldGen.npcs
+ normalize 占位 def 双保险 + 新贵 id 掺 seed 指纹(P1-10 防跨档碰撞)
- P0-3 校验死代码化 → installModText 装前 modPreflight(冲突拒+警告 toast)
- P0-4 MOD 读档静默消失 → state.pluginsMissing 收集提示(与 MOD_GUIDE 承诺对齐)
- P0-5 救济按钮乌龙(哈希序首个+直改 state)→ World.reliefCalamity(景气最低+双检)
- P0-6 天下快讯永久冻结(memo 依赖恒 120 length)→ 去 memo 每次 reverse
- P0-7 MOD 导出单行道 → modParse 识别 {app:'cotymod',bundle} 逐包递归(roundtrip 闭环)
【P1 批】
- P1-8 指纹补盲(worldGen/sagaAnnals/distress/declineYears 入 hash——0.1.27 成果真实锁定)+ 双基线重算
- P1-9 重放同步 worldGen.npcs(自捕获);P1-11 worldNum 字段接线(install/uninstall 复位)
- P1-13 落盘兜底(beforeunload/visibilitychange 突发 save,堵 12-tick 节流窗口)
- P1-14 onGameOver 停 timer + IPC mods 路径白名单(扩展名正则防穿越)
- P1-15 DataPack 摘除 npcs(覆写死路删除,与五口宣言对齐);P1-4 techniques 双源统一(techniquePrice 读包)
【P2 扫尾】tauntCD 清理/sagaAnnals 上限 40/worldGenMods 落空写入/validateMod 深度校验随 P0-3 提交
【测试】69 套件 2008 全绿(audit-0.1.28 八例:池回滚/僵尸/preflight/bundle/freeze/指纹敏感/worldNum/救济);
金钟罩 0.1.28 基线重算(指纹补盲+受控时序变更);build 通过
This commit is contained in:
@@ -82,6 +82,14 @@ export function addPillRecipe(r: PillRecipe): void {
|
||||
export function addForgeRecipe(r: ForgeRecipe): void {
|
||||
if (!EXTENDED_FORGES.find((x) => x.output === r.output)) EXTENDED_FORGES.push(r)
|
||||
}
|
||||
export function removePillRecipe(output: string): void {
|
||||
const idx = EXTENDED_PILLS.findIndex((x) => x.output === output)
|
||||
if (idx >= 0) EXTENDED_PILLS.splice(idx, 1)
|
||||
}
|
||||
export function removeForgeRecipe(output: string): void {
|
||||
const idx = EXTENDED_FORGES.findIndex((x) => x.output === output)
|
||||
if (idx >= 0) EXTENDED_FORGES.splice(idx, 1)
|
||||
}
|
||||
export function pillRecipeByOutput(id: string): PillRecipe | undefined {
|
||||
return PILL_RECIPES.find((r) => r.output === id) ?? EXTENDED_PILLS.find((r) => r.output === id)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ITEMS, ARTIFACT_POWER } from './items'
|
||||
import { TECHNIQUES } from './techniques'
|
||||
import { BUILDINGS } from './buildings'
|
||||
import { MISSIONS, ENEMIES } from './secrets'
|
||||
import { NPCS } from './npcs'
|
||||
// npcs 已从 DataPack 摘除(0.1.28:npcById 走静态+动态双源,包覆写是死路——与 MOD 五口宣言对齐)
|
||||
import { EVENTS } from './events'
|
||||
import { POSTS } from './posts'
|
||||
import { TRAITS } from './traits'
|
||||
@@ -19,7 +19,6 @@ export interface DataPack {
|
||||
buildings: typeof BUILDINGS
|
||||
missions: typeof MISSIONS
|
||||
enemies: typeof ENEMIES
|
||||
npcs: typeof NPCS
|
||||
events: typeof EVENTS
|
||||
posts: typeof POSTS
|
||||
traits: typeof TRAITS
|
||||
@@ -41,7 +40,6 @@ export const DEFAULT_PACK: DataPack = {
|
||||
buildings: BUILDINGS,
|
||||
missions: MISSIONS,
|
||||
enemies: ENEMIES,
|
||||
npcs: NPCS,
|
||||
events: EVENTS,
|
||||
posts: POSTS,
|
||||
traits: TRAITS,
|
||||
@@ -77,7 +75,7 @@ export class DataPackRegistry {
|
||||
}
|
||||
|
||||
fingerprint(): string {
|
||||
const key = ['items', 'techniques', 'buildings', 'missions', 'events', 'npcs'] as const
|
||||
const key = ['items', 'techniques', 'buildings', 'missions', 'events'] as const
|
||||
let h = 7
|
||||
for (const k of key) {
|
||||
const o = this.data[k] as unknown as Record<string, unknown>
|
||||
|
||||
@@ -43,7 +43,7 @@ export interface PluginContext {
|
||||
addPillRecipe: (r: { output: string; name: string; danfangLevel: number; stones: number; lingcao: number; beastcore: number; desc: string }) => void
|
||||
addForgeRecipe: (r: { output: string; name: string; stones: number; lingkuang: number; beastcore: number; desc: string }) => void
|
||||
/** 世界生成词库池注入(styles/regions/fams/suffixes) */
|
||||
addWorldGenPool: (part: { styles?: string[]; regions?: string[]; fams?: string[]; suffixes?: string[] }) => void
|
||||
addWorldGenPool: (part: { styles?: string[]; regions?: string[]; fams?: string[]; suffixes?: string[] }, source?: string) => void
|
||||
/** 灾因注入(0.1.26:进入世界灾签循环;重名拒绝 false) */
|
||||
addCalamity: (name: string, opts: { family?: Partial<Record<string, number>>; effect?: Partial<Record<string, number>> }) => boolean
|
||||
addCapability: (cap: { id: string; name: string; version: string; desc: string }) => void
|
||||
|
||||
@@ -137,7 +137,7 @@ export class GameFacade {
|
||||
about(): { title: string; version: string; modules: number; systems: number; plugins: number; packFingerprint: string } {
|
||||
return {
|
||||
title: '仙途家族志',
|
||||
version: '0.1.27',
|
||||
version: '0.1.28',
|
||||
modules: this.world.systemList().length,
|
||||
systems: this.world.systemList().filter((s) => s.enabled).length,
|
||||
plugins: this.world.pluginList().length,
|
||||
|
||||
@@ -16,7 +16,7 @@ import { aspirationById as aspirationOf } from '../../data/aspirations'
|
||||
import { computeLegacy, resolveLegacy, peakRealmIndex, grandTechniqueCount, LegacyArch } from '../narrative/legacy'
|
||||
import { needsTribulation, tribulationEventId } from './Systems/tribulation'
|
||||
import { generateWorld } from '../sim/worldgen'
|
||||
import { registerNpcDef, NpcFamilyDef } from '../../data/npcs'
|
||||
import { registerNpcDef, npcById, NpcFamilyDef } from '../../data/npcs'
|
||||
import { addPillRecipe, addForgeRecipe } from '../../data/items'
|
||||
import { WorldGenPools } from '../sim/worldgen'
|
||||
import { addCalamity as addCalamityImpl } from '../sim/worldsim-data'
|
||||
@@ -73,6 +73,16 @@ export function normalizeGameState(state: GameState): GameState {
|
||||
registerNpcDef(n as never)
|
||||
}
|
||||
}
|
||||
// P0-2 双保险:npcFamilies 中缺 def 的 id → 占位 def(防极端旧档僵尸)
|
||||
for (const id of Object.keys(state.npcFamilies ?? {})) {
|
||||
if (!npcById(id)) {
|
||||
const n = state.npcFamilies[id]
|
||||
registerNpcDef({
|
||||
id, name: n.name, region: n.region, style: '遗族', desc: '世族遗脉,家道中落而名犹在册。',
|
||||
leaderRealm: 'foundation', initialPower: n.power, powerGrowth: [2, 6], sells: [], buys: ['lingcao']
|
||||
})
|
||||
}
|
||||
}
|
||||
if (!state.worldGen && state.seed) {
|
||||
const wg = generateWorld(state.seed)
|
||||
state.worldGen = {
|
||||
@@ -82,7 +92,13 @@ export function normalizeGameState(state: GameState): GameState {
|
||||
regionFlavor: wg.regionFlavor,
|
||||
alliances: wg.alliances,
|
||||
feuds: wg.feuds,
|
||||
npcCount: wg.npcs.length
|
||||
npcCount: wg.npcs.length,
|
||||
// P1-9 重放同步 npcs 摘要(与 creation 一致——该档自此自捕获,不再每次重放)
|
||||
npcs: wg.npcs.map((n) => ({
|
||||
id: n.id, name: n.name, region: n.region, style: n.style,
|
||||
desc: n.desc, leaderRealm: n.leaderRealm, initialPower: n.initialPower,
|
||||
powerGrowth: n.powerGrowth, sells: n.sells, buys: n.buys
|
||||
}))
|
||||
}
|
||||
}
|
||||
// 老版本存档(<0.1.1)缺少新增字段,加载时补齐,避免运行期 undefined 崩溃
|
||||
@@ -176,13 +192,20 @@ export class World {
|
||||
this.plugins = new PluginManager(this.buildPluginContext())
|
||||
this.installCorePlugins()
|
||||
// 0.1.23 持久化插件重装:读档时按注册表重建非核心插件
|
||||
const missing: string[] = []
|
||||
for (const item of state.plugins ?? []) {
|
||||
const mk = PLUGIN_REGISTRY.get(item.id)
|
||||
if (!mk) continue
|
||||
const plugin = mk()
|
||||
if (plugin.version !== item.version) continue
|
||||
const r = this.installPlugin(plugin)
|
||||
if (!mk || mk().version !== item.version) {
|
||||
missing.push(item.id)
|
||||
continue
|
||||
}
|
||||
const r = this.installPlugin(mk())
|
||||
if (r.ok && !item.enabled) this.setPluginEnabled(item.id, false)
|
||||
else if (!r.ok) missing.push(item.id)
|
||||
}
|
||||
if (missing.length > 0) {
|
||||
// P0-4 读档缺失提示(README/MOD 生态承诺对齐)
|
||||
this.state.pluginsMissing = missing
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,7 +229,7 @@ export class World {
|
||||
},
|
||||
addPillRecipe: (r) => addPillRecipe(r),
|
||||
addForgeRecipe: (r) => addForgeRecipe(r),
|
||||
addWorldGenPool: (part) => WorldGenPools.add(part),
|
||||
addWorldGenPool: (part, source) => WorldGenPools.add(part, source ?? 'ctx'),
|
||||
addCalamity: (name, opts) => addCalamityImpl(name, opts),
|
||||
addCapability: (cap) => {
|
||||
// 0.1.23:能力卡注册入 World 实例(防跨档全局泄漏);UI 全局清单读 SYSTEM_DEFS 展示不受影响
|
||||
@@ -548,6 +571,27 @@ export class World {
|
||||
|
||||
// ==================== player actions ====================
|
||||
|
||||
/** 灾年救济:耗 50 灵石赈济——目标=当前受灾/景气最低之家(直改 state 的 UI 乌龙道修复) */
|
||||
reliefCalamity(cost = 50): boolean {
|
||||
const fam = this.state.family
|
||||
if (fam.stones < cost) return false
|
||||
const ws = this.state.worldSim as { calamity?: string; npcDyn?: Record<string, { prosperity?: number }> } | undefined
|
||||
const npcs = Object.values(this.state.npcFamilies)
|
||||
if (npcs.length === 0) return false
|
||||
// 选目标:灾年时优先受灾(no 直指字段——以景气最低替代),平时景气最低
|
||||
let target = npcs[0]!
|
||||
let best = Infinity
|
||||
for (const n of npcs) {
|
||||
const pr = (ws?.npcDyn?.[n.id]?.prosperity ?? 50)
|
||||
if (pr < best) { best = pr; target = n }
|
||||
}
|
||||
fam.stones -= cost
|
||||
target.relation = Math.min(100, target.relation + 8)
|
||||
this.log('info', `开仓济世(灵石${cost}),${target.name} 感念恩义,关系+8。`)
|
||||
void ws
|
||||
return true
|
||||
}
|
||||
|
||||
/** 盟友求援响应:捐灵石助其重整武备(power+15、关系+8) */
|
||||
assistAlly(npcId: string, cost = 200): boolean {
|
||||
const fam = this.state.family
|
||||
|
||||
@@ -33,6 +33,7 @@ export function createWorldState(opts: NewGameOptions): GameState {
|
||||
alliances: worldGen.alliances,
|
||||
feuds: worldGen.feuds,
|
||||
npcCount: worldGen.npcs.length,
|
||||
worldGenMods: [],
|
||||
npcs: worldGen.npcs.map((n) => ({
|
||||
id: n.id, name: n.name, region: n.region, style: n.style,
|
||||
desc: n.desc, leaderRealm: n.leaderRealm, initialPower: n.initialPower,
|
||||
|
||||
@@ -7,6 +7,9 @@ import { ModPack, validateMod } from './modSchema'
|
||||
import { EventDef } from '../../data/events'
|
||||
import { findEvent } from './Systems/events'
|
||||
import { World } from './World'
|
||||
import { WorldGenPools } from '../sim/worldgen'
|
||||
import { removeCalamityByName, overrideWorldNum, resetWorldNum } from '../sim/worldsim-data'
|
||||
import { removePillRecipe, removeForgeRecipe } from '../../data/items'
|
||||
|
||||
/** 安装前检查:事件 id 与核心/已装池冲突 + 范式校验警告 */
|
||||
const DYNAMIC_PREFIXES = ['ev-raid-', 'ev-trib-', 'ev-tournament-', 'ev-echo-', 'ev-auction', 'ev-centennial', 'ev-feisheng', 'ev-legacypass', 'ev-winterprayer', 'ev-jobai']
|
||||
@@ -37,8 +40,8 @@ export function modToPlugin(pack: ModPack): CotycPlugin {
|
||||
if (pack.data.events?.length) ctx.addEventPool(`mod-${pack.id}`, pack.data.events)
|
||||
// 世界模板(worldgen 池候选——新档参与生成)
|
||||
for (const def of pack.data.npcs ?? []) ctx.addNpcTemplate(def)
|
||||
// 词库注入(styles/regions/fams/suffixes)
|
||||
if (pack.data.worldgen) ctx.addWorldGenPool(pack.data.worldgen)
|
||||
// 词库注入(styles/regions/fams/suffixes;带 source 便于精确回滚)
|
||||
if (pack.data.worldgen) ctx.addWorldGenPool(pack.data.worldgen, `mod-${pack.id}`)
|
||||
// 配方补充
|
||||
for (const r of pack.data.pills ?? []) ctx.addPillRecipe(r)
|
||||
for (const r of pack.data.forges ?? []) ctx.addForgeRecipe(r)
|
||||
@@ -46,6 +49,18 @@ export function modToPlugin(pack: ModPack): CotycPlugin {
|
||||
for (const c of pack.data.calamities ?? []) {
|
||||
ctx.addCalamity(c.name, { family: c.family, effect: c.effect })
|
||||
}
|
||||
// P1-11 worldNum 白名单覆写(0.1.28 接线:仅 WORLDSIM 内键生效,卸载复位)
|
||||
for (const [k, v] of Object.entries(pack.data.worldNum ?? {})) {
|
||||
overrideWorldNum(k, v)
|
||||
}
|
||||
},
|
||||
uninstall(ctx2) {
|
||||
// 0.1.28 精确回滚:词库池/灾因/配方/世界数值按来源摘除(其余 MOD 共享词条保留)
|
||||
WorldGenPools.removePool(`mod-${pack.id}`)
|
||||
for (const c of pack.data.calamities ?? []) removeCalamityByName(c.name)
|
||||
for (const r of pack.data.pills ?? []) removePillRecipe(r.output)
|
||||
for (const r of pack.data.forges ?? []) removeForgeRecipe(r.output)
|
||||
if (pack.data.worldNum) for (const k of Object.keys(pack.data.worldNum)) resetWorldNum(k)
|
||||
},
|
||||
// M1:挂 pack 源——导出/显示用(插件实例可溯源到原始 MOD 包)
|
||||
source: pack
|
||||
|
||||
@@ -20,6 +20,8 @@ export interface ModPackData {
|
||||
}
|
||||
/** 灾因扩展(0.1.26:进入世界灾签循环) */
|
||||
calamities?: Array<{ name: string; family?: Record<string, number>; effect?: Record<string, number> }>
|
||||
/** 世界数值白名单覆写(0.1.27 声明口径,0.1.28 接线;卸载复位) */
|
||||
worldNum?: Record<string, number>
|
||||
/** 丹方/铸器配方 */
|
||||
pills?: Array<{ output: string; name: string; danfangLevel: number; stones: number; lingcao: number; beastcore: number; desc: string }>
|
||||
forges?: Array<{ output: string; name: string; stones: number; lingkuang: number; beastcore: number; desc: string }>
|
||||
@@ -51,6 +53,16 @@ export function modParse(text: string): ModParseResult {
|
||||
} catch {
|
||||
return { ok: false, error: 'MOD 文件不是合法 JSON。' }
|
||||
}
|
||||
// P0-7 bundle 分支:游戏内导出的合并包({app:'cotymod',bundle:[...]})→ 逐包递归(首包解析失败即报)
|
||||
const asRec = raw as { app?: string; bundle?: unknown[] }
|
||||
if (asRec?.app === 'cotymod' && Array.isArray(asRec.bundle)) {
|
||||
if (asRec.bundle.length === 0) return { ok: false, error: 'bundle 为空。' }
|
||||
for (const b of asRec.bundle) {
|
||||
const r = modParse(JSON.stringify(b))
|
||||
if (!r.ok) return { ok: false, error: `bundle 内包非法:${r.error}` }
|
||||
}
|
||||
return modParse(JSON.stringify(asRec.bundle[0]))
|
||||
}
|
||||
if (!raw || typeof raw !== 'object') return { ok: false, error: 'MOD 内容为空。' }
|
||||
const p = raw as Record<string, unknown>
|
||||
if (typeof p.id !== 'string' || !ID_RE.test(p.id)) return { ok: false, error: '缺失/非法的 MOD id。' }
|
||||
|
||||
@@ -54,6 +54,8 @@ export function buyTechnique(w: World, techId: string, price: number): boolean {
|
||||
}
|
||||
|
||||
export function techniquePrice(techId: string): number {
|
||||
const t = pack().techniques.find((x) => x.id === techId)
|
||||
if (t) return TECHNIQUE_GRADE_PRICE[t.grade] ?? 300
|
||||
return TECH_GRADE_BASE[techId] ?? 200
|
||||
}
|
||||
|
||||
|
||||
@@ -153,6 +153,7 @@ export class WorldSim {
|
||||
};当世最强为 ${topNowName}(${topNow})。`
|
||||
if (!s.sagaAnnals) s.sagaAnnals = []
|
||||
s.sagaAnnals.push({ year: y, kind, text })
|
||||
if (s.sagaAnnals.length > 40) s.sagaAnnals.splice(0, s.sagaAnnals.length - 40)
|
||||
this.w.log('info', `【世鉴】${text}`)
|
||||
}
|
||||
|
||||
@@ -595,6 +596,7 @@ function purgeNpc(w: World, s: WorldSimState, id: string): void {
|
||||
}
|
||||
delete w.state.family.flag[`raidCD-${id}`]
|
||||
delete w.state.family.flag[`auction-${id}`]
|
||||
delete w.state.family.flag[`tauntCD-${id}`]
|
||||
w.state.eventQueue = (w.state.eventQueue ?? []).filter((e) => !e.startsWith('ev-raid-') || !e.endsWith(id))
|
||||
if (s.distress?.id === id) s.distress = undefined
|
||||
unregisterNpcDef(id)
|
||||
@@ -609,7 +611,10 @@ function spawnNewbornDynasty(w: World, s: WorldSimState): void {
|
||||
const rng = w.rng
|
||||
const style = rng.pick([...NEWBORN_STYLES])
|
||||
const region = rng.pick([...NEWBORN_REGIONS])
|
||||
const id = `n-new-${rng.int(1, 9999)}`
|
||||
// P1-10 id 掺 seed 指纹(防跨档碰撞)
|
||||
let seedHash = 0
|
||||
for (let i = 0; i < w.state.seed.length; i++) seedHash = (seedHash * 31 + w.state.seed.charCodeAt(i)) >>> 0
|
||||
const id = `n-new-${(seedHash % 4096).toString(36)}-${rng.int(1, 9999)}`
|
||||
const name = `${region.slice(0, 2)}${NEWBORN_NAME_SUFFIX[rng.int(0, NEWBORN_NAME_SUFFIX.length - 1)]}`
|
||||
const def = {
|
||||
id,
|
||||
@@ -624,6 +629,15 @@ function spawnNewbornDynasty(w: World, s: WorldSimState): void {
|
||||
buys: ['lingcao', 'lingkuang']
|
||||
}
|
||||
registerNpcDef(def)
|
||||
// P0-2 生成即落档(worldGen.npcs 追加——读档幂等重注册,防御新贵僵尸)
|
||||
if (w.state.worldGen) {
|
||||
if (!w.state.worldGen.npcs) w.state.worldGen.npcs = []
|
||||
w.state.worldGen.npcs.push({
|
||||
id: def.id, name: def.name, region: def.region, style: def.style,
|
||||
desc: def.desc, leaderRealm: def.leaderRealm, initialPower: def.initialPower,
|
||||
powerGrowth: def.powerGrowth, sells: def.sells, buys: def.buys
|
||||
})
|
||||
}
|
||||
w.state.npcFamilies[id] = {
|
||||
id, name, region,
|
||||
power: def.initialPower,
|
||||
|
||||
@@ -22,23 +22,52 @@ const DEFAULT_GEN_REGIONS = ['北岳玄影峰', '西川药谷', '南都连港',
|
||||
const DEFAULT_GEN_SUFFIX = ['氏', '氏', '宗', '寨', '门']
|
||||
const DEFAULT_GEN_FAM = ['玄', '墨', '楚', '白', '萧', '洛', '燕', '秦', '顾', '周', '华', '苏']
|
||||
|
||||
/** 聚合词库池(MOD/插件注入端;读取端 worldgen 消费) */
|
||||
/** 聚合词库池(MOD/插件注入端;读取端 worldgen 消费)
|
||||
* 0.1.28 追踪回滚:add 记录来源(source key),removePool 精确退——卸 MOD 后同 seed 重放零漂移。 */
|
||||
type PoolPart = { styles?: string[]; regions?: string[]; suffixes?: string[]; fams?: string[] }
|
||||
const TRACKER = new Map<string, PoolPart>()
|
||||
|
||||
export const WorldGenPools = {
|
||||
styles: [...DEFAULT_GEN_STYLES],
|
||||
regions: [...DEFAULT_GEN_REGIONS],
|
||||
suffixes: [...DEFAULT_GEN_SUFFIX],
|
||||
fams: [...DEFAULT_GEN_FAM],
|
||||
add(part: { styles?: string[]; regions?: string[]; suffixes?: string[]; fams?: string[] }): void {
|
||||
add(part: PoolPart, source = 'unnamed'): void {
|
||||
TRACKER.set(source, part)
|
||||
if (part.styles) for (const s of part.styles) if (!this.styles.includes(s)) this.styles.push(s)
|
||||
if (part.regions) for (const r of part.regions) if (!this.regions.includes(r)) this.regions.push(r)
|
||||
if (part.suffixes) for (const s of part.suffixes) if (!this.suffixes.includes(s)) this.suffixes.push(s)
|
||||
if (part.fams) for (const f of part.fams) if (!this.fams.includes(f)) this.fams.push(f)
|
||||
},
|
||||
/** 精确回滚:移除某来源注入的词条(其他来源仍拥有的保留) */
|
||||
removePool(source: string): void {
|
||||
const part = TRACKER.get(source)
|
||||
if (!part) return
|
||||
TRACKER.delete(source)
|
||||
const ownedByOthers = new Set(
|
||||
[...TRACKER.values()].flatMap((p) => [
|
||||
...(p.styles ?? []), ...(p.regions ?? []), ...(p.suffixes ?? []), ...(p.fams ?? [])
|
||||
])
|
||||
)
|
||||
const removeIfUnshared = (arr: string[], items?: string[]) => {
|
||||
if (!items) return
|
||||
for (const s of items) {
|
||||
if (ownedByOthers.has(s)) continue
|
||||
const idx = arr.indexOf(s)
|
||||
if (idx >= 0) arr.splice(idx, 1)
|
||||
}
|
||||
}
|
||||
removeIfUnshared(this.styles, part.styles)
|
||||
removeIfUnshared(this.regions, part.regions)
|
||||
removeIfUnshared(this.suffixes, part.suffixes)
|
||||
removeIfUnshared(this.fams, part.fams)
|
||||
},
|
||||
reset(): void {
|
||||
this.styles = [...DEFAULT_GEN_STYLES]
|
||||
this.regions = [...DEFAULT_GEN_REGIONS]
|
||||
this.suffixes = [...DEFAULT_GEN_SUFFIX]
|
||||
this.fams = [...DEFAULT_GEN_FAM]
|
||||
TRACKER.clear()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -218,6 +218,13 @@ export function addCalamity(
|
||||
return true
|
||||
}
|
||||
|
||||
export function removeCalamityByName(name: string): void {
|
||||
const idx = EXTENDED_CALAMITIES.indexOf(name)
|
||||
if (idx >= 0) EXTENDED_CALAMITIES.splice(idx, 1)
|
||||
delete EXTENDED_FAMILY[name]
|
||||
delete EXTENDED_EFFECT[name]
|
||||
}
|
||||
|
||||
/** 聚合读口(WorldSim 灾签/体感/生产/Market 读取) */
|
||||
export function calamityNames(): string[] {
|
||||
return [...DEFAULT_CALAMITIES, ...EXTENDED_CALAMITIES]
|
||||
|
||||
@@ -187,6 +187,8 @@ export interface GameState {
|
||||
yearlyReports: YearlyReport[]
|
||||
/** 已装插件(id/version/enabled)——存档持久化,加载时按注册表重装 */
|
||||
plugins?: Array<{ id: string; version: string; enabled: boolean }>
|
||||
/** 读档时缺失的插件(工厂未注册/版本不符)——UI 提示:MOD 文件可能需重新放入 mods 目录 */
|
||||
pluginsMissing?: string[]
|
||||
/** 天下轮廓(种子生成器产物:关系网/开局 era/市场偏移/世仇盟约) */
|
||||
worldGen?: {
|
||||
relations: Record<string, Record<string, number>>
|
||||
|
||||
@@ -269,7 +269,7 @@ export default function SettingsPanel() {
|
||||
· 「外交」与四邻结好联姻;仇雠之族隔岁来犯,打得赢名望大涨,打不赢蚀钱伤丁。<br />
|
||||
· 「史书」自动记述繁华与凋零——百年之后,后人翻开这一卷家族志,见代代薪火、历历雪泥。
|
||||
</div>
|
||||
<div className="dim2" style={{ marginTop: 8 }}>版本 0.1.27 · Chronicle of the Immortal Clan</div>
|
||||
<div className="dim2" style={{ marginTop: 8 }}>版本 0.1.28 · Chronicle of the Immortal Clan</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -25,8 +25,8 @@ export default function WorldPanel() {
|
||||
const tide = ws.tide ?? 0.5
|
||||
const tideLabel = tide > 0.9 ? '灵涨' : tide < 0.7 ? '灵衰' : '汐平'
|
||||
const pool = ws.marketPool ?? {}
|
||||
const newsRaw = ws.newsFeed ?? []
|
||||
const news = useMemo(() => [...newsRaw].slice().reverse(), [newsRaw.length])
|
||||
// P0-6 修复:newsFeed 恒定 120 时 length 依赖 memo 永冻结——直接每次 reverse(120 项零成本)
|
||||
const news = [...(ws.newsFeed ?? [])].slice().reverse()
|
||||
const calamity = ws.calamity
|
||||
const calamityLeft = ws.calamityLeft ?? 0
|
||||
const myPower = Object.values(w.state.members)
|
||||
@@ -137,10 +137,7 @@ export default function WorldPanel() {
|
||||
className="btn btn-sm"
|
||||
title="引粥施药,换两家之谊"
|
||||
onClick={() => {
|
||||
const npc = Object.values(w.state.npcFamilies)[0]
|
||||
npc.relation = Math.min(100, npc.relation + 8)
|
||||
w.state.family.stones -= 50
|
||||
w.log('info', `开仓济世(灵石50),${npc.name} 感念恩义,关系+8。`)
|
||||
w.reliefCalamity(50)
|
||||
bump()
|
||||
}}
|
||||
>救济</button>
|
||||
|
||||
@@ -116,9 +116,13 @@ export const useGameStore = create<GameStore>((set, get) => ({
|
||||
const { modParse } = await import('../game/engine/runtime/modSchema')
|
||||
const parsed = modParse(text)
|
||||
if (!parsed.ok) { setNextToast(`MOD 解析失败:${parsed.error}`); return }
|
||||
const { modToPlugin } = await import('../game/engine/runtime/modManager')
|
||||
const { modToPlugin, modPreflight } = await import('../game/engine/runtime/modManager')
|
||||
const st = useGameStore.getState()
|
||||
if (!st.world) { setNextToast('请先进入一局再装 MOD。'); return }
|
||||
// P0-3 生产链路接入校验:id 冲突即拒(防撞核心事件池);warnings 走 toast
|
||||
const pf = modPreflight(st.world, parsed.pack)
|
||||
if (!pf.ok) { setNextToast(`MOD 安装被拒绝:${pf.reason ?? '校验未过'}`); return }
|
||||
if (pf.warnings.length > 0) setNextToast(`MOD 安装提示:${pf.warnings.slice(0, 2).join(';')}`)
|
||||
const res = st.world.installPlugin(modToPlugin(parsed.pack))
|
||||
if (res.ok) {
|
||||
setNextToast(`MOD「${parsed.pack.name}」已装——新开档的世界将受其影响。`)
|
||||
@@ -148,6 +152,18 @@ export const useGameStore = create<GameStore>((set, get) => ({
|
||||
soundOn: true,
|
||||
|
||||
init: async () => {
|
||||
// P1-13 落盘兜底:页面隐藏/关闭时瞬时保存(防节流窗口(≤12tick)丢档)
|
||||
if (typeof window !== 'undefined') {
|
||||
const flush = () => {
|
||||
const st = useGameStore.getState()
|
||||
if (!st.world || st.screen !== 'game') return
|
||||
void Stash.save(st, '隐藏前')
|
||||
}
|
||||
window.addEventListener('beforeunload', flush)
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'hidden') flush()
|
||||
})
|
||||
}
|
||||
const manager = getSlotManager()
|
||||
const slots = await manager.listSlotMetas()
|
||||
set({ slots })
|
||||
@@ -314,7 +330,10 @@ export const useGameStore = create<GameStore>((set, get) => ({
|
||||
set({ pendingEventId: id, pendingEventDef: def })
|
||||
},
|
||||
|
||||
onGameOver: (reason) => set({ gameOverReason: reason, speed: 0, revision: get().revision + 1 }),
|
||||
onGameOver: (reason) => {
|
||||
stopTimer()
|
||||
set({ gameOverReason: reason, revision: get().revision + 1 })
|
||||
},
|
||||
|
||||
onYearPaper: (report) => {
|
||||
if (report.year >= 2) {
|
||||
|
||||
Reference in New Issue
Block a user