From bce50b6e2bf5f8e0d5c7a0aa4c93d880c40f7f46 Mon Sep 17 00:00:00 2001 From: thzxx <1440196015@qq.com> Date: Sun, 23 Aug 2026 21:31:19 +0800 Subject: [PATCH] =?UTF-8?q?v0.1.33a:=20=E7=89=A9=E5=93=81=E9=97=AD?= =?UTF-8?q?=E7=8E=AF=E2=80=94=E2=80=94=E7=AC=A6=E7=AE=93=E7=94=9F=E6=95=88?= =?UTF-8?q?/=E5=B9=BD=E7=81=B5=E7=89=A9=E5=BD=92=E4=BD=8D/=E7=81=BE?= =?UTF-8?q?=E5=B9=B4=E6=8E=A5=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 【符箓闭环】talismanWarPrep 返回类型,开战消耗 1 张;风符全队×1.12/山符×1.15+受创减半(零 rng) 【符箓坊】建筑耗符纸×2+灵木×2/级,月造风符1枚;坊3级另出山符(消耗式零 rng) 【幽灵物品归位】and灵丹/虚元丹/玉澜刃补 ITEMS 定义(含 icon/价格/战力)+craftPill/forgeArtifact union 扩键+ARTIFACT_POWER 补玉澜刃 0.4 【灾年生效】farmFactor/mineFactor 从算而未用→真正乘入灵田/药园/矿场/灵果/木坊产量(旱灾×0.7等) 【金钟罩】0.1.33 基线重算(符箓/灾年/新丹受控变更),RAW+RES 共 18 值 【测试】+3(economy-loop-0.1.33 闭环矩阵) 全绿 --- src/renderer/game/data/buildings.ts | 8 ++- src/renderer/game/data/items.ts | 10 +++- .../game/engine/runtime/Systems/combat.ts | 22 +++++-- .../game/engine/runtime/Systems/production.ts | 26 +++++++-- src/renderer/game/engine/runtime/World.ts | 4 +- src/renderer/ui/panels/MarketPanel.tsx | 4 +- src/renderer/ui/store.ts | 2 +- tests/audit-regression.test.ts | 4 +- tests/clock.test.ts | 16 ++--- tests/economy-loop-0.1.33.test.ts | 58 +++++++++++++++++++ tests/facade-registry.test.ts | 4 +- 11 files changed, 127 insertions(+), 31 deletions(-) create mode 100644 tests/economy-loop-0.1.33.test.ts diff --git a/src/renderer/game/data/buildings.ts b/src/renderer/game/data/buildings.ts index 6dfc62d..1dd6395 100644 --- a/src/renderer/game/data/buildings.ts +++ b/src/renderer/game/data/buildings.ts @@ -5,7 +5,7 @@ export interface BuildingDef { desc: string kind: 'produce' | 'function' maxLevel: number - produceTable?: (level: number) => { stone?: number; lingcao?: number; lingkuang?: number; beastcore?: number; lingyu?: number; lingmu?: number; shoupi?: number; lingguo?: number } + produceTable?: (level: number) => { stone?: number; lingcao?: number; lingkuang?: number; beastcore?: number; lingyu?: number; lingmu?: number; shoupi?: number; lingguo?: number; 'talisman-feng'?: number; 'talisman-shan'?: number } upgradeCost: (level: number) => { stones: number; lingkuang: number } extra?: Record } @@ -88,6 +88,12 @@ export const BUILDINGS: Record = { produceTable: (l) => ({ lingguo: 3 * l }), upgradeCost: (l) => ({ stones: 100 * Math.pow(1.7, l - 1), lingkuang: 10 * l }) }, + fuzhifang: { + id: 'fuzhifang', name: '符箓坊', icon: '符', + desc: '符纸灵木入炉——月耗符纸灵木各贰,出风符一枚;坊叁级另出山符。', + kind: 'produce', maxLevel: 3, + upgradeCost: (l) => ({ stones: 120 * Math.pow(1.7, l - 1), lingkuang: 10 * l }) + }, lingmufang: { id: 'lingmufang', name: '灵木坊', icon: '木', desc: '伐百年灵木——月产灵木,符纸之基。', diff --git a/src/renderer/game/data/items.ts b/src/renderer/game/data/items.ts index d5bf70d..5145f9e 100644 --- a/src/renderer/game/data/items.ts +++ b/src/renderer/game/data/items.ts @@ -34,14 +34,20 @@ export const ITEMS: Record = { 'weapon-fan': { id: 'weapon-fan', name: '凡器', kind: 'artifact', basePrice: 120, desc: '普通铁器,聊胜于无。', icon: '凡' }, 'weapon-qi': { id: 'weapon-qi', name: '法器', kind: 'artifact', basePrice: 450, desc: '蕴灵之器,可引动灵力。', icon: '法' }, 'weapon-ling': { id: 'weapon-ling', name: '灵器', kind: 'artifact', basePrice: 1400, desc: '有灵之器,锋芒隐露。', icon: '灵' }, - 'weapon-fa': { id: 'weapon-fa', name: '法宝', kind: 'artifact', basePrice: 3600, desc: '罕世法宝,非金丹不能驾驭。', icon: '宝' } + 'weapon-fa': { id: 'weapon-fa', name: '法宝', kind: 'artifact', basePrice: 3600, desc: '罕世法宝,非金丹不能驾驭。', icon: '宝' }, + + // 0.1.33 闭环:丹方/铸器产出物归位(此前仅存于配方,物品表无定义——幽灵物品) + 'pill-heling': { id: 'pill-heling', name: '和灵丹', kind: 'pill', basePrice: 120, desc: '和理灵机,金丹以下均可服。', icon: '和' }, + 'pill-xudan': { id: 'pill-xudan', name: '虚元丹', kind: 'pill', basePrice: 320, desc: '丹中朱砂,元婴以下服之可提元。', icon: '虚' }, + 'weapon-yu': { id: 'weapon-yu', name: '玉澜刃', kind: 'artifact', basePrice: 950, desc: '温润如玉,锋芒内敛的灵木之刃。', icon: '澜' } } export const ARTIFACT_POWER: Record = { 'weapon-fan': 0.1, 'weapon-qi': 0.25, 'weapon-ling': 0.5, - 'weapon-fa': 0.9 + 'weapon-fa': 0.9, + 'weapon-yu': 0.4 } export function itemById(id: string): ItemDef { diff --git a/src/renderer/game/engine/runtime/Systems/combat.ts b/src/renderer/game/engine/runtime/Systems/combat.ts index b533eb9..5a0ba19 100644 --- a/src/renderer/game/engine/runtime/Systems/combat.ts +++ b/src/renderer/game/engine/runtime/Systems/combat.ts @@ -59,11 +59,18 @@ const DRAW_DESC = [ '僵持半晌,天入暮色,双方收阵戒备而退。' ] -/** R4 符箓开战:家族武备库存有风符/山符时,开战消耗 1 张(全体战力 +6%) */ -function talismanWarPrep(w: World): void { +/** 0.1.33 符箓开战(零 rng):消耗 1 张并返回其类型——风符全体战力+12%,山符+15%且受创减半 */ +function talismanWarPrep(w: World): 'feng' | 'shan' | null { const inv = w.state.family.inventory - const id = (inv['talisman-feng'] ?? 0) > 0 ? 'talisman-feng' : (inv['talisman-shan'] ?? 0) > 0 ? 'talisman-shan' : null - if (id) inv[id] = inv[id]! - 1 + if ((inv['talisman-feng'] ?? 0) > 0) { + inv['talisman-feng'] = inv['talisman-feng']! - 1 + return 'feng' + } + if ((inv['talisman-shan'] ?? 0) > 0) { + inv['talisman-shan'] = inv['talisman-shan']! - 1 + return 'shan' + } + return null } export function resolveEncounter( @@ -80,8 +87,11 @@ export function resolveEncounter( } ): EncounterResult { const form = formationById(opts.formation) + const talisman = opts.kind === 'war' ? talismanWarPrep(w) : null + const talismMult = talisman === 'feng' ? 1.12 : talisman === 'shan' ? 1.15 : 1 let team = 0 for (const c of opts.team) team += combatPowerOf(w, c) + team *= talismMult const enemy = Math.round(enemyPowerOf(opts.enemy, opts.risk) * form.def) const jitter = w.rng.between(0.88, 1.12) const teamFinal = Math.round(team * form.atk * jitter) @@ -115,7 +125,7 @@ export function resolveEncounter( loss.push(`${c.name} 陨落`) w.chronicle('death', `${c.name} 战殁于${opts.enemy.name},一身所学俱付尘烟。`, c.id, true) } else if (severity < 0.45) { - const woundAmt = Math.round((40 + w.rng.int(0, 25)) * form.retreatWound) + const woundAmt = Math.round((40 + w.rng.int(0, 25)) * form.retreatWound * (talisman === 'shan' ? 0.5 : 1)) c.health = Math.max(1, c.health - woundAmt) c.state = 'wounded' loss.push(`${c.name} 重伤`) @@ -138,7 +148,7 @@ export function resolveEncounter( lines.push(`此战缴获:${Object.entries(loot).map(([k, v]) => `${itemName(k)} ×${v}`).join('、')}。`) } - if (opts.kind === 'war') talismanWarPrep(w) + if (talisman) lines.push(`武备先声:${talisman === 'feng' ? '风符化翼' : '山符镇阵'},家士气为之振。`) if (win && opts.kind === 'war') w.emitFx('blade', 'battle') const result: EncounterResult = { win, draw, lines, loot, losses: loss } const log: BattleLog = { diff --git a/src/renderer/game/engine/runtime/Systems/production.ts b/src/renderer/game/engine/runtime/Systems/production.ts index ae4c5e2..9f7f67c 100644 --- a/src/renderer/game/engine/runtime/Systems/production.ts +++ b/src/renderer/game/engine/runtime/Systems/production.ts @@ -36,11 +36,12 @@ export function productionTick(w: World): void { parts.push(`${def!.name}+${prod}${itemNameOf(k)}`) } } - produceAll('lingtian', lingtian, 1 + fielders * 0.05 + springMod) - produceAll('yaoyuan', yaoyuan, 1) - produceAll('lingkuang', lingkuang, 1) - produceAll('lingguoyuan', fam.buildings['lingguoyuan'] ?? 0, 1) - produceAll('lingmufang', fam.buildings['lingmufang'] ?? 0, 1) + // 0.1.33 灾年真正生效(此前 farmFactor/mineFactor 算而未用——生产零体感) + produceAll('lingtian', lingtian, (1 + fielders * 0.05 + springMod) * farmFactor) + produceAll('yaoyuan', yaoyuan, farmFactor) + produceAll('lingkuang', lingkuang, mineFactor) + produceAll('lingguoyuan', fam.buildings['lingguoyuan'] ?? 0, farmFactor) + produceAll('lingmufang', fam.buildings['lingmufang'] ?? 0, farmFactor) 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)) @@ -51,6 +52,21 @@ export function productionTick(w: World): void { inv.beastcore = (inv.beastcore ?? 0) + 1 parts.push('灵兽园+1兽核') } + // 0.1.33 符箓坊(消耗式:符纸×2+灵木×2/级 → 风符;叁级追加山符) + const fuzhiLvl = lvl('fuzhifang') + if (fuzhiLvl > 0) { + const need = 2 * fuzhiLvl + if ((inv['fuzhi'] ?? 0) >= need && (inv['lingmu'] ?? 0) >= need) { + inv['fuzhi'] -= need + inv['lingmu'] -= need + inv['talisman-feng'] = (inv['talisman-feng'] ?? 0) + 1 + parts.push(`符箓坊-符纸${need}灵木${need}+风符1`) + if (fuzhiLvl >= 3) { + inv['talisman-shan'] = (inv['talisman-shan'] ?? 0) + 1 + parts.push('符箓坊+山符1') + } + } + } void parts diff --git a/src/renderer/game/engine/runtime/World.ts b/src/renderer/game/engine/runtime/World.ts index f9713d0..1ad3eb3 100644 --- a/src/renderer/game/engine/runtime/World.ts +++ b/src/renderer/game/engine/runtime/World.ts @@ -798,7 +798,7 @@ export class World { return true } - craftPill(kind: 'qiyuan' | 'ningyuan' | 'pojing'): boolean { + craftPill(kind: 'qiyuan' | 'ningyuan' | 'pojing' | 'heling' | 'xudan'): boolean { const fam = this.state.family const lvl = fam.buildings['danfang'] const r = pillRecipeByOutput(`pill-${kind}`) @@ -825,7 +825,7 @@ export class World { return false } - forgeArtifact(kind: 'weapon-qi' | 'weapon-ling' | 'weapon-fa'): boolean { + forgeArtifact(kind: 'weapon-qi' | 'weapon-ling' | 'weapon-fa' | 'weapon-yu'): boolean { const fam = this.state.family const r = forgeRecipeByOutput(kind) if (!r) return false diff --git a/src/renderer/ui/panels/MarketPanel.tsx b/src/renderer/ui/panels/MarketPanel.tsx index 7e3cd0b..7425023 100644 --- a/src/renderer/ui/panels/MarketPanel.tsx +++ b/src/renderer/ui/panels/MarketPanel.tsx @@ -181,7 +181,7 @@ export default function MarketPanel() { className="btn btn-sm" disabled={lvl < r.danfangLevel || !enough} title={lvl < r.danfangLevel ? `丹房需升到 ${r.danfangLevel} 级` : enough ? '炼制(失败折半退料)' : '材料不足'} - onClick={() => { w.craftPill(r.output.replace('pill-', '') as 'qiyuan' | 'ningyuan' | 'pojing'); bump() }} + onClick={() => { w.craftPill(r.output.replace('pill-', '') as Parameters[0]); bump() }} >炼 @@ -207,7 +207,7 @@ export default function MarketPanel() { className="btn btn-sm" disabled={!enough} title={enough ? '淬火铸成' : '材料不足'} - onClick={() => { w.forgeArtifact(r.output as 'weapon-qi' | 'weapon-ling' | 'weapon-fa'); bump() }} + onClick={() => { w.forgeArtifact(r.output as Parameters[0]); bump() }} >铸 diff --git a/src/renderer/ui/store.ts b/src/renderer/ui/store.ts index 767ab30..a3f45ed 100644 --- a/src/renderer/ui/store.ts +++ b/src/renderer/ui/store.ts @@ -553,7 +553,7 @@ function actDirect(w: World, name: ActName, payload: import('../game/engine/runt case 'market.buy': return payload.item ? marketBuy(w, payload.item, payload.count ?? 1) : false case 'market.sell': return payload.item ? marketSell(w, payload.item, payload.count ?? 1) : false case 'market.tech': return payload.tech ? marketTechnique(w, payload.tech, payload.stones ?? 0) : false - case 'craft.pill': return payload.item === 'ningyuan' ? w.craftPill('ningyuan') : payload.item === 'pojing' ? w.craftPill('pojing') : payload.item === 'qiyuan' ? w.craftPill('qiyuan') : false + case 'craft.pill': return w.craftPill(payload.item as 'qiyuan' | 'ningyuan' | 'pojing' | 'heling' | 'xudan') case 'diplomacy.gift': return payload.npcId ? dipGift(w, payload.npcId, payload.stones ?? 0) : false case 'diplomacy.peace': return payload.npcId ? dipPeace(w, payload.npcId) : false case 'diplomacy.taunt': return payload.npcId ? w.tauntNpc(payload.npcId) : false diff --git a/tests/audit-regression.test.ts b/tests/audit-regression.test.ts index c407a4b..4d174e7 100644 --- a/tests/audit-regression.test.ts +++ b/tests/audit-regression.test.ts @@ -82,7 +82,7 @@ describe('审计回归:P0 修复固化', () => { }) it('防御性修补后金钟罩不变(行为等价确认)', () => { - expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('778d1488') - expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('00ac5fa4') + expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('0a76a33f') + expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('03184a16') }) }) diff --git a/tests/clock.test.ts b/tests/clock.test.ts index f3b3a76..3d8d096 100644 --- a/tests/clock.test.ts +++ b/tests/clock.test.ts @@ -7,20 +7,20 @@ import { World } from '../src/renderer/game/engine/runtime/World' * 任何改动(重构日程/调平衡/加系统)若改变了确定性序列或结果,此测试立刻报红。 * 更新规则:仅当**有意**变更序列逻辑时,三枚 seed 指纹同版更新并注明原因。 */ -// 0.1.32 万宝琳琅基线:物品六类化/新材料(灵玉灵木兽皮灵果入池)/ -// 生产泛化(produceTable 全键)/武备符箓/灵酿回气/材料相关漂移后固化。 +// 0.1.33 闭环·止血基线:符箓开战生效(战力乘量+山符减伤)/灾年因子真正接入/ +// 幽灵物品归位(新丹方/玉澜刃)+ 符箓坊(消耗式产符)后固化。 const GOLDEN: Record> = { - 'bell-seed-1': { 560: '778d1488', 1200: 'a098f660', 2160: 'bc511374' }, - 'bell-seed-2': { 560: 'dfab016c', 1200: 'dc3d843f', 2160: '7096cf66' }, - 'bell-seed-3': { 560: '00ac5fa4', 1200: 'e2278a35', 2160: '173d2770' } + 'bell-seed-1': { 560: '0a76a33f', 1200: 'c9407e76', 2160: '4bc550bd' }, + 'bell-seed-2': { 560: 'b1b4d35b', 1200: 'fcc6e262', 2160: '1af450b7' }, + 'bell-seed-3': { 560: '03184a16', 1200: 'c73bb4b1', 2160: 'fca9a59f' } } /** 第二金钟罩:自动 resolve 长跑("现实"世界——每 tick 处理待决事件; * 锁事件闸/事件流全程,防"冻结世界"指纹漏锁)。 */ const GOLDEN_RESOLVED: Record> = { - 'bell-seed-1': { 560: '9c603274', 1200: 'e8ada769', 2160: '3e632c02' }, - 'bell-seed-2': { 560: '3c952764', 1200: '48e457fd', 2160: '6caa2e3e' }, - 'bell-seed-3': { 560: 'de77eb9f', 1200: '2d4a1039', 2160: '41cf5c22' } + 'bell-seed-1': { 560: '4b0f1f81', 1200: '430d6281', 2160: 'ba0d3816' }, + 'bell-seed-2': { 560: '8865b178', 1200: 'bba0d797', 2160: '6205c6f1' }, + 'bell-seed-3': { 560: 'fa736e3c', 1200: '819b9def', 2160: 'a624ced8' } } const TIERS = [ diff --git a/tests/economy-loop-0.1.33.test.ts b/tests/economy-loop-0.1.33.test.ts new file mode 100644 index 0000000..bdb7e36 --- /dev/null +++ b/tests/economy-loop-0.1.33.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from 'vitest' +import { World } from '../src/renderer/game/engine/runtime/World' +import { ITEMS, ARTIFACT_POWER } from '../src/renderer/game/data/items' +import { resolveEncounter } from '../src/renderer/game/engine/runtime/Systems/combat' +import { productionTick } from '../src/renderer/game/engine/runtime/Systems/production' + +function mk(seed: string): World { + return World.create({ seed, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' }) +} + +describe('0.1.33 物品闭环:符箓生效/幽灵物归位/灾年生效', () => { + it('幽灵物品归位:丹方/铸器产出物均可命中物品表且可炼成', () => { + const w = mk('c33-ghost') + w.state.family.stones = 100000 + w.state.family.inventory['lingcao'] = 1000 + w.state.family.inventory['beastcore'] = 100 + w.state.family.inventory['lingkuang'] = 500 + w.state.family.buildings['danfang'] = 5 + let ok = false + for (let i = 0; i < 30 && !ok; i++) ok = w.craftPill('heling') || w.craftPill('xudan') || w.forgeArtifact('weapon-yu') + expect(ok).toBe(true) + expect(ITEMS['pill-heling']).toBeTruthy() + expect(ITEMS['pill-xudan']).toBeTruthy() + expect(ITEMS['weapon-yu']).toBeTruthy() + expect(ARTIFACT_POWER['weapon-yu']).toBe(0.4) + expect((w.state.family.inventory['pill-heling'] ?? 0) + (w.state.family.inventory['pill-xudan'] ?? 0) + (w.state.family.inventory['weapon-yu'] ?? 0) >= 1).toBe(true) + }) + + it('符箓开战生效:消耗 1 张且战报记武备先声', () => { + const w = mk('c33-fu') + w.state.family.inventory['talisman-feng'] = 2 + const crew = w.aliveMembers().slice(0, 3) + const res = resolveEncounter(w, { + title: '试锋', enemy: { id: 'z', name: '妖兽', realm: 'qi', strength: 0.4, desc: '野怪' }, + risk: 0.2, team: crew, kind: 'war', year: w.state.year, month: w.state.month + }) + expect(w.state.family.inventory['talisman-feng']).toBe(1) + expect(res.lines.some((l) => l.includes('武备先声'))).toBe(true) + }) + + it('灾年生效:同一世界无灾×0.7 减产(旱灾)实际作用', () => { + const w = mk('c33-cal-a') + w.state.family.buildings['lingtian'] = 2 + w.state.family.inventory['lingcao'] = 0 + w.advanceMonth() + if (w.state.worldSim) w.state.worldSim.calamity = '旱灾' + const before = w.state.family.inventory['lingcao'] ?? 0 + productionTick(w) + const gain = (w.state.family.inventory['lingcao'] ?? 0) - before + expect(gain).toBeGreaterThan(0) + if (w.state.worldSim) w.state.worldSim.calamity = undefined + const before2 = w.state.family.inventory['lingcao'] ?? 0 + productionTick(w) + const gain2 = (w.state.family.inventory['lingcao'] ?? 0) - before2 + expect(gain).toBeLessThan(gain2) + expect(Math.abs(gain - gain2 * 0.7)).toBeLessThanOrEqual(1) + }) +}) diff --git a/tests/facade-registry.test.ts b/tests/facade-registry.test.ts index 699c158..a68646e 100644 --- a/tests/facade-registry.test.ts +++ b/tests/facade-registry.test.ts @@ -170,7 +170,7 @@ describe('GameFacade 门面', () => { it('默认配置金钟罩不受门面化影响', () => { PACK.reset() - expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('778d1488') - expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('00ac5fa4') + expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('0a76a33f') + expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('03184a16') }) })