updates
All checks were successful
Build and Deploy / Build & Push Image (push) Successful in 33s

This commit is contained in:
2026-03-25 20:45:57 -06:00
parent 5184b60e9c
commit 0fbb03f7de
5 changed files with 729 additions and 13 deletions

View File

@@ -14,6 +14,7 @@ defmodule CobblemonUi.Application do
CobblemonUi.CobblemonFS,
CobblemonUi.TierListScraper,
CobblemonUi.EvolutionApi,
CobblemonUi.PokeApi,
# Start to serve requests, typically the last entry
CobblemonUiWeb.Endpoint
]

View File

@@ -0,0 +1,89 @@
defmodule CobblemonUi.PokeApi do
@moduledoc """
Fetches and caches Pokémon type data from PokeAPI.
Uses an ETS table for fast lookups. Data is fetched once per species
and cached for the lifetime of the process.
"""
use GenServer
require Logger
@table :poke_api_types_cache
# ---------------------------------------------------------------------------
# Client API
# ---------------------------------------------------------------------------
def start_link(_opts) do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
@doc """
Returns a list of type names (lowercase strings) for the given species.
Example: `get_types("bulbasaur")` → `["grass", "poison"]`
"""
@spec get_types(String.t()) :: [String.t()]
def get_types(species) when is_binary(species) do
key = String.downcase(species)
case :ets.lookup(@table, key) do
[{^key, types}] ->
types
[] ->
GenServer.call(__MODULE__, {:fetch_types, key}, 15_000)
end
end
# ---------------------------------------------------------------------------
# Server callbacks
# ---------------------------------------------------------------------------
@impl true
def init(_opts) do
:ets.new(@table, [:named_table, :set, :public, read_concurrency: true])
{:ok, %{}}
end
@impl true
def handle_call({:fetch_types, species}, _from, state) do
result =
case :ets.lookup(@table, species) do
[{^species, types}] ->
types
[] ->
types = fetch_types(species)
:ets.insert(@table, {species, types})
types
end
{:reply, result, state}
end
# ---------------------------------------------------------------------------
# Private helpers
# ---------------------------------------------------------------------------
defp fetch_types(species) do
url = "https://pokeapi.co/api/v2/pokemon/#{species}"
case Req.get(url) do
{:ok, %Req.Response{status: 200, body: %{"types" => types}}} ->
types
|> Enum.sort_by(fn t -> t["slot"] end)
|> Enum.map(fn t -> get_in(t, ["type", "name"]) end)
|> Enum.reject(&is_nil/1)
{:ok, %Req.Response{status: status}} ->
Logger.warning("[PokeApi] Type lookup failed for #{species}: HTTP #{status}")
[]
{:error, reason} ->
Logger.warning("[PokeApi] Type lookup failed for #{species}: #{inspect(reason)}")
[]
end
end
end

View File

