v0.1.2: 宗族气韵(职事/悟道/谱系/碑录/庆典/飞升/音效)
- 职事任命:长老(+修炼)/供奉(+战力)/执事(+坊市)/掌教(+闭关) 每人加成叠加,上限管理 - 功法悟道:修习积悟性点,小成/大成逐级+战力,大境界突破传道于直系晚辈 - 宗族谱系页:世代分带 + 夫妻同卷 + 子孙连枝,点节点开详情,先人簿可查 - 功德碑:宗主辞世即勒石,宗祠页碑录永存 - 百年庆典 + 飞升之路事件链(留世坐镇/仙风入体 或 云游飞升/宗族蒙荫) - 合成音效(WebAudio):鼓点/钟鸣/纸韵/磬响,设置页开关 - 单测 29→38;typecheck/build 通过;GUI 冒烟受限于 WSLg 掉线(SMOKE-TIMEOUT 优雅退出)
This commit is contained in:
@@ -15,13 +15,20 @@ export function monthlyRate(w: World, c: Character): number {
|
||||
rate *= 0.5 + c.perception * 0.1
|
||||
rate *= ROOT_GRADES[c.roots.grade]?.expBonus ?? 0.5
|
||||
const tech = techniqueById(c.techniqueId)
|
||||
if (tech && c.realm.major !== 'mortal') rate *= 1 + tech.expBonus
|
||||
else if (c.realm.major !== 'mortal') rate *= 0.65
|
||||
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
|
||||
const juling = buildings['juling'] ?? 0
|
||||
rate *= 1 + juling * 0.05
|
||||
rate *= 1 + w.postBonus('expAll')
|
||||
if (st.family.flag['fengFeiBless']) rate *= 1.05
|
||||
if (c.traits.includes('fengxian')) rate *= 1.3
|
||||
if (c.state === 'meditation') {
|
||||
rate *= 1.35
|
||||
rate *= 1 + w.postBonus('meditation')
|
||||
const dongfu = buildings['dongfu'] ?? 0
|
||||
rate *= 1 + dongfu * 0.08
|
||||
} else if (c.state === 'expedition') {
|
||||
@@ -41,6 +48,25 @@ export function cultivationTick(w: World): void {
|
||||
const rate = monthlyRate(w, c)
|
||||
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))) {
|
||||
@@ -50,6 +76,25 @@ export function cultivationTick(w: World): void {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -73,12 +118,25 @@ export function resolveBreakthrough(w: World, c: Character, boost: number): void
|
||||
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}!`)
|
||||
if (next.major === 'spirit') {
|
||||
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))
|
||||
|
||||
@@ -7,6 +7,7 @@ import { MISSIONS } from '../../data/secrets'
|
||||
import { npcById } from '../../data/npcs'
|
||||
import { resolveRaid } from './combat'
|
||||
import { sendMission } from './missions'
|
||||
import { findInheritor } from '../creation'
|
||||
|
||||
const ALL_EVENTS: EventDef[] = [...EVENTS]
|
||||
|
||||
@@ -31,6 +32,35 @@ export function dynamicEventFor(id: string): EventDef | undefined {
|
||||
]
|
||||
}
|
||||
}
|
||||
if (id === 'ev-centennial') {
|
||||
return {
|
||||
id,
|
||||
name: '百年庆典',
|
||||
category: 'fate',
|
||||
weight: 0,
|
||||
once: true,
|
||||
text: '百年元辰已至:这百年间,苗裔繁衍、香火未断。大宴宾客?还是告天祭祖?',
|
||||
options: [
|
||||
{ label: '大开宴席,张灯结彩', hint: '灵石-200,声望+15,全族心境大悦', eff: { res: { stones: -200 }, rep: 15, memberBy: { by: 'inspire', target: 'all', n: 4 } } },
|
||||
{ label: '设坛告天', hint: '灵石-100,声望+10', eff: { res: { stones: -100 }, rep: 10, flag: { centennial: 'rite' } } },
|
||||
{ label: '阖家简庆', hint: '声望+5', eff: { rep: 5 } }
|
||||
]
|
||||
}
|
||||
}
|
||||
if (id === 'ev-feisheng') {
|
||||
return {
|
||||
id,
|
||||
name: '飞升之路',
|
||||
category: 'fate',
|
||||
weight: 0,
|
||||
once: true,
|
||||
text: '化神之上本为天堑,如今族中已有人触碰仙门。飞升之路敞开:宗族该作何抉择?',
|
||||
options: [
|
||||
{ label: '留仙坐镇', hint: '他留世镇族:天赋大盛', eff: { feisheng: { stay: true } } },
|
||||
{ label: '放仙飞升', hint: '他云游而去寻仙门:宗族蒙荫', eff: { feisheng: { stay: false } } }
|
||||
]
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -81,6 +111,18 @@ export function eventRoll(w: World): void {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 里程碑检测(优先于日常事件)
|
||||
if (s.year >= 100 && !s.completedEvents.includes('ev-centennial')) {
|
||||
fire(w, 'ev-centennial')
|
||||
return
|
||||
}
|
||||
const firstSpirit = s.flags['firstSpirit'] as number | undefined
|
||||
if (firstSpirit && s.year - firstSpirit >= 2 && s.year - firstSpirit <= 3 && !s.completedEvents.includes('ev-feisheng')) {
|
||||
fire(w, 'ev-feisheng')
|
||||
return
|
||||
}
|
||||
|
||||
const roll = w.rng.next()
|
||||
const category: 'daily' | 'major' | 'fate' | undefined = roll < 0.5 ? 'daily' : roll < 0.78 ? 'major' : roll < 0.86 ? 'fate' : undefined
|
||||
if (!category) return
|
||||
@@ -206,6 +248,30 @@ function applyEffect(w: World, eff: EffectDef, squad?: string[]): void {
|
||||
if (eff.addTech) {
|
||||
if (!fam.techniques.includes(eff.addTech)) fam.techniques.push(eff.addTech)
|
||||
}
|
||||
if (eff.feisheng) {
|
||||
const s2 = w.state
|
||||
const immortal = w
|
||||
.aliveMembers()
|
||||
.sort((a, b) => rankPower(w, b) - rankPower(w, a))[0]
|
||||
if (immortal) {
|
||||
if (eff.feisheng.stay) {
|
||||
if (!immortal.traits.includes('fengxian')) immortal.traits.push('fengxian')
|
||||
w.chronicle('event', `${immortal.name} 谢绝仙门征召,愿镇守此世,护佑宗族万世。`, immortal.id, true)
|
||||
w.log('good', `${immortal.name} 留世坐镇(仙风入体)。`)
|
||||
} else {
|
||||
immortal.alive = false
|
||||
immortal.deathYear = s2.year
|
||||
immortal.deathCause = '云游飞升'
|
||||
s2.family.flag['fengFeiBless'] = true
|
||||
w.chronicle('event', `${immortal.name} 于月首孤身东去,驾鹤而飞。天地留一缕仙风,庇佑宗族。`, immortal.id, true)
|
||||
w.log('good', `${immortal.name} 飞升而去,宗族蒙庇。`)
|
||||
if (s2.family.headId === immortal.id) {
|
||||
const heir = findInheritor(w)
|
||||
if (heir) w.assignHead(heir.id, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (eff.memberBy) {
|
||||
const targets = pickMember(w, eff.memberBy)
|
||||
const by = eff.memberBy.by
|
||||
|
||||
@@ -32,7 +32,7 @@ export function productionTick(w: World): void {
|
||||
parts.push(`灵矿+${v}灵矿`)
|
||||
}
|
||||
if (fangshi > 0) {
|
||||
const v = 55 * fangshi
|
||||
const v = Math.round(55 * fangshi * (1 + w.postBonus('marketIncome')))
|
||||
fam.stones += v
|
||||
parts.push(`坊市+${v}灵石`)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from '../types/domain'
|
||||
import { Rng } from '../core/rng'
|
||||
import { BUILDINGS } from '../data/buildings'
|
||||
import { POSTS } from '../data/posts'
|
||||
import { productionTick } from './systems/production'
|
||||
import { deathTick, woundHealTick } from './systems/lifecycle'
|
||||
import { cultivationTick, resolveBreakthrough } from './systems/cultivation'
|
||||
@@ -128,6 +129,7 @@ export class World {
|
||||
|
||||
private yearStart(): void {
|
||||
yearStartMarriage(this)
|
||||
this.state.family.reputation += this.postBonus('familyRep')
|
||||
const rep = this.state.family.reputation
|
||||
const power = this.familyPower()
|
||||
const report: YearlyReport = {
|
||||
@@ -164,9 +166,21 @@ export class World {
|
||||
if (!headId) return
|
||||
const head = this.memberById(headId)
|
||||
if (head.alive) return
|
||||
// 功德碑:宗主薨,勒石纪功
|
||||
const reignStart = (s.family.flag['reignStart'] as number | undefined) ?? 1
|
||||
const reignYears = Math.max(1, s.year - reignStart)
|
||||
const peak = s.family.reputation
|
||||
const top = this.aliveMembers().length
|
||||
this.chronicle(
|
||||
'misc',
|
||||
`功德碑:先主${head.name}承宗${reignYears}载,宗族声望达「${peak}」、丁口${top}。族人勒石铭功,立于宗祠。`,
|
||||
head.id,
|
||||
true
|
||||
)
|
||||
const heir = findInheritor(this)
|
||||
if (heir) {
|
||||
this.assignHead(heir.id, true)
|
||||
s.family.flag['reignStart'] = s.year
|
||||
} else if (this.aliveMembers().length === 0) {
|
||||
this.gameOver('满门凋零,香火断绝', s.year)
|
||||
}
|
||||
@@ -186,6 +200,7 @@ export class World {
|
||||
}
|
||||
c.isHead = true
|
||||
this.state.family.headId = id
|
||||
this.state.family.flag['reignStart'] = this.state.year
|
||||
if (!silent) {
|
||||
this.chronicle('misc', `${c.name} 继任为家主。`, c.id, true)
|
||||
this.log('info', `${c.name} 继任为家主。`)
|
||||
@@ -308,9 +323,40 @@ export class World {
|
||||
s.members[c.id] = c
|
||||
}
|
||||
|
||||
postCount(def: string): number {
|
||||
return this.aliveMembers().filter((c) => c.post === def).length
|
||||
}
|
||||
|
||||
assignPost(memberId: Id, postId: string | undefined): boolean {
|
||||
const c = this.memberById(memberId)
|
||||
if (!c.alive) return false
|
||||
if (postId === undefined || postId === '') {
|
||||
c.post = undefined
|
||||
return true
|
||||
}
|
||||
const def = POSTS[postId]
|
||||
if (!def || def.id === 'head') return false
|
||||
if (this.postCount(postId) >= def.max) return false
|
||||
c.post = postId
|
||||
return true
|
||||
}
|
||||
|
||||
postBonus(type: string): number {
|
||||
let sum = 0
|
||||
for (const c of this.aliveMembers()) {
|
||||
const def = POSTS[c.post ?? '']
|
||||
if (def && def.effect.type === type) sum += def.effect.value
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
familyPower(): number {
|
||||
const fam = this.state.family
|
||||
const bonus = 1 + (fam.buildings['yanwu'] ?? 0) * 0.04 + (fam.buildings['lingshou'] ?? 0) * 0.05
|
||||
const bonus =
|
||||
1 +
|
||||
(fam.buildings['yanwu'] ?? 0) * 0.04 +
|
||||
(fam.buildings['lingshou'] ?? 0) * 0.05 +
|
||||
this.postBonus('battlePower')
|
||||
const top = this.aliveMembers()
|
||||
.map((c) => combatPowerOf(this, c))
|
||||
.sort((a, b) => b - a)
|
||||
|
||||
Reference in New Issue
Block a user