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

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

【测试】1002 全绿(40 套件):fxqueue-semantic 3 例 + era-world 5 例;
金钟罩 0.1.18 基线固化(era/prosperity/蝴蝶效应受控变更)
2026-08-23 14:38:27 +08:00

254 lines
10 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.
import type { World } from '../World'
import { Character } from '../../../types/domain'
import { ROOT_GRADES } from '../../../data/elements'
import { masteryRateOfMajor } from '../../../data/pacing'
import { aspirationById, fitBonusOf } from '../../../data/aspirations'
import { techniqueById } from '../../../data/techniques'
import { nextRealm, breakthroughBaseChance, realmDeathChance, describeRealm, MAJOR_ORDER } from '../../../data/realms'
import { lifespanOf } from './lifecycle'
import { traitBonuses } from '../pcgen'
import { newCharacter } from '../pcgen'
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 { needsTribulation, tribulationEventId } from './tribulation'
export function monthlyRate(w: World, c: Character, herbFactor = 1): number {
const st = w.state
let rate = herbFactor
rate *= 1.0 + c.perception * 0.18
rate *= ROOT_GRADES[c.roots.grade]?.expBonus ?? 0.5
const tech = techniqueById(c.techniqueId)
if (tech && c.realm.major !== 'mortal') {
rate *= 1 + tech.expBonus + (c.techniqueRank ?? 0) * 0.05
} else if (c.realm.major !== 'mortal') {
rate *= 0.65
}
const buildings = st.family.buildings
rate *= 1 + bonusOf('juling', 'expBonus', buildings['juling'] ?? 0)
if (w.sysEnabled('season')) rate *= 1 + seasonMod(st.month, 'cult')
rate *= 1 + w.postBonus('expAll')
if (st.family.flag['fengFeiBless']) rate *= 1.05
if (c.traits.includes('fengxian')) rate *= 1.3
const aspiration = aspirationById(c.aspiration)
if (aspiration?.effect.type === 'cult') rate *= 1 + aspiration.effect.value
if (fitBonusOf(c).cult) rate *= 1.06
if (c.state === 'meditation') {
rate *= 1.35
rate *= 1 + w.postBonus('meditation') + (w.sysEnabled('season') ? seasonMod(st.month, 'meditation') : 0)
rate *= 1 + bonusOf('dongfu', 'expBonus', buildings['dongfu'] ?? 0)
} else if (c.state === 'expedition') {
rate *= 0.25
} else if (c.state === 'wounded') {
rate *= c.health > 40 ? 0.5 : 0.15
}
// 带伤不愈者难以静修(与状态无关的直观削率)
if (c.alive && c.state !== 'wounded') {
if (c.health <= 40) rate *= 0.45
else if (c.health <= 70) rate *= 0.8
}
rate *= traitBonuses(c).exp
if (w.ageOf(c) < 8) rate *= 0.4
if (w.ageOf(c) > 65) rate *= 0.72
rate *= masteryRateOfMajor(c.realm.major)
// 世界灵气潮汐
const tide = w.state.worldSim?.tide ?? 1
rate *= tide
return rate
}
export function cultivationTick(w: World): void {
// 圣者护族:家族有 60+ 且超过筑基者时,幼儿修行门槛更低
const hasPatriarch = w.aliveMembers().some((c) => w.ageOf(c) >= 60 && c.realm.major !== 'mortal' && c.realm.major !== 'qi')
// 成年礼定志(16-18 岁首次)
for (const c of Object.values(w.state.members)) {
if (c.alive && !c.aspiration && w.ageOf(c) >= 16 && w.ageOf(c) <= 19) {
c.aspiration = w.rng.pick(ASPIRATION_IDS)
}
}
// ---- A1 修为经济化:灵草是修行燃料 ----
const inv = w.state.family.inventory
let demand = 0
for (const c of Object.values(w.state.members)) {
if (!c.alive || c.state === 'apprentice') continue
if (c.realm.major === 'mortal') continue
demand += c.state === 'meditation' ? 2 : 1
}
let herbFactor = 1
if (demand > 0) {
const supply = inv.lingcao ?? 0
const used = Math.min(supply, demand)
inv.lingcao = supply - 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) {
w.state.family.flag['herbWarnYear'] = w.state.year
w.log('bad', '灵草告罄,族中修行渐缓——需扩灵田药园或购草补续。')
}
}
for (const c of Object.values(w.state.members)) {
if (!c.alive || c.state === 'apprentice') continue
const rate = monthlyRate(w, c, herbFactor)
if (rate <= 0) continue
c.realmProgress = Math.min(100, c.realmProgress + rate)
// 悟道进度:有功法且修为之外,另积一分慧根
if (c.techniqueId && c.realm.major !== 'mortal') {
const rank = c.techniqueRank ?? 0
const gain = rate * (rank === 0 ? 0.5 : rank === 1 ? 0.3 : 0.15)
c.techniqueProgress = Math.min(100, (c.techniqueProgress ?? 0) + gain)
if (c.techniqueProgress >= 100) {
if (rank === 0) {
c.techniqueRank = 1
c.techniqueProgress = 0
w.log('good', `✦ ${c.name} 参悟《${techniqueName(c.techniqueId)}》小成,战力精进。`)
w.chronicle('breakthrough', `${c.name} 参悟《${techniqueName(c.techniqueId)}》小成。`, c.id, false)
} else if (rank === 1) {
c.techniqueRank = 2
c.techniqueProgress = 0
w.log('good', `✦ ${c.name} 于《${techniqueName(c.techniqueId)}》上再进一层,臻至大成!`)
w.chronicle('breakthrough', `${c.name} 将《${techniqueName(c.techniqueId)}》精修大成。`, c.id, true)
}
}
}
if (c.realmProgress >= 100) {
const months = (w.state.year * 12 + w.state.month) - (c.lastBreakthroughAttempt ?? -999)
if (months >= 6 && w.rng.chance(perAttemptChance(w, c))) {
if (needsTribulation(c) && w.sysEnabled('tribulation')) {
// 已在等待渡劫(pending 未清)或压制期内,不再重设
if (w.state.pendingEvent === tribulationEventId(c)) {
c.realmProgress = 100
continue
}
if (c.tribDelayYear && w.state.year < c.tribDelayYear) {
c.realmProgress = 100
continue
}
c.tribDelayYear = undefined
w.pendingEvent(tribulationEventId(c))
w.state.pendingEvent = tribulationEventId(c)
} else {
resolveBreakthrough(w, c, 0)
}
}
}
}
}
function techniqueName(id: string | undefined): string {
const t = techniqueById(id)
return t ? t.name : '无名功法'
}
function closestDisciple(w: World, c: Character): Character | null {
const heirs = c.children
.map((id) => w.state.members[id])
.filter((ch): ch is Character => !!ch?.alive && !!ch.techniqueId)
.sort((a, b) => rankScore(b) - rankScore(a))
if (heirs.length > 0) return heirs[0]
return null
}
function rankScore(c: Character): number {
const order = ['mortal', 'qi', 'foundation', 'core', 'nascent', 'spirit']
return order.indexOf(c.realm.major) * 10 + c.realm.minor
}
export function perAttemptChance(w: World, c: Character): number {
const base = breakthroughBaseChance(c.realm)
const mind = c.mind * 0.008
const traits = traitBonuses(c)
const headMind = (w.state.members[w.state.family.headId]?.mind ?? 5) * 0.004
const healthMod = c.health > 60 ? 0.04 : -0.06
const bless = w.state.family.flag['headBless'] ? 0.03 : 0
return Math.min(0.95, Math.max(0.02, base + mind + traits.breakBonus + headMind + healthMod + bless))
}
export function resolveBreakthrough(w: World, c: Character, boost: number): void {
if (!c.alive || c.realmProgress < 100) return
const next = nextRealm(c.realm)
if (!next) return
const p = Math.min(0.95, Math.max(0.05, perAttemptChance(w, c) + boost))
c.lastBreakthroughAttempt = w.state.year * 12 + w.state.month
if (w.rng.chance(p)) {
const majorJump = next.major !== c.realm.major
c.realm = next
c.realmProgress = 0
if (majorJump) {
c.health = 100
// 悟道传承:大境界圆满者,将心得传于同枝晚辈
const apprentice = closestDisciple(w, c)
if (apprentice) {
const cur = apprentice.techniqueProgress ?? 0
if (cur < 60) {
apprentice.techniqueProgress = Math.max(cur, 60)
w.log('info', `${apprentice.name} 承得 ${c.name} 破境感悟,修行一日千里。`)
}
}
}
const desc = describeRealm(next)
w.chronicle('breakthrough', `${c.name} 突破至【${desc}】。`, c.id, majorJump)
w.log('good', `✦ ${c.name} 突破到 ${desc}`)
w.emitFx('spark', `break:${c.id}`)
if (next.major === 'spirit') {
w.emitFx('pulse', `spirit:${c.id}`)
w.chronicle('breakthrough', `华夏震惊:${c.name} 踏入化神之列。`, c.id, true)
if (!w.state.flags['firstSpirit']) {
w.state.flags['firstSpirit'] = w.state.year
w.log('bad', `天地异象:族中第一位化神出世,仙路大门洞开。`)
}
}
} else {
c.health = Math.max(1, c.health - 8 - w.rng.int(0, 10))
let log = `${c.name} 冲击瓶颈失败,灵力紊乱受创。`
if (c.traits.includes('jizao') || c.traits.includes('yiqi')) {
c.health = Math.max(1, c.health - 14)
log = `${c.name} 强行突破遭反噬,气息受创。`
}
const majorIdx = MAJOR_ORDER.indexOf(c.realm.major)
if (majorIdx >= 3 && w.rng.chance(realmDeathChance(c.realm, c.mind))) {
c.alive = false
c.deathYear = w.state.year
c.deathCause = '突破走火'
w.chronicle('death', `${c.name} 妄图冲击瓶颈,走火入魔而陨。`, c.id, true)
w.log('bad', `☠ ${c.name} 突破走火,当场陨落。`)
return
}
if (c.health < 20) c.state = 'wounded'
const loss = 26 + w.rng.int(0, 18)
c.realmProgress = Math.max(0, Math.min(95, 100 - loss - boost * 60))
w.log('bad', log)
}
}
export function produceOffspring(
w: World,
opts: {
father: Character | null
mother: Character | null
generation: number
bornYear: number
surname: string
spouseHouse?: string
}
): Character {
const rng = w.rng
const newborn = newCharacter(rng, {
name: `${opts.surname}${rng.pick(rng.chance(0.52) ? MALE_GIVEN : FEMALE_GIVEN)}`,
gender: rng.chance(0.52) ? 'male' : 'female',
generation: opts.generation,
bornYear: opts.bornYear,
age: 0,
realm: { major: 'mortal', minor: 0 },
father: opts.father ?? undefined,
mother: opts.mother ?? undefined
})
if (opts.spouseHouse) newborn.spouseHouse = opts.spouseHouse
return newborn
}
export function lifespanCheckPoint(w: World, c: Character): number {
return lifespanOf(w, c)
}