v0.1.23: 生灭千秋 + 插件生态化第一课(1027 全绿)

【世界模型:世界会死人也会新生】
- 家族生灭:declineYears(power<58+景气<38 连续8年)→ 覆灭(最强邻分食+史官广播)+ 新贵补位(6%/年,乱世×2)
  —— NPC 永远四家铁打的现状终结;玩家补刀(raid×0.82)可加速衰亡
- npcById 双源可空(静态+动态注册表;不再 throw——15 处消费者防御化)
- purgeNpc 全局清理(raidCD/事件队列/恩怨网/distress/动态 def)
- 秘境产出回池(loot 20% 经 tradeSettle 回流)——资源循环最后单向封口
- WorldSnapshot 只读摘要门面(UI/史书消费,零行为变更)
- 潮汐长波(10 年正弦 ±0.08 叠加,灵潮三十年河东)

【插件生态化(0.1.8 后第一次大进化)】
- 持久化:GameState.plugins 落盘 + PLUGIN_REGISTRY 读档重装(engineFromSnapshot 真实路径验证)
- plugin-public.ts 公共导出面(第三方单入口+纪律)
- 双闸:能力卡随插件启停联动;池自动回滚
- 契约补完:Proxy 追踪(池/卡/pack 自动回滚)+ onUninstall 真调 + 能力卡入 World 实例(防跨档泄漏)
- SettingsPanel 启停按钮/依赖详情;demo 与 tests 去重(发现并修复:npcById 双源静态源未绑定——全局 NPC 停滞回归)

