v0.1.8: 万物皆是插件 + 全应用审计修复(60+ 缺陷歼灭)

插件协议:
- CotycPlugin Manifest(dependencies/conflicts/protected/install/uninstall)
- PluginManager:安装序确定性/依赖拒绝/异常回滚/追踪型上下文(卸载全摘钩子)
- 三核心插件常驻(Systems/Data/Events);插件清单/安装移除/启停广播
- EventPoolRegistry 多池合并;facade about/query('plugins')/plugin 订阅
- 设置页插件清单(含卸载按钮、核心保护);examplePlugin 开发范本入仓

全应用审计修复(引擎 39 + UI/主进程 40 双路清单,P0→P2 歼灭):
- P0:单亲 rollRoots 崩/插件事件软锁(findEvent 查池)/fire 覆盖幸存事件/
  facade 未赋值(act 目录 6/24 生效→全量接通)/packOverride 死实现真接管/
  4 能力卡无接线(trib/apprentice/tournament/season)/读档 rowid 方言崩溃/
  buildApprentice 空候选崩/插件安装无回滚/trait 加成从未消费/DEV 双跑破坏时序
- P1:定时器幸存/advance in-flight 闸/battle 暂停/tournament 点将真传/
  史书 memo 依赖/面板 hooks 顺序违规/远征 squad 残留 id/跨档状态残留/
  toast 自清除/读档错误提示/导入后重载世界/pendingEvent 读档回填
- P2:版本三处统一/NewGame 随机收编/学费错误文案/姓氏池去重/
  功法品阶错位(凡阶哨兵)/功法定价对齐/market 白名单校验/
  战利功法去重/回声漏封缄/clock 迭代快照/core 事件池保护/
  渡劫陨落走真突破/寄读 desc 诚实化/doas 死代码清理等 30 项

