All checks were successful
Build and Deploy / Build & Push Image (push) Successful in 36s
618 lines
22 KiB
Elixir
618 lines
22 KiB
Elixir
defmodule CobblemonUiWeb.PokemonComponents do
|
||
use CobblemonUiWeb, :html
|
||
|
||
attr :pokemon, :map, required: true
|
||
attr :index, :integer, required: true
|
||
attr :compact, :boolean, default: false
|
||
attr :tier, :string, default: nil
|
||
|
||
def pokemon_card(%{pokemon: nil} = assigns) do
|
||
~H"""
|
||
<div class={[
|
||
"rounded-lg border border-base-300/20 bg-base-200/10 flex items-center justify-center",
|
||
if(@compact, do: "h-16", else: "h-24")
|
||
]}>
|
||
<span class="text-base-content/15 text-xs">Empty</span>
|
||
</div>
|
||
"""
|
||
end
|
||
|
||
def pokemon_card(assigns) do
|
||
~H"""
|
||
<div
|
||
phx-click="select_pokemon"
|
||
phx-value-index={@index}
|
||
class={[
|
||
"group rounded-lg border border-base-300/30 bg-base-200/20 hover:bg-base-200/40",
|
||
"hover:border-primary/30 transition-all duration-150 text-left cursor-pointer",
|
||
"hover:shadow-md hover:shadow-primary/5",
|
||
if(@compact, do: "p-2.5", else: "p-3.5")
|
||
]}
|
||
>
|
||
<div class="flex items-start justify-between">
|
||
<div class="min-w-0">
|
||
<p class={[
|
||
"font-semibold text-base-content/90 capitalize truncate group-hover:text-primary transition-colors",
|
||
if(@compact, do: "text-xs", else: "text-sm")
|
||
]}>
|
||
{@pokemon.species || "Unknown"}
|
||
</p>
|
||
<p class={[
|
||
"text-base-content/40 mt-0.5",
|
||
if(@compact, do: "text-[10px]", else: "text-xs")
|
||
]}>
|
||
Lv. {@pokemon.level || "?"}
|
||
</p>
|
||
</div>
|
||
<div class="flex items-center gap-1 shrink-0">
|
||
<.tier_badge :if={@tier} tier={@tier} species={@pokemon.species} compact={@compact} />
|
||
<div
|
||
:if={!@tier}
|
||
title="Tier data not downloaded"
|
||
class={[
|
||
"inline-flex items-center justify-center font-black rounded leading-none shrink-0",
|
||
"bg-base-300/20 text-base-content/25 ring-1 ring-base-300/30",
|
||
if(@compact, do: "text-[9px] w-4 h-4", else: "text-[10px] w-5 h-5")
|
||
]}
|
||
>
|
||
?
|
||
</div>
|
||
<div :if={@pokemon.shiny} title="Shiny">
|
||
<.icon
|
||
name="hero-sparkles"
|
||
class={[
|
||
"text-warning",
|
||
if(@compact, do: "size-3", else: "size-4")
|
||
]}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div :if={!@compact} class="flex items-center gap-2 mt-2">
|
||
<span
|
||
:if={@pokemon.nature}
|
||
class="inline-block text-[10px] px-1.5 py-0.5 rounded bg-base-300/40 text-base-content/50 capitalize"
|
||
>
|
||
{@pokemon.nature}
|
||
</span>
|
||
<span
|
||
:if={@pokemon.gender}
|
||
class={[
|
||
"text-[10px] font-bold",
|
||
gender_color(@pokemon.gender)
|
||
]}
|
||
>
|
||
{gender_symbol(@pokemon.gender)}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
"""
|
||
end
|
||
|
||
attr :pokemon, :map, required: true
|
||
attr :index, :integer, required: true
|
||
attr :tier, :string, default: nil
|
||
attr :types, :list, default: []
|
||
attr :evo_tiers, :list, default: []
|
||
|
||
def pc_pokemon_card(assigns) do
|
||
~H"""
|
||
<div
|
||
phx-click="select_pokemon"
|
||
phx-value-index={@index}
|
||
class="group rounded-lg border border-base-300/30 bg-base-200/20 hover:bg-base-200/40 hover:border-primary/30 transition-all duration-150 cursor-pointer hover:shadow-md hover:shadow-primary/5 p-3"
|
||
>
|
||
<div class="flex items-start gap-2.5">
|
||
<%!-- Sprite --%>
|
||
<div class="w-12 h-12 rounded-lg bg-base-300/15 flex items-center justify-center shrink-0 overflow-hidden">
|
||
<img
|
||
src={"https://img.rankedboost.com/wp-content/plugins/k-Pokemon/assets/sprites-official/#{String.downcase(@pokemon.species || "")}.png"}
|
||
alt={@pokemon.species}
|
||
class="w-10 h-10 object-contain drop-shadow-sm"
|
||
/>
|
||
</div>
|
||
<%!-- Info --%>
|
||
<div class="flex-1 min-w-0">
|
||
<div class="flex items-center gap-1.5">
|
||
<span class="text-xs font-semibold text-base-content/90 capitalize truncate group-hover:text-primary transition-colors">
|
||
{@pokemon.species || "Unknown"}
|
||
</span>
|
||
<.tier_badge :if={@tier} tier={@tier} species={@pokemon.species} compact={true} />
|
||
</div>
|
||
<p class="text-[10px] text-base-content/40 mt-0.5">
|
||
Lv. {@pokemon.level || "?"}
|
||
</p>
|
||
<%!-- Types --%>
|
||
<div :if={@types != []} class="flex flex-wrap gap-1 mt-1.5">
|
||
<span
|
||
:for={type <- @types}
|
||
class={[
|
||
"inline-block text-[9px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded leading-none text-white/90",
|
||
CobblemonUi.TypeChart.type_color(type)
|
||
]}
|
||
>
|
||
{type}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<%!-- Evolution tiers --%>
|
||
<div :if={@evo_tiers != []} class="mt-2 pt-2 border-t border-base-content/5">
|
||
<div class="flex items-center gap-1 flex-wrap">
|
||
<span class="text-[9px] text-base-content/25 uppercase tracking-wider font-medium mr-0.5">
|
||
Evo
|
||
</span>
|
||
<%= for evo <- @evo_tiers do %>
|
||
<div class="flex items-center gap-1 rounded bg-base-300/15 px-1.5 py-0.5">
|
||
<img
|
||
src={"https://img.rankedboost.com/wp-content/plugins/k-Pokemon/assets/sprites-official/#{evo.species}.png"}
|
||
alt={evo.species}
|
||
class="w-4 h-4 object-contain opacity-70"
|
||
/>
|
||
<span class="text-[9px] text-base-content/40 capitalize">{evo.species}</span>
|
||
<.tier_badge :if={evo.tier} tier={evo.tier} species={evo.species} compact={true} />
|
||
</div>
|
||
<% end %>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
"""
|
||
end
|
||
|
||
attr :tier, :string, required: true
|
||
attr :species, :string, required: true
|
||
attr :compact, :boolean, default: false
|
||
|
||
def tier_badge(assigns) do
|
||
~H"""
|
||
<a
|
||
href={"https://rankedboost.com/pokemon/#{String.downcase(@species || "")}/"}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
class={[
|
||
"inline-flex items-center justify-center font-black rounded leading-none shrink-0 hover:scale-110 transition-transform",
|
||
if(@compact, do: "text-[9px] w-4 h-4", else: "text-[10px] w-5 h-5"),
|
||
case @tier do
|
||
"S" -> "bg-red-500/20 text-red-400 ring-1 ring-red-500/30 hover:bg-red-500/30"
|
||
"A" -> "bg-orange-500/20 text-orange-400 ring-1 ring-orange-500/30 hover:bg-orange-500/30"
|
||
"B" -> "bg-yellow-500/20 text-yellow-400 ring-1 ring-yellow-500/30 hover:bg-yellow-500/30"
|
||
"C" -> "bg-green-500/20 text-green-400 ring-1 ring-green-500/30 hover:bg-green-500/30"
|
||
"D" -> "bg-blue-500/20 text-blue-400 ring-1 ring-blue-500/30 hover:bg-blue-500/30"
|
||
_ -> "bg-base-300/30 text-base-content/40 ring-1 ring-base-300/40"
|
||
end
|
||
]}
|
||
>
|
||
{@tier}
|
||
</a>
|
||
"""
|
||
end
|
||
|
||
attr :pokemon, :map, required: true
|
||
attr :tier, :string, default: nil
|
||
|
||
def pokemon_detail(assigns) do
|
||
~H"""
|
||
<div class="mt-6 rounded-xl border border-base-300/40 bg-base-200/30 overflow-hidden">
|
||
<%!-- Detail header --%>
|
||
<div class="px-5 py-4 border-b border-base-300/30 flex items-center justify-between">
|
||
<div class="flex items-center gap-3">
|
||
<div class="w-16 h-16 rounded-lg bg-base-300/20 flex items-center justify-center shrink-0 overflow-hidden">
|
||
<img
|
||
src={"https://img.rankedboost.com/wp-content/plugins/k-Pokemon/assets/sprites-official/#{String.downcase(@pokemon.species || "")}.png"}
|
||
alt={@pokemon.species}
|
||
class="w-14 h-14 object-contain drop-shadow-sm"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<h3 class="font-bold text-base-content capitalize text-lg flex items-center gap-2">
|
||
{@pokemon.species || "Unknown"}
|
||
<.tier_badge :if={@tier} tier={@tier} species={@pokemon.species} />
|
||
<span
|
||
:if={!@tier}
|
||
class="inline-flex items-center gap-1 text-xs text-base-content/30 font-normal italic"
|
||
title="Tier data not downloaded"
|
||
>
|
||
<.icon name="hero-arrow-down-tray" class="size-3" /> tier unavailable
|
||
</span>
|
||
<span :if={@pokemon.shiny} class="text-warning text-sm" title="Shiny">★</span>
|
||
</h3>
|
||
<p class="text-xs text-base-content/40">
|
||
Level {@pokemon.level || "?"} · {String.capitalize(@pokemon.form || "default")} form
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<button
|
||
id="close-pokemon-detail"
|
||
phx-click="close_pokemon"
|
||
class="btn btn-ghost btn-sm btn-circle hover:bg-base-300/50"
|
||
>
|
||
<.icon name="hero-x-mark" class="size-4" />
|
||
</button>
|
||
</div>
|
||
|
||
<div class="p-5 grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
<%!-- Info Column --%>
|
||
<div class="space-y-4">
|
||
<div>
|
||
<h4 class="text-xs font-semibold text-base-content/40 uppercase tracking-wider mb-2">
|
||
Details
|
||
</h4>
|
||
<div class="grid grid-cols-2 gap-2">
|
||
<.stat_pill label="Nature" value={@pokemon.nature} />
|
||
<.stat_pill label="Ability" value={@pokemon.ability} />
|
||
<.stat_pill label="Gender" value={@pokemon.gender} />
|
||
<.stat_pill label="Friendship" value={@pokemon.friendship} />
|
||
</div>
|
||
</div>
|
||
|
||
<%!-- Moves --%>
|
||
<div>
|
||
<h4 class="text-xs font-semibold text-base-content/40 uppercase tracking-wider mb-2">
|
||
Moves
|
||
</h4>
|
||
<div class="space-y-1.5">
|
||
<div
|
||
:for={move <- @pokemon.moves || []}
|
||
class="px-3 py-1.5 rounded-md bg-base-300/20 border border-base-300/20 text-sm text-base-content/70 capitalize"
|
||
>
|
||
{format_move(move)}
|
||
</div>
|
||
<div :if={(@pokemon.moves || []) == []} class="text-xs text-base-content/30 italic">
|
||
No moves
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<%!-- Stats Column --%>
|
||
<div class="space-y-4">
|
||
<%!-- IVs --%>
|
||
<div :if={@pokemon.ivs}>
|
||
<h4 class="text-xs font-semibold text-base-content/40 uppercase tracking-wider mb-2">
|
||
IVs
|
||
</h4>
|
||
<div class="space-y-1.5">
|
||
<.stat_bar
|
||
:for={{stat, val} <- stat_list(@pokemon.ivs)}
|
||
label={format_stat(stat)}
|
||
value={val}
|
||
max={31}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<%!-- EVs --%>
|
||
<div :if={@pokemon.evs}>
|
||
<h4 class="text-xs font-semibold text-base-content/40 uppercase tracking-wider mb-2">
|
||
EVs
|
||
<span class="text-base-content/25 font-normal ml-1">
|
||
({ev_total(@pokemon.evs)}/510)
|
||
</span>
|
||
</h4>
|
||
<div class="space-y-1.5">
|
||
<.stat_bar
|
||
:for={{stat, val} <- stat_list(@pokemon.evs)}
|
||
label={format_stat(stat)}
|
||
value={val}
|
||
max={252}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
"""
|
||
end
|
||
|
||
attr :label, :string, required: true
|
||
attr :value, :any, required: true
|
||
|
||
def stat_pill(assigns) do
|
||
~H"""
|
||
<div class="px-3 py-2 rounded-lg bg-base-300/15 border border-base-300/15">
|
||
<p class="text-[10px] text-base-content/35 uppercase tracking-wider">{@label}</p>
|
||
<p class="text-sm text-base-content/80 capitalize mt-0.5">{@value || "—"}</p>
|
||
</div>
|
||
"""
|
||
end
|
||
|
||
attr :label, :string, required: true
|
||
attr :value, :integer, required: true
|
||
attr :max, :integer, required: true
|
||
|
||
def stat_bar(assigns) do
|
||
pct = if assigns.max > 0, do: min(assigns.value / assigns.max * 100, 100), else: 0
|
||
|
||
color =
|
||
cond do
|
||
pct >= 90 -> "bg-success"
|
||
pct >= 60 -> "bg-info"
|
||
pct >= 30 -> "bg-warning"
|
||
true -> "bg-error/70"
|
||
end
|
||
|
||
assigns = assign(assigns, pct: pct, color: color)
|
||
|
||
~H"""
|
||
<div class="flex items-center gap-2">
|
||
<span class="text-[10px] text-base-content/40 w-10 uppercase tracking-wider shrink-0">
|
||
{@label}
|
||
</span>
|
||
<div class="flex-1 h-2 rounded-full bg-base-300/30 overflow-hidden">
|
||
<div
|
||
class={[@color, "h-full rounded-full transition-all duration-500"]}
|
||
style={"width: #{@pct}%"}
|
||
/>
|
||
</div>
|
||
<span class="text-xs text-base-content/50 w-8 text-right tabular-nums font-mono">
|
||
{@value}
|
||
</span>
|
||
</div>
|
||
"""
|
||
end
|
||
|
||
# --- Type Chart Component ---
|
||
|
||
alias CobblemonUi.TypeChart
|
||
|
||
attr :selected_type, :string, default: nil
|
||
|
||
def type_chart(assigns) do
|
||
chart = TypeChart.defensive_chart()
|
||
offense = TypeChart.offensive_chart()
|
||
types = TypeChart.types()
|
||
|
||
assigns = assign(assigns, chart: chart, offense: offense, types: types)
|
||
|
||
~H"""
|
||
<div class="mt-8 rounded-xl border border-base-300/40 bg-base-200/30 overflow-hidden">
|
||
<div class="px-5 py-4 border-b border-base-300/30 flex items-center gap-3">
|
||
<div class="w-10 h-10 rounded-lg bg-primary/15 flex items-center justify-center shrink-0">
|
||
<.icon name="hero-table-cells" class="size-5 text-primary/80" />
|
||
</div>
|
||
<div>
|
||
<h3 class="font-bold text-base-content text-lg">Type Chart</h3>
|
||
<p class="text-xs text-base-content/40">
|
||
Tap a type to see its matchups
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<%!-- Type picker grid --%>
|
||
<div class="px-5 pt-4 pb-2">
|
||
<div class="flex flex-wrap gap-1.5">
|
||
<button
|
||
:for={t <- @types}
|
||
phx-click="select_type"
|
||
phx-value-type={t}
|
||
class={[
|
||
"inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs font-semibold capitalize transition-all duration-150 border cursor-pointer",
|
||
if(@selected_type == t,
|
||
do: "ring-2 ring-primary/60 border-primary/40 bg-base-100 shadow-md scale-105",
|
||
else:
|
||
"border-base-300/30 bg-base-200/40 hover:bg-base-200/70 hover:border-base-300/50 hover:shadow-sm"
|
||
)
|
||
]}
|
||
>
|
||
<span class={[
|
||
"inline-flex items-center justify-center w-5 h-5 rounded-full shrink-0 p-0.5",
|
||
TypeChart.type_color(t)
|
||
]}>
|
||
<img src={TypeChart.type_icon_path(t)} alt={t} class="w-3.5 h-3.5" />
|
||
</span>
|
||
{t}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<%!-- Selected type detail --%>
|
||
<%= if @selected_type do %>
|
||
<div class="px-5 py-4 animate-in fade-in duration-200">
|
||
<div class="flex items-center gap-3 mb-5">
|
||
<div class={[
|
||
"w-12 h-12 rounded-xl flex items-center justify-center shrink-0 shadow-sm",
|
||
TypeChart.type_color(@selected_type)
|
||
]}>
|
||
<img
|
||
src={TypeChart.type_icon_path(@selected_type)}
|
||
alt={@selected_type}
|
||
class="w-7 h-7 drop-shadow"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<h4 class="font-bold text-base-content capitalize text-xl">{@selected_type}</h4>
|
||
<p class="text-xs text-base-content/40">Defensive & Offensive matchups</p>
|
||
</div>
|
||
<button
|
||
phx-click="select_type"
|
||
phx-value-type=""
|
||
class="ml-auto btn btn-ghost btn-sm btn-circle hover:bg-base-300/50"
|
||
>
|
||
<.icon name="hero-x-mark" class="size-4" />
|
||
</button>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||
<%!-- Defensive --%>
|
||
<div class="space-y-4">
|
||
<h5 class="text-xs font-semibold text-base-content/40 uppercase tracking-wider flex items-center gap-1.5">
|
||
<.icon name="hero-shield-check" class="size-3.5" /> Defensive
|
||
</h5>
|
||
|
||
<%!-- Weak to --%>
|
||
<div>
|
||
<p class="text-[11px] text-error/80 font-semibold uppercase tracking-wider mb-1.5 flex items-center gap-1">
|
||
<.icon name="hero-exclamation-triangle" class="size-3" /> Weak to (2×)
|
||
</p>
|
||
<div class="flex flex-wrap gap-1.5">
|
||
<.type_pill
|
||
:for={t <- @chart[@selected_type].weak_to}
|
||
type_name={t}
|
||
/>
|
||
<span
|
||
:if={@chart[@selected_type].weak_to == []}
|
||
class="text-xs text-base-content/25 italic"
|
||
>
|
||
None
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<%!-- Resists --%>
|
||
<div>
|
||
<p class="text-[11px] text-success/80 font-semibold uppercase tracking-wider mb-1.5 flex items-center gap-1">
|
||
<.icon name="hero-shield-check" class="size-3" /> Resists (½×)
|
||
</p>
|
||
<div class="flex flex-wrap gap-1.5">
|
||
<.type_pill
|
||
:for={t <- @chart[@selected_type].resists}
|
||
type_name={t}
|
||
/>
|
||
<span
|
||
:if={@chart[@selected_type].resists == []}
|
||
class="text-xs text-base-content/25 italic"
|
||
>
|
||
None
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<%!-- Immune to --%>
|
||
<div :if={@chart[@selected_type].immune_to != []}>
|
||
<p class="text-[11px] text-info/80 font-semibold uppercase tracking-wider mb-1.5 flex items-center gap-1">
|
||
<.icon name="hero-no-symbol" class="size-3" /> Immune to (0×)
|
||
</p>
|
||
<div class="flex flex-wrap gap-1.5">
|
||
<.type_pill
|
||
:for={t <- @chart[@selected_type].immune_to}
|
||
type_name={t}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<%!-- Offensive --%>
|
||
<div class="space-y-4">
|
||
<h5 class="text-xs font-semibold text-base-content/40 uppercase tracking-wider flex items-center gap-1.5">
|
||
<.icon name="hero-bolt" class="size-3.5" /> Offensive
|
||
</h5>
|
||
|
||
<%!-- Super effective --%>
|
||
<div>
|
||
<p class="text-[11px] text-error/80 font-semibold uppercase tracking-wider mb-1.5 flex items-center gap-1">
|
||
<.icon name="hero-bolt" class="size-3" /> Super effective (2×)
|
||
</p>
|
||
<div class="flex flex-wrap gap-1.5">
|
||
<.type_pill
|
||
:for={t <- @offense[@selected_type].strong_against}
|
||
type_name={t}
|
||
/>
|
||
<span
|
||
:if={@offense[@selected_type].strong_against == []}
|
||
class="text-xs text-base-content/25 italic"
|
||
>
|
||
None
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<%!-- Not very effective --%>
|
||
<div>
|
||
<p class="text-[11px] text-warning/80 font-semibold uppercase tracking-wider mb-1.5 flex items-center gap-1">
|
||
<.icon name="hero-shield-exclamation" class="size-3" /> Not very effective (½×)
|
||
</p>
|
||
<div class="flex flex-wrap gap-1.5">
|
||
<.type_pill
|
||
:for={t <- @offense[@selected_type].not_effective}
|
||
type_name={t}
|
||
/>
|
||
<span
|
||
:if={@offense[@selected_type].not_effective == []}
|
||
class="text-xs text-base-content/25 italic"
|
||
>
|
||
None
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<%!-- No effect --%>
|
||
<div :if={@offense[@selected_type].no_effect != []}>
|
||
<p class="text-[11px] text-base-content/40 font-semibold uppercase tracking-wider mb-1.5 flex items-center gap-1">
|
||
<.icon name="hero-no-symbol" class="size-3" /> No effect (0×)
|
||
</p>
|
||
<div class="flex flex-wrap gap-1.5">
|
||
<.type_pill
|
||
:for={t <- @offense[@selected_type].no_effect}
|
||
type_name={t}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<% else %>
|
||
<%!-- Compact overview when no type selected --%>
|
||
<div class="px-5 pb-4">
|
||
<p class="text-xs text-base-content/30 italic">Select a type above to view matchups</p>
|
||
</div>
|
||
<% end %>
|
||
</div>
|
||
"""
|
||
end
|
||
|
||
attr :type_name, :string, required: true
|
||
|
||
def type_pill(assigns) do
|
||
~H"""
|
||
<button
|
||
phx-click="select_type"
|
||
phx-value-type={@type_name}
|
||
class={[
|
||
"inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-semibold capitalize",
|
||
"border transition-all duration-150 cursor-pointer hover:scale-105 hover:shadow-sm",
|
||
TypeChart.type_color(@type_name),
|
||
"text-white border-white/10"
|
||
]}
|
||
>
|
||
<img
|
||
src={TypeChart.type_icon_path(@type_name)}
|
||
alt={@type_name}
|
||
class="w-3.5 h-3.5 drop-shadow"
|
||
/>
|
||
{@type_name}
|
||
</button>
|
||
"""
|
||
end
|
||
|
||
# --- Helpers ---
|
||
|
||
defp gender_symbol("male"), do: "♂"
|
||
defp gender_symbol("female"), do: "♀"
|
||
defp gender_symbol(_), do: "—"
|
||
|
||
defp gender_color("male"), do: "text-info"
|
||
defp gender_color("female"), do: "text-error"
|
||
defp gender_color(_), do: "text-base-content/40"
|
||
|
||
defp format_move(move) when is_binary(move), do: String.replace(move, "_", " ")
|
||
defp format_move(_), do: "—"
|
||
|
||
defp format_stat(:hp), do: "HP"
|
||
defp format_stat(:attack), do: "ATK"
|
||
defp format_stat(:defense), do: "DEF"
|
||
defp format_stat(:special_attack), do: "SPA"
|
||
defp format_stat(:special_defense), do: "SPD"
|
||
defp format_stat(:speed), do: "SPE"
|
||
defp format_stat(other), do: to_string(other)
|
||
|
||
defp stat_list(stats) when is_map(stats) do
|
||
[:hp, :attack, :defense, :special_attack, :special_defense, :speed]
|
||
|> Enum.map(fn key -> {key, Map.get(stats, key, 0)} end)
|
||
end
|
||
|
||
defp stat_list(_), do: []
|
||
|
||
defp ev_total(evs) when is_map(evs), do: evs |> Map.values() |> Enum.sum()
|
||
defp ev_total(_), do: 0
|
||
end
|