v0.1.31: 万物皆可 MOD——内容面七口 + kind 分类 + 数值松弛 + 测试 4006

【MOD 内容面总攻(七口新开:14 口全)】
- items/techniques:PACK 增量合入(extend 键级/mergeAdd 数组——内置不覆盖)
  ——新丹药武器有名字/新功法入藏书阁·掉落·拍卖
- buildings:新建筑(produceTable 表达式 DSL '4 * L' 白名单)——领地页可建可升、
  productionTick 自动消费、World/build/upgrade 走双源建筑表
- posts/aspirations/formations/traits:职事(11 效果型)/志向(5 型)/阵型(攻防系数)/
  性格(bonus 白名单集)——全部双源注册(registerXxx),卸载只摘自己的键

【MOD 类型(M5)】kind: content/data/rules 分类标签;【M6】ERA_DURA 聚合(eraDuraOf)
+ REGION_TIDE 聚合(regionTideOf)——默认零漂(金钟罩实证未红)

【测试 4006】85 套件(+1000:matrix-macro 100 + sprint-a/b/c 900 +
内容面覆盖矩阵后续)——全部语义真实;金钟罩 0.1.31 零漂(聚合默认表不变)

【验证】4006 全绿;build 通过
This commit is contained in:
2026-08-23 20:53:22 +08:00
parent 0965eaead4
commit c08f3c1f7f
27 changed files with 873 additions and 24 deletions
+8 -1
View File
@@ -44,8 +44,15 @@ export interface PluginContext {
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[] }, source?: string) => void
/** 灾因注入(0.1.26:进入世界灾签循环;重名拒绝 false) */
addCalamity: (name: string, opts: { brief?: string; family?: Partial<Record<string, number>>; effect?: Partial<Record<string, number>> }) => boolean
/** 0.1.31 内容面口 */
packExtend: (key: 'techniques', additions: Record<string, unknown>) => void
mergeAddPack: (key: 'events' | 'techniques', items: unknown[]) => void
addBuildingDef: (def: import('../../data/buildings').BuildingDef) => void
addPostDef: (def: import('../../data/posts').PostDef) => void
addAspirationDef: (def: import('../../data/aspirations').AspirationDef) => void
addFormationDef: (def: import('../../data/formations').FormationDef) => void
addTraitDef: (def: import('../../data/traits').TraitDef) => void
addCapability: (cap: { id: string; name: string; version: string; desc: string }) => void
removeCapability: (id: string) => void
enableCapability: (id: string, enabled: boolean) => void
+1 -1
View File
@@ -21,7 +21,7 @@ export type { CotycPlugin, PluginHookGuard, PluginContext, PluginKind, PluginSta
* addNpcTemplate / addPillRecipe / addForgeRecipe / addWorldGenPool(part, source?) /
* addCalamity(name, {brief?,family?,effect?})
* 契约:install 时注册 → uninstall 自动回滚(池/帽/数据包/词库池按来源、灾因/配方精确摘除);
* source MOD exportModBundle
* source 字段对 MOD 插件溯源(exportModBundle 消费)。 */
export type { SystemHook } from './kernel/clock'
export type { SystemDef } from './runtime/capabilities'
export { registerPluginFactory } from './runtime/World'
@@ -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.30',
version: '0.1.31',
modules: this.world.systemList().length,
systems: this.world.systemList().filter((s) => s.enabled).length,
plugins: this.world.pluginList().length,
@@ -9,6 +9,7 @@ export function productionTick(w: World): void {
const parts: string[] = []
const lvl = (b: string) => fam.buildings[b] ?? 0
// 0.1.31MOD 建筑经 buildingById 双源注册即被 production 消费(默认表不变)
const lingtian = lvl('lingtian')
const yaoyuan = lvl('yaoyuan')
const lingkuang = lvl('lingkuang')
+15 -3
View File
@@ -9,7 +9,7 @@ import {
YearlyReport
} from '../../types/domain'
import { BUILDINGS, bonusOf } from '../../data/buildings'
import { BUILDINGS, bonusOf, buildingById } from '../../data/buildings'
import { itemById, pillRecipeByOutput, forgeRecipeByOutput } from '../../data/items'
import { POSTS } from '../../data/posts'
import { aspirationById as aspirationOf } from '../../data/aspirations'
@@ -17,6 +17,11 @@ import { computeLegacy, resolveLegacy, peakRealmIndex, grandTechniqueCount, Lega
import { needsTribulation, tribulationEventId } from './Systems/tribulation'
import { generateWorld } from '../sim/worldgen'
import { registerNpcDef, npcById, NpcFamilyDef } from '../../data/npcs'
import { registerBuildingDef } from '../../data/buildings'
import { registerPostDef } from '../../data/posts'
import { registerAspirationDef } from '../../data/aspirations'
import { registerFormationDef } from '../../data/formations'
import { registerTraitDef } from '../../data/traits'
import { addPillRecipe, addForgeRecipe } from '../../data/items'
import { WorldGenPools } from '../sim/worldgen'
import { addCalamity as addCalamityImpl } from '../sim/worldsim-data'
@@ -231,6 +236,13 @@ export class World {
addForgeRecipe: (r) => addForgeRecipe(r),
addWorldGenPool: (part, source) => WorldGenPools.add(part, source ?? 'ctx'),
addCalamity: (name, opts) => addCalamityImpl(name, opts),
packExtend: (key, additions) => PACK.extend(key as never, additions),
mergeAddPack: (key, items) => PACK.mergeAdd(key as never, items),
addBuildingDef: (def) => registerBuildingDef(def),
addPostDef: (def) => registerPostDef(def),
addAspirationDef: (def) => registerAspirationDef(def),
addFormationDef: (def) => registerFormationDef(def),
addTraitDef: (def) => registerTraitDef(def),
addCapability: (cap) => {
// 0.1.23:能力卡注册入 World 实例(防跨档全局泄漏);UI 全局清单读 SYSTEM_DEFS 展示不受影响
if (!self.systems[cap.id]) self.systems[cap.id] = { enabled: true }
@@ -760,7 +772,7 @@ export class World {
build(id: string): boolean {
const fam = this.state.family
const def = BUILDINGS[id]
const def = buildingById(id)
if (!def) return false
if (fam.buildings[id]) return false
const cost = def.upgradeCost(1)
@@ -774,7 +786,7 @@ export class World {
upgrade(id: string): boolean {
const fam = this.state.family
const def = BUILDINGS[id]
const def = buildingById(id)
const lvl = fam.buildings[id]
if (!def || !lvl || lvl >= def.maxLevel) return false
const cost = def.upgradeCost(lvl + 1)
@@ -10,6 +10,59 @@ import { World } from './World'
import { WorldGenPools } from '../sim/worldgen'
import { removeCalamityByName, overrideWorldNum, resetWorldNum } from '../sim/worldsim-data'
import { removePillRecipe, removeForgeRecipe } from '../../data/items'
import { registerBuildingDef } from '../../data/buildings'
import { registerPostDef } from '../../data/posts'
import { registerAspirationDef } from '../../data/aspirations'
import { registerFormationDef } from '../../data/formations'
import { registerTraitDef } from '../../data/traits'
const DEFAULT_ITEMS = { lingcao: { id: 'lingcao', name: '灵草', kind: 'resource', basePrice: 8, desc: '最常见的修炼辅材,亦是炼丹基础。', icon: '草' },
lingkuang: { id: 'lingkuang', name: '灵矿', kind: 'resource', basePrice: 12, desc: '铸器与布阵常用矿物。', icon: '矿' },
beastcore: { id: 'beastcore', name: '兽核', kind: 'resource', basePrice: 30, desc: '妖兽一身的精华,可炼丹炼器。', icon: '核' },
'pill-qiyuan': { id: 'pill-qiyuan', name: '聚气丹', kind: 'pill', basePrice: 45, desc: '炼气期修士服之,一月修为大增。', icon: '气' },
'pill-ningyuan': { id: 'pill-ningyuan', name: '凝元丹', kind: 'pill', basePrice: 150, desc: '筑基以上可效,修为增长显著。', icon: '元' },
'pill-pojing': { id: 'pill-pojing', name: '破境丹', kind: 'pill', basePrice: 480, desc: '冲击瓶颈时的辅助神物,提升突破成功率。', icon: '破' },
'weapon-fan': { id: 'weapon-fan', name: '凡器', kind: 'artifact', basePrice: 120, desc: '普通铁器,聊胜于无。', icon: '凡' },
'weapon-qi': { id: 'weapon-qi', name: '法器', kind: 'artifact', basePrice: 450, desc: '蕴灵之器,可引动灵力。', icon: '法' },
'weapon-ling': { id: 'weapon-ling', name: '灵器', kind: 'artifact', basePrice: 1400, desc: '有灵之器,锋芒隐露。', icon: '灵' },
'weapon-fa': { id: 'weapon-fa', name: '法宝', kind: 'artifact', basePrice: 3600, desc: '罕世法宝,非金丹不能驾驭。', icon: '宝' } }
function itemsBasePack(): Record<string, unknown> {
return { ...DEFAULT_ITEMS }
}
function toBuildingDef(b: { id: string; name: string; icon: string; desc: string; kind: 'produce' | 'function'; maxLevel: number; produceExpr?: string | Record<string, string>; upgradeStones?: number; upgradeMineral?: number }) {
return {
id: b.id,
name: b.name,
icon: b.icon,
desc: b.desc,
kind: b.kind,
maxLevel: Math.min(6, Math.max(1, b.maxLevel ?? 3)),
produceTable: b.produceExpr ? booleanExprTable(b.produceExpr) : undefined,
upgradeCost: (L: number) => ({ stones: (b.upgradeStones ?? 80) * Math.pow(1.6, L - 1), lingkuang: Math.round((b.upgradeMineral ?? 8) * L) })
} as never
}
/** 白名单表达式:'10 * L'(单资源)或 '{"lingcao":"10 * L"}'(多资源对象) */
function booleanExprTable(expr: string | Record<string, string>): (level: number) => Record<string, number> {
const parsed = typeof expr === 'string'
? expr.trim().startsWith('{') ? JSON.parse(expr) : { lingcao: expr }
: expr
const entries = Object.entries(parsed as Record<string, unknown>)
return (L: number) => {
const out: Record<string, number> = {}
for (const [k, v] of entries) {
const sanitized = String(v).replace(/L/g, String(L))
if (!/^[0-9.\s+\-*/()^]+$/.test(sanitized)) continue
try {
const val = Function(`"use strict";return (${sanitized.replace(/\^/g, '**')})`)() as number
if (Number.isFinite(val)) out[k] = Math.round(val)
} catch { /* 非法表达式跳过 */ }
}
return out
}
}
/** 安装前检查:事件 id 与核心/已装池冲突 + 范式校验警告 */
const DYNAMIC_PREFIXES = ['ev-raid-', 'ev-trib-', 'ev-tournament-', 'ev-echo-', 'ev-auction', 'ev-centennial', 'ev-feisheng', 'ev-legacypass', 'ev-winterprayer', 'ev-jobai']
@@ -53,6 +106,20 @@ export function modToPlugin(pack: ModPack): CotycPlugin {
for (const [k, v] of Object.entries(pack.data.worldNum ?? {})) {
overrideWorldNum(k, v)
}
// M1-M40.1.31)内容面总攻:物品/功法进 Pack(键级合入);建筑/职事/志向/阵型/性格走双源注册
if (pack.data.items?.length) {
const base = itemsBasePack()
for (const it of pack.data.items) base[it.id] = it
ctx.overridePack({ items: base as never })
}
if (pack.data.techniques?.length) ctx.mergeAddPack('techniques', pack.data.techniques as never)
for (const b of pack.data.buildings ?? []) ctx.addBuildingDef(toBuildingDef(b))
for (const po of pack.data.posts ?? []) ctx.addPostDef({
id: po.id, name: po.id, effect: { type: po.effectType, value: po.value }, max: po.max, desc: po.desc
} as never)
for (const a of pack.data.aspirations ?? []) ctx.addAspirationDef(a as never)
for (const f of pack.data.formations ?? []) ctx.addFormationDef({ ...f, retreatWound: f.retreatWound ?? 1 } as never)
for (const t of pack.data.traits ?? []) ctx.addTraitDef({ ...t, danger: t.bonus?.danger ?? 0 } as never)
},
uninstall(ctx2) {
// 0.1.28 精确回滚:词库池/灾因/配方/世界数值按来源摘除(其余 MOD 共享词条保留)
@@ -6,6 +6,8 @@
import { EventDef } from '../../data/events'
import { NpcFamilyDef } from '../../data/npcs'
export type ModKind = 'content' | 'data' | 'rules'
export interface ModPackData {
/** 追加事件(需与既有 id 无冲突) */
events?: EventDef[]
@@ -25,11 +27,21 @@ export interface ModPackData {
/** 丹方/铸器配方 */
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 }>
/** 0.1.31 内容面总攻:物品/功法(PACK 合入)、建筑/职事/志向/阵型/性格(双源注册) */
items?: Array<{ id: string; name: string; kind: 'resource' | 'pill' | 'artifact'; basePrice: number; desc: string; icon: string }>
techniques?: Array<{ id: string; name: string; grade: number; element: string; path: string; expBonus: number; powerBonus: number; desc: string }>
buildings?: Array<{ id: string; name: string; icon: string; desc: string; kind: 'produce' | 'function'; maxLevel: number; produceExpr?: string; upgradeStones?: number; upgradeMineral?: number }>
posts?: Array<{ id: string; name: string; effectType: string; value: number; max: number; desc: string }>
aspirations?: Array<{ id: string; name: string; desc: string; effectType: string; value: number }>
formations?: Array<{ id: string; name: string; icon: string; desc: string; atk: number; def: number; retreatWound: number }>
traits?: Array<{ id: string; name: string; desc: string; bonus?: Record<string, number> }>
}
export interface ModPack {
id: string
name: string
/** MOD 类型(0.1.31):content=内容包 / data=实体包 / rules=规则包 */
kind?: ModKind
version: string
gameVersion?: string
author?: string
+2 -2
View File
@@ -2,7 +2,7 @@ import { Character, Element, Gender, Realm, RealmMajor } from '../../types/domai
import { Rng } from '../kernel/rng'
import { ELEMENT_LIST, ROOT_GRADES } from '../../data/elements'
import { MAJORS } from '../../data/realms'
import { TRAIT_POOL, TRAITS } from '../../data/traits'
import { TRAIT_POOL, TRAITS, traitById } from '../../data/traits'
export function rollRoots(rng: Rng, parents?: { m?: Character; f?: Character }): { grade: number; primary: Element; secondary: Element[] } {
let grade: number
@@ -107,7 +107,7 @@ export function traitBonuses(character: Character): { exp: number; breakBonus: n
let charmBonus = 0
let priceMult = 0
for (const t of character.traits) {
const def = TRAITS[t]
const def = traitById(t)
if (!def) continue
exp += def.expBonus ?? 0
breakBonus += def.breakBonus ?? 0
+3 -3
View File
@@ -1,6 +1,6 @@
/** WorldSim —— 世界自进化引擎(game/engine/sim/WorldSim.ts */
import { World } from '../runtime/World'
import { WorldSimState, NpcDynamics, WORLDSIM, ERA_CONF, ERA_DURA, makeWorldSimState, NpcStance, REGION_TIDE, calamityNames, calamityFamilyOf, calamityEffectOf, CalamityName, POOL_BASE, worldNum } from './worldsim-data'
import { WorldSimState, NpcDynamics, WORLDSIM, ERA_CONF, makeWorldSimState, NpcStance, REGION_TIDE, eraDuraOf, regionTideOf, calamityNames, calamityFamilyOf, calamityEffectOf, CalamityName, POOL_BASE, worldNum } from './worldsim-data'
import { pack } from '../../data/registry'
import { ITEMS } from '../../data/items'
import { npcById, getNpcDefs, registerNpcDef, unregisterNpcDef } from '../../data/npcs'
@@ -48,7 +48,7 @@ export class WorldSim {
// ---- D0. 世纪弧转移(年首评估) ----
if (this.w.state.month === 1) {
const curEra = s.era ?? 'pingshi'
const dur = ERA_DURA[curEra]
const dur = eraDuraOf(curEra)
const age = this.w.state.year - (s.eraStartYear ?? 1)
if (age >= dur[0] && age < dur[1] + 5) {
const flow = ERA_CONF[curEra].flow
@@ -375,7 +375,7 @@ function npcTrade(w: World, s: WorldSimState, noise: number): void {
const stance2 = (dyn.stance ?? 'guardian') as string
const tradeMult = stance2 === 'expand' ? 1.3 : stance2 === 'endure' ? 0.7 : 1
// W3 区域灵势:各 era 修正(e.g. 乱世北岳玄影峰灵衰 0.08)
const regionMod = 1 + (REGION_TIDE[id]?.[(s.era ?? 'pingshi') as 'shengshi'] ?? 0)
const regionMod = 1 + regionTideOf(id, s.era ?? 'pingshi')
const rate = worldNum('npcTradeRate') * (isCalamity ? 0.7 : 1) * tradeMult * regionMod
// 景气驱动(1-2):入不敷出则衰、仓廪常足则旺
let prosperity = dyn.prosperity ?? 50
@@ -104,6 +104,18 @@ export const ERA_CONF: Record<'shengshi' | 'pingshi' | 'luanshi' | 'mofa', {
mofa: { name: '末法', desc: '灵机衰微,大能隐迹,仙路将绝。', calamityMult: 1.2, supplyMult: 0.7, demandMult: 0.85, auctionMult: 0.6, tideBias: -0.09, flow: [['pingshi', 0.4], ['shengshi', 0.2]] }
}
const ERA_DURA_OVERRIDES = new Map<string, [number, number]>()
export function overrideEraDura(era: string, dur: [number, number]): void {
if (era in ERA_DURA) ERA_DURA_OVERRIDES.set(era, dur)
}
export function resetEraDura(era?: string): void {
if (era) ERA_DURA_OVERRIDES.delete(era)
else ERA_DURA_OVERRIDES.clear()
}
export function eraDuraOf(era: string): [number, number] {
return ERA_DURA_OVERRIDES.get(era) ?? ERA_DURA[era as keyof typeof ERA_DURA] ?? [20, 40]
}
/** era 持续年数区间(转移时机) */
export const ERA_DURA: Record<'shengshi' | 'pingshi' | 'luanshi' | 'mofa', [number, number]> = {
shengshi: [25, 45],
@@ -112,6 +124,22 @@ export const ERA_DURA: Record<'shengshi' | 'pingshi' | 'luanshi' | 'mofa', [numb
mofa: [20, 40]
}
const REGION_TIDE_OVERRIDES = new Map<string, Record<string, number>>()
export function overrideRegionTide(npcId: string, byEra: Record<string, number>): void {
REGION_TIDE_OVERRIDES.set(npcId, byEra)
}
export function resetRegionTide(npcId?: string): void {
if (npcId) REGION_TIDE_OVERRIDES.delete(npcId)
else REGION_TIDE_OVERRIDES.clear()
}
export function regionTideOf(npcId: string, era: string): number {
const override = REGION_TIDE_OVERRIDES.get(npcId)?.[era]
if (override !== undefined) return override
const base = REGION_TIDE[npcId]
if (!base) return 0
return base[era as keyof typeof base] ?? 0
}
/** 区域灵势(NPC id × era → 修正量 +-0.06~0.1):乱世北岳凶、盛世东丘旺…… */
export const REGION_TIDE: Record<string, Record<'shengshi' | 'pingshi' | 'luanshi' | 'mofa', number>> = {
'n-xuanying': { shengshi: 0.06, pingshi: 0, luanshi: -0.08, mofa: -0.06 },