feat(0.1.16-P1): 资源闭环引擎(修为耗草/炼丹概率化/铸器/灵脉收益/NPC战力/灾变落效)+ C13C14
【A 经济闭环】 - 修为经济化:练气以上修行者月耗灵草(闭关2普通1),无草速修0.6~1.0衰减+缺草年告警 - 炼丹等级化:bonusOf 表达式引擎(白名单安全求值),craftChance 转正;丹方单源 PILL_RECIPES(聚气L1/凝元L2/破境L3)+ 失败折半退料 - 炼器筑宝:FORGE_RECIPES(法器/灵器/法宝,灵矿+兽核+灵石) - 秘境灵脉:secretQiOf→rollWarbooty 收益门槛(0.5~1.3折算);掉落按秘境 techGrades 过滤 - NPC 实力参战:raid 强度/风险随 npc.power 浮动(0.5~1.8) - 灾年落效:疫病/兽潮一次性袭击 + CALAMITY_FAMILY 月度减产乘子 【C 治理】 - C13 破境丹渡劫修正:大境界时药力→tribBoost 加成(+12%)入渡劫事件,不再跳过三选 - C14 存储防线:loadState 补 migrate;MigrationError(TOO_NEW/未知版本)提示升级 【UI 半批】 - 坊市新增 定制tab(丹方/铸器);ChroniclePanel 手写史书输入条;MemberModal 册立家主钮 - 年报摘要(要闻5条+辞世讣告);EventModal 选项收益预览 - 结盟引擎:setAlliance(relation≥40)/盟友岁差不降/年利15灵石
This commit is contained in:
@@ -165,15 +165,18 @@ export function resolveRaid(
|
||||
): EncounterResult {
|
||||
const npc = w.state.npcFamilies[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 = {
|
||||
id: npcId,
|
||||
name: `${npc.name}的劫掠队`,
|
||||
realm: def.leaderRealm,
|
||||
strength: 0.78,
|
||||
strength,
|
||||
icon: '袭',
|
||||
desc: def.desc
|
||||
}
|
||||
const risk = 0.55
|
||||
const risk = Math.min(0.75, 0.5 + (npcPower / 120) * 0.1)
|
||||
const res = resolveEncounter(w, {
|
||||
title: `${npc.name}来袭!`,
|
||||
enemy,
|
||||
@@ -198,23 +201,29 @@ export function resolveRaid(
|
||||
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> = {}
|
||||
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
|
||||
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 a = w.rng.pick(pool)
|
||||
w.state.family.inventory[a] = (w.state.family.inventory[a] ?? 0) + 1
|
||||
result[a] = 1
|
||||
}
|
||||
if (loot.techniqueChance && w.rng.chance(loot.techniqueChance)) {
|
||||
const t = w.rng.pick(pack().techniques)
|
||||
w.state.family.techniques.push(t.id)
|
||||
result['tech'] = 1
|
||||
if (loot.techniqueChance && w.rng.chance(loot.techniqueChance * itemFactor)) {
|
||||
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)
|
||||
result['tech'] = 1
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -11,11 +11,12 @@ import { newCharacter } from '../pcgen'
|
||||
import { MALE_GIVEN, FEMALE_GIVEN } from '../../kernel/names'
|
||||
import { ASPIRATION_IDS } from '../../../data/aspirations'
|
||||
import { seasonMod } from '../../../data/season'
|
||||
import { bonusOf } from '../../../data/buildings'
|
||||
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
|
||||
let rate = 1
|
||||
let rate = herbFactor
|
||||
rate *= 1.0 + c.perception * 0.18
|
||||
rate *= ROOT_GRADES[c.roots.grade]?.expBonus ?? 0.5
|
||||
const tech = techniqueById(c.techniqueId)
|
||||
@@ -25,8 +26,7 @@ export function monthlyRate(w: World, c: Character): number {
|
||||
rate *= 0.65
|
||||
}
|
||||
const buildings = st.family.buildings
|
||||
const juling = buildings['juling'] ?? 0
|
||||
rate *= 1 + juling * 0.05
|
||||
rate *= 1 + bonusOf('juling', 'expBonus', buildings['juling'] ?? 0)
|
||||
if (w.sysEnabled('season')) rate *= 1 + seasonMod(st.month, 'cult')
|
||||
rate *= 1 + w.postBonus('expAll')
|
||||
if (st.family.flag['fengFeiBless']) rate *= 1.05
|
||||
@@ -37,8 +37,7 @@ export function monthlyRate(w: World, c: Character): number {
|
||||
if (c.state === 'meditation') {
|
||||
rate *= 1.35
|
||||
rate *= 1 + w.postBonus('meditation') + (w.sysEnabled('season') ? seasonMod(st.month, 'meditation') : 0)
|
||||
const dongfu = buildings['dongfu'] ?? 0
|
||||
rate *= 1 + dongfu * 0.08
|
||||
rate *= 1 + bonusOf('dongfu', 'expBonus', buildings['dongfu'] ?? 0)
|
||||
} else if (c.state === 'expedition') {
|
||||
rate *= 0.25
|
||||
} else if (c.state === 'wounded') {
|
||||
@@ -68,9 +67,29 @@ export function cultivationTick(w: World): void {
|
||||
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)) {
|
||||
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
|
||||
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)
|
||||
for (const npc of Object.values(s.npcFamilies)) {
|
||||
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
|
||||
}
|
||||
if (npc.relation < -50) {
|
||||
@@ -25,6 +25,10 @@ export function yearGrowth(w: World): void {
|
||||
const def = npcById(npc.id)
|
||||
const [a, b] = def.powerGrowth
|
||||
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'
|
||||
}
|
||||
} 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)}。`)
|
||||
} else if (stage.kind === 'combat' || stage.kind === 'boss') {
|
||||
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.result = 'success'
|
||||
releaseSquad(w, m)
|
||||
const total = rollWarbooty(w, def.completionLoot)
|
||||
const total = rollWarbooty(w, def.completionLoot, qiRatioOf(w, def.id))
|
||||
m.log.push(`凯旋而归,清点战利:${lootText(total)}。`)
|
||||
const survivors = squadOf(w, m).filter((c) => c.alive).map((c) => c.name).join('、')
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
const def = missionById(defId)
|
||||
if (!canSendMission(w, def, members)) return false
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { World } from '../World'
|
||||
import { aspirationById } from '../../../data/aspirations'
|
||||
import { seasonMod } from '../../../data/season'
|
||||
import { CALAMITY_FAMILY } from '../../sim/worldsim-data'
|
||||
|
||||
export function productionTick(w: World): void {
|
||||
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 merchants = w.aliveMembers().filter((c) => aspirationById(c.aspiration)?.effect.type === 'market').length
|
||||
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) {
|
||||
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
|
||||
parts.push(`灵田+${v}灵草`)
|
||||
}
|
||||
if (yaoyuan > 0) {
|
||||
const v = 5 * yaoyuan
|
||||
const v = Math.round(5 * yaoyuan * farmFactor)
|
||||
inv.lingcao = (inv.lingcao ?? 0) + v
|
||||
parts.push(`药园+${v}药草`)
|
||||
if (yaoyuan >= 3) {
|
||||
@@ -32,7 +37,7 @@ export function productionTick(w: World): void {
|
||||
}
|
||||
}
|
||||
if (lingkuang > 0) {
|
||||
const v = 8 * lingkuang
|
||||
const v = Math.round(8 * lingkuang * mineFactor)
|
||||
inv.lingkuang = (inv.lingkuang ?? 0) + v
|
||||
parts.push(`灵矿+${v}灵矿`)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,8 @@ export function resolveTribulation(w: World, c: Character, mode: 'rash' | 'guard
|
||||
|
||||
if (mode === 'delay') {
|
||||
c.tribDelayYear = w.state.year + 1
|
||||
w.log('info', `${c.name} 按兵不动,引而不发,待来年再渡。`)
|
||||
c.tribBoost = 0
|
||||
w.log('info', `${c.name} 按兵不动,引而不发,待来年再渡(丹力渐散)。`)
|
||||
return 'delayed'
|
||||
}
|
||||
|
||||
@@ -90,5 +91,5 @@ export function perTribChance(w: World, c: Character): number {
|
||||
nascent: 0.36,
|
||||
spirit: 0.26
|
||||
} 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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user