Compare commits
25 Commits
42e3a9b19e
...
0fde5c7f26
| Author | SHA1 | Date |
|---|---|---|
|
|
0fde5c7f26 | 1 month ago |
|
|
5bf7f5b0bd | 1 month ago |
|
|
082f96f627 | 1 month ago |
|
|
a415951876 | 1 month ago |
|
|
03208b17ba | 1 month ago |
|
|
13c7e65515 | 1 month ago |
|
|
32caac9e55 | 1 month ago |
|
|
7aaa1bd8d4 | 1 month ago |
|
|
a9b452e713 | 1 month ago |
|
|
dd21bff29d | 1 month ago |
|
|
4f97cd2b98 | 1 month ago |
|
|
22e6b3fac4 | 1 month ago |
|
|
51be614b9f | 1 month ago |
|
|
ae6fa7bb9c | 1 month ago |
|
|
485254d6cd | 1 month ago |
|
|
1aca5d265b | 1 month ago |
|
|
ddb5a3a2c4 | 1 month ago |
|
|
2f00103b90 | 1 month ago |
|
|
380fcd41ae | 1 month ago |
|
|
b6eb68bf11 | 1 month ago |
|
|
8ecb3981ac | 1 month ago |
|
|
9b5af1f93c | 1 month ago |
|
|
11d2c41e90 | 1 month ago |
|
|
86ffebf26a | 1 month ago |
|
|
0c52a369cb | 1 month ago |
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -0,0 +1,112 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/denisovdennis/autohero/internal/game"
|
||||||
|
"github.com/denisovdennis/autohero/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// gearCheckScenariosForTemplate builds one fair-tier cell per level in the intersection of
|
||||||
|
// [t.MinLevel..t.MaxLevel] with [levelMin..levelMax] when those flags are > 0 (0 = no bound from that side).
|
||||||
|
func gearCheckScenariosForTemplate(t model.Enemy, levelMin, levelMax int) []gridScenario {
|
||||||
|
minL := t.MinLevel
|
||||||
|
maxL := t.MaxLevel
|
||||||
|
if minL <= 0 || maxL < minL {
|
||||||
|
lvl := (t.MinLevel + t.MaxLevel) / 2
|
||||||
|
if lvl < 1 {
|
||||||
|
lvl = 1
|
||||||
|
}
|
||||||
|
if t.BaseLevel > 0 {
|
||||||
|
lvl = t.BaseLevel
|
||||||
|
}
|
||||||
|
if levelMin > 0 && lvl < levelMin {
|
||||||
|
lvl = levelMin
|
||||||
|
}
|
||||||
|
if levelMax > 0 && lvl > levelMax {
|
||||||
|
lvl = levelMax
|
||||||
|
}
|
||||||
|
if levelMin > 0 && levelMax > 0 && (lvl < levelMin || lvl > levelMax) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []gridScenario{{heroLv: lvl, enemyLv: lvl, gearIdx: 0}}
|
||||||
|
}
|
||||||
|
if levelMin > 0 && levelMin > minL {
|
||||||
|
minL = levelMin
|
||||||
|
}
|
||||||
|
if levelMax > 0 && levelMax < maxL {
|
||||||
|
maxL = levelMax
|
||||||
|
}
|
||||||
|
if minL > maxL {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]gridScenario, 0, maxL-minL+1)
|
||||||
|
for lv := minL; lv <= maxL; lv++ {
|
||||||
|
out = append(out, gridScenario{heroLv: lv, enemyLv: lv, gearIdx: 0})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// runGearCheck compares baseline (common) vs max (legendary) reference gear at each level.
|
||||||
|
// Rules (best gear vs baseline):
|
||||||
|
// - Kill time must not improve by more than maxSpeedupPct (median win duration max >= baseline * (1 - maxSpeedupPct/100)).
|
||||||
|
// - Median hero HP%% on wins with best gear must be <= maxHeroHpPct/100.
|
||||||
|
func runGearCheck(
|
||||||
|
tmpl model.Enemy,
|
||||||
|
et string,
|
||||||
|
scenarios []gridScenario,
|
||||||
|
n int,
|
||||||
|
seedBase int64,
|
||||||
|
maxSpeedupPct float64,
|
||||||
|
maxHeroHpPct float64,
|
||||||
|
strict bool,
|
||||||
|
) (failCells int, lines []string) {
|
||||||
|
minDurRatio := 1.0 - maxSpeedupPct/100.0
|
||||||
|
maxHpFrac := maxHeroHpPct / 100.0
|
||||||
|
if minDurRatio < 0 || minDurRatio > 1 {
|
||||||
|
minDurRatio = 0.80
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, sc := range scenarios {
|
||||||
|
h := hashGridScenario(et, sc)
|
||||||
|
heroB := game.NewReferenceHeroForBalance(sc.heroLv, game.ReferenceGearBaseline, nil)
|
||||||
|
heroM := game.NewReferenceHeroForBalance(sc.heroLv, game.ReferenceGearMax, nil)
|
||||||
|
|
||||||
|
rB := runOneGridScenario(tmpl, heroB, sc.enemyLv, n, seedBase, h)
|
||||||
|
rM := runOneGridScenario(tmpl, heroM, sc.enemyLv, n, seedBase, h+0x100000)
|
||||||
|
|
||||||
|
if rB.medianWinSec <= 0 {
|
||||||
|
if strict {
|
||||||
|
failCells++
|
||||||
|
lines = append(lines, fmt.Sprintf(
|
||||||
|
"FAIL %s L%d: baseline had no median win duration (strict; winRate=%.1f%%)",
|
||||||
|
et, sc.heroLv, 100*rB.winRate))
|
||||||
|
} else {
|
||||||
|
lines = append(lines, fmt.Sprintf("# %s L%d: SKIP — baseline had no median win duration (winRate=%.1f%%)", et, sc.heroLv, 100*rB.winRate))
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if rM.medianWinSec <= 0 {
|
||||||
|
failCells++
|
||||||
|
lines = append(lines, fmt.Sprintf("FAIL %s L%d: max gear had no median win duration (winRate=%.1f%%)", et, sc.heroLv, 100*rM.winRate))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Faster kill = lower duration. Allow at most maxSpeedupPct faster => durM >= durB * minDurRatio.
|
||||||
|
failDur := rM.medianWinSec < rB.medianWinSec*minDurRatio-1e-6
|
||||||
|
failHp := rM.medianHeroHp > maxHpFrac+1e-9
|
||||||
|
if failDur || failHp {
|
||||||
|
failCells++
|
||||||
|
speedupPct := (1.0 - rM.medianWinSec/rB.medianWinSec) * 100.0
|
||||||
|
lines = append(lines, fmt.Sprintf(
|
||||||
|
"FAIL %s L%d: baseline dur=%.1fs hp=%.1f%% | max dur=%.1fs (~%.1f%% faster) need dur>=%.1fs | max hp=%.1f%% need <=%.1f%% | dur_ok=%v hp_ok=%v",
|
||||||
|
et, sc.heroLv,
|
||||||
|
rB.medianWinSec, 100*rB.medianHeroHp,
|
||||||
|
rM.medianWinSec, speedupPct,
|
||||||
|
rB.medianWinSec*minDurRatio,
|
||||||
|
100*rM.medianHeroHp, maxHeroHpPct,
|
||||||
|
!failDur, !failHp,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return failCells, lines
|
||||||
|
}
|
||||||
@ -0,0 +1,170 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/denisovdennis/autohero/internal/model"
|
||||||
|
"github.com/denisovdennis/autohero/internal/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
func loadGearCatalog(ctx context.Context, cs *storage.ContentStore, base string) ([]model.GearFamily, error) {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(base)) {
|
||||||
|
case "", "db":
|
||||||
|
return cs.LoadGearFamilies(ctx)
|
||||||
|
case "code":
|
||||||
|
return model.DefaultGearFamilies(), nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("gear-base must be db or code, got %q", base)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// gearFamilyPatchJSON holds optional overrides merged onto catalog families after DB/code load.
|
||||||
|
type gearFamilyPatchJSON struct {
|
||||||
|
BasePrimary *int `json:"basePrimary,omitempty"`
|
||||||
|
SpeedModifier *float64 `json:"speedModifier,omitempty"`
|
||||||
|
BaseCrit *float64 `json:"baseCrit,omitempty"`
|
||||||
|
AgilityBonus *int `json:"agilityBonus,omitempty"`
|
||||||
|
StatType *string `json:"statType,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// listOverlayKeys returns top-level JSON object keys (patch targets).
|
||||||
|
func listOverlayKeys(path string) ([]string, error) {
|
||||||
|
b, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var raw map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(b, &raw); err != nil {
|
||||||
|
return nil, fmt.Errorf("gear overlay JSON: %w", err)
|
||||||
|
}
|
||||||
|
keys := make([]string, 0, len(raw))
|
||||||
|
for k := range raw {
|
||||||
|
keys = append(keys, strings.TrimSpace(k))
|
||||||
|
}
|
||||||
|
return keys, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyGearOverlayJSON clones families, applies patches keyed by "name" or "slot:name" (e.g. "main_hand:Soul Reaver").
|
||||||
|
func applyGearOverlayJSON(path string, families []model.GearFamily) ([]model.GearFamily, error) {
|
||||||
|
b, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var raw map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(b, &raw); err != nil {
|
||||||
|
return nil, fmt.Errorf("gear overlay JSON: %w", err)
|
||||||
|
}
|
||||||
|
patches := make(map[string]gearFamilyPatchJSON, len(raw))
|
||||||
|
for k, v := range raw {
|
||||||
|
var p gearFamilyPatchJSON
|
||||||
|
if err := json.Unmarshal(v, &p); err != nil {
|
||||||
|
return nil, fmt.Errorf("gear overlay key %q: %w", k, err)
|
||||||
|
}
|
||||||
|
patches[strings.TrimSpace(k)] = p
|
||||||
|
}
|
||||||
|
out := cloneGearFamilies(families)
|
||||||
|
for i := range out {
|
||||||
|
key := overlayKey(out[i])
|
||||||
|
if p, ok := patches[key]; ok {
|
||||||
|
mergePatch(&out[i], p)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if p, ok := patches[out[i].Name]; ok {
|
||||||
|
mergePatch(&out[i], p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func overlayKey(g model.GearFamily) string {
|
||||||
|
return fmt.Sprintf("%s:%s", g.Slot, g.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloneGearFamilies(f []model.GearFamily) []model.GearFamily {
|
||||||
|
out := make([]model.GearFamily, len(f))
|
||||||
|
copy(out, f)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergePatch(g *model.GearFamily, p gearFamilyPatchJSON) {
|
||||||
|
if p.BasePrimary != nil {
|
||||||
|
g.BasePrimary = *p.BasePrimary
|
||||||
|
}
|
||||||
|
if p.SpeedModifier != nil {
|
||||||
|
g.SpeedModifier = *p.SpeedModifier
|
||||||
|
}
|
||||||
|
if p.BaseCrit != nil {
|
||||||
|
g.BaseCrit = *p.BaseCrit
|
||||||
|
}
|
||||||
|
if p.AgilityBonus != nil {
|
||||||
|
g.AgilityBonus = *p.AgilityBonus
|
||||||
|
}
|
||||||
|
if p.StatType != nil {
|
||||||
|
g.StatType = *p.StatType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// printGearOverlayMigrationSQL prints UPDATEs for gear (and equipment_items for extended slots) for each overlay key.
|
||||||
|
func printGearOverlayMigrationSQL(families []model.GearFamily, overlayKeys []string) {
|
||||||
|
bySlotName := make(map[string]model.GearFamily, len(families))
|
||||||
|
byName := make(map[string]model.GearFamily, len(families))
|
||||||
|
for _, g := range families {
|
||||||
|
bySlotName[overlayKey(g)] = g
|
||||||
|
byName[g.Name] = g
|
||||||
|
}
|
||||||
|
fmt.Printf("-- balanceall -gear-print-sql (pairs with -gear-overlay)\n\n")
|
||||||
|
for _, k := range overlayKeys {
|
||||||
|
g, ok := bySlotName[k]
|
||||||
|
if !ok {
|
||||||
|
g, ok = byName[k]
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
fmt.Printf("-- WARNING: no catalog family for overlay key %q\n", k)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
r := catalogRarityForName(g.Name)
|
||||||
|
ps := model.ScalePrimary(g.BasePrimary, 1, r)
|
||||||
|
fmt.Printf(`UPDATE public.gear SET base_primary = %d, primary_stat = %d WHERE slot = '%s' AND name = %s;
|
||||||
|
`, g.BasePrimary, ps, string(g.Slot), sqlQuote(g.Name))
|
||||||
|
switch g.Slot {
|
||||||
|
case model.SlotMainHand, model.SlotChest:
|
||||||
|
// Catalog is fully in public.gear above.
|
||||||
|
default:
|
||||||
|
if g.FormID != "" {
|
||||||
|
fmt.Printf(`UPDATE public.equipment_items SET base_primary = %d, primary_stat = %d WHERE form_id = %s;
|
||||||
|
`, g.BasePrimary, ps, sqlQuote(g.FormID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func catalogRarityForName(name string) model.Rarity {
|
||||||
|
switch name {
|
||||||
|
case "Rusty Dagger", "Iron Sword", "Rusty Axe",
|
||||||
|
"Leather Armor", "Chainmail", "Iron Plate":
|
||||||
|
return model.RarityCommon
|
||||||
|
case "Iron Dagger", "Steel Sword", "Battle Axe",
|
||||||
|
"Ranger's Vest", "Reinforced Mail", "Steel Plate":
|
||||||
|
return model.RarityUncommon
|
||||||
|
case "Assassin's Blade", "Longsword", "War Axe",
|
||||||
|
"Shadow Cloak", "Battle Armor", "Fortress Armor", "Guardian's Plate":
|
||||||
|
return model.RarityRare
|
||||||
|
case "Phantom Edge", "Excalibur", "Infernal Axe",
|
||||||
|
"Phantom Garb", "Royal Guard", "Dragon Scale", "Guardian's Bastion":
|
||||||
|
return model.RarityEpic
|
||||||
|
case "Fang of the Void", "Soul Reaver", "Godslayer's Edge",
|
||||||
|
"Whisper of the Void", "Crown of Eternity", "Dragon Slayer", "Ancient Guardian's Aegis":
|
||||||
|
return model.RarityLegendary
|
||||||
|
default:
|
||||||
|
return model.RarityCommon
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sqlQuote(s string) string {
|
||||||
|
return "'" + strings.ReplaceAll(s, "'", "''") + "'"
|
||||||
|
}
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/denisovdennis/autohero/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestApplyGearOverlayJSON(t *testing.T) {
|
||||||
|
base := []model.GearFamily{
|
||||||
|
{Slot: model.SlotChest, Name: "Chainmail", BasePrimary: 7, Subtype: "medium"},
|
||||||
|
{Slot: model.SlotMainHand, Name: "Iron Sword", BasePrimary: 5, Subtype: "sword"},
|
||||||
|
}
|
||||||
|
path := filepath.Join("testdata", "gear_overlay_sample.json")
|
||||||
|
out, err := applyGearOverlayJSON(path, base)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if out[0].BasePrimary != 4 {
|
||||||
|
t.Fatalf("Chainmail basePrimary = %d, want 4", out[0].BasePrimary)
|
||||||
|
}
|
||||||
|
if out[1].BasePrimary != 7 {
|
||||||
|
t.Fatalf("Iron Sword basePrimary = %d, want 7", out[1].BasePrimary)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,150 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/denisovdennis/autohero/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// enemyPartial mirrors model.Enemy with pointer fields so JSON omits mean "keep DB value".
|
||||||
|
type enemyPartial struct {
|
||||||
|
ID *int64 `json:"id"`
|
||||||
|
Type *string `json:"type"`
|
||||||
|
Name *string `json:"name"`
|
||||||
|
HP *int `json:"hp"`
|
||||||
|
MaxHP *int `json:"maxHp"`
|
||||||
|
Attack *int `json:"attack"`
|
||||||
|
Defense *int `json:"defense"`
|
||||||
|
Speed *float64 `json:"speed"`
|
||||||
|
CritChance *float64 `json:"critChance"`
|
||||||
|
MinLevel *int `json:"minLevel"`
|
||||||
|
MaxLevel *int `json:"maxLevel"`
|
||||||
|
BaseLevel *int `json:"baseLevel"`
|
||||||
|
LevelVariance *float64 `json:"levelVariance"`
|
||||||
|
MaxHeroLevelDiff *int `json:"maxHeroLevelDiff"`
|
||||||
|
HPPerLevel *float64 `json:"hpPerLevel"`
|
||||||
|
AttackPerLevel *float64 `json:"attackPerLevel"`
|
||||||
|
DefensePerLevel *float64 `json:"defensePerLevel"`
|
||||||
|
XPPerLevel *float64 `json:"xpPerLevel"`
|
||||||
|
GoldPerLevel *float64 `json:"goldPerLevel"`
|
||||||
|
Level *int `json:"level"`
|
||||||
|
XPReward *int64 `json:"xpReward"`
|
||||||
|
GoldReward *int64 `json:"goldReward"`
|
||||||
|
SpecialAbilities *[]model.SpecialAbility `json:"specialAbilities"`
|
||||||
|
IsElite *bool `json:"isElite"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyEnemyOverlayJSON reads a JSON object keyed by enemy type (string), merges each partial onto templates.
|
||||||
|
// Unknown keys log a warning and are skipped. Keys for types not present in templates log a warning.
|
||||||
|
func applyEnemyOverlayJSON(path string, templates map[string]model.Enemy) (map[string]model.Enemy, error) {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read overlay %q: %w", path, err)
|
||||||
|
}
|
||||||
|
var raw map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(data, &raw); err != nil {
|
||||||
|
return nil, fmt.Errorf("parse overlay JSON: %w", err)
|
||||||
|
}
|
||||||
|
out := make(map[string]model.Enemy, len(templates))
|
||||||
|
for k, v := range templates {
|
||||||
|
out[k] = v
|
||||||
|
}
|
||||||
|
for typeKey, rawMsg := range raw {
|
||||||
|
base, ok := out[typeKey]
|
||||||
|
if !ok {
|
||||||
|
fmt.Fprintf(os.Stderr, "balanceall overlay: skip unknown type %q (not in loaded templates)\n", typeKey)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var p enemyPartial
|
||||||
|
if err := json.Unmarshal(rawMsg, &p); err != nil {
|
||||||
|
return nil, fmt.Errorf("overlay %q: %w", typeKey, err)
|
||||||
|
}
|
||||||
|
mergeEnemyPartial(&base, &p)
|
||||||
|
out[typeKey] = base
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeEnemyPartial(dst *model.Enemy, p *enemyPartial) {
|
||||||
|
if p.ID != nil {
|
||||||
|
dst.ID = *p.ID
|
||||||
|
}
|
||||||
|
if p.Type != nil {
|
||||||
|
dst.Slug = *p.Type
|
||||||
|
}
|
||||||
|
if p.Name != nil {
|
||||||
|
dst.Name = *p.Name
|
||||||
|
}
|
||||||
|
if p.HP != nil {
|
||||||
|
dst.HP = *p.HP
|
||||||
|
}
|
||||||
|
if p.MaxHP != nil {
|
||||||
|
dst.MaxHP = *p.MaxHP
|
||||||
|
}
|
||||||
|
if p.Attack != nil {
|
||||||
|
dst.Attack = *p.Attack
|
||||||
|
}
|
||||||
|
if p.Defense != nil {
|
||||||
|
dst.Defense = *p.Defense
|
||||||
|
}
|
||||||
|
if p.Speed != nil {
|
||||||
|
dst.Speed = *p.Speed
|
||||||
|
}
|
||||||
|
if p.CritChance != nil {
|
||||||
|
dst.CritChance = *p.CritChance
|
||||||
|
}
|
||||||
|
if p.MinLevel != nil {
|
||||||
|
dst.MinLevel = *p.MinLevel
|
||||||
|
}
|
||||||
|
if p.MaxLevel != nil {
|
||||||
|
dst.MaxLevel = *p.MaxLevel
|
||||||
|
}
|
||||||
|
if p.BaseLevel != nil {
|
||||||
|
dst.BaseLevel = *p.BaseLevel
|
||||||
|
}
|
||||||
|
if p.LevelVariance != nil {
|
||||||
|
dst.LevelVariance = *p.LevelVariance
|
||||||
|
}
|
||||||
|
if p.MaxHeroLevelDiff != nil {
|
||||||
|
dst.MaxHeroLevelDiff = *p.MaxHeroLevelDiff
|
||||||
|
}
|
||||||
|
if p.HPPerLevel != nil {
|
||||||
|
dst.HPPerLevel = *p.HPPerLevel
|
||||||
|
}
|
||||||
|
if p.AttackPerLevel != nil {
|
||||||
|
dst.AttackPerLevel = *p.AttackPerLevel
|
||||||
|
}
|
||||||
|
if p.DefensePerLevel != nil {
|
||||||
|
dst.DefensePerLevel = *p.DefensePerLevel
|
||||||
|
}
|
||||||
|
if p.XPPerLevel != nil {
|
||||||
|
dst.XPPerLevel = *p.XPPerLevel
|
||||||
|
}
|
||||||
|
if p.GoldPerLevel != nil {
|
||||||
|
dst.GoldPerLevel = *p.GoldPerLevel
|
||||||
|
}
|
||||||
|
if p.Level != nil {
|
||||||
|
dst.Level = *p.Level
|
||||||
|
}
|
||||||
|
if p.XPReward != nil {
|
||||||
|
dst.XPReward = *p.XPReward
|
||||||
|
}
|
||||||
|
if p.GoldReward != nil {
|
||||||
|
dst.GoldReward = *p.GoldReward
|
||||||
|
}
|
||||||
|
if p.SpecialAbilities != nil {
|
||||||
|
dst.SpecialAbilities = *p.SpecialAbilities
|
||||||
|
}
|
||||||
|
if p.IsElite != nil {
|
||||||
|
dst.IsElite = *p.IsElite
|
||||||
|
}
|
||||||
|
// If only one of hp/maxHp was overridden, keep them aligned for template rows.
|
||||||
|
if p.MaxHP != nil && p.HP == nil {
|
||||||
|
dst.HP = dst.MaxHP
|
||||||
|
}
|
||||||
|
if p.HP != nil && p.MaxHP == nil {
|
||||||
|
dst.MaxHP = dst.HP
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"chest:Chainmail": { "basePrimary": 4 },
|
||||||
|
"chest:Crown of Eternity": { "basePrimary": 4 },
|
||||||
|
"main_hand:Iron Sword": { "basePrimary": 7, "speedModifier": 1.0, "baseCrit": 0.03 },
|
||||||
|
"main_hand:Soul Reaver": { "basePrimary": 7, "speedModifier": 1.0, "baseCrit": 0.03 }
|
||||||
|
}
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"chest:Chainmail": { "basePrimary": 4 },
|
||||||
|
"Iron Sword": { "basePrimary": 7 }
|
||||||
|
}
|
||||||
@ -0,0 +1,293 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"math"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
|
||||||
|
"github.com/denisovdennis/autohero/internal/game"
|
||||||
|
"github.com/denisovdennis/autohero/internal/model"
|
||||||
|
"github.com/denisovdennis/autohero/internal/storage"
|
||||||
|
"github.com/denisovdennis/autohero/internal/tuning"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var (
|
||||||
|
dsnFlag = flag.String("dsn", "", "Postgres DSN (default: DATABASE_URL env)")
|
||||||
|
|
||||||
|
iterPerLevel = flag.Int("iterations-per-level", 40, "Monte Carlo iterations per hero level (higher = slower, smoother)")
|
||||||
|
seed = flag.Int64("seed", 20260331, "RNG seed base")
|
||||||
|
restSec = flag.Float64("rest-sec", 400, "seconds of rest after each fight")
|
||||||
|
accountLoss = flag.Bool("account-losses", false, "if true, XP rate uses all fights; else wins-only expectation")
|
||||||
|
|
||||||
|
gearStr = flag.String("gear", "median", "median|rolled reference gear")
|
||||||
|
|
||||||
|
typesFilter = flag.String("types", "", "comma-separated enemy types to include in SQL output (empty = all)")
|
||||||
|
|
||||||
|
maxHeroLevel = flag.Int("max-level", 49, "max hero level step simulated (L→L+1 up to this L); use lower if high levels never win")
|
||||||
|
|
||||||
|
targetW1 = flag.Float64("target-weeks-1-10", 1, "target wall-clock weeks for level-ups 1→10")
|
||||||
|
targetW2 = flag.Float64("target-weeks-10-20", 3, "target weeks for 10→20")
|
||||||
|
targetW3 = flag.Float64("target-weeks-20-30", 6, "target weeks for 20→30")
|
||||||
|
targetW4 = flag.Float64("target-weeks-30-40", 10, "target weeks for 30→40")
|
||||||
|
targetW5 = flag.Float64("target-weeks-40-50", 20, "target weeks for 40→50")
|
||||||
|
weekDur = flag.Duration("week", 7*24*time.Hour, "duration of one target week (default 7d)")
|
||||||
|
|
||||||
|
runReport = flag.Bool("report", true, "print band durations vs targets (baseline DB xp_reward)")
|
||||||
|
optTypes = flag.Bool("optimize-types", false, "optimize per-row xp_reward (each enemies.type); no global multiplier")
|
||||||
|
optBands = flag.Bool("optimize-bands", false, "optimize five content-tier multipliers (TemplateProgressionBand), not per-type")
|
||||||
|
eliteMul = flag.Float64("elite-scale", 1, "multiplier on is_elite rows (applied with per-type / per-band scales)")
|
||||||
|
optIters = flag.Int("optimize-iters", 100, "coordinate-descent passes per optimize round (-optimize-types / -optimize-bands)")
|
||||||
|
optRounds = flag.Int("optimize-rounds", 8, "for -optimize-types: repeat from last scaled values until -target-max-rel-err or max rounds")
|
||||||
|
enforceTierXP = flag.Bool("enforce-tier-xp", true, "for -optimize-types: xp_reward non-decreasing with min/max level tier (strict up when tier rises)")
|
||||||
|
targetMaxRelErr = flag.Float64("target-max-rel-err", 0.18, "stop optimize rounds when max relative error on bands+total is below this (e.g. 0.18 = 18%)")
|
||||||
|
|
||||||
|
printSQL = flag.Bool("sql", true, "print suggested UPDATE enemies SET xp_reward=...")
|
||||||
|
sqlAll = flag.Bool("sql-all", true, "emit UPDATE for every enemy row (not only changed)")
|
||||||
|
)
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
dsn := strings.TrimSpace(*dsnFlag)
|
||||||
|
if dsn == "" {
|
||||||
|
dsn = os.Getenv("DATABASE_URL")
|
||||||
|
}
|
||||||
|
if dsn == "" {
|
||||||
|
log.Fatal("DATABASE_URL or -dsn is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if *optTypes && *optBands {
|
||||||
|
log.Fatal("use only one of -optimize-types or -optimize-bands")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
pool, err := pgxpool.New(ctx, dsn)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("open db: %v", err)
|
||||||
|
}
|
||||||
|
defer pool.Close()
|
||||||
|
|
||||||
|
rcStore := storage.NewRuntimeConfigStore(pool)
|
||||||
|
if err := tuning.ReloadNow(ctx, nil, rcStore); err != nil {
|
||||||
|
log.Fatalf("load runtime config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cs := storage.NewContentStore(pool)
|
||||||
|
baseTemplates, err := cs.LoadEnemyTemplates(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("load enemies: %v", err)
|
||||||
|
}
|
||||||
|
if len(baseTemplates) == 0 {
|
||||||
|
log.Fatal("no enemy templates in database")
|
||||||
|
}
|
||||||
|
|
||||||
|
gear := game.ReferenceGearMedian
|
||||||
|
if strings.EqualFold(strings.TrimSpace(*gearStr), "rolled") {
|
||||||
|
gear = game.ReferenceGearRolled
|
||||||
|
}
|
||||||
|
|
||||||
|
maxL := *maxHeroLevel
|
||||||
|
if maxL < 1 {
|
||||||
|
maxL = 1
|
||||||
|
}
|
||||||
|
if maxL > 49 {
|
||||||
|
maxL = 49
|
||||||
|
}
|
||||||
|
|
||||||
|
params := game.ProgressionSimParams{
|
||||||
|
IterationsPerLevel: *iterPerLevel,
|
||||||
|
Seed: *seed,
|
||||||
|
RestAfterCombat: time.Duration(*restSec * float64(time.Second)),
|
||||||
|
Gear: gear,
|
||||||
|
AccountLosses: *accountLoss,
|
||||||
|
MinHeroLevel: 1,
|
||||||
|
MaxHeroLevelInclusive: maxL,
|
||||||
|
}
|
||||||
|
|
||||||
|
fullTargets := [5]time.Duration{
|
||||||
|
time.Duration(*targetW1 * float64(*weekDur)),
|
||||||
|
time.Duration(*targetW2 * float64(*weekDur)),
|
||||||
|
time.Duration(*targetW3 * float64(*weekDur)),
|
||||||
|
time.Duration(*targetW4 * float64(*weekDur)),
|
||||||
|
time.Duration(*targetW5 * float64(*weekDur)),
|
||||||
|
}
|
||||||
|
targets := game.ProratedBandTargets(maxL, fullTargets)
|
||||||
|
totalTarget := game.SumBandTargets(targets)
|
||||||
|
fmt.Printf("# xpprogsim: maxHeroLevel=%d | prorated target sum=%s (full 1→50 would be %s)\n",
|
||||||
|
maxL, totalTarget.Round(time.Second), (fullTargets[0]+fullTargets[1]+fullTargets[2]+fullTargets[3]+fullTargets[4]).Round(time.Second))
|
||||||
|
|
||||||
|
typeFilter := parseTypesFilter(*typesFilter)
|
||||||
|
|
||||||
|
if *runReport {
|
||||||
|
res, err := game.SimulateProgressionBands(params, game.CloneEnemyTemplates(game.EnemyTemplatesFromSlice(baseTemplates)))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("simulate: %v", err)
|
||||||
|
}
|
||||||
|
printReport("baseline (DB xp_reward)", res, targets, totalTarget)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *optTypes {
|
||||||
|
baseRound := game.CloneEnemyTemplates(game.EnemyTemplatesFromSlice(baseTemplates))
|
||||||
|
iters := *optIters
|
||||||
|
var lastScaled map[string]model.Enemy
|
||||||
|
var lastRes game.ProgressionBandResult
|
||||||
|
var lastSq float64
|
||||||
|
var lastPerType map[string]float64
|
||||||
|
for round := 0; round < *optRounds; round++ {
|
||||||
|
var scaled map[string]model.Enemy
|
||||||
|
lastPerType, scaled, lastRes, lastSq = game.OptimizePerTypeScales(baseRound, params, targets, *eliteMul, iters, *enforceTierXP)
|
||||||
|
lastScaled = scaled
|
||||||
|
maxRel := game.MaxRelativeErrorVsTargets(lastRes.BandDurations, targets, lastRes.Total, totalTarget)
|
||||||
|
fmt.Printf("\n# optimize-types round %d/%d: iters=%d sqErr=%.6f maxRelErr=%.2f%% enforceTier=%v\n",
|
||||||
|
round+1, *optRounds, iters, lastSq, 100*maxRel, *enforceTierXP)
|
||||||
|
if !math.IsInf(lastRes.TotalSec, 1) && maxRel <= *targetMaxRelErr {
|
||||||
|
fmt.Printf("# stopped: max relative error <= %.0f%%\n", 100*(*targetMaxRelErr))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
baseRound = game.CloneEnemyTemplates(scaled)
|
||||||
|
iters += *optIters / 3
|
||||||
|
if iters > 400 {
|
||||||
|
iters = 400
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if lastPerType != nil {
|
||||||
|
printPerTypeMultipliers(lastPerType)
|
||||||
|
}
|
||||||
|
printReport("after per-type optimization (final)", lastRes, targets, totalTarget)
|
||||||
|
if *printSQL && lastScaled != nil {
|
||||||
|
if *sqlAll {
|
||||||
|
printSQLAll(lastScaled, typeFilter)
|
||||||
|
} else {
|
||||||
|
printSQLDiff(game.EnemyTemplatesFromSlice(baseTemplates), lastScaled, typeFilter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if *optBands {
|
||||||
|
scales, res, sqErr := game.OptimizeBandScales(game.EnemyTemplatesFromSlice(baseTemplates), params, targets, 1, *eliteMul, *optIters)
|
||||||
|
fmt.Printf("\n# optimize-bands: per-band scales [%v] sqErr=%.6f\n", formatFloats(scales[:]), sqErr)
|
||||||
|
printReport("after band-tier optimization", res, targets, totalTarget)
|
||||||
|
spec := game.XPRewardScaleSpec{Global: 1, Elite: *eliteMul, PerBand: scales}
|
||||||
|
scaled := game.ApplyXPRewardScaleSpec(game.EnemyTemplatesFromSlice(baseTemplates), spec)
|
||||||
|
if *printSQL {
|
||||||
|
printSQLDiff(game.EnemyTemplatesFromSlice(baseTemplates), scaled, typeFilter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !*runReport && !*optTypes && !*optBands {
|
||||||
|
log.Fatal("nothing to do: enable -report and/or -optimize-types and/or -optimize-bands")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTypesFilter(s string) map[string]bool {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if s == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make(map[string]bool)
|
||||||
|
for _, p := range strings.Split(s, ",") {
|
||||||
|
p = strings.TrimSpace(p)
|
||||||
|
if p == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out[p] = true
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func printPerTypeMultipliers(m map[string]float64) {
|
||||||
|
keys := make([]string, 0, len(m))
|
||||||
|
for t := range m {
|
||||||
|
keys = append(keys, t)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
fmt.Print("# multipliers vs DB xp_reward: ")
|
||||||
|
for i, ks := range keys {
|
||||||
|
if i > 0 {
|
||||||
|
fmt.Print(" ")
|
||||||
|
}
|
||||||
|
fmt.Printf("%s=%.4f", ks, m[ks])
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
func printReport(title string, res game.ProgressionBandResult, targets [5]time.Duration, totalTarget time.Duration) {
|
||||||
|
fmt.Printf("\n## %s\n", title)
|
||||||
|
labels := []string{"1→10", "10→20", "20→30", "30→40", "40→50"}
|
||||||
|
errs := game.BandErrors(res.BandDurations, targets)
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
fmt.Printf(" band %s: sim=%s target=%s rel_err=%.2f%%\n",
|
||||||
|
labels[i], res.BandDurations[i].Round(time.Second), targets[i].Round(time.Second), 100*errs[i])
|
||||||
|
}
|
||||||
|
fmt.Printf(" TOTAL: sim=%s target=%s rel_err=%.2f%%\n",
|
||||||
|
res.Total.Round(time.Second), totalTarget.Round(time.Second),
|
||||||
|
100*(float64(res.Total)/float64(totalTarget)-1))
|
||||||
|
if math.IsInf(res.TotalSec, 1) {
|
||||||
|
fmt.Println(" NOTE: total time hit +Inf (some hero levels had zero XP rate — raise balance, use -account-losses, or -max-level).")
|
||||||
|
} else {
|
||||||
|
minWR := 1.0
|
||||||
|
for _, w := range res.WinRates {
|
||||||
|
if w < minWR {
|
||||||
|
minWR = w
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if minWR < 0.05 && len(res.WinRates) > 0 {
|
||||||
|
fmt.Printf(" NOTE: min MC win rate across levels=%.0f%% — progression may be unrealistic at high levels.\n", 100*minWR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printSQLDiff(base, scaled map[string]model.Enemy, filter map[string]bool) {
|
||||||
|
fmt.Println()
|
||||||
|
for typ, b := range base {
|
||||||
|
if filter != nil && !filter[typ] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s, ok := scaled[typ]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if s.XPReward == b.XPReward {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t := strings.ReplaceAll(typ, "'", "''")
|
||||||
|
fmt.Printf("UPDATE public.enemies SET xp_reward = %d WHERE type = '%s';\n", s.XPReward, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// printSQLAll emits UPDATE for every row in scaled (optionally filtered by type), tier order.
|
||||||
|
func printSQLAll(scaled map[string]model.Enemy, filter map[string]bool) {
|
||||||
|
fmt.Println()
|
||||||
|
order := game.SortEnemyTypesByLevelTier(scaled)
|
||||||
|
for _, typ := range order {
|
||||||
|
if filter != nil && !filter[typ] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s, ok := scaled[typ]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t := strings.ReplaceAll(typ, "'", "''")
|
||||||
|
fmt.Printf("UPDATE public.enemies SET xp_reward = %d WHERE type = '%s';\n", s.XPReward, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatFloats(v []float64) string {
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteByte('[')
|
||||||
|
for i, x := range v {
|
||||||
|
if i > 0 {
|
||||||
|
b.WriteString(", ")
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&b, "%.4f", x)
|
||||||
|
}
|
||||||
|
b.WriteByte(']')
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
@ -0,0 +1,110 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/denisovdennis/autohero/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BalanceEnemyMode selects how the opponent is chosen for balance Monte Carlo.
|
||||||
|
type BalanceEnemyMode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// BalanceEnemyWolfOnly uses a single scaled Forest Wolf (canonical curve check).
|
||||||
|
BalanceEnemyWolfOnly BalanceEnemyMode = iota
|
||||||
|
// BalanceEnemyMixedSpawn matches PickEnemyForLevelWithRNG (weighted random template in band).
|
||||||
|
BalanceEnemyMixedSpawn
|
||||||
|
)
|
||||||
|
|
||||||
|
// BalanceMonteCarloResult aggregates outcomes from RunBalanceMonteCarlo.
|
||||||
|
type BalanceMonteCarloResult struct {
|
||||||
|
Iterations int
|
||||||
|
Wins int
|
||||||
|
WinRate float64
|
||||||
|
MedianDur time.Duration
|
||||||
|
P90Dur time.Duration
|
||||||
|
MeanDur time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunBalanceMonteCarlo runs N independent fights at hero level against scaled enemies.
|
||||||
|
// Per-iteration RNG is derived from seed so results are reproducible.
|
||||||
|
// Global math/rand is re-seeded per fight for damage/crit/dodge rolls (same as legacy combat).
|
||||||
|
func RunBalanceMonteCarlo(level int, iterations int, seed int64, gearProfile ReferenceGearProfile, enemyMode BalanceEnemyMode) BalanceMonteCarloResult {
|
||||||
|
if iterations <= 0 {
|
||||||
|
return BalanceMonteCarloResult{}
|
||||||
|
}
|
||||||
|
var wins int
|
||||||
|
durations := make([]time.Duration, 0, iterations)
|
||||||
|
var sumDur time.Duration
|
||||||
|
|
||||||
|
for i := 0; i < iterations; i++ {
|
||||||
|
var gearRng *rand.Rand
|
||||||
|
if gearProfile == ReferenceGearRolled {
|
||||||
|
gearRng = rand.New(rand.NewSource(seed + int64(i)*1_000_003))
|
||||||
|
}
|
||||||
|
baseHero := NewReferenceHeroForBalance(level, gearProfile, gearRng)
|
||||||
|
hero := CloneHeroForCombatSim(baseHero)
|
||||||
|
|
||||||
|
// Combat RNG (damage rolls, dodge, crit, debuff procs).
|
||||||
|
rand.Seed(seed + int64(i)*9_999_983)
|
||||||
|
|
||||||
|
var enemy model.Enemy
|
||||||
|
switch enemyMode {
|
||||||
|
case BalanceEnemyWolfOnly:
|
||||||
|
enemy = firstEnemyForBalance(level)
|
||||||
|
case BalanceEnemyMixedSpawn:
|
||||||
|
pickRNG := rand.New(rand.NewSource(seed + int64(i)*2_000_001))
|
||||||
|
enemy = PickEnemyForLevelWithRNG(level, pickRNG, hero)
|
||||||
|
default:
|
||||||
|
enemy = firstEnemyForBalance(level)
|
||||||
|
}
|
||||||
|
|
||||||
|
survived, elapsed := ResolveCombatToEndWithDuration(hero, &enemy, CombatSimDeterministicStart, CombatSimOptions{
|
||||||
|
TickRate: 100 * time.Millisecond,
|
||||||
|
MaxSteps: CombatSimMaxStepsLong,
|
||||||
|
})
|
||||||
|
if survived {
|
||||||
|
wins++
|
||||||
|
}
|
||||||
|
durations = append(durations, elapsed)
|
||||||
|
sumDur += elapsed
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(durations, func(i, j int) bool { return durations[i] < durations[j] })
|
||||||
|
median := durations[len(durations)/2]
|
||||||
|
p90idx := int(0.9 * float64(len(durations)-1))
|
||||||
|
if p90idx < 0 {
|
||||||
|
p90idx = 0
|
||||||
|
}
|
||||||
|
p90 := durations[p90idx]
|
||||||
|
|
||||||
|
return BalanceMonteCarloResult{
|
||||||
|
Iterations: iterations,
|
||||||
|
Wins: wins,
|
||||||
|
WinRate: float64(wins) / float64(iterations),
|
||||||
|
MedianDur: median,
|
||||||
|
P90Dur: p90,
|
||||||
|
MeanDur: time.Duration(int64(sumDur) / int64(iterations)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstEnemyForBalance(level int) model.Enemy {
|
||||||
|
var best model.Enemy
|
||||||
|
bestSet := false
|
||||||
|
for _, t := range model.EnemyTemplates {
|
||||||
|
if !bestSet {
|
||||||
|
best = t
|
||||||
|
bestSet = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if t.BaseLevel > 0 && (best.BaseLevel == 0 || t.BaseLevel < best.BaseLevel) {
|
||||||
|
best = t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !bestSet {
|
||||||
|
return model.Enemy{}
|
||||||
|
}
|
||||||
|
return ScaleEnemyTemplate(best, level)
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import "github.com/denisovdennis/autohero/internal/model"
|
||||||
|
|
||||||
|
// combatLogPhraseKey maps combat swing to a client phrase key (see frontend adventureLog phrases).
|
||||||
|
func combatLogPhraseKey(source, outcome string) string {
|
||||||
|
switch source {
|
||||||
|
case "hero":
|
||||||
|
switch outcome {
|
||||||
|
case attackOutcomeStun:
|
||||||
|
return model.LogPhraseCombatHeroStun
|
||||||
|
case attackOutcomeDodge:
|
||||||
|
return model.LogPhraseCombatHeroDodge
|
||||||
|
default:
|
||||||
|
return model.LogPhraseCombatHeroHit
|
||||||
|
}
|
||||||
|
case "enemy":
|
||||||
|
if outcome == attackOutcomeBlock {
|
||||||
|
return model.LogPhraseCombatEnemyBlock
|
||||||
|
}
|
||||||
|
return model.LogPhraseCombatEnemyHit
|
||||||
|
default:
|
||||||
|
return model.LogPhraseCombatHeroHit
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import "github.com/denisovdennis/autohero/internal/model"
|
||||||
|
|
||||||
|
func ensureTestEnemyTemplates() {
|
||||||
|
if len(model.EnemyTemplates) > 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
model.SetEnemyTemplates([]model.Enemy{
|
||||||
|
{
|
||||||
|
Slug: "wolf",
|
||||||
|
Archetype: "wolf",
|
||||||
|
Name: "Forest Wolf",
|
||||||
|
MaxHP: 40,
|
||||||
|
HP: 40,
|
||||||
|
Attack: 8,
|
||||||
|
Defense: 2,
|
||||||
|
Speed: 1.2,
|
||||||
|
BaseLevel: 1,
|
||||||
|
LevelVariance: 0.3,
|
||||||
|
MaxHeroLevelDiff: 5,
|
||||||
|
HPPerLevel: 5,
|
||||||
|
AttackPerLevel: 1.5,
|
||||||
|
DefensePerLevel: 1.0,
|
||||||
|
XPPerLevel: 2,
|
||||||
|
XPReward: 1,
|
||||||
|
GoldReward: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Slug: "boar",
|
||||||
|
Archetype: "boar",
|
||||||
|
Name: "Wild Boar",
|
||||||
|
MaxHP: 60,
|
||||||
|
HP: 60,
|
||||||
|
Attack: 10,
|
||||||
|
Defense: 5,
|
||||||
|
Speed: 0.9,
|
||||||
|
BaseLevel: 3,
|
||||||
|
LevelVariance: 0.3,
|
||||||
|
MaxHeroLevelDiff: 5,
|
||||||
|
HPPerLevel: 6,
|
||||||
|
AttackPerLevel: 1.8,
|
||||||
|
DefensePerLevel: 1.3,
|
||||||
|
XPPerLevel: 2,
|
||||||
|
XPReward: 1,
|
||||||
|
GoldReward: 8,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -0,0 +1,82 @@
|
|||||||
|
// Resident hero policy (engine memory):
|
||||||
|
// - After the last WebSocket disconnect, the hero stays in Engine.movements; the world keeps ticking.
|
||||||
|
// - Cold start: ListHeroesForEngineBootstrap loads rows with ws_disconnected_at set (cap 500 default in main).
|
||||||
|
// - Full hero row is saved every offlineDisconnectedFullSaveInterval while heroSubscriber reports false.
|
||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/denisovdennis/autohero/internal/model"
|
||||||
|
"github.com/denisovdennis/autohero/internal/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BootstrapResidentHeroes loads heroes whose WebSocket session had ended before this process started,
|
||||||
|
// catches up wall time using the same batch path as server-downtime recovery, then registers them
|
||||||
|
// in the engine so movement and combat continue without a live subscriber.
|
||||||
|
func BootstrapResidentHeroes(ctx context.Context, e *Engine, heroStore *storage.HeroStore, sim *OfflineSimulator, limit int, logger *slog.Logger) {
|
||||||
|
if e == nil || heroStore == nil || sim == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
heroes, err := heroStore.ListHeroesForEngineBootstrap(ctx, limit)
|
||||||
|
if err != nil {
|
||||||
|
if logger != nil {
|
||||||
|
logger.Error("engine bootstrap: list heroes", "error", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
for _, h := range heroes {
|
||||||
|
if h == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
e.mu.Lock()
|
||||||
|
_, already := e.movements[h.ID]
|
||||||
|
rg := e.roadGraph
|
||||||
|
e.mu.Unlock()
|
||||||
|
if already || rg == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
e.mergeTownSessionFromRedis(h)
|
||||||
|
|
||||||
|
if err := sim.SimulateHeroAt(ctx, h, now, true); err != nil {
|
||||||
|
if logger != nil {
|
||||||
|
logger.Error("engine bootstrap: catch-up sim", "hero_id", h.ID, "error", err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
e.mu.Lock()
|
||||||
|
if e.roadGraph == nil {
|
||||||
|
e.mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, taken := e.movements[h.ID]; taken {
|
||||||
|
e.mu.Unlock()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
hm := NewHeroMovement(h, e.roadGraph, now)
|
||||||
|
e.movements[h.ID] = hm
|
||||||
|
hm.MarkTownPausePersisted(hm.townPausePersistSignature())
|
||||||
|
hm.SyncToHero()
|
||||||
|
if hm.State == model.StateFighting {
|
||||||
|
if _, exists := e.combats[h.ID]; !exists {
|
||||||
|
en := PickEnemyForHero(h)
|
||||||
|
if en.Slug != "" {
|
||||||
|
e.startCombatLocked(hm.Hero, &en)
|
||||||
|
} else {
|
||||||
|
hm.State = model.StateWalking
|
||||||
|
hm.Hero.State = model.StateWalking
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.mu.Unlock()
|
||||||
|
|
||||||
|
if logger != nil {
|
||||||
|
logger.Info("engine bootstrap: resident hero registered", "hero_id", h.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/denisovdennis/autohero/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHeroSocketDetachedKeepsMovement(t *testing.T) {
|
||||||
|
e := NewEngine(100*time.Millisecond, make(chan model.CombatEvent, 8), slog.Default())
|
||||||
|
e.SetRoadGraph(testGraph())
|
||||||
|
|
||||||
|
h := &model.Hero{
|
||||||
|
ID: 1, State: model.StateWalking, HP: 10, MaxHP: 10, Level: 1,
|
||||||
|
PositionX: 1, PositionY: 1,
|
||||||
|
}
|
||||||
|
hm := NewHeroMovement(h, testGraph(), time.Now())
|
||||||
|
e.mu.Lock()
|
||||||
|
e.movements[1] = hm
|
||||||
|
e.mu.Unlock()
|
||||||
|
|
||||||
|
disconnectAt := time.Date(2026, 4, 1, 12, 0, 0, 0, time.UTC)
|
||||||
|
e.HeroSocketDetached(1, true, disconnectAt)
|
||||||
|
|
||||||
|
e.mu.RLock()
|
||||||
|
_, ok := e.movements[1]
|
||||||
|
e.mu.RUnlock()
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected hero to remain resident in engine after last WS disconnect")
|
||||||
|
}
|
||||||
|
if h.WsDisconnectedAt == nil || !h.WsDisconnectedAt.Equal(disconnectAt) {
|
||||||
|
t.Fatalf("expected WsDisconnectedAt on in-memory hero, got %v", h.WsDisconnectedAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeResidentHeroState(t *testing.T) {
|
||||||
|
e := NewEngine(100*time.Millisecond, make(chan model.CombatEvent, 8), slog.Default())
|
||||||
|
e.SetRoadGraph(testGraph())
|
||||||
|
|
||||||
|
dst := &model.Hero{ID: 7, State: model.StateWalking, HP: 5, MaxHP: 10, Level: 2}
|
||||||
|
if e.MergeResidentHeroState(dst) {
|
||||||
|
t.Fatal("expected false when hero not resident")
|
||||||
|
}
|
||||||
|
|
||||||
|
h := &model.Hero{
|
||||||
|
ID: 7, State: model.StateWalking, HP: 9, MaxHP: 10, Level: 3,
|
||||||
|
PositionX: 2, PositionY: 3,
|
||||||
|
}
|
||||||
|
hm := NewHeroMovement(h, testGraph(), time.Now())
|
||||||
|
e.mu.Lock()
|
||||||
|
e.movements[7] = hm
|
||||||
|
e.mu.Unlock()
|
||||||
|
|
||||||
|
dst2 := &model.Hero{ID: 7, HP: 1, Level: 1}
|
||||||
|
if !e.MergeResidentHeroState(dst2) {
|
||||||
|
t.Fatal("expected true when resident")
|
||||||
|
}
|
||||||
|
if dst2.HP != 9 || dst2.Level != 3 {
|
||||||
|
t.Fatalf("expected engine stats copied, got hp=%d level=%d", dst2.HP, dst2.Level)
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,160 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/denisovdennis/autohero/internal/model"
|
||||||
|
"github.com/denisovdennis/autohero/internal/tuning"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBandIndexForHeroLevel(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
L int
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{1, 0}, {9, 0},
|
||||||
|
{10, 1}, {19, 1},
|
||||||
|
{20, 2}, {29, 2},
|
||||||
|
{30, 3}, {39, 3},
|
||||||
|
{40, 4}, {49, 4},
|
||||||
|
{0, -1}, {50, -1},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
if got := bandIndexForHeroLevel(tc.L); got != tc.want {
|
||||||
|
t.Fatalf("bandIndexForHeroLevel(%d)=%d want %d", tc.L, got, tc.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTemplateProgressionBand(t *testing.T) {
|
||||||
|
if TemplateProgressionBand(model.Enemy{MinLevel: 1, MaxLevel: 5, BaseLevel: 1}) != 0 {
|
||||||
|
t.Fatal("low-tier template should be band 0")
|
||||||
|
}
|
||||||
|
// Midpoint (25+35)/2 = 30 → tier band index 2 (see TemplateProgressionBand).
|
||||||
|
if TemplateProgressionBand(model.Enemy{MinLevel: 25, MaxLevel: 35, BaseLevel: 25}) != 2 {
|
||||||
|
t.Fatalf("mid 30 should map to template band 2, got %d", TemplateProgressionBand(model.Enemy{MinLevel: 25, MaxLevel: 35}))
|
||||||
|
}
|
||||||
|
if TemplateProgressionBand(model.Enemy{MinLevel: 41, MaxLevel: 45, BaseLevel: 41}) != 4 {
|
||||||
|
t.Fatalf("late template should map to band 4, got %d", TemplateProgressionBand(model.Enemy{MinLevel: 41, MaxLevel: 45}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAggregateBandDurationsPure(t *testing.T) {
|
||||||
|
// One second per level-up; 9+10+10+10+10 = 49 levels
|
||||||
|
sec := make([]float64, 49)
|
||||||
|
for i := range sec {
|
||||||
|
sec[i] = 1
|
||||||
|
}
|
||||||
|
var band [5]time.Duration
|
||||||
|
for idx, L := range levelRange(1, 49) {
|
||||||
|
bi := bandIndexForHeroLevel(L)
|
||||||
|
if bi >= 0 {
|
||||||
|
band[bi] += time.Duration(sec[idx] * float64(time.Second))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if band[0] != 9*time.Second {
|
||||||
|
t.Fatalf("band0: %v", band[0])
|
||||||
|
}
|
||||||
|
for i := 1; i <= 4; i++ {
|
||||||
|
if band[i] != 10*time.Second {
|
||||||
|
t.Fatalf("band %d: %v", i, band[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGlobalScaleMonotonicTotalTime(t *testing.T) {
|
||||||
|
ensureTestEnemyTemplates()
|
||||||
|
cfg := tuning.DefaultValues()
|
||||||
|
tuning.Set(cfg)
|
||||||
|
t.Cleanup(func() { tuning.Set(tuning.DefaultValues()) })
|
||||||
|
|
||||||
|
base := CloneEnemyTemplates(EnemyTemplatesFromSlice(model.EnemyTemplates))
|
||||||
|
params := ProgressionSimParams{
|
||||||
|
IterationsPerLevel: 6,
|
||||||
|
Seed: 42,
|
||||||
|
RestAfterCombat: 10 * time.Second,
|
||||||
|
Gear: ReferenceGearMedian,
|
||||||
|
AccountLosses: false,
|
||||||
|
MinHeroLevel: 1,
|
||||||
|
MaxHeroLevelInclusive: 15,
|
||||||
|
}
|
||||||
|
|
||||||
|
slow := ApplyXPRewardScaleSpec(base, XPRewardScaleSpec{Global: 0.2, Elite: 1, PerBand: [5]float64{1, 1, 1, 1, 1}})
|
||||||
|
fast := ApplyXPRewardScaleSpec(base, XPRewardScaleSpec{Global: 5.0, Elite: 1, PerBand: [5]float64{1, 1, 1, 1, 1}})
|
||||||
|
|
||||||
|
rSlow, err := SimulateProgressionBands(params, slow)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
rFast, err := SimulateProgressionBands(params, fast)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if rSlow.Total <= rFast.Total {
|
||||||
|
t.Fatalf("expected higher XP scale to reduce total time: slow=%s fast=%s", rSlow.Total, rFast.Total)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProratedBandTargets(t *testing.T) {
|
||||||
|
full := DefaultProgressionBandTargets
|
||||||
|
// Through L29: full bands 0,1,2 → 1+3+6 weeks
|
||||||
|
pr := ProratedBandTargets(29, full)
|
||||||
|
sum := SumBandTargets(pr)
|
||||||
|
wantWeeks := 10 // 1+3+6
|
||||||
|
if got := int(sum / (7 * 24 * time.Hour)); got != wantWeeks {
|
||||||
|
t.Fatalf("ProratedBandTargets(29) sum weeks=%d want %d (%v)", got, wantWeeks, pr)
|
||||||
|
}
|
||||||
|
// Through L9: only band 0 full
|
||||||
|
pr2 := ProratedBandTargets(9, full)
|
||||||
|
if SumBandTargets(pr2) != full[0] {
|
||||||
|
t.Fatalf("band0 only: %v", pr2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnforceMonotonicXPRewardByTier(t *testing.T) {
|
||||||
|
m := map[string]model.Enemy{
|
||||||
|
"a": {Slug: "a", MinLevel: 1, MaxLevel: 5, XPReward: 10},
|
||||||
|
"b": {Slug: "b", MinLevel: 20, MaxLevel: 30, XPReward: 5},
|
||||||
|
}
|
||||||
|
out := EnforceMonotonicXPRewardByTier(m)
|
||||||
|
if out["b"].XPReward <= out["a"].XPReward {
|
||||||
|
t.Fatalf("higher tier should have strictly higher xp: a=%d b=%d", out["a"].XPReward, out["b"].XPReward)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOptimizePerTypeScalesRuns(t *testing.T) {
|
||||||
|
ensureTestEnemyTemplates()
|
||||||
|
cfg := tuning.DefaultValues()
|
||||||
|
tuning.Set(cfg)
|
||||||
|
t.Cleanup(func() { tuning.Set(tuning.DefaultValues()) })
|
||||||
|
base := CloneEnemyTemplates(EnemyTemplatesFromSlice(model.EnemyTemplates))
|
||||||
|
params := ProgressionSimParams{
|
||||||
|
IterationsPerLevel: 3,
|
||||||
|
Seed: 1,
|
||||||
|
RestAfterCombat: 5 * time.Second,
|
||||||
|
Gear: ReferenceGearMedian,
|
||||||
|
MinHeroLevel: 1,
|
||||||
|
MaxHeroLevelInclusive: 6,
|
||||||
|
}
|
||||||
|
targets := ProratedBandTargets(6, DefaultProgressionBandTargets)
|
||||||
|
m, _, res, sq := OptimizePerTypeScales(base, params, targets, 1, 6, false)
|
||||||
|
if len(m) != len(base) {
|
||||||
|
t.Fatalf("len(perType)=%d len(base)=%d", len(m), len(base))
|
||||||
|
}
|
||||||
|
if math.IsNaN(sq) {
|
||||||
|
t.Fatal("sqErr nan")
|
||||||
|
}
|
||||||
|
if math.IsInf(res.TotalSec, 1) {
|
||||||
|
t.Skip("combat unwinnable in testdata")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSquaredErrorSum(t *testing.T) {
|
||||||
|
sim := [5]time.Duration{10 * time.Second, 20 * time.Second, 30 * time.Second, 40 * time.Second, 50 * time.Second}
|
||||||
|
tg := [5]time.Duration{10 * time.Second, 10 * time.Second, 10 * time.Second, 10 * time.Second, 10 * time.Second}
|
||||||
|
if SquaredErrorSum(sim, tg) <= 0 {
|
||||||
|
t.Fatal("expected positive error")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
ensureTestEnemyTemplates()
|
||||||
|
os.Exit(m.Run())
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import "github.com/denisovdennis/autohero/internal/model"
|
||||||
|
|
||||||
|
// TownEffectiveLevel is the reference level for shop gear ilvl / quest bands (mid of town bracket).
|
||||||
|
func TownEffectiveLevel(t *model.Town) int {
|
||||||
|
if t == nil {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
mid := (t.LevelMin + t.LevelMax) / 2
|
||||||
|
if mid < 1 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return mid
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
// AdventureLogEvent is persisted and sent over WebSocket.
|
||||||
|
// Code is a phrase key (dot-separated), e.g. log.defeated_enemy, roadside.silence_loading, town_visit.merchant.bell_traveler_pack.
|
||||||
|
// Args must be structured only: stable ids (enemyType, npcKey, questKey, achievementId), numbers, bools — no display sentences.
|
||||||
|
type AdventureLogEvent struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Args map[string]any `json:"args,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdventureLogLine is written to the DB and sent over WebSocket.
|
||||||
|
// Message is legacy plain text only; new rows use Event with phrase key and empty Message.
|
||||||
|
type AdventureLogLine struct {
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
Event *AdventureLogEvent `json:"event,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wandering merchant (road encounter) — stable keys for other packages (NPC labels live in client i18n).
|
||||||
|
const (
|
||||||
|
WanderingMerchantNPCKey = "npc.wandering_merchant.v1"
|
||||||
|
WanderingMerchantDialogueKey = "npc.wandering_merchant.dialogue.v1"
|
||||||
|
)
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAdventureLogLine_JSON_roundTrip(t *testing.T) {
|
||||||
|
line := AdventureLogLine{
|
||||||
|
Message: "legacy",
|
||||||
|
Event: &AdventureLogEvent{
|
||||||
|
Code: LogPhraseDefeatedEnemy,
|
||||||
|
Args: map[string]any{"enemyType": "wolf_l1_1_meadow", "xp": float64(10), "gold": float64(5)},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(line)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var got AdventureLogLine
|
||||||
|
if err := json.Unmarshal(b, &got); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got.Message != line.Message {
|
||||||
|
t.Fatalf("message: got %q want %q", got.Message, line.Message)
|
||||||
|
}
|
||||||
|
if got.Event == nil || got.Event.Code != LogPhraseDefeatedEnemy {
|
||||||
|
t.Fatalf("event code: %+v", got.Event)
|
||||||
|
}
|
||||||
|
if got.Event.Args["enemyType"] != "wolf_l1_1_meadow" {
|
||||||
|
t.Fatalf("args: %+v", got.Event.Args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdventureLogLine_JSON_legacyMessageOnly(t *testing.T) {
|
||||||
|
raw := `{"message":"hello"}`
|
||||||
|
var got AdventureLogLine
|
||||||
|
if err := json.Unmarshal([]byte(raw), &got); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got.Message != "hello" || got.Event != nil {
|
||||||
|
t.Fatalf("got %+v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,92 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
// Phrase keys for adventure_log.event_code / WS adventure_log_line.event.code.
|
||||||
|
// No human-readable text on the server — only keys and structured args.
|
||||||
|
|
||||||
|
const (
|
||||||
|
LogPhraseDefeatedEnemy = "log.defeated_enemy"
|
||||||
|
LogPhraseLeveledUp = "log.leveled_up"
|
||||||
|
LogPhraseEquippedNew = "log.equipped_new"
|
||||||
|
LogPhraseInventoryFullDropped = "log.inventory_full_dropped"
|
||||||
|
LogPhraseBuffActivated = "log.buff_activated"
|
||||||
|
LogPhraseHeroRevived = "log.hero_revived"
|
||||||
|
LogPhraseWanderingMerchant = "log.wandering_merchant_encounter"
|
||||||
|
LogPhraseEncounteredEnemy = "log.encountered_enemy"
|
||||||
|
LogPhraseDiedFighting = "log.died_fighting"
|
||||||
|
LogPhraseAutoReviveHours = "log.auto_revive_hours"
|
||||||
|
LogPhraseAutoReviveAfterSec = "log.auto_revive_after_sec"
|
||||||
|
LogPhrasePurchasedBuffRefill = "log.purchased_buff_refill"
|
||||||
|
LogPhrasePurchasedBuffRefillRub = "log.purchased_buff_refill_rub"
|
||||||
|
LogPhraseSubscribed = "log.subscribed"
|
||||||
|
LogPhraseUsedHealingPotion = "log.used_healing_potion"
|
||||||
|
LogPhraseAchievementUnlocked = "log.achievement_unlocked"
|
||||||
|
LogPhraseMetNPC = "log.met_npc"
|
||||||
|
LogPhraseWanderingAlmsEquipped = "log.wandering_alms_equipped"
|
||||||
|
LogPhraseWanderingAlmsDropped = "log.wandering_alms_dropped"
|
||||||
|
LogPhraseWanderingAlmsStashed = "log.wandering_alms_stashed"
|
||||||
|
LogPhraseHealedFullTown = "log.healed_full_town"
|
||||||
|
LogPhraseBoughtPotionTown = "log.bought_potion_town"
|
||||||
|
LogPhraseBoughtGearTownMerchant = "log.bought_gear_town_merchant"
|
||||||
|
LogPhraseSoldItemsMerchant = "log.sold_items_merchant"
|
||||||
|
LogPhraseNPCSkippedVisit = "log.npc_skipped_visit"
|
||||||
|
LogPhrasePurchasedPotionFromNPC = "log.purchased_potion_from_npc"
|
||||||
|
LogPhrasePaidHealerFull = "log.paid_healer_full"
|
||||||
|
LogPhraseQuestGiverChecked = "log.quest_giver_checked"
|
||||||
|
LogPhraseQuestAccepted = "log.quest_accepted"
|
||||||
|
LogPhraseCombatHeroHit = "log.combat.hero_hit"
|
||||||
|
LogPhraseCombatHeroDodge = "log.combat.hero_dodge"
|
||||||
|
LogPhraseCombatHeroStun = "log.combat.hero_stun"
|
||||||
|
LogPhraseCombatEnemyHit = "log.combat.enemy_hit"
|
||||||
|
LogPhraseCombatEnemyBlock = "log.combat.enemy_block"
|
||||||
|
LogPhraseCombatDebuffSuffix = "log.combat.debuff_suffix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Town visit line slugs per NPC kind (order = timed line 0..5). Unknown npcType uses generic slugs with key prefix "generic".
|
||||||
|
var townVisitLineSlugs = map[string][]string{
|
||||||
|
"merchant": {
|
||||||
|
"crates_in_shade",
|
||||||
|
"practiced_tired_smile",
|
||||||
|
"chalk_prices_twice",
|
||||||
|
"rumors_bandits_carts",
|
||||||
|
"bell_traveler_pack",
|
||||||
|
"step_back_tally_gold",
|
||||||
|
},
|
||||||
|
"healer": {
|
||||||
|
"linens_herbs_tent",
|
||||||
|
"professional_frown_onceover",
|
||||||
|
"slept_badly_nod",
|
||||||
|
"tonic_steams_table",
|
||||||
|
"blessings_salves_bandages",
|
||||||
|
"lighter_under_canvas",
|
||||||
|
},
|
||||||
|
"quest_giver": {
|
||||||
|
"scrolls_wax_desk",
|
||||||
|
"ink_stained_map_tap",
|
||||||
|
"busy_roads_noncommittal",
|
||||||
|
"draft_parchment_smell",
|
||||||
|
"squint_spine_legend",
|
||||||
|
"promise_listen_worth_it",
|
||||||
|
},
|
||||||
|
"generic": {
|
||||||
|
"town_noise_blanket",
|
||||||
|
"grain_prices_argument",
|
||||||
|
"dust_sunbeam_time",
|
||||||
|
"strap_tighten_pretend",
|
||||||
|
"dog_boring_sleeps",
|
||||||
|
"breathe_ready_move_on",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// TownVisitPhraseKey returns e.g. town_visit.merchant.bell_traveler_pack (lineIdx 0..5).
|
||||||
|
func TownVisitPhraseKey(npcType string, lineIdx int) string {
|
||||||
|
slugs, ok := townVisitLineSlugs[npcType]
|
||||||
|
keyType := npcType
|
||||||
|
if !ok {
|
||||||
|
slugs = townVisitLineSlugs["generic"]
|
||||||
|
keyType = "generic"
|
||||||
|
}
|
||||||
|
if lineIdx < 0 || lineIdx >= len(slugs) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return "town_visit." + keyType + "." + slugs[lineIdx]
|
||||||
|
}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRoadsideSlugsWellFormed(t *testing.T) {
|
||||||
|
if len(RoadsideSlugs) == 0 {
|
||||||
|
t.Fatal("RoadsideSlugs empty")
|
||||||
|
}
|
||||||
|
for _, s := range RoadsideSlugs {
|
||||||
|
if strings.Contains(s, ".") {
|
||||||
|
t.Fatalf("roadside slug must not contain dot: %q", s)
|
||||||
|
}
|
||||||
|
if RoadsidePhraseKey(s) != "roadside."+s {
|
||||||
|
t.Fatalf("RoadsidePhraseKey(%q)=%q want roadside.%s", s, RoadsidePhraseKey(s), s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTownVisitPhraseKeyUsesSlugs(t *testing.T) {
|
||||||
|
k := TownVisitPhraseKey("merchant", 4)
|
||||||
|
if k != "town_visit.merchant.bell_traveler_pack" {
|
||||||
|
t.Fatalf("got %q", k)
|
||||||
|
}
|
||||||
|
k2 := TownVisitPhraseKey("unknown_npc", 0)
|
||||||
|
if k2 != "town_visit.generic.town_noise_blanket" {
|
||||||
|
t.Fatalf("unknown type should use generic slugs, got %q", k2)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/denisovdennis/autohero/internal/tuning"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIlvlFactor_Geometric(t *testing.T) {
|
||||||
|
old := tuning.Get()
|
||||||
|
cfg := tuning.DefaultValues()
|
||||||
|
cfg.IlvlPerLevelMultiplier = 1.10
|
||||||
|
tuning.Set(cfg)
|
||||||
|
t.Cleanup(func() { tuning.Set(old) })
|
||||||
|
|
||||||
|
assertNear(t, IlvlFactor(1), 1.0, 1e-9)
|
||||||
|
assertNear(t, IlvlFactor(2), 1.10, 1e-9)
|
||||||
|
assertNear(t, IlvlFactor(10), math.Pow(1.10, 9), 1e-9)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRarityMultiplier_DefaultsAndFallback(t *testing.T) {
|
||||||
|
old := tuning.Get()
|
||||||
|
cfg := tuning.DefaultValues()
|
||||||
|
cfg.RarityMultiplierCommon = 1.00
|
||||||
|
cfg.RarityMultiplierUncommon = 1.0877573
|
||||||
|
cfg.RarityMultiplierRare = 0
|
||||||
|
cfg.RarityMultiplierEpic = 1.2870518
|
||||||
|
cfg.RarityMultiplierLegendary = 1.40
|
||||||
|
tuning.Set(cfg)
|
||||||
|
t.Cleanup(func() { tuning.Set(old) })
|
||||||
|
|
||||||
|
assertNear(t, RarityMultiplier(RarityCommon), 1.00, 1e-9)
|
||||||
|
assertNear(t, RarityMultiplier(RarityUncommon), 1.0877573, 1e-9)
|
||||||
|
assertNear(t, RarityMultiplier(RarityEpic), 1.2870518, 1e-9)
|
||||||
|
assertNear(t, RarityMultiplier(RarityLegendary), 1.40, 1e-9)
|
||||||
|
|
||||||
|
fallbackRare := tuning.DefaultValues().RarityMultiplierRare
|
||||||
|
assertNear(t, RarityMultiplier(RarityRare), fallbackRare, 1e-9)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScalePrimary_UsesIlvlAndRarity(t *testing.T) {
|
||||||
|
old := tuning.Get()
|
||||||
|
cfg := tuning.DefaultValues()
|
||||||
|
cfg.IlvlPerLevelMultiplier = 1.10
|
||||||
|
cfg.RarityMultiplierCommon = 1.00
|
||||||
|
cfg.RarityMultiplierLegendary = 1.40
|
||||||
|
tuning.Set(cfg)
|
||||||
|
t.Cleanup(func() { tuning.Set(old) })
|
||||||
|
|
||||||
|
if got := ScalePrimary(10, 2, RarityCommon); got != 11 {
|
||||||
|
t.Fatalf("ScalePrimary common ilvl2 = %d, want 11", got)
|
||||||
|
}
|
||||||
|
if got := ScalePrimary(10, 1, RarityLegendary); got != 14 {
|
||||||
|
t.Fatalf("ScalePrimary legendary ilvl1 = %d, want 14", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertNear(t *testing.T, got float64, want float64, eps float64) {
|
||||||
|
t.Helper()
|
||||||
|
if math.Abs(got-want) > eps {
|
||||||
|
t.Fatalf("got %.12f want %.12f (eps %.1e)", got, want, eps)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
// RestKind discriminates the context of a StateResting period.
|
||||||
|
type RestKind string
|
||||||
|
|
||||||
|
const (
|
||||||
|
RestKindNone RestKind = ""
|
||||||
|
RestKindTown RestKind = "town"
|
||||||
|
RestKindRoadside RestKind = "roadside"
|
||||||
|
RestKindAdventureInline RestKind = "adventure_inline"
|
||||||
|
)
|
||||||
@ -0,0 +1,62 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
// RoadsideSlugs are stable suffixes; full event codes are "roadside." + slug (matches en.yml / ru.yml keys under `roadside:`).
|
||||||
|
var RoadsideSlugs = []string{
|
||||||
|
"nothing_matters_crit",
|
||||||
|
"road_chose_you",
|
||||||
|
"coin_heavier_than_sword",
|
||||||
|
"consciousness_buff",
|
||||||
|
"grass_philosophical",
|
||||||
|
"braver_tomorrow",
|
||||||
|
"hero_job_or_tax",
|
||||||
|
"scars_bookmarks",
|
||||||
|
"breaths_on_purpose",
|
||||||
|
"universe_simulation_texture",
|
||||||
|
"resting_cheating_alive",
|
||||||
|
"real_loot_npcs",
|
||||||
|
"memoir_soup",
|
||||||
|
"time_circle_hp",
|
||||||
|
"silence_loading",
|
||||||
|
"meaning_lunch_later",
|
||||||
|
"trees_gossip_breaks",
|
||||||
|
"courage_silly_face",
|
||||||
|
"miss_never_met",
|
||||||
|
"wind_advice_ignore",
|
||||||
|
"gold_boots_happiness",
|
||||||
|
"gratitude_not_dummy",
|
||||||
|
"legend_sat_tired",
|
||||||
|
"fear_debuff_curiosity",
|
||||||
|
"slimes_electric_sheep",
|
||||||
|
"patience_skill_tree",
|
||||||
|
"road_crooked_stand",
|
||||||
|
"narrate_life_xp",
|
||||||
|
"gods_patch_notes",
|
||||||
|
"rock_throne_dramatic",
|
||||||
|
"forgive_panic_roll",
|
||||||
|
"love_side_quest",
|
||||||
|
"thoughts_loot_encumbered",
|
||||||
|
"sun_sets_optimize",
|
||||||
|
"fate_bad_ui",
|
||||||
|
"wounded_poetic_upgrade",
|
||||||
|
"heroic_pose_nobody",
|
||||||
|
"wisdom_stop_swinging",
|
||||||
|
"endgame_good_chair",
|
||||||
|
"doubt_armor_unkillable",
|
||||||
|
"world_spinning_pause",
|
||||||
|
"bird_screams_relate",
|
||||||
|
"regrets_shorter_list",
|
||||||
|
"hope_hp_cynical_patch",
|
||||||
|
"courage_stubborn_pr",
|
||||||
|
"merchants_fixed_prices",
|
||||||
|
"pause_rebellion_grind",
|
||||||
|
"dirt_nails_showed_up",
|
||||||
|
"meaning_hammer",
|
||||||
|
"smile_nothing_helps",
|
||||||
|
"tomorrow_walk_tonight_breathe",
|
||||||
|
"grind_volume_down",
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoadsidePhraseKey returns the full phrase code for a slug suffix.
|
||||||
|
func RoadsidePhraseKey(slug string) string {
|
||||||
|
return "roadside." + slug
|
||||||
|
}
|
||||||
@ -0,0 +1,121 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
|
||||||
|
"github.com/denisovdennis/autohero/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OfflineDigestRow is persisted counters + loot lines for the post-offline summary.
|
||||||
|
type OfflineDigestRow struct {
|
||||||
|
MonstersKilled int `json:"monstersKilled"`
|
||||||
|
XPGained int64 `json:"xpGained"`
|
||||||
|
GoldGained int64 `json:"goldGained"`
|
||||||
|
LevelsGained int `json:"levelsGained"`
|
||||||
|
Deaths int `json:"deaths"`
|
||||||
|
Revives int `json:"revives"`
|
||||||
|
Loot []model.LootDrop `json:"loot"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// OfflineDigestStore accumulates hero_offline_digest rows.
|
||||||
|
type OfflineDigestStore struct {
|
||||||
|
pool *pgxpool.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOfflineDigestStore(pool *pgxpool.Pool) *OfflineDigestStore {
|
||||||
|
return &OfflineDigestStore{pool: pool}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyDelta merges a delta into the hero's digest row (upsert).
|
||||||
|
func (s *OfflineDigestStore) ApplyDelta(ctx context.Context, heroID int64, d OfflineDigestDelta) error {
|
||||||
|
if d.MonstersKilled == 0 && d.XPGained == 0 && d.GoldGained == 0 && d.LevelsGained == 0 &&
|
||||||
|
d.Deaths == 0 && d.Revives == 0 && len(d.LootAppend) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
lootFragment := "[]"
|
||||||
|
if len(d.LootAppend) > 0 {
|
||||||
|
b, err := json.Marshal(d.LootAppend)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshal loot fragment: %w", err)
|
||||||
|
}
|
||||||
|
lootFragment = string(b)
|
||||||
|
}
|
||||||
|
_, err := s.pool.Exec(ctx, `
|
||||||
|
INSERT INTO hero_offline_digest (
|
||||||
|
hero_id, monsters_killed, xp_gained, gold_gained, levels_gained, deaths, revives, loot, updated_at
|
||||||
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, now())
|
||||||
|
ON CONFLICT (hero_id) DO UPDATE SET
|
||||||
|
monsters_killed = hero_offline_digest.monsters_killed + EXCLUDED.monsters_killed,
|
||||||
|
xp_gained = hero_offline_digest.xp_gained + EXCLUDED.xp_gained,
|
||||||
|
gold_gained = hero_offline_digest.gold_gained + EXCLUDED.gold_gained,
|
||||||
|
levels_gained = hero_offline_digest.levels_gained + EXCLUDED.levels_gained,
|
||||||
|
deaths = hero_offline_digest.deaths + EXCLUDED.deaths,
|
||||||
|
revives = hero_offline_digest.revives + EXCLUDED.revives,
|
||||||
|
loot = hero_offline_digest.loot || EXCLUDED.loot,
|
||||||
|
updated_at = now()
|
||||||
|
`, heroID, d.MonstersKilled, d.XPGained, d.GoldGained, d.LevelsGained, d.Deaths, d.Revives, lootFragment)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("apply offline digest delta: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OfflineDigestDelta is a single batch of offline stats to merge.
|
||||||
|
type OfflineDigestDelta struct {
|
||||||
|
MonstersKilled int
|
||||||
|
XPGained int64
|
||||||
|
GoldGained int64
|
||||||
|
LevelsGained int
|
||||||
|
Deaths int
|
||||||
|
Revives int
|
||||||
|
LootAppend []model.LootDrop
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns the current digest row or zero values if missing.
|
||||||
|
func (s *OfflineDigestStore) Get(ctx context.Context, heroID int64) (OfflineDigestRow, error) {
|
||||||
|
var row OfflineDigestRow
|
||||||
|
var lootRaw []byte
|
||||||
|
err := s.pool.QueryRow(ctx, `
|
||||||
|
SELECT monsters_killed, xp_gained, gold_gained, levels_gained, deaths, revives, loot, updated_at
|
||||||
|
FROM hero_offline_digest WHERE hero_id = $1
|
||||||
|
`, heroID).Scan(
|
||||||
|
&row.MonstersKilled, &row.XPGained, &row.GoldGained, &row.LevelsGained,
|
||||||
|
&row.Deaths, &row.Revives, &lootRaw, &row.UpdatedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
row.Loot = []model.LootDrop{}
|
||||||
|
return row, nil
|
||||||
|
}
|
||||||
|
return OfflineDigestRow{}, fmt.Errorf("get offline digest: %w", err)
|
||||||
|
}
|
||||||
|
if len(lootRaw) > 0 {
|
||||||
|
if err := json.Unmarshal(lootRaw, &row.Loot); err != nil {
|
||||||
|
row.Loot = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if row.Loot == nil {
|
||||||
|
row.Loot = []model.LootDrop{}
|
||||||
|
}
|
||||||
|
return row, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TakeDelete returns the digest and removes the row (for handing summary to client once).
|
||||||
|
func (s *OfflineDigestStore) TakeDelete(ctx context.Context, heroID int64) (OfflineDigestRow, error) {
|
||||||
|
row, err := s.Get(ctx, heroID)
|
||||||
|
if err != nil {
|
||||||
|
return OfflineDigestRow{}, err
|
||||||
|
}
|
||||||
|
if _, err := s.pool.Exec(ctx, `DELETE FROM hero_offline_digest WHERE hero_id = $1`, heroID); err != nil {
|
||||||
|
return OfflineDigestRow{}, fmt.Errorf("delete offline digest: %w", err)
|
||||||
|
}
|
||||||
|
return row, nil
|
||||||
|
}
|
||||||
@ -1,17 +1,24 @@
|
|||||||
package tuning
|
package tuning
|
||||||
|
|
||||||
// Defaults for enemy→hero damage (runtime_config JSON keys: enemyCombatDamageScale, enemyCombatDamageRollMin, enemyCombatDamageRollMax).
|
// Defaults for enemy→hero damage (runtime_config JSON keys: enemyCombatDamageScale, enemyCombatDamageRollMin, enemyCombatDamageRollMax).
|
||||||
|
// Kept in proportion to combatPaceMultiplier vs legacy (28): same incoming DPS when attack intervals shrink.
|
||||||
|
// DefaultEnemyAttackIntervalMultiplier stretches only enemy swing spacing; DefaultEnemyCombatDamageScale is paired so incoming DPS stays in the same ballpark.
|
||||||
const (
|
const (
|
||||||
DefaultEnemyCombatDamageScale = 1.0
|
DefaultEnemyAttackIntervalMultiplier = 1.5 // enemyAttackIntervalMultiplier
|
||||||
DefaultEnemyCombatDamageRollMin = 0.8
|
DefaultEnemyCombatDamageScale = 1.0 // enemyCombatDamageScale
|
||||||
DefaultEnemyCombatDamageRollMax = 1.0
|
DefaultEnemyCombatDamageRollMin = 0.82
|
||||||
|
DefaultEnemyCombatDamageRollMax = 1.0
|
||||||
)
|
)
|
||||||
|
|
||||||
// Enemy HP regen: fraction of MaxHP healed per second (runtime_config JSON keys below).
|
// Enemy HP regen: fraction of MaxHP healed per second (runtime_config JSON keys below).
|
||||||
|
// Hero attack intervals are often multi-second; regen accumulates over the full gap — keep rates low
|
||||||
|
// so net DPS stays positive (e.g. 0.003 ≈ 0.3%/s → ~3% MaxHP over a 10s gap).
|
||||||
// Loaded from DB via tuning.ReloadNow; use EffectiveEnemyRegen* when a positive DB value is required.
|
// Loaded from DB via tuning.ReloadNow; use EffectiveEnemyRegen* when a positive DB value is required.
|
||||||
const (
|
const (
|
||||||
DefaultEnemyRegenDefault = 0.02 // enemyRegenDefault
|
// Fraction of MaxHP healed per second. Must stay below hero sustained DPS / MaxHP at reference gear
|
||||||
DefaultEnemyRegenSkeletonKing = 0.04 // enemyRegenSkeletonKing
|
// or regen stalemates (long fights / maxSteps losses).
|
||||||
DefaultEnemyRegenForestWarden = 0.05 // enemyRegenForestWarden
|
DefaultEnemyRegenDefault = 0.0012 // enemyRegenDefault
|
||||||
DefaultEnemyRegenBattleLizard = 0.01 // enemyRegenBattleLizard
|
DefaultEnemyRegenSkeletonKing = 0.00003 // enemyRegenSkeletonKing
|
||||||
|
DefaultEnemyRegenForestWarden = 0.00010 // enemyRegenForestWarden
|
||||||
|
DefaultEnemyRegenBattleLizard = 0.0005 // enemyRegenBattleLizard
|
||||||
)
|
)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,2 @@
|
|||||||
|
-- Tracking lives in infra.schema_migrations; remove duplicate table left from 000001_init dump.
|
||||||
|
DROP TABLE IF EXISTS public.schema_migrations;
|
||||||
@ -1,8 +0,0 @@
|
|||||||
-- Free revive quota for non-subscribers (MVP: 2 lifetime revives unless subscription_active).
|
|
||||||
|
|
||||||
ALTER TABLE heroes
|
|
||||||
ADD COLUMN IF NOT EXISTS revive_count INT NOT NULL DEFAULT 0,
|
|
||||||
ADD COLUMN IF NOT EXISTS subscription_active BOOLEAN NOT NULL DEFAULT FALSE;
|
|
||||||
|
|
||||||
COMMENT ON COLUMN heroes.revive_count IS 'Number of revives consumed (free tier capped at 2 without subscription).';
|
|
||||||
COMMENT ON COLUMN heroes.subscription_active IS 'When true, revive limit does not apply.';
|
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
-- Enemy combat stats from balanceall grid (tiered targets, DoT-aware path for burn/poison).
|
||||||
|
-- fire_demon: tuned with burn enabled (grid dot branch in cmd/balanceall).
|
||||||
|
|
||||||
|
UPDATE enemies SET hp = 94, max_hp = 94, hp_per_level = 7.8681, attack = 20, attack_per_level = 2.7054 WHERE type = 'wolf';
|
||||||
|
UPDATE enemies SET hp = 102, max_hp = 102, hp_per_level = 8.2826, attack = 25, attack_per_level = 2.3190 WHERE type = 'boar';
|
||||||
|
UPDATE enemies SET hp = 107, max_hp = 107, hp_per_level = 6.9412, attack = 28, attack_per_level = 2.5898 WHERE type = 'zombie';
|
||||||
|
UPDATE enemies SET hp = 118, max_hp = 118, hp_per_level = 12.0614, attack = 24, attack_per_level = 2.7373 WHERE type = 'spider';
|
||||||
|
UPDATE enemies SET hp = 113, max_hp = 113, hp_per_level = 7.1338, attack = 27, attack_per_level = 2.6581 WHERE type = 'orc';
|
||||||
|
UPDATE enemies SET hp = 132, max_hp = 132, hp_per_level = 8.5586, attack = 28, attack_per_level = 2.2939 WHERE type = 'skeleton_archer';
|
||||||
|
UPDATE enemies SET hp = 105, max_hp = 105, hp_per_level = 5.7476, attack = 32, attack_per_level = 2.4140 WHERE type = 'battle_lizard';
|
||||||
|
UPDATE enemies SET hp = 177, max_hp = 177, hp_per_level = 11.7200, attack = 25, attack_per_level = 2.6587 WHERE type = 'fire_demon';
|
||||||
|
UPDATE enemies SET hp = 208, max_hp = 208, hp_per_level = 7.5649, attack = 37, attack_per_level = 3.0394 WHERE type = 'ice_guardian';
|
||||||
|
UPDATE enemies SET hp = 149, max_hp = 149, hp_per_level = 4.1663, attack = 24, attack_per_level = 1.8339 WHERE type = 'skeleton_king';
|
||||||
|
UPDATE enemies SET hp = 349, max_hp = 349, hp_per_level = 8.0285, attack = 45, attack_per_level = 3.1288 WHERE type = 'water_element';
|
||||||
|
UPDATE enemies SET hp = 338, max_hp = 338, hp_per_level = 6.1288, attack = 50, attack_per_level = 3.5033 WHERE type = 'forest_warden';
|
||||||
|
UPDATE enemies SET hp = 583, max_hp = 583, hp_per_level = 11.1055, attack = 48, attack_per_level = 2.9104 WHERE type = 'lightning_titan';
|
||||||
@ -1,9 +0,0 @@
|
|||||||
-- Free-tier buff activations: 3 per rolling 24h window (spec daily task "Use 3 Buffs").
|
|
||||||
-- Subscribers ignore quota (subscription_active).
|
|
||||||
|
|
||||||
ALTER TABLE heroes
|
|
||||||
ADD COLUMN IF NOT EXISTS buff_free_charges_remaining INT NOT NULL DEFAULT 3,
|
|
||||||
ADD COLUMN IF NOT EXISTS buff_quota_period_end TIMESTAMPTZ NULL;
|
|
||||||
|
|
||||||
COMMENT ON COLUMN heroes.buff_free_charges_remaining IS 'Free buff activations left in current window (non-subscribers; resets when period rolls).';
|
|
||||||
COMMENT ON COLUMN heroes.buff_quota_period_end IS 'End of current 24h buff quota window; NULL until first activation in a session.';
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
-- Migration: add hero position, potions, and adventure log.
|
|
||||||
|
|
||||||
-- Hero position persists across sessions so the client can restore the visual location.
|
|
||||||
ALTER TABLE heroes ADD COLUMN IF NOT EXISTS position_x DOUBLE PRECISION NOT NULL DEFAULT 0;
|
|
||||||
ALTER TABLE heroes ADD COLUMN IF NOT EXISTS position_y DOUBLE PRECISION NOT NULL DEFAULT 0;
|
|
||||||
|
|
||||||
-- Potions inventory (healing potions from monster drops).
|
|
||||||
ALTER TABLE heroes ADD COLUMN IF NOT EXISTS potions INT NOT NULL DEFAULT 0;
|
|
||||||
|
|
||||||
-- Adventure log: a chronological list of notable in-game events per hero.
|
|
||||||
CREATE TABLE IF NOT EXISTS adventure_log (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
hero_id BIGINT NOT NULL REFERENCES heroes(id) ON DELETE CASCADE,
|
|
||||||
message TEXT NOT NULL,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_adventure_log_hero_created
|
|
||||||
ON adventure_log (hero_id, created_at DESC);
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
-- Replace shared buff quota with per-buff quotas.
|
|
||||||
-- Each buff type gets its own charge counter and period window.
|
|
||||||
-- buff_charges stores: {"rush": {"remaining": 5, "periodEnd": "2026-03-29T00:00:00Z"}, ...}
|
|
||||||
|
|
||||||
ALTER TABLE heroes ADD COLUMN IF NOT EXISTS buff_charges JSONB NOT NULL DEFAULT '{}';
|
|
||||||
|
|
||||||
COMMENT ON COLUMN heroes.buff_charges IS 'Per-buff-type free charge state: map of buff_type -> {remaining, periodEnd}. Replaces shared buff_free_charges_remaining.';
|
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
-- xp_reward: tuned with xpprogsim -optimize-types (per-row, no global scale).
|
||||||
|
-- Constraints: -max-level 29, -enforce-tier-xp (xp_reward non-decreasing with level tier), prorated targets = 10 weeks.
|
||||||
|
-- Final sim vs prorated target: total ~-1.7%; bands 1→10 / 10→20 / 20→30 within ~4% (MC variance).
|
||||||
|
-- Regenerate: go run ./cmd/xpprogsim -optimize-types -max-level 29 -enforce-tier-xp -sql-all
|
||||||
|
|
||||||
|
UPDATE public.enemies SET xp_reward = 1 WHERE type = 'wolf';
|
||||||
|
UPDATE public.enemies SET xp_reward = 2 WHERE type = 'boar';
|
||||||
|
UPDATE public.enemies SET xp_reward = 5 WHERE type = 'zombie';
|
||||||
|
UPDATE public.enemies SET xp_reward = 7 WHERE type = 'spider';
|
||||||
|
UPDATE public.enemies SET xp_reward = 8 WHERE type = 'orc';
|
||||||
|
UPDATE public.enemies SET xp_reward = 9 WHERE type = 'skeleton_archer';
|
||||||
|
UPDATE public.enemies SET xp_reward = 10 WHERE type = 'battle_lizard';
|
||||||
|
UPDATE public.enemies SET xp_reward = 11 WHERE type = 'fire_demon';
|
||||||
|
UPDATE public.enemies SET xp_reward = 18 WHERE type = 'ice_guardian';
|
||||||
|
UPDATE public.enemies SET xp_reward = 35 WHERE type = 'skeleton_king';
|
||||||
|
UPDATE public.enemies SET xp_reward = 36 WHERE type = 'water_element';
|
||||||
|
UPDATE public.enemies SET xp_reward = 37 WHERE type = 'forest_warden';
|
||||||
|
UPDATE public.enemies SET xp_reward = 38 WHERE type = 'lightning_titan';
|
||||||
@ -1,247 +0,0 @@
|
|||||||
-- Migration 000006: Quest system — towns, NPCs, quests, hero quest tracking.
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- Towns: fixed settlements along the hero's travel road.
|
|
||||||
-- ============================================================
|
|
||||||
CREATE TABLE IF NOT EXISTS towns (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
name TEXT NOT NULL UNIQUE,
|
|
||||||
biome TEXT NOT NULL,
|
|
||||||
world_x DOUBLE PRECISION NOT NULL,
|
|
||||||
world_y DOUBLE PRECISION NOT NULL,
|
|
||||||
radius DOUBLE PRECISION NOT NULL DEFAULT 8.0,
|
|
||||||
level_min INT NOT NULL DEFAULT 1,
|
|
||||||
level_max INT NOT NULL DEFAULT 100,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- NPCs: non-hostile characters in towns.
|
|
||||||
-- ============================================================
|
|
||||||
CREATE TABLE IF NOT EXISTS npcs (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
town_id BIGINT NOT NULL REFERENCES towns(id) ON DELETE CASCADE,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
type TEXT NOT NULL CHECK (type IN ('quest_giver', 'merchant', 'healer')),
|
|
||||||
offset_x DOUBLE PRECISION NOT NULL DEFAULT 0,
|
|
||||||
offset_y DOUBLE PRECISION NOT NULL DEFAULT 0,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_npcs_town ON npcs(town_id);
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- Quests: template definitions offered by quest-giver NPCs.
|
|
||||||
-- ============================================================
|
|
||||||
CREATE TABLE IF NOT EXISTS quests (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
npc_id BIGINT NOT NULL REFERENCES npcs(id) ON DELETE CASCADE,
|
|
||||||
title TEXT NOT NULL,
|
|
||||||
description TEXT NOT NULL DEFAULT '',
|
|
||||||
type TEXT NOT NULL CHECK (type IN ('kill_count', 'visit_town', 'collect_item')),
|
|
||||||
target_count INT NOT NULL DEFAULT 1,
|
|
||||||
target_enemy_type TEXT, -- NULL = any enemy (for kill_count)
|
|
||||||
target_town_id BIGINT REFERENCES towns(id), -- for visit_town quests
|
|
||||||
drop_chance DOUBLE PRECISION NOT NULL DEFAULT 0.3, -- for collect_item
|
|
||||||
min_level INT NOT NULL DEFAULT 1,
|
|
||||||
max_level INT NOT NULL DEFAULT 100,
|
|
||||||
reward_xp BIGINT NOT NULL DEFAULT 0,
|
|
||||||
reward_gold BIGINT NOT NULL DEFAULT 0,
|
|
||||||
reward_potions INT NOT NULL DEFAULT 0,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_quests_npc ON quests(npc_id);
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- Hero quests: per-hero progress tracking.
|
|
||||||
-- ============================================================
|
|
||||||
CREATE TABLE IF NOT EXISTS hero_quests (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
hero_id BIGINT NOT NULL REFERENCES heroes(id) ON DELETE CASCADE,
|
|
||||||
quest_id BIGINT NOT NULL REFERENCES quests(id) ON DELETE CASCADE,
|
|
||||||
status TEXT NOT NULL DEFAULT 'accepted'
|
|
||||||
CHECK (status IN ('accepted', 'completed', 'claimed')),
|
|
||||||
progress INT NOT NULL DEFAULT 0,
|
|
||||||
accepted_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
||||||
completed_at TIMESTAMPTZ,
|
|
||||||
claimed_at TIMESTAMPTZ,
|
|
||||||
UNIQUE (hero_id, quest_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_hero_quests_hero ON hero_quests(hero_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_hero_quests_status ON hero_quests(hero_id, status);
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- Seed data: towns (idempotent — DB may already have these names)
|
|
||||||
-- ============================================================
|
|
||||||
INSERT INTO towns (name, biome, world_x, world_y, radius, level_min, level_max) VALUES
|
|
||||||
('Willowdale', 'meadow', 50, 15, 8.0, 1, 5),
|
|
||||||
('Thornwatch', 'forest', 200, 60, 8.0, 5, 10),
|
|
||||||
('Ashengard', 'ruins', 400, 120, 8.0, 10, 16),
|
|
||||||
('Redcliff', 'canyon', 650, 195, 8.0, 16, 22),
|
|
||||||
('Boghollow', 'swamp', 900, 270, 8.0, 22, 28),
|
|
||||||
('Cinderkeep', 'volcanic', 1200, 360, 8.0, 28, 34),
|
|
||||||
('Starfall', 'astral', 1550, 465, 8.0, 34, 40)
|
|
||||||
ON CONFLICT (name) DO NOTHING;
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- Seed data: NPCs (2-3 per town; resolve town_id by name)
|
|
||||||
-- ============================================================
|
|
||||||
INSERT INTO npcs (town_id, name, type, offset_x, offset_y)
|
|
||||||
SELECT t.id, v.npc_name, v.npc_type, v.ox, v.oy
|
|
||||||
FROM (VALUES
|
|
||||||
('Willowdale', 'Elder Maren', 'quest_giver', -2.0::double precision, 1.0::double precision),
|
|
||||||
('Willowdale', 'Peddler Finn', 'merchant', 3.0, 0.0),
|
|
||||||
('Willowdale', 'Sister Asha', 'healer', 0.0, -2.5),
|
|
||||||
('Thornwatch', 'Guard Halric', 'quest_giver', -3.0, 0.5),
|
|
||||||
('Thornwatch', 'Trader Wynn', 'merchant', 2.0, 2.0),
|
|
||||||
('Ashengard', 'Scholar Orin', 'quest_giver', 1.0, -2.0),
|
|
||||||
('Ashengard', 'Bone Merchant', 'merchant', -2.0, 3.0),
|
|
||||||
('Ashengard', 'Priestess Liora', 'healer', 3.0, 1.0),
|
|
||||||
('Redcliff', 'Foreman Brak', 'quest_giver', -1.0, 2.0),
|
|
||||||
('Redcliff', 'Miner Supplies', 'merchant', 2.5, -1.0),
|
|
||||||
('Boghollow', 'Witch Nessa', 'quest_giver', 0.0, 3.0),
|
|
||||||
('Boghollow', 'Swamp Trader', 'merchant', -3.0, -1.0),
|
|
||||||
('Boghollow', 'Marsh Healer Ren', 'healer', 2.0, 0.0),
|
|
||||||
('Cinderkeep', 'Forge-master Kael', 'quest_giver', -2.5, 0.0),
|
|
||||||
('Cinderkeep', 'Ember Merchant', 'merchant', 1.0, 2.5),
|
|
||||||
('Starfall', 'Seer Aelith', 'quest_giver', 0.0, -3.0),
|
|
||||||
('Starfall', 'Void Trader', 'merchant', 3.0, 1.0),
|
|
||||||
('Starfall', 'Astral Mender', 'healer', -2.0, 2.0)
|
|
||||||
) AS v(town_name, npc_name, npc_type, ox, oy)
|
|
||||||
JOIN towns t ON t.name = v.town_name
|
|
||||||
WHERE NOT EXISTS (
|
|
||||||
SELECT 1 FROM npcs n WHERE n.town_id = t.id AND n.name = v.npc_name
|
|
||||||
);
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- Seed data: quests (resolve npc_id / target_town_id by name; skip duplicates)
|
|
||||||
-- ============================================================
|
|
||||||
|
|
||||||
-- Willowdale — Elder Maren
|
|
||||||
INSERT INTO quests (npc_id, title, description, type, target_count, target_enemy_type, target_town_id, drop_chance, min_level, max_level, reward_xp, reward_gold, reward_potions)
|
|
||||||
SELECT n.id, s.title, s.description, s.qtype, s.target_count, s.target_enemy_type, s.target_town_id, s.drop_chance, s.min_level, s.max_level, s.reward_xp, s.reward_gold, s.reward_potions
|
|
||||||
FROM npcs n
|
|
||||||
JOIN towns t ON t.id = n.town_id
|
|
||||||
CROSS JOIN (VALUES
|
|
||||||
('Wolf Cull',
|
|
||||||
'The wolves near Willowdale are getting bolder. Thin their numbers.',
|
|
||||||
'kill_count', 5, 'wolf'::text, NULL::bigint, 0.0::double precision, 1, 5, 30::bigint, 15::bigint, 0),
|
|
||||||
('Boar Hunt',
|
|
||||||
'Wild boars are trampling the crops. Take care of them.',
|
|
||||||
'kill_count', 8, 'boar', NULL, 0.0, 2, 6, 50::bigint, 25::bigint, 1),
|
|
||||||
('Deliver to Thornwatch',
|
|
||||||
'Carry this supply manifest to Guard Halric in Thornwatch.',
|
|
||||||
'visit_town', 1, NULL, (SELECT id FROM towns WHERE name = 'Thornwatch' LIMIT 1), 0.0, 1, 10, 40::bigint, 20::bigint, 0)
|
|
||||||
) AS s(title, description, qtype, target_count, target_enemy_type, target_town_id, drop_chance, min_level, max_level, reward_xp, reward_gold, reward_potions)
|
|
||||||
WHERE t.name = 'Willowdale' AND n.name = 'Elder Maren'
|
|
||||||
AND NOT EXISTS (SELECT 1 FROM quests q WHERE q.npc_id = n.id AND q.title = s.title);
|
|
||||||
|
|
||||||
-- Thornwatch — Guard Halric
|
|
||||||
INSERT INTO quests (npc_id, title, description, type, target_count, target_enemy_type, target_town_id, drop_chance, min_level, max_level, reward_xp, reward_gold, reward_potions)
|
|
||||||
SELECT n.id, s.title, s.description, s.qtype, s.target_count, s.target_enemy_type, s.target_town_id, s.drop_chance, s.min_level, s.max_level, s.reward_xp, s.reward_gold, s.reward_potions
|
|
||||||
FROM npcs n
|
|
||||||
JOIN towns t ON t.id = n.town_id
|
|
||||||
CROSS JOIN (VALUES
|
|
||||||
('Spider Infestation',
|
|
||||||
'Cave spiders have overrun the logging trails. Clear them out.',
|
|
||||||
'kill_count', 12, 'spider'::text, NULL::bigint, 0.0::double precision, 5, 10, 80::bigint, 40::bigint, 1),
|
|
||||||
('Spider Fang Collection',
|
|
||||||
'We need spider fangs for antivenom. Collect them from slain spiders.',
|
|
||||||
'collect_item', 5, 'spider', NULL, 0.3, 5, 10, 100::bigint, 60::bigint, 1),
|
|
||||||
('Forest Patrol',
|
|
||||||
'Slay any 15 creatures along the forest road to keep it safe.',
|
|
||||||
'kill_count', 15, NULL::text, NULL::bigint, 0.0, 5, 12, 120::bigint, 70::bigint, 1)
|
|
||||||
) AS s(title, description, qtype, target_count, target_enemy_type, target_town_id, drop_chance, min_level, max_level, reward_xp, reward_gold, reward_potions)
|
|
||||||
WHERE t.name = 'Thornwatch' AND n.name = 'Guard Halric'
|
|
||||||
AND NOT EXISTS (SELECT 1 FROM quests q WHERE q.npc_id = n.id AND q.title = s.title);
|
|
||||||
|
|
||||||
-- Ashengard — Scholar Orin
|
|
||||||
INSERT INTO quests (npc_id, title, description, type, target_count, target_enemy_type, target_town_id, drop_chance, min_level, max_level, reward_xp, reward_gold, reward_potions)
|
|
||||||
SELECT n.id, s.title, s.description, s.qtype, s.target_count, s.target_enemy_type, s.target_town_id, s.drop_chance, s.min_level, s.max_level, s.reward_xp, s.reward_gold, s.reward_potions
|
|
||||||
FROM npcs n
|
|
||||||
JOIN towns t ON t.id = n.town_id
|
|
||||||
CROSS JOIN (VALUES
|
|
||||||
('Undead Purge',
|
|
||||||
'The ruins are crawling with undead. Destroy the zombies.',
|
|
||||||
'kill_count', 15, 'zombie'::text, NULL::bigint, 0.0::double precision, 10, 16, 150::bigint, 80::bigint, 1),
|
|
||||||
('Ancient Relics',
|
|
||||||
'Search fallen enemies for fragments of the old kingdom.',
|
|
||||||
'collect_item', 8, NULL::text, NULL::bigint, 0.25, 10, 16, 200::bigint, 120::bigint, 2),
|
|
||||||
('Report to Redcliff',
|
|
||||||
'Warn Foreman Brak about the growing undead threat.',
|
|
||||||
'visit_town', 1, NULL::text, (SELECT id FROM towns WHERE name = 'Redcliff' LIMIT 1), 0.0, 10, 20, 120::bigint, 60::bigint, 0)
|
|
||||||
) AS s(title, description, qtype, target_count, target_enemy_type, target_town_id, drop_chance, min_level, max_level, reward_xp, reward_gold, reward_potions)
|
|
||||||
WHERE t.name = 'Ashengard' AND n.name = 'Scholar Orin'
|
|
||||||
AND NOT EXISTS (SELECT 1 FROM quests q WHERE q.npc_id = n.id AND q.title = s.title);
|
|
||||||
|
|
||||||
-- Redcliff — Foreman Brak
|
|
||||||
INSERT INTO quests (npc_id, title, description, type, target_count, target_enemy_type, target_town_id, drop_chance, min_level, max_level, reward_xp, reward_gold, reward_potions)
|
|
||||||
SELECT n.id, s.title, s.description, s.qtype, s.target_count, s.target_enemy_type, s.target_town_id, s.drop_chance, s.min_level, s.max_level, s.reward_xp, s.reward_gold, s.reward_potions
|
|
||||||
FROM npcs n
|
|
||||||
JOIN towns t ON t.id = n.town_id
|
|
||||||
CROSS JOIN (VALUES
|
|
||||||
('Orc Raider Cleanup',
|
|
||||||
'Orc warriors are raiding the mine carts. Stop them.',
|
|
||||||
'kill_count', 20, 'orc'::text, NULL::bigint, 0.0::double precision, 16, 22, 250::bigint, 150::bigint, 2),
|
|
||||||
('Ore Samples',
|
|
||||||
'Collect glowing ore fragments from defeated enemies near the canyon.',
|
|
||||||
'collect_item', 6, NULL::text, NULL::bigint, 0.3, 16, 22, 200::bigint, 120::bigint, 1)
|
|
||||||
) AS s(title, description, qtype, target_count, target_enemy_type, target_town_id, drop_chance, min_level, max_level, reward_xp, reward_gold, reward_potions)
|
|
||||||
WHERE t.name = 'Redcliff' AND n.name = 'Foreman Brak'
|
|
||||||
AND NOT EXISTS (SELECT 1 FROM quests q WHERE q.npc_id = n.id AND q.title = s.title);
|
|
||||||
|
|
||||||
-- Boghollow — Witch Nessa
|
|
||||||
INSERT INTO quests (npc_id, title, description, type, target_count, target_enemy_type, target_town_id, drop_chance, min_level, max_level, reward_xp, reward_gold, reward_potions)
|
|
||||||
SELECT n.id, s.title, s.description, s.qtype, s.target_count, s.target_enemy_type, s.target_town_id, s.drop_chance, s.min_level, s.max_level, s.reward_xp, s.reward_gold, s.reward_potions
|
|
||||||
FROM npcs n
|
|
||||||
JOIN towns t ON t.id = n.town_id
|
|
||||||
CROSS JOIN (VALUES
|
|
||||||
('Swamp Creatures',
|
|
||||||
'The swamp beasts grow more aggressive by the day. Cull 25.',
|
|
||||||
'kill_count', 25, NULL::text, NULL::bigint, 0.0::double precision, 22, 28, 350::bigint, 200::bigint, 2),
|
|
||||||
('Venomous Harvest',
|
|
||||||
'Collect venom sacs from swamp creatures for my brews.',
|
|
||||||
'collect_item', 10, NULL::text, NULL::bigint, 0.25, 22, 28, 400::bigint, 250::bigint, 2),
|
|
||||||
('Message to Cinderkeep',
|
|
||||||
'The forgemaster needs to know about the corruption spreading here.',
|
|
||||||
'visit_town', 1, NULL::text, (SELECT id FROM towns WHERE name = 'Cinderkeep' LIMIT 1), 0.0, 22, 34, 200::bigint, 100::bigint, 1)
|
|
||||||
) AS s(title, description, qtype, target_count, target_enemy_type, target_town_id, drop_chance, min_level, max_level, reward_xp, reward_gold, reward_potions)
|
|
||||||
WHERE t.name = 'Boghollow' AND n.name = 'Witch Nessa'
|
|
||||||
AND NOT EXISTS (SELECT 1 FROM quests q WHERE q.npc_id = n.id AND q.title = s.title);
|
|
||||||
|
|
||||||
-- Cinderkeep — Forge-master Kael
|
|
||||||
INSERT INTO quests (npc_id, title, description, type, target_count, target_enemy_type, target_town_id, drop_chance, min_level, max_level, reward_xp, reward_gold, reward_potions)
|
|
||||||
SELECT n.id, s.title, s.description, s.qtype, s.target_count, s.target_enemy_type, s.target_town_id, s.drop_chance, s.min_level, s.max_level, s.reward_xp, s.reward_gold, s.reward_potions
|
|
||||||
FROM npcs n
|
|
||||||
JOIN towns t ON t.id = n.town_id
|
|
||||||
CROSS JOIN (VALUES
|
|
||||||
('Demon Slayer',
|
|
||||||
'Fire demons are emerging from the vents. Destroy them.',
|
|
||||||
'kill_count', 10, 'fire_demon'::text, NULL::bigint, 0.0::double precision, 28, 34, 500::bigint, 300::bigint, 2),
|
|
||||||
('Infernal Cores',
|
|
||||||
'Retrieve smoldering cores from defeated fire demons.',
|
|
||||||
'collect_item', 5, 'fire_demon'::text, NULL::bigint, 0.3::double precision, 28, 34, 600::bigint, 350::bigint, 3)
|
|
||||||
) AS s(title, description, qtype, target_count, target_enemy_type, target_town_id, drop_chance, min_level, max_level, reward_xp, reward_gold, reward_potions)
|
|
||||||
WHERE t.name = 'Cinderkeep' AND n.name = 'Forge-master Kael'
|
|
||||||
AND NOT EXISTS (SELECT 1 FROM quests q WHERE q.npc_id = n.id AND q.title = s.title);
|
|
||||||
|
|
||||||
-- Starfall — Seer Aelith
|
|
||||||
INSERT INTO quests (npc_id, title, description, type, target_count, target_enemy_type, target_town_id, drop_chance, min_level, max_level, reward_xp, reward_gold, reward_potions)
|
|
||||||
SELECT n.id, s.title, s.description, s.qtype, s.target_count, s.target_enemy_type, s.target_town_id, s.drop_chance, s.min_level, s.max_level, s.reward_xp, s.reward_gold, s.reward_potions
|
|
||||||
FROM npcs n
|
|
||||||
JOIN towns t ON t.id = n.town_id
|
|
||||||
CROSS JOIN (VALUES
|
|
||||||
('Titan''s Challenge',
|
|
||||||
'The Lightning Titans must be stopped before they breach the gate.',
|
|
||||||
'kill_count', 8, 'lightning_titan'::text, NULL::bigint, 0.0::double precision, 34, 40, 800::bigint, 500::bigint, 3),
|
|
||||||
('Void Fragments',
|
|
||||||
'Gather crystallized void energy from the astral enemies.',
|
|
||||||
'collect_item', 8, NULL::text, NULL::bigint, 0.2, 34, 40, 1000::bigint, 600::bigint, 3),
|
|
||||||
('Full Circle',
|
|
||||||
'Return to Willowdale and tell Elder Maren of your journey.',
|
|
||||||
'visit_town', 1, NULL::text, (SELECT id FROM towns WHERE name = 'Willowdale' LIMIT 1), 0.0, 34, 40, 500::bigint, 300::bigint, 2)
|
|
||||||
) AS s(title, description, qtype, target_count, target_enemy_type, target_town_id, drop_chance, min_level, max_level, reward_xp, reward_gold, reward_potions)
|
|
||||||
WHERE t.name = 'Starfall' AND n.name = 'Seer Aelith'
|
|
||||||
AND NOT EXISTS (SELECT 1 FROM quests q WHERE q.npc_id = n.id AND q.title = s.title);
|
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
-- 000006a: enemy archetypes column, quest targets, clear enemies before bulk insert.
|
||||||
|
-- Full chain: 000006a_head -> 000006b_enemy_data -> 000006c_tail (lexicographic order).
|
||||||
|
|
||||||
|
ALTER TABLE public.enemies ADD COLUMN IF NOT EXISTS archetype text;
|
||||||
|
|
||||||
|
ALTER TABLE public.enemies ADD COLUMN IF NOT EXISTS biome text;
|
||||||
|
|
||||||
|
UPDATE public.enemies SET archetype = type WHERE (archetype IS NULL OR archetype = '') AND type IS NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE public.quests ADD COLUMN IF NOT EXISTS target_enemy_archetype text;
|
||||||
|
|
||||||
|
UPDATE public.quests
|
||||||
|
SET target_enemy_archetype = target_enemy_type
|
||||||
|
WHERE target_enemy_type IS NOT NULL AND target_enemy_archetype IS NULL;
|
||||||
|
|
||||||
|
UPDATE public.quests SET target_enemy_type = NULL WHERE target_enemy_archetype IS NOT NULL;
|
||||||
|
|
||||||
|
DELETE FROM public.enemies;
|
||||||
@ -0,0 +1,220 @@
|
|||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (1, 'wolf_l1_1_meadow', 'wolf', 'meadow', 'Elder Verdant Wolf', 89, 89, 19, 1, 1.7460, 0.0500, 1, 1, 1, 1, ARRAY[]::text[], false, now(), 1, 0.3, 5, 7.8681, 2.7054, 1.2000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (2, 'wolf_l1_1_forest', 'wolf', 'forest', 'Woodland Elder Wolf', 98, 98, 21, 1, 1.7460, 0.0500, 1, 1, 2, 1, ARRAY[]::text[], false, now(), 1, 0.3, 5, 7.8681, 2.7054, 1.2000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (3, 'wolf_l2_2_forest', 'wolf', 'forest', 'Young Woodland Wolf', 92, 92, 19, 1, 1.7640, 0.0500, 2, 2, 4, 1, ARRAY[]::text[], false, now(), 2, 0.3, 5, 8.4975, 2.8677, 1.2600, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (4, 'wolf_l2_2_ruins', 'wolf', 'ruins', 'Forgotten Young Wolf', 101, 101, 21, 1, 1.7640, 0.0500, 2, 2, 5, 1, ARRAY[]::text[], false, now(), 2, 0.3, 5, 8.4975, 2.8677, 1.2600, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (5, 'wolf_l3_3_ruins', 'wolf', 'ruins', 'Lost Forgotten Wolf', 94, 94, 20, 1, 1.7820, 0.0500, 3, 3, 7, 1, ARRAY[]::text[], false, now(), 3, 0.3, 5, 9.1270, 3.0300, 1.3200, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (6, 'wolf_l3_3_canyon', 'wolf', 'canyon', 'Rift Lost Wolf', 104, 104, 22, 1, 1.7820, 0.0500, 3, 3, 8, 1, ARRAY[]::text[], false, now(), 3, 0.3, 5, 9.1270, 3.0300, 1.3200, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (7, 'wolf_l4_4_canyon', 'wolf', 'canyon', 'Cursed Rift Wolf', 97, 97, 20, 1, 1.8000, 0.0500, 4, 4, 10, 1, ARRAY[]::text[], false, now(), 4, 0.3, 5, 9.7564, 3.1924, 1.3800, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (8, 'wolf_l4_4_swamp', 'wolf', 'swamp', 'Bog Cursed Wolf', 107, 107, 22, 1, 1.8000, 0.0500, 4, 4, 11, 1, ARRAY[]::text[], false, now(), 4, 0.3, 5, 9.7564, 3.1924, 1.3800, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (9, 'wolf_l5_5_volcanic', 'wolf', 'volcanic', 'Rogue Ember Wolf', 100, 100, 21, 1, 1.8180, 0.0500, 5, 5, 13, 1, ARRAY[]::text[], false, now(), 5, 0.3, 5, 10.3859, 3.3547, 1.4400, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (10, 'wolf_l5_5_astral', 'wolf', 'astral', 'Astral Rogue Wolf', 110, 110, 23, 1, 1.8180, 0.0500, 5, 5, 14, 1, ARRAY[]::text[], false, now(), 5, 0.3, 5, 10.3859, 3.3547, 1.4400, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (11, 'boar_l2_2_meadow', 'boar', 'meadow', 'Elder Verdant Boar', 99, 99, 24, 1, 0.7760, 0.0800, 2, 2, 2, 1, ARRAY[]::text[], false, now(), 2, 0.3, 5, 8.2826, 2.3190, 1.6000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (12, 'boar_l2_2_forest', 'boar', 'forest', 'Woodland Elder Boar', 110, 110, 27, 2, 0.7760, 0.0800, 2, 2, 3, 1, ARRAY[]::text[], false, now(), 2, 0.3, 5, 8.2826, 2.3190, 1.6000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (13, 'boar_l3_3_forest', 'boar', 'forest', 'Young Woodland Boar', 102, 102, 25, 2, 0.7840, 0.0800, 3, 3, 5, 1, ARRAY[]::text[], false, now(), 3, 0.3, 5, 8.9452, 2.4581, 1.6800, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (14, 'boar_l3_3_ruins', 'boar', 'ruins', 'Forgotten Young Boar', 113, 113, 27, 2, 0.7840, 0.0800, 3, 3, 6, 1, ARRAY[]::text[], false, now(), 3, 0.3, 5, 8.9452, 2.4581, 1.6800, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (15, 'boar_l4_4_ruins', 'boar', 'ruins', 'Lost Forgotten Boar', 105, 105, 25, 2, 0.7920, 0.0800, 4, 4, 8, 1, ARRAY[]::text[], false, now(), 4, 0.3, 5, 9.6078, 2.5973, 1.7600, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (16, 'boar_l4_4_canyon', 'boar', 'canyon', 'Rift Lost Boar', 117, 117, 28, 2, 0.7920, 0.0800, 4, 4, 9, 1, ARRAY[]::text[], false, now(), 4, 0.3, 5, 9.6078, 2.5973, 1.7600, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (17, 'boar_l5_5_canyon', 'boar', 'canyon', 'Cursed Rift Boar', 108, 108, 26, 2, 0.8000, 0.0800, 5, 5, 11, 1, ARRAY[]::text[], false, now(), 5, 0.3, 5, 10.2704, 2.7364, 1.8400, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (18, 'boar_l5_5_swamp', 'boar', 'swamp', 'Bog Cursed Boar', 120, 120, 29, 2, 0.8000, 0.0800, 5, 5, 12, 1, ARRAY[]::text[], false, now(), 5, 0.3, 5, 10.2704, 2.7364, 1.8400, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (19, 'boar_l6_6_volcanic', 'boar', 'volcanic', 'Rogue Ember Boar', 111, 111, 27, 2, 0.8080, 0.0800, 6, 6, 14, 1, ARRAY[]::text[], false, now(), 6, 0.3, 5, 10.9330, 2.8756, 1.9200, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (20, 'boar_l6_6_astral', 'boar', 'astral', 'Astral Rogue Boar', 123, 123, 30, 2, 0.8080, 0.0800, 6, 6, 15, 1, ARRAY[]::text[], false, now(), 6, 0.3, 5, 10.9330, 2.8756, 1.9200, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (21, 'zombie_l3_4_meadow', 'zombie', 'meadow', 'Elder Verdant Zombie', 107, 107, 28, 2, 0.4850, 0.0000, 3, 4, 5, 1, ARRAY['poison']::text[], false, now(), 3, 0.3, 5, 6.9412, 2.5898, 1.8000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (22, 'zombie_l3_4_forest', 'zombie', 'forest', 'Woodland Elder Zombie', 119, 119, 31, 2, 0.4850, 0.0000, 3, 4, 6, 1, ARRAY['poison']::text[], false, now(), 3, 0.3, 5, 6.9412, 2.5898, 1.8000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (23, 'zombie_l5_5_forest', 'zombie', 'forest', 'Young Woodland Zombie', 114, 114, 29, 2, 0.4900, 0.0000, 5, 5, 8, 1, ARRAY['poison']::text[], false, now(), 5, 0.3, 5, 7.4965, 2.7452, 1.8900, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (24, 'zombie_l5_5_ruins', 'zombie', 'ruins', 'Forgotten Young Zombie', 126, 126, 33, 2, 0.4900, 0.0000, 5, 5, 9, 1, ARRAY['poison']::text[], false, now(), 5, 0.3, 5, 7.4965, 2.7452, 1.8900, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (25, 'zombie_l6_6_ruins', 'zombie', 'ruins', 'Lost Forgotten Zombie', 117, 117, 30, 2, 0.4950, 0.0000, 6, 6, 11, 1, ARRAY['poison']::text[], false, now(), 6, 0.3, 5, 8.0518, 2.9006, 1.9800, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (26, 'zombie_l6_6_canyon', 'zombie', 'canyon', 'Rift Lost Zombie', 129, 129, 33, 2, 0.4950, 0.0000, 6, 6, 12, 1, ARRAY['poison']::text[], false, now(), 6, 0.3, 5, 8.0518, 2.9006, 1.9800, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (27, 'zombie_l7_7_canyon', 'zombie', 'canyon', 'Cursed Rift Zombie', 120, 120, 31, 2, 0.5000, 0.0000, 7, 7, 14, 1, ARRAY['poison']::text[], false, now(), 7, 0.3, 5, 8.6071, 3.0560, 2.0700, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (28, 'zombie_l7_7_swamp', 'zombie', 'swamp', 'Bog Cursed Zombie', 133, 133, 34, 2, 0.5000, 0.0000, 7, 7, 15, 1, ARRAY['poison']::text[], false, now(), 7, 0.3, 5, 8.6071, 3.0560, 2.0700, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (29, 'zombie_l8_8_volcanic', 'zombie', 'volcanic', 'Rogue Ember Zombie', 123, 123, 32, 2, 0.5050, 0.0000, 8, 8, 17, 1, ARRAY['poison']::text[], false, now(), 8, 0.3, 5, 9.1624, 3.2114, 2.1600, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (30, 'zombie_l8_8_astral', 'zombie', 'astral', 'Astral Rogue Zombie', 136, 136, 35, 2, 0.5050, 0.0000, 8, 8, 18, 1, ARRAY['poison']::text[], false, now(), 8, 0.3, 5, 9.1624, 3.2114, 2.1600, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (31, 'spider_l4_5_meadow', 'spider', 'meadow', 'Elder Verdant Spider', 122, 122, 24, 1, 1.9400, 0.1500, 4, 5, 7, 1, ARRAY['critical']::text[], false, now(), 4, 0.3, 5, 12.0614, 2.7373, 1.0000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (32, 'spider_l4_5_forest', 'spider', 'forest', 'Woodland Elder Spider', 135, 135, 27, 1, 1.9400, 0.1500, 4, 5, 8, 1, ARRAY['critical']::text[], false, now(), 4, 0.3, 5, 12.0614, 2.7373, 1.0000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (33, 'spider_l6_6_forest', 'spider', 'forest', 'Young Woodland Spider', 129, 129, 26, 1, 1.9600, 0.1500, 6, 6, 10, 1, ARRAY['critical']::text[], false, now(), 6, 0.3, 5, 13.0263, 2.9015, 1.0500, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (34, 'spider_l6_6_ruins', 'spider', 'ruins', 'Forgotten Young Spider', 143, 143, 29, 1, 1.9600, 0.1500, 6, 6, 11, 1, ARRAY['critical']::text[], false, now(), 6, 0.3, 5, 13.0263, 2.9015, 1.0500, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (35, 'spider_l7_7_ruins', 'spider', 'ruins', 'Lost Forgotten Spider', 132, 132, 27, 1, 1.9800, 0.1500, 7, 7, 13, 1, ARRAY['critical']::text[], false, now(), 7, 0.3, 5, 13.9912, 3.0658, 1.1000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (36, 'spider_l7_7_canyon', 'spider', 'canyon', 'Rift Lost Spider', 146, 146, 29, 1, 1.9800, 0.1500, 7, 7, 14, 1, ARRAY['critical']::text[], false, now(), 7, 0.3, 5, 13.9912, 3.0658, 1.1000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (37, 'spider_l8_8_canyon', 'spider', 'canyon', 'Cursed Rift Spider', 136, 136, 27, 1, 2.0000, 0.1500, 8, 8, 16, 1, ARRAY['critical']::text[], false, now(), 8, 0.3, 5, 14.9561, 3.2300, 1.1500, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (38, 'spider_l8_8_swamp', 'spider', 'swamp', 'Bog Cursed Spider', 150, 150, 30, 1, 2.0000, 0.1500, 8, 8, 17, 1, ARRAY['critical']::text[], false, now(), 8, 0.3, 5, 14.9561, 3.2300, 1.1500, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (39, 'spider_l9_9_volcanic', 'spider', 'volcanic', 'Rogue Ember Spider', 139, 139, 28, 1, 2.0200, 0.1500, 9, 9, 19, 1, ARRAY['critical']::text[], false, now(), 9, 0.3, 5, 15.9210, 3.3943, 1.2000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (40, 'spider_l9_9_astral', 'spider', 'astral', 'Astral Rogue Spider', 154, 154, 31, 1, 2.0200, 0.1500, 9, 9, 20, 1, ARRAY['critical']::text[], false, now(), 9, 0.3, 5, 15.9210, 3.3943, 1.2000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (41, 'orc_l5_6_meadow', 'orc', 'meadow', 'Elder Verdant Orc', 120, 120, 28, 3, 0.9700, 0.0500, 5, 6, 8, 1, ARRAY['burst']::text[], false, now(), 5, 0.3, 5, 7.1338, 2.6581, 2.0000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (42, 'orc_l5_6_forest', 'orc', 'forest', 'Woodland Elder Orc', 133, 133, 31, 3, 0.9700, 0.0500, 5, 6, 9, 1, ARRAY['burst']::text[], false, now(), 5, 0.3, 5, 7.1338, 2.6581, 2.0000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (43, 'orc_l7_8_forest', 'orc', 'forest', 'Young Woodland Orc', 127, 127, 30, 3, 0.9800, 0.0500, 7, 8, 11, 1, ARRAY['burst']::text[], false, now(), 7, 0.3, 5, 7.7045, 2.8176, 2.1000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (44, 'orc_l7_8_ruins', 'orc', 'ruins', 'Forgotten Young Orc', 140, 140, 33, 3, 0.9800, 0.0500, 7, 8, 12, 1, ARRAY['burst']::text[], false, now(), 7, 0.3, 5, 7.7045, 2.8176, 2.1000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (45, 'orc_l9_10_ruins', 'orc', 'ruins', 'Lost Forgotten Orc', 133, 133, 32, 3, 0.9900, 0.0500, 9, 10, 14, 1, ARRAY['burst']::text[], false, now(), 9, 0.3, 5, 8.2752, 2.9771, 2.2000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (46, 'orc_l9_10_canyon', 'orc', 'canyon', 'Rift Lost Orc', 148, 148, 35, 3, 0.9900, 0.0500, 9, 10, 15, 1, ARRAY['burst']::text[], false, now(), 9, 0.3, 5, 8.2752, 2.9771, 2.2000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (47, 'orc_l11_11_canyon', 'orc', 'canyon', 'Cursed Rift Orc', 140, 140, 33, 3, 1.0000, 0.0500, 11, 11, 17, 1, ARRAY['burst']::text[], false, now(), 11, 0.3, 5, 8.8459, 3.1366, 2.3000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (48, 'orc_l11_11_swamp', 'orc', 'swamp', 'Bog Cursed Orc', 155, 155, 37, 4, 1.0000, 0.0500, 11, 11, 18, 1, ARRAY['burst']::text[], false, now(), 11, 0.3, 5, 8.8459, 3.1366, 2.3000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (49, 'orc_l12_12_volcanic', 'orc', 'volcanic', 'Rogue Ember Orc', 143, 143, 34, 3, 1.0100, 0.0500, 12, 12, 20, 1, ARRAY['burst']::text[], false, now(), 12, 0.3, 5, 9.4166, 3.2960, 2.4000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (50, 'orc_l12_12_astral', 'orc', 'astral', 'Astral Rogue Orc', 159, 159, 38, 4, 1.0100, 0.0500, 12, 12, 21, 1, ARRAY['burst']::text[], false, now(), 12, 0.3, 5, 9.4166, 3.2960, 2.4000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (51, 'skeleton_l6_7_meadow', 'skeleton', 'meadow', 'Elder Verdant Skeleton', 144, 144, 30, 2, 1.2610, 0.0600, 6, 7, 9, 1, ARRAY['dodge']::text[], false, now(), 6, 0.3, 5, 8.5586, 2.2939, 1.7000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (52, 'skeleton_l6_7_forest', 'skeleton', 'forest', 'Woodland Elder Skeleton', 160, 160, 33, 2, 1.2610, 0.0600, 6, 7, 10, 1, ARRAY['dodge']::text[], false, now(), 6, 0.3, 5, 8.5586, 2.2939, 1.7000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (53, 'skeleton_l8_9_forest', 'skeleton', 'forest', 'Young Woodland Skeleton', 152, 152, 32, 2, 1.2740, 0.0600, 8, 9, 12, 1, ARRAY['dodge']::text[], false, now(), 8, 0.3, 5, 9.2433, 2.4315, 1.7850, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (54, 'skeleton_l8_9_ruins', 'skeleton', 'ruins', 'Forgotten Young Skeleton', 168, 168, 35, 2, 1.2740, 0.0600, 8, 9, 13, 1, ARRAY['dodge']::text[], false, now(), 8, 0.3, 5, 9.2433, 2.4315, 1.7850, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (55, 'skeleton_l10_11_ruins', 'skeleton', 'ruins', 'Lost Forgotten Skeleton', 160, 160, 34, 2, 1.2870, 0.0600, 10, 11, 15, 1, ARRAY['dodge']::text[], false, now(), 10, 0.3, 5, 9.9280, 2.5692, 1.8700, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (56, 'skeleton_l10_11_canyon', 'skeleton', 'canyon', 'Rift Lost Skeleton', 177, 177, 37, 2, 1.2870, 0.0600, 10, 11, 16, 1, ARRAY['dodge']::text[], false, now(), 10, 0.3, 5, 9.9280, 2.5692, 1.8700, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (57, 'skeleton_l12_13_canyon', 'skeleton', 'canyon', 'Cursed Rift Skeleton', 168, 168, 35, 2, 1.3000, 0.0600, 12, 13, 18, 1, ARRAY['dodge']::text[], false, now(), 12, 0.3, 5, 10.6127, 2.7068, 1.9550, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (58, 'skeleton_l12_13_swamp', 'skeleton', 'swamp', 'Bog Cursed Skeleton', 185, 185, 39, 2, 1.3000, 0.0600, 12, 13, 19, 1, ARRAY['dodge']::text[], false, now(), 12, 0.3, 5, 10.6127, 2.7068, 1.9550, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (59, 'skeleton_l14_14_volcanic', 'skeleton', 'volcanic', 'Rogue Ember Skeleton', 175, 175, 37, 2, 1.3130, 0.0600, 14, 14, 21, 1, ARRAY['dodge']::text[], false, now(), 14, 0.3, 5, 11.2974, 2.8444, 2.0400, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (60, 'skeleton_l14_14_astral', 'skeleton', 'astral', 'Astral Rogue Skeleton', 194, 194, 41, 2, 1.3130, 0.0600, 14, 14, 22, 1, ARRAY['dodge']::text[], false, now(), 14, 0.3, 5, 11.2974, 2.8444, 2.0400, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (61, 'battle_lizard_l7_8_meadow', 'battle_lizard', 'meadow', 'Elder Verdant Scaleback', 118, 118, 36, 4, 0.6790, 0.0300, 7, 8, 10, 1, ARRAY['regen']::text[], false, now(), 7, 0.3, 5, 5.7476, 2.4140, 2.3000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (62, 'battle_lizard_l7_8_forest', 'battle_lizard', 'forest', 'Woodland Elder Scaleback', 130, 130, 39, 4, 0.6790, 0.0300, 7, 8, 11, 1, ARRAY['regen']::text[], false, now(), 7, 0.3, 5, 5.7476, 2.4140, 2.3000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (63, 'battle_lizard_l9_10_forest', 'battle_lizard', 'forest', 'Young Woodland Scaleback', 124, 124, 37, 4, 0.6860, 0.0300, 9, 10, 13, 1, ARRAY['regen']::text[], false, now(), 9, 0.3, 5, 6.2074, 2.5588, 2.4150, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (64, 'battle_lizard_l9_10_ruins', 'battle_lizard', 'ruins', 'Forgotten Young Scaleback', 137, 137, 41, 5, 0.6860, 0.0300, 9, 10, 14, 1, ARRAY['regen']::text[], false, now(), 9, 0.3, 5, 6.2074, 2.5588, 2.4150, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (65, 'battle_lizard_l11_12_ruins', 'battle_lizard', 'ruins', 'Lost Forgotten Scaleback', 130, 130, 39, 4, 0.6930, 0.0300, 11, 12, 16, 1, ARRAY['regen']::text[], false, now(), 11, 0.3, 5, 6.6672, 2.7037, 2.5300, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (66, 'battle_lizard_l11_12_canyon', 'battle_lizard', 'canyon', 'Rift Lost Scaleback', 144, 144, 44, 5, 0.6930, 0.0300, 11, 12, 17, 1, ARRAY['regen']::text[], false, now(), 11, 0.3, 5, 6.6672, 2.7037, 2.5300, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (67, 'battle_lizard_l13_14_canyon', 'battle_lizard', 'canyon', 'Cursed Rift Scaleback', 136, 136, 41, 5, 0.7000, 0.0300, 13, 14, 19, 1, ARRAY['regen']::text[], false, now(), 13, 0.3, 5, 7.1270, 2.8485, 2.6450, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (68, 'battle_lizard_l13_14_swamp', 'battle_lizard', 'swamp', 'Bog Cursed Scaleback', 151, 151, 46, 5, 0.7000, 0.0300, 13, 14, 20, 1, ARRAY['regen']::text[], false, now(), 13, 0.3, 5, 7.1270, 2.8485, 2.6450, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (69, 'battle_lizard_l15_15_volcanic', 'battle_lizard', 'volcanic', 'Rogue Ember Scaleback', 143, 143, 43, 5, 0.7070, 0.0300, 15, 15, 22, 1, ARRAY['regen']::text[], false, now(), 15, 0.3, 5, 7.5868, 2.9934, 2.7600, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (70, 'battle_lizard_l15_15_astral', 'battle_lizard', 'astral', 'Astral Rogue Scaleback', 158, 158, 48, 6, 0.7070, 0.0300, 15, 15, 23, 1, ARRAY['regen']::text[], false, now(), 15, 0.3, 5, 7.5868, 2.9934, 2.7600, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (71, 'element_l18_20_meadow', 'element', 'meadow', 'Elder Verdant Elemental', 516, 516, 66, 7, 0.7760, 0.0500, 18, 20, 36, 1, ARRAY['slow']::text[], true, now(), 19, 0.3, 5, 8.0285, 3.1288, 2.2000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (72, 'element_l12_14_forest', 'element', 'forest', 'Woodland Elder Elemental', 299, 299, 53, 8, 0.6790, 0.0400, 12, 14, 19, 1, ARRAY['ice_slow']::text[], true, now(), 13, 0.3, 5, 7.5649, 3.0394, 2.5000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (73, 'element_l21_22_forest', 'element', 'forest', 'Young Woodland Elemental', 537, 537, 69, 7, 0.7840, 0.0500, 21, 22, 39, 1, ARRAY['slow']::text[], true, now(), 21, 0.3, 5, 8.6708, 3.3165, 2.3100, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (74, 'element_l15_16_ruins', 'element', 'ruins', 'Forgotten Young Elemental', 313, 313, 55, 9, 0.6860, 0.0400, 15, 16, 22, 1, ARRAY['ice_slow']::text[], true, now(), 15, 0.3, 5, 8.1701, 3.2218, 2.6250, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (75, 'element_l23_24_ruins', 'element', 'ruins', 'Lost Forgotten Elemental', 557, 557, 71, 7, 0.7920, 0.0500, 23, 24, 42, 1, ARRAY['slow']::text[], true, now(), 23, 0.3, 5, 9.3131, 3.5043, 2.4200, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (76, 'element_l17_18_canyon', 'element', 'canyon', 'Rift Lost Elemental', 326, 326, 58, 9, 0.6930, 0.0400, 17, 18, 25, 1, ARRAY['ice_slow']::text[], true, now(), 17, 0.3, 5, 8.7753, 3.4041, 2.7500, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (77, 'element_l25_26_canyon', 'element', 'canyon', 'Cursed Rift Elemental', 578, 578, 74, 8, 0.8000, 0.0500, 25, 26, 45, 2, ARRAY['slow']::text[], true, now(), 25, 0.3, 5, 9.9553, 3.6920, 2.5300, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (78, 'element_l19_20_swamp', 'element', 'swamp', 'Bog Cursed Elemental', 340, 340, 60, 9, 0.7000, 0.0400, 19, 20, 28, 2, ARRAY['ice_slow']::text[], true, now(), 19, 0.3, 5, 9.3805, 3.5865, 2.8750, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (79, 'element_l27_28_volcanic', 'element', 'volcanic', 'Rogue Ember Elemental', 598, 598, 77, 8, 0.8080, 0.0500, 27, 28, 48, 3, ARRAY['slow']::text[], true, now(), 27, 0.3, 5, 10.5976, 3.8797, 2.6400, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (80, 'element_l21_22_astral', 'element', 'astral', 'Astral Rogue Elemental', 353, 353, 62, 10, 0.7070, 0.0400, 21, 22, 31, 3, ARRAY['ice_slow']::text[], true, now(), 21, 0.3, 5, 9.9857, 3.7689, 3.0000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (81, 'demon_l10_12_meadow', 'demon', 'meadow', 'Elder Verdant Demon', 220, 220, 31, 3, 1.1640, 0.1000, 10, 12, 11, 1, ARRAY['burn']::text[], true, now(), 11, 0.3, 5, 11.7200, 2.6587, 2.0000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (82, 'demon_l10_12_forest', 'demon', 'forest', 'Woodland Elder Demon', 243, 243, 34, 4, 1.1640, 0.1000, 10, 12, 12, 1, ARRAY['burn']::text[], true, now(), 11, 0.3, 5, 11.7200, 2.6587, 2.0000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (83, 'demon_l13_14_forest', 'demon', 'forest', 'Young Woodland Demon', 230, 230, 32, 3, 1.1760, 0.1000, 13, 14, 14, 1, ARRAY['burn']::text[], true, now(), 13, 0.3, 5, 12.6576, 2.8182, 2.1000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (84, 'demon_l13_14_ruins', 'demon', 'ruins', 'Forgotten Young Demon', 254, 254, 36, 4, 1.1760, 0.1000, 13, 14, 15, 1, ARRAY['burn']::text[], true, now(), 13, 0.3, 5, 12.6576, 2.8182, 2.1000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (85, 'demon_l15_16_ruins', 'demon', 'ruins', 'Lost Forgotten Demon', 241, 241, 34, 4, 1.1880, 0.1000, 15, 16, 17, 1, ARRAY['burn']::text[], true, now(), 15, 0.3, 5, 13.5952, 2.9777, 2.2000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (86, 'demon_l15_16_canyon', 'demon', 'canyon', 'Rift Lost Demon', 266, 266, 37, 4, 1.1880, 0.1000, 15, 16, 18, 1, ARRAY['burn']::text[], true, now(), 15, 0.3, 5, 13.5952, 2.9777, 2.2000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (87, 'demon_l17_18_canyon', 'demon', 'canyon', 'Cursed Rift Demon', 251, 251, 35, 4, 1.2000, 0.1000, 17, 18, 20, 2, ARRAY['burn']::text[], true, now(), 17, 0.3, 5, 14.5328, 3.1373, 2.3000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (88, 'demon_l17_18_swamp', 'demon', 'swamp', 'Bog Cursed Demon', 278, 278, 39, 4, 1.2000, 0.1000, 17, 18, 21, 2, ARRAY['burn']::text[], true, now(), 17, 0.3, 5, 14.5328, 3.1373, 2.3000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (89, 'demon_l19_20_volcanic', 'demon', 'volcanic', 'Rogue Ember Demon', 261, 261, 37, 4, 1.2120, 0.1000, 19, 20, 23, 3, ARRAY['burn']::text[], true, now(), 19, 0.3, 5, 15.4704, 3.2968, 2.4000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (90, 'demon_l19_20_astral', 'demon', 'astral', 'Astral Rogue Demon', 289, 289, 40, 4, 1.2120, 0.1000, 19, 20, 24, 3, ARRAY['burn']::text[], true, now(), 19, 0.3, 5, 15.4704, 3.2968, 2.4000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (91, 'skeleton_king_l15_17_meadow', 'skeleton_king', 'meadow', 'Elder Verdant Bone Sovereign', 207, 207, 33, 30, 0.8730, 0.0800, 15, 17, 35, 1, ARRAY['regen']::text[], true, now(), 16, 0.3, 5, 4.1663, 1.8339, 2.0000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (92, 'skeleton_king_l15_17_forest', 'skeleton_king', 'forest', 'Woodland Elder Bone Sovereign', 229, 229, 36, 33, 0.8730, 0.0800, 15, 17, 36, 1, ARRAY['regen','summon']::text[], true, now(), 16, 0.3, 5, 4.1663, 1.8339, 2.0000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (93, 'skeleton_king_l18_19_forest', 'skeleton_king', 'forest', 'Young Woodland Bone Sovereign', 216, 216, 34, 31, 0.8820, 0.0800, 18, 19, 38, 1, ARRAY['regen','summon']::text[], true, now(), 18, 0.3, 5, 4.4996, 1.9439, 2.1000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (94, 'skeleton_king_l18_19_ruins', 'skeleton_king', 'ruins', 'Forgotten Young Bone Sovereign', 238, 238, 38, 35, 0.8820, 0.0800, 18, 19, 39, 1, ARRAY['regen','summon']::text[], true, now(), 18, 0.3, 5, 4.4996, 1.9439, 2.1000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (95, 'skeleton_king_l20_21_ruins', 'skeleton_king', 'ruins', 'Lost Forgotten Bone Sovereign', 224, 224, 36, 33, 0.8910, 0.0800, 20, 21, 41, 1, ARRAY['regen']::text[], true, now(), 20, 0.3, 5, 4.8329, 2.0540, 2.2000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (96, 'skeleton_king_l20_21_canyon', 'skeleton_king', 'canyon', 'Rift Lost Bone Sovereign', 248, 248, 40, 36, 0.8910, 0.0800, 20, 21, 42, 1, ARRAY['regen','summon']::text[], true, now(), 20, 0.3, 5, 4.8329, 2.0540, 2.2000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (97, 'skeleton_king_l22_23_canyon', 'skeleton_king', 'canyon', 'Cursed Rift Bone Sovereign', 233, 233, 37, 34, 0.9000, 0.0800, 22, 23, 44, 2, ARRAY['regen','summon']::text[], true, now(), 22, 0.3, 5, 5.1662, 2.1640, 2.3000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (98, 'skeleton_king_l22_23_swamp', 'skeleton_king', 'swamp', 'Bog Cursed Bone Sovereign', 258, 258, 41, 38, 0.9000, 0.0800, 22, 23, 45, 2, ARRAY['regen','summon']::text[], true, now(), 22, 0.3, 5, 5.1662, 2.1640, 2.3000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (99, 'skeleton_king_l24_25_volcanic', 'skeleton_king', 'volcanic', 'Rogue Ember Bone Sovereign', 242, 242, 39, 35, 0.9090, 0.0800, 24, 25, 47, 3, ARRAY['regen']::text[], true, now(), 24, 0.3, 5, 5.4995, 2.2740, 2.4000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (100, 'skeleton_king_l24_25_astral', 'skeleton_king', 'astral', 'Astral Rogue Bone Sovereign', 267, 267, 43, 39, 0.9090, 0.0800, 24, 25, 48, 3, ARRAY['regen','summon']::text[], true, now(), 24, 0.3, 5, 5.4995, 2.2740, 2.4000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (101, 'forest_warden_l20_22_meadow', 'forest_warden', 'meadow', 'Elder Verdant Warden', 520, 520, 76, 12, 0.4850, 0.0300, 20, 22, 37, 1, ARRAY['regen']::text[], true, now(), 21, 0.3, 5, 6.1288, 3.5033, 2.8000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (102, 'forest_warden_l20_22_forest', 'forest_warden', 'forest', 'Woodland Elder Warden', 574, 574, 85, 13, 0.4850, 0.0300, 20, 22, 38, 1, ARRAY['regen']::text[], true, now(), 21, 0.3, 5, 6.1288, 3.5033, 2.8000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (103, 'forest_warden_l23_24_forest', 'forest_warden', 'forest', 'Young Woodland Warden', 540, 540, 79, 12, 0.4900, 0.0300, 23, 24, 40, 1, ARRAY['regen']::text[], true, now(), 23, 0.3, 5, 6.6191, 3.7135, 2.9400, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (104, 'forest_warden_l23_24_ruins', 'forest_warden', 'ruins', 'Forgotten Young Warden', 596, 596, 88, 14, 0.4900, 0.0300, 23, 24, 41, 1, ARRAY['regen']::text[], true, now(), 23, 0.3, 5, 6.6191, 3.7135, 2.9400, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (105, 'forest_warden_l25_26_ruins', 'forest_warden', 'ruins', 'Lost Forgotten Warden', 559, 559, 82, 13, 0.4950, 0.0300, 25, 26, 43, 1, ARRAY['regen']::text[], true, now(), 25, 0.3, 5, 7.1094, 3.9237, 3.0800, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (106, 'forest_warden_l25_26_canyon', 'forest_warden', 'canyon', 'Rift Lost Warden', 618, 618, 91, 14, 0.4950, 0.0300, 25, 26, 44, 1, ARRAY['regen']::text[], true, now(), 25, 0.3, 5, 7.1094, 3.9237, 3.0800, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (107, 'forest_warden_l27_28_canyon', 'forest_warden', 'canyon', 'Cursed Rift Warden', 579, 579, 85, 13, 0.5000, 0.0300, 27, 28, 46, 2, ARRAY['regen']::text[], true, now(), 27, 0.3, 5, 7.5997, 4.1339, 3.2200, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (108, 'forest_warden_l27_28_swamp', 'forest_warden', 'swamp', 'Bog Cursed Warden', 640, 640, 94, 15, 0.5000, 0.0300, 27, 28, 47, 2, ARRAY['regen']::text[], true, now(), 27, 0.3, 5, 7.5997, 4.1339, 3.2200, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (109, 'forest_warden_l29_30_volcanic', 'forest_warden', 'volcanic', 'Rogue Ember Warden', 599, 599, 88, 14, 0.5050, 0.0300, 29, 30, 49, 3, ARRAY['regen']::text[], true, now(), 29, 0.3, 5, 8.0900, 4.3441, 3.3600, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (110, 'forest_warden_l29_30_astral', 'forest_warden', 'astral', 'Astral Rogue Warden', 662, 662, 98, 15, 0.5050, 0.0300, 29, 30, 50, 3, ARRAY['regen']::text[], true, now(), 29, 0.3, 5, 8.0900, 4.3441, 3.3600, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (111, 'titan_l25_27_meadow', 'titan', 'meadow', 'Elder Verdant Titan', 983, 983, 80, 10, 1.4550, 0.1200, 25, 27, 38, 10, ARRAY['stun']::text[], true, now(), 26, 0.3, 5, 11.1055, 2.9104, 2.3000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (112, 'titan_l25_27_forest', 'titan', 'forest', 'Woodland Elder Titan', 1086, 1086, 89, 11, 1.4550, 0.1200, 25, 27, 39, 10, ARRAY['stun','chain_lightning']::text[], true, now(), 26, 0.3, 5, 11.1055, 2.9104, 2.3000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (113, 'titan_l28_29_forest', 'titan', 'forest', 'Young Woodland Titan', 1017, 1017, 83, 10, 1.4700, 0.1200, 28, 29, 41, 10, ARRAY['stun','chain_lightning']::text[], true, now(), 28, 0.3, 5, 11.9939, 3.0850, 2.4150, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (114, 'titan_l28_29_ruins', 'titan', 'ruins', 'Forgotten Young Titan', 1124, 1124, 92, 11, 1.4700, 0.1200, 28, 29, 42, 10, ARRAY['stun','chain_lightning']::text[], true, now(), 28, 0.3, 5, 11.9939, 3.0850, 2.4150, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (115, 'titan_l30_31_ruins', 'titan', 'ruins', 'Lost Forgotten Titan', 1051, 1051, 86, 10, 1.4850, 0.1200, 30, 31, 44, 10, ARRAY['stun']::text[], true, now(), 30, 0.3, 5, 12.8824, 3.2596, 2.5300, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (116, 'titan_l30_31_canyon', 'titan', 'canyon', 'Rift Lost Titan', 1162, 1162, 95, 11, 1.4850, 0.1200, 30, 31, 45, 10, ARRAY['stun','chain_lightning']::text[], true, now(), 30, 0.3, 5, 12.8824, 3.2596, 2.5300, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (117, 'titan_l32_33_canyon', 'titan', 'canyon', 'Cursed Rift Titan', 1086, 1086, 89, 11, 1.5000, 0.1200, 32, 33, 47, 11, ARRAY['stun','chain_lightning']::text[], true, now(), 32, 0.3, 5, 13.7708, 3.4343, 2.6450, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (118, 'titan_l32_33_swamp', 'titan', 'swamp', 'Bog Cursed Titan', 1200, 1200, 98, 12, 1.5000, 0.1200, 32, 33, 48, 11, ARRAY['stun','chain_lightning']::text[], true, now(), 32, 0.3, 5, 13.7708, 3.4343, 2.6450, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (119, 'titan_l34_35_volcanic', 'titan', 'volcanic', 'Rogue Ember Titan', 1120, 1120, 92, 11, 1.5150, 0.1200, 34, 35, 50, 12, ARRAY['stun']::text[], true, now(), 34, 0.3, 5, 14.6593, 3.6089, 2.7600, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (120, 'titan_l34_35_astral', 'titan', 'astral', 'Astral Rogue Titan', 1238, 1238, 101, 12, 1.5150, 0.1200, 34, 35, 51, 12, ARRAY['stun','chain_lightning']::text[], true, now(), 34, 0.3, 5, 14.6593, 3.6089, 2.7600, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (121, 'golem_l8_10_meadow', 'golem', 'meadow', 'Elder Verdant Golem', 260, 260, 41, 11, 0.5335, 0.0200, 8, 10, 15, 2, ARRAY['stun']::text[], false, now(), 9, 0.3, 5, 8.0000, 2.8000, 3.0000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (122, 'golem_l8_10_forest', 'golem', 'forest', 'Woodland Elder Golem', 288, 288, 45, 13, 0.5335, 0.0200, 8, 10, 16, 2, ARRAY['stun']::text[], false, now(), 9, 0.3, 5, 8.0000, 2.8000, 3.0000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (123, 'golem_l11_12_forest', 'golem', 'forest', 'Young Woodland Golem', 273, 273, 43, 12, 0.5390, 0.0200, 11, 12, 18, 2, ARRAY['stun']::text[], false, now(), 11, 0.3, 5, 8.6400, 2.9680, 3.1500, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (124, 'golem_l11_12_ruins', 'golem', 'ruins', 'Forgotten Young Golem', 302, 302, 48, 13, 0.5390, 0.0200, 11, 12, 19, 2, ARRAY['stun']::text[], false, now(), 11, 0.3, 5, 8.6400, 2.9680, 3.1500, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (125, 'golem_l13_14_ruins', 'golem', 'ruins', 'Lost Forgotten Golem', 286, 286, 45, 13, 0.5445, 0.0200, 13, 14, 21, 2, ARRAY['stun']::text[], false, now(), 13, 0.3, 5, 9.2800, 3.1360, 3.3000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (126, 'golem_l13_14_canyon', 'golem', 'canyon', 'Rift Lost Golem', 316, 316, 50, 14, 0.5445, 0.0200, 13, 14, 22, 2, ARRAY['stun']::text[], false, now(), 13, 0.3, 5, 9.2800, 3.1360, 3.3000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (127, 'golem_l15_16_canyon', 'golem', 'canyon', 'Cursed Rift Golem', 299, 299, 47, 13, 0.5500, 0.0200, 15, 16, 24, 2, ARRAY['stun']::text[], false, now(), 15, 0.3, 5, 9.9200, 3.3040, 3.4500, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (128, 'golem_l15_16_swamp', 'golem', 'swamp', 'Bog Cursed Golem', 331, 331, 52, 15, 0.5500, 0.0200, 15, 16, 25, 2, ARRAY['stun']::text[], false, now(), 15, 0.3, 5, 9.9200, 3.3040, 3.4500, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (129, 'golem_l17_18_volcanic', 'golem', 'volcanic', 'Rogue Ember Golem', 312, 312, 49, 14, 0.5555, 0.0200, 17, 18, 27, 2, ARRAY['stun']::text[], false, now(), 17, 0.3, 5, 10.5600, 3.4720, 3.6000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (130, 'golem_l17_18_astral', 'golem', 'astral', 'Astral Rogue Golem', 345, 345, 54, 15, 0.5555, 0.0200, 17, 18, 28, 2, ARRAY['stun']::text[], false, now(), 17, 0.3, 5, 10.5600, 3.4720, 3.6000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (131, 'wraith_l5_6_meadow', 'wraith', 'meadow', 'Elder Verdant Wraith', 106, 106, 32, 1, 1.0670, 0.0600, 5, 6, 12, 1, ARRAY['dodge']::text[], false, now(), 5, 0.3, 5, 6.5000, 2.7000, 1.0000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (132, 'wraith_l5_6_forest', 'wraith', 'forest', 'Woodland Elder Wraith', 118, 118, 35, 1, 1.0670, 0.0600, 5, 6, 13, 1, ARRAY['dodge']::text[], false, now(), 5, 0.3, 5, 6.5000, 2.7000, 1.0000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (133, 'wraith_l7_8_forest', 'wraith', 'forest', 'Young Woodland Wraith', 112, 112, 33, 1, 1.0780, 0.0600, 7, 8, 15, 1, ARRAY['dodge']::text[], false, now(), 7, 0.3, 5, 7.0200, 2.8620, 1.0500, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (134, 'wraith_l7_8_ruins', 'wraith', 'ruins', 'Forgotten Young Wraith', 124, 124, 37, 1, 1.0780, 0.0600, 7, 8, 16, 1, ARRAY['dodge']::text[], false, now(), 7, 0.3, 5, 7.0200, 2.8620, 1.0500, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (135, 'wraith_l9_10_ruins', 'wraith', 'ruins', 'Lost Forgotten Wraith', 118, 118, 35, 1, 1.0890, 0.0600, 9, 10, 18, 1, ARRAY['dodge']::text[], false, now(), 9, 0.3, 5, 7.5400, 3.0240, 1.1000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (136, 'wraith_l9_10_canyon', 'wraith', 'canyon', 'Rift Lost Wraith', 131, 131, 39, 1, 1.0890, 0.0600, 9, 10, 19, 1, ARRAY['dodge']::text[], false, now(), 9, 0.3, 5, 7.5400, 3.0240, 1.1000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (137, 'wraith_l11_12_canyon', 'wraith', 'canyon', 'Cursed Rift Wraith', 124, 124, 37, 1, 1.1000, 0.0600, 11, 12, 21, 1, ARRAY['dodge']::text[], false, now(), 11, 0.3, 5, 8.0600, 3.1860, 1.1500, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (138, 'wraith_l11_12_swamp', 'wraith', 'swamp', 'Bog Cursed Wraith', 137, 137, 41, 1, 1.1000, 0.0600, 11, 12, 22, 1, ARRAY['dodge']::text[], false, now(), 11, 0.3, 5, 8.0600, 3.1860, 1.1500, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (139, 'wraith_l13_14_volcanic', 'wraith', 'volcanic', 'Rogue Ember Wraith', 130, 130, 39, 1, 1.1110, 0.0600, 13, 14, 24, 1, ARRAY['dodge']::text[], false, now(), 13, 0.3, 5, 8.5800, 3.3480, 1.2000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (140, 'wraith_l13_14_astral', 'wraith', 'astral', 'Astral Rogue Wraith', 144, 144, 43, 1, 1.1110, 0.0600, 13, 14, 25, 1, ARRAY['dodge']::text[], false, now(), 13, 0.3, 5, 8.5800, 3.3480, 1.2000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (141, 'bandit_l4_5_meadow', 'bandit', 'meadow', 'Elder Verdant Bandit', 114, 114, 26, 2, 1.1155, 0.0700, 4, 5, 9, 2, ARRAY['burst']::text[], false, now(), 4, 0.3, 5, 7.2000, 2.5000, 1.5000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (142, 'bandit_l4_5_forest', 'bandit', 'forest', 'Woodland Elder Bandit', 126, 126, 29, 2, 1.1155, 0.0700, 4, 5, 10, 2, ARRAY['burst']::text[], false, now(), 4, 0.3, 5, 7.2000, 2.5000, 1.5000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (143, 'bandit_l6_7_forest', 'bandit', 'forest', 'Young Woodland Bandit', 120, 120, 28, 2, 1.1270, 0.0700, 6, 7, 12, 2, ARRAY['burst']::text[], false, now(), 6, 0.3, 5, 7.7760, 2.6500, 1.5750, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (144, 'bandit_l6_7_ruins', 'bandit', 'ruins', 'Forgotten Young Bandit', 133, 133, 31, 2, 1.1270, 0.0700, 6, 7, 13, 2, ARRAY['burst']::text[], false, now(), 6, 0.3, 5, 7.7760, 2.6500, 1.5750, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (145, 'bandit_l8_9_ruins', 'bandit', 'ruins', 'Lost Forgotten Bandit', 127, 127, 30, 2, 1.1385, 0.0700, 8, 9, 15, 2, ARRAY['burst']::text[], false, now(), 8, 0.3, 5, 8.3520, 2.8000, 1.6500, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (146, 'bandit_l8_9_canyon', 'bandit', 'canyon', 'Rift Lost Bandit', 140, 140, 33, 2, 1.1385, 0.0700, 8, 9, 16, 2, ARRAY['burst']::text[], false, now(), 8, 0.3, 5, 8.3520, 2.8000, 1.6500, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (147, 'bandit_l10_11_canyon', 'bandit', 'canyon', 'Cursed Rift Bandit', 133, 133, 31, 2, 1.1500, 0.0700, 10, 11, 18, 2, ARRAY['burst']::text[], false, now(), 10, 0.3, 5, 8.9280, 2.9500, 1.7250, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (148, 'bandit_l10_11_swamp', 'bandit', 'swamp', 'Bog Cursed Bandit', 147, 147, 34, 2, 1.1500, 0.0700, 10, 11, 19, 2, ARRAY['burst']::text[], false, now(), 10, 0.3, 5, 8.9280, 2.9500, 1.7250, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (149, 'bandit_l12_12_volcanic', 'bandit', 'volcanic', 'Rogue Ember Bandit', 140, 140, 33, 2, 1.1615, 0.0700, 12, 12, 21, 2, ARRAY['burst']::text[], false, now(), 12, 0.3, 5, 9.5040, 3.1000, 1.8000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (150, 'bandit_l12_12_astral', 'bandit', 'astral', 'Astral Rogue Bandit', 154, 154, 36, 2, 1.1615, 0.0700, 12, 12, 22, 2, ARRAY['burst']::text[], false, now(), 12, 0.3, 5, 9.5040, 3.1000, 1.8000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (151, 'cultist_l6_8_meadow', 'cultist', 'meadow', 'Elder Verdant Cultist', 107, 107, 24, 2, 0.8730, 0.0500, 6, 8, 10, 1, ARRAY['burn']::text[], false, now(), 7, 0.3, 5, 5.5000, 2.4000, 1.4000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (152, 'cultist_l6_8_forest', 'cultist', 'forest', 'Woodland Elder Cultist', 118, 118, 27, 2, 0.8730, 0.0500, 6, 8, 11, 1, ARRAY['burn']::text[], false, now(), 7, 0.3, 5, 5.5000, 2.4000, 1.4000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (153, 'cultist_l9_10_forest', 'cultist', 'forest', 'Young Woodland Cultist', 112, 112, 26, 2, 0.8820, 0.0500, 9, 10, 13, 1, ARRAY['burn']::text[], false, now(), 9, 0.3, 5, 5.9400, 2.5440, 1.4700, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (154, 'cultist_l9_10_ruins', 'cultist', 'ruins', 'Forgotten Young Cultist', 124, 124, 28, 2, 0.8820, 0.0500, 9, 10, 14, 1, ARRAY['burn']::text[], false, now(), 9, 0.3, 5, 5.9400, 2.5440, 1.4700, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (155, 'cultist_l11_12_ruins', 'cultist', 'ruins', 'Lost Forgotten Cultist', 118, 118, 27, 2, 0.8910, 0.0500, 11, 12, 16, 1, ARRAY['burn']::text[], false, now(), 11, 0.3, 5, 6.3800, 2.6880, 1.5400, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (156, 'cultist_l11_12_canyon', 'cultist', 'canyon', 'Rift Lost Cultist', 130, 130, 30, 2, 0.8910, 0.0500, 11, 12, 17, 1, ARRAY['burn']::text[], false, now(), 11, 0.3, 5, 6.3800, 2.6880, 1.5400, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (157, 'cultist_l13_14_canyon', 'cultist', 'canyon', 'Cursed Rift Cultist', 123, 123, 28, 2, 0.9000, 0.0500, 13, 14, 19, 1, ARRAY['burn']::text[], false, now(), 13, 0.3, 5, 6.8200, 2.8320, 1.6100, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (158, 'cultist_l13_14_swamp', 'cultist', 'swamp', 'Bog Cursed Cultist', 136, 136, 31, 2, 0.9000, 0.0500, 13, 14, 20, 1, ARRAY['burn']::text[], false, now(), 13, 0.3, 5, 6.8200, 2.8320, 1.6100, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (159, 'cultist_l15_16_volcanic', 'cultist', 'volcanic', 'Rogue Ember Cultist', 129, 129, 29, 2, 0.9090, 0.0500, 15, 16, 22, 1, ARRAY['burn']::text[], false, now(), 15, 0.3, 5, 7.2600, 2.9760, 1.6800, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (160, 'cultist_l15_16_astral', 'cultist', 'astral', 'Astral Rogue Cultist', 143, 143, 33, 3, 0.9090, 0.0500, 15, 16, 23, 1, ARRAY['burn']::text[], false, now(), 15, 0.3, 5, 7.2600, 2.9760, 1.6800, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (161, 'treant_l18_20_meadow', 'treant', 'meadow', 'Elder Verdant Treant', 444, 444, 59, 13, 0.4365, 0.0200, 18, 20, 32, 1, ARRAY['regen']::text[], true, now(), 19, 0.3, 5, 7.0000, 3.2000, 2.9000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (162, 'treant_l18_20_forest', 'treant', 'forest', 'Woodland Elder Treant', 490, 490, 65, 14, 0.4365, 0.0200, 18, 20, 33, 1, ARRAY['regen']::text[], true, now(), 19, 0.3, 5, 7.0000, 3.2000, 2.9000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (163, 'treant_l21_23_forest', 'treant', 'forest', 'Young Woodland Treant', 470, 470, 62, 14, 0.4410, 0.0200, 21, 23, 35, 1, ARRAY['regen']::text[], true, now(), 22, 0.3, 5, 7.5600, 3.3920, 3.0450, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (164, 'treant_l21_23_ruins', 'treant', 'ruins', 'Forgotten Young Treant', 520, 520, 69, 15, 0.4410, 0.0200, 21, 23, 36, 1, ARRAY['regen']::text[], true, now(), 22, 0.3, 5, 7.5600, 3.3920, 3.0450, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (165, 'treant_l24_26_ruins', 'treant', 'ruins', 'Lost Forgotten Treant', 497, 497, 66, 14, 0.4455, 0.0200, 24, 26, 38, 1, ARRAY['regen']::text[], true, now(), 25, 0.3, 5, 8.1200, 3.5840, 3.1900, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (166, 'treant_l24_26_canyon', 'treant', 'canyon', 'Rift Lost Treant', 549, 549, 73, 16, 0.4455, 0.0200, 24, 26, 39, 1, ARRAY['regen']::text[], true, now(), 25, 0.3, 5, 8.1200, 3.5840, 3.1900, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (167, 'treant_l27_28_canyon', 'treant', 'canyon', 'Cursed Rift Treant', 514, 514, 68, 15, 0.4500, 0.0200, 27, 28, 41, 2, ARRAY['regen']::text[], true, now(), 27, 0.3, 5, 8.6800, 3.7760, 3.3350, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (168, 'treant_l27_28_swamp', 'treant', 'swamp', 'Bog Cursed Treant', 568, 568, 75, 17, 0.4500, 0.0200, 27, 28, 42, 2, ARRAY['regen']::text[], true, now(), 27, 0.3, 5, 8.6800, 3.7760, 3.3350, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (169, 'treant_l29_30_volcanic', 'treant', 'volcanic', 'Rogue Ember Treant', 532, 532, 70, 15, 0.4545, 0.0200, 29, 30, 44, 3, ARRAY['regen']::text[], true, now(), 29, 0.3, 5, 9.2400, 3.9680, 3.4800, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (170, 'treant_l29_30_astral', 'treant', 'astral', 'Astral Rogue Treant', 588, 588, 78, 17, 0.4545, 0.0200, 29, 30, 45, 3, ARRAY['regen']::text[], true, now(), 29, 0.3, 5, 9.2400, 3.9680, 3.4800, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (171, 'basilisk_l9_11_meadow', 'basilisk', 'meadow', 'Elder Verdant Basilisk', 151, 151, 31, 3, 0.9700, 0.1200, 9, 11, 14, 1, ARRAY['poison']::text[], false, now(), 10, 0.3, 5, 9.0000, 2.6000, 1.8000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (172, 'basilisk_l9_11_forest', 'basilisk', 'forest', 'Woodland Elder Basilisk', 167, 167, 34, 4, 0.9700, 0.1200, 9, 11, 15, 1, ARRAY['poison','critical']::text[], false, now(), 10, 0.3, 5, 9.0000, 2.6000, 1.8000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (173, 'basilisk_l12_13_forest', 'basilisk', 'forest', 'Young Woodland Basilisk', 159, 159, 33, 3, 0.9800, 0.1200, 12, 13, 17, 1, ARRAY['poison','critical']::text[], false, now(), 12, 0.3, 5, 9.7200, 2.7560, 1.8900, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (174, 'basilisk_l12_13_ruins', 'basilisk', 'ruins', 'Forgotten Young Basilisk', 176, 176, 36, 4, 0.9800, 0.1200, 12, 13, 18, 1, ARRAY['poison','critical']::text[], false, now(), 12, 0.3, 5, 9.7200, 2.7560, 1.8900, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (175, 'basilisk_l14_15_ruins', 'basilisk', 'ruins', 'Lost Forgotten Basilisk', 166, 166, 34, 3, 0.9900, 0.1200, 14, 15, 20, 1, ARRAY['poison']::text[], false, now(), 14, 0.3, 5, 10.4400, 2.9120, 1.9800, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (176, 'basilisk_l14_15_canyon', 'basilisk', 'canyon', 'Rift Lost Basilisk', 184, 184, 38, 4, 0.9900, 0.1200, 14, 15, 21, 1, ARRAY['poison','critical']::text[], false, now(), 14, 0.3, 5, 10.4400, 2.9120, 1.9800, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (177, 'basilisk_l16_17_canyon', 'basilisk', 'canyon', 'Cursed Rift Basilisk', 173, 173, 36, 4, 1.0000, 0.1200, 16, 17, 23, 1, ARRAY['poison','critical']::text[], false, now(), 16, 0.3, 5, 11.1600, 3.0680, 2.0700, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (178, 'basilisk_l16_17_swamp', 'basilisk', 'swamp', 'Bog Cursed Basilisk', 192, 192, 39, 4, 1.0000, 0.1200, 16, 17, 24, 1, ARRAY['poison','critical']::text[], false, now(), 16, 0.3, 5, 11.1600, 3.0680, 2.0700, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (179, 'basilisk_l18_19_volcanic', 'basilisk', 'volcanic', 'Rogue Ember Basilisk', 181, 181, 37, 4, 1.0100, 0.1200, 18, 19, 26, 1, ARRAY['poison']::text[], false, now(), 18, 0.3, 5, 11.8800, 3.2240, 2.1600, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (180, 'basilisk_l18_19_astral', 'basilisk', 'astral', 'Astral Rogue Basilisk', 200, 200, 41, 4, 1.0100, 0.1200, 18, 19, 27, 1, ARRAY['poison','critical']::text[], false, now(), 18, 0.3, 5, 11.8800, 3.2240, 2.1600, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (181, 'wyvern_l12_14_meadow', 'wyvern', 'meadow', 'Elder Verdant Wyvern', 208, 208, 39, 5, 1.3580, 0.0900, 12, 14, 16, 2, ARRAY['burn']::text[], false, now(), 13, 0.3, 5, 8.5000, 2.7000, 2.0000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (182, 'wyvern_l12_14_forest', 'wyvern', 'forest', 'Woodland Elder Wyvern', 230, 230, 43, 5, 1.3580, 0.0900, 12, 14, 17, 2, ARRAY['burn']::text[], false, now(), 13, 0.3, 5, 8.5000, 2.7000, 2.0000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (183, 'wyvern_l15_17_forest', 'wyvern', 'forest', 'Young Woodland Wyvern', 222, 222, 41, 5, 1.3720, 0.0900, 15, 17, 19, 2, ARRAY['burn']::text[], false, now(), 16, 0.3, 5, 9.1800, 2.8620, 2.1000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (184, 'wyvern_l15_17_ruins', 'wyvern', 'ruins', 'Forgotten Young Wyvern', 246, 246, 46, 6, 1.3720, 0.0900, 15, 17, 20, 2, ARRAY['burn']::text[], false, now(), 16, 0.3, 5, 9.1800, 2.8620, 2.1000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (185, 'wyvern_l18_20_ruins', 'wyvern', 'ruins', 'Lost Forgotten Wyvern', 236, 236, 44, 5, 1.3860, 0.0900, 18, 20, 22, 2, ARRAY['burn']::text[], false, now(), 19, 0.3, 5, 9.8600, 3.0240, 2.2000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (186, 'wyvern_l18_20_canyon', 'wyvern', 'canyon', 'Rift Lost Wyvern', 261, 261, 49, 6, 1.3860, 0.0900, 18, 20, 23, 2, ARRAY['burn']::text[], false, now(), 19, 0.3, 5, 9.8600, 3.0240, 2.2000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (187, 'wyvern_l21_22_canyon', 'wyvern', 'canyon', 'Cursed Rift Wyvern', 246, 246, 46, 6, 1.4000, 0.0900, 21, 22, 25, 2, ARRAY['burn']::text[], false, now(), 21, 0.3, 5, 10.5400, 3.1860, 2.3000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (188, 'wyvern_l21_22_swamp', 'wyvern', 'swamp', 'Bog Cursed Wyvern', 272, 272, 51, 6, 1.4000, 0.0900, 21, 22, 26, 2, ARRAY['burn']::text[], false, now(), 21, 0.3, 5, 10.5400, 3.1860, 2.3000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (189, 'wyvern_l23_24_volcanic', 'wyvern', 'volcanic', 'Rogue Ember Wyvern', 255, 255, 47, 6, 1.4140, 0.0900, 23, 24, 28, 2, ARRAY['burn']::text[], false, now(), 23, 0.3, 5, 11.2200, 3.3480, 2.4000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (190, 'wyvern_l23_24_astral', 'wyvern', 'astral', 'Astral Rogue Wyvern', 282, 282, 52, 7, 1.4140, 0.0900, 23, 24, 29, 2, ARRAY['burn']::text[], false, now(), 23, 0.3, 5, 11.2200, 3.3480, 2.4000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (191, 'harpy_l6_7_meadow', 'harpy', 'meadow', 'Elder Verdant Harpy', 126, 126, 29, 2, 1.5520, 0.1100, 6, 7, 11, 1, ARRAY['critical']::text[], false, now(), 6, 0.3, 5, 7.8000, 2.5000, 1.2000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (192, 'harpy_l6_7_forest', 'harpy', 'forest', 'Woodland Elder Harpy', 139, 139, 32, 2, 1.5520, 0.1100, 6, 7, 12, 1, ARRAY['critical']::text[], false, now(), 6, 0.3, 5, 7.8000, 2.5000, 1.2000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (193, 'harpy_l8_9_forest', 'harpy', 'forest', 'Young Woodland Harpy', 132, 132, 31, 2, 1.5680, 0.1100, 8, 9, 14, 1, ARRAY['critical']::text[], false, now(), 8, 0.3, 5, 8.4240, 2.6500, 1.2600, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (194, 'harpy_l8_9_ruins', 'harpy', 'ruins', 'Forgotten Young Harpy', 146, 146, 34, 2, 1.5680, 0.1100, 8, 9, 15, 1, ARRAY['critical']::text[], false, now(), 8, 0.3, 5, 8.4240, 2.6500, 1.2600, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (195, 'harpy_l10_11_ruins', 'harpy', 'ruins', 'Lost Forgotten Harpy', 139, 139, 32, 2, 1.5840, 0.1100, 10, 11, 17, 1, ARRAY['critical']::text[], false, now(), 10, 0.3, 5, 9.0480, 2.8000, 1.3200, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (196, 'harpy_l10_11_canyon', 'harpy', 'canyon', 'Rift Lost Harpy', 154, 154, 36, 2, 1.5840, 0.1100, 10, 11, 18, 1, ARRAY['critical']::text[], false, now(), 10, 0.3, 5, 9.0480, 2.8000, 1.3200, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (197, 'harpy_l12_13_canyon', 'harpy', 'canyon', 'Cursed Rift Harpy', 146, 146, 34, 2, 1.6000, 0.1100, 12, 13, 20, 1, ARRAY['critical']::text[], false, now(), 12, 0.3, 5, 9.6720, 2.9500, 1.3800, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (198, 'harpy_l12_13_swamp', 'harpy', 'swamp', 'Bog Cursed Harpy', 161, 161, 38, 2, 1.6000, 0.1100, 12, 13, 21, 1, ARRAY['critical']::text[], false, now(), 12, 0.3, 5, 9.6720, 2.9500, 1.3800, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (199, 'harpy_l14_15_volcanic', 'harpy', 'volcanic', 'Rogue Ember Harpy', 153, 153, 35, 2, 1.6160, 0.1100, 14, 15, 23, 1, ARRAY['critical']::text[], false, now(), 14, 0.3, 5, 10.2960, 3.1000, 1.4400, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (200, 'harpy_l14_15_astral', 'harpy', 'astral', 'Astral Rogue Harpy', 169, 169, 39, 2, 1.6160, 0.1100, 14, 15, 24, 1, ARRAY['critical']::text[], false, now(), 14, 0.3, 5, 10.2960, 3.1000, 1.4400, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (201, 'manticore_l14_16_meadow', 'manticore', 'meadow', 'Elder Verdant Manticore', 258, 258, 44, 6, 0.8245, 0.0800, 14, 16, 19, 2, ARRAY['poison']::text[], false, now(), 15, 0.3, 5, 9.2000, 2.8000, 2.1000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (202, 'manticore_l14_16_forest', 'manticore', 'forest', 'Woodland Elder Manticore', 286, 286, 49, 7, 0.8245, 0.0800, 14, 16, 20, 2, ARRAY['poison','burst']::text[], false, now(), 15, 0.3, 5, 9.2000, 2.8000, 2.1000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (203, 'manticore_l17_19_forest', 'manticore', 'forest', 'Young Woodland Manticore', 275, 275, 47, 7, 0.8330, 0.0800, 17, 19, 22, 2, ARRAY['poison','burst']::text[], false, now(), 18, 0.3, 5, 9.9360, 2.9680, 2.2050, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (204, 'manticore_l17_19_ruins', 'manticore', 'ruins', 'Forgotten Young Manticore', 304, 304, 52, 8, 0.8330, 0.0800, 17, 19, 23, 2, ARRAY['poison','burst']::text[], false, now(), 18, 0.3, 5, 9.9360, 2.9680, 2.2050, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (205, 'manticore_l20_22_ruins', 'manticore', 'ruins', 'Lost Forgotten Manticore', 292, 292, 50, 7, 0.8415, 0.0800, 20, 22, 25, 2, ARRAY['poison']::text[], false, now(), 21, 0.3, 5, 10.6720, 3.1360, 2.3100, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (206, 'manticore_l20_22_canyon', 'manticore', 'canyon', 'Rift Lost Manticore', 323, 323, 56, 8, 0.8415, 0.0800, 20, 22, 26, 2, ARRAY['poison','burst']::text[], false, now(), 21, 0.3, 5, 10.6720, 3.1360, 2.3100, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (207, 'manticore_l23_24_canyon', 'manticore', 'canyon', 'Cursed Rift Manticore', 303, 303, 52, 7, 0.8500, 0.0800, 23, 24, 28, 2, ARRAY['poison','burst']::text[], false, now(), 23, 0.3, 5, 11.4080, 3.3040, 2.4150, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (208, 'manticore_l23_24_swamp', 'manticore', 'swamp', 'Bog Cursed Manticore', 335, 335, 58, 8, 0.8500, 0.0800, 23, 24, 29, 2, ARRAY['poison','burst']::text[], false, now(), 23, 0.3, 5, 11.4080, 3.3040, 2.4150, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (209, 'manticore_l25_26_volcanic', 'manticore', 'volcanic', 'Rogue Ember Manticore', 314, 314, 54, 8, 0.8585, 0.0800, 25, 26, 31, 2, ARRAY['poison']::text[], false, now(), 25, 0.3, 5, 12.1440, 3.4720, 2.5200, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (210, 'manticore_l25_26_astral', 'manticore', 'astral', 'Astral Rogue Manticore', 347, 347, 60, 9, 0.8585, 0.0800, 25, 26, 32, 2, ARRAY['poison','burst']::text[], false, now(), 25, 0.3, 5, 12.1440, 3.4720, 2.5200, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (211, 'shade_l10_12_meadow', 'shade', 'meadow', 'Elder Verdant Shade', 161, 161, 34, 2, 0.9700, 0.0500, 10, 12, 17, 1, ARRAY['slow']::text[], false, now(), 11, 0.3, 5, 6.8000, 2.6000, 1.3000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (212, 'shade_l10_12_forest', 'shade', 'forest', 'Woodland Elder Shade', 178, 178, 38, 2, 0.9700, 0.0500, 10, 12, 18, 1, ARRAY['slow','dodge']::text[], false, now(), 11, 0.3, 5, 6.8000, 2.6000, 1.3000, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (213, 'shade_l13_15_forest', 'shade', 'forest', 'Young Woodland Shade', 173, 173, 37, 2, 0.9800, 0.0500, 13, 15, 20, 1, ARRAY['slow','dodge']::text[], false, now(), 14, 0.3, 5, 7.3440, 2.7560, 1.3650, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (214, 'shade_l13_15_ruins', 'shade', 'ruins', 'Forgotten Young Shade', 191, 191, 41, 2, 0.9800, 0.0500, 13, 15, 21, 1, ARRAY['slow','dodge']::text[], false, now(), 14, 0.3, 5, 7.3440, 2.7560, 1.3650, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (215, 'shade_l16_18_ruins', 'shade', 'ruins', 'Lost Forgotten Shade', 184, 184, 39, 2, 0.9900, 0.0500, 16, 18, 23, 1, ARRAY['slow']::text[], false, now(), 17, 0.3, 5, 7.8880, 2.9120, 1.4300, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (216, 'shade_l16_18_canyon', 'shade', 'canyon', 'Rift Lost Shade', 204, 204, 43, 3, 0.9900, 0.0500, 16, 18, 24, 1, ARRAY['slow','dodge']::text[], false, now(), 17, 0.3, 5, 7.8880, 2.9120, 1.4300, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (217, 'shade_l19_20_canyon', 'shade', 'canyon', 'Cursed Rift Shade', 192, 192, 41, 2, 1.0000, 0.0500, 19, 20, 26, 1, ARRAY['slow','dodge']::text[], false, now(), 19, 0.3, 5, 8.4320, 3.0680, 1.4950, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (218, 'shade_l19_20_swamp', 'shade', 'swamp', 'Bog Cursed Shade', 212, 212, 45, 3, 1.0000, 0.0500, 19, 20, 27, 1, ARRAY['slow','dodge']::text[], false, now(), 19, 0.3, 5, 8.4320, 3.0680, 1.4950, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (219, 'shade_l21_22_volcanic', 'shade', 'volcanic', 'Rogue Ember Shade', 200, 200, 43, 3, 1.0100, 0.0500, 21, 22, 29, 1, ARRAY['slow']::text[], false, now(), 21, 0.3, 5, 8.9760, 3.2240, 1.5600, 2.0, 1.2);
|
||||||
|
INSERT INTO public.enemies (id, type, archetype, biome, name, hp, max_hp, attack, defense, speed, crit_chance, min_level, max_level, xp_reward, gold_reward, special_abilities, is_elite, created_at, base_level, level_variance_pct, max_hero_level_diff, hp_per_level, attack_per_level, defense_per_level, xp_per_level, gold_per_level) VALUES (220, 'shade_l21_22_astral', 'shade', 'astral', 'Astral Rogue Shade', 221, 221, 47, 3, 1.0100, 0.0500, 21, 22, 30, 1, ARRAY['slow','dodge']::text[], false, now(), 21, 0.3, 5, 8.9760, 3.2240, 1.5600, 2.0, 1.2);
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
ALTER TABLE public.enemies ALTER COLUMN archetype SET NOT NULL;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS enemies_type_key ON public.enemies (type);
|
||||||
|
|
||||||
|
SELECT setval('public.enemies_id_seq', (SELECT COALESCE(MAX(id), 1) FROM public.enemies));
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
-- Biome column for enemy templates (canonical ids from internal/world levelBands).
|
||||||
|
-- For databases that ran 000006a before biome existed; fresh installs get column from 000006a_head.
|
||||||
|
ALTER TABLE public.enemies ADD COLUMN IF NOT EXISTS biome text;
|
||||||
|
|
||||||
|
UPDATE public.enemies
|
||||||
|
SET biome = substring(type from '[^_]+$')
|
||||||
|
WHERE biome IS NULL OR trim(biome) = '';
|
||||||
|
|
||||||
|
ALTER TABLE public.enemies ALTER COLUMN biome SET NOT NULL;
|
||||||
@ -1,2 +0,0 @@
|
|||||||
-- Make hero name unique (case-insensitive)
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_heroes_name_lower ON heroes(LOWER(name)) WHERE name != '' AND name != 'Hero';
|
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
-- Item scaling: geometric ilvl, legendary = +40% vs common (log-spaced rarities).
|
||||||
|
UPDATE public.runtime_config
|
||||||
|
SET payload = payload || jsonb_build_object(
|
||||||
|
'ilvlPerLevelMultiplier', 1.10,
|
||||||
|
'rarityMultiplierCommon', 1.00,
|
||||||
|
'rarityMultiplierUncommon', 1.0877573,
|
||||||
|
'rarityMultiplierRare', 1.1832160,
|
||||||
|
'rarityMultiplierEpic', 1.2870518,
|
||||||
|
'rarityMultiplierLegendary', 1.40
|
||||||
|
),
|
||||||
|
updated_at = now()
|
||||||
|
WHERE id = true;
|
||||||
@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE weapons ADD COLUMN IF NOT EXISTS ilvl INT NOT NULL DEFAULT 1;
|
|
||||||
ALTER TABLE armor ADD COLUMN IF NOT EXISTS ilvl INT NOT NULL DEFAULT 1;
|
|
||||||
@ -0,0 +1,76 @@
|
|||||||
|
-- Normalize catalog stats: base damage/defense per weapon/armor *family* only.
|
||||||
|
-- Apply 000010_weapons_secondary_normalize.sql next (weapon speed/crit by class only).
|
||||||
|
-- Apply 000011_armor_base_tighten.sql (armor bases + gear-check HP target).
|
||||||
|
-- Rarity spread (+40% Legendary vs Common at same ilvl) comes from runtime_config M(rarity), not from inflating DB bases.
|
||||||
|
-- Sync template rows in `gear` (catalog mirror + denormalized primary_stat at ilvl=1).
|
||||||
|
|
||||||
|
UPDATE public.runtime_config
|
||||||
|
SET payload = payload || jsonb_build_object(
|
||||||
|
'ilvlPerLevelMultiplier', 1.10,
|
||||||
|
'rarityMultiplierCommon', 1.00,
|
||||||
|
'rarityMultiplierUncommon', 1.0877573,
|
||||||
|
'rarityMultiplierRare', 1.1832160,
|
||||||
|
'rarityMultiplierEpic', 1.2870518,
|
||||||
|
'rarityMultiplierLegendary', 1.40
|
||||||
|
),
|
||||||
|
updated_at = now()
|
||||||
|
WHERE id = true;
|
||||||
|
|
||||||
|
UPDATE public.weapons SET damage = CASE type
|
||||||
|
WHEN 'daggers' THEN 3
|
||||||
|
WHEN 'sword' THEN 7
|
||||||
|
WHEN 'axe' THEN 12
|
||||||
|
END;
|
||||||
|
|
||||||
|
UPDATE public.armor SET defense = CASE type
|
||||||
|
WHEN 'light' THEN 3
|
||||||
|
WHEN 'medium' THEN 7
|
||||||
|
WHEN 'heavy' THEN 14
|
||||||
|
END;
|
||||||
|
|
||||||
|
-- Denormalized primary at ilvl=1: round(base * M(rarity)) — same M as runtime_config / §6.4.2
|
||||||
|
UPDATE public.gear AS g
|
||||||
|
SET
|
||||||
|
base_primary = w.damage,
|
||||||
|
primary_stat = ROUND(w.damage * CASE w.rarity
|
||||||
|
WHEN 'common' THEN 1.0
|
||||||
|
WHEN 'uncommon' THEN 1.0877573
|
||||||
|
WHEN 'rare' THEN 1.1832160
|
||||||
|
WHEN 'epic' THEN 1.2870518
|
||||||
|
WHEN 'legendary' THEN 1.40
|
||||||
|
END)::integer
|
||||||
|
FROM public.weapons AS w
|
||||||
|
WHERE g.slot = 'main_hand'
|
||||||
|
AND g.name = w.name;
|
||||||
|
|
||||||
|
UPDATE public.gear AS g
|
||||||
|
SET
|
||||||
|
base_primary = a.defense,
|
||||||
|
primary_stat = ROUND(a.defense * CASE a.rarity
|
||||||
|
WHEN 'common' THEN 1.0
|
||||||
|
WHEN 'uncommon' THEN 1.0877573
|
||||||
|
WHEN 'rare' THEN 1.1832160
|
||||||
|
WHEN 'epic' THEN 1.2870518
|
||||||
|
WHEN 'legendary' THEN 1.40
|
||||||
|
END)::integer
|
||||||
|
FROM public.armor AS a
|
||||||
|
WHERE g.slot = 'chest'
|
||||||
|
AND g.name = a.name;
|
||||||
|
|
||||||
|
-- Extended-slot catalog: keep one base_primary per form; scale primary_stat by row rarity (same M).
|
||||||
|
UPDATE public.equipment_items AS e
|
||||||
|
SET
|
||||||
|
base_primary = s.canon_base,
|
||||||
|
primary_stat = ROUND(s.canon_base * CASE e.rarity
|
||||||
|
WHEN 'common' THEN 1.0
|
||||||
|
WHEN 'uncommon' THEN 1.0877573
|
||||||
|
WHEN 'rare' THEN 1.1832160
|
||||||
|
WHEN 'epic' THEN 1.2870518
|
||||||
|
WHEN 'legendary' THEN 1.40
|
||||||
|
END)::integer
|
||||||
|
FROM (
|
||||||
|
SELECT form_id, MIN(base_primary) AS canon_base
|
||||||
|
FROM public.equipment_items
|
||||||
|
GROUP BY form_id
|
||||||
|
) AS s
|
||||||
|
WHERE e.form_id = s.form_id;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue