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.

390 lines
16 KiB
Go

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"`
TownNPCRollMinMs int64 `json:"townNpcRollMinMs"`
TownNPCRollMaxMs int64 `json:"townNpcRollMaxMs"`
TownNPCRetryMs int64 `json:"townNpcRetryMs"`
TownNPCPauseMs int64 `json:"townNpcPauseMs"`
TownNPCLogIntervalMs int64 `json:"townNpcLogIntervalMs"`
TownNPCWalkSpeed float64 `json:"townNpcWalkSpeed"`
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"`
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"`
CombatDamageScale float64 `json:"combatDamageScale"`
EnemyDodgeChance float64 `json:"enemyDodgeChance"`
EnemyCriticalMinChance float64 `json:"enemyCriticalMinChance"`
EnemyBurstEveryN int64 `json:"enemyBurstEveryN"`
EnemyBurstMultiplier float64 `json:"enemyBurstMultiplier"`
EnemyChainEveryN int64 `json:"enemyChainEveryN"`
EnemyChainMultiplier float64 `json:"enemyChainMultiplier"`
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"`
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 float64 `json:"ilvlFactorSlope"`
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"`
// --- 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"`
}
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,
TownNPCRollMinMs: 800,
TownNPCRollMaxMs: 2600,
TownNPCRetryMs: 450,
TownNPCPauseMs: 30_000,
TownNPCLogIntervalMs: 5_000,
TownNPCWalkSpeed: 3.0,
WanderingMerchantPromptTimeoutMs: 15_000,
MerchantCostBase: 20,
MerchantCostPerLevel: 5,
MerchantTownAutoSellShare: 0.30,
MonsterEncounterWeightBase: 0.62,
MonsterEncounterWeightWildBonus: 0.18,
MerchantEncounterWeightBase: 0.04,
MerchantEncounterWeightRoadBonus: 0.10,
LootChanceCommon: 0.40,
LootChanceUncommon: 0.10,
LootChanceRare: 0.02,
LootChanceEpic: 0.003,
LootChanceLegendary: 0.0005,
GoldLootScale: 0.5,
PotionDropChance: 0.05,
EquipmentDropBase: 0.15,
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,
CombatDamageScale: 0.35,
EnemyDodgeChance: 0.20,
EnemyCriticalMinChance: 0.15,
EnemyBurstEveryN: 3,
EnemyBurstMultiplier: 1.5,
EnemyChainEveryN: 6,
EnemyChainMultiplier: 3.0,
DebuffProcBurn: 0.30,
DebuffProcPoison: 0.10,
DebuffProcSlow: 0.25,
DebuffProcStun: 0.25,
DebuffProcFreeze: 0.20,
DebuffProcIceSlow: 0.20,
EnemyRegenDefault: 0.02,
EnemyRegenSkeletonKing: 0.10,
EnemyRegenForestWarden: 0.05,
EnemyRegenBattleLizard: 0.02,
SummonCycleSeconds: 15,
SummonDamageDivisor: 4,
LuckBuffMultiplier: 1.75,
MinAttackIntervalMs: 250,
CombatPaceMultiplier: 5,
PotionHealPercent: 0.30,
PotionAutoUseThreshold: 0.30,
ReviveHpPercent: 0.50,
AutoReviveAfterMs: int64(time.Hour / time.Millisecond),
XPCurveEarlyBase: 180,
XPCurveEarlyScale: 1.28,
XPCurveMidBase: 1450,
XPCurveMidScale: 1.15,
XPCurveLateBase: 23000,
XPCurveLateScale: 1.10,
LevelUpHPEvery: 10,
LevelUpATKEvery: 30,
LevelUpDEFEvery: 30,
LevelUpSTREvery: 40,
LevelUpCONEvery: 50,
LevelUpAGIEvery: 60,
LevelUpLUCKEvery: 100,
AgilityCoef: 0.03,
MaxAttackSpeed: 4.0,
MinAttackSpeed: 0.1,
IlvlFactorSlope: 0.03,
RarityMultiplierCommon: 1.00,
RarityMultiplierUncommon: 1.12,
RarityMultiplierRare: 1.30,
RarityMultiplierEpic: 1.52,
RarityMultiplierLegendary: 1.78,
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.05,
EnemyScaleOvercapHP: 0.025,
EnemyScaleBandATK: 0.035,
EnemyScaleOvercapATK: 0.018,
EnemyScaleBandDEF: 0.035,
EnemyScaleOvercapDEF: 0.018,
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,
LowHpThreshold: 0.25,
RoadsideRestExitHp: 0.70,
AdventureRestTargetHp: 0.70,
RoadsideRestMinMs: 240_000,
RoadsideRestMaxMs: 600_000,
RoadsideRestHpPerS: 0.003,
AdventureRestHpPerS: 0.004,
RoadsideRestDepthWorldUnits: 12.0,
}
}
var current atomic.Value
func init() {
v := DefaultValues()
current.Store(&v)
}
func Get() Values {
p := current.Load().(*Values)
return *p
}
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
}