v0.1.18: 时代绘卷 + 特效入引擎(双主线完成,1002 测试破千)

【主线一:世界自演进深化《时代绘卷》】
- 世纪弧 era 状态机:盛世/平世/乱世/末法四态(ERA_CONF 全参数化)。
  灾年概率×0.5~1.6、世界供需×0.7~1.25、拍卖概率×0.6~1.3、tide 偏置 ±0.09;
  ERA_DURA 控制转换节奏(15~45年),转移时史官快讯+log。世界有大节律了
- NPC 景气仓廪:npcDyn.prosperity(8~100)月结余——买卖+0.5~0.8、断供-2、灾年-3;
  >75 景气 power+1、<30 衰闷 power-1;换代焕新(55~75)。NPC 在过日子
- 蝴蝶效应:互攻胜率 power 加权(强者恒强)、打残广播(败家邻望-8/胜家+4);
  玩家劫掠胜利/结盟直接写 NPC·NPC 关系网(-6/-3)——玩家行动推进世界历史
- 天下十年一鉴:每十年史官综述(灾/换代/行情/灵潮聚合,kind='annal')
- 灾年↔秘境:灾年灵脉恢复减半(末法感)
- WorldPanel 天下志:era 徽标(悬停描述)/史官十年鉴/群雄势衡柱状图/衰微标注

【主线二:特效系统并入引擎(架构升级)】
- WorldEventBus.onFx + Kernel 转发 + World.emitFx 语义发射(零 rng 消耗)
- 12 个语义发射点:突破/化神/渡劫/劫掠胜负/遭遇战/拍卖/大比/灾年起散/换代
  —— 取代 UI log 文本正则猜测(spark/blade 双源锁死)
- makeBus 桥接 FxGate(kind→gate.emit),gate 挡位/密度/reduced 逻辑保留在 UI