@@ -0,0 +1,272 @@
defmodule CobblemonUi.TypeChart do
@moduledoc """
Pokemon type effectiveness data. Contains strengths (super effective),
weaknesses (weak to), resistances (not very effective against), and immunities.
"""
@types ~w(normal fire water grass electric ice fighting poison ground flying psychic bug rock ghost dragon dark steel fairy)
def types, do: @types
@doc """
Returns a map of type -> %{weak_to: [...], resists: [...], immune_to: [...]}
representing the defensive matchups for each type.
"""
def defensive_chart do
%{
"normal" => %{weak_to: ["fighting"], resists: [], immune_to: ["ghost"]},
"fire" => %{
weak_to: ["water", "ground", "rock"],
resists: ["fire", "grass", "ice", "bug", "steel", "fairy"],
immune_to: []
},
"water" => %{
weak_to: ["electric", "grass"],
resists: ["fire", "water", "ice", "steel"],
immune_to: []
},
"grass" => %{
weak_to: ["fire", "ice", "poison", "flying", "bug"],
resists: ["water", "electric", "grass", "ground"],
immune_to: []
},
"electric" => %{
weak_to: ["ground"],
resists: ["electric", "flying", "steel"],
immune_to: []
},
"ice" => %{weak_to: ["fire", "fighting", "rock", "steel"], resists: ["ice"], immune_to: []},
"fighting" => %{
weak_to: ["flying", "psychic", "fairy"],
resists: ["bug", "rock", "dark"],
immune_to: []
},
"poison" => %{
weak_to: ["ground", "psychic"],
resists: ["grass", "fighting", "poison", "bug", "fairy"],
immune_to: []
},
"ground" => %{
weak_to: ["water", "grass", "ice"],
resists: ["poison", "rock"],
immune_to: ["electric"]
},
"flying" => %{
weak_to: ["electric", "ice", "rock"],
resists: ["grass", "fighting", "bug"],
immune_to: ["ground"]
},
"psychic" => %{
weak_to: ["bug", "ghost", "dark"],
resists: ["fighting", "psychic"],
immune_to: []
},
"bug" => %{
weak_to: ["fire", "flying", "rock"],
resists: ["grass", "fighting", "ground"],
immune_to: []
},
"rock" => %{
weak_to: ["water", "grass", "fighting", "ground", "steel"],
resists: ["normal", "fire", "poison", "flying"],
immune_to: []
},
"ghost" => %{
weak_to: ["ghost", "dark"],
resists: ["poison", "bug"],
immune_to: ["normal", "fighting"]
},
"dragon" => %{
weak_to: ["ice", "dragon", "fairy"],
resists: ["fire", "water", "electric", "grass"],
immune_to: []
},
"dark" => %{
weak_to: ["fighting", "bug", "fairy"],
resists: ["ghost", "dark"],
immune_to: ["psychic"]
},
"steel" => %{
weak_to: ["fire", "fighting", "ground"],
resists: [
"normal",
"grass",
"ice",
"flying",
"psychic",
"bug",
"rock",
"dragon",
"steel",
"fairy"
],
immune_to: ["poison"]
},
"fairy" => %{
weak_to: ["poison", "steel"],
resists: ["fighting", "bug", "dark"],
immune_to: ["dragon"]
}
}
end
@doc """
Returns a map of type -> %{strong_against: [...], not_effective: [...], no_effect: [...]}
representing the offensive matchups for each type.
"""
def offensive_chart do
%{
"normal" => %{strong_against: [], not_effective: ["rock", "steel"], no_effect: ["ghost"]},
"fire" => %{
strong_against: ["grass", "ice", "bug", "steel"],
not_effective: ["fire", "water", "rock", "dragon"],
no_effect: []
},
"water" => %{
strong_against: ["fire", "ground", "rock"],
not_effective: ["water", "grass", "dragon"],
no_effect: []
},
"grass" => %{
strong_against: ["water", "ground", "rock"],
not_effective: ["fire", "grass", "poison", "flying", "bug", "dragon", "steel"],
no_effect: []
},
"electric" => %{
strong_against: ["water", "flying"],
not_effective: ["electric", "grass", "dragon"],
no_effect: ["ground"]
},
"ice" => %{
strong_against: ["grass", "ground", "flying", "dragon"],
not_effective: ["fire", "water", "ice", "steel"],
no_effect: []
},
"fighting" => %{
strong_against: ["normal", "ice", "rock", "dark", "steel"],
not_effective: ["poison", "flying", "psychic", "bug", "fairy"],
no_effect: ["ghost"]
},
"poison" => %{
strong_against: ["grass", "fairy"],
not_effective: ["poison", "ground", "rock", "ghost"],
no_effect: ["steel"]
},
"ground" => %{
strong_against: ["fire", "electric", "poison", "rock", "steel"],
not_effective: ["grass", "bug"],
no_effect: ["flying"]
},
"flying" => %{
strong_against: ["grass", "fighting", "bug"],
not_effective: ["electric", "rock", "steel"],
no_effect: []
},
"psychic" => %{
strong_against: ["fighting", "poison"],
not_effective: ["psychic", "steel"],
no_effect: ["dark"]
},
"bug" => %{
strong_against: ["grass", "psychic", "dark"],
not_effective: ["fire", "fighting", "poison", "flying", "ghost", "steel", "fairy"],
no_effect: []
},
"rock" => %{
strong_against: ["fire", "ice", "flying", "bug"],
not_effective: ["fighting", "ground", "steel"],
no_effect: []
},
"ghost" => %{
strong_against: ["psychic", "ghost"],
not_effective: ["dark"],
no_effect: ["normal"]
},
"dragon" => %{strong_against: ["dragon"], not_effective: ["steel"], no_effect: ["fairy"]},
"dark" => %{
strong_against: ["psychic", "ghost"],
not_effective: ["fighting", "dark", "fairy"],
no_effect: []
},
"steel" => %{
strong_against: ["ice", "rock", "fairy"],
not_effective: ["fire", "water", "electric", "steel"],
no_effect: []
},
"fairy" => %{
strong_against: ["fighting", "dragon", "dark"],
not_effective: ["fire", "poison", "steel"],
no_effect: []
}
}
end
@doc """
Returns the color class for a given type.
"""
def type_color("normal"), do: "bg-[#A8A77A]"
def type_color("fire"), do: "bg-[#EE8130]"
def type_color("water"), do: "bg-[#6390F0]"
def type_color("grass"), do: "bg-[#7AC74C]"
def type_color("electric"), do: "bg-[#F7D02C]"
def type_color("ice"), do: "bg-[#96D9D6]"
def type_color("fighting"), do: "bg-[#C22E28]"
def type_color("poison"), do: "bg-[#A33EA1]"
def type_color("ground"), do: "bg-[#E2BF65]"
def type_color("flying"), do: "bg-[#A98FF3]"
def type_color("psychic"), do: "bg-[#F95587]"
def type_color("bug"), do: "bg-[#A6B91A]"
def type_color("rock"), do: "bg-[#B6A136]"
def type_color("ghost"), do: "bg-[#735797]"
def type_color("dragon"), do: "bg-[#6F35FC]"
def type_color("dark"), do: "bg-[#705746]"
def type_color("steel"), do: "bg-[#B7B7CE]"
def type_color("fairy"), do: "bg-[#D685AD]"
def type_color(_), do: "bg-base-300"
@doc """
Returns the text color class for a given type.
"""
def type_text_color("normal"), do: "text-[#A8A77A]"
def type_text_color("fire"), do: "text-[#EE8130]"
def type_text_color("water"), do: "text-[#6390F0]"
def type_text_color("grass"), do: "text-[#7AC74C]"
def type_text_color("electric"), do: "text-[#F7D02C]"
def type_text_color("ice"), do: "text-[#96D9D6]"
def type_text_color("fighting"), do: "text-[#C22E28]"
def type_text_color("poison"), do: "text-[#A33EA1]"
def type_text_color("ground"), do: "text-[#E2BF65]"
def type_text_color("flying"), do: "text-[#A98FF3]"
def type_text_color("psychic"), do: "text-[#F95587]"
def type_text_color("bug"), do: "text-[#A6B91A]"
def type_text_color("rock"), do: "text-[#B6A136]"
def type_text_color("ghost"), do: "text-[#735797]"
def type_text_color("dragon"), do: "text-[#6F35FC]"
def type_text_color("dark"), do: "text-[#705746]"
def type_text_color("steel"), do: "text-[#B7B7CE]"
def type_text_color("fairy"), do: "text-[#D685AD]"
def type_text_color(_), do: "text-base-content/50"
@doc """
Returns a heroicon name for each type.
"""
def type_icon("normal"), do: "hero-minus-circle"
def type_icon("fire"), do: "hero-fire"
def type_icon("water"), do: "hero-beaker"
def type_icon("grass"), do: "hero-puzzle-piece"
def type_icon("electric"), do: "hero-bolt"
def type_icon("ice"), do: "hero-cloud"
def type_icon("fighting"), do: "hero-hand-raised"
def type_icon("poison"), do: "hero-beaker"
def type_icon("ground"), do: "hero-globe-americas"
def type_icon("flying"), do: "hero-paper-airplane"
def type_icon("psychic"), do: "hero-eye"
def type_icon("bug"), do: "hero-bug-ant"
def type_icon("rock"), do: "hero-cube"
def type_icon("ghost"), do: "hero-moon"
def type_icon("dragon"), do: "hero-star"
def type_icon("dark"), do: "hero-moon"
def type_icon("steel"), do: "hero-shield-check"
def type_icon("fairy"), do: "hero-sparkles"
def type_icon(_), do: "hero-question-mark-circle"
end