1112 lines
51 KiB
TypeScript
1112 lines
51 KiB
TypeScript
/** WorldSim —— 世界自进化引擎(game/engine/sim/WorldSim.ts) */
|
||
import { World } from '../runtime/World'
|
||
import { WorldSimState, NpcDynamics, WORLDSIM, ERA_CONF, makeWorldSimState, NpcStance, REGION_TIDE, eraDuraOf, regionTideOf, calamityNames, calamityFamilyOf, calamityEffectOf, CalamityName, POOL_BASE, worldNum, WONDER_CHANCE, WONDER_NAMES, WONDER_DESCS, WONDER_DURATION, WorldWonderKind, WorldWonder, allWonderKinds, wonderDefOf } from './worldsim-data'
|
||
import { pack } from '../../data/registry'
|
||
import { ITEMS } from '../../data/items'
|
||
import { npcById, getNpcDefs, registerNpcDef, unregisterNpcDef } from '../../data/npcs'
|
||
import { allMissions, WONDER_TEMPLATES, type MissionDef } from '../../data/secrets'
|
||
import { MAJORS, MAJOR_ORDER } from '../../data/realms'
|
||
|
||
const MARKET_IDS = ['lingcao', 'lingkuang', 'beastcore', 'lingyu', 'lingmu', 'shoupi', 'lingguo', 'pill-qiyuan', 'pill-ningyuan']
|
||
|
||
export class WorldSim {
|
||
constructor(private w: World) {}
|
||
|
||
private s(): WorldSimState {
|
||
const w = this.w
|
||
if (!w.state.worldSim) w.state.worldSim = initSim(w)
|
||
const ws = w.state.worldSim as WorldSimState
|
||
if (!ws.secretQi || Object.keys(ws.secretQi).length === 0) {
|
||
const defs = [...allMissions(), ...Object.values(ws.wonders ?? {})]
|
||
ws.secretQi = {}
|
||
for (const m of defs) ws.secretQi[(m as { id: string }).id] = 50
|
||
}
|
||
return ws
|
||
}
|
||
|
||
tick(): void {
|
||
// 0.1.39 根治双闸:clocks.ts 外部守卫 + 此处入口二次守卫。
|
||
// 与 tournament/apprentice 等系统卡同模式(sysEnabled ?? true 兜底,
|
||
// 注册后默认开、关停后 false)。防止未来新增直接调 tick() 的路径绕过守卫。
|
||
if (!this.w.sysEnabled('worldsim')) return
|
||
const s = this.s()
|
||
const rng = this.w.rng
|
||
// ---- C. 灵气潮汐(大周期) ----
|
||
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) + longSpan + ERA_CONF[s.era ?? 'pingshi'].tideBias,
|
||
WORLDSIM.tideMin - 0.05,
|
||
WORLDSIM.tideMax + 0.05
|
||
)
|
||
|
||
// 秘境灵气(自动恢复 + 探索消耗由 missions 在探索时扣;灾年灵脉闭锁 0.5 恢复)
|
||
const qiRecover = s.calamity ? worldNum('secretRecover') * 0.5 : s.tide > 0.85 ? worldNum('secretRecoverHi') : worldNum('secretRecover')
|
||
for (const key of Object.keys(s.secretQi)) {
|
||
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 = 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
|
||
const roll = rng.next()
|
||
let acc = 0
|
||
let moved = false
|
||
// W1 景气闭环:世界温度调制转移概率(大世托盛世、市道衰微催乱世)
|
||
const temp = this.worldTemperature()
|
||
const k = WORLDSIM.tempEraK
|
||
const tempMod = (next: string): number => {
|
||
if (next === 'shengshi') return 1 + (temp - 1) * k
|
||
if (next === 'luanshi' || next === 'mofa') return 1 + (1 - temp) * k
|
||
return 1
|
||
}
|
||
for (const [next, wgt] of flow) {
|
||
const wgtMod = wgt * tempMod(next)
|
||
acc += wgtMod
|
||
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'
|
||
})
|
||
moved = true
|
||
break
|
||
}
|
||
}
|
||
void moved
|
||
// roll 落在 flow 权重之外 → 延续本届(era 有厚度)
|
||
}
|
||
}
|
||
|
||
// ---- D-0.8 功德因果年首回馈(0.1.37) ----
|
||
if (this.w.state.month === 1) {
|
||
const fam = this.w.state.family
|
||
// karma 年首向中性衰减(0.1.37:衰减率可经 worldNum('karmaDecayRate') 覆写)
|
||
const decayRate = worldNum('karmaDecayRate')
|
||
if (fam.karma !== 0) {
|
||
const decay = Math.round(fam.karma * decayRate * 10) / 10
|
||
fam.karma = Math.round((fam.karma - decay) * 10) / 10
|
||
if (Math.abs(fam.karma) < 0.1) fam.karma = 0
|
||
}
|
||
// 因果回馈:高 karma 年首广播福泽
|
||
const karmaHigh = worldNum('karmaHighThreshold')
|
||
if (fam.karma > karmaHigh && !fam.flag['karmaBlessed-' + this.w.state.year]) {
|
||
fam.flag['karmaBlessed-' + this.w.state.year] = true
|
||
this.w.log('good', `积善之家必有余庆——功德深厚(${Math.round(fam.karma)}),天地福泽暗佑。`)
|
||
}
|
||
// 因果回馈:低 karma 年首天谴预警
|
||
const karmaLow = worldNum('karmaLowThreshold')
|
||
if (fam.karma < karmaLow && !fam.flag['karmaWarned-' + this.w.state.year]) {
|
||
fam.flag['karmaWarned-' + this.w.state.year] = true
|
||
this.w.log('bad', `因果缠身——功德亏缺(${Math.round(fam.karma)}),灾祸或至。`)
|
||
}
|
||
}
|
||
|
||
// ---- D-0.9 世界奇观年首判定(0.1.37 种子派生——零 rng 消耗) ----
|
||
if (this.w.state.month === 1) {
|
||
// 上一年奇观到期清理
|
||
if (s.wonder && s.wonder.duration && s.wonder.duration > 0) {
|
||
s.wonder.elapsed = (s.wonder.elapsed ?? 0) + 12
|
||
if (s.wonder.elapsed >= s.wonder.duration) {
|
||
const wdefEnd = wonderDefOf(s.wonder.kind)
|
||
const name = wdefEnd?.name ?? s.wonder.kind
|
||
s.newsFeed.push({ year: this.w.state.year, month: 1, src: '天道', text: `${name}消散——天地复常。`, kind: 'annal' })
|
||
this.w.log('info', `【天下】${name}消散,天地复常。`)
|
||
s.wonder = undefined
|
||
}
|
||
}
|
||
// 年首种子派生判定(不消耗 rng 主序列)
|
||
if (!s.wonder) {
|
||
let sh = 0
|
||
for (let i = 0; i < this.w.state.seed.length; i++) sh = (sh * 31 + this.w.state.seed.charCodeAt(i)) >>> 0
|
||
const wonderRoll = ((sh ^ (this.w.state.year * 16777619) ^ (s.npcSuccessions * 2654435761)) >>> 0) % 1000 / 1000
|
||
if (wonderRoll < worldNum('wonderChance')) {
|
||
// 0.1.37 P5:奇观类型从聚合池选取(内置 + MOD 扩展——零 rng 消耗)
|
||
const allKinds = allWonderKinds()
|
||
const kindIdx = ((sh + this.w.state.year * 40503) >>> 0) % allKinds.length
|
||
const wdef = allKinds[kindIdx]!
|
||
const kind = wdef.kind as WorldWonderKind
|
||
s.wonder = {
|
||
kind,
|
||
year: this.w.state.year,
|
||
duration: wdef.duration,
|
||
elapsed: 0
|
||
}
|
||
const name = wdef.name
|
||
const desc = wdef.desc
|
||
s.newsFeed.push({ year: this.w.state.year, month: 1, src: '天机', text: `${name}——${desc}`, kind: 'annal' })
|
||
this.w.log('good', `【天下异变】${name}:${desc}`)
|
||
this.w.emitFx('pulse', `wonder:${kind}`)
|
||
// 一次性奇观效果(0.1.37 P5:通过 wonderDefOf 识别扩展奇观的 oneshot 类型)
|
||
const wdef2 = wonderDefOf(kind)
|
||
if (wdef2?.oneshot === 'bless') {
|
||
// 天降祥瑞:灾年消弭
|
||
if (s.calamity) {
|
||
s.calamity = undefined
|
||
s.calamityLeft = 0
|
||
this.w.log('good', '祥瑞降世,灾厄消弭——灾云散尽。')
|
||
}
|
||
// 声望 +5
|
||
this.w.state.family.reputation += 5
|
||
} else if (wdef2?.oneshot === 'loot') {
|
||
// 古遗迹/宝物型奇观:获得随机资源
|
||
const loot = ['lingcao', 'lingkuang', 'beastcore', 'lingyu'][((sh + this.w.state.year * 7919) >>> 0) % 4]
|
||
const qty = 10 + ((sh + this.w.state.year * 13) >>> 0) % 15
|
||
this.w.state.family.inventory[loot] = (this.w.state.family.inventory[loot] ?? 0) + qty
|
||
const lootName = { lingcao: '灵草', lingkuang: '灵矿', beastcore: '兽核', lingyu: '灵玉' }[loot] ?? loot
|
||
this.w.log('good', `${name}中得${lootName}×${qty}——前世遗珍。`)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---- 0.1.37 奇观持续效果(月度——通过 wonderDefOf 识别扩展奇观的 effect 类型) ----
|
||
if (s.wonder) {
|
||
const wdefCont = wonderDefOf(s.wonder.kind)
|
||
if (wdefCont?.effect === 'tide_up') {
|
||
// 灵气增益:tide 缓升(修炼加成)
|
||
s.tide = clamp(s.tide + 0.003, WORLDSIM.tideMin - 0.05, WORLDSIM.tideMax + 0.2)
|
||
} else if (wdefCont?.effect === 'tide_down') {
|
||
// 灵气衰减:tide 缓降(修行衰减)
|
||
s.tide = clamp(s.tide - 0.003, WORLDSIM.tideMin - 0.2, WORLDSIM.tideMax + 0.05)
|
||
}
|
||
}
|
||
|
||
// ---- D. 灾年(年签一掷 + 持续渐退;B12) ----
|
||
// 剩数递减
|
||
if ((s.calamityLeft ?? 0) > 0) s.calamityLeft = (s.calamityLeft ?? 0) - 1
|
||
// 0.1.37 因果调制:karma 高于阈值灾年概率 -15%,低于阈值 +15%(阈值可经 worldNum 覆写)
|
||
const kHigh = worldNum('karmaHighThreshold')
|
||
const kLow = worldNum('karmaLowThreshold')
|
||
const karmaCalamityMod = this.w.state.family.karma > kHigh ? 0.85 : this.w.state.family.karma < kLow ? 1.15 : 1
|
||
const calamityP = worldNum('calamityChance') * ERA_CONF[s.era ?? 'pingshi'].calamityMult * karmaCalamityMod
|
||
if (this.w.state.month === 1 && rng.chance(calamityP)) {
|
||
const cl = rng.pick(calamityNames())
|
||
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()) {
|
||
if (this.w.rng.chance(0.6)) {
|
||
const dmg = 8 + this.w.rng.int(0, 14)
|
||
c.health = Math.max(1, c.health - dmg)
|
||
if (c.health < 20) c.state = 'wounded'
|
||
}
|
||
}
|
||
this.w.log('bad', `瘟疫蔓延,族中大半病倒——灵药告急!`)
|
||
} else if (cl === '兽潮') {
|
||
const st = this.w.state.family.stones
|
||
this.w.state.family.stones -= Math.round(st * 0.04)
|
||
inv(this.w)['beastcore'] = (inv(this.w)['beastcore'] ?? 0) + 2
|
||
this.w.log('bad', `兽潮涌至,坊市稍有折损;猎得兽核数枚。`)
|
||
}
|
||
}
|
||
// 灾年到期散去
|
||
if (s.calamity && (s.calamityLeft ?? 0) <= 0) {
|
||
s.calamity = undefined
|
||
s.newsFeed.push({
|
||
year: this.w.state.year, month: this.w.state.month, src: '天道',
|
||
text: `灾云散尽,灵气复清,天下重归平宁。`, kind: 'calamity'
|
||
})
|
||
this.w.log('info', `【天下】灾云散尽,灵气复清。`)
|
||
this.w.emitFx('ripple', 'calm')
|
||
}
|
||
|
||
// ---- A. 资源循环市场(世界供给/需求 + NPC 上桌 + 再平衡) ----
|
||
worldBreath(this.w, s) // A2+A3:常驻供给与需求(潮汐乘化)
|
||
npcTrade(this.w, s, rng.next()) // A4:NPC 按 sells/buys 与池交易
|
||
driftMarket(s, rng.next(), this.w) // 波动项(保留噪声弹性)
|
||
|
||
|
||
// ---- B-0.5 战争疲劳休养(0.1.35:年首 ×0.9 回落——天下止戈则元气复) ----
|
||
if (this.w.state.month === 1 && s.warFatigue) s.warFatigue = clamp(s.warFatigue * 0.9, 0, 1)
|
||
|
||
// ---- B0. 新秘境灵机(0.1.34 万象归元:种子派生伪随机——零 rng 消耗,确定性天生) ----
|
||
if (this.w.state.month === 1) {
|
||
const era2 = s.era === 'luanshi' ? 2 : 1
|
||
const ch = worldNum('newSecretChance') * era2
|
||
let sh = 0
|
||
for (let i = 0; i < this.w.state.seed.length; i++) sh = (sh * 31 + this.w.state.seed.charCodeAt(i)) >>> 0
|
||
const pick = ((sh ^ (this.w.state.year * 2654435761) ^ (s.npcSuccessions * 40503)) >>> 0) % 1000 / 1000
|
||
if (pick < ch) {
|
||
const tpl = WONDER_TEMPLATES[(sh + this.w.state.year) % WONDER_TEMPLATES.length]
|
||
const def: MissionDef = {
|
||
...tpl,
|
||
id: `m-wonder-${(sh + this.w.state.year * 7919) % 9000 + 1000}`,
|
||
name: `${tpl.icon}地灵诏——${['古', '幽', '玄', '凌'][(sh + this.w.state.year) % 4]}坛`
|
||
}
|
||
s.wonders = s.wonders ?? {}
|
||
s.wonders[def.id] = def as unknown as Record<string, unknown>
|
||
s.secretQi[def.id] = Math.max(50, s.secretQi[def.id] ?? 50)
|
||
s.newsFeed.push({ year: this.w.state.year, month: 1, src: '天机', text: `${def.name} 现世——灵光冲霄,远近皆见。`, kind: 'annal' })
|
||
this.w.log('info', `【天下】有异宝出世:${def.name}。`)
|
||
}
|
||
}
|
||
|
||
// ---- B. NPC 演化(换代 + 关系网 + 互攻;B7) ----
|
||
evolveNpc(this.w, s, rng.next())
|
||
|
||
// ---- D1.5 盟友求援时效(18 月自清) ----
|
||
if (s.distress) {
|
||
const distAge = (this.w.state.year - s.distress.year) * 12 + (this.w.state.month - s.distress.month)
|
||
if (distAge > 18 && !this.w.state.npcFamilies[s.distress.id]?.allied) s.distress = undefined
|
||
}
|
||
|
||
// ---- D1.6 世鉴(50/100 年:开局 vs 现在——世界变过多少) ----
|
||
if (this.w.state.month === 1 && (this.w.state.year % 50 === 0 || this.w.state.year % 100 === 0)) {
|
||
const y = this.w.state.year
|
||
const wg = this.w.state.worldGen
|
||
const initial = wg?.npcCount ?? 0
|
||
const now = Object.keys(this.w.state.npcFamilies).length
|
||
const topNow = Math.max(...Object.values(this.w.state.npcFamilies).map((n) => n.power))
|
||
const topNowName = Object.values(this.w.state.npcFamilies).sort((a, b) => b.power - a.power)[0]?.name ?? '?'
|
||
const eraName = ERA_CONF[s.era ?? 'pingshi'].name
|
||
const kind = y % 100 === 0 ? 'centennial' : 'quinquagenary'
|
||
const text = `${y}年世鉴:天下历经${eraName}之世,${
|
||
now < initial ? `原有${initial}家凋零至${now}家——大浪淘沙` : now > initial ? `新版图扩展至${now}家——后浪迭起` : `格局维持${now}家之衡`
|
||
};当世最强为 ${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}`)
|
||
}
|
||
|
||
// ---- 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 })
|
||
if (s.worldAnnals.length > 80) s.worldAnnals.splice(0, s.worldAnnals.length - 80)
|
||
}
|
||
|
||
// ---- E. 天下快讯(节流) ----
|
||
if ((this.w.state.totalTicks % WORLDSIM.newsEvery) === 0) {
|
||
pushNews(this.w, s, MARKET_IDS)
|
||
}
|
||
|
||
// ---- C2. 快讯裁剪 ----
|
||
if (s.newsFeed.length > WORLDSIM.newsKeep) s.newsFeed.splice(0, s.newsFeed.length - WORLDSIM.newsKeep)
|
||
}
|
||
|
||
/** 秘境探索消耗灵气(missions 调用;统一入口,默认 6 点) */
|
||
consumeSecretQi(id: string, amount = 6): void {
|
||
const s = this.s()
|
||
if (s.secretQi[id] === undefined) s.secretQi[id] = 50
|
||
s.secretQi[id] = clamp(s.secretQi[id] - amount, 0, 100)
|
||
}
|
||
|
||
/** 秘境灵气对探索收益的门槛(低灵气收益衰减提示) */
|
||
secretQiOf(id: string): number {
|
||
const s = this.s()
|
||
return s.secretQi[id] ?? 50
|
||
}
|
||
|
||
/** 灵气系数(修炼/掉落市场乘子) */
|
||
tideMult(): number {
|
||
return this.s().tide
|
||
}
|
||
|
||
/** 当前市场行情(价格乘子,Market 唯一读口) */
|
||
marketMultFor(id: string): number {
|
||
const s = this.s()
|
||
const pool = s.marketPool[id] ?? 60
|
||
const base = poolBase(id)
|
||
return clamp(pool / base, WORLDSIM.priceFloor, WORLDSIM.priceCeil)
|
||
}
|
||
|
||
/** 玩家交易回写池:买 -delta / 卖 +delta(单件约 0.35 池份额波动) */
|
||
tradeSettle(resKey: string, delta: number): void {
|
||
const s = this.s()
|
||
const base = poolBase(resKey)
|
||
const cur = s.marketPool[resKey] ?? base
|
||
const impact = delta * 0.35
|
||
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()
|
||
const base = poolBase(resKey)
|
||
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
|
||
}
|
||
|
||
/** 世界温度(市场景气 0.5~2.4;era 转移的反馈输入) */
|
||
worldTemperature(): number {
|
||
const s = this.s()
|
||
let sum = 0
|
||
let n = 0
|
||
for (const id of MARKET_IDS) {
|
||
const base = poolBase(id)
|
||
sum += (s.marketPool[id] ?? base) / base
|
||
n++
|
||
}
|
||
const cold = n > 0 ? sum / n : 1
|
||
// 0.1.34 战争疲劳上浮:天下杀伐不止,寒气怨气凝而灵脉皆冷(打不动即荒年)
|
||
return cold - (s.warFatigue ?? 0) * worldNum('warFatigueTempK')
|
||
}
|
||
|
||
news(): WorldSimState['newsFeed'] {
|
||
return this.s().newsFeed
|
||
}
|
||
|
||
secretLis(): Record<string, number> {
|
||
return this.s().secretQi
|
||
}
|
||
|
||
npcDyn(): Record<string, NpcDynamics> {
|
||
return this.s().npcDyn
|
||
}
|
||
}
|
||
|
||
function initSim(w: World): WorldSimState {
|
||
const s = { ...empty() }
|
||
const wg = w.state.worldGen
|
||
// 开局关系网(worldgen 产物)——首月即全球有恩怨,不再等年首扩散
|
||
const relations = wg?.relations ?? {}
|
||
for (const id of Object.keys(w.state.npcFamilies)) {
|
||
s.npcDyn[id] = {
|
||
prosperity: 40 + ((Math.abs(id.charCodeAt(0) * 7) % 2) === 0 ? 25 : 0),
|
||
stance: 'guardian',
|
||
stanceSinceYear: 1,
|
||
leaderName: '新任宗主',
|
||
leaderRealmIdx: MAJOR_ORDER.indexOf(npcById(id)?.leaderRealm ?? 'qi'),
|
||
leaderAge: 40 + w.rng.int(0, 29),
|
||
lastEvent: '',
|
||
lastEventYear: -99,
|
||
relationsWithOthers: { ...(relations[id] ?? {}) }
|
||
}
|
||
}
|
||
// 开局 era + 市场偏置(worldgen 产物:同 seed 同天下)
|
||
if (wg?.eras) s.era = wg.eras
|
||
if (wg?.marketOffset) {
|
||
for (const [k, off] of Object.entries(wg.marketOffset)) {
|
||
const base = poolBase(k)
|
||
if (s.marketPool[k]) s.marketPool[k] = Math.max(base * 0.5, s.marketPool[k]! * (1 + off))
|
||
}
|
||
}
|
||
s.secretQi = {}
|
||
return s
|
||
}
|
||
|
||
function empty(): WorldSimState {
|
||
return { ...makeWorldSimState() }
|
||
}
|
||
|
||
/** A2+A3:世界常驻供给与需求(潮汐乘化)——池有了呼吸 */
|
||
function worldBreath(w: World, s: WorldSimState): void {
|
||
const tide = s.tide
|
||
const era = ERA_CONF[s.era ?? 'pingshi']
|
||
// 潮汐供给:灵涨万物丰(tide>1 供给×1.3、灵衰×0.7)——0.1.19 符号反转修复
|
||
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
|
||
let overflow = false
|
||
for (const id of MARKET_IDS) {
|
||
const base = poolBase(id)
|
||
const cur = s.marketPool[id] ?? base
|
||
// 超卖潮:池过剩 > 1.8×base 时世界产能自发回调(谷贱伤农)
|
||
const saturated = cur > base * 1.8
|
||
if (saturated) overflow = true
|
||
const supply = base * worldNum('worldSupplyRate') * supplySpan * eraSupply * (saturated ? 0.7 : 1)
|
||
const demand = base * worldNum('worldDemandRate') * eraDemand
|
||
s.marketPool[id] = clamp(cur + supply - demand, base * 0.12, base * WORLDSIM.tradeCeilPct)
|
||
}
|
||
// 超卖播报(年一次)——玩家能看到"别把市场玩崩"
|
||
if (overflow && s.overfluxYear !== w.state.year) {
|
||
s.overfluxYear = w.state.year
|
||
s.newsFeed.push({
|
||
year: w.state.year, month: w.state.month, src: '坊市', text: '谷贱伤农——市面货丰价滞,灵田多有弃耕,天下产能暂歇。', kind: 'quote'
|
||
})
|
||
if (w.state.totalTicks > 12) w.log('bad', '【坊市】谷贱伤农,世界产能回调。')
|
||
}
|
||
}
|
||
|
||
/** A4:NPC 按 def.sells/buys 与池交易——世界波动的背后有了玩家(NPC 化) */
|
||
function npcTrade(w: World, s: WorldSimState, noise: number): void {
|
||
let n = noise
|
||
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
|
||
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
|
||
// W3 区域灵势:各 era 修正(e.g. 乱世北岳玄影峰灵衰 0.08)
|
||
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
|
||
// 卖: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 采购 → 池减;池太浅采不到 → 断供受挫(景气大伤)
|
||
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) {
|
||
prosperity -= 2
|
||
} else {
|
||
s.marketPool[itemId] = cur - vol
|
||
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(52, npc.power - 1)
|
||
}
|
||
}
|
||
|
||
function itemNameOf(id: string): string {
|
||
const N: Record<string, string> = { lingcao: '灵草', lingkuang: '灵矿', beastcore: '兽核', lingyu: '灵玉', lingmu: '灵木', shoupi: '兽皮', lingguo: '灵果' }
|
||
return N[id] ?? id
|
||
}
|
||
|
||
function driftMarket(s: WorldSimState, noise: number, w: World): void {
|
||
let n = noise
|
||
// W3 材料生态相关:自然资源同向微漂(灵木/符链共享偏置——物产同源)
|
||
const matBias = (noise - 0.5) * 0.012
|
||
for (const id of MARKET_IDS) {
|
||
const per = n; n += 0.13
|
||
const base = poolBase(id)
|
||
const cur = s.marketPool[id] ?? base
|
||
// 向基准再平衡(弱化锚定:让供需流与 era 真正撬动价格)+ 噪声漂移(价格弹性)
|
||
const rebalance = (base - cur) * WORLDSIM.marketRebalance
|
||
const drift = ((per % 1) - 0.5) * WORLDSIM.marketDriftRate * base
|
||
const isMaterial = ['lingyu', 'lingmu', 'shoupi', 'lingguo'].includes(id)
|
||
|
||
s.marketPool[id] = Math.max(base * WORLDSIM.poolFloorPct, cur + rebalance + drift + (isMaterial ? matBias * base : 0))
|
||
// 0.1.34 断供监测:货源紧张连续月计数(触发价上浮×世界广播)
|
||
const line = base * WORLDSIM.shortageLine
|
||
if (s.marketPool[id] < line) {
|
||
s.shortage = s.shortage ?? {}
|
||
s.shortage[id] = (s.shortage[id] ?? 0) + 1
|
||
if (s.shortage[id] === worldNum('shortageMonths')) {
|
||
s.shortageWarned = s.shortageWarned ?? {}
|
||
s.shortageWarned[id] = w.state.year
|
||
s.newsFeed.push({ year: w.state.year, month: w.state.month, src: '行商', text: `${itemNameOf(id)}断货数月——坊市惜售,价腾三成。`, kind: 'annal' })
|
||
}
|
||
} else if (s.shortage?.[id]) {
|
||
delete s.shortage[id]
|
||
}
|
||
// 0.1.35 计数封顶(防断供期无限拉估)
|
||
if (s.shortage?.[id]) s.shortage[id] = Math.min(12, s.shortage[id] ?? 0)
|
||
}
|
||
}
|
||
|
||
function inv(w: World): Record<string, number> {
|
||
return w.state.family.inventory
|
||
}
|
||
|
||
function applyCalamityToMarket(s: WorldSimState, cl: CalamityName): void {
|
||
const eff = calamityEffectOf(cl)
|
||
for (const [id, pct] of Object.entries(eff) as [string, number][]) {
|
||
s.marketPool[id] = Math.max(10, (s.marketPool[id] ?? 50) * (1 + pct))
|
||
}
|
||
}
|
||
|
||
function poolBase(id: string): number {
|
||
return POOL_BASE[id] ?? 100
|
||
}
|
||
|
||
function evolveNpc(w: World, s: WorldSimState, noise: number): void {
|
||
void noise
|
||
const year = w.state.year
|
||
const ids = Object.keys(w.state.npcFamilies)
|
||
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
|
||
dyn.relationsWithOthers[oid] = sameStyle
|
||
? 30 + w.rng.int(0, 25)
|
||
: -35 + w.rng.int(0, 50)
|
||
}
|
||
}
|
||
const def = defFirst
|
||
const curRealm = MAJOR_ORDER[dyn.leaderRealmIdx] ?? def.leaderRealm
|
||
const lifespan = MAJORS[curRealm].lifespan
|
||
if (w.state.month === 1) {
|
||
dyn.leaderAge++
|
||
// 姿态评估(era>景气>power>relation)
|
||
if (w.state.month === 1 && (w.state.year - (dyn.stanceSinceYear ?? 1)) >= 8) {
|
||
const newStance = assessStance(w, id, dyn)
|
||
if (newStance !== dyn.stance) {
|
||
dyn.stance = newStance
|
||
dyn.stanceSinceYear = w.state.year
|
||
pushNews(w, s, [id])
|
||
w.log('info', `【天下】${def.name} 态度一变——${STANCE_NAME[newStance]}。`)
|
||
}
|
||
}
|
||
// W1 生灭:衰微累积/覆灭附庸/新贵补位(年首)
|
||
if (w.state.month === 1) {
|
||
// 残衰减判定(0.1.27 可达化):弱即衰(盛世会被增长拉回不灭、乱世真灭——世道越乱换血越烈)
|
||
const fragile = npc.power < 150
|
||
const battleWorn = (dyn.warLosses ?? 0) >= 2 && npc.power < 200
|
||
if (!npc.allied && (fragile || battleWorn)) {
|
||
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~8 的种子格)且几率(乱世更频)——世界会新生
|
||
const aliveCount = Object.keys(w.state.npcFamilies).length
|
||
const cap = Math.min(8, w.state.worldGen?.npcCount ?? 4)
|
||
if (aliveCount < cap && w.rng.chance(worldNum('greatNewbornChance') * (s.era === 'luanshi' ? 2 : 1))) {
|
||
spawnNewbornDynasty(w, s)
|
||
}
|
||
}
|
||
// 顶峰衰(0.1.27):绝顶不可久踞——power>=700 年首回落(豪强被制衡)
|
||
if (w.state.month === 1 && npc.power >= 700 && w.rng.chance(0.08)) {
|
||
npc.power = Math.round(npc.power * 0.9)
|
||
dyn.lastEvent = '顶峰回落'
|
||
dyn.lastEventYear = w.state.year
|
||
s.newsFeed.push({ year: w.state.year, month: 1, src: '史官', text: `${def.name} 盛极而衰,锋芒微敛。`, kind: 'annal' })
|
||
}
|
||
// 关系漂移:年首各 ±5(邻近的讲合、世仇的愈深)
|
||
for (const oid of ids) {
|
||
if (oid === id) continue
|
||
const cur = dyn.relationsWithOthers[oid] ?? 0
|
||
dyn.relationsWithOthers[oid] = clamp(cur + (w.rng.chance(0.5) ? 1 : -1) * 5, -100, 100)
|
||
}
|
||
// 0.1.40 换代评估:四模式(退隐传贤/遇害暴毙/夺位篡权/寿终正寝)
|
||
const chanceNow = dyn.leaderAge > lifespan * 0.9 ? 0.5 : 0.02
|
||
if (w.rng.chance(chanceNow) && !w.rng.chance(0.25)) {
|
||
const eraNow = (s.era ?? 'pingshi') as string
|
||
const prosperityNow = dyn.prosperity ?? 50
|
||
let mode: 'retire' | 'death' | 'usurp' | 'normal'
|
||
if (eraNow === 'luanshi' || eraNow === 'mofa') {
|
||
if (prosperityNow < 30 && w.rng.chance(0.4)) mode = 'death'
|
||
else if (npc.power > 400 && w.rng.chance(0.25)) mode = 'usurp'
|
||
else mode = 'normal'
|
||
} else {
|
||
if (prosperityNow > 70 && w.rng.chance(0.45)) mode = 'retire'
|
||
else if (npc.power > 400 && w.rng.chance(0.15)) mode = 'usurp'
|
||
else mode = 'normal'
|
||
}
|
||
switch (mode) {
|
||
case 'retire': {
|
||
// 退隐传贤:平盛世高景气——宗主功成身退,传位后辈
|
||
dyn.leaderAge = 30 + w.rng.int(0, 20)
|
||
dyn.leaderRealmIdx = Math.min(dyn.leaderRealmIdx + 1, 5)
|
||
dyn.leaderName = `${def.name.replace('氏', '')}氏新主`
|
||
dyn.prosperity = clamp(prosperityNow - 5, 8, 100)
|
||
npc.power = Math.round(Math.max(40, npc.power - 5))
|
||
dyn.lastEvent = '退隐传贤'
|
||
break
|
||
}
|
||
case 'death': {
|
||
// 遇害暴毙:乱世末法低景气——宗主陨落,群龙无首
|
||
dyn.leaderAge = 25 + w.rng.int(0, 20)
|
||
dyn.leaderRealmIdx = Math.max(0, dyn.leaderRealmIdx) // 境界不变或降
|
||
dyn.leaderName = `${def.name.replace('氏', '')}氏少主`
|
||
dyn.prosperity = clamp(prosperityNow - 20, 8, 100)
|
||
npc.power = Math.round(Math.max(40, npc.power - 30))
|
||
dyn.lastEvent = '宗主遇害'
|
||
break
|
||
}
|
||
case 'usurp': {
|
||
// 夺位篡权:高压家族——强人夺位,势力激增但人心不稳
|
||
dyn.leaderAge = 35 + w.rng.int(0, 15)
|
||
dyn.leaderRealmIdx = Math.min(dyn.leaderRealmIdx + (w.rng.chance(0.4) ? 2 : 1), 5)
|
||
dyn.leaderName = `${def.name.replace('氏', '')}氏篡主`
|
||
dyn.prosperity = clamp(prosperityNow + 8, 8, 100)
|
||
npc.power = Math.round(Math.max(40, npc.power + 40))
|
||
dyn.lastEvent = '夺位篡权'
|
||
// 篡权者树敌:与邻家关系恶化
|
||
for (const oid of ids) {
|
||
if (oid === id) continue
|
||
const cur = dyn.relationsWithOthers[oid] ?? 0
|
||
dyn.relationsWithOthers[oid] = clamp(cur - 12, -100, 100)
|
||
}
|
||
break
|
||
}
|
||
default: {
|
||
// 寿终正寝:常规换代
|
||
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])
|
||
const eventDesc = dyn.lastEvent === '退隐传贤' ? '宗主功成身退,后辈继位'
|
||
: dyn.lastEvent === '宗主遇害' ? '宗主遇害身亡,少主仓促继位'
|
||
: dyn.lastEvent === '夺位篡权' ? '强人夺位,人心浮动'
|
||
: '更易宗主,气象一新'
|
||
w.log('info', `【天下】${def.name} ${eventDesc}。`)
|
||
w.emitFx('ripple', `succession:${id}`)
|
||
}
|
||
// 0.1.40 NPC 势力分裂:power>500 且景气<25 的家族,年首 3% 概率分裂
|
||
if (w.rng.chance(0.03) && npc.power > 500 && (dyn.prosperity ?? 50) < 25 && !npc.allied) {
|
||
splitNpcDynasty(w, s, id)
|
||
}
|
||
// B7 互攻:与世仇(关系<-50)年首相搏——败方伤筋动骨
|
||
for (const oid of ids) {
|
||
if (oid === id) continue
|
||
const foe = w.state.npcFamilies[oid]
|
||
if (!foe) continue
|
||
const rel = dyn.relationsWithOthers[oid] ?? 0
|
||
const foeStance = dyn.stance ?? 'guardian'
|
||
const hateBar = foeStance === 'expand' ? -40 : foeStance === 'endure' ? -70 : -50
|
||
if (foeStance === 'ally') void hateBar
|
||
// W4 灾年全环:天下凶年群雄相噬——互攻概率 ×1.5
|
||
const foeEventChance = (s.calamity ? WORLDSIM.npcEventChance * 1.5 : WORLDSIM.npcEventChance)
|
||
if (rel < hateBar && foeStance !== 'ally' && w.rng.chance(foeEventChance)) {
|
||
// 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(52, Math.round(loser.power * 0.82))
|
||
;(s.npcDyn[loser.id] as { warLosses?: number }).warLosses = ((s.npcDyn[loser.id] as { warLosses?: number }).warLosses ?? 0) + 1
|
||
;(s.npcDyn[winner.id] as { warLosses?: number }).warLosses = 0
|
||
// 打残广播:天下敢弱必胜之——邻家对败者关系趋冷、对胜者暗生敬畏
|
||
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)
|
||
}
|
||
// 0.1.34 战争疲劳:乱象(互攻/大劫)累积——世界温度上浮,乱世加速
|
||
s.warFatigue = clamp((s.warFatigue ?? 0) + worldNum('warFatigueRise'), 0, 1)
|
||
s.newsFeed.push({
|
||
year, month: 1, src: winner.name,
|
||
text: `${winner.name} 与 ${loser.name} 起衅——痛挫其锋,势力大动。`
|
||
})
|
||
w.log('info', `【天下】${winner.name} 击破 ${loser.name},气象一新。`)
|
||
// 盟友求援:下盟友处告急,请其同盟(玩家)出手
|
||
if (loser.allied) {
|
||
s.distress = { id: loser.id, year, month: 1 }
|
||
s.newsFeed.push({
|
||
year, month: 1, src: loser.name,
|
||
text: `${loser.name} 遭${winner.name}重创,遣使来吾族求援:请解囊相助。`, kind: 'npc'
|
||
})
|
||
w.log('bad', `【天下】盟邦 ${loser.name} 遇袭告急!`)
|
||
}
|
||
break
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/** 覆灭清理:npcFamilies/npcDyn/关系网/raidCD/排队事件/distress/dynamic def/worldGen.npcs 全摘
|
||
* 0.1.39 根治:补全 worldGen.npcs + worldGen.relations 中已灭亡 NPC 的定义清理——
|
||
* 旧版 purgeNpc 遗漏了存档层数据,长跑 180 年后 worldGen.npcs 堆积 20+ 条僵尸定义,
|
||
* 直接膨胀存档体积且读档时重注册已灭亡 NPC 的 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[`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)
|
||
// 0.1.39:从 worldGen.npcs 移除已灭亡 NPC 的定义(存档体积精简——僵尸 def 不再落盘)
|
||
if (w.state.worldGen?.npcs) {
|
||
const idx = w.state.worldGen.npcs.findIndex((n) => n.id === id)
|
||
if (idx >= 0) w.state.worldGen.npcs.splice(idx, 1)
|
||
}
|
||
// 0.1.39:从 worldGen.relations 清理已灭亡 NPC 的关系条目(存档体积精简)
|
||
if (w.state.worldGen?.relations) {
|
||
delete w.state.worldGen.relations[id]
|
||
for (const rid of Object.keys(w.state.worldGen.relations)) {
|
||
delete w.state.worldGen.relations[rid]?.[id]
|
||
}
|
||
}
|
||
w.emitFx('ripple', `purge:${id}`)
|
||
}
|
||
|
||
const NEWBORN_STYLES = ['新锐剑宗', '灵植世家', '商盟豪族', '兵修门阀', '符箓仙门', '丹道世家', '阵法传承', '御兽门阀'] as const
|
||
const NEWBORN_REGIONS = ['南麓青泽', '西山雾谷', '东溪云汉', '北原古井', '中州云台', '东海浮屿'] as const
|
||
|
||
/**
|
||
* 0.1.40 NPC 势力分裂
|
||
*
|
||
* 触发条件:power>500 且景气<25 且非附庸,年首 3% 概率
|
||
* 效果:
|
||
* - 原家族 power 降 40%,景气降至 15
|
||
* - 分裂出的新家族继承 35% power + 部分关系网
|
||
* - 新家族注册为独立 NPC(registerNpcDef + npcFamilies + npcDyn)
|
||
* - 落档 worldGen.npcs 保证读档幂等
|
||
*/
|
||
function splitNpcDynasty(w: World, s: WorldSimState, parentId: string): void {
|
||
const parent = w.state.npcFamilies[parentId]
|
||
const parentDef = npcById(parentId)
|
||
const parentDyn = s.npcDyn[parentId]
|
||
if (!parent || !parentDef || !parentDyn) return
|
||
|
||
const rng = w.rng
|
||
const splitPower = Math.round(parent.power * 0.35)
|
||
parent.power = Math.round(parent.power * 0.6)
|
||
parentDyn.prosperity = 15
|
||
parentDyn.lastEvent = '势力分裂'
|
||
parentDyn.lastEventYear = w.state.year
|
||
|
||
// 分裂出的新家族
|
||
let seedHash = 0
|
||
for (let i = 0; i < w.state.seed.length; i++) seedHash = (seedHash * 31 + w.state.seed.charCodeAt(i)) >>> 0
|
||
const newId = `n-split-${(seedHash % 4096).toString(36)}-${rng.int(1, 9999999)}`
|
||
const newName = `${parent.name.slice(0, 2)}分支`
|
||
const newStyle = rng.pick([...NEWBORN_STYLES])
|
||
const newRegion = parent.region || rng.pick([...NEWBORN_REGIONS])
|
||
const newDef = {
|
||
id: newId,
|
||
name: newName,
|
||
region: newRegion,
|
||
desc: `${parentDef.name}内乱分裂而出,另立门户。`,
|
||
style: newStyle,
|
||
leaderRealm: 'foundation' as const,
|
||
initialPower: splitPower,
|
||
powerGrowth: [3, 9] as [number, number],
|
||
sells: parentDef.sells ? [...parentDef.sells] : [],
|
||
buys: parentDef.buys ? [...parentDef.buys] : ['lingcao', 'lingkuang']
|
||
}
|
||
registerNpcDef(newDef)
|
||
// 落档 worldGen.npcs
|
||
if (w.state.worldGen) {
|
||
if (!w.state.worldGen.npcs) w.state.worldGen.npcs = []
|
||
w.state.worldGen.npcs.push({
|
||
id: newDef.id, name: newDef.name, region: newDef.region, style: newDef.style,
|
||
desc: newDef.desc, leaderRealm: newDef.leaderRealm, initialPower: newDef.initialPower,
|
||
powerGrowth: newDef.powerGrowth, sells: newDef.sells, buys: newDef.buys
|
||
})
|
||
}
|
||
w.state.npcFamilies[newId] = {
|
||
id: newId, name: newName, region: newRegion,
|
||
power: splitPower, relation: 0, allied: false, raidCount: 0, declineYears: 0
|
||
}
|
||
const newDyn = initDynFor(newId)
|
||
newDyn.leaderName = `${newName.slice(0, 2)}氏少主`
|
||
newDyn.leaderAge = 25 + rng.int(0, 15)
|
||
newDyn.leaderRealmIdx = Math.max(0, parentDyn.leaderRealmIdx - 1)
|
||
newDyn.prosperity = 40
|
||
newDyn.lastEvent = '分裂自立'
|
||
newDyn.lastEventYear = w.state.year
|
||
// 继承部分关系网(与原家族的盟友关系减半、仇敌转为中立)
|
||
for (const [oid, rel] of Object.entries(parentDyn.relationsWithOthers)) {
|
||
if (oid === parentId) continue
|
||
newDyn.relationsWithOthers[oid] = rel > 0 ? Math.round(rel * 0.5) : Math.round(rel * 0.3)
|
||
// 分裂消息传至邻家:对原家族关系微降、对新家族态度不明
|
||
const otherDyn = s.npcDyn[oid]
|
||
if (otherDyn) {
|
||
otherDyn.relationsWithOthers[parentId] = clamp((otherDyn.relationsWithOthers[parentId] ?? 0) - 5, -100, 100)
|
||
otherDyn.relationsWithOthers[newId] = 0 // 中立起步
|
||
}
|
||
}
|
||
// 父子反目
|
||
newDyn.relationsWithOthers[parentId] = -40
|
||
parentDyn.relationsWithOthers[newId] = -40
|
||
s.npcDyn[newId] = newDyn
|
||
|
||
s.newsFeed.push({
|
||
year: w.state.year, month: 1, src: '史官',
|
||
text: `${parent.name} 内乱分裂——${newName} 另立门户,天下格局再变。`, kind: 'annal'
|
||
})
|
||
w.log('info', `【天下】${parent.name} 内乱分裂,${newName} 崛起自立。`)
|
||
w.emitFx('ripple', `split:${newId}`)
|
||
}
|
||
|
||
/** 新贵补位:随机风格/区域/初始 power 的新家族(乱世更频)
|
||
* 0.1.40 增强:灰烬重生模式——灭亡家族的位置有概率被新势力填补
|
||
*
|
||
* 模式选取(rng 派生):
|
||
* - 35% 灰烬重生:在已灭亡家族的原区域,以新风格重生(继承部分地缘)
|
||
* - 65% 全新初立:原逻辑(随机风格/区域)
|
||
*/
|
||
function spawnNewbornDynasty(w: World, s: WorldSimState): void {
|
||
const rng = w.rng
|
||
// 0.1.40 灰烬重生模式:检查 worldGen.npcs 中已被 purge 的家族区域
|
||
let ashesRegion: string | undefined
|
||
let ashesStyle: string | undefined
|
||
if (w.rng.chance(0.35) && w.state.worldGen?.npcs) {
|
||
// 从 worldGen.npcs 找到已不在 npcFamilies 中的灭亡家族
|
||
const fallen = w.state.worldGen.npcs.filter((n) => !w.state.npcFamilies[n.id])
|
||
if (fallen.length > 0) {
|
||
const pick = fallen[rng.int(0, fallen.length - 1)]!
|
||
ashesRegion = pick.region
|
||
// 不继承原风格——灰烬中重生的新风格
|
||
ashesStyle = rng.pick([...NEWBORN_STYLES])
|
||
}
|
||
}
|
||
const style = ashesStyle ?? rng.pick([...NEWBORN_STYLES])
|
||
const region = ashesRegion ?? rng.pick([...NEWBORN_REGIONS])
|
||
// 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, 9999999)}` // 0.1.35 A-10 碰撞窗口 9999→9999999
|
||
const name = `${region.slice(0, 2)}${NEWBORN_NAME_SUFFIX[rng.int(0, NEWBORN_NAME_SUFFIX.length - 1)]}`
|
||
const isAshes = !!ashesRegion
|
||
const def = {
|
||
id,
|
||
name,
|
||
region,
|
||
desc: isAshes
|
||
? `${style}于故地废墟上重建——前人虽去,灵脉犹存,新主收拾残局再起。`
|
||
: `${style}初立,闷头搞了十年发展,如今渐攒起一份家业。`,
|
||
style,
|
||
leaderRealm: 'foundation' as const,
|
||
initialPower: isAshes ? 90 + rng.int(0, 50) : 100 + rng.int(0, 60), // 灰烬重生起步略低
|
||
powerGrowth: [3, 9] as [number, number],
|
||
sells: [],
|
||
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,
|
||
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'
|
||
if (isAshes) {
|
||
w.log('good', `【天下】灰烬重生——${name}于${region}故地重建(${rgStyleName(def.style)}),旧地新主。`)
|
||
s.newsFeed.push({ year: w.state.year, month: 1, src: '史官', text: `灰烬重生——${name}于${region}故地重建,旧地新主。`, kind: 'annal' })
|
||
} else {
|
||
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
|
||
const prosperity = dyn.prosperity ?? 50
|
||
const power = npc?.power ?? 100
|
||
const relation = npc?.relation ?? 0
|
||
// 0.1.37 因果调制:玩家家族 karma 影响 NPC 姿态倾向(阈值可经 worldNum 覆写)
|
||
const karma = w.state.family.karma ?? 0
|
||
const kHighS = worldNum('karmaHighThreshold')
|
||
const kLowS = worldNum('karmaLowThreshold')
|
||
const karmaAllyMod = karma > kHighS ? 2 : karma < kLowS ? -1 : 0
|
||
const karmaExpandMod = karma < kLowS ? 2 : karma > kHighS ? -1 : 0
|
||
// 硬判据优先
|
||
if (prosperity < 30 || power < 45) return 'endure'
|
||
if (relation >= 50) return 'ally'
|
||
if (era === 'luanshi' && power >= 120) return 'expand'
|
||
if (era === 'mofa') return 'endure'
|
||
// 软权重:乱世偏扩张、盛世偏守成、平世均衡 + karma 调制
|
||
const W: Record<NpcStance, number> = {
|
||
guardian: 3,
|
||
expand: Math.max(0, (era === 'luanshi' ? 6 : 2) + karmaExpandMod),
|
||
endure: era === 'mofa' ? 5 : 2,
|
||
ally: Math.max(0, (era === 'shengshi' || era === 'pingshi' ? 4 : 1) + karmaAllyMod)
|
||
}
|
||
const pick = (Object.keys(W) as NpcStance[]).sort((a, b) => W[b] - W[a])
|
||
// 用幂等 rng 抽取(先取最高权)+ 30% 软翻转
|
||
const top = pick[0]!
|
||
if (w.rng.chance(0.7)) return top
|
||
return pick[1]!
|
||
}
|
||
|
||
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 ?? 'qi'),
|
||
leaderAge: 45,
|
||
lastEvent: '',
|
||
lastEventYear: -99,
|
||
relationsWithOthers: {}
|
||
}
|
||
}
|
||
|
||
function pushNews(w: World, s: WorldSimState, about: string[]): void {
|
||
const rng = w.rng
|
||
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' | 'annal' }
|
||
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,
|
||
src: def.name,
|
||
text: dyn ? `灵潮气象:${dyn.leaderName} 宗主更替,势力重排。` : `${def.name} 世务新张。`,
|
||
kind: 'npc'
|
||
}
|
||
} else {
|
||
const pool = clamp(s.marketPool[id] ?? poolBase(id), POOL_BASE[id] * 0.5, POOL_BASE[id] * 1.8)
|
||
const base = poolBase(id)
|
||
const pct = Math.round(((pool - base) / base) * 100)
|
||
const itemName = ITEMS[id]?.name ?? id
|
||
row = {
|
||
year: w.state.year,
|
||
month: w.state.month,
|
||
src: `世界·${itemName}`,
|
||
text: pct !== 0 ? `${itemName}行情${pct > 0 ? '升' : '降'}${Math.abs(pct)}% (${tide})` : `${itemName}行情平稳 (${tide})`,
|
||
itemId: id,
|
||
kind: 'quote'
|
||
}
|
||
}
|
||
s.newsFeed.push(row)
|
||
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' && !r.text.includes('态度')).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))
|
||
}
|