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"` Region *string `json:"region"` } `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, Region: addition.Region, }) } // 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 } case "region": if s, ok := value.(string); ok { e.Region = &s } else if value == nil { e.Region = 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 }