diff --git a/src/renderer/game/data/buildings.ts b/src/renderer/game/data/buildings.ts index 62f58be..38b1ae0 100644 --- a/src/renderer/game/data/buildings.ts +++ b/src/renderer/game/data/buildings.ts @@ -152,13 +152,24 @@ export function buildingYieldOf(id: string, level: number): Record } +/** 0.1.40 建筑专精乘数:专精建筑 extra 加成 ×1.5 */ +export const SPECIALTY_MULT = 1.5 + +/** 0.1.40 检查建筑是否为当前专精建筑(flag 路径:family.flag['buildingSpecialty']) */ +export function isSpecialty(id: string, specialtyId?: string): boolean { + return !!specialtyId && id === specialtyId +} + /** 解析 extra 表达式(如 '0.5 + 0.1*L'):仅允许 数字/括号/四则/空格 与变量 L。 - * 白名单校验后求值——所有数字加成以 extra 为单一权威。 */ -export function bonusOf(id: string, key: string, level: number): number { + * 白名单校验后求值——所有数字加成以 extra 为单一权威。 + * 0.1.40:专精建筑 extra 加成 ×1.5(specialtyId 传入时自动判定) */ +export function bonusOf(id: string, key: string, level: number, specialtyId?: string): number { const def = BUILDINGS[id] const expr = def?.extra?.[key] if (!expr) return 0 // 0.1.36 CSP safe:纯解析器——不再 Function()(unsafe-eval 被 Electron 打包版拦截) const v = arith(expr, level) - return Number.isFinite(v) ? v : 0 + if (!Number.isFinite(v)) return 0 + // 0.1.40 专精加成 + return isSpecialty(id, specialtyId) ? v * SPECIALTY_MULT : v } diff --git a/src/renderer/game/data/mandate.ts b/src/renderer/game/data/mandate.ts new file mode 100644 index 0000000..087a722 --- /dev/null +++ b/src/renderer/game/data/mandate.ts @@ -0,0 +1,669 @@ +/** + * 0.1.40 天命系统 · 数据层 + * + * 天命议题池:每 3~5 年(种子派生选取,零 rng 消耗),天下大势产生一个"天命议题"。 + * 玩家必须做出影响家族与天下走向的重大决策——"天命不可违"。 + * + * 天命经 fire(w, id, 1) 高优入事件闸(可顶替被占日常事件)。 + * 天命不可"稍后再议"(requeue)——不可回避。 + * + * 设计原则: + * - 效果复用 EffectDef 结构(零新类型) + * - 选取走种子派生(seed ^ year ^ mandateCount 哈希),零 rng 消耗 + * - 每条议题含 cond 过滤(不满足条件的议题不入候选池) + */ + +import type { Cond, EffectDef } from './events' + +/** 天命类别 */ +export type MandateCategory = 'calamity' | 'war' | 'fortune' | 'visitor' | 'turbulence' + +/** 天命选项 */ +export interface MandateOption { + label: string + hint?: string + eff: EffectDef + note?: string +} + +/** 天命议题定义 */ +export interface MandateDef { + id: string + name: string + category: MandateCategory + text: string + options: MandateOption[] + /** 触发条件(不满足的议题不出现在候选池中) */ + cond?: Cond + /** 天命权重(在同类中抽签的权重,默认 10) */ + weight?: number +} + +/** 天命类别中文名 */ +export function mandateCategoryName(cat: MandateCategory): string { + return cat === 'calamity' ? '天灾篇' + : cat === 'war' ? '人祸篇' + : cat === 'fortune' ? '天时篇' + : cat === 'visitor' ? '人事篇' + : '乱世篇' +} + +/** + * 天命议题池(20 条,分 5 类,每类 4 条) + * + * 设计要点: + * - calamity(天灾):考验玩家的资源调度与道德抉择 + * - war(人祸):考验玩家的军事与外交平衡 + * - fortune(天时):考验玩家的战略取舍(短期 vs 长期) + * - visitor(人事):考验玩家的人才经营 + * - turbulence(乱世):考验玩家的天下站位 + */ +export const MANDATES: MandateDef[] = [ + // ──────────────── 天灾篇 ──────────────── + { + id: 'mandate-drought', + name: '大旱连年', + category: 'calamity', + weight: 12, + text: '天象异变,赤地千里——方圆数百里灵田干裂,灵泉枯竭。周遭散修聚于族门求一口灵水。天灾面前,族中存粮与道义如何权衡?', + options: [ + { + label: '开仓赈灾', + hint: '灵石-300,声望+15,功德+8', + eff: { res: { stones: -300 }, rep: 15, karma: 8 }, + note: '四方散修感念恩德,族名远扬。' + }, + { + label: '筑坛祈雨', + hint: '灵草-50,全员修为微升,功德+3', + eff: { res: { lingcao: -50 }, karma: 3, memberBy: { by: 'exp', target: 'all', n: 3 } }, + note: '族中修士联手做法,天际隐隐有甘霖之意。' + }, + { + label: '坐视不理', + hint: '功德-12,声望-8,可能招致难民冲击', + eff: { karma: -12, rep: -8 }, + note: '散修愤而散去,日后或成祸患。' + } + ] + }, + { + id: 'mandate-plague', + name: '疫病横行', + category: 'calamity', + weight: 10, + text: '一场诡异疫病在周边坊市蔓延——染者灵力逆乱,三日不治则经脉尽毁。族中医者请命:是封锁山门隔绝疫源,还是以丹药济世?', + options: [ + { + label: '封锁山门', + hint: '声望-5,全族安全,功德-2', + eff: { rep: -5, karma: -2 }, + note: '紧闭门户虽保族中平安,却失了人心。' + }, + { + label: '炼丹济世', + hint: '灵草-80,兽核-10,声望+12,功德+10', + eff: { res: { lingcao: -80, beastcore: -10 }, rep: 12, karma: 10 }, + note: '丹药送出,疫病渐息,四方称颂。' + }, + { + label: '借疫敛财', + hint: '灵石+400,功德-20,声望-15', + eff: { res: { stones: 400 }, karma: -20, rep: -15 }, + note: '高价售药虽得暴利,却种下恶因。' + } + ] + }, + { + id: 'mandate-flood', + name: '洪涝滔天', + category: 'calamity', + weight: 10, + text: '连月暴雨如注,灵河决堤——下游数处坊市被淹,灵田被冲毁者十之六七。水患当前,治水还是自救?', + options: [ + { + label: '组织治水', + hint: '灵石-200,灵矿-20,声望+10,功德+6', + eff: { res: { stones: -200, lingkuang: -20 }, rep: 10, karma: 6 }, + note: '族中修士以术法疏导洪流,保住数处坊市。' + }, + { + label: '加固本族', + hint: '灵矿-30,仅保本族灵田', + eff: { res: { lingkuang: -30 } }, + note: '护住了自家,却眼看邻家田园尽没。' + }, + { + label: '趁灾捞利', + hint: '灵石+250,功德-15,声望-10', + eff: { res: { stones: 250 }, karma: -15, rep: -10 }, + note: '灾中哄抬物价,财是进了,道义却亏了。' + } + ] + }, + { + id: 'mandate-beasttide', + name: '兽潮汹涌', + category: 'calamity', + weight: 10, + cond: { minYear: 5 }, + text: '深山老林中妖兽暴动,成群结队涌向人烟——灵兽园首当其冲,周边散修纷纷逃向族门求庇护。兽潮将至,如何应对?', + options: [ + { + label: '出兵抵御', + hint: '全员参战,胜则兽核+20,声望+8', + eff: { memberBy: { by: 'exp', target: 'all', n: 2 }, res: { beastcore: 20 }, rep: 8, karma: 4 }, + note: '击退兽潮,满载而归。' + }, + { + label: '诱兽离境', + hint: '灵草-30,灵石-100,无损退敌', + eff: { res: { lingcao: -30, stones: -100 }, karma: 2 }, + note: '以灵草为饵将兽群引向荒山,不伤一兵一卒。' + }, + { + label: '献地求安', + hint: '灵石-350,声望-6', + eff: { res: { stones: -350 }, rep: -6, karma: -3 }, + note: '割让灵地赔款,暂保平安——但地盘缩了。' + } + ] + }, + + // ──────────────── 人祸篇 ──────────────── + { + id: 'mandate-bandits', + name: '流寇袭扰', + category: 'war', + weight: 12, + text: '乱世流寇盘踞山头,劫掠过往商旅——近日竟犯族门灵道,截去灵石若干。族中弟子请战:是出兵围剿,还是破财消灾?', + options: [ + { + label: '出兵围剿', + hint: '战力判定——胜则声望+10,灵石+150', + eff: { memberBy: { by: 'exp', target: 'highestPower', n: 5 }, rep: 10, res: { stones: 150 }, karma: 3 }, + note: '遣精锐扫荡匪巢,缴获颇丰。' + }, + { + label: '招安纳降', + hint: '灵石-100,人丁+2,声望-3', + eff: { res: { stones: -100 }, rep: -3, karma: 4, flag: { banditRecruit: true } }, + note: '流寇归顺入族,虽非良材,胜在充丁。' + }, + { + label: '贿赂驱离', + hint: '灵石-200', + eff: { res: { stones: -200 }, karma: -2 }, + note: '花钱买平安——但流寇去了又来,何时是头?' + } + ] + }, + { + id: 'mandate-borderwar', + name: '边境烽烟', + category: 'war', + weight: 10, + cond: { minYear: 8 }, + text: '两方天下家开战,战场就在族门百里之外——灵脉被战火波及,坊市断商。是选边站队,还是保持中立?', + options: [ + { + label: '支持强者', + hint: '灵石-150,与胜方关系+25,败方-25', + eff: { res: { stones: -150 }, flag: { borderWarPicked: 'strong' } }, + note: '押注强者——赢了得利,输了遭殃。' + }, + { + label: '支持弱者', + hint: '灵石-100,与弱方关系+30,强方-15,功德+5', + eff: { res: { stones: -100 }, karma: 5, flag: { borderWarPicked: 'weak' } }, + note: '雪中送炭,弱者或铭记在心。' + }, + { + label: '中立观望', + hint: '声望-3,两方关系各-5', + eff: { rep: -3 }, + note: '两边不得罪——也两边不讨好。' + } + ] + }, + { + id: 'mandate-siege', + name: '围庄之危', + category: 'war', + weight: 8, + cond: { minRep: 30, minYear: 10 }, + text: '一个崛起中的 NPC 家族觊觎族门灵脉,兵临庄下——围而不攻,限三日交出灵石一千否则破庄。兵临城下,何以处之?', + options: [ + { + label: '誓死抵抗', + hint: '全员出战——胜则声望+20,败则大伤', + eff: { memberBy: { by: 'exp', target: 'all', n: 4 }, rep: 20, karma: 5, flag: { siegeFought: true } }, + note: '族人同仇敌忾,破釜沉舟!' + }, + { + label: '献财求和', + hint: '灵石-800,声望-10', + eff: { res: { stones: -800 }, rep: -10, karma: -3 }, + note: '忍辱献财,暂保门户——但被人看了笑话。' + }, + { + label: '求援盟友', + hint: '需有盟友(关系≥40的NPC),灵石-100', + eff: { res: { stones: -100 }, rep: 5, karma: 2, flag: { siegeAlliance: true } }, + note: '盟友出兵来援,围敌退去,同盟之谊更深。' + } + ] + }, + { + id: 'mandate-mercenary', + name: '佣兵招募', + category: 'war', + weight: 8, + cond: { minYear: 6 }, + text: '一支游历散修队伍慕族名而来——为首者修为不俗,愿以灵石三百为酬,为族效力三年。收还是不收?', + options: [ + { + label: '重金聘用', + hint: '灵石-300,战力大增,三年后离开', + eff: { res: { stones: -300 }, rep: 3, flag: { mercenaryHired: true } }, + note: '佣兵入族,战力大增——但三年后便走。' + }, + { + label: '婉言谢绝', + hint: '声望+2', + eff: { rep: 2 }, + note: '不收外人——族中自有良材。' + }, + { + label: '以礼相待送行', + hint: '灵石-50,功德+3', + eff: { res: { stones: -50 }, karma: 3 }, + note: '赠以盘缠送走——江湖路远,或再相逢。' + } + ] + }, + + // ──────────────── 天时篇 ──────────────── + { + id: 'mandate-tidesurge', + name: '灵气大涌', + category: 'fortune', + weight: 12, + text: '天象异常——灵气潮汐骤然高涨,方圆百里灵雾弥漫。此等天赐良机,采气修炼还是广修灵田?', + options: [ + { + label: '闭门采气', + hint: '全员修为+8', + eff: { memberBy: { by: 'exp', target: 'all', n: 8 } }, + note: '族中修士闭关采气,修为精进。' + }, + { + label: '广修灵田', + hint: '灵石-150,灵草产出 ×1.5 持续 24 月', + eff: { res: { stones: -150 }, flag: { tideHarvest: true } }, + note: '趁灵气充沛广植灵草,来年丰收。' + }, + { + label: '抛售灵草', + hint: '灵草+100(可出售换取灵石)', + eff: { res: { lingcao: 100 } }, + note: '灵气催生灵草疯长——趁高价出手!' + } + ] + }, + { + id: 'mandate-spiritvein', + name: '灵脉显踪', + category: 'fortune', + weight: 10, + cond: { minYear: 5 }, + text: '族中探子在深山发现一条隐没灵脉——尚未被任何家族占据。是立刻开采,还是报与天下共享?', + options: [ + { + label: '独占开采', + hint: '灵矿+80,灵石+200,功德-3', + eff: { res: { lingkuang: 80, stones: 200 }, karma: -3 }, + note: '灵脉归我——但消息走漏或引来觊觎。' + }, + { + label: '报与仙盟', + hint: '声望+15,灵石+100,功德+6', + eff: { res: { stones: 100 }, rep: 15, karma: 6 }, + note: '仙盟嘉奖我族高义,分润一条灵脉。' + }, + { + label: '暗中出售', + hint: '灵石+350,功德-8', + eff: { res: { stones: 350 }, karma: -8 }, + note: '将灵脉位置卖给了一家——无本买卖。' + } + ] + }, + { + id: 'mandate-treasure', + name: '古宝出世', + category: 'fortune', + weight: 8, + cond: { minYear: 8 }, + text: '一座远古洞府在地震中显露——禁制松动,宝光冲天。四方修士云集争夺。是深入探索,还是坐收渔利?', + options: [ + { + label: '遣队探索', + hint: '灵石-100,可能得功法/宝器/丹药', + eff: { res: { stones: -100 }, techniqueChance: 0.35, artifactChance: 0.25, flag: { treasureExplored: true } }, + note: '遣精锐深入古洞——机缘与风险并存。' + }, + { + label: '设卡收税', + hint: '灵石+250,声望-5', + eff: { res: { stones: 250 }, rep: -5 }, + note: '在洞府入口设卡——凡入者交灵石百枚。' + }, + { + label: '通报仙盟', + hint: '声望+8,灵石+50,功德+4', + eff: { res: { stones: 50 }, rep: 8, karma: 4 }, + note: '古宝归公,仙盟论功行赏。' + } + ] + }, + { + id: 'mandate-heavenbless', + name: '天降祥瑞', + category: 'fortune', + weight: 8, + text: '天降异象——五彩祥云盘旋族门上空七日不散。祥瑞之下,万物滋长,族中人心振奋。这是天道的嘉许,还是考验的前奏?', + options: [ + { + label: '设坛祭天', + hint: '灵石-100,声望+12,功德+8,全员气血+10', + eff: { res: { stones: -100 }, rep: 12, karma: 8, memberBy: { by: 'heal', target: 'all', n: 10 } }, + note: '祭天谢恩——天道降福,族运昌隆。' + }, + { + label: '趁势修炼', + hint: '全员修为+12', + eff: { memberBy: { by: 'exp', target: 'all', n: 12 } }, + note: '祥瑞灵气入体——族中修士齐齐精进。' + }, + { + label: '广邀宾客', + hint: '灵石-200,声望+18,关系全面提升', + eff: { res: { stones: -200 }, rep: 18 }, + note: '趁祥瑞之名广邀天下——一时风头无两。' + } + ] + }, + + // ──────────────── 人事篇 ──────────────── + { + id: 'mandate-scholar', + name: '名士来访', + category: 'visitor', + weight: 12, + cond: { minRep: 15 }, + text: '一位游历天下的名士造访族门——此人参悟三教百家,论道之能名闻四海。他愿在族中盘桓数月,但需以灵石供养。是礼聘还是婉拒?', + options: [ + { + label: '以礼相待', + hint: '灵石-150,功法机率+30%,全员悟性微升', + eff: { res: { stones: -150 }, techniqueChance: 0.3, memberBy: { by: 'exp', target: 'all', n: 2 } }, + note: '名士居族数月,或传功法一部,族中子弟眼界大开。' + }, + { + label: '切磋论道', + hint: '灵石-50,全员悟性+1(高悟性者突破概率提升)', + eff: { res: { stones: -50 }, memberBy: { by: 'inspire', target: 'all', n: 3 } }, + note: '不聘为客——只做论道之交。' + }, + { + label: '婉言谢绝', + hint: '声望-2', + eff: { rep: -2 }, + note: '名士拂袖而去——或传我不敬贤才。' + } + ] + }, + { + id: 'mandate-artisan', + name: '匠师投奔', + category: 'visitor', + weight: 10, + cond: { minYear: 4, minBuilding: { id: 'danfang', level: 1 } }, + text: '一名游方匠师携炼器图谱来投——自称曾为仙盟名工,因故流落。他愿为族中效力,但需以灵矿灵石建炉开炉。收是不收?', + options: [ + { + label: '厚礼收留', + hint: '灵石-200,灵矿-30,丹房效率永久+10%', + eff: { res: { stones: -200, lingkuang: -30 }, flag: { artisanHired: true }, karma: 2 }, + note: '匠师入族,炼丹炼器皆有所长。' + }, + { + label: '试用三月', + hint: '灵石-80,得一件随机兵器', + eff: { res: { stones: -80 }, artifactChance: 0.5 }, + note: '先试后用——匠师卖力铸了一件兵器。' + }, + { + label: '荐与他人', + hint: '声望+3,灵石+50', + eff: { res: { stones: 50 }, rep: 3, karma: 1 }, + note: '将匠师荐给盟友——做了一桩人情。' + } + ] + }, + { + id: 'mandate-disciple', + name: '天纵之资', + category: 'visitor', + weight: 8, + text: '一幼童在族门附近迷路——经探查,此童灵根奇佳,悟性超群,乃百年难遇之才。是收为族中子弟,还是送还寻亲?', + options: [ + { + label: '收为族中', + hint: '人丁+1(天骄幼童),功德+3', + eff: { karma: 3, flag: { prodigyAdopted: true } }, + note: '幼童入族——或许将成为族中下一代栋梁。' + }, + { + label: '送还寻亲', + hint: '声望+5,功德+6', + eff: { rep: 5, karma: 6 }, + note: '幼童归家——其家族或日后登门致谢。' + }, + { + label: '试其根骨', + hint: '灵石-30,有概率得天才(也有概率伤其根基)', + eff: { res: { stones: -30 }, flag: { prodigyTested: true } }, + note: '以秘法探其根骨——有得有失。' + } + ] + }, + { + id: 'mandate-envoy', + name: '仙盟来使', + category: 'visitor', + weight: 10, + cond: { minRep: 25, minYear: 6 }, + text: '太虚仙盟遣使来访——提请族中派一名弟子入仙盟修行三年。仙盟资源丰厚但规矩森严,弟子或大有长进,或因过被逐。是否应允?', + options: [ + { + label: '遣送入盟', + hint: '灵石-50,成员悟性+2,三年后归', + eff: { res: { stones: -50 }, flag: { envoyToAlliance: true }, memberBy: { by: 'exp', target: 'highestPerception', n: 5 } }, + note: '遣最聪慧的弟子入盟——三年后归来,脱胎换骨。' + }, + { + label: '婉拒好意', + hint: '声望-5', + eff: { rep: -5 }, + note: '婉拒仙盟——或被视为不识抬举。' + }, + { + label: '献银代替', + hint: '灵石-300,声望+3', + eff: { res: { stones: -300 }, rep: 3 }, + note: '以灵石代遣——仙盟收了钱,但暗记一笔。' + } + ] + }, + + // ──────────────── 乱世篇 ──────────────── + { + id: 'mandate-choose-side', + name: '群雄逐鹿', + category: 'turbulence', + weight: 10, + cond: { minYear: 10 }, + text: '天下大势已至分水岭——两大阵营对峙,战云密布。中立者两面受压,选边者成王败寇。家族何去何从?', + options: [ + { + label: '组建联盟', + hint: '需关系≥40的两家以上NPC,声望+10', + eff: { rep: 10, karma: 3, flag: { coalitionFormed: true } }, + note: '牵头组建中立联盟——第三条路。' + }, + { + label: '保持中立', + hint: '声望-5,两方关系各-8', + eff: { rep: -5 }, + note: '两边不沾——但也两边受气。' + }, + { + label: '择强而从', + hint: '与最强NPC关系+30,其余-20', + eff: { flag: { choseStrongSide: true }, karma: -2 }, + note: '押注强者——赢了分蛋糕,输了上餐桌。' + } + ] + }, + { + id: 'mandate-rebellion', + name: '宗门叛逆', + category: 'turbulence', + weight: 8, + cond: { minYear: 8, minMembers: 12 }, + text: '族中一名高修为弟子心生异志——暗中联络外族,意图自立门户。事发东窗,如何处置?', + options: [ + { + label: '大义灭亲', + hint: '声望+5,功德+4,人丁-1', + eff: { rep: 5, karma: 4, flag: { rebelPunished: true } }, + note: '公审叛逆,以正族规——人心震慑。' + }, + { + label: '宽恕放逐', + hint: '功德+6,声望-3', + eff: { rep: -3, karma: 6 }, + note: '念其旧功,放逐出族——或日后报恩。' + }, + { + label: '安抚留用', + hint: '灵石-100,声望-8', + eff: { res: { stones: -100 }, rep: -8, karma: -2 }, + note: '好言安抚赐以灵石——但人心已散。' + } + ] + }, + { + id: 'mandate-elder-council', + name: '长老议政', + category: 'turbulence', + weight: 10, + cond: { minGeneration: 3, minAdult: 5 }, + text: '族中已传数代,人丁渐旺——长老们提议设立长老会,分权制衡。家主独尊还是从善如流?', + options: [ + { + label: '设立长老会', + hint: '声望+8,全员心境+3,家主权力分散', + eff: { rep: 8, karma: 2, memberBy: { by: 'inspire', target: 'all', n: 3 }, flag: { elderCouncil: true } }, + note: '长老会成立——族务共议,人心凝聚。' + }, + { + label: '独断专行', + hint: '声望-5,家主修为+5', + eff: { rep: -5, karma: -3, memberBy: { by: 'exp', target: 'head', n: 5 } }, + note: '家主一言堂——权力集中,但人心微妙。' + }, + { + label: '折中设参议', + hint: '声望+3,灵石-50', + eff: { rep: 3, res: { stones: -50 }, flag: { advisoryBoard: true } }, + note: '设参议之位——长老有言权无决权。' + } + ] + }, + { + id: 'mandate-succession', + name: '储位之争', + category: 'turbulence', + weight: 10, + cond: { minGeneration: 3, minYear: 15 }, + text: '家主年事渐高,继承人未定——族中分作两派,各推人选。纷争暗涌,若不决断,恐生内乱。储位归属何人?', + options: [ + { + label: '立长不立幼', + hint: '声望+5,族中安定', + eff: { rep: 5, karma: 2, flag: { successionElder: true } }, + note: '以长幼为序——纲常有序,人心服帖。' + }, + { + label: '立贤不立长', + hint: '声望+3,悟性最高者修为+8', + eff: { rep: 3, memberBy: { by: 'exp', target: 'highestPerception', n: 8 }, flag: { successionWise: true } }, + note: '以才德为先——族中或有不甘,但来日可期。' + }, + { + label: '不立储位', + hint: '声望-8,功德-4', + eff: { rep: -8, karma: -4 }, + note: '拖延不决——暗流涌动,人心浮动。' + } + ] + } +] + +/** 按类别分组的天命池索引 */ +export const MANDATE_BY_CATEGORY: Record = { + calamity: MANDATES.filter((m) => m.category === 'calamity'), + war: MANDATES.filter((m) => m.category === 'war'), + fortune: MANDATES.filter((m) => m.category === 'fortune'), + visitor: MANDATES.filter((m) => m.category === 'visitor'), + turbulence: MANDATES.filter((m) => m.category === 'turbulence') +} + +/** 天命 id 列表(用于冲突检测) */ +export const MANDATE_IDS: string[] = MANDATES.map((m) => m.id) + +/** 根据 id 查找天命定义 */ +export function mandateById(id: string): MandateDef | undefined { + return MANDATES.find((m) => m.id === id) +} + +/** + * 种子派生哈希(零 rng 消耗)——选取天命议题 + * hash = FNV1a(seed_str + ':' + year + ':' + mandateCount) % candidate_pool.length + */ +export function mandateHash(seed: string, year: number, mandateCount: number, salt: number): number { + const str = `${seed}:${year}:${mandateCount}:${salt}` + let h = 2166136261 + for (let i = 0; i < str.length; i++) { + h ^= str.charCodeAt(i) + h = Math.imul(h, 16777619) + h = (h << 13) | (h >>> 19) + } + return h >>> 0 +} + +/** 天命间隔年数(3~5 年,种子派生) */ +export function mandateInterval(seed: string, mandateCount: number): number { + const h = mandateHash(seed, mandateCount, 0, 0x4d41) + return 3 + (h % 3) // 3, 4, or 5 +} + +/** 天命类别选取(种子派生,零 rng 消耗) */ +export function mandateCategoryFor(seed: string, year: number, mandateCount: number): MandateCategory { + const categories: MandateCategory[] = ['calamity', 'war', 'fortune', 'visitor', 'turbulence'] + const h = mandateHash(seed, year, mandateCount, 0x4d43) + return categories[h % categories.length] +} diff --git a/src/renderer/game/engine/runtime/Systems/cultivation.ts b/src/renderer/game/engine/runtime/Systems/cultivation.ts index 61d3a95..f7ccfe6 100644 --- a/src/renderer/game/engine/runtime/Systems/cultivation.ts +++ b/src/renderer/game/engine/runtime/Systems/cultivation.ts @@ -38,7 +38,7 @@ export function monthlyRate(w: World, c: Character, herbFactor = 1): number { rate *= 0.65 } const buildings = st.family.buildings - rate *= 1 + bonusOf('juling', 'expBonus', buildings['juling'] ?? 0) + rate *= 1 + bonusOf('juling', 'expBonus', buildings['juling'] ?? 0, st.family.flag['buildingSpecialty'] as string | undefined) if (w.sysEnabled('season')) rate *= 1 + seasonMod(st.month, 'cult') rate *= 1 + w.postBonus('expAll') if (st.family.flag['fengFeiBless']) rate *= 1.05 @@ -49,7 +49,7 @@ export function monthlyRate(w: World, c: Character, herbFactor = 1): number { if (c.state === 'meditation') { rate *= 1.35 rate *= 1 + w.postBonus('meditation') + (w.sysEnabled('season') ? seasonMod(st.month, 'meditation') : 0) - rate *= 1 + bonusOf('dongfu', 'expBonus', buildings['dongfu'] ?? 0) + rate *= 1 + bonusOf('dongfu', 'expBonus', buildings['dongfu'] ?? 0, st.family.flag['buildingSpecialty'] as string | undefined) } else if (c.state === 'expedition') { rate *= 0.25 } else if (c.state === 'wounded') { diff --git a/src/renderer/game/engine/runtime/Systems/events.ts b/src/renderer/game/engine/runtime/Systems/events.ts index 85390c2..982ac19 100644 --- a/src/renderer/game/engine/runtime/Systems/events.ts +++ b/src/renderer/game/engine/runtime/Systems/events.ts @@ -12,6 +12,7 @@ import { pack } from '../../../data/registry' import { findInheritor } from '../creation' import { resolveTribulation, resolveAscension } from './tribulation' import { nextRealm, describeRealm } from '../../../data/realms' +import { findMandateEvent } from './mandate' const ALL_EVENTS: EventDef[] = [...EVENTS] @@ -270,6 +271,10 @@ export function dynamicEventFor(id: string, w?: World): EventDef | undefined { ] } } + // 0.1.40 天命事件查找 + if (id.startsWith('mandate-')) { + return findMandateEvent(id) + } return undefined } diff --git a/src/renderer/game/engine/runtime/Systems/mandate.ts b/src/renderer/game/engine/runtime/Systems/mandate.ts new file mode 100644 index 0000000..0e73b60 --- /dev/null +++ b/src/renderer/game/engine/runtime/Systems/mandate.ts @@ -0,0 +1,180 @@ +/** + * 0.1.40 天命系统 · 引擎层 + * + * 天命系统核心逻辑: + * - checkMandate(w): 检查是否到期 + 选取议题 + fire 高优入闸 + * - resolveMandate(w, idx): 玩家选择结算(复用 applyEventChoice 路径) + * - 动态事件生成:天命 id = `mandate-`,经 dynamicEventFor 路径返回 EventDef + * + * 确定性保证: + * - 选取走种子派生(mandateHash),零 rng 消耗 + * - 效果结算复用 applyEventChoice 路径(已验证的确定性通道) + * - 天命间隔 3~5 年(种子派生) + * - 天命经 fire(w, id, 1) 高优入闸——可顶替被占日常事件 + * - 天命不可"稍后再议"——不可回避(UI 层隐藏 dorm 按钮) + */ + +import type { World } from '../World' +import { fire } from './events' +import { matchesCond } from './events' +import type { EventDef } from '../../../data/events' +import { + MANDATES, + MANDATE_BY_CATEGORY, + mandateById, + mandateCategoryFor, + mandateHash, + mandateInterval, + mandateCategoryName, + type MandateDef, + type MandateCategory +} from '../../../data/mandate' + +/** 天命事件 id 前缀 */ +export const MANDATE_PREFIX = 'mandate-' + +/** 天命事件 id 构造 */ +export function mandateEventId(defId: string): string { + return `${MANDATE_PREFIX}${defId}` +} + +/** 判断事件 id 是否为天命 */ +export function isMandateEvent(eventId: string): boolean { + return eventId.startsWith(MANDATE_PREFIX) +} + +/** 从天命事件 id 提取天命 def id */ +export function mandateDefIdFromEvent(eventId: string): string { + return eventId.slice(MANDATE_PREFIX.length) +} + +/** + * 检查天命是否到期——如到期则选取议题并 fire 高优入闸 + * + * 在 epilogue 相位末调用(clocks.ts 注册)。 + * 检查逻辑: + * 1. 首次天命:开局第 5 年(给玩家前期适应时间) + * 2. 后续天命:上次天命年 + mandateInterval(seed, count)(3~5 年) + * 3. 如果有待决事件——不触发(避免覆盖渡劫/飞升等关键事件) + * 4. 如果 game over——不触发 + * + * 返回 true 表示已触发天命(调用了 fire) + */ +export function checkMandate(w: World): boolean { + const s = w.state + if (s.gameOver) return false + if (s.pendingEvent) return false // 有待决事件——不覆盖 + + const seed = s.seed + const mandateYear = (s.family.flag['mandateYear'] as number | undefined) ?? 0 + const mandateCount = (s.family.flag['mandateCount'] as number | undefined) ?? 0 + + // 首次天命:第 5 年 + if (mandateYear === 0) { + if (s.year < 5) return false + } else { + // 后续天命:上次天命年 + 间隔 + const interval = mandateInterval(seed, mandateCount) + if (s.year < mandateYear + interval) return false + } + + // 选取天命议题 + const def = selectMandate(w) + if (!def) return false + + // 记录天命年/次数 + s.family.flag['mandateYear'] = s.year + s.family.flag['mandateCount'] = mandateCount + 1 + s.family.flag['mandateLastId'] = def.id + + // 高优入闸 + const eventId = mandateEventId(def.id) + return fire(w, eventId, 1) +} + +/** + * 选取天命议题(种子派生,零 rng 消耗) + * + * 流程: + * 1. 种子派生选取类别 + * 2. 在该类别的议题池中过滤 cond + * 3. 在符合条件的议题中按权重+种子派生选取 + * 4. 如果该类别无符合条件议题——回退到全池(按 cond 过滤后种子派生) + */ +function selectMandate(w: World): MandateDef | undefined { + const s = w.state + const seed = s.seed + const mandateCount = (s.family.flag['mandateCount'] as number | undefined) ?? 0 + + // 种子派生选取类别 + const cat = mandateCategoryFor(seed, s.year, mandateCount) + + // 在该类别中过滤 cond + const pool = MANDATE_BY_CATEGORY[cat].filter((m) => matchesCond(w, m.cond)) + + if (pool.length > 0) { + return weightedPick(pool, seed, s.year, mandateCount) + } + + // 该类别无符合条件议题——回退到全池 + const fullPool = MANDATES.filter((m) => matchesCond(w, m.cond)) + if (fullPool.length === 0) return undefined + return weightedPick(fullPool, seed, s.year, mandateCount) +} + +/** 种子派生加权选取(零 rng 消耗) */ +function weightedPick(pool: MandateDef[], seed: string, year: number, mandateCount: number): MandateDef { + const weights = pool.map((m) => m.weight ?? 10) + const total = weights.reduce((a, b) => a + b, 0) + const h = mandateHash(seed, year, mandateCount, 0x4d50) + const r = h % total + let acc = 0 + for (let i = 0; i < pool.length; i++) { + acc += weights[i] + if (r < acc) return pool[i] + } + return pool[pool.length - 1] +} + +/** + * 天命事件动态生成——将 MandateDef 转为 EventDef + * 经 events.ts 的 dynamicEventFor 路径调用 + */ +export function mandateAsEvent(def: MandateDef): EventDef { + return { + id: mandateEventId(def.id), + name: `【天命】${def.name}`, + category: 'fate', + weight: 0, + once: false, // 天命可重复(不同年可选到不同议题) + text: def.text, + options: def.options.map((o) => ({ + label: o.label, + hint: o.hint, + eff: o.eff, + note: o.note + })) + } +} + +/** + * 在 events.ts 的 dynamicEventFor 中注册天命事件查找 + * + * 当事件 id 以 `mandate-` 开头时,查找天命定义并转为 EventDef + */ +export function findMandateEvent(eventId: string): EventDef | undefined { + if (!isMandateEvent(eventId)) return undefined + const defId = mandateDefIdFromEvent(eventId) + const def = mandateById(defId) + if (!def) return undefined + return mandateAsEvent(def) +} + +/** 天命类别名(供 UI 展示) */ +export function mandateEventCategoryName(eventId: string): string | undefined { + if (!isMandateEvent(eventId)) return undefined + const defId = mandateDefIdFromEvent(eventId) + const def = mandateById(defId) + if (!def) return undefined + return mandateCategoryName(def.category) +} diff --git a/src/renderer/game/engine/runtime/World.ts b/src/renderer/game/engine/runtime/World.ts index 0616018..ad8a5c6 100644 --- a/src/renderer/game/engine/runtime/World.ts +++ b/src/renderer/game/engine/runtime/World.ts @@ -778,7 +778,7 @@ export class World { inv['lingcao'] -= r.lingcao inv['beastcore'] -= r.beastcore fam.stones -= r.stones - const chance = bonusOf('danfang', 'craftChance', lvl) + const chance = bonusOf('danfang', 'craftChance', lvl, fam.flag['buildingSpecialty'] as string | undefined) if (this.rng.chance(chance)) { inv[`pill-${kind}`] = (inv[`pill-${kind}`] ?? 0) + 1 this.log('good', `丹房炼成一枚${r.name}。`) @@ -855,8 +855,8 @@ export class World { const fam = this.state.family const bonus = 1 + - bonusOf('yanwu', 'powerBonus', fam.buildings['yanwu'] ?? 0) + - bonusOf('lingshou', 'powerBonus', fam.buildings['lingshou'] ?? 0) + + bonusOf('yanwu', 'powerBonus', fam.buildings['yanwu'] ?? 0, fam.flag['buildingSpecialty'] as string | undefined) + + bonusOf('lingshou', 'powerBonus', fam.buildings['lingshou'] ?? 0, fam.flag['buildingSpecialty'] as string | undefined) + this.postBonus('battlePower') const top = this.aliveMembers() .map((c) => combatPowerOf(this, c)) diff --git a/src/renderer/game/engine/runtime/clocks.ts b/src/renderer/game/engine/runtime/clocks.ts index f476c96..9a5e11b 100644 --- a/src/renderer/game/engine/runtime/clocks.ts +++ b/src/renderer/game/engine/runtime/clocks.ts @@ -9,6 +9,7 @@ import { eventRoll } from './Systems/events' import { diplomacyTick } from './Systems/diplomacy' import { yearStartMarriage } from './Systems/marriage' import { WorldSim } from '../sim/WorldSim' +import { checkMandate } from './Systems/mandate' /** 原语义保留:满门凋零后仅存生产/寿元与收尾 */ function ifAlive(fn: (w: World) => void): (w: World) => void { @@ -62,6 +63,8 @@ export function installCoreSystems(ctx: PluginContext): Array<() => void> { } })) unsubs.push(clock.register('epilogue', (w: World) => w.epilogueTick())) + // 0.1.40 天命系统:epilogue 末检查天命是否到期——种子派生选取,零 rng 消耗 + unsubs.push(clock.register('epilogue', viaCap('events', ifAlive((w: World) => { void checkMandate(w) })))) return unsubs } diff --git a/src/renderer/game/engine/sim/WorldSim.ts b/src/renderer/game/engine/sim/WorldSim.ts index 66430f8..edd5029 100644 --- a/src/renderer/game/engine/sim/WorldSim.ts +++ b/src/renderer/game/engine/sim/WorldSim.ts @@ -676,21 +676,82 @@ function evolveNpc(w: World, s: WorldSimState, noise: number): void { const cur = dyn.relationsWithOthers[oid] ?? 0 dyn.relationsWithOthers[oid] = clamp(cur + (w.rng.chance(0.5) ? 1 : -1) * 5, -100, 100) } - // 换代评估:寿终阈值 或 年首小概率(退隐/遇害)——约 50 年一代 + // 0.1.40 换代评估:四模式(退隐传贤/遇害暴毙/夺位篡权/寿终正寝) const chanceNow = dyn.leaderAge > lifespan * 0.9 ? 0.5 : 0.02 if (w.rng.chance(chanceNow) && !w.rng.chance(0.25)) { - dyn.leaderAge = 30 + w.rng.int(0, 25) - dyn.leaderRealmIdx = Math.min(dyn.leaderRealmIdx + 1, 5) - dyn.leaderName = `${def.name.replace('氏', '')}氏新主` - dyn.prosperity = 55 + w.rng.int(0, 20) - npc.power = Math.round(Math.max(40, npc.power + 15)) - dyn.lastEvent = '宗祧更替' + const eraNow = (s.era ?? 'pingshi') as string + const prosperityNow = dyn.prosperity ?? 50 + let mode: 'retire' | 'death' | 'usurp' | 'normal' + if (eraNow === 'luanshi' || eraNow === 'mofa') { + if (prosperityNow < 30 && w.rng.chance(0.4)) mode = 'death' + else if (npc.power > 400 && w.rng.chance(0.25)) mode = 'usurp' + else mode = 'normal' + } else { + if (prosperityNow > 70 && w.rng.chance(0.45)) mode = 'retire' + else if (npc.power > 400 && w.rng.chance(0.15)) mode = 'usurp' + else mode = 'normal' + } + switch (mode) { + case 'retire': { + // 退隐传贤:平盛世高景气——宗主功成身退,传位后辈 + dyn.leaderAge = 30 + w.rng.int(0, 20) + dyn.leaderRealmIdx = Math.min(dyn.leaderRealmIdx + 1, 5) + dyn.leaderName = `${def.name.replace('氏', '')}氏新主` + dyn.prosperity = clamp(prosperityNow - 5, 8, 100) + npc.power = Math.round(Math.max(40, npc.power - 5)) + dyn.lastEvent = '退隐传贤' + break + } + case 'death': { + // 遇害暴毙:乱世末法低景气——宗主陨落,群龙无首 + dyn.leaderAge = 25 + w.rng.int(0, 20) + dyn.leaderRealmIdx = Math.max(0, dyn.leaderRealmIdx) // 境界不变或降 + dyn.leaderName = `${def.name.replace('氏', '')}氏少主` + dyn.prosperity = clamp(prosperityNow - 20, 8, 100) + npc.power = Math.round(Math.max(40, npc.power - 30)) + dyn.lastEvent = '宗主遇害' + break + } + case 'usurp': { + // 夺位篡权:高压家族——强人夺位,势力激增但人心不稳 + dyn.leaderAge = 35 + w.rng.int(0, 15) + dyn.leaderRealmIdx = Math.min(dyn.leaderRealmIdx + (w.rng.chance(0.4) ? 2 : 1), 5) + dyn.leaderName = `${def.name.replace('氏', '')}氏篡主` + dyn.prosperity = clamp(prosperityNow + 8, 8, 100) + npc.power = Math.round(Math.max(40, npc.power + 40)) + dyn.lastEvent = '夺位篡权' + // 篡权者树敌:与邻家关系恶化 + for (const oid of ids) { + if (oid === id) continue + const cur = dyn.relationsWithOthers[oid] ?? 0 + dyn.relationsWithOthers[oid] = clamp(cur - 12, -100, 100) + } + break + } + default: { + // 寿终正寝:常规换代 + dyn.leaderAge = 30 + w.rng.int(0, 25) + dyn.leaderRealmIdx = Math.min(dyn.leaderRealmIdx + 1, 5) + dyn.leaderName = `${def.name.replace('氏', '')}氏新主` + dyn.prosperity = 55 + w.rng.int(0, 20) + npc.power = Math.round(Math.max(40, npc.power + 15)) + dyn.lastEvent = '宗祧更替' + } + } dyn.lastEventYear = year s.npcSuccessions++ pushNews(w, s, [id]) - w.log('info', `【天下】${def.name} 更易宗主,气象一新。`) + const eventDesc = dyn.lastEvent === '退隐传贤' ? '宗主功成身退,后辈继位' + : dyn.lastEvent === '宗主遇害' ? '宗主遇害身亡,少主仓促继位' + : dyn.lastEvent === '夺位篡权' ? '强人夺位,人心浮动' + : '更易宗主,气象一新' + w.log('info', `【天下】${def.name} ${eventDesc}。`) w.emitFx('ripple', `succession:${id}`) } + // 0.1.40 NPC 势力分裂:power>500 且景气<25 的家族,年首 3% 概率分裂 + if (w.rng.chance(0.03) && npc.power > 500 && (dyn.prosperity ?? 50) < 25 && !npc.allied) { + splitNpcDynasty(w, s, id) + } // B7 互攻:与世仇(关系<-50)年首相搏——败方伤筋动骨 for (const oid of ids) { if (oid === id) continue @@ -773,27 +834,136 @@ function purgeNpc(w: World, s: WorldSimState, id: string): void { w.emitFx('ripple', `purge:${id}`) } -const NEWBORN_STYLES = ['新锐剑宗', '灵植世家', '商盟豪族', '兵修门阀', '符箓仙门'] as const -const NEWBORN_REGIONS = ['南麓青泽', '西山雾谷', '东溪云汉', '北原古井'] as const +const NEWBORN_STYLES = ['新锐剑宗', '灵植世家', '商盟豪族', '兵修门阀', '符箓仙门', '丹道世家', '阵法传承', '御兽门阀'] as const +const NEWBORN_REGIONS = ['南麓青泽', '西山雾谷', '东溪云汉', '北原古井', '中州云台', '东海浮屿'] as const -/** 新贵补位:随机风格/区域/初始 power 的新家族(乱世更频) */ +/** + * 0.1.40 NPC 势力分裂 + * + * 触发条件:power>500 且景气<25 且非附庸,年首 3% 概率 + * 效果: + * - 原家族 power 降 40%,景气降至 15 + * - 分裂出的新家族继承 35% power + 部分关系网 + * - 新家族注册为独立 NPC(registerNpcDef + npcFamilies + npcDyn) + * - 落档 worldGen.npcs 保证读档幂等 + */ +function splitNpcDynasty(w: World, s: WorldSimState, parentId: string): void { + const parent = w.state.npcFamilies[parentId] + const parentDef = npcById(parentId) + const parentDyn = s.npcDyn[parentId] + if (!parent || !parentDef || !parentDyn) return + + const rng = w.rng + const splitPower = Math.round(parent.power * 0.35) + parent.power = Math.round(parent.power * 0.6) + parentDyn.prosperity = 15 + parentDyn.lastEvent = '势力分裂' + parentDyn.lastEventYear = w.state.year + + // 分裂出的新家族 + let seedHash = 0 + for (let i = 0; i < w.state.seed.length; i++) seedHash = (seedHash * 31 + w.state.seed.charCodeAt(i)) >>> 0 + const newId = `n-split-${(seedHash % 4096).toString(36)}-${rng.int(1, 9999999)}` + const newName = `${parent.name.slice(0, 2)}分支` + const newStyle = rng.pick([...NEWBORN_STYLES]) + const newRegion = parent.region || rng.pick([...NEWBORN_REGIONS]) + const newDef = { + id: newId, + name: newName, + region: newRegion, + desc: `${parentDef.name}内乱分裂而出,另立门户。`, + style: newStyle, + leaderRealm: 'foundation' as const, + initialPower: splitPower, + powerGrowth: [3, 9] as [number, number], + sells: parentDef.sells ? [...parentDef.sells] : [], + buys: parentDef.buys ? [...parentDef.buys] : ['lingcao', 'lingkuang'] + } + registerNpcDef(newDef) + // 落档 worldGen.npcs + if (w.state.worldGen) { + if (!w.state.worldGen.npcs) w.state.worldGen.npcs = [] + w.state.worldGen.npcs.push({ + id: newDef.id, name: newDef.name, region: newDef.region, style: newDef.style, + desc: newDef.desc, leaderRealm: newDef.leaderRealm, initialPower: newDef.initialPower, + powerGrowth: newDef.powerGrowth, sells: newDef.sells, buys: newDef.buys + }) + } + w.state.npcFamilies[newId] = { + id: newId, name: newName, region: newRegion, + power: splitPower, relation: 0, allied: false, raidCount: 0, declineYears: 0 + } + const newDyn = initDynFor(newId) + newDyn.leaderName = `${newName.slice(0, 2)}氏少主` + newDyn.leaderAge = 25 + rng.int(0, 15) + newDyn.leaderRealmIdx = Math.max(0, parentDyn.leaderRealmIdx - 1) + newDyn.prosperity = 40 + newDyn.lastEvent = '分裂自立' + newDyn.lastEventYear = w.state.year + // 继承部分关系网(与原家族的盟友关系减半、仇敌转为中立) + for (const [oid, rel] of Object.entries(parentDyn.relationsWithOthers)) { + if (oid === parentId) continue + newDyn.relationsWithOthers[oid] = rel > 0 ? Math.round(rel * 0.5) : Math.round(rel * 0.3) + // 分裂消息传至邻家:对原家族关系微降、对新家族态度不明 + const otherDyn = s.npcDyn[oid] + if (otherDyn) { + otherDyn.relationsWithOthers[parentId] = clamp((otherDyn.relationsWithOthers[parentId] ?? 0) - 5, -100, 100) + otherDyn.relationsWithOthers[newId] = 0 // 中立起步 + } + } + // 父子反目 + newDyn.relationsWithOthers[parentId] = -40 + parentDyn.relationsWithOthers[newId] = -40 + s.npcDyn[newId] = newDyn + + s.newsFeed.push({ + year: w.state.year, month: 1, src: '史官', + text: `${parent.name} 内乱分裂——${newName} 另立门户,天下格局再变。`, kind: 'annal' + }) + w.log('info', `【天下】${parent.name} 内乱分裂,${newName} 崛起自立。`) + w.emitFx('ripple', `split:${newId}`) +} + +/** 新贵补位:随机风格/区域/初始 power 的新家族(乱世更频) + * 0.1.40 增强:灰烬重生模式——灭亡家族的位置有概率被新势力填补 + * + * 模式选取(rng 派生): + * - 35% 灰烬重生:在已灭亡家族的原区域,以新风格重生(继承部分地缘) + * - 65% 全新初立:原逻辑(随机风格/区域) + */ function spawnNewbornDynasty(w: World, s: WorldSimState): void { const rng = w.rng - const style = rng.pick([...NEWBORN_STYLES]) - const region = rng.pick([...NEWBORN_REGIONS]) + // 0.1.40 灰烬重生模式:检查 worldGen.npcs 中已被 purge 的家族区域 + let ashesRegion: string | undefined + let ashesStyle: string | undefined + if (w.rng.chance(0.35) && w.state.worldGen?.npcs) { + // 从 worldGen.npcs 找到已不在 npcFamilies 中的灭亡家族 + const fallen = w.state.worldGen.npcs.filter((n) => !w.state.npcFamilies[n.id]) + if (fallen.length > 0) { + const pick = fallen[rng.int(0, fallen.length - 1)]! + ashesRegion = pick.region + // 不继承原风格——灰烬中重生的新风格 + ashesStyle = rng.pick([...NEWBORN_STYLES]) + } + } + const style = ashesStyle ?? rng.pick([...NEWBORN_STYLES]) + const region = ashesRegion ?? rng.pick([...NEWBORN_REGIONS]) // P1-10 id 掺 seed 指纹(防跨档碰撞) let seedHash = 0 for (let i = 0; i < w.state.seed.length; i++) seedHash = (seedHash * 31 + w.state.seed.charCodeAt(i)) >>> 0 const id = `n-new-${(seedHash % 4096).toString(36)}-${rng.int(1, 9999999)}` // 0.1.35 A-10 碰撞窗口 9999→9999999 const name = `${region.slice(0, 2)}${NEWBORN_NAME_SUFFIX[rng.int(0, NEWBORN_NAME_SUFFIX.length - 1)]}` + const isAshes = !!ashesRegion const def = { id, name, region, - desc: `${style}初立,闷头搞了十年发展,如今渐攒起一份家业。`, + desc: isAshes + ? `${style}于故地废墟上重建——前人虽去,灵脉犹存,新主收拾残局再起。` + : `${style}初立,闷头搞了十年发展,如今渐攒起一份家业。`, style, leaderRealm: 'foundation' as const, - initialPower: 100 + rng.int(0, 60), + initialPower: isAshes ? 90 + rng.int(0, 50) : 100 + rng.int(0, 60), // 灰烬重生起步略低 powerGrowth: [3, 9] as [number, number], sells: [], buys: ['lingcao', 'lingkuang'] @@ -820,8 +990,13 @@ function spawnNewbornDynasty(w: World, s: WorldSimState): void { dyn.leaderName = `${name.slice(0, 2)}氏新主` s.npcDyn[id] = dyn const asr = s.era ?? 'pingshi' - w.log('good', `【天下】新贵${name}在${region}崛起(${rgStyleName(def.style)}),天下格局松动。`) - s.newsFeed.push({ year: w.state.year, month: 1, src: '史官', text: `新贵${name}崛起于${region}——天下格局松动。`, kind: 'annal' }) + if (isAshes) { + w.log('good', `【天下】灰烬重生——${name}于${region}故地重建(${rgStyleName(def.style)}),旧地新主。`) + s.newsFeed.push({ year: w.state.year, month: 1, src: '史官', text: `灰烬重生——${name}于${region}故地重建,旧地新主。`, kind: 'annal' }) + } else { + w.log('good', `【天下】新贵${name}在${region}崛起(${rgStyleName(def.style)}),天下格局松动。`) + s.newsFeed.push({ year: w.state.year, month: 1, src: '史官', text: `新贵${name}崛起于${region}——天下格局松动。`, kind: 'annal' }) + } void asr } diff --git a/src/renderer/ui/components/EventModal.tsx b/src/renderer/ui/components/EventModal.tsx index fd53e57..ecdc79b 100644 --- a/src/renderer/ui/components/EventModal.tsx +++ b/src/renderer/ui/components/EventModal.tsx @@ -8,6 +8,7 @@ import { combatPowerOf } from '../../game/engine/runtime/Systems/combat' import { FORMATIONS, FormationId } from '../../game/data/formations' import { buildingById } from '../../game/data/buildings' import { ModalShell } from './ModalShell' +import { isMandateEvent, mandateEventCategoryName } from '../../game/engine/runtime/Systems/mandate' const RES_NAME: Record = { lingcao: '灵草', lingkuang: '灵矿', beastcore: '兽核', stones: '灵石', @@ -63,6 +64,8 @@ export function EventModal() { // 0.1.39:飞升事件视觉强化——金色辉光标题 + 仙韵边框 + 金边选项 const isAscension = pendingEventId.startsWith('ev-ascension-') + // 0.1.40:天命事件视觉强化——紫金辉光标题 + 天命边框 + 不可回避 + const isMandate = isMandateEvent(pendingEventId) const raidIdx = pendingEventDef.options.findIndex((o) => o.eff.raid || o.eff.tournament) const squadPool: Character[] = @@ -99,6 +102,8 @@ export function EventModal() { } const dorm = () => { + // 天命不可回避——不可"稍后再议" + if (isMandate) return // 「稍后再议」:把事件按回队列,不消灭、不误弃 world.requeueEvent(pendingEventId) closeModal() @@ -112,7 +117,7 @@ export function EventModal() { allowOuter={false} onClose={dorm} closeLabel="稍后再议" - className={isAscension ? 'modal-ascension' : undefined} + className={isAscension ? 'modal-ascension' : isMandate ? 'modal-mandate' : undefined} footer={ squadPicking ? ( <> @@ -129,6 +134,8 @@ export function EventModal() { {squadSel.length === 0 ? '族中无可战之人' : `出战!(共${squadSel.length}人)`} + ) : isMandate ? ( +
天命不可违——必须做出抉择
) : ( ) diff --git a/src/renderer/ui/panels/MarketPanel.tsx b/src/renderer/ui/panels/MarketPanel.tsx index a84d389..a300d6e 100644 --- a/src/renderer/ui/panels/MarketPanel.tsx +++ b/src/renderer/ui/panels/MarketPanel.tsx @@ -160,7 +160,7 @@ export default function MarketPanel() {
丹房依「灵草+兽核+灵石」配比丹药,成数随丹房等级;破境丹需丹房三级。炼器以灵矿兽核+灵石相合,无建筑门槛,只看家底。
-

丹房炼制(当前 {fam.buildings['danfang'] ?? 0} 级 · 成率 {Math.round(bonusOf('danfang', 'craftChance', fam.buildings['danfang'] ?? 0) * 100)}%)

+

丹房炼制(当前 {fam.buildings['danfang'] ?? 0} 级 · 成率 {Math.round(bonusOf('danfang', 'craftChance', fam.buildings['danfang'] ?? 0, fam.flag['buildingSpecialty'] as string | undefined) * 100)}%)

diff --git a/src/renderer/ui/panels/TerritoryPanel.tsx b/src/renderer/ui/panels/TerritoryPanel.tsx index 810eee5..2febf6b 100644 --- a/src/renderer/ui/panels/TerritoryPanel.tsx +++ b/src/renderer/ui/panels/TerritoryPanel.tsx @@ -1,5 +1,5 @@ import { useGameStore } from '../store' -import { BUILDINGS, buildingById, listBuildingDefs } from '../../game/data/buildings' +import { BUILDINGS, buildingById, listBuildingDefs, SPECIALTY_MULT } from '../../game/data/buildings' import { useMemo } from 'react' export default function TerritoryPanel() { @@ -32,12 +32,13 @@ export default function TerritoryPanel() { !!cost && fam.stones >= cost.stones && fam.inventory['lingkuang'] >= cost.lingkuang + const isSpecialtyBuilding = fam.flag['buildingSpecialty'] === id return ( -
+
{def.icon}
-
{def.name}
+
{def.name}{isSpecialtyBuilding && 【专精】}
{def.kind === 'produce' ? '产出' : '功效'}
{built ? `${level}级` : ''} @@ -95,6 +96,26 @@ export default function TerritoryPanel() { 祭祖{world.state.year - ((fam.flag['lastRiteYear'] as number) ?? -9) < 2 ? '(冷却)' : '150灵石'} )} + {built && def.extra && Object.keys(def.extra).length > 0 && ( + + )} {levelLabel(level)} ) : ( diff --git a/src/renderer/ui/styles.css b/src/renderer/ui/styles.css index b517136..0bdc03b 100644 --- a/src/renderer/ui/styles.css +++ b/src/renderer/ui/styles.css @@ -1050,6 +1050,10 @@ html[data-blade] .battle-lines { color: #4a3d29; filter: saturate(0.6); } +.bld-card.bld-specialty { /* 0.1.40 专精建筑 */ + border-color: #c9a227; + box-shadow: 0 0 0 1px rgba(201, 162, 39, 0.25), 0 2px 8px rgba(201, 162, 39, 0.1); +} /* ============================================================ 坊市 / 外交 / 探秘 @@ -1942,3 +1946,35 @@ html[data-blade] .battle-lines { background: linear-gradient(180deg, #3a3018, #241e0d); border-color: #8a6d2f; } + +/* ---------- 0.1.40 天命叙事视觉强化(紫金辉光) ---------- */ +.modal-mandate .modal-title { + color: #6b4c9a; + text-shadow: 0 0 14px rgba(107, 76, 154, 0.45), 0 1px 0 rgba(245, 240, 255, 0.7); + letter-spacing: 6px; +} +.modal-mandate .modal-text { + border-top-color: rgba(107, 76, 154, 0.4); + border-bottom-color: rgba(107, 76, 154, 0.3); + color: #2a1e3d; +} +.modal-mandate .modal-cat { + color: #7a5aa8; +} +.modal-mandate .opt-btn { + border-color: #8b6db5; + background: linear-gradient(180deg, #f8f5fd, #f0ebf8); +} +.modal-mandate .opt-btn:hover { + border-color: #6b4c9a; + background: #f3edfa; + box-shadow: 0 0 0 1px rgba(107, 76, 154, 0.3), inset 0 1px 0 rgba(255, 250, 255, 0.6); +} +.modal-mandate .opt-btn .hint { + color: #6b4c9a; +} +.modal-mandate::before, +.modal-mandate::after { + background: linear-gradient(180deg, #2a1e3d, #1a1228); + border-color: #6b4c9a; +}
丹方药性配方门槛