【测试】1002 全绿(40 套件):fxqueue-semantic 3 例 + era-world 5 例;
金钟罩 0.1.18 基线固化(era/prosperity/蝴蝶效应受控变更)
This commit is contained in:
2026-08-23 14:38:27 +08:00
parent ea9f5c9c5d
commit 2f62eecb08
20 changed files with 347 additions and 41 deletions
+105 -13
View File
@@ -1,6 +1,6 @@
/** WorldSim —— 世界自进化引擎(game/engine/sim/WorldSim.ts */
import { World } from '../runtime/World'
import { WorldSimState, NpcDynamics, WORLDSIM, CALAMITY_EFFECT, CALAMITY_FAMILY, CalamityName, POOL_BASE } from './worldsim-data'
import { WorldSimState, NpcDynamics, WORLDSIM, ERA_CONF, ERA_DURA, 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'
@@ -30,23 +30,52 @@ 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)
s.tide = WORLDSIM.tideMin + sine * (WORLDSIM.tideMax - WORLDSIM.tideMin)
s.tide = Math.min(WORLDSIM.tideMax, WORLDSIM.tideMin + sine * (WORLDSIM.tideMax - WORLDSIM.tideMin) + ERA_CONF[s.era ?? 'pingshi'].tideBias)
// 秘境灵气(自动恢复 + 探索消耗由 missions 在探索时扣)
// 秘境灵气(自动恢复 + 探索消耗由 missions 在探索时扣;灾年灵脉闭锁 0.5 恢复
const qiRecover = s.calamity ? WORLDSIM.secretRecover * 0.5 : s.tide > 0.85 ? WORLDSIM.secretRecoverHi : WORLDSIM.secretRecover
for (const key of Object.keys(s.secretQi)) {
s.secretQi[key] = clamp(s.secretQi[key] + (s.tide > 0.85 ? 5 : 2) - (s.secretQi[key] > 70 ? 2 : 0), 0, 100)
s.secretQi[key] = clamp(s.secretQi[key] + qiRecover - (s.secretQi[key] > 70 ? 2 : 0), 0, 100)
}
// ---- D0. 世纪弧转移(年首评估) ----
if (this.w.state.month === 1) {
const curEra = s.era ?? 'pingshi'
const dur = ERA_DURA[curEra]
const age = this.w.state.year - (s.eraStartYear ?? 1)
if (age >= dur[0] && age < dur[1] + 5) {
const flow = ERA_CONF[curEra].flow
const roll = rng.next()
let acc = 0
for (const [next, wgt] of flow) {
acc += wgt
if (roll < acc) {
s.era = next
s.eraStartYear = this.w.state.year
const conf = ERA_CONF[next]
this.w.log('info', `【天下】世道更替——${conf.name}${conf.desc}`)
s.newsFeed.push({
year: this.w.state.year, month: 1, src: '天道',
text: `世道更替:${conf.name}来临。${conf.desc}`, kind: 'calamity'
})
break
}
}
}
}
// ---- D. 灾年(年签一掷 + 持续渐退;B12) ----
// 剩数递减
if ((s.calamityLeft ?? 0) > 0) s.calamityLeft = (s.calamityLeft ?? 0) - 1
if (this.w.state.month === 1 && rng.chance(WORLDSIM.calamityChance)) {
const calamityP = WORLDSIM.calamityChance * ERA_CONF[s.era ?? 'pingshi'].calamityMult
if (this.w.state.month === 1 && rng.chance(calamityP)) {
const cl = rng.pick([...WORLDSIM.calamities])
s.calamity = cl
s.calamityYear = this.w.state.year
s.calamityLeft = WORLDSIM.calamityMonths
applyCalamityToMarket(s, cl as CalamityName)
this.w.log('bad', `【天下灾年】${cl}——灵植减产,市价将行(约半年风雨)。`)
this.w.emitFx('pulse', `calamity:${cl}`)
// B7 灾年落家族(一次性体感效果)
if (cl === '疫病') {
for (const c of this.w.aliveMembers()) {
@@ -72,6 +101,7 @@ export class WorldSim {
text: `灾云散尽,灵气复清,天下重归平宁。`, kind: 'calamity'
})
this.w.log('info', `【天下】灾云散尽,灵气复清。`)
this.w.emitFx('ripple', 'calm')
}
// ---- A. 资源循环市场(世界供给/需求 + NPC 上桌 + 再平衡) ----
@@ -82,6 +112,11 @@ export class WorldSim {
// ---- B. NPC 演化(换代 + 关系网 + 互攻;B7) ----
evolveNpc(this.w, s, rng.next())
// ---- D2. 天下十年一鉴(史官综述) ----
if (this.w.state.month === 1 && this.w.state.year % 10 === 0) {
s.newsFeed.push({ year: this.w.state.year, month: 1, src: '史官', text: decadeChronicle(s, this.w.state.year), kind: 'annal' })
}
// ---- E. 天下快讯(节流) ----
if ((this.w.state.totalTicks % WORLDSIM.newsEvery) === 0) {
pushNews(this.w, s, MARKET_IDS)
@@ -133,6 +168,16 @@ export class WorldSim {
return (s.marketPool[resKey] ?? base) / base
}
/** 时代名(UI 与事件链读取) */
era(): string {
return ERA_CONF[(this.s().era ?? 'pingshi')].name
}
/** 时代拍卖/大比加成(events 周期链调用) */
eraAuctionMult(): number {
return ERA_CONF[(this.s().era ?? 'pingshi')].auctionMult
}
news(): WorldSimState['newsFeed'] {
return this.s().newsFeed
}
@@ -150,6 +195,7 @@ function initSim(w: World): WorldSimState {
const s = { ...empty() }
for (const id of Object.keys(w.state.npcFamilies)) {
s.npcDyn[id] = {
prosperity: 50,
leaderName: '新任宗主',
leaderRealmIdx: MAJOR_ORDER.indexOf(npcById(id).leaderRealm),
leaderAge: 40 + w.rng.int(0, 29),
@@ -180,13 +226,16 @@ function empty(): WorldSimState {
/** A2+A3:世界常驻供给与需求(潮汐乘化)——池有了呼吸 */
function worldBreath(w: World, s: WorldSimState): void {
const tide = s.tide
const era = ERA_CONF[s.era ?? 'pingshi']
const span = 1 - (tide - 1) * WORLDSIM.tideSupplySpan
const supplySpan = Math.max(0.75, Math.min(1.35, span))
const eraSupply = era.supplyMult
const eraDemand = era.demandMult
for (const id of MARKET_IDS) {
const base = poolBase(id)
const cur = s.marketPool[id] ?? base
const supply = base * WORLDSIM.worldSupplyRate * supplySpan
const demand = base * WORLDSIM.worldDemandRate
const supply = base * WORLDSIM.worldSupplyRate * supplySpan * eraSupply
const demand = base * WORLDSIM.worldDemandRate * eraDemand
s.marketPool[id] = clamp(cur + supply - demand, base * 0.12, base * WORLDSIM.tradeCeilPct)
}
void w
@@ -198,28 +247,39 @@ function npcTrade(w: World, s: WorldSimState, noise: number): void {
for (const [id, npc] of Object.entries(w.state.npcFamilies)) {
const per = n; n += 0.31
const def = npcById(id)
const dyn = s.npcDyn[id] ?? initDynFor(id)
s.npcDyn[id] = dyn
const isCalamity = !!s.calamity
const rate = WORLDSIM.npcTradeRate * (isCalamity ? 0.7 : 1)
// 卖:NPC 抛货 → 池增(量小到可忽略贸易盈亏,只表潮流)
// 景气驱动(1-2):入不敷出则衰、仓廪常足则旺
let prosperity = dyn.prosperity ?? 50
// 卖:NPC 抛货 → 池增;有货自给,景气微涨
for (const itemId of def.sells ?? []) {
if (!MARKET_IDS.includes(itemId)) continue
const base = poolBase(itemId)
const vol = base * rate * (0.6 + (per % 1) * 0.8)
s.marketPool[itemId] = clamp((s.marketPool[itemId] ?? base) + vol, base * 0.12, base * WORLDSIM.tradeCeilPct)
prosperity += 0.5
}
// 买:NPC 采购 → 池减;池太浅采不到 → NPC 繁荣受挫(power 微跌
// 买:NPC 采购 → 池减;池太浅采不到 → 断供受挫(景气大伤
for (const itemId of def.buys ?? []) {
if (!MARKET_IDS.includes(itemId)) continue
const base = poolBase(itemId)
const cur = s.marketPool[itemId] ?? base
const vol = base * rate * (0.6 + ((per * 7) % 1) * 0.8)
if (cur - vol < base * 0.12) {
npc.power = Math.max(40, Math.round(npc.power * 0.985))
prosperity -= 2
} else {
s.marketPool[itemId] = cur - vol
npc.power = Math.min(900, npc.power + 0.5)
prosperity += 0.8
}
}
// 灾年支出(共苦):景气再挫,若已困顿伤其筋骨
if (isCalamity) prosperity -= 3
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)
}
}
@@ -288,12 +348,14 @@ function evolveNpc(w: World, s: WorldSimState, noise: number): void {
dyn.leaderAge = 30 + w.rng.int(0, 25)
dyn.leaderRealmIdx = Math.min(dyn.leaderRealmIdx + 1, 5)
dyn.leaderName = `${def.name.replace('氏', '')}氏新主`
dyn.prosperity = 55 + w.rng.int(0, 20)
npc.power = Math.round(Math.max(40, npc.power + 15))
dyn.lastEvent = '宗祧更替'
dyn.lastEventYear = year
s.npcSuccessions++
pushNews(w, s, [id])
w.log('info', `【天下】${def.name} 更易宗主,气象一新。`)
w.emitFx('ripple', `succession:${id}`)
}
// B7 互攻:与世仇(关系<-50)年首相搏——败方伤筋动骨
for (const oid of ids) {
@@ -302,10 +364,21 @@ function evolveNpc(w: World, s: WorldSimState, noise: number): void {
if (!foe) continue
const rel = dyn.relationsWithOthers[oid] ?? 0
if (rel < -50 && w.rng.chance(WORLDSIM.npcEventChance)) {
const winner = w.rng.chance(0.5) ? npc : foe
// 1-3 蝴蝶效应:互攻按实力加权(强者越可能胜,弱者一败再败)
const wSum = npc.power + foe.power
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))
// 打残广播:天下敢弱必胜之——邻家对败者关系趋冷、对胜者暗生敬畏
for (const [oid, otherDyn] of Object.entries(s.npcDyn)) {
if (oid === loser.id) continue
const rr = otherDyn.relationsWithOthers[loser.id] ?? 0
otherDyn.relationsWithOthers[loser.id] = clamp(rr - 8, -100, 100)
if (oid === winner.id) continue
const wr = otherDyn.relationsWithOthers[winner.id] ?? 0
otherDyn.relationsWithOthers[winner.id] = clamp(wr + 4, -100, 100)
}
s.newsFeed.push({
year, month: 1, src: winner.name,
text: `${winner.name}${loser.name} 起衅——痛挫其锋,势力大动。`
@@ -321,6 +394,7 @@ function evolveNpc(w: World, s: WorldSimState, noise: number): void {
function initDynFor(id: string): NpcDynamics {
const def = npcById(id)
return {
prosperity: 50,
leaderName: `${def.name.replace('氏', '')}氏宗主`,
leaderRealmIdx: MAJOR_ORDER.indexOf(def.leaderRealm),
leaderAge: 45,
@@ -335,7 +409,7 @@ function pushNews(w: World, s: WorldSimState, about: string[]): void {
const idx = rng.int(0, about.length - 1)
const id = about[idx]
const tide = s.tide > 1.0 ? '灵潮上涨' : s.tide < 0.75 ? '灵潮回落' : '汐平'
let row: { year: number; month: number; src: string; text: string; itemId?: string; kind?: 'quote' | 'npc' | 'calamity' }
let row: { year: number; month: number; src: string; text: string; itemId?: string; kind?: 'quote' | 'npc' | 'calamity' | 'annal' }
if (id.startsWith('n-')) {
const def = npcById(id)
const dyn = s.npcDyn[id]
@@ -364,6 +438,24 @@ function pushNews(w: World, s: WorldSimState, about: string[]): void {
s.lastNewsMonth = w.state.month
}
/** 十年一鉴:从近十年快讯聚合综述(史官体) */
function decadeChronicle(s: WorldSimState, year: number): string {
const from = year - 9
const rows = s.newsFeed.filter((r) => r.year >= from && r.year < year)
const calamities = rows.filter((r) => r.kind === 'calamity' && r.text.includes('灾')).length
const successions = rows.filter((r) => r.kind === 'npc').length
const quotes = rows.filter((r) => r.kind === 'quote').length
const era = ERA_CONF[s.era ?? 'pingshi'].name
const tideAvg = s.tide
const parts: string[] = []
parts.push(`${from}年~${year - 1}年,天下${era}之期`)
if (calamities) parts.push(`灾年${calamities}`)
if (successions) parts.push(`宗祧更替${successions}`)
if (quotes) parts.push(`行情起落${quotes}`)
parts.push(`灵潮${tideAvg > 1 ? '善' : tideAvg < 0.8 ? '劣' : '平'}`)
return `史官曰:${parts.join('')}`
}
function clamp(v: number, lo: number, hi: number): number {
return Math.max(lo, Math.min(hi, v))
}
+35 -2
View File
@@ -1,4 +1,6 @@
export interface NpcDynamics {
/** 景气指数(0~100):月结余/断供受挫/灾年支出;驱动 power 微调 */
prosperity: number
/** 宗主姓名快照(换代时更新) */
leaderName: string
leaderRealmIdx: number
@@ -31,10 +33,13 @@ export interface WorldSimState {
/** 灾年剩余月数(递减,归 0 时散去) */
calamityLeft?: number
/** 天下快讯(滚动 N=120itemId/kind 供 UI 响应按钮与区分) */
newsFeed: { year: number; month: number; src: string; text: string; itemId?: string; kind?: 'quote' | 'npc' | 'calamity' }[]
newsFeed: { year: number; month: number; src: string; text: string; itemId?: string; kind?: 'quote' | 'npc' | 'calamity' | 'annal' }[]
lastNewsMonth: number
/** NPC 换代计数 */
npcSuccessions: number
/** 世纪弧(时代状态机) */
era?: 'shengshi' | 'pingshi' | 'luanshi' | 'mofa'
eraStartYear?: number
}
export function makeWorldSimState(): WorldSimState {
@@ -49,7 +54,9 @@ export function makeWorldSimState(): WorldSimState {
calamityLeft: 0,
newsFeed: [],
lastNewsMonth: -99,
npcSuccessions: 0
npcSuccessions: 0,
era: 'pingshi',
eraStartYear: 1
}
}
@@ -62,6 +69,32 @@ export const POOL_BASE: Record<string, number> = {
'pill-ningyuan': 40
}
/** 世纪弧:时代 → 世界调制量(0.1.18 大周期) */
export const ERA_CONF: Record<'shengshi' | 'pingshi' | 'luanshi' | 'mofa', {
name: string
desc: string
calamityMult: number
supplyMult: number
demandMult: number
auctionMult: number
tideBias: number
/** 转移概势表:[下一态, 权重] */
flow: Array<['shengshi' | 'pingshi' | 'luanshi' | 'mofa', number]>
}> = {
shengshi: { name: '盛世', desc: '灵气鼎盛,百业兴旺,天下升平。', calamityMult: 0.5, supplyMult: 1.25, demandMult: 1.15, auctionMult: 1.3, tideBias: 0.08, flow: [['pingshi', 0.7], ['luanshi', 0.3]] },
pingshi: { name: '平世', desc: '四海无波,仙凡相安。', calamityMult: 1.0, supplyMult: 1.0, demandMult: 1.0, auctionMult: 1.0, tideBias: 0, flow: [['shengshi', 0.35], ['luanshi', 0.45], ['mofa', 0.2]] },
luanshi: { name: '乱世', desc: '群雄相逐,烽烟四起,灾祸连年。', calamityMult: 1.6, supplyMult: 0.85, demandMult: 1.05, auctionMult: 0.75, tideBias: -0.04, flow: [['pingshi', 0.4], ['mofa', 0.6]] },
mofa: { name: '末法', desc: '灵机衰微,大能隐迹,仙路将绝。', calamityMult: 1.2, supplyMult: 0.7, demandMult: 0.85, auctionMult: 0.6, tideBias: -0.09, flow: [['pingshi', 0.6], ['shengshi', 0.4]] }
}
/** era 持续年数区间(转移时机) */
export const ERA_DURA: Record<'shengshi' | 'pingshi' | 'luanshi' | 'mofa', [number, number]> = {
shengshi: [25, 45],
pingshi: [20, 40],
luanshi: [15, 30],
mofa: [20, 40]
}
/** 世界演化参数表(全部可调) */
export const WORLDSIM = {
marketDriftRate: 0.03,