From f69f287ece10c2efbc87a0d2e0b8633a00c95771 Mon Sep 17 00:00:00 2001 From: Alex Mickelson Date: Mon, 16 Mar 2026 20:06:26 -0600 Subject: [PATCH] tier list --- k8s/deployment.yaml | 8 ++ lib/cobblemon_ui/tier_list_scraper.ex | 65 +++++++++++++++ lib/cobblemon_ui_web/live/dashboard_live.ex | 90 +++++++++++++++++---- mix.exs | 1 + mix.lock | 1 + 5 files changed, 150 insertions(+), 15 deletions(-) create mode 100644 lib/cobblemon_ui/tier_list_scraper.ex diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml index cf9218c..b7bf638 100644 --- a/k8s/deployment.yaml +++ b/k8s/deployment.yaml @@ -30,6 +30,8 @@ spec: value: "4000" - name: MINECRAFT_SERVER_URL value: "http://minecraft-svc:8085" + - name: CACHE_DIR + value: "/app/cache" - name: SECRET_KEY_BASE valueFrom: secretKeyRef: @@ -39,6 +41,8 @@ spec: - name: cobblemon-data mountPath: /cobblemon-data readOnly: true + - name: cache + mountPath: /app/cache resources: requests: cpu: 100m @@ -65,3 +69,7 @@ spec: hostPath: path: /data/minecraft/cobblemon-data type: Directory + - name: cache + hostPath: + path: /data/minecraft/cobblemon-ui-cache + type: DirectoryOrCreate diff --git a/lib/cobblemon_ui/tier_list_scraper.ex b/lib/cobblemon_ui/tier_list_scraper.ex new file mode 100644 index 0000000..305b0c7 --- /dev/null +++ b/lib/cobblemon_ui/tier_list_scraper.ex @@ -0,0 +1,65 @@ +defmodule CobblemonUi.TierListScraper do + @url "https://rankedboost.com/pokemon/tier-list/" + @filename "pokemon_tier_list.json" + + def output_file do + dir = System.get_env("CACHE_DIR", ".") + Path.join(dir, @filename) + end + + def run do + with {:ok, html} <- fetch_page(), + pokemon <- parse(html), + :ok <- write_json(pokemon) do + {:ok, pokemon} + end + end + + def fetch_page do + case Req.get(@url) do + {:ok, %Req.Response{status: 200, body: body}} -> {:ok, body} + {:ok, %Req.Response{status: status}} -> {:error, {:http_error, status}} + {:error, err} -> {:error, err} + end + end + + def parse(html) do + html + |> Floki.parse_document!() + |> Floki.find(".pokemon-tier") + |> Enum.map(&extract_pokemon/1) + end + + defp extract_pokemon(node) do + name = + node + |> Floki.find(".name") + |> Floki.text() + |> String.trim() + + tier = + node + |> Floki.find(".tier") + |> Floki.text() + |> String.trim() + + %{name: name, tier: tier} + end + + def write_json(data) do + json = Jason.encode!(data, pretty: true) + File.write(output_file(), json) + end + + # Returns a map of %{lowercase_name => tier} for fast lookup, or an empty map if unavailable. + def load_tier_list do + with {:ok, contents} <- File.read(output_file()), + {:ok, entries} <- Jason.decode(contents) do + Map.new(entries, fn %{"name" => name, "tier" => tier} -> + {String.downcase(name), tier} + end) + else + _ -> %{} + end + end +end diff --git a/lib/cobblemon_ui_web/live/dashboard_live.ex b/lib/cobblemon_ui_web/live/dashboard_live.ex index 6f9b0ee..176bd86 100644 --- a/lib/cobblemon_ui_web/live/dashboard_live.ex +++ b/lib/cobblemon_ui_web/live/dashboard_live.ex @@ -3,7 +3,10 @@ defmodule CobblemonUiWeb.DashboardLive do @impl true def mount(_params, _session, socket) do - if connected?(socket), do: :timer.send_interval(1000, self(), :tick) + if connected?(socket) do + :timer.send_interval(1000, self(), :tick) + unless File.exists?(CobblemonUi.TierListScraper.output_file()), do: send(self(), :scrape_tier_list) + end players = case CobblemonUi.CobblemonFS.list_players() do @@ -18,6 +21,7 @@ defmodule CobblemonUiWeb.DashboardLive do player_data: nil, battle: nil, selected_pokemon: nil, + tier_list: CobblemonUi.TierListScraper.load_tier_list(), view_mode: :party, loading: false, error: nil @@ -96,6 +100,19 @@ defmodule CobblemonUiWeb.DashboardLive do {:noreply, do_refresh(socket)} end + def handle_info(:scrape_tier_list, socket) do + lv = self() + Task.start(fn -> + CobblemonUi.TierListScraper.run() + send(lv, :reload_tier_list) + end) + {:noreply, socket} + end + + def handle_info(:reload_tier_list, socket) do + {:noreply, assign(socket, tier_list: CobblemonUi.TierListScraper.load_tier_list())} + end + defp do_refresh(socket) do players = case CobblemonUi.CobblemonFS.list_players() do @@ -262,7 +279,11 @@ defmodule CobblemonUiWeb.DashboardLive do
<%= for {pokemon, idx} <- Enum.with_index(@player_data.party) do %> - <.pokemon_card pokemon={pokemon} index={idx} /> + <.pokemon_card + pokemon={pokemon} + index={idx} + tier={Map.get(@tier_list, String.downcase(pokemon.species || ""), nil)} + /> <% end %>
@@ -278,6 +299,7 @@ defmodule CobblemonUiWeb.DashboardLive do <.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 /> <% end %> @@ -292,7 +314,11 @@ defmodule CobblemonUiWeb.DashboardLive do <%!-- Pokémon detail panel --%> - <.pokemon_detail :if={@selected_pokemon} pokemon={@selected_pokemon} /> + <.pokemon_detail + :if={@selected_pokemon} + pokemon={@selected_pokemon} + tier={Map.get(@tier_list, String.downcase(@selected_pokemon.species || ""), nil)} + /> @@ -307,6 +333,7 @@ defmodule CobblemonUiWeb.DashboardLive do attr :pokemon, :map, required: true attr :index, :integer, required: true attr :compact, :boolean, default: false + attr :tier, :string, default: nil defp pokemon_card(%{pokemon: nil} = assigns) do ~H""" @@ -321,7 +348,7 @@ defmodule CobblemonUiWeb.DashboardLive do defp pokemon_card(assigns) do ~H""" - + + """ + end + + attr :tier, :string, required: true + attr :species, :string, required: true + attr :compact, :boolean, default: false + + defp tier_badge(assigns) do + ~H""" + "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} + """ end attr :pokemon, :map, required: true + attr :tier, :string, default: nil defp pokemon_detail(assigns) do ~H""" @@ -389,11 +448,12 @@ defmodule CobblemonUiWeb.DashboardLive do <.icon name="hero-bolt" class="size-4 text-primary/70" />
-

+

{@pokemon.species || "Unknown"} + <.tier_badge :if={@tier} tier={@tier} species={@pokemon.species} /> ★ diff --git a/mix.exs b/mix.exs index 4bb58ad..48df182 100644 --- a/mix.exs +++ b/mix.exs @@ -60,6 +60,7 @@ defmodule CobblemonUi.MixProject do {:telemetry_poller, "~> 1.0"}, {:gettext, "~> 1.0"}, {:jason, "~> 1.2"}, + {:floki, "~> 0.35"}, {:dns_cluster, "~> 0.2.0"}, {:bandit, "~> 1.5"} ] diff --git a/mix.lock b/mix.lock index a490221..03fd108 100644 --- a/mix.lock +++ b/mix.lock @@ -8,6 +8,7 @@ "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, "finch": {:hex, :finch, "0.21.0", "b1c3b2d48af02d0c66d2a9ebfb5622be5c5ecd62937cf79a88a7f98d48a8290c", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "87dc6e169794cb2570f75841a19da99cfde834249568f2a5b121b809588a4377"}, "fine": {:hex, :fine, "0.1.4", "b19a89c1476c7c57afb5f9314aed5960b5bc95d5277de4cb5ee8e1d1616ce379", [:mix], [], "hexpm", "be3324cc454a42d80951cf6023b9954e9ff27c6daa255483b3e8d608670303f5"}, + "floki": {:hex, :floki, "0.38.0", "62b642386fa3f2f90713f6e231da0fa3256e41ef1089f83b6ceac7a3fd3abf33", [:mix], [], "hexpm", "a5943ee91e93fb2d635b612caf5508e36d37548e84928463ef9dd986f0d1abd9"}, "gettext": {:hex, :gettext, "1.0.2", "5457e1fd3f4abe47b0e13ff85086aabae760497a3497909b8473e0acee57673b", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "eab805501886802071ad290714515c8c4a17196ea76e5afc9d06ca85fb1bfeb3"}, "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "0435d4ca364a608cc75e2f8683d374e55abbae26", [tag: "v2.2.0", sparse: "optimized", depth: 1]}, "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},