v0.1.33b: 引擎止血——白屏/经济/治理 15 项

【白屏】equip() 校验物品存在;MemberModal 空守卫(B1 属 UI 同阶段)
【经济】tribBoost 渡劫成/败/陨落三分支尾部清零(+12% 破境丹不再永久白送);
assistAlly 先校验 NPC 再扣款;giftNpc/makePeace 加 npc 守卫
【治理】about 版本透传(删硬编码 0.1.29);engineFromSnapshot 尊重 noAutoCreate(不再重放创世);
回声事件 npc 空池守卫;squad 陈旧 id 过滤;mission 日志悬空 join 修复;
flag 修剪扩 legacyDone/tournamentYear + tauntCD 随家族消亡删;purgeNpc 删无效 auction 键;
chronicle/battle splice 去重;lifecycle 注释 240→200 一致;worldsim-data 俄语注释中文化;
maxRealmIdx 占位 0 且 normalize 重算(防虚报);TECH_GRADE_BASE 复用删除(实为局部 const 未动)
【测试】gameengine about 断言修正(0.1.32 透传) 5030 全绿
This commit is contained in:
2026-08-23 21:34:29 +08:00
parent bce50b6e2b
commit 3e4ed4d882
11 changed files with 40 additions and 28 deletions
+15 -9
View File
@@ -43,14 +43,20 @@ export class GameEngine {
this.datapack = PACK
if (opts.datapack) this.datapack.override(opts.datapack)
// 组装World(内核注入→时钟/随机/总线单源)
const state = opts.seed ? createWorldState({
seed: this._seed,
surname: opts.surname?.trim() || '',
familyName: opts.familyName?.trim() || `${opts.surname?.trim() || '林'}`,
motto: opts.motto?.trim() || '耕读传家,术法继世',
difficulty: opts.difficulty ?? 'normal'
}) : (globalThis as never as { __state?: GameState }).__state
this.world = new World(state!, [], this.kernel)
if (opts.noAutoCreate) {
this.world = new World(createWorldState({
seed: this._seed, surname: '占位', familyName: '占位', motto: '', difficulty: 'normal'
}), [], this.kernel)
} else {
const state = opts.seed ? createWorldState({
seed: this._seed,
surname: opts.surname?.trim() || '林',
familyName: opts.familyName?.trim() || `${opts.surname?.trim() || '林'}`,
motto: opts.motto?.trim() || '耕读传家,术法继世',
difficulty: opts.difficulty ?? 'normal'
}) : (globalThis as never as { __state?: GameState }).__state
this.world = new World(state!, [], this.kernel)
}
this.facade = new ApiFacade(this.world, 1)
// Kernel 总线 → World.out 桥接(attached 后同步 feed
this.world.out = this.world.out.concat([])
@@ -92,7 +98,7 @@ export class GameEngine {
about(): { title: string; version: string; modules: number; systems: number; plugins: number; packFingerprint: string } {
const a = this.facade.about()
return { ...a, version: '0.1.29' }
return a
}
installPlugin(p: CotycPlugin): { ok: boolean; reason?: string } {
@@ -70,9 +70,9 @@ export const GIFT_TIERS = [40, 120, 300] as const
export function giftNpc(w: World, npcId: string, stones: number): boolean {
const fam = w.state.family
if (stones <= 0 || fam.stones < stones) return false
fam.stones -= stones
const npc = w.state.npcFamilies[npcId]
if (!npc || stones <= 0 || fam.stones < stones) return false
fam.stones -= stones
let gain = calcGiftGain(stones)
// 结盟亲善势:报之以加倍情面
const stance = (w.state.worldSim?.npcDyn?.[npcId] as { stance?: string } | undefined)?.stance
@@ -96,7 +96,7 @@ export function giftNpc(w: World, npcId: string, stones: number): boolean {
export function makePeace(w: World, npcId: string): boolean {
const fam = w.state.family
const npc = w.state.npcFamilies[npcId]
if (fam.stones < 200) return false
if (!npc || fam.stones < 200) return false
fam.stones -= 200
npc.relation = Math.max(npc.relation + 35, 30)
npc.power = Math.min(900, npc.power + 5)
@@ -377,8 +377,9 @@ export function eventRoll(w: World): void {
const key = `echoDone-${s.year}`
if (!s.family.flag[key]) {
s.family.flag[key] = true
if (w.rng.chance(0.7)) {
const npc = w.rng.pick(Object.values(s.npcFamilies))
const npcs = Object.values(s.npcFamilies)
if (npcs.length > 0 && w.rng.chance(0.7)) {
const npc = w.rng.pick(npcs)
fire(w, `ev-echo-${npc.id}-${s.year}`)
return
}
@@ -716,7 +717,9 @@ export function applyEffect(w: World, eff: EffectDef, squad?: string[], _extra?:
.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)
team = squad
.map((id) => w.state.members[id] ? w.memberById(id) : null)
.filter((c): c is Character => !!c && c.alive && c.state !== 'expedition' && w.ageOf(c) >= 16)
}
if (team.length > 0) {
resolveRaid(w, eff.raid.npcId, team)
@@ -11,7 +11,7 @@ export function deathTick(w: World): void {
if (!c.alive) continue
const age = w.ageOf(c)
const span = lifespanOf(w, c)
// P0-A 天年:仙家 240 年为限(span 供凡人/低境界;高境界同样 240 内退场——世界人口会换血)
// P0-A 天年:仙家 200 年为限(span 供凡人/低境界;高境界同样 240 内退场——世界人口会换血)
const cap = Math.min(span, 200)
const softCap = cap * 0.85
let p = 0
@@ -57,7 +57,7 @@ export function missionTick(w: World): void {
if (stage.kind === 'boss') {
m.done = true
m.result = res.draw ? 'stalemate' : 'retreat'
m.log.join(' ')
m.log = [m.log.join(' ')]
w.chronicle('exploration', `${def.name}探路不遂,${resultText(m)}`, undefined, false)
}
}
@@ -60,6 +60,7 @@ export function resolveTribulation(w: World, c: Character, mode: 'rash' | 'guard
if (next.major === 'spirit' && !s.flags['firstSpirit']) {
s.flags['firstSpirit'] = s.year
}
c.tribBoost = 0
return 'success'
}
@@ -76,6 +77,7 @@ export function resolveTribulation(w: World, c: Character, mode: 'rash' | 'guard
c.deathCause = '渡劫陨落'
w.chronicle('death', `${c.name} 天劫加身,灵石俱焚而陨。`, c.id, true)
w.log('bad', `${c.name} 渡劫陨落。`)
c.tribBoost = 0
return 'dead'
}
if (guardian) {
@@ -87,6 +89,7 @@ export function resolveTribulation(w: World, c: Character, mode: 'rash' | 'guard
}
}
w.log('bad', text)
c.tribBoost = 0
return 'fail'
}
+7 -6
View File
@@ -10,7 +10,7 @@ import {
} from '../../types/domain'
import { BUILDINGS, bonusOf, buildingById } from '../../data/buildings'
import { itemById, pillRecipeByOutput, forgeRecipeByOutput } from '../../data/items'
import { itemById as defById, pillRecipeByOutput, forgeRecipeByOutput } from '../../data/items'
import { POSTS } from '../../data/posts'
import { aspirationById as aspirationOf } from '../../data/aspirations'
import { computeLegacy, resolveLegacy, peakRealmIndex, grandTechniqueCount, LegacyArch } from '../narrative/legacy'
@@ -405,14 +405,12 @@ export class World {
}
this.state.chronicle.push(entry)
if (this.state.chronicle.length > 520) this.state.chronicle.splice(0, this.state.chronicle.length - 520)
if (this.state.chronicle.length > 520) this.state.chronicle.splice(0, this.state.chronicle.length - 520)
this.out.forEach((o) => o.onChronicle(entry, important))
}
battle(log: BattleLog): void {
this.state.battles.push(log)
if (this.state.battles.length > 220) this.state.battles.splice(0, this.state.battles.length - 220)
if (this.state.battles.length > 220) this.state.battles.splice(0, this.state.battles.length - 220)
this.out.forEach((o) => o.onBattle(log))
}
@@ -480,8 +478,10 @@ export class World {
const fam = this.state.family
const cutoff = this.state.year - 3
for (const key of Object.keys(fam.flag)) {
const m = /^(auction-|prayerDone-|echoDone-|recruitDone-)(\d+)$/.exec(key)
const m = /^(auction-|prayerDone-|echoDone-|recruitDone-|legacyDone-|tournamentYear-)(\d+)$/.exec(key)
if (m && Number(m[2]) < cutoff) delete fam.flag[key]
// tauntCD-<npcId> 非年份键:随目标家族消亡删除(防长线膨胀)
if (key.startsWith('tauntCD-') && !this.state.npcFamilies[key.slice('tauntCD-'.length)]) delete fam.flag[key]
}
}
@@ -616,10 +616,10 @@ export class World {
const fam = this.state.family
const ws = this.state.worldSim as { distress?: { id: string } } | undefined
if (!ws?.distress || ws.distress.id !== npcId) return false
if (fam.stones < cost) return false
fam.stones -= cost
const npc = this.state.npcFamilies[npcId]
if (!npc) return false
if (fam.stones < cost) return false
fam.stones -= cost
npc.power = Math.min(900, npc.power + 15)
npc.relation = Math.min(100, npc.relation + 8)
fam.reputation += 3
@@ -728,6 +728,7 @@ export class World {
}
equip(memberId: Id, artifact: string): void {
if (!defById(artifact)) return
const c = this.memberById(memberId)
if (!c.alive) return
c.equipment = artifact
+1 -1
View File
@@ -97,7 +97,7 @@ export function createWorldState(opts: NewGameOptions): GameState {
stats: {
repPeak: 5,
popPeak: 5,
maxRealmIdx: 25,
maxRealmIdx: 0, // normalize(World) 时按开局成员重算真实峰值,此处仅为占位
techniqueGrand: 0,
tourneyHistory: [],
feishengCount: 0
-1
View File
@@ -598,7 +598,6 @@ function purgeNpc(w: World, s: WorldSimState, id: string): void {
if (dyn?.relationsWithOthers && id in dyn.relationsWithOthers) delete dyn.relationsWithOthers[id]
}
delete w.state.family.flag[`raidCD-${id}`]
delete w.state.family.flag[`auction-${id}`]
delete w.state.family.flag[`tauntCD-${id}`]
w.state.eventQueue = (w.state.eventQueue ?? []).filter((e) => !e.startsWith('ev-raid-') || !e.endsWith(id))
if (s.distress?.id === id) s.distress = undefined
@@ -200,7 +200,7 @@ export const WORLDSIM = {
// —— 新贵补位 ——
greatNewbornChance: 0.06, // 年首新贵出现率(乱世 ×2;上限 4 家)
// —— 盟约连坐(加性) ——
allianceGriefRaid: 0.012, // кажд 世仇盟连加性 raid 概率
allianceGriefRaid: 0.012, // 世仇盟连加性 raid 概率
// —— 天下事件 ——
calamityChance: 0.18,
calamities: ['旱灾', '涝灾', '蝗灾', '疫病', '兽潮', '寒潮'] as const,