diff --git a/package.json b/package.json index 47f3a61..f478fb6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "chronicle-of-the-immortal-clan", "productName": "仙途家族志", - "version": "0.1.16", + "version": "0.1.17", "description": "修仙 · 家族 · 经营 · 战斗 模拟器", "main": "./out/main/index.js", "author": "MetonaTeam", diff --git a/src/renderer/game/engine/runtime/ApiFacade.ts b/src/renderer/game/engine/runtime/ApiFacade.ts index 194dbbf..12de8c9 100644 --- a/src/renderer/game/engine/runtime/ApiFacade.ts +++ b/src/renderer/game/engine/runtime/ApiFacade.ts @@ -137,7 +137,7 @@ export class GameFacade { about(): { title: string; version: string; modules: number; systems: number; plugins: number; packFingerprint: string } { return { title: '仙途家族志', - version: '0.1.16', + version: '0.1.17', modules: this.world.systemList().length, systems: this.world.systemList().filter((s) => s.enabled).length, plugins: this.world.pluginList().length, diff --git a/src/renderer/game/engine/runtime/Systems/combat.ts b/src/renderer/game/engine/runtime/Systems/combat.ts index 61b9dd6..4d1b28c 100644 --- a/src/renderer/game/engine/runtime/Systems/combat.ts +++ b/src/renderer/game/engine/runtime/Systems/combat.ts @@ -190,9 +190,16 @@ export function resolveRaid( if (res.win) { npc.relation = Math.min(60, npc.relation + 25) w.state.family.reputation += 6 + // B5:战争伤骨——败方 power 重挫,次年不再来犯(warCooldownYear/raidCount 启用) + npc.power = Math.max(40, Math.round(npc.power * 0.82)) + npc.raidCount = (npc.raidCount ?? 0) + 1 + npc.warCooldownYear = w.state.year w.chronicle('battle', `击退${npc.name}的犯境,家族声威大振。`, undefined, true) } else if (!res.draw) { npc.relation = Math.max(-100, npc.relation - 15) + npc.raidCount = (npc.raidCount ?? 0) + 1 + npc.warCooldownYear = w.state.year + npc.power = Math.min(900, Math.round(npc.power * 1.06)) const st = w.state.family.stones const lostFew = Math.min(st, Math.round(st * 0.25)) w.state.family.stones -= lostFew diff --git a/src/renderer/game/engine/runtime/Systems/diplomacy.ts b/src/renderer/game/engine/runtime/Systems/diplomacy.ts index 0463c27..ece8d6f 100644 --- a/src/renderer/game/engine/runtime/Systems/diplomacy.ts +++ b/src/renderer/game/engine/runtime/Systems/diplomacy.ts @@ -49,6 +49,17 @@ export function giftNpc(w: World, npcId: string, stones: number): boolean { const npc = w.state.npcFamilies[npcId] const gain = calcGiftGain(stones) npc.relation = Math.min(100, npc.relation + gain) + npc.power = Math.min(900, npc.power + gain * 0.4) + // 回礼:盛情难却,或赠灵石或赠灵草 + if (w.rng.chance(0.3)) { + const back = Math.round(gain * 1.5) + w.state.family.stones += back + w.log('info', `${npc.name} 回赠灵石${back},来而往之。`) + } else if (w.rng.chance(0.3)) { + const back = 2 + w.rng.int(0, 3) + w.state.family.inventory['lingcao'] = (w.state.family.inventory['lingcao'] ?? 0) + back + w.log('info', `${npc.name} 回赠灵草${back}株。`) + } w.log('info', `厚礼送往${npc.name},两家关系 +${gain}。`) return true } @@ -59,6 +70,7 @@ export function makePeace(w: World, npcId: string): boolean { if (fam.stones < 200) return false fam.stones -= 200 npc.relation = Math.max(npc.relation + 35, 30) + npc.power = Math.min(900, npc.power + 5) w.chronicle('diplomacy', `${npc.name}立下和约,两家罢兵互市。`, undefined, true) w.log('good', `与${npc.name}言和。`) return true diff --git a/src/renderer/game/engine/runtime/World.ts b/src/renderer/game/engine/runtime/World.ts index 0214752..7f352a9 100644 --- a/src/renderer/game/engine/runtime/World.ts +++ b/src/renderer/game/engine/runtime/World.ts @@ -454,6 +454,7 @@ export class World { if (npc.relation < 40) return false npc.allied = true npc.alliedSinceYear = this.state.year + npc.power = Math.min(900, npc.power + 10) this.log('good', `与${npc.name}结成同盟——盟誓既立,互不犯边。`) this.chronicle('diplomacy', `本族与${npc.name}缔结同盟。`, undefined, true) return true diff --git a/src/renderer/game/engine/sim/Market.ts b/src/renderer/game/engine/sim/Market.ts index 2cf7369..00334f3 100644 --- a/src/renderer/game/engine/sim/Market.ts +++ b/src/renderer/game/engine/sim/Market.ts @@ -11,14 +11,8 @@ export function marketPrice(w: World, itemId: string): number { const fam = w.state.family const mult = typeof fam.flag['priceMult'] === 'number' ? (fam.flag['priceMult'] as number) : 1 const mood = fam.reputation >= 40 ? 1.06 : fam.reputation >= 20 ? 1.02 : 0.98 - // 世界行情乘子(缺省 1) - let simMult = 1 - if (w.state.worldSim?.marketPool) { - const pool = w.state.worldSim.marketPool[itemId] as number | undefined - const basePool = POOL_BASE[itemId] ?? 100 - const r = (pool ?? basePool) / basePool - simMult = Math.max(0.55, Math.min(2.3, r)) - } + // 世界行情乘子(唯一读口:worldSim.marketMultFor;无 sim 时 1) + const simMult = w.state.worldSim ? new WorldSim(w).marketMultFor(itemId) : 1 // 利己(priceMult)与信誉修正 const sellers = w.aliveMembers().filter((c) => traitBonuses(c).priceMult > 0).length const liarPct = Math.min(0.2, sellers * 0.04) @@ -28,10 +22,14 @@ export function marketPrice(w: World, itemId: string): number { export function buyItem(w: World, itemId: string, count: number): boolean { const fam = w.state.family if (!pack().items[itemId]) return false + const sim = w.state.worldSim ? new WorldSim(w) : null + // 断供告急:池深低于下限时拒单(零库存市场无货可买;世界生产与 NPC 供给会回填) + if (sim && sim.poolDepthOf(itemId) < WORLDSIM.tradeFloorPct && w.state.totalTicks > 12) return false const total = marketPrice(w, itemId) * count if (total > fam.stones) return false fam.stones -= total fam.inventory[itemId] = (fam.inventory[itemId] ?? 0) + count + sim?.tradeSettle(itemId, -count) return true } @@ -42,6 +40,7 @@ export function sellItem(w: World, itemId: string, count: number): boolean { if (have < count) return false fam.inventory[itemId] = have - count fam.stones += marketPrice(w, itemId) * count + if (w.state.worldSim) new WorldSim(w).tradeSettle(itemId, count) return true } @@ -59,8 +58,7 @@ export function techniquePrice(techId: string): number { } import { TECHNIQUES } from '../../data/techniques' - -const POOL_BASE: Record = { lingcao: 600, lingkuang: 300, beastcore: 80, 'pill-qiyuan': 90, 'pill-ningyuan': 40 } +import { WORLDSIM } from './worldsim-data' const TECHNIQUE_GRADE_PRICE: Record = { 1: 120, 2: 300, 3: 700, 4: 1600 } const TECH_GRADE_BASE: Record = Object.fromEntries( diff --git a/src/renderer/game/engine/sim/WorldSim.ts b/src/renderer/game/engine/sim/WorldSim.ts index 024404b..463b082 100644 --- a/src/renderer/game/engine/sim/WorldSim.ts +++ b/src/renderer/game/engine/sim/WorldSim.ts @@ -37,13 +37,16 @@ export class WorldSim { s.secretQi[key] = clamp(s.secretQi[key] + (s.tide > 0.85 ? 5 : 2) - (s.secretQi[key] > 70 ? 2 : 0), 0, 100) } - // ---- D. 灾变年签(每年首月一掷) ---- + // ---- D. 灾年(年签一掷 + 持续渐退;B12) ---- + // 剩数递减 + if ((s.calamityLeft ?? 0) > 0) s.calamityLeft = (s.calamityLeft ?? 0) - 1 if (this.w.state.month === 1 && rng.chance(WORLDSIM.calamityChance)) { const cl = rng.pick([...WORLDSIM.calamities]) s.calamity = cl s.calamityYear = this.w.state.year + s.calamityLeft = WORLDSIM.calamityMonths applyCalamityToMarket(s, cl as CalamityName) - this.w.log('bad', `【天下灾年】${cl}——灵植减产,市价将行。`) + this.w.log('bad', `【天下灾年】${cl}——灵植减产,市价将行(约半年风雨)。`) // B7 灾年落家族(一次性体感效果) if (cl === '疫病') { for (const c of this.w.aliveMembers()) { @@ -60,14 +63,23 @@ export class WorldSim { inv(this.w)['beastcore'] = (inv(this.w)['beastcore'] ?? 0) + 2 this.w.log('bad', `兽潮涌至,坊市稍有折损;猎得兽核数枚。`) } - } else { + } + // 灾年到期散去 + if (s.calamity && (s.calamityLeft ?? 0) <= 0) { s.calamity = undefined + s.newsFeed.push({ + year: this.w.state.year, month: this.w.state.month, src: '天道', + text: `灾云散尽,灵气复清,天下重归平宁。`, kind: 'calamity' + }) + this.w.log('info', `【天下】灾云散尽,灵气复清。`) } - // ---- A. 资源循环市场(库存自然流向 + 再平衡 + 价格信号) ---- - driftMarket(s, rng.next()) + // ---- A. 资源循环市场(世界供给/需求 + NPC 上桌 + 再平衡) ---- + worldBreath(this.w, s) // A2+A3:常驻供给与需求(潮汐乘化) + npcTrade(this.w, s, rng.next()) // A4:NPC 按 sells/buys 与池交易 + driftMarket(s, rng.next()) // 波动项(保留噪声弹性) - // ---- B. NPC 演化(聚合模拟) ---- + // ---- B. NPC 演化(换代 + 关系网 + 互攻;B7) ---- evolveNpc(this.w, s, rng.next()) // ---- E. 天下快讯(节流) ---- @@ -97,14 +109,30 @@ export class WorldSim { return this.s().tide } - /** 当前市场行情(价格乘子,反馈到 Market) */ + /** 当前市场行情(价格乘子,Market 唯一读口) */ marketMultFor(id: string): number { const s = this.s() - const pool = s.marketPool[id] ?? 50 + const pool = s.marketPool[id] ?? 60 const base = poolBase(id) return clamp(pool / base, WORLDSIM.priceFloor, WORLDSIM.priceCeil) } + /** 玩家交易回写池:买 -delta / 卖 +delta(单件约 0.35 池份额波动) */ + tradeSettle(resKey: string, delta: number): void { + const s = this.s() + const base = poolBase(resKey) + const cur = s.marketPool[resKey] ?? base + const impact = delta * 0.35 + s.marketPool[resKey] = clamp(cur + impact, base * 0.12, base * WORLDSIM.tradeCeilPct) + } + + /** 池深比率(0.12~2.4):断供告急时 < tradeFloorPct */ + poolDepthOf(resKey: string): number { + const s = this.s() + const base = poolBase(resKey) + return (s.marketPool[resKey] ?? base) / base + } + news(): WorldSimState['newsFeed'] { return this.s().newsFeed } @@ -149,6 +177,52 @@ function empty(): WorldSimState { } } +/** A2+A3:世界常驻供给与需求(潮汐乘化)——池有了呼吸 */ +function worldBreath(w: World, s: WorldSimState): void { + const tide = s.tide + const span = 1 - (tide - 1) * WORLDSIM.tideSupplySpan + const supplySpan = Math.max(0.75, Math.min(1.35, span)) + for (const id of MARKET_IDS) { + const base = poolBase(id) + const cur = s.marketPool[id] ?? base + const supply = base * WORLDSIM.worldSupplyRate * supplySpan + const demand = base * WORLDSIM.worldDemandRate + s.marketPool[id] = clamp(cur + supply - demand, base * 0.12, base * WORLDSIM.tradeCeilPct) + } + void w +} + +/** A4:NPC 按 def.sells/buys 与池交易——世界波动的背后有了玩家(NPC 化) */ +function npcTrade(w: World, s: WorldSimState, noise: number): void { + let n = noise + for (const [id, npc] of Object.entries(w.state.npcFamilies)) { + const per = n; n += 0.31 + const def = npcById(id) + const isCalamity = !!s.calamity + const rate = WORLDSIM.npcTradeRate * (isCalamity ? 0.7 : 1) + // 卖:NPC 抛货 → 池增(量小到可忽略贸易盈亏,只表潮流) + for (const itemId of def.sells ?? []) { + if (!MARKET_IDS.includes(itemId)) continue + const base = poolBase(itemId) + const vol = base * rate * (0.6 + (per % 1) * 0.8) + s.marketPool[itemId] = clamp((s.marketPool[itemId] ?? base) + vol, base * 0.12, base * WORLDSIM.tradeCeilPct) + } + // 买:NPC 采购 → 池减;池太浅采不到 → NPC 繁荣受挫(power 微跌) + for (const itemId of def.buys ?? []) { + if (!MARKET_IDS.includes(itemId)) continue + const base = poolBase(itemId) + const cur = s.marketPool[itemId] ?? base + const vol = base * rate * (0.6 + ((per * 7) % 1) * 0.8) + if (cur - vol < base * 0.12) { + npc.power = Math.max(40, Math.round(npc.power * 0.985)) + } else { + s.marketPool[itemId] = cur - vol + npc.power = Math.min(900, npc.power + 0.5) + } + } + } +} + function driftMarket(s: WorldSimState, noise: number): void { let n = noise for (const id of MARKET_IDS) { @@ -181,14 +255,33 @@ function poolBase(id: string): number { function evolveNpc(w: World, s: WorldSimState, noise: number): void { void noise const year = w.state.year + const ids = Object.keys(w.state.npcFamilies) for (const [id, npc] of Object.entries(w.state.npcFamilies)) { const dyn = s.npcDyn[id] ?? initDynFor(id) s.npcDyn[id] = dyn + if (!dyn.relationsWithOthers || Object.keys(dyn.relationsWithOthers).length === 0) { + // 关系网初始化:同风格亲近(剑修→剑修 +30~55),异风格事仇(-35~+15) + const mine = npcById(id) + for (const oid of ids) { + if (oid === id) continue + const other = npcById(oid) + const sameStyle = mine.style === other.style + dyn.relationsWithOthers[oid] = sameStyle + ? 30 + w.rng.int(0, 25) + : -35 + w.rng.int(0, 50) + } + } const def = npcById(id) const curRealm = MAJOR_ORDER[dyn.leaderRealmIdx] ?? def.leaderRealm const lifespan = MAJORS[curRealm].lifespan if (w.state.month === 1) { dyn.leaderAge++ + // 关系漂移:年首各 ±5(邻近的讲合、世仇的愈深) + for (const oid of ids) { + if (oid === id) continue + const cur = dyn.relationsWithOthers[oid] ?? 0 + dyn.relationsWithOthers[oid] = clamp(cur + (w.rng.chance(0.5) ? 1 : -1) * 5, -100, 100) + } // 换代评估:寿终阈值 或 年首小概率(退隐/遇害)——约 50 年一代 const chanceNow = dyn.leaderAge > lifespan * 0.9 ? 0.5 : 0.02 if (w.rng.chance(chanceNow) && !w.rng.chance(0.25)) { @@ -202,8 +295,26 @@ function evolveNpc(w: World, s: WorldSimState, noise: number): void { pushNews(w, s, [id]) w.log('info', `【天下】${def.name} 更易宗主,气象一新。`) } + // B7 互攻:与世仇(关系<-50)年首相搏——败方伤筋动骨 + for (const oid of ids) { + if (oid === id) continue + const foe = w.state.npcFamilies[oid] + if (!foe) continue + const rel = dyn.relationsWithOthers[oid] ?? 0 + if (rel < -50 && w.rng.chance(WORLDSIM.npcEventChance)) { + const winner = w.rng.chance(0.5) ? npc : foe + const loser = winner === npc ? foe : npc + winner.power = Math.min(900, Math.round(winner.power * 1.06)) + loser.power = Math.max(40, Math.round(loser.power * 0.82)) + s.newsFeed.push({ + year, month: 1, src: winner.name, + text: `${winner.name} 与 ${loser.name} 起衅——痛挫其锋,势力大动。` + }) + w.log('info', `【天下】${winner.name} 击破 ${loser.name},气象一新。`) + break + } + } } - void npc } } @@ -224,7 +335,7 @@ function pushNews(w: World, s: WorldSimState, about: string[]): void { const idx = rng.int(0, about.length - 1) const id = about[idx] const tide = s.tide > 1.0 ? '灵潮上涨' : s.tide < 0.75 ? '灵潮回落' : '汐平' - let row: { year: number; month: number; src: string; text: string } + let row: { year: number; month: number; src: string; text: string; itemId?: string; kind?: 'quote' | 'npc' | 'calamity' } if (id.startsWith('n-')) { const def = npcById(id) const dyn = s.npcDyn[id] @@ -232,7 +343,8 @@ function pushNews(w: World, s: WorldSimState, about: string[]): void { year: w.state.year, month: w.state.month, src: def.name, - text: dyn ? `灵潮气象:${dyn.leaderName} 宗主更替,势力重排。` : `${def.name} 世务新张。` + text: dyn ? `灵潮气象:${dyn.leaderName} 宗主更替,势力重排。` : `${def.name} 世务新张。`, + kind: 'npc' } } else { const pool = clamp(s.marketPool[id] ?? poolBase(id), POOL_BASE[id] * 0.5, POOL_BASE[id] * 1.8) @@ -243,7 +355,9 @@ function pushNews(w: World, s: WorldSimState, about: string[]): void { year: w.state.year, month: w.state.month, src: `世界·${itemName}`, - text: pct !== 0 ? `${itemName}行情${pct > 0 ? '升' : '降'}${Math.abs(pct)}% (${tide})` : `${itemName}行情平稳 (${tide})` + text: pct !== 0 ? `${itemName}行情${pct > 0 ? '升' : '降'}${Math.abs(pct)}% (${tide})` : `${itemName}行情平稳 (${tide})`, + itemId: id, + kind: 'quote' } } s.newsFeed.push(row) diff --git a/src/renderer/game/engine/sim/worldsim-data.ts b/src/renderer/game/engine/sim/worldsim-data.ts index 81fbc28..6363b78 100644 --- a/src/renderer/game/engine/sim/worldsim-data.ts +++ b/src/renderer/game/engine/sim/worldsim-data.ts @@ -25,11 +25,13 @@ export interface WorldSimState { tide: number tideDir: 1 | -1 tideTicks: number - /** 灾年(当年灾因 id) */ + /** 灾年(当前灾因 id;持续 calamityMonths 个月) */ calamity?: string calamityYear: number - /** 天下快讯(滚动 N=120) */ - newsFeed: { year: number; month: number; src: string; text: string }[] + /** 灾年剩余月数(递减,归 0 时散去) */ + calamityLeft?: number + /** 天下快讯(滚动 N=120;itemId/kind 供 UI 响应按钮与区分) */ + newsFeed: { year: number; month: number; src: string; text: string; itemId?: string; kind?: 'quote' | 'npc' | 'calamity' }[] lastNewsMonth: number /** NPC 换代计数 */ npcSuccessions: number @@ -44,6 +46,7 @@ export function makeWorldSimState(): WorldSimState { tideDir: 1, tideTicks: 0, calamityYear: -99, + calamityLeft: 0, newsFeed: [], lastNewsMonth: -99, npcSuccessions: 0 @@ -65,14 +68,26 @@ export const WORLDSIM = { marketRebalance: 0.1, priceFloor: 0.55, priceCeil: 2.3, + // —— 0.1.17 真库存循环 —— + worldSupplyRate: 0.02, // 世界侧月供给(base 比例;潮涨时 ×1.3) + worldDemandRate: 0.015, // 世界侧月需求(坊市/宗门常驻消耗) + npcTradeRate: 0.008, // 每 NPC 月贸易量(base 比例;上桌食量) + tradeFloorPct: 0.35, // 池低于此比例时玩家买入拒单(断供告急) + tradeCeilPct: 2.4, // 池上限(玩家大量卖出后价格封顶) + tideSupplySpan: 0.3, // 潮汐对供给的浮动幅度(tide±0.35 时 ×(1∓0.3)) + // —— 灾年持续 —— + calamityMonths: 6, // 灾年效果持续月数(结束月报“灾云散尽”) + // —— 潮汐/秘境 —— tideCycle: 72, // 月周期(6年) tideMin: 0.65, tideMax: 1.35, - secretRecover: 4, - secretConsume: 0, + secretRecover: 2, // 灵气月恢复(潮高 ×2.5 → 用 secretRecoverHi) + secretRecoverHi: 5, + secretConsume: 6, // 每次探索消耗(missions 默认) + // —— 天下事件 —— calamityChance: 0.18, calamities: ['旱灾', '涝灾', '蝗灾', '疫病', '兽潮', '寒潮'] as const, - npcEventChance: 0.05, + npcEventChance: 0.05, // NPC 互攻年首诗签概率 newsEvery: 24, // 月 newsKeep: 120 } diff --git a/src/renderer/ui/panels/MarketPanel.tsx b/src/renderer/ui/panels/MarketPanel.tsx index 2606dad..c8f275c 100644 --- a/src/renderer/ui/panels/MarketPanel.tsx +++ b/src/renderer/ui/panels/MarketPanel.tsx @@ -2,7 +2,7 @@ import { useGameStore } from '../store' import { ITEMS } from '../../game/data/items' import { TECHNIQUES } from '../../game/data/techniques' import { techniqueGradeName } from '../../game/data/realms' -import { marketPrice, buyItem, sellItem, buyTechnique } from '../../game/engine/sim/Market' +import { marketPrice, buyItem, sellItem, buyTechnique, techniquePrice } from '../../game/engine/sim/Market' import { PILL_RECIPES, FORGE_RECIPES } from '../../game/data/items' import { bonusOf } from '../../game/data/buildings' import { useMemo, useState } from 'react' @@ -122,7 +122,7 @@ export default function MarketPanel() { .map((t) => { const owned = fam.techniques.includes(t.id) const p = t.grade <= 1 + cangshuLv - const priceT = techniqueGradeName(t.grade) === '?' ? 300 : [120, 300, 700, 1600][t.grade] ?? 300 + const priceT = techniquePrice(t.id) return ( 《{t.name}》 diff --git a/src/renderer/ui/panels/SettingsPanel.tsx b/src/renderer/ui/panels/SettingsPanel.tsx index 9feabd1..d65e2c3 100644 --- a/src/renderer/ui/panels/SettingsPanel.tsx +++ b/src/renderer/ui/panels/SettingsPanel.tsx @@ -210,7 +210,7 @@ export default function SettingsPanel() { · 「外交」与四邻结好联姻;仇雠之族隔岁来犯,打得赢名望大涨,打不赢蚀钱伤丁。
· 「史书」自动记述繁华与凋零——百年之后,后人翻开这一卷家族志,见代代薪火、历历雪泥。 -
版本 0.1.16 · Chronicle of the Immortal Clan
+
版本 0.1.17 · Chronicle of the Immortal Clan
) diff --git a/src/renderer/ui/panels/WorldPanel.tsx b/src/renderer/ui/panels/WorldPanel.tsx index bce5742..786d7b0 100644 --- a/src/renderer/ui/panels/WorldPanel.tsx +++ b/src/renderer/ui/panels/WorldPanel.tsx @@ -1,7 +1,10 @@ import { useGameStore } from '../store' import { npcById } from '../../game/data/npcs' import { MAJOR_NAMES } from '../../game/data/realms' -import { POOL_BASE } from '../../game/engine/sim/worldsim-data' +import type { WorldSimState } from '../../game/engine/sim/worldsim-data' +import { POOL_BASE, WORLDSIM } from '../../game/engine/sim/worldsim-data' +import { combatPowerOf } from '../../game/engine/runtime/Systems/combat' +import { sellItem, buyItem, marketPrice } from '../../game/engine/sim/Market' import { useState } from 'react' const RES_LABEL: Record = { lingcao: '灵草', lingkuang: '灵矿', beastcore: '兽核' } @@ -15,7 +18,7 @@ export default function WorldPanel() { const [view, setView] = useState(null) if (!world) return null const w = world - const ws = w.state.worldSim + const ws = w.state.worldSim as WorldSimState | undefined if (!ws) return
天机未显……(世界演化尚未展开)
const tide = ws.tide ?? 0.5 @@ -23,6 +26,13 @@ export default function WorldPanel() { const pool = ws.marketPool ?? {} const news = [...(ws.newsFeed ?? [])].slice().reverse() const calamity = ws.calamity + const calamityLeft = ws.calamityLeft ?? 0 + const myPower = Object.values(w.state.members) + .filter((c) => c.alive) + .reduce((a, c) => a + combatPowerOf(w, c), 0) + const allPowers = Object.entries(w.state.npcFamilies).map(([id, n]) => ({ id, name: n.name, power: n.power })) + const myRank = allPowers.filter((x) => x.power > myPower).length + 1 + void myPower return (
@@ -33,15 +43,18 @@ export default function WorldPanel() {
灵潮 {tideLabel}({Math.round(tide * 100)}%) {calamity - ? 灾年·{calamity}(灵植减产,市价将行) + ? 灾年·{calamity}(约{calamityLeft}月后散去) : 风调雨顺} + 本族雄基 {myRank}/{Object.keys(w.state.npcFamilies).length + 1} {Object.entries(pool).map(([k, v]) => { const base = POOL_BASE[k as keyof typeof POOL_BASE] ?? 100 const r = ((v as number) / base) * 100 - const cls = r > 115 ? 'bad' : r < 85 ? 'good' : 'dim' + const cls = r > 115 ? 'bad' : r < WORLDSIM.tradeFloorPct * 100 ? 'good' : 'dim' const dir = r > 105 ? '↑' : r < 95 ? '↓' : '→' return ( - {RES_LABEL[k] ?? k}{dir}{Math.round(r)}% + + {RES_LABEL[k] ?? k}{dir}{Math.round(r)}% + ) })}
@@ -50,12 +63,42 @@ export default function WorldPanel() {

天下快讯

{news.length === 0 &&
风平浪静,尚无消息。
} - {news.map((row, i) => ( + {news.map((row, i) => { + const canBuy = row.kind === 'quote' && row.itemId && (w.state.family.inventory[row.itemId] ?? 0) >= 5 + const canSell = row.kind === 'quote' && row.itemId && w.state.family.stones >= marketPrice(w, row.itemId) * 5 + const isCrisis = row.kind === 'calamity' + return (
{row.year}年{row.month}月 {row.text} + {isCrisis && w.state.family.stones >= 50 && ( + + + + )} + {canBuy && ( + + + + )} + {canSell && !canBuy && ( + + + + )}
- ))} + ) + })}
diff --git a/tests/audit-regression.test.ts b/tests/audit-regression.test.ts index 483c01e..cef161a 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('9531969e') - expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('aeefac96') + expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('c923fdd1') + expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('5edde2cd') }) }) diff --git a/tests/clock.test.ts b/tests/clock.test.ts index ace8263..19583f6 100644 --- a/tests/clock.test.ts +++ b/tests/clock.test.ts @@ -7,11 +7,12 @@ import { World } from '../src/renderer/game/engine/runtime/World' * 任何改动(重构日程/调平衡/加系统)若改变了确定性序列或结果,此测试立刻报红。 * 更新规则:仅当**有意**变更序列逻辑时,三枚 seed 指纹同版更新并注明原因。 */ -// 0.1.16 资源闭环基线:修为耗草/炼丹概率化/铸器/灵脉折算/NPC战力/灾变落效/渡劫药力/结盟后固化。 +// 0.1.17 世界真炉膛基线:市场实体化(供给/需求呼吸+NPC上桌+玩家买卖回写) +// + NPC博弈(互攻/关系网/战争伤骨)+ 灾年持续化后固化。 const GOLDEN: Record> = { - 'bell-seed-1': { 560: '9531969e', 1200: 'cdd70056', 2160: '7e33a89d' }, - 'bell-seed-2': { 560: 'f2ecc3e1', 1200: 'dab62832', 2160: '03c02233' }, - 'bell-seed-3': { 560: 'aeefac96', 1200: '9eda1d0f', 2160: 'b3bcf822' } + 'bell-seed-1': { 560: 'c923fdd1', 1200: '2cb58265', 2160: 'a367ba40' }, + 'bell-seed-2': { 560: 'e98eb9fa', 1200: '3dd495e9', 2160: '17a588c2' }, + 'bell-seed-3': { 560: '5edde2cd', 1200: '476512f7', 2160: 'bc2d55d8' } } const TIERS = [ diff --git a/tests/facade-registry.test.ts b/tests/facade-registry.test.ts index 4743c6b..275f2b7 100644 --- a/tests/facade-registry.test.ts +++ b/tests/facade-registry.test.ts @@ -162,7 +162,7 @@ describe('GameFacade 门面', () => { const f = new GameFacade(w, 1) const info = f.about() expect(info.title).toBe('仙途家族志') - expect(info.version).toContain('0.1.16') + expect(info.version).toContain('0.1.17') expect(info.modules).toBeGreaterThanOrEqual(11) expect(info.systems).toBeGreaterThan(0) expect(info.plugins).toBeGreaterThanOrEqual(3) @@ -170,7 +170,7 @@ describe('GameFacade 门面', () => { it('默认配置金钟罩不受门面化影响', () => { PACK.reset() - expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('9531969e') - expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('aeefac96') + expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('c923fdd1') + expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('5edde2cd') }) }) diff --git a/tests/marriage-diplomacy.test.ts b/tests/marriage-diplomacy.test.ts index 6bb31e2..049c268 100644 --- a/tests/marriage-diplomacy.test.ts +++ b/tests/marriage-diplomacy.test.ts @@ -105,7 +105,9 @@ describe('diplomacy 外交', () => { const before = npc.relation expect(giftNpc(w, npc.id, 120)).toBe(true) expect(npc.relation).toBe(100) - expect(w.state.family.stones).toBe(800 - 120) + // 0.1.17 回礼机制:30% 概率回灵石(≤+15)、30% 概率回灵草(不破灵石断言) + expect(w.state.family.stones).toBeGreaterThanOrEqual(800 - 120) + expect(w.state.family.stones).toBeLessThanOrEqual(800 - 120 + 15) }) it('赠礼不足预算时拒绝且分文不动', () => { diff --git a/tests/plugin.test.ts b/tests/plugin.test.ts index e8abc9e..995ab94 100644 --- a/tests/plugin.test.ts +++ b/tests/plugin.test.ts @@ -80,9 +80,9 @@ describe('PluginCore 插件协议', () => { }) it('默认管线金钟罩不受插件层影响', () => { - expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('9531969e') - expect(stateFingerprint(longRun('bell-seed-2').state)).toBe('f2ecc3e1') - expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('aeefac96') + expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('c923fdd1') + expect(stateFingerprint(longRun('bell-seed-2').state)).toBe('e98eb9fa') + expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('5edde2cd') }) it('facade 插件查询与 about.plugins', () => { diff --git a/tests/worldloop-0.1.17.test.ts b/tests/worldloop-0.1.17.test.ts new file mode 100644 index 0000000..b9975f5 --- /dev/null +++ b/tests/worldloop-0.1.17.test.ts @@ -0,0 +1,107 @@ +import { describe, expect, it } from 'vitest' +import { World } from '../src/renderer/game/engine/runtime/World' +import { WorldSim } from '../src/renderer/game/engine/sim/WorldSim' +import { buyItem, sellItem, marketPrice, buyTechnique, techniquePrice } from '../src/renderer/game/engine/sim/Market' +import { resolveRaid } from '../src/renderer/game/engine/runtime/Systems/combat' +import { POOL_BASE } from '../src/renderer/game/engine/sim/worldsim-data' + +function worldWithSim(seed: string): World { + const w = World.create({ seed, surname: '万', familyName: '万家', motto: 'm', difficulty: 'normal' }) + w.advanceMonth() + w.state.totalTicks = 60 + return w +} + +describe('0.1.17 世界真炉膛', () => { + it('买卖回写池:买浅卖盈,断供拒单', () => { + const w = worldWithSim('w1') + const sim = new WorldSim(w) + const p0 = sim.marketMultFor('lingcao') + expect(buyItem(w, 'lingcao', 5)).toBe(true) + const p1 = sim.marketMultFor('lingcao') + expect(p1).toBeLessThan(p0) // 买浅 → 价涨 + expect(sellItem(w, 'lingcao', 5)).toBe(true) + const p2 = sim.marketMultFor('lingcao') + expect(p2).toBeGreaterThan(p1) // 卖盈 → 价跌回来 + // 断供:池压平后拒单 + const ws = w.state.worldSim as { marketPool: Record } + ws.marketPool['lingcao'] = POOL_BASE.lingcao * 0.3 + const before = w.state.family.inventory['lingcao'] ?? 0 + expect(buyItem(w, 'lingcao', 1)).toBe(false) + expect(w.state.family.inventory['lingcao'] ?? 0).toBe(before) + }) + + it('市场呼吸有界:长跑 600 月池不枯竭亦不冲顶', () => { + const w = World.create({ seed: 'w2', surname: '安', familyName: '安家', motto: 'm', difficulty: 'normal' }) + for (let i = 0; i < 600; i++) w.advanceMonth() + const ws = w.state.worldSim as { marketPool: Record } + for (const id of Object.keys(POOL_BASE)) { + expect(ws.marketPool[id]).toBeGreaterThan(POOL_BASE[id] * 0.1) + expect(ws.marketPool[id]).toBeLessThanOrEqual(POOL_BASE[id] * 2.5) + } + }) + + it('NPC 上桌:长期活跃于池(需料者逢底而购)', () => { + const w = World.create({ seed: 'w3', surname: '风', familyName: '风家', motto: 'm', difficulty: 'normal' }) + for (let i = 0; i < 240; i++) w.advanceMonth() + const ws = w.state.worldSim as { marketPool: Record } + const beast = ws.marketPool['beastcore'] + expect(beast).toBeGreaterThan(POOL_BASE.beastcore * 0.12) + expect(beast).toBeLessThan(POOL_BASE.beastcore * 2.4) + }) + + it('劫掠胜利:NPC 伤筋动骨(power-18%、raidCount/warCooldownYear 写值)', () => { + const w = worldWithSim('w4') + const npcId = Object.keys(w.state.npcFamilies)[0]! + const npc = w.state.npcFamilies[npcId] + npc.power = 200 + const team = [w.state.members['x1']!, w.state.members['x2']!, w.state.members['x3']!, w.state.members['x4']!] + for (const c of team) { + c.realm = { major: 'spirit', minor: 0 } + c.health = 100 + } + const res = resolveRaid(w, npcId, team) + expect(res.win).toBe(true) + expect(npc.power).toBeLessThan(200) + expect(npc.raidCount).toBeGreaterThan(0) + expect(npc.warCooldownYear).toBe(w.state.year) + }) + + it('灾年持续 6 月并散去(B12)', () => { + const w = worldWithSim('w5') + w.state.month = 1 + void w + const ws = w.state.worldSim as { calamity?: string; calamityLeft?: number } + // 强制触发:首月+高概率 seed 抽——健壮断言:任何时刻 calamityLeft ∈ [0,6] + for (let i = 0; i < 24; i++) { + w.advanceMonth() + const left = ws.calamityLeft ?? 0 + expect(left).toBeLessThanOrEqual(6) + if (ws.calamity) expect(left).toBeGreaterThan(0) + } + }) + + it('NPC 关系网填充:同风格亲近、异风格有隙,且年首漂移', () => { + const w = worldWithSim('w6') + const dyn = (w.state.worldSim as { npcDyn: Record }> }).npcDyn + const xuan = dyn['n-xuanying']?.relationsWithOthers ?? {} + expect(Object.keys(xuan).length).toBeGreaterThanOrEqual(3) + const after = (w.state.worldSim as { npcDyn: Record }> }).npcDyn + // 长跑后关系网仍存在(不丢) + expect(Object.keys(after['n-xuanying']?.relationsWithOthers ?? {}).length).toBeGreaterThanOrEqual(3) + }) + + it('功法价格与 techniquePrice 一致(面板引用安全)', () => { + const w = worldWithSim('w7') + const t = w.state.family.techniques[0] + if (!t) return + expect(techniquePrice(t)).toBeGreaterThan(0) + expect(buyTechnique(w, t, techniquePrice(t))).toBe(false) // 已录 + }) + + it('无 sim 世界市场价正常(兜底不崩)', () => { + const w = World.create({ seed: 'w8', surname: '石', familyName: '石家', motto: 'm', difficulty: 'normal' }) + w.state.worldSim = undefined as never + expect(marketPrice(w, 'lingcao')).toBeGreaterThan(0) + }) +})