Regional evolutions (e.g., Pikachu → Alolan Raichu) only occur in specific regions. This adds a nullable region column so the app can filter evolutions by the game's region. When a regional evolution exists for a given trigger/item, the non-regional counterpart is automatically hidden. Full-stack: migration, model, schemas, API with region query param, seeder, Go fetch tool, frontend types/API/hook/components, and admin form. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
280 lines
6.7 KiB
Go
280 lines
6.7 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"`
|
|
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
|
|
}
|