// genenemies prints SQL INSERT statements for public.enemies (220 rows: 22 archetypes × 5 level sub-bands × 2 biomes). // Biome ids match internal/world.Service levelBands (meadow, forest, ruins, canyon, swamp, volcanic, astral). // Each archetype uses min/max level span from CSV/spec (see arcLevelRange); span is split into 5 contiguous bands. // Run from backend: go run ./cmd/genenemies > migrations/000006b_enemy_data.sql package main import ( "fmt" "strings" ) type anchor struct { hp, atk, def int speed float64 crit float64 isElite bool hpPL, atkPL, defPL float64 xpRew, goldRew int64 abilities []string } // Thirteen CSV rows (000003/000005) drive mechanics; twelve user archetypes map 1:1 except element uses water+ice. var csvAnchors = map[string]anchor{ "wolf": {hp: 94, atk: 20, def: 1, speed: 1.8, crit: 0.05, hpPL: 7.8681, atkPL: 2.7054, defPL: 1.2, xpRew: 1, goldRew: 1, abilities: nil}, "boar": {hp: 102, atk: 25, def: 2, speed: 0.8, crit: 0.08, hpPL: 8.2826, atkPL: 2.319, defPL: 1.6, xpRew: 2, goldRew: 1, abilities: nil}, "zombie": {hp: 107, atk: 28, def: 2, speed: 0.5, crit: 0, hpPL: 6.9412, atkPL: 2.5898, defPL: 1.8, xpRew: 5, goldRew: 1, abilities: []string{"poison"}}, "spider": {hp: 118, atk: 24, def: 1, speed: 2.0, crit: 0.15, hpPL: 12.0614, atkPL: 2.7373, defPL: 1.0, xpRew: 7, goldRew: 1, abilities: []string{"critical"}}, "orc": {hp: 113, atk: 27, def: 3, speed: 1.0, crit: 0.05, hpPL: 7.1338, atkPL: 2.6581, defPL: 2.0, xpRew: 8, goldRew: 1, abilities: []string{"burst"}}, "skeleton": {hp: 132, atk: 28, def: 2, speed: 1.3, crit: 0.06, hpPL: 8.5586, atkPL: 2.2939, defPL: 1.7, xpRew: 9, goldRew: 1, abilities: []string{"dodge"}}, // skeleton_archer CSV "battle_lizard": {hp: 105, atk: 32, def: 4, speed: 0.7, crit: 0.03, hpPL: 5.7476, atkPL: 2.414, defPL: 2.3, xpRew: 10, goldRew: 1, abilities: []string{"regen"}}, "element_water": {hp: 349, atk: 45, def: 5, speed: 0.8, crit: 0.05, hpPL: 8.0285, atkPL: 3.1288, defPL: 2.2, xpRew: 36, goldRew: 1, isElite: true, abilities: []string{"slow"}}, "element_ice": {hp: 208, atk: 37, def: 6, speed: 0.7, crit: 0.04, hpPL: 7.5649, atkPL: 3.0394, defPL: 2.5, xpRew: 18, goldRew: 1, isElite: true, abilities: []string{"ice_slow"}}, "demon": {hp: 177, atk: 25, def: 3, speed: 1.2, crit: 0.1, hpPL: 11.72, atkPL: 2.6587, defPL: 2.0, xpRew: 11, goldRew: 1, isElite: true, abilities: []string{"burn"}}, "skeleton_king": {hp: 149, atk: 24, def: 22, speed: 0.9, crit: 0.08, hpPL: 4.1663, atkPL: 1.8339, defPL: 2.0, xpRew: 35, goldRew: 1, isElite: true, abilities: []string{"regen", "summon"}}, "forest_warden": {hp: 338, atk: 50, def: 8, speed: 0.5, crit: 0.03, hpPL: 6.1288, atkPL: 3.5033, defPL: 2.8, xpRew: 37, goldRew: 1, isElite: true, abilities: []string{"regen"}}, "titan": {hp: 583, atk: 48, def: 6, speed: 1.5, crit: 0.12, hpPL: 11.1055, atkPL: 2.9104, defPL: 2.3, xpRew: 38, goldRew: 10, isElite: true, abilities: []string{"stun", "chain_lightning"}}, // ten new — synthetic "golem": {hp: 220, atk: 35, def: 10, speed: 0.55, crit: 0.02, hpPL: 8.0, atkPL: 2.8, defPL: 3.0, xpRew: 15, goldRew: 2, abilities: []string{"stun"}}, "wraith": {hp: 100, atk: 30, def: 1, speed: 1.1, crit: 0.06, hpPL: 6.5, atkPL: 2.7, defPL: 1.0, xpRew: 12, goldRew: 1, abilities: []string{"dodge"}}, "bandit": {hp: 110, atk: 26, def: 2, speed: 1.15, crit: 0.07, hpPL: 7.2, atkPL: 2.5, defPL: 1.5, xpRew: 9, goldRew: 2, abilities: []string{"burst"}}, "cultist": {hp: 95, atk: 22, def: 2, speed: 0.9, crit: 0.05, hpPL: 5.5, atkPL: 2.4, defPL: 1.4, xpRew: 10, goldRew: 1, abilities: []string{"burn"}}, "treant": {hp: 300, atk: 40, def: 9, speed: 0.45, crit: 0.02, hpPL: 7.0, atkPL: 3.2, defPL: 2.9, xpRew: 32, goldRew: 1, isElite: true, abilities: []string{"regen"}}, "basilisk": {hp: 125, atk: 26, def: 3, speed: 1.0, crit: 0.12, hpPL: 9.0, atkPL: 2.6, defPL: 1.8, xpRew: 14, goldRew: 1, abilities: []string{"poison", "critical"}}, "wyvern": {hp: 160, atk: 30, def: 4, speed: 1.4, crit: 0.09, hpPL: 8.5, atkPL: 2.7, defPL: 2.0, xpRew: 16, goldRew: 2, abilities: []string{"burn"}}, "harpy": {hp: 115, atk: 27, def: 2, speed: 1.6, crit: 0.11, hpPL: 7.8, atkPL: 2.5, defPL: 1.2, xpRew: 11, goldRew: 1, abilities: []string{"critical"}}, "manticore": {hp: 190, atk: 33, def: 5, speed: 0.85, crit: 0.08, hpPL: 9.2, atkPL: 2.8, defPL: 2.1, xpRew: 19, goldRew: 2, abilities: []string{"poison", "burst"}}, "shade": {hp: 130, atk: 28, def: 2, speed: 1.0, crit: 0.05, hpPL: 6.8, atkPL: 2.6, defPL: 1.3, xpRew: 17, goldRew: 1, abilities: []string{"slow", "dodge"}}, } // Archetype order: 22 entries — keys match DB archetype column. var archetypeOrder = []string{ "wolf", "boar", "zombie", "spider", "orc", "skeleton", "battle_lizard", "element", "demon", "skeleton_king", "forest_warden", "titan", "golem", "wraith", "bandit", "cultist", "treant", "basilisk", "wyvern", "harpy", "manticore", "shade", } func anchorForArchetype(arch string, biomeIdx int) anchor { switch arch { case "element": if biomeIdx == 0 { return csvAnchors["element_water"] } return csvAnchors["element_ice"] default: return csvAnchors[arch] } } // arcLevelRange: inclusive [min,max] per archetype from specification / CSV anchors (13 types + 10 synthetic). var arcLevelRange = map[string][2]int{ "wolf": {1, 5}, "boar": {2, 6}, "zombie": {3, 8}, "spider": {4, 9}, "orc": {5, 12}, "skeleton": {6, 14}, "battle_lizard": {7, 15}, "demon": {10, 20}, "skeleton_king": {15, 25}, "forest_warden": {20, 30}, "titan": {25, 35}, // element: water vs ice ranges (per biome index in generator) "golem": {8, 18}, "wraith": {5, 14}, "bandit": {4, 12}, "cultist": {6, 16}, "treant": {18, 30}, "basilisk": {9, 19}, "wyvern": {12, 24}, "harpy": {6, 15}, "manticore": {14, 26}, "shade": {10, 22}, } // elementWaterRange / elementIceRange match CSV water_element vs ice_guardian bands. var elementWaterRange = [2]int{18, 28} var elementIceRange = [2]int{12, 22} func levelRangeFor(arch string, biomeIdx int) (int, int) { if arch == "element" { if biomeIdx == 0 { return elementWaterRange[0], elementWaterRange[1] } return elementIceRange[0], elementIceRange[1] } r, ok := arcLevelRange[arch] if !ok { return 1, 10 } return r[0], r[1] } // splitRange divides [minL,maxL] into `parts` contiguous inclusive bands. func splitRange(minL, maxL, parts int) [][2]int { if parts <= 0 { return nil } if minL > maxL { minL, maxL = maxL, minL } span := maxL - minL + 1 out := make([][2]int, parts) if span <= 0 { for i := range out { out[i] = [2]int{minL, maxL} } return out } if span < parts { for i := 0; i < parts; i++ { if i < span { lv := minL + i if lv > maxL { lv = maxL } out[i] = [2]int{lv, lv} } else { out[i] = [2]int{maxL, maxL} } } return out } step := span / parts rem := span % parts cur := minL for i := 0; i < parts; i++ { sz := step if i < rem { sz++ } lo := cur hi := cur + sz - 1 cur = hi + 1 out[i] = [2]int{lo, hi} } return out } func scaleForMidpoint(mid int) float64 { // Roughly matches old 5-band progression (~1.0 … ~2.35 for mid 1..45). s := 1.0 + float64(mid-1)*0.031 if s < 0.85 { s = 0.85 } if s > 2.6 { s = 2.6 } return s } var biomePairs = [][2]string{ {"meadow", "forest"}, {"forest", "ruins"}, {"ruins", "canyon"}, {"canyon", "swamp"}, {"volcanic", "astral"}, } func main() { id := 1 for _, arch := range archetypeOrder { for bandIdx := 0; bandIdx < 5; bandIdx++ { pair := biomePairs[bandIdx] for j, biome := range pair { a := anchorForArchetype(arch, j) lo, hi := levelRangeFor(arch, j) bands := splitRange(lo, hi, 5) br := bands[bandIdx] minL, maxL := br[0], br[1] baseLv := (minL + maxL) / 2 if baseLv < 1 { baseLv = 1 } mid := (minL + maxL) / 2 scale := scaleForMidpoint(mid) mult := scale if j == 0 { mult *= 0.95 } else { mult *= 1.05 } slug := fmt.Sprintf("%s_l%d_%d_%s", arch, minL, maxL, biome) name := makeName(arch, biome, bandIdx, j) hp := int(float64(a.hp) * mult) if hp < 1 { hp = 1 } atk := int(float64(a.atk) * mult) if atk < 1 { atk = 1 } def := int(float64(a.def) * mult) if a.def > 0 && def < 1 { def = 1 } speed := a.speed * (0.97 + float64(bandIdx)*0.01) if speed < 0.1 { speed = 0.1 } crit := a.crit hpPL := a.hpPL * (1 + float64(bandIdx)*0.08) atkPL := a.atkPL * (1 + float64(bandIdx)*0.06) defPL := a.defPL * (1 + float64(bandIdx)*0.05) xpRew := a.xpRew + int64(bandIdx*3+j) if xpRew < 1 { xpRew = 1 } goldRew := a.goldRew if a.isElite && bandIdx >= 3 { goldRew += int64(bandIdx - 2) } abilities := formatAbilities(variantAbilities(a.abilities, bandIdx, j)) elite := a.isElite fmt.Printf(`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 (%d, %s, %s, %s, %s, %d, %d, %d, %d, %.4f, %.4f, %d, %d, %d, %d, %s, %t, now(), %d, 0.3, 5, %.4f, %.4f, %.4f, 2.0, 1.2); `, id, quote(slug), quote(arch), quote(biome), quote(name), hp, hp, atk, def, speed, crit, minL, maxL, xpRew, goldRew, abilities, elite, baseLv, hpPL, atkPL, defPL, ) id++ } } } } func quote(s string) string { return "'" + strings.ReplaceAll(s, "'", "''") + "'" } func formatAbilities(a []string) string { if len(a) == 0 { return "ARRAY[]::text[]" } var b strings.Builder b.WriteString("ARRAY[") for i, x := range a { if i > 0 { b.WriteString(",") } b.WriteString("'") b.WriteString(strings.ReplaceAll(x, "'", "''")) b.WriteString("'") } b.WriteString("]::text[]") return b.String() } func variantAbilities(base []string, bandIdx, biomeIdx int) []string { if len(base) == 0 { return nil } out := append([]string(nil), base...) if bandIdx%2 == 0 && biomeIdx == 0 && len(out) > 1 { out = out[:len(out)-1] } return out } func makeName(arch, biome string, bandIdx, biomeIdx int) string { prefix := []string{"Elder", "Young", "Lost", "Cursed", "Rogue", "Ancient", "Feral", "Twilight", "Ashen", "Deep"}[bandIdx%10] biomeAdj := map[string]string{ "meadow": "Verdant", "forest": "Woodland", "ruins": "Forgotten", "canyon": "Rift", "swamp": "Bog", "volcanic": "Ember", "astral": "Astral", } adj := biomeAdj[biome] if adj == "" { adj = biome } species := map[string]string{ "wolf": "Wolf", "boar": "Boar", "zombie": "Zombie", "spider": "Spider", "orc": "Orc", "skeleton": "Skeleton", "battle_lizard": "Scaleback", "element": "Elemental", "demon": "Demon", "skeleton_king": "Bone Sovereign", "forest_warden": "Warden", "titan": "Titan", "golem": "Golem", "wraith": "Wraith", "bandit": "Bandit", "cultist": "Cultist", "treant": "Treant", "basilisk": "Basilisk", "wyvern": "Wyvern", "harpy": "Harpy", "manticore": "Manticore", "shade": "Shade", } sp := species[arch] if biomeIdx == 1 { return fmt.Sprintf("%s %s %s", adj, prefix, sp) } return fmt.Sprintf("%s %s %s", prefix, adj, sp) }