/** 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 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 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 = {} 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 { return this.s().secretQi } npcDyn(): Record { 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 = { 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 { 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 = { 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 = { 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)) }