|
|
// 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)
|
|
|
}
|