v0.1.34a: 功法万象+世界自演化——流派效果/元素时节/悟位大成/战争疲劳/换代半衰/断供应急/新秘境
【功法】TechniqueDef 新增 critChance/guardBonus/tribBonus/secretBonus(optional 零漂); 6 部新功法(玄冰凝甲/紫电惊雷/丹鼎护道/风遁青冥/山灵镇岳/澜海听涛); 季节元素共鸣(春木夏火秋金冬水——修炼额外×1.04);演武场联动悟位积累;悟位2大成战斗效果倍增 【世界】warFatigue 互攻累积→世界温度冷降→乱世加速(打不动即荒年); NpcDyn 换代恩义清(关系网向零收敛×0.5);断供应急(连续3月池深<70%→广播+价×1.5); 新秘境灵机(年首几率·乱世×2·模板化生成 addMission 扩展口);addMission 公共口(secrets.ts) 【修复】shortage 广播模块级全局污染→世界状态化(多世界确定性恢复) 【测试】world 确定性恢复绿;金钟罩预期漂移(换代/共鸣/warFatigue)——0.1.34 基线待重算
This commit is contained in:
@@ -114,6 +114,11 @@ export interface TechniqueDef {
|
|||||||
expBonus: number
|
expBonus: number
|
||||||
powerBonus: number
|
powerBonus: number
|
||||||
desc: string
|
desc: string
|
||||||
|
/** 0.1.34 流派效果(全 optional:缺省零行为——金钟罩零漂) */
|
||||||
|
critChance?: number // 暴击率:战斗获胜判定之手中暴击一击(落败时敌方反伤减半变数)
|
||||||
|
guardBonus?: number // 受创减免:战斗受伤(%)-如 0.25=受创 25%折
|
||||||
|
tribBonus?: number // 渡劫成功率加成(正值加成/负值谨防)
|
||||||
|
secretBonus?: number // 秘境掉落加成(任务完成掉落乘数)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TECHNIQUE_GRADE_NAMES = ['凡阶', '黄阶', '玄阶', '地阶', '天阶', '仙阶']
|
export const TECHNIQUE_GRADE_NAMES = ['凡阶', '黄阶', '玄阶', '地阶', '天阶', '仙阶']
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ export function seasonOf(month: number): Season {
|
|||||||
return 'winter'
|
return 'winter'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 0.1.34 季节元素共鸣:功法 element 与时令身合——修炼加成(万象归元) */
|
||||||
|
export const SEASON_ELEMENT: Record<Season, string> = {
|
||||||
|
spring: '木', summer: '火', autumn: '金', winter: '水'
|
||||||
|
}
|
||||||
|
|
||||||
export const SEASON: Record<Season, { name: string; field: number; cult: number; market: number; meditation: number }> = {
|
export const SEASON: Record<Season, { name: string; field: number; cult: number; market: number; meditation: number }> = {
|
||||||
spring: { name: '春', field: 0.1, cult: 0, market: 0, meditation: 0 },
|
spring: { name: '春', field: 0.1, cult: 0, market: 0, meditation: 0 },
|
||||||
summer: { name: '夏', field: 0, cult: 0.05, market: 0, meditation: 0 },
|
summer: { name: '夏', field: 0, cult: 0.05, market: 0, meditation: 0 },
|
||||||
|
|||||||
@@ -112,6 +112,49 @@ export const MISSIONS: MissionDef[] = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
/** 0.1.34 新秘境可加性:ext 表+唯一 id 追加(MOD/插件/世界生成共用——MISSIONS 静态表不变) */
|
||||||
|
const EXTRA_MISSIONS: MissionDef[] = []
|
||||||
|
export function addMission(def: MissionDef): void {
|
||||||
|
if (!EXTRA_MISSIONS.find((x) => x.id === def.id)) EXTRA_MISSIONS.push(def)
|
||||||
|
}
|
||||||
|
/** 全部任务:静态 + MOD/世界新增合并(每次构建——小规模数组,且扩张随时生效) */
|
||||||
|
export function allMissions(): MissionDef[] {
|
||||||
|
return [...MISSIONS, ...EXTRA_MISSIONS]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 新秘境生成模板(万象归元·天下灵机偶现)——年首世界侧 chance 生出,皆可探索 */
|
||||||
|
export const WONDER_TEMPLATES: Omit<MissionDef, 'id' | 'name'>[] = [
|
||||||
|
{
|
||||||
|
region: '云梦大泽', icon: '泽', desc: '千年碧潭,隐有灵机吞吐——是仙家典籍中的洞天妙处。',
|
||||||
|
realmHint: 'foundation', minMembers: 2, maxMembers: 4, risk: 0.7,
|
||||||
|
stages: [
|
||||||
|
{ kind: 'event', months: 2, title: '雾锁重泽', text: { safe: '泽畔灵雾和煦,众人寻得一泓灵泉。', bad: '泽中瘴气潜涌,队伍仓促退避。' } },
|
||||||
|
{ kind: 'combat', months: 2, title: '水泽龙王', enemyId: 'e-huanhan' },
|
||||||
|
{ kind: 'resource', months: 2, title: '泽底洞府', loot: { resources: { lingyu: [4, 10], linglu: [3, 8] }, artifactChance: 0.18, techniqueChance: 0.1, techGrades: [2, 3] } }
|
||||||
|
],
|
||||||
|
completionLoot: { resources: { lingyu: [3, 9], lingcao: [30, 70] }, artifactChance: 0.2, techniqueChance: 0.12, techGrades: [2, 3] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
region: '苍梧断崖', icon: '崖', desc: '断崖云雾缭绕,偶见琼光微闪——遗世洞府的门扉近在眼前。',
|
||||||
|
realmHint: 'qi', minMembers: 1, maxMembers: 3, risk: 0.55,
|
||||||
|
stages: [
|
||||||
|
{ kind: 'combat', months: 2, title: '崖间宿怨', enemyId: 'e-tiebei' },
|
||||||
|
{ kind: 'resource', months: 2, title: '崖扉初启', loot: { resources: { lingkuang: [25, 55], lingmu: [5, 12] }, artifactChance: 0.1, techniqueChance: 0.06 } }
|
||||||
|
],
|
||||||
|
completionLoot: { resources: { lingkuang: [20, 55], lingmu: [6, 16], shoupi: [3, 6] }, artifactChance: 0.12, techniqueChance: 0.08, techGrades: [1, 2] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
region: '幽都古道', icon: '幽', desc: '青石古道上阴风掠耳——上古修士埋骨之地,得之者富。',
|
||||||
|
realmHint: 'core', minMembers: 3, maxMembers: 4, risk: 0.8,
|
||||||
|
stages: [
|
||||||
|
{ kind: 'event', months: 2, title: '古道逢阴', text: { safe: '香火未绝的驿亭里,捡得一把尘封古剑。', bad: '阴煞蚀骨,众人寒战如大病初愈。' } },
|
||||||
|
{ kind: 'combat', months: 2, title: '尸修拦路', enemyId: 'e-baiqi' },
|
||||||
|
{ kind: 'boss', months: 3, title: '墓庭之主', enemyId: 'e-huanhan' }
|
||||||
|
],
|
||||||
|
completionLoot: { resources: { lingyu: [3, 8], beastcore: [4, 10], dansha: [3, 7] }, artifactChance: 0.3, techniqueChance: 0.14, techGrades: [3] }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
export function missionById(id: string): MissionDef {
|
export function missionById(id: string): MissionDef {
|
||||||
const m = MISSIONS.find((x) => x.id === id)
|
const m = MISSIONS.find((x) => x.id === id)
|
||||||
if (!m) throw new Error(`mission not found: ${id}`)
|
if (!m) throw new Error(`mission not found: ${id}`)
|
||||||
|
|||||||
@@ -16,7 +16,14 @@ export const TECHNIQUES: TechniqueDef[] = [
|
|||||||
{ id: 't-jiuxiao', name: '九霄龙吟诀', grade: 3, element: '金', path: '剑修', expBonus: 0.18, powerBonus: 0.35, 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-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-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: '混元一气,万法不侵。' }
|
{ id: 't-xiantian', name: '先天混元功', grade: 4, element: '木', path: '体修', expBonus: 0.28, powerBonus: 0.38, desc: '混元一气,万法不侵。' },
|
||||||
|
// 0.1.34 万象归元:流派效果示范(crit/guard/trib/secret——功法类型与效果丰富)
|
||||||
|
{ id: 't-xuanbing', name: '玄冰凝甲功', grade: 1, element: '水', path: '体修', expBonus: 0.11, powerBonus: 0.14, guardBonus: 0.2, desc: '寒甲覆体,攻守两全。' },
|
||||||
|
{ id: 't-zidian', name: '紫电惊雷诀', grade: 1, element: '金', path: '剑修', expBonus: 0.1, powerBonus: 0.22, critChance: 0.12, desc: '雷霆既至,一击定音。' },
|
||||||
|
{ id: 't-danding', name: '丹鼎护道妙典', grade: 2, element: '火', path: '丹修', expBonus: 0.17, powerBonus: 0.1, tribBonus: 0.06, desc: '丹香护鼎,大劫犹安。' },
|
||||||
|
{ id: 't-fengyu', name: '风遁青冥诀', grade: 2, element: '木', path: '符修', expBonus: 0.13, powerBonus: 0.2, secretBonus: 0.2, desc: '青冥风引,探幽得宝。' },
|
||||||
|
{ id: 't-shanling', name: '山灵镇岳录', grade: 2, element: '土', path: '体修', expBonus: 0.14, powerBonus: 0.26, guardBonus: 0.3, critChance: 0.06, desc: '山灵加护,崩山击岳。' },
|
||||||
|
{ id: 't-lanhai', name: '澜海听涛经', grade: 3, element: '水', path: '符修', expBonus: 0.19, powerBonus: 0.24, tribBonus: 0.09, secretBonus: 0.15, desc: '听涛悟澜,化水藏锋。' }
|
||||||
]
|
]
|
||||||
|
|
||||||
export function techniqueById(id: string | undefined): TechniqueDef | null {
|
export function techniqueById(id: string | undefined): TechniqueDef | null {
|
||||||
|
|||||||
@@ -90,7 +90,16 @@ export function resolveEncounter(
|
|||||||
const talisman = opts.kind === 'war' ? talismanWarPrep(w) : null
|
const talisman = opts.kind === 'war' ? talismanWarPrep(w) : null
|
||||||
const talismMult = talisman === 'feng' ? 1.12 : talisman === 'shan' ? 1.15 : 1
|
const talismMult = talisman === 'feng' ? 1.12 : talisman === 'shan' ? 1.15 : 1
|
||||||
let team = 0
|
let team = 0
|
||||||
for (const c of opts.team) team += combatPowerOf(w, c)
|
let guard = 0
|
||||||
|
let crit = 0
|
||||||
|
for (const c of opts.team) {
|
||||||
|
team += combatPowerOf(w, c)
|
||||||
|
const t = techniqueById(c.techniqueId)
|
||||||
|
guard += (t?.guardBonus ?? 0)
|
||||||
|
crit += (t?.critChance ?? 0)
|
||||||
|
}
|
||||||
|
guard /= Math.max(1, opts.team.length)
|
||||||
|
crit /= Math.max(1, opts.team.length)
|
||||||
team *= talismMult
|
team *= talismMult
|
||||||
const enemy = Math.round(enemyPowerOf(opts.enemy, opts.risk) * form.def)
|
const enemy = Math.round(enemyPowerOf(opts.enemy, opts.risk) * form.def)
|
||||||
const jitter = w.rng.between(0.88, 1.12)
|
const jitter = w.rng.between(0.88, 1.12)
|
||||||
@@ -118,7 +127,7 @@ export function resolveEncounter(
|
|||||||
if (!c.alive || c.state === 'wounded') continue
|
if (!c.alive || c.state === 'wounded') continue
|
||||||
const severity = w.rng.next()
|
const severity = w.rng.next()
|
||||||
if (!win) {
|
if (!win) {
|
||||||
if (severity < 0.1 && w.rng.chance(opts.risk * 0.08 + 0.02)) {
|
if (severity < 0.1 && w.rng.chance((opts.risk * 0.08 + 0.02) * (1 - crit))) {
|
||||||
c.alive = false
|
c.alive = false
|
||||||
c.deathYear = year
|
c.deathYear = year
|
||||||
c.deathCause = `战殁于${opts.enemy.name}之手`
|
c.deathCause = `战殁于${opts.enemy.name}之手`
|
||||||
@@ -126,13 +135,13 @@ export function resolveEncounter(
|
|||||||
w.chronicle('death', `${c.name} 战殁于${opts.enemy.name},一身所学俱付尘烟。`, c.id, true)
|
w.chronicle('death', `${c.name} 战殁于${opts.enemy.name},一身所学俱付尘烟。`, c.id, true)
|
||||||
w.emitFx('ripple', `death:${c.id}`)
|
w.emitFx('ripple', `death:${c.id}`)
|
||||||
} else if (severity < 0.45) {
|
} else if (severity < 0.45) {
|
||||||
const woundAmt = Math.round((40 + w.rng.int(0, 25)) * form.retreatWound * (talisman === 'shan' ? 0.5 : 1))
|
const woundAmt = Math.round((40 + w.rng.int(0, 25)) * form.retreatWound * (talisman === 'shan' ? 0.5 : 1) * (1 - guard))
|
||||||
c.health = Math.max(1, c.health - woundAmt)
|
c.health = Math.max(1, c.health - woundAmt)
|
||||||
c.state = 'wounded'
|
c.state = 'wounded'
|
||||||
loss.push(`${c.name} 重伤`)
|
loss.push(`${c.name} 重伤`)
|
||||||
}
|
}
|
||||||
} else if (w.rng.chance(0.12)) {
|
} else if (w.rng.chance(0.12)) {
|
||||||
c.health = Math.max(1, c.health - 20 - w.rng.int(0, 15))
|
c.health = Math.max(1, c.health - Math.round((20 + w.rng.int(0, 15)) * (1 - guard)))
|
||||||
if (c.health < 35) c.state = 'wounded'
|
if (c.health < 35) c.state = 'wounded'
|
||||||
loss.push(`${c.name} 轻伤`)
|
loss.push(`${c.name} 轻伤`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ROOT_GRADES } from '../../../data/elements'
|
|||||||
import { masteryRateOfMajor } from '../../../data/pacing'
|
import { masteryRateOfMajor } from '../../../data/pacing'
|
||||||
import { aspirationById, fitBonusOf } from '../../../data/aspirations'
|
import { aspirationById, fitBonusOf } from '../../../data/aspirations'
|
||||||
import { techniqueById } from '../../../data/techniques'
|
import { techniqueById } from '../../../data/techniques'
|
||||||
|
import { seasonOf, SEASON_ELEMENT } from '../../../data/season'
|
||||||
import { nextRealm, breakthroughBaseChance, realmDeathChance, describeRealm, MAJOR_ORDER } from '../../../data/realms'
|
import { nextRealm, breakthroughBaseChance, realmDeathChance, describeRealm, MAJOR_ORDER } from '../../../data/realms'
|
||||||
import { lifespanOf } from './lifecycle'
|
import { lifespanOf } from './lifecycle'
|
||||||
import { traitBonuses } from '../pcgen'
|
import { traitBonuses } from '../pcgen'
|
||||||
@@ -24,6 +25,8 @@ export function monthlyRate(w: World, c: Character, herbFactor = 1): number {
|
|||||||
const tech = techniqueById(c.techniqueId)
|
const tech = techniqueById(c.techniqueId)
|
||||||
if (tech && c.realm.major !== 'mortal') {
|
if (tech && c.realm.major !== 'mortal') {
|
||||||
rate *= 1 + tech.expBonus + (c.techniqueRank ?? 0) * 0.05
|
rate *= 1 + tech.expBonus + (c.techniqueRank ?? 0) * 0.05
|
||||||
|
// 0.1.34 万象归元:功法元素与时令共鸣(春木夏火秋金冬水——身合自然)
|
||||||
|
if (w.sysEnabled('season') && SEASON_ELEMENT[seasonOf(st.month)] === tech.element) rate *= 1.04
|
||||||
} else if (c.realm.major !== 'mortal') {
|
} else if (c.realm.major !== 'mortal') {
|
||||||
rate *= 0.65
|
rate *= 0.65
|
||||||
}
|
}
|
||||||
@@ -108,7 +111,9 @@ export function cultivationTick(w: World): void {
|
|||||||
// 悟道进度:有功法且修为之外,另积一分慧根
|
// 悟道进度:有功法且修为之外,另积一分慧根
|
||||||
if (c.techniqueId && c.realm.major !== 'mortal') {
|
if (c.techniqueId && c.realm.major !== 'mortal') {
|
||||||
const rank = c.techniqueRank ?? 0
|
const rank = c.techniqueRank ?? 0
|
||||||
const gain = rate * (rank === 0 ? 0.5 : rank === 1 ? 0.3 : 0.15)
|
const wuLvl = w.state.family.buildings['yanwu'] ?? 0
|
||||||
|
const wuMult = 1 + wuLvl * 0.08 // 0.1.34 演武场联动——日日操练,悟性开慧
|
||||||
|
const gain = rate * (rank === 0 ? 0.5 : rank === 1 ? 0.3 : 0.15) * wuMult
|
||||||
c.techniqueProgress = Math.min(100, (c.techniqueProgress ?? 0) + gain)
|
c.techniqueProgress = Math.min(100, (c.techniqueProgress ?? 0) + gain)
|
||||||
if (c.techniqueProgress >= 100) {
|
if (c.techniqueProgress >= 100) {
|
||||||
if (rank === 0) {
|
if (rank === 0) {
|
||||||
|
|||||||
@@ -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, qiRatioOf(w, def.id))
|
const loot = scaleBySecret(w, m, 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, qiRatioOf(w, def.id))
|
const total = scaleBySecret(w, m, rollWarbooty(w, def.completionLoot, qiRatioOf(w, def.id)))
|
||||||
// 0.1.23 秘境回池:探得之物近 2 成回流人间(负反馈世界侧供给)
|
// 0.1.23 秘境回池:探得之物近 2 成回流人间(负反馈世界侧供给)
|
||||||
if (w.state.worldSim) {
|
if (w.state.worldSim) {
|
||||||
const sim = new WorldSim(w)
|
const sim = new WorldSim(w)
|
||||||
@@ -92,6 +92,19 @@ export function missionTick(w: World): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 0.1.34 功法秘境加成:结队平均 secretBonus → 战利乘数(零 rng——值域确定) */
|
||||||
|
function scaleBySecret(w: World, m: MissionState, loot: Record<string, number>): Record<string, number> {
|
||||||
|
const squad = squadOf(w, m)
|
||||||
|
if (squad.length === 0) return loot
|
||||||
|
let bonus = 0
|
||||||
|
for (const c of squad) bonus += techniqueById(c.techniqueId)?.secretBonus ?? 0
|
||||||
|
bonus /= squad.length
|
||||||
|
if (bonus <= 0) return loot
|
||||||
|
const out: Record<string, number> = {}
|
||||||
|
for (const [k, v] of Object.entries(loot)) out[k] = typeof v === 'number' ? Math.max(1, Math.round(v * (1 + bonus))) : v
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
function squadOf(w: World, m: MissionState) {
|
function squadOf(w: World, m: MissionState) {
|
||||||
return m.memberIds.map((id) => w.memberById(id)).filter((c) => c.alive)
|
return m.memberIds.map((id) => w.memberById(id)).filter((c) => c.alive)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { World } from '../World'
|
import type { World } from '../World'
|
||||||
import { Character } from '../../../types/domain'
|
import { Character } from '../../../types/domain'
|
||||||
import { nextRealm, MAJOR_ORDER, realmDeathChance, describeRealm } from '../../../data/realms'
|
import { nextRealm, MAJOR_ORDER, realmDeathChance, describeRealm } from '../../../data/realms'
|
||||||
|
import { techniqueById } from '../../../data/techniques'
|
||||||
|
|
||||||
export const TRIB_MAJORS: string[] = ['foundation', 'core', 'nascent', 'spirit']
|
export const TRIB_MAJORS: string[] = ['foundation', 'core', 'nascent', 'spirit']
|
||||||
|
|
||||||
@@ -100,5 +101,6 @@ 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 + (c.tribBoost ?? 0))
|
const tb = techniqueById(c.techniqueId)?.tribBonus ?? 0
|
||||||
|
return Math.min(0.92, base + c.mind * 0.01 + c.health / 400 + (c.tribBoost ?? 0) + tb)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,13 @@ export function marketPrice(w: World, itemId: string): number {
|
|||||||
const mood = fam.reputation >= 40 ? 1.06 : fam.reputation >= 20 ? 1.02 : 0.98
|
const mood = fam.reputation >= 40 ? 1.06 : fam.reputation >= 20 ? 1.02 : 0.98
|
||||||
// 世界行情乘子(唯一读口:worldSim.marketMultFor;无 sim 时 1)
|
// 世界行情乘子(唯一读口:worldSim.marketMultFor;无 sim 时 1)
|
||||||
const simMult = w.state.worldSim ? new WorldSim(w).marketMultFor(itemId) : 1
|
const simMult = w.state.worldSim ? new WorldSim(w).marketMultFor(itemId) : 1
|
||||||
|
// 0.1.34 断供应急:货源连续告急(shortage ≥ 触发月数)→ 价格上浮(恐慌:供不应求者贵)
|
||||||
|
const shortN = w.state.worldSim?.shortage?.[itemId]
|
||||||
|
const shortMult = shortN && shortN >= worldNum('shortageMonths') ? worldNum('shortagePrice') : 1
|
||||||
// 利己(priceMult)与信誉修正
|
// 利己(priceMult)与信誉修正
|
||||||
const sellers = w.aliveMembers().filter((c) => traitBonuses(c).priceMult > 0).length
|
const sellers = w.aliveMembers().filter((c) => traitBonuses(c).priceMult > 0).length
|
||||||
const liarPct = Math.min(0.2, sellers * 0.04)
|
const liarPct = Math.min(0.2, sellers * 0.04)
|
||||||
return Math.max(1, Math.round(base * mult * mood * simMult * (1 - liarPct)))
|
return Math.max(1, Math.round(base * mult * mood * simMult * shortMult * (1 - liarPct)))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buyItem(w: World, itemId: string, count: number): boolean {
|
export function buyItem(w: World, itemId: string, count: number): boolean {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { WorldSimState, NpcDynamics, WORLDSIM, ERA_CONF, makeWorldSimState, NpcS
|
|||||||
import { pack } from '../../data/registry'
|
import { pack } from '../../data/registry'
|
||||||
import { ITEMS } from '../../data/items'
|
import { ITEMS } from '../../data/items'
|
||||||
import { npcById, getNpcDefs, registerNpcDef, unregisterNpcDef } from '../../data/npcs'
|
import { npcById, getNpcDefs, registerNpcDef, unregisterNpcDef } from '../../data/npcs'
|
||||||
|
import { allMissions, addMission, WONDER_TEMPLATES, type MissionDef } from '../../data/secrets'
|
||||||
import { MAJORS, MAJOR_ORDER } from '../../data/realms'
|
import { MAJORS, MAJOR_ORDER } from '../../data/realms'
|
||||||
|
|
||||||
const MARKET_IDS = ['lingcao', 'lingkuang', 'beastcore', 'lingyu', 'lingmu', 'shoupi', 'lingguo', 'pill-qiyuan', 'pill-ningyuan']
|
const MARKET_IDS = ['lingcao', 'lingkuang', 'beastcore', 'lingyu', 'lingmu', 'shoupi', 'lingguo', 'pill-qiyuan', 'pill-ningyuan']
|
||||||
@@ -16,7 +17,7 @@ export class WorldSim {
|
|||||||
if (!w.state.worldSim) w.state.worldSim = initSim(w)
|
if (!w.state.worldSim) w.state.worldSim = initSim(w)
|
||||||
const ws = w.state.worldSim as WorldSimState
|
const ws = w.state.worldSim as WorldSimState
|
||||||
if (!ws.secretQi || Object.keys(ws.secretQi).length === 0) {
|
if (!ws.secretQi || Object.keys(ws.secretQi).length === 0) {
|
||||||
const defs = pack().missions
|
const defs = allMissions()
|
||||||
ws.secretQi = {}
|
ws.secretQi = {}
|
||||||
for (const m of defs) ws.secretQi[m.id] = 50
|
for (const m of defs) ws.secretQi[m.id] = 50
|
||||||
}
|
}
|
||||||
@@ -127,7 +128,25 @@ export class WorldSim {
|
|||||||
// ---- A. 资源循环市场(世界供给/需求 + NPC 上桌 + 再平衡) ----
|
// ---- A. 资源循环市场(世界供给/需求 + NPC 上桌 + 再平衡) ----
|
||||||
worldBreath(this.w, s) // A2+A3:常驻供给与需求(潮汐乘化)
|
worldBreath(this.w, s) // A2+A3:常驻供给与需求(潮汐乘化)
|
||||||
npcTrade(this.w, s, rng.next()) // A4:NPC 按 sells/buys 与池交易
|
npcTrade(this.w, s, rng.next()) // A4:NPC 按 sells/buys 与池交易
|
||||||
driftMarket(s, rng.next()) // 波动项(保留噪声弹性)
|
driftMarket(s, rng.next(), this.w) // 波动项(保留噪声弹性)
|
||||||
|
|
||||||
|
// ---- B0. 新秘境灵机(0.1.34 万象归元:年首机率——乱世×2) ----
|
||||||
|
if (this.w.state.month === 1) {
|
||||||
|
const era2 = s.era === 'luanshi' ? 2 : 1
|
||||||
|
if (rng.chance(worldNum('newSecretChance') * era2)) {
|
||||||
|
const tpl = rng.pick(WONDER_TEMPLATES)
|
||||||
|
const seedS = rng.int(1000, 9999)
|
||||||
|
const def: MissionDef = {
|
||||||
|
...tpl,
|
||||||
|
id: `m-wonder-${seedS}`,
|
||||||
|
name: `${tpl.icon}地灵诏——「${'古幽玄凌'}${rng.int(1, 9)}」`
|
||||||
|
}
|
||||||
|
addMission(def)
|
||||||
|
s.secretQi[def.id] = Math.max(50, s.secretQi[def.id] ?? 50)
|
||||||
|
s.newsFeed.push({ year: this.w.state.year, month: 1, src: '天机', text: `${def.name} 现世——灵光冲霄,远近皆见。`, kind: 'annal' })
|
||||||
|
this.w.log('info', `【天下】有异宝出世:${def.name}。`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- B. NPC 演化(换代 + 关系网 + 互攻;B7) ----
|
// ---- B. NPC 演化(换代 + 关系网 + 互攻;B7) ----
|
||||||
evolveNpc(this.w, s, rng.next())
|
evolveNpc(this.w, s, rng.next())
|
||||||
@@ -282,7 +301,9 @@ export class WorldSim {
|
|||||||
sum += (s.marketPool[id] ?? base) / base
|
sum += (s.marketPool[id] ?? base) / base
|
||||||
n++
|
n++
|
||||||
}
|
}
|
||||||
return n > 0 ? sum / n : 1
|
const cold = n > 0 ? sum / n : 1
|
||||||
|
// 0.1.34 战争疲劳上浮:天下杀伐不止,寒气怨气凝而灵脉皆冷(打不动即荒年)
|
||||||
|
return cold - (s.warFatigue ?? 0) * worldNum('warFatigueTempK')
|
||||||
}
|
}
|
||||||
|
|
||||||
news(): WorldSimState['newsFeed'] {
|
news(): WorldSimState['newsFeed'] {
|
||||||
@@ -409,7 +430,12 @@ function npcTrade(w: World, s: WorldSimState, noise: number): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function driftMarket(s: WorldSimState, noise: number): void {
|
function itemNameOf(id: string): string {
|
||||||
|
const N: Record<string, string> = { lingcao: '灵草', lingkuang: '灵矿', beastcore: '兽核', lingyu: '灵玉', lingmu: '灵木', shoupi: '兽皮', lingguo: '灵果' }
|
||||||
|
return N[id] ?? id
|
||||||
|
}
|
||||||
|
|
||||||
|
function driftMarket(s: WorldSimState, noise: number, w: World): void {
|
||||||
let n = noise
|
let n = noise
|
||||||
// W3 材料生态相关:自然资源同向微漂(灵木/符链共享偏置——物产同源)
|
// W3 材料生态相关:自然资源同向微漂(灵木/符链共享偏置——物产同源)
|
||||||
const matBias = (noise - 0.5) * 0.012
|
const matBias = (noise - 0.5) * 0.012
|
||||||
@@ -423,6 +449,19 @@ function driftMarket(s: WorldSimState, noise: number): void {
|
|||||||
const isMaterial = ['lingyu', 'lingmu', 'shoupi', 'lingguo'].includes(id)
|
const isMaterial = ['lingyu', 'lingmu', 'shoupi', 'lingguo'].includes(id)
|
||||||
|
|
||||||
s.marketPool[id] = Math.max(base * WORLDSIM.poolFloorPct, cur + rebalance + drift + (isMaterial ? matBias * base : 0))
|
s.marketPool[id] = Math.max(base * WORLDSIM.poolFloorPct, cur + rebalance + drift + (isMaterial ? matBias * base : 0))
|
||||||
|
// 0.1.34 断供监测:货源紧张连续月计数(触发价上浮×世界广播)
|
||||||
|
const line = base * WORLDSIM.shortageLine
|
||||||
|
if (s.marketPool[id] < line) {
|
||||||
|
s.shortage = s.shortage ?? {}
|
||||||
|
s.shortage[id] = (s.shortage[id] ?? 0) + 1
|
||||||
|
if (s.shortage[id] === worldNum('shortageMonths')) {
|
||||||
|
s.shortageWarned = s.shortageWarned ?? {}
|
||||||
|
s.shortageWarned[id] = w.state.year
|
||||||
|
s.newsFeed.push({ year: w.state.year, month: 6, src: '行商', text: `${itemNameOf(id)}断货数月——坊市惜售,价腾三成。`, kind: 'annal' })
|
||||||
|
}
|
||||||
|
} else if (s.shortage?.[id]) {
|
||||||
|
delete s.shortage[id]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -569,6 +608,8 @@ function evolveNpc(w: World, s: WorldSimState, noise: number): void {
|
|||||||
const wr = otherDyn.relationsWithOthers[winner.id] ?? 0
|
const wr = otherDyn.relationsWithOthers[winner.id] ?? 0
|
||||||
otherDyn.relationsWithOthers[winner.id] = clamp(wr + 4, -100, 100)
|
otherDyn.relationsWithOthers[winner.id] = clamp(wr + 4, -100, 100)
|
||||||
}
|
}
|
||||||
|
// 0.1.34 战争疲劳:乱象(互攻/大劫)累积——世界温度上浮,乱世加速
|
||||||
|
s.warFatigue = clamp((s.warFatigue ?? 0) + worldNum('warFatigueRise'), 0, 1)
|
||||||
s.newsFeed.push({
|
s.newsFeed.push({
|
||||||
year, month: 1, src: winner.name,
|
year, month: 1, src: winner.name,
|
||||||
text: `${winner.name} 与 ${loser.name} 起衅——痛挫其锋,势力大动。`
|
text: `${winner.name} 与 ${loser.name} 起衅——痛挫其锋,势力大动。`
|
||||||
|
|||||||
@@ -52,6 +52,12 @@ export interface WorldSimState {
|
|||||||
overfluxYear?: number
|
overfluxYear?: number
|
||||||
/** 盟友求援(最近一次:互攻失利波及盟我之族) */
|
/** 盟友求援(最近一次:互攻失利波及盟我之族) */
|
||||||
distress?: { id: string; year: number; month: number }
|
distress?: { id: string; year: number; month: number }
|
||||||
|
/** 战争疲劳(0~1):NPC 互攻/玩家 raid 逐年累积——世界温度上浮,乱世加速 */
|
||||||
|
warFatigue?: number
|
||||||
|
/** 断供监测:货源id→连续低于警戒线月数(≥3 触发断供告警×1.5 价) */
|
||||||
|
shortage?: Record<string, number>
|
||||||
|
/** 断供已播年(id→年——仅始发广播,持续断供不刷屏) */
|
||||||
|
shortageWarned?: Record<string, number>
|
||||||
/** 天下史书(十年一鉴独立留档,免 newsFeed 裁剪;上限 80 页) */
|
/** 天下史书(十年一鉴独立留档,免 newsFeed 裁剪;上限 80 页) */
|
||||||
worldAnnals?: { year: number; text: string }[]
|
worldAnnals?: { year: number; text: string }[]
|
||||||
/** 世鉴(50/100 年宏观:开局 vs 现在/era 路径/最强家更替——独立数组零漂) */
|
/** 世鉴(50/100 年宏观:开局 vs 现在/era 路径/最强家更替——独立数组零漂) */
|
||||||
@@ -206,7 +212,14 @@ export const WORLDSIM = {
|
|||||||
calamities: ['旱灾', '涝灾', '蝗灾', '疫病', '兽潮', '寒潮'] as const,
|
calamities: ['旱灾', '涝灾', '蝗灾', '疫病', '兽潮', '寒潮'] as const,
|
||||||
npcEventChance: 0.05, // NPC 互攻年首诗签概率
|
npcEventChance: 0.05, // NPC 互攻年首诗签概率
|
||||||
newsEvery: 24, // 月
|
newsEvery: 24, // 月
|
||||||
newsKeep: 120
|
newsKeep: 120,
|
||||||
|
// —— 0.1.34 万象归元(战争疲劳/断供/新秘境) ——
|
||||||
|
warFatigueRise: 0.008, // 每次乱象(NPC 互攻)累积的战争疲劳
|
||||||
|
warFatigueTempK: 0.6, // 疲劳对世界温度的冷降幅度(打不动即荒年)
|
||||||
|
shortageLine: 0.7, // 断供警戒线:池/基准 < 此值起计连续月
|
||||||
|
shortageMonths: 3,
|
||||||
|
shortagePrice: 1.5, // 断供价乘
|
||||||
|
newSecretChance: 0.04 // 年首新秘境现身概率(乱世 ×2)
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CalamityName = string
|
export type CalamityName = string
|
||||||
|
|||||||
@@ -227,6 +227,8 @@ export interface GameState {
|
|||||||
era?: string
|
era?: string
|
||||||
eraStartYear?: number
|
eraStartYear?: number
|
||||||
overfluxYear?: number
|
overfluxYear?: number
|
||||||
|
warFatigue?: number
|
||||||
|
shortage?: Record<string, number>
|
||||||
distress?: { id: string; year: number; month: number }
|
distress?: { id: string; year: number; month: number }
|
||||||
worldAnnals?: { year: number; text: string }[]
|
worldAnnals?: { year: number; text: string }[]
|
||||||
newsFeed?: { year: number; month: number; src: string; text: string; itemId?: string; kind?: string }[]
|
newsFeed?: { year: number; month: number; src: string; text: string; itemId?: string; kind?: string }[]
|
||||||
|
|||||||
Reference in New Issue
Block a user