import { World } from '../world' import { Character } from '../../types/domain' import { Cond, EffectDef, EventDef, EVENTS, MemberEffect } from '../../data/events' import { MAJOR_ORDER } from '../../data/realms' import { TECHNIQUES } from '../../data/techniques' 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] export function findEvent(id: string, world?: World): EventDef | undefined { return ALL_EVENTS.find((e) => e.id === id) ?? dynamicEventFor(id, world) } export function dynamicEventFor(id: string, w?: World): EventDef | undefined { if (id.startsWith('ev-raid-')) { const npcId = id.replace('ev-raid-', '') const npc = npcById(npcId) return { id, name: `${npc.name}来犯`, category: 'major', weight: 0, text: `${npc.name}与贵庄积怨已久,如今撕破脸面,遣来劫掠队围门叫战。`, options: [ { label: '迎战!', hint: '大战一场,胜则大利,败则伤财', eff: { raid: { npcId } } }, { label: '割地求和', hint: '灵石-250,关系+25', eff: { res: { stones: -250 }, relation: { [npcId]: 25 }, flag: { [npcId]: 'paid' } } }, { label: '先议和缓兵', hint: '关系+10', eff: { relation: { [npcId]: 10 } } } ] } } 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.startsWith('ev-tournament-')) { const year = Number(id.replace('ev-tournament-', '')) return { id, name: '太虚大比', category: 'major', weight: 0, text: `太虚仙盟十年一会,新一期大比于祖庭开擂。观礼之众云集,百族竞锋——可遣嫡系赴赛,亦可称病让席。`, options: [ { label: '点将赴赛', hint: '战三关,位次定声望', eff: { tournament: true, flag: { [`tournamentYear-${year}`]: true } } }, { label: '献礼买名', hint: '灵石-200,名次垫底,各族好感略升', eff: { res: { stones: -200 }, relation: { 'n-xuanying': 5, 'n-danxin': 5, 'n-sihai': 5, 'n-nulei': 5 } } }, { label: '称病不出', hint: '声望小幅受挫', eff: { rep: -3 } } ] } } if (id.startsWith('ev-apprentice-')) { const rest = id.replace('ev-apprentice-', '') const memberId = rest.split('-')[0] const member = w?.state.members[memberId] ?? undefined const sectName = member?.apprentice?.sect ?? '师门' const name = member?.name ?? '弟子' return { id, name: '寄读还乡', category: 'major', weight: 0, text: `${name} 在${sectName}寄读期满。宗门遣人传书:或归家,或续练。`, options: [ { label: '接其归家', hint: '其悟道大进,或携功法而归', eff: { flag: { apprenticeRet: memberId } } }, { label: '令其再拜师门', hint: '寄读二年,资源更厚', eff: { flag: { apprenticeStay: memberId } } } ] } } if (id === 'ev-recruit') { return { id, name: '宗门收徒', category: 'major', weight: 0, text: '远方宗门遣师来访,观族中少年灵根不俗,欲收为记名弟子寄读。', options: [ { label: '送子寄读', hint: '其人外派,归时精进', eff: { apprentice: { build: true } } }, { label: '婉拒', hint: '无', eff: {} } ] } } 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 } function asFlagString(flag: Record | undefined, key: string): string | null { if (!flag) return null const v = flag[key] return typeof v === 'string' ? v : null } import { runTournament as _runTournament, buildApprentice as _buildApprentice, finalizeApprentice as _finalizeApprentice, checkApprenticeExpiry, recruitCheck } from './tournament' export const runTournament = _runTournament export const buildApprentice = _buildApprentice export const finalizeApprentice = _finalizeApprentice export function matchesCond(w: World, cond?: Cond): boolean { if (!cond) return true const s = w.state const fam = s.family const alive = w.aliveMembers() const adults = alive.filter((c) => w.ageOf(c) >= 16) const head = s.members[fam.headId] if (cond.all && !cond.all.every((c) => matchesCond(w, c))) return false if (cond.any && !cond.any.some((c) => matchesCond(w, c))) return false if (cond.not && matchesCond(w, cond.not)) return false if (cond.minYear !== undefined && s.year < cond.minYear) return false if (cond.minGeneration !== undefined && fam.generation < cond.minGeneration) return false if (cond.minHeadRealm !== undefined && (!head || MAJOR_ORDER.indexOf(head.realm.major) < MAJOR_ORDER.indexOf(cond.minHeadRealm as never))) return false if (cond.minBuilding && (fam.buildings[cond.minBuilding.id] ?? 0) < cond.minBuilding.level) return false if (cond.minRep !== undefined && fam.reputation < cond.minRep) return false if (cond.maxRep !== undefined && fam.reputation > cond.maxRep) return false if (cond.minResource && (fam.inventory[cond.minResource.id] ?? 0) < cond.minResource.n) return false if (cond.minAdult !== undefined && adults.length < cond.minAdult) return false if (cond.maxAdult !== undefined && adults.length > cond.maxAdult) return false if (cond.minMembers !== undefined && alive.length < cond.minMembers) return false if (cond.eligibleAdult !== undefined) { const eligible = adults.filter((c) => !c.spouseId && !c.spouseHouse) if (eligible.length < cond.eligibleAdult) return false } if (cond.hasMeditation && !alive.some((c) => c.state === 'meditation')) return false if (cond.relation) { const r = s.npcFamilies[cond.relation.npcId]?.relation ?? 0 if (cond.relation.gt !== undefined && r <= cond.relation.gt) return false if (cond.relation.lt !== undefined && r >= cond.relation.lt) return false } if (cond.flag) { const v = fam.flag[cond.flag.key] if (v !== cond.flag.eq) return false } if (cond.minTechCount !== undefined && fam.techniques.length < cond.minTechCount) return false return true } export function eventRoll(w: World): void { const s = w.state if (s.pendingEvent || s.eventQueue.length > 0) { if (!s.pendingEvent && s.eventQueue.length > 0) { fire(w, s.eventQueue.shift()!) } 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 } checkApprenticeExpiry(w) recruitCheck(w) // 五年一会的太虚大比(该年任一时刻) const tourneyNext = Math.ceil(s.year / 5) * 5 if (s.year >= 10 && s.year === tourneyNext && !s.completedEvents.includes(`ev-tournament-${tourneyNext}`)) { fire(w, `ev-tournament-${tourneyNext}`) 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 const candidates = ALL_EVENTS.filter( (e) => e.category === category && !(e.once && s.completedEvents.includes(e.id)) && matchesCond(w, e.cond) ) if (candidates.length === 0) return const total = candidates.reduce((a, e) => a + e.weight, 0) let r = w.rng.next() * total for (const e of candidates) { r -= e.weight if (r <= 0) { fire(w, e.id) return } } } export function fire(w: World, id: string): void { w.state.pendingEvent = id w.pendingEvent(id) } export function applyEventChoice(w: World, eventId: string, optionIdx: number, squad?: string[]): void { const s = w.state const def = findEvent(eventId, w) if (!def) { s.pendingEvent = undefined return } const opt = def.options[optionIdx] if (opt) { applyEffect(w, opt.eff, squad) if (def.once && !s.completedEvents.includes(def.id)) s.completedEvents.push(def.id) } s.pendingEvent = undefined } export type OptionSquadPicker = (w: World) => { pool: string[]; initial: string[] } // ---------------- effects ---------------- function pickMember(w: World, spec: MemberEffect): Character | Character[] { const alive = w.aliveMembers() if (alive.length === 0) return [] const byTarget = (t: string): Character[] => { const sorted = [...alive] switch (t) { case 'random': return [w.rng.pick(sorted)] case 'head': { const h = w.state.members[w.state.family.headId] return h && h.alive ? [h] : [] } case 'youngest': return [sorted.sort((a, b) => w.ageOf(a) - w.ageOf(b))[0]] case 'oldest': return [sorted.sort((a, b) => w.ageOf(b) - w.ageOf(a))[0]] case 'highestPerception': return [sorted.sort((a, b) => b.perception - a.perception)[0]] case 'highestPower': return [sorted.sort((a, b) => rankPower(w, b) - rankPower(w, a))[0]] case 'highestFortune': return [sorted.sort((a, b) => b.fortune - a.fortune)[0]] case 'all': return sorted default: return [w.rng.pick(sorted)] } } return byTarget(spec.target) } export function rankPower(w: World, c: Character): number { const order = ['mortal', 'qi', 'foundation', 'core', 'nascent', 'spirit'] return order.indexOf(c.realm.major) * 10 + c.realm.minor } function applyEffect(w: World, eff: EffectDef, squad?: string[]): void { const s = w.state const fam = s.family if (eff.res) { for (const [k, v] of Object.entries(eff.res)) { if (k === 'stones') fam.stones = Math.max(0, fam.stones + v) else fam.inventory[k] = Math.max(0, (fam.inventory[k] ?? 0) + v) } } if (eff.pillGain) { for (const [k, v] of Object.entries(eff.pillGain)) fam.inventory[k] = (fam.inventory[k] ?? 0) + v } if (eff.rep) { fam.reputation += eff.rep if (Math.abs(eff.rep) >= 4) w.log(eff.rep > 0 ? 'good' : 'bad', `家族声望${eff.rep > 0 ? '上升' : '下跌'}${Math.abs(eff.rep)}。`) } if (eff.relation) { for (const [k, v] of Object.entries(eff.relation)) { const npc = s.npcFamilies[k] if (npc) npc.relation = Math.max(-100, Math.min(100, npc.relation + v)) } } if (eff.addBuilding && !fam.buildings[eff.addBuilding]) { fam.buildings[eff.addBuilding] = 1 } if (eff.flag) { Object.assign(fam.flag, eff.flag) } if (eff.techniqueChance && w.rng.chance(eff.techniqueChance)) { const t = w.rng.pick(TECHNIQUES) if (!fam.techniques.includes(t.id)) { fam.techniques.push(t.id) w.log('good', `得《${t.name}》残篇,录入藏书阁。`) } } if (eff.artifactChance && w.rng.chance(eff.artifactChance)) { const a = w.rng.pick(['weapon-fan', 'weapon-qi', 'weapon-ling']) fam.inventory[a] = (fam.inventory[a] ?? 0) + 1 w.log('good', '库中多了一件法器。') } if (eff.addTech) { if (!fam.techniques.includes(eff.addTech)) fam.techniques.push(eff.addTech) } if (eff.tournament) { runTournament(w) } if (eff.apprentice?.build) { buildApprentice(w) } const retId = asFlagString(eff.flag, 'apprenticeRet') const stayId = asFlagString(eff.flag, 'apprenticeStay') if (retId) { finalizeApprentice(w, retId, 'return') delete fam.flag['apprenticeRet'] } if (stayId) { finalizeApprentice(w, stayId, 'stay') delete fam.flag['apprenticeStay'] } 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 const n = eff.memberBy.n ?? 1 const list = Array.isArray(targets) ? targets : [targets] for (const c of list) { if (!c.alive) continue switch (by) { case 'exp': c.realmProgress = Math.min(100, c.realmProgress + n) w.log('info', `${c.name} 感悟顿生,修为精进。`) break case 'wound': c.health = Math.max(1, c.health - 20 - n) if (c.health < 35) c.state = 'wounded' w.log('bad', `${c.name} 因此事负伤。`) break case 'heal': c.health = Math.min(100, c.health + 20) break case 'breakthrough': c.realmProgress = 100 break case 'fatal': { if (w.rng.chance(0.35)) { c.alive = false c.deathYear = s.year c.deathCause = '遭遇不测' w.chronicle('death', `${c.name} 突遭不测,殒命于家宅之内。`, c.id, true) } else { c.health = Math.max(1, c.health - 60) c.state = 'wounded' } break } case 'repGain': fam.reputation += 2 break case 'inspire': c.realmProgress = Math.min(100, c.realmProgress + n) break case 'loot': c.fortune = Math.min(12, c.fortune + n) break case 'madness': c.mind = Math.max(1, c.mind - 1) c.health = Math.max(30, c.health - 10) break case 'genius': c.perception = Math.min(10, c.perception + 1) c.mind = Math.min(10, c.mind + 1) break } } } if (eff.mission) { const def = MISSIONS.find((m) => m.id === eff.mission) if (def) { const squad = w .aliveMembers() .filter((c) => w.ageOf(c) >= 16 && c.state !== 'expedition' && c.realm.major !== 'mortal') .sort((a, b) => rankPower(w, b) - rankPower(w, a)) .slice(0, def.maxMembers) if (squad.length >= def.minMembers) { sendMission(w, def.id, squad.map((c) => c.id)) w.log('info', `家族闻讯而动,遣人奔赴【${def.name}】。`) } } } if (eff.raid) { const npc = s.npcFamilies[eff.raid.npcId] if (npc) { let team = w .aliveMembers() .filter((c) => w.ageOf(c) >= 16 && c.state !== 'expedition') .sort((a, b) => rankPower(w, b) - rankPower(w, a)) .slice(0, 4) if (squad && squad.length > 0) { team = squad.map((id) => w.memberById(id)).filter((c) => c.alive && c.state !== 'expedition' && w.ageOf(c) >= 16) } if (team.length > 0) { resolveRaid(w, eff.raid.npcId, team) fam.flag[`raidCD-${eff.raid.npcId}`] = s.year } } } }