测试 239→256;金钟罩复验(防御批零漂移)
This commit is contained in:
Hermes Agent
2026-08-23 09:58:15 +08:00
parent 624e30498b
commit 967ef1240d
39 changed files with 932 additions and 157 deletions
@@ -27,7 +27,7 @@ export function monthlyRate(w: World, c: Character): number {
const buildings = st.family.buildings
const juling = buildings['juling'] ?? 0
rate *= 1 + juling * 0.05
rate *= 1 + seasonMod(st.month, 'cult')
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
@@ -36,7 +36,7 @@ export function monthlyRate(w: World, c: Character): number {
if (fitBonusOf(c).cult) rate *= 1.06
if (c.state === 'meditation') {
rate *= 1.35
rate *= 1 + w.postBonus('meditation') + seasonMod(st.month, 'meditation')
rate *= 1 + w.postBonus('meditation') + (w.sysEnabled('season') ? seasonMod(st.month, 'meditation') : 0)
const dongfu = buildings['dongfu'] ?? 0
rate *= 1 + dongfu * 0.08
} else if (c.state === 'expedition') {
@@ -49,6 +49,7 @@ export function monthlyRate(w: World, c: Character): number {
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) > 55) rate *= 0.7
rate *= masteryRateOfMajor(c.realm.major)
@@ -89,7 +90,7 @@ export function cultivationTick(w: World): void {
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)) {
if (needsTribulation(c) && w.sysEnabled('tribulation')) {
// 大境界晋升改走渡劫事件(玩家三选);压制者待来年
if (!c.tribDelayYear || w.state.year >= c.tribDelayYear) {
c.tribDelayYear = undefined
+20 -10
View File
@@ -15,7 +15,13 @@ import { nextRealm, describeRealm } from '../../data/realms'
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)
const found = ALL_EVENTS.find((e) => e.id === id)
if (found) return found
if (world) {
const pooled = world.allEvents().find((e) => e.id === id)
if (pooled) return pooled
}
return dynamicEventFor(id, world)
}
export function dynamicEventFor(id: string, w?: World): EventDef | undefined {
@@ -57,7 +63,7 @@ export function dynamicEventFor(id: string, w?: World): EventDef | undefined {
name: '太虚大比',
category: 'major',
weight: 0,
text: `太虚仙盟年一会,新一期大比于祖庭开擂。观礼之众云集,百族竞锋——可遣嫡系赴赛,亦可称病让席。`,
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 } } },
@@ -243,7 +249,7 @@ export function eventRoll(w: World): void {
return
}
const firstSpirit = s.flags['firstSpirit'] as number | undefined
if (firstSpirit && s.year - firstSpirit >= 2 && s.year - firstSpirit <= 3 && !s.completedEvents.includes('ev-feisheng')) {
if (firstSpirit && s.year - firstSpirit >= 2 && !s.completedEvents.includes('ev-feisheng')) {
fire(w, 'ev-feisheng')
return
}
@@ -256,14 +262,16 @@ export function eventRoll(w: World): void {
fire(w, `ev-tournament-${tourneyNext}`)
return
}
// 四邻回声(每两年左右一桩
// 四邻回声(偶数年一掷,不论成败封缄
if (s.year % 2 === 0) {
const key = `echoDone-${s.year}`
if (!s.family.flag[key] && w.rng.chance(0.7)) {
const npc = w.rng.pick(Object.values(s.npcFamilies))
if (!s.family.flag[key]) {
s.family.flag[key] = true
fire(w, `ev-echo-${npc.id}-${s.year}`)
return
if (w.rng.chance(0.7)) {
const npc = w.rng.pick(Object.values(s.npcFamilies))
fire(w, `ev-echo-${npc.id}-${s.year}`)
return
}
}
}
@@ -271,7 +279,8 @@ export function eventRoll(w: World): void {
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(
const pool = w.allEvents()
const candidates = pool.filter(
(e) =>
e.category === category &&
!(e.once && s.completedEvents.includes(e.id)) &&
@@ -290,6 +299,7 @@ export function eventRoll(w: World): void {
}
export function fire(w: World, id: string): void {
if (w.state.pendingEvent) return
w.state.pendingEvent = id
w.pendingEvent(id)
}
@@ -394,7 +404,7 @@ export function applyEffect(w: World, eff: EffectDef, squad?: string[]): void {
if (!fam.techniques.includes(eff.addTech)) fam.techniques.push(eff.addTech)
}
if (eff.tournament) {
runTournament(w)
runTournament(w, squad)
}
if (eff.apprentice?.build) {
buildApprentice(w)
+2 -2
View File
@@ -111,8 +111,8 @@ function resultText(m: MissionState): string {
export function canSendMission(w: World, def: MissionDef, members: string[]): boolean {
if (members.length < def.minMembers || members.length > def.maxMembers) return false
for (const id of members) {
const c = w.memberById(id)
if (!c.alive || c.state === 'expedition' || c.state === 'apprentice') return false
const c = w.state.members[id]
if (!c || !c.alive || c.state === 'expedition' || c.state === 'apprentice') return false
}
return w.state.missions.filter((m) => !m.done).length < 3
}
@@ -16,7 +16,7 @@ export function productionTick(w: World): void {
const fielders = w.aliveMembers().filter((c) => aspirationById(c.aspiration)?.effect.type === 'field').length
const merchants = w.aliveMembers().filter((c) => aspirationById(c.aspiration)?.effect.type === 'market').length
const springMod = seasonMod(w.state.month, 'field')
const springMod = w.sysEnabled('season') ? seasonMod(w.state.month, 'field') : 0
if (lingtian > 0) {
const v = Math.round(10 * lingtian * (1 + fielders * 0.05 + springMod))
inv.lingcao = (inv.lingcao ?? 0) + v
@@ -36,7 +36,7 @@ export function productionTick(w: World): void {
inv.lingkuang = (inv.lingkuang ?? 0) + v
parts.push(`灵矿+${v}灵矿`)
}
const autumnMod = seasonMod(w.state.month, 'market')
const autumnMod = w.sysEnabled('season') ? seasonMod(w.state.month, 'market') : 0
if (fangshi > 0) {
const v = Math.round(55 * fangshi * (1 + w.postBonus('marketIncome') + merchants * 0.03 + autumnMod))
fam.stones += v
+12 -2
View File
@@ -20,6 +20,10 @@ export function gateEnemy(idx: number, year: number): EnemyDef {
}
export function runTournament(w: World, squad?: string[]): void {
if (!w.sysEnabled('tournament')) {
w.log('info', '太虚大比:赛事未启。')
return
}
const s = w.state
const eligible = w
.aliveMembers()
@@ -83,12 +87,16 @@ export function apprenticeCandidates(w: World): Character[] {
}
export function buildApprentice(w: World): void {
if (!w.sysEnabled('apprentice')) {
w.log('bad', '宗门来使失望而归:族中暂拒通学。')
return
}
const cand = apprenticeCandidates(w)
const c = w.rng.pick(cand)
if (!c) {
if (cand.length === 0) {
w.log('bad', '宗门来使失望而归:族中无适龄儿郎。')
return
}
const c = w.rng.pick(cand)
const sect = w.rng.pick(SECTS)
const years = w.rng.int(2, 4)
c.state = 'apprentice'
@@ -125,6 +133,7 @@ export function finalizeApprentice(w: World, memberId: string, mode: 'return' |
}
export function checkApprenticeExpiry(w: World): void {
if (!w.sysEnabled('apprentice')) return
for (const c of Object.values(w.state.members)) {
if (!c.alive || !c.apprentice || c.apprentice.quiet) continue
if (w.state.year >= c.apprentice.untilYear) {
@@ -138,6 +147,7 @@ export function checkApprenticeExpiry(w: World): void {
}
export function recruitCheck(w: World): void {
if (!w.sysEnabled('apprentice')) return
const s = w.state
if (s.year % 4 !== 0) return
const key = `recruitDone-${s.year}`
@@ -21,7 +21,6 @@ export function resolveTribulation(w: World, c: Character, mode: 'rash' | 'guard
if (mode === 'delay') {
c.tribDelayYear = w.state.year + 1
w.state.family.stones = w.state.family.stones
w.log('info', `${c.name} 按兵不动,引而不发,待来年再渡。`)
return 'delayed'
}