You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

669 lines
30 KiB
Go

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

package tuning
import (
"context"
"encoding/json"
"log/slog"
"sync/atomic"
"time"
)
// Values contains runtime-tunable gameplay knobs loaded from DB.
// Missing JSON fields keep default values.
type Values struct {
EncounterCooldownBaseMs int64 `json:"encounterCooldownBaseMs"`
EncounterActivityBase float64 `json:"encounterActivityBase"`
BaseMoveSpeed float64 `json:"baseMoveSpeed"`
MovementTickRateMs int64 `json:"movementTickRateMs"`
PositionSyncRateMs int64 `json:"positionSyncRateMs"`
TownRestMinMs int64 `json:"townRestMinMs"`
TownRestMaxMs int64 `json:"townRestMaxMs"`
TownRestHPPerS float64 `json:"townRestHpPerSecond"`
TownArrivalRadius float64 `json:"townArrivalRadius"`
TownNPCVisitChance float64 `json:"townNpcVisitChance"`
// TownNPCApproachChance: second roll after a visit timer fires — whether the hero commits to walking
// toward the next queued NPC. 1.0 = same as legacy (only TownNPCVisitChance gates approach).
TownNPCApproachChance float64 `json:"townNpcApproachChance"`
// TownNPCInteractChance: offline only — after reaching an NPC, probability of “using” services
// (buy potion, full heal, accept a quest) instead of walking past.
TownNPCInteractChance float64 `json:"townNpcInteractChance"`
TownNPCRollMinMs int64 `json:"townNpcRollMinMs"`
TownNPCRollMaxMs int64 `json:"townNpcRollMaxMs"`
TownNPCRetryMs int64 `json:"townNpcRetryMs"`
TownNPCPauseMs int64 `json:"townNpcPauseMs"`
// TownLastNpcLingerMs: after the final NPC in the tour, stand near them this long before walking to the plaza (shifted while shop/quest UI is open).
TownLastNpcLingerMs int64 `json:"townLastNpcLingerMs"`
TownNPCLogIntervalMs int64 `json:"townNpcLogIntervalMs"`
TownNPCWalkSpeed float64 `json:"townNpcWalkSpeed"`
// TownNPCStandoffWorld: hero stops this many world units short of the NPC tile (along approach).
TownNPCStandoffWorld float64 `json:"townNpcStandoffWorld"`
// TownAfterNPCRestChance: after the NPC tour, at town center — probability of a full town rest
// (same duration/regen as towns without NPCs). Otherwise only a short TownNPCPauseMs wait.
TownAfterNPCRestChance float64 `json:"townAfterNpcRestChance"`
// Town tour (ExcursionKindTown): attractor retarget cadence and P(stand near NPC vs random plaza wander).
TownTourWanderRetargetMinMs int64 `json:"townTourWanderRetargetMinMs"`
TownTourWanderRetargetMaxMs int64 `json:"townTourWanderRetargetMaxMs"`
TownTourNpcAttractorChance float64 `json:"townTourNpcAttractorChance"`
TownWelcomeDurationMs int64 `json:"townWelcomeDurationMs"`
TownServiceMaxMs int64 `json:"townServiceMaxMs"`
TownRestHpThreshold float64 `json:"townRestHpThreshold"`
TownRestChance float64 `json:"townRestChance"`
TownTourRestMinMs int64 `json:"townTourRestMinMs"`
TownTourRestMaxMs int64 `json:"townTourRestMaxMs"`
WanderingMerchantPromptTimeoutMs int64 `json:"wanderingMerchantPromptTimeoutMs"`
MerchantCostBase int64 `json:"merchantCostBase"`
MerchantCostPerLevel int64 `json:"merchantCostPerLevel"`
MerchantTownAutoSellShare float64 `json:"merchantTownAutoSellShare"`
MonsterEncounterWeightBase float64 `json:"monsterEncounterWeightBase"`
MonsterEncounterWeightWildBonus float64 `json:"monsterEncounterWeightWildBonus"`
MerchantEncounterWeightBase float64 `json:"merchantEncounterWeightBase"`
MerchantEncounterWeightRoadBonus float64 `json:"merchantEncounterWeightRoadBonus"`
LootChanceCommon float64 `json:"lootChanceCommon"`
LootChanceUncommon float64 `json:"lootChanceUncommon"`
LootChanceRare float64 `json:"lootChanceRare"`
LootChanceEpic float64 `json:"lootChanceEpic"`
LootChanceLegendary float64 `json:"lootChanceLegendary"`
GoldLootScale float64 `json:"goldLootScale"`
// GoldDropChance is P(at least one gold line) per kill before luck; rolled first, then rarity/amount.
GoldDropChance float64 `json:"goldDropChance"`
PotionDropChance float64 `json:"potionDropChance"`
EquipmentDropBase float64 `json:"equipmentDropBase"`
GoldCommonMin int64 `json:"goldCommonMin"`
GoldCommonMax int64 `json:"goldCommonMax"`
GoldUncommonMin int64 `json:"goldUncommonMin"`
GoldUncommonMax int64 `json:"goldUncommonMax"`
GoldRareMin int64 `json:"goldRareMin"`
GoldRareMax int64 `json:"goldRareMax"`
GoldEpicMin int64 `json:"goldEpicMin"`
GoldEpicMax int64 `json:"goldEpicMax"`
GoldLegendaryMin int64 `json:"goldLegendaryMin"`
GoldLegendaryMax int64 `json:"goldLegendaryMax"`
AutoSellCommon int64 `json:"autoSellCommon"`
AutoSellUncommon int64 `json:"autoSellUncommon"`
AutoSellRare int64 `json:"autoSellRare"`
AutoSellEpic int64 `json:"autoSellEpic"`
AutoSellLegendary int64 `json:"autoSellLegendary"`
RESTEncounterCooldownMs int64 `json:"restEncounterCooldownMs"`
RESTEncounterNPCChance float64 `json:"restEncounterNpcChance"`
NPCCostHeal int64 `json:"npcCostHeal"`
NPCCostPotion int64 `json:"npcCostPotion"`
NPCCostNearbyRadius float64 `json:"npcCostNearbyRadius"`
// MerchantTownGearCostBase / PerTownLevel: in-town merchant random gear (ilvl/rarity scale with town tier).
MerchantTownGearCostBase int64 `json:"merchantTownGearCostBase"`
MerchantTownGearCostPerTownLevel int64 `json:"merchantTownGearCostPerTownLevel"`
// MerchantTownStockCount: gear rows shown at in-town merchant (hard-capped small).
MerchantTownStockCount int `json:"merchantTownStockCount"`
// MerchantTownGearPricePerIlvl: gold multiplier for item level in town merchant pricing (before rarity and variance).
MerchantTownGearPricePerIlvl int64 `json:"merchantTownGearPricePerIlvl"`
// MerchantTownGearPriceVariancePct: uniform random ±% on the listed buy price (e.g. 15 → 85%115%).
MerchantTownGearPriceVariancePct int `json:"merchantTownGearPriceVariancePct"`
// QuestOffersPerNPC caps how many quest templates a quest_giver offers per interaction (after filtering taken quests).
QuestOffersPerNPC int `json:"questOffersPerNPC"`
// QuestOfferRefreshHours controls how often quest_giver offers rotate (hours).
QuestOfferRefreshHours int `json:"questOfferRefreshHours"`
// QuestOfferDrySpellChance is the probability (01) that a quest_giver returns no offers
// for a given hero/NPC/time bucket even when offerable templates exist. Deterministic per bucket.
QuestOfferDrySpellChance float64 `json:"questOfferDrySpellChance"`
CombatDamageScale float64 `json:"combatDamageScale"`
CombatDamageRollMin float64 `json:"combatDamageRollMin"`
CombatDamageRollMax float64 `json:"combatDamageRollMax"`
// EnemyCombatDamageScale / Roll* apply only when an enemy hits the hero (not hero→enemy).
EnemyCombatDamageScale float64 `json:"enemyCombatDamageScale"`
EnemyCombatDamageRollMin float64 `json:"enemyCombatDamageRollMin"`
EnemyCombatDamageRollMax float64 `json:"enemyCombatDamageRollMax"`
// EnemyAttackIntervalMultiplier applies only to enemy attack spacing (hero cadence unchanged). Pair with enemy damage scale for similar incoming DPS.
EnemyAttackIntervalMultiplier float64 `json:"enemyAttackIntervalMultiplier"`
EnemyDodgeChance float64 `json:"enemyDodgeChance"`
EnemyCriticalMinChance float64 `json:"enemyCriticalMinChance"`
EnemyCritChanceCap float64 `json:"enemyCritChanceCap"`
HeroCritChanceCap float64 `json:"heroCritChanceCap"`
HeroBlockChancePerDefense float64 `json:"heroBlockChancePerDefense"`
HeroBlockChanceCap float64 `json:"heroBlockChanceCap"`
EnemyBurstEveryN int64 `json:"enemyBurstEveryN"`
EnemyBurstMultiplier float64 `json:"enemyBurstMultiplier"`
EnemyChainEveryN int64 `json:"enemyChainEveryN"`
EnemyChainMultiplier float64 `json:"enemyChainMultiplier"`
// EnemyEncounterStatMultiplier scales enemy MaxHP/HP/Attack/Defense after template+level math (default 1.2 = +20%).
EnemyEncounterStatMultiplier float64 `json:"enemyEncounterStatMultiplier"`
// EnemyStatMultiplierVsUnequippedHero scales the same stats when the hero has no equipped items (default 0.75 = 25%).
EnemyStatMultiplierVsUnequippedHero float64 `json:"enemyStatMultiplierVsUnequippedHero"`
DebuffProcBurn float64 `json:"debuffProcBurn"`
DebuffProcPoison float64 `json:"debuffProcPoison"`
DebuffProcSlow float64 `json:"debuffProcSlow"`
DebuffProcStun float64 `json:"debuffProcStun"`
DebuffProcFreeze float64 `json:"debuffProcFreeze"`
DebuffProcIceSlow float64 `json:"debuffProcIceSlow"`
EnemyRegenDefault float64 `json:"enemyRegenDefault"`
EnemyRegenSkeletonKing float64 `json:"enemyRegenSkeletonKing"`
EnemyRegenForestWarden float64 `json:"enemyRegenForestWarden"`
EnemyRegenBattleLizard float64 `json:"enemyRegenBattleLizard"`
SummonCycleSeconds int64 `json:"summonCycleSeconds"`
SummonDamageDivisor int64 `json:"summonDamageDivisor"`
LuckBuffMultiplier float64 `json:"luckBuffMultiplier"`
MinAttackIntervalMs int64 `json:"minAttackIntervalMs"`
CombatPaceMultiplier int64 `json:"combatPaceMultiplier"`
PotionHealPercent float64 `json:"potionHealPercent"`
PotionAutoUseThreshold float64 `json:"potionAutoUseThreshold"`
ReviveHpPercent float64 `json:"reviveHpPercent"`
AutoReviveAfterMs int64 `json:"autoReviveAfterMs"`
XPCurveEarlyBase float64 `json:"xpCurveEarlyBase"`
XPCurveEarlyScale float64 `json:"xpCurveEarlyScale"`
XPCurveMidBase float64 `json:"xpCurveMidBase"`
XPCurveMidScale float64 `json:"xpCurveMidScale"`
XPCurveLateBase float64 `json:"xpCurveLateBase"`
XPCurveLateScale float64 `json:"xpCurveLateScale"`
LevelUpHPEvery int64 `json:"levelUpHpEvery"`
// LevelUpHpBase is added to MaxHP together with Constitution/6 when LevelUpHPEvery fires (spec §3.3 cadence).
LevelUpHpBase int `json:"levelUpHpBase"`
LevelUpATKEvery int64 `json:"levelUpAtkEvery"`
LevelUpDEFEvery int64 `json:"levelUpDefEvery"`
LevelUpSTREvery int64 `json:"levelUpStrEvery"`
LevelUpCONEvery int64 `json:"levelUpConEvery"`
LevelUpAGIEvery int64 `json:"levelUpAgiEvery"`
LevelUpLUCKEvery int64 `json:"levelUpLuckEvery"`
AgilityCoef float64 `json:"agilityCoef"`
MaxAttackSpeed float64 `json:"maxAttackSpeed"`
MinAttackSpeed float64 `json:"minAttackSpeed"`
// IlvlFactorSlope is deprecated; kept for backward-compatible payloads.
IlvlFactorSlope float64 `json:"ilvlFactorSlope"`
IlvlPerLevelMultiplier float64 `json:"ilvlPerLevelMultiplier"`
RarityMultiplierCommon float64 `json:"rarityMultiplierCommon"`
RarityMultiplierUncommon float64 `json:"rarityMultiplierUncommon"`
RarityMultiplierRare float64 `json:"rarityMultiplierRare"`
RarityMultiplierEpic float64 `json:"rarityMultiplierEpic"`
RarityMultiplierLegendary float64 `json:"rarityMultiplierLegendary"`
RollIlvlEliteBaseChance float64 `json:"rollIlvlEliteBaseChance"`
RollIlvlElitePlusOneChance float64 `json:"rollIlvlElitePlusOneChance"`
BuffChargePeriodMs int64 `json:"buffChargePeriodMs"`
FreeBuffActivationsPerPeriod int64 `json:"freeBuffActivationsPerPeriod"`
SubscriptionDurationMs int64 `json:"subscriptionDurationMs"`
SubscriptionWeeklyPriceRUB int64 `json:"subscriptionWeeklyPriceRub"`
BuffRefillPriceRUB int64 `json:"buffRefillPriceRub"`
ResurrectionRefillPriceRUB int64 `json:"resurrectionRefillPriceRub"`
MaxRevivesFree int64 `json:"maxRevivesFree"`
MaxRevivesSubscriber int64 `json:"maxRevivesSubscriber"`
EnemyScaleBandHP float64 `json:"enemyScaleBandHp"`
EnemyScaleOvercapHP float64 `json:"enemyScaleOvercapHp"`
EnemyScaleBandATK float64 `json:"enemyScaleBandAtk"`
EnemyScaleOvercapATK float64 `json:"enemyScaleOvercapAtk"`
EnemyScaleBandDEF float64 `json:"enemyScaleBandDef"`
EnemyScaleOvercapDEF float64 `json:"enemyScaleOvercapDef"`
EnemyScaleBandXP float64 `json:"enemyScaleBandXp"`
EnemyScaleOvercapXP float64 `json:"enemyScaleOvercapXp"`
EnemyScaleBandGold float64 `json:"enemyScaleBandGold"`
EnemyScaleOvercapGold float64 `json:"enemyScaleOvercapGold"`
AutoEquipThreshold float64 `json:"autoEquipThreshold"`
LootHistoryLimit int64 `json:"lootHistoryLimit"`
// --- Adventure / excursion (mini-adventure off-road) ---
// AdventureStartChance is the per-tick probability of starting an adventure while walking.
// With 500ms ticks and ~50% walking uptime, 0.0001 ≈ 3 adventures per 8 h.
AdventureStartChance float64 `json:"adventureStartChance"`
// AdventureCooldownMs is the minimum wall-time between two adventure sessions.
AdventureCooldownMs int64 `json:"adventureCooldownMs"`
// AdventureOutDurationMs is how long the "out" phase lasts (hero moves off-road into forest).
AdventureOutDurationMs int64 `json:"adventureOutDurationMs"`
// AdventureWildMinMs / AdventureWildMaxMs define the random range for the "wild" phase
// (encounters in the forest). Total adventure ≈ out + wild + return.
AdventureWildMinMs int64 `json:"adventureWildMinMs"`
AdventureWildMaxMs int64 `json:"adventureWildMaxMs"`
// AdventureReturnDurationMs is how long the "return" phase lasts (hero walks back to road).
AdventureReturnDurationMs int64 `json:"adventureReturnDurationMs"`
// AdventureDepthWorldUnits is the max perpendicular offset from road during an adventure.
AdventureDepthWorldUnits float64 `json:"adventureDepthWorldUnits"`
// AdventureEncounterCooldownMs is the encounter cooldown while in the wild/return phases.
AdventureEncounterCooldownMs int64 `json:"adventureEncounterCooldownMs"`
// AdventureReturnEncounterEnabled allows encounters during the return phase.
AdventureReturnEncounterEnabled bool `json:"adventureReturnEncounterEnabled"`
// AdventureReturnWildnessMin is the minimum wilderness factor (0..1) used during return.
AdventureReturnWildnessMin float64 `json:"adventureReturnWildnessMin"`
// AdventureDurationMinMs / AdventureDurationMaxMs: wall-time for the wandering phase (attractor model).
AdventureDurationMinMs int64 `json:"adventureDurationMinMs"`
AdventureDurationMaxMs int64 `json:"adventureDurationMaxMs"`
// AdventureWanderRadius: new random attractor within this distance of the hero (world units).
AdventureWanderRadius float64 `json:"adventureWanderRadius"`
// AdventureWanderRetargetMinMs / MaxMs: random interval between wander retarget rolls.
AdventureWanderRetargetMinMs int64 `json:"adventureWanderRetargetMinMs"`
AdventureWanderRetargetMaxMs int64 `json:"adventureWanderRetargetMaxMs"`
// ExcursionArrivalEpsilonWorld: hero is considered to have reached the attractor within this distance.
ExcursionArrivalEpsilonWorld float64 `json:"excursionArrivalEpsilonWorld"`
// --- HP-based rest triggers ---
// LowHpThreshold is the HP/MaxHP fraction below which rest may trigger (0..1).
LowHpThreshold float64 `json:"lowHpThreshold"`
// RoadsideRestExitHp is the HP/MaxHP fraction at which roadside rest ends early (0..1).
RoadsideRestExitHp float64 `json:"roadsideRestExitHp"`
// AdventureRestTargetHp is the HP/MaxHP fraction at which adventure inline rest ends (0..1).
AdventureRestTargetHp float64 `json:"adventureRestTargetHp"`
// RoadsideRestMinMs is the minimum duration for a roadside rest period.
RoadsideRestMinMs int64 `json:"roadsideRestMinMs"`
// RoadsideRestMaxMs is the maximum duration for a roadside rest period.
RoadsideRestMaxMs int64 `json:"roadsideRestMaxMs"`
// RoadsideRestHpPerS is the HP/MaxHP fraction healed per second during roadside rest.
RoadsideRestHpPerS float64 `json:"roadsideRestHpPerSecond"`
// AdventureRestHpPerS is the HP/MaxHP fraction healed per second during adventure inline rest.
AdventureRestHpPerS float64 `json:"adventureRestHpPerSecond"`
// RoadsideRestDepthWorldUnits is the perpendicular offset from road during roadside rest.
RoadsideRestDepthWorldUnits float64 `json:"roadsideRestDepthWorldUnits"`
// --- Hero meet (paired social encounter) ---
// HeroMeetStandHalfOffsetWorld is half the lateral gap between the two heroes at the meet stand (world X from midpoint).
// Large values push the partner off-screen: isometric projection scales ~1 world unit to tens of pixels.
HeroMeetStandHalfOffsetWorld float64 `json:"heroMeetStandHalfOffsetWorld"`
// HeroMeetAdminSnapSeparationWorld: admin-started meet teleports the other hero to this distance from the primary (world units).
HeroMeetAdminSnapSeparationWorld float64 `json:"heroMeetAdminSnapSeparationWorld"`
HeroMeetRadiusWorld float64 `json:"heroMeetRadiusWorld"`
HeroMeetChancePerTick float64 `json:"heroMeetChancePerTick"`
HeroMeetCooldownMs int64 `json:"heroMeetCooldownMs"`
HeroMeetOfflineMinMs int64 `json:"heroMeetOfflineMinMs"`
HeroMeetOfflineMaxMs int64 `json:"heroMeetOfflineMaxMs"`
HeroMeetPromptWindowMs int64 `json:"heroMeetPromptWindowMs"`
HeroMeetAutoLineIntervalMs int64 `json:"heroMeetAutoLineIntervalMs"`
HeroMeetPartnerLingerMs int64 `json:"heroMeetPartnerLingerMs"`
HeroMeetMessageMaxRunes int `json:"heroMeetMessageMaxRunes"`
HeroMeetMessageCooldownMs int64 `json:"heroMeetMessageCooldownMs"`
}
func DefaultValues() Values {
return Values{
EncounterCooldownBaseMs: 12_000,
EncounterActivityBase: 0.035,
BaseMoveSpeed: 2.0,
MovementTickRateMs: 500,
PositionSyncRateMs: 10_000,
TownRestMinMs: 5 * 60 * 1000,
TownRestMaxMs: 20 * 60 * 1000,
TownRestHPPerS: 0.002,
TownArrivalRadius: 0.5,
TownNPCVisitChance: 0.78,
TownNPCApproachChance: 1.0,
TownNPCInteractChance: 0.65,
TownNPCRollMinMs: 800,
TownNPCRollMaxMs: 2600,
TownNPCRetryMs: 450,
TownNPCPauseMs: 30_000,
TownLastNpcLingerMs: 10_000,
TownNPCLogIntervalMs: 5_000,
TownNPCWalkSpeed: 3.0,
TownNPCStandoffWorld: 0.65,
TownAfterNPCRestChance: 0.78,
TownTourWanderRetargetMinMs: 5_000,
TownTourWanderRetargetMaxMs: 14_000,
TownTourNpcAttractorChance: 0.14,
TownWelcomeDurationMs: 30_000,
TownServiceMaxMs: 240_000,
TownRestHpThreshold: 0.8,
TownRestChance: 0.7,
TownTourRestMinMs: 240_000,
TownTourRestMaxMs: 360_000,
WanderingMerchantPromptTimeoutMs: 15_000,
MerchantCostBase: 900,
MerchantCostPerLevel: 5,
MerchantTownAutoSellShare: 0.30,
MonsterEncounterWeightBase: 0.62,
MonsterEncounterWeightWildBonus: 0.18,
MerchantEncounterWeightBase: 0.02,
MerchantEncounterWeightRoadBonus: 0.05,
LootChanceCommon: 0.40,
LootChanceUncommon: 0.10,
LootChanceRare: 0.02,
LootChanceEpic: 0.003,
LootChanceLegendary: 0.0005,
GoldLootScale: 0.62,
GoldDropChance: 0.92,
PotionDropChance: 0.06,
EquipmentDropBase: 0.20,
GoldCommonMin: 0,
GoldCommonMax: 5,
GoldUncommonMin: 6,
GoldUncommonMax: 20,
GoldRareMin: 21,
GoldRareMax: 50,
GoldEpicMin: 51,
GoldEpicMax: 120,
GoldLegendaryMin: 121,
GoldLegendaryMax: 300,
AutoSellCommon: 3,
AutoSellUncommon: 8,
AutoSellRare: 20,
AutoSellEpic: 60,
AutoSellLegendary: 180,
RESTEncounterCooldownMs: 16_000,
RESTEncounterNPCChance: 0.10,
NPCCostHeal: 100,
NPCCostPotion: 50,
NPCCostNearbyRadius: 3.0,
MerchantTownGearCostBase: 180,
MerchantTownGearCostPerTownLevel: 40,
MerchantTownStockCount: 3,
MerchantTownGearPricePerIlvl: 115,
MerchantTownGearPriceVariancePct: 15,
QuestOffersPerNPC: 2,
QuestOfferRefreshHours: 2,
QuestOfferDrySpellChance: 0.20,
// combatDamageScale tracks combatPaceMultiplier: DPS ~ scale/pace, so halving pace halves scale to keep fight length.
CombatDamageScale: 0.216,
CombatDamageRollMin: 0.60,
CombatDamageRollMax: 1.10,
EnemyCombatDamageScale: DefaultEnemyCombatDamageScale,
EnemyCombatDamageRollMin: DefaultEnemyCombatDamageRollMin,
EnemyCombatDamageRollMax: DefaultEnemyCombatDamageRollMax,
EnemyAttackIntervalMultiplier: DefaultEnemyAttackIntervalMultiplier,
EnemyDodgeChance: 0.14,
EnemyCriticalMinChance: 0.10,
EnemyCritChanceCap: 0.20,
HeroCritChanceCap: 0.12,
HeroBlockChancePerDefense: 0.0025,
HeroBlockChanceCap: 0.20,
EnemyBurstEveryN: 3,
EnemyBurstMultiplier: 1.5,
EnemyChainEveryN: 6,
EnemyChainMultiplier: 3.0,
EnemyEncounterStatMultiplier: 1.2,
EnemyStatMultiplierVsUnequippedHero: 0.85,
DebuffProcBurn: 0.18,
DebuffProcPoison: 0.10,
DebuffProcSlow: 0.25,
DebuffProcStun: 0.25,
DebuffProcFreeze: 0.20,
DebuffProcIceSlow: 0.20,
EnemyRegenDefault: DefaultEnemyRegenDefault,
EnemyRegenSkeletonKing: DefaultEnemyRegenSkeletonKing,
EnemyRegenForestWarden: DefaultEnemyRegenForestWarden,
EnemyRegenBattleLizard: DefaultEnemyRegenBattleLizard,
SummonCycleSeconds: 18,
SummonDamageDivisor: 10,
// Spec §7.1 luck ×2.5, weakened by ⅓ → ×(5/3) on drop chances and gold amount when gold drops.
LuckBuffMultiplier: 5.0 / 3.0,
MinAttackIntervalMs: 250,
CombatPaceMultiplier: 14,
PotionHealPercent: 0.30,
PotionAutoUseThreshold: 0.30,
ReviveHpPercent: 0.50,
AutoReviveAfterMs: int64(time.Hour / time.Millisecond),
XPCurveEarlyBase: 100,
XPCurveEarlyScale: 1.5,
XPCurveMidBase: 2947,
XPCurveMidScale: 1.15,
XPCurveLateBase: 48232,
XPCurveLateScale: 1.10,
LevelUpHPEvery: 4,
LevelUpHpBase: 10,
LevelUpATKEvery: 3,
LevelUpDEFEvery: 3,
LevelUpSTREvery: 2,
LevelUpCONEvery: 2,
LevelUpAGIEvery: 2,
LevelUpLUCKEvery: 5,
AgilityCoef: 0.03,
MaxAttackSpeed: 4.0,
MinAttackSpeed: 0.1,
IlvlFactorSlope: 0.03,
IlvlPerLevelMultiplier: 1.10,
RarityMultiplierCommon: 1.00,
RarityMultiplierUncommon: 1.0877573,
RarityMultiplierRare: 1.1832160,
RarityMultiplierEpic: 1.2870518,
RarityMultiplierLegendary: 1.40,
RollIlvlEliteBaseChance: 0.4,
RollIlvlElitePlusOneChance: 0.4,
BuffChargePeriodMs: 24 * 60 * 60 * 1000,
FreeBuffActivationsPerPeriod: 2,
SubscriptionDurationMs: 7 * 24 * 60 * 60 * 1000,
SubscriptionWeeklyPriceRUB: 299,
BuffRefillPriceRUB: 50,
ResurrectionRefillPriceRUB: 150,
MaxRevivesFree: 1,
MaxRevivesSubscriber: 2,
EnemyScaleBandHP: 0.062,
EnemyScaleOvercapHP: 0.031,
EnemyScaleBandATK: 0.044,
EnemyScaleOvercapATK: 0.024,
EnemyScaleBandDEF: 0.038,
EnemyScaleOvercapDEF: 0.020,
EnemyScaleBandXP: 0.05,
EnemyScaleOvercapXP: 0.03,
EnemyScaleBandGold: 0.05,
EnemyScaleOvercapGold: 0.025,
AutoEquipThreshold: 1.03,
LootHistoryLimit: 50,
AdventureStartChance: 0.0001,
AdventureCooldownMs: 300_000,
AdventureOutDurationMs: 20_000,
AdventureWildMinMs: 560_000,
AdventureWildMaxMs: 2_960_000,
AdventureReturnDurationMs: 20_000,
AdventureDepthWorldUnits: 40.0,
AdventureEncounterCooldownMs: 6_000,
AdventureReturnEncounterEnabled: true,
AdventureReturnWildnessMin: 0.35,
AdventureDurationMinMs: 560_000,
AdventureDurationMaxMs: 2_960_000,
AdventureWanderRadius: 18.0,
AdventureWanderRetargetMinMs: 4_000,
AdventureWanderRetargetMaxMs: 14_000,
ExcursionArrivalEpsilonWorld: 0.35,
LowHpThreshold: 0.25,
RoadsideRestExitHp: 0.85,
AdventureRestTargetHp: 0.85,
RoadsideRestMinMs: 240_000,
RoadsideRestMaxMs: 600_000,
RoadsideRestHpPerS: 0.003,
AdventureRestHpPerS: 0.004,
RoadsideRestDepthWorldUnits: 12.0,
HeroMeetStandHalfOffsetWorld: 0.9,
HeroMeetAdminSnapSeparationWorld: 12.0,
HeroMeetRadiusWorld: 120.0,
HeroMeetChancePerTick: 0.0004,
HeroMeetCooldownMs: 300_000,
HeroMeetOfflineMinMs: 4 * 60 * 1000,
HeroMeetOfflineMaxMs: 6 * 60 * 1000,
HeroMeetPromptWindowMs: 20_000,
HeroMeetAutoLineIntervalMs: 10_000,
HeroMeetPartnerLingerMs: 20_000,
HeroMeetMessageMaxRunes: 140,
HeroMeetMessageCooldownMs: 3000,
}
}
var current atomic.Value
func init() {
v := DefaultValues()
current.Store(&v)
}
func Get() Values {
p := current.Load().(*Values)
return *p
}
// EffectiveNPCShopCosts returns potion and full-heal prices from runtime tuning (DB-merged JSON),
// falling back to defaults when unset or non-positive.
func EffectiveNPCShopCosts() (potionCost, healCost int64) {
cfg := Get()
potionCost = cfg.NPCCostPotion
if potionCost <= 0 {
potionCost = DefaultValues().NPCCostPotion
}
healCost = cfg.NPCCostHeal
if healCost <= 0 {
healCost = DefaultValues().NPCCostHeal
}
return potionCost, healCost
}
// EffectiveTownMerchantGearCost returns a town-tier gold anchor (used in merchant pricing and legacy paths).
const merchantTownStockHardMax = 3
// EffectiveMerchantTownStockCount returns how many gear offers to roll at the town merchant (max 3).
func EffectiveMerchantTownStockCount() int {
n := Get().MerchantTownStockCount
if n <= 0 {
n = DefaultValues().MerchantTownStockCount
}
if n > merchantTownStockHardMax {
n = merchantTownStockHardMax
}
return n
}
func EffectiveTownMerchantGearCost(townLevel int) int64 {
cfg := Get()
base := cfg.MerchantTownGearCostBase
if base <= 0 {
base = DefaultValues().MerchantTownGearCostBase
}
per := cfg.MerchantTownGearCostPerTownLevel
if per < 0 {
per = DefaultValues().MerchantTownGearCostPerTownLevel
}
if townLevel < 1 {
townLevel = 1
}
return base + int64(townLevel)*per
}
// EffectiveMerchantTownGearPricePerIlvl returns the peritem-level gold factor for town merchant offers.
func EffectiveMerchantTownGearPricePerIlvl() int64 {
cfg := Get()
v := cfg.MerchantTownGearPricePerIlvl
if v <= 0 {
v = DefaultValues().MerchantTownGearPricePerIlvl
}
return v
}
// EffectiveMerchantTownGearPriceVariancePct returns ±% jitter (clamped) for town merchant prices.
func EffectiveMerchantTownGearPriceVariancePct() int {
cfg := Get()
v := cfg.MerchantTownGearPriceVariancePct
d := DefaultValues().MerchantTownGearPriceVariancePct
if v < 0 || v > 45 {
if d < 0 {
d = 15
}
return d
}
return v
}
// EffectiveQuestOffersPerNPC returns the max quest offers per quest_giver interaction from runtime tuning.
func EffectiveQuestOffersPerNPC() int {
n := Get().QuestOffersPerNPC
if n <= 0 {
return DefaultValues().QuestOffersPerNPC
}
return n
}
// EffectiveQuestOfferRefreshHours returns the rotation cadence (hours) for quest_giver offers.
func EffectiveQuestOfferRefreshHours() int {
n := Get().QuestOfferRefreshHours
if n <= 0 {
return DefaultValues().QuestOfferRefreshHours
}
return n
}
// EffectiveQuestOfferDrySpellChance returns P(no offers) when templates exist (01). Invalid values fall back to default.
func EffectiveQuestOfferDrySpellChance() float64 {
c := Get().QuestOfferDrySpellChance
if c < 0 || c > 1 {
return DefaultValues().QuestOfferDrySpellChance
}
return c
}
func effectiveRegenPerSecond(cfg float64, fallback float64) float64 {
if cfg <= 0 {
return fallback
}
return cfg
}
// EffectiveEnemyRegenDefault returns enemy regen rate (MaxHP per second) from runtime config with code fallback.
func EffectiveEnemyRegenDefault() float64 {
return effectiveRegenPerSecond(Get().EnemyRegenDefault, DefaultEnemyRegenDefault)
}
// EffectiveEnemyRegenSkeletonKing returns Skeleton King regen rate (MaxHP per second) from runtime config with code fallback.
func EffectiveEnemyRegenSkeletonKing() float64 {
return effectiveRegenPerSecond(Get().EnemyRegenSkeletonKing, DefaultEnemyRegenSkeletonKing)
}
// EffectiveEnemyRegenForestWarden returns Forest Warden regen rate from runtime config with code fallback.
func EffectiveEnemyRegenForestWarden() float64 {
return effectiveRegenPerSecond(Get().EnemyRegenForestWarden, DefaultEnemyRegenForestWarden)
}
// EffectiveEnemyRegenBattleLizard returns Battle Lizard regen rate from runtime config with code fallback.
func EffectiveEnemyRegenBattleLizard() float64 {
return effectiveRegenPerSecond(Get().EnemyRegenBattleLizard, DefaultEnemyRegenBattleLizard)
}
// EffectiveEnemyAttackIntervalMultiplier returns the factor applied only to enemy attack intervals (>=1 = slower enemy swings).
func EffectiveEnemyAttackIntervalMultiplier() float64 {
m := Get().EnemyAttackIntervalMultiplier
if m <= 0 {
return DefaultEnemyAttackIntervalMultiplier
}
return m
}
func Set(v Values) {
current.Store(&v)
}
type PayloadLoader interface {
LoadRuntimeConfigPayload(ctx context.Context) ([]byte, error)
}
func ReloadNow(ctx context.Context, logger *slog.Logger, loader PayloadLoader) error {
payload, err := loader.LoadRuntimeConfigPayload(ctx)
if err != nil {
if logger != nil {
logger.Warn("runtime config reload failed", "error", err)
}
return err
}
next := DefaultValues()
if len(payload) > 0 {
if err := json.Unmarshal(payload, &next); err != nil {
if logger != nil {
logger.Warn("runtime config payload parse failed", "error", err)
}
return err
}
}
Set(next)
return nil
}