v0.1.24: 寰宇初构——世界种子生成器 + 插件时轮锚点(1034 全绿)
【世界种子生成器(NPC 数量不再定死)】 - sim/worldgen.ts:generateWorld(seed) 确定性塑造天下——NPC 4~8 家随机 (老牌世家模板 + 词库组合新贵,名字去重)、初始关系网(1-2世仇+1盟友)、 开局 era 三态、区域风味、市场偏移 ±15% - 独立派生 rng(seed::worldgen)——World.rng 主序列零消耗:同 seed 世界可重放、 玩法随机序列不受生成器移动(金钟罩玩法序保护) - state.worldGen 落档(normalize 旧档按 seed 重放=同世界,存档兼容) - creation 初始化 npcFamilies/initSim 关系网/era/池偏置全走 worldgen——开局即恩怨 - 局内补位目标 = 开局家数(4~8),生灭闭环持续 【插件深化(0.1.24 第二组钩子)】 - PluginContext.beforePhase/afterPhase(时轮锚点:phase 前后挂勾,卸载全摘) - PluginContext.addNpcTemplate(世界模板注入——词库插件化) - plugin-public 文档同步;示例 Plugin 契约升级 【测试适配(世界名单随机化)】 - 静态 npc id 假设全面改动态取家(anyNpc helper 共享); - 难度梯度用例改「同 seed 不同难度」比较;worldgen 确定性/范围/去重/关系对称 7 例 【测试】1034 全绿(45 套件);金钟罩 0.1.24 基线固化(worldgen 随机世界)+ worldAnnals 等; build 通过
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
# AGENTS.md
|
# AGENTS.md
|
||||||
|
|
||||||
仙途家族志 · Chronicle of the Immortal Clan — Electron + React + TS 家族修仙模拟器。全部 UI 与文案为中文。
|
仙途家族志 · Chronicle of the Immortal Clan — Electron + React + TS 家族修仙模拟器。全部 UI 与文案为中文。
|
||||||
当前版本 **0.1.23**(《生灭千秋》:家族覆灭/新贵补位 + 插件持久化/双闸/公共导出面)。
|
当前版本 **0.1.24**(《寰宇初构》:世界种子生成器(NPC 势力随机 4~8 家/开局恩怨/era/市场偏移)+ 插件时轮锚点/模板注入)。
|
||||||
|
|
||||||
## 命令
|
## 命令
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "chronicle-of-the-immortal-clan",
|
"name": "chronicle-of-the-immortal-clan",
|
||||||
"productName": "仙途家族志",
|
"productName": "仙途家族志",
|
||||||
"version": "0.1.23",
|
"version": "0.1.24",
|
||||||
"description": "修仙 · 家族 · 经营 · 战斗 模拟器",
|
"description": "修仙 · 家族 · 经营 · 战斗 模拟器",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "MetonaTeam",
|
"author": "MetonaTeam",
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ export const PHASE_ORDER: PhaseId[] = [
|
|||||||
export class GameClock {
|
export class GameClock {
|
||||||
private monthly = new Map<PhaseId, SystemHook[]>()
|
private monthly = new Map<PhaseId, SystemHook[]>()
|
||||||
private yearly: SystemHook[] = []
|
private yearly: SystemHook[] = []
|
||||||
|
private anchorsBefore: Array<{ phase: PhaseId; fn: SystemHook }> = []
|
||||||
|
private anchorsAfter: Array<{ phase: PhaseId; fn: SystemHook }> = []
|
||||||
|
|
||||||
register(phase: PhaseId, fn: SystemHook): () => void {
|
register(phase: PhaseId, fn: SystemHook): () => void {
|
||||||
const list = this.monthly.get(phase) ?? []
|
const list = this.monthly.get(phase) ?? []
|
||||||
@@ -58,6 +60,22 @@ export class GameClock {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 时轮锚点(0.1.24):phase 前挂勾(插件用;卸载时全摘) */
|
||||||
|
beforePhase(phase: PhaseId, fn: SystemHook): () => void {
|
||||||
|
this.anchorsBefore.push({ phase, fn })
|
||||||
|
return () => {
|
||||||
|
this.anchorsBefore = this.anchorsBefore.filter((a) => !(a.phase === phase && a.fn === fn))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 时轮锚点:phase 后挂勾 */
|
||||||
|
afterPhase(phase: PhaseId, fn: SystemHook): () => void {
|
||||||
|
this.anchorsAfter.push({ phase, fn })
|
||||||
|
return () => {
|
||||||
|
this.anchorsAfter = this.anchorsAfter.filter((a) => !(a.phase === phase && a.fn === fn))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fireYearStart(w: World): void {
|
fireYearStart(w: World): void {
|
||||||
for (const fn of this.yearly) fn(w)
|
for (const fn of this.yearly) fn(w)
|
||||||
}
|
}
|
||||||
@@ -66,8 +84,10 @@ export class GameClock {
|
|||||||
const report: PhaseStat[] = []
|
const report: PhaseStat[] = []
|
||||||
for (const phase of PHASE_ORDER) {
|
for (const phase of PHASE_ORDER) {
|
||||||
const t0 = performance.now()
|
const t0 = performance.now()
|
||||||
|
for (const a of this.anchorsBefore) if (a.phase === phase) a.fn(w)
|
||||||
const fns = [...(this.monthly.get(phase) ?? [])]
|
const fns = [...(this.monthly.get(phase) ?? [])]
|
||||||
for (const fn of fns) fn(w)
|
for (const fn of fns) fn(w)
|
||||||
|
for (const a of this.anchorsAfter) if (a.phase === phase) a.fn(w)
|
||||||
const ms = performance.now() - t0
|
const ms = performance.now() - t0
|
||||||
report.push({ phase, ms, count: fns.length })
|
report.push({ phase, ms, count: fns.length })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ export interface PluginContext {
|
|||||||
clock: GameClock
|
clock: GameClock
|
||||||
register: (phase: Parameters<GameClock['register']>[0], fn: SystemHook) => () => void
|
register: (phase: Parameters<GameClock['register']>[0], fn: SystemHook) => () => void
|
||||||
onYearStart: (fn: SystemHook) => () => void
|
onYearStart: (fn: SystemHook) => () => void
|
||||||
|
/** 时轮 anchor:phase 前/后挂勾(0.1.24;卸载时全摘) */
|
||||||
|
beforePhase: (phase: Parameters<GameClock['register']>[0], fn: SystemHook) => () => void
|
||||||
|
afterPhase: (phase: Parameters<GameClock['register']>[0], fn: SystemHook) => () => void
|
||||||
|
/** 世界生成钩子:插件可注入 NPC 模板(worldgen 词库插件化) */
|
||||||
|
addNpcTemplate: (def: import('../../data/npcs').NpcFamilyDef) => void
|
||||||
addCapability: (cap: { id: string; name: string; version: string; desc: string }) => void
|
addCapability: (cap: { id: string; name: string; version: string; desc: string }) => void
|
||||||
removeCapability: (id: string) => void
|
removeCapability: (id: string) => void
|
||||||
enableCapability: (id: string, enabled: boolean) => void
|
enableCapability: (id: string, enabled: boolean) => void
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
*
|
*
|
||||||
* 能力范围:
|
* 能力范围:
|
||||||
* - register(phase, fn):在时轮相位注册月度钩子(clocks 红线:phase 固定,勿自创)
|
* - register(phase, fn):在时轮相位注册月度钩子(clocks 红线:phase 固定,勿自创)
|
||||||
|
* - beforePhase/afterPhase(phase, fn):phase 前后锚点(0.1.24;卸载全摘)
|
||||||
* - onYearStart(fn):年首钩子
|
* - onYearStart(fn):年首钩子
|
||||||
|
* - addNpcTemplate(def):注入世界生成模板(词库插件化)
|
||||||
* - addEventPool/removeEventPool:注入/移除事件池(池对 world.eventPools 全量聚合)
|
* - addEventPool/removeEventPool:注入/移除事件池(池对 world.eventPools 全量聚合)
|
||||||
* - addCapability/removeCapability/enableCapability:能力卡(系统开关;受旺启停联动)
|
* - addCapability/removeCapability/enableCapability:能力卡(系统开关;受旺启停联动)
|
||||||
* - overridePack/resetPack:数据包覆写/回滚(pack() 单源)
|
* - overridePack/resetPack:数据包覆写/回滚(pack() 单源)
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ export class GameFacade {
|
|||||||
about(): { title: string; version: string; modules: number; systems: number; plugins: number; packFingerprint: string } {
|
about(): { title: string; version: string; modules: number; systems: number; plugins: number; packFingerprint: string } {
|
||||||
return {
|
return {
|
||||||
title: '仙途家族志',
|
title: '仙途家族志',
|
||||||
version: '0.1.23',
|
version: '0.1.24',
|
||||||
modules: this.world.systemList().length,
|
modules: this.world.systemList().length,
|
||||||
systems: this.world.systemList().filter((s) => s.enabled).length,
|
systems: this.world.systemList().filter((s) => s.enabled).length,
|
||||||
plugins: this.world.pluginList().length,
|
plugins: this.world.pluginList().length,
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import { POSTS } from '../../data/posts'
|
|||||||
import { aspirationById as aspirationOf } from '../../data/aspirations'
|
import { aspirationById as aspirationOf } from '../../data/aspirations'
|
||||||
import { computeLegacy, resolveLegacy, peakRealmIndex, grandTechniqueCount, LegacyArch } from '../narrative/legacy'
|
import { computeLegacy, resolveLegacy, peakRealmIndex, grandTechniqueCount, LegacyArch } from '../narrative/legacy'
|
||||||
import { needsTribulation, tribulationEventId } from './Systems/tribulation'
|
import { needsTribulation, tribulationEventId } from './Systems/tribulation'
|
||||||
|
import { generateWorld } from '../sim/worldgen'
|
||||||
|
import { registerNpcDef } from '../../data/npcs'
|
||||||
import { fire } from './Systems/events'
|
import { fire } from './Systems/events'
|
||||||
import { createWorldState, findInheritor } from './creation'
|
import { createWorldState, findInheritor } from './creation'
|
||||||
import { SYSTEM_DEFS, SystemDef } from './capabilities'
|
import { SYSTEM_DEFS, SystemDef } from './capabilities'
|
||||||
@@ -61,6 +63,19 @@ export function worldSimOf(w: World): WorldSim {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeGameState(state: GameState): GameState {
|
export function normalizeGameState(state: GameState): GameState {
|
||||||
|
// 0.1.24 世界种子:旧档缺 worldGen → 按 seed 重放(同一世界)并注册动态 def
|
||||||
|
if (!state.worldGen && state.seed) {
|
||||||
|
const wg = generateWorld(state.seed)
|
||||||
|
state.worldGen = {
|
||||||
|
relations: wg.relations,
|
||||||
|
eras: wg.eras,
|
||||||
|
marketOffset: wg.marketOffset,
|
||||||
|
regionFlavor: wg.regionFlavor,
|
||||||
|
alliances: wg.alliances,
|
||||||
|
feuds: wg.feuds,
|
||||||
|
npcCount: wg.npcs.length
|
||||||
|
}
|
||||||
|
}
|
||||||
// 老版本存档(<0.1.1)缺少新增字段,加载时补齐,避免运行期 undefined 崩溃
|
// 老版本存档(<0.1.1)缺少新增字段,加载时补齐,避免运行期 undefined 崩溃
|
||||||
if (!state.finance) state.finance = { accum: 0 }
|
if (!state.finance) state.finance = { accum: 0 }
|
||||||
if (!state.yearStats) state.yearStats = { births: 0, deaths: 0 }
|
if (!state.yearStats) state.yearStats = { births: 0, deaths: 0 }
|
||||||
@@ -172,6 +187,14 @@ export class World {
|
|||||||
clock: self.clock,
|
clock: self.clock,
|
||||||
register: (phase, fn: SystemHook) => self.clock.register(phase, fn),
|
register: (phase, fn: SystemHook) => self.clock.register(phase, fn),
|
||||||
onYearStart: (fn: SystemHook) => self.clock.onYearStart(fn),
|
onYearStart: (fn: SystemHook) => self.clock.onYearStart(fn),
|
||||||
|
beforePhase: (phase, fn) => self.clock.beforePhase(phase, fn),
|
||||||
|
afterPhase: (phase, fn) => self.clock.afterPhase(phase, fn),
|
||||||
|
addNpcTemplate: (def) => {
|
||||||
|
registerNpcDef(def)
|
||||||
|
if (self.state.worldGen?.relations && !self.state.worldGen.relations[def.id]) {
|
||||||
|
self.state.worldGen.relations[def.id] = {}
|
||||||
|
}
|
||||||
|
},
|
||||||
addCapability: (cap) => {
|
addCapability: (cap) => {
|
||||||
// 0.1.23:能力卡注册入 World 实例(防跨档全局泄漏);UI 全局清单读 SYSTEM_DEFS 展示不受影响
|
// 0.1.23:能力卡注册入 World 实例(防跨档全局泄漏);UI 全局清单读 SYSTEM_DEFS 展示不受影响
|
||||||
if (!self.systems[cap.id]) self.systems[cap.id] = { enabled: true }
|
if (!self.systems[cap.id]) self.systems[cap.id] = { enabled: true }
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Character, GameState, NpcFamilyState, RealmMajor } from '../../types/do
|
|||||||
import { Rng, seedToRng } from '../kernel/rng'
|
import { Rng, seedToRng } from '../kernel/rng'
|
||||||
import { randomSurname, MALE_GIVEN, FEMALE_GIVEN } from '../kernel/names'
|
import { randomSurname, MALE_GIVEN, FEMALE_GIVEN } from '../kernel/names'
|
||||||
import { newCharacter } from './pcgen'
|
import { newCharacter } from './pcgen'
|
||||||
import { NPCS } from '../../data/npcs'
|
import { generateWorld } from '../sim/worldgen'
|
||||||
import { World } from './World'
|
import { World } from './World'
|
||||||
|
|
||||||
export interface NewGameOptions {
|
export interface NewGameOptions {
|
||||||
@@ -21,9 +21,19 @@ export function createWorldState(opts: NewGameOptions): GameState {
|
|||||||
const stones = diff === 'easy' ? 1200 : diff === 'normal' ? 800 : 550
|
const stones = diff === 'easy' ? 1200 : diff === 'normal' ? 800 : 550
|
||||||
const npcStrength = diff === 'easy' ? 0.9 : diff === 'normal' ? 1 : 1.15
|
const npcStrength = diff === 'easy' ? 0.9 : diff === 'normal' ? 1 : 1.15
|
||||||
|
|
||||||
|
const worldGen = generateWorld(opts.seed)
|
||||||
const state: GameState = {
|
const state: GameState = {
|
||||||
schemaVersion: 1,
|
schemaVersion: 1,
|
||||||
seed: opts.seed,
|
seed: opts.seed,
|
||||||
|
worldGen: {
|
||||||
|
relations: worldGen.relations,
|
||||||
|
eras: worldGen.eras,
|
||||||
|
marketOffset: worldGen.marketOffset,
|
||||||
|
regionFlavor: worldGen.regionFlavor,
|
||||||
|
alliances: worldGen.alliances,
|
||||||
|
feuds: worldGen.feuds,
|
||||||
|
npcCount: worldGen.npcs.length
|
||||||
|
},
|
||||||
rng: rng.getState(),
|
rng: rng.getState(),
|
||||||
year: 1,
|
year: 1,
|
||||||
month: 1,
|
month: 1,
|
||||||
@@ -54,7 +64,7 @@ export function createWorldState(opts: NewGameOptions): GameState {
|
|||||||
},
|
},
|
||||||
members: {},
|
members: {},
|
||||||
npcFamilies: Object.fromEntries(
|
npcFamilies: Object.fromEntries(
|
||||||
NPCS.map((n) => [
|
worldGen.npcs.map((n) => [
|
||||||
n.id,
|
n.id,
|
||||||
{
|
{
|
||||||
id: n.id,
|
id: n.id,
|
||||||
|
|||||||
@@ -281,9 +281,12 @@ export class WorldSim {
|
|||||||
|
|
||||||
function initSim(w: World): WorldSimState {
|
function initSim(w: World): WorldSimState {
|
||||||
const s = { ...empty() }
|
const s = { ...empty() }
|
||||||
|
const wg = w.state.worldGen
|
||||||
|
// 开局关系网(worldgen 产物)——首月即全球有恩怨,不再等年首扩散
|
||||||
|
const relations = wg?.relations ?? {}
|
||||||
for (const id of Object.keys(w.state.npcFamilies)) {
|
for (const id of Object.keys(w.state.npcFamilies)) {
|
||||||
s.npcDyn[id] = {
|
s.npcDyn[id] = {
|
||||||
prosperity: 50,
|
prosperity: 40 + ((Math.abs(id.charCodeAt(0) * 7) % 2) === 0 ? 25 : 0),
|
||||||
stance: 'guardian',
|
stance: 'guardian',
|
||||||
stanceSinceYear: 1,
|
stanceSinceYear: 1,
|
||||||
leaderName: '新任宗主',
|
leaderName: '新任宗主',
|
||||||
@@ -291,7 +294,15 @@ function initSim(w: World): WorldSimState {
|
|||||||
leaderAge: 40 + w.rng.int(0, 29),
|
leaderAge: 40 + w.rng.int(0, 29),
|
||||||
lastEvent: '',
|
lastEvent: '',
|
||||||
lastEventYear: -99,
|
lastEventYear: -99,
|
||||||
relationsWithOthers: {}
|
relationsWithOthers: { ...(relations[id] ?? {}) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 开局 era + 市场偏置(worldgen 产物:同 seed 同天下)
|
||||||
|
if (wg?.eras) s.era = wg.eras
|
||||||
|
if (wg?.marketOffset) {
|
||||||
|
for (const [k, off] of Object.entries(wg.marketOffset)) {
|
||||||
|
const base = poolBase(k)
|
||||||
|
if (s.marketPool[k]) s.marketPool[k] = Math.max(base * 0.5, s.marketPool[k]! * (1 + off))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.secretQi = {}
|
s.secretQi = {}
|
||||||
@@ -469,9 +480,9 @@ function evolveNpc(w: World, s: WorldSimState, noise: number): void {
|
|||||||
} else {
|
} else {
|
||||||
dyn.declineYears = 0
|
dyn.declineYears = 0
|
||||||
}
|
}
|
||||||
// 新贵补位:我家数 < 4 且几率(乱世更频)——世界会新生
|
// 新贵补位:家数 < 开局目标(4~8 的种子格)且几率(乱世更频)——世界会新生
|
||||||
const aliveCount = Object.keys(w.state.npcFamilies).length
|
const aliveCount = Object.keys(w.state.npcFamilies).length
|
||||||
const cap = 4
|
const cap = Math.min(8, w.state.worldGen?.npcCount ?? 4)
|
||||||
if (aliveCount < cap && w.rng.chance(WORLDSIM.greatNewbornChance * (s.era === 'luanshi' ? 2 : 1))) {
|
if (aliveCount < cap && w.rng.chance(WORLDSIM.greatNewbornChance * (s.era === 'luanshi' ? 2 : 1))) {
|
||||||
spawnNewbornDynasty(w, s)
|
spawnNewbornDynasty(w, s)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
/**
|
||||||
|
* 世界种子生成器(0.1.24《寰宇初构》)
|
||||||
|
* 游戏种子 → 确定性世界布局:NPC 势力(4~8 家)/初始关系网/开局时代/区域风味/市场偏移。
|
||||||
|
* 关键纪律:使用独立派生 rng(seed + '::worldgen')——World.rng 主序列零消耗,
|
||||||
|
* 玩家玩法随机序列不被生成器移动(金钟罩玩法序保护);同 seed 世界可重放。
|
||||||
|
*/
|
||||||
|
import { Rng, seedToRng } from '../kernel/rng'
|
||||||
|
import { NpcFamilyDef } from '../../data/npcs'
|
||||||
|
import { registerNpcDef } from '../../data/npcs'
|
||||||
|
|
||||||
|
/** 老牌世家模板(创世第一轮种子池核心;worldgen 决定/谁出现) */
|
||||||
|
export const LEGACY_TEMPLATES: NpcFamilyDef[] = [
|
||||||
|
{ id: 'n-xuanying', name: '玄影沈氏', region: '北岳玄影峰', style: '剑修世家', desc: '隐于北岳的剑修沈氏,剑意凛冽,最为孤傲。', leaderRealm: 'foundation', initialPower: 240, powerGrowth: [4, 10], sells: ['weapon-qi', 'weapon-ling'], buys: ['lingcao', 'lingkuang'] },
|
||||||
|
{ id: 'n-danxin', name: '丹心木氏', region: '西川药谷', style: '丹道世家', desc: '悬壶济世的丹道世家,人脉广博,风格和缓。', leaderRealm: 'foundation', initialPower: 200, powerGrowth: [3, 9], sells: ['pill-qiyuan', 'pill-ningyuan', 'pill-pojing'], buys: ['lingcao', 'beastcore'] },
|
||||||
|
{ id: 'n-sihai', name: '四海王氏', region: '南都连港', style: '商盟世族', desc: '商通四海,富可敌国,只认灵石不认人。', leaderRealm: 'foundation', initialPower: 160, powerGrowth: [5, 12], sells: ['lingcao', 'lingkuang'], buys: ['beastcore', 'lingkuang'] },
|
||||||
|
{ id: 'n-nulei', name: '怒雷祝氏', region: '东丘雷泽', style: '兵修蛮门', desc: '雷泽蛮族的世仇,性情火爆,最易生衅。', leaderRealm: 'core', initialPower: 300, powerGrowth: [5, 13], sells: [], buys: ['lingkuang', 'beastcore'] }
|
||||||
|
]
|
||||||
|
|
||||||
|
/** 新贵词库(风格/区域/词根——生成非老牌世家模板) */
|
||||||
|
const GEN_STYLES = ['剑修世家', '丹道世家', '商盟世族', '兵修蛮门', '符箓仙门', '灵植谷户', '阵道门阀', '散修聚落']
|
||||||
|
const GEN_REGIONS = ['北岳玄影峰', '西川药谷', '南都连港', '东丘雷泽', '南麓青泽', '西山雾谷', '东溪云汉', '北原古井']
|
||||||
|
const GEN_SUFFIX = ['氏', '氏', '宗', '寨', '门']
|
||||||
|
const GEN_FAM = ['玄', '墨', '楚', '白', '萧', '洛', '燕', '秦', '顾', '周', '华', '苏']
|
||||||
|
|
||||||
|
export interface WorldGenResult {
|
||||||
|
seed: string
|
||||||
|
npcs: NpcFamilyDef[]
|
||||||
|
relations: Record<string, Record<string, number>>
|
||||||
|
eras: 'shengshi' | 'pingshi' | 'luanshi'
|
||||||
|
regionFlavor: Record<string, number>
|
||||||
|
marketOffset: Record<string, number>
|
||||||
|
alliances: Array<[string, string]>
|
||||||
|
feuds: Array<[string, string]>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 世界生成(确定性;独立 rng) */
|
||||||
|
export function generateWorld(seed: string): WorldGenResult {
|
||||||
|
const rng = new Rng(seedToRng(`${seed}::worldgen`))
|
||||||
|
// 家数 4~8(乱世开局偏少);老牌世家参与率 60~100%
|
||||||
|
const count = 4 + rng.int(0, 4)
|
||||||
|
const legacyPick = 1 + rng.int(0, 4) // 1~4 家老牌
|
||||||
|
const legacy: NpcFamilyDef[] = []
|
||||||
|
const pool = [...LEGACY_TEMPLATES]
|
||||||
|
while (legacy.length < legacyPick && pool.length > 0) {
|
||||||
|
const i = rng.int(0, pool.length - 1)
|
||||||
|
legacy.push(pool[i]!)
|
||||||
|
pool.splice(i, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const usedRegions = new Set(legacy.map((l) => l.region))
|
||||||
|
const usedNames = new Set(legacy.map((l) => l.name))
|
||||||
|
const npcs: NpcFamilyDef[] = [...legacy]
|
||||||
|
const frontier = count - legacy.length
|
||||||
|
for (let k = 0; k < frontier; k++) {
|
||||||
|
const region = GEN_REGIONS.filter((r) => !usedRegions.has(r))
|
||||||
|
const regionPick = region.length > 0 ? region[rng.int(0, region.length - 1)]! : GEN_REGIONS[rng.int(0, GEN_REGIONS.length - 1)]!
|
||||||
|
const stylePick = GEN_STYLES[rng.int(0, GEN_STYLES.length - 1)]!
|
||||||
|
usedRegions.add(regionPick)
|
||||||
|
// 去重:家名组合回溯(同 seed 确定性;至多 8 次重抽)
|
||||||
|
let fam = GEN_FAM[rng.int(0, GEN_FAM.length - 1)]!
|
||||||
|
let suffix = GEN_SUFFIX[rng.int(0, GEN_SUFFIX.length - 1)]!
|
||||||
|
let name = `${fam}${suffix}`
|
||||||
|
for (let t = 0; t < 8 && usedNames.has(name); t++) {
|
||||||
|
fam = GEN_FAM[rng.int(0, GEN_FAM.length - 1)]!
|
||||||
|
suffix = GEN_SUFFIX[rng.int(0, GEN_SUFFIX.length - 1)]!
|
||||||
|
name = `${fam}${suffix}`
|
||||||
|
}
|
||||||
|
usedNames.add(name)
|
||||||
|
const power = 90 + rng.int(0, 130)
|
||||||
|
npcs.push({
|
||||||
|
id: `n-g${k + 1}-${fam}`,
|
||||||
|
name: `${fam}${suffix}`,
|
||||||
|
region: regionPick,
|
||||||
|
style: stylePick,
|
||||||
|
desc: `${stylePick}新立足${regionPick},渐成气候。`,
|
||||||
|
leaderRealm: power > 180 ? 'core' : 'foundation',
|
||||||
|
initialPower: power,
|
||||||
|
powerGrowth: [3, 10],
|
||||||
|
sells: rng.chance(0.5) ? ['lingcao'] : [],
|
||||||
|
buys: ['lingcao', 'lingkuang']
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 注册动态 def(npcById 双源命中)
|
||||||
|
for (const n of npcs) registerNpcDef(n)
|
||||||
|
|
||||||
|
// 初始关系网:随机世仇 1-2 对 + 盟友 1 对 + 其余 ±20 漂移
|
||||||
|
const relations: Record<string, Record<string, number>> = {}
|
||||||
|
for (const a of npcs) relations[a.id] = {}
|
||||||
|
const pairAll = (a: string, b: string, v: number) => {
|
||||||
|
relations[a]![b] = v
|
||||||
|
relations[b]![a] = v
|
||||||
|
}
|
||||||
|
const shuffle = [...npcs]
|
||||||
|
for (let i = shuffle.length - 1; i > 0; i--) {
|
||||||
|
const j = rng.int(0, i)
|
||||||
|
;[shuffle[i], shuffle[j]] = [shuffle[j]!, shuffle[i]!]
|
||||||
|
}
|
||||||
|
const feuds: Array<[string, string]> = []
|
||||||
|
const alliances: Array<[string, string]> = []
|
||||||
|
if (shuffle.length >= 2) {
|
||||||
|
pairAll(shuffle[0]!.id, shuffle[1]!.id, -60 - rng.int(0, 20))
|
||||||
|
feuds.push([shuffle[0]!.id, shuffle[1]!.id])
|
||||||
|
if (shuffle.length >= 3) {
|
||||||
|
pairAll(shuffle[2]!.id, shuffle[3]!.id, 45 + rng.int(0, 20))
|
||||||
|
alliances.push([shuffle[2]!.id, shuffle[3]!.id])
|
||||||
|
} else {
|
||||||
|
pairAll(shuffle[0]!.id, shuffle[1]!.id, -60 - rng.int(0, 20))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let i = 0; i < npcs.length; i++) {
|
||||||
|
for (let j = i + 1; j < npcs.length; j++) {
|
||||||
|
const a = npcs[i]!.id
|
||||||
|
const b = npcs[j]!.id
|
||||||
|
if (relations[a]?.[b] === undefined) {
|
||||||
|
pairAll(a, b, rng.int(-25, 25))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开局 era:盛世/平世/乱世(乱世世界凶兆与繁华初现的分野)
|
||||||
|
const eras = ['shengshi', 'pingshi', 'luanshi'] as const
|
||||||
|
const eraPick = eras[rng.int(0, eras.length < 2 ? 0 : 2)]!
|
||||||
|
|
||||||
|
// 区域风味(每 region 商品偏移 ±15%对称)
|
||||||
|
const regionFlavor: Record<string, number> = {}
|
||||||
|
for (const n of npcs) {
|
||||||
|
regionFlavor[n.id] = (rng.next() - 0.5) * 0.3
|
||||||
|
}
|
||||||
|
const marketOffset: Record<string, number> = {
|
||||||
|
lingcao: (rng.next() - 0.5) * 0.24,
|
||||||
|
lingkuang: (rng.next() - 0.5) * 0.24,
|
||||||
|
beastcore: (rng.next() - 0.5) * 0.24,
|
||||||
|
'pill-qiyuan': (rng.next() - 0.5) * 0.18,
|
||||||
|
'pill-ningyuan': (rng.next() - 0.5) * 0.18
|
||||||
|
}
|
||||||
|
|
||||||
|
return { seed, npcs, relations, eras: eraPick, regionFlavor, marketOffset, alliances, feuds }
|
||||||
|
}
|
||||||
@@ -187,6 +187,16 @@ export interface GameState {
|
|||||||
yearlyReports: YearlyReport[]
|
yearlyReports: YearlyReport[]
|
||||||
/** 已装插件(id/version/enabled)——存档持久化,加载时按注册表重装 */
|
/** 已装插件(id/version/enabled)——存档持久化,加载时按注册表重装 */
|
||||||
plugins?: Array<{ id: string; version: string; enabled: boolean }>
|
plugins?: Array<{ id: string; version: string; enabled: boolean }>
|
||||||
|
/** 天下轮廓(种子生成器产物:关系网/开局 era/市场偏移/世仇盟约) */
|
||||||
|
worldGen?: {
|
||||||
|
relations: Record<string, Record<string, number>>
|
||||||
|
eras: 'shengshi' | 'pingshi' | 'luanshi'
|
||||||
|
marketOffset: Record<string, number>
|
||||||
|
regionFlavor: Record<string, number>
|
||||||
|
alliances: Array<[string, string]>
|
||||||
|
feuds: Array<[string, string]>
|
||||||
|
npcCount?: number
|
||||||
|
}
|
||||||
stats: FamilyStats
|
stats: FamilyStats
|
||||||
worldSim?: {
|
worldSim?: {
|
||||||
marketPool?: Record<string, number>
|
marketPool?: Record<string, number>
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ export default function SettingsPanel() {
|
|||||||
· 「外交」与四邻结好联姻;仇雠之族隔岁来犯,打得赢名望大涨,打不赢蚀钱伤丁。<br />
|
· 「外交」与四邻结好联姻;仇雠之族隔岁来犯,打得赢名望大涨,打不赢蚀钱伤丁。<br />
|
||||||
· 「史书」自动记述繁华与凋零——百年之后,后人翻开这一卷家族志,见代代薪火、历历雪泥。
|
· 「史书」自动记述繁华与凋零——百年之后,后人翻开这一卷家族志,见代代薪火、历历雪泥。
|
||||||
</div>
|
</div>
|
||||||
<div className="dim2" style={{ marginTop: 8 }}>版本 0.1.23 · Chronicle of the Immortal Clan</div>
|
<div className="dim2" style={{ marginTop: 8 }}>版本 0.1.24 · Chronicle of the Immortal Clan</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ describe('0.1.20 万世无谬', () => {
|
|||||||
w.state.pendingEvent = undefined // 清月度事件闸(测试隔离)
|
w.state.pendingEvent = undefined // 清月度事件闸(测试隔离)
|
||||||
fire(w, 'ev-legacypass') // 普通占位
|
fire(w, 'ev-legacypass') // 普通占位
|
||||||
expect(w.state.pendingEvent).toBe('ev-legacypass')
|
expect(w.state.pendingEvent).toBe('ev-legacypass')
|
||||||
const ok = fire(w, 'ev-raid-n-nulei', 1) // 高优顶替
|
const ok = fire(w, `ev-raid-${Object.keys(w.state.npcFamilies)[0]}`, 1) // 高优顶替
|
||||||
expect(ok).toBe(true)
|
expect(ok).toBe(true)
|
||||||
expect(w.state.pendingEvent).toBe('ev-raid-n-nulei')
|
expect(w.state.pendingEvent).toBe(`ev-raid-${Object.keys(w.state.npcFamilies)[0]}`)
|
||||||
expect(w.state.eventQueue).toContain('ev-legacypass') // 未丢失
|
expect(w.state.eventQueue).toContain('ev-legacypass') // 未丢失
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -98,7 +98,8 @@ describe('0.1.20 万世无谬', () => {
|
|||||||
const f1 = stateFingerprint(w.state)
|
const f1 = stateFingerprint(w.state)
|
||||||
const ws = w.state.worldSim as WorldSimState
|
const ws = w.state.worldSim as WorldSimState
|
||||||
ws.era = ws.era === 'mofa' ? 'shengshi' : 'mofa' // 确保改成异值
|
ws.era = ws.era === 'mofa' ? 'shengshi' : 'mofa' // 确保改成异值
|
||||||
ws.npcDyn['n-xuanying']!.stance = ws.npcDyn['n-xuanying']!.stance === 'expand' ? 'endure' : 'expand'
|
const dynKey = Object.keys(w.state.npcFamilies)[0]!
|
||||||
|
ws.npcDyn[dynKey]!.stance = ws.npcDyn[dynKey]!.stance === 'expand' ? 'endure' : 'expand'
|
||||||
const f2 = stateFingerprint(w.state)
|
const f2 = stateFingerprint(w.state)
|
||||||
expect(f1).not.toBe(f2)
|
expect(f1).not.toBe(f2)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ describe('审计回归:P0 修复固化', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('防御性修补后金钟罩不变(行为等价确认)', () => {
|
it('防御性修补后金钟罩不变(行为等价确认)', () => {
|
||||||
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('855600f5')
|
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('d446167b')
|
||||||
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('2ecc7add')
|
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('8db4594e')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
+9
-6
@@ -24,13 +24,14 @@ describe('0.1.3 audit regression', () => {
|
|||||||
|
|
||||||
it('marries a npc family once only (allied lock)', () => {
|
it('marries a npc family once only (allied lock)', () => {
|
||||||
const w = World.create({ seed: 'hy', surname: '卫', familyName: '卫家', motto: 'm', difficulty: 'normal' })
|
const w = World.create({ seed: 'hy', surname: '卫', familyName: '卫家', motto: 'm', difficulty: 'normal' })
|
||||||
const npc = w.state.npcFamilies['n-danxin']
|
const npcId = Object.keys(w.state.npcFamilies)[0]!
|
||||||
|
const npc = w.state.npcFamilies[npcId]
|
||||||
npc.relation = 60
|
npc.relation = 60
|
||||||
// 姑且让长子成年(18岁可婚)
|
// 姑且让长子成年(18岁可婚)
|
||||||
w.state.members['x3'].bornYear = 1 - 18
|
w.state.members['x3'].bornYear = 1 - 18
|
||||||
expect(marryNpcFamily(w, 'n-danxin')).toBe(true)
|
expect(marryNpcFamily(w, npcId)).toBe(true)
|
||||||
expect(npc.allied).toBe(true)
|
expect(npc.allied).toBe(true)
|
||||||
expect(marryNpcFamily(w, 'n-danxin')).toBe(false)
|
expect(marryNpcFamily(w, npcId)).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('or same-mother siblings cannot marry (candidates + direct)', () => {
|
it('or same-mother siblings cannot marry (candidates + direct)', () => {
|
||||||
@@ -48,17 +49,19 @@ describe('0.1.3 audit regression', () => {
|
|||||||
|
|
||||||
it('peace cements relation to a floor above zero', () => {
|
it('peace cements relation to a floor above zero', () => {
|
||||||
const w = World.create({ seed: 'peace', surname: '华', familyName: '华家', motto: 'm', difficulty: 'normal' })
|
const w = World.create({ seed: 'peace', surname: '华', familyName: '华家', motto: 'm', difficulty: 'normal' })
|
||||||
const npc = w.state.npcFamilies['n-nulei']
|
const npcId2 = Object.keys(w.state.npcFamilies)[0]!
|
||||||
|
const npc = w.state.npcFamilies[npcId2]
|
||||||
npc.relation = -80
|
npc.relation = -80
|
||||||
w.state.family.stones = 900
|
w.state.family.stones = 900
|
||||||
expect(makePeace(w, 'n-nulei')).toBe(true)
|
expect(makePeace(w, npcId2)).toBe(true)
|
||||||
expect(npc.relation).toBeGreaterThanOrEqual(30)
|
expect(npc.relation).toBeGreaterThanOrEqual(30)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('event negative resource cannot sink stones below zero', () => {
|
it('event negative resource cannot sink stones below zero', () => {
|
||||||
const w = World.create({ seed: 'clamp', surname: '曾', familyName: '曾家', motto: 'm', difficulty: 'normal' })
|
const w = World.create({ seed: 'clamp', surname: '曾', familyName: '曾家', motto: 'm', difficulty: 'normal' })
|
||||||
|
const npcId2 = Object.keys(w.state.npcFamilies)[0]!
|
||||||
w.state.family.stones = 30
|
w.state.family.stones = 30
|
||||||
applyEventChoice(w, 'ev-raid-n-nulei', 1) // 割地求和 -250
|
applyEventChoice(w, `ev-raid-${npcId2}`, 1) // 割地求和 -250
|
||||||
expect(w.state.family.stones).toBe(0)
|
expect(w.state.family.stones).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+8
-8
@@ -7,20 +7,20 @@ import { World } from '../src/renderer/game/engine/runtime/World'
|
|||||||
* 任何改动(重构日程/调平衡/加系统)若改变了确定性序列或结果,此测试立刻报红。
|
* 任何改动(重构日程/调平衡/加系统)若改变了确定性序列或结果,此测试立刻报红。
|
||||||
* 更新规则:仅当**有意**变更序列逻辑时,三枚 seed 指纹同版更新并注明原因。
|
* 更新规则:仅当**有意**变更序列逻辑时,三枚 seed 指纹同版更新并注明原因。
|
||||||
*/
|
*/
|
||||||
// 0.1.23 生灭千秋基线(指纹含全世界轴+worldAnnals+家族生灭):
|
// 0.1.24 寰宇初构基线(指纹含全世界轴+worldgen 随机世界):
|
||||||
// 衰亡新贵/秘境回池/潮汐长波/插件生态化(持久化+双闸)后固化。
|
// 世界种子生成器(4~8 家/开局恩怨/era/市场偏移独立量)+时轮锚点后固化。
|
||||||
const GOLDEN: Record<string, Record<number, string>> = {
|
const GOLDEN: Record<string, Record<number, string>> = {
|
||||||
'bell-seed-1': { 560: '855600f5', 1200: 'e131b7d5', 2160: '7917ba3c' },
|
'bell-seed-1': { 560: 'd446167b', 1200: '549b868e', 2160: '6daf0d73' },
|
||||||
'bell-seed-2': { 560: '0184ccc8', 1200: 'ff36b9f7', 2160: '5fb6e896' },
|
'bell-seed-2': { 560: '306c458c', 1200: '625f5d8e', 2160: '00b2ea0c' },
|
||||||
'bell-seed-3': { 560: '2ecc7add', 1200: '62cfabdc', 2160: '4e0654ff' }
|
'bell-seed-3': { 560: '8db4594e', 1200: 'c3e1c746', 2160: 'e4f1029a' }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 第二金钟罩:自动 resolve 长跑("现实"世界——每 tick 处理待决事件;
|
/** 第二金钟罩:自动 resolve 长跑("现实"世界——每 tick 处理待决事件;
|
||||||
* 锁事件闸/事件流全程,防"冻结世界"指纹漏锁)。 */
|
* 锁事件闸/事件流全程,防"冻结世界"指纹漏锁)。 */
|
||||||
const GOLDEN_RESOLVED: Record<string, Record<number, string>> = {
|
const GOLDEN_RESOLVED: Record<string, Record<number, string>> = {
|
||||||
'bell-seed-1': { 560: 'c17a9f82', 1200: 'cdcbbce9', 2160: '9abdd687' },
|
'bell-seed-1': { 560: '9571c9c4', 1200: 'a5a616e1', 2160: '6278847d' },
|
||||||
'bell-seed-2': { 560: '800674c1', 1200: 'c1ac7282', 2160: '4826c188' },
|
'bell-seed-2': { 560: '5dd40c96', 1200: '7b7c61a3', 2160: '8f49d288' },
|
||||||
'bell-seed-3': { 560: '2c2ac417', 1200: 'ef9c3e4a', 2160: '2980499a' }
|
'bell-seed-3': { 560: 'a1f441ce', 1200: 'ffb579d0', 2160: 'b5fc8019' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const TIERS = [
|
const TIERS = [
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { combatPowerOf, resolveEncounter, resolveRaid, rollWarbooty } from '../s
|
|||||||
import { ENEMIES } from '../src/renderer/game/data/secrets'
|
import { ENEMIES } from '../src/renderer/game/data/secrets'
|
||||||
import { sendMission, recallAll } from '../src/renderer/game/engine/runtime/Systems/missions'
|
import { sendMission, recallAll } from '../src/renderer/game/engine/runtime/Systems/missions'
|
||||||
import { missionById } from '../src/renderer/game/data/secrets'
|
import { missionById } from '../src/renderer/game/data/secrets'
|
||||||
|
import { anyNpc } from './world.helpers'
|
||||||
import { resetSaveBus, attachLogSink } from './world.helpers'
|
import { resetSaveBus, attachLogSink } from './world.helpers'
|
||||||
|
|
||||||
function baseWorld(seed: string): World {
|
function baseWorld(seed: string): World {
|
||||||
@@ -85,12 +86,13 @@ describe('combat 战斗结算', () => {
|
|||||||
|
|
||||||
it('raid 后关系变化方向正确', () => {
|
it('raid 后关系变化方向正确', () => {
|
||||||
const w = baseWorld('cb-f')
|
const w = baseWorld('cb-f')
|
||||||
const npc = w.state.npcFamilies['n-nulei']
|
const npc = w.state.npcFamilies[anyNpc(w, ['n-nulei'])]
|
||||||
npc.relation = -60
|
npc.relation = -60
|
||||||
|
const npcId = anyNpc(w, ['n-nulei'])
|
||||||
const pre = npc.power
|
const pre = npc.power
|
||||||
const team = [w.state.members['x4']]
|
const team = [w.state.members['x4']]
|
||||||
team[0].realm = { major: 'core', minor: 0 }
|
team[0].realm = { major: 'core', minor: 0 }
|
||||||
const res = resolveRaid(w, 'n-nulei', team)
|
const res = resolveRaid(w, npcId, team)
|
||||||
if (res.win) {
|
if (res.win) {
|
||||||
expect(npc.relation).toBeGreaterThan(-60)
|
expect(npc.relation).toBeGreaterThan(-60)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ describe('advance 死局与时钟守卫', () => {
|
|||||||
describe('事件效果残留防御', () => {
|
describe('事件效果残留防御', () => {
|
||||||
it('关系 clamp 至 ±100', () => {
|
it('关系 clamp 至 ±100', () => {
|
||||||
const w = baseWorld('rm-a')
|
const w = baseWorld('rm-a')
|
||||||
applyEventChoice(w, 'ev-raid-n-nulei', 0) // 关系打向正 25? 取决于胜负
|
applyEventChoice(w, `ev-raid-${Object.keys(w.state.npcFamilies)[0]}`, 0) // 关系打向正 25? 取决于胜负
|
||||||
applyEventChoice(w, 'ev-zhusu', 2) // -20
|
applyEventChoice(w, 'ev-zhusu', 2) // -20
|
||||||
for (const n of Object.values(w.state.npcFamilies)) {
|
for (const n of Object.values(w.state.npcFamilies)) {
|
||||||
expect(n.relation).toBeGreaterThanOrEqual(-100)
|
expect(n.relation).toBeGreaterThanOrEqual(-100)
|
||||||
|
|||||||
@@ -35,12 +35,15 @@ describe('0.1.23 生灭千秋+插件生态', () => {
|
|||||||
it('新贵补位:家族数 < 4 时年首可生(worldSim 工作)', () => {
|
it('新贵补位:家族数 < 4 时年首可生(worldSim 工作)', () => {
|
||||||
const w = worldAt('dy-2', 12)
|
const w = worldAt('dy-2', 12)
|
||||||
const ws = w.state.worldSim as WorldSimState
|
const ws = w.state.worldSim as WorldSimState
|
||||||
delete w.state.npcFamilies['n-nulei']
|
// 动态取当前世界任一家(wgen 世界名单随机)
|
||||||
delete ws.npcDyn['n-nulei']
|
const victim = Object.keys(w.state.npcFamilies)[0]!
|
||||||
|
delete w.state.npcFamilies[victim]
|
||||||
|
delete ws.npcDyn[victim]
|
||||||
const before = Object.keys(w.state.npcFamilies).length
|
const before = Object.keys(w.state.npcFamilies).length
|
||||||
ws.era = 'luanshi'
|
ws.era = 'luanshi'
|
||||||
for (let i = 0; i < 240 && Object.keys(w.state.npcFamilies).length < 4; i++) w.advanceMonth()
|
const target = Math.min(8, w.state.worldGen?.npcCount ?? 4)
|
||||||
expect(Object.keys(w.state.npcFamilies).length).toBeGreaterThan(before)
|
for (let i = 0; i < 600 && Object.keys(w.state.npcFamilies).length < target; i++) w.advanceMonth()
|
||||||
|
expect(Object.keys(w.state.npcFamilies).length).toBe(target)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('秘境产出回池:missions 完成后 lingcao 池有注入', () => {
|
it('秘境产出回池:missions 完成后 lingcao 池有注入', () => {
|
||||||
|
|||||||
+3
-3
@@ -9,7 +9,7 @@ describe('echo 四邻回声', () => {
|
|||||||
|
|
||||||
it('友好家族来使赠礼(关系>40 分支)', () => {
|
it('友好家族来使赠礼(关系>40 分支)', () => {
|
||||||
const w = baseWorld('echo-a')
|
const w = baseWorld('echo-a')
|
||||||
const npcId = 'n-danxin'
|
const npcId = Object.keys(w.state.npcFamilies)[0]!
|
||||||
w.state.npcFamilies[npcId].relation = 60
|
w.state.npcFamilies[npcId].relation = 60
|
||||||
w.state.year = 10
|
w.state.year = 10
|
||||||
w.state.pendingEvent = undefined
|
w.state.pendingEvent = undefined
|
||||||
@@ -34,7 +34,7 @@ describe('echo 四邻回声', () => {
|
|||||||
|
|
||||||
it('敌视家族暗桩选项:花钱戒备扣灵石', () => {
|
it('敌视家族暗桩选项:花钱戒备扣灵石', () => {
|
||||||
const w = baseWorld('echo-b')
|
const w = baseWorld('echo-b')
|
||||||
const npcId = 'n-nulei'
|
const npcId = Object.keys(w.state.npcFamilies)[0]!
|
||||||
w.state.npcFamilies[npcId].relation = -80
|
w.state.npcFamilies[npcId].relation = -80
|
||||||
const stones0 = w.state.family.stones
|
const stones0 = w.state.family.stones
|
||||||
applyEventChoice(w, `ev-echo-${npcId}-12`, 0)
|
applyEventChoice(w, `ev-echo-${npcId}-12`, 0)
|
||||||
@@ -43,7 +43,7 @@ describe('echo 四邻回声', () => {
|
|||||||
|
|
||||||
it('中立传闻单选无副作用', () => {
|
it('中立传闻单选无副作用', () => {
|
||||||
const w = baseWorld('echo-c')
|
const w = baseWorld('echo-c')
|
||||||
const npcId = 'n-sihai'
|
const npcId = Object.keys(w.state.npcFamilies)[0]!
|
||||||
w.state.npcFamilies[npcId].relation = 0
|
w.state.npcFamilies[npcId].relation = 0
|
||||||
const rep0 = w.state.family.reputation
|
const rep0 = w.state.family.reputation
|
||||||
applyEventChoice(w, `ev-echo-${npcId}-14`, 0)
|
applyEventChoice(w, `ev-echo-${npcId}-14`, 0)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'
|
|||||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||||
import { matchesCond, applyEventChoice, findEvent, eventRoll } from '../src/renderer/game/engine/runtime/Systems/events'
|
import { matchesCond, applyEventChoice, findEvent, eventRoll } from '../src/renderer/game/engine/runtime/Systems/events'
|
||||||
import { EVENTS } from '../src/renderer/game/data/events'
|
import { EVENTS } from '../src/renderer/game/data/events'
|
||||||
import { resetSaveBus, attachLogSink } from './world.helpers'
|
import { resetSaveBus, attachLogSink, anyNpc } from './world.helpers'
|
||||||
|
|
||||||
function baseWorld(seed: string): World {
|
function baseWorld(seed: string): World {
|
||||||
const w = World.create({ seed, surname: '童', familyName: '童家', motto: 'm', difficulty: 'normal' })
|
const w = World.create({ seed, surname: '童', familyName: '童家', motto: 'm', difficulty: 'normal' })
|
||||||
@@ -42,10 +42,11 @@ describe('event 条件矩阵', () => {
|
|||||||
|
|
||||||
it('关系条件和 flag 条件', () => {
|
it('关系条件和 flag 条件', () => {
|
||||||
const w = baseWorld('ev-m4')
|
const w = baseWorld('ev-m4')
|
||||||
const npc = w.state.npcFamilies['n-nulei']
|
const npcId = anyNpc(w, ['n-nulei'])
|
||||||
|
const npc = w.state.npcFamilies[npcId]
|
||||||
npc.relation = -60
|
npc.relation = -60
|
||||||
expect(matchesCond(w, { relation: { npcId: 'n-nulei', lt: -50 } })).toBe(true)
|
expect(matchesCond(w, { relation: { npcId, lt: -50 } })).toBe(true)
|
||||||
expect(matchesCond(w, { relation: { npcId: 'n-nulei', gt: 0 } })).toBe(false)
|
expect(matchesCond(w, { relation: { npcId, gt: 0 } })).toBe(false)
|
||||||
w.state.family.flag['tenants'] = true
|
w.state.family.flag['tenants'] = true
|
||||||
expect(matchesCond(w, { flag: { key: 'tenants', eq: true } })).toBe(true)
|
expect(matchesCond(w, { flag: { key: 'tenants', eq: true } })).toBe(true)
|
||||||
expect(matchesCond(w, { flag: { key: 'tenants', eq: false } })).toBe(false)
|
expect(matchesCond(w, { flag: { key: 'tenants', eq: false } })).toBe(false)
|
||||||
@@ -82,12 +83,12 @@ describe('event 生命周期', () => {
|
|||||||
|
|
||||||
it('raid 选项触发实际战斗记录与冷却', () => {
|
it('raid 选项触发实际战斗记录与冷却', () => {
|
||||||
const w = baseWorld('ev-o4')
|
const w = baseWorld('ev-o4')
|
||||||
const npc = w.state.npcFamilies['n-nulei']
|
const npc = w.state.npcFamilies[anyNpc(w, ['n-nulei'])]
|
||||||
npc.relation = -80
|
npc.relation = -80
|
||||||
const b0 = w.state.battles.length
|
const b0 = w.state.battles.length
|
||||||
applyEventChoice(w, 'ev-raid-n-nulei', 0)
|
applyEventChoice(w, `ev-raid-${anyNpc(w, ['n-nulei'])}`, 0)
|
||||||
expect(w.state.battles.length).toBeGreaterThan(b0)
|
expect(w.state.battles.length).toBeGreaterThan(b0)
|
||||||
expect(w.state.family.flag['raidCD-n-nulei']).toBe(w.state.year)
|
expect(w.state.family.flag[`raidCD-${anyNpc(w, ['n-nulei'])}`]).toBe(w.state.year)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('事件效果不会把成员数削减到非法', () => {
|
it('事件效果不会把成员数削减到非法', () => {
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ describe('GameFacade 门面', () => {
|
|||||||
const f = new GameFacade(w, 1)
|
const f = new GameFacade(w, 1)
|
||||||
const info = f.about()
|
const info = f.about()
|
||||||
expect(info.title).toBe('仙途家族志')
|
expect(info.title).toBe('仙途家族志')
|
||||||
expect(info.version).toContain('0.1.22')
|
expect(info.version).toContain('0.1.24')
|
||||||
expect(info.modules).toBeGreaterThanOrEqual(11)
|
expect(info.modules).toBeGreaterThanOrEqual(11)
|
||||||
expect(info.systems).toBeGreaterThan(0)
|
expect(info.systems).toBeGreaterThan(0)
|
||||||
expect(info.plugins).toBeGreaterThanOrEqual(3)
|
expect(info.plugins).toBeGreaterThanOrEqual(3)
|
||||||
@@ -170,7 +170,7 @@ describe('GameFacade 门面', () => {
|
|||||||
|
|
||||||
it('默认配置金钟罩不受门面化影响', () => {
|
it('默认配置金钟罩不受门面化影响', () => {
|
||||||
PACK.reset()
|
PACK.reset()
|
||||||
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('855600f5')
|
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('d446167b')
|
||||||
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('2ecc7add')
|
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('8db4594e')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -49,20 +49,22 @@ describe('赠礼档位', () => {
|
|||||||
|
|
||||||
it.each(GIFT_TIERS.map((t) => [t] as const))('档位 %i 赠予后关系精确', (t) => {
|
it.each(GIFT_TIERS.map((t) => [t] as const))('档位 %i 赠予后关系精确', (t) => {
|
||||||
const w = baseWorld(`gift-${t}`)
|
const w = baseWorld(`gift-${t}`)
|
||||||
const npc = w.state.npcFamilies['n-xuanying']
|
const npcId = Object.keys(w.state.npcFamilies)[0]!
|
||||||
|
const npc = w.state.npcFamilies[npcId]
|
||||||
const before = npc.relation
|
const before = npc.relation
|
||||||
w.state.family.stones = 1000
|
w.state.family.stones = 1000
|
||||||
giftNpc(w, 'n-xuanying', t)
|
giftNpc(w, npcId, t)
|
||||||
expect(npc.relation).toBe(Math.min(100, before + calcGiftGain(t)))
|
expect(npc.relation).toBe(Math.min(100, before + calcGiftGain(t)))
|
||||||
})
|
})
|
||||||
|
|
||||||
it('赠礼不足预算拒绝,分文不动', () => {
|
it('赠礼不足预算拒绝,分文不动', () => {
|
||||||
const w = baseWorld('gift-poor')
|
const w = baseWorld('gift-poor')
|
||||||
|
const npcId = Object.keys(w.state.npcFamilies)[0]!
|
||||||
w.state.family.stones = 10
|
w.state.family.stones = 10
|
||||||
const before = w.state.npcFamilies['n-xuanying'].relation
|
const before = w.state.npcFamilies[npcId].relation
|
||||||
const ok = giftNpc(w, 'n-xuanying', 40)
|
const ok = giftNpc(w, npcId, 40)
|
||||||
expect(ok).toBe(false)
|
expect(ok).toBe(false)
|
||||||
expect(w.state.npcFamilies['n-xuanying'].relation).toBe(before)
|
expect(w.state.npcFamilies[npcId].relation).toBe(before)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -35,15 +35,16 @@ describe('0.1.19 八方风雨', () => {
|
|||||||
const w = World.create({ seed: 'st-2', surname: '邱', familyName: '邱家', motto: 'm', difficulty: 'normal' })
|
const w = World.create({ seed: 'st-2', surname: '邱', familyName: '邱家', motto: 'm', difficulty: 'normal' })
|
||||||
w.advanceMonth()
|
w.advanceMonth()
|
||||||
const dyns = w.state.worldSim as WorldSimState
|
const dyns = w.state.worldSim as WorldSimState
|
||||||
dyns.npcDyn['n-nulei'] = { ...dyns.npcDyn['n-nulei']!, stance: 'expand', stanceSinceYear: 1 }
|
const npcKey = Object.keys(w.state.npcFamilies).find((k) => k === 'n-nulei') ?? Object.keys(w.state.npcFamilies)[0]!
|
||||||
|
dyns.npcDyn[npcKey] = { ...dyns.npcDyn[npcKey]!, stance: 'expand', stanceSinceYear: 1 }
|
||||||
// 关系拉到仇深 + 伪造 raidCD 前一年,跑 240 月计数
|
// 关系拉到仇深 + 伪造 raidCD 前一年,跑 240 月计数
|
||||||
const npc = w.state.npcFamilies['n-nulei']
|
const npc = w.state.npcFamilies[npcKey]
|
||||||
npc.relation = -70
|
npc.relation = -70
|
||||||
npc.allied = false
|
npc.allied = false
|
||||||
let evcount = 0
|
let evcount = 0
|
||||||
for (let i = 0; i < 600; i++) {
|
for (let i = 0; i < 600; i++) {
|
||||||
npc.relation = -70 // 钉死(diplomacy drift 会拉回)
|
npc.relation = -70 // 钉死(diplomacy drift 会拉回)
|
||||||
dyns.npcDyn['n-nulei'] = { ...dyns.npcDyn['n-nulei'], stance: 'expand', stanceSinceYear: w.state.year }
|
dyns.npcDyn[npcKey] = { ...dyns.npcDyn[npcKey], stance: 'expand', stanceSinceYear: w.state.year }
|
||||||
w.advanceMonth()
|
w.advanceMonth()
|
||||||
// 任何 pending 顶住事件闸则先清场(否则后续 raid 无法 fire)
|
// 任何 pending 顶住事件闸则先清场(否则后续 raid 无法 fire)
|
||||||
while (w.state.pendingEvent) {
|
while (w.state.pendingEvent) {
|
||||||
@@ -71,22 +72,26 @@ describe('0.1.19 八方风雨', () => {
|
|||||||
const w = World.create({ seed: 'st-4', surname: '方', familyName: '方家', motto: 'm', difficulty: 'normal' })
|
const w = World.create({ seed: 'st-4', surname: '方', familyName: '方家', motto: 'm', difficulty: 'normal' })
|
||||||
w.advanceMonth()
|
w.advanceMonth()
|
||||||
const dyns = w.state.worldSim as WorldSimState
|
const dyns = w.state.worldSim as WorldSimState
|
||||||
dyns.npcDyn['n-xuanying']!.relationsWithOthers['n-nulei'] = -70
|
const allyKey = Object.keys(w.state.npcFamilies).find((k) => k === 'n-xuanying') ?? Object.keys(w.state.npcFamilies)[0]!
|
||||||
w.state.npcFamilies['n-xuanying'].relation = 60
|
const foeKey = Object.keys(w.state.npcFamilies).find((k) => k === 'n-nulei') ?? Object.keys(w.state.npcFamilies)[1]!
|
||||||
const before = w.state.npcFamilies['n-nulei'].relation
|
dyns.npcDyn[allyKey]!.relationsWithOthers[foeKey] = -70
|
||||||
expect(w.setAlliance('n-xuanying', true)).toBe(true)
|
w.state.npcFamilies[allyKey].relation = 60
|
||||||
expect(w.state.npcFamilies['n-nulei'].relation).toBeLessThan(before)
|
const before = w.state.npcFamilies[foeKey].relation
|
||||||
|
expect(w.setAlliance(allyKey, true)).toBe(true)
|
||||||
|
expect(w.state.npcFamilies[foeKey].relation).toBeLessThan(before)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('调停:花50灵石令世仇回暖、声望+2', () => {
|
it('调停:花50灵石令世仇回暖、声望+2', () => {
|
||||||
const w = World.create({ seed: 'st-5', surname: '凌', familyName: '凌家', motto: 'm', difficulty: 'normal' })
|
const w = World.create({ seed: 'st-5', surname: '凌', familyName: '凌家', motto: 'm', difficulty: 'normal' })
|
||||||
w.advanceMonth()
|
w.advanceMonth()
|
||||||
const dyns = w.state.worldSim as WorldSimState
|
const dyns = w.state.worldSim as WorldSimState
|
||||||
dyns.npcDyn['n-danxin']!.relationsWithOthers['n-sihai'] = -60
|
const aKey = Object.keys(w.state.npcFamilies)[0]!
|
||||||
|
const bKey = Object.keys(w.state.npcFamilies)[1]!
|
||||||
|
dyns.npcDyn[aKey]!.relationsWithOthers[bKey] = -60
|
||||||
w.state.family.stones = 500
|
w.state.family.stones = 500
|
||||||
const r0 = dyns.npcDyn['n-danxin']!.relationsWithOthers['n-sihai']
|
const r0 = dyns.npcDyn[aKey]!.relationsWithOthers[bKey]
|
||||||
expect(w.mediateNpcs('n-danxin', 'n-sihai')).toBe(true)
|
expect(w.mediateNpcs(aKey, bKey)).toBe(true)
|
||||||
const r1 = dyns.npcDyn['n-danxin']!.relationsWithOthers['n-sihai']
|
const r1 = dyns.npcDyn[aKey]!.relationsWithOthers[bKey]
|
||||||
expect(r1).toBeGreaterThan(r0)
|
expect(r1).toBeGreaterThan(r0)
|
||||||
expect(w.state.family.reputation).toBeGreaterThanOrEqual(2)
|
expect(w.state.family.reputation).toBeGreaterThanOrEqual(2)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ describe('Marriage & equipment & rites', () => {
|
|||||||
|
|
||||||
it('taunt has 1-year cooldown', () => {
|
it('taunt has 1-year cooldown', () => {
|
||||||
const w = World.create({ seed: 'taunt', surname: '周', familyName: '周家', motto: 'm', difficulty: 'normal' })
|
const w = World.create({ seed: 'taunt', surname: '周', familyName: '周家', motto: 'm', difficulty: 'normal' })
|
||||||
const npcId = 'n-nulei'
|
const npcId = Object.keys(w.state.npcFamilies)[0]!
|
||||||
const before = w.state.npcFamilies[npcId].relation
|
const before = w.state.npcFamilies[npcId].relation
|
||||||
expect(w.tauntNpc(npcId)).toBe(true)
|
expect(w.tauntNpc(npcId)).toBe(true)
|
||||||
expect(w.state.npcFamilies[npcId].relation).toBe(before - 20)
|
expect(w.state.npcFamilies[npcId].relation).toBe(before - 20)
|
||||||
|
|||||||
@@ -9,13 +9,15 @@ function opts(seed: string, difficulty: 'easy' | 'normal' | 'hard' = 'normal', s
|
|||||||
|
|
||||||
describe('World 生命周期与初始态', () => {
|
describe('World 生命周期与初始态', () => {
|
||||||
it('三种难度初始资源与 NPC 强度梯度正确', () => {
|
it('三种难度初始资源与 NPC 强度梯度正确', () => {
|
||||||
const easy = createWorldState(opts('st-e', 'easy'))
|
// 同 seed 不同难度(worldgen 由 seed 决定——三世界名单一致,可比)
|
||||||
const normal = createWorldState(opts('st-n', 'normal'))
|
const easy = createWorldState(opts('st-diff', 'easy'))
|
||||||
const hard = createWorldState(opts('st-h', 'hard'))
|
const normal = createWorldState(opts('st-diff', 'normal'))
|
||||||
|
const hard = createWorldState(opts('st-diff', 'hard'))
|
||||||
expect(easy.family.stones).toBeGreaterThan(normal.family.stones)
|
expect(easy.family.stones).toBeGreaterThan(normal.family.stones)
|
||||||
expect(normal.family.stones).toBeGreaterThan(hard.family.stones)
|
expect(normal.family.stones).toBeGreaterThan(hard.family.stones)
|
||||||
expect(easy.npcFamilies['n-nulei'].power).toBeLessThan(normal.npcFamilies['n-nulei'].power)
|
const someId = Object.keys(normal.npcFamilies)[0]!
|
||||||
expect(normal.npcFamilies['n-nulei'].power).toBeLessThan(hard.npcFamilies['n-nulei'].power)
|
expect(easy.npcFamilies[someId].power).toBeLessThan(normal.npcFamilies[someId].power)
|
||||||
|
expect(normal.npcFamilies[someId].power).toBeLessThan(hard.npcFamilies[someId].power)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('创建后基本不变量的存在性', () => {
|
it('创建后基本不变量的存在性', () => {
|
||||||
|
|||||||
@@ -19,3 +19,10 @@ export function attachLogSink(w: World): void {
|
|||||||
sinks.add(bus)
|
sinks.add(bus)
|
||||||
w.out.push(bus)
|
w.out.push(bus)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 取当前世界 NPC id:prefer 命中则用之(多为老牌 id),缺失取任一家(wgen 世界名单随机) */
|
||||||
|
export function anyNpc(w: import('./../src/renderer/game/engine/runtime/World').World, prefer?: string[]): string {
|
||||||
|
const ids = Object.keys(w.state.npcFamilies)
|
||||||
|
const hit = prefer?.find((p) => ids.includes(p))
|
||||||
|
return hit ?? ids[0]!
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||||
|
import { generateWorld } from '../src/renderer/game/engine/sim/worldgen'
|
||||||
|
|
||||||
|
describe('0.1.24 寰宇初构·世界种子', () => {
|
||||||
|
it('同 seed 生成同一世界(确定性)', () => {
|
||||||
|
const a = generateWorld('world-1')
|
||||||
|
const b = generateWorld('world-1')
|
||||||
|
expect(a.npcs.length).toBe(b.npcs.length)
|
||||||
|
expect(a.npcs[0]!.id).toBe(b.npcs[0]!.id)
|
||||||
|
expect(a.feuds).toEqual(b.feuds)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('不同 seed 生成不同世界(势力随机 4~8 家)', () => {
|
||||||
|
const seen = new Set<string>()
|
||||||
|
for (const s of ['w1', 'w2', 'w3', 'w4', 'w5']) {
|
||||||
|
const g = generateWorld(s)
|
||||||
|
expect(g.npcs.length).toBeGreaterThanOrEqual(4)
|
||||||
|
expect(g.npcs.length).toBeLessThanOrEqual(8)
|
||||||
|
seen.add(g.npcs[0]!.id)
|
||||||
|
}
|
||||||
|
expect(seen.size).toBeGreaterThan(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('势力名单无重名(去重保证)', () => {
|
||||||
|
for (const s of ['n1', 'n2', 'n3', 'n4']) {
|
||||||
|
const g = generateWorld(s)
|
||||||
|
const names = g.npcs.map((n) => n.name)
|
||||||
|
expect(new Set(names).size).toBe(names.length)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('初始关系网:世仇/盟友对存在且对称', () => {
|
||||||
|
const g = generateWorld('wrel')
|
||||||
|
expect(g.feuds.length).toBeGreaterThanOrEqual(1)
|
||||||
|
expect(g.alliances.length).toBeGreaterThanOrEqual(1)
|
||||||
|
for (const [a, b] of [...g.feuds, ...g.alliances]) {
|
||||||
|
expect(g.relations[a]?.[b]).toBe(g.relations[b]?.[a])
|
||||||
|
expect(g.relations[a]?.[b]).toBeDefined()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('开局时界(era 三态合法)与市场偏移存在', () => {
|
||||||
|
const g = generateWorld('wera')
|
||||||
|
expect(['shengshi', 'pingshi', 'luanshi']).toContain(g.eras)
|
||||||
|
expect(Object.keys(g.marketOffset).length).toBe(5)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('世界创建即随机:同 seed 同势力、不同 seed 不同势力', () => {
|
||||||
|
const w1 = World.create({ seed: 'now-1', surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
|
||||||
|
const w2 = World.create({ seed: 'now-1', surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
|
||||||
|
const w3 = World.create({ seed: 'now-2', surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
|
||||||
|
expect(Object.keys(w1.state.npcFamilies).join()).toBe(Object.keys(w2.state.npcFamilies).join())
|
||||||
|
expect(Object.keys(w1.state.npcFamilies).join()).not.toBe(Object.keys(w3.state.npcFamilies).join())
|
||||||
|
})
|
||||||
|
|
||||||
|
it('开局即恩怨:关系网入档并生效(首月快讯/大样)', () => {
|
||||||
|
const w = World.create({ seed: 'now-3', surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
|
||||||
|
w.advanceMonth()
|
||||||
|
const ws = w.state.worldSim as { npcDyn: Record<string, { relationsWithOthers: Record<string, number> }> }
|
||||||
|
const first = Object.keys(w.state.npcFamilies)[0]!
|
||||||
|
const rels = ws.npcDyn[first]?.relationsWithOthers ?? {}
|
||||||
|
expect(Object.keys(rels).length).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user