Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
809707408b | ||
|
|
de46808188 |
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "chronicle-of-the-immortal-clan",
|
"name": "chronicle-of-the-immortal-clan",
|
||||||
"productName": "仙途家族志",
|
"productName": "仙途家族志",
|
||||||
"version": "0.1.15",
|
"version": "0.1.16",
|
||||||
"description": "修仙 · 家族 · 经营 · 战斗 模拟器",
|
"description": "修仙 · 家族 · 经营 · 战斗 模拟器",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "MetonaTeam",
|
"author": "MetonaTeam",
|
||||||
|
|||||||
@@ -95,3 +95,15 @@ export const BUILDING_IDS = Object.keys(BUILDINGS)
|
|||||||
export function buildingById(id: string): BuildingDef {
|
export function buildingById(id: string): BuildingDef {
|
||||||
return BUILDINGS[id]
|
return BUILDINGS[id]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 解析 extra 表达式(如 '0.5 + 0.1*L'):仅允许 数字/括号/四则/空格 与变量 L。
|
||||||
|
* 白名单校验后求值——所有数字加成以 extra 为单一权威。 */
|
||||||
|
export function bonusOf(id: string, key: string, level: number): number {
|
||||||
|
const def = BUILDINGS[id]
|
||||||
|
const expr = def?.extra?.[key]
|
||||||
|
if (!expr) return 0
|
||||||
|
const sanitized = expr.replace(/L/g, String(level))
|
||||||
|
if (!/^[0-9.\s+\-*/()]+$/.test(sanitized)) return 0
|
||||||
|
const v = Function(`"use strict";return (${sanitized})`)() as number
|
||||||
|
return Number.isFinite(v) ? v : 0
|
||||||
|
}
|
||||||
|
|||||||
@@ -41,3 +41,41 @@ export interface SimpleTradeItem {
|
|||||||
desc: string
|
desc: string
|
||||||
count: number
|
count: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 丹方:丹房等级门槛 + 材料 + 灵石;唯一权威(craftPill 引用)。 */
|
||||||
|
export interface PillRecipe {
|
||||||
|
output: string
|
||||||
|
name: string
|
||||||
|
danfangLevel: number
|
||||||
|
stones: number
|
||||||
|
lingcao: number
|
||||||
|
beastcore: number
|
||||||
|
desc: string
|
||||||
|
}
|
||||||
|
export const PILL_RECIPES: PillRecipe[] = [
|
||||||
|
{ output: 'pill-qiyuan', name: '聚气丹', danfangLevel: 1, stones: 20, lingcao: 15, beastcore: 0, desc: '炼气期修士服之,一月修为大增。' },
|
||||||
|
{ output: 'pill-ningyuan', name: '凝元丹', danfangLevel: 2, stones: 60, lingcao: 25, beastcore: 4, desc: '筑基以上可效,修为增长显著。' },
|
||||||
|
{ output: 'pill-pojing', name: '破境丹', danfangLevel: 3, stones: 160, lingcao: 40, beastcore: 10, desc: '冲击瓶颈时的辅助神物,提升突破成功率。' }
|
||||||
|
]
|
||||||
|
|
||||||
|
/** 铸器配方:灵矿+兽核+灵石 → 兵刃;无建筑限制(自建工坊念头,数值即门槛)。 */
|
||||||
|
export interface ForgeRecipe {
|
||||||
|
output: string
|
||||||
|
name: string
|
||||||
|
stones: number
|
||||||
|
lingkuang: number
|
||||||
|
beastcore: number
|
||||||
|
desc: string
|
||||||
|
}
|
||||||
|
export const FORGE_RECIPES: ForgeRecipe[] = [
|
||||||
|
{ output: 'weapon-qi', name: '法器', stones: 300, lingkuang: 8, beastcore: 2, desc: '蕴灵之器,可引动灵力。' },
|
||||||
|
{ output: 'weapon-ling', name: '灵器', stones: 900, lingkuang: 20, beastcore: 6, desc: '有灵之器,锋芒隐露。' },
|
||||||
|
{ output: 'weapon-fa', name: '法宝', stones: 2600, lingkuang: 40, beastcore: 15, desc: '罕世法宝,非金丹不能驾驭。' }
|
||||||
|
]
|
||||||
|
|
||||||
|
export function pillRecipeByOutput(id: string): PillRecipe | undefined {
|
||||||
|
return PILL_RECIPES.find((r) => r.output === id)
|
||||||
|
}
|
||||||
|
export function forgeRecipeByOutput(id: string): ForgeRecipe | undefined {
|
||||||
|
return FORGE_RECIPES.find((r) => r.output === id)
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import { POSTS } from './posts'
|
|||||||
import { TRAITS } from './traits'
|
import { TRAITS } from './traits'
|
||||||
import { ROOT_GRADES, ROOT_GRADE_NAMES, ELEMENT_LIST } from './elements'
|
import { ROOT_GRADES, ROOT_GRADE_NAMES, ELEMENT_LIST } from './elements'
|
||||||
import { MAJORS, MAJOR_ORDER } from './realms'
|
import { MAJORS, MAJOR_ORDER } from './realms'
|
||||||
|
import { ASPIRATIONS } from './aspirations'
|
||||||
|
import { FORMATIONS } from './formations'
|
||||||
|
import { SEASON } from './season'
|
||||||
|
|
||||||
export interface DataPack {
|
export interface DataPack {
|
||||||
items: typeof ITEMS
|
items: typeof ITEMS
|
||||||
@@ -25,6 +28,9 @@ export interface DataPack {
|
|||||||
elements: typeof ELEMENT_LIST
|
elements: typeof ELEMENT_LIST
|
||||||
majors: typeof MAJORS
|
majors: typeof MAJORS
|
||||||
majorOrder: typeof MAJOR_ORDER
|
majorOrder: typeof MAJOR_ORDER
|
||||||
|
aspirations: typeof ASPIRATIONS
|
||||||
|
formations: typeof FORMATIONS
|
||||||
|
season: typeof SEASON
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 默认包:与库内静态数据**同引用**(保证确定性指纹不变);未来 MOD 通过注入覆盖 */
|
/** 默认包:与库内静态数据**同引用**(保证确定性指纹不变);未来 MOD 通过注入覆盖 */
|
||||||
@@ -43,7 +49,10 @@ export const DEFAULT_PACK: DataPack = {
|
|||||||
rootGradeNames: ROOT_GRADE_NAMES,
|
rootGradeNames: ROOT_GRADE_NAMES,
|
||||||
elements: ELEMENT_LIST,
|
elements: ELEMENT_LIST,
|
||||||
majors: MAJORS,
|
majors: MAJORS,
|
||||||
majorOrder: MAJOR_ORDER
|
majorOrder: MAJOR_ORDER,
|
||||||
|
aspirations: ASPIRATIONS,
|
||||||
|
formations: FORMATIONS,
|
||||||
|
season: SEASON
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ export interface LootDef {
|
|||||||
artifactChance: number
|
artifactChance: number
|
||||||
techniqueChance: number
|
techniqueChance: number
|
||||||
beastcoreChance?: number
|
beastcoreChance?: number
|
||||||
|
/** 掉功法品阶上限(按秘境难度设定,防低阶秘境出四阶仙典) */
|
||||||
|
techGrades?: number[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MissionDef {
|
export interface MissionDef {
|
||||||
@@ -60,7 +62,7 @@ export const MISSIONS: MissionDef[] = [
|
|||||||
{ kind: 'combat', months: 2, title: '遭遇袭击', enemyId: 'e-tiebei' },
|
{ kind: 'combat', months: 2, title: '遭遇袭击', enemyId: 'e-tiebei' },
|
||||||
{ kind: 'resource', months: 2, title: '寻获残洞', loot: { resources: { lingcao: [30, 80], beastcore: [1, 3] }, artifactChance: 0.05, techniqueChance: 0.03 } }
|
{ kind: 'resource', months: 2, title: '寻获残洞', loot: { resources: { lingcao: [30, 80], beastcore: [1, 3] }, artifactChance: 0.05, techniqueChance: 0.03 } }
|
||||||
],
|
],
|
||||||
completionLoot: { resources: { lingcao: [20, 60], lingkuang: [5, 15] }, artifactChance: 0.08, techniqueChance: 0.03 }
|
completionLoot: { resources: { lingcao: [20, 60], lingkuang: [5, 15] }, artifactChance: 0.08, techniqueChance: 0.03, techGrades: [1, 2] }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'm-xuangu', name: '玄冰谷', region: '北境寒渊', icon: '冰',
|
id: 'm-xuangu', name: '玄冰谷', region: '北境寒渊', icon: '冰',
|
||||||
@@ -71,7 +73,7 @@ export const MISSIONS: MissionDef[] = [
|
|||||||
{ kind: 'combat', months: 2, title: '泉主现身', enemyId: 'e-huiyuan' },
|
{ kind: 'combat', months: 2, title: '泉主现身', enemyId: 'e-huiyuan' },
|
||||||
{ kind: 'boss', months: 2, title: '冰潭之下', enemyId: 'e-baiqi' }
|
{ kind: 'boss', months: 2, title: '冰潭之下', enemyId: 'e-baiqi' }
|
||||||
],
|
],
|
||||||
completionLoot: { resources: { lingkuang: [30, 70], beastcore: [2, 5] }, artifactChance: 0.15, techniqueChance: 0.06 }
|
completionLoot: { resources: { lingkuang: [30, 70], beastcore: [2, 5] }, artifactChance: 0.15, techniqueChance: 0.06, techGrades: [2, 3] }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'm-guzhan', name: '古战阵', region: '南陵荒原', icon: '战',
|
id: 'm-guzhan', name: '古战阵', region: '南陵荒原', icon: '战',
|
||||||
@@ -83,7 +85,7 @@ export const MISSIONS: MissionDef[] = [
|
|||||||
{ kind: 'combat', months: 2, title: '守阵铁骑', enemyId: 'e-huanhan' },
|
{ kind: 'combat', months: 2, title: '守阵铁骑', enemyId: 'e-huanhan' },
|
||||||
{ kind: 'resource', months: 3, title: '挖开枯井', loot: { resources: { lingkuang: [40, 90], beastcore: [2, 6] }, artifactChance: 0.2, techniqueChance: 0.08 } }
|
{ kind: 'resource', months: 3, title: '挖开枯井', loot: { resources: { lingkuang: [40, 90], beastcore: [2, 6] }, artifactChance: 0.2, techniqueChance: 0.08 } }
|
||||||
],
|
],
|
||||||
completionLoot: { resources: { lingkuang: [40, 90], beastcore: [2, 8] }, artifactChance: 0.22, techniqueChance: 0.1 }
|
completionLoot: { resources: { lingkuang: [40, 90], beastcore: [2, 8] }, artifactChance: 0.22, techniqueChance: 0.1, techGrades: [2, 3] }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'm-lingshan', name: '灵鹫山', region: '东海之滨', icon: '鹫',
|
id: 'm-lingshan', name: '灵鹫山', region: '东海之滨', icon: '鹫',
|
||||||
|
|||||||
@@ -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.15',
|
version: '0.1.16',
|
||||||
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,
|
||||||
|
|||||||
@@ -165,15 +165,18 @@ export function resolveRaid(
|
|||||||
): EncounterResult {
|
): EncounterResult {
|
||||||
const npc = w.state.npcFamilies[npcId]
|
const npc = w.state.npcFamilies[npcId]
|
||||||
const def = npcById(npcId)
|
const def = npcById(npcId)
|
||||||
|
// 宗主实力分层:power 由年度成长+换代驱动,劫掠强度随之浮动(0.5↔1.8)
|
||||||
|
const npcPower = npc.power ?? 60
|
||||||
|
const strength = Math.min(1.8, Math.max(0.5, 0.5 + (npcPower / 120) * 0.5))
|
||||||
const enemy: EnemyDef = {
|
const enemy: EnemyDef = {
|
||||||
id: npcId,
|
id: npcId,
|
||||||
name: `${npc.name}的劫掠队`,
|
name: `${npc.name}的劫掠队`,
|
||||||
realm: def.leaderRealm,
|
realm: def.leaderRealm,
|
||||||
strength: 0.78,
|
strength,
|
||||||
icon: '袭',
|
icon: '袭',
|
||||||
desc: def.desc
|
desc: def.desc
|
||||||
}
|
}
|
||||||
const risk = 0.55
|
const risk = Math.min(0.75, 0.5 + (npcPower / 120) * 0.1)
|
||||||
const res = resolveEncounter(w, {
|
const res = resolveEncounter(w, {
|
||||||
title: `${npc.name}来袭!`,
|
title: `${npc.name}来袭!`,
|
||||||
enemy,
|
enemy,
|
||||||
@@ -198,23 +201,29 @@ export function resolveRaid(
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
export function rollWarbooty(w: World, loot: LootDef): Record<string, number> {
|
export function rollWarbooty(w: World, loot: LootDef, qiRatio = 1): Record<string, number> {
|
||||||
const result: Record<string, number> = {}
|
const result: Record<string, number> = {}
|
||||||
for (const [k, r] of Object.entries(loot.resources)) {
|
for (const [k, r] of Object.entries(loot.resources)) {
|
||||||
const v = w.rng.int(r[0], r[1])
|
const v = Math.round(w.rng.int(r[0], r[1]) * qiRatio)
|
||||||
result[k] = v
|
result[k] = v
|
||||||
w.state.family.inventory[k] = (w.state.family.inventory[k] ?? 0) + v
|
w.state.family.inventory[k] = (w.state.family.inventory[k] ?? 0) + v
|
||||||
}
|
}
|
||||||
if (loot.artifactChance && w.rng.chance(loot.artifactChance)) {
|
const itemFactor = Math.min(1.4, Math.max(0.5, 0.6 + 0.4 * qiRatio))
|
||||||
|
if (loot.artifactChance && w.rng.chance(loot.artifactChance * itemFactor)) {
|
||||||
const pool = ['weapon-fan', 'weapon-qi', 'weapon-ling']
|
const pool = ['weapon-fan', 'weapon-qi', 'weapon-ling']
|
||||||
const a = w.rng.pick(pool)
|
const a = w.rng.pick(pool)
|
||||||
w.state.family.inventory[a] = (w.state.family.inventory[a] ?? 0) + 1
|
w.state.family.inventory[a] = (w.state.family.inventory[a] ?? 0) + 1
|
||||||
result[a] = 1
|
result[a] = 1
|
||||||
}
|
}
|
||||||
if (loot.techniqueChance && w.rng.chance(loot.techniqueChance)) {
|
if (loot.techniqueChance && w.rng.chance(loot.techniqueChance * itemFactor)) {
|
||||||
const t = w.rng.pick(pack().techniques)
|
const pool = loot.techGrades && loot.techGrades.length > 0
|
||||||
|
? pack().techniques.filter((t) => loot.techGrades!.includes(t.grade))
|
||||||
|
: pack().techniques
|
||||||
|
if (pool.length > 0) {
|
||||||
|
const t = w.rng.pick(pool)
|
||||||
w.state.family.techniques.push(t.id)
|
w.state.family.techniques.push(t.id)
|
||||||
result['tech'] = 1
|
result['tech'] = 1
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,12 @@ import { newCharacter } from '../pcgen'
|
|||||||
import { MALE_GIVEN, FEMALE_GIVEN } from '../../kernel/names'
|
import { MALE_GIVEN, FEMALE_GIVEN } from '../../kernel/names'
|
||||||
import { ASPIRATION_IDS } from '../../../data/aspirations'
|
import { ASPIRATION_IDS } from '../../../data/aspirations'
|
||||||
import { seasonMod } from '../../../data/season'
|
import { seasonMod } from '../../../data/season'
|
||||||
|
import { bonusOf } from '../../../data/buildings'
|
||||||
import { needsTribulation, tribulationEventId } from './tribulation'
|
import { needsTribulation, tribulationEventId } from './tribulation'
|
||||||
|
|
||||||
export function monthlyRate(w: World, c: Character): number {
|
export function monthlyRate(w: World, c: Character, herbFactor = 1): number {
|
||||||
const st = w.state
|
const st = w.state
|
||||||
let rate = 1
|
let rate = herbFactor
|
||||||
rate *= 1.0 + c.perception * 0.18
|
rate *= 1.0 + c.perception * 0.18
|
||||||
rate *= ROOT_GRADES[c.roots.grade]?.expBonus ?? 0.5
|
rate *= ROOT_GRADES[c.roots.grade]?.expBonus ?? 0.5
|
||||||
const tech = techniqueById(c.techniqueId)
|
const tech = techniqueById(c.techniqueId)
|
||||||
@@ -25,8 +26,7 @@ export function monthlyRate(w: World, c: Character): number {
|
|||||||
rate *= 0.65
|
rate *= 0.65
|
||||||
}
|
}
|
||||||
const buildings = st.family.buildings
|
const buildings = st.family.buildings
|
||||||
const juling = buildings['juling'] ?? 0
|
rate *= 1 + bonusOf('juling', 'expBonus', buildings['juling'] ?? 0)
|
||||||
rate *= 1 + juling * 0.05
|
|
||||||
if (w.sysEnabled('season')) rate *= 1 + seasonMod(st.month, 'cult')
|
if (w.sysEnabled('season')) rate *= 1 + seasonMod(st.month, 'cult')
|
||||||
rate *= 1 + w.postBonus('expAll')
|
rate *= 1 + w.postBonus('expAll')
|
||||||
if (st.family.flag['fengFeiBless']) rate *= 1.05
|
if (st.family.flag['fengFeiBless']) rate *= 1.05
|
||||||
@@ -37,8 +37,7 @@ export function monthlyRate(w: World, c: Character): number {
|
|||||||
if (c.state === 'meditation') {
|
if (c.state === 'meditation') {
|
||||||
rate *= 1.35
|
rate *= 1.35
|
||||||
rate *= 1 + w.postBonus('meditation') + (w.sysEnabled('season') ? seasonMod(st.month, 'meditation') : 0)
|
rate *= 1 + w.postBonus('meditation') + (w.sysEnabled('season') ? seasonMod(st.month, 'meditation') : 0)
|
||||||
const dongfu = buildings['dongfu'] ?? 0
|
rate *= 1 + bonusOf('dongfu', 'expBonus', buildings['dongfu'] ?? 0)
|
||||||
rate *= 1 + dongfu * 0.08
|
|
||||||
} else if (c.state === 'expedition') {
|
} else if (c.state === 'expedition') {
|
||||||
rate *= 0.25
|
rate *= 0.25
|
||||||
} else if (c.state === 'wounded') {
|
} else if (c.state === 'wounded') {
|
||||||
@@ -68,9 +67,29 @@ export function cultivationTick(w: World): void {
|
|||||||
c.aspiration = w.rng.pick(ASPIRATION_IDS)
|
c.aspiration = w.rng.pick(ASPIRATION_IDS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// ---- A1 修为经济化:灵草是修行燃料 ----
|
||||||
|
const inv = w.state.family.inventory
|
||||||
|
let demand = 0
|
||||||
for (const c of Object.values(w.state.members)) {
|
for (const c of Object.values(w.state.members)) {
|
||||||
if (!c.alive || c.state === 'apprentice') continue
|
if (!c.alive || c.state === 'apprentice') continue
|
||||||
const rate = monthlyRate(w, c)
|
if (c.realm.major === 'mortal') continue
|
||||||
|
demand += c.state === 'meditation' ? 2 : 1
|
||||||
|
}
|
||||||
|
let herbFactor = 1
|
||||||
|
if (demand > 0) {
|
||||||
|
const supply = inv.lingcao ?? 0
|
||||||
|
const used = Math.min(supply, demand)
|
||||||
|
inv.lingcao = supply - used
|
||||||
|
const ratio = supply >= demand ? 1 : supply / demand
|
||||||
|
herbFactor = 0.6 + 0.4 * ratio
|
||||||
|
if (ratio < 0.5 && w.state.family.flag['herbWarnYear'] !== w.state.year) {
|
||||||
|
w.state.family.flag['herbWarnYear'] = w.state.year
|
||||||
|
w.log('bad', '灵草告罄,族中修行渐缓——需扩灵田药园或购草补续。')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const c of Object.values(w.state.members)) {
|
||||||
|
if (!c.alive || c.state === 'apprentice') continue
|
||||||
|
const rate = monthlyRate(w, c, herbFactor)
|
||||||
if (rate <= 0) continue
|
if (rate <= 0) continue
|
||||||
c.realmProgress = Math.min(100, c.realmProgress + rate)
|
c.realmProgress = Math.min(100, c.realmProgress + rate)
|
||||||
// 悟道进度:有功法且修为之外,另积一分慧根
|
// 悟道进度:有功法且修为之外,另积一分慧根
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export function diplomacyTick(w: World): void {
|
|||||||
const drift = w.rng.chance(0.15)
|
const drift = w.rng.chance(0.15)
|
||||||
for (const npc of Object.values(s.npcFamilies)) {
|
for (const npc of Object.values(s.npcFamilies)) {
|
||||||
if (drift) {
|
if (drift) {
|
||||||
if (npc.relation > 0) npc.relation -= 1
|
if (npc.relation > 0 && !npc.allied) npc.relation -= 1
|
||||||
else if (npc.relation < 0) npc.relation += 1
|
else if (npc.relation < 0) npc.relation += 1
|
||||||
}
|
}
|
||||||
if (npc.relation < -50) {
|
if (npc.relation < -50) {
|
||||||
@@ -25,6 +25,10 @@ export function yearGrowth(w: World): void {
|
|||||||
const def = npcById(npc.id)
|
const def = npcById(npc.id)
|
||||||
const [a, b] = def.powerGrowth
|
const [a, b] = def.powerGrowth
|
||||||
npc.power += w.rng.int(a, b)
|
npc.power += w.rng.int(a, b)
|
||||||
|
if (npc.allied) {
|
||||||
|
w.state.family.stones += 15
|
||||||
|
w.log('info', `同盟${npc.name}遣使来贺,赠灵石15(结盟年利)。`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export function missionTick(w: World): void {
|
|||||||
if (victim.health < 35) victim.state = 'wounded'
|
if (victim.health < 35) victim.state = 'wounded'
|
||||||
}
|
}
|
||||||
} else if (stage.kind === 'resource') {
|
} else if (stage.kind === 'resource') {
|
||||||
const loot = rollWarbooty(w, stage.loot ?? def.completionLoot)
|
const loot = rollWarbooty(w, stage.loot ?? def.completionLoot, qiRatioOf(w, def.id))
|
||||||
m.log.push(`${stage.title}:收获 ${lootText(loot)}。`)
|
m.log.push(`${stage.title}:收获 ${lootText(loot)}。`)
|
||||||
} else if (stage.kind === 'combat' || stage.kind === 'boss') {
|
} else if (stage.kind === 'combat' || stage.kind === 'boss') {
|
||||||
const enemy = ENEMIES.find((e) => e.id === stage.enemyId) ?? ENEMIES[0]
|
const enemy = ENEMIES.find((e) => e.id === stage.enemyId) ?? ENEMIES[0]
|
||||||
@@ -69,7 +69,7 @@ export function missionTick(w: World): void {
|
|||||||
m.done = true
|
m.done = true
|
||||||
m.result = 'success'
|
m.result = 'success'
|
||||||
releaseSquad(w, m)
|
releaseSquad(w, m)
|
||||||
const total = rollWarbooty(w, def.completionLoot)
|
const total = rollWarbooty(w, def.completionLoot, qiRatioOf(w, def.id))
|
||||||
m.log.push(`凯旋而归,清点战利:${lootText(total)}。`)
|
m.log.push(`凯旋而归,清点战利:${lootText(total)}。`)
|
||||||
const survivors = squadOf(w, m).filter((c) => c.alive).map((c) => c.name).join('、')
|
const survivors = squadOf(w, m).filter((c) => c.alive).map((c) => c.name).join('、')
|
||||||
w.chronicle(
|
w.chronicle(
|
||||||
@@ -120,6 +120,11 @@ export function canSendMission(w: World, def: MissionDef, members: string[]): bo
|
|||||||
return w.state.missions.filter((m) => !m.done).length < 3
|
return w.state.missions.filter((m) => !m.done).length < 3
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function qiRatioOf(w: World, missionId: string): number {
|
||||||
|
const qi = new WorldSim(w).secretQiOf(missionId)
|
||||||
|
return Math.min(1.5, Math.max(0.5, 0.5 + qi / 125))
|
||||||
|
}
|
||||||
|
|
||||||
export function sendMission(w: World, defId: string, members: string[], formation?: FormationId): boolean {
|
export function sendMission(w: World, defId: string, members: string[], formation?: FormationId): boolean {
|
||||||
const def = missionById(defId)
|
const def = missionById(defId)
|
||||||
if (!canSendMission(w, def, members)) return false
|
if (!canSendMission(w, def, members)) return false
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { World } from '../World'
|
import type { World } from '../World'
|
||||||
import { aspirationById } from '../../../data/aspirations'
|
import { aspirationById } from '../../../data/aspirations'
|
||||||
import { seasonMod } from '../../../data/season'
|
import { seasonMod } from '../../../data/season'
|
||||||
|
import { CALAMITY_FAMILY } from '../../sim/worldsim-data'
|
||||||
|
|
||||||
export function productionTick(w: World): void {
|
export function productionTick(w: World): void {
|
||||||
const fam = w.state.family
|
const fam = w.state.family
|
||||||
@@ -17,13 +18,17 @@ export function productionTick(w: World): void {
|
|||||||
const fielders = w.aliveMembers().filter((c) => aspirationById(c.aspiration)?.effect.type === 'field').length
|
const fielders = w.aliveMembers().filter((c) => aspirationById(c.aspiration)?.effect.type === 'field').length
|
||||||
const merchants = w.aliveMembers().filter((c) => aspirationById(c.aspiration)?.effect.type === 'market').length
|
const merchants = w.aliveMembers().filter((c) => aspirationById(c.aspiration)?.effect.type === 'market').length
|
||||||
const springMod = w.sysEnabled('season') ? seasonMod(w.state.month, 'field') : 0
|
const springMod = w.sysEnabled('season') ? seasonMod(w.state.month, 'field') : 0
|
||||||
|
// 灾年减产(CALAMITY_FAMILY 乘量)
|
||||||
|
const cl = w.state.worldSim?.calamity as (keyof typeof CALAMITY_FAMILY) | undefined
|
||||||
|
const farmFactor = cl ? (CALAMITY_FAMILY[cl]?.lingcao ?? 1) : 1
|
||||||
|
const mineFactor = cl ? (CALAMITY_FAMILY[cl]?.lingkuang ?? 1) : 1
|
||||||
if (lingtian > 0) {
|
if (lingtian > 0) {
|
||||||
const v = Math.round(10 * lingtian * (1 + fielders * 0.05 + springMod))
|
const v = Math.round(10 * lingtian * (1 + fielders * 0.05 + springMod) * farmFactor)
|
||||||
inv.lingcao = (inv.lingcao ?? 0) + v
|
inv.lingcao = (inv.lingcao ?? 0) + v
|
||||||
parts.push(`灵田+${v}灵草`)
|
parts.push(`灵田+${v}灵草`)
|
||||||
}
|
}
|
||||||
if (yaoyuan > 0) {
|
if (yaoyuan > 0) {
|
||||||
const v = 5 * yaoyuan
|
const v = Math.round(5 * yaoyuan * farmFactor)
|
||||||
inv.lingcao = (inv.lingcao ?? 0) + v
|
inv.lingcao = (inv.lingcao ?? 0) + v
|
||||||
parts.push(`药园+${v}药草`)
|
parts.push(`药园+${v}药草`)
|
||||||
if (yaoyuan >= 3) {
|
if (yaoyuan >= 3) {
|
||||||
@@ -32,7 +37,7 @@ export function productionTick(w: World): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (lingkuang > 0) {
|
if (lingkuang > 0) {
|
||||||
const v = 8 * lingkuang
|
const v = Math.round(8 * lingkuang * mineFactor)
|
||||||
inv.lingkuang = (inv.lingkuang ?? 0) + v
|
inv.lingkuang = (inv.lingkuang ?? 0) + v
|
||||||
parts.push(`灵矿+${v}灵矿`)
|
parts.push(`灵矿+${v}灵矿`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ export function resolveTribulation(w: World, c: Character, mode: 'rash' | 'guard
|
|||||||
|
|
||||||
if (mode === 'delay') {
|
if (mode === 'delay') {
|
||||||
c.tribDelayYear = w.state.year + 1
|
c.tribDelayYear = w.state.year + 1
|
||||||
w.log('info', `${c.name} 按兵不动,引而不发,待来年再渡。`)
|
c.tribBoost = 0
|
||||||
|
w.log('info', `${c.name} 按兵不动,引而不发,待来年再渡(丹力渐散)。`)
|
||||||
return 'delayed'
|
return 'delayed'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,5 +91,5 @@ export function perTribChance(w: World, c: Character): number {
|
|||||||
nascent: 0.36,
|
nascent: 0.36,
|
||||||
spirit: 0.26
|
spirit: 0.26
|
||||||
} as Record<string, number>)[c.realm.major] ?? 0.9
|
} as Record<string, number>)[c.realm.major] ?? 0.9
|
||||||
return Math.min(0.92, base + c.mind * 0.01 + c.health / 400)
|
return Math.min(0.92, base + c.mind * 0.01 + c.health / 400 + (c.tribBoost ?? 0))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ import {
|
|||||||
YearlyReport
|
YearlyReport
|
||||||
} from '../../types/domain'
|
} from '../../types/domain'
|
||||||
|
|
||||||
import { BUILDINGS } from '../../data/buildings'
|
import { BUILDINGS, bonusOf } from '../../data/buildings'
|
||||||
|
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'
|
||||||
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 { createWorldState, findInheritor } from './creation'
|
import { createWorldState, findInheritor } from './creation'
|
||||||
import { SYSTEM_DEFS, SystemDef } from './capabilities'
|
import { SYSTEM_DEFS, SystemDef } from './capabilities'
|
||||||
import { emptyClock } from './clocks'
|
import { emptyClock } from './clocks'
|
||||||
@@ -445,6 +447,26 @@ export class World {
|
|||||||
|
|
||||||
// ==================== player actions ====================
|
// ==================== player actions ====================
|
||||||
|
|
||||||
|
setAlliance(npcId: string, on: boolean): boolean {
|
||||||
|
const npc = this.state.npcFamilies[npcId]
|
||||||
|
if (!npc) return false
|
||||||
|
if (on) {
|
||||||
|
if (npc.relation < 40) return false
|
||||||
|
npc.allied = true
|
||||||
|
npc.alliedSinceYear = this.state.year
|
||||||
|
this.log('good', `与${npc.name}结成同盟——盟誓既立,互不犯边。`)
|
||||||
|
this.chronicle('diplomacy', `本族与${npc.name}缔结同盟。`, undefined, true)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (npc.allied) {
|
||||||
|
npc.allied = false
|
||||||
|
npc.relation = Math.max(-100, npc.relation - 20)
|
||||||
|
this.log('bad', `与${npc.name}的盟约破裂——关系转恶。`)
|
||||||
|
this.chronicle('diplomacy', `与${npc.name}结盟破裂。`, undefined, true)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
assignHead(id: Id, silent = false): void {
|
assignHead(id: Id, silent = false): void {
|
||||||
const c = this.memberById(id)
|
const c = this.memberById(id)
|
||||||
if (!c.alive) return
|
if (!c.alive) return
|
||||||
@@ -498,7 +520,16 @@ export class World {
|
|||||||
inv[pill] = inv[pill]! - 1
|
inv[pill] = inv[pill]! - 1
|
||||||
if (pill === 'pill-pojing') {
|
if (pill === 'pill-pojing') {
|
||||||
if (c.realmProgress >= 100) {
|
if (c.realmProgress >= 100) {
|
||||||
|
if (needsTribulation(c) && this.sysEnabled('tribulation')) {
|
||||||
|
// C13:大境界渡劫不可跳过——丹药转为天劫助益
|
||||||
|
c.tribBoost = (c.tribBoost ?? 0) + 0.12
|
||||||
|
c.realmProgress = 100
|
||||||
|
this.pendingEvent(tribulationEventId(c))
|
||||||
|
this.state.pendingEvent = tribulationEventId(c)
|
||||||
|
this.log('info', `${c.name} 服下破境丹,丹力浑厚——雷云受感而聚,九霄震动。`)
|
||||||
|
} else {
|
||||||
this.resolveBottleneck(c, 0.22)
|
this.resolveBottleneck(c, 0.22)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.memberById(memberId).realmProgress = Math.min(100, c.realmProgress + 20)
|
this.memberById(memberId).realmProgress = Math.min(100, c.realmProgress + 20)
|
||||||
this.log('info', `${c.name} 服下破境丹,灵力充盈。`)
|
this.log('info', `${c.name} 服下破境丹,灵力充盈。`)
|
||||||
@@ -548,22 +579,44 @@ export class World {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
craftPill(kind: 'qiyuan' | 'ningyuan'): boolean {
|
craftPill(kind: 'qiyuan' | 'ningyuan' | 'pojing'): boolean {
|
||||||
const fam = this.state.family
|
const fam = this.state.family
|
||||||
const lvl = fam.buildings['danfang']
|
const lvl = fam.buildings['danfang']
|
||||||
if (!lvl) return false
|
const r = pillRecipeByOutput(`pill-${kind}`)
|
||||||
const cost = kind === 'qiyuan'
|
if (!lvl || !r) return false
|
||||||
? { lingcao: 15, beastcore: 0, stones: 20 }
|
if (lvl < r.danfangLevel) {
|
||||||
: { lingcao: 25, beastcore: 4, stones: 60 }
|
this.log('info', `丹房不足(需 ${r.danfangLevel} 级方可炼${r.name})。`)
|
||||||
if ((fam.inventory['lingcao'] ?? 0) < cost.lingcao) return false
|
return false
|
||||||
if ((fam.inventory['beastcore'] ?? 0) < cost.beastcore) return false
|
}
|
||||||
if (fam.stones < cost.stones) return false
|
const inv = fam.inventory
|
||||||
fam.inventory['lingcao'] -= cost.lingcao
|
if ((inv['lingcao'] ?? 0) < r.lingcao || (inv['beastcore'] ?? 0) < r.beastcore || fam.stones < r.stones) return false
|
||||||
fam.inventory['beastcore'] -= cost.beastcore
|
inv['lingcao'] -= r.lingcao
|
||||||
fam.stones -= cost.stones
|
inv['beastcore'] -= r.beastcore
|
||||||
fam.inventory[kind === 'qiyuan' ? 'pill-qiyuan' : 'pill-ningyuan'] =
|
fam.stones -= r.stones
|
||||||
(fam.inventory[kind === 'qiyuan' ? 'pill-qiyuan' : 'pill-ningyuan'] ?? 0) + 1
|
const chance = bonusOf('danfang', 'craftChance', lvl)
|
||||||
this.log('info', `丹房炼成一枚${kind === 'qiyuan' ? '聚气丹' : '凝元丹'}。`)
|
if (this.rng.chance(chance)) {
|
||||||
|
inv[`pill-${kind}`] = (inv[`pill-${kind}`] ?? 0) + 1
|
||||||
|
this.log('good', `丹房炼成一枚${r.name}。`)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
inv['lingcao'] += Math.ceil(r.lingcao / 2)
|
||||||
|
inv['beastcore'] += Math.ceil(r.beastcore / 2)
|
||||||
|
fam.stones += Math.ceil(r.stones / 2)
|
||||||
|
this.log('bad', `${r.name}炼废了——丹火失控,药材折半。`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
forgeArtifact(kind: 'weapon-qi' | 'weapon-ling' | 'weapon-fa'): boolean {
|
||||||
|
const fam = this.state.family
|
||||||
|
const r = forgeRecipeByOutput(kind)
|
||||||
|
if (!r) return false
|
||||||
|
const inv = fam.inventory
|
||||||
|
if ((inv['lingkuang'] ?? 0) < r.lingkuang || (inv['beastcore'] ?? 0) < r.beastcore || fam.stones < r.stones) return false
|
||||||
|
inv['lingkuang'] -= r.lingkuang
|
||||||
|
inv['beastcore'] -= r.beastcore
|
||||||
|
fam.stones -= r.stones
|
||||||
|
inv[kind] = (inv[kind] ?? 0) + 1
|
||||||
|
this.log('good', `炉火淬炼,得一${r.name}。`)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -611,8 +664,8 @@ export class World {
|
|||||||
const fam = this.state.family
|
const fam = this.state.family
|
||||||
const bonus =
|
const bonus =
|
||||||
1 +
|
1 +
|
||||||
(fam.buildings['yanwu'] ?? 0) * 0.04 +
|
bonusOf('yanwu', 'powerBonus', fam.buildings['yanwu'] ?? 0) +
|
||||||
(fam.buildings['lingshou'] ?? 0) * 0.05 +
|
bonusOf('lingshou', 'powerBonus', fam.buildings['lingshou'] ?? 0) +
|
||||||
this.postBonus('battlePower')
|
this.postBonus('battlePower')
|
||||||
const top = this.aliveMembers()
|
const top = this.aliveMembers()
|
||||||
.map((c) => combatPowerOf(this, c))
|
.map((c) => combatPowerOf(this, c))
|
||||||
|
|||||||
@@ -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, CALAMITY_EFFECT, CalamityName, POOL_BASE } from './worldsim-data'
|
import { WorldSimState, NpcDynamics, WORLDSIM, CALAMITY_EFFECT, CALAMITY_FAMILY, CalamityName, POOL_BASE } 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 } from '../../data/npcs'
|
import { npcById } from '../../data/npcs'
|
||||||
@@ -44,6 +44,22 @@ export class WorldSim {
|
|||||||
s.calamityYear = this.w.state.year
|
s.calamityYear = this.w.state.year
|
||||||
applyCalamityToMarket(s, cl as CalamityName)
|
applyCalamityToMarket(s, cl as CalamityName)
|
||||||
this.w.log('bad', `【天下灾年】${cl}——灵植减产,市价将行。`)
|
this.w.log('bad', `【天下灾年】${cl}——灵植减产,市价将行。`)
|
||||||
|
// B7 灾年落家族(一次性体感效果)
|
||||||
|
if (cl === '疫病') {
|
||||||
|
for (const c of this.w.aliveMembers()) {
|
||||||
|
if (this.w.rng.chance(0.6)) {
|
||||||
|
const dmg = 8 + this.w.rng.int(0, 14)
|
||||||
|
c.health = Math.max(1, c.health - dmg)
|
||||||
|
if (c.health < 20) c.state = 'wounded'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.w.log('bad', `瘟疫蔓延,族中大半病倒——灵药告急!`)
|
||||||
|
} else if (cl === '兽潮') {
|
||||||
|
const st = this.w.state.family.stones
|
||||||
|
this.w.state.family.stones -= Math.round(st * 0.04)
|
||||||
|
inv(this.w)['beastcore'] = (inv(this.w)['beastcore'] ?? 0) + 2
|
||||||
|
this.w.log('bad', `兽潮涌至,坊市稍有折损;猎得兽核数枚。`)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
s.calamity = undefined
|
s.calamity = undefined
|
||||||
}
|
}
|
||||||
@@ -147,6 +163,10 @@ function driftMarket(s: WorldSimState, noise: number): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function inv(w: World): Record<string, number> {
|
||||||
|
return w.state.family.inventory
|
||||||
|
}
|
||||||
|
|
||||||
function applyCalamityToMarket(s: WorldSimState, cl: CalamityName): void {
|
function applyCalamityToMarket(s: WorldSimState, cl: CalamityName): void {
|
||||||
const eff = CALAMITY_EFFECT[cl]
|
const eff = CALAMITY_EFFECT[cl]
|
||||||
for (const [id, pct] of Object.entries(eff) as [string, number][]) {
|
for (const [id, pct] of Object.entries(eff) as [string, number][]) {
|
||||||
|
|||||||
@@ -79,6 +79,16 @@ export const WORLDSIM = {
|
|||||||
|
|
||||||
export type CalamityName = (typeof WORLDSIM.calamities)[number]
|
export type CalamityName = (typeof WORLDSIM.calamities)[number]
|
||||||
|
|
||||||
|
/** 灾因 → 家族实际生产乘量(1 = 正常) */
|
||||||
|
export const CALAMITY_FAMILY: Record<CalamityName, 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<CalamityName, Partial<Record<string, number>>> = {
|
export const CALAMITY_EFFECT: Record<CalamityName, Partial<Record<string, number>>> = {
|
||||||
旱灾: { lingcao: -0.3 },
|
旱灾: { lingcao: -0.3 },
|
||||||
|
|||||||
@@ -4,10 +4,14 @@ import { normalizeGameState } from '../engine/runtime/World'
|
|||||||
export const CURRENT_SCHEMA = 2
|
export const CURRENT_SCHEMA = 2
|
||||||
export const APP_ID = 'cotyc'
|
export const APP_ID = 'cotyc'
|
||||||
|
|
||||||
export interface MigrationError {
|
export class MigrationError extends Error {
|
||||||
code: 'TOO_NEW' | 'UNKNOWN_VERSION'
|
code: 'TOO_NEW' | 'UNKNOWN_VERSION'
|
||||||
version: number
|
version: number
|
||||||
message: string
|
constructor(code: 'TOO_NEW' | 'UNKNOWN_VERSION', version: number, message: string) {
|
||||||
|
super(message)
|
||||||
|
this.code = code
|
||||||
|
this.version = version
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,7 +35,7 @@ export function migrateIfNeeded(state: GameState): { ok: true; state: GameState
|
|||||||
if (version > CURRENT_SCHEMA) {
|
if (version > CURRENT_SCHEMA) {
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
error: { code: 'TOO_NEW', version, message: `存档版本 ${version} 高于当前游戏支持(${CURRENT_SCHEMA}),请升级游戏。` }
|
error: new MigrationError('TOO_NEW', version, `存档版本 ${version} 高于当前游戏支持(${CURRENT_SCHEMA}),请升级游戏。`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let cur = state
|
let cur = state
|
||||||
@@ -40,12 +44,12 @@ export function migrateIfNeeded(state: GameState): { ok: true; state: GameState
|
|||||||
while (v < CURRENT_SCHEMA && guard < 20) {
|
while (v < CURRENT_SCHEMA && guard < 20) {
|
||||||
const step = MIGRATIONS.find((m) => m.from === v)
|
const step = MIGRATIONS.find((m) => m.from === v)
|
||||||
if (!step) {
|
if (!step) {
|
||||||
return { ok: false, error: { code: 'UNKNOWN_VERSION', version: v, message: `未知存档版本 ${v},无法迁移。` } }
|
return { ok: false, error: new MigrationError('UNKNOWN_VERSION', v, `未知存档版本 ${v},无法迁移。`) }
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
cur = step.fn(cur)
|
cur = step.fn(cur)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return { ok: false, error: { code: 'UNKNOWN_VERSION', version: v, message: `迁移失败:${String(e)}` } }
|
return { ok: false, error: new MigrationError('UNKNOWN_VERSION', v, `迁移失败:${String(e)}`) }
|
||||||
}
|
}
|
||||||
v = step.to
|
v = step.to
|
||||||
guard++
|
guard++
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { GameState, SaveMeta, SnapshotMeta, Id } from '../types/domain'
|
import { GameState, SaveMeta, SnapshotMeta, Id } from '../types/domain'
|
||||||
import { CURRENT_SCHEMA, APP_ID, exportEnvelope, parseImportEnvelope, migrateIfNeeded } from './migrate'
|
import { CURRENT_SCHEMA, APP_ID, exportEnvelope, parseImportEnvelope, migrateIfNeeded, MigrationError } from './migrate'
|
||||||
|
|
||||||
const DB_PREFIX = 'cotyc-save-'
|
const DB_PREFIX = 'cotyc-save-'
|
||||||
let seqCounter = 0
|
let seqCounter = 0
|
||||||
@@ -89,7 +89,10 @@ export class SaveSlot {
|
|||||||
id ? [id] : []
|
id ? [id] : []
|
||||||
)
|
)
|
||||||
if (!rows || rows.length === 0) return null
|
if (!rows || rows.length === 0) return null
|
||||||
return JSON.parse(rows[0]!.data) as GameState
|
const state = JSON.parse(rows[0]!.data) as GameState
|
||||||
|
const r = migrateIfNeeded(state)
|
||||||
|
if (!r.ok) throw new MigrationError(r.error.code, r.error.version, r.error.message)
|
||||||
|
return r.state
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteSnapshot(id: string): Promise<void> {
|
async deleteSnapshot(id: string): Promise<void> {
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ export interface Character {
|
|||||||
aspiration?: string
|
aspiration?: string
|
||||||
tribDelayYear?: number
|
tribDelayYear?: number
|
||||||
tribPendingMonths?: number
|
tribPendingMonths?: number
|
||||||
|
/** 渡劫药力加成(破境丹等),渡劫结算后清零 */
|
||||||
|
tribBoost?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FamilyState {
|
export interface FamilyState {
|
||||||
|
|||||||
@@ -8,6 +8,37 @@ import { combatPowerOf } from '../../game/engine/runtime/Systems/combat'
|
|||||||
import { FORMATIONS, FormationId } from '../../game/data/formations'
|
import { FORMATIONS, FormationId } from '../../game/data/formations'
|
||||||
import { ModalShell } from './ModalShell'
|
import { ModalShell } from './ModalShell'
|
||||||
|
|
||||||
|
const RES_NAME: Record<string, string> = { lingcao: '灵草', lingkuang: '灵矿', beastcore: '兽核', stones: '灵石' }
|
||||||
|
|
||||||
|
function describeEff(eff: Record<string, never> | { [k: string]: unknown }): string {
|
||||||
|
const parts: string[] = []
|
||||||
|
const e = eff as { res?: Record<string, number>; rep?: number; relation?: Record<string, number>; addBuilding?: string; pillGain?: Record<string, number>; mission?: string; addTech?: string; raid?: { npcId: string }; trib?: { mode: string }; feisheng?: { stay: boolean }; techniqueChance?: number; artifactChance?: number; flag?: Record<string, unknown> }
|
||||||
|
if (e.res) {
|
||||||
|
for (const [k, v] of Object.entries(e.res)) {
|
||||||
|
const name = RES_NAME[k] ?? k
|
||||||
|
parts.push(v > 0 ? `${name}+${v}` : `${name}${v}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (e.rep) parts.push(`声望${e.rep > 0 ? '+' : ''}${e.rep}`)
|
||||||
|
if (e.relation) {
|
||||||
|
for (const v of Object.values(e.relation)) parts.push(v > 0 ? `交好+${v}` : `交恶${v}`)
|
||||||
|
}
|
||||||
|
if (e.addBuilding) parts.push('增筑' + e.addBuilding)
|
||||||
|
if (e.pillGain) {
|
||||||
|
for (const v of Object.values(e.pillGain)) parts.push(`得丹+${v}`)
|
||||||
|
}
|
||||||
|
if (e.addTech) parts.push('得功法')
|
||||||
|
if (e.mission) parts.push('触发任务')
|
||||||
|
if (e.techniqueChance) parts.push(`功法机率+${Math.round(e.techniqueChance * 100)}%`)
|
||||||
|
if (e.artifactChance) parts.push(`宝器机率+${Math.round(e.artifactChance * 100)}%`)
|
||||||
|
if (e.raid) parts.push('来敌犯境')
|
||||||
|
if (e.trib) {
|
||||||
|
parts.push(e.trib.mode === 'rash' ? '硬渡天劫' : e.trib.mode === 'guard' ? '护法渡劫' : '渡劫延一年')
|
||||||
|
}
|
||||||
|
if (e.feisheng) parts.push(e.feisheng.stay ? '留世不飞升' : '飞升离世')
|
||||||
|
return parts.join(' · ')
|
||||||
|
}
|
||||||
|
|
||||||
export function EventModal() {
|
export function EventModal() {
|
||||||
const pendingEventId = useGameStore((s) => s.pendingEventId)
|
const pendingEventId = useGameStore((s) => s.pendingEventId)
|
||||||
const pendingEventDef = useGameStore((s) => s.pendingEventDef)
|
const pendingEventDef = useGameStore((s) => s.pendingEventDef)
|
||||||
@@ -132,6 +163,7 @@ export function EventModal() {
|
|||||||
{o.label}
|
{o.label}
|
||||||
{isRaid && <span className="hint">点将迎战 · 可自选阵容</span>}
|
{isRaid && <span className="hint">点将迎战 · 可自选阵容</span>}
|
||||||
{!isRaid && o.hint && <span className="hint">{o.hint}</span>}
|
{!isRaid && o.hint && <span className="hint">{o.hint}</span>}
|
||||||
|
<span className="opt-eff">{describeEff(o.eff as never)}</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -252,6 +252,20 @@ export function MemberModal({ member }: { member: Character }) {
|
|||||||
|
|
||||||
{member.alive && (
|
{member.alive && (
|
||||||
<div className="mm-sec">
|
<div className="mm-sec">
|
||||||
|
<div className="dim" style={{ marginBottom: 6 }}>家主权柄</div>
|
||||||
|
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 10 }}>
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-primary"
|
||||||
|
disabled={member.id === w.state.family.headId}
|
||||||
|
title={member.id === w.state.family.headId ? '已在主位' : '册立为家主(名分即天命)'}
|
||||||
|
onClick={() => {
|
||||||
|
w.assignHead(member.id)
|
||||||
|
bump()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
册立家主
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div className="dim" style={{ marginBottom: 6 }}>职事(族中分工)</div>
|
<div className="dim" style={{ marginBottom: 6 }}>职事(族中分工)</div>
|
||||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -4,11 +4,17 @@ import { ModalShell } from './ModalShell'
|
|||||||
|
|
||||||
export function PaperModal() {
|
export function PaperModal() {
|
||||||
const report = useGameStore((s) => s.paperReport)
|
const report = useGameStore((s) => s.paperReport)
|
||||||
|
const world = useGameStore((s) => s.world)
|
||||||
const closePaper = useGameStore((s) => s.closePaper)
|
const closePaper = useGameStore((s) => s.closePaper)
|
||||||
if (!report) return null
|
if (!report) return null
|
||||||
|
|
||||||
const nets = report.nets
|
const nets = report.nets
|
||||||
const y = report.year
|
const y = report.year
|
||||||
|
const yearEntries = (world?.state.chronicle ?? [])
|
||||||
|
.filter((e) => e.year === y)
|
||||||
|
.sort((a, b) => a.month - b.month)
|
||||||
|
const majors = yearEntries.filter((e) => e.important).slice(0, 5)
|
||||||
|
const obits = yearEntries.filter((e) => e.category === 'death').slice(0, 5)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalShell
|
<ModalShell
|
||||||
@@ -25,6 +31,22 @@ export function PaperModal() {
|
|||||||
{report.deaths > report.births && <div className="bad">丁口在减 —— 传宗接代是头等大事</div>}
|
{report.deaths > report.births && <div className="bad">丁口在减 —— 传宗接代是头等大事</div>}
|
||||||
{report.births > report.deaths && <div className="good">丁口渐旺,香火有望。</div>}
|
{report.births > report.deaths && <div className="good">丁口渐旺,香火有望。</div>}
|
||||||
</div>
|
</div>
|
||||||
|
{majors.length > 0 && (
|
||||||
|
<div style={{ marginTop: 12, borderTop: '1px solid rgba(140,100,50,0.35)', paddingTop: 10 }}>
|
||||||
|
<div className="dim" style={{ marginBottom: 4 }}>家 族 要 闻</div>
|
||||||
|
{majors.map((e) => (
|
||||||
|
<div key={e.id} className="dim" style={{ fontSize: '0.9rem', lineHeight: 1.9 }}>· {e.month}月 {e.text}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{obits.length > 0 && (
|
||||||
|
<div style={{ marginTop: 8 }}>
|
||||||
|
<div className="dim" style={{ marginBottom: 4 }}>此 年 辞 世 者</div>
|
||||||
|
{obits.map((e) => (
|
||||||
|
<div key={e.id} className="dim" style={{ fontSize: '0.9rem', lineHeight: 1.9 }}>· {e.month}月 {e.text}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</ModalShell>
|
</ModalShell>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export default function ChroniclePanel() {
|
|||||||
void revision
|
void revision
|
||||||
const [filter, setFilter] = useState<string>('all')
|
const [filter, setFilter] = useState<string>('all')
|
||||||
const [battleId, setBattleId] = useState<string | null>(null)
|
const [battleId, setBattleId] = useState<string | null>(null)
|
||||||
|
const [draft, setDraft] = useState('')
|
||||||
const entries = useMemo(() => {
|
const entries = useMemo(() => {
|
||||||
if (!world) return []
|
if (!world) return []
|
||||||
const all = [...world.state.chronicle]
|
const all = [...world.state.chronicle]
|
||||||
@@ -64,7 +65,30 @@ export default function ChroniclePanel() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="help-text">
|
<div className="help-text">
|
||||||
族史一卷:由系统自动记下的家族大事。倒序排列,可回看任意一年的兴衰细节。
|
族史一卷:系统自动记下的家族大事,供族人题翰墨于旁。倒序排列,可回看任意一年的兴衰细节。
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: 8, marginBottom: 10 }}>
|
||||||
|
<input
|
||||||
|
className="ch-draft"
|
||||||
|
placeholder="题一笔族史……(如:某年宗祠大修,阖族祭祖)"
|
||||||
|
value={draft}
|
||||||
|
maxLength={80}
|
||||||
|
onChange={(e) => setDraft(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' && draft.trim()) {
|
||||||
|
world.chronicle('misc', draft.trim(), undefined, false)
|
||||||
|
setDraft('')
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="btn btn-sm"
|
||||||
|
disabled={!draft.trim()}
|
||||||
|
onClick={() => {
|
||||||
|
world.chronicle('misc', draft.trim(), undefined, false)
|
||||||
|
setDraft('')
|
||||||
|
}}
|
||||||
|
>落笔</button>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 10 }}>
|
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 10 }}>
|
||||||
<button className={`btn btn-sm ${filter === 'all' ? 'btn-primary' : ''}`} onClick={() => setFilter('all')}>全部</button>
|
<button className={`btn btn-sm ${filter === 'all' ? 'btn-primary' : ''}`} onClick={() => setFilter('all')}>全部</button>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { combatPowerOf } from '../../game/engine/runtime/Systems/combat'
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { describeRealm, MAJOR_ORDER } from '../../game/data/realms'
|
import { describeRealm, MAJOR_ORDER } from '../../game/data/realms'
|
||||||
import { FORMATIONS, FormationId } from '../../game/data/formations'
|
import { FORMATIONS, FormationId } from '../../game/data/formations'
|
||||||
|
import { WorldSim } from '../../game/engine/sim/WorldSim'
|
||||||
|
|
||||||
export default function ExpeditionPanel() {
|
export default function ExpeditionPanel() {
|
||||||
const world = useGameStore((s) => s.world)
|
const world = useGameStore((s) => s.world)
|
||||||
@@ -24,6 +25,14 @@ export default function ExpeditionPanel() {
|
|||||||
)
|
)
|
||||||
const def = selectedMissionDef ? missionById(selectedMissionDef) : null
|
const def = selectedMissionDef ? missionById(selectedMissionDef) : null
|
||||||
|
|
||||||
|
const qiOf = (id: string): number | null => {
|
||||||
|
try {
|
||||||
|
return Math.round(new WorldSim(w).secretQiOf(id))
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const toggle = (id: string) => {
|
const toggle = (id: string) => {
|
||||||
setSquad((old) => (old.includes(id) ? old.filter((x) => x !== id) : [...old, id]))
|
setSquad((old) => (old.includes(id) ? old.filter((x) => x !== id) : [...old, id]))
|
||||||
}
|
}
|
||||||
@@ -58,6 +67,11 @@ export default function ExpeditionPanel() {
|
|||||||
<div className="dim2" style={{ fontSize: '0.78rem' }}>{m.region} · 适合{describeRealm({ major: m.realmHint, minor: 0 })}</div>
|
<div className="dim2" style={{ fontSize: '0.78rem' }}>{m.region} · 适合{describeRealm({ major: m.realmHint, minor: 0 })}</div>
|
||||||
<div className="dim2" style={{ fontSize: '0.78rem' }}>{m.minMembers}-{m.maxMembers}人 · {m.stages.length}段</div>
|
<div className="dim2" style={{ fontSize: '0.78rem' }}>{m.minMembers}-{m.maxMembers}人 · {m.stages.length}段</div>
|
||||||
<div className="dim" style={{ fontSize: '0.8rem', marginTop: 3 }}>{m.desc}</div>
|
<div className="dim" style={{ fontSize: '0.8rem', marginTop: 3 }}>{m.desc}</div>
|
||||||
|
{qiOf(m.id) !== null && (
|
||||||
|
<div className="dim2" style={{ fontSize: '0.76rem', marginTop: 3 }}>
|
||||||
|
灵脉 {qiOf(m.id)}% (探索后-6,回复2/月;灵气越足所获越丰)
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { ITEMS } from '../../game/data/items'
|
|||||||
import { TECHNIQUES } from '../../game/data/techniques'
|
import { TECHNIQUES } from '../../game/data/techniques'
|
||||||
import { techniqueGradeName } from '../../game/data/realms'
|
import { techniqueGradeName } from '../../game/data/realms'
|
||||||
import { marketPrice, buyItem, sellItem, buyTechnique } from '../../game/engine/sim/Market'
|
import { marketPrice, buyItem, sellItem, buyTechnique } from '../../game/engine/sim/Market'
|
||||||
|
import { PILL_RECIPES, FORGE_RECIPES } from '../../game/data/items'
|
||||||
|
import { bonusOf } from '../../game/data/buildings'
|
||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
|
|
||||||
export default function MarketPanel() {
|
export default function MarketPanel() {
|
||||||
@@ -10,7 +12,7 @@ export default function MarketPanel() {
|
|||||||
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 [tab, setTab] = useState<'goods' | 'tech'>('goods')
|
const [tab, setTab] = useState<'goods' | 'tech' | 'craft'>('goods')
|
||||||
if (!world) return null
|
if (!world) return null
|
||||||
const w = world
|
const w = world
|
||||||
const fam = w.state.family
|
const fam = w.state.family
|
||||||
@@ -28,6 +30,7 @@ export default function MarketPanel() {
|
|||||||
<div className="tabs" style={{ padding: 0, background: 'none', border: 'none', marginBottom: 10 }}>
|
<div className="tabs" style={{ padding: 0, background: 'none', border: 'none', marginBottom: 10 }}>
|
||||||
<div className={`tab ${tab === 'goods' ? 'sel' : ''}`} onClick={() => setTab('goods')}>货栈</div>
|
<div className={`tab ${tab === 'goods' ? 'sel' : ''}`} onClick={() => setTab('goods')}>货栈</div>
|
||||||
<div className={`tab ${tab === 'tech' ? 'sel' : ''}`} onClick={() => setTab('tech')}>法帖</div>
|
<div className={`tab ${tab === 'tech' ? 'sel' : ''}`} onClick={() => setTab('tech')}>法帖</div>
|
||||||
|
<div className={`tab ${tab === 'craft' ? 'sel' : ''}`} onClick={() => setTab('craft')}>定制</div>
|
||||||
</div>
|
</div>
|
||||||
{tab === 'goods' && (
|
{tab === 'goods' && (
|
||||||
<>
|
<>
|
||||||
@@ -90,30 +93,7 @@ export default function MarketPanel() {
|
|||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
<tr>
|
|
||||||
<td><b><span className="s-icon">炼</span> 丹房炼制</b></td>
|
|
||||||
<td className="dim">灵草+辅矿 → 丹药(丹房等级越高成数越足)</td>
|
|
||||||
<td className="dim">{fam.buildings['danfang'] ? `${fam.buildings['danfang']}级丹房` : '未建丹房'}</td>
|
|
||||||
<td className="dim">灵草{inv['lingcao'] ?? 0}</td>
|
|
||||||
<td>
|
|
||||||
{fam.buildings['danfang'] && (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
className="btn btn-sm"
|
|
||||||
disabled={(inv['lingcao'] ?? 0) < 15 || fam.stones < 20}
|
|
||||||
title="需灵草15+灵石20"
|
|
||||||
onClick={() => { w.craftPill('qiyuan'); bump() }}
|
|
||||||
>炼气丹</button>{' '}
|
|
||||||
<button
|
|
||||||
className="btn btn-sm"
|
|
||||||
disabled={(inv['lingcao'] ?? 0) < 25 || (inv['beastcore'] ?? 0) < 4 || fam.stones < 60}
|
|
||||||
title="需灵草25+兽核4+灵石60"
|
|
||||||
onClick={() => { w.craftPill('ningyuan'); bump() }}
|
|
||||||
>炼元丹</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</>
|
</>
|
||||||
@@ -173,6 +153,72 @@ export default function MarketPanel() {
|
|||||||
</table>
|
</table>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{tab === 'craft' && (
|
||||||
|
<>
|
||||||
|
<div className="help-text">
|
||||||
|
丹房依「灵草+兽核+灵石」配比丹药,成数随丹房等级;破境丹需丹房三级。炼器以灵矿兽核+灵石相合,无建筑门槛,只看家底。
|
||||||
|
</div>
|
||||||
|
<h4 className="dim" style={{ margin: '8px 0 4px' }}>丹房炼制(当前 {fam.buildings['danfang'] ?? 0} 级 · 成率 {Math.round(bonusOf('danfang', 'craftChance', fam.buildings['danfang'] ?? 0) * 100)}%)</h4>
|
||||||
|
<table className="market-table">
|
||||||
|
<thead>
|
||||||
|
<tr><th>丹方</th><th>药性</th><th>配方</th><th>门槛</th><th></th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{PILL_RECIPES.map((r) => {
|
||||||
|
const lvl = fam.buildings['danfang'] ?? 0
|
||||||
|
const enough = (inv['lingcao'] ?? 0) >= r.lingcao && (inv['beastcore'] ?? 0) >= r.beastcore && fam.stones >= r.stones
|
||||||
|
return (
|
||||||
|
<tr key={r.output}>
|
||||||
|
<td><b><span className="s-icon">丹</span> {r.name}</b></td>
|
||||||
|
<td className="dim">{r.desc}</td>
|
||||||
|
<td className="dim">草{r.lingcao}·核{r.beastcore}·灵石{r.stones}</td>
|
||||||
|
<td className="dim">{r.danfangLevel}级丹房</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
className="btn btn-sm"
|
||||||
|
disabled={lvl < r.danfangLevel || !enough}
|
||||||
|
title={lvl < r.danfangLevel ? `丹房需升到 ${r.danfangLevel} 级` : enough ? '炼制(失败折半退料)' : '材料不足'}
|
||||||
|
onClick={() => { w.craftPill(r.output.replace('pill-', '') as 'qiyuan' | 'ningyuan' | 'pojing'); bump() }}
|
||||||
|
>炼</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<h4 className="dim" style={{ margin: '14px 0 4px' }}>铸器坊(灵矿+兽核+灵石 → 兵刃)</h4>
|
||||||
|
<table className="market-table">
|
||||||
|
<thead>
|
||||||
|
<tr><th>兵刃</th><th>妙用</th><th>配方</th><th></th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{FORGE_RECIPES.map((r) => {
|
||||||
|
const enough = (inv['lingkuang'] ?? 0) >= r.lingkuang && (inv['beastcore'] ?? 0) >= r.beastcore && fam.stones >= r.stones
|
||||||
|
return (
|
||||||
|
<tr key={r.output}>
|
||||||
|
<td><b><span className="s-icon">器</span> {r.name}</b></td>
|
||||||
|
<td className="dim">{r.desc}</td>
|
||||||
|
<td className="dim">矿{r.lingkuang}·核{r.beastcore}·灵石{r.stones}</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
className="btn btn-sm"
|
||||||
|
disabled={!enough}
|
||||||
|
title={enough ? '淬火铸成' : '材料不足'}
|
||||||
|
onClick={() => { w.forgeArtifact(r.output as 'weapon-qi' | 'weapon-ling' | 'weapon-fa'); bump() }}
|
||||||
|
>铸</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{(fam.buildings['danfang'] ?? 0) > 0 && (
|
||||||
|
<div className="dim2" style={{ marginTop: 8 }}>
|
||||||
|
破境丹亦可等拍卖会落槌(需要时留神百年拍卖)。
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<div className="dim2" style={{ marginTop: 10 }}>
|
<div className="dim2" style={{ marginTop: 10 }}>
|
||||||
灵矿不足时可上坊市民间收购;妖丹于探秘与灵兽园中可得。
|
灵矿不足时可上坊市民间收购;妖丹于探秘与灵兽园中可得。
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ export default function SettingsPanel() {
|
|||||||
· 「外交」与四邻结好联姻;仇雠之族隔岁来犯,打得赢名望大涨,打不赢蚀钱伤丁。<br />
|
· 「外交」与四邻结好联姻;仇雠之族隔岁来犯,打得赢名望大涨,打不赢蚀钱伤丁。<br />
|
||||||
· 「史书」自动记述繁华与凋零——百年之后,后人翻开这一卷家族志,见代代薪火、历历雪泥。
|
· 「史书」自动记述繁华与凋零——百年之后,后人翻开这一卷家族志,见代代薪火、历历雪泥。
|
||||||
</div>
|
</div>
|
||||||
<div className="dim2" style={{ marginTop: 8 }}>版本 0.1.15 · Chronicle of the Immortal Clan</div>
|
<div className="dim2" style={{ marginTop: 8 }}>版本 0.1.16 · Chronicle of the Immortal Clan</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import { useGameStore } from '../store'
|
||||||
|
import { npcById } from '../../game/data/npcs'
|
||||||
|
import { MAJOR_NAMES } from '../../game/data/realms'
|
||||||
|
import { POOL_BASE } from '../../game/engine/sim/worldsim-data'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
const RES_LABEL: Record<string, string> = { lingcao: '灵草', lingkuang: '灵矿', beastcore: '兽核' }
|
||||||
|
const REALM_IDX = ['mortal', 'qi', 'foundation', 'core', 'nascent', 'spirit'] as const
|
||||||
|
|
||||||
|
export default function WorldPanel() {
|
||||||
|
const world = useGameStore((s) => s.world)
|
||||||
|
const bump = useGameStore((s) => s.bump)
|
||||||
|
const revision = useGameStore((s) => s.revision)
|
||||||
|
void revision
|
||||||
|
const [view, setView] = useState<string | null>(null)
|
||||||
|
if (!world) return null
|
||||||
|
const w = world
|
||||||
|
const ws = w.state.worldSim
|
||||||
|
if (!ws) return <div className="help-text">天机未显……(世界演化尚未展开)</div>
|
||||||
|
|
||||||
|
const tide = ws.tide ?? 0.5
|
||||||
|
const tideLabel = tide > 0.9 ? '灵涨' : tide < 0.7 ? '灵衰' : '汐平'
|
||||||
|
const pool = ws.marketPool ?? {}
|
||||||
|
const news = [...(ws.newsFeed ?? [])].slice().reverse()
|
||||||
|
const calamity = ws.calamity
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="help-text">
|
||||||
|
天下时局尽览:行市起落、宗主更替、灾年吉凶——皆随世界自演化而来。
|
||||||
|
结盟后岁差不降,每年另有盟利奉上(需关系≥40)。
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', marginBottom: 10 }}>
|
||||||
|
<span className="tag gold-t">灵潮 {tideLabel}({Math.round(tide * 100)}%)</span>
|
||||||
|
{calamity
|
||||||
|
? <span className="tag" style={{ borderColor: '#a33', color: '#e8a08a' }}>灾年·{calamity}(灵植减产,市价将行)</span>
|
||||||
|
: <span className="tag">风调雨顺</span>}
|
||||||
|
{Object.entries(pool).map(([k, v]) => {
|
||||||
|
const base = POOL_BASE[k as keyof typeof POOL_BASE] ?? 100
|
||||||
|
const r = ((v as number) / base) * 100
|
||||||
|
const cls = r > 115 ? 'bad' : r < 85 ? 'good' : 'dim'
|
||||||
|
const dir = r > 105 ? '↑' : r < 95 ? '↓' : '→'
|
||||||
|
return (
|
||||||
|
<span key={k} className="tag"><span className={cls}>{RES_LABEL[k] ?? k}{dir}{Math.round(r)}%</span></span>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
|
||||||
|
<div>
|
||||||
|
<h4 className="dim" style={{ margin: '4px 0 6px' }}>天下快讯</h4>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, maxHeight: 380, overflowY: 'auto' }}>
|
||||||
|
{news.length === 0 && <div className="dim2">风平浪静,尚无消息。</div>}
|
||||||
|
{news.map((row, i) => (
|
||||||
|
<div key={i} className="ch-item" style={{ padding: '5px 8px' }}>
|
||||||
|
<span className="ch-month">{row.year}年{row.month}月</span>
|
||||||
|
<span>{row.text}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="dim" style={{ margin: '4px 0 6px' }}>群雄谱(点开宗主详情)</h4>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||||
|
{Object.values(w.state.npcFamilies).map((npc) => {
|
||||||
|
const def = npcById(npc.id)
|
||||||
|
const dyn = (ws.npcDyn ?? {})[npc.id]
|
||||||
|
const open = view === npc.id
|
||||||
|
return (
|
||||||
|
<div key={npc.id} style={{ border: '1px solid rgba(140,100,50,0.3)', borderRadius: 6, padding: '7px 10px', background: 'rgba(18,14,9,0.4)' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }} onClick={() => setView(open ? null : npc.id)}>
|
||||||
|
<span>
|
||||||
|
<b>{npc.name}</b> <span className="dim">{def.style}</span>
|
||||||
|
{npc.allied && <span className="tag gold-t" style={{ marginLeft: 6 }}>盟</span>}
|
||||||
|
</span>
|
||||||
|
<span className="dim">势力{npc.power} · 关系{npc.relation}</span>
|
||||||
|
</div>
|
||||||
|
{open && (
|
||||||
|
<div style={{ marginTop: 6, fontSize: '0.86rem', color: '#c8b285', borderTop: '1px solid rgba(140,100,50,0.25)', paddingTop: 6 }}>
|
||||||
|
<div className="dim">宗主:{dyn?.leaderName ?? '未知'}({dyn ? MAJOR_NAMES[REALM_IDX[dyn.leaderRealmIdx] ?? 'qi'] : ''} · {dyn?.leaderAge ?? '?'}岁)</div>
|
||||||
|
<div className="dim2" style={{ margin: '2px 0 6px' }}>{def.desc}</div>
|
||||||
|
<div className="dim">新近之事:{dyn?.lastEvent ? `${dyn.lastEvent}(${dyn.lastEventYear ?? '?'}年)` : '暂无'}</div>
|
||||||
|
{npc.alliedSinceYear && <div className="dim">同盟自 {npc.alliedSinceYear} 年</div>}
|
||||||
|
<div style={{ marginTop: 6, display: 'flex', gap: 6 }}>
|
||||||
|
<button
|
||||||
|
className="btn btn-sm"
|
||||||
|
disabled={npc.allied || npc.relation < 40}
|
||||||
|
title={npc.relation < 40 ? '关系不足(需40)' : ''}
|
||||||
|
onClick={() => { w.setAlliance(npc.id, true); bump() }}
|
||||||
|
>缔盟</button>
|
||||||
|
{npc.allied && (
|
||||||
|
<button
|
||||||
|
className="btn btn-sm"
|
||||||
|
onClick={() => { w.setAlliance(npc.id, false); bump() }}
|
||||||
|
>解盟(关系-20)</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import MarketPanel from '../panels/MarketPanel'
|
|||||||
import DiplomacyPanel from '../panels/DiplomacyPanel'
|
import DiplomacyPanel from '../panels/DiplomacyPanel'
|
||||||
import ExpeditionPanel from '../panels/ExpeditionPanel'
|
import ExpeditionPanel from '../panels/ExpeditionPanel'
|
||||||
import ChroniclePanel from '../panels/ChroniclePanel'
|
import ChroniclePanel from '../panels/ChroniclePanel'
|
||||||
|
import WorldPanel from '../panels/WorldPanel'
|
||||||
import SettingsPanel from '../panels/SettingsPanel'
|
import SettingsPanel from '../panels/SettingsPanel'
|
||||||
import { LogFeed } from '../components/LogFeed'
|
import { LogFeed } from '../components/LogFeed'
|
||||||
import { UrgentBadges } from '../components/UrgentBadges'
|
import { UrgentBadges } from '../components/UrgentBadges'
|
||||||
@@ -28,6 +29,7 @@ const TABS: { id: PanelId; label: string }[] = [
|
|||||||
{ id: 'expedition', label: '探秘' },
|
{ id: 'expedition', label: '探秘' },
|
||||||
{ id: 'chronicle', label: '史书' },
|
{ id: 'chronicle', label: '史书' },
|
||||||
{ id: 'legacy', label: '春秋原' },
|
{ id: 'legacy', label: '春秋原' },
|
||||||
|
{ id: 'world', label: '天下' },
|
||||||
{ id: 'settings', label: '设置' }
|
{ id: 'settings', label: '设置' }
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -113,6 +115,7 @@ export default function GameScreen() {
|
|||||||
{panel === 'expedition' && <ExpeditionPanel />}
|
{panel === 'expedition' && <ExpeditionPanel />}
|
||||||
{panel === 'chronicle' && <ChroniclePanel />}
|
{panel === 'chronicle' && <ChroniclePanel />}
|
||||||
{panel === 'legacy' && <LegacyPanel />}
|
{panel === 'legacy' && <LegacyPanel />}
|
||||||
|
{panel === 'world' && <WorldPanel />}
|
||||||
{panel === 'settings' && <SettingsPanel />}
|
{panel === 'settings' && <SettingsPanel />}
|
||||||
{gameOverReason && (
|
{gameOverReason && (
|
||||||
<div className="gameover-banner">
|
<div className="gameover-banner">
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { setSoundEnabled, sPaper, sGood, sBad, sWar, sBell, sTick } from './soun
|
|||||||
import { seasonOf } from '../game/data/season'
|
import { seasonOf } from '../game/data/season'
|
||||||
|
|
||||||
export type Screen = 'boot' | 'newgame' | 'game'
|
export type Screen = 'boot' | 'newgame' | 'game'
|
||||||
export type PanelId = 'family' | 'genealogy' | 'territory' | 'market' | 'diplomacy' | 'expedition' | 'chronicle' | 'legacy' | 'settings'
|
export type PanelId = 'family' | 'genealogy' | 'territory' | 'market' | 'diplomacy' | 'expedition' | 'chronicle' | 'legacy' | 'world' | 'settings'
|
||||||
|
|
||||||
export interface GameStore {
|
export interface GameStore {
|
||||||
screen: Screen
|
screen: Screen
|
||||||
@@ -150,7 +150,8 @@ export const useGameStore = create<GameStore>((set, get) => ({
|
|||||||
set({ world, engine, slot, screen: 'game', panel: 'family', logFeed: [], battleView: undefined, pendingEventId: undefined, pendingEventDef: undefined, revision: 1, speed: 0, gameOverReason: undefined, paperReport: undefined, toast: undefined, selectedMemberId: undefined, selectedMissionDef: undefined })
|
set({ world, engine, slot, screen: 'game', panel: 'family', logFeed: [], battleView: undefined, pendingEventId: undefined, pendingEventDef: undefined, revision: 1, speed: 0, gameOverReason: undefined, paperReport: undefined, toast: undefined, selectedMemberId: undefined, selectedMissionDef: undefined })
|
||||||
const st = get()
|
const st = get()
|
||||||
st.world?.out.push(makeBus(st))
|
st.world?.out.push(makeBus(st))
|
||||||
const facade = st.facade ?? new GameFacade(world, slot)
|
// 新档永远建新门面(旧 facade 绑定旧 world,复用会回放错误目标)
|
||||||
|
const facade = new GameFacade(world, slot)
|
||||||
set({ facade })
|
set({ facade })
|
||||||
await st.saveNow('开局')
|
await st.saveNow('开局')
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1355,6 +1355,29 @@ html[data-blade] .battle-lines {
|
|||||||
.modal-title {
|
.modal-title {
|
||||||
padding-right: 44px;
|
padding-right: 44px;
|
||||||
}
|
}
|
||||||
|
.opt-eff {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.74rem;
|
||||||
|
color: rgba(150, 132, 96, 0.9);
|
||||||
|
margin-top: 3px;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
.ch-draft {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 200px;
|
||||||
|
background: rgba(18, 14, 9, 0.75);
|
||||||
|
border: 1px solid rgba(140, 100, 50, 0.5);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #e8d5a4;
|
||||||
|
padding: 7px 10px;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.ch-draft:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--vermilion);
|
||||||
|
box-shadow: 0 0 0 2px rgba(200, 60, 30, 0.18);
|
||||||
|
}
|
||||||
.modal-opts {
|
.modal-opts {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ describe('aspirations 志向', () => {
|
|||||||
it('耕读志向提升灵田产出', () => {
|
it('耕读志向提升灵田产出', () => {
|
||||||
const w = baseWorld('asp-d')
|
const w = baseWorld('asp-d')
|
||||||
w.state.family.buildings = { lingtian: 1 }
|
w.state.family.buildings = { lingtian: 1 }
|
||||||
|
for (const c of Object.values(w.state.members)) c.realm = { major: 'mortal', minor: 0 }
|
||||||
|
w.state.worldSim = undefined as never
|
||||||
w.state.members['x3'].aspiration = 'geng'
|
w.state.members['x3'].aspiration = 'geng'
|
||||||
const c0 = w.state.family.inventory['lingcao'] ?? 0
|
const c0 = w.state.family.inventory['lingcao'] ?? 0
|
||||||
w.advanceMonth()
|
w.advanceMonth()
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ describe('审计回归:P0 修复固化', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('防御性修补后金钟罩不变(行为等价确认)', () => {
|
it('防御性修补后金钟罩不变(行为等价确认)', () => {
|
||||||
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('a977654a')
|
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('9531969e')
|
||||||
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('ee501983')
|
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('aeefac96')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
+4
-4
@@ -7,11 +7,11 @@ import { World } from '../src/renderer/game/engine/runtime/World'
|
|||||||
* 任何改动(重构日程/调平衡/加系统)若改变了确定性序列或结果,此测试立刻报红。
|
* 任何改动(重构日程/调平衡/加系统)若改变了确定性序列或结果,此测试立刻报红。
|
||||||
* 更新规则:仅当**有意**变更序列逻辑时,三枚 seed 指纹同版更新并注明原因。
|
* 更新规则:仅当**有意**变更序列逻辑时,三枚 seed 指纹同版更新并注明原因。
|
||||||
*/
|
*/
|
||||||
// 0.1.15 深度审计最终基线:worldsim 修复(换代年首化/power年度/relation去漂/快讯节流/灵脉初始化)后固化。
|
// 0.1.16 资源闭环基线:修为耗草/炼丹概率化/铸器/灵脉折算/NPC战力/灾变落效/渡劫药力/结盟后固化。
|
||||||
const GOLDEN: Record<string, Record<number, string>> = {
|
const GOLDEN: Record<string, Record<number, string>> = {
|
||||||
'bell-seed-1': { 560: 'a977654a', 1200: 'faefa00a', 2160: '5b7b375b' },
|
'bell-seed-1': { 560: '9531969e', 1200: 'cdd70056', 2160: '7e33a89d' },
|
||||||
'bell-seed-2': { 560: '1c41881c', 1200: 'a06af06c', 2160: '142d04c3' },
|
'bell-seed-2': { 560: 'f2ecc3e1', 1200: 'dab62832', 2160: '03c02233' },
|
||||||
'bell-seed-3': { 560: 'ee501983', 1200: '2a899a90', 2160: '0e490e5c' }
|
'bell-seed-3': { 560: 'aeefac96', 1200: '9eda1d0f', 2160: 'b3bcf822' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const TIERS = [
|
const TIERS = [
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||||
|
import { PILL_RECIPES, FORGE_RECIPES, pillRecipeByOutput, forgeRecipeByOutput } from '../src/renderer/game/data/items'
|
||||||
|
import { bonusOf } from '../src/renderer/game/data/buildings'
|
||||||
|
import { MAJORS } from '../src/renderer/game/data/realms'
|
||||||
|
import { monthlyRate } from '../src/renderer/game/engine/runtime/Systems/cultivation'
|
||||||
|
|
||||||
|
describe('0.1.16 资源闭环', () => {
|
||||||
|
it('修为耗草:有草速修,无草缓修(0.6底)', () => {
|
||||||
|
const w = World.create({ seed: 'l1', surname: '叶', familyName: '叶家', motto: 'm', difficulty: 'normal' })
|
||||||
|
const c = Object.values(w.state.members)[0]!
|
||||||
|
c.realm = { major: 'qi', minor: 2 }
|
||||||
|
createWorldWithHerbs(w, 10)
|
||||||
|
const fast = monthlyRate(w, c, 1)
|
||||||
|
const slow = monthlyRate(w, c, 0.6)
|
||||||
|
expect(fast).toBeGreaterThan(slow * 1.5)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('丹方单源:craftPill 按配方消耗且门槛生效', () => {
|
||||||
|
const w = World.create({ seed: 'l2', surname: '李', familyName: '李家', motto: 'm', difficulty: 'normal' })
|
||||||
|
w.state.family.buildings = { danfang: 1 }
|
||||||
|
const fam = w.state.family
|
||||||
|
fam.inventory['lingcao'] = 100
|
||||||
|
fam.inventory['beastcore'] = 10
|
||||||
|
fam.stones = 1000
|
||||||
|
const r = pillRecipeByOutput('pill-ningyuan')!
|
||||||
|
expect(r.danfangLevel).toBe(2)
|
||||||
|
expect(w.craftPill('ningyuan')).toBe(false) // 丹房1级不足
|
||||||
|
expect(fam.inventory['lingcao']).toBe(100)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('铸器配方:材料足够即成,入库', () => {
|
||||||
|
const w = World.create({ seed: 'l3', surname: '韩', familyName: '韩家', motto: 'm', difficulty: 'normal' })
|
||||||
|
const fam = w.state.family
|
||||||
|
fam.inventory['lingkuang'] = 100
|
||||||
|
fam.inventory['beastcore'] = 50
|
||||||
|
fam.stones = 5000
|
||||||
|
const r = forgeRecipeByOutput('weapon-ling')!
|
||||||
|
expect(w.forgeArtifact('weapon-ling')).toBe(true)
|
||||||
|
expect(fam.inventory['weapon-ling']).toBe(1)
|
||||||
|
expect(fam.inventory['lingkuang']).toBe(100 - r.lingkuang)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('bonusOf 表达式安全:非法符号返回0', () => {
|
||||||
|
expect(bonusOf('danfang', 'craftChance', 2)).toBeCloseTo(0.7)
|
||||||
|
expect(bonusOf('lingtian', 'nonexist', 1)).toBe(0)
|
||||||
|
expect(bonusOf('danfang', 'craftChance', 1)).toBeCloseTo(0.6)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('tribBoost:破境丹大境界走渡劫而非直破', () => {
|
||||||
|
const w = World.create({ seed: 'l4', surname: '齐', familyName: '齐家', motto: 'm', difficulty: 'normal' })
|
||||||
|
const c = Object.values(w.state.members)[1]!
|
||||||
|
c.realm = { major: 'foundation', minor: (MAJORS.foundation.minorLayers - 1) }
|
||||||
|
c.realmProgress = 100
|
||||||
|
w.state.family.inventory['pill-pojing'] = 1
|
||||||
|
w.state.family.buildings = { danfang: 1 }
|
||||||
|
w.takePill(c.id, 'pill-pojing')
|
||||||
|
expect(c.tribBoost).toBeGreaterThan(0)
|
||||||
|
expect(w.state.pendingEvent).toBeTruthy() // 渡劫事件挂起
|
||||||
|
})
|
||||||
|
|
||||||
|
it('setAlliance:关系不足拒绝,足够成盟', () => {
|
||||||
|
const w = World.create({ seed: 'l5', surname: '荀', familyName: '荀家', motto: 'm', difficulty: 'normal' })
|
||||||
|
const npcId = Object.keys(w.state.npcFamilies)[0]!
|
||||||
|
w.state.npcFamilies[npcId].relation = 10
|
||||||
|
expect(w.setAlliance(npcId, true)).toBe(false)
|
||||||
|
w.state.npcFamilies[npcId].relation = 60
|
||||||
|
expect(w.setAlliance(npcId, true)).toBe(true)
|
||||||
|
expect(w.state.npcFamilies[npcId].allied).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('劫掠强度随NPC实力浮动(早期远弱于后期)', () => {
|
||||||
|
const w = World.create({ seed: 'l6', surname: '苗', familyName: '苗家', motto: 'm', difficulty: 'normal' })
|
||||||
|
const npcId = Object.keys(w.state.npcFamilies)[0]!
|
||||||
|
const w1 = World.create({ seed: 'l6', surname: '苗', familyName: '苗家', motto: 'm', difficulty: 'normal' })
|
||||||
|
w.state.npcFamilies[npcId].power = 60
|
||||||
|
w1.state.npcFamilies[npcId].power = 240
|
||||||
|
const team = [w.state.members['x1']!]
|
||||||
|
expect(combatStrengthOf(w, npcId)).toBeLessThan(combatStrengthOf(w1, npcId))
|
||||||
|
void team
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function createWorldWithHerbs(w: World, n: number): void {
|
||||||
|
w.state.family.inventory['lingcao'] = n
|
||||||
|
}
|
||||||
|
|
||||||
|
function combatStrengthOf(w: World, npcId: string): number {
|
||||||
|
const npc = w.state.npcFamilies[npcId]
|
||||||
|
return Math.min(1.8, Math.max(0.5, 0.5 + ((npc.power ?? 60) / 120) * 0.5))
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ describe('economy 经济系统', () => {
|
|||||||
it('灵田月产随等级线性增长', () => {
|
it('灵田月产随等级线性增长', () => {
|
||||||
const w = World.create({ seed: 'eco-a', surname: '简', familyName: '简家', motto: 'm', difficulty: 'normal' })
|
const w = World.create({ seed: 'eco-a', surname: '简', familyName: '简家', motto: 'm', difficulty: 'normal' })
|
||||||
w.state.family.buildings = { lingtian: 1 }
|
w.state.family.buildings = { lingtian: 1 }
|
||||||
|
for (const c of Object.values(w.state.members)) c.realm = { major: 'mortal', minor: 0 }
|
||||||
const c0 = w.state.family.inventory['lingcao'] ?? 0
|
const c0 = w.state.family.inventory['lingcao'] ?? 0
|
||||||
w.advanceMonth() // month 2 = 春季,田地有±季相但至少≥9
|
w.advanceMonth() // month 2 = 春季,田地有±季相但至少≥9
|
||||||
const c1 = w.state.family.inventory['lingcao'] ?? 0
|
const c1 = w.state.family.inventory['lingcao'] ?? 0
|
||||||
@@ -73,7 +74,7 @@ describe('economy 经济系统', () => {
|
|||||||
|
|
||||||
it('丹房炼制消耗正确产出丹药', () => {
|
it('丹房炼制消耗正确产出丹药', () => {
|
||||||
const w = World.create({ seed: 'eco-h', surname: '宋', familyName: '宋家', motto: 'm', difficulty: 'normal' })
|
const w = World.create({ seed: 'eco-h', surname: '宋', familyName: '宋家', motto: 'm', difficulty: 'normal' })
|
||||||
w.state.family.buildings = { danfang: 1 }
|
w.state.family.buildings = { danfang: 5 }
|
||||||
const fam = w.state.family
|
const fam = w.state.family
|
||||||
fam.inventory['lingcao'] = 100
|
fam.inventory['lingcao'] = 100
|
||||||
fam.inventory['beastcore'] = 10
|
fam.inventory['beastcore'] = 10
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ describe('CapabilityRegistry 能力卡', () => {
|
|||||||
const c0 = w.state.family.inventory['lingcao'] ?? 0
|
const c0 = w.state.family.inventory['lingcao'] ?? 0
|
||||||
w.toggleSystem('production')
|
w.toggleSystem('production')
|
||||||
for (let i = 0; i < 6; i++) w.advanceMonth()
|
for (let i = 0; i < 6; i++) w.advanceMonth()
|
||||||
expect(w.state.family.inventory['lingcao'] ?? 0).toBe(c0)
|
expect(w.state.family.inventory['lingcao'] ?? 0).toBeLessThanOrEqual(c0)
|
||||||
w.toggleSystem('production')
|
w.toggleSystem('production')
|
||||||
for (let i = 0; i < 6; i++) w.advanceMonth()
|
for (let i = 0; i < 6; i++) w.advanceMonth()
|
||||||
expect(w.state.family.inventory['lingcao'] ?? 0).toBeGreaterThan(c0)
|
expect(w.state.family.inventory['lingcao'] ?? 0).toBeGreaterThan(c0)
|
||||||
@@ -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.15')
|
expect(info.version).toContain('0.1.16')
|
||||||
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('a977654a')
|
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('9531969e')
|
||||||
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('ee501983')
|
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('aeefac96')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -80,9 +80,9 @@ describe('PluginCore 插件协议', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('默认管线金钟罩不受插件层影响', () => {
|
it('默认管线金钟罩不受插件层影响', () => {
|
||||||
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('a977654a')
|
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('9531969e')
|
||||||
expect(stateFingerprint(longRun('bell-seed-2').state)).toBe('1c41881c')
|
expect(stateFingerprint(longRun('bell-seed-2').state)).toBe('f2ecc3e1')
|
||||||
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('ee501983')
|
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('aeefac96')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('facade 插件查询与 about.plugins', () => {
|
it('facade 插件查询与 about.plugins', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user