From 6c5e5acdc3b8c601e00c7db91d2f6c11176d662d Mon Sep 17 00:00:00 2001 From: Alex Mickelson Date: Mon, 16 Mar 2026 19:29:49 -0600 Subject: [PATCH] minecraft server --- config/runtime.exs | 3 + k8s/deployment.yaml | 2 + lib/cobblemon_ui/battles_api.ex | 74 ++++++ lib/cobblemon_ui_web/live/battles_live.ex | 243 ++++++++++++++++++++ lib/cobblemon_ui_web/live/dashboard_live.ex | 22 +- lib/cobblemon_ui_web/router.ex | 1 + 6 files changed, 338 insertions(+), 7 deletions(-) create mode 100644 lib/cobblemon_ui/battles_api.ex create mode 100644 lib/cobblemon_ui_web/live/battles_live.ex diff --git a/config/runtime.exs b/config/runtime.exs index 4ffa7b3..19d63bc 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -23,6 +23,9 @@ end config :cobblemon_ui, CobblemonUiWeb.Endpoint, http: [port: String.to_integer(System.get_env("PORT", "4000"))] +config :cobblemon_ui, + minecraft_server_url: System.get_env("MINECRAFT_SERVER_URL", "http://localhost:80") + if config_env() == :prod do # The secret key base is used to sign/encrypt cookies and other secrets. # A default value is used in config/dev.exs and config/test.exs but you diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml index f9a1115..747aad8 100644 --- a/k8s/deployment.yaml +++ b/k8s/deployment.yaml @@ -28,6 +28,8 @@ spec: value: "cobblemon.alexmickelson.guru" - name: PORT value: "4000" + - name: MINECRAFT_SERVER_URL + value: "http://minecraft.beefalo-newton.ts.net:8085" - name: SECRET_KEY_BASE valueFrom: secretKeyRef: diff --git a/lib/cobblemon_ui/battles_api.ex b/lib/cobblemon_ui/battles_api.ex new file mode 100644 index 0000000..dc50f7c --- /dev/null +++ b/lib/cobblemon_ui/battles_api.ex @@ -0,0 +1,74 @@ +defmodule CobblemonUi.BattlesApi do + @moduledoc """ + Fetches active battle data from the Cobblemon Minecraft server. + """ + + def server_url do + Application.get_env(:cobblemon_ui, :minecraft_server_url, "http://localhost:80") + end + + def list_battles do + url = "#{server_url()}/battles" + + case Req.get(url) do + {:ok, %Req.Response{status: 200, body: body}} -> + battles = parse_battles(body) + {:ok, battles} + + {:ok, %Req.Response{status: status}} -> + {:error, "Server returned status #{status}"} + + {:error, reason} -> + {:error, "Request failed: #{inspect(reason)}"} + end + end + + defp parse_battles(%{"battles" => battles}) when is_list(battles) do + Enum.map(battles, &parse_battle/1) + end + + defp parse_battles(_), do: [] + + defp parse_battle(battle) do + %{ + battle_id: battle["battleId"], + actors: Enum.map(battle["actors"] || [], &parse_actor/1) + } + end + + defp parse_actor(actor) do + %{ + name: actor["name"], + type: actor["type"], + player_id: actor["playerId"], + active_pokemon: Enum.map(actor["activePokemon"] || [], &parse_active_pokemon/1), + party: Enum.map(actor["party"] || [], &parse_party_pokemon/1) + } + end + + defp parse_active_pokemon(p) do + %{ + uuid: p["uuid"], + species: p["species"], + level: p["level"], + hp: p["hp"], + max_hp: p["maxHp"] + } + end + + defp parse_party_pokemon(p) do + %{ + uuid: p["uuid"], + species: p["species"], + level: p["level"], + hp: p["hp"], + max_hp: p["maxHp"], + ability: p["ability"], + nature: p["nature"], + shiny: p["shiny"], + moves: Enum.map(p["moves"] || [], fn m -> + %{name: m["name"], pp: m["pp"], max_pp: m["maxPp"]} + end) + } + end +end diff --git a/lib/cobblemon_ui_web/live/battles_live.ex b/lib/cobblemon_ui_web/live/battles_live.ex new file mode 100644 index 0000000..086c2d8 --- /dev/null +++ b/lib/cobblemon_ui_web/live/battles_live.ex @@ -0,0 +1,243 @@ +defmodule CobblemonUiWeb.BattlesLive do + use CobblemonUiWeb, :live_view + + @impl true + def mount(_params, _session, socket) do + {battles, error} = + case CobblemonUi.BattlesApi.list_battles() do + {:ok, battles} -> {battles, nil} + {:error, reason} -> {[], reason} + end + + {:ok, + assign(socket, + page_title: "Active Battles", + battles: battles, + error: error + )} + end + + @impl true + def handle_event("refresh", _params, socket) do + {battles, error} = + case CobblemonUi.BattlesApi.list_battles() do + {:ok, battles} -> {battles, nil} + {:error, reason} -> {[], reason} + end + + {:noreply, assign(socket, battles: battles, error: error)} + end + + @impl true + def render(assigns) do + ~H""" + +
+
+ <%!-- Header --%> +
+
+
+ <.icon name="hero-bolt" class="size-6 text-error" /> +
+
+

