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

@@ -24,9 +24,11 @@ defmodule CobblemonUiWeb.DashboardLive do
battle: nil,
selected_pokemon: nil,
tier_list: CobblemonUi.TierListScraper.get_tier_list(),
species_info: %{},
view_mode: :party,
loading: false,
error: nil
error: nil,
selected_type: nil
)}
end
@@ -36,14 +38,19 @@ defmodule CobblemonUiWeb.DashboardLive do
{:ok, data} ->
battle = find_player_battle(uuid)
{:noreply,
assign(socket,
selected_player: uuid,
player_data: data,
battle: battle,
selected_pokemon: nil,
error: nil
)}
socket =
assign(socket,
selected_player: uuid,
player_data: data,
battle: battle,
selected_pokemon: nil,
species_info: %{},
error: nil
)
if connected?(socket), do: send(self(), {:load_species_info, data})
{:noreply, socket}
{:error, :not_found} ->
{:noreply,
@@ -94,11 +101,24 @@ defmodule CobblemonUiWeb.DashboardLive do
{:noreply, assign(socket, view_mode: String.to_existing_atom(mode), selected_pokemon: nil)}
end
def handle_event("select_type", %{"type" => ""}, socket) do
{:noreply, assign(socket, selected_type: nil)}
end
def handle_event("select_type", %{"type" => type}, socket) do
{:noreply, assign(socket, selected_type: type)}
end
@impl true
def handle_info(:tick, socket) do
{:noreply, do_refresh(socket)}
end
def handle_info({:load_species_info, player_data}, socket) do
species_info = compute_species_info(player_data, socket.assigns.tier_list)
{:noreply, assign(socket, species_info: species_info)}
end
defp do_refresh(socket) do
players =
case CobblemonUi.CobblemonFS.list_players() do
@@ -276,13 +296,16 @@ defmodule CobblemonUiWeb.DashboardLive do
<h3 class="text-sm font-semibold text-base-content/50 uppercase tracking-wider mb-3">
Box {box.box + 1}
</h3>
<div class="grid grid-cols-3 sm:grid-cols-5 lg:grid-cols-6 gap-2">
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-2">
<%= for {pokemon, idx} <- Enum.with_index(box.pokemon), not is_nil(pokemon) do %>
<.pokemon_card
<% skey = String.downcase(pokemon.species || "") %>
<% info = Map.get(@species_info, skey, %{}) %>
<.pc_pokemon_card
pokemon={pokemon}
index={pc_global_index(@player_data.pc, box.box, idx)}
tier={Map.get(@tier_list, String.downcase(pokemon.species || ""), nil)}
compact
tier={Map.get(@tier_list, skey, nil)}
types={Map.get(info, :types, [])}
evo_tiers={Map.get(info, :evo_tiers, [])}
/>
<% end %>
</div>
@@ -301,6 +324,9 @@ defmodule CobblemonUiWeb.DashboardLive do
pokemon={@selected_pokemon}
tier={Map.get(@tier_list, String.downcase(@selected_pokemon.species || ""), nil)}
/>
<%!-- Type Chart --%>
<.type_chart selected_type={@selected_type} />
</div>
</div>
</div>
@@ -333,6 +359,38 @@ defmodule CobblemonUiWeb.DashboardLive do
end
end
defp compute_species_info(player_data, tier_list) do
all_pokemon =
(player_data.party ++ Enum.flat_map(player_data.pc, & &1.pokemon))
|> Enum.reject(&is_nil/1)
unique_species =
all_pokemon
|> Enum.map(&String.downcase(&1.species || ""))
|> Enum.reject(&(&1 == ""))
|> Enum.uniq()
unique_species
|> Task.async_stream(
fn species ->
types = CobblemonUi.PokeApi.get_types(species)
evolutions = CobblemonUi.EvolutionApi.get_evolutions(species)
evo_tiers =
Enum.map(evolutions, fn evo ->
%{species: evo, tier: Map.get(tier_list, evo)}
end)
{species, %{types: types, evo_tiers: evo_tiers}}
end,
max_concurrency: 10,
timeout: :infinity
)
|> Enum.reduce(%{}, fn {:ok, {species, info}}, acc ->
Map.put(acc, species, info)
end)
end
defp opponent_evolutions(nil, _player_id), do: %{}
defp opponent_evolutions(battle, player_id) do

View File

@@ -89,6 +89,76 @@ defmodule CobblemonUiWeb.PokemonComponents do
"""
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
@@ -281,6 +351,232 @@ defmodule CobblemonUiWeb.PokemonComponents do
"""
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 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-block w-2.5 h-2.5 rounded-full shrink-0",
type_bg(@selected_type == t, t)
]} />
{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)
]}>
<.icon name={TypeChart.type_icon(@selected_type)} class="size-6 text-white 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"
]}
>
<.icon name={TypeChart.type_icon(@type_name)} class="size-3 drop-shadow" />
{@type_name}
</button>
"""
end
defp type_bg(true, type), do: TypeChart.type_color(type)
defp type_bg(false, type), do: TypeChart.type_color(type) <> "/60"
# --- Helpers ---
defp gender_symbol("male"), do: ""