You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

302 lines
11 KiB
Go

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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