Active Battles

+

Live battle monitor

+
+
+
+ <.link + navigate={~p"/"} + class="btn btn-ghost btn-sm gap-2 hover:bg-base-300/50 transition-colors" + > + <.icon name="hero-arrow-left" class="size-4" /> Players + + +
+
+ + <%!-- Error --%> +
+
+ <.icon name="hero-exclamation-triangle" class="size-5" /> + {@error} +
+
+ + <%!-- Empty state --%> +
+ <.icon name="hero-shield-check" class="size-10 mx-auto mb-4 text-base-content/20" /> +

No active battles

+

All is peaceful on the server

+
+ + <%!-- Battle cards --%> +
+ <%= for battle <- @battles do %> +
+ <%!-- Battle header --%> +
+
+ + + Live + + {battle.battle_id} +
+ + {length(battle.actors)} combatants + +
+ + <%!-- Actors --%> +
+ <%!-- VS divider on md+ --%> + + + <%= for actor <- battle.actors do %> +
+ <%!-- Actor header --%> +
+
+ <%= if actor.type == "player" do %> + <.icon name="hero-user" class="size-4 text-primary" /> + <% else %> + <.icon name="hero-cpu-chip" class="size-4 text-warning" /> + <% end %> +
+
+

{actor.name}

+

+ {actor.type} +

+
+
+ + <%!-- Active Pokémon --%> +
+ <%= for poke <- actor.active_pokemon do %> +
+
+
+ {poke.species} + + Lv.{poke.level} + +
+ + {poke.hp}/{poke.max_hp} + +
+ <%!-- HP bar --%> +
+
"bg-base-content/20" + poke.hp / poke.max_hp > 0.5 -> "bg-success" + poke.hp / poke.max_hp > 0.2 -> "bg-warning" + true -> "bg-error" + end + ]} + style={"width: #{if poke.max_hp > 0, do: Float.round(poke.hp / poke.max_hp * 100, 1), else: 0}%"} + > +
+
+
+ <% end %> +
+ + <%!-- Party moves (player only) --%> + <%= if actor.party != [] do %> +
+ + <.icon name="hero-chevron-right" class="size-3 group-open:rotate-90 transition-transform" /> + Party details + +
+ <%= for party_poke <- actor.party do %> +
+
+
+ + {party_poke.species} + + <%= if party_poke.shiny do %> + ✦ Shiny + <% end %> +
+ + Lv.{party_poke.level} + +
+
+ + {party_poke.ability} + + + {party_poke.nature |> String.replace("cobblemon:", "")} + +
+
+ <%= for move <- party_poke.moves do %> + + {move.name} + {move.pp}/{move.max_pp} + + <% end %> +
+
+ <% end %> +
+
+ <% end %> +
+ <% end %> +
+
+ <% end %> +
+
+
+
+ """ + end +end diff --git a/lib/cobblemon_ui_web/live/dashboard_live.ex b/lib/cobblemon_ui_web/live/dashboard_live.ex index 519f76d..964535c 100644 --- a/lib/cobblemon_ui_web/live/dashboard_live.ex +++ b/lib/cobblemon_ui_web/live/dashboard_live.ex @@ -117,13 +117,21 @@ defmodule CobblemonUiWeb.DashboardLive do

Player Data Explorer

- +
+ <.link + navigate={~p"/battles"} + class="btn btn-ghost btn-sm gap-2 hover:bg-base-300/50 transition-colors" + > + <.icon name="hero-bolt" class="size-4 text-error" /> Battles + + +
diff --git a/lib/cobblemon_ui_web/router.ex b/lib/cobblemon_ui_web/router.ex index 46fd757..aee3517 100644 --- a/lib/cobblemon_ui_web/router.ex +++ b/lib/cobblemon_ui_web/router.ex @@ -19,6 +19,7 @@ defmodule CobblemonUiWeb.Router do live "/", DashboardLive live "/player/:uuid", DashboardLive + live "/battles", BattlesLive end # Other scopes may use custom stacks.