v0.1.0: 仙途家族志 · Chronicle of the Immortal Clan

家族模拟器首版:修仙/经营/战斗/外交/叙事全套系统
- Electron + React + TS + MetonaSqlark(aria+OPFS)
- 水墨中国风 UI
- 引擎种子随机、确定性可回放
- 19 项单元测试、端到端冒烟验证
This commit is contained in:
2026-08-23 00:04:16 +08:00
commit adb22f0558
69 changed files with 14305 additions and 0 deletions
+97
View File
@@ -0,0 +1,97 @@
export interface BuildingDef {
id: string
name: string
icon: string
desc: string
kind: 'produce' | 'function'
maxLevel: number
produceTable?: (level: number) => { stone?: number; lingcao?: number; lingkuang?: number; beastcore?: number }
upgradeCost: (level: number) => { stones: number; lingkuang: number }
extra?: Record<string, string>
}
export const BUILDINGS: Record<string, BuildingDef> = {
lingtian: {
id: 'lingtian', name: '灵田', icon: '田',
desc: '种植灵草,每月产出稳定。',
kind: 'produce', maxLevel: 5,
produceTable: (l) => ({ lingcao: 10 * l }),
upgradeCost: (l) => ({ stones: 80 * Math.pow(1.7, l - 1), lingkuang: 12 * l })
},
yaoyuan: {
id: 'yaoyuan', name: '药园', icon: '药',
desc: '培育稀有药草以炼丹药。',
kind: 'produce', maxLevel: 5,
produceTable: (l) => ({ lingcao: 5 * l, beastcore: l >= 3 ? 1 : 0 }),
upgradeCost: (l) => ({ stones: 110 * Math.pow(1.7, l - 1), lingkuang: 15 * l })
},
lingkuang: {
id: 'lingkuang', name: '灵矿', icon: '矿',
desc: '开采灵石矿脉,供给炼器布阵。',
kind: 'produce', maxLevel: 5,
produceTable: (l) => ({ lingkuang: 8 * l }),
upgradeCost: (l) => ({ stones: 130 * Math.pow(1.7, l - 1), lingkuang: 10 * l })
},
fangshi: {
id: 'fangshi', name: '坊市', icon: '市',
desc: '设摊售货,每月进账灵石。',
kind: 'produce', maxLevel: 5,
produceTable: (l) => ({ stone: 55 * l }),
upgradeCost: (l) => ({ stones: 150 * Math.pow(1.7, l - 1), lingkuang: 20 * l })
},
danfang: {
id: 'danfang', name: '丹房', icon: '丹',
desc: '炼制丹药,等级越高成丹率越高。',
kind: 'function', maxLevel: 5,
upgradeCost: (l) => ({ stones: 170 * Math.pow(1.7, l - 1), lingkuang: 18 * l }),
extra: { craftChance: '0.5 + 0.1*L' }
},
cangshu: {
id: 'cangshu', name: '藏书阁', icon: '书',
desc: '收藏功法典籍,等级越高越能寻得宝典。',
kind: 'function', maxLevel: 5,
upgradeCost: (l) => ({ stones: 160 * Math.pow(1.7, l - 1), lingkuang: 12 * l }),
extra: { unlockGrade: '1 + L' }
},
juling: {
id: 'juling', name: '聚灵阵', icon: '阵',
desc: '汇聚天地灵气,全族修炼加成。',
kind: 'function', maxLevel: 5,
upgradeCost: (l) => ({ stones: 200 * Math.pow(1.7, l - 1), lingkuang: 30 * l }),
extra: { expBonus: '0.05 * L' }
},
yanwu: {
id: 'yanwu', name: '演武场', icon: '武',
desc: '操练武艺,家族战力加成。',
kind: 'function', maxLevel: 5,
upgradeCost: (l) => ({ stones: 140 * Math.pow(1.7, l - 1), lingkuang: 25 * l }),
extra: { powerBonus: '0.04 * L' }
},
dongfu: {
id: 'dongfu', name: '洞府', icon: '府',
desc: '闭关修行之所,闭关者修为加成。',
kind: 'function', maxLevel: 5,
upgradeCost: (l) => ({ stones: 220 * Math.pow(1.7, l - 1), lingkuang: 35 * l }),
extra: { expBonus: '0.08 * L' }
},
zongci: {
id: 'zongci', name: '宗祠', icon: '祠',
desc: '供奉先祖,家族声望与生育兴旺。',
kind: 'function', maxLevel: 5,
upgradeCost: (l) => ({ stones: 120 * Math.pow(1.7, l - 1), lingkuang: 8 * l }),
extra: { repBonus: '0.3 * L /年' }
},
lingshou: {
id: 'lingshou', name: '灵兽园', icon: '兽',
desc: '豢养灵兽,战阵之助,偶得兽核。',
kind: 'function', maxLevel: 5,
upgradeCost: (l) => ({ stones: 190 * Math.pow(1.7, l - 1), lingkuang: 28 * l }),
extra: { powerBonus: '0.05 * L' }
}
}
export const BUILDING_IDS = Object.keys(BUILDINGS)
export function buildingById(id: string): BuildingDef {
return BUILDINGS[id]
}
+27
View File
@@ -0,0 +1,27 @@
import { Element } from '../types/domain'
export const ELEMENT_LIST: Element[] = ['金', '木', '水', '火', '土']
export const ROOT_GRADE_NAMES = ['伪灵根', '凡品灵根', '玄品灵根', '真品灵根', '天品灵根', '仙品灵根']
export const ROOT_GRADE_COLORS = ['#8a8078', '#9cb27c', '#7fa7c9', '#b08cd9', '#e6b84c', '#e05a5a']
export interface RootGradeDef {
name: string
expBonus: number
drawWeight: number
}
export const ROOT_GRADES: Record<number, RootGradeDef> = {
0: { name: '伪灵根', expBonus: 0.5, drawWeight: 14 },
1: { name: '凡品灵根', expBonus: 0.8, drawWeight: 38 },
2: { name: '玄品灵根', expBonus: 1.0, drawWeight: 30 },
3: { name: '真品灵根', expBonus: 1.3, drawWeight: 12 },
4: { name: '天品灵根', expBonus: 1.7, drawWeight: 5 },
5: { name: '仙品灵根', expBonus: 2.2, drawWeight: 1.5 }
}
export function describeRoots(roots: { grade: number; primary: Element; secondary: Element[] }): string {
const parts = [roots.primary, ...roots.secondary].join('')
return `${ROOT_GRADE_NAMES[roots.grade] ?? '伪灵根'}(${parts})`
}
+305
View File
@@ -0,0 +1,305 @@
export interface Cond {
all?: Cond[]
any?: Cond[]
not?: Cond
minYear?: number
minGeneration?: number
minHeadRealm?: string
minBuilding?: { id: string; level: number }
minRep?: number
maxRep?: number
minResource?: { id: string; n: number }
minAdult?: number
maxAdult?: number
minMembers?: number
eligibleAdult?: number
relation?: { npcId: string; gt?: number; lt?: number }
flag?: { key: string; eq: number | boolean | string }
hasMeditation?: boolean
noWarForYears?: number
minTechCount?: number
}
export interface MemberEffect {
by: 'exp' | 'wound' | 'heal' | 'breakthrough' | 'fatal' | 'repGain' | 'inspire' | 'loot' | 'madness' | 'genius'
target: 'random' | 'head' | 'youngest' | 'oldest' | 'highestPerception' | 'highestPower' | 'highestFortune' | 'all'
n?: number
desc?: string
}
export interface EffectDef {
res?: Record<string, number>
rep?: number
relation?: Record<string, number>
addBuilding?: string
memberBy?: MemberEffect
pillGain?: Record<string, number>
mission?: string
raid?: { npcId: string }
flag?: Record<string, number | boolean | string>
techniqueChance?: number
artifactChance?: number
addTech?: string
}
export interface EventOptionDef {
label: string
hint?: string
eff: EffectDef
note?: string
}
export interface EventDef {
id: string
name: string
category: 'daily' | 'major' | 'fate'
weight: number
once?: boolean
cond?: Cond
text: string
options: EventOptionDef[]
}
export function eventCategoryName(cat: EventDef['category']): string {
return cat === 'daily' ? '族中日常' : cat === 'major' ? '家国大事' : '乾坤造化'
}
export const EVENTS: EventDef[] = [
{
id: 'ev-youdao', name: '云游道人', category: 'daily', weight: 8,
text: '一名袖袍破旧的云游道人路过祖宅门前,自称识得丹药机巧,愿以一枚聚气丹换五斤灵草。',
options: [
{ label: '换!', hint: '灵草-5,聚气丹+1', eff: { res: { lingcao: -5 }, pillGain: { 'pill-qiyuan': 1 } } },
{ label: '送他些斋饭', hint: '灵石-10,声望+2', eff: { res: { stones: -10 }, rep: 2 } },
{ label: '赶走', hint: '无', eff: {} }
]
},
{
id: 'ev-chonghai', name: '灵田虫害', category: 'daily', weight: 8,
cond: { minBuilding: { id: 'lingtian', level: 1 } },
text: '成片的灵草被青翅虫啃食,照此下去今季收成不保。',
options: [
{ label: '请灵师除虫', hint: '灵石-60', eff: { res: { stones: -60 } } },
{ label: '亲自下田燃烧草', hint: '收成-40%', eff: { res: { lingcao: -8 } } },
{ label: '冷眼旁观', hint: '灵草-20', eff: { res: { lingcao: -20 } } }
]
},
{
id: 'ev-liumin', name: '流民投奔', category: 'daily', weight: 6,
cond: { minRep: 0 },
text: '乱世流民逃难至此,成群跪在庄外,望收留他们做佃户。',
options: [
{ label: '开仓设粥', hint: '灵石-40,声望+4', eff: { res: { stones: -40 }, rep: 4 } },
{ label: '遣散', hint: '声望-2', eff: { rep: -2 } },
{ label: '收为佃户', hint: '灵田产量长增', eff: { res: { stones: -30 }, rep: 2, flag: { tenants: true } } }
]
},
{
id: 'ev-takuang', name: '后山塌方', category: 'daily', weight: 6,
cond: { minBuilding: { id: 'lingkuang', level: 1 } },
text: '后山矿洞一段塌方,矿工数人被困,矿脉暂无人敢下。',
options: [
{ label: '出灵石救治', hint: '灵石-50', eff: { res: { stones: -50 } } },
{ label: '调族人救援', hint: '随机成员负伤', eff: { memberBy: { by: 'wound', target: 'random', n: 15 } } },
{ label: '封洞了事', hint: '矿脉-5', eff: { res: { lingkuang: -5 } } }
]
},
{
id: 'ev-lieshou', name: '猎户献兽', category: 'daily', weight: 5,
cond: { minRep: 10 },
text: '山中猎户捉得一头灵鹿,闻贵族历代尚灵,特来献上。',
options: [
{ label: '买下放生', hint: '灵石-20', eff: { res: { stones: -20 } } },
{ label: '收为坐骑', hint: '兽核+2', eff: { res: { beastcore: 2 } } },
{ label: '收他入庄', hint: '声望+1', eff: { rep: 1 } }
]
},
{
id: 'ev-jiangjiang', name: '巧匠上门', category: 'daily', weight: 5,
cond: { minResource: { id: 'stones', n: 20 } },
text: '一位独臂巧匠携着琳琅法器上门求售,说是祖传手笔。',
options: [
{ label: '买一件回头客', hint: '灵石-100,凡器+1', eff: { res: { stones: -100 }, pillGain: { 'weapon-fan': 1 } } },
{ label: '替他说媒去', hint: '声望+2', eff: { rep: 2 } },
{ label: '让他留个住址', hint: '无', eff: {} }
]
},
{
id: 'ev-lingyu', name: '灵雨', category: 'daily', weight: 6,
cond: { minBuilding: { id: 'lingtian', level: 1 } },
text: '一夜灵雨滂沱,田里的灵草吸饱了灵雨,长势惊人。',
options: [
{ label: '天佑我家!', hint: '灵草+60', eff: { res: { lingcao: 60 } } }
]
},
{
id: 'ev-yupei', name: '孩童拾玉', category: 'daily', weight: 5,
text: '族中孩童在后山拾到一枚玄玉,触手生温,似有不凡。',
options: [
{ label: '供奉宗祠', hint: '声望+5', eff: { rep: 5, flag: { jade: true } } },
{ label: '锁入库房', hint: '灵石+80', eff: { res: { stones: 80 } } },
{ label: '让孩童留之', hint: '其气运+2', eff: { memberBy: { by: 'loot', target: 'youngest', n: 2 }, flag: { jade2: true } } }
]
},
{
id: 'ev-shangdui', name: '路过商队', category: 'daily', weight: 6,
text: '一支四海王家的商队路过,问庄上可有灵矿出手,价给得公道。',
options: [
{ label: '出货', hint: '灵矿-15,灵石+240', eff: { res: { lingkuang: -15, stones: 240 } } },
{ label: '请他们留宿', hint: '声望+2', eff: { rep: 2 } },
{ label: '不理', hint: '无', eff: {} }
]
},
{
id: 'ev-zhusu', name: '祝氏家奴', category: 'daily', weight: 5,
cond: { relation: { npcId: 'n-nulei', lt: 20 } },
text: '捉住一个东丘祝氏的家奴,偷我庄上的灵草,被族人绑来听候发落。',
options: [
{ label: '重罚后放回', hint: '祝氏关系-8', eff: { relation: { 'n-nulei': -8 } } },
{ label: '送还祝氏', hint: '祝氏关系+6', eff: { relation: { 'n-nulei': 6 } } },
{ label: '就地正法', hint: '祝氏关系-20,声望+3', eff: { relation: { 'n-nulei': -20 }, rep: 3 } }
]
},
{
id: 'ev-lunda', name: '论道会', category: 'daily', weight: 5,
cond: { minAdult: 2 },
text: '方圆修士开论道会,族人收到请帖,座上皆是气息不凡之辈。',
options: [
{ label: '遣长老赴会', hint: '随机成员修为+15%', eff: { memberBy: { by: 'exp', target: 'highestPower', n: 15 } } },
{ label: '全族观礼', hint: '声望+3', eff: { rep: 3 } },
{ label: '婉拒', hint: '无', eff: {} }
]
},
{
id: 'ev-wenyi', name: '西川瘟疫', category: 'major', weight: 7,
cond: { minYear: 2 },
text: '西川爆发古怪瘟疫,卢氏求药无门,遣人来向庄上求助,愿以族传丹方相谢。',
options: [
{ label: '赠药救人', hint: '灵草-30,灵石-50,木氏关系+15', eff: { res: { lingcao: -30, stones: -50 }, relation: { 'n-danxin': 15 }, rep: 5 } },
{ label: '坐地起价', hint: '灵石+200,木氏关系-15', eff: { res: { stones: 200 }, relation: { 'n-danxin': -15 } } },
{ label: '关门避险', hint: '声望-2', eff: { rep: -2 } }
]
},
{
id: 'ev-qiuchee', name: '借剑之请', category: 'major', weight: 5,
cond: { minBuilding: { id: 'cangshu', level: 1 } },
text: '玄影沈氏遣使来借一部剑典,言按借期十年奉还,愿以两部法器为押。',
options: [
{ label: '借!', hint: '法器+1,沈氏关系+12', eff: { pillGain: { 'weapon-qi': 1 }, relation: { 'n-xuanying': 12 } } },
{ label: '婉拒', hint: '沈氏关系-5', eff: { relation: { 'n-xuanying': -5 } } },
{ label: '借,但要人质', hint: '沈氏关系+2,声望-2', eff: { relation: { 'n-xuanying': 2 }, rep: -2 } }
]
},
{
id: 'ev-xiamaozi', name: '神秘夜访', category: 'fate', weight: 4,
once: true,
text: '月黑风高,一位蒙面客深夜造访,自称知晓祖地深处有一处上古洞府,愿以两成收成易开启之诀。',
options: [
{ label: '与他交易', hint: '灵石-120', eff: { res: { stones: -120 }, flag: { caveClue: true }, rep: 2 } },
{ label: '绑了问话', hint: '可能招祸', eff: { flag: { caveRumor: true }, rep: -2 } },
{ label: '赏夜赶出', hint: '无', eff: {} }
]
},
{
id: 'ev-zuling', name: '祖祠异动', category: 'fate', weight: 3,
once: true,
cond: { minBuilding: { id: 'zongci', level: 2 } },
text: '深夜祖祠忽闻钟鸣,牌位前竟现一尊淡影,凝视良久,留下二字:「续裔」。',
options: [
{ label: '上香叩拜', hint: '声望+8,全族修炼+', eff: { rep: 8, memberBy: { by: 'inspire', target: 'all', n: 5 }, flag: { ancestorBless: true } } },
{ label: '祭以三牲', hint: '声望+12', eff: { rep: 12, flag: { ancestorBless: true } } },
{ label: '令人守夜', hint: '无', eff: {} }
]
},
{
id: 'ev-leize', name: '万兽潮', category: 'fate', weight: 3,
once: true,
cond: { minYear: 3 },
text: '数万妖兽从东畔雷泽奔涌而下,漫山遍野,直逼庄墙!须速决断。',
options: [
{ label: '举族死守', hint: '战斗!打赢得兽核与声望', eff: { raid: { npcId: 'n-nulei' }, flag: { beastTide: true } } },
{ label: '迁建宗祠', hint: '声望-6,灵石-100', eff: { res: { stones: -100 }, rep: -6 } },
{ label: '联姻求救', hint: '灵石-80,木氏关系+8', eff: { res: { stones: -80 }, relation: { 'n-danxin': 8 } } }
]
},
{
id: 'ev-jobai', name: '卖女求荣', category: 'major', weight: 4,
cond: { minAdult: 1, maxAdult: 8 },
text: '有媒人上门,说东丘祝氏大公子瞧上族中一位姑娘,愿出三百灵石聘礼换两家结好。',
options: [
{ label: '应下这门亲', hint: '灵石+300,祝氏关系+10', eff: { res: { stones: 300 }, relation: { 'n-nulei': 10 } } },
{ label: '再加价', hint: '灵石+450,祝氏关系-5', eff: { res: { stones: 450 }, relation: { 'n-nulei': -5 } } },
{ label: '回绝', hint: '无', eff: {} }
]
},
{
id: 'ev-lingshou', name: '灵兽认主', category: 'major', weight: 4,
cond: { minBuilding: { id: 'lingshou', level: 2 } },
text: '园中一只幼兽七日不食,气若游丝,唯对一位族人亲昵异常。',
options: [
{ label: '牵线认主', hint: '随机成员获得灵兽伴侣', eff: { memberBy: { by: 'genius', target: 'highestFortune', n: 1 } } },
{ label: '卖了换钱', hint: '灵石+150', eff: { res: { stones: 150 } } },
{ label: '放归山林', hint: '声望+2', eff: { rep: 2 } }
]
},
{
id: 'ev-xianyi', name: '残简偶得', category: 'major', weight: 5,
cond: { minBuilding: { id: 'cangshu', level: 1 } },
text: '有人兜售一页古玉残简,字迹晦暗,却似有大道气息流动。',
options: [
{ label: '买下参悟', hint: '灵石-150', eff: { res: { stones: -150 }, techniqueChance: 0.8 } },
{ label: '献诸官家', hint: '声望+6', eff: { rep: 6 } },
{ label: '不信邪,扔了', hint: '无', eff: {} }
]
},
{
id: 'ev-kuangxie', name: '矿脉异动', category: 'major', weight: 4,
cond: { minBuilding: { id: 'lingkuang', level: 2 } },
text: '矿工急报:井下竟听到龙吟之声,地气翻涌,恐是宝脉将成。',
options: [
{ label: '加派人手', hint: '灵矿+80', eff: { res: { lingkuang: 80 } } },
{ label: '镇压封脉', hint: '灵矿-20,声望+3', eff: { res: { lingkuang: -20 }, rep: 3 } },
{ label: '上报求封赏', hint: '灵石+100,声望+5', eff: { res: { stones: 100 }, rep: 5 } }
]
},
{
id: 'ev-yaoqi', name: '玉露灵液', category: 'major', weight: 4,
cond: { minBuilding: { id: 'yaoyuan', level: 1 } },
text: '药园一株千年石斛今夜吐露玉露,紫光浸雾,实为难得。',
options: [
{ label: '专人采露', hint: '聚气丹+2', eff: { pillGain: { 'pill-qiyuan': 2 } } },
{ label: '留作传家', hint: '声望+5', eff: { rep: 5 } },
{ label: '泡酒宴客', hint: '声望+7', eff: { rep: 7 } }
]
},
{
id: 'ev-jiazu', name: '氏族纠纷', category: 'daily', weight: 5,
cond: { minAdult: 3 },
text: '族中兄弟为一处祖屋的归属争执不下,闹到了宗祠。',
options: [
{ label: '秉公裁断', hint: '声望+3', eff: { rep: 3, memberBy: { by: 'wound', target: 'random', n: 5 } } },
{ label: '各打五十大板', hint: '声望-1', eff: { rep: -1 } },
{ label: '拖到明年再说', hint: '声望-3', eff: { rep: -3 } }
]
},
{
id: 'ev-waizushao', name: '修士来投', category: 'major', weight: 5,
cond: { minRep: 20, maxAdult: 12 },
text: '一位炼气散修慕名而来,愿入庄效力,只是要一份例钱。',
options: [
{ label: '开门收下', hint: '族人+1', eff: { memberBy: { by: 'inspire', target: 'all', n: 0 }, rep: 1, flag: { recruit: true } } },
{ label: '考其心性', hint: '族人+1(或负伤)', eff: { memberBy: { by: 'loot', target: 'highestPower', n: 1 }, flag: { recruit2: true } } },
{ label: '婉拒', hint: '无', eff: {} }
]
},
{
id: 'ev-tianlei', name: '天雷淬体', category: 'fate', weight: 2,
once: true,
text: '晴空忽落一道紫雷,劈在宗祠檐角,却汇聚成光球,悬于一位弟子头顶。',
options: [
{ label: '接引练体', hint: '随机成员修为大涨', eff: { memberBy: { by: 'genius', target: 'random', n: 3 } } },
{ label: '存入玉瓶', hint: '聚气丹+3', eff: { pillGain: { 'pill-qiyuan': 3 } } },
{ label: '请道人镇压', hint: '技能+1', eff: { memberBy: { by: 'madness', target: 'random', n: 1 } } }
]
}
]
+43
View File
@@ -0,0 +1,43 @@
export interface ItemDef {
id: string
name: string
kind: 'resource' | 'pill' | 'artifact'
basePrice: number
desc: string
icon: string
cooldown?: number
}
export const ITEMS: Record<string, ItemDef> = {
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: '宝' }
}
export const ARTIFACT_POWER: Record<string, number> = {
'weapon-fan': 0.1,
'weapon-qi': 0.25,
'weapon-ling': 0.5,
'weapon-fa': 0.9
}
export function itemById(id: string): ItemDef {
return ITEMS[id]
}
export interface SimpleTradeItem {
id: string
name: string
price: number
desc: string
count: number
}
+43
View File
@@ -0,0 +1,43 @@
import { RealmMajor } from '../types/domain'
export interface NpcFamilyDef {
id: string
name: string
region: string
desc: string
style: string
leaderRealm: RealmMajor
initialPower: number
powerGrowth: [number, number]
sells?: string[]
buys?: string[]
}
export const NPCS: NpcFamilyDef[] = [
{
id: 'n-xuanying', name: '玄影沈氏', region: '北岳玄影峰', style: '剑修世家', desc: '隐于北岳的剑修吕氏,剑意凛冽,最为孤傲。',
leaderRealm: 'foundation', initialPower: 240, powerGrowth: [4, 10],
sells: ['weapon-qi', 'weapon-ling'], buys: ['lingcao', 'lingkuang']
},
{
id: 'n-danxin', name: '丹心木氏', region: '西川药谷', style: '丹道世家', desc: '悬壶济世的丹道世家,人脉广博,风格和缓。',
leaderRealm: 'foundation', initialPower: 200, powerGrowth: [3, 9],
sells: ['pill-qiyuan', 'pill-ningyuan', 'pill-pojing'], buys: ['lingcao', 'beastcore']
},
{
id: 'n-sihai', name: '四海王氏', region: '南都连港', style: '商盟世族', desc: '商通四海,富可敌国,只认灵石不认人。',
leaderRealm: 'foundation', initialPower: 160, powerGrowth: [5, 12],
sells: ['lingcao', 'lingkuang'], buys: ['beastcore', 'lingkuang']
},
{
id: 'n-nulei', name: '怒雷祝氏', region: '东丘雷泽', style: '兵修蛮门', desc: '雷泽蛮族的世仇,性情火爆,最易生衅。',
leaderRealm: 'core', initialPower: 300, powerGrowth: [5, 13],
sells: [], buys: ['lingkuang', 'beastcore']
}
]
export function npcById(id: string): NpcFamilyDef {
const n = NPCS.find((x) => x.id === id)
if (!n) throw new Error(`npc not found: ${id}`)
return n
}
+14
View File
@@ -0,0 +1,14 @@
import { RealmMajor } from '../types/domain'
export const MAJOR_RATE: Record<RealmMajor, number> = {
mortal: 1,
qi: 1,
foundation: 0.62,
core: 0.38,
nascent: 0.23,
spirit: 0.15
}
export function masteryRateOfMajor(major: RealmMajor): number {
return MAJOR_RATE[major] ?? 1
}
+119
View File
@@ -0,0 +1,119 @@
import { Element, Realm, RealmMajor } from '../types/domain'
export interface MajorDef {
name: string
short: string
minorLayers: number
lifespan: number
// 修为点数 第1层到满层所需(每层指数)
expBase: number
expGrowth: number
}
export const MAJORS: Record<RealmMajor, MajorDef> = {
mortal: { name: '凡人', short: '凡', minorLayers: 0, lifespan: 62, expBase: 0, expGrowth: 1 },
qi: { name: '炼气', short: '炼气', minorLayers: 9, lifespan: 110, expBase: 60, expGrowth: 1.35 },
foundation: { name: '筑基', short: '筑基', minorLayers: 3, lifespan: 165, expBase: 1400, expGrowth: 1.6 },
core: { name: '金丹', short: '金丹', minorLayers: 3, lifespan: 260, expBase: 6200, expGrowth: 1.7 },
nascent: { name: '元婴', short: '元婴', minorLayers: 3, lifespan: 400, expBase: 22000, expGrowth: 1.75 },
spirit: { name: '化神', short: '化神', minorLayers: 3, lifespan: 600, expBase: 60000, expGrowth: 1.8 }
}
export const MAJOR_ORDER: RealmMajor[] = ['mortal', 'qi', 'foundation', 'core', 'nascent', 'spirit']
export const MAJOR_NAMES: Record<RealmMajor, string> = {
mortal: '凡人',
qi: '炼气',
foundation: '筑基',
core: '金丹',
nascent: '元婴',
spirit: '化神'
}
export function describeRealm(realm: Realm): string {
if (realm.major === 'mortal') return '凡人'
const def = MAJORS[realm.major]
return `${def.name}${realm.minor + 1}`
}
export function realmTier(realm: Realm): number {
return MAJOR_ORDER.indexOf(realm.major) * 10 + (realm.major === 'mortal' ? 0 : realm.minor + 1)
}
export function compareRealm(a: Realm, b: Realm): number {
return realmTier(a) - realmTier(b)
}
export function maxRealmExp(major: RealmMajor, minor: number): number {
if (major === 'mortal') return 0
const def = MAJORS[major]
return Math.round(def.expBase * Math.pow(def.expGrowth, minor))
}
export function nextRealm(realm: Realm): Realm | null {
const idx = MAJOR_ORDER.indexOf(realm.major)
if (realm.major === 'mortal') return { major: 'qi', minor: 0 }
if (idx >= MAJOR_ORDER.length - 1 && realm.minor >= MAJORS[realm.major as RealmMajor].minorLayers - 1) return null
const def = MAJORS[realm.major as RealmMajor]
if (realm.minor + 1 < def.minorLayers) return { major: realm.major, minor: realm.minor + 1 }
return { major: MAJOR_ORDER[idx + 1], minor: 0 }
}
export function basePower(realm: Realm): number {
if (realm.major === 'mortal') return 1 * (realm.minor * 0.15 + 1)
const def = MAJORS[realm.major]
return Math.pow(MODEL_BASE, MAJOR_ORDER.indexOf(realm.major)) * (1 + realm.minor * 0.18)
}
const MODEL_BASE = 3.4
export function realmDeathChance(realm: Realm, mind: number): number {
switch (realm.major) {
case 'mortal':
return 0.002
case 'qi':
return 0.004
case 'foundation':
return 0.008
case 'core':
return 0.02
case 'nascent':
return 0.035
case 'spirit':
return 0.05
default:
return 0.005
}
}
export function breakthroughBaseChance(realm: Realm): number {
switch (realm.major) {
case 'mortal':
return 0.9
case 'qi':
return 0.45
case 'foundation':
return 0.32
case 'core':
return 0.22
case 'nascent':
return 0.14
case 'spirit':
return 0.08
default:
return 0.4
}
}
export interface TechniqueDef {
id: string
name: string
grade: number
element: Element
path: string
expBonus: number
powerBonus: number
desc: string
}
export const TECHNIQUE_GRADE_NAMES = ['黄阶', '玄阶', '地阶', '天阶', '仙阶']
+117
View File
@@ -0,0 +1,117 @@
import { RealmMajor } from '../types/domain'
export interface EnemyDef {
id: string
name: string
realm: RealmMajor
strength: number
icon: string
desc: string
}
export const ENEMIES: EnemyDef[] = [
{ id: 'e-huiyuan', name: '灰狼王', realm: 'qi', strength: 0.8, icon: '狼', desc: '啸聚山林的妖兽头领,凶性难驯。' },
{ id: 'e-tiebei', name: '铁背熊罴', realm: 'qi', strength: 1.1, icon: '熊', desc: '皮糙肉厚的山中猛兽,以力御敌。' },
{ id: 'e-muche', name: '赤瞳木魅', realm: 'foundation', strength: 1.0, icon: '木', desc: '千年老树成精,嗜血成性。' },
{ id: 'e-baiqi', name: '白骨修士', realm: 'foundation', strength: 1.2, icon: '骨', desc: '枯骨道人的傀儡余孽,邪气森森。' },
{ id: 'e-huanhan', name: '幻寒螭', realm: 'core', strength: 1.15, icon: '螭', desc: '远古寒螭之遗种,吐息生霜。' },
{ id: 'e-yeshen', name: '夜哭古影', realm: 'core', strength: 1.3, icon: '影', desc: '怨念凝结,闻见其声者神魂惑乱。' },
{ id: 'e-jiaoyun', name: '焦云妖鹤', realm: 'nascent', strength: 1.2, icon: '鹤', desc: '焚山煮海的老妖,万兽避让。' },
{ id: 'e-kuangzun', name: '堕墟魔尊', realm: 'spirit', strength: 1.5, icon: '魔', desc: '屠戮一族的魔道巨擘,举手投足皆天威。' }
]
export interface MissionStageDef {
kind: 'event' | 'combat' | 'resource' | 'boss'
months: number
title: string
text?: { safe?: string; good?: string; bad?: string }
enemyId?: string
loot?: LootDef
}
export interface LootDef {
resources: Record<string, [number, number]>
artifactChance: number
techniqueChance: number
beastcoreChance?: number
}
export interface MissionDef {
id: string
name: string
region: string
icon: string
desc: string
realmHint: RealmMajor
minMembers: number
maxMembers: number
risk: number
stages: MissionStageDef[]
completionLoot: LootDef
}
export const MISSIONS: MissionDef[] = [
{
id: 'm-anmoku', name: '暗墨林', region: '祖城西郊', icon: '林',
desc: '灵气暗涌的密林,常有低阶妖兽出没,是子弟试炼的好去处。',
realmHint: 'qi', minMembers: 1, maxMembers: 4, risk: 0.5,
stages: [
{ kind: 'event', months: 3, title: '深入林间', text: { safe: '一路无波,采得几株灵草压惊。', bad: '林深迷路,误入瘴气,队伍折损了体力。' } },
{ 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 } }
],
completionLoot: { resources: { lingcao: [20, 60], lingkuang: [5, 15] }, artifactChance: 0.08, techniqueChance: 0.03 }
},
{
id: 'm-xuangu', name: '玄冰谷', region: '北境寒渊', icon: '冰',
desc: '寒潭彻骨,谷中灵泉可淬骨洗髓,也匿着护泉的凶兽。',
realmHint: 'qi', minMembers: 1, maxMembers: 4, risk: 0.65,
stages: [
{ kind: 'resource', months: 3, title: '踏雪寻泉', loot: { resources: { lingkuang: [20, 50] }, artifactChance: 0.02, techniqueChance: 0.02 } },
{ kind: 'combat', months: 2, title: '泉主现身', enemyId: 'e-huiyuan' },
{ kind: 'boss', months: 2, title: '冰潭之下', enemyId: 'e-baiqi' }
],
completionLoot: { resources: { lingkuang: [30, 70], beastcore: [2, 5] }, artifactChance: 0.15, techniqueChance: 0.06 }
},
{
id: 'm-guzhan', name: '古战阵', region: '南陵荒原', icon: '战',
desc: '上古两族交兵的战场,杀气千年未散,宝物与凶险兼存。',
realmHint: 'foundation', minMembers: 2, maxMembers: 4, risk: 0.75,
stages: [
{ kind: 'event', months: 3, title: '踏勘阵眼', text: { safe: '寻得阵眼遗骸,得一部残简。', bad: '触动禁制,一行人负伤遁走。' } },
{ kind: 'combat', months: 2, title: '怨灵缠身', enemyId: 'e-baiqi' },
{ 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 } }
],
completionLoot: { resources: { lingkuang: [40, 90], beastcore: [2, 8] }, artifactChance: 0.22, techniqueChance: 0.1 }
},
{
id: 'm-lingshan', name: '灵鹫山', region: '东海之滨', icon: '鹫',
desc: '孤峰入云,鹫鸣震谷,山腹有灵矿与丹谷仙人遗泽。',
realmHint: 'foundation', minMembers: 2, maxMembers: 4, risk: 0.85,
stages: [
{ kind: 'combat', months: 2, title: '登山遇鹫', enemyId: 'e-muche' },
{ kind: 'resource', months: 3, title: '灵鹫巢穴', loot: { resources: { lingkuang: [50, 120], beastcore: [3, 8] }, artifactChance: 0.25, techniqueChance: 0.15 } },
{ kind: 'boss', months: 3, title: '丹谷试剑', enemyId: 'e-yeshen' }
],
completionLoot: { resources: { lingcao: [60, 140], beastcore: [4, 10] }, artifactChance: 0.3, techniqueChance: 0.2 }
},
{
id: 'm-tiankeng', name: '天地秘境', region: '传说天外', icon: '玄',
desc: '百年一开的无上秘境,传言其中有登天造化。',
realmHint: 'core', minMembers: 3, maxMembers: 5, risk: 1.0,
stages: [
{ kind: 'event', months: 2, title: '踏入秘境', text: { safe: '奇花异果满目,造化之气扑面。', bad: '紫霄神雷击碎阵法,众人惊魂不定。' } },
{ kind: 'combat', months: 2, title: '守宫神兽', enemyId: 'e-huanhan' },
{ kind: 'combat', months: 3, title: '天劫傀儡', enemyId: 'e-jiaoyun' },
{ kind: 'boss', months: 3, title: '群雄夺宝', enemyId: 'e-kuangzun' }
],
completionLoot: { resources: { lingcao: [100, 220], lingkuang: [80, 160], beastcore: [10, 22] }, artifactChance: 0.6, techniqueChance: 0.5 }
}
]
export function missionById(id: string): MissionDef {
const m = MISSIONS.find((x) => x.id === id)
if (!m) throw new Error(`mission not found: ${id}`)
return m
}
+27
View File
@@ -0,0 +1,27 @@
import { TechniqueDef } from './realms'
export const TECHNIQUES: TechniqueDef[] = [
{ id: 't-qinglian', name: '青莲剑诀', grade: 1, element: '木', path: '剑修', expBonus: 0.12, powerBonus: 0.15, desc: '青莲花开,剑气如雨。' },
{ id: 't-canglei', name: '苍雷炼体功', grade: 1, element: '金', path: '体修', expBonus: 0.1, powerBonus: 0.2, desc: '引雷淬体,肉身如兵。' },
{ id: 't-xuanyue', name: '玄月斩灵诀', grade: 1, element: '水', path: '剑修', expBonus: 0.12, powerBonus: 0.1, desc: '月华落处,万灵俯首。' },
{ id: 't-lieliuxin', name: '离火心经', grade: 1, element: '火', path: '丹修', expBonus: 0.14, powerBonus: 0.08, desc: '以火养心,丹道通神。' },
{ id: 't-houtu', name: '厚土镇岳功', grade: 1, element: '土', path: '体修', expBonus: 0.1, powerBonus: 0.16, desc: '不动如山,守御无双。' },
{ id: 't-baihui', name: '百草经', grade: 1, element: '木', path: '丹修', expBonus: 0.13, powerBonus: 0.06, desc: '识百草而济世人。' },
{ id: 't-zhenyu', name: '阵纹天书', grade: 2, element: '土', path: '阵修', expBonus: 0.16, powerBonus: 0.12, desc: '经纬天地,伏杀千里。' },
{ id: 't-hanyu', name: '寒玉心法', grade: 2, element: '水', path: '剑修', expBonus: 0.15, powerBonus: 0.18, desc: '心如寒玉,剑出无痕。' },
{ id: 't-liehuo', name: '烈火焚空诀', grade: 2, element: '火', path: '剑修', expBonus: 0.16, powerBonus: 0.24, desc: '烈焰焚空,寸草不留。' },
{ id: 't-wanjian', name: '万剑归一诀', grade: 2, element: '金', path: '剑修', expBonus: 0.14, powerBonus: 0.28, desc: '万道剑影,尽归一念。' },
{ id: 't-yushen', name: '驭兽真解', grade: 2, element: '木', path: '御灵', expBonus: 0.15, powerBonus: 0.14, desc: '万物有灵,皆可为兵。' },
{ id: 't-geling', name: '化灵经', grade: 3, element: '水', path: '符修', expBonus: 0.2, powerBonus: 0.22, desc: '墨落成灵,一符镇幽。' },
{ id: 't-jiuxiao', name: '九霄龙吟诀', grade: 3, element: '金', path: '剑修', expBonus: 0.18, powerBonus: 0.35, desc: '龙吟九霄,一剑开天。' },
{ id: 't-tianya', name: '天涯孤鸿法', grade: 3, element: '火', path: '符修', expBonus: 0.21, powerBonus: 0.25, desc: '孤鸿照影,天涯为路。' },
{ id: 't-qiankun', name: '乾坤七星阵', grade: 4, element: '土', path: '阵修', expBonus: 0.25, powerBonus: 0.3, desc: '七星成阵,乾坤为牢。' },
{ id: 't-xiantian', name: '先天混元功', grade: 4, element: '木', path: '体修', expBonus: 0.28, powerBonus: 0.38, desc: '混元一气,万法不侵。' }
]
export function techniqueById(id: string | undefined): TechniqueDef | null {
if (!id) return null
return TECHNIQUES.find((t) => t.id === id) ?? null
}
export const TECHNIQUES_MARKET_POOL = TECHNIQUES.filter((t) => t.grade <= 3)
+31
View File
@@ -0,0 +1,31 @@
export interface TraitDef {
id: string
name: string
desc: string
expBonus?: number
breakBonus?: number
windBonus?: number
charmBonus?: number
danger: number
priceMult?: number
}
export const TRAITS: Record<string, TraitDef> = {
tiangan: { id: 'tiangan', name: '坚毅', desc: '突破成功率提升,突破失败代价减轻。', breakBonus: 0.06, expBonus: 0, danger: 0 },
jizao: { id: 'jizao', name: '急躁', desc: '突破更鲁莽,失败代价更高,但修炼略快。', expBonus: 0.08, danger: 0.7 },
shensui: { id: 'shensui', name: '沉静', desc: '心性稳重,突破不轻易失败。', breakBonus: 0.05, danger: 0 },
xinheng: { id: 'xinheng', name: '性狠', desc: '战时战力提升,掳掠资源更多。', windBonus: 0.06, danger: 0.2 },
haoxiao: { id: 'haoxiao', name: '嗜酒', desc: '心境易起伏,但与人亲近。', charmBonus: 0.04, danger: 0.15 },
xinnian: { id: 'xinnian', name: '多情', desc: '魅力提升,联姻时好感度更高。', charmBonus: 0.08, danger: 0.1 },
zisheng: { id: 'zisheng', name: '自律', desc: '修为获取速度提升。', expBonus: 0.1, danger: 0 },
lijian: { id: 'lijian', name: '利己', desc: '贸易买卖价格更优。', priceMult: 0.05, danger: 0.3 },
keji: { id: 'keji', name: '克己', desc: '重伤时不易陨落。', breakBonus: 0.02, danger: 0 },
youmo: { id: 'youmo', name: '潇洒', desc: '风波中更易化险为夷。', windBonus: 0.05, danger: 0 },
guguai: { id: 'guguai', name: '古怪', desc: '行为难以捉摸,战力微增。', windBonus: 0.08, danger: 0.3 },
chiyi: { id: 'chiyi', name: '斥医', desc: '不喜丹药,突破失败易重伤。', breakBonus: -0.04, danger: 0.6 },
jingkan: { id: 'jingkan', name: '精研', desc: '闭关修炼效率更高。', expBonus: 0.12, danger: 0 },
haoyi: { id: 'haoyi', name: '豪义', desc: '声望获取更多。', charmBonus: 0.06, danger: 0.2 },
yiqi: { id: 'yiqi', name: '易喜', desc: '脾气如火,战斗极勇。', windBonus: 0.1, danger: 0.5 }
}
export const TRAIT_POOL = Object.keys(TRAITS)