v0.1.25: 乾坤一统——MOD 系统首版(1040 全绿,金钟罩不漂)
【P0 修复(0.1.24 遗留)】worldGen.npcs defs 落档 + normalize 幂等重注册——读档后
动态家族(n-g-*)npcById 永命中;MOD 卸载后存档世界仍完整(存档世界永稳)
【MOD 系统四层】
- M2 内容层:modSchema.ts(ModPack 全 JSON:manifest+data{events/npcs/worldgen/pills/forges};
modParse 纯解析校验 id/name/version/事件模板合法性)
- M3 管理器:modToPlugin(MOD→CotycPlugin 一等子类——插件持久化/双闸/启停/回滚全复用)
- M1 传输层:main IPC mods:scan/read(userData/mods/*.json|*.cotymod)+ preload + env.d.ts
+ store refreshMods/installModByName/installModText
- M4 UI:SettingsPanel「MOD 层」卡(扫描目录/候选列表/安装);内置示例包通路
【世界/插件协同】
- WorldGenPools 聚合词库池(默认不变→基准世界不变;reset() 供隔离);MOD 于新档参与生成
- items.ts 配方聚合注册表(PILL/FORGE base+扩展);PluginContext addPillRecipe/addForgeRecipe/addWorldGenPool
【边界(明注)】灾因/世界数值表注入归平衡版;脚本型 MOD 不做;MOD 只影响新档世界(老档不变形)
【测试】1040 全绿(46 套件):worldGen 存档 1 例 + mod-system 5 例(解析/转换/模板/依赖/基准指纹);
金钟罩 0.1.24 基线未漂(世界生成默认池不变);build 通过
This commit is contained in:
@@ -37,6 +37,11 @@ export interface PluginContext {
|
||||
afterPhase: (phase: Parameters<GameClock['register']>[0], fn: SystemHook) => () => void
|
||||
/** 世界生成钩子:插件可注入 NPC 模板(worldgen 词库插件化) */
|
||||
addNpcTemplate: (def: import('../../data/npcs').NpcFamilyDef) => void
|
||||
/** 配方注册(MOD/内容包扩展丹药/铸器表) */
|
||||
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
|
||||
addCapability: (cap: { id: string; name: string; version: string; desc: string }) => void
|
||||
removeCapability: (id: string) => void
|
||||
enableCapability: (id: string, enabled: boolean) => 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.24',
|
||||
version: '0.1.25',
|
||||
modules: this.world.systemList().length,
|
||||
systems: this.world.systemList().filter((s) => s.enabled).length,
|
||||
plugins: this.world.pluginList().length,
|
||||
|
||||
@@ -16,7 +16,9 @@ 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 } from '../../data/npcs'
|
||||
import { registerNpcDef, NpcFamilyDef } from '../../data/npcs'
|
||||
import { addPillRecipe, addForgeRecipe } from '../../data/items'
|
||||
import { WorldGenPools } from '../sim/worldgen'
|
||||
import { fire } from './Systems/events'
|
||||
import { createWorldState, findInheritor } from './creation'
|
||||
import { SYSTEM_DEFS, SystemDef } from './capabilities'
|
||||
@@ -64,6 +66,12 @@ export function worldSimOf(w: World): WorldSim {
|
||||
|
||||
export function normalizeGameState(state: GameState): GameState {
|
||||
// 0.1.24 世界种子:旧档缺 worldGen → 按 seed 重放(同一世界)并注册动态 def
|
||||
// P0:世界实例 NPC 定义重注册(幂等——存档世界永稳,MOD 卸载亦完整)
|
||||
if (state.worldGen?.npcs) {
|
||||
for (const n of state.worldGen.npcs) {
|
||||
registerNpcDef(n as never)
|
||||
}
|
||||
}
|
||||
if (!state.worldGen && state.seed) {
|
||||
const wg = generateWorld(state.seed)
|
||||
state.worldGen = {
|
||||
@@ -195,6 +203,9 @@ export class World {
|
||||
self.state.worldGen.relations[def.id] = {}
|
||||
}
|
||||
},
|
||||
addPillRecipe: (r) => addPillRecipe(r),
|
||||
addForgeRecipe: (r) => addForgeRecipe(r),
|
||||
addWorldGenPool: (part) => WorldGenPools.add(part),
|
||||
addCapability: (cap) => {
|
||||
// 0.1.23:能力卡注册入 World 实例(防跨档全局泄漏);UI 全局清单读 SYSTEM_DEFS 展示不受影响
|
||||
if (!self.systems[cap.id]) self.systems[cap.id] = { enabled: true }
|
||||
|
||||
@@ -32,7 +32,12 @@ export function createWorldState(opts: NewGameOptions): GameState {
|
||||
regionFlavor: worldGen.regionFlavor,
|
||||
alliances: worldGen.alliances,
|
||||
feuds: worldGen.feuds,
|
||||
npcCount: worldGen.npcs.length
|
||||
npcCount: worldGen.npcs.length,
|
||||
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,
|
||||
powerGrowth: n.powerGrowth, sells: n.sells, buys: n.buys
|
||||
}))
|
||||
},
|
||||
rng: rng.getState(),
|
||||
year: 1,
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* MOD 管理器:ModPack → CotycPlugin 构造(MOD=插件一等子类)。
|
||||
* 全量复用 PluginManager 管线:持久化/启停/双闸/自动回滚。
|
||||
*/
|
||||
import { CotycPlugin } from '../kernel/plugin'
|
||||
import { ModPack } from './modSchema'
|
||||
|
||||
export function modToPlugin(pack: ModPack): CotycPlugin {
|
||||
return {
|
||||
id: `mod-${pack.id}`,
|
||||
name: `MOD·${pack.name}`,
|
||||
version: pack.version,
|
||||
author: pack.author ?? 'mod',
|
||||
description: pack.description ?? '',
|
||||
kind: 'content',
|
||||
dependencies: (pack.depends ?? []).map((d) => `mod-${d}`),
|
||||
conflicts: (pack.conflicts ?? []).map((c) => `mod-${c}`),
|
||||
install(ctx) {
|
||||
// 事件池(与核心事件同池聚合;findEvent 查池)
|
||||
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)
|
||||
// 配方补充
|
||||
for (const r of pack.data.pills ?? []) ctx.addPillRecipe(r)
|
||||
for (const r of pack.data.forges ?? []) ctx.addForgeRecipe(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* MOD 包 schema(0.1.25《乾坤一统》)
|
||||
* MOD = 可分发的外部内容包(JSON 单文件/文件)——Manifest + 纯数据。
|
||||
* MOD 经 ModManager 转换为 CotycPlugin 进标准插件管线(持久化/双闸/回滚免费获得)。
|
||||
*/
|
||||
import { EventDef } from '../../data/events'
|
||||
import { NpcFamilyDef } from '../../data/npcs'
|
||||
|
||||
export interface ModPackData {
|
||||
/** 追加事件(需与既有 id 无冲突) */
|
||||
events?: EventDef[]
|
||||
/** 世界模板(worldgen 池候选——新档生成时参与) */
|
||||
npcs?: NpcFamilyDef[]
|
||||
/** 世界生成词库注入 */
|
||||
worldgen?: {
|
||||
styles?: string[]
|
||||
regions?: string[]
|
||||
fams?: string[]
|
||||
suffixes?: string[]
|
||||
}
|
||||
/** 丹方/铸器配方 */
|
||||
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 }>
|
||||
}
|
||||
|
||||
export interface ModPack {
|
||||
id: string
|
||||
name: string
|
||||
version: string
|
||||
gameVersion?: string
|
||||
author?: string
|
||||
description?: string
|
||||
depends?: string[]
|
||||
conflicts?: string[]
|
||||
data: ModPackData
|
||||
}
|
||||
|
||||
export type ModParseResult = { ok: true; pack: ModPack } | { ok: false; error: string }
|
||||
|
||||
const ID_RE = /^[a-z0-9][a-z0-9-_]{1,31}$/i
|
||||
|
||||
/** 纯解析(任何文本 → ModPack 或错误);校验层供 vitest 直测 */
|
||||
export function modParse(text: string): ModParseResult {
|
||||
let raw: unknown
|
||||
try {
|
||||
raw = JSON.parse(text)
|
||||
} catch {
|
||||
return { ok: false, error: 'MOD 文件不是合法 JSON。' }
|
||||
}
|
||||
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。' }
|
||||
if (typeof p.name !== 'string' || !p.name.trim()) return { ok: false, error: '缺失 MOD 名称。' }
|
||||
if (typeof p.version !== 'string' || !p.version.trim()) return { ok: false, error: '缺失 MOD 版本号。' }
|
||||
const data = (p.data ?? {}) as Record<string, unknown>
|
||||
if (!data || typeof data !== 'object') return { ok: false, error: 'MOD data 段缺失。' }
|
||||
if (Array.isArray(data.events)) {
|
||||
for (const e of data.events) {
|
||||
if (!e || typeof e !== 'object') return { ok: false, error: 'MOD 事件表含非法行。' }
|
||||
const ev = e as Record<string, unknown>
|
||||
if (typeof ev.id !== 'string' || typeof ev.name !== 'string' || !Array.isArray(ev.options)) {
|
||||
return { ok: false, error: `MOD 事件 ${String(ev.id ?? '?')} 缺 id/name/options。` }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Array.isArray(data.npcs)) {
|
||||
for (const n of data.npcs) {
|
||||
const nn = n as Record<string, unknown>
|
||||
if (typeof nn.id !== 'string' || typeof nn.name !== 'string' || typeof nn.region !== 'string') {
|
||||
return { ok: false, error: 'MOD 模板表含非法行。' }
|
||||
}
|
||||
}
|
||||
}
|
||||
return { ok: true, pack: raw as ModPack }
|
||||
}
|
||||
@@ -16,11 +16,31 @@ export const LEGACY_TEMPLATES: NpcFamilyDef[] = [
|
||||
{ id: 'n-nulei', name: '怒雷祝氏', region: '东丘雷泽', style: '兵修蛮门', desc: '雷泽蛮族的世仇,性情火爆,最易生衅。', leaderRealm: 'core', initialPower: 300, powerGrowth: [5, 13], sells: [], buys: ['lingkuang', 'beastcore'] }
|
||||
]
|
||||
|
||||
/** 新贵词库(风格/区域/词根——生成非老牌世家模板) */
|
||||
const GEN_STYLES = ['剑修世家', '丹道世家', '商盟世族', '兵修蛮门', '符箓仙门', '灵植谷户', '阵道门阀', '散修聚落']
|
||||
const GEN_REGIONS = ['北岳玄影峰', '西川药谷', '南都连港', '东丘雷泽', '南麓青泽', '西山雾谷', '东溪云汉', '北原古井']
|
||||
const GEN_SUFFIX = ['氏', '氏', '宗', '寨', '门']
|
||||
const GEN_FAM = ['玄', '墨', '楚', '白', '萧', '洛', '燕', '秦', '顾', '周', '华', '苏']
|
||||
/** 新贵词库(默认池——MOD/插件可注入扩展;默认不变 → 基准世界不变) */
|
||||
const DEFAULT_GEN_STYLES = ['剑修世家', '丹道世家', '商盟世族', '兵修蛮门', '符箓仙门', '灵植谷户', '阵道门阀', '散修聚落']
|
||||
const DEFAULT_GEN_REGIONS = ['北岳玄影峰', '西川药谷', '南都连港', '东丘雷泽', '南麓青泽', '西山雾谷', '东溪云汉', '北原古井']
|
||||
const DEFAULT_GEN_SUFFIX = ['氏', '氏', '宗', '寨', '门']
|
||||
const DEFAULT_GEN_FAM = ['玄', '墨', '楚', '白', '萧', '洛', '燕', '秦', '顾', '周', '华', '苏']
|
||||
|
||||
/** 聚合词库池(MOD/插件注入端;读取端 worldgen 消费) */
|
||||
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 {
|
||||
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)
|
||||
},
|
||||
reset(): void {
|
||||
this.styles = [...DEFAULT_GEN_STYLES]
|
||||
this.regions = [...DEFAULT_GEN_REGIONS]
|
||||
this.suffixes = [...DEFAULT_GEN_SUFFIX]
|
||||
this.fams = [...DEFAULT_GEN_FAM]
|
||||
}
|
||||
}
|
||||
|
||||
export interface WorldGenResult {
|
||||
seed: string
|
||||
@@ -52,17 +72,17 @@ export function generateWorld(seed: string): WorldGenResult {
|
||||
const npcs: NpcFamilyDef[] = [...legacy]
|
||||
const frontier = count - legacy.length
|
||||
for (let k = 0; k < frontier; k++) {
|
||||
const region = GEN_REGIONS.filter((r) => !usedRegions.has(r))
|
||||
const regionPick = region.length > 0 ? region[rng.int(0, region.length - 1)]! : GEN_REGIONS[rng.int(0, GEN_REGIONS.length - 1)]!
|
||||
const stylePick = GEN_STYLES[rng.int(0, GEN_STYLES.length - 1)]!
|
||||
const region = WorldGenPools.regions.filter((r) => !usedRegions.has(r))
|
||||
const regionPick = region.length > 0 ? region[rng.int(0, region.length - 1)]! : WorldGenPools.regions[rng.int(0, WorldGenPools.regions.length - 1)]!
|
||||
const stylePick = WorldGenPools.styles[rng.int(0, WorldGenPools.styles.length - 1)]!
|
||||
usedRegions.add(regionPick)
|
||||
// 去重:家名组合回溯(同 seed 确定性;至多 8 次重抽)
|
||||
let fam = GEN_FAM[rng.int(0, GEN_FAM.length - 1)]!
|
||||
let suffix = GEN_SUFFIX[rng.int(0, GEN_SUFFIX.length - 1)]!
|
||||
let fam = WorldGenPools.fams[rng.int(0, WorldGenPools.fams.length - 1)]!
|
||||
let suffix = WorldGenPools.suffixes[rng.int(0, WorldGenPools.suffixes.length - 1)]!
|
||||
let name = `${fam}${suffix}`
|
||||
for (let t = 0; t < 8 && usedNames.has(name); t++) {
|
||||
fam = GEN_FAM[rng.int(0, GEN_FAM.length - 1)]!
|
||||
suffix = GEN_SUFFIX[rng.int(0, GEN_SUFFIX.length - 1)]!
|
||||
fam = WorldGenPools.fams[rng.int(0, WorldGenPools.fams.length - 1)]!
|
||||
suffix = WorldGenPools.suffixes[rng.int(0, WorldGenPools.suffixes.length - 1)]!
|
||||
name = `${fam}${suffix}`
|
||||
}
|
||||
usedNames.add(name)
|
||||
|
||||
Reference in New Issue
Block a user