This commit is contained in:
@@ -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
|
||||
]
|
||||
|
||||
89
lib/cobblemon_ui/poke_api.ex
Normal file
89
lib/cobblemon_ui/poke_api.ex
Normal 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
|
||||
272
lib/cobblemon_ui/type_chart.ex
Normal file
272
lib/cobblemon_ui/type_chart.ex
Normal 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
|
||||
Reference in New Issue
Block a user