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) }