Files
ChronicleOfTheImmortalClan/src/renderer/game/engine/sim/worldsim-data.ts
T

401 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
export type NpcStance = 'guardian' | 'expand' | 'endure' | 'ally'
export interface NpcDynamics {
/** 景气指数(0~100):月结余/断供受挫/灾年支出;驱动 power 微调 */
prosperity: number
/** 战略姿态:守成/扩张/隐忍/结盟(年首评估) */
stance: NpcStance
stanceSinceYear: number
/** 衰微连续年数(power<58 起计) */
declineYears?: number
/** 互攻败仗累计(0.1.27 残衰减判定) */
warLosses?: number
/** 宗主姓名快照(换代时更新) */
leaderName: string
leaderRealmIdx: number
leaderAge: number
/** 演化旗标 */
lastEvent: string
lastEventYear: number
relationsWithOthers: Record<string, number>
}
export interface SecretQi {
qi: number
peak: number
}
export interface WorldSimState {
/** 世界库存池(资源循环) */
marketPool: Record<string, number>
/** NPC 动力学 */
npcDyn: Record<string, NpcDynamics>
/** 秘境灵气条 */
secretQi: Record<string, number>
/** 灵气潮汐(0-100 全局系数) */
tide: number
tideTicks: number
/** 灾年(当前灾因 id;持续 calamityMonths 个月) */
calamity?: string
calamityYear: number
/** 灾年剩余月数(递减,归 0 时散去) */
calamityLeft?: number
/** 天下快讯(滚动 N=120itemId/kind 供 UI 响应按钮与区分) */
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
/** 超卖潮播报年份(谷贱伤农,一年一次) */
overfluxYear?: number
/** 盟友求援(最近一次:互攻失利波及盟我之族) */
distress?: { id: string; year: number; month: number }
/** 战争疲劳(0~1):NPC 互攻/玩家 raid 逐年累积——世界温度上浮,乱世加速 */
warFatigue?: number
/** 断供监测:货源id→连续低于警戒线月数(≥3 触发断供告警×1.5 价) */
shortage?: Record<string, number>
/** 断供已播年(id→年——仅始发广播,持续断供不刷屏) */
shortageWarned?: Record<string, number>
/** 0.1.34 新秘境定义(世界状态内——每档独立,绝无全局跨世界污染) */
wonders?: Record<string, unknown>
/** 天下史书(十年一鉴独立留档,免 newsFeed 裁剪;上限 80 页) */
worldAnnals?: { year: number; text: string }[]
/** 世鉴(50/100 年宏观:开局 vs 现在/era 路径/最强家更替——独立数组零漂) */
sagaAnnals?: { year: number; kind: 'quinquagenary' | 'centennial'; text: string }[]
/** 0.1.37 世界奇观(当前活跃奇观;年首种子派生判定——零 rng 消耗) */
wonder?: WorldWonder
}
export function makeWorldSimState(): WorldSimState {
return {
marketPool: { lingcao: 600, lingkuang: 300, beastcore: 80, 'pill-qiyuan': 90, 'pill-ningyuan': 40 },
npcDyn: {},
secretQi: {},
tide: 0.5,
tideTicks: 0,
calamityYear: -99,
calamityLeft: 0,
newsFeed: [],
lastNewsMonth: -99,
npcSuccessions: 0,
era: 'pingshi',
eraStartYear: 1,
overfluxYear: -99,
worldAnnals: [],
warFatigue: 0,
shortage: {},
shortageWarned: {},
wonders: {}
}
}
/** 市场基准库存(单一权威:WorldSim 再平衡 / Market 行情 / brief 图标 三方共用) */
export const POOL_BASE: Record<string, number> = {
lingcao: 600,
lingkuang: 300,
beastcore: 80,
lingyu: 120,
lingmu: 220,
shoupi: 160,
lingguo: 260,
'pill-qiyuan': 90,
'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.45], ['luanshi', 0.15]] },
pingshi: { name: '平世', desc: '四海无波,仙凡相安。', calamityMult: 1.0, supplyMult: 1.0, demandMult: 1.0, auctionMult: 1.0, tideBias: 0, flow: [['shengshi', 0.22], ['luanshi', 0.28], ['mofa', 0.1]] },
luanshi: { name: '乱世', desc: '群雄相逐,烽烟四起,灾祸连年。', calamityMult: 1.6, supplyMult: 0.85, demandMult: 1.05, auctionMult: 0.75, tideBias: -0.04, flow: [['pingshi', 0.25], ['mofa', 0.35]] },
mofa: { name: '末法', desc: '灵机衰微,大能隐迹,仙路将绝。', calamityMult: 1.2, supplyMult: 0.7, demandMult: 0.85, auctionMult: 0.6, tideBias: -0.09, flow: [['pingshi', 0.4], ['shengshi', 0.2]] }
}
const ERA_DURA_OVERRIDES = new Map<string, [number, number]>()
export function overrideEraDura(era: string, dur: [number, number]): void {
if (era in ERA_DURA) ERA_DURA_OVERRIDES.set(era, dur)
}
export function resetEraDura(era?: string): void {
if (era) ERA_DURA_OVERRIDES.delete(era)
else ERA_DURA_OVERRIDES.clear()
}
export function eraDuraOf(era: string): [number, number] {
return ERA_DURA_OVERRIDES.get(era) ?? ERA_DURA[era as keyof typeof ERA_DURA] ?? [20, 40]
}
/** era 持续年数区间(转移时机) */
export const ERA_DURA: Record<'shengshi' | 'pingshi' | 'luanshi' | 'mofa', [number, number]> = {
shengshi: [25, 45],
pingshi: [20, 40],
luanshi: [15, 30],
mofa: [20, 40]
}
const REGION_TIDE_OVERRIDES = new Map<string, Record<string, number>>()
export function overrideRegionTide(npcId: string, byEra: Record<string, number>): void {
REGION_TIDE_OVERRIDES.set(npcId, byEra)
}
export function resetRegionTide(npcId?: string): void {
if (npcId) REGION_TIDE_OVERRIDES.delete(npcId)
else REGION_TIDE_OVERRIDES.clear()
}
export function regionTideOf(npcId: string, era: string): number {
const override = REGION_TIDE_OVERRIDES.get(npcId)?.[era]
if (override !== undefined) return override
const base = REGION_TIDE[npcId]
if (!base) return 0
return base[era as keyof typeof base] ?? 0
}
/** 区域灵势(NPC id × era → 修正量 +-0.06~0.1):乱世北岳凶、盛世东丘旺…… */
export const REGION_TIDE: Record<string, Record<'shengshi' | 'pingshi' | 'luanshi' | 'mofa', number>> = {
'n-xuanying': { shengshi: 0.06, pingshi: 0, luanshi: -0.08, mofa: -0.06 },
'n-danxin': { shengshi: 0.04, pingshi: 0, luanshi: -0.04, mofa: -0.02 },
'n-sihai': { shengshi: 0.05, pingshi: 0.01, luanshi: -0.06, mofa: -0.04 },
'n-nulei': { shengshi: 0.02, pingshi: 0, luanshi: 0.05, mofa: -0.03 }
}
/** 0.1.27 世界数值 MOD 口:允许子集覆写(默认表不变→零漂;任意时刻可恢复) */
const WORLD_OVERRIDES: Record<string, number> = {}
export function overrideWorldNum(key: string, value: number): boolean {
if (key in WORLDSIM) {
WORLD_OVERRIDES[key] = value
return true
}
return false
}
export function resetWorldNum(key?: string): void {
if (key) delete WORLD_OVERRIDES[key]
else for (const k of Object.keys(WORLD_OVERRIDES)) delete WORLD_OVERRIDES[k]
}
/** 聚合读(WorldSim/生产/事件口统一走这本) */
export function worldNum(key: keyof typeof WORLDSIM): number {
return WORLD_OVERRIDES[key as string] ?? ((WORLDSIM as unknown as Record<string, number>)[key as string] ?? 0)
}
/** 世界演化参数表(全部可调) */
export const WORLDSIM = {
marketDriftRate: 0.03,
marketRebalance: 0.025, // 0.1.20 弱锚定:让供需/era/灾年真实驱动价格(0.1.19 为 0.1 硬锚死)
priceFloor: 0.55,
priceCeil: 2.3,
// —— 0.1.17 真库存循环 ——
worldSupplyRate: 0.02, // 世界侧月供给(base 比例;潮涨时 ×1.3)
worldDemandRate: 0.015, // 世界侧月需求(坊市/宗门常驻消耗)
npcTradeRate: 0.008, // 每 NPC 月贸易量(base 比例;上桌食量)
tradeFloorPct: 0.35, // 拒单线:池低于此比例时玩家买入拒单(断供告急)
poolFloorPct: 0.25, // driftMarket 池底(0.1.35 调低——地板与拒单线分离,断供期间真买不到料)
tradeCeilPct: 2.4, // 池上限(玩家大量卖出后价格封顶)
tideSupplySpan: 0.3, // 潮汐对供给的浮动幅度(tide±0.35 时 ×(1∓0.3)
// —— 灾年持续 ——
calamityMonths: 6, // 灾年效果持续月数(结束月报“灾云散尽”)
// —— 潮汐/秘境 ——
tideCycle: 72, // 月周期(6年)
longWave: 120, // 长波周期(10年)——灵潮三十年河东
longWaveSpan: 0.08, // 长波振幅(±0.08 叠加)
tideMin: 0.65,
tideMax: 1.35,
secretRecover: 2, // 灵气月恢复(潮高 ×2.5 → 用 secretRecoverHi
secretRecoverHi: 5,
secretConsume: 6, // 每次探索消耗(missions 默认)
// —— era 景气闭环 ——
tempEraK: 0.8, // 世界温度对 era 转移权重的调制强度(±0.8/单位温差)
// —— 新贵补位 ——
greatNewbornChance: 0.06, // 年首新贵出现率(乱世 ×2;上限 4 家)
// —— 盟约连坐(加性) ——
allianceGriefRaid: 0.012, // 每 世仇盟连加性 raid 概率
// —— 天下事件 ——
calamityChance: 0.18,
calamities: ['旱灾', '涝灾', '蝗灾', '疫病', '兽潮', '寒潮'] as const,
npcEventChance: 0.05, // NPC 互攻年首诗签概率
newsEvery: 24, // 月
newsKeep: 120,
// —— 0.1.34 万象归元(战争疲劳/断供/新秘境) ——
warFatigueRise: 0.008, // 每次乱象(NPC 互攻)累积的战争疲劳
warFatigueTempK: 0.6, // 疲劳对世界温度的冷降幅度(打不动即荒年)
shortageLine: 0.7, // 断供警戒线:池/基准 < 此值起计连续月
shortageMonths: 3,
shortagePrice: 1.5, // 断供价乘
newSecretChance: 0.04, // 年首新秘境现身概率(乱世 ×2)
// —— 0.1.37 因果/奇观 ——
wonderChance: 0.08, // 世界奇观年首现身概率(种子派生——零 rng 消耗)
karmaDecayRate: 0.1, // 功德年首衰减率(向中性靠拢——善行需持续)
karmaHighThreshold: 30, // 功德深厚阈值(触发灾年 -15%/渡劫 +5%)
karmaLowThreshold: -20 // 功德亏缺阈值(触发灾年 +15%/渡劫 -5%)
}
export type CalamityName = string
// ==================== 0.1.37 世界奇观 ====================
/** 世界奇观类型(年首种子派生判定——零 rng 消耗) */
export type WorldWonderKind = 'tide_surge' | 'ancient_ruin' | 'heaven_bless' | 'qi_wane' | string
export interface WorldWonder {
kind: WorldWonderKind
year: number
/** 持续月数(灵气涌潮/灵脉枯竭有持续效果;古遗迹/天降祥瑞一次性) */
duration?: number
/** 已过月数 */
elapsed?: number
}
/** 世界奇观年首现身概率(聚合读口——MOD 可经 worldNum('wonderChance') 覆写) */
export const WONDER_CHANCE = 0.08
/** 奇观名称映射 */
export const WONDER_NAMES: Record<WorldWonderKind, string> = {
tide_surge: '灵气涌潮',
ancient_ruin: '古遗迹现身',
heaven_bless: '天降祥瑞',
qi_wane: '灵脉枯竭'
}
/** 奇观描述 */
export const WONDER_DESCS: Record<WorldWonderKind, string> = {
tide_surge: '天地灵气如潮涌至,万灵共鸣——修炼事半功倍。',
ancient_ruin: '远古修士洞府现世——其中或藏功法丹材之珍。',
heaven_bless: '祥瑞降世——风调雨顺,灾厄消弭。',
qi_wane: '灵脉逆行——天地灵气骤衰,修行维艰。'
}
/** 奇观持续月数(灵气涌潮/灵脉枯竭持续 12 月;古遗迹/天降祥瑞一次性) */
export const WONDER_DURATION: Record<WorldWonderKind, number> = {
tide_surge: 12,
ancient_ruin: 0,
heaven_bless: 0,
qi_wane: 12
}
/** 默认灾因集(聚合读口:默认+MOD/插件扩展——默认表和不变 → 基准世界指纹不漂) */
export const DEFAULT_CALAMITIES = ['旱灾', '涝灾', '蝗灾', '疫病', '兽潮', '寒潮'] as const
/** 灾因 → 家族实际生产乘量(1 = 正常) */
export const CALAMITY_FAMILY: Record<string, Partial<Record<string, number>>> = {
旱灾: { lingcao: 0.7 },
涝灾: { lingcao: 0.8, lingkuang: 0.95 },
蝗灾: { lingcao: 0.55 },
疫病: {},
兽潮: { beastcore: 1.4 },
寒潮: { lingcao: 0.85, lingkuang: 0.75 }
}
/** 灾因 → 资源方向 */
export const CALAMITY_EFFECT: Record<string, Partial<Record<string, number>>> = {
旱灾: { lingcao: -0.3 },
涝灾: { lingcao: -0.2, lingkuang: -0.1 },
蝗灾: { lingcao: -0.45 },
疫病: { beastcore: -0.25 },
兽潮: { beastcore: 0.3 },
寒潮: { lingcao: -0.2, beastcore: -0.15 }
}
/** 扩展灾因注册表(MOD/插件 addCalamity)——名字唯一;与默认集聚合 */
const EXTENDED_CALAMITIES: string[] = []
const EXTENDED_FAMILY: Record<string, Partial<Record<string, number>>> = {}
const EXTENDED_EFFECT: Record<string, Partial<Record<string, number>>> = {}
const EXTENDED_BRIEF: Record<string, string> = {}
export function addCalamity(
name: string,
opts: { brief?: string; family?: Partial<Record<string, number>>; effect?: Partial<Record<string, number>> }
): boolean {
if (DEFAULT_CALAMITIES.includes(name as never) || EXTENDED_CALAMITIES.includes(name)) return false
EXTENDED_CALAMITIES.push(name)
if (opts.brief) EXTENDED_BRIEF[name] = opts.brief
if (opts.family) EXTENDED_FAMILY[name] = opts.family
if (opts.effect) EXTENDED_EFFECT[name] = opts.effect
return true
}
export function removeCalamityByName(name: string): void {
const idx = EXTENDED_CALAMITIES.indexOf(name)
if (idx >= 0) EXTENDED_CALAMITIES.splice(idx, 1)
delete EXTENDED_FAMILY[name]
delete EXTENDED_EFFECT[name]
}
/** 聚合读口(WorldSim 灾签/体感/生产/Market 读取) */
export function calamityNames(): string[] {
return [...DEFAULT_CALAMITIES, ...EXTENDED_CALAMITIES]
}
export function calamityFamilyOf(name: string): Partial<Record<string, number>> {
return EXTENDED_FAMILY[name] ?? CALAMITY_FAMILY[name] ?? {}
}
export function calamityEffectOf(name: string): Partial<Record<string, number>> {
return EXTENDED_EFFECT[name] ?? CALAMITY_EFFECT[name] ?? {}
}
/** 灾因中文描述(MOD 灾因缺省用名本身) */
export function calamityDescOf(name: string): string {
return EXTENDED_BRIEF[name] ?? `${name}——灵植减产,市价将行。`
}
// ==================== 0.1.37 奇观类型扩展注册表(MOD 口) ====================
/** 扩展奇观类型定义(MOD 可注入自定义奇观——零 rng 消耗,种子派生选取) */
export interface ExtendedWonderDef {
/** 奇观类型 id(唯一——与内置不冲突) */
kind: string
/** 中文名称 */
name: string
/** 描述文本 */
desc: string
/** 持续月数(0 = 一次性) */
duration: number
/** 持续效果类型:tide_up = 灵气增益 / tide_down = 灵气衰减 / none = 无持续效果 */
effect: 'tide_up' | 'tide_down' | 'none'
/** 一次性效果:bless = 灾年消弭+声望 / loot = 资源获取 / none = 无 */
oneshot: 'bless' | 'loot' | 'none'
}
const EXTENDED_WONDERS: ExtendedWonderDef[] = []
/** 内置奇观定义(供聚合读口统一访问) */
const BUILTIN_WONDERS: ExtendedWonderDef[] = [
{ kind: 'tide_surge', name: '灵气涌潮', desc: '天地灵气如潮涌至,万灵共鸣——修炼事半功倍。', duration: 12, effect: 'tide_up', oneshot: 'none' },
{ kind: 'ancient_ruin', name: '古遗迹现身', desc: '远古修士洞府现世——其中或藏功法丹材之珍。', duration: 0, effect: 'none', oneshot: 'loot' },
{ kind: 'heaven_bless', name: '天降祥瑞', desc: '祥瑞降世——风调雨顺,灾厄消弭。', duration: 0, effect: 'none', oneshot: 'bless' },
{ kind: 'qi_wane', name: '灵脉枯竭', desc: '灵脉逆行——天地灵气骤衰,修行维艰。', duration: 12, effect: 'tide_down', oneshot: 'none' }
]
/** MOD/插件注册奇观类型(kind 唯一——与内置+已注册不冲突) */
export function addWonderType(def: Omit<ExtendedWonderDef, 'name' | 'desc' | 'duration' | 'effect' | 'oneshot'> & Partial<Omit<ExtendedWonderDef, 'kind'>>): boolean {
if (!def.kind || BUILTIN_WONDERS.some((w) => w.kind === def.kind) || EXTENDED_WONDERS.some((w) => w.kind === def.kind)) return false
EXTENDED_WONDERS.push({
kind: def.kind,
name: def.name ?? '异变',
desc: def.desc ?? '天地异变,灵机暗涌。',
duration: def.duration ?? 0,
effect: def.effect ?? 'none',
oneshot: def.oneshot ?? 'none'
})
return true
}
/** 卸载时按 kind 摘除扩展奇观 */
export function removeWonderType(kind: string): void {
const idx = EXTENDED_WONDERS.findIndex((w) => w.kind === kind)
if (idx >= 0) EXTENDED_WONDERS.splice(idx, 1)
}
/** 聚合读口:全部奇观类型(内置 + 扩展) */
export function allWonderKinds(): ExtendedWonderDef[] {
return [...BUILTIN_WONDERS, ...EXTENDED_WONDERS]
}
/** 聚合读口:按 kind 查奇观定义 */
export function wonderDefOf(kind: string): ExtendedWonderDef | undefined {
return BUILTIN_WONDERS.find((w) => w.kind === kind) ?? EXTENDED_WONDERS.find((w) => w.kind === kind)
}