Files
nuzlocke-tracker/tools/fetch-pokeapi/evolutions.go
Julian Tabel 0bf628157f Add Go-based PokeAPI fetch tool
Replaces the Python fetch_pokeapi.py script with a Go tool that crawls
a local PokeAPI instance and writes seed JSON files. Supports caching
and special encounter definitions via JSON config.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 19:44:05 +01:00

272 lines
6.5 KiB
Go

package main
import (
"context"
"encoding/json"
"fmt"
"os"
"sort"
"strings"
"sync"
)
// fetchEvolutionData fetches evolution chains and returns flattened pairs.
func fetchEvolutionData(
ctx context.Context,
client *Client,
speciesData map[int]*SpeciesResp,
seededDex map[int]bool,
) ([]EvolutionOutput, error) {
fmt.Println("\n--- Fetching evolution chains ---")
// Extract unique chain IDs from species data
chainIDSet := make(map[int]bool)
for sid, species := range speciesData {
if seededDex[sid] {
chainIDSet[species.EvolutionChain.ID()] = true
}
}
chainIDs := make([]int, 0, len(chainIDSet))
for id := range chainIDSet {
chainIDs = append(chainIDs, id)
}
sort.Ints(chainIDs)
fmt.Printf(" Found %d unique evolution chains\n", len(chainIDs))
// Fetch chains concurrently
type chainResult struct {
chain EvolutionChainResp
}
results := make([]chainResult, len(chainIDs))
var wg sync.WaitGroup
errs := make([]error, len(chainIDs))
for i, cid := range chainIDs {
wg.Add(1)
go func(i, cid int) {
defer wg.Done()
data, err := client.Get(ctx, fmt.Sprintf("evolution-chain/%d", cid))
if err != nil {
errs[i] = err
return
}
if err := json.Unmarshal(data, &results[i].chain); err != nil {
errs[i] = fmt.Errorf("parsing evolution chain %d: %w", cid, err)
}
}(i, cid)
}
wg.Wait()
for _, err := range errs {
if err != nil {
return nil, err
}
}
// Flatten all chains
var allPairs []EvolutionOutput
type dedupeKey struct {
from, to int
trigger string
}
seen := make(map[dedupeKey]bool)
for _, r := range results {
pairs := flattenChain(r.chain.Chain, seededDex)
for _, p := range pairs {
key := dedupeKey{p.FromPokeAPIID, p.ToPokeAPIID, p.Trigger}
if !seen[key] {
seen[key] = true
allPairs = append(allPairs, p)
}
}
}
sort.Slice(allPairs, func(i, j int) bool {
if allPairs[i].FromPokeAPIID != allPairs[j].FromPokeAPIID {
return allPairs[i].FromPokeAPIID < allPairs[j].FromPokeAPIID
}
return allPairs[i].ToPokeAPIID < allPairs[j].ToPokeAPIID
})
fmt.Printf(" Total evolution pairs: %d\n", len(allPairs))
return allPairs, nil
}
// flattenChain recursively flattens an evolution chain into (from, to) pairs.
func flattenChain(chain ChainLink, seededDex map[int]bool) []EvolutionOutput {
var pairs []EvolutionOutput
fromDex := chain.Species.ID()
for _, evo := range chain.EvolvesTo {
toDex := evo.Species.ID()
for _, detail := range evo.EvolutionDetails {
trigger := detail.Trigger.Name
var minLevel *int
if detail.MinLevel != nil {
v := *detail.MinLevel
minLevel = &v
}
var item *string
if detail.Item != nil {
s := detail.Item.Name
item = &s
}
var heldItem *string
if detail.HeldItem != nil {
s := detail.HeldItem.Name
heldItem = &s
}
conditions := CollectEvolutionConditions(detail)
var condition *string
if len(conditions) > 0 {
s := strings.Join(conditions, ", ")
condition = &s
}
if seededDex[fromDex] && seededDex[toDex] {
pairs = append(pairs, EvolutionOutput{
FromPokeAPIID: fromDex,
ToPokeAPIID: toDex,
Trigger: trigger,
MinLevel: minLevel,
Item: item,
HeldItem: heldItem,
Condition: condition,
})
}
}
// Recurse
pairs = append(pairs, flattenChain(evo, seededDex)...)
}
return pairs
}
// EvolutionOverrides represents the evolution_overrides.json structure.
type EvolutionOverrides struct {
Remove []struct {
FromDex int `json:"from_dex"`
ToDex int `json:"to_dex"`
} `json:"remove"`
Add []struct {
FromDex int `json:"from_dex"`
ToDex int `json:"to_dex"`
Trigger string `json:"trigger"`
MinLevel *int `json:"min_level"`
Item *string `json:"item"`
HeldItem *string `json:"held_item"`
Condition *string `json:"condition"`
} `json:"add"`
Modify []struct {
FromDex int `json:"from_dex"`
ToDex int `json:"to_dex"`
Set map[string]interface{} `json:"set"`
} `json:"modify"`
}
// applyEvolutionOverrides applies overrides from evolution_overrides.json.
func applyEvolutionOverrides(evolutions []EvolutionOutput, overridesPath string) ([]EvolutionOutput, error) {
data, err := os.ReadFile(overridesPath)
if err != nil {
if os.IsNotExist(err) {
return evolutions, nil
}
return nil, fmt.Errorf("reading evolution overrides: %w", err)
}
var overrides EvolutionOverrides
if err := json.Unmarshal(data, &overrides); err != nil {
return nil, fmt.Errorf("parsing evolution overrides: %w", err)
}
// Remove entries
for _, removal := range overrides.Remove {
filtered := evolutions[:0]
for _, e := range evolutions {
if !(e.FromPokeAPIID == removal.FromDex && e.ToPokeAPIID == removal.ToDex) {
filtered = append(filtered, e)
}
}
evolutions = filtered
}
// Add entries
for _, addition := range overrides.Add {
trigger := addition.Trigger
if trigger == "" {
trigger = "level-up"
}
evolutions = append(evolutions, EvolutionOutput{
FromPokeAPIID: addition.FromDex,
ToPokeAPIID: addition.ToDex,
Trigger: trigger,
MinLevel: addition.MinLevel,
Item: addition.Item,
HeldItem: addition.HeldItem,
Condition: addition.Condition,
})
}
// Modify entries
for _, mod := range overrides.Modify {
for i := range evolutions {
e := &evolutions[i]
if e.FromPokeAPIID == mod.FromDex && e.ToPokeAPIID == mod.ToDex {
for key, value := range mod.Set {
switch key {
case "trigger":
if s, ok := value.(string); ok {
e.Trigger = s
}
case "min_level":
if v, ok := value.(float64); ok {
level := int(v)
e.MinLevel = &level
} else if value == nil {
e.MinLevel = nil
}
case "item":
if s, ok := value.(string); ok {
e.Item = &s
} else if value == nil {
e.Item = nil
}
case "held_item":
if s, ok := value.(string); ok {
e.HeldItem = &s
} else if value == nil {
e.HeldItem = nil
}
case "condition":
if s, ok := value.(string); ok {
e.Condition = &s
} else if value == nil {
e.Condition = nil
}
}
}
}
}
}
// Re-sort
sort.Slice(evolutions, func(i, j int) bool {
if evolutions[i].FromPokeAPIID != evolutions[j].FromPokeAPIID {
return evolutions[i].FromPokeAPIID < evolutions[j].FromPokeAPIID
}
return evolutions[i].ToPokeAPIID < evolutions[j].ToPokeAPIID
})
fmt.Printf(" Applied overrides: %d pairs after overrides\n", len(evolutions))
return evolutions, nil
}