monsters rebalance

master
Denis Ranneft 1 month ago
parent 2f00103b90
commit ddb5a3a2c4

@ -21,7 +21,8 @@ func ensureTestEnemyTemplates() {
HPPerLevel: 5,
AttackPerLevel: 1.5,
DefensePerLevel: 1.0,
XPReward: 10,
XPPerLevel: 2,
XPReward: 1,
GoldReward: 5,
},
model.EnemyBoar: {
@ -38,7 +39,8 @@ func ensureTestEnemyTemplates() {
HPPerLevel: 6,
AttackPerLevel: 1.8,
DefensePerLevel: 1.3,
XPReward: 14,
XPPerLevel: 2,
XPReward: 1,
GoldReward: 8,
},
})

@ -535,7 +535,13 @@ func BuildEnemyInstanceForLevel(tmpl model.Enemy, level int) model.Enemy {
picked.HP = picked.MaxHP
picked.Attack = max(1, int(math.Round(float64(picked.Attack)+levelDelta*picked.AttackPerLevel)))
picked.Defense = max(0, int(math.Round(float64(picked.Defense)+levelDelta*picked.DefensePerLevel)))
picked.XPReward = max(1, int64(math.Round(float64(picked.XPReward)+levelDelta*picked.XPPerLevel)))
xpPerLevel := picked.XPPerLevel
// Keep early-game kill cadence predictable (~1 XP from template base for normal mobs);
// xp_per_level ramps from instance level 10+ (and always applies to elites).
if level < 10 && !picked.IsElite {
xpPerLevel = 0
}
picked.XPReward = max(1, int64(math.Round(float64(picked.XPReward)+levelDelta*xpPerLevel)))
picked.GoldReward = max(0, int64(math.Round(float64(picked.GoldReward)+levelDelta*picked.GoldPerLevel)))
return picked
}

@ -56,9 +56,9 @@ func TestSimulateOneFight_HeroDies(t *testing.T) {
}
func TestSimulateOneFight_LevelUp(t *testing.T) {
// Seed XP just below L1->L2 threshold (180 in v3).
// Seed XP just below L1->L2 threshold (100 XP with default tuning).
hero := &model.Hero{
Level: 1, XP: 179,
Level: 1, XP: 99,
MaxHP: 10000, HP: 10000,
Attack: 100, Defense: 60, Speed: 1.0,
Strength: 10, Constitution: 10, Agility: 10, Luck: 5,
@ -75,7 +75,30 @@ func TestSimulateOneFight_LevelUp(t *testing.T) {
t.Fatal("expected XP gain")
}
if hero.Level < 2 {
t.Fatalf("expected level 2+ after gaining %d XP from 179 base, got level %d", xpGained, hero.Level)
t.Fatalf("expected level 2+ after gaining %d XP from 99 base, got level %d", xpGained, hero.Level)
}
}
func TestBuildEnemyInstanceForLevel_XPPerLevelRampsFrom10(t *testing.T) {
tmpl := model.Enemy{
BaseLevel: 1,
XPReward: 1,
XPPerLevel: 4,
IsElite: false,
}
early := BuildEnemyInstanceForLevel(tmpl, 6)
if early.XPReward != 1 {
t.Fatalf("normal mob instance L6: want base XP only (no per-level ramp), got %d", early.XPReward)
}
mid := BuildEnemyInstanceForLevel(tmpl, 12)
if mid.XPReward <= 1 {
t.Fatalf("normal mob instance L12: want xp_per_level applied, got %d", mid.XPReward)
}
elite := tmpl
elite.IsElite = true
el := BuildEnemyInstanceForLevel(elite, 5)
if el.XPReward <= 1 {
t.Fatalf("elite instance L5: want xp_per_level even before 10, got %d", el.XPReward)
}
}

@ -86,12 +86,12 @@ type BuffChargeState struct {
}
// XPToNextLevel returns the XP delta required to advance from the given level
// to level+1. Phase-based curve (spec §9) — v3 scales bases ×10 vs v2 for ~10×
// slower leveling when paired with reduced kill XP:
// to level+1. Early band uses a nonlinear step (~100 kills at 1 XP/kill for L1→2,
// ~150 for L2→3, ~225 for L3→4 with defaults). Mid/late bands use tuning bases.
//
// L 19: round(180 * 1.28^(L-1))
// L 1029: round(1450 * 1.15^(L-10))
// L 30+: round(23000 * 1.10^(L-30))
// L 19: round(earlyBase * earlyScale^(L-1))
// L 1029: round(midBase * midScale^(L-10))
// L 30+: round(lateBase * lateScale^(L-30))
func XPToNextLevel(level int) int64 {
cfg := tuning.Get()
if level < 1 {

@ -304,17 +304,21 @@ func TestProgressionV3CanonicalSnapshots(t *testing.T) {
}
func TestXPToNextLevelFormula(t *testing.T) {
if got := XPToNextLevel(1); got != 180 {
t.Fatalf("XPToNextLevel(1) = %d, want 180", got)
// Early: ~100 / 150 / 225 kills at 1 XP per kill (nonlinear 1.5× per level band).
if got := XPToNextLevel(1); got != 100 {
t.Fatalf("XPToNextLevel(1) = %d, want 100", got)
}
if got := XPToNextLevel(2); got != 230 {
t.Fatalf("XPToNextLevel(2) = %d, want 230", got)
if got := XPToNextLevel(2); got != 150 {
t.Fatalf("XPToNextLevel(2) = %d, want 150", got)
}
if got := XPToNextLevel(10); got != 1450 {
t.Fatalf("XPToNextLevel(10) = %d, want 1450", got)
if got := XPToNextLevel(3); got != 225 {
t.Fatalf("XPToNextLevel(3) = %d, want 225", got)
}
if got := XPToNextLevel(30); got != 23000 {
t.Fatalf("XPToNextLevel(30) = %d, want 23000", got)
if got := XPToNextLevel(10); got != 2947 {
t.Fatalf("XPToNextLevel(10) = %d, want 2947", got)
}
if got := XPToNextLevel(30); got != 48232 {
t.Fatalf("XPToNextLevel(30) = %d, want 48232", got)
}
}

@ -264,10 +264,10 @@ func DefaultValues() Values {
LootChanceRare: 0.02,
LootChanceEpic: 0.003,
LootChanceLegendary: 0.0005,
GoldLootScale: 0.5,
GoldDropChance: 0.90,
PotionDropChance: 0.05,
EquipmentDropBase: 0.15,
GoldLootScale: 0.62,
GoldDropChance: 0.92,
PotionDropChance: 0.06,
EquipmentDropBase: 0.20,
GoldCommonMin: 0,
GoldCommonMax: 5,
GoldUncommonMin: 6,
@ -327,11 +327,11 @@ func DefaultValues() Values {
PotionAutoUseThreshold: 0.30,
ReviveHpPercent: 0.50,
AutoReviveAfterMs: int64(time.Hour / time.Millisecond),
XPCurveEarlyBase: 180,
XPCurveEarlyScale: 1.28,
XPCurveMidBase: 1450,
XPCurveEarlyBase: 100,
XPCurveEarlyScale: 1.5,
XPCurveMidBase: 2947,
XPCurveMidScale: 1.15,
XPCurveLateBase: 23000,
XPCurveLateBase: 48232,
XPCurveLateScale: 1.10,
LevelUpHPEvery: 4,
LevelUpHpBase: 10,

Loading…
Cancel
Save