Files
ChronicleOfTheImmortalClan/src/renderer/game/engine/runtime/Systems/cultivation.ts
T
thzxx be6f2f5fe7 v0.1.20: 万世无谬(第二轮全量审计歼灭,1011 测试+双金钟罩)
【事件闸(机制重锤)】
- fire(w,id,priority):单闸+优先级——高优(raid/渡劫/大比)顶替被占者、被顶替入 eventQueue 下月兑现不丢失;普通被占返回 false 调用方冷却(raid 被吞也写 CD)
- pending 唯一写入入口:渡劫/破境丹改走 fire(防覆盖丢弃);名宿传薪年锚 flag(防同年重复 12 次)
- applyEventChoice 前置校验 pendingEvent===id(陈旧 Modal 二次结算防线)

【世界模型四修(真·活世界)】
- era 延续档:flow 权重和<1(留白=续任)——盛世能撑 30 年而非固定 25
- 潮汐供给符号修正:灵涨万物丰(原实现反了)+tide 双界 clamp
- driftMarket 弱锚定:rebalance 0.1→0.025——供需/era/灾年真实撬动价格(原五池被锚死±5%)
- 断供/池底单常量 poolFloorPct=0.35(消灭禁买暗雷带)

【知识防线(审计虫洞修复)】
- 指纹扩展:era/stance/prosperity/relationsWithOthers/calamityLeft/pendingEvent 全入 hash
- 第二金钟罩 GOLDEN_RESOLVED:自动 resolve 长跑锁“现实”世界(原指纹锁的只是“事件流冻结”世界)

【性能与存储】
- 存档节流(疾12/常6/缓4,手动即时)+ saveChronicle O(N)→O(1) 单查增量写(2160 月档告别每 tick 数千 OPFS)
- battles≤220/chronicle≤520 截断(stringify 线性膨胀止血);startNewGame/openState 停 timer 摘旧 bus

【杂项】actDirect 对齐 ACT_CATALOG 全 24 项;about 三版本统一 0.1.20;kernel.bus 死总线删;seed 改 RngHub.rollSeed;normalize 补 eraStartYear/overflux/distress;WorldPanel 买卖条件反转修复;音效与视效同闸

【测试】1011 全绿(42 套件):audit-0.1.20 9 例(事件闸/传薪/二次结算/era延续/潮汐符号/断供一致/弱锚定/指纹敏感)
2026-08-23 15:14:28 +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'
import { fire } from './events'
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
fire(w, tribulationEventId(c), 1)
} 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)
}