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:
@@ -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])
|
||||||
|
|||||||
@@ -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 数值 patch(mult 0.5~2 / add 实数——作用于数据包表项;卸载复原) */
|
/** 0.1.34 MOD 数值 patch(mult 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-6:MOD 功法注入行摘除(卸载时按 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)
|
||||||
|
|||||||
@@ -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))
|
||||||
// —— 灾年持续 ——
|
// —— 灾年持续 ——
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
|||||||
@@ -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% 振幅的时机)', () => {
|
||||||
|
|||||||
@@ -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
@@ -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 = [
|
||||||
|
|||||||
@@ -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')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user