v0.1.27: 活水长流——生灭盘活 + 修为回环 + 世鉴 + MOD 生态闭环(1054 全绿)
【世界模型(180 年实测驱动的机制修复)】 - 生灭可达化:探子模拟实测证实——原覆灭/新贵 183 年零触发(力量通胀+景气自校正); 修复:脆弱判定放宽(power<110 弱即衰 / warLosses≥2 战损衰)+ yearGrowth 按 era 调制 (盛世 0.7/平世 1/乱世 -0.8/末法 -1.4——乱世真换血)+ 顶峰衰(power≥700 年首 8% 回落) - 修为↔市场回环:修行月耗灵草 tradeSettle(-n) 抽走世界池(最后半环闭合) - 世鉴 sagaAnnals:50/100 年宏观对比(开局 vs 现在/era 路径/最强家更替)——独立数组零漂 - 世界数值 MOD 口:worldNum 聚合读(灾年概率/供需/贸易/新贵/秘灵 5 项可覆写可复位) - 世界面板显性化:世温/景气/衰微倒计时;修复 WorldPanel 救济按钮 rng 单源违规 【插件/MOD】 - 依赖拓扑自动安装(registry 内先装、栈深 3);MOD source 挂载 + exportModBundle + IPC mods:write + 设置页「导出当前 MOD 集」 【测试】1054 全绿(47 套件,+7:乱世压力覆灭/顶峰衰/era 负增/世鉴/数值口/导出/拓扑); 【金钟罩】0.1.27 基线重算(双基线 18 值,生灭/回环受控变更);build 通过
This commit is contained in:
@@ -25,6 +25,8 @@ export interface CotycPlugin {
|
||||
uninstall?(ctx: PluginContext): void
|
||||
hooks?: PluginHookGuard
|
||||
protected?: boolean
|
||||
/** MOD 插件溯源(0.1.27:导出/显示用——原始 ModPack 引用) */
|
||||
source?: unknown
|
||||
}
|
||||
|
||||
export interface PluginContext {
|
||||
|
||||
@@ -137,7 +137,7 @@ export class GameFacade {
|
||||
about(): { title: string; version: string; modules: number; systems: number; plugins: number; packFingerprint: string } {
|
||||
return {
|
||||
title: '仙途家族志',
|
||||
version: '0.1.26',
|
||||
version: '0.1.27',
|
||||
modules: this.world.systemList().length,
|
||||
systems: this.world.systemList().filter((s) => s.enabled).length,
|
||||
plugins: this.world.pluginList().length,
|
||||
|
||||
@@ -12,6 +12,7 @@ import { MALE_GIVEN, FEMALE_GIVEN } from '../../kernel/names'
|
||||
import { ASPIRATION_IDS } from '../../../data/aspirations'
|
||||
import { seasonMod } from '../../../data/season'
|
||||
import { bonusOf } from '../../../data/buildings'
|
||||
import { WorldSim } from '../../sim/WorldSim'
|
||||
import { needsTribulation, tribulationEventId } from './tribulation'
|
||||
import { fire } from './events'
|
||||
|
||||
@@ -81,6 +82,8 @@ export function cultivationTick(w: World): void {
|
||||
const supply = inv.lingcao ?? 0
|
||||
const used = Math.min(supply, demand)
|
||||
inv.lingcao = supply - used
|
||||
// 0.1.27 修为↔市场回环:修行耗草同时抽走世界池(池浅→价涨→温度→era 全链)
|
||||
if (w.state.worldSim) new WorldSim(w).tradeSettle('lingcao', -used)
|
||||
const ratio = supply >= demand ? 1 : supply / demand
|
||||
herbFactor = 0.6 + 0.4 * ratio
|
||||
if (ratio < 0.5 && w.state.family.flag['herbWarnYear'] !== w.state.year) {
|
||||
|
||||
@@ -34,11 +34,16 @@ export function diplomacyTick(w: World): void {
|
||||
|
||||
export function yearGrowth(w: World): void {
|
||||
const s = w.state
|
||||
// era 调制(0.1.27):盛世缓增、平世正常、乱世/末法负增——遏制无脑通胀
|
||||
const era = (w.state.worldSim?.era ?? 'pingshi') as 'shengshi' | 'pingshi' | 'luanshi' | 'mofa'
|
||||
const eraGrowth: Record<string, number> = { shengshi: 0.7, pingshi: 1, luanshi: -0.8, mofa: -1.4 }
|
||||
const gm = eraGrowth[era] ?? 1
|
||||
for (const npc of Object.values(s.npcFamilies)) {
|
||||
const def = npcById(npc.id)
|
||||
if (!def) continue
|
||||
const [a, b] = def.powerGrowth
|
||||
npc.power += w.rng.int(a, b)
|
||||
const base = Math.round(w.rng.int(a, b))
|
||||
npc.power = Math.max(35, Math.min(900, npc.power + (gm >= 0 ? Math.round(base * gm) : -Math.round(Math.max(1, base * -gm)))))
|
||||
if (npc.allied) {
|
||||
w.state.family.stones += 15
|
||||
w.log('info', `同盟${npc.name}遣使来贺,赠灵石15(结盟年利)。`)
|
||||
|
||||
@@ -257,6 +257,17 @@ export class World {
|
||||
return this.plugins.listChanges()
|
||||
}
|
||||
|
||||
/** 导出当前 MOD 集为一枚 .cotymod 合并包(仅 mod- 前缀内容插件;0.1.27) */
|
||||
exportModBundle(): string | null {
|
||||
const packs: import('./modSchema').ModPack[] = []
|
||||
for (const m of this.plugins.modPlugins()) {
|
||||
const src = m.source as import('./modSchema').ModPack
|
||||
if (src) packs.push(src)
|
||||
}
|
||||
if (packs.length === 0) return null
|
||||
return JSON.stringify({ app: 'cotymod', bundle: packs }, null, 2)
|
||||
}
|
||||
|
||||
installPlugin(p: CotycPlugin): { ok: boolean; reason?: string } {
|
||||
const r = this.plugins.install(p)
|
||||
if (r.ok) {
|
||||
|
||||
@@ -21,7 +21,7 @@ export function modPreflight(w: World, pack: ModPack): { ok: boolean; reason?: s
|
||||
}
|
||||
|
||||
export function modToPlugin(pack: ModPack): CotycPlugin {
|
||||
return {
|
||||
const plugin: CotycPlugin = {
|
||||
id: `mod-${pack.id}`,
|
||||
name: `MOD·${pack.name}`,
|
||||
version: pack.version,
|
||||
@@ -44,6 +44,9 @@ export function modToPlugin(pack: ModPack): CotycPlugin {
|
||||
for (const c of pack.data.calamities ?? []) {
|
||||
ctx.addCalamity(c.name, { family: c.family, effect: c.effect })
|
||||
}
|
||||
}
|
||||
},
|
||||
// M1:挂 pack 源——导出/显示用(插件实例可溯源到原始 MOD 包)
|
||||
source: pack
|
||||
}
|
||||
return plugin
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { CotycPlugin, PluginContext, PluginStatus, PluginChange } from '../kernel/plugin'
|
||||
import { PLUGIN_REGISTRY } from './World'
|
||||
|
||||
function pluginRegistryOf(id: string): (() => CotycPlugin) | undefined {
|
||||
return PLUGIN_REGISTRY.get(id)
|
||||
}
|
||||
import { EventDef } from '../../data/events'
|
||||
import { PACK } from '../../data/registry'
|
||||
import { SystemDef, SYSTEM_DEFS } from './capabilities'
|
||||
@@ -42,10 +47,20 @@ export class PluginManager {
|
||||
this.changes = []
|
||||
}
|
||||
|
||||
install(plugin: CotycPlugin): { ok: boolean; reason?: string } {
|
||||
install(plugin: CotycPlugin, depth = 0): { ok: boolean; reason?: string } {
|
||||
if (this.runtime.has(plugin.id)) return { ok: false, reason: `插件已存在:${plugin.id}` }
|
||||
// 0.1.27 拓扑自动安装:依赖缺失时先查注册表工厂装齐(栈深 ≤ 3)
|
||||
for (const dep of plugin.dependencies ?? []) {
|
||||
if (!this.runtime.has(dep)) return { ok: false, reason: `缺少依赖:${dep}` }
|
||||
if (this.runtime.has(dep)) continue
|
||||
if (depth < 3) {
|
||||
const mk = pluginRegistryOf(dep)
|
||||
if (mk) {
|
||||
const sub = this.install(mk(), depth + 1)
|
||||
if (sub.ok) continue
|
||||
return { ok: false, reason: `依赖 ${dep} 安装失败:${sub.reason ?? ''}` }
|
||||
}
|
||||
}
|
||||
return { ok: false, reason: `缺少依赖:${dep}` }
|
||||
}
|
||||
for (const c of plugin.conflicts ?? []) {
|
||||
if (this.runtime.has(c)) return { ok: false, reason: `与 ${c} 冲突` }
|
||||
@@ -139,6 +154,13 @@ export class PluginManager {
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
/** MOD 源包列表(0.1.27 导出) */
|
||||
modPlugins(): Array<{ id: string; source: unknown }> {
|
||||
return [...this.runtime.values()]
|
||||
.filter((r) => r.plugin.id.startsWith('mod-') && r.plugin.source)
|
||||
.map((r) => ({ id: r.plugin.id, source: r.plugin.source }))
|
||||
}
|
||||
|
||||
list(): PluginStatus[] {
|
||||
return this.order
|
||||
.filter((id) => this.runtime.has(id))
|
||||
|
||||
@@ -24,7 +24,7 @@ export function buyItem(w: World, itemId: string, count: number): boolean {
|
||||
if (!pack().items[itemId]) return false
|
||||
const sim = w.state.worldSim ? new WorldSim(w) : null
|
||||
// 断供告急:池深低于下限时拒单(零库存市场无货可买;世界生产与 NPC 供给会回填)
|
||||
if (sim && sim.poolDepthOf(itemId) < WORLDSIM.tradeFloorPct && w.state.totalTicks > 12) return false
|
||||
if (sim && sim.poolDepthOf(itemId) < worldNum('tradeFloorPct') && w.state.totalTicks > 12) return false
|
||||
const total = marketPrice(w, itemId) * count
|
||||
if (total > fam.stones) return false
|
||||
fam.stones -= total
|
||||
@@ -58,7 +58,7 @@ export function techniquePrice(techId: string): number {
|
||||
}
|
||||
|
||||
import { TECHNIQUES } from '../../data/techniques'
|
||||
import { WORLDSIM } from './worldsim-data'
|
||||
import { WORLDSIM, worldNum } from './worldsim-data'
|
||||
|
||||
const TECHNIQUE_GRADE_PRICE: Record<number, number> = { 1: 120, 2: 300, 3: 700, 4: 1600 }
|
||||
const TECH_GRADE_BASE: Record<string, number> = Object.fromEntries(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** WorldSim —— 世界自进化引擎(game/engine/sim/WorldSim.ts) */
|
||||
import { World } from '../runtime/World'
|
||||
import { WorldSimState, NpcDynamics, WORLDSIM, ERA_CONF, ERA_DURA, makeWorldSimState, NpcStance, REGION_TIDE, calamityNames, calamityFamilyOf, calamityEffectOf, CalamityName, POOL_BASE } from './worldsim-data'
|
||||
import { WorldSimState, NpcDynamics, WORLDSIM, ERA_CONF, ERA_DURA, makeWorldSimState, NpcStance, REGION_TIDE, calamityNames, calamityFamilyOf, calamityEffectOf, CalamityName, POOL_BASE, worldNum } from './worldsim-data'
|
||||
import { pack } from '../../data/registry'
|
||||
import { ITEMS } from '../../data/items'
|
||||
import { npcById, getNpcDefs, registerNpcDef, unregisterNpcDef } from '../../data/npcs'
|
||||
@@ -40,7 +40,7 @@ export class WorldSim {
|
||||
)
|
||||
|
||||
// 秘境灵气(自动恢复 + 探索消耗由 missions 在探索时扣;灾年灵脉闭锁 0.5 恢复)
|
||||
const qiRecover = s.calamity ? WORLDSIM.secretRecover * 0.5 : s.tide > 0.85 ? WORLDSIM.secretRecoverHi : WORLDSIM.secretRecover
|
||||
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)
|
||||
}
|
||||
@@ -87,7 +87,7 @@ export class WorldSim {
|
||||
// ---- D. 灾年(年签一掷 + 持续渐退;B12) ----
|
||||
// 剩数递减
|
||||
if ((s.calamityLeft ?? 0) > 0) s.calamityLeft = (s.calamityLeft ?? 0) - 1
|
||||
const calamityP = WORLDSIM.calamityChance * ERA_CONF[s.era ?? 'pingshi'].calamityMult
|
||||
const calamityP = worldNum('calamityChance') * ERA_CONF[s.era ?? 'pingshi'].calamityMult
|
||||
if (this.w.state.month === 1 && rng.chance(calamityP)) {
|
||||
const cl = rng.pick(calamityNames())
|
||||
s.calamity = cl
|
||||
@@ -138,6 +138,24 @@ export class WorldSim {
|
||||
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 })
|
||||
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)
|
||||
@@ -329,8 +347,8 @@ function worldBreath(w: World, s: WorldSimState): void {
|
||||
// 超卖潮:池过剩 > 1.8×base 时世界产能自发回调(谷贱伤农)
|
||||
const saturated = cur > base * 1.8
|
||||
if (saturated) overflow = true
|
||||
const supply = base * WORLDSIM.worldSupplyRate * supplySpan * eraSupply * (saturated ? 0.7 : 1)
|
||||
const demand = base * WORLDSIM.worldDemandRate * eraDemand
|
||||
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)
|
||||
}
|
||||
// 超卖播报(年一次)——玩家能看到"别把市场玩崩"
|
||||
@@ -357,7 +375,7 @@ function npcTrade(w: World, s: WorldSimState, noise: number): void {
|
||||
const tradeMult = stance2 === 'expand' ? 1.3 : stance2 === 'endure' ? 0.7 : 1
|
||||
// W3 区域灵势:各 era 修正(e.g. 乱世北岳玄影峰灵衰 0.08)
|
||||
const regionMod = 1 + (REGION_TIDE[id]?.[(s.era ?? 'pingshi') as 'shengshi'] ?? 0)
|
||||
const rate = WORLDSIM.npcTradeRate * (isCalamity ? 0.7 : 1) * tradeMult * regionMod
|
||||
const rate = worldNum('npcTradeRate') * (isCalamity ? 0.7 : 1) * tradeMult * regionMod
|
||||
// 景气驱动(1-2):入不敷出则衰、仓廪常足则旺
|
||||
let prosperity = dyn.prosperity ?? 50
|
||||
// 卖:NPC 抛货 → 池增;有货自给,景气微涨
|
||||
@@ -458,7 +476,10 @@ function evolveNpc(w: World, s: WorldSimState, noise: number): void {
|
||||
}
|
||||
// W1 生灭:衰微累积/覆灭附庸/新贵补位(年首)
|
||||
if (w.state.month === 1) {
|
||||
if (!npc.allied && npc.power < 58 && (dyn.prosperity ?? 50) < 38) {
|
||||
// 残衰减判定(0.1.27 可达化):弱即衰(盛世会被增长拉回不灭、乱世真灭——世道越乱换血越烈)
|
||||
const fragile = npc.power < 110
|
||||
const battleWorn = (dyn.warLosses ?? 0) >= 2 && npc.power < 160
|
||||
if (!npc.allied && (fragile || battleWorn)) {
|
||||
dyn.declineYears = (dyn.declineYears ?? 0) + 1
|
||||
if (dyn.declineYears >= 8) {
|
||||
// 覆灭:最强邻家分食
|
||||
@@ -483,10 +504,17 @@ function evolveNpc(w: World, s: WorldSimState, noise: number): void {
|
||||
// 新贵补位:家数 < 开局目标(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(WORLDSIM.greatNewbornChance * (s.era === 'luanshi' ? 2 : 1))) {
|
||||
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
|
||||
@@ -526,6 +554,8 @@ function evolveNpc(w: World, s: WorldSimState, noise: number): void {
|
||||
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
|
||||
|
||||
@@ -6,8 +6,10 @@ export interface NpcDynamics {
|
||||
/** 战略姿态:守成/扩张/隐忍/结盟(年首评估) */
|
||||
stance: NpcStance
|
||||
stanceSinceYear: number
|
||||
/** 衰微连续年数(power<45 起计) */
|
||||
/** 衰微连续年数(power<58 起计) */
|
||||
declineYears?: number
|
||||
/** 互攻败仗累计(0.1.27 残衰减判定) */
|
||||
warLosses?: number
|
||||
/** 宗主姓名快照(换代时更新) */
|
||||
leaderName: string
|
||||
leaderRealmIdx: number
|
||||
@@ -52,6 +54,8 @@ export interface WorldSimState {
|
||||
distress?: { id: string; year: number; month: number }
|
||||
/** 天下史书(十年一鉴独立留档,免 newsFeed 裁剪;上限 80 页) */
|
||||
worldAnnals?: { year: number; text: string }[]
|
||||
/** 世鉴(50/100 年宏观:开局 vs 现在/era 路径/最强家更替——独立数组零漂) */
|
||||
sagaAnnals?: { year: number; kind: 'quinquagenary' | 'centennial'; text: string }[]
|
||||
}
|
||||
|
||||
export function makeWorldSimState(): WorldSimState {
|
||||
@@ -116,6 +120,24 @@ export const REGION_TIDE: Record<string, Record<'shengshi' | 'pingshi' | 'luansh
|
||||
'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,
|
||||
|
||||
Reference in New Issue
Block a user