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:
@@ -43,6 +43,11 @@ export class GameEngine {
|
|||||||
this.datapack = PACK
|
this.datapack = PACK
|
||||||
if (opts.datapack) this.datapack.override(opts.datapack)
|
if (opts.datapack) this.datapack.override(opts.datapack)
|
||||||
// 组装World(内核注入→时钟/随机/总线单源)
|
// 组装World(内核注入→时钟/随机/总线单源)
|
||||||
|
if (opts.noAutoCreate) {
|
||||||
|
this.world = new World(createWorldState({
|
||||||
|
seed: this._seed, surname: '占位', familyName: '占位', motto: '', difficulty: 'normal'
|
||||||
|
}), [], this.kernel)
|
||||||
|
} else {
|
||||||
const state = opts.seed ? createWorldState({
|
const state = opts.seed ? createWorldState({
|
||||||
seed: this._seed,
|
seed: this._seed,
|
||||||
surname: opts.surname?.trim() || '林',
|
surname: opts.surname?.trim() || '林',
|
||||||
@@ -51,6 +56,7 @@ export class GameEngine {
|
|||||||
difficulty: opts.difficulty ?? 'normal'
|
difficulty: opts.difficulty ?? 'normal'
|
||||||
}) : (globalThis as never as { __state?: GameState }).__state
|
}) : (globalThis as never as { __state?: GameState }).__state
|
||||||
this.world = new World(state!, [], this.kernel)
|
this.world = new World(state!, [], this.kernel)
|
||||||
|
}
|
||||||
this.facade = new ApiFacade(this.world, 1)
|
this.facade = new ApiFacade(this.world, 1)
|
||||||
// Kernel 总线 → World.out 桥接(attached 后同步 feed)
|
// Kernel 总线 → World.out 桥接(attached 后同步 feed)
|
||||||
this.world.out = this.world.out.concat([])
|
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 } {
|
about(): { title: string; version: string; modules: number; systems: number; plugins: number; packFingerprint: string } {
|
||||||
const a = this.facade.about()
|
const a = this.facade.about()
|
||||||
return { ...a, version: '0.1.29' }
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
installPlugin(p: CotycPlugin): { ok: boolean; reason?: string } {
|
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 {
|
export function giftNpc(w: World, npcId: string, stones: number): boolean {
|
||||||
const fam = w.state.family
|
const fam = w.state.family
|
||||||
if (stones <= 0 || fam.stones < stones) return false
|
|
||||||
fam.stones -= stones
|
|
||||||
const npc = w.state.npcFamilies[npcId]
|
const npc = w.state.npcFamilies[npcId]
|
||||||
|
if (!npc || stones <= 0 || fam.stones < stones) return false
|
||||||
|
fam.stones -= stones
|
||||||
let gain = calcGiftGain(stones)
|
let gain = calcGiftGain(stones)
|
||||||
// 结盟亲善势:报之以加倍情面
|
// 结盟亲善势:报之以加倍情面
|
||||||
const stance = (w.state.worldSim?.npcDyn?.[npcId] as { stance?: string } | undefined)?.stance
|
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 {
|
export function makePeace(w: World, npcId: string): boolean {
|
||||||
const fam = w.state.family
|
const fam = w.state.family
|
||||||
const npc = w.state.npcFamilies[npcId]
|
const npc = w.state.npcFamilies[npcId]
|
||||||
if (fam.stones < 200) return false
|
if (!npc || fam.stones < 200) return false
|
||||||
fam.stones -= 200
|
fam.stones -= 200
|
||||||
npc.relation = Math.max(npc.relation + 35, 30)
|
npc.relation = Math.max(npc.relation + 35, 30)
|
||||||
npc.power = Math.min(900, npc.power + 5)
|
npc.power = Math.min(900, npc.power + 5)
|
||||||
|
|||||||
@@ -377,8 +377,9 @@ export function eventRoll(w: World): void {
|
|||||||
const key = `echoDone-${s.year}`
|
const key = `echoDone-${s.year}`
|
||||||
if (!s.family.flag[key]) {
|
if (!s.family.flag[key]) {
|
||||||
s.family.flag[key] = true
|
s.family.flag[key] = true
|
||||||
if (w.rng.chance(0.7)) {
|
const npcs = Object.values(s.npcFamilies)
|
||||||
const npc = w.rng.pick(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}`)
|
fire(w, `ev-echo-${npc.id}-${s.year}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -716,7 +717,9 @@ export function applyEffect(w: World, eff: EffectDef, squad?: string[], _extra?:
|
|||||||
.sort((a, b) => rankPower(w, b) - rankPower(w, a))
|
.sort((a, b) => rankPower(w, b) - rankPower(w, a))
|
||||||
.slice(0, 4)
|
.slice(0, 4)
|
||||||
if (squad && squad.length > 0) {
|
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) {
|
if (team.length > 0) {
|
||||||
resolveRaid(w, eff.raid.npcId, team)
|
resolveRaid(w, eff.raid.npcId, team)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export function deathTick(w: World): void {
|
|||||||
if (!c.alive) continue
|
if (!c.alive) continue
|
||||||
const age = w.ageOf(c)
|
const age = w.ageOf(c)
|
||||||
const span = lifespanOf(w, c)
|
const span = lifespanOf(w, c)
|
||||||
// P0-A 天年:仙家 240 年为限(span 供凡人/低境界;高境界同样 240 内退场——世界人口会换血)
|
// P0-A 天年:仙家 200 年为限(span 供凡人/低境界;高境界同样 240 内退场——世界人口会换血)
|
||||||
const cap = Math.min(span, 200)
|
const cap = Math.min(span, 200)
|
||||||
const softCap = cap * 0.85
|
const softCap = cap * 0.85
|
||||||
let p = 0
|
let p = 0
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export function missionTick(w: World): void {
|
|||||||
if (stage.kind === 'boss') {
|
if (stage.kind === 'boss') {
|
||||||
m.done = true
|
m.done = true
|
||||||
m.result = res.draw ? 'stalemate' : 'retreat'
|
m.result = res.draw ? 'stalemate' : 'retreat'
|
||||||
m.log.join(' ')
|
m.log = [m.log.join(' ')]
|
||||||
w.chronicle('exploration', `${def.name}探路不遂,${resultText(m)}。`, undefined, false)
|
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']) {
|
if (next.major === 'spirit' && !s.flags['firstSpirit']) {
|
||||||
s.flags['firstSpirit'] = s.year
|
s.flags['firstSpirit'] = s.year
|
||||||
}
|
}
|
||||||
|
c.tribBoost = 0
|
||||||
return 'success'
|
return 'success'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +77,7 @@ export function resolveTribulation(w: World, c: Character, mode: 'rash' | 'guard
|
|||||||
c.deathCause = '渡劫陨落'
|
c.deathCause = '渡劫陨落'
|
||||||
w.chronicle('death', `${c.name} 天劫加身,灵石俱焚而陨。`, c.id, true)
|
w.chronicle('death', `${c.name} 天劫加身,灵石俱焚而陨。`, c.id, true)
|
||||||
w.log('bad', `☠ ${c.name} 渡劫陨落。`)
|
w.log('bad', `☠ ${c.name} 渡劫陨落。`)
|
||||||
|
c.tribBoost = 0
|
||||||
return 'dead'
|
return 'dead'
|
||||||
}
|
}
|
||||||
if (guardian) {
|
if (guardian) {
|
||||||
@@ -87,6 +89,7 @@ export function resolveTribulation(w: World, c: Character, mode: 'rash' | 'guard
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
w.log('bad', text)
|
w.log('bad', text)
|
||||||
|
c.tribBoost = 0
|
||||||
return 'fail'
|
return 'fail'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
} from '../../types/domain'
|
} from '../../types/domain'
|
||||||
|
|
||||||
import { BUILDINGS, bonusOf, buildingById } from '../../data/buildings'
|
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 { POSTS } from '../../data/posts'
|
||||||
import { aspirationById as aspirationOf } from '../../data/aspirations'
|
import { aspirationById as aspirationOf } from '../../data/aspirations'
|
||||||
import { computeLegacy, resolveLegacy, peakRealmIndex, grandTechniqueCount, LegacyArch } from '../narrative/legacy'
|
import { computeLegacy, resolveLegacy, peakRealmIndex, grandTechniqueCount, LegacyArch } from '../narrative/legacy'
|
||||||
@@ -405,14 +405,12 @@ export class World {
|
|||||||
}
|
}
|
||||||
this.state.chronicle.push(entry)
|
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)
|
||||||
if (this.state.chronicle.length > 520) this.state.chronicle.splice(0, this.state.chronicle.length - 520)
|
|
||||||
this.out.forEach((o) => o.onChronicle(entry, important))
|
this.out.forEach((o) => o.onChronicle(entry, important))
|
||||||
}
|
}
|
||||||
|
|
||||||
battle(log: BattleLog): void {
|
battle(log: BattleLog): void {
|
||||||
this.state.battles.push(log)
|
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)
|
||||||
if (this.state.battles.length > 220) this.state.battles.splice(0, this.state.battles.length - 220)
|
|
||||||
this.out.forEach((o) => o.onBattle(log))
|
this.out.forEach((o) => o.onBattle(log))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -480,8 +478,10 @@ export class World {
|
|||||||
const fam = this.state.family
|
const fam = this.state.family
|
||||||
const cutoff = this.state.year - 3
|
const cutoff = this.state.year - 3
|
||||||
for (const key of Object.keys(fam.flag)) {
|
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]
|
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 fam = this.state.family
|
||||||
const ws = this.state.worldSim as { distress?: { id: string } } | undefined
|
const ws = this.state.worldSim as { distress?: { id: string } } | undefined
|
||||||
if (!ws?.distress || ws.distress.id !== npcId) return false
|
if (!ws?.distress || ws.distress.id !== npcId) return false
|
||||||
if (fam.stones < cost) return false
|
|
||||||
fam.stones -= cost
|
|
||||||
const npc = this.state.npcFamilies[npcId]
|
const npc = this.state.npcFamilies[npcId]
|
||||||
if (!npc) return false
|
if (!npc) return false
|
||||||
|
if (fam.stones < cost) return false
|
||||||
|
fam.stones -= cost
|
||||||
npc.power = Math.min(900, npc.power + 15)
|
npc.power = Math.min(900, npc.power + 15)
|
||||||
npc.relation = Math.min(100, npc.relation + 8)
|
npc.relation = Math.min(100, npc.relation + 8)
|
||||||
fam.reputation += 3
|
fam.reputation += 3
|
||||||
@@ -728,6 +728,7 @@ export class World {
|
|||||||
}
|
}
|
||||||
|
|
||||||
equip(memberId: Id, artifact: string): void {
|
equip(memberId: Id, artifact: string): void {
|
||||||
|
if (!defById(artifact)) return
|
||||||
const c = this.memberById(memberId)
|
const c = this.memberById(memberId)
|
||||||
if (!c.alive) return
|
if (!c.alive) return
|
||||||
c.equipment = artifact
|
c.equipment = artifact
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export function createWorldState(opts: NewGameOptions): GameState {
|
|||||||
stats: {
|
stats: {
|
||||||
repPeak: 5,
|
repPeak: 5,
|
||||||
popPeak: 5,
|
popPeak: 5,
|
||||||
maxRealmIdx: 25,
|
maxRealmIdx: 0, // normalize(World) 时按开局成员重算真实峰值,此处仅为占位
|
||||||
techniqueGrand: 0,
|
techniqueGrand: 0,
|
||||||
tourneyHistory: [],
|
tourneyHistory: [],
|
||||||
feishengCount: 0
|
feishengCount: 0
|
||||||
|
|||||||
@@ -598,7 +598,6 @@ function purgeNpc(w: World, s: WorldSimState, id: string): void {
|
|||||||
if (dyn?.relationsWithOthers && id in dyn.relationsWithOthers) delete dyn.relationsWithOthers[id]
|
if (dyn?.relationsWithOthers && id in dyn.relationsWithOthers) delete dyn.relationsWithOthers[id]
|
||||||
}
|
}
|
||||||
delete w.state.family.flag[`raidCD-${id}`]
|
delete w.state.family.flag[`raidCD-${id}`]
|
||||||
delete w.state.family.flag[`auction-${id}`]
|
|
||||||
delete w.state.family.flag[`tauntCD-${id}`]
|
delete w.state.family.flag[`tauntCD-${id}`]
|
||||||
w.state.eventQueue = (w.state.eventQueue ?? []).filter((e) => !e.startsWith('ev-raid-') || !e.endsWith(id))
|
w.state.eventQueue = (w.state.eventQueue ?? []).filter((e) => !e.startsWith('ev-raid-') || !e.endsWith(id))
|
||||||
if (s.distress?.id === id) s.distress = undefined
|
if (s.distress?.id === id) s.distress = undefined
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ export const WORLDSIM = {
|
|||||||
// —— 新贵补位 ——
|
// —— 新贵补位 ——
|
||||||
greatNewbornChance: 0.06, // 年首新贵出现率(乱世 ×2;上限 4 家)
|
greatNewbornChance: 0.06, // 年首新贵出现率(乱世 ×2;上限 4 家)
|
||||||
// —— 盟约连坐(加性) ——
|
// —— 盟约连坐(加性) ——
|
||||||
allianceGriefRaid: 0.012, // кажд 世仇盟连加性 raid 概率
|
allianceGriefRaid: 0.012, // 每 世仇盟连加性 raid 概率
|
||||||
// —— 天下事件 ——
|
// —— 天下事件 ——
|
||||||
calamityChance: 0.18,
|
calamityChance: 0.18,
|
||||||
calamities: ['旱灾', '涝灾', '蝗灾', '疫病', '兽潮', '寒潮'] as const,
|
calamities: ['旱灾', '涝灾', '蝗灾', '疫病', '兽潮', '寒潮'] as const,
|
||||||
|
|||||||
@@ -47,11 +47,11 @@ describe('GameEngine 引擎门面', () => {
|
|||||||
expect(e.status()).toBe('running')
|
expect(e.status()).toBe('running')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('about 元数据含 plugins(≥3)与版本 0.1.20', () => {
|
it('about 元数据含 plugins(≥3)与版本透传', () => {
|
||||||
const e = new GameEngine({ seed: 'engine-5' })
|
const e = new GameEngine({ seed: 'engine-5' })
|
||||||
const a = e.about()
|
const a = e.about()
|
||||||
expect(a.plugins).toBeGreaterThanOrEqual(3)
|
expect(a.plugins).toBeGreaterThanOrEqual(3)
|
||||||
expect(a.version).toContain('0.1.29')
|
expect(a.version).toContain('0.1.32')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user