monsters rebalance

master
Denis Ranneft 1 month ago
parent 2f00103b90
commit ddb5a3a2c4

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

@ -535,7 +535,13 @@ func BuildEnemyInstanceForLevel(tmpl model.Enemy, level int) model.Enemy {
picked.HP = picked.MaxHP picked.HP = picked.MaxHP
picked.Attack = max(1, int(math.Round(float64(picked.Attack)+levelDelta*picked.AttackPerLevel))) 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.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))) picked.GoldReward = max(0, int64(math.Round(float64(picked.GoldReward)+levelDelta*picked.GoldPerLevel)))
return picked return picked
} }

@ -56,9 +56,9 @@ func TestSimulateOneFight_HeroDies(t *testing.T) {
} }
func TestSimulateOneFight_LevelUp(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{ hero := &model.Hero{
Level: 1, XP: 179, Level: 1, XP: 99,
MaxHP: 10000, HP: 10000, MaxHP: 10000, HP: 10000,
Attack: 100, Defense: 60, Speed: 1.0, Attack: 100, Defense: 60, Speed: 1.0,
Strength: 10, Constitution: 10, Agility: 10, Luck: 5, Strength: 10, Constitution: 10, Agility: 10, Luck: 5,
@ -75,7 +75,30 @@ func TestSimulateOneFight_LevelUp(t *testing.T) {
t.Fatal("expected XP gain") t.Fatal("expected XP gain")
} }
if hero.Level < 2 { 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 // 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× // to level+1. Early band uses a nonlinear step (~100 kills at 1 XP/kill for L1→2,
// slower leveling when paired with reduced kill XP: // ~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 19: round(earlyBase * earlyScale^(L-1))
// L 1029: round(1450 * 1.15^(L-10)) // L 1029: round(midBase * midScale^(L-10))
// L 30+: round(23000 * 1.10^(L-30)) // L 30+: round(lateBase * lateScale^(L-30))
func XPToNextLevel(level int) int64 { func XPToNextLevel(level int) int64 {
cfg := tuning.Get() cfg := tuning.Get()
if level < 1 { if level < 1 {

@ -304,17 +304,21 @@ func TestProgressionV3CanonicalSnapshots(t *testing.T) {
} }
func TestXPToNextLevelFormula(t *testing.T) { func TestXPToNextLevelFormula(t *testing.T) {
if got := XPToNextLevel(1); got != 180 { // Early: ~100 / 150 / 225 kills at 1 XP per kill (nonlinear 1.5× per level band).
t.Fatalf("XPToNextLevel(1) = %d, want 180", got) if got := XPToNextLevel(1); got != 100 {
t.Fatalf("XPToNextLevel(1) = %d, want 100", got)
} }
if got := XPToNextLevel(2); got != 230 { if got := XPToNextLevel(2); got != 150 {
t.Fatalf("XPToNextLevel(2) = %d, want 230", got) t.Fatalf("XPToNextLevel(2) = %d, want 150", got)
} }
if got := XPToNextLevel(10); got != 1450 { if got := XPToNextLevel(3); got != 225 {
t.Fatalf("XPToNextLevel(10) = %d, want 1450", got) t.Fatalf("XPToNextLevel(3) = %d, want 225", got)
} }
if got := XPToNextLevel(30); got != 23000 { if got := XPToNextLevel(10); got != 2947 {
t.Fatalf("XPToNextLevel(30) = %d, want 23000", got) 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, LootChanceRare: 0.02,
LootChanceEpic: 0.003, LootChanceEpic: 0.003,
LootChanceLegendary: 0.0005, LootChanceLegendary: 0.0005,
GoldLootScale: 0.5, GoldLootScale: 0.62,
GoldDropChance: 0.90, GoldDropChance: 0.92,
PotionDropChance: 0.05, PotionDropChance: 0.06,
EquipmentDropBase: 0.15, EquipmentDropBase: 0.20,
GoldCommonMin: 0, GoldCommonMin: 0,
GoldCommonMax: 5, GoldCommonMax: 5,
GoldUncommonMin: 6, GoldUncommonMin: 6,
@ -327,11 +327,11 @@ func DefaultValues() Values {
PotionAutoUseThreshold: 0.30, PotionAutoUseThreshold: 0.30,
ReviveHpPercent: 0.50, ReviveHpPercent: 0.50,
AutoReviveAfterMs: int64(time.Hour / time.Millisecond), AutoReviveAfterMs: int64(time.Hour / time.Millisecond),
XPCurveEarlyBase: 180, XPCurveEarlyBase: 100,
XPCurveEarlyScale: 1.28, XPCurveEarlyScale: 1.5,
XPCurveMidBase: 1450, XPCurveMidBase: 2947,
XPCurveMidScale: 1.15, XPCurveMidScale: 1.15,
XPCurveLateBase: 23000, XPCurveLateBase: 48232,
XPCurveLateScale: 1.10, XPCurveLateScale: 1.10,
LevelUpHPEvery: 4, LevelUpHPEvery: 4,
LevelUpHpBase: 10, LevelUpHpBase: 10,

Loading…
Cancel
Save