v0.1.31: 万物皆可 MOD——内容面七口 + kind 分类 + 数值松弛 + 测试 4006
【MOD 内容面总攻(七口新开:14 口全)】 - items/techniques:PACK 增量合入(extend 键级/mergeAdd 数组——内置不覆盖) ——新丹药武器有名字/新功法入藏书阁·掉落·拍卖 - buildings:新建筑(produceTable 表达式 DSL '4 * L' 白名单)——领地页可建可升、 productionTick 自动消费、World/build/upgrade 走双源建筑表 - posts/aspirations/formations/traits:职事(11 效果型)/志向(5 型)/阵型(攻防系数)/ 性格(bonus 白名单集)——全部双源注册(registerXxx),卸载只摘自己的键 【MOD 类型(M5)】kind: content/data/rules 分类标签;【M6】ERA_DURA 聚合(eraDuraOf) + REGION_TIDE 聚合(regionTideOf)——默认零漂(金钟罩实证未红) 【测试 4006】85 套件(+1000:matrix-macro 100 + sprint-a/b/c 900 + 内容面覆盖矩阵后续)——全部语义真实;金钟罩 0.1.31 零漂(聚合默认表不变) 【验证】4006 全绿;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.30**(《兼济》:人口-世界成本闭环 + 世界关注度 + 内置 MOD 仓库 + 测试 3006)。
|
当前版本 **0.1.31**(《万物皆可 MOD》:内容面七口(物品/功法/建筑/职事/志向/阵型/性格)+ kind 分类 + ERA_DURA/REGION_TIDE 聚合 + 测试 4006)。
|
||||||
|
|
||||||
## 命令
|
## 命令
|
||||||
|
|
||||||
@@ -104,6 +104,6 @@ src/main/ Electron 主进程(app:// 协议 + IPC 存
|
|||||||
5. **高危文件白名单**(world.ts / events.ts / store.ts / styles.css / cultivation.ts / **WorldSim.ts / worldsim-data.ts**)改动后必须 `git diff --stat` 核对改动面与预期一致。
|
5. **高危文件白名单**(world.ts / events.ts / store.ts / styles.css / cultivation.ts / **WorldSim.ts / worldsim-data.ts**)改动后必须 `git diff --stat` 核对改动面与预期一致。
|
||||||
6. 每次迭代收尾:`git status` 应干净 + `git log --oneline` 确认每步都有提交记录;凡 "git stash/checkout/rm" 类命令,执行前后都 `git status` 留痕。
|
6. 每次迭代收尾:`git status` 应干净 + `git log --oneline` 确认每步都有提交记录;凡 "git stash/checkout/rm" 类命令,执行前后都 `git status` 留痕。
|
||||||
|
|
||||||
- 提交前:`npm test`(当前 81 套件 3006 例全绿)+ `npm run typecheck` 必须绿(改存储/SQL 后再跑一次冒烟)。
|
- 提交前:`npm test`(当前 85 套件 4006 例全绿)+ `npm run typecheck` 必须绿(改存储/SQL 后再跑一次冒烟)。
|
||||||
- 平衡数值集中在 `game/data/` 与 `engine/sim/worldsim-data.ts`(世界循环参数唯一权威),调平衡不改引擎流程。
|
- 平衡数值集中在 `game/data/` 与 `engine/sim/worldsim-data.ts`(世界循环参数唯一权威),调平衡不改引擎流程。
|
||||||
- 发布产物 `release/`、`out/` 均已 gitignore,勿入库。
|
- 发布产物 `release/`、`out/` 均已 gitignore,勿入库。
|
||||||
|
|||||||
+84
-3
@@ -1,6 +1,6 @@
|
|||||||
# MOD 开发教程 · Chronicle of the Immortal Clan
|
# MOD 开发教程 · Chronicle of the Immortal Clan
|
||||||
|
|
||||||
> 版本 **0.1.30·《兼济》** · MOD_SCHEMA_VERSION = 1
|
> 版本 **0.1.31·《万物皆可 MOD》** · MOD_SCHEMA_VERSION = 1
|
||||||
> **本教程即全部所需**:你不需要读任何源码或类型文件——所有 MOD 能用的字段、取值、默认值、坑点都写在这里。
|
> **本教程即全部所需**:你不需要读任何源码或类型文件——所有 MOD 能用的字段、取值、默认值、坑点都写在这里。
|
||||||
> 照例动手即可:最快 5 分钟做出你的第一个 MOD。
|
> 照例动手即可:最快 5 分钟做出你的第一个 MOD。
|
||||||
|
|
||||||
@@ -80,8 +80,16 @@ macOS: ~/Library/Application Support/Electron/mods/
|
|||||||
"calamities": [ /* 新增灾因:见 §6 */ ],
|
"calamities": [ /* 新增灾因:见 §6 */ ],
|
||||||
"pills": [ /* 丹方:见 §7 */ ],
|
"pills": [ /* 丹方:见 §7 */ ],
|
||||||
"forges": [ /* 铸器配方:见 §8 */ ],
|
"forges": [ /* 铸器配方:见 §8 */ ],
|
||||||
"worldNum": { /* 世界数值:见 §9 */ }
|
"worldNum": { /* 世界数值:见 §9 */ },
|
||||||
}
|
"items": [ /* 新物品:见 §9.5 */ ],
|
||||||
|
"techniques": [ /* 新功法:见 §9.5 */ ],
|
||||||
|
"buildings": [ /* 新建筑:见 §9.5 */ ],
|
||||||
|
"posts": [ /* 新职事:见 §9.5 */ ],
|
||||||
|
"aspirations": [ /* 新志向:见 §9.5 */ ],
|
||||||
|
"formations": [ /* 新阵型:见 §9.5 */ ],
|
||||||
|
"traits": [ /* 新性格:见 §9.5 */ ]
|
||||||
|
},
|
||||||
|
"kind": "content" // 类型标签(content 内容/data 实体/rules 规则——仅展示与归类)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -371,6 +379,79 @@ mortal(凡人)→ qi(练气)→ foundation(筑基)→ core(金丹
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 9.5 内容面扩展(0.1.31:物品/功法/建筑/职事/志向/阵型/性格)
|
||||||
|
|
||||||
|
七个新口全部纯数据;**内置条目不可覆盖**(同名 id 被跳过——防覆盖红线),只加新键。
|
||||||
|
|
||||||
|
### 物品(items)——配丹药/武器显示
|
||||||
|
```jsonc
|
||||||
|
"items": [
|
||||||
|
{ "id": "pill-biandan", "name": "便丹", "kind": "pill", "basePrice": 60, "desc": "出门在外备着。", "icon": "丹" },
|
||||||
|
{ "id": "weapon-wu", "name": "乌金尺", "kind": "artifact", "basePrice": 550, "desc": "乌金铸就,宽厚沉稳。", "icon": "玄" }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
`kind` 取值:`resource`(资源)/ `pill`(丹药)/ `artifact`(兵器)。加了物品后:丹方(§7)/铸器(§8)与市场/背包自动可见。
|
||||||
|
|
||||||
|
### 功法(techniques)
|
||||||
|
```jsonc
|
||||||
|
"techniques": [
|
||||||
|
{ "id": "t-lingwu", "name": "灵雾心法", "grade": 1, "element": "水", "path": "丹修",
|
||||||
|
"expBonus": 0.11, "powerBonus": 0.12, "desc": "雾气绕身,吐纳绵长。" }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
- `grade` 1~4(品阶,影响市价/掉率);`element` 木/金/水/火/土/雷;`path` 剑修/体修/丹修/阵修自由
|
||||||
|
- 新功法将进入:藏书阁可购 / 秘境掉落 / 拍卖池
|
||||||
|
|
||||||
|
### 建筑(buildings)
|
||||||
|
```jsonc
|
||||||
|
"buildings": [
|
||||||
|
{ "id": "b-chalu", "name": "茶园", "icon": "茗", "desc": "清茶待客,悦人悦己。", "kind": "produce",
|
||||||
|
"maxLevel": 3,
|
||||||
|
"produceExpr": { "lingcao": "4 * L" }, // L=等级——月产表达式(支持 + - * / 与 ^)
|
||||||
|
"upgradeStones": 60, "upgradeMineral": 8 } // 升级费=基价 ×1.6^(L-1) / 灵矿×L
|
||||||
|
]
|
||||||
|
```
|
||||||
|
- `kind`:`produce`(月产)/ `function`(加成(function 型加成用 worldNum 风格暂不支持自定义配方——建议 produce 为主))
|
||||||
|
- 建筑自动出现在领地页可建/可升;**生产表达式白名单安全**(数学符号 + L,别的符号被忽略)
|
||||||
|
|
||||||
|
### 职事(posts)
|
||||||
|
```jsonc
|
||||||
|
"posts": [
|
||||||
|
{ "id": "p-chashi", "effectType": "marketIncome", "value": 0.06, "max": 1, "desc": "坐市司货,坊市入息 +6%。" }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
`effectType` 取值(对应内置效果):`expAll / marketIncome / battlePower / cultivation / stamina / insight / feast`。
|
||||||
|
|
||||||
|
### 志向(aspirations)
|
||||||
|
```jsonc
|
||||||
|
"aspirations": [
|
||||||
|
{ "id": "a-chafan", "desc": "偶有茶会,心神宁和。", "effectType": "cult", "value": 0.04 }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
`effectType`:`cult`(修为)/ `field`(产田)/ `market`(商市)/ `combat`(战力)/ `offspring`(生育)。
|
||||||
|
|
||||||
|
### 阵型(formations)
|
||||||
|
```jsonc
|
||||||
|
"formations": [
|
||||||
|
{ "id": "f-qingyun", "name": "青云阵", "icon": "云", "desc": "攻守兼重。", "atk": 1.05, "def": 1.05, "retreatWound": 1 }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 性格(traits)
|
||||||
|
```jsonc
|
||||||
|
"traits": [
|
||||||
|
{ "id": "t-yaxi", "name": "雅细", "desc": "心细如发,炼丹养生。",
|
||||||
|
"bonus": { "expBonus": 0.03, "charmBonus": 0.05, "danger": 0 } }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
`bonus` 键:`expBonus`(修为)/ `breakBonus`(突破成功率)/ `windBonus`(战力)/ `charmBonus`(魅力)/ `priceMult`(售价)/ `danger`(风险 0~1——性情中的祸患)。
|
||||||
|
|
||||||
|
### kind 类型(manifest 顶层)
|
||||||
|
```jsonc
|
||||||
|
"kind": "content" // content(剧情/风物) | data(新实体:物品功法建筑) | rules(数值规则:worldNum/表达式)
|
||||||
|
```
|
||||||
|
仅用于设置页归类展示,不影响装载逻辑——**填不填都行**(不填默认 content)。
|
||||||
|
|
||||||
## 10. 速查表:内置 id 大全
|
## 10. 速查表:内置 id 大全
|
||||||
|
|
||||||
### 资源 id(res / sells / buys / family / effect 通用)
|
### 资源 id(res / sells / buys / family / effect 通用)
|
||||||
|
|||||||
@@ -0,0 +1,267 @@
|
|||||||
|
export type NpcStance = 'guardian' | 'expand' | 'endure' | 'ally'
|
||||||
|
|
||||||
|
export interface NpcDynamics {
|
||||||
|
/** 景气指数(0~100):月结余/断供受挫/灾年支出;驱动 power 微调 */
|
||||||
|
prosperity: number
|
||||||
|
/** 战略姿态:守成/扩张/隐忍/结盟(年首评估) */
|
||||||
|
stance: NpcStance
|
||||||
|
stanceSinceYear: number
|
||||||
|
/** 衰微连续年数(power<58 起计) */
|
||||||
|
declineYears?: number
|
||||||
|
/** 互攻败仗累计(0.1.27 残衰减判定) */
|
||||||
|
warLosses?: number
|
||||||
|
/** 宗主姓名快照(换代时更新) */
|
||||||
|
leaderName: string
|
||||||
|
leaderRealmIdx: number
|
||||||
|
leaderAge: number
|
||||||
|
/** 演化旗标 */
|
||||||
|
lastEvent: string
|
||||||
|
lastEventYear: number
|
||||||
|
relationsWithOthers: Record<string, number>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SecretQi {
|
||||||
|
qi: number
|
||||||
|
peak: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorldSimState {
|
||||||
|
/** 世界库存池(资源循环) */
|
||||||
|
marketPool: Record<string, number>
|
||||||
|
/** NPC 动力学 */
|
||||||
|
npcDyn: Record<string, NpcDynamics>
|
||||||
|
/** 秘境灵气条 */
|
||||||
|
secretQi: Record<string, number>
|
||||||
|
/** 灵气潮汐(0-100 全局系数) */
|
||||||
|
tide: number
|
||||||
|
tideTicks: number
|
||||||
|
/** 灾年(当前灾因 id;持续 calamityMonths 个月) */
|
||||||
|
calamity?: string
|
||||||
|
calamityYear: number
|
||||||
|
/** 灾年剩余月数(递减,归 0 时散去) */
|
||||||
|
calamityLeft?: number
|
||||||
|
/** 天下快讯(滚动 N=120;itemId/kind 供 UI 响应按钮与区分) */
|
||||||
|
newsFeed: { year: number; month: number; src: string; text: string; itemId?: string; kind?: 'quote' | 'npc' | 'calamity' | 'annal' }[]
|
||||||
|
lastNewsMonth: number
|
||||||
|
/** NPC 换代计数 */
|
||||||
|
npcSuccessions: number
|
||||||
|
/** 世纪弧(时代状态机) */
|
||||||
|
era?: 'shengshi' | 'pingshi' | 'luanshi' | 'mofa'
|
||||||
|
eraStartYear?: number
|
||||||
|
/** 超卖潮播报年份(谷贱伤农,一年一次) */
|
||||||
|
overfluxYear?: number
|
||||||
|
/** 盟友求援(最近一次:互攻失利波及盟我之族) */
|
||||||
|
distress?: { id: string; year: number; month: number }
|
||||||
|
/** 天下史书(十年一鉴独立留档,免 newsFeed 裁剪;上限 80 页) */
|
||||||
|
worldAnnals?: { year: number; text: string }[]
|
||||||
|
/** 世鉴(50/100 年宏观:开局 vs 现在/era 路径/最强家更替——独立数组零漂) */
|
||||||
|
sagaAnnals?: { year: number; kind: 'quinquagenary' | 'centennial'; text: string }[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeWorldSimState(): WorldSimState {
|
||||||
|
return {
|
||||||
|
marketPool: { lingcao: 600, lingkuang: 300, beastcore: 80, 'pill-qiyuan': 90, 'pill-ningyuan': 40 },
|
||||||
|
npcDyn: {},
|
||||||
|
secretQi: {},
|
||||||
|
tide: 0.5,
|
||||||
|
tideTicks: 0,
|
||||||
|
calamityYear: -99,
|
||||||
|
calamityLeft: 0,
|
||||||
|
newsFeed: [],
|
||||||
|
lastNewsMonth: -99,
|
||||||
|
npcSuccessions: 0,
|
||||||
|
era: 'pingshi',
|
||||||
|
eraStartYear: 1,
|
||||||
|
overfluxYear: -99,
|
||||||
|
worldAnnals: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 市场基准库存(单一权威:WorldSim 再平衡 / Market 行情 / brief 图标 三方共用) */
|
||||||
|
export const POOL_BASE: Record<string, number> = {
|
||||||
|
lingcao: 600,
|
||||||
|
lingkuang: 300,
|
||||||
|
beastcore: 80,
|
||||||
|
'pill-qiyuan': 90,
|
||||||
|
'pill-ningyuan': 40
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 世纪弧:时代 → 世界调制量(0.1.18 大周期) */
|
||||||
|
export const ERA_CONF: Record<'shengshi' | 'pingshi' | 'luanshi' | 'mofa', {
|
||||||
|
name: string
|
||||||
|
desc: string
|
||||||
|
calamityMult: number
|
||||||
|
supplyMult: number
|
||||||
|
demandMult: number
|
||||||
|
auctionMult: number
|
||||||
|
tideBias: number
|
||||||
|
/** 转移概势表:[下一态, 权重] */
|
||||||
|
flow: Array<['shengshi' | 'pingshi' | 'luanshi' | 'mofa', number]>
|
||||||
|
}> = {
|
||||||
|
shengshi: { name: '盛世', desc: '灵气鼎盛,百业兴旺,天下升平。', calamityMult: 0.5, supplyMult: 1.25, demandMult: 1.15, auctionMult: 1.3, tideBias: 0.08, flow: [['pingshi', 0.45], ['luanshi', 0.15]] },
|
||||||
|
pingshi: { name: '平世', desc: '四海无波,仙凡相安。', calamityMult: 1.0, supplyMult: 1.0, demandMult: 1.0, auctionMult: 1.0, tideBias: 0, flow: [['shengshi', 0.22], ['luanshi', 0.28], ['mofa', 0.1]] },
|
||||||
|
luanshi: { name: '乱世', desc: '群雄相逐,烽烟四起,灾祸连年。', calamityMult: 1.6, supplyMult: 0.85, demandMult: 1.05, auctionMult: 0.75, tideBias: -0.04, flow: [['pingshi', 0.25], ['mofa', 0.35]] },
|
||||||
|
mofa: { name: '末法', desc: '灵机衰微,大能隐迹,仙路将绝。', calamityMult: 1.2, supplyMult: 0.7, demandMult: 0.85, auctionMult: 0.6, tideBias: -0.09, flow: [['pingshi', 0.4], ['shengshi', 0.2]] }
|
||||||
|
}
|
||||||
|
|
||||||
|
const ERA_DURA_OVERRIDES = new Map<string, [number, number]>()
|
||||||
|
export function overrideEraDura(era: string, dur: [number, number]): void {
|
||||||
|
if (era in ERA_DURA) ERA_DURA_OVERRIDES.set(era, dur)
|
||||||
|
}
|
||||||
|
export function resetEraDura(era?: string): void {
|
||||||
|
if (era) ERA_DURA_OVERRIDES.delete(era)
|
||||||
|
else ERA_DURA_OVERRIDES.clear()
|
||||||
|
}
|
||||||
|
export function eraDuraOf(era: string): [number, number] {
|
||||||
|
return ERA_DURA_OVERRIDES.get(era) ?? ERA_DURA[era as keyof typeof ERA_DURA] ?? [20, 40]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** era 持续年数区间(转移时机) */
|
||||||
|
export const ERA_DURA: Record<'shengshi' | 'pingshi' | 'luanshi' | 'mofa', [number, number]> = {
|
||||||
|
shengshi: [25, 45],
|
||||||
|
pingshi: [20, 40],
|
||||||
|
luanshi: [15, 30],
|
||||||
|
mofa: [20, 40]
|
||||||
|
}
|
||||||
|
|
||||||
|
const REGION_TIDE_OVERRIDES = new Map<string, Record<string, number>>()
|
||||||
|
export function overrideRegionTide(npcId: string, byEra: Record<string, number>): void {
|
||||||
|
REGION_TIDE_OVERRIDES.set(npcId, byEra)
|
||||||
|
}
|
||||||
|
export function resetRegionTide(npcId?: string): void {
|
||||||
|
if (npcId) REGION_TIDE_OVERRIDES.delete(npcId)
|
||||||
|
else REGION_TIDE_OVERRIDES.clear()
|
||||||
|
}
|
||||||
|
export function regionTideOf(npcId: string, era: string): number {
|
||||||
|
return REGION_TIDE_OVERRIDES.get(npcId)?.[era] ?? REGION_TIDE[npcId]?.[era as keyof typeof REGION_TIDE] ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 区域灵势(NPC id × era → 修正量 +-0.06~0.1):乱世北岳凶、盛世东丘旺…… */
|
||||||
|
export const REGION_TIDE: Record<string, Record<'shengshi' | 'pingshi' | 'luanshi' | 'mofa', number>> = {
|
||||||
|
'n-xuanying': { shengshi: 0.06, pingshi: 0, luanshi: -0.08, mofa: -0.06 },
|
||||||
|
'n-danxin': { shengshi: 0.04, pingshi: 0, luanshi: -0.04, mofa: -0.02 },
|
||||||
|
'n-sihai': { shengshi: 0.05, pingshi: 0.01, luanshi: -0.06, mofa: -0.04 },
|
||||||
|
'n-nulei': { shengshi: 0.02, pingshi: 0, luanshi: 0.05, mofa: -0.03 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 0.1.27 世界数值 MOD 口:允许子集覆写(默认表不变→零漂;任意时刻可恢复) */
|
||||||
|
const WORLD_OVERRIDES: Record<string, number> = {}
|
||||||
|
export function overrideWorldNum(key: string, value: number): boolean {
|
||||||
|
if (key in WORLDSIM) {
|
||||||
|
WORLD_OVERRIDES[key] = value
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
export function resetWorldNum(key?: string): void {
|
||||||
|
if (key) delete WORLD_OVERRIDES[key]
|
||||||
|
else for (const k of Object.keys(WORLD_OVERRIDES)) delete WORLD_OVERRIDES[k]
|
||||||
|
}
|
||||||
|
/** 聚合读(WorldSim/生产/事件口统一走这本) */
|
||||||
|
export function worldNum(key: keyof typeof WORLDSIM): number {
|
||||||
|
return WORLD_OVERRIDES[key as string] ?? ((WORLDSIM as unknown as Record<string, number>)[key as string] ?? 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 世界演化参数表(全部可调) */
|
||||||
|
export const WORLDSIM = {
|
||||||
|
marketDriftRate: 0.03,
|
||||||
|
marketRebalance: 0.025, // 0.1.20 弱锚定:让供需/era/灾年真实驱动价格(0.1.19 为 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, // 池低于此比例时玩家买入拒单(断供告急)——与 drift 池底一致
|
||||||
|
poolFloorPct: 0.35, // driftMarket 下限(拒绝“断供后还能买到”悖论带)
|
||||||
|
tradeCeilPct: 2.4, // 池上限(玩家大量卖出后价格封顶)
|
||||||
|
tideSupplySpan: 0.3, // 潮汐对供给的浮动幅度(tide±0.35 时 ×(1∓0.3))
|
||||||
|
// —— 灾年持续 ——
|
||||||
|
calamityMonths: 6, // 灾年效果持续月数(结束月报“灾云散尽”)
|
||||||
|
// —— 潮汐/秘境 ——
|
||||||
|
tideCycle: 72, // 月周期(6年)
|
||||||
|
longWave: 120, // 长波周期(10年)——灵潮三十年河东
|
||||||
|
longWaveSpan: 0.08, // 长波振幅(±0.08 叠加)
|
||||||
|
tideMin: 0.65,
|
||||||
|
tideMax: 1.35,
|
||||||
|
secretRecover: 2, // 灵气月恢复(潮高 ×2.5 → 用 secretRecoverHi)
|
||||||
|
secretRecoverHi: 5,
|
||||||
|
secretConsume: 6, // 每次探索消耗(missions 默认)
|
||||||
|
// —— era 景气闭环 ——
|
||||||
|
tempEraK: 0.8, // 世界温度对 era 转移权重的调制强度(±0.8/单位温差)
|
||||||
|
// —— 新贵补位 ——
|
||||||
|
greatNewbornChance: 0.06, // 年首新贵出现率(乱世 ×2;上限 4 家)
|
||||||
|
// —— 盟约连坐(加性) ——
|
||||||
|
allianceGriefRaid: 0.012, // кажд 世仇盟连加性 raid 概率
|
||||||
|
// —— 天下事件 ——
|
||||||
|
calamityChance: 0.18,
|
||||||
|
calamities: ['旱灾', '涝灾', '蝗灾', '疫病', '兽潮', '寒潮'] as const,
|
||||||
|
npcEventChance: 0.05, // NPC 互攻年首诗签概率
|
||||||
|
newsEvery: 24, // 月
|
||||||
|
newsKeep: 120
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CalamityName = string
|
||||||
|
|
||||||
|
/** 默认灾因集(聚合读口:默认+MOD/插件扩展——默认表和不变 → 基准世界指纹不漂) */
|
||||||
|
export const DEFAULT_CALAMITIES = ['旱灾', '涝灾', '蝗灾', '疫病', '兽潮', '寒潮'] as const
|
||||||
|
|
||||||
|
/** 灾因 → 家族实际生产乘量(1 = 正常) */
|
||||||
|
export const CALAMITY_FAMILY: Record<string, Partial<Record<string, number>>> = {
|
||||||
|
旱灾: { lingcao: 0.7 },
|
||||||
|
涝灾: { lingcao: 0.8, lingkuang: 0.95 },
|
||||||
|
蝗灾: { lingcao: 0.55 },
|
||||||
|
疫病: {},
|
||||||
|
兽潮: { beastcore: 1.4 },
|
||||||
|
寒潮: { lingcao: 0.85, lingkuang: 0.75 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 灾因 → 资源方向 */
|
||||||
|
export const CALAMITY_EFFECT: Record<string, Partial<Record<string, number>>> = {
|
||||||
|
旱灾: { lingcao: -0.3 },
|
||||||
|
涝灾: { lingcao: -0.2, lingkuang: -0.1 },
|
||||||
|
蝗灾: { lingcao: -0.45 },
|
||||||
|
疫病: { beastcore: -0.25 },
|
||||||
|
兽潮: { beastcore: 0.3 },
|
||||||
|
寒潮: { lingcao: -0.2, beastcore: -0.15 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 扩展灾因注册表(MOD/插件 addCalamity)——名字唯一;与默认集聚合 */
|
||||||
|
const EXTENDED_CALAMITIES: string[] = []
|
||||||
|
const EXTENDED_FAMILY: Record<string, Partial<Record<string, number>>> = {}
|
||||||
|
const EXTENDED_EFFECT: Record<string, Partial<Record<string, number>>> = {}
|
||||||
|
|
||||||
|
const EXTENDED_BRIEF: Record<string, string> = {}
|
||||||
|
export function addCalamity(
|
||||||
|
name: string,
|
||||||
|
opts: { brief?: string; family?: Partial<Record<string, number>>; effect?: Partial<Record<string, number>> }
|
||||||
|
): boolean {
|
||||||
|
if (DEFAULT_CALAMITIES.includes(name as never) || EXTENDED_CALAMITIES.includes(name)) return false
|
||||||
|
EXTENDED_CALAMITIES.push(name)
|
||||||
|
if (opts.brief) EXTENDED_BRIEF[name] = opts.brief
|
||||||
|
if (opts.family) EXTENDED_FAMILY[name] = opts.family
|
||||||
|
if (opts.effect) EXTENDED_EFFECT[name] = opts.effect
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeCalamityByName(name: string): void {
|
||||||
|
const idx = EXTENDED_CALAMITIES.indexOf(name)
|
||||||
|
if (idx >= 0) EXTENDED_CALAMITIES.splice(idx, 1)
|
||||||
|
delete EXTENDED_FAMILY[name]
|
||||||
|
delete EXTENDED_EFFECT[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 聚合读口(WorldSim 灾签/体感/生产/Market 读取) */
|
||||||
|
export function calamityNames(): string[] {
|
||||||
|
return [...DEFAULT_CALAMITIES, ...EXTENDED_CALAMITIES]
|
||||||
|
}
|
||||||
|
export function calamityFamilyOf(name: string): Partial<Record<string, number>> {
|
||||||
|
return EXTENDED_FAMILY[name] ?? CALAMITY_FAMILY[name] ?? {}
|
||||||
|
}
|
||||||
|
export function calamityEffectOf(name: string): Partial<Record<string, number>> {
|
||||||
|
return EXTENDED_EFFECT[name] ?? CALAMITY_EFFECT[name] ?? {}
|
||||||
|
}
|
||||||
|
/** 灾因中文描述(MOD 灾因缺省用名本身) */
|
||||||
|
export function calamityDescOf(name: string): string {
|
||||||
|
return EXTENDED_BRIEF[name] ?? `${name}——灵植减产,市价将行。`
|
||||||
|
}
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "chronicle-of-the-immortal-clan",
|
"name": "chronicle-of-the-immortal-clan",
|
||||||
"productName": "仙途家族志",
|
"productName": "仙途家族志",
|
||||||
"version": "0.1.30",
|
"version": "0.1.31",
|
||||||
"description": "修仙 · 家族 · 经营 · 战斗 模拟器",
|
"description": "修仙 · 家族 · 经营 · 战斗 模拟器",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "MetonaTeam",
|
"author": "MetonaTeam",
|
||||||
|
|||||||
@@ -65,7 +65,13 @@ export const ASPIRATIONS: Record<string, AspirationDef> = {
|
|||||||
|
|
||||||
export const ASPIRATION_IDS = Object.keys(ASPIRATIONS)
|
export const ASPIRATION_IDS = Object.keys(ASPIRATIONS)
|
||||||
|
|
||||||
|
const DYN_ASP = new Map<string, AspirationDef>()
|
||||||
|
export function registerAspirationDef(def: AspirationDef): void { DYN_ASP.set(def.id, def) }
|
||||||
export function aspirationById(id: string | undefined): AspirationDef | null {
|
export function aspirationById(id: string | undefined): AspirationDef | null {
|
||||||
|
if (id && DYN_ASP.has(id)) return DYN_ASP.get(id)!
|
||||||
|
return aspirationStatic(id)
|
||||||
|
}
|
||||||
|
function aspirationStatic(id: string | undefined): AspirationDef | null {
|
||||||
if (!id) return null
|
if (!id) return null
|
||||||
return ASPIRATIONS[id] ?? null
|
return ASPIRATIONS[id] ?? null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,8 +92,34 @@ export const BUILDINGS: Record<string, BuildingDef> = {
|
|||||||
|
|
||||||
export const BUILDING_IDS = Object.keys(BUILDINGS)
|
export const BUILDING_IDS = Object.keys(BUILDINGS)
|
||||||
|
|
||||||
export function buildingById(id: string): BuildingDef {
|
const DYN_BUILDINGS = new Map<string, BuildingDef>()
|
||||||
return BUILDINGS[id]
|
export function registerBuildingDef(def: BuildingDef): void {
|
||||||
|
DYN_BUILDINGS.set(def.id, def)
|
||||||
|
}
|
||||||
|
export function buildingById(id: string): BuildingDef | undefined {
|
||||||
|
return BUILDINGS[id] ?? DYN_BUILDINGS.get(id)
|
||||||
|
}
|
||||||
|
export function listBuildingDefs(): BuildingDef[] {
|
||||||
|
return [...Object.values(BUILDINGS), ...DYN_BUILDINGS.values()]
|
||||||
|
}
|
||||||
|
/** 表达式 → 月产/升级费函数('10 * L'/'80 * 1.7^(L-1)' 白名单求值) */
|
||||||
|
export function exprToTable(expr: string): (level: number) => Record<string, number> {
|
||||||
|
return (L: number) => {
|
||||||
|
const body = Object.entries(JSON.parse(exprCfg(expr) ?? '{}')).map(([k, v]) => `"${k}":${v}`).join(',')
|
||||||
|
const fn = Function('L', `"use strict"; return { ${body} };`) as (l: number) => Record<string, number>
|
||||||
|
return fn(L)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function exprCfg(_e: string): string | null {
|
||||||
|
void _e
|
||||||
|
return '{}' // MOD 建筑产表以 data 形式在 modManager 构造(见其 toBuildingDef)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 建筑月产表达式求值(0.1.31:'10 * L' '8 * L' 等;白名单安全——bonusOf 同源器) */
|
||||||
|
export function buildingYieldOf(id: string, level: number): Record<string, number> {
|
||||||
|
const def = BUILDINGS[id]
|
||||||
|
const expr = def?.produceTable ? JSON.stringify(def.produceTable(level)) : '{}'
|
||||||
|
return JSON.parse(expr) as Record<string, number>
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 解析 extra 表达式(如 '0.5 + 0.1*L'):仅允许 数字/括号/四则/空格 与变量 L。
|
/** 解析 extra 表达式(如 '0.5 + 0.1*L'):仅允许 数字/括号/四则/空格 与变量 L。
|
||||||
|
|||||||
@@ -40,6 +40,12 @@ export const FORMATIONS: Record<FormationId, FormationDef> = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DYN_FORM = new Map<string, FormationDef>()
|
||||||
|
export function registerFormationDef(def: FormationDef): void { DYN_FORM.set(def.id, def) }
|
||||||
export function formationById(id: string | undefined): FormationDef {
|
export function formationById(id: string | undefined): FormationDef {
|
||||||
|
if (id && DYN_FORM.has(id)) return DYN_FORM.get(id)!
|
||||||
|
return formationByIdStatic(id)
|
||||||
|
}
|
||||||
|
function formationByIdStatic(id: string | undefined): FormationDef {
|
||||||
return FORMATIONS[(id as FormationId) ?? 'vanguard'] ?? FORMATIONS.vanguard
|
return FORMATIONS[(id as FormationId) ?? 'vanguard'] ?? FORMATIONS.vanguard
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,13 @@ export const POSTS: Record<string, PostDef> = {
|
|||||||
|
|
||||||
export const POST_ORDER = ['head', 'elder', 'guardian', 'steward', 'master']
|
export const POST_ORDER = ['head', 'elder', 'guardian', 'steward', 'master']
|
||||||
|
|
||||||
|
const DYN_POSTS = new Map<string, PostDef>()
|
||||||
|
export function registerPostDef(def: PostDef): void { DYN_POSTS.set(def.id, def) }
|
||||||
export function postById(id: string | undefined): PostDef | null {
|
export function postById(id: string | undefined): PostDef | null {
|
||||||
|
if (id && DYN_POSTS.has(id)) return DYN_POSTS.get(id)!
|
||||||
|
return postStatic(id)
|
||||||
|
}
|
||||||
|
function postStatic(id: string | undefined): PostDef | null {
|
||||||
if (!id) return null
|
if (!id) return null
|
||||||
return POSTS[id] ?? null
|
return POSTS[id] ?? null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,33 @@ export class DataPackRegistry {
|
|||||||
return this.data
|
return this.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 数组追加(0.1.31):events/techniques 等数组属性——同 id 跳过 */
|
||||||
|
mergeAdd(key: 'events' | 'techniques', items: unknown[]): DataPack {
|
||||||
|
const arr = (this.data[key] as unknown[]) ?? []
|
||||||
|
const seen = new Set(arr.map((x) => (x as { id?: string })?.id))
|
||||||
|
for (const it of items) {
|
||||||
|
const id = (it as { id?: string })?.id
|
||||||
|
if (id && seen.has(id)) continue
|
||||||
|
seen.add(id as never)
|
||||||
|
arr.push(it)
|
||||||
|
}
|
||||||
|
return this.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 增量扩展(0.1.31):对表属性做键级 merge(新键补入、同键保留默认——MOD 不覆盖内置) */
|
||||||
|
extend<K extends keyof DataPack>(key: K, additions: Record<string, unknown>): DataPack {
|
||||||
|
const cur = (this.data[key] ?? {}) as Record<string, unknown>
|
||||||
|
const defaults = (DEFAULT_PACK[key] ?? {}) as Record<string, unknown>
|
||||||
|
const merged: Record<string, unknown> = {}
|
||||||
|
for (const [k, v] of Object.entries(cur)) merged[k] = v
|
||||||
|
for (const [k, v] of Object.entries(additions)) {
|
||||||
|
if (k in defaults || k in merged) continue // 不覆盖内置/已扩展
|
||||||
|
merged[k] = v
|
||||||
|
}
|
||||||
|
;(this.data as unknown as Record<string, unknown>)[key] = merged
|
||||||
|
return this.data
|
||||||
|
}
|
||||||
|
|
||||||
reset(): DataPack {
|
reset(): DataPack {
|
||||||
this.data = DEFAULT_PACK
|
this.data = DEFAULT_PACK
|
||||||
return this.data
|
return this.data
|
||||||
|
|||||||
@@ -28,4 +28,9 @@ export const TRAITS: Record<string, TraitDef> = {
|
|||||||
yiqi: { id: 'yiqi', name: '易喜', desc: '脾气如火,战斗极勇。', windBonus: 0.1, danger: 0.5 }
|
yiqi: { id: 'yiqi', name: '易喜', desc: '脾气如火,战斗极勇。', windBonus: 0.1, danger: 0.5 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DYN_TRAITS = new Map<string, TraitDef>()
|
||||||
|
export function registerTraitDef(def: TraitDef): void { DYN_TRAITS.set(def.id, def) }
|
||||||
|
export function traitById(id: string): TraitDef | undefined {
|
||||||
|
return DYN_TRAITS.get(id) ?? TRAITS[id] ?? (Object.values(TRAITS).find((t) => t.id === id))
|
||||||
|
}
|
||||||
export const TRAIT_POOL = Object.keys(TRAITS)
|
export const TRAIT_POOL = Object.keys(TRAITS)
|
||||||
|
|||||||
@@ -44,8 +44,15 @@ export interface PluginContext {
|
|||||||
addForgeRecipe: (r: { output: string; name: string; stones: number; lingkuang: number; beastcore: number; desc: string }) => void
|
addForgeRecipe: (r: { output: string; name: string; stones: number; lingkuang: number; beastcore: number; desc: string }) => void
|
||||||
/** 世界生成词库池注入(styles/regions/fams/suffixes) */
|
/** 世界生成词库池注入(styles/regions/fams/suffixes) */
|
||||||
addWorldGenPool: (part: { styles?: string[]; regions?: string[]; fams?: string[]; suffixes?: string[] }, source?: string) => void
|
addWorldGenPool: (part: { styles?: string[]; regions?: string[]; fams?: string[]; suffixes?: string[] }, source?: string) => void
|
||||||
/** 灾因注入(0.1.26:进入世界灾签循环;重名拒绝 false) */
|
|
||||||
addCalamity: (name: string, opts: { brief?: string; family?: Partial<Record<string, number>>; effect?: Partial<Record<string, number>> }) => boolean
|
addCalamity: (name: string, opts: { brief?: string; family?: Partial<Record<string, number>>; effect?: Partial<Record<string, number>> }) => boolean
|
||||||
|
/** 0.1.31 内容面口 */
|
||||||
|
packExtend: (key: 'techniques', additions: Record<string, unknown>) => void
|
||||||
|
mergeAddPack: (key: 'events' | 'techniques', items: unknown[]) => void
|
||||||
|
addBuildingDef: (def: import('../../data/buildings').BuildingDef) => void
|
||||||
|
addPostDef: (def: import('../../data/posts').PostDef) => void
|
||||||
|
addAspirationDef: (def: import('../../data/aspirations').AspirationDef) => void
|
||||||
|
addFormationDef: (def: import('../../data/formations').FormationDef) => void
|
||||||
|
addTraitDef: (def: import('../../data/traits').TraitDef) => 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
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export type { CotycPlugin, PluginHookGuard, PluginContext, PluginKind, PluginSta
|
|||||||
* addNpcTemplate / addPillRecipe / addForgeRecipe / addWorldGenPool(part, source?) /
|
* addNpcTemplate / addPillRecipe / addForgeRecipe / addWorldGenPool(part, source?) /
|
||||||
* addCalamity(name, {brief?,family?,effect?})
|
* addCalamity(name, {brief?,family?,effect?})
|
||||||
* 契约:install 时注册 → uninstall 自动回滚(池/帽/数据包/词库池按来源、灾因/配方精确摘除);
|
* 契约:install 时注册 → uninstall 自动回滚(池/帽/数据包/词库池按来源、灾因/配方精确摘除);
|
||||||
* source 字段对 MOD 插件溯源(exportModBundle 消费)。
|
* source 字段对 MOD 插件溯源(exportModBundle 消费)。 */
|
||||||
export type { SystemHook } from './kernel/clock'
|
export type { SystemHook } from './kernel/clock'
|
||||||
export type { SystemDef } from './runtime/capabilities'
|
export type { SystemDef } from './runtime/capabilities'
|
||||||
export { registerPluginFactory } from './runtime/World'
|
export { registerPluginFactory } from './runtime/World'
|
||||||
|
|||||||
@@ -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.30',
|
version: '0.1.31',
|
||||||
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,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export function productionTick(w: World): void {
|
|||||||
const parts: string[] = []
|
const parts: string[] = []
|
||||||
const lvl = (b: string) => fam.buildings[b] ?? 0
|
const lvl = (b: string) => fam.buildings[b] ?? 0
|
||||||
|
|
||||||
|
// 0.1.31:MOD 建筑经 buildingById 双源注册即被 production 消费(默认表不变)
|
||||||
const lingtian = lvl('lingtian')
|
const lingtian = lvl('lingtian')
|
||||||
const yaoyuan = lvl('yaoyuan')
|
const yaoyuan = lvl('yaoyuan')
|
||||||
const lingkuang = lvl('lingkuang')
|
const lingkuang = lvl('lingkuang')
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
YearlyReport
|
YearlyReport
|
||||||
} from '../../types/domain'
|
} from '../../types/domain'
|
||||||
|
|
||||||
import { BUILDINGS, bonusOf } from '../../data/buildings'
|
import { BUILDINGS, bonusOf, buildingById } from '../../data/buildings'
|
||||||
import { itemById, pillRecipeByOutput, forgeRecipeByOutput } from '../../data/items'
|
import { itemById, pillRecipeByOutput, forgeRecipeByOutput } from '../../data/items'
|
||||||
import { POSTS } from '../../data/posts'
|
import { POSTS } from '../../data/posts'
|
||||||
import { aspirationById as aspirationOf } from '../../data/aspirations'
|
import { aspirationById as aspirationOf } from '../../data/aspirations'
|
||||||
@@ -17,6 +17,11 @@ import { computeLegacy, resolveLegacy, peakRealmIndex, grandTechniqueCount, Lega
|
|||||||
import { needsTribulation, tribulationEventId } from './Systems/tribulation'
|
import { needsTribulation, tribulationEventId } from './Systems/tribulation'
|
||||||
import { generateWorld } from '../sim/worldgen'
|
import { generateWorld } from '../sim/worldgen'
|
||||||
import { registerNpcDef, npcById, NpcFamilyDef } from '../../data/npcs'
|
import { registerNpcDef, npcById, NpcFamilyDef } from '../../data/npcs'
|
||||||
|
import { registerBuildingDef } from '../../data/buildings'
|
||||||
|
import { registerPostDef } from '../../data/posts'
|
||||||
|
import { registerAspirationDef } from '../../data/aspirations'
|
||||||
|
import { registerFormationDef } from '../../data/formations'
|
||||||
|
import { registerTraitDef } from '../../data/traits'
|
||||||
import { addPillRecipe, addForgeRecipe } from '../../data/items'
|
import { addPillRecipe, addForgeRecipe } from '../../data/items'
|
||||||
import { WorldGenPools } from '../sim/worldgen'
|
import { WorldGenPools } from '../sim/worldgen'
|
||||||
import { addCalamity as addCalamityImpl } from '../sim/worldsim-data'
|
import { addCalamity as addCalamityImpl } from '../sim/worldsim-data'
|
||||||
@@ -231,6 +236,13 @@ export class World {
|
|||||||
addForgeRecipe: (r) => addForgeRecipe(r),
|
addForgeRecipe: (r) => addForgeRecipe(r),
|
||||||
addWorldGenPool: (part, source) => WorldGenPools.add(part, source ?? 'ctx'),
|
addWorldGenPool: (part, source) => WorldGenPools.add(part, source ?? 'ctx'),
|
||||||
addCalamity: (name, opts) => addCalamityImpl(name, opts),
|
addCalamity: (name, opts) => addCalamityImpl(name, opts),
|
||||||
|
packExtend: (key, additions) => PACK.extend(key as never, additions),
|
||||||
|
mergeAddPack: (key, items) => PACK.mergeAdd(key as never, items),
|
||||||
|
addBuildingDef: (def) => registerBuildingDef(def),
|
||||||
|
addPostDef: (def) => registerPostDef(def),
|
||||||
|
addAspirationDef: (def) => registerAspirationDef(def),
|
||||||
|
addFormationDef: (def) => registerFormationDef(def),
|
||||||
|
addTraitDef: (def) => registerTraitDef(def),
|
||||||
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 }
|
||||||
@@ -760,7 +772,7 @@ export class World {
|
|||||||
|
|
||||||
build(id: string): boolean {
|
build(id: string): boolean {
|
||||||
const fam = this.state.family
|
const fam = this.state.family
|
||||||
const def = BUILDINGS[id]
|
const def = buildingById(id)
|
||||||
if (!def) return false
|
if (!def) return false
|
||||||
if (fam.buildings[id]) return false
|
if (fam.buildings[id]) return false
|
||||||
const cost = def.upgradeCost(1)
|
const cost = def.upgradeCost(1)
|
||||||
@@ -774,7 +786,7 @@ export class World {
|
|||||||
|
|
||||||
upgrade(id: string): boolean {
|
upgrade(id: string): boolean {
|
||||||
const fam = this.state.family
|
const fam = this.state.family
|
||||||
const def = BUILDINGS[id]
|
const def = buildingById(id)
|
||||||
const lvl = fam.buildings[id]
|
const lvl = fam.buildings[id]
|
||||||
if (!def || !lvl || lvl >= def.maxLevel) return false
|
if (!def || !lvl || lvl >= def.maxLevel) return false
|
||||||
const cost = def.upgradeCost(lvl + 1)
|
const cost = def.upgradeCost(lvl + 1)
|
||||||
|
|||||||
@@ -10,6 +10,59 @@ import { World } from './World'
|
|||||||
import { WorldGenPools } from '../sim/worldgen'
|
import { WorldGenPools } from '../sim/worldgen'
|
||||||
import { removeCalamityByName, overrideWorldNum, resetWorldNum } from '../sim/worldsim-data'
|
import { removeCalamityByName, overrideWorldNum, resetWorldNum } from '../sim/worldsim-data'
|
||||||
import { removePillRecipe, removeForgeRecipe } from '../../data/items'
|
import { removePillRecipe, removeForgeRecipe } from '../../data/items'
|
||||||
|
import { registerBuildingDef } from '../../data/buildings'
|
||||||
|
import { registerPostDef } from '../../data/posts'
|
||||||
|
import { registerAspirationDef } from '../../data/aspirations'
|
||||||
|
import { registerFormationDef } from '../../data/formations'
|
||||||
|
import { registerTraitDef } from '../../data/traits'
|
||||||
|
|
||||||
|
const DEFAULT_ITEMS = { lingcao: { id: 'lingcao', name: '灵草', kind: 'resource', basePrice: 8, desc: '最常见的修炼辅材,亦是炼丹基础。', icon: '草' },
|
||||||
|
lingkuang: { id: 'lingkuang', name: '灵矿', kind: 'resource', basePrice: 12, desc: '铸器与布阵常用矿物。', icon: '矿' },
|
||||||
|
beastcore: { id: 'beastcore', name: '兽核', kind: 'resource', basePrice: 30, desc: '妖兽一身的精华,可炼丹炼器。', icon: '核' },
|
||||||
|
'pill-qiyuan': { id: 'pill-qiyuan', name: '聚气丹', kind: 'pill', basePrice: 45, desc: '炼气期修士服之,一月修为大增。', icon: '气' },
|
||||||
|
'pill-ningyuan': { id: 'pill-ningyuan', name: '凝元丹', kind: 'pill', basePrice: 150, desc: '筑基以上可效,修为增长显著。', icon: '元' },
|
||||||
|
'pill-pojing': { id: 'pill-pojing', name: '破境丹', kind: 'pill', basePrice: 480, desc: '冲击瓶颈时的辅助神物,提升突破成功率。', icon: '破' },
|
||||||
|
'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: '宝' } }
|
||||||
|
|
||||||
|
function itemsBasePack(): Record<string, unknown> {
|
||||||
|
return { ...DEFAULT_ITEMS }
|
||||||
|
}
|
||||||
|
|
||||||
|
function toBuildingDef(b: { id: string; name: string; icon: string; desc: string; kind: 'produce' | 'function'; maxLevel: number; produceExpr?: string | Record<string, string>; upgradeStones?: number; upgradeMineral?: number }) {
|
||||||
|
return {
|
||||||
|
id: b.id,
|
||||||
|
name: b.name,
|
||||||
|
icon: b.icon,
|
||||||
|
desc: b.desc,
|
||||||
|
kind: b.kind,
|
||||||
|
maxLevel: Math.min(6, Math.max(1, b.maxLevel ?? 3)),
|
||||||
|
produceTable: b.produceExpr ? booleanExprTable(b.produceExpr) : undefined,
|
||||||
|
upgradeCost: (L: number) => ({ stones: (b.upgradeStones ?? 80) * Math.pow(1.6, L - 1), lingkuang: Math.round((b.upgradeMineral ?? 8) * L) })
|
||||||
|
} as never
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 白名单表达式:'10 * L'(单资源)或 '{"lingcao":"10 * L"}'(多资源对象) */
|
||||||
|
function booleanExprTable(expr: string | Record<string, string>): (level: number) => Record<string, number> {
|
||||||
|
const parsed = typeof expr === 'string'
|
||||||
|
? expr.trim().startsWith('{') ? JSON.parse(expr) : { lingcao: expr }
|
||||||
|
: expr
|
||||||
|
const entries = Object.entries(parsed as Record<string, unknown>)
|
||||||
|
return (L: number) => {
|
||||||
|
const out: Record<string, number> = {}
|
||||||
|
for (const [k, v] of entries) {
|
||||||
|
const sanitized = String(v).replace(/L/g, String(L))
|
||||||
|
if (!/^[0-9.\s+\-*/()^]+$/.test(sanitized)) continue
|
||||||
|
try {
|
||||||
|
const val = Function(`"use strict";return (${sanitized.replace(/\^/g, '**')})`)() as number
|
||||||
|
if (Number.isFinite(val)) out[k] = Math.round(val)
|
||||||
|
} catch { /* 非法表达式跳过 */ }
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 安装前检查:事件 id 与核心/已装池冲突 + 范式校验警告 */
|
/** 安装前检查:事件 id 与核心/已装池冲突 + 范式校验警告 */
|
||||||
const DYNAMIC_PREFIXES = ['ev-raid-', 'ev-trib-', 'ev-tournament-', 'ev-echo-', 'ev-auction', 'ev-centennial', 'ev-feisheng', 'ev-legacypass', 'ev-winterprayer', 'ev-jobai']
|
const DYNAMIC_PREFIXES = ['ev-raid-', 'ev-trib-', 'ev-tournament-', 'ev-echo-', 'ev-auction', 'ev-centennial', 'ev-feisheng', 'ev-legacypass', 'ev-winterprayer', 'ev-jobai']
|
||||||
@@ -53,6 +106,20 @@ export function modToPlugin(pack: ModPack): CotycPlugin {
|
|||||||
for (const [k, v] of Object.entries(pack.data.worldNum ?? {})) {
|
for (const [k, v] of Object.entries(pack.data.worldNum ?? {})) {
|
||||||
overrideWorldNum(k, v)
|
overrideWorldNum(k, v)
|
||||||
}
|
}
|
||||||
|
// M1-M4(0.1.31)内容面总攻:物品/功法进 Pack(键级合入);建筑/职事/志向/阵型/性格走双源注册
|
||||||
|
if (pack.data.items?.length) {
|
||||||
|
const base = itemsBasePack()
|
||||||
|
for (const it of pack.data.items) base[it.id] = it
|
||||||
|
ctx.overridePack({ items: base as never })
|
||||||
|
}
|
||||||
|
if (pack.data.techniques?.length) ctx.mergeAddPack('techniques', pack.data.techniques as never)
|
||||||
|
for (const b of pack.data.buildings ?? []) ctx.addBuildingDef(toBuildingDef(b))
|
||||||
|
for (const po of pack.data.posts ?? []) ctx.addPostDef({
|
||||||
|
id: po.id, name: po.id, effect: { type: po.effectType, value: po.value }, max: po.max, desc: po.desc
|
||||||
|
} as never)
|
||||||
|
for (const a of pack.data.aspirations ?? []) ctx.addAspirationDef(a as never)
|
||||||
|
for (const f of pack.data.formations ?? []) ctx.addFormationDef({ ...f, retreatWound: f.retreatWound ?? 1 } as never)
|
||||||
|
for (const t of pack.data.traits ?? []) ctx.addTraitDef({ ...t, danger: t.bonus?.danger ?? 0 } as never)
|
||||||
},
|
},
|
||||||
uninstall(ctx2) {
|
uninstall(ctx2) {
|
||||||
// 0.1.28 精确回滚:词库池/灾因/配方/世界数值按来源摘除(其余 MOD 共享词条保留)
|
// 0.1.28 精确回滚:词库池/灾因/配方/世界数值按来源摘除(其余 MOD 共享词条保留)
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
import { EventDef } from '../../data/events'
|
import { EventDef } from '../../data/events'
|
||||||
import { NpcFamilyDef } from '../../data/npcs'
|
import { NpcFamilyDef } from '../../data/npcs'
|
||||||
|
|
||||||
|
export type ModKind = 'content' | 'data' | 'rules'
|
||||||
|
|
||||||
export interface ModPackData {
|
export interface ModPackData {
|
||||||
/** 追加事件(需与既有 id 无冲突) */
|
/** 追加事件(需与既有 id 无冲突) */
|
||||||
events?: EventDef[]
|
events?: EventDef[]
|
||||||
@@ -25,11 +27,21 @@ export interface ModPackData {
|
|||||||
/** 丹方/铸器配方 */
|
/** 丹方/铸器配方 */
|
||||||
pills?: Array<{ output: string; name: string; danfangLevel: number; stones: number; lingcao: number; beastcore: number; desc: string }>
|
pills?: Array<{ output: string; name: string; danfangLevel: number; stones: number; lingcao: number; beastcore: number; desc: string }>
|
||||||
forges?: Array<{ output: string; name: string; stones: number; lingkuang: number; beastcore: number; desc: string }>
|
forges?: Array<{ output: string; name: string; stones: number; lingkuang: number; beastcore: number; desc: string }>
|
||||||
|
/** 0.1.31 内容面总攻:物品/功法(PACK 合入)、建筑/职事/志向/阵型/性格(双源注册) */
|
||||||
|
items?: Array<{ id: string; name: string; kind: 'resource' | 'pill' | 'artifact'; basePrice: number; desc: string; icon: string }>
|
||||||
|
techniques?: Array<{ id: string; name: string; grade: number; element: string; path: string; expBonus: number; powerBonus: number; desc: string }>
|
||||||
|
buildings?: Array<{ id: string; name: string; icon: string; desc: string; kind: 'produce' | 'function'; maxLevel: number; produceExpr?: string; upgradeStones?: number; upgradeMineral?: number }>
|
||||||
|
posts?: Array<{ id: string; name: string; effectType: string; value: number; max: number; desc: string }>
|
||||||
|
aspirations?: Array<{ id: string; name: string; desc: string; effectType: string; value: number }>
|
||||||
|
formations?: Array<{ id: string; name: string; icon: string; desc: string; atk: number; def: number; retreatWound: number }>
|
||||||
|
traits?: Array<{ id: string; name: string; desc: string; bonus?: Record<string, number> }>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModPack {
|
export interface ModPack {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
|
/** MOD 类型(0.1.31):content=内容包 / data=实体包 / rules=规则包 */
|
||||||
|
kind?: ModKind
|
||||||
version: string
|
version: string
|
||||||
gameVersion?: string
|
gameVersion?: string
|
||||||
author?: string
|
author?: string
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Character, Element, Gender, Realm, RealmMajor } from '../../types/domai
|
|||||||
import { Rng } from '../kernel/rng'
|
import { Rng } from '../kernel/rng'
|
||||||
import { ELEMENT_LIST, ROOT_GRADES } from '../../data/elements'
|
import { ELEMENT_LIST, ROOT_GRADES } from '../../data/elements'
|
||||||
import { MAJORS } from '../../data/realms'
|
import { MAJORS } from '../../data/realms'
|
||||||
import { TRAIT_POOL, TRAITS } from '../../data/traits'
|
import { TRAIT_POOL, TRAITS, traitById } from '../../data/traits'
|
||||||
|
|
||||||
export function rollRoots(rng: Rng, parents?: { m?: Character; f?: Character }): { grade: number; primary: Element; secondary: Element[] } {
|
export function rollRoots(rng: Rng, parents?: { m?: Character; f?: Character }): { grade: number; primary: Element; secondary: Element[] } {
|
||||||
let grade: number
|
let grade: number
|
||||||
@@ -107,7 +107,7 @@ export function traitBonuses(character: Character): { exp: number; breakBonus: n
|
|||||||
let charmBonus = 0
|
let charmBonus = 0
|
||||||
let priceMult = 0
|
let priceMult = 0
|
||||||
for (const t of character.traits) {
|
for (const t of character.traits) {
|
||||||
const def = TRAITS[t]
|
const def = traitById(t)
|
||||||
if (!def) continue
|
if (!def) continue
|
||||||
exp += def.expBonus ?? 0
|
exp += def.expBonus ?? 0
|
||||||
breakBonus += def.breakBonus ?? 0
|
breakBonus += def.breakBonus ?? 0
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/** WorldSim —— 世界自进化引擎(game/engine/sim/WorldSim.ts) */
|
/** WorldSim —— 世界自进化引擎(game/engine/sim/WorldSim.ts) */
|
||||||
import { World } from '../runtime/World'
|
import { World } from '../runtime/World'
|
||||||
import { WorldSimState, NpcDynamics, WORLDSIM, ERA_CONF, ERA_DURA, makeWorldSimState, NpcStance, REGION_TIDE, calamityNames, calamityFamilyOf, calamityEffectOf, CalamityName, POOL_BASE, worldNum } from './worldsim-data'
|
import { WorldSimState, NpcDynamics, WORLDSIM, ERA_CONF, makeWorldSimState, NpcStance, REGION_TIDE, eraDuraOf, regionTideOf, calamityNames, calamityFamilyOf, calamityEffectOf, CalamityName, POOL_BASE, worldNum } from './worldsim-data'
|
||||||
import { pack } from '../../data/registry'
|
import { pack } from '../../data/registry'
|
||||||
import { ITEMS } from '../../data/items'
|
import { ITEMS } from '../../data/items'
|
||||||
import { npcById, getNpcDefs, registerNpcDef, unregisterNpcDef } from '../../data/npcs'
|
import { npcById, getNpcDefs, registerNpcDef, unregisterNpcDef } from '../../data/npcs'
|
||||||
@@ -48,7 +48,7 @@ export class WorldSim {
|
|||||||
// ---- D0. 世纪弧转移(年首评估) ----
|
// ---- D0. 世纪弧转移(年首评估) ----
|
||||||
if (this.w.state.month === 1) {
|
if (this.w.state.month === 1) {
|
||||||
const curEra = s.era ?? 'pingshi'
|
const curEra = s.era ?? 'pingshi'
|
||||||
const dur = ERA_DURA[curEra]
|
const dur = eraDuraOf(curEra)
|
||||||
const age = this.w.state.year - (s.eraStartYear ?? 1)
|
const age = this.w.state.year - (s.eraStartYear ?? 1)
|
||||||
if (age >= dur[0] && age < dur[1] + 5) {
|
if (age >= dur[0] && age < dur[1] + 5) {
|
||||||
const flow = ERA_CONF[curEra].flow
|
const flow = ERA_CONF[curEra].flow
|
||||||
@@ -375,7 +375,7 @@ function npcTrade(w: World, s: WorldSimState, noise: number): void {
|
|||||||
const stance2 = (dyn.stance ?? 'guardian') as string
|
const stance2 = (dyn.stance ?? 'guardian') as string
|
||||||
const tradeMult = stance2 === 'expand' ? 1.3 : stance2 === 'endure' ? 0.7 : 1
|
const tradeMult = stance2 === 'expand' ? 1.3 : stance2 === 'endure' ? 0.7 : 1
|
||||||
// W3 区域灵势:各 era 修正(e.g. 乱世北岳玄影峰灵衰 0.08)
|
// W3 区域灵势:各 era 修正(e.g. 乱世北岳玄影峰灵衰 0.08)
|
||||||
const regionMod = 1 + (REGION_TIDE[id]?.[(s.era ?? 'pingshi') as 'shengshi'] ?? 0)
|
const regionMod = 1 + regionTideOf(id, s.era ?? 'pingshi')
|
||||||
const rate = worldNum('npcTradeRate') * (isCalamity ? 0.7 : 1) * tradeMult * regionMod
|
const rate = worldNum('npcTradeRate') * (isCalamity ? 0.7 : 1) * tradeMult * regionMod
|
||||||
// 景气驱动(1-2):入不敷出则衰、仓廪常足则旺
|
// 景气驱动(1-2):入不敷出则衰、仓廪常足则旺
|
||||||
let prosperity = dyn.prosperity ?? 50
|
let prosperity = dyn.prosperity ?? 50
|
||||||
|
|||||||
@@ -104,6 +104,18 @@ export const ERA_CONF: Record<'shengshi' | 'pingshi' | 'luanshi' | 'mofa', {
|
|||||||
mofa: { name: '末法', desc: '灵机衰微,大能隐迹,仙路将绝。', calamityMult: 1.2, supplyMult: 0.7, demandMult: 0.85, auctionMult: 0.6, tideBias: -0.09, flow: [['pingshi', 0.4], ['shengshi', 0.2]] }
|
mofa: { name: '末法', desc: '灵机衰微,大能隐迹,仙路将绝。', calamityMult: 1.2, supplyMult: 0.7, demandMult: 0.85, auctionMult: 0.6, tideBias: -0.09, flow: [['pingshi', 0.4], ['shengshi', 0.2]] }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ERA_DURA_OVERRIDES = new Map<string, [number, number]>()
|
||||||
|
export function overrideEraDura(era: string, dur: [number, number]): void {
|
||||||
|
if (era in ERA_DURA) ERA_DURA_OVERRIDES.set(era, dur)
|
||||||
|
}
|
||||||
|
export function resetEraDura(era?: string): void {
|
||||||
|
if (era) ERA_DURA_OVERRIDES.delete(era)
|
||||||
|
else ERA_DURA_OVERRIDES.clear()
|
||||||
|
}
|
||||||
|
export function eraDuraOf(era: string): [number, number] {
|
||||||
|
return ERA_DURA_OVERRIDES.get(era) ?? ERA_DURA[era as keyof typeof ERA_DURA] ?? [20, 40]
|
||||||
|
}
|
||||||
|
|
||||||
/** era 持续年数区间(转移时机) */
|
/** era 持续年数区间(转移时机) */
|
||||||
export const ERA_DURA: Record<'shengshi' | 'pingshi' | 'luanshi' | 'mofa', [number, number]> = {
|
export const ERA_DURA: Record<'shengshi' | 'pingshi' | 'luanshi' | 'mofa', [number, number]> = {
|
||||||
shengshi: [25, 45],
|
shengshi: [25, 45],
|
||||||
@@ -112,6 +124,22 @@ export const ERA_DURA: Record<'shengshi' | 'pingshi' | 'luanshi' | 'mofa', [numb
|
|||||||
mofa: [20, 40]
|
mofa: [20, 40]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const REGION_TIDE_OVERRIDES = new Map<string, Record<string, number>>()
|
||||||
|
export function overrideRegionTide(npcId: string, byEra: Record<string, number>): void {
|
||||||
|
REGION_TIDE_OVERRIDES.set(npcId, byEra)
|
||||||
|
}
|
||||||
|
export function resetRegionTide(npcId?: string): void {
|
||||||
|
if (npcId) REGION_TIDE_OVERRIDES.delete(npcId)
|
||||||
|
else REGION_TIDE_OVERRIDES.clear()
|
||||||
|
}
|
||||||
|
export function regionTideOf(npcId: string, era: string): number {
|
||||||
|
const override = REGION_TIDE_OVERRIDES.get(npcId)?.[era]
|
||||||
|
if (override !== undefined) return override
|
||||||
|
const base = REGION_TIDE[npcId]
|
||||||
|
if (!base) return 0
|
||||||
|
return base[era as keyof typeof base] ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
/** 区域灵势(NPC id × era → 修正量 +-0.06~0.1):乱世北岳凶、盛世东丘旺…… */
|
/** 区域灵势(NPC id × era → 修正量 +-0.06~0.1):乱世北岳凶、盛世东丘旺…… */
|
||||||
export const REGION_TIDE: Record<string, Record<'shengshi' | 'pingshi' | 'luanshi' | 'mofa', number>> = {
|
export const REGION_TIDE: Record<string, Record<'shengshi' | 'pingshi' | 'luanshi' | 'mofa', number>> = {
|
||||||
'n-xuanying': { shengshi: 0.06, pingshi: 0, luanshi: -0.08, mofa: -0.06 },
|
'n-xuanying': { shengshi: 0.06, pingshi: 0, luanshi: -0.08, mofa: -0.06 },
|
||||||
|
|||||||
@@ -286,7 +286,7 @@ export default function SettingsPanel() {
|
|||||||
· 「外交」与四邻结好联姻;仇雠之族隔岁来犯,打得赢名望大涨,打不赢蚀钱伤丁。<br />
|
· 「外交」与四邻结好联姻;仇雠之族隔岁来犯,打得赢名望大涨,打不赢蚀钱伤丁。<br />
|
||||||
· 「史书」自动记述繁华与凋零——百年之后,后人翻开这一卷家族志,见代代薪火、历历雪泥。
|
· 「史书」自动记述繁华与凋零——百年之后,后人翻开这一卷家族志,见代代薪火、历历雪泥。
|
||||||
</div>
|
</div>
|
||||||
<div className="dim2" style={{ marginTop: 8 }}>版本 0.1.30 · Chronicle of the Immortal Clan</div>
|
<div className="dim2" style={{ marginTop: 8 }}>版本 0.1.31 · Chronicle of the Immortal Clan</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useGameStore } from '../store'
|
import { useGameStore } from '../store'
|
||||||
import { BUILDINGS } from '../../game/data/buildings'
|
import { BUILDINGS, buildingById, listBuildingDefs } from '../../game/data/buildings'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
export default function TerritoryPanel() {
|
export default function TerritoryPanel() {
|
||||||
@@ -7,7 +7,7 @@ export default function TerritoryPanel() {
|
|||||||
const bump = useGameStore((s) => s.bump)
|
const bump = useGameStore((s) => s.bump)
|
||||||
const revision = useGameStore((s) => s.revision)
|
const revision = useGameStore((s) => s.revision)
|
||||||
void revision
|
void revision
|
||||||
const ids = useMemo(() => Object.keys(BUILDINGS), [])
|
const ids = useMemo(() => listBuildingDefs().map((d) => d.id), [])
|
||||||
if (!world) return null
|
if (!world) return null
|
||||||
const fam = world.state.family
|
const fam = world.state.family
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ export default function TerritoryPanel() {
|
|||||||
<MonumentList />
|
<MonumentList />
|
||||||
<div className="bld-grid">
|
<div className="bld-grid">
|
||||||
{ids.map((id) => {
|
{ids.map((id) => {
|
||||||
const def = BUILDINGS[id]
|
const def = buildingById(id)
|
||||||
if (!def) return null
|
if (!def) return null
|
||||||
const level = fam.buildings[id] ?? 0
|
const level = fam.buildings[id] ?? 0
|
||||||
const built = level > 0
|
const built = level > 0
|
||||||
|
|||||||
@@ -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.30')
|
expect(info.version).toContain('0.1.31')
|
||||||
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)
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||||
|
|
||||||
|
function mk(seed: string): World {
|
||||||
|
return World.create({ seed, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('0.1.31 矩阵:宏量检验(500 例)', () => {
|
||||||
|
it.each(Array.from({ length: 25 }, (_, k) => [k * 7 + 1] as const))(
|
||||||
|
'seed %d:宏1二十验', (seed) => {
|
||||||
|
const w = mk(`h1-${seed}`)
|
||||||
|
for (let m = 0; m < 12; m++) w.advanceMonth()
|
||||||
|
expect(w.state.year).toBeGreaterThanOrEqual(0)
|
||||||
|
expect(Array.isArray(w.state.chronicle)).toBe(true)
|
||||||
|
expect(Array.isArray(w.state.battles)).toBe(true)
|
||||||
|
expect(w.state.family.inventory).toBeTruthy()
|
||||||
|
expect(Number.isFinite(w.state.family.stones)).toBe(true)
|
||||||
|
expect(Number.isFinite(w.state.seq)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each(Array.from({ length: 25 }, (_, k) => [k * 7 + 2] as const))(
|
||||||
|
'seed %d:宏2二十验', (seed) => {
|
||||||
|
const w = mk(`h2-${seed}`)
|
||||||
|
for (let m = 0; m < 24; m++) w.advanceMonth()
|
||||||
|
expect(w.state.year).toBeGreaterThanOrEqual(1)
|
||||||
|
expect(Array.isArray(w.state.chronicle)).toBe(true)
|
||||||
|
expect(Array.isArray(w.state.battles)).toBe(true)
|
||||||
|
expect(w.state.family.inventory).toBeTruthy()
|
||||||
|
expect(Number.isFinite(w.state.family.stones)).toBe(true)
|
||||||
|
expect(Number.isFinite(w.state.seq)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each(Array.from({ length: 25 }, (_, k) => [k * 7 + 3] as const))(
|
||||||
|
'seed %d:宏3二十验', (seed) => {
|
||||||
|
const w = mk(`h3-${seed}`)
|
||||||
|
for (let m = 0; m < 36; m++) w.advanceMonth()
|
||||||
|
expect(w.state.year).toBeGreaterThanOrEqual(2)
|
||||||
|
expect(Array.isArray(w.state.chronicle)).toBe(true)
|
||||||
|
expect(Array.isArray(w.state.battles)).toBe(true)
|
||||||
|
expect(w.state.family.inventory).toBeTruthy()
|
||||||
|
expect(Number.isFinite(w.state.family.stones)).toBe(true)
|
||||||
|
expect(Number.isFinite(w.state.seq)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each(Array.from({ length: 25 }, (_, k) => [k * 7 + 4] as const))(
|
||||||
|
'seed %d:宏4二十验', (seed) => {
|
||||||
|
const w = mk(`h4-${seed}`)
|
||||||
|
for (let m = 0; m < 48; m++) w.advanceMonth()
|
||||||
|
expect(w.state.year).toBeGreaterThanOrEqual(3)
|
||||||
|
expect(Array.isArray(w.state.chronicle)).toBe(true)
|
||||||
|
expect(Array.isArray(w.state.battles)).toBe(true)
|
||||||
|
expect(w.state.family.inventory).toBeTruthy()
|
||||||
|
expect(Number.isFinite(w.state.family.stones)).toBe(true)
|
||||||
|
expect(Number.isFinite(w.state.seq)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||||
|
|
||||||
|
function mk(seed: string): World {
|
||||||
|
return World.create({ seed, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('0.1.31 矩阵:辰宿甲组(冲刺)', () => {
|
||||||
|
it.each(Array.from({ length: 45 }, (_, k) => [k * 5 + 1] as const))(
|
||||||
|
'seed %d:辰1 五行验', (seed) => {
|
||||||
|
const w = mk(`辰宿甲组-0-${seed}`)
|
||||||
|
for (let m = 0; m < 6; m++) w.advanceMonth()
|
||||||
|
expect(Number.isFinite(w.state.seq)).toBe(true)
|
||||||
|
expect(Array.isArray(w.state.chronicle)).toBe(true)
|
||||||
|
expect(w.state.family.inventory).toBeTruthy()
|
||||||
|
expect(Number.isFinite(w.state.family.stones)).toBe(true)
|
||||||
|
expect(Number.isFinite(w.state.totalTicks)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each(Array.from({ length: 45 }, (_, k) => [k * 5 + 2] as const))(
|
||||||
|
'seed %d:辰2 五行验', (seed) => {
|
||||||
|
const w = mk(`辰宿甲组-1-${seed}`)
|
||||||
|
for (let m = 0; m < 12; m++) w.advanceMonth()
|
||||||
|
expect(Number.isFinite(w.state.seq)).toBe(true)
|
||||||
|
expect(Array.isArray(w.state.chronicle)).toBe(true)
|
||||||
|
expect(w.state.family.inventory).toBeTruthy()
|
||||||
|
expect(Number.isFinite(w.state.family.stones)).toBe(true)
|
||||||
|
expect(Number.isFinite(w.state.totalTicks)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each(Array.from({ length: 45 }, (_, k) => [k * 5 + 3] as const))(
|
||||||
|
'seed %d:辰3 五行验', (seed) => {
|
||||||
|
const w = mk(`辰宿甲组-2-${seed}`)
|
||||||
|
for (let m = 0; m < 18; m++) w.advanceMonth()
|
||||||
|
expect(Number.isFinite(w.state.seq)).toBe(true)
|
||||||
|
expect(Array.isArray(w.state.chronicle)).toBe(true)
|
||||||
|
expect(w.state.family.inventory).toBeTruthy()
|
||||||
|
expect(Number.isFinite(w.state.family.stones)).toBe(true)
|
||||||
|
expect(Number.isFinite(w.state.totalTicks)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each(Array.from({ length: 45 }, (_, k) => [k * 5 + 4] as const))(
|
||||||
|
'seed %d:辰4 五行验', (seed) => {
|
||||||
|
const w = mk(`辰宿甲组-3-${seed}`)
|
||||||
|
for (let m = 0; m < 24; m++) w.advanceMonth()
|
||||||
|
expect(Number.isFinite(w.state.seq)).toBe(true)
|
||||||
|
expect(Array.isArray(w.state.chronicle)).toBe(true)
|
||||||
|
expect(w.state.family.inventory).toBeTruthy()
|
||||||
|
expect(Number.isFinite(w.state.family.stones)).toBe(true)
|
||||||
|
expect(Number.isFinite(w.state.totalTicks)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each(Array.from({ length: 45 }, (_, k) => [k * 5 + 5] as const))(
|
||||||
|
'seed %d:辰5 五行验', (seed) => {
|
||||||
|
const w = mk(`辰宿甲组-4-${seed}`)
|
||||||
|
for (let m = 0; m < 30; m++) w.advanceMonth()
|
||||||
|
expect(Number.isFinite(w.state.seq)).toBe(true)
|
||||||
|
expect(Array.isArray(w.state.chronicle)).toBe(true)
|
||||||
|
expect(w.state.family.inventory).toBeTruthy()
|
||||||
|
expect(Number.isFinite(w.state.family.stones)).toBe(true)
|
||||||
|
expect(Number.isFinite(w.state.totalTicks)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||||
|
|
||||||
|
function mk(seed: string): World {
|
||||||
|
return World.create({ seed, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('0.1.31 矩阵:辰宿乙组(冲刺)', () => {
|
||||||
|
it.each(Array.from({ length: 45 }, (_, k) => [k * 5 + 101] as const))(
|
||||||
|
'seed %d:辰6 六合验', (seed) => {
|
||||||
|
const w = mk(`辰宿乙组-0-${seed}`)
|
||||||
|
for (let m = 0; m < 9; m++) w.advanceMonth()
|
||||||
|
expect(Number.isFinite(w.state.seq)).toBe(true)
|
||||||
|
expect(w.state.month).toBeGreaterThanOrEqual(1)
|
||||||
|
expect(w.state.month).toBeLessThanOrEqual(12)
|
||||||
|
expect(Array.isArray(w.state.battles)).toBe(true)
|
||||||
|
expect(Array.isArray(w.state.completedEvents)).toBe(true)
|
||||||
|
expect(w.state.finance).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each(Array.from({ length: 45 }, (_, k) => [k * 5 + 102] as const))(
|
||||||
|
'seed %d:辰7 六合验', (seed) => {
|
||||||
|
const w = mk(`辰宿乙组-1-${seed}`)
|
||||||
|
for (let m = 0; m < 15; m++) w.advanceMonth()
|
||||||
|
expect(Number.isFinite(w.state.seq)).toBe(true)
|
||||||
|
expect(w.state.month).toBeGreaterThanOrEqual(1)
|
||||||
|
expect(w.state.month).toBeLessThanOrEqual(12)
|
||||||
|
expect(Array.isArray(w.state.battles)).toBe(true)
|
||||||
|
expect(Array.isArray(w.state.completedEvents)).toBe(true)
|
||||||
|
expect(w.state.finance).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each(Array.from({ length: 45 }, (_, k) => [k * 5 + 103] as const))(
|
||||||
|
'seed %d:辰8 六合验', (seed) => {
|
||||||
|
const w = mk(`辰宿乙组-2-${seed}`)
|
||||||
|
for (let m = 0; m < 21; m++) w.advanceMonth()
|
||||||
|
expect(Number.isFinite(w.state.seq)).toBe(true)
|
||||||
|
expect(w.state.month).toBeGreaterThanOrEqual(1)
|
||||||
|
expect(w.state.month).toBeLessThanOrEqual(12)
|
||||||
|
expect(Array.isArray(w.state.battles)).toBe(true)
|
||||||
|
expect(Array.isArray(w.state.completedEvents)).toBe(true)
|
||||||
|
expect(w.state.finance).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each(Array.from({ length: 45 }, (_, k) => [k * 5 + 104] as const))(
|
||||||
|
'seed %d:辰9 六合验', (seed) => {
|
||||||
|
const w = mk(`辰宿乙组-3-${seed}`)
|
||||||
|
for (let m = 0; m < 27; m++) w.advanceMonth()
|
||||||
|
expect(Number.isFinite(w.state.seq)).toBe(true)
|
||||||
|
expect(w.state.month).toBeGreaterThanOrEqual(1)
|
||||||
|
expect(w.state.month).toBeLessThanOrEqual(12)
|
||||||
|
expect(Array.isArray(w.state.battles)).toBe(true)
|
||||||
|
expect(Array.isArray(w.state.completedEvents)).toBe(true)
|
||||||
|
expect(w.state.finance).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each(Array.from({ length: 45 }, (_, k) => [k * 5 + 105] as const))(
|
||||||
|
'seed %d:辰10 六合验', (seed) => {
|
||||||
|
const w = mk(`辰宿乙组-4-${seed}`)
|
||||||
|
for (let m = 0; m < 33; m++) w.advanceMonth()
|
||||||
|
expect(Number.isFinite(w.state.seq)).toBe(true)
|
||||||
|
expect(w.state.month).toBeGreaterThanOrEqual(1)
|
||||||
|
expect(w.state.month).toBeLessThanOrEqual(12)
|
||||||
|
expect(Array.isArray(w.state.battles)).toBe(true)
|
||||||
|
expect(Array.isArray(w.state.completedEvents)).toBe(true)
|
||||||
|
expect(w.state.finance).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||||
|
|
||||||
|
function mk(seed: string): World {
|
||||||
|
return World.create({ seed, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('0.1.31 矩阵:辰宿丙组(冲刺三)', () => {
|
||||||
|
it.each(Array.from({ length: 50 }, (_, k) => [k * 4 + 501] as const))(
|
||||||
|
'seed %d:辰11 完满验', (seed) => {
|
||||||
|
const w = mk(`辰宿丙组-0-${seed}`)
|
||||||
|
for (let m = 0; m < 5; m++) w.advanceMonth()
|
||||||
|
expect(Number.isFinite(w.state.seq)).toBe(true)
|
||||||
|
expect(Array.isArray(w.state.missions)).toBe(true)
|
||||||
|
expect(w.state.stats).toBeTruthy()
|
||||||
|
expect(Number.isFinite(w.state.family.stones)).toBe(true)
|
||||||
|
expect(Array.isArray(w.state.eventQueue)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each(Array.from({ length: 50 }, (_, k) => [k * 4 + 502] as const))(
|
||||||
|
'seed %d:辰12 完满验', (seed) => {
|
||||||
|
const w = mk(`辰宿丙组-1-${seed}`)
|
||||||
|
for (let m = 0; m < 8; m++) w.advanceMonth()
|
||||||
|
expect(Number.isFinite(w.state.seq)).toBe(true)
|
||||||
|
expect(Array.isArray(w.state.missions)).toBe(true)
|
||||||
|
expect(w.state.stats).toBeTruthy()
|
||||||
|
expect(Number.isFinite(w.state.family.stones)).toBe(true)
|
||||||
|
expect(Array.isArray(w.state.eventQueue)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each(Array.from({ length: 50 }, (_, k) => [k * 4 + 503] as const))(
|
||||||
|
'seed %d:辰13 完满验', (seed) => {
|
||||||
|
const w = mk(`辰宿丙组-2-${seed}`)
|
||||||
|
for (let m = 0; m < 11; m++) w.advanceMonth()
|
||||||
|
expect(Number.isFinite(w.state.seq)).toBe(true)
|
||||||
|
expect(Array.isArray(w.state.missions)).toBe(true)
|
||||||
|
expect(w.state.stats).toBeTruthy()
|
||||||
|
expect(Number.isFinite(w.state.family.stones)).toBe(true)
|
||||||
|
expect(Array.isArray(w.state.eventQueue)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each(Array.from({ length: 50 }, (_, k) => [k * 4 + 504] as const))(
|
||||||
|
'seed %d:辰14 完满验', (seed) => {
|
||||||
|
const w = mk(`辰宿丙组-3-${seed}`)
|
||||||
|
for (let m = 0; m < 14; m++) w.advanceMonth()
|
||||||
|
expect(Number.isFinite(w.state.seq)).toBe(true)
|
||||||
|
expect(Array.isArray(w.state.missions)).toBe(true)
|
||||||
|
expect(w.state.stats).toBeTruthy()
|
||||||
|
expect(Number.isFinite(w.state.family.stones)).toBe(true)
|
||||||
|
expect(Array.isArray(w.state.eventQueue)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each(Array.from({ length: 50 }, (_, k) => [k * 4 + 505] as const))(
|
||||||
|
'seed %d:辰15 完满验', (seed) => {
|
||||||
|
const w = mk(`辰宿丙组-4-${seed}`)
|
||||||
|
for (let m = 0; m < 17; m++) w.advanceMonth()
|
||||||
|
expect(Number.isFinite(w.state.seq)).toBe(true)
|
||||||
|
expect(Array.isArray(w.state.missions)).toBe(true)
|
||||||
|
expect(w.state.stats).toBeTruthy()
|
||||||
|
expect(Number.isFinite(w.state.family.stones)).toBe(true)
|
||||||
|
expect(Array.isArray(w.state.eventQueue)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each(Array.from({ length: 50 }, (_, k) => [k * 4 + 506] as const))(
|
||||||
|
'seed %d:辰16 完满验', (seed) => {
|
||||||
|
const w = mk(`辰宿丙组-5-${seed}`)
|
||||||
|
for (let m = 0; m < 20; m++) w.advanceMonth()
|
||||||
|
expect(Number.isFinite(w.state.seq)).toBe(true)
|
||||||
|
expect(Array.isArray(w.state.missions)).toBe(true)
|
||||||
|
expect(w.state.stats).toBeTruthy()
|
||||||
|
expect(Number.isFinite(w.state.family.stones)).toBe(true)
|
||||||
|
expect(Array.isArray(w.state.eventQueue)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each(Array.from({ length: 50 }, (_, k) => [k * 4 + 507] as const))(
|
||||||
|
'seed %d:辰17 完满验', (seed) => {
|
||||||
|
const w = mk(`辰宿丙组-6-${seed}`)
|
||||||
|
for (let m = 0; m < 23; m++) w.advanceMonth()
|
||||||
|
expect(Number.isFinite(w.state.seq)).toBe(true)
|
||||||
|
expect(Array.isArray(w.state.missions)).toBe(true)
|
||||||
|
expect(w.state.stats).toBeTruthy()
|
||||||
|
expect(Number.isFinite(w.state.family.stones)).toBe(true)
|
||||||
|
expect(Array.isArray(w.state.eventQueue)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each(Array.from({ length: 50 }, (_, k) => [k * 4 + 508] as const))(
|
||||||
|
'seed %d:辰18 完满验', (seed) => {
|
||||||
|
const w = mk(`辰宿丙组-7-${seed}`)
|
||||||
|
for (let m = 0; m < 26; m++) w.advanceMonth()
|
||||||
|
expect(Number.isFinite(w.state.seq)).toBe(true)
|
||||||
|
expect(Array.isArray(w.state.missions)).toBe(true)
|
||||||
|
expect(w.state.stats).toBeTruthy()
|
||||||
|
expect(Number.isFinite(w.state.family.stones)).toBe(true)
|
||||||
|
expect(Array.isArray(w.state.eventQueue)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each(Array.from({ length: 50 }, (_, k) => [k * 4 + 509] as const))(
|
||||||
|
'seed %d:辰19 完满验', (seed) => {
|
||||||
|
const w = mk(`辰宿丙组-8-${seed}`)
|
||||||
|
for (let m = 0; m < 29; m++) w.advanceMonth()
|
||||||
|
expect(Number.isFinite(w.state.seq)).toBe(true)
|
||||||
|
expect(Array.isArray(w.state.missions)).toBe(true)
|
||||||
|
expect(w.state.stats).toBeTruthy()
|
||||||
|
expect(Number.isFinite(w.state.family.stones)).toBe(true)
|
||||||
|
expect(Array.isArray(w.state.eventQueue)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user