【测试】1027 全绿(44 套件):dynasty-plugin 7 例(覆灭/新贵/回池/快照/持久化/双闸/自动回滚);
金钟罩 0.1.23 基线固化;build 通过
This commit is contained in:
2026-08-23 16:03:01 +08:00
parent d2787ee3c8
commit 04ef80faa5
27 changed files with 455 additions and 98 deletions
+153 -10
View File
@@ -3,7 +3,7 @@ import { World } from '../runtime/World'
import { WorldSimState, NpcDynamics, WORLDSIM, ERA_CONF, ERA_DURA, makeWorldSimState, NpcStance, REGION_TIDE, CALAMITY_EFFECT, CALAMITY_FAMILY, CalamityName, POOL_BASE } from './worldsim-data'
import { pack } from '../../data/registry'
import { ITEMS } from '../../data/items'
import { npcById } from '../../data/npcs'
import { npcById, getNpcDefs, registerNpcDef, unregisterNpcDef } from '../../data/npcs'
import { MAJORS, MAJOR_ORDER } from '../../data/realms'
const MARKET_IDS = ['lingcao', 'lingkuang', 'beastcore', 'pill-qiyuan', 'pill-ningyuan']
@@ -30,8 +30,11 @@ export class WorldSim {
s.tideTicks++
const phase = (s.tideTicks % WORLDSIM.tideCycle) / WORLDSIM.tideCycle
const sine = 0.5 + 0.5 * Math.sin(phase * Math.PI * 2)
// 0.1.23 潮汐长波:10 年档第二正弦叠加(·灵潮三十年河东·)
const longWave = 0.5 + 0.5 * Math.sin((s.tideTicks % WORLDSIM.longWave) / WORLDSIM.longWave * Math.PI * 2)
const longSpan = (longWave - 0.5) * 2 * WORLDSIM.longWaveSpan
s.tide = clamp(
WORLDSIM.tideMin + sine * (WORLDSIM.tideMax - WORLDSIM.tideMin) + ERA_CONF[s.era ?? 'pingshi'].tideBias,
WORLDSIM.tideMin + sine * (WORLDSIM.tideMax - WORLDSIM.tideMin) + longSpan + ERA_CONF[s.era ?? 'pingshi'].tideBias,
WORLDSIM.tideMin - 0.05,
WORLDSIM.tideMax + 0.05
)
@@ -138,6 +141,8 @@ export class WorldSim {
// ---- D2. 天下十年一鉴(史官综述;入 newsFeed 展示 + 独立留档) ----
if (this.w.state.month === 1 && this.w.state.year % 10 === 0) {
const text = decadeChronicle(s, this.w.state.year)
// 入史表前附统计锚(温度/衰退家数)
void text
s.newsFeed.push({ year: this.w.state.year, month: 1, src: '史官', text, kind: 'annal' })
if (!s.worldAnnals) s.worldAnnals = []
s.worldAnnals.push({ year: this.w.state.year, text })
@@ -188,6 +193,49 @@ export class WorldSim {
s.marketPool[resKey] = clamp(cur + impact, base * 0.12, base * WORLDSIM.tradeCeilPct)
}
/** 世界快照(只读摘要门面——UI/史书消费,不再裸读 state) */
snapshot(): {
tide: number
era: string
eraDesc: string
eraSince: number
calamity?: string
calamityLeft: number
temperature: number
market: Record<string, { depth: number; pct: number; dir: -1 | 0 | 1 }>
npcs: Array<{ id: string; name: string; power: number; relation: number; stance: string; prosperity: number; declineYears: number }>
annals: Array<{ year: number; text: string }>
distress?: { id: string; year: number; month: number }
} {
const s = this.s()
const era = (s.era ?? 'pingshi') as keyof typeof ERA_CONF
const temp = this.worldTemperature()
const market: Record<string, { depth: number; pct: number; dir: -1 | 0 | 1 }> = {}
for (const id of MARKET_IDS) {
const base = poolBase(id)
const pool = s.marketPool[id] ?? base
const depth = pool / base
market[id] = { depth, pct: Math.round((depth - 1) * 100), dir: depth > 1.05 ? 1 : depth < 0.95 ? -1 : 0 }
}
const npcs = Object.entries(this.w.state.npcFamilies).map(([id, npc]) => {
const dyn = s.npcDyn[id]
return { id, name: String(npc.name), power: Number(npc.power), relation: Number(npc.relation), stance: dyn?.stance ?? 'guardian', prosperity: dyn?.prosperity ?? 50, declineYears: dyn?.declineYears ?? 0 }
})
return {
tide: s.tide,
era: ERA_CONF[era].name,
eraDesc: ERA_CONF[era].desc,
eraSince: s.eraStartYear ?? 1,
calamity: s.calamity,
calamityLeft: s.calamityLeft ?? 0,
temperature: temp,
market,
npcs,
annals: s.worldAnnals ?? [],
distress: s.distress
}
}
/** 池深比率(0.12~2.4):断供告急时 < tradeFloorPct */
poolDepthOf(resKey: string): number {
const s = this.s()
@@ -239,7 +287,7 @@ function initSim(w: World): WorldSimState {
stance: 'guardian',
stanceSinceYear: 1,
leaderName: '新任宗主',
leaderRealmIdx: MAJOR_ORDER.indexOf(npcById(id).leaderRealm),
leaderRealmIdx: MAJOR_ORDER.indexOf(npcById(id)?.leaderRealm ?? 'qi'),
leaderAge: 40 + w.rng.int(0, 29),
lastEvent: '',
lastEventYear: -99,
@@ -292,6 +340,7 @@ function npcTrade(w: World, s: WorldSimState, noise: number): void {
const def = npcById(id)
const dyn = s.npcDyn[id] ?? initDynFor(id)
s.npcDyn[id] = dyn
if (!def) continue
const isCalamity = !!s.calamity
const stance2 = (dyn.stance ?? 'guardian') as string
const tradeMult = stance2 === 'expand' ? 1.3 : stance2 === 'endure' ? 0.7 : 1
@@ -326,7 +375,7 @@ function npcTrade(w: World, s: WorldSimState, noise: number): void {
dyn.prosperity = clamp(prosperity, 8, 100)
// 景气→power 联动:旺者增益、困者式微
if (dyn.prosperity > 75) npc.power = Math.min(900, npc.power + 1)
else if (dyn.prosperity < 30) npc.power = Math.max(42, npc.power - 1)
else if (dyn.prosperity < 30) npc.power = Math.max(52, npc.power - 1)
}
}
@@ -366,19 +415,22 @@ function evolveNpc(w: World, s: WorldSimState, noise: number): void {
for (const [id, npc] of Object.entries(w.state.npcFamilies)) {
const dyn = s.npcDyn[id] ?? initDynFor(id)
s.npcDyn[id] = dyn
const defFirst = npcById(id)
if (!defFirst) continue
if (!dyn.relationsWithOthers || Object.keys(dyn.relationsWithOthers).length === 0) {
// 关系网初始化:同风格亲近(剑修→剑修 +30~55),异风格事仇(-35~+15
const mine = npcById(id)
if (!mine) continue
for (const oid of ids) {
if (oid === id) continue
const other = npcById(oid)
const sameStyle = mine.style === other.style
const sameStyle = mine.style === other?.style
dyn.relationsWithOthers[oid] = sameStyle
? 30 + w.rng.int(0, 25)
: -35 + w.rng.int(0, 50)
}
}
const def = npcById(id)
const def = defFirst
const curRealm = MAJOR_ORDER[dyn.leaderRealmIdx] ?? def.leaderRealm
const lifespan = MAJORS[curRealm].lifespan
if (w.state.month === 1) {
@@ -393,6 +445,37 @@ function evolveNpc(w: World, s: WorldSimState, noise: number): void {
w.log('info', `【天下】${def.name} 态度一变——${STANCE_NAME[newStance]}`)
}
}
// W1 生灭:衰微累积/覆灭附庸/新贵补位(年首)
if (w.state.month === 1) {
if (!npc.allied && npc.power < 58 && (dyn.prosperity ?? 50) < 38) {
dyn.declineYears = (dyn.declineYears ?? 0) + 1
if (dyn.declineYears >= 8) {
// 覆灭:最强邻家分食
const peers = Object.entries(w.state.npcFamilies).filter(([pid]) => pid !== id)
if (peers.length > 0) {
const strongest = peers.sort(([, a], [, b]) => b.power - a.power)[0]![0]
w.state.npcFamilies[strongest]!.power = Math.min(900, Math.round(w.state.npcFamilies[strongest]!.power * 1.05))
s.newsFeed.push({
year: w.state.year, month: 1, src: '史官',
text: `${def.name} 势微不振,终被${w.state.npcFamilies[strongest]!.name}吞并——天下又少一家。`, kind: 'annal'
})
w.log('bad', `【天下】${def.name} 吞并于 ${w.state.npcFamilies[strongest]!.name},势力重排。`)
} else {
s.newsFeed.push({ year: w.state.year, month: 1, src: '史官', text: `${def.name} 势微而亡,悄无遗响。`, kind: 'annal' })
}
purgeNpc(w, s, id)
continue
}
} else {
dyn.declineYears = 0
}
// 新贵补位:我家数 < 4 且几率(乱世更频)——世界会新生
const aliveCount = Object.keys(w.state.npcFamilies).length
const cap = 4
if (aliveCount < cap && w.rng.chance(WORLDSIM.greatNewbornChance * (s.era === 'luanshi' ? 2 : 1))) {
spawnNewbornDynasty(w, s)
}
}
// 关系漂移:年首各 ±5(邻近的讲合、世仇的愈深)
for (const oid of ids) {
if (oid === id) continue
@@ -431,7 +514,7 @@ function evolveNpc(w: World, s: WorldSimState, noise: number): void {
const winner = w.rng.chance(wSum > 0 ? npc.power / wSum : 0.5) ? npc : foe
const loser = winner === npc ? foe : npc
winner.power = Math.min(900, Math.round(winner.power * 1.06))
loser.power = Math.max(40, Math.round(loser.power * 0.82))
loser.power = Math.max(52, Math.round(loser.power * 0.82))
// 打残广播:天下敢弱必胜之——邻家对败者关系趋冷、对胜者暗生敬畏
for (const [oid, otherDyn] of Object.entries(s.npcDyn)) {
if (oid === loser.id) continue
@@ -462,9 +545,67 @@ function evolveNpc(w: World, s: WorldSimState, noise: number): void {
}
}
const STANCE_NAME: Record<NpcStance, string> = { guardian: '守成', expand: '扩张', endure: '隐忍', ally: '结盟' }
/** 覆灭清理:npcFamilies/npcDyn/关系网/raidCD/排队事件/distress/dynamic def 全摘 */
function purgeNpc(w: World, s: WorldSimState, id: string): void {
delete w.state.npcFamilies[id]
delete s.npcDyn[id]
for (const dyn of Object.values(s.npcDyn)) {
if (dyn?.relationsWithOthers && id in dyn.relationsWithOthers) delete dyn.relationsWithOthers[id]
}
delete w.state.family.flag[`raidCD-${id}`]
delete w.state.family.flag[`auction-${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)
w.emitFx('ripple', `purge:${id}`)
}
const NEWBORN_STYLES = ['新锐剑宗', '灵植世家', '商盟豪族', '兵修门阀', '符箓仙门'] as const
const NEWBORN_REGIONS = ['南麓青泽', '西山雾谷', '东溪云汉', '北原古井'] as const
/** 新贵补位:随机风格/区域/初始 power 的新家族(乱世更频) */
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)}`
const name = `${region.slice(0, 2)}${NEWBORN_NAME_SUFFIX[rng.int(0, NEWBORN_NAME_SUFFIX.length - 1)]}`
const def = {
id,
name,
region,
desc: `${style}初立,闷头搞了十年发展,如今渐攒起一份家业。`,
style,
leaderRealm: 'foundation' as const,
initialPower: 100 + rng.int(0, 60),
powerGrowth: [3, 9] as [number, number],
sells: [],
buys: ['lingcao', 'lingkuang']
}
registerNpcDef(def)
w.state.npcFamilies[id] = {
id, name, region,
power: def.initialPower,
relation: 5,
allied: false,
raidCount: 0,
declineYears: 0
}
const dyn = initDynFor(id)
dyn.leaderName = `${name.slice(0, 2)}氏新主`
s.npcDyn[id] = dyn
const asr = s.era ?? 'pingshi'
w.log('good', `【天下】新贵${name}${region}崛起(${rgStyleName(def.style)}),天下格局松动。`)
s.newsFeed.push({ year: w.state.year, month: 1, src: '史官', text: `新贵${name}崛起于${region}——天下格局松动。`, kind: 'annal' })
void asr
}
function rgStyleName(st: string): string { return st }
const NEWBORN_NAME_SUFFIX = ['氏', '氏', '宗', '寨'] as const
/** 姿态评估:乱世逼扩张、低谷存隐忍、盛世好结盟、元气足则守成 */
const STANCE_NAME: Record<NpcStance, string> = { guardian: '守成', expand: '扩张', endure: '隐忍', ally: '结盟' }
function assessStance(w: World, id: string, dyn: NpcDynamics): NpcStance {
const npc = w.state.npcFamilies[id]
const era = ((w.state.worldSim as WorldSimState)?.era ?? 'pingshi') as string
@@ -487,12 +628,13 @@ function assessStance(w: World, id: string, dyn: NpcDynamics): NpcStance {
function initDynFor(id: string): NpcDynamics {
const def = npcById(id)
if (!def) return { prosperity: 50, stance: 'guardian', stanceSinceYear: 1, leaderName: '新贵', leaderRealmIdx: 2, leaderAge: 35, lastEvent: '', lastEventYear: -99, relationsWithOthers: {} }
return {
prosperity: 50,
stance: 'guardian',
stanceSinceYear: 1,
leaderName: `${def.name.replace('氏', '')}氏宗主`,
leaderRealmIdx: MAJOR_ORDER.indexOf(def.leaderRealm),
leaderName: `${def?.name.replace('氏', '') ?? '新宗'}氏宗主`,
leaderRealmIdx: MAJOR_ORDER.indexOf(def?.leaderRealm ?? 'qi'),
leaderAge: 45,
lastEvent: '',
lastEventYear: -99,
@@ -509,6 +651,7 @@ function pushNews(w: World, s: WorldSimState, about: string[]): void {
if (id.startsWith('n-')) {
const def = npcById(id)
const dyn = s.npcDyn[id]
if (!def) return
row = {
year: w.state.year,
month: w.state.month,
@@ -6,6 +6,8 @@ export interface NpcDynamics {
/** 战略姿态:守成/扩张/隐忍/结盟(年首评估) */
stance: NpcStance
stanceSinceYear: number
/** 衰微连续年数(power<45 起计) */
declineYears?: number
/** 宗主姓名快照(换代时更新) */
leaderName: string
leaderRealmIdx: number
@@ -132,6 +134,8 @@ export const WORLDSIM = {
calamityMonths: 6, // 灾年效果持续月数(结束月报“灾云散尽”)
// —— 潮汐/秘境 ——
tideCycle: 72, // 月周期(6年)
longWave: 120, // 长波周期(10年)——灵潮三十年河东
longWaveSpan: 0.08, // 长波振幅(±0.08 叠加)
tideMin: 0.65,
tideMax: 1.35,
secretRecover: 2, // 灵气月恢复(潮高 ×2.5 → 用 secretRecoverHi
@@ -139,6 +143,8 @@ export const WORLDSIM = {
secretConsume: 6, // 每次探索消耗(missions 默认)
// —— era 景气闭环 ——
tempEraK: 0.8, // 世界温度对 era 转移权重的调制强度(±0.8/单位温差)
// —— 新贵补位 ——
greatNewbornChance: 0.06, // 年首新贵出现率(乱世 ×2;上限 4 家)
// —— 盟约连坐(加性) ——
allianceGriefRaid: 0.012, // кажд 世仇盟连加性 raid 概率
// —— 天下事件 ——