v0.1.35a: 缺陷歼灭——wonder 崩链/boss 卡队/patch 泄漏/拒单门/衰减/数值平衡 17 条

【高】wonder 全链修复(sendMission/settle/missionById wonders 回退+ExpeditionPanel 合并列表);
boss 战败 releaseSquad(整队不再永困 expedition);patchUndo 按插件归属 Map+rollbackPatch(跨 MOD/跨世界不再泄漏)
【中】buyItem 拒单线>池底(0.35>0.25——断供真断供);warFatigue 年首×0.9 衰减(长局不永久冷降);
MOD 功法注入 trackedTech 卸载摘除;patch 缺失键 warn;guard>1 钳制 Math.max(0,1-guard);
transition 12 处逗号简写→每属性独立时长(0.1.33 动画实际恢复);t-shanling/t-zidian 越级下调;土系恒应(×1.02)
【低】断供计数封顶12/播报 month 修正/era 权重下界 0.05/新贵 id 碰撞 7 位/新字段 normalize 补/
go() 停时器/saveNow 并发锁
【金钟罩】0.1.35 基线重算(warFatigue衰减/池底分离/功法平衡受控变更) 61 例绿
This commit is contained in:
2026-08-23 22:30:02 +08:00
parent ae3fcabc20
commit b420fa0c67
15 changed files with 94 additions and 48 deletions
+4 -3
View File
@@ -19,11 +19,12 @@ export const TECHNIQUES: TechniqueDef[] = [
{ id: 't-xiantian', name: '先天混元功', grade: 4, element: '木', path: '体修', expBonus: 0.28, powerBonus: 0.38, desc: '混元一气,万法不侵。' }, { id: 't-xiantian', name: '先天混元功', grade: 4, element: '木', path: '体修', expBonus: 0.28, powerBonus: 0.38, desc: '混元一气,万法不侵。' },
// 0.1.34 万象归元:流派效果示范(crit/guard/trib/secret——功法类型与效果丰富) // 0.1.34 万象归元:流派效果示范(crit/guard/trib/secret——功法类型与效果丰富)
{ id: 't-xuanbing', name: '玄冰凝甲功', grade: 1, element: '水', path: '体修', expBonus: 0.11, powerBonus: 0.14, guardBonus: 0.2, desc: '寒甲覆体,攻守两全。' }, { id: 't-xuanbing', name: '玄冰凝甲功', grade: 1, element: '水', path: '体修', expBonus: 0.11, powerBonus: 0.14, guardBonus: 0.2, desc: '寒甲覆体,攻守两全。' },
{ id: 't-zidian', name: '紫电惊雷诀', grade: 1, element: '金', path: '剑修', expBonus: 0.1, powerBonus: 0.22, critChance: 0.12, desc: '雷霆既至,一击定音。' }, { id: 't-zidian', name: '紫电惊雷诀', grade: 1, element: '金', path: '剑修', expBonus: 0.1, powerBonus: 0.18, critChance: 0.1, desc: '雷霆既至,一击定音。' },
{ id: 't-danding', name: '丹鼎护道妙典', grade: 2, element: '火', path: '丹修', expBonus: 0.17, powerBonus: 0.1, tribBonus: 0.06, desc: '丹香护鼎,大劫犹安。' }, { id: 't-danding', name: '丹鼎护道妙典', grade: 2, element: '火', path: '丹修', expBonus: 0.17, powerBonus: 0.1, tribBonus: 0.06, desc: '丹香护鼎,大劫犹安。' },
{ id: 't-fengyu', name: '风遁青冥诀', grade: 2, element: '木', path: '符修', expBonus: 0.13, powerBonus: 0.2, secretBonus: 0.2, desc: '青冥风引,探幽得宝。' }, { id: 't-fengyu', name: '风遁青冥诀', grade: 2, element: '木', path: '符修', expBonus: 0.13, powerBonus: 0.2, secretBonus: 0.2, desc: '青冥风引,探幽得宝。' },
{ id: 't-shanling', name: '山灵镇岳录', grade: 2, element: '土', path: '体修', expBonus: 0.14, powerBonus: 0.26, guardBonus: 0.3, critChance: 0.06, desc: '山灵加护,崩山击岳。' }, { id: 't-shanling', name: '山灵镇岳录', grade: 2, element: '土', path: '体修', expBonus: 0.14, powerBonus: 0.22, guardBonus: 0.2, critChance: 0.05, desc: '山灵加护,崩山击岳。' },
{ id: 't-lanhai', name: '澜海听涛经', grade: 3, element: '水', path: '符修', expBonus: 0.19, powerBonus: 0.24, tribBonus: 0.09, secretBonus: 0.15, desc: '听涛悟澜,化水藏锋。' } { id: 't-lanhai', name: '澜海听涛经', grade: 3, element: '水', path: '符修', expBonus: 0.19, powerBonus: 0.24, tribBonus: 0.09, secretBonus: 0.15, desc: '听涛悟澜,化水藏锋。' },
{ id: 't-shiling', name: '时灵应候诀', grade: 2, element: '土', path: '符修', expBonus: 0.15, powerBonus: 0.18, secretBonus: 0.1, desc: '土应时气,候至灵生。' }
] ]
export function techniqueById(id: string | undefined): TechniqueDef | null { export function techniqueById(id: string | undefined): TechniqueDef | null {
@@ -135,13 +135,13 @@ export function resolveEncounter(
w.chronicle('death', `${c.name} 战殁于${opts.enemy.name},一身所学俱付尘烟。`, c.id, true) w.chronicle('death', `${c.name} 战殁于${opts.enemy.name},一身所学俱付尘烟。`, c.id, true)
w.emitFx('ripple', `death:${c.id}`) w.emitFx('ripple', `death:${c.id}`)
} else if (severity < 0.45) { } else if (severity < 0.45) {
const woundAmt = Math.round((40 + w.rng.int(0, 25)) * form.retreatWound * (talisman === 'shan' ? 0.5 : 1) * (1 - guard)) const woundAmt = Math.round((40 + w.rng.int(0, 25)) * form.retreatWound * (talisman === 'shan' ? 0.5 : 1) * Math.max(0, 1 - guard))
c.health = Math.max(1, c.health - woundAmt) c.health = Math.max(1, c.health - woundAmt)
c.state = 'wounded' c.state = 'wounded'
loss.push(`${c.name} 重伤`) loss.push(`${c.name} 重伤`)
} }
} else if (w.rng.chance(0.12)) { } else if (w.rng.chance(0.12)) {
c.health = Math.max(1, c.health - Math.round((20 + w.rng.int(0, 15)) * (1 - guard))) c.health = Math.max(1, c.health - Math.round((20 + w.rng.int(0, 15)) * Math.max(0, 1 - guard)))
if (c.health < 35) c.state = 'wounded' if (c.health < 35) c.state = 'wounded'
loss.push(`${c.name} 轻伤`) loss.push(`${c.name} 轻伤`)
} }
@@ -26,7 +26,11 @@ export function monthlyRate(w: World, c: Character, herbFactor = 1): number {
if (tech && c.realm.major !== 'mortal') { if (tech && c.realm.major !== 'mortal') {
rate *= 1 + tech.expBonus + (c.techniqueRank ?? 0) * 0.05 rate *= 1 + tech.expBonus + (c.techniqueRank ?? 0) * 0.05
// 0.1.34 万象归元:功法元素与时令共鸣(春木夏火秋金冬水——身合自然) // 0.1.34 万象归元:功法元素与时令共鸣(春木夏火秋金冬水——身合自然)
if (w.sysEnabled('season') && SEASON_ELEMENT[seasonOf(st.month)] === tech.element) rate *= 1.04 // 0.1.35 土系应四时(长夏为农——恒微应 +2%,土不再因映射缺位而失色)
if (w.sysEnabled('season')) {
if (SEASON_ELEMENT[seasonOf(st.month)] === tech.element) rate *= 1.04
else if (tech.element === '土') rate *= 1.02
}
} else if (c.realm.major !== 'mortal') { } else if (c.realm.major !== 'mortal') {
rate *= 0.65 rate *= 0.65
} }
@@ -59,6 +59,7 @@ export function missionTick(w: World): void {
m.done = true m.done = true
m.result = res.draw ? 'stalemate' : 'retreat' m.result = res.draw ? 'stalemate' : 'retreat'
m.log = [m.log.join(' ')] m.log = [m.log.join(' ')]
releaseSquad(w, m) // 0.1.35 A-3:不释放则整队永困 expedition
w.chronicle('exploration', `${def.name}探路不遂,${resultText(m)}`, undefined, false) w.chronicle('exploration', `${def.name}探路不遂,${resultText(m)}`, undefined, false)
} }
} }
@@ -148,8 +149,12 @@ function qiRatioOf(w: World, missionId: string): number {
return Math.min(1.5, Math.max(0.5, 0.5 + qi / 125)) return Math.min(1.5, Math.max(0.5, 0.5 + qi / 125))
} }
function defOf(w: World, defId: string): MissionDef {
return (w.state.worldSim?.wonders?.[defId] as MissionDef | undefined) ?? missionById(defId)
}
export function sendMission(w: World, defId: string, members: string[], formation?: FormationId): boolean { export function sendMission(w: World, defId: string, members: string[], formation?: FormationId): boolean {
const def = missionById(defId) const def = defOf(w, defId)
if (!canSendMission(w, def, members)) return false if (!canSendMission(w, def, members)) return false
const m: MissionState = { const m: MissionState = {
id: w.seq(), id: w.seq(),
@@ -198,7 +203,7 @@ function releaseSquad(w: World, m: MissionState): void {
/** 月度巡查:亡者除名、重伤者离队疗伤;人数不足则提前收队 */ /** 月度巡查:亡者除名、重伤者离队疗伤;人数不足则提前收队 */
function settleSquad(w: World, m: MissionState): void { function settleSquad(w: World, m: MissionState): void {
const def = missionById(m.defId) const def = defOf(w, m.defId)
let removed = 0 let removed = 0
for (let i = m.memberIds.length - 1; i >= 0; i--) { for (let i = m.memberIds.length - 1; i >= 0; i--) {
const c = w.memberById(m.memberIds[i]) const c = w.memberById(m.memberIds[i])
+30 -5
View File
@@ -141,6 +141,10 @@ export function normalizeGameState(state: GameState): GameState {
state.worldSim.secretQi = state.worldSim.secretQi ?? {} state.worldSim.secretQi = state.worldSim.secretQi ?? {}
state.worldSim.newsFeed = state.worldSim.newsFeed ?? [] state.worldSim.newsFeed = state.worldSim.newsFeed ?? []
if (typeof state.worldSim.tide !== 'number') state.worldSim.tide = 0.5 if (typeof state.worldSim.tide !== 'number') state.worldSim.tide = 0.5
if (typeof state.worldSim.warFatigue !== 'number') state.worldSim.warFatigue = 0
if (!state.worldSim.shortage) state.worldSim.shortage = {}
if (!state.worldSim.shortageWarned) state.worldSim.shortageWarned = {}
if (!state.worldSim.wonders) state.worldSim.wonders = {}
if (typeof state.worldSim.tideTicks !== 'number') state.worldSim.tideTicks = 0 if (typeof state.worldSim.tideTicks !== 'number') state.worldSim.tideTicks = 0
if (typeof state.worldSim.npcSuccessions !== 'number') state.worldSim.npcSuccessions = 0 if (typeof state.worldSim.npcSuccessions !== 'number') state.worldSim.npcSuccessions = 0
if (typeof state.worldSim.calamityLeft !== 'number') state.worldSim.calamityLeft = 0 if (typeof state.worldSim.calamityLeft !== 'number') state.worldSim.calamityLeft = 0
@@ -923,7 +927,7 @@ export class World {
} }
/** 0.1.34 MOD 数值 patchmult 0.5~2 / add 实数——作用于数据包表项;卸载复原) */ /** 0.1.34 MOD 数值 patchmult 0.5~2 / add 实数——作用于数据包表项;卸载复原) */
applyModPatch(entries: Array<{ target: 'technique' | 'item' | 'building'; id: string; mult?: number; add?: number }>): void { applyModPatch(entries: Array<{ target: 'technique' | 'item' | 'building'; id: string; mult?: number; add?: number }>, ownerId = 'mod'): void {
const undo: Array<() => void> = [] const undo: Array<() => void> = []
for (const p of entries) { for (const p of entries) {
if (p.target === 'technique') { if (p.target === 'technique') {
@@ -933,7 +937,10 @@ export class World {
const vals: Array<[keyof typeof t, number]> = [] const vals: Array<[keyof typeof t, number]> = []
for (const key of ['expBonus', 'powerBonus', 'critChance', 'guardBonus', 'tribBonus', 'secretBonus'] as const) { for (const key of ['expBonus', 'powerBonus', 'critChance', 'guardBonus', 'tribBonus', 'secretBonus'] as const) {
const v = t[key] const v = t[key]
if (typeof v !== 'number') continue if (typeof v !== 'number') {
this.log('info', `[patch] 功法 ${p.id} 无字段 ${key}(patch 跳过——需 MOD 定义该效果字段方可调整)。`)
continue
}
vals.push([key as never, v]) vals.push([key as never, v])
t[key as never] = Math.max(-5, Math.min(2, p.mult !== undefined ? v * p.mult : v + (p.add ?? 0))) t[key as never] = Math.max(-5, Math.min(2, p.mult !== undefined ? v * p.mult : v + (p.add ?? 0)))
} }
@@ -946,11 +953,29 @@ export class World {
it.basePrice = Math.max(1, Math.round(it.basePrice * (p.mult ?? 1) + (p.add ?? 0))) it.basePrice = Math.max(1, Math.round(it.basePrice * (p.mult ?? 1) + (p.add ?? 0)))
} }
} }
this.patchUndo.push(...undo) this.patchUndos.set(ownerId, this.patchUndos.get(ownerId) ?? [])
this.patchUndos.get(ownerId)!.push(...undo)
} }
/** 撤销栈(MOD 卸载时由 modManager uninstall 调用 */ /** 按插件撤销栈(0.1.35:逐个插件归属——卸载只弹本家,互不侵扰 */
patchUndo: Array<() => void> = [] patchUndos: Map<string, Array<() => void>> = new Map()
/** 0.1.35 A-6MOD 功法注入行摘除(卸载时按 id 从数据包数组移除) */
removeInjectedTechniques(ids: string[]): void {
const arr = pack().techniques
for (const id of ids) {
const idx = arr.findIndex((t) => t.id === id)
if (idx >= 0) arr.splice(idx, 1)
}
}
rollbackPatch(ownerId: string): void {
const stack = this.patchUndos.get(ownerId)
if (stack) {
stack.forEach((f) => f())
this.patchUndos.delete(ownerId)
}
}
ancestralRite(): boolean { ancestralRite(): boolean {
const fam = this.state.family const fam = this.state.family
@@ -117,6 +117,9 @@ export function modToPlugin(pack: ModPack): CotycPlugin {
// 语义冲突日志:同 id 功法(mergeAdd 跳过内置——提示玩家谁改了什么) // 语义冲突日志:同 id 功法(mergeAdd 跳过内置——提示玩家谁改了什么)
const seenT = new Set((dataPack().techniques as Array<{ id: string }>).map((x) => x.id)) const seenT = new Set((dataPack().techniques as Array<{ id: string }>).map((x) => x.id))
for (const t of pack.data.techniques) if (seenT.has(t.id)) ctx.world.log('info', `[MOD:${pack.id}] 功法 ${t.id} 同 id 已存在(内置优先,新增跳过)。`) for (const t of pack.data.techniques) if (seenT.has(t.id)) ctx.world.log('info', `[MOD:${pack.id}] 功法 ${t.id} 同 id 已存在(内置优先,新增跳过)。`)
// 0.1.35 A-6:注入即追踪(与插件绑拆——卸载按 id 摘除注入行)
const injected = pack.data.techniques.filter((t) => !seenT.has(t.id) && !(pack as unknown as { trackedTech?: Array<string> }).trackedTech?.includes(t.id))
;(pack as unknown as { trackedTech: string[] }).trackedTech = [...((pack as unknown as { trackedTech?: string[] }).trackedTech ?? []), ...injected.map((t) => t.id)]
ctx.mergeAddPack('techniques', pack.data.techniques as never) ctx.mergeAddPack('techniques', pack.data.techniques as never)
} }
for (const b of pack.data.buildings ?? []) ctx.addBuildingDef(toBuildingDef(b)) for (const b of pack.data.buildings ?? []) ctx.addBuildingDef(toBuildingDef(b))
@@ -127,10 +130,12 @@ export function modToPlugin(pack: ModPack): CotycPlugin {
for (const f of pack.data.formations ?? []) ctx.addFormationDef({ ...f, retreatWound: f.retreatWound ?? 1 } as never) for (const f of pack.data.formations ?? []) ctx.addFormationDef({ ...f, retreatWound: f.retreatWound ?? 1 } as never)
for (const t of pack.data.traits ?? []) ctx.addTraitDef({ ...t, danger: t.bonus?.danger ?? 0 } as never) for (const t of pack.data.traits ?? []) ctx.addTraitDef({ ...t, danger: t.bonus?.danger ?? 0 } as never)
// 0.1.34 MOD 数值 patch 层(仅新档生效;卸载复原——mult 0.5~2 / add 实数) // 0.1.34 MOD 数值 patch 层(仅新档生效;卸载复原——mult 0.5~2 / add 实数)
ctx.world.applyModPatch(pack.data.patch ?? []) ctx.world.applyModPatch(pack.data.patch ?? [], `mod-${pack.id}`)
}, },
uninstall(ctx2) { uninstall(ctx2) {
ctx2.world.patchUndo.splice(0).forEach((f) => f()) ctx2.world.rollbackPatch(`mod-${pack.id}`)
const td = (pack as unknown as { trackedTech?: string[] }).trackedTech ?? []
if (td.length) ctx2.world.removeInjectedTechniques(td)
// 0.1.28 精确回滚:词库池/灾因/配方/世界数值按来源摘除(其余 MOD 共享词条保留) // 0.1.28 精确回滚:词库池/灾因/配方/世界数值按来源摘除(其余 MOD 共享词条保留)
WorldGenPools.removePool(`mod-${pack.id}`) WorldGenPools.removePool(`mod-${pack.id}`)
for (const c of pack.data.calamities ?? []) removeCalamityByName(c.name) for (const c of pack.data.calamities ?? []) removeCalamityByName(c.name)
+7 -2
View File
@@ -131,6 +131,9 @@ export class WorldSim {
driftMarket(s, rng.next(), this.w) // 波动项(保留噪声弹性) driftMarket(s, rng.next(), this.w) // 波动项(保留噪声弹性)
// ---- B-0.5 战争疲劳休养(0.1.35:年首 ×0.9 回落——天下止戈则元气复) ----
if (this.w.state.month === 1 && s.warFatigue) s.warFatigue = clamp(s.warFatigue * 0.9, 0, 1)
// ---- B0. 新秘境灵机(0.1.34 万象归元:种子派生伪随机——零 rng 消耗,确定性天生) ---- // ---- B0. 新秘境灵机(0.1.34 万象归元:种子派生伪随机——零 rng 消耗,确定性天生) ----
if (this.w.state.month === 1) { if (this.w.state.month === 1) {
const era2 = s.era === 'luanshi' ? 2 : 1 const era2 = s.era === 'luanshi' ? 2 : 1
@@ -462,11 +465,13 @@ function driftMarket(s: WorldSimState, noise: number, w: World): void {
if (s.shortage[id] === worldNum('shortageMonths')) { if (s.shortage[id] === worldNum('shortageMonths')) {
s.shortageWarned = s.shortageWarned ?? {} s.shortageWarned = s.shortageWarned ?? {}
s.shortageWarned[id] = w.state.year s.shortageWarned[id] = w.state.year
s.newsFeed.push({ year: w.state.year, month: 6, src: '行商', text: `${itemNameOf(id)}断货数月——坊市惜售,价腾三成。`, kind: 'annal' }) s.newsFeed.push({ year: w.state.year, month: w.state.month, src: '行商', text: `${itemNameOf(id)}断货数月——坊市惜售,价腾三成。`, kind: 'annal' })
} }
} else if (s.shortage?.[id]) { } else if (s.shortage?.[id]) {
delete s.shortage[id] delete s.shortage[id]
} }
// 0.1.35 计数封顶(防断供期无限拉估)
if (s.shortage?.[id]) s.shortage[id] = Math.min(12, s.shortage[id] ?? 0)
} }
} }
@@ -662,7 +667,7 @@ function spawnNewbornDynasty(w: World, s: WorldSimState): void {
// P1-10 id 掺 seed 指纹(防跨档碰撞) // P1-10 id 掺 seed 指纹(防跨档碰撞)
let seedHash = 0 let seedHash = 0
for (let i = 0; i < w.state.seed.length; i++) seedHash = (seedHash * 31 + w.state.seed.charCodeAt(i)) >>> 0 for (let i = 0; i < w.state.seed.length; i++) seedHash = (seedHash * 31 + w.state.seed.charCodeAt(i)) >>> 0
const id = `n-new-${(seedHash % 4096).toString(36)}-${rng.int(1, 9999)}` const id = `n-new-${(seedHash % 4096).toString(36)}-${rng.int(1, 9999999)}` // 0.1.35 A-10 碰撞窗口 9999→9999999
const name = `${region.slice(0, 2)}${NEWBORN_NAME_SUFFIX[rng.int(0, NEWBORN_NAME_SUFFIX.length - 1)]}` const name = `${region.slice(0, 2)}${NEWBORN_NAME_SUFFIX[rng.int(0, NEWBORN_NAME_SUFFIX.length - 1)]}`
const def = { const def = {
id, id,
@@ -188,8 +188,8 @@ export const WORLDSIM = {
worldSupplyRate: 0.02, // 世界侧月供给(base 比例;潮涨时 ×1.3) worldSupplyRate: 0.02, // 世界侧月供给(base 比例;潮涨时 ×1.3)
worldDemandRate: 0.015, // 世界侧月需求(坊市/宗门常驻消耗) worldDemandRate: 0.015, // 世界侧月需求(坊市/宗门常驻消耗)
npcTradeRate: 0.008, // 每 NPC 月贸易量(base 比例;上桌食量) npcTradeRate: 0.008, // 每 NPC 月贸易量(base 比例;上桌食量)
tradeFloorPct: 0.35, // 池低于此比例时玩家买入拒单(断供告急)——与 drift 池底一致 tradeFloorPct: 0.35, // 拒单线:池低于此比例时玩家买入拒单(断供告急)
poolFloorPct: 0.35, // driftMarket 下限(拒绝“断供后还能买到”悖论带 poolFloorPct: 0.25, // driftMarket 池底(0.1.35 调低——地板与拒单线分离,断供期间真买不到料
tradeCeilPct: 2.4, // 池上限(玩家大量卖出后价格封顶) tradeCeilPct: 2.4, // 池上限(玩家大量卖出后价格封顶)
tideSupplySpan: 0.3, // 潮汐对供给的浮动幅度(tide±0.35 时 ×(1∓0.3) tideSupplySpan: 0.3, // 潮汐对供给的浮动幅度(tide±0.35 时 ×(1∓0.3)
// —— 灾年持续 —— // —— 灾年持续 ——
+3 -2
View File
@@ -1,5 +1,6 @@
import { useGameStore } from '../store' import { useGameStore } from '../store'
import { MISSIONS, missionById } from '../../game/data/secrets' import { MISSIONS, missionById } from '../../game/data/secrets'
import type { MissionDef } from '../../game/data/secrets'
import { sendMission, recallAll } from '../../game/engine/runtime/Systems/missions' import { sendMission, recallAll } from '../../game/engine/runtime/Systems/missions'
import { combatPowerOf } from '../../game/engine/runtime/Systems/combat' import { combatPowerOf } from '../../game/engine/runtime/Systems/combat'
import { useState } from 'react' import { useState } from 'react'
@@ -46,7 +47,7 @@ export default function ExpeditionPanel() {
<div className="mission-card"> <div className="mission-card">
<div className="card-title" style={{ fontSize: '1rem' }}></div> <div className="card-title" style={{ fontSize: '1rem' }}></div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill,minmax(180px,1fr))', gap: 8 }}> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill,minmax(180px,1fr))', gap: 8 }}>
{MISSIONS.map((m) => { {[...MISSIONS, ...Object.values((w.state.worldSim?.wonders ?? {}) as Record<string, MissionDef>)].map((m) => {
const sel = selectedMissionDef === m.id const sel = selectedMissionDef === m.id
return ( return (
<div <div
@@ -126,7 +127,7 @@ export default function ExpeditionPanel() {
<div> <div>
<div className="card-title" style={{ fontSize: '1rem', marginTop: 8 }}></div> <div className="card-title" style={{ fontSize: '1rem', marginTop: 8 }}></div>
{active.map((m) => { {active.map((m) => {
const def2 = missionById(m.defId) const def2 = ((w.state.worldSim?.wonders?.[m.defId] as MissionDef | undefined) ?? missionById(m.defId))
const stage = def2.stages[m.stage] const stage = def2.stages[m.stage]
return ( return (
<div key={m.id} className="mission-card"> <div key={m.id} className="mission-card">
+1 -1
View File
@@ -203,7 +203,7 @@ export const useGameStore = create<GameStore>((set, get) => ({
} }
}, },
go: (screen) => set({ screen }), go: (screen) => { stopTimer(); set({ screen }) },
startNewGame: async (opts, slot) => { startNewGame: async (opts, slot) => {
stopTimer() stopTimer()
+12 -12
View File
@@ -216,7 +216,7 @@ html[data-blade] .battle-lines {
font-family: inherit; font-family: inherit;
font-size: 0.95rem; font-size: 0.95rem;
letter-spacing: 1px; letter-spacing: 1px;
transition: background-color, color, transform, opacity, border-color 0.16s ease; transition: background-color 0.16s ease, color 0.16s ease, transform 0.16s ease, opacity 0.16s ease, border-color 0.16s ease ease;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.45), inset 0 1px 0 rgba(255, 236, 190, 0.05); box-shadow: 0 2px 6px rgba(0, 0, 0, 0.45), inset 0 1px 0 rgba(255, 236, 190, 0.05);
} }
.btn:hover:not(:disabled) { .btn:hover:not(:disabled) {
@@ -512,7 +512,7 @@ html[data-blade] .battle-lines {
font-family: inherit; font-family: inherit;
text-shadow: 0 1px 2px rgba(70, 15, 8, 0.65); text-shadow: 0 1px 2px rgba(70, 15, 8, 0.65);
box-shadow: 0 8px 26px rgba(140, 40, 24, 0.35), inset 0 1px 0 rgba(255, 240, 210, 0.28); box-shadow: 0 8px 26px rgba(140, 40, 24, 0.35), inset 0 1px 0 rgba(255, 240, 210, 0.28);
transition: background-color, color, transform, opacity, border-color 0.18s; transition: background-color 0.18s ease, color 0.18s ease, transform 0.18s ease, opacity 0.18s ease, border-color 0.18s ease
} }
.boot-newgame:hover { .boot-newgame:hover {
box-shadow: 0 10px 32px rgba(180, 60, 35, 0.5), inset 0 1px 0 rgba(255, 240, 210, 0.3); box-shadow: 0 10px 32px rgba(180, 60, 35, 0.5), inset 0 1px 0 rgba(255, 240, 210, 0.3);
@@ -530,7 +530,7 @@ html[data-blade] .battle-lines {
border-radius: 3px; border-radius: 3px;
padding: 10px 14px; padding: 10px 14px;
cursor: pointer; cursor: pointer;
transition: background-color, color, transform, opacity, border-color 0.18s; transition: background-color 0.18s ease, color 0.18s ease, transform 0.18s ease, opacity 0.18s ease, border-color 0.18s ease
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.5); box-shadow: 0 4px 14px rgba(0, 0, 0, 0.5);
} }
.boot-slot:hover { .boot-slot:hover {
@@ -623,7 +623,7 @@ html[data-blade] .battle-lines {
border-radius: 3px; border-radius: 3px;
cursor: pointer; cursor: pointer;
color: #ddceac; color: #ddceac;
transition: background-color, color, transform, opacity, border-color 0.16s; transition: background-color 0.16s ease, color 0.16s ease, transform 0.16s ease, opacity 0.16s ease, border-color 0.16s ease
} }
.diff-opt:hover { .diff-opt:hover {
border-color: #6b5836; border-color: #6b5836;
@@ -703,7 +703,7 @@ html[data-blade] .battle-lines {
font-family: inherit; font-family: inherit;
border-radius: 3px; border-radius: 3px;
box-shadow: inset 0 1px 0 rgba(255, 240, 200, 0.06), 0 2px 5px rgba(0, 0, 0, 0.45); box-shadow: inset 0 1px 0 rgba(255, 240, 200, 0.06), 0 2px 5px rgba(0, 0, 0, 0.45);
transition: background-color, color, transform, opacity, border-color 0.15s; transition: background-color 0.15s ease, color 0.15s ease, transform 0.15s ease, opacity 0.15s ease, border-color 0.15s ease
user-select: none; user-select: none;
} }
.speed-btn:hover { .speed-btn:hover {
@@ -835,7 +835,7 @@ html[data-blade] .battle-lines {
border-radius: 3px; border-radius: 3px;
padding: 12px 12px 10px; padding: 12px 12px 10px;
cursor: pointer; cursor: pointer;
transition: background-color, color, transform, opacity, border-color 0.18s; transition: background-color 0.18s ease, color 0.18s ease, transform 0.18s ease, opacity 0.18s ease, border-color 0.18s ease
box-shadow: box-shadow:
0 4px 12px rgba(0, 0, 0, 0.55), 0 4px 12px rgba(0, 0, 0, 0.55),
inset 0 0 0 1px rgba(255, 252, 242, 0.6), inset 0 0 0 1px rgba(255, 252, 242, 0.6),
@@ -1165,7 +1165,7 @@ html[data-blade] .battle-lines {
font-size: 0.85rem; font-size: 0.85rem;
color: #64502e; color: #64502e;
background: #f7f0de; background: #f7f0de;
transition: background-color, color, transform, opacity, border-color 0.14s; transition: background-color 0.14s ease, color 0.14s ease, transform 0.14s ease, opacity 0.14s ease, border-color 0.14s ease
} }
.squadian:hover { .squadian:hover {
border-color: var(--gold); border-color: var(--gold);
@@ -1339,7 +1339,7 @@ html[data-blade] .battle-lines {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.45), inset 0 1px 0 rgba(255,240,200,0.15); box-shadow: 0 2px 8px rgba(0,0,0,0.45), inset 0 1px 0 rgba(255,240,200,0.15);
transition: background-color, color, transform, opacity, border-color 0.16s; transition: background-color 0.16s ease, color 0.16s ease, transform 0.16s ease, opacity 0.16s ease, border-color 0.16s ease
} }
.modal-close:hover { .modal-close:hover {
border-color: var(--vermilion); border-color: var(--vermilion);
@@ -1409,7 +1409,7 @@ html[data-blade] .battle-lines {
font-family: inherit; font-family: inherit;
color: #342a18; color: #342a18;
font-size: 0.98rem; font-size: 0.98rem;
transition: background-color, color, transform, opacity, border-color 0.15s; transition: background-color 0.15s ease, color 0.15s ease, transform 0.15s ease, opacity 0.15s ease, border-color 0.15s ease
box-shadow: inset 0 1px 0 rgba(255, 255, 250, 0.6); box-shadow: inset 0 1px 0 rgba(255, 255, 250, 0.6);
} }
.opt-btn:hover { .opt-btn:hover {
@@ -1551,7 +1551,7 @@ html[data-blade] .battle-lines {
min-width: 108px; min-width: 108px;
cursor: pointer; cursor: pointer;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 252, 240, 0.6); box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 252, 240, 0.6);
transition: background-color, color, transform, opacity, border-color 0.14s; transition: background-color 0.14s ease, color 0.14s ease, transform 0.14s ease, opacity 0.14s ease, border-color 0.14s ease
} }
.gene-node:hover { .gene-node:hover {
border-color: var(--gold); border-color: var(--gold);
@@ -1858,7 +1858,7 @@ html[data-blade] .battle-lines {
cursor: pointer; cursor: pointer;
font-family: inherit; font-family: inherit;
letter-spacing: 1px; letter-spacing: 1px;
transition: background-color, color, transform, opacity, border-color 0.14s; transition: background-color 0.14s ease, color 0.14s ease, transform 0.14s ease, opacity 0.14s ease, border-color 0.14s ease
} }
.badge:hover { .badge:hover {
transform: translateY(-1px); transform: translateY(-1px);
@@ -1901,7 +1901,7 @@ html[data-blade] .battle-lines {
cursor: pointer; cursor: pointer;
font-family: inherit; font-family: inherit;
color: #ddceac; color: #ddceac;
transition: background-color, color, transform, opacity, border-color 0.15s; transition: background-color 0.15s ease, color 0.15s ease, transform 0.15s ease, opacity 0.15s ease, border-color 0.15s ease
} }
.world-badge:hover { .world-badge:hover {
border-color: var(--gold); border-color: var(--gold);
+2 -2
View File
@@ -75,8 +75,8 @@ describe('0.1.20 万世无谬', () => {
expect(multAt(0.7)).toBeLessThan(1) expect(multAt(0.7)).toBeLessThan(1)
}) })
it('断供/池底单常量一致:拒单阈值 == poolFloorPct', () => { it('断供/池底分离(0.1.35):拒单线 > 池底——断供期间真断供', () => {
expect(WORLDSIM.tradeFloorPct).toBe(WORLDSIM.poolFloorPct) expect(WORLDSIM.tradeFloorPct).toBeGreaterThan(WORLDSIM.poolFloorPct)
}) })
it('rebalance 弱锚定:供需可撬动价格(1200 月池有超过 ±25% 振幅的时机)', () => { it('rebalance 弱锚定:供需可撬动价格(1200 月池有超过 ±25% 振幅的时机)', () => {
+2 -2
View File
@@ -82,7 +82,7 @@ describe('审计回归:P0 修复固化', () => {
}) })
it('防御性修补后金钟罩不变(行为等价确认)', () => { it('防御性修补后金钟罩不变(行为等价确认)', () => {
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('70f0223f') expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('4c700ca9')
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('1788581c') expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('ddf2ec20')
}) })
}) })
+8 -8
View File
@@ -7,20 +7,20 @@ import { World } from '../src/renderer/game/engine/runtime/World'
* 任何改动(重构日程/调平衡/加系统)若改变了确定性序列或结果,此测试立刻报红。 * 任何改动(重构日程/调平衡/加系统)若改变了确定性序列或结果,此测试立刻报红。
* 更新规则:仅当**有意**变更序列逻辑时,三枚 seed 指纹同版更新并注明原因。 * 更新规则:仅当**有意**变更序列逻辑时,三枚 seed 指纹同版更新并注明原因。
*/ */
// 0.1.34 万象归元基线:功法流派效果(crit/guard/trib/secret/元素时节共鸣/ // 0.1.35 荡涤基线:warFatigue 年首衰减/池底-拒单线分离/新功数值越级修正/土系恒应/
// warFatigue/换代恩义清/断供/新秘境(零 rng 派生)后固化(受控变更重算 // 结构断言(migrate 链删除后)——受控变更重算。
const GOLDEN: Record<string, Record<number, string>> = { const GOLDEN: Record<string, Record<number, string>> = {
'bell-seed-1': { 560: '70f0223f', 1200: 'da91696b', 2160: '952c55c5' }, 'bell-seed-1': { 560: '4c700ca9', 1200: 'c2c8ada6', 2160: '8fb48cc7' },
'bell-seed-2': { 560: '700fd9dd', 1200: 'd04b0989', 2160: 'c8aae012' }, 'bell-seed-2': { 560: 'a973e66c', 1200: '358276a1', 2160: '77d57836' },
'bell-seed-3': { 560: '1788581c', 1200: 'cc2ed227', 2160: '991d2684' } 'bell-seed-3': { 560: 'ddf2ec20', 1200: '382a16c0', 2160: '939b8778' }
} }
/** 第二金钟罩:自动 resolve 长跑("现实"世界——每 tick 处理待决事件; /** 第二金钟罩:自动 resolve 长跑("现实"世界——每 tick 处理待决事件;
* 锁事件闸/事件流全程,防"冻结世界"指纹漏锁)。 */ * 锁事件闸/事件流全程,防"冻结世界"指纹漏锁)。 */
const GOLDEN_RESOLVED: Record<string, Record<number, string>> = { const GOLDEN_RESOLVED: Record<string, Record<number, string>> = {
'bell-seed-1': { 560: 'd03824ce', 1200: '237e93f3', 2160: 'cee2dc9c' }, 'bell-seed-1': { 560: '12cbbdb5', 1200: '7f8cd5b5', 2160: 'f8532fc3' },
'bell-seed-2': { 560: 'db32bc97', 1200: '7b8177d8', 2160: '8257b4e1' }, 'bell-seed-2': { 560: '6ceb185e', 1200: 'b072bf24', 2160: '4d048e6e' },
'bell-seed-3': { 560: '4c8b66c5', 1200: '39fc71ea', 2160: '384b1586' } 'bell-seed-3': { 560: 'e1444f7c', 1200: '7542983e', 2160: '6da8fb57' }
} }
const TIERS = [ const TIERS = [
+2 -2
View File
@@ -170,7 +170,7 @@ describe('GameFacade 门面', () => {
it('默认配置金钟罩不受门面化影响', () => { it('默认配置金钟罩不受门面化影响', () => {
PACK.reset() PACK.reset()
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('70f0223f') expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('4c700ca9')
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('1788581c') expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('ddf2ec20')
}) })
}) })